Skip to content

CWE-114: Process Control - C

Overview

In C, CWE-114 vulnerabilities occur when loading shared libraries or executing processes without proper validation. On Unix/Linux, this involves dlopen(), dlsym(), and exec*() functions. On Windows, LoadLibrary() and CreateProcess() are vulnerable. Attackers exploit weak library loading through DLL hijacking, LD_PRELOAD attacks, and path manipulation.

Primary Defence: Use absolute paths for library loading with dlopen() and LoadLibrary(), validate library names against allowlists, verify library signatures when possible, and use execve() with explicit argument arrays and sanitized environments to prevent command injection and path manipulation attacks.

Common Vulnerable Patterns

Using dlopen() with relative path (Unix/Linux)

#include <dlfcn.h>
#include <stdio.h>

// VULNERABLE - Searches LD_LIBRARY_PATH, can be hijacked
void load_plugin(const char *plugin_name) {
    char library_path[256];

    // User controls plugin_name - attacker can manipulate search path
    snprintf(library_path, sizeof(library_path), "lib%s.so", plugin_name);

    // dlopen() searches current dir, LD_LIBRARY_PATH, system paths
    // Attacker can place malicious library in search path
    void *handle = dlopen(library_path, RTLD_LAZY);

    if (!handle) {
        fprintf(stderr, "dlopen failed: %s\n", dlerror());
        return;
    }

    // Execute code from potentially malicious library
}

// Attack: Set LD_LIBRARY_PATH=/tmp/evil or place evil library in current dir

Why this is vulnerable: Using relative paths with dlopen() allows attackers to control which library gets loaded by manipulating LD_LIBRARY_PATH, placing malicious libraries in the current directory, or exploiting the library search order to execute arbitrary code.

LoadLibrary() without absolute path (Windows)

#include <windows.h>

// VULNERABLE - Searches current directory first (DLL hijacking)
HMODULE load_library_unsafe(const char *dll_name) {
    // LoadLibrary searches:
    // 1. Application directory
    // 2. Current directory  ← DANGEROUS
    // 3. System32
    // 4. Windows directory
    // 5. PATH directories

    // Attacker places malicious DLL in current directory
    HMODULE hModule = LoadLibrary(dll_name);

    if (hModule == NULL) {
        fprintf(stderr, "LoadLibrary failed: %lu\n", GetLastError());
    }

    return hModule;
}

// Attack: Place evil.dll in current directory, gets loaded instead of system DLL

Why this is vulnerable: LoadLibrary searches the current directory before system directories, allowing DLL hijacking attacks where attackers place malicious DLLs with legitimate names in user-writable locations to achieve code execution.

system() with user-controlled input

#include <stdlib.h>
#include <stdio.h>

// VULNERABLE - Command injection
void convert_image(const char *input_file) {
    char command[512];

    // User controls input_file - can inject shell commands
    snprintf(command, sizeof(command), "convert %s output.png", input_file);

    // Executes via /bin/sh - subject to shell interpretation
    system(command);
}

// Attack: input_file = "input.jpg; rm -rf /"
// Executes: convert input.jpg; rm -rf / output.png

Why this is vulnerable: The system() function executes commands through a shell, allowing attackers to inject shell metacharacters (;, |, &, etc.) and execute arbitrary commands by manipulating the input string.

execve() with unsanitized PATH

#include <unistd.h>
#include <stdio.h>

// VULNERABLE - Uses PATH search, can execute wrong binary
void run_converter(const char *input_file) {
    char *args[] = {"convert", (char *)input_file, "output.png", NULL};

    // execlp() searches PATH for "convert"
    // Attacker can manipulate PATH to execute malicious binary
    execvp("convert", args);

    perror("execvp failed");
}

// Attack: Set PATH=/tmp/evil:$PATH with malicious "convert" binary

Why this is vulnerable: Using execvp() searches the PATH environment variable for the executable, allowing attackers who can manipulate PATH to execute malicious binaries instead of the intended program.

Secure Patterns

dlopen() with absolute path and validation (Unix/Linux)

#include <dlfcn.h>
#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <stdlib.h>
#include <sys/stat.h>

#define LIBRARY_DIR "/opt/app/lib"
#define MAX_LIBRARIES 3

static const char *allowed_libraries[] = {
    "libcrypto.so",
    "libssl.so",
    "libcustom.so"
};

