0%

逆向学习 0x06JAVA层hook的API

API

现在介绍的只是Java层的API

注意frida代码需要写在 Java.perform(function() {}); 函数体中,下面省略不写了

静态方法和实例方法的hook

1
2
3
4
5
6
7
8
9
10
11
12
// 不需要区分修饰符,也不需要区分静态方法和实例方法,hook代码的写法都是一样的
// 无论带不带static都可以直接hook

// 得到要hook的类的对象
var xxx = Java.use("com.xxxx.xxx")

// hook实例方法
xxx.(方法).implementation = function () {
var result = this.(方法)
console.log("xxx :", result)
return result;
}

函数参数和返回值的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
var money = Java.use("com.hello.demo.money")
var str = Java.use("java.lang.String")
money.getInfo.implementation = function(a) {
var result = this.getInfo("123");
console.log(result);
return str.$new("hello world");

// 这里直接返回 "hello world" 是没有什么问题的,这是因为frida会自动帮忙处理掉这些问题
// "hello world" 这个是JS类型的字符串,而函数需要的返回值是Java类型的字符串,需要进行一个转化
// frida 是会自动处理这个问题的,但是在一些情况下可能处理不了
// 这个时候就需要手动来进行一个转换了,就是调用String方法来new String
// 在参数传递上也是类似的
}

构造方法的hook

1
2
3
4
5
6
7
8
9
10
11
12
// 基本上都是使用$init来表示构造函数,例外:String

var money = Java.use("com.hello.demo.money")
money.$init.implementation = function() {
var result = this.$init;
return result;
}

var str = Java.use("java.lang.StringFactory")
str.newStringFromString.implementation = function(a) {
// ……………………
}

对象参数的构造和修改

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
// 其实上面那个String就是一种构造

var wallet = Java.use("");
var money = Java.use("");
wallet.(方法).implementation = function(a) {
console.log(a);
return this.方法(money.$new("你好", 200))
}

// 这个$new其实就是一种构造,在需要传递自己定义的类型数据的时候执行,只需要找到相应的类,然后进行$new


var wallet = Java.use("");
wallet.(方法).implementation = function(a) {

// console.log(a);
// 这里的a类型,如果没有toString方法的话,是只能打印出返回值的
// 需要自己去找相应的变量名,去点出方法获取输出,或者点变量再点value
console.log(a.currency.value);
console.log(a.getCurrency);

// 如果要进行修改操作
// 也是两个操作,第一个是找到要更改的变量直接点value进行更改
// 第二个是找到更改的方法执行
a.amount.value = 3000;
a.setAmount(3000);
return this.方法(a);
}

HashMap的打印

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
var utils = Java.use("");
var stringBuilder = Java.use("java.lang.StringBuilder");
utils.方法.implementation. = function(a) {

// 如果传入的参数是HashMap类型的,无法向下转型,自然也没有toString方法,这个时候就需要手动将内容打印出来
// keySet()方法是获取到key值
var key = a.keySet();

// 定义一个迭代器
var it = key.iterator();
var result = stringBuilder.$new();

// 迭代器方法类似于for循环
while(it.hasNext()) {
var keystr = it.next();
var valuestr = a.get(keystr);
result.append(valuestr);
}
console.log(result.toString());

var result = this.shufferMap(a);
console.log(result);
return result;
}

// 题外话,如果是byte数组,就使用 JSON.stringify() 处理

重载方法的hook

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
z// 之前比比皆是,在 implementation 前加上 overload() 括号内指定类型
// 这个也不用特意的去管他,如果碰到重载方法,一执行就会提示相应的类型,复制粘贴即可

var str = Java.use("java.lang.String")
str.getBytes.overload().implementation = function() {
console.log("getBytes() called");
return "hello world".getBytes();
};
});


// 这个东西太慢了,尤其是重载方法比较多的时候
// 直接hook所有重载方法

// 利用frida中的一个方法获取到所有的重载方法,复写implementation方法,便利这个数组
var utils = Java.use("com.xxx.xxx");
var overloadsArr = utils.方法.overloads;
for (var i = 0; i < overloadsArr.length; i++) {
overloadsArr[i].implementation = function() {
var params = "";
for (var j = 0; j < arguments.length; j++) {
params += arguments[j] + "";
}
console.log(params);
console.log(this);

// 介绍一下JS中的两个小方法
// call 和 apply
// 下面这个就相当于 this.方法(arguments) 这个东西
// 这两个的区别是接收多个参数还是单个,如果是call 的话就需要将参数挨个传入
// 如果要根据参数的个数不同进行处理的话,使用 if(arguments.length)
return this.方法.apply(thisarguments);
}
}

