2021数字中国创新大赛虎符网络安全

第二届虎符CTF,分享一下逆向题解。

链接:https://pan.baidu.com/s/15CH8WMsHoaRmMeIflV3Llg
提取码:z531
复制这段内容后打开百度网盘手机App,操作更方便哦

redemption_code

32位mips架构动态链接的程序。

image-20210403195047229

因为动态链接运行要指定库路径,这里就直接静态分析,这个题也足够了。

首先看到对输入字符串处理的第一个函数pre:判断长度后,一个字符串赋值操作,一起传入check。

image-20210403200825059

然后server_check_redemption_code函数:创建14个表,然后我们的输入的字符串作为index在其中以此递增的做标记。最后用上面赋值的字符串作为index依次查表,直到到最后到达最后一个表才成功。

image-20210403201314116

上面如果不好理解的话,可以把要得到的表打印出来看一下就清楚了。

所以说,要使第一个函数不返回-1,只要我们的输入为赋值字符串的前14个就好了。Ninja Must Die

紧接着,又是一个字符串赋值操作加一个check()函数,与前面不同的是这个check()函数的返回值要为7,计算可以知道也就是check()函数最后要寻找21长度的字符串。而赋值的字符串也变了,如何让我们的输入字符串满足2个check()函数呢。

观察一下第二个字符串的前21位:I Love Ninja Must Die,它的后14位正好和第一个check()函数要满足的一样,而它的前7位可以跳过的,也正好满足了返回值要为7。

所以最后的flag就是:flag{Ninja Must Die}

CrackMe

程序应该是realse模式编译的,代码都杂合到一起了。做题时注意识别关键代码。

来到main函数,首先就看到了输入和长度判断,明显的stl模板的string结构。

image-20210403204000350

动调看一下这个结构:

image-20210403205021175

后面一直到下一次输入的的地方,动态分析下可以快速知道是将我们的输入前7位和后10位分为了2个string来存储。注意看内存。

image-20210403205837005

接下来关键是第二个输入后进行浮点数运算的地方,输入数据经过浮点运算对比正确后继续进行后面的分支。

image-20210403210353368

我开始直接跳过了这个判断,先看看后面做了什么。

分析得到下面是用输入的前7位和第二次的输入变换的得到的数据进行异或,

image-20210403211320207

继续看下面,可以看到输入的rc4结构,且把我们的前7位做了key:

image-20210403212129295

到这里就清楚程序的整体流程了。

输入位17位,前7位为key,后10位为明文。第二次输入经过浮点运输后,经过判断,正确的话将其经过有规律变换后与第一次输入的前7位进行异或运算,然后与编码的enc对比,正确的话继续后面的rc4加密。

所以现在就是求出第二次输入一个int数据。

开始想的用angr来跑局部,但不熟练😢,换用复制ida中的代码到vs中运行,爆破出这个int数据。

注意一下__m128这个变量:

image-20210403232640041

爆破代码:

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
#include <iostream>
#include <stdio.h>
#include <emmintrin.h>

__m128 __fastcall sub_7FF64A4E1360(double a1, double a2)
{
double v2; // xmm7_8
__m128 v3; // xmm6

v2 = a1;
a1 = pow(a1, a2 - 1.0);
*((double*)&v3 + 1) = *(&a1 + 1);
*(double*)&v3 = a1 / exp(v2);
return (__m128)v3;
}

