Frida Hook 总结

Frida 常用操作总结

Frida环境

https://github.com/frida/frida

Pyenv

python全版本随机切换,这里提供macOS上的配置方法

Frida安装

如果直接按下述安装则会直接安装frida和frida-tools的最新版本。

pip install frida-tools
frida --version
frida-ps --version

我们也可以自由安装旧版本的frida,例如12.8.0

pyenv install 3.7.7
pyenv local 3.7.7
pip install frida==12.8.0
pip install frida-tools==5.3.0

安装objection

pip install objection
objection -h

Frida开发环境搭建

安装

git clone https://github.com/oleavr/frida-agent-example.git
cd frida-agent-example/
npm install

npm run watch会监控代码修改自动编译生成js文件

python脚本或者cli加载_agent.js

frida -U -f com.example.android --no-pause -l _agent.js

下面是测试脚本

s1.js

function main() {
Java.perform(function x() {
console.log("ol4three")
})
}
setImmediate(main)

loader.py

import time
import frida

device8 = frida.get_device_manager().add_remote_device("192.168.0.9:8888")
pid = device8.spawn("com.android.settings")
device8.resume(pid)
time.sleep(1)
session = device8.attach(pid)
with open("si.js") as f:
script = session.create_script(f.read())
script.load()
input() #等待输入

解释一下,这个脚本就是先通过frida.get_device_manager().add_remote_device来找到device,然后spawn方式启动settings,然后attach到上面,并执行frida脚本。

Frida使用

查看自己模拟器或者手机架构

cat /proc/cpuinfo

1.下载frida-server并解压

https://github.com/frida/frida/releases

2.运行adb shell

adb push /Users/sakura/Desktop/lab/alpha/tools/android/frida-server-x.x.x-android-arm64 /data/local/tmp
chmod +x frida-server
./frida-server

如果要监听端口:

./frida-server -l 0.0.0.0:8888

Frida基础

frida查看当前存在的进程

frida-ps -U查看通过usb连接的android手机上的进程。

╰─$ frida-ps --help
Usage: frida-ps [options]

Options:
--version show program's version number and exit
-h, --help show this help message and exit
-D ID, --device=ID connect to device with the given ID
-U, --usb connect to USB device
-R, --remote connect to remote frida-server
-H HOST, --host=HOST connect to remote frida-server on HOST
-a, --applications list only applications
-i, --installed include all installed applications
frida-ps -H 172.20.10.5:8888
frida-ps -U

通过grep过滤就可以找到我们想要的包名。

frida打印参数和修改返回值

package com.example.frida_demo;

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

private String total = "@@@###@@@";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

while (true){

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

fun(50,30);
Log.d("sakura.string" , fun("LoWeRcAsE Me!!!!!!!!!"));
}
}

void fun(int x , int y ){
Log.d("sakura.Sum" , String.valueOf(x+y));
}

String fun(String x){
total +=x;
return x.toLowerCase();
}

String secret(){
return total;
}
}
function main(){
console.log("Enter the Script");
Java.perform(function x(){
console.log("Inside Java perform");
var MainActivity = Java.use('com.example.frida_demo.MainActivity');
//重载找到制定的函数
MainActivity.fun.overload("java.lang.String").implementation = function(str){
//打印参数
var ret_value = "ol4three";
return ret_value;
};
})

}
setImmediate(main);
╰─$ frida-ps -U | grep com.example.frida_demo
30592 com.example.frida_demo

╰─$ frida -U -f com.example.frida_demo -l hook_string1.js --no-pause

╰─$ adb logcat
12-27 18:01:56.002 7041 7041 D ol4three.Sum: 80
12-27 18:01:56.004 7041 7041 D ol4three.string: ol4three

frida寻找instance,主动调用

function main(){
console.log("Enter the Script");
Java.perform(function x(){
console.log("Inside Java perform");
var MainActivity = Java.use('com.example.frida_demo.MainActivity');
//重载找到制定的函数
MainActivity.fun.overload("java.lang.String").implementation = function(str){
//打印参数
var ret_value = "ol4three";
return ret_value;
};
//寻找类型为classname的实例
Java.choose("com.example.frida_demo.MainActivity",{
onMatch: function(x){
console.log("find instance :" + x);
console.log("result of secret func:" + x.secret());
},
onComplete: function(){
console.log("end");
}
});
})

}
setImmediate(main);
Enter the Script
Inside Java perform
find instance :com.example.frida_demo.MainActivity@bb40738
result of secret func:@@@###@@@
end

frida rpc

