FCAnd

GitHub - deathmemory/FridaContainer: FridaContainer 整合了网上流行的和自己编写的常用的 frida 脚本。 frida 脚本模块化,Java & Jni Trace。
FRIDA 使用经验交流分享-看雪

使用

首先编辑index.ts,这个文件在 npm run build 后会生成编译的_fcagent.js
然后frida注入_fcagent.js就行。

# after edit index.ts

npm run watch # 自动编译
# 或者
npm run build # 手动编译

frida -U -f com.example.android --no-pause -l _fcagent.js

关于环境构建

npm

查看当前项目已安装包(项目跟目录必须有 package.json 文件)并把所有包的依赖显示出来。

$ npm ls  --depth 0
@dmemory/fridacontainer@1.0.10 ...\FridaContainer-master
├── @types/base64-js@1.3.0
├── @types/frida-gum@18.3.1
├── @types/node@18.19.87
├── base64-ts@2.0.1
├── frida-compile@10.2.5
└── glob@11.0.2

如果构建成功了,会在当前项目下生成该环境,包括frida-compile等。

在配置过程中遇到以下问题:IDE对frida-gum库的类型定义文件(index.d.ts)的索引依赖于工作区是否显式打开该文件,关闭后索引失效。
解决方法:
@types/frida-gum 添加到 tsconfig.json 的 types 数组中:

{
  "compilerOptions": {
    // ... existing code ...
    "types": ["frida-gum","base64-js","node"],
    // ... existing code ...
  },
  "include": [
    "utils",
    "index.ts"
  ],
  // ... existing code ...
}

ts

原文中的评论区也有提到过,使用typescript写脚本需要被frida-compile编译过后才能使用的,需要使用npm run build编译,编译之后会生成一个新的_fcagent.js脚本。
总结:开发时使用的是ts,编译ts后生成js,注入时使用的是js。

npm run build # 编译脚本
frida -U -f com.example.android --no-pause -l _fcagent.js # 注入脚本

假如我不想每次修改完ts脚本后都重新使用npm run build,那么我们可以使用npm run watch达到实时编译的效果。
效果图如下,也就是保存一次就会自动compile一次,期间不要关闭这个终端和终止这个watch就行。
assets/FCAnd/file-20250504182407969.png

为什么使用命令行注入而不是使用python脚本注入呢?
DMemory大佬的解释是:
我是用 frida 命令行注入脚本的,python 非必要的情况下一般不用。在控制台直接调用 rpc.exports 就行。
比如,在 ts 里这样赋值

import {DMLog} from "../utils/dmlog";
import {FCAnd} from "../utils/FCAnd";
import {FCCommon} from "../utils/FCCommon";

rpc.exports = {
    main() { 
        console.log("rpc inject");
        // 接下来就可以使用FCAnd的模块了
    }
}

然后直接在控制台调用:
rpc.exports.main()

这样做的好处是可以在修改完ts脚本后,js就自动编译(因为启动了npm run watch),在命令行里就能实时注入,不用说需要Ctrl+C终止frida -U -f后再重新注入js脚本。

如果使用的是python注入,就需要每次重新启动 python 脚本,才能使用在 typescript 中新写的函数

例如说:
ts中:
assets/FCAnd/file-20250504183130011.png
自动编译的js脚本:
assets/FCAnd/file-20250504183212217.png
那么我们直接使用js脚本就可以了,里面已经包含了编译的库代码了,该打包的都封装好了,方便了我们编写ts。

agent

在index.ts中,会看到有一行导入被注释掉了:
import {DianPing} from "./agent/dp/dp";
这一行是我们的业务脚本存放的地方,在项目根目录创建 agent 目录,在这里开发业务脚本。
例如说有个app叫DianPing,我们就可以取名这个模块为DianPing,并开发自己的业务脚本。

模块介绍

对于初学者只需要学会使用几个常见的模块就行
FridaContainer/docs/android.md
可以通过vscode的大纲快速浏览文件的函数

