Evading Elastic EDR call stack signatures through dynamic code cave injection
RefinedPool is an enhancement of LibTPLoadLib by @AlmondOffSec, which uses call gadgets to break the call stack signature used by Elastic EDR on proxying module loads via Windows Thread Pool API.
The original LibTPLoadLib technique (detailed in this blogpost) works by:
- Using Thread Pool callbacks to proxy
LoadLibraryAcalls - Leveraging a call gadget (
call r10; add rsp, 0x28; ret) to remove the callback function from the call stack
- This breaks Elastic's detection pattern that looks for suspicious call stacks when loading libraries
The original approach required:
- Loading a specific hardcoded DLL (
dsdmo_10.0.26100.1882.dll) containing the gadget - The DLL had to be manually placed at
C:\dsdmo_10.0.26100.1882.dll - This creates a circular dependency: loading a suspicious DLL to evade detection of loading DLLs
The need for external hardcoded DLLs was eliminated by implementing dynamic code cave injection.
Instead of searching for pre-existing gadgets in hardcoded DLLs, RefinedPool:
- Enumerates loaded modules in the current process
- Locates code caves within executable sections, specifically:
- Searches inside existing function boundaries using the Exception Directory (
.pdata/RUNTIME_FUNCTIONtable) - Looks for continuous sequences of null bytes or INT3 instructions (
0xCC) - Ensures the cave is at least 10 bytes to fit the gadget
- Searches inside existing function boundaries using the Exception Directory (
- Writes the gadget dynamically into the discovered code cave:
41 FF D2 ; call r10 33 C0 ; xor eax, eax 48 83 C4 28 ; add rsp, 0x28 C3 ; ret
- Uses the injected gadget for the Thread Pool callback, just like the original technique
- No external DLL dependencies: Works with modules already loaded in memory
- Stealthier: No suspicious DLL load operations that could trigger alerts
- More practical: Doesn't require specific Windows versions or pre-staged files
- Dynamic adaptation: Searches across multiple candidate modules automatically
- Function-aware: Preferentially places gadgets within legitimate function boundaries for "better stealth"
To avoid critical system modules, RefinedPool excludes:
ntdll.dllkernel32.dllkernelbase.dll
The methods can be easily alternated in "loadlib.c".
-
WriteGadget (primary method):
- Combines code cave detection with dynamic gadget injection
- Uses
FindCodeCaveInFunctionto locate suitable memory space - Writes the 10-byte gadget sequence directly into discovered code caves
- Handles memory protection changes (VirtualProtect) automatically
-
FindCallGadget (original method):
- Searches for pre-existing gadget patterns in loaded modules
- Scans candidate DLLs for the byte sequence:
41 FF D2 ... 48 83 C4 28 C3 - Kept for compatibility and fallback scenarios
- Can be used as alternative when code cave injection is not desired
- LibTPLoadLib by @AlmondOffSec
- Repository: https://github.com/AlmondOffSec/LibTPLoadLib
- Blog: https://offsec.almond.consulting/evading-elastic-callstack-signatures.html
- License: BSD 3-Clause License (see tploadlib.c)
- Elastic EDR detection rule - The signature this technique bypasses
- @rasta-mouse's LibTP - Format inspiration for the original project
- Crystal Palace - Shared library framework used in the original
- Carregamento por Proxy - My research on proxy loading techniques
- Return Address Spoofing - My research on call stack manipulation
This project respects the original BSD 3-Clause License from LibTPLoadLib. See the copyright notice in tploadlib.c for the original license terms.
Special thanks to @AlmondOffSec / @SAERXCIT for the original research and implementation that made this enhancement possible.