hook全部重载方法的这个了解就可以了,有相关的命令可以解决这个问题,遇到命令解决不了的,再来手敲

主动调用

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
// 对于静态方法,直接打印调用即可
var money = Java.use("com.xxx.xxxx");
console.log(money.方法())
// 这样这个静态方法就调用成功了


// 如果用同样的方法来调用非static修饰的方法那就出事了,会报错
// Error: 方法名: cannot call instance method without an instance
// 不使用 一个对象就不能调用方法
// 现在需要一个对象,方法一,新建一个对象
console.log(money.$new.方法())


// 方法二,获取已有的对象
// 要和需要获取的对象的位置相同
Java.choose("com.xxx.xxxx", {
onMatch: function (obj) {
// 主动调用成功
console.log(obj.方法());
},
onComplete: function () {
console.log("END")
}
});


// Android Context的获取
var current_application = Java.use("android.app.ActivityThread").cuttrntApplication();
var content = current_application.getApplicationContext();

获取和修改类的字段

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
// 静态字段
var money = Java.use("com.xxx.xxxx");
console.log(money.变量名.value);
money.变量名.value = "hello world";
console.log(money.变量名.value);


// 实例字段 创建新对象
var moneyObj = money.$new();
console.log(moneyObj.变量名.value)
money.变量名.value = "hello world";
console.log(money.变量名.value);


// 实例字段(获取已有对象)
Java.choose("com.xxx.xxxx", {
onMatch: function (obj) {
console.log(obj.变量名.value);
}, onComplete: function() {

}
});


// 如果有字段名和方法名一样,如果想要获取到属性需要加下划线前缀
// 例如一个类中有一个accountName属性也有一个accountName方法,如果想要指定accountName属性,需要加_
// 调用方法直接用即可
Java.choose("com.xxx.xxxx", {
onMatch: function (obj) {
console.log(obj._accountName.value);
}, onComplete: function() {

}
});

hook内部类和匿名类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
内部类简单来说就是class中的class
比如调用R文件中的内部类
public class tset {
class a {
public static int b = 1;
}
}
com.xxx.xxx.R$id
// 这个就是调用了R中的内部类id


// 这里创建的就是匿名类,方法没有名字,也就是说hook这个getInfo()方法是hook不到东西的
// 只能使用 $ + 数字进行hook,比如 com.demo.tast$1
// 这个数字从1开始排序,从上到下
public static void main(String[] args) {
System.out.println(new people("张三", 19) {
@Override
public String getInfo() {
return getName() + "" + getAge();
}
}.getInfo());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// hook内部类,需要使用Java.choose来获取匿名对象
Java.choose("com.xxx.xxxx$a", {
onMatch: function (obj) {
console.log(obj._accountName.value);
}, onComplete: function() {

}
});


// hook匿名类
var people$1 = Java.use("com.xxx.people$1);
people$1.getInfo.implementation = function() {
var result = this.getInfo();
return result;
};
});

// 如果有匿名内部类就使用Java.choose进行嵌套即可,如果太过于复杂,可以直接遍历所有的类,直接去搜索

混淆函数的hook方法

1
2
3
4
5
6
7
8
9
10
11
12
13
// 函数被混淆之后,这个时候直接根据它的名字hook即可,如果有乱码或者难以填写可以进行编码,将编码后的数据去JS脚本中解密,用变量来表示名称
// 假如进行了base64编码处理

var people = Java.use("com.xxx.xxxx");
var base64 = Java.use("android.util.Base64");
var str = Java.use("java.lang.String");
var funcName = str.$new(base64.decode("Z2V0SW5mbw==", 0));
console.log(funcName);
people[funcName].implementation = function() {
var result = this.funcName();
console.log(result);
return result;
}

做一个小小的混淆,就是将类名、方法名进行base64加密

如果要更改dex中的类名,需要用到一个库dexlib2

输出结果:

image-20241016112901816

混淆前后对比

image-20241016113231981

面对混淆的名字,jadx可能会处理一部分,如果出现注释和名字不一样的,以注释为准进行hook

image-20241016113314869

混淆代码

