Flare-On 2017 Write-up: “pewpewboat.exe”

Flare-On 2017 Challenge #5 — pewpewboat.exe

As usual, the first thing to do when tackling the challenge is to run the binary first, to see what it does. You will soon learn that it’s not actually a Windows executable, but rather a 64-bit Linux ELF.

``````\$ ./pewpewboat.exe
1 2 3 4 5 6 7 8
_________________
A |_|_|_|_|_|_|_|_|
B |_|_|_|_|_|_|_|_|
C |_|_|_|_|_|_|_|_|
D |_|_|_|_|_|_|_|_|
E |_|_|_|_|_|_|_|_|
F |_|_|_|_|_|_|_|_|
G |_|_|_|_|_|_|_|_|
H |_|_|_|_|_|_|_|_|

Rank: Seaman Recruit

Welcome to pewpewboat! We just loaded a pew pew map, start shootin'!

Enter a coordinate:
``````

So this is a Battleship game. Playing manually for a bit, I see the “ships” form up in the shape what looked like a letter. Hmm could this be the flag?

It’s now time to read the code.

Static Analysis

Looking at the `main` function, the following (cleaned up and renamed) decompiled output is obtained:

```v3 = time(0LL);
srand(v3);
gameState = malloc(576uLL);
v9  = 'L';
v10 = 'o';
v11 = 'a';
v12 = 'd';
.... (snip) ....
v36 = '.';
v37 = '\n';
v38 = 0;
printf(format, &v9);

seed = genInitialSeed();
for ( i = 0; i <= 99; ++i )
{
memcpy(gameState, (char *)&magicTable + 576 * i, 576uLL);
decryptGameData((char *)gameState, 576u, seed);
if ( (unsigned int)mainProgLoop((__int64)gameState) != 1 )
break;
seed = *((_QWORD *)gameState + 2);          // next seed comes from solve gameState
}
free(gameState);
```

Just a quick note that `v9`, `v10` till `v38` are actually part of a `char[]` string, but IDA Pro doesn’t know about it.

The function `genInitialSeed (0x403C85)` generates the initial seed that is used to “decrypt” the game data. I’ll leave you to read it, but it essentially MD5s the string `"loading... %d%%"` in a loop, incrementing `%d` each time.

You will then see a for-loop of 99 levels, with each iteration copying the game data from a table of 576 bytes, then decrypting it with `seed`:

```memcpy(gameState, &magicTable + 576 * i, 576uLL);
decryptGameData(gameState, 576u, seed);
if ( mainProgLoop(gameState) != 1 )
break;
seed = *(gameState + 2);  // next seed comes from solved gameState
```

Control then goes to the `mainProgLoop`, which prompts for user input performs hit/miss checking. After that round is complete, `seed` will be set to the solved state, ready to decrypt the subsequent round.

Main Loop

Let’s now look at the `mainProgLoop (0x403C05)` function, which is where the program reads user input and figures out if it’s a hit or miss.

```prevInput = 0LL;
v2 = 0;
for ( i = 0; i <= 63; ++i ) {
clearScreen();
drawGrid(state);
v2 = process((void *)state, prevInput);
if ( (signed int)v2 > 0 )
break;

prevInput = *(_QWORD *)(state + 8);
getInput(state);
}
return v2;
```

From the `drawGrid (0x403263)` function, we can see that our entered coordinates and the actual position of the “ships” are both stored in the `gameState`. This code will check the correct ship coordinates against the user’s entered coordinates, and only if there is a match, an X is drawn.

```printf(format, &colHeader);
for ( row = 0; row <= 7; ++row ) {
printf("%c ", (row + 'A'));
for ( col = 0; col <= 7; ++col ) {
putchar('|');
mask = 1LL << (8 * row + col);
if ( mask & *(gameState + 8) && mask & *gameState )
printf(format, &v5);
else if ( col <= 7 )
putchar('_');
}
puts("|");
}
```

The `mask` is generated by `1 << 8 * row + col`, meaning `A1` would be `0x1` and `A2` is `0x2`, `B1` is `0x100`, etc. The 64-bit integer stored at `gameState + 0` represents all the actual ship positions and the user’s input so far is stored in `gameState + 8`, also 64-bit. Therefore, a set bit in this mask represents a ship at the corresponding row/col position.

We move on to the `process (0x04038D6)` function, which processes input from the user, checks for hits and advance the game state as necessary. In this function, you will find references to more stuff inside the game state, such as your rank (e.g. “Seaman Recruit”):

```v7 = 'R';
v8 = 'a';
v9 = 'n';
v10 = 'k';
v11 = ':';
v12 = ' ';
v13 = '%';
v14 = 's';
v15 = '\n';
v16 = '\n';
v17 = 0;
printf(&v7, gameState + 30);
```

Also, you will find the “level welcome” message at `gameState + 62`. In the first level, this string reads “Welcome to pewpewboat! We just loaded a pew pew map, start shootin’!”.

Lastly, we have come to the `getInput (0x40377D)` function, called at the end of the `mainProgLoop`:

```if ( fgets(input, 17, stdin) ) {
c = (char)(input[0] & 0xDF) - 'A';
n = input[1] - '1';
if ( c < 0 || c > 7 || n < 0 || n > 7 ) {
sub_403411(input);
} else {
*(_QWORD *)(gameState + 8) |= 1LL << (8 * c + n); // forms a bitmask
*(_BYTE *) (gameState + 28) = input[0] & 0xDF;    // input coord letter
*(_BYTE *) (gameState + 29) = input[1];           // input coord num
}
}
```