function callFun(){
Java.perform(function fn(){
console.log('begin');
Java.choose("com.example.frida_demo.MainActivity",{
onMatch: function(x){
console.log("find instance :" + x);
console.log("result of fun(string) func:" + x.fun(Java.use("java.lang.String").$new("space")));
},
onComplete: function(){
console.log("end")
}
})
})
}
rpc.exports = {
callfun : callFun
}
import time,frida

device = frida.get_usb_device()
pid = device.spawn(["com.example.frida_demo"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
with open("frida_demo_rpc_call.js") as f:
script = session.create_script(f.read())

def my_message_handler(message, payload):
print(message)
print(payload)

script.on("message", my_message_handler)
script.load()

script.exports.callfun()
╰─$ python3 frida_rpc_loader.py
begin
find instance :com.example.frida_demo.MainActivity@bb40738
result of fun(string) func:space
end

frida动态修改

将手机上的app内容发送到PC上的frida 程序,处理后返回给app,然后app在做后续的流程,核心是send/recv函数

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="please input username and password"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />


<EditText
android:id="@+id/editText"
android:layout_width="fill_parent"
android:layout_height="48dp"
android:hint="username"
android:maxLength="20"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.095" />

<EditText
android:id="@+id/editText2"
android:layout_width="fill_parent"
android:layout_height="48dp"
android:hint="password"
android:maxLength="20"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.239"
tools:ignore="MissingConstraints" />

<Button
android:id="@+id/button"
android:layout_width="100dp"
android:layout_height="48dp"
android:layout_gravity="right|center_horizontal"
android:text="提交"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.745" />


</androidx.constraintlayout.widget.ConstraintLayout>
public class MainActivity extends AppCompatActivity {

EditText username_et;
EditText password_et;
TextView message_tv;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
username_et = this.findViewById((R.id.editText));
password_et = this.findViewById(R.id.editText2);
message_tv = this.findViewById(R.id.textView);

this.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (username_et.getText().toString().compareTo("admin") == 0) {
message_tv.setText("You cannot login as admin");
return;
}
//hook target
message_tv.setText("Sending to the server :" + android.util.Base64.encodeToString((username_et.getText().toString() + ":" + password_et.getText().toString()).getBytes(StandardCharsets.UTF_8), Base64.DEFAULT));

}
});

}
}

首先分析一下我们的目的是让message_tv.setText来发送admin的base64字符串。所以我们需要hook TextView.setText这个函数。

image-20211229190444921
╰─$ python3 frida_demo_rpc_loader2.py
Script loaded successfully
{'type': 'send', 'payload': 'Sending to the server :ZWVlZWU6MTIzMTIz\n'}
None
Sending to the server :ZWVlZWU6MTIzMTIz

message: {'type': 'send', 'payload': 'Sending to the server :ZWVlZWU6MTIzMTIz\n'}
data: b'eeeee:123123'
pw: 123123'
encoded data: b'YWRtaW46MTIzMTIzJw=='
Modified data sent
string_to_recv: b'YWRtaW46MTIzMTIzJw=='
YWRtaW46MTIzMTIzJw==   admin:123123'

API List

  • Java.choose(className: string, callbacks: Java.ChooseCallbacks): void
    通过扫描Java VM的堆来枚举className类的live instance。
  • Java.use(className: string): Java.Wrapper<{}>
    动态为className生成JavaScript Wrapper,可以通过调用$new()来调用构造函数来实例化对象。
    在实例上调用$dispose()以对其进行显式清理,或者等待JavaScript对象被gc。
  • Java.perform(fn: () => void): void
    Function to run while attached to the VM.
    Ensures that the current thread is attached to the VM and calls fn. (This isn’t necessary in callbacks from Java.)
    Will defer calling fn if the app’s class loader is not available yet. Use Java.performNow() if access to the app’s classes is not needed.
  • send(message: any, data?: ArrayBuffer | number[]): void
    任何JSON可序列化的值。
    将JSON序列化后的message发送到您的基于Frida的应用程序,并包含(可选)一些原始二进制数据。
    The latter is useful if you e.g. dumped some memory using NativePointer#readByteArray().
  • recv(callback: MessageCallback): MessageRecvOperation
    Requests callback to be called on the next message received from your Frida-based application.
    This will only give you one message, so you need to call recv() again to receive the next one.
  • wait(): void
    堵塞,直到message已经receive并且callback已经执行完毕并返回

更新中。。。

Android 加固应用Hook方式-Frida

