0x00 测试用例

本次的hook代码都用 python接口方式 书写。首先写一个简单的程序用来测试。后续的测试就在这个程序上小修小改,不做赘述。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layout_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:context=".MainActivity">

<Button
android:id="@+id/btn_create"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="实例化一个Calc" />

<LinearLayout
android:id="@+id/layout_add1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal">

<EditText
android:id="@+id/edt_add1"
android:layout_width="198dp"
android:layout_height="wrap_content"
android:autofillHints=""
android:ems="5"
android:hint="输入一个数"
android:inputType="number" />

<Button
android:id="@+id/btn_add1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="add1()" />
</LinearLayout>

</LinearLayout>
package com.zyc.fridademo;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private EditText edtAdd1;
private Button btnCreate;
private Button btnAdd1;
private Calc calc;

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

edtAdd1 = findViewById(R.id.edt_add1);
btnCreate = findViewById(R.id.btn_create);
btnCreate.setOnClickListener(this);
btnAdd1 = findViewById(R.id.btn_add1);
btnAdd1.setOnClickListener(this);
}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_create:
calc = new Calc(1);
Toast.makeText(this, "已创建一个Calc\nbase=" + calc.base, Toast.LENGTH_SHORT).show();
break;
case R.id.btn_add1:
int p1 = Integer.parseInt(String.valueOf(edtAdd1.getText()));
int res = calc.add(p1);
Toast.makeText(this, "执行add(p1)\n结果=" + res, Toast.LENGTH_SHORT).show();
break;
}
}
}
package com.zyc.fridademo;

public class Calc {
public int base;

/**
* 构造方法
*/
public Calc(int p1) {
this.base = p1;
}

/**
* 普通方法
*
* @return num1与base相加结果
*/
public int add(int num1) {
return base + num1;
}
}

运行:

img

0x01hook普通函数

import frida, sys

jscode = """
Java.perform(function(){
var clazz = Java.use("com.zyc.fridademo.Calc");
clazz.add.implementation = function(p1)
{
console.log("Hook开始...");
send("原p1="+p1);
console.log("Hook修改参数...");
p1+=100;
send("现p1="+p1);
return this.add(p1);
}
});
"""

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

process=frida.get_remote_device().attach('com.zyc.fridademo')
script=process.create_script(jscode)
script.on("message",message)
script.load()
sys.stdin.read()

启动frida-server,使用端口转发,执行上面的脚本,即可实现输入的数增加100之后再执行原函数。

img

0x02 Hook构造函数

如果是Hook构造函数,只需使用$init引用,将.py的js部分修改为:

Java.perform(function(){
var clazz = Java.use("com.zyc.fridademo.Calc");
clazz.$init.implementation = function(p1)
{
console.log("Hook构造开始...");
send("原p1="+p1);
p1+=200;
send("现p1="+p1);
return this.$init(p1);
}
});

执行:

img

0x03 Hook重载函数

在Calc类中增加一个重载方法add(int num1,String num2):

public int add(int num1,String num2) {
return base + num1;
}

由于Calc中有add(int num1)和add(int num1,String num2),像普通函数那样Hook是会报错的,涉及到重载就要用到 overload()

Java.perform(function(){
var clazz = Java.use("com.zyc.fridademo.Calc");
clazz.add.overload("int").implementation = function()
{
console.log("Hook add(int num1)...");
return 888;
}

clazz.add.overload("int","java.lang.String").implementation = function()
{
console.log("Hook add(int num1,String num2)...");
return 999;
}
});

img

如果重载函数多,可以通过 overloads 获得全部重载对象,并通过js的 applyarguments 特性继续程序流程。下面例子遍历了add(int a)和add(int a,String b)函数,并修改了两个函数的第一个参数,打印了第二个参数(如果有)。

