Evasion techniques built into Tengu - sleep obfuscation, string obfuscation, and in-memory execution.
Tengu was not designed to evade EDR products out of the box. The primitives documented here are a solid operational baseline, but Tengu is primarily designed to be capable and modular - not to win a cat-and-mouse game with vendors. Operators are expected to wrap it with their own tooling where stealth is a hard requirement.
Sleep obfuscation
During each sleep interval, Tengu XOR-encrypts its own r-x code pages in memory. An in-memory scanner running while Tengu sleeps sees only garbage.
How it works
- A helper thread is spawned before sleep. It holds pre-resolved libc function pointers (
nanosleep,mprotect,sem_post,sem_wait) obtained viadlsymbefore encryption starts. These pointers are stored in a heap-allocated context, so the thread can operate without touching the PLT (which is inside the encrypted region). - The main thread reads
/proc/self/mapsand collects allr-xsegments belonging to the agent binary. The pages containing the obfuscation code itself are excluded using GNU ld section boundary symbols (__start_obf_text/__stop_obf_text). - Each segment is XOR-encrypted with a 32-byte random key and re-mprotected to
PROT_NONE. The agent is now invisible to scanners. - After the sleep interval, the helper thread decrypts each segment back to
PROT_READ|EXECand signals completion via a semaphore. - The main thread resumes from the semaphore wait with fully restored code.
Robustness
All obfuscation functions are placed in a dedicated ELF section (obf_text) using __attribute__((section("obf_text"))). GNU ld auto-generates __start_obf_text and __stop_obf_text symbols that bracket the section exactly, regardless of how GCC orders functions within the binary (GCC -Os does not guarantee source order). This makes the page exclusion robust across all optimisation levels.
If any setup step fails (dlsym, /proc/self/maps parse, memory allocation), Tengu falls back silently to plain sleep().
Tunnel bypass
Sleep obfuscation is skipped when SOCKS5, reverse port forwarding, or a TCP pivot server is active. These tunnels require fast polling and cannot tolerate the agent being unresponsive for the full sleep interval.
String obfuscation
All sensitive strings in the binary are XOR-encrypted at compile time using a per-build key. This includes paths (/proc/self/maps, /proc/self/exe), library symbol names (mprotect, nanosleep, sem_post), and any other string that would be a detection indicator.
The SXOR(SXOR_CONSTANT) macro decrypts the string inline at the point of use. The plaintext never exists as a static string in the binary - only the encrypted bytes are present in the .rodata section.
The key and encrypted constants are generated by the teamserver builder at payload compile time via an auto-generated obfstr_data.h header. Each build gets a unique key.
In-memory execution (memfd)
The memfd command executes arbitrary ELF binaries entirely in memory using memfd_create(2). The steps are:
- The agent creates an anonymous file descriptor with
memfd_create("kworker/u:0", MFD_CLOEXEC). - The binary is written to the file descriptor.
- The binary is executed via
/proc/self/fd/<n>usingexecve. - The process appears in
/procaskworker/u:0(a legitimate kernel worker thread name). - No bytes are written to the filesystem at any point.
This bypasses filesystem-based detection (file creation events, inode monitoring, AV scanning of new files).
ChaCha20 application-layer encryption
All C2 traffic is encrypted with ChaCha20 regardless of transport. See C2 Profiles for details.
Even over plain HTTP, a passive observer sees only ciphertext. Network signatures based on plaintext payload patterns do not apply.
Summary
| Technique | Enabled by default |
|---|---|
| Sleep code-page encryption | Yes |
| Compile-time string obfuscation | Yes (per-build key) |
| In-memory binary execution (memfd) | On demand (memfd command) |
| ChaCha20 traffic encryption | Yes |
| ELF section-based exclusion (GCC-safe) | Yes |
