Frida用法詳解【附用例】

Frida,是一個類似 Xposed/Substrate 的跨平臺動態插裝工具,藉助 python 和 javascript,可以非常快速便捷地操作目標進程、修改函數參數返回值等,支持 Android/iOS/Macos/Linux/Windows,參考官網

0x01 工具安裝

有兩種安裝方式,命令行直接安裝

# python 支持
$ pip3 install frida
# CLI命令行界面工具,支持 frida、frida-ls-devices、frida-ps、frida-kill、frida-trace、frida-discover
$ pip3 install frida-tools

或者直接從源碼編譯

$ git clone git://github.com/frida/frida.git
$ cd frida
# Linux 下直接 make 編譯
# Macos 或 iOS 下需要安裝證書,然後導出證書名指定爲環境變量,再編譯
$ export MAC_CERTID=frida-cert/export IOS_CERTID=frida-cert
$ make
# 確保系統信任新安裝的證書,需要重啓一下 taskgated 守護進程
$ sudo killall taskgated   

0x02 設備環境配置(Android)

原理:手機端安裝一個server程序,將手機端的端口轉發到PC端,PC端通過python腳本進行交互,腳本內部又是通過javascript語言來寫代碼執行操作

frida-server下載地址,注意對應版本和支持平臺,可以使用下面的命令查看設備abi信息

$ adb shell getprop | grep abi
[ro.product.cpu.abi]: [arm64-v8a]
[ro.product.cpu.abilist]: [arm64-v8a,armeabi-v7a,armeabi]
[ro.product.cpu.abilist32]: [armeabi-v7a,armeabi]
[ro.product.cpu.abilist64]: [arm64-v8a]

下載server程序並解壓縮,然後推送到手機內部存儲路徑

$ curl -O https://github.com/frida/frida/releases/download/12.8.1/frida-server-12.8.1-android-arm64.xz
$ xz -d frida-server-12.8.1-android-arm64.xz
$ adb push frida-server-12.8.1-android-arm64 /data/local/tmp/frida-server

修改權限運行

blueline:/ # chmod 777 frida-server
blueline:/ # ./frida-server

跟IDA一下,需要對端口進行轉發,默認使用27042端口與frida-server通信

$ adb forward tcp:27042 tcp:27042

然後可以運行ps查看手機端進程列表

$ frida-ps -R
  PID  Name
-----  ---------------------------------------------------
  581  ATFWD-daemon
29920  adbd
  586  [email protected]
  398  [email protected]
  399  [email protected]
  400  [email protected]
  401  [email protected]
  556  audioserver
  557  cameraserver
  ......

附加某個進程

$ frida -R com.demo.fridahook
     ____
    / _  |   Frida 12.8.1 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://www.frida.re/docs/home/

[Remote::com.demo.fridahook]->

對於非root設備,網上也給出了方法,需要反編譯目標應用包,然後注入frida-gadget,參考這裏【待驗證】

下載對應版本和平臺的工具,frida-gadget下載地址,修改命名爲libfrida-gadget.so,放到反編譯包/lib/目錄下

從AndroidManifest.xml文件中找到應用入口文件,在較爲靠前的函數中加載動態庫

const-string v0, "frida-gadget"
invoke-static {v0}, Ljava/lang/System;>loadLibrary(Ljava/lang/String;)V

另外還需要檢查下網絡權限,如沒有需要添加

<uses-permission android:name="android.permission.INTERNET" />

回編譯應用包,並簽名安裝,查看會顯示Gadget進程

$> frida-ps -U
  PID  Name
-----  ------
16251  Gadget

執行命令進行連接

frida -U gadget

安裝objection,自動完成frida gadget注入到apk中

$ pip3 install -U objection
$ objection patchapk -s target_app.apk

0x03 命令行運行

命令行附加應用進程然後敲代碼進程注入

$ frida -U -f com.demo.fridahook
     ____
    / _  |   Frida 12.8.1 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://www.frida.re/docs/home/
