【原创】从开源软件学习windows下游戏插件的开发快速入门 (1)

  • Post author:
  • Post category:其他


首先,我们先看下一些法律基础,以确保大家是在学习插件,而不是wai挂。后者是国家严厉打击的。

一个游戏插件的发布,前提是要得到游戏开发或者发行商的许可(或者有利游戏方不会封禁,比如直播软件),其中包括了功能会不会破环游戏平衡性的讨论和达成共识。

当然,今天这个快速入门版的文章假设了已告知anti-cheat开发团队我们不会做坏事,在允许的范围下做事情。

那么,我们开始今天的旅程。



一、选择开源软件学习在游戏上显示内容

刚才我们学习了一下法律小基础,里面提到了一个tricky的地方是可以有利游戏方不会封禁,这个是因为软件安装在用户的电脑上,用户有一定权力去确保自己的系统运行在自己的方式上。比如我想直播一个游戏,除了游戏画面,我想放一些文字比如战绩比如广告,然后推送流(直播软件无法一个一个和游戏方谈判的,但是直播有利于更多玩家进入所以游戏方默许;再一个例子是杀毒软件,因为kernel driver的控制权问题,游戏制作发行方一般也不会太深究)。这里当然方法很多。比如可以使用录屏然后后期处理加上各种特效显示文字,这样直播人看不到这些特效;所以还有一种方法是注入一些代码到游戏程序上,然后画出想显示的内容,这样在直播的时候,主播可以看到内容,观众也可以看到,就比较方便了。

其实有很多方法显示,我们就快速过一下常用的两种方式。

第一种就是非侵入式的,但是对游戏的设置有要求。很简单,就是画个窗口显示内容,然后保证这个窗口top-most在所有窗口最前面显示就好。所以这个方法的好处就是,不会修改游戏的任何地方就能显示内容,坏处是游戏必须处于非全屏独占模式(无边框模式或者窗口模式),不然top-most是不生效的。github上也有一大把repo,比如

https://github.com/lolp1/Overlay.NET

当然overlay.net稍微和我们说的有点差别,它是创建一个窗口,然后把窗口塞到目标上去。

关键代码比如DirectX下,CreateWindow 再 DwmExtendFrameIntoClientArea 就能实现这部分功能了

