Level 3 앱 을 설치한 후 실행해보자.
이번에는 Level 1~2 와는 다르게 문구가 다르게 보인다.
빠르게 디컴파일 툴 로 해당 문구를 출력해주는 부분을 보자.
그림 부분에서는 잘렸지만 이번에도 루팅 체크 를 하는 것 같다.
다시 if문 조건절에 있는걸 보면 총 5개 의 메소드 로 루팅 을 체크하는 로직이 존재한다.
모두 OR 로 되어있기 때문에 하나 하나 분석해봐야 한다.
먼저 Level 1~2 에서 한 것처럼 RootDetection.checkRoot1() 을 먼저 살펴보자.
아래의 그림은 checkRoot1 () 메소드이다.
해당 메소드를 보면 이름만 바뀐 것을 볼 수 있다.
즉, 로직은 Level 1~2와 같다는 말이다.
이 부분을 빠르게 후킹 해서 루팅 우회를 진행해보자.
Frida 가 실행된 후 앱은 바로 꺼졌다.
따라서 뒤의 IntegrityCheck.isDebuggable(this.getApplicationContext() 와
MainActivity.tampered != 0 도 추가적으로 우회를 해보았다.
하지만 똑같이 우회가 되지 않았다.
그래서 앱이 문제인지 아니면 문제에서 의도한 것인지 파악하기 위해 로그 를 분석해보았다.
위의 그림은 앱이 실행될 때 발생되는 로그 를 캡처한 것이다.
처음 시작하자마자 Tampering detected! Terminating... 라는 문구가 나온다.
뭔가 탐지됐다는 말이다.
따라서 코드단에서 분석을 진행할 때 놓친 부분이 있나 싶어서 디컴파일 툴에서
"Tampering detected! Terminating..." 문구를
검색해보았다.
나오지 않았다.
이는 앱단(java)에서 처리하는 것이 아니다고 판단할 수 있다.
100%로는 아니다.
저 문자열이 동적으로 생성되거나 문자열이 난독화되어있으면
원하는 문자인 "Tampering detected! Terminating..." 을 검색해도 나오지 않는다.
따라서 어딘가에서 루팅 혹은 디버거 탐지 등 어떤 것인지 모르는 탐지 로직 이 실행되고 있으며
탐지 로직에 걸려 로그가 나왔다는 것만 알 수 있다.
따라서 우리가 Secret String 을 맞춰 성공하는 alert 가 있는 곳으로 가보자.
if문 앞의 this.check.check_code(v4) 가 실행되면서 true을 return 해주면
우리는 성공한 메시지를 볼 수 있다.
this.check.check_code 메소드를 살펴보자.
살펴보면 check_code 메소드에서 this.bar 을 호출하고 this.bar에서 return 된 값을 return 한다.
this.bar 함수는 위에 정의가 되어있으며 이 역시 native 이다.
native 를 분석하기 전에 컨셉을 잡아보자.
아직 루팅 우회가 되지 않았다.
앱단에서는 해당 로직을 찾을 수 있었지만 후킹 한 결과 앱이 종료가 되는 현상이 있었다.
또한 log에는 Tampering detected! Terminating... 라는 문구가 나왔었다.
따라서 첫 번째로는 native 에서 루팅 우회 를 하는 로직을 찾아봐야 한다.
두 번째 로는 Secret String 과 관련된 key 를 찾는 것이다.
먼저 루팅 우회를 먼저 해야 다음 과정을 진행할 수 있기 때문에 루팅 탐지 로직 을 찾아보자.
먼저 해당 문구를 IDA에서 찾아보자.
찾아본 결과 sub_30D0 에서 Tampering detected! Terminating... 문구가 사용되고 있다.
그렇다면 바로 sub_30D0 함수를 살펴보자.
살펴보면 우리가 원했던 문구인 Tampering detected! Terminating ... 를 찾아볼 수 있다.
또한 log에서 볼 수 있었던 TAG 가 UnCrackable3 , 내용은 Tampering detected! Terminating... 인
log_print 하는 부분도 찾았다.
따라서 여기에서 탐지한다는 의미이다.
천천히 코드를 살펴보자.
v0 = fopen("/proc/self/maps", "r");
먼저 fopen 함수를 사용해서 /proc/self/maps를 읽어온다.
fopen 함수는 원형은 다음과 같고 하는 역할은 파일을 읽어 FILE 구조체 포인터 넘겨주는 함수 이다.
따라서 v0 에는 /proc/self/maps 파일의 포인터가 담겨있다.
/proc/self/maps 는 무슨 역할을 하는 파일일까?
먼저 /proc 디렉터리는 리눅스 계열에서 사용되는 디렉터리이고 실행되는 프로세스 정보를 담고 있다.
여기서 의미하는 self는 자신의 pid를 의미한다.
maps은 현재 실행되고 있는 프로세스의 주소 맵 또는 프로세스의 메모리 주소 공간을 보여주는 파일이다.
종합해보면 현재 실행하고 있는 프로세스(Level 3 앱)의 메모리 주소 맵을 담고 있는
파일이라고 생각하면 된다.
실제 테스트 단말기에서 maps 을 확인해보면 다음과 같은 정보를 볼 수 있다.
jackpotlteks:/ # cat /proc/27212/maps
.....
12c00000-13cc0000 rw-p 00000000 00:01 33530 /dev/ashmem/dalvik-main space (region space)_4735_4735 (deleted)
13cc0000-13ec0000 rw-p 010c0000 00:01 33530 /dev/ashmem/dalvik-main space (region space)_4735_4735 (deleted)
13ec0000-14140000 ---p 012c0000 00:01 33530 /dev/ashmem/dalvik-main space (region space)_4735_4735 (deleted)
14140000-2ac00000 rw-p 01540000 00:01 33530 /dev/ashmem/dalvik-main space (region space)_4735_4735 (deleted)
6f13f000-6f41a000 rw-p 00000000 103:11 228934 /data/dalvik-cache/arm64/system@framework@boot.art
6f41a000-6f430000 r--p 002db000 103:11 228934 /data/dalvik-cache/arm64/system@framework@boot.art
6f430000-6f565000 rw-p 00000000 103:11 228943 /data/dalvik-cache/arm64/system@framework@boot-core-libart.art
6f565000-6f577000 r--p 00135000 103:11 228943 /data/dalvik-cache/arm64/system@framework@boot-core-libart.art
6f577000-6f5b7000 rw-p 00000000 103:11 228949 /data/dalvik-cache/arm64/system@framework@boot-conscrypt.art
6f5b7000-6f5ba000 r--p 00040000 103:11 228949 /data/dalvik-cache/arm64/system@framework@boot-conscrypt.art
......
그다음 sub_30D0 함수의 코드를 분석해보자.
v0 = fopen("/proc/self/maps", "r"); // v0에 /proc/self/maps 파일 포인터 저장
if ( v0 )
{
......
}
else
{
LABEL_7:
v1 = "Error opening /proc/self/maps! Terminating...";
}
만약 if 문에서 v0 이 null 이면 즉, 파일을 열었을 때 열지 못했다면
LABEL 7: 로 가면서 "Error opening /proc/self/maps! Terminating..." 라는 문구와 종료가 된다.
하지만 v0 에 파일 포인터가 잘 넘어왔으면 if문 내부로 들어가게 된다.
...
if ( v0 )
{
do
{
while ( !fgets(&v3, 512LL, v0) ) // /proc/self/maps 512 길이만큼 가져와 v3에 담는다.
{
fclose(v0);
usleep(500LL);
v0 = fopen("/proc/self/maps", "r");
if ( !v0 )
goto LABEL_7;
}
}
while ( !strstr(&v3, "frida") && !strstr(&v3, "xposed") );
v1 = "Tampering detected! Terminating...";
}
...
if 문 내부로 들어와서 처음 실행되는 곳이 바로 do while 문이다.
while 문 안에는 fgets 라는 함수가 존재한다.
fgets 함수의 원형은 다음과 같다.
쉽게 설명해서 파일 스트림(stream)에서 해당 길이(num)만큼 문자열을 받는다(str).
정리하면 /proc/self/maps의 파일 정보를 길이만큼 읽어와서 str에 넣어주는 함수이다.
여기까지 정리하면 코드는 다음과 같다.
v0 = fopen("/proc/self/maps", "r"); // v0에 /proc/self/maps 파일 포인터 저장
if ( v0 )
{
do
{
while ( !fgets(&v3, 512LL, v0) ) // /proc/self/maps 512 길이만큼 가져와 v3에 담는다.
{
fclose(v0);
usleep(500LL);
v0 = fopen("/proc/self/maps", "r");
if ( !v0 )
goto LABEL_7;
}
}
while ( !strstr(&v3, "frida") && !strstr(&v3, "xposed") );
v1 = "Tampering detected! Terminating...";
}
else
{
LABEL_7:
v1 = "Error opening /proc/self/maps! Terminating...";
}
이제 if 문안의 2번째 while 을 보자.
strstr 함수가 2 개가 있고 첫 번째 인자에는 v3
즉, 위의 fgets 함수에서 /proc/self/maps 의 파일 정보를 v3 에 담았기 때문에
strstr 함수로 파일 정보을 한 줄 한 줄 읽어 오면서 중 frida 나 xposed 가 들어가 있는지 확인하는 것이다.
그렇다면 /proc/self/maps에 frida 나 xposed 가 있는지 확인해보자.
.........
7d7844e000-7d7844f000 ---p 00000000 00:00 0
7d7844f000-7d78553000 rw-p 00000000 00:00 0
7d78553000-7d78bdc000 r--p 00000000 103:11 972986 /data/local/tmp/re.frida.server/frida-agent-64.so
7d78bdc000-7d78bdd000 ---p 00000000 00:00 0
7d78bdd000-7d799da000 r-xp 00689000 103:11 972986 /data/local/tmp/re.frida.server/frida-agent-64.so
7d799da000-7d79a74000 r--p 01485000 103:11 972986 /data/local/tmp/re.frida.server/frida-agent-64.so
7d79a74000-7d79a8a000 rw-p 0151e000 103:11 972986 /data/local/tmp/re.frida.server/frida-agent-64.so
7d79a8a000-7d79af5000 rw-p 00000000 00:00 0 [anon:.bss]
7d79b23000-7d79b24000 ---p 00000000 00:00 0 [anon:thread stack guard]
......
이렇게 maps 파일에 /data/local/tmp/re.frida.server/frida-agent-64.so 가 존재한다.
따라서 Tampering detected! Terminating... 문구가 log 에 남은 것이다.
이제 후킹 포인트를 잡았다.
바로 strstr 함수 이다.
첫 번째 인자를 바꾸거나 두 번째 인자인 frida 를 다른 문자열로 바꾸면 되는 것이다.
밑의 그림은 strstr 함수를 후킹 하여 첫 번째 인자를 출력해보았다.
정상적으로 후킹이 되고 frida 가 출력된 것으로 보아 frida 가 포함된 문자열을 다른 문자열로 바꿔보자.
후킹 한 결과 원래 봤던 로그도 없으며 정상적으로 앱이 실행된다.
이렇게 루팅 또는 디버깅 우회 가 끝이 났다.
다시 돌아와서 이제 Level 3 에서 원하는 Secret String 을 찾아야 한다.
위의 bar 메소드 가 native 메소드이면 Level 2 에서처럼 so 파일을 분석해보아야 한다.
so 파일 을 IDA 로 열어보자.
Level 2 에서 분석한 것과 마찬가지로 CodeCheck_bar 함수를 클릭해서 분석을 해보자.
그 이유는 Level 2에 설명되어있다.
retrun 하는 result 변수가 처음에는 0 으로 설정되어있다.
이렇게 0으로 설정되어있으면 Secret String 을 맞췄다는 문구를 볼 수가 없다.
따라서 result 값이 1 이 되도록 만들어줘야 한다.
물론 Level 2에서도 bar 함수의 return 값을 1로 설정해주면 되지만 이는 출제의도가 아니다.
따라서 코드를 분석해서 Secret String 을 알아내야 한다.
Level 2를 제대로 이해를 했다면 Level 2에서와 마찬가지로 해당 코드를 보고 생각나는 것이 있을 것이다.
총길이가 24 인 것을 생각할 수 있다.
그다음은 while 문을 살펴보자.
while 문에서 == 연산이 보인다.
== 기준으로 왼쪽 값 과 오른쪽 값 이 같냐라는 것이다.
같지 않은 경우 while 문이 끝이 나며 result는 그대로 0으로 return 되기 때문에 문제를 해결하지 못한다.
따라서 while문이 이 문제의 핵심이다.
먼저 왼쪽을 보자.
먼저 v7 이 뭐고 v8 이 먼지 알아야 한다.
v8 은 코드에서 그냥 0 이다.
v7 을 알아야 하는데 v7 은 IDA에서 다음과 같이 정의되어있다.
따라서 v5+1472 이가 어떤 함수인지 어떤 역할을 하는지 알아봐야 한다.
이를 frida 를 활용해서 찍어보면 GetByteArrayElements 라는 함수가 나온다.
GetByteArrayElements 함수의 정의는 다음과 같다.
GetByteArrayElements 함수는 java의 byte 배열을 JNI로 전달하는 함수이다.
즉 앱단에서 어떤 값을 so 파일로 전달하는 함수이다.
어떤 값을 전달할까???
GetByteArrayElements가 실행 후 return 값이 담긴 v7 를 출력해보자.
이렇게 앱단에서 입력한 "test123456781264864984" 값이
GetByteArrayElements 함수가 실행된 후 v7 에 저장이 된다.
따라서 while 문의 왼쪽은 사용자가 입력한 Secret String 이라는 의미이다.
그렇다면 이제 ==을 기준으로 오른쪽 의 값이 바로 문제에서 원하는 Secret String 이라는 말이다.
따라서 우리는 알아야 할 것이 3가지 이다.
&qword_15038 + v8 , &v9 , v8 이다.
먼저 &qword_15038 을 알아보자.
먼저 &연산은 주소 값을 가져오는 의미이다.
qword_15038 에 어떠한 값이 저장되어있는데 그 메모리 주소값을 가져오는 의미이다.
그 후 v8 을 더해준다.
처음에 v8 은 0 이다.
하지만 바로 밑에서 ++연산을 통해 v8을 1씩 증가시켜주고 있다.
이는 어떠한 문자열이나 값이 배열에 저장되어있는데 index 를 한 개씩 증가시킨다는 의미이다.
즉, 24자리인 문자열이면 한 문자씩 가져온다는 의미이다.
그렇다면 qword_15038 에 어떤 값이 들어있는지 살펴보자.
qword_15038 은 Java_sg_vantagepoint_uncrackable3_MainActivity_init 함수에서 사용되었다.
따라서 Java_sg_vantagepoint_uncrackable3_MainActivity_init 함수를 분석해보자.
Java_sg_vantagepoint_uncrackable3_MainActivity_init 에서 strncpy 함수에서
qword_15038 으로 어떤 값을 copy하는데 무슨 값을? v5 에 저장되어있는어떤 값을
얼만큼? 24자리 만큼 copy 하는 것이 보인다.
이를 frida 를 이용해서 strncpy 함수의 두 번째 인자인 v5 을 출력해보자.
출력해보니 "pizzapizzapizzapizzapizz683" 라는 문자열이 나왔다.
24자리 를 qword_15038 에 copy하니 최종적으로 qword_15038 에는
"pizzapizzapizzapizzapizz" 가 저장된다.
이는 어디서 나온 것일까?라고 생각을 해보자.
qword_15038 는 Java_sg_vantagepoint_uncrackable3_MainActivity_init 함수에서 초기화되었다.
따라서 함수명에서 추측할 수 있듯이 다시 입단으로 돌아가 MainActivity 에 선언되어있는
native init 메서드가 있는지 확인해보자.
이렇게 pizzapizzapizzapizzapizz 가 init 메소드의 인자로 넘겨주었다.
따라서 현재까지 다음과 같은 값을 도출할 수 있다.
v8 은 앞에서도 설명했듯이 0~24 까지 증가한다.
따라서 pizzapizzapizzapizzapizz 라는 문자열에서
v8가 0일 때 p,
v8가 1일 때 i... v8이 24일 때 z
이렇게 한 문자씩 처리하겠다는 의미이다.
v8 가 0 일 때를 보자면 문자 'p' 와 xor 연산을 한다.
^ 가 xor 을 의미하는데 p 와 &v9+v8 값을 xor 한다는 의미이다.
&v9+v8 에서도 앞서 설명한 것처럼 한 문자씩 가져와서 p와 xor 하겠다는 것이다.
뒤에서 그림으로 설명되어있다.
그렇다면 v9 에는 어떤 값이 들어있는지 확인해보자.
v9 는 sub_10E0 함수에서 처리된다.
sub_10E0 함수에서 v9 의 인자 값을 넘겨주고 sub_10E0 함수가 끝나면 v9 에는
어떤 값이 저장되어있을 것이다.
sub_10E0 함수가 끝날 때 v9 에는 어떤값이 들어있는지 확인해보자.
sub_10E0 함수가 끝날때 v9 에는 이렇게 값이 들어가 있다.
여기까지 정리하자면 다음과 같이 도출할 수 있다.
이렇게 xor을 하는 코드를 작성해서 실행해보면 우리가 원하던 Secret String이 나온다.
이를 Level 3 에 입력해보자.
정답이다.
이번 문제는 조금 설명이 많이길었다.
최대한 자세히 step by step으로 설명하려고 노력했다.
후킹 코드나 프리다 코드는 일부러 올리지 않았다.
적어도 후킹 코드나 프리다 코드는 직접 작성해보길 바란다.
검색하면 많이 나온다.