因为是使用Android开发的,所有目录在安卓端,运行比较慢,也可以使用Java写

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
public class MainActivity extends AppCompatActivity {

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

try {
// 读取原本的dex和新创建的dex,这里是手机中的文件路径
File oldDexFile = new File("/data/data/com.example.demo/classes.dex");
File newDexFile = new File("/data/data/com.example.demo/new_classes.dex");

Log.d("demo", "从assets中读取classes.dex");

// 打开放在assets目录下的dex文件,输入流,然后将文件写出到app私有空间去
InputStream ins = getApplicationContext().getAssets().open("classes.dex");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] bytes = new byte[1024];
int index;
while ((index = ins.read(bytes)) != -1)
baos.write(bytes, 0 ,index);

Log.d("demo", "开始写出classes.dex到" + oldDexFile.toString() + "...");

FileOutputStream fos = new FileOutputStream(oldDexFile);
fos.write(baos.toByteArray());
ins.close();
fos.close();
baos.close();

Log.d("demo", "开始使用dexlib2加载classes.dex...");


// 指明api版本
Opcodes opcodes = Opcodes.forApi(29);
// DexFileFactory.loadDexFile(oldDexFile, opcodes)是用来指明dex文件,两个参数为路径和api版本
// 这一步的操作是将dex文件不进行修改直接写进来,如果这一步报错了其他也没戏,就是一个测试属于是
DexFileFactory.writeDexFile(oldDexFile.toString(), DexFileFactory.loadDexFile(oldDexFile, opcodes));
// 重新将dex文件加载进来
DexBackedDexFile dexBackedDexFile = DexFileFactory.loadDexFile(oldDexFile, opcodes);
Log.d("demo", "dexFile:" + dexBackedDexFile.getClass().getName());

// 设定要更改的内容
DexRewriter rewriter = new DexRewriter(new RewriterModule() {

// getClassDefRewriter获取一个类的时候调用该方法
@Override
public Rewriter<ClassDef> getClassDefRewriter(Rewriters rewriters) {
return new ClassDefRewriter(rewriters) {
@Override
public ClassDef rewrite(ClassDef classDef) {
String className = classDef.getType();

// 获取类名,如果类名包含com.example就重写,getType()方法中是获取类名后进行重写的具体操作
if (className.contains("com/dodonew")) {
return new RewrittenClassDef(classDef) {
@Override
public String getType() {
String class_fullName = classDef.getType();
int start = class_fullName.lastIndexOf("/");
int end = class_fullName.lastIndexOf("$");
int length = class_fullName.length();
if (end < 0) {
if (class_fullName.contains(";")) {
end = length - 1;
} else {
end = length;
}
}

// 进行base64编码,替换旧名字
String class_name = class_fullName.substring(start + 1, end);
String class_name_enc = Base64.encodeToString(class_name.getBytes(), 0);
Log.d("demo", class_name + "" + class_name_enc);
return class_fullName.replace(class_name, class_name_enc);
}

@Override
public String getSourceFile() {return null;}
};
}
return super.rewrite(classDef);
}
};
}

// getFieldRewriter获取类中的一个字段的时候调用该方法
@Override
public Rewriter<Field> getFieldRewriter(Rewriters rewriters) {
return new FieldRewriter(rewriters) {
@Override
public Field rewrite(Field field) {
String className = field.getDefiningClass();
if (className.contains("com/dodonew")) {
return new RewrittenField(field) {
@Override
public String getName() {
return Base64.encodeToString(field.getName().getBytes(), 0);
}
};
}
return super.rewrite(field);
}
};
}

// getMethodRewriter获取方法的时候调用该方法
@Override
public Rewriter<Method> getMethodRewriter(Rewriters rewriters) {
return new MethodRewriter(rewriters) {
@Override
public Method rewrite(Method value) {
String className = value.getDefiningClass();
if (className.contains("com/dodonew")) {
return new RewrittenMethod(value) {
@Override
public String getName() {
String methodName = Base64.encodeToString(value.getName().getBytes(), 0);
Log.d("demo", "methodName:" + methodName);
return methodName;
}
};
}
return super.rewrite(value);
}
};
}
});

// 将设定好的rewrite保存,这个写的是dexBackedDexFile,这是一开始加载的dex就相当于覆盖写出了
DexFile rewriteDexFile = new DexFileRewriter(rewriter).rewrite(dexBackedDexFile);

// 将改好的dex写出保存
DexPool dexPool = new DexPool(rewriteDexFile.getOpcodes());
for (ClassDef classDef : rewriteDexFile.getClasses()) {
dexPool.internClass(classDef);
}

dexPool.writeTo(new FileDataStore(newDexFile));
Log.d("demo", "new_classes.dex生成完毕,保存路径为:" + newDexFile.toString());

} catch (IOException e) {
e.printStackTrace();
}
}
}

