Frida的使用¶
frida-ps¶
# Connect Frida to an iPad over USB and list running processes
frida-ps -U
# List running applications
frida-ps -Ua
# List installed applications
frida-ps -Uai

frida-trace¶
# Trace recv* and send* APIs in Safari, insert library names
# in logging
frida-trace --decorate -i "recv*" -i "send*" Safari
# Trace ObjC method calls in Safari
frida-trace -m "-[NSView drawRect:]" Safari
# Launch SnapChat on your iPhone and trace crypto API calls
frida-trace \
-U \
-f com.toyopagroup.picaboo \
-I "libcommonCrypto*"
# Launch YouTube on your Android device and trace Java methods
# with “certificate” in their signature (s), ignoring case (i)
# and only searching in user-defined classes (u)
frida-trace \
-U \
-f com.google.android.youtube \
--runtime=v8 \
-j '*!*certificate*/isu'
frida-ls-devices¶
This is a command-line tool for listing attached devices, which is very useful when interacting with multiple devices.

frida-kill¶
This is a command-line tool for killing processes.
frida-kill -D <DEVICE-ID> <PID>
# List active applications
frida-ps -D 1d07b5f6a7a72552aca8ab0e6b706f3f3958f63e -a
PID Name Identifier
---- ------------------ -----------------------------------------------------
4433 Camera com.apple.camera
4001 Cydia com.saurik.Cydia
4997 Filza com.tigisoftware.Filza
4130 IPA Installer com.slugrail.ipainstaller
3992 Mail com.apple.mobilemail
4888 Maps com.apple.Maps
6494 Messages com.apple.MobileSMS
5029 Safari com.apple.mobilesafari
4121 Settings com.apple.Preferences
# Connect Frida to the device and kill running process
frida-kill -D 1d07b5f6a7a72552aca8ab0e6b706f3f3958f63e 5029
Frida Java层 Hook¶
基础编写¶
示例apk下载地址:https://github.com/aszx87410/demo/raw/master/android/demoapp.apk
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0"
android:compileSdkVersion="30"
android:compileSdkVersionCodename="11"
package="com.cymetrics.demo"
platformBuildVersionCode="30"
platformBuildVersionName="11">
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="30"/>
<application
android:theme="@style/AppTheme"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:allowBackup="true"
android:supportsRtl="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:appComponentFactory="androidx.core.app.CoreComponentFactory">
<activity
android:theme="@style/AppTheme.NoActionBar"
android:label="@string/app_name"
android:name="com.cymetrics.demo.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
我们hook主activity: com.cymetrics.demo.MainActivity
// script.js
function run() {
Java.perform(() => {
var MainActivity = Java.use('com.cymetrics.demo.MainActivity')
MainActivity.onCreate.implementation = function() {
console.log('MainActivity onCreate')
}
})
}
setImmediate(run)
接下来启动frida

如果存在报错:Failed to spawn: need Gadget to attach on jailed Android; its default location is: /Users/\<username>/.cache/frida/gadget-android-arm64.so
则说明对应的gadget-android-arm64.so文件缺失,此时我们需要下载和frida版本一致的so文件,下载完成后将其移动到对应路径下即可


此时成功打印出日志,并且app处于崩溃状态,这是因為我們自己實作的 function 除了 log 以外什麼都沒做,也就是說原本的 onCreate 該做的事情都被拿掉了,所以 crash 也是合情合理
frida hook脚本基本结构如下:
接著就看你想要 hook 什麼樣的方法,以我們剛剛的程式碼來說,先用 Java.use 拿到想要 hook 的 class,再用 MainActivity.onCreate.implementation 把原本的實作換成我們自己定義的 function。
如果我们想要程序不崩溃,只需要和原有逻辑保持一致即可
function run() {
Java.perform(() => {
var MainActivity = Java.use('com.cymetrics.demo.MainActivity')
MainActivity.onCreate.implementation = function() {
console.log('MainActivity onCreate')
this.onCreate.call(this)
}
})
}
setImmediate(run)
this 會是原本的 MainActivity,透過 this.onCreate.call 可以呼叫到原本的實作,而 call 這個方法第一個要傳入的參數就是 this,後面傳入參數。

這是因為 onCreate 其實應該是有帶參數的,只是我們覆蓋的時候沒有接收參數,因此就出錯了。為了避免這個問題,我會建議在覆蓋實作的時候在前面加上 .overload(),像這樣:
这样frida就会在出错时提示正确参数是什么

我们按照frida的提示加上android.os.Bundle,并作为参数传入onCreate函数
function run() {
Java.perform(() => {
var MainActivity = Java.use('com.cymetrics.demo.MainActivity')
MainActivity.onCreate.overload('android.os.Bundle').implementation = function(a) {
console.log('MainActivity onCreate')
this.onCreate.call(this, a)
}
})
}
setImmediate(run)

此时就能正常注入了,此时我们也可以注入其他代码,只要最后调用原有逻辑即可
function run() {
Java.perform(() => {
var MainActivity = Java.use('com.cymetrics.demo.MainActivity')
MainActivity.onCreate.overload('android.os.Bundle').implementation = function(a) {
console.log('MainActivity onCreate')
// Toast 一定要跑在 main thread(UI thread)
Java.scheduleOnMainThread(function() {
var Toast = Java.use("android.widget.Toast");
var currentApplication = Java.use('android.app.ActivityThread').currentApplication();
// Toast 的第一個參數需要 context 才能執行
var context = currentApplication.getApplicationContext();
Toast.makeText(
context,
// 這個參數的型態要正確,直接傳字串會出錯
Java.use("java.lang.String").$new("Hello!"),
Toast.LENGTH_SHORT.value
).show();
});
this.onCreate.call(this, a)
}
})
}
setImmediate(run)

比如上处hook是弹窗Hello!
Hook构造函数¶
示例如下:
Java.perform(function () {
// 加载目标类
var TargetClass = Java.use('com.example.TargetClass');
console.log("Class loaded successfully!");
// Hook 带两个字符串参数的构造函数
TargetClass.$init.overload('java.lang.String', 'java.lang.String').implementation = function (param1, param2) {
console.log("Hooked constructor called!");
console.log("Parameter 1: " + param1);
console.log("Parameter 2: " + param2);
// 调用原始构造函数
var result = this.$init(param1, param2);
// 返回结果
return result;
};
});
无参和有参的构造函数hook区别如下:
// Hook 无参构造函数
TargetClass.$init.overload().implementation = function () {
console.log("Hooked no-arg constructor!");
return this.$init();
};
// Hook 多参数构造函数
TargetClass.$init.overload('java.lang.String', 'int', 'boolean').implementation = function (str, num, bool) {
console.log("String: " + str + ", Int: " + num + ", Boolean: " + bool);
return this.$init(str, num, bool);
};
Note
- 构造函数调用时机:如果目标类的构造函数在应用启动时已经调用,后续 Hook 将无法捕获。需要在应用启动前注入脚本。
- 注入方式:使用 frida -U -n
-l 或 frida -U -f-n 注入脚本。 - 参数类型匹配:确保 .overload 中的参数类型与目标构造函数完全一致,否则会导致 Hook 失败。
Hook示例¶
绕过Root检测¶
示例apk下载地址:https://github.com/aszx87410/demo/raw/master/android/demoapp.apk
这个app会简单检测设备是否已经root


