Frida调试

frida常见使用及脚本收集 | Xman

环境搭建

  1. Android:安装Frida-Server(建议使用16.x版本)https://github.com/frida/frida/releases/download/16.5.9/frida-server-16.5.9-android-arm64.xz

    1
    2
    3
    adb root
    adb push frida-server /data/local/tmp/frida
    adb shell "chmod 777 /data/local/tmp/frida"
  2. Windows:安装与server版本对应的客户端

    1
    2
    3
    pip install frida~=16.5.9
    pip install frida-tools~=12.3.0
    frida --version

动态调试

1
2
3
4
5
6
7
8
# 启动frida-server
adb shell
su
/data/local/tmp/frida


# 使用hook.js脚本hook吉利银河中的某个函数
frida -U -n "吉利银河" -l ./hook.js

frida常用命令

命令 解释
adb forward tcp:27041 tcp:27042 将电脑的tcp/27041和手机的tcp/27042建立连接
adb devices 查看当前连接设备
adb -s 设备号 其他指令 多个设备时指定设备(adb -s FA6AE0309067 shell)
adb shell getprop ro.product.cpu.abi 查看Android处理器架构
adb install xxx.apk 安装APP
adb install -r xxx.apk 安装APP,已经存在,覆盖安装
adb uninstall app包名 卸载APP
adb uninstall -k app包名 卸载APP,保留数据
adb push 文件名 手机路径 往手机传递文件
adb pull 手机路径/文件名 从手机端获取文件
adb logcat 查看日志
adb logcat -c 清日志
adb shell pm list packages 列出手机端安装的所有app包名
adb shell dumpsys window | findstr mCurrentFocus 查看当前包名和主Activity
adb shell am start 包名/主Activity 启动Activity(adb shell am start com.autonavi.minimap/com.autonavi.map.activity.NewMapActivity)
frida -U -f 包名 -l demo.js -U=USB设备,-f=spawn(冷启动,主动运行app),-l=hook脚本。可以写包名,也可以写APP的名称
frida -U -n 包名 -l demo.js -U=USB设备,-n=spawn(热启动,hook代码注入到已经启动的app进程中。缺省选项,可不写)
frida-ps -U 列举出来设备上的所有进程
frida-ls-devices 列举出来所有连接到电脑上的设备
frida-ps -Uai 列举出来设备上的所有已安装应用程序和对应的名字
frida-trace -U -f Name -i “函数名” 跟踪某个函数(frida-trace -U -f com.autonavi.minimap -i “getRequestParams”)

hook模板

普通方法

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
'use strict';
const PKG = "com.example.target"; //包名
const CLS = "com.example.target.UserManager"; //类名
const METHOD= "login"; //只有1个签名的普通方法

Java.perform(function(){
const clazz = Java.use(CLS);
clazz[METHOD].implementation = function (pa, pb) {
console.log("\n========== Frida Enter ==========");
console.log("参数1:"+pa);
console.log("参数2:"+pb);

/* 打印调用链 */
const thread = Java.use("java.lang.Thread").currentThread();
const stack = thread.getStackTrace();
for(let i=3;i<stack.length;i++){
const cls = stack[i].getClassName();
const mtd = stack[i].getMethodName();
const file= stack[i].getFileName()||"Unknown";
const line= stack[i].getLineNumber();
console.log(` ${i}: ${cls}.${mtd}(${file}:${line})调用了↑`);
}

const ret = this[METHOD](pa, pb);
console.log("返回:"+ret);
console.log("=========== Frida Leave ===========\n");
return ret;
};
});

静态方法

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
'use strict';
const PKG = "com.example.target"; //包名
const CLS = "com.example.target.UserManager"; //类名
const METHOD= "staticLogin"; //static方法

Java.perform(function(){
const clazz = Java.use(CLS);
clazz[METHOD].implementation = function (pa, pb) {
console.log("\n========== Frida Enter (static) ==========");
console.log("参数1:"+pa);
console.log("参数2:"+pb);

const thread = Java.use("java.lang.Thread").currentThread();
const stack = thread.getStackTrace();
for(let i=3;i<stack.length;i++){
const cls = stack[i].getClassName();
const mtd = stack[i].getMethodName();
const file= stack[i].getFileName()||"Unknown";
const line= stack[i].getLineNumber();
console.log(` ${i}: ${cls}.${mtd}(${file}:${line})调用了↑`);
}

const ret = this[METHOD](pa, pb);
console.log("返回:"+ret);
console.log("=========== Frida Leave ===========\n");
return ret;
};
});

构造方法

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
'use strict';
const PKG = "com.example.target";
const CLS = "com.example.target.UserManager";
const METHOD= "$init";

