プロセス実行先・動的ライブラリーの操作

C/C++のプロセス実行先・動的ライブラリーの操作

説明

信頼できないパスやファイル名を、プロセス実行APIや動的ライブラリーAPIの対象として使うと、攻撃者がアプリケーションの読み込むコードや実行するコードを選べます。絶対パスなら攻撃者が用意したファイルを直接指定でき、ファイル名や相対パスなら PATH、ライブラリー検索パス、現在のディレクトリーなどの検索規則を悪用して、意図しないファイルを選択させることができます。

Windowsでは、CreateProcess*() の lpApplicationName に NULL を渡すと、lpCommandLine の最初のトークンから実行ファイル名が解釈されます。空白を含む実行ファイルのパスが曖昧な場合や、検索パスに攻撃者が書き込めるディレクトリーがある場合、別の実行ファイルが選ばれるおそれがあります。LoadLibrary*() は対象DLLの依存DLLにも検索規則を適用するため、対象を1つ固定するだけでは、すべてのDLL検索パスの問題を解消できません。

これは、シェルがコマンド文字列を解釈するOSコマンドインジェクションとは異なります。execve() や CreateProcessW() は通常シェルのメタ文字を解釈しませんが、外部入力が実行ファイル自体を選べば、攻撃者のコードが実行されるおそれがあります。また、実行ファイルを固定しても、信頼できない引数が対象プログラムのオプションや構文として解釈されないかは別に確認する必要があります。

想定される影響

  • アプリケーションの権限で、任意の実行ファイルや動的ライブラリーのコードが実行される可能性があります。
  • 高い権限を持つサービスでは、権限昇格、セキュリティ境界の回避、システム全体の侵害につながるおそれがあります。
  • DLLサイドローディングや検索パスの乗っ取りによって、攻撃者がアクセスを維持したり、正規プログラムの動作を変えたりする可能性があります。
  • 悪意のある対象や互換性のない対象を実行すると、機密情報の漏えいやサービス停止につながるおそれがあります。

対処方法

外部入力には実行パスではなく少数の業務上の識別子だけを受け付け、信頼できるコードや設定内の固定対象に対応付けてください。次の保護を併用してください。

  1. 実行ファイルとライブラリーには、信頼できるコードや設定に固定した明示的なフルパスを使用してください。パス、ファイル名、拡張子を外部入力から直接受け取らないでください。
  2. 許可した識別子に完全一致する場合だけ固定パスを選び、それ以外は拒否してください。パスの正規化、拡張子の確認、sanitize_path() といった関数名だけで実行を許可しないでください。
  3. POSIXでは固定の絶対パスを execve() や posix_spawn() に渡し、引数配列と必要最小限の信頼できる環境を別に構成してください。対象の選択に PATH を使わせない場合は、execvp()、execvpe()、execlp()、posix_spawnp() を避けてください。
  4. dlopen() や dlmopen() には信頼できるフルパスを渡し、対象と依存ライブラリーの検索パス、RPATH、RUNPATH、環境変数も確認してください。
  5. Windowsでは lpApplicationName に固定の実行ファイルのフルパスを指定し、lpCommandLine をAPIが要求する書き込み可能な別のバッファーで構成してください。DLLには LoadLibraryExW() と目的に合った制限的な LOAD_LIBRARY_SEARCH_* フラグを使い、必要に応じて SetDefaultDllDirectories() でプロセスの既定の検索方針も制限してください。
  6. 実行ファイル、ライブラリー、依存ファイルとそれらを格納するディレクトリーを、信頼できないユーザーが変更できないようにしてください。プロセスは最小限の権限で実行してください。
  7. コード署名やハッシュ検証は多層防御になりますが、信頼できるパスやファイルシステムの権限に代わるものではありません。検証から実行までの間にファイルが置き換わる競合も考慮してください。

例

C

変更前

c
#include <dlfcn.h>
#include <stddef.h>

int main(int argc, char **argv) {
    if (argc != 2) {
        return 2;
    }

    void *handle = dlopen(argv[1], RTLD_NOW);
    if (handle == NULL) {
        return 1;
    }
    dlclose(handle);
    return 0;
}

argv[1] がライブラリーのパスを直接決めるため、呼び出し側がアプリケーションの権限で読み込まれるコードを選べます。

変更後

c
#include <dlfcn.h>
#include <stddef.h>
#include <string.h>

static const char *plugin_path(const char *selector) {
    if (strcmp(selector, "image") == 0) {
        return "/opt/example/lib/image-plugin.so";
    }
    if (strcmp(selector, "report") == 0) {
        return "/opt/example/lib/report-plugin.so";
    }
    return NULL;
}

int main(int argc, char **argv) {
    if (argc != 2) {
        return 2;
    }

    const char *path = plugin_path(argv[1]);
    if (path == NULL) {
        return 2;
    }

    void *handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
    if (handle == NULL) {
        return 1;
    }
    dlclose(handle);
    return 0;
}

外部入力は image または report という識別子だけを選び、実際のライブラリーパスはコードに固定されています。デプロイ先では、これらのディレクトリーやライブラリーの依存ファイルも、信頼できないユーザーが変更できない必要があります。

固定のプロセスを起動する場合も、対象パスと引数を分離してください。

c
#include <spawn.h>
#include <stddef.h>

int run_worker(char *job_id) {
    char *const arguments[] = {
        "worker", "--job-id", job_id, NULL
    };
    char *const environment[] = {
        "LANG=C", NULL
    };
    pid_t child;

    return posix_spawn(
        &child,
        "/opt/example/bin/worker",
        NULL,
        NULL,
        arguments,
        environment);
}

この例では実行ファイルを絶対パスに固定し、引数と環境を別に渡します。job_id には業務上許可する長さ、文字、値の検証も適用し、対象プログラムがオプションや別の構文として解釈しないかを確認してください。

C++

変更前

cpp
#include <dlfcn.h>
#include <iostream>
#include <string>

int main() {
    std::string path;
    if (!std::getline(std::cin, path)) {
        return 2;
    }

    void *handle = ::dlopen(path.c_str(), RTLD_NOW);
    if (handle == nullptr) {
        return 1;
    }
    ::dlclose(handle);
    return 0;
}

std::string に格納したり c_str() で変換したりしても、選ばれたコードの実行が正当に許可されたことにはなりません。この例は標準入力から受け取ったパスのライブラリーをそのまま読み込みます。

変更後

外部入力は2つの業務識別子のいずれかだけを選び、実際のパスはコードに固定されています。

cpp
#include <dlfcn.h>
#include <iostream>
#include <string>
#include <string_view>

static const char *plugin_path(std::string_view selector) {
    if (selector == "image") {
        return "/opt/example/lib/image-plugin.so";
    }
    if (selector == "report") {
        return "/opt/example/lib/report-plugin.so";
    }
    return nullptr;
}

int main() {
    std::string selector;
    if (!std::getline(std::cin, selector)) {
        return 2;
    }

    const char *path = plugin_path(selector);
    if (path == nullptr) {
        return 2;
    }

    void *handle = ::dlopen(path, RTLD_NOW | RTLD_LOCAL);
    if (handle == nullptr) {
        return 1;
    }
    ::dlclose(handle);
    return 0;
}

std::filesystem::path でパスを表したり、canonical() で解決したりするだけでは、この識別子の対応付けに代わる保護にはなりません。

参考資料