CrackMe

本帖主要对练习Crackme的知识点与突破点进行记录。

翩若惊鸿

——题目来自看雪题库

无壳,窗口程序,随便输入一个name与key看程序的反馈。

有错误提示框,载入OD搜索相应的API,只是很多API序号,应该是MFC42库序号导出的函数。image-20200813120443310

那现在搜索字符串看看,或使用一个下断点的插件下断,然后查看调用堆栈信息找到API。这里2种方法均可。image-20200813121018755

向上回溯:image-20200813125708215

开始在OD中跟进004015E0函数,但很多MFC42中导出的函数,跟起来实在恼火。。还是结合ida的F5大法来看:

一开始逆向分析算法就发现了问题:image-20200813133718116

上面说明是个死胡同,这里就可以猜测是上面有对函数对402010处代码进行了重写。看前面的代码,果不其然:用了我们输入key的前4位来重写402010处代码,且最后一个校验(检测重写的代码是否正确),校验通过才将重写的代码拷贝到原地址处(0x402010)。image-20200813134136959

看了看重写与校验的代码,都不复杂,大多数可以直接复制到编译器来作为一个函数使用进而爆破出正确的重写的代码,也就是得到key的前4位。

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

int hash_table[0x401];

unsigned int sub_401500()
{
int v0;
unsigned int v1;
int *v2;
unsigned int result;
signed int v4;

hash_table[0x400] = 1;
v0 = 0x0EDB88320;
v1 = 0;
v2 = hash_table;
do
{
*v2 = v1;
result = v1;
v4 = 8;
do
{
result = ((result & 1) != 0 ? v0 : 0) ^ (result >> 1);
--v4;
}
while ( v4 );
*v2 = result;
++v2;
++v1;
}
while ( v1 < 0x400);

return result;
}

void decode_code(char *a1, int a2, int a3)
{
unsigned int result;

result = 0;
a3 ^= 0xD9EE7A1B;
if ( a2 )
{
do
{
*(result + a1) ^= *((char *)&a3 + (result & 3));
++result;
}
while ( result < a2 );
}
}

unsigned int sub_401550(int a1, char *a2, int a3)
{
int v3;
unsigned int i;

v3 = 0;
for ( i = ~a1; v3 < a3; ++v3 )
i = hash_table[(unsigned __int8)i ^ *(unsigned __int8 *)(v3 + a2)] ^ (i >> 8);
return ~i;
}

int main(void)
{
unsigned char ida_chars[] = {0x402010-0x402AC0的数据};

int s = 0x30303030, e = 0x7a7a7a7a, i = 0;
char decode_data[0xAB0];
clock_t start, end, total;

start = clock();
sub_401500();
for(i = s; i <= e; i++)
{
char *a = (char *)&i;
if(a[0] < 48 || a[1] < 48 || a[2] < 48 || a[3] < 48)
continue;
else if((a[0] > 57 && a[0] < 65) || (a[1] > 57 && a[1] < 65) ||
(a[2] > 57 && a[2] < 65) || (a[3] > 57 && a[3] < 65))
continue;
else if((a[0] > 90 && a[0] < 97) || (a[1] > 90 && a[1] < 97) ||
(a[2] > 90 && a[2] < 97) || (a[3] > 90 && a[3] < 97))
continue;
else if(a[0] > 122 || a[1] > 122 || a[2] > 122 || a[3] > 122)
continue;
memcpy(decode_data, ida_chars, 0xAB0);
decode_code(decode_data, 0xAB0, i);
if(sub_401550(0, decode_data, 0xAB0) == 0xAFFE390F)
{
printf("%X\n", i);
printf("%c%c%c%c\n", a[0], a[1], a[2], a[3]);
end = clock();
total = (end-start)/CLOCKS_PER_SEC;
printf("爆破所用时间:%d分:%d秒\n", total/60, total%60);
break;
}
}

return 0;
}

还挺快就得到了答案,主要是要优化去除不可能的字符。image-20200813135150617

以为到这里应该就结束了,解密出的代码就是一个简单的对key与name判断,算法会很简单。是我想多了🤣,说起这个算法我就是痛,花了好多时间动调猜测一些函数的作用,伪代码实在不好看,也不想看了,汇编就不说了。。

开始进入解密部分

写ida-python脚本对00402010开始的代码解密:

1
2
3
4
addr = 0x402010
s = [89, 63, -85, -97]
for i in range(0xab0):
PatchByte(addr+i, (Byte(addr+i)^s[i&3])&0xff)

来到解密后的函数:image-20200813141328038

