Search This Blog

Showing posts with label installer. Show all posts
Showing posts with label installer. Show all posts

10/9/25

CVE-2025-23297 NVIDIA FrameView SDK Local Privilege Escalation and DLL hijacking

CVE-2025-23297 NVIDIA Local Privilege Escalation via DLL hijacking

Reverse Engineering NVIDIA FrameView SDK installer and escalating priviliges via CVE-2025-23297

Overview

(Note: This article is for educational and security research purposes only. The information provided should not be used for any malicious activities, nor I bear any responsibility for any misuse of it)

Pasted image 20251009192446.png

On 29.9.2025 Nvidia disclosed a high severity vulnerability in the NVIDIA installer for NvAPP. Since no deep dive documentation is present for this CVE I was down for the task.

This document contains my reverse engineering analysis of CVE-2025-23297, a local privilege escalation vulnerability in NVIDIA FrameView SDK. Through binary analysis using Ghidra, I identified multiple instances where the installer creates directories with NULL security attributes, enabling DLL hijacking attacks.

Disclosure of this vulnerability was only carried out by Dong-uk Kim and JunYoung Park of KAIST Hacking Lab.

Key Info:

  • Vulnerability Type: Insecure directory creation → DLL hijacking
  • Root Cause: CreateDirectoryW() called with NULL security attributes
  • Affected: NVIDIA App installer v11.0.4.526 and earlier
  • Fixed: NVIDIA App v11.0.5.245
  • Impact: Local privilege escalation (standard user → SYSTEM)

Files Analyzed

Extracted files from both patched and vulnerable versions of the NvApp installer to compare:

Compared Versions:

  • NVIDIA_app_v11.0.4.526.exe (vulnerable)
  • NVIDIA_app_v11.0.5.245.exe (patched)

Key Components:

  • nvfvsdksvc_x64.exe
  • installation scripts
  • core SDK scripts: bin_x64.dll
  • container application: FvContainer.exe

Initial Analysis - Unpacking and Comparing Installers

Unpacking the Installers

Before diving into binary analysis with Ghidra, I needed to understand the scope of changes between the vulnerable and patched versions. I unpacked both NVIDIA App installers to extract their contents.

Unpacking tools used:

  • Uni-Extract 2 for initial extraction

Once unpacked, I had two directory trees to compare - one from the vulnerable version and one from the patched version.

PowerShell Comparison Script

I wrote a PowerShell script to systematically compare the two unpacked installer directories. The script performed two levels of comparison:

  1. Whole directory comparison - comparing ALL files across both installers
  2. FrameView-specific comparison - focusing on FrameView-related files

Whole Directory Comparison Results

The first pass comparing all files across both unpacked installers revealed:

  • Several new files were created in the patched version
  • Some files were deleted
  • Many files had size differences (indicating modifications)

Since this was quite a large surface to examine and nothing much caught my eye immediately, I decided to focus more on the FrameView-specific components, as NVIDIA’s security bulletin specifically mentions the FrameView SDK installation process.

FrameView Directory Comparison Results

Narrowing down to FrameView-related files, the comparison revealed these modified files:

Pasted image 20251009144011.png

Key findings from the comparison:

  • Multiple executables and DLLs were modified
  • Installation scripts were updated
  • The main service executable nvfvsdksvc_x64.exe showed changes

All these were files that theoretically I should examine, but since NVIDIA’s bulletin states “the issue is during the installation process,” I decided to start with the installer executable itself: nvfvsdksvc_x64.exe.

This strategic decision paid off, as the installer contained the vulnerable CreateDirectoryW() calls that create the weak directory permissions.


Technical Background - CreateDirectoryW Function

Before diving into the Ghidra analysis, let me explain the Windows API function that’s at the center of this vulnerability.

The CreateDirectoryW Function

The CreateDirectoryW() function creates a new directory and can apply a specified security descriptor to the new directory if the underlying file system supports security.

Function Signature:

BOOL CreateDirectoryW(
  LPCWSTR               lpPathName,          // [rcx] or stack
  LPSECURITY_ATTRIBUTES lpSecurityAttributes // [rdx] or stack
);

Parameters:

  • lpPathName - The path of the directory to be created
  • lpSecurityAttributes - Pointer to a SECURITY_ATTRIBUTES structure that specifies a security descriptor for the new directory. If lpSecurityAttributes is NULL, the directory inherits the default security descriptor from its parent directory.

x64 Windows calling convention:

  • 1st parameter (lpPathName) → RCX register
  • 2nd parameter (lpSecurityAttributes) → RDX register

Why NULL Security Attributes Are Dangerous