Java.perform(function(){
const clazz = Java.use(CLS);
clazz[METHOD].implementation = function (pa, pb) {
console.log("\n========== Frida Enter <$init> ==========");
console.log("参数1:"+pa);
console.log("参数2:"+pb);

const thread = Java.use("java.lang.Thread").currentThread();
const stack = thread.getStackTrace();
for(let i=3;i<stack.length;i++){
const cls = stack[i].getClassName();
const mtd = stack[i].getMethodName();
const file= stack[i].getFileName()||"Unknown";
const line= stack[i].getLineNumber();
console.log(` ${i}: ${cls}.${mtd}(${file}:${line})调用了↑`);
}

const ret = this[METHOD](pa, pb); // 构造方法返回 this
console.log("构造完成");
console.log("=========== Frida Leave ===========\n");
return ret;
};
});

读取字段

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
'use strict';
const PKG = "com.example.target";
const CLS = "com.example.target.UserManager";
const FIELD = "token"; // 字段名

Java.perform(function(){
const clazz = Java.use(CLS);
clazz[FIELD].getter.implementation = function () {
console.log("\n========== Frida Field Read ==========");
const val = this[FIELD].value;
console.log("字段值:"+val);

const thread = Java.use("java.lang.Thread").currentThread();
const stack = thread.getStackTrace();
for(let i=3;i<stack.length;i++){
const cls = stack[i].getClassName();
const mtd = stack[i].getMethodName();
const file= stack[i].getFileName()||"Unknown";
const line= stack[i].getLineNumber();
console.log(` ${i}: ${cls}.${mtd}(${file}:${line})调用了↑`);
}
console.log("=========== Frida Leave ===========\n");
return val;
};
});

修改字段

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
'use strict';
const PKG = "com.example.target";
const CLS = "com.example.target.UserManager";
const FIELD = "token"; //字段名

Java.perform(function(){
const clazz = Java.use(CLS);
clazz[FIELD].setter.implementation = function (newVal) {
console.log("\n========== Frida Field Write ==========");
console.log("原值将被覆盖为:"+newVal);

const thread = Java.use("java.lang.Thread").currentThread();
const stack = thread.getStackTrace();
for(let i=3;i<stack.length;i++){
const cls = stack[i].getClassName();
const mtd = stack[i].getMethodName();
const file= stack[i].getFileName()||"Unknown";
const line= stack[i].getLineNumber();
console.log(` ${i}: ${cls}.${mtd}(${file}:${line})调用了↑`);
}

this[FIELD].value = newVal;
console.log("=========== Frida Leave ===========\n");
};
});

枚举所有类的信息

1
2
3
4
5
6
7
8
9
10
11
Java.perform(function (){
console.log("n[*] enumerating classes...");
Java.enumerateLoadedClasses({
onMatch: function(_className){
console.log("[*] found instance of '"+_className+"'");
},
onComplete: function(){
console.log("[*] class enuemration complete");
}
});
});

枚举类加载器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Java.perform(function () {
if(Java.available)
{
//枚举当前加载的Java VM类加载器
Java.enumerateClassLoaders({
//回调函数,参数loader是类加载的信息
onMatch: function (loader)
{
console.log("",loader);
},
//枚举完毕所有类加载器之后的回调函数
onComplete: function ()
{
console.log(">>>>>>>>>>>>>>>>>>>>> END <<<<<<<<<<<<<<<<<<<");
}
});
}else{
console.log("error");
}
});

查找堆上的实例化对象

1
2
3
4
5
6
7
8
9
10
11
12
13
Java.perform(function () {
//查找android.view.View类在堆上的实例化对象
Java.choose("android.view.View", {
//枚举时调用
onMatch:function(instance){
//打印实例
console.log(instance);
},
//枚举完成后调用
onComplete:function() {
console.log(">>>>>>>>>>>>>>>>>>>>> END <<<<<<<<<<<<<<<<<<<")
}});
});

native层某个可导出的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
'use strict';
const SO_NAME = "libtarget.so"; //.so文件
const FUNC = "malloc"; //可导出的函数

Java.perform(async function (){
/* 等待 so 加载 */
const m = await Process.getModuleByName(SO_NAME);
console.log(`[*] ${SO_NAME} @ ${m.base}`);

/* 附加导出函数 */
const mallocPtr = Module.findExportByName(SO_NAME, FUNC);
Interceptor.attach(mallocPtr, {
onEnter: function (args) {
this.size = args[0].toInt32();
console.log(`\n[malloc] size=${this.size}`);
/* 打印 Native 调用链 */
console.log(Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join('\n'));
},
onLeave: function (retval) {
console.log(`[malloc] return=${retval}`);
}
});
});

native层内部函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
'use strict';
const SO_NAME = "libtarget.so"; // 要分析的.so文件
const OFFSET = 0x1234; // 通过IDA或Ghidra看到的逻辑偏移量
const RET_TYPE = 'int';
const ARGS = ['pointer', 'int'];

