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; } public int add (int num1) { return base + num1; } }
运行:
0x01hook普通函数 import frida, sysjscode = """ 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之后再执行原函数。
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); } });
执行:
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 ; } });
如果重载函数多,可以通过 overloads 获得全部重载对象,并通过js的 apply 和 arguments 特性继续程序流程。下面例子遍历了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 ); } } });
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 ]); } });
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 ]); } });
0x06 hook内部类 使用 外部类$内部类 的写法可以实现内部类Hook,为了测试简单改写一下程序:
static class InnerClass { public static String print () { return "我是内部类" ; } } 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的内部类" ; } });
0x07 Hook匿名类 看过smali的都知道匿名类反编译出来是 类$数字 形式,如上面调用内部类方法时创建的View.OnClickListener就是一个匿名类,通过反编译工具能看到其名称为 MainActivity$1 。
于是可以利用frida来监听View.OnclickListener方法
Java .perform (function ( ){ var innerClazz = Java .use ("com.zyc.fridademo.MainActivity$1" ); innerClazz.onClick .implementation = function ( ) { send ("执行了匿名类点击方法" ); return ; } });
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 ( ){} }); });
这里引申一下用 类变量[方法名] 方式的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" ; } } } });
运行
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()..." ); calc.$new(2 ); calc.$new(3 ); calc.$new(4 ); 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 ]); } });
0x10 Hook动态加载dex 首先写一个供动态加载的jar包放置在data/data/com.zyc.fridademo/files,反编译出来是这样的
上面的案例程序中多加一个按钮来使用该jar包中类,详细流程自行参考DexClassLoader相关知识,这里只展示按钮部分代码:
case R.id.btn_mydex: if (dexClassLoader==null ){ 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)); 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 ( ){} }); });
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() :
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; } });
运行: