这个例子是一个入门级难度的手游脱机解密

引子

脱机的风险是非常大的,而且难度也是非常大的,开发周期也很长。非常不建议玩这个, 本文只作技术分享。

Cocos2d Lua

我们来看一款 lua 卡牌手游

IDA 打开 libgame.so


可以快速观察到这是一个 cocos2d 游戏,用 lua 来加解密的(当然也可能用 so 去加解密)

解密 Lua 文件

可以看到原本目录中的 lua 是被加密过的


我们需要解密后才能看到 lua 脚本的内容, 解密方法分为下面 2 种

静态解密

利用工具 xxtea_decrypt 来解密百度搜索就能下载
填入 so 中解析到的 key 就可以解密文件了,但是经常不准,所以推荐下吗一种动态解密的方法
• 签名 QIDEAENCRYPTED
• key GDv4gFTMcTGkRgAGB
• 拿到结果是 zip 压缩文件,再解压一下就出来了

动态解密(推荐)

用这个 Frida 脚本进入游戏界面后才算完全dump
在未混淆 so 的情况下 hook 的函数为 luaL_loadbufferx

#!/usr/bin/python
import frida
import sys
import os
import time

# put your javascript-code here
jscode = """
console.log("[*] Starting script");
var addr = Module.findExportByName("libgame.so",'luaL_loadbufferx');
while (addr == null) {
    addr = Module.findExportByName("libgame.so",'luaL_loadbufferx');
}
console.log("addr: " + addr);
Interceptor.attach(addr, {
    onEnter: function(args) {
        var name = Memory.readUtf8String(args[3]);
        var obj = {}
        obj.size = args[2].toInt32()
        obj.name = name;
        obj.content = Memory.readCString(args[1], obj.size);
        //obj.content = Memory.readByteArray(args[1], obj.size);

        console.log(name);
        send(name, Memory.readByteArray(args[1], obj.size));
    }
})
"""


def write(path, content):
    print('write:', path)
    folder = os.path.dirname(path)
    if not os.path.exists(folder):
        os.makedirs(folder)
    open(path, 'wb').write(content)


def on_message(message, data):
    # print(message)
    # name = message['payload']['name'].replace('@', '')
    name = message['payload'].replace('@', '')
    content = data
    write("lua/" + name + '.lua', content)
    # if name.endswith('.luac') or name.endswith('.lua'):


if __name__ == "__main__":

    device = frida.get_remote_device()
    pid = device.spawn(["xxx.xxx.xxx"])
    session = device.attach(pid)
    device.resume(pid)
    time.sleep(1)
    script = session.create_script(jscode)

    script.on("message", on_message)
    # and load the script
    script.load()
    sys.stdin.read()


可以看到这都是一些明文了
这里在平时逆向中优先推荐搜索 send 关键词 或者 encry 关键词来开始定位

ProtoBuf

解密 XXTEA


这款游戏用到了 XXTEA 的加密方式,数据传输方式是谷歌的 ProtoBuf 的传输方式

要想加解密 ProtoBuf 的内容,那么就要拿到 XXTEA 的秘钥,秘钥通常情况下是放在 LUA 或者 So 中,在这个游戏是在 so 中,每次接收 protobuf 的时候会在 so 中解析


本游戏 so 中解密的函数名为 xxtea_encrypt 我们 hook 他即可, 用到的脚本是

# -*- coding: UTF-8 -*-
import frida, sys

jsCode = """
Java.perform(function(){

    var soAddr = Module.findBaseAddress("libgame.so");
    send('libgame soAddr:' + soAddr);


    var send_ptr = Module.findExportByName("libgame.so", "socket_send"); 
    send('send_ptr:' + send_ptr);

   Interceptor.attach(send_ptr, {
    onEnter: function (args) {
            var buf = Memory.readByteArray(args[1] , args[2].toInt32());
            console.log("---------------------------------libgame send:["+ args[2].toInt32() +"]---------------------------------");
            //console.log('libgame send called from:' + Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress));
            console.log(hexdump(buf,{
               offset:0,
               lenth:args[2].toInt32(),
               header:false,
               ansi:true
            }));
      
    },
    onLeave: function (retval) {
    }
  });

    var recv_ptr = Module.findExportByName("libgame.so", "socket_recv"); 
    send('recv_ptr:' + recv_ptr);

   Interceptor.attach(recv_ptr, {
    onEnter: function (args) {
    /*
            var buf = Memory.readByteArray(args[1] , args[2].toInt32());
            console.log("---------------------------------libgame recv:["+ args[2].toInt32() +"]---------------------------------");
            //console.log('libgame send called from:' + Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress));
            if(args[2].toInt32() < 500)
            {
            console.log(hexdump(buf,{
               offset:0,
               lenth:args[2].toInt32(),
               header:false,
               ansi:true
            }));
            }
            */
    },
    onLeave: function (retval) {
    }
  });


    var encryptXXTEA_ptr = Module.findExportByName("libgame.so", "xxtea_encrypt"); 
    send('encryptXXTEA_ptr:' + encryptXXTEA_ptr);

   Interceptor.attach(encryptXXTEA_ptr, {
    onEnter: function (args) {

            var buflen = args[1].toInt32();
            var keylen = args[3].toInt32();
            var buf = Memory.readByteArray(args[0] , buflen);
            var key = Memory.readByteArray(args[2] , keylen);
            console.log("---------------------------------encryptXXTEA :["+ buflen +"]---------------------------------");
            //console.log('encryptXXTEA_ptr send called from:' + Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress));
            console.log('key:');
            console.log(hexdump(key,{
               offset:0,
               lenth:keylen,
               header:false,
               ansi:true
            }));
            console.log('buf:');
            console.log(hexdump(buf,{
               offset:0,
               lenth:buflen,
               header:false,
               ansi:true
            }));
    },
    onLeave: function (retval) {
    }
  });




});
"""


def message(message, data):
    if message["type"] == 'send':
        print(u"[*] {0}".format(message['payload']))
    else:
        print(message)


process = frida.get_remote_device().attach("xxxx")
script = process.create_script(jsCode)
script.on("message", message)
script.load()
sys.stdin.read()


可以自己写代码测试,也可以用工具测试一下

查看 pb 值

用刚刚解析出来的值,查看 pb,无头值

这里可以用工具也可以用代码实现
可以查看到具体值了,只是目前还不知道每个值对应的key 是什么,这种情况下,需要分析 LUA 文件,查找对应定义才能够匹配上了(当然如果靠猜就能分析的话最好了)

解密 key

想要解析 proto 的 key 必须要拿到 proto 文件才行,否则就要分析 lua 序列化的堆栈(当然也可以找规律的方式分析)

我们先找找本地的 pb 文件 , 如何在 cocos 中使用 protobuf 我们可以参考文章
https://www.cnblogs.com/chevin/p/6001872.html
所以可能本地会有一个 pb 文件解析头, 通常放在 res 文件夹中

本地也定义了大量的文件, 当然这些文件都是秘闻的,需要在so中hook出明文

通过 hook 和插桩就可以轻松拿到 pb 的 key 值了。

总结

  • 解密难度大
  • 写脱机麻烦程度高
  • 要解决 HTTP 登录
  • 要解决 TCP 通讯

这个例子是一个入门级难度的手游脱机解密, 后面有机会会分享几个中级,高级难度的手游脱机解密,或者代码编写的文章

Last modification:June 17, 2021
如果觉得我的文章对你有用,请随意赞赏