Write-up

Uncrackable 3 write up

ch4rli3kop 2020. 7. 10. 23:33
반응형

Uncrackable 3 write up

MainActivity

다른 부분들에 대해서는 분석을 상세히 서술할 필요가 없을 것 같아, 필요한 부분들에 대해서만 간단히 짚고 넘어간다.

package sg.vantagepoint.uncrackable3;

import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Debug;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import owasp.mstg.uncrackable3.R;
import sg.vantagepoint.util.IntegrityCheck;
import sg.vantagepoint.util.RootDetection;

public class MainActivity extends AppCompatActivity {
   private static final String TAG = "UnCrackable3";
   static int tampered = 0;
   private static final String xorkey = "pizzapizzapizzapizzapizz";
   private CodeCheck check;
   Map<String, Long> crc;

   private native long baz();

   private native void init(byte[] bArr);

   /* access modifiers changed from: private */
   public void showDialog(String str) {
       AlertDialog create = new AlertDialog.Builder(this).create();
       create.setTitle(str);
       create.setMessage("This is unacceptable. The app is now going to exit.");
       create.setButton(-3, "OK", new DialogInterface.OnClickListener() {
           public void onClick(DialogInterface dialogInterface, int i) {
               System.exit(0);
          }
      });
       create.setCancelable(false);
       create.show();
  }

   private void verifyLibs() {
       this.crc = new HashMap();
       this.crc.put("armeabi-v7a", Long.valueOf(Long.parseLong(getResources().getString(R.string.armeabi_v7a))));
       this.crc.put("arm64-v8a", Long.valueOf(Long.parseLong(getResources().getString(R.string.arm64_v8a))));
       this.crc.put("x86", Long.valueOf(Long.parseLong(getResources().getString(R.string.x86))));
       this.crc.put("x86_64", Long.valueOf(Long.parseLong(getResources().getString(R.string.x86_64))));
       try {
           ZipFile zipFile = new ZipFile(getPackageCodePath());
           for (Map.Entry next : this.crc.entrySet()) {
               String str = "lib/" + ((String) next.getKey()) + "/libfoo.so";
               ZipEntry entry = zipFile.getEntry(str);
               Log.v(TAG, "CRC[" + str + "] = " + entry.getCrc());
               if (entry.getCrc() != ((Long) next.getValue()).longValue()) {
                   tampered = 31337;
                   Log.v(TAG, str + ": Invalid checksum = " + entry.getCrc() + ", supposed to be " + next.getValue());
              }
          }
           ZipEntry entry2 = zipFile.getEntry("classes.dex");
           Log.v(TAG, "CRC[" + "classes.dex" + "] = " + entry2.getCrc());
           if (entry2.getCrc() != baz()) {
               tampered = 31337;
               Log.v(TAG, "classes.dex" + ": crc = " + entry2.getCrc() + ", supposed to be " + baz());
          }
      } catch (IOException unused) {
           Log.v(TAG, "Exception");
           System.exit(0);
      }
  }

   /* access modifiers changed from: protected */
   public void onCreate(Bundle bundle) {
       verifyLibs();
       init(xorkey.getBytes());
       new AsyncTask<Void, String, String>() {
           /* access modifiers changed from: protected */
           public String doInBackground(Void... voidArr) {
               while (!Debug.isDebuggerConnected()) {
                   SystemClock.sleep(100);
              }
               return null;
          }

           /* access modifiers changed from: protected */
           public void onPostExecute(String str) {
               MainActivity.this.showDialog("Debugger detected!");
               System.exit(0);
          }
      }.execute(new Void[]{null, null, null});
       if (RootDetection.checkRoot1() || RootDetection.checkRoot2() || RootDetection.checkRoot3() || IntegrityCheck.isDebuggable(getApplicationContext()) || tampered != 0) {
           showDialog("Rooting or tampering detected.");
      }
       this.check = new CodeCheck();
       super.onCreate(bundle);
       setContentView((int) R.layout.activity_main);
  }

