游戏逆向之手搓封包工具思路
先介绍发包单线程发包(同步发包) 整条业务逻辑、组包、网络发送全部在同一个线程一次性跑完,调用完发包 call 就立刻走网络。 ① 无加密版本流程:控件 call → 功能 call1 → 功能 call2 → 组包 → 发包 控件 call:入口,触发发包事件(比如人物移动、技能、聊天这类动作入口函数) 功能 call1:业务逻辑,填充对象、校验状态、读取角色数据 功能 call2:
先介绍发包
单线程发包(同步发包)
整条业务逻辑、组包、网络发送全部在同一个线程一次性跑完,调用完发包 call 就立刻走网络。
① 无加密版本
流程:控件 call → 功能 call1 → 功能 call2 → 组包 → 发包
- 控件 call:入口,触发发包事件(比如人物移动、技能、聊天这类动作入口函数)
- 功能 call1:业务逻辑,填充对象、校验状态、读取角色数据
- 功能 call2:业务处理,计算要发送的数据内容
- 组包:把数据按游戏协议格式拼接成二进制数据包(包头 + 消息 ID + 包体)
- 发包 call:直接调用底层 send 函数,把拼好的包发给服务器,没有加密步骤
✅ 优点:
- 链路简单,逆向最简单,就是现在找到的 push 参数 + call send
- 没有队列、没有多线程锁,不容易出现包乱序问题 ❌ 缺点:
- 网络阻塞会卡住游戏主线程,网络卡顿的时候游戏会掉帧、卡顿
- 大部分网游不会把核心业务放在这个链路,一般用于次要、轻量消息
② 单线程加密版本
流程:控件 call → 功能 call1 → 功能 call2 → 组包 → 加密 call → 发包 和上面几乎一样,多一步加密 call: 组包完成之后,调用加密函数对整个二进制包做加密 / 异或 / 压缩,加密完成后再调用 send 发包。
很多老游戏单线程加密包,加密逻辑直接嵌在主线程里。
双线程异步队列发包(主流网游标准方案)
线程1(生产者:构造数据包):控件call → 功能call1 → 功能call2 → 组包 → 加密call → 写入【全局结构(包队列)】
线程2(消费者:网络发送):循环读取全局结构的封包信息 → 拷贝缓冲区 → 调用底层send发包
- 线程 1(游戏主线程 / 逻辑线程)
只负责生产数据包:执行业务逻辑、组包、加密。
包做好之后不直接调用 send,而是把包指针 / 包数据写入全局结构体(环形队列),写完就返回,继续跑游戏逻辑,不会等待网络 IO。
- 全局结构(共享包队列)
一块全局内存,作为缓冲区,存放待发送的封包。
⚠️ 重点:多线程读写,游戏内部会加互斥锁,防止两个线程同时读写导致内存错乱、崩溃。
- 线程 2(网络发送线程,独立后台线程)
死循环不断扫描全局队列:
- 队列有包 → 取出封包信息,拷贝包缓冲区
- 调用底层 send 发包
- 发送完成,清理队列里这条包
✅ 优点:
- 网络 IO 阻塞完全隔离,就算网络卡,游戏主线程不会卡顿(网游最核心需求)
- 可以做包合并、限流、重发、丢包检测
❌ 逆向难点:
- 组包和 send 发包不在同一段汇编,断点断 send 很难定位上层业务 call
- 存在全局队列、锁,写模拟调用很容易因为多线程竞争导致崩溃
快速区分两种模型(逆向调试判断方法)
- 单线程(你当前找到的)
断点断在 send 发包 call,往上回溯,组包、业务逻辑都在同一个调用栈,组包完直接 push 参数 call send,没有写入全局队列的操作。
- 双线程队列发包
断 send 的时候,调用栈只有网络线程代码;上层控件 call 执行完,只是写全局内存,根本不会走到 send。
Windows send 函数(WS2_32.dll)
send 是 WinSock 标准 API,位于 ws2_32.dll,TCP 连接建立之后,用来把内存里的二进制数据包交给操作系统内核缓冲区,最终发给游戏服务器。
就是你逆向里 call <JMP.&send> 这个 API。
1、函数原型(C 语言)
int send(
SOCKET s, // 参数1:套接字句柄(游戏和服务器的连接句柄)
const char *buf, // 参数2:封包缓冲区指针(存放组包好的二进制数据)
int len, // 参数3:封包长度,多少字节
int flags // 参数4:标志位,游戏里几乎永远传 0
);调用约定:stdcall,32 位程序,从右往左 push 参数,函数内部自己平衡堆栈。
所以汇编顺序:先 push 最右边的 flags,依次往左。
对应你之前那段汇编
mov eax,dword ptr ds:[0x1D6A368] ; eax = 套接字s(第一个参数)
push ebx ; flags 第4参数
push 0x1 ; len 第3参数
push 0x1326B7E ; buf 第2参数
push eax ; s 第1参数
call <JMP.&send>注意:这里是游戏封装后的 send 包装 call,不是原生 ws2_32!send,它重新包装了参数顺序,所以参数顺序和原生 send 反过来了,这是游戏自己封装的发包壳函数。
2、参数详解
- SOCKET s 游戏客户端和服务器 TCP 连接的句柄,全局唯一,登录成功后就固定。 游戏一般放在全局结构体,保存 socket、加密密钥、会话信息。
- buf封包缓冲区指针,就是组包完的二进制数据,包头 + 消息 ID + 包体。 你下断点断在这里,就能直接看到明文包内容(无加密发包)。
- len 数据包字节长度,告诉操作系统,缓冲区有多少字节要发。
- flags 控制选项,网游绝大多数情况填 0。
- MSG_OOB:发送带外紧急数据(游戏极少用)
- MSG_DONTROUTE:不走路由,内网直连
3、返回值
- 0:成功,返回本次拷贝到系统内核缓冲区的字节数(不一定等于 len,TCP 分片)
- =0:连接正常关闭
- SOCKET_ERROR (-1):发送失败,网络断 / 服务器断开,可以用WSAGetLastError拿错误码
⚠️重点:send不是直接把数据包立刻发到服务器! send 只是把数据拷贝到操作系统内核的 TCP 发送缓冲区,send 返回只代表数据进了系统缓冲区,不代表服务器已经收到。真正的网络
ctrl+g搜索send
断点断在函数头
看堆栈一直运行
可以发现2个地方调用了这个函数,
分别标记为send1与send2,
由于断点看到send1与send2,可以发现send1每次断下发包大小都为1,
所以send2是真正的发包函数
已知信息图中注释可以这样
这里先教会你怎么追一个参数的基地址吧,
这里一eax为例子
当前eax==512061A8
00D0C144 | mov eax,dword ptr ds:[eax+0x4] ; [eax+4]
dump [eax+4],这个直接在终端验证看看eax是不是我的那个值
eax==512061A8
00D0C133 | 8B46 14 | mov eax,dword ptr ds:[esi+0x14]
dump [[esi+0x14] +4]
00DAF531 8B742408 mov esi,dword ptr ss:[esp+0x8
dump [[[esp+0x8] +0x14] +4]
68750008
在这个地方发现每次下断点都直接断下来了,都没移动角色使它发包
说明有多个地方调用了这个call,或者说函数
所以解决办法是在这个地方下条件断,然后看堆栈图
也就是追eax
dump [[eax+0x14] +4]
00DB0773 | mov eax,dword ptr ds:[esi] |
dump [[[esi] +0x14] +4]
esi==2051ACB4 3E2DA3C0 1711A6AC
发现进入循环判定了这里
可以分析假如esi没有等于ebx的值
则esi一直加4知道等于ebx则跳出循环
在循环头部下一个断点
esi==2051ACB0
所以直接写dump [[[esi+4]+0x14]+4]
00DB0766 mov esi,dword ptr ss:[esp+0x14]
dump [[[[esp+0x14] +4] +0x14] +4]
看堆栈发现传的是第3个call的参数,所以直接esp+8的位置点到call的返回值
所以这个参数为edx
dump [[[edx +4] +0x14]]
mov edx,dword ptr ds:[0x1D6FOD4]
dump [[[[0x1D6F0D4] +4] +0x14] +4]
01D6F0D0
这个就是基址偏移量
封包小技巧
1.想分析封包必须从buffer入手
2.确定buffer是否发生变化
3.明文包
4.buffer是从哪里写入的
组包->明文->加密call->缓冲区拷贝->倒几手
心跳包(Heartbeat / KeepAlive)
一、是什么
心跳包是客户端定时发给服务器的极小数据包,作用:告诉服务器「我这个客户端还在线,连接存活」。
TCP 本身有系统层的 KeepAlive,但游戏几乎都会自己实现应用层心跳,因为系统 TCP 保活粒度太粗、延迟高,容易被防火墙、中间代理误断连接。
一句话:客户端每隔固定时间,发一个很小的包,告诉服务器:我还活着,别踢我下线。
二、核心作用
- 保活,防止连接被断开
运营商、防火墙、NAT 网关,长时间没有流量就会自动掐断空闲 TCP 连接。心跳包持续产生流量,维持通道。
- 服务器检测客户端是否掉线
服务器设定超时阈值(例如:5 秒没收到心跳 → 判断客户端离线,踢下线、释放角色资源)。
- 部分游戏附带同步时间、延迟探测
心跳包里附带时间戳,服务器用来计算客户端网络延迟 RTT。
三、心跳包特征(逆向一眼识别)
- 包体极小:很多只有包头 + 消息 ID,没有额外数据,几十字节以内。
- 固定周期发送:最常见 2s / 3s / 5s 定时发送,不是玩家操作触发(走路、放技能才发包属于业务包)。
- 独立消息 ID:单独一个协议号,和移动、聊天、战斗包分开。
- 触发方式:定时器触发,不是玩家按键触发。
重点区分:
- 业务包:玩家操作(移动、对话)才发包,按需触发
- 心跳包:不管玩家动不动,定时自动发包
四、两种实现方式,对应你前面的发包模型
1. 单线程同步心跳(对应单线程发包模型)
游戏主线程定时器触发 → 控件 call → 组心跳小包 → 直接调用 send 发送
- 优点:简单,逆向容易找到心跳 call
- 缺点:主线程阻塞会影响心跳发送,网络卡顿会导致心跳延迟、被服务器踢下线
2. 线程异步心跳(双线程队列发包)
定时器触发主线程组包 → 写入全局包队列 → 网络线程读取队列,执行 send
心跳包和普通业务包走同一套队列,统一网络线程发送。
五、交互逻辑(标准流程)
- 客户端登录成功,连接建立
- 客户端启动定时器,每 N 秒构造心跳包
- 调用发包链路,发送心跳包给服务器
- 服务器收到心跳,更新该玩家的最后收到数据包时间戳
- 服务器持续轮询:
- 在超时时间内收到心跳 → 保持在线
- 超过超时时间仍未收到心跳 → 判定掉线,踢下线
可选:双向心跳:客户端发心跳给服务器,服务器回复心跳应答包。客户端也通过服务器应答判断连接是否断开。
六、逆向排查心跳包的方法
- 在 send 下断点,不操作游戏,静置不动,看会不会每隔固定时间自动断下;自动断下的小包大概率就是心跳。
- 抓包看流量:静止状态下,周期性出现的短小数据包就是心跳。
- 向上回溯调用栈:找到定时器函数,就是心跳的入口 call。
所以回到之前的send2
己经知道eax为存储发包数据的地方,只有心跳包或者你移动释放技能等才会改变里面的值.这里我以大厅发送消息为例子
为了防止心跳包,我们对比一下
这是我没有任何运动的堆栈
发现下面的值都不变了,那是不是假设我在地址偏移很远的地方下硬件访问断就可以过滤掉心跳包,那我直接在大厅发送很多的信息,这个就会追溯到我的发送地方改变的地方
所以可以下一个硬件写入断
看堆栈可以知道是系统领空
所以直接ctrl+f9 加f8跳出这个调用的系统函数,分析系统函数及其参数
发现是memmove
可以知道edi是目的地址
可以在edi那里下条件断点,&edi==68750008
然后接着发信息使其断下
得到ecx的值
ecx==7DA909F0
分析call,看显示cpu可以看到 ecx为一个很小的值应该为大小,eax为追溯地址
edx为最终地址
所以call为赋值call
然后接着追溯eax
发现上面有一个mov eax,dword ptr ds:[esi+4]
经过调试发现eax的值不断发生变化,猜测应该是新申请的
先在这里下断点一直点运行发现esi的值不会发生变化
将esi的值转到内存窗口去看,已经知道+4的地方为发包起始地址,那+8猜测为发包结束地址,
接着找esi
已经知道第2个为起始地址
发送11111
发现发送的包终于不是加密的了
发现前面追的send2这里还是加密的
所以在这个call里面发生了变化
当在这里时候还是明文
这里怀疑是这种发包,可以验证一下
怎么返回第2条线程呢
可以在共同用的读写内存区,下一个写入call就能找到线程1的写
可以在eax+4下断点,先看看eax+4下是否发生变化
eax==43C7AED8
[eax+4]==59E01530
2个内存窗口不同的值,一个是0,一个是一个地址值,所以就是那个地址值且线程不一样验证先下断然后喊话
有地址值的点运行,发现只有2次的写入
然后一次没值,一次有对象
点开对象
我这由于心跳包所以7E3E4870地址下的东西,看内容发现并没有发送的111111
所以肯定是后面的东西是赋值地方
mov [esi+4] ,eax
这个时候这个 之前EAx+4下的地址的地址为未知的结构,没有写入数据
应该先写入地址,再往封包地址下写入数据
确实后面给封包数据填充了东西
所以整个发包流程都有了
可能是
先加密call->send1 ->send2
也可能send1->加密call->send2
如何确定呢
来到注释send1,加密call,send2 3个地方,都下断点,然后接着发送消息,一直点运行,看先后顺序
可以发现先是send1后加密call然后 send2
接着打开我的链接中的代码注入器
尝试这个流程能不能走通
发现不论如何重启游戏ebx的值都是0,但eax的值会发生变化,虽然eax在游戏中调试不变,
所以这里也直接追一下eax,和上面send2追eax一样
eax==694
发现前面就有mov eax ,dword ptr ds:[0x1D6A368]
这个是send1
接着分析加密call
mov ecx,dword ptr ds:[edi+0x3C]
dump [edi+0x3C] 50F23130
mov ecx,dword ptr ds:[edi+0x3C]
ctrl+f9 f8,跳出这个call
mov ecx,dword ptr ds:[esi+0xC]
dump [[esi+0xC]+0x3C]
esi==4C4AE890
跳出call,不断点运行
edi的值一直发生2次变化,但有一个值满足4C4AE890,
下条件断就直接断下来了
dump [[edi+0xC]+0x3C] 1365CD28
接着条件断发现断下来了,
00DB1954 | mov edi,dword ptr ss:[esp+0x14] | 暂停条件([esp+0x14]==4C4AE890)
所以这个值是ecx
00DB1C1E | mov ecx,dword ptr ds:[esi+0x10] |
下条件断后可以断下
dump [[[esi+0x10] +0xC]+0x3C]
esi==0EE29398
经过调试可以发现这里是个循环
370BECE0
00DB1C68 | mov eax,dword ptr ds:[esi+0x8] |
dump [[[[esi+0x8] +0x10] +0xC]+0x3C]
1B1D3338
00DB1C8B | mov eax,dword ptr ds:[esi+0x4]
dump [[[[[esi+0x4] +0x8] +0x10] +0xC]+0x3C]
esi== 370BECE0
mov esi,[eax]
mov eax,[0x1D6C944]
dump [[[[[[[0x1D6C944]]+0x4] +0x8] +0x10] +0xC]+0x3C]
send1->加密call已经写好了
就只要写send2了,所以
接着跳到send2
看ecx可以我们发现和之前找的eax有关
dump [[[0x1D6F0D4] +4] +0x14]
ecx==[[[[0x1D6F0D4] +4] +0x14]+8] -[[[[0x1D6F0D4] +4] +0x14]+4]
接着追edx
edx==F1C
00D0C147 | 8B56 04 | mov edx,dword ptr ds:[esi+0x4] |
dump [esi+0x4]
上面看见mov esi,ecx
发现又很熟悉,又是之前追过的
buffer3
dump [[[0x1D6F0D4] +0x4]+0x4]为edx
dump [[[[0x1D6F0D4] +0x4] +0x14] +0x4]这个为eax
ecx==[[[[0x1D6F0D4] +0x4] +0x14]+0x8] -[[[[0x1D6F0D4] +0x4] +0x14]+0x4]
发现esi+0 的值一直不变 esi+4为发包内容初始值 esi+8为esi结尾
esi+4的值每次发生变化,,我追了一下可以知道每次都调用系统call使其申请一段新的内存,所以我也申请一个新的内存
0000204F0000000000006A60000000000401000000310031
这个就是复制的值
```asm
push 0
push 0x1
push 0x1326B7E
mov ebx, 0x1D6A368
mov ebx, [ebx]
push ebx
mov eax, 0x772A7630
call eax
mov ecx, 0x778906F0
push 0x11650000
mov eax, 0x00DAFAF0
call eax
push 0
mov eax, [0x1D6F0DD4]
mov eax, [eax+0x4]
mov esi, eax
mov esi, [esi+0x4]
mov eax, [eax+0x14]
mov ecx, eax
mov ecx, [ecx+0x8]
mov eax, [eax+0x4]
sub ecx, eax
push ecx
push eax
push esi
mov eax, 0x772A7630
call eax
```成功了了并且还要追这个虚函数地址
也就是加密call 传的参首地址还要追
在那个地方下硬件写入断点
数据找完了
开始教封包工具思路
esp+0 0040cb9e
esp+4 00000001
ESP+8 00000002
假设将跳转的函数的前3行代码取消替换掉了来个jump,直接写一个裸函数
直接输出esp的值进行堆栈回溯例如esp+4为
假如只要发生改变例如硬编码改变就算发生hook
先介绍一下inline hook
00081780 55 push ebp
00081781 8B EC mov ebp,esp
00081783 81 EC C0 00 00 00 sub esp,0C0h
提升栈底
00081789 53 push eb
x0008178A 56 push esi
0008178B 57 push edi
保存异变寄存器
0008178C 8B FD mov edi,ebp
0008178E 33 C9 xor ecx,ecx (没写局部变量不填充)00081790 B8 CC CC CC CC mov eax,0CCCCCCCCh
00081795 F3 AB rep stos dword ptr es:[edi]
为缓存区填充数据
00081797 B9 04 C1 08 00
mov ecx,offset _37123520_Main2@cpp (08C104h)
0008179C E8 84 FB FF FF
call @__CheckForDebuggerJustMyCode@4
编译器填充的
(081325h)000817A1 90 nop
return a + b;
000817A2 8B 45 08 mov eax,dword ptr [a]
000817A5 03 45 0C add eax,dword ptr [b]
核心代码
000817A8 5F pop edi
000817A9 5E pop esi
000817AA 5B pop ebx
恢复异变寄存器
000817AB 81 C4 C0 00 00 00 add esp,0C0h
000817B1 3B EC cmp ebp,esp
000817B3 E8 91 FA FF FF call __RTC_CheckEsp (081249h)
将栈顶栈底降下来 ,并进行一个比较
000817B8 8B E5 mov esp,ebp
000817BA 5D pop ebp000817BB C3 ret
释放栈顶栈底,然后返回
00081780 55 push ebp
00081781 8B EC mov ebp,esp
00081783 81 EC C0 00 00 00 sub esp,0C0h
提升栈底
//所以在这里HOOK
JMP 0xe80000
call 0xe80000
一般将上面9字节的参数全部覆盖再恢复
00081789 53 push ebx
0008178A 56 push esi
0008178B 57 push edi
保存异变寄存器
0008178C 8B FD mov edi,ebp
0008178E 33 C9 xor ecx,ecx 没写局部变量不填充)
00081790 B8 CC CC CC CC mov eax,0CCCCCCCCh
00081795 F3 AB rep stos dword ptr es:[edi]
为缓存区填充数据
00081797 B9 04 C1 08 00 mov ecx,offset _37123520_Main2@cpp (08C104h)
0008179C E8 84 FB FF FF call @__CheckForDebuggerJustMyCode@4
编译器填充的
(081325h)000817A1 90 nop
return a + b;
000817A2 8B 45 08 mov eax,dword ptr [a]
000817A5 03 45 0C add eax,dword ptr [b]
核心代码
000817A8 5F pop edi000817A9 5E pop esi
000817AA 5B pop ebx
恢复异变寄存器
000817AB 81 C4 C0 00 00 00 add esp,C0h
000817B1 3B EC cmp ebp,esp
000817B3 E8 91 FA FF FF call __RTC_CheckEsp (081249h)
将栈顶栈底降下来 ,并进行一个比较
000817B8 8B E5 mov esp,ebp
000817BA 5D pop ebp000817BB C3 ret
释放栈顶栈底,然后返回
ctrl+G 00401000
和call 硬编码计算一样 都是jmp或call 的地址减去其返回的下一个地址
所以硬编码是 E9 FBEF9A00
0x00 InlineHook 简单示例(x86)
32位程序,对自定义Add函数实现Inline Hook,原理:修改目标函数头部5字节为
E9 相对跳转,跳转到自定义Hook函数,执行完逻辑再跳回原函数。
#include <iostream>
#include <Windows.h>
CHAR opCode[9] = { 0 };
DWORD dwRetData = 0;
CHAR szBuff[0x256] = { 0 };
DWORD dwRetAddr = 0;
DWORD dwParam1 = 0;
DWORD dwParam2 = 0;
using namespace std;
int Add(int a, int b) {
return a + b;
}
_declspec(naked) void HookFun() {
//保存寄存器环境
__asm {
PUSHAD
PUSHFD
}
//功能代码实现
__asm
{
//获取返回地址
MOV EAX,[ESP+0x24]
MOV [dwRetAddr],EAX
//获取参数1
MOV EAX,[ESP+0x28]
MOV [dwParam1],EAX
//获取参数2
MOV EAX, [ESP + 0x2c]
MOV[dwParam2], EAX
}
printf("%x \n", dwRetAddr);
printf("%x \n", dwParam1);
printf("%x \n", dwParam2);
//恢复寄存器环境
__asm {
POPFD
POPAD
}
//恢复原函数开头被覆盖的指令(这里写的是Add默认开头 prolog)
__asm {
PUSH EBP
MOV EBP,ESP
SUB ESP, 0xC0
}
//跳转回原函数剩余代码
__asm {
jmp [dwRetData]
}
}
/// <summary>
/// 设置Inline Hook
/// </summary>
/// <param name="dwHOOKAddr">目标函数地址</param>
/// <param name="dwTargetAddr">自定义Hook函数地址</param>
VOID SetInlineHook(DWORD dwHOOKAddr, DWORD dwTargetAddr) {
DWORD dwOldPro = 0;
DWORD dwOldPr1 = 0;
BOOL bRet = 0;
DWORD dwJmpData = 0;
//修改内存页属性为可读写执行
bRet=VirtualProtect((LPVOID)dwHOOKAddr, 9, PAGE_EXECUTE_READWRITE, &dwOldPro);
if (!bRet)
{
return;
}
//备份目标函数前9字节原始机器码
memcpy(opCode, (LPVOID)dwHOOKAddr, 9);
//填充NOP
memset((LPVOID)dwHOOKAddr, 0x90,9);
//构造E9相对跳转指令 E9 + 4字节偏移
char szCode[5] = { 0 };
szCode[0] = 0xE9;
dwJmpData = dwTargetAddr-dwHOOKAddr - 5;
*(PDWORD)(szCode+1) = dwJmpData;
memcpy((LPVOID)dwHOOKAddr, szCode, 5);
//记录跳转返回地址:原地址+5(跳过E9 jmp指令)
dwRetData = dwHOOKAddr + 5;
//还原内存页权限
VirtualProtect((LPVOID)dwHOOKAddr, 9, dwOldPro, &dwOldPr1);
}
/// <summary>
/// 解除Inline Hook,恢复原始机器码
/// </summary>
/// <param name="dwHookAddr">目标函数地址</param>
VOID UnInlineHook(DWORD dwHookAddr) {
DWORD dwOldPro = 0;
DWORD dwOldPr1 = 0;
BOOL bRet = 0;
//修改内存属性
bRet = VirtualProtect((LPVOID)dwHookAddr, 9, PAGE_EXECUTE_READWRITE, &dwOldPro);
if (!bRet)
{
return;
}
//写回原始备份机器码
memcpy((LPVOID)dwHookAddr, opCode, 9);
//恢复内存页权限
VirtualProtect((LPVOID)dwHookAddr, 9, dwOldPro, &dwOldPr1);
}
int main() {
//Hook Add函数
SetInlineHook((DWORD)Add,(DWORD)HookFun);
Add(1, 2);
//卸载Hook
UnInlineHook((DWORD)Add);
return 0;
}0x01 代码说明
- 编译环境:VS,必须32位项目,
_declspec(naked)取消编译器自动生成函数序言。 - Hook原理:覆盖
Add函数头部5字节为E9 偏移,执行Add时直接跳转到HookFun。 - Hook函数内
PUSHAD/PUSHFD保存全部寄存器,读取栈上返回地址与函数参数。 - 执行完自定义逻辑后,手动执行原函数被覆盖的汇编指令,再
jmp回到原函数继续执行。
0x02 注意缺陷
- ⚠️ 硬编码写死
PUSH EBP;MOV EBP,ESP;SUB ESP,0xC0,只适配当前Add函数序言。如果目标函数头部机器码不是这几条,直接崩溃,真实项目需要先备份原指令,在hook函数里执行备份指令,而不是硬写汇编。 - ⚠️ 没有多线程保护,多线程场景下会出现竞态,触发崩溃。
- ⚠️ 只支持x86,x64地址大于4字节,E9跳转范围有限,实现方式完全不同。
- ⚠️ 测试仅用于学习研究,不可用于商业/恶意用途。
回到加密call
每次经过这里就会发一次包,所以就可以进行标志位检测
可以手写一个标志数传参
然后再jmp回去
思路有了写工具,先搭建框架
待发送内容字符到16进制字节转换
接着写个这个函数再将我上面查的数据改改就可以完美封包了
很好奇笔者是做什么工作的,因为上次看wpe类似的文章还是好多年前了,现阶段很少有人讲发包这方面内容了