void *load_library_secure(const char *library_name) {
    // Validate library is in allowlist
    int found = 0;
    for (int i = 0; i < MAX_LIBRARIES; i++) {
        if (strcmp(library_name, allowed_libraries[i]) == 0) {
            found = 1;
            break;
        }
    }

    if (!found) {
        fprintf(stderr, "Library not in allowlist: %s\n", library_name);
        return NULL;
    }

    // Construct absolute path
    char absolute_path[PATH_MAX];
    if (snprintf(absolute_path, sizeof(absolute_path), 
                 "%s/%s", LIBRARY_DIR, library_name) >= sizeof(absolute_path)) {
        fprintf(stderr, "Path too long\n");
        return NULL;
    }

    // Resolve to canonical path (eliminates .., symlinks)
    char canonical_path[PATH_MAX];
    if (realpath(absolute_path, canonical_path) == NULL) {
        perror("realpath failed");
        return NULL;
    }

    // Verify path hasn't escaped library directory
    if (strncmp(canonical_path, LIBRARY_DIR, strlen(LIBRARY_DIR)) != 0) {
        fprintf(stderr, "Path traversal attempt detected\n");
        return NULL;
    }

    // Verify file exists and is regular file
    struct stat st;
    if (stat(canonical_path, &st) != 0) {
        perror("stat failed");
        return NULL;
    }

    if (!S_ISREG(st.st_mode)) {
        fprintf(stderr, "Not a regular file\n");
        return NULL;
    }

    // Load with absolute path - bypasses LD_LIBRARY_PATH
    void *handle = dlopen(canonical_path, RTLD_NOW | RTLD_LOCAL);

    if (!handle) {
        fprintf(stderr, "dlopen failed: %s\n", dlerror());
        return NULL;
    }

    return handle;
}

Why this works:

  • Absolute paths bypass LD_LIBRARY_PATH and current-directory search.
  • Allowlists ensure only approved libraries are loadable.
  • realpath() collapses .. and symlinks to prevent traversal.
  • Canonical path prefix checks prevent directory escape.
  • File type validation blocks non-regular files.

LoadLibraryEx() with LOAD_LIBRARY_SEARCH_SYSTEM32 (Windows)

#include <windows.h>
#include <stdio.h>

#define MAX_PATH_LENGTH 260

HMODULE load_library_secure(const TCHAR *dll_name) {
    // Load from System32 only (no current-directory search)
    HMODULE hModule = LoadLibraryEx(
        dll_name,
        NULL,
        LOAD_LIBRARY_SEARCH_SYSTEM32
    );

    if (hModule == NULL) {
        fprintf(stderr, "LoadLibraryEx failed: %lu\n", GetLastError());
        return NULL;
    }

    return hModule;
}

// Alternative: Load from application directory only
HMODULE load_app_library_secure(const TCHAR *dll_name) {
    TCHAR app_path[MAX_PATH_LENGTH];
    TCHAR full_path[MAX_PATH_LENGTH];

    // Get application directory
    if (GetModuleFileName(NULL, app_path, MAX_PATH_LENGTH) == 0) {
        fprintf(stderr, "GetModuleFileName failed\n");
        return NULL;
    }

    // Remove executable name, keep directory
    TCHAR *last_slash = _tcsrchr(app_path, TEXT('\\'));
    if (last_slash) {
        *last_slash = TEXT('\0');
    }

    // Construct full path
    HRESULT hr = StringCchPrintf(full_path, MAX_PATH_LENGTH,
                                 TEXT("%s\\%s"), app_path, dll_name);
    if (FAILED(hr)) {
        return NULL;
    }

    // Load from application directory only
    return LoadLibraryEx(
        full_path,
        NULL,
        LOAD_LIBRARY_SEARCH_APPLICATION_DIR
    );
}

Why this works:

  • Search flags restrict loading to trusted directories only.
  • The current-directory search is eliminated, blocking DLL hijacking.
  • Absolute paths avoid PATH-based resolution.
  • GetSystemDirectory()/GetModuleFileName() anchor paths to trusted locations.

execve() with absolute path and cleared environment

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <sys/wait.h>

#define ALLOWED_BINARY_DIR "/usr/bin"

int execute_command_secure(const char *input_file, const char *output_file) {
    // Validate input file path
    char input_canonical[PATH_MAX];
    if (realpath(input_file, input_canonical) == NULL) {
        perror("realpath failed for input");
        return -1;
    }

    // Verify input is within allowed directory
    const char *allowed_input_dir = "/opt/app/uploads";
    if (strncmp(input_canonical, allowed_input_dir, 
                strlen(allowed_input_dir)) != 0) {
        fprintf(stderr, "Input file outside allowed directory\n");
        return -1;
    }

    // Use absolute path to binary - bypasses PATH
    const char *binary_path = "/usr/bin/convert";

    // Arguments as array (no shell interpretation)
    char *args[] = {
        "convert",
        input_canonical,
        (char *)output_file,
        NULL
    };

    // Create minimal safe environment (no LD_PRELOAD, no LD_LIBRARY_PATH)
    char *env[] = {
        "PATH=/usr/bin:/bin",
        "HOME=/tmp",
        "LANG=C",
        NULL
    };

    pid_t pid = fork();

    if (pid == -1) {
        perror("fork failed");
        return -1;
    }

    if (pid == 0) {
        // Child process
        execve(binary_path, args, env);

        // execve only returns on error
        perror("execve failed");
        _exit(1);
    }

    // Parent process - wait for child
    int status;
    if (waitpid(pid, &status, 0) == -1) {
        perror("waitpid failed");
        return -1;
    }

    if (WIFEXITED(status)) {
        return WEXITSTATUS(status);
    }

    return -1;
}

