pk8900 发表于 2018-1-25 12:42

blowfish加密算法代码还原--分析[反汇编练习] 160个CrackMe之092(crackme1)

本帖最后由 pk8900 于 2018-1-26 12:02 编辑

【前言】      
          最近几天都在研究160个CrackMe之092(crackme1) 这个标了五星难度的crackme,直到昨晚总算将其分析完成,也可以说是将汇编代码逆向还原为C++代码后,并找到注册方法,今天将分析的过程总结整理到笔记中,同时也分享给坛友们。
【crackme简介】
       下载地址:http://pan.baidu.com/share/link?shareid=541269&uk=4146939145
      Visual C++ 6.0编写,UPX 0.89.6 - 1.02壳,是一个computer ID+Unlock code方式验证方式,有提示文字。标注难度为5星。
工具:IDA/X64dbg/OD,代码编写:VS2013
【crackme截图】

【算法分析】
         本文主要讲算法部分,调试跟踪可能不会太详细,具体操作可以结合我分析算法对相应代码位置下断点,单步运行,即可明白程序的代码流程。
       通过搜索字符串,找到Don't give up. Try agagin!代码处。
004015E0 | E8 EB FA FF FF               | call <crackm.sub_4010D0>                        |
004015E5 | 8B 44 24 64                  | mov eax, dword ptr ss:                |
004015E9 | 8B 0D F0 99 40 00            | mov ecx, dword ptr ds:                |直正注册码KEY1
004015EF | 83 C4 1C                     | add esp, 0x1C                                 |
004015F2 | 3B C1                        | cmp eax, ecx                                    |
004015F4 | 75 29                        | jne crackm.40161F                               |
004015F6 | 8B 4C 24 4C                  | mov ecx, dword ptr ss:                |
004015FA | A1 EC 99 40 00               | mov eax, dword ptr ds:                |直正注册码KEY1
004015FF | 3B C8                        | cmp ecx, eax                                    |
00401601 | 75 1C                        | jne crackm.40161F                               |
00401603 | 6A 30                        | push 0x30                                       | UINT uType = MB_OK | MB_ICONEXCLAMATION | MB_ICONQUESTION | MB_ICONSTOP
00401605 | 68 F0 80 40 00               | push <crackm.sub_4080F0>                        | LPCTSTR lpCaption = "Success"
0040160A | 68 D4 80 40 00               | push crackm.4080D4                              | LPCTSTR lpText = "You have done a good job."
0040160F | 56                           | push esi                                        | HANDLE hWnd
00401610 | FF 15 20 61 40 00            | call dword ptr ds:[<&MessageBoxA>]            | MessageBoxA
00401616 | 33 C0                        | xor eax, eax                                    |
00401618 | 5E                           | pop esi                                       |
00401619 | 83 C4 40                     | add esp, 0x40                                 |
0040161C | C2 10 00                     | ret 0x10                                        |
0040161F | 6A 30                        | push 0x30                                       | UINT uType = MB_ICONEXCLAMATION | MB_ICONERROR | MB_ICONHAND | MB_ICONQUESTION | MB_ICONSTOP | MB_ICONWARNING | MB_OK
00401621 | FF 15 24 61 40 00            | call dword ptr ds:[<&MessageBeep>]            | MessageBeep
00401627 | 6A 10                        | push 0x10                                       |
00401629 | 68 CC 80 40 00               | push crackm.4080CC                              | 4080CC:"Failed"
0040162E | 68 B0 80 40 00               | push <crackm.sub_4080B0>                        | 4080B0:"Don't give up. Try agagin!"
00401633 | 56                           | push esi                                        |
00401634 | FF 15 20 61 40 00            | call dword ptr ds:[<&MessageBoxA>]            |
       往上找到关键比较代码,直正注册码KEY1和Key2保存在:和: 中,这是两个连续的地址,也就是两个DWORD值,在OD中对该地址下内存写入断点,缕清程序的流程。
