← Back

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 Professional Environment Detected Window

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:

OCCT .NET Assembly Open 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!