Frida Cheat Sheet

First Post:

Last Update:

前言

在动笔之前我其实还没想好该怎么写。比如这篇文章的内容是什么,我需要写到多详细,以及我所写作的目标是什么,读者读完后能收获什么,我全都没想好,在这么一种混沌的状态下,我开始动笔。

另外两个关于原理的文章:

Frida-gum 源代码速通笔记: https://bbs.kanxue.com/thread-278423.htm
Frida-Core 源代码速通笔记: https://bbs.kanxue.com/thread-278533.htm

本文主要还是关于使用,如果您对原理不感兴趣,只阅读本文即可。我希望它帮我实现的目的是,在我需要某个功能的时候能够直接通过本文的一些案例来完成。若未来有新的需求,希望能及时更新。

Usage Cheat Sheet

记录那些使用上的条目。

启动模式

frida 自带两种启动模式,分别是 attachspawn 。前者是启动应用以后再接入 frida ,后者则是先启动 frida 再启动应用。二者各有不同的好处,比方说程序在启动时检查 frida 的话,则后来附加进去的 frida 是不会被发现的;而如果想要在程序启动时就钩取一些函数,则 spawn 模式能够在最早的时机生效。

通过 python 运行时的模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import time
import frida

# 连接安卓机上的frida-server
device = frida.get_usb_device()
# 启动`demo02`这个app
pid = device.spawn(["com.demo.demo02"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
# 加载s1.js脚本
with open("s1.js") as f:
script = session.create_script(f.read())
script.load()

# 脚本会持续运行等待输入
raw_input()

注意这里的 device.resume 函数,通过 spawn 启动的程序会在启动后立即暂停,而该函数则会恢复程序的执行。

如果我们希望以 attach 的方式进行连接,那么在脚本调用 device.spawn 函数后立即使用 device.resume 即可实现相同的操作;而如果我们希望以 spawn 模式进行工作,那么在 device.spawn 函数后则需要先完成 script.load ,然后再调用 device.resume

Windows Platform

frida 在 Windows 平台下也能用来钩一些 PE 文件,装好环境以后能直接这样用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import subprocess
import string
string.printable
from colorama import init
init(autoreset=True)
import frida, sys
def on_message(message, data):
global new_number
print(message)
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
new_number = message['payload']
elif message['type'] == "error":
print(message["description"])
print(message["stack"])
print(message["fileName"],"line:",message["lineNumber"],"colum:",message["columnNumber"])
else:
print(message)

jscode = open("test.js","rb").read().decode()
process = subprocess.Popen("demo.exe",
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,bufsize=0
)
session = frida.attach("demo.exe")
script = session.create_script(jscode)
script.on('message', on_message)
print('[*] Start attach')
script.load()
process.stdin.write("data")
output, error = process.communicate()
print(output)
process.terminate()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var number=0;
function main()
{
var base = Module.findBaseAddress("demo.exe")
console.log(base)
if(base){
Interceptor.attach(base.add(0x11F4C), {
onEnter: function(args) {
if (res==1)
{
number+=1;
}
else{
send(number);
}
}
});
}
}
setImmediate(main);

Command Line

利用 frida-trace 跟踪两个函数,同时以 spawn 模式启动该程序:

1
frida-trace -U -i open -i strcmp -f com.android.chrome

跟踪所有 libcommonCrypto 下的函数,这里是通配符:

1
frida-trace -U -I "libcommonCrypto*" -f com.toyopagroup.picaboo

在 IOS 平台下用这样跟踪 OBJC 函数:

1
frida-trace -U -m "-[NSView drawRect:]" -f target

跟踪类BitmapFactory中方法名中包含 native 的所有Java方法:

1
frida-trace -U -j '*BitmapFactory*!*native*' com.android.chrome

提一句:frida-trace 的原理实际上是会根据参数在当前目录下生成对应的 JavaScript 脚本,然后再用 frida 加载这份脚本实现跟踪。所以我们实际上可以先用这个方法把目标钩出来,然后魔改生成的脚本就能比较方便的实现自定义功能了。

如果需要启用内置的 V8 引擎,也能通过这个参数启用:

1
frida -U -f com.apple.calculator --runtime=v8 -l agent.js

但请注意,V8 引擎并不总是有效的,在某些平台的某些版本下,启用后会导致无法正常 hook

如果我们希望在命令行模式下以启动命令的方式,却使用类似 attach 的办法,通过添加 --no-pause 参数可以避免程序在启动后被中断:

1
frida -U --no-pause -l hookNative.js -f com.example.app

Remote Frida Server

在一些使用网络进行通信的场景下,利用 frida-server 可以创建远程的调试环境:

1
frida-server -l 0.0.0.0
1
frida-trace -H 192.168.1.3 -i "open*"

Code Cheat Sheet

记录那些代码的条目。

Script Function

我们有这些函数可供使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import inspect
import frida
for mb in inspect.getmembers(frida, inspect.isfunction):
print(mb)

('attach', <function attach at 0x000002A27AAC9318>)
('enumerate_devices', <function enumerate_devices at 0x000002A27AAFAF78>)
('get_device', <function get_device at 0x000002A27AAF64C8>)
('get_device_manager', <function get_device_manager at 0x000002A27AA9D4C8>)
('get_device_matching', <function get_device_matching at 0x000002A27AAF63A8>)
('get_local_device', <function get_local_device at 0x000002A27AAE8F78>)
('get_remote_device', <function get_remote_device at 0x000002A27AAE81F8>)
('get_usb_device', <function get_usb_device at 0x000002A27AAF6558>)
('inject_library_blob', <function inject_library_blob at 0x000002A27AA9A558>)
('inject_library_file', <function inject_library_file at 0x000002A27AAD8EE8>)
('kill', <function kill at 0x000002A27AAAE558>)
('query_system_parameters', <function query_system_parameters at 0x000002A27A894048>)
('resume', <function resume at 0x000002A27AAAE048>)
('shutdown', <function shutdown at 0x000002A27AAE2168>)
('spawn', <function spawn at 0x000002A27AAA1318>)

在下面的代码中可能会涉及到这些函数的使用。

Python Template

一个简单的 Frida-Python 交互模板,通过 spawn 模式启动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Early instrumentation
import frida, sys, time

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

js = """js code here"""

device = frida.get_usb_device()
pid = device.spawn(["com.example.app"])
session = device.attach(pid)
script = session.create_script(js)
script.on('message', on_message)
script.load()
device.resume(pid)
sys.stdin.read()

或是通过附加的方式接入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Normal start - app needs to be opened
import frida, sys

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

js = """js code here"""

process = frida.get_usb_device().attach('com.example.app')
script = process.create_script(js)
script.on('message', on_message)
script.load()
sys.stdin.read()

JavaScript Template

记录那些常用的 JavaScript 脚本,希望我们不再需要反复的手写了。

替换函数实现

通过 Java.use 找到特定的类,然后使用 class.function_name.implementation 能够获取到其实现,如果修改改变量则会替换该函数;而在其中调用 this 可以访问原本的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
console.log("Script loaded successfully ");
Java.perform(function x() {
console.log("Inside java perform function");
//定位类
var my_class = Java.use("com.demo.MainActivity");
console.log("Java.Use.Successfully!");//定位类成功!
//在这里更改类的方法的实现(implementation)
my_class.fun.implementation = function(x,y){
//打印替换前的参数
console.log( "original call: fun("+ x + ", " + y + ")");
//把参数替换成2和5,依旧调用原函数
var ret_value = this.fun(2, 5);
return ret_value;
}
});

获取类及其下的方法

或许可以通过这样的方法来获得一个类和它对应的方法,在这个用例里我们打印了 MainActivity 下的所有方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const main = Java.use("com.example.app.MainActivity");
function inspectClass(obj) {
const obj_class = Java.cast(obj.getClass(), Class);
const fields = obj_class.getDeclaredFields();
const methods = obj_class.getMethods();
console.log("Inspect " + obj.getClass().toString());
console.log("\tFields:");
for (var i in fields)
console.log("\t" + fields[i].toString());
console.log("\tMethods:");
for (var i in methods)
console.log("\t" + methods[i].toString());
}
inspectClass(main)

获取类及其下的 native 方法

在这篇问答中提到:Find manually registered (obfuscated) native function address

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
var RevealNativeMethods = function() {
var pSize = Process.pointerSize;
var env = Java.vm.getEnv();
var RegisterNatives = 215, FindClassIndex = 6; // search "215" @ https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html
var jclassAddress2NameMap = {};
function getNativeAddress(idx) {
return env.handle.readPointer().add(idx * pSize).readPointer();
}
// intercepting FindClass to populate Map<address, jclass>
Interceptor.attach(getNativeAddress(FindClassIndex), {
onEnter: function(args) {
jclassAddress2NameMap[args[0]] = args[1].readCString();
}
});
// RegisterNative(jClass*, .., JNINativeMethod *methods[nMethods], uint nMethods) // https://android.googlesource.com/platform/libnativehelper/+/master/include_jni/jni.h#977
Interceptor.attach(getNativeAddress(RegisterNatives), {
onEnter: function(args) {
for (var i = 0, nMethods = parseInt(args[3]); i < nMethods; i++) {
/*
https://android.googlesource.com/platform/libnativehelper/+/master/include_jni/jni.h#129
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
*/
var structSize = pSize * 3; // = sizeof(JNINativeMethod)
var methodsPtr = ptr(args[2]);
var signature = methodsPtr.add(i * structSize + pSize).readPointer();
var fnPtr = methodsPtr.add(i * structSize + (pSize * 2)).readPointer(); // void* fnPtr
var jClass = jclassAddress2NameMap[args[0]].split('/');
console.log('\x1b[3' + '6;01' + 'm', JSON.stringify({
module: DebugSymbol.fromAddress(fnPtr)['moduleName'], // https://www.frida.re/docs/javascript-api/#debugsymbol
package: jClass.slice(0, -1).join('.'),
class: jClass[jClass.length - 1],
method: methodsPtr.readPointer().readCString(), // char* name
signature: signature.readCString(), // char* signature TODO Java bytecode signature parser { Z: 'boolean', B: 'byte', C: 'char', S: 'short', I: 'int', J: 'long', F: 'float', D: 'double', L: 'fully-qualified-class;', '[': 'array' } https://github.com/skylot/jadx/blob/master/jadx-core/src/main/java/jadx/core/dex/nodes/parser/SignatureParser.java
address: fnPtr
}), '\x1b[39;49;00m');
}
}
});
}
Java.perform(RevealNativeMethods);

对特定动态库进行 hook

1
2
3
4
5
6
7
8
9
10
11
var sign2 = Module.findExportByName("libhello-jni.so", "Java_com_example_hellojni_HelloJni_sign2");
console.log(sign2);
Interceptor.attach(sign2, {
onEnter: function (args) {
//jstring
console.log("sign2 str1:", ptr(Java.vm.tryGetEnv().getStringUtfChars(args[2])).readCString());
console.log("sign2 str2:", ptr(Java.vm.tryGetEnv().getStringUtfChars(args[3])).readCString());
}, onLeave: function (retval) {
console.log("sign2 retval:", ptr(Java.vm.tryGetEnv().getStringUtfChars(retval)).readCString());
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Module.enumerateExports("mylib.so", {
onMatch: function(e) {
if (e.type == 'function') {
console.log("name of function = " + e.name);
if (e.name == "Java_example_decrypt") {
console.log("Function Decrypt recognized by name");
Interceptor.attach(e.address, {
onEnter: function(args) {
console.log("Interceptor attached onEnter...");
},
onLeave: function(retval) {
console.log("Interceptor attached onLeave...");
}
});
}
}
},
onComplete: function() {}
});

请注意,这个方法并不总是有效,尤其是当我们需要以 spawn 的方式启动应用时,这个脚本会由于目标还尚未被加载导致无效。

修改 native 函数返回值

1
2
3
4
5
6
7
8
9
10
11
Interceptor.attach(Module.getExportByName('libnative-lib.so', 'Jniint'), {
onEnter: function(args) {
this.first = args[0].toInt32(); // int
console.log("on enter with: " + this.first)
},
onLeave: function(retval) {
const dstAddr = Java.vm.getEnv().newIntArray(1117878);
console.log("dstAddr is : " + dstAddr.toInt32())
retval.replace(dstAddr);
}
});

如果 getExportByName 的第一个参数为 null,那么 frida 将会查找所有动态库

dlopen hook

dlopen 往往被用于加载动态库,那么如果对这个函数进行 hook,那么就能解决前文所说的 “在加载动态库后再对其进行 hook” 的操作了。

主动加载:

1
2
3
4
5
6
7
const dlopen = new NativeFunction(Module.findExportByName(null, 'dlopen'), 'pointer', ['pointer', 'int']);
const dlerror = new NativeFunction(Module.findExportByName(null, 'dlerror'), 'pointer', []);
const path = Memory.allocUtf8String("/data/local/tmp/libdummy.so");
var ret = dlopen(path, 2);
console.log("ret = " + ret);
var error = dlerror();
console.log("error = " + Memory.readUtf8String(error));

被动加载:

1
2
3
4
5
6
7
8
9
var dlopen = new NativeFunction(Module.findExportByName(null, 'dlopen'), 'pointer', ['pointer', 'int']);
Interceptor.replace(dlopen, new NativeCallback(function(path, mode) {
console.log("dlopen(" + "path=\"" + Memory.readUtf8String(path) + "\"" + ", mode=" + mode + ")");
var name = Memory.readUtf8String(path);
if (name !== null) {
console.log("[*] dlopen " + name);
}
return dlopen(path, mode);
}, 'pointer', ['pointer', 'int']));

更常用的方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//刚注入的时候这个so还没加载,需要hook dlopen
function inline_hook() {
var base_hello_jni = Module.findBaseAddress("libxxxx.so");
console.log("base_hello_jni:", base_hello_jni);
if (base_hello_jni) {
console.log(base_hello_jni);
//inline hook
var addr_07320 = base_hello_jni.add(0x07320);//指令执行的地址,不是变量所在的栈或堆
Interceptor.attach(addr_07320, {
onEnter: function (args) {
console.log("addr_07320 x13:", this.context.x13);//注意这里是怎么得到寄存器值的
}, onLeave: function (retval) {
}
});
}
}

//8.0以下所有的so加载都通过dlopen
function hook_dlopen() {
var dlopen = Module.findExportByName(null, "dlopen");
Interceptor.attach(dlopen, {
onEnter: function (args) {
this.call_hook = false;
var so_name = ptr(args[0]).readCString();
if (so_name.indexOf("libxxxx.so") >= 0) {
console.log("dlopen:", ptr(args[0]).readCString());
this.call_hook = true;//dlopen函数找到了
}

}, onLeave: function (retval) {
if (this.call_hook) {//dlopen函数找到了就hook so
inline_hook();
}
}
});
// 高版本Android系统使用android_dlopen_ext
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
this.call_hook = false;
var so_name = ptr(args[0]).readCString();
if (so_name.indexOf("libhxxxx.so") >= 0) {
console.log("android_dlopen_ext:", ptr(args[0]).readCString());
this.call_hook = true;
}

}, onLeave: function (retval) {
if (this.call_hook) {
inline_hook();
}
}
});
}

通过这个方式能保证在对动态库进行钩取时候对其是否加载进行检测。

在限定 Android 平台时,还可以直接对 loadLibrary 进行 hook:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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) {
try {
console.log('System.loadLibrary("' + library + '")');
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), library);
} catch(ex) {
console.log(ex);
}
};

System.load.implementation = function(library) {
try {
console.log('System.load("' + library + '")');
Runtime.getRuntime().nativeLoad(library, VMStack.getCallingClassLoader());
} catch(ex) {
console.log(ex);
}
};
});

打印调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const membase = Module.findBaseAddress('libtest.so');
const funcs = [ '0x21B248', '0x21D0C8', '0x234730', '0x23F718', '0x259E68' ];
for (var i in funcs) {
var funcPtr = membase.add(funcs[i]);
var handler = (function() {
var name = funcs[i];
return function(args) {
console.log(name + ': ');
var trace = Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress);
for (var j in trace)
console.log(trace[j]);
};
})();
Interceptor.attach(funcPtr, {onEnter: handler});
}

用 Stalker 跟踪函数接下来的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const funcs = [ '0x870FF0', '0x871BA0' ];
const STALKED = 12345;
const STARTING_ADDRESS = "0x102FE0";
const ENDING_ADDRESS = "0x89BE04";
const base = Module.findBaseAddress('libtest.so');
var threads = [];
for (var i in funcs) {
console.log('Hooking funcs[' + i + '] ' + funcs[i]);
Interceptor.attach(base.add(funcs[i]), {
onEnter: function (args) {
var tid = Process.getCurrentThreadId();
if (threads[tid] == STALKED)
return;
Stalker.follow(tid, {
events: {
call: true, // CALL instructions: yes please
ret: false, // RET instructions: no thanks
exec: false // all instructions: no thanks
},
onCallSummary: function (summary) {
var log = []
for (i in summary) {
var addr = ptr(i).sub(base));
if (addr.compare(ptr(STARTING_ADDRESS)) >= 0 && addr.compare(ptr(ENDING_ADDRESS)) <= 0)
log.push(addr);
}
console.log(JSON.stringify(log));
}
});
threads[tid] = STALKED;
},
onLeave: function (retval) {
var tid = Process.getCurrentThreadId();
if (threads[tid] == STALKED)
return;
Stalker.unfollow(tid);
Stalker.garbageCollect();
}
});
}

events 中能决定跟踪的颗粒度,包含跟踪所有 call,ret 和所有指令三个等级

Hook一个Obj-C方法

1
2
3
4
5
6
7
const sendMessage = ObjC.classes.SecureStorage["- readFile:"];
Interceptor.attach(sendMessage.implementation, {
onLeave: function (retval) {
var message = ObjC.Object(retval);
console.log("- [SecureStorage readFile:] -->\n\"" + message.toString() + "\"");
}
});

JavaScript Function

string2byte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function string2Bytes(str) {  
var ch, st, re = [];
for (var i = 0; i < str.length; i++ ) {
ch = str.charCodeAt(i);
st = [];
do {
st.push( ch & 0xFF );
ch = ch >> 8;
}
while ( ch );
re = re.concat( st.reverse() );
}
return re;
}

hex2bytes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const hex2bytes=(hex)=>{
let pos=0,len=hex.length;
if(len%2!=0){
return null
}
len/2;
let bytes=new Array();
for(let i=0;i<len;i++){
let s=hex.substr(pos,2);
let v=parseInt(s,16);
bytes.push(v);
pos+=2;
}
return bytes
}

bytes2hex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export const bytes2hex=(bytes)=>{
let hex="",len=bytes.length;
for(let i=0;i<len;i++){
let tmp,num=bytes[i];
if(num<0){
tmp=(255+num+1).toString(16);
}else{
tmp=num.toString(16);
}
if(tmp.length==1){
return "0"+tmp;
}
hex+=tmp;
}
return hex
}

Other

frida-snippets: 一些可用的脚本或工具