深入理解逆向工程:利用OD进行汇编级别的代码分析与调试
Yu9 发表于 河南 二进制安全 2129浏览 · 2024-05-14 08:30

前置知识

  • 熟悉OD的界面与功能
  • 了解什么是断点
  • C/C++基础知识
  • 汇编指令

程序GUI

程序和用到的代码附件获取,切记不要本机运行!

信息收集

破解第一步,永远都是对程序的信息收集,不熟悉程序的使用,就是纸上谈兵。

点击 Help --> register,弹出注册框。

首先,根据GUI,我们就可以了解到,程序有可能使用的Windows API

  • GetDlgItemTextA/GetWindowTextA:函数获取到用户输入的内容,
  • MessageBoxA:显示一个模式对话框,其中包含系统图标、一组按钮和一条简短的应用程序特定消息,例如状态或错误信息。

当我们输入不同的Name来测试程序不同的响应,经过测试我们发现一下几种情况

  1. 当输入为空/包含数字时,程序会弹窗两次
  2. 当输入纯字母时,程序弹窗一次

因此我们可以猜测,程序会对输入的Name检测,只有纯字母时在可以继续注册。

Crack

这里面我们可以有两种破解方式:

  • 直接修改程序的跳转,无论输入什么都可以正确注册
  • 梳理程序逻辑,写出注册机

无情破解

这里面演示无情破解-一路通天式,即无论输入什么都破解成功。

1.API断点设置

前面我们猜测可能存在的函数,这里面我们来设置断点

首先在CPU界面,反汇编列,右键 -->查找 -->当前模块中的名称(或者 直接Ctrl +N)。

可以看到 如我们猜想的那样,存在GetDlgItemTextAMessageBoxA

由于这里面是无情破解,我们只在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                                     ;  返回

继续分析

  1. 将ESI重新指向Name地址,然后跳转到Name算法函数
0040139C  |> \5E            pop esi                                  ;  ESI出栈,因为上面的检测改变了ES值,这里是恢复ESI值
0040139D  |.  E8 20000000   call CRACKME.004013C2                    ;  这个函数分析后是Name的算法,前面的检测
  1. 循环遍历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
  1. 将相加后的结果与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;
}

验证

附件:
0 条评论
某人
表情
可输入 255