N
Back to writeup archive
Hack@10 International Capture The Flag Pre-Eliminary Round 2026

easy re

APK and binary resource inspection using jadx, apktool, and Python helpers.

Reverse EngineeringMarkdown + PDFEasysolved3 minread·762words
route
/TryN3rr0r/writeups/hack10-pre-eliminary-2026/easy-re
archive index
9 / 15
read mode
parsed markdown
Writeup loaded.

easy re

challenge brief
target profile, scoring, and execution stack
verified solve
target
chall.apk
category
Reverse Engineering
points
500
difficulty
Easy
flag format
hack10{flag_here}
tools used
filesha256sumunzipstringsapktooljadxgrepsedpython3objdumpnmexiftoolxxdPillowtesseractview_image
final flag
hack10{t3r_ez_X0r}
reveal and copy from section 5

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)

PDF evidence frame 01

2. Initial Reconnaissance

I first confirmed the APK format, listed the archive contents, and checked for obvious strings.

Command:

bash1 lines
1pwd \&\& ls -la \&\& file chall.apk

Output:

bash19 lines
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.xml

Command:

bash1 lines
1sha256sum chall.apk \&\& unzip -l chall.apk | sed -n '1,120p'

Output:

bash23 lines
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 files

Command:

bash1 lines
1strings -n 6 chall.apk | grep -iE 'hack10|HACK10|flag|pass|user|ctf' | sed -n '1,200p'

Output:

bash8 lines
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.

PDF evidence frame 02

3. Analysis / Forensics Path

I decompiled the APK and inspected the visible loader classes.

Command:

bash3 lines
1apktool d -f chall.apk -o easy_re_decoded23jadx -d /home/nopalinto/Downloads/CTF\\ Codex/easy_re_jadx /home/nopalinto/Downloads/CTF\\ Codex/chall.apk

Output:

bash15 lines
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  - done

The 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:

java4 lines
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:

bash8 lines
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'))8PY

Output:

bash4 lines
1classes.dex size: 17255662last 32 bytes: baacabd1b2b9afb4faf9fffffffff0fff0ffccfbffff46e7e5ffffff001a1d023payload_len_be: 17113624payload_len_le: 35461632

Command:

bash12 lines
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:

bash24 lines
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 files

After decompiling easy_re_payload.apk, I found the real app. MainActivity contained a broken login check:

java10 lines
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:

bash1 lines
1nm -D easy_re_payload_decoded/lib/x86_64/libnative-lib.so | sed -n '1,200p'

Relevant output:

bash3 lines
10000000000067740 T Java_com_example_myapk_ImageEncryptor_encryptDataNative200000000000670e0 T Java_com_example_myapk_ImageEncryptor_getEncryptionKey300000000000663c0 T _Z14generateKeyBufv

The 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:

bash9 lines
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())9PY

Output:

bash3 lines
1xor_len 2686052xor_first_32_hex e7c35ac886acdbfe24cf7b7a68883cae27b5d67f2033f785f7b7349a29f32f9a3xor_first_64_hex e7c35ac886acdbfe24cf7b7a68883cae27b5d67f2033f785f7b7349a29f32f9ae7c35ac886acdbfe24cf7b7a68883cae27b5d67f2033f785f7b7349a29f32f9a

That 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:

bash34 lines
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)34PY

Literal output of the final image conversion step:

bash1 lines
1(1080, 1920) RGB

After opening easy_re_bkp_decrypted.png, the handwritten text in the image spelled the real flag:

captured flag
hack10{t3r_ez_X0r}

Additional supporting command used to validate the decoded background source:

bash13 lines
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.jpg

Output:

bash4 lines
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 3

The 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

captured flag
hack10{t3r_ez_X0r}

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

===