抖音資料采集Frida教程,Java、Interceptor、NativePointer(Function/Callback)使用方法及示例
注意,運行以下任何代碼時都需要提前啟動手機中的frida-server檔案,
1.1 Java物件
Java是十分哦不,應該說是極其重要的API,無論是想對so層亦或java層進行攔截,都必須撰寫Java.perform,在使用上面這些API時,應該都已經發現了吧這章我們就來詳細看看`Java`物件都有哪些`API`
1.1.1 Java.available
該函式一般用來判斷當前行程是否加載了JavaVM,Dalvik或ART虛擬機,咱們來看代碼示例!
function frida_Java() {
Java.perform(function () {
//作為判斷用
if(Java.available)
{
//注入的邏輯代碼
console.log("hello java vm");
}else{
//未能正常加載JAVA VM
console.log("error");
}
});
}
setImmediate(frida_Java,0);
輸出如下,
hello java vm
核心注入的邏輯代碼寫在<注入的邏輯代碼>內會非常的安全萬無一失~
1.1.2 Java.androidVersion
顯示android系統版本號
function frida_Java() {
Java.perform(function () {
//作為判斷用
if(Java.available)
{
//注入的邏輯代碼
console.log("",Java.androidVersion);
}else{
//未能正常加載JAVA VM
console.log("error");
}
});
}
setImmediate(frida_Java,0);
輸出如下,
9
因為我的系統版本是9版本~
1.1.3 列舉類Java.enumerateLoadedClasses
該API列舉當前加載的所有類資訊,它有一個回呼函式分別是onMatch、onComplete函式,我們來看看代碼示例以及效果!
function frida_Java() {
Java.perform(function () {
if(Java.available)
{
//console.log("",Java.androidVersion);
//列舉當前加載的所有類
Java.enumerateLoadedClasses({
//每一次回呼此函式時其引數className就是類的資訊
onMatch: function (className)
{
//輸出類字串
console.log("",className);
},
//列舉完畢所有類之后的回呼函式
onComplete: function ()
{
//輸出類字串
console.log("輸出完畢");
}
});
}else{
console.log("error");
}
});
}
setImmediate(frida_Java,0);
咱們來看執行的效果圖1-7,
圖1-7 終端執行
它還有一個好兄弟 Java.enumerateLoadedClassesSync(),它回傳的是一個陣列,
1.1.4 列舉類加載器Java.enumerateLoadedClasses
該api列舉Java VM中存在的類加載器,其有一個回呼函式,分別是onMatch: function (loader)與onComplete: function (),接著我們來看代碼示例,
function frida_Java() {
Java.perform(function () {
if(Java.available)
{
//列舉當前加載的Java VM類加載器
Java.enumerateClassLoaders({
//回呼函式,引數loader是類加載的資訊
onMatch: function (loader)
{
console.log("",loader);
},
//列舉完畢所有類加載器之后的回呼函式
onComplete: function ()
{
console.log("end");
}
});
}else{
console.log("error");
}
});
}
setImmediate(frida_Java,0);
執行的效果圖1-8,
圖1-8 終端執行
它也有一個好兄弟叫Java.enumerateClassLoadersSync()也是回傳的陣列,
1.1.5 附加呼叫Java.perform
該API極其重要,Java.perform(fn)主要用于當前執行緒附加到Java VM并且呼叫fn方法,我們來看看示例代碼及其含義,
function frida_Java() {
//運行當前js腳本時會對當前執行緒附加到Java VM虛擬機,并且執行function方法
Java.perform(function () {
//判斷是否Java VM正常運行
if(Java.available)
{
//如不意外會直接輸出 hello
console.log("hello");
}else{
console.log("error");
}
});
}
setImmediate(frida_Java,0);
輸出如下,
[Google Pixel::com.roysue.roysueapplication]-> hello
沒錯你猜對了,它也有一個好兄弟,Java.performNow(fn)~
1.1.6 獲取類Java.use
Java.use(className),動態獲取className的類定義,通過對其呼叫$new()來呼叫建構式,可以從中實體化物件,當想要回收類時可以呼叫$Dispose()方法顯式釋放,當然也可以等待JavaScript的垃圾回識訓制,當實體化一個物件之后,可以通過其實體物件呼叫類中的靜態或非靜態的方法,官方代碼示例定義如下,
Java.perform(function () {
//獲取android.app.Activity類
var Activity = Java.use('android.app.Activity');
//獲取java.lang.Exception類
var Exception = Java.use('java.lang.Exception');
//攔截Activity類的onResume方法
Activity.onResume.implementation = function () {
//呼叫onResume方法的時候,會在此處被攔截并且呼叫以下代碼拋出例外!
throw Exception.$new('Oh noes!');
};
});
1.1.7 掃描實體類Java.choose
在堆上查找實體化的物件,示例代碼如下!
Java.perform(function () {
//查找android.view.View類在堆上的實體化物件
Java.choose("android.view.View", {
//列舉時呼叫
onMatch:function(instance){
//列印實體
console.log(instance);
},
//列舉完成后呼叫
onComplete:function() {
console.log("end")
}});
});
輸出如下:
android.view.View{2292774 V.ED..... ......ID 0,1794-1080,1920 #1020030 android:id/navigationBarBackground}
android.view.View{d43549d V.ED..... ......ID 0,0-1080,63 #102002f android:id/statusBarBackground}
end
1.1.8 型別轉換器Java.cast
Java.cast(handle, klass),就是將指定變數或者資料強制轉換成你所有需要的型別;創建一個 JavaScript 包裝器,給定從 Java.use() 回傳的給定類klas的句柄的現有實體,此類包裝器還具有用于獲取其類的包裝器的類屬性,以及用于獲取其類名的字串表示的$className屬性,通常在攔截so層時會使用此函式將jstring、jarray等等轉換之后查看其值,
1.1.9 定義任意陣列型別Java.array
frida提供了在js代碼中定義java陣列的api,該陣列可以用于傳遞給java API,我們來看看如何定義,代碼示例如下,
Java.perform(function () {
//定義一個int陣列、值是1003, 1005, 1007
var intarr = Java.array('int', [ 1003, 1005, 1007 ]);
//定義一個byte陣列、值是0x48, 0x65, 0x69
var bytearr = Java.array('byte', [ 0x48, 0x65, 0x69 ]);
for(var i=0;i<bytearr.length;i++)
{
//輸出每個byte元素
console.log(bytearr[i])
}
});
我們通過上面定義int陣列和byte的例子可以知道其定義格式為Java.array('type',[value1,value2,....]);那它都支持type呢?我們來看看~
| type | 含義 |
|---|---|
| Z | boolean |
| B | byte |
| C | char |
| S | short |
| I | int |
| J | long |
| F | float |
| D | double |
| V | void |
1.1.10 注冊類Java.registerClass(spec)
Java.registerClass:創建一個新的Java類并回傳一個包裝器,其中規范是一個包含:name:指定類名稱的字串,superClass:(可選)父類,要從 java.lang.Object 繼承的省略,implements:(可選)由此類實作的介面陣列,fields:(可選)物件,指定要公開的每個欄位的名稱和型別,methods:(可選)物件,指定要實作的方法,
注冊一個類,回傳類的實體,下面我貼一個基本的用法~實體化目標類物件并且呼叫類中的方法
Java.perform(function () {
//注冊一個目標行程中的類,回傳的是一個類物件
var hellojni = Java.registerClass({
name: 'com.roysue.roysueapplication.hellojni'
});
console.log(hellojni.addInt(1,2));
});
我們再深入看看官方怎么來玩的:
//獲取目標行程的SomeBaseClass類
var SomeBaseClass = Java.use('com.example.SomeBaseClass');
//獲取目標行程的X509TrustManager類
var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
var MyWeirdTrustManager = Java.registerClass({
//注冊一個類是行程中的MyWeirdTrustManager類
name: 'com.example.MyWeirdTrustManager',
//父類是SomeBaseClass類
superClass: SomeBaseClass,
//實作了MyWeirdTrustManager介面類
implements: [X509TrustManager],
//類中的屬性
fields: {
description: 'java.lang.String',
limit: 'int',
},
//定義的方法
methods: {
//類的建構式
$init: function () {
console.log('Constructor called');
},
//X509TrustManager介面中方法之一,該方法作用是檢查客戶端的證書
checkClientTrusted: function (chain, authType) {
console.log('checkClientTrusted');
},
//該方法檢查服務器的證書,不信任時,在這里通過自己實作該方法,可以使之信任我們指定的任何證書,在實作該方法時,也可以簡單的不做任何處理,即一個空的函式體,由于不會拋出例外,它就會信任任何證書,
checkServerTrusted: [{
//回傳值型別
returnType: 'void',
//引數串列
argumentTypes: ['[Ljava.security.cert.X509Certificate;', 'java.lang.String'],
//實作方法
implementation: function (chain, authType) {
//輸出
console.log('checkServerTrusted A');
}
}, {
returnType: 'java.util.List',
argumentTypes: ['[Ljava.security.cert.X509Certificate;', 'java.lang.String', 'java.lang.String'],
implementation: function (chain, authType, host) {
console.log('checkServerTrusted B');
//回傳null會信任所有證書
return null;
}
}],
// 回傳受信任的X509證書陣列,
getAcceptedIssuers: function () {
console.log('getAcceptedIssuers');
return [];
},
}
});
我們來看看上面的示例都做了啥?實作了證書類的javax.net.ssl.X509TrustManager類,,這里就是相當于自己在目標行程中重新創建了一個類,實作了自己想要實作的類構造,重構造了其中的三個介面函式、從而繞過證書校驗,
1.1.11 Java.vm物件
Java.vm物件十分常用,比如想要拿到JNI層的JNIEnv物件,可以使用getEnv();我們來看看具體的使用和基本小實體,~
function frida_Java() {
Java.perform(function () {
//攔截getStr函式
Interceptor.attach(Module.findExportByName("libhello.so" , "Java_com_roysue_roysueapplication_hellojni_getStr"), {
onEnter: function(args) {
console.log("getStr");
},
onLeave:function(retval){
//它的回傳值的是retval 在jni層getStr的回傳值的jstring
//我們在這里做的事情就是替換掉結果
//先獲取一個Env物件
var env = Java.vm.getEnv();
//通過newStringUtf方法構建一個jstirng字串
var jstring = env.newStringUtf('roysue');
//replace替換掉結果
retval.replace(jstring);
console.log("getSum方法回傳值為:roysue")
}
});
}
setImmediate(frida_Java,0);
1.2 Interceptor物件
該物件功能十分強大,函式原型是Interceptor.attach(target, callbacks):引數target是需要攔截的位置的函式地址,也就是填某個so層函式的地址即可對其攔截,target是一個NativePointer引數,用來指定你想要攔截的函式的地址,NativePointer我們也學過是一個指標,需要注意的是對于Thumb函式需要對函式地址+1,callbacks則是它的回呼函式,分別是以下兩個回呼函式:
1.2.1 Interceptor.attach
onEnter:函式(args):回呼函式,給定一個引數args,可用于讀取或寫入引數作為 NativePointer 物件的陣列,onLeave:函式(retval):回呼函式給定一個引數 retval,該引數是包含原始回傳值的 NativePointer 派生物件,可以呼叫 retval.replace(1337) 以整數 1337 替換回傳值,或者呼叫 retval.replace(ptr("0x1234"))以替換為指標,請注意,此物件在 OnLeave 呼叫中回收,因此不要將其存盤在回呼之外并使用它,如果需要存盤包含的值,請制作深副本,例如:ptr(retval.toString()),
我們來看看示例代碼~
//使用Module物件getExportByNameAPI直接獲取libc.so中的匯出函式read的地址,對read函式進行附加攔截
Interceptor.attach(Module.getExportByName('libc.so', 'read'), {
//每次read函式呼叫的時候會執行onEnter回呼函式
onEnter: function (args) {
this.fileDescriptor = args[0].toInt32();
},
//read函式執行完成之后會執行onLeave回呼函式
onLeave: function (retval) {
if (retval.toInt32() > 0) {
/* do something with this.fileDescriptor */
}
}
});
通過我們對Interceptor.attach函式有一些基本了解了~它還包含一些屬性
我們來看看示例代碼,
function frida_Interceptor() {
Java.perform(function () {
//對So層的匯出函式getSum進行攔截
Interceptor.attach(Module.findExportByName("libhello.so" , "Java_com_roysue_roysueapplication_hellojni_getSum"), {
onEnter: function(args) {
//輸出
console.log('Context information:');
//輸出背景關系因其是一個Objection物件,需要它進行接送、轉換才能正常看到值
console.log('Context : ' + JSON.stringify(this.context));
//輸出回傳地址
console.log('Return : ' + this.returnAddress);
//輸出執行緒id
console.log('ThreadId : ' + this.threadId);
console.log('Depth : ' + this.depth);
console.log('Errornr : ' + this.err);
},
onLeave:function(retval){
}
});
});
}
setImmediate(frida_Interceptor,0);
我們注入腳本之后來看看執行之后的效果以及輸出的這些都是啥,執行的效果圖1-9,
圖1-9 終端執行
1.2.2 Interceptor.detachAll
簡單來說這個的函式的作用就是讓之前所有的Interceptor.attach附加攔截的回呼函式失效,
1.2.3 Interceptor.replace
相當于替換掉原本的函式,用替換時的實作替換目標處的函式,如果想要完全或部分替換現有函式的實作,則通常使用此函式,,我們也看例子,例子是最直觀的!代碼如下,
function frida_Interceptor() {
Java.perform(function () {
//這個c_getSum方法有兩個int引數、回傳結果為兩個引數相加
//這里用NativeFunction函式自己定義了一個c_getSum函式
var add_method = new NativeFunction(Module.findExportByName('libhello.so', 'c_getSum'),
'int',['int','int']);
//輸出結果 那結果肯定就是 3
console.log("result:",add_method(1,2));
//這里對原函式的功能進行替換實作
Interceptor.replace(add_method, new NativeCallback(function (a, b) {
//h不論是什么引數都回傳123
return 123;
}, 'int', ['int', 'int']));
//再次呼叫 則回傳123
console.log("result:",add_method(1,2));
});
}
我來看注入腳本之后的終端是是不是顯示了3和123見下圖1-10,
圖1-10 終端執行
1.3 NativePointer物件
同等與C語言中的指標
1.3.1 new NativePointer(s)
宣告定義NativePointer型別
function frida_NativePointer() {
Java.perform(function () {
//第一種字串定義方式 十進制的100 輸出為十六進制0x64
const ptr1 = new NativePointer("100");
console.log("ptr1:",ptr1);
//第二種字串定義方式 直接定義0x64 同等與定義十六進制的64
const ptr2 = new NativePointer("0x64");
console.log("ptr2:",ptr2);
//第三種定數值義方式 定義數字int型別 十進制的100 是0x64
const ptr3 = new NativePointer(100);
console.log("ptr3:",ptr3);
});
}
setImmediate(frida_NativePointer,0);
輸出如下,都會自動轉為十六進制的0x64
ptr1: 0x64
ptr2: 0x64
ptr3: 0x64
1.3.2 運算子以及指標讀寫API
它也能呼叫以下運算子

看完API含義之后,我們來使用他們,下面該腳本是readByteArray()示例~
function frida_NativePointer() {
Java.perform(function () {
console.log("");
//拿到libc.so在記憶體中的地址
var pointer = Process.findModuleByName("libc.so").base;
//讀取從pointer地址開始的16個位元組
console.log(pointer.readByteArray(0x10));
});
}
setImmediate(frida_NativePointer,0);
輸出如下:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 .ELF............
首先我先來用readByteArray函式來讀取libc.so檔案在記憶體中的資料,這樣我們方便測驗,我們從libc檔案讀取0x10個位元組的長度,肯定會是7F 45 4C 46...因為ELF檔案頭部資訊中的Magic屬性,
1.3.3 readPointer()
咱們直接從API索引11開始玩readPointer(),定義是從此記憶體位置讀取NativePointer,示例代碼如下,省略function以及Java.perform~
var pointer = Process.findModuleByName("libc.so").base;
console.log(pointer.readByteArray(0x10));
console.log("readPointer():"+pointer.readPointer());
輸出如下,
readPointer():0x464c457f
也就是將readPointer的前四個位元組的內容轉成地址產生一個新的NativePointer,
1.3.4 writePointer(ptr)
讀取ptr指標地址到當前指標
//先列印pointer指標地址
console.log("pointer :"+pointer);
//分配四個位元組的空間地址
const r = Memory.alloc(4);
//將pointer指標寫入剛剛申請的r內
r.writePointer(pointer);
//讀取r指標的資料
var buffer = Memory.readByteArray(r, 4);
//r指標內放的pointer指標地址
console.log(buffer);
輸出如下,
//console.log("pointer :"+pointer); 這句列印的地址 也就是libc的地址
pointer :0xf588f000
//console.log(buffer); 輸出buffer 0xf588f000在記憶體資料會以00 f0 88 f5方式顯示
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 00 f0 88 f5 ....
1.3.5 readS32()、readU32()
從該記憶體位置讀取有符號或無符號8/16/32/etc或浮點數/雙精度值,并將其作為數字回傳,這里拿readS32()、readU32()作為演示.
//從pointer地址讀4個位元組 有符號
console.log(pointer.readS32());
//從pointer地址讀4個位元組 無符號
console.log(pointer.readU32());
輸出如下,
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 .ELF............
1179403647 == 0x464c457f
1179403647 == 0x464c457f
1.3.6 writeS32()、writeU32()
將有符號或無符號8/16/32/等或浮點數/雙精度值寫入此記憶體位置,
//申請四個位元組的記憶體空間
const r = Memory.alloc(4);
//將0x12345678寫入r地址中
r.writeS32(0x12345678);
//輸出
console.log(r.readByteArray(0x10));
// writeS32()、writeU32()輸出的也是一樣的,只是區別是有符號和無符號
輸出如下,
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 78 56 34 12 00 00 00 00 00 00 00 00 00 00 00 00 xV4.............
1.3.7 readByteArray(length))、writeByteArray(bytes)
readByteArray(length))連續讀取記憶體length個位元組,、writeByteArray連續寫入記憶體bytes,
//先定義一個需要寫入的位元組陣列
var arr = [ 0x72, 0x6F, 0x79, 0x73, 0x75, 0x65];
//這里申請以arr大小的記憶體空間
const r = Memory.alloc(arr.length);
//將arr陣列位元組寫入r
Memory.writeByteArray(r,arr);
//讀取arr.length大小的陣列
var buffer = Memory.readByteArray(r, arr.length);
console.log("Memory.readByteArray:");
console.log(hexdump(buffer, {
offset: 0,
length: arr.length,
header: true,
ansi: false
}));
輸出如下,
Memory.readByteArray:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 72 6f 79 73 75 65 roysue
1.3.8 readCString([size = -1])、writeUtf8String(str)
readCString功能是讀取指標地址位置的位元組字串,對應的writeUtf8String是寫入指標地址位置的字串處,(這里的r是接著上面的代碼的變數),
//在這里直接使用readCString讀取會把上面的'roysue'字串讀取出來
console.log("readCString():"+r.readCString());
//這里是寫入字串 也就是 roysue起始位置開始被替換為haha
const newPtrstr = r.writeUtf8String("haha");
//替換完了之后再繼續輸出 必然是haha
console.log("readCString():"+newPtrstr.readCString());
圖1-11 終端執行
1.4 NativeFunction物件
創建新的NativeFunction以呼叫address處的函式(用NativePointer指定),其中rereturn Type指定回傳型別,argTypes陣列指定引數型別,如果不是系統默認值,還可以選擇指定ABI,對于可變函式,添加一個‘.’固定引數和可變引數之間的argTypes條目,我們來看看官方的例子,
// LargeObject HandyClass::friendlyFunctionName();
//創建friendlyFunctionPtr地址的函式
var friendlyFunctionName = new NativeFunction(friendlyFunctionPtr,
'void', ['pointer', 'pointer']);
//申請記憶體空間
var returnValue = https://www.cnblogs.com/titodata/archive/2021/01/18/Memory.alloc(sizeOfLargeObject);
//呼叫friendlyFunctionName函式
friendlyFunctionName(returnValue, thisPtr);
我來看看它的格式,函式定義格式為new NativeFunction(address, returnType, argTypes[, options]),參照這個格式能夠創建函式并且呼叫!returnType和argTypes[,]分別可以填void、pointer、int、uint、long、ulong、char、uchar、float、double、int8、uint8、int16、uint16、int32、uint32、int64、uint64這些型別,根據函式的所需要的type來定義即可,
在定義的時候必須要將引數型別個數和引數型別以及回傳值完全匹配,假設有三個引數都是int,則new NativeFunction(address, returnType, ['int', 'int', 'int']),而回傳值是int則new NativeFunction(address, 'int', argTypes[, options]),必須要全部匹配,并且第一個引數一定要是函式地址指標,
1.5 NativeCallback物件
new NativeCallback(func,rereturn Type,argTypes[,ABI]):創建一個由JavaScript函式func實作的新NativeCallback,其中rereturn Type指定回傳型別,argTypes陣列指定引數型別,您還可以指定ABI(如果不是系統默認值),有關支持的型別和Abis的詳細資訊,請參見NativeFunction,注意,回傳的物件也是一個NativePointer,因此可以傳遞給Interceptor#replace,當將產生的回呼與Interceptor.replace()一起使用時,將呼叫func,并將其系結到具有一些有用屬性的物件,就像Interceptor.Attach()中的那樣,我們來看一個例子,如下,利用NativeCallback做一個函式替換,
Java.perform(function () {
var add_method = new NativeFunction(Module.findExportByName('libhello.so', 'c_getSum'),
'int',['int','int']);
console.log("result:",add_method(1,2));
//在這里new一個新的函式,但是引數的個數和回傳值必須對應
Interceptor.replace(add_method, new NativeCallback(function (a, b) {
return 123;
}, 'int', ['int', 'int']));
console.log("result:",add_method(1,2));
});
結語
本篇咱們學習了非常實用的API,如Interceptor物件對so層匯出庫函式攔截、NativePointer物件的指標操作、NativeFunction物件的實體化so函式的使用等都是當前灰常好用的函式建議童鞋了多多嘗試
短視頻、直播資料實時采集介面,請查看檔案: TiToData
免責宣告:本檔案僅供學習與參考,請勿用于非法用途!否則一切后果自負,
轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/250591.html
標籤:其他
上一篇:與HBase對比,Cassandra的優勢特性是什么?
下一篇:基于業務和平臺理解數字營銷概念
