OWASP Crackme 두 번째 문제인 Uncrackable level 2이다.

jadx-gui로 파일을 열어서 Uncrackable2 패키지의 MainActivity 코드를 확인 해 보면 아래와 같다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package sg.vantagepoint.uncrackable2;

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.c;
import android.view.View;
import android.widget.EditText;
import owasp.mstg.uncrackable2.R;
import sg.vantagepoint.a.a;
import sg.vantagepoint.a.b;

public class MainActivity extends c {
    private CodeCheck m;

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

    /* access modifiers changed from: private */
    public void a(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 native void init();

    /* access modifiers changed from: protected */
    public void onCreate(Bundle bundle) {
        init();
        if (b.a() || b.b() || b.c()) {
            a("Root detected!");
        }
        if (a.a(getApplicationContext())) {
            a("App is debuggable!");
        }
        new AsyncTask<Void, String, String>() {
            /* access modifiers changed from: protected */
            /* renamed from: a */
            public String doInBackground(Void... voidArr) {
                while (!Debug.isDebuggerConnected()) {
                    SystemClock.sleep(100);
                }
                return null;
            }

            /* access modifiers changed from: protected */
            /* renamed from: a */
            public void onPostExecute(String str) {
                MainActivity.this.a("Debugger detected!");
            }
        }.execute(new Void[]{null, null, null});
        this.m = new CodeCheck();
        super.onCreate(bundle);
        setContentView((int) R.layout.activity_main);
    }

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

전체적인 흐름은 Uncrackable level 1과 비슷하다. onCreate() 메소드에 아래의 부분이 추가되었는데, 이는 디버거가 연결되어 있을 경우 SystemClock.sleep(100)을 실행시켜 어플리케이션을 중지시키는 코드이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
new AsyncTask<Void, String, String>() {
    /* access modifiers changed from: protected */
    /* renamed from: a */
    public String doInBackground(Void... voidArr) {
        while (!Debug.isDebuggerConnected()) {
            SystemClock.sleep(100);
        }
        return null;
    }
    /* access modifiers changed from: protected */
    /* renamed from: a */
    public void onPostExecute(String str) {
        MainActivity.this.a("Debugger detected!");
    }
}.execute(new Void[]{null, null, null});

verify()를 보면 이번에는 this.m.a() 메소드의 결과가 참일 경우 문제를 풀 수 있다.

this.monCreate() 메소드의 하단에 this.m = new CodeCheck();와 같이 선언되어 있다.

CodeCheck() 클래스의 코드는 아래와 같다.

1
2
3
4
5
6
7
8
9
package sg.vantagepoint.uncrackable2;

public class CodeCheck {
    private native boolean bar(byte[] bArr);

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

a() 메소드는 입력 한 값을 인자로 bar() 메소드를 호출하는데, 상단을 확인 해 보면 bar() 메소드 앞에 native가 있는 것을 확인할 수 있다. 이는 JNI(Java Native Interface)로, 자바코드에서 다른 언어들로 작성된 라이브러리를 호출하거나 반대로 호출되게 하는 프레임워크이다.

자바 네이티브 인터페이스

Native 메소드를 확인하기 위해서는 사용하는 라이브러리를 확인하고, 해당 라이브러리에서 메소드를 찾으면 된다. 앞서 확인 한 MainActivity의 상단에 라이브러리를 로드하는 부분이 존재한다.

1
2
3
static {
    System.loadLibrary("foo");
}

foo라는 라이브러리를 로드하는 코드로, 라이브러리를 보기 위해서는 어플리케이션을 디컴파일 해야 한다. 디컴파일 방법은 아래 링크를 참조하면 된다.

[Android] 안드로이드 어플리케이션 디컴파일 및 리패키징

디컴파일을 하면 아래와 같은 폴더를 확인할 수 있는데, 이 중 lib 폴더에서 라이브러리를 찾으면 된다.

lib 폴더 아래에 각 환경별로 라이브러리가 있을텐데, 이 중 각자 환경에 맞는 폴더에서 라이브러리를 확인하면 된다. 나는 x86 환경이기 때문에 x86 폴더에서 라이브러리를 확인했다. 라이브러리 명은 lib<호출 시 사용한 이름>.so 이다. 이 경우 라이브러리를 호출할 때 foo라는 이름을 사용했으므로, 라이브러리의 실제 이름은 libfoo.so가 된다.

libfoo.so 파일이 있는 것을 확인할 수 있다. 라이브러리 파일은 IDA를 사용하여 분석해야 한다. IDA를 사용 해 파일을 열어보면 여러 함수들이 존재하는데, 이 중 호출한 함수 명을 찾으면 된다. 함수명 앞에 Java가 붙고 뒤에 패키지명과 클래스 명, 함수명이 차례로 나온다. 때문에 bar 함수는 Java_sg_vantagepoint_uncrackable2_CodeCheck_bar로 되어있으며, 코드는 아래와 같다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
signed int __cdecl Java_sg_vantagepoint_uncrackable2_CodeCheck_bar(int a1, int a2, int a3)
{
  const char *v3; // esi
  signed int result; // eax
  int s2; // [esp+0h] [ebp-2Ch]
  int v6; // [esp+4h] [ebp-28h]
  int v7; // [esp+8h] [ebp-24h]
  int v8; // [esp+Ch] [ebp-20h]
  __int16 v9; // [esp+10h] [ebp-1Ch]
  int v10; // [esp+12h] [ebp-1Ah]
  __int16 v11; // [esp+16h] [ebp-16h]
  unsigned int v12; // [esp+18h] [ebp-14h]

  v12 = __readgsdword(0x14u);
  if ( byte_4008 != 1 )
    goto LABEL_9;
  s2 = 1851877460;
  v6 = 1713402731;
  v7 = 1629516399;
  v8 = 1948281964;
  v9 = 25960;
  v10 = 1936287264;
  v11 = 104;
  v3 = (const char *)(*(int (__cdecl **)(int, int, _DWORD))(*(_DWORD *)a1 + 736))(a1, a3, 0);
  if ( (*(int (__cdecl **)(int, int))(*(_DWORD *)a1 + 684))(a1, a3) != 23 )
    goto LABEL_9;
  if ( !strncmp(v3, (const char *)&s2, 0x17u) )
    result = 1;
  else
LABEL_9:
    result = 0;
  return result;
}

입력받은 값과 라이브러리에 선언되어 있는 값을 strncmp로 비교하며, 그 두 값이 같을 때 1을 반환 해 준다. 0x17은 10진수로 23이므로, secret의 글자 수는 23글자라는 것을 알 수 있다. 비교 대상인 s2 ~ v11 변수에 선언되어 있는 값을 char로 변경하면 각각 아래와 같다.

1
2
3
4
5
6
7
  s2 = 'nahT';
  v6 = 'f sk';
  v7 = 'a ro';
  v8 = 't ll';
  v9 = 'eh';
  v10 = 'sif ';
  v11 = 'h';

이를 쭉 연결 해 보면 Thanks for all the fish인데, 사실 이 값이 secret이다. ㅎㅎ

그런데 나는 native 메소드를 Hooking 하는 방법을 알고 싶은 것이라 Frida를 사용하여 문제를 풀어보려 한다. 비교 대상인 값을 찾기 위해서는 strncmp를 Hooking하는 것이 편하다. 코드는 아래와 같다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import frida, sys

def on_message(message, data):
	print(message)

PACKAGE_NAME = "owasp.mstg.uncrackable2"

jscode = """
console.log("[+] Start Script");

	Java.perform(function() {
		console.log("[+] Hooking System.exit");
		var exitClass = Java.use("java.lang.System");
		exitClass.exit.implementation = function() {
			console.log("[+] System.exit called");
		}
	});

	Interceptor.attach (Module.findExportByName ("libfoo.so", "strncmp"), {
		onEnter: function (args) {
			if(args[2].toInt32() == 23) {
				console.log(Memory.readUtf8String(args[1], 23));
			}
		}
	});
"""

process = frida.get_usb_device(1).attach(PACKAGE_NAME)
script = process.create_script(jscode)
script.on('message', on_message)
print('[+] Running Hook')
script.load()
sys.stdin.read()

루팅 탐지 로직이 있으므로, System.exit()를 Hooking하여 어플리케이션이 종료되지 않도록 해야한다. bar() 함수를 후킹하는 코드는 아래와 같다.

1
2
3
4
5
6
7
	Interceptor.attach (Module.findExportByName ("libfoo.so", "strncmp"), {
		onEnter: function (args) {
			if(args[2].toInt32() == 23) {
				console.log(Memory.readUtf8String(args[1], 23));
			}
		}
	});

libfoo.so 라이브러리의 strncmp 함수를 Hooking하는 코드이며, 만약 bar 함수를 Hooking해야 할 경우 함수의 전체 명(IDA에서 나오는 전체 함수명)인 Java_sg_vantagepoint_uncrackable2_CodeCheck_bar를 입력 해 주어야 한다. if(args[2].toInt32() == 23)strncmp를 Hooking할 경우 너무 많은 결과값이 나와서 bar 함수에서 호출될 때만 인자를 확인하려 넣은 조건문이다. 때문에 23글자를 입력해야 bar 함수에서 호출 한 strncmp 함수의 인자를 확인할 수 있다.

결과로 나온 값을 입력하면 Success를 확인할 수 있다.