wshuo 发表于 2022-1-30 00:18

写一个frida自吐算法通杀脚本

### 1. 前言

过年对我来说和平常没什么区别,该干什么干什么。

之前没接触过 **frida** 这个工具,前几天用了一些时间学习了一下,相比于 **xposed hook** 框架,frida 相对于调试方面真的很方便。现在网上也有一些 **frida** 通杀脚本(也有叫自吐算法脚本的),但是一般都是在 **iv**向量构造,**key** 构造分别进行 hook ,这样就导致 最后输出结果不是一个整体,加密和解密的数据,iv向量,key,输出不在同一块。我也不想从网上拿来就用(总感觉自己写一遍用起来才舒服,毕竟这个不算太复杂,还能熟悉一下 frida),所以我想制作一个输出以上信息在同一块算法通杀脚本,后面也用C++ Qt写了一个软件用来查看记录的数据。

### 2. 什么是hook?

这个问题让我想起我在大学期间,当时我用 **Linux mint** 系统,linux 系统上没有 QQ,所以我用 deepin-wine封装的QQ软件。但是使用过程中我发现有一个bug,就是不能打开接收到文件或文件夹,我当时猜测这个问题是 mint 没有 对应的文件管理器导致的,因为我用的是mint, 而软件使用的系统是在 deepin 上使用的,所以我在 mint系统上建立了一个与 deepin系统上的文件管理器同名的命令脚本,然后这个命令脚本去调用 mint 本地文件管理器去打开 对应的文件夹或文件,这样问题就解决了。

