前言

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。(本文仅用于交流学习),本文仅作技术研究。

本文涉及到的案例可在附件获取。

导读

本文章主要分享,在学习逆向工程中,所遇到的常见注册类型。

注册类型

硬编码

"硬编码注册"是指在软件的源代码中直接嵌入注册密钥或许可证信息。

信息收集

运行程序,输入Serial,点击Check,弹出错误信息。

调试

导入OD

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

  • GetWindowTextA:用来获取我们输入的Serial
  • lstrcmA:对比我们输入的Serial与真正的Serial
  • MessageBoxA:根据对比结果弹窗。

我们在这这几个函数设置断点,可以使用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,验证

软编码

软编码注册, 或者称之为动态生成序列号的方式,与硬编码注册相反,其并没有将序列号直接植入到程序代码中。而是通过一种算法在用户进行注册时动态地生成激活密钥。

软编码注册的步骤大致如下:

  1. 用户在软件中申请注册,提供必要的用户信息。
  2. 软件用这些信息作为输入,通过内嵌的算法生成序列号或激活秘钥。
  3. 用户使用生成的序列号或激活秘钥进行软件激活。
  4. 软件接收到激活秘钥后,会再次进行校验,确认秘钥的合法性。一般情况下,合法的秘钥会解锁软件的完整功能。

单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文件注册的基本步骤:

  1. 用户购买软件后,从供应商处获得一个Key文件,这个文件一般会通过电子邮件或者在线账户页面进行分发。
  2. 用户下载并保存这个Key文件在本地电脑上。
  3. 在软件的注册页面,用户选择Key文件进行注册。
  4. 软件读取Key文件,验证其中的注册信息是否有效。
  5. 如果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,发现成功验证。