鹏城杯Reverse

鹏城杯Reverse

rota

查壳,发现为无壳64位exe文件,拖入IDA反编译。查看主函数,上来就给出了比较数据:

qmemcpy(v25, "ksPhS/34MXifj+Ibtjud2Tikj5HkA7iTpbaNELBebOaIm", sizeof(v25))

接着是一个很经典的变表base64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
do                                            // 变表base64
{
v6 = input1[v5];
v4 += 4;
v7 = input1[v5 + 1];
*(v4 - 4) = base64_table[(unsigned __int64)v6 >> 2];
v8 = v6;
v9 = (unsigned __int8)input1[v5 + 2];
v5 += 3i64;
*(v4 - 3) = base64_table[(16i64 * (v8 & 3)) | ((unsigned __int64)v7 >> 4)];
*(v4 - 2) = base64_table[(4i64 * (v7 & 0xF)) | ((unsigned __int64)v9 >> 6)];
*(v4 - 1) = base64_table[v9 & 0x3F];
}
while ( v5 < 30 );
v10 = input2;
v11 = v31;
*v4 = base64_table[(unsigned __int64)input2 >> 2];
v12 = base64_table[4 * (v11 & 0xF)];
v4[1] = base64_table[(16i64 * (v10 & 3)) | (v11 >> 4)];
v4[2] = v12;
v13 = dword_7FF76BE26018 < 2;
*(_WORD *)(v4 + 3) = '=';

他之所以把base64最后三个字节加密单拎出来写是因为输入只有32个字节,不足3的倍数,*(_WORD *)(v4 + 3) = '='补了’=’。

base64变表加密过后,传入了新的加密函数:

1
2
init((__int64)key);
crypto(key, (__int64)v32, (__int64)v33);

v32存储了加密结果v4的首地址,init函数对key进行了初始化。一并传入crypto函数。函数内部是循环加密,每个循环都由五组一样的操作组成,我们但拎其中一组来说:

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
  v3 = key_in[192];
