Rasyad Satyatma
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.

      Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Explore these features while you wait
      Complete general settings
      Bookmark and like published notes
      Write a few more notes
      Complete general settings
      Write a few more notes
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.

    Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Explore these features while you wait
    Complete general settings
    Bookmark and like published notes
    Write a few more notes
    Complete general settings
    Write a few more notes
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    <h1 style="text-align:center"> HMIF COMQUALS 2025 - WriteUp </h1> <center> Written by CaitViLover a.k.a LiroSphere ![image](https://hackmd.io/_uploads/B1cyGljpee.png) </center> <h2 style="text-align:center"> Table Of Contents </h2> * [Digital Forensics](#Digital-Forensics) * [Asep's Secret - 100](#Aseps-Secret---100) * [Asep Kasep - 493](#Asep-Kasep---493) * [Reverse Engineering](#Reverse-Engineering) * [Yamada Ransom - 100](#Yamada-Ransom---100) * [TBFO STIMA - 500](#TBFO-STIMA---500) * [Cookie Monster - 500](#Cookie-Monster---500) * [Cryptography](#Cryptography) * [Easy Hash - 200](#Easy-Hash---200) * [Misc](#Misc) * [Irony - 100](#Irony---100) * [Pulkam - 283](#Pulkam---285) * [Afterword](#Afterword) <br> <h2 style="text-align:center"> Digital Forensics </h2> <h3 style="text-align:center"> Asep's Secret - 100 </h3> <center> ![image](https://hackmd.io/_uploads/BkawMxsTex.png) </center> We're given an attachment `chall.pcap`, inside we can find a lot of packets using the USB protocol. Looking closer at the info column of wireshark, there's a few thing. It starts by the usb device "establishing connection" to the computer, and then a lot of USB request block (URB) interrupts. <center> ![dh8FSo7NnZ](https://hackmd.io/_uploads/HJrwOlsTle.png) </center> Remembering our OS project to make a whole operating system from scratch (thx labsister), when keyboard (or any input for that matter) needs to call an interrupt to actually input to the computer, hence the name USB request block (URB). Looking closer into each URB_INTERRUPT packets, we found the packets that being sent from the device has HID (Human Interface Device) data, which contains 8 bytes of.... something. At this point i was a bit stumped, so we turn to out favorite LLM, and find out those are keyboard HID data! What's left to do is to decrypt them into actual text, and maybe we can find the flag? (clueless) ::: info The keyboard mappings could be found on [Official USB Documentation](https://usb.org/sites/default/files/hut1_6.pdf) ::: ```py mappings = { 0x04:"A", 0x05:"B", 0x06:"C", 0x07:"D", 0x08:"E", 0x09:"F", 0x0A:"G", 0x0B:"H", 0x0C:"I", 0x0D:"J", 0x0E:"K", 0x0F:"L", 0x10:"M", 0x11:"N", 0x12:"O", 0x13:"P", 0x14:"Q", 0x15:"R", 0x16:"S", 0x17:"T", 0x18:"U", 0x19:"V", 0x1A:"W", 0x1B:"X", 0x1C:"Y", 0x1D:"Z", 0x1E:"1", 0x1F:"2", 0x20:"3", 0x21:"4", 0x22:"5", 0x23:"6", 0x24:"7", 0x25:"8", 0x26:"9", 0x27:"0", 0x28:"n", 0x2a:"[DEL]", 0X2B:"[TAB]", 0x2C:" ", 0x2D:"-", 0x2E:"=", 0x2F:"[", 0x30:"]", 0x31:"\\", 0x32:"~", 0x33:";", 0x34:"'", 0x36:",", 0x37:"." } nums = [] # tshark -r chall.pcap -T fields -e usbhid.data > usb.txt keys = open('./usb.txt') for line in keys: line = line.strip() if len(line) < 6: continue if line[:2] != '00' or line[4:6] != '00': nums.append(int(line[4:6],16)) keys.close() output = "" for n in nums: if n == 0: continue if n in mappings: output += mappings[n] else: output += '' ``` Well, that went as well as one can imagine. Now we look inside the decoded text, and found a pastebin link (well, at least it's supposed to be.) ``` IINTOO II CHRROOMME PAASTTEBBIIN.CCOOM/YJRVPAB5 II TTHHINNKK ``` The issue with this decoder is that it cannot differentiate between capitalized and non-capitalized letters. So we look again to our most loved method of problem-solving, brute-force. ```py base_url = "https://pastebin.com/raw/" slug = "YJRVPAB5" chars = [([c.lower(), c.upper()]) for c in slug] urls = [base_url + ''.join(p) for p in product(*chars)] for url in urls: try: resp = requests.get(url) body = resp.text if ("Not Found" not in body): print(f"Found: {url}") break else: print(f"Not Found: {url}") except Exception as e: continue ``` Now we can find the true pastebin link, maybe now we can find the flag. After running it, the valid pastebin link turns to be: https://pastebin.com/yjrVPaB5. The link is pastebin is key-protected, which turns us back into the decoded dump, and found the key right under where we found the pastebin slug. ``` PAASSWWORRDED THHO OOOKAAY RAHASIAASEP1284 DDAMNnOOKAY VIM TODO.TXTnI- ``` Some trial and error, after that we found the valid password to be `rahasiaasep1284`. After that, the flag can be found! ![image](https://hackmd.io/_uploads/H1pGTWspeg.png) <h4 style="text-align:center"> CTFITB2025{h4ti_h4ti_trafF1c_UsB_ny4_d15ad4p!!!s33_y0u_1n_ctf_c0mmun1ty} </h4> <hr> <h3 style="text-align:center"> Asep Kasep - 493 </h3> <center> ![image](https://hackmd.io/_uploads/ry0Lymo6ll.png) </center> The attachment given is a zip file containing 1 file, `chall.ad1`. An image file which can be open using FTK Imager. Doing some basic investiagtion results in finding a file named "I GOT HACKED.txt" which told the story of Asep running a malicious powershell script that results in his computer getting encrypted (cmon man, in big 25 🥀). On the same `Downloads` folder, we also found a `traffic.pcapng` file, filled with Asep's network activity. <center> ![image](https://hackmd.io/_uploads/r18fNQs6ge.png) </center> Opening the capture file in wireshark, we found the call that happened when Asep downloaded the ZIP file, and just export that packet so we can do more investigative work. <center> ![image](https://hackmd.io/_uploads/H1y_y4opgl.png) </center> The ZIP file contains 2 files, `Helpdesk.ps1` the actual malware in the form of a powershell script, and `READ_FIRST.txt` which is filled with instruction on how to run the script. <center> ![image](https://hackmd.io/_uploads/HJ9oUXiage.png) </center> Opening the script in a code editor reveals what the Powershell script does. In short, the script uses a Linear Congruential Generator (LCG) to generate keys that is used to encrypt files recursively. Here's a snippet of the initial LCG seed, the LCG algorithm, and the function that does the encryption. ```ps $Global:LCGSeed = [Convert]::ToUInt32('C0FFEE12',16) function Next-State([uint32]$state){ $tmp = ([uint64]$state * 1103515245) + 12345 $mod = $tmp % 4294967296 return [uint32]$mod } function Protect-OneFile { param([Parameter(Mandatory)][string]$InPath) $plain = [System.IO.File]::ReadAllBytes($InPath) $ct = New-Object byte[] ($plain.Length + 6) $ct[0]=0x48; $ct[1]=0x45; $ct[2]=0x4C; $ct[3]=0x50; $ct[4]=0x01; $ct[5]=0x00 # "HELP"+v1 [uint32]$state = $Global:LCGSeed for ($i=0; $i -lt $plain.Length; $i++) { $state = Next-State $state $k = [byte](($state -shr 24) -band 0xFF) $ct[6+$i] = $plain[$i] -bxor $k } $outPath = "$InPath.enc" [System.IO.File]::WriteAllBytes($outPath, $ct) $ok = ([System.IO.File]::Exists($outPath) -and ((Get-Item -LiteralPath $outPath).Length -eq $ct.Length)) if (-not $ok) { throw "Output verification failed (size mismatch)." } Remove-Item -LiteralPath $InPath -Force return $outPath } ``` The encryption process goes like this: * Program reads a file input and prepares a buffer with the size of the input + 6 bytes, in which place a set of 6 bytes is put as a header `0x48 0x45 0x4C 0x50 0x01 0x00` (The first 4 is HELP in ASCII) * The LCG is called, the first time using the initial seed `0xC0FFEE12` * After the initial call, the seed advanced by using the value returned by the LCG function itself. * The value is then used to XOR with the input (in this case the files that is to be encrypted), and repeat until all of the input is encrypted, each with a new key from the LCG function. * The input is then saved into the the filesystem using the same name as the original input name, plus a `.enc` extension (input.txt -> input.txt.enc) * After the encryption process is done, the program verifies the size of the output (accounting extra space for the header) to match the input. * Lastly, the program deletes any input files that previously got encrypted. Now that we understand what the script does, time to exploit it's weakness and make a decryptor. When looking closer, one realizes that the LCG algorithm is actually deterministic, meaning if we know the initial seed (which we do), we can make the decryptor! ```python def lcg_next(state): return ((state * 1103515245) + 12345) & 0xFFFFFFFF def decrypt_file(enc_path, out_path): with open(enc_path, 'rb') as f: data = f.read() # Verify header if data[:6] != b'HELP\x01\x00': raise ValueError("Invalid encrypted file header") ciphertext = data[6:] plaintext = bytearray() state = 0xC0FFEE12 for byte in ciphertext: state = lcg_next(state) key_byte = (state >> 24) & 0xFF plaintext.append(byte ^ key_byte) with open(out_path, 'wb') as f: f.write(plaintext) prob_folder = "/home/lirosphere/ctf/contests/hmifcomquals/foren/Asep Kasep" decrypt_file(f'{prob_folder}/Input.enc', f'{prob_folder}/Output')% ``` Thanks to your favorite LLM for the decryptor, now what file should we decrypt? The `SECRET.zip.enc` and `SECRET_README.txt.enc` in the `Documents` folder looks like a good target. ``` SECRET_README.txt so uhh... if I ever forget my super SECRET zip password, don’t worry, it's the same as my Instagram password 😏 ``` Oh, looks like we're on the correct track, the ZIP file is password-protected, but that doesn't stop us from peeking into what's inside. <center> ![image](https://hackmd.io/_uploads/r1rps8nple.png) </center> 4 PNG files, maybe the flag is in there? Time to find the password, the SECRET_README.txt mentioned it's the same as Asep's Instagram password. This leads us into extracting the saved passwords of browsers, since to access instagram, you'll need to use a browser. After some more sniffing around, we find Asep uses Firefox, a browser that saves logins (usernames and passwords), and already has a [decryptor](https://github.com/unode/firefox_decrypt), we can just extract the profile folder and find the password. <center> ![image](https://hackmd.io/_uploads/S14EyP3Txl.png) </center> Now that we got the password, let's unzip the `SECRET.zip` file and see what's inside. <center> | <center>1.png</center> | <center>2.png</center> | <center>3.png</center> | <center>4.png</center> | | -------- | -------- | -------- | --- | | ![1](https://hackmd.io/_uploads/SJ9-gDnTxg.png) | ![2](https://hackmd.io/_uploads/Hydblw26gl.png) | ![3](https://hackmd.io/_uploads/ryubePnpgg.png) | ![4](https://hackmd.io/_uploads/Byd-lwnage.png) | </center> All that's left is to construct the flag, and that's it! <h4 style="text-align:center"> CTFITB2025{A53p_9oB10k_G4mp4n9_k3n4_t1pu} </h4> <hr> <br> <h2 style="text-align:center"> Reverse Engineering </h2> <h3 style="text-align:center"> Yamada Ransom - 100 </h3> <center> ![image](https://hackmd.io/_uploads/H1cEQPnaex.png) </center> We're given a ZIP file containing an encrypted flag aptly named `flag.encrypted` and an .exe file `yamada.exe`. From the name of the problem, one should not just run the executable, so we throw it into your favorite decompiler. The executable is a simple encryption "malware" that uses AES to encrypt a file named `flag.txt` into `flag.encrypted`, time to put your favorite LLM into use again. ```python from Crypto.Cipher import AES from Crypto.Hash import SHA256 from Crypto.Util.Padding import unpad def solve(): key = b"ThisIsTheEncryptKey" iv = b'\x00' * 16 hasher = SHA256.new(key) derived_key = hasher.digest() with open("flag.encrypted", "rb") as f: encrypted_data = f.read() cipher = AES.new(derived_key, AES.MODE_CBC, iv) decrypted_data = cipher.decrypt(encrypted_data) try: unpadded_data = unpad(decrypted_data, AES.block_size) except ValueError: unpadded_data = decrypted_data.rstrip(b'\0') print("Decrypted flag:") print(unpadded_data.decode(errors='ignore')) if __name__ == "__main__": solve() ``` :::info 🕙 The part that took the most time was actually looking for the correct IV. Note the `\x00*16` used as the IV, in the executable exists a string `IVCanObfuscation`, but returns a partially broken flag when used. Not sure if this was a red-herring, intended, or an oversight. ::: <h4 style="text-align:center"> CTFITB2025{omg99_ry0_d0nt_g1v3_th3m_m4lwar3_alr3ady!_:shakemyhead:} </h4> <hr> <h3 style="text-align:center"> TBFO STIMA - 500 </h3> <center> ![image](https://hackmd.io/_uploads/ByZxuZapge.png) </center> The attachment given is a program that prompts the user for an input, which can only consists of the characters "c", "t", and "f". <center> ![image](https://hackmd.io/_uploads/Hy_TObaaxl.png) </center> Trying to decompile the program directly results in a suboptimal results, maybe it has some sort of anti-tamper? <center> ![image](https://hackmd.io/_uploads/B1_J9W6Tee.png) </center> Well, that solves it, time to unpack it before we continue on with our journey. After doing that (and some decompiling), time to put your favorite LLM to use (again). ```python import struct from collections import deque def decrypt_byte(encrypted, key2, index, key4, key5): return ((key4 * index + key5) ^ key2 ^ encrypted) & 0xFF def decrypt_data(data, key2, key4, key5): result = [] for i, byte in enumerate(data): decrypted = decrypt_byte(byte, key2, i, key4, key5) result.append(decrypted) return bytes(result) with open('chall_unpacked', 'rb') as f: f.seek(0x2100) params_encrypted = f.read(6) f.seek(0x2120) emission_encrypted = f.read(0x6c) f.seek(0x21a0) transition_encrypted = f.read(0x144) print("[*] Decrypting tables...") transition_decrypted = decrypt_data(transition_encrypted, 0x9d, 0x3d, 7) emission_decrypted = decrypt_data(emission_encrypted, 0xc3, 0x55, 0x11) params_decrypted = decrypt_data(params_encrypted, 0xa7, 0x21, 0x42) transition_table = [] for i in range(0, len(transition_decrypted), 2): value = struct.unpack('<H', transition_decrypted[i:i+2])[0] transition_table.append(value) emission_table = [] for i in range(0, len(emission_decrypted), 2): value = struct.unpack('<H', emission_decrypted[i:i+2])[0] emission_table.append(value) print(f"[*] Transition table entries: {len(transition_table)}") print(f"[*] Emission table entries: {len(emission_table)}") print(f"[*] Number of states: {len(transition_table) // 3}") num_states = len(transition_table) // 3 alphabet = ['c', 't', 'f'] def get_next_state(current_state, input_char_idx): """Get next state from transition table""" index = current_state * 3 + input_char_idx if index < len(transition_table): return transition_table[index] return None graph = {} for state in range(num_states): graph[state] = [] for char_idx, char in enumerate(alphabet): next_state = get_next_state(state, char_idx) if next_state is not None and next_state < num_states: graph[state].append((next_state, char, char_idx)) print(f"[*] FSM Graph built with {len(graph)} states") def find_path(start_state, target_state): """BFS to find shortest path""" queue = deque([(start_state, [])]) visited = {start_state} while queue: state, path = queue.popleft() if state == target_state: return path if state in graph: for next_state, char, char_idx in graph[state]: if next_state not in visited: visited.add(next_state) queue.append((next_state, path + [(char, char_idx, state, next_state)])) return None print("[*] Finding path from state 1 to state 19...") path = find_path(1, 19) if path is None: print("[-] No path found!") exit(1) print(f"[+] Path found! Length: {len(path)}") input_string = ''.join([char for char, _, _, _ in path]) print(f"[+] Input string: {input_string}") def simulate_fsm(input_chars): """Simulate FSM and generate output""" state = 1 output = [] init_param1 = decrypt_byte(0x30, 0x5e, 0, 0x12, 0x34) init_param2 = decrypt_byte(0xbd, 0x5e, 1, 0x12, 0x34) init_params = bytes([init_param1, init_param2]) for char, char_idx, old_state, new_state in input_chars: old_emission = emission_table[old_state] if old_state < len(emission_table) else 0 new_emission = emission_table[new_state] if new_state < len(emission_table) else 0 params_val = struct.unpack('<H', params_decrypted[char_idx*2:char_idx*2+2])[0] if char_idx*2+1 < len(params_decrypted) else 0 old_b1 = old_emission & 0xFF old_b2 = (old_emission >> 8) & 0xFF new_b1 = new_emission & 0xFF new_b2 = (new_emission >> 8) & 0xFF p_b1 = params_val & 0xFF p_b2 = (params_val >> 8) & 0xFF init_b1 = init_params[0] init_b2 = init_params[1] out_b1 = (((new_b1 ^ old_b1) + p_b1) ^ init_b1) & 0xFF out_b2 = (((new_b2 ^ old_b2) + p_b2) ^ init_b2) & 0xFF output.append(out_b1) output.append(out_b2) state = new_state return bytes(output) output = simulate_fsm(path) print(f"[+] Output length: {len(output)} bytes") if len(output) >= 36: flag = output[:36].decode('ascii', errors='ignore') print(f"[+] Flag: {flag}") else: print(f"[-] Output too short: {len(output)} < 36") print(f"[*] Raw output: {output.hex()}") ``` :::spoiler :abcd: The program is a [Mealy Machine](https://en.wikipedia.org/wiki/Mealy_machine). I feel dirty just saying "put it through an LLM" but that is what i did :p The solver extract encrypted data from the binary, and builds a graph from that data, pathfinds into the correct sequence, and voila. Don't @ me pls n thx ::: <center> ![image](https://hackmd.io/_uploads/B1YG47a6ge.png) </center> <h4 style="text-align:center"> CTFITB2025{j4go_tbF0_d4n_st1M4_n1cH} </h4> <hr> <h3 style="text-align:center"> Cookie Monster - 500 </h3> <center> ![image](https://hackmd.io/_uploads/B1CldQpaex.png) </center> Ah, a game reverse-engineering, thank you my [goat](https://cryptohack.org/user/Etynso/) <3. Anyway, enough glazing, let's dive in! <center> ![image](https://hackmd.io/_uploads/ByB7F7aTlg.png) </center> `game.x86_64` and `game.pck`, all these files reeks of Godot (my beloved), time to bring out the big guns ([GDRE Tools](https://github.com/GDRETools/gdsdecomp)). <center> ![image](https://hackmd.io/_uploads/r1FsFmpagg.png) </center> Aw, it's encrypted, good thing each Godot game that is encrypted packed with the key! "Doesn't that defeat the purpose of the encryption?" Well, how else are people gonna play the game? [Food for thought](https://www.reddit.com/r/godot/comments/1j1kt1h/is_setting_an_encryption_key_for_compilation/). Well, only one way available now, find the key inside the binary! <center> ![image](https://hackmd.io/_uploads/BJQosQppxx.png) </center> Since Godot is open-source, we can look on how it does it's encryption. Mostly is just this [line](https://github.com/godotengine/godot/blob/d3b052df8f0c3051bb488b71eb3f1ba2ce511060/core/io/file_access_pack.cpp#L300) here. So we just need to sniff around in ghidra near the errors with that exact messages, and find the variable referencing the key. There's also a [YouTube Tutorial](https://www.youtube.com/watch?v=fWjuFmYGoSY) on this. Goodluck :D ::: warning My Ghidra broke while writing this, so no screenshots. @lirosphere on discord if you want more information on how to find the key :) ::: After getting the encryption key and extracting the files, we're presented with the core logic of the game inside the `main.gd` <center> ![image](https://hackmd.io/_uploads/B1jdpX6age.png) </center> And of course it's obfuscated, nothing's ever that simple, well you know the drill, put it through your favorite LLM. ```python #!/usr/bin/env python3 """ Cookie Monster CTF Challenge Solver This script simulates the button-clicking game logic to find the correct sequence of 100 button clicks, then uses that sequence to decrypt the flag. """ import hashlib import struct # Constants from the deobfuscated code ENCRYPTED_FLAG = "99901a046bbf02b1b9ca54230460017fd90e85e91c00bba0366ab2b117261486f60296fe565ff9f80d78dbef7f5f3e8f0bbdb4b40102" CHACHA20_NONCE = "514bce624f016e0b6e99fd3c" # Expected MD5 hashes (100 total) EXPECTED_MD5_HASHES = [ "cfcd208495d565ef66e7dff9f98764da", "35f4a8d465e6e1edc05f3d8ab658c551", "1c383cd30b7c298ab50293adfecb7b18", "b53b3a3d6ab90ce0268229151c9bde11", "c20ad4d76fe97759aa27a0c99bff6710", "3c59dc048e8850243be8079a5c74d079", "37693cfc748049e45d87b8c7d8b9aacd", "aab3238922bcc25a6f606eb525ffdc56", "ed3d2c21991e3bef5e069713af9fa6ca", "28dd2c7955ce926456240b2ff0100bde", "a684eceee76fc522773286a895bc8436", "8613985ec49eb8f757ae6439e879bb2a", "c16a5320fa475530d9583c34fd356ef5", "6c8349cc7260ae62e3b1396831a8398f", "ad61ab143223efbc24c7d2583be69251", "d82c8d1619ad8176d665453cfb2e55f0", "33e75ff09dd601bbe69f351039152189", "1ff1de774005f8da13f42943881c655f", "8e296a067a37563370ded05f5a3bf3ec", "d2ddea18f00665ce8623e36bd4e3c7c5", "e369853df766fa44e1ed0ff613f563bd", "02e74f10e0327ad868d138f2b4fdd6f0", "a87ff679a2f3e71d9181a67b7542122c", "7f39f8317fbdb1988ef4c628eba02591", "f033ab37c30201f73f142449d037028d", "2838023a778dfaecdc212708f721b788", "735b90b4568125ed6c3f678819b6e058", "d67d8ab4f4c10bf22aa353e27879133c", "d3d9446802a44259755d38e6d163e820", "98f13708210194c475687be6106a3b84", "17e62166fc8586dfa4d1bc0e1742c08b", "9778d5d219c5080b9a6a17bef029331c", "6ea9ab1baa0efb9e19094440c317e21b", "eccbc87e4b5ce2fe28308fd9f2a7baf3", "d645920e395fedad7bbbed0eca3fe2e0", "9bf31c7ff062936a96d3c8bd1f8f2ff3", "9a1158154dfa42caddbd0694a4e9bdc8", "f7177163c833dff4b38fc8d2872f1ec6", "072b030ba126b2f4b2374f342be9ed44", "a5771bce93e200c36f7cd9dfd0e5deaa", "f4b9ec30ad9f68f89b29639786cb62ef", "fbd7939d674997cdb4692d34de8633c4", "93db85ed909c13838ff95ccfa94cebd9", "fc490ca45c00b1249bbe3554a4fdf6fb", "32bb90e8976aab5298d5da10fe66f21d", "7647966b7343c29048673252e490f736", "45c48cce2e2d7fbdea1afc51c7c6ad26", "8f14e45fceea167a5a36dedd4bea2543", "34173cb38f07f89ddbebc2ac9128303f", "ea5d2f1c4608232e07d3aa3d998e5135", "6f4922f45568161a8cdf4ad2299f6d23", "d1fe173d08e959397adf34b1d77e88d7", "19ca14e7ea6328a42e0eb13d585e4c22", "c74d97b01eae257e44aa9d5bade97baf", "ac627ab1ccbdb62ec96e702f07f6425b", "642e92efb79421734881b53e1e1b18b6", "98dce83da57b0395e163467c9dae521b", "e2c420d928d4bf8ce0ff2ec19b371514", "3295c76acbf4caaed33c36b1b5fc2cb1", "54229abfcfa5649e7003b83dd4755294", "72b32a1f754ba1c09b3695e0cb6cde7f", "e4da3b7fbbce2345d7772b0674a318d5", "92cc227532d17e56e07902b254dfad10", "c0c7c76d30bd3dcaefc96f40275bdc0a", "182be0c5cdcd5072bb1864cdee4d3d6e", "d09bf41544a3365a46c9077ebb5e35c3", "2a38a4a9316c49e5a833517c45d31070", "44f683a84163b3523afe57c2e008bc8c", "a1d0c6e83f027327d8461063f4ac58a6", "fe9fc289c3ff0af142b6d3bead98a923", "a5bfc9e07964f8dddeb95fc584cd965d", "e2ef524fbf3d9fe611d5a8e90fefdc9c", "093f65e080a295f8076b1c5722a46aa2", "c51ce410c124a10e0db5e4b97fc2af39", "b6d767d2f8ed5d21a44b0e5886680cb9", "70efdf2ec9b086079795c442636b55fb", "6512bd43d9caa6e02c990b0a82652dca", "68d30a9594728bc39aa24be94b319d21", "a3f390d88e4c41f2747bfa2f1b5f87db", "67c6a1e7ce56d3d6fa748ab6d9af3fd7", "03afdbd66e7929b125f8597834fa83a4", "43ec517d68b6edd3015b3edc9a11367b", "1f0e3dad99908345f7439f8ffabdffc4", "3ef815416f775098fe977004015c6193", "66f041e16a60928b05a7e228a89c3799", "c4ca4238a0b923820dcc509a6f75849b", "f457c545a9ded88f18ecee47145a72c0", "6364d3f0f495b6ab9dcf8d3b5c6e0b01", "d9d4f495e875a2e075a1a4a6e1b9770f", "c9f0f895fb98ab9159f51fd0297e236d", "c81e728d9d4c2f636f067f89cc14862c", "14bfa6bb14875e45bba028a21ed38046", "7cbbc409ec990f19c78c75bd1e06f215", "9f61408e3afb633e50cdf1b20de6f466", "3416a75f4cea9109507cacd8e2f2aefc", "26657d5ff9020d2abefe558796b99584", "c7e1249ffc03eb9ded908c236bd1996d", "1679091c5a880faf6fb5e6087eb1b2dc", "4e732ced3463d06de0ca9a15b6153677", "812b4ba287f5ee0bc9d43bbf5bbe87fb" ] def compute_md5(text: str) -> str: """Compute MD5 hash of a string""" return hashlib.md5(text.encode()).hexdigest() def extract_index_from_hash(hash_str: str) -> int: """ Extract an index from the last 4 bytes of an MD5 hash. Takes characters 24-31 (last 8 hex chars = last 4 bytes). """ last_8_chars = hash_str[24:32] # Convert hex string to integer (big-endian interpretation) result = int(last_8_chars, 16) return result def build_hash_to_button_map(): """ Build a reverse lookup table mapping MD5 hashes to button IDs. Since the hashes are MD5 of button IDs (0-99), we can pre-compute them. """ hash_map = {} for button_id in range(100): hash_val = compute_md5(str(button_id)) hash_map[hash_val] = button_id return hash_map def find_correct_sequence(): """ Simulate the game logic to find the correct sequence of button clicks. Returns: List of button IDs in the correct order """ print("[*] Building hash-to-button lookup table...") hash_to_button = build_hash_to_button_map() print("[*] Simulating game logic to find correct sequence...") # Start with all expected hashes remaining_hashes = EXPECTED_MD5_HASHES.copy() current_index = 0 correct_sequence = [] step = 0 while remaining_hashes: step += 1 # Get the expected hash at current position expected_hash = remaining_hashes[current_index] # Look up which button ID has this hash if expected_hash not in hash_to_button: print(f"[!] ERROR: Hash {expected_hash} not found in lookup table!") return None button_id = hash_to_button[expected_hash] # Add to sequence correct_sequence.append(button_id) # Remove this hash from remaining remaining_hashes.pop(current_index) # If no more hashes, we're done if not remaining_hashes: break # Calculate next index from the hash next_index_raw = extract_index_from_hash(expected_hash) current_index = next_index_raw % len(remaining_hashes) print(f"[+] Found complete sequence of {len(correct_sequence)} buttons!") return correct_sequence def chacha20_quarter_round(state, a, b, c, d): """ChaCha20 quarter round operation""" state[a] = (state[a] + state[b]) & 0xFFFFFFFF state[d] = ((state[d] ^ state[a]) << 16 | (state[d] ^ state[a]) >> 16) & 0xFFFFFFFF state[c] = (state[c] + state[d]) & 0xFFFFFFFF state[b] = ((state[b] ^ state[c]) << 12 | (state[b] ^ state[c]) >> 20) & 0xFFFFFFFF state[a] = (state[a] + state[b]) & 0xFFFFFFFF state[d] = ((state[d] ^ state[a]) << 8 | (state[d] ^ state[a]) >> 24) & 0xFFFFFFFF state[c] = (state[c] + state[d]) & 0xFFFFFFFF state[b] = ((state[b] ^ state[c]) << 7 | (state[b] ^ state[c]) >> 25) & 0xFFFFFFFF def chacha20_block(key, nonce, counter): """Generate a single ChaCha20 block (64 bytes)""" # ChaCha20 constants: "expand 32-byte k" constants = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574] # Initialize state (16 x 32-bit words) state = constants.copy() # Add key (8 words, little-endian) for i in range(0, 32, 4): state.append(struct.unpack('<I', key[i:i+4])[0]) # Add counter (1 word) state.append(counter & 0xFFFFFFFF) # Add nonce (3 words, little-endian) for i in range(0, 12, 4): state.append(struct.unpack('<I', nonce[i:i+4])[0]) # Save initial state initial_state = state.copy() # Perform 20 rounds (10 double rounds) for _ in range(10): # Column rounds chacha20_quarter_round(state, 0, 4, 8, 12) chacha20_quarter_round(state, 1, 5, 9, 13) chacha20_quarter_round(state, 2, 6, 10, 14) chacha20_quarter_round(state, 3, 7, 11, 15) # Diagonal rounds chacha20_quarter_round(state, 0, 5, 10, 15) chacha20_quarter_round(state, 1, 6, 11, 12) chacha20_quarter_round(state, 2, 7, 8, 13) chacha20_quarter_round(state, 3, 4, 9, 14) # Add initial state keystream = bytearray() for i in range(16): word = (state[i] + initial_state[i]) & 0xFFFFFFFF keystream.extend(struct.pack('<I', word)) return bytes(keystream) def chacha20_decrypt(key, nonce, ciphertext): """Decrypt data using ChaCha20 stream cipher""" assert len(key) == 32, "Key must be 32 bytes" assert len(nonce) == 12, "Nonce must be 12 bytes" plaintext = bytearray() block_counter = 0 offset = 0 while offset < len(ciphertext): # Generate keystream block keystream_block = chacha20_block(key, nonce, block_counter) # XOR ciphertext with keystream chunk_size = min(64, len(ciphertext) - offset) for i in range(chunk_size): plaintext.append(ciphertext[offset + i] ^ keystream_block[i]) block_counter += 1 offset += chunk_size return bytes(plaintext) def decrypt_flag(correct_sequence): """ Use the correct sequence to decrypt the flag. Args: correct_sequence: List of button IDs in order Returns: Decrypted flag as a string """ print("\n[*] Decrypting flag...") # Convert sequence to string (as the game does) sequence_str = str(correct_sequence) print(f"[*] Sequence string: {sequence_str[:100]}...") # Compute SHA256 of the sequence string to get the key key_hash = hashlib.sha256(sequence_str.encode()).hexdigest() print(f"[*] Key (SHA256 hash): {key_hash}") # Convert hex strings to bytes key = bytes.fromhex(key_hash) nonce = bytes.fromhex(CHACHA20_NONCE) ciphertext = bytes.fromhex(ENCRYPTED_FLAG) print(f"[*] Key length: {len(key)} bytes") print(f"[*] Nonce length: {len(nonce)} bytes") print(f"[*] Ciphertext length: {len(ciphertext)} bytes") # Decrypt using ChaCha20 plaintext = chacha20_decrypt(key, nonce, ciphertext) # Convert to ASCII string flag = plaintext.decode('ascii', errors='ignore') return flag def main(): print("=" * 70) print("Cookie Monster CTF Challenge Solver") print("=" * 70) # Step 1: Find the correct sequence correct_sequence = find_correct_sequence() if not correct_sequence: print("[!] Failed to find correct sequence!") return print(f"\n[+] Correct sequence found!") print(f"[*] First 20 buttons: {correct_sequence[:20]}") print(f"[*] Last 20 buttons: {correct_sequence[-20:]}") # Step 2: Decrypt the flag flag = decrypt_flag(correct_sequence) print("\n" + "=" * 70) print("[+] FLAG DECRYPTED:") print(f" {flag}") print("=" * 70) if __name__ == "__main__": main() ``` Well that went better than expected, the encryption uses [ChaCha20](https://en.wikipedia.org/wiki/ChaCha20-Poly1305) (Add-Rotate-XOR). The solver brute-forces the correct sequences of buttons to be pressed, since the script stores the order (Each button number MD5 hashed). The buttons are numbered from 0-99, so they could be pre-computed, after that just a matter of finding the correct sequence. <center> ![image](https://hackmd.io/_uploads/ryiIJ46ael.png) </center> <h4 style="text-align:center"> CTFITB2025{y34_60d07_15_pr377y_345y_70_r3v3r53_tbh_GG} </h4> <hr> <br> <h2 style="text-align:center"> Cryptography </h2> <h3 style="text-align:center"> Easy Hash - 200 </h3> A Cryptography solve? impossible! Not.... Well this one is just pure LLM madness. We're given a python script. ```python #!/usr/bin/env python3 from hashlib import sha256 import base64 import os from libnum import s2n from secret import FLAG assert len(FLAG) == 78 KEY = os.urandom(32) def get_hmac(data: bytes) -> str: return sha256(KEY + data).hexdigest() def parse_token(token: str) -> dict: token_data, token_hmac = base64.b64decode(token).split(b":::hmac=") if get_hmac(token_data) != token_hmac.decode('latin-1'): return None user_data = dict() for part in token_data.decode('latin-1').split(":::"): key, value = part.split("=") user_data[key] = value return user_data def register_user(): name = input("Who are you?\n>>> ") user_data = {"user_id": 0, "name": name, "authorized": "false"} token = ":::".join(f"{key}={value}" for key, value in user_data.items()) secure_token = f"{token}:::hmac={get_hmac(token.encode())}" encoded_secure_token = base64.b64encode(secure_token.encode()).decode('latin-1') print("Your access token: ", encoded_secure_token) def login_user(): user_data = parse_token(input("Enter access token: ")) if user_data is None: print("Unverified login detected :(") return print(f"Hello {user_data['name']}, why don't you stay and relax here? [https://youtu.be/vy63u2hKoPE?si=CI0Fl5xu4sVj2DbK]") def request_secret(): user_data = parse_token(input("Enter access token: ")) if user_data is None: print("Unverified login detected") return if user_data["authorized"] == "true": print("Hmmm looks forged to me...") user_id = int(user_data["user_id"], 2) print(f"I'm not gonna give the secret right away...\n{sha256(FLAG[user_id : user_id + 3].encode()).hexdigest()}") else: print("Uh oh :-(") def main(): while True: print("1. Register\n2. Login \n3. Request Secret ;)\n4. Exit") try: choice = int(input("Enter your choice: ")) if choice == 1: register_user() elif choice == 2: login_user() elif choice == 3: request_secret() elif choice == 4: break else: print("Invalid choice") except Exception: print(f"Oh no...") if __name__ == "__main__": main() ``` Putting it through your favorite LLM (for the nth time) explained that it's vulnerability is on: ```python def get_hmac(data: bytes) -> str: return sha256(KEY + data).hexdigest() ``` A Hash Length Extension Attack, a secure HMAC construction should be resistant to this, but the way it's constructed using SHA256, we can extend the original hash and add our data on it. The solver is 2 files, so here's the [gist](https://gist.github.com/PikaProgram/9c84ca86b42a9450847bb011f1e781ee): we add the property `authorized=true` in order to leak a portion of the flag. Which is repeated until all the flag bytes are leaked, and it's just a matter of reconstructing it. <center> ![image](https://hackmd.io/_uploads/SJRwKV66ll.png) </center> Uhmm, that doesn't seems like a valid flag. Well, time for some good old trial and error, and voila. <h4 style="text-align:center"> CTFITB2025{biar_gak_bosen_number_theory_terus____non_math_lebih_seru_juga_loh} </h4> <br> <h2 style="text-align:center"> Misc </h2> <h3 style="text-align:center"> Irony - 100 </h3> <center> ![image](https://hackmd.io/_uploads/SJ18g4Taxg.png) </center> Funny lore, let's start by search "MrBert" on your favorite search engine. <center> ![image](https://hackmd.io/_uploads/H1K7-EaTxl.png) </center> Stumbled right into his Instagram, let's try `CTFITB2025{Namsun_Bertin}`, aaaaand nope, that's not his full name i guess. Let's dig deeper, maybe by searching "Namsun Bertin". A [news article](https://kumparan.com/kumparanbisnis/waspada-marak-modus-penipuan-pakai-struk-atm-palsu-masyarakat-harus-teliti-1viTDe7mW7k), and what's inside? "Namsum Bertin Sembiring", well that wasn't too hard. <center> ![image](https://hackmd.io/_uploads/SkO3qNT6gg.png) </center> <h4 style="text-align:center"> CTFITB2025{Namsun_Bertin_Sembiring} </h4> <hr> <h3 style="text-align:center"> Pulkam - 285 </h3> <center> ![image](https://hackmd.io/_uploads/SJYKLNapxe.png) </center> Oh no, it's those kind of challenges. Buckle up, this is gonna take a while. [Draw some circles](https://www.mapdevelopers.com/draw-circle-tool.php) <3 <center> ![iamge](https://hackmd.io/_uploads/SJZ6BEaplx.png) </center> After some town lookups and circle adjustments, turns out Pasilihan was the correct one. So let's figure out it's identity. It's on [Sumatera Barat, Indonesia](https://id.wikipedia.org/wiki/Pasilihan,_X_Koto_Diatas,_Solok), The Village Head Name is [Yonhi Nofri](https://www.baritonagarinews.com/2025/09/penyampaian-nota-keuangan-dan-penetapan.html). <center> ![image](https://hackmd.io/_uploads/S1QaiN6agg.png) </center> So we just need to construct the valid flag. And my job here is done. <h4 style="text-align:center"> CTFITB2025{Pasilihan_YonhiNofri_SumatraBarat_Indonesia} </h4> <h2 style="text-align:center"> Afterword </h2> <center> ![https://www.pinterest.com/pin/15340454976921161/](https://hackmd.io/_uploads/BJ1y0Vaagx.jpg) Any lack of concise explanation on any problems should and will be blamed on doing this writeup 3 hours before the Linear Algebra Quiz and on 4 hours of sleep. - CaitViLover. @LiroSphere on Discord, hmu </center>

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password
    or
    Sign in via Facebook Sign in via X(Twitter) Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    By signing in, you agree to our terms of service.

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully