Unsubscriptions are Free is a 100 point binary-exploitation challenge in picoCTF. The description states:
Check out my new video-game and spaghetti-eating streaming channel on Twixer!
program
and get a flag.source
nc mercury.picoctf.net 48259
So we get the program, the source code, and the address:port to connect to the remote challenge.
The hint just links to a PDF here.
The hint links to a PDF titled "Heap Overflows and Double-Free Attacks" by Yan Huang at Indiana University. The presentation covers variable arguments in format strings, abuse of dynamic memory on the heap, and use after free attacks. Given the title of the challenge, I will assume the author of this challenge wants us to key in on the use after free section. It's a good presentation!
$ checksec ./vuln
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
I always check out main
first; I think it gives a good starting point to
understanding the program.
int main(){
setbuf(stdout, NULL);
user = (cmd *)malloc(sizeof(user));
while(1){
printMenu();
processInput();
//if(user){
doProcess(user);
//}
}
return 0;
}
The first thing we notice is that there are commented lines that look like it
used to check whether or not the user
was NULL. user
is dynamically created
on the heap in this function, so that might be a clue. Let's walk down the
functions that main
calls. printMenu()
doesn't look interesting.
processInput
does some parsing of the user data, so let's look at that.
void processInput(){
scanf(" %c", &choice);
choice = toupper(choice);
switch(choice){
case 'S':
if(user){
user->whatToDo = (void*)s;
}else{
puts("Not logged in!");
}
break;
case 'P':
user->whatToDo = (void*)p;
break;
case 'I':
user->whatToDo = (void*)i;
break;
case 'M':
user->whatToDo = (void*)m;
puts("===========================");
puts("Registration: Welcome to Twixer!");
puts("Enter your username: ");
user->username = getsline();
break;
case 'L':
leaveMessage();
break;
case 'E':
exit(0);
default:
puts("Invalid option!");
exit(1);
break;
}
}
The call to scanf
looks fine. It also doesn't look like we can provide any
other input other than ['S', 'P', 'I', 'M', 'L', 'E'] without exit
'ing the
program. There are an appropriate amount of break
statements and a default
option. Choosing S
will set the user->whatToDo pointer
to the function s
,
which prints out the address to a function, hahaexploitgobrrr
, that will read
the flag. The pointer to the function hahaexploitgobrrr
will probably come in
handy.
void s(){
printf("OOP! Memory leak...%p\n",hahaexploitgobrrr);
puts("Thanks for subsribing! I really recommend becoming a premium member!");
}
void hahaexploitgobrrr(){
char buf[FLAG_BUFFER];
FILE *f = fopen("flag.txt","r");
fgets(buf,FLAG_BUFFER,f);
fprintf(stdout,"%s\n",buf);
fflush(stdout);
}
Entering P
at the prompt just prints a message. Doesn't look interesting.
Entering I
asks the user to confirm that they are leaving via a valid scanf
call and then free
's the malloc
'ed user
. It is notable that the call to
free
does not have any checks to ensure it hasn't already been free
'd.
Based on what we've seen so far, it looks like it would be possible to call
free
multiple times over and over again since this takes place in a loop.
This could be interesting.
void i(){
char response;
puts("You're leaving already(Y/N)?");
scanf(" %c", &response);
if(toupper(response)=='Y'){
puts("Bye!");
free(user);
}else{
puts("Ok. Get premium membership please!");
}
}
Entering M
will allow the user to enter a username and store it on the heap
inside of the user
structure. The processing of user input looks OK, as it
realloc
's the size of the buffer if we enter more than the initial buffer can
hold. Entering L
allows us to leave a message which suspiciously only reads
in eight bytes (the size of a pointer or integer) onto the heap. Hmm.
void leaveMessage(){
puts("I only read premium member messages but you can ");
puts("try anyways:");
char* msg = (char*)malloc(8);
read(0, msg, 8);
}
Finally, E
just exits the program.
- We can call
doProcess
even if theuser
has beenfree
'd. - We can leak the address of the function that will read out the flag.
- We can
free
theuser
at any point. - We can read a message onto the heap that contains exactly eight bytes.
Here's my current thought process. We malloc
a user that gets allocated on
the heap (this is done for us in main
). We then unsubscribe and free
our
user. We could then leave a message, msg
, which will malloc
eight bytes of
data and set it to the message that we write.
When we malloc
eight bytes for our message, the heap manager will first look
in the per-thread cache (also known as the tcache) to see if a
recently free
'd chunk can satisfy the request. Because user
was also eight
bytes, this same chunk will be given to malloc
when we ask to leave a
message. We now have a situation where msg
and user
(even though we have
free
'd it) point to the same thing. Of course, the program shouldn't be using
user
anymore since we have released that chunk.
But it does! We noticed that we can free
the user
and continue to call
processInput
which references user
a lot. Even worse, after processInput
is
called in main
, there is a call to doProcess(user)
. This function will just
invoke the whatToDo
function that belongs to user
's struct.
Here is where we get lucky. The cmd
structure that implements user
is
defined as so:
typedef struct {
uintptr_t (*whatToDo)();
char *username;
} cmd;
How structs are laid out in memory is implementation specific. However, there's
a really good chance that when we write our msg
that shares the same pointer
to user
, the eight bytes that we write will overwrite the whatToDo
field.
This means that the pointer we end up writing to msg
will just be directly
called by doProcess(user)
.
Let's set breakpoints for free
and malloc
. If we run our program up until
we malloc
our msg
(GDB script Part 1 in solve.py
), we can see that the
address 0x8b861a0
(this may change) is in the per-thread cache. This was our
pointer to the user
struct. If we continue our script (Part 2) and examine
what malloc
returns when we dynamically create our msg
, we get the same
pointer returned, 0x8b861a0
! When we next call read
from stdin, we will
write our function pointer and overwrite the function pointer in the user
struct. When this is called later, we will get our flag.
picoCTF{d0ubl3_j30p4rdy_cff1f12d}