【実務・中級編】 メモリ上の資格情報抽出(Mimikatzの痕跡とLSASS解析) – インシデントレスポンス & フォレンジック(DFIR)防御ガイド

おい、ちょっと手を止めてこっちを向いてくれ。

今朝、ウチのSOC(セキュリティオペレーションセンター)で冷や汗もののアラートが上がった。某クライアントのドメインコントローラーに繋がる踏み台サーバーで、不審なプロセスの挙動が検知されたんだ。原因を追っていくと、やっぱり奴らがやっていた。そう、lsass.exeをターゲットにした資格情報の抜き取り、いわゆる「Mimikatz」の影だ。

インシデントレスポンスの現場にいると、この手口には毎日のように遭遇する。攻撃者はネットワークの境界防御をいとも簡単にすり抜け、内部に入り込んだ瞬間にこの手法を使って特権アカウントの資格情報を奪い取る。そして、組織全体のキーストロークを握っていくんだ。

今日は、開発者やインフラエンジニアである君たちに、この「LSASSからいかにして認証情報が抜かれるのか」という攻撃のリアルと、それを根本から無力化するための実践的なアプローチを叩き込んでおこう。教科書に書いてある綺麗ごとは抜きだ。現場の泥臭い防衛術を共有する。

—

なぜ攻撃者は lsass.exe を狙うのか?

Windows環境において、lsass.exe(Local Security Authority Subsystem Service)はセキュリティの心臓部だ。ユーザーがログオンする際に入力したパスワードのハッシュ、Kerberosのチケット、トークンなど、認証に必要なすべての機密情報をこのプロセスのメモリ上に保持している。

攻撃者がローカル管理者権限(SYSTEMまたはそれに準ずる権限)を手に入れたら、次にやることは決まっている。この lsass.exe のプロセス空間にアクセスし、メモリダンプを採取するか、直接コードをインジェクションして平文パスワードやNTLMハッシュをごっそりいただくのだ。これがMimikatzの基本動作であり、現代のサイバー攻撃における「王道」の横展開(ラテラルムーブメント)の起点となる。

現場のフォレンジック調査では、lsass.exe に対して不審なハンドル(PROCESS_VM_READやPROCESS_QUERY_LIMITED_INFORMATIONなど)を開いたプロセスの痕跡をメモリやイベントログ(Event ID 4688やSysmon Event ID 10)から探していくことになる。だが、攻撃者もバカじゃない。タスクマネージャーからのダンプ採取なんて古典的な手はすぐバレるから、APIを直接ハックしたり、ミニダンプ機能を悪用したりして、あの手この手で目を盗もうとする。

—

攻撃を無力化する:現代のインフラとOSの要塞化

では、この脅威に対して我々はどう立ち向かうべきか。
「侵入されることを前提とする」ゼロトラストの思想に立てば、仮に端末が破られても、lsass.exe の中身を読ませなければいいだけの話だ。

ここからは、実務で今すぐ適用すべき具体的な防御設定と、コードレベルでのアプローチを解説する。

1. Windows Defender Credential Guard の強制有効化

もっとも効果的で、かつOSレベルで標準提供されている最強の盾が Credential Guard だ。これは仮想化ベースのセキュリティ(VBS)を利用して、LSASSプロセスを通常のOS空間から隔離し、たとえ SYSTEM 権限を持っていようとも直接アクセスできないようにする仕組みだ。

もし君たちが管理しているサーバーやクライアント端末で、これが有効になっていないなら、今すぐグループポリシー(GPO)かIntuneで設定を流し込むべきだ。

[グループポリシーの設定パス]
コンピュータの構成 > 管理用テンプレート > システム > 仮想化ベースのセキュリティ > 「Credential Guard の有効化」
設定値: 「UEFI ロックとともに有効」または「有効」

これを有効化するだけで、Mimikatzの sekurlsa::logonpasswords コマンドは「平文パスワードが見つからない(またはセッションが存在しない)」という空振りに終わるようになる。

2.LSASS保護(RunAsPPL)の適用

Credential Guardの要件を満たせないレガシーな環境であっても、RunAsPPL(Protected Process Light) を有効にすることで、LSASSを保護されたプロセスとして動作させ、不正なデバッグやメモリ読み取りをブロックできる。

以下のPowerShellスクリプト(管理者権限で実行)またはレジストリ設定を、インフラのプロビジョニングスクリプトに組み込んでおこう。

# [PowerShell] LSASSをProtected Process Light (PPL) として有効化するスクリプト
# 備考: 適用後はシステムの再起動が必要です。

$RegistryPath = "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa"
$ValueName = "RunAsPPL"
$ValueData = 1 # 1: 有効, 2: 監査モード

