Frida逆向与协议分析-3

 frida逆向与协议分析第三部分,主要就是Android源码编译、沙箱等。

0x05 Android源码编译与Xposed魔改

 市面上绝大多数app都会对xposed框架进行特征检测,绕过的思路就是找到检测点(java层或者native层),然后hook修改返回结果,或者以硬编码、置零等方式来绕过检测逻辑。但是检测点很难找到(代码太多,或者以ollvm、vmp加固)。

 一个绝杀点就是:在源头消灭xposed特征,让你检测不到。本章就介绍如何魔改编译魔改xposed,从而绕过开源xposed检测工具Xposed Checker。

Android源码环境搭建

 为什么要编译Android源码?Xposed源码不就得了。这是因为Xposed的编译过程很依赖android的源码,因此,我们先对android源码进行编译。(需要12G运存+450G硬盘,电脑办不了,只能组装了)


 由于安卓源码引用了外部开源工具,例如OpenSSL,每一个子项目都是Git仓库,为了方便的管理这个Git仓库,安卓官方推出了相关的管理工具,名为repo。Repo封装了一系列的Git指令,可以方便的对多个Git仓库进行管理。

1
2
3
mkdir aosp712_r8 && cd aosp712_r8
repo init -u git://mirrors.ustc.edu.cn/aosp/platform/manifest -b android-7.1.2_r8
repo sync

repo sync只是下载了系统运行必须的代码,只能编译出运行Android Emulator(模拟器)的虚拟机系统,要是想让此系统安装到设备中,还需要下载设备对应的驱动(作用是在物理机系统上起到协调上层系统与底层硬件的通信)。

 编译完成后,编译出来的镜像是不全的,还需要下载BootLoader等关键的系统镜像,之后即可刷上自编译系统。

Xposed定制

模块 功能
XposedInstaller 用于下载和安装Xposed.zip刷机包。
XposedBridge java层的API提供者,调用Xposed相关的API时,首先调用XposedBridge中的函数,之后转发到Native方法。
Xposed 位于Native层的Xposed实际实现,是对Zygote的二次开发。
android_art 对art的二次开发,以提供对Xposed的支持。
XposedTools 负责编译和打包刷机的ZIP包。

 就是,将XposedInstaller安装到设备之后,XposedInstaller会下载由XposedTools打包的含有XposedBridge,Xposed,android_art的ZIP包,并将此ZIP包刷入到系统(放置与替换系统文件)。

 Xposed包的编译过程见书P137。

 之后,使用XposedChecker检测Xposed模块。其中有很多不同的检测项,例如:

(1)载入Xposed工具类检测项,是通过使用系统类加载器加载XposedHelper类,如果可以加载那么说明此系统中是有Xposed模块的。

(2)寻找特征动态链接库检测项,是通过查看/proc/self/maps文件,其中如果有XposedBridge字符串,那么说明此系统中有Xposed模块。

(3)检测Xposed安装情况检测项,是查看系统中已安装的App列表是否包含Xposed相关的App。

(4)环境变量特征字判断检测项,通过获得环境变量,查看环境变量中是否有XposedBridge。

 可以发现,大多数Xposed的特征都是字符串特征,所以可将Xposed检测字符串的点修改为其它字符串。步骤如下:

 (1) 修改XposedInstaller App的Xposed字符串特征,即修改整体包名以及prop配置文件相关字符串。根据书P144,可以修改所有xposed字符串变为xppsed,之后,修改配置文件与字符串硬编码的字符,例如AndroidManifest.xml,这里不再赘述,详见书P145左右。

 (2) 根据 (1) 的步骤修改XposedBridge,最终制作出XppsedBridge.jar。

 (3) 修改Xposed项目源代码,详见书P147。

 (4) 修改XposedTools工具的源码,保证编译过程中不报错。

 但是跟书修改完之后,始终无法检测到新编译的 xposed-v89-sdk25-arm64.zip,但是试了原代码编译的 xposed-v89-sdk25-arm64.zip,发现是可以检测到的。通过查看 xposedInstaller 源代码并调试,也未发现原因。为了不耽误时间,所以直接跳过此处的实验。耽误了两三周。

 本章最后还说明了基于自定义修改的 Xposed 框架编写 Xposed 模块的方式,见书 P150。本章大部分是 Xposed 魔改过检测,主要是针对字符串的检测,但是被 Xposed Hook 的函数,其 access_flags 属性变成了 native。

相关知识补充

 见link

Android 的平台架构如下所示:

image-20240115194321408

(1)Linux 内核。Android 平台的基础是 linux 内核,Android Runtime(ART)依靠 Linux 内核来执行底层功能,基于linux 内核让 Android 更安全并且可以拥有很多设备驱动。

(2)硬件抽象层(HAL)。HAL 向更高级别 Java API 框架显示设备硬件功能,其中每个模块都为特定类型的硬件组件实现一个界面,例如相机和蓝牙模块,当框架 API 要访问设备硬件时,Android 系统为该硬件组件加载库模块。

(3)Android Runtime。Android 5.0 之前 Android Runtime 为 Dalvik,之后为 ART。Dalvik 是 JIT (运行前转为机器码),ART 是 AOT (运行时转为机器码)。

(4)原生 C/C++ 库。许多核心 Android 系统组件和服务(例如 ART 和 HAL)需要以 C 和 C++ 编写的原生库。Android 平台提供 Java 框架 API 以向应用显示其中部分原生库的功能,我们可以通过 NDK 开发 Android 中的 C/C++ 库。

(5)Java API 框架。这些 API 形成创建 Android 应用所需的构建块。

一些文件类型:

