Manipulation of process or dynamic-library targets

Manipulation of process or dynamic-library targets in C/C++

Description

Using an untrusted path or filename as the target of a process execution or dynamic-library API lets an attacker select code that the application loads or runs. An absolute path can point directly to an attacker-controlled file. A filename or relative path can exploit lookup rules such as PATH, library search paths, and the current directory to select an unintended file.

On Windows, passing NULL for lpApplicationName in CreateProcess*() causes the executable name to be parsed from the first token of lpCommandLine. An ambiguous executable path containing spaces, or an attacker-writable directory in the search path, can select a different executable. LoadLibrary*() also applies search rules to the target DLL's dependencies, so fixing only the target does not resolve every DLL search-path issue.

This differs from operating-system command injection, where a shell interprets a command string. execve() and CreateProcessW() do not normally interpret shell metacharacters, but attacker code can still run if external input selects the executable itself. Even with a fixed executable, separately assess whether untrusted arguments become options or syntax in the target program.

Potential impact

  • Arbitrary executable or dynamic-library code may run with the application's permissions.
  • In a privileged service, this can cause privilege escalation, bypass a security boundary, or compromise the system.
  • DLL sideloading or search-path hijacking can establish persistence or alter legitimate program behavior.
  • Malicious or incompatible targets can disclose sensitive information or disrupt service.

Remediation

Accept only a small set of business identifiers from external input, and map them to fixed targets in trusted code or configuration. Apply these controls together:

  1. Use explicit full paths fixed in trusted code or configuration for executables and libraries. Do not accept their paths, filenames, or extensions directly from external input.
  2. Select a fixed path only on an exact match to an allowed identifier; reject other values. Path normalization, extension checks, or a function name such as sanitize_path() do not authorize execution.
  3. On POSIX, pass a fixed absolute path to execve() or posix_spawn(), with a separate argument array and a minimal trusted environment. Avoid execvp(), execvpe(), execlp(), and posix_spawnp() when target selection must not use PATH.
  4. Pass a trusted full path to dlopen() or dlmopen(). Review search paths, RPATH, RUNPATH, and environment variables for both the target and its dependencies.
  5. On Windows, set lpApplicationName to the fixed executable's full path and build lpCommandLine in the separate writable buffer required by the API. For DLLs, use LoadLibraryExW() with restrictive LOAD_LIBRARY_SEARCH_* flags appropriate to the task. Where needed, also restrict the process's default search policy with SetDefaultDllDirectories().
  6. Prevent untrusted users from modifying executables, libraries, their dependencies, or the directories containing them. Run the process with minimum permissions.
  7. Code signatures and hashes can provide defense in depth but do not replace trusted paths and filesystem permissions. Account for file replacement between verification and execution.

Examples

C

Before

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] directly determines the library path, allowing the caller to select code loaded with the application's permissions.

After

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;
}

External input selects only the image or report identifier; the actual library paths are fixed in code. In deployment, untrusted users must also be unable to modify these directories or the libraries' dependencies.

Separate the target path from arguments when launching a fixed process as well:

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);
}

This fixes the executable to an absolute path and supplies arguments and the environment separately. Validate job_id for the application's permitted length, characters, and values, and separately check whether the target program reinterprets it as an option or other syntax.

C++

Before

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;
}

Storing input in std::string or converting it with c_str() does not authorize the selected code to run. This example loads the library at the path received from standard input.

After

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;
}

External input selects only one of two business identifiers; the actual paths are fixed in code. Representing a path with std::filesystem::path or resolving it with canonical() does not replace this identifier mapping.

References