反编译发现其关键函数为com.scottyab.rootbeer.RootBeer.isRooted函数,在com.cymetrics.demo.FristFragment中会对该函数的返回值进行判断,为True就表示检测到root,我们直接hook这个函数:
function run() {
Java.perform(() => {
var RootBeer = Java.use('com.scottyab.rootbeer.RootBeer')
RootBeer.isRooted.overload().implementation = function() {
console.log('isRooted Bypassed')
return false
}
})
}
setImmediate(run)


此时已经成功绕过Root检测,可见当App源码没有加固和混淆,易于反编译分析逻辑时,以动态hook的方式绕过Root检测较于修改smail代码的静态绕过方式更加简单
Frida Native层 Hook¶
Android Native层位于Android系统架构的底层,介于Linux内核和Java应用层之间。它主要由C/C++语言编写,提供了一系列的库和API,使得Java应用能够与底层的硬件进行交互。
为什么apk反编译后存在so文件¶
Android App 主要用 Java/Kotlin 编写,运行在 **ART 虚拟机**上。但某些场景需要直接调用 底层 C/C++ 代码,这时就用到 JNI(Java Native Interface) 机制:
| 场景 | 说明 |
|---|---|
| 高性能计算 | 如图像处理(OpenCV)、音视频编解码(FFmpeg)、加密算法 |
| 复用 C/C++ 代码库 | 如旧系统迁移、跨平台 SDK(微信、游戏引擎) |
| 反逆向/加固 | 将核心逻辑(如签名校验、加密密钥)放在 Native 层,增加逆向难度 |
| 系统级操作 | 直接调用 Linux 系统调用(如 root 检测、进程隐藏) |
关键点: Java 代码通过
System.loadLibrary("native")加载libnative.so, 然后调用其中的函数(通过 JNI 规则命名,如Java_com_example_MainActivity_hello)。
so层与java层的安全性对比:
| 特性 | Java 层 | Native 层 |
|---|---|---|
| 逆向难度 | 低(Jadx、Bytecode Viewer 可直接看源码) | 高(需 IDA/Ghidra,分析汇编) |
| 动态调试 | 容易(Frida、Xposed) | 较难(需 frida-server + native hook) |
| 代码混淆 | ProGuard/R8(仅改名,逻辑不变) | 控制流混淆、字符串加密、反调试 |
| 敏感信息存储 | 易被内存 dump | 可结合内存保护(如 mprotect) |
总结:
| 问题 | 答案 |
|---|---|
APK 中为什么有 .so 文件? |
App 使用了 C/C++ 编写的 Native 代码 |
| 和 Native 层什么关系? | .so 就是 Native 层的可执行代码,通过 JNI 与 Java 交互 |
| 安全意义是什么? | 常用于存放核心逻辑(加密、反调试),提高逆向门槛 |
| 逆向时如何处理? | 提取 .so → 用 IDA/Ghidra 分析 → 用 Frida Hook 动态验证 |
导出函数与非导出函数¶
导出函数(Exported)¶
-
定义:在动态库(
.so)中,被标记为可被外部模块(其他 so 或主程序)调用的函数。 -
ELF 结构:
-
出现在
.dynsym(动态符号表) 中 - 在
.dynstr(动态字符串表) 中有函数名 -
通常有
STB_GLOBAL或STB_WEAK绑定属性 -
如何识别:
输出中带T(全局函数)或W(弱符号)的即为导出函数:
导出函数 = 可被动态链接器解析的函数,例如 JNI 函数、
__attribute__((visibility("default")))的函数。
非导出函数(Non-Exported)¶
-
定义:**仅在当前
.so内部使用**的函数,不对外暴露。 -
常见类型:
-
用
static修饰的 C 函数 - 用
__attribute__((visibility("hidden")))显式隐藏的函数 -
编译时未加
-fvisibility=default的函数(默认 hidden) -
ELF 结构:
-
不在
.dynsym中(动态链接器看不到) - 可能在
.symtab(静态符号表) 中(调试符号,Release 版常被 strip) -
函数名在
.strtab中(但 Release 版常被移除) -
如何识别:
⚠️ Release 版
.so通常会strip掉.symtab和.strtab,导致非导出函数**既无名也无符号**,只能通过地址或特征定位。
基础编写¶
Hook导出函数¶
基础Hook打印¶
- 整数型、布尔值类型、char类型
function hookTest2(){
Java.perform(function(){
//根据导出函数名打印地址
var helloAddr = Module.findExportByName("lib52pojie.so","Java_com_zj_wuaipojie_util_SecurityUtil_checkVip");
console.log(helloAddr);
if(helloAddr != null){
//Interceptor.attach是Frida里的一个拦截器
Interceptor.attach(helloAddr,{
//onEnter里可以打印和修改参数
onEnter: function(args){ //args传入参数
console.log(args[0]); //打印第一个参数的值
console.log(this.context.x1); // 打印寄存器内容
console.log(args[1].toInt32()); //toInt32()转十进制
console.log(args[2].readCString()); //读取字符串 char类型
console.log(hexdump(args[2])); //内存dump
},
//onLeave里可以打印和修改返回值
onLeave: function(retval){ //retval返回值
console.log(retval);
console.log("retval",retval.toInt32());
}
})
}
})
}
- 字符串类型
function hookTest2(){
Java.perform(function(){
//根据导出函数名打印地址
var helloAddr = Module.findExportByName("lib52pojie.so","Java_com_zj_wuaipojie_util_SecurityUtil_vipLevel");
if(helloAddr != null){
Interceptor.attach(helloAddr,{
//onEnter里可以打印和修改参数
onEnter: function(args){ //args传入参数
// 方法一
var jString = Java.cast(args[2], Java.use('java.lang.String'));
console.log("参数:", jString.toString());
// 方法二
var JNIEnv = Java.vm.getEnv();
var originalStrPtr = JNIEnv.getStringUtfChars(args[2], null).readCString();
console.log("参数:", originalStrPtr);
},
//onLeave里可以打印和修改返回值
onLeave: function(retval){ //retval返回值
var returnedJString = Java.cast(retval, Java.use('java.lang.String'));
console.log("返回值:", returnedJString.toString());
}
})
}
})
}
基础Hook修改¶
- 整数型修改
function hookTest3(){
Java.perform(function(){
//根据导出函数名打印地址
var helloAddr = Module.findExportByName("lib52pojie.so","func_four");
console.log(helloAddr);
if(helloAddr != null){
Interceptor.attach(helloAddr,{
onEnter: function(args){ //args参数
args[0] = ptr(1000); //第一个参数修改为整数 1000,先转为指针再赋值
console.log(args[0]);
},
onLeave: function(retval){ //retval返回值
retval.replace(20000); //返回值修改
console.log("retval",retval.toInt32());
}
})
}
})
}
- 字符串类型修改
function hookTest2(){
Java.perform(function(){
//根据导出函数名打印地址
var helloAddr = Module.findExportByName("lib52pojie.so","Java_com_zj_wuaipojie_util_SecurityUtil_vipLevel");
if(helloAddr != null){
Interceptor.attach(helloAddr,{
//onEnter里可以打印和修改参数
onEnter: function(args){ //args传入参数
var JNIEnv = Java.vm.getEnv();
var originalStrPtr = JNIEnv.getStringUtfChars(args[2], null).readCString();
console.log("参数:", originalStrPtr);
var modifiedContent = "至尊";
var newJString = JNIEnv.newStringUtf(modifiedContent);
args[2] = newJString;
},
//onLeave里可以打印和修改返回值
onLeave: function(retval){ //retval返回值
var returnedJString = Java.cast(retval, Java.use('java.lang.String'));
console.log("返回值:", returnedJString.toString());
var JNIEnv = Java.vm.getEnv();
var modifiedContent = "无敌";
var newJString = JNIEnv.newStringUtf(modifiedContent);
retval.replace(newJString);
}
})
}
})
}
Hook非导出函数¶
对于非导出函数的Hook来说,我们无法像导出函数一样直接使用findExportByName去通过导出函数名直接获取到内存地址,只能通过基址+偏移量的方式来获取非导出函数的地址,从而去Hook该函数,示例代码如下:
var module = Process.getModuleByName("libnative-lib.so");
var funcAddr = module.base.add(0x1234);
Interceptor.attach(funcAddr, {
onEnter: function(args) {
console.log("[+] Hidden function called!");
}
});
Hook示例¶
app-debug¶
示例apk下载地址:JNIapp/app/build/outputs/apk/debug/app-debug.apk at master · erev0s/JNIapp

