DXライブラリのバッファオーバーフローの脆弱性が修正された

2015年11月、DXライブラリの書式指定描画関数におけるバッファオーバーフロー脆弱性IPAに報告しました。2015年12月29日に修正バージョンVer 3.16が公開され、JVNでも情報が公開されました(JVN#49476817)。 最悪の場合、任意のコードが実行されます。一定の条件を満たしているアプリケーションのみが影響を受けますが、DXライブラリを利用してアプリケーションを開発されている方は、念のため最新バージョンでビルドしなおすことをお勧めします。

私としてはソフトウェアの脆弱性IPAに報告するのは初めての経験なのですが、今後の参考のために、発見の経緯や気をつけたいことを記したいと思います。

脆弱性の内容

DXライブラリは、Windows向けのC++用ゲームライブラリです。DXライブラリ 管理人様の情報によると

 DrawFormatString や printfDx などの書式指定文字列を引数とする関数で、書式に基づいて生成される文字列の長さが 1023文字( 関数によっては 2047文字 )を超えるとバッファオーバーフローが発生します。
 スタック領域に対するメモリの不正なアクセスが発生してしまうので、引数として渡すデータを工夫することで任意のコードが実行できてしまいます。

と説明されています。具体的には以下のようなコードは脆弱性の影響を受けます。

char buf[4096];
/* (外部からbufに文字列を読み込むコード) */
DrawFormatString(0, 0, GetColor(255, 255, 255), "%s", buf);

これだけ見ると、『画面に描画するための文字列にそんなに大きなバッファを確保する訳がない』とか、『ライブラリの脆弱性というより2048文字以上処理できないという仕様であって、アプリケーション側の実装の問題では』と思われる方もいるかもしれません。しかし、DrawFormatStringのリファレンスにはそのようなことは書かれていませんし、下記のような実装がされているアプリケーションも十分考えられるため、DXライブラリの脆弱性として報告しました。

char buf[256];
/* (外部からbufに文字列を読み込むコード) */
DrawFormatString(0, 0, GetColor(255, 255, 255), buf);  // format string attackが可能
std::string buf;
/* (外部からbufに文字列を読み込むコード) */
DrawFormatString(0, 0, GetColor(255, 255, 255), "%s", buf.c_str());

具体的な名前は書きませんが、私は3つのフリーソフトでこの脆弱性が存在することを確認しました。

発見の経緯

2015年11月10日のことです。きっかけは、私が大学で所属している部活の部員が制作した音ゲーのテストプレイ中のことでした。非常に長い文字列を曲のタイトルに設定するとアプリケーションが異常終了することに気が付きました。 そのゲームは、DXライブラリを使って開発されており、異常終了の原因を調べているうちに、どうやらDrawFormatString関数の内部関数でバッファオーバーフローが発生していることがわかりました。

その日から実証コードと報告内容を作成し、10日後の11月20日に報告しました。

CL_vsprintf

JVNの解説に登場するCL_vsprintf()とは、名前から推測できるようにC言語の標準関数であるvsprintf()とほぼ同じ動作をするDXライブラリの内部関数です。DXライブラリの書式指定系関数はすべてこの関数を使って書式変換を行っています。vsprintf()は、書き込み先バッファのサイズをチェックしないため、バッファオーバーフローを引き起こしやすい関数の一つとして知られています。MSVCではvsprintf()を使用すると、この関数は安全ではないとして、コンパイラ警告 C4996が発生します。

複数のlibファイル

DXライブラリは、Windows 98を現在でもサポートしており、開発環境としてVisual C++ 6.0をサポートするために、Visual C++ 6.0でコンパイルされています*1。 このバージョンのVC++では、/GSや/SAFESEHといったコンパイラのセキュリティ機能はサポートされていません。DXライブラリは静的ライブラリであるため、アプリケーションのビルド時の/GSオプションの有無にかかわらず、stack canaryによるバッファオーバーランのチェックが行われません。このことは攻撃を容易にし、事態をより深刻にするように思います。

ただし、DXライブラリ Ver 3.14cより、Visual Studio 2012・2015でビルドされたライブラリが追加されており*2、/GSも有効になっています。 ソースコードのDxDataTypeWin.hを読むとわかるのですが、アプリケーションをビルドする時のVisual Studioのバージョンによってリンクされるlibファイルを切り替えるようになっています。Visual Studio 2010以下のMSVCでビルドすると、VC++6.0でコンパイルされたlibファイルとリンクされているため、注意が必要です。

実証コード

この脆弱性によって発生しうる脅威を説明するために、2種類のデモアプリケーションを作成しました。

  • ファイルから読み込んだ文字列を描画するアプリケーション
  • ネットワークから受信した文字列を描画するアプリケーション

どちらも本質的には同じなのですが、後者はリモートでの攻撃が可能なことを示すために(そのようなアプリケーションが実際に存在するかは別として)作成しました。

以下は、作成したデモアプリケーションの一つです。ファイルから文字列を読み込み、DrawFormatString()関数で描画します。

#include <Windows.h>
#include <fstream>
#include <string>
#include "DxLib.h"

#ifndef INPUT_FILE
#define INPUT_FILE "input.txt"
#endif

int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
    std::ifstream ifs(INPUT_FILE);
    std::string buf;

    if (!ifs.fail()) {
        getline(ifs, buf);
        ifs.close();
    }

    SetUseDirectInputFlag(FALSE);   // デバッガの動作を軽くするため、入力処理にDirectInputを使用しない
    ChangeWindowMode(TRUE);

    if (DxLib_Init() == -1)
        return 0;

    ClearDrawScreen();
    DrawFormatString(0, 0, GetColor(255, 255, 255), "%s", buf.c_str());
    WaitKey();

    DxLib_End();
    return 0;
}

