frida hook 加固app

前言

要求测试一个app,说是没加固的,实际上也就是没加壳,像模拟器环境检测、ROOT检测等等都有。

环境搭建

可参考:https://jdr2021.github.io/2023/04/01/%E5%AE%9E%E6%88%98frida-hook-%E5%AE%89%E5%8D%93app/#frida%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE

注:记得要做流量转发

hook app

APP运行流程分析

AndroidManifest.xml文件中,直接搜索android.intent.action.MAIN

可以看见,app在运行时,最先加载的activitycn.xxx.intelligentoffice.SplashActivity

image

追入到该activity

activity的生命周期里面,最先执行的方法是onCreate(),在此处onCreate()执行时,会执行startApp()方法

image

startApp()方法里,先会对当前设备的ROOT环境进行检测,然后再检测是否是在模拟器中。如果都不是,则应该正常运行。

这里关键点在于CommonUtil.isRooted()VerifyDevice.verifyPhone()的布尔值。

image

ROOT绕过

分析

image

追入到RootBeer下的isRooted()

关于这里面方法的作用可以阅读这篇文章:http://lzonel.cn/3136.html

image

那么此时思路也就很简单了,只要让com.scottyab.rootbeer.RootBeer下的isRooted的值为false即可解决ROOT检测的问题。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function bypass() {  
Java.perform(function(){
var rootbeer = Java.use("com.scottyab.rootbeer.RootBeer");
rootbeer.isRooted.implementation = function(){
console.log("root hook");
console.log(this.isRooted())
return false;
}
});
}

function main(){
bypass();
}

setImmediate(main)

测试

运行hook脚本

frida -U -f cn.xxx.smartoffice -l hook.js --no-pause

可以看见ROOT检测是被绕过了,接下来需要绕过模拟器检测

image

模拟器绕过

分析

回到cn.xxx.intelligentoffice.SplashActivity中,并分析VerifyDevice.verifyPhone()

image

这里对模拟器的识别是通过下面这些方法识别的

notHasBlueTooth()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static boolean notHasBlueTooth() {  
infoStr.append("\n");
BluetoothAdapter defaultAdapter = BluetoothAdapter.getDefaultAdapter();
if (defaultAdapter == null) {
infoStr.append("\n蓝牙是否有效 :true");
return true;
} else if (TextUtils.isEmpty(defaultAdapter.getName())) {
infoStr.append("\n蓝牙是否无效 :true");
return true;
} else {
infoStr.append("\n蓝牙是否无效 :false");
return false;
}
}

notHasLightSensorManager(context)

1
2
3
4
5
6
7
8
9
private static boolean notHasLightSensorManager(Context context) {  
infoStr.append("\n");
if (((SensorManager) context.getSystemService("sensor")).getDefaultSensor(5) == null) {
infoStr.append("\n光感传感器是否不存在 :true");
return true;
}
infoStr.append("\n光感传感器是否不存在 :false");
return false;
}

ifFeatures()

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
private static boolean ifFeatures() {  
infoStr.append("\n");
infoStr.append("\n部分特诊信息打印 :");
StringBuilder sb = infoStr;
sb.append("\nBuild.FINGERPRINT :" + Build.FINGERPRINT);
StringBuilder sb2 = infoStr;
sb2.append("\nBuild.MODEL :" + Build.MODEL);
StringBuilder sb3 = infoStr;
sb3.append("\nBuild.MANUFACTURER :" + Build.MANUFACTURER);
StringBuilder sb4 = infoStr;
sb4.append("\nBuild.BRAND :" + Build.BRAND);
StringBuilder sb5 = infoStr;
sb5.append("\nBuild.DEVICE :" + Build.DEVICE);
StringBuilder sb6 = infoStr;
sb6.append("\nBuild.PRODUCT :" + Build.PRODUCT);
StringBuilder sb7 = infoStr;
sb7.append("\nBuild.TELEPHONY_SERVICE :" + ((TelephonyManager) ContextHolder.getContext().getSystemService("phone")).getNetworkOperatorName().toLowerCase());
boolean z = Build.FINGERPRINT.startsWith("generic") || Build.FINGERPRINT.startsWith("unknown") || Build.FINGERPRINT.toLowerCase().contains("vbox") || Build.FINGERPRINT.toLowerCase().contains("test-keys") || Build.MODEL.contains("google_sdk") || Build.MODEL.contains("Emulator") || Build.MODEL.contains("MuMu") || Build.MODEL.contains("Android SDK built for x86") || Build.MANUFACTURER.contains("Genymotion") || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) || "google_sdk".equals(Build.PRODUCT);
if (z) {
infoStr.append("\n");
infoStr.append("\n根据部分特征参数设备信息来判断是否为模拟器 :true");
} else {
infoStr.append("\n根据部分特征参数设备信息来判断是否为模拟器 :true");
infoStr.append("\n");
}
return z;
}

checkIsNotRealPhone()

1
2
3
4
5
6
7
8
9
10
11
12
private static boolean checkIsNotRealPhone() {  
infoStr.append("\n");
String readCpuInfo = readCpuInfo();
StringBuilder sb = infoStr;
sb.append("\nCUP型号 :" + readCpuInfo);
if (readCpuInfo.contains("intel") || readCpuInfo.contains("amd")) {
infoStr.append("\nCUP型号中有intel或amd :true");
return true;
}
infoStr.append("\nCUP型号中有intel或amd :false");
return false;
}

getCanCallPhone(context)

1
2
3
4
5
6
7
8
9
10
11
12
private static boolean getCanCallPhone(Context context) {  
infoStr.append("\n");
Intent intent = new Intent();
intent.setData(Uri.parse("tel:123456"));
intent.setAction("android.intent.action.DIAL");
if (intent.resolveActivity(context.getPackageManager()) != null) {
infoStr.append("\n是否能跳到拨号界面 :true");
} else {
infoStr.append("\n是否能跳到拨号界面 :false");
}
return intent.resolveActivity(context.getPackageManager()) != null;
}

