DawgCTF2020 “Ocean Boutique”
2020, April 12 (Sunday)
Yep, I solved one entire problem for UMBC’s DawgCTF 2020. I didn’t spend too much time on this CTF—Redpwn doesn’t really need my help. ;)
“Ocean Boutique” was a neat VM-type problem. Most of its challenge was trying to figure out what global variables represented. And figuring out the stupid linked lists.
$ ./ocean_boutique
Welcome to the Ocean Boutique.
Last year, y'all enjoyed the Snake Boutique, but I wanted to move closer to
the C and find sequels by the shore, so I opened a new boutique.
Your boss has directed you to purchase exactly 31337.42 of goods and keep the
receipt. Don't forget to make sure it all fits in your suitcase!
Thank you for using our self checkout, enjoy your vacation!
Enter transaction.
It looks like we need to enter some sort of transaction in order for it to give us the flag. Let’s disassemble.
Overall flow
Main@0x1143 calls process-input@0x0DBB, then process-transaction@0x10DB. These are just names I made up that make some sense.
Process-input() then uses a loop to call sym.imp.fgets(), then sym.imp.strtok() to split one line of input into two strings by a delimiting space. These strings are then passed to sym.imp.atoi() to convert them into two integers; we’ll call these ATOI0 and ATOI1.
Next, process-input() calls LL8-append@0x0C84(ATOI0, ATOI1). This process is looped until ATOI0 and ATOI1 are 9 and 10 respectively, which sets the byte 0x2020B1 to 1 and returns.
LL8-append
The LL8-append function is mostly just one loop.
Sym.imp.malloc() is called to obtain a pointer HEAP_PTR, and the arguments ATOI0 and ATOI1 are then written to a pair of four bytes each at that heap address. Then, the function checks the LL8_head@0x2021B0 variable in memory, which is the first element of the linked list. This is loaded into the LL_item variable, and then the loop recursively traverses the addresses at (LL_item, 8) until the end of the list is reached. LL8-append will then write HEAP_PTR to the end of the last element in the linked list. In essence, a struct containing ATOI0, ATOI1 is appended to the tail of the linked list with head at LL8_head.
Why does this linked list matter? It’s used in the process-transaction function.
Process-transaction
What process-transaction does is traverse the linked list created through all those calls to LL8-append, and for each item in the list it calls process-instruction@0x0F97(ATOI0, ATOI1).
Operations and arguments
It’s clear from the switch-case structure that ATOI0, ATOI1 become a sort of instruction in the process-instruction function. We’ll call ATOI0 and ATOI1 by OP, ARG, respectively.
Operations:
- 3: (0x2020B0==0, arg>=0) op-three(arg);
- 4: (0x2020B0==0, arg>=0) 0x2020BC=arg;
- 6: (0x2020B0==1, arg>=0) 0x2020B8+=arg; 0x2020B4-=arg;
- 8: (0x2020B0==0, arg==8) 0x2020B0=1;
- 9: nop (used in process-input() to end the stream).
Note that the argument must fit in a signed long integer (4 bytes). Every operation here is rather simple, and detailed in the list above—with the exception of operation three, which calls its very own function.
Operation three
OP of three will call op-three(ARG)@0x0F2A. This in turn will call a function I’ve named E0-match(ARG)@0x0EB8.
E0-match will loop through (0x2020E0, IDX, 24) to check those integers against ARG. The sequence, where IDX = range(0, 6), is [0x01, 0x1E, 0x0A, 0x02, 0x2A, 0x4D] or [1, 30, 10, 2, 42, 77]. If there’s a match, the function will return a pointer to the matching integer.
Op-three will then take a weight associated with that argument and then add it to the tally. The weights are: [0x08, 0x03, 0x01, 0x01, 0x03E9, 0x0A]. Main() will then check this cost against $0x03E8, which means the item with argument 42 (the ACME anvil) is not purchasable.
Then:
- Op-three will obtain an integer at 0x10(E0MATCH_PTR);
- this integer is multiplied by the value at 0x2020BC;
- the result is added to 0x2020B4;
- 0x2020BC is set to 1.
This means that we can buy x amount of y with op4(x), op3(y). That’ll deplete it from 0x2020B4 (your wallet?) but it won’t add it to your spent total in 0x2020B8. So we still need to call op6. Here’s the list of prices: [0x020A44, 0x03E8, 0x2A, 0x01, 0x2FD12E, 0xC350]. Note that the ACME anvil costs exactly what you would need to spend your budget all on one thing. Too bad you can’t carry it.
Note: op-three will also call another function, LL16-append, similar to the LL8-append function used earlier. It might’ve been a red herring, because I couldn’t really figure out why it was needed. Regardless, we don’t need to understand that part to solve the challenge.
Main checks
Checks in main() beyond the regular sym.imp.error() checks include:
- 0x2020B0 != $0 (thus, must call op8 before we pay);
- 0x2020B1 != $0 (thus, complete the transaction with “9 10”);
- 0x2020B4 == $0 (thus, we must appropriately balance use of op3 and op6);
- 0x2020B8 == $0x2FD12E or $3133742 (thus, we must set it with op6);
- 0x2020C0 <= 0x03E8 or $1000 (thus, don’t buy the anvil)
Now we can name these variables appropriately. 0x2020B8 is the total, and 0x2020C0 is the suitcase weight.
Transaction
Just satisfying checks 1 and 2:
8 8
9 10
Some messing around with my CASIO calculator reveals that the total can be produced by buying:
- 23 of item A (ID 1),
- 1 of item F (ID 77),
- 8 of item B (ID 30),
- 15 of item C (ID 10), and
- 12 of item D (ID 2).
Now, let’s give names to the more annoying ops. op4 x op3 y
is “buy x of y,”
which will create debt in 0x2020B4; op6 x
is “pay,” which will settle that
debt by x and add to the spent amount in 0x2020B8. Time to make that
transaction:
4 23
3 1
4 1
3 77
4 8
3 30
4 15
3 10
4 12
3 2
8 8
6 3133742
9 10
That’s it!
Receipt
23 Plush Retriever
1 Link of Blockchain
8 Cruise Ship Ticket
15 Retriever Sticker
12 Authentic Meme
DogeCTF{Flag is different on the server.}