堆栈内存模块

堆栈跟踪

// 获取当前堆栈信息
FCAnd.getStacks();

// 打印当前堆栈信息
FCAnd.showStacks();

堆栈信息显示

// 打印指定层数的堆栈信息,并显示相关模块信息
FCCommon.showStacksModInfo(context, 10);

这个函数会从当前堆栈指针开始,打印指定数量的堆栈内容,并尝试解析每个地址所属的模块信息。

获取 LR 寄存器

// 获取 LR 寄存器值(链接寄存器,存储函数返回地址)
let lr = FCCommon.getLR(context);

该函数支持 ARM 和 ARM64 架构,会根据当前架构自动获取正确的 LR 寄存器值。

模块操作

模块信息获取

// 根据内存地址获取所属模块信息
let moduleInfo = FCCommon.getModuleByAddr(address);

// 打印所有已加载模块信息
FCCommon.printModules();

// 显示所有已加载模块信息
FCAnd.showModules();

模块转储

// 转储指定模块到文件
FCCommon.dump_module("libc.so", "/data/local/tmp/");

// 将内存区域转储到文件
FCCommon.dump2file(address, size, "/data/local/tmp/memory_dump.bin");

dump_module 函数会自动计算模块大小,并将整个模块内容保存到指定目录。如果在 Android 环境下,建议保存到应用的私有目录(如 /data/data/com.package.name/)以避免权限问题。

内存操作

// 向指定内存地址写入字符串内容。
FCAnd.writeMemory(addr, "new data");

// 在指定内存范围内搜索匹配的模式,并将其替换为指定的数据。
FCAnd.replaceMemoryData()

DEX 转储

// 通用 DEX 转储方法
FCAnd.dump_dex_common();

// 使用 loadClass 方式转储 DEX
// 通过 `loadClass` 的方式实现 Dex 文件的 Dump。该方法会加载所有类,并在加载过程中拦截 `DefineClass` 方法以获取 Dex 文件的内存地址和大小,最终将 Dex 文件保存到指定路径。
FCAnd.dump_dex_loadAllClass();
// 当程序启动完成后,调用 rpc.exports.ddc() 完成转储

上下文获取

// 获取应用上下文
let context = FCAnd.getApplicationContext();

Hook Trace

URI/URL Hook

// Hook android.net.Uri.parse 方法,参数为是否显示堆栈
FCAnd.hook_uri(true);

// Hook java.net.URL 构造函数,参数为是否显示堆栈
FCAnd.hook_url(true);

JSON Hook

// Hook `org.json.JSONObject.getString` 方法,监控其调用。当该方法被调用且键值为指定的 `pKey` 时,会打印键值,并且可以打印堆栈信息。
FCAnd.hook_JSONObject_getString("auth_token");

// Hook `com.alibaba.fastjson.JSONObject` 的多个方法(如 `getString`、`getJSONArray`、`getJSONObject`、`getInteger`)。当这些方法被调用且键值为指定的 `pKey` 时,会打印键值,并且可以打印堆栈信息。
FCAnd.hook_fastJson("user_info");

Map Hook

// Hook Map.put 和 LinkedHashMap.put 方法
// 第一个参数为要监控的 key,第二个参数为是否精确匹配
FCAnd.hook_Map("password", true);

打印 Map

// 打印 HashMap 内容
FCAnd.printHashMap(hashMapObject);

日志log Hook

// Hook android.util.Log 的所有日志方法
FCAnd.hook_log();

跟踪 Native 加载

// 跟踪所有动态库加载
// 监控 `dlopen` 函数的调用,打印加载的动态库路径。用于追踪动态库的加载过程。
FCAnd.traceLoadlibrary();
// 跟踪所有 fopen 调用
FCAnd.traceFopen();
// 当指定的共享库(`.so` 文件)加载时,附加到指定的地址并执行回调函数。
FCAnd.attachWhenSoLoad();

// 监控动态库的加载过程,当加载指定的动态库时,执行回调函数。
FCAnd.whenSoLoad();

send_recv Hook

// 监控 `send` 和 `recv` 系统调用,打印发送和接收的数据内容及堆栈信息。
FCAnd.hook_send_recv();

svc Hook

// 监控指定的 SVC 地址列表,当这些地址被调用时,打印堆栈信息。
FCAnd.watch_svc_address_list();

方法追踪

Java方法 追踪

// 使用默认配置追踪 Java 方法
FCAnd.traceJavaMethods();
// 自定义追踪配置
/**
 * java 方法追踪
 * @param clazzes 要追踪类数组 ['M:Base64', 'E:java.lang.String'] 
 *                      类前面的 M 代表 match 模糊匹配,E 代表 equal 精确匹配
 * 
 * @param whitelist 指定某类方法 Hook 细则,可按白名单或黑名单过滤方法。
 *                  { '类名': {white: true, methods: ['toString', 'getBytes']} }
 * 
 * @stackFilter 按匹配字串打印堆栈。如果要匹配 bytes 数组需要十进制无空格字串,例如:"104,113,-105"
 */
FCAnd.traceJavaMethods(
    ['M:Base64', 'E:java.lang.String'],  // 要追踪的类
    {'java.lang.String': {white: true, methods: ['substring', 'getBytes']}},  // 方法白名单
    "password"  // 堆栈过滤字符串
);

// traceArtMethods 是 traceJavaMethods 的别名
FCAnd.traceArtMethods(['M:retrofit2']);

通过 python/android/traceLogCleaner.py 脚本收集 trace 日志,可以按线程、格式化输出日志

格式化 trace 效果
assets/FCAnd/javamethodtracepic.jpg

  • 注:如果 java trace 出现崩溃可以尝试调用纯净模式 FCAnd.traceJavaMethods_custom,这里没有默认 trace 的类 FCAnd.tjm_default_cls和默认单类白名单FCAnd.tjm_default_white_detail,需要自己手动附加,可以减少默认 trace 的类来判断崩溃的原因。若还有崩溃,请提交 issue 。
FCAnd.traceJavaMethods_custom(['E:java.net.URI'],
            {'java.net.URI': {white: true, methods: ['$init']}},
            "match_str_show_stacks");

JNI 跟踪

本功能是 jnitrace 的一个简化和嵌入版。
通过 FCAnd.jni 命名空间访问:

// 简单跟踪所有 JNI 函数
FCAnd.jni.traceAllJNISimply();

// 跟踪特定 JNI 函数
FCAnd.jni.traceJNI(['CallStaticObjectMethod', 'CallObjectMethod']);

FCAnd.jni.hookJNI('NewStringUTF', {
    onEnter: function (args) {
        var str = args[1].readCString();
        DMLog.i('NewStringUTF', 'str: ' + str);
        if (null != str) {
            if (str == 'mesh' || str.startsWith('6962')) {
                var lr =  FCAnd.getLR(this.context);
                DMLog.i('NewStringUTF', '(' + Process.arch + ')lr: ' + lr
                    + ', foundso:' + FCAnd.getModuleByAddr(lr) );
                // FCCommon.getStacksModInfo(this.context, 100);
            }
        }
    }
});

通过 python/android/traceLogCleaner.py 脚本收集 trace 日志,可以按线程、格式化输出日志

输出样例:

assets/FCAnd/jnitracelog.jpg

Native方法 追踪

FCAnd.jni.hook_registNatives();

Stalker 跟踪

// 使用 Frida Stalker 跟踪指定模块中的函数执行
FCCommon.stalkerTrace("libssl.so", functionAddress);

stalkerTrace 函数使用 Frida 的 Stalker API 跟踪指定函数的执行流程,记录指令执行和寄存器变化,对于分析复杂算法和加密函数非常有用。

注意:由于函数内使用了 Stalker.exclude,每次使用后建议重启进程,否则可能会出现段错误或访问错误。

寄存器变化跟踪

用于跟踪寄存器值的变化,通常与 stalkerTrace 配合使用。

// 获取上下文中变化的寄存器值
let changedRegs = FCCommon.get_diff_regs(context, previousRegs);

工具函数

字符串与数组处理

// 将 JS 对象转换为 Java 字符串
let javaStr = FCAnd.newString("Hello World");

// 将 Java 字节数组打印为十六进制字符串
let hexStr = FCAnd.printByteArray(byteArray);

//将字节数组以十六进制格式打印,类似于 `hexdump` 工具的输出格式。
FCAnd.byteshexdump();
// 将字符串转换为十六进制字符串
let hexStr = FCCommon.str2hexstr("Hello");  // 输出: "48656c6c6f"

// 将字符串转换为十进制数组(数组的保存格式为十进制)
let decArray = FCCommon.str2hexArray("ABC");  // 输出: [65, 66, 67]

// 将 ArrayBuffer 转换为十六进制字符串(带空格分隔)
let hexWithSpace = FCCommon.arrayBuffer2Hex(buffer);  // 输出: "48 65 6c 6c 6f"

C++ 标准字符串处理

// 创建新的 C++ 标准字符串对象
let stdString = FCCommon.newStdString();

这个函数返回一个 StdString 对象,用于处理 C++ 标准字符串。

签名转换

// 将 C++ 的符号名称(如 `_Z4hahaii`)转换为可读的函数签名(如 `haha(int, int)`)。
FCAnd.prettyMethod_C();

// 将 JNI 方法 ID 转换为可读的函数签名。
FCAnd.prettyMethod_Jni();

复制文件

// 复制文件
FCCommon.copyFile("/data/local/tmp/source.bin", "/data/data/com.example.app/files/dest.bin");

gson.toJson

增加 gson 库,可使用 gson.toJson 等功能
将 Java 对象转化成 Json,仓库已经集成了 gson 库,即使 APP 没有内置 gson 也可以使用。
当 gson 功能遇到瓶颈崩溃时,会用自实现的方法做兜底转换。

// 将 Java 对象转换为 JSON 字符串。如果无法直接转换,会尝试通过反射解析对象的字段。
FCAnd.toJSONString(javaObject);

// 通过反射解析 Java 对象的字段,并将其转换为 JSON 字符串。
FCAnd.parseObject(javaObject);

// 注册 Gson 库,用于 JSON 序列化和反序列化。如果 Gson 未加载,会尝试加载。
FCAnd.registGson();

反调试

通过 FCAnd.anti 命名空间访问:

普通反调试

// 绕过调试检测
FCAnd.anti.anti_debug();

// 绕过 ptrace 检测
FCAnd.anti.anti_ptrace();

// 绕过 fgets 检测
FCAnd.anti.anti_fgets();

抓包反调试

// SSL 证书固定绕过,相当于 frida 版的 JustTrustMe 
FCAnd.anti.anti_sslPinning("/data/local/tmp/cert-der.crt");

// SSL 证书验证绕过
FCAnd.anti.anti_ssl_unpinning();

// Cronet 库 SSL 证书验证绕过 (32位)
FCAnd.anti.anti_ssl_cronet_32();

load 自定义 ssl 证书

将证书 cert-der.crt 传到手机,然后调用下面的语句

FCAnd.anti.anti_sslLoadCert("/data/local/tmp/cert-der.crt");

类加载器

frida multi dex hook(java use)

目前支持通过 DexClassLoader | InMemoryDexClassLoader | BaseDexClassLoader | LoadClass 动态加载的 Dex

// 通过 `dalvik.system.DexClassLoader` 加载指定的类,并在加载后执行回调函数。
FCAnd.useWithDexClassLoader('com.cls.name', function (cls: Wrapper) {
    DMLog.i('tag', JSON.stringify(cls));
});