v4 = (_BYTE *)(generat_text + 1);
v5 = key_in[193];
result = key_in[194];
v7 = crypto_text - generat_text;
v9 = 9i64;
do
{
v10 = 0;
while ( (unsigned __int8)v4[v7 - 1] != base64_table[v10] )// 寻找下标
{
if ( ++v10 >= 64 )
{
LOBYTE(v10) = 0;
break;
}
}
v11 = v5;
v12 = (v5 + key_in[(v3 + (_BYTE)v10) & 0x3F]) & 0x3F;
v13 = (v3 + 1) & 0x3F;
v14 = (unsigned __int8)key_in[((result + key_in[v12 + 64]) & 0x3F) + 128];
key_in[192] = v13;
*(v4 - 1) = base64_table[v14];
if ( !v13 )
{
v5 = (v5 + 1) & 0x3F;
key_in[193] = v5;
if ( ((v11 + 1) & 0x3F) == 0 )
{
result = (result + 1) & 0x3F;
key_in[194] = result;
}
}

可以看的出来是一个单字节加密,对我们变表base64加密进行回溯索引,然后进行一系列嵌套操作最后再映射到变表上。所以我们直接写脚本单字节爆破即可

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

key=[0x33, 0x34, 0x2C, 0x36, 0x1D, 0x12, 0x1E, 0x0C, 0x1A, 0x3C, 0x29, 0x10, 0x20, 0x14, 0x3D, 0x3B, 0x19, 0x08, 0x0E, 0x1F, 0x30, 0x05, 0x38, 0x03, 0x11, 0x1B, 0x17, 0x21, 0x2E, 0x04, 0x18, 0x23, 0x2B, 0x02, 0x27, 0x37, 0x1C, 0x24, 0x39, 0x3F, 0x35, 0x2D, 0x26, 0x13, 0x2A, 0x0A, 0x00, 0x07, 0x3E, 0x01, 0x28, 0x2F, 0x32, 0x22, 0x0D, 0x06, 0x25, 0x3A, 0x09, 0x0F, 0x16, 0x0B, 0x15, 0x31, 0x0C, 0x2C, 0x0D, 0x21, 0x22, 0x09, 0x02, 0x39, 0x31, 0x17, 0x1A, 0x33, 0x06, 0x24, 0x10, 0x04, 0x1B, 0x0B, 0x34, 0x12, 0x38, 0x27, 0x0E, 0x20, 0x2B, 0x2E, 0x00, 0x13, 0x3E, 0x3A, 0x05, 0x1E, 0x36, 0x08, 0x32, 0x29, 0x19, 0x23, 0x3D, 0x3B, 0x3C, 0x3F, 0x37, 0x30, 0x18, 0x16, 0x35, 0x25, 0x0A, 0x2D, 0x28, 0x26, 0x15, 0x11, 0x07, 0x1D, 0x2A, 0x0F, 0x1F, 0x14, 0x01, 0x1C, 0x03, 0x2F, 0x13, 0x0D, 0x35, 0x31, 0x07, 0x11, 0x1B, 0x23, 0x0B, 0x0C, 0x10, 0x25, 0x2B, 0x21, 0x33, 0x18, 0x27, 0x29, 0x02, 0x2F, 0x28, 0x30, 0x0E, 0x19, 0x3C, 0x08, 0x34, 0x20, 0x3D, 0x2E, 0x05, 0x15, 0x2C, 0x1C, 0x36, 0x22, 0x1E, 0x24, 0x38, 0x0A, 0x3F, 0x1A, 0x04, 0x26, 0x16, 0x2A, 0x3A, 0x1F, 0x2D, 0x32, 0x06, 0x37, 0x03, 0x3B, 0x00, 0x17, 0x1D, 0x12, 0x09, 0x01, 0x3E, 0x39, 0x0F, 0x14]
compare_data='ksPhS/34MXifj+Ibtjud2Tikj5HkA7iTpbaNELBebOaIm'
base64_table='XiIzDuAoGlaK6JcjM3g/9YQmHBOsxn1hLZ4w7Tt0PV5pNqUFC+rE2dSfyvWe8kRb'
base64_standard_table='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
flag_tmp=''
for i in range(45):
for str in base64_table:
index=base64_table.find(str)
v12=key[(i+index)&0x3f]&0x3f
v14=key[(key[v12+64]&0x3f)+128]
if base64_table[v14] == compare_data[i]:
flag_tmp+=str
break
# print(flag_tmp)

flag_tmp=flag_tmp[:44]
print(base64.b64decode(flag_tmp.translate(str.maketrans(base64_table,base64_standard_table))))

Tips:这里比较数据是45字节,但base64变表加密结果只有44字节,所以猜测是额外读取了栈上的不相关数据,不同输入去尝试最后的加密结果发现都是’Im’,’I’为’=’的加密结果,’m’为不相关数据的加密结果。所以只要爆破完以后去掉最后一个字节即可。

maze

迷宫题,题目提示我是六角迷宫,拖入IDA分析:

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
__int64 main_main()
{
__int64 end; // rbp
char input; // [rsp+Fh] [rbp-29h] BYREF
__int64 position; // [rsp+10h] [rbp-28h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-20h]

v4 = __readfsqword(0x28u);
init();
end = *(_QWORD *)(*(_QWORD *)(*(_QWORD *)(qword_5D3680 + 8) + 8LL) + 8LL);
position = *(_QWORD *)(*(_QWORD *)(*(_QWORD *)(*(_QWORD *)(qword_5D36A0 + 8) + 8LL) + 8LL) + 8LL);
*(_BYTE *)(position + 27) = 1;
printf(&unk_5D45C0, "Welcome to Hexagonal maze\nplz enter the foot print\n");
input = 0;
do
{
print(&unk_5D45C0, "> ", 2LL);
scanf(&unk_5D46E0, &input);
maze(&position, (unsigned int)input);
}
while ( position != end );
print(&unk_5D45C0, "flag is md5 your input", 22LL);
sub_46D7E0(&unk_5D45C0);
if ( __readfsqword(0x28u) != v4 )
sub_52CB40();
return 0LL;
}

可以很明显的看到此题的整体逻辑是先生成迷宫,然后确定起点终点,再一次一次输入一次一次判断。那显而易见的是最重要的部分maze函数,我们进入并分析:

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
_QWORD *__fastcall sub_404BF5(_QWORD **a1, char a2)
{
_QWORD *v2; // rax
_QWORD *v3; // rax
_QWORD *result; // rax
_QWORD *v5; // rax

switch ( a2 )
{
case 'l':
if ( *((_BYTE *)*a1 + 24) || (v3 = (_QWORD *)**a1) == 0LL )
exit_exit(0LL);
*a1 = v3;
break;
case 'r':
if ( *((_BYTE *)*a1 + 25) || (v5 = (_QWORD *)(*a1)[1]) == 0LL )
exit_exit(0LL);
*a1 = v5;
break;
case 't':
if ( *((_BYTE *)*a1 + 26) || (v2 = (_QWORD *)(*a1)[2]) == 0LL )
exit_exit(0LL);
*a1 = v2;
break;
default:
exit_exit(0LL);
}
result = *a1;
if ( *((_BYTE *)*a1 + 27) )
exit_exit(0LL);
*((_BYTE *)result + 27) = 1;
return result;
}

可以得知,可以走了’l’,’r’,’t’三种路径,每种路径都会有一个判断,判断该路径是否能走,判断沿着这个路径走是否有下一个节点。对于路径判断之后是对前往的节点是否走过做判断,若为走过则标记为走过(标记1)。所以思路就很明确了,把迷宫提取出来然后搜索路径。

发现迷宫是链表结构,每个节点都有6*8个数据,IDA Python提取出来,然后我们搜索路径:

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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import hashlib
maze_map=[[7657696, 0, 0, 0, 0, 0, 49],
[7657744, 0, 7657792, 7658128, 1, 0, 49],
[7657792, 7657744, 7657840, 0, 65536, 0, 49],
[7657840, 7657792, 7657888, 7658224, 65536, 0, 49],
[7657888, 7657840, 7657936, 0, 256, 0, 49], #end
[7657936, 7657888, 7657984, 7658320, 65537, 0, 49],
[7657984, 7657936, 7658032, 0, 65536, 0, 49],
[7658032, 7657984, 0, 7658416, 256, 0, 49],
[7658080, 0, 7658128, 7658560, 1, 0, 49],
[7658128, 7658080, 7658176, 7657744, 256, 0, 49],
[7658176, 7658128, 7658224, 7658656, 1, 0, 49],
[7658224, 7658176, 7658272, 7657840, 65536, 0, 49],
[7658272, 7658224, 7658320, 7658752, 65536, 0, 49],
[7658320, 7658272, 7658368, 7657936, 65536, 0, 49],
[7658368, 7658320, 7658416, 7658848, 256, 0, 49],
[7658416, 7658368, 7658464, 7658032, 1, 0, 49],
[7658464, 7658416, 0, 7658944, 256, 0, 49],
[7658512, 0, 7658560, 7659040, 257, 0, 49],
[7658560, 7658512, 7658608, 7658080, 1, 0, 49],
[7658608, 7658560, 7658656, 7659136, 65536, 0, 49],
[7658656, 7658608, 7658704, 7658176, 256, 0, 49],
[7658704, 7658656, 7658752, 7659232, 1, 0, 49],
[7658752, 7658704, 7658800, 7658272, 65536, 0, 49],
[7658800, 7658752, 7658848, 7659328, 256, 0, 49],
[7658848, 7658800, 7658896, 7658368, 1, 0, 49],
[7658896, 7658848, 7658944, 7659424, 256, 0, 49],
[7658944, 7658896, 7658992, 7658464, 1, 0, 49],
[7658992, 7658944, 0, 7659520, 256, 0, 49],
[7659040, 0, 7659088, 7658512, 1, 0, 49],
[7659088, 7659040, 7659136, 7659568, 0, 0, 49],
[7659136, 7659088, 7659184, 7658608, 65536, 0, 49],
[7659184, 7659136, 7659232, 7659664, 65792, 0, 49],
[7659232, 7659184, 7659280, 7658704, 1, 0, 49],
[7659280, 7659232, 7659328, 7659760, 0, 0, 49],
[7659328, 7659280, 7659376, 7658800, 256, 0, 49],
[7659376, 7659328, 7659424, 7659856, 257, 0, 49],
[7659424, 7659376, 7659472, 7658896, 1, 0, 49],
[7659472, 7659424, 7659520, 7659952, 0, 0, 49],
[7659520, 7659472, 0, 7658992, 256, 0, 49],
[7659568, 0, 7659616, 7659088, 1, 0, 49],
[7659616, 7659568, 7659664, 7660000, 256, 0, 49],
[7659664, 7659616, 7659712, 7659184, 65537, 0, 49],
[7659712, 7659664, 7659760, 7660096, 65536, 0, 49],
[7659760, 7659712, 7659808, 7659280, 16777216, 0, 49], #start
[7659808, 7659760, 7659856, 7660192, 0, 0, 49],
[7659856, 7659808, 7659904, 7659376, 0, 0, 49],
[7659904, 7659856, 7659952, 7660288, 65536, 0, 49],
[7659952, 7659904, 0, 7659472, 256, 0, 49],
[7660000, 0, 7660048, 7659616, 1, 0, 49],
[7660048, 7660000, 7660096, 0, 65536, 0, 49],
[7660096, 7660048, 7660144, 7659712, 65536, 0, 49],
[7660144, 7660096, 7660192, 0, 65536, 0, 49],
[7660192, 7660144, 7660240, 7659808, 0, 0, 49],
[7660240, 7660192, 7660288, 0, 65536, 0, 49]]
data=maze_map
for i in range(54):
if data[i][4] & 0x1 == 1:
data[i][1] = 0
if data[i][4] & 0x100 == 0x100:
data[i][2] = 0
if data[i][4] & 0x10000 == 0x10000:
data[i][3] = 0

start_add=7659760
end_add=7657888
path=[]

def run_maze(path,start_add):
global end_add
global data
choice = ['l', 'r', 't']
position_add = start_add
backtrack_add = position_add
index = 0

if position_add == end_add: #到达终点输出路径
path_str = ''
print(path_str.join(path))
cipher_md5 = hashlib.md5()
cipher_md5.update(path_str.join(path).encode(encoding='utf-8'))
print( 'PCL{' + cipher_md5.hexdigest() + '}' )
exit(0)

for j in range(54): #寻找当前节点
if data[j][0] == position_add:
index = j
break
if data[j][0] != position_add:
return

if data[index][5] == 1: #原先的标志位与判断位有冲突 所以使用全零的第5位元素
return
else:
data[index][5] = 1

for i in choice: #循环递归
if i == 'l': #'l'
if data[index][1] == 0:
continue
position_add = data[index][1]
path.append(i)
# print(path)
run_maze(path, position_add)
path.pop()
position_add = backtrack_add
# print(path)

elif i == 'r': #'r'
if data[index][2] == 0:
continue
position_add = data[index][2]
path.append(i)
# print(path)
run_maze(path, position_add)
path.pop()
position_add = backtrack_add
# print(path)

elif i == 't': #'t'
if data[index][3] == 0:
return
position_add = data[index][3]
path.append(i)
# print(path)
run_maze(path, position_add)
path.pop()
# print(path)
return

run_maze(path,start_add)

得到路径:rrrrtltltlllltlltrtrrr

md5+format:PCL{988b0f23719099efcbd66586a168bab9}

Gocode

无壳64位go语言写的exe文件,放入IDA中分析。

程序很长,还有很多跳转,直接分析不太容易。我们动态调试,发现其实中间的一大长串代码通过if语句是不用先执行的,程序在开头qmemcpy(Go_Code, "\n", sizeof(Go_Code))向Go_Code数组进行了赋值,’\n’是一堆内存数据。然后直接跳过中间冗杂的代码,到最后,提示用户输入,对输入长度以及格式进行判断。回头我们来看中间的大段代码:

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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
 v5 = Go_Code[v1];
if ( v5 <= 0xC )
break;
if ( v5 > 0xE )
{
switch ( v5 )
{
case 0xFuLL:
if ( (unsigned __int64)(v1 + 1) >= 0x176 )
runtime_panicIndex();
v22 = Go_Code[v1 + 1];
if ( v22 >= 4 )
runtime_panicIndexU();
if ( (unsigned __int64)(v1 + 2) >= 0x176 )
runtime_panicIndex();
v23 = Go_Code[v1 + 2];
if ( v23 >= 4 )
goto LABEL_97;
v24 = *((_QWORD *)buf + v23);
if ( !v24 )
{
runtime_panicdivide();
LABEL_97:
runtime_panicIndexU();
}
v0 = v3;
*((_QWORD *)buf + v22) /= v24;
v1 += 3LL;
break;
case 0xAAuLL:
if ( (unsigned __int64)(v1 + 1) >= 0x176 )
runtime_panicIndex();
v25 = Go_Code[v1 + 1]; // 源操作数下标
if ( v25 >= 4 )
runtime_panicIndexU();
v26 = *((_QWORD *)buf + v25);
if ( (unsigned __int64)(v1 + 2) >= 0x176 )
runtime_panicIndex();
v27 = Go_Code[v1 + 2]; // 目的操作数下标
if ( v27 >= 4 )
runtime_panicIndexU();
v0 = *((_QWORD *)buf + v27);
if ( v0 != v26 ) // 比较
{
v47[0] = &unk_1032740;
v47[1] = &off_1070B58;
fmt_Fprintln((__int64)&off_1072218, qword_10F3428, (__int64)v47, 1LL, 1LL, v33, *((__int64 *)&v33 + 1), v34);
v31 = os_Exit(0LL);
v1 = v38;
v2 = v37;
v3 = v44;
}
v1 += 3LL;
break;
case 0xBBuLL:
if ( (unsigned __int64)(v1 + 2) >= 0x176 )
runtime_panicIndex();
v28 = Go_Code[v1 + 2]; // 目的操作数下标
if ( v28 >= v2 )
runtime_panicIndexU();
v29 = *(_QWORD *)(v3 + 8 * v28); // input[下标]
if ( (unsigned __int64)(v1 + 1) >= 0x176 )
runtime_panicIndex();
v0 = Go_Code[v1 + 1];
if ( v0 >= 4 )
runtime_panicIndexU();
*((_QWORD *)buf + v0) = v29; // v42暂存 即buf
v1 += 3LL;
break;
}
}
else if ( v5 == 0xD )
{
if ( (unsigned __int64)(v1 + 1) >= 0x176 )
runtime_panicIndex();
v18 = Go_Code[v1 + 1]; // 源操作数下标
if ( v18 >= 4 )
runtime_panicIndexU();
if ( (unsigned __int64)(v1 + 2) >= 0x176 )
runtime_panicIndex();
v19 = Go_Code[v1 + 2]; // 目的操作数下标
if ( v19 >= 4 )
runtime_panicIndexU();
v0 = *((_QWORD *)buf + v18) - *((_QWORD *)buf + v19);// sub
*((_QWORD *)buf + v18) = v0;
v1 += 3LL;
}
else
{
if ( (unsigned __int64)(v1 + 1) >= 0x176 )
runtime_panicIndex();
v20 = Go_Code[v1 + 1]; // 源操作数下标
if ( v20 >= 4 )
runtime_panicIndexU();
if ( (unsigned __int64)(v1 + 2) >= 0x176 )
runtime_panicIndex();
v21 = Go_Code[v1 + 2]; // 目的操作数下标
if ( v21 >= 4 )
runtime_panicIndexU();
v0 = *((_QWORD *)buf + v21) * *((_QWORD *)buf + v20);// mul
*((_QWORD *)buf + v20) = v0;
v1 += 3LL;
}
LABEL_12:
if ( v1 >= 374 )
{
v46[0] = &unk_1032740;
v46[1] = &off_1070B78;
fmt_Fprintln((__int64)&off_1072218, qword_10F3428, (__int64)v46, 1LL, 1LL, v33, *((__int64 *)&v33 + 1), v34);
return;
}
}
if ( v5 <= 2 )
{
if ( v5 == 1 )
{
if ( (unsigned __int64)(v1 + 1) >= 0x176 )
runtime_panicIndex();
v6 = Go_Code[v1 + 1]; // 目的操作数下标
if ( v6 >= 4 )
runtime_panicIndexU();
if ( (unsigned __int64)(v1 + 2) >= 0x176 )
runtime_panicIndex();
v7 = Go_Code[v1 + 2]; // 源操作数下标
if ( v7 >= 4 )
runtime_panicIndexU();
v0 = *((_QWORD *)buf + v7) ^ *((_QWORD *)buf + v6);// xor
*((_QWORD *)buf + v6) = v0;
v1 += 3LL;
}
else if ( v5 == 2 )
{
if ( (unsigned __int64)(v1 + 2) >= 0x176 )
runtime_panicIndex();
v0 = Go_Code[v1 + 2]; // 目的操作数
if ( (unsigned __int64)(v1 + 1) >= 0x176 )
runtime_panicIndex();
v8 = Go_Code[v1 + 1]; // 源操作数下标
if ( v8 >= 4 )
runtime_panicIndexU();
*((_QWORD *)buf + v8) = v0; // mov
v1 += 3LL;
}

可以看到这是一个大的switch操作,对v5的值进行筛选,然后执行对应的操作。而从他执行操作可以明白Go_Code数组中的数据是以三个为一组的,第一个元素代表指令,第二个元素代表源操作数,第三个元素代表目的操作数(下标)(_int64类型),并且根据题目Gocode猜测此题为Go的字节码题目,而且我们发现一个有意思的一点就是我们的输入,每两字节当作一个16进制的值而储存(strconv_ParseUint函数)。综上,我们只需将字节码提取出来并且代换成对应的指令即可:

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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
mov  buf[0]   input[0]
mov buf[1] input[1]
mov buf[2] input[2]
mov buf[3] input[3]
add buf[1] buf[2]
mov buf[2] 11B
cmp buf[1] buf[2]


mov buf[1] input[1]
mov buf[2] input[2]
add buf[0] buf[1]
mul buf[0] buf[2]
sub buf[0] buf[3]
mov buf[2] 7901
cmp buf[0] buf[2]


mov buf[2] input[2]
mov buf[0] 63
xor buf[2] buf[0]
mov buf[0] input[0]
mul buf[0] buf[2]
sub buf[0] buf[1]
add buf[0] buf[3]
mov buf[1] 1FF6
cmp buf[0] buf[1]


mov buf[1] input[1]
mov buf[0] input[0]
mov buf[2] input[2]
add buf[1] buf[0]
add buf[1] buf[2]
add buf[1] buf[3]
mov buf[0] 21E
cmp buf[1] buf[0]


mov buf[1] input[1]
mov buf[0] input[0]
mul buf[0] buf[3]
sub buf[0] buf[1]
add buf[0] buf[2]
mov buf[3] 3331
cmp buf[0] buf[3]


mov buf[0] input[4]
mov buf[1] input[5]
mov buf[2] input[6]
mov buf[3] 63
add buf[0] buf[3]
mov buf[3] 68
xor buf[0] buf[3]
mov buf[3] 152
cmp buf[0] buf[3]


mov buf[3] 33
sub buf[1] buf[3]
mov buf[3] 63
mul buf[1] buf[3]
mov buf[0] 441
cmp buf[0] buf[1]


mov buf[3] 63
xor buf[2] buf[3]
mov buf[3] 6B
add buf[2] buf[3]
mov buf[1] 10E
cmp buf[2] buf[1]


mov buf[1] 9E
mov buf[0] input[7]
cmp buf[0] buf[1]


mov buf[0] input[8]
mov buf[1] input[9]
mov buf[2] input[A]
mov buf[3] input[B]
mul buf[0] buf[3]
mov buf[3] 36CE
cmp buf[0] buf[3]


mov buf[0] input[8]
mov buf[3] input[B]
add buf[0] buf[1]
mul buf[0] buf[2]
sub buf[0] buf[3]
mov buf[2] 682D
cmp buf[0] buf[2]


mov buf[2] input[A]
mov buf[0] 63
xor buf[2] buf[0]
mov buf[0] input[8]
mul buf[0] buf[2]
sub buf[0] buf[1]
add buf[0] buf[3]
mov buf[1] 15
cmp buf[0] buf[1]


mov buf[1] input[9]
mov buf[0] input[8]
mov buf[2] input[A]
add buf[1] buf[0]
add buf[1] buf[2]
add buf[1] buf[3]
mov buf[0] 1AE
cmp buf[1] buf[0]


mov buf[1] input[9]
mov buf[0] input[8]
mul buf[0] buf[3]
sub buf[0] buf[1]
add buf[0] buf[2]
mov buf[3] 3709
cmp buf[0] buf[3]


mov buf[0] input[C]
mov buf[1] input[D]
mov buf[2] input[E]
mov buf[3] 63
add buf[0] buf[3]
mov buf[3] 68
xor buf[0] buf[3]
mov buf[3] FA
cmp buf[0] buf[3]


mov buf[3] 1E
sub buf[1] buf[3]
mov buf[3] 63
mul buf[1] buf[3]
mov buf[0] 18C
cmp buf[0] buf[1]


mov buf[3] 63
xor buf[2] buf[3]
mov buf[3] 6B
add buf[2] buf[3]
mov buf[1] 83
cmp buf[2] buf[1]


mov buf[1] 47
mov buf[0] input[F]
cmp buf[0] buf[1]

这里就是一堆的运算和比较的约束条件,我们一部分一部分的写好约束,用Z3求解即可:

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

s=Solver()
buf=[BitVec(f'x%d'%i,8) for i in range(16)]

s.add(buf[1]+buf[2]==0x11b)
s.add(((buf[0]+buf[1])*buf[2]-buf[3])==0x7901)
s.add(((buf[2]^0x63)*buf[0])-buf[1]+buf[3]==0x1FF6)
s.add(buf[1]+buf[0]+buf[2]+buf[3]==0x21E)
s.add(buf[0]*buf[3]-buf[1]+buf[2]==0x3331)
s.add((buf[4]+0x63)^0x68==0x152)
s.add((buf[5]-0x33)*0x63==0x441)
s.add((buf[6]^0x63)+0x6B==0x10E)
s.add(buf[7]==0x9E)
s.add(buf[8]*buf[0xb]==0x36CE)
s.add((buf[8]+buf[9])*buf[0xa]-buf[0xb]==0x682D)
s.add((buf[0xa]^0x63)*buf[8]-buf[9]+buf[0xb]==0x15)
s.add(buf[9]+buf[8]+buf[0xa]+buf[0xb]==0x1AE)
s.add(buf[8]*buf[0xb]-buf[9]+buf[0xa]==0x3709)
s.add((buf[0xc]+0x63)^0x68==0xFA)
s.add((buf[0xd]-0x1E)*0x63==0x18C)
s.add((buf[0xe]^0x63)+0x6B==0x83)
s.add(buf[0xf]==0x47)

check=s.check()
if check == sat:
print("Congratulations!")
result=s.model()
flag_arr=[(result[buf[i]].as_long().real) for i in range(16)]
print("PCL{",end='')
for i in flag_arr:
print(hex(i)[2:],end='')
print("}",end='')
else:
print("Try again QAQ")

Tips:Go_Code的前两个元素是0x0A和0x0B,第一个元素用不到,因为初始的索引就是1,第二个元素将swich引导去执行strconv_ParseUint函数来使输入字符串每两字节以16进制数值存储

flag=PCL{bd4ccf46d73ec09ee628633d2f227b47}

bug之眼

这道题非常的有意思,也是第一次见到这种反调试技术(DebugBlocker反调试技术

参考文章:奇安信攻防社区-DebugBlocker反调试技术 (butian.net)

首先ExeinfoPe查壳发现为无壳64位exe文件,IDA反编译开始分析程序。

1
2
3
4
5
6
7
8
int __cdecl main(int argc, const char **argv, const char **envp)
{
if ( IsDebuggerPresent() )
sub_1400024B0();
else
sub_140002D50();
return 0;
}

上来就是*IsDebuggerPresent()*函数检测进程是否处于调试状态,根据判断结果跳转到两个不同的函数执行。我们先来观察这个非调试状态执行的函数:

1
2
3
4
5
6
7
8
9
memset(Filename, 0, sizeof(Filename));
GetModuleFileNameA(0i64, Filename, 0x100u);
memset(Destination, 0, sizeof(Destination));
strcat_s(Destination, 0x100ui64, "\"");
strcat_s(Destination, 0x100ui64, Filename);
strcat_s(Destination, 0x100ui64, "\"");
memset(&StartupInfo, 0, sizeof(StartupInfo));
memset(&ProcessInformation, 0, sizeof(ProcessInformation));
CreateProcessA(0i64, Destination, 0i64, 0i64, 0, 1u, 0i64, 0i64, &StartupInfo, &ProcessInformation)

程序读取了我们运行文件的路径,字符串拼接上双引号后创建了一个子进程,即调用CreateProcessA函数。我们查阅MSDN该函数的参数详情:

1
2
3
4
5
6
7
8
9
10
11
12
>BOOL CreateProcessA(
[in, optional] LPCSTR lpApplicationName,
[in, out, optional] LPSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCSTR lpCurrentDirectory,
[in] LPSTARTUPINFOA lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
>);

CreateProcessA(0i64, Destination, 0i64, 0i64, 0, 1u, 0i64, 0i64, &StartupInfo, &ProcessInformation)

观察题目中调用该函数所传递的参数,除了一些必要的参数和0之外,我们发现其在dwCreationFlags参数位置赋上了’1’,我们查阅该参数:

dwCreationFlags(进程创建标志)

The flags that control the priority class and the creation of the process.

常量/值 描述
CREATE_BREAKAWAY_FROM_JOB0x01000000 与作业关联的进程的子进程不与作业关联。 如果调用进程未与作业关联,则此常量不起作用。如果调用进程与作业关联,则该作业必须设置JOB_OBJECT_LIMIT_BREAKAWAY_OK限制。
CREATE_DEFAULT_ERROR_MODE0x04000000 新进程不会继承调用进程的错误模式。相反,新进程将获得默认的错误模式。 此功能对于在禁用硬错误的情况下运行的多线程 shell 应用程序特别有用。 默认行为是让新进程继承调用方的错误模式。设置此标志会更改该默认行为。
CREATE_NEW_CONSOLE0x00000010 新进程具有新的控制台,而不是继承其父进程的控制台(默认值)。有关更多信息,请参阅 创建控制台。 此标志不能与DETACHED_PROCESS一起使用。
CREATE_NEW_PROCESS_GROUP0x00000200 新进程是新进程组的根进程。进程组包括作为此根进程的后代的所有进程。新进程组的进程标识符与在 lpProcessInformation 参数中返回的进程标识符相同。进程组由 GenerateConsoleCtrlEvent 函数使用,以便能够向一组控制台进程发送 CTRL+BREAK 信号。 如果指定了此标志,则将为新进程组中的所有进程禁用 Ctrl+C 信号。 如果使用 CREATE_NEW_CONSOLE 指定,则忽略此标志。
CREATE_NO_WINDOW0x08000000 该进程是一个控制台应用程序,在没有控制台窗口的情况下运行。因此,未设置应用程序的控制台句柄。 如果应用程序不是控制台应用程序,或者与CREATE_NEW_CONSOLE或DETACHED_PROCESS一起使用,则忽略标志。
CREATE_PROTECTED_PROCESS0x00040000 该进程将作为受保护的进程运行。系统限制对受保护进程和受保护进程的线程的访问。有关进程如何与受保护的进程交互的详细信息,请参阅进程安全性和访问权限。 若要激活受保护的进程,二进制文件必须具有特殊签名。此签名由 Microsoft 提供,但目前不适用于非 Microsoft 二进制文件。目前有四个受保护的进程:媒体基础、音频引擎、Windows 错误报告和系统。加载到这些二进制文件的组件也必须签名。多媒体公司可以利用前两个受保护的进程。有关详细信息,请参阅受保护的媒体路径概述Windows Server 2003 和 Windows XP:不支持此值。
CREATE_PRESERVE_CODE_AUTHZ_LEVEL0x02000000 允许调用方执行绕过通常自动应用于进程的进程限制的子进程。
CREATE_SECURE_PROCESS0x00400000 此标志允许在基于虚拟化的安全环境中运行的安全进程启动。
CREATE_SEPARATE_WOW_VDM0x00000800 此标志仅在启动基于 16 位 Windows 的应用程序时有效。如果设置,新进程将在专用虚拟 DOS 计算机 (VDM) 中运行。默认情况下,所有基于 Windows 的 16 位应用程序在单个共享 VDM 中作为线程运行。单独运行的优点是崩溃只会终止单个VDM;在不同的 VDM 中运行的任何其他程序仍可继续正常运行。此外,在单独的 VDM 中运行的基于 Windows 的 16 位应用程序具有单独的输入队列。这意味着,如果一个应用程序暂时停止响应,则不同 VDM 中的应用程序将继续接收输入。单独运行的缺点是这样做需要更多的内存。仅当用户请求 16 位应用程序在其自己的 VDM 中运行时,才应使用此标志。
CREATE_SHARED_WOW_VDM0x00001000 该标志仅在启动基于 16 位 Windows 的应用程序时有效。如果 DefaultSeparateVDM 在 WIN 的 Windows 部分中切换。INI 为 TRUE,此标志将覆盖交换机。新进程在共享的虚拟 DOS 计算机中运行。
CREATE_SUSPENDED0x00000004 新进程的主线程在挂起状态下创建,并且在调用 ResumeThread 函数之前不会运行。
CREATE_UNICODE_ENVIRONMENT0x00000400 如果设置了此标志,则 lp 环境所指向的环境块将使用 Unicode 字符。否则,环境块将使用 ANSI 字符。
DEBUG_ONLY_THIS_PROCESS0x00000002 调用线程启动并调试新进程。它可以使用 WaitForDebugEvent 函数接收所有相关的调试事件。
DEBUG_PROCESS0x00000001 调用线程启动并调试新进程以及新进程创建的所有子进程。它可以使用 WaitForDebugEvent 函数接收所有相关的调试事件。 使用DEBUG_PROCESS的进程将成为调试链的根。这一直持续到使用DEBUG_PROCESS创建链中的另一个进程。 如果此标志与DEBUG_ONLY_THIS_PROCESS相结合,则调用方仅调试新进程,而不调试任何子进程。
DETACHED_PROCESS0x00000008 对于控制台进程,新进程不会继承其父进程的控制台(默认值)。新进程可以在以后调用 AllocConsole 函数来创建控制台。有关更多信息,请参阅 创建控制台。 此值不能与CREATE_NEW_CONSOLE一起使用。
EXTENDED_STARTUPINFO_PRESENT0x00080000 该过程是使用扩展的启动信息创建的;lpStartupInfo 参数指定 STARTUPINFOEX 结构。 Windows Server 2003 和 Windows XP:不支持此值。
INHERIT_PARENT_AFFINITY0x00010000 该进程继承其父级的相关性。如果父进程在多个处理器组中具有线程,则新进程将继承父进程正在使用的任意组的组相对相关性。 Windows Server 2008、Windows Vista、Windows Server 2003 和 Windows XP:不支持此值。

当该值赋1时,代表DEBUG_PROCESS,代表着新创建的进程将处于调试状态,并且调试状态下的任何事件都可以被WaitForDebugEvent函数接收。(这里也是做题时卡住的点,阅读文档不够仔细,忽略了dwCreationFlags参数的作用)

这里明确一个概念,父进程创建子进程,二者是同时执行,并非父进程执行完毕子进程再执行。

从这里开始子进程开始运行,并执行处于调试状态下进入的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
v12 = VirtualAlloc((LPVOID)0x141000000i64, 8ui64, 0x3000u, 0x40u);
if ( v12 )
{
sub_140001000(v12, 8ui64, &unk_140006048, 8ui64);// 给首地址写入int3
((void (*)(void))v12)(); // 第一个异常处理点
}
memset(Filename, 0, sizeof(Filename));
GetModuleFileNameA(0i64, Filename, 0x100u);
memset(Destination, 0, sizeof(Destination));
strcat_s(Destination, 0x100ui64, "\"");
strcat_s(Destination, 0x100ui64, Filename);
strcat_s(Destination, 0x100ui64, "\"");
memset(&StartupInfo, 0, sizeof(StartupInfo));
memset(&ProcessInformation, 0, sizeof(ProcessInformation));
CreateProcessA(0i64, Destination, 0i64, 0i64, 0, 1u, 0i64, 0i64, &StartupInfo, &ProcessInformation);

通过观察可以发现该函数的开头和上一函数很像,多了一步分配内存,向内存的第一个字节写入0xCC,并用函数指针执行此分配内存的操作。这里函数指针执行到0xCC子进程便先行中断了,我们回头看父进程如何操作:

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
memset(&DebugEvent, 0, sizeof(DebugEvent));
while ( 1 )
{
WaitForDebugEvent(&DebugEvent, 0xFFFFFFFF);
if ( DebugEvent.dwDebugEventCode == 5 ) // 退出进程调试事件
break;
if ( DebugEvent.dwDebugEventCode == 1 && DebugEvent.u.Exception.ExceptionRecord.ExceptionCode == 0x80000003 )// 报告异常调试事件 并为断点异常
{
lpBaseAddress = DebugEvent.u.Exception.ExceptionRecord.ExceptionAddress;
if ( DebugEvent.u.Exception.ExceptionRecord.ExceptionAddress == (PVOID)0x141000000i64 )
{
memset(&Context, 0, sizeof(Context));
Context.ContextFlags = 1048607;
GetThreadContext(ProcessInformation.hThread, &Context);// 获取上下文句柄
--Context.Rip; // rip指针回退一字节 即回退至0xCC之前
SetThreadContext(ProcessInformation.hThread, &Context);// 设置线程上下文
Buffer = 0xC3i64; // ret
NumberOfBytesWritten = 0i64;
WriteProcessMemory(ProcessInformation.hProcess, lpBaseAddress, &Buffer, 8ui64, &NumberOfBytesWritten);// 将ret写入修改0xCC
v9 = operator new(0x14E8ui64);
lpBuffer = v9;
ReadProcessMemory(ProcessInformation.hProcess, &unk_140006050, v9, 0x14E8ui64, &NumberOfBytesWritten);// 将650处数据读入v9
memset(v15, 0, sizeof(v15));
vsprintf_s(v15, 256i64, "%lld", &unk_140006050);
for ( i = 0i64; i < 0x14E8; ++i )
{
v10 = v15;
v3 = -1i64;
do
++v3;
while ( v10[v3] );
v11 = v3;
lpBuffer[i] ^= v15[i % v3];
v12 = v15;
v4 = -1i64;
do
++v4;
while ( v12[v4] );
if ( !((i + 1) % v4) )
vsprintf_s(v15, 256i64, "%lld", (char *)&unk_140006050 + i);
}
WriteProcessMemory(ProcessInformation.hProcess, &unk_140006050, lpBuffer, 0x14E8ui64, &NumberOfBytesWritten);
Block = lpBuffer;
j_j_free(lpBuffer);
}
}
ContinueDebugEvent(DebugEvent.dwProcessId, DebugEvent.dwThreadId, 0x10002u);
}

这里代码比较长,我们慢慢分析。前文中提到,dwCreationFlags标志位标记1代表新建进程处于调试状态并且调试事件都可以被WaitForDebugEvent函数接收。所以这段代码开头是对子进程调试过程中的异常事件进行接收和处理。首先我们关注DebugEvent结构体中的dwDebugEventCode成员:

dwDebugEventCode

The code that identifies the type of debugging event.

价值 意义
CREATE_PROCESS_DEBUG_EVENT 3 报告创建进程调试事件(包括进程及其主线程)。u.CreateProcessInfo 的值指定CREATE_PROCESS_DEBUG_INFO结构。
**CREATE_THREAD_DEBUG_EVENT **2 报告创建线程调试事件(不包括进程的主线程,请参阅“CREATE_PROCESS_DEBUG_EVENT”)。u.CreateThread 的值指定CREATE_THREAD_DEBUG_INFO结构。
EXCEPTION_DEBUG_EVENT 1 报告异常调试事件。u.Exception 的值指定EXCEPTION_DEBUG_INFO结构。
**EXIT_PROCESS_DEBUG_EVENT ** 5 报告退出进程调试事件。u.ExitProcess 的值指定EXIT_PROCESS_DEBUG_INFO结构。
**EXIT_THREAD_DEBUG_EVENT ** 4 报告退出线程调试事件。u.ExitThread 的值指定EXIT_THREAD_DEBUG_INFO结构。
**LOAD_DLL_DEBUG_EVENT ** 6 报告加载动态链接库 (DLL) 调试事件。u.LoadDll 的值指定LOAD_DLL_DEBUG_INFO结构
**OUTPUT_DEBUG_STRING_EVENT ** 8 报告输出调试字符串调试事件。u.DebugString 的值指定一个OUTPUT_DEBUG_STRING_INFO结构。
RIP_EVENT 9 报告 RIP 调试事件(系统调试错误)。u.RipInfo 的值指定RIP_INFO结构。
UNLOAD_DLL_DEBUG_EVENT 7 报告卸载 DLL 调试事件。u.UnloadDll 的值指定UNLOAD_DLL_DEBUG_INFO结构。

等于5接收到退出进程调试事件信息,就break了。接下来等于1代表接收到异常调试事件,关于异常处理,我们关注DebugEvent中的u.Exception.ExceptionRecord结构体中的ExceptionCode成员,该成员描述发生异常的原因。题目中的ExceptionCode为0x80000003,为EXCEPTION_BREAKPOINT,断点异常,就是去检测子进程是否遇到断点。接下来获取句柄,eip回退1,修改断点为0xC3(ret),让子进程继续执行,而父进程也继续执行接下来的代码。即对140006050进行自解密,并写入新建进程,通过ContinueDebugEvent通知调试器继续调试。这里vsprintf_s将地址转化为10进制,相当于当作一个key去自解密。

我们再来看子进程:

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
memset(&DebugEvent, 0, sizeof(DebugEvent));
while ( 1 )
{
WaitForDebugEvent(&DebugEvent, 0xFFFFFFFF);
if ( DebugEvent.dwDebugEventCode == 5 )
break;
if ( DebugEvent.dwDebugEventCode == 1 && DebugEvent.u.Exception.ExceptionRecord.ExceptionCode == 0x80000003 )// STATUS_BREAKPOINT 断点检测
{
ExceptionAddress = (va_list)DebugEvent.u.Exception.ExceptionRecord.ExceptionAddress;
if ( DebugEvent.u.Exception.ExceptionRecord.ExceptionAddress == (PVOID)0x141000000i64 )// 孙子进程的141000000处检测到断点
{
memset(&Context, 0, sizeof(Context));
Context.ContextFlags = 0x10001F;
GetThreadContext(ProcessInformation.hThread, &Context);
Context.Rip = (DWORD64)byte_140001330; // 将执行处改为140001330
SetThreadContext(ProcessInformation.hThread, &Context);
}
else if ( (__int64)DebugEvent.u.Exception.ExceptionRecord.ExceptionAddress < 0x141000000i64 )
{
memset(&v32, 0, sizeof(v32));
v32.ContextFlags = 0x10001F;
GetThreadContext(ProcessInformation.hThread, &v32);
--v32.Rip;
SetThreadContext(ProcessInformation.hThread, &v32);
for ( i = 0i64; i < 0x29D; i += 3i64 ) // 3个一组
{
if ( ExceptionAddress == &byte_140001330[*(_QWORD *)&unk_140006050[8 * i]] )// 6050数据索引断点位置
{
if ( qword_140006040 != -1 )
{
lpBaseAddress = &byte_140001330[*(_QWORD *)&unk_140006050[8 * qword_140006040]];// 指针指向异常处
v25 = operator new(*(_QWORD *)&unk_140006050[8 * qword_140006040 + 8]);// 6050数据为3个一组 每组第二个为块长度
lpBuffer = v25;
NumberOfBytesRead = 0i64;
ReadProcessMemory(
ProcessInformation.hProcess,
lpBaseAddress,
v25,
*(_QWORD *)&unk_140006050[8 * qword_140006040 + 8],
&NumberOfBytesRead);
*lpBuffer ^= unk_140006050[8 * qword_140006040 + 16];// 每组第三个数据为修复数据 修复0xCC
memset(v27, 0, sizeof(v27));
vsprintf_s(v27, 0x100ui64, "%lld", lpBaseAddress + 1);// 10进制
for ( j = 1i64; j < *(_QWORD *)&unk_140006050[8 * qword_140006040 + 8]; ++j )
{
v16 = v27;
v7 = -1i64;
do
++v7;
while ( v16[v7] ); // len地址十进制长度
lpBuffer[j] ^= v27[(j - 1) % v7];
v17 = v27;
v8 = -1i64;
do
++v8;
while ( v17[v8] );
v18 = v8;
if ( !(j % v8) )
vsprintf_s(v27, 0x100ui64, "%lld", &lpBaseAddress[j]);
}
WriteProcessMemory(
ProcessInformation.hProcess,
lpBaseAddress,
lpBuffer,
*(_QWORD *)&unk_140006050[8 * qword_140006040 + 8],
&NumberOfBytesRead);
Block = lpBuffer;
j_j_free(lpBuffer);
}

子进程创建并调试孙子进程,孙子进程同样卡在断点处,然后子进程检测到断点异常修改孙子进程的断点为ret,孙子进程继续执行。子进程继续检测断点,当孙子进程检测到141000000处的断点后,子进程将rip改到140001330处继续执行检测。我们观察140001330地址的内存,发现里面有许多0xCC,这种可以被系统检测到的断点异常,系统会检测这些异常并用父进程修改过的140006050数据去索引修改。通过理解for循环我们可以知道140006050的数据分为三组,第一个数据是断点位置,也可以理解为修复位置,第二个数据为修复块的大小,第三个数据为用于修复的数据。程序在循环之前先对断点进行修改,再在循环时对整个修复块进行修改。因此我们可以模仿程序的逻辑自己去修复140001330的数据。我们只需要寻找140001330在文件中的位置即可。

我们用010Editor打开程序,读取.text节节表中的VirtualAddresPointToRawData,根据计算方法RAW = RVA - VirtualAddress + PointToRawData计算出数据在文件中的地址,并写脚本修复:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
table = [0, 95, 140, 95, 30, 132, 125, 14, 132, 139, 20, 132, 159, 44, 132, 203, 13, 132, 216, 12, 132, 228, 37, 132, 265, 11, 132, 276, 20, 132, 296, 8, 255, 304, 14, 132, 318, 24, 132, 342, 15, 132, 357, 41, 132, 398, 16, 132, 414, 8, 255, 422, 14, 132, 436, 24, 132, 460, 15, 132, 475, 41, 132, 516, 16, 132, 532, 39, 132, 571, 11, 132, 582, 18, 132, 600, 25, 132, 625, 11, 132, 636, 11, 132, 647, 22, 132, 669, 28, 132, 697, 24, 132, 721, 11, 132, 732, 13, 132, 745, 12, 132, 757, 8, 255, 765, 11, 132, 776, 24, 132, 800, 15, 132, 815, 41, 132, 856, 13, 132, 869, 8, 255, 877, 14, 132, 891, 24, 132, 915, 15, 132, 930, 41, 132, 971, 16, 132, 987, 33, 132, 1020, 8, 132, 1028, 18, 132, 1046, 22, 132, 1068, 8, 132, 1076, 8, 132, 1084, 22, 132, 1106, 25, 132, 1131, 27, 132, 1158, 13, 132, 1171, 12, 132, 1183, 35, 132, 1218, 11, 132, 1229, 18, 132, 1247, 25, 132, 1272, 8, 132, 1280, 8, 132, 1288, 22, 132, 1310, 25, 132, 1335, 22, 132, 1357, 35, 132, 1392, 11, 132, 1403, 18, 132, 1421, 25, 132, 1446, 8, 132, 1454, 8, 132, 1462, 22, 132, 1484, 25, 132, 1509, 22, 132, 1531, 8, 132, 1539, 8, 132, 1547, 18, 132, 1565, 22, 132, 1587, 35, 132, 1622, 11, 132, 1633, 22, 132, 1655, 28, 132, 1683, 22, 132, 1705, 8, 132, 1713, 8, 132, 1721, 18, 132, 1739, 22, 132, 1761, 35, 132, 1796, 11, 132, 1807, 22, 132, 1829, 28, 132, 1857, 18, 132, 1875, 20, 132, 1895, 5, 37, 1900, 11, 132, 1911, 13, 132, 1924, 12, 132, 1936, 11, 132, 1947, 13, 132, 1960, 12, 132, 1972, 20, 132, 1992, 21, 132, 2013, 21, 132, 2034, 35, 132, 2069, 11, 132, 2080, 18, 132, 2098, 25, 132, 2123, 35, 132, 2158, 11, 132, 2169, 22, 132, 2191, 28, 132, 2219, 18, 132, 2237, 20, 132, 2257, 5, 37, 2262, 5, 37, 2267, 11, 132, 2278, 13, 132, 2291, 12, 132, 2303, 11, 132, 2314, 13, 132, 2327, 12, 132, 2339, 20, 132, 2359, 12, 132, 2371, 35, 132, 2406, 11, 132, 2417, 18, 132, 2435, 25, 132, 2460, 35, 132, 2495, 11, 132, 2506, 22, 132, 2528, 28, 132, 2556, 21, 132, 2577, 20, 132, 2597, 12, 132, 2609, 35, 132, 2644, 11, 132, 2655, 18, 132, 2673, 25, 132, 2698, 35, 132, 2733, 11, 132, 2744, 22, 132, 2766, 28, 132, 2794, 21, 132, 2815, 20, 132, 2835, 5, 37, 2840, 5, 37, 2845, 5, 37, 2850, 11, 132, 2861, 13, 132, 2874, 12, 132, 2886, 35, 132, 2921, 13, 132, 2934, 12, 132, 2946, 8, 132, 2954, 8, 132, 2962, 18, 132, 2980, 22, 132, 3002, 8, 132, 3010, 8, 132, 3018, 22, 132, 3040, 25, 132, 3065, 71, 132, 3136, 8, 132, 3144, 18, 132, 3162, 22, 132, 3184, 8, 132, 3192, 8, 132, 3200, 22, 132, 3222, 25, 132, 3247, 68, 132, 3315, 14, 132, 3329, 14, 132, 3343, 20, 132, 3363, 5, 37, 3368, 14, 132, 3382, 19, 132, 3401, 15, 132, 3416, 14, 132, 3430, 19, 132, 3449, 15, 132, 3464, 26, 132, 3490, 19, 132, 3509, 15, 132, 3524, 14, 132, 3538, 19, 132, 3557, 15, 132, 3572, 36, 132, 3608, 11, 132, 3619, 18, 132, 3637, 25, 132, 3662, 36, 132, 3698, 11, 132, 3709, 22, 132, 3731, 28, 132, 3759, 68, 132, 3827, 5, 37, 3832, 14, 132, 3846, 20, 132, 3866, 5, 37, 3871, 5, 37, 3876, 8, 255, 3884, 11, 116, 3895, 18, 132, 3913, 25, 116, 3938, 8, 255, 3946, 11, 116, 3957, 22, 132, 3979, 31, 116, 4010, 27, 132, 4037, 11, 116, 4048, 18, 132, 4066, 25, 116, 4091, 8, 255, 4099, 11, 116, 4110, 22, 132, 4132, 31, 116, 4163, 37, 132, 4200, 38, 132, 4238, 38, 132, 4276, 20, 132, 4296, 28, 132, 4324, 9, 132]
# print(len(table))

start_add=0x140001330
exe_bin=bytearray(open('D:\\ctf\\BUG之眼-题目附件\\Bug.exe','rb').read())
file_start=0x730
for i in range(0,len(table),3):
offset=table[i]
size=table[i+1]
improve_data=table[i+2]
d=start_add+offset
fix_int3=list(str(d+1))
exe_bin[file_start+offset]^=improve_data
for j in range(1,size):
exe_bin[file_start+offset+j]^=ord(fix_int3[(j-1)%len(fix_int3)])
if j%len(fix_int3) == 0:
fix_int3=list(str(d+j))
open('D:\\ctf\\BUG之眼-题目附件\\Bug.exe','wb').write(exe_bin)

我们打开修改后的指针,发现140001330处变为了函数可以执行。我们patch程序,丢掉不需要的部分,然后动态调试帮助我们分析程序:

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
memset(input, 0, 0x100ui64);
printf("There are some bugs!\nPlease fix them up by inputing something: ");
scanf("%s", input);
v11 = -1i64;
do
++v11;
while ( input[v11] );
if ( v11 != 81 ) // 输入长度判断
{
printf("Oops! It doesn't work.");
exit(0);
}
memset(v65, 0, sizeof(v65)); // qword 一个元素存两个数独元素
sub_140002420(v65); // 分配全零内存空间
for ( i = 0i64; i < 81; ++i )
{
input_shudu = input[i] - 48i64; // 将输入的数字转化为内存里的数字
if ( input_shudu < 1 || input_shudu > 9 ) // 输入控制在1-9
{
printf("Oops! It doesn't work.");
exit(0);
}
v17 = 0i64;
for ( j = 9i64; j <= i; j += 9i64 )
++v17; // 最高加到9
v19 = 0i64;
for ( k = 9i64; k <= i; k += 9i64 )
++v19; // 最高加到9
v21 = i - 9 * v19; // 指向当前行的当前元素
if ( v17 >= 0 && v17 <= 8 ) // 判断是否在0-8行
v20 = &v65[5 * v17]; // 向v20赋值输入行地址
else
v20 = &v65[45]; // 第十行行地址
if ( v21 >= 0 && v21 <= 8 )
v53 = (__int64 *)v20 + v21; // 行列坐标地址 且会跳过第十列输入
else // 为第十列时
v53 = (__int64 *)v20 + 9; // 走不到这个else
*v53 = input_shudu; // 0-8行 0-8列按顺序input给数独赋值
} // 10行10列 10行10列都为零 其余按照输入填写

这里的我们可以知道这是一个10*10的矩阵,除了第10行和第十列的所有元素都为零外,其余元素都由输入的在1-9之间的数字按顺序填充,一共81字节输入。

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
 for ( m = 0i64; m < 81; ++m )
{
v24 = 0i64;
for ( n = 9i64; n <= m; n += 9i64 )
++v24;
v26 = 0i64;
for ( ii = 9i64; ii <= m; ii += 9i64 )
++v26;
v0 = m - 9 * v26; // 当前列
if ( v24 >= 0 && v24 <= 8 )
v27 = &v65[5 * v24]; // 当前行地址
else
v27 = &v65[45]; // 第十行
if ( v0 >= 0 && v0 <= 8 )
v54 = (__int64 *)v27 + v0; // 确定坐标地址
else
v54 = (__int64 *)v27 + 9;
v1 = *v54; // 从数独中取值
for ( jj = 1i64; jj < 9; ++jj )
{
v28 = jj + v24;
if ( jj + v24 >= 0 && v28 <= 8 )
v29 = &v65[5 * v28];
else
v29 = &v65[45];
if ( v0 >= 0 && v0 <= 8 )
v55 = (_QWORD *)v29 + v0;
else
v55 = (_QWORD *)v29 + 9;
if ( v1 == *v55 ) // 一列从当前元素开始向下遍历不能有重复
goto exxxxittttt;
v30 = v24 - jj;
if ( v24 - jj >= 0 && v30 <= 8 )
v31 = &v65[5 * v30];
else
v31 = &v65[45];
v56 = v0 >= 0 && v0 <= 8 ? (_QWORD *)((char *)v31 + 8 * v0) : (_QWORD *)((char *)v31 + 72);
if ( v1 != *v56 ) // 一列元素从当前元素开始向上遍历不能有重复
{
v32 = v24 >= 0 && v24 <= 8 ? &v65[5 * v24] : &v65[45];// 行
v33 = jj + v0; // 从当前元素开始的当前行的每一个元素
if ( jj + v0 >= 0 && v33 <= 8 )
v57 = (_QWORD *)v32 + v33;
else
v57 = (_QWORD *)v32 + 9;
if ( v1 != *v57 ) // 一行从当前元素开始向右遍历不能有重复
{
v34 = v24 >= 0 && v24 <= 8 ? &v65[5 * v24] : &v65[45];
v35 = v0 - jj;
if ( v0 - jj >= 0 && v35 <= 8 )
v58 = (_QWORD *)v34 + v35;
else
v58 = (_QWORD *)v34 + 9;
if ( v1 != *v58 ) // 一行从当前元素开始向左遍历不能有重复
continue;
}
}
exxxxittttt:
printf("Oops! It doesn't work.");
exit(0);
}

分析到这里基本可以察觉到类似于数独的特征,比如只能填1-9,行列不能有重复等等。我们不用把他当作数独来解,我们只需要z3照抄其约束即可。故我们构造Z3解题脚本:

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

s=Solver()

shudu_data=[[0 for i in range(10)] for j in range(10)]
# print(shudu_data)

for i in range(9):
for j in range(9):
shudu_data[i][j] = BitVec("x%d%d"%(i,j),8)

for i in range(10):
for j in range(10):
shudu_data[i][9] = 0
shudu_data[9][j] = 0
# print(shudu_data)

# 数独基础约束

for i in range(10):
for j in range(10):
s.add(shudu_data[i][9] == 0)
s.add(shudu_data[9][j] == 0)

for i in range(9):
for j in range(9):
s.add(shudu_data[i][j] >= 1)
s.add(shudu_data[i][j] <= 9)


# 每行不能有相同
for i in range(9):
for j in range(9):
for k in range(j):
s.add(shudu_data[i][j] != shudu_data[i][k])

# 每列不能有相同
for i in range(9):
for k in range(9):
for k in range(i):
s.add(shudu_data[i][j] != shudu_data[k][j])

for i in range(9):
for j in range(9):
for kk in range(-2, 3, 1):
for mm in range(-2, 3, 1):
if (mm * kk) and (mm * kk != -4) and (mm * kk != 4):
if (kk + i >= 0) and (kk + i <= 8):
if (mm + j >= 0) and (mm + j <= 8):
s.add(shudu_data[i][j] != shudu_data[kk+i][mm+j])
else:
s.add(shudu_data[i][j] != shudu_data[kk+i][9])
else:
if (mm + j >= 0) and (mm + j <= 8):
s.add(shudu_data[i][j] != shudu_data[9][mm+j])
else:
s.add(shudu_data[i][j] != shudu_data[9][9])

for i in range(9):
for j in range(9):
for nn in range(-1, 2, 1):
for i1 in range(-1, 2, 1):
if i1 * nn == 0:
if (nn + i >= 0) and (nn + i <= 8):
if (i1 + j >= 0) and (i1 + j <= 8):
s.add(shudu_data[nn + i][i1 + j] != If(shudu_data[i][j] != 1, shudu_data[i][j] -1, If(shudu_data[i][j] != 9, shudu_data[i][j] + 1, -1)))
else:
s.add(shudu_data[nn + i][9] != If(shudu_data[i][j] != 1, shudu_data[i][j] -1, If(shudu_data[i][j] != 9, shudu_data[i][j] + 1, -1)))
else:
if (i1 + j >= 0) and (i1 + j <= 8):
s.add(shudu_data[9][i1 + j] != If(shudu_data[i][j] != 1, shudu_data[i][j] -1, If(shudu_data[i][j] != 9, shudu_data[i][j] + 1, -1)))
else:
s.add(shudu_data[9][9] != If(shudu_data[i][j] != 1, shudu_data[i][j] -1, If(shudu_data[i][j] != 9, shudu_data[i][j] + 1, -1)))

for i2 in range(9):
v47 = 0
v48 = 0
for i3 in range(9):
v47 |= 1 << (shudu_data[i2][i3] - 1)
v48 |= 1 << (shudu_data[i3][i2] - 1)
s.add(v47 == 511)
s.add(v48 == 511)

for i4 in range(3):
for i5 in range(3):
v52 = 0
for i6 in range(3):
for i7 in range(3):
v52 |= 1 << (shudu_data[i6+3*i4][i7+3*i5]-1)
s.add(v52 == 511)

s.add(shudu_data[4][2] < shudu_data[5][6])
s.add((shudu_data[4][2] * shudu_data[4][2]) - 3 * shudu_data[4][2] == - 2 )
s.add((shudu_data[5][6] * shudu_data[5][6]) - 3 * shudu_data[5][6]== - 2 )

result = s.check()
if result == sat:
print("Congratulations!")
anwser = s.model()
flag_arr=[]
for i in range(9):
for j in range(9):
flag_arr.append(anwser[shudu_data[i][j]].as_long())
print(''.join([str(x) for x in flag_arr]))
else:
print("try again~")

flag{483726159726159483159483726837261594261594837594837261372615948615948372948372615}

这道题之前有群友在群里问过师傅,师傅给出的思路是调试部分其实是在patch子进程,看懂程序如何patch然后自己去patch,set ip走子进程流程。自己,没有那样去写,但也是一个需要去研究的思路。就如参考文章里所说的:“我们是否能找到合适的patch点,让孙子进程再CreateProcess,并且不设置调试的运行关系,通过调试器attch来拿到解密后的代码。或者是通过API来使子进程退出调试状态或许也能达到预期的效果。”值得再去好好的思考

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2022-2023 Syclover.Kama
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信