*For the full experience with images see the original blog post!*
The heavily Doctor Who-themed challenge Exterminate provides us with an apk file for some Android reversing.Looking at the decompilation (most online tools or IDEs should suffice) result I found a few simple classes including two activities.The main activity of the app is the `CountdownActivity` but is otherwise irrelevantsince it simply counts down from three until the extermination by the Daleks and effectively closes the app.The other activity however, in the `CodeActivity` class, contains a code input field, a button and a message field like a plain login flow.The button on-click listener is defined as an external synthetic lambda, suggesting a JNI call.We find corresponding libraries at the path `./resources/lib/<arch>/libexterminate.so` which we can analyze with the decompiler of our choice (in my case ghidra).
Exterminate CountdownActivity screenshot
By default, ghidra doesn't resolve JNI types and signatures well.I often used the [JNIAnalyzer plugin](https://github.com/Ayrx/JNIAnalyzer) by Ayrx which extracts function definitions from an apk file, includes a JNI headerand automatically updates the relevant function signatures.In this case it helps a little in identifying the JNI string parameter and return value.The `CodeActivity.getResponseCode()` function is fairly simple though, first checking whether the code is correct via `isCodeCorrect` and if so calling `getFlag(out, code)`.
The check method `isCodeCorrect` first checks that the length of the code is 8.In case your not familiar with the C++ basic_string struct ([the libc++ version](https://joellaity.com/2020/01/31/string.html)), it has a small version for 22 or less bytes and a version for longer strings.For the small version, the first bit of the first byte is set and additionally contains the length shifted left by one.The data pointer is The version for long strings has the length at offset 8 and the string data at offset `0x10`.While otherwise irrelevant for the check logic, it does clobber the decompilation result and makes it harder to read without the correct struct header.
isCodeCorrect main logic
The main logic of the check validates that the characters at index 1 and 2 are equal.Then it looks for the substring "on" (by checking every index that contains 'o', with `memchr`) and checks that index 6 contains '-'.Finally, the check rejects characters below or equal to '9'.With 4 valid positions for the substring "on" and assuming the other four characters are in limited printable range, this limits our search space to `4 * (128 - 0x3A)**4 = 96040000` valid codes but does not give us a definite result.
isCodeCorrect final check
The `getFlag` function is very simple: it concatenates the code with "geronimo" and then calls `decrypt` which decrypts two blocks with AES ECB mode and combines them to get the flag.Since we can check a key by checking the flag format, we can brute-force the code in the limited search space relatively easily.Depending on the available hardware, we can execute the brute force in under 20 minutes (full runtime around 30 minutes on my old laptop).I simply asked my teammates and we crowd sourced the problem, running one of the four cases on separate laptops in the background.
```pyfrom Crypto.Cipher import AESimport rich.progress
FIRST = bytes.fromhex("d66d203aadabea4f8e583745eff2b871")[::-1]SECOND = bytes.fromhex("3f97be164d0323d7c7e98af11a35c8a8")[::-1]SUFFIX = bytes.fromhex("6f6d696e6f726567")[::-1]
def test(key: bytes) -> bool:    cipher = AES.new(key, AES.MODE_ECB)    out = cipher.decrypt(SECOND)
    return b"CSR{" in out
def by(val: int):    return int.to_bytes(val, 1, "big")
# b"ABBCEF-G" with b"on" somewhere?!def brute_force() -> bytes:    for a in rich.progress.track(range(0x3A, 128)):        for b in range(0x3A, 128):            for c in range(0x3A, 128):                for d in range(0x3A, 128):                    key = b"onn" + by(a) + by(b) + by(c) + b"-" + by(d) + SUFFIX
                    if test(key):                        return key
    for a in rich.progress.track(range(0x3A, 128)):        for b in range(0x3A, 128):            for c in range(0x3A, 128):                for d in range(0x3A, 128):                    key = by(a) + b"oon" + by(b) + by(c) + b"-" + by(d) + SUFFIX                     if test(key):                        return key
    for a in rich.progress.track(range(0x3A, 128)):        for b in range(0x3A, 128):            for c in range(0x3A, 128):                for d in range(0x3A, 128):                    key = by(a) + by(b) + by(b) + b"on" + by(c) + b"-" + by(d) + SUFFIX                     if test(key):                        return key
    for a in rich.progress.track(range(0x3A, 128)):        for b in range(0x3A, 128):            for c in range(0x3A, 128):                for d in range(0x3A, 128):                    key = by(a) + by(b) + by(b) + by(c) + b"on" + b"-" + by(d) + SUFFIX                     if test(key):                        return key
KEY = brute_force()assert KEY == b"allons-ygeronimo"
cipher = AES.new(KEY, AES.MODE_ECB)out = cipher.decrypt(SECOND)out += cipher.decrypt(FIRST)
print(out)
```
The resulting key "allons-ygeronimo" is a combination of two popular exclamations of the doctor, hurray!
Funny enough, I had a pretty much complete solve script lying around for a few hours before finally solving this challenge.Turns out I simply mixed up the two decrypted blocks and accidentally checked the second block for the flag start, ouch!It did help to look at the problem again with fresh energy later though and we got flag ?