Frida逆向与利用自动化

这篇文章记录一下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 modules

查看库中的导出函数:

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

使用命令

1
jobs list

可以看到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
//MainActivity.java
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
//test01.js
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 
  • -U:连接到USB设备
  • -l:载入脚本

2.1 修改参数

修改传入的参数,返回值的结果也随之改变:

1
2
3
4
5
6
7
8
9
10
11
//test01.js
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
//test01.js
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.javaonCreate()函数中来的。

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
//MainActivity.java
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
//test01.js
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
//test01.js
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")
//return "METOO"
}
})
}
setImmediate(main)

2.5 动静态处理和主动调用

如果想打印没有被调用的函数,比如secret()方法,也可以打印该方法的实例和返回值。如果是动态调用,需要找到实例进行主动调用:

1
2
3
4
5
6
7
8
9
10
11
12
//test01.js
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
//MainActivity.java
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
//test01.js
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
//app3.js
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")}
})
})
}

//函数回调,3s后执行invoke函数
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
//MainActivity.java
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]));
//Log.d("SimpleArraysStringBytes",Arrays.toString(Arrays.toString(arr[i]).getBytes()));
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
//test02.js
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
//MainActivity.java
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]));
//Log.d("SimpleArraysStringBytes",Arrays.toString(Arrays.toString(arr[i]).getBytes()));
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
//test02.js
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
//test02.js
function main(){
Java.perform(function(){
Java.use("java.util.Arrays").toString.overload('[C').implementation = function(args1){
var result = this.toString(args1)
//打印Java对象的内容可以使用json包
console.log("args1, result", JSON.stringify(args1), result)
return result
}
})
}
setImmediate(main)

接下来正式进行构造数组的内容:

1
2
3
4
5
6
7
8
9
10
11
12
//test02.js
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 "烟村四五家"
//return Java.use("java.lang.String").$new("烟村四五家")
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
//test02.js
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", args1, result)
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
//Water.java
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
//juice.java
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();
//w2.fillEnergy();//错误,父类对象不能使用子类方法
((juice) w2).fillEnergy();//需要进行强制类型转换
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//MainActivity.java
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
//test02.js
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
//test02.js
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
//test02.js
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
//liquid.java
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
//milk.java
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
//MainActivity.java
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
//test02.js
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
//TrafficLight.java
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.getClass().getName().toString());
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
//MainActivity.java
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
//test02.js
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()
//console.log(all_methods)
for(var i=0; i<all_methods.length; i++){
var method = all_methods[i]
//console.log(method.toString())
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
//MainActivity.java
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
//test02.js
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
//test02.js
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
//rpc.js
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
//rpc.js
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.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
#loader.py
import time
import frida

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

device = frida.get_usb_device()
#pid = device.spawn(["com.v5le0n9.test01"])
#device.resume
#time.sleep(1)
#session = device.attach(pid)
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
#loader.py
import time
import frida

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

#device = frida.get_usb_device()
device = frida.get_device_manager().add_remote_device('192.168.24.104:9999')
print(device.enumerate_applications())
#pid = device.spawn(["com.v5le0n9.test01"])
#device.resume
#time.sleep(1)
#session = device.attach(pid)
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
<!--activity_main.xml-->
<?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
//MainActivity.java
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;
}
//hook target
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
//test03.js
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
//test03.js
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
#test03loader.py
import frida
import base64

def my_message_handler(message,payload):
print(message)#打印出一个字典
print(payload)#None
if message["type"] == "send":
print(message["payload"])#打印关键字“payload”的值
data = message["payload"].split(":")[1].strip()#以“:”分隔,将“:”后面的字符串赋值给data
print("message:", message)
data = str(base64.b64decode(data))#解密
print("data:",data)#打印解密后的字符串
usr,pw = data.split(":")#用“:”分隔用户名与密码并赋值给usr和pw
print("pw",pw)#打印密码
data = str(base64.b64encode(("admin" + ":" + pw).encode()))#构造用户名与密码组合,用base64加密
print("encode data",data)
script.post({"my_data":data})#将data的值发送到test03.js中id为my_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
//return Java.use("java.lang.String").$new("V@]EAASB\u0012WZF\u0012e,a$7(&am2(3.\u0003")
}
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
//return Java.use('java.lang.String').$new("\u0000dslp}oQ\u0000 dks$|M\u0000h +AYQg\u0000P*!M$gQ\u0000")
}
})
}
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
//reverseA.java
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
//reverseB.java
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)
//reverseB result: 0064736c707d6f510020646b73247c4d0068202b4159516700502a214d24675100

