Android | Uncrackable Lv 1
Frida Android Rooting Detection Bypass, Uncrackable1
루팅 탐지 우회
루팅 탐지 우회에서 진단할 APK 파일은 OWASP에서 배포한 UnCrackable-Level1.apk 파일이다.
의도적으로 취약하게 만든 파일로 진단 공부할 때 용이하게 이용된다.
파일을 다운받고 CMD를 다운로드 받은 경로로 이동한다.
C:\Users\users>adb install UnCrackable-Level1.apk
Performing Streamed Install
Success
adb 명령어를 통해 단말기에 다운받은 파일을 install 해주고 Success가 나오면 단말기에는 정상적으로 다운이 된 것을 알 수 있다.
해당 파일을 실행시키면 아래와 같이 Root detected! 다이얼로그가 나오고 OK 버튼을 누르면 프로세스가 종료되는 것을 알 수 있다.
Frida를 이용하려면 진단할 APK 파일이 어떤 클래스에서 탐지를 하는지 파악해야 하기에 이전에 설치한 jadx를 이용해 디컴파일을 한다.
루팅 탐지 코드 분석
소스코드 > sg.vantagepoint > uncrackable1 > MainActivity를 보면 onCreate함수가 있는데 모바일에서는 MainActivity > onCreate가 제일 실행되는 Main이라 볼 수 있다.
@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);
}
- c.a()
public static boolean a() {
for (String str : System.getenv("PATH").split(":")) {
if (new File(str, "su").exists()) {
return true;
}
}
return false;
}
환경변수 PATH를 가져오는데 루팅하면 su라는 바이너리 파일이 생성되기에 PATH에 su가 있으면 루팅되어 있음을 감지한다.
- c.b()
public static boolean b() {
String str = Build.TAGS;
return str != null && str.contains("test-keys");
}
Bulid.TAGS의 값을 가져오는데 기본 값으로는 release-keys로 되어 있지만 루팅하게 되면 해당 값이 test-keys로 변경되기에 이를 감지하는 것이다.
- c.c()
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;
}
str은 루팅 시 사용되는 apk와 파일을 기반으로 루팅을 감지하는 것이다.
C 객체의 a(), b(), c() 셋 중 하나라도 해당된다면 위 처럼 루팅을 감지하는 것으로 되어 있습니다.
if (c.a() || c.b() || c.c()) {
a("Root detected!");
}
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();
}
a()함수를 보면 커스텀 다이얼로그를 출력하게 되는데 OK 버튼을 누르면 System.exit(0);을 통해 프로그램을 종료시키는 것을 알 수 있다.
if문의 모든 값을 false로 바꾸는 것보단 System.exit(0)를 후킹하여 종료되지 않게 하는게 편할 것을 예상됩니다.
우선, 단말기에서 frida-server를 실행시키고 아래의 둘 중 하나의 후킹 스크립트를 작성해 우회가 가능하다.
Hooking
- exit() hook
console.log("[+] System Hooking");
Java.perform(function() {
var hook = Java.use("java.lang.System");
hook.exit.implementation = function () {
console.log("[+] Hooking System exit");
}
});
- return value hook
console.log("[+] System Hooking");
Java.perform(function() {
var hook = Java.use("sg.vantagepoint.a.c");
hook.a.implementation = function () {
console.log("[+] Hooking a()");
return false;
}
hook.b.implementation = function () {
console.log("[+] Hooking b()");
return false;
}
hook.c.implementation = function () {
console.log("[+] Hooking c()");
return false;
}
});
frida -U -f 'process name' -l 'script'
Verify
이후 OK 버튼을 클릭하면 꺼지지 않고 대기 중인 것을 알 수 있다. 그리고 EditText가 보이는 데 VERIFY 버튼을 클릭하면 아래처럼 다이얼로그가 나온다.
Verify 코드 분석
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)가 True면 넘어갈 수 있을 것으로 보인다. obj는 우리가 EditText에 입력한 값이 됩니다.
a.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));
}
b() 메소드를 통한 8d127684cbc37c17616d806cf50473cc 반환 값과 5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=를 base64로 디코딩한 값을 sg.vatagepoint.a.a.a() 호출하여 나온 값을 bArr에 저장한다.
이 값을 우리의 입력한 값과 비교하여 참인지 거짓인지 return 한다.
sg.vatagepoint.a.a.a()
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 방식으로 decrypt하는 함수다.
bArr와 우리의 입력 값을 비교하기에 bArr가 무엇인지 스크립트를 짜면 된다.
Hooking
// Secret String.js
console.log("[+] Secret String");
Java.perform(function() {
var secret = Java.use("sg.vantagepoint.a.a");
secret.a.implementation = function(arg1, arg2) {
console.log("[+] Hooking sg.vatagepoint.a.a");
var retval = this.a(arg1, arg2);
var secret_str = "";
for (var i = 0; i < retval.length; i++) {
secret_str += String.fromCharCode(retval[i]);
}
console.log("[+] Secret String :", secret_str);
return retval;
}
});
Secret String을 단말기에서 EditText에 넣고 Verify 버튼을 누르면 성공했다는 다이얼로그가 출력된다.