![[붉은외계인] Mobile - OWASP UnCrackable L3](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGGfDl%2FbtsFKRalBt5%2FC1EHREQB2fYbkITV8boMsk%2Fimg.png)
2024.03.05 - [Mobile] - [붉은외계인] Mobile - OWASP UnCrackable L2
[붉은외계인] Mobile - OWASP UnCrackable L2
2024.03.04 - [Mobile] - [붉은외계인] Mobile - OWASP UnCrackable L1 [붉은외계인] Mobile - OWASP UnCrackable L1 Frida 연습을 위하여, OWASP에서 래퍼런스로 제공하는 UnCrackable Level 1을 풀어보았다 OWASP에서는 래퍼런스
redalien.tistory.com
이전 Level 2단계에 이어서, 3단계를 진행하겠다
바로 들어가보자!
시작
해당 단계 또한, Rooting을 감지하면 앱을 종료시켜버린다
매 단계에서 진행했던 Rooting 우회를 시도해보겠다
먼저, 해당 AlertDialog에 세팅된 문구를 출력하는 메소드를 찾아보겠다
위에 코드들은 모두 sg.vantagepoint.uncrackable3.MainActivity 클래스에 존재하는 코드이다
코드를 살펴보면 AlertDialog를 출력하는 showDialog 메소드를 만들어 사용하고 있다
그 아래 그림의 if 조건문은 MainActivity.onCreate 메소드내에 쓰여진 코드이다
해당 조건문을 충족하면 showDialog에 String을 전달하며, showDialog메소드를 실행한다
해당 조건문에 사용된 메소드들을 살펴보자
checkRoot 메소드
먼저, RootDetection 클래스의 checkRoot 메소드들을 살펴보자
코드를 살펴보면, Level 1 ~ 2에서 다뤘던 구조와 비슷하다
이전 포스트에서 다뤘기 때문에, 해당 클래스의 메소드를 우회하는 내용은 생략하겠다
isDebuggable 메소드
// 해당 상수의 값은 2
ApplicationInfo.FLAG_DEBUGGABLE
다음은 IntergrityCheck 클래스의 isDebuggable 메소드를 살펴보자
flag값과 2를 AND 비트연산하고 있다
참고로, 여기서 2인 이유는 위의 쓰여진 FLAG_DEBUGGABLE 상수 대신 바로 2를 써넣은것이다
이게 0이 아니라면 Debug 모드로 판단하고 true를 반환하고, 그 반대는 false를 반환한다
AsyncTask 클래스
추가로, onCreate 메소드가 실행될 때, AsyncTask 클래스를 사용하여 비동기 작업을 하고 있다
내용을 살펴보자
Debug.isDebuggerConnected() 메소드는 디버거가 연결되어 있지 않으면 false를 반환하는데
false를 반환할 때, 0.1초 sleep에 들어간다 그리고 while문으로 인하여 이것을 계속 반복한다
이러한 과정은 AsyncTask 사용으로 인하여, 비동기로 처리하면서 계속 검사하고 있는 것이다
만약, 디버거가 연결되어 있으면 true를 반환하면서 onPostExecute()를 실행한다
onPostExecute() 메소드 안에는 위에서 살펴본 showDialog메소드에 String을 전달하고
System.exit() 메소드를 실행하여 앱을 종료시켜버린다
◆ 참고로, 현재 AsyncTask는 deprecated 되었기 때문에 다시 보기는 힘들 것이다
이제 지금까지 알아본 매소드들을 우회하는 코드를 짜보겠다
우회 코드 1
Java.perform(function(){
console.warn("[+] Frida Start ...");
let system = Java.use("java.lang.System")
system.exit.implementation = function(){
console.warn("Avoid Exit !");
}
let Activity = Java.use("sg.vantagepoint.util.RootDetection")
Activity.checkRoot1.implementation = function(){console.warn("Avoid checkRoot1() !"); return false}
Activity.checkRoot2.implementation = function(){console.warn("Avoid checkRoot2() !"); return false}
Activity.checkRoot3.implementation = function(){console.warn("Avoid checkRoot3() !"); return false}
let Activity2 = Java.use("sg.vantagepoint.util.IntegrityCheck")
Activity2.isDebuggable.overload("android.content.Context").implementation = function(){
console.warn("Avoide isDebuggable() !");
return false
}
let Activity3 = Java.use("android.os.Debug")
Activity3.isDebuggerConnected.implementation = function(){
console.warn("Avoid isDebuggerConnected() !");
return false
}
console.warn("[+] Done !");
})
하지만 해당 코드를 실행하여도, 우회는 실패한다
다시 jadx를 통해 코드를 살펴보자
log 분석
코드를 살펴보던 중, 위의 그림과 같이 Log를 출력하는 코드를 찾았다
간단하게 보자면, CRC값 체크를 통해 데이터 변조 여부를 파악하는 것 같다
다른것들을 더 살펴보기 전에, logcat을 사용하여 어떤 내용이 log로 출력되는지 체크해 보겠다
위의 코드를 통하여, Priority는 V를 사용하고, Tag는 UnCrackable3를 사용한다는 것을 알았으니,
logcat의 필터 표현식을 사용하여 체크해보겠다
필터 표현식 마지막에 쓰인 *:S는 앞에 표현식 외 내용들은 출력하지 않겠다는 의미이다
필터 표현식 결과, 위와 그림과 같이 나오는 것을 확인할 수 있다
libfoo.so 파일과 연관되어 있는 것 같으니, 사용중인 so파일을 추출한 후, IDA로 열어보겠다
2024.03.05 - [Mobile] - [붉은외계인] Mobile - OWASP UnCrackable L2
[붉은외계인] Mobile - OWASP UnCrackable L2
2024.03.04 - [Mobile] - [붉은외계인] Mobile - OWASP UnCrackable L1 [붉은외계인] Mobile - OWASP UnCrackable L1 Frida 연습을 위하여, OWASP에서 래퍼런스로 제공하는 UnCrackable Level 1을 풀어보았다 OWASP에서는 래퍼런스
redalien.tistory.com
사용중인 so 파일을 찾는 방법은 Level 2 단계에 자세하게 기록했기 때문에 여기서는 생략하겠다
libfoo.so 파일 분석
jadx를 통해서 살펴보았을 때는 Tampering detected! 라는 문자열 출력 메소드를 찾지 못하였다
그래서 libfoo.so 파일에서 Tampering detected! 라는 문자열을 찾아볼 것이다
IDA의 Strings 기능과 External Reference 기능을 사용하여, 해당 문자열이 사용되는 곳을 검색하였다
그 결과, sub_3080() 메소드에서 사용되는 것을 확인하였다
해당 메소드의 이름이 sub인 이유는 대상 함수의 심볼이 죽어있어, IDA에서 자체적으로 이름을 붙인 것이다
코드를 살펴보면, do while 안에 strstr() 함수를 사용하여 frida와 xposed 문자열이 있는지 검사하고 있다
해당 문자열이 없다면, 계속 while문을 돌겠지만, 문자열이 있다면 v2에 문자열을 할당한 후,
__android_log_print() 함수를 실행한다
결론적으로, strstr() 함수가 0을 반환하도록 하면 해당 함수를 우회할 수 있을 것이다
strstr() 함수를 우회하는 코드를 추가하면 아래와 같다
우회 코드 2
Java.perform(function(){
console.warn("[+] Frida Start ...");
Interceptor.attach(Module.getExportByName("libc.so", "strstr"), {
onEnter: function(arg){
let arg1 = Memory.readUtf8String(arg[0])
this.frida = 0
if(arg1.indexOf("frida") !== -1){ this.frida = 1}
},
onLeave: function(retval){
if(this.frida){
retval.replace(0);
}
}
})
console.warn("Avoid strstr()");
let system = Java.use("java.lang.System")
system.exit.implementation = function(){
console.warn("Avoid Exit !");
}
let Activity = Java.use("sg.vantagepoint.util.RootDetection")
Activity.checkRoot1.implementation = function(){console.warn("Avoid checkRoot1() !"); return false}
Activity.checkRoot2.implementation = function(){console.warn("Avoid checkRoot2() !"); return false}
Activity.checkRoot3.implementation = function(){console.warn("Avoid checkRoot3() !"); return false}
let Activity2 = Java.use("sg.vantagepoint.util.IntegrityCheck")
Activity2.isDebuggable.overload("android.content.Context").implementation = function(){
console.warn("Avoid isDebuggable() !");
return false
}
let Activity3 = Java.use("android.os.Debug")
Activity3.isDebuggerConnected.implementation = function(){
console.warn("Avoid isDebuggerConnected() !");
return false
}
console.warn("[+] Frida Done !");
})
해당 코드를 실행하면, AlertDialog 알림창 뜨는 것 없이 바로 접속되는 것을 확인할 수 있다
참고로, prompt에는 Avoid isDebuggerConnected() 가 계속 출력 될 것이다
입맛에 맞게, console.warn()을 삭제하여도 좋다
코드에서 libfoo.so 가 아닌 libc.so 파일을 로드한 이유는 libfoo.so 파일을 로드하면 strstr()를 찾을 수 없다는
에러가 발생하였기 때문이다 구글링을 했지만 에러 원인을 정확하게 이해하지는 못했다
◆ 모듈 이름을 모르면 null을 넣으라고 공식 문서에 명시되어 있긴 하나, 성능 저하 때문에 가급적 지양하라고 한다
나름 이해한 결과로는, strstr은 표준 라이브러리인 libc.so 에 속해 있기 때문에libfoo.so 파일이 로드되기전이라도, libc에 연결하면 strstr()을 사용할 수 있다 (?) 이다
해당 원인을 드디어 찾았다 !
frida -U -f owasp.mstg.uncrackable3 -l 123.js
스크립트를 실행할 때는 위와 같이 입력할 것이다 여기서 포인트는 -f 옵션이다
-f 옵션을 사용하면 앱이 실행되기 전에, 스크립트를 먼저 적용한 다음에 앱을 실행한다
이것은 앱이 모듈을 로드하기도 전에 스크립트를 적용하는 것과 같다
즉, libfoo.so 를 로드하기도 전에 스크립트를 먼저 적용하기 때문에,
스크립트에 libfoo.so 모듈을 검색하는 코드가 있을 경우, 에러가 발생하는 것이다
하지만 우리가 찾으려는 strstr() 함수는 libfoo.so에 속해있기도 하나,
표준 라이브러리인 libc.so에도 속해있다
그렇기 때문에, 로드되기 전인 libfoo.so에 접근하면 에러가 발생하지만
libc.so을 통한다면 strstr() 함수에 접근 할 수 있다
분석
우회에 성공하였으니, 이제 Secret String을 찾아보자
check_code(obj)을 살펴보면 사용자 입력값인 obj를 넣고 있다
CodeCheck.check_code()가 true가 나오기 위해서는, Native 메소드인 bar()가 true가 나와야한다
다시 한 번 libfoo.so를살펴보자
◆ 참고로, IDA는 x86 so 파일의 bar()함수 로드에 실패하기 때문에 Ghidra를 사용하였다
해당 코드는 Java_sg_vantagepoint_uncrackable3_CodeCheck_bar 의 중요 부분이다
do while문을 사용하여, if (*(byte *)(iVar1 + uVar3) != (*pbVar5 ^ local_40[uVar3])) 가 같지 않다면
goto LAB_00013456 로 이동하고, 같다면 uVar3과 pbVar5를 1씩 증가시킨다
그리고 uVar3가 24와 같아지기전까지 해당 과정을 반복하다가,
같아지면 do while문을 빠져나가 그 아래 내용을 수행한다
가장 먼저, &DAT_0001601c 주소의 내용을 pbVar5에 할당하고 있는데 이거 먼저 알아보겠다
◆ 0x18를 10진수로 바꾸면 24이다
이러한 &DAT_0001601c 는 init 함수에서 strncpy()를 사용하여,
init함수에 들어온 Argument에서 길이 24에 해당하는 문자를 &DAT_0001601c 에 복사하고 있다
이러한 init함수는 jadx를 통해 사용을 확인해본결과, 길이 24인 pizzapizzapizzapizzapizz 문자열을
Argument로 넣고 있다
즉, &DAT_0001601c에는 pizzapizzapizzapizzapizz 문자열이 복사될 것이다
그렇기에, 해당 주소를 위의 그림과 같이 pizza로 이름을 변경하였다
다음으로, 살펴볼 것은 local_40 변수이다
조건문을 보면 pbVar5와 local_40의 인덱스와 xor연산을 하고 있는데,
이러한 local_40은 먼저, FUN_00010fa0 함수의 Argument로 전달되어, 특정 연산을 하고 있다
아마 특정 값을 생성하여 local_40에 할당하는 것처럼 보인다
해당 함수를 살펴보자
먼저 FUN_00010fa0 함수를 decompile을 통해 살펴보면 param_1이라는 포인터를 Argument받고 있다
또한 1525 줄이나 될 정도로, 코드의 길이가 길다
중요한점은 이렇게 긴 연산을 마무리하면, param_1 포인터가 가리키는 메모리 주소에
해당 연산 값이 저장될 거라는 점이다
param_1을 마우스로 클릭하면, 위와 같이, 00010fa0 주소를 가리키는데,
이것은 해당 포인터의 주소가 00010fa0 이라는 것이다
위에서 언급하였듯, 연산이 끝나면 포인터가 가리키는 메모리 주소에 연산 값이 저장된다고 하였다
즉, 연산이 끝난 뒤에 00010fa0 을 살펴보면 해당 연산 값을 알 수 있다는 것이다
또한 위에서 알아본 bar함수의 do while문은 uVar3의 값이 24가 되면 빠져나갔기 때문에
FUN_00010fa0 함수는 24 길이에 해당하는 연산 값을 만들거라 예상할 수 있다
해당 연산값을 추출하는 코드를 짜보겠다
Secret 값 추출 코드
// 해당 메모리 위치에서, 길이 24만큼 읽어들이자
setTimeout(function(){
let length = 24
Interceptor.attach(Module.findBaseAddress("libfoo.so").add(0xFA0), {
onEnter : function(arg){this.location = arg[0]}, // 현재 메모리위치 기록
onLeave : function(retval){
let buffer = Memory.readByteArray(this.location, length ) // ArrayBuffer를 반환
// ArrayBuffer에 접근하기 위한 Uint8Array 객체 생성
let secretArrayBuffer = new Uint8Array(buffer)
console.warn(`Secret Array Byte : ${secretArrayBuffer}`);
}
})
}, 2000)
해당 코드를 실행한 후, 아무 문구나 입력하면 위와 같이 값을 반환하는 것을 확인할 수 있다
즉, FUN_00010fa0 함수의 반환값은 위와 같다는 것이다
참고로, ArrayBuffer에는 직접 접근할 수 없기 때문에 Uint8Array 혹은 DataView를 통해 접근해야 한다
이러한 반환값의 각 인덱스와 pbVar5을 24번 동안 xor 연산하고 있기 때문에
우리 또한 pbVar5와 반환값을 24번 xor 연산하면 어떠한 값을 얻을 수 있을 것이다
위에서 알아보았듯, pbVar5는 pizzapizzapizzapizzapizz 문자열 이였다
해당 문자열과 반환값을 xor 하는 연산 그리고 최종 코드를 작성해보겠다
최종 코드
Java.perform(function(){
console.warn("[+] Frida Start ...");
Interceptor.attach(Module.getExportByName("libc.so", "strstr"), {
onEnter: function(arg){
let arg1 = Memory.readUtf8String(arg[0])
this.frida = 0
if(arg1.indexOf("frida") !== -1){ this.frida = 1}
},
onLeave: function(retval){
if(this.frida){
retval.replace(0);
}
}
})
console.warn("Avoid strstr()");
let system = Java.use("java.lang.System")
system.exit.implementation = function(){ console.warn("Avoid Exit !");}
let Activity = Java.use("sg.vantagepoint.util.RootDetection")
Activity.checkRoot1.implementation = function(){console.warn("Avoid checkRoot1() !"); return false}
Activity.checkRoot2.implementation = function(){console.warn("Avoid checkRoot2() !"); return false}
Activity.checkRoot3.implementation = function(){console.warn("Avoid checkRoot3() !"); return false}
let Activity2 = Java.use("sg.vantagepoint.util.IntegrityCheck")
Activity2.isDebuggable.overload("android.content.Context").implementation = function(){
console.warn("Avoid isDebuggable() !");
return false
}
setTimeout(function(){
let xorkey = "pizzapizzapizzapizzapizz"
let length = 24
Interceptor.attach(Module.findBaseAddress("libfoo.so").add(0xFA0), {
onEnter : function(arg){this.location = arg[0]}, // 현재 메모리위치 기록
onLeave: function(retval){
let buffer = Memory.readByteArray(this.location, length ) // ArrayBuffer를 반환
let secretArrayBuffer = new Uint8Array(buffer)
console.warn(`Secret Array Byte : ${secretArrayBuffer}`);
let xor_result = []
for(let i=0; i<length; i++){
xor_result[i] = String.fromCharCode(secretArrayBuffer[i] ^ xorkey.charCodeAt(i))
}
console.warn(`xorkey : ${xorkey}`);
console.warn(`xor_result : ${xor_result.join(" ")}`);
}
})
}, 2000)
})
해당 코드를 실행하여 얻은 문자열을 입력하면 성공하는 것을 확인할 수 있다
마무리
해당 풀이를 진행하고, 습득하는데까지 얼추 3일정도 걸린 거 같다
일단 어셈블리어와 C언어를 하나도 모르는 상태에서 진행하다보니, 코드가 전혀 읽히지 않았다
또한 IDA와 Ghidra를 다루는거에서도 많은 시간을 썼다
바로 Uncrackable 4단계를 풀려고 하였으나, 해당 풀이를 통해 부족한 부분을 파악할 수 있었기 때문에,
다음 공부는 개인적으로 밀렸던 공부와 C언어를 하지 않을까 싶다
얼른 해당 공부를 마치고 4단계를 풀어보고 싶다!
끝 !
참고
https://www.youtube.com/watch?v=bJgR5PKv2t0&t=690s
https://nibarius.github.io/learning-frida/2020/06/05/uncrackable3
'Security > Mobile' 카테고리의 다른 글
[붉은외계인] Mobile - Frida Detected Java 코드 (0) | 2024.05.09 |
---|---|
[붉은외계인] Mobile - OWASP UnCrackable L2 (0) | 2024.03.05 |
[붉은외계인] Mobile - OWASP UnCrackable L1 (0) | 2024.03.04 |
[붉은외계인] Mobile - FridaLab 풀이 (1) | 2024.02.26 |
[붉은외계인] Mobile - smali 코드 분석 1 with KGB Messenger (0) | 2024.02.14 |
IT / Android
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!