深入理解逆向工程之常见注册类型
前言 由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。(本文仅用于交流学习),本文仅作技术研究。 本文涉及到的案例可在附件获取。 导读 本文章主要分享,在学习逆向工程中,所遇到的常见注册类型。 注册类型 硬编码 "硬编码注册"是指在软件的源代码中直接嵌入注册密钥或许可证信息。 信息收集 运行程序,输入Serial,点击Check,
前言
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。(本文仅用于交流学习),本文仅作技术研究。
本文涉及到的案例可在附件获取。
导读
本文章主要分享,在学习逆向工程中,所遇到的常见注册类型。
注册类型
硬编码
"硬编码注册"是指在软件的源代码中直接嵌入注册密钥或许可证信息。
信息收集
运行程序,输入Serial,点击Check,弹出错误信息。

调试
导入OD

查看此程序可能用到的Windows API

GetWindowTextA:用来获取我们输入的SeriallstrcmA:对比我们输入的Serial与真正的SerialMessageBoxA:根据对比结果弹窗。
我们在这这几个函数设置断点,可以使用BP WindowsAPI快速设置函数断点。

PS:实际上可以单独设置lstrcmpA函数的断点,因为由于是硬编码,其Serial是固定的。
这里面我们按照正常流程来进行分析,当我们输入我们的Serial,后点击check程序会中断在GetWindowTextA

我们Alt + F9,直接将此函数运行完,返回主函数。
我们看到GetWindowTextA,后面紧跟着的是lstrcmpA,出现一个可疑字符串cannabis,猜测这就是真正的Serial。

我们继续F9,当遇到我们设置lstrcmpA的断点时,我们继续Alt+F9,返回到主程序。因为Reverse与cannabis不相等,所以EAX的值为非0。OR EAX,EAX,因为EAX!=0,所以此汇编指令后的结果也为非0。
接下来我们F8单步运行,我们运行到0x0040124F,因为前面结果为非0,所以ZF标志位为0,跳转语句生效。
我们观察跳转语句后的MessageboxA,其中的信息就是我们前面信息收集时弹窗的内容。

我们继续F9

因此我们得到正确的序列号cannabis,验证

软编码
软编码注册, 或者称之为动态生成序列号的方式,与硬编码注册相反,其并没有将序列号直接植入到程序代码中。而是通过一种算法在用户进行注册时动态地生成激活密钥。
软编码注册的步骤大致如下:
- 用户在软件中申请注册,提供必要的用户信息。
- 软件用这些信息作为输入,通过内嵌的算法生成序列号或激活秘钥。
- 用户使用生成的序列号或激活秘钥进行软件激活。
- 软件接收到激活秘钥后,会再次进行校验,确认秘钥的合法性。一般情况下,合法的秘钥会解锁软件的完整功能。
单Serial注册
abexcm5.exe
调试环境:
- Windows 10
- OllyDbg 110
信息收集
PE信息
使用Exeinfo PE查看,发现是汇编语言编写的,无壳,WindowsGUI程序

程序运行信息
运行程序,给出一个GUI,然后输入框输入Serial,点击check,会出现校验弹窗信息。

调试
首先,这里梳理一下思路,有一下几种情况可以作为切入点
- 字符串信息
- 程序可能使用的Windows API
- 消息函数-
MessageBox - 文本获取-
GetDlgItemText/GetWindowText
- 消息函数-
这里面我们使用文本获取窗口,因为一般都是获取后会立马跳转到算法,我们可以详细追踪。
abexcm5导入OD

使用Ctrl+N快捷键,或者在反汇编窗口,右键 --> 查找 -->当前模块中的名称

在当前模块中的名称 窗口,可以看到该程序使用的Widnows API,在GetDlgItemTextA函数上设置断点

然后,点击C,即可来到CPU窗口,按F9或者点击运行图标运行程序

我们输入Serial,点击check,程序会中断在我们这值得API断点上
在程序堆栈区,在栈顶那一行,右键,点击在反汇编窗口中跟随,即可回到主程序

