DNF补丁大合集的内辅/广告去除思路:一次传统程序员的逆向初体验

jbxs-ixugiafbz5n5fbqouzmqbmbbhyin56m67xpiybg2j4lo7r4qdsvq

作为一个搞传统软件开发的程序员,平时习惯了写业务逻辑、搞架构设计,对于“外挂”、“逆向”、“汇编”这些领域,一直觉得隔行如隔山。

最近手头有个“DNF补丁大合集”的工具,好用是好用,但里面强制捆绑的“内辅窗口”和满屏的滚动广告实在让人难受。出于职业本能,我突然想:既然是代码写的,就一定有逻辑;既然有逻辑,就一定能改。

于是,我决定开启这扇“新世界的大门”。

一、 起手式:解包

拿到手的 DNF.exe 显然是被修改打包过的。第一步自然是还原它的真面目。
我使用了 evbunpacker 对其进行解包。

  1. DNF.exe 拖入解包工具。
  2. 提取出所有文件到一个独立目录。
  3. 将解包出来的文件覆盖回游戏目录(为了保证环境一致),同时保留解包目录备用。

二、 补课:Cheat Engine 从入门到放弃...再入门

解包完成后,我自信满满地下载了传说中的神器 Cheat Engine (CE)

然而打开软件的那一刻,我看着满屏的十六进制和陌生的按钮,大脑一片空白——这玩意儿怎么用? 我以前只知道它能改单机游戏金币,真要拿来分析一个复杂的补丁,我心里完全没底。

好在 CE 非常贴心地自带了一个 Tutorial (新手教程)。为了不至于出师未捷身先死,我耐着性子把教程从头到尾刷了一遍:

  • Step 2:学会了怎么搜“精确数值”(比如血量是100,就搜100)。
  • Step 3:学会了搜“未知的初始值”和“数值减少了”,这招专门用来对付没有具体数字显示的进度条。
  • Step 6:接触到了“指针”的概念,原来内存地址是会变的,得找到它的基址(废话, 我当然直到, 只不过第一次以这种原生的视角看指针, 这也让我的本职专业有了更多的感悟)。
  • Step 9:终于碰到了“注入代码”,学会了怎么把原来的指令替换成 NOP(空指令)。
  • Step 10:简单了解了一下常见的汇编命令。

通关教程后,我感觉自己仿佛打通了任督二脉,看着手里的 DNF 补丁,心里默念:“这就拿你练手!”

三、 实战“案发现场”:活学活用

启动游戏,看着那个碍眼的内辅窗口,我深吸一口气,开始运用刚学的招数。

既然教程里教过“变动数值查找”,那我就拿这个窗口做实验:

  1. 窗口显示时,我猜测内存里肯定有个变量是 1 (或者其他非0值)。
  2. 窗口隐藏时,这个变量应该是 0

经过几轮筛选(打开窗口搜1,关闭窗口搜0,反复几次),结果出奇的顺利!我很快锁定了一个内存地址。接着,我右键点击这个地址,选择了教程里教的那一招——“Find out what writes to this address” (找出是谁改写了这个地址)

CE 瞬间捕获到了那条关键的汇编指令:

d3d9.dll+140A9E:
6CD00A98 - 8B 73 04 - mov esi,[ebx+04]
6CD00A9B - 8B 7B 08 - mov edi,[ebx+08]
6CD00A9E - 89 53 38 - mov [ebx+38],edx  << 凶手找到了!
6CD00AA1 - 8B 43 68 - mov eax,[ebx+68]
6CD00AA4 - 89 73 48 - mov [ebx+48],esi

观察与分析:

  • 模块来源d3d9.dll。这名字太熟了,DirectX 9 的核心库。显然,补丁作者利用了 Windows 的 DLL 劫持 技术,放了一个假的 d3d9.dll 在游戏目录,优先被游戏加载。
  • 关键行为mov [ebx+38], edx
  • 状态确认:通过观察寄存器,当内辅窗口打开时,EDX 的值为 1;窗口关闭时,EDX0

四、 深度解读(职业病犯了)

作为一个写代码的,虽然我是 CE 新手,但看到这段汇编,我忍不住用程序员的思维去推演它背后的逻辑。

此时寄存器状态:

  • EBX = 07207978 (基址)
  • EDX = 1 (状态值)

结合上下文,这看起来像是一个 渲染命令包 的处理过程。D3D9 在构建渲染任务时,补丁作者插入了代码,强制将 [ebx+38] 赋值为 1,告诉渲染引擎“把我的窗口画出来”。

验证猜想:
我想起 CE 教程里的“代码注入”一章,于是直接右键 -> Replace with code that does nothing (NOP)
也就是把 89 53 38 (mov [ebx+38],edx) 替换为 90 90 90 (空指令)。

神奇的一幕来了:内辅窗口瞬间消失,世界清静了。

本来以为要大战三百回合,没想到刚学完教程的第一招就把它秒了。

五、 移花接木:二次劫持 (Double Hijack)

既然手动 NOP 有效,那接下来的任务就是把这个操作自动化。总不能每次玩游戏都开个 CE 手动改吧?原补丁有F3热键可以关闭/打开, 我连按下F3都觉得费劲, 所以如果不做成自动化的, 那么将毫无意义.