   public void verify(View view) {
       String obj = ((EditText) findViewById(R.id.edit_text)).getText().toString();
       AlertDialog create = new AlertDialog.Builder(this).create();
       if (this.check.check_code(obj)) {
           create.setTitle("Success!");
           create.setMessage("This is the correct secret.");
      } else {
           create.setTitle("Nope...");
           create.setMessage("That's not it. Try again.");
      }
       create.setButton(-3, "OK", new DialogInterface.OnClickListener() {
           public void onClick(DialogInterface dialogInterface, int i) {
               dialogInterface.dismiss();
          }
      });
       create.show();
  }

   static {
       System.loadLibrary("foo");
  }
}

onCreate()

    public void onCreate(Bundle bundle) {
       verifyLibs();
       init(xorkey.getBytes());
       new AsyncTask<Void, String, String>() {
           /* access modifiers changed from: protected */
           public String doInBackground(Void... voidArr) {
               while (!Debug.isDebuggerConnected()) {
                   SystemClock.sleep(100);
              }
               return null;
          }

           /* access modifiers changed from: protected */
           public void onPostExecute(String str) {
               MainActivity.this.showDialog("Debugger detected!");
               System.exit(0);
          }
      }.execute(new Void[]{null, null, null});
       if (RootDetection.checkRoot1() || RootDetection.checkRoot2() || RootDetection.checkRoot3() || IntegrityCheck.isDebuggable(getApplicationContext()) || tampered != 0) {
           showDialog("Rooting or tampering detected.");
      }
       this.check = new CodeCheck();
       super.onCreate(bundle);
       setContentView((int) R.layout.activity_main);
  }

verifyLibs()를 이용하여 로드되는 libfoo.so에 대한 crc 검사를 진행한 뒤, "pizzapizzapizzapizzapizz" xor key를 인자로 JNI init method를 실행한다. 그 뒤, AsyncTask를 이용하여 디버거 탐지하는 쓰레드를 새로 생성한다. RootDetection 클래스의 메소드들로 루팅 탐지를 진행한다.

verify()

다른 문제와 마찬가지로 activity_main.xml을 살펴보면 verify 버튼을 눌렀을 때, verify()가 호출되는 것을 확인할 수 있다.

...
<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/button_verify" android:onClick="verify"/>
...

verify()는 다음과 같다.

   public void verify(View view) {
       String obj = ((EditText) findViewById(R.id.edit_text)).getText().toString();
       AlertDialog create = new AlertDialog.Builder(this).create();
       if (this.check.check_code(obj)) {
           create.setTitle("Success!");
           create.setMessage("This is the correct secret.");
      } else {
           create.setTitle("Nope...");
           create.setMessage("That's not it. Try again.");
      }
       create.setButton(-3, "OK", new DialogInterface.OnClickListener() {
           public void onClick(DialogInterface dialogInterface, int i) {
               dialogInterface.dismiss();
          }
      });
       create.show();
  }

사용자가 입력한 값을 check.check_code()를 통하여 검사하는 것을 확인할 수 있다.

CodeCheck

check_code()

package sg.vantagepoint.uncrackable3;

public class CodeCheck {
   private static final String TAG = "CodeCheck";

   private native boolean bar(byte[] bArr);

   public boolean check_code(String str) {
       return bar(str.getBytes());
  }
}

JNI native function bar를 호출한다.

libfoo.so (x86)

다음 내용에서 다룰 함수들의 이름은 그냥 내가 대충 지은거다.

init_array

elf 파일은 오브젝트 파일이 로드될 때, 특정 배열에 등록된 함수들을 실행하게 된다. 이 배열을 init_array라고 하는데, libfoo.so elf 파일에는 다음과 같이 특정함수가 init_array로 등록되어, 메모리에 로드될 때 실행되는 것을 확인할 수 있다.

LOAD:00005E74 ; ELF Initialization Function Table
LOAD:00005E74                 dd offset protection2

protection2()

