dustland

dustball in dustland

ret2dl-resolve

ret2dl-resolve

在学习这部分之前,最好有带调试符号的libc和ld,

也就是说自己编译一个待调试符号的glibc

然后编译链接程序时指定使用我们的glibc,不用系统自带那个

或者用patchelf把程序链接的glibc调包

然而调试版和发行版调用的函数好像不太一样

延迟绑定

假设有程序

1
2
3
4
5
6
//test.c
#include <stdio.h>
int main(){
write(1, "Hello, world!\n", 14);
return 0;
}
1
gcc test.c -O0 -g -no-pie -m32 -o test

write函数位于libc.so中,main程序是如何与write符号链接的呢?

这个过程叫做延迟绑定,又叫做懒加载,意思是我们的程序第一次调用动态库中的符号时,动态连接器ld才会解析write函数

这个解析过程如下

第一次解析

经过第一次解析,write@got被填充了正确的write地址,此后的write调用将如图所示

此后的调用

这里面提到了两个表,PLT表,GOT表

如果gdb加了pwndbg插件,可以使用plt和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
pwndbg> elfheader
0x8048194 - 0x80481b4 .interp
0x80481b4 - 0x80481d8 .note.gnu.build-id
0x80481d8 - 0x80481f8 .note.ABI-tag
0x80481f8 - 0x8048218 .gnu.hash
0x8048218 - 0x8048268 .dynsym
0x8048268 - 0x80482d1 .dynstr
0x80482d2 - 0x80482dc .gnu.version
0x80482dc - 0x804830c .gnu.version_r
0x804830c - 0x8048314 .rel.dyn
0x8048314 - 0x8048324 .rel.plt
0x8049000 - 0x8049020 .init
0x8049020 - 0x8049050 .plt
0x8049050 - 0x80491a6 .text
0x80491a8 - 0x80491bc .fini
0x804a000 - 0x804a013 .rodata
0x804a014 - 0x804a048 .eh_frame_hdr
0x804a048 - 0x804a110 .eh_frame
0x804bef8 - 0x804befc .init_array
0x804befc - 0x804bf00 .fini_array
0x804bf00 - 0x804bff0 .dynamic
0x804bff0 - 0x804bff4 .got
0x804bff4 - 0x804c008 .got.plt
0x804c008 - 0x804c010 .data
0x804c010 - 0x804c014 .bss

pwndbg> plt
0x8049030: __libc_start_main@plt
0x8049040: printf@plt
pwndbg> got

GOT protection: Partial RELRO | GOT functions: 2

[0x804c000] __libc_start_main@GLIBC_2.34 -> 0xf7cf8cf0 (__libc_start_main_impl) ◂— push ebp
[0x804c004] printf@GLIBC_2.0 -> 0x8049046 (printf@plt+6) ◂— push 8

PLT的作用

对每一个glibc中的函数func,都会有一个plt表项

1
2
3
jmp * func@got
push index
jmp * _dl_runtime_resolve@got

如果got中填写了正确的函数地址,则会直接调用该函数

如果got中填写了push index的地址,则调用_dl_runtime_resolve解析符号

解析符号的依据就是这个index,导入函数下标

.plt(procedure Linkage Table,过程链接表)

.plt.got专门用于存放__cxa_finalize函数的plt条目

GOT的作用

存放函数地址

如果尚未解析则存放对应函数plt中的下一条指令地址

GOT表分成两部分

.got和.got.plt

.got(Global Offset Table),全局变量地址表

.got.plt是全局函数地址表

前面我们所说的write@got实际上是write@got.plt

.got表纯纯存放全局变量地址,有一个算一个,没有特别之处

.got.plt的前三个表项存放了特殊地址,其后的表项就是全局函数地址了

