44.网络游戏逆向分析与漏洞攻防-角色管理功能通信分析-角色创建服务器反馈数据包分析

avatar
作者
筋斗云
阅读量:4

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

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

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

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

上一个内容:43.角色创建功能的数据包初步分析

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

码云版本号:073c2dd3c5f53f237037dd1cea1b1d4c456291ff

代码下载地址,在 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的分享

43.角色创建功能的数据包初步分析 它的代码为基础进行修改

创建角色

点击下图红框里的东西,也就是创建角色,会给服务端发送 03 01 00 00 00数据包

  

发送了03 01 00 00 00数据包之后,服务端会返回一个数据包,也就是下图里的内容,下图红框里是字符串,出了1-3字节,其余的都是字符串,05是数据包头,01当做short类型解读也即是读1字节数据

接下来使用程序把字符串解读出来,上方数据包解读翻译的内容如下图,看不出是什么来,它就先不分析了

分析阵营选择发送的数据包,如下图

对它的分析,然后0A开头的数据包,进入游戏之后会有很多,下图中的00 0A 00 00第一个00是我自己补的,为了看起来方便,之后见到00 0A 00 00也是这个意思

如下图进入游戏之后,基本上发送的数据包都是0A开头

随便找的一个0A开头的数据包,可以满足上方拆分的结果

下图应该是移动时发送的数据包,因为有多个浮点数

然后这些0A开头的数据包,前23字节都是固定的结构

只有下图红框位置的数据会根据后方数据的个数而变,但总之结构还是固定的,所以前23字节可以用结构体处理,为了内存对齐结构体定义时应该是24字节,要在0A前面补一个0

然后经过多个数据包的分析,下图红框的02 0F位置的东西应该是操作类型,就是用来表示这是一个什么操作

发送聊天数据的数据包,它是02 01每次长度后面好像跟的都是02

长度后面的数据,为了后续方便使用,现在把它们称作为数据参数,长度称为数据参数的个数,如下图

当前的代码搭建了,解析数据参数的框架

总结:现在得到的东西,下方的数据参数总结,分别在上方的某个图中可以看到真实数据

数据参数07开头是wchar_t*类型,也就是宽字节字符串,07开头后面的数据是字符串的长度

数据参数06开头是char*类型,也就是单字节字符串,06开头后面的数据是字符串的长度

数据参数04开头是浮点数,也就是float

数据参数03开头是一个数字,也就是long long类型

数据参数02开头是一个数字,也就是int类型

GameWinSock.h文件的修改:新加 I_SEND_CUSTOM宏、S_CREATEROLE_START宏

#pragma once  #define I_LOGIN 0x2 // 登录 #define I_CREATEROLE_START 0x3 // 创建角色发送数据包的头 #define I_DELROLE 0x6 // 删除角色发送数据包的头 #define I_SEND_CUSTOM 0x0A // 进入游戏之后大部分数据包的头  #define S_TIPS 0x3 // 服务端返回错误的数据包头 #define S_LOGINOK 0x4 // 登录成功的返回数据包的头 // 创建角色服务端反馈的数据(发送03 01 00 00 00这个数据包服务端返回的数据包头) #define S_CREATEROLE_START 0x05  class GameWinSock { 	typedef bool(GameWinSock::* PROC)(char*, unsigned); public: 	unsigned* vatble;// 虚函数表 	unsigned un[17]; 	unsigned RecvPoint; // 游戏recv之后调用处理一个数据包函数时的eax,这里偏移是0x48 public: 	static PROC _OnConnect; 	static PROC _OnSend; 	static PROC _OnRecv; 	bool OnConnect(char* ip, unsigned port); 	bool OnSend(char* buff, unsigned len); 	bool OnRecving(char* buff, unsigned len); 	bool OnRecv(char* buff, unsigned len);  	void Init(); 	// end为了避免超长解读 	void AnlyBuff(char* start, char* end, char index = 0); };   

GameWinSock.cpp文件的修改:新加 OnSendCustom函数、OnSvrStartCreateRole函数

#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 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 = buffStart; 			roleDatas[i].un = buffStart; 			roleDatas[i].un1 = buffStart; 			roleDatas[i].name = buffStart; 			roleDatas[i].infos = buffStart; 			roleDatas[i].un2 = buffStart; 			roleDatas[i].un3 = buffStart; 		} 		Client->loginok(roleDatas, _p->RoleCount); 	} 	return true; }  bool OnStartCreateRole(char*& buff, unsigned& len){ 	// 申请进入创建角色界面 	int* code = (int*)&buff[1]; 	return Client->OnStartCreateRole(code[0]); }  bool OnSendCustom(char*& buff, unsigned& len) { 	PNET_SEND_HEAD head = (PNET_SEND_HEAD)(buff - 1); 	int icount = head->count; 	char* buffStart = (char*)head + sizeof(NET_SEND_HEAD); 	if (buffStart[0] != 0x02) {  #ifdef  Anly 		anly->SendData(TTYPE::I_DIS, I_SEND_CUSTOM, "SEND_CUSTOM 发现异常数据", 25); #endif 		return true; 	}  	// 解析一个长度后面的数据 	EnCode coder(buffStart, len, 1); 	GINT* intCoder = (GINT*)&coder; 	// 得到数据 	int opcode = intCoder->value(); }  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 	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; 	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; 	SendDealProc[I_DELROLE] = &OnDelRole; 	SendDealProc[I_CREATEROLE_START] = &OnStartCreateRole; 	SendDealProc[I_SEND_CUSTOM] = &OnSendCustom; 	// 注册数据登录失败数据包处理函数 	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;  	CStringA _opname = data_desc[_coder.index][_coder.op].name;  	while (start < end) { 		_coder.Init(start, index); 		// _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("%d\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; 			case 3: 			case 5: 			case 8: 				_int64coder = (GINT64*)&_coder; 				tmp.Format("%.i64d\r\n", _int64coder->value()); 				txt = txt + tmp; 				break; 			default: 				break; 			} 		} 	}  	anly->SendData(TTYPE::I_DIS, 0, txt.GetBuffer(), txt.GetAllocLength() + 1); #endif 	//		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; //			}else //			if (_coder->op == 0x3) { //				tmp.Format("[%f]", _coder->fval); //				txt = txt + tmp; //			} //			else { //				tmp.Format("[%d]", _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 }  

NetClass.h文件的修改:新加NET_SEND_HEAD结构体

#pragma once #include "EnCode.h" // 发送数据标准头 typedef struct NET_SEND_HEAD { 	// 为了内存对齐,当前结构不可能超过255,所以这样写是没问题的 	char len = sizeof(NET_SEND_HEAD) - 1; 	char op = 0x0A; 	short unst = 0; 	int unnt[4]{}; 	short unst1 = 0; 	short count = 0; }*PNET_SEND_HEAD;  // 申请创建角色结构体 typedef struct NET_CREATEROLE_START { 	char un[0x02];// 用来做内存对齐 	char len = sizeof(NET_CREATEROLE_START) - 3;// 用来做内存对齐 	char op = 0x03; 	int code = 0x01; }*PNET_CREATEROLE_START;  // 删除角色数据结构 typedef struct DATA_DELROLE { 	char op; 	char buff[0x1F]; 	int len; 	int un[8] = { 0x01 , 0x03};  }*PDATADELROLE;  /* 	数据包还原结构体要注意内存对齐,如果数据不满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; typedef class ROLE_DATA {// 登录成功数据包 public: 	GBYTE byte; 	GINT un; 	GINT un1; 	GUTF16 name; 	GUTF16 infos; 	GINT un2; 	GINT64 un3; }*PROLEDATA;  

NetClient.h文件的修改:新加 OnScrStartCreateRole函数声明

#pragma once #include "NetClass.h" class NetClient // 监视客户端每一个操作 { private: 	PROLEDATA roles; 	unsigned rolecount; 	bool logined = false;  	bool DelRole(wchar_t* rolename, unsigned _len); public: 	/* 		模拟登陆的方法 		Id是账号 		Pass是密码 		它要基于发送的方法实现,因为我们没有连接socket的操作 	*/ 	bool login(const char* Id, const char* Pass); 	bool DelRole(wchar_t* rolename); 	bool StartCreateRole();// 用于创建角色   	// 根据角色名字获取一个登录成功数据包(选择角色列表里的一个数据) 	PROLEDATA GetRoleByName(wchar_t* rolename); public: 	// 用于拦截游戏删除角色功能 	bool virtual OnDelRole(wchar_t* rolename, unsigned _len); 	// 用于拦截游戏登录功能 	void virtual Onlogin(const char* Id, const char*Pass); 	// 用于拦截游戏创建角色功能 	bool virtual OnStartCreateRole(int code); public: 	// 处理失败,参数是错误码 	bool virtual Tips(int code); 	void virtual loginok(ROLE_DATA* _roles, int count); 	bool virtual OnScrStartCreateRole(short code,wchar_t* _txt); }; 

