CyberSpaceKenya CTF: ZuluMeats3
Summary⌗
I recently worked on a CTF challenge by CyberSpace Kenya which I ended up winning. This is a writeup on how I solved the challenge. We were provided with an android app to reverse engineer and submit 3 flags.
Flag 3⌗
I haven’t done tons of mobile app reverse engineering but as I was learning about the subject in previous challenges I found this 2-step approach to work well:
- Extracting the apk contents with apktool
- Reversing the app code with jadx-gui
Using apktool is pretty simple just run this command on the apk:
> apktool d cyberspace-ctf.apk
This generates files in a folder cyberspace-ctf as below:
.
├── AndroidManifest.xml
├── apktool.yml
├── assets
├── lib
├── original
├── res
├── smali
└── unknown
Checking the AndroidManifest.xml and res/values/strings.xml doesn’t reveal any flag. Usually grepping recursively through the apk files for interesting names would hint at something. After a few tries with the words flag, flag1, flag2, flag3 I got some interesting output.
Without much thought I opened jadx to find more mentions of that ValidateFlag3 and found the code.
Reading through the code I was able to figure out that the validate function does the following:
- takes our input
- xor encrypts it with a key(which is generated by getKey() function)
- base64 encodes it
- checks if it is equal to the hardcoded flag3.
From previous experience I know that with xor encryption if you have the key and the encrypted data getting the plain text output is simple. Let me illustrate:
encrypted = key ^ plaintext
plaintext = key ^ encrypted
Getting the code to work in java isn’t as easy so I decided to use python and script the solution.
import base64
key1 = "hUZAf9gFIARRFTAvKRDs8A=="
key2 = "gp9jPELztMsd51Ih81gO0Q=="
flag3 = "RJ9qOOqa7pAinT19vyuQRHOGSi3FnPW5La0BYb4tnw=="
data1 = base64.b64decode(key1)
data2 = base64.b64decode(key2)
key = []
for i in range(len(data2)):
key.append(data1[i] ^ data2[i])
decodedflag = []
input = base64.b64decode(flag3)
encryptedFlag = input
for i in range(len(encryptedFlag)):
decodedflag.append(encryptedFlag[i] ^ key[i % len(key)])
b = [chr(s) for s in decodedflag]
print("".join(b))
Flag 3: CFI{plz_no_secret_in_java_code}
Since I found flag3 first I must have missed some stuff.
Flag 1⌗
Refering back to my earlier screenshot, after performing the grep there’s some base64 encoded data which I should have been more keen to look at!
base64:
Q1NfS0V7YmFzZTY0X2VuY29kaW5nX2lzX25vdF90aGF0X3NlY3VyZX0=
Flag 1: CS_KE{base64_encoding_is_not_that_secure}
Flag 2⌗
Now flag 2 was the last to find. Since most of the clues we have been finding were in that assets/index.android.bundle
file let me have a look at it.
Something I noted was that jadx didn’t find this file, that why the approach to use two tools is good IMO. A friend of mine has pointed out that it was actually there under Resources/assets on the jadx ui, nevertheless using two or more tools for analysis is still a good practice IMO. There is a lot of code in this file that is not easily readable, so I just googled what this file is.
I found that this file contains minified-javascript, it is created when an android app is developed using react. We already saw mentions of react when reading the code in jadx.
Reading the code in it’s minified form isn’t easy so js-beautifier to the rescue. I then opened the readable version in vs code and searched for the validateflag function again.
I landed here and we can clearly see the function that contained the base64 and the the other one that called flag3, due to their order flag 2 is what is left.
I took the same approach to porting the code to python as it is easier for me to work with. I had to do a bit of googling to understand the js flow but came up with this:
def f(n):
t = 2
r = 0
e = []
for r in range(len(n)):
if 1 & r:
t += 13
else:
t -= len(n) % 9
u = ord(n[r]) + t
e.append(str(u))
return e
At the end, the output of the function is checked if it is equal to that list of numbers which is a hex representation of characters. Instead of trying to undo the transformation that was going on in the function where they add 13… which I tried for a few hours and failed, better to bruteforce all possibilities.
I came up with this script for that:
import string
k = [64,80,78,141,124,124,123,151,144,141,134,166,146,158,148,172,158,192,166,197,176,204,190,210,209,201,206,229,204,232,228,246,220,253,234,245,258,268,250,262,282]
all = string.printable
a = "a"*41
flag = [a[i] for i in range(len(a))]
def f(n):
t = 2
r = 0
e = []
for r in range(len(n)):
if 1 & r:
t += 13
else:
t -= len(n) % 9
u = ord(n[r]) + t
e.append(str(u))
return e
for i in range(len(flag)):
for j in range(len(all)):
flag[i] = all[j]
out = f(flag)
if out[i] == str(k[i]):
break
print("".join(flag))
Flag 2: CFI{obfuscated_javascript_is_not_secured}
Conclusion⌗
This was a good ctf for practising some code review. I also had never played around with an app made with react so that was something I learnt.
I’d like to thank the CyberSpace Kenya team for putting up the challenge as well as ZuluMeats for the prize.
You can find more challenges on their website: https://ctf.cyberspace.co.ke
For any questions you can reach me on Twitter.
Twitter: ikuamike
Github: ikuamike