Mobile h1-702 CTF Writeups
This is a walkthrough of all 12 challenges of the 2017 edition of the h1-702 (or h1702) single-player mobile reverse engineering ctf organized by hackerone. Tasks come in two tracks: Android and iOS. Some of the tasks' solutions depend on earlier solutions, so reading this document from top to bottom is recommended.
Challenges
If you want to try to solve the challenges on your own first, I mirrored them over here for you:
-
Android Levels 1, 2, 3, 4: ctfone-490954d49dd51911bc730d8161541cf13e7416f9.apk
-
Android Level 5: ctfone5-8d51e73cf81c0391575de7b40226f19645777322.apk
-
Android Level 6: ctfone6-6118c10be480b994654a1f01cd322af2df2ceab6.apk
-
iOS Levels 1, 2, 3, 4: IntroLevels-727e07e27199b5431fccc16850d67c4fea6596f7.ipa
-
iOS Level 5: Level5-69c2713162cb8f5e9418f8c08f3fa0a1ecb4928d.ipa
-
iOS Level 6: Level6-679e59bdfb40233fb1359d098d7269a3320eabd2.ipa
-
iOS Level 6 (updated during competition): Level6-update-f0887a253daaa02e584bc9ff4edfeca1300887dc.ipa
Solutions
Android
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
So, they promise something easy, giving us a compiled Android App (apk
file). APKs are nothing else but ZIP directories conaining the compiled program
code and all assets the Application needs to run (such as pictures, gui
elements, and so on …). As you might remember, Java (or Dalvik) bytecode is
straightforward to decompile. I don’t have a Java runtime on my computer, which
is why I use (this)[http://www.javadecompilers.com/apk] useful website to decompile
the APK. A quick glance over the contents of the archive
➜ ctfone_source ls
android apktool.yml com okhttp3 original unknown
AndroidManifest.xml assets lib okio res
tells us that the most important parts are
- the decompiled program code in
com/h1702ctf/ctfone/
:
➜ ctfone_source ls com/h1702ctf/ctfone/
ArraysArraysArrays.java InCryption.java MonteCarlo.java TabFragment1.java
BuildConfig.java Level3Activity.java PagerAdapter.java TabFragment2.java
C0222R.java MainActivity.java Requestor.java
- a shared library containing compiled (native) ARMv7 code in
lib/armeabi-v7a
➜ ctfone_source ls lib/armeabi-v7a
libnative-lib.so
- and the
assets
directory containing, well, assets needed by the application
➜ ctfone_source ls assets/
asset1 asset2 asset4 asset6 asset8 tHiS_iS_nOt_tHE_SeCrEt_lEveL_1_fiLE
asset10 asset3 asset5 asset7 asset9
But wait, what is the file with the strange name?
tHiS_iS_nOt_tHE_SeCrEt_lEveL_1_fiLE
? You bet it is:
feh tHiS_iS_nOt_tHE_SeCrEt_lEveL_1_fiLE
Picture showing android level 1 flag
Entering cApwn{WELL_THAT_WAS_SUPER_EASY}
into the score board solves the task.
Level 2
Maybe something a little more difficult?
(Note: Levels 1-4 use the same application)
ctfone-490954d49dd51911bc730d8161541cf13e7416f9.apk
I see. Let’s have a look on the decompiled source code of the application. In
TabFragment1.java
we find code that displays pictures from the assets
directory (which probably should have hinted us towards inspecting the assets
there).
TabFragment2.java
contains the following:
package com.h1702ctf.ctfone;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
public class TabFragment2 extends Fragment {
TextView mHashView;
class C02241 implements OnClickListener {
C02241() {
}
public void onClick(View v) {
try {
TabFragment2.this.mHashView.setText(InCryption.hashOfPlainText());
TabFragment2.this.mHashView.setBackgroundColor(-1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(C0222R.layout.tag_fragment2, container, false);
this.mHashView = (TextView) v.findViewById(C0222R.id.hashText);
((Button) v.findViewById(C0222R.id.hashmebutton)).setOnClickListener(new C02241());
return v;
}
}
Looking at TabFragment2.java
(where we presumably might find the next flag) we
can see that some text field is being assigned the return value of
InCrpytion.hashOfPlainText())
. Let’s inspect this function:
package com.h1702ctf.ctfone;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class InCryption {
static String encryptedHex = "ec49822b8804c07f128bb0552673171d078c";
/* huge hex blob shortened for readability */
public static String hashOfPlainText() throws Exception {
return getHash(new String(hex2bytes(new String(decrypt(hex2bytes("0123456789ABCDEF0123456789ABCDEF"), hex2bytes(encryptedHex))).trim())));
}
private static byte[] decrypt(byte[] raw, byte[] encrypted) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(2, skeySpec);
return cipher.doFinal(encrypted);
}
public static String getHash(String text) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.reset();
return bin2hex(digest.digest(text.getBytes()));
} catch (NoSuchAlgorithmException e1) {
e1.printStackTrace();
return "";
}
}
static byte[] hex2bytes(String s) {
byte[] b = new byte[(s.length() / 2)];
for (int i = 0; i < b.length; i++) {
int index = i * 2;
b[i] = (byte) Integer.parseInt(s.substring(index, index + 2), 16);
}
return b;
}
static String bin2hex(byte[] data) {
return String.format("%0" + (data.length * 2) + "X", new Object[]{new BigInteger(1, data)});
}
public static void main(String args[]) {
System.println(decrypt(hex2bytes("0123456789ABCDEF0123456789ABCDEF"), hex2bytes(encryptedHex)));
}
}
That’s interesting. The code basically decrypts a huge blob using AES in ECB
mode with key 0123456789ABCDEF0123456789ABCDEF
and hashes the result using
SHA-256
. Now, what is the decrypted value of that blob? Three lines of python
reveal the secret:
#!/usr/bin/env python3
from Crypto.Cipher import AES
import binascii
## msg again shortened for better readability
msg = binascii.unhexlify("ec49822b5417f4dad5d6048804c07f128bb055267")
key = binascii.unhexlify("0123456789ABCDEF0123456789ABCDEF")
aes = AES.new(key, AES.MODE_ECB)
## strip PKCS5 padding (11 times hex value 11) and newline
print(binascii.unhexlify(aes.decrypt(msg).decode().strip("\x0b").strip("\x0a")))
Executing this script yields
➜ android ./pwn2.py
b'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 \n'
which looks a lot like Morse code. The internet is full of Morse decoders, so we can extend our python script easily to print the message in clear text:
#!/usr/bin/env python3
from Crypto.Cipher import AES
import binascii
## msg again shortened for better readability
msg = binascii.unhexlify("ec49822b5417f4dad5d6048804c07f128bb055267")
key = binascii.unhexlify("0123456789ABCDEF0123456789ABCDEF")
aes = AES.new(key, AES.MODE_ECB)
dat = binascii.unhexlify(aes.decrypt(msg).strip(b"\x0b").strip(b"\x0a")).decode()
print(dat)
dat = dat.replace('\n', ' ').replace('DASH ','-').replace('DOT ', '.').replace('SPACE ', ' ')
print(dat)
CODE = {'A': '.-', 'B': '-...', 'C': '-.-.',
'D': '-..', 'E': '.', 'F': '..-.',
'G': '--.', 'H': '....', 'I': '..',
'J': '.---', 'K': '-.-', 'L': '.-..',
'M': '--', 'N': '-.', 'O': '---',
'P': '.--.', 'Q': '--.-', 'R': '.-.',
'S': '...', 'T': '-', 'U': '..-',
'V': '...-', 'W': '.--', 'X': '-..-',
'Y': '-.--', 'Z': '--..',
'0': '-----', '1': '.----', '2': '..---',
'3': '...--', '4': '....-', '5': '.....',
'6': '-....', '7': '--...', '8': '---..',
'9': '----.'
}
CODE_REVERSED = {value:key for key,value in CODE.items()}
def from_morse(s):
return ''.join(CODE_REVERSED.get(i) for i in s.split())
print(from_morse(dat))
print(from_morse(dat).replace("BRACKET", "{").replace("UNDERSCORE", "_"))
Where we happily find flag number 2: CAPWN{CRYP706R4PHY_15_H4RD_BR0}
Level 3
Think you can solve level 3?
(Note: Levels 1-4 use the same application)
ctfone-490954d49dd51911bc730d8161541cf13e7416f9.apk
It was not immediately clear to me where the flag for level 3 would be, even
though there was a Level3Activity.java
file. After doing a stupid recursive
grep
over all files, however we can find the string “X-Level3-Flag” in the
shared object with the native arm code. IDA time!
Starting from the X-Level3-Flag
string in the binary and following cross
references backwards one ends up in an exported native function
Java_com_h1702ctf_ctfone_Requestor_hName
. Indeed, there also exists a class
Requestor
in the decompiled java bytecode that references a function hName
as well as a function hVal
:
public class Requestor {
private static String sHostname = "h1702ctf.com";
private static String sUrl = "https://h1702ctf.com/About";
public static native String hName();
public static native String hVal();
public static void request() {
try {
new Builder().certificatePinner(new CertificatePinner.Builder().add(sHostname, "sha256/8yKUtMm6FtEse2v0yDMtT0hKagvpKSWHpnufb1JP5g8=").add(sHostname, "sha256/YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=").add(sHostname, "sha256/Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys=").build()).build().newCall(new Request.Builder().url(sUrl).addHeader(hName(), hVal()).build()).execute();
} catch (IOException e) {
}
}
}
Okay, so hName
is X-Level3-Flag
, what is hVal
? Getting back to IDA Pro,
the Hex-Rays decompiler gives us a simple answer:
int __fastcall Java_com_h1702ctf_ctfone_Requestor_hVal(int a1)
{
int v1; // r1@2
if ( !byte_C0CA[0] )
{
v1 = 0;
do
{
byte_C0CA[v1] = magic_str_at_addr_c012[v1] ^ 0x3E;
++v1;
}
while ( v1 != 72 );
}
return (*(int (**)(void))(*(_DWORD *)a1 + 668))();
}
This function simply xors a string with the constant 0x3e
and returns it back
to the Dalvik runtime. Performing the xor ourselves we find the secret to be
V1RCR2QyUXdOVGROVmpnd1lsWTVkV1JYTVdsTk0wcG1UakpvZVUxNlRqbERaejA5Q2c9PQo=
.
Seeing this, the next step becomes immediately clear, as the trailing =
is
quite characteristic for base64
encoding. This time, we don’t even need python
to do the trick:
echo "V1RCR2QyUXdOVGROVmpnd1lsWTVkV1JYTVdsTk0wcG1UakpvZVUxNlRqbERaejA5Q2c9PQo=" | base64 -d | base64 -d | base64 -d
cApwN{1_4m_numb3r_7hr33}
Yaaay. Flag number three is cApwN{1_4m_numb3r_7hr33}
.
Level 4
Hope you kept your notes.
(Note: Levels 1-4 use the same application)
ctfone-490954d49dd51911bc730d8161541cf13e7416f9.apk
Of course I did. :)
The Java code in Level3Activity
dispatches a sub thread that executes a
function called MonteCarlo.start()
. Unsurprisingly, the code we find there
uses a classic Monte Carlo simulation to calculate the value of pi.
package com.h1702ctf.ctfone;
import android.util.Log;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class MonteCarlo {
private static final String TAG = MonteCarlo.class.toString();
public static class PiValue implements Callable {
double inside = 0.0d;
double pi;
double total = 0.0d;
double f5x;
double f6y;
public Double call() {
for (double i = 0.0d; i < 1000000.0d; i += 1.0d) {
this.f5x = Math.random();
this.f6y = Math.random();
if ((this.f5x * this.f5x) + (this.f6y * this.f6y) <= 1.0d) {
this.inside += 1.0d;
}
this.total += 1.0d;
}
this.pi = (this.inside / this.total) * 4.0d;
return Double.valueOf(this.pi);
}
}
public native String functionnameLeftbraceOneCommaTwoCommaThreeCommaRightbraceFour(String str, String str2, String str3);
public static void start() throws InterruptedException, ExecutionException {
long startTime = System.currentTimeMillis();
ArrayList<Future<Double>> values = new ArrayList();
ExecutorService exec = Executors.newFixedThreadPool(2);
for (int i = 0; i < 2; i++) {
values.add(exec.submit(new PiValue()));
}
ArraysArraysArrays.start();
Double sum = Double.valueOf(0.0d);
Iterator it = values.iterator();
while (it.hasNext()) {
sum = Double.valueOf(sum.doubleValue() + ((Double) ((Future) it.next()).get()).doubleValue());
}
Log.i(TAG, "" + (sum.doubleValue() / ((double) 2)));
Log.i(TAG, "" + ((System.currentTimeMillis() - startTime) / 1000));
}
}
That’s nice, but totally useless for us. But wait, there is another native
function that takes three strings as parameters? Combining the text from the
task description with the function name
functionnameLeftbraceOneCommaTwoCommaThreeCommaRightbraceFour
I assume that we
should call this function with the first three flags as parameters. I’m lazy, so
I don’t have an Android emulator at hand. Let’s see what the native code of this
function does.
int __fastcall Java_com_h1702ctf_ctfone_MonteCarlo_functionnameLeftbraceOneCommaTwoCommaThreeCommaRightbraceFour(int a1, int a2, int a3, int a4, int a5)
{
int v5; // r4@1
int v6; // ST1C_4@1
int v7; // r5@1
const char *v8; // r10@1
int v9; // r0@1
const char *v10; // r5@1
int v11; // ST18_4@1
int v12; // r0@1
const char *v13; // r6@1
int v14; // ST14_4@1
size_t v15; // r9@1
size_t v16; // r8@1
size_t v17; // ST10_4@1
int v18; // r3@1
int v19; // r3@1
int v20; // r3@1
int result; // r0@1
char v22; // [sp+23h] [bp-CDh]@1
char v23; // [sp+47h] [bp-A9h]@1
int v24[14]; // [sp+48h] [bp-A8h]@1
char hash2[32]; // [sp+80h] [bp-70h]@1
char hash1[32]; // [sp+A0h] [bp-50h]@1
char hash0[32]; // [sp+C0h] [bp-30h]@1
int v28; // [sp+E0h] [bp-10h]@1
v5 = a1;
v6 = a3;
v7 = a4;
v28 = _stack_chk_guard;
v8 = (const char *)(*(int (**)(void))(*(_DWORD *)a1 + 676))();
v9 = (*(int (__fastcall **)(int, int, _DWORD))(*(_DWORD *)v5 + 676))(v5, v7, 0);
v10 = (const char *)v9;
v11 = v9;
v12 = (*(int (__fastcall **)(int, int, _DWORD))(*(_DWORD *)v5 + 676))(v5, a5, 0);
v13 = (const char *)v12;
v14 = v12;
v15 = strlen(v8);
v16 = strlen(v10);
v17 = strlen(v13);
j_crypto_generichash((int)hash0, 32, (int)v8, v18, v15, 0, 0);
j_crypto_generichash((int)hash1, 32, (int)v10, v19, v16, 0, 0);
j_crypto_generichash((int)hash2, 32, v14, v20, v17, 0, 0);
v24[8] = *(_DWORD *)hash0;
v24[9] = *(_DWORD *)&hash0[4];
v24[10] = *(_DWORD *)&hash0[8];
v24[11] = *(_DWORD *)hash1;
v24[12] = *(_DWORD *)&hash1[4];
v24[13] = *(_DWORD *)&hash1[8];
v24[0] = *(_DWORD *)hash2;
v24[1] = *(_DWORD *)&hash2[4];
v24[2] = *(_DWORD *)&hash2[8];
v24[3] = *(_DWORD *)&hash2[12];
v24[4] = *(_DWORD *)&hash2[16];
v24[5] = *(_DWORD *)&hash2[20];
v24[6] = *(_DWORD *)&hash2[24];
v24[7] = *(_DWORD *)&hash2[28];
j_crypto_stream_xsalsa20_xor(&v22, secret, 36, 0, &v24[8], v24);
v23 = 0;
(*(void (__fastcall **)(int, int, const char *))(*(_DWORD *)v5 + 680))(v5, v6, v8);
(*(void (__fastcall **)(int, int, int))(*(_DWORD *)v5 + 680))(v5, v6, v11);
(*(void (__fastcall **)(int, int, int))(*(_DWORD *)v5 + 680))(v5, v6, v14);
result = (*(int (__fastcall **)(int, char *))(*(_DWORD *)v5 + 668))(v5, &v22);
if ( _stack_chk_guard != v28 )
_stack_chk_fail(result, _stack_chk_guard - v28);
return result;
}
From a high level perspective it seems the three arguments (flags) are
hashed using whatever method j_crypto_generichash
uses, and the results are
used as decryption key and initialization vector (iv) for the salsa20
stream
cipher.
Tracing the hierarchy of called functions from j_crypto_generichash
quickly
ends up at methods that have blake2b
in their names. A quick web search
confirms that blake2b
is indeed a hash function with configurable digest
length.
At this point, I struggled a lot with finding python implementations of
blake2b
and salsa20
– especially ones that work consistently for Python3.
At the end of the day, I simply came up with two scripts to first use blake2b
to generate key and iv (in python3) and then perform the actual decryption in
python2. The hashing part
#!/usr/bin/env python3.6
from hashlib import blake2b
import binascii
#import nacl.secret
#import nacl.utils
fs = [
"cApwN{WELL_THAT_WAS_SUPER_EASY}",
"CAPWN{CRYP706R4PHY_15_H4RD_BR0}",
"cApwN{1_4m_numb3r_7hr33}",
]
hs = []
for f in fs:
b = blake2b(digest_size = 32)
b.update(f.encode())
hs.append(b.digest())
print("key = binascii.unhexlify(\"{}\")".format(binascii.hexlify(hs[2]).decode()))
print("iv = binascii.unhexlify(\"{}\")".format(binascii.hexlify(hs[0][:12] + hs[1][:12]).decode()))
gives us
➜ 2017_07_h1702 ./pwn4_blake.py
key = binascii.unhexlify("26da0d94c3371d1f56615ce14b2dd01664b7eecb83bb8112677044a61f0466b8")
iv = binascii.unhexlify("cd200e8391fc7603abd77974cd04e9c369374a4ad5a3bde8")
Whereas the decryption (note how key =
and iv =
are pasted into the script)
#!/usr/bin/env python
import binascii
import salsa20
flag = ''.join(map(chr, [
0x48, 0x04, 0x6D, 0xB4, 0x8C, 0x8A, 0x6D, 0x57, 0x4C, 0xC5,
0x4B, 0x41, 0xD1, 0xDC, 0xB2, 0xC0, 0x90, 0x1D, 0xE2, 0x6B,
0x85, 0x15, 0x25, 0xFD, 0x91, 0x1F, 0x62, 0x00, 0x45, 0xA9,
0x54, 0x5A, 0x85, 0x75, 0xA5, 0xFC
]))
key = binascii.unhexlify("26da0d94c3371d1f56615ce14b2dd01664b7eecb83bb8112677044a61f0466b8")
iv = binascii.unhexlify("cd200e8391fc7603abd77974cd04e9c369374a4ad5a3bde8")
print(salsa20.XSalsa20_xor(flag, iv, key))
prints
➜ 2017_07_h1702 ./pwn4_salsa.py
cApwN{w1nn3r_w1nn3r_ch1ck3n_d1nn3r!}
Giving us flag number four cApwN{w1nn3r_w1nn3r_ch1ck3n_d1nn3r!}
.
Level 5
Hmmm... looks like you need to get past something...
ctfone5-8d51e73cf81c0391575de7b40226f19645777322.apk
A new APK file! \o/
Decompiling the APK yields the following code in MainActivity.java
:
package com.h1702ctf.ctfone5;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
class C02201 implements OnClickListener {
C02201() {
}
public void onClick(View view) {
Snackbar.make(view, (CharSequence) "State the secret phrase (omit the oh ex)", 0).setAction((CharSequence) "Action", null).show();
}
}
class C02212 implements OnClickListener {
C02212() {
}
public void onClick(View v) {
((TextView) MainActivity.this.findViewById(C0222R.id.flagOutput)).setText(MainActivity.this.flag(((TextView) MainActivity.this.findViewById(C0222R.id.s0)).getText().toString(), ((TextView) MainActivity.this.findViewById(C0222R.id.s1)).getText().toString(), ((TextView) MainActivity.this.findViewById(C0222R.id.s2)).getText().toString()));
((TextView) MainActivity.this.findViewById(C0222R.id.flagOutput)).setTextColor(-1);
}
}
public native String flag(String str, String str2, String str3);
static {
System.loadLibrary("native-lib");
}
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView((int) C0222R.layout.activity_main);
setSupportActionBar((Toolbar) findViewById(C0222R.id.toolbar));
((FloatingActionButton) findViewById(C0222R.id.fab)).setOnClickListener(new C02201());
((Button) findViewById(C0222R.id.submitButton)).setOnClickListener(new C02212());
}
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(C0222R.menu.menu_main, menu);
return true;
}
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == C0222R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
}
The application wants us to State the secret phrase
without
0x
and offers three input fields to do so. The user inputs are then fed into
a native function flag
. Additionally, there is another native function one
that is called in CruelIntentions.java
. As we have no idea what we should feed
into the flag
function, let’s focus on the cruel intentions the challenge
authors might have.
signed int __fastcall Java_com_h1702ctf_ctfone5_CruelIntentions_one(int a1, int a2)
{
int v2; // r0@4
__pid_t v3; // r0@5
FILE *v4; // r0@5
int v5; // r0@8
int v6; // r0@9
int v7; // r0@12
int *v8; // r1@16
const char *v9; // r1@18
unsigned __int32 rand; // r0@24
int palindrome_len; // r1@28
signed int v13; // r1@31
signed int v14; // r1@32
signed int v15; // r2@33
int *v16; // r0@41
int *v17; // r0@42
int v18; // r1@42
int *v19; // r0@43
int v20; // r1@44
bool v21; // zf@44
bool v22; // nf@44
unsigned __int8 v23; // vf@44
int v24; // t1@55
__pid_t v25; // r0@23
FILE *v26; // r0@23
int v27; // r0@60
int v28; // r0@61
int v29; // r0@64
int v30; // r0@68
signed int result; // r0@69
int v32; // [sp+8h] [bp-960h]@25
int v33; // [sp+Ch] [bp-95Ch]@68
int *v34; // [sp+10h] [bp-958h]@68
int v35; // [sp+14h] [bp-954h]@67
int v36; // [sp+18h] [bp-950h]@64
int v37; // [sp+1Ch] [bp-94Ch]@61
const char *v38; // [sp+20h] [bp-948h]@59
int v39; // [sp+24h] [bp-944h]@23
int *v40; // [sp+28h] [bp-940h]@23
__pid_t v41; // [sp+2Ch] [bp-93Ch]@23
int v42; // [sp+30h] [bp-938h]@44
int v43; // [sp+34h] [bp-934h]@32
int v44; // [sp+38h] [bp-930h]@31
char *v45; // [sp+3Ch] [bp-92Ch]@28
int *v46; // [sp+40h] [bp-928h]@25
unsigned int v47; // [sp+44h] [bp-924h]@24
int v48; // [sp+48h] [bp-920h]@15
int v49; // [sp+4Ch] [bp-91Ch]@12
int v50; // [sp+50h] [bp-918h]@9
const char *v51; // [sp+54h] [bp-914h]@7
int v52; // [sp+58h] [bp-910h]@5
int *v53; // [sp+5Ch] [bp-90Ch]@5
__pid_t v54; // [sp+60h] [bp-908h]@5
int v55; // [sp+64h] [bp-904h]@4
int v56; // [sp+68h] [bp-900h]@4
int v57; // [sp+6Ch] [bp-8FCh]@3
int v58; // [sp+70h] [bp-8F8h]@1
int v59; // [sp+74h] [bp-8F4h]@1
int *v60; // [sp+78h] [bp-8F0h]@1 MAPDST
int cookie; // [sp+7Ch] [bp-8ECh]@1
int v62; // [sp+80h] [bp-8E8h]@1
int v63; // [sp+88h] [bp-8E0h]@1
int v64; // [sp+8Ch] [bp-8DCh]@1
char being_traced; // [sp+A3h] [bp-8C5h]@9
char not_rooted; // [sp+ABh] [bp-8BDh]@19
char is_a_palindrome; // [sp+CBh] [bp-89Dh]@25
char v68; // [sp+E3h] [bp-885h]@61
int v69; // [sp+E4h] [bp-884h]@68
char *v70[6]; // [sp+140h] [bp-828h]@16
char *v71; // [sp+144h] [bp-824h]@16
char *v72; // [sp+148h] [bp-820h]@16
char *v73; // [sp+14Ch] [bp-81Ch]@16
char *v74; // [sp+150h] [bp-818h]@16
char *v75; // [sp+154h] [bp-814h]@16
int v76; // [sp+158h] [bp-810h]@6
__int16 v77; // [sp+162h] [bp-806h]@8
int v78; // [sp+558h] [bp-410h]@5
cookie = _stack_chk_guard;
v64 = a1;
v63 = a2;
v60 = &v62;
v59 = a2;
v58 = a1;
if ( prctl(3) )
{
if ( is_dumpable & 1 )
v57 = raise(11);
v56 = 0;
v2 = prctl(4);
is_dumpable = 1;
v55 = v2;
}
v60[7] = 0x400;
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]);
being_traced = 1;
v50 = v6;
goto LABEL_14;
}
break;
}
}
v7 = fclose((FILE *)v60[5]);
being_traced = 0;
v49 = v7;
}
else
{
being_traced = 0;
}
LABEL_14:
if ( being_traced == 1 )
v48 = raise(SIGSEGV);
v70[0] = off_CD3C[0];
v71 = off_CD40[0];
v72 = off_CD44[0];
v73 = off_CD48[0];
v74 = off_CD4C[0];
v75 = off_CD50;
v8 = v60;
v60[1] = 0;
*v8 = 0;
v8[9] = 0;
while ( (unsigned int)v60[9] <= 0xA )
{
v9 = g_su_paths[v60[9]];
v60[11] = (int)v9;
if ( access(v9, 0) == -1 )
{
not_rooted = 1;
goto LABEL_22;
}
++v60[9];
}
not_rooted = 0;
LABEL_22:
if ( not_rooted )
{
rand = sub_29A8();
v60[1] = rand % 6;
v47 = 0xAAAAAAAB * rand;
while ( 1 )
{
v60[19] = (int)v70[v60[1]];
is_a_palindrome = 1;
v60[17] = 0;
v60[16] = 0;
v60[15] = 0;
v60[14] = 0;
v46 = &v32;
while ( *(_BYTE *)(v60[19] + v60[17]) )
++v60[17];
palindrome_len = v60[17];
v60[13] = (int)&v32;
v45 = (char *)&v32 - ((palindrome_len + 8) & 0xFFFFFFF8);
while ( v60[16] < v60[17] )
{
while ( 1 )
{
if ( (signed int)*(_BYTE *)(v60[19] + v60[16]) < 0x41
|| (v13 = *(_BYTE *)(v60[19] + v60[16]), v44 = 1, v13 >= 0x5B) )
{
v14 = *(_BYTE *)(v60[19] + v60[16]);
v43 = 0;
if ( v14 >= 0x61 )
{
v15 = 0;
if ( (signed int)*(_BYTE *)(v60[19] + v60[16]) < 0x7B )
v15 = 1;
v43 = v15;
}
v44 = v43;
}
if ( !(v44 & 1) )
break;
++v60[16];
}
v60[12] = v60[15];
while ( v60[12] < v60[16] )
{
v16 = v60;
v45[v60[14]] = *(_BYTE *)(v16[19] + v16[12]);
++v16[14];
++v16[12];
}
v17 = v60;
v18 = v60[16] + 1;
v60[16] = v18;
v17[15] = v18;
}
v19 = v60;
v45[v60[14]] = 0;
v19[16] = 0;
while ( 1 )
{
v20 = v60[16];
v23 = __OFSUB__(v20, (v60[14] - 1) / 2);
v21 = v20 == (v60[14] - 1) / 2;
v22 = v20 - (v60[14] - 1) / 2 < 0;
v42 = 0;
if ( (unsigned __int8)(v22 ^ v23) | v21 )
v42 = (unsigned __int8)is_a_palindrome;
if ( !(v42 & 1) )
break;
if ( (unsigned __int8)v45[v60[16]] != (unsigned __int8)v45[v60[14] - v60[16] - 1]
&& (unsigned __int8)v45[v60[16]] != (unsigned __int8)v45[v60[14] - v60[16] - 1] - 32
&& (unsigned __int8)v45[v60[16]] != (unsigned __int8)v45[v60[14] - v60[16] - 1] + 32
&& (unsigned __int8)v45[v60[16]] - 32 != (unsigned __int8)v45[v60[14] - v60[16] - 1]
&& (unsigned __int8)v45[v60[16]] + 32 != (unsigned __int8)v45[v60[14] - v60[16] - 1] )
{
is_a_palindrome = 0;
}
++v60[16];
}
v24 = v60[13];
if ( is_a_palindrome & 1 )
break;
v60[1] = 0;
}
}
else
{
v60[23] = 1024;
v25 = getpid();
v60[22] = v25;
v41 = v25;
v40 = &v78;
v39 = sprintf((char *)&v78, "/proc/%d/status", v25);
v26 = fopen((const char *)&v78, "r");
v60[21] = (int)v26;
if ( v26 )
{
while ( fgets((char *)&v76, 1024, (FILE *)v60[21]) )
{
v38 = "TracerPid";
if ( !strncmp((const char *)&v76, "TracerPid", 9u) )
{
v27 = atoi((const char *)&v77);
v60[20] = v27;
if ( v27 )
{
v28 = fclose((FILE *)v60[21]);
v68 = 1;
v37 = v28;
goto LABEL_66;
}
break;
}
}
v29 = fclose((FILE *)v60[21]);
v68 = 0;
v36 = v29;
}
else
{
v68 = 0;
}
LABEL_66:
if ( v68 == 1 )
{
v35 = raise(11);
return 0xBEA7AB1E;
}
v34 = &v69;
_aeabi_memclr();
v33 = _system_property_get("mobsec.setme");
v30 = atoi((const char *)&v69);
*v60 = v30;
if ( v30 == 1 )
return 0xBEA7AB1E;
}
result = _stack_chk_guard;
if ( _stack_chk_guard != cookie )
_stack_chk_fail(_stack_chk_guard);
return result;
}
Reading this huge function makes clear that it basically tries to detect all different kinds of dynamic analysis artefacts/techniques/tools/frameworks, such as:
- The
dumpability
state of the main memory of the program - The linux debugging API
ptrace
by reading the value ofTracerPid
from/proc/self/status
- Any sign of a rooted phone by checking whether any of the files
/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
, or/data/local/su
exists. - The system property
mobsec.setme
which (presumably) is an artefact of the Mobi Sec analysis framework
If any of the conditions is true, the process is killed.
What is most interesting is the magic value set if v68
is true (right at
LABEL_66
). The value 0xBEA7AB1E
could be read as hex-speak. More
interestingly, Hex-Rays fools us at this point as there is not only one constant
but actually four, as becomes evident from the assembly code:
.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
This happens because the calling convention on ARM only specifies one return
value via r0
, luring the decompiler into removing the other constants from the
output. Performing the additions manually (and truncating to 32 bits) we find
the following constants:
seed = [
0xbea7ab1e,
0xface1e55,
0xda7aba5e,
0xdeadbabe,
]
Alright! We found the components of the secret phrase (remember the String in
MainActivity
?). We should feed them into the flag
function, which
fortunately looks very familiar:
int __fastcall Java_com_h1702ctf_ctfone5_MainActivity_flag(_JNIEnv *a1, int a2, int a3, int a4, int a5)
{
int v5; // r0@1
int v6; // r0@1
char *v7; // r1@1
int v8; // r1@1
int v9; // r4@1
int v10; // r0@1
int v11; // t1@1
bool v12; // zf@1
int v14; // [sp+10h] [bp-128h]@1
int v15; // [sp+14h] [bp-124h]@1
int v16; // [sp+18h] [bp-120h]@1
const char *v17; // [sp+1Ch] [bp-11Ch]@1
char *v18; // [sp+20h] [bp-118h]@1
int v19; // [sp+24h] [bp-114h]@1
char *v20; // [sp+28h] [bp-110h]@1
int v21; // [sp+2Ch] [bp-10Ch]@1
int v22; // [sp+30h] [bp-108h]@1
signed int v23; // [sp+34h] [bp-104h]@1
int v24; // [sp+38h] [bp-100h]@1
_DWORD *v25; // [sp+3Ch] [bp-FCh]@1
_JNIEnv *v26; // [sp+40h] [bp-F8h]@1
int v27; // [sp+44h] [bp-F4h]@1
int v28; // [sp+48h] [bp-F0h]@1
char *v29; // [sp+4Ch] [bp-ECh]@1
int v30; // [sp+50h] [bp-E8h]@1
int v31; // [sp+54h] [bp-E4h]@1
char v32; // [sp+58h] [bp-E0h]@1
size_t v33; // [sp+5Ch] [bp-DCh]@1
size_t v34; // [sp+60h] [bp-D8h]@1
size_t v35; // [sp+64h] [bp-D4h]@1
const char *v36; // [sp+68h] [bp-D0h]@1
const char *v37; // [sp+6Ch] [bp-CCh]@1
const char *v38; // [sp+70h] [bp-C8h]@1
int v39; // [sp+74h] [bp-C4h]@1
int v40; // [sp+78h] [bp-C0h]@1
int v41; // [sp+7Ch] [bp-BCh]@1
int v42; // [sp+80h] [bp-B8h]@1
int v43; // [sp+84h] [bp-B4h]@1
char key; // [sp+8Ch] [bp-ACh]@1
int iv[6]; // [sp+ACh] [bp-8Ch]@1
char hash2[8]; // [sp+C4h] [bp-74h]@1
int hash1[8]; // [sp+E4h] [bp-54h]@1
int hash0[8]; // [sp+104h] [bp-34h]@1
v31 = _stack_chk_guard;
v43 = a2;
v42 = a3;
v41 = a4;
v40 = a5;
v39 = 30;
v30 = 0;
v29 = &v32;
v28 = a3;
v27 = a2;
v26 = a1;
v25 = &_stack_chk_guard;
v24 = a4;
v38 = (const char *)_JNIEnv::GetStringUTFChars(a1, a3, 0);
v37 = (const char *)_JNIEnv::GetStringUTFChars(a1, v41, 0);
v36 = (const char *)_JNIEnv::GetStringUTFChars(a1, v40, 0);
v35 = strlen(v38);
v34 = strlen(v37);
v33 = strlen(v36);
v23 = 32;
v22 = j_crypto_generichash(hash0, 32, v38);
v21 = j_crypto_generichash(hash1, 32, v37);
v20 = hash2;
v5 = j_crypto_generichash(hash2, 32, v36);
iv[2] = hash0[2];
iv[1] = hash0[1];
iv[0] = hash0[0];
iv[5] = hash1[2];
iv[4] = hash1[1];
iv[3] = hash1[0];
v19 = v5;
v18 = &key;
_aeabi_memcpy(&key, hash2, 32);
*(_DWORD *)&v32 = &v14;
v17 = (char *)&v14 - ((v39 + 8) & 0xFFFFFFF8);
v6 = j_crypto_stream_xsalsa20_xor((int)v17, (int)byte_B236, v39, 0, (int)iv, (int)&key);
v7 = v29;
v17[*((_DWORD *)v29 + 7)] = v30;
v8 = *((_DWORD *)v7 + 10);
v9 = *((_DWORD *)v29 + 6);
v16 = v6;
_JNIEnv::ReleaseStringUTFChars(a1, v8, v9);
_JNIEnv::ReleaseStringUTFChars(a1, *((_DWORD *)v29 + 10), *((_DWORD *)v29 + 5));
_JNIEnv::ReleaseStringUTFChars(a1, *((_DWORD *)v29 + 10), *((_DWORD *)v29 + 4));
v10 = _JNIEnv::NewStringUTF(a1, v17);
v11 = *(_DWORD *)v29;
v12 = *v25 == v31;
v15 = v10;
if ( !v12 )
_stack_chk_fail(v10);
return v15;
}
Yep! That’s again the combination of blake2b
and salsa20
from Level 4.
Unfortunately, this time my ghetto python3 to python2 copying approach will not
work as we still do not know which ones of the four magic words to use in what
order. Let’s automate things a bit. The following is do_blake.py
which brute
forces the correct key and IV:
#!/usr/bin/env python3.6
from hashlib import blake2b
import binascii
import subprocess
import itertools
import time
seed = [
"bea7ab1e",
"face1e55",
"da7aba5e",
"deadbabe",
]
for fs in itertools.permutations(seed, 3):
hs = []
for f in fs:
b = blake2b(digest_size = 32)
b.update(f.encode())
hs.append(b.digest())
open('key', 'wb').write(hs[2])
open('iv', 'wb').write(hs[0][:12] + hs[1][:12])
time.sleep(1)
p = subprocess.Popen('./do_salsa.py', stdout=subprocess.PIPE)
buf = p.stdout.read(2048)
print(buf)
And this, in turn, is do_salsa.py
which deals with the crypto part
#!/usr/bin/env python
import binascii
import salsa20
flag = ''.join(map(chr, [
0x2F, 0xA5, 0x44, 0x02, 0xF1, 0x1C, 0x38, 0x03, 0xF4, 0x9F,
0x00, 0x04, 0x38, 0x62, 0xD9, 0xE1, 0x53, 0x14, 0x3D, 0x7F,
0x25, 0x1A, 0x4D, 0xD2, 0xBC, 0x48, 0x16, 0x4E, 0xD9, 0xB4,
]))
key = open('key', 'rb').read()
iv = open('iv', 'rb').read()
#print(flag, key, iv)
flag = salsa20.XSalsa20_xor(flag, iv, key)
if 'cApwN' in flag:
print(flag)
Executing do_blake.py
prints flag number 5: cApwN{sPEaK_FrieNd_aNd_enteR!}
Level 6
I can't think of anything creative... just try to solve this one :)
ctfone6-6118c10be480b994654a1f01cd322af2df2ceab6.apk
This level basically ping-pong between the Dalvik code and the native runtime.
First, a file something.jar
is decrypted using AES in CBC mode with key
booper
and iv dooper
, which we can get from strings.xml
. This leads us to
the following python code for decryption:
#!/usr/bin/env python3
from Crypto.Cipher import AES
key = b"UCFh%divfMtY3pPD"
iv = b"nY6FtpPFXnh,yjvc"
aes = AES.new(key, AES.MODE_CBC, iv)
open('something_decrypted.jar', 'wb').write(aes.decrypt(open('something.jar',
'rb').read()))
Decompiling this decrypted JAR file gives us a file Pooper.jar
which again
performs some crypto:
package com.example.something;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class Pooper extends BroadcastReceiver {
private BufferedInputStream bis;
public Pooper(BufferedInputStream _bis) {
this.bis = _bis;
}
public boolean checkSomething1(String a) {
boolean didSomething = true;
int i = 0;
while (i < a.length()) {
switch (a.charAt(i)) {
case '1':
if (i == 1) {
break;
}
didSomething = false;
break;
case '4':
if (!(i == 6 || i == 10)) {
didSomething = false;
break;
}
case 'a':
if (i == 2) {
break;
}
didSomething = false;
break;
case 'b':
if (!(i == 0 || i == 4 || i == 8 || i == 12)) {
didSomething = false;
break;
}
case 'h':
if (!(i == 3 || i == 7 || i == 11)) {
didSomething = false;
break;
}
case 'l':
if (!(i == 5 || i == 9 || i == 13)) {
didSomething = false;
break;
}
case 'o':
if (i == 14) {
break;
}
didSomething = false;
break;
case 'p':
if (i == 15) {
break;
}
didSomething = false;
break;
default:
didSomething = false;
break;
}
i++;
}
return didSomething;
}
public boolean checkSomething2(String a) {
boolean didSomething = true;
int i = 0;
while (i < a.length()) {
switch (a.charAt(i)) {
case 'a':
if (i == 9) {
break;
}
didSomething = false;
break;
case 'd':
if (!(i == 8 || i == 14)) {
didSomething = false;
break;
}
case 'g':
if (i == 11) {
break;
}
didSomething = false;
break;
case 'h':
if (!(i == 2 || i == 5)) {
didSomething = false;
break;
}
case 'i':
if (i == 6) {
break;
}
didSomething = false;
break;
case 'm':
if (!(i == 0 || i == 1 || i == 3)) {
didSomething = false;
break;
}
case 'o':
if (!(i == 12 || i == 13)) {
didSomething = false;
break;
}
case 's':
if (!(i == 7 || i == 15)) {
didSomething = false;
break;
}
case 't':
if (!(i == 10 || i == 4)) {
didSomething = false;
break;
}
default:
didSomething = false;
break;
}
i++;
}
return didSomething;
}
public void onReceive(Context context, Intent intent) {
String thing1 = intent.getStringExtra("herpaderp");
String thing2 = intent.getStringExtra("lerpaherp");
if (!(checkSomething1(thing1) && checkSomething2(thing2))) {
System.exit(0);
}
File soInternalStoragePath = new File(context.getDir("dex", 0), "super-dooper");
soInternalStoragePath.delete();
try {
BufferedOutputStream soWriter = new BufferedOutputStream(new FileOutputStream(soInternalStoragePath));
byte[] buf = new byte[this.bis.available()];
this.bis.read(buf);
soWriter.write(decrypt(thing1, thing2, buf));
soWriter.close();
this.bis.close();
} catch (IOException e) {
}
soInternalStoragePath.setExecutable(true);
try {
Runtime.getRuntime().exec(soInternalStoragePath.getAbsolutePath());
} catch (Exception e2) {
}
}
public static byte[] decrypt(String key, String initVector, byte[] encrypted) {
try {
IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(2, skeySpec, iv);
return cipher.doFinal(encrypted);
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}
}
Again AES-CBC. The most interesting part is that key
and iv
are checked
by checkSomething1
and checkSomething2
by simply matching characters to
string positions. Understanding the ´for´-´switch´-combinations brings us to the
following python decryptor:
#!/usr/bin/env python3
from Crypto.Cipher import AES
key = b"b1ahbl4hbl4hblop"
iv = b"mmhmthisdatgoods"
aes = AES.new(key, AES.MODE_CBC, iv)
open('secretasset_decrypted', 'wb').write(aes.decrypt(open('secretasset',
'rb').read()))
The decrypted asset now is a native object:
➜ lvl6 file secretasset_decrypted
secretasset_decrypted: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /system/bin/linker, BuildID[sha1]=12c1ab8273eb1b3b193b61aaa45a2a02a332f32f, stripped
This is a standalone binary with lots of functions. Even though I did not read the complete binary the basic functionality is as follows:
- Open up a listening socket on port 1337
- Spawn one worker thread per connection
- Interact with the other side by means of the commands
\QUIT
\PING
\NAME
and\PRIVATE
- The
\PRIVATE
packet is checked for some structure and data is secrypted using static byte keys, eventually calling function0x1820
- Function
0x1820
performs byte-wise hashing of the supplied payload and compares the hashes against an array of 33 hard-coded hashes
Hmm … We should be able to crack 1-byte-hashes shouldn’t we? Hashing is
performed using a classic MD_Init
/MD_Update
/MD_Finalize
combintation, and
MD_Init
looks very familiar:
int __fastcall MD_Init(int result)
{
*(_DWORD *)(result + 8) = 0x67452301;
*(_DWORD *)(result + 20) = 0x10325476;
*(_DWORD *)(result + 12) = 0xEFCDAB89;
*(_DWORD *)(result + 16) = 0x98BADCFE;
*(_DWORD *)result = 0;
*(_DWORD *)(result + 4) = 0;
return result;
}
Those are the MD5 constants! So writing python code that hashes one byte in the range from 0 to 255 once and comparing the output to the hashes should be enough to win, right?
… nope.
Turns out, none of the hashes generated from the 1-byte-input matches any of the hashes stored in the binary. sad They customized their MD5 implementation.
At this point, after quickly skimming over the MD5 implementation to make sure it wasn’t something obvious, where they deviated from textbook MD5 I decided that the hashing code was self-contained enough to be executed using an emulator.
The following is a python script that uses the Unicorn Engine to first execute the custom hash in the ARM binary on all possible 1-byte-inputs to build up a small rainbow-table. In a second step, the code looks up the hashes found in the binary and prints out the corresponding preimage:
#!/usr/bin/env python3
from unicorn import *
from unicorn.arm_const import *
from IPython import embed
import struct
dat = open('secretasset_decrypted', 'rb').read()[:0x6000]
print(hex(len(dat)))
ADDRESS = 0
CTXT = 0x40000000
INPUT = 0x50000000
STACK = 0x60000000
def hook_block(uc, address, size, user_data):
print(">>> Tracing basic block at 0x%x, block size = 0x%x" %(address, size))
# callback for tracing instructions
#def hook_code(uc, address, size, user_data):
# print(">>> Tracing instruction at 0x%x, instruction size = 0x%x"
# %(address, size))
def hook_memcpy(uc, address, size, user_data):
print(">>> Patching memcpy ...")
dat = uc.mem_read(uc.reg_read(UC_ARM_REG_R1), uc.reg_read(UC_ARM_REG_R2))
uc.mem_write(uc.reg_read(UC_ARM_REG_R0), bytes(dat))
uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR))
def hook_memset(uc, address, size, user_data):
print(">>> Patching memset ...")
dat = uc.reg_read(UC_ARM_REG_R1)
size = uc.reg_read(UC_ARM_REG_R2)
uc.mem_write(uc.reg_read(UC_ARM_REG_R0), bytes(dat * size))
uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR))
def try_char(c):
# Initialize emulator in ARM mode
mu = Uc(UC_ARCH_ARM, UC_MODE_ARM)
# map 2MB memory for this emulation
mu.mem_map(ADDRESS, len(dat))
mu.mem_map(CTXT, 0x1000)
mu.mem_map(INPUT, 0x1000)
mu.mem_map(STACK, 0x1000)
# write machine code to be emulated to memory
mu.mem_write(ADDRESS, dat)
mu.mem_write(CTXT, b"\x00" * 0x1000)
mu.mem_write(INPUT, b"\xff" * 0x1000)
mu.mem_write(STACK, b"\x00" * 0x1000)
mu.mem_write(INPUT, bytes([c]))
# initialize machine registers
mu.reg_write(UC_ARM_REG_R5, 0) ## counter
mu.reg_write(UC_ARM_REG_R6, CTXT)
mu.reg_write(UC_ARM_REG_R8, 0) ## some offset
mu.reg_write(UC_ARM_REG_R11, INPUT) ## input
mu.reg_write(UC_ARM_REG_SP, STACK + 0xf00) ## input
mu.reg_write(UC_ARM_REG_APSR, 0xFFFFFFFF) #All application flags turned on
# tracing all basic blocks with customized callback
mu.hook_add(UC_HOOK_BLOCK, hook_block)
#mu.hook_add(UC_HOOK_CODE, hook_code)
mu.hook_add(UC_HOOK_CODE, hook_memcpy, begin = 0x958, end = 0x958)
mu.hook_add(UC_HOOK_CODE, hook_memcpy, begin = 0x3cec, end = 0x3cec)
mu.hook_add(UC_HOOK_CODE, hook_memset, begin = 0x964, end = 0x964)
# tracing one instruction at ADDRESS with customized callback
#mu.hook_add(UC_HOOK_CODE, hook_code, begin=ADDRESS, end=ADDRESS)
# emulate machine code in infinite time
mu.emu_start(0x1916 | 1, 0x1932)
# now print out some registers
print(">>> Emulation done. Below is the CPU context")
return mu.mem_read(mu.reg_read(UC_ARM_REG_SP) + 0xac, 0x10)
hashes = []
for i in range(256):
hashes.append(try_char(i))
tgts = []
for i in range(35):
tgts.append(bytearray(struct.unpack("<16I", dat[0x5020 + i * 0x10 * 4:][:0x10 * 4])))
for i in range(35):
print(chr(hashes.index(tgts[i])), end = '')
print('')
I had to monkey-patch memset
and memcpy
, as those are normally implemented by
the C standard library and therefore not available during emulation. Afterwards,
it prints out the flag just fine: cApwN{d3us_d3x_my_4pk_is_augm3nted}
This finalizes the Android part.
iOS
For solving the iOS tasks I only took short notes, but it is probably still pretty easy to follow along.
Level 1
- Unpack the IPA file using
unzip
- Open
Payload/IntroLevels.app/IntroLevels
in IDA Pro - Find string
Level 1: The flag isn't in the code!
- Open
Assets.car
using https://github.com/insidegui/AssetCatalogTinkerer - Find picture showing the flag for Level 1:
cApwN{y0u_are_th3_ch0sen_1}
Level 2
- Open
Payload/IntroLevels.app/IntroLevels
in IDA Pro - Search for
Level2
- Find
Level2ViewController
- Inspect
:buttonTouched
method - Code hashes user input using MD5 and compares against
5b6da8f65476a399050c501e27ab7d91
- Crack the hash above, find preimage “424241”
- Afterwards the code constructs
"424241" + "1234" + "424241"
usingStringCore6Append
– this is a AES key used later - Then, “deadbeefc4febab3” is instantiated as an AES IV (call @ 0xAC34)
- Afterwards, the message that should be decrypted is created in-line by the code using (mostly) byte constants (addr 0xAC9C)
- Last, AES128 in CBC mode is invoked
- Solution script:
#!/usr/bin/env python3
from Crypto.Cipher import AES
wat = bytes([
0xd3, 0x33, 0x6b, 0x68, 0x29, 0xf6, 0x72, 0x67,
0x0e, 0x80, 0x21, 0x03, 0x3a, 0x73,
0x1C, 0x94, 0x0F, 0x31, 0x28, 0xAB, 0x40, 0x63,
0x4E, 0x29, 0x11, 0xB9, 0xF1, 0xF4, 0x3F, 0x92,
0xd4, 0xa6
])
msg = bytes([
0xDD, 0x2A, 0x7A, 0xEC, 0xEE, 0x8B, 0x7D, 0xEC,
0x0E, 0x72, 0x33, 0xC7, 0x1B, 0xE3, 0xF7, 0x50,
0xFC, 0x4B, 0x7A, 0x85, 0x2C, 0xA0, 0xE1, 0x19,
0x7F, 0x54, 0x60, 0xD3, 0x16, 0x6D, 0x62, 0xFD,
])
iv = b"deadbeefc4febab3"
key = b"4242411234424241"
aes = AES.new(key, AES.MODE_CBC, iv)
print(aes.decrypt(wat))
print(len(key))
- Flag (after removing PKCS5 padding):
cApwN{0mg_d0es_h3_pr4y}
Level 3
- Open
Payload/IntroLevels.app/IntroLevels
in IDA Pro - Search for
Level3
- Find
Level3ViewController
- Inspect constructor being referenced by data structure at @ 0x45688
- Constructor starts incrementally building up strings contained in a dictionary at 0xE964.
- Strings built using character-wise append on dictionary elements
1hpsqszartypcxwxuiqj
andquiwqadzxvyqueucsqi
- Inspect each call manually, find following sequence of added characters:
append_o_to_1hpsqszartypcxwxuiqj(a1);
append_A_to_quiwqadzxvyqueucsqi(a1);
append_p_to_quiwqadzxvyqueucsqi(a1);
apppend_w_to_quiwqadzxvyqueucsqi(a1);
append_o_to_1hpsqszartypcxwxuiqj(a1);
append_k_to_1hpsqszartypcxwxuiqj(a1);
append_space_to_1hpsqszartypcxwxuiqj(a1);
append_a_to_1hpsqszartypcxwxuiqj(a1);
append_t_to_1hpsqszartypcxwxuiqj(a1);
append_space_to_1hpsqszartypcxwxuiqj(a1);
apppend_N_to_quiwqadzxvyqueucsqi(a1);
apppend_brace_to_quiwqadzxvyqueucsqi(a1);
apppend_1_to_quiwqadzxvyqueucsqi(a1);
apppend_m_to_quiwqadzxvyqueucsqi(a1);
apppend_underscore_to_quiwqadzxvyqueucsqi(a1);
append_m_to_1hpsqszartypcxwxuiqj(a1);
append_e_to_1hpsqszartypcxwxuiqj(a1);
append_space_to_1hpsqszartypcxwxuiqj(a1);
apppend_1_to_quiwqadzxvyqueucsqi(a1);
apppend_n_to_quiwqadzxvyqueucsqi(a1);
apppend_underscore_to_quiwqadzxvyqueucsqi(a1);
apppend_u_to_quiwqadzxvyqueucsqi(a1);
apppend_r_to_quiwqadzxvyqueucsqi(a1);
apppend_underscore_to_quiwqadzxvyqueucsqi(a1);
apppend_n_to_quiwqadzxvyqueucsqi(a1);
apppend_0_to_quiwqadzxvyqueucsqi(a1);
apppend_0_to_quiwqadzxvyqueucsqi(a1);
apppend_t_to_quiwqadzxvyqueucsqi(a1);
append_i_to_1hpsqszartypcxwxuiqj(a1);
append_space_to_1hpsqszartypcxwxuiqj(a1);
append_a_to_1hpsqszartypcxwxuiqj(a1);
append_m_to_1hpsqszartypcxwxuiqj(a1);
apppend_w_to_quiwqadzxvyqueucsqi(a1);
apppend_o_to_quiwqadzxvyqueucsqi(a1);
apppend_r_to_quiwqadzxvyqueucsqi(a1);
apppend_k_to_quiwqadzxvyqueucsqi(a1);
apppend_underscore_to_quiwqadzxvyqueucsqi(a1);
apppend_t_to_quiwqadzxvyqueucsqi(a1);
append_space_to_1hpsqszartypcxwxuiqj(a1);
append_a_to_1hpsqszartypcxwxuiqj(a1);
apppend_e_to_quiwqadzxvyqueucsqi(a1);
apppend_r_to_quiwqadzxvyqueucsqi(a1);
apppend_e_to_quiwqadzxvyqueucsqi(a1);
append_space_to_1hpsqszartypcxwxuiqj(a1);
append_h_to_1hpsqszartypcxwxuiqj(a1);
append_e_to_1hpsqszartypcxwxuiqj(a1);
append_a_to_1hpsqszartypcxwxuiqj(a1);
append_d_to_1hpsqszartypcxwxuiqj(a1);
append_e_to_1hpsqszartypcxwxuiqj(a1);
apppend_3_to_quiwqadzxvyqueucsqi(a1);
apppend_f_to_quiwqadzxvyqueucsqi(a1);
apppend_i_to_quiwqadzxvyqueucsqi(a1);
apppend_k_to_quiwqadzxvyqueucsqi(a1);
apppend_brace_close_to_quiwqadzxvyqueucsqi(a1);
append_r_to_1hpsqszartypcxwxuiqj(a1);
- Read appended chars per dict element and find
1hpsqszartypcxwxuiqj
beinglook at
andquiwqadzxvyqueucsqi
being flag number three:cApwN{1m_1n_ur_n00twork_tere3fik}
- (dict elements are later used in a “notify loser” routine as HTTP headers)S
Level 4
- Open
Payload/IntroLevels.app/IntroLevels
in IDA Pro - Find
doTheThing
, as referenced by task description - Like in the Android task, the method name and the objective C structures tell us that we should use the flags from Level 1 to Level 3 to get the Flag for Level 4
- Assume arguments to the functio
doTheThing
are the earlier flags - Hex-Rays indicates tells us that all flags are hashed using SHA1 to construct again a AES key.
- AES key is built up using array slices and the return value from
specialSauce
which is a constantbler
- Decrypt message at 0x435F0 using AES-CBC 128 with guessed IV all zeroes
- Python solution script:
#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Hash import SHA
import binascii
## read code in doTheThing
msg = bytes([
0xDD, 0x2A, 0x7A, 0xEC, 0xEE, 0x8B, 0x7D, 0xEC,
0x0E, 0x72, 0x33, 0xC7, 0x1B, 0xE3, 0xF7, 0x50,
0xFC, 0x4B, 0x7A, 0x85, 0x2C, 0xA0, 0xE1, 0x19,
0x7F, 0x54, 0x60, 0xD3, 0x16, 0x6D, 0x62, 0xFD,
])
flag1 = b"cApwN{y0u_are_th3_ch0sen_1}"
flag2 = b"cApwN{0mg_d0es_h3_pr4y}"
flag3 = b"cApwN{1m_1n_ur_n00twork_tere3fik}"
sha = SHA.new()
sha.update(flag1)
flag1 = sha.hexdigest().encode()
sha = SHA.new()
sha.update(flag2)
flag2 = sha.hexdigest().encode()
sha = SHA.new()
sha.update(flag3)
flag3 = sha.hexdigest().encode()
key = flag3[:5] + flag1[:4] + flag2[:5] + b"bler" + flag1[36:] + flag3[35:] + flag2[35:]
## is cac9ef88a55c22blerc69c9091161a12
print(key)
aes = AES.new(key, AES.MODE_CBC, b'\x00' * 0x10)
print(aes.decrypt(msg))
- Flag for level 4 is
cApwN{f0h_sw1zzle_my_n1zzle}
Level 5
- Unzip and open
Payload/Level5.app/Level5Demo
in IDA Pro - Find method
hammerTime
which does again some anti-dynamic-analysis trickery by checking whether “setmeinurkeychain” is set to “youdidathing” (addr 0xA2DC) - Find 4 Calls (at 0xA38C) to functions that perform RotateLeft operations on a custom implementation of bit arrays
- Check cross references of the bit array and find another function
getBit
(0x8740) accessing them - The
getBit
function is only being used by function0xBBA8
- This function calls
CGContextFillRect
andCGContextSetFillColorWithColor
- Most likely the bits are drawed on the screen at this point
- The bit array are groups of 15 bits, combine with the fact that the drawing code performs a modulo 3 operation to assume that the 15 bits are arranged in 3 x 5 rectangles either set to black or white
- The following python code visualizes the bits, that form indeed the flag
#!/usr/bin/env python3
import struct
dat = open('Payload/Level5.app/Level5Demo', 'rb').read()
ptrs = struct.unpack('<33I', dat[0x154d0:][:33 * 4])
bits = []
for ptr in ptrs:
tmp = []
for i in range(15):
x = struct.unpack('<I', dat[ptr + i * 4:][:4])[0]
tmp.append(x)
bits.append(tmp)
for j in range(33):
SHIFT = -1
for i in range(5):
for k in range(3):
x = bits[j][(i * 3 + k + SHIFT) % 15]
if x == 1:
print('X', end = '')
elif x == 0:
print(' ', end = '')
print('')
print('')
- Flag is
cApwN{i_guess_you_can_touch_this}
Level 6
- Unzip and open
Payload/Level6.app/Level6
in IDA Pro - Find
viewDidLoad
function ofLevel6ViewController
- Find function
0xA8A4
which basically builds up a huffman tree based on the frequencies of the letters contained in the hugeLorem Ipsum
string at0x142DC
- Be super happy that the only reference implementation of Huffman in Objective-C (https://rosettacode.org/wiki/Huffman_coding#Objective-C) is very close to what the binary does.
- Replace the string in the example implementation above to build up dictionary (python re-implementation resulted in different dictionary due to ambiguities in the huffman tree construction algorithm for elements with equal absolute frequencies) and get
{'000': 'i',
'0010': 'r',
'00110': 'd',
'001110': 'g',
'001111': ',',
'0100': 'l',
'010100': 'v',
'010101000': 'C',
'010101001': 'E',
'01010101': 'N',
'010101100': 'I',
'01010110100': 'X',
'01010110101': '0',
'0101011011': 'A',
'0101011100': 'F',
'01010111010': '9',
'01010111011': '1',
'01010111100': '6',
'01010111101': 'Y',
'01010111110': '_',
'01010111111': 'Z',
'01011': 'c',
'0110': 's',
'011100': 'b',
'01110100000': 'W',
'01110100001': 'H',
'01110100010': 'J',
'01110100011': '4',
'0111010010': 'L',
'0111010011': 'S',
'01110101000': '}',
'01110101001': '{',
'01110101010': 'y',
'01110101011': 'w',
'01110101100': '8',
'01110101101': 'K',
'01110101110': 'U',
'01110101111': 'T',
'01110110000': 'B',
'01110110001': '3',
'01110110010': '7',
'01110110011': '2',
'01110110100': 'z',
'01110110101': '5',
'01110110110': 'k',
'01110110111': 'G',
'01110111': 'D',
'011110': 'p',
'0111110': 'f',
'0111111': 'q',
'1000': 't',
'1001': 'a',
'101': ' ',
'1100': 'u',
'11010': 'm',
'11011': 'o',
'111000': '.',
'111001000': 'x',
'11100100100': 'R',
'11100100101': 'O',
'1110010011': 'P',
'111001010': 'V',
'1110010110': 'j',
'1110010111': 'Q',
'11100110': 'h',
'11100111': 'M',
'11101': 'n',
'1111': 'e'}
- Find
prepareForSegue
function in the binary that does some crypto operations starting at 0xA798 - A variant of salsa20 is again used to decrypt a secret with key at 0x154EC, iv at 0x154FC and secret message at 0x1550C
- Using Unicorn once again, we obtain the decoded secret as an ASCII-Bitstring
0101101010110110111100111010101101010101011101010010101011101111
0100101011111011010001001010111110110100111011000101110110001011
0011101100010111011000101110110110011001010111110010001010110101
0101011010101110110110010101111101001100001010111110110101111111
111111111111111111111111111111111111101110101000
- Huffman decompressing this bitstring gives us the flag:
#!/usr/bin/env python3
from unicorn import *
from unicorn.arm_const import *
from IPython import embed
import struct
from heapq import heappush, heappop, heapify, nlargest, nsmallest
from collections import defaultdict
import collections
from pprint import pprint
dat = open('Level6', 'rb').read()[:0x16000]
print(hex(len(dat)))
ADDRESS = 0
STACK = 0x60000000
INPUT = 0x50000000
def hook_block(uc, address, size, user_data):
print(">>> Tracing basic block at 0x%x, block size = 0x%x" %(address, size))
# callback for tracing instructions
#def hook_code(uc, address, size, user_data):
# print(">>> Tracing instruction at 0x%x, instruction size = 0x%x"
# %(address, size))
def hook_divsi3(uc, address, size, user_data):
r0 = uc.reg_read(UC_ARM_REG_R0)
r1 = uc.reg_read(UC_ARM_REG_R1)
uc.reg_write(UC_ARM_REG_R0, r0 // r1)
uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR))
def hook_memcpy(uc, address, size, user_data):
print(">>> Patching memcpy ...")
dat = uc.mem_read(uc.reg_read(UC_ARM_REG_R1), uc.reg_read(UC_ARM_REG_R2))
uc.mem_write(uc.reg_read(UC_ARM_REG_R0), bytes(dat))
uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR))
def hook_memset(uc, address, size, user_data):
print(">>> Patching memset ...")
dat = uc.reg_read(UC_ARM_REG_R1)
size = uc.reg_read(UC_ARM_REG_R2)
uc.mem_write(uc.reg_read(UC_ARM_REG_R0), bytes(dat * size))
uc.reg_write(UC_ARM_REG_PC, uc.reg_read(UC_ARM_REG_LR))
# Initialize emulator in ARM mode
mu = Uc(UC_ARCH_ARM, UC_MODE_ARM)
mu.mem_map(ADDRESS, len(dat))
mu.mem_map(STACK, 0x1000)
mu.mem_map(INPUT, 0x1000)
mu.mem_write(ADDRESS, dat)
mu.mem_write(STACK, b"\x00" * 0x1000)
mu.mem_write(INPUT, b"\x00" * 0x1000)
# initialize machine registers
#mu.reg_write(UC_ARM_REG_R4, 0x1550c)
mu.reg_write(UC_ARM_REG_SP, STACK + 0x800) ## input
mu.reg_write(UC_ARM_REG_APSR, 0xFFFFFFFF) #All application flags turned on
mu.mem_write(mu.reg_read(UC_ARM_REG_SP) + 0x58, struct.pack("<I", 0x1550c))
# tracing all basic blocks with customized callback
mu.hook_add(UC_HOOK_BLOCK, hook_block)
#mu.hook_add(UC_HOOK_CODE, hook_code)
mu.hook_add(UC_HOOK_CODE, hook_memset, begin = 0x10684, end = 0x10684)
mu.hook_add(UC_HOOK_CODE, hook_divsi3, begin = 0x10444, end = 0x10444)
# tracing one instruction at ADDRESS with customized callback
#mu.hook_add(UC_HOOK_CODE, hook_code, begin=ADDRESS, end=ADDRESS)
# emulate machine code in infinite time
mu.emu_start(0xa798 | 1, 0xa7c4)
secret = mu.mem_read(0x1550c, 0x130).decode()
huff = {
"000": "i",
"0010": "r",
"00110": "d",
"001110": "g",
"001111": ",",
"0100": "l",
"010100": "v",
"010101000": "C",
"010101001": "E",
"01010101": "N",
"010101100": "I",
"01010110100": "X",
"01010110101": "0",
"0101011011": "A",
"0101011100": "F",
"01010111010": "9",
"01010111011": "1",
"01010111100": "6",
"01010111101": "Y",
"01010111110": "_",
"01010111111": "Z",
"01011": "c",
"0110": "s",
"011100": "b",
"01110100000": "W",
"01110100001": "H",
"01110100010": "J",
"01110100011": "4",
"0111010010": "L",
"0111010011": "S",
"01110101000": "}",
"01110101001": "{",
"01110101010": "y",
"01110101011": "w",
"01110101100": "8",
"01110101101": "K",
"01110101110": "U",
"01110101111": "T",
"01110110000": "B",
"01110110001": "3",
"01110110010": "7",
"01110110011": "2",
"01110110100": "z",
"01110110101": "5",
"01110110110": "k",
"01110110111": "G",
"01110111": "D",
"011110": "p",
"0111110": "f",
"0111111": "q",
"1000": "t",
"1001": "a",
"101": " ",
"1100": "u",
"11010": "m",
"11011": "o",
"111000": ".",
"111001000": "x",
"11100100100": "R",
"11100100101": "O",
"1110010011": "P",
"111001010": "V",
"1110010110": "j",
"1110010111": "Q",
"11100110": "h",
"11100111": "M",
"11101": "n",
"1111": "e",
}
print(secret)
ptr = 0
pprint(huff)
while ptr < len(secret):
i = 0
found = False
for i in range(12):
if secret[ptr:][:i] in huff:
found = True
print(huff[secret[ptr:][:i]], end = '')
ptr += i
if not found:
print("({})".format(secret[ptr:][:i]), end = "")
ptr += 11
- Obtain the last flag:
cApwN{1m_mr_m33s33ks_l00k_at_meeeeeeeeeee}