60.网络游戏逆向分析与漏洞攻防-利用数据包构建角色信息-根据数据包内容判断数据包作用

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

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

现在的代码都是依据数据包来写的,如果看不懂代码,就说明没看懂数据包

内容参考于: 易道云信息技术研究院VIP课

上一个内容:59.角色上线过程数据逻辑分析

从本章开始就开始分析人物数据了,目标是通过网络数据包分析不去逆向不去读取内存,然后构建出角色信息,通过上一个内容,把09数据包屏蔽,客户端还是可以正常读取角色信息,所以接下来就分析屏蔽09数据包之后的那些数据包,通过这些数据包我们把角色信息给构建出来,原则上游戏可以搞的事我们也可以搞,然后如果数据包过于复杂根据数据约定解不出来,还是要去逆向

然后进入游戏

从下往上分析

然后下图红框两个大的数据包先不解读,现在没感觉解不出来

然后就从10开头数据包下手

10数据包如下图前12字节都是一样的,后面是一个字符串

然后这个01 00现在倾向于它是数据参数个数

下图红框是数据参数

然后现在游戏处于一种半进不进的状态,这些数据包肯定跟客户端初始化有关系,与角色有关系,然后接下来重新选择角色看看,下图红框位置的数据会不会变

在返回角色选择时,发现无法进入角色选择界面了,这说明09数据包还是影响返回角色选择的

所以做测试10数据包变不变之前,要把09数据包放掉,如下图把if判断注释掉就可以了

然后现在的10数据包

同样一个角色重新登录之后它就变了,所以这些字节就是跟角色有关,可以把D4 44 00 01 12 32 DC 03 猜测为它与角色有关

现在就可以这样拆分 10 00是数据头,D4 44 00 01 12 32 DC 03 代表了角色,01 00代表数据参数个数,剩下的就是数据参数,这个意思是角色它肯定有很多属性,根据下图的数据包来说,这个数据包代表了人物第72个属性

第72个属性是21063,它应该就这样的一个意思

然后现在周围有一个r角色

接下来选中它,看看选中的数据包是什么,选中的数据包是07,但是现在看不懂什么意思

然后自己选中自己,然后就看到,07数据包里我们看不懂的东西是角色id

然后现在确认10的数据是角色信息的更新,然后看看附近角色的数据更新是不是也是10,然后首先选中附近角色,看看它的id,它的id现在是 F8 44 00 01 51 13 DB 03

然后我想她发起决斗打它一下血量会变,然后现在变成了1794

接下来通过内容搜索附近角色id,发现2D开头的数据包会更新附近角色信息,所以除了10还有2d会更新角色信息

整理说明:

下图意思是把角色id为 D4 44 00 01 12 32 DC 03 的第AB(十进制是171)个数据改成一个空字符(为什么是空字符?因为下图中AB 00 01这个01后面有5个0,这样的数据不符合内存对齐,所以只能是01 00 00 00然后还剩一个0是字符串结尾符,这样的就是一个空字符,所以它是一个空字符),然后角色id是一个long long类型

然后通过下图知道了 29 是血量

然后把我们的血量进行一个修改,验证29是不是血量,现在血量是1531

然后把加血量的装备下掉,然后变成了1180

然后搜索1180,可以百分百确认29代表的是血量了,然后还有一些看不懂的数据,这些看不懂的先不管

有了这个概念之后,再来看28数据包,然后发现它的数据包很大,全部搞出来有点费力,所以接下来对它逆向分析,游戏既然可以用,它肯定会有一个表,就是04代表什么类型05代表什么类型,这样的一个表,逆向分析就是去找它这个表,因为现在知道它的一个套路了,就差这关键的信息了,找到这张表关键信息也就是知道了

然后打开x96dbg

来到下图红框位置0x1061157E,为什么是下图红框位置看53.逆向分析寻找消息数据解压缩过程,这个位置是决定数据包是否解压的位置,ecx值就是数据包头

然后下一个条件断点

然后返回到角色选择界面重新进入游戏,来到断点

然后按f8,来到下图蓝框位置时,在红框位置打个断点,然后按f9,下图蓝框位置是我们HOOK点

然后来到下图红框位置

再按F8,来到28数据处理函数