当点击按钮时,下方回显不同的数字,初步判断逻辑为点击按钮时,随机生成一个随机数并在页面上展示,接下来我们看一下反编译代码
package com.erev0s.jniapp;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
/* loaded from: classes3.dex */
public class MainActivity extends AppCompatActivity {
public native int Jniint();
static {
System.loadLibrary("native-lib");
}
@Override // androidx.appcompat.app.AppCompatActivity, androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = (TextView) findViewById(R.id.onlyYou);
tv.setText("erev0s.com");
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() { // from class: com.erev0s.jniapp.MainActivity.1
@Override // android.view.View.OnClickListener
public void onClick(View view) {
Snackbar.make(view, "Our magic JNI number is :" + String.valueOf(MainActivity.this.Jniint()) + " !", 0).setAction("Action", (View.OnClickListener) null).show();
}
});
}
}
显然String.valueOf(MainActivity.this.Jniint())就是显示的数字,且Jniint是一个native方法,其函数实现在native-lib中,对于这种native方法,显然我们不能像普通方法那样进行hook,我们首先分析这个函数是否为导出函数,我们分析native-lib这个so文件

这是一个导出函数,所以我们可以通过导出函数名直接找到函数地址,在此之前,我们静态分析一下so文件,看一下该函数的实现逻辑

找到jniint函数,查看其反汇编结果
int Jniint(void)
{
int iVar1;
time_t tVar2;
tVar2 = time((time_t *)0x0); // 获取当前 Unix 时间戳(秒)
srand((uint)tVar2); // 用时间戳作为随机数种子
iVar1 = rand(); // 生成一个 0~RAND_MAX 的随机数
return iVar1 % 0x379 + 0x65; // 取模后加偏移,返回特定范围的值
}
计算结果范围:
0x379= 889(十进制)0x65= 101(十进制)- 所以返回值范围:101 ~ 989(包含)
所以该函数作用为返回一个在101-989之间的随机数
到此我们的静态分析就完成了,接下来我们想要这个程序给我们返回一个超过三位数的666666,应该如何使用frida去hook呢,这里有两种做法:
- 从java层考虑,hook com.erev0s.jniapp.MainActivity.Jniint函数,修改其返回值为666666
- 从so/native层考虑,hook Jniint的原生C/C++函数,修改其返回值为666666
第一种方案的frida hook脚本如下:
function run() {
Java.perform(() => {
var MainActivity = Java.use('com.erev0s.jniapp.MainActivity')
MainActivity.Jniint.overload().implementation = function(s) {
console.log('MainActivity.Jniint called');
return 666666;
}
})
}
setImmediate(run)

第二种方案的frida脚本如下:
function hookTest2(){
Java.perform(function(){
//根据导出函数名打印地址
var JniintAddr = Module.findExportByName("libnative-lib.so","Java_com_erev0s_jniapp_MainActivity_Jniint");
console.log(JniintAddr);
if(JniintAddr != null){
//Interceptor.attach是Frida里的一个拦截器
Interceptor.attach(JniintAddr,{
onEnter: function(args){ //args传入参数
// 判断如果参数为空
if (args[1].isNull()) {
console.log("参数为空");
}
},
//onLeave里可以打印和修改返回值
onLeave: function(retval){ //retval返回值
console.log("Original retval: " + String(retval));
// simply replace the value to be returned with 666666
retval.replace(666666);
}
})
}
})
}
setImmediate(hookTest2);

其他Hook小脚本¶
打印某个函数的调用堆栈¶
var threadef = Java.use('java.lang.Thread');
var threadinstance = threadef.$new();
function Where(stack){
var at = ""
for(var i = 0; i < stack.length; ++i){
at += stack[i].toString() + "\n"
}
return at
}
// 关键在于var stack和console打印Where(stack)两句, 其余替换为你想hook的函数即可
function run() {
Java.perform(() => {
var xxx = Java.use('xxx')
RootBeer.xxx.overload().implementation = function() {
var stack = threadinstance.currentThread().getStackTrace();
console.log("Full call stack:" + Where(stack));
}
})
}
setImmediate(run)

