GWCTF 2019 - xxor 笔记与思考

First Post:

Last Update:

插图ID : 85072434

对我这种新手来说算是比较怪异的一题了,故此记录一下过程。

解题过程:

    直接放入IDA,并找到main函数,得到如下代码(看了一些其他师傅的WP,发现我们的IDA分析结果各不相同,最明显的就是HIDOWRD和LODWORD函数,该差异将在下文分析)

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
__int64 __fastcall main(int a1, char **a2, char **a3)
{
int i; // [rsp+8h] [rbp-68h]
int j; // [rsp+Ch] [rbp-64h]
__int64 v6[6]; // [rsp+10h] [rbp-60h] BYREF
__int64 v7[6]; // [rsp+40h] [rbp-30h] BYREF

v7[5] = __readfsqword(0x28u);
puts("Let us play a game?");
puts("you have six chances to input");
puts("Come on!");
v6[0] = 0LL;
v6[1] = 0LL;
v6[2] = 0LL;
v6[3] = 0LL;
v6[4] = 0LL;
for ( i = 0; i <= 5; ++i )
{
printf("%s", "input: ");
a2 = (v6 + 4 * i);
__isoc99_scanf("%d", a2);
}
v7[0] = 0LL;
v7[1] = 0LL;
v7[2] = 0LL;
v7[3] = 0LL;
v7[4] = 0LL;
for ( j = 0; j <= 2; ++j )
{
tmp1 = v6[j];
tmp2 = HIDWORD(v6[j]);
a2 = &unk_601060;
sub_400686(&tmp1, &unk_601060);
LODWORD(v7[j]) = tmp1;
HIDWORD(v7[j]) = tmp2;
}
if ( sub_400770(v7, a2) != 1 )
{
puts("NO NO NO~ ");
exit(0);
}
puts("Congratulation!\n");
puts("You seccess half\n");
puts("Do not forget to change input to hex and combine~\n");
puts("ByeBye");
return 0LL;
}

逻辑分析:

    分别输入 六个字符串 ,作v6用于储存输入,v7用于储存结果

    在一个for循环中,将v6的数据一个个保存入tmp,并根据unk_601060的密码表进行sub_400686函数加密并放入v7

    在sub_400770函数中比较 v7 和结果是否吻合(多余参数a2为IDA分析差错的结果,此处忽略不影响解题)

    首先进入sub_400770以获取结果

1
unsigned int a1[6] = { 3746099070,550153460, 3774025685 ,1548802262 ,2652626477 ,2230518816 };

注意点①

    刚入门逆向的臭毛病是习惯Hide casts隐藏指针以清晰代码方便阅读的,但在本题中,倘若不留意类型而直接隐藏,在IDA窗口中将得到这样的数据:

1
2
3
4
5
6
//未隐藏指针的代码:注意到 v7 应该是一个unsigned int 数组
if ( (unsigned int)sub_400770(v7, a2) != 1 )
{
puts("NO NO NO~ ");
exit(0);
}

1
2
3
4
5
6
if ( a1[2] - a1[3] == 2225223423LL
&& a1[3] + a1[4] == 4201428739LL
&& a1[2] - a1[4] == 1121399208LL
&& *a1 == -548868226
&& a1[5] == -2064448480
&& a1[1] == 550153460 )

    显然,这些数据并不是标准的unsigned int类型,在获取这些数据时应从汇编窗口逐个获取并计算,且存放数组使用相应的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.text:00000000004007D0                 mov     [rbp+var_8], rax
.text:00000000004007D4 mov eax, 84A236FFh
.text:00000000004007D9 cmp [rbp+var_18], rax
.text:00000000004007DD jnz short loc_400845
.text:00000000004007DF mov eax, 0FA6CB703h
.text:00000000004007E4 cmp [rbp+var_10], rax
.text:00000000004007E8 jnz short loc_400845
.text:00000000004007EA cmp [rbp+var_8], 42D731A8h
.text:00000000004007F2 jnz short loc_400845
.text:00000000004007F4 mov rax, [rbp+var_28]
.text:00000000004007F8 mov eax, [rax]
.text:00000000004007FA cmp eax, 0DF48EF7Eh
.text:00000000004007FF jnz short loc_400834
.text:0000000000400801 mov rax, [rbp+var_28]
.text:0000000000400805 add rax, 14h
.text:0000000000400809 mov eax, [rax]
.text:000000000040080B cmp eax, 84F30420h
.text:0000000000400810 jnz short loc_400834
.text:0000000000400812 mov rax, [rbp+var_28]
.text:0000000000400816 add rax, 4
.text:000000000040081A mov eax, [rax]
.text:000000000040081C cmp eax, 20CAACF4h

    来到进行加密的for循环处:

