文章目录

  • 前言
  • 封包式功能的实现步骤
  • 定位发包函数
    • 三大发包函数
    • 重写发包函数
    • 线程发包
      • 线程发包的形态和特点
      • 条件断点筛出心跳包
      • 线程发包的传参方式
      • 跳出线程发包
      • 定位加密封包内容
    • 线程发包总结
  • 定位明文发包函数
  • 定位封包加密call
    • 定位封包加密call
    • 封包加密call参数分析
  • 复制封包加密函数
  • 调用函数实现功能

前言

游戏外挂按制造难度总共分为下面三类:

  1. 模拟式:通过调用Windows API来控制鼠标键盘等,使游戏中的人物进行流动或攻击。优点是实现较为简单,周期短,涉及技术面小。缺点是功能不多,较为单一。按键精灵就是其中的代表。
  2. 内存式:通过修改游戏的关键数据和代码或者非法调用游戏内部的call,来实现一系列功能。相对第一种功能大大增加,再加上以内存数据为依托,能达到更广泛和精准的控制。这种外挂可以快速提升你对内存地址的理解和运用,是编程技术提升的好帮手。难点在于定位需要的功能call和追踪数据。
  3. 封包式:基于客户端和服务器的数据包通信,通过给服务器发送或者拦截封包,来实现游戏功能。这类外挂的缺点是涉及技术面比前两者更为广泛,开发周期长。优点是所实现的功能强大到难以想象,而且可以无视绝大部分游戏检测,足以弥补对时间上的消耗。

封包式功能的实现步骤

  1. 定位到游戏的发包函数
  2. 通过发包函数定位到明文发包函数
  3. 通过明文发包函数定位到封包加密函数
  4. 复制整个封包加密函数到自己的dll
  5. 组包调用游戏功能

整个过程看似简单,实则困难重重,下面就通过一个例子来复现整个过程。

这里用来进行分析的游戏是幻想神域 链接如下:

链接:https://pan.baidu.com/s/1kGAzv8hTV3EylXKWchpmKA
提取码:evc8

自己搭建的一个私服,无论游戏有没有更新都可以跟着步骤操作,随时复现。按照文件中的视频教程搭建即可。

定位发包函数

三大发包函数

在网络游戏中,客户端和服务器的通信基于一系列的数据包。每个数据包都类似于一条指令,客户端和服务器在这个系列指令中完成指定动作。

客户端要与服务器进行通信,必须调用下面的三个发包函数发送数据包

send();
sendto();
WSASend();

那么我们只要在这三个函数下断点,然后进行堆栈回溯分析,就能准确定位关键的函数调用链。在这条链上,快速排查出需要的功能call。

不过,发包函数在下断点的时候,可能会碰到下面两个棘手的问题:

  1. 明明对send()函数下断了,却断不下来
  2. 由于游戏中存在一个发包线程,所以即使断下send()函数,也无法回溯出有用的逻辑

幻想神域就是第二种情况,属于线程发包。

重写发包函数

对于第一个问题,是因为游戏设计者知道发包函数的重要性,重写了一份发包函数。对于这种情况有两种解决方案

  1. 寻找send()函数内调用的底层函数,对底层函数下断。

send sendto和WSASend在底层都会调用一个函数叫WSPSend,F7进入send函数,第三个调用的call就WSPSend函数。

  1. 搜索send函数的特征,定位到重写的send函数

线程发包

接下来解决第二个问题,游戏单独起了一个线程进行发包

线程发包的形态和特点

  1. 发包函数断的很频繁
  2. 任何功能在发包函数断下,调用堆栈都是一样的

由于线程发包是在游戏内部用一个死循环不断的发送数据包,其中包括心跳包,所以会出现发包函数断的很频繁的问题,这就导致无法在我们想要的时机断下,不利于调试。我们需要先解决频繁断下的问题。

条件断点筛出心跳包

幻想神域这个游戏的发包函数的WSASend(),首先来了解一下这个函数参数的含义

int WSAAPI WSASend(SOCKET                             s,LPWSABUF                           lpBuffers,DWORD                              dwBufferCount,LPDWORD                            lpNumberOfBytesSent,DWORD                              dwFlags,LPWSAOVERLAPPED                    lpOverlapped,LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

唯一有用的参数: lpBuffers: 指向WSABUF结构体的指针,存储的是包长和包内容

typedef struct _WSABUF {ULONG len;           //包长CHAR  *buf;         //包内容
} WSABUF, *LPWSABUF;

接着打开游戏,用OD附加,在WSASend函数下断,程序断下

查看一下第二个参数lpBuffers,数据包长度为1E,我们可以以数据包长度为限制条件在这个地方下条件断点,条件如下:

[[esp+8]]!=1E

如果有多个心跳包可以用与的方式进行过滤

[[esp+8]]!=11&&[[esp+8]]!=7

通过条件断点的方式,就可以解决发包函数频繁断下的问题。

线程发包的传参方式

游戏想要单独开一个线程进行发包,必然要用一个地址作为参数传递给发包线程。

第一个线程将发包内容写入地址,第二个线程从这个地址中读取发包内容。这个地址会有两种形式,一种是不变的,从正向代码的角度看就是用全局变量传递,伪代码如下:

LPVOID g_addr=0;functions()
{//给地址赋值g_addr=xxxxx;//创建线程发包CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)send, g_addr, 0, 0);
}

另外一种是动态变化的,从正向代码的角度看就是用堆空间传递,伪代码如下:

functions()
{//申请堆空间wchar_t* lpaddr=new wchar_t[sizof(buff)];//将包内容拷贝到堆空间memcpy(lpaddr,buff,sizof(buff))//创建线程发包CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)send, lpaddr, 0, 0);
}

跳出线程发包的核心思路就是要找到是谁将发包内容写入。也就是找到上面的memcpy的位置。

在WSASend函数下断,查看一下pBuffers的地址。这个地方的地址是一直变化的,应该是用的堆空间的方式来传递参数。

如果这个地址是不变的,说明是用的全局变量来传递参数。不变的情况下直接在这个地址下写入断点就能跳出发包线程了。

现在这个地址是每次都动态变化的,所以我们需要往上追到这个地址的来源,然后对地址的来源下写入断点,跳出发包线程。

跳出线程发包

首先需要找到包的来源,在WSASend函数下断。

eax是pBuffers的结构体地址,而eax来源于[esp+0x28]

经过这一个push,堆栈地址发生改变,包长=esp+24 包地址=esp+28,而esp+24来自eax,那么eax就等于包长

再经过上面几个push,包地址=esp+18,继续追esp+18

而esp+18来自ecx,包地址=ecx,继续追ecx

ecx的值来自[edx+esi],edx的值断下后为0,那么包地址就等于esi,继续追esi

esi来自[ebx+4],而ebx来自[edi+2888],那么包地址就等于[[edi+2888]+4]

在这个地方下个断点,可以发现edi的地址是不变的。这个时候就没有必要往上追了。

接着我们在[[edi+2888]+4]的地址下硬件写入断点,找到往这个地址写入数据的地方

断点断下以后,eax=[edi+2888],是被写入数据的地址,包内容=[eax+4]

我们需要判断这个地方是在发包线程内还是线程外。

判断的方法是对比WSASend和找到地址的调用堆栈。

我们发现两个调用堆栈的地址是相同的,说明还没有跳出发包线程。需要继续追eax的来源然后下写入断点。

eax来自[ebx+8],ebx来自edx,而edx的地址是不变的,包内容=[[edx]+8]+4],直接在edx下写入断点

断到了第二次断下的位置

这个时候再查看调用堆栈,返回地址都是游戏主模块,明显这次我们跳出了线程发包函数

定位加密封包内容

接着我们需要在这个函数内找到加密的封包内容,之前的包内容偏移如下:

包内容=[[edx]+8]+4]

对比之前追的偏移表达式,这个地方就是将ebp写入到[eax],[eax]其实就相当于包内容表达式的[edx],所以

加密的封包内容就等于[ebp+8]+4]

那么怎么验证这个地方就是加密的封包内容呢?直接对比WSASend的pBuffers和[ebp+8]+4]的数据内容

这两个地方是一致的,那么说明[ebp+8]+4]就是加密的封包内容。接下来测试一下能不能通过跳出的发包线程找到游戏的喊话call。在第二次断下的位置下断点

然后在游戏内喊话,断下以后在堆栈中的返回地址,我们找到了当前的喊话内容,说明这个call就是喊话call

线程发包总结

  1. 对于重写发包函数的问题,只需要在三个发包函数的底层函数下断或者搜索内层的特征码即可找到游戏重写的发包函数
  2. 对于线程发包的问题,需要找到数据包的地址来源,然后对基地址下写入断点。重复这个过程,即可跳出线程发包函数。

定位明文发包函数

定位到了加密的封包位置以后,我们再来找明文发包call。

在游戏内进行喊话,内容为三个1

在加密的封包内容处下断点,喊话让游戏断下,并且在堆栈中找到第一个返回地址


分析这个call的相关参数,esi是一个结构体指针

+0的位置指向的是一个虚函数表

+4的位置里面有我们的喊话内容3个1,这个可能就是我们要的明文发包函数了

为了进一步确认,把这个地方的内容修改为222,F9运行

喊话的内容也修改成了222,说明这个就是我们要的明文发包call。

定位封包加密call

定位封包加密call

我们在加密封包处下断点,第一层返回地址找到了明文发包函数,那么封包的加密call肯定就在中间。

在明文发包函数下个断点,F7进入函数并单步跟踪,上面所有的跳转都执行了,上面4个call没有执行的机会

然后在单步不过这个call的时候,喊话的内容被加密了。这个有可能就是加密call。

为了进一步确认,将断点断到加密封包内容处,查看[[ebp+8]+4]地址处的值,和之前的内容一致,说明这个call就是我们要的封包加密call

封包加密call参数分析

首先来看eax,eax地址指向的值每次都是变化的,对于加密函数来说,为了让密文每次都变得不一样,一个有效的方法就是让秘钥变的随机。这个eax加密call的秘钥

eax往上追可以追出一个偏移表达式,这里省略追秘钥的过程,直接给出表达式

[[[00f84ba4]+4]+0xC+8]+54

edi是一个数值,可能是包长

我们在WSASend函数下断,查看一下,和上面的edi是一样的。那么edi就是包长-2。

封包分为两部分:前两个字节是包的头部,头部往后才是封包数据。

这个参数的含义其实就是要加密的内容长度,-2是因为要减掉封包头部的长度。

ebp和ebx可以用同样的方法论证得出是包地址+2。也就是要加密的数据地址,+2是为了不加密封包头。

到此,封包加密call的参数就分析完成了

复制封包加密函数

到这里,只剩下最后一步,将封包加密函数整个复制到自己的dll代码中并修改,就能彻底脱离游戏代码了。修改后的代码如下:

__declspec(naked) void Encrypt(DWORD key,DWORD len,DWORD addr1,DWORD addr2)
{__asm{push    ebppush    ebxpush    esipush    edimov     edi, dword ptr [esp+0x14]mov     edx, dword ptr [esp+0x18]mov     esi, dword ptr [esp+0x1C]mov     ebp, dword ptr [esp+0x20]xor     eax, eaxxor     ebx, ebxcmp     edx, 0je      Label1mov     al, byte ptr [edi]mov     bl, byte ptr [edi+4]add     edi, 8lea     ecx, dword ptr [esi+edx]sub     ebp, esimov     dword ptr [esp+0x18], ecxinc     alcmp     dword ptr [edi+0x100], -1je      Label2mov     ecx, dword ptr [edi+eax*4]and     edx, 0xFFFFFFFCje      Label3lea     edx, dword ptr [esi+edx-4]mov     dword ptr [esp+0x1C], edxmov     dword ptr [esp+0x20], ebpLabel4:add     bl, clmov     edx, dword ptr [edi+ebx*4]mov     dword ptr [edi+ebx*4], ecxmov     dword ptr [edi+eax*4], edxadd     edx, ecxinc     aland     edx, 0x0FFmov     ecx, dword ptr [edi+eax*4]mov     ebp, dword ptr [edi+edx*4]add     bl, clmov     edx, dword ptr [edi+ebx*4]mov     dword ptr [edi+ebx*4], ecxmov     dword ptr [edi+eax*4], edxadd     edx, ecxinc     aland     edx, 0x0FFror     ebp, 8mov     ecx, dword ptr [edi+eax*4]or      ebp, dword ptr [edi+edx*4]add     bl, clmov     edx, dword ptr [edi+ebx*4]mov     dword ptr [edi+ebx*4], ecxmov     dword ptr [edi+eax*4], edxadd     edx, ecxinc     aland     edx, 0x0FFror     ebp, 8mov     ecx, dword ptr [edi+eax*4]or      ebp, dword ptr [edi+edx*4]add     bl, clmov     edx, dword ptr [edi+ebx*4]mov     dword ptr [edi+ebx*4], ecxmov     dword ptr [edi+eax*4], edxadd     edx, ecxinc     aland     edx, 0x0FFror     ebp, 8mov     ecx, dword ptr [esp+0x20]or      ebp, dword ptr [edi+edx*4]ror     ebp, 8xor     ebp, dword ptr [esi]cmp     esi, dword ptr [esp+0x1C]mov     dword ptr [ecx+esi], ebplea     esi, dword ptr [esi+4]mov     ecx, dword ptr [edi+eax*4]jb      Label4cmp     esi, dword ptr [esp+0x18]je      Label5mov     ebp, dword ptr [esp+0x20]
Label3:add     bl, clmov     edx, dword ptr [edi+ebx*4]mov     dword ptr [edi+ebx*4], ecxmov     dword ptr [edi+eax*4], edxadd     edx, ecxinc     aland     edx, 0x0FFmov     edx, dword ptr [edi+edx*4]xor     dl, byte ptr [esi]lea     esi, dword ptr [esi+1]mov     ecx, dword ptr [edi+eax*4]cmp     esi, dword ptr [esp+0x18]mov     byte ptr [ebp+esi-1], dljb      Label3jmp     Label5
Label2:movzx   ecx, byte ptr [edi+eax]
Label6:add     bl, clmovzx   edx, byte ptr [edi+ebx]mov     byte ptr [edi+ebx], clmov     byte ptr [edi+eax], dladd     dl, clmovzx   edx, byte ptr [edi+edx]add     al, 1xor     dl, byte ptr [esi]lea     esi, dword ptr [esi+1]movzx   ecx, byte ptr [edi+eax]cmp     esi, dword ptr [esp+0x18]mov     byte ptr [ebp+esi-1], dljb      Label6
Label5:dec     almov     byte ptr [edi-4], blmov     byte ptr [edi-8], al
Label1:pop     edipop     esipop     ebxpop     ebpretn}}

调用函数实现功能

接着我们在代码中调用加密函数,然后发送封包来实现喊话功能。

这里是直接用的组装好的喊话分包,至于分包要如何分析,如何组装,这个我们后面再讨论。示例代码如下:

void :SendAnnounce()
{byte a[100]  = {0x11,0x00,0x7E,0x00,0x00,0x00,0x00,0x02,0x00,0x31,0x31,0xFF,0xFF,0xFF,0xFF,0x00,0x00,0x00,0x00,0x60,0xA8,0x6C};DWORD datalen = 0x13; DWORD data = (DWORD)a;DWORD addr = data + 2;DWORD addrlen = datalen - 2;DWORD key = 0;__asm{mov ecx,0x00f84ba4mov ecx,[ecx]mov ecx,[ecx]mov ecx,[ecx+0x4]mov ecx,[ecx+0x14]mov ecx,[ecx]lea ecx,[ecx+0x54]mov key,ecx}Encrypt(key,addrlen,addr,addr);HWND hWnd =FindWindowA("Lapis Network Class",0);DWORD A = GetWindowLongW(hWnd,-21);DWORD S =*(DWORD*)(A+0x38);send(S,(const char*)data,datalen,0);}

这里我省略了send函数的套接字来源。直接在WSASend下断,往上追第一个参数,就能看到游戏中的SOCKET是通过GetWindowLongW获取的。

实现效果如图:

最后,附上Github地址,里面有游戏下载链接和相关工具,需要请自取:
https://github.com/TonyChen56/GameReverseNote