由于b()算法有按位运算操作,确实有点麻烦,所以我们利用Z3库来求解这个问题。

1
pip install z3-solver
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
#reverseB.py
from z3 import *
from binascii import b2a_hex, a2b_hex

s = 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
//reverseB.java
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;
}
}
}
//Log.i("ceshi", "plaintext="+plaintext);
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不成功,为什么呢?

  1. 这个so库真的有被加载进内存吗?
  2. so库中的导出函数真的有被加载进内存吗?
  3. 我们找的导出函数真的是我们想要找的导出函数吗?

而这几个问题,完全可以通过objection来查看内存中的信息,验证是否是我们想要找的so库和导出函数。

用objection查看内存中加载的库:

1
memory list modules

当然也可以使用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)

//对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)

//对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)
//var result = myfirstjniJNI_invoke(args0,args1,args2)
//return result
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()
//console.log("Process.enumerateModules => ",JSON.stringfy(modules))
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命令窗口中输入

1
frida -UF

即可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
//枚举libart.so中的符号表
var symbols = Process.findModuleByName("libart.so").enumerateSymbols()
//查看GetStrinUTFChars()是否在内存中
//console.log(JSON.stringify(symbols))

//确认存在后发现有两个,对函数进行过滤
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()
//查看GetStrinUTFChars()是否在内存中
//console.log(JSON.stringify(symbols))

//确认存在后发现有两个,对函数进行过滤
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])

//因为hook的是系统层,所以会有其他程序的参数和返回值干扰,所以可以通过调用栈查看哪个是我们想要的
//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.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)
//打印char*的内容
console.log("args2 => ",args2.readCString())
//修改char*的内容
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
//libc.js
function hook_pthread(){
var pthread_create_addr = null
//查找符号表有无创建线程函数
//var symbols = Process.findModuleByName("libc.so").enumerateSymbols()
//查找导出表有无创建线程函数
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
//libc.js
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
//libc.js
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
//libc.js
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
//libc.js
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)
//var detect_frida_loop_offset = args[2] - libnativebaseaddress
//console.log("detect_frida_loop_offset => ",detect_frida_loop_offset)
if(args[2]-libnativebaseaddress == 64900){
//args[2] = null//置空会失败
args[2] = time_addr
}
}
},onLeave:function(retval){
console.log("retval => ",retval)
}
})
}
setImmediate(hook_pthread)

或者可以把反调试的线程创建函数直接整个替换掉。替换的前提条件需要知道它的参数和返回值,可以在Kali终端输入查看pthread_create()的参数和返回值:

1
man 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
//libc.js
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
//libc.js
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){
//为什么这里可以为空,而attach不行?
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()
//console.log("Process.enumerateModules => ",JSON.stringfy(modules))
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))
//exports中有三个属性:type,name,address
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
// /bionic/linker/linker_soinfo.cpp
388 void soinfo::call_constructors() {
389 if (constructors_called) {
390 return;
391 }
392
393 // We set constructors_called before actually calling the constructors, otherwise it doesn't
394 // protect against recursive constructor calls. One simple example of constructor recursion
395 // is the libc debug malloc, which is implemented in libc_malloc_debug_leak.so:
396 // 1. The program depends on libc, so libc's constructor is called here.
397 // 2. The libc constructor calls dlopen() to load libc_malloc_debug_leak.so.
398 // 3. dlopen() calls the constructors on the newly created
399 // soinfo for libc_malloc_debug_leak.so.
400 // 4. The debug .so depends on libc, so CallConstructors is
401 // called again with the libc soinfo. If it doesn't trigger the early-
402 // out above, the libc constructor will be called again (recursively!).
403 constructors_called = true;
404
405 if (!is_main_executable() && preinit_array_ != nullptr) {
406 // The GNU dynamic linker silently ignores these, but we warn the developer.
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 // DT_INIT should be called before DT_INIT_ARRAY if both are present.
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];
//LogPrint(linkername + "->" + symbol.name + "---" + symbol.address);
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)