# Write-up CyberEDU ## Mobile ### tinygame Chall [tinygame](https://app.cyber-edu.co/challenges/9fcbff8d-0037-4316-938b-0eb6b4b74350?tenant=cyberedu) is a challenge from [D-CTF2025](https://app.cyber-edu.co/challenges?tenant=cyberedu&sortBy=newest). ![image](https://hackmd.io/_uploads/rktsnSIDZx.png) It splits the flag into two parts, and you have to find both to put them back together. PartB Drop the file into jadx ![image](https://hackmd.io/_uploads/rk7xaBIDWl.png) `MainActivity`: The logic code found is not here. The application uses `Engine.findAndBuildLoader` to load a class named `ui.UiFactory` at runtime. ![image](https://hackmd.io/_uploads/H1AWaHIw-g.png) `Engine.java`: Noticed the presence of the native library `libtinymedia.so` and native functions. Specifically, the `getFlagPartB()` function. ![image](https://hackmd.io/_uploads/BkdnTB8DZg.png) Decomplie it using apktool. And drop `/lib/arm64-v8a/libtinymedia.so` into IDA pro. ```C++ __int64 __fastcall Java_com_tinygame_app_Engine_getFlagPartB(__int64 a1) { const void *v1; // x19 size_t v2; // x20 __int64 v3; // x22 __int128 *v4; // x24 __int64 v5; // x25 __int64 v6; // x28 char *v7; // x21 __int64 v8; // x0 __int64 v9; // x20 bool v10; // zf unsigned __int64 v11; // x0 char v12; // w27 __int64 v13; // x0 void *v14; // x21 __int64 v15; // x0 __int64 v16; // x21 __int64 v17; // x23 __int64 v18; // x1 size_t v19; // x23 __int64 v20; // x0 __int64 v21; // x23 __int64 v22; // x1 __int64 v23; // x0 __int64 v24; // x29 __int64 v25; // x1 __int64 v26; // x21 __int64 v28; // x0 __int64 v29; // x24 __int64 v30; // x1 __int64 v31; // x1 __int64 v32; // x0 __int64 v33; // [xsp+8h] [xbp-168h] BYREF size_t v34; // [xsp+10h] [xbp-160h] BYREF void *v35; // [xsp+18h] [xbp-158h] size_t v36; // [xsp+20h] [xbp-150h] _QWORD v37[3]; // [xsp+28h] [xbp-148h] BYREF char *v38; // [xsp+40h] [xbp-130h] BYREF __int64 v39; // [xsp+48h] [xbp-128h] __int64 v40; // [xsp+50h] [xbp-120h] __int128 v41; // [xsp+58h] [xbp-118h] BYREF __int128 v42; // [xsp+68h] [xbp-108h] __int128 v43; // [xsp+78h] [xbp-F8h] __int64 v44; // [xsp+88h] [xbp-E8h] __int128 fd; // [xsp+90h] [xbp-E0h] BYREF __int128 v46; // [xsp+A0h] [xbp-D0h] __int128 v47; // [xsp+B0h] [xbp-C0h] __int64 v48; // [xsp+C0h] [xbp-B0h] _QWORD v49[2]; // [xsp+C8h] [xbp-A8h] BYREF __int128 *v50; // [xsp+D8h] [xbp-98h] BYREF __int64 (__fastcall *v51)(); // [xsp+E0h] [xbp-90h] size_t n; // [xsp+E8h] [xbp-88h] _QWORD v53[2]; // [xsp+F0h] [xbp-80h] BYREF __int64 v54[2]; // [xsp+100h] [xbp-70h] BYREF v7 = &byte_8C094; v33 = a1; v8 = sub_32C10(32, 1); if ( !v8 ) { sub_709BC(1, 32, &off_83CC0); goto LABEL_68; } v9 = 0; v4 = (__int128 *)byte_76C8; *(_QWORD *)&v41 = 32; *((_QWORD *)&v41 + 1) = v8; *(_QWORD *)&v42 = 0; do { v11 = qword_79B8[v9]; if ( v11 >= 0x20 ) sub_74374(); LODWORD(v5) = byte_6840[v11]; v12 = byte_76C8[v9 & 7]; if ( v9 == (_QWORD)v41 ) sub_707D8(&v41, &off_83CF0); v10 = v9 == 31; *(_BYTE *)(*((_QWORD *)&v41 + 1) + v9++) = v12 ^ v5; *(_QWORD *)&v42 = v9; } while ( !v10 ); sub_70E70(&fd, *((_QWORD *)&v41 + 1), 32); v1 = (const void *)*((_QWORD *)&fd + 1); v2 = v46; if ( (v46 & 0x8000000000000000LL) != 0 ) { v3 = 0; goto LABEL_66; } if ( (_QWORD)v46 ) { v3 = 1; v13 = sub_32C10(v46, 1); if ( v13 ) { v14 = (void *)v13; goto LABEL_12; } LABEL_66: sub_709BC(v3, v2, &off_83A68); goto LABEL_68; } v14 = &dword_0 + 1; LABEL_12: memcpy(v14, v1, v2); v34 = v2; v35 = v14; v36 = v2; if ( (_QWORD)v41 ) sub_32C14(*((_QWORD *)&v41 + 1), v41, 1); if ( ((unsigned __int64)fd | 0x8000000000000000LL) != 0x8000000000000000LL ) sub_32C14(v1, fd, 1); *(_QWORD *)&v41 = &v34; *((_QWORD *)&v41 + 1) = sub_2C0C4; *(_QWORD *)&fd = &off_83D40; *((_QWORD *)&fd + 1) = 2; *(_QWORD *)&v46 = &v41; *((_QWORD *)&v46 + 1) = 1; *(_QWORD *)&v47 = 0; sub_70CE4(v37, &fd); v1 = (const void *)v37[0]; v2 = v37[1]; v3 = v37[2]; v49[0] = aSdcardAndroidD; v49[1] = 52; LODWORD(fd) = 511; BYTE4(fd) = 1; v15 = sub_538D4(&fd); if ( v15 ) { if ( (v15 & 3) == 1 ) { v16 = v15 - 1; v17 = *(_QWORD *)(v15 - 1); v4 = *(__int128 **)(v15 + 7); if ( *(_QWORD *)v4 ) (*(void (__fastcall **)(__int64))v4)(v17); v18 = *((_QWORD *)v4 + 1); if ( v18 ) sub_32C14(v17, v18, *((_QWORD *)v4 + 2)); sub_32C14(v16, 24, 8); } } else { *(_QWORD *)&v41 = v49; *((_QWORD *)&v41 + 1) = sub_30F08; LODWORD(v5) = 1; *(_QWORD *)&fd = &unk_83D08; *((_QWORD *)&fd + 1) = 2; *(_QWORD *)&v46 = &v41; *((_QWORD *)&v46 + 1) = 1; *(_QWORD *)&v47 = 0; sub_70CE4(&v50, &fd); v4 = v50; v7 = (char *)v51; v19 = n; DWORD2(fd) = 16777472; *(_QWORD *)&fd = 0x1B600000000LL; WORD6(fd) = 1; sub_536A4((__int64 *)&v41, (int)&fd, (int)v51, n); v6 = (unsigned int)v41; if ( (v41 & 1) == 0 ) { LODWORD(fd) = DWORD1(v41); v23 = sub_30C48(&fd, v2, v3); v3 = v23; if ( (v23 & 3) == 1 ) { v3 = v23 - 1; v5 = *(_QWORD *)(v23 - 1); v24 = *(_QWORD *)(v23 + 7); if ( *(_QWORD *)v24 ) (*(void (__fastcall **)(__int64))v24)(v5); v25 = *(_QWORD *)(v24 + 8); if ( v25 ) sub_32C14(v5, v25, *(_QWORD *)(v24 + 16)); sub_32C14(v3, 24, 8); } close(fd); if ( v4 == (__int128 *)0x8000000000000000LL ) goto LABEL_45; goto LABEL_59; } if ( v4 ) sub_32C14(v7, v4, 1); sub_2BFFC(&v41); } v53[0] = &aSdcardAndroidD[52]; v53[1] = 42; LODWORD(fd) = 511; BYTE4(fd) = 1; v20 = sub_538D4(&fd); v7 = (char *)v20; if ( v20 ) { if ( (v20 & 3) == 1 ) { v7 = (char *)(v20 - 1); v3 = *(_QWORD *)(v20 - 1); v21 = *(_QWORD *)(v20 + 7); if ( *(_QWORD *)v21 ) (*(void (__fastcall **)(__int64))v21)(v3); v22 = *(_QWORD *)(v21 + 8); if ( v22 ) sub_32C14(v3, v22, *(_QWORD *)(v21 + 16)); sub_32C14(v7, 24, 8); } LABEL_45: sub_2D560(&v33, &unk_7BBC, 35); goto LABEL_46; } v54[0] = (__int64)v53; v54[1] = (__int64)sub_30F08; v4 = (_OWORD *)(&dword_0 + 1); *(_QWORD *)&fd = &unk_83D08; *((_QWORD *)&fd + 1) = 2; *(_QWORD *)&v46 = v54; *((_QWORD *)&v46 + 1) = 1; *(_QWORD *)&v47 = 0; sub_70CE4(&v41, &fd); v7 = (char *)*((_QWORD *)&v41 + 1); v19 = v42; DWORD2(fd) = 16777472; *(_QWORD *)&fd = 0x1B600000000LL; WORD6(fd) = 1; sub_536A4(v54, (int)&fd, SDWORD2(v41), v42); LODWORD(v5) = v54[0]; if ( (v54[0] & 1) != 0 ) { if ( (_QWORD)v41 ) sub_32C14(v7, v41, 1); sub_2BFFC(v54); goto LABEL_45; } LODWORD(fd) = HIDWORD(v54[0]); v28 = sub_30C48(&fd, v2, v3); v3 = v28; if ( (v28 & 3) == 1 ) { v3 = v28 - 1; v29 = *(_QWORD *)(v28 - 1); v6 = *(_QWORD *)(v28 + 7); if ( *(_QWORD *)v6 ) (*(void (__fastcall **)(__int64))v6)(v29); v30 = *(_QWORD *)(v6 + 8); if ( v30 ) sub_32C14(v29, v30, *(_QWORD *)(v6 + 16)); sub_32C14(v3, 24, 8); } v4 = (__int128 *)v41; close(fd); if ( v4 == (__int128 *)0x8000000000000000LL ) goto LABEL_45; LABEL_59: *(_QWORD *)&v41 = v4; *((_QWORD *)&v41 + 1) = v7; v50 = &v41; v51 = sub_2C0C4; *(_QWORD *)&fd = &off_83D60; *((_QWORD *)&fd + 1) = 1; *(_QWORD *)&v42 = v19; *((_QWORD *)&v46 + 1) = 1; *(_QWORD *)&v47 = 0; *(_QWORD *)&v46 = &v50; sub_70CE4(&v38, &fd); v7 = v38; v3 = v39; sub_2D560(&v33, v39, v40); if ( v7 ) sub_32C14(v3, v7, 1); if ( (_QWORD)v41 ) sub_32C14(*((_QWORD *)&v41 + 1), v41, 1); LABEL_46: sub_201AC(&v41, &v33, &unk_7BDF, 10); if ( (unsigned __int8)v41 != 15 ) { fd = v41; v46 = v42; v47 = v43; v48 = v44; sub_74534(aCalledResultUn, 43, &fd, &off_83A48, &off_83D70); LABEL_68: __break(1u); v31 = *(_QWORD *)(v6 + 8); if ( v31 ) sub_32C14(v4, v31, *(_QWORD *)(v6 + 16)); sub_32C14(v3, 24, 8); close(fd); if ( (_QWORD)v41 ) sub_32C14(v7, v41, 1); if ( (_DWORD)v5 ) sub_2BFFC(v54); if ( v1 ) sub_32C14(v2, v1, 1); v32 = sub_2BDD4(&v34); sub_743C4(v32); } v26 = *((_QWORD *)&v41 + 1); if ( v1 ) sub_32C14(v2, v1, 1); if ( v34 ) sub_32C14(v35, v34, 1); return v26; } ``` Objective function: `Java_com_tinygame_app_Engine_getFlagPartB` Decoding logic: The author uses the XOR algorithm with 3 components: - qword_79B8: Array containing the indices. ![image](https://hackmd.io/_uploads/H19gy8IwZl.png) - byte_6840: Array of encrypted data. ![image](https://hackmd.io/_uploads/rJOZJI8PWe.png) - byte_76C8: XOR key consisting of 8 bytes. ![image](https://hackmd.io/_uploads/H1hfJ88Dbg.png) Solution: Write a Python script to simulate the XOR loop to extract the Part B string. ```python= byte_76C8 = [ 0xA5, 0x1F, 0xC3, 0x77, 0x19, 0xEE, 0x42, 0x58 ] byte_6840 = [ 0xF5,0xDB,0xA6,0x60,0x2F,0x45,0x74,0x21,0x12, 0x2C,0xC4,0x7D,0x2B,0x23,0x90,0x28,0x7A,0x2E, 0xD6,0xF0,0xF4,0x26,0xDD,0x4E,0xDA,0x7A,0x11, 0x3E,0xC3,0x3E,0xC7,0x39 ] qword_79B8 = [ 0x1E,0x11,0x14,0x17,0x19,0x01,0x07,0x1F, 0x1C,0x0B,0x02,0x05,0x09,0x12,0x0D,0x03, 0x0A,0x15,0x13,0x08,0x0F,0x16,0x06,0x1D, 0x0E,0x04,0x00,0x1A,0x0C,0x18,0x10,0x1B ] out = [] for i in range(32): out.append(byte_76C8[i % 8] ^ byte_6840[qword_79B8[i]]) flag_part_b = bytes(out) print(flag_part_b.decode(errors="ignore")) #b179c5cafbe258a8a93e136f506f248f ``` PartA: Dynamic Loading Since the main code `ui.UiFactory` is not in the original APK but is decoded and loaded into RAM, we need to use dynamic parsing. This place was shit at first. I spent a lot of time trying to hook natively into `ui.UIFactory`, but it didn't work. Technique used: `Frida-dexdump` Execution: Run the application on the emulator, press the `Start button` to activate the code loading trigger. ![image](https://hackmd.io/_uploads/HyH2xIUDZg.png) Use `frida-dexdump -U -p 15054 -d` to scan all DEX files in memory. ![image](https://hackmd.io/_uploads/HJJTl8LPZx.png) Analyze the dumped file: Search for the classes `ui.UiFactory` and `ui.BunnyArcade`. ![image](https://hackmd.io/_uploads/HJCaxLLvWe.png) `Get-ChildItem -Recurse | Select-String "UiFactory"` ![image](https://hackmd.io/_uploads/S1lZWLUDWg.png) Look at this line: `classes03.dex:28512:Lui/R$string; Lui/R$style; Lui/R; Lui/UiFactory; MM::MAGIC`. And especially: `classes03.dex:31028:UiFactory.kt`. This is the DEX file containing the UI logic that the application loads dynamically. ![image](https://hackmd.io/_uploads/SJI8-UUw-l.png) Exploitation: In the `BunnyArcade class`, the `FLAG_B64` variable is detected. ![image](https://hackmd.io/_uploads/r1iu-8LvWx.png) ![image](https://hackmd.io/_uploads/HkqtZL8wZl.png) Decode Base64 obtain the string containing PartA `Congrats: First part of the flag: dctf{60087b379564677d411af3b7bb7ef0d0...}`