← Back to blog
PebGhost kernel rootkit signed-drivers PEB IOCTL WHCP KernelLoader covertChannel ReverseEngineering

PebGhost rootkit loader

Sep 24, 2026 9 min read Malware Analysis

While hunting for abused kernel drivers, I found a malicious kernel loader signed by Microsoft through the Windows Hardware Compatibility Program with the account "OBSS." The first driver was signed on 3/7/2026, and the last one was signed on 16/9/2026. The driver is a kernel loader that loads an unsigned kernel driver received from a user-mode client — a pattern threat actors use more often now: sign one driver, then load an unlimited number of unsigned drivers behind it. This year alone I have found other kernel loaders, each signed with a different signer.

Microsoft WHCP digital signature for the loader

Figure 1: Valid Microsoft Windows Hardware Compatibility Publisher signature.

I am writing this post because it was interesting to me: it is a kernel loader, and its user-to-kernel communication is unusual. The driver still uses an IOCTL dispatch path, but instead of exchanging data through the usual buffered or direct I/O methods, the user-mode client places the payload at *(PEB + 0x2D0) — the AppCompatFlagsUser field — and the kernel side reads it from there. I have seen other odd kernel–user channels before (registry notifications, process notifications, filesystem, and network paths; many of those show up in cheat engines). I will cover those in a separate post.

Driver initialization

Once loaded, the driver creates a device and symbolic link named momoac for user-mode communication, installs the major dispatch routines, and registers an image-load notification callback. It also builds a watch list of processes and modules to monitor. That list is configurable from the user-mode client, and it ships with hardcoded entries for common security and analysis tools (for example IDA).

DriverEntry creating momoac device and registering callbacks

Figure 2: Driver entry — device/symlink setup, dispatch table, watch list, and image-load notify.

Hardcoded analysis-tool process names in the watch list

Additional hardcoded process and module monitor entries

Figure 3: Process and module names the driver monitors (analysis / security tools).

Image-load notification

The image-load notification callback watches modules as they are mapped into processes. When a loaded module matches an entry on the process/module watch list, the driver appends a detection record to a doubly linked list. That list is later returned to the user-mode agent through the IOCTL dispatch path when requested.

typedef struct _DETECTION_ENTRY {   // 0x230 bytes, PagedPool, tag 'moac'
    LIST_ENTRY Links;          // +0x00
    LONGLONG   Timestamp;      // +0x10  KUSER_SHARED_DATA.SystemTime @ 0xFFFFF78000000014
    ULONG      Data1;          // +0x18  always 1
    ULONG      Data2;          // +0x1C
    HANDLE     ProcessId;      // +0x20
    WCHAR      ImageName[260]; // +0x28
} DETECTION_ENTRY;

Image-load notify populating the detection list

Figure 4: Image-load notification building detection entries.

Dispatch functions

Create / Close

These handlers are simple: any user-mode caller may open and close the device.

Dispatch_CreateClose accepting all connections

Figure 5: Create/Close dispatch — always succeeds.

IOCTL dispatch

The dispatch routine accepts several IOCTL codes, but it will not process most of them unless the caller’s PID is on an authorized linked list. That list is populated by only one IOCTL: 0x22A000.

IOCTL dispatch gate checking the authorized PID list

IOCTL 0x22A000

This IOCTL first reads the user-mode client’s PEB field at *(PEB + 0x2D0) (AppCompatFlagsUser), which points at a small request block:

// PEB+0x2D0 request block — the covert channel (0x18)
typedef struct {
    PVOID  EncryptedImage;
    SIZE_T ImageSize;
    PVOID  KeySeed;
} _PEB_LOAD_REQUEST;

After parsing that structure, the driver uses KeySeed to initialize an RC4 key, decrypts the encrypted MZ image, and stores the decrypted address in a global kernel variable.

RC4 decrypt of the PE staged via the PEB channel

If decryption succeeds, it adds the caller’s PID to the authorized-process list so later IOCTLs are allowed instead of returning access denied:

// AuthorizedPids (0x18)
typedef struct {
    LIST_ENTRY Links;
    PVOID      PID;
} _VALUE_ENTRY;

IOCTL 0x222004

This IOCTL manually maps and executes the PE staged by 0x22A000. Afterward it removes the caller from the authorized PID list, so the client cannot issue further IOCTLs until it calls 0x22A000 again.

That is an odd, redundant design: the malware cannot keep using the rootkit’s monitoring features across requests. Each follow-up IOCTL requires shipping another encrypted PE and RC4 seed through the PEB channel just to re-authenticate.

Manual-map and execute of the staged PE, then de-authorize PID

Additional detail for the 0x222004 map-and-execute path

IOCTLs 0x226048, 0x2260C0, 0x22A040, 0x22A044, 0x22A0C4

Once the caller is on the authorized list, these IOCTLs add, remove, or enumerate the process watch list, and collect or clear detections of modules that matched the watch list.

Collecting detected loaded modules for the user-mode client

Figure 6: Collecting detected loaded modules from the watch list.

Full IOCTL dispatch table

IOCTL Auth Purpose
0x22A000 None Fetch and RC4-decrypt a PE from the PEB channel into kernel pool; authorize the caller PID for later requests
0x222004 PID in auth list Manual-map the PE staged by 0x22A000, call its entry point, then de-authorize the caller PID
0x226048 PID in auth list Dump the process/module watch list
0x2260C0 PID in auth list Dump the detection log (watched process/modules that loaded)
0x22A040 PID in auth list Add a watch-list entry
0x22A044 PID in auth list Remove a watch-list entry
0x22A0C4 PID in auth list Free and clear the entire detection log

IOCs

PebGhost IOCs

eab035d86067b3e318ca35133e61eb25b2b97bb5c0346445f6ae4e6dc42f193f
133cb83b66baee01c507dca2d3e83aa57034ebc09e570bddc358f9b827aa6d6f
ede9f89342530c6b539d5de4a162c2dbab970b33caaca2c82af35d024a9e3002
e21c518d6f913a7b05f97f843c262f70c9ef911fe45315c9d0ed3f7ee922edc7
9c16ce082bc646133a0fd2d2c956671195dc91eac3077ddd2d169dc3f9739a0c
f77eb704a59d1553587655e20526b65cacbdf74d41734425a2c432801e1c8b14