루팅 탐지 우회하는 스크립트를 인젝션 해보기 위해 웹 보안기구인 OWSAP에서 배포하는 앱을 하나 다운로드 받아 설치할 것이다. 이 앱은 보안 공부를 위해 일부러 취약하게 만든 앱이다.
https://github.com/OWASP/owasp-mstg/blob/master/Crackmes/Android/Level_01/UnCrackable-Level1.apk
위 링크에서 apk 파일을 다운로드 받아 nox_adb install
명령어를 사용하여 녹스 앱 플레이어에 설치한다.
시작하기 전에 위 캡처화면처럼 녹스 앱 플레이어 설정에서 ROOT켜기를 체크하여 루팅된 기기 상태로 만든다.
그 후에 설치한 앱을 실행하면 루팅된 기기에서는 사용할 수 없다는 경고창이 뜨고, OK 버튼을 클릭하면 종료된다. 이제부터 이 루팅된 기기에서 사용할 수 없도록 루팅 탐지 로직을 우회하여 루팅된 기기에서도 실행할 수 있게 프리다로 스크립트를 인젝션 해보자.
먼저 앱 패키지 이름부터 알아보자. 아래 캡처화면을 보면 나오듯이 패키지 이름은 owasp.mstg.uncrackable1
이다.
루팅 탐지 로직을 확인하기 위해 jadx
로 앱 파일을 디컴파일 할 것이다. 루팅 탐지 로직이 어디에 있는지 찾기 위해 가장 먼저 시도해 볼 만한 것은 위에서 앱을 실행했을 때 나왔던 에러 메세지를 검색해보는 것이다.
Root detected!
메세지를 검색해본 결과 MainActivity
클래스 내에서 사용되고 있었고, 조건문을 확인해 보니 c.a()
, c.b()
, c.c()
중 하나라도 참이면 a()
메소드에 루팅 탐지 메세지 문자열을 인자 값으로 전달하면서 호출하는 것을 알 수 있다. 이 a()
메소드의 로직을 확인해 보자.
AlertDialog
클래스를 통해 알림 대화창을 만들고, 여기에 OnClickListener
인스턴스를 생성하며 OK 버튼을 클릭했을 때 앱 프로세스가 종료되도록 구현되어 있음을 알 수 있다. 따라서, 이 onClick()
메소드를 재작성하여 앱 프로세스를 종료하는 로직 대신에 로그를 출력하도록 스크립트를 인젝션해보자. onClick()
을 재작성하기 위해 이 메소드를 구현하고 있는 클래스를 가지고 와야하는데, 이 메소드가 OnClickListener
생성자가 호출될 때 구현하고 있으므로 이 생성자가 속한 android.content.DialogInterface
라는 것을 알 수 있다.
// Uncrackable1.js
setImmediate(function() {
Java.perform(function() {
var exit_bypass = Java.use("android.content.DialogInterface.OnClickListener");
exit_bypass.onClick.implementation = function(arg1, arg2) {
console.log("[*] Exit Bypass");
}
})
})
스크립트 작성 후, 프리다를 통해 앱 프로세스에 스크립트를 인젝션 해야 하는데, 루팅 탐지의 경우 앱을 실행하자 마자 로직이 실행되므로, 인젝션 후에 앱 프로세스를 실행하기 위해 인젝션 후 Spawn 하는 방식을 사용할 것이다.
보면 에러가 나오는데, 이 android.content.DialogInterface.OnClickListener
의 경우 클래스가 아닌 인터페이스라서 클래스를 가져오는 프리다 API를 적용하지 못하는 것 같다.
=> 여기에 메소드 후킹하는 방법 나와있음.
따라서, onClick()
메소드를 재작성하는 방법 대신에 System.exit(0)
메소드를 재작성하여 앱 프로세스가 종료되지 않도록 해보자. 이 메소드 이름을 구글에 검색해보면 java.lang.System
클래스에 속하는 메소드임을 알 수 있다.
// Uncrackable1.js
setImmediate(function() {
Java.perform(function() {
var exit_bypass = Java.use("java.lang.System");
exit_bypass.exit.implementation = function(arg) {
console.log("[*] Exit Bypass");
}
})
})
스크립트 수정 후, 다시 프리다 명령어를 실행하면, 앱이 실행되고 똑같이 루팅 탐지 알림창이 표시되지만 이전과 달리 OK 버튼을 클릭하면 캡처 화면처럼 종료되지 않는다.
위 루팅 탐지 우회에 사용했던 앱을 계속 사용하겠다. 성공적으로 루팅 탐지 우회에 성공했다면, Secret String 입력창에 아무 문자열을 입력하고 VERIFY 버튼을 클릭해보자.
test 라는 문자열 입력 후 VERIFY 버튼을 클릭해보니 위 캡처화면과 같은 알림창이 표시되었다. 루팅 탐지 우회 때 했던 방법처럼 알림창 메세지를 검색하여 어떤 로직이 구현되어 있는지 확인해보자.
Nope 이라는 메세지를 검색해보니 MainActivity
클래스 내에 verify()
메소드에 해당 로직이 구현되어 있음을 알 수 있다. 입력한 문자열은 obj
라는 변수 안에 저장되고, 이 값을 a.a()
메소드의 인자 값으로 전달했을 때 참을 리턴하면 성공 메세지를 출력하므로 이 a.a()
메소드의 내용을 확인해 보자.
일단 복잡해 보이는 로직은 무시하고, 가장 아래 리턴문을 확인해 보면 bArr
이라는 바이트 배열을 문자열로 변환하여 a()
메소드 호출할 때 인자로 전달받은 문자열(입력한 Secret String)과 비교하여 같으면 참을 반환한다. 즉, 위에 있는 try 문을 봤을 때 sg.vantagepoint.a.a.a()
메소드 호출하고 반환된 바이트 배열이 복호화된 Secret String 임을 알 수 있다. 이 메소드의 내용도 확인해 보자.
암호화에 대해 정확히는 모르지만 AES
라는 암호화 알고리즘을 통해 복호화한 값을 리턴하고 있음을 알 수 있다.
여기서 사용할 수 있는 방법은 두 가지 인데, 하나는 애초에 verify()
메소드에 있는 조건문인 a.a(obj)
가 무조건 참을 반환하도록 재작성하는 방법이고, 다른 하나는 복호화 된 bArr
바이트 배열에 있는 값을 알아내서 Secret String 에 정확한 정답을 입력하는 방법이 있다. 일단 전자의 방법을 사용해 보자.
// Uncrackable1.js
setImmediate(function() {
Java.perform(function() {
// 루팅 탐지 우회 로직 생략
var trueClass = Java.use("sg.vantagepoint.uncrackable1.a");
trueClass.a.implementation = function(arg) {
console.log("\n[*] Return True");
return true;
}
})
})
이전처럼 test를 입력해보니 이번에는 Success 라는 성공 메세지가 표시되었다. 사실 이런 방법을 사용해서 해결하는 게 목적은 아니고, 복호화 하는 메소드인 sg.vantagepoint.a.a.a()
메소드를 후킹하여 복호화 된 바이트 배열 값을 알아내는 것이 이 문제의 목적이다.
// Uncrackable1.js
setImmediate(function() {
Java.perform(function() {
// 루팅 탐지 우회 로직 생략
var decrypteClass = Java.use("sg.vantagepoint.a.a");
decrypteClass.a.implementation = function(arg1, arg2) {
var secret_string = this.a(arg1, arg2);
console.log(secret_string);
return secret_string;
}
})
})
해당 메소드가 정의 된 sg.vantagepoint.a.a
클래스를 가지고 와서 a()
메소드를 재작성하는데, 메소드를 그대로 호출하여 반환 되는 복호화된 바이트 배열을 secret_string
변수에 저장한다. 그 후 그 값을 로그로 출력하고, 정상적으로 로직을 진행하기 위해 마지막에 반환까지 해주어야 한다. 한 번 이 스크립트를 프로세스에 인젝션 해보자.
실행 된 앱에서 Secret String 입력 값으로 아무 문자열을 입력 후 VERIFY 버튼을 클릭하면 위와 같은 [Object Object
라는 로그가 출력된다. 이는 호출한 a()
메소드의 반환 값이 객체(바이트 배열)임을 나타낸다. 바이트 배열의 내용을 하나씩 출력하도록 수정하자.
// Uncrackable1.js
setImmediate(function() {
Java.perform(function() {
// 루팅 탐지 우회 로직 생략
var decrypteClass = Java.use("sg.vantagepoint.a.a");
decrypteClass.a.implementation = function(arg1, arg2) {
var secret_string = this.a(arg1, arg2);
for(var i=0;i<secret_string.length;i++) {
console.log(secret_string[i]);
}
return secret_string;
}
})
})
이번에는 정수형 값들이 출력되고 있는데, 이는 아스키 코드 값이다. 따라서 이 아스키 코드 값에 해당하는 문자로 변환하도록 수정하자. 자바스크립트에서 아스키 코드 값을 문자로 변환해주는 함수는 String.fromCharCode()
이다.
// Uncrackable1.js
setImmediate(function() {
Java.perform(function() {
// 루팅 탐지 우회 로직 생략
var decrypteClass = Java.use("sg.vantagepoint.a.a");
decrypteClass.a.implementation = function(arg1, arg2) {
var secret_string = this.a(arg1, arg2);
for(var i=0;i<secret_string.length;i++) {
console.log(String.fromCharCode(secret_string[i]));
}
return secret_string;
}
})
})
출력된 로그를 확인해보니 복호화 된 문자열이 I want to believe
임을 알 수 있다. 한 번 이 값을 Secret String 값으로 입력해보자.
성공적으로 암호화 된 문자열을 찾았음을 알 수 있다.