Spawned `com.demo.fridahook`. Use %resume to let the main thread start executing!
[LGE Nexus 5X::com.demo.fridahook]-> Java                                                                                                                                                                  
{
    "androidVersion": "8.1.0",
    "available": true
}
[LGE Nexus 5X::com.demo.fridahook]->  

插裝指定函數,會在當前目錄__handlers__文件夾下生成open.js腳本

$ frida-trace -i open -U com.demo.fridahook

或者直接加載js腳本

$ frida -U/-R -l <script_file> --no-pause -f <process_name/pid>

js腳本用法示例

// javascript
// 將線程附加到Java虛擬機,成功會回調function()
Java.perform(function() {});
// 由於js代碼注入存在超時,通常在外面再包裝一層
setImmediate(function() {
    ...
});

// 動態獲取一個js包裝的Java類
var clazz = Java.use(className);
// $new()調用類構造方法,$dispose()調用析構清空js對象
// 獲取到Java類之後,直接通過<wrapper>.<method>獲取方法
// 有些重載的方法需要通過.overload()傳入參數類型指定方法
// 通過方法的.call(args)來調用函數,通過方法的.implementations = function(){}來實現hook
clazz.method.implementation = function(args) {
    ......
}
clazz.method.overload(argumentsType[]).call(args);
// 將js包裝的類實例轉換成另一種js包裝類
Java.cast(clazz1, Clazz2)

0x04 Python腳本運行

除了在命令行直接將腳本注入進程,也可以通過python來加載腳本到指定進程
這裏給出一個通用模板

# python
# -*- coding: utf-8 -*-

import frida
import sys

def on_message(message, data):
    if message['type'] == 'send':
        print("*****[frida hook]***** : {0}".format(message['payload']))
    else:
        print("*****[frida hook]***** : " + str(message))

def get_javascript(filepath):
    code = ''
    with open(filepath, 'r') as file:
        code = code + file.read()
    return code

# 連接遠端設備
device = frida.get_remote_device()
# 附加到進程
session = device.attach(package_name)
# 1、直接寫入 javascript 代碼
javascript = """
<javascript code>
"""
# 2、從文件中加載 javascript 腳本代碼
javascript = get_javascript(javascript_file)
# 基於腳本內容創建運行腳本對象
script = session.create_script(javascript)
script.on('message', on_message)
# 加載腳本並執行
script.load()
sys.stdin.read()

0x05 基本使用

Frida提供了一些工具直接操作
比如查看連接的設備

# bash
$ frida-ls-devices
Id                Type    Name        
----------------  ------  ------------
local             local   Local System
0105a575959b9fa9  usb     LGE Nexus 5X
tcp               remote  Local TCP 

使用python腳本獲取的也是這樣

# python
import frida
print(frida.get_local_device())
print(frida.get_usb_device())
print(frida.get_remote_device())
-------------------------------------------------------------------------
Device(id="local", name="Local System", type='local')
Device(id="0105a575959b9fa9", name="LGE Nexus 5X", type='usb')
Device(id="tcp", name="Local TCP", type='remote')

其中,local device對應的就是PC本機,usb device對應連接的Android設備,remote device也是經過端口轉發的Android設備

附加指定進程

$ frida-trace -i open -U <package_name/pid>

也封裝了許多常用功能和函數,可以直接調用接口

查看所有進程

# python
processes = device.enumerate_processes()
for process in processes:
    print(process)

查看所有安裝的應用

# python
applications = device.enumerate_applications()
for application in applications:
	print(application)

獲取頂層應用進程

# python
front_app = remote_device.get_frontmost_application()
print(front_app)
-------------------------------------------------------------------------
Application(identifier="com.android.settings", name="設置", pid=6683)

獲取加載的類

// javascript
Java.enumerateLoadedClasses({
    onMatch:function(_className){
        log("found instance of '" + _className + "'");
    },
    onComplete: function(){
        log("enumerating completed !!!");
    }
});

獲取類聲明的函數

