免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动!

如果看不懂、不知道现在做的什么,那就跟着做完看效果

内容参考于:如果看不懂、不知道现在做的什么,那就跟着做完看效果,代码看不懂是正常的,只要会抄就行,抄着抄着就能懂了

上一个内容:53.逆向分析寻找消息数据解压缩过程

码云地址(master 分支):https://gitee.com/dye_your_fingers/titan

码云版本号:6685f3162cbb162a0ecb4ab32c6de9a2c86684e4

代码下载地址,在 titan 目录下,文件名为:titan-截取解压后的数据包.zip

HOOK引擎,文件名为:黑兔sdk升级版.zip

以 53.逆向分析寻找消息数据解压缩过程 它的代码为基础进行修改

最终选择在下图红框位置做HOOK,下图红框位置,EDI的值是解压之后的数据包,EBP的值是数据包大小,详情看下图中的内容

拦截解压之后的数据包

现在的配置

CAnly.h文件的修改:使用 DataAnly.exe 工具新加I_ZIP变量

#define Anly
#ifdef Anly
#pragma once
enum class TTYPE :int {
	I_SEND=0,
	I_RECV=1,
	I_LOG=2,
	I_DIS=3,
	I_ZIP=4,
	MAX
};
#define _TITLE L"titan_data"
typedef struct COPY_DATA {
	time_t ltime; // 时间
	unsigned MsgType; // 数据类型
	unsigned len; // 数据长度
	char buff[0x2000]; // 数据
}*PCOPYDATA;
class CAnly{
public:
LRESULT SendData(TTYPE type, unsigned MsgType, void* buff, unsigned len);
};
#endif

GameProc.cpp文件的修改:新加 _OnComprase函数,修改了 InitInterface函数

#include "pch.h"
#include "GameProc.h"
#include "extern_all.h"

// typedef bool(GameWinSock::* U)(char*, unsigned);

bool _OnComprase(HOOKREFS2) {
	return WinSock->OnRecv((char*)_EDI, _EBP + 1);
}

bool _OnRecv(HOOKREFS2) {
	_EAX = WinSock->RecvPoint;
	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之后当游戏执行到第一个参数的位置的时候跳转的位置
		第四个参数是 _OnRecv 函数返回 false 之后跳转的位置
	*/
	hooker->SetHook((LPVOID)0x10618480, 0x1, _OnRecv, 0);
	hooker->SetHook((LPVOID)0x1061161D, 0x3, _OnComprase, (LPVOID)0x10611602);
	/*
		在这里绑定游戏处理数据包函数(0x10618480函数)
		然后0x10618480函数在上面一行代码已经进行了HOOK
		所以在调用_OnRecv函数指针时,它就会进入我们HOOK
	*/
	InitClassProc(&GameWinSock::_OnRecv, 0x10618480);

}

GameWinSock.h文件的修改:删除 OnRecving函数,原本名为 OnRecv函数改名为Recv,新加 OnRecv函数

#include "pch.h"
#include "GameProc.h"
#include "extern_all.h"

// typedef bool(GameWinSock::* U)(char*, unsigned);

bool _OnComprase(HOOKREFS2) {
	return WinSock->OnRecv((char*)_EDI, _EBP + 1);
}

bool _OnRecv(HOOKREFS2) {
	_EAX = WinSock->RecvPoint;
	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之后当游戏执行到第一个参数的位置的时候跳转的位置
		第四个参数是 _OnRecv 函数返回 false 之后跳转的位置
	*/
	hooker->SetHook((LPVOID)0x10618480, 0x1, _OnRecv, 0);
	hooker->SetHook((LPVOID)0x1061161D, 0x3, _OnComprase, (LPVOID)0x10611602);
	/*
		在这里绑定游戏处理数据包函数(0x10618480函数)
		然后0x10618480函数在上面一行代码已经进行了HOOK
		所以在调用_OnRecv函数指针时,它就会进入我们HOOK
	*/
	InitClassProc(&GameWinSock::_OnRecv, 0x10618480);

}

GameWinSock.cpp文件的修改:删除 OnRecving函数,原本名为 OnRecv函数改名为Recv,新加 OnRecv函数