Java.perform(function () {
var application = Java.use('android.app.Application');

application.attach.overload('android.content.Context').implementation = function(context){

var result = this.attach(context);
var classloader = context.getClassLoader();
Java.classFactory.loader = classloader;

var yeyoulogin = Java.classFactory.use('com.zcm.主窗口');
console.log("yeyoulogin:"+ yeyoulogin);


yeyoulogin.按钮_用户登录$被单击.implementation = function(arg){
console.log("retval:"+ this.返回值);
}

}
});

列出加载的类

Java.enumerateLoadedClasses(
{
"onMatch": function(className){
console.log(className)
},
"onComplete":function(){}
}
)

Hook 动态加载类

获取构造函数的参数

Java.perform(function(){
//创建一个DexClassLoader的wapper
var dexclassLoader = Java.use("dalvik.system.DexClassLoader");
//hook 它的构造函数$init,我们将它的四个参数打印出来看看。
dexclassLoader.$init.implementation = function(dexPath,optimizedDirectory,librarySearchPath,parent){
console.log("dexPath:"+dexPath);
console.log("optimizedDirectory:"+optimizedDirectory);
console.log("librarySearchPath:"+librarySearchPath);
console.log("parent:"+parent);
//不破换它原本的逻辑,我们调用它原本的构造函数。
this.$init(dexPath,optimizedDirectory,librarySearchPath,parent);
}
console.log("down!");

});

获取动态加载的类

Java.perform(function(){
var dexclassLoader = Java.use("dalvik.system.DexClassLoader");
var hookClass = undefined;
var ClassUse = Java.use("java.lang.Class");

dexclassLoader.loadClass.overload('java.lang.String').implementation = function(name){
//定义一个String变量,指定我们需要的类
var hookname = "cn.chaitin.geektan.crackme.MainActivityPatch";
//直接调用第二个重载方法,跟原本的逻辑相同。
var result = this.loadClass(name,false);
//如果loadClass的name参数和我们想要hook的类名相同
if(name === hookname){
//则拿到它的值
hookClass = result;
//打印hookClass变量的值
console.log(hookClass);
send(hookClass);
return result;
}
return result;
}
});

通过Java.cast处理泛型方法(JAVA中Class<?>表示泛型),在调用动态加载方法

Java.perform(function(){
var hookClass = undefined;
var ClassUse = Java.use("java.lang.Class");
var dexclassLoader = Java.use("dalvik.system.DexClassLoader");
var constructorclass = Java.use("java.lang.reflect.Constructor");
var objectclass= Java.use("java.lang.Object");
dexclassLoader.loadClass.overload('java.lang.String').implementation = function(name){
var hookname = "cn.chaitin.geektan.crackme.MainActivityPatch";
var result = this.loadClass(name,false);

if(name == hookname){
var hookClass = result;
console.log("------------------------------CAST--------------------------------")
//类型转换
var hookClassCast = Java.cast(hookClass,ClassUse);
//调用getMethods()获取类下的所有方法
var methods = hookClassCast.getMethods();
console.log(methods);
console.log("-----------------------------NOT CAST-----------------------------")
//未进行类型转换,看看能否调用getMethods()方法
var methodtest = hookClass.getMethods();
console.log(methodtest);
console.log("---------------------OVER------------------------")
return result;

}
return result;
}


});

利用getDeclaredConstructor()获取具有指定参数列表构造函数的Constructor 并实例化

Java.perform(function(){
var hookClass = undefined;
var ClassUse = Java.use("java.lang.Class");
var objectclass= Java.use("java.lang.Object");
var dexclassLoader = Java.use("dalvik.system.DexClassLoader");
var orininclass = Java.use("cn.chaitin.geektan.crackme.MainActivity");
var Integerclass = Java.use("java.lang.Integer");
//实例化MainActivity对象
var mainAc = orininclass.$new();


dexclassLoader.loadClass.overload('java.lang.String').implementation = function(name){
var hookname = "cn.chaitin.geektan.crackme.MainActivityPatch";
var result = this.loadClass(name,false);

if(name == hookname){
var hookClass = result;
var hookClassCast = Java.cast(hookClass,ClassUse);
console.log("-----------------------------BEGIN-------------------------------------");
//获取构造器
var ConstructorParam =Java.array('Ljava.lang.Object;',[objectclass.class]);
var Constructor = hookClassCast.getDeclaredConstructor(ConstructorParam);
console.log("Constructor:"+Constructor);
console.log("orinin:"+mainAc);
//实例化,newInstance的参数也是Ljava.lang.Object;
var instance = Constructor.newInstance([mainAc]);
console.log("patchAc:"+instance);
send(instance);
console.log("--------------------------------------------------------------------");
return result;

}
return result;
}
});

利用getDeclaredMethods(),获取本类中的所有方法