int main()
{
double v96 = 100;
double v16 = 0.0;
double v17 = 0.0;
double v18 = 0.0;
double v19;
int v20;
double v21, v22;

for (int i = 0; i < 0xffffffff; i++)
{
double v16 = 0.0;
double v17 = 0.0;
double v18 = 0.0;
v96 = i;
v19 = (double)((int)v96 / 12379) + 1.0;
do
{
v17 = v17 + *(double*)sub_7FF64A4E1360(v18, v19).m128_u64 * 0.001;
v18 = v18 + 0.001;
} while (v18 <= 100.0);
v20 = (int)(v17 + v17 + 3.0);
v21 = 0.0;
v22 = (double)((int)v96 % 12379) + 1.0;
do
{
v16 = v16 + *(double*)sub_7FF64A4E1360(v21, v22).m128_u64 * 0.001;
v21 = v21 + 0.001;
} while (v21 <= 100.0);

printf("%d\n", i);
if (v20 == 0x13B03 && (int)(v16 + v16 + 3.0) == 0x5A2)
break;
}
}

虽然不是很快,但还是跑出来了:99038

接下来动态得到要与第一次输入前7位进行异或的数据。开始是99038,后面都是乘2后的字符串。

image-20210403213606134

进行异或解密:

1
2
3
4
5
>>> s = [57, 57, 48, 51, 56, 49, 57]
>>> t = [8, 77, 89, 6, 115, 2, 64]
>>> ans = [s[i]^t[i] for i in range(7)]
>>> bytes(ans)
b'1ti5K3y'

这就得到了key。

然后就是进行rc4解密,这个直接动调修改输入数据为密文数据进行一次加密变得到明文了。

image-20210403214259089

image-20210403214337600

最后输入得到flag:flag{1ti5K3yRC4_crypt099038}

再看题

因为当时做题时,没有注意细节或者方法什么的,哪里爆破的很慢,现在是我后面对这个题再次学习写下的。

首先是对__m128数据类型的重新认识,它是一个联合,这里它的成员:

image-20210419214224132

所以题目中的.m128_u64就是取低8字节。

image-20210419214254776

exp()是求e的次方的值。pow(a1, a2-1)/exp(a1)

image-20210419214354418

另外这里是除12379,第一次是用商,第二次用余数,所以这里爆破的步长应该是每次加12379,确定了商,再确定余数就很快了。。

image-20210419214519931

正确的爆破脚本:

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
#include <stdio.h>
#include <math.h>

int main(void)
{
int i, ans;
double a = 0, b = 0, c;

for(i = 0; i < 0xffffffff; i += 12379)
{
a = 0, b = 0;
c = i/12379;
do
{
a = a + (pow(b, c)/exp(b))*0.001;
b += 0.001;
}while(b <= 100);

ans = (int)(a+a+3);
if(ans == 80643)
{
printf("found: %d\n\n", i);
break;
}
else
printf("%d\n", i);
}

for(i = 0; i < 12379; i++)
{
a = 0, b = 0;
c = i;
do
{
a = a + (pow(b, c)/exp(b))*0.001;
b += 0.001;
}while(b <= 100);

ans = (int)(a+a+3);
if(ans == 0x5A2)
{
printf("found: %d\n", i);
break;
}
else
printf("%d\n", i);

}

}

其次还有一种用使用idapython来爆破,实质就是调用ida调试器中的函数来控制程序的流程和寄存器值等,为了让自己用的函数有一个统一规范,我统一使用ida_dbg模块的函数(idapython封装过的其实也不错):运行脚本直接得到结果。

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
from ida_dbg import *
from ida_bytes import*

def init():
rsp = get_reg_val("rsp")
set_reg_val("rip", 0x140001768)
patch_qword(rsp+0x40, i)

i = 0
run_to(0x140001658)
wait_for_next_event(WFNE_SUSP, -1)
init()

while(True):
print(">>> i: %d"%i)
run_to(0x14000184E)
wait_for_next_event(WFNE_SUSP, -1)
ebx = get_reg_val("ebx")
eax = get_reg_val("eax")
if ebx != 80643:
i += 12379
init()
elif eax != 1442:
i += 1
init()
else:
print(">>> find: %d"%i)
break
exit_process()

image-20210419222558380

感觉idapython里面的函数真的繁杂。。同一个功能各个模块都有。但自己要有个规范。

