Reversing for fun thanks to intelligence service

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 \

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:

The challenge application running into the Android 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" 
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>

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
    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
// 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
// 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
    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
// 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
    const-string v0, "Incorrect!"
    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
    .packed-switch 0x0
    .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"?>
    <public type="string" name="action_header" id="0x7f11001b" />
    <public type="string" name="alphabet" id="0x7f11001d" />
    <public type="string" name="edit" id="0x7f110033" />
<?xml version="1.0" encoding="utf-8"?>
    <string name="action_header">!NFt8g7f_NOLtL</string>
    <string name="alphabet">ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890àé_{}</string>
    <string name="edit">8qROIjDcr1fdXUSAkFH5hà9QToé3xg6eKnVlmu2zpCb4GJEtiwWy70vLPsYBaNZM_{}</string>

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 😇