When CreateDirectoryW is called with NULL security attributes:

  • The directory inherits default permissions from its parent (C:\ProgramData)
  • This often results in BUILTIN: Read & Execute, Write permissions
  • Any local user can write files to this directory
  • When NVIDIA FrameView SDK loads files from this directory with elevated privileges, it can execute attacker’s code

Windows DLL Search Order

This is crucial to understand the attack. When a process loads a DLL, Windows searches in this order (with SafeDllSearchMode enabled, which is default):

  1. The directory where the application loaded fromAttacker wins here!
  2. The system directory (C:)
  3. The Windows directory (C:)
  4. Current directory
  5. Directories in PATH

So if an attacker can plant a malicious DLL in the application directory before a privileged process loads it, game over.


Analysis - Opening the Installers in Ghidra

Since NVIDIA mentions the issue is caused during the installation process of the FrameViewSDK, I deduced that the vulnerability is likely related to how directories are created. Such attacks are often accompanied by specific Win32 API calls, particularly the CreateDirectoryW() function.

Pasted image 20251008173206.png

Finding CreateDirectoryW References

Upon searching the two disassembled files, I noticed that the function is differently named by Ghidra in the vulnerable installer compared to the patched version (different addresses, different function names due to recompilation). But in both installers, we can find the CreateDirectoryW function in the Symbol tree.

Pasted image 20251008174227.png

Both of these functions use this Windows function. Comparing the pointers:

Pasted image 20251008174618.png

Locating All References

In Ghidra’s Symbol Tree → Exports, I navigated to the function CreateDirectoryW() and inspected all references to this function inside both executables.

Vulnerable version: Pasted image 20251008182651.png

Patched version: Pasted image 20251008182935.png

Result: 3 call sites to CreateDirectoryW in each version (the 4th entry is just the import table pointer itself, not an actual call).


First Vulnerable Call - Addresses 0x140006c78 & 0x14000b542

Location Investigation

Navigating to the first reference in the vulnerable version, it points to address 0x140006c78, which is part of the address space of function FUN_140006b10.

I did this for both versions:

Vulnerable: Pasted image 20251008183307.png

Patched: Pasted image 20251008183334.png

Version Tracking in Ghidra

After finding the addresses, I used Ghidra’s Version Tracking feature to compare the two versions.

The vulnerable call is at address 0x140006c78 inside function FUN_140006b10. I filtered the Version Tracking Matches window for this specific function, and Ghidra’s correlator gave me several function matches on the vulnerable exe side.

Ghidra, using the BSim Function Matching Algorithm, matched the function where CreateDirectoryW() was called in the vulnerable version (FUN_140006b10) to function FUN_140008130 in the patched version.

This clearly pointed out that I should investigate these two functions more deeply. And after inspecting the whole decompiled code…

BINGO! Found the Vulnerability

Inside the vulnerable version, they’re setting the LPSECURITY_ATTRIBUTES to 0x0, meaning NULL - no security attributes are applied!

Vulnerable code (FUN_140006b10):

void FUN_140006b10(undefined8 ****param_1, longlong *param_2, char param_3)
{
  LPVOID pvVar1;
  code *pcVar2;
  int iVar3;
  LPCWSTR ***ppppWVar4;
  // ... variable declarations ...
  
  // Get known folder path
  iVar3 = SHGetKnownFolderPath(&DAT_14008afa0, 0, 0);
  if (iVar3 != 0) {
    CoTaskMemFree(local_70);
    OutputDebugStringA("SHGetKnownFolderPath Failed\n");
    return;
  }
  
  // Build path: \NVIDIA Corporation\FrameView
  FUN_14000a270((longlong *)&local_60, local_70, uVar7);
  FUN_14000a390(&local_60, (undefined8 *)L"\\NVIDIA Corporation", 0x13);
  FUN_14000a390(&local_60, (undefined8 *)L"\\FrameView", 10);
  
  if (param_3 != '\0') {
    FUN_14000a390(&local_60, (undefined8 *)&DAT_140097ef0, 3);
  }
  
  ppppWVar4 = &local_60;
  if (7 < local_48) {
    ppppWVar4 = (LPCWSTR ***)local_60;
  }
  
  // [!!!!] VULNERABLE: NULL security attributes!
  CreateDirectoryW((LPCWSTR)ppppWVar4, (LPSECURITY_ATTRIBUTES)0x0);
  //                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  //                                    This is the bug!
  
  // Rest of the function...
}

Patched code (FUN_140008130):

