实战网页游戏外挂hook技术——攻城掠地(二)

Home / C++ MrLee 2016-1-20 8137

在前面一篇中讲到了HOOK系统的send和recv函数,本篇继续讲解后期的数据包处理,这也是重中之重!
开始之前,我必须要说明一点。要做外挂必须要有丰富的开发经验,要对基本数据很懂。比如位运算,与运算,大端序,小端序……总之,经验越丰富越容易成功!攻城掠地这款游戏是没有对数据包加密的,但是简单的处理过,也并不是明文(肉眼可见通讯)

开发思路

一,首先要知道对方通讯协议,那么最简单的方法就是看人家的源码喽,看不到就用汇编进行调试(这个本人汇编比较菜,找找简单的还行),幸好flash是加载到本地再执行的(swf文件)。网上有很多破解swf的软件,这个度娘一下就知道了我就不多复述了。我花了几个小时破解了该游戏协议,数据包格式是4个字节的int类型,也就是该包的长度。后面就是内容喽,
比如:【0x0,0x0,0x0,0x0A,0x01,0x01,0x010x01,0x01,0x01,0x01,0x010x01,0x01】前面4个字节转成10进制就是10嘛,后面10个0x1就是数据内容。转成可见字符,在Java里面直接new String(byte[]...);,在C++直接看就行了。char*就是字符串了。这里强调一点,在攻城掠地游戏数据包中(游戏通讯是以json格式),json部分是被zlib压缩过的,需要用开源库mz.lib的uncompress来解压,才能变成可见字符,不然压缩过的数据是乱码哦!
以下是我解析数据库的代码,现在的游戏版本也是可以用的,刚刚测试过(我很久没弄了,今天是了为写博客)
void CParseGcldJsonData::ParseData(CDataInputStream &dis)
{
	try
	{
		////数据长度
		int data_length = dis.readInt();//数据包的长度
		if(data_length < 32) return; byte *command = new byte[32];//command dis.read(command,32); CString str_command(command); OutputDebugString(str_command); OutputDebugString("\r\n"); this->token = dis.readInt();//token
		int srcLength = data_length - 36;//减去token和command长度-32-4
		byte *srcData = new byte[srcLength];//就等于content的长度
		dis.read(srcData,srcLength);
		unsigned long dLen = 512000;
		byte *temp = new byte[dLen];//500kb数据
		memset(temp,0,dLen);
		uncompress(temp,&dLen,srcData,srcLength);
		CString content(temp);
		GT_UTF8toANSI(content);
		if(command != NULL)
			delete[] command;
		command = NULL;
		if(temp != NULL)
			delete[] temp;
		temp = NULL;
		if(srcData != NULL)
			delete[] srcData;
		srcData = NULL;
		if(str_command.Left(5) == "world")
			TRACE("\r\nrecv[%s]\r\n",str_command+content);
		if(str_command == ROLE_GET_LIST)
			ParsePlayerList(content);
		else if(str_command == ROLE_GET_INFO)
			ParsePlayerInfo(content);
		else if(str_command == MC_GET_BUILDING_INFO)
			ParseBuildInfo(content);
		else if(str_command == NATION_POWER_REWARD_INFO)
			ParseReward(content);
		else if(str_command == ZEROONLINE_GET_NUM)
			ParseOnlineGiftNumber(content);
		else if(str_command == OPEN_MARKET_PANEL)
			ParseMarket(content);
		else if(str_command == MC_GET_MAIN_CITY_INFO)
			ParseMainCityInfo(content);
		else if(str_command == POLITICS_GETEVENT_INFO)
			ParseEventInfo(content);
		else if(str_command == GET_BUY_BARBARIAN)
			parseManzuShoumaiInfo(content);
		else if(str_command == GET_SHOP_ITEMS || str_command == REFRESH_SHOP_ITEMS)
			ParseItem(content);
		else if(str_command == BUILDING_CD_SPEED_UP)
			ParseSpeedCd(content);
		else if(str_command == GET_EQUIPS_LIST)
			ParseEquips(content);
		else if(str_command == GET_REFRESH_INFO)
			ParseQuenching(content);
		else if(str_command == GET_FORCE_INFO || str_command == SWITCH_FORCE_INFO)
			ParsePowerInfo(content);
		else if(str_command == GENERAL_GET_GENERALSIMPLEINFO)
			ParseGeneralSimpleInfo(content);
		else if(str_command == BATTLE_PERMIT)
			ParseBattlePermit(content);
		else if(str_command == PUSH_BATTLE)
			ParseBattle(content);
		else if(str_command == GET_CITIES)
			ParseWorldScene(content);
		else if(str_command == PUSH_GENBATTLE)
			ParseGeneralBattle(content);
		else if(str_command == WAR_PREPARE)
			ParseBattlePrepare(content);
		else if(str_command == INCENSE_GET_INCENSEINFO)
			ParseIncenseInfo(content);
		else if(str_command == CITY_DETAIL_INFO)
			ParseCityDetailInfo(content);
		else if(str_command == PUSH_CITIES)
			ParseCities(content);
		else if(str_command == WORLD_ASSEMBLE_BATTLE)
			ParseAssembleBattleAll(content);
		else if(str_command == OPEN_DINNER)
			ParseDinner(content);
		else if(str_command == WORLD_AUTO_MOVE)
			ParseAutoMove(content);
		else if(str_command == OPEN_SLAVE_GETINFO)
			ParseGetSlaveInfo(content);
		else if(str_command == TAVERN_GETGENERALS)
			ParseTavern(content);
		else if(str_command == FEAT_LIST_GET_INFO)
			ParseRankInfo(content);
		else if(str_command == FARM_GET_INFO)
			ParseFarmInfo(content);
		//有可能多个数据包组合在一起
		if (dis.available() > 4)
			ParseData(dis);
	}
	catch (CException* e)
	{
		char errorMsg[INFO_SIZE];
		e->GetErrorMessage(errorMsg,INFO_SIZE);
		strcat_s(errorMsg,"P002");
		GT_WriteReleaseLog(errorMsg);
	}
}