枚举所有已加载的类与枚举类的所有方法

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
// 这个之前就用过一个一键打印所有已经加载的类的方法
// 1、注意是已加载不是所有
// 2、加载的类也不一定能hook到,原因是存在类加载器,有的类的类加载器和正在使用的类加载器不一样,会找不到这个类,但是会被打印出来
console.log(Java.enumerateLoadedClassesSync().join("\n"))


// 枚举类的所有方法
// 实现思想呢就是Java中的反射思想
// 第一步就是获取class对象,这个和Java中的实现不一样直接use就行了
var people = Java.use("com.xxx.xxxx.People");

// 获取函数名、构造函数、字段、内部类
var methods = people.class.getDeclaredMethods();
var constructors = people.class.getDeclaredConstructors();
var fields = people.class.getDeclaredFields();
var classes = people.class.getDeclaredClasses();

for (let i = 0; i < methods.length; i++) {
console.log(methods[i].getName());
}
console.log("============================")
for (let i = 0; i < fields.length; i++) {
console.log(constructors[i].getName())
}
console.log("============================")
for (let i = 0; i < fields.length; i++) {
console.log(fields[i].getName())
}
console.log("============================")
for (let i = 0; i < fields.length; i++) {
console.log(classes[i].getName())
// classes[i] 这里得到的已经是类的字节码,不需要再.class
var Wallet$InnerStructure = classes[i].getDeclaredFields();
for (var j = 0; j < Wallet$InnerStructure.length; j++) {
console.log(Wallet$InnerStructure[j].getName());
}
}

hook类的所有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function hookFunc(methodName) {
console.log(methodName)
var overloadsArr = utils[methodName].overloads;
for (var j = 0; j < overloadsArr.length; j++) {
overloadsArr[j].implementation = function() {
var params = "";
for (var k = 0; k < arguments.length; k++) {
params += arguments[k] + " ";
}
console.log("utils." + methodName + "is called! params is: ", params);
return this[methodName].apply(this, arguments);
}
}
}

var utils = Java.use("com.dodonew.online.DodonewOnlineApplication");
var methods = utils.class.getDeclaredMethods();
console.log("1")
for (var i = 0; i < methods.length; i++) {
var methodName = methods[i].getName();
hookFunc(methodName);
}

image-20241017192903835

image-20241017193046511

注入dex

Java.registerClass

这个是使用JS语言写的,frida注入之后可能会出现bug

这个必须需要实现一个接口,这个还是比较麻烦的,接口内的东西还需要都实现

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
Java.perform(function () {
const MyWeirdTrustManager = Java.registerClass({
name: "com.dodonew.online.MyWeirdTrustManager",

// 实现接口,这个貌似是必须要实现一个接口才行
implements: [Java.use("接口名")],

// 继承父类
superClass:

// 定义变量
fields: {
description: 'java.lang.String',
limit: 'int'
},

// 定义方法
methods: {

// 构建方法
$init() {
console.log("Constructor called")
},

// 重载方法需要使用中括号
test1: [{
returnType: 'void',
argumentTypes: [],
implementation() {
console.log("test1 called")
}
},{
returnType: 'void',
argumentTypes: ["java.lang.String", "int"],
implementation(str, num) {
console.log("test1 called with " + str + " and " + num)
}
}],
test2(str, num) {
console.log("test2 called with " + str + " and " + num)
return null;
},
}
});

var myObj = MyWeirdTrustManager.$new();
myObj.test1();
myObj.test1("Hello", 123);
myObj.test2("World", 456);
myObj.limit.value = 100;
console.log(myObj.limit.value);
});

image-20241021083842196

frida注入dex

这个操作是自己写好一个dex文件,将其注入到app中去然后再调用其中的方法

image-20241021085640901

先使用studio打包一个自己随便写的apk,然后取出classes.dex文件,将这个文件放到手机的 /data/local/tmp 路径下

在进行注入,这样的方法是写成Java类型的,容易纠错和编写