void FUN_140008130(LPCWSTR ****param_1, longlong *param_2, char param_3)
{
  // ... variable declarations ...
  _SECURITY_ATTRIBUTES local_168;  // <<<<<<<<<<< New: Security attributes structure!
  uint uStack_14c;
  PSID local_288;
  PSID pvStack_280;
  PSID local_278;
  // ... more security-related variables ...
  
  // Initialize security structures
  FUN_14008aad0((undefined1 (*) [32])&local_288, 0, 0x120);
  local_288 = (PSID)0x0;
  pvStack_280 = (PSID)0x0;
  local_278 = (PSID)0x0;
  // ... initialize more security fields ...
  
  // Build proper security attributes with ACL
  uVar4 = FUN_140006cf0(&local_288, (undefined8 *)&local_168);
  //                     ^^^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^^
  //                     SIDs        Populates security attributes
  
  if (uVar4 == 0) {  // Success
    // Build directory path...
    ppppppppWVar11 = &local_2f8;
    if (7 < local_2e0) {
      ppppppppWVar11 = (LPCWSTR *******)local_2f8;
    }
    
    // SECURE: Proper security attributes passed!
    CreateDirectoryW((LPCWSTR)ppppppppWVar11, &local_168);
    //                                         ^^^^^^^^^^^
    //                                         Restricts access
    
    // ... rest of function ...
  }
  
  // Cleanup security resources
  if (local_288 != (PSID)0x0) {
    FreeSid(local_288);
  }
  if (pvStack_280 != (PSID)0x0) {
    FreeSid(pvStack_280);
  }
  // ... more cleanup ...
}

Assembly Verification

Looking at the assembly at address 0x14006c78 (vulnerable):

14006c70  46 0f 47 3c 24 10    CMOVA   param_1=>local_60, qword ptr [RSP + 0x33]
14006c78  ff 15 12 34 05 00    CALL    qword ptr [->KERNEL32.DLL::CreateDirectoryW]

Before this CALL, the second parameter (RDX) was set to 0 (NULL) via XOR RDX, RDX earlier in the function.

The Complete Security Fix

The patched version adds three key things:

  1. Declares a SECURITY_ATTRIBUTES structure:

    _SECURITY_ATTRIBUTES local_168;
  2. Initializes it with proper security settings:

    FUN_140006cf0(&local_288, (undefined8 *)&local_168);

    This function sets up proper ACLs (Access Control Lists) to restrict who can access the directory - only Administrators and SYSTEM, NOT regular users.

  3. Passes the structure to CreateDirectoryW:

    CreateDirectoryW(path, &local_168);  // Instead of NULL

Comparing with Meld

The tool Meld is great for comparing these functions as it highlights the differences. Even though its main use is for git management, it works well for comparing decompiled code.

Inspecting the two variants, we can see some differences. Besides some code updates, additions, and compiler optimizations (running the same code through the same compiler doesn’t always guarantee identical binary output - compilers can reorder instructions and variables), we can clearly spot the introduction of the _SECURITY_ATTRIBUTES variable in the patched version.

Pasted image 20251009014933.png

Why This Matters - The Attack

When CreateDirectoryW is called with NULL security attributes:

  • The directory inherits default permissions from its parent (C:\ProgramData)
  • Often results in Everyone: Full Control or Users: Write permissions
  • Any local user can place malicious DLLs in this directory
  • When NVIDIA FrameView SDK service (running as SYSTEM) loads files from this directory, it executes the attacker’s code with SYSTEM privileges

The attack is simple:

  1. Attacker monitors for the directory creation (takes like 50ms to detect)
  2. Immediately plants a malicious DLL (like version.dll)
  3. Waits for the NVIDIA service to start
  4. Service loads the malicious DLL from the application directory (Windows searches there FIRST)
  5. PRIVILEGE ESCALATION - code execution as SYSTEM

There’s NO narrow race condition here - the attacker has a wide window (maybe 30+ seconds during installation) to plant the DLL. This makes it trivially exploitable.


Done? No. Investigating Other References

Despite these findings, I decided to continue digging so as not to leave the job half done. There were still two more references to check. I’m glad I continued, as the attack surface expanded :)


Second Vulnerable Call - Addresses 0x14002055a & 0x1400256ee

Function Location

  • Vulnerable: Address 0x14002055a inside function FUN_1400203e0
  • Patched: Address 0x1400256ee inside function FUN_1400253b0

I repeated the same procedures I did with the first call - finding where the call was made, what function uses it, and comparing the two versions.

Finding: TWO Vulnerable Calls in One Function!

Upon inspection, I found something surprising - the vulnerable version has TWO insecure CreateDirectoryW() calls in the same function! Kinda strange - the developers are using this Windows function multiple times without reading the documentation at all (either this), or they’re purposefully excluding security attributes (which I doubt, of course).

Vulnerable version (FUN_1400203e0):

