[逆向工程]160个CrackMe入门实战之aLoNg3x.2解析(七)
[逆向工程]160个CrackMe入门实战之aLoNg3x.2解析(七)
一.aLoNg3x.2功能
作者说不要打补丁,找出用户名和注册码,隐藏下面的按钮,算成功
还是用补丁方式,验证是否逆向成功
二.查壳
无壳,32位程序,Delphi语言
DeDe查看事件及控件相关信息:
事件:
00442B98 取消按钮事件
00442CC8 关于按钮事件
00442F28 注册按钮事件
004430BC 重试按钮事件
控件ID:
2E8 重试按钮控件ID
2E4 取消控件ID
2DC 注册码控件ID
2D8 输入名控件ID
2CC 注册控件ID
三.逆向分析
3.1 00442F28 注册按钮事件
还是使用nop爆破方法分析事件,注册按钮入口地址00442F28处下断点
根据DeDe以及dbg分析以下是爆破点注释
00442F9F处nop填充
00442FC0处nop填充
00442F9F处nop填充
00442FCF处mov dl,1-----修改为mov dl,0
修改后将该文件打补丁为3.exe
备注:源程序中对注册码也做了一些简单校验,本次爆破输入时全部为数字
00442F28|55|push ebp|注册按钮入口 00442F29|8BEC|mov ebp,esp|00442F2B|83C4 F8|addesp,FFFFFFF8|00442F2E|53|push ebx|00442F2F|56|push esi|esi:"U嬱兡舾(3D"00442F30|33C9|xor ecx,ecx|ecx:"U嬱兡舾(3D"00442F32|894D F8|mov dword ptr ss:[ebp-8],ecx|00442F35|8BD8|mov ebx,eax|00442F37|33C0|xor eax,eax|00442F39|55|push ebp|00442F3A|6822304400|push3.443022|00442F3F|64:FF30|push dword ptr fs:[eax]|00442F42|64:8920|mov dword ptr fs:[eax],esp|00442F45|8D55 F8|lea edx,dword ptr ss:[ebp-8]|00442F48|8B83 DC020000|mov eax,dword ptr ds:[ebx+2DC]|00442F4E|E8 ED02FEFF|call3.423240|00442F53|8B45 F8|mov eax,dword ptr ss:[ebp-8]|00442F56|8D55 FC|lea edx,dword ptr ss:[ebp-4]|[ebp-04]:BaseThreadInitThunk 00442F59|E8 FAF9FBFF|call3.402958|00442F5E|8BF0|mov esi,eax|esi:"U嬱兡舾(3D"00442F60|837D FC 00|cmpdword ptr ss:[ebp-4],0|[ebp-04]:BaseThreadInitThunk 00442F64|7437|je3.442F9D|00442F66|B838304400|mov eax,3.443038|443038:"You MUST insert a valid Long Integer Value in the Code Editor... Thank you :)"00442F6B|E8 00F6FFFF|call3.442570|00442F70|8D55 F8|lea edx,dword ptr ss:[ebp-8]|00442F73|8B83 DC020000|mov eax,dword ptr ds:[ebx+2DC]|00442F79|E8 C202FEFF|call3.423240|00442F7E|8B45 F8|mov eax,dword ptr ss:[ebp-8]|00442F81|E8 06FBFFFF|call3.442A8C|00442F86|A330584400|mov dword ptr ds:[445830],eax|00442F8B|BA90304400|mov edx,3.443090|edx:"U嬱兡舾(3D"00442F90|8B83 DC020000|mov eax,dword ptr ds:[ebx+2DC]|2DC 注册码控件ID 00442F96|E8 D502FEFF|call3.423270|00442F9B|EB 6F|jmp3.44300C|00442F9D|85F6|testesi,esi|esi:"U嬱兡舾(3D"00442F9F|90|nop|跳过隐藏注册按钮,显示Again按钮 00442FA0|90|nop|00442FA1|8D55 F8|lea edx,dword ptr ss:[ebp-8]|00442FA4|8B83 D8020000|mov eax,dword ptr ds:[ebx+2D8]|2D8 输入名字控件id 00442FAA|E8 9102FEFF|call3.423240|00442FAF|8B4D F8|mov ecx,dword ptr ss:[ebp-8]|00442FB2|8BD6|mov edx,esi|edx:"U嬱兡舾(3D", esi:"U嬱兡舾(3D"00442FB4|A130584400|mov eax,dword ptr ds:[445830]|00442FB9|E8 EAF9FFFF|call3.4429A8|00442FBE|84C0|testal,al|00442FC0|90|nop|第三处判断 跳过隐藏注册按钮 显示重试按钮 00442FC1|90|nop|00442FC2|33D2|xor edx,edx|edx:"U嬱兡舾(3D"00442FC4|8B83 CC020000|mov eax,dword ptr ds:[ebx+2CC]|2CC 注册控件ID 00442FCA|E8 6101FEFF|call3.423130|00442FCF|B2 00|mov dl,0|修改常量1 为0 00442FD1|8B83 E8020000|mov eax,dword ptr ds:[ebx+2E8]|2E8 重试按钮控件ID 00442FD7|E8 5401FEFF|call3.423130|00442FDC|33D2|xor edx,edx|2D8 输入名控件ID其中修改常量方式如下:ctrl+e 之前01修改为00
3.2 代码分析过程
004429A8|55|push ebp|关键call 004429A9|8BEC|mov ebp,esp|004429AB|83C4 F4|addesp,FFFFFFF4|004429AE|53|push ebx|004429AF|56|push esi|004429B0|57|push edi|004429B1|894D F8|mov dword ptr ss:[ebp-8],ecx|用户名入栈 004429B4|8955FC|mov dword ptr ss:[ebp-4],edx|004429B7|8BF8|mov edi,eax|004429B9|8B45 F8|mov eax,dword ptr ss:[ebp-8]|从栈中取用户名 004429BC|E8 2712FCFF|call along3x.2.403BE8|004429C1|33C0|xor eax,eax|004429C3|55|push ebp|004429C4|687A2A4400|push along3x.2.442A7A|004429C9|64:FF30|push dword ptr fs:[eax]|004429CC|64:8920|mov dword ptr fs:[eax],esp|004429CF|8B45 F8|mov eax,dword ptr ss:[ebp-8]|[ebp-08]:"11111"004429D2|E8 5D10FCFF|call along3x.2.403A34|跟进去 取用户名长度 返回eax 004429D7|83F8 04|cmpeax,4|004429DA|0F8E82000000|jle along3x.2.442A62|小于等于4则跳转,那么输入用户名必须大于4 004429E0|33DB|xor ebx,ebx|004429E2|8B45 F8|mov eax,dword ptr ss:[ebp-8]|[ebp-08]:"11111"004429E5|E8 4A10FCFF|call along3x.2.403A34|004429EA|85C0|testeax,eax|004429EC|7E38|jle along3x.2.442A26|004429EE|8945F4|mov dword ptr ss:[ebp-C],eax|用户名长度入栈 004429F1|BE 01000000|mov esi,1|循环起始 004429F6|8B45 F8|mov eax,dword ptr ss:[ebp-8]|[ebp-08]:"11111"004429F9|E8 3610FCFF|call along3x.2.403A34|004429FE|83F8 01|cmpeax,1|00442A01|7C 1D|jl along3x.2.442A20|小于则跳转,如果不跳转 则eax也就是用户名长度要大于1 00442A03|8B55 F8|mov edx,dword ptr ss:[ebp-8]|[ebp-08]:"11111"00442A06|0FB65432 FF|movzx edx,byte ptr ds:[edx+esi-1]|例如用户名输入为11111则从11111 中取1 显示31也就是第一个字符的ASCII值 00442A0B|8B4D F8|mov ecx,dword ptr ss:[ebp-8]|ecx等于用户名值 00442A0E|0FB64C01 FF|movzx ecx,byte ptr ds:[ecx+eax-1]|每次循环最后一个字符16进制ASCII值 00442A13|0FAFD1|imul edx,ecx|edx=edx*ecx 第一位和最后一位相乘 00442A16|0FAFD7|imul edx,edi|edx=edx*edi 00442A19|03DA|addebx,edx|ebx=ebx+edx 00442A1B|48|dec eax|eax递减 00442A1C|85C0|testeax,eax|eax是否为0 00442A1E|75E3|jne along3x.2.442A03|00442A20|46|inc esi|00442A21|FF4D F4|dec dword ptr ss:[ebp-C]|00442A24|75D0|jne along3x.2.4429F6|循环结果 00442A26|8BC3|mov eax,ebx|00442A28|99|cdq|将 eax 中的有符号数符号扩展为64位,结果保存在 edx:eax 中(edx 存放高位,eax 存放低位) 00442A29|33C2|xor eax,edx|00442A2B|2BC2|sub eax,edx|00442A2D|B9 2A2C0A00|mov ecx,A2C2A|00442A32|99|cdq|00442A33|F7F9|idiv ecx|eax / ecxeax=(商),edx=(余数) 00442A35|8BDA|mov ebx,edx|00442A37|8B45 FC|mov eax,dword ptr ss:[ebp-4]|00442A3A|B959000000|mov ecx,59|59:'Y'00442A3F|99|cdq|00442A40|F7F9|idiv ecx|ecx/59 商eax 余数 edx 00442A42|8BC8|mov ecx,eax|00442A44|8B45 FC|mov eax,dword ptr ss:[ebp-4]|00442A47|BE50000000|mov esi,50|50:'P'00442A4C|99|cdq|00442A4D|F7FE|idiv esi|esi/50 商eax 余数edx 00442A4F|03CA|addecx,edx|00442A51|41|inc ecx|ecx=ecx+1 00442A52|894D FC|mov dword ptr ss:[ebp-4],ecx|00442A55|3B5D FC|cmpebx,dword ptr ss:[ebp-4]|ebx和注册码比较 00442A58|7504|jne along3x.2.442A5E|不相等则直接跳转了 00442A5A|B3 01|mov bl,1|bl等于1正确3.2.1 隐藏条件(必须先触发)
- 原因:程序内部有一个全局变量
[0x445830],初始值为0。
如果这个值是0,所有算法计算结果都是0,永远无法注册成功。 - 触发方法:
在Codice(注册码)输入框中输入至少 6 个英文字母(例如AAAAAA),然后点击Register按钮。
此时会弹出错误提示(因为纯字母不是有效注册码),但程序内部调用call 00442A8C将[0x445830]赋值为0x1686(十进制 5766)。
这一步只需做一次,之后整个程序运行期间该值保持有效。
3.2.2 隐藏 Register 按钮(第一次成功)
- 输入用户名:在Name框中输入任意长度大于4的字符串(例如
admin)。 - 输入正确注册码:在Codice框中输入根据用户名计算出的纯数字注册码(由注册机生成)。
- 点击 Register:
- 程序用
0x1686和用户名经过双层循环算出nTemp。 - 验证
注册码 / 89 + 注册码 % 80 + 1 == nTemp。 - 验证通过后,Register 按钮隐藏,同时Again(重试)按钮出现。
- 程序用
此时你看到的界面:Register 按钮消失,多了 Again 按钮。
3.2.3 隐藏 Again 按钮(第二次成功)
为了完全破解,还需要把 Again 按钮也隐藏掉。方法类似:
- 再次输入假注册码:
在Codice框中再次输入至少6个英文字母(例如AAAAAA),点击Again按钮。
会弹出错误提示(Again 按钮不会消失,但内部状态被重置)。 - 输入正确注册码:
在Codice框中重新输入刚才的纯数字正确注册码,再次点击Again按钮。 - 验证通过后,Again 按钮隐藏,最终界面只剩下 Logo 和 Cancella(取消)按钮。
3.2.4 Cancella(取消)按钮的彩蛋
作者在取消按钮的事件(call 00442B30)中隐藏了一个特殊分支:
普通情况:点击 Cancella 会把 Codice 输入框的内容清零。
特殊情况:
当Name 框的内容与Codice 框的内容完全相等(字符串相同)时,点击 Cancella 会弹出两个信息框(而不是清空注册码)。注意:Codice 框通常要求纯数字,所以要想触发这个彩蛋,用户名也必须是一个纯数字字符串(例如
12345)。此时点击 Cancella 会弹出两个提示框,算是作者留下的一个“惊喜”。
四、总结操作流程(完整版)
| 步骤 | 操作 | 结果 |
|---|---|---|
| 前置 | 在 Codice 框输入AAAAAA,点击 Register | 弹出错误,但内部[0x445830] = 0x1686 |
| 1 | 输入用户名(>4位)和正确纯数字注册码,点击 Register | Register 按钮隐藏,Again 按钮出现 |
| 2 | 在 Codice 框再次输入AAAAAA,点击 Again | 弹出错误(Again 按钮还在) |
| 3 | 在 Codice 框重新输入正确纯数字注册码,点击 Again | Again 按钮隐藏,破解完成 |
| 彩蛋 | 用户名和 Codice 框输入相同数字(如12345),点击 Cancella | 弹出两个消息框(不清零) |
五、注册机核心公式
正确注册码(纯数字整数)serial满足:
serial / 89 + serial % 80 + 1 == nTemp其中nTemp = abs( sum ) % 666666,sum = Σ (0x1686 * username[j] * username[i]),双层循环 i=1…len, j=len…1。
用 C++ 注册机即可根据任意用户名(长度>4)计算出唯一正确的serial。
注册机源码如下:
#include <iostream> #include <string> #include <cctype> #include <limits> // 检查字符串是否全为数字 bool isAllDigits(const std::string& str) { for (char c : str) { if (!std::isdigit(static_cast<unsigned char>(c))) return false; } return true; } int main() { std::string username, fakeSerial; int lenx = 0, leny = 0; // 输入用户名(长度至少5) while (lenx < 5) { std::cout << "请输入用户名(5位以上): "; std::getline(std::cin, username); lenx = username.length(); if (lenx < 5) { std::cout << "用户名太短,请重新输入。\n"; } } // 输入错误注册码(不能全数字,长度至少6) while (leny < 6) { std::cout << "请输入错误注册码(不能全数字6位以上): "; std::getline(std::cin, fakeSerial); leny = fakeSerial.length(); if (leny < 6) { std::cout << "长度不足6,请重新输入。\n"; continue; } if (isAllDigits(fakeSerial)) { std::cout << "不能全为数字,请重新输入。\n"; leny = 0; // 重置,继续循环 } } // 计算隐藏值 edi int edi = 0x37B; // 891 for (size_t i = 1; i < fakeSerial.length(); ++i) { unsigned char cur = static_cast<unsigned char>(fakeSerial[i]); unsigned char prev = static_cast<unsigned char>(fakeSerial[i - 1]); edi += (cur % 0x11 + 1) * prev; } edi %= 0x7148; // 29000 std::cout << "EDI为: " << edi << std::endl; // 根据用户名和 edi 计算 ebx long long ebx = 0; size_t len = username.length(); for (size_t i = 0; i < len; ++i) { for (size_t j = len; j > 0; --j) { unsigned char ch_i = static_cast<unsigned char>(username[i]); unsigned char ch_j = static_cast<unsigned char>(username[j - 1]); ebx += static_cast<long long>(ch_i) * ch_j * edi; } } ebx %= 0xA2C2A; // 666666 std::cout << "EBX为: " << ebx << std::endl; ebx -= 1; int code = 0; // 原C代码中的近似求解 code = static_cast<int>(ebx * 0x59); // 取最大值 code = code - (code % 0x50) + 1; // 调整满足 %0x50 条件 std::cout << "正确注册码为: " << code << "\n\n"; // 枚举所有可能的解 std::cout << "所有可能的注册码:\n"; int start = static_cast<int>((ebx - 0x50) * 0x59); int end = static_cast<int>(ebx * 0x59); for (int i = start; i < end; ++i) { if (i / 0x59 + i % 0x50 == ebx) { std::cout << i << std::endl; } } std::cout << "\n按回车键退出..."; std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); std::cin.get(); return 0; }