或者,我们直接
Alt + F9,即直接执行完此函数,返回到主程序
这里我们Alt F9执行到主程序页面,当前EIP指向0x0040107D

算法分析

程序接下来要运行
GetVolumeInformationA函数用来获取磁盘信息https://learn.microsoft.com/zh-cn/windows/win32/api/fileapi/nf-fileapi-getvolumeinformationa然后通过
lstrcatA函数,将4562-ABEX拼接到VolumeNameBuffer再通过自定义流程,将新获取到的
VolumeNameBuffer,通过两次循环,每次循环将前四个字符+1再将
VolumeNameBuffer拼接到L2C-5781后面得到最终的字符串再与我们输入的Serial相比,根据对比结果,弹出对应窗口
注册机
#include<iostream>
#include<Windows.h>
int main()
{
char VolumeNameBuffer[0x32] = {0};
DWORD VolumeNameSize = sizeof(VolumeNameBuffer);
DWORD VolumeSerialNumber = 0;
DWORD MaxFilenameLength = 0;
DWORD FileSystemFlags = 0;
char Serial[MAX_PATH] = {};
if (GetVolumeInformationA(NULL, VolumeNameBuffer, VolumeNameSize, &VolumeSerialNumber, &MaxFilenameLength, &FileSystemFlags, NULL, NULL))
{
char str1[MAX_PATH] = "4562-ABEX";
char str2[MAX_PATH] = "L2C-5781";
lstrcatA(VolumeNameBuffer, str1);
std::cout << Serial << std::endl;
for (int i = 0; i < 2; ++i)
{
VolumeNameBuffer[0] += 1;
VolumeNameBuffer[1] += 1;
VolumeNameBuffer[2] += 1;
VolumeNameBuffer[3] += 1;
}
lstrcatA(Serial, str2);
lstrcatA(Serial, VolumeNameBuffer);
std::cout << Serial << std::endl;
}
else
{
std::cerr << "GetVolumeInformationA failed with error code: " << GetLastError() << std::endl;
}
return 0;
}
PS:注意,这里面注册机程序要和注册程序放在同一目录,这样搞获取的文件系统与磁盘信息才一致。

Name/Serial注册
信息收集
我们发现点击后,程序无响应,那么只有输入正确的Serial才会注册成功。

OD调试
导入OD

查看程序使用的Windows API,在GetDlgItemTextA设置断点

F9运行,输入Reverse/88888888,点击验证后,程序断在GetDlgItemTextA函数口。

我们ctrl + F9 可以看到,buffer区域存储的是我们输入的Name,地址0x00403080

F8运行,我们发现,该程序会对Name的长度进行一个校验,长度需要≥5。

我们继续运行,运行到第二次调用GetDlgItemTextA函数,这次存储的是我们输入的Serial。地址是0x00403280

我们继续向下查看代码,发现一个循环

汇编指令详情
00401148 |. B8 80304000 MOV EAX,AD_CM#2.00403080 ; ASCII "Reverse"
0040114D |. BB 80324000 MOV EBX,AD_CM#2.00403280 ; ASCII "88888888"
00401152 |. 8BCE MOV ECX,ESI ; 将Name长度复制给ECX
00401154 |> 8A10 /MOV DL,BYTE PTR DS:[EAX] ; 将单个字符赋值给DL
00401156 |. 2AD1 |SUB DL,CL ; DL减去CL的值
00401158 |. 3813 |CMP BYTE PTR DS:[EBX],DL ; 将计算后的值与Serial对应的单个字符相比
0040115A |. 75 18 |JNZ SHORT AD_CM#2.00401174 ; 只要有一个不想等,就跳出循环,跳转至0x00401174
0040115C |. 40 |INC EAX ; EAX+1,即遍历Name
0040115D |. 43 |INC EBX ; EBX+1 便利对应的Serial
0040115E |.^ E2 F4 \LOOPD SHORT AD_CM#2.00401154 ; 循环对比 LOOPD循环,循环会减少ECX的值
注册机
#include <iostream>
#include <vector>
#include <string>
int main() {
std::string Name;
std::string Serial;
std::cout << "Please enter your name(length >= 5): ";
while (std::getline(std::cin, Name))
{
if (Name.length()<5)
{
std::cout << "Your name must be at least five characters! Enter your Name: ";
continue;
}
else
{
for (int i = 0; i < Name.length(); ++i)
Serial.push_back(Name[i] - (Name.length()-i));
break;
}
}
std::cout << "Your Serial is :\"" << Serial << "\"" << std::endl;
return 0;
}

