0%

逆向学习 0x05关键代码快速定位

关键代码快速定位

1
2
3
4
5
搜索不太靠谱,需要更精准更通用的方式来定位关键代码
Java:hook系统函数、沙盒
C:hook系统函数

通过hook一些底层的方法来定位

HashMap的应用场景一般是在提交参数的时候使用

ArrayList的应用场景是需要对数据进行排序的时候

hook系统函数

一、HashMap.put
1
2
3
4
5
6
7
8
Java.perform(function() {

var HashMap = Java.use('java.util.HashMap');
HashMap.put.implementation = function(a, b) {
console.log("put: ", a, b);
return this.put(a, b);
}
});

只要代码中使用了HashMap.put添加数据,就把数据打印出来

包有关键信息的湿鸽

image-20241012152611580

打印一下函数栈,看看是哪个函数调用的HashMap.put

利用 logOutPut(Log.getStackTraceString(new Throwable())); 打印函数栈

注意:因为有很多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
Java.perform(function() {

function showStacks() {
console.log(
Java.use("android.util.Log")
.getStackTraceString(
Java.use("java.lang.Throwable").$new()
)
);
}

var HashMap = Java.use('java.util.HashMap');
HashMap.put.implementation = function(a, b) {

// 打印username的堆栈信息
if (a.equals("username")) {
showStacks();
console.log("hashMap.put :", a, b);
}

console.log("put: ", a, b);
return this.put(a, b);
}
});

包有内容的,可以看到堆栈信息,有login方法和onClick方法,通过这样的方式来快速定位代码。被hook的函数带有 (Native Method) 的标志

image-20241012164606352

除了HashMap之外,LinkedHashMap中也有put方法,也是有可能有内容的

java.util.concurrent.ConcurrentHashMap中有有put方法

二、ArrayList.add
1
2
3
4
5
6
7
8
9
10
11
12
13
Java.perform(function() {
var addlist = Java.use("java.util.ArrayList");

// add方法有两个重载方法,将两个方法的都打印一下
addlist.add.overload("java.lang.Object").implementation = function(obj) {
console.log("add: ", obj);
return this.add(obj);
}
addlist.add.overload("int", "java.lang.Object").implementation = function(a, obj) {
console.log("add a + obj: ", a, obj);
return this.add(a, obj);
}
});

这个输出的内容多点多,在命令行中不太好看,输出成文件格式

1
fride -UF -l 1.js -o log.txt

如果失败了可能不是脚本的问题重启一下app试试,还是不行就换hook点

image-20241012195847768

找到需要比对的字符串之后再进行筛选

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
Java.perform(function() {

function showStacks() {
console.log(
Java.use("android.util.Log")
.getStackTraceString(
Java.use("java.lang.Throwable").$new()
)
);
}


// add方法有两个重载方法,将两个方法的都打印一下
var addlist = Java.use("java.util.ArrayList");
var addlist = Java.use("java.util.ArrayList");
addlist.add.overload("java.lang.Object").implementation = function(obj) {
console.log("add: ", obj);
if (obj.equals("username=13112345678")) {
showStacks();
console.log("addlist.add :", obj);
}
return this.add(obj);
}
//addlist.add.overload("int", "java.lang.Object").implementation = function(a, obj) {
// console.log("add a + obj: ", a, obj);
// return this.add(a, obj);
//}
});

一注入运行这个代码模拟器直接就是崩溃了,没有成功啊,但是思路就是这样的,打印出来的堆栈也是从调用add开始一直到login方法,在往上就是Android包中的了

除了add方法之外,还可能用到addAll、set方法

其他函数中也有add方法 Vector、LinkedList

三、TextUtils的isEmpty方法

这个方法是判断是否为空的方法,一般有输入的地方很有可能使用了这个方法来进行非空判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Java.perform(function() {

function showStacks() {
console.log(
Java.use("android.util.Log")
.getStackTraceString(
Java.use("java.lang.Throwable").$new()
)
);
}

var textUtils = Java.use("android.text.TextUtils");
textUtils.isEmpty.implementation = function(str) {
console.log("TextUtils.isEmpty: ", str);
// if (str == "13112345678") {
// showStacks();
// console.log("TextUtils.isEmpty :", str);
// }
return this.isEmpty(str);
}
});

这里输入的账号密码是会调用isEmpty的,这个时候再判断一下打印出堆栈即可

image-20241012205746668

不打印电话号了,没新意,打印下面的加密数据看看是在哪里调用的

image-20241012210144357

