풀이를 적기 전에 앞으로는 존댓말로 적으려고 합니다. 제가 쓴 글을 가끔 다시 보면 매우 딱딱하게 느껴지는 게 있네요. 구글링을 할 때도 이렇게 적는 게 더 나은 것 같기도 하고요. 그리고 아직은 사이트맵 등록을 하지 않아서 검색으로 글을 볼 수 없지만 언젠가는 할 거기 때문에 블로그의 목적에 맞게끔 글을 적기 위함입니다. 암튼 그렇습니다.
키워드: Mobile, Reversing
코드 분석
앱을 설치하고 실행해보면 다음과 같이 게임이 나옵니다. 하지만 그 외에 별다른 기능은 없었습니다. 제목답게 Why so serious?
라고 적혀있네요.
바로 jadx를 이용하여 디컴파일해보면 아래와 같이 onCreate
시 1732145681(epoch time)이면 a.f40a
를 str 변수에 할당합니다. 하지만 이 시간이 맞지 않기 때문에 내부 로직이 실행되지 않습니다.
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하면 읽을 수 있는 문자열이 나오겠죠?
빠른 동적분석을 위해서 아래와 같이 코드를 패치하고 앱을 설치하였습니다. 코드 패치 방법은 이 글을 참고해주세요.
그리고 호출되는 함수를 확인하기 위해서 a2.a 클래스와 c.a 클래스를 추적 해보면, a2.a.b()
에서 c.a.o()
를 호출하고 해당 메서드에서 특정 url을 리턴하고 있습니다.
a2.a.b()
의 로직을 살펴보면 url로 GET
요청을 보내는데 응답코드가 200이 아니어서 a2.a.a()
가 실행되지 않는 것으로 보입니다.
그래서 귀찮지만 getResponseCode()!=200
으로 다시 코드를 패치하고 호출되는 함수를 살펴보았습니다. 앱이 알 수 없는 이유로 강제종료되지만 아래와 같이 a2.a.a()
에 진입한 모습을 볼 수 있습니다.
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 확장자를 가지지만 모두 바이너리 형태였습니다.
그럼 이제 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
를 확인해 보면 될 것 같습니다.
file
명령어로 확인해보니 ELF
포맷을 갖춘 파일이었습니다. (뒤에 NDK
(Native Development Kit)는 C 또는 C++를 이용하여 네이티브 코드로 앱의 일부를 구현할 수 있도록 하는 tool kit을 뜻합니다) 즉, 라이브러리 파일임을 유추할 수 있습니다.
라이브러리 분석
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
인 것을 알 수 있습니다.
다시 잠깐 생각해보면 a()가 assetManager
를 인자로 받는 점과 JokerNat 클래스에서 native
키워드가 붙은 goto2()
도 assetManager
타입을 인자로 받는 것을 보면 a()는 goto2()임을 유추할 수 있습니다.
그리고 27 번째 줄부터 보면 eibephonenumberse300.txt
의 데이터와 size를 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합니다.
마지막으로 75 번째 줄부터 보면 /data/data/meet.the.joker/i
에 위의 XOR 연산을 한 내용을 씁니다.
하지만 위의 디렉토리로 직접 가보니 해당 파일이 존재하지 않았습니다. 그래서 jadx를 통해서 eibephonenumberse300.txt
를 추출하고 The flag is:
와 XOR 하였더니 아래와 같이 dex
파일 시그니처를 확인할 수 있었습니다.
파일을 다운받고 jadx로 열어서 플레그를 확인할 수 있었습니다.
나온지 2주정도된 따끈따끈한 모바일 문제를 풀어보았습니다. 난이도가 HARD여서 좀 두려웠는데 모두 XOR 연산으로 이루어져 있어서 절차가 좀 많다 뿐이지 개인적으로 어려운 문제는 아닌 듯합니다. 이렇게 말하지만 좀 막히는 부분이 있었는데 m2 맥북 환경에서 버전이 조금 낮은 ida를 통해서 라이브러리를 열었을 때 분석이 제대로 되지 않는 부분이 있었습니다. 그래서 The flag is:
를 보고 왜 뒤는 안알려주지?
라는 생각밖에 못했습니다ㅋㅋㅋㅋ 그래서 윈도우컴으로 다시 열어서 풀 수 있었습니다. 분명 라이브러리라고 생각했는데 a()가 goto2 메서드 임을 바로 떠올리지 못한 점이 아쉽습니다.
Comments powered by Disqus.