Overview
A couple of days ago i came across this post detailing a joint project between Ethan Heilman and Will Cummings discussing Hacking Team’s crypter named ‘core-packer’. The crypter’s source was leaked online after the Hacking Team compromise of July 2015. Taking a look at ‘core-packer’ provides an interesting glimpse at the quality (or lack thereof) of ‘government-grade’commercial malware products.
Warning: The repository associated with this post contains malicious binaries (core, core_packed, soldier, soldier_packed) for educational purposes. Don’t go around toying with them if you don’t know what you’re doing.
Heilman’s discussion of the crypter is fairly complete and mentions the important distinction between the common use of cryptography and the use of cryptography in anti-anti-virus techniques: while the former seeks to guarantee security properties like confidentiality, integrity, etc. (preferably on a long-term basis) the latter simply seeks to force detection solutions to integrate code that recognizes the packer, locates the cryptographic keying material and applies the decryption process in the appropriate fashion in order to obtain the original malicious binary in the hopes that the performance penalty imposed by integrating this kind of code for every crypter out there is simply too much. So while the usage of ciphers like Tiny Encryption Algorithm (TEA) or RC4 would be ill-advised (to say the least) in regular cryptographic contexts in the case of ‘core-packer’ that’s not much of an issue. Given that the keying material is stored together with the ciphertext the hardly is the guarantee of long-term confidentiality.
But that doesn’t mean the developers of ‘core-packer’ didn’t mess up anyway. The crypter works by iterating over all sections in a PE and when a section is not shared (IMAGE_SCN_MEM_SHARED) and it is either executable (IMAGE_SCN_MEM_EXECUTE) or named .data the section is encrypted. As can be seen here if the PE in question is a DLL the section is encrypted using RC4 while if it is an executable it is encrypted using TEA. Key generation is done as follows:
1srand(GetTickCount()); // initialize for (rand)
2
3(...)
4
5char passKey[16];
6
7for(int i =0; i < sizeof(passKey); i++)
8 passKey[i] = rand() % 256;
Where the 128-bit key is used directly for TEA and used to initialize the S-Box of RC4 using its Key Scheduling Algorithm (KSA).
Both ciphers, however, are used in fatally flawed ways.
Take a look at the crypter source part handling the encryption of section data using TEA:
1{
2 uint32_t *key = (uint32_t *) rc4sbox;
3 LPDWORD encptr = (LPDWORD) pProcessSection->RawData();
4 encptr += 0x800;
5
6 for(DWORD dwPtr = 0x2000; dwPtr < pProcessSection->SizeOfRawData(); dwPtr += 8, encptr += 2)
7 { // no encryption!
8 //if (encptr[0] == 0xCCCCCCCC || encptr[1] == 0xCCCCCCCC)
9 //{ // alignment .. skip
10 //}
11 //else
12 //{ // encrypt block!
13 tea_encrypt((uint32_t *) encptr, key);
14 //}
15 /*DWORD tmp = encptr[0] = encptr[1];*/
16 }
17
18 pProcessSection->hide(true);
19 //pSectionHeader->Characteristics ^= IMAGE_SCN_MEM_EXECUTE;
20 //pSectionHeader->Characteristics &= IMAGE_SCN_MEM_READ;
21}
As we can see the code iterates over the data in a block-wise fashion and applies TEA to each block seperately, ie. it uses the cipher in the Electronic Codebook (ECB) mode of operation. It is more or less common knowledge (or at least, should be) that ECB mode replicates plaintext patterns in its ciphertext since identical plaintext blocks get mapped to identical ciphertext blocks as shown by the notorious ‘ECB penguin’:
What this means for the crypter is that identical plaintext block patterns within or among sections are preserved in the ciphertext.
Hence anyone seeking to detect crypted versions of known malicious executable can do so by seeing whether this ‘ECB constraint’ holds over a supicious executable to confirm whether it is a crypted version of this or that malicious executable or not. This is made even worse by using TEA as their choice of blockcipher since its small block size (64 bits = 8 bytes) means more blocks per given plaintext and hence a higher chance of identical blocks (and thus a more reliable constraint).
I wrote a little proof-of-concept tool to detect whether, given a set of profiles for known malicious executables, a suspicious PE corresponds to a ‘core-packer’ crypted version. Given that checking whether this constraint holds given a particular profile is faster than the process of crypter detection and automatic key extraction from the stub it defeats the crypter’s purpose.
The proof-of-concept i wrote extracts rather crude and strict ‘profiles’ (it simply finds identical blocks within a section and the offsets they correspond to) but these constraints could be relaxed to accomodate more flexible ways of abusing this flaw (though a sufficiently fine-grained profile would be necessary to avoid false positives, especially with a large profile database). Either way, the bottomline is that ‘core-packer’ uses TEA in ECB mode and even if its purpose is not to offer ‘strong cryptographic’ guarantees on confidentiality it fails in its obfuscation purpose this way.
But what about the crypter’s use of RC4 for DLLs? Take a look at the relevant part of the crypter source:
1for(int i = 0; i < pInfectMe->NumberOfSections(); i++)
2{ // each section must be packed
3 if (pInfectMe->IsDLL())
4 {
5 init_sbox(rc4sbox);
6 init_sbox_key(rc4sbox, (BYTE *) passKey, 16);
7 }
8 (...)
9 else if ((pSectionHeader->Characteristics & IMAGE_SCN_MEM_EXECUTE)
10 == IMAGE_SCN_MEM_EXECUTE)
11 {
12 //pSectionHeader->Characteristics |= 0x02;
13
14 if (pInfectMe->IsDLL())
15 cypher_msg(rc4sbox, (PBYTE) pProcessSection->RawData(),
16 pProcessSection->SizeOfRawData());
Since RC4 is a streamcipher there is no need for either padding or dividing the section data into blocks. One simply takes the plaintext and XORs it on a byte-by-byte basis with the keystream generated by the RC4 Pseudorandom Generation Algorithm (PRGA). The mistake made by the developers of ‘core-packer’, however, lies in the fact that they re-initialize the KSA for every section and do so without using a nonce (since the RC4 specifications do not require one to be used). This means that if a single key is used to encrypt multiple plaintexts the same keystream will be used (since the keystream is deterministically generated based on the initial ‘seed’ key) for all of them. This results in what is known as a collection of ‘parallel ciphertexts’, ie. a collection of ciphertexts sharing the same keystream. This results in two types of attacks we can mount against the crypter for detection purposes.
The first of these is a known plaintext attack (KPA).
As can be seen in the above image it is trivial to obtain a keystream segment of length n from offset i to offset i+n if i have a known (or probable) plaintext segment of length n at the same offset. A simple XOR between the ciphertext and known plaintext will result in the keystream segment corresponding to that offset. A well-designed streamcipher ensures that such KPAs yielding keystream derivation do not allow for recovery of the internal state of the PRGA (and hence recovery of other keystream fragments or even the seed key). But if we have a set of parallel ciphertexts we can use such a KPA to derive keystream at offset i in one ciphertext and apply it at the same offset to another ciphertext (since they were encrypted using the same keystream) and hence obtain another plaintext fragment.
Since we are not looking to obtain unknown plaintext (as would be the case in a regular KPA against parallel ciphertexts) but rather seek to confirm the presence of several known plaintext fragments (which an anti-virus product would have in the form of signatures), in order to confirm a particular suspicious binary is in fact a ‘core-packer’ crypted version of a known malicious binary, exploiting this weakness is easier than the usual cryptanalysis approach for this scenario.
Given a set of probable plaintext fragments (coupled to a particular offset within a particular PE section) extracted from a malicious binary we can confirm (with a False Positive/False Negative ratio dependant upon the amount of plaintext fragments we have) our suspect binary is indeed a crypted version of this or that malicious binary by deriving the keystreams for all probable plaintexts in one section and applying them to the corresponding offsets in subsequent sections and confirming whether our probable plaintexts for those offsets within those sections match.
My proof-of-concept tool does this in the following fashion:
1known_plaintext = offset_list[0]['plaintext']
2keystream = self.xor_strings(known_plaintext, section_data[offset: offset+len(known_plaintext)])
3
4# Check whether keystream holds over other known plaintexts at identical offsets
5for i in xrange(1, len(offset_list)):
6
7 section = self.get_section_by_name(pe, offset_list[i]['section_name'])
8 assert(section != None)
9
10 # Has to be a core-packer section encryption candidate
11 assert((hasattr(section, 'IMAGE_SCN_MEM_EXECUTE') or (section.Name[:5] == ".data")))
12 section_data = section.get_data()
13 assert(len(section_data) > offset)
14
15 known_plaintext = offset_list[i]['plaintext']
16 candidate_plaintext = self.xor_strings(section_data[offset: offset+len(known_plaintext)], keystream)
17
18 if(candidate_plaintext != known_plaintext):
19 return False
A second approach abusing this flawed usage of streamciphers relies on the fact that a XOR operation between the elements an (even-numbered) set of parallel ciphertexts results in a XOR operation between the corresponding plaintexts:
Extracting this XOR ‘product’ from an unpacked and packed version of a given binary (see below for the binaries in question) as follows:
1def get_blob(self, pe, use_sections):
2
3 # Use even number of sections (at least 2) to 'cancel out' keystreams
4 assert((len(use_sections) >= 2) and (len(use_sections) % 2 == 0))
5 data_blob = ""
6 for section in pe.sections:
7
8 secname = section.Name[:section.Name.index("\x00")]
9
10 # Is target core-packer section and under our consideration?
11 if((hasattr(section, 'IMAGE_SCN_MEM_EXECUTE') or (secname == ".data")) and (secname in use_sections)):
12
13 # Set data_blob to XOR between current content and section ciphertext
14 section_data = section.get_data()
15 if(data_blob == ""):
16 data_blob = section_data
17
18 else:
19 data_blob = self.xor_strings(data_blob[:min(len(section_data), len(data_blob))], section_data[:min(len(section_data), len(data_blob))])
20
21 return data_blob
Will yield the same binary ‘blob’ of XORed plaintexts:
1$ sha1sum core_blob
2145ab4b99a493ab16e3a5ff81a91b3b91c97cd9a core_blob
3$ sha1sum core_packed_blob
4145ab4b99a493ab16e3a5ff81a91b3b91c97cd9a core_packed_blob
An interesting property of the above scenario is that the resulting XOR-product of the plaintexts has a very particular statistical profile which we can use to confirm a suspect binary is a ‘core-packer’ crypted version of a known malicious binary.
We can do this by, given a known malicious binary, building a so-called n-gram model of the XOR-product of the PE sections which would be encrypted using ‘core-packer’ (ie. its executable sections and the .data section). Given this XOR-product ‘blob’ the n-gram model lists, for a given length n, the relative frequencies of the various n-grams (that is, sequences of n bytes) occurring in the ‘blob’.
Now, given a suspect binary we obtain the same XOR-product of the applicable sections, determine its n-gram model and compare it against our database of n-gram models for known malicious binaries. If the total ‘error’ (defined as the sum of absolute differences in relative frequencies for a given n-gram in two between n-gram models) is below an (experientially determined) threshold value t we have a match and can confirm that our suspect binary is in fact a crypted version of a known malicious binary. Here the false positive and false negative rates depend on n-gram size n and treshold value t.
Check out the following proof-of-concept tool implementing this.
Another, more general, approach (ab)using the above weaknesses allows one to distinguish between the XOR-product of parallel ciphertexts and the XOR-product of arbitrary data. As Eric Filiol describes this effectively comes down to distinguishing random from non-random data and can be done by taking the XOR-product of what you suspect to be parallel ciphertexts, counting the number of null bytes z and testing whether z has normal distribution law:
If so we are not dealing with parallel ciphertexts. In the case that we are z has normal distribution law (where p > 1/2 is the probability for a bit to be zero):
Using this distinguishing attack we can determine whether the sections of a suspect binary are parallel ciphertexts and hence encrypted by a (badly implemented) streamcipher. This holds for all crypters operating in this fashion (not just ‘core-packer’) and hence can be a useful heuristic.
In order to test ‘core-packer’ and the techniques discussed above i set up a virtual machine running Windows XP and built the 32-bit version of the crypter using Microsoft Visual Studio 2012. The following modifications to the solution are necessary for it to work:
Link with /SAFESEH:NO
Project -> core-packer properties -> Linker -> Advanced -> Image Has Safe Exception Handlers -> No
Compile for WinXP
Project -> core-packer properties -> General -> Platform Toolset -> Visual Studio 2012 - Windows XP (v110_xp)
We also need to make sure the Microsoft Visual C++ 2012 redistributable package (x86) is installed on our VM.
As noted by Heilman running the crypter on random PEs caused the crypter either to crash or produce corrupted executables so i simply ran it on pre-compiled Hacking Team malware samples (RCS-Agent for a sample DLL and RCS-Soldier for a sample executable, sources can be found here and here) which were then crypted without problems:
1> packer32.exe core core_packed
2[CONFIG] random file sectoin: C:\WINDOWS\system32\dmutil.dll
3Input file is DLL!
4.pedll32/32bit section found in code
5 Size of raw data: 1400
6 Virtual size: 126b
7 RVA: 21000
8 Virtual Address: 00421000
9[CONFIG] Section Name: .calc
10[CONFIG] base: 0
11[CONFIG] size: 1400
12
13> packer32.exe soldier soldier_packed
14[CONFIG] random file sectoin: C:\WINDOWS\system32\d3d8.dll
15Input file is EXECUTABLE
16.peexe32/32bit section found in code
17 Size of raw data: c00
18 Virtual size: b95
19 RVA: 23000
20 Virtual Address: 00423000
21 **WARNING** LOAD_CONFIG Data Directory isn't NULL! Removing!
22[CONFIG] Section Name: .hermit
23[CONFIG] base: 0
24[CONFIG] size: c00
The following SHA1 digests correspond to the binaries involved:
1core: f0f14b8c20c727721188cb14071ad8997cef2181
2core_packed: 100350f5cae5b4bb39052244e8498e98da0e33ad
3soldier: 5fcca76ea037d311bc5614c16da91a15a5a9c44e
4soldier_packed: 0d4addd4784dcae45146145344f9b8e48edd4407
I also uploaded them to virustotal which showed some disappointing detection results (considering the Hacking Team malware and crypter are pretty high profile and have been out there for quite a bit now):
I generated profiles (for ECB identical block detection, known plaintext detection and n-gram profile detection) for the core and soldier binaries:
1$python core_packer_detect.py --file soldier_packed --get-ecb .text .data
2 .Hacking Team 'core-packer' PoC detector.
3 (c) 2015, Jos Wetzels
4
5{'.data': [[7840, 7912, 8056, 8200, 8560, 8632, 8848], [4088, 4120, 4152, 4184,
64264, 4488, 4712, 9456], [4112, 4256, 9448], [7152, 7264, 7336, 7408, 776
7(...)
8
9$python core_packer_detect.py --file core_packed --get-stats .text .data --n 1
10 .Hacking Team 'core-packer' PoC detector.
11 (c) 2015, Jos Wetzels
12
13defaultdict(<type 'float'>, {'\x00': 0.09117759146341463, '\x83': 0.022389481707317072, '\x04': 0.030892721036585365, '\x87': 0.000857469512195122,
14(...)
And used the proof-of-concept tool against the packed versions as follows:
1$python core_packer_detect.py --file core_packed
2 .Hacking Team 'core-packer' PoC detector.
3 (c) 2015, Jos Wetzels
4
5[*]Matching known plaintext profiles...
6[!]File matches malicious profile of 'rcs_agent'!
7
8$python core_packer_detect.py --file soldier_packed
9 .Hacking Team 'core-packer' PoC detector.
10 (c) 2015, Jos Wetzels
11
12[*]Matching known ECB-mode profiles...
13[!]File matches malicious profile of 'rcs_soldier'!
The lesson here is that while ‘cryptographically secure’ might not be as much of an issue with obfuscation tools (such as crypters) as with other applications of cryptography, poorly implemented crypto can still completely defeat its own purpose. In this case for better rather than for worse.
All items