Frida-CheatSheet
Android逆向-Frida-CheatSheet
加载脚本
直接执行Js脚本(Windows)
1 | frida <进程路径> -l <js脚本> |
直接启动APP并执行Js脚本(Android)
1 | frida -U -f com.example.a11x256.frida_test -l <js脚本> |
换端口(Android)
首先在frida-server
执行:(server的名字被我改了)
1 | ./data/local/tmp/rf-server --listen=0.0.0.0:11451 |
然后执行:
1 | adb forward tcp:11451 tcp:11451 |
Python loader脚本(Android)
这个脚本用于在Python中加载Js的frida脚本。
1 | import time |
获取指定 UID 设备
1 | device = frida.get_device_manager().get_device("094fdb0a0b0df7f8") |
获取远程设备
1 | mgr = frida.get_device_manager() |
交互式界面
似乎只要使用
1 | frida <程序路径> (-l <脚本路径>) |
这样的命令就可以在终端中启动Frida的交互式界面。配合自带的补全功能,就可以随意测试函数了。
RPC远程调用-Js与Python脚本通信
js与py脚本交互
1 | import frida |
Python这边就是script.export
向其中传入json对象;Js这边是用rpc.exports
构成一个导出函数字典。
又有一个例子:
1 | //Javascript code |
过时警告⭐
1 | DeprecationWarning: Script.exports will become asynchronous in the future, use the explicit Script.exports_sync instead |
以后用Script.exports_sync
。
send/recv
首先是Js2Py:
在Js中:
1 | send(data) |
然后在Python中:
1 | def my_message_handler(message, payload): |
其次是Py2Js,首先在Python里:
1 | script.post({"my_data": data}) # send JSON object |
在Js端:
1 | recv(function (received_json_object) { |
注意这里要用wait
方法等待数据完全传输过来。
执行函数
setTimeout
延迟执行一次
1 | setTimeout(funcA, 15000); |
setInterval
间隔循环执行
1 | var id_ = setInterval(funcB, 15000); |
变量类型
FRIDA-API使用篇:rpc、Process、Module、Memory使用方法及示例-安全客 - 安全资讯平台 (anquanke.com)
索引 | API | 含义 |
---|---|---|
1 | new Int64(v) | 定义一个有符号Int64类型的变量值为v,参数v可以是字符串或者以0x开头的的十六进制值 |
2 | new UInt64(v) | 定义一个无符号Int64类型的变量值为v,参数v可以是字符串或者以0x开头的的十六进制值 |
3 | new NativePointer(s) | 定义一个指针,指针地址为s |
4 | ptr(“0”) | 同上 |
例子:
1 | Java.perform(function () { |
frida
也为Int64(v)
提供了一些相关的API:
索引 | API | 含义 |
---|---|---|
1 | add(rhs)、sub(rhs)、and(rhs)、or(rhs)、xor(rhs) | 加、减、逻辑运算 |
2 | shr(N)、shl(n) | 向右/向左移位n位生成新的Int64 |
3 | Compare(Rhs) | 返回整数比较结果 |
4 | toNumber() | 转换为数字 |
5 | toString([radix=10]) | 转换为可选基数的字符串(默认为10) |
我也写了一些使用案例,代码如下。
1 | function hello_type() { |
Java Hook
打印Java函数参数&返回值(免写参数)
简单来说就是,不需要单独写函数原型里的每一个形参,直接用arguments
即可。匿名函数function
的形参表里都不需要放个args
。
1 | // 函数原型 encodeRequest(int i, String str, String str2, String str3, String str4, String str5, byte[] bArr, int i2, int i3, String str6, byte b, byte b2, byte[] bArr2, boolean z) |
Hook所有重载函数
重载方法(Overloaded methods)是指在同一个类中定义了多个具有相同名称但参数列表不同的方法。
1 | function hookAllOverloads(targetClass, targetMethod) { |
Hook Java String类
1 | function hook_javalangString() { |
Hook JSONObject
调用了send
方法与Python进行RPC通信。
1 | var JSONObject=Java.use('org.json.JSONObject'); |
Hook LinkedHashMap
1 | var linkerHashMap=Java.use('java.util.LinkedHashMap'); |
Hook Java普通函数(非重载)⭐
Hook一个最简单的Java函数:
1 | void fun(int x, int y) { |
下面是Hook:
1 | console.log("Script loaded successfully "); |
核心点即:
1 | class.function.implementation = function() {} |
Hook Java重载函数
1 | void fun(int x , int y ){ |
Hook 类构造函数
1 | JavaClass.$init.implementation = function () {} |
Hook 类实例化函数
1 | JavaClass.$new.implementation = function () {} |
注册Java接口
看Java/注册Java类
章节。
主要利用Java.registerClass
方法。
Hook Log类
1 | var cLog = Java.use("android.util.Log"); |
Hook 已经存在的Java类⭐
使用Java.choose
方法。
1 | Java.choose("com.example.a11x256.frida_test.my_activity", { |
Hook Crypto库
Frida hooking android part 5: Bypassing AES encryption - 11x256’s Infosec blog (infosec-blog.com)
1 | console.log("Script loaded successfully 55"); |
以及对应的Py脚本:(懒得改Python3版本了)
1 | import time |
Hook 短信发送
1 | function hook_sms() { |
Hook URI
1 | var hook_uri = function() { |
Hook KXmlSerializer 拼装内容
用来看XML什么的。随便找了个blog:
Android 之 pull 生成 XML 及 XmlSerializer 详解 - 掘金 (juejin.cn)
1 | function hook_xml() { |
Hook 常见密码库
1 | Java.perform(function () { |
Java.retrain保存对象
1 | Java.perform(() => { |
Hook 内部类
1 | function main(){ |
Hook 匿名类
1 | // 接口, 抽象类, 不可以被new |
Hook 动态替换 ClassLoader(不知道干啥的)
说实话我也不知道这是用来干啥的。
java ClassLoader(类加载器) - 知乎 (zhihu.com)
1 | // 方法一 |
经常在加壳的 app 中, 没办法正确找到正常加载 app 类的 classloader, 可以使用以下代码:
1 | function hook() { |
Hook Runnable
用于多线程的。而且这个代码还关闭了FLAG_SECURE
标志,使得能够截屏。
1 | Java.perform(function() { |
Hook onClick
可以Hook按钮点击事件。
1 | var jclazz = null; |
Hook startActivity
startActivity()是一个用于启动新的Activity的方法。比如:
1 | // 创建一个Intent对象,指定要启动的目标Activity |
1 | Java.perform(function () { |
Hook Intent实现Activity跳转
1 | function jumpActivity() { |
Hook root检测
太强了。这位师傅的代码:分类: frida | 凡墙总是门 (kevinspider.github.io)
1 | // $ frida -l antiroot.js -U -f com.example.app --no-pause |
Hook 强制主线程运行
1 | Java.perform(function() { |
Hook Java exit禁止退出
1 | function hookExit(){ |
Hook 修改设备参数
修改了IMEI,IMSI,ANDROID_ID等一系列数据。
1 | // frida hook 修改设备参数 |
Hook 打印调用栈
这里举了socket为例。用这个代码能够通过stacktrace跟踪上级调用这个socket的函数与类。
1 | var class_Socket = Java.use("java.net.Socket"); |
Hook UI注入
说白话就是能强制调用Toast等UI显示类:
1 | Java.perform(function() { |
Universal antidebug
说是Universal,但应该不保真。
1 | frida --codeshare meerkati/universal-android-debugging-bypass -f YOUR_BINARY |
代码:
1 | /* |
Bypass Android Debug mode
1 | setTimeout(function() { |
Native Hook
Hook so导出函数模板⭐
1 | function hookNativeFun(callback, funName, moduleName) { |
寻找模块基址
1 | jscode = """ |
Hook未导出Native函数(指定地址)
1 | Java.perform(function(){ |
Hook registerNative
有的so不直接在导出表里面导出函数,而是后面用这个API去动态注册。因此Hook这个函数来获得某个so真正想要导出的函数名和地址。
1 | function hook_libart() { |
注意:因为一个jni函数注册只调用一次registerNative,所以这里建议用frida -U -f com.xxxx.xxxx -l xxxx.js命令注入js,同时启动目标app;如果人为开启目标app,再运行frida,可能regiserNative
函数已经执行过了!
Inline Hook
this.context.x13
打印此地址时的寄存器值,和调试器很像了。
1 | //刚注入的时候这个so还没加载,需要hook dlopen |
Hook dlopen 打印并Hook刚刚加载完的so⭐⭐⭐
可以与
Android逆向-Native调试init,init-array,JNI-Onload段
结合。
这个方法只能在.init
与.init_array
被执行后去Hook。如果想要在.init
段执行之前,需要更细节的操作。
this.context.xxx
寄存器
不过说来高版本frida有了Module.load(),可能就不需要这样麻烦的加载so了
1 | //刚注入的时候这个so还没加载,需要hook dlopen |
测试结果:(也必须在刚刚打开APP的时候Hook上去)
1 | dlopen: /data/app/~~gdw57-wVrIhpUjCSB8JIeA==/com.example.hookinunidbg-m7RhT-gNYVLxau89hcsDig==/oat/arm/base.odex |
Hook JNI API函数
在Java/调用JNI native API(创建JNI对象)⭐
中,可以获取JNIEenv
对象然后调用JNI函数。但是这只是调用,并不能Hook JNI函数。下面提供一个可能可行的思路,即直接获得env的handle地址,然后+offset拿到函数地址,再直接替换。
newStringUTF(没测试)
1 | function hook_native_newString() { |
getStringUTFChars(没测试)
1 | function hook_native_GetStringUTFChars() { |
Hook init/init_array⭐
在另外一个blog里。
类型转换
主要是用于输入输出的。
如果漏了什么,看#43:
分类: frida | 凡墙总是门 (kevinspider.github.io)
frida打印与参数构造 - Runope丶 - 博客园 (cnblogs.com)
Java2Frida
java type |
frida type |
---|---|
int |
int |
float |
float |
boolean |
boolean |
string |
java.lang.String |
byte array |
[B |
array
java type |
so type |
---|---|
int |
I |
float |
F |
boolean |
Z |
string |
java.lang.String |
byte |
B |
long |
J |
short |
S |
double |
D |
char |
C |
在Frida中用[
表示数组。例如是int
类型的数组,写法为:[I
如果是String
类型的数组,则写法为: [java.lang.String;
注意:后面还有个;
号
具体怎么创建的话可以看
参数构造
一节,提供了Java.array
方法。
jstring,jbyteArray输出(JNI)
主要是用于与JNI对象交互的。转换为Java对象,与Java层的东西交互。
1 | function jstring2Str(jstring) { |
Java Array 2 Hex string打印
主要是格式化打印Javascript的bytes对象的。
1 | function bytes2hex(arr) { |
举个例子:
1 | var b = new Uint8Array([0x41, 0x42, 0x43]); |
Js Str 2 Js Bytes
1 | function string2Bytes(str) { |
举例:
1 | string2Bytes("aaa124123242") |
Js Bytes 2 Js Str
1 | function bytes2String(arr) { |
举例:
1 | var d = [1,2,3, 0x41, 0x42 , 0x1234] |
Js Bytes 2 Base64
1 | function bytes2Base64(e) { |
举例:
1 | c = [ |
Base64 2 Js Bytes
1 | function base64ToBytes(e) { |
JNI jobject 2 Js Str(JSON化)
将JNI的jobject
序列化
1 | function jobj2Str(jobject) { |
举例:
1 | var jstr = Java.vm.getEnv().newStringUtf("aaaaa"); |
ArrayBuffer 输出
ArrayBuffer
是JavaScript对象。
1 | function ab2Hex(buffer) { |
举例:
1 | // 创建一个长度为 3 字节的 ArrayBuffer |
Hexdump
1 | var libc = Module.findBaseAddress('libc.so'); |
Java String 2 Java Byte Array
1 | var javaBytes = Java.use('java.lang.String').$new("aaaaa").getBytes(); |
Java Byte Array 2 Java Object
1 | var javaBytesClass = Java.cast(javaBytes, Java.use('java.lang.Object')).getClass(); |
Js Array 2 Java Array
1 | // 使用 Java.array 把 js array 转成 java Object array |
Java Map 2 Js Map
将 Java 中的 Map
对象转换为 JavaScript 中的对象
1 | function getMapData(mapSet) { |
举例:
1 | var cHashMap = Java.use('java.util.HashMap'); |
Js Bytes 2 Java Bytes
可行的方法:ArrayBuffer
–>Uint8Array
–>Java.array
1 | let file = new File("/data/local/tmp/a.enc", "rb") |
Java Bytes 2 Js Bytes
调用了Java方法,返回了一个Java的bytes[]
对象后,得转化成Js对象输出。
可行的方法:Java bytes[]
–>Uint8Array
这是一个例子:
1 | var b = AES256JNCryptor_obj.decryptData(a0, a1); |
实用函数
这一节主要用于记录通用模板函数。一般不需要修改,功能也复杂,直接套用即可。
获取正在执行的方法名
这个函数用于获取当前进程正在执行的方法的名称。通过使用Java反射机制,并结合Frida提供的Java.perform方法,可以在运行时动态获取当前方法的名称。
在函数内部,通过JavaThread.currentThread().getStackTrace()[2].getMethodName()
这一行代码,获取了当前线程的调用栈,并从中获取了正在执行的方法的名称。通过使用索引2
,可以获取调用栈中的第三个元素,即调用getMethodName
函数的方法。
需要注意的是,由于Frida的Java.perform方法是异步执行的,获取到的方法名称可能需要一些时间才能返回。此外,这个函数只能获取当前线程正在执行的方法的名称,并不能获取其他线程的方法名称。
1 | function getCurrMethodName() { |
打印堆栈
按理来说应该会打印堆栈,但是我测试的时候没啥效果,可能是因为现在没有执行任何方法吧。
1 | function showStacks() { |
如果不起效果,可以尝试上面获取正在执行的方法名
的JavaThread.currentThread().getStackTrace()
。
枚举Java类所有方法名
传入的targetClass
是类名字符串。
1 | function enumMethods(targetClass) { |
用Objection可以达到同样的效果。
枚举Java类
Java.enumerateLoadedClasses
1 | function enumClass() { |
它还有一个好兄弟 Java.enumerateLoadedClassesSync()
,它返回的是一个数组。
Java.choose
好像都行:
1 | function enumPrint(){ |
获取类型⭐
很好用的函数,可以用于判断参数,变量,对象的类型。
1 | function getParamType(obj) { |
打印指定地址的C字符串
1 | function print_string(addr) { |
反汇编
1 | function dis(address, number) { |
封装log函数加入线程ID 和时间
1 | function getFormatDate() { |
sleep
1 | function sleep(numberMillis) { |
遍历实例对象的方法,成员变量
1 | function main(){ |
Process
枚举进程中的线程
1 | function enumThreads() { |
获取线程ID
1 | Process.getCurrentThreadId() // 获取此线程的操作系统特定 ID 作为数字 |
枚举进程模块
1 | var mods = Process.enumerateModules() |
获取模块
1 | Process.findModuleByName(module_name) |
Module
获取模块信息
1 | function frida_Module() { |
枚举模块导入函数
1 | function enumImport(moduleName) { |
枚举模块导出函数
1 | function enumExports(moduleName) { |
枚举模块符号
1 | function enumSymbols(moduleName) { |
获取模块基地址
在很多代码片段里面都用到了。
1 | var baseAddr = Module.findBaseAddress("libnative-lib.so"); |
获取导出函数地址
1 | Module.findExportByName("libc.so", "strcmp"); |
获取非导出函数地址
1 | var soAddr = Module.findBaseAddress("libnative-lib.so"); |
Memory
内存复制
1 | function frida_Memory() { |
内存搜索(并替换)
利用Memory.scan()
函数
类似ida的bin search功能
此外,当匹配了之后,还可以直接用address.writeByteArray
进行替换。
1 | function frida_Memory() { |
Memory.scanSync 功能与Memory.scan
一样,只不过它是返回多个匹配到条件的数据。
1 | function frida_Memory() { |
内存分配
在目标进程中的堆上申请size
大小的内存,并且会按照Process.pageSize
对齐,返回一个NativePointer
,并且申请的内存如果在JavaScript
里面没有对这个内存的使用的时候会自动释放的。也就是说,如果你不想要这个内存被释放,你需要自己保存一份对这个内存块的引用。
1 | function frida_Memory() { |
Memory.allocUtf8String(str)
分配utf字符串Memory.allocUtf16String
分配utf16字符串Memory.allocAnsiString
分配ansi字符串
写入内存
向刚刚分配的arr中写入array
1 | function frida_Memory() { |
读取内存
1 | function frida_Memory() { |
汇编修改
看下面汇编Patch
一节。
Java
显示虚拟机(Dalvik)是否启动
Java.available
该函数一般用来判断当前进程是否加载了JavaVM,Dalvik
或ART
虚拟机,咱们来看代码示例!
1 | function frida_Java() { |
显示系统版本号
Java.androidVersion
显示android系统版本号
1 | function frida_Java() { |
Java类型转换⭐
见上文复杂函数打印(HashMap等)
1 | Java.cast(handle, klass); |
Java.cast(handle, klass)
,就是将指定变量或者数据强制转换成你所有需要的类型;创建一个 JavaScript
包装器,给定从 Java.use()
返回的给定类klas
的句柄的现有实例。此类包装器还具有用于获取其类的包装器的类属性,以及用于获取其类名的字符串表示的$className
属性,通常在拦截so
层时会使用此函数将jstring、jarray
等等转换之后查看其值。
创建Java数组⭐
1 | Java.perform(function () { |
索引 | type | 含义 |
---|---|---|
1 | Z | boolean |
2 | B | byte |
3 | C | char |
4 | S | short |
5 | I | int |
6 | J | long |
7 | F | float |
8 | D | double |
9 | V | void |
注册Java类(Java接口实现)⭐
Java.registerClass
:创建一个新的Java
类并返回一个包装器,其中规范是一个包含:name
:指定类名称的字符串。superClass
:(可选)父类。要从 java.lang.Objec
t 继承的省略。implements
:(可选)由此类实现的接口数组。fields
:(可选)对象,指定要公开的每个字段的名称和类型。methods
:(可选)对象,指定要实现的方法。
1 | Java.perform(function () { |
比如,下面代码注册了一个自定义的Java类,然后继承了一个父类A,并实现了一个接口类B:
1 | //获取目标进程的SomeBaseClass类 |
获取env对象⭐
1 | Java.vm.getEnv() |
利用env,像NDK开发一样创建jstring等类型、调用函数。
调用JNI native API(创建JNI对象)⭐
Java.vm.tryGetEnv().xxxx
或者Java.vm.getEnv()
1 | var sign2 = Module.findExportByName("libhello-jni.so", "Java_com_example_hellojni_HelloJni_sign2"); |
又有拦截JNI函数:
1 | function frida_Java() { |
寻找已经加载的Java类
Java.choose
方法。详情看Java Hook
一节。
Interceptor
Hook Native函数⭐
Intercepter.attach
这里的例子使用的还是so里的导出函数,有符号表。
1 | function frida_Interceptor() { |
替换 Native函数⭐
Intercepter.replace
将so中一个拥有Symbol的native函数替换成了自行定义的函数,无论什么输入都会返回123。
1 | function frida_Interceptor() { |
举例:
1 | [HOOK] java.lang.StringFactory.newStringFromString->Hello World, this is an example string in Java. |
参数构造
Intent
如何在Frida中使用intent - 问答 - 腾讯云开发者社区-腾讯云 (tencent.com)
1 | const intentClass = Java.use("android.content.Intent"); |
Context
frida-rpc之如何构造context参数_mob604756f828bf的技术博客_51CTO博客
1 | var current_application = Java.use('android.app.ActivityThread').currentApplication(); |
HashMap(就是Map)
1 | var cHashMap = Java.use('java.util.HashMap'); |
Java String
1 | var arr = Java.use("java.util.Arrays"); |
比如:
1 | var intarr = Java.array('int', [ 1003, 1005, 1007 ]); |
array
1 | Java.perform(function () { |
NativePointer(指针)
ptr()
这个简化函数意义是一样的。
1 | p = new NativePointer(0x114514); |
NativeFunction对象(创建Native函数)
Frida Javascript api #NativeFunction #NativeCallback 与 #SystemFunction (中文版) - 简书 (jianshu.com)
创建新的NativeFunction
以调用address
处的函数(用NativePointer
指定),其中return Type
指定返回类型,argTypes
数组指定参数类型。如果不是系统默认值,还可以选择指定ABI
。对于可变函数,添加一个‘.’固定参数和可变参数之间的argTypes
条目,我们来看看官方的例子。
1 | // LargeObject HandyClass::friendlyFunctionName(); |
我来看看它的格式,函数定义格式为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 | ### Supported Types |
第四个参数 options
是一个包含一个或多个以下键的对象:
abi
:和上面一样的枚举.scheduling
:字符串类型的调度行为, 仅限于:cooperative
允许其他线程在调用原生函数时执行JavaScript代码.
即在调用之前放开锁, 然后再重新获取它. 这是默认行为.exclusive
禁止其他线程在调用原生函数时执行JavaScript代码.
即持续占有 JavaScript 锁. 这会更快一些, 但有可能导致死锁.
exceptions
字符串类型的异常行为, 仅限于:
steal
:如果被调用的方法抛出了一个原生异常, 例如对一个无效的指针撤销引用, Frida 将调整栈堆并拦截这个异常, 将其转换成一个可以被处理的 JavaScript 异常. 这有可能导致应用处于未定义的状态, 但在试验时能有效避免进程崩溃. 这是默认行为.propagate
:让应用处理调用方法期间的任何原生异常.(或者让通过Process.setExceptionHandler()
安装的异常处理器进行处理)
traps
:字符串类型的, 待启用的代码陷阱. 仅限于:
NativeFunction支持的类型
SUPPORTED TYPES |
---|
void |
pointer |
int |
uint |
long |
ulong |
char |
uchar |
size_t |
ssize_t |
float |
double |
int8 |
uint8 |
int16 |
uint16 |
int32 |
uint32 |
int64 |
uint64 |
bool |
NativeCallback对象(用Javascript函数实现Native方法)⭐
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
做一个函数替换。
1 | Java.perform(function () { |
SystemFunction
直接借用了:Frida Javascript api #NativeFunction #NativeCallback 与 #SystemFunction (中文版) - 简书 (jianshu.com)
new SystemFunction(address, returnType, argTypes[, abi])
:
类似于NativeFunction
, 但它提供了线程最后一个错误状态的快照.
返回的值是一个包裹着实际返回值作为value
的对象, 以及一个平台相关的字段, UNIX 上是errno
, 而 Windows 上则是lastError
.new SystemFunction(address, returnType, argTypes[, options])
:
和上面一样, 但是像NativeFunction
对应的构造器一样接受一个options
对象.
创建jstring并替换函数返回值
1 | var env = Java.vm.getEnv(); |
CharSequence
在Toast.makeText
里面用上了。
1 | var CharSequence = Java.use('java.lang.CharSequence'); |
Object数组嵌套Java String数组
1 | var string1 = Java.use("java.lang.String").$new("123"); |
主动调用
Java
实例化一个对象
1 | var javaString = Java.use("java.lang.String"); |
调用静态函数
1 | var javaString = Java.use("java.lang.String"); |
调用类方法
1 | var javaString = Java.use("java.lang.String"); |
其实就是用Java.use
拿到类后,构造好参数,直接调用里面的方法。
这是一个例子,一个爆破脚本:
Frida主动调用爆破密码 - AskTa0 - 博客园 (cnblogs.com)
1 | function main(){ |
修改成员变量
属性和函数名重名了,需要在属性前面 加一个 下划线
_
1 | function hook_java() { |
主动加载Dex并调用方法⭐
可以主动加载自己实现的Java代码并执行。
比如实现一个自定义个Base64方法:
1 | class Base64DIY { |
然后用javac Base64DIY.java
编译成.class
文件
然后打包生成jar文件:
1 | .jdks\corretto-1.8.0_382\bin\jar.exe -cvf ddex.jar .\Base64DIY.class |
然后再转换成.dex
文件:
1 | D:\Environment\Android_SDK\build-tools\30.0.3\dx.bat --dex --output=ddex.dex .\ddex.jar |
然后push并给权限:
1 | adb push .\ddex.dex /data/local/tmp/ddex.dex |
然后在Frida脚本里面实现加载与调用:
1 | ///<reference path='./index.d.ts'/> |
Native
暂时放最简单的一个例子。注意这里返回值是额外分配的一块内存。
1 | var friendlyFunctionName = new NativeFunction(friendlyFunctionPtr, 'void', ['pointer', 'pointer']); |
文件读写(File类)⭐
frida native层读写文件_mb5fdb09c3c3319的技术博客_51CTO博客
在Frida Js里面,是用File
对象来实现文件读写。下面是一个代码示例:
1 | this.file = new File(this.filepath, "rb"); |
除此之外还有一种通过Hook主动调用Native文件读写函数来实现的方法,但是太过复杂,还要考虑存在其他Hook的需要,就pass。
static readAllBytes
1 | /** |
static readAllText
1 | /** |
static writeAllBytes
1 | /** |
static writeAllText
1 | /** |
new File
1 | /** |
tell
1 | /** |
seek
1 | /** |
readBytes
1 | /** |
readText
1 | /** |
readLine
1 | /** |
write
1 | /** |
flush
1 | /** |
close
1 | /** |
反反调试
单独放一个blog吧。
中级使用
https://learnfrida.info/intermediate_usage
主要涉及了更多具体的结构体什么的,可以看看
Table of contents
- Defining globals in Frida’s REPL
- Following child processes
- Creating NativeFunctionsUsing NativeFunction to call system APIs
- Modifying return values
- Access values after usage
- CryptDecrypt: A practical case.
- Modifying values before execution
- Undoing instrumentation
- std::string
- std::vectorstd::vector in MSVC
- Operating with ArrayBuffers
高级使用
https://learnfrida.info/advanced_usage
NOP函数
让一个函数失效。
使用replace API替换导出函数
1 | Interceptor.replace(CreateFileWPtr, new NativeCallback(function(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile) {}, 'int', ['pointer', 'int', 'int', 'int', 'int', 'int', 'pointer'])); |
举个例子:
这是Hook的程序:
1 |
|
脚本(这里比较复杂,因为需要具体从指定DLL里面遍历符号找到open
函数的地址。)
1 | // var mods = Process.enumerateModules() |
使用replace API替换模块内函数
比如上面那个例子中的fprintf
函数,在编译后就内联了。现在我希望让无效,无论是任何一个其他函数去调用它。
1 | var fprintf_offset = 0x1530 |
好吧,其实差不多,主要核心点在于要主动return
来提前返回,不继续执行原函数逻辑。
内存Patch
使用Memory.patchCode
来直接修改内存。具体怎么用的看下面的章节。
1 | function patch_open() { |
使用自定义DLL/so
这里偷个懒,直接复制过来。
Advanced Frida - Frida HandBook (learnfrida.info)
There might be scenarios when using custom libraries is required be it because there are functions in the library that are useful in our instrumentation code hence it is interesting to call them from our instrumentation code or because there are already replacements written in the library for the functions are going to be instrumented (essentially, to avoid reinventing the wheel). For this use case, Frida offers the
Module.load
method.
Module.load
allows to load an external library into our instrumentation session, once loaded it behaves as a regular module in Frida meaning it has access to Module‘s methods likefindExportByName
,enumerateExports
,enumerateImports
… etc.To illustrate how this works, the following C program is used:
1
2
3
4
5
6
7
8
9
10 #include <stdio.h>
#include <stdlib.h>
int main() {
FILE *fp;
fp = fopen("file.txt", "w+");
fprintf(fp, "%s %s", "May the force", "be with you");
fclose(fp);
return 0;
}The purpose of this example is to replace the function
fopen
using a custom DLL instead of using Frida’sInterceptor.replace
.Creating a custom DLL¶
The first step is having a custom DLL, if you know how this process works then you can skip this subsection.
The DLL will have a single function that envelopes
fopen
and prints thefilepath
argument, therefore what is needed is ourlibtest.c
file containing the functionmy_fopen
:
1
2
3
4
5
6
7 #include <stdlib.h>
#include <stdio.h>
FILE *my_fopen(const char *filename, const char *mode) {
printf("lib: %s\n", filename);
return fopen(filename, mode);
}And a separate
libtest.h
with themy_fopen
declaration:
1
2
3
4 #include <stdlib.h>
#include <stdio.h>
FILE *my_fopen(const char *filename, const char *mode);Once these files are created, then the only remaining task is using
clang
to create a shared library:
1 $ clang -shared -undefined dynamic_lookup -o libtest.so libtest.cIf everything went well, there should be a
libtest.so
shared library file in your current folder:
1
2 file libtest.o
libtest.o: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, not strippedUsing our custom library¶
Once the custom library is created it can be used from Frida. To illustrate this example everything is done in Frida’s REPL with the aforementioned program, no special arguments are required. This workflow requires us to first load our custom DLL, then obtain a pointer to our custom function and create a NativeFunction from this function so that it can be used from our code. Then the final step is replacing the original function with the custom one.
When inside the command line, the first step is loading our custom library so
Module.load
provides this functionality:
1
2
3
4
5
6
7 [Local::a.out]-> myModule = Module.load('/home/lazarus/libtest.so')
{
"base": "0x7f0cf409c000",
"name": "libtest.so",
"path": "/home/lazarus/libtest.so",
"size": 20480
}When Frida loads the module it returns a module object that now operates as is. For example it is possible to enumerate the loaded library exports:
1
2
3
4
5
6
7
8 [Local::a.out]-> myModule.enumerateExports()
[
{
"address": "0x7f0cf409d120",
"name": "my_fopen",
"type": "function"
}
]Our custom
my_fopen
function is at the address0x7f0cf409d120
, this is the address that is needed when creating aNativeFunction
to the custom function:
1 myfopen = new NativeFunction(ptr("0x7f0cf409d120"), 'pointer', ['pointer', 'pointer']);The return value is a pointer to a FILE object, so it is set as
pointer
type and the arguments are pointers toconst char
as well. Note that once theNativeFunction
is created then it can be called from the instrumentation code as main time as desired. The final step is to callInterceptor.replace
and call the custom function instead:
1
2
3 Interceptor.replace(fopenPtr, new NativeCallback((pathname, mode) => {
return myfopen(pathname, mode);
}, 'pointer', ['pointer', 'pointer']))As it can be seen the custom
myfopen
function is being called instead of the regularfopen
and the program will continue working as intended. The effects of this replacement can be seen when running the%resume
command:
1
2
3 [Local::a.out]-> %resume
[Local::a.out]-> lib: file.txt
lib: /dev/urandomThe custom library function correctly prints the values of the first argument.
读写寄存器
两种方式,Interceptor.attach
内直接修改或读取;或者内存Patch。
1 | const addPtr = Module.getExportByName(null, "add"); |
读取结构体
1 | struct myStruct |
说白了还是用指针+偏移的野蛮方法。
1 | // Given s = args[0]:NativePointer |
SYSCALL struct
For this example we will be using a known linux SYSCALL named gettimeofday.
MAN page for gettimeofday: https://man7.org/linux/man-pages/man2/gettimeofday.2.html
We have the following declaration:
1 int gettimeofday(struct timeval *tv, struct timezone *tz);From this we can quickly figure out that
timeval
andtimezone
are two structs. And we cannot check what these values are by simply using Frida’s API.The timeval struct is:
1
2
3
4 struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};Note: The
time_t
size is even dependent on the API level you are targeting in Android systems. Do not forget to get it’s size withProcess.PointerSize()
And the timezone struct is:
1
2
3
4 struct timezone {
int tz_minuteswest; /* minutes west of Greenwich */
int tz_dsttime; /* type of DST correction */
};For this example we will write a simple command and compile it with clang:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int
main()
{
struct timeval current_time;
gettimeofday(¤t_time, NULL);
printf("seconds : %ld\nmicro seconds : %ld\n",
current_time.tv_sec, current_time.tv_usec);
printf("%p", ¤t_time);
getchar();
return 0;
}And run:
clang -Wall program.c
. The expected output should be:
1
2
3
4 pala@jkded:~/code$ ./a.out
seconds : 1601394944
micro seconds : 402896
0x7fff4a1f8d48So, given this we will try to access the time_t structure given 0x7fff4a1f8d48 is the structure pointer:
1
2
3
4
5
6 [Local::a.out]-> structPtr = ptr("0x7fff0b9a3118")
"0x7fff0b9a3118"
[Local::a.out]-> structPtr.readLong()
"1601395177"
[Local::a.out]-> structPtr.add(8).readLong()
"439353"As we can see, the first member is already at offset 0, however we need to get the process pointer size to guess the next offset:
1
2 [Local::a.out]-> Process.pointerSize
8Now that we know that the
pointerSize
is 8, we can infer that long’s size will be 8 bytes and place ourselves in the right offset.
WINAPI struct
There are a lot of structures in the Windows API and therefore we need to be confident in our structure parsing skills. We can find these structures in
NTDLL
calls to represent strings such asUNICODE_STRING
and other structures such as theSYSTEMINFO
one.For this example we will take a look at the
WINAPI
callGetSystemInfo
that takes aLPSYSTEM_INFO
structure as an argument. And this is what aLPSYSTEM_INFO
struct looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 typedef struct _SYSTEM_INFO {
union {
DWORD dwOemId;
struct {
WORD wProcessorArchitecture;
WORD wReserved;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;
DWORD dwPageSize;
LPVOID lpMinimumApplicationAddress;
LPVOID lpMaximumApplicationAddress;
DWORD_PTR dwActiveProcessorMask;
DWORD dwNumberOfProcessors;
DWORD dwProcessorType;
DWORD dwAllocationGranularity;
WORD wProcessorLevel;
WORD wProcessorRevision;
} SYSTEM_INFO, *LPSYSTEM_INFO;Wow! Quite a complicated struct that we have here right? Let’s first find the size of each offset, especially the ones that can be troublesome such as LPVOID.
On a Windows 10 64-bit system compiled for 32-bit under Visual C++ we get the following values:
Type Size WORD 2 DWORD 4 DWORD_PTR 4 LPVOID 4 We can check this is true by calling Process.pointerSize() in an attached process:
1
2 [Local::ConsoleApplication2.exe]-> Process.pointerSize
4Beware that these numbers will change if compiled on 64 bit:
Type Size WORD 2 DWORD 4 DWORD_PTR 8 LPVOID 8 Beware that compilers may align the stack so ALWAYS be careful when calculating offset.s
Once we have these values, we can infer the offset for each member. Don’t be afraid of the union keyword, it won’t be affecting our calculations for the time being.
Getting all the values is out of the scope of this part, so we will getting some of them as an example:
1
2
3 dwPageSize
lpMinimumApplicationAddress
dwNumberOfProcessorsComplete offset list:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 typedef struct _SYSTEM_INFO {
union {
DWORD dwOemId; // offset: 0
struct {
WORD wProcessorArchitecture;
WORD wReserved;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;
DWORD dwPageSize; // offset: 4
LPVOID lpMinimumApplicationAddress; // offset: 8
LPVOID lpMaximumApplicationAddress; // offset: 12
DWORD_PTR dwActiveProcessorMask; // offset: 16
DWORD dwNumberOfProcessors; // offset: 20
DWORD dwProcessorType; // offset: 24
DWORD dwAllocationGranularity; // offset: 28
WORD wProcessorLevel; // offset 32
WORD wProcessorRevision; // offset 34
} SYSTEM_INFO, *LPSYSTEM_INFO;And this is the example program that we will be using to test our guesses:
1
2
3
4
5
6
7
8
9 #include <iostream>
#include <Windows.h>
int main()
{
SYSTEM_INFO sysInfo ;
GetSystemInfo(&sysInfo);
printf("%p", &sysInfo);
getchar();
}Now that we have the complete offset list, we can know get the values of
dwPageSize
,lpMinimumApplicationAddress
, anddwNumberOfProcessors
respectively:
1
2
3
4
5
6 [Local::ConsoleApplication2.exe]-> sysInfoPtr.add(4).readInt()
4096
[Local::ConsoleApplication2.exe]-> sysInfoPtr.add(8).readInt()
65536
[Local::ConsoleApplication2.exe]-> sysInfoPtr.add(20).readInt()
8
CModule⭐⭐⭐
放在另一个blog了。
汇编Patch
一些例子
这是一个Windows的例子。不过也有参考价值。
1 | import {log} from "./logger.js"; |
这是另一个:
1 | function dis(address, number) { |
相关API
Memory.patchCode
1 | /** |
address
:起始地址,一般填入要patch的函数头地址。size
:准备patch的大小。想要保险起见的话,可以恰好计算好整个函数的字节大小并填入。偷懒的话可以用Process.pageSize
直接填充整个页,但是极有可能会把其他函数给覆盖了。apply
:一个lambda函数,填入了准备patch的逻辑。需要配合Writer来使用。
X86Writer
1 | /** |
一般情况下,只需要传入codeAddress
就好了,这会指定字节码要写入的地方。
剩下的API太多了,这里不细说,具体的直接看Doc。后面可以考虑写一个翻译器,自动把nasm汇编转换成Frida的这个格式。
https://frida.re/docs/javascript-api/#x86writer
https://frida.re/docs/javascript-api/#arm64writer
模板
1 | Memory.patchCode(open_ptr, Process.pageSize, |
下面提供一个更特殊的例子:将原来的函数的第一个指令修改成Jmp,然后在一个用Frida开辟的新内存区域内,用另一个Writer写下nop逻辑,然后返回。
1 | function patch_open2(){ |
这个例子主要适用于测试Writer的灵活性。
特殊注意
- 属性和函数名重名了,需要在属性前面 加一个 下划线
_
- 内部类使用
$
进行分隔,不使用.
其他工具
基于Frida的跟踪JNI函数调用的工具。
整合网上各种脚本的仓库:
其他问题
传递Java参数至Python的方法
ios - How to pass bytes(ArrayBuffer) between python & js in frida? - Stack Overflow
尤其是二进制数据,比如ArrayBuffer,需要先用ab2Str
这样的帮助函数变成Js类型,然后再用send函数传出去。
Hook System类
没解决。
Hook JNIEnv内的API
没解决。
include/nativehelper/jni.h - platform/libnativehelper - Git at Google (googlesource.com)
参考
(116条消息) frida复杂类型参数打印、参数转换、调用栈打印_一点尘心的博客-CSDN博客_frida 打印对象参数
FRIDA-API使用篇:rpc、Process、Module、Memory使用方法及示例 - 安全客,安全资讯平台 (anquanke.com)
r0ysue/AndroidSecurityStudy: 安卓应用安全学习 (github.com)
FRIDA-API使用篇:Java、Interceptor、NativePointer(Function/Callback)使用方法及示例 - 安全客,安全资讯平台 (anquanke.com)
(117条消息) frida hook java和so层函数常用脚本_william~的博客-CSDN博客_frida so层
frida学习笔记3–hook so中的方法 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn
Frida主动调用爆破密码 - AskTa0 - 博客园 (cnblogs.com)
(117条消息) Frida 逆向hook方法传参_dounine的博客-CSDN博客
Frida Javascript api #NativeFunction #NativeCallback 与 #SystemFunction (中文版) - 简书 (jianshu.com)
Frida hooking android part 5: Bypassing AES encryption - 11x256’s Infosec blog (infosec-blog.com)
frida 常用方法 java jni native 层操作 - qinless
Frida高级逆向-Hook Java - 小伟哥哥~ - 博客园 (cnblogs.com)
分类: frida | 凡墙总是门 (kevinspider.github.io)
这恐怕是学习Frida最详细的笔记了(2) - 知乎 (zhihu.com)
- 本文作者: Taardis
- 本文链接: https://taardisaa.github.io/2023/09/04/Android逆向-Frida-CheatSheet/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!