1
2
3
4
5
6
7
8
9
.got.plt:0804A000 14 9F 04 08                   _GLOBAL_OFFSET_TABLE_ dd offset _DYNAMIC
.got.plt:0804A000 ; DATA XREF: _init_proc+9↑o
.got.plt:0804A000 ; _start+10↑o ...
.got.plt:0804A004 00 00 00 00 dword_804A004 dd 0 ; DATA XREF: sub_80482D0↑r
.got.plt:0804A008 00 00 00 00 dword_804A008 dd 0 ; DATA XREF: sub_80482D0+6↑r
.got.plt:0804A00C 24 A0 04 08 off_804A00C dd offset __libc_start_main
.got.plt:0804A00C ; DATA XREF: ___libc_start_main↑r
.got.plt:0804A010 28 A0 04 08 off_804A010 dd offset write ; DATA XREF: _write↑r
.got.plt:0804A010 _got_plt ends

.GOT.PLT[0]=.dynamic

.GOT.PLT[0]存放.dynamic节的地址,在节头表中可以查看

1
2
3
4
5
6
7
8
9
10
11
12
13
root@Destroyer:/usr/src/CTF-All-In-One/src/writeup/6.1.3_pwn_xdctf2015_pwn200# readelf -S test
There are 35 section headers, starting at offset 0x1fbc:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
...
[21] .dynamic DYNAMIC 08049f14 000f14 0000e8 08 WA 6 0 4
...
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specific)

.dynamic节在0x08049f14,因此GOT[0]=0x08049f14

这个节的作用是什么呢?

可以用readelf -d查看节内容

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
root@Destroyer:/usr/src/CTF-All-In-One/src/writeup/6.1.3_pwn_xdctf2015_pwn200# readelf -d test

Dynamic section at offset 0xf14 contains 24 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000c (INIT) 0x80482ac
0x0000000d (FINI) 0x80484d4
0x00000019 (INIT_ARRAY) 0x8049f0c
0x0000001b (INIT_ARRAYSZ) 4 (bytes)
0x0000001a (FINI_ARRAY) 0x8049f10
0x0000001c (FINI_ARRAYSZ) 4 (bytes)
0x6ffffef5 (GNU_HASH) 0x80481ac
0x00000005 (STRTAB) 0x804821c
0x00000006 (SYMTAB) 0x80481cc
0x0000000a (STRSZ) 75 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x804a000
0x00000002 (PLTRELSZ) 16 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x804829c
0x00000011 (REL) 0x8048294
0x00000012 (RELSZ) 8 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x6ffffffe (VERNEED) 0x8048274
0x6fffffff (VERNEEDNUM) 1
0x6ffffff0 (VERSYM) 0x8048268
0x00000000 (NULL) 0x0

每一项都是一个Elf32_Dyn

1
2
3
4
5
6
7
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;

这是一个键值对,键是d_tag,值要么是d_val要么是d_ptr

d_tag是一些枚举值

1
2
3
4
5
6
7
8
9
10
11
12
13
#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_STRSZ 10 /* Size in bytes 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 */

这个节指示了很多信息,比如init函数和init_array的地址

1
2
0x0000000c (INIT)                       0x80482ac
0x00000019 (INIT_ARRAY) 0x8049f0c

比如符号表和字符串表地址

1
2
0x00000005 (STRTAB)                     0x804821c
0x00000006 (SYMTAB) 0x80481cc

比如重定位信息

1
0x00000011 (REL)                        0x8048294

这个表有何作用呢?

一是指导动态链接器进行

​ 加载so

​ 解析符号

​ 重定位

​ 调用初始化函数

二是运行时

​ 延迟绑定

​ 处理dlopen显示加载的函数

关于重定位

1
2
3
4
5
6
7
8
9
10
root@Destroyer:/usr/src/CTF-All-In-One/src/writeup/6.1.3_pwn_xdctf2015_pwn200# readelf -r test

Relocation section '.rel.dyn' at offset 0x294 contains 1 entry:
Offset Info Type Sym.Value Sym. Name
08049ffc 00000106 R_386_GLOB_DAT 00000000 __gmon_start__

Relocation section '.rel.plt' at offset 0x29c contains 2 entries:
Offset Info Type Sym.Value Sym. Name
0804a00c 00000207 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
0804a010 00000307 R_386_JUMP_SLOT 00000000 write@GLIBC_2.0

每一个重定位表项都对应一个Elf32_Rel