1
2
3
4
5
6
7
8
9
for ( j = 0; j <= 2; ++j )
{
tmp1 = v6[j];
tmp2 = HIDWORD(v6[j]);
a2 = (char **)&unk_601060;
sub_400686(&tmp1, &unk_601060);
LODWORD(v7[j]) = tmp1;
HIDWORD(v7[j]) = tmp2;
}

    可以注意到,IDA中并没有为tmp1、tmp2声明变量(实际上,它们本不是这个名字,但为了方便阅读而被我改成了这个名字;从汇编窗口可以知道它们均为4个字节的变量(int))

1
2
3
4
unsigned int a1[6] = { 3746099070,550153460, 3774025685 ,1548802262 ,2652626477 ,2230518816 };
int tmp1, tmp2;
tmp1 = LODWORD(a1[0]);// -548868226
tmp2 = HIDWORD(a1[1]);// 550153460

    如上代码展示了LODWORDHIDWORD的结果,乍一看似乎相当不同,但实际上这不过是一种比较别扭的写法罢了

    注意到tmp2的结果和a1[1]相同,而将a1[0]的类型换为int之后也将得到与tmp1相同的结果,也就是说,这两个函数并没有起到任何作用,只是做了简单的赋值罢了

    (尽管我想说具体问题具体分析,但倘若使用的是LOBYTEHIBYTE的话,结果就将彻底不同了。但通常来说,出题人并不会特地去这样写,至少一般来说,并没有LODWORD这样的函数)

注意点②:

1
.text:0000000000400984                 add     [rbp+var_64], 2

    该汇编代码为for循环中对变量 j 的操作

    在C伪代码中可以看见为 **j++**,而在汇编中的结果显然应该是 j+=2,所以过于依赖伪代码的话在编写解密脚本时将遇到麻烦

    因此我们可以知道,这个循环每次获取 v6 中的两个进行加密并放入

    最后是加密函数本身:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__int64 __fastcall sub_400686(unsigned int *a1, _DWORD *a2)
{
__int64 result; // rax
unsigned int v3; // [rsp+1Ch] [rbp-24h]
unsigned int v4; // [rsp+20h] [rbp-20h]
int v5; // [rsp+24h] [rbp-1Ch]
unsigned int i; // [rsp+28h] [rbp-18h]

v3 = *a1;
v4 = a1[1];
v5 = 0;
for ( i = 0; i <= 0x3F; ++i )
{
v5 += 1166789954;
v3 += (v4 + v5 + 11) ^ ((v4 << 6) + *a2) ^ ((v4 >> 9) + a2[1]) ^ 0x20;
v4 += (v3 + v5 + 20) ^ ((v3 << 6) + a2[2]) ^ ((v3 >> 9) + a2[3]) ^ 0x10;
}
*a1 = v3;
result = v4;
a1[1] = v4;
return result;
}

    (应该记得,形参a1为输入流v6,a2为加密表{2,2,3,4}(DWORD类型数组每4字节一个,应将中间的0省略))

    分别获取 v3为第一个数组,v4为第二个数字,v5为一个轮替变量

    经过一个for循环后,将结果放回原数组

    通过如上分析,应该就能写出差不多的解密脚本了,但还是有一些细节,这里也不好再多叙述,便就此打住吧

解密脚本:

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
#include<iostream>
using namespace std;
int main()
{
unsigned int a1[6] = { 3746099070,550153460, 3774025685 ,1548802262 ,2652626477 ,2230518816 };
unsigned int table[4] = { 2,2,3,4 };
unsigned int decode[6];
int v5 = 1166789954 * (0x3F+1);
unsigned int v3, v4;

for (int i = 0; i <= 5; i+=2)
{
int v5 = 0x458BCD42 * 64;
v3 = a1[i];
v4 = a1[i + 1];
for (int j = 0; j <= 0x3F; j++)
{
v4 -= (v3 + v5 + 20) ^ ((v3 << 6) + table[2]) ^ ((v3 >> 9) + table[3]) ^ 0x10;
v3 -= (v4 + v5 + 11) ^ ((v4 << 6) + table[0]) ^ ((v4 >> 9) + table[1]) ^ 0x20;
v5 -= 0x458BCD42;
}
decode[i] = v3;
decode[i + 1] = v4;
}
for (int i = 0; i < 6; i++)
{
printf("%x", decode[i]);//666c61677b72655f69735f6772656174217d
}
}


注意点③:

    最终得到的decode数组便是flag,但由于VS默认显示为10进制数,所以应该将结果输出为16进制数并另外进行转换

1
unsigned int decode[6]={6712417, 6781810, 6643561, 7561063, 7497057, 7610749};