Pwnables Write-up: FSB

I'm on vacation in Mexico this week, which essentially means blow off pool time and hack stuff. I decided a relatively simple challenge would be a good starting point for the week, since I've never CTFed on this laptop and I was expecting to be constantly interrupted by vacation conversation from my girlfriend's family.

I decided to hop on pwnables FSB for 20 points:

Isn't FSB almost obsolete in computer security?
Anyway, have fun with it :)

ssh [email protected] -p2222 (pw:guest)

Code Analysis

I dropped onto the server, issued an ls, noticed a file named fsb.c, and pulled it down:

#include <stdio.h>
#include <alloca.h>
#include <fcntl.h>

unsigned long long key;
char buf[100];
char buf2[100];

int fsb(char** argv, char** envp){
        char* args[]={"/bin/sh", 0};
        int i;

        char*** pargv = &argv;
        char*** penvp = &envp;
        char** arg;
        char* c;
        for(arg=argv;*arg;arg++) for(c=*arg; *c;c++) *c='\0';
        for(arg=envp;*arg;arg++) for(c=*arg; *c;c++) *c='\0';
        *pargv=0;
        *penvp=0;

        for(i=0; i<4; i++){
                printf("Give me some format strings(%d)\n", i+1);
                read(0, buf, 100);
                printf(buf);
        }

        printf("Wait a sec...\n");
        sleep(3);

        printf("key : \n");
        read(0, buf2, 100);
        unsigned long long pw = strtoull(buf2, 0, 10);
        if(pw == key){
                printf("Congratz!\n");
                execve(args[0], args, 0);
                return 0;
        }

        printf("Incorrect key \n");
        return 0;
}

int main(int argc, char* argv[], char** envp){

        int fd = open("/dev/urandom", O_RDONLY);
        if( fd==-1 || read(fd, &key, 8) != 8 ){
                printf("Error, tell admin\n");
                return 0;
        }
        close(fd);

        alloca(0x12345 & key);

        fsb(argv, envp); // exploit this format string bug!
        return 0;
}

Essentially, this code does the following:

  • Generates an 8-byte random key
  • Allocates a random block of stack space
  • Accepts 4 strings on stdin, subsequently passing them to printf()
  • Prompts for a key on stdin
  • Pops a shell if the supplied key matches the generated one, exits otherwise

Crafting an Attack

Once I had a good understanding of the code, I knew what I needed to do: use the format string bug to overwrite key with a value I control.

Attacking Format Strings

If you're already familiar with format string attacks, you can skip this little box. Otherwise, keep reading for a quick introduction to the concept.

The nature of printf() means that anybody who controls the first parameter can control the program. Take this code:

int count = 0;
const char* name = "nick";
printf("My name is %s\nI've written %n", name, &count);
printf("%d bytes so far!\n", count);

which outputs this:

My name is nick
I've written 29 bytes so far!

The %s caused the first printf() to read and print a string from memory pointed to by the second argument. The %n caused it to write an integer value, equal to the number of bytes previously written, to memory pointed to by the third argument. The %d caused the second call to print the exact value passed as the second argument.

What this shows is that, when an attacker has control of a format string, they can leak memory and stack values, as well as overwrite memory.

To pull off my attack, I needed to first understand what the stack would look like at the time of the vulnerable printf(), as this information would determine how my attack would play out. I ran the program in gdb, dropped a breakpoint on the printf() call, and dumped the stack:

Next, I skimmed over every value, taking note of what they were. Soon, I realized that the stack was not only self-referential, but it even referenced itself multiple times:

There are three major wins here:

  • A self-referential stack means %n can write data without leaking any locality information
  • Having two refs means controlling two values at once, and these refs were consecutive even
  • In this case, the refs and targets exist above the randomized stack chunk, meaning their positions--and the positions they reference--are static

With this, I knew I had won. I began crafting my payload. I used %134520928c to output exactly 0x804a060 bytes (this is the address of key). Within the same string, I appended %14$n to write this value to the memory pointed to by argument 14. Still in the same string, I incremented the count by 4, yielding 0x804a064, and made it write this value to the memory pointed to by argument 15: 1111%15$n. Then, I crafted the second format string. Simply put, it would write 0x00000000 to the memory pointed to by arguments 20 and 21 (which, respectively, were pointed to by 14 and 15): %20$n%21$n. Effectively, this overwrote all eight bytes of key with 0, allowing me to supply 0 as the key and pass the check.

With that, my attack was complete. I still needed to send two more format strings, since it expected four, but they were non-consequential. Additionally, I made sure to run the program with output sent to /dev/null, since it would be printing out a couple hundred megabytes of blank space. My attack accounted for this by writing the flag to stderr:

./fsb >/dev/null
%134520928c%14$n1111%15$n
%20$n%21$n
i am gonna
hax u down
0
(>&2 cat flag)
{flag ommitted, go play yourselves!}

Attack Brevity

If you've read this blog before, you might know that I like to approach solved challenges from different angles in hopes of optimizing and shortening my attacks. This challenge was a great candidate.

My winning attack essentially went after the low-hanging fruit; altering the key was easy and obvious. I realized that, if I used a traditional control flow hijack, I could reduce the total number of inputs to two.

In Linux, calls between modules happen using the Global Offset Table (GOT). Effectively speaking, this is a list of addresses to functions in other modules, and it is filled at module-load time; you could say it is analogous to the Import Address Table in Windows. The GOT exists in writable memory, meaning it can be mutated without triggering any memory protection.

I drilled down into the assembly code and found the address of the block which calls execve("/bin/sh") at 0x804869f:

I decided that the call to read() would be as good as any to hijack, so I checked the call site:

Drilled into the trampoline stub:

readcode

And discovered that 0x804a000 is the address of read() residency in the GOT.

Knowing this I crafted a two-stage attack. The first stage used the ref at argument 14 to write 0x804a000 into argument 20. The second stage wrote 0x804869f to memory pointed to by argument 20. This caused the subsequent read() call to land after the key check, popping open a shell without prompting for the remaining two inputs:

./fsb >/dev/null
%134520832c%14$n
%134514335c%20$n
(>&2 cat flag)
{flag ommitted, go play yourselves!}

At 65 characters, this attack can fit in a tweet, even with the old 140 count. This is just barely ahead of the first attack, which weights in at 72 characters if I send the final two inputs as empty strings.

Improving The Attack

It might be possible to get a win using a single format string, but I haven't yet found a way. If, for instance, the GOT came before the code in-memory, the latter attack could be reduced to a single input like %134520832c%14$n%9999c%20$n, where 9999 is &GOT - &CODE. There might be some variation of this, but I'm content with my solutions and won't be looking for it.

Adios

I've got to get back to vacation, so that's all I have for now. Even though this challenge was very light, it gave me some entertainment and I think the write-up will make good material for those wanting to learn about format string vulnerabilities. If you have a different solution, or have any questions, you'll find me in the comments!