# レジストリキーが存在するか確認し、設定を書き込む
if (!(Test-Path $RegistryPath)) {
    New-Item -Path $RegistryPath -Force
}

New-ItemProperty -Path $RegistryPath -Name $ValueName -Value $ValueData -PropertyType Dword -Force

Write-Host "LSASSのPPL保護を有効化しました。変更を適用するためには再起動を行ってください。" -ForegroundColor Green

—

アプリケーション開発者への教訓:資格情報依存からの脱却

インフラ側でどれだけLSASSをガチガチに固めても、アプリケーション設計に不備があれば意味がない。特に、社内システムや管理画面を作る際に、OSの資格情報(Windows認証やActive Directory連携)に過度に依存した設計をしているチームは要注意だ。

例えば、Webアプリケーション内で平文のユーザー名とパスワードをそのまま変数に保持し続けたり、不必要に永続的なセッションクッキーを発行したりする実装は、万が一アプリ側の脆弱性(RCEやLFIなど)が突かれた際に、メモリ上のデータを通じて二次被害を引き起こす。

ここでは、Python(Flask等)を想定し、セッションやパスワードハッシュを安全に取り扱うためのセキュアな実装サンプルを示す。

【Python】安全なパスワード検証とセッション管理の実装サンプル

import hmac
import hashlib
from flask import Flask, request, jsonify, session

app = Flask(__name__)
# 本番環境では必ず環境変数等から強固なランダムシークレットキーを読み込むこと
app.secret_key = "replace_with_a_very_secure_random_secret_key_in_production"

# ダミーのユーザーデータベース(実際には安全にハッシュ化されたDBを使用)
# パスワードはプレーンテキストではなく、必ず強固なアルゴリズム(Argon2やbcrypt)でハッシュ化して保存する
USER_DATABASE = {
    "admin": {
        # 例としてのハッシュ値(実際にはソルト付きハッシュを使うこと)
        "password_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 
    }
}

def verify_password(stored_hash, provided_password):
    """
    タイミング攻撃を防ぐために hmac.compare_digest を使用してハッシュを比較する
    """
    provided_hash = hashlib.sha256(provided_password.encode('utf-8')).hexdigest()
    return hmac.compare_digest(stored_hash, provided_hash)

@app.route('/api/login', methods=['POST'])
def login():
    """
    ログインエンドポイント
    平文のパスワードをメモリ上に長く保持せず、即座にハッシュ検証を行って破棄する
    """
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')

    if not username or not password:
        return jsonify({"error": "ユーザー名とパスワードは必須です"}), 400

    user = USER_DATABASE.get(username)
    if user and verify_password(user["password_hash"], password):
        # 認証成功時、セッションに機密情報(パスワードやハッシュ)を含めない
        session['user'] = username
        session['authenticated'] = True
        return jsonify({"message": "ログインに成功しました"}), 200
    
    # 資格情報不正時は詳細な理由を返さず、一律で拒否する(ユーザー列挙攻撃対策)
    return jsonify({"error": "認証に失敗しました"}), 401

@app.route('/api/dashboard', methods=['GET'])
def dashboard():
    """
    セッション状態の確認
    """
    if session.get('authenticated'):
        return jsonify({"status": "access granted", "user": session.get('user')}), 200
    return jsonify({"error": "認可されていません"}), 403

if __name__ == '__main__':
    # デバッグモードは本番環境では絶対にオフにすること(メモリダンプや情報漏洩のリスク)
    app.run(host='127.0.0.1', port=5000, debug=False)

このコードのポイントは、認証情報をメモリ上で不必要に長期間保持せず、検証が終わったら即座にガベージコレクションの対象にする設計にしている点だ。アプリのメモリ空間がダンプされたとしても、平文パスワードがそのまま露出するリスクを最小限に抑えている。

—

チーフエンジニアからの最後の言葉

メモリフォレンジックの世界では、「攻撃者はどこからともなくやってくる」のではなく、「我々の見落とした隙間から静かに入り込んでいる」。MimikatzによるLSASS解析は、彼らにとっては「合鍵を作るための定番の作業」にすぎない。

インフラ担当者は Credential Guard と RunAsPPL でOSの防壁を固めろ。そして開発者は、資格情報の扱い方に妥協せず、セキュアな設計をコードに落とし込め。

セキュリティは、誰か一人が頑張ればいいってもんじゃない。チーム全員がこの「メモリの裏側」で何が起きているかという共通認識を持つこと。それが、インシデントを未然に防ぐ唯一にして最強の防衛策だ。さあ、自分の担当システムのサーバー設定とコードをもう一度見直してくれ。

コメント

タイトルとURLをコピーしました