Writeup Aria
C2C Qualification EnglishReverse Engineering

bunaken

1771214529457

description

Author: vidner

Can you help me to recover the flag?

Attachments

unzip bunaken_bunaken-dist.zip
# Archive:  bunaken_bunaken-dist.zip
#   inflating: bunaken
#   inflating: flag.txt.bunakencrypted

ls
# bunaken  bunaken_bunaken-dist.zip  flag.txt.bunakencrypted

file *
# bunaken:                  ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=109a021c1b3405d73bd0e95dcad52ec5857f4ed9, not stripped
# bunaken_bunaken-dist.zip: Zip archive data, at least v2.0 to extract, compression method=deflate
# flag.txt.bunakencrypted:  ASCII text, with no line terminators

Overview

Short summary

  • bunaken is a Bun (JavaScript) binary that hides the password inside obfuscated code and uses AES‑CBC + gzip to store the encrypted flag. Goal: extract the password → derive the key → decrypt → decompress → read the flag. ✅

Artifacts & techniques

  • Binary: bunaken (decompile using bun-decompile).
  • Encryption: AES‑CBC (IV || ciphertext), key = SHA-256(password)[0:16].
  • Obfuscation: string array + RC4‑like decoder → password sulawesi.

Solution flow (brief)

  1. Decompile bunaken → find the string decoder and call c(373, "rG]G") → get sulawesi. 🔑
  2. Derive key: SHA256("sulawesi")[0:16].
  3. Decrypt flag.txt.bunakencrypted (take the first 16 bytes as IV), remove padding, then decompress (gzip) → flag. 🔓

Why this works

  • All secrets (password) and decryption logic are inside the binary (client side) — reverse engineering is sufficient to recover the decryption materials. ⚠️

AI-assisted output

1771298186016 1771298203401

analysis

./bunaken
# ENOENT: no such file or directory, open 'flag.txt'
#     path: "flag.txt",
#  syscall: "open",
#    errno: -2,
#     code: "ENOENT"


# Bun v1.3.6 (Linux x64)

From the error above, we can see that the program attempts to open a file named flag.txt, which does not exist. However, we do have a file named flag.txt.bunakencrypted, which is most likely the encrypted version of the flag.

In addition, we also get important information that this binary was built using Bun v1.3.6, which is a relatively new JavaScript/TypeScript runtime. This indicates that the binary likely uses modern JavaScript features, and we may need appropriate reverse engineering techniques to understand how flag.txt is generated from flag.txt.bunakencrypted.

I searched on Google for information related to bun decompile.

1771215025947

After opening it, we obtained information about how to install and use bun-decompile.

1771215133298

Installing bun decompile

Install bun first if it is not already installed

curl -fsSL https://bun.com/install | bash
exec /usr/bin/zsh
bun --version

Then install the bun-decompile library

bun add -g @shepherdjerred/bun-decompile

1771215308415

solution steps

1. Decompile the bunaken binary

We decompile the bunaken binary to obtain the JavaScript/TypeScript source code used to build the binary, using the following command:

bun-decompile ./bunaken -o ./extracted

1771215396651

2. Decompiled Source Code Analysis

After the decompilation process is complete, we will be presented with an extracted folder containing the decompiled JavaScript/TypeScript files. We will open the main file, which likely contains the logic for opening the flag.txt file and generating the flag. We will then search the source code to find the section that attempts to open flag.txt and see how the flag.txt.bunakencrypted file is used in that process.

bunaken.js

