2 ポイント 投稿者 GN⁺ 2025-01-10 | 1件のコメント | WhatsAppで共有
  • WindowsのBest-Fit文字変換は、UTF-16文字列をANSIコードページに変換する過程で見た目の似た文字に置き換え、この挙動がPath Traversal、Argument Injection、RCEにつながるWorstFit攻撃面となる
  • 問題は、ANSI API、C/C++ランタイム、コンパイラが挿入するスタートアップコード、開発者による非wide文字APIの使用が重なる構造で発生し、GetCommandLineAGetEnvironmentVariableAgetenvint main()の経路が影響を受ける
  • CVE-2024-4577では、Chinese/JapaneseコードページでU+00ADが-に変換され、PHP-CGIのパッチを回避したほか、Filename Smugglingでは¥、全角スラッシュが/または\\に変換され、パスの混同を生む
  • Argument Splittingでは、全角ダブルクォートやYen/Won記号がコマンドラインの解析文字を生成し、wget.exetar.exeopenssl.exejava.exeのようなCLIツールに引数を注入でき、PHP・Python・Node.js・Rustの一般的な引数エスケープだけでは防ぎにくい
  • 緩和するには、WindowsのUTF-8オプションを有効にするか、開発者がWide Character API_wgetcwd_wgetenvwmain()のようなwide文字経路を使う必要があり、MicrosoftがすべてのWindowsエディションでUTF-8を既定で有効にするまで、類似の問題は繰り返される可能性がある

Windowsのエンコーディング構造とBest-Fit

  • Windowsは初期にANSIコードページを使用しており、言語圏に応じて1252、932、936、949、950といったコードページが異なっていた
    • ACP(ANSI Code Page)は、ファイル操作や環境変数など、ほとんどのアプリケーションとシステム設定で使われる
    • OEMCP(Original Equipment Manufacturer Code Page)は、主にコンソールの読み書きのようなデバイス通信に使われる
    • chcpはACPではなくOEMCPを表示するため、この研究の中心であるACPを確認する手段ではない
  • Windowsは1990年代半ばにUnicodeへ移行し、現在の中核APIはUTF-16ベースのwide characterを使用する
    • ファイルシステム、システム情報、テキスト処理といった中核APIがwide文字APIへ移行した
    • UTF-8機能は存在するが、大半の言語では既定で有効になっておらず、記事ではbeta段階と表現されている
  • 下位互換性のため、Windows APIはANSI版とUnicode版を併せて提供している
    • ANSI APIはGetEnvironmentVariableAのようにA接尾辞を持つ
    • Unicode APIはGetEnvironmentVariableWのようにW接尾辞を持つ
    • ANSI APIが呼び出されると、Windowsは内部UTF-16文字列をRtlUnicodeStringToAnsiStringまたはWideCharToMultiByteでANSI文字列に変換する

Best-FitがWorstFitになる仕組み

  • Best-Fitは、UTF-16文字を対象のANSIコードページで正確に表現できないとき、見た目が似ている、または近いと感じられる文字へマッピングする挙動である
    • たとえばWindows-1252では U+221Eは8にマッピングされる
    • √π⁷≤∞がANSI APIを通ると、"vp7=8"のように変わり得る
  • マッピングはコードページごとに異なる挙動を示す
    • ¥ U+00A5はJapanese 932コードページで\\にマッピングされる
    • Central European 1250コードページではYにマッピングされる
    • そのほか大半のコードページでは変更されない
  • Windows APIの直接呼び出しだけでなく、CRT関数や一般的なmain関数経路でも同じ変換が発生する
    • getenvのようなnon-wide CRT関数でBest-Fit変換が適用される
    • int main(int argc, char* argv[], char* envp[])の形で引数や環境変数を受け取る場合にも変換が介在する
    • これは、コンパイラが挿入するCRTスタートアップコードとANSI Windows APIの使用が組み合わさるためである
  • マッピングの確認にはBest-fit Mapping GrepperUnicode.orgのWindowsBestFit生マッピングデータを参照できる

最初のWorstFit事例: PHP-CGI CVE-2024-4577

  • CVE-2024-4577は、Chinese/Japaneseコードページに設定されたPHP-CGIサーバーを?%ADsリクエストだけで侵害できるWorstFit攻撃の事例である
    • 影響を受けるコードページは932(Japanese)、936(Simplified Chinese)、950(Traditional Chinese)
    • 脅威となる文字は­ U+00AD
  • 2012年のPHP-CGI脆弱性は、Apacheがクエリ文字列をCGIプログラムの最初の引数として自動処理したことで発生したargument injectionだった
    • ?-sを付けるとページのソースコード漏えいとRCEが可能だった
    • PHPのパッチは、クエリ文字列がダッシュで始まる場合に引数解析を中断する方式だった
  • Best-Fitにより、U+00AD soft hyphenがChinese/Japaneseコードページで-に変換され、既存のパッチが回避された
    • ?%ADsはPHP-CGIの観点では-sのように動作し得る
    • この事例をきっかけに、研究チームはBest-Fitという用語に初めて出会った

Filename Smuggling: パス文字が変換される問題

  • Filename Smugglingは、ファイル名に含まれるUnicode文字がANSI API経路で/または\\に変換され、パストラバーサルを生み出し得る攻撃である
    • 関連APIはGetCurrentDirectoryAgetcwdFindFirstFileAfindfirst*GetFullPathNameAなど
    • 影響を受けるコードページは874、125x、932(JP)、949(KR)
    • 脅威となる文字は U+FF0F、 U+FF3C、¥ U+00A5(JP)、 U+20A9(KR)
  • Chrome V8のDeveloper Shellであるd8.exeは、内部実装でGetCurrentDirectoryA()を使って現在の作業ディレクトリを取得する
    • 悪意あるUnicode文字を含む作業ディレクトリを作成できると、ANSI APIアクセス時にパストラバーサルのペイロードへ変化する
    • 例として、意図しないC:/windows/win.iniへのアクセスが可能になる
  • mrubyのDir.getwd()のWindows実装は、ANSI CRT関数_getcwd()に依存している
    • 戻り値が汚染される可能性があり、Path Traversalにつながり得る

Cuckoo Sandbox: Path TraversalからRCEまで

  • PythonのWindowsファイルシステムアクセスでは、文字列がwideかnarrowかに応じてwide APIまたはANSI APIを使用できた
    • PEP 529以降、WindowsファイルシステムのエンコーディングはUTF-8に標準化された
    • Python 2およびPython 3.6以前のPython 3は、WorstFit攻撃に対して脆弱なままだった
  • Cuckoo Sandboxは自動化されたマルウェア分析プラットフォームであり、最新の公式バージョンはPython 2.7に依存している
    • CuckooはCuckoo HostとVM Clusterで構成される
    • アップロードされたサンプルはVMで隔離実行され、ネットワークパケット・ドロップファイル・ログを独自メカニズムで同期する
  • マルウェアがUnicodeファイル名を持つドロップファイルを作成すると、Cuckoo HostのPythonパス処理でPath Traversalが発生する可能性がある
    • 例のPoCは AAAA\\u00a5..\\u00a5..\\u00a5..\\u00a5..\\u00a5..\\u00a5conf\\u00a5cuckoo.conf パスを生成する
    • 分析終了後、ユーザーがWebインターフェースでダウンロードボタンを押すと、Pythonのファイル操作がトリガーされる
    • Cuckoo Hostは変換後の ../ を含むパスを処理し、攻撃者に機密データを送る可能性がある
  • 攻撃者は cuckoo.conf をダウンロードし、Flask PINの計算に必要な機密情報を収集して、Sandbox Host上でRCEを達成できる
    • デモ動画は Video 11 で提供されている

Argument Splitting: コマンドライン解析を変えるBest-Fit

  • Argument Splitting は、GetCommandLineA の出力またはnon-Unicodeな int main() 経路でコマンドライン文字列が変化し、引数が分割される攻撃である
    • 関連APIと経路は GetCommandLineAint main()
    • 影響を受けるコードページは874、125x、932(JP)、949(KR)
    • 脅威となる文字は U+FF02、 U+FF3C、¥ U+00A5(JP)、 U+20A9(KR)
  • 例のPHPコードは escapeshellarg() でURLを安全に囲んだうえで wget.exe -q を実行するが、入力 " --use-askpass=calc " により calc.exe の実行が可能になる
    • 同じ入力はNode.js、Rust、Pythonに置き換えても防げない
    • Python最新バージョンの subprocess.run(["wget", "-q", ...]) の例でも動作する
  • Windowsは新しいプロセスにコマンドライン全体を1つの文字列として渡し、実行ファイルがこれを直接解析する
    • UNIX系のように引数配列が常に渡される構造ではない
    • CreateProcess APIは lpCommandLine パラメータを直接受け取る
  • 一般的なコマンドライン解析で重要な文字は、空白・タブ、double quote、backslashである
    • 空白とタブはquote modeでないときに引数を分割する
    • " はquote modeを切り替える
    • \\ は特定のシーケンスでdouble quoteとbackslashをescapeする
  • 多くの言語の標準ライブラリはこの規則に合わせてユーザー引数をエスケープするが、エスケープはBest-Fit変換の前に完了する
    • PHPの escapeshellarg はdouble quoteを空白に置き換え、引数をquoteで囲み、backslashを処理する
    • Pythonの subprocesslist2cmdline によりMicrosoft CRTのコマンドライン解析規則に合わせてエスケープする
    • その後のANSI変換で U+FF02が " U+0022 に変わると、元のコマンドライン構文が変化する
  • int main() のみを使うプログラムも脆弱でありうる
    • コンパイラは mainCRTStartup をバイナリに生成し、この開始関数がCRTライブラリとリンクされる
    • CRT内部がANSI APIでコマンドラインを取得して解析すると、Best-Fit変換が介入する
    • この動作のため、特定のプログラミング言語の標準ライブラリだけでは攻撃を完全に防ぐのが難しい

Argument Splittingの実例

  • ElFinderはPHPバックエンドベースのオープンソースWebファイルマネージャーで、デフォルトでWindowsサーバーと圧縮の作成・展開をサポートする
    • アーカイブ処理はshell commandの実行として実装されており、引数は escapeshellarg でエスケープされる
    • tar形式の処理にはWindows内蔵の tar.exe が使用される
    • aaa" "--use-compress-program=calc" "bbb.tar のようなtarファイル名で --use-compress-program 引数を注入し、任意コマンド実行が可能になる
    • デモはEnglish-configured Windows server、Code Page 1252基準で、125xコードページとCode Page 874でも動作するはずだとまとめられている
    • デモ動画は Video 12 で提供されている
  • TortoiseGitで使われる修正済み plink.exe の事例では、悪意のあるURIをclone入力として与えるとコード実行をトリガーできる
  • RStudioはSVNバージョン管理をサポートしており、悪意を持って作成されたフォルダにSVNプロジェクトがあると、クリック1回で電卓を起動できる
  • Microsoft Excelの事例は、Argument SplittingとWindowsの「Open-With」機能を組み合わせた CVE-2024-49026 である
    • Windowsはファイル拡張子ごとのhandler tableを維持しており、ftypeassoc で確認できる
    • ファイル名はhandlerプログラムの引数の一部になるため、ファイル名を通じて攻撃を適用できる
    • dots、slash、backslash、double quoteをfullwidth形式に置き換えたファイル名により、Excel.exe へのargument injectionを引き起こす
    • Excel自体には追加悪用に適した引数がないため、NTLM RelayとRBCD/ADCSを併用してRCEを達成する
    • デモ動画は Video 15

環境変数の混同

  • Environment Variable Confusion は、GetEnvironmentVariableAGetEnvironmentStringsAchar *getenv() が環境変数の Best-Fit 変換版を返すことで発生する
    • 影響を受けるコードページと脅威文字は特定されていない
    • Apache HTTPd の事例では 0x00-0xFF が関連する
  • この攻撃が成立するには、環境変数がユーザーによって制御可能である必要がある
    • 親プロセスが生成した子プロセスに情報を渡す場合がこれに当たる
    • CGI では、query string、HTTP header など HTTP リクエスト情報の多くが環境変数として渡される
  • WAF 回避の例は、CGI スクリプトが routing service のように動作する状況を扱う
    • Apache 設定には、/admin を含む REQUEST_URI を拒否して /cgi.pl/admin へのリモートアクセスを防ぐルールがある
    • Windows Perl の WorstFit 動作により、admin の一部を Best-Fit equivalent に置き換えると回避できる
    • Code Page 1250 では、à U+00E0 は ANSI 変換中に a に変わる
    • /cgi.pl/%E0dmin リクエストはサーバー側ルールでは別のパスに見えるが、Perl CGI スクリプトが ANSI API で PATH_INFO を読むと /admin として処理される
  • Windows 上の PHP-CGI では、特定の構成で file existence oracle と潜在的な LFI が確認された
    • 原因は PATH_INFO とその他の path 関連環境変数の処理方法にある
    • /index.php/foo/bar リクエストは Apache 基準で REDIRECT_URLREQUEST_URIPATH_INFOPATH_TRANSLATED のような環境変数として渡される
    • この情報だけでは PHP ファイル名と追加の PATH_INFO の境界を明確に区別しにくく、php-cgi.exe がこれを解釈する
  • Japanese コードページで ¥ を使うと、Web サーバーと PHP-CGI のパス解釈が異なる
    • Web サーバーは /..¥..¥windows/win.ini/foo 全体を追加の PATH_INFO として処理する
    • PHP-CGI は REQUEST_URI=/index.php/..\\..\\windows/win.ini/foo のように変換された値を受け取り、実際の PHP ファイルと PATH_INFO を区別する過程で混同する
    • Apache では、存在しないファイルと存在するファイルの応答差により file existence oracle が可能になる
    • IIS で doc_root directive が設定されている場合、/index.php/..¥..¥..¥windows/win.ini/ のようなパスで C:\\Windows\\win.ini を include して読み込む LFI が可能になる
    • include されたファイルが実行可能だったり、ユーザー制御可能なコードを含んでいたりする場合は潜在的に RCE につながるが、そのシナリオは実アプリケーションではまれな bug に近いものと分類される