由于最后要name计算出的整数与key计算出的整数相等,而name计算出的整数在OD的内存中十六进制存放,这是可以看到的。而key计算出数据时也是依次得到数据的每一位,然后*16转化一个整数。所以我们只要让key计算出的每一位与name计算后的数据的每一位相等即可。且16进制数只有“0123456789ABCDEF”这些数字,所以可以先得到一张这样的映射表。

模拟sub_4021A0与sub_402A40函数得到表:这里我对sub_402A40进行了简化,因为只有0-F的数字,所以只有这几个小正数情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

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

for(j = 0; j < 16; j++)
for(i = 0; i < 0xff; i++)
if(((i^0x86)-48) == j)
{
if(j == 0)
printf("{%#X", i);
else
printf(", %#X", i);
break;
}
printf("}");

return 0;
}

image-20200813144333400

最后我以计算name为Bxb的key,事先得到了Bxb计算出的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

int main(void)
{
char s[] = "1D702A7304A94";
int a[] = {0XB6, 0XB7, 0XB4, 0XB5, 0XB2, 0XB3, 0XB0,
0XB1, 0XBE, 0XBF, 0XBC, 0XBD, 0XBA, 0XBB, 0XB8, 0XB9};
int i = 0, j = 0;
char flag[100] = "BEEF";

for(i = 0; s[i]; i++)
{
int temp = s[i];
if(temp >= 48 && temp <= 57)
temp -= 48;
else
temp -= 55;
sprintf(flag+i*2+4, "%X", a[temp]);
}
puts(flag);

return 0;
}

image-20200813144918630

做到这,真的不想再看这个题了,我就想不通,伪代码怎么如此绕,key的生成算法都动调了好多遍。所以那个name计算的算法也真的不想看了。。。进而不能得到注册机。

其次这个题,对要改变数据的内存地址,传参都是使用ecx传参指向this*。

乘风破浪

——题目来自看雪题库

题目难点:题目开始启动双线程,线程之间以事件通讯。对于不清楚双线程,自然会有很多疑问,跟踪程序起来也会困难一些。

首先以不知道双线程的角度来跟踪程序:

查看程序中的获取控件文本内容的API,搜索发现没有GetDlgItemText,但是有GetWindowTextLenthW与GetWindowTextW。双击进入。image-20200731174647264

由于后面肯定会对输入数据进行验证处理,所以在数据窗口对该地址下硬件访问断点, F9运行断下后:image-20200731175348583

上面汇编代码很简单,就是比较输入的name与key是否相等且最后以这个来确定返回值,尝试打开程序看验证一下,不对的。继续跟着程序返回:既然上一个验证函数是错误的,下面这个箭头所指跳转就不能实现才正确。image-20200731175649160

修改标志寄存器后,单步分析401299以下的代码,很多无用的跳转,还是简单,一个验证:name与key的逆序是否相等且长度要不小于8。再次打开程序验证一下,正确。所以程序的key只要是name的逆序就正确。

当然我对程序还是有很多疑问,从出题人的帖子看到说是双线程。

开始去学习多线程的知识,觉得讲的很清楚的一个帖子:https://blog.csdn.net/LL596214569/article/details/89163734

了解多线程后,理解这个程序就很好了,首先是看是否有创建事件,查找API:CreatEvent。在创建线程API:CreatThread下断点,从CreatThread找到线程函数的地址。接着就是下图中的函数:image-20200731182155636

最后,所以程序的整体逻辑就是首先创建2个事件,再创建2个线程,2个线程函数都有WaitForSingleObject函数一直等待事件的信号后再向下继续执行。而我们输入name与key后点击Ok就会发生按钮事件,触发OS向程序发现消息,然后执行SetEvent函数将指定线程的WaitForSingleObject函数等待的指定事件设置为有信号状态。image-20200731183904491

接下来在该线程进行验证name与key,如果验证通过的话设置另外一个线程等待的指定的事件设置为有信号状态,另一个线程开始执行显示提示正确的对话框的代码。简而言之:一个线程负责验证,一个线程负责显示。

渐入佳境

——题目来自看雪题库

运行一下程序,有错误输入提示框。

上手直接找MessageBox,到达代码处向上分析:所以我们要向上回溯找到对ebp+c赋值处。image-20200801132230026

继续向上回溯可以看到:image-20200801132543497

所以下面跳转的地方就是验证过程:算法很简单。image-20200801132628646

CrackMe01

——题目来自“百度杯”CTF比赛十一月场

还是上手载入OD,找一下获取输入文本相关的API:image-20200805164512873

