新160个CrackMe分析-第5组:41-50(上)

2022-10-12 23:51:50 浏览数 (2)

作者:selph

​目录:

041-genocidel1

042-crackme2

043-riijj_cm_200411213

044-tsrh-crackme4

045-CyTom-crackme5

046-keyme16

047-surre7

048-monkeycrackme18

049-THraw-crackme89

050-daxxor10

1. 041-genocidel

算法难度:⭐⭐⭐

爆破难度:⭐

信息收集

运行情况:

弹窗提示说这里应该有个Reg.dat文件

创建这个文件之后就能打开页面了:

查壳与脱壳:

UPX壳,ESP定律即可脱壳

调试分析

OK按钮按下的时候:直接弹窗Success

但是这个OK按钮按不下去,应该是验证在别的地方

这里还有一个事件就是Serial的四个框发生值改变的时候:

会调用同一个函数,这个函数应是校验了,参数是第几个框的标识

这个函数一进去就是一个switch分支,分别对应四个框处理

四个框的处理相同:都是把值保存在一个数组里,然后调用同一个函数

这个调用的函数才是真正验证的部分:

首先获取Name部分,Name长度需要大于等于5,这里分别取1345四个字符除以10保存到一个数组里

然后接下来就是一个循环:这个循环的作用是刚刚那个数组里保存的值是否为个位数,如果不是,就再次除以10,得到一个个位数的值

最后,又是一个循环,依次比对刚刚保存的四个值,和上一层函数从四个框里保存的四个值是否相同,如果都相同就启用OK按钮,否则就不启用

注册机

注册码生成算法:(CSharp)

string Name = Console.ReadLine(); if (Name == null) return; for (int i = 0; i < Name.Length; i )     if(i is 0 or 2 or 3 or 4)     {         int tmp = (int)Name[i];         if (tmp >= 100) tmp /= 100;         else if (tmp >= 10) tmp /= 10;         Console.Write(tmp " ");     }

效果:

总结

编辑框更变事件里验证,首先把编辑框4个值保存起来,然后用Name计算4个值,然后进行比对,4个值都比对成功,就成功

比之前遇到的那些还算有了点新意

2. 042-crackme

算法难度:⭐⭐⭐⭐

爆破难度:⭐⭐

信息收集

运行情况:

直接打开说需要一个dat文件

创建了之后,说你确定这是正确的keyfile?感觉意思是启动时验证,根据文件内容验证

查壳与脱壳:

无壳:C 程序

调试分析

C 程序就抄起IDA干!

首先是创建对话框,这里主要进这个窗口函数去看

这里是窗口过程函数,根据不同的消息码有一大堆分支,这里只有一个分支调用了自定义的函数:

上面一堆无关紧要的部分,这个函数很可疑

这就是个check函数了:

首先是一堆变量初始化工作,就跳过不管了,程序首先是通过pushpopedi赋值了初值:3

然后紧接着就是打开文件key.dat,打开失败(文件不存在)就显示提示信息,如果有这个文件就往下跳过

往下就是读取文件内容的操作,这里会判断读取的字节数,如果为0则意味着没内容,就跳到能通往Fail的程序路线

接下来读取到内容了之后,有两个循环:

循环1:首先是使用读取的字节数对每一个字节进行一次异或操作

接下来对于前三个字节,额外再异或一个魔数,分别异或0x540x4d0x47

循环2:三个字节为一组,向后异或,142536的值进行异或,然后475869的值进行异或,依次进行下去,处理完输入的长度,因为是三个一组,所以最后可能长度比输入的长度长,需要手动在字节数组结尾赋值0来截断

接下来:获取一个地址,405030,这是个字符数组(注意这个值!!后面有用到)

然后使用我们的输入的前三个字节,循环去异或这个字节数组,依然是3个一组,对于字符数组,每三个字节,都分别与输入的前三字节异或一遍,直到字符数组的结尾标识FF出现

跳出循环之后,是第一层校验:前三个字节的乘积为0x2A8BF4

如果不是,则就显示提示信息说我们的输入是错误的

