picoCTF21 - Shop Writeup

Shop

Best Stuff - Cheap Stuff, Buy Buy Buy... Store Instance: source. The shop is open for business at nc mercury.picoctf.net 11371.

Upon first inspection, this is a go binary. According to some of my friends, it’s annoying to reverse (haha).

scribbl@beepboop:~$ file source
source: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, Go BuildID=r5IKmnk_hVFErwy5ewa3/PyI570w85RI5Xa1aSnrW/RxmeFbAluXa5Hnisdodi/MJQIt60cZyLjm5Wta-r0, with debug_info, not stripped

Loading it into IDA, we trace through main() without much success. There’s a get_flag() function that we can follow through the menu code.

// main.get_flag
// local variable allocation has failed, the output may be wrong!
void __golang __noreturn main_get_flag()
{
  string filename; // [esp+0h] [ebp-44h]
  _DWORD *filenamea; // [esp+0h] [ebp-44h]
  _BYTE filename_4[24]; // [esp+4h] [ebp-40h] OVERLAPPED
  int elem[3]; // [esp+28h] [ebp-1Ch] BYREF
  _DWORD a[2]; // [esp+34h] [ebp-10h] BYREF
  runtime_eface a_8; // [esp+3Ch] [ebp-8h]

  filename.str = (uint8 *)"flag.txt";
  filename.len = 8;
  *(retval_80D3040 *)&filename_4[4] = io_ioutil_ReadFile(filename);
  main_check(*(error *)&filename_4[16]);
  elem[0] = *(_DWORD *)&filename_4[4];
  elem[1] = *(_DWORD *)&filename_4[8];
  elem[2] = *(_DWORD *)&filename_4[12];
  a_8 = 0LL;
  a[0] = &RTYPE_string_0;
  a[1] = &main_statictmp_14;
  a_8 = runtime_convT2Eslice((runtime__type *)&RTYPE__slice_uint8_0, elem);
  filenamea = a;
  *(_QWORD *)filename_4 = 0x200000002LL;
  fmt_Println(*(_slice_interface_ *)&filename_4[-4]);
  os_Exit(0);
}

However, where does the user even interact with the program? We need to look at where the function takes user input since at the end of the day, it’s our input that will make a difference.

if ( wallet < inv.array[v11].price )
{
  v35[0] = &RTYPE_string_0;
  v35[1] = &main_statictmp_7;
  ai.array = (interface_ *)v35;
  *(_QWORD *)&ai.len = 0x100000001LL;
  fmt_Println(ai);
  v15 = wallet;
}
else
{
  inv.array[v11].count = count - v9;
  if ( *(_DWORD *)v10 >= inv.len )
    runtime_panicindex();
  v14 = *(_DWORD *)v10;
  v15 = wallet - *(_DWORD *)_num * inv.array[v14].price;
  if ( *(_DWORD *)v10 >= user.len )
    runtime_panicindex();
  user.array[v14].count += *(_DWORD *)_num;
  if ( inv.len <= 2u )
    runtime_panicindex();
  if ( inv.array[2].count != 1 )
    main_get_flag();
}
v13 = v15;

In this block, we notice our wallet is being updated with the price of the object we’re buying multiplied by the amount we set in _num. However, it doesn’t validate any negative inputs. Thus, we can just buy a negative value to increase the amount of money we have in our wallet!

scribbl@beepboop:~$ nc mercury.picoctf.net 11371
Welcome to the market!
=====================
You have 40 coins
        Item            Price   Count
(0) Quiet Quiches       10      12
(1) Average Apple       15      8
(2) Fruitful Flag       100     1
(3) Sell an Item
(4) Exit
Choose an option:
0
How many do you want to buy?
-100000
You have 1000040 coins
        Item            Price   Count
(0) Quiet Quiches       10      100012
(1) Average Apple       15      8
(2) Fruitful Flag       100     1
(3) Sell an Item
(4) Exit
Choose an option:
2
How many do you want to buy?
1
Flag is:  [112 105 99 111 67 84 70 123 98 52 100 95 98 114 111 103 114 97 109 109 101 114 95 98 56 100 55 50 55 49 102 125]
>>> flag = "112 105 99 111 67 84 70 123 98 52 100 95 98 114 111 103 114 97 109 109 101 114 95 98 56 100 55 50 55 49 102 125".split(" ")
>>> "".join(chr(int(c)) for c in flag)
'picoCTF{b4d_brogrammer_b8d7271f}'