公開と修正プロセスの難しさ

  • 研究チームは、プログラミング言語、オープンソースプロジェクト、Windows 内蔵 CLI プログラムの複数の問題を各 upstream maintainer に報告した
    • 最も大きな議論は Argument Splitting で発生した
    • 一部ベンダーは、ユーザー入力をコマンドラインに渡すこと自体が脆弱性だとみなした
  • 責任の所在が不明確な点も問題だった
    • 問題のあるコードが、コンパイル中に自動挿入される mainCRTStartup() と MSVCRT/UCRT 内部の ANSI API 呼び出しにまたがっている
    • 開発者が wmain() を使わなかったことが問題なのか、CRT がコマンドラインを誤って分割して main() に誤った引数を渡したことが問題なのか区別しにくい
    • 一部プロジェクトはソースコードのみを提供し、Windows prebuilt executable はインターネット上の第三者ボランティアが配布している
  • 修正は単に main() を wide-character 版に置き換えるだけではない
    • 関数シグネチャが変わると、変数定義と引数パースロジックを char * から wchar_t * ベースに書き直す必要がある
    • この過程は苦痛で、エラーが起きやすい
  • Curl は Windows 機能だとして修正計画はないと回答し、Microsoft が移植した Curl は entry を wmain() に修正しているため、Windows 内蔵の curl.exe は影響を受けない
    • Curl 公式ビルドバイナリは Argument Splitting 攻撃の影響を受ける
    • 全体報告は HackerOne で公開されている
  • OpenSSL は OPENSSL_WIN32_UTF8 環境変数により、引数を wide character 形式で処理できる
    • 元の目的は UI での UTF-8 表示問題を修正することだったが、Argument Splitting 攻撃の緩和にもなる
    • 標準的な OpenSSL 利用では、開発者がこの環境変数を設定すべきことを知らない場合が多く、-engine 引数を使った任意コード実行が可能になる
  • Perl 公式配布版は Windows prebuilt executable を提供しておらず、Strawberry Perl や ActiveState Perl のようなサードパーティ製インストーラーが一般的に使われる
    • 両配布版は Argument Splitting 攻撃の影響を受ける
    • Perl maintainer との議論の結果、「Perl のバグというより Microsoft のバグに近い」という結論となり、現在も未解決のままとなっている
  • Microsoft には 3 件が MSRC に報告されたが、いずれも当初は重大度基準を満たさないとして却下された
    • 複数回の再オープン後、Excel の事例だけが 3 回目の試行後に受理された
    • 他の事例は現在まで未解決のままである
    • MSRC は、別アプリケーションが信頼できない入力をコマンドラインに入れて実行する脆弱性に依存しており、それを悪用可能にする technique 自体は脆弱性要件を満たさないと回答した
  • CERT/CC にも支援を求め、Microsoft は数か月後に GetCommandLineA のドキュメントへ セキュリティ警告 を追加した
    • 警告は GetCommandLineA にのみ追加され、注意が必要な ANSI API はまだ残っている

