跳转至

Frida的使用

frida-ps

Bash
# 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

image-20250521221726897

frida-trace

Bash
# 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.

Text Only
frida-ls-devices

image-20250521224638713

frida-kill

This is a command-line tool for killing processes.

Bash
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
<?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

JavaScript
// 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

Bash
frida -U --no-pause -l script.js -f "com.cymetrics.demo"

image-20251215223649392

如果存在报错: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文件,下载完成后将其移动到对应路径下即可

image-20251215230744786

image-20251215230807816

此时成功打印出日志,并且app处于崩溃状态,这是因為我們自己實作的 function 除了 log 以外什麼都沒做,也就是說原本的 onCreate 該做的事情都被拿掉了,所以 crash 也是合情合理

frida hook脚本基本结构如下:

JavaScript
function run() {
  Java.perform(() => {
    // 程式碼放這邊
  })
}

setImmediate(run)

接著就看你想要 hook 什麼樣的方法,以我們剛剛的程式碼來說,先用 Java.use 拿到想要 hook 的 class,再用 MainActivity.onCreate.implementation 把原本的實作換成我們自己定義的 function。

如果我们想要程序不崩溃,只需要和原有逻辑保持一致即可

JavaScript
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,後面傳入參數。

image-20251217224405360

Bash
Error: onCreate(): argument types do not match any of:
  .overload('android.os.Bundle')

這是因為 onCreate 其實應該是有帶參數的,只是我們覆蓋的時候沒有接收參數,因此就出錯了。為了避免這個問題,我會建議在覆蓋實作的時候在前面加上 .overload(),像這樣:

JavaScript
MainActivity.onCreate.overload().implementation = function() {

}

这样frida就会在出错时提示正确参数是什么

image-20251217224646750

我们按照frida的提示加上android.os.Bundle,并作为参数传入onCreate函数

JavaScript
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)

image-20251217224915955

此时就能正常注入了,此时我们也可以注入其他代码,只要最后调用原有逻辑即可

JavaScript
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)

image-20251217225322745

比如上处hook是弹窗Hello!

Hook构造函数

示例如下:

JavaScript
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区别如下:

JavaScript
// 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

  1. 构造函数调用时机:如果目标类的构造函数在应用启动时已经调用,后续 Hook 将无法捕获。需要在应用启动前注入脚本。
  2. 注入方式:使用 frida -U -n -l 或 frida -U -f -n 注入脚本。
  3. 参数类型匹配:确保 .overload 中的参数类型与目标构造函数完全一致,否则会导致 Hook 失败。

Hook示例

绕过Root检测

示例apk下载地址:https://github.com/aszx87410/demo/raw/master/android/demoapp.apk

这个app会简单检测设备是否已经root

image-20251217225611976

image-20251217225745296

反编译发现其关键函数为com.scottyab.rootbeer.RootBeer.isRooted函数,在com.cymetrics.demo.FristFragment中会对该函数的返回值进行判断,为True就表示检测到root,我们直接hook这个函数:

JavaScript
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)

image-20251217230427411

image-20251217230451172

此时已经成功绕过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 绑定属性

  • 如何识别:

Bash
readelf -Ws libexample.so | grep FUNC  # 查看所有符号
nm -D libexample.so                    # 只显示导出符号

输出中带T(全局函数)或W(弱符号)的即为导出函数:

Text Only
00001234 T Java_com_example_decrypt

导出函数 = 可被动态链接器解析的函数,例如 JNI 函数、__attribute__((visibility("default"))) 的函数。

非导出函数(Non-Exported)

  • 定义:**仅在当前 .so 内部使用**的函数,不对外暴露。

  • 常见类型:

  • 用 static 修饰的 C 函数

  • 用 __attribute__((visibility("hidden"))) 显式隐藏的函数
  • 编译时未加 -fvisibility=default 的函数(默认 hidden)

  • ELF 结构:

  • 不在 .dynsym 中(动态链接器看不到)

  • 可能在 .symtab(静态符号表) 中(调试符号,Release 版常被 strip)
  • 函数名在 .strtab 中(但 Release 版常被移除)

  • 如何识别:

Bash
readelf -Ws libexample.so   # 看不到(因为不在 .dynsym)
objdump -t libexample.so    # 可能在 .symtab(如果未 strip)

