Skip to content

Lab 10 — Anti-Analysis Techniques

Hands-on lab · ← Back to the module concept

Setup

git clone https://github.com/plaintext-security/plaintext-labs
cd plaintext-labs/malware/10-anti-analysis
make up
make fetch-sample      # opt-in: pulls a real GuLoader sample into ./samples/ (static-only)
make demo              # runs the bundled SYNTHETIC demo — no key, no network needed

⚠ This lab studies a live, heavily evasive malware sample. Handle it accordingly. - Static only — the real sample is NEVER executed. GuLoader exists to defeat dynamic analysis; you identify its anti-VM / anti-debug logic by reading it (strings, objdump, Ghidra), never by detonation. Do not run the fetched binary. - The synthetic demo is the only thing that runs. data/anti_analysis_demo.c is benign teaching code compiled inside the isolated container — the legible offline fallback that lets you see each mechanism (ptrace anti-debug, RDTSC/timing, /proc/cpuinfo VM check) fire before you hunt the same families in the real sample. - Isolation. All handling of the real sample stays inside the isolated container; never copy it to your host except into a disposable analysis VM. - Hygiene. The sample is fetched at lab time (password-protected zip, password infected) and is never committed.gitignore covers samples/. make fetch-sample needs a free abuse.ch Auth-Key (set MB_AUTH_KEY). - Offline fallback. No key / MalwareBazaar unreachable? Skip make fetch-sample; make demo runs entirely on the bundled synthetic demo — you still complete every learning objective on legible code.

Scenario

A loader flagged as GuLoader (a.k.a. CloudEyE, MITRE S0561) lands in your triage queue. A sandbox reported "no suspicious activity" — and that is precisely the problem. GuLoader is one of the most aggressively anti-analysis loaders in the wild: CrowdStrike documents it scanning the entire process memory for VM-related strings (hashing window-class names like VMSwitchUserControlClass and vmtoolsdControlWndClass), and Unit42 documents its vectored-exception-handler anti-debug that scatters 0xCC (INT3) bytes and terminates when hardware or software breakpoints are found. A sandbox that reports "benign" almost certainly tripped one of these checks. Your job is to identify GuLoader's documented anti-analysis logic statically — the anti-VM string scan and the exception-handler anti-debug — and map each to MITRE ATT&CK (T1497 Virtualization/Sandbox Evasion, T1622 Debugger Evasion).

To understand each mechanism before you go hunting it in obfuscated real code, the lab ships data/anti_analysis_demo.c — a benign C program that implements the same three families on Linux: a ptrace debugger check, a /proc/cpuinfo hypervisor scan, and a clock_gettime timing check. make demo runs it with and without each bypass so you can watch the checks fire and then defeat them. The synthetic demo is the legible model; GuLoader is the real-world target you identify the same families in.

Throughout: the real sample = the GuLoader PE that make fetch-sample drops in samples/static identification only, never executed. The demo = the bundled synthetic anti_analysis_demo you may safely run inside the container.

Do

  1. [ ] Observe the baseline behaviour (checks firing). Run make shell and inside the container: ./anti_analysis_demo. It should print which checks detected an analyst environment and exit early. Record which checks triggered.

Hint: in the container, the hypervisor flag is likely present in /proc/cpuinfo. The ptrace check will fire only if a tracer is attached.

  1. [ ] Trace syscalls with strace. Run strace -e trace=ptrace,openat,read ./anti_analysis_demo 2>&1 | head -40. Identify which syscalls the ptrace check generates. Does strace itself cause the ptrace check to trigger or not?

Hint: strace uses ptrace internally. Does PTRACE_TRACEME return -1 under strace?

  1. [ ] Bypass the /proc/cpuinfo hypervisor check with a bind mount. The container has a modified /lab/data/fake_cpuinfo with the hypervisor flag removed. Run the bypass script: python3 /lab/data/run_with_fake_cpuinfo.py. Confirm the hypervisor check no longer triggers.

  2. [ ] Bypass the ptrace check with an LD_PRELOAD hook. Examine data/ptrace_hook.c. It defines a replacement ptrace() that always returns 0 (success). Compile it: gcc -shared -fPIC -o /tmp/ptrace_hook.so /lab/data/ptrace_hook.c. Then run: LD_PRELOAD=/tmp/ptrace_hook.so ./anti_analysis_demo. Confirm the ptrace check no longer triggers.

  3. [ ] Defeat the timing check. Examine how the timing check works in anti_analysis_demo.c. Then compile data/clock_hook.c and preload it alongside the ptrace hook. Run both together: LD_PRELOAD="/tmp/ptrace_hook.so /tmp/clock_hook.so" ./anti_analysis_demo. All checks should now pass.

  4. [ ] Now find the same evasion families in the REAL GuLoader sample — statically. Run make fetch-sample, then analyse the binary it stages in samples/ without ever executing it. GuLoader's logic is documented by CrowdStrike and Unit42 — use those writeups as your map. Locate:

  5. The anti-VM string scan (T1497.001). GuLoader walks process memory with NtQueryVirtualMemory and DJB2-hashes window-class names. The synthetic demo's /proc/cpuinfo scan is the legible version of this same family. Pull strings and imports from the sample and look for VM/sandbox artefacts and the memory-scan API surface.
  6. The exception-handler anti-debug (T1622). GuLoader registers a vectored exception handler (AddVectoredExceptionHandler) and scatters 0xCC (INT3) bytes; the handler decodes the real control flow and bails when a breakpoint is detected. In a disassembler, find the VEH registration and the dense 0xCC regions that confuse linear disassembly — the real-world analogue of the demo's ptrace/timing checks.

