yellowtail 发表于 2023-5-2 02:06

某棋牌游戏lua逆向破解修改(一)

本帖最后由 yellowtail 于 2023-5-2 02:12 编辑

## 背景
最近朋友在玩几个棋牌游戏,我花了点时间研究了一下,虽然破解失败,但是过程感觉还是有收获,分享给大家

这篇文章主要分享 `如何找key`

破解思路后面有空再分享

## cocos2dlua
棋牌游戏的逻辑一般是使用lua编写的,lua脚本加载一般是用 cocos2d 加载的,我的破解方向集中在这里,如果你要破解的棋牌游戏不是这个技术栈,那就不能照搬

lua脚本常见情况如下:
- luac(编辑器打开脚本,前面几个字节是lua)
- cocos2dlua(编辑器打开脚本,前面几个字节,不是lua,多打开几个会发现是固定的)

我这里遇到的就是 cocos2dlua (看一下lib文件有没有叫类似名字的so)

cocos2dlua 加密原理给大家简单介绍一下
1. 写好lua脚本
2. 使用key进行加密(加密算法叫 xxtea)
3. 加密完成之后在最前面拼接sign

这个`sign`就是刚刚提到的固定字符串

解密步骤如下:
1. 读取脚本,切掉前缀,也就是sign
2. 使用找到的key进行解密


那么解密难点就是找key

key一般来说都在 `libcocos2dlua.so`里面, 有以下几种玩法:
1. 不修改 cocos2d 代码,明文存储
2. 修改cocos2d 代码,明文存储
3. 修改cocos2d 代码,代码计算得到

### 不修改 cocos2d 代码,明文存储
这种最简单了,编辑器打开lua脚本,直接搜前缀,key就在附近

为什么在附近?因为源码要求sign和key作为参数一起传

```c
bool AppDelegate::applicationDidFinishLaunching()
{
    // set default FPS
    Director::getInstance()->setAnimationInterval(1.0 / 60.0f);

    // register lua module
    auto engine = LuaEngine::getInstance();
    ScriptEngineManager::getInstance()->setScriptEngine(engine);
    lua_State* L = engine->getLuaStack()->getLuaState();
    lua_module_register(L);

    register_all_packages();

    LuaStack* stack = engine->getLuaStack();
    stack->setXXTEAKeyAndSign("2dxLua", strlen("2dxLua"), "XXTEA", strlen("XXTEA"));
```

上面的 2dxlua 就是sign,XXTEA 就是 key (加密算法也叫 xxtea)

