easy re
chall.apkReverse Engineering500Easyhack10{flag_here}1. Challenge Overview
This challenge provided an Android APK (chall.apk) and hinted that it was a warm-up reverse-engineering task. The objective was to recover the real flag in hack10{...} format and explicitly avoid stopping at the obvious fake flag bait.
The APK turned out to be a loader stub. Its visible classes.dex contained an XOR-obfuscated second-stage APK appended to the end of the DEX. After extracting that payload APK, the native library exposed a repeating 32-byte XOR key that produced the fake HACK10{e7c35ac8...} value. That value was only a decoy. The real flag was recovered by decrypting the bundled backup image artifact and reading the handwritten text revealed in the transformed image.
Username / password discovered during analysis:
Username: "" (empty)
Password: "" (empty)
2. Initial Reconnaissance
I first confirmed the APK format, listed the archive contents, and checked for obvious strings.
Command:
1pwd \&\& ls -la \&\& file chall.apkOutput:
1/home/nopalinto/Downloads/CTF Codex2total 92043drwxr-xr-x 6 nopalinto nopalinto 28672 Mar 27 14:18 .4drwxr-xr-x 14 nopalinto nopalinto 4096 Mar 27 14:18 ..5drwxrwxr-x 2 nopalinto nopalinto 4096 Feb 7 09:49 .codex6drwxrwxr-x 5 nopalinto nopalinto 4096 Feb 13 06:44 .venv7-rw-rw-r-- 1 nopalinto nopalinto 13734 Mar 27 14:11 TryN3rr0r_Boot2Root_Freshman-V2_-_root.md8-rw-rw-r-- 1 nopalinto nopalinto 21881 Mar 27 14:07 TryN3rr0r_Boot2Root_Freshman-V2_-_user.md9-rw-rw-r-- 1 nopalinto nopalinto 15231 Mar 27 10:08 TryN3rr0r_Reverse_Engineering_detonator.md10-rw-rw-r-- 1 nopalinto nopalinto 13948 Mar 27 10:45 TryN3rr0r_Web_Exploitation_Phantom_Relay.md11drwxrwxr-x 10 nopalinto nopalinto 4096 Mar 27 11:06 automotive_decoded12drwxrwxr-x 4 nopalinto nopalinto 4096 Mar 27 11:07 automotive_jadx13-rw-rw-r-- 1 nopalinto nopalinto 8967385 Mar 27 10:03 chall.apk14-rw-rw-r-- 1 nopalinto nopalinto 284430 Mar 27 10:03 detonator.exe15-rw-rw-r-- 1 nopalinto nopalinto 25931 Mar 27 10:38 phantom_favicon.ico16-rw-rw-r-- 1 nopalinto nopalinto 72 Mar 27 14:01 shell.php17-rw-rw-r-- 1 nopalinto nopalinto 275 Mar 27 10:06 solve_detonator.py18-rw-rw-r-- 1 nopalinto nopalinto 586 Mar 27 10:43 solve_phantom_relay.py19chall.apk: Android package (APK), with AndroidManifest.xmlCommand:
1sha256sum chall.apk \&\& unzip -l chall.apk | sed -n '1,120p'Output:
1e9d9cbe0587a1a7e12853f97a28ca5a59440724615aa67acac798a4d969b05f5 chall.apk2Archive: chall.apk3 Length Date Time Name4--------- ---------- ----- ----5 2720 1981-01-01 01:01 AndroidManifest.xml6 4432188 1981-01-01 01:01 lib/armeabi-v7a/libdummy.so7 123012 1981-01-01 01:01 res/drawable-hdpi-v4/ic_launcher.png8 644 1981-01-01 01:01 res/layout/activity_main.xml9 123012 1981-01-01 01:01 res/drawable-mdpi-v4/ic_launcher.png10 358168 1981-01-01 01:01 assets/background.txt11 123012 1981-01-01 01:01 res/drawable-xhdpi-v4/ic_launcher.png12 5995184 1981-01-01 01:01 lib/arm64-v8a/libdummy.so13 1880 1981-01-01 01:01 resources.arsc14 4470068 1981-01-01 01:01 lib/x86/libdummy.so15 123012 1981-01-01 01:01 res/drawable-xxhdpi-v4/ic_launcher.png16 297142 1981-01-01 01:01 assets/background.bkp17 5715592 1981-01-01 01:01 lib/x86_64/libdummy.so18 1725566 2026-03-27 04:37 classes.dex19 1514 2026-03-27 09:37 META-INF/TESTKEY.SF20 1722 2026-03-27 09:37 META-INF/TESTKEY.RSA21 1387 2026-03-27 09:37 META-INF/MANIFEST.MF22--------- -------23 23495823 17 filesCommand:
1strings -n 6 chall.apk | grep -iE 'hack10|HACK10|flag|pass|user|ctf' | sed -n '1,200p'Output:
1 <exif:UserComment>2 </exif:UserComment>3 <exif:UserComment>4 </exif:UserComment>5 <exif:UserComment>6 </exif:UserComment>7 <exif:UserComment>8 </exif:UserComment>The lack of an immediate plaintext flag and the presence of assets/background.txt, assets/background.bkp, and large dummy native libraries suggested a staged APK or decoy structure.
3. Analysis / Forensics Path
I decompiled the APK and inspected the visible loader classes.
Command:
1apktool d -f chall.apk -o easy_re_decoded23jadx -d /home/nopalinto/Downloads/CTF\\ Codex/easy_re_jadx /home/nopalinto/Downloads/CTF\\ Codex/chall.apkOutput:
1I: Using Apktool 2.7.0-dirty on chall.apk2I: Loading resource table...3I: Decoding AndroidManifest.xml with resources...4I: Loading resource table from file: /home/nopalinto/.local/share/apktool/framework/1.apk5I: Regular manifest package...6I: Decoding file-resources...7I: Decoding values */* XMLs...8I: Baksmaling classes.dex...9I: Copying assets and libs...10I: Copying unknown files...11I: Copying original files...12INFO - loading ...13INFO - processing ...14INFO - progress: 0 of 17 (0%)15INFO - doneThe decompiled loader showed the real trick:
ProxyApplication.readDexFileFromApk() read classes.dex
splitPayLoadFromDex() took the last 4 bytes as a big-endian payload length
It copied that trailing payload out of classes.dex
It decrypted the payload by XORing every byte with 0xFF
It wrote the result as payload.apk
The critical Java logic was:
1int readInt = in.readInt();2byte[] newdex = new byte[readInt];3System.arraycopy(apkdata, (ablen - 4) - readInt, newdex, 0, readInt);4byte[] newdex2 = proxyApplication.decrypt(newdex);I reproduced that extraction exactly from the command line.
Command:
1unzip -p chall.apk classes.dex > easy_re_classes.dex \&\& python3 - <<'PY'2from pathlib import Path3p = Path('easy_re_classes.dex').read_bytes()4print('classes.dex size:', len(p))5print('last 32 bytes:', p[-32:].hex())6print('payload_len_be:', int.from_bytes(p[-4:], 'big'))7print('payload_len_le:', int.from_bytes(p[-4:], 'little'))8PYOutput:
1classes.dex size: 17255662last 32 bytes: baacabd1b2b9afb4faf9fffffffff0fff0ffccfbffff46e7e5ffffff001a1d023payload_len_be: 17113624payload_len_le: 35461632Command:
1python3 - <<'PY'2from pathlib import Path3p = Path('easy_re_classes.dex').read_bytes()4plen = int.from_bytes(p[-4:], 'big')5print('dex_len', len(p), 'payload_len', plen)6payload = bytes(b ^ 0xff for b in p[-4-plen:-4])7Path('easy_re_payload.apk').write_bytes(payload)8print('wrote', len(payload), 'bytes to easy_re_payload.apk')9print(payload[:8].hex())10PY11file easy_re_payload.apk12unzip -l easy_re_payload.apk | sed -n '1,120p'Output:
1dex_len 1725566 payload_len 17113622wrote 1711362 bytes to easy_re_payload.apk3504b0304000000004easy_re_payload.apk: Android package (APK), with zipflinger virtual entry, with APK Signing Block5Archive: easy_re_payload.apk6 Length Date Time Name7--------- ---------- ----- ----8 2436 1981-01-01 01:01 AndroidManifest.xml9 597076 1981-01-01 01:01 lib/armeabi-v7a/libnative-lib.so10 949400 1981-01-01 01:01 lib/arm64-v8a/libnative-lib.so11 123012 1981-01-01 01:01 res/drawable-hdpi-v4/ic_launcher.png12 668 1981-01-01 01:01 res/layout/activity_main.xml13 123012 1981-01-01 01:01 res/drawable-mdpi-v4/ic_launcher.png14 926336 1981-01-01 01:01 lib/x86_64/libnative-lib.so15 123012 1981-01-01 01:01 res/drawable-xhdpi-v4/ic_launcher.png16 1352 1981-01-01 01:01 META-INF/CERT.SF17 1167 1981-01-01 01:01 META-INF/CERT.RSA18 1872 1981-01-01 01:01 resources.arsc19 16920 1981-01-01 01:01 classes.dex20 123012 1981-01-01 01:01 res/drawable-xxhdpi-v4/ic_launcher.png21 853404 1981-01-01 01:01 lib/x86/libnative-lib.so22 1278 1981-01-01 01:01 META-INF/MANIFEST.MF23--------- -------24 3843957 15 filesAfter decompiling easy_re_payload.apk, I found the real app. MainActivity contained a broken login check:
1if ((!username.isEmpty() || !password.isEmpty()) \&\& username.equals(password)) {2 Toast.makeText(this, "Login Failed: Password cannot be the same as Username!", 1).show();3 return;4}5String username_md5 = md5(username);6String password_md5 = md5(password);7if (!username_md5.equals(password_md5)) {8 Toast.makeText(this, "Login Failed: Incorrect" + username_md5 + " or " + password_md5, 1).show();9 return;10}That means empty username and empty password both work:
They do not trigger the username.equals(password) rejection, because the left side of the \&\& is false when both strings are empty.
md5("") == md5(""), so the MD5 comparison passes.
This gave:
Username: empty string
Password: empty string
The native library was another decoy layer. It exported:
1nm -D easy_re_payload_decoded/lib/x86_64/libnative-lib.so | sed -n '1,200p'Relevant output:
10000000000067740 T Java_com_example_myapk_ImageEncryptor_encryptDataNative200000000000670e0 T Java_com_example_myapk_ImageEncryptor_getEncryptionKey300000000000663c0 T _Z14generateKeyBufvThe encryptDataNative function XORed image bytes with a generated repeating 32-byte key. When I XORed the bundled background.bkp against the decoded background JPEG, I recovered the exact fake value the challenge warned about:
Command:
1python3 - <<'PY'2from pathlib import Path3jpg = Path('easy_re_background.jpg').read_bytes()4bkp = Path('easy_re_decoded/assets/background.bkp').read_bytes()5x = bytes(a^b for a,b in zip(jpg,bkp))6print('xor_len', len(x))7print('xor_first_32_hex', x[:32].hex())8print('xor_first_64_hex', x[:64].hex())9PYOutput:
1xor_len 2686052xor_first_32_hex e7c35ac886acdbfe24cf7b7a68883cae27b5d67f2033f785f7b7349a29f32f9a3xor_first_64_hex e7c35ac886acdbfe24cf7b7a68883cae27b5d67f2033f785f7b7349a29f32f9ae7c35ac886acdbfe24cf7b7a68883cae27b5d67f2033f785f7b7349a29f32f9aThat proves the fake HACK10{e7c35ac8...} value was only the repeating XOR key serialized as hex, not the flag.
The real clue came from decrypting the backup image artifact and viewing the resulting transformed image, which revealed a handwritten flag.
4. Exploitation / Recovery
The exact command chain I used to recover the real flag was:
1unzip -p chall.apk classes.dex > easy_re_classes.dex2python3 - <<'PY'3from pathlib import Path4p = Path('easy_re_classes.dex').read_bytes()5plen = int.from_bytes(p[-4:], 'big')6payload = bytes(b ^ 0xff for b in p[-4-plen:-4])7Path('easy_re_payload.apk').write_bytes(payload)8PY9apktool d -f easy_re_payload.apk -o easy_re_payload_decoded10jadx -d /home/nopalinto/Downloads/CTF\\ Codex/easy_re_payload_jadx /home/nopalinto/Downloads/CTF\\ Codex/easy_re_payload.apk11python3 - <<'PY'12from pathlib import Path13import base6414s = Path('easy_re_decoded/assets/background.txt').read_text().strip()15if s.startswith('url(data:image/jpeg;base64,'):16 s = s[len('url(data:image/jpeg;base64,'):]17if s.endswith(')'):18 s = s[:-1]19b = base64.b64decode(s)20Path('easy_re_background.jpg').write_bytes(b)21PY22python3 - <<'PY'23from pathlib import Path24bkp = Path('easy_re_decoded/assets/background.bkp').read_bytes()25key = bytes.fromhex('e7c35ac886acdbfe24cf7b7a68883cae27b5d67f2033f785f7b7349a29f32f9a')26out = bytes(b ^ key[i % len(key)] for i, b in enumerate(bkp))27Path('easy_re_bkp_decrypted.bin').write_bytes(out)28PY29python3 - <<'PY'30from PIL import Image31img = Image.open('easy_re_bkp_decrypted.bin')32img.save('easy_re_bkp_decrypted.png')33print(img.size, img.mode)34PYLiteral output of the final image conversion step:
1(1080, 1920) RGBAfter opening easy_re_bkp_decrypted.png, the handwritten text in the image spelled the real flag:
Additional supporting command used to validate the decoded background source:
1python3 - <<'PY'2from pathlib import Path3import base644s = Path('easy_re_decoded/assets/background.txt').read_text().strip()5if s.startswith('url(data:image/jpeg;base64,'): s = s[len('url(data:image/jpeg;base64,'):]6if s.endswith(')'): s = s[:-1]7b = base64.b64decode(s)8Path('easy_re_background.jpg').write_bytes(b)9print('decoded jpg bytes', len(b))10print('jpg magic', b[:8].hex())11print('bkp bytes', len(Path('easy_re_decoded/assets/background.bkp').read_bytes()))12PY13file easy_re_background.jpgOutput:
1decoded jpg bytes 2686052jpg magic ffd8ffe10ffe45783bkp bytes 2971424easy_re_background.jpg: JPEG image data, Exif standard: [TIFF image data, big-endian, direntries=6, orientation=upper-left, xresolution=86, yresolution=94, resolutionunit=2], baseline, precision 8, 1080x1920, components 3The key lesson from the recovery path is that the visible hex string was only the XOR key, and the actual flag was embedded in the image produced from decrypting the pre-bundled backup artifact.
5. Flag
6. Summary of Approach & Key Takeaways
Step-by-step recap:
Identified chall.apk as an Android APK with suspicious assets and large dummy native libraries.
Decompiled the visible APK and found ProxyApplication, which unpacked a second-stage payload from the end of classes.dex.
Reproduced the loader logic manually and extracted easy_re_payload.apk by XORing the appended blob with 0xFF.
Decompiled the payload APK and analyzed MainActivity plus the JNI-backed ImageEncryptor.
Determined that empty username and empty password bypassed the broken login logic.
Recovered the native repeating XOR key and verified that it produced the known fake HACK10{e7c35ac8...} bait.
Decrypted assets/background.bkp with that key and opened the transformed image.
Read the actual handwritten flag from the recovered image and verified the final value.
Key takeaways:
A plaintext flag-like value in an APK is not automatically the real flag.
Loader stubs and appended payloads are common Android reversing patterns.
A broken validation condition can make “credentials” trivial even when the visible logic looks impossible.
Native encryption output should be validated against the surrounding artifact flow before treating derived values as flags.
Boot2Root Category
===