PebGhost rootkit loader
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.

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).

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


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;

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.

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 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.

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.


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.

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