おい、ちょっと手を止めてこっちを向いてくれ。
今朝、ウチのSOC(セキュリティオペレーションセンター)で冷や汗をかくようなアラートが上がった。ある踏み台サーバーから内部のドメインコントローラーへ向けて、不審なセッション確立の試行が検知されたんだ。原因を追っていくと、初期侵入を許したWebアプリの脆弱性から攻撃者が足場を固め、最終的に lsass.exe のメモリをゴッソリ抜き取っていやがった。
そう、今日話すのは 「Mimikatzとメモリ上のクレデンシャル抽出」 だ。
Webエンジニアやインフラエンジニアの連中は、「うちはデータベースのパスワードをハッシュ化しているから大丈夫」「クラウドだから安全」なんて甘い考えを持っていることが多い。だが、OSのメモリ上に何が展開されているか、一度でも真面目に覗いたことがあるか?
今回は、攻撃者がどのようにしてWindowsの心臓部からパスワードを掠め取るのか、その生々しい手口と、我々エンジニアがインフラとコードの両面でどうやってその首に鈴を付けるのか、現場のリアルな知見を共有しよう。
—
1. なぜ攻撃者は lsass.exe を狙うのか?
Windows環境において、認証プロセスを一手に引き受けているのが C:\Windows\System32\lsass.exe(Local Security Authority Subsystem Service)だ。こいつは、ユーザーがログインする際に入力したパスワードの平文、NTLMハッシュ、Kerberosのチケット、さらにはAPIトークンまで、認証に必要なクレデンシャルをメモリ上に保持している。
攻撃者がリモートコード実行(RCE)やSQLインジェクションなどでWebアプリを踏み台にし、OSの管理者権限(NT AUTHORITY\SYSTEM など)を奪取した瞬間、彼らの次なるターゲットは間違いなくこの lsass.exe になる。
Mimikatzの恐ろしさと「生メモリ」の現実
代表的なツールである Mimikatz は、この lsass.exe のプロセス空間にアタッチし、認証情報を次々と画面に吐き出す。
「Windows 10や11、あるいはServer 2019以降は、デフォルトでLSA保護(Protected Process Light: PPL)が有効だから安全だろ?」と油断しているそこの君、甘い。確かにPPLが有効であれば、通常のプロセスから lsass.exe を開くことはできない。
しかし、攻撃者はすでにその先を行っている。独自に書いたカーネルドライバをロードしてPPLをバイパスしたり、ダンプしたメモリファイルをオフラインの解析環境に持ち帰って解析したりと、あの手この手で網をかいくぐってくるのだ。インシデントレスポンスの現場で、ダンプされたメモリのバイナリから自社ドメインの管理者パスワードが綺麗に並んでいるログを見たときの絶望感といったら、言葉では言い表せないぞ。
—
2. 攻撃の足場を断つ:Webアプリ・インフラの多層防御
この悪夢を防ぐためには、単に「パスワードを複雑にする」だけでは意味がない。攻撃者がOSの奥深くへ侵入する「最初のステップ(Webアプリの脆弱性や過剰な権限)」を完全に塞ぎ、仮に侵入されても lsass.exe に触らせないための要塞化が必要だ。
ここからは、インフラとコードの両面で、今日からすぐに適用できる具体的な設定と実装を見ていく。
① Windowsセキュリティ設定:LSA保護の強制とCredential Guard
まずはOS側の防壁を固める。デフォルト任せにせず、グループポリシー(GPO)やレジストリで明示的に保護を有効化するんだ。
以下のレジストリ設定を適用し、LSAの保護を強制する。
# PowerShell管理者コンソールで実行し、LSA保護(RunAsPPL)を有効化する
# これにより、署名されていないプロセスが lsass.exe のメモリを読むことを防ぐ
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" -Name "RunAsPPL" -Value 1 -PropertyType DWORD -Force
# 再起動が必要だが、インフラ基盤の標準テンプレートに必ず組み込んでおくこと
さらに、VBS(Virtualization-based Security)を利用した Credential Guard を有効化し、認証情報をハードウェアレベルで隔離するのが現代のインフラの常識だ。これを怠ると、いかに強力なパスワードポリシーを敷いても無意味と心得よ。
② アプリケーション層の要塞化:脆弱性と過剰権限の排除
いくらOSを固めても、Webアプリケーションの脆弱性(RCEやファイルアップロードなど)から SYSTEM 権限を取られたらゲームオーバーだ。アプリ層での防御を徹底しろ。
例えば、PHPなどの動的言語でサーバー内部を操作するような危険なコマンド実行関数(exec、system、passthru など)を安易に使っていないか?もしどうしても外部プロセスを叩く必要がある場合は、引数を完全にサニタイズし、実行ユーザーの権限を極限まで絞る必要がある。
以下に、Python(Flask)を用いた堅牢なバックエンド処理のサンプルを示す。ここでは、入力値の厳格なバリデーションと、特権プロセスを絶対に起動させない安全な設計思想をコードに落とし込んでいる。
# filename: secure_backend_example.py
import re
from flask import Flask, jsonify, request
app = Flask(__name__)
# 許可された入力パターンのホワイトリスト定義(インジェクション対策の基本)
ALLOWED_USERNAME_PATTERN = re.compile(r"^[a-zA-Z0-9_]{3,20}$")
def process_user_data(username: str) -> dict:
"""ユーザーからの入力を安全に検証し、処理を行う関数。
OSコマンドの直接実行や危険なAPI呼び出しを排除し、
最小権限の原則に基づいた安全なロジックを担保する。
"""
# 1. 入力値の厳格な検証(ホワイトリスト方式)
if not ALLOWED_USERNAME_PATTERN.match(username):
raise ValueError(
"不正な入力値が検出されました。許可されているのは英数字とアンダーバーのみです。"
)
# 2. 意図しない特権昇格を防ぐためのモック処理
# 実際のインフラ操作を行う場合でも、root/SYSTEM権限を持つデーモンから
# 独立した、最低限の権限を持つ専用ユーザーでプロセスを実行すること。
safe_result = {
"status": "success",
"message": f"ユーザー {username} の処理が安全に完了しました。",
}
return safe_result
@app.route("/api/v1/process", methods=["POST"])
def handle_request():
data = request.get_json()
if not data or "username" not in data:
return jsonify({"error": "リクエストボディが不正です。"}), 400
try:
result = process_user_data(data["username"])
return jsonify(result), 200
except ValueError as e:
# セキュリティ上の詳細を攻撃者に推測させないため、エラーメッセージは適切に抽象化する
return jsonify({"error": str(e)}), 400
if __name__ == "__main__":
# デバッグモードは本番環境では絶対にオフにすること(RCE脆弱性の温床になる)
app.run(host="127.0.0.1", port=5000, debug=False)
③ Nginxによる不正リクエストの遮断(WAF的なアプローチ)
Webサーバーの手前にある Nginx などのリバースプロキシでも、不審なペイロードやリバースシェル、メモリダンプツールをダウンロードしようとする動きを検知・ブロックできるようにヘッダーやルールの見直しを行っておくべきだ。
以下は、Nginxにおいて特定の不審な文字列や攻撃パターンを含むリクエストを弾くための設定例だ。
# /etc/nginx/conf.d/security_headers.conf
# サーバーバージョンの隠蔽
server_tokens off;
# クエリ文字列やリクエストボディにMimikatzや一般的な攻撃コマンドのシグネチャが含まれる場合をブロック
# (※本番運用時は誤検知に注意し、WAFやIDS/IPSと連携させること)
map $query_string $bad_bot {
default 0;
"~*mimikatz" 1;
"~*lsadump" 1;
"~*sekurlsa" 1;
"~*whoami" 1;
"~*net\.exe" 1;
}
server {
listen 80;
server_name example.com;
# 不審なクエリを検知した場合は強制的に403を返す
if ($bad_bot) {
return 403;
}
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 不要なHTTPメソッドの制限
if ($request_method !~ ^(GET|POST|HEAD)$ ) {
return 405;
}
}
}
—
3. チーフからの実践的なアドバイス
インシデント対応の現場にいると、「セキュリティ対策は終わりのないイタチごっこだ」と嘆くエンジニアによく出会う。だが、それは違う。攻撃者が100回試行して1回でも成功すれば我々の負けだが、我々は「攻撃者が侵入し、権限を昇格し、メモリを漁るまでのすべてのハードルを極限まで引き上げる」ことができる。
今回のテーマである Mimikatz によるクレデンシャル抽出を防ぐための要点をもう一度まとめる。
1. アプリの脆弱性を放置するな: 最初の足がかりを作らせなければ、lsass.exe にたどり着くことすらない。
2. OSの標準機能を信じるな、強制しろ: LSA保護(RunAsPPL)や Credential Guard は「あれば良い機能」ではなく「必須の要件」だ。
3. 最小権限の徹底: 万が一アプリが踏み破られても、そのプロセスが SYSTEM 権限を持っていなければ、メモリの深部にまで手を伸ばすことはできない。
セキュリティは、派手なハッキングテクニックの応酬ではなく、こうした地道な設定の積み重ねとコードの品質管理にほかならない。
自分の担当しているシステムが、明日狙われてもびくともしない要塞になっているか、今一度コードと設定ファイルを見直してくれ。頼んだぞ。
コメント