From the code, you can see a check to make sure that letter `c` and number `n` are within range (0 – 7). Once that happens, the `mask` will be created, same as above, and stored into `gameState + 8`. The corresponding letter and number are placed into `gameState + 28` and `+29`, respectively.

Curiously, if the `c` and `n` checks are out of range, it calls another function, `sub_403411`. What does this function do? For now, it doesn’t look too important, so let’s move on first.

So once we complete all levels we should get the flag… or will we?

Solving Game Levels

If there is going to be 99 levels as the for-loop suggests, I will not sit down and play each level by hand. We need a smarter solution.

I have been using Frida for some time now. If you haven’t heard, it is basically a dynamic instrumentation framework, allowing you to modify code and data of a process programmatically. It can be controlled via a Javascript script, and has a pretty powerful API.

We can simulate user input by simply patching the part where `fgets` is called, in this case the `getInput` function. We shall simulate whatever `getInput` does by writing to the same locations in the `gameState` and return. We can get the correct “ship” coordinates from the 64-bit integer mask at `gameState + 0`.

```// patch getInput
Interceptor.replace(getInput, new NativeCallback(function(state) {
// iterate through all set positions, then "input" them
positions = positions.shr(coord);
while (positions.compare(0) != 0) {
if (positions.and(1).compare(1) == 0)
break;
positions = positions.shr(1);
coord++;
}

var newbit = uint64(1).shl(coord);

// coordinates
var c = 'A'.charCodeAt(0) + (coord >> 3);
var n = '1'.charCodeAt(0) + (coord  & 7);
if (debug) console.log('guess: ' + String.fromCharCode(c) + String.fromCharCode(n));

coord++;
if (coord > 077) {
coord = 0;
}

return 0;
}, 'int', ['pointer']));
```

After solving one level, you will find that you are prompted for this:

```    NotMd5Hash("AJEE") >
```

This prompt basically tries to slow you down by asking you to compute an MD5 hash of the given string. Let’s patch that out as well, and while we are at it, the `clearScreen` function as well:

```var clearScreen =  ptr(0x4031E1);
var notMd5Prompt = ptr(0x403530);

Interceptor.replace(clearScreen,  new NativeCallback(function() {}, 'int', []));
Interceptor.replace(notMd5Prompt, new NativeCallback(function() {}, 'int', []));
```

And as we expected, the “ship” positions do indeed form a letter. Let’s add a function that helps to print all these letters. There is also bound to be something in the `gameState` messages, so let’s dump the entire `gameState` for each level as well.

```Interceptor.attach(mainProgLoop, {
onEnter: function(args) {
coord = 0;

var state = args[0];
console.log('level ' + level + ' state: ' + gameState.toString(16));
console.log(drawMap(gameState));

var f = new File('pew-dump-' + level + '.bin', 'wb');
f.write(data);
f.close();
level++;
},
});
```

When the script is pieced together, running `frida` will automatically solve all levels:

`````` frida -l pew.js -f pewpewboat.exe -o pew.log --no-pause
``````

As expected, the final message on level 10 reads:

Rank: Congratulation!

Aye!PEWYouPEWfoundPEWsomePEWlettersPEWdidPEWya?PEWToPEWfindPEWwhatPEWyou’rePEWlookingPEWfor,PEWyou’llPEWwantPEWtoPEWre-orderPEWthem:PEW9,PEW1,PEW2,PEW7,PEW3,PEW5,PEW6,PEW5,PEW8,PEW0,PEW2,PEW3,PEW5,PEW6,PEW1,PEW4.PEWNextPEWyouPEWletPEW13PEWROTPEWinPEWthePEWsea!PEWTHEPEWFINALPEWSECRETPEWCANPEWBEPEWFOUNDPEWWITHPEWONLYPEWTHEPEWUPPERPEWCASE.

Thanks for playing!

When you read “between the lines”, the message is:

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.

This is where the printed maps come in handy:

``````level 0 state       level 1 state
. . . . . . . .     . . . . . . . .
. . . @ @ @ @ .     . . . @ . . . @
. . . @ . . . .     . . . @ . . . @
. . . @ . . . .     . . . @ . . . @
. . . @ @ @ @ .     . . . @ @ @ @ @      o  o  o
. . . @ . . . .     . . . @ . . . @
. . . @ . . . .     . . . @ . . . @
. . . . . . . .     . . . . . . . .
``````

Pieceing the letters in the specified order gives you: `ohGJuRERvFGuREHz`.

Transforming the string using ROT-13 then gets you: `buTWhEREiSThERUm`.

But this doesn’t look like a flag (even with uppercase)!

The Last Bit

If you recall, the `getInput` function had a check for valid input, but called `sub_403411` if it failed:

```if ( fgets(input, 17, stdin) ) {
c = (char)(input[0] & 0xDF) - 'A';
n = input[1] - '1';
if ( c < 0 || c > 7 || n < 0 || n > 7 ) {
sub_403411(input);
} else ...
```

The string matches the condition and it is exactly 16 characters (+ `NULL` = 17). Run the game once again and provide this string when asked for the coordinates:

``````Welcome to pewpewboat! We just loaded a pew pew map, start shootin'!

Enter a coordinate: BUTWHEREISTHERUM
very nicely done!  here have this key:  y0u__sUnK_mY__P3Wp3w_b04t@flare-on.com
``````

As usual, you can download my `pew.js` Frida script here.