unsigned int protection2()
{
 pthread_t newthread; // [esp+Ch] [ebp-18h]
 unsigned int v2; // [esp+10h] [ebp-14h]

 v2 = __readgsdword(0x14u);
 pthread_create(&newthread, 0, (void *(*)(void *))monitoring_frida, 0);
 dword_6020 = 0;
 xor_key = 0;
 dword_6028 = 0;
 dword_6024 = 0;
 byte_6034 = 0;
 dword_6030 = 0;
 dword_602C = 0;
 ++check_flag;
 return __readgsdword(0x14u);
}

여러 전역 변수들을 초기화하고, monitoring_frida()를 수행하는 쓰레드를 하나 생성한다.

monitoring_frida()

void __noreturn monitoring_frida()
{
 FILE *v0; // esi
 char s; // [esp+1Ch] [ebp-210h]

 v0 = fopen("/proc/self/maps", "r");
 if ( v0 )
{
   do
  {
     while ( !fgets(&s, 512, v0) )
    {
       fclose(v0);
       usleep(0x1F4u);
       v0 = fopen("/proc/self/maps", "r");
       if ( !v0 )
         goto LABEL_7;
    }
  }
   while ( !strstr(&s, "frida") && !strstr(&s, "xposed") );
}
LABEL_7:
 __android_log_print();
 goodbye();
}

maps 파일은 해당 프로세스의 메모리 공간이 어떻게 구성되어있는지 나타내는 파일이다. 해당 파일을 계속해서 읽으면서 "frida"나 "xposed" 문자열이 있는지 검사하고, 만약 해당 문자열이 발견된다면 프로그램을 종료시킨다.

실제 frida를 사용하는 경우, 다음과 같이 메모리 공간에 frida-agent-32.so가 로드되는 것을 확인할 수 있다.

root@shamu:/ # cat /proc/9757/maps  | grep frida
a2f58000-a30db000 r-xp 00000000 08:13 3506187   /data/local/tmp/re.frida.server/frida-agent-32.so
a30db000-a30dc000 rwxp 00183000 08:13 3506187   /data/local/tmp/re.frida.server/frida-agent-32.so
a30dc000-a4355000 r-xp 00184000 08:13 3506187   /data/local/tmp/re.frida.server/frida-agent-32.so
a4355000-a439c000 r--p 013fc000 08:13 3506187   /data/local/tmp/re.frida.server/frida-agent-32.so
a439c000-a43e4000 rw-p 01443000 08:13 3506187   /data/local/tmp/re.frida.server/frida-agent-32.so

init()

int __cdecl Java_sg_vantagepoint_uncrackable3_MainActivity_init(JNIEnv *env, jobject jobj, int a3)
{
 JNIEnv *v3; // edi
 const char *v4; // esi
 int result; // eax

 v3 = env;
 protection();
 v4 = (*v3)->GetByteArrayElements(v3, (jbyteArray)a3, 0);
 strncpy((char *)&xor_key, v4, 0x18u);
 result = ((int (__cdecl *)(JNIEnv *, int, const char *, signed int))(*v3)->ReleaseByteArrayElements)(v3, a3, v4, 2);
 ++check_flag;
 return result;
}

MainActivity onCreate()에서 실행되는 init()에 대한 JNI method이다. 인자로 가져간 xor key 데이터를 c에서 사용하는 타입으로 바꿔주고, strncpy()를 이용하여 xor_key로 사용할 전역 변수에 저장한다.

protection()

unsigned int protection()
{
 __pid_t v0; // esi
 int v2; // [esp+0h] [ebp-1Ch]
 int stat_loc; // [esp+4h] [ebp-18h]
 unsigned int v4; // [esp+8h] [ebp-14h]

 v4 = __readgsdword(0x14u);
 if ( fork() )
{
   pthread_create((pthread_t *)&v2, 0, (void *(*)(void *))thread_exit, 0);
}
 else
{
   v0 = getppid();
   if ( !ptrace(PTRACE_ATTACH, v0, 0, 0) )
  {
     waitpid(v0, &stat_loc, 0);
     while ( 1 )
    {
       ptrace(PTRACE_CONT, v0, 0);
       if ( !waitpid(v0, &stat_loc, 0) )
         break;
       if ( (stat_loc & 0x7F) != 127 )
         _exit(0);
    }
  }
}
 return __readgsdword(0x14u);
}

ptrace 를 이용하여 프로세스의 상태 값을 가져와서 비교하며, 현재 프로그램이 디버깅 중인지 탐지한다.

bar()

char __cdecl Java_sg_vantagepoint_uncrackable3_CodeCheck_bar(JNIEnv *env, jobject jobj, char *string)
{
 JNIEnv *_env; // edi
 int v4; // esi
 unsigned int idx; // eax
 _BYTE *key_value; // ecx
 bool v7; // zf
 char result; // al
 char string_[24]; // [esp+0h] [ebp-3Ch]
 char v10; // [esp+18h] [ebp-24h]
 unsigned int v11; // [esp+28h] [ebp-14h]

 v11 = __readgsdword(0x14u);
 *(_OWORD *)string_ = 0LL;
 *(_DWORD *)&string_[20] = 0;
 *(_DWORD *)&string_[16] = 0;
 v10 = 0;
 if ( check_flag == 2 )
{
   _env = env;
   load_string(string_);
   v4 = (int)(*_env)->GetByteArrayElements(_env, string, 0);
   if ( (*_env)->GetArrayLength(_env, string) == 24 )// length
  {
     idx = 0;
     key_value = &xor_key;
     while ( *(_BYTE *)(v4 + idx) == ((unsigned __int8)string_[idx] ^ *key_value) )
    {
       ++idx;
       ++key_value;
       v7 = idx == 24;
       if ( idx >= 0x18 )
      {
         result = 1;
         if ( v7 )
           return result;
         return 0;
      }
    }
  }
}
 return 0;
}

check_flag는 앞서 두 개의 protection이 실행될 때 1씩 증가하는 전역변수이다. protection이 모두 정상적으로 실행되었을 때, 실행되는 함수임을 알 수 있고, load_string()을 통해 string_ 변수에 특정 문자열을 불러온 뒤 xor_key로 xor 시킨 값과 사용자가 입력한 값을 비교함으로써 참 or 거짓을 리턴한다.

load_string()으로 생긴 string_ 데이터를 알아낸다면 xor_key와 xor 연산하여 참이되는 문자열을 알아낼 수 있을 것이다.

Hooking

다음과 같이 총 세가지 함수에 대해서 후킹을 함으로써 이 문제를 해결할 수 있다.

  1. Frida check를 우회하기 위해 libc.so의 strstr()의 리턴 값을 항상 0(false)로 변조한다.

  2. System.exit()을 implementation 함으로써 루팅을 탐지해도 프로그램이 종료되지 않게 한다.

  3. libfoo.so의 load_string()을 후킹하여 결과 값으로 생긴 string_ 값을 읽는다.

Script

// D:\Android Study>frida -l uc3.js -f owasp.mstg.uncrackable3 -U
var t = Module.findExportByName('libc.so', 'strstr');
console.log("strstr @ " + t.toString());

var xor_key = 'pizzapizzapizzapizzapizz';

Interceptor.attach(t, {
   onEnter: function(args){
       //console.log('XXXXXXXXXX');
  },
   onLeave: function(retval){
       //console.log('strstr called!');
       retval.replace(0);
  }
});

function native_hook(){
   var foo = Module.getBaseAddress('libfoo.so');
   if (!foo){
       console.log('libfoo not loaded!');
       return 0;
  }
   console.log('[+] libfoo.so @ ' + foo.toString());
   var target = foo.add(0xfa0);
   var save;
   Interceptor.attach(target, {
       onEnter: function(args){
           console.log('OnEnter :');
           save = ptr(args[0]);
           console.log(save);
           // console.log(Memory.read)
      },
       onLeave: function(retval){
           console.log('OnLeave :');
           var data2 = Memory.readByteArray(save, 0x18);
           console.log(data2);
           var data = new Uint8Array(data2);
           var result = '';
           for (var i=0; i<0x18; i++){
               var tmp = String.fromCharCode(data[i] ^ xor_key[i].charCodeAt());
               //console.log(tmp);
               result += tmp;
          }
           console.log(result);
      }
  });
}

