Capture The Flag, or CTF for short, always are a good opportunity to challenge its knowledge and capabilities about software security. This year again, the DGSE (Directorate General for External Security, the French foreign intelligence services) and Telecom SudParis school teamed up to create the 404 CFT: a free-to-join online CTF. I decided to give it a try to check how my reverse engineering skills aged.
I tackled the Android reverse engineering challenge. It was split in two parts: Bugdroid Fight [1/2]
, rated as easy I won’t detail here, and Bugdroid Fight [2/2]
rated as average difficulty (the second of the four difficulty levels before hard and extreme) I will walk through in this post. The goal for a CTF challenge is to find a flag: like an hidden text or a password. Running the application inside an emulator quickly gives how to find the flag: you have to find the text that validates the application form.
Getting the application to run
While an APK is provided for the first part of the challenge, the second part involves an AAB. APK (Android Package) is the usual format to distribute and install Android applications but Google moved away for Play Store distribution in favor of AAB: Android Application Bundle. An AAB bundles multiple APKs and their metadata in order to slice an Android application, and allow installing only the required parts for the users (like skipping languages unrelated to the device, or the unsupported screen resolution resources, …). The first step of the challenge is to convert the provided AAB using the Google bundletool
CLI into an APK to install:
$ java -jar bundletool-all-1.16.0.jar build-apks \
--bundle=Bugdroid_Fight_-_Part_2.aab \
--output=part2.apks
This generates an .apks
file, which is a zip file containing the multiple parts of the application into separate APK files, in particular the application logic into split/base-master.apk
. Extracting base-master.apk
and installing using adb install
allows to start the application into my emulator:
Decompiling and looking for the validation logic
The second step is to decompile the APK to check at the bytecode what is happening behind the form validation. I used the best friend of all Android reverse engineer: apktool
from Connor Tumbleson:
$ apktool d base-master.apk
This tool is able to extract a lot of information from a binary Android application, including: the application manifest, its resources, and the behavior logic as smali file, a textual representation of the Android virtual machine bytecode. The application manifest gives the activity started by the Android launcher (in which you must input the password): com.example.reverseexample.MainActivity
:
<activity android:exported="true"
android:label="@string/app_name"
android:name="com.example.reverseexample.MainActivity"
android:theme="@style/Theme.ReverseExample.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
Sadly, the code was obfuscated, and the logic is no more located near the activity (within the com.example.reverseexample
package). A quick search at the toast text Incorrect!
gives the location of the password validation logic: the method void r0.a.onClick(View)
(r0
being the package name, a
the class name, onClick
the method name after obfuscation). The onClick
method name usually denotes a button action event, and hints us on finding the right location.
Reversing the validation logic
Below is the content of the onClick
method, in smali format, that I annotated with some comments to describe the important parts and operations:
.method public final onClick(Landroid/view/View;)V
.locals 10
iget v0, p0, Lr0/a;->a:I
iget-object v1, p0, Lr0/a;->c:Ljava/lang/Object;
iget-object v2, p0, Lr0/a;->b:Ljava/lang/Object;
const/4 v3, 0x1
packed-switch v0, :pswitch_data_0
check-cast v2, LQ0/n;
check-cast v1, Landroid/view/View$OnClickListener;
sget-object v0, LQ0/n;->C:[I
invoke-virtual {v2}, Ljava/lang/Object;->getClass()Ljava/lang/Class;
invoke-interface {v1, p1}, Landroid/view/View$OnClickListener;->onClick(Landroid/view/View;)V
invoke-virtual {v2, v3}, LQ0/l;->a(I)V
return-void
:pswitch_0
check-cast v2, Lcom/example/reverseexample/ui/home/HomeFragment;
//
// Part 1 - get the input field text value
//
// Check that v1 is the input field
check-cast v1, Lcom/google/android/material/textfield/TextInputEditText;
sget p1, Lcom/example/reverseexample/ui/home/HomeFragment;->W:I
const-string p1, "this$0"
invoke-static {p1, v2}, LL0/c;->o(Ljava/lang/String;Ljava/lang/Object;)V
const-string p1, "$textInput"
invoke-static {p1, v1}, LL0/c;->o(Ljava/lang/String;Ljava/lang/Object;)V
invoke-virtual {v2}, LW/y;->i()Landroid/content/Context;
move-result-object p1
// Get the text of the input field and store it into v0
invoke-virtual {v1}, Lk/A;->getText()Landroid/text/Editable;
move-result-object v0
// Use String.valueOf() on input text and store it into v0
invoke-static {v0}, Ljava/lang/String;->valueOf(Ljava/lang/Object;)Ljava/lang/String;
move-result-object v0
invoke-virtual {v2}, LW/y;->G()Landroid/content/Context;
move-result-object v1
invoke-virtual {v1}, Landroid/content/Context;->getResources()Landroid/content/res/Resources;
move-result-object v1
//
// Part 2 - create a character mapping
//
// Get the ressource with ID 0x7f11001b into v1
const v4, 0x7f11001b
invoke-virtual {v1, v4}, Landroid/content/res/Resources;->getString(I)Ljava/lang/String;
move-result-object v1
const-string v4, "getString(...)"
invoke-static {v4, v1}, LL0/c;->n(Ljava/lang/String;Ljava/lang/Object;)V
new-instance v5, Ljava/util/HashMap;
// Create a new HashMap into v5
invoke-direct {v5}, Ljava/util/HashMap;-><init>()V
invoke-virtual {v2}, LW/y;->G()Landroid/content/Context;
move-result-object v6
invoke-virtual {v6}, Landroid/content/Context;->getResources()Landroid/content/res/Resources;
move-result-object v6
// Get the ressource with ID 0x7f11001d into v6
const v7, 0x7f11001d
invoke-virtual {v6, v7}, Landroid/content/res/Resources;->getString(I)Ljava/lang/String;
move-result-object v6
invoke-static {v4, v6}, LL0/c;->n(Ljava/lang/String;Ljava/lang/Object;)V
invoke-virtual {v2}, LW/y;->G()Landroid/content/Context;
move-result-object v2
invoke-virtual {v2}, Landroid/content/Context;->getResources()Landroid/content/res/Resources;
move-result-object v2
// Get the ressource with ID 0x7f110033 into v7
const v7, 0x7f110033
invoke-virtual {v2, v7}, Landroid/content/res/Resources;->getString(I)Ljava/lang/String;
move-result-object v2
invoke-static {v4, v2}, LL0/c;->n(Ljava/lang/String;Ljava/lang/Object;)V
const/4 v4, 0x0
move v7, v4
// First loop label
:goto_0
// First loop conditions:
// Check that v6 and v2 strings lengths are greater than v7 int
invoke-virtual {v6}, Ljava/lang/String;->length()I
move-result v8
if-ge v7, v8, :cond_0
invoke-virtual {v2}, Ljava/lang/String;->length()I
move-result v8
if-ge v7, v8, :cond_0
// First loop operations
// Takes v6 character at position v7
// Takes v2 character at position v7
// Add entry to the map: v6 char as key, v2 char as value
invoke-virtual {v6, v7}, Ljava/lang/String;->charAt(I)C
move-result v8
invoke-static {v8}, Ljava/lang/Character;->valueOf(C)Ljava/lang/Character;
move-result-object v8
invoke-virtual {v2, v7}, Ljava/lang/String;->charAt(I)C
move-result v9
invoke-static {v9}, Ljava/lang/Character;->valueOf(C)Ljava/lang/Character;
move-result-object v9
invoke-virtual {v5, v8, v9}, Ljava/util/HashMap;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
// Increment v7
add-int/lit8 v7, v7, 0x1
// Looping first loop
goto :goto_0
// End of first loop
:cond_0
//
// Part 3 - validate input text
//
// Create a new StringBuilder into v6
new-instance v2, Ljava/lang/StringBuilder;
invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v0}, Ljava/lang/String;->length()I
move-result v6
sub-int/2addr v6, v3
// Second loop label
:goto_1
const/4 v3, -0x1
// Second loop condition:
// v3 greater or equals to -1
if-ge v3, v6, :cond_1
// Get the char of the input text at index v6
invoke-virtual {v0, v6}, Ljava/lang/String;->charAt(I)C
move-result v3
invoke-static {v3}, Ljava/lang/Character;->valueOf(C)Ljava/lang/Character;
move-result-object v7
invoke-static {v3}, Ljava/lang/Character;->valueOf(C)Ljava/lang/Character;
move-result-object v3
// Get the mapped char into v3
invoke-virtual {v5, v7, v3}, Ljava/util/HashMap;->getOrDefault(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
move-result-object v3
check-cast v3, Ljava/lang/Character;
invoke-virtual {v3}, Ljava/lang/Character;->charValue()C
move-result v3
// Append the mapped char into the StringBuilder
invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(C)Ljava/lang/StringBuilder;
// Decrement v6
add-int/lit8 v6, v6, -0x1
// Looping second loop
goto :goto_1
// End of second loop
:cond_1
// Get the mapped input text result
invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
const-string v2, "toString(...)"
// Calls LL0/c on both v2 and the mapped input text
// LL0/c is just on obfuscated Object.equals() method
invoke-static {v2, v0}, LL0/c;->n(Ljava/lang/String;Ljava/lang/Object;)V
invoke-static {v0, v1}, LL0/c;->f(Ljava/lang/Object;Ljava/lang/Object;)Z
move-result v0
// Check if v2 and the mapped input text match
if-eqz v0, :cond_2
const-string v0, "Correct!"
goto :goto_2
:cond_2
const-string v0, "Incorrect!"
:goto_2
invoke-static {p1, v0, v4}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
move-result-object p1
invoke-virtual {p1}, Landroid/widget/Toast;->show()V
return-void
:pswitch_data_0
.packed-switch 0x0
:pswitch_0
.end packed-switch
.end method
The Part 1 describes how the event listener gets the input text. The Parts 2 creates a mapping table between two sets of characters both get from Android string resources: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890àé_{}"
with ID 0x0x7f11001d
named alphabet
, and "8qROIjDcr1fdXUSAkFH5hà9QToé3xg6eKnVlmu2zpCb4GJEtiwWy70vLPsYBaNZM_"
with ID 0x7f110033
named edit
. To get such resources values, you can have a look at the resources files res/values/public.xml
to get their names and types, and then to res/values/strings.xml
to get their values:
<?xml version="1.0" encoding="utf-8"?>
<resources>
…
<public type="string" name="action_header" id="0x7f11001b" />
…
<public type="string" name="alphabet" id="0x7f11001d" />
…
<public type="string" name="edit" id="0x7f110033" />
…
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources>
…
<string name="action_header">!NFt8g7f_NOLtL</string>
…
<string name="alphabet">ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890àé_{}</string>
…
<string name="edit">8qROIjDcr1fdXUSAkFH5hà9QToé3xg6eKnVlmu2zpCb4GJEtiwWy70vLPsYBaNZM_{}</string>
…
</resources>
By the end of part 2, the resulting mapping table between alphabet
, as key, and edit
, as value, was built into a Java Map:
0->N, 1->7, 2->0, 3->v, 4->L, 5->P, 6->s, 7->Y, 8->B, 9->a, A->8, B->q, C->R, D->O, E->I, F->j, G->D, H->c, I->r, J->1, K->f, L->d, M->X, N->U, O->S, P->A, Q->k, R->F, S->H, T->5, U->h, V->à, W->9, X->Q, Y->T, Z->o, _->_, à->Z, a->é, b->3, c->x, d->g, e->6, f->e, g->K, h->n, i->V, é->M, j->l, k->m, l->u, m->2, n->z, o->p, p->C, q->b, r->4, s->G, t->J, u->E, v->t, w->i, x->w, y->W, z->y, {->{, }->}
The part 3 takes the input text, maps it using the mapping table, and compares the output to the third resource string "!NFt8g7f_NOLtL"
with ID 0x7f11001b
(weirdly) named action_header
. There are some subtle steps in the mapping logic to take care. First, it is mapped in reverse order (index running through the input text is decreased and checked against -1
rather than the text string length). Secondly, the mapping table is incomplete, and can map using identity in case of missing correspondances (there is a call to Map.getOrDefault(Object, Object)
using the same value for both parameters, meaning if the character is not found into the mapping table, it will be mapped to itself).
Solving the challenge
The only step missing to obtain the flag is then to take the expected text "!NFt8g7f_NOLtL"
, reverse it, apply the reversed mapping table (the !
character being mapped with itself as it is missing from the table) and you will obtain the flag to validate the challenge.
I really enjoyed this kind of challenge, and reversing again brought back some fun memories. I always find entertaining to be on the other side of the software development but I sadly miss the time to practice more frequently. I can thank my wife for sleeping in this morning so I could spent the hour playing the CFT. Who knows, I might plan some days in advance for the next edition 😇