#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) {
	PDATALOGIN _data = (PDATALOGIN)(buff + 1);
	char* _id = _data->Id;
	_data = (PDATALOGIN)(buff + 1 + _data->lenId - 0x10);
	char* _pass = _data->Pass;

	Client->Onlogin(_id, _pass);

	/* 修改账号密码
	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 OnTips(char*& buff, unsigned& len) {
	int* code = (int*)&buff[1];
	return Client->Tips(code[0]);
}

bool OnSelectRole(char*& buff, unsigned& len) {
	PNS_SELECTROLE p = (PNS_SELECTROLE)buff;
	return Client->OnSelectRole((wchar_t*)(p->buff));
}

bool OnDelRole(char*& buff, unsigned& len) {

	PDATADELROLE p = (PDATADELROLE)buff;
	return Client->OnDelRole((wchar_t*)(p->buff), p->len);



	// 返回值改为false将拦截发送的删除角色数据包
	// 详情看注册 OnDelRole 函数的位置,Init函数
	// return true;
}

bool OnloginOk(char*& buff, unsigned& len) {

	PDATALOGINOK _p = (PDATALOGINOK)&buff[1];
	ROLE_DATA* roleDatas = nullptr;
	if (_p->RoleCount > 0) {
		char* buffStart = buff + 1 + sizeof(DATA_LOGIN_OK);
		WinSock->AnlyBuff(buffStart, buff + len);
		roleDatas = new ROLE_DATA[_p->RoleCount];
		for (int i = 0; i < _p->RoleCount; i++)
		{
			roleDatas[i].byte.Init(buffStart, 0);
			roleDatas[i].index.Init(buffStart, 0);
			roleDatas[i].un1.Init(buffStart, 0);
			roleDatas[i].name.Init(buffStart, 0);
			roleDatas[i].infos.Init(buffStart, 0);
			roleDatas[i].un2.Init(buffStart, 0);
			roleDatas[i].un3.Init(buffStart, 0);
		}
		Client->loginok(roleDatas, _p->RoleCount);
	}
	return true;
}

bool OnStartCreateRole(char*& buff, unsigned& len){
	// 申请进入创建角色界面
	int* code = (int*)&buff[1];
	return Client->OnStartCreateRole(code[0]);
}

bool OnCreateRole(char*& buff, unsigned& len) {
	PNS_CREATEROLE head = (PNS_CREATEROLE)(buff - 3);
	int icount = head->count;
	if (icount < 1)return true;
	char* buffStart = (char*)head + sizeof(NET_SEHD_CREATEROLE_HEAD);
	
#ifdef  Anly
	WinSock->AnlyBuff(buffStart, buff + len, 1);// 翻译解析约定数据
#endif

	EnCode codes[sizeof(CREATE_ROLE_DATAS) / sizeof(EnCode)]{};
	int stDecode = 0;
	while (stDecode < icount) {
		codes[stDecode++] = buffStart;
	}


	/* 
		Client->OnCreateRole(head, (PCREATE_ROLE_DATAS)codes) 数据包传给虚函数
		如果想对发送创建角色数据包做些什么直接继承NetClient重写OnCreateRole函数既可以了
	*/
	return Client->OnCreateRole(head, (PCREATE_ROLE_DATAS)codes);// 返回false屏蔽05开头的数据包,也就是创建角色发送的数据包
}

bool OnSendCustom(char*& buff, unsigned& len) {
	PNET_SEND_HEAD head = (PNET_SEND_HEAD)(buff - 1);
	int icount = head->count;
	if (icount < 1)return true;
	char* buffStart = (char*)head + sizeof(NET_SEND_HEAD);
	if (buffStart[0] != 0x02) {

#ifdef  Anly
		if(icount < MAX_SEND_COUNT)
			anly->SendData(TTYPE::I_DIS, I_SEND_CUSTOM, "SEND_CUSTOM MAX_SEND_COUNT 内存解码器空间不足", 46);

		anly->SendData(TTYPE::I_DIS, I_SEND_CUSTOM, "SEND_CUSTOM 发现异常数据", 25);
#endif
		return true;
	}

#ifdef  Anly
	WinSock->AnlyBuff(buffStart, buff + len, 1);
#endif

	int stDecode = 0;
	EnCode codes[MAX_SEND_COUNT]{};
	while (stDecode < icount) {
		codes[stDecode++] = buffStart;
	}

	/*
		Client->OnSendCustom((PNET_SEND_CHEAD)codes, buff, len); 数据包传给虚函数
		如果想对发送数据的0A开头的据包做些什么直接继承NetClient重写OnSendCustom函数既可以了
	*/
	return Client->OnSendCustom((PNET_SEND_CHEAD)codes, buff, len);

}

bool OnSvrStartCreateRole(char*& buff, unsigned& len) {
	short* _st = (short*)&buff[1];
	wchar_t* _txt = (wchar_t*)&buff[3];
#ifdef  Anly
	CString txt;
	CStringA txtA;
	txt.Format(L"code:%d\r\n%s", _st[0], _txt);
	txtA = txt;
	//AfxMessageBox(txtA);
	anly->SendData(TTYPE::I_DIS, S_CREATEROLE_START, txtA.GetBuffer(), txt.GetAllocLength() + 1);
#endif
	/*
		Client->OnSendCustom((PNET_SEND_CHEAD)codes, buff, len); 数据包传给虚函数
		如果想对0A开头的据包做些什么直接继承NetClient重写OnSendCustom函数既可以了
	*/
	return Client->OnScrStartCreateRole(_st[0], _txt);
}