// javascript
var clazz = Java.use(className);
var methods = clazz.class.getDeclaredMethods();
if(methods.length > 0){
    log("getDeclaredMethods of class '" + className + "':");
    methods.forEach(function(method){
        log(method);
    });
}

獲取調用堆棧

// javascript
var stack = Java.use("java.lang.Thread").$new().currentThread().getStackTrace();
for(var i = 2; i < stack.length; i++){
    log("getStackTrace[" + (i-2) + "] : " + stack[i].toString());
}

獲取加載的所有模塊

// javascript
Process.enumerateModules({
    onMatch: function(module) {
        log(module.name + " : " +  module.base + "\t" + module.size + "\t" + module.path);
    },
    onComplete: function() {
        log("enumerating completed !!!");
    }
});

獲取所有的導出符號

// javascript
var symbols = Module.enumerateExportsSync(moduleName);
symbols.forEach(function(symbol){
    log(symbol.name + " address = " + symbol.address);
});
return symbols;

獲取JNI註冊的函數信息

// javascript
var RegisterNativesAddr = getSymbolAddr("libart.so", "_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi");
if(RegisterNativesAddr != null){
    log("find symbol 'RegisterNatives' in libart.so, address = " + RegisterNativesAddr);
    Interceptor.attach(RegisterNativesAddr, {
        onEnter: function(args) {
            var class_name = Java.vm.getEnv().getClassName(args[1]);
            var methods_ptr = ptr(args[2]);
            var module = Process.findModuleByAddress(Memory.readPointer(methods_ptr));
            log("RegisterNativeMethod class = " + class_name + ", module = " + module.name + ", base = " + module.base);
            var method_count = parseInt(args[3]);
            log("registered methods count = " + method_count);
            // get registered native method info
            var offset = Process.pointerSize;
            for (var i = 0; i < method_count; i++) {
                var name = Memory.readCString(Memory.readPointer(methods_ptr.add(offset*3*i)));
                var sig = Memory.readCString(Memory.readPointer(methods_ptr.add(offset*3*i+offset)));
                var address = Memory.readPointer(methods_ptr.add(offset*(3*i+2)));
                log("methods name = " + name + ", sig = " + sig + ", address = " + ptr(address) + ", offset = " + ptr(address).sub(module.base));
            }
        },
        onLeave: function() {}
    });
}

導出遠程方法,注意不支持大寫字母和下劃線

// javascript
rpc.exports = {
    <exportfuncname1>: function(args) {
        ......
    },
    <exportfuncname2>: function(args) {
        ......
    }
};

遠程調用

# python
script.exports.exportfuncname1(args)

