Frida實(shí)踐
原鏈接:https://github.com/whyTalent/whyTalent/blob/033717eaf177fb39580a475fd913f00e52d33d33/Frida%E6%A1%86%E6%9E%B6/2%20frida%E5%AE%9E%E8%B7%B5.md
Frida分客戶端環(huán)境和服務(wù)端環(huán)境
- 客戶端:編寫Python代碼伯诬,用于連接遠(yuǎn)程設(shè)備瘟裸,提交要注入的代碼到遠(yuǎn)程只泼,接受服務(wù)端的發(fā)來的消息等;
- 服務(wù)端:需要用Javascript代碼注入到目標(biāo)進(jìn)程涨岁,操作內(nèi)存數(shù)據(jù),給客戶端發(fā)送消息等操作
也可以把客戶端理解成控制端胁镐,服務(wù)端理解成被控端啊楚,假如要用PC來對(duì)Android設(shè)備上的某個(gè)進(jìn)程進(jìn)行操作,那么PC就是客戶端,而Android設(shè)備就是服務(wù)端拾积。
按照功能層級(jí)殉挽,可以劃分可以分為四個(gè)級(jí)別:
- CPU 指令集級(jí)別的 inline-hook 框架: frida-gum
- 使用 JavaScript 引擎對(duì) gum 進(jìn)行封裝實(shí)現(xiàn)腳本拓展的能力: gum-js
- 運(yùn)行時(shí)進(jìn)程注入、腳本加載拓巧、RPC 通信管理等功能: frida-core
- 針對(duì)特殊運(yùn)行環(huán)境的 JS 模塊及其接口斯碌,如 frida-java-bridge、frida-objc-bridge 等
一肛度、frida自動(dòng)化基礎(chǔ)
1傻唾、frida Javascript 引擎
1)由于 iOS 的 JIT 限制,以及嵌?式設(shè)備的內(nèi)存壓?承耿,新版將默認(rèn)腳 本引擎從 V8 遷移? Duktape
2)在 Android 等?持 v8 的平臺(tái)上仍然可以使? enable-jit 選項(xiàng)切換回 v8
3)Duktape ? v8 缺失了?常多 ECMAScript 6 特性冠骄,如箭頭表達(dá)式、 let 關(guān)鍵字
箭頭函數(shù)
- ECMAScript 6 引?的書寫匿名函數(shù)的特性
- 需要啟? JIT加袋,或 frida-compile 轉(zhuǎn)譯才可在 frida 中使?
- ? function 表達(dá)式簡(jiǎn)潔凛辣。適合編寫邏輯較短的回調(diào)函數(shù)
- 語義并?完全等價(jià)。箭頭函數(shù)中的 this 指向?作?域中的上下?职烧;? function 可以通過 Function.prototype.bind ?法指定上下?
4)frida --debug 啟?調(diào)試需使? Duktape扁誓,不兼容 v8-inspector
5)以下代碼等價(jià)
// 普通函數(shù)
Process.enumerateModulesSync().filter(function(module) { return
module.path.startsWith('/Applications') })
// 箭頭函數(shù)
Process.enumerateModulesSync().filter(module => module.path.startsWith('/
Applications'))
2、npm && frida-compile
**命令參數(shù) **
- -o 輸出?件名
- -w 監(jiān)視模式蚀之,源?件改動(dòng)后?即編譯
- -c 開啟 uglify 腳本壓縮
- -b 輸出字節(jié)碼
- -h 查看完整命令??法
- -x, —no-babelify 關(guān)閉 babel 轉(zhuǎn)譯
特點(diǎn)
1)需求
- 默認(rèn)使?的 Duktape 不?持最新的 ECMAScript 特性
- 單個(gè) js ?件蝗敢,難以管理?型項(xiàng)?
2)可將 TypeScript 或 ES6 轉(zhuǎn)譯成 Duktape 可?的 ES5 語法
3)?持 Browserify 的打包,?持 ES6 modules足删、source map 和 uglify 代碼壓縮寿谴。甚? 可?成 Duktape 字節(jié)
4)?持使? require 或 es6 module 引?第三? npm 包
5)frida-compile 是 npm 包,需要 node.js 運(yùn)?環(huán)境失受,與 frida-python 不沖突拭卿,可同時(shí)安 裝使?价脾。其中,在npm 內(nèi)可創(chuàng)建?錄結(jié)構(gòu)伊佃、安裝依賴骤铃,并可在 package.json 中添加構(gòu)建腳本
6)使? TypeScript 可享受到類型系統(tǒng)帶來的?型項(xiàng)?管理便利
7)Babel 添加插件?持?級(jí)的語法特性(generator / async-await)
補(bǔ)充:npm package.json文件介紹
1)創(chuàng)建package.json文件:使用
npm init
即可在當(dāng)前目錄創(chuàng)建一個(gè)package.json
文件,依次確認(rèn)即可創(chuàng)建惠桃∑忠模或npm init --yes
跳過回答問題步驟,直接生成默認(rèn)值的package.json
文件2)基礎(chǔ)屬性:
name:全部小寫辜王,沒有空格劈狐,可以使用下劃線或者橫線
version:x.x.x 的格式
description:描述信息,有助于搜索
main: 入口文件呐馆,一般都是 index.js
keywords:關(guān)鍵字肥缔,有助于在人們使用 npm search 搜索時(shí)發(fā)現(xiàn)你的項(xiàng)目
author:作者信息
license:默認(rèn)是 MIT
bugs:當(dāng)前項(xiàng)目的一些錯(cuò)誤信息
指定依賴包:指定項(xiàng)目依賴的包,通過
npm install
默認(rèn)下載
dependencies
:在生產(chǎn)環(huán)境中需要用到的依賴devDependencies
:在開發(fā)汹来、測(cè)試環(huán)境中用到的依賴scripts:指定了運(yùn)行腳本命令的npm命令行縮寫续膳,比如:start 指定了運(yùn)行
npm run start
時(shí),所要執(zhí)行的命令"scripts": { "prepare": "npm run build", // npm run prepare "preinstall": "echo here it comes!", // npm run preinstall "postinstall": "echo there it goes!", // npm run postinstall "start": "node index.js", // npm run start "test": "tap test/*.js" // npm run test }
3收班、frida session實(shí)例的生命周期
4坟岔、firda 平臺(tái)實(shí)踐
4.1 frida on Android
frida-java 是 frida 內(nèi)置庫,即 Java 命名空間下的函數(shù)摔桦,可對(duì) ART 和 Dalvik 運(yùn)?時(shí)插樁(源代碼 github/frida/frida-java)社付。其次,在 frida 框架基礎(chǔ)上完全由 javascript 實(shí)現(xiàn)邻耕,frida-gum 只實(shí)現(xiàn)了通?的?進(jìn)制插樁鸥咖。總的來說兄世,frida-java通過兩步實(shí)現(xiàn)js世界到j(luò)ava世界的單向通道扛或,首先利用frida-gum提供的js接口操作native世界,然后再基于jni連通到j(luò)ava世界碘饼。
1)操作對(duì)象或字段
a. 操作對(duì)象
frida 既可以 new 對(duì)象實(shí)例熙兔,也可以搜索已有的對(duì)象
-
$new
:new 運(yùn)算符,初始化新對(duì)象艾恼。注意與 $init 區(qū)分 -
$alloc
:分配內(nèi)存住涉,但不初始化 -
$init
:構(gòu)造器?法,?來 hook, ?不是給 js 調(diào)? -
$dispose
:析構(gòu)函數(shù) -
$isSameObject
:是否與另?個(gè) Java 對(duì)象相同 -
$className
:類名
if (!Java.available)
throw new Error('requires Android');
Java.perform(function() {
const JavaString = Java.use('java.lang.String');
var exampleString1 = JavaString.$new('Hello World, this is an example string in Java.');
console.log('[+] exampleString1: ' + exampleString1);
console.log('[+] exampleString1.length(): ' + exampleString1.length());
});
b. 訪問 / 修改對(duì)象成員
instance.field.value = newValue
钠绍,這種?式不區(qū)分成員可?性舆声,即使是私有成員同樣可以直接訪問,其次除 value 的 setter
和 getter
之外,fieldType
和 fieldReturnType
獲取類型信息
// 字段賦值和讀取要在字段名后加.value媳握,假設(shè)有這樣的一個(gè)類
package com.luoyesiqiu.app;
public class Person{
private String name;
private int age;
}
// 操作Person類的name字段和age字段
var person_class = Java.use("com.luoyesiqiu.app.Person");
// 實(shí)例化Person類
var person_class_instance = person_class.$new();
// 給name字段賦值
person_class_instance.name.value = "luoyesiqiu";
// 給age字段賦值
person_class_instance.age.value = 18;
// 輸出name字段和age字段的值
console.log("name = ",person_class_instance.name.value, "," ,"age = " ,person_class_instance.age.value);
frida 對(duì)數(shù)組做了封裝碱屁,直接取下標(biāo)即可訪問
// 注意 instance 和 Class 的區(qū)別
// Java.choose 找到實(shí)例后查詢字段的類型
Java.perform(function () {
var MainActivity = Java.use('com.example.seccon2015.rock_paper_scissors.MainActivity');
Java.choose(MainActivity.$className, {
onMatch: function(instance) {
console.log(JSON.stringify(instance.P.fieldReturnType));
},
onComplete: function() {}
});
})
2)修改函數(shù)實(shí)現(xiàn) Hook Java
修改一個(gè)函數(shù)的實(shí)現(xiàn)后,如果這個(gè)函數(shù)被調(diào)用蛾找,則avascript代碼里的函數(shù)實(shí)現(xiàn)也會(huì)被調(diào)用
Java 層的插樁:
// 格式 Java.use().method.implementation = hookCallback // 由于 Java ?持同名?法重載娩脾,需要? .overload 確定具體的?法 Java.use('java.lang.String').$new.overload('[B', 'java.nio.charset.Charset')
JNI 層插樁:JNI 實(shí)現(xiàn)在 so 中,且符號(hào)必然是導(dǎo)出函數(shù)打毛,照常使?
Interceptor
即可
a. 函數(shù)參數(shù)類型表示
基本類型縮寫表示表:
<div align="center"><img src="imgs/基本類型縮寫.png" alt="基本類型縮寫" style="zoom:80%;" /></div>
注意:
int[]
類型:重載時(shí)要寫成[I
任意類:直接寫完整類名即可柿赊,比如:
java.lang.String
對(duì)象數(shù)組:用左中括號(hào)接上完整類名再接上分號(hào)
[java.lang.String;
b. 帶參數(shù)構(gòu)造函數(shù)
// 修改參數(shù)為byte[]類型的構(gòu)造函數(shù)的實(shí)現(xiàn)
ClassName.$init.overload('[B').implementation=function(param){
//do something
}
// 修改多參數(shù)的構(gòu)造函數(shù)的實(shí)現(xiàn)
ClassName.$init.overload('[B','int','int').implementation=function(param1,param2,param3){
//do something
}
注:ClassName是使用 Java.use 定義的類,param是可以在函數(shù)體中訪問的參數(shù)
c. 無參構(gòu)造函數(shù)
// 默認(rèn)格式
ClassName.$init.overload().implementation=function(){
//do something
}
// 調(diào)用原構(gòu)造函數(shù)
ClassName.$init.overload().implementation=function(){
//do something
this.$init();
//do something
}
注:當(dāng)構(gòu)造函數(shù)(函數(shù))有多種重載形式幻枉,比如一個(gè)類中有兩個(gè)形式的func:void func()
和 void func(int)
碰声,要加上 overload來對(duì)函數(shù)進(jìn)行重載,否則可以省略overload
d. 普通函數(shù) & 無參函數(shù)
// 修改函數(shù)名為func熬甫,參數(shù)為byte[]類型的函數(shù)的實(shí)現(xiàn)
ClassName.func.overload('[B').implementation=function(param){
//do something
//return ...
}
// 無參數(shù)的函數(shù)
ClassName.func.overload().implementation=function(){
//do something
}
// 帶返回值, 則hook時(shí)也應(yīng)有返回值
ClassName.func.overload().implementation=function(){
//do something
return this.func();
}
注: 在修改函數(shù)實(shí)現(xiàn)時(shí)胰挑,如果原函數(shù)有返回值,那么我們?cè)趯?shí)現(xiàn)時(shí)也要返回合適的值
3)函數(shù)調(diào)用&實(shí)例化
// 和Java一樣椿肩,創(chuàng)建類實(shí)例就是調(diào)用構(gòu)造函數(shù)瞻颂,而在這里用$new表示一個(gè)構(gòu)造函數(shù)
var ClassName = Java.use("com.luoye.test.ClassName");
var instance = ClassName.$new();
// 實(shí)例化以后調(diào)用其他函數(shù)
var ClassName = Java.use("com.luoye.test.ClassName");
var instance = ClassName.$new();
instance.func();
4)常用Java hook方法
a. 獲取調(diào)用堆棧
Android 提供了?具函數(shù)可以打印 Exception 的堆棧,此?式等價(jià) 于 Log.getStackTraceString(new Exception)
Java.perform(function () {
const Log = Java.use('android.util.Log');
const Exception = Java.use('java.lang.Exception');
const MainActivity = Java.use('com.example.seccon2015.rock_paper_scissors.MainActivity');
MainActivity.onClick.implementation = function(v) {
this.onClick(v);
console.log(Log.getStackTraceString(Exception.$new()));
};
});
b. 枚舉所有類方法
Java.perform(function() {
//enter class name here: example android.security.keystore.KeyGenParameterSpec$Builder
//class inside a class is defined using CLASS_NAME$SUB_CLASS_NAME
var class_name = "android.security.keystore.KeyGenParameterSpec$Builder";
var methodArr = Java.use(class_name).class.getMethods();
console.log("[*] Class Name: " + class_name)
console.log("[*] Method Names:")
for(var m in methodArr)
{
console.log(methodArr[m]);
}
});
5)Hook 動(dòng)態(tài)鏈接庫(loadLibrary)
Android中我們通常使用系統(tǒng)提供的兩種API:System.loadLibrary或者System.load來加載so文件:
// 加載的是libnative-lib.so覆旱,注意的是這邊只需要傳入"native-lib"
System.loadLibrary("native-lib");
// 傳入的是so文件完整的絕對(duì)路徑
System.load("/data/data/應(yīng)用包名/lib/libnative-lib.so")
System.loadLibrary()和System.load()的區(qū)別:
1)loadLibray傳入的是編譯腳本指定生成的so文件名稱,一般不需要包含開頭的lib和結(jié)尾的.so核无,而load傳入的是so文件所在的絕對(duì)路徑
2)loadLibrary傳入的不能是路徑扣唱,查找so時(shí)會(huì)優(yōu)先從應(yīng)用本地路徑下(/data/data/${package-name}/lib/arm/)進(jìn)行查找,不存在的話才會(huì)從系統(tǒng)lib路徑下(/system/lib团南、/vendor/lib等)進(jìn)行查找噪沙;而load則沒有路徑查找的過程
3)load傳入的不能是sdcard路徑,會(huì)導(dǎo)致加載失敗吐根,一般只支持應(yīng)用本地存儲(chǔ)路徑/data/data/${package-name}/正歼,或者是系統(tǒng)lib路徑system/lib等這2類路徑
4)loadLibrary加載的都是一開始就已經(jīng)打包進(jìn)apk或系統(tǒng)的so文件了,而load可以是一開始就打包進(jìn)來的so文件拷橘,也可以是后續(xù)從網(wǎng)絡(luò)下載局义,外部導(dǎo)入的so文件
5)重復(fù)調(diào)用loadLibrar, load并不會(huì)重復(fù)加載so,會(huì)優(yōu)先從已加載的緩存中讀取冗疮,所以只會(huì)加載一次
6)加載成功后會(huì)去搜索so是否有"JNI_OnLoad"萄唇,有的話則進(jìn)行調(diào)用,所以"JNI_OnLoad"只會(huì)在加載成功后被主動(dòng)回調(diào)一次术幔,一般可以用來做一些初始化的操作另萤,比如動(dòng)態(tài)注冊(cè)jni相關(guān)方法等
底層Android 加載動(dòng)態(tài)鏈接庫Java代碼:
// System.load("/data/data/應(yīng)用包名/lib/libnative-lib.so")
public static void load(String filename) {
Runtime.getRuntime().load0(VMStack.getStackClass1(), filename);
}
// System.loadLibrary("native-lib")
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
Hook底層System.loadLibrary方式:
// 1) overload 重載
Java.perform(function() {
const System = Java.use('java.lang.System');
const Runtime = Java.use('java.lang.Runtime');
const VMStack = Java.use('dalvik.system.VMStack');
// System.loadLibrary 函數(shù)重載
System.loadLibrary.overload('java.lang.String').implementation = function(library) {
console.log("[*] Loading dynamic library => " + library);
try {
// android OAID 動(dòng)態(tài)鏈接庫加載
// PS: frida實(shí)踐過程so庫發(fā)現(xiàn)影響APP啟動(dòng),跳過so庫加載邏輯,規(guī)避導(dǎo)致此問題,理論上不影響APP整體功能
if(library === 'msaoaidsec') {
return;
}
const loaded = Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), library);
return loaded;
} catch(ex) {
console.log(ex);
}
};
});
// 2) 重寫
Java.perform(function(){
const system = Java.use('java.lang.System');
const Runtime = Java.use('java.lang.Runtime');
const VMStack = Java.use('dalvik.system.VMStack');
system.loadLibrary.implementation = function(library){
console.log("[*] Loading dynamic library => " + library);
// this.loadLibrary(library);
const loaded = Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), library);
// 底層注入
var mbase = Module.getBaseAddress('libluajava.so');
Interceptor.attach(mbase.add(0xC999), {
onEnter:function(args){
console.log(hexdump(Memory.readPointer(args[2]),{ length: 100, ansi: true }));
}
});
}
});
注:loadLibrary
內(nèi)部會(huì)修改 classloader
,不能直接調(diào)用 this.loadLibrary(library)
,故主動(dòng)調(diào)用更底層的loadLibrary0
詳情:
- Android 加載動(dòng)態(tài)鏈接庫的過程及其涉及的底層原理
- [原創(chuàng)]frida hook loadLibrary
- Github: hook diopen & android_dlopen_ext
6)hook dlopen 和 android_dlopen_ext
function hook_dlopen(module_name) {
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
if (android_dlopen_ext) {
Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr) {
this.path = (pathptr).readCString();
if (this.path.indexOf(module_name) >= 0) {
this.canhook = true;
console.log("android_dlopen_ext:", this.path);
}
}
},
onLeave: function (retval) {
if (this.canhook) {
console.log("[*] android_dlopen_ext can hook");
}
}
});
}
var dlopen = Module.findExportByName(null, "dlopen");
if (dlopen) {
Interceptor.attach(dlopen, {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr) {
this.path = (pathptr).readCString();
if (this.path.indexOf(module_name) >= 0) {
this.canhook = true;
console.log("dlopen:", this.path);
}
}
},
onLeave: function (retval) {
if (this.canhook) {
console.log("[*] dlopen can hook");
}
}
});
}
console.log("android_dlopen_ext:", android_dlopen_ext, "dlopen:", dlopen);
}
詳情:
7)調(diào)用APP Router實(shí)現(xiàn)schema跳轉(zhuǎn)
假設(shè)DemoAPP的Router實(shí)例調(diào)用格式定義如下:
// 包名: com.android.router
// Builder是RouteRequest類的子類
APPRouter.routeTo(RouteRequest.Builder(url).build(), Application)
frida調(diào)用方式
Java.perform(function() {
setTimeout(function() {
Java.choose('android.app.Application', {
onComplete: function() {
},
onMatch: function(instance) {
const schema = "demoapp://test_schema"
console.log("[*] routing to: " + schema);
// 子類
const RouteRequestBuilder = Java.use('com.android.router.RouteRequest$Builder');
var request = RouteRequestBuilder.$new(schema).build()
console.log(Java.use('com.android.router').routeTo(request, instance));
}
})
}, 3000);
})
8)assert斷言驗(yàn)證
配合frida動(dòng)態(tài)測(cè)試框架四敞,通過assert模塊斷言驗(yàn)證執(zhí)行結(jié)果的準(zhǔn)確性
import assert from "assert";
export function image_test() {
// assert
assert.strictEqual(1, 1, "[*] image_test assert pass");
const apples = 1;
const oranges = 1;
assert.strictEqual(apples, oranges, `image: apples ${apples} !== oranges ${oranges}`);
console.log("[*] image test complete")
}
export function hook_view_click() {
const View = Java.use('android.view.View');
View.setOnClickListener.implementation = function (v: Object) {
assert.ok(true, "[*] hook_view_click assert pass");
this.setOnClickListener(v);
};
}
4.2 frida on iOS
frida-objc:對(duì)應(yīng) Java泛源,ObjC api 是 frida 的另?個(gè)“?等公?”,源代碼 github/frida/frida-objc忿危,與 JVM 類似达箍,Objective C 也提供了 runtime api,其次frida 將 Objective C 的部分 runtime api 提供到 ObjC.api 中
1)特點(diǎn)
與 Java 顯著不同癌蚁,frida-objc 將所有 class 信息 保存到 ObjC.classes
中幻梯,直接對(duì)其 for in 遍歷 key 即可
// Objective C 實(shí)現(xiàn)
[NSString stringWithString:@"Hello World”]
// 對(duì)應(yīng) frida
var NSString = ObjC.classes.NSString;
NSString.stringWithString_("Hello World”);
new ObjC.Object
可以將指針轉(zhuǎn)換為 Objective C 對(duì)象,但如果指針不是合法的對(duì)象或合法的地址努释,將拋出異车馍遥或?qū)е挛炊x?為
2)hook objective C
firda提供了3種方式hook objective C方法:
1)
ObjC.classes.Class.method
以及ObjC.Block
:都提供了?個(gè).implementation
(獲取內(nèi)存地址)的 setter 來 hook ?法實(shí)現(xiàn),實(shí)際上就是 iOS 開發(fā)者熟悉的 Method Swizzling2)使?攔截器
Interceptor.attach(ObjC.classes.Class.method.implementation)
伐蒂,看上去很相似煞躬,但實(shí)現(xiàn)原理是對(duì) selector 指向的代碼進(jìn)? inline hook3)Proxy 也是 Objective C 當(dāng)中的?種 hook ?式,其次frida 提供了ObjC.registerClass 來創(chuàng)建 Proxy
a. ObjC.classes.Class.method
格式:ObjC.classes.className["funcName"]
其中逸邦,className指具體的類名稱恩沛,funcName指類方法名稱
const { AVModel } = ObjC.classes;
// 獲取函數(shù)內(nèi)存地址
const oldImpl = AVModel["- getDataGotoType"].implementation
// 函數(shù)替換
AVModel["- getDataGotoType"].implementation = ObjC.implement(AVModel["- getDataGotoType"], (handle, selector) => {
console.log("AVModel.getDataGotoType hooked")
// 返回值替換
return ObjC.classes.NSString.stringWithString_("hello, world");
});
// 初始化對(duì)象
const model = AVModel.alloc().init();
// 調(diào)用對(duì)象為 hook 函數(shù)
console.log(model.getDataGotoType());
// 解除 hook
AVModel["- getDataGotoType"].implementation = oldImpl;
// 調(diào)用原函數(shù)
console.log(model.getDataGotoType());
b. Objc.Block
格式:new Objc.Block(target[, options])
其中,target 是一個(gè) NativePointer 對(duì)象
// Defining a Block that will be passed as handler parameter to +[UIAlertAction actionWithTitle:style:handler:]
var handler = new ObjC.Block({
retType: 'void',
argTypes: ['object'],
implementation: function () {
}
});
// Import ObjC classes
var UIAlertController = ObjC.classes.UIAlertController;
var UIAlertAction = ObjC.classes.UIAlertAction;
var UIApplication = ObjC.classes.UIApplication;
// Using Grand Central Dispatch to pass messages (invoke methods) in application's main thread
ObjC.schedule(ObjC.mainQueue, function () {
// Using integer numerals for preferredStyle which is of type enum UIAlertControllerStyle
var alert = UIAlertController.alertControllerWithTitle_message_preferredStyle_('Frida', 'Hello from Frida', 1);
// Again using integer numeral for style parameter that is enum
var defaultAction = UIAlertAction.actionWithTitle_style_handler_('OK', 0, handler);
alert.addAction_(defaultAction);
// Instead of using `ObjC.choose()` and looking for UIViewController instances
// on the heap, we have direct access through UIApplication:
UIApplication.sharedApplication().keyWindow().rootViewController().presentViewController_animated_completion_(alert, true, NULL);
})
c. Interceptor 攔截器
格式:Interceptor.attach(target, callbacks[, data])
其中缕减,target是 NativePointer 指定要攔截調(diào)用的函數(shù)的地址雷客,如果從Frida API獲取地址(例如Module.getExportByName()
),F(xiàn)rida將處理詳細(xì)信息
攔截C函數(shù)
fopen 函數(shù):其功能是使用給定的模式 mode 打開 filename 所指向的文件桥狡,如果文件打開成功搅裙,會(huì)返回一個(gè)指針,相當(dāng)于句柄裹芝。如果文件打開失敗則返回 0
原型如下:
FILE *fopen(const char *filename, const char *mode)
// a. 底層系統(tǒng)函數(shù)
Interceptor.attach(Module.findExportByName(null, "fopen"), {
// onEnter 是進(jìn)入 fopen 函數(shù)時(shí)要執(zhí)行的代碼
onEnter: function(args) {
if (args[0].isNull()) return;
var path = args[0].readUtf8String();
console.log("fopen " + path);
},
// onLeave 是離開 fopen 函數(shù)時(shí)要執(zhí)行的代碼
onLeave: function(retval) {
console.log("\t[-] Type of return value: " + typeof retval);
console.log("\t[-] Original Return Value: " + retval);
retval.replace(0); //將返回值替換成0
console.log("\t[-] New Return Value: " + retval);
},
})
// b. 自定義函數(shù)
// 自定義一個(gè) getStr 函數(shù)部逮,返回的參數(shù)是一個(gè)字符串指針,在 onLeave 函數(shù)中新建一個(gè)變量 string嫂易,分配內(nèi)存并填充字符串 4567789, 然后將返回值替換變量 string
Interceptor.attach(Module.findExportByName(null, "getStr"), {
onEnter: function(args) {
console.log("getStr");
},
onLeave: function(retval) {
console.log("\t[-] Type of return value: " + typeof retval);
console.log("\t[-] Original Return Value: " + retval.readUtf8String());
var string = Memory.allocUtf8String("456789"); //分配內(nèi)存
retval.replace(string); //替換
console.log("\t[-] New Return Value: " + retval.readUtf8String());
},
})
攔截 Objective-C 方法
frida 不僅可以攔截 C 函數(shù)兄朋,還可以攔截 Objective-C 方法,比如編寫腳本對(duì) +[NSURL URLWithString:] 進(jìn)行攔截怜械,代碼如下颅和,其中 onEnter 調(diào)用 ObjC.classes.NSString.stringWithString_ 給 NSString 傳遞新的值,這樣相當(dāng)于替換原本的 URL缕允。
var className = "NSURL";
var funcName = "+ URLWithString:";
var hook = eval('ObjC.classes.' + className + '["' + funcName + '"]');
Interceptor.attach(hook.implementation, {
onLeave: function(retval) {
console.log("[*] Class Name: " + className);
console.log("[*] Method Name: " + funcName);
console.log("\t[-] Type of return value: " + typeof retval);
console.log("\t[-] Original Return Value: " + retval);
},
onEnter: function(args){
var className = ObjC.Object(args[0]);
var methodName = args[1];
var urlString = ObjC.Object(args[2]);
console.log("className: " + className.toString());
console.log("methodName: " + methodName.readUtf8String());
console.log("urlString: " + urlString.toString());
console.log("-----------------------------------------");
urlString = ObjC.classes.NSString.stringWithString_("http://www.baidu.com")
console.log("newUrlString: " + urlString.toString());
console.log("-----------------------------------------");
}
});
3)示例
a. iOS / macOS 定位偽造
基礎(chǔ):
iOS 和 macOS 定位使?統(tǒng)? API:
CLLocationManager
-
需指定?個(gè) delegate 實(shí)現(xiàn)如下回調(diào)?法獲取相應(yīng)事件:
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation: (CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation;
-
如下?法開始定位
- (void)requestLocation; - (void)startUpdatingLocation
流程:
- 先處理 requestLocation 等?法拿到 delegate 的指針
- 在 delegate 上查找對(duì)應(yīng)回調(diào)?法是否存在融虽,逐個(gè) hook
- CLLocation 的經(jīng)緯度是只讀屬性,需要?jiǎng)?chuàng)建新的副本灼芭。為了對(duì)抗時(shí)間戳等特征檢測(cè)有额,最好把正確的 CLLocation 除經(jīng)緯度之外所有的屬性復(fù)制上去
const { BWAJSEventAuthorizationHandler, CLLocation } = ObjC.classes;
var hook_cllocation = ObjC.classes.CLLocation["- coordinate"]
Interceptor.attach(hook_cllocation.implementation, {
onLeave: function(return_value) {
var spoofed_return_value = (new ObjC.Object(return_value)).initWithLatitude_longitude_(20.5937, 78.9629)
return_value.replace(spoofed_return_value)
}
});
var jsbHook = BWAJSEventAuthorizationHandler["- handleWithEvent:params:callback:"]
Interceptor.attach(jsbHook.implementation, {
onEnter: function() {
console.log("BWAJSEventAuthorizationHandler hooked")
}
});
b. 調(diào)用openURL
// Get a reference to the openURL selector
var openURL = ObjC.classes.UIApplication["- openURL:"];
// Intercept the method
Interceptor.attach(openURL.implementation, {
onEnter: function(args) {
// As this is an ObjectiveC method, the arguments are as follows:
// 0. 'self'
// 1. The selector (openURL:)
// 2. The first argument to the openURL selector
var myNSURL = new ObjC.Object(args[2]);
// Convert it to a JS string
var myJSURL = myNSURL.absoluteString().toString();
// Log it
console.log("Launching URL: " + myJSURL);
}
});
c. 攔截網(wǎng)絡(luò)請(qǐng)求
//判斷Object-C類方法是否已經(jīng)加載進(jìn)來
if(ObjC.available){
console.log('\n[*] Starting Hooking');
var _className = "AFHTTPSessionManager"; //類名
var _methodName = "- POST:parameters:progress:success:failure:"; //方法名
// 通過ObjC.classes返回當(dāng)前注冊(cè)類的映射表找到想要hook的類名、方法名
var hooking = ObjC.classes[_className][_methodName];
console.log('className is: ' + _className + ' and methodName is: ' + _methodName);
const pendingBlocks = new Set()
Interceptor.attach(hooking.implementation,{
onEnter: function(args) {
// args[0]:self, args[1]:The selector
// args[2]: 請(qǐng)求url args[3] 請(qǐng)求參數(shù)
var param = new ObjC.Object(args[2]);
var param2 = new ObjC.Object(args[3]);
const block = new ObjC.Block(args[5]);
pendingBlocks.add(block); // Keep it alive
const appCallback = block.implementation;
block.implementation = (success1, success2) => {
console.log('網(wǎng)絡(luò)請(qǐng)求成功回調(diào)success1'+success1+'success2'+success2);
const result = appCallback(success1, success2);
pendingBlocks.delete(block);
return result;
};
},
onLeave:function(returnValue){
//如下代碼則是在函數(shù)調(diào)用之后 打印函數(shù)的返回值及函數(shù)返回值類型
console.log('Return value of: ');
console.log(' ' + this._className + ' --> ' + this._methodName);
var typeValue = Object.prototype.toString.call(returnValue);
console.log("\t[-] Type of return value: " + typeValue);
console.log("\t[-] Return Value: " + returnValue);
}
});
}
d. 攔截類所有方法
想對(duì)某個(gè)類的所有方法進(jìn)行批量攔截,可以使用ApiResolver
接口巍佑,它可以根據(jù)正則表達(dá)式獲取符合條件的所有方法
var resolver = new ApiResolver('objc')
resolver.enumerateMatches('*[T1TranslateButton *]', {
onMatch: function (match) {
console.log(match['name'] + ":" + match['address'])
},
onComplete: function () {}
})
e. 替換原有方法hook
Interceptor.attach() 可以在攔截目標(biāo)后茴迁,可以打印參數(shù),修改返回值萤衰,但無法阻止原方法的執(zhí)行
var didTap = ObjC.classes.T1TranslateButton['- _didTap:forEvent:']
var didTapOldImp = didTap.implementation
// 覆蓋實(shí)現(xiàn)
didTap.implementation = ObjC.implement(setTitle, function(handle, selector, arg1, arg2) {
var self = ObjC.Object(handle)
console.log("self -- ", self)
// 調(diào)用舊實(shí)現(xiàn)
// didTapOldImp(handle, selector, arg1, arg2)
})
需要注意的是堕义,像_didTap:forEvent:
這里需要傳遞兩個(gè)參數(shù),則ObjC.implement
的回調(diào)中也需要寫明兩個(gè)參數(shù)(arg1脆栋、arg2)倦卖,即需要多少參數(shù)就寫多少,沒有則不用寫
二椿争、frida python實(shí)踐
1怕膛、get_device_manager
獲取設(shè)備管理器
manager = frida.get_device_manager()
2、enumerate_devices
選擇設(shè)備連接方式
# a. 通過 device manager 管理器獲取設(shè)備對(duì)象
devices = manager.enumerate_devices()
> Device(id="local", name="Local System", type='local')
> Device(id="socket", name="Local Socket", type='remote')
> Device(id="b868ca03", name="M2011J18C", type='usb')
device = manager.get_device(devices[2].id, timeout) # 指定設(shè)備id連接
# b. 指定設(shè)備連接方式
device = frida.get_local_device() # Local System
device = frida.get_usb_device(timeout) # usb
device = frida.get_remote_device() # 遠(yuǎn)程, Local Socket
3秦踪、enumerate_applications 和enumerate_processes
獲取device上的所有App和進(jìn)程
# 等價(jià)于 frida-ps -aU
# PID Name Identifier
# 5 ------ ---------------
# 21550 Gadget re.frida.Gadget
pid = None
for a in device.enumerate_applications():
if a.identifier == 're.frida.Gadget':
pid = a.pid
break
## 補(bǔ)充:獲取設(shè)備上正則運(yùn)行的進(jìn)程信息
# 等價(jià)于 frida-ps -a
# PID Name
# ----- ------
# 32429 Gadget
all_processes = device.enumerate_processes()
for per_process in all_processes:
print(per_process)
4褐捻、attach
功能:附加進(jìn)程或APP,生成session實(shí)例
1)啟動(dòng)新的實(shí)例:
device.spawn(‘path or bundle id’)
- 可指定啟動(dòng)參數(shù)
- ?持在進(jìn)程初始化之前執(zhí)??些操作
- iOS 上如果已經(jīng) App 運(yùn)?(包括后臺(tái)休眠)會(huì)導(dǎo)致失敗
2)附加到現(xiàn)有進(jìn)程:
device.attach(pid)
- 可能會(huì)錯(cuò)過 hook 時(shí)機(jī)
- spawn 在移動(dòng)設(shè)備容易出現(xiàn)不穩(wěn)定現(xiàn)象椅邓,可使? attach 模式
# 等價(jià)于 frida -U -f xx.xx.xx --no-pause
# 啟動(dòng)應(yīng)用進(jìn)入交互模式, 應(yīng)用于 App 未打開的情景
# a. pid 方式, app package (identifier)柠逞,適用于iOS
pid = device.spawn([self.package])
device.resume(pid)
time.sleep(2) # Without it Java.perform silently fails
session = device.attach(pid)
# b.app name,適用于Android
subprocess.call("adb shell pm clear app.identifier", shell=True) # 清理APP應(yīng)用數(shù)據(jù)
subprocess.call("adb shell monkey -p app.identifier -v 1", shell=True) # 通過monkey指令指定包名喚醒APP
time.sleep(5)
session = device.attach('app name')
# session = device.attach('app identifier')
5景馁、create_script
注入JS腳本
"""
def create_script(
self, source: str, name: Optional[str] = None, snapshot: Optional[bytes] = None, runtime: Optional[str] = None
) -> Script
"""
# a. js字符串
jsScript = """
console.log('this is inject javascript code')
"""
script = session.create_script(jsScript)
# b. js文件
with open("hook.js",mode='r',encoding='UTF-8') as f:
Log.info('Inject script name: ' + full_js_file_name)
script = session.create_script(f.read())
// 程序入口: hook.js
Java.perform(function()
{
// 獲取類
var clazz = Java.use("com.unity3d.player.UnityPlayerActivity");
// 獲取類中所有函數(shù)
var methods = clazz.class.getDeclaredMethods();
console.log("have method count:"+methods.length);
var i=0
if(methods.length > 0){
//遍歷函數(shù)名
methods.forEach(function(method){
i = i+1
console.log(i+":"+method);
});
}
});
6板壮、load
打印日志&執(zhí)行注入
# 打印js注入信息
def on_message(message, data):
if message['type'] == 'send':
Log.send(message['payload'])
elif message['type'] == 'error':
Log.error(message['description'])
else:
Log.error(message)
# 設(shè)備事件處理
script.on("message", on_message) # listen
# script.off("message", on_message) # remove listen
# 執(zhí)行
script.load()
# prevent the python script from terminating
log.info('Waiting for JavaScript...')
sys.stdin.read()
7、listen: on / off
# 設(shè)備事件處理
device_manager = frida.get_device_manager()
device_manager.on('changed', on_changed) # listen
device_manager.off('changed', on_changed) # remove listener
# 監(jiān)聽設(shè)備插拔
device_manager.on('add', on_changed)
device_manager.on('changed', on_changed)
device_manager.on('remove', on_removed)
8合住、Demo示例
8.1 初始化設(shè)備
import frida
# 初始化設(shè)備連接
def init_device():
Log.info('Current frida version: ' + str(frida.__version__))
# 獲取設(shè)備管理器
manager = frida.get_device_manager()
Log.print('Select a frida device:')
# 默認(rèn)設(shè)備連接方式
devices = manager.enumerate_devices()
for i, ldevice in enumerate(devices, 1):
Log.print(str(i) + ' => ' + str(ldevice))
# 選擇設(shè)備連接方式
select = int(input())
if select > len(devices):
Log.error('Out of range.')
sys.exit(1)
device_id = devices[select - 1].id
# 鏈接設(shè)備: 獲取指定 UID 設(shè)備
device = manager.get_device(device_id, 1)
Log.info('Connect to device \'' + device.name + '\' successfully.')
return device
if __name__ == '__main__':
try:
device = init_device()
# 遍歷需要hook的APP&進(jìn)程列表
for per_hook_process in processes_to_hook:
# 鏈接設(shè)備
session = attach_android(per_hook_process['name'], per_hook_process['identifier'])
# js 腳本注入
for js_module in js_modules:
process_name_var = 'var __process_name = "' + per_hook_process['identifier'] + '";'
module_name_var = 'var __module_name = "' + js_module['name'] + '";'
full_js_file_name = 'example/hook_' + js_module['type'] + '_' + js_module['name'] + '.js'
with open(full_js_file_name) as f:
Log.info('Inject script name: ' + full_js_file_name)
script = session.create_script(process_name_var + module_name_var + f.read())
script.on('message', on_message)
Log.info('Load script name: ' + full_js_file_name)
script.load()
Log.info('Waiting for JavaScript...')
print('----------------------------------------')
sys.stdin.read()
except Exception as e:
Log.error(repr(e))
8.2 設(shè)備連接 Android & iOS
1)Android
def attach_android(app_name: str, app_identifier: str):
"""Android設(shè)備連接方式
"""
try:
# 清理APP應(yīng)用數(shù)據(jù)
Log.info('Launching process \'' + app_name + '\'')
subprocess.call("adb shell pm clear " + app_identifier, shell=True)
# 通過monkey指令指定包名喚醒APP
subprocess.call("adb shell monkey -v 1 -p " + app_identifier, shell=True)
except frida.ExecutableNotFoundError as e2:
Log.error('Unable to find execuable \'' + app_name + '\'.')
Log.info('Attaching process \'' + app_name + '\'')
time.sleep(5)
return device.attach(app_name)
2)iOS
def attach_ios(app_name: str, app_identifier: str):
"""iOS設(shè)備連接方式
"""
try:
device.get_process(app_name)
except frida.ProcessNotFoundError as e:
Log.warn('Unable to find process \'' + app_name + '\', try to spawn...')
# Must use identifier to spawn
try:
pid = device.spawn(app_identifier)
device.resume(pid)
time.sleep(5)
except frida.ExecutableNotFoundError as e2:
Log.error('Unable to find execuable \'' + app_name + '\'.')
Log.info('Attaching: ' + app_name)
return device.attach(app_name)
8.3 js腳本注入
js_modules = [
{'type': 'android', 'name': 'env'},
]
def inject_js(session: frida.core.Session, modules: list, app_identifier: str):
"""注入JS腳本
"""
# js 腳本注入
for js_module in modules:
process_name_var = 'var __process_name = "' + app_identifier + '";'
module_name_var = 'var __module_name = "' + js_module['name'] + '";'
full_js_file_name = 'example/hook_' + js_module['type'] + '_' + js_module['name'] + '.js'
# 加載js腳本
with open(full_js_file_name) as f:
Log.info('Inject script name: ' + full_js_file_name)
script = session.create_script(process_name_var + module_name_var + f.read())
# 打印日志
script.on('message', on_message)
# 執(zhí)行注入
Log.info('Load script name: ' + full_js_file_name)
script.load()
8.4 執(zhí)行
if __name__ == '__main__':
try:
device = init_device()
# 遍歷需要hook的APP&進(jìn)程列表
for per_hook_process in processes_to_hook:
# 鏈接設(shè)備
session = attach_android(per_hook_process['name'], per_hook_process['identifier'])
# js 腳本注入
inject_js(session, js_modules)
Log.info('Waiting for JavaScript...')
print('----------------------------------------')
sys.stdin.read()
except Exception as e:
Log.error(repr(e))
三绰精、frida-compile 實(shí)踐
1、環(huán)境配置
# 安裝 frida
pip install frida
pip install frida-tools
# 安裝 node
brew install node
# 環(huán)境配置完畢后聊疲,在工程目錄安裝項(xiàng)目依賴(package.json文件), 使用教程: https://www.runoob.com/nodejs/nodejs-npm.html
npm install
// package.json
"scripts": {
"prepare": "npm run build",
"build": "frida-compile agent/android.ts -o _android.js -c && frida-compile agent/ios.ts -o _ios.js -c",
"watch": "frida-compile agent/android.ts -o _android.js -w && frida-compile agent/ios.ts -o _ios.js -w",
"test_android": "python runner.py android",
"test_ios": "python runner.py ios"
}
// runner.py
import sys
import subprocess
import time
if __name__ == '__main__':
platform = sys.argv[1] if len(sys.argv) >= 2 else ""
extraParam = " ".join(sys.argv[2:]) if len(sys.argv) >= 4 else ""
if platform == 'android':
bundle = sys.argv[2] if len(sys.argv) == 3 else "com.app.application"
// 殺死應(yīng)用 & 啟動(dòng)APP
subprocess.call(f"adb shell am force-stop {bundle} && adb shell am start -n {bundle}/.MainActivityV2", shell=True)
time.sleep(5)
// 注入&執(zhí)行js腳本
subprocess.call(f"frida -U -l _android.js -F {extraParam}", shell=True)
elif platform == 'ios':
bundle = sys.argv[2] if len(sys.argv) == 3 else "com.app.application"
// 拉起應(yīng)用(iOS 手動(dòng)點(diǎn)擊啟動(dòng)無法 attach 進(jìn)程)
subprocess.call(f"frida -U -f {bundle} {extraParam} &", shell=True)
time.sleep(5)
// 注入&執(zhí)行js腳本(注意attach的進(jìn)程是Gadget茬底,而不是對(duì)應(yīng)的bundleid)
subprocess.call(f"frida -U -l _ios.js -n Gadget {extraParam}", shell=True)
else:
print("[*] Invalid platform " + platform)
2沪悲、編譯執(zhí)行測(cè)試代碼
1)安裝包含 Frida SDK 測(cè)試包获洲,通過 USB 線將手機(jī)與電腦連接
2)編譯 JavaScript 用例代碼,編譯生成
_android.js
殿如、_ios.js
兩個(gè)文件贡珊,文件內(nèi)包含 import 測(cè)試用例npm run build
3)執(zhí)行測(cè)試
# 安靜模式執(zhí)行測(cè)試腳本,30秒后自動(dòng)退出 npm run test_android -- com.app.application -q -t 30 npm run test_ios -- com.app.application -q -t 30
命令支持的參數(shù)如下:
--runtime {qjs,v8} :執(zhí)行 JS 腳本的引擎 --pause:創(chuàng)建進(jìn)程成功后涉馁,暫停應(yīng)用主線程(main thread) -q :安靜模式(沒有 prompt)執(zhí)行完腳本后立即退出 -t TIMEOUT:在安靜模式下门岔,等待 N 秒后退出進(jìn)程
4)增量編譯:開啟一個(gè)終端,輸入 watch 命令監(jiān)聽
_android.js
烤送、_ios.js
是否有變化寒随,有變化則會(huì)重新加載 js 文件npm run watch
注:增量編譯后,偶現(xiàn) Frida Session 關(guān)閉問題,遇到關(guān)閉后可以重新執(zhí)行測(cè)試命令