https://mas.owasp.org/crackmes/
MAS Crackmes - OWASP Mobile Application Security
MAS Crackmes Welcome to the MAS Crackmes aka. UnCrackable Apps, a collection of mobile reverse engineering challenges. These challenges are used as examples throughout the OWASP MASTG. Of course, you can also solve them for fun.
mas.owasp.org
안드로이드 해킹 공부를 위해 알아보던 중, Uncrackable이라는 게 있어서 풀이를 적어보도록 하겠다.
[루팅 탐지 우회]

루팅된 블루스택으로 들어갈 시, 위와 같이 루팅 탐지에 걸리는 것을 확인할 수 있다. frida를 사용하기 위해선 이 루트 권한이 필요하기 때문에 루팅 탐지 우회 방법에 대해 고민해보자
package sg.vantagepoint.uncrackable1;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import owasp.mstg.uncrackable1.R;
import sg.vantagepoint.a.b;
import sg.vantagepoint.a.c;
/* loaded from: classes.dex */
public class MainActivity extends Activity {
private void a(String str) {
AlertDialog create = new AlertDialog.Builder(this).create();
create.setTitle(str);
create.setMessage("This is unacceptable. The app is now going to exit.");
create.setButton(-3, "OK", new DialogInterface.OnClickListener() { // from class: sg.vantagepoint.uncrackable1.MainActivity.1
@Override // android.content.DialogInterface.OnClickListener
public void onClick(DialogInterface dialogInterface, int i) {
System.exit(0);
}
});
create.setCancelable(false);
create.show();
}
@Override // android.app.Activity
protected void onCreate(Bundle bundle) {
if (c.a() || c.b() || c.c()) {
a("Root detected!");
}
if (b.a(getApplicationContext())) {
a("App is debuggable!");
}
super.onCreate(bundle);
setContentView(R.layout.activity_main);
}
public void verify(View view) {
String str;
String obj = ((EditText) findViewById(R.id.edit_text)).getText().toString();
AlertDialog create = new AlertDialog.Builder(this).create();
if (a.a(obj)) {
create.setTitle("Success!");
str = "This is the correct secret.";
} else {
create.setTitle("Nope...");
str = "That's not it. Try again.";
}
create.setMessage(str);
create.setButton(-3, "OK", new DialogInterface.OnClickListener() { // from class: sg.vantagepoint.uncrackable1.MainActivity.2
@Override // android.content.DialogInterface.OnClickListener
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
}
});
create.show();
}
}
onCreate()를 보면, c.a(), c.b(), c.c()로 루팅 탐지가 이루어지고 있는 것을 알 수 있다.
package sg.vantagepoint.a;
import android.os.Build;
import java.io.File;
/* loaded from: classes.dex */
public class c {
public static boolean a() {
for (String str : System.getenv("PATH").split(":")) {
if (new File(str, "su").exists()) {
return true;
}
}
return false;
}
public static boolean b() {
String str = Build.TAGS;
return str != null && str.contains("test-keys");
}
public static boolean c() {
for (String str : 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/"}) {
if (new File(str).exists()) {
return true;
}
}
return false;
}
}
c.a(), c.b(), c.c()는 위와 같이 루팅 탐지가 이루어지고 있다. frida로 위 메서드가 실행될 때, return 값을 false로 고정하여 우회할 수 있다.
Java.perform(function () {
var C = Java.use("sg.vantagepoint.a.c");
C.a.implementation = function () {
console.log("[+] Hooked c.a() - Returning false");
return false;
};
C.b.implementation = function () {
console.log("[+] Hooked c.b() - Returning false");
return false;
};
C.c.implementation = function () {
console.log("[+] Hooked c.c() - Returning false");
return false;
};
});
위와 같이 작성하여
frida -U -f owasp.mstg.uncrackable1 -l .\exploit.js
앱 실행시 바로 작동하도록 위와 같이 명령어를 주었을 때,

