kernel driver that injects DLLs into Sea of Thieves bypassing EAC. manual maps the dll, hides executable memory from the anticheat using page table manipulation, and triggers execution through the steam overlay — no threads, no driver object, nothing for EAC to find. still undetected as of feb 2026.
so i wanted to inject a cheat dll into sea of thieves. sounds simple right? except EAC (easy anti cheat) is running in kernel mode and it checks basically everything. i tried all the normal stuff first — CreateRemoteThread, APC injection, SetThreadContext — all got flagged instantly. like literally within seconds the game would close.
spent about 2 weeks figuring out what EAC actually checks and how to get around each thing individually. this readme is basically me explaining what i did and why, its not super organized but whatever
first problem: EAC enumerates all loaded drivers. if you register a DriverObject with a device etc its game over immediately.
so my driver just... doesnt have one. the DriverEntry has a weird signature (PVOID, DWORD64 instead of the normal PDRIVER_OBJECT, PUNICODE_STRING) because its not meant to be loaded normally. you map it with kdmapper or something similar. DriverEntry spawns one system thread with PsCreateSystemThread and thats it. no device, no symbolic link, no nothing. EAC cant find what doesnt exist i guess
obviously cant use IOCTLs because EAC monitors that. i use a shared memory section instead (\BaseNamedObjects\Global\SOTInjectConfig). the usermode loader creates it, writes the target PID and dll path, driver opens it and reads everything from there. no IOCTLs needed.
the section name is built char by char at runtime btw. no static strings in the binary that scanners could find. maybe overkill but idk better safe
before injecting i need to make sure the game process is actually ready. i attach with KeStackAttachProcess and walk the PEB loader data looking for kernel32.dll. if its loaded, process is ready. retries every 500ms, up to 60 seconds total. sea of thieves takes forever to start so yeah
ok so this is probably the most important part. i need to somehow execute code in the game process but i cant create threads because EAC detects that.
what i figured out: steam's overlay hooks the games Present function (IDXGISwapChain::Present). it stores the original function pointer in a global inside GameOverlayRenderer64.dll. i found the exact offset by loading the dll in IDA — its at +0x1621D8, theres a qword there that holds the real Present address.
so the idea is: from kernel mode, find that dll in the process, read the original Present pointer, then later replace it with my shellcode. the game calls Present every frame so my code runs automatically. no thread creation at all.
honestly i was surprised this worked first try. well not first try but like third try lol
the driver reads the dll file into kernel pool memory and then does a full manual map into the game process. nothing too crazy here but some parts were annoying:
sections — allocate SizeOfImage bytes with ZwAllocateVirtualMemory, copy PE sections. standard stuff
relocations — process DIR64 and HIGHLOW relocations. the image always gets rebased so this always runs
imports — ok this part sucked. cant use GetProcAddress or LdrLoadDll because EAC hooks those (or at least monitors them, not 100% sure). so i walk the PEB module list myself, find the exporting dll, parse its export table manually. works fine for normal dlls but then i ran into the api-set thing. modern dlls import from api-ms-win-crt-runtime-l1-1-0.dll and stuff like that which arent real dlls. had to build a resolver that maps those to the actual dlls — crt stuff goes to ucrtbase.dll, core stuff to kernelbase.dll, vcruntime to ntdll. probably not complete but covers everything my dll needs
the PTE trick — ok this is the cool part and the main reason EAC cant detect the injected memory.
normally youd call ZwProtectVirtualMemory with PAGE_EXECUTE_READWRITE to make your code executable. but EAC walks the VAD tree and looks for executable memory that isnt backed by a legitimate module. instant detection.
what i do instead: i leave everything as PAGE_READWRITE in the VAD. then i go directly into the page table entries and clear the NX bit (bit 63) for the pages that need to be executable. to find the PTE base i scan ntoskrnl's .text section for the MiGetPteAddress pattern (48 C1 E9 09 48 B8). then for each page in executable sections i just clear bit 63 in the PTE and flush TLB with __writecr3(__readcr3()).
result: NtQueryVirtualMemory says its RW memory. but the CPU sees the PTE and executes it fine. EAC has no idea. i think i found this idea from some random post about VAD vs PTE inconsistency, then looked at the intel manual (vol 3) to understand how it actually works. took me a while to get right tbh, had a bunch of BSODs before i got the TLB flush correct
exception handling — almost forgot about this. the mapped dll uses C++ exceptions so i need to register the .pdata section via RtlAddFunctionTable. without this every try/catch crashes. learned that the hard way
i build a small shellcode (~160 bytes or so) that does:
- calls RtlAddFunctionTable for exception support
- calls the dlls entry point (DllMainCRTStartup) with a flag page as lpReserved — flag page has the original Present address and the pointer location
- jumps to original Present so the game keeps rendering
then i just overwrite the overlay's Present pointer with my shellcode address. next frame the game calls Present, hits my shellcode, dll initializes, and the dll itself restores the original Present pointer from the flag page.
so its a one-shot thing. after that first frame everything is back to normal. clean stack traces from then on.
if the overlay isnt loaded (disabled or whatever) theres a fallback. i find a syscall stub in ntdll like NtWaitForSingleObject (pattern match the 4C 8B D1 B8 prologue), save original bytes, and patch in a jump to my shellcode. the shellcode uses lock bts for atomic one-shot execution then restores the original stub. works but the overlay method is cleaner
been running this for weeks now, no ban so far. heres what EAC checks and why this avoids it (as far as i understand it):
- threads — i dont create any. execution comes from the games own Present call
- VAD flags — all memory is PAGE_READWRITE. the executable bit is only in the PTEs which NtQueryVirtualMemory doesnt report
- driver objects — there is no driver object. nothing to enumerate
- IOCTLs — no device, no irps. shared memory only
- module list — dll is manually mapped, not in any loader list
- imports — resolved manually. no LdrLoadDll calls
- pool tags — randomized with rdtsc. no fixed tag
- stack traces — shellcode is one shot. after first frame, Present is restored, stack is clean
- string sigs — section name built at runtime char by char
i might be wrong about some of these. EAC changes their stuff all the time. but so far so good
you need WDK and visual studio 2022 with kernel driver stuff installed. windows sdk 10.0.26100.0 or whatever version you have should work
the vcxproj is already set up:
- entry point is DriverEntry
- subsystem native
/kernel /GS-- links ntoskrnl.lib, hal.lib, wmilib.lib
- no CRT, no manifests
msbuild driver.vcxproj /p:Configuration=Release /p:Platform=x64
output goes to bin\Release\driver.sys
to load it use kdmapper or any vuln driver mapper. do NOT try to load it through service control manager, it will not work (wrong DriverEntry signature on purpose)
main.cpp - all the driver code, single file ~1060 lines
ntoskrnl_apc.def - def file for some unexported ntoskrnl stuff
driver.vcxproj - vs build project
README.md - this
educational purposes only etc. dont use this to cheat in online games. or do idc im not your mom
