Back to posts
HolaCTF 2025 - Journal
ForensicsMediumpowershellwiresharkidawriteup

HolaCTF 2025 - Journal

h26v
01/09/2025
15 min read

Forensics

APT

image

image

Q1: The user opened a software that contained malicious content from the attacker. What was the name of the software? Ex: Skype

image

Walking through Application.evtx we can see

thunderbird.exe was captured by Windows Error Reporting

Ans: Thunderbird

Q2: What is the name of the file that makes user fall into the attacker's trap? Ex: example.js

image

Here, I found an attacker email with a suspicious attachment "diemchuan.zip". I tried to submit this but it failed. Then I thought of another way.

image

Rummaging through the mess of files I found that the attachments were saved under Mail/Inbox. Using DBViewer we identified the received file was stored as base64.

image

Decoding it we obtained a zip file.

image

Just open the zip file and we have a malicious html file here.

Ans: diemchuan.html

Q3: What MITRE ATT&CK techniques did the attacker use? Ex: T1234.00

This html file impersonates the Word interface to trick the user into viewing hidden content, but in reality it triggers embedded malicious code.

image

image

It looks verbose but it's actually just reverse and base64.

image

From here we can see the query that will be executed after clicking the "Cloud View" button. Clearly this payload leverages the WebDAV attack vector to make the user think the file popped by Explorer is a file on their machine. That enables subsequent payloads to execute.

Ans: T1566.001

Q4: What is the domain of the website where the malicious file for stage 2 is located? Ex: mediafire.com

When I reached this step the author had already taken down the ngrok server hosting the WebDAV site, so I found another way to retrieve stage 2. Looking at the PowerShell logs I saw suspicious commands with parameters like Hidden and Bypass.

image

Ans: sakamoto.moe

Q5: Digging into the malicious file, what was the original name of the encrypted file that was downloaded? Ex: payload.enc

Analyzing the loader in IDA, the program's control flow is a bit nonsensical. With a closer look I noticed that aside from the main flow there is a Run path that will be jumped to if the right order is provided.

image

This seems to be what we need. The summarized workflow is:

  • The hardcoded string (URL) in the code is XOR-decoded (10,13).
  • Use WinINet API to download stage 3 which is encrypted and save to C:\\Users\\public\\Miyamizu_Mitsuha.yourname
  • Deploy RC4 algorithm to decrypt stage 3
  • Execute stage 3 and clean up remnants

image

image

image

You can get the decrypted URL easier by using floss

image

Ans: steam.enc

Q6: What is the encryption algorithm and key to decrypt the encrypted payload? (Convert it to hex) Ex: AES_0x1a,0x2b,0x3c,0x4d,...

Based on the workflow above, I identified &unk_33E154020 as the key and v3 as the ciphertext.

C
RC4(&unk_33E154020, 16LL, hMem, v3);

image

Ans:

bash
RC4_0x3A,0x2D,0x1C,0x4D,0x5E,0x2F,0x7B,0x81,0x3D,0xAB,0xBC,0xCD,0xDE,0x2F,0xF0,0x01

Q7: In stage 3, which C2 IP address is the user's network traffic transmit to? Ex: 192.168.1.1

When I loaded stage 3 into Detect It Easy, the language was detected as C++ (by muscle memory I immediately threw it into IDA and spent 30 minutes reading without understanding much 😀). But when I glanced at the string view I saw strings like .NET, .NET compiler,... From there I suspected a wrapper, so I tried dnSpy but it didn't recognize it (damn dnSpy).

image

Using dotPeek made everything clear; dnSpy is too old for this wrapper (damn dnSpy).

image

Skimming the code I found everything obfuscated and there was a "decoding table" at the bottom.

image

Using that we can deobfuscate.

