CARS

a detailed breakdown of the challenge and my solving process

Description:

I've been playing around with this reporting system for "cyber affairs", generating ticket after ticket in hopes of breaking it. I have a feeling that there's a way to access the admin panel...

Challenge Files:

We are given an executable and a source code file

file-download
19KB
cars.c
#include <stdio.h>
#include <stdlib.h>

// Compiled using gcc -o cars -g -fno-stack-protector cars.c

unsigned long report_number = 0;

void setup()
{
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stdin, NULL, _IONBF, 0);
}

void file_report()
{
    char input[28];
    printf("Please input your student ID: ");
    fgets(input, 28, stdin);
    printf("Please describe the incident: ");
    fgets(input, 256, stdin);
    printf("The matter has been recorded and will be investigated. Thank you.\n");
}

void admin()
{
    // how did you even get here?
    FILE *fptr = fopen("flag", "r");
    if (fptr == NULL)
    {
        printf("Cannot open flag\n");
        exit(0);
    }
    char c;
    while ((c = fgetc(fptr)) != EOF)
    {
        printf("%c", c);
    }
    fclose(fptr);
}

int main()
{
    setup();
    srand(0xb1adee); // this random seed is sooo drain
    report_number = rand();
    printf("Welcome to the Cyber Affairs Reporting System (CARS)\n");
    printf("Report number: #%lu\n", &report_number);
    file_report();
    return 0;
}

Static analysis of the source code file

Upon looking at the .c file, we can notice a few things:

There are 3 functions of interest here, lets look at each one of them

main()

This function:

  1. seeds the rand() function with the seed 0xb1adee

  2. generates a random report number

  3. prints stuff and prints the report number

  4. calls file_report()

file_report()

This function:

  1. creates a character array variable called input with size of 28

  2. prints a prompt and gets the student id, which is written to the input variable

  3. prints a prompt and gets the description of the incident, which is also written to the input variable

  4. prints a fancy thank you message

Notice how the first fgets() call allows the user to write 28 bytes of input to the variable, and the second fgets() call allows the user to write 256 bytes of input to the same variable. As the amount of characters written to the input variable is more than it can store, there is a buffer overflow vulnerability present here which we will use later on in the exploit.

admin()

Now this is an interesting function, as it is not called from anywhere within the code

This function:

  1. opens a file called flag using fopen in read mode

  2. checks if the file is present

  3. if it is present, prints out the output of the file

This is our win function, aka the function that will allow us to get the flag.

The only thing we need to do is to hijack the execution flow and call this function via a ret2win exploit.

Checksec

This challenge appears to be a pretty simple ret2win challenge.

Except that it isnt.

The binary has PIE enabled, which stands for Position Independent Executable

PIE

The main difference between a binary with PIE and a binary without, is that every time you run the binary with PIE, it gets loaded into a different memory address

In a binary with PIE, things like function addresses are saved as an offset, and when the program is run, a randomnised base address will be generated and the function location during runtime would be equal to base address + offset

Defeating PIE

In order to bypass PIE and get a sucessful ret2win, we need to find some sort of leak to get the PIE base address.

Sus report number

Remember how we have identified that in main(), the program seeds the rand() function? Usually, if the seed is constant, the output of rand() will also be constant.

circle-info

This is the reason why programmers use the current time as the seed, as it is not constant and it is everchanging

However, in the program, our report number appears to be random, even though the seed never changes.

Due toe PIE randomnising the memory layout of the executable, the location of the rand() function changes too, which can affect its output and make it appear to change

Finding the base address

Since we have access to the C source file, we can recompile the binary on our own without PIE enabled

running the program gives us a static value report number, 4210832 (or 0x404090 in hex, easier to remember imo).

Theoretically, taking the report number the actual binary generates and subtracting it from this report number will give us our base address

Writing our exploit, part 1

lets run it.

gdb

Oops, looks like we have segfaulted the program.

From pwndbg, we can see that the program crashes due to the location of admin() that our solve script calculated being invalid

Simply put, our base address is wrong.

Finding the base address part 2

In order find the actual base address, I had to find out the margin of error which I got.

To do that, I disassembled the admin function in the same gdb session that was spawned by pwntools after the segfault.

Here's how it works:

Drawing

As we can see, there is a difference of 0x400000 between the actual base address and the calculated one.

Writing our exploit part 2

Lets modify our solve script a little bit

Lets test it out.

yay! our solve script works

Solving the challenge

Lets connect our solve script to the remote container

hmmm... our exploit works locally, but not on the remote container.

Stack alignment

A common reason why exploits work locally but not remotely is due to an issue called Stack Alignment.

From https://ir0nstone.gitbook.io/notes/types/stack/return-oriented-programming/stack-alignmentarrow-up-right

Essentially the x86-64 ABI (application binary interface) guarantees 16-byte alignment on a call instructionarrow-up-right. LIBC takes advantage of this and uses SSE data transfer instructionsarrow-up-right to optimise execution; system in particular utilises instructions such as movaps.

That means that if the stack is not 16-byte aligned - that is, RSP is not a multiple of 16 - the ROP chain will fail on system.

To solve this issue, we need to add a ret gadget into our payload, right before the hidden_function address

Final solve script:

blahaj{r0pp1n6_w17h_p13}

This was a fun challenge

Last updated