ret2dl-resolve
在学习这部分之前,最好有带调试符号的libc和ld,
也就是说自己编译一个待调试符号的glibc
然后编译链接程序时指定使用我们的glibc,不用系统自带那个
或者用patchelf把程序链接的glibc调包
然而调试版和发行版调用的函数好像不太一样
延迟绑定
假设有程序
1 | //test.c |
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 | pwndbg> elfheader |
PLT的作用
对每一个glibc中的函数func,都会有一个plt表项
1 | jmp * func@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 | .got.plt:0804A000 14 9F 04 08 _GLOBAL_OFFSET_TABLE_ dd offset _DYNAMIC |
.GOT.PLT[0]=.dynamic
.GOT.PLT[0]存放.dynamic节的地址,在节头表中可以查看
1 | root@Destroyer:/usr/src/CTF-All-In-One/src/writeup/6.1.3_pwn_xdctf2015_pwn200# readelf -S test |
.dynamic节在0x08049f14,因此GOT[0]=0x08049f14
这个节的作用是什么呢?
可以用readelf -d查看节内容
1 | root@Destroyer:/usr/src/CTF-All-In-One/src/writeup/6.1.3_pwn_xdctf2015_pwn200 |
每一项都是一个Elf32_Dyn
1 | typedef struct { |
这是一个键值对,键是d_tag,值要么是d_val要么是d_ptr
d_tag是一些枚举值
1 |
这个节指示了很多信息,比如init函数和init_array的地址
1 | 0x0000000c (INIT) 0x80482ac |
比如符号表和字符串表地址
1 | 0x00000005 (STRTAB) 0x804821c |
比如重定位信息
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
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 _initreadelf -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]=link_map @ ld
.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 | //____libc_start_main |
符号又到底是如何解析并且回填到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 | 0x80483a0 <write@plt>: jmp DWORD PTR ds:0x80498d4 |
然后跳转到plt[0]=0x8048350
,这是_dl_runtime_resolve
的导火索,在这里首先将link_map *l
压入栈中,然后跳转_dl_runtime_resolve
1 | ► 0x8048350 push dword ptr [0x80498bc] |
在_dl_runtime_resolve
中,这两个参数又改用eax(link_map *l)
和edx(reloc_arg)
传递,稍微违背了x86调用约定,但是问题不大
1 | _dl_runtime_resolve: |
_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 | _dl_fixup(struct link_map *l,ElfW(Word) reloc_arg) |
在图上意思意思
下面详细说明每一步
1.由主模块link_map找dynamic节
这个link_map
中有一个成员叫l_info
1 |
|
l_info
的类型是ElfW(Dyn)**
,也就是Elf32_Dyn**
,这是一个二级指针,或者说数组指针
意思是在内存某个地方有一个dynamic
数组,然后这个指针指向数组的基地址
在运行时l_info
指向所在模块的的dynamic
节
dynamic
节加载进入内存的地址是确定的,比如本程序中在0x080497c4
,可以使用readelf -S
查看
1 | ┌──(root㉿Executor)-[/home/dustball/ctf-challenges/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/32/no-relro] |
在GOT.PLT
表的最头部,也保存着一个_DYNAMIC
的指针
1 | GOT.PLT[0] => _DYNAMIC |
dynamic节中的内容可以用readelf -d
查看
1 | ┌──(root㉿Executor)-[/home/dustball/ctf-challenges/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/32/no-relro] |
每一项都是一个Elf32_Dyn
1 | LOAD:080497C4 _DYNAMIC Elf32_Dyn <1, <1>> ; DATA XREF: LOAD:080480BC↑o |
1 | pwndbg> ptype Elf32_Dyn |
2.由dynamic节找其他各节
1 | symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]); |
这里有四个节,实际上每个节都是表
表名 | 元素类型 | 作用 |
---|---|---|
symtab符号表 | struct Elf32_Sym | 保存符号名在strtab中的偏移, 保存符号在模块中的相对地址 ... |
strtab字符串表 | char | 保存本模块中所有需要动态链接的符号名 |
pltgot过程链接表 | 实际上_dl_fixup 中没有用到 |
|
jmprel重定位表 | struct Elf32_Rel | 保存符号的虚拟地址, 保存符号在符号表中的偏移 |
symtab符号表
1 | typedef struct |
jmprel重定位表
1 | typedef struct |
3.在本模块符号表中找到对应表项
1 | reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset (pltgot, reloc_arg));//在重定位表中的表项 |
下面到4之前是一些检查,忽略
4.解析符号
1 | struct link_map* result; |
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 | call _dl_fixup # Call resolver. |
注意到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
函数解析符号的过程
1 | Algorithm _dl_fixup |
no_relro
strtab节通常和text节加载到同一个只读段,因此在strtab上篡改函数名字符串是不可能的
在no_relro保护下,dynamic节可写, 可以篡改dynamic.strtab指针指向fake strtab
partial_relro
在no_relro保护中,可以通过篡改dynamic节中的指针指向假的strtab伪造假的函数名
但是partial_relro保护使得dynamic节只读,无法篡改其中的字符串表指针
stage1
由于我们需要构造“/bin/sh”这种字符串,要么调用read函数往内存里写,要么溢出时写进去
前者需要再构造read调用的rop链,并且还得给字符串找地方,找一个我们知道地址并且可写的地方,比如bss段
后者由于栈地址不知道在哪,需要做一个栈迁移,首先把栈搬到bss段上
后者更加方便,采取后者
stage2
在本阶段我们构造rop链条,手动调用
1 | dl_runtime_resolve@.GOT.PLT[0]( |
也就是再解析调用一下write函数,目的是验证一下,已经解析过的符号,使用rop方法能够再次触发解析过程,并且该过程是正确的
stage3
在本阶段我们在bss段伪造一个重定位表项, 但是该表项的内容指向正确的symtab表
为了使用这个假重定位表项,我们将dl_runtime_resolve的参数reloc_arg改成,该bss段假表项与真的重定位表的偏移量
该偏移量显然会大的离谱,远远超出重定位表的范围,因为bss和relplt段相距甚远
此举目的是验证即使传递的reloc_arg超过重定位表范围,也没有任何安全检查阻拦,使得我们能够正确解析到符号
stage4
在本阶段我们既要构造假的重定位表项,又要构造假的符号表项
此时假重定位表项不再指向正确的符号,而是指向我们构造的符号
但是这个假符号依然索引正确的符号名称
显然此时reloc_arg索引重定位表的偏移量远超重定位表范围,并且假重定位项索引假符号的偏移量也远超了符号表范围
此举目的是验证,即使符号表的索引越界,也没有任何安全检查阻拦,使得我们能够正确解析到符号
想法很好,然而在dl_runtime_resolve中,r_info不只会被用来索引符号表,还会索引versym符号版本表
1 | if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL){ |
我们依据假符号与符号表的偏移量,计算出r_info,这保证了假重定位项可以索引假符号
但是不能保证r_info索引versym表的什么地方
实际运行时ndx=0x442c
&l->l_versions=0xf7f5a710
然后versions表里面一项是0x10字节
所以version = &l->l_versions[ndx]=0xf7f9e9d4;
下一条指令就要解引用了version->hash
然而0xf7f9e9d4上并没有在任何一个内存映射区,是一个非法地址
1 | 0xf7f95000 0xf7f96000 rw-p 1000 32000 /usr/lib/i386-linux-gnu/ld-linux.so.2 |
因此对非法地址解引用就段错误了
怎么修复这个过程呢?
1 | ElfW(Half) ndx = vernum[ELFW(R_SYM)(reloc->r_info)] & 0x7fff; |
假重定位项的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 | .eh_frame:080487A8 2C db 2Ch ; , |
0x080487C2=0x80482d8+index*2
那么index=0x275
那么r_info就得是0x27507
注意如果只修改假的重定位项,令其r_info=0x27507,这样就又不能正确索引到假的符号表项了
按下葫芦浮起瓢,因此还需要修正bss段伪造的假符号位置,在原位置基础上加一个(0x275-0x268)*16
即可
乘16的原因是,符号表项一个占用16字节
stage5
在本阶段,伪造假符号名字符串,并令假符号的st_name指向它,目的是证明即使st_name远超strtab范围,依然没有任何安全检查阻拦
stage6
把stage5中的假符号名字符串改成“system”,并把write的参数(1,“/bin/sh”,“7”)改成system的参数(“/bin/sh”)
在目标模块中阴暗地爬行
分析了_dl_fixup
的源码之后,已经能够理解ret2dl-resolve的原理了
下面的问题是,_dl_fixup
中调用的_dl_lookup_symbol_x
函数,是如何查找符号的呢?
可想而知的是,glibc
中的符号成百上千,如果纯纯使用符号名字符串,进行模式匹配,那可真是慢了去了
到底怎么在目标模块中解析符号的呢?
GNU Hash ELF Sections (oracle.com)
[翻译]GNU Hash ELF Sections-外文翻译-看雪-安全社区|安全招聘|kanxue.com
符号解析中的哈希算法
1 | dl_runtime_resolve |