记一款游戏脱机登录协议分析过程
0x0 分析目标
名称: 三国杀
包名: com.—.sgs.—-
背景: 项目需求, 脱机登录获取信息
分析目的: 脱机判断账号状态
0x1 抓包与Dump
尝试抓包
通过Charles看下, 登录过程中并无相关登录包, 猜测可能不是Http协议, 遂拖进JADX看看, 使用Frida一通hook无果后, 发现了Cocos2d
相关的函数, 翻了下之前的笔记, 业务逻辑应该都在lua里
dumplua脚本
搜下luaL_loadbuffer在哪个so, 上Frida直奔而去, 顺利dump下一堆luac, 使用unluac反编译后就可愉快的读源码了1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21var luaL_loadbuffer = Module.findExportByName("libsgs.so", "luaL_loadbuffer")
console.log("luaL_loadbuffer : ", luaL_loadbuffer)
Interceptor.attach(luaL_loadbuffer, {
onEnter: function(args){
var name = args[3].readCString().replace("/", "_")
for (var i = 0; i < 10; i++) {
name = name.replace("/", "_")
}
if(name.length < 100){
var len = args[2]
WriteMemoryToFile(name, ptr(args[1]), len)
console.log(name)
}
},
onLeave: function(retval){
}
});
转义lua字符串
读源码过程中发现, unluac反编译后的utf-8都经过转义了, 影响阅读, 如下:
也没找到转换的相关代码, 只能自己动手撸了, 转换代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67import os
import sys
import re
import codecs
def myfind(regx, data):
res = re.compile(regx, re.M).findall(data)
if len(res):
return res
else:
return None
def converFile(path):
if('app.gamelogic.OnRoleOptTargetLogic' in path):
print('pass')
f = open(path)
data = f.read()
f.close()
for i in range(10):
strs = myfind(r'\"(.*?)\"', data)
if (strs != None):
for m in strs:
if('\\' in m):
result = conver(m)
data = data.replace(m, result, 1)
if('"\\' not in data):
break
return data
def conver(target):
mbyteAry = target.split('\\')
out = b''
for b in mbyteAry:
try:
if(b != ''):
btys = int(b[0:3]).to_bytes(1, byteorder="big")
suffix = bytes(b[3:], encoding='utf-8')
out = out + btys + suffix
#out = out + r"\x" +str(hex(int(b[0:3])))[2:] + b[3:]
except Exception as e:
if(b != ''):
# out = out + '\\' + b
nobtys = bytes(b, encoding='utf-8')
out = out + nobtys
try:
return out.decode('utf-8')
except Exception as e:
print('decode utf-8 error : ' + target)
return target
def main(p):
for root, dirs, files in os.walk(p):
for filename in files:
curPath = os.path.join(root, filename)
data = converFile(curPath)
newPath = curPath.replace('unluac_mario', 'unluac_mario_conver')
f = open(newPath, 'w', encoding='utf-8')
f.write(data)
f.close()
print('conver file new path : ' + newPath)
跑完之后, 可以愉快的根据字符串去定位相关代码了…
抓登录包
因为不是http, 只能抓tcp包了, 用模拟器比较方便, 多次尝试确定了登录包, 不过发现包体是加密的(废话)
抓包工具: 抓包工具
0x2 分析发包逻辑
读了下登录相关的源码确定了登录逻辑:1
app.data.manager.LoginManager -> net.netmsg.ClientLoginReq -> net.NetEngine
于是想看下发包的内容是否与我抓到的一致, 就改了下NetEngine把发包内容打出来了
找到发包前的位置, 添加tips弹窗
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49function TableToStr(t)
if t == nil then return "" end
local retstr= "{"
local i = 1
for key,value in pairs(t) do
local signal = ","
if i==1 then
signal = ""
end
if key == i then
retstr = retstr..signal..ToStringEx(value)
else
if type(key)=='number' or type(key) == 'string' then
retstr = retstr..signal..'['..ToStringEx(key).."]="..ToStringEx(value)
else
if type(key)=='userdata' then
retstr = retstr..signal.."*s"..TableToStr(getmetatable(key)).."*e".."="..ToStringEx(value)
else
retstr = retstr..signal..key.."="..ToStringEx(value)
end
end
end
i = i+1
end
retstr = retstr.."}"
return retstr
end
function ToStringEx(value)
if type(value)=='table' then
return TableToStr(value)
elseif type(value)=='string' then
return "\'"..value.."\'"
else
return tostring(value)
end
end
local function bin2hex(s)
s=string.gsub(s,"(.)",function (x) return string.format("%02X ",string.byte(x)) end)
return s
end
app:showPropTips({autoBound = true,x = 1,y = 1,tittle = "mario",tips = ToStringEx(param)})
app:showPropTips({autoBound = true,x = 1,y = 1,tittle = "mario",tips = bin2hex(msgReq.data:getPack())})替换lua文件的关键frida代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46var addr_fopen = Module.findExportByName("libc.so","fopen")
var addr_fputs = Module.findExportByName("libc.so","fputs")
var addr_fclose = Module.findExportByName("libc.so","fclose")
var addr_ftell = Module.findExportByName("libc.so","ftell")
var addr_fseek = Module.findExportByName("libc.so","fseek")
var addr_malloc = Module.findExportByName("libc.so","malloc")
var addr_fread = Module.findExportByName("libc.so","fread")
var fopen = new NativeFunction(addr_fopen,"pointer",["pointer","pointer"])
var fputs = new NativeFunction(addr_fputs,"int",["pointer","pointer"]);
var fclose = new NativeFunction(addr_fclose,"int",["pointer"]);
var ftell = new NativeFunction(addr_ftell,"int",["pointer"]);
var fseek = new NativeFunction(addr_fseek,"int",["pointer","int","int"]);
var malloc = new NativeFunction(addr_malloc,"pointer",["int"]);
var fread = new NativeFunction(addr_fread,"pointer",["pointer","int","int","pointer"]);
var luaL_loadbuffer = Module.findExportByName("libsgs.so", "luaL_loadbuffer")
console.log("luaL_loadbuffer : ", luaL_loadbuffer)
Interceptor.attach(luaL_loadbuffer, {
onEnter: function(args){
var name = args[3].readCString()
if(name.indexOf('net.NetEngine') != -1){
console.log("hit " + name)
var filename = Memory.allocUtf8String("xxxxxxx/net.NetEngine.lua");
var rb = Memory.allocUtf8String("rb");
var f = fopen(filename, rb)
console.log("fopen : " + f)
fseek(f, 0, 2)
var filesize = ftell(f)
console.log("filesize : " + filesize)
var tBuff = malloc(filesize)
console.log("malloc : " + tBuff)
fseek(f, 0, 0);
fread(tBuff, 1, filesize, f);
//console.log("fread : " + hexdump(tBuff))
args[2] = ptr(filesize)
args[1] = ptr(tBuff)
}
},
onLeave: function(retval){
}
});
可以看到包体已经打出来了, 但是已经是明文, 这里已经是lua层逻辑的最后的执行位置, 再往下就是so里了, 看来加密是在so里
不过so还好没有对抗, 很快就定位到发包相关的位置… 确定了加密使用的是AES256-CFB, 并且不对前十二个字节加密(是opcode, ver等信息)
掏出来刚刚抓的加密包, 试了下可以正常加解密…
接下来就是分析如何组包, 这个没什么好写的, 对着源码分就行了…
0x4 写发包逻辑
这段没什么好写的, 已经弄清楚包体结构… 直接组包就行了, 直接上代码吧
1 | def loginSgs(user, psw): |
Done~