python
import base64 dictionary = { "0": "\uD83C\uDF7A", "1": "\uD83D\uDE82", "2": "\uD83D\uDC0F", "3": "⚽", "4": "\uD83C\uDE3A", "5": "⛵", "6": "\uD83D\uDD3C", "7": "\uD83C\uDF4D", "8": "\uD83C\uDFA5", "9": "\uD83D\uDEBE", "A": "\uD83D\uDC3D", "B": "\uD83C\uDF0A", "C": "\uD83D\uDCAC", "D": "\uD83C\uDFAD", "E": "\uD83C\uDF06", "F": "\uD83D\uDD0E", "G": "\uD83D\uDC7B", "H": "\uD83C\uDE51", "I": "\uD83D\uDC9B", "J": "\uD83C\uDD7F️", "K": "\uD83C\uDDEA\uD83C\uDDF8", "L": "\uD83D\uDC9C", "M": "\uD83D\uDC36", "N": "\uD83C\uDF8F", "O": "\uD83D\uDC60", "P": "\uD83C\uDF83", "Q": "\uD83C\uDF75", "R": "\uD83D\uDDFF", "S": "\uD83D\uDEB9", "T": "\uD83D\uDD28", "U": "\uD83D\uDD1C", "V": "\uD83D\uDCAF", "W": "ℹ️", "X": "\uD83D\uDCC9", "Y": "\uD83C\uDE01", "Z": "⏩", "a": "9️⃣", "b": "\uD83D\uDD01", "c": "\uD83D\uDCD3", "d": "\uD83D\uDC23", "e": "\uD83D\uDC58", "f": "\uD83C\uDF6F", "g": "\uD83D\uDCBB", "h": "\uD83D\uDE8F", "i": "\uD83D\uDC8B", "j": "\uD83C\uDFA9", "k": "\uD83C\uDF63", "l": "\uD83D\uDC5E", "m": "\uD83D\uDD38", "n": "\uD83C\uDF7B", "o": "\uD83D\uDC9A", "p": "\uD83C\uDF87", "q": "\uD83D\uDE9B", "r": "\uD83D\uDC88", "s": "\uD83C\uDFC8", "t": "\uD83D\uDE0D", "u": "\uD83D\uDEB4", "v": "\uD83D\uDCBF", "w": "\uD83D\uDC8F", "x": "✒️", "y": "\uD83C\uDF46", "z": "\uD83C\uDF67", "_": "\uD83D\uDD52", "-": "\uD83D\uDC4C", "/": "\uD83D\uDC2E", "+": "\uD83D\uDC2D", "=": "\uD83D\uDD0C" } def decode_unicode_string(unicode_str, unicode_to_char): result = "" i = 0 unicode_keys = sorted(unicode_to_char.keys(), key=lambda x: -len(x)) while i < len(unicode_str): matched = False for code in unicode_keys: if unicode_str[i:i+len(code)] == code: result += unicode_to_char[code] i += len(code) matched = True break if not matched: result += "?" i += 1 return base64.b64decode(result).decode('utf-8') unicode_to_char = {v: k for k, v in dictionary.items()} input_unicode = "\ud83d\udc9b\ud83d\udc3d\ud83d\udd0c\ud83d\udd0c" print(decode_unicode_string(input_unicode, unicode_to_char))

The deobfuscated code was easier to read and we can analyze it.

Continuing the code I found the function:

image

Ans: 192.168.244.129

Q8: The attacker made a mistake, take advantage of it and tell us the name of files encrypted by attacker's ransomware in folder Videos ? Ex: ehc.mp3

At this point I had to research quite a bit. In the code there are functions named ddAxJkAciSG, lSJNJosACuzaDIkQoaEIkvSckaEPgrBKmY, ... calling cryptographic and hashing algorithms like RSA, SHA256, MD5, AES. I'll note them and investigate further to see what they do.

image

Earlier I noticed there's a pcap file in temp, let's open it and see what's interesting.

image

There are TLS v1.2 encrypted data streams sent out, which matches the C2 IP found earlier.

image