Java.perform(function(){
var hookClass = undefined;
var ClassUse = Java.use("java.lang.Class");
var objectclass= Java.use("java.lang.Object");
var dexclassLoader = Java.use("dalvik.system.DexClassLoader");
var orininclass = Java.use("cn.chaitin.geektan.crackme.MainActivity");
var Integerclass = Java.use("java.lang.Integer");
//实例化MainActivity对象
var mainAc = orininclass.$new();


dexclassLoader.loadClass.overload('java.lang.String').implementation = function(name){
var hookname = "cn.chaitin.geektan.crackme.MainActivityPatch";
var result = this.loadClass(name,false);

if(name == hookname){
var hookClass = result;
var hookClassCast = Java.cast(hookClass,ClassUse);
console.log("-----------------------------BEGIN-------------------------------------");
//获取构造器
var ConstructorParam =Java.array('Ljava.lang.Object;',[objectclass.class]);
var Constructor = hookClassCast.getDeclaredConstructor(ConstructorParam);
console.log("Constructor:"+Constructor);
console.log("orinin:"+mainAc);
//实例化,newInstance的参数也是Ljava.lang.Object;
var instance = Constructor.newInstance([mainAc]);
console.log("MainActivityPatchInstance:"+instance);
send(instance);
console.log("----------------------------Methods---------------------------------");
var func = hookClassCast.getDeclaredMethods();
console.log(func);
console.log("--------------------------Need Method---------------------------------");
console.log(func[0]);
var f = func[0];
console.log("---------------------------- OVER---------------------------------");
return result;

}
return result;
}
});

调用Method.invoke()去执行方法(invoke方法的第一个参数是执行这个方法的对象实例,第二个参数是带入的实际值数组,返回值是Object,也既是该方法执行后的返回值)

f.invoke(instance,Array);

read-std-string

/*
* Note: Only compatible with libc++, though libstdc++'s std::string is a lot simpler.
*/

function readStdString (str) {
const isTiny = (str.readU8() & 1) === 0;
if (isTiny) {
return str.add(1).readUtf8String();
}

return str.add(2 * Process.pointerSize).readPointer().readUtf8String();
}

whereisnative

Java.perform(function() {

var SystemDef = Java.use('java.lang.System');

var RuntimeDef = Java.use('java.lang.Runtime');

var exceptionClass = Java.use('java.lang.Exception');

var SystemLoad_1 = SystemDef.load.overload('java.lang.String');

var SystemLoad_2 = SystemDef.loadLibrary.overload('java.lang.String');

var RuntimeLoad_1 = RuntimeDef.load.overload('java.lang.String');

var RuntimeLoad_2 = RuntimeDef.loadLibrary.overload('java.lang.String');

var ThreadDef = Java.use('java.lang.Thread');

var ThreadObj = ThreadDef.$new();

SystemLoad_1.implementation = function(library) {
send("Loading dynamic library => " + library);
stackTrace();
return SystemLoad_1.call(this, library);
}

SystemLoad_2.implementation = function(library) {
send("Loading dynamic library => " + library);
stackTrace();
SystemLoad_2.call(this, library);
return;
}

RuntimeLoad_1.implementation = function(library) {
send("Loading dynamic library => " + library);
stackTrace();
RuntimeLoad_1.call(this, library);
return;
}

RuntimeLoad_2.implementation = function(library) {
send("Loading dynamic library => " + library);
stackTrace();
RuntimeLoad_2.call(this, library);
return;
}

function stackTrace() {
var stack = ThreadObj.currentThread().getStackTrace();
for (var i = 0; i < stack.length; i++) {
send(i + " => " + stack[i].toString());
}
send("--------------------------------------------------------------------------");
}

});

Non-ASCII

int ֏(int x) {
return x + 100;
}

甚至有一些不可视, 所以可以先编码打印出来, 再用编码后的字符串去 hook.<\br>