EmulatorDetectUtil.isEmulator(context)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class EmulatorDetectUtil {  
public static native boolean detect();
public void throwNativeCrash() {
}
static {
System.loadLibrary("emulator_check");
}
public static boolean isEmulator(Context context) {
return AndroidDeviceIMEIUtil.isRunOnEmulator(context) || detect();
}

public static boolean isEmulator() {
return detect();
}
}

方法总结

方法名 作用/功能
notHasBlueTooth 检测蓝牙是否有效
notHasLightSensorManager 检测光学传感器是否有效
ifFeatures 通过特征信息(如Build.FINGERPRINT、Build.MODEL中是否有generic、vbox、test-keys等特征值)判断是否是模拟器
checkIsNotRealPhone 通过CPU信息判断当前环境是否是模拟器或虚拟机
getCanCallPhone 检测是否具备拨打电话的功能
EmulatorDetectUtil.isEmulator 一个开源的模拟器检测工具

这里也很好理解,模拟器不支持蓝牙连接,不支持调整灯光亮度,也不支持拨打电话。(也有可能不绝对)

代码

思路同上,将cn.xxxx.intelligentoffice.utils.VerifyDevice中的verifyPhone方法的返回值设置成false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function bypass() {  
Java.perform(function(){
var rootbeer = Java.use("com.scottyab.rootbeer.RootBeer");
rootbeer.isRooted.implementation = function(){
console.log("root hook");
console.log(this.isRooted())
return false;
}
var VerifyDevice = Java.use("cn.xxxx.intelligentoffice.utils.VerifyDevice");
VerifyDevice.verifyPhone.implementation = function (){
console.log("virmachine hook");
return false;
}
});
}
function main(){
bypass();
}
setImmediate(main)

测试

frida -U -f cn.xxx.smartoffice -l hook.js --no-pause

此时app成功打开。

image

尝试抓包

尝试抓包,结果发现,好像还有证书需要解决一下。

image

将bp的证书放在/data/local/目录下

然后给它777的权限

image

在网上找了一个脚本

https://blog.csdn.net/w1590191166/article/details/106308028/

最终版脚本

将这篇文章里的脚本同我的代码整合

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
function bypass() {  
Java.perform(function(){
var rootbeer = Java.use("com.scottyab.rootbeer.RootBeer");
rootbeer.isRooted.implementation = function(){
console.log("root hook");
console.log(this.isRooted())
return false;
}
var VerifyDevice = Java.use("cn.xxxx.intelligentoffice.utils.VerifyDevice");
VerifyDevice.verifyPhone.implementation = function (){
console.log("virmachine hook");
return false;
}
console.log("");
console.log("[.] Cert Pinning Bypass/Re-Pinning");
var CertificateFactory = Java.use("java.security.cert.CertificateFactory");
var FileInputStream = Java.use("java.io.FileInputStream");
var BufferedInputStream = Java.use("java.io.BufferedInputStream");
var X509Certificate = Java.use("java.security.cert.X509Certificate");
var KeyStore = Java.use("java.security.KeyStore");
var TrustManagerFactory = Java.use("javax.net.ssl.TrustManagerFactory");
var SSLContext = Java.use("javax.net.ssl.SSLContext");
console.log("[+] Loading our CA...")
var cf = CertificateFactory.getInstance("X.509");
try {
var fileInputStream = FileInputStream.$new("/data/local/cacert.cer");
}
catch(err) {
console.log("[o] " + err);
}
var bufferedInputStream = BufferedInputStream.$new(fileInputStream);
var ca = cf.generateCertificate(bufferedInputStream);
bufferedInputStream.close();
var certInfo = Java.cast(ca, X509Certificate);
console.log("[o] Our CA Info: " + certInfo.getSubjectDN());
console.log("[+] Creating a KeyStore for our CA...");
var keyStoreType = KeyStore.getDefaultType();
var keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);
console.log("[+] Creating a TrustManager that trusts the CA in our KeyStore...");
var tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
var tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
console.log("[+] Our TrustManager is ready...");
console.log("[+] Hijacking SSLContext methods now...")
console.log("[-] Waiting for the app to invoke SSLContext.init()...")
SSLContext.init.overload("[Ljavax.net.ssl.KeyManager;", "[Ljavax.net.ssl.TrustManager;", "java.security.SecureRandom").implementation = function(a,b,c) {
console.log("[o] App invoked javax.net.ssl.SSLContext.init...");
SSLContext.init.overload("[Ljavax.net.ssl.KeyManager;", "[Ljavax.net.ssl.TrustManager;", "java.security.SecureRandom").call(this, a, tmf.getTrustManagers(), c);
console.log("[+] SSLContext initialized with our custom TrustManager!");
}
});
}
function main(){
bypass();
}
setImmediate(main)

测试

运行脚本

frida -U -f cn.xxx.smartoffice -l hook.js --no-pause

此时没有了证书报错

image

尝试使用burpsuite抓包,也成功抓到了最后的https协议的数据包。

image

关于参数加解密的问题,这里也就不做分析了(基本的数据结构都是没有变化,只是参数值被加密)。

总结

app测试的确挺麻烦的,还好这里没加商业壳,不然直接凉凉。

解决问题之前,烦的要命,解决完问题后,还是很开心的。

参考文章

# 使用frida绕过安卓ssl pinning
# 修改ROM实现自定义su命令-root检测通杀
# 实战frida-hook-安卓app

Author: jdr
Link: https://jdr2021.github.io/2023/04/27/frida-hook-加固app/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.