因为Flash的语法和Java很相似,我用C++写了一个类似Java的数据流类DataInputStream.h
#pragma once
class CDataInputStream
{
private:
	byte *m_pData;//16进制的数据
	int m_nLen;
	int m_nPos;
public:
	CDataInputStream(char *pData,int nLen);
	virtual ~CDataInputStream(void);
	int readInt();
	void read(byte *buf,int nLen);
	void close();
	int getLength(void);
	int available(void);
};

DataInputStream.cpp
#include "stdafx.h"
#include "DataInputStream.h"

CDataInputStream::CDataInputStream(char *pData,int nLen)
{
	this->m_nLen = nLen;
	this->m_pData = new byte[m_nLen];
	memcpy_s(this->m_pData,m_nLen,pData,nLen);
	this->m_nPos = 0;
}

CDataInputStream::~CDataInputStream(void)
{
	if(this->m_pData != NULL)
		delete[] this->m_pData;
	this->m_pData = NULL;
}
int CDataInputStream::readInt()
{
	try
	{
		if (m_nPos+4 > m_nLen)
		{
			TRACE("ReadInt Memory overflow pos:%d len:%d\r\n",m_nPos,m_nLen);
			return -1;
		}
		int ch1 = m_pData[m_nPos++];
		int ch2 = m_pData[m_nPos++];
		int ch3 = m_pData[m_nPos++];
		int ch4 = m_pData[m_nPos++];
		return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0)); } catch (CException* e) { char errorMsg[1024]; e->GetErrorMessage(errorMsg,1024);
		strcat_s(errorMsg,"D001");
		AfxMessageBox(errorMsg);
	}
	return -1;
}
void CDataInputStream::read(byte *buf,int nLen)
{
	try
	{
		if (m_nPos+nLen > m_nLen)
		{
			TRACE("Read %d Memory overflow pos:%d len:%d\r\n",nLen,m_nPos,m_nLen);
			return;
		}
		memcpy_s(buf,nLen,m_pData+m_nPos,nLen);
		m_nPos += nLen;
	}
	catch (CException* e)
	{
		char errorMsg[1024];
		e->GetErrorMessage(errorMsg,1024);
		strcat_s(errorMsg,"D002");
		AfxMessageBox(errorMsg);
	}
}
void CDataInputStream::close()
{
	this->m_nLen = 0;
	this->m_nPos = 0;
	if(m_pData != NULL)
		delete[] m_pData;
	this->m_pData = NULL;
}
int CDataInputStream::getLength(void)
{
	return m_nLen;
}
int CDataInputStream::available(void)
{
	return this->m_nLen - this->m_nPos;
}