思路如下:

  1. 原补丁是通过劫持游戏的 d3d9.dll 实现的。
  2. 我不能删掉原补丁的 DLL(否则补丁功能也没了)。
  3. 计策:我也写一个 d3d9.dll 劫持游戏,然后把原补丁的 DLL 改名为 plugin.dll
  4. 调用链:游戏 -> 我的 d3d9.dll -> (加载并修补) -> 原 plugin.dll -> 系统 DirectX。

这就是传说中的“给外挂打外挂”。

5.1 编写代码 (C++)

打开 VS,新建一个 C++ DLL 工程。这里有个坑:原补丁可能加了壳,或者代码是动态解压的。如果我在 DLL 加载瞬间就去改内存,可能会因为代码还没解压出来而修改失败。

所以我采用了一个“异步等待脱壳”的策略:启动一个线程,死循环检测目标内存地址,直到发现它变成了预期的机器码,再动手修改。

核心代码实现:

#include <windows.h>
#include <iostream>

// --- 1. 导出转发 ---
// 必须导出 Direct3DCreate9,否则游戏无法启动
// 我们先拦截,然后偷偷转交给 plugin.dll (原补丁)
typedef void* (WINAPI* tDirect3DCreate9)(UINT);
tDirect3DCreate9 oDirect3DCreate9 = nullptr;

extern "C" __declspec(dllexport) void* WINAPI Direct3DCreate9(UINT SDKVersion)
{
    if (!oDirect3DCreate9)
    {
        HMODULE hPlugin = LoadLibraryA("plugin.dll"); // 加载原补丁
        if (hPlugin) {
            oDirect3DCreate9 = (tDirect3DCreate9)GetProcAddress(hPlugin, "Direct3DCreate9");
        }
    }
    if (oDirect3DCreate9) {
        return oDirect3DCreate9(SDKVersion); // 假装无事发生,继续传递
    }
    return nullptr;
}

// --- 2. 内存补丁逻辑 ---
const DWORD TARGET_OFFSET = 0x140A9E; // CE 里抓到的偏移
const BYTE EXPECTED_BYTES[] = { 0x89, 0x53, 0x38 }; // 原指令机器码
const BYTE NOP_BYTES[] = { 0x90, 0x90, 0x90 };      // NOP

void PatchThread(HMODULE hMyModule)
{
    // 等待 plugin.dll 加载
    HMODULE hPlugin = NULL;
    while ((hPlugin = GetModuleHandleA("plugin.dll")) == NULL) {
        Sleep(100);
    }

    BYTE* targetAddr = (BYTE*)((DWORD)hPlugin + TARGET_OFFSET);

    // --- 关键逻辑:等待脱壳完成 ---
    // 只有当内存里的字节变成了我们熟悉的 89 53 38,说明壳解开了,代码还原了
    while (true)
    {
        if (memcmp(targetAddr, EXPECTED_BYTES, sizeof(EXPECTED_BYTES)) == 0)
        {
            // 动手!
            DWORD oldProtect;
            VirtualProtect(targetAddr, sizeof(NOP_BYTES), PAGE_EXECUTE_READWRITE, &oldProtect);
            memcpy(targetAddr, NOP_BYTES, sizeof(NOP_BYTES)); // 写入 NOP
            VirtualProtect(targetAddr, sizeof(NOP_BYTES), oldProtect, &oldProtect);
            
            // 搞定收工
            break; 
        }
        Sleep(100);
    }
}

// --- 3. 入口 ---
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
    if (ul_reason_for_call == DLL_PROCESS_ATTACH)
    {
        DisableThreadLibraryCalls(hModule);
        // 启动监控线程
        CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)PatchThread, hModule, 0, NULL);
        // 主动加载原补丁
        LoadLibraryA("plugin.dll");
    }
    return TRUE;
}

别忘了创建一个 d3d9.def 文件来定义导出,不然游戏会报错找不到入口:

LIBRARY "d3d9"
EXPORTS
Direct3DCreate9

编译,选择 x86 Release 模式(因为 DNF 是 32 位程序)。

六、 重新打包:完美伪装

代码写好了,接下来就是组装。

  1. 把原补丁的 d3d9.dll 重命名为 plugin.dll
  2. 把我编译生成的 d3d9.dll 放入目录。
  3. 上游戏测试——完美!内辅窗口消失,功能一切正常。

最后一步,为了整洁,我使用 Enigma Virtual Box 把这些零零碎碎的文件重新打包进 DNF.exe

  • 将解包目录下的所有依赖文件(包括我的 d3d9.dllplugin.dll)全部拖入 Enigma。
  • 生成新的 DNF.exe

清理现场:
删除游戏目录下乱七八糟的解包文件,只保留登录器、audio.xmlDNF.toml 和资源目录,放入新生成的 DNF.exe

七、 总结

这次逆向之旅比我想象中要简单有趣。

  • 技术层面:其实就是利用了 DLL 劫持链(Game -> MyDll -> OriginalDll)。
  • 思维层面:作为一个开发者,对内存结构和指令逻辑的敏感度帮了大忙。看到 mov [ebx+38], 1 能联想到 Boolean 标记,这让定位关键点变得非常快。

虽然隔行如隔山,但代码的底层逻辑是相通的。只要敢于打开那扇门(和 CE),你会发现新世界其实挺精彩的。

(注:本文纯属技术研究,请支持正版游戏环境。)

posted @ 2026-01-08 11:24  Only丿阿海  阅读(21)  评论(0)    收藏  举报