Hello! In this post I wanted to go over the university challenge that caused me unnecessary stress due to my oversight. This technique is known as Return To LibC or ret2libc. This method bypasses security mechanisms like non-executable (NX) stack protection.
I want to add some quick Disclaimers that this post is still a work in progress and will be on my specific experience with this attack which as mentioned above was in the form of a university challenge. Due to this, I will be providing an overview of the attack and will not give out any specific details/code to avoid potentially spoiling the challenge for others. Apologies, struggling Coventry students who thought they hit the gold mine!
This article will also be focusing on a x86_64 system, so information may differ compared to 32bit, ARM, MIIPS etc.
Please do not take this information as gospel either, I am still quite new to exploit development and this is just to provide insight into my understanding of this attack, if you would like more detailed versions, please look at the notes.
What is a stack?
To understand ret2libc, we must also have a basic understanding of the stack. The stack is a region of a computer’s memory that operates in a Last-In, First-Out (LIFO) manner. This means that the system will process the most recent entry first. The stack helps manage where to return after a function call, store the function parameters, and keep track of local variables (more on those shortly).
In a x86_64 system, the stack grows downwards, meaning it starts from a high memory address and grows towards lower addresses as more data is added, so when something new is added, the stack itself expands downwards in memory.
|-----------------------------| <-- High Memory Address
| (Other data) |
|-----------------------------|
| Function Parameters |
|-----------------------------|
| Local Variables |
|-----------------------------|
| Return Address |
|-----------------------------|
| Stack grows downwards |
|-----------------------------| <-- Low Memory Address
When a function is called in a program, certain information will be pushed onto the stack, including:
- Return Address – The address to which the program should return after the function call is completed.
- Function Parameters – The arguments passed into the function.
- Local Variables – Variables that are declared inside the function.
- Saved Frame Pointer – To keep track of the caller function’s stack frame (also known as the EBP register)
Let’s have a look at this example script:
#include <stdio.h>
void calculateTotal(int items) {
printf("Calculating total for %d items\n", items);
}
void processOrder() {
int itemCount = 5;
calculateTotal(itemCount);
printf("Order processed with %d items\n", itemCount);
}
int main() {
processOrder();
return 0;
}
Here’s how the stack would be used:
- “
main()” Calls “processOrder()”- The return address (where ‘
main‘ should continue after the ‘processOrder‘ function finishes) is pushed onto the stack. - Space for local variables of ‘
processOrder‘ is allocated on the stack. - Control of the stack transfers to ‘
processOrder‘
- The return address (where ‘
- ‘
processOrder‘ Calls ‘calculateTotal(int items)’- Same again here, but this time the return address, where ‘
processOrder‘ should continue after ‘calculateTotal‘ finishes, is pushed onto the stack. - The parameter ‘
itemCount‘ is passed to ‘calculateTotal‘ via the RDI register - The control transfers to ‘
calcuateTotal‘
- Same again here, but this time the return address, where ‘
- ‘calculateTotal’ Completes execution
- After the function finishes, the return address is removed from the stack, and control will return to ‘
processOrder‘
- After the function finishes, the return address is removed from the stack, and control will return to ‘
- ‘
processOrder‘ Completes execution- Local variables of the function are deallocated by adjusting the stack pointer (RSP)
- The return address is removed from the stack, and control will return to ‘
main‘
Here’s what the stack would look like when ‘processOrder‘ is calling ‘calculateTotal‘
|-----------------------------| <-- High Memory Address
| Parameter `items` | <-- `calculateTotal` uses this value
|-----------------------------|
| Return Address | <-- `processOrder` will return here after `calculateTotal`
|-----------------------------|
| Local Variable `itemCount` |
|-----------------------------|
| Return Address | <-- `main` will return here after `processOrder`
|-----------------------------|
| Stack grows downwards |
|-----------------------------| <-- Low Memory Address
There are 16 general purpose registers (e.g. RAX, RBX, RCX, RDX, RSI, RDI,RBP and RSP to name a few.) The stack pointer (RSP) points to the top of the stack, the base pointer (RBP) is often used to reference the stack frame of the current function.
What is ret2libc?
So, the ret2libc exploit is a computer security attack that typically starts with a buffer overflow exploit, where a threat actor provides more input than the target program can handle to cause an overflow of data into adjacent memory. Simply put it allows them to overwrite and leverage existing executable code in the program’s memory, specifically the standard C library (libc) to execute malicious commands.
ret2libc is usually based on the system function within the libc library as this function will execute the argument passed through it. The libc library also contains the string /bin/sh, so to put it simply during the buffer overflow by passing this string through system will pop a shell.
So let’s say a C program that uses the gets() function, a function that reads user without checking the buffer size, we could overflow this buffer and overwrite the return address to point to system and use the /bin/sh string to be placed next on the stack.
#include <stdio.h>
int main() {
char buffer[128];
gets(buffer);
printf("You entered: %s\n", buffer);
return 0;
}
Here’s a basic step by step of what the threat actor can do:
- Overflow buffer
- Threat actor sends a string longer than 128 bytes, causing the buffer to overflow
- Overwrite return address
- The return address on the stack is overwritten to point to ‘
system()‘
- The return address on the stack is overwritten to point to ‘
- Push argument ‘
/bin/sh‘- Pushes ‘
/bin/sh‘ to the stack after the return address.
- Pushes ‘
- Execute payload
- Function is returned and control is transferred to ‘
system(/bin/sh)‘, spawning a shell.
- Function is returned and control is transferred to ‘
Here’s how it would look on the stack:
Before:
|-----------------------------| <-- High Memory Address
| Return Address | <-- Points to next instruction in main
|-----------------------------|
| Local Variables |
|-----------------------------|
| Buffer |
|-----------------------------|
| Stack grows downwards |
|-----------------------------| <-- Low Memory Address
After:
|-----------------------------| <-- High Memory Address
| Address of `system()` | <-- Overwritten return address
|-----------------------------|
| Address of "/bin/sh" string | <-- Argument for `system()`
|-----------------------------|
| Buffer (overflow) |
|-----------------------------|
| Stack grows downwards |
|-----------------------------| <-- Low Memory Address
What register is needed to overflow?
In x86_64 architecture, we need to overwrite the return address on the stack, this address is stored on the stack, and ‘rsp’ points to it. By overflowing the buffer, we write past the intended buffer space and overwrite the saved return address. By overflowing the buffer of the target, we can write past the intended buffer space and overwrite the saved return address.
Buffer Offsets
In some cases, we may need to calculate the offset to ensure that we are overwriting the saved return address, as without a calculated overflow it may not be precise enough and may not overwrite the return address.
So, what would happen if the buffer size is incorrect? (I mean errors galore, obviously!) but in a more detailed version, if the buffer size is too small, you won’t reach the return address, and you wont be able to overwrite it, so the function will return to its original address as intended.
If the buffer size is too large, it will not just overwrite the return address but also additional memory locations beyond it, causing errors galore!
How can ‘ret’ help:
A ‘ret’ (return) instruction is used to correctly align the stack, it removes the top value from the stack and jumps to that address, returning control the the called function.
It is also used to chain ROP (Return-Oriented Programming) gadgets, by chaining the gadgets found in existing code found in libc, we can perform arbitrary operations, like setting the UID to 0 and chaining it to call the /bin/sh command to the system call.
Conclusion
This is quite a basic view on ret2libc and how it works, in the future, I want to add more to this post by going over what an example script would look like and maybe even create my own challenge! Again, this information is based on research and my own knowledge of the attack, if something is wrong, please feel free to let me know 🙂
Notes
If this post didn’t satisfy your needs, please feel free to check out these amazing articles which should give you more detailed information:
- https://ir0nstone.gitbook.io/notes/types/stack/return-oriented-programming/ret2libc
- https://www.exploit-db.com/docs/english/28553-linux-classic-return-to-libc-&-return-to-libc-chaining-tutorial.pdf
- https://www.spiceworks.com/tech/devops/articles/fifo-vs-lifo/#:~:text=LIFO%20stands%20for%20%27last%20in,the%20stack%20is%20processed%20first.
- https://www.spiceworks.com/tech/devops/articles/fifo-vs-lifo/#:~:text=LIFO%20stands%20for%20%27last%20in,the%20stack%20is%20processed%20first.
- https://www.ired.team/offensive-security/code-injection-process-injection/binary-exploitation/return-to-libc-ret2libc
Leave a comment