跟着方法名找过去,找到了解密返回数据的方法,说明这串加密数据,就是服务端返回的数据结果

image-20241012210555730

四、Log

一般发布的apk是不会有Log打印信息的,但是也不排除有的忘删掉了,或者开发不规范。不推荐一开始就使用。

1
2
3
4
5
6
var log = Java.use("android.util.Log");
log.w.overload("java.lang.String", "java.lang.String").implementation = function(tag, msg) {
showStacks();
console.log("Log.w: ", tag, msg);
return this.w(tag, msg);
}

image-20241012213241815

五、Collections的sort方法

这是一个排序方法,可能会用到,直接hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var collection = Java.use("java.util.Collections");
collection.sort.overload("java.util.List").implementation = function(list) {

try {
// 可以看到类型是List,输出结果包是[object, object],所有转换一下数据格式
// 防止转换失败,使用try catch
showStacks();
var a = Java.cast(list, Java.use("java.util.ArrayList"));
console.log("Collections.sort: ", a.toString());
} catch (error) {

}

return this.sort(list);
}

image-20241012214953389

六、JSONObject的put、getString方法等

这个是Java中的一个处理json数据的方法,但是现在用的比较少了,会用到外面的库的json数据处理方法。因为嘟嘟牛的抓包结果是想服务端发送了json数据的,所以就可以hook处理json的方法

put是将数据转换成json形式,getString是将json格式的数据还原

1
2
3
4
5
6
7
8
9
10
11
12
13
var jSONObject = Java.use("org.json.JSONObject");
jSONObject.put.overload("java.lang.String", "java.lang.Object").implementation = function(a, b) {
showStacks();
console.log("JSONObject.put: ", a, b);
return this.put(a, b);
}
jSONObject.getString.implementation = function(a) {
showStacks();
console.log("JSONObject.getString: ", a);
var result = this.getString(a);
console.log("result: ", result);
return result;
}

image-20241012221216949

七、Toast的show方法
1
2
3
4
5
6
var toast = Java,use("android.widget.Toast");
toast.show.implementation = function() {
showStacks();
console.log("Toast.show :");
return this.show()
}

app登录失败是有提示的,就可以试试hook Toast

image-20241013160757422

八、Base64
1
2
3
4
5
6
7
8
var base64 = Java.use("android.util.Base64");
base64.encodeToString.overload("[B", "int").implementation = function(a, b) {
showStacks();
console.log("Base64.encodeToString: ", a, b);
var result = this.encodeToString(a, b);
console.log("result: ", result);
return result;
}

内容包括字节数据,和加密后的数据,一般为了保存字节数据会进行base64编码

image-20241013162220492

九、String的getBytes、isEmpty方法

既然保存的是字节数据,那么就有可能使用了系统函数的转化字节的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var str = Java.use("java.lang.String");
str.getBytes.overload("java.lang.String").implementation = function(a) {
// showStacks();
var result = this.getBytes(a);
var newStr = str.$new(result);
console.log("getBytes: ", newStr);
return result;
}
str.getBytes.overload().implementation = function() {
// showStacks();
var result = this.getBytes();
var newStr = str.$new(result);
console.log("getBytes: ", newStr);
return result;
}

可以看到这些数据和协议还有密文有很大关系,加上数据也不多,直接打印出堆栈,都不需要过滤

image-20241013163441418

十、String的构造函数
1
2
3
4
5
6
7
8
9
var str = Java.use("java.lang.String");
str.$init.overload('[B').implementation = function(a) {
var result = this.$init(a);
console.log("str $init:")
return result;
}

// 按道理来说是这样的,但是这样运行会直接崩溃掉,没有例外,因为String的构造是通过StringFactory来构造的
// 这个就需要从源码中来分析了

java/lang/string.java

可以看到string的任何一种构造方法都是抛出了一个错误,提示使用 StringFactory

image-20241013170412420

搜索StringFactory找到StringFactory.java

在Java中使用的 new String() 方法其实在经过虚拟机的编译之后转换成了 StringFactory 中的方法,返回一个字符串,至于虚拟机怎么做到的,那就要看虚拟机的源码了,这个太深了,知道即可,所以要hook String的构造方法就需要hook StringFactory

image-20241013171014269

image-20241013171727574

String函数的构造替换成了StringFactory,所以应该去hook StringFactory中的方法

newStringFactoryString、newStringFactoryChars、newStringFactoryBytes、newStringFactoryBuffer、newStringFactoryBuilder

这是几个定义的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
	var stringFactory = Java.use("java.lang.StringFactory")