开始只看到第一个,下断后在程序输入内容后却没有断下。还以为又是多线程,分析一波后确实创建了一个线程,但不对。后面才在第二个GetWindowTextW下断,输入内容后程序断下。

单步跟踪后发现,将输入的字符传到另外一个内存空间存储后,再调用PostMessageW函数将输入字符传给指定句柄的窗口。这里打开ida看下:image-20200805170315887

之后可以直接查找默认消息处理函数:DefWindowProc 来定位处理输入字符的代码处。

这里看PostMessage函数第一个参数的交叉引用来定位目的代码。因为hWnd是接收消息的窗口句柄,我们看个窗口的句柄赋值给了hWnd。hWnd是全局变量。

从交叉引用来到下面的函数:image-20200805172125636

可以看到,使用了WinClass定义了一个窗口的类,对每个属性赋值后使用RegisterClassW来注册窗口,最后通过CreatWindowExW将该窗口实例化,并此窗口句柄赋值给Hwnd。所以我们输入的字符串是发送给了这个窗口。

另外,WndClass中的成员lpfnWndProc指向一个回调函数,是窗体的消息处理函数。它接受到PostMessageW发送来的消息后即调用。

进入该窗口消息处理函数:image-20200805172955592

从上面函数知道,虽然不知道输入的具体字符串是什么,但是可以爆破出累加和,且不大于17*122 = 2074(因为输入字符长度<= 17)。得到累加和我们就能知道,窗口显示出的是什么内容了。

写程序收工:

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

int main(void)
{
int i = 0, j = 0, k = 0;
unsigned char ida_chars[] =
{
0xF0, 0x04, 0xDA, 0x04, 0xD7, 0x04, 0xD1, 0x04, 0x8C, 0x04,
0xFF, 0x04, 0xF5, 0x04, 0xFE, 0x04, 0xE3, 0x04, 0xF8, 0x04,
0xE7, 0x04, 0xFF, 0x04, 0xE3, 0x04, 0xE9, 0x04, 0xF0, 0x04,
0xF3, 0x04, 0x85, 0x04, 0x80, 0x04, 0x84, 0x04, 0xF2, 0x04,
0xF4, 0x04, 0xF3, 0x04, 0x00, 0x00, 0x00, 0x00
};
char flag[100] = {0};

for(i = 0; i < 2074; i++)
if(((i&0xf00) == 1024) && ((i&0xf0) == 0xb0) && ((i&6) == 6))
{
for(j = 0; j < 44; j += 2)
{
flag[k++] = *(short int *)&ida_chars[j]^i;
}
puts(flag), k = 0;
strcpy(flag, "");
}

return 0;
}

最终得到:即第一个满足。image-20200805173523092

dame

——题目来自DAMN’s Official joining Contest(是一个机构的加入测试,将程序指定破解并写出注册机)

题目链接:https://pan.baidu.com/s/1y0pznGu6qEb1gut2M0tXog 提取码:h0z9

要求:大按钮和标题显示未锁定的消息并且程序说按钮被按下时未锁定与注册机的编写。image-20200806181244476

载入OD发现有壳,直接堆栈平衡原理把壳带走,然后dump出无壳文件。

由于我们要修改对话框模块上的内容,所以首先找到创建对话框函数。OD载入在程序入口点即可看到:DialogBoxParamAimage-20200806181904819

可以看到上图中的对话框过程函数的起始地址:401045,该处代码好像没有识别出来,且下断有提示并且执行很奇怪。载入ida中看看:image-20200806182148079

所以enter在OD中没有识别出来,且401035-401045确实数据,401049才是代码开始,所以返回OD进行处理,注意不能nop,因为数据在后面有使用到,直接下断401049。

接下来,不断单步调试知道,DialogProc函数根据不同的消息做出相应的响应。首先创建整个对话框模板,然后不断载入各种控件。image-20200806182822916

OD调试后,在IDA中看指定代码段:可以看到dword_402313处的值决定了控件的显示内容和样式。image-20200806183539623

找到dword_402313赋值处:是对程序部分的代码的校验,检测是否下断点。image-20200806183725134

虽然知道了dowrd_402313处的值影响对话框的显示,但是什么样的值才能达到我们的目的呢,回到OD动调。image-20200806185407720

由于程序中存在代码校验,多次调试发现,当下断后[402313] = 1,为下断点[402313] = 0。 继续调试:image-20200806185813389

到这里就清楚了,[402313]的值还决定了显示字符串的偏移位置,当[402313] = 1时,字符串为:DAMN’s TryMe -CRACKED! 而它后面一个字符串就是:UNLOCKED!所以推出:[402313]的值要等于2。