報告された影響対象と状況

  • 公開プロセスで確認・報告された項目は次の通り
    • 2024/05/07: PHP php-cgi.exeCVE-2024-4577
    • 2024/06/13: Curl Official BuildWon’t Fix
    • 2024/06/13: Apache Subversion svn.exeCVE-2024-45720
    • 2024/06/16: Microsoft Tar tar.exe — Won’t Fix
    • 2024/06/19: Microsoft Excel excel.exeCVE-2024-49026
    • 2024/06/19: Microsoft PhoneBook rasphone.exe — Won’t Fix
    • 2024/06/19: Oracle Java java.exe — Pending Fix
    • 2024/06/19: Perl perl.exe — Won’t Fix
    • 2024/07/15: Perforce p4.exeCVE-2024-8067
    • 2024/08/05: PostgreSQL psql.exe — Won’t Fix
    • 2024/08/08: Putty plink.exe — Fixed
    • 2024/08/19: OpenSSL openssl.exe — Other
    • 2024/08/19: wkhtmltopdf wkhtmltopdf.exe — EOL
    • 2024/08/19: GNU Wget — No Reply

緩和策と残された攻撃面

  • WorstFit攻撃はオペレーティングシステムレベルの問題であるため、MicrosoftがすべてのWindowsエディションでUTF-8をデフォルトで有効にするまでは、類似の問題が今後も繰り返し現れる可能性がある
  • ユーザーができる対策は、WindowsのUTF-8オプションを確認して有効にすること
    • この機能はまだbeta段階として表示されており、副作用の有無ははっきりしていない
  • 開発者は可能な限りWide Character APIを使うべき
    • CRTも_wgetcwd_wgetenvのようなwide character版を提供している
    • non-wide経路を使い続けると、内部実装がANSI APIを呼び出す可能性があり、WorstFit攻撃にさらされるおそれがある
  • Windowsの下位互換性のため、ANSI APIが潜んでいる場所はほかにもある可能性がある
    • 例としてRegQueryValueAのようなWindows Registry queryが影響を受ける可能性はあるが、脆弱なシナリオを見つける必要がある
    • 研究チームはActive DirectoryでもBest-Fit動作を観測している