1
2
3
4
Java.openClassFile("/data/local/tmp/patch.dex").load();
var use = Java.use("com.example.demo.test");
var obj = use.test();
console.log(obj);

image-20241021085159066

通过这种方式就可以在hook其他app的时候主动调用自己写好的方法

hook枚举类

回忆一下枚举

image-20241021090808965

hook枚举类没什么价值,可以直接看的

1
2
3
4
5
6
7
8
Java.choose("com.xiaojianbang.app.Season", {
onMatch: function (obj){
console.log(obj.ordinal());
}, onComplete: function () {

}
})
console.log(Java.use("com.xiaojianbang.app.Season").values())

image-20241021091445508

firda写文件

frida是有API支持文件写入的

1
2
3
4
5
6
7
8
9
10
    var ios = new File("/data/data/com.xiaojianbang.app/test.txt", 'w')
ios.write("Hello, world!\n")
ios.flush()
ios.close()

最好将文件路径写在当前包名下,否则容易没有写入权限
w 覆盖
a+ 追加

注意:frida只有写文件的API,没有读文件API,如果要读文件,要去hook libc.so 中的读文件的函数

image-20241021093410843

转型 Java.cast

进行向上或者向下转型,应对那种不能 toString() 的情况

之前也使用过这个东西,注意使用 cast 转型要是本来就是这个类型的才能转型

例如这个参数传进来的时候就进行了一个向上转型,在hook代码中向下转型是没什么问题的

image-20241021094753946

1
2
3
4
5
6
7
var utils = Java.use("com.xiaojianbang.hook.Utils");
utils.shufferMap2.implementation = function(map) {
console.log("map: " map);
var result = Java.cast(map, Java.use("java.util.HashMap"));
console.log("map: ", result);
return this.shufferMap2(result);
}

数组的构建

这样也算是创建了一个数组

image-20241021100342745

frida对于创建数组是专门提供了API的,array就是将JS数组转化为Java数组传到方法中

1
2
3
4
5
6
7
var Utils = Java.use("com.xiaojianbang.hook.Utils");
// var obj = Utils.myPrint(["Hello", "world", "!"])
// console.log(obj)

var strarr = Java.array("java.lang.String", ["Hello", "world", "!"])
var obj = Utils.myPrint(strarr)
console.log(obj)

image-20241021100657763

object数组的构建

image-20241021174344517

1
2
3
4
5
6
7
8
9
可以看到传入的东西花里胡哨,有字符串,基本数据类型和对象
这个时候就麻爪了,原因就是在Java中万物皆对象,基本数据类型put到数组时是会转换成包装类,而js中没有这个操作,所以就报错了
其实也是可以直接传的
给他手动转换成包装类不就是了

var money = Java.use("com.xiaojianbang.hook.Money").$new("demoMoney", 2000);
var bool = Java.use("java.lang.Boolean");
var integer = Java.use("java.lang.Integer");
var obj = Utils.myPrint(["demo", integer.$new(100), bool.$new(true), money])

image-20241021180237078

只需要记住 Java.array 可以创建一个Java数组即可

ArrayList主动调用

list是数组数据,map是键值对数据

原理和object一样的,都是基本数据类型要转换成包装类

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
var Utils = Java.use("com.xiaojianbang.hook.Utils");
// var obj = Utils.myPrint(["Hello", "world", "!"])
// console.log(obj)

// var strarr = Java.array("java.lang.String", ["Hello", "world", "!"])
// var obj = Utils.myPrint(strarr)

var money = Java.use("com.xiaojianbang.hook.Money").$new("demoMoney", 2000);
var bool = Java.use("java.lang.Boolean");
var integer = Java.use("java.lang.Integer");

// var strarr = Java.array("Ljava.lang.Object;", ["demo", integer.$new(100), bool.$new(true), money])
// var obj = Utils.myPrint(strarr)
var obj = Utils.myPrint(["demo", integer.$new(100), bool.$new(true), money])
console.log(obj)
console.log("==============================")

var arraylist = Java.use("java.util.ArrayList").$new();
arraylist.add("demo");
arraylist.add(integer.$new(100));
arraylist.add(bool.$new(true));
arraylist.add(money);
var obj1 = Utils.myPrint(arraylist)
console.log(obj1)
console.log("==============================")

image-20241021185206996

hook动态加载的dex