其实就是对char*操作的一个封装。继续讲解ParseData(CDataInputStream &dis)的说明。在这之前,我们先看一段已经解析好的数据包,是Json格式的。该游戏是json通讯的。
[world@getManzuShoumaiInfo{"action":{"state":1,"data":{"manzuInfo":[{"cityId":250,"isOurs":true},{"cityId":251,"isOurs":false,"canShoumai":true,"canFadong":true,"qinMiDu":228},{"cityId":252,"isOurs":false,"canShoumai":true,"canFadong":true,"qinMiDu":301}]}}}]

我们拿这条数据包来举例说明。
首先int data_length = dis.readInt();读取4个字节的数据包长度。前面讲过的。不懂往前看。
接下来就是正式的内容,然后再读取32个字节的命令行,byte *command = new byte[32];//command dis.read(command,32);这个command其实就是world@getManzuShoumaiInfo,再接下来是4个字节的token值,这个貌似没啥用,估计是用来验证游戏的通讯,每次通讯一次会自增1.this->token = dis.readInt();//token 有人到这里估计会问,你怎么知道是读32字符为command,4字节是token值呢?哈哈,我是破解了人家的swf文件,看了人家的通讯源码。
继续往下看,上面已经读取了多少个字节呢?一个32,一个4,那就位移就到了32+4=36了,这里再次举例说明数据包的格式:
假如总数据包是100的话,4个字节头是内容的长度,剩余96就是内容了,然后32字节是command,4字节是token值,也就是剩余60个字节就是Json数据了,这60字节是需要用uncompress解压。
int srcLength = data_length - 36;//减去token和command长度-32-4
		byte *srcData = new byte[srcLength];//就等于content的长度
		dis.read(srcData,srcLength);
		unsigned long dLen = 512000;
		byte *temp = new byte[dLen];//500kb数据
		memset(temp,0,dLen);
		uncompress(temp,&dLen,srcData,srcLength);

我这里用500KB的内存空间存储被解压的json数据,因为人家通讯数据包不可能超过500的。这个temp就是json数据啦,是UTF-8格式的,要转成可见的还需要转成ascii
static void GT_UTF8toANSI(CString &strUTF8)  
{  
	//获取转换为多字节后需要的缓冲区大小,创建多字节缓冲区
	UINT nLen = MultiByteToWideChar(CP_UTF8,NULL,strUTF8,-1,NULL,NULL);
	WCHAR *wszBuffer = new WCHAR[nLen+1];
	nLen = MultiByteToWideChar(CP_UTF8,NULL,strUTF8,-1,wszBuffer,nLen);
	wszBuffer[nLen] = 0;
	nLen = WideCharToMultiByte(CP_ACP,NULL,wszBuffer,-1,NULL,NULL,NULL,NULL);
	CHAR *szBuffer = new CHAR[nLen+1];
	nLen = WideCharToMultiByte(CP_ACP,NULL,wszBuffer,-1,szBuffer,nLen,NULL,NULL);
	szBuffer[nLen] = 0;  
	strUTF8 = szBuffer;
	//清理内存
	delete []szBuffer;
	delete []wszBuffer;
}

然后content就是可见的json数据包了。好了,可能有些地方大家不明白,可以留言或者加我Q。
下面截的图就是破译的内容:

20160119222151

本文链接:https://www.it72.com/7712.htm

推荐阅读
最新回复 (5)
  • wssssss 2016-1-20
    引用 2
    666666太6了兄弟,有些地方不是很清楚请教你下可以么? 我是新手 。。。。你Q的信息在哪找?
    要不你留个Q吧
  • cyc8127c 2016-1-22
    引用 3
    从哪里看出攻城掠地的通讯是以JSON格式, 然后 这边JSON是怎么获取的呢? 我打开 ,然后上面图是编译网页 .js文件? 望指教
  • osoyavobe 2016-1-23
    引用 4
    仔细看文章,我是先获取对方的二进制数据,然后再对二进制进行解密。得到json
  • hupengpeng 2017-9-24
    引用 5
    老哥请教下,我这边也反编译了他的swf也看到了 readBytes(contentBytes, 0, this._dataLength - 4 - 32); 他这边的协议。用winpcap拦截了也能正确解密, 但是你的发送数据是什么思路呢?我这边发包就是没有收到相应。请留个联系方式请教一下
  • MrLee 2017-9-24
    引用 6
    发送复制他的SOCKET描述,用自己创建的SOCKET发送。关键函数WSADuplicateSocket,你多研究下。
返回