......
        private bool CreateWindow() {
            Handle = Native.CreateWindowEx(
                WindowConstants.WindowExStyleDx,
                WindowConstants.DesktopClass,
                "",
                WindowConstants.WindowStyleDx,
                X,
                Y,
                Width,
                Height,
                IntPtr.Zero,
                IntPtr.Zero,
                IntPtr.Zero,
                IntPtr.Zero);

            if (Handle == IntPtr.Zero) {
                return false;
            }

            Native.SetLayeredWindowAttributes(Handle, 0, 255, WindowConstants.LwaAlpha);
......
        private void ExtendFrameIntoClient() {
            _margin.cxLeftWidth = X;
            _margin.cxRightWidth = Width;
            _margin.cyBottomHeight = Height;
            _margin.cyTopHeight = Y;
            Native.DwmExtendFrameIntoClientArea(Handle, ref _margin);
        }

第二种就是通过注入DLL的方式,hook到游戏的画面刷新函数,因为像windows游戏底层最终要么是DirectX要么是GL,很少会有开发商比出新裁自己开发底层库,所以函数就那么几个系列。所以,这里我们可以学习有名的直播软件OBS的代码

https://github.com/obsproject/obs-studio

首先简单介绍下DLL注入,这个其实就是柔和版的shellcode,注入一个自己的DLL到一个进程里,这样可以触发DLL的加载,从而在进程内部运行一次初始化DLL的代码,这样就可以在知道函数偏移以后,替换成自己的函数,达到hook的目的。如何知道函数偏移这个我们后面第二部分再说。不过,这里还要提一嘴,这个DLL的注入一般都是被anti-cheat监控的,所以并不是100%成功,当然一般没必要封死,毕竟还有诸如直播软件要用。

我们直接来看看obs干了啥吧,在这个程序里就是先

load_deubg_privilege

,这个是提权操作,确保最大成功率;当然,这个在driver面前就是个0,这个我们也后面再说。接着就执行了

inject_helper

。逆向嘛,也就是一堆压缩扭曲的源代码,麻烦点但是也是和代码一样追,我们看源代码多舒服(懒腰)…然后继续追着

inject_helper

里面有两种方法

inject_library_obf



inject_library_safe_obf

int main(void)
{
    wchar_t dll_path[MAX_PATH];
    LPWSTR pCommandLineW;
    int argc;
    LPWSTR *argv;
    int ret = INJECT_ERROR_INVALID_PARAMS;

    SetErrorMode(SEM_FAILCRITICALERRORS);
    load_debug_privilege();

    pCommandLineW = GetCommandLineW();
    argv = CommandLineToArgvW(pCommandLineW, &argc);
    if (argv) {
        if (argc == 4) {
            if (GetModuleFileNameW(NULL, dll_path, MAX_PATH))
                ret = inject_helper(argv, argv[1]);
        }

        LocalFree(argv);
    }

    return ret;
}

这里我就不贴一长串代码了,这两函数具体内容就写在一个文件里:

https://github.com/obsproject/obs-studio/blob/master/plugins/win-capture/inject-library.c

显然,为何

inject_library_safe_obf

叫safe,那是因为完全没有直接

WriteProcessMemory

硬核操作,safe的方式就是我使用Windows API给进程的主线程注册一个事件callback,这样线程比如是待窗口的,那么除了它会自动加载你的DLL到进程里,还可以截获诸如鼠标键盘操作的事件;这个方法注册完回调,

PostThreadMessage

触发一下,确保DLL在进程中加载;当然你看到了RETRY,那是因为这个是走windows的消息队列,注册表里默认10000的长度,万一丢包了可以重试增加成功率;这个方法也得看bit的,一般32位注入32位程序,64位注入64位的,所以这个程序就会编译成俩。另一个方法,就是硬核写入进程内存,十分粗鲁,大家自己看吧

create_remote_thread

是启动DLL的重点。另外提一嘴,想学习更多注入方法,可以看看

https://github.com/vinjn/injector

方法还蛮多的。

之后我们可以参考如何实现 hook 从而在游戏画面中展示我们想要画出的内容。举起一个栗子:

https://github.com/obsproject/obs-studio/blob/master/plugins/win-capture/graphics-hook/d3d9-capture.cpp

我们就不管它是如何初始化和如何得到DirectX的画面要去推流的,我们看看OBS如何hook画面。 里面有个

manually_get_d3d9_addrs

函数,这个其实是如何得到DirectX的渲染关键函数,它拿到了vtable,然后使用特定index定位函数。之后就是替换这个函数,换成自己的,然后就可以在游戏内容绘制以后画你的内容,后面就是DirectX的知识了,好了,可以去学习imgui画界面了…

bool hook_d3d9(void)
{
    HMODULE d3d9_module = get_system_module("d3d9.dll");
    uint32_t d3d9_size;
    void *present_addr = nullptr;
    void *present_ex_addr = nullptr;
    void *present_swap_addr = nullptr;

    if (!d3d9_module) {
        return false;
    }

    d3d9_size = module_size(d3d9_module);

    if (global_hook_info->offsets.d3d9.present < d3d9_size &&
        global_hook_info->offsets.d3d9.present_ex < d3d9_size &&
        global_hook_info->offsets.d3d9.present_swap < d3d9_size) {

        present_addr = get_offset_addr(
            d3d9_module, global_hook_info->offsets.d3d9.present);
        present_ex_addr = get_offset_addr(
            d3d9_module, global_hook_info->offsets.d3d9.present_ex);
        present_swap_addr = get_offset_addr(
            d3d9_module,
            global_hook_info->offsets.d3d9.present_swap);
    } else {
        if (!dummy_window) {
            return false;
        }

        if (!manually_get_d3d9_addrs(d3d9_module, &present_addr,
                         &present_ex_addr,
                         &present_swap_addr)) {
            hlog("Failed to get D3D9 values");
            return true;
        }
    }

    if (!present_addr && !present_ex_addr && !present_swap_addr) {
        hlog("Invalid D3D9 values");
        return true;
    }

    DetourTransactionBegin();

    if (present_swap_addr) {
        RealPresentSwap = (present_swap_t)present_swap_addr;
        DetourAttach((PVOID *)&RealPresentSwap, hook_present_swap);
    }
    if (present_ex_addr) {
        RealPresentEx = (present_ex_t)present_ex_addr;
        DetourAttach((PVOID *)&RealPresentEx, hook_present_ex);
    }
    if (present_addr) {
        RealPresent = (present_t)present_addr;
        DetourAttach((PVOID *)&RealPresent, hook_present);
    }

    const LONG error = DetourTransactionCommit();
    const bool success = error == NO_ERROR;
    if (success) {
        if (RealPresentSwap)
            hlog("Hooked IDirect3DSwapChain9::Present");
        if (RealPresentEx)
            hlog("Hooked IDirect3DDevice9Ex::PresentEx");
        if (RealPresent)
            hlog("Hooked IDirect3DDevice9::Present");
        hlog("Hooked D3D9");
    } else {
        RealPresentSwap = nullptr;
        RealPresentEx = nullptr;
        RealPresent = nullptr;
        hlog("Failed to attach Detours hook: %ld", error);
    }

    return success;
}

好,我们稍微展开下那个vtable,这个其实是个编译器知识,

struct {
   int var;
   void (*fn)();
} *a;

简化点,就是编译一般是顺序放数据结构的,如果是32位的,在对齐的情况我们把a看成一个

void*

数组,

a[0]

对应

var



a[1]

对应

fn

的函数指针。



二、如何找到游戏中的数据

这个问题其实我们可以从单机游戏开始,尤其是没有压缩的保存文件。和上面稍微展开的vtable类似,当内存中的数据要存储到硬盘上的时候,一般就是直接serialize,所以可以很好的去分析保存文件学习内存中的样子。这里有一个分析仙剑本地保存文件的帖子

https://blog.csdn.net/prog_6103/article/details/6604276

这个只要多看看,自然就熟了;原来的金山游侠 fps2000旧时代的内存搜索或者cheat engine新时代的替代搜索内存就相当于这样在文件里找数据。

为了找到数据的位置,我们需要知道进程中任意内存中的内容,这个就是逆向的重点了。为了让它足够简单,我们会合理运用前一部分的知识。

单机游戏不会那么追求保护,所以可以先从单机游戏学习起来;一般刚才谈到的金山 fps ce直接搜索诸如血量之类的数值,就能找到那个位置。或者可以直接把相关游戏的exe dll拖到IDA里,找到写数据的位置。一个简单的例子,扫雷程序很简单,我们可以想想如果你写个程序会如何生成雷,应该是会用到随机数之类的函数,比如

rand

,所以扫雷exe拖到IDA里,找到

call rand

,顺藤摸瓜,就可以找到雷会存储到内存的哪个位置了。一般全局变量的地址都是固定的,这个就很好处理,写起插件来,只要能注入然后读取那个固定地址就好了。如果对于局部变量,一般它会存在heap堆里,遍历起来慢慢搜索;也有存在stack栈里的,比如一个函数while死循环就可以有一些变量在stack里,这个时候得先拿到进程PEB或者windows api可以得到模块的基础地址,exe其实也是一个模块,也有基址,然后找一些有特征的点,再计算这个点和数据的偏移,甚至hook一些必要的函数,让函数调用的时候通知出来也是一种方法。

网络游戏的保护很重要,它确保了游戏正常公平运营。所以才有了跌宕起伏的游戏和wai挂激战。一般游戏发布的时候exe dll都是要加壳的,anti-cheat都是会要kernel driver保护的。在和游戏开发和运营商谈拢后,比如插件可以被允许读取游戏数据,然后分析再给玩家出报告,让玩家打得更好,这个就是增加游戏的retention,有成就感就能一直打这个游戏。那么这个时候需要突破一些限制。

首先静态分析是比较复杂的,因为加壳了,所以脱壳dump很重要,这个看雪的教程还是刚刚的

https://www.kanxue.com/chm.htm?id=2277&pid=node1000293

但是大家会发现诸如apxx 原x这样的重量级联网游戏,会有anti-cheat去patch内核层面的函数来达到保护(实际是一种奇怪的对抗了,装个游戏并且还要在用户机器上装一个rootkit…最理想的情况是服务器端通过各种检测来分析数据;但是为了节省成本和性能损耗网络游戏还是会在客户端这样保护),大家会发现attach debugger会失败,因为anti-cheat屏蔽了权限。这个权限大家可以参考微软自己的文档

https://learn.microsoft.com/zh-CN/windows/win32/procthread/process-security-and-access-rights

想要突破这个process的权限,有很多方法;我们回到开源上,比如

https://github.com/notscimmy/libelevate

(这个就有点过了,只是学习的时候自己可以用) 通过隐藏的数据结构重新拿到权限,当然anti-cheat会一直扫描,所以这就是一种对抗了。内核调试到ring0是肯定解决问题的,还有一种就是在可能的情况下注入DLL然后dump内存。这个注入DLL的方法我们在前一部分已经谈过了。

快速入门就暂时到这里吧。其实游戏插件的话题还有很多,比如说插件做出来了,如何保护自己让自己不被滥用?万一别人写了个DLL注入到你的程序里dump游戏内容,这个可能会遭到游戏开发和发行商的封杀的。安全问题一层又一层。科技向善吧。

本文拙劣,欢迎批评指正。谢谢。



版权声明:本文为prog_6103原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。