1
2
3
4
5
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;

r_offset是符号got表项的虚拟地址

r_info是符号的重定位类型和符号下标

​ 重定位类型最常见全局变量的R_386_GLOB_DAT和全局函数的R_386_JUMP_SLOT

​ 符号下标索引.dynsym节

比如write的索引是3,

libc_start_main的索引是2

__gmon_start__的索引是1

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
root@Destroyer:/usr/src/CTF-All-In-One/src/writeup/6.1.3_pwn_xdctf2015_pwn200# readelf -s test

Symbol table '.dynsym' contains 5 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
2: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0 (2)
3: 00000000 0 FUNC GLOBAL DEFAULT UND write@GLIBC_2.0 (2)
4: 080484ec 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used

Symbol table '.symtab' contains 70 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
...
32: 00000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
33: 08048370 0 FUNC LOCAL DEFAULT 14 deregister_tm_clones
34: 080483b0 0 FUNC LOCAL DEFAULT 14 register_tm_clones
35: 080483f0 0 FUNC LOCAL DEFAULT 14 __do_global_dtors_aux
36: 0804a01c 1 OBJECT LOCAL DEFAULT 25 completed.7283
37: 08049f10 0 OBJECT LOCAL DEFAULT 20 __do_global_dtors_aux_fin
38: 08048420 0 FUNC LOCAL DEFAULT 14 frame_dummy
39: 08049f0c 0 OBJECT LOCAL DEFAULT 19 __frame_dummy_init_array_
40: 00000000 0 FILE LOCAL DEFAULT ABS test.c
...
66: 08048426 64 FUNC GLOBAL DEFAULT 14 main
67: 08048466 0 FUNC GLOBAL HIDDEN 14 __x86.get_pc_thunk.ax
68: 0804a01c 0 OBJECT GLOBAL HIDDEN 24 __TMC_END__
69: 080482ac 0 FUNC GLOBAL DEFAULT 11 _init

readelf -s会读取两个表,一个是本地符号表symtab,一个是链接符号表.dynsym

这个symtab可以strip掉,不影响程序执行

而实际上这两个符号表的表项中并没有Name数组,st_name是一个索引,索引字符串表.strtab

1
2
3
4
5
6
7
8
9
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;
1
2
3
4
1C7Ch: 00 63 72 74 73 74 75 66 66 2E 63 00 64 65 72 65  .crtstuff.c.dere 
1C8Ch: 67 69 73 74 65 72 5F 74 6D 5F 63 6C 6F 6E 65 73 gister_tm_clones
1C9Ch: 00 5F 5F 64 6F 5F 67 6C 6F 62 61 6C 5F 64 74 6F .__do_global_dto
...

比如crtstuff.c的符号表项Elf32_Sym中,st_name=1,对应0x1c7c+1

比如deregister_tm_clones的符号表项Elf32_Sym中,st_name=c,对应0x1c7c+c

这里0x1c7c是.strtab节在文件中的偏移量,可以使用readelf -S查看对应节区

.GOT.PLT[1]和.GOT.PLT[2]在编译链接时无法决定是啥,只有在运行时才会知道是什么

.GOT.PLT[1]用于存放主模块的link_map数据结构的地址

每个模块(主模块以及所有加载的动态库)都各自有一个link_map

这个link_map保存了对应模块的诸多信息,比如各个节区的地址,elf头的地址,模块名等

.GOT.PLT[2]=dl_runtime_resolve @ ld

存放dl_runtime_resolve的地址

.GOT.PLT[3+]

第四项及之后,该表用于保存函数的虚拟地址

疑问

每个函数的plt表都有三项

jmp

push

jmp

这里第一个jmp调试观察是到push这行

第二个jmp是到dl_runtime_resolve函数中

那么中间push了什么呢?

1
2
3
4
5
6
7
8
9
//____libc_start_main
.plt:080482E0 FF 25 0C A0 04 08 jmp ds:off_804A00C
.plt:080482E6 68 00 00 00 00 push 0
.plt:080482EB E9 E0 FF FF FF jmp sub_80482D0

