メモリパッチによるmsv1_0!SpAcceptCredentialsのフックをやってみる
メモリパッチによるmsv1_0!SpAcceptCredentialsのフックをやってみる
Windowsは対話型のユーザー名/パスワードベースのログオン用に2つの認証パッケージ、すなわちMSV1_0とKerberosを使用する。
後者はよく知られているように、ドメインのログオン用に利用される。 一方ドメインコントローラがネットワーク上で発見できない場合は、 キャッシュされた認証情報を基にMSV1_0パッケージを使用してローカルコンピュータへログオンする。
MSV1_0パッケージはrunasなどの一時的に他ユーザーの権限でコマンドを実行する場合の認証にも使用される。
MSV1_0パッケージの実体はmsv1_0.dllであり、内部に含まれるSpAcceptCredentialsFn関数がローカルセキュリティ機関(LSA)を呼び出すことで認証を行う。
今回は以下のサイトを参考に、DLLインジェクションによるメモリパッチを行うことで、msv1_0.dll内のSpAcceptCredentials関数をフックし認証情報を取得してみる。
カーネルモードでのデバッグ
まずはmsv1_0.dll内のSpAcceptCredentialsFn関数を発見する。 この関数はエクスポートもインポートもされない、DLL内のみで使用されるラッパー関数である。 ユーザーが入力した認証情報はまずこの関数に渡され、さらに Lsassの関数を使用するなどしてハッシュ化した認証情報の比較を行う。
LsassはWindowsの認証の根幹を成すシステムプロセスであり、その実態はlsass.exeである。 lsass.exeプロセスへ通常プロセスのようにアタッチするとシステムがクラッシュまたはフリーズする。 したがって、解析にはカーネルモードでのデバッグが必要になる。
今回はユーザーモードおよびカーネルモードの両方に対応したGUIベースのデバッガーであるWinDbg Previewを使用する。
WinDbg Previewを使ったカーネルデバッグに必要な準備は以下の記事を参考にしてほしい。
lsass.exeの解析
カーネルデバッグの準備が済めばいよいよlsass.exeプロセスの解析を開始する。
まずは全プロセスからlsass.exeプロセスのEPROCESS構造体を見つける。
見つけたlsass.exeプロセスのEPROCESS構造体の値を元に、lsass.exeプロセスのコンテキストにスイッチする。
ロードされているモジュールのリストを表示してみる。
msv1_0.dllが表示されない。
!pebでプロセスが読み込んでいるモジュールの一覧を表示する。 この中にはmsv1_0.dllが存在し、正しく読み込まれていることがわかる。
シンボル情報のリロードを行って正しく読み込まれるかやってみる。
読み込まれたようだ。
次に問題の関数へブレークポイントを設定する。
最後にターゲットホストでrunasコマンドなどの認証が必要な処理を呼び出す。
以下の画像はターゲットホスト上のPowerShellでrunasコマンド発行しようとしている瞬間の画像である。 認証情報を入力しエンターを押したところでブレークポイントに到達し停止している。
SpAcceptCredentials関数のインターフェースは以下のようになっており、第2引数に渡されるPSECPKG_PRIMARY_CRED構造体に認証情報が格納される。
SpAcceptCredentialsFn Spacceptcredentialsfn; NTSTATUS Spacceptcredentialsfn( SECURITY_LOGON_TYPE LogonType, PUNICODE_STRING AccountName, PSECPKG_PRIMARY_CRED PrimaryCredentials, PSECPKG_SUPPLEMENTAL_CRED SupplementalCredentials ) {...}
typedef struct _SECPKG_PRIMARY_CRED { LUID LogonId; UNICODE_STRING DownlevelName; // Sam Account Name UNICODE_STRING DomainName; // Netbios domain name where account is located UNICODE_STRING Password; UNICODE_STRING OldPassword; PSID UserSid; ULONG Flags; UNICODE_STRING DnsDomainName; // DNS domain name where account is located (if known) UNICODE_STRING Upn; // UPN of account (if known) UNICODE_STRING LogonServer; UNICODE_STRING Spare1; UNICODE_STRING Spare2; UNICODE_STRING Spare3; UNICODE_STRING Spare4; } SECPKG_PRIMARY_CRED, *PSECPKG_PRIMARY_CRED;
デバッガで中を表示した様子が以下。 引数に渡されている文字列を参照すると確かにこの関数に認証情報が渡されていることが確認できる。
以上のことからこの関数をフックしてその引数をインターセプトしてやれば認証情報を取得できることがわかる。
以降からこの関数をフックするプログラムを開発する。
フックコードの開発
これからSpAcceptCredentialsをフックし、認証情報をリークさせるDLLを開発する。
CreateProcessやLoadlibraryなどのように、DLLからエクスポートされている関数をフックする場合その関数へのアドレスは関数ポインタなどを利用することによって簡単に求められる。
しかし今回フックするSpAcceptCredentialsはmsv1_0.dllからエクスポートされていないため、関数ポインタ利用によるアドレスの取得はできない。 このような場合、メモリ領域を探索しそのアドレスを求める必要がある。
まず特徴点となるSpAcceptCredentialsの機械語を調査する。 実際には、これからやりたいことを既に実現してるMimikatzのコードを参考に以下の機械語を特徴点として選択した。
4883ec20498bd9498bf88bf148
したがってmsv1_0.dllがロードされているイメージ領域内を上の値で探索すればSpAcceptCredentialsへのアドレスが求められる。
ちなみにIDAやGhidraを使ってSpAcceptCredentialsの特徴点となる機械語で検索をかけ、デコンパイルするとSpAcceptCredentialsの内部構造がわかりやすい。 以下の画像が実際にデコンパイルを行い、調整を加えたSpAcceptCredentialsである。
まとめると、SpAcceptCredentialsをフックするには以下のような手順を踏めばよい
- SpAcceptCredentialsをインポートするmsv1_0.dllのハンドルを取得
- ハンドルからmsv1_0.dllのイメージ領域とそのサイズを取得
- イメージ領域内をSpAcceptCredentialsの特徴点となる機械語で探索
- SpAcceptCredentials内の数バイトを認証情報をリークさせる関数へとジャンプする機械語へ書き換える
- 認証情報をリークさせる関数の最後で、正規のSpAcceptCredentialsを呼び出させる
以上の手順でフックは完了だ。
デモ
SpAcceptCredentialsFn callback function memory patch https://t.co/exyKQXf5OU @YouTubeさんから
— ry0kvn (@ry0kvn) 2020年2月1日
まとめ
コードは参考サイトからみつけることができる。 通常のフックとは違い、内部関数のリバーシング、バイトパターンでの仮想メモリの探索など勉強になった。