Flare-On 4 CTF write-up (part 2)

Flare-On 4 CTF write-up (part 2)

. 9 min read

Welcome back. This is the 2nd part of the 4-part write-up for Flareon 4 challenge. If you haven’t read the 1st part, you can find it here.

#5 – pewpewboat.exe

Challenge #5 is an x86_64 ELF although named as .exe. This is similar to those hidden ships game. Running on the terminal a pretty map is printed.

There is an 8×8 rectangular grid. Some of the cells have a ship hidden beneath it. The objective is to uncover all the ships in a limited number of moves. Lets’ try a coordinate say G1.

We failed at the first attempt. The map is static, with many more attempts it’s possible to uncover all the ships. When all ships are uncovered, the program prompts for an input – a hash value of sorts. Let’s have a look in IDA.

Preliminary Analysis

IDA identifies the main function correctly. The program initializes the random number generator via a call to srand . Below that we have a call to malloc requesting 576 bytes of memory. We will see later that these 576 bytes are used for storing the map.

We can use the decompiler for a better understanding of the code.

The printf function at line 45 prints the initial “Loading first pew pew map” message. The memcpy  call at line 49 copies 576 bytes to the map buffer allocated just before. If we navigate to map_data  we can see it is composed of seemingly randomly bytes and probably encrypted.

The for loop encompassing the decrypt_map  and play_map  call indicates that there may be 100 such levels to play, although we will see later this is not the case. Each level is stored encrypted. Since we can perform live debugging on the program there is no need to analyze the decryption routine. We just need to understand how a map is represented in memory.

The structure of the level map

Internally, the map for a level is represented by the following structure.

struct map { QWORD goal_state; QWORD current_state; QWORD next_dec_key; DWORD max_ammo; char last_move[2]; char rank[32]; char message[514]; };

The current and the goal states are stored as bit strings. There are 64 cells. Each cell can have only one of two states. If each cell is represented by a bit a total of 64 bits i.e. a quadword is enough to represent the complete map. We can debug the app for a better understanding. Let’s set a breakpoint before play_map is called to inspect its layout of the map. The pointer to the map is passed in rdi.

From the image, we can easily decipher the goal state for level 1 is the first 8-byte block. After taking endianness into consideration it comes out to be 00 08 08 78 08 08 78 00. The rightmost bit (LSB) represents cell A1, whereas the leftmost bit (MSB) represents cell H8. The bits are stored in a row major form.

Converting to a binary representation we obtain 00000000 00001000 00001000 01111000 00001000 00001000 01111000 00000000 .Let’s try to have a visual representation of this bit string.

from future import print_function pattern = format(0x0008087808087800, 'b').zfill(64) for i in xrange(len(pattern)-1, -1, -1): if (i+1)%8 == 0: print() if pattern[i] == '1': print('@', end='') else: print('-', end='') print()

We iterate over all of the 64 characters in the bit string and print ‘@’ if it contains 1. Running the code, we immediately get the position of the ships.

Our findings are in line with what we discovered earlier during playing the game.

Neutralizing the NotMd5Hash check

With the above information we can pass the level, however, at the end, the program prompts for a “not md5 hash”. This is more of an annoyance and does not have to do anything with the game. The notmd5hash  function is called whenever we complete a level successfully. Inspecting the function which performs this check we can see that our input is compared to an array computed from random numbers.

Going by the assumption that there is no way to predict those random numbers I decided to patch out the check. This is simple and can be done by simply inverting the jz instruction to jnz. However, there is an even better way by nopping out the call notmd5hash  instruction in the first place.

Building a semi-automated bot in radare

So far so good. We know how the map is stored and can construct the goal state from it. However, there were a lot of levels to play and I didn’t want to enter the coordinates manually. With information obtained from the static analysis I decided to code a semi automated solver using python radare.

We need to set a breakpoint on the play_map  call at 403EB4 in order to fetch the map passed in rdi  and thus the goal state.

The solver script is as follows:

!/usr/bin/env python import r2pipe import sys def get_ships(state): print 'Writing moves...' f = open('moves', 'w') for row in xrange(8): for col in xrange(8): bitmask = 1 << ((row * 8) + col) if state & bitmask != 0: f.write('%s%s' %(chr(65+row), chr(49+col))) f.write('\n') f.close() def main(): r2 = r2pipe.open('tcp://') # r2.cmd('aa') # r2.cmd('doo') """ .text:0000000000403EB1 mov rdi, rax .text:0000000000403EB4 call play_map .text:0000000000403EB9 mov [rbp+var_4C], eax .text:0000000000403EBC cmp [rbp+var_4C], 1 """ # Set breakpoint on play_map r2.cmd('db 0x403EB4') # Resume execution r2 = r2pipe.open('tcp://') r2.cmd('dc') while True: # Breakpoint hit, get address of map in rdi r2 = r2pipe.open('tcp://') map_addr = r2.cmdj('drj')['rdi'] # Get goal state r2 = r2pipe.open('tcp://') goal_state = r2.cmdj('pv8j @ %d' %map_addr)['value'] get_ships(goal_state) # Resume execution r2 = r2pipe.open('tcp://') r2.cmd('dc') if name == 'main': main()

Before running the script we need to disable ASLR which can be done using

echo 0 > /proc/sys/kernel/randomize_va_space