// @bun
function w(){let n=["WR0tF8oezmkl","toString","W603xSol","1tlHJnY","1209923ghGtmw","text","13820KCwBPf","byteOffset","40xRjnfn","Cfa9","bNaXh8oEW6OiW5FcIq","alues","lXNdTmoAgqS0pG","D18RtemLWQhcLConW5a","nCknW4vfbtX+","WOZcIKj+WONdMq","FCk1cCk2W7FcM8kdW4y","a8oNWOjkW551fSk2sZVcNa","yqlcTSo9xXNcIY9vW7dcS8ky","from","iSoTxCoMW6/dMSkXW7PSW4xdHaC","c0ZcS2NdK37cM8o+mW","377886jVoqYx","417805ESwrVS","7197AxJyfv","cu7cTX/cMGtdJSowmSk4W5NdVCkl","W7uTCqXDf0ddI8kEFW","write","encrypt","ted","xHxdQ0m","byteLength","6CCilXQ","304OpHfOi","set","263564pSWjjv","subtle","945765JHdYMe","SHA-256","Bu7dQfxcU3K","getRandomV"];return w=function(){return n},w()}function l(n,r){return n=n-367,w()[n]}var y=l,s=c;function c(n,r){n=n-367;let t=w(),x=t[n];if(c.uRqEit===void 0){var b=function(i){let f="",a="";for(let d=0,o,e,p=0;e=i.charAt(p++);~e&&(o=d%4?o*64+e:e,d++%4)?f+=String.fromCharCode(255&o>>(-2*d&6)):0)e="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=".indexOf(e);for(let d=0,o=f.length;d<o;d++)a+="%"+("00"+f.charCodeAt(d).toString(16)).slice(-2);return decodeURIComponent(a)};let U=function(i,B){let f=[],a=0,d,o="";i=b(i);let e;for(e=0;e<256;e++)f[e]=e;for(e=0;e<256;e++)a=(a+f[e]+B.charCodeAt(e%B.length))%256,d=f[e],f[e]=f[a],f[a]=d;e=0,a=0;for(let p=0;p<i.length;p++)e=(e+1)%256,a=(a+f[e])%256,d=f[e],f[e]=f[a],f[a]=d,o+=String.fromCharCode(i.charCodeAt(p)^f[(f[e]+f[a])%256]);return o};c.yUvSwA=U,c.MmZTqk={},c.uRqEit=!0}let u=t[0],I=n+u,A=c.MmZTqk[I];return!A?(c.ftPoNg===void 0&&(c.ftPoNg=!0),x=c.yUvSwA(x,r),c.MmZTqk[I]=x):x=A,x}(function(n,r){let t=c,x=l,b=n();while(!0)try{if(parseInt(x(405))/1*(parseInt(x(383))/2)+-parseInt(x(385))/3*(parseInt(t(382,"9Dnx"))/4)+parseInt(x(384))/5*(-parseInt(x(393))/6)+parseInt(x(396))/7*(parseInt(x(369))/8)+parseInt(t(381,"R69F"))/9+-parseInt(x(367))/10+-parseInt(x(406))/11===r)break;else b.push(b.shift())}catch(u){b.push(b.shift())}})(w,105028);var h=async(n)=>{let r=l,t=c,x=n instanceof ArrayBuffer?new Uint8Array(n):new Uint8Array(n[t(400,"I2yl")],n[r(368)],n.byteLength);if(x.byteLength===16||x.byteLength===24||x.byteLength===32)return x;let b=await crypto.subtle[t(402,"Fw]1")](r(399),x);return new Uint8Array(b).subarray(0,16)},g=(n,r)=>{let t=l,x=new Uint8Array(n.byteLength+r.byteLength);return x.set(n,0),x[t(395)](r,n[t(392)]),x},m=async(n,r)=>{let t=c,x=l,b=crypto[x(401)+x(372)](new Uint8Array(16)),u=await h(n),I=await crypto[x(397)][t(371,"kAmA")](t(370,"CYgn"),u,{name:"AES-CBC"},!1,[x(389)]),A=await crypto.subtle[x(389)]({name:t(375,"dHTh"),iv:b},I,r);return g(b,new Uint8Array(A))},S=Bun[s(391,"9Dnx")](s(377,"R69F")),k=await S[y(407)](),v=await Bun[s(387,"f]pG")+"ss"](k),z=await m(Buffer[y(380)](s(373,"rG]G")),v);Bun[y(388)]("flag.txt.b"+s(374,"CYgn")+y(390),Buffer[s(404,"(Y*]")](z)[y(403)](s(376,"$lpa")));

//# debugId=89BBB7E67C06C2CD64756E2164756E21

3. decrypt password

Short explanation:

  • The c(index, key) function in the decompiled output is a string-obfuscation/decoder—it first base64-decodes the internal data, then performs an RC4-like XOR with the key (the second argument). To get the password, we simply execute c(373, "rG]G").

Technical steps (quick fix): create a decrypt_password.js file containing the decompiled function (the w + c function) and run it with Node.

decrypt_password.js

// 1. Array String & Accessor 'w'
function w() {
  let n = [
    "WR0tF8oezmkl",
    "toString",
    "W603xSol",
    "1tlHJnY",
    "1209923ghGtmw",
    "text",
    "13820KCwBPf",
    "byteOffset",
    "40xRjnfn",
    "Cfa9",
    "bNaXh8oEW6OiW5FcIq",
    "alues",
    "lXNdTmoAgqS0pG",
    "D18RtemLWQhcLConW5a",
    "nCknW4vfbtX+",
    "WOZcIKj+WONdMq",
    "FCk1cCk2W7FcM8kdW4y",
    "a8oNWOjkW551fSk2sZVcNa",
    "yqlcTSo9xXNcIY9vW7dcS8ky",
    "from",
    "iSoTxCoMW6/dMSkXW7PSW4xdHaC",
    "c0ZcS2NdK37cM8o+mW",
    "377886jVoqYx",
    "417805ESwrVS",
    "7197AxJyfv",
    "cu7cTX/cMGtdJSowmSk4W5NdVCkl",
    "W7uTCqXDf0ddI8kEFW",
    "write",
    "encrypt",
    "ted",
    "xHxdQ0m",
    "byteLength",
    "6CCilXQ",
    "304OpHfOi",
    "set",
    "263564pSWjjv",
    "subtle",
    "945765JHdYMe",
    "SHA-256",
    "Bu7dQfxcU3K",
    "getRandomV",
  ];
  return (
    (w = function () {
      return n;
    }),
    w()
  );
}

