zoukankan      html  css  js  c++  java
  • [转载]给网游写一个挂吧(二) – 启动外挂上

    给网游写一个挂吧(二) – 启动外挂上 

    前面的文章给网游写一个挂吧 – 反反外挂驱动的驱动,我们已经可以访问游戏的内存之后,接下来需要:

    1.         找到游戏里关键元素的偏移量,比如生命值的内存的位置。一般来说,大部分大型3D游戏都是用C++编写的,游戏里面的元素都是面向对象的,比如玩家是一个对象,那么生命值、魔法值之类的东西都是这个对象的一个属性。按照C++的内存布局,一般来说,只要源代码里的结构体不发生变化,属性的偏移量一般来说都是一样的。

    2.         找到游戏里一些关键函数的地址,便于外挂程序来调用。

    查找关键元素的偏移量和关键函数地址一般来说都是苦力活,当然也是智力活,需要你的逆向工程水平不错,网上有些相关的教程,这里我就不再详述了。

    这里假设我们已经找到游戏的偏移量了,现在的问题是如何启动外挂以操控游戏,一般来说有几种选择:

    1.         要么是内挂,将挂注入到网游进程的内存空间里,这样挂就相当于网游自己的一个组件,对游戏进程拥有绝对的访问权,可以读写游戏的虚拟内存地址以及调用游戏内置的函数。这种做法的弊端是,如果游戏有非法组件检测线程的话,很有可能被发现。

    2.         要么是外挂,将挂作为一个独立的进程,这样挂可以通过Read/WriteVirtualMemory来读写游戏的内存,再通过CreateRemoteThread API启动一个远程线程来调用游戏内置的函数。这种做法可以查看文章:代码注入的三种方法

    那本文我们讲解第一种方法 - 内挂。并针对两款游戏来说说注入内挂的方法:

    DNF – 使用输入法注入技术

    输入法注入技术的原理是,写一个输入法DLL并在系统中注册,然后向游戏发送一个切换输入法的消息 – 当然是切换到我们写的输入法,Windows会加载我们的输入法DLL,在这个DLL的DllMain函数里,我们就可以完成一些内挂加载以及初始化的工作:

    1.         首先写一个输入法DLL,随便从网上下载一个示例用的输入法源码即可。

    2.         在输入法DLL的DllMain函数的DLL_PROCESS_ATTACH事件中,启动外挂线程。

    3.         在单独的外挂进程里 – 一般来说这个进程就是用来给外挂用户操作的一个Windows GUI程序,在合适的地方:

    a)         用imm32.dll里的ImmInstallIMEw API函数在系统里注册我们的输入法。

    b)         用FindWindows API查找到所有需要注入的窗口,这里就是获取DNF的窗口句柄。

    c)         最后用PostMessage WM_INPUTLANGCHANGEREQUEST消息强迫Windows针对DNF窗口切换我们的输入法,从而达到加载内挂的目的。

    关键代码如下 – 整个程序大部分代码都是用C#完成,稍后介绍选用C#的原因:

    输入法注入代码C#部分:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    31

    32

    33

    34

    35

    36

    37

    38

    39

    40

    41

    42

    43

    44

    45

    46

    47

    48

    49

    50

    51

    52

    53

    54

    55

    56

    57

    58

    59

    public bool InjectDllToWindow(string dllPath, string windowText = "地下城与勇士",

    string classText = "地下城与勇士",bool IfMonitor=true)

    {

        InjectedDll = dllPath;

        WindowText = windowText;

        ClassText = classText;

     

        // 1, 注册输入法

        HKL = RegisterIME();

        if (HKL == IntPtr.Zero)

        {

            MessageBox.Show(string.Format("GetLastError: {0}", GetLastError()));

            //2,如果注册失败,检查是否已经被注册

            HKL = MImeFindByName();

        }

     

        if (HKL == IntPtr.Zero)

        {

            isRegister = false;

            return false;

        }

        isRegister = true;

     

        //3,把需要注入的dll传递给服务输入法dll中

        IMESetPubString(dllPath, 0, 0, 0, 0);

     

        //4,查找所有需要注入的窗口

        List<IntPtr> windowsToInject = FindWindows(classText, windowText);

     

        //5,注入输入法到窗口

        foreach (IntPtr window in windowsToInject)

        {

            InjectToWindow(window);

        }

     

        WindowsHaveInjected = windowsToInject;

     

        if(IfMonitor)

        {

        //6,开启监视线程,监视新的窗口,一旦开启,立刻注入

            WorkThread thread = new WorkThread(MonitorDNFWindow);

            workThreadAsyncResult = thread.BeginInvoke(null, null);

        }

        return true;

    }

     

    private IntPtr RegisterIME()

    {

        string tempDir = Environment.CurrentDirectory;

        Environment.CurrentDirectory = Environment.SystemDirectory;//把工作目录切换到系统目录

        IntPtr hkl = ImmInstallIMEW(ImeName, ImeFriendlyName); //安装服务输入法

        Environment.CurrentDirectory=tempDir; //切换回原目录

        return hkl;

    }

     

    private void InjectToWindow(IntPtr hWnd)

    {

        PostMessage(hWnd, WM_INPUTLANGCHANGEREQUEST, (IntPtr)0x01, HKL);

    }

    在上面第9行调用47 – 54行的函数,将外挂的工作目录切换到系统目录,因为我们将输入法放到系统目录,方便系统查找,并安装输入法。

    第25行里,设置在输入法注入成功后,需要执行的操作,一般来说就是启动挂了。有些内挂会在注入成功后,注册一个快捷键,通过快捷键呼出一个窗口,这个窗口可以用来跟用户操作界面通信,执行操作界面来的命令。然而,在某些游戏里,呼出的窗口会马上被检查到,我们这里将介绍在游戏进程里启动.NET程序,启动一个.NET Remoting服务的方式。

    第31 - 34行,通过FindWindows系统调用枚举系统上的窗口,找到目标窗口,执行注入操作,具体的注入操作参见56 – 59行的代码。在.NET代码里调用C/C++函数的方式,请参阅文章:使用Signature Tool自动生成P/Invoke调用Windows API的C#函数声明

    输入法C++部分关键代码:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    31

    32

    33

    34

    35

    36

    37

    38

    39

    40

    41

    42

    43

    44

    45

    46

    47

    48

    49

    50

    51

    52

    53

    54

    BOOL WINAPI DllMain(HINSTANCE hinstDLL,DWORD fdwReason,LPVOID lpvReserved)

    {

       switch(fdwReason)

        {

          case DLL_PROCESS_ATTACH:

        if (CilentDLL==NULL)

        {

              if (lstrlen(g_IMEDLLString)>0)

              {

                  StartTheDotNetRuntime();

              }

        }

              break;

          case DLL_THREAD_ATTACH:

             break;

          case DLL_THREAD_DETACH:

             break;

          case DLL_PROCESS_DETACH:

            break;

          default:

            break;

        }

        return true;

    }

     

    DWORD CALLBACK StartTheDotNetRuntime(LPVOID lp)

    {

            HRESULT hr = S_OK;

            ICLRMetaHost    *m_pMetaHost = NULL;

            ICLRRuntimeInfo *m_pRuntimeInfo = NULL;

            ICLRRuntimeHost    *pClrHost = NULL;

          

    hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*) &m_pMetaHost);

    if (hr != S_OK)

        return hr;

    hr = m_pMetaHost->GetRuntime (L"v4.0.30319", IID_ICLRRuntimeInfo, (LPVOID*) &m_pRuntimeInfo);

    if (hr != S_OK)

        return hr;

    hr = m_pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (LPVOID*) &pClrHost );

           if (FAILED(hr)) return hr;

     

        hr = pClrHost->Start();

     

        DWORD dwRet = 0;

        hr = pClrHost->ExecuteInDefaultAppDomain(

            g_IMEDLLString,

            _T("ManagedDll.Program"), _T("Start"), _T("nothing to post"), &dwRet);

     

        hr = pClrHost->Stop();

     

        pClrHost->Release();

     

        return S_OK;

    }

    在第10行代码,输入法注入成功后在游戏进程里启动.NET虚拟机,这里启动的4.0的运行库 – 参看36行代码,虚拟机成功启动后,会返回一个ICLRRuntimeHost的COM接口,根据这个接口,外挂就可以创建托管代码运行需要的应用程序域 – 参看45 – 47行。在应用程序域里执行代码并不需要一个.exe的可执行文件,只需要是一个托管程序的DLL文件,这个DLL文件需要放在游戏的目录里,因为我们的挂是运行在游戏的进程里,工作目录也自然变成了游戏的工作目录了。

    在47行,我们可以看到,可以指定DLL内部任意一个类型的静态函数作为入口点,下面是ManagedDll.Program.Start的源代码:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    namespace ManagedDll

    {

         public class Program

        {

            static int Start(string argument)

            {

                RemotingServer.Start();

                while (true)

                {

                    Thread.Sleep(1000);

                }

                return 0;

            }

        }

    }

    在第7行,我们启动了一个.NET Remoting服务(或者说是web服务,因为.NET Web服务本来就是基于Remoting的),等待任意一个地方的Remoting客户端链接……对于在非托管进程当中启动托管程序的方法,详情请参看:将托管dll注入到非托管进程中

    设计考量

    之所以选用C#的原因是:

    1.         可以快速编程,而且有丰富的类库。

    2.         垃圾回收机制可以增强挂的稳定性,而且也不用考虑内存泄露的问题。

    3.         有很强大的调试工具,以我经验来看,暂时还没有看到比VS更强大的调试工具。

    4.         最后,在游戏进程里的内挂和外部供用户配置的GUI程序需要通信,没有比.NET Remoting更方便的东西了!

     

    最后如果大家对调试技术感兴趣的话,可以考虑购买我的新书: 应用程序调试技术,这套视频除了讲解调试的技巧外,还尽量完整地讲解了周边用到的技术,这是因为调试技术要好的话,需要基础功和背景知识扎实才行。
     
    未完待续……
  • 相关阅读:
    Python环境搭建-anaconda
    UITableView的基本使用方法
    模拟网易新闻上方滚动条
    iOS之导航栏基本设置
    UITextField 方法和代理的使用
    UITextField详解
    init方法的重写与自定义
    OC中协议的理解protocal
    IOS中检测键盘出现和消失的消息
    怎么重装系统(一)
  • 原文地址:https://www.cnblogs.com/dabaopku/p/2552020.html
Copyright © 2011-2022 走看看