--- title: "Securinets CTF 2025" description: "Writeup for Forensic Challenge" summary: "Writeup for Forensic Challenge" categories: ["Writeup"] tags: ["Forensic"] #externalUrl: "" date: 2025-10-04 draft: false authors: - ducnocrypt cover: "images/post_covers/securinet.jpg" --- ## Silent Visitor This challenge requires us to investigate a disk image file from a Windows computer to understand suspicious activity. The investigation is divided into logical stages, from initial analysis, identifying the attack source, malware analysis, and finally tracing its actions on the system. ``` What is the SHA256 hash of the disk image provided? Input: 122b2b4bf1433341ba6e8fefd707379a98e6e9ca376340379ea42edb31a5dba2 Correct answer Identify the OS build number of the victim’s system? Input: 19045 Correct answer What is the ip of the victim's machine? Input: 192.168.206.131 Correct answer What is the name of the email application used by the victim? Input: thunderbird Correct answer What is the email of the victim? Input: ammar55221133@gmail.com Correct answer What is the email of the attacker? Input: masmoudim522@gmail.com Correct answer What is the URL that the attacker used to deliver the malware to the victim? Input: https://tmpfiles.org/dl/23860773/sys.exe Correct answer What is the SHA256 hash of the malware file? Input: be4f01b3d537b17c5ba7dc1bb7cd4078251364398565a0ca1e96982cff820b6d Correct answer What is the IP address of the C2 server that the malware communicates with? Input: 40.113.161.85 Correct answer What port does the malware use to communicate with its Command & Control (C2) server? Input: 5000 Correct answer What is the url if the first Request made by the malware to the c2 server? Input: http://40.113.161.85:5000/helppppiscofebabe23 Correct answer The malware created a file to identify itself. What is the content of that file? Input: 3649ba90-266f-48e1-960c-b908e1f28aef Correct answer Which registry key did the malware modify or add to maintain persistence? Input: HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\MyApp Correct answer What is the content of this registry? Input: C:\Users\ammar\Documents\sys.exe Correct answer The malware uses a secret token to communicate with the C2 server. What is the value of this key? Input: Input: e7bcc0ba5fb1dc9cc09460baaa2a6986 Correct answer Sahaaaaaaaaaaa Securinets{de2eef165b401a2d89e7df0f5522ab4f} by enigma522 ``` ### Stage 1: Initial Analysis and System Identification The first step in any forensics investigation is gathering basic information about the victim system to establish context. **Q1: What is the SHA256 hash of the disk image file?** Use any tool (e.g., sha256sum on Linux or Get-FileHash on PowerShell) to calculate the hash. **Answer:**`122B2B4BF1433341BA6E8FEFD707379A98E6E9CA376340379EA42EDB31A5DBA2` **Q2: What is the operating system build number?** Analyze the SOFTWARE registry hive. This information is located in the key: `SOFTWARE\Microsoft\Windows NT\CurrentVersion`. ![image](https://hackmd.io/_uploads/HysgyodSZx.png) **Answer:** `19045` Extract the SOFTWARE file FIRST. Load it into Registry Explorer. ![image](https://hackmd.io/_uploads/SytdyoOB-l.png) **Question 3: What is the victim machine's IP address?** Analyze the SYSTEM registry hive. Network configuration is stored at key: `SYSTEM\ControlSet001\Services\Tcpip\Parameters\Interfaces\{GUID}`. **Answer:** `192.168.206.131` ![image](https://hackmd.io/_uploads/rJtKJsOrbl.png) Extract the SYSTEM file first. Load it into Registry Explorer. ### Stage 2: Investigating Entry Point and Attack Vector After obtaining basic information, we trace how the attacker infiltrated the system. Clues typically lie in user activity. **Question 4: Which email application does the victim use?** Check user application folders, especially AppData. During this process, a suspicious file named sys.exe is discovered. When uploaded to VirusTotal, it's confirmed as malware. **Answer:** `thunderbird` **Question 5: What is the victim's email address?** Analyze the Thunderbird profile at `Profiles/6red5uxz.default-release/ImapMail/`. The INBOX file contains all received emails, revealing the victim's address. **Answer:** `ammar55221133@gmail.com` ![image](https://hackmd.io/_uploads/HJQTksOBbl.png) **Question 6: What is the attacker's email address?** Read the INBOX file contents, discovering emails from someone named "mohamed Masmoudi". One of these emails contains a link to a GitHub source code repository, disguised as an educational project. **Answer:** `masmoudim522@gmail.com` **Question 7: What URL did the attacker use to distribute the malware?** Analyze the GitHub repository sent in the email. The package.json file contains a PowerShell script. This script doesn't actually clone the repo but instead downloads sys.exe from another URL. **Answer:** `https://tmpfiles.org/dl/23860773/sys.exe` ![image](https://hackmd.io/_uploads/ryYyesOHZe.png) Access the GitHub link ![image](https://hackmd.io/_uploads/BkJZlj_SZx.png) ![image](https://hackmd.io/_uploads/HJVWejuB-l.png) ### Stage 3: Malware Analysis Now that we have the malware and know its origin, the next stage is analyzing it to understand its behavior. **Question 8: What is the malware file's SHA256 hash?** - **Method:** Calculate the hash of sys.exe file. - **Answer:** `BE4F01B3D537B17C5BA7DC1BB7CD4078251364398565A0CA1E96982CFF820B6D` ![image](https://hackmd.io/_uploads/BJwzlsuB-g.png) **Questions 9 & 10: What are the IP address and port of the C2 (Command & Control) server?** Use dynamic analysis results from VirusTotal or run in a sandbox like Any.run. Reports show the malware (written in Go) connects to a specific IP and port. - **IP Answer:** `40.113.161.85` - **Port Answer:** `5000` ![image](https://hackmd.io/_uploads/B1QXliuB-x.png) **Question 11: What is the URL of the first request the malware sends to C2?** Analyze network protocol logs from Any.run sandbox. **Answer:** `http://40.113.161.85:5000/helppppiscofebabe23` ![image](https://hackmd.io/_uploads/B1ZEes_Bbl.png) ### Stage 4: Post-Infection Artifacts & Persistence Mechanisms Finally, we investigate changes the malware made to the system to ensure persistent access. **Question 12: The malware creates an identification file. What is its content?** VirusTotal reports show the malware creates id.txt file at `C:\Users\Public\Documents`. We return to the disk image and read this file's contents. **Answer:** `3649ba90-266f-48e1-960c-b908e1f28aef` ![image](https://hackmd.io/_uploads/Bk8Ilj_BZl.png) **Question 13: Which Registry key did the malware modify/add to maintain access?** VirusTotal shows the malware creates an entry in the Registry Run key, a common technique for automatic startup with Windows. **Answer:** `HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\MyApp` ![image](https://hackmd.io/_uploads/ryy_xoOSWe.png) **Question 14: What is the content of this Registry key?** Extract the NTUSER.DAT registry hive (corresponding to HKEY_CURRENT_USER) and read the MyApp key value. **Answer:** `C:\Users\ammar\Documents\sys.exe` **Question 15: The malware uses a "secret token" to communicate with C2. What is its value?** Use simple static analysis with the strings command on sys.exe file and filter (grep) with the keyword "secret". **Answer:** `e7bcc0ba5fb1dc9cc09460baaa2a6986` ![image](https://hackmd.io/_uploads/rkrFgidHWe.png) ## Lost File ![image](https://hackmd.io/_uploads/rJbBMouH-e.png) ### Stage 1: Static Analysis of Executable File (locker_sim.exe) The first step is understanding how the executable operates to determine its encryption mechanism. **Question 1: How does the executable work?** - **Purpose:** Understand the encryption program's logic. - **Method:** Use a decompiler tool like IDA to analyze the C source code of locker_sim.exe. - **Source Code Analysis:** 1. The program receives an input parameter from the command line (argv[1]). 2. It reads the computer name (hostname) from the Registry. 3. It reads the contents of a file named secret_part.txt, then **deletes this file**. 4. All three pieces of information are combined into a single string in the format: `<command line parameter>|<computer name>|<secret file content>`. 5. This string is then used to generate the encryption key. **Question 2: How is the encryption key generated?** - **Purpose:** Identify the algorithm and source data for key generation. - **Method:** Based on source code analysis from IDA. - **Result:** The program uses the **SHA256** algorithm to hash the combined string above. This hash result is the key used for AES-256 encryption. The first 16 bytes of the hash are used as the IV (Initialization Vector). **Question 3: Which file is encrypted and what is the result?** - **Purpose:** Identify the target and output of the encryption process. - **Method:** Source code analysis. - **Result:** The program encrypts the file to_encrypt.txt using **AES-256** algorithm (CBC mode) and saves the result as to_encrypt.txt.enc. ### Stage 2: Memory Dump Analysis (mem.vmem) Since secret_part.txt has been deleted and we don't know the command line parameter that was used, we need to analyze the memory dump file to retrieve this information. **Question 4: What command line parameter (argv[1]) was used?** - **Purpose:** Find the first piece of the key generation string. - **Method:** Use Volatility with the consoles plugin to review commands executed in the command line interface. ```bash python2 volatility/vol.py -f mem.vmem --profile=WinXPSP2x86 consoles ``` - **Result:** `hmmisitreallyts` ![image](https://hackmd.io/_uploads/H1x7uGo_BZg.png) **Question 5: What is the victim's computer name (hostname)?** - **Purpose:** Find the second piece of the key generation string. - **Method:** Use Volatility with the envars plugin to extract environment variables, which contain the hostname. - **Command:** ```bash python2 volatility/vol.py -f mem.vmem --profile=WinXPSP2x86 envars ``` - **Result:** `RAGDOLLF-F9AC5A` ![image](https://hackmd.io/_uploads/S1wFfsuSWe.png) ### Stage 3: Recovering Deleted File The final piece is the content of the secret_part.txt file that was deleted by the program. **Question 6: What is the content of secret_part.txt?** - **Purpose:** Find the final piece of the key generation string. - **Method:** Since the file was deleted, it's likely still in the system's Recycle Bin. Analyze the disk image (if available) or use file recovery tools to retrieve this content. - **Result:** `sigmadroid` ### Stage 4: File Decryption and Flag Recovery With all three pieces of information: command line parameter, hostname, and secret file content, we can reconstruct the key and decrypt the file. **Question 7: How is the decryption script written?** ```python import hashlib from Crypto.Cipher import AES from Crypto.Util.Padding import unpad def derive_key(arg, computername, secret_part): data = f"{arg}|{computername}|{secret_part}" sha = hashlib.sha256(data.encode()).digest() return sha def decrypt_file(enc_path, out_path, arg, computername, secret_part): key = derive_key(arg, computername, secret_part) iv = key[:16] cipher = AES.new(key, AES.MODE_CBC, iv) with open(enc_path, "rb") as f: ciphertext = f.read() plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size) with open(out_path, "wb") as f: f.write(plaintext) print(f"[+] Decrypted -> {out_path}") print(f"Content: {plaintext.decode()}") decrypt_file( "to_encrypt.txt.enc", "to_encrypt_decrypted.txt", arg="hmmisitreallyts", computername="RAGDOLLF-F9AC5A", secret_part="sigmadroid" ) ``` **Question 8: What is the final flag?** - **Method:** Run the decryption script and process the resulting string. - **Decrypted content:** `Vm14U1MxWXlSblJWYkd4VVltdEtjRmxzV2xwa01XdzJWR3BDYkdKSGREWlZNakUwV1ZaYU5sVnViRnBOYWtaWVdXMHhSMWRXVW5GUmJYQnBZbGhTTlZkWGVHdFpWVEZIVVdwYVVGWkhjems9` - **Processing:** The above string is Base64 encoded. After Base64 decoding, we obtain the final flag. - **Flag:** `Securinets{screen+registry+mft??}` ![image](https://hackmd.io/_uploads/B1ZbDs_HWl.png) ## Recovery We had 2 files: a pcapng file and a backup. First, I looked through the backup: ![image](https://hackmd.io/_uploads/rk3wDouSWl.png) When I opened files, I could not read since they were encrypted although their name looked normal: ![image](https://hackmd.io/_uploads/rJ5Ows_Bbe.png) From here I read content of powershell_history.txt for more information and I noticed a github repo: ![image](https://hackmd.io/_uploads/H1AtDjurbl.png) It looked so suspicious so I accessed this repo. Read app.py and this was result: ![image](https://hackmd.io/_uploads/r1T5DidBbe.png) I checked commit to see file history and I found many things interesting, especially DNS exfiltration which used domain **meow**. To confirm this information I opened **Wireshark** and fortunately it's correct: ![image](https://hackmd.io/_uploads/H1ooPs_S-l.png) When we solved, we found the dns6 commit contained the correct decryption method for this case, and I rewrote the script for decryption: ```python import argparse import base64 import os from collections import defaultdict from dnslib import DNSRecord from scapy.all import PcapReader, UDP def xor_bytes(data_bytes, key_byte): """XOR every byte with a single-byte key.""" return bytes([b ^ key_byte for b in data_bytes]) def padded_base32(s: str) -> bytes: """Pad a base32 string to a multiple of 8 and decode.""" # base32 expects padding with '=' to a multiple of 8 pad_len = (8 - (len(s) % 8)) % 8 s_padded = s + ("=" * pad_len) return base64.b32decode(s_padded, casefold=True) def process_dns_qname(qname: str, special_domain: str = "meow"): """ If query name matches the pattern chunk.index.meow... (i.e. labels[0]=chunk, labels[1]=index, labels[2]=meow), return (index:int, chunk_bytes:bytes). Otherwise return None. """ labels = qname.rstrip(".").split(".") if len(labels) < 3: return None # We expect the third label to be the special domain according to user's format if labels[2].lower() != special_domain.lower(): return None chunk_label = labels[0] index_label = labels[1] # special-case: end.<something>.meow (original script used labels[0]=="end") if chunk_label.lower() == "end": try: # if index present use it, otherwise -1 idx = int(index_label) if index_label.isdigit() else -1 except Exception: idx = -1 return ("__END__", idx) # otherwise try to decode try: decoded = padded_base32(chunk_label) if len(decoded) < 1: return None key_byte = decoded[0] encrypted_chunk = decoded[1:] original = xor_bytes(encrypted_chunk, key_byte) index = int(index_label) return (index, original) except Exception: return None def extract_from_pcap(pcap_path: str, out_path: str, special_domain: str = "meow", verbose: bool = True): """ Iterate through pcapng, parse DNS queries and collect chunks. When an 'end' marker is found, reconstruct file and write to out_path. """ chunks = dict() seen_indices = set() end_seen = False if verbose: print(f"[+] Opening pcap file: {pcap_path}") total_packets = 0 dns_packets = 0 with PcapReader(pcap_path) as reader: for pkt in reader: total_packets += 1 # Filter UDP DNS queries (port 53) - both src or dst 53 possible depending on capture direction if not pkt.haslayer(UDP): continue udp = pkt[UDP] sport = int(udp.sport) if hasattr(udp, "sport") else None dport = int(udp.dport) if hasattr(udp, "dport") else None if sport != 53 and dport != 53: continue # get raw UDP payload (may be DNS) try: raw = bytes(udp.payload) if not raw: continue # parse DNS packet using dnslib for robustness try: dns = DNSRecord.parse(raw) except Exception: continue # only process queries (QR=0) and at least one question if dns.header.get_qr() != 0 or len(dns.questions) == 0: continue qname = str(dns.q.qname) dns_packets += 1 result = process_dns_qname(qname, special_domain=special_domain) if result is None: continue if isinstance(result, tuple) and result[0] == "__END__": end_seen = True if verbose: idx = result[1] print(f"[+] Found END marker (index={idx}) at packet #{total_packets}, qname={qname}") # do not break; keep scanning to collect all chunks (pcap might have chunks after end marker) continue index, data = result if index in chunks: # if duplicate, skip or optionally prefer first seen if verbose: print(f"[*] Duplicate chunk index {index} encountered; skipping duplicate.") else: chunks[index] = data seen_indices.add(index) if verbose: print(f"[+] Collected chunk index={index}, len={len(data)} qname={qname}") except Exception as e: if verbose: print(f"[!] Failed to process packet #{total_packets}: {e}") continue if verbose: print(f"[+] Finished scanning pcap: total pkts={total_packets}, DNS-like pkts={dns_packets}") print(f"[+] Collected {len(chunks)} chunks, end_seen={end_seen}") if not chunks: raise RuntimeError("No valid meow chunks found in pcap.") # Reconstruct ordered by index (lowest to highest) ordered_indices = sorted(chunks.keys()) # Check for missing indices (optional) min_idx = ordered_indices[0] max_idx = ordered_indices[-1] missing = [i for i in range(min_idx, max_idx + 1) if i not in chunks] if missing and verbose: print(f"[!] Warning: missing chunk indices between {min_idx} and {max_idx}: {missing}") reconstructed = b"".join(chunks[i] for i in ordered_indices if i in chunks) # Write to disk out_dir = os.path.dirname(out_path) if out_dir and not os.path.exists(out_dir): os.makedirs(out_dir, exist_ok=True) with open(out_path, "wb") as f: f.write(reconstructed) if verbose: print(f"[+] Reconstructed file written to: {out_path} (size={len(reconstructed)} bytes)") print("[!] Note: this script does NOT execute the file. If you need to run it, do so manually in a safe, isolated environment (VM).") return out_path, len(reconstructed), missing def main(): ap = argparse.ArgumentParser(description="Extract meow DNS exfil chunks from pcapng and reconstruct file.") ap.add_argument("pcap", help="Path to pcapng / pcap file") ap.add_argument("-o", "--out", help="Output file path", required=True) ap.add_argument("--domain", help="Special domain label (default: meow)", default="meow") ap.add_argument("--noisy", help="Verbose output", action="store_true") args = ap.parse_args() try: out_path, size, missing = extract_from_pcap(args.pcap, args.out, special_domain=args.domain, verbose=args.noisy) print(f"Done. Wrote {size} bytes to {out_path}. Missing indices: {missing}") except Exception as e: print(f"Error: {e}") if __name__ == "__main__": main() ``` I ran the code and got a packed executable file Simply I unpacked it and used IDA Pro again: ![image](https://hackmd.io/_uploads/B1nAwsOSWe.png) I searched and found the function for encrypting files: ![image](https://hackmd.io/_uploads/B1vbOiuSbl.png) You could see that they used a simple XOR operation for encryption. But we need to know exactly how they implemented their encryption method. Next we will dig into **sub_401460** which processed the **Filename** for something: ```asm int __cdecl sub_401460(const char *a1, int a2, int a3) { int v3; // edx int v4; // ebx unsigned int v5; // kr04_4 char v6; // cl int v7; // esi int i; // eax int v9; // ebx char v10; // cl int result; // eax v3 = 0; v4 = 0; v5 = strlen(a1) + 1; while ( v4 != v5 - 1 ) { v6 = 8 * (v4 & 3); v7 = a1[v4++]; v3 ^= v7 << v6; } for ( i = 0; i != 37; ++i ) { v9 = byte_40B200[i]; v10 = i; v3 ^= v9 << (8 * (v10 & 3)); } for ( result = a2; result != a2 + a3; *(_BYTE *)(result - 1) = v3 ) { ++result; v3 = 1664525 * v3 + 1013904223; } return result; } ``` **sub_401460** takes a string a1, a buffer address a2, and a length a3, and uses the string to produce a deterministic stream of pseudorandom bytes written into the buffer. It begins by building a 32-bit seed v3 from the input string: each character is XORed into v3 at byte-aligned positions (cycling through shifts of 0, 8, 16, 24 bits), then the seed is further mixed by XORing in 37 bytes from **byte_40B200**. That mixed value becomes the initial state for a standard linear congruential generator (v3 = 1664525 * v3 + 1013904223), and the routine iterates the LCG to produce a3 bytes, storing the low byte of the LCG state sequentially into the buffer at a2. To know what 37 bytes string was, we just simple click on the variable and we can see the content: ![image](https://hackmd.io/_uploads/r1Ofus_S-g.png) Because filename was an important part of seeding process, giving correct filepath is very essential and just a small modification will change the seed. And fortunately this function below gave me how the filepath looked like: ```asm void *__cdecl sub_4015FD(char *a1) { void *result; // eax void *v2; // edi int v3; // eax const char *Str1; // ebx _stat32 Stat; // [esp+2Ch] [ebp-43Ch] BYREF char FileName[1048]; // [esp+50h] [ebp-418h] BYREF result = (void *)sub_403A60(a1); if ( result ) { v2 = result; while ( 1 ) { v3 = sub_403C20(v2); if ( !v3 ) break; Str1 = (const char *)(v3 + 12); if ( strcmp((const char *)(v3 + 12), ".") ) { if ( strcmp(Str1, "..") ) { if ( strcmp(Str1, "AppData") ) { sub_4023B0(FileName, 1024, "%s\\%s", a1, Str1); if ( stat(FileName, &Stat) != -1 ) { if ( (Stat.st_mode & 0xF000) == 0x4000 ) { sub_4015FD(FileName); } else if ( (Stat.st_mode & 0xF000) == 0x8000 ) { sub_4014D1(FileName); } } } } } } return (void *)sub_403C70(v2); } return result; } ``` In short, the filepath will use double backslash, filepath will be put into the seeding. So this is my Python script for decryption: ```python import sys import os SECRET = b"evilsecretcodeforevilsecretencryption" A = 1664525 C = 1013904223 MASK32 = 0xFFFFFFFF BLOCK_SIZE = 64 * 1024 # 64 KiB def build_seed_from_filename(filename: str) -> int: """ Recreate seed from the original malware logic: full = "C:\\Users\\gumba\\Desktop\\" + filename seed = 0 for each byte in full: seed ^= (byte << ((i % 4) * 8)) for each byte in SECRET: seed ^= (byte << ((i % 4) * 8)) """ full = "C:\\Users\\gumba\\Desktop\\" + filename fb = full.encode("utf-8", errors="replace") seed = 0 for i, b in enumerate(fb): seed ^= (b & 0xFF) << ((i & 3) * 8) seed &= MASK32 for i, b in enumerate(SECRET): seed ^= (b & 0xFF) << ((i & 3) * 8) seed &= MASK32 return seed & MASK32 def keystream_generator_for_filename(filename: str): """ Generate keystream bytes for the given filename. Update LCG state first, then output (state & 0xFF). """ state = build_seed_from_filename(filename) while True: state = (state * A + C) & MASK32 yield state & 0xFF def decrypt_file(filename: str): if not os.path.isfile(filename): print(f"[!] File not found: {filename}") return 1 size = os.path.getsize(filename) if size == 0: print(f"[!] Empty file, skipping: {filename}") return 1 keystream = keystream_generator_for_filename(filename) outname = f"decrypted_{filename}" key_preview = bytearray() with open(filename, "rb") as inf, open(outname, "wb") as outf: while True: block = inf.read(BLOCK_SIZE) if not block: break out_block = bytearray(len(block)) for i, b in enumerate(block): k = next(keystream) out_block[i] = b ^ k if len(key_preview) < 64: key_preview.append(k) outf.write(out_block) print(f"[+] Decrypted {filename} -> {outname} (size={size} bytes)") print(f"[+] Keystream preview (first {len(key_preview)} bytes): {key_preview.hex()}") return 0 def main(): if len(sys.argv) != 2: print(f"Usage: {sys.argv[0]} <filename>") sys.exit(1) filename = sys.argv[1] decrypt_file(filename) if __name__ == "__main__": main() ``` Run with the filename and you got the flag: ![image](https://hackmd.io/_uploads/SJD7djuB-l.png) That's my writeup for all forensic challenges. Thank you for reading my blog, see you in the next post. Byeeee!!!