exploitは悪用可能なので公開しませんが、技術的に特に新しいことはしていません。 /GS有り、DEP有り、ASLR無し、SafeSEH無しなexeファイル向けにSEH overwrite+ROPのexploitを作成した程度です。

ユーザが気をつけたいこと

  • 特別な理由がない限りアプリケーションは最新版を使用する
  • 信頼できない入手先のセーブデータ、リプレイデータ、追加データを不用意に追加しない
  • ネットワーク対応のアプリケーション・ゲームで不審なサーバーに接続しない

脆弱性攻撃が行われる場合、pdfファイルやdocxファイルからマルウェアに感染することもあります。そのソフトウェアに脆弱性が存在する場合、追加ファイルの拡張子がexeでないから安全とは言えません。 一般的なことしか書いていませんが、未知の脆弱性に備えて、DXライブラリで開発されたアプリケーションに限らず心がけたいところです。

開発者が気をつけたいこと

  • [脆弱性の内容]に示したようなコードで書式文字列攻撃が可能な場合、この脆弱性の悪用が容易になります
  • コンパイラは最新版を使いましょう。万が一、脆弱性があっても/GS(バッファのセキュリティチェック)、/NXCOMPAT(DEP)、/SAFESEHや/DYNAMICBASE(ASLR)といった機能のおかげで攻撃か困難になります
  • ライブラリを過信しすぎない。std::stringはバッファオーバーフローしないと思っていると、思わぬ抜け穴があるものです

思うところ

管理人様は、少なくとも2008年にはDrawFormatStringがバッファオーバーフローすること自体は認識されていたようです*3。当時はそれが任意のコード実行につながると認識されていなかったのだと思います。 私も実際の攻撃手法を知ったのはここ2,3年のことです。

こんなブログ記事がありました( C言語簡単にクラッシュしすぎワロタw · DQNEO起業日記)。この方を名指ししたいわけではありませんが、この記事は小さなバッファに対してgets()を使うと簡単にクラッシュすることを指摘しています。 この指摘自体は正しいですが、バッファオーバーフローを単にクラッシュするだけと捕えているか、任意コードの実行につながると捕えているかの意識の差だと思いました。