文件类型 类型含义
dex Android 将所有的 class 文件打包形成一个 dex 文件,是 Dalvik 运行的程序。
odex 优化过的 dex 文件,apk 在安装时会进行验证和优化,通过 dexopt 生成 odex 文件,加快 apk 的响应时间。
oat android 私有 ELF 文件格式,有 dex2oat 处理生成,包含(原 dex 文件+dex 翻译的本地机器指令),是 ART 虚拟机使用的文件,可以直接加载。
vdex 包含 APK 的未压缩 DEX 代码,以及一些旨在加快验证速度的元数据

一些广泛的安卓版本:

版本号 特性
Android 2.2 支持已转换成 dex 格式的 android 应用,基于寄存器,指令执行更快,加载的是 odex 文件,采用 JIT 运行时编译。但是由于是 JIT,每次启动应用都需要重新编译。
Android 4.4 ART 和 AOT。ART 和 Dalvik 是共存的,用户可以在两者之间选择。
Android 5.0 ART 取代 Dalvik。AOT是一种运行前编译的策略,缺点:(1)应用安装和系统升级之后的应用优化比较耗时;(2)优化后的文件会占用额外的存储空间。
Android 7.0 考虑上面 AOT 的缺点,dex2oat 过程比较耗时且会占用额外的存储空间,Android 7.0 再次加入 JIT 形成AOT+JIT+解释器模式。混合编译模式综合了 AOT 和 JIT 的各种优点,使得应用在安装速度加快的同时,运行速度、存储空间和耗电量等指标都得到了优化。应用在安装的时候 dex 不会被编译,应用在运行时 dex 文件先通过解析器(Interpreter)后会被直接执行,与此同时,热点函数(Hot Code)会被识别并被 JIT 编译后存储在 jit code cache 中并生成 profile 文件以记录热点函数的信息,手机进入 IDLE(空闲) 的时候,系统会扫描 App 目录下的 profile 文件并执行 AOT 过程进行编译。

 Android 2.2 的 APP 运行图如下所示:

image-20240115204417368

 Android 5.0 的 APP 运行图如下所示:

image-20240115204957856

JIT 与 AOT 的区别:

 JIT 在每次运行程序的时候都需要对 odex 重新进行编译。AOT 是静态编译,应用在安装的时候会启动 dex2oat 过程把 dex 预编译成 ELF 文件,每次运行程序的时候不用重新编译。

JVM、Dalvik 和 ART 区别

 JVM:传统的 Java 虚拟机、基于栈、运行 class 文件。Dalvik,支持已转换成 dex 格式的 android 应用,基于寄存器,指令执行更快,加载的是 odex。ART,第一次安装时,将 dex 进行 Aot (预编译),字节码预先编译成机器码,生成可执行 oat 文件(ELF文件)。

Android 各版本 ClassLoader 加载 dex 时的 dexopt 过程:

image-20240115205940127

0x06 Android沙箱之加解密库“自吐”

 每个安卓应用都运行在独立的沙箱中,而本章介绍的沙箱指的是系统级的沙箱,即通过自定义系统源码编译特定系统,是得运行在自定义系统上的 App 行为都暴露在系统的监控下。

自吐沙箱的建立

 对于系统而言,App 的行为是没有隐私的。基于这种系统级沙箱从而监控 App 行为的思路,DexHunter、FART 等脱壳机从 ART 虚拟机层面对 App 进行内存数据的 dump,从而提出第一代、第二代(整体加固与函数加固)的解决方案。TinyTool 从内核中调用 JProbe(动态跟踪 Java 方法执行的工具,它是 Linux 内核提供的功能)来监控 syscall 系统调用,这样即使 App 应用使用静态编译的二进制文件,或者通过 svc 汇编指令在用户态直接进行系统调用,还可以打印出一份日志,来分析 App 的行为。

 除了基于系统源码的沙箱外,还有其它类型的沙箱,例如基于 Hook 类型的沙箱 r0capture,其虽然没有修改系统源码,但是基于 Hook 对系统收发包函数进行插桩,从而可以对应用层进行抓包。

 App 由于要依赖系统的 API,从而导致本身行为暴露在系统监控中。那么 App 如何抵抗沙箱分析?(1)App 尽可能少的减少系统 API 的调用;(2)关键函数的算法尽量不直接使用系统的加密库。

 本章基于 Hook 类型的沙箱,即 appmon,从而提出针对加密库进行分析的脚本,结合 Frida 开发自己的加密库沙箱。安卓提供了便利的加密封装库,我们可以直接通过 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// hookEvent.js
var jclazz = null;
var jobj = null;

function getObjClassName(obj){
if(!jclazz){
jclazz = Java.use("java.lang.Class");
}
if(!jobj){
jobj = Java.use("java.lang.Object");
}
return jclazz.getName.call(jobj.getClass.call(obj));
}

function watch(obj, mtdName){
var listener_name = getObjClassName(obj);
var target = Java.use(listener_name);
if(!target || !mtdName in target){
return;
}
// overload onClick function
target[mtdName].overloads.forEach(function(overload){
overload.implementation = function(){
console.log("[WatchEvent] " + mtdName + ": " + getObjClassName(this));
return this[mtdName].apply(this, arguments);
};
})
}

// hook View all onClick listener
function OnClickListener(){
Java.perform(function(){
// spawn 模式
Java.use("android.view.View").setOnClickListener.implementation = function(listener){
if(listener != null){
watch(listener, "onClick");
}
return this.setOnClickListener(listener);
};
// attach 模式
Java.choose("android.view.View$ListenerInfo", {
onMatch: function(instance){
instance = instance.mOnClickListener.value;
if(instance){
console.log("mOnClickListener name is :" + getObjClassName(instance));
watch(instance, "onClick");
}
},
onComplete: function(){}
})
})
}