[当年文章](https://blog.csdn.net/chouzhou9701/article/details/100800806)

这个原理就类似 hook,只不过我没有拦截消息的传递(因为压根就没有接收消息的命令)。通俗来讲就是 拦截住消息传递,然后再去处理这个消息。当时我解决 deepin-wine QQ上这个bug,感觉自己这个操作太秀了,现在想想不过是当时自己了解的东西太少,是一种无知的体现。

### 3. 通杀算法(自吐算法)脚本原理

安卓上调用 AES 加密解密, MD5 摘要算法时,都是需要调用基础的一个类,所以只要 hook 这个基础类,那么无论在什么时候什么地方调用到 算法,都会执行到基础类。除非是app 里面自己实现的加密算法,那就只能分析 app 内部的代码了。

例如一个AES加密的调用示例:

```java
public static byte[] aes_enc(byte[] bytesContent, String key) throws Exception
{
    byte[] raw = key.getBytes("utf-8");
    SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
    byte[] enc = cipher.doFinal(bytesContent);
    return enc;
}
```

可以看到这里有一个关键类 `Cipher` ,只需 hook `doFinal()`这个函数,就可以获得密文和明文,而秘钥 可以通过 hook `SecretKeySpec()` 这个类构造函数来获得。

这些类的实现都在 `jce.jar` 中实现,在 JDK中有。

**Cipher** 类:

!(https://img-blog.csdnimg.cn/99c6a250da9045b587b65071bd72f6bc.png)

**SecretKeySpec** 类

!(https://img-blog.csdnimg.cn/52c05880a6c14089ae42f49e055d653a.png)

### 4. 分析

在 hook 这些类的构造函数或普通函数的时候,遇到了很多重载,下面简单理出这些重载的调用关系。

Cipher 类

```js
// getInstance 函数
// 2.overload('java.lang.String', 'java.lang.String') -> 3
// 1.overload('java.lang.String') |
// 3.overload('java.lang.String', 'java.security.Provider') |


// init 函数
// 1.overload('int', 'java.security.Key') -> 4
// 2.overload('int', 'java.security.cert.Certificate') -> 6
// 3.overload('int', 'java.security.Key', 'java.security.AlgorithmParameters') -> 7
// 5.overload('int', 'java.security.Key', 'java.security.spec.AlgorithmParameterSpec') ->8
// 6.overload('int', 'java.security.cert.Certificate', 'java.security.SecureRandom') |
// 4.overload('int', 'java.security.Key', 'java.security.SecureRandom') |
// 7.overload('int', 'java.security.Key', 'java.security.AlgorithmParameters', 'java.security.SecureRandom') |
// 8.overload('int', 'java.security.Key', 'java.security.spec.AlgorithmParameterSpec', 'java.security.SecureRandom') |

// doFinal 函数
// 1.overload() |
// 2.overload('[B') |
// 3.overload('java.nio.ByteBuffer', 'java.nio.ByteBuffer') |
// 4.overload('[B', 'int') |
// 5.overload('[B', 'int', 'int') |
// 6.overload('[B', 'int', 'int', '[B') |
// 7.overload('[B', 'int', 'int', '[B', 'int') |
```

这里的数字表示重载函数的编号条目, `->` 表示调用,`|` 表示不调用其它重载函数了。通过这种写法,可以清晰的看出 重载函数之间的调用关系。

所以为什么需要理清重载关系? 因为这样可以知道 hook 哪些 函数是必要的。如果我们不知道重载函数之间调用的关系,直接hook 一个函数的所有重载:

```js
var cipher = Java.use("javax.crypto.Cipher");
    // 加密类型
    // 2.overload('java.lang.String', 'java.lang.String') -> 3
    // 1.overload('java.lang.String') |
    // 3.overload('java.lang.String', 'java.security.Provider') |
    for (let index = 0; index < cipher.getInstance.overloads.length; index++) {
      cipher.getInstance.overloads.implementation = function () {
            console.log("类型:" + JSON.stringify(arguments));
            console.log(JSON.stringify(this));
            return this.getInstance.apply(this, arguments);
      }
    }
```

这样,你可能会发现,输出 `类型:` 两次, 因为 1,2,3 函数都被 hook了,**app** 可能只是调用了 2 ,而 2 本身有调用了 3。所以就会输出两次,所以针对这个 `getInstance` 函数,只需要 **hook**1,3 重载函数就可以实现对所有调用的监听。

### 5. 深度分析

上述内容,可以实现对调用函数参数的监听,并且减少了 不必要 函数的 hook。但是还是不能实现 输出数据 在一整块地方,加密类型输出 和 明文,密文 可能是分散输出的。而要实现我 **前言** 中所述 的功能,就必须通过 hook 一个函数来实现,在一个函数内 获取 **秘钥**,**iv 向量**,**明文**, **密文**,**模式**,**加密or解密**,这些信息,然后输出,其实这种肯定是要利用对象本身来传递的,就是查看对象属性上的绑定。

----

**模式** : 这里所说的模式,指的是`"AES/ECB/PKCS5Padding"` 字符串,所以从 **getInstance** 函数开始分析,当然这里只需要分析 肯定会被调用到的函数重载,也就是结尾带`|`的函数:

!(https://img-blog.csdnimg.cn/c2bd4d9aa3be4d57bf819d4bcdab6fc4.png)

!(https://img-blog.csdnimg.cn/1a053e50910444758984b913c189caa3.png)

可以看到,`getInstance` 这个函数就是返回了一个 **cipher** 类的实例化对象,并且这个字符串传递过去,所以进一步跟踪分析其构造函数:

!(https://img-blog.csdnimg.cn/08856fba536841c89664ac601924889d.png)

可以看到这几个构造函数,将 **paramString** 参数都专递给了 `this.transformation`,那么这个 **模式** 就可以通过 **this**来获取到了

---

**加密or解密**: 加密或解密,是 **init** 函数的第一个参数,所以这里重 init 函数开始分析参数的传递状态,同样也只需要分析肯定能被调用到的函数就可以:

!(https://img-blog.csdnimg.cn/928e8e05ed074ea9ae1cf1a3c6a258ff.png)

!(https://img-blog.csdnimg.cn/d3efc3b2dfab49929f598fc23aef356a.png)

!(https://img-blog.csdnimg.cn/0907598374564fd8a6a5b573925ddfe6.png)

!(https://img-blog.csdnimg.cn/e83ec13538b547f890449fe153f5a436.png)

这4个重载函数都是 `paramInt`函数都是 传递给 `this.opmode`。

---

**IV向量**, 这个有点难找,具体过程不多说了,可以通过 `this.spi.engineGetIV()` 或者 `this.getIV()` 获得,其实`this.getIV()` 也是调用 `this.spi.engineGetIV()`获得的。

---

**密文和明文**:因为我要hook 一个函数,所以我想hook的就是最后的 doFinal 函数,这样就可以获取到 密文和明文了,然后再通过 this ,获取到上述所说的 **模式**,**加密或解密**,**iv向量**。

---

**秘钥**: 只有这个参数是我没有通过 **this** 获取到,所以我 hook 了**init** 函数,其第二个参数就是秘钥。

---

### 6. 问题及解决

因为除了秘钥我都可以 通过 this 来获取到,所以秘钥获得后我在 JS 用一个全局变量来保存,然后在我hook 的 doFinal 函数 中进行输出,但是这里就遇到一个问题,多个线程可能同时进行加密解密,key 可能不是当时 对象 doFinal 函数使用的 key,那么这里我需要对实例化对象的唯一ID 与 key 进行一个绑定,然后 doFinal 函数里通过对象唯一ID 来获取key ,进行输出。

这里的解决方案是我制作了一个字典(python 叫字典,js叫啥我忘了),其中键为对象的唯一ID,值为 秘钥。这就能保证其对应关系的准确性了。

### 7. python 调用及数据保存

`hookCalc.js`

```js
var allKeys = {};

Java.perform(function () {
    var cipher = Java.use("javax.crypto.Cipher");

    for (let index = 0; index < cipher.init.overloads.length; index++) {
      cipher.init.overloads.implementation = function () {
            allKeys = arguments.getEncoded();
            this.init.apply(this, arguments);
      }
    }

    for (let index = 0; index < cipher.doFinal.overloads.length; index++) {
      cipher.doFinal.overloads.implementation = function () {
            var dict = {};
            dict["EorD"] = this.opmode.value; //模式 加密解密
            dict["method"] = this.transformation.value; //加密类型
            var iv =this.spi.value.engineGetIV();
            if (iv){
                dict["iv"] = iv;
            }else{
                dict["iv"] = "";
            }
            if (allKeys){
                dict["password"] = allKeys
            }else{
                dict["password"] = "";
            }
            var retVal = this.doFinal.apply(this, arguments);
            dict["receData"] = "";
            dict["resData"] = "";
            if (arguments.length >= 1 && arguments.$className != "java.nio.ByteBuffer") {
                dict['receData'] = arguments;
                dict["resData"] = retVal;
            }
            send(dict);
            return retVal;
      }
    }
})

```

`main.py`

```python
import frida
import sys
import sqlite3
import hashlib


index = 0
db = "me.db"

def md5(data):
    hl = hashlib.md5()
    hl.update(data)
    return hl.hexdigest()

def createDB():
    sql = '''
    CREATE TABLE IF NOT EXISTS "record" (
      "id"    TEXT NOT NULL,
      "method"    INTEGER,
      "EorD"TEXT,
      "password"BLOB,
      "iv"    BLOB,
      "receData"BLOB,
      "resData"   BLOB,
      PRIMARY KEY("id")
    );
    '''
    conn = sqlite3.connect(db)
    cursor = conn.cursor()
    cursor.execute(sql)
    conn.commit()
    conn.close()

def message(message,arg2):
    try:
      global index
      conn = sqlite3.connect(db)
      cursor = conn.cursor()

      if message['type'] == "send":
            data = message['payload']
            method = data["method"]
            EorD = data["EorD"]
            password = bytes(])
            iv =bytes(])
            receData =bytes(])
            resData =bytes(])
            id_md5 = md5((method+str(EorD)).encode()+password+iv+receData+resData)
            
            sql = "insert into record(id,method,EorD,password,iv,receData,resData) values (?,?,?,?,?,?,?)"
            cursor.execute(sql,(id_md5,method,EorD,sqlite3.Binary(password),sqlite3.Binary(iv),sqlite3.Binary(receData),sqlite3.Binary(resData)))
            conn.commit()
            print(index)
            index += 1
    except Exception as e:
      print(e)
      pass

with open("hookCalc.js",encoding='utf8') as f:
    js = f.read()

process = frida.get_remote_device().attach("APP名字,非包名")
script = process.create_script(js)
script.on("message",message)
script.load()
createDB()
print(process)
sys.stdin.read()

```

数据不便于输出预览,因为量大且不可输出(二进制数据输出乱码),JS 虽然有 转换编码及数据格式的函数,但是我还是习惯用 python 来处理。我将其保存到一个 sqlite3 数据库,这里我做了md5,保证数据保存的唯一性,然后我用 C++ QT 写个简单软件预览这些数据。

### C++预览数据软件

!(https://img-blog.csdnimg.cn/f7d3ea157a764aa989101b4eeb33797c.png)

!(https://img-blog.csdnimg.cn/a8db80df25ca49869d223d3dfdc42732.png)

软件读取同级目录的 **me.db** 数据库。

### 其它

脚本主要针对 AES 算法,也应该可以捕获的RSA,至于md5可以自行拓展,因为主要就是 AES算法 参数较多,会有输出会这一块那一块的问题,md5 根本不会有这样问题。

[软件及脚本](https://wshuo.lanzouq.com/iHLytzei5uh)

不苦小和尚 发表于 2022-1-30 07:48

早就有了吧

baoqi 发表于 2022-1-30 12:18

不苦小和尚 发表于 2022-1-30 07:48
早就有了吧

早就有了就能人家拉出来分享一下思路啦&#128516;

飘浮 发表于 2022-2-3 10:01

学习下。。

jackchen66 发表于 2022-7-24 17:56

感谢分享,有了这个脚本一些基础的加密可以不需要反编译了

hymnmx 发表于 2022-10-27 17:26

我的乖不行我得细品一下

lele99164 发表于 2022-11-12 15:40

写在so里的是不是就不能hook出来了

james180 发表于 2023-11-14 09:31

感谢楼主,实测有用

sujun2002 发表于 2023-11-14 16:11

正学习frida中
页: [1]
查看完整版本: 写一个frida自吐算法通杀脚本