准备环境、工具
Pixel(Magisk+Xposed/EdXposed)
这里要安利一下Magisk,它现在有个超强的Magisk Hide功能,能隐藏root状态,不过我还没研究过它的原理,如果后面有机会的话会稍微整理一下的。
UnCrackable-Level1
进入到存有UnCrackable的文件目录下,adb install UnCrackable-Level1.apk
把APK安装到手机上。打开应用之后,发现应用存在root检测,一旦发现了root状态,点击OK之后应用就会被kill。
可以反编译apk之后patch root检测部分的内容,重打包之后安装到手机里面绕过root检测,也可以用Magisk自带的Magisk Hide绕过。Magisk Hide设置方法如下:
当然第二张图里会不一样,我们需要选中UnCrackable-Level1.apk。
随便输个内容,按下VERIFY,发现出现了个弹窗,要我们再试一下。OK,可以看出我们应该从反编译apk开始入手,找找它这个验证的代码逻辑,看看能不能找到正确的字符串,或者看看有没有方法绕过这样的条件判断。
如果代码量过大的话,我们可以通过adb shell dumpsys activity top | findstr ACTIVITY
定位要找的界面的代码。像下面这样,就能定位到界面
不过用JEB看一下,发现这个代码量真的不是很大....用JEB打开MainActivity之后看到一个verify的函数没有经过混淆,看代码逻辑,判断的条件由a.a()函数传递,返回的是一个boolean类型的值。如果Level1真要让我们弹出Success的话,就没什么意思了,那就跟进a.a()函数里面看看。
发现a.a()中调用了加密的算法,之后将用户输入的值也就是arg5与加密后的结果进行比对,若比对成功的话,就说明我们输入的是正确的。明显这个crackme的意图不是要我们patch源代码然后得到弹窗Success, 而是获得flag, 也就是密文5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=
解密后的结果。
CTF解题法
可以使用动态和静态两种方法获得明文, 静态的方法参照文章OWASP Android Crackme Level 1, 文章的作者使用了openssl和硬编码在源码中的密钥8d127684cbc37c17616d806cf50473cc
解密5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=
。
通过代码我们可以知道AES算法使用了ECB加密模式, 且在CTF中AES-128加密算法的密钥一般为32位, 输入命令echo 5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc= | openssl enc -aes-128-ecb -base64 -d -nopad -K 8d127684cbc37c17616d806cf50473cc
可获取flagI want to believe
。
Xposed解法
感觉上面的方法太巧妙了, 下面会补充一个没那么巧妙且常用的方法——Xposed hook, 我一开始找到函数a.a(), 但是因为参数类型填写不正确的原因, 加上该函数实际上返回的是byte []
类型的数据, 所以不能得到正确的flag, Xposed模块param.getResult()的返回值又是String类型的数据,所以在这里我们需要处理数据转换的问题。具体的Xposed模块代码如下:
package com.example.unlock;
import android.util.Log;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XC_MethodReplacement;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
public class HookMain implements IXposedHookLoadPackage {
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
if(!lpparam.packageName.equals("owasp.mstg.uncrackable1")) //过滤包名
return;
XposedBridge.log("Loaded app: " + lpparam.packageName); //Hook a方法
try {
XposedHelpers.findAndHookMethod("sg.vantagepoint.a.a", lpparam.classLoader, "a", byte [].class, byte [].class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
}
protected void afterHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
// 转换数据类型
String flag = new String((byte []) param.getResult());
// 在这里使用Log.i()可能会输出失败, 最好还是用XposedBridge.log
XposedBridge.log("SECRET: " + flag);
}
});
} catch (Throwable e){
XposedBridge.log("hook failed");
XposedBridge.log(e);
}
}
}
安装Xposed模块并重启之后,随便输入之后提交,得到flagI want to believe
。
Frida解题法
还可以按照wp Level1 Writeup使用Frida hook代码获取flag。
Frida使用的过程中不能和Magisk Hide一起使用, 不然会出现Failed to spawn: unable to access zygote64 while preparing for app launch; try disabling Magisk Hide in case it is active
的报错。所以我们需要先关掉Magisk Hide(Magisk Manager > Settings >Magisk > Magisk Hide (关掉)), 并且还要在Frida脚本里面绕过root的检测。
因为之前没有接触过Frida, 也算是用这个实例来学习Frida的使用, 如果跟我一样不是很熟悉Frida, 可以配合Frida的官方文档一起食用, 文档中有Frida的js API使用说明, 下面的代码主要涉及的是调用Java函数的部分。作者在这里写的绕过root和debug的思路我觉得值得学习和思考, 有种听君一席话, 胜读十年书的感觉。当然, 这样讲还是有点夸张的, 不过真的很值得学习。
Java.perform(function () {
send("Starting hooks OWASP uncrackable1...");
/*
hook java.lang.System.exit, 使该函数只用来输出下面的字符串
避免了应用的检测机制导致应用退出, 使用该方法绕过Java层的root/debug检测
*/
var sysexit = Java.use("java.lang.System");
sysexit.exit.overload("int").implementation = function(var_0) {
send("java.lang.System.exit(I)V // We avoid exiting the application :)");
};
var aes_decrypt = Java.use("sg.vantagepoint.a.a");
aes_decrypt.a.overload("[B","[B").implementation = function(var_0,var_1) {
send("sg.vantagepoint.a.a.a([B[B)[B doFinal(enc) // AES/ECB/PKCS7Padding");
send("Key : " + var_0);
send("Encrypted : " + var_1);
/*
重载解密函数, 并获取其返回值, 因其类型为byte [],
js在调用Java方法之后只能返回一个对象, 而不是返回一个byte类型的数组
*/
var ret = this.a.overload("[B","[B").call(this,var_0,var_1);
send("Decrypted : " + ret);
var flag = "";
//将char类型转换为String类型
for (var i=0; i < ret.length; i++){
flag += String.fromCharCode(ret[i]);
}
send("Decrypted flag: " + flag);
return ret; //[B
};
var mainactivity = Java.use("sg.vantagepoint.uncrackable1.MainActivity");
mainactivity.onStart.overload().implementation = function() {
send("MainActivity.onStart() HIT!!!");
var ret = this.onStart.overload().call(this);
};
//var mainactivity = Java.use("sg.vantagepoint.uncrackable1.MainActivity");
mainactivity.onCreate.overload("android.os.Bundle").implementation = function(var_0) {
send("MainActivity.onCreate() HIT!!!");
var ret = this.onCreate.overload("android.os.Bundle").call(this,var_0);
};
var activity = Java.use("android.app.Activity");
activity.onCreate.overload("android.os.Bundle").implementation = function(var_0) {
send("Activity HIT!!!");
var ret = this.onCreate.overload("android.os.Bundle").call(this,var_0);
};
send("Hooks installed.");
});
这里小小啰嗦一下Frida的使用方法, 安装的话直接pip install frida-tools
, 还有下载适合自己设备的frida-server的版本, push进设备, 加上权限就行。下面是对frida-server进行的操作的操作:
$ adb shell
$ su
# cd /data/local/tmp # 进入frida-server的目录下
# ./frida-server &
之后重新开一个cmd窗口, 进入.js脚本, 也就是hook的脚本所在的目录下执行frida -U owasp.mstg.uncrackable1 -l cracker.js
(我的hook文件名为cracker.js), 就能获取flag:
结语
其实在使用Frida绕过应用检测的时候我想到一个问题, 我认为重载java.lang.System.exit
的方法只能绕过Java层调用的exit函数, 而不能绕过native层的函数。Java层,测试后已知hookjava.lang.System.exit
是不能阻止应用被系统层强制退出的。而native层的, 还没遇到一个这样的demo可以用来测试, 但是我猜测native层的还是需要hook native层的exit函数。如果跳出了这两个层级, 直接对系统层的exit进行注入, 会导致系统崩溃吗?如我的思考或文章出现了错误,希望各位大佬不吝赐教。
最后,附上CrackMe的下载链接。