Write-up of the challenge “Reverse – SoStealthy” of Nuit du Hack 2018 CTF qualifications.
The weekend of 03/31/2018 is pre-qualification for the Nuit du Hack 2018 as a Jeopardy CTF. Having had the opportunity and the time to participate with some colleagues and friends, here’s a write-up resolution of the challenges which we could participate.
- Category: Reverse
- Name: SoStealthy
- Description : During an incident response, we captured the network traffic from a suspected compromised host. Could you help us reverse the installed malware?
- File : suspicious.pcap (5.80MB – 7b34f24ad1a87204bc5b1aa4044013c270e171d89d07c1eab0f24e9e2cc5498b)
- Points : 150
For this challenge a suspicious PCAP file is provided. There are several HTTP requests in it. We begin by extracting HTTP objects from Wireshark :
There is a lot of objects. Analyze them :
A “favicon” file is suspicious, with some code :
This file begins with an XML header then a JavaScript code loads an ActiveX payload base64 encoded. It’s definitively suspicious.
We cleaned the XML header and associated footer. This script can now be run as JScript via Windows binaries “cscript.exe” or “wscript.exe” :
When launched, a GUI tells us to provide a “magic word” :
The flag is the magic word, go reverse it.
Several ways without success were tried, like decompiling the script through IDA, or trying to debug it with “cscript //X” and VisualStudio. Dump the memory for post-analysis too…
Then the name of James Forshaw and the DotNetToJscript tool inform us about the technology in place : .Net and how the initial binary was embeded in the JScript file. The embeded file is certainly a PE (Portable Executable) file, dig into this :
PE file contains an “MZ” header. But in the base64 decoded, the MZ header isn’t at the offset “0” :
We cleaned the header before the “MZ” and save the output file as “.exe” :
This binary with valid PE header is now ready to be decompiled through dnSpy :
We can now see the .Net operation and the XOR function :
// Token: 0x06000005 RID: 5 RVA: 0x0000226C File Offset: 0x0000046C private bool MeeBish0iotho9biBuJi(string magicWord) { for (int i = 0; i < this.Tai8Aip0ua3ULi6zo1je.Length; i++) { uint num = (uint)(magicWord[i] ^ this.Tai8Aip0ua3ULi6zo1je[i]); bool flag = (ulong)num != (ulong)((long)this.az5nieghahj0Iekah0ph[i]); if (flag) { return false; } } return true; }
The XOR key is in a decimal-array format :
private int[] az5nieghahj0Iekah0ph = new int[] { 21, 91, 20, 0, 126, 0, 61, 24, 2, 82, 7, 17, 88, 22, 18, 21, 114, 117, 15, 80, 59, 24 };
The input “magic word” string is xored with the last 22 bytes of the base64 payload itself :
These 22 last bytes are :
FkKEJ5dGVbXSkIAAAACgsA
The XOR key in decimal-array is :
key = [21,91,20,0,126,0,61,24,2,82,7,17,88,22,18,21,114,117,15,80,59,24]
To find the “magic word”, just do a XOR between the key and the last 22 bytes :
def xor_strings(xs, ys): return "".join(chr(ord(x) ^ ord(y)) for x, y in zip(xs, ys)) last22bytes = "FkKEJ5dGVbXSkIAAAACgsA" xorKey = [21,91,20,0,126,0,61,24,2,82,7,17,88,22,18,21,114,117,15,80,59,24] xorKeyStr = "" for c in xorKey: xorKeyStr = xorKeyStr + str(unichr(c)) print "Magic Word : NDH{" + xor_strings(last22bytes, xorKeyStr) + "}"
Run :
Flag : NDH{S0_E45Y_T0_B3_ST34L7HY}
This challenge will have resisted us a good part of the night! Congrats to Estelle, Martin, Georges and Timothée for this one 🙂
Greeting to the whole team ! 🙂