007 封包式游戏功能的原理与实现相关推荐

  1. 按键改变元素背景颜色 链式编程的原理 评分案例 each方法的使用

    按键改变元素背景颜色 <!DOCTYPE html> <html lang="en"> <head><meta charset=" ...

  2. element-UI响应式(布局原理)讲解 - 贴文篇

    element-UI响应式(原理)- 讲解 element-UI官方说明:响应式布局 参照了 Bootstrap 的 响应式设计,预设了五个响应尺寸:xs.sm.md.lg 和 xl. Element ...

  3. 增量式编码器工作原理超详细图解

    旋转编码器是由光栅盘(又叫分度码盘)和光电检测装置(又叫接收器)组成.光栅盘是在一定直径的圆板上等分地开通若干个长方形孔.由于光栅盘与电机同轴,电机旋转时,光栅盘与电机同速旋转,发光二极管垂直照射光栅 ...

  4. 增量式(相对式)编码器与绝对式编码器工作原理

    增量式(相对式)编码器与绝对式编码器工作原理 增量式编码器工作原理 绝对式编码器工作原理 根据检测原理,编码器可分为光学式.磁式.感应式和电容式.根据其刻度方法及信号输出形式,可分为增量式.绝对式以及 ...

  5. 桥式整流电路原理;电感滤波原理;电容滤波原理

    桥式整流电路原理 桥式整流电路如图1所示,图中B为电源变压器,它的作用是将交流电网电压e1变成整流电路要求的交流电压,RL是要求直流供电的负载电阻,四只整流二极管D1-D4接成电桥的形式,故有桥式整流 ...

  6. 碱性干电池的内阻测试方法_碱性锌锰干电池电极反应式 锌锰干电池原理是什么【详细介绍】...

    摘要:干电池是一种以糊状电解液来产生直流电的化学电池,最常见的干电池就是锌锰干电池了.那么碱性锌锰干电池电极反应式是什么呢?您知道锌锰干电池原理吗?下面小编就为您介绍. [碱性锌锰电池]碱性锌锰干电池 ...

  7. 基于 mini2440 电阻式触摸屏:电阻式触摸屏工作原理

    ========================================================== 开发环境 编译系统 :fedora9 编译器 :arm-linux-4.4.3 主 ...

  8. 服务器ups后备式好还是在线式好,后备式UPS电源和在线式UPS工作原理和优缺点

    后备式UPS电源和在线式UPS工作原理和优缺点.目前市场上的UPS不间断电源主要分为两大类:在线式UPS电源与后备式UPS电源.我们在负载小功率设备的时候,如果设备本身对电能的要求不是很高的话,一般情 ...

  9. 全自动刷式过滤器工作原理

    全自动刷式过滤器工作原理: 全自动刷式过滤器是一种利用滤网直接拦截水中的杂质,去除水体悬浮物.颗粒物,降低浊度,净化水质,减少系统污垢.菌藻.锈蚀等产生,以净化水质及保护系统其他设备正常工作的精密设备 ...

最新文章

  1. 求未知数X最临近的能被某个数字N整除的数
  2. underscorejs之 _.indexBy(list, iteratee, [context])
  3. 菜鸟笔记(一) - Java常见的乱码问题
  4. [转载] StringBuffer和StringBuilder类
  5. .net框架读书笔记---虚方法
  6. 本地项目怎么推送到码云_【重谈npm】当下载一个项目到本地执行npm install报错时应该怎么办...
  7. c#编程指南(四) 组元(Tuple)
  8. Spring中拦截/和拦截/*的区别
  9. Python基础练习-002-求1000以内的完全数
  10. 七年级画图计算机教案,信息技术画图软件学习教案
  11. vsftpd基于mysql_vsftpd基于mysql实现用户认证
  12. “dying ReLU“问题
  13. 【离散】画哈斯图--最好理解绝不会出错
  14. 【IM-03】Web端匿名聊天
  15. python九九乘法口诀_Python3 九九乘法口诀(99乘法口诀)
  16. mmsegmentation 训练自制数据集
  17. web ui 套件_复古UI套件
  18. MySQL自动删除指定时间以前的记录
  19. 如何设置云服务器语言,云服务器如何更换语言
  20. 谷歌浏览器突然打不开

热门文章

  1. RabbitMQ底层原理及安装使用详解
  2. GD32移植STM32HAL库接口
  3. 系统对接方案_科技之星——【TMS系统与客户服务管理系统对接项目组 】为创新发展凝聚智慧力量...
  4. 身份证号处理 c 字符串
  5. 实验室网页以及后台管理系统(1)-表格设计和建表语句
  6. 微信公众号搭建营销型房产项目程序后台开发
  7. 年月日转天数,天数转月日
  8. Android Wi-Fi SSR功能
  9. linux dd 装系统,Linux一键DD重装纯净版系统
  10. 百度果园自动浇水常规任务完成自动工具