CertCTF 2025
I had the pleasure of participating in CertCTF, organized as part of a bachelor’s thesis in digital forensics at Halmstad University in collaboration with MSB and CERT-SE. As someone who had never “competed” in a CTF before but had worked through various Hack The Box, TryHackMe, and OverTheWire challenges, CertCTF was incredibly fun and educational.
I also took the opportunity to document the flags I managed to collect. Below is a quick and braindump I compiled during the event. Unfortunately, I didn’t capture all the flags I had hoped for, but Iām satisfied with placing 11th out of 61 participants. Not bad for a first time! š
When the CTF started, we received a zip file containing:
- A network dump from the attack
- Windows logs from a compromised server
- A memory dump from the compromised server
Also included was the scenario for the CTF, detailed below.
Intro
The original message was recieved in Swedish, I’ve transelated it into english.
Gentle Dentalās IT manager, Micke, had just finished his 45-minute coffee break, during which he and the clinicās head dentist had been discussing expensive cybersecurity solutions that the boss deemed unnecessary. As Micke sat down at his desk, the dentist, Per, mentioned that the printer was acting strangely.
Upon investigating, Micke noticed a small computer plugged into the Ethernet port that the printer normally used. Panicking, he realized something terrible might have happened. He called you, an expert in IT attacks, to help since Gentle Dental has some important, high-profile clients.
You received all network traffic that occurred during Mickeās coffee break on the companyās network, along with event logs and a memory dump from their file server. Your mission: investigate the IT attack and determine what happened.
Attackerās IPv4 Address
What IPv4 address did the attacker initially use in the attack?
Answer format: 111.222.333.444
I noticed an unusual number of ARP requests from a specific address, clearly scanning the network for responsive hosts.
Flag: 192.168.177.141
Domain Controllerās IPv4 Address
What IPv4 address belonged to the domain controller?
Answer format: 111.222.333.444
Domain controllers often communicate via LDAP. Searching the network dump for LDAP traffic, I found that 192.168.177.129 was talking over port 389 and was most likely the domain controller.
Flag: 192.168.177.129
FTP Serverās IPv4 Address
What IPv4 address belonged to the FTP server?
Answer format: 111.222.333.444
FTP typically uses port 21. Searching the network dump for traffic on port 21, I saw that the attacker’s server tried to communicate with several IP addresses, but only 192.168.177.155 responded.
Flag: 192.168.177.155
Attackerās Hostname
What was the attackerās hostname?
This was trickier but still straightforward. Filtering the network dump for the attacker’s server revealed a DHCP request, which included the hostname under Option 12.
Flagga: Kali
Timestamp of Last Packet
What was the local Swedish time when the last packet in the network traffic recording was received? Round down to the nearest whole second.
Answer format: HH:MM:SS
In Wireshark, I set the timestamp to local time and scrolled to the end of the network dump.
Flag: 09:42:01
Windows Defender Modification
Which MpPreference parameter did the attacker modify for Windows Defender Antivirus?
Answer format: ParameterName
Looking through the Windows Defender logs, I found that the registry value:
HKLM\SOFTWARE\Microsoft\Windows Defender\Exclusions
was altered, which corresponds to modifying:
Get-MpPreference | Select-Object ExclusionPath
Flag: ExclusionPath
Unauthorized Login
The attacker managed to log into one of the machines. What was the Logon ID for this unauthorized login?
Answer format: 0x123AB
Checking the Security logs for the attacker’s local IP (192.168.177.141), I found a successful login eventājackpot!
Flag: 0xFFCAB
Attackerās Password
What password did the attacker use to log in during the "Unauthorized Login" challenge?
Answer format: Password123
Based on the log in Unauthorized Login we know the time of login and that the authentication was done with NTLM V2. NTLM V2 is known to be weak, so we should be able to crack it. We also have access to the network dump of the authentication.
To crack NTLMV2 you need to know: User, Domain, Challenge, NTLMV2Response and HMAC-MD5.
Challenge can be found in Session Setup Response for NTLM authentication.
Challenge: NTLM Server Challenge: 19c218f2f9a912c1
HMAC-MD5 and NTLMv2Response can be found in the authentication.
NTProofStr is the same as HMAC-MD5
HMAC-MD5: 7db9780888f4832bd162cf679cbffc0e
If you scrolldown you can also observe user and domain.
NTLMv2Response: 010100000000000080bf6de99591db01f7bccb719a3b1d400000000002001800470045004e0054004c004500440045004e00540041004c0001001400460054005000530045005200560049004300450004002400670065006e0074006c006500640065006e00740061006c002e006c006f00630061006c0003003a0046005400500053006500720076006900630065002e00670065006e0074006c006500640065006e00740061006c002e006c006f00630061006c00070008001829aae99591db010000000000000000
Add all this together to a format that hascaat can use.
FTPService::gentledental.local:19c218f2f9a912c1:7db9780888f4832bd162cf679cbffc0e:010100000000000080bf6de99591db01f7bccb719a3b1d400000000002001800470045004e0054004c004500440045004e00540041004c0001001400460054005000530045005200560049004300450004002400670065006e0074006c006500640065006e00740061006c002e006c006f00630061006c0003003a0046005400500053006500720076006900630065002e00670065006e0074006c006500640065006e00740061006c002e006c006f00630061006c00070008001829aae99591db010000000000000000
Use wordlist of your choice, i used rockyou
Using Hashcat:
hashcat.exe -a0 -m5600 hashcatcrack.txt rockyou.txt
a0 specifies straight mode (wordlist attack).
m5600 specifies the NetNTLMv2 hash type.
hashcatcrack.txt is our compiled hash.
rockyou.txt is our wordlist.
Success!
Session..........: hashcat
Status...........: Cracked
FTPSERVICE::gentledental.local:19c218f2f9a912c1:7db9780888f4832bd162cf679cbffc0e:010100000000000080bf6de99591db01f7bccb719a3b1d400000000002001800470045004e0054004c004500440045004e00540041004c0001001400460054005000530045005200560049004300450004002400670065006e0074006c006500640065006e00740061006c002e006c006f00630061006c0003003a0046005400500053006500720076006900630065002e00670065006e0074006c006500640065006e00740061006c002e006c006f00630061006c00070008001829aae99591db010000000000000000:DentalSurgery528
Password can be found at the end of the string.
Flag: DentalSurgery528
Attackerās Server
What was the attackerās own serverās IPv4 address?
Answer format: 111.222.333.444
PowerShell logs showed a script modifying DNS DoH settings, which, combined with suspicious DNS traffic, suggested data exfiltration via DNS.
$script:ClassName = 'ROOT/StandardCimv2/MSFT_DNSClientDohServerAddress'
Unusual traffic from the FTP server to an unknown IP led me to suspect it was the attacker’s server.
Flag: 10.245.122.37
Ransomware
Apart from the “regular” challenges, there was also a sub-challenge related to ransomware. The scenario is described below.
Your colleague, who was responsible for disk forensics, found an executable file and three other text files. One file containing sensitive data now appears to contain some form of encrypted data. Your colleague is fairly certain that the sensitive file can be decrypted and comes to you for help, as you are an expert in areas such as reverse engineering and cryptography.
What is the key to decrypt the file?
Along with the introduction, the following files were provided:
- ransom_note.txt
- pubkey.txt
- sensative_data.encrypted
- enc.exe
The file enc.exe was used to encrypt the data.
pubkey.txt contained a public key: 506395958
sensative_data.encrypted contained encrypted data in the format:
(540488803, 187826906) (512275051, 36345508)...
If you open enc.exe in Ghidra and dig around a bit, you can find the function below, which is likely used to generate the public key.
After a lot of Googling and a bit of AI chatting, I got the following answer, which seemed reasonable:
void gen_pub_k(uint param_1)
{
mod_exp(2, param_1, 0x40000017);
return;
}
mod_exp(base, exponent, modulus): This function is likely performing modular exponentiation:
result = (2^{\text{param_1}}) \mod 0x40000017 The value 0x40000017 in decimal is 1073741847, which is close to 2^30 , possibly chosen for cryptographic reasons.
param_1 is the exponent (probably a secret key or part of the key derivation process).
Just a few clicks away in Ghidra, you can find the mod_exp function.
Exactly how this works is a mystery to meāI probably should have taken some cryptography courses in college… But ChatGPT had an answer!
That being said, it also helped generate a Python script to compute the private key. Some modifications were needed to make it work.
import math
def brute_force_discrete_log(base, modulus, public_key, max_attempts=2**24):
"""Brute-force method to find x in base^x ā” public_key (mod modulus)"""
value = 1 # Start with base^0 = 1
for x in range(max_attempts):
if value == public_key:
return x # Found private key!
value = (value * base) % modulus # Compute next power
return None # Not found within the attempt limit
def baby_step_giant_step(base, modulus, public_key):
"""Baby-step Giant-step algorithm for discrete logarithm"""
m = math.isqrt(modulus) + 1 # Step size
table = {}
# Baby-step: Compute base^j % modulus for j in [0, m]
value = 1
for j in range(m):
table[value] = j
value = (value * base) % modulus
# Compute base^(-m) mod modulus (modular inverse of base^m)
base_inv_m = mod_inverse(pow(base, m, modulus), modulus)
# Giant-step: Compute public_key * base^(-i*m) % modulus
value = public_key
for i in range(m):
if value in table:
return i * m + table[value] # Solution found
value = (value * base_inv_m) % modulus
return None # No solution found
# Given values
base = 2
modulus = 1073741847 # 0x40000017
public_key = 506395958 # Example public key (replace with actual value)
# Try brute-force first (for small keys)
private_key = brute_force_discrete_log(base, modulus, public_key)
if private_key is None:
print("Brute-force failed, trying Baby-Step Giant-Step...")
private_key = baby_step_giant_step(base, modulus, public_key)
if private_key is not None:
print(f"Recovered private key: {private_key}")
else:
print("Failed to recover the private key.")
Running this results in retrieving the private key used in the encryption: 177370085.
In Ghidra, you can investigate further and find the following function, which is used for encrypting the data:
void eg_enc(int param_1,uint param_2,uint *param_3,uint *param_4)
{
int iVar1;
uint uVar2;
uint uVar3;
iVar1 = rand();
uVar3 = iVar1 % 0x40000016 + 1;
uVar2 = mod_exp(2,uVar3,0x40000017);
*param_3 = uVar2;
uVar3 = mod_exp(param_2,uVar3,0x40000017);
*param_4 = (uVar3 * param_1) % 0x40000017;
return;
}
With a bit more AI help, you can get the following solution:
Again, AI helped generate a script to decrypt the files using the private key.
from sympy import mod_inverse
def elgamal_decrypt(private_key, p, c1, c2):
"""Decrypts a single ElGamal encrypted pair (c1, c2)."""
s = pow(c1, private_key, p) # Compute shared secret: s = c1^private_key mod p
s_inv = mod_inverse(s, p) # Compute modular inverse of s
M = (c2 * s_inv) % p # Recover plaintext
return M
# Given values
p = 1073741847 # Prime modulus
private_key = 177370085 # Recovered private key
# Encrypted data from file
encrypted_pairs = [
(540488803, 187826906), (512275051, 37345508), (231599851, 823437630),
(631399654, 628252041), (1035050165, 1006835836), (896417195, 206453254),
(14141576, 263001145), (204987116, 186642158), (606374554, 598224082),
(547306682, 128791588), (587262377, 531917810), (982294489, 538345953),
(471862643, 967908048), (634400714, 763656344), (375171454, 544183238),
(1055094817, 793021731), (39117044, 951544349), (361114958, 4360479),
(327139844, 501552997), (467622466, 316485080), (36903752, 556784675),
(44658571, 501318067), (443001524, 1031119903), (555206443, 620324572),
(499990172, 366058919)
]
# Decrypt all pairs
decrypted_messages = [elgamal_decrypt(private_key, p, c1, c2) for c1, c2 in encrypted_pairs]
print("Decrypted messages:", decrypted_messages)
Running this results in a bunch of ASCII values:
Decrypted messages: [mpz(80), mpz(101), mpz(108), mpz(108), mpz(101), mpz(95), mpz(83), mpz(118), mpz(97), mpz(110), mpz(115), mpz(108), mpz(111), mpz(115), mpz(95), mpz(72), mpz(97), mpz(114), mpz(95), mpz(83), mpz(118), mpz(97), mpz(110), mpz(115), mpz(10)]
Which can be decoded into text:
decrypted_ascii = [80, 101, 108, 108, 101, 95, 83, 118, 97, 110, 115, 108, 111, 115, 95, 72, 97, 114, 95, 83, 118, 97, 110, 115, 10]
decrypted_text = "".join(chr(c) for c in decrypted_ascii)
print(decrypted_text)
Result?
Flag 1: 177370085
Flag 2: Pelle_Svanslos_Har_Svans
Outro
I absolutely can’t say that I understand everything that was done, especially not the underlying mathematics, but I still learned something new. Decompiling binary files and finding functions in them was actually really fun!
One cannot sweep under the rug that AI is a useful tool, especially when one has a rough idea of what they want to achieve. Without ChatGPT and Deepseekās developing reasoning, I would have easily gotten stuck on certain parts. Itās simply about using them in the right way; one should see them as an extension of oneās own abilities.
If I had a bit more time over the weekend, I’m sure I could have captured a few more flags, especially the ones related to authentication.
A full write-up of all the CTF questions is available at the organizers Github: https://github.com/heltseriost/CertCTF2025-Writeup
I read through all of them, and itās a bit frustrating when you realize that you were so close to solving a flag but didnāt quite get there. One big mistake I made was not checking the memory dumpāI got completely stuck in event logs and decompiling.
For the next CTF, which isn’t too far away, I will take this with me: not getting too caught up in the details, as well as trying to form a picture of the entire sequence of events rather than just each individual flag.
All in all, it was a fun CTF, especially for a beginner. The ransomware part was tough but still doable with the help of Google and some AI. Definitely happy with my 11th place.
And to end it all…
https://www.youtube.com/watch?v=bLlj_GeKniA
See you at LAN, or maybe on UNDUTMANINGEN 2025 š¤