setImmediate(function(){
   Java.performNow(function(){
       console.log('Hooking Start!');
       
       var System = Java.use("java.lang.System");
       System.exit.overload('int').implementation = function(args){
           console.log("System.exit called");
           native_hook();
      };
   
       // while(true){
       //     try{
       //         var foo = Module.getBaseAddress('libfoo.so');
       //         console.log('[+] libfoo.so @ ' + foo.toString());
       //         break;
       //     } catch (e){
       //         continue;
       //     }
       // }
       // if (!foo){
       //     console.log('[+] libfoo.so @ ' + foo.toString());
       // }
   
  });

});

사실 Native function에 대해서는 Java vm 내부에서 실행되는 것이 아니므로 Java.perfom에서 후킹할 필요가 없다. 또한 이 문제의 경우 너무 늦게 하면 frida 탐지에 이미 걸리기 때문에 libc strstr()은 가장 먼저 변조한다. 리턴 값을 항상 0으로 고정시킴으로써 frida 탐지를 우회하였다.

다음으로는 앞선 문제들과 마찬가지로 System.exit을 후킹하여 프로세스가 종료되지 않도록 하였고, 위에서 잠깐 언급했었던 libfoo.so의 load_string() 함수를 후킹하여 로딩된 문자열을 가져와서 key와 xor하여 답을 구하였다.

libfoo.so 라이브러리가 클래스가 로드된 뒤에 메모리에 로드되기 때문에, libc처럼 일찍 후킹하면 라이브러리를 찾을 수 없다. 후킹하는 타이밍이 중요하기 때문에, 아예 System.exit이 실행될 때 load_string()을 후킹함으로써 libfoo.so가 메모리에 로드된 뒤에 후킹할 수 있도록 타이밍을 맞췄음.

Result

D:\Android Study>frida -l uc3.js -f owasp.mstg.uncrackable3 -U
    ____
  / _ |   Frida 12.6.18 - A world-class dynamic instrumentation toolkit
  | (_| |
  > _ |   Commands:
  /_/ |_|       help      -> Displays the help system
  . . . .       object?   -> Display information about 'object'
  . . . .       exit/quit -> Exit
  . . . .
  . . . .   More info at http://www.frida.re/docs/home/
Spawning `owasp.mstg.uncrackable3`...
strstr @ 0xb7450b00
Spawned `owasp.mstg.uncrackable3`. Use %resume to let the main thread start executing!
[Samsung SM-G965N::owasp.mstg.uncrackable3]-> Hooking Start!
[Samsung SM-G965N::owasp.mstg.uncrackable3]-> %resume
[Samsung SM-G965N::owasp.mstg.uncrackable3]-> System.exit called
[+] libfoo.so @ 0xaf428000
OnEnter :
0xbfee1c00
OnLeave :
          0  1  2  3  4  5  6  7  8  9 A B C D E F 0123456789ABCDEF
00000000 1d 08 11 13 0f 17 49 15 0d 00 03 19 5a 1d 13 15 ......I.....Z...
00000010  08 0e 5a 00 17 08 13 14                         ..Z.....
[Result] : making owasp great again


반응형

'Write-up' 카테고리의 다른 글

[ASIS CTF Quals 2020] Full protection  (0) 2020.07.12
[ASIS CTF Quals 2020] Baby note  (0) 2020.07.12
[Defenit CTF 2020] warmup writeup  (0) 2020.06.14
[bytebandits 2020] baby_rust writeup  (0) 2020.04.14
[bytebandits 2020] fmt-me writeup  (0) 2020.04.14