源码在 (https://github.com/cocos2d/cocos2d-x/blob/90f6542cf7fb081335f04e474b880d7ce8c445a1/templates/lua-template-default/frameworks/runtime-src/Classes/AppDelegate.cpp#L74)

### 修改cocos2d 代码,明文存储
不修改源码,直接记事本打开就能找到key,所以一些开发者为了提升安全性,修改了 cocos2d代码,让sign和key远离(比如写一个函数获取key),增加破解难度

这里列举几个可能的变形点,依次在 ida里找就可能找到

以下均为 function
1. applicationDidFinishLaunching
入口点,默认sign和key在这里
2. setXXTEAKeyAndSign
如函数名所示,设置sign和key
3. xxtea_decrypt
算法解密点,因为需要传入key进行解密,按x看调用的地方,有key的踪迹

下面举一个例子

010打开lua脚本


看到了一个前缀
ida分析 libcocos2dlua.so
搜索 applicationDidFinishLaunching,没有看到明文


搜索 setXXTEAKeyAndSign,也没有看到明文


搜索 xxtea_decrypt,出现两个,有戏,因为一般是一个



依次按x看调用的地方,第一个比较正经,是正常调用关系
第二个是自定义的函数,进去看看



经过分析源码,得知 key 是 图中的 v21 (这里就不贴明文了,避免被搜索到,哈哈)



到这里,key就找到了

### 修改cocos2d 代码,代码计算得到

另外一种变形就是key不是明文存的,是计算得到,下面举一个例子说明

步骤类似
010打开


搜索 applicationDidFinishLaunching,看到明文了,但是有好几个字符串,到底是哪一个呢?


上面出现好几个字符串的那一行,实际调用的是函数 setXXTEAKeyAndSign
搜索 setXXTEAKeyAndSign,


经过阅读代码,慢慢扣偏移,最后得知 key 是 v21 (上面的例子也是v21,这个只是巧合,不用在意)

当然了,这里也不用慢慢扣代码,可以写代码梭哈,输入4个字符串,前两个用 0x2019%s%s 组合起来了,后面两个也是一样的组合方式,加起来6个可能,依次遍历,看看到底是哪个就行

这里是梭哈的golang代码


## 解密
上面是找key的方法,找到之后就要解密了
推荐自己写,我找了一些成品,只能说差强人意

推荐用golang写,无需乱七八糟的依赖,写完就运行
对golang不熟悉,也可以用 python,但是要依赖 特定版本 visual studio, 有多恶心,我就不多说了

以下是 golang代码
```
package main

import (
      "flag"
      "fmt"
      "io/ioutil"
      "os"
      "path/filepath"
      "time"

      "github.com/xxtea/xxtea-go/xxtea"
)

type CMD struct {
      encrypt_file string
      decrypt_file string
}

var prefix string = "xxxx"
var lua_key string = "xxxx"

func main() {

      cmd, r := initFlag()
      if r {
                return
      }

      if cmd.decrypt_file != "" {
                err := decrypt(cmd.decrypt_file)
                if err != nil {
                        fmt.Println(err)
                }
                return
      }

      if cmd.encrypt_file != "" {
                err := encrypt(cmd.encrypt_file)
                if err != nil {
                        fmt.Println(err)
                }

                return
      }

      fmt.Println("error, empty input")
}

func encrypt(path string) error {

      by, err := os.ReadFile(path)

      if err != nil {
                fmt.Println("read fail", err)
                return err
      }

      encrypt_data := xxtea.Encrypt(by, []byte(lua_key))

      // 拼接前缀
      data := append([]byte(prefix), encrypt_data...)

      // fileInfo, err := os.Stat(path)
      // if err != nil {
      //         return err
      // }

      name := filepath.Base(path)
      x1 := name[:len(name)-len(filepath.Ext(name))]

      newName := x1 + "_" + time.Now().Format("2006-01-02_15-04-05") + ".luac"

      fmt.Println("output file: ", newName)

      err = os.WriteFile(newName, data, 0666)
      if err != nil {
                fmt.Println("write fail")
      }

      return nil
}

func decrypt(path string) error {

      by, err := ioutil.ReadFile(path)

      if err != nil {
                fmt.Println("read fail")
                return err
      }

      // 去除前缀
      data := by

      decrypt_data := string(xxtea.Decrypt(data, []byte(lua_key)))
      fmt.Println(decrypt_data)
      return nil
}

func initFlag() (CMD, bool) {
      var decrypt_file = flag.String("d", "", "解密path")
      var encrypt_file = flag.String("e", "", "加密path")
      var help = flag.Bool("h", false, "help")

      flag.Parse()

      if *help {
                flag.Usage()

                return CMD{}, true
      }

      if *decrypt_file == "" && *encrypt_file == "" {
                flag.Usage()

                return CMD{}, true
      }

      c := CMD{*encrypt_file, *decrypt_file}
      return c, false

}

```

再贴一个python的(只有解密的,文件是写死的,所以代码行数少很多)
```
#coding: utf-8

# lua 解密单个文件

import os
import sys
import xxtea
import logging
import pathlib


a = "xxxx"
b = "0x201xxxx"

def decrypt(filename):
    f = open(filename, mode='rb')

    data = f.read()

    data2 = data

    data3 = xxtea.decrypt(data2, b)
    return data3

inputName = r"X:\007-project\003-lua\xxx\main.lua"


x = decrypt(inputName)
print(x)
```


## frida
以上是通过 ida 分析得到key,比较花时间,也看运气,因为可以修改的点太多了
这里再介绍一个釜底抽薪的方式

经过上面的分析得知,只要开发者没有丧心病狂的去修改 xxtea 算法,那么最后key一定会传到 xxtea_decrypt 里面,我们可以通过 frida 观察入参就能得到 key

```js

function xx() {
    console.log("===============================================================");

    var coco = Process.findModuleByName("libcocos2dlua.so");
    var exports = coco.enumerateExports();
    for(var i = 0; i < exports.length; i++) {
      var name = exports.name;

      // 不一定叫这个名字,匹配一下
      if (name.indexOf("xxtea_decrypt")!=-1) {
            console.log("name:", name);

            console.log("exports:", JSON.stringify(exports));

            var addr = exports.address;
            Interceptor.attach(addr, {
                onEnter: function (args) {

                  console.log('[+] args0,r0: ' + args);//data数据

                  console.log('[+] args1,r1: ' + args);//data长度


                  //密钥
                  console.log('[+] args2,r2: ' + args);
                  console.log(Memory.readCString(args));

                  console.log('[+] args2,r3: ' + args);//密钥长度
                } onLeave: function (retval) {


                }
            }
      });
    }

}

setImmediate(xx,0);
```



## 效果
这里贴一下反编译之后的效果图


yellowtail 发表于 2023-5-3 08:48

骇客之技术 发表于 2023-5-2 23:39
就是链接base64,比如百度网址 aHR0cHM6Ly93d3cuYmFpZHUuY29tLw==

两个app 链接地址
1. aHR0cHM6Ly9qenl3ZWIuaW5rL2JlSDR4SA==

2. aHR0cHM6Ly9jcHBzLnhmZW5mYS5jbi9pY2N5

有一个好像跑路了,连不上服务器

soyadokio 发表于 2023-5-6 16:34

感谢大佬分享,对一个外行而言,第一句话就指明方向很有意义:棋牌游戏的逻辑一般是使用lua编写的,lua脚本加载一般是用 cocos2d 加载的

暂时不会接触这方面,但顺着大佬思路看下来还真是舒服

xavier001 发表于 2023-5-2 09:27

学习了,感谢大佬分享,膜拜啊

zxcv75429 发表于 2023-5-2 11:06

厉害了谢谢分享

makmak79 发表于 2023-5-2 11:12

真是厉害!高手

aa2923821a 发表于 2023-5-2 13:37

可以可以感谢分享

骇客之技术 发表于 2023-5-2 13:41

楼主提示下样本链接

fscc无误 发表于 2023-5-2 15:12


真是厉害!高手

yellowtail 发表于 2023-5-2 16:03

骇客之技术 发表于 2023-5-2 13:41
楼主提示下样本链接

毕竟是赌博的,不确定能不能发;www

rflinker 发表于 2023-5-2 16:45

yellowtail 发表于 2023-5-2 16:03
毕竟是赌博的,不确定能不能发

发出来试试就逝世。

471757009 发表于 2023-5-2 16:46

不错哦啊啊是啊
页: [1] 2 3 4 5
查看完整版本: 某棋牌游戏lua逆向破解修改(一)