记一个idapython文档,需要什么都有,直接查就完事了:idapython_docs

Golang encrypter

64位,go语言写的程序,经过符号表还原后,虽然代码还是难看,但有了大多数函数名称还是好很多的。

对输入的进行的第一个检查函数:main_check(),首先判断输入字符串长度,然后引入一个正则表达式对flag的格式进行了过滤,最后提取出每部分的字符串进行连接,最后再进行一个hex()操作。

image-20210403215520889

后面开始用密钥初始化一个密钥对象,从密钥长度开始还是以为aes加密,测试后发现并不是,,

关键是这里的加密还是8个一组,分2次加密,且密钥长度又是16,这就排除aes和des加密了。

然后是从myCipher字符串让我想到这应该是作者自己写的一个加密,进而进入加密函数分析:
首先从动调分析知道下面这个函数就是交换数据字节顺序,_byteswap_ulong(),这里开始调用了2次,结束调用了2次还原。分为2组,正好取了8个字节。

image-20210403220950328

接着就是关键加密模板了,说实话,难看,其实就是魔改的tea加密,分析数据时细心点。。

image-20210403221308523

把上面的加密化简一下,得到:

1
2
enc[0] += (enc[1] + ((enc[1]>>5)^(16*enc[1]))) ^ (key[v23]+v22);
enc[1] += (enc[0] + ((enc[0]>>5) ^ (16*enc[0]))) ^ (key[v25]+v22+0x12345678);

最后就是解密:注意细心吧。。这里因为加号运算符优先级大于异或,我又忘了在他们一起时给异或加一个括号,一直解密出错,真就硬看了二个小时。。。1个括号,2个小时。。

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
#include <stdio.h>

int opcode[100000];

unsigned char KEY[] = {3, 2, 1, 0, 7, 6, 5, 4, 11, 10,
9, 8, 15, 14, 13, 12};

unsigned char ENC[] = {14, 195, 17, 240, 69, 199, 154, 243, 237, 245,
217, 16, 84, 39, 2, 203};

void tea_decode()
{
int i = 0, j = 0;

unsigned int *key = (unsigned int *)KEY;
for(i = 0; i < 2; i++)
{
unsigned int *enc = (unsigned int *)(ENC+8*i);
for(j = 31; j >= 0; j--)
{
enc[1] -= (enc[0] + ((enc[0]>>5) ^ (16*enc[0]))) ^ (key[opcode[3*j+2]]+opcode[3*j]+0x12345678);
enc[0] -= (enc[1] + ((enc[1]>>5) ^ (16*enc[1]))) ^ (key[opcode[3*j+1]]+opcode[3*j]);
}
}
}

void swap(unsigned char enc[])
{
int i = 0;

for(i = 0; i < 2; i++)
{
unsigned char tmp = enc[i];
enc[i] = enc[3-i];
enc[3-i] = tmp;
}
}

int main(void)
{
int i = 0;
int v17 = 0, v18 = 0, v22, v23, v25;


while(v17 < 32)
{
v22 = v18;
v23 = v18&3;

v18 = (unsigned int)(v22+0x12345678);
v25 = ((unsigned int)v18 >> 11) & 3;

opcode[v17*3] = v22;
opcode[v17*3+1] = v23;
opcode[v17*3+2] = v25;
v17++;
}

for(i = 0; i < 16; i += 4) //交换字节序
{
swap(ENC+i);
}

putchar(10);
tea_decode();

for(i = 0; i < 16; i += 4) //交换字节序
{
swap(ENC+i);
}

printf("flag{");
for(i = 0; i < 16; i++)
{
if(i == 4 || i == 6 || i == 8 || i == 10)
putchar('-');
printf("%x", ENC[i]);
}
printf("}");

return 0;
}
//flag{3bbcf9ea-2918-4fee-8a2e-201b47dfcb4e}
-------------本文结束感谢您的阅读-------------