//write
.plt:080482F0 FF 25 10 A0 04 08 jmp ds:off_804A010
.plt:080482F6 68 08 00 00 00 push 8
.plt:080482FB E9 D0 FF FF FF jmp sub_80482D0

符号又到底是如何解析并且回填到GOT表的呢?

为了解决这些问题,以及学习ret2dlresolve的原理,

下面我们阅读_dl_fixup的源码寻找答案

_dl_fixup

以write为例,观察该符号是如何解析的

解析符号发生在_dl_fixup函数中

1
_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)

该函数有两个参数,其中link_map *l参数就是.got.plt[1],

参数reloc_arg是目标函数在.rel.plt中的偏移量

reloc_arg在write@plt中被压入栈中

1
2
3
0x80483a0 <write@plt>:       jmp    DWORD PTR ds:0x80498d4
0x80483a6 <write@plt+6>: push 0x20
0x80483ab <write@plt+11>: jmp 0x8048350

然后跳转到plt[0]=0x8048350,这是_dl_runtime_resolve的导火索,在这里首先将link_map *l压入栈中,然后跳转_dl_runtime_resolve

1
2
0x8048350                              push   dword ptr [0x80498bc]
0x8048356 jmp dword ptr [0x80498c0] <_dl_runtime_resolve>

_dl_runtime_resolve中,这两个参数又改用eax(link_map *l)edx(reloc_arg)传递,稍微违背了x86调用约定,但是问题不大

1
2
3
4
5
6
7
8
9
10
11
12
_dl_runtime_resolve:
cfi_adjust_cfa_offset (8)
_CET_ENDBR
pushl %eax # Preserve registers otherwise clobbered.
cfi_adjust_cfa_offset (4)
pushl %ecx
cfi_adjust_cfa_offset (4)
pushl %edx
cfi_adjust_cfa_offset (4)
movl 16(%esp), %edx # Copy args pushed by PLT in register. Note
movl 12(%esp), %eax # that `fixup' takes its parameters in regs.
call _dl_fixup # Call resolver.

_dl_runtime_resolve实际上只是一个包装函数,实际工作是_dl_fixup完成的

_dl_fixup干了啥呢?

_dl_fixup知道两件事,

一个是主程序模块的link_map,这玩意儿保存了很多信息,包括它属于哪个模块,该模块的elf信息等等

一个进程所有模块的link_map以双向链表连接

每个模块(主程序和每个so库)各自有一个link_map

1
2
3
4
5
6
7
8
9
10
pwndbg> p/s l.l_name
$8 = 0xf7ffdd2c ""
pwndbg> p/s l.l_next.l_name
$9 = 0xf7fc828c "linux-gate.so.1"
pwndbg> p/s l.l_next.l_next.l_name
$10 = 0xf7fc2390 "/home/glibc32/lib/libc.so.6"
pwndbg> p/s l.l_next.l_next.l_next.l_name
$11 = 0x8046174 "/home/glibc32/lib/ld-linux.so.2"
pwndbg> p/s l.l_next.l_next.l_next.l_next.l_name
Cannot access memory at address 0x4

另一个就是reloc_arg,也就是他要解析的函数符号,在.rel.plt节区中的偏移

显然抛开基地址谈偏移量是没有意义的,因此下面要做的第一件事是找到.rel.plt的基地址

怎么找呢?

大体步骤如下伪代码

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
_dl_fixup(struct link_map *l,ElfW(Word) reloc_arg)
{
// 1.找到dynamic节,1->1l_info就是dynamic节,
//从dynamic节中找到.rel.plt节记为reloc
//从dynamic节中找到符号表symtab,
//从dynamic节中找到字符串表strtab


// 2.使用reloc_arg配合reloc表找到该表中的具体项目Elf32_Rel
// typedef struct{
// Elf32_Addr r_offset; //符号要填充的got表项的虚拟地址
// Elf32_Word r_info; //符号在符号表中的下标
// } Elf32_Rel;
const PLTREL *const reloc = (const void *)(D_PTR(l, l_info[DT_JMPREL]) + reloc_offset);


// 3.找到符号表对应表项
// typedef struct {
// Elf32_Word st_name; //符号名字符串在字符串表中的偏移
// Elf32_Addr st_value; //符号在其所在模块中的偏移量
// Elf32_Word st_size;
// unsigned char st_info;
// unsigned char st_other;
// Elf32_Half st_shndx;
// } Elf32_Sym;
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];