stringFactory.newStringFromString.implementation = function(a) {
showStacks();
var result = this.newStringFromString(a);
console.log("StringFactory.newStringFromString: ", result);
return result;
}
stringFactory.newStringFromChars.overload('[C').implementation = function(a) {
showStacks();
var result = this.newStringFromChars(a);
console.log("StringFactory.newStringFromChars: ", result);
return result;
}

// 先hook两个演示一下吧,能hook出东西进行

image-20241013172915181

十一、StringBuider、StringBuffer的hook

奖池还在累积

上面介绍了String转换的方法,String定义的方法,StringBuider就是String拼接的方法 +

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var sb = Java.use("java.lang.StringBuilder");
sb.toString.implementation = function() {
var result = this.toString();
if (result.indexOf("Encrypt") != -1) {
showStacks();
}
console.log("StringBuilder.toString: ", result);
return result;
}

var sb2 = Java.use("java.lang.StringBuffer");
sb2.toString.implementation = function() {
var result = this.toString();
if (result.indexOf("username") != -1) {
showStacks();
}
console.log("StringBuffer.toString: ", result);
return result;
}

image-20241013202318594

十二、findViewByld找控件id(打印R$id属性)

找到findViewByld的位置,但是运行会报错

image-20241013205232411

有时候在源码中看到的名字不一定是正确的,需要检验一下

比如 android.appcompat.app 包中的 AppCompatActivity 。

枚举所有已经加载的类

1
2
3
4
5
6
7
// 枚举所有已经加载的类

var classes = Java.enumerateLoadedClassesSync();
for (var i = 0; i < classes.length; i++) {
var className = classes[i];
console.log("className: " + className);
}

AppCompatActivity 类只有一个,和看到的包名不一样,以frida的类为准,这个时候再去反 AppCompatActivity 类

image-20241013204635738

你别说fittencode挺通人性,直接把正确的提示出来了

image-20241013205419763

但是又有问题了,如何找到控件id呢——去hook R文件

而且,id是R.java的内部类,不同通过 . 来指向要使用 R$id

需要使用

1
2
3
var btn_login_id = Java.use("com.dodonew.online.R$id").btn_login.value;

// .btn_login 是获取了这个对象,需要加上 .value 才下面的 0x7f…… 的十进制

image-20241013210122620

1
2
3
4
5
6
7
8
9
10
11
var btn_login_id = Java.use("com.dodonew.online.R$id").btn_login.value;
console.log("btn_login_id: " + btn_login_id);

var appCompatActivity = Java.use("android.support.v7.app.AppCompatActivity");
appCompatActivity.findViewById.implementation = function(a) {
if (a == btn_login_id) {
showStacks();
console.log("AppCompatActivity.findViewByle: ", a);
}
return this.findViewByle(a);
}

这个时候发现点击是没有反应的,原因就是界面已经创建好了,代码已经执行过了,还找什么控件啊,不执行这个方法了。这是一个生命周期的问题,想要有效果就需要换一个hook模式

image-20241013211210479

现在需要在界面创建之前进行注入操作

1
2
frida -U -f com.dodonew.online -l 1.js
frida -U -f com.dodonew.online -l 1.js --no-pause

image-20241013212229262

我勒个一波三折,学到不少

十三、setOnClickstener

hook这个函数,对比控件id,打印函数栈

1
2
3
4
5
6
7
8
9
10
11
12
   // 获取控件id
var btn_login_id = Java.use("com.dodonew.online.R$id").btn_login.value;
console.log("btn_login_id: " + btn_login_id);

var view = Java.use("android.view.View");
view.setOnClickListener.implementation = function(a) {
if (this.getId() == btn_login_id) {
showStacks();
console.log("View.setOnClickListener: ", this.getId());
}
return this.setOnClickListener(a);
}

和刚刚的是一个操作也需要加上 -f

image-20241013213248087

上面这两个方法不推荐有限使用,因为距离加密的方法太远了,而且还需要分析出来控件id进行过滤,还是比较麻烦的。

可hook点老多了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
加密库相关的hook
SSL相关的hook
socket相关的hook
SocketOutputStream
SocketInputStream
读写文件相关的 java.io.File
证书双向验证 Keystore.load 通常有证书和密码
安卓退出进程的方式

// 快速定位协议头加密 okhttp3的addHeader 方法
var okhttp_Builder = Java.use("okhttp3.Request$Builder");
okhttp_Builder.addHeader.implementation = function(a, b) {
showStacks();
return this.addHeader(a, b);
}


// 其实就是猜,猜app中使用了哪些方法,一般找一些底层的方法