Java.perform(async function (){
const m = await Process.getModuleByName(SO_NAME);
const addr= m.base.add(OFFSET);
console.log(`[*] hook @ ${addr}`);

Interceptor.attach(addr, {
onEnter: function (args) {
console.log(`\n[internal] arg0=${args[0].readUtf8String()} arg1=${args[1].toInt32()}`);
console.log(Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join('\n'));
},
onLeave: function (retval) {
console.log(`[internal] ret=${retval.toInt32()}`);
}
});
});

native层所有可导出的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
'use strict';
const SO_NAME = "libtarget.so";

Java.perform(async function (){
const m = await Process.getModuleByName(SO_NAME);
const exports = Module.enumerateExports(SO_NAME);
console.log(`[*] ${SO_NAME} 导出共计 ${exports.length} 个`);

exports.forEach(exp => {
if (exp.name.indexOf('crypto') === -1) return;
if (exp.type === 'function') {
Interceptor.attach(exp.address, {
onEnter: function (args) {
console.log(`[trace] ${exp.name}(${args[0]}, ${args[1]})`);
}
});
}
});
});

检测frida

检测手段

  1. 遍历运行的进程列表从而检查frida-server是否在运行

  2. 遍历data/local/tmp目录查看有无frida相关文件

  3. 遍历映射库,检测frida相关so

  4. 检查27042这个默认端口

  5. 内存中扫描frida特征字符串 “LIBFRIDA”

  6. frida使用D-Bus协议通信,我们为每个开放的端口发送D-Bus的认证消息

  7. 检测有无frida特征的线程名(如gum-js-loop)

  8. 检测库函数开头八个字节,判断是否被frida hook

  9. 出现检测时的现象

    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
    [09:41:15 RedTeamer@APT]# frida -U -f com.lynkco.customer -l hook_oss.js
    ____
    / _ | Frida 16.7.19 - A world-class dynamic instrumentation toolkit
    | (_| |
    > _ | Commands:
    /_/ |_| help -> Displays the help system
    . . . . object? -> Display information about 'object'
    . . . . exit/quit -> Exit
    . . . .
    . . . . More info at https://frida.re/docs/home/
    . . . .
    . . . . Connected to MI 6 (id=8d818833)
    Spawned `com.lynkco.customer`. Resuming main thread!
    [MI 6::com.lynkco.customer ]-> Error: java.lang.ClassNotFoundException: Didn't find class "com.zeekr.snc.rpa.log.c" on path: DexPathList[[zip file "/data/app/~~T8pAaiNyANupydEtoLqQnQ==/com.lynkco.customer-T_Dwo9RnDX_4ESKNXi8UBA==/base.apk"],nativeLibraryDirectories=[/data/app/~~T8pAaiNyANupydEtoLqQnQ==/com.lynkco.customer-T_Dwo9RnDX_4ESKNXi8UBA==/lib/arm64, /data/app/~~T8pAaiNyANupydEtoLqQnQ==/com.lynkco.customer-T_Dwo9RnDX_4ESKNXi8UBA==/base.apk!/lib/arm64-v8a, /system/lib64, /system/system_ext/lib64]]
    at <anonymous> (frida/node_modules/frida-java-bridge/lib/env.js:124)
    at <anonymous> (frida/node_modules/frida-java-bridge/lib/class-factory.js:502)
    at value (frida/node_modules/frida-java-bridge/lib/class-factory.js:949)
    at value (frida/node_modules/frida-java-bridge/lib/class-factory.js:954)
    at _make (frida/node_modules/frida-java-bridge/lib/class-factory.js:165)
    at use (frida/node_modules/frida-java-bridge/lib/class-factory.js:62)
    at use (frida/node_modules/frida-java-bridge/index.js:256)
    at <anonymous> (C:\Users\RedTeamer\Desktop\workspace\LynkCo\v4.1.0\scripts\hook_oss.js:2)
    at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
    at _performPendingVmOps (frida/node_modules/frida-java-bridge/index.js:248)
    at <anonymous> (frida/node_modules/frida-java-bridge/index.js:240)
    at apply (native)
    at re (frida/node_modules/frida-java-bridge/lib/class-factory.js:677)
    at <anonymous> (frida/node_modules/frida-java-bridge/lib/class-factory.js:655)

    Process terminated

绕过检测

  1. 修改默认端口

    1
    adb shell su -c "/data/local/tmp/frida-server -l 0.0.0.0:1234 &"
  2. frida-server和android_server的名字不要直接写本名,改成as,fs这种

  3. server位置不要放在data/local/tmp,可以放在data/目录下

排错

找不到类

1
Java.perform(() => console.log(Java.enumerateLoadedClassesSync().join("\n")));

方法冲突overload

1
2
# 根据报错提示进行overload
.overload("java.lang.String", "[B", "int", "int")

xxx is watting for debug

1
adb shell "am clear-debug-app"