Researching a bit I know TLS v1.2 used to use RSA key exchange, where the public key is used to encrypt and then the private key on the server decrypts. Looking back at the code we can see the public key is hardcoded.

image

Dumping the cert, I found the public key is only 1024 bits; at this size RSA is weak if p and q are close or primes are reused. In a CTF case it will likely be weak to allow further progress.

image

ps
RsaCtfTool --publickey '/home/kali/Desktop/cert.pem' --private

image

With the private key we can add it into Wireshark to read the previously encrypted packets.

image

I found the header of a zip file inside a POST packet.

image

image

To know the password for the zip we need to look back at the payload. At a glance you might think "there's no -p parameter?" — you'd be wrong =))) I didn't expect -parameter-none to be the -p <pass> parameter written without a space to fool solvers.

image

image

Ans: video.mp4

Q9: What are the contents of the file flag.txt? (Convert the content of the flag.txt to md5 and submit) Ex: 4a34decdd3494446ff0546364aa975b5

The data is encrypted by the following two functions:

image

It can be understood that the data is Custom XOR (3 Rounds) with a key, and the trick is the key is generated during runtime. Based on the properties of XOR we can reverse the process as:

key = cipher ^ plaintext

I also found the leftover file video.mp4 in temp (this approach is easy but likely not how the author intended).

python
def inverse_transform_key_byte(b: int, rounds: int = 3) -> int: for _ in range(rounds): b = (b - 37) & 0xFF b = b ^ 0xA5 return b def recover_key(cipher: bytes, plain: bytes, rounds: int = 3) -> bytes: if len(cipher) != len(plain): raise ValueError("Ciphertext and plaintext must be the same length") recovered = [] for i in range(len(cipher)): tkey = cipher[i] ^ plain[i] # key after transform key_byte = inverse_transform_key_byte(tkey, rounds) recovered.append(key_byte) return bytes(recovered) if __name__ == "__main__": # Read encrypted file and original file with open("xrYvsozYaitKuzsiktrX.EHC", "rb") as f: cipher_data = f.read() with open("video.mp4", "rb") as f: plain_data = f.read() # Recover key key_length = 32 recovered = bytes(recover_key(cipher_data, plain_data)) print(recovered.hex()) rec = recovered[:key_length] print("[+] Key (hex):", rec.hex())

Once we have the key we can decrypt the remaining files:

python
import os def transform_key_byte( b, rounds=3): for _ in range(rounds): b = ((b ^ 165) + 37) & 0xff return b def decrypt_file_data(encrypted_data, key, rounds=3): decrypted = bytearray(len(encrypted_data)) key_len = len(key) for i in range(len(encrypted_data)): transformed_key_byte = transform_key_byte(key[i % key_len], rounds) decrypted[i] = encrypted_data[i] ^ transformed_key_byte return bytes(decrypted) key = bytes.fromhex("15a60be5edf31a095afec7b17f06fe98c4230a3e264ca54c7e99b14bc2cfaf6c") def decrypt_file(input_file, output_file, key, rounds=3): try: with open(input_file, 'rb') as f: encrypted_data = f.read() decrypted_data = decrypt_file_data(encrypted_data, key, rounds) with open(output_file, 'wb') as f: f.write(decrypted_data) print(f"Successfully decrypted {input_file} to {output_file}") return True except Exception as e: print(f"Error decrypting file: {e}") return False if __name__ == "__main__": inp = "TYTUTtTtkPuXsZaiwiKa.EHC" out = inp.rsplit('.', 1)[0] + "_dec." + inp.rsplit('.', 1)[1] if os.path.exists(inp): decrypt_file(inp, out, key, rounds=3) else: print(f"Input file {inp} not found")

image

Ans: 0e2c58ea5f51647dc2f81a03b43b6580

Flag:

bash
HOLACTF{Dun6_6A0_GlO_cIick_V4O_StRaNgE_FiLe_NH3_aHUHU_3fea6408058f}

#vlquadetmq