Home [Hackthebox] Joker
Post
Cancel

[Hackthebox] Joker

풀이를 적기 전에 앞으로는 존댓말로 적으려고 합니다. 제가 쓴 글을 가끔 다시 보면 매우 딱딱하게 느껴지는 게 있네요. 구글링을 할 때도 이렇게 적는 게 더 나은 것 같기도 하고요. 그리고 아직은 사이트맵 등록을 하지 않아서 검색으로 글을 볼 수 없지만 언젠가는 할 거기 때문에 블로그의 목적에 맞게끔 글을 적기 위함입니다. 암튼 그렇습니다.

키워드: Mobile, Reversing

description

코드 분석


앱을 설치하고 실행해보면 다음과 같이 게임이 나옵니다. 하지만 그 외에 별다른 기능은 없었습니다. 제목답게 Why so serious?라고 적혀있네요.
game

바로 jadx를 이용하여 디컴파일해보면 아래와 같이 onCreate시 1732145681(epoch time)이면 a.f40a를 str 변수에 할당합니다. 하지만 이 시간이 맞지 않기 때문에 내부 로직이 실행되지 않습니다.
oncreate

f40a에 어떤 문자열이 담기는지 보기위해서 선언부분으로 가보면 아래와 같이 c.a.o()를 이용하여 결과값을 담습니다.

1
public static String f40a = c.a.o(new StringBuffer("Z3qSpRpRxWs"), new StringBuffer("3\\^>_>_>W"));


c.a.o()의 로직은 단순히 arg1와 arg2를 XOR 연산하는 것을 알 수 있습니다. 따라서 저 문자열들을 XOR하면 읽을 수 있는 문자열이 나오겠죠?
cao

빠른 동적분석을 위해서 아래와 같이 코드를 패치하고 앱을 설치하였습니다. 코드 패치 방법은 이 글을 참고해주세요.
patch1

그리고 호출되는 함수를 확인하기 위해서 a2.a 클래스와 c.a 클래스를 추적 해보면, a2.a.b()에서 c.a.o()를 호출하고 해당 메서드에서 특정 url을 리턴하고 있습니다.
url

a2.a.b()의 로직을 살펴보면 url로 GET 요청을 보내는데 응답코드가 200이 아니어서 a2.a.a()가 실행되지 않는 것으로 보입니다.
a2ab

그래서 귀찮지만 getResponseCode()!=200으로 다시 코드를 패치하고 호출되는 함수를 살펴보았습니다. 앱이 알 수 없는 이유로 강제종료되지만 아래와 같이 a2.a.a()에 진입한 모습을 볼 수 있습니다.
patch2

a2.a.a()는 다음과 같습니다. 메서드 길이가 길기 때문에 간단하게 요약하자면 먼저 assets/io/m/l/l/d 에있는 파일들을 리스트화합니다. 그다음 for문을 돌게되는데 파일이름이 301.txt로 끝날 때 if문 내부로 진입하고 최종적으로 c.a.v()를 호출합니다.

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
// a.java: 44
public static void a(Context context, String str) {
        String[] list;
        try {
            Method method = context.getClass().getMethod(c.a.o(new StringBuffer("FAUeRWDPR"), new StringBuffer("!$")), new Class[0]); // getAssets
            for (String str2 : ((Resources) context.getClass().getMethod(c.a.o(new StringBuffer("TVGaV@\\FAPV@"), new StringBuffer("3")), new Class[0]).invoke(context, new Object[0])).getAssets().list(str)) {
                try { 
                    if (str2.endsWith(c.a.o(new StringBuffer("spqn484"), new StringBuffer("@")))) { // 301.txt
                        StringBuffer stringBuffer = new StringBuffer();
                        stringBuffer.append("ma1");
                        stringBuffer.append("7FEC");
                        InputStream open = ((AssetManager) method.invoke(context, new Object[0])).open(f40a + str2);
                        File file = new File(context.getCacheDir(), c.a.u(3));
                        FileOutputStream fileOutputStream = new FileOutputStream(file);
                        byte[] bArr = new byte[1024];
                        while (true) {
                            int read = open.read(bArr);
                            if (-1 == read) {
                                break;
                            }
                            fileOutputStream.write(bArr, 0, read);
                        }
                        open.close();
                        fileOutputStream.flush();
                        fileOutputStream.close();
                        c.a.f1860a = new String(stringBuffer).concat("2_l").concat("Yuo").concat("NQ").concat("$_To").concat("T99u_e0kINhw_Bzy");
                        c.a.v(context, file.getPath(), c.a.f1860a, new File(context.getCacheDir(), c.a.u(2).concat(".temp")).getPath());
                    }
                    Log.e("fileName", str2);
                } catch (Exception e2) {
                    e2.printStackTrace();
                }
            }
        } catch (IOException | NoSuchMethodException unused) {
        } catch (IllegalAccessException e3) {
            e = e3;
            e.printStackTrace();
        } catch (InvocationTargetException e4) {
            e = e4;
            e.printStackTrace();
        }
    }


실제로 assets 디렉토리를 확인해보면 아래와 같습니다. 그리고 txt 확장자를 가지지만 모두 바이너리 형태였습니다.
assets

