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 fsb@pwnable.kr -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 toprintf()
- 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 firstprintf()
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:
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!