// 4.解析符号
//从strtab偏移Elf32_Sym.st_name处找到符号名,
//从链表相接的各个模块的link_map入手,遍历各个模块,寻找该符号名,如果找到,result返回对应模块的link_map
//同时sym废物利用,从对应模块的符号表中抄了同名的符号过来,但是这个符号是有虚拟地址的
result = _dl_lookup_symbol_x (strtab + sym ->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);
// result中保存着目标模块的基地址,加上目标符号的偏移量,得到该符号纯纯的虚拟地址,放到value里
// 这里DL_FIXUP_MAKE_VALUE(map,addr) = addr,纯纯弱智
value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS(result) + sym->st_value) : 0);


// 5.回写GOT表
//最后把value写入相应的GOT表条目中,rel_addr就是GOT地址
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}

在图上意思意思

image-20240819193758449

下面详细说明每一步

这个link_map中有一个成员叫l_info

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

struct link_map
{
...
/* Indexed pointers to dynamic section. dynamic节的索引指针
[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];
...

l_info的类型是ElfW(Dyn)**,也就是Elf32_Dyn**,这是一个二级指针,或者说数组指针

意思是在内存某个地方有一个dynamic数组,然后这个指针指向数组的基地址

在运行时l_info指向所在模块的的dynamic

dynamic节加载进入内存的地址是确定的,比如本程序中在0x080497c4,可以使用readelf -S查看

1
2
3
4
5
6
7
8
9
┌──(root㉿Executor)-[/home/dustball/ctf-challenges/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/32/no-relro]
└─# readelf -S main_no_relro_32
There are 30 section headers, starting at offset 0x10b0:

Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
...
[21] .dynamic DYNAMIC 080497c4 0007c4 0000e8 08 WA 6 0 4
...

GOT.PLT表的最头部,也保存着一个_DYNAMIC的指针

1
2
3
4
GOT.PLT[0] => _DYNAMIC
GOT.PLT[1] => link_map
GOT.PLT[2] => dl_runtime_resolve
...

dynamic节中的内容可以用readelf -d查看

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
┌──(root㉿Executor)-[/home/dustball/ctf-challenges/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/32/no-relro]
└─# readelf -d main_no_relro_32

Dynamic section at offset 0x7c4 contains 24 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000c (INIT) 0x804832c
0x0000000d (FINI) 0x8048634
0x00000019 (INIT_ARRAY) 0x80497bc
0x0000001b (INIT_ARRAYSZ) 4 (bytes)
0x0000001a (FINI_ARRAY) 0x80497c0
0x0000001c (FINI_ARRAYSZ) 4 (bytes)
0x6ffffef5 (GNU_HASH) 0x804818c
0x00000005 (STRTAB) 0x804824c
0x00000006 (SYMTAB) 0x80481ac
0x0000000a (STRSZ) 107 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x80498b8
0x00000002 (PLTRELSZ) 40 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x8048304
0x00000011 (REL) 0x80482ec
0x00000012 (RELSZ) 24 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x6ffffffe (VERNEED) 0x80482cc
0x6fffffff (VERNEEDNUM) 1
0x6ffffff0 (VERSYM) 0x80482b8
0x00000000 (NULL) 0x0

每一项都是一个Elf32_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
LOAD:080497C4 _DYNAMIC        Elf32_Dyn <1, <1>>      ; DATA XREF: LOAD:080480BC↑o
LOAD:080497C4 ; .got.plt:_GLOBAL_OFFSET_TABLE_↓o
LOAD:080497C4 ; DT_NEEDED libc.so.6
LOAD:080497CC Elf32_Dyn <0Ch, <804832Ch>> ; DT_INIT
LOAD:080497D4 Elf32_Dyn <0Dh, <8048634h>> ; DT_FINI
LOAD:080497DC Elf32_Dyn <19h, <80497BCh>> ; DT_INIT_ARRAY
LOAD:080497E4 Elf32_Dyn <1Bh, <4>> ; DT_INIT_ARRAYSZ
LOAD:080497EC Elf32_Dyn <1Ah, <80497C0h>> ; DT_FINI_ARRAY
LOAD:080497F4 Elf32_Dyn <1Ch, <4>> ; DT_FINI_ARRAYSZ
LOAD:080497FC Elf32_Dyn <6FFFFEF5h, <804818Ch>> ; DT_GNU_HASH
LOAD:08049804 Elf32_Dyn <5, <804824Ch>> ; DT_STRTAB
LOAD:0804980C Elf32_Dyn <6, <80481ACh>> ; DT_SYMTAB
LOAD:08049814 Elf32_Dyn <0Ah, <6Bh>> ; DT_STRSZ
LOAD:0804981C Elf32_Dyn <0Bh, <10h>> ; DT_SYMENT
LOAD:08049824 Elf32_Dyn <15h, <0>> ; DT_DEBUG
LOAD:0804982C Elf32_Dyn <3, <80498B8h>> ; DT_PLTGOT
LOAD:08049834 Elf32_Dyn <2, <28h>> ; DT_PLTRELSZ
LOAD:0804983C Elf32_Dyn <14h, <11h>> ; DT_PLTREL
LOAD:08049844 Elf32_Dyn <17h, <8048304h>> ; DT_JMPREL
LOAD:0804984C Elf32_Dyn <11h, <80482ECh>> ; DT_REL
LOAD:08049854 Elf32_Dyn <12h, <18h>> ; DT_RELSZ
LOAD:0804985C Elf32_Dyn <13h, <8>> ; DT_RELENT
LOAD:08049864 Elf32_Dyn <6FFFFFFEh, <80482CCh>> ; DT_VERNEED
LOAD:0804986C Elf32_Dyn <6FFFFFFFh, <1>> ; DT_VERNEEDNUM
LOAD:08049874 Elf32_Dyn <6FFFFFF0h, <80482B8h>> ; DT_VERSYM
LOAD:0804987C Elf32_Dyn <0> ; DT_NULL
1
2
3
4
5
6
7
8
pwndbg> ptype Elf32_Dyn
type = struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
}

2.由dynamic节找其他各节

1
2
3
4
symtab 	= (const void *) D_PTR (l, l_info[DT_SYMTAB]);
strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
pltgot = (uintptr_t) D_PTR (l, l_info[DT_PLTGOT]); //实际上_dl_fixup中没有用到
reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset (pltgot, reloc_arg));

这里有四个节,实际上每个节都是表

表名 元素类型 作用
symtab符号表 struct Elf32_Sym 保存符号名在strtab中的偏移,
保存符号在模块中的相对地址
...
strtab字符串表 char 保存本模块中所有需要动态链接的符号名
pltgot过程链接表 实际上_dl_fixup中没有用到
jmprel重定位表 struct Elf32_Rel 保存符号的虚拟地址,
保存符号在符号表中的偏移

symtab符号表

1
2
3
4
5
6
7
8
9
10
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) *///符号名在strtab中的偏移
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;

jmprel重定位表

1
2
3
4
5
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;

3.在本模块符号表中找到对应表项

1
2
3
4
reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset (pltgot, reloc_arg));//在重定位表中的表项
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); //GOT表项地址,为后来回填做准备

