LoadLibraryAによるDLL読み込み順序とDLLプリロード攻撃


はじめに

Windowsで割と有名なDLLプリロード攻撃がどうして起こるのか。
どのようなコードの場合に発生するのか・例外はあるのかについて色々実験してみた。

JPCERTではLoadLibraryベースの攻撃やSearchPathベースの攻撃などが載っているが
どこまで現時点では事実なのか、古い情報なのかを分類するために実際にやってみた。

このJPCERTの記事の最終更新日、7年前ですよ。何らかのセキュリティ周りの更新はWindowsに入ってても良いはず。

OS:Windows10 1903(Build 18362.356)

これってトリビアの種になりませんか?

user32.dllをプログラムと同じフォルダに置いたら先に読み込まれるのはsystem32にある方ですか?それともexeファイルと同じフォルダにあるものですか?これってトリビアの種になりませんか?

LoadLibraryA 単体

// 以下のソースではここから
#include <windows.h>
#include <tchar.h>
#include<stdio.h>

typedef int (*MSGBOX)(HWND,LPCWSTR,LPCWSTR,UINT);
// ここまでが省略されています

int main(){
    // DLLを読み込む
    HMODULE user32 = LoadLibraryA("user32.dll");
    // MessageBox関数のポインタを取得する
    MSGBOX F_MessageBoxW =(MSGBOX) GetProcAddress(user32, "MessageBoxW");
    // 使う
    F_MessageBoxW(NULL,L"hello world!",L"not malware!",0);
    FreeLibrary(user32);
}

OSに存在するDLLの読み込み(上記のソース)

結果

規定のSystem32ディレクトリにあるuser32.dllが検索されずに直接使われることが分かる。
(そもそもMessageBoxW以外の関数で(ランタイム初期化時に)使われるため)

ファイルシステムに"存在しない"DLLの読み込み

結果

以下の順序で検索がかかることが確認できる(MSDNに記述の通り)
1. プログラムが存在するディレクトリ
2. system32ディレクトリ
3. system(16bit)ディレクトリ
4. windowsディレクトリ
5. カレントディレクトリ
6. 環境変数に登録されているディレクトリ

ファイルシステムに存在するがOSのものではないDLL読み込み(念の為)

int main(){
    test();
}

DLLのソース(test.dll、C:\Windows\System に配置する)

#include<stdio.h>
__declspec(dllexport) int test(void){
    printf("test(init) running.\n");
    return 0;
}

結果

検索がプログラムの配置フォルダから始まっていることが分かる。
やはり直接は使われない模様。

では、DLL使用側のプログラムでLoadLibraryを使ってみる
DLL側に関数を一つだけ追加する

#include<stdio.h>
// この関数を追加(LoadLibraryから呼ばれる)
__declspec(dllexport) int MessageBoxW(void* p,const short* text,const short* caption,unsigned int t){
    printf("testdll MessageBoxW\n");
    return 0;
}
// libファイルをリンクしたことで呼ばれる
__declspec(dllexport) int test(void){
    printf("test(init) running.\n");
    return 0;
}

利用側

int main(){
    // 使う
    test();
    // test.dllをロードする
    HMODULE htest = LoadLibraryA("test.dll");
    // MessageBox関数のポインタを取得する
    MSGBOX F_MessageBoxW =(MSGBOX) GetProcAddress(htest , "MessageBoxW");
    F_MessageBoxW(NULL,L"hello world!",L"not malware!",0);

    FreeLibrary(htest);
}

このようにしても、実行結果(Procmon)の出力は変わらず。
おそらく1度ロードされたDLLは検索せずに使い回されるのかなと。しらんけど。
でも、このコードには欠陥があるのでまだ安心はできない。

CWDから検索すること自体をやめさせる必要がある。
 => ネットワークフォルダに置いたDLLを読み込ませることができてしまう可能性がある

SearchPathA + LoadLibraryA

クソ危険だからやめろと言われている最悪の組み合わせ

int main(){
    char user32_path[255]={0};
    char *file_name = NULL;
    // DLLのサーチを行う(!!!!!Unsafe!!!!!)
    SearchPathA(NULL,"user32.dll",NULL,255,user32_path,&file_name);
    // サーチしたDLLパスからDLLを読み込む
    HMODULE user32 = LoadLibraryA(user32_path);
    // MessageBox関数のポインタを取得する
    MSGBOX F_MessageBoxW =(MSGBOX) GetProcAddress(user32, "MessageBoxW");
    // 使う
    F_MessageBoxW(NULL,L"hello world!",L"not malware!",0);
    FreeLibrary(user32);
}

結果

はい、最悪ですね。Systemディレクトリ等に検索が走らず、カレントディレクトリ(C:\attacker)にあるuser32.dllが直接読み込まれました。
(上記のtest.dllを改名してC:\attackerディレクトリに置いて、exeを直叩きしただけ。)

これは、SearchPath自体の検索のロジックがイケてない(LoadLibraryと整合性が取れていないため。DLL以外の検索もするから良いっしょwというノリかもしれない、しらんけど)

SetSearchPathMode + SearchPathA + LoadLibraryA

カレントディレクトリの優先順位を下げる感じにしてみる。

int main(){
    char user32_path[255]={0};
    char *file_name = NULL;
    // BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODEを指定して安全にする。
    SetSearchPathMode(BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE);
    // DLLのサーチを行う
    SearchPathA(NULL,"user32.dll",NULL,255,user32_path,&file_name);
    // サーチしたDLLパスからDLLを読み込む
    HMODULE user32 = LoadLibraryA(user32_path);
    // MessageBox関数のポインタを取得する
    MSGBOX F_MessageBoxW =(MSGBOX) GetProcAddress(user32, "MessageBoxW");
    // 使う
    F_MessageBoxW(NULL,L"hello world!",L"not malware!",0);
    FreeLibrary(user32);
}

結果

未ロードのDLLをLoadLibraryしたときみたいになる。

まとめ

Windows 10上ではLoadLibraryだけ使うとすでに読み込まれたDLLを使う。
読み込まれていない場合は

  1. プログラムが存在するディレクトリ
  2. system32ディレクトリ
  3. system(16bit)ディレクトリ
  4. windowsディレクトリ
  5. カレントディレクトリ
  6. 環境変数に登録されているディレクトリ

の順で読み込まれる。
SearchPathを何も対策せずに使う場合は、

  1. プログラムが存在するディレクトリ
  2. カレントディレクトリ
  3. system32ディレクトリ
  4. system(16bit)ディレクトリ
  5. windowsディレクトリ
  6. 環境変数に登録されているディレクトリ

SearchPathをSetSearchPathMode(BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE);で使う場合

  1. プログラムが存在するディレクトリ
  2. system32ディレクトリ
  3. system(16bit)ディレクトリ
  4. windowsディレクトリ
  5. カレントディレクトリ
  6. 環境変数に登録されているディレクトリ

ただし、同ファイル名のDLLがすでにロードされているかどうかをチェックしない(挙動からの推測)ので1番で読み込まれる。
これが単純にLoadLibraryしたときとSearchPathしたときの大きな差。

ある程度OSに改良が入っているので、アプリ側はある程度安心できるけど、やっぱりマズイ書き方もあるのでちゃんとこういう検証をしてから作りましょう。
というか、前提として、JPCERTが出しているセオリーを全部実装してればこんなことは起こらないのでセオリー通りにやれば問題なし。

参考にした資料

マイクロソフトサポート,DLL プリロード攻撃を防止するためのライブラリの安全な読み込み
JPCERT,第9回 WindowsのDLLだけが危ないのか?DLL hijacking vulnerability概説(後編)