协议型外挂制作五

 第一节到第三节我们说了基本工具的准备,第四节使用免升级和免弹出主页来做了一下基本的练习。第四节中和普通的游戏修改器没有太大的区别,只是一个修改的是数据,一个修改的是代码。这一节中我们将通过在dll中修改大话的代码来进行外挂的制作。其实在dll中动态修改代码和上一节用的方法一样,只是要改写的东西更多了而已。
       原理和上一节中的函数一样,都是调用 WriteProcessMemory
       这一节的任务是拦截接收到的数据,关于发送的数据可以进行相类似的处理。一般在分析网络游戏的时候,都是先分析接收到的数据。对于大话的程序,中间如何去分析的过程就不说了,这要看自己的调适能力了。不过对于 9.16 之前的大话客户端程序,里面含有大量的调试信息(也可能是脚本信息),大致分析程序可以发现,程序总是在打印调试信息之后,然后做实际的工作。其中对于 "rx_decode” 这个字段很感兴趣,看看调用的地方:
.text:00449D4F 154                 push    offset aRx_decode ; "rx_decode"
在前方不久的地方就是网络函数 recv ,因此可以这样来理解,程序接收到数据之后,打印出调试信息,然后跳转到:
.text:00449D95 154                 push    ebp
的地方继续执行,通过不断的跟踪发现,大部分时候程序都执行到地址:
.text:00449DED 154                 mov     [eax], edi
而且, [edi] 中的内容在相同的时刻几乎是相似的,通过在游戏中随机的打开中断,将 [edi] 中的内容 dump 出来,然后组成 ASCII 码便可以发现,里面的内容相对来说是不变的,如果你运气好刚好可以拦截到聊天数据的话,就会发现里面的内容就是聊天的内容。这有点像碰运气。不过,如果采用下面的方法的话,就可以不用碰运气了。首先,我们发现 edi 是一个数据的地址, ebp 中是我们接收到的数据的长度。当对其中的内容感到怀疑的时候,我们就想将该语句执行的时候 [edi] 中的内容 dump 出来, dump 的长度就是 ebp 中的值。因此我们通过 w32dasm 来制作内存补丁,使用 W32dasm 反编译程序之后,使用快捷键 Ctrl+L 可以将程序加载到内存中,不让程序执行,快捷键 Ctrl+F12 跳转代码窗口的地址到 00449DED                一行,方便恢复代码的时候用。在调试窗口中按 Ctrl+F12 将当前代码位置跳转到 00449DED
我们将在这里进行内存补丁的编写。点击 Patch Code 按钮就可以直接写内存代码了。在 00449DED 的位置的补丁如下:
:00449DED E90ECA0400                    jmp 00496800
00496800 地址的内容是一段空闲得内存。在 ida 中可以看到程序中没有任何地方使用这块内存,我们将在这里进行程序的修改。当程序执行到 00449DED 的时候,就会跳转到 00496800 接着执行,因此,我们还必须修改 00496800 处的代码,使用 Ctrl+F12 跳转到 00496800 处,开始打补丁:
:00496800 50                            push eax; 保存各寄存器的值
:00496801 53                            push ebx
:00496802 51                            push ecx
:00496803 52                            push edx
:00496804 55                            push ebp ebp 为这次接收到的数据长度
:00496805 57                            push edi edi 为数据地址
:00496806 6804040000                    push 00000404 ;向外挂程序发送拦截消息ID
:0049680B A1D0664900                    mov eax, dword ptr [004966D0] [004966d0] 中包含的是外挂窗口的窗口句柄
:00496810 50                            push eax
:00496811 3EFF1574924700                call dword ptr ds:[00479274] ds:[00479274] SendMessage 的函数地址,调用SendMessage 函数向外挂发送命令
:00496818 5A                            pop edx; 恢复各寄存器
:00496819 59                            pop ecx
:0049681A 5B                            pop ebx
:0049681B 58                            pop eax
:0049681C 8938                          mov dword ptr [eax], edi ;调用原来的操作,因为我们打补丁的时候跳过了部分操作,因此在这里进行原来的操作。
:0049681E 5F                            pop edi
:0049681F 5E                            pop esi
:00496820 5D                            pop ebp
:00496821 E9CC35FBFF                    jmp 00449DF2; 跳回原来的地址之后接着执行
以上就是拦截补丁的完整代码。在使用的时候,必须先将大话程序的 [004966d0] 中填充上外挂的窗口句柄,要不然是没办法弄得。
使用 W32dasm 做补丁的时候好处在于我们看到的就是程序执行时用的虚拟地址,并且, W32dasm 在给出汇编代码的同时给出了代码的 16 进制表示。补丁做完之后,剩下的就是如何将补丁程序放入到目标程序中了。
当外挂窗口创建之后,我们通过向外挂窗口发送 WM_USER+2 来命令外挂窗口执行修改大话程序的操作。
下面是具体的修改操作:
void TwgHookForm::ModifyXy2(TMessage Message)
{
 DWORD dwIdOld1, dwIdOld2, dwIdOld3, dwIdOld4, dwIdOld5;
 DWORD id=GetCurrentProcessId();
 HANDLE handle1 = OpenProcess(PROCESS_ALL_ACCESS, FALSE,id);
 if (wghandle)
    {
        Byte getrecv1[] =
        {
            0xE9, 0x0E, 0xCa, 0x04, 0x00
        }; //5
        Byte getrecv2[] =
        {
            0x50, 0x53, 0x51, 0x52, 0x55, 0x57, 0x68, 0x04, 0x04, 0x00,
                0x00, 0xA1, 0xd0, 0x66, 0x49, 0x00, 0x50, 0x3E, 0xFF, 0x15,
                0x74, 0x92, 0x47, 0x00, 0x5A, 0x59, 0x5B, 0x58, 0x89, 0x38,
                0x5F, 0x5E, 0x5D, 0xE9, 0xcc, 0x35, 0xFB, 0xFF
        }; //38
        VirtualProtectEx(handle1, (void*)(0x004966d0), 4, PAGE_READWRITE,
            &dwIdOld1);
        if ((WriteProcessMemory(handle1, (void*)(0x004966d0), &wghandle, 4,
            NULL)) == false)
        {
            ShowMessage(" 写句柄错误! ");
            return ;
        }
        VirtualProtectEx(handle1, (void*)(0x004966d0), 4, dwIdOld1, &dwIdOld1);
        VirtualProtectEx(handle1, (void*)(0x00449DED), 5, PAGE_READWRITE,
            &dwIdOld4);
        if ((WriteProcessMemory(handle1, (void*)(0x00449DED), getrecv1, 5, NULL)
            ) == false)
        {
            ShowMessage(" 拦截接收修正补丁错误! ");
        }
        VirtualProtectEx(handle1, (void*)(0x00449DED), 5, dwIdOld4, &dwIdOld4);
        VirtualProtectEx(handle1, (void*)(0x00496800), 38, PAGE_READWRITE,
            &dwIdOld5);
        if ((WriteProcessMemory(handle1, (void*)(0x00496800), getrecv2, 38,
            NULL)) == false)
        {
            ShowMessage(" 拦截接收补丁错误! ");
        }
        VirtualProtectEx(handle1, (void*)(0x00496800), 38, dwIdOld5, &dwIdOld5);
    }
}
CB 中使用自定义消息,需要在头文件中加入:
#define WM_USER_MODIF (WM_USER+2) // 修改大话程序的消息
#define WM_USER_GETSEND (WM_USER+1) // 拦截到发送数据接收到的消息
#define WM_USER_APIHOOK (WM_USER+5) //APIHOOK 拦截到接收到的消息
#define WM_USER_GETRECV (WM_USER+4) // 修改后的程序会向外挂窗口发送该消息。
在外挂窗口类里面(我这里是 TwgHookForm ),添加函数 void ModifyXy2(TMessage Message)
void GetRecv(TMessage Message);
protected: 关键字下面添加消息映射声明:
BEGIN_MESSAGE_MAP
                VCL_MESSAGE_HANDLER(WM_USER_MODIF, TMessage, ModifyXy2)
                VCL_MESSAGE_HANDLER(WM_USER_APIHOOK, TMessage, HOOKAPITest)
                VCL_MESSAGE_HANDLER(WM_USER_GETSEND, TMessage, GetSend)
                VCL_MESSAGE_HANDLER(WM_USER_GETRECV, TMessage, GetRecv)
        END_MESSAGE_MAP(TForm)