下面到4之前是一些检查,忽略

4.解析符号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 	struct link_map* result;
Elf32_Addr value;

result = _dl_lookup_symbol_x (
strtab + sym->st_name, //符号名字符串
l, //本模块的link_map
&sym, //返回值,如果在其他模块找到该符号则返回其符号表项
l->l_scope,
version,
ELF_RTYPE_CLASS_PLT,
flags,
NULL
);//返回值result是找到符号实现所在模块的link_map

value = DL_FIXUP_MAKE_VALUE (
result,
SYMBOL_ADDRESS (result, sym, false)//从link_map *result中提取目标模块基地址,加上sym.st_value偏移量得到符号虚拟地址
);

5.回填GOT表项

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

这里有一个压行,干了两个事情,

一是调用elf_machine_fixup_plt把value回写到rel_addr上

二是把value值,也就是已经解析出来的符号地址,放到eax寄存器上返回

注意此时是在dl_fixup中返回到dl_runtime_resolve中

下面的指令是这样的:

1
2
3
4
5
6
call _dl_fixup		# Call resolver.
popl %edx # Get register content back.
movl (%esp), %ecx
movl %eax, (%esp) # Store the function address.
movl 4(%esp), %eax
ret $12 # Jump to function address.

注意到dl_runtime_resolve返回之前,栈顶是刚刚放入的eax,也就是刚解析出来的符号值