setImmediate(OnClickListener);

 使用 frida,将 hookEvent.js 注入到 com.xiaojianbang.app,发现 JAVAMD5 按钮响应函数位于 com.xiaojianbang.app.MainActivity,之后使用 JADX 定位到 JAVAMD5 按钮的响应函数。JAVAMD5 使用了 java.security.MessageDigest 类中的函数,用于进行密码计算。主要包括 MessageDigest.getInstance()/update()/digest() 函数,由于每个函数可能存在多个重载,所以编写通用的可以 hook 任意函数所有重载的脚本(这里针对 MessageDigest.getInstance()),代码如下:

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
// hook.js
function hookMD5(targetClassMethod){
var delim = targetClassMethod.lastIndexOf(".");
if(delim === -1) return;
var targetClass = targetClassMethod.slice(0, delim);
var targetMethod = targetClassMethod.slice(delim + 1, targetClassMethod.length);

var hook = Java.use(targetClass);
var overloadCount = hook[targetMethod].overloads.length;
for(var i = 0; i < overloadCount; i++){
hook[targetMethod].overloads[i].implementation = function(){
// this and arguments
console.warn("\n*** entered " + targetClassMethod);
if(arguments.length >= 0){
for(var j = 0; j < arguments.length; j++){
console.log("arg[" + j + "]: " + arguments[j],'=>', JSON.stringify(arguments[j]));
}
}
var retval = this[targetMethod].apply(this, arguments);
console.log("\nretval: " + retval,'=>', JSON.stringify(retval));
// 打印调用栈
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
return retval;
}
}
}

function main(){
var targetClassMethod = "java.security.MessageDigest.getInstance";
hookMD5(targetClassMethod);
targetClassMethod = "java.security.MessageDigest.update";
hookMD5(targetClassMethod);
targetClassMethod = "java.security.MessageDigest.digest";
hookMD5(targetClassMethod);
}
setImmediate(main);

 结果如下:

image-20240116204627467

Appmon 沙箱是怎么做的呢?我们只关注功能本身,看针对 Hash 函数进行 trace 的脚本(appmon/scripts/Android/Crypto/Hash.js),注入后发现:

image-20240116205248420

 其识别算法时,并未通过勾取 getInstance() 来获取算法信息,而是在勾取 digest 函数时通过 getAlgorithm 获得算法的种类,且 data 总为空。下面分析一下它的代码:

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
// Hash.js

// 严格模式,对代码的解析和执行施加更严格的限制和规则
'use strict';

// 修改 byteArraytoHexString,将 map 方法修改为如下
var byteArraytoHexString = function(byteArray) {
if(!byteArray){return '11';}
var result = "";
for(var i = 0; i < byteArray.length; i++){
result += ('0' + (byteArray[i] & 0xFF).toString(16)).slice(-2);
}
return result;
}

var updateInput = function(input) {
if (input.length && input.length > 0) {
var normalized = byteArraytoHexString(input);
} else if (input.array) {
var normalized = byteArraytoHexString(input.array());
} else {
var normalized = input.toString();
}
return normalized;
}

Java.perform(function() {
var MessageDigest = Java.use("java.security.MessageDigest");
// 如果有 digest 函数
if(MessageDigest.digest){
MessageDigest.digest.overloads[0].implementation = function() {
var digest = this.digest.overloads[0].apply(this, arguments);

// 获取密码算法
var algorithm = this.getAlgorithm().toString();

// Payload 头
var send_data = {};
send_data.time = new Date();
send_data.txnType = 'Crypto';
send_data.lib = 'java.security.MessageDigest';
send_data.method = 'digest';
send_data.artifact = [];

// Payload 体
var data = {};
data.name = "Algorithm";
data.value = algorithm;
data.argSeq = 0;
send_data.artifact.push(data);

// Payload 体
var data = {};
data.name = "Digest";
data.value = byteArraytoHexString(digest);
data.argSeq = 0;
send_data.artifact.push(data);

send(JSON.stringify(send_data));
return digest;
}

MessageDigest.digest.overloads[1].implementation = function(input) {
// same as above
}
}

if (MessageDigest.update) {
MessageDigest.update.overloads[0].implementation = function(input) {
var send_data = {};
send_data.time = new Date();
send_data.txnType = 'Crypto';
send_data.lib = 'java.security.MessageDigest';
send_data.method = 'update';
send_data.artifact = [];
var data = {};
data.name = "Raw Data";
data.value = updateInput(input);
data.argSeq = 0;
send_data.artifact.push(data);
send(JSON.stringify(send_data));
return this.update.overloads[0].apply(this, arguments);
}

MessageDigest.update.overloads[1].implementation = function(input, offset, len) {
// same as above
}

MessageDigest.update.overloads[2].implementation = function(input) {
// same as above
}

MessageDigest.update.overloads[3].implementation = function(input) {
// same as above
}
}
});

 考虑到 hook 主要依赖于 Frida,Xposed 等工具,这类工具可能会由 app 检测出来,因此,我们打算直接从系统源码层面修改代码。虽然 hook 沙箱与源码沙箱实现方式不同,但是两者都是采取对源码插桩的方式实现的,只是 hook 是针对二进制的动态代码插桩,源码沙箱是针对源码的插桩。我们要实现源码沙箱,只需要针对目标函数内容进行修改即可。

 以 Android 7.1.2_r8 为例,生成对应的 idegen.jar,android.iml(包含源码导入 Android studio 时会被导入和派出的子目录),android.ipr(源码工程的具体配置、代码以及依赖的 lib)文件,之后直接用 android studio 打开 ipr 文件即可。

 我们直接修改 MessageDigest 的源码,但要解决 2 个问题:(1)用什么方式进行自吐,解决方法 a:日志打印,即调用 android.util.Log,但是无法通过 import 导入,因为会出现 cannot find symbol 的问题,可使用反射方式(运行时动态获取类的信息并操作对象)调用 Log 中的函数,但要处理反射可能带来的异常;(2)确定哪一个是重载函数,是否存在相互调用的情况,解决方法 b:首先用 Objection 确定 MessageDigest 类中存在的目标函数,找到所有重载后,直接源码分析每一个重载函数,如果某函数内没有再次调用其他重载函数,那么就要进行源码插桩;解决方法 c:方法 b 无法处理添加新的函数的问题,因此,运行 make update-api。具体见 P163。

 解决方法 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
