Hack The Box: SAW
앱(https://app.hackthebox.com/challenges/saw) 설치하여도 실행이 안됩니다.
디컴파일 결과를 보니 인텐트를 실어서 액티비티를 실행해야 다음단계로 넘어갈 수 있을 것 같습니다.
AndroidManifest.xml 파일을 보니 "open": "sesame" 인텐트와 함께 "com.stego.saw.MainActivity" 액티비티의 "android.intent.action.MAIN" 액션을 실행시키면 되는 것으로 보입니다.
am start -a android.intent.action.MAIN --es open "sesame" -n com.stego.saw/.MainActivity
그런데, 다시 "Click me" 버튼을 클릭하면 앱이 종료됩니다. 디컴파일 결과에 의하면 "click me" 버튼 클릭시 새로운 버튼이 생성되며, 새로운 버튼 클릭 시 사용자 값을 입력받는 AlertDialog가 팝업 되어야 합니다.
로그캣을 통해 크래시 로그를 보면 "permission denied for window type 2038" 메세지를 확인할 수 있습니다.
https://stackoverflow.com/a/52917515 에 의하면 해당 에러는 Setting에서 앱 권한을 설정하여 해결할 수 있습니다.
앱 권한 설정 항목으로 들어가서 "다른 앱 위에 표시 허용" 을 체크해줍니다.
다시 액티비티를 실행하고 "Click me" 버튼을 클릭하면 버튼이 생성되고, 생성된 버튼을 클릭하면 AlertDialog가 보입니다.
소스코드에 의하면 알람창에 아무값이나 입력하고 XORIFY 터치하면 네이티브 메서드 "a" 에 "FILE_PATH_PREFIX" 와 함께 입력값이 전달되는 로직입니다.
네이티브 메서드 "a"와 shared library(.so파일) 내 함수의 매핑정보는 libart.so 파일의 RegisterNatives 메서드를 후킹하면 확인 가능합니다.
"com.stego.saw.MainActivity.a" 메서드는 libdefault.so 라이브러리의 offset 0xc90 에 위치한 함수와 매핑되고 있습니다.
해당 네이티브 메서드의 psuedocode는 다음과 같습니다. _Z1aP7_JNIEnvP8_1 함수에 const char * v7, const char *v8이 전달되고 있으며, 이는 다시 _Z17_Z1aP7_JNIEnvP8_1PKcS0_ 함수에 전달되고 있으므로 어떤 값이 전달되는지 확인해볼 필요가 있겠습니다.
var awaitForCondition = function(callback) {
var module_loaded = 0;
var int = setInterval(function() {
Process.enumerateModulesSync()
.filter(function(m){ return m['path'].toLowerCase().indexOf('libdefault.so') != -1; })
.forEach(function(m) {
console.log("libdefault.so loaded!");
return module_loaded = 1;
})
if(module_loaded) {
clearInterval(int);
callback();
return;
}
}, 0);
}
function nativeTrace(nativefunc) {
var nativefunc_addr=Module.getExportByName("libdefault.so", nativefunc)
var func=ptr(nativefunc_addr);
Interceptor.attach(func, {
// set hook
onEnter: function (args) {
console.warn("\n[+] " + nativefunc + " called"); // before call
if (nativefunc == "_Z17_Z1aP7_JNIEnvP8_1PKcS0_") {
console.log("\n\x1b[31margs[0]:\x1b[0m \x1b[34m" + args[0].readUtf8String() + ", \x1b[32mType: ");
console.log("\n\x1b[31margs[1]:\x1b[0m \x1b[34m" + args[1].readUtf8String() + ", \x1b[32mType: ");
}
},
onLeave: function (retval) {
if(nativefunc == "_Z17_Z1aP7_JNIEnvP8_1PKcS0_"){
console.warn("[-] " + nativefunc + " ret: " + retval.toString() ); // after call
}
}
});
}
function hook() {
nativeTrace("_Z17_Z1aP7_JNIEnvP8_1PKcS0_");
}
awaitForCondition(hook);
임의의 문자열 입력 후 XORIFY 클릭하면 _Z17_Z1aP7_JNIEnvP8_1PKcS0_ 함수의 첫번째 인자로는 "/data/user/0/com.stego.saw/", 두번째 인자로는 내가 입력한 임의의 문자열이 전달되는 것을 확인 가능합니다.
_Z17_Z1aP7_JNIEnvP8_1PKcS0_ 함수의 pseudocode는 다음과 같은데, 내가 입력한 문자열의 각 자리와 XOR연산을 수행하여 하나라도 일치하지 않으면 함수를 종료하고 있습니다.
[0xA, 0xB, 0x18, 0xF, 0x5E, 0x31, 0xC, 0xF] ^ [내가 입력한 문자열] == [0x6C, 0x67, 0x28, 0x6E, 0x2A, 0x58, 0x62, 0x68] 이 되어야 합니다.
XOR 연산의 역연산은 XOR이므로 다음 관계가 성립합니다.
[0xA, 0xB, 0x18, 0xF, 0x5E, 0x31, 0xC, 0xF] ^ [0x6C, 0x67, 0x28, 0x6E, 0x2A, 0x58, 0x62, 0x68] ==> [내가 입력해야 할 문자열]
XOR 연산을 통해 "내가 입력해야 할 문자열"은 "fl0ating" 임을 알 수 있습니다.
_Z17_Z1aP7_JNIEnvP8_1PKcS0_ 함수의 pseudocode를 조금 더 살펴보면 문자열을 제대로 입력하면 파일을 만들어서 무언가를 쓰는 로직입니다.
"fl0ating" 문자열 입력 및 "XORIFY" 버튼 클릭 후 위에서 _Z17_Z1aP7_JNIEnvP8_1PKcS0_ 함수의 첫번째 인자로 전달된 "/data/user/0/com.stego.saw/" 경로에 가보면, h 라는 파일이 생성되어 있는 것을 볼 수 있습니다.
생성된 파일을 읽으면 플래그 획득이 가능합니다.