接下来紧接着又是一个循环,遍历输入的值,复制到一个缓冲区里,结束标志是出现0x20,给缓冲区的字符串一个00结尾(实际上这一段是在定位UserName

接下来是另一个循环,循环遍历的对象依然是我们的输入,这个循环的起点是上一个循环的终点,在0x20之后开始,把每一个字节都复制到另一个局部变量里[ebp ver_58](实际这就是解密出来的用户名)

接下来是一个神奇的VirtualProtect调用,可能有人觉得到前面验证三数乘积的时候就结束了,实际上并没有,之前对一个字符数组进行了大量的异或,然后这里使用VirtualProtect函数给字符数组修改了内存属性,然后直接call了这个字符数组的地址

合着这字符数组是shellcode啊,然后那一堆异或是解密操作!

关于shellcode的功能,就不详细分析了,具体功能是生成字符串Registered to: 然后拼接用户名

到这里就分析完了验证流程,接下来该写注册机了,接下来分析注册机怎么写

注册机分析

这里验证如下:

a. 经过一系列异或之后的前三字节的乘积为固定值

b. 前三个字节可以用于解密shellcode,解密的结果可执行

c. 输入的字符里存在分隔符,分隔符经过一系列异或之后的值为0x20,分隔符之后的内容则是UserName,经过一系列异或解密出UserName,如果没有这个分隔符则会奔溃退出程序

首先处理第一个验证,三个值的乘积为固定值,这三个值是输入的前三字节经过已知数值的异或而得到,所以这个计算是可逆的;处理思路是找到满足要求的三字节,然后反推找到原始的前三字节,数据量不大,简单粗暴处理:

void getvalue() {     for (size_t i = 0; i < 0xff; i )         for (size_t j = 0; j < 0xff; j )             for (size_t k = 0; k < 0xff; k )                 if (i * j * k == 0x2A8BF4)                     std::cout << i << " " << j << " " << k << std::endl;;

得到一组结果:

unsigned char val[] = { 85, 139, 236, 85, 236, 139, 118, 139, 170, 118, 170, 139, 139, 85, 236, 139, 85, 236, 139, 118, 170, 139, 170, 118, 139, 236, 85, 170, 118, 139, 170, 139, 118, 236, 85, 139, 236, 139, 85 };

接下来使用这组结果去进行异或反推得到新的一组输入值:

每个字节会异或字符数组对应的字节,然后异或固定的值,然后异或读取字符长度

int main() {     //字符数组的前三字节:0x1E, 0xBF, 0xA2     for (int i = 0; i < 39; i =3)     {         int len = 3;         val[i] ^= 0x1E^0x54^len;         val[i 1] ^= 0xbf^0x4d^len;         val[i 2] ^= 0xa2^0x47^len;         printf("x x x n",val[i],val[i 1],val[i 2]);     } }

得到一组结果:

1c 7a 0a 1c 1d 6d 3f 7a 4c 3f 5b 6d c2 a4 0a c2 a4 0a c2 87 4c c2 5b 90 c2 1d b3 e3 87 6d e3 7a 90 a5 a4 6d a5 7a b3

这一组结果作为输入的前三字节,经过一系列异或之后的乘积满足固定值要求

因为结果有限,可能只有一组,也可能有多组答案能解密shellcode,所以这里先不管

接下来是计算分隔符,没有分隔符程序会崩溃:因为把分隔符放在第四位,所以根据之前的异或操作规律,需要和第一个值进行异或一下:

int main() {     for (int i = 0; i < 39; i =3)     {         int len = 3 1;         val[i] ^= 0x1E^0x54^len;         val[i 1] ^= 0xbf^0x4d^len;         val[i 2] ^= 0xa2^0x47^len;         char tmp = 0x20 ^ val[i] ^ 0x54;         printf("x x x x n",val[i],val[i 1],val[i 2],tmp);     } }

得到新的一组结果:

1b 7d 0d 6f 1b 1a 6a 6f 38 7d 4b 4c 38 5c 6a 4c c5 a3 0d ffffffb1 c5 a3 0d ffffffb1 c5 80 4b ffffffb1 c5 5c 97 ffffffb1 c5 1a b4 ffffffb1 e4 80 6a ffffff90 e4 7d 97 ffffff90 a2 a3 6a ffffffd6 a2 7d b4 ffffffd6

到这里如果计算没错的话,程序应该可以正常运行了,使用第一组输入试试:成功运行,显示提示字符已注册

但是这里用户名还没有显示出来,用户名经过的操作只有两次,一次是最初的对每个字符都异或一遍读取长度,然后就是用前三字节去循环异或用户名了:

int main() {     char name[100] = {0};     std::cin >> name;     int tmp_len = strlen(name);     int nLen = tmp_len % 3 == 0 ? tmp_len : tmp_len 3-(tmp_len % 3);     //0x1E, 0xBF, 0xA2     for (int i = 0; i < 39; i =3)     {         int len = 3 1 nLen;         val[i] ^= 0x1E^0x54^len;         val[i 1] ^= 0xbf^0x4d^len;         val[i 2] ^= 0xa2^0x47^len;         for (int j = 0; j < strlen(name); j = 3) {             name[j] ^= val[i 1]^0x4d;             name[j 1] ^= val[i 2]^0x47;             name[j 2] ^= val[i]^0x54;         }         char tmp = 0x20 ^ val[i] ^ 0x54;         printf("x x x x ",val[i],val[i 1],val[i 2],tmp);         for (int j = 0; j < strlen(name); j )         {             printf("x ",name[j]);         }         printf("n");     } }

计算结果:用户名:selph,结果:15 73 03 61 4d 21 2d 4e 2c 41,用010 editor写入key文件,打开程序:用户名也出来了,到此完成这个CM 的算法分析

总结

这个有意思,通过key文件进行校验,key里有分隔符决定程序是否会崩溃,分隔符后面的是用户名,通过异或操作进行解密

这里还通过前三字节作为密码去解密一段shellcode,然后执行shellcode来生成验证通过的字符串进行显示

3. 043-riijj_cm_20041121

算法难度:⭐⭐⭐(这个有意思)

爆破难度:⭐⭐

反调试:⭐⭐

信息收集

运行情况:

查壳与脱壳:

无壳,但是自带了一个dll文件

调试分析

是个C/C 程序,直接打开可以运行,但是用x86dbg打开则会发生异常,因为跳转到的0地址执行

因为调试器会接管异常的缘故,不管怎么点运行,程序都停留在了这里

首先推测是这里存在反调试!有两个可能:

a. 在这之前进行反调试检测,检测到调试器,则不往这个地址里填值,使得这里读取到的内容是0

b. 这里就是通过故意触发异常来进行反调试的

接下来先对抗一下反调试

处理反调试

对于猜想1,我先启动了程序,然后再附加查看内存,这个地址的内容依然是0,所以要么是猜想1不成立,要么是跳转完成之后又把值改为了0,经过IDA静态分析里查找这个变量,通过交叉引用查看,发现并没有修改这个变量的迹象,故认为猜想1不成立

对于猜想2,由于这个函数在跳转0地址之前,注册了一个SEH处理函数,所以这里就让SEH来处理异常,我们去SEH里下断点(4024D0),同时设置x86dbg忽略0xc0000005异常

程序成功在异常处理里断下了,如果只是简单的通过让调试器接管异常使得程序不能进行下去来反调试,那么到此程序应该就能运行下去了,点击运行之后,又再次执行到了异常处理里的断点处:

这不对劲,有猫腻!经过不停的步过,发现异常处理函数下面的这个call造成了异常:

这个call的地址取决于ediecxedi是个基址,ecx是索引

这里ecx的值不同,就会执行不同的函数,一定有一个是程序正常执行需要执行的,然后有很多干扰项

不停的运行,到这里ecx的值依次增加30369......当运行到某一个函数的时候(ecx=C):手动生成了一个奇怪的异常(之前都是jmp 0地址)

再往下,第7次进入这个异常处理的时候,call了另一个不一样的函数

这个函数调用了函数SetUnhandledExceptionFilter

经过查阅资料:这个函数的功能注册一个终极异常处理函数

通常来说,当程序发生异常之后,如果没有异常处理器能处理,就会执行终极异常处理:UnhandledExceptionFilter,而这个函数的作用就是自定义一个终极异常处理函数

而这个终极异常处理函数在调试器下不会被调用,在调试器下,处理不了的异常就交给调试器了,只有在无调试器情况下才会交给终极异常处理让程序正常执行,从而达到反调试的效果

也正因为处理不了的异常会交给调试器,所以调试的时候能见到同一个异常出现了很多次程序都不退出

这里因为在调试器下没法进入这个终极异常处理,所以这里对程序进行一些修改,这个call之下有一个push F,然后再下面的那个call会产生异常,所以这里干脆直接让下面的程序直接变成跳转到异常处理里,修改如下:

运行,程序能跑起来了,接下来打补丁保存这个修改,右键,patch,patch file

调试新保存的程序,结果依然跑不起来,发生了异常:又在jmp 0地址,但是右下角给出了提示,IsDebuggerPresent,看来是另一个反调试

这个反调试好处理,直接修改fs:[30]地址处的第二个字节为0即可,或者用x86dbg插件ScallyHide来隐藏PEB调试痕迹

然后再次重新运行,程序可以调试了,反调试都处理完了(应该)

定位校验函数

输入Name Serial,点击按钮,发现没反应,没有错误提示

那就用老办法去找C 界面程序的按钮事件,学习Win32编程的时候,我们知道窗口程序有一个窗口过程函数,当有消息了,就传递给窗口过程来处理

使用xspy工具查看控件ID:按钮id=3ee

接下来,用IDA分析,在导入表中找到分发消息的程序段一定会用的函数:DispatchMessage,然后通过交叉引用定位找到窗口创建函数,在上面不远处看到窗口过程函数:

这个函数很简单:就处理一个事件

查阅资料[3]可知,按钮按下的事件是一个WM_COMMAND消息,这个按钮ID刚好是e33,那这个处理就是按钮按下的过程了(实际测试,在动态调试器下,按下按钮确实会断在这里)

这里的功能是给一个变量赋值为1

接下来看看这个变量是干嘛用的:这里如果按钮按下了,就会进入一大段程序里,否则就跳出

这一大段程序除了不知道干啥的初始化和赋值,值得关注的就是这个call了(名字是逆向完之后重命名的)

交叉引用查看这个call的地址,找到赋值的地方:是来自dll的导出函数

上面那个call:fp_CopyDll,的功能是复制dll到某个地方,然后将文件名保存到全局变量里:

然后对dll文件进行一个解密循环操作:

(难怪之前直接dllIDA识别不出来),等执行完这个循环去临时目录拿到解密后的dll来继续分析导出函数,这里按钮的功能就是调用这个导出函数

分析校验函数

随便输入点啥,调用导出函数之前下个断点,看看参数是啥:参数1是用户名,参数2是序列号

这个校验函数的内容就很简单:

上面的初始化过程就跳过不管了,这里分为两部分:

a. 生成真码

b. 序列号对比

后者就是strcmp,没啥好说的,这里主要就是真码的生成:

遍历Name,取一个字节

除以0x62得到余数

余数作为一个超长随机字符串的索引,取2个值,拼接在一起

依次循环,直到拼接完成每个字节索引到的两个字符

注册机

注册码生成算法:

#include auto arr = "fytugjhkuijonlbpvqmcnxbvzdaeqrwtryetdgfkgphonuivmdbxfanqydexzwztqnkcfkvcpvlbmhotyiufdkdnjxuzyqh"; int main() {     char name[100] = { 0 };     char serial[100] = {0};     std::cin >> name;     int len = strlen(name);     for (int i = 0,j=0; i < len; i ,j =2)     {         int tmp = name[i];         serial[j] = arr[tmp % 0x62];         serial[j 1] = arr[tmp % 0x62 1];     }     std::cout << serial; }

效果:

总结

这个有意思,基于异常的反调试和基于PEB的反调试,解密dll文件使用导出函数进行密码校验

这个反调试对于新手来说,确实不好搞,很有价值的一次逆向学习!

参考资料

–[1] Anti-Debug: Exceptions (checkpoint.com)

[2]反调试 - SetUnhandledExceptionFilter_(-: LYSM :-)的博客-CSDN博客_setunhandledexceptionfilter

[3] (Winuser.h) WM_COMMAND消息 - Win32 apps | Microsoft Docs

–[4]虚拟键码键盘消息(初稿)_tiandyoin的博客-CSDN博客

4. 044-tsrh-crackme

算法难度:⭐⭐⭐⭐

爆破难度:⭐

信息收集

运行情况:

一开始就是Nag:

然后进入界面:

查壳与脱壳:

调试分析

首先是一个创建互斥量,然后接收一个错误,如果是特定错误就不执行程序,这是一种简单有效的防多开手段,然后就是创建窗口了,这里跟进窗口函数

去除Nag

窗口过程的开头:

第一个0x110号消息的分支里,存在一个MessageBoxA的调用,查阅资料

[1]可知,创建对话框的时候,会有一个初始化消息会发送到消息循环,这个时候窗口还未显示出来,这个消息WM_INITDIALOG就是0x110号消息

Nag存在于这个消息里,而这里MessageBox调用前面有一个跳过的条件,这里如果要消除Nag,就直接改这个跳转语句jzjmp即可

分析校验算法

进入之后就是switch-case里的多分支语句了,使用xray查看check按钮id=22b,按钮按下的消息码是0x111,直接找111消息码控件id为22b的分支:

首先是先获取Name到全局变量里(长度需要大于等于5

接下来是Serial校验过程

这里调用了三个生成真码的函数,这一段反汇编的主要功能就是这三个函数,中间的这些对比可以忽视,是提升效率用的

先看第一个生成函数:

获取了Name长度,保存到全局变量里

然后拼接字符串:tsrh-一个数字-

第二个生成函数:

获取生成Serial的长度,遍历用户名,对于每个用户名字节,都进行一系列计算得到一个新的数值,以十六进制的形式拼接到生成Serial的后面

第三个生成函数:

这里分别从Name和生成Serial里取一个字节,异或(Name从头开始取,Serial12偏移处开始取)

把结果变成大写字母,然后把该大写字母以int赋值的形式,赋值到Serial10字节处

最后调用对比函数

对比函数:逐字节对比,如果全都一样,则返回1,返回1则会跳转到成功提示弹窗处

注册机

注册码生成算法:

#define _CRT_SECURE_NO_WARNINGS #include #pragma region check int midValue = 0; char serial[100]; char name[100]; void GenSerial1() {     midValue = strlen(name);     sprintf(serial,"tsrh-%d-",midValue 0x7d3); } void GenSerial2() {     int len = strlen(serial);     int tmp = midValue;     for (int i = 0; i < midValue; i )     {         int tmp1 = name[i] 0xc;         int tmp2 = tmp1 - 0x11 tmp1 - len;         tmp1 ^= tmp2;         sprintf(&serial[len], "%X", tmp1);         len = strlen(serial);     } } void GenSerial3() {     for (int i = 0; i < 0x10; i )     {         if (name[i] == 0)break;         char tmpn = name[i] 1;         char tmps = serial[i 12];         char c = tmpn ^= tmps;         while (c < 'A')c = 8;         while (c > 'Z')c -= 3;         *(int*)&serial[i 10] = c;     }     std::cout << serial; } #pragma endregion 校验函数 int main() {     std::cin >> name;     GenSerial1();     GenSerial2();     GenSerial3(); }

效果:

总结

分析这类的窗口程序,如果知道常见的窗口消息是什么,那就很好定位到控件操作的事件

参考资料

[1]WM_INITDIALOG消息 (Winuser.h) - Win32 apps | Microsoft Docs

5. 045-CyTom-crackme

算法难度:⭐⭐

爆破难度:⭐

信息收集

运行情况:

查壳与脱壳:

调试分析

虽然上面写的Delphi程序,然而IDR并不能识别,使用DD去反编译倒是成功了,可能版本Delphi太低吧

找到按钮函数:

逻辑很简单:获取Serial,获取Name,对Serial计算一个值,对Name计算一个值,然后进行比对

对Serial的计算:

首先判断第一个字符是否合法,不合法就跳转

然后是一个循环,分析发现,又分析了一遍_atoi函数

这个计算的本质就是把Serial字符串转换成数字

对Name的计算:

依然很简单,累乘每一个字节,得到最后的累乘值

注册机

注册码生成算法:

#include int main() {     char name[100] = { 0 };     char serial[100] = { 0 };     int checknum = 1;     // Name计算出校验值     std::cin >> name;     for (int i = 0; i < strlen(name);i ) {         checknum *= name[i];     }     checknum &= 0x0FFFFFFF;     // 校验值反推Serial     printf("%d",checknum); }

效果:

0 人点赞