H1702 CTF was a CTF organized by hackerone. There were 6 Android and 6 iOS reverse engineering challenges. I finished 4th.
Table of Contents
Android Write ups
Level 1
Let’s start you off with something easy to get you started.
(Note: Levels 1-4 use the same application)
ctfone-490954d49dd51911bc730d8161541cf13e7416f9.apk
Since Level 1-4 uses the same APK, I am going to do a long writeup for the 4 challenges.
As with any other Android challenge, the first step is to decompile the APK using apktool as well as dex2jar.
After which, the next step would be to look at the AndroidManifest.xml
file. Looking at the manifest file would allow you to identify entry points.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.h1702ctf.ctfone" platformBuildVersionCode="25" platformBuildVersionName="7.1.1">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<meta-data android:name="android.support.VERSION" android:value="25.3.1"/>
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme">
<activity android:label="@string/app_name" android:name="com.h1702ctf.ctfone.MainActivity" android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:exported="true" android:name="com.h1702ctf.ctfone.Level3Activity"/>
</application>
</manifest>
In this case, there is two other files we should focus on, com.h1702ctf.ctfone.MainActivity
, the main class and com.h1702ctf.ctfone.Level3Activity
, an additional activity class.
We first look at com.h1702ctf.ctfone.MainActivity
, we can view the decoded smali code or the decompiled class file using JD-GUI
or any other Java Decompiler you are familiar with.
Immediately in the onCreate
method, we can see that two tabs are created with the labels Level 1
and Level 2
. Following this lead, we immediately focus onto TabFragment1.class
to look for leads that’d allow us to solve the first challenge.
Again, as the class is relatively simple, it is obvious that in the onCreate
method, a certain resource is loaded depending on the value of a text input.
1
2
3
4
5
6
7
8
9
10
public void onClick(View paramAnonymousView) {
String str = TabFragment1.this.mInput.getText().toString();
paramAnonymousView = str;
if (str.isEmpty())
{
int i = new Random().nextInt();
paramAnonymousView = "asset" + (i % 10 + 1);
}
TabFragment1.this.loadDataFromAsset(paramAnonymousView);
}
We can immediately see that if the input is empty, a random asset[1-10] is loaded. Following that hint, we can browse to the assets folder to look for the flag. Just by looking at the contents of the folder, we would be able to immediately identify the asset that’d give us the flag.
1
2
3
4
5
6
7
8
9
10
11
12
→ ls
asset1
asset10
asset2
asset3
asset4
asset5
asset6
asset7
asset8
asset9
tHiS_iS_nOt_tHE_SeCrEt_lEveL_1_fiLE
The first flag is immediately presented to us in the tHiS_iS_nOt_tHE_SeCrEt_lEveL_1_fiLE
image file.
Flag #1: cApwN{WELL_THAT_WAS_SUPER_EASY}
Level 2
Maybe something a little more difficult?
Following what we saw in challenge 1, we can immediately focus onto TabFragment2.class
to try to solve for challenge 2.
1
2
3
4
5
6
7
8
9
10
11
public void onClick(View paramAnonymousView) {
try {
paramAnonymousView = InCryption.hashOfPlainText();
TabFragment2.this.mHashView.setText(paramAnonymousView);
TabFragment2.this.mHashView.setBackgroundColor(-1);
return;
}
catch (Exception paramAnonymousView) {
paramAnonymousView.printStackTrace();
}
}
Here we see that the method hashOfPlainText
from the InCryption
class is called. From the method name, it seems to be telling us that this function returns the hash of a certain Plaintext string.
Looking at the InCryption
class, we can immediately recreate the class in Java to run it locally on our machine.
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
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class Solve {
public static void main(String[] args) {
try {
System.out.println(Solve.plainText());
} catch (Exception e) {
}
}
static String encryptedHex = "ec49822b5417f4dad5d6048804c07f128bb0552673171d078cc2e6b191b4cbaa2d8c5c0bb95e29cee0cb0ea0db961a33ad074d17c3a227d487c85b88487435c97c3f9992b1a3d08f43fc92f416820f95ba463a67b09eb3433fb94ce2d2c5ac25ec49822b5417f4dad5d6048804c07f127c3f9992b1a3d08f43fc92f416820f95ba463a67b09eb3433fb94ce2d2c5ac25069590e37f53bebf7c16b617482a4b8ae151286044c2a8237302612c7d88c8b2ce0eef96ce210effc6a2733a73b371b1d764b219e31def9c556a602e6b236b483469118ed906f6b3abeff55bcdc7e5a00d0d3f3cb085730e3335b7170a03ed91fca1309cccb8078e2a9100cbbeaff61e4be535b1314ecb6fb99b3985c06ae00616288270b1670f7bf609b8c9c3d62e29069590e37f53bebf7c16b617482a4b8ae151286044c2a8237302612c7d88c8b2ad074d17c3a227d487c85b88487435c9828b8963fd132831a1f74db480a35711e151286044c2a8237302612c7d88c8b2ad074d17c3a227d487c85b88487435c97c3f9992b1a3d08f43fc92f416820f950d0d3f3cb085730e3335b7170a03ed9186622d5215afd9192d1b553cba233ff2244ded87c4d06dd82895f2b20110bbadfca1309cccb8078e2a9100cbbeaff61e4be535b1314ecb6fb99b3985c06ae00616288270b1670f7bf609b8c9c3d62e29ec49822b5417f4dad5d6048804c07f127c3f9992b1a3d08f43fc92f416820f953d255754f0e4004ac69e2e9b2e35ebc79ef39beaabf2ba23780e727eeb4e277a2757324dfeeb647dd2ab010b91af0bd3ad074d17c3a227d487c85b88487435c98bb0552673171d078cc2e6b191b4cbaa2d8c5c0bb95e29cee0cb0ea0db961a339ef39beaabf2ba23780e727eeb4e277a3cad3497d53b3c22fdd26fbb83f5cae8fe5f529bf234dcef4e76913394ae8f85244ded87c4d06dd82895f2b20110bbade57dded1cc4a151b2da4b3fa1041bc7f569f11fcae23f0661a6722466e5697ce069590e37f53bebf7c16b617482a4b8ae151286044c2a8237302612c7d88c8b29a4ef6d9c9cf8127de64c59ecc865fd89a4ef6d9c9cf8127de64c59ecc865fd8d5a6b3cf8313ced4ca89414d00adb0c2a854ed5338d047e0b65b956bd2a19fcc0d0d3f3cb085730e3335b7170a03ed91e891d349f90afb2d3f9608a7cdba0714e891d349f90afb2d3f9608a7cdba071486622d5215afd9192d1b553cba233ff216288270b1670f7bf609b8c9c3d62e299ef39beaabf2ba23780e727eeb4e277a4be535b1314ecb6fb99b3985c06ae00616288270b1670f7bf609b8c9c3d62e299a4ef6d9c9cf8127de64c59ecc865fd8ce0eef96ce210effc6a2733a73b371b1d764b219e31def9c556a602e6b236b48ba463a67b09eb3433fb94ce2d2c5ac25069590e37f53bebf7c16b617482a4b8ac9dcd54eb33f50a80149e8457d843b84ba463a67b09eb3433fb94ce2d2c5ac25e6ff67049d59429fa81ea1d14e1a4abde5beb88aa4073a5c948816378f2cc96b87b18c9563646c0652a9efd72f29cdd33cad3497d53b3c22fdd26fbb83f5cae82d8c5c0bb95e29cee0cb0ea0db961a33ce0eef96ce210effc6a2733a73b371b10ea54646c339600f167b5dd2029ba72fe5beb88aa4073a5c948816378f2cc96b5762452778b31d42ead4f81062775b69e891d349f90afb2d3f9608a7cdba07147c3f9992b1a3d08f43fc92f416820f953d255754f0e4004ac69e2e9b2e35ebc7ec49822b5417f4dad5d6048804c07f127c3f9992b1a3d08f43fc92f416820f95c8dab6841e142338fcc2d01ad0a3bce686622d5215afd9192d1b553cba233ff216288270b1670f7bf609b8c9c3d62e29ec49822b5417f4dad5d6048804c07f127c3f9992b1a3d08f43fc92f416820f950d0d3f3cb085730e3335b7170a03ed91fca1309cccb8078e2a9100cbbeaff61e0f1f25bef4b7f6442b420b861ad834aac8dab6841e142338fcc2d01ad0a3bce67c3f9992b1a3d08f43fc92f416820f953469118ed906f6b3abeff55bcdc7e5a03469118ed906f6b3abeff55bcdc7e5a00d0d3f3cb085730e3335b7170a03ed91e891d349f90afb2d3f9608a7cdba071486622d5215afd9192d1b553cba233ff2244ded87c4d06dd82895f2b20110bbad828b8963fd132831a1f74db480a35711e151286044c2a8237302612c7d88c8b29a4ef6d9c9cf8127de64c59ecc865fd8d5a6b3cf8313ced4ca89414d00adb0c22d8c5c0bb95e29cee0cb0ea0db961a33ad074d17c3a227d487c85b88487435c9828b8963fd132831a1f74db480a35711c9dcd54eb33f50a80149e8457d843b845bd9ba2fb7cea981a019c784939dfd0587b18c9563646c0652a9efd72f29cdd328d63f46073aec9a139375fd6d2917d3e5beb88aa4073a5c948816378f2cc96b87b18c9563646c0652a9efd72f29cdd30f1f25bef4b7f6442b420b861ad834aa41c1b4f70e10af5fa9e82a2b773ea7070ea54646c339600f167b5dd2029ba72fe5beb88aa4073a5c948816378f2cc96b5762452778b31d42ead4f81062775b697c3f9992b1a3d08f43fc92f416820f953469118ed906f6b3abeff55bcdc7e5a05bd9ba2fb7cea981a019c784939dfd055762452778b31d42ead4f81062775b69e891d349f90afb2d3f9608a7cdba0714fca1309cccb8078e2a9100cbbeaff61e2757324dfeeb647dd2ab010b91af0bd3ad074d17c3a227d487c85b88487435c9828b8963fd132831a1f74db480a35711e151286044c2a8237302612c7d88c8b29a4ef6d9c9cf8127de64c59ecc865fd8d5a6b3cf8313ced4ca89414d00adb0c2fc59c44e8f481760ef82750176f42291fb7648043fce2338843c67eae566b35c8bb0552673171d078cc2e6b191b4cbaa2d8c5c0bb95e29cee0cb0ea0db961a33ec49822b5417f4dad5d6048804c07f12828b8963fd132831a1f74db480a3571116d696017b13e85d5aaf28d6ac7c3d315762452778b31d42ead4f81062775b698bb0552673171d078cc2e6b191b4cbaa2d8c5c0bb95e29cee0cb0ea0db961a339a4ef6d9c9cf8127de64c59ecc865fd8ce0eef96ce210effc6a2733a73b371b1d764b219e31def9c556a602e6b236b48ba463a67b09eb3433fb94ce2d2c5ac25ce0eef96ce210effc6a2733a73b371b1d764b219e31def9c556a602e6b236b48c8dab6841e142338fcc2d01ad0a3bce67c3f9992b1a3d08f43fc92f416820f95ba463a67b09eb3433fb94ce2d2c5ac25ce0eef96ce210effc6a2733a73b371b1a25c129f9071f52f674b28cff9f4ade7244ded87c4d06dd82895f2b20110bbade891d349f90afb2d3f9608a7cdba0714828b8963fd132831a1f74db480a35711c9dcd54eb33f50a80149e8457d843b843d255754f0e4004ac69e2e9b2e35ebc7e6ff67049d59429fa81ea1d14e1a4abd569f11fcae23f0661a6722466e5697ce9ef39beaabf2ba23780e727eeb4e277abea40e40b98659cafe52c74461e7015a87b18c9563646c0652a9efd72f29cdd33cad3497d53b3c22fdd26fbb83f5cae8fe5f529bf234dcef4e76913394ae8f8516288270b1670f7bf609b8c9c3d62e29ec49822b5417f4dad5d6048804c07f127c3f9992b1a3d08f43fc92f416820f95ba463a67b09eb3433fb94ce2d2c5ac25e6ff67049d59429fa81ea1d14e1a4abd31e2e92c15e8b3afb3b4a4344f6d37a33d255754f0e4004ac69e2e9b2e35ebc7e6ff67049d59429fa81ea1d14e1a4abd31e2e92c15e8b3afb3b4a4344f6d37a341c1b4f70e10af5fa9e82a2b773ea707a25c129f9071f52f674b28cff9f4ade7244ded87c4d06dd82895f2b20110bbad011d2d66c36261ef7fb7ca949a22ed84";
static String bin2hex(byte[] paramArrayOfByte)
{
return String.format("%0" + paramArrayOfByte.length * 2 + "X", new Object[] { new BigInteger(1, paramArrayOfByte) });
}
private static byte[] decrypt(byte[] paramArrayOfByte1, byte[] paramArrayOfByte2)
throws Exception
{
SecretKeySpec a = new SecretKeySpec(paramArrayOfByte1, "AES");
Cipher localCipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
localCipher.init(2, a);
return localCipher.doFinal(paramArrayOfByte2);
}
public static String getHash(String paramString)
{
try
{
MessageDigest localMessageDigest = MessageDigest.getInstance("SHA-256");
localMessageDigest.reset();
return bin2hex(localMessageDigest.digest(paramString.getBytes()));
}
catch (NoSuchAlgorithmException e)
{
e.printStackTrace();
}
return "";
}
public static String plainText()
throws Exception
{
return new String(hex2bytes(new String(decrypt(hex2bytes("0123456789ABCDEF0123456789ABCDEF"), hex2bytes(encryptedHex))).trim()));
}
static byte[] hex2bytes(String paramString)
{
byte[] arrayOfByte = new byte[paramString.length() / 2];
int i = 0;
while (i < arrayOfByte.length)
{
int j = i * 2;
arrayOfByte[i] = ((byte)Integer.parseInt(paramString.substring(j, j + 2), 16));
i += 1;
}
return arrayOfByte;
}
}
Running this, we obtain what appears to be morse code.
1
2
→ java Solve
DASH DOT DASH DOT SPACE DOT DASH SPACE DOT DASH DASH DOT SPACE DOT DASH DASH SPACE DASH DOT SPACE DASH DOT DOT DOT SPACE DOT DASH DOT SPACE DOT DASH SPACE DASH DOT DASH DOT SPACE DASH DOT DASH SPACE DOT SPACE DASH SPACE DASH DOT DASH DOT SPACE DOT DASH DOT SPACE DASH DOT DASH DASH SPACE DOT DASH DASH DOT SPACE DASH DASH DOT DOT DOT SPACE DASH DASH DASH DASH DASH SPACE DASH DOT DOT DOT DOT SPACE DOT DASH DOT SPACE DOT DOT DOT DOT DASH SPACE DOT DASH DASH DOT SPACE DOT DOT DOT DOT SPACE DASH DOT DASH DASH SPACE DOT DOT DASH SPACE DASH DOT SPACE DASH DOT DOT SPACE DOT SPACE DOT DASH DOT SPACE DOT DOT DOT SPACE DASH DOT DASH DOT SPACE DASH DASH DASH SPACE DOT DASH DOT SPACE DOT SPACE DOT DASH DASH DASH DASH SPACE DOT DOT DOT DOT DOT SPACE DOT DOT DASH SPACE DASH DOT SPACE DASH DOT DOT SPACE DOT SPACE DOT DASH DOT SPACE DOT DOT DOT SPACE DASH DOT DASH DOT SPACE DASH DASH DASH SPACE DOT DASH DOT SPACE DOT SPACE DOT DOT DOT DOT SPACE DOT DOT DOT DOT DASH SPACE DOT DASH DOT SPACE DASH DOT DOT SPACE DOT DOT DASH SPACE DASH DOT SPACE DASH DOT DOT SPACE DOT SPACE DOT DASH DOT SPACE DOT DOT DOT SPACE DASH DOT DASH DOT SPACE DASH DASH DASH SPACE DOT DASH DOT SPACE DOT SPACE DASH DOT DOT DOT SPACE DOT DASH DOT SPACE DASH DASH DASH DASH DASH SPACE DASH DOT DOT DOT SPACE DOT DASH DOT SPACE DOT DASH SPACE DASH DOT DASH DOT SPACE DASH DOT DASH SPACE DOT SPACE DASH
Converting this to real dots and dashes, we get the following morse code:
-.-. .- .--. .-- -. -... .-. .- -.-. -.- . - -.-. .-. -.-- .--. --... ----- -.... .-. ....- .--. .... -.-- ..- -. -.. . .-. ... -.-. --- .-. . .---- ..... ..- -. -.. . .-. ... -.-. --- .-. . .... ....- .-. -.. ..- -. -.. . .-. ... -.-. --- .-. . -... .-. ----- -... .-. .- -.-. -.- . -
The morse code is then translated to our second flag, cApwN{CRYP706R4PHY_15_H4RD_BR0}
.
Level 3
Think you can solve level 3?
Looking back, we can immediately focus onto Level3Activity.class
to begin our hunt for the third flag. Also a very simple class, we can trace the onClick
method to MonteCarlo.start()
.
In MonteCarlo
, the class seems to be trying to approximate the value of Pi, which is consistent with what the name suggests. Everything else in the class seems normal except for a call to another class ArraysArraysArrays.start()
and a JNI method being declared and not called anywhere.
Starting with ArraysArraysArrays
, this class seems to be sorting arrays of integers and printing them after. However, again, a JNI method x()
is declared, but is called in the start
method this time.
To analyze the JNI method, we can view the libnative-lib.so
file in IDA and focus on the method directly. The decompilation of the method is as follows:
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
int __fastcall Java_com_h1702ctf_ctfone_ArraysArraysArrays_x(int a1)
{
int v1; // r4@1
int v2; // r0@2
int v3; // r6@4
int v4; // r0@5
int v5; // r0@8
int result; // r0@10
v1 = a1;
if ( !byte_C113[0] )
{
v2 = 0;
do
{
byte_C113[v2] = byte_C05B[v2] ^ 0x3D;
++v2;
}
while ( v2 != 29 );
}
v3 = (*(int (__fastcall **)(int, char *))(*(_DWORD *)v1 + 24))(v1, byte_C113);
if ( !byte_C131[0] )
{
v4 = 0;
do
{
byte_C131[v4] = aIYi_x[v4] ^ 0x2C;
++v4;
}
while ( v4 != 7 );
}
if ( !byte_C139[0] )
{
v5 = 0;
do
{
byte_C139[v5] = byte_C081[v5] ^ 0x58;
++v5;
}
while ( v5 != 3 );
}
result = (*(int (__fastcall **)(int, int, char *, char *))(*(_DWORD *)v1 + 452))(v1, v3, byte_C131, byte_C139);
if ( result )
result = _JNIEnv::CallStaticVoidMethod(v1, v3);
return result;
}
Here it is obvious that some constant in the library is being decrypted with a fixed key. Using a quick Python script, we can quickly decrypt the constants.
1
2
3
4
5
6
7
from pwn import *
first = [0x5E,0x52,0x50,0x12,0x55, 0xC, 0xA, 0xD, 0xF,0x5E,0x49,0x5B,0x12,0x5E,0x49,0x5B,0x52,0x53,0x58,0x12,0x6F,0x58,0x4C,0x48,0x58,0x4E,0x49,0x52,0x4F]
print xor(first,0x3d)
second = [0x5E, 0x49, 0x5D, 0x59, 0x49, 0x5F, 0x58]
print xor(second,0x2c)
third = [0x70,0x71,0xE]
print xor(third,0x58)
Running the script gives us the following:
com/h1702ctf/ctfone/Requestor
request
()V
Together with the decompilation of the class, we can quickly infer that the JNI method x()
is calling the request
method located in com/h1702ctf/ctfone/Requestor
. With that, we can quickly return to JD-GUI to look at the Requestor
class.
In the Requestor
class, we see that the method is used to make a request to https://h1702ctf.com/About
and a custom header key/value is added with the values obtained through two other JNI methods hName
and hVal
.
Returning to IDA, we see that in the two JNI methods, constants are being decrypted with a fixed xor key again. Again, with a Python script, we easily decrypt the two constants to get the following:
1
2
3
4
5
6
7
8
9
10
11
headerKey =[0x6F,0x1A,0x7B ,0x52,0x41,0x52,0x5B, 4,0x1A,0x71,0x5B,0x56,0x50]
print xor(headerKey, 0x37)
headerValue =[ 0x68, 0x0F, 0x6C, 0x7D, 0x6C, 0x0C, 0x6F, 0x47, 0x6B, 0x66,
0x5A, 0x71, 0x68, 0x79, 0x6C, 0x71, 0x68, 0x53, 0x4E, 0x50,
0x5A, 0x0F, 0x52, 0x4D, 0x69, 0x6A, 0x68, 0x55, 0x68, 0x0F,
0x74, 0x67, 0x6A, 0x68, 0x5A, 0x4D, 0x6A, 0x55, 0x0E, 0x49,
0x5D, 0x79, 0x0F, 0x6B, 0x5F, 0x55, 0x4E, 0x48, 0x64, 0x68,
0x6B, 0x46, 0x70, 0x52, 0x6C, 0x4F, 0x5C, 0x7B, 0x6C, 0x5F,
0x5B, 0x54, 0x7F, 0x0B, 0x6F, 0x0C, 0x5D, 0x07, 0x6E, 0x6F,
0x51, 0x03]
print xor(headerValue, 0x3e)
X-Level3-Flag
V1RCR2QyUXdOVGROVmpnd1lsWTVkV1JYTVdsTk0wcG1UakpvZVUxNlRqbERaejA5Q2c9PQo=
It is apparent that the header value is a base64 encoded value. We can easily decode it to obtain our third flag!
In [6]: "V1RCR2QyUXdOVGROVmpnd1lsWTVkV1JYTVdsTk0wcG1UakpvZVUxNlRqbERaejA5Q2c9PQo=".deco
...: de('base64').decode('base64').decode('base64')
Out[6]: 'cApwN{1_4m_numb3r_7hr33}\n'
Level 4
Hope you kept your notes.
Challenge 4 stumbled me for awhile, it wasn’t as obvious as to where to begin as compared to the previous 3 challenges. However, recalling the unused JNI method declared in the MonteCarlo
class, it soon became apparent to me that the solution of Challenge 4 would be given by invoking the functionnameLeftbraceOneCommaTwoCommaThreeCommaRightbraceFour
class with the flags of the first 3 challenge.
There is two way to approach this:
- Create an APK to call the method in the native library directly.
- Use frida to invoke the method with the correct parameters.
As I do not have frida readily available beside me, I chose to go with method #1. Using a simple template provided by Android Studio, I quickly write up a simple APK that loads the same library and calls the method with the flags as parameter.
Note that we’d have to replicate an APK with the same package name and class in order for this method to work smoothly.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.h1702ctf.ctfone;
/**
* Created by quanyang on 11/7/17.
*/
public class MonteCarlo {
public native String functionnameLeftbraceOneCommaTwoCommaThreeCommaRightbraceFour(String paramString1, String paramString2, String paramString3);
public String test() {
String a="cApwN{WELL_THAT_WAS_SUPER_EASY}";
String b="CAPWN{CRYP706R4PHY_15_H4RD_BR0}";
String c="cApwN{1_4m_numb3r_7hr33}";
return functionnameLeftbraceOneCommaTwoCommaThreeCommaRightbraceFour(a,b,c);
}
}
With that, we obtain our 4th flag!
4th Flag: cApwN{w1nn3r_w1nn3r_ch1ck3n_d1nn3r!}
Level 5
Hmmm… looks like you need to get past something…
ctfone5-8d51e73cf81c0391575de7b40226f19645777322.apk
Similarly, I start with decoding/decompilation of the APK with apktool
and dex2jar
.
Looking at the AndroidManifest.xml
,
1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.h1702ctf.ctfone5" platformBuildVersionCode="25" platformBuildVersionName="7.1.1">
<meta-data android:name="android.support.VERSION" android:value="25.3.1"/>
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme">
<activity android:background="@drawable/stars" android:label="@string/app_name" android:name="com.h1702ctf.ctfone5.MainActivity" android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service android:exported="true" android:name="com.h1702ctf.ctfone5.CruelIntentions"/>
</application>
</manifest>
We see that there is an additional service linked to the class com.h1702ctf.ctfone5.CruelIntentions
. From the unpacked APK, we can also find a native library libnative-lib.so
.
With that, we first jump to the MainActivity
class. From the dex2jar
decompiled code of the class, we can immediately notice a few things. First, there is a hint that says State the secret phrase (omit the oh ex)
, where oh ex seems to refer to 0x
, something you usually find before a hexadecimal representation of a value. Second, there is a JNI method defined flag
, which seems to be taking in three String values and returning the flag is the values are correct, this seems to remind me of Challenge 4 previously.
The next step would be to look at the JNI method flag
in IDA Pro. The decompilation of the method in IDA Pro appears to be very similar to challenge 4 previously as well. It seems to be using the blake2b hash values of the three strings and xor’ed together using the stream_xsalsa_xor with an unknown value (which is probably the encrypted value of the flag). Therefore, if the three strings provided were accurate, the result from this function would likely give us the flag for this challenge.
With that, we would need to figure out the three Strings required.
We now move on to look at com.h1702ctf.ctfone5.CruelIntentions
. The decompiled class is pretty simple, the class is a subclass of IntentService
and is used to set up an intent service which reacts to the com.h1702ctf.ctfone5.action.HINT
action and calls a JNI method one()
if the com.h1702ctf.ctfone5.extra.PARAM1
param is equal to orange
.
Knowing this, we can call the intent using the following ADB command:
adb shell am startservice -n "com.h1702ctf.ctfone5/com.h1702ctf.ctfone5.CruelIntentions" -a com.h1702ctf.ctfone5.action.HINT -e com.h1702ctf.ctfone5.extra.PARAM1 orange
I tried running the intent service, but nothing appears to be happening. We did notice this in the logcat output.
I/BOOYA ( 5120): got intent
I/BOOYA ( 5120): got hint
I/BOOYA ( 5120): param: orange
This indicates that the intent service was successfully invoked.
With the jar side of things covered (i.e. we have looked at all custom Java classes), we can now move to the JNI method one()
.
The IDA Pro decompilation of the method is long and complex therefore I will talk about snippets that appears interesting to me.
The first interesting snippet is:
1
2
3
4
5
6
7
8
9
if ( prctl(3) )
{
if ( byte_D064 & 1 )
v57 = raise(11);
v56 = 0;
v2 = prctl(4);
byte_D064 = 1;
v55 = v2;
}
This code snippet seems to check if the process is dumpable, and if so, sets it to be not dumpable.
The second interesting snippet is:
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
v3 = getpid();
v60[6] = v3;
v54 = v3;
v53 = &v78;
v52 = sprintf((char *)&v78, "/proc/%d/status", v3);
v4 = fopen((const char *)&v78, "r");
v60[5] = (int)v4;
if ( v4 ) {
while ( fgets((char *)&v76, 1024, (FILE *)v60[5]) ) {
v51 = "TracerPid";
if ( !strncmp((const char *)&v76, "TracerPid", 9u) ) {
v5 = atoi((const char *)&v77);
v60[4] = v5;
if ( v5 ) {
v6 = fclose((FILE *)v60[5]);
v65 = 1;
v50 = v6;
goto LABEL_14;
}
break;
}
}
v7 = fclose((FILE *)v60[5]);
v65 = 0;
v49 = v7;
}
This portion seems to be checking if the TracePid of the process is non-zero, which hints whether the process is being traced/debugged.
The third interesting snippet is:
1
2
3
4
5
6
7
8
9
10
while ( (unsigned int)v60[9] <= 0xA ) {
v9 = g_su_paths[v60[9]];
v60[11] = (int)v9;
if ( access(v9, 0) == -1 ) {
v66 = 1;
goto LABEL_22;
}
++v60[9];
}
v66 = 0;
This portion seems to be checking if filepaths specified in g_su_paths
is accessible.
And the filepaths are:
/su/bin/su
/system/bin/daemonsu
/system/xbin/daemonsu
/sbin/su
/system/bin/su
/system/xbin/su
/data/local/xbin/su
/data/local/bin/su
/system/sd/xbin/su
/system/bin/failsafe/su
/data/local/su
The fourth snippet is:
1
2
3
4
5
v33 = _system_property_get("mobsec.setme");
v30 = atoi((const char *)&v69);
*v60 = v30;
if ( v30 == 1 )
return -1096307938;
This portion seems to check that a certain system property mobsec.setme
is equal to 1 and to return a certain value.
Putting the four parts together, it seems that the function is checking if the device is rooted or being traced/debugged and the fourth part appears to be a bypass for the check.
However, looking carefully, we can see that the return value of this native function is always a constant when the constraints are met.
.text:00002920 LDR R0, =0x5F53D58F
.text:00002922 LDR R3, =0x5F53D58F
.text:00002924 ADD R0, R3
.text:00002926 LDR R1, =0x7D670F2A
.text:00002928 LDR R3, =0x7D670F2B
.text:0000292A ADD R1, R3
.text:0000292C LDR R2, =0x6D3D5D2F
.text:0000292E LDR R3, =0x6D3D5D2F
.text:00002930 ADD R2, R3
.text:00002932 LDR R3, =0x6F56DD5F
.text:00002934 LDR.W LR, =0x6F56DD5F
.text:00002938 ADD LR, R3
.text:0000293A BX LR
If we were to evaluate the ADD
instructions, this is what we will obtain:
1
2
3
4
5
6
7
8
In [1]: hex(0x5F53D58F+0x5F53D58F)
Out[1]: '0xbea7ab1e'
In [2]: hex(0x7D670F2A+0x7D670F2B)
Out[2]: '0xface1e55'
In [3]: hex(0x6D3D5D2F+0x6D3D5D2F)
Out[3]: '0xda7aba5e'
Therefore, it becomes apparent that this three values are the String values that is to be submitted to the flag()
function. Attempting to submit this, we get our fifth flag! Remember that we are suppose to omit the 0x.
Our 5th Flag: cApwN{sPEak_FrieNd_aNd_enteR!}
Level 6
I can’t think of anything creative… just try to solve this one :)
ctfone6-6118c10be480b994654a1f01cd322af2df2ceab6.apk
The AndroidManifest.xml
doesn’t show any additional service or activity this time round other than the MainActivity. There is however a native library called libidk-really.so
.
Also, within /res/raw
there is two files called something.jar
and secretasset
.
The name itself seems to hint that they have something to do with the flag. Though its called something.jar
, the file command doesn’t seem to recognize it as a legitimate file.
1
2
3
→ file secretasset something.jar
secretasset: data
something.jar: data
Looking at MainActivity.class
, we see that the onClick
event attempts to open the something.jar
file and to execute it after it has been handled by the prepareDex
method.
From the smali code, we can see that in the prepareDex
method, something.jar
is being decrypted using the following decryption method.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static byte[] decrypt(String paramString1, String paramString2, byte[] paramArrayOfByte)
{
try
{
paramString2 = new IvParameterSpec(paramString2.getBytes("UTF-8"));
paramString1 = new SecretKeySpec(paramString1.getBytes("UTF-8"), "AES");
Cipher localCipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
localCipher.init(2, paramString1, paramString2);
paramString1 = localCipher.doFinal(paramArrayOfByte);
return paramString1;
}
catch (Exception paramString1)
{
paramString1.printStackTrace();
}
return null;
}
However, the key and IV is not clearly identified in the decryption method.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
move-result-object v11
const v12, 0x7f050001
invoke-virtual {v11, v12}, Landroid/content/res/Resources;->getString(I)Ljava/lang/String;
move-result-object v11
invoke-virtual/range {p0 .. p0}, Lcom/example/asdf/MainActivity;->getResources()Landroid/content/res/Resources;
move-result-object v12
const v13, 0x7f050004
invoke-virtual {v12, v13}, Landroid/content/res/Resources;->getString(I)Ljava/lang/String;
move-result-object v12
invoke-static {v11, v12, v5}, Lcom/example/asdf/MainActivity;->decrypt(Ljava/lang/String;Ljava/lang/String;[B)[B
By looking again at the smali
code, we see that the String resource with id 0x7f050001
and 0x7f050004
is used as parameter to the decrypt
method, which means that the key and IV is hard coded.
With that, we can cross reference from /res/values/ids.xml
to figure out that 0x7f050001
is referring to booper
and 0x7f050004
is referring to dooper
.
1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Level 6</string>
<string name="booper">UCFh%divfMtY3pPD</string>
<string name="diag_message">Processing dex file...</string>
<string name="diag_title">Wait</string>
<string name="dooper">nY6FtpPFXnh,yjvc</string>
<string name="message">Come at me bro</string>
<string name="toast">Toast!</string>
</resources>
The contents of /res/values/strings.xml
then tells us the String values. Using the decompiled code for the decrypt
method, I quickly wrote a Java console application to decrypt something.jar
.
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
import java.io.File;
import java.security.MessageDigest;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Solution {
public static void main(String args[]) {
String key = "nY6FtpPFXnh,yjvc";
String iv = "UCFh%divfMtY3pPD";
BufferedInputStream br = null;
FileInputStream fr = null;
try {
fr = new FileInputStream(new File("something.jar"));
br = new BufferedInputStream(fr);
byte[] ab = new byte[br.available()];
br.read(ab);
FileOutputStream fos = new FileOutputStream("something-decrypted.jar");
fos.write(decrypt(iv,key,ab));
fos.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (br != null)
br.close();
if (fr != null)
fr.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
public static byte[] decrypt(String paramString1, String paramString2, byte[] paramArrayOfByte)
{
try
{
IvParameterSpec a = new IvParameterSpec(paramString2.getBytes("UTF-8"));
SecretKeySpec b = new SecretKeySpec(paramString1.getBytes("UTF-8"), "AES");
Cipher localCipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
localCipher.init(2, b, a);
byte[] paramString12 = localCipher.doFinal(paramArrayOfByte);
return paramString12;
}
catch (Exception e)
{
e.printStackTrace();
}
return null;
}
}
Running the Java program gives us the decrypted something.jar
file.
1
2
→ file something-decrypted.jar
something-decrypted.jar: Java archive data (JAR)
We then use dex2jar to decompile the decrypted jar, this gives us two classes, IReallyHaveNoIdea.class
and Pooper.class
.
IReallyHaveNoIdea.class
seems to be registering a new intent receiver for the com.example.asdf.SEND
intent. Pooper.class
seems to be the receiver for the intent and appears to be decrypting
something when it receives an intent call.
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
public void onReceive(Context paramContext, Intent paramIntent)
{
String str = paramIntent.getStringExtra("herpaderp");
paramIntent = paramIntent.getStringExtra("lerpaherp");
if ((!checkSomething1(str)) || (!checkSomething2(paramIntent))) {
System.exit(0);
}
paramContext = new File(paramContext.getDir("dex", 0), "super-dooper");
paramContext.delete();
try
{
BufferedOutputStream localBufferedOutputStream = new BufferedOutputStream(new FileOutputStream(paramContext));
byte[] arrayOfByte = new byte[this.bis.available()];
this.bis.read(arrayOfByte);
localBufferedOutputStream.write(decrypt(str, paramIntent, arrayOfByte));
localBufferedOutputStream.close();
this.bis.close();
paramContext.setExecutable(true);
try
{
Runtime.getRuntime().exec(paramContext.getAbsolutePath());
return;
}
catch (Exception paramContext) {}
}
catch (IOException paramIntent)
{
for (;;) {}
}
}
The onCreate
function looks for the super-dooper
file and then tries to decrypt it based on the params herpaderp
and lerpaherp
. There is however, a check for the two values and this might be the constraint that we have to solve to get the decryption key.
Looking at checkSomething1
and checkSomething2
, we can slowly reverse the smali
code which is iterating each character in a switch case and checking if the index of the character matches the value expected. With some work, we obtained b1ahbl4hbl4hblop
for checkSomething1
and mmhmthisdatgoods
for checkSomething2
.
Though we are stuck without the super-dooper
file, a simple test with secretasset
shows that this decryption is meant for it. Again, using the same decryption Java program above, but with the new key/IV, we obtained a ELF binary.
1
2
→ file secretasset-decrypted.jar
b.jar: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /system/bin/linker, BuildID[sha1]=12c1ab8273eb1b3b193b61aaa45a2a02a332f32f, stripped
Throwing the binary in IDA pro, we immediately see that a couple of interesting libc methods are imported like bind
, accept
and listen
, this seems to hint that the program might be communicating through the network.
With that in mind, I ran the APK application, fed the text input with UCFh%divfMtY3pPD
, which the process then proceeds to decrypt something.jar
and sets up the intent receiver. Following that, I invoke the intent with the following command, adb shell am broadcast -a com.example.asdf.SEND -e herpaderp b1ahbl4hbl4hblop -e lerpaherp mmhmthisdatgoods
to start the execution of the ELF binary.
Once that is in place, a simple netstat shows that a service is indeed listening on port 1337.
We can attempt to communicate with it using netcat:
1
2
3
4
5
6
7
8
9
10
→ nc 192.168.1.20 1337
<<JOIN, HELLO 13
\HELP
\QUIT Quit chatroom
\PING Server test
\NAME <name> Change nickname
\PRIVATE <reference> <message> Send private message
\ACTIVE Show active clients
\HELP Show help
Looking at the bind method in IDA pro, we can trace the code to the method located at 0x00001B10
, the method which handles all data received by the socket.
The method appears to be decrypting string constants with static xor keys and with some effort, we can figure out what the method is doing. For most part of the method, the procedure seems to be doing nothing malicious. However, for \PRIVATE
, there appears to be a hidden procedure that resemblance a backdoor.
The \PRIVATE
procedure calls a method located at 0x00001820
that takes in the reference
and message
as parameter.
In the method, it appears that if the reference
equals to 1337
, the backdoor is activated. The decrypted String constants in the method is gettin it done
and Nice one!
.
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
result = strncmp(v5, (const char *)&dword_6CA0, result);
if ( !result )
{
v12 = v7 + 2;
v13 = 0;
v14 = &dword_6020;
v15 = v12;
while ( 1 )
{
result = strlen(v5);
if ( v13 >= result )
break;
sub_15A0(&v18);
sub_15D8(&v18, &v4[v13] + v15, 1);
sub_1668(&v19, &v18);
if ( *v14 == v19
&& v14[1] == v20
&& v14[2] == v21
&& v14[3] == v22
&& v14[4] == v23
&& v14[5] == v24
&& v14[6] == v25
&& v14[7] == v26
&& v14[8] == v27
&& v14[9] == v28
&& v14[10] == v29
&& v14[11] == v30
&& v14[12] == v31
&& v14[13] == v32
&& v14[14] == v33
&& v14[15] == v34 )
{
if ( !byte_6CB0[0] )
{
byte_6CB0[1] = unk_68E1 ^ 0x26;
byte_6CB0[2] = unk_68E2 ^ 0x26;
byte_6CB0[3] = unk_68E3 ^ 0x26;
byte_6CB0[4] = unk_68E4 ^ 0x26;
byte_6CB0[5] = unk_68E5 ^ 0x26;
byte_6CB0[6] = unk_68E6 ^ 0x26;
byte_6CB0[0] = unk_68E0 ^ 0x26;
byte_6CB0[7] = unk_68E7 ^ 0x26;
byte_6CB0[8] = unk_68E8 ^ 0x26;
}
v16 = strlen(byte_6CB0);
write(fd, byte_6CB0, v16);
}
++v13;
v14 += 16;
}
v3 = 1337;
}
This portion of the method seems to be checking that the message
provided starts with gettin it done
and then appears to be checking each character after with against a hash value (by first hashing each character and checking against a hash stored in the binary). If the hash matches, it writes Nice one!
to the socket response.
With that, I wrote a simple Python script that bruteforces each printable character until we obtain our 6th flag!
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
from pwn import *
context(arch = 'i386', os = 'linux')
r = remote('192.168.1.20', 1337)
pre="\PRIVATE 1337 gettin it done "
print r.recvline()
import string
curr = "cApwN{d"
while len(curr) < 35:
for i in string.printable:
r.sendline(pre+curr+i)
r.sendline("\PING")
a= r.recvuntil("<<PONG")
if a.count("Nice one!") > len(curr):
curr += i
print curr
```
After running the python script, we eventually obtain our flag after some time.
```
<<JOIN, HELLO 32
cApwN{d3
cApwN{d3u
cApwN{d3us
cApwN{d3us_
cApwN{d3us_d
cApwN{d3us_d3
cApwN{d3us_d3x
cApwN{d3us_d3x_
cApwN{d3us_d3x_m
cApwN{d3us_d3x_my
cApwN{d3us_d3x_my_
cApwN{d3us_d3x_my_4
cApwN{d3us_d3x_my_4p
cApwN{d3us_d3x_my_4pk
cApwN{d3us_d3x_my_4pk_
cApwN{d3us_d3x_my_4pk_i
cApwN{d3us_d3x_my_4pk_is
cApwN{d3us_d3x_my_4pk_is_
cApwN{d3us_d3x_my_4pk_is_a
cApwN{d3us_d3x_my_4pk_is_au
cApwN{d3us_d3x_my_4pk_is_aug
cApwN{d3us_d3x_my_4pk_is_augm
cApwN{d3us_d3x_my_4pk_is_augm3
cApwN{d3us_d3x_my_4pk_is_augm3n
cApwN{d3us_d3x_my_4pk_is_augm3nt
cApwN{d3us_d3x_my_4pk_is_augm3nte
cApwN{d3us_d3x_my_4pk_is_augm3nted
cApwN{d3us_d3x_my_4pk_is_augm3nted}
Our 6th and Last flag for Android: cApwN{d3us_d3x_my_4pk_is_augm3nted}
iOS Write ups
I solved most of the iOS challenge with snoop-it
, mobile assistant
, jailbroken 32-bit device and an un-jailbroken 64-bit device.
Level 1
WAKE ME UP, WAKE ME UP INSIDE. SAVE ME!!!!!
(Note: Levels 1-4 use the same application)
IntroLevels-727e07e27199b5431fccc16850d67c4fea6596f7.ipa
We can unzip the IPA file with an unarchiver tool to attempt to see if there are any asset or files which may be of interest. After which, we can use the cartool from https://github.com/steventroughtonsmith/cartool
to decode the Assets.car
file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
→ ~/CTF/tools/cartool/cartool Assets.car .
Matrix-Morpheus
Matrix-Morpheus.png
first
first.png
first@2x.png
he-man-jon-chu-camp
he-man-jon-chu-camp.png
hqdefault
hqdefault.png
lizard
lizard.png
paper
paper.png
rock
rock.png
scissors
scissors.png
second
second.png
second@2x.png
spock
spock.png
Viewing the resources, we found our first flag in Matrix-Morpheus.png
.
Flag #1: cApwN{y0u_are_th3_ch0sen_1}
Level 2
And he prays…
For Challenge 2, we can trace the ViewController for Level 2 to a method located at 0xA930
, this method seems to be checking the hash value of the text input with 5b6da8f65476a399050c501e27ab7d91
before appending 1234
and then using it as a key to decrypt some constant values.
Googling 5b6da8f65476a399050c501e27ab7d91
immediately gives us the MD5 Plaintext value of 424241
, and entering the plaintext value into the level 2 view gives us our second flag!
Flag #2: cApwN{0mg_d0es_h3_pr4y}
Level 3
Rock, paper, scissors is so juvenile. Play rock, paper, scissors, lizard, Spock!
Again, in challenge 3, we can use the snoop-it
application to attempt to figure out what the third view controller is doing.
When viewing the class, we notice that the view controller has a number of methods like setLizardImage
, reportBeingALoser
and so on. However, invoking the things
method gave us our third flag.
Flag #3: cApwN{1m_1n_ur_n00twork_tere3fik}
Level 4
Use your flags from levels 1, 2, and 3 to do the thing!
Challenge 4 appears to be telling us to use the first three flags to do the thing. This reminds me of the Android Challenge 4 where the first three flags are used to obtain the fourth flag.
Looking at the functions defined in IDA Pro, it seems like we are suppose to invoke the ZhuLi.doTheThing
method.
Using cycript, we can easily invoke the method with our three previous flags, giving us our fourth flag!
1
2
3
4
5
6
7
8
9
Quanyangs-iPhone:~ root# cycript -p IntroLevels
cy# ls
throw new ReferenceError("Can't find variable: ls")
cy# [ZhuLi doTheThing: @"cApwN{y0u_are_th3_ch0sen_1}"flag2: @"cApwN{0mg_d0es_h3_pr4y}"lag3: @"cApwN{1m_1n_ur_n00twork_tere3fik}"]
@"634170774e7b6630685f7377317a7a6c655f6d795f6e317a7a6c657d"
cy#
In [1]: "634170774e7b6630685f7377317a7a6c655f6d795f6e317a7a6c657d".decode('hex')
Out[1]: 'cApwN{f0h_sw1zzle_my_n1zzle}'
Flag #4: cApwN{f0h_sw1zzle_my_n1zzle}
Level 5
Looks like this thing is pretty locked down, I don’t think you can touch this.
Level5-69c2713162cb8f5e9418f8c08f3fa0a1ecb4928d.ipa
Throwing Level5Demo
into IDA Pro, we can first see three custom methods which appears to be meddling with Keychain objects.
1
2
3
-[KeychainThing newSearchDictionary:]
-[KeychainThing searchKeychainCopyMatching:]
-[KeychainThing createKeychainValue:forIdentifier:]
Also, there are a couple of interesting Strings being defined.
__cstring:00011680 00000014 C com.uber.ctf.level5
__cstring:00012280 00000012 C setmeinurkeychain
__cstring:00012292 0000000D C youdidathing
Tracing youdidathing
, we reach a method located at 0xa180
, this method appears to be searching the Keychain for the key setmeinurkeychain
and then checking if the data value matches youdidathing
.
With that, we can again make use of cycript to create the required Keychain items to see if this gives us our flag.
The following cycript script sets the required items.
1
2
3
4
5
6
7
8
9
10
11
var key = @"setmeinurkeychain"
var encodedKey = [key dataUsingEncoding:NSUTF8StringEncoding];
var dict = [[NSMutableDictionary alloc] init];
[dict setObject:kSecClassGenericPassword forKey:kSecClass];
[dict setObject:encodedKey forKey:kSecAttrAccount];
[dict setObject:encodedKey forKey:kSecAttrGeneric];
var service = @"com.uber.ctf.level5"
[dict setObject:service forKey:kSecAttrService];
[dict setObject:[@"youdidathing" dataUsingEncoding:NSUTF8StringEncoding] forKey:kSecValueData];
[dict setObject:kSecAttrAccessibleAlwaysThisDeviceOnly forKey:kSecAttrAccessible]
SecItemAdd(dict,NULL);
We can also notice with snoop-it
that there indeed was a keychain request for the key setmeinurkeychain
.
After which, when we click the Hammer time!
button, the app no longer crashes.
This gives us our fifth flag!
Flag #5: cApwN{i_guess_you_can_touch_this}
Level 6
Hey look at me im Tiny Rick! Yeah now that I got your attention, I got this app here that Squanchy squanched on my phone. Looks like there is something in there… But I don’t give a @#$! I’m Tiny Rick!
Level6-679e59bdfb40233fb1359d098d7269a3320eabd2.ipa
Update: This challenge did not function properly on iOS 32bit devices, here is the updated challenge Level6-update-f0887a253daaa02e584bc9ff4edfeca1300887dc.ipa
Note: The original version of the app is still solvable. The update is only for those who wish to run the app on a 32bit device.
Update: If you are attempting to solve the 32bit challenge and running into issues, contact @suspiciousfudge on the Slack channel
***I noticed that the 32-bit version was not solve-able and so I told @suspiciousfudge and he updated the binary after.
For the last iOS Challenge, running the app displays a text input that appears to be encoded with some sort of huffman tree.
We then throw the binary into IDA Pro. We see a couple of class being defined, MrPoopyButtHoleThing
, MrPoopyButtholeWasInnocent
and MrPoopyButtholeGappingOrifice
, this three methods seems to be the binaryheaptree used to build the huffman tree.
We are then able to find a suspicious String constant That's Right Morty! This is gonna be a lot like that. Except you know. Its gonna make sense.
and
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque suscipit, ligula vitae fringilla fringilla, lectus tortor eleifend ligula, vitae sodales mauris nibh a elit. Maecenas nec pellentesque massa. Curabitur volutpat lobortis risus id aliquet. Donec eget viverra enim. Etiam massa nibh, lobortis id pretium quis, consequat ut libero. Integer aliquet lacinia ex sed porttitor. Maecenas auctor eget nisl et mollis. Mauris et suscipit lorem, a facilisis magna. Etiam at facilisis lacus, mollis rhoncus lorem. Morbi vitae volutpat lectus. Donec ut vestibulum justo. Nullam ullamcorper ligula vel dignissim viverra. Quisque mi sapien, auctor quis quam ac, gravida ullamcorper purus. Morbi ut mi vitae massa dapibus rhoncus sed ut ipsum. Suspendisse accumsan dui at velit ultrices, ac hendrerit metus ullamcorper.Duis volutpat condimentum faucibus. Aliquam ex nisl, sodales in urna vel, vestibulum faucibus metus. Donec dapibus ante magna, luctus hendrerit felis commodo vitae. Vivamus quis sodales quam. Nullam dictum venenatis eros, vitae feugiat erat sollicitudin eu. Mauris aliquam, purus id porta porttitor, ligula felis egestas ex, non feugiat urna sem a nisi. Nunc eget tincidunt lorem, et dictum diam. Integer sodales tempus finibus. Donec pharetra ut risus sit amet bibendum. Morbi molestie lacinia varius. Duis diam dui, pulvinar non orci a, malesuada dictum metus. Morbi semper at ante in dignissim. Maecenas at molestie nibh. Mauris sollicitudin, ipsum eu imperdiet tristique, neque purus tristique sem, quis porta leo libero et orci. Fusce sed odio lobortis, pharetra justo et, tristique mauris. Vestibulum in interdum libero, et euismod lacus. Nulla volutpat pulvinar tortor at placerat. In non magna eget nibh egestas lacinia eleifend eu metus. Nullam ac mattis nisi. Curabitur porttitor enim sed elementum interdum. Duis sed molestie enim. Nullam varius ex efficitur efficitur mollis. Vestibulum in sollicitudin erat. Quisque in turpis eget leo eleifend ultricies at blandit arcu. Vivamus at pretium quam. Praesent laoreet ligula faucibus ante tincidunt, in euismod massa auctor.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_
The second string appears to be the string for which the huffman tree is built on. I proved my hypothesis by attempting to encode characters that doesn’t exist within the second string, and for those characters, the encoding fails. Also, we see that at the end of the string, there is the {}_
, those are common characters found in CTF flags and was the initial reason for my suspicion.
With that being said, we are able to trace the Morty message to a method located at 0xA514
. In the method,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sub_AE7A((const char *)&unk_154EC, 1, (int)&unk_154FC, 0, v18, 304);
while ( v16 < 0x130 )
{
if ( v18[v16] != byte_1550C[v16] )
{
v19 = 0;
break;
}
++v16;
}
free(v18);
if ( v19 & 1 )
{
v13 = _objc_msgSend(&OBJC_CLASS___NSMutableString, "stringWithString:");
v28 = objc_retainAutoreleasedReturnValue(v13);
}
It seems like our encoded input is fed to the method sub_AE7A
, before being compared to the 0x130 sized byte array byte_1550C
. However, attempts to reverse engineer sub_AE7A
proves to be too complex, and so I chose to go the path of dynamic analysis.
Since the encoded input is thrown into sub_AE7A
, the encoded input string should be 1
or 0
, therefore, I need to provide a plaintext input that gets encoded to a string of 1
or 0
only with a length of 304 characters.
With that being said, I found that e
is encoded to 1111
, and so, 76 e’s would give us an encoded string of 304*1
. And so, Making us of debugserver
and lldb
, I set a breakpoint at 0x0000A780
and dumped the result of the method call for sub_AE7A
.
After some testing, I found out that the encoded string is xor’ed with some unknown values and then compared to byte_1550C
, and so, with the dumped values from before, I wrote a quick Python script to xor each byte with 0x31
which is the hexadecimal representation of 1
, and then comparing this with byte_1550C
, I obtained the encoded string value which we are suppose to provide to solve this challenge.
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
test = "1001}011100}01011}00110}1111}0111110}001110}11100110}000}1110010110}01110110110}0100}11010}11101}11011}011110}0111111}0010}0110}1000}1100}010100}01110101011}111001000}01110101010}01110110100}0101011011}01110110000}010101000}01110111}010101001}0101011100}01110110111}01110100001}010101100}01110100010}01110101101}0111010010}11100111}01010101}11100100101}1110010011}1110010111}11100100100}0111010011}01110101111}01110101110}111001010}01110100000}01010110100}01010111101}01010111111}01010110101}01010111011}01110110011}01110110001}01110100011}01110110101}01010111100}01110110010}01110101100}01010111010}01110101001}01010111110"
value= { " " : "101", "," : "010100", "." : "111000", "0" : "01110101110", "1" : "01110110011", "2" : "01110101010", "3" : "01110111000", "4" : "01110101111", "5" : "01010110110", "6" : "01110110111", "7" : "01110110110", "8" : "01110111100", "9" : "01010110101", "A" : "0111010110", "B" : "01110111110", "C" : "1110011110", "D" : "01110100", "E" : "010101000", "F" : "0101011110", "G" : "01110111101", "H" : "01010110100", "I" : "010101110", "J" : "01110110001", "K" : "01110110000", "L" : "0101011000", "M" : "11100110", "N" : "01010101", "O" : "11100111001", "P" : "1110011101", "Q" : "1110011111", "R" : "01110101000", "S" : "0101011001", "T" : "01010110111", "U" : "01010111111", "V" : "111001001", "W" : "01110110101", "X" : "01110101011", "Y" : "01110111011", "Z" : "01110111010", "_""" : "01110110010", "a" : "1000", "b" : "011100", "c" : "01011", "d" : "00110", "e" : "1111", "f" : "0111110", "g" : "001110", "h" : "11100101", "i" : "000", "j" : "010101001", "k" : "01010111110", "l" : "0100", "m" : "11010", "n" : "11101", "o" : "11011", "p" : "011110", "q" : "0111111", "r" : "0010", "s" : "0110", "t" : "1001", "u" : "1100", "v" : "001111", "w" : "01110111111", "x" : "111001000", "y" : "01110101001", "z" : "01110111001", "{" : "01110110100", "}" : "01110101000" }
b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{_"
for x,i in enumerate(test.split("}")):
#print i,x
value[b[x]]=i
#c5eg s I r Gr esatcn guc a toi oi osskl997ktakmeeeeeeeeeeeR
remappedValue = dict()
for i in value:
remappedValue[value[i]] = i
#print hex(int(value[i],2))
flag = """77 5E 85 25 7A C9 2F B3 31 13 5B B0 E7 89 4E 0B 36 35 D9 1A BB CE EF D6 0E 68 2C 3F C3 9E 94 89 ED 76 0C 39 23 1E 2D 62 44 8C 01 A9 2D AF E9 F0 4B C3 CC BC EB 26 21 61 E5 B6 21 99 6A E4 A8 0A AA 1A FB F6 10 B1 E7 50 47 05 71 9B CB D5 D8 23 6F B4 39 A1 F9 28 B9 F5 14 3B 0C 06 ED 65 D7 D4 D6 25 29 54 6E D9 1B C8 40 68 97 1B 57 D1 43 5B 7B 1E E9 39 13 F0 75 ED C1 7B 1E 71 86 0A 75 5E 68 50 80 0F F4 BA 04 86 9A 69 5A B3 84 BD 8E BF 5D BB CD F6 0F 86 51 7B F9 4E 51 F8 98 C1 03 2E 30 58 CE F8 F0 B9 E9 23 97 8D 83 56 56 86 98 46 EE D7 C6 B2 B6 E4 A3 FB 6C 57 91 85 51 3D F6 07 8C 62 65 98 41 EB 9A F6 14 28 AC 9B DD E2 F7 F3 0C 86 01 60 09 89 11 AC C6 A2 0C BE 2B D2 99 47 EB 11 43 A7 F5 B3 9E 02 7B 89 4C 5C 57 D9 8E 13 27 E3 80 D5 2A 4A 56 78 82 64 4C D6 36 96 81 B1 F1 0E 47 63 23 1C 90 72 7F 3C 81 DA C6 E1 7E 01 CD 90 77 45 39 26 76 CC 79 2A 83 37 90 20 8E F0 61 BA 66 F4 82 4A 3E B1 EB 35 2B 87 15 EE 40 F7"""
key = """76 5e 84 25 7a c8 2f b2 31 12 5b b0 e6 89 4e 0a 36 35 d9 1a ba cf ef d6 0e 69 2c 3e c3 9f 94 89 ec 76 0d 39 22 1e 2c 62 45 8c 01 a9 2c af e8 f0 4a c2 cc bd eb 27 21 60 e5 b6 21 98 6a e4 a8 0a ab 1a fa f7 10 b0 e7 51 47 05 71 9b cb d4 d8 23 6e b4 38 a0 f8 28 b8 f4 14 3a 0c 07 ed 65 d7 d4 d6 24 29 54 6f d9 1a c9 40 68 97 1a 57 d1 42 5a 7a 1e e8 39 13 f0 74 ed c1 7a 1f 70 86 0b 75 5e 69 51 80 0f f4 bb 04 86 9b 68 5b b3 85 bd 8e bf 5c bb cd f7 0e 87 51 7a f9 4e 51 f9 98 c1 02 2e 30 59 cf f8 f0 b8 e8 23 96 8d 82 56 56 86 98 46 ef d6 c6 b3 b7 e5 a3 fa 6c 56 91 85 50 3d f7 07 8d 62 64 98 40 eb 9a f7 14 29 ac 9a dd e2 f7 f2 0c 86 00 60 09 88 10 ac c7 a2 0d be 2b d2 99 47 ea 11 42 a6 f5 b3 9f 03 7a 88 4c 5d 57 d8 8e 13 27 e3 80 d4 2a 4a 57 78 83 64 4c d6 36 96 81 b1 f1 0e 47 63 23 1c 90 72 7f 3c 81 da c6 e1 7e 01 cd 90 77 45 39 26 76 cc 79 2a 83 37 90 20 8e f0 61 ba 66 f4 82 4b 3e b1 eb 34 2b 86 15 ef 41 f6"""
flag = flag.split(" ")
key = key.split(" ")
for x,i in enumerate(key):
key[x]= ("0"+hex(int(key[x],16)^0x31)[2:])[-2:]
from pwn import *
flag_real = ""
for x,i in enumerate(key):
#print x,i, flag[x], xor(int(i,16),int(flag[x],16))
flag_real += xor(int(i,16),int(flag[x],16))
curr = ""
flag3=""
for i in flag_real:
if curr+i in remappedValue:
#skip
curr=curr+i
continue
elif curr in remappedValue:
flag3+=remappedValue[curr]
curr = i
else:
curr=curr+i
print flag3+remappedValue[curr]
Running this Python script gave us our last flag!
Flag #6: cApwN{1m_mr_m33s33ks_l00k_at_meeeeeeeeeee}
Extra information
Notice that in my Python Script, the huffman tree is obtained in a pretty hackish way, this way due to the errors in the 32-bit binaries at the start. I then noticed that with the huffman tree of the 64-bit binary, I’d then be able to get cApwN
at the start of the encoded flag and so, I devised a way to obtain the 64-bit binary huffman tree.
The reason why I was not able to use snoop-it
was because I do not have a jailbroken 64-bit device.
To obtain the huffman tree for 64-bit, I crafted a plaintext string that contains every possible character needed in a normal flag.
The list of possible characters are:
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_
In order to delimit the encoding, I used }
as a delimiter inbetween each character. As }
is located at the bottom of the huffman tree (based on playing around and experimenting), we are able to confidently delimit the encoded huffman code to obtain the accurate 64-bit huffman tree values for each character.