The script sets breakpoint before play_map is called. When the breakpoint is hit it fetches the goal state and writes it to a file named moves. We can copy the moves and paste them in the terminal to pass the level. A short demo is provided below.

Getting the flag

After passing all the levels a message is displayed.

Replacing all occurrences of the string PEW with a space we get:

Aye! You found some letters did ya? To find what you’re looking for, you’ll want to re-order them: 9, 1, 2, 7, 3, 5, 6, 5, 8, 0, 2, 3, 5, 6, 1, 4. Next you let 13 ROT in the sea! THE FINAL SECRET CAN BE FOUND WITH ONLY THE UPPER CASE.

If we look back, we can see the position of the ships represented some english alphabet. The first level resembled “F”.  The complete list of characters is “F, H, G, U, Z, R, E, J, V, O”. The message wants us to rearrange the letters and ROT-13 the result. Re-ordering the letters gives OHGJURERVFGUREHZ. Performing a ROT-13 on this results in the string BUTWHEREISTHERUM.

However, this is not the flag for sure. Going back to IDA and inspecting the function which takes the move as input we can see fgets  is called with the 17 as an argument. Discarding the line terminator, it means our input can be upto 16 characters long,

Co-incidentally the string BUTWHEREISTHERUM is also 16 in length. Restarting the game, and providing this as a coordinate prints the flag.

The flag is y0u__sUnK_mY__P3Wp3w_b04t@flare-on.com .

#6 – payload.dll

Our next challenge is a PE32+ dll. There’s only a single exported function named EntryPoint (note that this is not the DllEntryPoint )

Judging from the disassembly, this function should pop out MessageBox when called. However, nothing like that happens when calling this export via rundll32.

This is strange. There’s is a exported function but we cannot call it by its name. On loading a dll the first piece of code that gets executed is DllMain . It’s possible that DllMain  is modifying the export table so by the time we’re ready to call the exported function named EntryPoint it no longer exists.

Dumping the dll at runtime

To test the hypothesis, I loaded payload.dll using rundll32 just as instructed. Before dismissing the message box I used Scylla to dump it from memory.

Inspecting the export table of the dumped dll confirms our hypothesis. The said export function no longer exists. Instead there is a new name “orphanedirreproducibleconfidences”. The internal name of the dll also has changed to “lustrated.dll”.

Lets try to call the new exported function “orphanedirreproducibleconfidences” with rundll32.

A message box pops out revealing a part of the key which looks to be the flag. However, this is only just a single letter what about the others.

Getting the remaining parts of the flag

The PE header is normally read-only at run-time. To modify the header the application at some point must make it writable using one of the VirtualProtect  functions.  Searching for cross references leads us to the location where it makes a call to VirtualProtectEx  to make the PE header writable.

Just below it, func_to_decrypt  calculates the value of (year + month) modulo 26.

The returned integer in eax  is passed to function decrypt . The exported function in the dumped dll takes a different name for each of the 26 possible in (year + month) % 26. To get all the possible names of the exported functions, I wrote a small script in x64dbg.

ctr = 0 @run: init "C:\Documents and Settings\Administrator\Desktop\payload.dll" doSleep 100 run 180005DDD mov eax, ctr run 180005D24 log ========================================================= log {d:ctr} log dll: {s:rdx+32} find rdx+33, 00 log function: {s:$result+1} log ========================================================= stop ctr = ctr + 1 cmp ctr, 1a jl @run

Running this we get the full list of the exported function for every value in (year+month) % 26. The table is given at the end. Finally, an ugly batch script to launch rundll32 with the export names.

@echo off setlocal enabledelayedexpansion set fn[0]=filingmeteorsgeminately set fn[1]=leggykickedflutters set fn[2]=incalculabilitycombustionsolvency set fn[3]=crappingrewardsanctity set fn[4]=evolvablepollutantgavial set fn[5]=ammoniatesignifiesshampoo set fn[6]=majesticallyunmarredcoagulate set fn[7]=roommatedecapitateavoider set fn[8]=fiendishlylicentiouslycolouristic set fn[9]=sororityfoxyboatbill set fn[10]=dissimilitudeaggregativewracks set fn[11]=allophoneobservesbashfulness set fn[12]=incuriousfatherlinessmisanthropically set fn[13]=screensassonantprofessionalisms set fn[14]=religionistmightplaythings set fn[15]=airglowexactlyviscount set fn[16]=thonggeotropicermines set fn[17]=gladdingcocottekilotons set fn[18]=diagrammaticallyhotfootsid set fn[19]=corkerlettermenheraldically set fn[20]=ulnacontemptuouscaps set fn[21]=impureinternationalisedlaureates set fn[22]=anarchisticbuttonedexhibitionistic set fn[23]=tantalitemimicryslatted set fn[24]=basophileslapsscrapping set fn[25]=orphanedirreproducibleconfidences for /l %%n in (0,1,25) do ( set /a year=2001+%%n date 1-2-!year! rundll32 payload.dll !fn[%%n]! !fn[%%n]! )

For each of the 26 exported names, a letter of the flag is displayed in the message box. The dll names although provided in the table below had no real use.


(y+m)%26DLL nameExported function nameFlag character

Combining the characters we get the flag wuuut-exp0rts@flare-on.com .

Stay tuned for the rest of the challenge levels walkthrough.