루팅 탐지 경고 문구가 뜨지 않는 것을 확인할 수 있다.
이제 secret string을 구하여 보자.
[secret string 획득]
public void verify(View view) {
String str;
String obj = ((EditText) findViewById(R.id.edit_text)).getText().toString();
AlertDialog create = new AlertDialog.Builder(this).create();
if (a.a(obj)) {
create.setTitle("Success!");
str = "This is the correct secret.";
} else {
create.setTitle("Nope...");
str = "That's not it. Try again.";
}
create.setMessage(str);
create.setButton(-3, "OK", new DialogInterface.OnClickListener() { // from class: sg.vantagepoint.uncrackable1.MainActivity.2
@Override // android.content.DialogInterface.OnClickListener
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
}
});
create.show();
}
위 a.a(obj)에서 우리가 입력한 값이 secret string인지 검증하는 과정이 이루어진다.
package sg.vantagepoint.uncrackable1;
import android.util.Base64;
import android.util.Log;
/* loaded from: classes.dex */
public class a {
public static boolean a(String str) {
byte[] bArr;
byte[] bArr2 = new byte[0];
try {
bArr = sg.vantagepoint.a.a.a(b("8d127684cbc37c17616d806cf50473cc"), Base64.decode("5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", 0));
} catch (Exception e) {
Log.d("CodeCheck", "AES error:" + e.getMessage());
bArr = bArr2;
}
return str.equals(new String(bArr));
}
public static byte[] b(String str) {
int length = str.length();
byte[] bArr = new byte[length / 2];
for (int i = 0; i < length; i += 2) {
bArr[i / 2] = (byte) ((Character.digit(str.charAt(i), 16) << 4) + Character.digit(str.charAt(i + 1), 16));
}
return bArr;
}
}
a.a(obj)는 위와 같다.
8d127684cbc37c17616d806cf50473cc
위와 같이 키 값을 가지고 있으며,
5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=
위와 같이 암호화된 값을 가지고 있는 것을 알 수 있다.
package sg.vantagepoint.a;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
/* loaded from: classes.dex */
public class a {
public static byte[] a(byte[] bArr, byte[] bArr2) {
SecretKeySpec secretKeySpec = new SecretKeySpec(bArr, "AES/ECB/PKCS7Padding");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(2, secretKeySpec);
return cipher.doFinal(bArr2);
}
}
위 클래스를 통해 AES를 사용하는 것을 알 수 있다. 이를 통해 복호화를 직접 수행할 수도 있다.
from Crypto.Cipher import AES
from base64 import b64decode
key = bytes.fromhex("8d127684cbc37c17616d806cf50473cc")
cipher_text = b64decode("5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=")
cipher = AES.new(key, AES.MODE_ECB)
plaintext = cipher.decrypt(cipher_text)
print("복호화된 값(패스워드):", plaintext.decode('utf-8'))
위와 같이 python 코드를 통해
복호화된 값(패스워드): I want to believe
위와 같이 secret string을 구하였다.
frida를 사용하면 복호화했을 때, 그 값을 가져오는 방법을 생각해볼 수 있다.
Java.perform(function () {
var C = Java.use("sg.vantagepoint.a.c");
C.a.implementation = function () {
console.log("[+] Hooked c.a() - Returning false");
return false;
};
C.b.implementation = function () {
console.log("[+] Hooked c.b() - Returning false");
return false;
};
C.c.implementation = function () {
console.log("[+] Hooked c.c() - Returning false");
return false;
};
var A = Java.use("sg.vantagepoint.a.a");
A.a.implementation = function (arg1, arg2) {
var secret = this.a(arg1, arg2);
console.log("[+] Secret: " + secret);
var secretMessage = '';
for(var i=0; i<secret.length; i++) {
secretMessage += String.fromCharCode(secret[i]);
}
console.log("[+] Secret Message: " + secretMessage);
return secret;
};
});
루팅 탐지 우회부터 secret string을 가져오는 스크립트는 위와 같다.
[Android Emulator 5554::owasp.mstg.uncrackable1 ]-> [+] Hooked c.a() - Returning false
[+] Hooked c.b() - Returning false
[+] Hooked c.c() - Returning false
[+] Secret: 73,32,119,97,110,116,32,116,111,32,98,101,108,105,101,118,101
[+] Secret Message: I want to believe
위와 같이 secret message를 구할 수 있다.

짜잔