印出 stack trace¶
假設某個 app 有檢查機制,會偵測是不是有 root,然後原始碼經過混淆所以比較難追蹤,但是在檢查時會用 Log.d 輸出檢查相關資訊,這時候我們可以 hook Log.d,並且利用 Log.getStackTraceString 輸出 stack trace,就能知道是在哪邊呼叫這個 function:
var Log = Java.use("android.util.Log");
var Exception = Java.use("java.lang.Exception");
Log.d.overload("java.lang.String", "java.lang.String").implementation = function (a, b) {
// 發現輸出 root 偵測資訊的時候
if (b.indexOf('root') >= 0) {
// 印出 stack trace 方便追蹤
console.log(Log.getStackTraceString( Exception.$new()));
}
return this.d.overload("java.lang.String", "java.lang.String").call(this, a, b)
};
打印包调用加载的so文件¶
Java.perform(function() {
var Runtime = Java.use("java.lang.Runtime");
var loadLibrary = Runtime.loadLibrary0.overload('java.lang.Class', 'java.lang.String');
loadLibrary.implementation = function(cls, lib) {
console.log("[+] Loading library: " + lib);
return this.loadLibrary0(cls, lib);
};
});

r0tracer¶
该脚本能批量追踪指定类或对象的所有域值、参数、调用栈和返回值
hook Reflect 相關方法¶
在 Java 中除了直接呼叫方法以外,也可以透過反射(Reflect)的方式去呼叫,有些混淆的程式會大量運用這種技巧來加強靜態分析的難度,我們可以把每一個動態呼叫的方法都印出來,看看有沒有什麼蛛絲馬跡:
// hook Class.forName
var JavaClass = Java.use('java.lang.Class');
JavaClass.forName.overload('java.lang.String', 'boolean', 'java.lang.ClassLoader').implementation = function(name, b, c) {
console.log('Class.forName', name)
// 還可以印出特定 class 底下所有的方法
if (name.indexOf('cymetrics') === 0) {
var TargetClass = Java.use(name);
var methodsList = TargetClass.class.getDeclaredMethods();
for (var k=0; k<methodsList.length; k++){
console.log(methodsList[k].getName());
}
}
return this.forName.overload('java.lang.String', 'boolean', 'java.lang.ClassLoader').call(this, name, b, c)
}
// hook Method.invoke,知道動態呼叫了哪些方法
var Method = Java.use('java.lang.reflect.Method')
Method.invoke.overload('java.lang.Object', '[Ljava.lang.Object;').implementation = function(a,b){
console.log('reflect', a, b)
return this.invoke.call(this,a,b)
}
hook 字串操作¶
有些混淆程式會把程式中寫死的字串全都透過各種步驟打亂,讓人不易搜尋,例如說把字串變成數字然後再還原之類的,而在還原的時候通常都會經過字串操作,這時候我們可以直接去 hook 字串操作,並搭配前面提過的 stack trace 去追蹤:
['java.lang.StringBuilder', 'java.lang.StringBuffer'].forEach(function(clazz, i) {
Java.use(clazz)['toString'].implementation = function() {
var ret = this.toString();
console.log('ret:', ret)
return ret;
}
});
hook 加解密相關操作¶
通常在 Android App 裡面要進行加解密的話,都會透過內建的 API 來進行,像是這樣(來源:Android中的AES加密–上):
public static final String CODE_TYPE = "UTF-8";
public static final String AES_TYPE = "AES/ECB/PKCS5Padding";
private static final String AES_KEY="1111222233334444";
public static String encrypt(String cleartext) {
try {
SecretKeySpec key = new SecretKeySpec(AES_KEY.getBytes(), "AES");
Cipher cipher = Cipher.getInstance(AES_TYPE);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encryptedData = cipher.doFinal(cleartext.getBytes(CODE_TYPE));
return Base64.encodeToString(encryptedData,Base64.DEFAULT);
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
所以只要能 hook 像是 SecretKeySpec 或是 doFinal 這些方法,就能夠攔截到 key 跟加密前的明文。
這篇文章值得一看:How Secure is your Android Keystore Authentication ?,裡面有附了一堆加解密相關的 Frida 腳本,在這裡:https://github.com/FSecureLABS/android-keystore-audit/blob/master/frida-scripts/tracer-cipher.js
話說腳本裡面沒有直接把 byte array 轉成字串,這邊提供一個比較方便的方式(來源:frida小技巧之string与byte转化):
function bytesToString(bytes) {
var javaString = Java.use('java.lang.String');
return javaString.$new(bytes);
}
var Base64 = Java.use('android.util.Base64')
Base64.decode.overload('[B', 'int').implementation = function(a, b) {
console.log(bytesToString(a))
return this.decode.call(this, a, b)
}
Rpc远程调用¶
和js中的Rpc远程调用js函数类似,frida也支持Rpc操作
SSL Pinning¶
之前看到一篇 Defeating Android Certificate Pinning with Frida 裡面有附了一段很讚的腳本,自動去 hook 各種會做 SSL pinning 的 function,讓你繞過這個機制,存一份備份在這裡:https://gist.github.com/aszx87410/f7ae60826d436d8e5bd17deb3e40c249
/* reference: https://httptoolkit.tech/blog/frida-certificate-pinning/
* This script combines, fixes & extends a long list of other scripts, most notably including:
*
* - https://codeshare.frida.re/@akabe1/frida-multiple-unpinning/
* - https://codeshare.frida.re/@avltree9798/universal-android-ssl-pinning-bypass/
* - https://pastebin.com/TVJD63uM
*/
setTimeout(function () {
Java.perform(function () {
console.log("---");
console.log("Unpinning Android app...");
/// -- Generic hook to protect against SSLPeerUnverifiedException -- ///
// In some cases, with unusual cert pinning approaches, or heavy obfuscation, we can't
// match the real method & package names. This is a problem! Fortunately, we can still
// always match built-in types, so here we spot all failures that use the built-in cert
// error type (notably this includes OkHttp), and after the first failure, we dynamically
// generate & inject a patch to completely disable the method that threw the error.
try {
const UnverifiedCertError = Java.use('javax.net.ssl.SSLPeerUnverifiedException');
UnverifiedCertError.$init.implementation = function (str) {
console.log(' --> Unexpected SSL verification failure, adding dynamic patch...');
try {
const stackTrace = Java.use('java.lang.Thread').currentThread().getStackTrace();
const exceptionStackIndex = stackTrace.findIndex(stack =>
stack.getClassName() === "javax.net.ssl.SSLPeerUnverifiedException"
);
const callingFunctionStack = stackTrace[exceptionStackIndex + 1];
const className = callingFunctionStack.getClassName();
const methodName = callingFunctionStack.getMethodName();
console.log(` Thrown by ${className}->${methodName}`);
const callingClass = Java.use(className);
const callingMethod = callingClass[methodName];
if (callingMethod.implementation) return; // Already patched by Frida - skip it
console.log(' Attempting to patch automatically...');
const returnTypeName = callingMethod.returnType.type;
callingMethod.implementation = function () {
console.log(` --> Bypassing ${className}->${methodName} (automatic exception patch)`);
// This is not a perfect fix! Most unknown cases like this are really just
// checkCert(cert) methods though, so doing nothing is perfect, and if we
// do need an actual return value then this is probably the best we can do,
// and at least we're logging the method name so you can patch it manually:
if (returnTypeName === 'void') {
return;
} else {
return null;
}
};
console.log(` [+] ${className}->${methodName} (automatic exception patch)`);
} catch (e) {
console.log(' [ ] Failed to automatically patch failure');
}
return this.$init(str);
};
console.log('[+] SSLPeerUnverifiedException auto-patcher');
} catch (err) {
console.log('[ ] SSLPeerUnverifiedException auto-patcher');
}
/// -- Specific targeted hooks: -- ///
// HttpsURLConnection
try {
const HttpsURLConnection = Java.use("javax.net.ssl.HttpsURLConnection");
HttpsURLConnection.setDefaultHostnameVerifier.implementation = function (hostnameVerifier) {
console.log(' --> Bypassing HttpsURLConnection (setDefaultHostnameVerifier)');
return; // Do nothing, i.e. don't change the hostname verifier
};
console.log('[+] HttpsURLConnection (setDefaultHostnameVerifier)');
} catch (err) {
console.log('[ ] HttpsURLConnection (setDefaultHostnameVerifier)');
}
try {
const HttpsURLConnection = Java.use("javax.net.ssl.HttpsURLConnection");
HttpsURLConnection.setSSLSocketFactory.implementation = function (SSLSocketFactory) {
console.log(' --> Bypassing HttpsURLConnection (setSSLSocketFactory)');
return; // Do nothing, i.e. don't change the SSL socket factory
};
console.log('[+] HttpsURLConnection (setSSLSocketFactory)');
} catch (err) {
console.log('[ ] HttpsURLConnection (setSSLSocketFactory)');
}
try {
const HttpsURLConnection = Java.use("javax.net.ssl.HttpsURLConnection");
HttpsURLConnection.setHostnameVerifier.implementation = function (hostnameVerifier) {
console.log(' --> Bypassing HttpsURLConnection (setHostnameVerifier)');
return; // Do nothing, i.e. don't change the hostname verifier
};
console.log('[+] HttpsURLConnection (setHostnameVerifier)');
} catch (err) {
console.log('[ ] HttpsURLConnection (setHostnameVerifier)');
}
// SSLContext
try {
const X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
const SSLContext = Java.use('javax.net.ssl.SSLContext');
const TrustManager = Java.registerClass({
// Implement a custom TrustManager
name: 'dev.asd.test.TrustManager',
implements: [X509TrustManager],
methods: {
checkClientTrusted: function (chain, authType) { },
checkServerTrusted: function (chain, authType) { },
getAcceptedIssuers: function () { return []; }
}
});
// Prepare the TrustManager array to pass to SSLContext.init()
const TrustManagers = [TrustManager.$new()];
// Get a handle on the init() on the SSLContext class
const SSLContext_init = SSLContext.init.overload(
'[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom'
);
// Override the init method, specifying the custom TrustManager
SSLContext_init.implementation = function (keyManager, trustManager, secureRandom) {
console.log(' --> Bypassing Trustmanager (Android < 7) request');
SSLContext_init.call(this, keyManager, TrustManagers, secureRandom);
};
console.log('[+] SSLContext');
} catch (err) {
console.log('[ ] SSLContext');
}
// TrustManagerImpl (Android > 7)
try {
const array_list = Java.use("java.util.ArrayList");
const TrustManagerImpl = Java.use('com.android.org.conscrypt.TrustManagerImpl');
// This step is notably what defeats the most common case: network security config
TrustManagerImpl.checkTrustedRecursive.implementation = function(a1, a2, a3, a4, a5, a6) {
console.log(' --> Bypassing TrustManagerImpl checkTrusted ');
return array_list.$new();
}
TrustManagerImpl.verifyChain.implementation = function (untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData) {
console.log(' --> Bypassing TrustManagerImpl verifyChain: ' + host);
return untrustedChain;
};
console.log('[+] TrustManagerImpl');
} catch (err) {
console.log('[ ] TrustManagerImpl');
}
// OkHTTPv3 (quadruple bypass)
try {
// Bypass OkHTTPv3 {1}
const okhttp3_Activity_1 = Java.use('okhttp3.CertificatePinner');
okhttp3_Activity_1.check.overload('java.lang.String', 'java.util.List').implementation = function (a, b) {
console.log(' --> Bypassing OkHTTPv3 (list): ' + a);
return;
};
console.log('[+] OkHTTPv3 (list)');
} catch (err) {
console.log('[ ] OkHTTPv3 (list)');
}
try {
// Bypass OkHTTPv3 {2}
// This method of CertificatePinner.check could be found in some old Android app
const okhttp3_Activity_2 = Java.use('okhttp3.CertificatePinner');
okhttp3_Activity_2.check.overload('java.lang.String', 'java.security.cert.Certificate').implementation = function (a, b) {
console.log(' --> Bypassing OkHTTPv3 (cert): ' + a);
return;
};
console.log('[+] OkHTTPv3 (cert)');
} catch (err) {
console.log('[ ] OkHTTPv3 (cert)');
}
try {
// Bypass OkHTTPv3 {3}
const okhttp3_Activity_3 = Java.use('okhttp3.CertificatePinner');
okhttp3_Activity_3.check.overload('java.lang.String', '[Ljava.security.cert.Certificate;').implementation = function (a, b) {
console.log(' --> Bypassing OkHTTPv3 (cert array): ' + a);
return;
};
console.log('[+] OkHTTPv3 (cert array)');
} catch (err) {
console.log('[ ] OkHTTPv3 (cert array)');
}
try {
// Bypass OkHTTPv3 {4}
const okhttp3_Activity_4 = Java.use('okhttp3.CertificatePinner');
okhttp3_Activity_4['check$okhttp'].implementation = function (a, b) {
console.log(' --> Bypassing OkHTTPv3 ($okhttp): ' + a);
return;
};
console.log('[+] OkHTTPv3 ($okhttp)');
} catch (err) {
console.log('[ ] OkHTTPv3 ($okhttp)');
}
// Trustkit (triple bypass)
try {
// Bypass Trustkit {1}
const trustkit_Activity_1 = Java.use('com.datatheorem.android.trustkit.pinning.OkHostnameVerifier');
trustkit_Activity_1.verify.overload('java.lang.String', 'javax.net.ssl.SSLSession').implementation = function (a, b) {
console.log(' --> Bypassing Trustkit OkHostnameVerifier(SSLSession): ' + a);
return true;
};
console.log('[+] Trustkit OkHostnameVerifier(SSLSession)');
} catch (err) {
console.log('[ ] Trustkit OkHostnameVerifier(SSLSession)');
}
try {
// Bypass Trustkit {2}
const trustkit_Activity_2 = Java.use('com.datatheorem.android.trustkit.pinning.OkHostnameVerifier');
trustkit_Activity_2.verify.overload('java.lang.String', 'java.security.cert.X509Certificate').implementation = function (a, b) {
console.log(' --> Bypassing Trustkit OkHostnameVerifier(cert): ' + a);
return true;
};
console.log('[+] Trustkit OkHostnameVerifier(cert)');
} catch (err) {
console.log('[ ] Trustkit OkHostnameVerifier(cert)');
}
try {
// Bypass Trustkit {3}
const trustkit_PinningTrustManager = Java.use('com.datatheorem.android.trustkit.pinning.PinningTrustManager');
trustkit_PinningTrustManager.checkServerTrusted.implementation = function () {
console.log(' --> Bypassing Trustkit PinningTrustManager');
};
console.log('[+] Trustkit PinningTrustManager');
} catch (err) {
console.log('[ ] Trustkit PinningTrustManager');
}
// Appcelerator Titanium
try {
const appcelerator_PinningTrustManager = Java.use('appcelerator.https.PinningTrustManager');
appcelerator_PinningTrustManager.checkServerTrusted.implementation = function () {
console.log(' --> Bypassing Appcelerator PinningTrustManager');
};
console.log('[+] Appcelerator PinningTrustManager');
} catch (err) {
console.log('[ ] Appcelerator PinningTrustManager');
}
// OpenSSLSocketImpl Conscrypt
try {
const OpenSSLSocketImpl = Java.use('com.android.org.conscrypt.OpenSSLSocketImpl');
OpenSSLSocketImpl.verifyCertificateChain.implementation = function (certRefs, JavaObject, authMethod) {
console.log(' --> Bypassing OpenSSLSocketImpl Conscrypt');
};
console.log('[+] OpenSSLSocketImpl Conscrypt');
} catch (err) {
console.log('[ ] OpenSSLSocketImpl Conscrypt');
}
// OpenSSLEngineSocketImpl Conscrypt
try {
const OpenSSLEngineSocketImpl_Activity = Java.use('com.android.org.conscrypt.OpenSSLEngineSocketImpl');
OpenSSLEngineSocketImpl_Activity.verifyCertificateChain.overload('[Ljava.lang.Long;', 'java.lang.String').implementation = function (a, b) {
console.log(' --> Bypassing OpenSSLEngineSocketImpl Conscrypt: ' + b);
};
console.log('[+] OpenSSLEngineSocketImpl Conscrypt');
} catch (err) {
console.log('[ ] OpenSSLEngineSocketImpl Conscrypt');
}
// OpenSSLSocketImpl Apache Harmony
try {
const OpenSSLSocketImpl_Harmony = Java.use('org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl');
OpenSSLSocketImpl_Harmony.verifyCertificateChain.implementation = function (asn1DerEncodedCertificateChain, authMethod) {
console.log(' --> Bypassing OpenSSLSocketImpl Apache Harmony');
};
console.log('[+] OpenSSLSocketImpl Apache Harmony');
} catch (err) {
console.log('[ ] OpenSSLSocketImpl Apache Harmony');
}
// PhoneGap sslCertificateChecker (https://github.com/EddyVerbruggen/SSLCertificateChecker-PhoneGap-Plugin)
try {
const phonegap_Activity = Java.use('nl.xservices.plugins.sslCertificateChecker');
phonegap_Activity.execute.overload('java.lang.String', 'org.json.JSONArray', 'org.apache.cordova.CallbackContext').implementation = function (a, b, c) {
console.log(' --> Bypassing PhoneGap sslCertificateChecker: ' + a);
return true;
};
console.log('[+] PhoneGap sslCertificateChecker');
} catch (err) {
console.log('[ ] PhoneGap sslCertificateChecker');
}
// IBM MobileFirst pinTrustedCertificatePublicKey (double bypass)
try {
// Bypass IBM MobileFirst {1}
const WLClient_Activity_1 = Java.use('com.worklight.wlclient.api.WLClient');
WLClient_Activity_1.getInstance().pinTrustedCertificatePublicKey.overload('java.lang.String').implementation = function (cert) {
console.log(' --> Bypassing IBM MobileFirst pinTrustedCertificatePublicKey (string): ' + cert);
return;
};
console.log('[+] IBM MobileFirst pinTrustedCertificatePublicKey (string)');
} catch (err) {
console.log('[ ] IBM MobileFirst pinTrustedCertificatePublicKey (string)');
}
try {
// Bypass IBM MobileFirst {2}
const WLClient_Activity_2 = Java.use('com.worklight.wlclient.api.WLClient');
WLClient_Activity_2.getInstance().pinTrustedCertificatePublicKey.overload('[Ljava.lang.String;').implementation = function (cert) {
console.log(' --> Bypassing IBM MobileFirst pinTrustedCertificatePublicKey (string array): ' + cert);
return;
};
console.log('[+] IBM MobileFirst pinTrustedCertificatePublicKey (string array)');
} catch (err) {
console.log('[ ] IBM MobileFirst pinTrustedCertificatePublicKey (string array)');
}
// IBM WorkLight (ancestor of MobileFirst) HostNameVerifierWithCertificatePinning (quadruple bypass)
try {
// Bypass IBM WorkLight {1}
const worklight_Activity_1 = Java.use('com.worklight.wlclient.certificatepinning.HostNameVerifierWithCertificatePinning');
worklight_Activity_1.verify.overload('java.lang.String', 'javax.net.ssl.SSLSocket').implementation = function (a, b) {
console.log(' --> Bypassing IBM WorkLight HostNameVerifierWithCertificatePinning (SSLSocket): ' + a);
return;
};
console.log('[+] IBM WorkLight HostNameVerifierWithCertificatePinning (SSLSocket)');
} catch (err) {
console.log('[ ] IBM WorkLight HostNameVerifierWithCertificatePinning (SSLSocket)');
}
try {
// Bypass IBM WorkLight {2}
const worklight_Activity_2 = Java.use('com.worklight.wlclient.certificatepinning.HostNameVerifierWithCertificatePinning');
worklight_Activity_2.verify.overload('java.lang.String', 'java.security.cert.X509Certificate').implementation = function (a, b) {
console.log(' --> Bypassing IBM WorkLight HostNameVerifierWithCertificatePinning (cert): ' + a);
return;
};
console.log('[+] IBM WorkLight HostNameVerifierWithCertificatePinning (cert)');
} catch (err) {
console.log('[ ] IBM WorkLight HostNameVerifierWithCertificatePinning (cert)');
}
try {
// Bypass IBM WorkLight {3}
const worklight_Activity_3 = Java.use('com.worklight.wlclient.certificatepinning.HostNameVerifierWithCertificatePinning');
worklight_Activity_3.verify.overload('java.lang.String', '[Ljava.lang.String;', '[Ljava.lang.String;').implementation = function (a, b) {
console.log(' --> Bypassing IBM WorkLight HostNameVerifierWithCertificatePinning (string string): ' + a);
return;
};
console.log('[+] IBM WorkLight HostNameVerifierWithCertificatePinning (string string)');
} catch (err) {
console.log('[ ] IBM WorkLight HostNameVerifierWithCertificatePinning (string string)');
}
try {
// Bypass IBM WorkLight {4}
const worklight_Activity_4 = Java.use('com.worklight.wlclient.certificatepinning.HostNameVerifierWithCertificatePinning');
worklight_Activity_4.verify.overload('java.lang.String', 'javax.net.ssl.SSLSession').implementation = function (a, b) {
console.log(' --> Bypassing IBM WorkLight HostNameVerifierWithCertificatePinning (SSLSession): ' + a);
return true;
};
console.log('[+] IBM WorkLight HostNameVerifierWithCertificatePinning (SSLSession)');
} catch (err) {
console.log('[ ] IBM WorkLight HostNameVerifierWithCertificatePinning (SSLSession)');
}
// Conscrypt CertPinManager
try {
const conscrypt_CertPinManager_Activity = Java.use('com.android.org.conscrypt.CertPinManager');
conscrypt_CertPinManager_Activity.isChainValid.overload('java.lang.String', 'java.util.List').implementation = function (a, b) {
console.log(' --> Bypassing Conscrypt CertPinManager: ' + a);
return true;
};
console.log('[+] Conscrypt CertPinManager');
} catch (err) {
console.log('[ ] Conscrypt CertPinManager');
}
// CWAC-Netsecurity (unofficial back-port pinner for Android<4.2) CertPinManager
try {
const cwac_CertPinManager_Activity = Java.use('com.commonsware.cwac.netsecurity.conscrypt.CertPinManager');
cwac_CertPinManager_Activity.isChainValid.overload('java.lang.String', 'java.util.List').implementation = function (a, b) {
console.log(' --> Bypassing CWAC-Netsecurity CertPinManager: ' + a);
return true;
};
console.log('[+] CWAC-Netsecurity CertPinManager');
} catch (err) {
console.log('[ ] CWAC-Netsecurity CertPinManager');
}
// Worklight Androidgap WLCertificatePinningPlugin
try {
const androidgap_WLCertificatePinningPlugin_Activity = Java.use('com.worklight.androidgap.plugin.WLCertificatePinningPlugin');
androidgap_WLCertificatePinningPlugin_Activity.execute.overload('java.lang.String', 'org.json.JSONArray', 'org.apache.cordova.CallbackContext').implementation = function (a, b, c) {
console.log(' --> Bypassing Worklight Androidgap WLCertificatePinningPlugin: ' + a);
return true;
};
console.log('[+] Worklight Androidgap WLCertificatePinningPlugin');
} catch (err) {
console.log('[ ] Worklight Androidgap WLCertificatePinningPlugin');
}
// Netty FingerprintTrustManagerFactory
try {
const netty_FingerprintTrustManagerFactory = Java.use('io.netty.handler.ssl.util.FingerprintTrustManagerFactory');
netty_FingerprintTrustManagerFactory.checkTrusted.implementation = function (type, chain) {
console.log(' --> Bypassing Netty FingerprintTrustManagerFactory');
};
console.log('[+] Netty FingerprintTrustManagerFactory');
} catch (err) {
console.log('[ ] Netty FingerprintTrustManagerFactory');
}
// Squareup CertificatePinner [OkHTTP<v3] (double bypass)
try {
// Bypass Squareup CertificatePinner {1}
const Squareup_CertificatePinner_Activity_1 = Java.use('com.squareup.okhttp.CertificatePinner');
Squareup_CertificatePinner_Activity_1.check.overload('java.lang.String', 'java.security.cert.Certificate').implementation = function (a, b) {
console.log(' --> Bypassing Squareup CertificatePinner (cert): ' + a);
return;
};
console.log('[+] Squareup CertificatePinner (cert)');
} catch (err) {
console.log('[ ] Squareup CertificatePinner (cert)');
}
try {
// Bypass Squareup CertificatePinner {2}
const Squareup_CertificatePinner_Activity_2 = Java.use('com.squareup.okhttp.CertificatePinner');
Squareup_CertificatePinner_Activity_2.check.overload('java.lang.String', 'java.util.List').implementation = function (a, b) {
console.log(' --> Bypassing Squareup CertificatePinner (list): ' + a);
return;
};
console.log('[+] Squareup CertificatePinner (list)');
} catch (err) {
console.log('[ ] Squareup CertificatePinner (list)');
}
// Squareup OkHostnameVerifier [OkHTTP v3] (double bypass)
try {
// Bypass Squareup OkHostnameVerifier {1}
const Squareup_OkHostnameVerifier_Activity_1 = Java.use('com.squareup.okhttp.internal.tls.OkHostnameVerifier');
Squareup_OkHostnameVerifier_Activity_1.verify.overload('java.lang.String', 'java.security.cert.X509Certificate').implementation = function (a, b) {
console.log(' --> Bypassing Squareup OkHostnameVerifier (cert): ' + a);
return true;
};
console.log('[+] Squareup OkHostnameVerifier (cert)');
} catch (err) {
console.log('[ ] Squareup OkHostnameVerifier (cert)');
}
try {
// Bypass Squareup OkHostnameVerifier {2}
const Squareup_OkHostnameVerifier_Activity_2 = Java.use('com.squareup.okhttp.internal.tls.OkHostnameVerifier');
Squareup_OkHostnameVerifier_Activity_2.verify.overload('java.lang.String', 'javax.net.ssl.SSLSession').implementation = function (a, b) {
console.log(' --> Bypassing Squareup OkHostnameVerifier (SSLSession): ' + a);
return true;
};
console.log('[+] Squareup OkHostnameVerifier (SSLSession)');
} catch (err) {
console.log('[ ] Squareup OkHostnameVerifier (SSLSession)');
}
// Android WebViewClient (double bypass)
try {
// Bypass WebViewClient {1} (deprecated from Android 6)
const AndroidWebViewClient_Activity_1 = Java.use('android.webkit.WebViewClient');
AndroidWebViewClient_Activity_1.onReceivedSslError.overload('android.webkit.WebView', 'android.webkit.SslErrorHandler', 'android.net.http.SslError').implementation = function (obj1, obj2, obj3) {
console.log(' --> Bypassing Android WebViewClient (SslErrorHandler)');
};
console.log('[+] Android WebViewClient (SslErrorHandler)');
} catch (err) {
console.log('[ ] Android WebViewClient (SslErrorHandler)');
}
try {
// Bypass WebViewClient {2}
const AndroidWebViewClient_Activity_2 = Java.use('android.webkit.WebViewClient');
AndroidWebViewClient_Activity_2.onReceivedSslError.overload('android.webkit.WebView', 'android.webkit.WebResourceRequest', 'android.webkit.WebResourceError').implementation = function (obj1, obj2, obj3) {
console.log(' --> Bypassing Android WebViewClient (WebResourceError)');
};
console.log('[+] Android WebViewClient (WebResourceError)');
} catch (err) {
console.log('[ ] Android WebViewClient (WebResourceError)');
}
// Apache Cordova WebViewClient
try {
const CordovaWebViewClient_Activity = Java.use('org.apache.cordova.CordovaWebViewClient');
CordovaWebViewClient_Activity.onReceivedSslError.overload('android.webkit.WebView', 'android.webkit.SslErrorHandler', 'android.net.http.SslError').implementation = function (obj1, obj2, obj3) {
console.log(' --> Bypassing Apache Cordova WebViewClient');
obj3.proceed();
};
} catch (err) {
console.log('[ ] Apache Cordova WebViewClient');
}
// Boye AbstractVerifier
try {
const boye_AbstractVerifier = Java.use('ch.boye.httpclientandroidlib.conn.ssl.AbstractVerifier');
boye_AbstractVerifier.verify.implementation = function (host, ssl) {
console.log(' --> Bypassing Boye AbstractVerifier: ' + host);
};
} catch (err) {
console.log('[ ] Boye AbstractVerifier');
}
console.log("Unpinning setup completed");
console.log("---");
});
}, 0);
存檔以後這樣跑起來:
Frida检测与绕过¶
检测方案¶
进程和文件检测¶
- 检测 Frida 进程:执行
ps命令,查找包含"frida"或"gadget"的进程。 - 检测 Frida 文件:检查以下路径是否存在相关文件:
/data/local/tmp/frida-server/data/local/tmp/gadget/system/lib/libfrida-gadget.so/system/lib64/libfrida-gadget.so
端口和网络检测¶
- 默认端口检测:尝试连接
127.0.0.1:27042(Frida 默认通信端口)。 - 网络连接检测:执行
netstat -an,查找包含"27042"或"frida"的连接。
Hook 检测¶
- Java 层 Hook 检测:
- 检查关键方法(如
String.equals())的declaringClass是否被篡改。 - 检查异常堆栈中是否包含
"frida"相关类名。 - Native 层 Hook 检测:
- 检查函数入口是否被插入
JMP指令(如0xE9、0xEB)。
线程检测¶
- Frida 线程名检测:枚举线程,查找包含以下关键词的线程名:
"frida"、"gadget"、"gum-js-loop"- 线程数量异常检测:若线程数 > 100,视为可疑。
文件描述符(FD)检测¶
- FD 文件检测:执行
ls -la /proc/self/fd,查找包含"frida"或"gadget"的条目。 - 网络 FD 检测:执行
lsof -p <pid>,查找连接到:27042的 TCP 连接。
Trace 状态检查(调试器检测)¶
- TracerPid 检测:读取
/proc/self/status,若TracerPid != 0,说明被调试。 - wchan 检测:读取
/proc/self/wchan,若包含"ptrace",说明被附加。
内存检测¶
- 内存映射检测:读取
/proc/self/maps,查找包含"frida"或"gadget"的内存段。 - 内存完整性检测:计算关键代码段哈希,与预期值比对(检测是否被 Inline Hook)。
绕过方案¶
绕过进程 & 文件检测¶
- 进程名伪装:启动 frida-server 时用
--name指定伪装进程名
-
文件路径隐藏:将 frida 相关文件放入 App 私有目录(如
/data/data/.../lib/) -
动态加载:运行时解密并加载 Frida 脚本,避免静态文件存在
绕过端口 & 网络检测¶
- 端口随机化:使用非 27042 端口通信
-
远程代理:通过
frida.get_device_manager().add_remote_device("IP:PORT")转发 -
本地通信:使用文件、Unix Domain Socket 等替代 TCP
-
Port Scanning / Fingerprinting Bypass:
By default, Frida communicates with the client using WebSocket. In some case, an app will pretend to be a WebSocket and act like a client. To bypass or detect this behavior, we can set the ORIGIN to any string.
# On frida-server
frida-server -l 0.0.0.0:27043 --origin=AKANE
# On client
frida-ps -H device_ip:27043 --origin=AKANE
Alternatively, we can set CERTIFICATE to make sure the frida communication is only performed in TLS.
# On frida-server
frida-server -l 0.0.0.0:27043 --certificate=certificate.pem
# On client
frida-ps -H device_ip:27043 --certificate=certificate.pem
绕过 Hook 检测¶
- Java Hook 绕过:Hook 检测函数,返回虚假结果
Java.use("java.io.File").exists.implementation = function() {
if (this.getAbsolutePath().includes("frida")) return false;
return this.exists();
};
-
Native Hook 绕过:
-
恢复原始函数头 5 字节(解除 Inline Hook)
- 使用更隐蔽的 Hook 技术(如 Got 表 Hook)
绕过线程检测¶
- 线程名伪装:Hook
Thread构造函数,替换含"frida"的线程名 - 线程数量伪装:Hook
Thread.activeCount(),返回正常值(如 30)
绕过 FD 检测¶
- Hook 文件读取:拦截
/proc/self/fd读取,过滤含"frida"的行 - Hook
lsof执行:返回空结果或过滤 27042 端口
绕过 Trace 状态检测¶
-
伪造
/proc/self/status:HookFileInputStream.read(),将TracerPid: xxx替换为TracerPid: 0 -
伪造
wchan:将"ptrace"替换为正常系统调用(如"sys_epoll_wait") -
Hook 调试检测 API:
绕过内存检测¶
- 内存映射隐藏:Hook
/proc/self/maps读取,过滤"frida"相关行 - 内存完整性绕过:
- Patch 掉检测函数逻辑
- 使用
mprotect保护关键内存区域