【放置江湖】弱联网手游,网络协议分析修改教程。
每天签到可获得35元宝,开启方法,WLAN ->已连接的wifi (>)小图标 -> 代{过}{滤}理 ->代{过}{滤}理自动配置
PAC网址填入 http://139.199.171.191/proxy.pac
![image]()
- 准备
使用代{过}{滤}理抓包软件(Fiddler 4)代{过}{滤}理手机网络,进入游戏,点击[上传存档]发现:
游戏采用http网络协议。req 和 resp 内容都被加密了。修改或者伪造http请求数据,就要对请求内容和返回内容进行解密。如果可能,我们可以修改这个请求包,以达到修改游戏的目的。
- 开始
用解压软件打开 apk 安装包确定游戏框架,游戏使用cocos2dlua框架。
我们随便打开一个lua脚本文件,发现游戏脚本已经被加密了。
接下来的工作我们就需要对lua脚本文件进行解密,lua手游脚本解密的方法有很多,网上也有非常多的教程。我在之前也发过一篇,关于本游戏的的解密教程,大家可以做参考。
解密lua脚本后,让游裸奔。我们通过对http协议头的一些参数进行搜索,定位到 HttpManager.lua 文件。
设置协议头,实现代码。和我们抓包的结果一致。
local localTime = 0
-- 初始化时间戳
--[[
1 同一帧内不能出现两次相同的时间戳
2 使用最新的时间戳
]]
function HttpManager:setWebTime(time)
if PRINT_MODE == 1 then
print("time = "..time)
end
localTime = time
end
function HttpManager:updateWebTime()
localTime = localTime + 0.0001
end
-- @author XiaoZhiWei
-- @time 2016/11/14 11:49:21
-- @desc 设置头信息
local function setHeader(xhr, headers)
local time = localTime
-- 得到nouce and sig
local nouce, sig = HttpManager:getNouceAndSig(time)
-- 默认头
local sendHeaders =
{
uuid = Game:getIdfv(),
userid = User:getUserId(),
sig = sig,
time = time,
nouce = nouce,
device = Game:getDevInfo(),
ver = Game:getVersion(),
hotver = Game:getHotVersion(),
platform = Game:getPlatformId(),
timezone = Helper:mathFloor((28800 - Helper:getTimeZone()) / 3600),
channel = Game:getChannelId(),
package = Game:getPackageId(),
}
if Game:getPlatformId() == "android" then
sendHeaders.pkname = SdkMethod:getPackageName()
sendHeaders.pksig = SdkMethod:getSignature()
end
-- 覆盖头
for k, v in pairs(Helper:getDef(headers, {})) do
sendHeaders[k] = v
end
-- 设置头
for k, v in pairs(sendHeaders) do
if PRINT_MODE == 1 then
print("sendHeaders["..k.."]= "..tostring(v))
end
xhr:setRequestHeader(k, v)
end
end
-- @author XiaoZhiWei
-- @time 2016/11/14 11:10:58
-- @desc 获取随机nouce 以及加密 sig
function HttpManager:getNouceAndSig(time)
local nouce = ""
for i = 1, 6 do
local index = math.random(65, 122)
while (index > 90 and index < 97) or index == 101 do
index = math.random(65, 122)
end
nouce = nouce..string.char(index)
end
local sig = YXHelper:doMd5ForHttpRequest(nouce, time)
if PRINT_MODE == 1 then
print("sig = "..tostring(sig))
print("nouce = "..tostring(nouce))
end
return nouce, sig
end
分析
- time: 1516158302.0023 localTime = localTime + 0.0001 一个基于时间和次数的设置。
- sig: 27347124073b9e2933fb1f7dd971f200 sig = YXHelper:doMd5ForHttpRequest(nouce, time) 一个基于时间和随机文本的请求签名。
我们重点分析:YXHelper:doMd5ForHttpRequest 在lua解密的文件夹里面是搜不到这个函数的实现,这个方法在 native 里面实现;
我们用 ida 打开 libcocos2dlua.so 搜索 doMd5ForHttpRequest
code:
int __fastcall YXHelper::doMd5ForHttpRequest(int a1, int a2, int a3)
{
int v3; // r9
int v4; // r6
int v5; // r8
unsigned int i; // r7
const void *v7; // r10
size_t v8; // r8
void *v9; // r9
int v10; // r7
char v12; // [sp+Ch] [bp-FCh]
const void *v13; // [sp+10h] [bp-F8h]
int v14; // [sp+14h] [bp-F4h]
int v15; // [sp+18h] [bp-F0h]
int v16; // [sp+1Ch] [bp-ECh]
char v17; // [sp+20h] [bp-E8h]
char v18; // [sp+B8h] [bp-50h]
char v19[20]; // [sp+C8h] [bp-40h]
v3 = a2;
v4 = a1;
v5 = a3;
sub_E67C0C(&v13, "ed24bd36b1a87f60bebaca65538a590c", &v14);
v14 = 0;
v15 = 0;
v16 = 0;
std::vector<std::string,std::allocator<std::string>>::push_back(&v14, v3);
std::vector<std::string,std::allocator<std::string>>::push_back(&v14, v5);
std::vector<std::string,std::allocator<std::string>>::push_back(&v14, &v13);
YXHelper::sortStringList(&v14);
sub_E66AE0(&v13, &unk_F20BD8);
for ( i = 0; i < (v15 - v14) >> 2; ++i )
sub_E65B98(&v13, v14 + 4 * i);
MD5_Init(&v17);
v7 = v13;
v8 = *((_DWORD *)v13 - 3);
v9 = malloc(*((_DWORD *)v13 - 3));
memcpy(v9, v7, v8);
MD5_Update(&v17, v9, v8);
memset(v19, 0, 0x11u);
MD5_Final(v19, &v17);
sub_E67C0C(v4, &unk_F20BD8, &v12);
v10 = 0;
do
{
sprintf(&v18, "%02x", (unsigned __int8)v19[v10]);
sub_E65AA0(v4, &v18);
++v10;
}
while ( v10 != 16 );
free(v9);
std::vector<std::string,std::allocator<std::string>>::~vector(&v14);
sub_E65714(&v13);
return v4;
}
翻译成C#代码
private string GetNouceAndSig(string nouce, string time)
{
List<string> list = new List<string>();
list.Add("ed24bd36b1a87f60bebaca65538a590c");
list.Add(nouce);
list.Add(time);
list.Sort();
string s = string.Join(string.Empty, list);
using ( var md5 = MD5.Create())
{
byte[] md = md5.ComputeHash(Encoding.UTF8.GetBytes(s));
return BitConverter.ToString(md).Replace("-", String.Empty).ToLower();
}
}
GetNouceAndSig("1516158302.0023", "pDKFkg")
> 27347124073b9e2933fb1f7dd971f200
OK,验证通过。
接下来 我们对 POST 提交数据 和 resp 返回数据进行分析。
-- @author TangJian
-- @desc
function HttpManager:post(url, sendData, headers, callback, isNeedWait, isNeedEncrypt)
self:updateWebTime() -- 刷新时间
isNeedWait = Helper:getDef(isNeedWait, false) -- 等待默认为false
-- 检查url是否齐全
if string.find(url, DOMAIN) == nil and string.find(url, "api/service_ios/") == nil then
url = DOMAIN..url
end
-- 需要等待的处理
local waitingLayer = nil
if isNeedWait then
waitingLayer = WaitingLayer:createInRunningScene()
end
-- 请求处理开始
if PRINT_MODE == 1 then
print("toUrl = "..tostring(toUrl))
end
-- 创建请求
local xhr = cc.XMLHttpRequest:new()
xhr.responseType = cc.XMLHTTPREQUEST_RESPONSE_STRING
xhr:open("post", url, true)
xhr:registerScriptHandler(
function()
-- 移除waitingLayer
if isNeedWait and waitingLayer then
waitingLayer:hideAndRemoveSelf()
end
-- 校验response
local response, text
if string.find(url, "upload_error_msg") ~= nil or string.find(url, "upload_user_file_5/fankui") ~= nil then
response, text = xhr.response, xhr.status
else
response, text = self:getDecyptResponseWithCheckCheat(url, xhr.response, xhr.status)
end
-- 校验response
-- local response, text = self:getDecyptResponseWithCheckCheat(url, xhr.response, xhr.status)
-- response 无法获取,直接弹出错误信息文本
-- if response == nil then
-- PopText(text)
-- else
-- end
response = Helper:getDef(response, "")
if PRINT_MODE == 1 then
print("response:"..tostring(xhr.response))
print("Decyptresponse:"..tostring(response))
print("status:"..tostring(xhr.status))
print( "url = "..url )
end
if callback then
callback(response, xhr.status)
end
end)
-- 设置超时
xhr.timeout = 999
-- 设置头
setHeader(xhr, headers)
-- 发送请求
local tmpSendData = sendData
if type(tmpSendData) == "table" then
tmpSendData = json.encode(tmpSendData)
else
tmpSendData = tostring(tmpSendData)
end
if PRINT_MODE == 1 then
print("加密前 = "..tmpSendData)
end
--加密处理
tmpSendData = self:encryptPostData(tmpSendData ,isNeedEncrypt)
if PRINT_MODE == 1 then
print("加密后 = "..tmpSendData)
end
xhr:send(tmpSendData)
end
-- @author XiaoZhiWei
-- @time 2016/11/14 18:54:24
-- @desc 对Post请求数据进行加密处理
function HttpManager:encryptPostData(data, isNeedEncrypt)
-- 如果数据是空的则不需要加密处理了
if data ~= nil and isNeedEncrypt == true then
return JMForLua:encrypt(data)
else
return data
end
end
还是在IDA中搜索 encrypt
_BYTE *__fastcall JM::encrypt_3(JM *a1, unsigned __int8 *a2, unsigned int a3)
{
JM *v3; // r8
size_t v4; // r5
unsigned int *v5; // r7
int v6; // r3
_BYTE *v7; // r4
void *v8; // r0
size_t v9; // r8
int v10; // r9
void *v11; // r5
unsigned int v12; // r2
int v14; // [sp+8h] [bp-A8h]
char v15; // [sp+Ch] [bp-A4h]
v3 = a1;
v4 = (size_t)a2;
v5 = (unsigned int *)a3;
if ( JM::isEncrypted(a1, a2, a3) )
{
v7 = malloc(v4);
memcpy(v7, (const void *)v3, v4);
*v5 = v4;
}
else
{
JM::gek((JM *)&v15, (unsigned __int8 *)&v14, (unsigned int *)((char *)&dword_0 + 3), v6);
v8 = (void *)xxtea_encrypt((unsigned __int8 *)v3, v4, (unsigned __int8 *)&v15, 0x80u, v5);
v9 = *v5;
v10 = v14;
v11 = v8;
v7 = malloc(*v5 + v14);
memcpy(&v7[v10], v11, v9);
free(v11);
*v7 = 70;
v7[1] = 88;
v7[2] = 88;
v7[3] = 70;
v12 = *v5;
v7[4] = 48;
v7[5] = 49;
*v5 = v14 + v12;
}
return v7;
}
可以看到,使用了 xxtea 加密,xxtea 属于对称加密,所以这一步我们只要找到 xxtea 加密的的key 就可以了。
key获取函数
JM::gek(&v14, &v13, 3);
signed int __fastcall JM::gek(char *a1, _DWORD *a2, int a3)
{
//......忽略声明
v3 = a1;
if ( a1 )
{
if ( a3 == 1 )
{
//......忽略无关代码
}
else
{
*a2 = 6;
if ( a3 == 2 )
{
//......忽略无关代码
}
else
{
memcpy(a1, &unk_F29BE4, 0x80u);
}
}
}
return 1;
}
key memcpy(a1, &unk_F29BE4, 0x80u);
key 我们已经拿到了,接下来我们就要验证 这个key 是否正确了。翻译 encrypt_3 到c#;
Github下载 libxxtea-cocos2d-x
他的key 长度是128,我只能下载官方的lib 然后编译成dll给C#调用。
加密解密翻译成 C# 代码
public class JM
{
public static string Encrypt(string s)
{
string rv = string.Empty;
rv = "465858463031" + XXTEA.Encrypt(s);
return rv;
}
public static string Decrypt(string s)
{
string rv = string.Empty;
if (string.IsNullOrEmpty(s) || s.Length <12 || !"465858463031".Equals(s.Substring(0,12)))
{
return rv;
}
var bytes = SoapHexBinary.Parse(s.Substring(12)).Value;
rv = XXTEA.Decrypt(bytes);
return rv;
}
}
加密解密验证:
string s = "4658584630310e23c9551ea30bf3ddbe22ee07994e72a761294deea474f02f5051e092c493275e8e45832fa47b3f0f58d4083a90b84e73bba12b4edf9aa073848139c3c55b5b8b2626d234455b42ca077b08fdb7ef9b8fd3d930356b75afa1c23468efc4ff2df768275649a050989d5504c80a0b1e795db7700f";
Debug.WriteLine(JM.Decrypt(s));
Debug.WriteLine(JM.Encrypt(JM.Decrypt(s)).Equals(s));
>{"errcode":0,"sig":{"time":"1516158827.6761","nouce":"TMIPVP","signature":"6e691fe50ce57bef45361df01463a032"}}
>True
- 总结
协议分析已经完成,接下来的工作就是 开发一个http代{过}{滤}理工具,捕捉和修改 http 请求包。实现 解密 修改 加密;
劫持修改工具开发中。