一.前言
今天来和大家说一款app名叫DYM,我们选择版本v8.6.0,今天通过这个可以学到的知识点有绕过root检测,通过frida-rpc和自己编写一款小的app来调用so文件,然后再来破解登录接口
二.绕过root检测
我们进入app后发现,弹出一个检测到了root了
那绕过之前,我们得先了解一下root检测原理
root检测原来是
-使用java代码或者so代码---》检测root的特征
-一个手机一旦root了,在某些目录下,就会有对应的一些文件(su)
-app做root检测,就是在所有可能的目录下,检索有没有这些特征,如果有,app就不允许运行了
手机root了,会在那些目录下有特征呢?--》特征一般都是 [su]
"/system/bin/su", # 不同root软件,生成的位置不同,面具一般在这个目录下
"/data/local/bin/su",
"/data/local/su",
"/data/local/xbin/su",
"/sbin/su",
"/system/app/Superuser.apk",
"/system/bin/failsafe/su",
"/su/bin/su",
"/system/sd/xbin/su",
"/system/xbin/busybox",
"/system/xbin/daemonsu",
"/system/xbin/su",
"/system/sbin/su",
我们手机已经root了,会不会有这些特征
-/system/bin/su 路径下有su 可执行文件
-查看手机是否被root
adb shell # 进入手机
su # 只要有执行,其实手机就是被root了
cd /system/bin/ # 进入到/system/bin/ 目录下
ls |grep su #过滤当前目录下有没有su 可执行文件,发现有,就是被root了我们手机已经root了,我们要不让app检测到我们root了
-1 反编译apk,找到检测root的代码--》hook--》让它不执行
-2 直接修改特征文件-->把这些关键位置的 su --》改个名字
-app搜索不到相关特征,他就认为我们没有root
改名字,一劳永逸
- 把su --》su1--》以后获取root权限,就不是执行su ,而是执行 su1
当然改名字是最捞的方法可对,我们就要整其他的绕过检测方法
2.1 通用hook脚本绕过
github上有人开源了绕过的通用脚本,这里给出地址
这里我也贴出代码,方便大家使用
const commonPaths = [
"/data/local/bin/su",
"/data/local/su",
"/data/local/xbin/su",
"/dev/com.koushikdutta.superuser.daemon/",
"/sbin/su",
"/system/app/Superuser.apk",
"/system/bin/failsafe/su",
"/system/bin/su",
"/su/bin/su",
"/system/etc/init.d/99SuperSUDaemon",
"/system/sd/xbin/su",
"/system/xbin/busybox",
"/system/xbin/daemonsu",
"/system/xbin/su",
"/system/sbin/su",
"/vendor/bin/su",
"/cache/su",
"/data/su",
"/dev/su",
"/system/bin/.ext/su",
"/system/usr/we-need-root/su",
"/system/app/Kinguser.apk",
"/data/adb/magisk",
"/sbin/.magisk",
"/cache/.disable_magisk",
"/dev/.magisk.unblock",
"/cache/magisk.log",
"/data/adb/magisk.img",
"/data/adb/magisk.db",
"/data/adb/magisk_simple",
"/init.magisk.rc",
"/system/xbin/ku.sud",
"/data/adb/ksu",
"/data/adb/ksud"
];
const ROOTmanagementApp = [
"com.noshufou.android.su",
"com.noshufou.android.su.elite",
"eu.chainfire.supersu",
"com.koushikdutta.superuser",
"com.thirdparty.superuser",
"com.yellowes.su",
"com.koushikdutta.rommanager",
"com.koushikdutta.rommanager.license",
"com.dimonvideo.luckypatcher",
"com.chelpus.lackypatch",
"com.ramdroid.appquarantine",
"com.ramdroid.appquarantinepro",
"com.topjohnwu.magisk",
"me.weishu.kernelsu"
];
function stackTraceHere(isLog){
var Exception = Java.use('java.lang.Exception');
var Log = Java.use('android.util.Log');
var stackinfo = Log.getStackTraceString(Exception.$new())
if(isLog){
console.log(stackinfo)
}else{
return stackinfo
}
}
function stackTraceNativeHere(isLog){
var backtrace = Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress)
.join("\n\t");
console.log(backtrace)
}
function bypassJavaFileCheck(){
var UnixFileSystem = Java.use("java.io.UnixFileSystem")
UnixFileSystem.checkAccess.implementation = function(file,access){
var stack = stackTraceHere(false)
const filename = file.getAbsolutePath();
if (filename.indexOf("magisk") >= 0) {
console.log("Anti Root Detect - check file: " + filename)
return false;
}
if (commonPaths.indexOf(filename) >= 0) {
console.log("Anti Root Detect - check file: " + filename)
return false;
}
return this.checkAccess(file,access)
}
}
function bypassNativeFileCheck(){
var fopen = Module.findExportByName("libc.so","fopen")
Interceptor.attach(fopen,{
onEnter:function(args){
this.inputPath = args[0].readUtf8String()
},
onLeave:function(retval){
if(retval.toInt32() != 0){
if (commonPaths.indexOf(this.inputPath) >= 0) {
console.log("Anti Root Detect - fopen : " + this.inputPath)
retval.replace(ptr(0x0))
}
}
}
})
var access = Module.findExportByName("libc.so","access")
Interceptor.attach(access,{
onEnter:function(args){
this.inputPath = args[0].readUtf8String()
},
onLeave:function(retval){
if(retval.toInt32()==0){
if(commonPaths.indexOf(this.inputPath) >= 0){
console.log("Anti Root Detect - access : " + this.inputPath)
retval.replace(ptr(-1))
}
}
}
})
}
function setProp(){
var Build = Java.use("android.os.Build")
var TAGS = Build.class.getDeclaredField("TAGS")
TAGS.setAccessible(true)
TAGS.set(null,"release-keys")
var FINGERPRINT = Build.class.getDeclaredField("FINGERPRINT")
FINGERPRINT.setAccessible(true)
FINGERPRINT.set(null,"google/crosshatch/crosshatch:10/QQ3A.200805.001/6578210:user/release-keys")
// Build.deriveFingerprint.inplementation = function(){
// var ret = this.deriveFingerprint() //该函数无法通过反射调用
// console.log(ret)
// return ret
// }
var system_property_get = Module.findExportByName("libc.so", "__system_property_get")
Interceptor.attach(system_property_get,{
onEnter(args){
this.key = args[0].readCString()
this.ret = args[1]
},
onLeave(ret){
if(this.key == "ro.build.fingerprint"){
var tmp = "google/crosshatch/crosshatch:10/QQ3A.200805.001/6578210:user/release-keys"
var p = Memory.allocUtf8String(tmp)
Memory.copy(this.ret,p,tmp.length+1)
}
}
})
}
//android.app.PackageManager
function bypassRootAppCheck(){
var ApplicationPackageManager = Java.use("android.app.ApplicationPackageManager")
ApplicationPackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function(str,i){
// console.log(str)
if (ROOTmanagementApp.indexOf(str) >= 0) {
console.log("Anti Root Detect - check package : " + str)
str = "ashen.one.ye.not.found"
}
return this.getPackageInfo(str,i)
}
//shell pm check
}
function bypassShellCheck(){
var String = Java.use('java.lang.String')
var ProcessImpl = Java.use("java.lang.ProcessImpl")
ProcessImpl.start.implementation = function(cmdarray,env,dir,redirects,redirectErrorStream){
if(cmdarray[0] == "mount"){
console.log("Anti Root Detect - Shell : " + cmdarray.toString())
arguments[0] = Java.array('java.lang.String',[String.$new("")])
return ProcessImpl.start.apply(this,arguments)
}
if(cmdarray[0] == "getprop"){
console.log("Anti Root Detect - Shell : " + cmdarray.toString())
const prop = [
"ro.secure",
"ro.debuggable"
];
if(prop.indexOf(cmdarray[1]) >= 0){
arguments[0] = Java.array('java.lang.String',[String.$new("")])
return ProcessImpl.start.apply(this,arguments)
}
}
if(cmdarray[0].indexOf("which") >= 0){
const prop = [
"su"
];
if(prop.indexOf(cmdarray[1]) >= 0){
console.log("Anti Root Detect - Shell : " + cmdarray.toString())
arguments[0] = Java.array('java.lang.String',[String.$new("")])
return ProcessImpl.start.apply(this,arguments)
}
}
return ProcessImpl.start.apply(this,arguments)
}
}
console.log("Attach")
bypassNativeFileCheck()
bypassJavaFileCheck()
setProp()
bypassRootAppCheck()
bypassShellCheck()
终端执行 frida -U -f com.yoloho.dayima -l 1-通用绕过root检测脚本.js 就ok了
执行好了就能顺利进入啦
2.2 Shamiko模块绕过
有的一些app会不光会检测是否root,还会检测是否安装了magisk,这个时候我们就需要借助shamiko模块进行隐藏magisk,这里给出网站
我们安装好之后直接把下载好的zip推送到手机里(不要解压)
adb push 下载好的 /sdcard/Download/Shamiko.zip
放到这个目录比较好找,推送好之后,将zip刷入后重启
进入之后选择你要绕过的app就好,那如何绕过magisk检测呢
我们点击上面的隐藏Magisk应用,改成你想要取的名字,这个时候桌面上就会生成那个app,此时那个才是真正的magisk,原,magisk就会失效
2.3 hook检测点
这个我不太想讲,但是因为学习,所以和大家说一下
我们反编译搜索关键字
找到后我们进入
发现这里就是判断是不是模拟器和是否root,我们hook这两个方法让他们返回false就好了
这里给出hook代码
import frida
import sys
rdev = frida.get_remote_device()
pid = rdev.spawn(["com.yoloho.dayima"])
session = rdev.attach(pid)
scr = """
Java.perform(function () {
var MiscUtil = Java.use("com.yoloho.libcore.util.MiscUtil");
MiscUtil.isRooted.implementation = function () {
console.log('绕过')
return false;
}
MiscUtil.isSimulator.implementation = function (ctx) {
console.log('绕过')
return false;
}
});
"""
script = session.create_script(scr)
def on_message(message, data):
print(message, data)
script.on("message", on_message)
script.load()
rdev.resume(pid)
sys.stdin.read()
2.4 aosp刷机绕过
这里我们讲不来,得等到后面才能和大家讲 (其实我也不会)
什么是aosp?
Android是开源的,AOSP(Android Open Source Project)为Android开源项目的缩写,自己编译Android系统
例如鸿蒙就是使用了aosp源码,再改底层
而小米之前的miui则是没有修改源码 修改的是ui界面
后面会和大家说aosp的知识
三.抓包分析
抓包发现,url是 https://uicapi.yoloho.com/user/login
需要破解的参数有 device,password,sign
四.破解device
我们搜索device,发现太多了,于是我们就搜索请求头里的其他参数,然后看看附近有没有device
那我们搜索releasever看看
搜索发现五个,但是我们选择第四个,因为第四个是interceptor,拦截器里面的,而且每个请求都有device,那我们点进去,所以大概率就是拦截器生成的
发现就是在这里,发现外面是对他进行url编码,所以生成的是在里面那个函数,我们再次点进去
发现核心是setDeviceCode,我们点进去
发现主要加密逻辑就是在这里,大概意思就是str6是几个字符串拼接来的,然后再通过sha1加密,最后转成字符串,那我们是不是就可以通过hook messageDigest.update查看里面参数,然后打印参数的字符串形式,就知道str6是多少了,但是问题就是这个方法在很多地方都有打印,所以我们需要同时hook里面的大方法setDeviceCode,打印一下标记,在这个下面的不就是我们要的嘛,这里给出hook代码
Java.perform(function () {
var PeriodAPIV2 = Java.use("com.yoloho.controller.api.PeriodAPIV2");
var flag = false;
PeriodAPIV2.setDeviceCode.implementation = function () {
console.log("-------------------------setDeviceCode-------------------------")
flag = true;
return this.setDeviceCode();
}
var MessageDigest = Java.use("java.security.MessageDigest");
var ByteString = Java.use("com.android.okhttp.okio.ByteString");
// update是一个重载方法,所以要用overload
MessageDigest.update.overload("[B").implementation = function (data) {
if (flag) {
console.log(ByteString.of(data).utf8(), '\n' );
}
return this.update(data);
}
});
// frida -U -f com.yoloho.dayima -l hook_str6.js
发现这个就是了,前面那个三个看就是包名,所以肯定是第三个了,而且发现,还没有登录就生成了,所以说肯定是在刚开始就生成,接下来每次请求都一样
null 设备id号
2d1fc41fe54b945 安卓id 可以随机生成
cbluelinegooglearm64-v8abluelineRQ3A.211001.001abfarm-01362RQ3A.211001.001GooglePixel 3bluelinerelease-keysuserandroid-build 设备信息
3A:38:93:AD:E6:12 wifi的mac地址 可以伪造
02:00:00:00:00:00 蓝牙的mac地址 可以伪造
然后对这个拼接今昔sha1加密,这种一看就是标准库,所以肯定是标准的,大家也可以去验证一下
五.破解password
我们搜索 "password"
我们搜索到了四个,其中第二个是最像的,因为他的包名翻译就是登录,我们点进去
小惊喜,我们不光看到了password还看到了sign,一看就知道sign是把这些加在一起进行加密的,那我们就先看password
可以看到password传入了两个参数,一个是账号,一个是密码明文,我们点击进去
可以看到这个加密逻辑不就是aes嘛
其中: 待加密字符串是密码,key是账号进行md5加密取前16位字符串,iv则是固定字符串"yoloho_dayima!%_"
再点进去,发现返回的是base64的,不正是我们抓包所抓到的嘛
大家可以去hook一下判断和抓包的是否一样,我这里就不带大家hook了,我这里去看看md5里有没有加盐
发现只update了一次,没有加盐,至此password破解完成
六.破解sign
sign的位置刚才已经找到,这里我们退到之前的位置
sb拼接了:getDeviceCode 上节课破解的device
sb.append(PeriodAPIV2.getInstance().getDeviceCode());
sb拼接了 user/login
sb.append("user/login");
sb拼接了 手机号
sb.append(str);
sb拼接了 加密后的密码
sb.append(privateStrHandle);
把sb这个字符串--》调用encrypt_data 进行加密生成了 sign
arrayList.add(new BasicNameValuePair("sign", Crypt.encrypt_data(0L, sb.toString(), sb.length())));
第一个参数是0,第二个参数是sb,第三个参数是sb的长度
我们点进去,看看
发现是个jni方法,在libCrypt.so里面加密(记住这个,后面都要用得到),这里进去非常难读,今天我们介绍个新的知识点,frida-rpc,就通过这个sign来教大家使用,因为这个是今天的重点,所以我单独开一页教大家
七.frida-rpc
什么是frida-rpc?
frida 提供了一种跨平台的 rpc (远程过程调用)机制,通过 frida rpc 可以在主机和目标设备之间进行通信,并在目标设备上执行代码
Frida-RPC 是一个基于 Frida 框架的扩展,用于在不同进程之间进行远程过程调用(RPC)。Frida 是一个功能强大的动态插桩工具,它允许开发人员在运行时监控、修改和控制应用程序的行为。通过使用 Frida-RPC,您可以在不同的进程之间建立通信通道,使得您能够从一个进程中调用另一个进程的函数或方法,就好像它们都在同一个进程中一样
这有一张图方便大家理解
使用流程
流程
1 手机端启动 frida-server # 启动了
2 运行app # 手机端要运行app--》手机的内存中,存在 encrpyt_data函数
3 电脑端端口转发,运行脚本 # 端口转发
4 电脑端写脚本,远程调用
import frida
rdev = frida.get_remote_device()
session = rdev.attach("大姨妈")
scr = """
rpc.exports = {
//这里可以放很多键值,但是注意,名字任意 但不要带符号那些
encryptdata:function(j2,str,j3){ //写传入的参数
var res;
Java.perform(function () { //固定写法
// 包.类
var Crypt = Java.use("com.yoloho.libcore.util.Crypt"); //导入包和类
// 类中的方法
res = Crypt.encrypt_data(j2,str,j3); //执行类中的方法
});
return res;
}
}
"""
script = session.create_script(scr)
script.load()
# python 调用
# frida低版本使用
# sign = script.exports_sync.aa(0, "bcae572b84d20c385d6d9d2d7d9e645da29da3c0user/login18953675221WA89qByLlDeaGjmVNzXm/w==", 85)
# frida高版本使用
sign = script.exports.encryptdata(0, "bcae572b84d20c385d6d9d2d7d9e645da29da3c0user/login18953675221WA89qByLlDeaGjmVNzXm/w==", 85)
print(sign)
运行结果
八.编写app调用so
这里我截图教大家一步步做,首先我们进入AndroidStudio
新建一个这个项目,然后就下一步选择项目就行了,语言选择java
看到这个页面就算好了,然后我们刚刚之前看到jni 是libCrypto.so,那我们就去找到这个so文件
把这两个包中的so文件删除到只剩 libCrypto.so,然后把这两个文件夹复制到项目的lib下面
然后再在 build.gradle里的android里面加上
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
然后创建包 这个包名和你要调用的包名一致
然后创建一个类也要叫要调用地方的类名,然后再把这个代码c进去
再给主页面的的文字加上一个id 就叫sign
再在这个位置加上这些代码
TextView tv=findViewById(R.id.sign);
String str="64e6176e45397c5989504e76f98ecf2e63b2679euser/login15131255555A4rE0CKaCsUMlKpHjtL0AQ==";
String sign = Crypt.encrypt_data(0, str, str.length());
Log.e("sign=", sign);
tv.setText(sign);
大功告成,然后运行
大功告成
九.总结
今天知识点有点多,主要是给大家说了一下绕过root检测,frida-rpc和安卓编写调用so,这个其实后期用的不多,主要是做个了解,后期遇到难读的so我们会用unidbg,还差两个知识点就能讲到了,讲完那个我们的基础课程也算是结束了,大家就到了app逆向入门了,好了,到时候再说吧,晚安!
补充
有需要源码的看我主页签名名字私信我,有求必应