pixel poker

In Pixel Poker, I’ll reverse engineer a Windows Direct-X 11 application using both Ghidra and x32dbg to find the correct pixel to click on. On clicking, it returns a meme and the flag.


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?

The download contains a 32-bit Windows exe file:

$ file PixelPoker.exe 
PixelPoker.exe: PE32 executable (GUI) Intel 80386, for MS Windows

Run It

In a Windows VM, I’ll run it, and it opens a window with a title bar, a menu bar, and a main canvas with a bunch of pixels:


Under File, there’s an option of “I Give Up”, which exits the game. Under Help, there’s “How To …” which opens a pop up:


Moving the mouse around the canvas, the pixel location is updated as (horizontal position, vertical position). Each time I click in the canvas, the counter at the top advances:


If I click after it gets to 10/10, it pops a message:


Clicking OK exits the game.


Identify Directx 11

I’ll start by finding the “Womp womp” string, which actually shows up in two functions:


Stepping up the call tree, the second is actually two calls down from the first. A few calls up, I’ll find this function at 0x401120:

void __cdecl initWinClass(HINSTANCE param_1)

  winClass.cbSize = 0x30;
  winClass.style = 3;
  winClass.lpfnWndProc = WndProc;
  winClass.cbClsExtra = 0;
  winClass.cbWndExtra = 0;
  winClass.hInstance = param_1;
  winClass.hIcon = LoadIconW(param_1,(LPCWSTR)0x6b);
  winClass.hCursor = LoadCursorW((HINSTANCE)0x0,(LPCWSTR)0x7f00);
  winClass.hbrBackground = (HBRUSH)0x6;
  winClass.lpszMenuName = (LPCWSTR)0x6d;
  winClass.lpszClassName = (LPCWSTR)&lpBuffer_004131b0;
  winClass.hIconSm = LoadIconW(winClass.hInstance,(LPCWSTR)0x6c);

I’ve renamed the function and the winClass object, WndProc, and the function itself, but the rest is already named.

In a strange case of beneficial typos, I googled “WINDCLASSEXW” (with an extra “I” in “WIND”), and got this article (that makes the same typo) titled “Using DirectX11 for games: Part 1”. It was a very lucky find, because it walks through the structure of a DirectX11 application, which is what this game is.

For example, from the post:


This looks very much like what I have above.


After the initWinClass call, the hInstance object is passed to a function I named CreateWindow (0x401040). This function gets a buffer the size of size DAT_00413284 * DAT_00413280 * 4. Given that colors can be stored in 4-bytes per pixel, and I know we’re looking at a grid of pixels, it makes sense to think these are the MAX_X and MAX_Y values.


In that initWinClass function, it sets the winClass.lpfnWndProc to the address of a function at 0x4012c0 that I’ll rename to WinProc to match what’s in the post above.

From the post:

We’re also passing a pointer to a function called WndProc seen in the line winClass.lpfnWndProc = &WndProc;


WndProc Callback handles out input events

This is the callback that handles events sent to our app like resizing, keyboard input, and exiting our program. We have to define it ourselves, so we’ll do that above our main function.

The example in the post shows this function taking four parameters, and then doing a switch on msg to handle different kinds of events:

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
    LRESULT result = 0;
    switch(msg) {
        case WM_KEYDOWN: {
            if(wparam == VK_ESCAPE)
        case WM_DESTROY: {
        } break;
            result = DefWindowProcW(hwnd, msg, wparam, lparam);
    return result;

The WndProc in PixelPoker.exe looks similar. I’ll rename/retype the variables to match. This one doesn’t decompile to a switch, but a bunch of if / else. I’ll use Equates in Ghidra to set the constants to names, and towards the bottom I’ll find the code for msg == WM_LBUTTONDOWN (for left button down):

    if (msg == WM_LBUTTONDOWN) {
      xcoord = (uint)(short)lparam;
      ycoord = (uint)(short)((uint)lparam >> 0x10);
      if (CLICK_COUNTER == 10) {
        MessageBoxA((HWND)0x0,"Womp womp... :(","Please play again!",0);
      else {
        if ((xcoord == s_FLARE-On_00412004._0_4_ % (uint)MAX_X) &&
           (ycoord == s_FLARE-On_00412004._4_4_ % (uint)MAX_Y)) {
          if (0 < MAX_Y) {
            CVar5 = 0;
            iVar3 = MAX_X;
            iVar4 = MAX_Y;
            do {
              ycoord = 0;
              if (0 < iVar3) {
                do {
                  ycoord = ycoord + 1;
                  iVar3 = MAX_X;
                  iVar4 = MAX_Y;
                } while ((int)ycoord < MAX_X);
              CVar5 = CVar5 + 1;
            } while ((int)CVar5 < iVar4);
        else if ((xcoord < (uint)MAX_X) && (ycoord < (uint)MAX_Y)) {

The docs on WM_LBUTTONDOWN show that this is getting the x and y coordinate from the lparam, and checking if the global I’ve named CLICK_COUNTER is equal to 10 (if so, print “Womp womp” and DestroyWindow). Otherwise, it increments CLICK_COUNTER and then checks if x and y are some specific values:


s_FLARE-On_00412004 is a global, named by Ghidra, that’s initialized to “FLARE-On”. If I can figure out what MAX_X and MAX_Y are, I’ll know the pixel to click.


I’ll open x32dbg and set a breakpoint at 0x401477, where it is loading “FLAR” into EAX to compare. I’ll run until the program is running, and then click somewhere in the pixels. The breakpoint is hit:

image-20221003084103345Click for full size image

Stepping forward two to 0x401482, it’s loaded 0x52414C46 (“RALF”) into EAX, and 0x2e5 into ESI. 0x2e5 = 741 must be MAX_X, which makes sense as when I move my cursor to the far left hand side of the image, the title bar goes up to 740 as the first coordinate:


Stepping once more, the div instruction stores the result of the division in EAX, with the remainder in EDX, which is what I need. That reads 0x5F, or 95.

Because I just clicked on a random pixel, this check fails. I’ll click again, this time making sure the x coordinate is 95.

This time the jump is not taken, and I get to where “nO-E” is loaded into EAX, and divided by 0x281, or 641. The remainder this time is 0x139, or 313.

When I click on pixel 95, 313, the flag is shown:


Flag: w1nN3r_W!NneR_cHick3n_d1nNer@flare-on.com