Hint (static only): strings -a samples/<sha> | grep -iE 'vm|vbox|qemu|sandbox|hypervisor'; objdump -d or Ghidra to find AddVectoredExceptionHandler and the 0xCC clusters. Record the evidence (offsets / strings / imports) in your notes. Do not run this binary. No key / offline? Skip this step — every objective below is completable on the synthetic demo.

  1. [ ] Map to ATT&CK. Map each mechanism — in both the demo and the real sample — to the relevant ATT&CK (sub-)technique:
  2. ptrace self-debug check (demo) / VEH breakpoint-detection (GuLoader) → T1622 Debugger Evasion
  3. VM/hypervisor detection (/proc/cpuinfo demo / NtQueryVirtualMemory string scan in GuLoader) → T1497.001
  4. Timing check (demo) → T1497.003 Record the technique IDs and one-line evidence (which string/syscall/offset) for each. Cross-check the GuLoader mappings against MITRE S0561.

  5. [ ] Author a YARA rule for the anti-analysis indicators and prove it (the build half). detect_anti_analysis.py (below) greps the indicator strings — promote that finding to a committed, ATT&CK-tagged detection. Write anti-analysis.yar that keys on the evasion indicators you confirmed in steps 1–5: the ptrace / PTRACE_TRACEME strings, the /proc/cpuinfo + hypervisor pair, and a timing-call name (clock_gettime / rdtsc). Require a combination (e.g. 2 of them) so a binary that merely uses one of these for legitimate reasons doesn't trip, and tag each string or the rule's meta with its ATT&CK ID (T1497.001 / T1622) so the match is self-documenting. Then prove the two-sided result: yara anti-analysis.yar ./anti_analysis_demo must match, and yara anti-analysis.yar /bin/ls (a benign control that calls clock_gettime but performs no evasion) must not match. If /bin/ls matches, you keyed on a single common library call — raise the N of them threshold or pin to the more specific indicators (PTRACE_TRACEME, the /proc/cpuinfo scan) until only the demo trips it. Bypassing the checks and authoring the detection that flags the evasion are equal halves. (Stretch, if you fetched the real sample: add a second rule keyed on a GuLoader anti-VM artefact you confirmed statically — e.g. a window-class string or the AddVectoredExceptionHandler import — and note whether it fires on the real PE.) Hint: yara /path/to/rule /path/to/file; these are plain strings in the binary, so a string-condition rule with a count threshold is enough — no pe module needed for the ELF demo.

Success criteria — you're done when

  • [ ] You can articulate which check each of the three techniques implements.
  • [ ] strace output shows the specific syscalls involved in the ptrace check.
  • [ ] The LD_PRELOAD hook causes the ptrace check to report "no debugger detected."
  • [ ] The timing bypass causes the timing check to report "timing normal."
  • [ ] All three ATT&CK IDs (T1497.001, T1497.003, T1622) are recorded in your notes with one-line evidence summaries, for both the demo and — if fetched — the real GuLoader sample.
  • [ ] (If you ran make fetch-sample) You located GuLoader's anti-VM string scan and exception-handler anti-debug statically, with evidence (strings / imports / offsets) recorded — and never executed the sample.
  • [ ] anti-analysis.yar matches ./anti_analysis_demo and does not match /bin/ls — the build half, proven two-sided, with the ATT&CK tags on the rule.

Deliverables

Commit to your portfolio repo: - anti-analysis-notes.txt — which checks fired, which bypasses worked, ATT&CK IDs, your static-identification evidence for the real GuLoader sample (if fetched), and a one-paragraph summary of what this means for sandbox reliability. - anti-analysis.yar — the authored, ATT&CK-tagged detection rule, with the match (./anti_analysis_demo) / no-match (/bin/ls) proof recorded in anti-analysis-notes.txt. - Do not commit compiled .so hook files, core dumps, or strace logs.

Automate & own it

Required. Write detect_anti_analysis.py — a static scanner that: 1. Takes a binary path as an argument. 2. Runs strings against the binary and searches for known anti-analysis indicators: ptrace, PTRACE_TRACEME, /proc/cpuinfo, hypervisor, clock_gettime. 3. For each match, prints [INDICATOR] <string> — possible <technique> (ATT&CK <ID>). 4. Outputs a summary count.

Draft with AI assistance. Test it against anti_analysis_demo (expect hits) and /bin/ls (expect no hits). Commit detect_anti_analysis.py.

AI acceleration

Paste data/anti_analysis_demo.c into a model. Prompt: "List every anti-analysis technique in this C program, name the bypass for each, and map each to a MITRE ATT&CK technique ID." Cross-check each ATT&CK ID against attack.mitre.org — models occasionally hallucinate sub-technique numbers.

Connects forward

Module 11 applies similar evasion logic to the document and script layer: Office macros and PDF JavaScript use their own anti-analysis idioms (environment variable checks, application-title detection). The bypass philosophy — intercept the check before it reaches the OS — transfers directly.

Marketable proof

"I statically identify anti-debug, anti-VM, and timing evasion in real malware — I located GuLoader's process-memory anti-VM string scan and its vectored-exception-handler anti-debug without executing it, reproduced and bypassed the same families on a legible demo with LD_PRELOAD and environment manipulation, and mapped each to ATT&CK (T1497 / T1622) — giving sandbox operators and IR engineers a clear picture of why a sample appeared benign."

Stretch

Add a fourth bypass: write a GDB script (bypass.gdb) that patches the binary in memory at the ptrace call site to return 0 unconditionally, then runs the binary to completion. This demonstrates that symbol-patching in a debugger works for statically linked binaries where LD_PRELOAD fails.

Comments

Sign in with GitHub to comment. Choose the type: Feedback (errors or suggestions on this page) · Hints (help for fellow learners — no spoilers) · General (anything else).