그럼 이제 c.a.v()의 로직을 살펴봅시다. 두 번째 인자인 str2를 key로 설정합니다. 그리고 AES 알고리즘을 이용하는데 cipher.init()의 첫 번째 인자가 2(DECRYPT_MODE)인 것을 보아 복호화를 진행하는 것을 알 수 있습니다. 즉, str의 내용을 AES 복호화를 이용하여 str3 파일에 쓰게 됩니다. 그렇다면 c.a.v()의 세 번째 인자만 알면 복호화된 내용을 확인할 수 있습니다.

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
// a.java: 626
public static void v(Context context, String str, String str2, String str3) {
        if (TextUtils.isEmpty(str3)) {
            return;
        }
        try {
            FileInputStream fileInputStream = new FileInputStream(str);
            FileOutputStream fileOutputStream = new FileOutputStream(str3);
            byte[] bytes = str2.getBytes();
            MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
            SecretKeySpec secretKeySpec = new SecretKeySpec(Arrays.copyOf(messageDigest.digest(bytes), 16), "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(2, secretKeySpec, new IvParameterSpec(Arrays.copyOf(messageDigest.digest(bytes), 16)));
            CipherInputStream cipherInputStream = new CipherInputStream(fileInputStream, cipher);
            byte[] bArr = new byte[8];
            while (true) {
                int read = cipherInputStream.read(bArr);
                if (read == -1) {
                    System.load(str3);
                    JokerNat.goto2((AssetManager) context.getClass().getMethod(o(new StringBuffer("FAUeRWDPR"), new StringBuffer("!$")), new Class[0]).invoke(context, new Object[0]));
                    fileOutputStream.flush();
                    fileOutputStream.close();
                    cipherInputStream.close();
                    return;
                }
                fileOutputStream.write(bArr, 0, read);
            }
        } catch (FileNotFoundException | UnsupportedEncodingException | IOException | IllegalAccessException | NoSuchMethodException | InvocationTargetException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException e2) {
            e2.printStackTrace();
        }
    }


/data/user/0/meet.the.joker/cache/ll.temp를 확인해 보면 될 것 같습니다.
cav

file 명령어로 확인해보니 ELF 포맷을 갖춘 파일이었습니다. (뒤에 NDK(Native Development Kit)는 C 또는 C++를 이용하여 네이티브 코드로 앱의 일부를 구현할 수 있도록 하는 tool kit을 뜻합니다) 즉, 라이브러리 파일임을 유추할 수 있습니다.
ll

라이브러리 분석


ida를 이용하여 열어보면 빠르게 a()를 볼 수 있습니다. 20~23 번째 줄에서 71q3q2q2q:q;7<;.610;0+3<;,-;mnnp*&*^XOR합니다.

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
void __fastcall a(JNIEnv *a1, int a2, jobject assetManager)
{
  AAssetManager *v3; // x19
  unsigned __int64 v4; // x22
  char v5; // w23
  unsigned __int64 v6; // x0
  AAsset *v7; // x0
  AAsset *v8; // x19
  off_t v9; // w20
  void *v10; // x21

  v3 = AAssetManager_fromJava(a1, assetManager);
  if ( time(0LL) >= 1762355499LL )
  {
    if ( __strlen_chk("71q3q2q2q:q;7<;.610;0+3<;,-;mnnp*&*", 0x24u) )
    {
      v4 = 0LL;
      do
      {
        v5 = fn[v4];
        v6 = __strlen_chk("^", 2u);
        fn[v4] = k[v4 - v4 / v6 * v6] ^ v5;
        ++v4;
      }
      while ( __strlen_chk("71q3q2q2q:q;7<;.610;0+3<;,-;mnnp*&*", 0x24u) > v4 );
    }
    v7 = AAssetManager_open(v3, "71q3q2q2q:q;7<;.610;0+3<;,-;mnnp*&*", 0);
    if ( v7 )
    {
      v8 = v7;
      v9 = AAsset_getLength(v7);
      v10 = malloc(v9 + 1);
      AAsset_read(v8, v10, v9);
      *((_BYTE *)v10 + v9) = 0;
      d((char *)v10, v9);
      AAsset_close(v8);
      free(v10);
    }
  }
}


CyberChef를 통해서 복호화를 해보면 io/m/l/l/d/eibephonenumberse300.txt 인 것을 알 수 있습니다.
300

다시 잠깐 생각해보면 a()가 assetManager를 인자로 받는 점과 JokerNat 클래스에서 native 키워드가 붙은 goto2()assetManager 타입을 인자로 받는 것을 보면 a()는 goto2()임을 유추할 수 있습니다.
goto2

그리고 27 번째 줄부터 보면 eibephonenumberse300.txt의 데이터와 size를 d() 메서드의 인자로 전달합니다.
d

d()은 아래처럼 생겼습니다.

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
89
90
91
92
93
94
95
96
97
FILE *__fastcall d(char *a1, signed int a2)
{
  char *v3; // x19
  const char *v4; // x10
  unsigned __int64 v5; // x8
  __int64 v6; // x9
  char *v7; // x13
  char v8; // w14
  unsigned __int64 v9; // x24
  char v10; // w25
  unsigned __int64 v11; // x0
  unsigned __int64 v12; // x24
  char v13; // w25
  unsigned __int64 v14; // x0
  unsigned __int64 v15; // x24
  char v16; // w25
  unsigned __int64 v17; // x0
  FILE *result; // x0
  FILE *v19; // x21
  __int128 v20[8]; // [xsp+0h] [xbp-90h] BYREF
  __int64 v21; // [xsp+88h] [xbp-8h]

  v3 = a1;
  v21 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
  if ( a2 >= 1 )
  {
    v4 = "The flag is:";
    v5 = 0LL;
    v6 = (unsigned int)a2;
    v7 = a1;
    do
    {
      v8 = v4[-12 * (v5 / 0xC)];
      ++v5;
      --v6;
      ++v4;
      *v7++ ^= v8;
    }
    while ( v6 );
  }
  v20[6] = 0u;
  v20[7] = 0u;
  v20[4] = 0u;
  v20[5] = 0u;
  v20[2] = 0u;
  v20[3] = 0u;
  v20[0] = 0u;
  v20[1] = 0u;
  if ( __strlen_chk("-fcvc-fcvc-oggv,vjg,hmigp-k", 0x1Cu) )
  {
    v9 = 0LL;
    do
    {
      v10 = dPath[v9];
      v11 = __strlen_chk(kdPath, 2u);
      dPath[v9] = kdPath[v9 - v9 / v11 * v11] ^ v10;
      ++v9;
    }
    while ( __strlen_chk("-fcvc-fcvc-oggv,vjg,hmigp-k", 0x1Cu) > v9 );
  }
  if ( stat("-fcvc-fcvc-oggv,vjg,hmigp-k", (struct stat *)v20) == -1 )
  {
    if ( __strlen_chk("-fcvc-fcvc-oggv,vjg,hmigp-k", 0x1Cu) )
    {
      v12 = 0LL;
      do
      {
        v13 = dPath[v12];
        v14 = __strlen_chk(kdPath, 2u);
        dPath[v12] = kdPath[v12 - v12 / v14 * v14] ^ v13;
        ++v12;
      }
      while ( __strlen_chk("-fcvc-fcvc-oggv,vjg,hmigp-k", 0x1Cu) > v12 );
    }
    mkdir("-fcvc-fcvc-oggv,vjg,hmigp-k", 0x180u);
  }
  if ( __strlen_chk("-fcvc-fcvc-oggv,vjg,hmigp-k", 0x1Cu) )
  {
    v15 = 0LL;
    do
    {
      v16 = dPath[v15];
      v17 = __strlen_chk(kdPath, 2u);
      dPath[v15] = kdPath[v15 - v15 / v17 * v17] ^ v16;
      ++v15;
    }
    while ( __strlen_chk("-fcvc-fcvc-oggv,vjg,hmigp-k", 0x1Cu) > v15 );
  }
  result = fopen("-fcvc-fcvc-oggv,vjg,hmigp-k", "wb");
  if ( result )
  {
    v19 = result;
    fwrite(v3, a2, 1u, result);
    result = (FILE *)fclose(v19);
  }
  return result;
}


27 번째 줄을 보면 위와 마찬가지로 XOR 연산을 하는 것을 알 수 있습니다. 여기서는 인자로 들어온 eibephonenumberse300.txt의 data와 The flag is:를 XOR합니다.
27

마지막으로 75 번째 줄부터 보면 /data/data/meet.the.joker/i에 위의 XOR 연산을 한 내용을 씁니다.
75

하지만 위의 디렉토리로 직접 가보니 해당 파일이 존재하지 않았습니다. 그래서 jadx를 통해서 eibephonenumberse300.txt를 추출하고 The flag is: 와 XOR 하였더니 아래와 같이 dex파일 시그니처를 확인할 수 있었습니다.
dex

파일을 다운받고 jadx로 열어서 플레그를 확인할 수 있었습니다.
flag

나온지 2주정도된 따끈따끈한 모바일 문제를 풀어보았습니다. 난이도가 HARD여서 좀 두려웠는데 모두 XOR 연산으로 이루어져 있어서 절차가 좀 많다 뿐이지 개인적으로 어려운 문제는 아닌 듯합니다. 이렇게 말하지만 좀 막히는 부분이 있었는데 m2 맥북 환경에서 버전이 조금 낮은 ida를 통해서 라이브러리를 열었을 때 분석이 제대로 되지 않는 부분이 있었습니다. 그래서 The flag is:를 보고 왜 뒤는 안알려주지?라는 생각밖에 못했습니다ㅋㅋㅋㅋ 그래서 윈도우컴으로 다시 열어서 풀 수 있었습니다. 분명 라이브러리라고 생각했는데 a()가 goto2 메서드 임을 바로 떠올리지 못한 점이 아쉽습니다.

Reference


[TIL] Mass Assignment Vulnerability in Spring Boot

[Tip] Time based SQLi with self join

Comments powered by Disqus.