这个和枚举所有已加载的类长得很相似 Java.enumerateLoadedClassesSync()

Java.enumerateClassLoaders() 这个是动态加载类

提出问题

报错提示没有找到 com.xiaojianbang.app.Dynamic 这个类

image-20241021190736252

但是可以看到 com.xiaojianbang.app.Dynamic 这个类是已经加载了的,这个就和类加载器有关了,回旋镖转回来了,因为类加载器不同,所以没有找到这个类

image-20241021190636980

解决方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
   // 获取所有的加载器
Java.enumerateClassLoaders({

// 每有一个加载器,执行这个方法
onMatch: function (loader) {
try {
// 在这个加载器中去寻找 dynamic 这个类,找到了就执行这个类,没找打就打印这个加载器
Java.classFactory.loader = loader;
var dynamic = Java.use("com.xiaojianbang.app.Dynamic");
console.log("dynamic: ", dynamic);
dynamic.sayHello.implementation = function () {
console.log("hook sayHello called");
return "demo"
}
} catch (error) {
console.log(loader);
}
},

// 全部加载完毕执行这个方法
onComplete: function () {}
});

image-20241021192603865

动态加载:也可以理解成按需加载,就是在代码执行过程中加载dex文件,就比如 dynamic类,它不是classes.dex文件中的类,是apk执行过程中动态加载的一个类。hook动态加载类的时候就容易碰到,枚举类中有这个方法,但是hook说找不到这个方法。如果这个dex文件加密了,jadx也不会反编译出来这个类。

image-20241021193047976

Java.enumerateClassLoaders()这个API要安卓7以上才能使用

让hook只在指定函数内生效(了解)

如果hook系统函数,调用很频繁的话是hook不成功的,直接崩溃了。

但是如果已经确定了范围,就可以用这种方式,其实也可以理解为hook嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
var mainActivity = Java.use("com.xiaojianbang.app.MainActivity");
var stringBuilder = Java.use("java.lang.StringBuilder")
mainActivity.generateAESKey.implementation = function () {
console.log("mainActivity generateAESKey called");
stringBuilder.toString.implementation = function () {
var result = this.toString();
console.log("generateAESKey result: " + result);
return result;
};
var result = this.generateAESKey.apply(this, arguments);
stringBuilder.toString.implementation = null; // 取消hook
return result;
};

image-20241021194022869

hook定位接口\抽象类的实现类

接口

如果定位代码找到接口了,单纯的hook接口是没有什么东西的,我们要去找接口的实现类

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
   var classes = Java.enumerateLoadedClassesSync();

// 循环所有已经加载的类
for (const index in classes) {
let className = classes[index];

// 过滤掉部分系统类
if (className.indexOf("com.xiaojianbang") === -1) continue;

// try一下防止有的类不在同一加载器,直接一个报错处理
try {
let clazz = Java.use(className);

// 获取接口数组
let resultArr = clazz.class.getInterfaces();
console.log("resultArr: ", resultArr);
if (resultArr.length === 0) continue;
for (let i = 0; i < resultArr.length; i++) {

// 比对这个接口,打印这个数组
if (resultArr[i].toString().indexOf("com.xiaojianbang.app.TestRegisterClass") !== -1) {
console.log(className, resultArr);
}
}
} catch (error) {
console.log("Didn't find class: " + className)
}
}

出了一点小bug,但是问题不大,还是得到了接口的实现类,对于不在同一个dex中的直接 try-catch 处理算了

image-20241022215523856

抽象类

和接口的方法极其相似,但是牢弟你要知道接口可以实现多个,但是抽象类只能实现一个,因此 getSuperclass() 获取到的不是一个数组,所以不需要循环了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
   var classes = Java.enumerateLoadedClassesSync();

// 循环所有已经加载的类
for (const index in classes) {
let className = classes[index];

// 过滤掉部分系统类
if (className.indexOf("com.xiaojianbang") === -1) continue;

// try一下防止有的类不在同一加载器,直接一个报错处理
try {
let clazz = Java.use(className);

// 获取抽象类
let resultArr = clazz.class.getSuperclass();
console.log("resultArr: ", className, resultArr);
if (resultArr == null) continue;
if (resultArr[i].toString().indexOf("com.xiaojianbang.app.TestRegisterClass") !== -1) {
console.log(className, resultArr);
}
} catch (error) {
// console.log("Didn't find class: " + className)
}
}

image-20241023110937621

END