Skip to content

Latest commit

 

History

History
398 lines (276 loc) · 9.77 KB

File metadata and controls

398 lines (276 loc) · 9.77 KB

flagdroid

Task

Categories: rev

Difficulty: easy

This app won't let me in without a secret message. Can you do me a favor and find out what it is?

File: fragdroid.zip

Solution

We use apktool to unpack the apk from the zip:

$ apktool d --no-src flagdroid.apk
$ cd flagdroid

We then convert the classes.dex with dex2jar to a jar to open it with jd:

$ d2j-dex2jar.sh -d classes.dex

We look at the lu.hack.Flagdroid.MainActivity.class:

  • a libary is loaded native-lib
  • the onCreate method matches the text of an EditText to a flag pattern
  • if a match is found the flag content is split on underscores
  • if there are 4 parts each part is checked with a checkSplit[1-4] function

The challenge is now to reverse engineer each flag part according to the check function.

Part 1

private boolean checkSplit1(String paramString) {
  byte[] arrayOfByte = Base64.decode(getResources().getString(2131492894), 0);
  try {
    return (new String(arrayOfByte, "UTF-8")).equals(paramString);
  } catch (UnsupportedEncodingException unsupportedEncodingException) {
    return false;
  }
}

A string resource is loaded and base64 decoded. This is the flag part.

We take a look at: https://developer.android.com/guide/topics/resources/providing-resources

The table lists the directory values and strings.xml for string values.

We therefore open res/values/strings.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  ...
  <string name="encoded">dEg0VA==</string>
  ...
</resources>

First part: tH4T

Part 2

This one failed to decompile, so we need to reconstruct the opcodes to java. We could also do this with the bytecode from jd, but I prefer the smali files apktool produces when also decompiling the sources.

$ apktool d --no-res -o sources flagdroid.apk
$ cd sources/smali/lu/hack/Flagdroid/

We can now take a look at the MainActivity.smali:

