This post is also available in: English (英語)
概要
ソフトウェア界隈ではユーザビリティかセキュリティかの論争がたえません。多くのサードパーティプログラムは、ユーザーの認証情報(クレデンシャル)を保存することで生活を便利にし、時間を節約してくれます。ただしこの便利さはセキュリティの犠牲のうえに成り立っており、クレデンシャルの保存はパスワード窃取のリスクとなります。こうして集められたクレデンシャルは、その後サイバー攻撃に利用されるおそれがあるのです。
本稿はクレデンシャル窃取の危険性を解説するため、一般的なサードパーティソフトウェアにおけるクレデンシャルの収集シナリオをいくつか検証していきます。実際の攻撃シナリオにもとづいて、それらのサードパーティソフトウェアでパスワードがどのように保存されているのか、それらパスワードはどのように取得されているのか、パスワード取得の振る舞いをどのように監視するのかについて見ていきます。
Cortex XDRをご利用のお客様は、Windows、Linux、MacOSエージェントのCortex 3.4でリリースされたCredential Gathering Protectionモジュール(クレデンシャル収集防止モジュール)により、本稿で解説した攻撃からの保護を受けています。
関連するUnit 42のトピック | Credential harvesting |
目次
クレデンシャル窃取の危険性: 攻撃者のアクセス拡大方法
クレデンシャル収集の実際
WinSCPの場合
Gitの場合
RDCManの場合
OpenVPNの場合
Chromiumベースのブラウザの場合
Firefoxブラウザの場合
Emotet?
結論
IoC
追加リソース
クレデンシャル窃取の危険性: 攻撃者のアクセス拡大方法
クレデンシャル窃取は明らかな悪です。ただしクレデンシャル窃取がもたらす影響の大きさを強調することは重要です。
多くの人は、複数のプログラムに同じパスワードを使いまわす傾向があります。しかもほとんどパスワードを変更しません。変更する場合でも、見抜かれやすいパターンでやってしまいます。
ですから、あるソースからパスワードをひとつ入手できれば、攻撃者はそのパスワードをべつのリソースに使いまわしてみることができます。使いまわしてみる対象には、しっかり保護されたソースも含まれます。つまり、安全性の高いプログラムAがあるとして、それより安全性の低いプログラムBで、同じパスワードやパターンが使われていれば、結果的にプログラムAの安全性を低下させてしまうことになるのです。
このほか、あるユーザーがオペレーティングシステム用のパスワードをほかの安全でない場所で使っていることがわかった場合、その攻撃者にはまったく新たな可能性の地平が開けることになります。
たとえばあるユーザーXがWindowsアカウントのドメインとLinuxのFTPファイルサーバーに同じパスワードを使っているとしましょう。このシナリオでユーザーXは一般的なプログラムWinSCPを使い、ファイルサーバー内のファイルを管理しています。WinSCPはパスワードの保存を推奨していませんが、ユーザーXは毎週このファイルサーバーにアクセスするので、パスワードを保存することで時間を節約したがっています。
本稿後半でデモを行いますが、ユーザーパスワードはWinSCPの保存場所から簡単に取得できます。XのPCに侵入できれば、Xのドメインアカウントのパスワードも入手できます。なぜならパスワードが安全な状態で保存されていないからです。しかもこのパスワードはファイルサーバーへの接続にも有効です。このファイルサーバーにはなにか機微情報を含むファイルがあって、いまや攻撃者はそれらにアクセスできる状態かもしれません。そこからさらにBloodHoundようなツールを使えば、組織内をどこまで侵害できそうかを推定することもできます。
クレデンシャル収集の実際
WinSCPの場合
WinSCPはメジャーなMicrosoft Windows用SFTP・FTPクライアントです。ローカルWindowsコンピュータとリモートサーバーとの間で、FTP・FTPS・SCP・SFTPなどでファイルをコピーするのに使います。
検証したバージョン:
5.19.6 (Build 12002 2022-02-22)
クレデンシャルの保存場所:
WinSCPは暗号化したユーザーパスワードをレジストリキーHKCU\software\martin prikryl\winscp 2\sessions\<session_name>以下のPasswordという値に保存します。
クレデンシャルの復元方法:
WinSCPはユーザーパスワードのビットに対して対称的な数学的演算を行います。パスワードの各バイトを受け取り、0xFF(11111111)との補数を計算した後、バイト0xA3 (10100011)とのXORを取ります。
この暗号化処理は、補数を求めた後1回XORを行うという内容になっています。そしてそのパスワードがレジストリ値Passwordに保存されます。この一連の演算は対称的なので、同じ演算をもう一回逆順に行えば元の値を得られます。
たとえばよく使われるパスワードの「Aa123456」を例に取ってみましょう。WinSCPはこのパスワードを「1D3D6D6E6F68696A」として保存します。
図2がこのパスワードの復号手順です。
このパスワードはHostName、UserNameと一緒に保存されています。Passwordのレジストリ値からこれを取得するにはパスワード先頭のインデックスを見つけねばなりませんが、この計算はとても簡単です。WinSCPのバージョンにより、レジストリ値の1バイト目か3バイト目が、ユーザー名、ホスト名、パスワードを連結した長さになります。この長さを表すバイトの後ろに続くバイトが開始インデックスで、この値は2倍されます。長さと開始インデックスは同じ方法で暗号化されています。
UserNameとHostNameも別のレジストリ値に保存されているので、その長さと値もわかっています。Passwordレジストリの値をインデックス「開始インデックス + UserNameの長さ + HosNameの長さ」から「長さ」まで復号すればパスワードを得られます。
利用実態
私たちはこれまでに図4のようなスクリプトが複数のお客様環境で実行されている様子を確認しました。
- -enc は「EncodedCommand」の略で、base-64でエンコードされた文字列がコマンドとして使用されることを意味しています。
デコードされたスクリプトからはWinSCPのパスワード復号の試みが確認されました。
Gitの場合
検証したバージョン:
2.35.1.windows.2
クレデンシャルの保存場所:
GitではパスワードとPAT(Personal Access Token)の両方を使えます。
ユーザーがGitのクレデンシャルを保存して時間を節約したければ、次のコマンドで行えます。
git config credential.helper 'store'
このコマンドを使うと、Gitはユーザーのクレデンシャルを平文で無期限にディスク上に保存します。
パスワードを含む可能性のあるファイル:
- <userprofile>\.git-credentials
- <userprofile>\.config\git\credentials
Gitでは、従来のパスワードの代わりに、PATをクレデンシャルとして使えます。このトークンはよりモジュール化されていて、それぞれに異なる権限と有効期限を持つアクセストークンをいくつでも作成できます。
ユーザーのアクションをさらに細かくモジュール化してそれぞれのアクションを特定のPATに関連付けた形で制御することは可能ですが、ユーザーのPATを持つ人は当該ユーザーがアクセスできるリポジトリをすべて閲覧できます。
これらのトークンは上に記載したのと同じファイルにも平文で保存されます。
クレデンシャルの復元方法:
これらのファイルを読める人は、ユーザー名、パスワード、トークン、関連Gitリポジトリを平文で見られます。
RDCManの場合
検証したバージョン:
2.83
リモートデスクトップ接続マネージャーのRDCManは、複数のリモートデスクトップ接続を管理します。自動チェックインシステムやデータセンターなど、定期的にマシンにアクセスする必要があるサーバーラボの管理に便利です。
クレデンシャルの保存場所:
ユーザーがRDCManを使うセッションのパスワードを保存する場合、デフォルトの設定ファイルは%localappdata%\Microsoft\Remote Desktop Connection Manager\RDCMan.settingsです。
このファイルは各RDP接続に関するメタデータ一般を含むXMLファイルです。
私たちは、このデータのなかにあるCredentialsProfilesというXMLタグに着目しました。
このタグの下にはCredentialsProfilesという別のタグがあって、その中にcredentialsProfileというXMLタグがあり、ここにPasswordというタグがあります。
クレデンシャルの復元方法:
パスワードを取得するには、RDCManプログラムを利用するユーザーのコンテキストでコマンドを実行する必要があります。これはパスワードが DATA Protection API (DPAPI) を使って保存されているためです。DPAPIはCryptProtectData関数とCryptUnprotectData関数を使うことで、あらゆる種類のデータをそれぞれの関数で対称暗号化/復号できます。
つまり、パスワードを取得するにはCryptUnprotectData関数を呼び出さねばなりません。
通常であれば、データを復号できるのは、そのデータを暗号化したユーザーと同じログインクレデンシャルを持つユーザーだけです。
RDCManからのクレデンシャル収集は、ほかのソフトウェアで見てきた例とちがい、攻撃者側に「対象ユーザーのコンテキストでソフトウェアを実行する」という余計なステップが必要ですが、その結果はじゅうぶんその価値に見合うものです。これがうまくいけば、この特定のユーザーが接続するすべてのマシンのすべてのユーザー名とそのパスワードを取得できるからです。
攻撃者が対象ユーザーのコンテキストでコマンドを実行さえできれば、あとクレデンシャル収集のためにやるべきことは以下だけです。
- RDCMan.settingsファイルを開いてpasswordというXMLタグがあることを確認する
- タグ内の文字列をbase64でデコードする
- デコードしたパスワード文字列を引数に指定してCryptUnprotectData関数を呼び出す
- 戻り値をUTF-8(ないしそのほかの適切なフォーマット)でデコードする
- 不要なNull文字を削除する
上の例の場合、ファイルに保存されているパスワードは次のとおりです。
AQAAANCMnd8BFdERjHoAwE/Cl+sBAAAA8/nnW5aFNUi0AKiTG4y9UQAAAAACAAAAAAAQZgAAAAEAACAAAADIjLLw0X4z9RDdWgPpqabLU7hTcJ1HVlFklpzX3eA14QAAAAAOgAAAAAIAACAAAAB01OvDCNCjaEhrq8J8hRm/SKycef7nR52ZkqcPLJqMsCAAAACg2htaeRsutDziS3FISeEAg3DsBpGxBGpPeWlUSVnXOkAAAAB5Tei9g5KWcVIhOKQ2cXxr5ONUOHMEEH5h3Lmp12mPlWaaZ6y8dGIVz8WnNKr4e73dhqNU8NyzI7RZBamS6DG6
そして復号されたパスワードはAa123456となります。
OpenVPNの場合
検証したバージョン:
2.5.029
OpenVPNはVPN(仮想プライベートネットワーク)ソフトウェアで、ルーティング構成ないしブリッジ構成や、リモートアクセス設備内で、ポイント・ツー・ポイントまたはサイト・ツー・サイト接続を安全に行う技術を実装しています。
クレデンシャルの保存場所:
OpenVPN はユーザーのパスワードをレジストリキーHKCU\software\openvpn-gui\configs\<session_name> のauth-dataという値に保存します。
クレデンシャルの復元方法:
OpenVPNもDPAPIのしくみを使っています。エントロピー用のパラメータ(NULLに設定可能)がオプションで使えます。
暗号化の段階でオプションのエントロピー用DATA_BLOB構造体を使った場合は、同じDATA_BLOB構造体を復号段階でも使う必要があります。
OpenVPNの場合、このエントロピーはentropyというレジストリ値に保存されます。また、entropyレジストリの値はHKCU\software\openvpn-gui\configs\<session_name>にも格納されています。
したがって、CryptUnprotectData関数をauth-dataからのパスワードとentropy(entropyレジストリ値から)を引数にして呼び出すと、セッションパスワードを得られます。
entropyというレジストリ値には00が1バイト余分に含まれているので、それは削除する必要があります。
Chromiumベースのブラウザの場合
検証したバージョン:
- Google Chrome: 103.0.5060.53 (Official Build) (64-bit)
- Microsoft Edge: 103.0.1264.37 (Official Build) (64-bit)
- Opera: 88.0.4412.53
Chromiumプロジェクトは、Google Chromeブラウザを支えるオープンソースプロジェクト、Chromiumを含んでいます。
一般的傾向として、多くの人はインターネットを閲覧するさい、パスワードを保存しています。
クレデンシャルの保存場所:
Microsoft Edge、Opera、Google ChromeなどのChromiumベースのブラウザを使用する場合、パスワードは通常 login dataと呼ばれるSQLiteデータベースファイル内に暗号化した状態で配置されます。
各プロファイルは、パスワードデータベース(login dataファイル)を持っています。
パスワードの暗号化に使用される鍵は、親フォルダ内のlocal stateというJSONファイルに配置されています。
例:
login data の場所:
- Google Chrome: %localappdata%\google\chrome\user data\<PROFILE>\login data
- Microsoft Edge: %localappdata%\microsoft\edge\user data\<PROFILE>\login data
- Opera: %appdata%\opera software\opera stable\<PROFILE>\login data
local stateの場所:
- Google Chrome: %localappdata%\google\chrome\user data\local state
- Microsoft Edge: %localappdata%\microsoft\edge\user data\local state
- Opera: %appdata%\opera software\opera stable\local state
クレデンシャルの復元方法:
login dataデータベース内にある各パスワードはAES-GCMモード(Advanced Encryption Standard with GCM)で暗号化されています。AES-GCMは共通鍵暗号方式なので暗号化にも復号にも同じ鍵が使えます。AESのアルゴリズムは、128ビットブロックごとに異なる鍵を使用し、その鍵は前のブロックの計算にもとづいて作られます。最初のブロックでは、初期化ベクトル(IV)を使用するオプションがあります。
Chromiumベースのブラウザが保存しているパスワードを復号するには以下が必要です。
- 暗号化されたパスワード
- 初期化ベクトル
- AES鍵
それぞれどのように取り出すかを見てみましょう。
A. 暗号化されたパスワード
login dataデータベースからエクスポート可能です。暗号化されたパスワードはpassword_value列の15番目の位置の文字から最後の文字から16文字を引いた位置までを取り出します: [15:-16]
B. 初期化ベクトル
同じくpassword_valueフィールド列の3番目の位置の文字から15番目の位置の文字までに格納されています: [3:15]
C. AES鍵
local state JSONファイルに書き込まれています。書き込まれている場所はos_cryptキーと encrypted_keyキーの下で、これをbase64でデコードします。
ChromiumベースのブラウザはDPAPIのしくみを使ってAES鍵を保存しているので、この鍵を取得するにはbase64からデコードして対象ユーザーのコンテキストでCryptUnprotectData関数を使います。
Google Chromeの例:
これは先頭にDPAPIという5文字のプレフィックスを付けた状態で保存されています。
当該ユーザーのコンテキストでの実行が可能である場合に、攻撃者によるユーザークレデンシャル収集の完了までに必要なステップは次のとおりです。
- login dataファイル、local stateファイルの両方をコピーする
- local state JSONファイルからAES-GCM鍵を得る
- (base64で)デコードし、(CryptUnprotectData関数で)復号し、鍵からパディングを除去する
- login dataデータベースの各パスワードを復号されたAES-GCM鍵で復号する
PythonによるChromeパスワード抽出方法についてはこちらの記事をご覧ください。
利用実態
私たちは、regsvr32.exeを使って、以下のコマンドラインでexcel.exeからDLLを動作させる例を確認しています。
C:\windows\system32\regsvr32.exe
C:\users\<username>\appdata\local\uolegxnwf\kgnkudbadmpogg.dll
(kgnkudbadmpogg.dllのSHA256: 6599FEE8C7ADF30A00889A7070600F472F8CEAD8EA4DD1A85E724ED15F2AED0F)
一連のイベント後、最終ペイロードはMicrosoft Edgeのクレデンシャルファイルにアクセスしようとしていました。
- login dataファイル(SQLiteデータベースファイル):
C:\users\<username>\appdata\local\microsoft\edge\user data\default\login data
- local stateファイル(暗号鍵を含む)
C:\users\<username>\appdata\local\microsoft\edge\user data\local state
Firefoxブラウザの場合
検証したバージョン:
Firefox Version 101.0.1 (64-bit)
これまでパスワードを保存するという振る舞いパターンに関して述べてきたことは、Mozilla Firefoxなどのブラウザを使う場合にも当てはまります。
クレデンシャルの保存場所:
Mozilla Firefoxも、Chromiumベースのブラウザ同様、プロファイルごとにパスワードファイルを持てます。
このファイルをlogins.jsonといい、これは%appdata%\mozilla\firefox\profiles\<PROFILE>\logins.jsonに配置されています。
ユーザー名もパスワードも暗号化された状態で保存されています。
クレデンシャルの復元方法:
logins.jsonファイル内の各ユーザー名とパスワードはPKCS #11という暗号化規格で暗号化されています。FirefoxではNSSライブラリ(nss3.dll)を開発してこの規格をブラウザに採用しています。
NSSはそのバージョンによって、key3.dbかkey4.dbというファイルに秘密鍵を保存します。
対象ユーザーのパスワードを得るには、攻撃者はこれらのファイルのいずれか1つとlogins.jsonファイルにアクセスする必要があります。
つまり、攻撃者が対象ユーザーと同一のマシン上での実行権限を得られるなら、次のステップでパスワードを窃取可能です。
- 攻撃者はlogins.jsonファイルをコピーする
- NSSライブラリ(nss3.dll)をロードする
- コピーしたlogins.jsonからencryptedUsernameとencryptedPasswordを(base64で)デコードする
- 各入力をSecItemに格納する。このオブジェクトは後でNSS全体でバイナリデータのブロックをやり取りするのに使われる
- 出力用のSecItemオブジェクトを作成する
- nss3.dllのPK11復号関数を使ってencryptedUsernameとencryptedPasswordの入力オブジェクトをそれぞれ復号し、先程作った出力用SecItemオブジェクトにデータを格納する
Chromiumベースのブラウザとはちがい、対象ユーザーのパスワードを取得するさい、攻撃者が当該ユーザーのコンテキストで実行している必要はありません。対象ユーザーのファイルシステムのプロファイルにアクセスする権限を持っているなら、どのユーザーでも利用可能です。
利用実態
私たちは、以下のようなスクリプトが実行されていた様子を観測しました。
難読化を解除した状態:
スクリプトの内容:
- %localappdata%\ujXgADというフォルダを作成する
- $Linksの各リンクに対し、Invoke-WebRequestの作成を試み、DLLを1つダウンロードして先に作成したフォルダにrRXqwGvGNR.wTjという名前で保存する
- エクスプロイトが成功したらbreakする
次に、エンドポイントにDLLが1つ作成され、以下のコマンドラインを指定してregsvr32.exeが使われていました。
C:\WINDOWS\system32\regsvr32.exe
C:\Users\<USERNAME>\AppData\Local\Temp\..\ujXgAD\rRXqwGvGNR.wTj
このパスは回避技術が使われている点が要注目です。\..\を使ってLocalフォルダに戻ることで攻撃者は直接のアクセスを避けています。
regsvr32.exeの使用後:
A. DLLはランダムな名前のフォルダに、ランダムな名前で、拡張子をDLLにした自分自身をコピーする
C:\Users\<USERNAME>\AppData\Local\<random_folder_name>\<random_dll_name>.dll
B. DLLが探索コマンドを複数実行する
-
- systeminfo: マシンの情報を一覧表示
- ipconfig /all: マシンのすべてのネットワークインターフェイスをリストアップ
- nltest.exe /dclist: ドメイン内の全ドメインコントローラを一覧表示
C. DLLがcertutil.exeをベースにした2つのファイルをランダムな名前で作成して実行する
- ファイルの1つには新たなランダム名が付けられるがMicrosoftの署名がついたままになっている
- もう1つはcertutilを改ざんしたもので、名前は元のままだが機能は異なり署名もついていない
D. 上記ステップCは2回行われる
署名のないファイルのSHA256:
A88C344F3F80F8A3EA2E9BA0687FEBCEE2A730FD9AC037D54C4FD21C0AB91039
CertutilのSHA256: このファイルじたいは良性であることに注意
D252235AA420B91C38BFEEC4F1C3F3434BC853D04635453648B26B2947352889
この署名のないcertutil.exeは、ChromiumベースのブラウザとFirefoxベースのブラウザの両方のパスワードファイルにアクセスしようとします。
図19のリンクを確認すると2つのリンクだけが機能していました。
- 最初にダウンロードされたDLL:
hxxps://www[.]yell[.]ge/nav_logo/AEnTP/
ダウンロードされたファイルの名前: RwuuPYoVei7FkJB.dll
(SHA256: A1D513E4A5C83895E5769C994C4D319959EF5AE3F679CE6C0C5211B5BECA7695)
- 2番目にダウンロードされたDLL:
hxxps://yakosurf[.]com/wp-includes/S/
ダウンロードしたファイル名:lw1JF63zARLUV8UwpwGnWpgg.dll
(SHA256:1B8638333751EFCB6B5332C801C11DF0DE3D7077C6ACEA1D663C0302519D7172)
どちらのファイルも実は同じDLLで、SHA256ハッシュを変えるささいな変更があるのみです。
このサンプルを調べたところ、最初のDLLはEmotetマルウェアファミリに属するものであることが確認されました。
Cortex XDRはこの攻撃を発生と同時に防止するので攻撃の次の段階が実行されていません。このマルウェアは、最初にFirefox、次にMicrosoft Edge、最後にGoogle Chromeの順でパスワードを読み取ろうとします。
デモのためにレポートモードにしたCortex XDRでの例を示します。この例からは、Credential Gathering Protectionモジュールが、Chromiumベースのブラウザに保存されたパスワードを読み取ろうとする試みについても検知していることがわかります。
Emotet?
Emotetが関与した事例を2つ確認したので、このマルウェアファミリとそのサードパーティクレデンシャルの収集方法についてもう少し詳しく調べてみました。マルウェアはコードをすべて自前で実装する必要がない場合もあります。NirsoftのWebBrowserPassViewツールのような既存ツールをラップして、Webブラウザが保存しているパスワードを明らかにするだけでよいのです。
Chromiumベースブラウザのlogin dataファイルとFirefoxブラウザのlogins.jsonファイルが確認できます。
結論
サードパーティのソフトウェアのなかには、クレデンシャルの保存方法が、思ったより安全ではないものもあることがわかりました。これらのプログラムのほとんどは、ユーザーのクレデンシャルをファイルやレジストリ値としてローカルディスクに保存しています。この事実が攻撃者の探すセキュリティのアキレス腱となり、組織を攻撃するためのアクセス権を与えてしまうことになります。
Cortex XDRをご利用中のパロアルトネットワークスのお客様は、新たなCredential Gathering Protectionモジュールにより、上記で解説したシナリオにくわえ、本稿で言及しなかったほかのクレデンシャル収集技術に対する保護も受けています。またLocal Analysis、BTP(Behavioral Threat Protection)、BIOC、Analytics BIOCsルールなど、追加の保護レイヤーも利用できます。
WildFireをお使いのパロアルトネットワークスのお客様は、こうしたクレデンシャルの収集を試行するツールからの保護を受けています。
NirsoftのツールはWildFireではグレーウェアとしてマークされ、XDRエージェントによってブロックされます。
IoC
以下のレジストリ値への不正アクセス |
|
以下のファイルへの不正アクセス |
|
悪意のあるハッシュ |
6599FEE8C7ADF30A00889A7070600F472F8CEAD8EA4DD1A85E724ED15F2AED0F
A88C344F3F80F8A3EA2E9BA0687FEBCEE2A730FD9AC037D54C4FD21C0AB91039 A1D513E4A5C83895E5769C994C4D319959EF5AE3F679CE6C0C5211B5BECA7695 1B8638333751EFCB6B5332C801C11DF0DE3D7077C6ACEA1D663C0302519D7172 |
追加リソース
- Detecting Credential Stealing with Cortex XDR(Cortex XDRによるクレデンシャル窃取の検出)
- git-credential-storeのドキュメント
- Git Tools - Credential Storage
- GitHub Docs: 個人用アクセス トークンの作成
- CryptUnprotectData 関数 (dpapi.h)
- How Emotet is altering techniques in response to Microsoft’s tightening of Workplace macro safety
- How to crack Firefox passwords with Python
- servo/nss