也就是直接 ret2目标函数 了

ret2dl_resolve

能不能进行这种利用,得看RELRO✌的脸色

RELRO保护:

read only relocation,只读重定位

鉴于攻击者可以篡改GOT表,填充危险函数,因此如果GOT表是只读的,攻击者就没法写了

RELRO的目的是保护函数指针,防止篡改

保护程度 效果 编译选项
NO_RELRO dynamic段可写
GOT表可写,允许延迟绑定
-z norelro
PARTIAL_RELRO dynamic段只读,
但是GOT表还是可写的,允许延迟绑定
-z lazy
FULL_RELRO dynamic段只读
GOT表只读,不允许延迟绑定,所有符号必须在加载程序时立刻解析
-z now

回顾_dl_fixup函数解析符号的过程

image-20240819193758449
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Algorithm _dl_fixup
Input: a Link_Map linkmap of the Main module, an index reloc_arg of the jmprel table
Output: virtual address of the target symbol

dynamic = linkmap.l_info
jmprel = dynamic[DT_JMPREL]
strtab = dynamic[DT_STRTAB]
symtab = dynamic[DT_SYMTAB]

reloc = jmprel[reloc_arg]
sym = symtab[reloc.r_info]
str = strtab[sym.st_name]

//other_linkmap是其他模块的Link_Map结构
//_dl_lookup_symbol_x(str)从其他模块中寻找str符号,如果找到则返回该符号与其所在的link_map
[sym,other_linkmap] = _dl_lookup_symbol_x(str)

//link_map中保存着目标模块的基地址,sym中保存着符号相对目标模块的偏移量,加起来得到符号的虚拟地址
vaddr = other_linkmap.laddr + sym.st_info

no_relro

strtab节通常和text节加载到同一个只读段,因此在strtab上篡改函数名字符串是不可能的

在no_relro保护下,dynamic节可写, 可以篡改dynamic.strtab指针指向fake strtab

image-20240819193931068

partial_relro

在no_relro保护中,可以通过篡改dynamic节中的指针指向假的strtab伪造假的函数名

但是partial_relro保护使得dynamic节只读,无法篡改其中的字符串表指针

stage1

由于我们需要构造“/bin/sh”这种字符串,要么调用read函数往内存里写,要么溢出时写进去

前者需要再构造read调用的rop链,并且还得给字符串找地方,找一个我们知道地址并且可写的地方,比如bss段

后者由于栈地址不知道在哪,需要做一个栈迁移,首先把栈搬到bss段上

后者更加方便,采取后者

stage2

在本阶段我们构造rop链条,手动调用

1
2
3
4
dl_runtime_resolve@.GOT.PLT[0](
link_map=.GOT.PLT[1]
reloc_arg=0x20
)

也就是再解析调用一下write函数,目的是验证一下,已经解析过的符号,使用rop方法能够再次触发解析过程,并且该过程是正确的

stage3

在本阶段我们在bss段伪造一个重定位表项, 但是该表项的内容指向正确的symtab表

为了使用这个假重定位表项,我们将dl_runtime_resolve的参数reloc_arg改成,该bss段假表项与真的重定位表的偏移量

该偏移量显然会大的离谱,远远超出重定位表的范围,因为bss和relplt段相距甚远