0x06 常用API整理

  • console

    • log/warn/error(message) : 打印日誌
    • hexdump(address, options) : 打印address內存,輸出格式由options定義,一般爲offset、length、header、ansi
  • Process

    • id : 返回目標進程id
    • isDebuggerAttached() : 檢查目標進程是否附加成功
    • enumerateModules() : 枚舉當前進程已加載的所有模塊,並返回數組
    • enumerateThreads() : 枚舉當前進程的所有線程,並返回數組
    • getCurrentThreadId() : 返回當前線程id
  • Module

    • name : 返回模塊名稱
    • base : 返回模塊加載到內存中的地址,類型爲 NativePointer
    • size : 模塊大小
    • path : 模塊的完整文件路徑
    • load() : 加載so文件,返回 Module 對象
    • enumerateImports() : 枚舉所有導入函數,返回數組(type/name/module/address)
    • enumerateExports() : 枚舉所有導出函數,返回數組(type/name/module/address)
    • enumerateSymbols() : 枚舉所有符號,返回數組(isGlobal/type/section/name/address)
    • findExportByName()/getExportByName() : 返回指定so文件中導出函數絕對地址
    • findBaseAddress()/getBaseAddress() : 返回指定so文件內存基址
  • Memory

    • scan(address, size, pattern, callback) : 從address開始搜索size大小空間中滿足pattern條件的數據回調callback,可以返回“stop”提前取消內存掃描
    • scan(address, size, pattern) : 返回符合條件的數據數組
    • alloc(size) : 在目標進程堆中申請size大小的內存,並按照Process.pageSize對齊,返回NativePointer的地址指針,且自動釋放
    • allocUtf8String(string)/allocUtf16String(string)/allocAnsiString(string) : 分配UTF-8/UTF-16/ANSI字符串
    • copy(obj_addr, tar_addr, size) : 將目標地址開始size大小的數據拷貝到目的地址
    • writeByteArray(addr, array) : 將數組寫入目的地址
    • readByteArray(addr, size) : 從目標地址讀取size大小的數組
  • Java

    • available : 判斷當前進程是否加載JavaVM,Dalvik/ART虛擬機
    • androidVersion : 當前設備系統版本
    • enumerateLoadedClasses() : 枚舉當前加載的所有類,回調onMatch(name)/onComplete()
    • enumerateLoadedClassesSync() : 枚舉當前加載的所有類,返回數組
    • enumerateClassLoaders() : 枚舉當前所有的類加載器,回調onMatch(loader)/onComplete()
    • enumerateClassLoadersSync() : 枚舉當前所有的類加載器,返回數組
    • perform(callback) : 用於當前線程附加到Java VM,回調callback
    • use(classname) : 動態獲取classname對應的類定義,$new()調用構造函數,$dispose()顯式釋放
    • choose(classname) : 在堆上查找實例化對象,回調onMatch(instance)/onComplete()
    • cast(instance, clazz) : 將指定變量或數據強制轉化爲給定類定義
    • array(type, [value1, value2, …]) : 定義type類型的Java的數組
    • registerClass(args) : 註冊一個新的Java類並返回對象,類定義由args給出,name指定類名[superClass指定父類,implements指定實現接口數組,fields指定域字段,methods指定方法]
    • vm : 虛擬機,一般用於調用getEnv()來獲取JNIEnv對象
  • Interceptor

    • attach(address, callback) : 攔截NativPointer類型的函數地址,回調onEnter/onLeave
    • detach() : 取消對地址的攔截
    • replace(function, callback) : 替換NativeFunction類型的原函數爲回調函數
  • NativePointer : javascript中的指針

    • new NativePointer(var) : 構造指向var的指針
    • add/sub/and/or/xor(var) : 四則和邏輯運算
    • shl/shr(n) : 左/右移動n位
    • isNull() : 檢查指針是否爲空
    • toString/toInt32() : 轉爲字符串/32位整數
    • readXXX()/writeXXX(value) : 從內存讀取內容/向內存寫入內容
    • readByteArray(length) : 從內存讀取length長度字節以ArrayBuffer返回
    • writeByteArray(bytes) : 將Arraybuffer類型或整數數組寫入內存位置
    • readCSString/readUtf8String/readUtf16String/readAnsiString() : 將內存位置的字節作爲ASCII、UTF-8、UTF-16、ANSI字符串讀取
    • writeUtf8String/writeUtf16String/writeAnsiString(value) : 對JavaScript字符串進行編碼並寫入內存位置
  • NativeFunction : 本地函數

    • new NativeFunction(address, retType, argTypes) : 構造方法,address爲NativePointer類型函數地址,retType指定返回類型,argTypes數組指定參數類型,返回函數對象,可以直接調用
  • NativeCallback : 本地函數回調

    • new NativeCallback(callback, retType, argTypes) : 構造方法,callback爲回調函數,retType指定返回類型,argTypes數組指定參數類型,返回NativePointer對象

0x07 開發用例

上面所有的代碼都可以在這裏找到,這是我從學習frida開始寫過的一些用例,包含測試應用程序、hook測試用例,還封裝了一些常用功能

另外總結了一些優秀的應用案例:

appmon

objection

ssl_logger

passionfruit

r2frida = Frida + Radare2

Brida = Frida + Burp

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章