// 通过 `dalvik.system.InMemoryDexClassLoader` 加载指定的类,并在加载后执行回调函数。
FCAnd.useWithInMemoryDexClassLoader('com.cls.name', function (cls: Wrapper) {
    DMLog.i('tag', JSON.stringify(cls));
});

// 通过 `dalvik.system.BaseDexClassLoader` 加载指定的类,并在加载后执行回调函数。
FCAnd.useWithBaseDexClassLoader('com.cls.name', function (cls: Wrapper) {
    DMLog.i('tag', JSON.stringify(cls));
});

// 监控 `ClassLoader.loadClass` 方法,当加载指定类时执行回调函数。
FCAnd.useWhenLoadClass('com.cls.name', function (cls: Wrapper) {
    DMLog.i('tag', JSON.stringify(cls));
});

动态加载 dex

在利用 InMemoryDexClassLoader 加载内存 Dex 找不到类的情况下适用。

FCAnd.anti.anti_InMemoryDexClassLoader(function(){
    const cls = Java.use("find/same/multi/dex/class");
    // ...
});

查找指定类

// 通过多种方式(如 `ClassLoader` 和 `DexClassLoader`)查找指定的类,并打印查找结果。
FCAnd.findClass();

枚举 ClassLoader

// 枚举当前进程的所有 `ClassLoader`,查找指定的类并执行回调函数。
FCAnd.enumerateClassLoadersAndUse();

// 枚举当前进程的所有 `ClassLoader`,并为每个 `ClassLoader` 获取一个 `Java.ClassFactory`,用于后续操作。
FCAnd.enumerateClassLoadersAndGetFactory();

DMLog 模块功能使用介绍

DMLog 是一个用于 Frida 脚本中的日志记录工具类,提供了多种级别的日志输出功能,支持在 Android 和 iOS 环境下使用。

主要特性

  1. 支持多种日志级别(DEBUG、INFO、WARN、ERROR)
  2. 自动获取并显示线程信息(在 Java 环境下)
  3. 输出格式化的日志,包含时间戳、进程ID、线程名称、线程ID和标签
  4. 提供 send 方法用于向 Frida 客户端发送消息

类属性

  • static bDebug: boolean = true - 控制是否输出 DEBUG 级别的日志,默认为 true

日志输出方法

  1. d(tag: string, str: string) - 输出 DEBUG 级别日志

    // 使用示例
    DMLog.d('TAG', '这是一条调试信息');
    
  2. i(tag: string, str: string) - 输出 INFO 级别日志

    // 使用示例
    DMLog.i('TAG', '这是一条信息日志');
    
  3. w(tag: string, str: string) - 输出 WARN 级别日志

    // 使用示例
    DMLog.w('TAG', '这是一条警告信息');
    
  4. e(tag: string, str: string) - 输出 ERROR 级别日志

    // 使用示例
    DMLog.e('TAG', '这是一条错误信息');
    

内部方法

  1. log_(logfunc, leval, tag, str) - 内部使用的日志格式化和输出方法

    • 参数说明:
      • logfunc: 日志输出函数(如 console.log, console.error 等)
      • leval: 日志级别字符串
      • tag: 日志标签
      • str: 日志内容

    该方法会自动添加以下信息到日志中:

    • 日志级别 [DEBUG/INFO/WARN/ERROR]
    • 时间戳 [yyyy/MM/dd HH:mm:ss]
    • 进程ID [PID:xxx]
    • 线程名称(Java环境下)[ThreadName]
    • 线程ID [ThreadID]
    • 标签 [TAG]

消息发送方法

  1. send(tag: string, content: string) - 向 Frida 客户端发送消息

    // 使用示例
    DMLog.send('TAG', '这是发送给客户端的消息');
    

    发送的消息格式为 JSON 字符串:

    {
      "tid": 线程ID,
      "status": "msg",
      "tag": "标签",
      "content": "内容"
    }
    
posted @ 2025-05-04 19:00  方北七  阅读(208)  评论(0)    收藏  举报