void FUN_1400203e0(undefined8 *param_1, DWORD *param_2, byte param_3)
{
  // ... lots of setup code ...
  
  // Build path for FvContainer
  FUN_14000ac10(&local_80, param_1);
  pwVar9 = L"\\FvContainer\\FvContainer.exe";
  if (param_3 != 0) {
    pwVar9 = L"\\FvContainer\\FvContainer.System.exe";
  }
  
  // Get special folder path and build: \NVIDIA Corporation\FrameViewSDK
  FUN_14001e550(local_60);
  FUN_14000a270((longlong *)&local_100, ppppuVar7, local_50);
  FUN_14000a390(&local_100, (undefined8 *)L"\\NVIDIA Corporation", 0x13);
  FUN_14000a390(&local_100, (undefined8 *)L"\\FrameViewSDK", 0xd);
  
  ppppWVar6 = &local_100;
  if (7 < local_e8) {
    ppppWVar6 = (LPCWSTR ***)local_100;
  }
  
  // [!!!!!!!] VULNERABILITY #1: FrameViewSDK directory with NULL security!
  CreateDirectoryW((LPCWSTR)ppppWVar6, (LPSECURITY_ATTRIBUTES)0x0);
  
  // Now build MessageBus subdirectory path
  // This part is interesting - they're manually writing bytes to build the string
  builtin_wcsncpy((wchar_t *)((longlong)ppppWVar5 + uVar12 * 2), L"\\Message", 8);
  *(undefined4 *)((longlong)ppppWVar5 + (uVar12 + 8) * 2) = 0x750062;  // "bu" in little-endian
  *(undefined2 *)((longlong)ppppWVar5 + (uVar12 + 10) * 2) = 0x73;     // "s"
  // This creates: \MessageBus
  
  ppppWVar6 = &local_a0;
  if (7 < uStack_88) {
    ppppWVar6 = (LPCWSTR ***)local_a0;
  }
  
  // [!!!!!!!] VULNERABILITY #2: MessageBus subdirectory with NULL security!
  CreateDirectoryW((LPCWSTR)ppppWVar6, (LPSECURITY_ATTRIBUTES)0x0);
  
  // ... rest of function ...
}

String Construction Analysis

That weird byte-writing code deserves explanation:

*(undefined4 *)((longlong)ppppWVar5 + (uVar12 + 8) * 2) = 0x750062;
*(undefined2 *)((longlong)ppppWVar5 + (uVar12 + 10) * 2) = 0x73;

This manually writes bytes to extend the string. The values 0x750062 and 0x73 correspond to the little-endian representation of wide characters 'b', 'u', 's' (forming the string "bus"). This appends "bus" to create the path \\MessageBus.

Pasted image 20251009134300.png

What This Means - More Attack Vectors!

This finding means the insecure usage of CreateDirectoryW() opens another attack vector for injecting malicious DLLs, but this time in a different folder: \\MessageBus.

The vulnerable installer creates these directories with weak permissions:

  1. C:\ProgramData\NVIDIA Corporation\FrameViewSDK - vulnerable
  2. C:\ProgramData\NVIDIA Corporation\FrameViewSDK\MessageBus - vulnerable

Patched Version - Proper Security

In the patched version (FUN_1400253b0), the function takes a 4th parameter (just pointing this out, I believe it’s not relevant to the CVE) and correctly sets the security attributes:

Pasted image 20251009132529.png
Pasted image 20251009132806.png

Patched version (FUN_1400253b0):

void FUN_1400253b0(undefined8 *param_1, DWORD *param_2, byte param_3, undefined8 param_4)
{
  _SECURITY_ATTRIBUTES local_390;  // Security attributes declared
  PSID local_378;
  PSID pvStack_370;
  // ... more security variables ...
  
  // Initialize security structures
  FUN_14008aad0((undefined1 (*) [32])&local_378, 0, 0x120);
  local_378 = (PSID)0x0;
  pvStack_370 = (PSID)0x0;
  // ... more initialization ...
  
  // Build security attributes with proper ACL
  uVar4 = FUN_140006cf0(&local_378, (undefined8 *)&local_390);
  
  if (uVar4 == 0) {  // Success
    // Build FrameViewSDK directory path
    ppppWVar7 = &local_450;
    if (7 < local_438) {
      ppppWVar7 = (LPCWSTR ***)local_450;
    }
    
    // SECURE: First CreateDirectoryW with proper security
    CreateDirectoryW((LPCWSTR)ppppWVar7, &local_390);
    
    // Build MessageBus subdirectory path
    // ... path construction code ...
    
    ppppWVar7 = &local_3f0;
    if (7 < uStack_3d8) {
      ppppWVar7 = (LPCWSTR ***)local_480;
    }
    
    // SECURE: Second CreateDirectoryW with SAME security attributes
    CreateDirectoryW((LPCWSTR)ppppWVar7, &local_390);
  }
  
  // Cleanup security resources
  if (local_378 != (PSID)0x0) FreeSid(local_378);
  if (pvStack_370 != (PSID)0x0) FreeSid(pvStack_370);
  // ... more cleanup ...
}

