FLARE-ON 2022 writeup

Writer:b1uef0x / Webページ建造途中

概要

昨年と異なり全て純粋なリバース問題だった。5問目まで到達したが、5問目のT8は解くことができなかった。

目次

01 - Flaredle

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/

Flaredle実行画面

Wordleを模したWebアプリを解析する問題。読み込まれているscript.jsの先頭に正解のWORDの番号が記載されている。

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];

WORDSはwords.jsから読み込まれており、57番目が正解。

flareonisallaboutcats@flare-on.com

02 - Pixel Poker

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?

Pixel Poker

Windowsアプリ解析問題。画面の座標を10回クリックできる。11回目に出てくる失敗メッセージ「Womp womp...」について、Ghidraを使ってコードを検索するとFUN_004012c0が当たる。

LRESULT FUN_004012c0(HWND param_1,uint param_2,uint param_3,LPARAM param_4)

{
  HDC hdc;
  HANDLE hHeap;
  LRESULT LVar1;
  int iVar2;
  int iVar3;
  uint uVar4;
  uint uVar5;
  COLORREF CVar6;
  DWORD dwFlags;
  LPVOID lpMem;
  int local_148 [65];
  PAINTSTRUCT local_44;
  
  FUN_00401870(local_148,0,0x104);
  if (param_2 < 0x112) {
    if (param_2 == 0x111) {
      if ((param_3 & 0xffff) == 0x68) {
        DialogBoxParamW(DAT_004130e0,(LPCWSTR)0x67,param_1,FUN_00401000,0);
        return 0;
      }
      if ((param_3 & 0xffff) == 0x69) {
        DestroyWindow(param_1);
        return 0;
      }
      LVar1 = DefWindowProcW(param_1,0x111,param_3,param_4);
      return LVar1;
    }
    if (param_2 == 2) {
      DeleteDC(hdcSrc_00413278);
      if (DAT_00413294 != (LPVOID)0x0) {
        dwFlags = 0;
        lpMem = DAT_00413294;
        hHeap = GetProcessHeap();
        HeapFree(hHeap,dwFlags,lpMem);
      }
      PostQuitMessage(0);
      return 0;
    }
    if (param_2 == 0xf) {
      hdc = BeginPaint(param_1,(LPPAINTSTRUCT)&local_44);
      if (DAT_00412000 != '\0') {
        hdcSrc_00413278 = CreateCompatibleDC(hdc);
        SelectObject(hdcSrc_00413278,h_004130e4);
        DAT_00412000 = '\0';
      }
      BitBlt(hdc,0,0,cx_00413280,cy_00413284,hdcSrc_00413278,0,0,0xcc0020);
      EndPaint(param_1,&local_44);
      return 0;
    }
  }
  else {
    if (param_2 == 0x200) {
      FUN_004017be((char *)local_148,0x104,(byte *)"PixelPoker (%d,%d) - #%d/%d");
      SetWindowTextA(param_1,(LPCSTR)local_148);
      return 0;
    }
    if (param_2 == 0x201) {
      uVar5 = SEXT24((short)param_4);
      uVar4 = SEXT24((short)((uint)param_4 >> 0x10));
      if (DAT_00413298 == 10) {
        MessageBoxA((HWND)0x0,"Womp womp... :(","Please play again!",0);
        DestroyWindow(param_1);
      }
      else {
        DAT_00413298 = DAT_00413298 + 1;
        if ((uVar5 == s_FLARE-On_00412004._0_4_ % cx_00413280) &&
           (uVar4 == s_FLARE-On_00412004._4_4_ % cy_00413284)) {
          if (0 < cy_00413284) {
            CVar6 = 0;
            iVar2 = cx_00413280;
            iVar3 = cy_00413284;
            do {
              uVar4 = 0;
              if (0 < iVar2) {
                do {
                  FUN_004015d0(uVar4,CVar6);
                  uVar4 = uVar4 + 1;
                  iVar2 = cx_00413280;
                  iVar3 = cy_00413284;
                } while ((int)uVar4 < cx_00413280);
              }
              CVar6 = CVar6 + 1;
            } while ((int)CVar6 < iVar3);
          }
        }
        else {
          if ((uVar5 < (uint)cx_00413280) && (uVar4 < (uint)cy_00413284)) {
            FUN_004015d0(uVar5,uVar4);
          }
        }
      }
      FUN_004017be((char *)local_148,0x104,(byte *)"PixelPoker (%d,%d) - #%d/%d");
      SetWindowTextA(param_1,(LPCSTR)local_148);
      hdc = GetDC(param_1);
      BitBlt(hdc,0,0,cx_00413280,cy_00413284,hdcSrc_00413278,0,0,0xcc0020);
      ReleaseDC(param_1,hdc);
      return 0;
    }
  }
  LVar1 = DefWindowProcW(param_1,param_2,param_3,param_4);
  return LVar1;
}