.method private checkSplit2(Ljava/lang/String;)Z
    .locals 7

    const-string v0, "\u001fTT:\u001f5\u00f1HG"

    const/4 v1, 0x0

    .line 131
    :try_start_0
    invoke-virtual {p1}, Ljava/lang/String;->toCharArray()[C

    move-result-object p1

    const-string v2, "hack.lu20"

    const-string v3, "UTF-8"

    .line 132
    invoke-virtual {v2, v3}, Ljava/lang/String;->getBytes(Ljava/lang/String;)[B

    move-result-object v2

    .line 134
    array-length v3, p1

    const/16 v4, 0x9

    if-eq v3, v4, :cond_0

    return v1

    :cond_0
    const/4 v3, 0x0

    :goto_0
    if-ge v3, v4, :cond_1

    .line 140
    aget-char v5, p1, v3

    add-int/2addr v5, v3

    int-to-char v5, v5

    aput-char v5, p1, v3

    .line 141
    aget-char v5, p1, v3

    aget-byte v6, v2, v3

    xor-int/2addr v5, v6

    int-to-char v5, v5

    aput-char v5, p1, v3

    add-int/lit8 v3, v3, 0x1

    goto :goto_0

    .line 143
    :cond_1
    invoke-static {p1}, Ljava/lang/String;->valueOf([C)Ljava/lang/String;

    move-result-object p1

    invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

    move-result p1
    :try_end_0
    .catch Ljava/io/UnsupportedEncodingException; {:try_start_0 .. :try_end_0} :catch_0

    return p1

    :catch_0
    return v1
.end method

This might be helpful: http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html

After some time we get (I left out the try-catch to keep it simple, it is also just a return of v1b):

private static boolean checkSplit2(String p1s) throws UnsupportedEncodingException {
   // const-string v0, "\u001fTT:\u001f5\u00f1HG"
   String v0s = "\u001fTT:\u001f5\u00f1HG";

   // const/4 v1, 0x0
   boolean v1b = false;

   // invoke-virtual {p1}, Ljava/lang/String;->toCharArray()[C
   // move-result-object p1
   char[] p1ca = p1s.toCharArray();

   // const-string v2, "hack.lu20"
   String v2s = "hack.lu20";

   // const-string v3, "UTF-8"
   String v3s = "UTF-8";

   // invoke-virtual {v2, v3}, Ljava/lang/String;->getBytes(Ljava/lang/String;)[B
   // move-result-object v2
   byte[] v2ba = v2s.getBytes(v3s);

   // array-length v3, p1
   int v3i = p1ca.length;

   // const/16 v4, 0x9
   int v4i = 0x9;

   // if-eq v3, v4, :cond_0
   if (v3i == v4i) {
       // :cond_0

       // const/4 v3, 0x0
       v3i = 0;

       // if-ge v3, v4, :cond_1
       while (v3i < v4i) {
           // aget-char v5, p1, v3
           int v5i = p1ca[v3i];

           // add-int/2addr v5, v3
           v5i += v3i;

           // int-to-char v5, v5
           char v5c = (char) v5i;

           // aput-char v5, p1, v3
           p1ca[v3i] = v5c;

           // aget-char v5, p1, v3
           v5i = p1ca[v3i];

           // aget-byte v6, v2, v3
           byte v6b = v2ba[v3i];

           // xor-int/2addr v5, v6
           v5i = v5i ^ v6b;

           // int-to-char v5, v5
           v5c = (char) v5i;

           // aput-char v5, p1, v3
           p1ca[v3i] = v5c;

           // add-int/lit8 v3, v3, 0x1
           v3i = v3i + 1;
       }

       // :cond_1

       // invoke-static {p1}, Ljava/lang/String;->valueOf([C)Ljava/lang/String;
       // move-result-object p1
       p1s = String.valueOf(p1ca);

       // invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
       // move-result p1
       boolean p1b = p1s.equals(v0s);

       return p1b;
   } else {
       return v1b;
   }
}

That looks complicated. Let's break it down:

private static boolean checkSplit2(String paramString) {
        String expected = "\u001fTT:\u001f5\u00f1HG";
        boolean equals = false;

        char[] charArray = paramString.toCharArray();
        byte[] byteArray = "hack.lu20".getBytes(StandardCharsets.UTF_8);

        if (charArray.length == 9) {
            for (int i = 0; i < charArray.length; i++) {
                charArray[i] = (char) ((charArray[i] + i) ^ byteArray[i]);
            }

            equals = String.valueOf(charArray).equals(expected);
        }

        return equals;
    }

Now we need a function to reverse this:

private static void reverseSplit2() {
    String res = "\u001fTT:\u001f5\u00f1HG";
    byte[] xor = "hack.lu20".getBytes(StandardCharsets.UTF_8);
    char[] inp = res.toCharArray();

    for (int i = 0; i < 9; i++) {
        System.out.print((char) ((inp[i] ^ xor[i]) - i));
    }

    System.out.println();
}

Second part: w45N-T~so

Part 3

private boolean checkSplit3(String paramString) {
  paramString = paramString.toLowerCase();
  return (paramString.length() != 8) ? false : (!paramString.substring(0, 4).equals("h4rd") ? false : md5(paramString).equals("6d90ca30c5de200fe9f671abb2dd704e"));
}

What we know:

  • length has to be 8.
  • first 4 characters ar h4rd
  • md5(string) is 6d90ca30c5de200fe9f671abb2dd704e

We now write a script that bruteforces the last 4 characters:

from hashlib import md5

chars = [x.encode("utf-8") for x in "abcdefghijklmnopqrstuvwxyz1234567890-~!?"]
solve = b'h4rd'
search = "6d90ca30c5de200fe9f671abb2dd704e"

def addChar(s, length=8):
 if len(s) < length:
   for char in chars:
     addChar(s + char)
 else:
   hexdigest = md5(s).hexdigest()
   if hexdigest == search:
     print(s.decode("utf-8"))

addChar(solve)

You could also use the full ascii range or more special characters, I assumed this chars from the other flag parts to speed up the process. Also no uppercase is needed because the paramString is converted to lowercase before hashing.

Third part: h4rd~huh

Part 4

private boolean checkSplit4(String paramString) {
  return paramString.equals(stringFromJNI());
}

public native String stringFromJNI();

We need to look at the native-lib now. It is located under lib/${arch}/libnative-lib.so:

$ r2 -AA x86_64/libnative-lib.so
[0x000005b0]> afl
0x000005b0    1 12           entry0
0x00000610    1 57           sym.Java_lu_hack_Flagdroid_MainActivity_stringFromJNI
0x000005a0    1 6            sym.imp.malloc
0x00000590    1 6            sym.imp.__cxa_atexit
0x00000580    1 6            sym.imp.__cxa_finalize
0x000005d0    2 21   -> 6    entry.fini0
[0x000005b0]> pdf @ sym.Java_lu_hack_Flagdroid_MainActivity_stringFromJNI
57: sym.Java_lu_hack_Flagdroid_MainActivity_stringFromJNI (int64_t arg1);
           ; arg int64_t arg1 @ rdi
0x00000610      53             push rbx
0x00000611      4889fb         mov rbx, rdi                ; arg1
0x00000614      bf0d000000     mov edi, 0xd                ; size_t size
0x00000619      e882ffffff     call sym.imp.malloc         ;  void *malloc(size_t size)
0x0000061e      c6400874       mov byte [rax + 8], 0x74    ; 't'
                                                                      ; [0x74:1]=0
0x00000622      c740093f3829.  mov dword [rax + 9], 0x29383f ; '?8)'
                                                                      ; [0x29383f:4]=-1
0x00000629      48b930727e77.  movabs rcx, 0x312d5334777e7230 ; '0r~w4S-1'
0x00000633      488908         mov qword [rax], rcx
0x00000636      488b0b         mov rcx, qword [rbx]
0x00000639      488b89380500.  mov rcx, qword [rcx + 0x538]
0x00000640      4889df         mov rdi, rbx
0x00000643      4889c6         mov rsi, rax
0x00000646      5b             pop rbx
0x00000647      ffe1           jmp rcx

We need to find the final value of rax here.

rcx = 0x312d5334777e7230

rax + 8 = 0x74

rax + 9 = 0x29383f

After mov qword [rax], rcx:

rax = 0x29383f74312d5334777e7230

Using python:

>>> import binascii
>>> binascii.unhexlify("29383f74312d5334777e7230").decode("utf-8")[::-1]
'0r~w4S-1t?8)'

Fourth part: 0r~w4S-1t?8)

Flag

We still need to join the parts with underscores and surround them with flag{}:

flag{tH4T_w45N-T~so_h4rd~huh_0r~w4S-1t?8)}