The patched version initializes the security attributes ONCE and uses them for BOTH directory creation calls. This is good coding practice - reusable security policy.

FvContainer Executable Context

Looking at the vulnerable function, it’s creating directories for FvContainer (the FrameView Container process). The code builds paths to executables:

FUN_14000a390(&local_80, (undefined8 *)L"\\FvContainer\\FvContainer.exe", ...);
// or
FUN_14000a390(&local_80, (undefined8 *)L"\\FvContainer\\FvContainer.System.exe", ...);

There are at least TWO different executables that can be exploited:

  • FvContainer.exe (regular mode)
  • FvContainer.System.exe (system mode - likely runs as SYSTEM!)

The code also builds command lines for plugin loading:

FUN_14000a390(&local_c0, (undefined8 *)L" -d \"", 5);
FUN_14000ac10(&local_c0, local_110);  // Adds plugin directory path
FUN_14000a390(&local_c0, (undefined8 *)L"\\FvContainer\\plugins\" ", ...);

When FvContainer.exe or FvContainer.System.exe runs:

  • It loads DLLs from the application directory
  • Application directory = C:\ProgramData\NVIDIA Corporation\FrameViewSDK\
  • Finds attacker’s malicious DLL (Windows searches application dir FIRST)
  • Executes with elevated privileges (SYSTEM if it’s the .System.exe version)

MessageBus Directory - IPC Attack Vector

The MessageBus subdirectory suggests inter-process communication. Looking at the code building \MessageBus, this is likely used for IPC between different FrameView components. Processes might load plugins or DLLs from the MessageBus directory to communicate. Perfect DLL hijacking target!

Real Attack Scenario

Step 1: Attacker plants malicious DLL

# Vulnerable directories created with weak permissions:
$targets = @(
    "C:\ProgramData\NVIDIA Corporation\FrameView",
    "C:\ProgramData\NVIDIA Corporation\FrameViewSDK",
    "C:\ProgramData\NVIDIA Corporation\FrameViewSDK\MessageBus"
)

foreach ($dir in $targets) {
    if (Test-Path $dir) {
        # Plant malicious version.dll in each location
        Copy-Item "evil.dll" "$dir\version.dll"
        Write-Host "[+] Planted in: $dir"
    }
}

Step 2: FvContainer.exe launches

When the executable runs:

  • Loads DLLs from application directory
  • Finds attacker’s malicious DLL first (Windows DLL search order)
  • Executes malicious code with elevated privileges

Step 3: Multiple processes = Multiple exploit chances

Conditional execution means different modes:

if (param_3 != 0) {
  pwVar11 = L"\\FvContainer\\FvContainer.System.exe";  // System service
} else {
  pwVar11 = L"\\FvContainer\\FvContainer.exe";          // Normal mode
}

More executables = more opportunities to exploit.


Third Reference - Completing the Picture

The cool thing is that by analyzing these two functions, we also examined the 3rd reference that Ghidra found, since that address is inside one of the functions we already looked at!

Pasted image 20251009135218.png

Version Tracking confirmation:

Pasted image 20251009140455.png

Summary of Findings

Vulnerability Count

Total CreateDirectoryW() calls analyzed: 3 calls (across 2 functions)

  • Function 1: 1 vulnerable call
  • Function 2: 2 vulnerable calls (same function!)

All 3 calls in the vulnerable version use NULL security attributes. All 3 calls in the patched version use proper security attributes.

Affected Directories

Directory Permissions (Vulnerable) Attack Vector
C:\ProgramData\NVIDIA Corporation\FrameView Inherited (weak) DLL hijacking
C:\ProgramData\NVIDIA Corporation\FrameViewSDK Inherited (weak) DLL hijacking
C:\ProgramData\NVIDIA Corporation\FrameViewSDK\MessageBus Inherited (weak) IPC/Plugin DLL hijacking

Exploitable Processes

Process Privilege Level Loads From
nvfvsdksvc_x64.exe SYSTEM FrameView directories
FvContainer.exe User/Admin FrameViewSDK directories
FvContainer.System.exe SYSTEM FrameViewSDK directories

How the Attack Works

No Race Condition Required

I initially thought this might be a race condition, but it’s actually much simpler (of course, if someone thinks otherwise, please write a comment and let me fix the article :). The attacker has a wide window of opportunity:

Time 0:   NVIDIA installer starts
Time 0.1s: CreateDirectoryW() creates vulnerable directories
Time 0.2s: Attacker's monitoring script detects new directories
Time 0.3s: Attacker plants malicious DLL(s)
Time 30s:  Installation continues (WIDE WINDOW - no race!)
Time 45s:  Service starts or admin launches FrameView
Time 45s:  Malicious DLL loaded by privileged process
Time 45s:  Code execution achieved as SYSTEM

The key insight: The attacker can plant the DLL immediately when the folder is created and doesn’t need to wait for a specific time. Since Windows searches the application directory FIRST when loading DLLs, the malicious DLL will be found before legitimate ones in System32.

Attack Complexity: LOW

  • No narrow timing window
  • No sophisticated techniques needed
  • Simple filesystem monitoring
  • Standard user account sufficient
  • High success rate

Finding Target DLLs

Attackers can identify which DLL to hijack using:

Method 1: Static Analysis (Ghidra)

  1. Open nvfvsdksvc_x64.exe in Ghidra
  2. Search for LoadLibrary() / LoadLibraryEx() calls
  3. Find strings referencing DLL names
  4. Identify DLLs loaded from ProgramData

Method 2: Dynamic Analysis (Easier)

Use Process Monitor (Procmon):
1. Filter: Process = nvfvsdksvc_x64.exe
2. Filter: Operation = CreateFile
3. Filter: Path contains .dll
4. Filter: Result = NAME NOT FOUND

Output shows DLLs the process tries to load but can't find.
Plant a malicious DLL with that name!

Common Target DLLs:

  • version.dll - version checking
  • dwmapi.dll - desktop window manager
  • profapi.dll - user profiles
  • cryptbase.dll - cryptography
  • Application-specific: nvfvconfig.dll, nvfvhelper.dll

Comparison: Vulnerable vs Patched

Function-Level Changes

Function Vulnerable Patched Changes
FUN_140006b10FUN_140008130 NULL security Proper ACL Added security structures, ACL init
FUN_1400203e0FUN_1400253b0 NULL security (2x) Proper ACL (2x) Added shared security structure

Finding Hijackable DLLs Using Ghidra

Before crafting our PoC exploit, I needed to analyze what DLLs are used by the installer and service to identify the best hijacking targets. This section documents the systematic approach to finding DLL hijacking opportunities.

Quick note: in this article I will not publish the working exploit/PoC since I am still working on it and I felt that the article publication was more important from ethical standpoint. Soon when I am done with the PoC I will reference a link here, to it in another post.

Despite that since later in the article I explain which DLLs are valid for hijacking, version.dll I believe would be a good target, since it is a common .dll and also if one is interested for more advanced fooling around, a more advanced technique could be applied (relatively easy) when hijacking such as DLL Proxying, as often DLL Hijacks cause a binary break and tools like Spartacus take care of this. It is able restore the native flow of the DLL execution. There are great explanations for this out there.

Methodology Overview

The process involves:

  1. Static analysis - Using Ghidra to find all DLL references
  2. String search - Locating .dll strings in the binary
  3. Import table analysis - Distinguishing static vs dynamic loads
  4. Prioritization - Identifying the most exploitable targets

Step 1: Searching for DLL Strings

First, I used Ghidra’s string search to find all DLL references in nvfvsdksvc_x64.exe.

Ghidra Steps:

1. Search → For Strings
2. Minimum Length: 5
3. Click "Search"
4. In "Defined Strings" window, Filter: .dll

Going into the Symbol Tree and examining the imports, I saw quite many functions. So I decided to Search for Strings and filter out only the .dll extensions.

Pasted image 20251009180820.png

Initial Results:Step 2: Analyzing DLL Load Patterns

Next, I examined how these DLLs are loaded by looking at the decompiled code around each DLL string reference.

Example 1: drvstore.dll

Navigating to the reference for drvstore.dll: Decompiled code shows:

