my heaven on earth

blog about pentesting / reverse engineering
106 visitors

Reversing A Custom Driver For The Valve Index (Specifically)

Symbol Tree Of Exports
Symbol Tree Of Exports
Hello, this post is purely made out of boredom. Im typing this out while getting my tools ready to analyze this small DLL.
This post describes quick reversing of a DLL in order to uncover its true identity ( If its malware or actual help in the community )

Tools used:
Strings (https://learn.microsoft.com/en-us/sysinternals/downloads/strings),
Ghidra (https://github.com/NationalSecurityAgency/ghidra) and x64dbg (https://x64dbg.com/)

First I started to check the functions using a self made C++ tool; what did we find?

IMPORTED FUNCTIONS
AcquireSRWLockExclusive (Address: 0x180012158)
CloseHandle (Address: 0x180012000)
CreateFileW (Address: 0x1800120f0)
CreateThread (Address: 0x180012040)
CreateToolhelp32Snapshot (Address: 0x1800120d0)
DeleteCriticalSection (Address: 0x1800121f8)
EncodePointer (Address: 0x180012238)
EnterCriticalSection (Address: 0x1800121e8)
ExitProcess (Address: 0x180012250)
FindClose (Address: 0x180012268)
FindFirstFileExW (Address: 0x180012270)
FindNextFileW (Address: 0x180012278)
FlsAlloc (Address: 0x1800122d0)
FlsFree (Address: 0x1800122e8)
FlsGetValue (Address: 0x1800122d8)
FlsSetValue (Address: 0x1800122e0)
FlushFileBuffers (Address: 0x180012118)
FlushInstructionCache (Address: 0x180012078)
FreeEnvironmentStringsW (Address: 0x1800122c8)
FreeLibrary (Address: 0x180012228)
GetACP (Address: 0x180012288)
GetCommandLineA (Address: 0x1800122a0)
GetCommandLineW (Address: 0x1800122a8)
GetConsoleMode (Address: 0x180012100)
GetConsoleOutputCP (Address: 0x180012108)
GetCPInfo (Address: 0x180012298)
GetCurrentProcess (Address: 0x180012030)
GetCurrentProcessId (Address: 0x180012038)
GetCurrentThreadId (Address: 0x180012048)
GetEnvironmentStringsW (Address: 0x1800122c0)
GetFileType (Address: 0x180012138)
GetLastError (Address: 0x1800121d8)
GetModuleFileNameW (Address: 0x1800120a8)
GetModuleHandleA (Address: 0x1800120b0)
GetModuleHandleExW (Address: 0x180012260)
GetModuleHandleW (Address: 0x1800120b8)
GetOEMCP (Address: 0x180012290)
GetProcAddress (Address: 0x1800120c0)
GetProcessHeap (Address: 0x180012148)
GetStartupInfoW (Address: 0x1800121b8)
GetStdHandle (Address: 0x180012140)
GetStringTypeW (Address: 0x180012130)
GetSystemInfo (Address: 0x180012080)
GetSystemTimeAsFileTime (Address: 0x180012178)
GetThreadContext (Address: 0x180012068)
HeapAlloc (Address: 0x180012010)
HeapCreate (Address: 0x180012008)
HeapFree (Address: 0x180012020)
HeapReAlloc (Address: 0x180012018)
HeapSize (Address: 0x180012128)
InitializeCriticalSectionAndSpinCount (Address: 0x180012200)
InitializeSListHead (Address: 0x180012180)
InterlockedFlushSList (Address: 0x1800121d0)
IsDebuggerPresent (Address: 0x1800121a0)
IsProcessorFeaturePresent (Address: 0x1800121c0)
IsValidCodePage (Address: 0x180012280)
LCMapStringW (Address: 0x1800122f0)
LeaveCriticalSection (Address: 0x1800121f0)
LoadLibraryExW (Address: 0x180012230)
LoadLibraryW (Address: 0x1800120c8)
MultiByteToWideChar (Address: 0x1800122b0)
OpenThread (Address: 0x180012050)
QueryPerformanceCounter (Address: 0x180012170)
RaiseException (Address: 0x180012240)
ReleaseSRWLockExclusive (Address: 0x180012150)
ResumeThread (Address: 0x180012060)
RtlCaptureContext (Address: 0x180012188)
RtlLookupFunctionEntry (Address: 0x180012190)
RtlPcToFileHeader (Address: 0x180012248)
RtlUnwindEx (Address: 0x1800121c8)
RtlVirtualUnwind (Address: 0x180012198)
SetFilePointerEx (Address: 0x1800120f8)
SetLastError (Address: 0x1800121e0)
SetStdHandle (Address: 0x180012120)
SetThreadContext (Address: 0x180012070)
SetUnhandledExceptionFilter (Address: 0x1800121b0)
Sleep (Address: 0x180012028)
SleepConditionVariableSRW (Address: 0x180012168)
SuspendThread (Address: 0x180012058)
TerminateProcess (Address: 0x180012258)
Thread32First (Address: 0x1800120d8)
Thread32Next (Address: 0x1800120e0)
TlsAlloc (Address: 0x180012208)
TlsFree (Address: 0x180012220)
TlsGetValue (Address: 0x180012210)
TlsSetValue (Address: 0x180012218)
UnhandledExceptionFilter (Address: 0x1800121a8)
VirtualAlloc (Address: 0x180012088)
VirtualFree (Address: 0x180012098)
VirtualProtect (Address: 0x180012090)
VirtualQuery (Address: 0x1800120a0)
WakeAllConditionVariable (Address: 0x180012160)
WideCharToMultiByte (Address: 0x1800122b8)
WriteConsoleW (Address: 0x1800120e8)
WriteFile (Address: 0x180012110)

SHELL32.dll
Shell_NotifyIconA (Address: 0x180012300)

USER32.dll
CreateIconFromResourceEx (Address: 0x180012340)
CreatePopupMenu (Address: 0x180012388)
CreateWindowExA (Address: 0x180012348)
DefWindowProcA (Address: 0x180012358)
DestroyIcon (Address: 0x180012310)
DispatchMessageA (Address: 0x180012368)
GetCursorPos (Address: 0x180012318)
GetMessageA (Address: 0x180012378)
InsertMenuA (Address: 0x180012338)
MessageBoxA (Address: 0x180012320)
RegisterClassA (Address: 0x180012350)
RegisterWindowMessageA (Address: 0x180012380)
SendMessageA (Address: 0x180012360)
SetForegroundWindow (Address: 0x180012328)
TrackPopupMenu (Address: 0x180012330)
TranslateMessage (Address: 0x180012370)

EXPORTED FUNCTIONS:
HmdDriverFactory (Ordinal: 1, Address: 0x2b30)

Seems pretty safe so far, the most notable combination is:
CreateToolhelp32Snapshot + Thread32First + Thread32Next these are used to enumerate all running processes/threads on the system
SuspendThread + ResumeThread + GetThreadContext + SetThreadContext these allow pausing, inspecting, and modifying other threads.
OpenThread opens handles to threads in other processes.

The key clues are the exported function:

HmdDriverFactory: This is a SteamVR/OpenVR driver entry point. This is the standard export that Valve requires for all VR headset drivers. So we already know the DLL is actually doing its work (perchance).

Verdict: Almost certainly legitimate. This looks like a legitimate SteamVR hardware driver for the Valve Index to emulate Oculus Rift physics. The threads enumeration/manipulation APIs make sense. In that context VR drivers need tight control over rendering threads for performance. The Shell_NotifyIconA and tray/popup menu imports also suggest that it creates a system tray icon, common in VR runtime software, to get a sharp overview.

Now let's get to the fun! We launch a new project on Ghidra, and let it get analyzed.
Done.
We're going to be looking for calls, let's decompile the code first so its much more readable:

undefined ** HmdDriverFactory(char *param_1)

{
undefined *puVar1;
size_t sVar2;
size_t sVar3;
undefined **ppuVar4;
ulonglong uVar5;
bool bVar6;

/ 0x2b30 1 HmdDriverFactory /
uVar5 = 0;
sVar2 = uVar5;
if (param_1 != (char *)0x0) {
sVar2 = strlen(param_1);
}
puVar1 = PTR_s_IServerTrackedDeviceProvider_004_18001d238;
sVar3 = strlen(PTR_s_IServerTrackedDeviceProvider_004_18001d238);
if (sVar2 == sVar3) {
if (sVar2 != 0) {
bVar6 = sVar2 == 0;
do {
if (sVar2 <= uVar5 && !bVar6) break;
if (param_1[uVar5] != (param_1 + uVar5)[(longlong)puVar1 - (longlong)param_1])
goto LAB_180002b99;
uVar5 = uVar5 + 1;
bVar6 = uVar5 == sVar2;
} while (!bVar6);
}
ppuVar4 = &PTR_PTR_18001d2e8;
}
else {
LAB_180002b99:
ppuVar4 = (undefined **)0x0;
}
return ppuVar4;
}

What does this do?
  • SteamVR calls this function with a string like "IServerTrackedDeviceProvider_004"
  • The DLL checks if the string matches what it supports
  • If yes > return a pointer to the driver interface
  • If no > return null

This is exactly the standard SteamVR driver factory pattern. Every single official SteamVR driver works this way. Valve literally specifies this in their driver SDK.
However this is just the entry point. The real behavior is in the driver implementation itself.

Next step analyzing PTR_PTR_18001d2e8 since that pointer leads to an actual driver object and its methods. That's where the real implementation lives including the hooking code we saw earlier with MinHook. Specifically we want to find where MH_CreateHook gets called and see what it's hooking that's the most important thing to verify.


/ WARNING: Globals starting with '_' overlap smaller symbols at the same address /

undefined8 FUN_180001270(undefined8 param_1,undefined8 *param_2)

{
undefined8 *puVar1;
int iVar2;
undefined8 uVar3;
char *lpText;
ulonglong uVar4;
bool bVar5;
undefined1 local_res10 [8];

DAT_1800202d8 = param_2;
uVar3 = FUN_1800013f0((undefined8 *)&DAT_1800202e0);
if ((int)uVar3 != 0) {
return uVar3;
}
uVar4 = 0;
LOCK();
bVar5 = DAT_18001dda0 == 0;
if (bVar5) {
DAT_18001dda0 = 1;
}
UNLOCK();
while (!bVar5) {
Sleep((uint)(0x1f < uVar4));
uVar4 = uVar4 + 1;
LOCK();
bVar5 = DAT_18001dda0 == 0;
if (bVar5) {
DAT_18001dda0 = 1;
}
UNLOCK();
}
if (DAT_18001dda8 != (HANDLE)0x0) {
LOCK();
UNLOCK();
LAB_1800013b3:
DAT_18001dda0 = 0;
MessageBoxA((HWND)0x0,"MH_Initialize failed","PseudoRift Error",0);
return 0;
}
DAT_18001dda8 = HeapCreate(0,0,0);
LOCK();
DAT_18001dda0 = 0;
UNLOCK();
if (DAT_18001dda8 == (HANDLE)0x0) goto LAB_1800013b3;
if (DAT_1800202f8 == (longlong *)0x0) {
_DAT_1800202e0 =
(longlong )((code )DAT_1800202d8)(DAT_1800202d8,"IVRProperties_001",local_res10);
_DAT_1800202e8 = _DAT_1800202e0;
DAT_1800202f8 = _DAT_1800202e0;
}
puVar1 = *(undefined8 **)(*DAT_1800202f8 + 8);
uVar3 = FUN_180002c30(puVar1,FUN_180001a70,&DAT_18001ddc8);
if ((int)uVar3 == 0) {
iVar2 = FUN_180002480((longlong)puVar1,1);
if (iVar2 == 0) goto LAB_18000138e;
lpText = "MH_EnableHook failed";
}
else {
lpText = "MH_CreateHook failed";
}
MessageBoxA((HWND)0x0,lpText,"PseudoRift Error",0);
LAB_18000138e:
CreateThread((LPSECURITY_ATTRIBUTES)0x0,0,FUN_180001840,(LPVOID)0x0,0,(LPDWORD)0x0);
return 0;
}

This is really interesting and mostly reassuring.
Let me break down what this function does:

IT initializes a heap; HeapCreate for MinHook's memory, totally normal.
IT gets IVRProperties_001; a standard SteamVR interface, to be expected.
IT creates a hook; FUN_180002c30; with FUN_180001a70 — this is the MinHook setup.
IT spawns a thread; FUN_180001840; this runs something in the background.

The two things we need to investigate:
FUN_180001a70; this is what's being hooked, critical to check.
FUN_180001840; this is what the background thread does.

Now let's check them one at a time.


ulonglong FUN_180001a70(undefined8 param_1,longlong param_2,uint param_3,uint param_4)

{
uint *puVar1;
undefined8 *puVar2;
uint *puVar3;
uint uVar4;
bool bVar5;
uint *puVar6;
size_t sVar7;
longlong lVar8;
char cVar9;
ulonglong uVar10;
uint *puVar11;
ulonglong uVar12;
ulonglong unaff_RBX;
undefined1 *puVar13;
char *_Str;
uint uVar14;
longlong *plVar15;
undefined *puVar16;
undefined1 *puVar17;
char *pcVar18;
int iVar19;
longlong lVar20;
uint local_res8 [2];
uint *local_res18;
uint local_res20;
undefined4 local_80 [2];
uint **local_78;
undefined4 local_70;
int local_6c;
int local_64;
undefined4 local_60;
undefined4 local_5c;
uint *local_58;
undefined8 local_50;
undefined4 local_48;
int local_44;

bVar5 = false;
cVar9 = '\0';
local_res8[0] = local_res8[0] & 0xffffff00;
puVar3 = param_3 + (ulonglong)param_4 * 10;
local_res18 = param_3;
local_res20 = param_4;
if (param_3 != puVar3) {
puVar11 = param_3 + 6;
lVar20 = DAT_180020330;
bVar5 = false;
do {
puVar16 = PTR_s_Oculus_18001d2b8;
if (puVar11[-5] == 0) {
if (puVar11[-6] == 0x3ed) {
sVar7 = strlen(PTR_s_Oculus_18001d2b8);
FUN_1800016e0((longlong)param_1,param_2,0x3ed,*(undefined1 **)(puVar11 + -2),
(ulonglong)*puVar11,puVar16,sVar7 + 1);
bVar5 = true;
lVar20 = DAT_180020330;
}
if (puVar11[-6] == 0x3e9) {
uVar12 = (ulonglong)*puVar11;
puVar13 = *(undefined1 **)(puVar11 + -2);
for (plVar15 = &DAT_18001ddd0; &DAT_18001ddd0 + lVar20 * 0x25 != plVar15;
plVar15 = plVar15 + 0x25) {
if ((((int)plVar15[2] == 0x3e9) && (plVar15[1] == param_2)) &&
((undefined8 )plVar15 == param_1)) {
if (puVar13 != (undefined1 *)0x0) {
uVar10 = 0x80;
if (uVar12 < 0x80) {
uVar10 = uVar12;
}
plVar15[0x23] = uVar10;
puVar17 = (undefined1 *)((longlong)plVar15 + 0x14);
for (; uVar10 != 0; uVar10 = uVar10 - 1) {
puVar17 = puVar13;
puVar13 = puVar13 + 1;
puVar17 = puVar17 + 1;
}
}
goto LAB_180001c03;
}
}
if (lVar20 != 0x20) {
lVar20 = lVar20 + 1;
lVar8 = lVar20 * 0x128;
DAT_180020330 = lVar20;
*(undefined8 **)(&DAT_18001dca8 + lVar8) = param_1;
(longlong )(&DAT_18001dcb0 + lVar8) = param_2;
(undefined4 )(&DAT_18001dcb8 + lVar8) = 0x3e9;
if (puVar13 == (undefined1 *)0x0) {
(&DAT_18001ddc0)[lVar20 * 0x25] = 0;
}
else {
uVar10 = 0x80;
if (uVar12 < 0x80) {
uVar10 = uVar12;
}
(&DAT_18001ddc0)[lVar20 * 0x25] = uVar10;
puVar16 = &DAT_18001dcbc + lVar8;
for (; uVar10 != 0; uVar10 = uVar10 - 1) {
puVar16 = puVar13;
puVar13 = puVar13 + 1;
puVar16 = puVar16 + 1;
}
}
(&DAT_18001ddc8)[lVar20 0x25] = (code )0x0;
}
}
LAB_180001c03:
uVar14 = puVar11[-6];
if (((uVar14 == 0x3e9) || (uVar14 == 0x405)) || (uVar14 == 0xbbf)) {
local_res8[0] = CONCAT31(local_res8[0]._1_3_,1);
cVar9 = '\x01';
}
else {
cVar9 = (char)local_res8[0];
}
}
puVar6 = local_res18;
puVar1 = puVar11 + 4;
puVar11 = puVar11 + 10;
} while (puVar1 != puVar3);
if (cVar9 != '\0') {
iVar19 = 0;
uVar14 = 0;
puVar11 = local_res18 + 4;
do {
if ((puVar11[-3] == 0) && (uVar4 = puVar11[-4], uVar4 != 0x3e9)) {
if (uVar4 == 0x405) {
iVar19 = (int )puVar11;
}
else if (uVar4 == 0xbbf) {
uVar14 = (uint )puVar11;
}
}
puVar1 = puVar11 + 6;
puVar11 = puVar11 + 10;
} while (puVar1 != puVar3);
if (iVar19 == 0) {
local_78 = &local_res18;
local_80[0] = 0x405;
local_70 = 4;
((code )*param_1)(param_1,param_2,local_80,1);
lVar20 = DAT_180020330;
if ((local_6c == 2) && (local_64 == 0)) {
iVar19 = (int)local_res18;
}
else {
iVar19 = 0;
}
}
if ((uVar14 == 0) && (iVar19 == 2)) {
local_58 = local_res8;
local_60 = 0xbbf;
local_50 = (undefined *)CONCAT44(local_50._4_4_,4);
((code )*param_1)(param_1,param_2,&local_60,1);
lVar20 = DAT_180020330;
if ((local_50._4_4_ != 2) || (uVar14 = local_res8[0], local_44 != 0)) {
uVar14 = 0;
}
LAB_180001d7a:
_Str = PTR_s_Vive_Controller_MV_HTC_LHR-00000_18001d2d0;
if (uVar14 == 2) {
_Str = PTR_s_Vive_Controller_MV_HTC_LHR-00000_18001d2d8;
}
}
else {
_Str = PTR_s_Oculus_Sensor_18001d2c8;
if ((iVar19 != 4) && (_Str = PTR_s_Oculus_Rift_CV1_18001d2c0, iVar19 == 2))
goto LAB_180001d7a;
}
sVar7 = strlen(_Str);
uVar12 = sVar7 + 1;
for (plVar15 = &DAT_18001ddd0; &DAT_18001ddd0 + lVar20 * 0x25 != plVar15;
plVar15 = plVar15 + 0x25) {
if ((((int)plVar15[2] == 0x3e9) && (plVar15[1] == param_2)) &&
((undefined8 )plVar15 == param_1)) {
if (_Str != (char *)0x0) {
uVar10 = 0x80;
if (uVar12 < 0x80) {
uVar10 = uVar12;
}
plVar15[0x24] = uVar10;
pcVar18 = (char *)((longlong)plVar15 + 0x94);
for (; uVar10 != 0; uVar10 = uVar10 - 1) {
pcVar18 = _Str;
_Str = _Str + 1;
pcVar18 = pcVar18 + 1;
}
}
goto LAB_180001e50;
}
}
if (lVar20 != 0x20) {
lVar20 = lVar20 + 1;
lVar8 = lVar20 * 0x128;
DAT_180020330 = lVar20;
*(undefined8 **)(&DAT_18001dca8 + lVar8) = param_1;
(longlong )(&DAT_18001dcb0 + lVar8) = param_2;
(undefined4 )(&DAT_18001dcb8 + lVar8) = 0x3e9;
(&DAT_18001ddc0)[lVar20 * 0x25] = 0;
if (_Str == (char *)0x0) {
(&DAT_18001ddc8)[lVar20 0x25] = (code )0x0;
}
else {
uVar10 = 0x80;
if (uVar12 < 0x80) {
uVar10 = uVar12;
}
(&DAT_18001ddc8)[lVar20 0x25] = (code )uVar10;
pcVar18 = (char *)((longlong)&std::bad_exception::RTTI_Type_Descriptor + lVar8 + 0xc);
for (; uVar10 != 0; uVar10 = uVar10 - 1) {
pcVar18 = _Str;
_Str = _Str + 1;
pcVar18 = pcVar18 + 1;
}
}
}
LAB_180001e50:
(*DAT_18001ddc8)(param_1,param_2,puVar6,local_res20);
goto LAB_180001ea3;
}
}
(*DAT_18001ddc8)(param_1,param_2,local_res18,local_res20);
if (!bVar5) {
return unaff_RBX;
}
LAB_180001ea3:
if ((DAT_18001d2e0 != '\0') &&
(lVar20 = DAT_180020330 * 0x25, &DAT_18001ddd0 + lVar20 != &DAT_18001ddd0)) {
puVar16 = &DAT_18001de64;
do {
local_60 = (undefined4 )(puVar16 + -0x84);
local_5c = 0;
if (puVar16 != (undefined *)0x0) {
local_48 = (undefined4 )(puVar16 + 0x8c);
local_44 = 5;
local_50 = puVar16;
(DAT_18001ddc8)((undefined8 )(puVar16 + -0x94),(undefined8 *)(puVar16 + -0x8c),&local_60
,1);
}
puVar2 = (undefined8 *)(puVar16 + 0x94);
puVar16 = puVar16 + 0x128;
} while (puVar2 != &DAT_18001ddd0 + lVar20);
}
return unaff_RBX;
}

This function hooks into SteamVR's property system and intercepts device property reads.
When SteamVR asks "What device is this?", the hook intercepts that and substitutes Oculus device names/identifiers instead.
It's basically spoofing:

Property 0x3ed > Injects the string "Oculus" as manufacturer
Property 0x3e9 > Substitutes device model names
Property 0x405 > Checks device type (controller vs headset)
Property 0xbbf > Checks handedness (left/right controller)

Then based on those checks it returns either:

"Oculus Rift CV1"
"Oculus Sensor"
"Vive Controller MV HTC LHR-00000000"
"Vive Controller MV HTC LHR-00000001"

This is exactly what the DLL claims to do.

THE DLL IS SAFE TO USE!
Decompilation Of Call
Decompilation Of Call