它的参数有一个长度,一个指针,一个我们的数据包

然后往下滑可以看到一堆st寄存器的使用,st寄存器是用来存放浮点数的

然后通过上方我们手动的分析28数据包,可以看到开头确实是有多个浮点数坐标,所以直接把断点打到下图位置,因为我们现在要找后面以数据解析约定的数据,它既然是在处理浮点数,这就说明是处理前面的,并不是我们要找的,所以在下图红框位置打断点不影响什么

然后一路f8 会来到下图红框位置,这个位置是处理完前面浮点数之后第一次调用函数的位置

函数的参数这里从数据包3D位置取了一个值,这个值可能是数据参数个数

然后它的栈情况,一个长度一个不知道什么的字符串

然后鼠标双击进入函数,先通过静态的方式过一遍代码

过了一遍之后并没有发现[XX*4+XXXX]这样的写法(这样的写法是符合读取表的写法),所以接下来直接对数据包做访问断点,给1B下一个1字节的访问断点

然后来到下图红框位置,发现它变成了0,这说明在访问之前有人对它进行了修改

然后它的附近有一个像是表的东西,如下图红框

然后也看不出是个什么东西来

然后现在先结束,现在东西已经很多了,然后下一次再下一个写断点试一下

总结:

通过分析10数据包得出一个id,然后通过选择自己确认了10数据包里就是角色id,然后通过修改血量也确定了29,也就是第29个位置是血量,然后通过这样一个新思路去尝试解析28号数据,然后发现28号数据可以这样分析出来,但是它有很多东西,人工分析不好,所以又去逆向找游戏的类型表(29代表了第29位置是血量,类型表的意思是,通过读取表的第29位置就能确定是血量,以及数据类型,这样的一个意思,以后为了方便都会称为类型表)

本次代码就修改了一行,没有提交百度网盘,只提交了码云

GameWinSock.cpp文件的修改:修改了 OnSend函数

#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, buff[0]);
		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, buff[0], 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, buff[0], 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);

}

/*
 OnSverrNotice函数处理的数据包格式如下
	1E 06 00 
	06 11 00 00 00 70 6C 61 79 5F 70 6F 69 6E 74 5F 73 6F 75 6E 64 00 
	06 01 00 00 00 00 
	04 2C 92 87 C5 
	04 FA 03 BF 42 
	04 33 14 BD 45 
	02 00 00 00 00 
	1E 06 00 是 PNR_NOTICE_HEAD
	06 11 00 00 00 70 6C 61 79 5F 70 6F 69 6E 74 5F 73 6F 75 6E 64 00是一个EnCode
	06 01 00 00 00 00是一个EnCode
	04 2C 92 87 C5是一个EnCode
	04 FA 03 BF 42是一个EnCode
	04 33 14 BD 45是一个EnCode
	02 00 00 00 00是一个EnCode
*/
bool OnSverrNotice(char*& buff, unsigned& len) {
	PNR_NOTICE_HEAD head = (PNR_NOTICE_HEAD)(buff - 1);
	int icount = head->count;
	char* buffStart = (char*)head + sizeof(NR_NOTICE_HEAD);
	if (icount < 1) {
		return true;
	}
	if (icount > MAX_RECV_COUNT) {
#ifdef  Anly
		anly->SendData(TTYPE::I_DIS, S_NOTICE, "S_NOTICE 解码器内存不足", 24);
#endif
		return true;
	}
#ifdef  Anly
	WinSock->AnlyBuff(buffStart, buff + len, buff[0], 1);
#endif
	int stDecode = 0;
	EnCode codes[MAX_RECV_COUNT]{};
	while (stDecode < icount) {
		codes[stDecode++] = buffStart;
	}
	return Client->OnSvrNotice((PNET_SEND_CHEAD)codes, icount, 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)
{
	// if (buff[0] == 0x09)return true;
	
	/*
		这里就可以监控游戏发送的数据了
	*/

#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;
	RecvDealProc[S_NOTICE] = &OnSverrNotice;
	RecvDealProc[S_NOTICE_COM] = &OnSverrNotice;
}

// 它会生成一个结构体,详情看效果图
void GameWinSock::AnlyBuff(char* start, char* end, int MsgId, 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, MsgId, 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);
}


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值