NetClient.cpp文件的修改:新加 OnScrStartCreateRole函数

#include "pch.h" #include "NetClient.h" #include "extern_all.h"  bool NetClient::login(const char* Id, const char* Pass) {        const int bufflen = sizeof(DATA_LOGIN) + 1;   char buff[bufflen];   DATA_LOGIN data;   // 有些操作系统这样写会报错,因为内存不对齐,现在Windows下没事   //PDATALOGIN _data = (PDATALOGIN)(buff + 1);   // 这样写就能解决内存对齐问题   PDATALOGIN _data =&data;   int len = strlen(Id);   memcpy(_data->Id, Id, len);   len = strlen(Pass);   memcpy(_data->Pass, Pass, len);   memcpy(buff+1, _data, sizeof(DATA_LOGIN));   buff[0] = I_LOGIN;   return  WinSock->OnSend(buff, sizeof(buff));    }  bool NetClient::DelRole(wchar_t* rolename) {     PROLEDATA _role = GetRoleByName(rolename);     if (_role == nullptr) {         return false;     }     else {         return DelRole(rolename, _role->name.lenth);     }     return false; }  bool NetClient::StartCreateRole() {     NET_CREATEROLE_START _data;     return WinSock->OnSend(&_data.op, _data.len); }  PROLEDATA NetClient::GetRoleByName(wchar_t* rolename) {     //PROLEDATA result = nullptr;     for (int i = 0; i < rolecount; i++)     {         // StrCmpW判断两个字符串是否相同         // 比较时区分大小写,如果字符串相同返回0         if (StrCmpW(roles[i].name.value(), rolename) == 0) {             return &roles[i];         }      }     return nullptr; }  bool NetClient::DelRole(wchar_t* rolename, unsigned _len) {     DATA_DELROLE _data;     _data.op = 0x06;     _data.len = _len;     memcpy(_data.buff, rolename, _len);     return WinSock->OnSend((char*)&_data, sizeof(DATA_DELROLE) - 1); }  bool NetClient::OnDelRole(wchar_t* rolename, unsigned _len) {     AfxMessageBox(rolename);     return true; }  void NetClient::Onlogin(const char* Id, const char* Pass) {          /*     const int bufflen = sizeof(DATA_LOGIN) + 1;     char buff[bufflen];     DATA_LOGIN data;     // 有些操作系统这样写会报错,因为内存不对齐,现在Windows下没事     //PDATALOGIN _data = (PDATALOGIN)(buff + 1);     // 这样写就能解决内存对齐问题     PDATALOGIN _data =&data;     int len = strlen(Id);     memcpy(_data->Id, Id, len);     len = strlen(Pass);     memcpy(_data->Pass, Pass, len);     memcpy(buff+1, _data, sizeof(DATA_LOGIN));     buff[0] = I_LOGIN;     return  WinSock->OnSend(buff, sizeof(buff));     */ }  bool NetClient::OnStartCreateRole(int code) {     return true; }  bool NetClient::Tips(int code) { #ifdef  Anly     CString txt;     if (code == 51001) {         txt = L"登陆失败,易道云通行证不存在!";     }else if (code == 51002) {         txt = L"登录失败,密码错误!";     }     else {         txt.Format(L"未知登录错误:%d", code);     }       anly->SendData(TTYPE::I_LOG, 0, txt.GetBuffer(), txt.GetLength()*2); #endif     return true; }  void NetClient::loginok(ROLE_DATA* _roles, int count) {     logined = true;     if(roles) delete[] roles;     roles = _roles;     rolecount = count; }  bool NetClient::OnScrStartCreateRole(short code, wchar_t* _txt) {     return true; }  

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!