TL;DR
You can redirect a program’s execution without directly injecting and running shellcode by manipulating existing code or function pointers. This guide shows how to achieve control flow hijacking using techniques like Return-Oriented Programming (ROP) and function pointer overwrites.
Understanding the Problem
Sometimes, security measures prevent you from simply inserting and executing your own malicious code (shellcode). However, many programs still have vulnerabilities that allow you to change where the program executes. This is control flow hijacking. You’re not running new code; you’re making the existing code run in a different order or call different functions than intended.
Techniques for Control Flow Hijacking
- Return-Oriented Programming (ROP)
- ROP involves finding short sequences of instructions (‘gadgets’) that end in a
retinstruction. These gadgets typically perform useful operations like moving data, arithmetic, or system calls. - You chain these gadgets together by overwriting the return address on the stack with the addresses of your chosen gadgets. Each gadget’s
retinstruction then jumps to the next gadget in the chain. - Finding Gadgets: Use tools like
ROPgadgetorrp++to identify suitable gadgets within the target binary.ropgadget --binary vulnerable_program | grep 'ret' - Building a ROP Chain: Carefully construct your chain, ensuring each gadget sets up the necessary data for the next. This often involves manipulating registers and the stack.
# Example (simplified) - move value from address A to register EAX, then call function at address Bmov eax, [A]; ret call B; ret
- If a program stores function pointers in memory, you can overwrite these pointers with the addresses of other functions (either existing ones or those provided by you).
- When the program calls through the overwritten pointer, it will execute your chosen function instead.
- Identifying Function Pointers: Disassemble the target binary and look for variables that are used as arguments to function calls.
objdump -d vulnerable_program | grep 'call *(' - Overwriting the Pointer: Use a buffer overflow or other memory corruption vulnerability to overwrite the function pointer with your desired address.
- In C++, virtual functions are accessed through vtables. Overwriting entries in a vtable can redirect calls to different methods.
- This is more complex than simple function pointer overwrites, as it requires understanding the object’s layout and vtable structure.
Practical Steps
- Identify a Vulnerability: Find a buffer overflow, format string bug, or other memory corruption issue that allows you to overwrite control data (return address, function pointer, etc.).
- Locate Useful Code/Gadgets: If using ROP, find gadgets within the target binary. If using function pointers, identify the relevant pointers and their locations in memory.
- Craft Your Payload: Construct your payload to overwrite the control data with your desired addresses.
- For ROP chains, this involves calculating offsets and arranging gadget addresses correctly on the stack.
- For function pointer overwrites, it’s a simpler matter of providing the target address.
- Exploit: Trigger the vulnerability to execute your payload.
Example Scenario (Function Pointer Overwrite)
Let’s say a program has this code:
void (*func_ptr)(int); // Function pointer
...
func_ptr = some_function; // Initialized to 'some_function'
...
func_ptr(user_input); // Call the function pointed to by func_ptr
If you can overwrite func_ptr with the address of a different function, you can control which function is called. You’d need to find an exploitable vulnerability that allows writing to the memory location where func_ptr is stored.
Important Considerations
- Address Space Layout Randomization (ASLR): ASLR randomizes the base addresses of libraries and other program components, making it harder to predict gadget addresses. Techniques like information leaks can help bypass ASLR.
- Data Execution Prevention (DEP/NX): DEP prevents code execution from data sections of memory. ROP is often used to bypass DEP by chaining together existing code snippets.