安卓逆向方面好像没有类似vulnhub的网站,有大量的靶机练习渗透测试。目前我找到了XCTF有逆向和移动端的题目,和 OWASP MSTG CTF项目。先从MSTG开始做完吧!
CTF项目的github在点击这里。不过由于项目较大,github经常会出现下载失败的情况,像我就下载了一个上午,50m的带宽下载速度只有20kb/s,速度极其感人。不过我已经上传到优快云了,有需要的小伙伴可以直接下载。
运行
将UnCrackable-Level1.apk安装到安卓设备。
adb install UnCrackable-Level1.apk
打开运行,存在root检测,由于我的模拟器是已经root了的,所以点击OK,程序它就自己退出了。
有两种方法可以解决这个问题:
- 关掉root权限
- 绕过root检测
第一种方法显然不符合我气质,那么就开始进行检测绕过。
反编译静态分析
我使用jeb进行反编译,编译后是smali汇编代码,按tab可以将smali汇编转成Java伪代码。
.uncrackable1.MainActivity代码部分
package sg.vantagepoint.uncrackable1;
import android.app.Activity;
import android.app.AlertDialog$Builder;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface$OnClickListener;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.View;
import sg.vantagepoint.a.b;
import sg.vantagepoint.a.c;
public class MainActivity extends Activity {
public MainActivity() {
super();
}
private void a(String arg4) {
AlertDialog v0 = new AlertDialog$Builder(((Context)this)).create();
v0.setTitle(((CharSequence)arg4));
v0.setMessage("This is unacceptable. The app is now going to exit.");
v0.setButton(-3, "OK", new DialogInterface$OnClickListener() {
public void onClick(DialogInterface arg1, int arg2) {
System.exit(0);
}
});
v0.setCancelable(false);
v0.show();
}
protected void onCreate(Bundle arg2) {
if((c.a()) || (c.b()) || (c.c())) {
this.a("Root detected!");
}
if(b.a(this.getApplicationContext())) {
this.a("App is debuggable!");
}
super.onCreate(arg2);
this.setContentView(0x7F030000);
}
public void verify(View arg4) {
String v4 = this.findViewById(0x7F020001).getText().toString();
AlertDialog v0 = new AlertDialog$Builder(((Context)this)).create();
if(a.a(v4)) {
v0.setTitle("Success!");
v4 = "This is the correct secret.";
}
else {
v0.setTitle("Nope...");
v4 = "That\'s not it. Try again.";
}
v0.setMessage(((CharSequence)v4));
v0.setButton(-3, "OK", new DialogInterface$OnClickListener() {
public void onClick(DialogInterface arg1, int arg2) {
arg1.dismiss();
}
});
v0.show();
}
}
程序首先运行onCreate函数,从onCreate一段一段分析看它干了什么。
首先,如果c类中的a,b,c方法中有一个条件满足,那么它就会进入MainActivity类的a方法,并传入"Root detected!"字符串。
这就是程序一开始打开时的情况,所以我们先看看c类中的a、b、c方法和MainActivity.a方法分别做了些啥。
a类的代码如下:
package sg.vantagepoint.a;
import android.os.Build;
import java.io.File;
public class c {
public static boolean a() {
String[] v0 = System.getenv("PATH").split(":");
int v1 = v0.length;
int v3;
for(v3 = 0; v3 < v1; ++v3) {
if(new File(v0[v3], "su").exists()) {
return 1;
}
}
return 0;
}
public static boolean b() {
String v0 = Build.TAGS;
if(v0 != null && (v0.contains("test-keys"))) {
return 1;
}
return 0;
}
public static boolean c() {
String[] v0 = new String[]{"/system/app/Superuser.apk", "/system/xbin/daemonsu", "/system/etc/init.d/99SuperSUDaemon", "/system/bin/.ext/.su", "/system/etc/.has_su_daemon", "/system/etc/.installed_su_daemon", "/dev/com.koushikdutta.superuser.daemon/"};
int v1 = v0.length;
int v3;
for(v3 = 0; v3 < v1; ++v3) {
if(new File(v0[v3]).exists()) {
return 1;
}
}
return 0;
}
}
- a方法检测路径中有没有存在su这个文件,如果存在就判断为设备已经被root了
- b方法检查Build.TAGS中是否存在test-keys,如果存在就判断设备已经被root了
- c方法是在检测一系列文件,如果有一个被找到就判断设备已经被root。
很显然,c类中使用3中方式对root进行检测。
MainActivity.a方法代码
private void a(String arg4) {
AlertDialog v0 = new AlertDialog$Builder(((Context)this)).create();
v0.setTitle(((CharSequence)arg4));
v0.setMessage("This is unacceptable. The app is now going to exit.");
v0.setButton(-3, "OK", new DialogInterface$OnClickListener() {
public void onClick(DialogInterface arg1, int arg2) {
System.exit(0);
}
});
v0.setCancelable(false);
v0.show();
}
调用了此方法,函数会弹出对话框,并显示传入的字符串内容,如果点击OK,程序将退出,System.exit(0)。
绕过root 检测
回到onCreate中,经过静态分析,有两种办法绕过root检测。
- 使用动态调试的方式,查看c类中a、b、c三个方法中哪一个方法返回了1,选择hook修改返回值或者修改smali代码重新打包,绕过root检测。
- 修改MainActivity.a函数,让其不执行System.exit(0),而只是return void。需要修改smali代码并重新打包。
目前选择第二种方法,因为它是最简单的,只需要将System.exit(0)对应的smali汇编代码注释掉即可。但由于出于学习目的,后面文章我还会附上第一种方法的做法。
使用apktool将apk进行反编译。
apktool d UnCrackable-Level1.apk -o uncrackable_dissas
smali代码位于
uncrackable_dissas/smali/sg/vantagepoint/
可以看出和jeb反编译的目录是一样的。
接下来要找smali代码的注入点,我发现其实这一步是有技巧的,而且非常有用,这个项目比较小,所以找的比较快。如果是比较大的项目,没有一定技巧是没有那么容易就能找到注入点的。
首先我们需要确定要修改的smali代码在哪个文件里,主要针对含有匿名内部类的Java文件而言。MainActivity方法被反编译成MainActivity.smali、MainActivity$ 1.smali、MainActivity$ 2.smali。这里MainActivity$ 1.smali、MainActivity$ 2.smali这些都是匿名内部类的smali代码文件,由于没有名字,所以编译后只能用$XXX来区分。
然后使用vscode打开的smali文件和jeb打开的Java代码进行上下文比对,就能找到注入点。(ps:推荐使用vscode打开smali,下载smali插件即可完美编辑)。
将41行进行注释,那么调用MainActivity.a方法后,点击确定也不会退出程序,就可以进行绕过了。
重新进行打包
apktool b uncrackable_dissas -o modified_uncracjable.apk
然后进行签名,就可以安装运行在android设备上。
关于签名我写了一键签名工具,直接拖入apk位置就可以自动完成签名。
点击了ok,程序也没有退出,root检测成功绕过!
拿到FLAG!
接下来在jeb中找到关键函数,verify函数很可疑,并且有Sucess提示。
关键是a.a()内部,让其返回值为真就可以。
进入a.a函数看看。
package sg.vantagepoint.uncrackable1;
import android.util.Base64;
import android.util.Log;
public class a {
public static boolean a(String arg5) {
byte[] v0_2;
String v0 = "8d127684cbc37c17616d806cf50473cc";
byte[] v1 = Base64.decode("5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", 0);
byte[] v2 = new byte[0];
try {
v0_2 = sg.vantagepoint.a.a.a(a.b(v0), v1);
}
catch(Exception v0_1) {
Log.d("CodeCheck", "AES error:" + v0_1.getMessage());
v0_2 = v2;
}
return arg5.equals(new String(v0_2));
}
public static byte[] b(String arg7) {
int v0 = arg7.length();
byte[] v1 = new byte[v0 / 2];
int v2;
for(v2 = 0; v2 < v0; v2 += 2) {
v1[v2 / 2] = ((byte)((Character.digit(arg7.charAt(v2), 16) << 4) + Character.digit(arg7.charAt(v2 + 1), 16)));
}
return v1;
}
}
函数内部实现比较简单,主要是算法构成。
v1是字符串5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=
进行base64解密后的byte数组。
v0也是一个常量。
这里的关键点在于v0_2,这个是由加密最密集的地方产生的数据,而它也是最后和传入的arg5进行比较的,所以我会把关注点放在产生v0_2的函数:sg.vantagepoint.a.a.a()
而它的函数实现如下:
package sg.vantagepoint.a;
import java.security.Key;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class a {
public static byte[] a(byte[] arg2, byte[] arg3) {
SecretKeySpec v0 = new SecretKeySpec(arg2, "AES/ECB/PKCS7Padding");
Cipher v2 = Cipher.getInstance("AES");
v2.init(2, ((Key)v0));
return v2.doFinal(arg3);
}
}
这里的方法有两个参数、arg2和arg3。
首先使用arg2随机生成一个key秘钥,再使用秘钥对arg3进行AES进行对称加密,外部参数和它进行对比。
而这里由于传入的v0、v1都是常量,我们需要hook此方法,把这个固定的加密返回值打印出来即可,这就是crack的思路。
这里使用frida工具进行解密,有关frida的介绍和文档在这里。
贴上脚本:
Java.perform(function(){
//hook the target class
var aes = Java.use("sg.vantagepoint.a.a");
//hook the function inside the class
aes.a.implementation = function(var0, var1){
//call itself
var decrypt = this.a(var0,var1);
var flag = "";
for (var i =0; i < decrypt.length; i++){
flag += String.fromCharCode(decrypt[i]);
}
console.log(flag);
return decrypt;
}
});
找到运行的apk名称,
执行,
frida -U -f owap.mstg.uncrackable1 -l exploit.js --no-pause
-U 代表进入USB设备
-f 代表指定应用程序文件
-l 指定脚本
–no-pause 在应用程序启动后,自动加载到主进程中去。
当我们运行上面的命令行后,模拟器或手机APP会自动加载,
随便在app中输入
查看脚本打印…
ok,脚本将flag打印出来了。
拿到flag!
总结
- 在进行静态分析的时候,对一个问题进行多角度思考,有没有更好的办法能够解决目前的问题
- 找到smali注入点的技巧,熟能生巧,经验活。