程序三次调用子程序sub_401130,压栈参数中分别是三个不同的字符串,和相同的地址指针,通过IDA中反汇编的伪代码,将sub_401130整理还原代码如下:
void __cdecl sub_401130(unsigned long*base_a1, unsigned char *char_a2, signed int leng_a3)
{
      int z = 0;
      //unsigned long * key =(unsigned long *)char_a2;
      unsigned long tmp =0;
      unsigned long tmp_char = 0;
      unsigned long key1=0, key2=0;//   KEY初始为0
      unsigned long *k1=&key1;   
      unsigned long *k2=&key2;
      memcpy_s(base_a1 + 0x12, 0x1000 , mem_6198, 0x1000); //复制 00406198内存数据0x1000字节,无变动

      //print_base(base_a1, 20);
      for (int x = 0; x < 0x12; x++)
      {
                tmp = 0;
                for (int y = 0; y <4; y++)   //"94940361391";   //循环取字符串中4个字符组成一个DWORD值
                {
                        if (z == leng_a3) z = 0;
                        tmp_char = 0+char_a2;
                        tmp_char = tmp_char << ((3 - y) * 8);
                        tmp |= tmp_char;
                        z++;
                }
                base_a1 = mem_6150 ^ tmp;    //mem_6150 内存00406150数据,取48字节
      }
      //print_base(base_a1, 20);
      z = 0;
      for (int x = 0; x < 9; x++)   //分9次用Key异或加密数据表头部,每次写入两个DWORD值,共18次,即0x48(72.)个字节
      {
                sub_401070(base_a1, k1, k2);
                *(base_a1 + z) = *k1;
                *(base_a1 + z + 1) = *k2;
                z+=2;
      }
      //print_base(base_a1, 20);
      for (int y = 0; y < 4; y++)   //分4次,每次0x80(128),共0x200(512.)次,每次写入两个DWORD值,即0x1000(4096.)个字节
      {
                for (int x = 0; x < 0x80; x++)
                {
                        sub_401070(base_a1, k1, k2);
                        *(base_a1 + z) = *k1;
                        *(base_a1 + z + 1) = *k2;
                        z += 2;
                }
      }
}
      参考我代码后的备注可以看出,sub_401130函数的三个数参,unsigned long*base_a1这是一个固定的地址,实际是一个用于加密的数据表的指针, unsigned char *char_a2是用于对数据表中的数据进行加密的字符串,循环取字符串中4个字符组成一个DWORD值与数据表中数据异或加密,signed int leng_a3为传入的字符串的长度。在sub_401130中又调用了sub_401070,于是对sub_401070进行分析和代码还原,整理如下:
void sub_401070(unsigned long *base, unsigned long *k1, unsigned long *k2)//加密密钥Key1和Key2,不变更数据表
{
      unsigned long tmp = *k1;
      for (int x = 0; x < 0x10; x++)
      {
                *k1 ^= base;
                tmp = *k1;
                *k1 = sub_401000(base, tmp);
                *k1 = *k1^*k2;
                *k2 = tmp;
      }
      *k1 ^= *(base + 0x10) ;
      *k2 ^= *(base + 0x11) ;
      swap(*k1, *k2);
}
      sub_401070有三个参数:unsigned long *base是数据表,另外还有 unsigned long *k1, unsigned long *k2,这是两个DWORD值,k1,k2,通过sub_401070子程序进行加密变换,sub_401070不变更数据表。在上面的sub_401130调用sub_401070,最初传入k1,k2值均为0,经sub_401070加密后得到的DWORD值,填充到数据表。所以说sub_401130是加密数据表的过程,sub_401070是加密KEY1和KEY2的过程。在sub_401070中调用了sub_401000函数,sub_401000还原代码如下:
int __cdecl sub_401000(unsigned long *base, unsigned long a2)//查表函数{
      unsigned char * x1 = (unsigned char *)&a2;   //转为char型指针,分别取出DWORD值中的四个字节值
      unsigned charx2 = *(x1 + 1);
      unsigned long C1 = *(base + (*(x1 + 3)) + 0x48 / 4);
      unsigned long C2 = *(base + (*(x1 + 2)) + 0x448 / 4);
      unsigned long C3 = *(base + (*(x1 + 1)) + 0x848 / 4);
      unsigned long C4 = *(base + (*(x1 + 0)) + 0xC48 / 4);
      return ((C1 + C2) ^ C3) + C4;
}
      可以看出,sub_401000是一个查表函数,用给定的DWORD值进行查表,得到一个新的DWORD值返回。以上三个函数即程序启动用调用用于加密数据表,并生成Computer ID,并将第二次加密处理后的KEY1和KEY2值保存在和: 中,后来用于对比验证。接下来跟程序对我们输入的假码进行加密的过程,程序调用了sub_4010D0,对sub_4010D0代码还原如下:
void sub_4010D0(unsigned long *base, unsigned long *k1, unsigned long *k2)//对输入的key进行加密,与sub_401070极似
{
      unsigned long tmp;
      int s = 0x44 / 4;
      for (int x = 0; x < 0x10; x++)
      {
                *k1 ^= base;
                tmp = *k1;                *k1 = sub_401000(base, tmp);
                *k1 = *k1^*k2;
                s--;
                *k2 = tmp;
      }
      *k1 ^= *(base + 1);
      *k2 ^= *base;
      swap(*k1, *k2);
}

      sub_4010D0接受三个参数,unsigned long *base,是引用数据表的指针, unsigned long *k1, unsigned long *k2是我们输入的Key1,key2,经sub_4010D0函数中对数据库数据异或操作加密后,与程序在初始过程中保存在和: 中两个DWORD值进行比较,如果一样,程序提示注册成功,不一样,则提示失败。通过以上分析后,我们将程序的流程进行还原整理,代码如下,这段代码中已包含了逆向找到直接Unlock code的部分:void main()
{
      unsigned char base_key[] ="94940361391";
      unsigned char base_key_CGG[] = "ChinaCrackingGroup";
      unsigned char base_key_CFF[] = "CrackingForFun";
      unsigned long key1 = 0x11111111;//测试用
      unsigned long key2 = 0x22222222;//测试用
      unsigned char base_key1[] = "fishblow";
      unsigned char* the_key =new unsigned char;
      unsigned long * k1 = &key1;//测试用,可改变指针
      unsigned long * k2 = &key2;//测试用,可改变指针
      
      unsigned long org_k1 = 0x776F6C62;//"blow";
      unsigned long org_k2 = 0x68736966;//"fish"
      LPTSTRuser_key = new TCHAR;//wsprintf 函数缓冲区
      memset(the_key, 0, 260);
      getReg((char *)the_key);
      if (strlen((char *)the_key) == 0)
                the_key = base_key;            //注册表获取ProductID失败则采用默认:"94940361391";
      cout << "01--key word is : " << the_key << endl;
      unsigned long* buf_8980 = new unsigned long;   //分配加密数据表空间
      memset(buf_8980, 0, 0x1000 + 0x20);                           //填零

      sub_401130(buf_8980, the_key, strlen((char *)the_key));   //第一次初始化加密数据表
      sub_401070(buf_8980, &org_k1, &org_k2);   //加密字符:"fish"   "blow";

      wsprintf(user_key, "%08lX%08lX", org_k1, org_k2);//输出结果为:界面上的 computer ID

      cout << "your computer ID:" << user_key << endl;

      the_key = base_key_CGG;   //"ChinaCrackingGroup";
      cout << "02--key word is : " << the_key << endl;
      sub_401130(buf_8980, the_key, strlen((const char *)the_key));   //第二次初始化加密数据表
      sub_401070(buf_8980, &org_k1, &org_k2);      //加密字符 “第一次结果”

      wsprintf(user_key, "%08lX%08lX", org_k1,org_k2);
      cout <<"key code is:" <<user_key << endl;

      
      the_key = base_key_CFF;    // "CrackingForFun"
      cout << "03--key word is : " << the_key << endl;
      sub_401130(buf_8980, the_key, strlen((const char *)the_key));   //第三次初始化加密数据表

      //int x2 = sub_401000(buf_8980, key1);
      k1 = &org_k1; k2 = &org_k2;//改变指针,指向内存中正确KEY比较值
      cout << "input code is : " <<hex<<uppercase<< *k1 << *k2 << endl;
      crack_4010D0(buf_8980, k1, k2);//解密得到用户应该输入的Unlock Code!!
      //sub_4010D0(buf_8980, k1, k2);//对输入的KEY进行正常加密后判断是否正确
      cout << hex << "Unlock Code is:" << uppercase << "" << *k1<< *k2 << "   " << endl;
cout<<"auto head...hello!"<<endl;
system("pause");
}
以上流程用一张流程图来给大家演示一下:

通过上图演示,弄清了程序的完整流程,想做出注册机就得逆向sub_4010D0,得到Unlock Code,经分析,sub_4010D0是可以逆向推算的,具体代码如下:
void crack_4010D0(unsigned long *base, unsigned long *k1, unsigned long *k2)//注册机反推部分:根据生成过程反推应输入的unlock code
{
      unsigned long tmp1;
      unsigned long tmp2;
      int s = 0x44 / 4-0x10;
      swap(*k1, *k2);
      *k1 ^= *(base + 1);
      *k2 ^= *base;
      
      for (int x = 0; x < 0x10; x++)
      {
                s++;
                tmp2 = sub_401000(base, *k2);
                tmp1 = *k2^ base;//最开始的k1
                *k2 = tmp2^(*k1);//最开始的k2
                *k1 = tmp1;
      }
      //tmp1 = *k2      
}
       到这里我们就能通过程序反推出正确的Unlock Code了,但我到虚拟机里测试了一下,发现这个注册机并不通用,在虚拟机里生成的computer ID和电脑里生成的不同,通过对比用于复制加密数据的00406198处0x1000个字节发现,这些代码中包含了四个函数的调用地址,这些函数地址是程序运行后动态填入的,因此可以说程序在不同的电脑上运行会出现不同的computer ID,也就是要做出通用的注册机只能注入程序中获取00406198处0x1000个字节的内容来计算注册码。