Why this works:

  • Absolute binary paths bypass PATH search hijacking.
  • A clean environment removes LD_PRELOAD/LD_LIBRARY_PATH abuse.
  • realpath() validation blocks path traversal.
  • execve() avoids shell parsing and metacharacter injection.

Secure library loading with signature verification (Unix)

#include <dlfcn.h>
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/sha.h>
#include <openssl/crypto.h>
#include <limits.h>

#define LIBRARY_DIR "/opt/app/lib"

typedef struct {
    const char *name;
    const unsigned char *sha256_hash;
} trusted_library_t;

static const unsigned char libcrypto_hash[] = {
    0xab, 0xcd, 0xef, /* ... 32 bytes total ... */
};

static const trusted_library_t trusted_libraries[] = {
    {"libcrypto.so", libcrypto_hash},
    {NULL, NULL}
};

int verify_library_hash(const char *path, const unsigned char *expected_hash) {
    FILE *f = fopen(path, "rb");
    if (!f) {
        perror("fopen");
        return 0;
    }

    EVP_MD_CTX *ctx = EVP_MD_CTX_new();
    const EVP_MD *md = EVP_sha256();
    unsigned char hash[EVP_MAX_MD_SIZE];
    unsigned int hash_len;

    EVP_DigestInit_ex(ctx, md, NULL);

    unsigned char buffer[8192];
    size_t bytes;
    while ((bytes = fread(buffer, 1, sizeof(buffer), f)) > 0) {
        EVP_DigestUpdate(ctx, buffer, bytes);
    }

    EVP_DigestFinal_ex(ctx, hash, &hash_len);
    EVP_MD_CTX_free(ctx);
    fclose(f);

    // Compare hashes (constant-time to prevent timing attacks)
    return CRYPTO_memcmp(hash, expected_hash, SHA256_DIGEST_LENGTH) == 0;
}

void *load_trusted_library(const char *library_name) {
    // Find in trusted list
    const trusted_library_t *lib = NULL;
    for (int i = 0; trusted_libraries[i].name != NULL; i++) {
        if (strcmp(library_name, trusted_libraries[i].name) == 0) {
            lib = &trusted_libraries[i];
            break;
        }
    }

    if (!lib) {
        fprintf(stderr, "Library not in trusted list\n");
        return NULL;
    }

    // Construct absolute path
    char path[PATH_MAX];
    snprintf(path, sizeof(path), "%s/%s", LIBRARY_DIR, library_name);

    // Verify hash before loading
    if (!verify_library_hash(path, lib->sha256_hash)) {
        fprintf(stderr, "Library hash verification failed - possible tampering\n");
        return NULL;
    }

    // Load verified library
    return dlopen(path, RTLD_NOW | RTLD_LOCAL);
}

Why this works:

  • SHA-256 verification detects tampering or replacement.
  • Attackers cannot swap libraries without failing the hash check.
  • Absolute paths and allowlists reduce the load surface.
  • Defense-in-depth requires name, path, and hash to match.

Verification

After implementing secure library loading with absolute paths and allowlist validation, verify the fix through multiple approaches:

  • Manual testing: Attempt to load libraries from unauthorized paths and verify they're rejected
  • Code review: Confirm all library loading uses absolute paths with no relative paths or environment-dependent searches
  • Static analysis: Use security scanners to verify no process control vulnerabilities exist
  • Regression testing: Ensure legitimate library loading and application functionality continue to work correctly
  • Edge case validation: Test with path traversal attempts (../, symlinks) to verify proper validation
  • Environment isolation: Verify LD_LIBRARY_PATH and LD_PRELOAD cannot influence library loading
  • Hash verification: If implemented, confirm cryptographic signatures are validated before loading
  • Process inspection: Check /proc/<pid>/maps (Linux) or equivalent to verify libraries loaded from expected absolute paths only
  • Rescan: Run the security scanner again to confirm the finding is resolved

Additional Resources