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)
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:
- Whole directory comparison - comparing ALL files across both installers
- 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:
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, // [rcx] or stack
LPCWSTR lpPathName// [rdx] or stack
LPSECURITY_ATTRIBUTES lpSecurityAttributes );
Parameters:
lpPathName
- The path of the directory to be createdlpSecurityAttributes
- Pointer to aSECURITY_ATTRIBUTES
structure that specifies a security descriptor for the new directory. IflpSecurityAttributes
isNULL
, 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):
- The directory where the application loaded from ← Attacker wins here!
- The system directory (C:)
- The Windows directory (C:)
- Current directory
- 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.
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.
Both of these functions use this Windows function. Comparing the pointers:
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:
Patched version:
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:
Patched:
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*pcVar2;
code int iVar3;
***ppppWVar4;
LPCWSTR // ... variable declarations ...
// Get known folder path
= SHGetKnownFolderPath(&DAT_14008afa0, 0, 0);
iVar3 if (iVar3 != 0) {
(local_70);
CoTaskMemFree("SHGetKnownFolderPath Failed\n");
OutputDebugStringAreturn;
}
// Build path: \NVIDIA Corporation\FrameView
((longlong *)&local_60, local_70, uVar7);
FUN_14000a270(&local_60, (undefined8 *)L"\\NVIDIA Corporation", 0x13);
FUN_14000a390(&local_60, (undefined8 *)L"\\FrameView", 10);
FUN_14000a390
if (param_3 != '\0') {
(&local_60, (undefined8 *)&DAT_140097ef0, 3);
FUN_14000a390}
= &local_60;
ppppWVar4 if (7 < local_48) {
= (LPCWSTR ***)local_60;
ppppWVar4 }
// [!!!!] VULNERABLE: NULL security attributes!
((LPCWSTR)ppppWVar4, (LPSECURITY_ATTRIBUTES)0x0);
CreateDirectoryW// ^^^^^^^^^^^^^^^^^^^^^^^^^^^
// 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 ...
; // <<<<<<<<<<< New: Security attributes structure!
_SECURITY_ATTRIBUTES local_168;
uint uStack_14c;
PSID local_288;
PSID pvStack_280;
PSID local_278// ... more security-related variables ...
// Initialize security structures
((undefined1 (*) [32])&local_288, 0, 0x120);
FUN_14008aad0= (PSID)0x0;
local_288 = (PSID)0x0;
pvStack_280 = (PSID)0x0;
local_278 // ... initialize more security fields ...
// Build proper security attributes with ACL
= FUN_140006cf0(&local_288, (undefined8 *)&local_168);
uVar4 // ^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
// SIDs Populates security attributes
if (uVar4 == 0) { // Success
// Build directory path...
= &local_2f8;
ppppppppWVar11 if (7 < local_2e0) {
= (LPCWSTR *******)local_2f8;
ppppppppWVar11 }
// SECURE: Proper security attributes passed!
((LPCWSTR)ppppppppWVar11, &local_168);
CreateDirectoryW// ^^^^^^^^^^^
// Restricts access
// ... rest of function ...
}
// Cleanup security resources
if (local_288 != (PSID)0x0) {
(local_288);
FreeSid}
if (pvStack_280 != (PSID)0x0) {
(pvStack_280);
FreeSid}
// ... 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:
Declares a SECURITY_ATTRIBUTES structure:
; _SECURITY_ATTRIBUTES local_168
Initializes it with proper security settings:
(&local_288, (undefined8 *)&local_168); FUN_140006cf0
This function sets up proper ACLs (Access Control Lists) to restrict who can access the directory - only Administrators and SYSTEM, NOT regular users.
Passes the structure to CreateDirectoryW:
(path, &local_168); // Instead of NULL CreateDirectoryW
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.
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:
- Attacker monitors for the directory creation (takes like 50ms to detect)
- Immediately plants a malicious DLL (like
version.dll
) - Waits for the NVIDIA service to start
- Service loads the malicious DLL from the application directory (Windows searches there FIRST)
- 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 functionFUN_1400203e0
- Patched: Address
0x1400256ee
inside functionFUN_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
(&local_80, param_1);
FUN_14000ac10= L"\\FvContainer\\FvContainer.exe";
pwVar9 if (param_3 != 0) {
= L"\\FvContainer\\FvContainer.System.exe";
pwVar9 }
// Get special folder path and build: \NVIDIA Corporation\FrameViewSDK
(local_60);
FUN_14001e550((longlong *)&local_100, ppppuVar7, local_50);
FUN_14000a270(&local_100, (undefined8 *)L"\\NVIDIA Corporation", 0x13);
FUN_14000a390(&local_100, (undefined8 *)L"\\FrameViewSDK", 0xd);
FUN_14000a390
= &local_100;
ppppWVar6 if (7 < local_e8) {
= (LPCWSTR ***)local_100;
ppppWVar6 }
// [!!!!!!!] VULNERABILITY #1: FrameViewSDK directory with NULL security!
((LPCWSTR)ppppWVar6, (LPSECURITY_ATTRIBUTES)0x0);
CreateDirectoryW
// Now build MessageBus subdirectory path
// This part is interesting - they're manually writing bytes to build the string
((wchar_t *)((longlong)ppppWVar5 + uVar12 * 2), L"\\Message", 8);
builtin_wcsncpy*(undefined4 *)((longlong)ppppWVar5 + (uVar12 + 8) * 2) = 0x750062; // "bu" in little-endian
*(undefined2 *)((longlong)ppppWVar5 + (uVar12 + 10) * 2) = 0x73; // "s"
// This creates: \MessageBus
= &local_a0;
ppppWVar6 if (7 < uStack_88) {
= (LPCWSTR ***)local_a0;
ppppWVar6 }
// [!!!!!!!] VULNERABILITY #2: MessageBus subdirectory with NULL security!
((LPCWSTR)ppppWVar6, (LPSECURITY_ATTRIBUTES)0x0);
CreateDirectoryW
// ... 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
.
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:
C:\ProgramData\NVIDIA Corporation\FrameViewSDK
- vulnerableC:\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:
Patched version (FUN_1400253b0):
void FUN_1400253b0(undefined8 *param_1, DWORD *param_2, byte param_3, undefined8 param_4)
{
; // Security attributes declared
_SECURITY_ATTRIBUTES local_390;
PSID local_378;
PSID pvStack_370// ... more security variables ...
// Initialize security structures
((undefined1 (*) [32])&local_378, 0, 0x120);
FUN_14008aad0= (PSID)0x0;
local_378 = (PSID)0x0;
pvStack_370 // ... more initialization ...
// Build security attributes with proper ACL
= FUN_140006cf0(&local_378, (undefined8 *)&local_390);
uVar4
if (uVar4 == 0) { // Success
// Build FrameViewSDK directory path
= &local_450;
ppppWVar7 if (7 < local_438) {
= (LPCWSTR ***)local_450;
ppppWVar7 }
// SECURE: First CreateDirectoryW with proper security
((LPCWSTR)ppppWVar7, &local_390);
CreateDirectoryW
// Build MessageBus subdirectory path
// ... path construction code ...
= &local_3f0;
ppppWVar7 if (7 < uStack_3d8) {
= (LPCWSTR ***)local_480;
ppppWVar7 }
// SECURE: Second CreateDirectoryW with SAME security attributes
((LPCWSTR)ppppWVar7, &local_390);
CreateDirectoryW}
// 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:
(&local_80, (undefined8 *)L"\\FvContainer\\FvContainer.exe", ...);
FUN_14000a390// or
(&local_80, (undefined8 *)L"\\FvContainer\\FvContainer.System.exe", ...); FUN_14000a390
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:
(&local_c0, (undefined8 *)L" -d \"", 5);
FUN_14000a390(&local_c0, local_110); // Adds plugin directory path
FUN_14000ac10(&local_c0, (undefined8 *)L"\\FvContainer\\plugins\" ", ...); FUN_14000a390
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) {
= L"\\FvContainer\\FvContainer.System.exe"; // System service
pwVar11 } else {
= L"\\FvContainer\\FvContainer.exe"; // Normal mode
pwVar11 }
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!
Version Tracking confirmation:
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)
- Open
nvfvsdksvc_x64.exe
in Ghidra - Search for
LoadLibrary()
/LoadLibraryEx()
calls - Find strings referencing DLL names
- 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 checkingdwmapi.dll
- desktop window managerprofapi.dll
- user profilescryptbase.dll
- cryptography- Application-specific:
nvfvconfig.dll
,nvfvhelper.dll
Comparison: Vulnerable vs Patched
Function-Level Changes
Function | Vulnerable | Patched | Changes |
---|---|---|---|
FUN_140006b10 → FUN_140008130 |
NULL security | Proper ACL | Added security structures, ACL init |
FUN_1400203e0 → FUN_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:
- Static analysis - Using Ghidra to find all DLL references
- String search - Locating
.dll
strings in the binary - Import table analysis - Distinguishing static vs dynamic loads
- 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.
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) {
= L"drvstore.dll";
lpLibFileName }
}
else {
= GetSystemDirectoryW((LPWSTR)0x0,0);
UVar3 = (LPWSTR)LocalAlloc(0x40,(ulonglong)(uVar3 + 1) * 2 + 2);
pWVar6 if (pWVar6 != (LPWSTR)0x0) {
= GetSystemDirectoryW(pWVar6,(ulonglong)(UVar3 + 1) * 2 + 2);
UVar3 if ((pWVar6[UVar3 - 1] != L'\\') {
[UVar3] = L'\\';
pWVar6}
}
= L"drvstore.dll";
pwVar10 = pWVar6;
pWVar11 do {
= (int)pwVar11;
iVar2 = iVar2 + 1;
uVar1 = pwVar11 + 1;
pWVar11 } while (uVar1 < 0xc);
= LoadLibraryExW(pwVar6,(HANDLE)0x0,local_158);
DAT_1400b5d60 }
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) {
= L"ldp.dll";
lpLibFileName }
}
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) {
= L"cryptnet.dll";
pwVar15 }
}
else {
// Path construction code...
= LoadLibraryExW(pwVar6,(HANDLE)0x0,local_158);
DAT_1400b5dd8 }
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) {
= L"cryptbase.dll";
pwVar13 }
}
else {
= GetSystemDirectoryW((LPWSTR)0x0,0);
UVar3 = (LPWSTR)LocalAlloc(0x40,(ulonglong)(uVar3 + 1) * 2 + 2);
pWVar6 // ... path construction ...
= L"cryptbase.dll";
pwVar13 // ... more code ...
= LoadLibraryExW(pwVar6,(HANDLE)0x0,local_158);
DAT_1400b5db0 }
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 informationGetFileVersionInfoSizeW
- Gets size of version infoGetFileVersionInfoA
- 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:
version.dll - PRIMARY TARGET
- Functions:
VerQueryValueW
,GetFileVersionInfoSizeW
- Usage: Version checking functionality
- Load method: Dynamic (likely LoadLibrary with no path)
- Windows searches application directory FIRST
- Functions:
cryptbase.dll
- Usage: Cryptography base functions
- Load method:
LoadLibraryExW
with path construction - Multiple references (4 XREFs)
- Loaded conditionally
cryptnet.dll
- Usage: Cryptography network functions
- Load method:
LoadLibraryExW
- Similar pattern to cryptbase.dll
drvstore.dll
- Usage: Driver store operations
- Load method:
LoadLibraryExW
- Path construction from System directory
wldp.dll
- Usage: Windows Lockdown Policy
- Load method: Dynamic
- Loaded conditionally
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:
- Ubiquitous Usage: Many Windows applications check file versions
- Application Directory Search: Windows DLL search order checks app directory FIRST
- Not Critical for Loading: If it fails, the app usually continues (error handling present)
- Confirmed Usage: Import table clearly shows
VerQueryValueW
,GetFileVersionInfoSizeW
,GetFileVersionInfoA
- 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
= LoadLibraryExW(L"version.dll", NULL, 0);
hDll
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
= GetProcAddress(hDll, "VerQueryValueW"); FARPROC pFunc
Attack window:
- Installer creates
C:\ProgramData\NVIDIA Corporation\FrameViewSDK\
with weak permissions - Attacker plants
version.dll
in that directory - Service starts and tries to load
version.dll
- Windows finds attacker’s DLL FIRST (application directory)
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:
- Systemic Issue: The same insecure pattern appeared in 3 separate locations, suggesting lack of secure coding guidelines or security review
- Wide Attack Surface: Multiple directories, multiple processes, multiple exploitation paths
- Easy Exploitation: No race condition, wide time window, low technical complexity
- Multiple DLL Targets: Identified 6+ hijackable DLLs with version.dll as primary target
- 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