附上源代码中用到的其它代码,这样代码就全贴上来了,平台VS2013,可以测试,注意mem_6198数据需要自己抓取:
#include<iostream>
#include <windows.h>
using namespace std;
//typedef long _DWORD;
unsigned long mem_6198[] = {
      0xD1310BA6, 0x98DFB5AC, 0x2FFD72DB, 0xD01ADFB7, 0xB8E1AFED, 0x6A267E96, 0xBA7C9045, 0xF12C7F99, 0x24A19947, 0xB3916CF7, 0x0801F2E2, 0x858EFC16, 0x636920D8, 0x71574E69, 0xA458FEA3, 0xF4933D7E, 0x0D95748F, 0x728EB658, 0x718BCD58, 0x82154AEE, 0x7B54A41D, 0xC25A59B5, 0x9C30D539, 0x2AF26013, 0xC5D1B023, 0x286085F0, 0xCA417918, 0xB8DB38EF, 0x8E79DCB0, 0x603A180E, 0x6C9E0E8B, 0xB01E8A3E,............此处省略..........
};
unsigned long mem_6150[] = {
      0x243F6A88, 0x85A308D3, 0x13198A2E, 0x03707344, 0xA4093822, 0x299F31D0, 0x082EFA98, 0xEC4E6C89, 0x452821E6, 0x38D01377, 0xBE5466CF, 0x34E90C6C, 0xC0AC29B7, 0xC97C50DD, 0x3F84D5B5, 0xB5470917,
      0x9216D5D9, 0x8979FB1B, 0xD1310BA6, 0x98DFB5AC, 0x2FFD72DB, 0xD01ADFB7, 0xB8E1AFED, 0x6A267E96, 0xBA7C9045, 0xF12C7F99, 0x24A19947, 0xB3916CF7, 0x0801F2E2, 0x858EFC16, 0x636920D8, 0x71574E69
};
void getReg(char * ret_value)
{
      //HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion
      LPBYTE   p = new byte;
      char * val = (char*)p;
      HKEY reg_handle;
      HKEY h_main = HKEY_LOCAL_MACHINE;
      LPCSTR data = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion";
      LPCSTR data1 = "ProductID";
      DWORD keyType = REG_SZ;//定义数据类型
      DWORD DataLen = 260;//定义数据长度   
      if (RegOpenKeyExA(h_main, data, 0, KEY_READ, ®_handle) == ERROR_SUCCESS)
      {
                if (RegQueryValueExA(reg_handle, data1, NULL, &keyType, p, &DataLen) == ERROR_SUCCESS)
                {
                        if (strlen(val))
                        memcpy_s(ret_value,strlen(val), val,strlen(val));
                }               
      }
}
void print_base(unsigned long * base, int len) //打印输出数据表
{
      for (int x = 0; x < len; x++)
      {
                cout << hex << uppercase << *(base + x) << "   ";
                if (x % 4 == 3)cout << endl;
      }
}
总算总结完成了,希望大家能看懂,我也是还原完代码后,感觉这算法很精妙,不可能师出无名,于是百度搜索,找到了blowfish加密算法,大家可以百度查找。
在帖子https://www.52pojie.cn/thread-470028-1-1.html中有对于此Crackme的介绍,大家可以参阅学习。
最后,给注册成功后的CRACKME拍个照:

分析有不对的地方,还请大家指正,共同交流。

sky_bro 发表于 2019-10-24 16:18

楼主看下我刚写的分析 https://www.52pojie.cn/thread-1042630-1-1.html
00406198处0x1000个字节应该是固定的,是pi的十六进制形式的小数部分145~8336位(1~144位-72字节,为初始化S盒所用),每一位是4-bit的十六进制数

pk8900 发表于 2018-1-25 20:29

zckun 发表于 2018-1-25 20:20
想玩crackme。。。就是有点太小白了,不知道能不能给个学习路线,汇编入门了

熟悉工具软件,从OD开始,我也没看过那些入门的书籍,网上教程多了。

buddama 发表于 2018-1-25 15:18

楼主高产啊!

mayl8822 发表于 2018-1-25 17:23

感谢分享

february 发表于 2018-1-25 18:19

楼主牛了,这个也搞了

zckun 发表于 2018-1-25 20:20

想玩crackme。。。就是有点太小白了,不知道能不能给个学习路线,汇编入门了

939372735 发表于 2018-1-25 22:10

楼主,建议加个导航贴啊,从1开始到160

zq3332427 发表于 2018-1-26 07:54

感谢分享,暂时还没看明白

冰露㊣神 发表于 2018-1-26 10:01

pk8900 发表于 2018-1-25 20:29
熟悉工具软件,从OD开始,我也没看过那些入门的书籍,网上教程多了。

目前逆算法到第三个了。。开始卡主了

ygfygf_888 发表于 2018-1-26 10:16


谢谢大神分享
页: [1] 2
查看完整版本: blowfish加密算法代码还原--分析[反汇编练习] 160个CrackMe之092(crackme1)