1件のコメント

 
GN⁺ 2025-01-10
Hacker News のコメント
  • これはかなり厄介な問題だ。Microsoft の「best fit」コードマッピングは、広範な Unicode を ASCII に変換する、公開されてはいるものの実質的には「感覚ベース」のマッパーで、システム全体に組み込まれている。
    このマッパーは非常に多くの場所でデフォルトでリンクされており、Microsoft の下位互換性に対する考え方からすると、今後も含まれ続けるように見える。エクスプロイトはたいてい、特殊なコードポイントがスラッシュ、ハイフン、引用符のようなものに「それっぽく」マッピングされることから生じる。現代的な言語の中では正しい Unicode として検査されるが、シェルコマンドや Win32 API に渡ると、制御を引き渡した後で別の方法で縮小変換される。curl の管理者が言うように、ここでは「curl は被害者」だが、犯人が誰なのかが問題だ。サーバーがユーザー入力を検証するときとシステムライブラリに渡すときで違う形につぶしてしまえば、結局問題が起きる。Win32 側で best fit 変換を無効にできる選択肢が解決策になり得るが、Windows の専門家ではないので推測だ。そうしたとしても、公式 API や、まだ無効化していないソフトウェアとは引き続き相互作用することになる。

    • opt-out は Unicode Windows API を使うこと、つまり "a" ではなく "w" で終わる関数を使うことだ。この方法は "\\?\" プレフィックスを付けるか、マニフェストを正しく設定すれば、260 文字超のパス問題も同時に解決し、Windows XP 以降で利用可能で推奨されてきた。
      非 Unicode API が今でもこれほど広く使われている理由はよく分からない。Windows 98 や Windows 2000 をサポートしたいからだとは考えにくい。
    • Windows には Windows XP から、レガシー動作を無効にする方法であるマニフェストファイルがある。マニフェストがないと、GetWindowsVersion でさえ現在のバージョンを返さなかったと記憶している。ここに opt-out を追加し、いずれ Visual Studio のデフォルトにするのは、それほど難しくなさそうだ。
      もう一つ必要なのは一種のリンティングだ。現代的なアプリケーションで ANSI WinAPI 関数を呼ぶ理由は通常ない。ロケールを UTF-8 に設定して 8 ビット関数だけを使う方法もあるだろうが、どれほどうまく動くかは分からない。argvprintfstd::cout が UTF-8 で動作し、変な変換なしに WinAPI 用の UTF-8/UTF-16 変換関数だけを使わせる設定やヘッダーもいくつかあると理解している。こうした手順を Microsoft が一か所に文書化すべきだ。
    • セキュリティ脆弱性かどうかにかかわらず、Windows で Unicode 引数を適切に処理できないなら、それは curl のバグでもある。
    • コードポイントを文字へ緩くマッピングする方式は、Unicode でいつも気になっていた。
  • これはある程度予想できることではあるが、W/A の混乱が起きていた頃に 10 年ほど Windows 開発と Wine API のハッキングをしていた身としても新しかった。
    Windows はカードゲームの Munchkin のようなもので、複数の機能が偶然かみ合うと、信じがたいほどランダムで強力なエクスプロイトにまとまることがある。ANSI サブシステムを UTF-8 に変えつつある点は歓迎でき、理論上はこうした問題の多くを緩和できるはずだ。Rust チームがプロセス生成 API にさらに別の修正を入れる必要があるのかも気になる。

    • Rust 標準ライブラリは基本的に ANSI API をほとんど使わない。記事では Rust に通用する攻撃は示されておらず、もしそのような攻撃があるならぜひ報告した方がよい。
      もちろん Rust がプロセス境界の向こう側で起きることを制御できるわけではない。Rust が実行したアプリケーションが ANSI API を使えばそちらに問題が起きるが、それはそのアプリケーションの責任だ。
  • 「ANSI を段階的に廃止し、Wide Character API の使用を推奨すること」は、記憶が正しければ NT 3.5 の頃から Microsoft の公式方針だった。
    残念ながら大きな障害の一つは、Microsoft の C/C++ ランタイムライブラリ msvcrt.dll の実装方法だ。_wfopen()_wgetenv() のような非標準の wide 関数は内部的に Win API の W 関数を使うが、fopen()getenv() のような標準の narrow 関数は wide 版に変換する代わりに A 関数をそのまま使う。そして A 関数は通常、Unicode 変換の失敗を報告せず best-fit 方式で上書きしてしまう。C で書かれたソフトウェアを Windows に移植する人が、標準関数の使用をすべて Microsoft の非移植関数に置き換えたいとは思わない。その時点からは実質的に全面的な書き直しだ。

    • ここ 2 年ほど Microsoft の文書を読んで受けた印象は逆だった。アプリケーションマニフェストで activeCodePageUTF-8 に設定し、「ANSI」関数だけを使え、という方向だった。
    • 移植可能なコードでは、Windows ビルドのときに mainfopen のような標準関数を wide 対応関数へ #define する。
      こうすると char* と装飾なしの文字列リテラルをそのまま使うことはできないので、Linux では char、Windows では wchar_t になる tchar 型と、文字列リテラル用の _T() マクロを定義する。たいていはあまり考えずにうまく動く。
    • 最近本当にいら立つのは、Google で Win32 API を検索すると、いつも -W 変種ではなく -A 変種が先に出てくることだ。robots.txt に何か変なものがあるのかは分からないが、新しいコードでは -W 変種を使えと推奨している API が、デフォルトでレガシー API を返すのはおかしい。
    • Microsoft の C/C++ ランタイム msvcrt.dllUniversal C Runtime(UCRT)[1] に置き換えられており、UCRT は C99 に準拠している。
    • Windows はパス名を、こんな馬鹿げたエンコーディング処理なしに、単にバイト列として扱う API を提供すべきだった。UNC パスを導入したときにそうできたようにも思う。
  • 自分で書いたアプリケーションやパッチした EXE で「Ansi」コードページを実際に UTF-8 に強制する方法は二つある。
    一つはマニフェストファイルを使う方法で、Windows 10 の特定のビルド以降で動作する。ビルド後の任意の EXE にも適用できるので、プログラムに UTF-8 対応を無理やり入れられる。コンソールモードのプログラムで特に有用だ。もう一つは「App Locale」系のツールが使うハックを使うことだ。ある方法には、NTDLL の文書化されていない関数呼び出しが含まれる。正確にどの関数が必要なのかは分からないが、RtlInitNlsTablesRtlResetRtlTranslations が関係しているかもしれない。

  • Microsoft がすべての Windows エディションで UTF-8 をデフォルトで有効にする可能性は、正直よく分からない。特定のコードページや「1文字あたり1バイト」を前提にした古いアプリケーションが多く、壊れる可能性がある
    さらに微妙なのは、ワイド文字から ANSI に変換するときにバイト数が増えないと仮定して、既存のバッファを再利用するアプリケーションもあること。UTF-8 ではそうはならず、従来のコードページの大半ではだいたい成り立っていたため、新たな脆弱性が生まれる可能性がある。むしろ Win32 の xxxA API から Best-Fit ロジックを取り除き、マッピングできない文字を、共通のメタ的意味を持たない x のような文字に置き換える方が、はるかに壊れにくそうだ

    • そうしたアプリケーションの例として Adobe After Effects がある[0]。少なくとも昔はそうだったし、今は Windows を使っていない
      [0] https://tambre.ee/blog/adobe_after_effects_windows_utf-8/
    • まだ存在しないなら OS API バージョンを導入して、新しい API バージョンや新しい SDK を対象にする新規・更新アプリでは、デフォルトで UTF-8 を前提にできるのではないかと思う。特定の API バージョン未満はレガシーモードでエミュレーションすればよい。Windows にはすでに、複数の Windows バージョンの挙動をまねる shim の概念がある
    • UTF-8 以前の Windows でも、既定のコードページを変更するとアプリがおかしくなる問題はすでにあった。だからユーザーに UTF-8 オプションを提供するのは合理的だ
      Best-Fit マッピングが引き起こす問題を見ると、それをデフォルトにするのも合理的だが、Microsoft はユーザーが古いコードを簡単に実行できる方法を見つけられるよう支援する必要があるだろう。あまり合理的でない方法としては、Best-Fit マッピングから「特殊な」ASCII 文字へのマッピングをすべてなくすことがあるが、CRT を静的リンクしたアプリには役に立たない。脆弱性を修正できるわけでもないので、良い解決策ではない。時にはセキュリティ脆弱性が後方互換性の破壊を推し進める動機になる
  • Microsoft は少なくとも1年前からこの問題を把握していた。CA2101[1] という特別なコード分析ルールを出し、best-fit マッピングの使用を明示的に推奨しないようにしていたからだ
    ルールの説明ではセキュリティ脆弱性に言及していたが、詳細は意図的に曖昧にされていた
    [1] https://learn.microsoft.com/en-us/dotnet/fundamentals/code-a...

  • すべてを char * から wchar * に変える必要はない。受け取ったワイド文字を UTF-8 に変換するか、対になっていないサロゲートのような不正なシーケンスまで許容したいなら Rust の WTF-8 のようなものに変換したうえで、そのまま char を使い続ければよい
    もちろん ANSI や OEMCP の文字列を UTF-8 文字列と混ぜないよう注意は必要だが、単に UTF-8 だけを使えば簡単だ。古典的な https://utf8everywhere.org/ サイトが推奨しているアプローチはこの方法だ

  • 個人用の Windows PC では数年前から UTF-8 モードを有効にしていたおかげで、このバグを偶然回避できていた。記事の下部に出ている設定だ
    古い海外ゲームで文字化けが表示されるため有効にしていたもので、「Beta」と表示されているが、バグや副作用は感じなかった

    • 興味深いが、自分の場合、そのチェックボックスはランダムなアプリを大量にクラッシュさせただけだった。オフになっているときのユーザーの既定コードページが何かによって、うまく動くかどうかが変わるようだ
    • たった今「Beta: Use Unicode UTF-8 for worldwide language support」オプションを有効にした。どれだけ多くのアプリが壊れるのか、興味深そうだ
  • ベータのチェックボックスが、マニフェストで ActiveCodePage を UTF-8 に設定することと同じなのか気になっていたが、ドキュメント[0]を見ると、GDI はプロセスごとのコードページには従わず、チェックボックスが設定する単一のグローバルコードページだけに従うと明記されている
    自分のアプリで *A API に対して完全に UTF-8 へ opt-in できないのは少し残念だ。それでも記事で強調されている問題に対しては、依然として有効な回避策や多層防御の手段になり得ると思う
    [0] https://learn.microsoft.com/en-us/windows/apps/design/global...

  • なんてことだ。Windows API がその種の best-fit 変換を提供していることは知っていたが、自分の既定コードページである 949[1] で、複数の ANSI 関数の既定動作になっているとは知らなかった
    ここまで来たら gets のように、単純に禁止すべきだ。[1] UTF-8 コードページ 65001 があることは知っている。長い間、本当に使いものにならないレベルだったし、今でも互換性問題に悩まされている