以上就是拦截接收数据的全部了。结合以前的代码,运行程序,可以发现,拦截到的数据为所有的已经解密了的游戏数据,至于数据的解析,就看自己的了,这里给出一个解析的代码框架:
#define CMDVOID(a) /
void CMDSAY##a(int cmd, int length, char* date)
#define RECVCASE(cmd,length,data) /
case cmd:/
CMDSAY##cmd(cmd,length,data);/
break
 
class 中的声明:
CMDVOID(10);
CMDVOID(11);
CMDVOID(21);
在接收到的函数 GetRecv 中:
void TwgHookForm::GetRecv(TMessage Message)
{
 if (Message.WParam == 1)
    {
        cmdrecvstate = RECVCMD;// 如果接收到的是 1 个字符,则接收到的是命令
    }
    else if (Message.WParam == 2)
    {
        cmdrecvstate = RECVLENGTH;// 如果接收到长度是 2 个字符,则为将要接收到的数据长度
    }
    else
    {
        cmdrecvstate = RECVDATA;// 否则接收到的就是数据
    }
    static int cmd,datalength;
    switch (cmdrecvstate)
    {
        case RECVCMD:
            cmd = *((int*)(Message.LParam));
            return;
        case RECVLENGTH:
            datalength = *((int*)(Message.LParam));
            return;
        case RECVDATA:
            if (datalength == Message.WParam)
            {
                Cmdsay(cmd, datalength, (char*)Message.LParam);// 如果数据包没有被拆分,则进行命令解释
            }
          else
            {// 否则,打印这个包现在内容
                    Memo1->Lines->Add(" 注意:这个数据不完整 :");
                    AnsiString astemp1 = "[ 接受 ][";
                    astemp1.cat_sprintf("%x][", cmd);
                     astemp1.cat_sprintf(" 应收长度: %d 实际长度 :%d][",datalength,Message.WParam);
                BYTE *temp = (BYTE*)Message.LParam;
                    for (int i = 0; i < Message.WParam; i++)
                    {
                        astemp1.cat_sprintf("%0.2x ", temp[i]);
                    }
                    astemp1.cat_sprintf("][");
                     for (int i = 0; i < Message.WParam; i++)
                    {
                    if(temp[i]>=0x20)
                        astemp1.cat_sprintf("%c", temp[i]);
                        else
                        astemp1.cat_sprintf(".");
 
                    }
                    astemp1.cat_sprintf("]");
 
                Memo1->Lines->Add(astemp1);
            }
            return;
    }
程序仅仅很简单的进行数据包的拦截和解析,对于被拆分的包不作处理,在真实应用中应该将这些包合并。
上面的接收规则对于 9.16 之前的大话程序有效,大话程序的一组数据会分成 3 次发送,首先接收到的是名字字,这个为 1 字节长度,接着是要接收的数据的长度,这个为 2 个字节,接下来就是数据了。如果真实接收到的数据和应该接收的数据长度不一样的话,表示这个包被拆分了。
对于 9.16 之后的程序,大话不一定按照这样的规则来进行,因此上面的只适应于 9.16 之前的程序。
下面是 Cmdsay 的函数实现:
void TwgHookForm::Cmdsay(int cmd, int length, char* date)
{
        switch (cmd)
    {
RECVCASE (10,length,date);
 RECVCASE (11,length,date);
 RECVCASE (21,length,date);
        default:// 未被解析的命令
                 AnsiString astemp1 = "[ 接受 ][";
                astemp1.cat_sprintf("%x][", cmd);
                astemp1.cat_sprintf("%d][", length);
            BYTE *temp = (BYTE*)date;
                for (int i = 0; i < length; i++)
                {
                    astemp1.cat_sprintf("%0.2x ", temp[i]);
                }
            astemp1.cat_sprintf("][");      
            for(int i=0;i<length;i++)     
            if(temp[i]>=0x20)
            astemp1.cat_sprintf("%c",temp[i]);
             else
            astemp1.cat_sprintf(".");         
            astemp1.cat_sprintf("]");
            Memo1->Lines->Add(astemp1);
                                         break;
    }
}
上面就是全部了,对于发送的命令拦截,和接收的类似,可以自己分析。在前几节教程中我已经将打补丁的注意点说了一遍,这里就不再说了。对于 9.16 之后的程序,我只做了很少的研究,主要是因为没有大量的时间和心情来做这些事情,这里仅仅给一点初步的提示:
1、  对于发送的数据,有些部分可能会采用二次加密的办法。
2、  对于接收的数据,程序为了增加调试的难度,将数据转换成浮点数之后不断的进行地址的变换和数据的转移。
3、  程序将接收和发送放入了一个线程中(这个可能,因为 9.16 之后比 9.16 之前多了一个线程)。
4、  数据采用浮点数存储,加密过程中有可能转换成浮点数,但加密完之后又会转换成浮点数,直到最后的时候才会转换成整数发送。
由于大话程序内部进行了非常大量的浮点和整数的转换,因此现在的大话程序是非常的耗费 CPU 资源的。以下面的代码来说:
Int I;
Float k=(float)I;// 利用浮点数寄存器,这个用的时间很短
I=(int)k;// 在编译器编译的时候,会用自己的浮点整型转换,而不是数学协处理器进行转换,这个转换大概比使用浮点寄存器转换慢 12—60 倍左右或者更多。使用 ida4.7 可以看到反编译之后调用了库函数 _ftol 。慢的原因是因为库函数是为数学计算进行的,而不是为了游戏中的效率而设计的。这是 9.16 之后大话变得卡的主要原因。
对于 9.16 之后的程序,也可以进行相同的处理,只是在调试的时侯对于代码地址的查找比较麻烦。如果不喜欢自己来写代码数组的话,可以自己根据 PE 文件来写个代码补丁工具。这个我就不讲了,事实上,我自己也没有写,时间不足是一方面,人懒是没办法的。
在前面 APIHOOK 一节(教程三)中,我的 API 类有点错误,这里更正一下:
APIHOOK.h
private:
pfnOrig,PROC pfnHook,BOOL fExcludeAPIHookMod);
 void WINAPI ReplaceIATEntryInOneMod(PCSTR pszCalleeModName,PROC pfnOrig,PROC pfnHook,HMODULE hmodcaller,HANDLE handle);
 void WINAPI FixupNewlyLoadedModule(HMODULE hmod,DWORD dwFlags);
