앱 정보
이전 루팅 우회를 사용하여 애플리케이션을 실행하면 다음과 같이 문자열을 입력받는 부분과 VERIFY 버튼을 확인할 수 있다.
이때 문자열을 아무 값을 넣은 후 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() {
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
}
});
create.show();
}
verify() 함수를 분석하면 다음과 같은 정보를 알 수 있다.
- obj 변수에 입력값을 저장한 후 a 클래스의 a() 함수에 인자로 넘겨준다.
- 호출한 a() 함수의 반환 값이 True라면 "Success"가 타이틀인 박스를 생성하고 False라면 "Nope..."이 타이틀인 박스를 생성한다.
a()
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;
}
}
public class a { //sg.vantagepoint.a.a.a
public static byte[] a(byte[] bArr, byte[] bArr2) {
SecretKeySpec secretKeySpec = new SecretKeySpec(bArr, "AES/ECB/PKCS7Padding");
Cipher instance = Cipher.getInstance("AES");
instance.init(2, secretKeySpec);
return instance.doFinal(bArr2);
}
}
a() 함수를 분석하면 다음과 같은 정보를 알 수 있다.
- sg.vantagepoint.a.a.a() 함수에 첫 번째 인자로는 "8d127684cbc37c17616d806cf50473cc"의 바이트 값을 b() 함수로 넘긴 후의 반환 값을, 두 번째 인자로는 "5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc="를 디코딩한 값을 넘겨준다.
- sg.vantagepoint.a.a.a() 함수에서는 다음과 같이 진행된다.
- obj 변수에 입력값을 저장한 후 a 클래스의 a() 함수에 인자로 넘겨준다.
- SecretKeySpec(byte[]key,String Algorithm) 함수를 통해 넘어온 인자와 암호 알고리즘을 사용하여 SecretKeySpec을 생성한다.
- Cipher.getInstance(String Algorithm) 를 통해 해당 알고리즘의 인스턴스를 생성한다.
- public void init(int keysize, SecureRandom random)를 통해 난수를 생성한다.
- 마지막으로 doFinal(byte[] input)을 통해 마무리한 후 이를 반환해준다.
- 마지막으로 사용자가 입력한 값과 암호화된 문자열을 비교하여 같다면 True 다르다면 False를 반환한다.
Exploit
후킹을 이용한 암호화 키 확인
Exploit Code
console.log("Script loaded successfully");
Java.perform(function x() {
console.log("java perform function");
var sys = Java.use('java.lang.System');
sys.exit.implementation = function () {
console.log("success");
}
var crypto = Java.use("sg.vantagepoint.a.a");
crypto.a.implementation = function (a,b) {
var retval = this.a(a,b);
var passcode = "";
for (var i = 0 ; i < retval.length ; i++) {
passcode += String.fromCharCode(retval[i]);
}
console.log("key : " + passcode);
return retval;
}
});
스크립트를 확인해보면 다음과 같다.
- exit() 함수를 후킹 하여 루팅 감지에 걸려도 종료되지 않도록 한다.
- sg.vantagepoint.a.a.a() 함수를 후킹 한 후 넘어온 인자를 그대로 다시 함수로 넘겨 반환 값을 retval 변수에 저장한다.
- 반복문을 통해 retval 변수에 저장된 값을 다시 문자열 형태로 저장한다.
- 이후 해당 값을 출력하면 비밀키를 알아낼 수 있다.
스크립트 실행 후 아무 문자나 입력하여 VERIFY 버튼을 눌러주면 다음과 같이 비밀키가 출력되는 것을 확인할 수 있다.
마지막으로 획득한 비밀키를 입력 후 VERIFY 버튼을 눌러주면 "Success!"라는 타이틀의 메시지 박스를 확인할 수 있다.
Reference
'Android > Uncrackable' 카테고리의 다른 글
Uncrackable 2 (0) | 2021.11.08 |
---|---|
Uncrackable 1 - Root Detection (0) | 2021.11.01 |