x32dbgでプログラムをデバッグしてこの関数を追ってみると、次の部分でクリック座標を取得している。

uVar5 = SEXT24((short)param_4);
uVar4 = SEXT24((short)((uint)param_4 >> 0x10));

「Womp womp...」を表示させるIF文のところはクリック回数の累積で、その次に座標を判定する部分がある。取得した座標が入っているuVar5とuVar4を比較しているのは次の部分。

if ((uVar5 == s_FLARE-On_00412004._0_4_ % cx_00413280) &&
(uVar4 == s_FLARE-On_00412004._4_4_ % cy_00413284)) {

x32dbgでこの部分を確認するとx座標が0x5F、y座標が0x139であったので、(95,313)の座標をクリックしてみる。

Pixel Pokerクリア画面

flagが表示された。

w1nN3r_W!NneR_cHick3n_d1nNer@flare-on.com

03 - Magic 8 Ball

You got a question? Ask the 8 ball!

Magic 8 Ball

Windowsアプリ解析問題。文字列を入力するとボールの中にランダムに文字列が返ってくるほか、ボールを方向キーで揺らすことができる。

一体何をしているのかわからないので、Ghidraに読み込ませて処理を追ってみる。まず気になった関数はFUN_00402090。

FUN_00402090(void *this,undefined4 *param_1,undefined4 param_2,undefined4 param_3,undefined4 param_4
            ,undefined4 param_5)

{
  int iVar1;
  int *this_00;
  basic_ostream<char,struct_std::char_traits<char>_> *pbVar2;
  char *pcVar3;
  undefined4 uVar4;
  undefined4 uVar5;
  void **this_01;
  code *pcVar6;
  
  iVar1 = func_0x0040291f(0x20);
  if (iVar1 == 0) {
    iVar1 = IMG_Init(2);
    if (iVar1 != -1) {
      iVar1 = TTF_Init();
      if (iVar1 != -1) {
        if (0xf < (uint)param_1[5]) {
          param_1 = (undefined4 *)*param_1;
        }
        iVar1 = SDL_CreateWindow(param_1,param_2,param_3,param_4,param_5,0);
        *(int *)((int)this + 4) = iVar1;
        if (iVar1 != 0) {
          iVar1 = SDL_CreateRenderer(iVar1,0xffffffff,0);
          *(int *)((int)this + 8) = iVar1;
          if (iVar1 != 0) {
            SDL_SetRenderDrawColor(iVar1,0x32,0x32,0x96,0xff);
            *(undefined2 *)this = 1;
            *(undefined4 *)((int)this + 0x164) = 0;
            FUN_004018f0((void *)((int)this + 0x110),&DAT_0040426c,(int *)0x0);
            *(undefined *)((int)this + 0x159) = 0;
            FUN_004018f0((void *)((int)this + 0x128),&DAT_0040426c,(int *)0x0);
            *(undefined4 *)((int)this + 0x5c) = 0x6d6d6967;
            *(undefined4 *)((int)this + 0x60) = 0x6c662065;
            *(undefined4 *)((int)this + 100) = 0x70206761;
            *(undefined4 *)((int)this + 0x68) = 0x3f736c;
            *(undefined4 *)((int)this + 0x168) = 0xffffff;
            uVar4 = TTF_OpenFont("assets/OpenSans_regular.ttf",0xc);
            *(undefined4 *)((int)this + 0x16c) = uVar4;
            uVar4 = IMG_Load("assets/ball_paint.png");
            uVar5 = SDL_CreateTextureFromSurface(*(undefined4 *)((int)this + 8),uVar4);
            *(undefined4 *)((int)this + 0x6c) = uVar5;
            SDL_FreeSurface(uVar4);
            uVar4 = TTF_OpenFont("assets/NotoSans_Regular.ttf",0xc);
            *(undefined4 *)((int)this + 0x170) = uVar4;
            uVar4 = TTF_RenderText_Solid
                              (uVar4,"Press arrow keys to shake the ball",
                               *(undefined4 *)((int)this + 0x168));
            uVar5 = SDL_CreateTextureFromSurface(*(undefined4 *)((int)this + 8),uVar4);
            *(undefined4 *)((int)this + 0x70) = uVar5;
            SDL_FreeSurface(uVar4);
            this_01 = (void **)((int)this + 0xe0);
            FUN_004018f0(this_01,"Start typing your question (max. 75 characters): ",(int *)0x31);
            FUN_004018f0((void *)((int)this + 0xf8),&DAT_0040426c,(int *)0x0);
            if (0xf < *(uint *)((int)this + 0xf4)) {
              this_01 = (void **)*this_01;
            }
            uVar4 = TTF_RenderText_Solid
                              (*(undefined4 *)((int)this + 0x170),this_01,
                               *(undefined4 *)((int)this + 0x168));
            uVar5 = SDL_CreateTextureFromSurface(*(undefined4 *)((int)this + 8),uVar4);
            *(undefined4 *)((int)this + 0x74) = uVar5;
            SDL_FreeSurface(uVar4);
            *(undefined *)((int)this + 0x158) = 1;
            uVar4 = SDL_StartTextInput();
            return CONCAT31((int3)((uint)uVar4 >> 8),1);
          }
        }
        pcVar6 = this_004011f0;
        pcVar3 = (char *)SDL_GetError();
        this_00 = FUN_00401000((int *)cerr_exref,pcVar3);
        pbVar2 = operator<<((basic_ostream<char,struct_std::char_traits<char>_> *)this_00,pcVar6);
        return (uint)pbVar2 & 0xffffff00;
      }
    }
  }
  pcVar6 = this_004011f0;
  pcVar3 = (char *)SDL_GetError();
  this_00 = FUN_00401000((int *)cerr_exref,pcVar3);
  pbVar2 = operator<<((basic_ostream<char,struct_std::char_traits<char>_> *)this_00,pcVar6);
  *(undefined *)this = 0;
  return (uint)pbVar2 & 0xffffff00;
}

次の部分にASCIIコードが見える。

            *(undefined4 *)((int)this + 0x5c) = 0x6d6d6967;
            *(undefined4 *)((int)this + 0x60) = 0x6c662065;
            *(undefined4 *)((int)this + 100) = 0x70206761;
            *(undefined4 *)((int)this + 0x68) = 0x3f736c;

これは「gimme flag pls?」で、意味ありげだ。しかし試しにこの文字列を入力しても何も起きない。

次に気になるのは以下の関数。

void __fastcall FUN_004024e0(void *param_1)

{
  uint uVar1;
  int iVar2;
  int iVar3;
  char **ppcVar4;
  undefined4 *_Str1;
  char **this;
  undefined4 *in_stack_ffffffc0;
  int *local_1c;
  char *local_18;
  int *local_14;
  char *local_10;
  int *local_c;
  char local_5;
  
  local_5 = *(char *)((int)param_1 + 0x164);
  *(undefined4 *)((int)param_1 + 0x9c) = 400;
  *(undefined4 *)((int)param_1 + 0x98) = 0x1c2;
  if ((((local_5 == '\0') || (*(char *)((int)param_1 + 0x165) == '\0')) ||
      (*(char *)((int)param_1 + 0x166) == '\0')) || (*(char *)((int)param_1 + 0x167) == '\0')) {
    local_1c = (int *)((int)param_1 + 0x90);
    local_18 = (char *)((int)param_1 + 0x167);
    local_10 = (char *)((int)param_1 + 0x166);
    local_c = (int *)((int)param_1 + 0x94);
    *local_1c = 0x96;
    *(undefined4 *)((int)param_1 + 0x94) = 0;
    local_14 = local_1c;
    if (local_5 == '\0') goto LAB_004025c1;
  }
  local_10 = (char *)((int)param_1 + 0x166);
  local_18 = (char *)((int)param_1 + 0x167);
  local_1c = (int *)((int)param_1 + 0x90);
  local_c = (int *)((int)param_1 + 0x94);
  *local_c = *local_c + -0x14;
  local_14 = local_1c;
  FUN_00401780((void *)((int)param_1 + 0x110),&DAT_004044c4,(void *)0x1);
LAB_004025c1:
  this = (char **)((int)param_1 + 0x110);
  if (*(char *)((int)param_1 + 0x165) != '\0') {
    *local_c = *local_c + 0x14;
    FUN_00401780(this,&DAT_004044c8,(void *)0x1);
  }
  if (*local_10 != '\0') {
    *local_14 = *local_14 + -0x14;
    FUN_00401780(this,&DAT_004044cc,(void *)0x1);
  }
  if (*local_18 != '\0') {
    *local_1c = *local_1c + 0x14;
    FUN_00401780(this,&DAT_004044d0,(void *)0x1);
  }
  if (*(char *)((int)param_1 + 0x158) != '\0') {
    if (*(int *)((int)param_1 + 0x78) != 0) {
      SDL_DestroyTexture();
      *(undefined4 *)((int)param_1 + 0x78) = 0;
      *(undefined4 *)((int)param_1 + 0x15c) = 0;
      *(undefined4 *)((int)param_1 + 0x160) = 0;
    }
    TTF_OpenFont();
    local_c = (int *)0x776fe6;
    iVar2 = TTF_RenderText_Solid();
    if (iVar2 != 0) {
      iVar3 = SDL_CreateTextureFromSurface();
      *(int *)((int)param_1 + 0x78) = iVar3;
      if (iVar3 != 0) {
        *(undefined4 *)((int)param_1 + 0x15c) = *(undefined4 *)(iVar2 + 8);
        *(undefined4 *)((int)param_1 + 0x160) = *(undefined4 *)(iVar2 + 0xc);
      }
      SDL_FreeSurface();
    }
  }
  if (*(char *)((int)param_1 + 0x159) != '\0') {
    uVar1 = *(uint *)((int)param_1 + 0x124);
    ppcVar4 = this;
    if (0xf < uVar1) {
      ppcVar4 = (char **)*this;
    }
    if (*(char *)ppcVar4 == 'L') {
      ppcVar4 = this;
      if (0xf < uVar1) {
        ppcVar4 = (char **)*this;
      }
      if (*(char *)((int)ppcVar4 + 1) == 'L') {
        ppcVar4 = this;
        if (0xf < uVar1) {
          ppcVar4 = (char **)*this;
        }
        if (*(char *)((int)ppcVar4 + 2) == 'U') {
          ppcVar4 = this;
          if (0xf < uVar1) {
            ppcVar4 = (char **)*this;
          }
          if (*(char *)((int)ppcVar4 + 3) == 'R') {
            ppcVar4 = this;
            if (0xf < uVar1) {
              ppcVar4 = (char **)*this;
            }
            if (*(char *)(ppcVar4 + 1) == 'U') {
              ppcVar4 = this;
              if (0xf < uVar1) {
                ppcVar4 = (char **)*this;
              }
              if (*(char *)((int)ppcVar4 + 5) == 'L') {
                ppcVar4 = this;
                if (0xf < uVar1) {
                  ppcVar4 = (char **)*this;
                }
                if (*(char *)((int)ppcVar4 + 6) == 'D') {
                  ppcVar4 = this;
                  if (0xf < uVar1) {
                    ppcVar4 = (char **)*this;
                  }
                  if (*(char *)((int)ppcVar4 + 7) == 'U') {
                    ppcVar4 = this;
                    if (0xf < uVar1) {
                      ppcVar4 = (char **)*this;
                    }
                    if (*(char *)(ppcVar4 + 2) == 'L') {
                      _Str1 = (undefined4 *)((int)param_1 + 0xf8);
                      if (0xf < *(uint *)((int)param_1 + 0x10c)) {
                        _Str1 = (undefined4 *)*_Str1;
                      }
                      iVar2 = strncmp((char *)_Str1,(char *)((int)param_1 + 0x5c),0xf);
                      if (iVar2 == 0) {
                        FUN_00401220(&stack0xffffffc0,this);
                        FUN_00401a10(param_1,in_stack_ffffffc0);
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
  return;
}

ここでは方向キーのLRUDのパターンを確認している。

x32dbgで実行プログラムにアタッチして動作を追うと、文字列入力後にLRUDのパターンを調べるこの関数に飛ぶことがわかった。

そこで分岐通りに方向キーでLLURULDULと入力してから適当な文字列を入れて進むと、カッコの一番内側に入ることができる。ここでは文字列の比較が行われている。

iVar2 = strncmp((char *)_Str1,(char *)((int)param_1 + 0x5c),0xf);

この部分では入力した文字列と「gimme flag pls?」が比較されている。

よって、方向キーでLLURULDULと入力してから、「gimme flag pls?」の文字列を入力してエンターを押す。

Magic 8 Ballクリア画面

flagが表示された。

U_cRackeD_th1$_maG1cBaLL_!!_@flare-on.com

04 - darn_mice

"If it crashes its user error." -Flare Team

Windowsで動作するコマンドラインプログラムdarn_mice.exeが配布される。そのまま実行しても何も起きない。引数を与えると次の文字列を出力して終了する。

On your plate, you see four olives.
You leave the room, and a mouse EATS one!

Ghidraに読み込ませて、この文字列を表示する部分の関数を確認する。

void __cdecl FUN_00401000(PUCHAR param_1)

{
  size_t local_34;
  code *local_38;
  uint local_30;
  char local_2c [36];
  uint local_8;
  
  local_8 = DAT_00419178 ^ (uint)&stack0xfffffffc;
  local_30 = 0;
  local_38 = (code *)0x0;
  local_2c[0] = 'P';
  local_2c[1] = 0x5e;
  local_2c[2] = 0x5e;
  local_2c[3] = 0xa3;
  local_2c[4] = 0x4f;
  local_2c[5] = 0x5b;
  local_2c[6] = 0x51;
  local_2c[7] = 0x5e;
  local_2c[8] = 0x5e;
  local_2c[9] = 0x97;
  local_2c[10] = 0xa3;
  local_2c[11] = 0x80;
  local_2c[12] = 0x90;
  local_2c[13] = 0xa3;
  local_2c[14] = 0x80;
  local_2c[15] = 0x90;
  local_2c[16] = 0xa3;
  local_2c[17] = 0x80;
  local_2c[18] = 0x90;
  local_2c[19] = 0xa3;
  local_2c[20] = 0x80;
  local_2c[21] = 0x90;
  local_2c[22] = 0xa3;
  local_2c[23] = 0x80;
  local_2c[24] = 0x90;
  local_2c[25] = 0xa3;
  local_2c[26] = 0x80;
  local_2c[27] = 0x90;
  local_2c[28] = 0xa3;
  local_2c[29] = 0x80;
  local_2c[30] = 0x90;
  local_2c[31] = 0xa2;
  local_2c[32] = 0xa3;
  local_2c[33] = 0x6b;
  local_2c[34] = 0x7f;
  local_2c[35] = 0;
  FUN_00401240((int)s_On_your_plate,_you_see_four_oliv_00419034);
  local_34 = _strlen((char *)param_1);
  if ((local_34 == 0) || (0x23 < local_34)) {
    FUN_00401240((int)s_No,_nevermind._0041905c);
  }
  else {
    FUN_00401240((int)s_You_leave_the_room,_and_a_mouse_E_0041906c);
    local_30 = 0;
    while (((local_30 < 0x24 && (local_2c[local_30] != '\0')) && (param_1[local_30] != '\0'))) {
      local_38 = (code *)VirtualAlloc((LPVOID)0x0,0x1000,0x3000,0x40);
      *(UCHAR *)local_38 = local_2c[local_30] + param_1[local_30];
      (*local_38)();
      FUN_00401240((int)s_Nibble..._00419098);
      local_30 = local_30 + 1;
    }
    FUN_00401240((int)s_When_you_return,_you_only:_%s_004190a4);
    FUN_00401280((int)&DAT_00419000,DAT_00419030,param_1,(PUCHAR)s_salty_004190c4,(int)&DAT_00419000
                 ,DAT_00419030);
    FUN_00401240((int)&DAT_004190cc);
  }
  FUN_00401615(local_38,local_34,local_30);
  return;
}

x32dbgでアタッチしてこの関数の処理を追うと、次の部分でエラー終了していることがわかった。

(*local_38)();

local_38はVirtualAllocで仮想メモリとして領域が確保され、中身が命令コードとして実行される。実行されるlocal_38の内容は、1行上で計算されている。

*(UCHAR *)local_38 = local_2c[local_30] + param_1[local_30];

param_1は入力文字列、local_2cは関数の上で代入されているバイト列だ。これらのlocal_30番目同士を足したものが実行される。

local_38が実行された際にエラー終了せずに関数に復帰するには、ret命令になるようにバイト値を制御すれば良さそうだ。ret命令はバイナリ値でc3だから、c3 - local_2c[i] = param_1[i]になるように入力文字列を作ればよい。

local_2cのバイト列から入力文字列を計算すると次のようになる。

see three, C3 C3 C3 C3 C3 C3 C3! XD

この文字列を引数にしてプログラムを実行するとflagを取得できる。

>darn_mice.exe "see three, C3 C3 C3 C3 C3 C3 C3! XD"
On your plate, you see four olives.
You leave the room, and a mouse EATS one!
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
Nibble...
When you return, you only: see three, C3 C3 C3 C3 C3 C3 C3! XD
i_w0uld_l1k3_to_RETurn_this_joke@flare-on.com

i_w0uld_l1k3_to_RETurn_this_joke@flare-on.com