Key文件
Key文件注册也是一种常见的软件注册方式,这种方式的主要特点在于,它使用一个特定的文件(通常被称为“Key文件”或者“许可文件”)来作为软件的注册凭证,而非一个简单的序列号或激活码。
以下是Key文件注册的基本步骤:
- 用户购买软件后,从供应商处获得一个Key文件,这个文件一般会通过电子邮件或者在线账户页面进行分发。
- 用户下载并保存这个Key文件在本地电脑上。
- 在软件的注册页面,用户选择Key文件进行注册。
- 软件读取Key文件,验证其中的注册信息是否有效。
- 如果Key文件验证成功,那么软件就会被注册并激活,用户便可以使用软件的全部功能。
信息收集
运行程序,发现没有注册框,那么这种就有可能是通过KeyFile来获取注册码

调试
将程序导入OD,我们在汇编窗口,就发一个APICrateFileA。
CreateFileA函数功能是创建或打开文件或I/O设备。FileName参数即要打开的文件(CRACKME3.KEY)。

执行过CrateFileA函数后,会校验是否成功打开CRACKME3.KEY,如果正常打开,EAX存储的是该文件的句柄,未成功打开即EAX存储的为FFFFFFFF。

我们向下查看汇编指令,发现ReadFile函数,其中发现要读取的字节数为0x12 即 18个字节,即18个字符
我们自建CRACKME3.KEY,里面填入123456789ABCDEFFFF

执行过ReadFile函数后,有一个自定义函数,参数是KeyFile的内容,则有可能是算法函数

F7跟踪函数,跟踪发现确实是Serial算法

算法如下:
- 将KeyFile前14个字符进行计算
- 字符遍历与
A-N相异或 - 将异或后的字符的Hex值累加
执行完函数,返回到主程序,我们发现,异或后累加的值要与0x12345678再次进行异或处理,

后面又有一个自定义函数,我们继续追踪,此程序只是获取剩余的四个字符。

然后比较这四个字符的Hex与前面与0x123456788异或后的值是否相等。
这里的比较就是判断是否能成功注册

注册机
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
int main() {
std::string username;
std::cout << "请输入用户名(不超过14位):" << std::endl;
std::getline(std::cin, username);
if (username.length() > 14) {
std::cout << "请输入小于14位的用户名!" << std::endl;
return 0;
}
username.resize(14, ' '); // 如果用户名不足14位,用空格填充
int tmp_sum = 0;
std::vector<char> crypt_result;
for (int i = 0; i < 14; ++i) {
char xor_key = 'A' + i;
char user_chr = username[i];
char xor_user_char = xor_key ^ user_chr;
crypt_result.push_back(xor_user_char);
tmp_sum = tmp_sum + static_cast<int>(user_chr);
}
// 对用户名ascii码累加结果进行加密
int key = tmp_sum ^ 0x12345678;
std::ofstream outfile("CRACKME3.KEY", std::ofstream::binary| std::ofstream::out);
// 将加密后的用户名字符串写入文件
outfile.write(reinterpret_cast<const char*>(&crypt_result[0]), crypt_result.size());
// 将用户名字串密钥写入文件
outfile.write(reinterpret_cast<const char*>(&key), sizeof(key));
outfile.close();
std::cout << "密钥文件已生成!请拷贝到软件所在目录下使用!" << std::endl;
return 0;
}
验证,可以看到在当前文件生成一个key文件

我们点击Cruehead-CrackMe-3.EXE,发现成功验证。