此举目的是验证即使传递的reloc_arg超过重定位表范围,也没有任何安全检查阻拦,使得我们能够正确解析到符号

image-20240819195308717

stage4

在本阶段我们既要构造假的重定位表项,又要构造假的符号表项

此时假重定位表项不再指向正确的符号,而是指向我们构造的符号

但是这个假符号依然索引正确的符号名称

显然此时reloc_arg索引重定位表的偏移量远超重定位表范围,并且假重定位项索引假符号的偏移量也远超了符号表范围

此举目的是验证,即使符号表的索引越界,也没有任何安全检查阻拦,使得我们能够正确解析到符号

image-20240819193641215

想法很好,然而在dl_runtime_resolve中,r_info不只会被用来索引符号表,还会索引versym符号版本表

1
2
3
4
5
6
7
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;
}
image-20240819213749275

我们依据假符号与符号表的偏移量,计算出r_info,这保证了假重定位项可以索引假符号

但是不能保证r_info索引versym表的什么地方

实际运行时ndx=0x442c

&l->l_versions=0xf7f5a710

然后versions表里面一项是0x10字节

所以version = &l->l_versions[ndx]=0xf7f9e9d4;

下一条指令就要解引用了version->hash

然而0xf7f9e9d4上并没有在任何一个内存映射区,是一个非法地址

1
2
0xf7f95000 0xf7f96000 rw-p     1000  32000 /usr/lib/i386-linux-gnu/ld-linux.so.2
0xffb01000 0xfff59000 rw-p 458000 0 [stack]

因此对非法地址解引用就段错误了

怎么修复这个过程呢?

1
2
3
4
ElfW(Half) ndx = vernum[ELFW(R_SYM)(reloc->r_info)] & 0x7fff;
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL;

假重定位项的r_info既索引假符号表项,又索引假versym表项

如果能控制假versym表项为空,则ndx就是0,此时l_versions[ndx]=l_versions[0]就一定是合法的了

也就是说,我们可以微操控制一下r_info的值

如何控制呢?

原本r_info=0x26807,其中的索引值是0x268

vernum基地址是0x80482d8

vernum[ELFW(R_SYM)(reloc->r_info)]这个假表项,在0x80482d8+0x268*2=0x080487A8上,使用ida观察这里是.eh_frame段

往下翻找一个全零的假表项位置比如0x080487C2就很好

1
2
3
4
.eh_frame:080487A8 2C                                            db  2Ch ; ,
...
.eh_frame:080487C2 00 db 0
.eh_frame:080487C3 00 db 0

0x080487C2=0x80482d8+index*2

那么index=0x275

那么r_info就得是0x27507

注意如果只修改假的重定位项,令其r_info=0x27507,这样就又不能正确索引到假的符号表项了

按下葫芦浮起瓢,因此还需要修正bss段伪造的假符号位置,在原位置基础上加一个(0x275-0x268)*16即可

乘16的原因是,符号表项一个占用16字节

stage5

在本阶段,伪造假符号名字符串,并令假符号的st_name指向它,目的是证明即使st_name远超strtab范围,依然没有任何安全检查阻拦

image-20240820103319870

stage6

把stage5中的假符号名字符串改成“system”,并把write的参数(1,“/bin/sh”,“7”)改成system的参数(“/bin/sh”)

image-20240820111602402

在目标模块中阴暗地爬行

分析了_dl_fixup的源码之后,已经能够理解ret2dl-resolve的原理了

下面的问题是,_dl_fixup中调用的_dl_lookup_symbol_x函数,是如何查找符号的呢?

可想而知的是,glibc中的符号成百上千,如果纯纯使用符号名字符串,进行模式匹配,那可真是慢了去了

到底怎么在目标模块中解析符号的呢?

GNU Hash ELF Sections (oracle.com)

[翻译]GNU Hash ELF Sections-外文翻译-看雪-安全社区|安全招聘|kanxue.com

符号解析中的哈希算法

1
2
3
4
5
6
dl_runtime_resolve
_dl_fixup
_dl_lookup_symbol_x
do_lookup_x
do_lookup_unique
enter_unique_sym