⚠️ Release 版 .so 通常会 strip 掉 .symtab 和 .strtab,导致非导出函数**既无名也无符号**,只能通过地址或特征定位。

基础编写

Hook导出函数

基础Hook打印
  1. 整数型、布尔值类型、char类型
Java
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());
                }
            })
        }
    })
}
  1. 字符串类型
JavaScript
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修改
  1. 整数型修改
JavaScript
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());
                }
            })
        }
    })
}
  1. 字符串类型修改
JavaScript
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该函数,示例代码如下:

JavaScript
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

image-20251225214841888

当点击按钮时,下方回显不同的数字,初步判断逻辑为点击按钮时,随机生成一个随机数并在页面上展示,接下来我们看一下反编译代码

Java
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文件

Bash
nm --demangle --dynamic libnative-lib.so

image-20251225215635062

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

image-20251225221338773

找到jniint函数,查看其反汇编结果

Java
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呢,这里有两种做法:

  1. 从java层考虑,hook com.erev0s.jniapp.MainActivity.Jniint函数,修改其返回值为666666
  2. 从so/native层考虑,hook Jniint的原生C/C++函数,修改其返回值为666666

第一种方案的frida hook脚本如下:

JavaScript
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)

image-20251225222922936

第二种方案的frida脚本如下:

JavaScript
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);

image-20251225223814220

其他Hook小脚本

打印某个函数的调用堆栈

JavaScript
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)

image-20251219201537008

印出 stack trace

假設某個 app 有檢查機制,會偵測是不是有 root,然後原始碼經過混淆所以比較難追蹤,但是在檢查時會用 Log.d 輸出檢查相關資訊,這時候我們可以 hook Log.d,並且利用 Log.getStackTraceString 輸出 stack trace,就能知道是在哪邊呼叫這個 function:

JavaScript
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文件

JavaScript
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);
    };
});

image-20251226202038189

r0tracer

github:r0ysue/r0tracer: 安卓Java层多功能追踪脚本

该脚本能批量追踪指定类或对象的所有域值、参数、调用栈和返回值

hook Reflect 相關方法

在 Java 中除了直接呼叫方法以外,也可以透過反射(Reflect)的方式去呼叫,有些混淆的程式會大量運用這種技巧來加強靜態分析的難度,我們可以把每一個動態呼叫的方法都印出來,看看有沒有什麼蛛絲馬跡:

JavaScript
// 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 去追蹤:

JavaScript
['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加密–上):

Java
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转化):

JavaScript
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操作

参考文章:frida_rpc算法转发实战 | Muhe's Blog

SSL Pinning

之前看到一篇 Defeating Android Certificate Pinning with Frida 裡面有附了一段很讚的腳本,自動去 hook 各種會做 SSL pinning 的 function,讓你繞過這個機制,存一份備份在這裡:https://gist.github.com/aszx87410/f7ae60826d436d8e5bd17deb3e40c249

JavaScript
/* 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);

存檔以後這樣跑起來:

Text Only
frida -U -l ssl.js -f "com.example"

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 指定伪装进程名
Bash
frida-server --name "com.example.app"
  • 文件路径隐藏:将 frida 相关文件放入 App 私有目录(如 /data/data/.../lib/)

  • 动态加载:运行时解密并加载 Frida 脚本,避免静态文件存在

绕过端口 & 网络检测

  • 端口随机化:使用非 27042 端口通信
Bash
port = random.randint(30000, 60000)
# 监听除27042外任意端口
frida-server -l 0.0.0.0:<any_port>
  • 远程代理:通过 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.

Bash
# 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.

Bash
# 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 检测函数,返回虚假结果
JavaScript
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:Hook FileInputStream.read(),将 TracerPid: xxx 替换为 TracerPid: 0

  • 伪造 wchan:将 "ptrace" 替换为正常系统调用(如 "sys_epoll_wait")

  • Hook 调试检测 API:

JavaScript
Java.use("android.os.Debug").isDebuggerConnected.implementation = () => return false;

绕过内存检测

  • 内存映射隐藏:Hook /proc/self/maps 读取,过滤 "frida" 相关行
  • 内存完整性绕过:
  • Patch 掉检测函数逻辑
  • 使用 mprotect 保护关键内存区域

参考文章

Frida特征检测及对应的绕过技术解析 | 一枚酸心果子

aimardcr/FridaDetectionBypass