这篇文章记录一下objection和frida的使用方法,相当于操作手册。
1. objection objection功能强大,命令众多,而且不用写一行代码,便可实现诸如内存搜索、类和模块搜索、方法hook打印参数返回值调用栈等常用功能,是一个非常方便的,逆向必备、内存漫游神器。
下面以安卓内置应用“设置”为例,示范一下objection的基本用法。
运行“设置”App,启动frida-server。
1 2 3 4 5 ┌──(root㉿kali)-[~] └─# adb shell bullhead:/ $ su - bullhead:/ # cd /data/local/tmp bullhead:/data/local/tmp # ./frida-server-12.8.0-android-arm64
查看“设置”应用的包名:
1 2 3 ┌──(root㉿kali)-[~] └─# frida-ps -U|grep -i setting 5649 com.android.settings
再使用objection注入“设置”应用:
1 objection -g com.android.settings explore
1.1 获取基本信息 启动objection之后,会出现提示它的logo,这时候不知道输入什么命令的话,可以按下空格,有提示的命令及其功能出来;再按空格选中,又会有新的提示命令出来,这时候按回车就可以执行该命令。
如果不知道当前命令的效果是什么,在当前命令前加help,比如help env,回车之后会出现当前命令的解释信息。
objection还有一个jobs(作业系统),建议一定要掌握,可以同时运行多项(hook)作业。
1 2 3 com.android.settings on (google: 8.1.0) [usb] # jobs list Job ID Hooks Type ------ ----- ----
1.2 提取内存信息 查看内存中加载的库:
查看库中的导出函数:
1 memory list exports libssl.so
如果输出结果太多,终端无法全部显示的时候,可以将结果导出到文件中,然后使用其他软件查看内容:
1 memory list exports libart.so --json /root/libart.json
提取整个(或部分)内存到当前目录的from_base文件中:
1 memory dump all from_base
搜索整个内存:
1 memory search --string --offsets-only
1.3 内存堆搜索与执行 我们查看AOSP源码关于设置里显示系统设置的部分,发现存在着DisplaySettings类,可以在堆上搜索是否存在着该类的实例。首先在手机上点击进入“显示”设置,然后运行以下命令,并得到相应的实例地址:
1 android heap search instances com.android.settings.DisplaySettings
查看源码得知com.android.settings.DisplaySettings类有着getPreferenceScreenResId()
方法,这样就可以直接调用该实例的getPreferenceScreenResId()
方法,用excute命令:
1 android heap execute 0x22a2 getPreferenceScreenResId
也可以在找到的实例上直接编写js脚本,输入
1 android heap evaluate 0x22a2
命令后,会进入一个迷你编辑器环境,输入
1 console.log("evaluate result:"+clazz.getPreferenceScreenResId())
这串脚本,按ESC退出编辑器,然后按回车,即会开始执行这串脚本,输出结果。
这个功能非常厉害,可以即时编写、出结果、即时调试自己的代码,不用再编写→注入→操作→看结果→再调整,而是直接出结果。
1.4 启动activity或service 直接某个启动activity:
1 android intent launch_activity com.android.settings.DisplaySettings
查看当前可用的activity:
1 android hooking list activities
启动service:
1 android intent launch_service com.android.settings.bluetooth.BluetoothPairingService
1.5 内存漫游 列出内存中所有的类:
1 android hooking list classes
在内存中所有已加载的类中搜索包含特定关键词的类:
1 android hooking search classes display
在内存中所有已加载的类的方法中搜索包含特定关键词的方法:
1 android hooking search methods display
列出指定类的所有方法:
1 android hooking list class_methods com.android.settings.DisplaySettings
在列出类的方法时,还直接把参数也提供了,也就是说我们可以直接动手写hook了,既然上述写hook的要素已经全部都有了,objection这个“自动化”工具,当然可以直接生成代码。
1 android hooking generate simple com.android.settings.DisplaySettings
生成的代码大部分要素都有了,只是参数貌似没有填上,还是需要我们后续补充一些,看来还是无法做到完美。
1.6 hook 上述操作均是基于在内存中直接枚举搜索,已经可以获取到大量有用的静态信息,我们再来介绍几个方法,可以获取到执行时动态的信息。
我们以手机连接蓝牙耳机播放音乐为例为例,看看手机蓝牙接口的动态信息。
首先我们将手机连接上我的蓝牙耳机,并可以正常播放音乐;然后我们按照上文的方法,搜索一下与蓝牙相关的类:
1 android hooking search classes bluetooth
搜到一个高度可疑的类:android.bluetooth.BluetoothDevice。运行以下命令,hook这个类:
1 android hooking watch class android.bluetooth.BluetoothDevice
使用命令
可以看到objection为我们创建的hooks数为55,也就是将android.bluetooth.BluetoothDevice类下的所有方法都hook了。
这时候我们在“声音”或者“蓝牙”上进行操作,会命中这些hook,此时objection就会将方法打印出来,会将类似这样的信息“吐”出来:
在这些方法中,我们对哪些方法感兴趣,就可以查看哪些方法的参数、返回值和调用栈,比如想看getName()
方法,则运行以下命令:
1 android hooking watch class_method android.bluetooth.BluetoothDevice.getName --dump-args --dump-return --dump-backtrace
注意最后加上的三个选项—dump-args —dump-return —dump-backtrace,为我们成功打印出来了我们想要看的信息,其实返回值Return Value就是getName()方法的返回值,我的蓝牙耳机的型号名字M200 Pro。
再来看个有参数的,比如equals()
方法:
1 android hooking watch class_method android.bluetooth.BluetoothDevice.equals --dump-args --dump-return --dump-backtrace
objection的help中指出,在hook给出的单个方法的时候,会hook它的所有重载。那我们可以用File类的构造器来试一下效果:
1 android hooking watch class_method java.io.File.$init --dump-args
可以看到objection为我们hook了File构造器的所有重载,一共是6个。在设置界面随意进出几个子设置界面,可以看到命中很多次该方法的不同重载,每次参数的值也都不同。
有些类和方法在App启动时就加载完毕了,这时如何hook这些类和方法呢?
1 objection -g <packagename> explore --startup-command 'android hooking watch ......'
或者用上面的方法,它既然被调用过了,那它肯定在内存堆中,搜索并且主动执行该类或方法。
另外,objection无法hook so函数。
1.7 插件 有很多小工具支持作为objection插件来使用,比如Wallbreaker、frida-dexdump等。
1.7.1 Wallbreaker 加载Wallbreaker插件,load后面跟插件所在路径:
1 plugin load ~/.objection/plugins/Wallbreaker
Wallbreaker搜索类,根据给的 pattern 对所有类名进行匹配,列出匹配到的所有类名:
1 plugin wallbreaker classsearch <pattern>
搜索对象,根据类名搜索内存中已经被创建的实例,列出 handle 和 toString() 的结果:
1 plugin wallbreaker objectsearch <classname>
ClassDump,输出类的结构, 若加了 —fullname 参数,打印的数据中类名会带着完整的包名:
1 plugin wallbreaker classdump <classname> [--fullname]
ObjectDump,在 ClassDump 的基础上,输出指定对象中的每个字段的数据:
1 plugin wallbreaker objectdump <handle> [--fullname]
1.8 例子 可用上次做过的例子攻防世界Mobile app3 ,这次用另一种方法,利用frida hook解题。
可知下图是解题的关键,如何把getWritableDatabase()
方法的返回值hook出来呢?
由于调用它的a()
方法在App启动时就加载了,并且后面的操作再也没有使用过它,所以我们要在App启动前hookgetWritableDatabase()
。
首先运行frida-server,注入objection:
1 objection -g com.example.yaphetshan.tencentwelcome explore
一个App中有很多个类,因为不仅有自定义的,还有系统自带的,所以我们可以搜索App包名下有哪些类:
1 android hooking search classes com.example.yaphetshan.tencentwelcome
接着可以将某个类中的所有方法列举出来:
1 android hooking list class_methods com.example.yaphetshan.tencentwelcome.a
回归正题。要hook一个方法,首先要知道它所在的包名、类名与方法名。
1 android hooking search methods getWritableDatabase
呃…我用上面这个进行搜索没找到,是因为我用了Android 4.4。Android 5.1又可以了。
也可以看从jadx中查看getWritableDatabase()
的定义,就可以知道它从哪里来的,但静态反编译不一定准确。
hook这个方法可以用以下命令:
1 android hooking watch class_method net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase --dump-args --dump-backtrace --dump-return
虽然我们成功hook了,但是无论我们怎么运行都不会命中它,因为它只有在启动时才会被加载。所以我们需要在App启动前就hook。将模拟器中的程序完全退出,执行命令:
1 objection -g com.example.yaphetshan.tencentwelcome explore --startup-command 'android hooking watch class_method net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase --dump-args --dump-backtrace --dump-return'
有个警告说某些hook失败,马上换Android 5。hook成功了,但是在App启动过程中没有命中该方法。
用另一种方法,先hook好getWritableDatabase()
方法,再利用内存堆搜索MainActivity类的句柄,进而主动执行该类的a()
方法。因为MainActivity类中的a()
方法调用了getWritableDatabase()
方法,所以按理说是可以命中的。
1 2 3 android hooking watch class_method net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase --dump-args --dump-backtrace --dump-return android heap search instances com.example.yaphetshan.tencentwelcome.MainActivity android heap execute 0x20043e a
2. frida基本操作 在AS中创建一个Empty Activity,直接运行安装在手机上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.v5le0n9.test01;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }
现在我们可以在代码中添加一点东西,让它每隔1s在LogCat中输出加法运算的结果:
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 package com.v5le0n9.test01;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.util.Log;public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); while (true ){ try { Thread.sleep(5000 ); } catch (InterruptedException e) { e.printStackTrace(); } int m = fun(50 ,100 ); Log.d("l30n9ry0n2" , String.valueOf(m)); } } int fun (int x, int y) { Log.d("l30n9ry0n1" , String.valueOf(x+y)); return x+y; } }
接下来可以编写js代码将App在LogCat中的结果打印出来:
1 2 3 4 5 6 7 8 9 10 11 function main ( ){ Java .perform (function ( ){ Java .use ("com.v5le0n9.test01.MainActivity" ).fun .implementation = function (args1, args2 ){ var result = this .fun (args1, args2) console .log ("args1, args2, result" , args1, args2, result) return result } }) } setImmediate (main)
启动frida-server,执行js代码:
1 frida -U com.v5le0n9.test01 -l test01.js
2.1 修改参数 修改传入的参数,返回值的结果也随之改变:
1 2 3 4 5 6 7 8 9 10 11 function main ( ){ Java .perform (function ( ){ Java .use ("com.v5le0n9.test01.MainActivity" ).fun .implementation = function (args1, args2 ){ var result = this .fun (20 , 90 ) console .log ("args1, args2, result" , args1, args2, result) return result } }) } setImmediate (main)
2.2 修改返回值 直接修改返回值结果:
1 2 3 4 5 6 7 8 9 10 11 function main ( ){ Java .perform (function ( ){ Java .use ("com.v5le0n9.test01.MainActivity" ).fun .implementation = function (args1, args2 ){ var result = this .fun (args1, args2) console .log ("args1, args2, result" , args1, args2, result) return 80 } }) } setImmediate (main)
2.3 查找调用栈 调用栈,可以查看函数是从哪里来的:
1 2 3 4 5 6 7 8 9 10 11 function main ( ){ Java .perform (function ( ){ Java .use ("com.v5le0n9.test01.MainActivity" ).fun .implementation = function (args1, args2 ){ var result = this .fun (args1, args2) console .log (Java .use ("android.util.Log" ).getStackTraceString (Java .use ("java.lang.Throwable" ).$new())) console .log ("args1, args2, result" , args1, args2, result) return result } }) } setImmediate (main)
比如这个fun()
函数就是从MainActivity.java
的onCreate()
函数中来的。
2.4 函数重载 如果代码中有函数重载,而我们刚好需要hook这个函数,此时利用上面的js代码就会出错:
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 package com.v5le0n9.test01;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.util.Log;import java.util.Locale;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(5000 ); } catch (InterruptedException e) { e.printStackTrace(); } int m = fun(50 ,100 ); Log.d("l30n9ry0n2" , String.valueOf(m)); Log.d("l30n9ry0n tolowercase" , fun("LOWERCASEME!" )); } } String fun (String x) { total += x; return x.toLowerCase(); } String secret () { return total; } int fun (int x, int y) { Log.d("l30n9ry0n1" , String.valueOf(x+y)); return x+y; } }
它也提示了说加上.overload(<signature>)
成员。如果想两个函数都打印出来,再添加一个Java.use即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function main ( ){ Java .perform (function ( ){ Java .use ("com.v5le0n9.test01.MainActivity" ).fun .overload ('int' , 'int' ).implementation = function (args1, args2 ){ var result = this .fun (args1, args2) console .log ("args1, args2, result" , args1, args2, result) return result } Java .use ("com.v5le0n9.test01.MainActivity" ).fun .overload ('java.lang.String' ).implementation = function (args1 ){ var result = this .fun (args1) console .log ("args1, result" , args1, result) return result } }) } setImmediate (main)
同样也可以修改参数和返回值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function main ( ){ Java .perform (function ( ){ Java .use ("com.v5le0n9.test01.MainActivity" ).fun .overload ('int' , 'int' ).implementation = function (args1, args2 ){ var result = this .fun (args1, args2) console .log ("args1, args2, result" , args1, args2, result) return result } Java .use ("com.v5le0n9.test01.MainActivity" ).fun .overload ('java.lang.String' ).implementation = function (args1 ){ var result = this .fun (Java .use ("java.lang.String" ).$new("LIKEYOU" )) console .log ("args1, result" , args1, result) return Java .use ("java.lang.String" ).$new("METOO" ) } }) } setImmediate (main)
2.5 动静态处理和主动调用 如果想打印没有被调用的函数,比如secret()
方法,也可以打印该方法的实例和返回值。如果是动态调用,需要找到实例进行主动调用:
1 2 3 4 5 6 7 8 9 10 11 12 function main ( ){ Java .perform (function ( ){ Java .choose ("com.v5le0n9.test01.MainActivity" ,{ onMatch :function (instance ){ console .log ("found instance:" ,instance) console .log ("found instance:" ,instance.secret ()) },onComplete :function ( ){} }) }) } setImmediate (main)
静态调用直接打印就好:
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 package com.v5le0n9.test01;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.util.Log;import java.util.Locale;public class MainActivity extends AppCompatActivity { private static String total = "!!!---!!!" ; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); while (true ){ try { Thread.sleep(5000 ); } catch (InterruptedException e) { e.printStackTrace(); } int m = fun(50 ,100 ); Log.d("l30n9ry0n2" , String.valueOf(m)); Log.d("l30n9ry0n tolowercase" ,fun("LOWERCASEME!" )); } } String fun (String x) { total += x; return x.toLowerCase(); } String secret () { return total; } public static String secret2 () { return total; } int fun (int x, int y) { Log.d("l30n9ry0n1" , String.valueOf(x+y)); return x+y; } }
1 2 3 4 5 6 7 8 function main ( ){ Java .perform (function ( ){ var result = Java .use ("com.v5le0n9.test01.MainActivity" ).secret2 () console .log ("invoke secret2:" , result) }) } setImmediate (main)
2.6 例子 同样用app3做例子,在1.8上讲过,先hook getWritableDatabase()
方法,再主动调用MainActivity类中的a()
方法。
因为getWritableDatabase()
方法有重载,所以我们把所有重载都hook上看看。还要知道该方法是动态的还是静态的,使用动静态处理。由于a()
方法在App完全启动前就加载了,所以如果想再次执行,需要主动调用该方法。
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 function main ( ){ Java .perform (function ( ){ Java .use ("net.sqlcipher.database.SQLiteOpenHelper" ).getWritableDatabase .overload ('java.lang.String' ).implementation = function (args1 ){ var result = this .getWritableDatabase (args1) console .log ("args1 string, result" , args1, result) return result } Java .use ("net.sqlcipher.database.SQLiteOpenHelper" ).getWritableDatabase .overload ('[C' ).implementation = function (args1 ){ var result = this .getWritableDatabase (args1) console .log ("args1 char[], result" , args1, result) return result } }) } setImmediate (main)function invoke ( ){ Java .perform (function ( ){ Java .choose ("com.example.yaphetshan.tencentwelcome.MainActivity" ,{ onMatch :function (instance ){ console .log ("found instance:" ,instance) console .log ("invoke instance.a:" ,instance.a ()) },onComplete :function ( ){console .log ("search completed" )} }) }) } setTimeout (invoke,3000 )
执行命令:
1 2 frida -U com.example.yaphetshan.tencentwelcome -l app3.js invoke()
3. frida构造数组、对象、Map和类参数 3.1 构造数组 先将每个字逐个hook出来,也就是使用Character.toString()
方法那一条语句。
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 package com.v5le0n9.test02;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.util.Log;import java.nio.charset.StandardCharsets;import java.util.Arrays;public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.d("SimpleArray" , "onCreate: SimpleArray" ); char arr[][] = new char [4 ][]; arr[0 ] = new char [] {'春' ,'眠' ,'不' ,'觉' ,'晓' }; arr[1 ] = new char [] {'处' ,'处' ,'闻' ,'啼' ,'鸟' }; arr[2 ] = new char [] {'夜' ,'来' ,'风' ,'雨' ,'声' }; arr[3 ] = new char [] {'花' ,'落' ,'知' ,'多' ,'少' }; Log.d("SimpleArray" , "-----横板-----" ); for (int i=0 ; i<4 ; i++){ for (int j=0 ; j<5 ; j++){ Log.d("SimpleArray" ,Character.toString(arr[i][j])); } if (i % 2 == 0 ){ Log.d("SimpleArray" ,"," ); }else { Log.d("SimpleArray" ,"." ); } } } }
1 2 3 4 5 6 7 8 9 10 11 function main ( ){ Java .perform (function ( ){ Java .use ("java.lang.Character" ).toString .overload ('char' ).implementation = function (args1 ){ var result = this .toString (args1) console .log ("args1, result" , args1, result) return result } }) } setImmediate (main)
执行命令:
1 frida -U -f com.v5le0n9.test02 -l test02.js --no-pause
-f:spawn模式,frida 重新打开进程。没有-f默认为attach模式,附加在打开的进程。
hook出不来,咋办?如何主动调用onCreate()
方法?
接下来将一整行hook出来,也就是使用Arrays.toString()
方法那一条语句。
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 package com.v5le0n9.test02;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.util.Log;import java.nio.charset.StandardCharsets;import java.util.Arrays;public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.d("SimpleArray" , "onCreate: SimpleArray" ); char arr[][] = new char [4 ][]; arr[0 ] = new char [] {'春' ,'眠' ,'不' ,'觉' ,'晓' }; arr[1 ] = new char [] {'处' ,'处' ,'闻' ,'啼' ,'鸟' }; arr[2 ] = new char [] {'夜' ,'来' ,'风' ,'雨' ,'声' }; arr[3 ] = new char [] {'花' ,'落' ,'知' ,'多' ,'少' }; Log.d("SimpleArray" , "-----横板-----" ); for (int i=0 ; i<4 ; i++){ Log.d("SimpleArraysToString" , Arrays.toString(arr[i])); for (int j=0 ; j<5 ; j++){ } if (i % 2 == 0 ){ Log.d("SimpleArray" ,"," ); }else { Log.d("SimpleArray" ,"." ); } } } }
1 2 3 4 5 6 7 8 9 10 11 function main ( ){ Java .perform (function ( ){ Java .use ("java.util.Arrays" ).toString .overload ('[C' ).implementation = function (args1 ){ var result = this .toString (args1) console .log ("args1, result" , args1, result) return result } }) } setImmediate (main)
我一直hook不出来,只能盗图了。
要想打印Java对象的内容,需要Google的json包。
1 2 3 4 5 6 7 8 9 10 11 12 function main ( ){ Java .perform (function ( ){ Java .use ("java.util.Arrays" ).toString .overload ('[C' ).implementation = function (args1 ){ var result = this .toString (args1) console .log ("args1, result" , JSON .stringify (args1), result) return result } }) } setImmediate (main)
接下来正式进行构造数组的内容:
1 2 3 4 5 6 7 8 9 10 11 12 function main ( ){ Java .perform (function ( ){ Java .use ("java.util.Arrays" ).toString .overload ('[C' ).implementation = function (args1 ){ var charArray = Java .array ('char' , ['一' ,'去' ,'二' ,'三' ,'里' ]) var result = this .toString (charArray) console .log ("charArray, result" , gson.$new().toJson (charArray), result) return result } }) } setImmediate (main)
修改返回值更简单,因为返回的是一个字符串,直接在返回处写字符串即可。
1 2 3 return Java .use ("java.lang.String" ).$new(Java .array ('char' , ['烟' ,'村' ,'四' ,'五' ,'家' ]))
可以在LogCat中看到返回值已被修改。
继续hookArrays.toString()
方法,只是传入的参数不一样了。
1 2 3 4 5 6 7 8 9 10 11 12 function main ( ){ Java .perform (function ( ){ Java .use ("java.util.Arrays" ).toString .overload ('[B' ).implementation = function (args1 ){ var result = this .toString (args1) console .log ("args1, result" , JSON .stringify (args1), result) return result } }) } setImmediate (main)
3.2 构造对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.v5le0n9.test02;import android.util.Log;public class Water { public static String flow (Water w) { Log.d("20bject" ,"water flow:I'm flowing" ); return "water flow:I'm flowing" ; } public String still (Water w) { Log.d("20bject" ,"water still: still water runs deep!" ); return "water still: still water runs deep!" ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.v5le0n9.test02;import android.util.Log;public class juice extends Water { public String fillEnergy () { Log.d("20bject" , "juice: I'm fillingEnergy" ); return "Juice: I'm fillingEnergy" ; } public static void mian () { Water w1 = new Water (); flow(w1); juice j = new juice (); flow(j); Water w2 = new juice (); ((juice) w2).fillEnergy(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.v5le0n9.test02;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.util.Log;import java.nio.charset.StandardCharsets;import java.util.Arrays;public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); com.v5le0n9.test02.juice.mian(); } }
那js也可以进行强制类型转换,也就是将一个父类对象转化成子类再使用子类方法吗?先找到父类对象的实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function main ( ){ Java .perform (function ( ){ var Waterhandle = null ; Java .choose ("com.v5le0n9.test02.Water" ,{ onMatch :function (instance ){ console .log ("found instance:" ,instance) console .log ("water instance can still:" ,instance.still (instance)) Waterhandle = instance },onComplete :function ( ){console .log ("search completed!" )} }) }) } setImmediate (main)
执行命令:
1 frida -U com.v5le0n9.test02 -l test02.js
乌鱼子,我怎么老hook不上。听说是跟App生命周期相关。
在知道对象的实例后,尝试在js中进行强制类型转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function main ( ){ Java .perform (function ( ){ var Waterhandle = null ; Java .choose ("com.v5le0n9.test02.Water" ,{ onMatch :function (instance ){ console .log ("found instance:" ,instance) console .log ("water instance can still:" ,instance.still (instance)) Waterhandle = instance },onComplete :function ( ){console .log ("search completed!" )} }) var juicehandle = Java .cast (Waterhandle ,Java .use ("com.v5le0n9.test02.juice" )) console .log ("juice fillEnergy method:" , juicehandle.fillEnergy ()) }) } setImmediate (main)
结果说不行。
那子类对象可以转化成父类对象吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function main ( ){ Java .perform (function ( ){ var juicehandle = null ; Java .choose ("com.v5le0n9.test02.juice" ,{ onMatch :function (instance ){ console .log ("found instance:" , instance) console .log ("filling energy:" ,instance.fillEnergy ()) juicehandle = instance },onComplete :function ( ){console .log ("search completed!" )} }) var Waterhandle = Java .cast (juicehandle, Java .use ("com.v5le0n9.test02.Water" )) console .log ("Water invoke still:" , Waterhandle .still (Waterhandle )) }) } setImmediate (main)
综上,在js中父类转子类不行,而子类转父类可行。
3.3 利用接口构造类和方法 1 2 3 4 5 6 package com.v5le0n9.test02;public interface liquid { public String flow () ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.v5le0n9.test02;import android.util.Log;public class milk implements liquid { public String flow () { Log.d("3interface" ,"flowing: interface" ); return "nihao" ; } public static void main () { milk m = new milk (); m.flow(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.v5le0n9.test02;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.util.Log;import java.nio.charset.StandardCharsets;import java.util.Arrays;public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); milk.main(); } }
用js实现milk.java
中的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function main ( ){ Java .perform (function ( ){ var beer = Java .registerClass ({ name : 'com.v5le0n9.test02.beer' , implements : [Java .use ('com.v5le0n9.test02.liquid' )], methods :{ flow :function ( ){ console .log ("look I'm beer!" ) return "taste good!" } } }) console .log ("beer.flow:" ,beer.$new().flow ()) }) } setTimeout (main,2000 )
3.4 枚举 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 package com.v5le0n9.test02;import android.util.Log;enum Signal { GREEN,YELLOW,RED } public class TrafficLight { public static Signal color = Signal.RED; public static void main () { Log.d("4enum" ,"enum" + color); switch (color){ case RED: color = Signal.GREEN; break ; case YELLOW: color = Signal.RED; break ; case GREEN: color = Signal.YELLOW; break ; } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.v5le0n9.test02;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); TrafficLight.main(); } }
枚举可以看作一个类,可以使用js代码方式使用枚举的方法,比如将枚举的值列出用getDeclaringClass()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 function main ( ){ Java .perform (function ( ){ Java .choose ("com.v5le0n9.test02.Signal" ,{ onMatch :function (instance ){ console .log ("found instance:" ,instance) console .log ("invoke getDeclaringClass:" ,instance.getDeclaringClass ()) },onComplete :function ( ){console .log ("Serach completed!" )} }) }) } setTimeout (main,2000 )
当一个类中需要hook的方法较多,则可以使用枚举:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function main ( ){ Java .perform (function ( ){ var class_name = "com.example.androiddemo.Activity.FridaActivity4$InnerClasses" var InnerClass = Java .use (class_name) var all_methods = InnerClass .class .getDeclaredMethods () for (var i=0 ; i<all_methods.length ; i++){ var method = all_methods[i] var subString = method.toString ().substr (method.toString ().indexOf (class_name)+class_name.length +1 ) var finalMethodString = substring.substr (0 ,substring.indexOf ("(" )) console .log (finalMethodString) InnerClass [finalMethodString].implementation = function ( ){return true } } }) }
3.5 Map 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 package com.v5le0n9.test02;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.util.Log;import java.util.HashMap;import java.util.Iterator;import java.util.Map;import java.util.Set;public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); Map<String,String> mapv5le0n9 = new HashMap <>(); mapv5le0n9.put("ISBN 978-7-5677-8742-1" ,"Android项目开发实战入门" ); mapv5le0n9.put("ISBN 978-7-5677-8741-4" ,"C语言项目开发实战入门" ); mapv5le0n9.put("ISBN 978-7-5677-9897-1" ,"PHP项目开发实战入门" ); mapv5le0n9.put("ISBN 978-7-5677-8748-7" ,"Java项目开发实战入门" ); Set<String> set = mapv5le0n9.keySet(); Iterator<String> it = set.iterator(); Log.d("5map" ,"key:" ); while (it.hasNext()){ try { Thread.sleep(2000 ); Log.d("5map" ,it.next()+" " ); } catch (InterruptedException e) { e.printStackTrace(); } } Log.d("5map" ,"key toString" + mapv5le0n9.toString()); } }
使用js代码查看Map的toString()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function hashmap888 ( ){ Java .perform (function ( ){ Java .choose ("java.util.HashMap" ,{ onMatch :function (instance ){ if (instance.toString ().indexOf ("ISBN" )!=-1 ){ console .log ("found HashMap" ,instance) console .log ("HashMap toString" , instance.toString ()) } },onComplete :function ( ){console .log ("Search Completed!" )} }) }) } setTimeout (hashmap888,2000 )
尝试hook Map的put()
方法:
1 2 3 4 5 6 7 8 9 10 11 function hashmap888 ( ){ Java .perform (function ( ){ Java .use ("java.util.HashMap" ).put .implementation = function (args1, args2 ){ var result = this .put (args1, args2) console .log ("args1, args2, result" , args1, args2, result) return result } }) } setImmediate (hashmap888)
4. 一些查漏补缺 4.1 利用adb在文本框输入内容 通过hook或者算法得出来的结果不能复制到Android设备的文本框中,可以使用adb传入结果。点击需要填入结果的文本框,通过以下命令传到文本框中:
1 adb shell input text <结果>
4.2 动静态处理成员变量 用js修改静态成员变量:
1 Java .use ("<包名+类名>" ).<静态成员变量>.value = <想要修改的值>
用js修改动态成员变量:
1 2 3 4 5 6 7 8 9 Java .choose ("<包名+类名>" ,{ onMatch :function (instance ){ console .log ("found instance" , instance) instance.<动态成员变量>.value = <想要修改的值> instance._ <动态成员变量>.value = <想要修改的值> },onComplete :function ( ){} })
5. RPC远程调用 5.1 远程调用 在主动调用成功之后才能用RPC。拿test01做例子,我们将没有用到的secret()
方法进行主动调用:
1 2 3 4 5 6 7 8 9 10 11 12 function invoke ( ){ Java .perform (function ( ){ Java .choose ("com.v5le0n9.test01.MainActivity" ,{ onMatch :function (instance ){ console .log ("found instance:" ,instance) console .log ("found instance:" ,instance.secret ()) },onComplete :function ( ){} }) }) } setTimeout (invoke,2000 )
没有问题,接下来就使用RPC来远程调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function invoke ( ){ Java .perform (function ( ){ Java .choose ("com.v5le0n9.test01.MainActivity" ,{ onMatch :function (instance ){ console .log ("found instance:" ,instance) console .log ("found instance:" ,instance.secret ()) },onComplete :function ( ){} }) }) } rpc.exports = { invokefunc :invoke }
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 import timeimport fridadef my_message_handler (message,payload ): print (message) print (payload) device = frida.get_usb_device() session = device.attach("com.v5le0n9.test01" ) with open ("rpc.js" ) as f: script = session.create_script(f.read()) script.on("message" ,my_message_handler) script.load() command = "" while True : command = input ("Enter Command:" ) if command == "1" : break elif command == "2" : script.exports.invokefunc()
5.2 多主机多手机多端口混连 5.2.1 多主机 现在的模拟器大多都可以实现多主机连接,如果真机中ADB WiFi也可以实现多主机连接,但貌似用数据线连接只能连一台主机。也就是说模拟器可以跟物理机连的同时也可以跟虚拟机连。
将虚拟机与模拟器断开连接:
1 adb disconnect 192.168.24.104:5555
5.2.2 多端口 通过objection可以看到Android设备与主机连接的默认端口为27042。
更改端口为9999,即Android设备监听9999端口判断是否有主机连接:
1 ./frida-server-12.8.0-android-x86 -l 0.0.0.0:9999
此时27042端口不管用了,而是用的是9999端口。这个远程连接也提供了很多API给我们查看设备信息,比如枚举应用程序。
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 import timeimport fridadef my_message_handler (message,payload ): print (message) print (payload) device = frida.get_device_manager().add_remote_device('192.168.24.104:9999' ) print (device.enumerate_applications())session = device.attach("com.v5le0n9.test01" ) with open ("rpc.js" ) as f: script = session.create_script(f.read()) script.on("message" ,my_message_handler) script.load() command = "" while True : command = input ("Enter Command:" ) if command == "1" : break elif command == "2" : script.exports.invokefunc()
5.2.3 多手机 主机可以连接多台手机,使用adb连接即可。不使用adb连接也可以用上述远程调用的方法连接手机。
1 2 3 device = frida.get_device_manager ().add_remote_device ('192.168.24.104:9999' ) device = frida.get_device_manager ().add_remote_device ('192.168.24.103:9999' ) device = frida.get_device_manager ().add_remote_device ('192.168.24.102:6666' )
5.3 互联互通,动态修改 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 54 55 56 57 58 59 60 61 <?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" > <EditText android:id ="@+id/editTextTextPersonName" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:ems ="10" android:hint ="Name" android:inputType ="textPersonName" android:minHeight ="48dp" app:layout_constraintBottom_toBottomOf ="parent" app:layout_constraintEnd_toEndOf ="parent" app:layout_constraintHorizontal_bias ="0.497" app:layout_constraintStart_toStartOf ="parent" app:layout_constraintTop_toTopOf ="parent" app:layout_constraintVertical_bias ="0.196" /> <EditText android:id ="@+id/editTextTextPersonName2" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:ems ="10" android:hint ="Password" android:inputType ="textPersonName" android:minHeight ="48dp" app:layout_constraintBottom_toBottomOf ="parent" app:layout_constraintEnd_toEndOf ="parent" app:layout_constraintHorizontal_bias ="0.497" app:layout_constraintStart_toStartOf ="parent" app:layout_constraintTop_toTopOf ="parent" app:layout_constraintVertical_bias ="0.309" /> <Button android:id ="@+id/button" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:text ="Login" app:layout_constraintBottom_toBottomOf ="parent" app:layout_constraintEnd_toEndOf ="parent" app:layout_constraintHorizontal_bias ="0.498" app:layout_constraintStart_toStartOf ="parent" app:layout_constraintTop_toTopOf ="parent" app:layout_constraintVertical_bias ="0.653" /> <TextView android:id ="@+id/textView3" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:text ="Please input your username and password" app:layout_constraintBottom_toBottomOf ="parent" app:layout_constraintEnd_toEndOf ="parent" app:layout_constraintStart_toStartOf ="parent" app:layout_constraintTop_toTopOf ="parent" /> </androidx.constraintlayout.widget.ConstraintLayout >
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 package com.v5le0n9.test03;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.util.Base64;import android.view.View;import android.widget.EditText;import android.widget.TextView;public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); EditText username = (EditText) this .findViewById(R.id.editTextTextPersonName); EditText password = (EditText) this .findViewById(R.id.editTextTextPersonName2); TextView message = (TextView) findViewById(R.id.textView3); this .findViewById(R.id.button).setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { if (username.getText().toString().compareTo("admin" ) == 0 ){ message.setText("You can not login as admin" ); return ; } message.setText("Sending to the server:" + Base64.encodeToString((username.getText().toString() + ":" + password.getText().toString()).getBytes(),Base64.DEFAULT)); } }); } }
这个APK的逻辑是输入用户名和密码,用户名不能是admin,输入的用户名和密码通过“:”连接进行Base64加密。
最后一条语句是我们hook的目标。我们可以hook setText()
方法,转到定义可以看到它有很多重载。
那么可以用objection来看看它到底走了哪个重载。将该方法的所有重载都hook上:
1 2 android hooking watch class_method android.widget.TextView.setText --dump-args -- dump-backtrace --dump-return
输入用户名和密码查看哪个重载被调用。是java.lang.CharSequence。
编写hook代码,单纯打印setText()
方法传入的参数及返回值:
1 2 3 4 5 6 7 8 9 10 11 function main ( ){ Java .perform (function ( ){ Java .use ("android.widget.TextView" ).setText .overload ("java.lang.CharSequence" ).implementation = function (x ){ var result = this .setText (x) console .log ("x.toString(), result" , x.toString (), result) return result } }) } setImmediate (main)
执行命令:
1 frida -U com.v5le0n9.test03 -l test03.js
输入用户名与密码,点击Login,成功捕获到传入setText()
方法的参数。
hook没问题后,开始利用RPC动态修改参数的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Java .perform (function ( ){ Java .use ("android.widget.TextView" ).setText .overload ("java.lang.CharSequence" ).implementation = function (x ){ var string_to_send_x = x.toString () var string_to_recv send (string_to_send_x) recv (function (received_json_objection ){ string_to_recv = received_json_objection.my_data console .log ("string_to_recv:" + string_to_recv) }).wait () var javaStringTosend = Java .use ("java.lang.String" ).$new(string_to_recv) var result = this .setText (javaStringTosend) return result } })
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 import fridaimport base64def my_message_handler (message,payload ): print (message) print (payload) if message["type" ] == "send" : print (message["payload" ]) data = message["payload" ].split(":" )[1 ].strip() print ("message:" , message) data = str (base64.b64decode(data)) print ("data:" ,data) usr,pw = data.split(":" ) print ("pw" ,pw) data = str (base64.b64encode(("admin" + ":" + pw).encode())) print ("encode data" ,data) script.post({"my_data" :data}) print ("Modified data sent" ) device = frida.get_usb_device() session = device.attach("com.v5le0n9.test03" ) with open ("test03.js" ) as f: script = session.create_script(f.read()) script.on("message" ,my_message_handler) script.load() input ()
6. RPC开到公网 使用5.2.2的例子,主动调用secret()
函数。如果我们进行一系列端口转发操作,将手机的IP地址通过一系列的映射,映射到云服务器(VPS)上,是什么样的结果呢?
常用的端口转发工具nps、frp,frp稳定性高于nps,但nps有Web界面,方便调试。
搭建nps。在主机上安装服务端,手机上安装客户端。由于需要云服务器,先跳过吧。
7. 综合案例 使用的案例是kgb-messenger 这个开源项目的APK。
将APK安装到手机上,开启frida-server,将APK或DEX文件载入jadx查看APK的包名,使用objection让它跑起来:
1 objection -g com.tlamb96.spetsnazmessenger explore
查看该APK有哪些activities:
1 android hooking list activities
尝试是否可以直接某个activity:
1 2 3 android intent launch_activity com.tlamb96.kgbmessenger.MainActivity android intent launch_activity com.tlamb96.kgbmessenger.MessengerActivity android intent launch_activity com.tlamb96.kgbmessenger.LoginActivity
发现MainActivity不行,说只能运行在俄罗斯的设备上。但MessengerActivity和LoginActivity是可以直接intent进去的,当然可能是这个APK防护不到位才给我们钻了空子。
那我们先把MainActivity搞定,如何才能绕过这个错误框。在jadx中查找错误信息,发现这个错误信息就在MainActivity中。
上面流程太清楚了,只要hook getProperty()
方法修改它的返回值为“Russia”,和修改getenv()
方法的返回值让它为下图的值即可。
首先要找到System的包名,右键发现走不到它的定义处。可以看Smali代码,搜索System,发现找到它在java.lang包中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function main ( ){ Java .perform (function ( ){ Java .use ("java.lang.System" ).getProperty .overload ('java.lang.String' ).implementation = function (x ){ var result = this .getProperty (x) console .log ("getProperty,result" ,x,result) return result } Java .use ("java.lang.System" ).getenv .overload ('java.lang.String' ).implementation = function (x ){ var result = this .getenv (x) console .log ("getenv,result" ,x,result) return result } }) } setImmediate (main)
由于是在App启动一开始就执行了这两个方法,所以使用-f选项,执行命令:
1 frida -U -f com.tlamb96.spetsnazmessenger -l kgb.js --no-pause
getProperty()
方法竟然没有显示返回值,getenv()
方法返回值为空。但我们从Smali中分明看到两个方法的返回值都是String类型。所以不用管那么多,我们直接修改它们的返回值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function main ( ){ Java .perform (function ( ){ Java .use ("java.lang.System" ).getProperty .overload ('java.lang.String' ).implementation = function (x ){ var result = this .getProperty (x) console .log ("getProperty,result" ,x,result) return Java .use ("java.lang.String" ).$new("Russia" ) } Java .use ("java.lang.System" ).getenv .overload ('java.lang.String' ).implementation = function (x ){ var result = this .getenv (x) console .log ("getenv,result" ,x,result) return Java .use ("java.lang.String" ).$new("RkxBR3s1N0VSTDFOR180UkNIM1J9Cg==" ) } }) } setImmediate (main)
这样就成功跳转到LoginActivity。输入用户名和密码,显示“User not recognized.”。同样搜索关键字,发现在LoginActivity中,查看Java代码,发现用户名,还有密码经过加密后的字符串都在strings.xml
中,直接找到即可。
用户名为:codenameduchess
密码经过加密后为:84e343a0486ff05530df6c705c8bb4
用户名可以直接输入或hook setText()
方法;由于我们不知道传入的密码,只知道最终的加密结果,所以我们直接hook j()
方法。而j()
方法的返回值是个boolean值,所以返回true万事大吉。
直接输入的话,选中文本框,输入以下命令:
1 bullhead:/ $ input text "codenameduchess"
当我尝试hook setText()
方法时,出现错误:
那我们就增加一个参数,这个参数应该是辨认TextView的id值,确保我们在众多文本框中找到特定id值的文本框。这个参数我们不需要修改。
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 function main ( ){ Java .perform (function ( ){ Java .use ("java.lang.System" ).getProperty .overload ('java.lang.String' ).implementation = function (x ){ var result = this .getProperty (x) console .log ("getProperty,result" ,x,result) return Java .use ("java.lang.String" ).$new("Russia" ) } Java .use ("java.lang.System" ).getenv .overload ('java.lang.String' ).implementation = function (x ){ var result = this .getenv (x) console .log ("getenv,result" ,x,result) return Java .use ("java.lang.String" ).$new("RkxBR3s1N0VSTDFOR180UkNIM1J9Cg==" ) } }) } function login ( ){ Java .perform (function ( ){ Java .use ("android.widget.EditText" ).setText .overload ('java.lang.CharSequence' , 'android.widget.TextView$BufferType' ).implementation = function (x,type ){ var result = this .setText (Java .use ("java.lang.String" ).$new("codenameduchess" ),type) return result } Java .use ("com.tlamb96.kgbmessenger.LoginActivity" ).j .implementation = function ( ){ return true } }) } setImmediate (login)
成功登录。是一个群聊界面,搜索群聊的信息查看在哪个Activity,在MessengerActivity中。
从流程来看,我们输入的信息经过MessengerActivity.a()
方法加密等于p,经过MessengerActivity.b()
方法加密等于r。我们直接hook a()
和b()
方法,修改它们的返回值。
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 function main ( ){ Java .perform (function ( ){ Java .use ("java.lang.System" ).getProperty .overload ('java.lang.String' ).implementation = function (x ){ var result = this .getProperty (x) console .log ("getProperty,result" ,x,result) return Java .use ("java.lang.String" ).$new("Russia" ) } Java .use ("java.lang.System" ).getenv .overload ('java.lang.String' ).implementation = function (x ){ var result = this .getenv (x) console .log ("getenv,result" ,x,result) return Java .use ("java.lang.String" ).$new("RkxBR3s1N0VSTDFOR180UkNIM1J9Cg==" ) } }) } function login ( ){ Java .perform (function ( ){ Java .use ("android.widget.EditText" ).setText .overload ('java.lang.CharSequence' , 'android.widget.TextView$BufferType' ).implementation = function (x,type ){ var result = this .setText (Java .use ("java.lang.String" ).$new("codenameduchess" ),type) return result } Java .use ("com.tlamb96.kgbmessenger.LoginActivity" ).j .implementation = function ( ){ return true } }) } function message ( ){ Java .perform (function ( ){ Java .use ("com.tlamb96.kgbmessenger.MessengerActivity" ).a .overload ('java.lang.String' ).implementation = function (x ){ var result = this .a (x) console .log ("a:" ,x) console .log ("result:" ,result) return result } Java .use ("com.tlamb96.kgbmessenger.MessengerActivity" ).b .overload ('java.lang.String' ).implementation = function (x ){ var result = this .b (x) console .log ("b:" ,x) console .log ("result:" ,result) return result } }) } setImmediate (message)
吐了呀姐妹,怎么一直崩溃。a()
可以成功hook,但b()
方法一直崩溃,这个错误提示看得我云里雾里。
现在这种情况只能逆向算法了。由于a()
和b()
传入的都是我们输入的信息,a()
算法比b()
算法要简单很多,如果输入的信息可以通过两个算法,那就可以免除逆向b()
算法了,先来破解a()
算法。
a()
算法的意思是将字符串转化为字符数组,第一个字符是最后一个字符异或’2’,最后一个字符是第一个字符异或’A’,第二个字符是倒数第二个字符异或’2’,倒数第二个字符是第二个字符异或’A’,以此类推。
可用Java写脚本,直接复制算法修改修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class reverseA { public static String decode_p () { String p = "V@]EAASB\u0012WZF\u0012e,a$7(&am2(3.\u0003" ; String result = reverse_a(p); return result; } private static String reverse_a (String str) { char [] charArray = str.toCharArray(); for (int i = 0 ; i < charArray.length / 2 ; i++) { char c = charArray[i]; charArray[i] = (char ) (charArray[(charArray.length - i) - 1 ] ^ 'A' ); charArray[(charArray.length - i) - 1 ] = (char ) (c ^ '2' ); } return new String (charArray); } }
执行命令:
1 2 3 4 5 //编译成.class文件 javac reverseA.java //编译成DEX文件 java -jar dx.jar --dex --output=reverseA.dex reverseA.class
将reverseA.dex
文件push到手机的/data/local/tmp
目录,授予777权限,使用js代码来加载它。
1 2 3 4 5 6 7 8 function message ( ){ Java .perform (function ( ){ Java .openClassFile ("/data/local/tmp/reverseA.dex" ).load () const ra = Java .use ("reverseA" ) console .log ("reverseA result:" , ra.decode_p ()) }) } setImmediate (message)
结果发现群聊并没有第二条消息出来,也就是a()
与b()
传入的消息是不一样的。所以要接着逆向b()
算法。
第一种方法:
分析代码可知变量r有很多不可见的字符,我们可以把变量r打印出来,使它成为可见字符:
1 2 3 4 5 6 7 8 9 10 11 12 public class reverseB { public static String r_to_hex () { String r = "\u0000dslp}oQ\u0000 dks$|M\u0000h +AYQg\u0000P*!M$gQ\u0000" ; byte [] bytes = r.getBytes(); String result = "" ; for (int i=0 ; i<bytes.length; i++){ result += String.format("%02x" ,bytes[i]); } return result; } }
1 2 3 4 5 6 7 8 9 function message ( ){ Java .perform (function ( ){ Java .openClassFile ("/data/local/tmp/reverseB.dex" ).load () const rb = Java .use ("reverseB" ) console .log ("reverseB result:" , rb.r_to_hex ()) }) } setImmediate (message)
由于b()
算法有按位运算操作,确实有点麻烦,所以我们利用Z3库来求解这个问题。
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 from z3 import *from binascii import b2a_hex, a2b_hexs = Solver() r = "0064736c707d6f510020646b73247c4d0068202b4159516700502a214d24675100" r_result = bytearray (a2b_hex(r)) print (r_result)for i in range (int (len (r_result)/2 )): c = r_result[i] r_result[i] = r_result[len (r_result)-i-1 ] r_result[len (r_result)-i-1 ] = c print (b2a_hex(r_result))x = [BitVec("x%s" % i,32 ) for i in range (len (r_result))] for i in range (len (r_result)): c = r_result[i] print (i,hex (c)) s.add(((x[i] >> (i % 8 )) ^ x[i]) == r_result[i]) if (s.check() == sat): model = s.model() print (model) flag = "" for i in range (len (r_result)): if (model[x[i]] != None ): flag += chr (model[x[i]].as_long().real) else : flag += " " print ('"' + flag + '"' ) print (len (flag),len (r_result))
但是我用这个脚本时失败了,不知道什么原因。只能借用大佬的截图。
第二种方法:
后来看其它博客说可以用暴力枚举每个字符的方法解出明文:
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 public class reverseB { public static String search () { String characterset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!\"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r" ; char [] charactersetArray = characterset.toCharArray(); String ciphertext = "\000dslp}oQ\000 dks$|M\000h +AYQg\000P*!M$gQ\000" ; char [] charArray = ciphertext.toCharArray(); for (int i2 = 0 ; i2 < charArray.length / 2 ; i2++) { char c = charArray[i2]; charArray[i2] = charArray[(charArray.length - i2) - 1 ]; charArray[(charArray.length - i2) - 1 ] = c; } String plaintext="" ; for (int i = 0 ; i < charArray.length; i++) { for (int j = 0 ; j < charactersetArray.length ; j++ ){ char c = charactersetArray[j]; char result = (char )(char )((c >> (i % 8 ) )^ c); if (result == charArray[i]){ plaintext+=charactersetArray[j]; break ; } } } return plaintext; } }
同样将JAVA文件转换为DEX文件,push到手机上授予权限,执行js代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 function message ( ){ Java .perform (function ( ){ Java .openClassFile ("/data/local/tmp/reverseA.dex" ).load () const ra = Java .use ("reverseA" ) console .log ("reverseA result:" , ra.decode_p ()) }) Java .perform (function ( ){ Java .openClassFile ("/data/local/tmp/reverseB.dex" ).load () const rb = Java .use ("reverseB" ) console .log ("reverseB result:" , rb.search ()) }) } setImmediate (message)
哈哈哈发现暴力枚举也不太准确,flag里面有非法字符。应该用Z3是正确解法,但看不懂啊啊啊!好难!
8. frida Native hook 以上都是frida在Java层实现的hook操作,而到了JNI层,frida又是如何hook so库中的函数呢?
8.1 Native hook基本操作 有时候,当我们用IDA查看so库中的导出函数时(静态反编译),可能会hook不成功,为什么呢?
这个so库真的有被加载进内存吗?
so库中的导出函数真的有被加载进内存吗?
我们找的导出函数真的是我们想要找的导出函数吗?
而这几个问题,完全可以通过objection来查看内存中的信息,验证是否是我们想要找的so库和导出函数。
用objection查看内存中加载的库:
当然也可以使用js代码来查看so库在内存中的地址:
1 2 3 4 5 function hook_nativelib ( ){ var native_lib_addr = Module .findBaseAddress ("libnative-lib.so" ) console .log ("native_lib_addr => " , native_lib_addr) } setImmediate (hook_nativelib)
获得该so库在内存中的地址后,再找到导出函数的名字。同样可以用objection查看加载的库的导出函数:
1 memory list exports libnative-lib.so
可以使用js代码验证地址是否一致:
1 2 3 4 5 6 7 8 function hook_nativelib ( ){ var native_lib_addr = Module .findBaseAddress ("libnative-lib.so" ) console .log ("native_lib_addr => " , native_lib_addr) var myfirstjniJNI = Module .findExportByName ("libnative-lib.so" , "Java_com_example_demoso1_MainActivity_myfirstjniJNI" ) console .log ("myfirstjniJNI addr =>" , myfirstjniJNI) } setImmediate (hook_nativelib)
接下来就是hook native,单纯打印该Native函数的参数和返回值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function hook_nativelib ( ){ var native_lib_addr = Module .findBaseAddress ("libnative-lib.so" ) console .log ("native_lib_addr => " , native_lib_addr) var myfirstjniJNI = Module .findExportByName ("libnative-lib.so" , "Java_com_example_demoso1_MainActivity_myfirstjniJNI" ) console .log ("myfirstjniJNI addr => " , myfirstjniJNI) Interceptor .attach (myfirstjniJNI,{ onEnter :function (args ){ console .log ("Interceptor.attach myfirstjniJNI args:" , args[0 ], args[1 ], args[2 ]) console .log ("args2 jstring is " ,Java .vm .getEnv ().getStringUTFChars (args[2 ],null ).readCString ()) },onLeave :function (retval ){ console .log ("Interceptor.attach myfirstjniJNI retval => " ,retval) console .log ("retval jstring is " ,Java .vm .getEnv ().getStringUTFChars (retval,null ).readCString ()) } }) } setImmediate (hook_nativelib)
确认可以打印并没有错误后,再进行修改参数和返回值的操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function hook_nativelib ( ){ var native_lib_addr = Module .findBaseAddress ("libnative-lib.so" ) console .log ("native_lib_addr => " , native_lib_addr) var myfirstjniJNI = Module .findExportByName ("libnative-lib.so" , "Java_com_example_demoso1_MainActivity_myfirstjniJNI" ) console .log ("myfirstjniJNI addr => " , myfirstjniJNI) Interceptor .attach (myfirstjniJNI,{ onEnter :function (args ){ console .log ("Interceptor.attach myfirstjniJNI args:" , args[0 ], args[1 ], args[2 ]) console .log ("args2 jstring is " ,Java .vm .getEnv ().getStringUTFChars (args[2 ],null ).readCString ()) var newArgs2 = Java .vm .getEnv ().newStringUtf ("new Args2 from frida" ) args[2 ] = newArgs2 },onLeave :function (retval ){ console .log ("Interceptor.attach myfirstjniJNI retval => " ,retval) console .log ("retval jstring is " ,Java .vm .getEnv ().getStringUTFChars (retval,null ).readCString ()) var newRetval = Java .vm .getEnv ().newStringUtf ("new Retval from frida" ) retval.replace (newRetval) } }) } setImmediate (hook_nativelib)
除了基本操作,还可以让这个Native函数不运行,基于主动调用。
8.2 主动调用 当我们想找一个函数却发现内存中没有类似于“Java_com_example_demoso1_MainActivity_v5add”的Native函数名时,不一定函数没有加载进内存,而是改了个名字。比如_Z5v5addii
,这时候就看不出来它是哪个函数,可以拿到 http://demangler.com/ 去demangle一下,就得到函数原本的定义。
比如下面这个Native函数,对它进行主动调用。
1 2 3 4 5 6 7 int v5add (int x, int y) { int i; for (i=0 ; i<x; i++){ i = i + y; } return i; }
还是先要找到so库和Native函数地址,再对函数进行主动调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function hookandinvoke_add ( ){ var native_lib_addr = Module .findBaseAddress ("libnative-lib.so" ) console .log ("native_lib_addr => " , native_lib_addr) var v5add_addr = Module .findExportByName ("libnative-lib.so" , "_Z5v5addii" ) console .log ("v5add addr => " , v5add_addr) Interceptor .attach (v5add_addr,{ onEnter :function (args ){ console .log ("x => " , args[0 ], "y => " , args[1 ]) },onLeave :function (retval ){ console .log ("retval => " ,retval) } }) var v5add = new NativeFunction (v5add_addr,"int" ,["int" ,"int" ]) var v5add_result = v5add (50 ,1 ) console .log ("invoke result => " ,v5add_result) } setImmediate (hookandinvoke_add)
上面是一个简单的主动调用并修改参数,现在对myfirstjniJNI()
主动调用试试。
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 function hook_nativelib ( ){ var native_lib_addr = Module .findBaseAddress ("libnative-lib.so" ) console .log ("native_lib_addr => " , native_lib_addr) var myfirstjniJNI = Module .findExportByName ("libnative-lib.so" , "Java_com_example_demoso1_MainActivity_myfirstjniJNI" ) console .log ("myfirstjniJNI addr => " , myfirstjniJNI) var myfirstjniJNI_invoke = new NativeFunction (myfirstjniJNI,"pointer" ,["pointer" ,"pointer" ,"pointer" ]) Interceptor .attach (myfirstjniJNI,{ onEnter :function (args ){ console .log ("Interceptor.attach myfirstjniJNI args:" , args[0 ], args[1 ], args[2 ]) console .log ("args2 jstring is " ,Java .vm .getEnv ().getStringUTFChars (args[2 ],null ).readCString ()) console .log ("myfirstjniJNI_invoke result => " ,Java .vm .getEnv ().getStringUTFChars (myfirstjniJNI_invoke (args[0 ], args[1 ], args[2 ]),null ).readCString ()) var newArgs2 = Java .vm .getEnv ().newStringUtf ("new Args2 from frida" ) args[2 ] = newArgs2 },onLeave :function (retval ){ console .log ("Interceptor.attach myfirstjniJNI retval => " ,retval) console .log ("retval jstring is " ,Java .vm .getEnv ().getStringUTFChars (retval,null ).readCString ()) var newRetval = Java .vm .getEnv ().newStringUtf ("new Retval from frida" ) retval.replace (newRetval) } }) } setImmediate (hook_nativelib)
8.3 替换值 修改参数和返回值也可以用Interceptor.replace()
方法,但比较少用,一般Interceptor.attach()
就够用了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function hook_replace ( ){ var native_lib_addr = Module .findBaseAddress ("libnative-lib.so" ) console .log ("native_lib_addr => " , native_lib_addr) var myfirstjniJNI = Module .findExportByName ("libnative-lib.so" , "Java_com_example_demoso1_MainActivity_myfirstjniJNI" ) console .log ("myfirstjniJNI addr => " , myfirstjniJNI) var myfirstjniJNI_invoke = new NativeFunction (myfirstjniJNI,"pointer" ,["pointer" ,"pointer" ,"pointer" ]) Interceptor .replace (myfirstjniJNI,new NativeCallback (function (args0, args1, args2 ){ console .log ("Interceptor.replace myfirstjniJNI args:" , args0, args1, args2) return Java .vm .getEnv ().newStringUtf ("new Retval from frida" ) },"pointer" ,["pointer" ,"pointer" ,"pointer" ])) } setImmediate (hook_replace)
8.4 调用栈 在add()
的Native函数中添加调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function hookandinvoke_add ( ){ var native_lib_addr = Module .findBaseAddress ("libnative-lib.so" ) console .log ("native_lib_addr => " , native_lib_addr) var v5add_addr = Module .findExportByName ("libnative-lib.so" , "_Z5v5addii" ) console .log ("v5add addr => " , v5add_addr) Interceptor .attach (v5add_addr,{ onEnter :function (args ){ console .log ("x => " , args[0 ], "y => " , args[1 ]) console .log ("CCCryptorCreate called from:\n" + Thread .backtrace (this .context , Backtracer .ACCURATE ).map (DebugSymbol .fromAddress ).join ('\n' ) + '\n' ) },onLeave :function (retval ){ console .log ("retval => " ,retval) } }) var v5add = new NativeFunction (v5add_addr,"int" ,["int" ,"int" ]) var v5add_result = v5add (50 ,1 ) console .log ("invoke result => " ,v5add_result) } setImmediate (hookandinvoke_add)
8.5 枚举 想要在众多so中找到某个Native函数,如果这个APK没有做动态注册(JNI_Onload),我们可以使用枚举将函数打印出来,或导出到文件对应去找。
1 2 3 4 5 6 7 8 9 10 11 function EnumerateAllExports ( ){ var modules = Process .enumerateModules () for (var i=0 ; i<modules.length ; i++){ var module = modules[i] var module_name = modules[i].name var exports = module .enumerateExports () console .log ("module_name => " ,module_name,"module.enumerateExports => " ,JSON .stringify (exports )) } } setImmediate (EnumerateAllExports )
8.6 Process、Thread、Module、Memory 这些用法可直接在Frida指南 JavaScript API 中找到。
打开某一个应用程序,开启frida-server,在Kali命令窗口中输入
即可attach上该应用程序。
8.6.1 Process 可以直接在命令窗口输入。
8.6.2 Module 也可以写js代码。比如枚举导入表函数。
1 2 3 4 5 6 function MODULE ( ){ var native_lib_addr = Process .findModuleByAddress (Module .findBaseAddress ("libnative-lib.so" )) console .log ("native_lib_addr => " , JSON .stringify (native_lib_addr)) console .log ("enumerateImports => " , JSON .stringify (native_lib_addr.enumerateImports ())) } setImmediate (MODULE )
9. 系统框架层Native hook 9.1 JNI框架层的hook利用 9.1.1 找到函数的地址 假如我们想要hook GetStrinUTFChars()
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function hook_JNI ( ){ var GetStringUTFChars _addr = null var symbols = Process .findModuleByName ("libart.so" ).enumerateSymbols () for (var i=0 ; i<symbols.length ; i++){ var symbol = symbols[i].name if ((symbol.indexOf ("CheckJNI" ) == -1 ) && (symbol.indexOf ("JNI" ) >= 0 )){ if (symbol.indexOf ("GetStrinUTFChars" )>=0 ){ console .log ("finally found GetStrinUTFChars name:" , symbol) GetStringUTFChars _addr = symbols[i].address console .log ("finally found GetStrinUTFChars address:" , symbol.address ) } } } } setImmediate (hook_JNI)
9.1.2 查看调用栈、参数和返回值 找到函数的地址后attach这个函数,查看参数和返回值:
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 function hook_JNI ( ){ var GetStringUTFChars _addr = null var symbols = Process .findModuleByName ("libart.so" ).enumerateSymbols () for (var i=0 ; i<symbols.length ; i++){ var symbol = symbols[i].name if ((symbol.indexOf ("CheckJNI" ) == -1 ) && (symbol.indexOf ("JNI" ) >= 0 )){ if (symbol.indexOf ("GetStrinUTFChars" )>=0 ){ console .log ("finally found GetStrinUTFChars name:" , symbol) GetStringUTFChars _addr = symbols[i].address console .log ("finally found GetStrinUTFChars address:" , GetStringUTFChars _addr) } } } Interceptor .attach (GetStringUTFChars _addr,{ onEnter :function (args ){ console .log ("GetStringUTFChars(_JNIEnv*, _jstring*, unsigned char*) => " , args[0 ],Java .vm .getEnv ().getStringUTFChars (args[1 ],null ).readCString (),args[1 ],args[2 ]) },onLeave :function (retval ){ console .log ("retval => " ,retval.readCString ()) } }) } setImmediate (hook_JNI)
9.1.3 修改参数和返回值 如果要将参数或返回值替换,可以直接在attach中new一个,也可以使用15.1.7的知识,使用replace来替换参数或返回值。首先还是把函数的参数和返回值打印出来,确保无误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function replace_JNI ( ){ var NewStringUTF _addr = null var symbols = Process .findModuleByName ("libart.so" ).enumerateSymbols () for (var i=0 ; i<symbols.length ; i++){ var symbol = symbols[i].name if ((symbol.indexOf ("CheckJNI" ) == -1 ) && (symbol.indexOf ("JNI" ) >= 0 )){ if (symbol.indexOf ("NewStrinUTF" )>=0 ){ console .log ("finally found NewStrinUTF name:" , symbol) NewStringUTF _addr = symbols[i].address console .log ("finally found NewStrinUTF address:" , NewStringUTF _addr) } } } var NewStringUTF = new NativeFunction (NewStringUTF _addr,"pointer" ,["pointer" ,"pointer" ]) Interceptor .replace (NewStringUTF _addr,new NativeCallback (function (args1,args2 ){ console .log ("args1,args2 => " ,args1, args2) NewStringUTF (args1,args2) },"pointer" ,["pointer" ,"pointer" ])) } setImmediate (replace_JNI)
查看调用栈,发现有其它程序的NewStringUTF()
输出,此时如果直接修改参数或返回值可能会导致其他程序出现错误,但目标程序的目标函数的参数确实是被替换掉了。
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 function replace_JNI ( ){ var NewStringUTF _addr = null var symbols = Process .findModuleByName ("libart.so" ).enumerateSymbols () for (var i=0 ; i<symbols.length ; i++){ var symbol = symbols[i].name if ((symbol.indexOf ("CheckJNI" ) == -1 ) && (symbol.indexOf ("JNI" ) >= 0 )){ if (symbol.indexOf ("NewStrinUTF" )>=0 ){ console .log ("finally found NewStrinUTF name:" , symbol) NewStringUTF _addr = symbols[i].address console .log ("finally found NewStrinUTF address:" , NewStringUTF _addr) } } } var NewStringUTF = new NativeFunction (NewStringUTF _addr,"pointer" ,["pointer" ,"pointer" ]) Interceptor .replace (NewStringUTF _addr,new NativeCallback (function (args1,args2 ){ console .log ("args1,args2 => " ,args1, args2) console .log ("args2 => " ,args2.readCString ()) var newArgs2 = Memory .allocUtfString ("newArgs2" ) var result = NewStringUTF (args1,newArgs2) return result },"pointer" ,["pointer" ,"pointer" ])) } setImmediate (replace_JNI)
9.1.4 hook动态注册 动态注册是在App启动时进行的,所以要在App启动时进行hook。(-f参数)
JNI_Onload()
中的主要实现是RegisterNatives()
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function hook_RegisterNatives ( ){ var RegisterNatives _addr = null var symbols = Process .findModuleByName ("libart.so" ).enumerateSymbols () for (var i=0 ; i<symbols.length ; i++){ var symbol = symbols[i].name if ((symbol.indexOf ("CheckJNI" ) == -1 ) && (symbol.indexOf ("JNI" ) >= 0 )){ if (symbol.indexOf ("RegisterNatives" )>=0 ){ console .log ("finally found RegisterNatives name:" , symbol) RegisterNatives _addr = symbols[i].address console .log ("finally found RegisterNatives address:" , RegisterNatives _addr) } } } } setImmediate (hook_RegisterNatives)
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 function hook_RegisterNatives ( ){ var RegisterNatives _addr = null var symbols = Process .findModuleByName ("libart.so" ).enumerateSymbols () for (var i=0 ; i<symbols.length ; i++){ var symbol = symbols[i].name if ((symbol.indexOf ("CheckJNI" ) == -1 ) && (symbol.indexOf ("JNI" ) >= 0 )){ if (symbol.indexOf ("RegisterNatives" )>=0 ){ console .log ("finally found RegisterNatives name:" , symbol) RegisterNatives _addr = symbols[i].address console .log ("finally found RegisterNatives address:" , RegisterNatives _addr) } } } if (RegisterNatives _addr != null ){ Interceptor .attach (RegisterNatives _addr,{ onEnter :function (args ){ console .log ("[RegisterNatives] method counts:" , args[3 ]) var env = args[0 ] var jclass = args[1 ] var class_name = Java .vm .tryGetEnv ().getClassName (jclass) var method_ptr = ptr (args[2 ]) var method_conut = parseInt (args[3 ]) for (var i=0 ; i<method_conut; i++){ var name_ptr = Memory .readPointer (method_ptr.add (i * Process .pointerSize * 3 )) var sig_ptr = Memory .readPointer (method_ptr.add (i * Process .pointerSize * 3 + Process .pointerSize )) var fnPtr_ptr = Memory .readPointer (method_ptr.add (i * Process .pointerSize * 3 + Process .pointerSize * 2 )) var name = Memory .readCString (name_ptr) var sig = Memory .readCString (sig_ptr) var find_module = Process .findModuleByAddress (fnPtr_ptr) console .log ("[RegisterNatives] java class:" ,class_name,"name:" ,name,"sig:" ,sig,"fnPtr:" ,fnPtr_ptr,"module_name:" ,find_module.name ,"module_base:" ,find_module.base ,"offset:" ,ptr (fnPtr_ptr).sub (find_module.base )) } },onLeave :function (retval ){ } }) }else { console .log ("didn't found RegisterNatives address" ) } } setImmediate (hook_RegisterNatives)
9.2 libc框架层的hook利用 9.2.1 hook pthread_create 很多应用是单独开一个线程来进行反调试的,而线程函数pthread是在libc中。假如我们hook创建线程的函数,是不是就可以找到它的地址,进而把这个反调试的线程关闭呢?
先找一下创建线程函数在不在内存里:
1 2 3 4 5 6 7 8 9 10 function hook_pthread ( ){ var pthread_create_addr = null var symbols = Process .findModuleByName ("libc.so" ).enumerateExports () console .log (JSON .stringify (symbols)) } setImmediate (hook_pthread)
找到后直接得到它的地址,再attach上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function hook_pthread ( ){ var pthread_create_addr = Module .findExportByName ("libc.so" , "pthread_create" ) console .log ("pthread_create_addr => " ,pthread_create_addr) Interceptor .attach (pthread_create_addr,{ onEnter :function (args ){ console .log ("args => " , args[0 ],args[1 ],args[2 ],args[3 ]) },onLeave :function (retval ){ console .log ("retval => " ,retval) } }) } setImmediate (hook_pthread)
由于应用可能一开始就进行反调试或者还没触发到反调试的函数,所以需要我们主动调用。
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 function beginAnti ( ){ Java .perform (function ( ){ Java .choose ("com.example.demoso1.MainActivity" ,{ onMatch :function (instance ){ console .log ("found instance:" ,instance) instance.init () },onComplete :function ( ){console .log ("serach complete!" )} }) }) } function hook_pthread ( ){ var pthread_create_addr = Module .findExportByName ("libc.so" , "pthread_create" ) console .log ("pthread_create_addr => " ,pthread_create_addr) Interceptor .attach (pthread_create_addr,{ onEnter :function (args ){ console .log ("args => " , args[0 ],args[1 ],args[2 ],args[3 ]) },onLeave :function (retval ){ console .log ("retval => " ,retval) } }) } setImmediate (hook_pthread)
执行命令:
1 2 frida -U -f com.example.demoso1 -l libc.js --no-pause beginAnti() //主动调用
其中pthread_create()
传入的第三个参数是线程运行函数的起始地址。每次主动调用它,起始地址可能会改变,但它相对于基地址的偏移是不变的。
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 function beginAnti ( ){ Java .perform (function ( ){ Java .choose ("com.example.demoso1.MainActivity" ,{ onMatch :function (instance ){ console .log ("found instance:" ,instance) instance.init () },onComplete :function ( ){console .log ("serach complete!" )} }) }) } function hook_pthread ( ){ var pthread_create_addr = Module .findExportByName ("libc.so" , "pthread_create" ) console .log ("pthread_create_addr => " ,pthread_create_addr) Interceptor .attach (pthread_create_addr,{ onEnter :function (args ){ console .log ("args => " , args[0 ],args[1 ],args[2 ],args[3 ]) var libnativebaseaddress = Moudle .findBaseAddress ("libnative-lib.so" ) if (libnativebaseaddress != null ){ console .log ("libnaticebaseaddress => " ,libnativebaseaddress) var detect_frida_loop_offset = args[2 ] - libnativebaseaddress console .log ("detect_frida_loop_offset => " ,detect_frida_loop_offset) } },onLeave :function (retval ){ console .log ("retval => " ,retval) } }) } setImmediate (hook_pthread)
也就是反调试的线程创建函数在libnative-lib.so
中的偏移为64900。要想它不执行,可以将反调试的线程创建函数的args[2]指向另一个函数(置空会失败),比如libc.so
中的time()
函数。
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 function beginAnti ( ){ Java .perform (function ( ){ Java .choose ("com.example.demoso1.MainActivity" ,{ onMatch :function (instance ){ console .log ("found instance:" ,instance) instance.init () },onComplete :function ( ){console .log ("serach complete!" )} }) }) } function hook_pthread ( ){ var pthread_create_addr = Module .findExportByName ("libc.so" , "pthread_create" ) var time_addr = Module .findExportByName ("libc.so" , "time" ) console .log ("pthread_create_addr => " ,pthread_create_addr) Interceptor .attach (pthread_create_addr,{ onEnter :function (args ){ console .log ("args => " , args[0 ],args[1 ],args[2 ],args[3 ]) var libnativebaseaddress = Moudle .findBaseAddress ("libnative-lib.so" ) if (libnativebaseaddress != null ){ console .log ("libnaticebaseaddress => " ,libnativebaseaddress) if (args[2 ]-libnativebaseaddress == 64900 ){ args[2 ] = time_addr } } },onLeave :function (retval ){ console .log ("retval => " ,retval) } }) } setImmediate (hook_pthread)
或者可以把反调试的线程创建函数直接整个替换掉。替换的前提条件需要知道它的参数和返回值,可以在Kali终端输入查看pthread_create()
的参数和返回值:
先什么都不干,只是打印几个参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function beginAnti ( ){ Java .perform (function ( ){ Java .choose ("com.example.demoso1.MainActivity" ,{ onMatch :function (instance ){ console .log ("found instance:" ,instance) instance.init () },onComplete :function ( ){console .log ("serach complete!" )} }) }) } function replace_pthread ( ){ var pthread_create_addr = Module .findExportByName ("libc.so" , "pthread_create" ) console .log ("pthread_create_addr => " ,pthread_create_addr) var pthread_create = new NativeFunction (pthread_create_addr,"int" ,["pointer" ,"pointer" ,"pointer" ,"pointer" ]) Interceptor .replace (pthread_create_addr,new NativeCallback (function (parg1,parg2,parg3,parg4 ){ console .log (parg1,parg2,parg3,parg4) return pthread_create (parg1,parg2,parg3,parg4) },"int" ,["pointer" ,"pointer" ,"pointer" ,"pointer" ])) } setImmediate (replace_pthread)
让整个反调试的pthread_create()
函数不执行:
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 function beginAnti ( ){ Java .perform (function ( ){ Java .choose ("com.example.demoso1.MainActivity" ,{ onMatch :function (instance ){ console .log ("found instance:" ,instance) instance.init () },onComplete :function ( ){console .log ("serach complete!" )} }) }) } function replace_pthread ( ){ var pthread_create_addr = Module .findExportByName ("libc.so" , "pthread_create" ) console .log ("pthread_create_addr => " ,pthread_create_addr) var pthread_create = new NativeFunction (pthread_create_addr,"int" ,["pointer" ,"pointer" ,"pointer" ,"pointer" ]) Interceptor .replace (pthread_create_addr,new NativeCallback (function (parg1,parg2,parg3,parg4 ){ console .log (parg1,parg2,parg3,parg4) var libnativebaseaddress = Moudle .findBaseAddress ("libnative-lib.so" ) if (libnativebaseaddress != null ){ console .log ("libnaticebaseaddress => " ,libnativebaseaddress) if (parg3-libnativebaseaddress == 64900 ){ return null } } return pthread_create (parg1,parg2,parg3,parg4) },"int" ,["pointer" ,"pointer" ,"pointer" ,"pointer" ])) } setImmediate (replace_pthread)
9.2.2 hook fopen fputs fclose 主动调用libc.so
中的fopen()
、fputs()
、fclose()
向内存中写东西。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function writeSomething ( ){ var fopen_addr = Module .findExportByName ("libc.so" ,"fopen" ) var fputs_addr = Module .findExportByName ("libc.so" ,"fputs" ) var fclose_addr = Module .findExportByName ("libc.so" ,"fclose" ) console .log ("fopen => " , fopen_addr,"fpunts => " ,fputs_addr,"fclose => " ,fclose_addr) var fopen = new NativeFunction (fopen_addr,"pointer" ,["pointer" ,"pointer" ]) var fputs = new NativeFunction (fputs_addr,"int" ,["pointer" ,"pointer" ]) var fclose = new NativeFunction (fclose_addr,"int" ,["pointer" ]) var fileName = Memory .allocUtf8String ("/sdcard/v5le0n9.txt" ) var mode = Memory .allocUtf8String ("w+" ) var fp = fopen (fileName,mode) var content = Memory .allocUtf8String ("Hello from frida" ) fputs (content,fp) fclose (fp) } setImmediate (writeSomething)
将路径和内容抽出来作为参数传入,就可以在任意地方写任意东西了。比如将“设置”App中所有的导出函数写进文件中。
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 function writeSomething (path,contents ){ var fopen_addr = Module .findExportByName ("libc.so" ,"fopen" ) var fputs_addr = Module .findExportByName ("libc.so" ,"fputs" ) var fclose_addr = Module .findExportByName ("libc.so" ,"fclose" ) console .log ("fopen => " , fopen_addr,"fpunts => " ,fputs_addr,"fclose => " ,fclose_addr) var fopen = new NativeFunction (fopen_addr,"pointer" ,["pointer" ,"pointer" ]) var fputs = new NativeFunction (fputs_addr,"int" ,["pointer" ,"pointer" ]) var fclose = new NativeFunction (fclose_addr,"int" ,["pointer" ]) var fileName = Memory .allocUtf8String (path) var mode = Memory .allocUtf8String ("a+" ) var fp = fopen (fileName,mode) var content = Memory .allocUtf8String (contents) fputs (content,fp) fclose (fp) } function EnumerateAllExports ( ){ var modules = Process .enumerateModules () for (var i=0 ; i<modules.length ; i++){ var module = modules[i] var module_name = modules[i].name var exports = module .enumerateExports () console .log ("module_name => " ,module_name,"module.enumerateExports => " ,JSON .stringify (exports )) for (var m=0 ; m<exports .length ; m++){ writeSomething ("/sdcard/settings/" +module_name+".txt" ,"type:" +exports [m].type +" name:" +exports [m].name +" address:" +exports [m].address +"\n" ) } } } setImmediate (EnumerateAllExports )
1 frida -U -f com.android.setting -l libc.js --no-pause
9.3 linker框架层的hook利用 基于ELF文件的特性,很多加固厂商在进行Android逆向的对抗时,都会在Android的so文件中进行动态的对抗,对抗点一般在so文件的.init
段和JNI_OnLoad
处。因此,我们在逆向分析各种厂商的加固so时,需要在so文件的.init
段和JNI_OnLoad
处下断点进行分析,绕过这些加固的so对抗。
从.init
段和.init_array
段构造函数的调用实现来看,最终都是调用call_function()
函数,因此IDA动态调试so时,只要守住call_function()
就可以对.init
段和.init_array
段构造函数调用的监控。
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 388 void soinfo::call_constructors () {389 if (constructors_called) {390 return ;391 }392 393 394 395 396 397 398 399 400 401 402 403 constructors_called = true ;404 405 if (!is_main_executable() && preinit_array_ != nullptr) {406 407 PRINT("\"%s\": ignoring DT_PREINIT_ARRAY in shared library!" , get_realpath());408 }409 410 get_children().for_each([] (soinfo* si) {411 si->call_constructors();412 });413 414 if (!is_linker()) {415 bionic_trace_begin((std ::string ("calling constructors: " ) + get_realpath()).c_str());416 }417 418 419 call_function("DT_INIT" , init_func_, get_realpath());420 call_array("DT_INIT_ARRAY" , init_array_, init_array_count_, false , get_realpath());421 422 if (!is_linker()) {423 bionic_trace_end();424 }425 }334 static void call_function (const char * function_name __unused, 335 linker_ctor_function_t function,336 const char * realpath __unused) {337 if (function == nullptr || reinterpret_cast<uintptr_t >(function) == static_cast<uintptr_t >(-1 )) {338 return ;339 }340 341 TRACE("[ Calling c-tor %s @ %p for '%s' ]" , function_name, function, realpath);342 function(g_argc, g_argv, g_envp);343 TRACE("[ Done calling c-tor %s @ %p for '%s' ]" , function_name, function, realpath);344 }345 346 static void call_function (const char * function_name __unused, 347 linker_dtor_function_t function,348 const char * realpath __unused) {349 if (function == nullptr || reinterpret_cast<uintptr_t >(function) == static_cast<uintptr_t >(-1 )) {350 return ;351 }352 353 TRACE("[ Calling d-tor %s @ %p for '%s' ]" , function_name, function, realpath);354 function();355 TRACE("[ Done calling d-tor %s @ %p for '%s' ]" , function_name, function, realpath);356 }359 static void call_array (const char * array_name __unused, 360 F* functions,361 size_t count,362 bool reverse,363 const char * realpath) {364 if (functions == nullptr) {365 return ;366 }367 368 TRACE("[ Calling %s (size %zd) @ %p for '%s' ]" , array_name, count, functions, realpath);369 370 int begin = reverse ? (count - 1 ) : 0 ;371 int end = reverse ? -1 : count;372 int step = reverse ? -1 : 1 ;373 374 for (int i = begin; i != end; i += step) {375 TRACE("[ %s[%d] == %p ]" , array_name, i, functions[i]);376 call_function("function" , functions[i], realpath);377 }378 379 TRACE("[ Done calling %s for '%s' ]" , array_name, realpath);380 }
由于arm64架构中的linker64.so
中没有call_function()
函数,所以需要强制将App运行在32位模式下:
1 adb install --abi armeabi-v7a <path to apk>
hook call_function()
函数:
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 function LogPrint (log ) { var theDate = new Date (); var hour = theDate.getHours (); var minute = theDate.getMinutes (); var second = theDate.getSeconds (); var mSecond = theDate.getMilliseconds () hour < 10 ? hour = "0" + hour : hour; minute < 10 ? minute = "0" + minute : minute; second < 10 ? second = "0" + second : second; mSecond < 10 ? mSecond = "00" + mSecond : mSecond < 100 ? mSecond = "0" + mSecond : mSecond; var time = hour + ":" + minute + ":" + second + ":" + mSecond; var threadid = Process .getCurrentThreadId (); console .log ("[" + time + "]" + "->threadid:" + threadid + "--" + log); } function hooklinker ( ) { var linkername = "linker" ; var call_function_addr = null ; var arch = Process .arch ; LogPrint ("Process run in:" + arch); if (arch.endsWith ("arm" )) { linkername = "linker" ; } else { linkername = "linker64" ; LogPrint ("arm64 is not supported yet!" ); } var symbols = Module .enumerateSymbolsSync (linkername); for (var i = 0 ; i < symbols.length ; i++) { var symbol = symbols[i]; if (symbol.name .indexOf ("__dl__ZL13call_functionPKcPFviPPcS2_ES0_" ) != -1 ) { call_function_addr = symbol.address ; LogPrint ("linker->" + symbol.name + "---" + symbol.address ) } } if (call_function_addr != null ) { var func_call_function = new NativeFunction (call_function_addr, 'void' , ['pointer' , 'pointer' , 'pointer' ]); Interceptor .replace (new NativeFunction (call_function_addr, 'void' , ['pointer' , 'pointer' , 'pointer' ]), new NativeCallback (function (arg0, arg1, arg2 ) { var functiontype = null ; var functionaddr = null ; var sopath = null ; if (arg0 != null ) { functiontype = Memory .readCString (arg0); } if (arg1 != null ) { functionaddr = arg1; } if (arg2 != null ) { sopath = Memory .readCString (arg2); } var modulebaseaddr = Module .findBaseAddress (sopath); LogPrint ("after load:" + sopath + "--start call_function,type:" + functiontype + "--addr:" + functionaddr + "---baseaddr:" + modulebaseaddr); if (sopath.indexOf ('libnative-lib.so' ) >= 0 && functiontype == "DT_INIT" ) { LogPrint ("after load:" + sopath + "--ignore call_function,type:" + functiontype + "--addr:" + functionaddr + "---baseaddr:" + modulebaseaddr); } else { func_call_function (arg0, arg1, arg2); LogPrint ("after load:" + sopath + "--end call_function,type:" + functiontype + "--addr:" + functionaddr + "---baseaddr:" + modulebaseaddr); } }, 'void' , ['pointer' , 'pointer' , 'pointer' ])); } } setImmediate (hooklinker)