Java.perform(function(){
var clazz = Java.use("com.zyc.fridademo.Calc");
var count = clazz.add.overloads.length;
for(var i=0;i<count;i++){
clazz.add.overloads[i].implementation = function(){
arguments[0] = 8;
if(arguments[1]){
send(arguments[1]);
}
return this.add.apply(this,arguments);
}
}
});

img

0x04 实例化类

使用 $new() 可以实例化一个类,这里我们在MainActivity onCreate()时实例化一个Calc,构造方法传入10(按钮实例化的传入的是1):

Java.perform(function(){
var clazz = Java.use("com.zyc.fridademo.MainActivity");
var calc = Java.use("com.zyc.fridademo.Calc");
clazz.onCreate.implementation = function()
{
console.log("Hook MainActivity onCreate()...");
var myCalc = calc.$new(10);
return this.onCreate(arguments[0]);
}

});

img

0x05 访问类的属性

类的属性可通过 .属性名.value 访问。如果有函数与属性名相同,则需要使用下划线方式 ._属性名.value 访问。

Java.perform(function(){
var clazz = Java.use("com.zyc.fridademo.MainActivity");
var calc = Java.use("com.zyc.fridademo.Calc");
clazz.onCreate.implementation = function()
{
console.log("Hook MainActivity onCreate()...");
var myCalc = calc.$new(10);
send(myCalc.base.value);
console.log("修改一下base...");
myCalc.base.value = 88;
send(myCalc.base.value);
return this.onCreate(arguments[0]);
}
});

img

0x06 hook内部类

使用 外部类$内部类 的写法可以实现内部类Hook,为了测试简单改写一下程序:

//Calc类中增加静态内部类InnerClass
static class InnerClass{
public static String print(){
return "我是内部类";
}
}

//MainActivity的onCreate()中加入一个按钮,点击触发InnerClass的print()方法
btnInnerClass = findViewById(R.id.btn_inner_class);
btnInnerClass.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String txt = Calc.InnerClass.print();
Toast.makeText(MainActivity.this, txt, Toast.LENGTH_SHORT).show();
}
});

Hook代码,修改print()方法的返回值:

Java.perform(function(){
var innerClazz = Java.use("com.zyc.fridademo.Calc$InnerClass");
innerClazz.print.implementation = function()
{
return "我是被hook的内部类";
}
});

img

0x07 Hook匿名类

看过smali的都知道匿名类反编译出来是 类$数字 形式,如上面调用内部类方法时创建的View.OnClickListener就是一个匿名类,通过反编译工具能看到其名称为 MainActivity$1

img

于是可以利用frida来监听View.OnclickListener方法

Java.perform(function(){
var innerClazz = Java.use("com.zyc.fridademo.MainActivity$1");
innerClazz.onClick.implementation = function()
{
send("执行了匿名类点击方法");
return ;
}
});

img

0x08 遍历已加载的类与类方法

enumerateLoadedClasses 可以异步获取已加载的类,再通过类名反射即可获得类方法:

Java.perform(function(){
Java.enumerateLoadedClasses({
onMatch : function(name,handle){
if(name.indexOf("com.zyc.fridademo") != -1){
console.log(name);
var clazz = Java.use(name);
var methods = clazz.class.getDeclaredMethods();
for(var i=0;i<methods.length;i++){
console.log(methods[i]);
}
}
},
onComplete:function(){}
});
});

img

这里引申一下用 类变量[方法名] 方式的Hook:

Java.perform(function(){
var innerClazz = Java.use("com.zyc.fridademo.Calc$InnerClass");
var methods = innerClazz.class.getDeclaredMethods();
for(var i=0;i<methods.length;i++){
var methodName = methods[i].getName();
if(methodName.indexOf("print") != -1){
innerClazz[methodName].implementation = function(){
send("我是反射来的");
return "123";
}
}
}
});

运行

img

0x09 遍历类实例

使用 choose 可查找堆中的类实例:

Java.perform(function(){
var clazz = Java.use("com.zyc.fridademo.MainActivity");
var calc = Java.use("com.zyc.fridademo.Calc");
clazz.onCreate.implementation = function()
{
console.log("Hook MainActivity onCreate()...");

//实例化3个Calc
calc.$new(2);
calc.$new(3);
calc.$new(4);

//打印每个实例的base值
Java.choose("com.zyc.fridademo.Calc" , {
onMatch : function(instance){
console.log("Found instance: "+instance);
send("instance.base="+instance.base.value);
},
onComplete:function(){}
});
return this.onCreate(arguments[0]);
}
});

img

0x10 Hook动态加载dex

首先写一个供动态加载的jar包放置在data/data/com.zyc.fridademo/files,反编译出来是这样的

img

上面的案例程序中多加一个按钮来使用该jar包中类,详细流程自行参考DexClassLoader相关知识,这里只展示按钮部分代码:

case R.id.btn_mydex:
//加载dex class
if (dexClassLoader==null){ //确保只创建一个DexClassLoader,不然多个loader影响hook
String dexPath = this.getFilesDir() + File.separator + "mydex.jar";
dexClassLoader = new DexClassLoader(dexPath, this.getFilesDir().getAbsolutePath(), null, getClassLoader());
}
try {
Class<?> clz = dexClassLoader.loadClass("com.zyc.mydex.Human");
Object human = clz.newInstance() ;
if (human != null) {
Method say = clz.getDeclaredMethod("say");
say.setAccessible(true);
String what = String.valueOf(say.invoke(human)); //反射调用say,返回“我是动态dex”
Toast.makeText(this, what, Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
e.printStackTrace();
}
break;

在获取loader时记得使用 try catch,否则loadClass()发生异常会导致程序终止。Hook动态dex的关键在于使用正确的loader:

Java.perform(function(){
Java.enumerateClassLoaders({
onMatch : function(loader){
try{
if(loader.loadClass("com.zyc.mydex.Human")){
console.log("正确loader");
Java.classFactory.loader = loader;
var clazz = Java.use("com.zyc.mydex.Human");
send(clazz);
clazz.say.implementation = function(){
return "我是被hook的动态dex";
}
}
}catch(err){
console.log(err)
}
},
onComplete:function(){}
});
});

img

0x11 打印函数堆栈

打印函数堆栈的关键在于 android.util.Log.getStackTraceString(new Throwable())android.util.Log.getStackTraceString(new Exception()) ,于是我们可以这样Hook:

Java.perform(function(){
var clazz = Java.use("com.zyc.fridademo.Calc");
clazz.add.overload("int").implementation = function(num)
{
var log = Java.use("android.util.Log");
var throwable = Java.use("java.lang.Throwable");
var stack = log.getStackTraceString(throwable.$new());
send(stack);
return this.add(num);
}
});

运行,可以看到除系统调用外,执行流程为 MainActivity.onCreate -> Calc.add()

img

0x12 注入dex文件

当程序本身功能不能满足我们需求时,如果要利用frida增加功能,可以通过下面方法:

  • Java.registerClass 注入自己的类,需要使用js撰写一个java类传入。
  • Java.openClassFile 注入自己的dex文件,只需传入dex路径。

显然,注入dex文件会方便很多。那么现在我写一个简单的类打包后提取其dex放置为 /data/local/tmp/injectiondex.dex:

package com.zyc.injectiondex;

public class Cat {
public String say() {
return "喵喵喵";
}

public String eat(){
return "多吃几口";
}
}

使用frida注入之前的内部类方法:

Java.perform(function(){
Java.openClassFile("/data/local/tmp/injectiondex.dex").load();
var cat = Java.use("com.zyc.injectiondex.Cat");
var oneCat = cat.$new();
var catsay = oneCat.say();
var cateat = oneCat.eat();

var innerClazz = Java.use("com.zyc.fridademo.Calc$InnerClass");
innerClazz.print.implementation = function()
{
return catsay+cateat;
}
});

运行:

img