if ((DAT_1400b5d60 != (HMODULE)0x0) || 
    (pVar4 = GetLastError(), pVar4 == (LPWSTR)0x7e)) {
  if (param_1 == 0) {
    lpLibFileName = L"drvstore.dll";
  }
}
else {
  UVar3 = GetSystemDirectoryW((LPWSTR)0x0,0);
  pWVar6 = (LPWSTR)LocalAlloc(0x40,(ulonglong)(uVar3 + 1) * 2 + 2);
  if (pWVar6 != (LPWSTR)0x0) {
    UVar3 = GetSystemDirectoryW(pWVar6,(ulonglong)(UVar3 + 1) * 2 + 2);
    if ((pWVar6[UVar3 - 1] != L'\\') {
      pWVar6[UVar3] = L'\\';
    }
  }
  
  pwVar10 = L"drvstore.dll";
  pWVar11 = pWVar6;
  do {
    iVar2 = (int)pwVar11;
    uVar1 = iVar2 + 1;
    pWVar11 = pwVar11 + 1;
  } while (uVar1 < 0xc);
  
  DAT_1400b5d60 = LoadLibraryExW(pwVar6,(HANDLE)0x0,local_158);
}

Key observation: The code builds a path to drvstore.dll and uses LoadLibraryExW. This is dynamically loaded, making it a potential hijacking target.

Example 2: devobj.dll (ldp.dll reference in image)

Decompiled code shows:

if ((_DAT_1400b5dd8 != (HMODULE)0x0) || 
    (pVar4 = GetLastError(), pVar4 == GetLastError())) {
  if (param_1 == 0) {
    lpLibFileName = L"ldp.dll";
  }
}

Similar pattern - dynamic loading via LoadLibrary.

Example 3: cryptnet.dll

Looking at the cryptnet.dll reference: Decompiled code:

if ((_DAT_1400b5dd8 != (HMODULE)0x0) || 
    (pVar4 = GetLastError(), pVar4 == (LPWSTR)0x7e)) {
  if (param_1 == 0) {
    pwVar15 = L"cryptnet.dll";
  }
}
else {
  // Path construction code...
  DAT_1400b5dd8 = LoadLibraryExW(pwVar6,(HANDLE)0x0,local_158);
}

Example 4: cryptbase.dll

Examining cryptbase.dll references: String location:

140097af0  63 00 72  unicode  u"cryptbase.dll"
           00 79 00
           70 00 74

XREF References:

XREF[4,2]:  FUN_140001680:140001b9c(*),
            FUN_140001680:140001ba2(R),
            FUN_140001680:140001bc2(*),
            FUN_140001680:140001bc2(R)

Multiple references indicate this DLL is loaded in several places! Decompiled code:

if ((DAT_1400b5db0 != (HMODULE)0x0) || 
    (pVar4 = GetLastError(), pVar4 == (LPWSTR)0x7e)) {
  if (param_1 == 0) {
    pwVar13 = L"cryptbase.dll";
  }
}
else {
  UVar3 = GetSystemDirectoryW((LPWSTR)0x0,0);
  pWVar6 = (LPWSTR)LocalAlloc(0x40,(ulonglong)(uVar3 + 1) * 2 + 2);
  // ... path construction ...
  pwVar13 = L"cryptbase.dll";
  // ... more code ...
  DAT_1400b5db0 = LoadLibraryExW(pwVar6,(HANDLE)0x0,local_158);
}

String reference confirmation:

140097940  63 00 72  unicode  u"cryptbase.dll"
           00 79 00
           70 00 74

XREF References:

XREF[4,2]:  FUN_140001680:140001ba9e(*),
            FUN_140001680:140001ba2(R),
            FUN_140001680:140001bc1(*),
            FUN_140001680:140001b18

Step 3: Examining Import Table for VERSION.dll

While examining the import table in Ghidra, I noticed functions that belong to version.dll:

Import Table Analysis:

IMAGE_IMPORT_BY_NAME_15_1400aab6
  dw    10h
  ds    "VerQueryValueW"

IMAGE_IMPORT_BY_NAME_24_1400aac8  
  dw    04h
  ds    "GetFileVersionInfoSizeW..."
  
IMAGE_IMPORT_BY_NAME_20_1400aae2
  dw    00h
  ds    "GetFileVersionInfoA"

Key functions imported:

  • VerQueryValueW - Retrieves version information
  • GetFileVersionInfoSizeW - Gets size of version info
  • GetFileVersionInfoA - Retrieves file version info

These functions are from VERSION.dll, indicating this DLL is loaded to check file versions.

Step 4: Distinguishing Hijackable vs Non-Hijackable

After analyzing all DLL references, I categorized them:

HIJACKABLE - Dynamically Loaded

These DLLs are loaded via LoadLibrary/LoadLibraryEx with either:

  • Relative paths
  • No path specified (searches application directory first)
  • Conditional loading based on availability

Primary Targets:

  1. version.dll - PRIMARY TARGET

    • Functions: VerQueryValueW, GetFileVersionInfoSizeW
    • Usage: Version checking functionality
    • Load method: Dynamic (likely LoadLibrary with no path)
    • Windows searches application directory FIRST
  2. cryptbase.dll

    • Usage: Cryptography base functions
    • Load method: LoadLibraryExW with path construction
    • Multiple references (4 XREFs)
    • Loaded conditionally
  3. cryptnet.dll

    • Usage: Cryptography network functions
    • Load method: LoadLibraryExW
    • Similar pattern to cryptbase.dll
  4. drvstore.dll

    • Usage: Driver store operations
    • Load method: LoadLibraryExW
    • Path construction from System directory
  5. wldp.dll

    • Usage: Windows Lockdown Policy
    • Load method: Dynamic
    • Loaded conditionally
  6. devobj.dll (referenced as ldp.dll in code)

    • Usage: Device object operations
    • Load method: Dynamic

NOT HIJACKABLE - Static Imports

These are in the import table and loaded by Windows before any code executes:

  • KERNEL32.dll - Core Windows API
  • USER32.dll - Window management
  • ADVAPI32.dll - Advanced Windows API
  • SHELL32.dll - Shell functions
  • ole32.dll - COM/OLE
  • bcrypt.dll - Cryptography (newer, static)
  • SHLWAPI.dll - Shell lightweight API

Priority Rankings:

Rank DLL Score Reasoning
1 version.dll 13 Imported functions visible, definitely loaded, application directory searched first, SYSTEM service, multiple version-checking calls
2 cryptbase.dll 11 4 XREFs, dynamic loading, cryptography usage (important), SYSTEM service, error handling present
3 cryptnet.dll 9 Similar to cryptbase, network crypto operations, conditional loading
4 wldp.dll 8 Windows policy DLL, conditional loading, SYSTEM context
5 drvstore.dll 7 Driver store operations, system integration
6 devobj.dll 6 Device operations, less critical

Why version.dll is the Primary Target

version.dll is the best hijacking target because:

  1. Ubiquitous Usage: Many Windows applications check file versions
  2. Application Directory Search: Windows DLL search order checks app directory FIRST
  3. Not Critical for Loading: If it fails, the app usually continues (error handling present)
  4. Confirmed Usage: Import table clearly shows VerQueryValueW, GetFileVersionInfoSizeW, GetFileVersionInfoA
  5. SYSTEM Privileges: Loaded by nvfvsdksvc_x64.exe which runs as NT AUTHORITY

DLL Load Location Analysis

Examining the code patterns, the DLLs follow this loading behavior:

// Typical loading pattern found in the binary
HMODULE hDll;

// Try loading from application directory first
hDll = LoadLibraryExW(L"version.dll", NULL, 0);

if (hDll == NULL) {
    // If not found, Windows automatically searches:
    // 1. Application directory ← WE CONTROL THIS!
    // 2. System32 directory
    // 3. System directory
    // 4. Windows directory
    // 5. Current directory
    // 6. PATH directories
}

// Use the DLL
FARPROC pFunc = GetProcAddress(hDll, "VerQueryValueW");

Attack window:

  1. Installer creates C:\ProgramData\NVIDIA Corporation\FrameViewSDK\ with weak permissions
  2. Attacker plants version.dll in that directory
  3. Service starts and tries to load version.dll
  4. Windows finds attacker’s DLL FIRST (application directory)
  5. Malicious code executes as SYSTEM

Conclusion

Through systematic reverse engineering analysis using Ghidra, I identified the critical local privilege escalation vulnerability in NVIDIA FrameView SDK’s installer. The root cause is consistent use of CreateDirectoryW() with NULL security attributes across multiple functions, creating three vulnerable directories that enable DLL hijacking attacks.

Key Takeaways:

  1. Systemic Issue: The same insecure pattern appeared in 3 separate locations, suggesting lack of secure coding guidelines or security review
  2. Wide Attack Surface: Multiple directories, multiple processes, multiple exploitation paths
  3. Easy Exploitation: No race condition, wide time window, low technical complexity
  4. Multiple DLL Targets: Identified 6+ hijackable DLLs with version.dll as primary target
  5. Proper Fix: NVIDIA’s patch correctly implements Windows security descriptors with restrictive ACLs for all affected calls

This vulnerability demonstrates the critical importance of proper security attribute configuration in Windows API calls, especially in privileged installation contexts. Small oversights (passing NULL instead of proper security attributes) can lead to complete system compromise through DLL hijacking.

This concludes the reverse-engineering part.


Tools Used

  • Ghidra 11.0+ - Disassembly, decompilation, version tracking
  • Meld - Visual diff comparison
  • Uni-Extract 2 - Installer unpacking
  • PowerShell - Filesystem comparison, permission checking
  • Process Monitor - Dynamic analysis (optional)

References

  • CVE-2025-23297 - NVIDIA Security Bulletin
  • Microsoft Docs - CreateDirectoryW function
  • Windows Security - ACLs and Security Descriptors
  • MITRE ATT&CK - T1574.001 (DLL Search Order Hijacking)

BYE from

TFLL37

P.S For any mistakes, improvements or suggestions, write me using the contact form on this blog

Popular