前言
要求测试一个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在运行时,最先加载的activity
是cn.xxx.intelligentoffice.SplashActivity
追入到该activity
中
在activity
的生命周期里面,最先执行的方法是onCreate()
,在此处onCreate()
执行时,会执行startApp()
方法
在startApp()
方法里,先会对当前设备的ROOT环境进行检测,然后再检测是否是在模拟器中。如果都不是,则应该正常运行。
这里关键点在于CommonUtil.isRooted()
和VerifyDevice.verifyPhone()
的布尔值。
ROOT绕过
分析
追入到RootBeer
下的isRooted()
关于这里面方法的作用可以阅读这篇文章:http://lzonel.cn/3136.html
那么此时思路也就很简单了,只要让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
检测是被绕过了,接下来需要绕过模拟器检测
模拟器绕过
分析
回到cn.xxx.intelligentoffice.SplashActivity
中,并分析VerifyDevice.verifyPhone()
这里对模拟器的识别是通过下面这些方法识别的
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成功打开。
尝试抓包
尝试抓包,结果发现,好像还有证书需要解决一下。
将bp的证书放在/data/local/目录下
然后给它777的权限
在网上找了一个脚本
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
此时没有了证书报错
尝试使用burpsuite
抓包,也成功抓到了最后的https
协议的数据包。
关于参数加解密的问题,这里也就不做分析了(基本的数据结构都是没有变化,只是参数值被加密)。
总结
app测试的确挺麻烦的,还好这里没加商业壳,不然直接凉凉。
解决问题之前,烦的要命,解决完问题后,还是很开心的。
参考文章
# 使用frida绕过安卓ssl pinning
# 修改ROM实现自定义su命令-root检测通杀
# 实战frida-hook-安卓app