Flare On 9 Writeup

Here’s my write up of the first challenge of the Flare-On 9 CTF, and a log of my attempts for the second challenge.

I’ve managed to complete just the first challenge, which was kinda simple. I tried quite a bit with the second one but I stuck to a static analysis approach, which probably required more than my current skills in order to be successful. In any case, it was very interesting and I learned a bit more about Windows binaries

Warning: I called them write-ups, but they are more like notes taken wrongly. I apologize for any errors / BS that I have written.

Challenge 01

Let’s start, here’s the text:

01 - Flaredle 1

Welcome to Flare-On 9!

You probably won't win. Maybe you're like us and spent the year playing Wordle.
We made our own version that is too hard to beat without cheating.

Play it live at: http://flare-on.com/flaredle/

7-zip password: flare

The website is a variant of the game wordle with 21 letters. First of all we take a look at script.js

import { WORDS } from "./words.js";

const NUMBER_OF_GUESSES = 6;
const WORD_LENGTH = 21;
const CORRECT_GUESS = 57;
let guessesRemaining = NUMBER_OF_GUESSES;
let currentGuess = [];
let nextLetter = 0;
let rightGuessString = WORDS[CORRECT_GUESS];

After those global variable declarations we have some functions:

  • initBoard, that draws the board;
  • shadeKeyBoard, that highlights with the various colors the on screen keyboard,
  • deleteLetter, the function name explains itself,
  • checkGuess, that checks if the inserted word is the guess,
  • insertLetter
  • animateCSS

in checkGuess we see that it checks if the guess is equal to rightGuessString, which we already saw in the first section of the code.

In order to discover the right guess string we just need to go to words.js and take the element in the position number 57, which is flareonisallaboutcats.

If we enter the string in the site, a popup tells us that the flag is flareonisallaboutcats@flare-on.com. Let’s go!

Challenge 02

02 - Pixel Poker 1

I said you wouldn't win that last one. I lied. The last challenge was basically a captcha. Now the real work begins. Shall we play another game?

7-zip password: flare

Into the archive there is an executable, PixelPoker.exe, and readme.txt. The content of the latter is the following:

Welcome to PixelPoker ^_^, the pixel game that's sweeping the nation!

Your goal is simple: find the correct pixel and click it

Good luck!

If I try to execute PixelPoker.exe (with wine) a GUI with a bunch of colored pixels appears. Apparently the goal is to click the right pixel, a impossible thing to do manually.

The file is a PE32 executable (GUI) for Intel 80386 for MS Windows (as we already knew).

First of all I tried running strings on the binary but the output is nothing but rubbish.

Then I tried to open it in ghidra, but I couldn’t understand a thing. At this point I decided that maybe I needed something more windows-specific. After a little search I found the existence of FlareVM, a “fully customizable, Windows-based security distribution for malware analysis, incident response, penetration testing, etc.” Seems the right thing, right?

To be honest, I haven’t used any of the tools on the VM properly, but with pestudio I took a look at the imports of the executable, and I saw that it uses 3 differents DLLs: KERNEL32.dll, USER32.dll and, most importantly, GDI32.dll. After a bit of searching I discovered that GDI, Graphics Device Interface is a windows api used for the rendering of graphical interfaces.

By looking to the Xrefs of this DLL (in radare2 and then in ghidra) I think I found the function that initializes the GUI: FUN_004012c0. I proceded to rename it to initialize_gui.

After looking at the GDI Doc I tried to think what the variables could be used for. I found the handle of the window, the check of the remaining guesses and some other stuff.

At a certain point I took a look at the xrefs to initialize_gui: there’s just one call from FUN_00401120. This function initializes a struct, called Window Class in the docs, and it sets the field lpfnWndProc as a pointer to initialize_gui. Searching lpfnWndProc in the doc gives us this kind of tutorial that says

lpfnWndProc is a pointer to an application-defined function called the window procedure or “window proc.” The window procedure defines most of the behavior of the window. We’ll examine the window procedure in detail later. For now, just treat this as a forward reference.

Ok, so let’s rename initialize_gui to window_proc. Indeed, we were right! that function is a sort of main for the GUI behavior.

Now stuff are a bit more clear. in this tutorial is written that the signature of the window procedure is

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

and also that

There are four parameters: hwnd is a handle to the window. uMsg is the message code; for example, the WM_SIZE message indicates the window was resized. wParam and lParam contain additional data that pertains to the message. The exact meaning depends on the message code.

The guess check is in a branch that starts by verifying that uMsg is >= 274 and then that is equal to 513. What does that means?

        guess_counter = guess_counter + 1;
        if ((copy_lParam == s_FLARE-On_00412004._0_4_ % DAT_00413280) &&
           (uVar5 == s_FLARE-On_00412004._4_4_ % DAT_00413284)) {

At this page in the doc is listed every message code. We just need to find 513 or 0x201. I don’t want to search every voice, so i found this table from wine wiki.

0x201 is… WM_LBUTTONDOWN, the left click! It does make sense! When the left click is pressed, the execution follows this branch.

At this point the guess counter is incremented. If it reaches the limit (10), game over. But if it doesn’t, it checks copy_lParam, which is the cast of lParam into an unsigned short, and uVar5, which I haven’t renamed yet but is the unsigned cast of what ghidra calls sVar1 which is sVar1 = (short)((uint)lParam >> 0x10), lParam right shifted by 16 positions. I don’t realy know what is this, but i presume that in this branch the program needs to check if the pixel clicked is the right one. Could lParam contain the coordinates of the clicked pixel? the doc said that “wParam and lParam contain additional data that pertains to the message. The exact meaning depends on the message code”. so that’s an option for sure.

The documentation for WM_LBUTTONDOWN says that wParam Indicates whether various virtual keys are down (we don’t really care), but about lParam “The low-order word specifies the x-coordinate of the cursor. The coordinate is relative to the upper-left corner of the client area. The high-order word specifies the y-coordinate of the cursor. The coordinate is relative to the upper-left corner of the client area.”. BINGO! now we just need to understand what the following section of code does.

        if ((x_coord == s_FLARE-On_00412004._0_4_ % DAT_00413280) &&
           (y_coord == s_FLARE-On_00412004._4_4_ % DAT_00413284)) {
          if (0 < (int)DAT_00413284) {
            iVar6 = 0;
            uVar3 = DAT_00413280;
            uVar4 = DAT_00413284;
            do {
              iVar5 = 0;
              if (0 < (int)uVar3) {
                do {
                  FUN_004015d0(iVar5,iVar6);
                  iVar5 = iVar5 + 1;
                  uVar3 = DAT_00413280;
                  uVar4 = DAT_00413284;
                } while (iVar5 < (int)DAT_00413280);
              }
              iVar6 = iVar6 + 1;
            } while (iVar6 < (int)uVar4);
          }
        }
        else if ((x_coord < DAT_00413280) && (y_coord < DAT_00413284)) {
          FUN_004015d0(x_coord,y_coord);
        }

After this point.. I just gave up. s_FLARE-On_00412004 pointed to an unused region of the memory, which I assumed was dynamically allocated.

Today, after two weeks, the challenge is over. I took a quick look at some writeups (this one, by Ghetto Forensics and this one, made by the CTF organizers) and I’m glad to understand that I wasn’t totally, absolutely wrong. In the next few days I will try to look at the writeups better and try to replicate, but I wanted to publish this post as soon as possible before I completely forget about the challenge.