// update
Class logClass = null;
try {
// load class
logClass = this.getClass().getClassLoader().loadClass("android.util.Log");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

Method loge = null;
try {
// get corresponding method
loge = logClass.getMethod("e", String.class, String.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
try {
// call method
loge.invoke(null, "wd2711", "input => " + inputString);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}

 ps:自己编译的镜像一般都有 root 权限(具有 su 指令),要想不要 root 权限,那么直接 lunch 选择 user 类型(直接打字 user,而不是选择标号)即可。

 最后,就可以建立一个 Hash 自吐的沙箱。由于我已经详细知晓步骤,所以,为了加快时间,就不进行实验了,对下面的章节也是这样,除非我认为这是很有必要做的实验。

crypto_filter_aosp 项目移植

 下面针对 com.xiaojianbang.app 关于 AES/DES/RSA 算法进行分析,使用 jadx 可以得到它们的源码(在此只列出 AES 的源码):

image-20240119210512045

 可以发现,控制加解密的类是 javax.crypto.Cipher,init 用于传递密钥和向量,update 用于更新加解密输入,doFinal 用于进行真正的加解密过程。本小节不进行相关的沙箱开发,而是使用一个项目 crypro_filter_aosp,它是一个监控 java 层加密算法的 ROM(只读存储),它的输出就是向目录中写文件,且只能监控一个 app。

 此项目是针对 nexux 6p android 6.0.1 的,如果想要复用就要做修改。此项目包含 6 个文件,修改后直接覆盖掉 android 8.1.0 的源码即可。要进行修改,就要对比 android 6.0.1 源码与项目代码,比较改了什么。其次,此项目在输出时将一次加解密过程输出一条日志(前面的沙箱在一次加解密过程中输出多条日志),这是因为,此项目增加了成员变量 jsoninfo,在 init 与 update 函数中,更新 jsoninfo 信息,在最终的 doFinal 运行时,把此变量输出到文件,并清空 jsoninfo。

 由于此项目添加了原来 android 源码中没有的文件,所以需要在 libcore 目录的 obenjdk_java_files.mk 中增加相应文件的全路径,添加完成后还需要执行 make update-api 更新系统 api。

ps:check_oom 一般是检测内存是否溢出的函数,在微软的区块链钱包中也有此函数。

0x07 Android沙箱开发之网络库与系统库“自吐”

 很多 App 都对抓包进行了相应的防御,例如反 wifi 代理、反 VPN 代理、服务器校验客户端/客户端校验服务器(CA 证书层面)。虽然工具 r0capture 从代码层面抓取数据包,绕过了以上这些应对中间人抓包的对抗方式,但是由于其依赖于 frida,因此很容易被检测到。

基于 r0capture 的源码沙箱网络库“自吐”

 要构建沙箱,首先要找到源码中关键代码的位置。在 TCP/IP 模型中,由于 App 是属于应用层,因此只能修改所使用的应用层协议类型、数据格式、传输端口、或者用 TCP/UDP 直接通信。基本没有 App 可以修改网络层内容,即使是 VPN app,也只是创建出新的网络接口,IP 还是 VPN server 分配的。目前,有很多网络通信框架通信,例如 Okhttp(访问网站)、Exoplayer(播放视频)、Glide(异步平滑图片滚动加载框架),这些框架底层还是使用系统 API 处理。基于此,app 采取多种手段防止应用层的抓包,例如,app 使用特定 API(Proxy.NO_PROXYSystem.getProperty("http.proxyHost") 等)检测来防止 wifi 代理,即使使用 VPN app 从网络层将数据流转发到抓包软件,也可以使用 getNetWorkCapbilities() 来检测网络接口,从而检测 VPN app。我们可以使用 objection 对 android.net.ConnectivityManager.getNetWorkCapbilities() 进行勾取,从而发现一些 VPN 使用的痕迹。

 App 还可以通过证书层面来检测抓包,例如客户端校验服务器的方式,即在客户端和服务器进行握手时,验证 CA 的 hash 值,来达到只与持有相同 CA 的服务器进行通信,而服务器只与持有特定 CA 的客户端进行交互

 由于协议通用性问题,即使在应用层做了很多防护手段,攻击者也可以绕过。因此开发者可能会使用小众协议甚至自研应用层协议(腾讯的 JceStruct 协议),即使数据流量被窃取,也无法得到有效信息。自研的协议可以很大发挥传输层功能,例如,(1)某厂商使用自建代理长连的网络方案,app 请求通过 CIP (Common industrial protocol,用于工业自动化领域的通信协议,提供了标准化的方式来相互通信)通道中的 TCP 子通道与长连服务器通信,长连服务器与业务服务器进行通信;(2)某厂商自研内核、算法、传输层网络库与服务端,此时使用沙箱也无法对 app 进行抓包。

 但是大多数 app 都是直接调用系统 API,我们只需要在应用层下层,对 socket 接口相关函数进行 hook,就可以抓到封装成 http 的应用数据,之后,这些数据使用 SSL 进行加密,并通过 socket 与服务器进行通信。下面是一个 app 的网络函数调用图(会话层 (SSL) -> 表示层 (HTTP) -> 应用层 (自定义视频流解密) -> 应用层 (播放解密后的流媒体)):

image-20240120105331069

 通过 hook socket 函数,所监听的上层数据可以分为加密/未加密两种类型,并针对多个应用层协议进行验证与测试。我们可以得出以下结论,如果数据未加密,那么如果是发送数据,那么一定会经过 java.net.SocketOutputStreamsocketWrite0() 函数,如果是接收数据,那么一定会经过java.net.SocketInputStreamsocketRead0() 函数。通过分析以下 r0capture 代码,从而了解关于未加密数据的 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
35
36
37
38
39
40
41
42
43
44
45
// overload socketWrite0
// 有参数函数的 overload
Java.use("java.net.SocketOutputStream").socketWrite0.overload('java.io.FileDescriptor', '[B', 'int', 'int').implementation = function (fd, bytearry, offset, byteCount) {
// 调用原函数
var result = this.socketWrite0(fd, bytearry, offset, byteCount);
// 进行信息记录
var message = {};
message["function"] = "HTTP_send";
message["ssl_session_id"] = "";
message["src_addr"] = ntohl(ipToNumber((this.socket.value.getLocalAddress().toString().split(":")[0]).split("/").pop()));
message["src_port"] = parseInt(this.socket.value.getLocalPort().toString());
// 需要注意的是,这里完全可以用 this.socket.toString 而不是 this.socket.value.getRemoteSocketAddress 来实现目的地址的获取,因为 Socket 对应的内容就是目的地址信息,这是通过 Objection 的插件 wallbreaker 查看 java.net.SocketOutputStream 对象的成员结构发现的
message["dst_addr"] = ntohl(ipToNumber((this.socket.value.getRemoteSocketAddress().toString().split(":")[0]).split("/").pop()));
message["dst_port"] = parseInt(this.socket.value.getRemoteSocketAddress().toString().split(":").pop());
// 打印调用栈
message["stack"] = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()).toString();
var ptr = Memory.alloc(byteCount);
// 将 bytearray 写入到 ptr,并将 ptr 与记录的信息一块发出去
for (var i = 0; i < byteCount; ++i) {
Memory.writeS8(ptr.add(i), bytearry[offset + i]);
}
send(message, Memory.readByteArray(ptr, byteCount));
return result;
}

Java.use("java.net.SocketInputStream").socketRead0.overload('java.io.FileDescriptor', '[B', 'int', 'int', 'int').implementation = function (fd, bytearry, offset, byteCount, timeout) {
var result = this.socketRead0(fd, bytearry, offset, byteCount, timeout);
var message = {};
message["function"] = "HTTP_recv";
message["ssl_session_id"] = "";
message["src_addr"] = ntohl(ipToNumber((this.socket.value.getRemoteSocketAddress().toString().split(":")[0]).split("/").pop()));
message["src_port"] = parseInt(this.socket.value.getRemoteSocketAddress().toString().split(":").pop());
message["dst_addr"] = ntohl(ipToNumber((this.socket.value.getLocalAddress().toString().split(":")[0]).split("/").pop()));
message["dst_port"] = parseInt(this.socket.value.getLocalPort());
message["stack"] = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()).toString();
if (result > 0) {
// bytearry 的数据长度并不是 byteCount,而是 scoketRead0 执行完后的返回值 result
var ptr = Memory.alloc(result);
for (var i = 0; i < result; ++i) {
Memory.writeS8(ptr.add(i), bytearry[offset + i]);
}
send(message, Memory.readByteArray(ptr, result))
}
return result;
}

 可以发现,输出的信息包括:地址,数据信息,函数调用栈(便于解密数据)。

 上面是 r0capture 使用 frida 的勾取,对于修改安卓源码的沙箱而言,(1)完全可以用 this.socket.toString 而不是 this.socket.value.getRemoteSocketAddress(r0capture 的做法) 来实现目的地址的获取,因为 Socket 对应的内容就是目的地址信息;(2)由于 socketWrite0socketRead0 都是 native 函数,其具体实现都是 native 层,为了避免对 native 层的代码(so 文件中)进行修改,所以对其上层函数(调用链为: socketRead -> socketRead0),也就是 socketRead 进行修改;(3)调用栈打印时,可以使用 Exception e = new Exception("wd2711SOCKETresponse"); e.printStackTrace(); 修改 Log.getStackTraceString(Throwable)(r0capture 的做法)。最后修改后的安卓源码为:

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
// socketInputStream.java
private int socketRead(FileDescriptor fd, byte b[], int off, int len, int timeout) throws IOException {
int result = socketRead0(fd, b, off, len, timeout);

if (result > 0) {
// b -> input
byte[] input = new byte[result];
System.arraycopy(b, off, input, 0, result);
String inputString = new String(input);
// 获得 Log.e 函数
Class logClass = null;
try {
logClass = this.getClass().getClassLoader().loadClass("android.util.Log");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Method loge = null;
try {
loge = logClass.getMethod("e", String.class, String.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
try {
// 打印目的地址
loge.invoke(null,"r0ysueSOCKETresponse","Socket is => " + this.socket.toString());
// 打印接收到的信息
loge.invoke(null,"r0ysueSOCKETresponse","buffer is => " + inputString);
// 打印函数调用栈
Exception e = new Exception("wd2711SOCKETresponse");
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
return result;
}

 以上是未加密数据的沙箱“自吐”,下面关注一波加密数据的沙箱“自吐”。要是加密的话,加密机制拉满了的话就是先 app 数据加密,之后再 SSL 加密。r0capture(基于 frida)的工具针对 SSL 加密的话(代码如下)主要是参考 frida_ssl_logger 在 native 层的 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// r0capture 对 SSL data 的自吐,这里没有打印调用栈信息,这是因为 frida 打印的 native 层(SSL_read 与 SSL_write)的调用栈信息可能不准确,所以在 java 层处理调用栈信息,并保存到全局变量中

var SSLstackwrite = null;
var SSLstackread = null;

// SSL_read 的 args[0] -> SSL 连接的上下文指针
// args[1] -> SSL_read 执行完后存放到 args[1],是 SSL 解密后的数据
Interceptor.attach(addresses["SSL_read"], {
onEnter: function(args) {
var ms = getPortAndAddresses(SSL_get_fd(args[0]), true);
ms["ssl_session_id"] = getSslSessionId(args[0]);
ms["function"] = "SSL_read";
ms["stack"] = SSLstackread;
// 构造 this.message 与 this.buf 结构
this.message = ms;
this.buf = args[1];
},
onLeave: function(ret) {
// 将 ret 转为 32bit 数字
ret |= 0;
if (ret <= 0) {
return;
}
// 展示 ssl_session_id + function name + SSL_read 后的结果 + SSL_read 返回的值
send(this.message, Memory.readByteArray(this.buf, ret));
}
});

// SSL_read 的 args[0] -> SSL 连接的上下文指针
// args[1] -> 将要经过 SSL 加密的数据放到 args[1] 中
Interceptor.attach(addresses["SSL_write"], {
onEnter: function(args) {
var ms = getPortAndAddresses(SSL_get_fd(args[0]), false);
ms["ssl_session_id"] = getSslSessionId(args[0]);
ms["function"] = "SSL_write";
ms["stack"] = SSLstackwrite;
send(message, Memory.readByteArray(args[1], parseInt(args[2])));
},
onLeave: function(ret) {}
});

Java.use("com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream").read.overload('[B', 'int', 'int').implementation = function (bytearry, int1, int2) {
var result = this.write(bytearry, int1, int2);
SSLstackwrite = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()).toString();
return result;
}

Java.use("com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream").write.overload('[B', 'int', 'int').implementation = function (bytearry, int1, int2) {
var result = this.write(bytearry, int1, int2);
SSLstackwrite = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()).toString();
return result;
}

 那我们咋找其他办法呢?首先,我们先快速定位 SSL 相关的函数,通过使用 Objection 搜索所有与 socket 相关的类(objection -g packagename explore; android class search socket),并利用 objection 在执行注入时 -c hook.txt,hook.txt 包含要执行的命令,从而 trace 这些类,从而快速定位到这些类在代码中的位置。最后定位到两个关键函数:com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream.read() (数据接收)与 com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream.write()(数据发送)的自吐函数。

 之后,我们就要寻思如何输出(1)数据内容;(2)地址信息;(3)函数调用栈。

 针对数据内容,进一步调研后发现,com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream.read() (数据接收)与 com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream.write()(数据发送)会调用 SSL 成员所在类的函数,即 ssl.readssl.write,跟踪之后发现 ssl 实际上是 sslWrapper 类型的的对象,因此,我们最终在 sslWrapper 中实现源码修改,从而完成沙箱的自吐。

 针对地址信息,使用 objection 的 WallBreaker 插件查看 sslWrapper 类的实例信息,发现其多个成员(例如 handshakeCallbacks 成员)的值与 socket 成员起到的作用一致(上文中使用 this.socket.value.getRemoteSocketAddress() 获取地址信息),因此此问题解决。

 针对打印调用栈的问题,与 未加密 数据的处理方法相同。最终,修改的源代码如下所示:

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
// SslWrapper.java
void write(FileDescriptor fd, byte[] buf, int offset, int len, int timeoutMillis) throws IOException {
if (len > 0) {
byte[] input = new byte[len];
System.arraycopy(buf, offset, input, 0, len);
String inputString = new String(input);

// 获取 Log.e 函数
Class logClass = null;
try {
logClass = this.getClass().getClassLoader().loadClass("android.util.Log");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Method loge = null;
try {
loge = logClass.getMethod("e", String.class, String.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
try {
// 打印目的地址信息
loge.invoke(null, "wd2711SSLrequest", "SSL is => " + this.handshakeCallbacks.toString());
// 打印 SSL 要加密的信息
loge.invoke(null, "wd2711SSLrequest", "buffer is => " + inputString);
// 打印函数调用栈
Exception e = new Exception("wd2711SSLrequest");
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
// 原始函数,调用 SSL_write native 层的代码
NativeCrypto.SSL_write(ssl, fd, handshakeCallbacks, buf, offset, len, timeoutMillis);
}

最终编译出不带 root 的系统,运行后 Log 会存放到某个 txt 中。这是因为,我们可以使用 crypto_filter_aosp,注入到 ROM,并监控我们想要的 app,之后,将 android 的 SslWrapper.java 修改为上述代码,之后,输出的 log 就可以存放到某个 txt 中。

使用沙箱辅助中间人抓包

 前面所提出的技术,例如 r0capture 使用 frida 进行 SSL 层的抓包,修改系统源码实现 SSL 层的抓包(沙箱),但是对于采用自定义 SSL 框架进行通信的方式来说就行不通了。这类自定义的 SSL 框架(webview,小程序,flutter)不是依赖系统进行收发数据的,而是通过 App 自己进行收发数据的。此时,就需要进行中间人抓包。

 但是有很多对抗中间人抓包的方式,例如安卓自己的 API (Proxy.NO_PROXY 对抗 Wifi 代理抓包,getNetWorkCapabilities 检测 VPN 代理),服务器校验客户端,客户端校验服务器的方式。

 如何对上述手段进行反制呢?我们可以通过修改系统源码,生成沙箱来进行反制。

HTTPS 抓包

 中间人抓包(只说对于 HTTPS):对于需要 CA 认证成功才能通信的协议,例如 HTTPS,如果我们使用简单的 wifi 代理与 vpn 代理 (系统设置)来设置中间人,那么在访问网页的时候就会显示您的链接不是私密链接警告。为了解决这个问题,我们可以使用其他代理软件,例如 charles,并将其相应的证书文件放到用户信任的凭据空间安卓系统信任的凭据空间中。这需要使用 mount 指令将系统分区设置为可写后,才能进行放置,也就是说,需要 root 权限。

 那么对于非 root 环境如何做呢?答案是:将 charles 证书文件转换为安卓系统能识别的形式,并放置到系统证书在源码中的对应目录下即可。

(1)将证书转为安卓系统能识别的形式。安装 charles 证书,这样的话证书会被放置在用户信任的凭据空间中,这样就变成了安卓系统能识别的形式,即xxx.0

(2)将证书放置到系统证书在源码下的对应目录。对应目录为/system/ca-certificates/google/files/,移动后确认证书所属用户/用户组/对应权限都与其他证书一致(ls -alit)即可。

 放到系统信任的凭据空间之后,即使是抓取 HTTPs 数据,也不会报警告。

对抗服务器校验客户端和 SSL pinning 的问题

 服务器校验客户端,指的是服务器在与客户端进行通信时,会在握手阶段验证客户端使用证书的公钥。但是当使用中间人进行抓包时,与服务器进行通信的是 charles 抓包软件,其使用的证书就不是服务端认证的证书文件。具体来说,手机安装 app 后,会一并安装 app 自带的证书,服务器就要验证这个证书。代理软件一般是没有这个 app 自带的证书的,所以服务器验证客户端就会失败

 绕过思路也比较简单:在 app 中找到相应的证书文件与对应密码(打开证书的密码),转为 P12 格式的证书,最终导入到代理软件中,以欺骗服务器

 客户端(手机、代理软件)想要与特定证书与服务器通信,就要用密码打开证书。开发者通常使用 KeyStore(InputStream, char[]) 函数使用密码打开证书,我们可以 hook 该函数,从而 dump 证书文件与相应密码。具体 hook 脚本如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// saveClientCer.js
Java.perform(function(){
var StringClass = Java.use("java.lang.String");
var KeyStore = Java.use("java.security.KeyStore");
// KeyStore.load(InputStream, char[])
KeyStore.load.overload("java.io.InputStream", "[C").implementation = function(arg0, arg1){
// 打印堆栈
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
// arg1 为证书密钥
console.log("KeyStore.load2:", arg0, arg1 ? StringClass.$new(arg1) : null);
if (arg0) {
// 将证书(加密的证书)保存到 /sdcard/Download/ 目录下
var file = Java.use("java.io.File").$new("/sdcard/Download/" + String(arg0) + ".p12");
var out = Java.use("java.io.FileOutputStream").$new(file);
var r;
while ((r = arg0.read(buffer)) > 0) {
out.write(buffer, 0, r);
}
out.close();
}
this.load(arg0, arg1);
};
})

 因此,我们可以使用 frida hook 到证书文件与密码,使用 keyStore Explorer 查看证书文件,并用密码进行解密,就可以查看证书的各种信息(书中说可以查看到证书私钥,我存疑)。之后,将证书文件导入到代理软件(例如 charles)(我理解应该也导入密码),就可以进行上网。我们也可以通过修改系统源码 hook 到证书文件与密码,具体而言是 hook java/security/KeyStore.java 中的 load 函数,如下所示:

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
public final void load(InputStream stream, char[] password) throws IOException, NoSuchAlgorithmException, CertificateException {
if (password != null) {
String inputPASSWORD = new String(password);
// import Log class
Class logClass = null;
try {
logClass = this.getClass().getClassLoader().loadClass("android.util.Log");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// import Log.e function
Method loge = null;
try {
loge = logClass.getMethod("e", String.class, String.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}

try {
// log password and call stack
loge.invoke(null, "KeyStoreLoad", "KeyStore load PASSWORD is => " + inputPASSWORD);
Exception e = new Exception("KeyStoreLoad");
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
// write stream into file
Date now = new Date();
String currentTime = String.valueOf(now.getTime());
FileOutputStream fos = new FileOutputStream("/sdcard/Download/" + inputPASSWORD + currentTime);
byte[] b = new byte[1024];
int length;
while ((length = stream.read(b)) > 0) {
fos.write(b, 0, length);
}
fos.flush();
fos.close();

}
keyStoreSpi.engineLoad(stream, password);
initialized = true;
}

 但是对证书的 dump 会报错,因此使用 Objection trace 与 keystore 相关的类(打开样本 apk 时调用 keystore 的哪些函数),从而找到其他更通用的函数。之后,使用 wallbreaker 查看函数中对象的数据结构,发现 java.security.KeyStore$PrivateKeyEntry 对象中存在证书信息。

 我们还了解到,如果知道证书链信息与 privatekey 之后,就可以将证书保存在文件中,具体是用 storeP12(PrivateKey sk, String p7, String p12Path, String p12Password)。其中,sk 指的是 privatekey,p7 指的是证书链,p12Path 是将证书导出到哪里,p12Password 指的是证书密码(用户指定)。此函数的在源码中的代码路径我并未找到。

 使用 Objection 对类 KeyStore$PrivateKeyEntry 中的函数进行 trace,来查看哪些函数在服务端验证客户端的流程中被调用,发现会调用 getPrivateKey() 与 getCertificateChain() 函数。之后,使用 frida 进行简单测试,具体而言,就是使用 js 重写了 KeyStore$PrivateKeyEntry.getPrivateKeyKeyStore$PrivateKeyEntry.getCertificateChain,然后注入到了样本中。

 与之前 hook KeyStore.load 相比,这种方法是 dump 成功的。KeyStore.load 中 dump 失败的原因不详。在 getPrivateKey 与 getCertificateChain 中进行 dump 时,我们可以自定义证书的密码,而在 KeyStore.load 中是不行的。

 最后,我们在源码中修改 getPrivateKey 与 getCertificateChain:

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
// KeyStore-PrivateEntry.java
public PrivateKey getPrivateKey() {
String p12Password = "wd2711";
Date now = new Date();
String currentTime = String.valueOf(now.getTime());
String p12Path = "/sdcard/Download/tmp" + currentTime + ".p12";

// 初始化证书链
X509Certificate p7X509 = (X509Certificate) chain[0];
Certificate[] mychain = new Certificate[]{p7X509};

// 生成一个空的 p12 证书
KeyStore myks = null;
try {
myks = KeyStore.getInstance("PKCS12", "BC");
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (NoSuchProviderException e) {
e.printStackTrace();
}
try {
myks.load(null, null);
} catch (IOException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
}

// 设置此证书的 privatekey 以及相应的自定义的 password,原来的密码可能不是这个,这里更改了
try {
myks.setKeyEntry("client", privKey, p12Password.toCharArray(), mychain);
} catch (KeyStoreException e) {
e.printStackTrace();
}

// 使用自定义的密码加密保存 p12 证书,证书已经保存在 p12Path 中
FileOutputStream fOut = null;
try {
fOut = new FileOutputStream(p12Path);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
try {
myks.store(fOut, p12Password.toCharArray());
} catch (KeyStoreException e) {
e.printStackTrace();
}
return privKey;
}

 以上解决服务端验证客户端时出现的问题,但是如果证书内容被硬编码到代码中,就可能绕过上述说的相关函数,例如 keyStore.load。

客户端校验服务器的过程与 SSL Pinning 有关。SSL pinning 意思就是客户端校验当前使用的证书是不是特定证书,其实是在 app 层面对证书进行校验。相关的 SSL pinning 绕过工具为:Objection的 SSL pinning bypass、FridaContainer、DroidSSLUnpinning,这些工具的原理就是对 app 中的证书校验代码部分进行 patch。

 对于系统源码来说,我们无法直接干预这个过程,但是由于 SSL pinning 的校验流程都会有打开证书、进行哈希这两个步骤,因此我们可以监控文件打开的操作,从而绕过 SSL pinning 校验。通过 objection hook Java.io.File 类的构造函数 $init,最后发现证书校验函数所在的类总是调用java.io.File.$init(File, String)这样一个函数重载,第二个参数是.0格式的证书名。因此,我们对java.io.File.$init(File, String)函数使用 frida 进行 hook,通过打印调用栈后发现,并不是所有证书都会进行校验。而对于进行校验的证书来说,其调用栈中都会存在X509TrustManagerExtensions.checkServerTrusted函数。

 因此,我们可以在源码的 file 类中打印调用栈中有X509TrustManagerExtensions.checkServerTrusted函数的调用栈信息,因此,我们就可以得到 apk 中进行 SSL pinning 校验的相关函数名,从而进行下一步操作。总的来说,绕过 SSL pinning 的基础是假定 app 会通过打开证书文件以及调用栈中会包含X509TrustManagerExtensions.checkServerTrusted函数,这其实是可以反绕过的。

风控对抗-设备信息篡改

 风险控制,即在电子支付或其他场景下保护甲方产品免受利益损失,其与黑产相对立。风控判断用户的真实性往往是通过用户是否使用真实的设备来进行,本节主要是使用沙箱的方式,不切换真实设备,通过修改源码来让 app 认为是两台设备。

 设备的相关信息(设备指纹/设备名称/设备型号)都是通过 android.os.Build 类中的成员值得到的。在安卓开发中,要获取此类中的值,直接访问该类即可。通过查看 android.os.Build 的构造源码,可以发现好多信息,例如 ID/DEVICE/BOARD 都是通过 getString(“ro.build.xxx”) 来得到的。

 通过跟踪 getString 函数,最终可以发现实际上 getString 调用了 native 层的 __system_property_get()函数,此函数位于 bionic/libc/bionic/system_properties.cpp中,属于 libc 基础库的内容。__system_property_get()已经是最底层的了,该函数通过与 property_service_socket 设备进行 socket 通信来获取具体的属性值。某些黑产通过修改 ROM(即修改__system_property_get()代码),来修改返回值。

 本节则是修改 getString 函数,修改方式很简单,就是修改源码即可,其是 android.os.Build 类中的函数。Build 类中大部分信息无法表示设备的唯一性,用来表示唯一性的有:IMEI(国际移动设备识别码)、IMSI(国际移动用户识别码)、Android_id、SN

 IMEI 是设备的唯一标识,由 15 位数字组成。开发者需要用 TelephonyManager.getDeviceID/getImei来获得,当然需要在 androidManifest.xml 中声明一些权限,我们可以在 frameworks/base/telephony/java/android/teltephony/TelephonyManager.java中修改这两个函数的返回值,从而让开发者分不清两个设备。

 IMSI 是移动网络中区分不同用户的识别码,其存储在 SIM 卡中,由 15 位数字组成。IMSI 由移动国家号码(MCC)、移动网络号码(MNC)、移动用户识别号码(MSIN)链接而成。中国的 MCC 为 460,中国移动的 MNC 是 00。开发者需要用 TelephonyManager.getSubscriberId来获得,也需要在 androidManifest.xml 中声明一些权限。

 Android_id 是设备第一次启动时产生与存储的 64 bit 数,也叫做 SSAID(Settings.Secure.ANDROID_ID)。此值只有在设备被刷机或者恢复出厂设置时才会被修改。在 android 8 及以上版本,每一个 app 在第一次安装时都会根据 app 签名 + 设备信息生成针对于 app 的 android_id。因此,不同的 app 所拿到的 android_id 是不一样的。不同于 IMEI 与 IMSI,拿到 android_id 不需要任何权限。获取代码如下:

1
2
3
import android.provider.Settings;
String android_id = Settings.Secure.getString(getContentResolver(),
Settings.Secure.ANDROID_ID);

 看一下 Settings.Secure.getString 的源码,并修改它,让它返回自定义的值。

 SN(serial number)就是 build 类中的 SERIAL 成员,它是手机生产厂商提供的设备序列号,是为了验证产品的合法而存在的,其格式由生产厂商自定义。也需要在 androidManifest.xml 中声明一些权限,通过 Build.getSerial(android 8-10)来获取 SN 号,还可以在 adb shell 中以 getprop ro.serialno获取。

真实黑产不可能这么简单

 总结一下,如果 app 想实现不被系统底层窥探,则要尽量将所有关键功能交由自身应用实现,减少对系统的依赖,例如自实现虚拟机,不依赖于 art 虚拟机解析指令;自定义 openssl 库,而不是简单的使用系统 api。

留言

© 2024 wd-z711

⬆︎TOP