重新载入,找到合适位置在内存窗口修改[402313]的值为2,然后运行。image-20200806190419665

对话框弹出的内容仍然不是UNLOCKED!ida中OD都可以找到MessageBoxA看一下。这里使用OD:image-20200806190848568

从上面看到,[402308]的值会决定对不对[402313]的值修改,所以对[402308]的值修改:成功。最后要让程序之后也可以这样运行,简单的patch一下即可。

下面开始找注册算法,疑惑的是Register按钮为什么不能点击?难道还要对程序打补丁什么的,不熟悉Win32编程脑子还是一片空白。

但是机缘巧合下,让我找到了这里面的玄机。在OD中找到获取我们输入字符串的函数后,条件反射下了断。随便点击了下程序的输入文本框,程序突然在OD中断了下来。。且看到下面的一个EnableWindow函数,百度下功能:设置窗口的可用性,即我们程序的Resiter按钮是否可用。image-20200806193140672

多次测试,发现第一个GetDlgItemTextA是获取name字符串,第二个获取key。且每输入一个name字符就调用一次第一个GetDlgItemTextA函数将该字符存入指定的内存区域,返回字符长度;每输入一个key就调用第二个GetDlgItemTextA函数将其存入指定内存区域,返回字符长度,并调用004012F3函数(应该是name与key的验证过程),最后通过004012F3函数的返回值来执行EnableWindow函数,以此决定Register按钮的按钮是否可用。

输入数据,下断004012F3并进入:image-20200806233948875

到此,结束对程序的分析。最后将每个十六进制数转化为对应的字符的汇编指令还要多看看:sbb al, 69;das;。这里al = 0xE,sbb带借位减法后:al = al-0x69-CF = 0xAE。

das:组合(压缩)BCD码的加法调整指令。功能:如果AL低四位>9或AF=1 ,则AL的值减06h,并置AF=1;如果AL高四位>9或CF=1 ,则AL的值减60h ,且置CF=1;

所以这里:al = al-0x60 = 0x45。而0x45就是’E’字符的ASCII。

最后C语言简单写个注册机程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <math.h>

int main(void)
{
char name[100] = {0};
unsigned int ans = 0x44414D4E, i = 0, temp = 0;

printf("name:");
gets(name);
while(name[i])
{
temp += name[i];
ans = ((ans >> 1) | ((ans&1) << 31));
ans = ((ans >> i) | ((ans&((int)(pow(2, i)-1))) << 32-i));
ans ^= temp;
i++;
}
ans |= 0x10101010;

printf("%X", ans);

return 0;
}

跃跃欲试

——题目来自看雪题库

题目加了Aspack壳,直接OD中堆栈平衡原理脱壳。

找到获取用户输入信息的API,来到验证过程:首先是下断点的4个函数对email_address验证。image-20200810111355447

动态很多次,始终没调出这几个函数的功能且代码逻辑也奇怪。。。

题目的要求只要后面serial,或许serial与email_address与serial没有关系,干脆直接跳过email_addree设置新的EIP,来到验证serial处:image-20200810111950081

果然,serial与email_address没有关系,serial的验证也只是简单的加减法。就是整理起来有点麻烦。

最后载入IDA看看伪代码呢。原来是C标准库里查找子字符串的函数。image-20200810112350707

一知半解

——题目来自看雪题库

从这个CM认识了一个新壳:PEcompact

载入OD仍然使用堆栈平衡原理脱壳,但对这个壳好像不适用。。。认为自己找到了OEP,dump后发现打不开。

来到看雪论坛找师傅们脱这个壳的方法,先是使用了CCDebuger大师傅的脱壳脚本,真的是强,算是脱壳机了。脱壳后发现和我找到的OEP是一样,那我的应该是没有修复输入表导致打不开。

后面又看到了一位师傅脱这个壳的帖子,里面这样说道:

外壳完整地保留了输入表,外壳加载时没有对IAT加密;外壳解压数据时,完整的输入表会在内存中出现,然后外壳用显示加载DLL的方式获得各个函数的地址,并将该地址填充到IAT中。

所以我回到OD换了第二个dump的方式:image-20200811111903851

哈哈,还没有用importRec就能成功打开,只不过这个也看情况。

另外记录一下用esp定律脱这个壳不一样的地方:首先下硬件访问断点。image-20200811112234416

F9后,断在程序的领空:可以看到并不是从堆栈pop出下断的数据断下的,所以继续F9。image-20200811112818716

后面遇到同上处理,直到看到对堆栈访问。不久看到来到OEP标志。image-20200811113028787

最后,程序的逆向很简单。

-------------本文结束感谢您的阅读-------------