免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动!
如果看不懂、不知道现在做的什么,那就跟着做完看效果
内容参考于:易道云信息技术研究院VIP课
上一个内容:35.登录成功数据包内容分析
码云地址(master 分支):https://gitee.com/dye_your_fingers/titan
码云版本号:9474c79b07d63defaf0ca32e3cd3df21f8625505
代码下载地址,在 titan 目录下,文件名为:titan-数据解码器的实现.zip
链接:https://pan.baidu.com/s/1W-JpUcGOWbSJmMdmtMzYZg
提取码:q9n5
--来自百度网盘超级会员V4的分享
HOOK引擎,文件名为:黑兔sdk升级版.zip
链接:https://pan.baidu.com/s/1IB-Zs6hi3yU8LC2f-8hIEw
提取码:78h8
--来自百度网盘超级会员V4的分享
以 34.登录数据包的监视与模拟 它的代码为基础进行修改
首先添加一个类:
然后把所有角色都删了,看看没有角色登录之后的数据包,如下图没有角色的登录成功数据包
效果图:本次实现了,解读数据解析约定的数据包
新加 EnCode.h文件:用于数据解析约定数据包解析,一次只能解析一个
#pragma once
class EnCode
{
private:
char un[2]{};// 这个是为了内存对齐
public:
char index = 0;
// op + un[3]是四字节,op与buffer是6字节,如果自动内存对齐op与buffer可能不会挨在一起
char op = 0;
union
{
char dataPool[0x8];
int lenth;
char byte;
short stval;
int val;
float fval;
double dbval;
long long lval = 0;
};
char* pointer = 0;
public:
EnCode(char*& buff, unsigned int& _len, char ExIndex = 0);
~EnCode();
};
新加 EnCode.cpp文件:用于数据解析约定数据包解析,一次只能解析一个
#include "pch.h"
#include "EnCode.h"
#include "extern_all.h"
/*
buff是数据包
_len暂时没用
ExIndex是解析方式,因为分析的时候发现6有时是char类型有时是char*类型
看懂此方法需要分析手动分析一次数据包(数据解析约定的数据包)
然后带着数据包去看这个函数
*/
EnCode::EnCode(char*& buff, unsigned int& _len,char ExIndex)
{
index = ExIndex;
op = buff[0];
int len = data_desc[index][op].lenth;
/*
比如 07 0A 00 00 00 34 00 33 00 39 00 39 00 00 00
buff + 1 的结果是 0A 00 00 00 34 00 33 00 39 00 39 00 00 00
len是 07 所代表的类型的长度,也就是0A 00 00 00 这个东西
memcpy(dataPool, buff + 1, len);也就是把 0A 00 00 00 复制到 dataPool里
然后从下方的if ((op == 0x7))得到它的字符串
*/
memcpy(dataPool, buff + 1, len);
buff = buff + 1 + len;// 下一个结构
if (index == 0) {
if ((op == 0x7)) {
/*
dataPool 与 lenth是一个联合体,联合体的特性就是
所有变量共用一个内存,所以 dataPool 的值就是lenth的值
从上方的注释得知现在dataPool的值是0A 00 00 00
然后通过 lenth去读 0A 00 00 00结果就是十进制的 10
然后创建一个10字节的空间给pointer
然后在通过 memcpy(pointer, buff, lenth); 把字符串赋值给pointer
*/
pointer = new char[lenth];
memcpy(pointer, buff, lenth);
buff = buff + lenth;// 指向下一个字符串
}
}
if (index == 1) {
if ((op == 0x06)||(op == 0x7)) {
pointer = new char[lenth];
memcpy(pointer, buff, lenth);
buff = buff + lenth;
}
}
}
EnCode::~EnCode()
{
if(pointer)
delete[] pointer;
}
GameWinSock.cpp文件的修改:新加 OnloginOk函数,修改了 Init函数
#include "pch.h"
#include "GameWinSock.h"
#include "extern_all.h"
#include "NetClass.h"
#include "EnCode.h"
typedef bool(* DealProc)(char*&, unsigned&);
DealProc SendDealProc[0x100];
DealProc RecvDealProc[0x100];
GameWinSock::PROC GameWinSock::_OnConnect{};
GameWinSock::PROC GameWinSock::_OnSend{};
GameWinSock::PROC GameWinSock::_OnRecv{};
bool DeafaultDeal(char*&, unsigned&) { return true; }
// 登录数据包的处理
bool Onlogin(char *& buff, unsigned& len) {
/* 修改账号密码
len = sizeof(DATA_LOGIN) + 1;
buff = new char[len];
DATA_LOGIN data;
PDATALOGIN _data = &data;
buff[0] = 0x2;
CStringA _id = "";// 补充账号
CStringA _pass = "";// 补充密码
memcpy(_data->Id, _id.GetBuffer(), _id.GetLength());
memcpy(_data->Pass, _pass.GetBuffer(), _pass.GetLength());
memcpy(buff + 1, _data, len - 1);
*/
/* 监控登录数据
PDATALOGIN _data = (PDATALOGIN)buff;
CStringA _id = _data->Id;
_data = (PDATALOGIN)(buff + _data->lenId - 0x10);
CStringA _pass = _data->Pass;
CStringA _tmp;
// 请求登录 账号[% s]密码[% s] 这个内容别人在逆向的时候就会看到
// 所以这种东西需要自己搞个编码来代替它
_tmp.Format("请求登录 账号[%s]密码[%s]", _id, _pass);
#ifdef Anly
anly->SendData(TTYPE::I_DIS, 1, _tmp.GetBuffer(), _tmp.GetAllocLength());
#endif
*/
/*
返回false,游戏无法发送数据包
原因看调用此此函数的位置 OnSend 函数(if (SendDealProc[buff[0]]((buff + 1), len - 1)))
*/
return true;
}
bool Onloginfailed(char*&buff, unsigned& len) {
int* code = (int*)&buff[1];
Client->loginfailed(code[0]);
return true;
}
bool OnloginOk(char*& buff, unsigned& len) {
CStringA txt;
CStringA tmp;
CString utmp;
PDATALOGINOK _p = (PDATALOGINOK)&buff[1];
if (_p->RoleCount > 0) {
char* buffStart = buff + 1 + sizeof(DATA_LOGIN_OK);
char* buffEnd = buff + len;
while (buffStart < buffEnd) {
/*
由于 EnCode 的入参带了& 所以可以在EnCode里直接对buffStart或len进行操作
也就是说只要入参带了&在函数里的操作会影响它原本的值
比如
char*a = 0;
aaa(a)
aaa(char*&AA){
AA = new char[10]; // 它就会把a的值改成new char[10]
}
*/
EnCode* _coder = new EnCode(buffStart, len);
tmp.Format("[%s]",data_desc[_coder->index][_coder->op].name);
txt = txt + tmp;
if (_coder->op == 0x7) {
utmp = (wchar_t*)_coder->pointer;
tmp = utmp;
txt = txt + tmp;
}
if (_coder->op == 0x3) {
tmp.Format("[%f]", _coder->fval);
txt = txt + tmp;
}
else {
tmp.Format("[%f]", _coder->val);
txt = txt + tmp;
}
}
#ifdef Anly
// 长度24的原因,它是宽字节要,一个文字要2个字节,一共是10个文字加上结尾的0是11个
// 所以 11 乘以2,然后再加2
anly->SendData(TTYPE::I_DIS, 0, txt.GetBuffer(), txt.GetAllocLength() + 1);
#endif
}
return true;
}
// 这个函数拦截了游戏的连接
bool GameWinSock::OnConnect(char* ip, unsigned port)
{
#ifdef Anly
// 长度24的原因,它是宽字节要,一个文字要2个字节,一共是10个文字加上结尾的0是11个
// 所以 11 乘以2,然后再加2
anly->SendData(TTYPE::I_LOG, 0, L"服务器正在连接。。。", 24);
#endif
// this是ecx,HOOK的点已经有ecx了
WinSock = this;
bool b = (this->*_OnConnect)(ip, port);
// 下方注释的代码时为了防止多次注入,导致虚函数地址不恢复问题导致死循环,通过一次性HOOK也能解决
/*unsigned* vtable = (unsigned*)this;
vtable = (unsigned*)vtable[0];
union {
unsigned value;
bool(GameWinSock::* _proc)(char*, unsigned);
} vproc;
vproc._proc = _OnConnect;
DWORD oldPro, backProc;
VirtualProtect(vtable, 0x10x00, PAGE_EXECUTE_READWRITE, &oldPro);
vtable[0x34 / 4] = vproc.value;
VirtualProtect(vtable, 0x10x00, oldPro, &backProc);*/
return b;
}
bool GameWinSock::OnSend(char* buff, unsigned len)
{
/*
这里就可以监控游戏发送的数据了
*/
#ifdef Anly
anly->SendData(TTYPE::I_SEND, buff[0], buff, len);
#endif
/*
数据包的头只有一字节所以它的取值范围就是0x0-0xFF
*/
if (SendDealProc[buff[0]]((buff), len)) {// 执行失败不让游戏发送数据包
return (this->*_OnSend)(buff, len);
}
else {// 发送失败屏蔽消息
return true;// 屏蔽消息
}
}
bool GameWinSock::OnRecving(char* buff, unsigned len)
{
// MessageBoxA(0, "11111111111111", "0", MB_OK);
/*
监控游戏接收的数据包
*/
#ifdef Anly
anly->SendData(TTYPE::I_RECV, buff[0], buff, len);
#endif
return RecvDealProc[buff[0]](buff, len);
}
bool GameWinSock::OnRecv(char* buff, unsigned len)
{
//#ifdef Anly
// anly->SendData(1, buff, len);
//#endif
return (this->*_OnRecv)(buff, len);
}
void GameWinSock::Init()
{
for (int i = 0; i < 0x100; i++) {
SendDealProc[i] = &DeafaultDeal;
RecvDealProc[i] = &DeafaultDeal;
}
// 注册登录数据包处理函数
// SendDealProc[I_LOGIN] = &Onlogin;
// 注册数据登录失败数据包处理函数
RecvDealProc[S_LOGINFAIL] = &Onloginfailed;
RecvDealProc[S_LOGINOK] = &OnloginOk;
}
GameProc.cpp文件的修改:修改了 Init函数
#include "pch.h"
#include "GameProc.h"
#include "extern_all.h"
// typedef bool(GameWinSock::* U)(char*, unsigned);
bool _OnRecv(HOOKREFS2) {
unsigned* _esp = (unsigned*)_ESP;
_EAX = WinSock->RecvPoint;
WinSock->OnRecving((char*)_esp[1], _esp[2]);
return true;
}
bool _OnConnect(HOOKREFS2) {
/*
根据虚函数表做HOOK的操作
截取 ecx 获取 winsock 的值(指针)
*/
unsigned* vtable = (unsigned*)_EDX;
//WinSock = (GameWinSock *)_ECX;
/*
联合体的特点是共用一个内存
由于 GameWinSock::OnConnect 的 OnConnect函数是 GameWinSock类的成员函数
直接 vtable[0x34 / 4] = (unsigned)&GameWinSock::OnConnect; 这样写语法不通过
所以使用联合体,让语法通过
*/
union {
unsigned value;
bool(GameWinSock::* _proc)(char*, unsigned);
} vproc;
DWORD oldPro, backProc;
VirtualProtect(vtable, 0x100, PAGE_EXECUTE_READWRITE, &oldPro);
/*
vproc._proc = &GameWinSock::OnConnect; 这一句是把我们自己写的调用connect函数的地址的出来
*/
vproc._proc = &GameWinSock::OnConnect;
/*
InitClassProc函数里做的是给指针赋值的操作
InitClassProc(&GameWinSock::_OnConnect, vtable[0x34/4]);这一句的意思是把
GameWinSock类里的_OnConnect变量的值赋值成vtable[0x34/4],这个 vtable[0x34/4] 是虚表里的函数
vtable[0x34/4]是游戏中调用connect函数的函数地址,经过之前的分析调用connect是先调用了虚表中的
一个函数,然后从这个函数中调用了connect函数
*/
InitClassProc(&GameWinSock::_OnConnect, vtable[0x34/4]);
vtable[0x34 / 4] = vproc.value;
vproc._proc = &GameWinSock::OnSend;
InitClassProc(&GameWinSock::_OnSend, vtable[0x3C / 4]);
vtable[0x3C / 4] = vproc.value;
VirtualProtect(vtable, 0x100, oldPro, &backProc);
return true;
}
GameProc::GameProc()
{
hooker = new htd::hook::htdHook2();
Init();
InitInterface();
}
void GameProc::LoadBase()
{
LoadLibraryA("fxnet2.dll");
}
void GameProc::Init()
{
#ifdef anly
anly = new CAnly();
#endif
/*
这里的 WinSock 是0没有创建对象,但是还是能调用Init函数
这是因为Init函数里面没有用到this,没用到就不会报错
*/
WinSock->Init();
Client = new NetClient();
init_datadesc();
}
void GameProc::InitInterface()
{
LoadBase();
// MessageBoxA(0, "1", "1", MB_OK);
// 只会HOOK一次,一次性的HOOK
hooker->SetHook((LPVOID)0x10617046, 0x1, _OnConnect, 0, true);
/*
第一个参数是HOOK的位置
第二个参数是HOOK的位置的汇编代码的长度(用于保证执行的汇编代码完整)
第三个参数是HOOK之后当游戏执行到第一个参数的位置的时候跳转的位置
*/
hooker->SetHook((LPVOID)0x10618480, 0x1, _OnRecv);
/*
在这里绑定游戏处理数据包函数(0x10618480函数)
然后0x10618480函数在上面一行代码已经进行了HOOK
所以在调用_OnRecv函数指针时,它就会进入我们HOOK
*/
InitClassProc(&GameWinSock::_OnRecv, 0x10618480);
}
extern_all.cpp文件的修改:新加 data_desc变量初始化、init_datadesc函数
/*
此文件是用来存放全局变量、全局函数(通用函数)
*/
#include "pch.h"
#include "extern_all.h"
GameWinSock* WinSock = nullptr;
GameProc* PGameProc = nullptr;
NetClient* Client = nullptr;
#ifdef Anly
CAnly* anly = nullptr;
#endif
DATA_DESC data_desc[2][9]{};
void init_datadesc() {
data_desc[0][0] = { "none", 0 };
data_desc[0][1] = { "short", 2 };
data_desc[0][2] = { "int", 4 };
data_desc[0][3] = { "int64", 8 };
data_desc[0][4] = { "float", 4 };
data_desc[0][5] = { "int64", 8 };
data_desc[0][6] = { "char", 1 };
data_desc[0][7] = { "wchar_t*", 4 };
data_desc[0][8] = { "int64", 8 };
data_desc[1][0] = { "none", 0 };
data_desc[1][1] = { "short", 2 };
data_desc[1][2] = { "int", 4 };
data_desc[1][3] = { "int64", 8 };
data_desc[1][4] = { "float", 4 };
data_desc[1][5] = { "int64", 8 };
data_desc[1][6] = { "char*", 4 };
data_desc[1][7] = { "wchar_t*", 4 };
data_desc[1][8] = { "int64", 8 };
}
void InitClassProc(LPVOID proc_addr, unsigned value)
{
unsigned* writer = (unsigned*)proc_addr;
writer[0] = value;
}
extern_all.h文件的修改:新加 DATA_DESC结构体、data_desc变量、init_datadesc函数
/*
此文件是用来存放全局变量、全局函数(通用函数)
*/
#pragma once
#include "GameWinSock.h"
#include "GameProc.h"
#include "CAnly.h"
#include "NetClient.h"
typedef struct DATA_DESC {
char* name;
short lenth;
}*PDATADESC;
extern GameWinSock* WinSock;
extern GameProc* PGameProc;
extern NetClient* Client;
extern DATA_DESC data_desc[2][9];
extern void InitClassProc(LPVOID proc_addr, unsigned value);
extern void init_datadesc();
#ifdef Anly
extern CAnly* anly;
#endif
NetClient.h文件的修改:写了一个 loginok函数,这个loginok函数暂无用
#pragma once
#include "NetClass.h"
class NetClient // 监视客户端每一个操作
{
public:
/*
模拟登陆的方法
Id是账号
Pass是密码
它要基于发送的方法实现,因为我们没有连接socket的操作
*/
bool login(const char* Id, const char*Pass);
public:
// 登陆失败,参数是错误码
void loginfailed(int code);
// void loginok();
};
NetClass.h文件的修改:
#pragma once
/*
数据包还原结构体要注意内存对齐,如果数据不满4字节,它字段会补齐
比如结构体里有一个char变量,它是1字节,在内存里它可能会为了内存对齐
让它变成4字节,所以这要注意
*/
// 登录数据
typedef struct DATA_LOGIN {
int op = 0x0300;
char buff[0x10]{};
int lenId = 0x10;
/*
这个是登录的账号,它可能会变成0x20或更长,现在默认让它0x10
读的时候以长度为准就好了
*/
char Id[0x10]{};
int lenPass = 0x10;
/*
这个是登录的密码,它可能会变成0x20或更长,现在默认让它0x10
读的时候以长度为准就好了
*/
char Pass[0x10]{};
int lenCode = 0x10;
char Code[0x10]{};
int eop = 0x01;
}*PDATALOGIN;
typedef struct DATA_LOGIN_OK {
int un[8] = { 0, 0, 0x76B, 0x0C, 0x1E,0, 0, 0 };
int index = 0;
int RoleCount = 0;
}*PDATALOGINOK;