记一款游戏脱机登录协议分析过程
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
 49- function 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
 46- var 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~

 
		