应该改为:
private:
//pfnOrig,PROC pfnHook,BOOL fExcludeAPIHookMod); 这个是没有删除干净的注释
 void WINAPI ReplaceIATEntryInOneMod(PCSTR pszCalleeModName,PROC pfnOrig,PROC pfnHook,HMODULE hmodcaller,HANDLE handle);
 void WINAPI FixupNewlyLoadedModule(HMODULE hmod,DWORD dwFlags);
APIHOOK.cpp 中:
m_module = GetModuleHandle(pszCalleeModName);
 ReplaceIATEntryInOneMod(m_pszCalleeModName, m_pfnOrig, m_pfnHook, m_module,
    prochandle);
应该修改为:
m_module = hmod;
 ReplaceIATEntryInOneMod(m_pszCalleeModName, m_pfnOrig, m_pfnHook, m_module,
    prochandle);
上次alan给我的源代码我上传之后才看了一下他的代码,发现他仅仅做了教程一里的。我将源代码整理了一下并给了他一份,应该不久之后就可以看到了。关于alan的联系方式:
E-mail:tyr_alan@hotmal.com 如果对他的代码有疑问的话,可以给他发邮件。
主要内容终于讲完了,比我想象得要少,下一节主要讲解一下善后的处理,以便让外挂看起来更专业。由于下一节不是主要的内容,因此可能会拖后一点。
 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值