Investigating OCCT Enterprise Environment Detection (Part 1)
Background
OCCT is an incredibly powerful benchmarking and monitoring tool. Most importantly, it is cross-platform and can run on Linux. I recently built an X99-based PC in a small-form-factor case, and stability testing was an essential step to ensure my build was actually practical (high TDP = toasty). I really love the detailed yet elegant OCCT interface, so it was immediately my tool of choice.
Upon launching the native x64 Linux binary, I was met with the startup window and then...
OCCT requires enterprise users to purchase an enterprise license. An enterprise user is determined, as the message suggests, by the presence of enterprise hardware (like the Xeon E5-2697 V4 I am running) or software (like an Active Directory domain). Unfortunately for me, I am far from an enterprise user.
That sucks. Let's try to break it!
Exploring OCCT
Let's run binwalk on the binary and see if there's anything interesting going on:
DECIMAL HEXADECIMAL DESCRIPTION
0 0x0 ELF binary, 64-bit shared object, AMD X86-64 for Linux, little endian
268320 0x41820 CRC32 polynomial table, little endian
269361 0x41C31 Copyright text: "Copyright 1995-2026 Jean-loup Gailly and Mark Adler "
272561 0x428B1 Copyright text: "Copyright 1995-2026 Mark Adler "
1364328 0x14D168 ZIP archive, file count: 2, total size: 11744233 bytes
13108561 0xC80551 ZIP archive, file count: 2, total size: 11645536 bytes
24754097 0x179B7B1 ZIP archive, file count: 2, total size: 257920 bytes
25012017 0x17DA731 ZIP archive, file count: 1, total size: 130604 bytes
25142621 0x17FA55D ZIP archive, file count: 1, total size: 337519 bytes
25480140 0x184CBCC ZIP archive, file count: 2, total size: 7247022 bytes
32727162 0x1F3607A ZIP archive, file count: 1, total size: 1968740 bytes
34695902 0x2116ADE ZIP archive, file count: 1, total size: 2585522 bytes
37281424 0x238DE90 ZIP archive, file count: 1, total size: 1182778 bytes
38464202 0x24AEACA ZIP archive, file count: 15, total size: 63014983 bytes
101479185 0x60C7311 ZIP archive, file count: 7, total size: 46232089 bytes
147711274 0x8CDE52A ZIP archive, file count: 1, total size: 1960282 bytes
149671556 0x8EBCE84 ZIP archive, file count: 3, total size: 57575250 bytes
Cool. Just a native binary packed with some ZIP resources. The UI seems to be suspiciously consistent across platforms, however. Let's disassemble in Ghidra to find out more.
After analysis, we are greeted with a massive main function with nearly 100 parameters.
bVar1 = IsAnotherLauncherRunning();
if ((bVar1) || (bVar1 = IsOCCTCoreRunning(), bVar1)) {
bVar1 = true;
}
else {
bVar1 = false;
}
if (bVar1) {
printf("Only one instance of OCCT can run at a time");
iVar3 = 0;
}
Wait... IsAnotherLauncherRunning()? Is this binary merely a launcher for another?
std::filesystem::temp_directory_path[abi:cxx11]();
std::allocator<char>::allocator();
std::__cxx11::basic_string<>::basic_string<>(&dirName,"OCCT",&local_589);
std::allocator<char>::~allocator(&local_589);
std::filesystem::__cxx11::path::append<>(&outputPath,&dirName);
We seem to be crafting a string that represents directory to be made in /tmp, specifically /tmp/OCCT.
Later, a ResourceManager object is called to ReadMap given parameters provided to main such as the builtEdition. This is presumably what unpacks those ZIP archives in the binary.
We branch based on the value of builtExecutable, an instance of an ExecutableType that apparently dictates whether a given binary is the GUI, CMD, or "Diag" version. To my knowledge, we're launching the GUI version, so let's follow that.
Now, we seem to branch based on whether we are running the Enterprise binary. Both branches similarly seem to unpack and dump resources into /tmp/OCCT but differ in how they... launch another executable? The Enterprise branch calls
LaunchAttachedGUI(outputPath_00,resources_00,(vector<> *)&local_378,selfPath_00,
(Edition)&local_3b8,SUB81(tempElements,0),args_00,awaitedProcesses_00,
(ulong *)&local_3e8);
while other editions call
LaunchDetachedGUI(flagPathStr,outputPath_02,resources_02,(vector<> *)&local_268,
selfPath_02,(Edition)&local_298,SUB81(&local_2d8,0),args_02,
(vector<>)stack0xfffffffffffff960,(ulong *)tempElements);
LaunchedDetachedGUI extracts some more resources to the tmp directory and calls LaunchExecutable which... launches an executable, presumably located in /tmp/OCCT. Let's take a look at this directory:
/tmp/OCCT
├── Resources
│ ├── CPULINPACK
│ │ └── 2025
│ │ └── linpack_xeon64
│ ├── CPUOCCT
│ │ ├── cpuocct
│ │ └── libhwloc.so.15
│ ├── GPUMEMTEST
│ │ └── gpumemtest
│ ├── GPUUNREAL
│ │ ├── Engine
│ │ │ ├── Binaries
│ │ │ │ └── ThirdParty
│ │ │ │ └── MsQuic
│ │ │ │ └── v220
│ │ │ │ └── linux
│ │ │ │ ├── libmsquic.so
│ │ │ │ └── libmsquic.so.2
│ │ │ └── Content
│ │ │ └── Renderer
│ │ │ └── TessellationTable.bin
│ │ ├── gpu3d
│ │ │ ├── Binaries
│ │ │ │ └── Linux
│ │ │ │ └── gpu3d-Linux-Shipping
│ │ │ └── Content
│ │ │ └── Paks
│ │ │ ├── global.ucas
│ │ │ ├── global.utoc
│ │ │ ├── gpu3d-Linux.pak
│ │ │ ├── gpu3d-Linux.ucas
│ │ │ └── gpu3d-Linux.utoc
│ │ └── gpu3d.sh
│ ├── libsteam_api.so
│ ├── MEMTEST
│ │ └── memtest
│ ├── OCCTGUI
│ ├── OCCTMonitoring
│ │ ├── libhwloc.so.15
│ │ └── OcctMonitoringLib.so
│ ├── OCCTMonitoringNoNVML
│ │ ├── libhwloc.so.15
│ │ └── OcctMonitoringLib-no-nvml.so
│ ├── ORING
│ │ ├── ORingDrv.so
│ │ └── ORingLib.so
│ ├── SOUND
│ │ ├── Glados-ErrorDetected.wav
│ │ └── glados-scheduleComplete.wav
│ ├── steam_appid.txt
│ └── STORAGE
│ └── diskspd
└── State
Great. A few native dependencies and a binary named OCCTGUI. This is where all the magic happens! I don't feel like tearing the launcher binary apart to find out how exactly it is launched (the many gnarly arguments passed to LaunchedDetatchedGUI and LaunchExecutable), so let's use top to find its PID. We're lucky that the GUI process doesn't get killed upon failing the enterprise check!
After grabbing that, let's ask the kernel what command-line arguments were passed to it. We can do this by reading /proc/<pid>/cmdline:
/tmp/OCCT/Resources/OCCTGUI --temp-path=/tmp/OCCT --launcher-exe-path=/home/ethanjoseph/Downloads/OCCT
Perfect! We no longer have to bother with the launcher; now we can just call the GUI directly, given the tmp directory is correctly populated.
Let's turn our attention to the OCCTGUI binary now.
Exploring OCCTGUI
Running binwalk again:
DECIMAL HEXADECIMAL DESCRIPTION
0 0x0 ELF binary, 64-bit shared object, AMD X86-64 for System-V (Unix), little endian
1264544 0x134BA0 CRC32 polynomial table, little endian
1265599 0x134FBF Copyright text: "Copyright 1995-2024 Jean-loup Gailly and Mark Adler "
1265920 0x135100 CRC32 polynomial table, little endian
1277519 0x137E4F Copyright text: "Copyright 1995-2024 Mark Adler "
1281648 0x138E70 CRC32 polynomial table, little endian
1285360 0x139CF0 CRC32 polynomial table, little endian
11915264 0xB5D000 Windows PE binary, machine type: Intel x86-64
14720484 0xE09DE4 PNG image, total size: 3218 bytes
14723724 0xE0AA8C PNG image, total size: 3218 bytes
14922032 0xE3B130 PNG image, total size: 3218 bytes
15479420 0xEC327C Copyright text: "CopyrightAttribute"
16558119 0xFCA827 Copyright text: "CopyrightAttribute"
16896000 0x101D000 Windows PE binary, machine type: Intel x86
16899023 0x101DBCF Copyright text: "CopyrightAttribute"
16916480 0x1022000 Windows PE binary, machine type: Intel x86
16918111 0x102265F Copyright text: "CopyrightAttribute"
16949345 0x102A061 Copyright text: "CopyrightAttribute"
16969728 0x102F000 Windows PE binary, machine type: Intel x86
16971158 0x102F596 Copyright text: "CopyrightAttribute"
16986112 0x1033000 Windows PE binary, machine type: Intel x86
...
Wow! A ton of detected signatures; this is what we're looking for. PE binaries compose much of this file, yet it's a native executable. Is this a .NET app? Let's open up Ghidra again.
main doesn't seem to do anything interesting, other than calling another function and passing argc and argv along. Let's analyze that function and call it real_main.
FUN_00b20fa0("Invoking fx resolver [%s] hostfxr_main_startupinfo",local_e0);
FUN_00b20fa0("Host path: [%s]",local_58);
FUN_00b20fa0("Dotnet path: [%s]",local_100);
FUN_00b20fa0("App path: [%s]",local_78);
hostfxr? Yep, this binary is just a custom .NET host. It doesn't appear to do anything other than launch the .NET app, so all licensing/enterprise checks must occur within the packed PE binaries.
Let's begin carving out the PE binaries with binwalk:
binwalk --include=pe --carve OCCTGUI
We are left with 171 independent binaries.
Now, let's open the first PE binary at offset 11915264 in dnSpy:
We've successfully extracted the main .NET assembly. We can now clearly see that OCCT is built with the Avalonia GUI framework, and we can even spot the ProfessionalEnvironmentDetected window implementation. Yay!
We're not done yet, though. We carved out many other .NET assemblies that implement the app's core functionality. dnSpy's current decompilation will be riddled with broken references because we have not yet loaded the other assemblies. That's a job for next time.
See you in Part 2!