// 2. Simple Accessor 'l' (digunakan saat shuffle untuk baca angka)
function l(n, r) {
  return ((n = n - 367), w()[n]);
}

// 3. Complex Decryptor 'c'
var c = function (n, r) {
  n = n - 367;
  let t = w(),
    x = t[n];
  if (c.uRqEit === void 0) {
    var b = function (i) {
      let f = "",
        a = "";
      for (
        let d = 0, o, e, p = 0;
        (e = i.charAt(p++));
        ~e && ((o = d % 4 ? o * 64 + e : e), d++ % 4) ? (f += String.fromCharCode(255 & (o >> ((-2 * d) & 6)))) : 0
      )
        e = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=".indexOf(e);
      for (let d = 0, o = f.length; d < o; d++) a += "%" + ("00" + f.charCodeAt(d).toString(16)).slice(-2);
      return decodeURIComponent(a);
    };
    let U = function (i, B) {
      let f = [],
        a = 0,
        d,
        o = "";
      i = b(i);
      let e;
      for (e = 0; e < 256; e++) f[e] = e;
      for (e = 0; e < 256; e++) ((a = (a + f[e] + B.charCodeAt(e % B.length)) % 256), (d = f[e]), (f[e] = f[a]), (f[a] = d));
      ((e = 0), (a = 0));
      for (let p = 0; p < i.length; p++)
        ((e = (e + 1) % 256),
          (a = (a + f[e]) % 256),
          (d = f[e]),
          (f[e] = f[a]),
          (f[a] = d),
          (o += String.fromCharCode(i.charCodeAt(p) ^ f[(f[e] + f[a]) % 256])));
      return o;
    };
    ((c.yUvSwA = U), (c.MmZTqk = {}), (c.uRqEit = !0));
  }
  let u = t[0],
    I = n + u,
    A = c.MmZTqk[I];
  return (!A ? (c.ftPoNg === void 0 && (c.ftPoNg = !0), (x = c.yUvSwA(x, r)), (c.MmZTqk[I] = x)) : (x = A), x);
};

// 4. Shuffle Logic (Bagian IIFE dari bunaken.js)
(function (n, r) {
  let t = c,
    x = l,
    b = n();
  while (true) {
    try {
      if (
        (parseInt(x(405)) / 1) * (parseInt(x(383)) / 2) +
          (-parseInt(x(385)) / 3) * (parseInt(t(382, "9Dnx")) / 4) +
          (parseInt(x(384)) / 5) * (-parseInt(x(393)) / 6) +
          (parseInt(x(396)) / 7) * (parseInt(x(369)) / 8) +
          parseInt(t(381, "R69F")) / 9 +
          -parseInt(x(367)) / 10 +
          -parseInt(x(406)) / 11 ===
        r
      )
        break;
      else b.push(b.shift());
    } catch (u) {
      b.push(b.shift());
    }
  }
})(w, 105028);

// 5. Success! Get the password.
try {
  const password = c(373, "rG]G");
  console.log("Decrypted Password:", password);
} catch (e) {
  console.error(e);
}

Run:

node decrypt_password.js

Output:

Decrypted Password: sulawesi

1771215972413

4. decrypt file terenkripsi (flag.txt.bunakencrypted)

Observations from the decompiled source:

  • The binary uses AES-CBC.
  • The AES key is SHA-256 (password) and then truncated to 16 bytes (AES-128 key).
  • The encrypted file is stored as IV || ciphertext (IV 16 bytes ahead).

decrypt_flag.py

import base64
import hashlib
import gzip
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

def decrypt_flag_from_file(filename, password):
    enc = open(filename, "rb").read().strip()

    # decode base64
    data = base64.b64decode(enc)

    iv = data[:16]
    ct = data[16:]

    key = hashlib.sha256(password.encode()).digest()[:16]

    print("[*] Key:", key.hex())
    print("[*] IV :", iv.hex())

    cipher = AES.new(key, AES.MODE_CBC, iv)
    decrypted = cipher.decrypt(ct)

    try:
        decrypted = unpad(decrypted, 16)
    except:
        print("[!] No padding or bad padding")

    print("[*] Raw decrypted:", decrypted)
    print("[*] Hex:", decrypted.hex())

    # cek apakah gzip
    if decrypted[:2] == b'\x1f\x8b':
        print("[*] GZIP detected, decompressing...")
        decrypted = gzip.decompress(decrypted)

    print("\n[+] FLAG:", decrypted.decode(errors="ignore"))

decrypt_flag_from_file("flag.txt.bunakencrypted", "sulawesi")

Install dependencies (if not already):

pip install pycryptodome

Then run the decrypt file.

python decrypt_flag.py

1771216631970

flag

C2C{BUN_AwKward_ENcryption_compression_obfuscation}

On this page