Java.perform(
function x() {

var targetClass = "com.example.hooktest.MainActivity";

var hookCls = Java.use(targetClass);
var methods = hookCls.class.getDeclaredMethods();

for (var i in methods) {
console.log(methods[i].toString());
console.log(encodeURIComponent(methods[i].toString().replace(/^.*?\.([^\s\.\(\)]+)\(.*?$/, "$1")));
}

hookCls[decodeURIComponent("%D6%8F")]
.implementation = function (x) {
console.log("original call: fun(" + x + ")");
var result = this[decodeURIComponent("%D6%8F")](900);
return result;
}
}
)

Hook 数据库

var SQLiteDatabase = Java.use('com.tencent.wcdb.database.SQLiteDatabase');
var Set = Java.use("java.util.Set");
var ContentValues = Java.use("android.content.ContentValues");
SQLiteDatabase.insert.implementation = function (arg1,arg2,arg3) {

this.insert.call(this, arg1, arg2, arg3);
console.log("[insert] -> arg1:" + arg1 + "\t arg2:" + arg2);
var values = Java.cast(arg3, ContentValues);
var sets = Java.cast(values.keySet(), Set);

var arr = sets.toArray().toString().split(",");
for (var i = 0; i < arr.length; i++){
console.log("[insert] -> key:" + arr[i] + "\t value:" + values.get(arr[i]));
}
};

Hook JNI Native GetStringUTFChars

function hook_native_GetStringUTFChars() {
var env = Java.vm.getEnv();
var handlePointer = Memory.readPointer(env.handle);
console.log("env handle: " + handlePointer);
var GetStringUTFCharsPtr = Memory.readPointer(handlePointer.add(0x2A4));
console.log("GetStringUTFCharsPtr addr: " + GetStringUTFCharsPtr);
Interceptor.attach(GetStringUTFCharsPtr, {
onEnter: function (args) {
var str = "";
Java.perform(function () {
str = Java.cast(args[1], Java.use('java.lang.String'));
});
console.log("GetStringUTFChars: " + str);

}
});
}

主动弹窗

Java.perform(function() {
var System = Java.use('java.lang.System');
var ActivityThread = Java.use("android.app.ActivityThread");
var AlertDialogBuilder = Java.use("android.app.AlertDialog$Builder");
var DialogInterfaceOnClickListener = Java.use('android.content.DialogInterface$OnClickListener');

Java.use("android.app.Activity").onCreate.overload("android.os.Bundle").implementation = function(savedInstanceState) {
var currentActivity = this;

// Get Main Activity
var application = ActivityThread.currentApplication();
var launcherIntent = application.getPackageManager().getLaunchIntentForPackage(application.getPackageName());
var launchActivityInfo = launcherIntent.resolveActivityInfo(application.getPackageManager(), 0);

// Alert Will Only Execute On Main Package Activity Creation
console.log(this.getComponentName().getClassName())
/**
* non protective application
* if (launchActivityInfo === this.getComponentName().getClassName()) {
* ...
* }
*/

if (this.getComponentName().getClassName() === "com.xxx") {

var alert = AlertDialogBuilder.$new(this);
var jString = Java.use('java.lang.String');
var CharSequence = Java.use('java.lang.CharSequence');
var charSequence = Java.cast(jString.$new("What you want to do now?"), CharSequence);
var charSequence1 = Java.cast(jString.$new("Dismiss"), CharSequence);
var charSequence2 = Java.cast(jString.$new("Force Close!"), CharSequence);
alert.setMessage(charSequence);

alert.setPositiveButton(charSequence1, Java.registerClass({
name: 'il.co.realgame.OnClickListenerPositive',
implements: [DialogInterfaceOnClickListener],
methods: {
getName: function() {
return 'OnClickListenerPositive';
},
onClick: function(dialog, which) {
// Dismiss
dialog.dismiss();
}
}
}).$new());

alert.setNegativeButton(charSequence2, Java.registerClass({
name: 'il.co.realgame.OnClickListenerNegative',
implements: [DialogInterfaceOnClickListener],
methods: {
getName: function() {
return 'OnClickListenerNegative';
},
onClick: function(dialog, which) {
// Close Application
//currentActivity.finish();
System.exit(0);
}
}
}).$new());

// Create Alert
alert.create().show();
}
return this.onCreate.overload("android.os.Bundle").call(this, savedInstanceState);
};
});

Hook prettyMethod

const STD_STRING_SIZE = 3 * Process.pointerSize;
class StdString {
constructor() {
this.handle = Memory.alloc(STD_STRING_SIZE);
}

dispose() {
const [data, isTiny] = this._getData();
if (!isTiny) {
Java.api.$delete(data);
}
}

disposeToString() {
const result = this.toString();
this.dispose();
return result;
}

toString() {
const [data] = this._getData();
return data.readUtf8String();
}

_getData() {
const str = this.handle;
const isTiny = (str.readU8() & 1) === 0;
const data = isTiny ? str.add(1) : str.add(2 * Process.pointerSize).readPointer();
return [data, isTiny];
}
}

function prettyMethod(method_id, withSignature) {
const result = new StdString();
Java.api['art::ArtMethod::PrettyMethod'](result, method_id, withSignature ? 1 : 0);
return result.disposeToString();
}

参考

https://eternalsakura13.com/2020/07/04/frida/#more

Author

ol4three

Posted on

2021-12-21

Updated on

2022-07-05

Licensed under


Comments