一.封包,协议
什么是封包?封包就是按照某种协议组成的一段字节集.这个很容易理解
那为什么要按照某种协议呢?这个我们一会再说
所有联网的软件,包括网络游戏,客户端和服务端进行通信都是通过封包进行的.
大家可以想象成2个人打电话,封包就是说话的内容.
而协议呢?就是封包的语种, 2个人打电话一个用日语,一个用英文
もしもし、韓さんです。 那边接电话 What are you talking? 这怎么交流啊?
好,那么我们定好协议,本通电话我们全部采用中文,这样就可以正常交流了对吧?
这就是协议
一句话概括, 联网软件,客户端和服务端通过约定好的协议用封包来进行通信.
二.收发包
上面了解了什么是封包协议,我们来看下什么是收发包
网络游戏和单机游戏的区别就在于时刻要和服务器进行通信,也就是收发包.我们发给服务器的叫做发包, 我们收到服务器发给我们的叫做收包,
当然这是相当于我们而言.反之一样.
来一个简单的例子:
我们进入游戏说一句:你好! 为什么别的玩家可以看到?
通过这个流程我们就会了解收发包以及服务器的作用.
首先我们发送要和周围人说你好的封包给服务器,
服务器收到我们的封包以后, 一般会给我们回一个封包,说OK没问题,当然也可能高冷的不回啊.
然后服务器会执行代码判断我周围都有哪些玩家,然后对这些玩家逐个发包
这些玩家收到服务器的封包以后,就会看到我们的喊话内容了.
了解了什么是收发包
我们就要知道调用什么函数来实现.
这里的三大快递公司
就是三个发包函数 send sendto WSASend 以及对应的 3个收包函数 recv recvfrom WSARecv
用哪一组都是可以的.
例如 口袋西游是 send recv
幻想神域是 WSASend WSARecv
三.心跳包
上面了解了封包协议,收发包,我们来看下什么是心跳包?
心跳包也是封包,只是一种比较特殊的封包.
他可以证明我们的客户端还活着,顾名思义,有心跳就是活着.
举个简单的例子:
2个人打电话, A: 喂在吗? B:... A:在吗? B....A:挂了. 多次没得到回应直接就挂断了.
心跳包也一样,当服务器给我们发包,我们没有回复的时候,服务器就有可能把我们强制踢下线. 当然也有可能我们定时主动发送
1-2次服务器可能会认为偶尔丢包,如果2个以上应该就会直接认为我们掉线,直接强制踢下线了.
心跳包有很多好处:
1.一定程度防止脱机外挂
2.掉线立刻下线, 比如以前的老游戏,电脑蓝屏关机了,由于没有自然下线, 心跳也特别少,人物可能还在上面很长一段时间,这是不安全的,比如传奇2
3.可以隐藏一些数据在心跳包中.
四.针对发包函数的保护方法
了解了封包的概念,我们发现发包函数非常重要,
监听了发包函数就相当于掌握了所有功能实现的方法,可以替代所有call.监听了收包函数就相当于掌握了所以数据信息.因为数据本质来源于服务器
同时通过调用关系我们还知道,可以通过发包函数返回到所有的功能函数.
这是连新手的都知道的2点,所以一定要进行防护
针对这2点我们可以做什么防护呢?
那么第一件事,当然是把这个函数给尽量的隐藏起来
有的时候我们会发现,三大发包函数都不断!
那么是不是这个游戏不用这3个函数发包呢?当然不是,我们只能用这三个发包函数发包,没有其他的方法。
那就是重新实现发包函数,自己重新写一个发包函数,不用系统提供的成品函数了。
说的简单点就是,代码还是这三个发包函数的代码,只是换了个位置,搬家了,不直接调用这3个函数了。
知道这个原理以后就很容易处理了
方法一
既然还是之前的代码,那么我们到他的更内层函数下断就可以了
既WSPSend,相当于用内层函数当特征码。
函数体内的虚函数
X64结构不一样,但是一样是虚函数进去 找到WSPSend
方法二
通过三个函数的特征码到对应模块中进行搜索,这里注意的是这个重新实现的发包函数,可能在原来的模块中ws2_32,也可能在游戏自己的某个模块中。
所以搜索的时候要注意.
特征码比如头部这一段
可能会有微小变化,所以我们要尝试
同时也可能搜索到多个位置,全部下断,测试即可.
总之两种方法思路是一致的,都是找原代码的特征而已。
对发包函数第二个保护方案就是加密
如果不加密,直接监听发包函数就可以获得所有的信息了.
比如直接hook send函数
例如WPE等简单工具就是hook的发包函数
所以必须进行加密
加密也一定要进行动态加密,就是同样的结果加密后内容是不同的.那么以上方法也就没有任何作用了.
五.对发包函数第三个保护方案就是线程发包
正常的游戏调用流程是
功能函数1---》功能函数2---》组包过程---》明文封包---》加密---》发包函数
所谓组包过程就是把功能函数的参数按照格式组成一个字节集
明文封包就是组装完成但是没有进行加密的封包
最后一步加密发送.
通过上面的调用关系,我们能看出来
这样只要找到发包函数,所以功能函数以及加密解密函数随意返回,随意分析
那怎么样避免呢?线程发包!
线程发包原理
就是说用2条线程来控制以上流程,让逆向者不能直接返回
第一条线程
功能函数1---》功能函数2---》组包---》明文封包---》封包写到某一个地址中
第二条线程
得到那个地址中的内容---》加密---》发包函数
当然两条线程分工不一定是严格这样写的,也可能明文封包在第二条线程中
这样的发包会有什么的特征呢?
1.无论什么功能,堆栈返回都是一样的,因为第二条线程的调用过程,什么功能都一样了
2.第二条线程由于不断循环,断的可能会比较频繁
六.线程发包跳出循环线程
先不管线程通信都有哪些方法,我们先来一个锻炼,然后我们再详细分析原理.
上面我们知道了线程发包的原理,那么这2条线程唯一的关联就是封包内容
所以我们直接追封包内容来源即可.就有线索从第二条线程返回到第一条线程了.
1.包内容地址如果是固定的,我们直接下断就能跳出去了,相当于用全局变量做线程之间通信
这里是变化的
那么我们追其来源
edi+2888 开始不发生变化了
根据我们的思路,要找变化的值来源, 那么我们在该地址上下写入断,
找到写入来源以后 我们看是否跳出了线程
发现并未跳出线程,还在原来的线程里.
2.那么我们还要继续追其来源
edx开始不变化了,对EDX下断
跳出了线程,我们可以通过这里下断,断到功能函数了
返回就是明文发包call
而这里就可以当成我们的"send"了
七.整理完全线程发包流程
1.跳出线程在这个位置,我们来整理下整体流程
线程1将ebp写入一个地址.
ebp 是会变的是动态申请的,存放的封包结构
这个地址是全局类对象指向封包结构的指针
线程2将线程1写入的 封包结构取出来
edi+2880就是全局类对象指向封包结构的指针
取出来给edx,我们依然写ebp 为了知道是线程1断出去的ebp
[[ebp]+8] 又写入 全局类型对象 +2888的位置
全局类对象+2888的位置 取出来给ebx
取出来的就是前面的 [[ebp]+8]
[[ebp+8]+4] 就是是封包内容
八.验证发包内容
我们下断可以验证一下 里面的封包内容是否是最后发送的内容,这个必须验证,防止中间又有多次加密
如果不相同,我们还要在这里对封包下访问断,追踪他经历了什么
方法很简单:
我们同时在这个位置下断 和WSASend 下断
对比两个位置封包是否一样
分别下条件断
byte ptr[[[EBP+8]+4]] != 0x0F
喊话11对比
2B771F00 C2C70011
2B771F04 C0105E58
2B771F08 D805FBB8
2B771F0C 910B1805
2B771F10 6F043A08
$ ==> >C2C70011
$+4 >C0105E58
$+8 >D805FBB8
$+C >910B1805
$+10 >6F043A08
$+14 >404B0000
发现是一样的那么我们在这里继续分析即可
九.明文发包call 和锁定加密call
我们现在跳出的位置 其实就是相当于"send"
正常可以返回到各种功能call, 如果使用call的话,我们现在已经足够了
如果想自己加密自己send发送封包,那么我们还需要分析加密解密过程,分析加密算法.
返回一层层看看 是否有明文内容,喊话是最容易看到明文的位置
返回的第一个call里面就看到了明文
那么说明 从这个call的头部 到第二次跳出的位置 中间就有加密call
如果这层发现不了明文,我们继续返回
当然我们也可以从ebp 一句一句逆向来源也是可以的
这个地方同样需要条件断 byte ptr[[[esp]+4]]!=0f
那么这地方就是明文发包call
找到了明文发包call,我们可以直接调用或则调用更外层功能函数实现各种功能,这之前我们都已经讲过了,这节课我们想用更好的方法
就是找到加密函数,我们自己加密封包,然后通过发包函数发送
十.加密call
加密函数的位置我们已经锁定了,就是在返回的call到我们断的位置之间
那么可以进行分析了
从call 断下以后 F7单独执行即可
发现了加密calll
以及分析出来加密call的参数
00B92266 2BF8 sub edi, eax
00B92268 83C5 02 add ebp, 2
00B9226B 55 push ebp ; 加密的地址
00B9226C 83C3 02 add ebx, 2
00B9226F 53 push ebx ; 加密的地址
00B92270 83C7 FE add edi, -2
00B92273 8D46 54 lea eax, dword ptr [esi+54]
00B92276 57 push edi ; 加密的长度
00B92277 50 push eax ; 秘钥 往上追 [[[[[00f84ba4]]+4]+0xC+8]]+54
00B92278 E8 83240000 call 00B94700 ; 加密call
00B9227D 8B9E 80280000 mov ebx, dword ptr [esi+2880]
全过程:
功能call参数进行组包,调用明文发包call, 明文发包call内部进行加密, 写入到一个全局类对象的属性中
线程2访问该属性,进行发包
十一.偷功能
偷功能很容易就是把程序的汇编代码复制到我们的程序内进行使用
我们自己组包,自己加密,自己发送封包
好处是,彻底不走游戏的任何代码.
复制出来的汇编代码需要进行初步处理才能写到内联汇编中
1.常数全部要加上0x
2.如果有call 还要自己分析出来继续执行内容 否则还是要以来原有程序
3.mov eax,[0x12345678] 类似这种容易错误的汇编代码也要转一下
4.跳转都标注出来
例如 jnz 12345678 他是要跳转到目的代码地址执行的,我们偷出来的代码已经不是原来的地址了
所以要通过标签修改跳转的地址。
我们把加密call的代码全部偷出来
push ebp
push ebx
push esi
push edi
mov 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, eax
xor ebx, ebx
cmp edx, 0
je Label1 =========================================
mov al, byte ptr [edi]
mov bl, byte ptr [edi+4]
add edi, 8
lea ecx, dword ptr [esi+edx]
sub ebp, esi
mov dword ptr [esp+0x18], ecx
inc al
cmp dword ptr [edi+0x100], -1
je Label2================================================
mov ecx, dword ptr [edi+eax*4]
and edx, 0xFFFFFFFC
je Label3===============================================
lea edx, dword ptr [esi+edx-4]
mov dword ptr [esp+0x1C], edx
mov dword ptr [esp+0x20], ebp
Label4:
add bl, cl
mov edx, dword ptr [edi+ebx*4]
mov dword ptr [edi+ebx*4], ecx
mov dword ptr [edi+eax*4], edx
add edx, ecx
inc al
and edx, 0x0FF
mov ecx, dword ptr [edi+eax*4]
mov ebp, dword ptr [edi+edx*4]
add bl, cl
mov edx, dword ptr [edi+ebx*4]
mov dword ptr [edi+ebx*4], ecx
mov dword ptr [edi+eax*4], edx
add edx, ecx
inc al
and edx, 0x0FF
ror ebp, 8
mov ecx, dword ptr [edi+eax*4]
or ebp, dword ptr [edi+edx*4]
add bl, cl
mov edx, dword ptr [edi+ebx*4]
mov dword ptr [edi+ebx*4], ecx
mov dword ptr [edi+eax*4], edx
add edx, ecx
inc al
and edx, 0x0FF
ror ebp, 8
mov ecx, dword ptr [edi+eax*4]
or ebp, dword ptr [edi+edx*4]
add bl, cl
mov edx, dword ptr [edi+ebx*4]
mov dword ptr [edi+ebx*4], ecx
mov dword ptr [edi+eax*4], edx
add edx, ecx
inc al
and edx, 0x0FF
ror ebp, 8
mov ecx, dword ptr [esp+0x20]
or ebp, dword ptr [edi+edx*4]
ror ebp, 8
xor ebp, dword ptr [esi]
cmp esi, dword ptr [esp+0x1C]
mov dword ptr [ecx+esi], ebp
lea esi, dword ptr [esi+4]
mov ecx, dword ptr [edi+eax*4]
jb Label4============================================
cmp esi, dword ptr [esp+0x18]
je Label5===================================
mov ebp, dword ptr [esp+0x20]
Label3:
add bl, cl
mov edx, dword ptr [edi+ebx*4]
mov dword ptr [edi+ebx*4], ecx
mov dword ptr [edi+eax*4], edx
add edx, ecx
inc al
and edx, 0x0FF
mov 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], dl
jb Label3==========================================
jmp Label5========================================
Label2:
movzx ecx, byte ptr [edi+eax]
Label6:
add bl, cl
movzx edx, byte ptr [edi+ebx]
mov byte ptr [edi+ebx], cl
mov byte ptr [edi+eax], dl
add dl, cl
movzx edx, byte ptr [edi+edx]
add al, 1
xor 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], dl
jb Label6=============================================
Label5:
dec al
mov byte ptr [edi-4], bl
mov byte ptr [edi-8], al
Label1:
pop edi
pop esi
pop ebx
pop ebp
retn
最终代码
__declspec(naked) void 加密call(DWORD 秘钥,DWORD 加密长度,DWORD 加密地址,DWORD 加密地址2)
{
__asm
{
push ebp
push ebx
push esi
push edi
mov 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, eax
xor ebx, ebx
cmp edx, 0
je Label1
mov al, byte ptr [edi]
mov bl, byte ptr [edi+4]
add edi, 8
lea ecx, dword ptr [esi+edx]
sub ebp, esi
mov dword ptr [esp+0x18], ecx
inc al
cmp dword ptr [edi+0x100], -1
je Label2
mov ecx, dword ptr [edi+eax*4]
and edx, 0xFFFFFFFC
je Label3
lea edx, dword ptr [esi+edx-4]
mov dword ptr [esp+0x1C], edx
mov dword ptr [esp+0x20], ebp
Label4:
add bl, cl
mov edx, dword ptr [edi+ebx*4]
mov dword ptr [edi+ebx*4], ecx
mov dword ptr [edi+eax*4], edx
add edx, ecx
inc al
and edx, 0x0FF
mov ecx, dword ptr [edi+eax*4]
mov ebp, dword ptr [edi+edx*4]
add bl, cl
mov edx, dword ptr [edi+ebx*4]
mov dword ptr [edi+ebx*4], ecx
mov dword ptr [edi+eax*4], edx
add edx, ecx
inc al
and edx, 0x0FF
ror ebp, 8
mov ecx, dword ptr [edi+eax*4]
or ebp, dword ptr [edi+edx*4]
add bl, cl
mov edx, dword ptr [edi+ebx*4]
mov dword ptr [edi+ebx*4], ecx
mov dword ptr [edi+eax*4], edx
add edx, ecx
inc al
and edx, 0x0FF
ror ebp, 8
mov ecx, dword ptr [edi+eax*4]
or ebp, dword ptr [edi+edx*4]
add bl, cl
mov edx, dword ptr [edi+ebx*4]
mov dword ptr [edi+ebx*4], ecx
mov dword ptr [edi+eax*4], edx
add edx, ecx
inc al
and edx, 0x0FF
ror ebp, 8
mov ecx, dword ptr [esp+0x20]
or ebp, dword ptr [edi+edx*4]
ror ebp, 8
xor ebp, dword ptr [esi]
cmp esi, dword ptr [esp+0x1C]
mov dword ptr [ecx+esi], ebp
lea esi, dword ptr [esi+4]
mov ecx, dword ptr [edi+eax*4]
jb Label4
cmp esi, dword ptr [esp+0x18]
je Label5
mov ebp, dword ptr [esp+0x20]
Label3:
add bl, cl
mov edx, dword ptr [edi+ebx*4]
mov dword ptr [edi+ebx*4], ecx
mov dword ptr [edi+eax*4], edx
add edx, ecx
inc al
and edx, 0x0FF
mov 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], dl
jb Label3
jmp Label5
Label2:
movzx ecx, byte ptr [edi+eax]
Label6:
add bl, cl
movzx edx, byte ptr [edi+ebx]
mov byte ptr [edi+ebx], cl
mov byte ptr [edi+eax], dl
add dl, cl
movzx edx, byte ptr [edi+edx]
add al, 1
xor 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], dl
jb Label6
Label5:
dec al
mov byte ptr [edi-4], bl
mov byte ptr [edi-8], al
Label1:
pop edi
pop esi
pop ebx
pop ebp
retn
}
}
十二.不走游戏代码自己发送封包
我们简单分析一个喊话封包,开始自己调用不走任何游戏代码
send头文件
#include "winsock.h"
有问题记得加上如下代码
#pragma comment(lib,"ws2_32.lib")
void HXSYDialog::OnBnClickedButton16()
{
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 包长 = 0x13;
DWORD 包地址 = (DWORD)a;
DWORD 加密地址 = 包地址 + 2;
DWORD 加密长度 = 包长 - 2;
DWORD 秘钥 = 0;
__asm
{
mov ecx,0x00f84ba4
mov ecx,[ecx]
mov ecx,[ecx]
mov ecx,[ecx+0x4]
mov ecx,[ecx+0x14]
mov ecx,[ecx]
lea ecx,[ecx+0x54]
mov 秘钥,ecx
}
加密call(秘钥,加密长度,加密地址,加密地址);
HWND 窗口句柄 =FindWindowA("Lapis Network Class",0);
DWORD A = GetWindowLongW(窗口句柄,-21);
DWORD S =*(DWORD*)(A+0x38);
send(S,(const char*)包地址,包长,0);
// TODO: 在此添加控件通知处理程序代码
}
我们分析几个明文包 ,然后调用测试
十三.收包
收包函数recv,recvfrom, WSARecv
(收包的参数包地址和长度 是准备接受的包地址 和最大包长)
这是必然的,因为调用前是不可能知道收到的内容和具体长度的,只有执行完毕才会获得
recv 的实际包长 在返回值eax
WSARecv 在第四个参数
十四.粘包
我们发现断的时间越长 收包越大 原因很简单 因为可能沾包
当我们线程停住的时候,服务器依然在给我们发包,这个时候就会一起接收到
那么停住的时间越长,包就粘的越长.
每个独立的封包都有很明显的划分方法,所以拆包并不难
十五.明文收包和解密call
收包我们收到的肯定也是服务器发给我们的加密封包
加密封包经过解密处理,把里面的数据写入到对应内存位置,这样我们就在界面上显示相应的反馈了.
明文收包和明文发包方向是反的
要找去哪里了,所以是下访问断追其去向
我们发现 lpbuffers +4里的包地址 是固定的, 是否固定其实无所谓
那么我们想追解密函数 就要对 他进行访问断
当然 通过我们自己喊话等方式 让其断下
2个拷贝的位置都可以
再对拷贝的地址下访问断
返回就是解密call
同时也是明文收包的位置了
然后我们发现 加密和解密是同一个函数
加密和解密是一个可逆的过程
正常我们其实直接搜索call 就可以找到明文收包的位置了
十六.线程发包实现的原理和方法
上面的例子学习完毕以后,实际上大家可以在追线程发包的时候还会存在一些疑惑
为什么一定要追到不变的地址,还有什么其他情况等等
其实,
线程发包本质是两条线交互信息.
那么无论是全局变量还是全局类对象下面的属性
肯定都是不变的地址
通信必然要写到这里
当然还有线程其他的通信方式
我们以后学习更多线程的知识再进行讲解