- XALZ 압축해제
- CVE-2010-1622
- Xamarin 분석
- blind sql injection
- CVE-2022-22965
- PortSwigger
- DOM
- File Upload
- CVE-2014-0094
- JSP
- xss
- MariaDB
- HackTheBox APKey
- Hackthebox cat
- login form
- HackTheBox
- Directory traversal
- Android Backup
- Frida
- UnCrackable level 1
- mstg
- Android 6.0
- mongoDB
- UnCrackable
- SeeTheSharpFlag
- NoSQL
- nginx
- JAVA ClassLoader 취약점
- HacktheBox Mobile
- getCachedIntrospectionResults
- Today
- Total
끄적끄적
[UnCrackable] Level 2 본문
배경지식
AsyncTask
설명
하나의 클래스에서 UI 작업과 Background 작업을 수행할 수 있도록 도와주는 클래스이다.
동작과정
AsyncTask의 실행 순서는 아래와 같다.
1. execute(): AsyncTask 실행
2. onPreExecute(): UI 쓰레드에서 호출되며 Background 작업이 실행되기 전 수행해야할 동작 구현
3. doInBackground(Params ...): 실제 비동기 작업 쓰레드로 execute() 메소드 호출 시 사용된 파라미터를 전달 받음
4. publishProgress(Progress ...): doInBackground() 중간에 UI 업데이트를 위해 호출
5. onProgressUpdate(): publishProgress()가 호출될 때 마다 자동으로 호출
6. onPostExecuted(): doInBackground() 메소드 작업 종료 시, 결과 파라미터를 리턴하여 쓰레드 작업이 종료 됐을 때의 작업을 정의
소스코드 예제
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
// 버튼을 클릭하면 파일 다운로드 경로를 파라미터로 AsyncTask 실행
public void OnClick(View view) {
switch (view.getId()) {
case R.id.button:
try {
new DownloadFilesTask().execute(new URL("파일 다운로드 경로1"));
} catch (MalformedURLException e) {
e.printStackTrace();
}
break;
}
}
private class DownloadFilesTask extends AsyncTask {
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected Long doInBackground(URL... urls) {
// 전달된 URL 사용 작업
return total;
}
@Override
protected void onProgressUpdate(Integer... progress) {
// 파일 다운로드 퍼센티지 표시 작업
}
@Override
protected void onPostExecute(Long result) {
// doInBackground 에서 받아온 total 값 사용 장소
}
}
}
현재 AsyncTask는...
Android 11에서 퇴출되었다... Why..?
- Memory Leak: 엑티비티 전환 후 AsyncTask가 중복 실행될 경우, 이전 화면에서 호출한 AsyncTask가 해제되지 않아 OutofMemory가 발생
- 순차적으로 실행되기 때문에 속도가 저하될 수 있음
- Fragement에서 AsyncTask를 실행하는 과정에서 뒤로가기로 액티비티가 종료될 경우 getContext(), getActivity() 등이 null을 반환하여 onPostExecute()에서 NullPointerExcepion이 발생
- onError()와 같은 예외처리 메소드 미존재
- AsyncTask 병렬 실행 시 doInBackground()의 실행 순서가 보장되지 않아 플랫폼 버전에 따라 일관되지 않은 동작 야기
*RxJava 혹은 **Coroutine을 사용하자
*RxJava: 비동기 처리를 함수 프로그래밍을 통해 처리할 수 있도록 도와주는 라이브러리
**Coroutine: 작업의 단위를 Thread가 아닌 Object 단위로 할당하여 Context Switching 비용 감소
문제
본 문제는 Reference [2]를 통해 다운로드할 수 있다. 앱 실행 결과 안티루팅 로직이 실행되며 앱이 종료된다.
본 문제의 안티루팅 로직은 이전 Level 1의 문제와 동일하게 우회가 가능하다. 추가로 isDebuggerConnected를 통해 백그라운드에서 디버깅 여부를 확인하는 로직이 있지만, 결과적으로 System.exit()를 호출하기 때문에 Level 1과 동일하게 우회가 가능하다.
2022.05.29 - [Security/Mobile] - [UnCrackable] Level 1
[UnCrackable] Level 1
배경지식 안티루팅(루팅 탐지) 루팅 탐지 방법은 아래와 같은 방법들이 있다. 안드로이드 환경 내에 설치된 Package(앱) 검색 설치된 파일 검사(/bin/su, /sbin/su 등) 빌드 태그 검사(test-key 검사) 쉘 커
go0g.org
안티루팅을 우회하여 실행 시, Level 1과 동일하게 올바른 문자열을 찾아야 할거 같은 느낌이 강하게 든다.
분석
분석에 앞서, PoC에 대한 두가지 목표를 설정하였다.
- "Success!" 알림 메시지 출력
- "Success!" 알림 메시지를 띄우기 위한 문자열 찾기
메인엑티비티에서는 foo 라이브러리를 로드한다.
사용자의 입력 값을 비교하는 구문을 살펴보면, 사용자의 입력 값을 m.a에 전달하는 것을 확인할 수 있다.
a 메소드의 리턴 값에 포함된 bar 메소드는 native로 작성되어 있어, 앞서 로드한 라이브러리에 대한 분석이 필요하다.
해당 라이브러리는 APK 파일을 디컴파일하여 접근할 수 있다.
2021.09.12 - [Security/Mobile] - [Android analysis] 디컴파일 및 리패키징
라이브러리 분석
라이브러리가 동작하는 과정을 정확하게 확인하기 위해서는 CPU 아키텍처에 맞는 라이브러리를 대상으로 진행하여야 한다.
USB 및 가상 환경으로 구동되는 기기의 CPU 아키텍처를 확인하는 명령어는 아래와 같다.
adb shell getprop ro.product.cpu.abi
adb shell getprop ro.product.cpu.abi2
애플리케이션이 구동되는 환경에 따라 CPU 아키텍처가 상이할 수 있기 때문에, 아래와 같이 ARM, X86 등의 라이브러리를 지원한다.
일반적인 기기들은 ARM 아키텍처를 사용
x86 디렉터리에 접근하여 ida-32bit를 통해 so 파일을 실행시킨다. 이후, Exports 탭으로 이동하면 외부에서 추가한 함수를 확인할 수 있다. 이중 JADX에서 확인한 bar 메소드와 가장 관련 있어보이는 Java_sg_vantagepoint_uncrackable2_CodeCheck_bar 함수를 클릭한다.
64bit로 실행시 디컴파일 불가
해당 함수의 주요 로직은 아래와 같다. strncmp 함수의 결과 값에 따라 반환 하는 값이 달라지는 것을 확인할 수 있다.
결과적으로 반환 값이 1 이여야 "Success!" 메시지를 띄울 수 있다.
또한, "Success!"를 출력하는 문자열을 찾기 위해선 strncmp에 대한 후킹을 진행하거나 so 파일 내에서 해당 문자열을 찾아야 한다.
PoC
앞서 언급한 두가지 목표에 대하여 접근한 PoC는 아래와 같다.
- "Success!" 알림 메시지 출력
- CodeCheck 클래스의 a 메소드 반환 값 변경
- 네이티브 함수를 so 파일에서 후킹하여 반환 값 수정
- 네이티브 함수 주소로 접근 후, 레지스트리 값을 수정하여 반환 값 변경
- "Success!" 알림 메시지를 띄우기 위한 문자열 찾기
- x86 이외 라이브러리 분석을 통하여 비교 문자열 찾기
- NDK(so 파일)내 strncmp 매개 변수를 후킹하여 비교 문자열 찾기
"Success!" 알림 메시지 출력
1. CodeCheck 클래스의 a 메소드 반환 값 변경
CodeCheck.a의 반환 값을 True로 변경한다면 원하는 알람 메시지를 확인할 수 있을 것이다.
Frida 코드
setImmediate(function () {
Java.perform(function () {
console.log("[*] START")
var exit_class = Java.use("java.lang.System")
exit_class.exit.implementation = function(){ //exit method Hooking
console.log("exit Hook");
}
var Rooting_Check = Java.use("sg.vantagepoint.a.b");
Rooting_Check.a.overload().implementation = function() { //"su" 명령어 확인
console.log("a() Method");
return false;
}
Rooting_Check.b.overload().implementation = function() { //빌드 설명 태그
console.log("b() Method");
return false;
}
Rooting_Check.c.overload().implementation = function() { //시스템 내 루팅 관련 파일 존재여부 확인
console.log("c() Method");
return false;
}
var Debug_Check = Java.use("sg.vantagepoint.a.a");
Debug_Check.a.overload('android.content.Context').implementation = function() { //안티디버그 우회
console.log("c() Method");
return false;
}
const hook2 = Java.use("android.os.Debug"); //Background에서 지속적으로 디버깅 여부를 판단하는 로직 종료
hook2.isDebuggerConnected.implementation = function() {
console.log("[*] isDebuggerConnected is called");
return true; //결과적으로 디버깅이 탐지되었다는 알람을 띄우지만 exit 함수를 후킹하여 종료되지 않음
}
//PoC #1-1
const CodeCheck = Java.use("sg.vantagepoint.uncrackable2.CodeCheck");
CodeCheck.a.implementation = function() {
console.log("[*] CodeCheck is called");
return true;
}
});
});
2. 네이티브 함수를 so 파일에서 후킹하여 반환 값 수정
Interceptor.replace를 통해 함수를 새로 작성
Frida 코드
setImmediate(function () {
Java.perform(function () {
console.log("[*] START")
var exit_class = Java.use("java.lang.System")
exit_class.exit.implementation = function(){ //exit method Hooking
console.log("exit Hook");
}
var Rooting_Check = Java.use("sg.vantagepoint.a.b");
Rooting_Check.a.overload().implementation = function() { //"su" 명령어 확인
console.log("a() Method");
return false;
}
Rooting_Check.b.overload().implementation = function() { //빌드 설명 태그
console.log("b() Method");
return false;
}
Rooting_Check.c.overload().implementation = function() { //시스템 내 루팅 관련 파일 존재여부 확인
console.log("c() Method");
return false;
}
var Debug_Check = Java.use("sg.vantagepoint.a.a");
Debug_Check.a.overload('android.content.Context').implementation = function() { //안티디버그 우회
console.log("c() Method");
return false;
}
const hook2 = Java.use("android.os.Debug"); //Background에서 지속적으로 디버깅 여부를 판단하는 로직 종료
hook2.isDebuggerConnected.implementation = function() {
console.log("[*] isDebuggerConnected is called");
return true; //결과적으로 디버깅이 탐지되었다는 알람을 띄우지만 exit 함수를 후킹하여 종료되지 않음
}
// PoC #1-2: 네이티브 함수의 Return 값을 변경하여 "Success!" 메시지 띄우기
//Interceptor의 replace: 함수 전체 내용 수정
const rootCheckPtr = Module.getExportByName('libfoo.so', 'Java_sg_vantagepoint_uncrackable2_CodeCheck_bar');
Interceptor.replace(rootCheckPtr, new NativeCallback(function () { //NativeCallback
console.log('[*] Function Return Change');
return 1;
}, 'int', []));
});
});
3. 네이티브 함수 주소로 접근 후, 레지스트리 값을 수정하여 반환 값 변경
함수의 리턴 Offset 값을 구하여 해당 로직 진행 전 레지스트리 값을 변경
Frida 코드
setImmediate(function () {
Java.perform(function () {
console.log("[*] START")
var exit_class = Java.use("java.lang.System")
exit_class.exit.implementation = function(){ //exit method Hooking
console.log("exit Hook");
}
var Rooting_Check = Java.use("sg.vantagepoint.a.b");
Rooting_Check.a.overload().implementation = function() { //"su" 명령어 확인
console.log("a() Method");
return false;
}
Rooting_Check.b.overload().implementation = function() { //빌드 설명 태그
console.log("b() Method");
return false;
}
Rooting_Check.c.overload().implementation = function() { //시스템 내 루팅 관련 파일 존재여부 확인
console.log("c() Method");
return false;
}
var Debug_Check = Java.use("sg.vantagepoint.a.a");
Debug_Check.a.overload('android.content.Context').implementation = function() { //안티디버그 우회
console.log("c() Method");
return false;
}
const hook2 = Java.use("android.os.Debug"); //Background에서 지속적으로 디버깅 여부를 판단하는 로직 종료
hook2.isDebuggerConnected.implementation = function() {
console.log("[*] isDebuggerConnected is called");
return true; //결과적으로 디버깅이 탐지되었다는 알람을 띄우지만 exit 함수를 후킹하여 종료되지 않음
}
//PoC #1-3 레지스트리 값을 변경하여 반환값 수정
var moduleName = "libfoo.so";
var nativeFuncAddr = 0x1014;
var baseAddr = Module.findBaseAddress(moduleName);
Interceptor.attach(baseAddr.add(nativeFuncAddr), {
onEnter:function(args){
console.log('[변경 전]: ' + JSON.stringify(this.context)) // 변조 전 레지스터 값 출력
this.context.eax=1;
console.log('[변경 후]: ' + JSON.stringify(this.context)) // 변조 전 레지스터 값 출력
}
});
});
});
"Success!" 알림 메시지를 띄우기 위한 문자열 찾기
x86 이외 라이브러리 분석을 통하여 비교 문자열 찾기
다수의 안드로이드 디바이스는 x86 이아닌 ARM 아키텍처를 사용한다. x86이 아닌 arm을 위해 작성된 라이브러리 파일을 열고 디컴파일을 진행한 결과이다. 우리가 찾던 문자열이 하드코딩되어 나타나는 것을 확인할 수 있다.
해당 값을 앱에 입력 시, 우리가 찾던 문자열인 것을 확인할 수 있다.
NDK(so 파일)내 strncmp 매개 변수를 후킹하여 비교 문자열 찾기
strncmp는 문자열을 비교하는 함수이다. Reference [3]을 참고하면 해당 함수에 대한 자세한 설명을 살펴볼 수 있다.
- strcmp의 3번째 매개변수인 args[2]를 후킹하여 문자열의 길이를 파악(IDA 분석으로 파악 가능)
- 해당 문자열의 길이(0x17)만큼의 길이에 해당되는 2번째 매개변수 args[1] 값을 후킹
- 해당 값들 중 유효한 문자열 찾기
Frida 코드
setImmediate(function () {
Java.perform(function () {
console.log("[*] START")
var exit_class = Java.use("java.lang.System")
exit_class.exit.implementation = function(){ //exit method Hooking
console.log("exit Hook");
}
var Rooting_Check = Java.use("sg.vantagepoint.a.b");
Rooting_Check.a.overload().implementation = function() { //"su" 명령어 확인
console.log("a() Method");
return false;
}
Rooting_Check.b.overload().implementation = function() { //빌드 설명 태그
console.log("b() Method");
return false;
}
Rooting_Check.c.overload().implementation = function() { //시스템 내 루팅 관련 파일 존재여부 확인
console.log("c() Method");
return false;
}
var Debug_Check = Java.use("sg.vantagepoint.a.a");
Debug_Check.a.overload('android.content.Context').implementation = function() { //안티디버그 우회
console.log("c() Method");
return false;
}
const hook2 = Java.use("android.os.Debug"); //Background에서 지속적으로 디버깅 여부를 판단하는 로직 종료
hook2.isDebuggerConnected.implementation = function() {
console.log("[*] isDebuggerConnected is called");
return true; //결과적으로 디버깅이 탐지되었다는 알람을 띄우지만 exit 함수를 후킹하여 종료되지 않음
}
// PoC #2-2: strncmp의 매개변수를 후킹
//Interceptor의 attach: 함수 자체에 대하여
//onEnter: 후킹 대상 메소드(함수) 진입 시 동작하는 코드를 명시 -> 함수 인자 값 확인 가능
//onLeave: 후킹 대상 메소드가 벗어날 시 동작하는 코드를 코드 불럭내에 명시 -> 리턴 값 변경
//retval.replace : 반환 값 변경
Interceptor.attach(Module.getExportByName('libfoo.so', 'strncmp'), {
onEnter: function(args){
if(args[2].toInt32() == 23){
//console.log("String len ="+args[2].toInt32()); //바이너리 형식을 int 형태로 변환
console.log("Compare Value ="+args[1].readUtf8String()); //UTF-8 Encoding
}
}
});
});
});
출력되는 문자열들 중 유효한 문자열을 발견할 수 있다. 해당 로직 외에도 Nox 환경에서 앱을 구동하면서 호출되는 strncmp 관련 내용들이 출력된다.
이와 같이 출력되는 문자열을 입력 시, 앞서 찾았던 결과와 동일한 결과를 얻을 수 있다.
Reference
[1] AsyncTask 관련: https://developer.android.com/reference/android/os/AsyncTask
[2] owasp-mstg Level_02: https://github.com/OWASP/owasp-mstg/tree/master/Crackmes/Android/Level_02
[3] strncmp 함수: https://www.cplusplus.com/reference/cstring/strncmp/
[4] Javscript API 사용: https://frida.re/docs/javascript-api/
[5] PoC 관련: https://redteam-securitylab.tistory.com/21
'Security > Mobile' 카테고리의 다른 글
[UnCrackable] Level 1 (0) | 2022.05.29 |
---|---|
[HacktheBox] SeeTheSharpFlag (feat. Xamarin) (0) | 2022.05.27 |
[HackTheBox] APKey (0) | 2022.05.24 |
[HackTheBox] Cat (Feat. Android Back Up) (0) | 2022.05.23 |
[ERROR] jadx is running low on memory (0) | 2022.04.16 |