This challenge is designed so that the full solving process provides you with a realistic approximation of completing an actual malware analysis task. Common malware techniques like anti-debugging, string obfuscation, encryption, compression, packing, and process injection are used.
Challenge Description
Author: rainbowpigeon, DraftDown Labs
It has long been rumored that many threat actors were after zeyu2001’s personal stockpile of the latest web 0-days. Alas, tragedy struck when he joined too many suspicious Telegram groups without due caution and ended up getting infected by suspicious malware.
He quickly put his Cyber Olympianโข skills to use and managed to retrieve the offending binary as well as capture the network traffic sent out by it. However, he’s too busy getting more CVEs and HackerOne bounties to conduct any further analysis. Can you help him decipher the malware’s traffic?
The password for the ZIP file is infected. While the binary will not harm your system, I suggest analyzing everything in a Virtual Machine with antiviruses switched off.
This challenge was made in collaboration with DraftDown Labs!
Initial triage of susware.exe in tools like pestudio will highlight that there are multiple TLS callbacks. These callbacks also show up in Exports in IDA Pro so they aren’t hard to miss.
We should inspect these callbacks first as they will actually be executed before WinMain. Cursory analysis of the WinMain code should reveal to you that the code in WinMain is just a red herring as it’s impossible to guess any flag generated by it.
The last TLS callback will AES-CBC-192 decrypt resource 139 with key 0xakm_0xpwn_0xDr4ftD0wn. and IV 02bfb0a6c9296f0f provided that anti-debug checks are passed. It’s not necessary to identify the encryption algorithm yourself because you can just dynamically debug the executable, change EIP to bypass the anti-debug checks, and dump out the decrypted resource from memory. But if you do identify the encryption algorithm (either manually or with the help of IDA plugins like capa explorer or findcrypt-yara) then you could also dump out the resource using Resource Hacker or CFF Explorer and then decrypt it in CyberChef.
The MZ header of the decrypted resource should tell you that it is a PE. This PE is injected into C:\\Windows\\SysWOW64\\svchost.exe for execution via the RunPE technique.
Injected Executable
Unpacking
The injected PE is packed with UPX but modified to bypass being detected as UPX by tools like Exeinfo PE, Detect It Easy, Nauz File Detector, and the standard UPX utility itself.
Exeinfo PE will only show “Unknown Packer-Protector , 4 sections”, Nauz File Detector suggests a “Protector” is at play because of high section entropy, while upx -d will give the error “NotPackedException: not packed by UPX”.
Apart from high entropy in the section of the entry point which can be shown by multiple tools and not just Nauz File Detector, there are other telltale signs that there is possibly some type of packing involved:
small amount of and suspicious imported functions that the PE uses (VirtualProtect, LoadLibraryA, GetProcAddress)
IDA Pro will tell you that the imports segment seems to be destroyed
the PE section named “.text” has a raw size of 0 bytes but is writable and has a virtual size of 0x7000 bytes
So what do we do? It is feasible to just analyze it entirely in memory, but there are a few ways to unpack it for easier analysis.
Tools like XVolkolak, Scylla, pe-sieve can be used to dump out the unpacked executable. Some may then further require manual fixing of the entry point location and disabling of the PE’s “DLL can move” flag so that it can be dynamically run.
Manual unpacking is also possible by breakpointing at the only popad call, stepping to the original entry point (OEP) and then dumping out memory segments.
I think one could also modify UPX’s source to remove any packing detection checks so that it can unpack the PE.
If you’re really familiar with UPX, it’s possible to identify the modifications I made to the PE and reverse those changes so that the standard UPX can unpack it just fine
Encryption
RC4 KSA and PRGA routines can be easily spotted in the IDA Pro pseudocode - once before the send and once more after the recv. Hardcoded key is FtkvWPDfIdKGWvP5788D4kNeO6FMCXsO. There are, however, a few modifications that the usual standard RC4 doesn’t have:
RC4-drop[n] is used meaning n initial keystream bytes are discarded. n is 3328 for the client-to-server traffic and 3840 for the server-to-client traffic
Extra bitwise negation just before the plaintext is XOR’d with the keystream: result = v13 ^ ~(a6 + a1[v14]);
Extra parameter a6 in actual RC4 encryption/decryption function will offset either the plaintext or resulting ciphertext depending on the sign of a6. If a6 is 2 for encryption, a6 needs to be -2 for decryption.
A common problem malware analysts face is identifying compression formats used or even identifying that compression is used in the first place.
If you have already successfully decrypted the traffic in the PCAP or you were dynamically inspecting buffers in the PE before encryption takes place, you would notice that you’re able to see plaintext but it is still slightly garbled. This is an indication that there’s possibly compression used.
A closer look at the pseudocode should reveal to you that the supposed compression could be occurring at sub_402C00 which happens after ReadFile but before the encryption and send.
char*__cdeclsub_402CE0(inta1,unsignedinta2,char*a3,inta4,__int16a5){// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v5=-1;v6=(unsigned__int8*)a1;v89=a5-1;v91=a1;v7=(unsigned__int8*)(a1+a2);v92=(unsigned__int8*)(a1+a2);if(a2<0xF){v8=a3;v67=(unsigned__int8*)a1;}else{v90=(unsignedint)(v7-15);v8=a3;v82=*(unsigned__int8*)(a1+1)|((*(unsigned__int8*)(a1+2)|((*(unsigned__int8*)(a1+3)|(*(unsigned__int8*)(a1+4)<<8))<<8))<<8);v83=a1;LABEL_3:v101=v6;a1=(int)(v6+1);v9=*(unsigned__int16*)(v6+7);v10=__PAIR64__((unsigned__int64)v6[8]>>24,v9)>>24;v11=*(unsigned__int8*)(a1+5)|(v9<<8);v12=__PAIR64__(v10,v11)>>24;v13=*(unsigned__int8*)(a1+4)|(v11<<8);v14=__PAIR64__(v12,v13)>>24;v15=*(unsigned__int8*)(a1+3)|(v13<<8);v16=__PAIR64__(v14,v15)>>24;v17=*(unsigned__int8*)(a1+2)|(v15<<8);v18=__PAIR64__(v16,v17)>>24;v19=*(unsigned__int8*)(a1+1)|(v17<<8);v20=__PAIR64__(v18,v19)>>24;v21=32;v22=(unsigned__int8*)a1;v23=*(unsigned__int8*)a1|(v19<<8);v94=v20;v24=-a1;v100=v23;if((int)(v90-a1)>=16){v86=0;v25=(unsigned__int8*)(a1+10);v84=(unsigned__int16*)(a1+10);do{v88=0;v26=(int)&v25[v24-10];v95=v26;do{v27=v82;if(v26)v27=v23;v28=*(_DWORD*)&v89&((0x1E35A7BD*v27)>>18);v29=(unsigned__int8*)(v91+*(unsigned__int16*)(a4+2*v28));............
It is also not hard to find this function statically through sheer exploration because there aren’t too many nested function calls.
Ok so now that we know where the compression occurs, how do we identify the compression format? One common method is to look for magic constants in the code and search them up.
In the code this interesting 0x1E35A7BD is used a few times. If you Google or Github search it, results will show that it is associated with Google’s Snappy compression format. Technically the constant is also used in Google’s WebP and Brotli compression formats, but we can rule WebP out because that’s for image compression and we can exclude Brotli by doing a quick comparison of the compression code on GitHub with the IDA Pro pseudocode.
inlineuint32_tHashBytes(uint32_tbytes,uint32_tmask){constexpruint32_tkMagic=0x1e35a7bd;return((kMagic*bytes)>>(32-kMaxHashTableBits))&mask;}char*CompressFragment(constchar*input,size_tinput_size,char*op,uint16_t*table,constinttable_size){// "ip" is the input pointer, and "op" is the output pointer.
constchar*ip=input;assert(input_size<=kBlockSize);assert((table_size&(table_size-1))==0);// table must be power of two
constuint32_tmask=table_size-1;constchar*ip_end=input+input_size;constchar*base_ip=ip;constsize_tkInputMarginBytes=15;if(SNAPPY_PREDICT_TRUE(input_size>=kInputMarginBytes)){constchar*ip_limit=input+input_size-kInputMarginBytes;for(uint32_tpreload=LittleEndian::Load32(ip+1);;){// Bytes in [next_emit, ip) will be emitted as literal bytes. Or
// [next_emit, ip_end) after the main loop.
constchar*next_emit=ip++;uint64_tdata=LittleEndian::Load64(ip);uint32_tskip=32;constchar*candidate;if(ip_limit-ip>=16){autodelta=ip-base_ip;for(intj=0;j<4;++j){for(intk=0;k<4;++k){inti=4*j+k;// These for-loops are meant to be unrolled. So we can freely
// special case the first iteration to use the value already
// loaded in preload.
uint32_tdword=i==0?preload:static_cast<uint32_t>(data);assert(dword==LittleEndian::Load32(ip+i));uint32_thash=HashBytes(dword,mask);...}...}...}...}...}...}...
# Script modified from https://github.com/manojpandey/rc4/blob/master/rc4-3.py by rainbowpigeonimportsysimportcramjamimportpysharkfrombinasciiimportunhexlify,hexlifyMOD=256defbit_not(n,numbits=8):return(1<<numbits)-1-ndefKSA(key):key_length=len(key)S=list(range(MOD))j=0foriinrange(MOD):j=(j+S[i]+key[i%key_length])%MODS[i],S[j]=S[j],S[i]returnSdefPRGA(S):i=0j=0whileTrue:i=(i+1)%MODj=(j+S[i])%MODS[i],S[j]=S[j],S[i]K=S[(S[i]+S[j])%MOD]K=K^j# MODIFICATION: extra XORyieldKdefget_keystream(key,drop):"""
Takes the encryption key to get the keystream using PRGA
return object is a generator
"""S=KSA(key)i=0j=0# MODIFICATION: drop nfor_inrange(drop):i=(i+1)%MODj=(j+S[i])%MODS[i],S[j]=S[j],S[i]returnPRGA(S)defencrypt_logic(key,input,shift,drop):"""
:key -> encryption key used for encrypting, as hex string
:input -> array of unicode values/ byte string to encrpyt/decrypt
"""key=[ord(c)forcinkey]keystream=get_keystream(key,drop)result=""forcininput:# MODIFICATION: shift, bitwise negationifshift>=0:val="%02X"%(bit_not((c+shift)&0xFF)^next(keystream)&0xFF)elifshift:val="%02X"%(((bit_not(c)^next(keystream))+shift)&0xFF)result+=valreturnresultdefdecrypt(key,ciphertext,shift,drop):"""
:key -> encryption key used for encrypting, as hex string
:ciphertext -> hex encoded ciphered text using RC4
"""ciphertext=unhexlify(ciphertext)result=encrypt_logic(key,ciphertext,shift,drop)ifnotlen(result)%2:returnunhexlify(result)else:returnunhexlify("0"+result)defmain():key="FtkvWPDfIdKGWvP5788D4kNeO6FMCXsO"iflen(sys.argv)!=2:print("Specify pcap filename as argument")exit(1)filename=sys.argv[1]needs_decompressing=Falsewithpyshark.FileCapture(filename)ascap:forpacketincap:try:data=packet.data.dataexceptAttributeError:# TCP packets like SYN, SYN ACK, ACK won't carry any datapasselse:# if server to clientifpacket[packet.transport_layer].srcport=="1337":decrypted=decrypt(key,data,-3,3840)# if client to serverelse:decrypted=decrypt(key,data,-2,3328)needs_decompressing=Trueifdecrypted:ifneeds_decompressing:try:decompressed=cramjam.snappy.decompress_raw(decrypted)exceptExceptionase:print("decrypted:",hexlify(decrypted))print(e)else:print("decompressed: ",bytes(decompressed).decode())else:print("decrypted:",decrypted.decode())print("_____________________________")needs_decompressing=Falseprint("Done")if__name__=="__main__":main()
C:\Downloads>py traffic_decrypter.py traffic_capture.pcapng
decompressed: Microsoft Windows [Version 10.0.19044.1288](c) Microsoft Corporation. All rights reserved.
FLARE Sun 05/15/2022 14:01:44.72
C:\Users\a\AppData\Local\Temp>
_____________________________
#...# omitted for brevity#..._____________________________
decrypted: cd flag
_____________________________
decompressed: cd flag
FLARE Sun 05/15/2022 14:02:14.14
C:\flag>
_____________________________
decrypted: dir
_____________________________
decompressed: dir
Volume in drive C has no label.
Volume Serial Number is 7EAB-E766
Directory of C:\flag
05/12/2022 11:30 PM <DIR> .
05/12/2022 11:30 PM <DIR> ..
05/08/2022 10:18 AM 224 flag.txt
1 File(s)224 bytes
2 Dir(s) 24,372,260,864 bytes free
FLARE Sun 05/15/2022 14:02:18.77
C:\flag>
_____________________________
decrypted: type flag.txt
_____________________________
decompressed: type flag.txt
01010011010001010100010101111011010011010011010001101100011101110011010001110010001100110010110100110100011100100011001100101101001000010110111001110100001100110111001000110011011100110111010000110001011011100110011101111101FLARE Sun 05/15/2022 14:02:28.99
C:\flag>
_____________________________
decrypted: ipconfig /all
_____________________________
#...# omitted for brevity#..._____________________________
decrypted: exit_____________________________
Done
Decoding 01010011010001010100010101111011010011010011010001101100011101110011010001110010001100110010110100110100011100100011001100101101001000010110111001110100001100110111001000110011011100110111010000110001011011100110011101111101 gives you SEE{M4lw4r3-4r3-!nt3r3st1ng}!
Flag: SEE{M4lw4r3-4r3-!nt3r3st1ng}
Thanks for reading! I hope you were able to learn something from this challenge or this writeup :)