前置知识
- 熟悉OD的界面与功能
- 了解什么是断点
- C/C++基础知识
- 汇编指令
程序GUI
程序和用到的代码附件获取,切记不要本机运行!
信息收集
破解第一步,永远都是对程序的信息收集,不熟悉程序的使用,就是纸上谈兵。
点击 Help
--> register
,弹出注册框。
首先,根据GUI,我们就可以了解到,程序有可能使用的Windows API
。
-
GetDlgItemTextA
/GetWindowTextA
:函数获取到用户输入的内容, -
MessageBoxA
:显示一个模式对话框,其中包含系统图标、一组按钮和一条简短的应用程序特定消息,例如状态或错误信息。
当我们输入不同的Name来测试程序不同的响应,经过测试我们发现一下几种情况
- 当输入为空/包含数字时,程序会弹窗两次
- 当输入纯字母时,程序弹窗一次
因此我们可以猜测,程序会对输入的Name检测,只有纯字母时在可以继续注册。
Crack
这里面我们可以有两种破解方式:
- 直接修改程序的跳转,无论输入什么都可以正确注册
- 梳理程序逻辑,写出注册机
无情破解
这里面演示无情破解-一路通天式,即无论输入什么都破解成功。
1.API断点设置
前面我们猜测可能存在的函数,这里面我们来设置断点
首先在CPU界面,反汇编列,右键 -->查找 -->当前模块中的名称(或者 直接Ctrl +N)。
可以看到 如我们猜想的那样,存在GetDlgItemTextA
和MessageBoxA
由于这里面是无情破解,我们只在MessageBoxA
设置断点,左键点击选取,右键 --> 在输入函数上切换断电
MessageBoxA解释
https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-messageboxa
-
含义:
显示一个模式对话框,其中包含一个系统图标、一组按钮和一条简短的应用程序特定消息,例如状态或错误信息。 消息框返回一个整数值,该值指示用户单击了哪个按钮。
-
函数原型
int MessageBoxA( [in, optional] HWND hWnd, [in, optional] LPCSTR lpText, [in, optional] LPCSTR lpCaption, [in] UINT uType );
参数详解:
-
[in, optional] hWnd
类型:
HWND
要创建的消息框的所有者窗口的句柄。 如果此参数为 NULL,则消息框没有所有者窗口。
-
-
[in, optional] lpText
类型:
LPCTSTR
要显示的消息。 如果字符串由多行组成,则可以在每行之间使用回车符和/或换行符分隔这些行。
-
[in, optional] lpCaption
类型:
LPCTSTR
对话框标题。 如果此参数为 NULL,则默认标题为 Error。
-
[in] uType
类型:
UINT
对话框的内容和行为。 此参数可以是以下标志组中的标志的组合。
若要指示消息框中显示的按钮,请指定以下值之一。
返回值:
-
类型:
int
- 如果消息框有“取消”按钮,则如果按下 ESC 键或选择了“取消”按钮,函数将返回 IDCANCEL 值。
- 如果消息框没有 “取消 ”按钮,则按 ESC 将不起作用 - 除非存在MB_OK按钮。 如果显示MB_OK按钮,并且用户按 ESC,则返回值为 IDOK。
- 如果函数失败,则返回值为零。 要获得更多的错误信息,请调用 GetLastError。
- 如果函数成功,则返回值为以下菜单项值之一。
我们可以在BreakPoint窗口发现我们设置的断点
2.运行追踪
第一次弹窗-Name验证
F9运行程序,然后在任务栏可以发现我们要破解的程序(任务栏未显示则可以将OD页面最小化寻找),然后我们注册。
Name : 9999,Serial : 8888
,这里是为了区别。点击OK后我们发现程序停留在0x77D507EA
,这是在MessageBoxA
函数内部。
然后我们在右下角堆栈区,可以发现函数信息,点击数值0x004013C1
,这是函数返回到主程序的内存地址。
右键 --> 在反汇编窗口中跟随
我们跟踪发现,retn语句可以由0x004013AA
直接跳转到,且跳过0x004013ABC
MessageBoxA
函数调用语句。
向上继续分析,发现在0x0040139C
可以由0x00401387
判断跳转,因为我们输入,要想使得其永远跳转,我们可以修改成无条件跳转。
双击 0x00401387
,会弹出一个汇编窗口,可以修改会汇编语句
将je short 0040139C
修改为jmp short 0040139C
第二次弹窗-Serinal验证
此时我们F9执行,此时会弹窗一次,我们点击弹窗OK,让程序继续运行,程序会停留到第二次弹窗。
PS:这里之所以会弹窗,是因为我们刚才修改的是他已经执行过的语句。
跟前面同样的操作0x0040137D
,右键 在反汇编窗口跟随
我们在这个程序段,发现此程序段是由0x00401245
调用的,继续追踪
我们分析发现,在这个函数调用窗口上,有一个跳转语句,当跳转执行时,我们可以绕过该函数
此时跟前面一样,我们直接修改汇编代码,修改为无条件跳转代码
je short 0040124C --> jmp short 0040124C
当修改后保存,在反汇编窗口,右键 -->复制到可执行文件 --> 所有修改
在新弹窗中 点击全部复制
在新弹窗中,直接右键 -->保存文件
修改文件名,点击保存即可
3.破解核实
在文件夹中找到我们破解的程序,运行他
点击OK 发现成功注册
注册机 - 算法
1.API断点设置
注册机API设置断点与前面步骤类似,不过这里面断点设置为GetDlgItemTextA
GetDlgItemTextA
解释
https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-getdlgitemtexta
-
含义:
检索与对话框中的控件关联的标题或文本。
-
函数原型
UINT GetDlgItemTextA( [in] HWND hDlg, [in] int nIDDlgItem, [out] LPSTR lpString, [in] int cchMax );
参数列表:
-
[in] hDlg
类型:
HWND
包含控件的对话框的句柄。
-
-
[in] nIDDlgItem
类型:
int
要检索其标题或文本的控件的标识符。
-
[out] lpString
类型:
LPTSTR
用于接收标题或文本的缓冲区。
-
[in] cchMax
类型:
int
要复制到
pString
指向的缓冲区的字符串的最大长度(以字符为单位)。如果字符串的长度(包括 null 字符)超出限制,则字符串将被截断。
返回值
-
类型:
UINT
-
如果函数成功,则返回值将指定复制到缓冲区的字符数,不包括终止 null 字符。
-
如果函数失败,则返回值为零。 要获得更多的错误信息,请调用 GetLastError。
2.程序追踪
然后我们F9运行程序,弹出注册框,输入我们的用户与序列号 Test : 666666
,发现我们点击后,处于中断程序
在右下边对战窗口中,我们发现buffer
参数,这个参数的内存地址就是我们输入的Test写入的内存地址。
点击导航栏 调试 -->执行到函数返回(Ctrl + F9
)运行到函数返回语句,
与Alt + F9
的区别时,一个执行到返回语句(不执行返回语句),一个执行到返回主程序,(执行返回语句后,返回的页面)
我们发现,我们输入的Test写到了内存
后续的Serial也是同样的步骤
3.内存断点
此时注意,我们要设置一个内存断点,因为一般注册,会根据Name计算值,然后再匹配输入的Serial值。
左键选取Test的16进制,右键 --> 断点 --> 内存访问
后面的Serinal也要设置断点,但是由于内存断点只能设置一个,所以我们先不设置。
PS:这里为什么不设置硬件断点,是为了方便分析,目前只考虑Name的算法。
4.Name算法
我们F9运行到Name算法,即当我们前面输入Test : 666666
后,按两次F9即可
我们逐步分析
0040137E /$ 8B7424 04 mov esi,dword ptr ss:[esp+0x4] ; 将输入的用户名的地址 赋值给ESI
00401382 |. 56 push esi ; ESI值入栈
00401383 |> 8A06 /mov al,byte ptr ds:[esi] ; 将用户名第一个字符机器码 赋值给AL
00401385 |. 84C0 |test al,al ; 这里面有两个功能,1判断是否为空, 2判断是否已到字符串末尾
00401387 74 13 je short CRACKME.0040139C ; 为空则跳转至 0x40139A
00401389 |. 3C 41 |cmp al,0x41 ; 判断该字符是否为小于A,再结合0x0040138D,即检测是否为字母
0040138B |. 72 1F |jb short CRACKME.004013AC ; 若不是,则跳转
0040138D |. 3C 5A |cmp al,0x5A ; 判断该字符是否小于0x5A即 Z,跟上面的0x0040139结合判断
0040138F |. 73 03 |jnb short CRACKME.00401394 ; 这里判断是否为大写字母,当字符的ASCII码大于5A,则有可能是小写字母
00401391 |. 46 |inc esi ; 这里面ESI自增,是用来循环遍历输入的每一个字符
00401392 |.^ EB EF |jmp short CRACKME.00401383 ; 跳转到循环开始,重新检测
00401394 |> E8 39000000 |call CRACKME.004013D2 ; 当字符是小写字母后的处理
00401399 |. 46 |inc esi ; 同0x401391,因为是另个分支,所以自增语句
0040139A |.^ EB E7 \jmp short CRACKME.00401383 ; 同0x00401392,两个分支,有两个跳转语句
0040139C |> 5E pop esi ; ESI出栈,因为上面的检测改变了ES值,这里是恢复ESI值
小写字母处理
004013D2 /$ 2C 20 sub al,0x20 ; 将字符的ASCII码 - 0x20 因为大小写字母ASCII相差32 即十六进制0x20
004013D4 |. 8806 mov byte ptr ds:[esi],al ; 更改内存中的值
004013D6 \. C3 retn ; 返回
继续分析
- 将ESI重新指向Name地址,然后跳转到Name算法函数
0040139C |> \5E pop esi ; ESI出栈,因为上面的检测改变了ES值,这里是恢复ESI值
0040139D |. E8 20000000 call CRACKME.004013C2 ; 这个函数分析后是Name的算法,前面的检测
-
循环遍历Name的值,然后将其字符的ASCII码16进制数相加
T E S T 54 45 53 54 16进制 :54+45+43+54 = 0x140
004013C2 /$ 33FF xor edi,edi ; 清空EDI
004013C4 |. 33DB xor ebx,ebx ; 清空EDI
004013C6 |> 8A1E /mov bl,byte ptr ds:[esi] ; ESI存储的是Name的地址,此语句把单个字符的HEX赋值给BL
004013C8 |. 84DB |test bl,bl ; 检查是否为0
004013CA |. 74 05 |je short CRACKME.004013D1 ; 为0 则跳转至0x004013D1
004013CC |. 03FB |add edi,ebx ; 将EDI的值 加上EBX的值 复制给EDI
004013CE |. 46 |inc esi ; ESI自增益,为了循环遍历输入的NAME
004013CF |.^ EB F5 \jmp short CRACKME.004013C6 ; 跳转至循环开头
004013D1 \> C3 retn ; 当BL为0,即到字符为0或循环到串末尾后 跳转至0x004013CA
-
将相加后的结果与0x5678异或
0x140 ^ 0x5678 = 0x5738
004013A2 |. 81F7 78560000 xor edi,0x5678 ; 通过0x0040139D函数后,将得到的值与0x5678异或
004013A8 |. 8BC7 mov eax,edi ; 将结果付赋值给EAX
004013AA |. EB 15 jmp short CRACKME.004013C1 ; 算法结束,函数返回
C++语法
#include <iostream>
#include<string>
using std::cin; using std::cout; using std::endl;
using std::string;
int main()
{
string username;
int registerValue = 0;
std::getline(cin, username);
for (auto &c : username)
c = toupper(c);
for (int i = 0; i < username.length(); ++i)
{
registerValue += static_cast<int>(username[i]);
}
cout << registerValue;
}
5.Serinal算法
因为内存地址只能同时存在一个,所以我们可以两次运行程序,来分步分析
设置新的内存断点
注意,这里有个技巧,当断电多的时候,我们可以禁止断点而不是删除断点,内存断点在这里不显示,这里显示的是CC断点
F9当运行到Serial算法时,会中断,因为设置了内存断点
实际上这个算法,就是计算我们输入字符串转换成其16进制整数
字符 666666
将他看成十进制 666666
转换成十六进制 A2C2A
004013D8 /$ 33C0 xor eax,eax ; 清空EAX
004013DA |. 33FF xor edi,edi ; 清空EDI
004013DC |. 33DB xor ebx,ebx ; 清空EBX
004013DE |. 8B7424 04 mov esi,dword ptr ss:[esp+0x4] ; 将Serial的内存地址付赋值给ESI
004013E2 |> B0 0A /mov al,0xA ; AL赋值为 0xA
004013E4 |. 8A1E |mov bl,byte ptr ds:[esi] ; 将字符的ASCII码赋值飞AL
004013E6 |. 84DB |test bl,bl ; 判断是否为0
004013E8 |. 74 0B |je short CRACKME.004013F5 ; 为空则跳转至 0x4013F5,不为空则向下执行
004013EA |. 80EB 30 |sub bl,0x30 ; 将字符的机器码 - 0x30
004013ED |. 0FAFF8 |imul edi,eax ; 将 EDI 与 EAX的值相乘 赋值给 EDI
004013F0 |. 03FB |add edi,ebx ; EDI 加上 EBX 即 加上字符的机器码-0x30
004013F2 |. 46 |inc esi ; ESI自加 ,即循环输入的Serial
004013F3 |.^ EB ED \jmp short CRACKME.004013E2 ; 跳转至循环开始处
004013F5 |> 81F7 34120000 xor edi,0x1234 ; 循环结束后,将得到的值与0x1234异或
004013FB |. 8BDF mov ebx,edi ; 将异或的值 赋值飞EBX
004013FD \. C3 retn
C++
#include <iostream>
#include<string>
using std::cin; using std::cout; using std::endl;
using std::string;
int main()
{
string str;
int sum = 0, temp = 0;
std::getline(cin, str);
for (int i = 0; i < str.length(); ++i)
{
temp = str[i] - 0x30;
sum = sum * 0xA + temp;
}
cout << std::hex << sum << endl;
}
6.注册机
当从算法出来后,就是最终比较
即是这连个算法结果相比
-
是Name的大写字母ASCII码相加后的结果与0x5678异或结果,
-
输入的字符串(按照十进制数)转换后的16进制数与0x1234异或结果相比
注册机代码
#include <iostream>
#include <string>
#include <algorithm> // std::all_of
#include <cctype> // std::isalpha
using std::cin;
using std::cout;
using std::endl;
using std::string;
bool isAllAlphabetic(const string& str) {
return std::all_of(str.begin(), str.end(), [](unsigned char c) {
return std::isalpha(c);
});
}
int main() {
string username;
int registerValue = 0;
while (true) {
cout << "Name: ";
std::getline(cin, username);
// 检查输入的字符串是否全部由字母组成
if (!isAllAlphabetic(username)) {
cout << "Error: The name must contain only alphabetic characters. Please try again." << endl;
continue;
}
break;
}
for (auto& c : username)
c = toupper(c); // 将小写都转换为大写
for (int i = 0; i < username.length(); ++i)
registerValue += username[i]; // 每个字母的ASCII值相加
registerValue ^= 0x5678 ^ 0x1234; // 相加后的结果 异或0x5678,再异或0x1234
// 因为有这样一个特点 A^B^B == A
cout << "Serial: " << registerValue << endl;
return 0;
}
验证
- Yu9.zip 下载