// 这个函数拦截了游戏的连接
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;
	Client->Init(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::Recv(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;
	SendDealProc[I_DELROLE] = &OnDelRole;
	SendDealProc[I_CREATEROLE_START] = &OnStartCreateRole;
	SendDealProc[I_SEND_CUSTOM] = &OnSendCustom;
	SendDealProc[I_CREATEROLE] = &OnCreateRole;
	SendDealProc[I_SELECT_ROLE] = &OnSelectRole;
	// 注册数据登录失败数据包处理函数
	RecvDealProc[S_TIPS] = &OnTips;
	RecvDealProc[S_LOGINOK] = &OnloginOk;
	RecvDealProc[S_CREATEROLE_START] = &OnSvrStartCreateRole;
}

// 它会生成一个结构体,详情看效果图
void GameWinSock::AnlyBuff(char* start, char* end, char index)
{
#ifdef  Anly
	CStringA txt;
	CStringA tmp;
	CString utmp;
	EnCode _coder;

	GBYTE* _bytecoder;
	GSHORT* _shortcoder;
	GINT* _intcoder;
	GFLOAT* _floatcoder;
	GDOUBLE* _doublecoder;
	GCHAR* _asccoder;
	GUTF16* _utfcoder;
	GINT64* _int64coder;



	while (start < end) {
		_coder.Init(start, index);
		CStringA _opname = data_desc[_coder.index][_coder.op].name;
		// _opname.MakeLower()是变为小写字母,会影响 _opname它的值
		// 所以又写了一边 data_desc[_coder.index][_coder.op].name
		tmp.Format("%s %s;//", data_desc[_coder.index][_coder.op].name, _opname.MakeLower());
		txt = txt + tmp;
		if (_coder.index == 0) {
			switch (_coder.op)
			{
			case 1:
				_shortcoder = (GSHORT*)&_coder;
				tmp.Format("%d\r\n", _shortcoder->value());
				txt = txt + tmp;
				break;
			case 2:
				_intcoder = (GINT*)&_coder;
				tmp.Format("%d\r\n", _intcoder->value());
				txt = txt + tmp;
				break;
			case 4:
				_floatcoder = (GFLOAT*)&_coder;
				tmp.Format("%f\r\n", _floatcoder->value());
				txt = txt + tmp;
				break;
			case 6:
				_bytecoder = (GBYTE*)&_coder;
				tmp.Format("%d\r\n", _bytecoder->value());
				txt = txt + tmp;
				break;
			case 7:
				_utfcoder = (GUTF16*)&_coder;
				utmp.Format(L"[%s]\r\n", _utfcoder->value());
				tmp = utmp;
				txt = txt + tmp;
				break;
			// 5号之前分析的忘记截图了,现在找不到它的数据包了,如果后面再见到05的时候再详细补充说明
			// 之前的分析05就是double类型
			case 5:
				_doublecoder = (GDOUBLE*)&_coder;
				tmp.Format("%lf\r\n", _doublecoder->value());
				txt = txt + tmp;
				break;
			case 8:
			case 3:
				_int64coder = (GINT64*)&_coder;
				tmp.Format("%lld\r\n", _int64coder->value());
				txt = txt + tmp;
				break;
			default:
				break;
			}
		}

		if (_coder.index == 1) {
			switch (_coder.op)
			{
			case 1:
				_shortcoder = (GSHORT*)&_coder;
				tmp.Format("%d\r\n", _shortcoder->value());
				txt = txt + tmp;
				break;
			case 2:
				_intcoder = (GINT*)&_coder;
				tmp.Format("%d\r\n", _intcoder->value());
				txt = txt + tmp;
				break;
			case 4:
				_floatcoder = (GFLOAT*)&_coder;
				tmp.Format("%f\r\n", _floatcoder->value());
				txt = txt + tmp;
				break;
			case 6:
				_asccoder = (GCHAR*)&_coder;
				tmp.Format("%s\r\n", _asccoder->value());
				txt = txt + tmp;
				break;
			case 7:
				_utfcoder = (GUTF16*)&_coder;
				utmp.Format(L"[%s]\r\n", _utfcoder->value());
				tmp = utmp;
				txt = txt + tmp;
				break;
			case 5:
				_doublecoder = (GDOUBLE*)&_coder;
				tmp.Format("%lf\r\n", _doublecoder->value());
				txt = txt + tmp;
				break;
			case 8:
			case 3:
				_int64coder = (GINT64*)&_coder;
				tmp.Format("%lld\r\n", _int64coder->value());
				txt = txt + tmp;
				break;
			default:
				break;
			}
		}
	} 
	anly->SendData(TTYPE::I_DIS, 0, txt.GetBuffer(), txt.GetAllocLength() + 1);
#endif
}

bool GameWinSock::OnRecv(char* buff, unsigned len)
{
	// 解除压缩后的数据
#ifdef  Anly
	anly->SendData(TTYPE::I_RECV, buff[0], buff, len);
#endif
	return RecvDealProc[buff[0]](buff, len);
}


54.网络游戏逆向分析与漏洞攻防-基础数据分析筛选-截取解压后的数据包-LMLPHP

04-17 15:11