Tags: reverse-engineering dalvik-bytecode apktool hashing base64 apk android
Rating:
# 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:
```bash
$ apktool d --no-src flagdroid.apk
$ cd flagdroid
```
We then convert the `classes.dex` with `dex2jar` to a jar to open it with `jd`:
```bash
$ 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
```java
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
<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.
```bash
$ 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`):
```java
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:
```java
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:
```java
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
```java
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:
```python
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
```java
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`:
```nasm
$ 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:
```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)}`