好奇心の足跡

飽きっぽくすぐ他のことをしてしまうので、忘れないため・形にして頭に残すための備忘録。

picoCTF2022 [Reverse Engineering] writeup

2022年3月15日~3月29日に開催された中高生向けのCTF大会、picoCTFの[Reverse Engineering]分野のwriteupです。 その他のジャンルについてはこちらを参照。

tech.kusuwada.com

file-run1

A program has been provided to you, what happens if you try to run it on the command line?

Download the program here.

プログラムを走らせるだけの問題。

$ ./run
The flag is: picoCTF{U51N6_Y0Ur_F1r57_F113_e5559d46} 

file-run2

Another program, but this time, it seems to want some input. What happens if you try to run it on the command line with input "Hello!"?

Download the program here.

$ ./run               
Run this file with only one argument.

何やら引数がいるらしい。解析する前にstringsコマンドしたらflagが出てきました。

$ strings run | grep pico
picoCTF{F1r57_4rgum3n7_96f2195f}

GDB Test Drive

Can you get the flag?

Download this binary.

Here's the test drive instructions: * $ chmod +x gdbme * $ gdb gdbme * (gdb) layout asm * (gdb) break *(main+99) * (gdb) run * (gdb) jump *(main+104)

gdbあまり使う機会がないので、言われたとおりにやってみます。

(gdb) break *(main+99)
Breakpoint 1 at 0x132a
(gdb) run
Starting program: /home/kali/ctf/pico2022/gdbme

Breakpoint 1, 0x000055555555532a in main ()
(gdb) jump *(main+104)
Continuing at 0x55555555532f.
picoCTF{d3bugg3r_dr1v3_72bd8355}
(gdb) ior 1 (process 249590) exited normally]

おおー。出ました。

patchme.py

Can you get the flag?

Run this Python program in the same directory as this encrypted flag.

配布されたpythonのコードに、必要なパスワードが平書きされているので、これを使ってコードを実行します。

$ python patchme.flag.py 
Please enter correct password for flag: ak98-=90adfjhgj321sleuth9000
Welcome back... your flag, user:
picoCTF{p47ch1ng_l1f3_h4ck_f01eabfa}

Safe Opener

Can you open this safe?

I forgot the key to my safe but this program is supposed to help me with retrieving the lost key. Can you help me unlock my safe?

Put the password you recover into the picoCTF flag format like: picoCTF{password}

javaファイルが配布されます。
中身を読んでみると、encodedkeyがあり、これはpasswordをbase64 encodeしたものだということがわかるので、このkeyをbase64 decodeしてあげればOK。

unpackme.py

Can you get the flag?

Reverse engineer this Python program.

pythonコードが配布されます。そのまま実行すると

$ python unpackme.flag.py 
What's the password? a
That password is incorrect.

パスワードを聞かれます。payloadがスクリプトになってるみたいです。
最後のexecprintに書き換えて実行すると

$ python unpackme.flag.py 

pw = input('What\'s the password? ')

if pw == 'batteryhorse':
  print('picoCTF{175_chr157m45_cd82f94c}')
else:
  print('That password is incorrect.')

passwordもflagも全部出てきました。

bloat.py

Can you get the flag?

Run this Python program in the same directory as this encrypted flag.

コードと暗号化されたflagが配布されます。
そのまま実行するとパスワードを聞かれるので、flag.txt.encを読み出してdecodeするだけの関数呼び出しに変えて実行してみます。
※下の関数はbloat.flag.pyのメイン部分の書き換え

arg444 = arg132()
print(arg111(arg444))

実行するとflagが得られました。

Fresh Java

Can you get the flag?

Reverse engineer this Java program.

KeygenMe.classというJava classファイルが配布されます。

$ file KeygenMe.class 
KeygenMe.class:  [64-bit architecture=6912] [] [64-bit architecture=1596] [architecture=6619392] [64-bit architecture=1133]

javaのonline decoder にuploadして、配布ファイルをdecompileしてもらいました。

import java.util.Scanner;

// 
// Decompiled by Procyon v0.5.36
// 

public class KeygenMe
{
    public static void main(final String[] array) {
        final Scanner scanner = new Scanner(System.in);
        System.out.println("Enter key:");
        final String nextLine = scanner.nextLine();
        if (nextLine.length() != 34) {
            System.out.println("Invalid key");
            return;
        }
        if (nextLine.charAt(33) != '}') {
            System.out.println("Invalid key");
            return;
        }
...(omit)...
            System.out.println("Invalid key");
            return;
        }
        if (nextLine.charAt(7) != '{') {
            System.out.println("Invalid key");
            return;
        }
        if (nextLine.charAt(6) != 'F') {
            System.out.println("Invalid key");
            return;
        }
        if (nextLine.charAt(5) != 'T') {
            System.out.println("Invalid key");
            return;
        }
        if (nextLine.charAt(4) != 'C') {
            System.out.println("Invalid key");
            return;
        }
        if (nextLine.charAt(3) != 'o') {
            System.out.println("Invalid key");
            return;
        }
        if (nextLine.charAt(2) != 'c') {
            System.out.println("Invalid key");
            return;
        }
        if (nextLine.charAt(1) != 'i') {
            System.out.println("Invalid key");
            return;
        }
        if (nextLine.charAt(0) != 'p') {
            System.out.println("Invalid key");
            return;
        }
        System.out.println("Valid key");
    }
}

下からflagが順番に現れているので、つなげればOK。

Bbbbloat

Can you get the flag?

Reverse engineer this binary.

実行ファイル bbbbloat が配布されます。

$ file bbbbloat 
bbbbloat: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=e25da088d4acf23216039d30e2a6b423eee1b0bd, for GNU/Linux 3.2.0, stripped

ghidraに突っ込んで解析してもらいます。entryから呼ばれているのはこのコード。

undefined8 FUN_00101307(void)

{
  char *__s;
...(omit)...
  local_28 = 0x68653066635f3d33;
  local_20 = 0x4e623665625f64;
  printf("What\'s my favorite number? ");
  __isoc99_scanf();
  if (local_48 == 0x86187) {
    __s = (char *)FUN_00101249(0,&local_38);
...(omit)...

どうやらこのファイルを実行すると聞かれる What's my favorite number? の答えは、 0x86187 のようです。これを10進に直して実行時に入力してみます。

$ ./bbbbloat                
What's my favorite number? 549255
picoCTF{cu7_7h3_bl047_695036e3}

flag出ました👍

unpackme

Can you get the flag?

Reverse engineer this binary.

また実行ファイルが配布されます。

$ file unpackme-upx 
unpackme-upx: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, no section header

実行してみると

$ ./unpackme-upx 
What's my favorite number?

また好きな数字を聞かれます。

ヒントにUPXというワードがありました。WaniCTF 2020 Reversing staticで出てきたやつ!

unpackしてみます。

$ upx -d unpackme-upx 
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2020
UPX 3.96        Markus Oberhumer, Laszlo Molnar & John Reiser   Jan 23rd 2020

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
   1002408 <-    379108   37.82%   linux/amd64   unpackme-upx

Unpacked 1 file.

これでunpackした実行ファイルを、ghidraに食わせてみます。
(Functions windowの、Exports > main でmain関数を探しました。)

undefined8 main(void)
...(omit)...
  printf(&UNK_004b3004);
  __isoc99_scanf(&UNK_004b3020,&iStack68);
  if (iStack68 == 0xb83cb) {
    uStack64 = rotate_encrypt(0,&uStack56);
...(omit)...

この比較されているhexがkeyと思われるので10進数に直して入力します。

 ./unpackme-upx
What's my favorite number? 754635
picoCTF{up><_m3_f7w_e510a27f}

Keygenme

Can you get the flag?

Reverse engineer this binary.

実行ファイルが配布されます。

$ file keygenme    
keygenme: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2a1bc98527b9a13f65225043b8537546ec74f7cf, for GNU/Linux 3.2.0, stripped

実行してみると

$ ./keygenme 
Enter your license key: 
That key is invalid.

ライセンスキーが必要らしい。またghidraに突っ込んでみます。

entryから呼ばれているFUN_0010148b関数はこちら。

undefined8 FUN_0010148b(void)

{
  char cVar1;
  long in_FS_OFFSET;
  char local_38 [40];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("Enter your license key: ");
  fgets(local_38,0x25,stdin);
  cVar1 = FUN_00101209(local_38);
  if (cVar1 == '\0') {
    puts("That key is invalid.");
  }
  else {
    puts("That key is valid.");
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

ここでライセンスキーを比較しているっぽいFUN_00101209

undefined8 FUN_00101209(char *param_1)

{
  size_t sVar1;
  undefined8 uVar2;
  long in_FS_OFFSET;
  int local_d0;
  int local_cc;
  int local_c8;
  int local_c4;
  int local_c0;
  undefined2 local_ba;
  byte local_b8 [16];
  byte local_a8 [16];
  undefined8 local_98;
  undefined8 local_90;
  undefined8 local_88;
  undefined4 local_80;
  char local_78 [14];
  undefined local_6a;
  undefined local_62;
  undefined local_60;
  undefined local_5b;
  char local_58 [21];
  undefined local_43;
  char acStack56 [40];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_98 = 0x7b4654436f636970;
  local_90 = 0x30795f676e317262;
  local_88 = 0x6b5f6e77305f7275;
  local_80 = 0x5f7933;
  local_ba = 0x7d;
  sVar1 = strlen((char *)&local_98);
  MD5((uchar *)&local_98,sVar1,local_b8);
  sVar1 = strlen((char *)&local_ba);
  MD5((uchar *)&local_ba,sVar1,local_a8);
  local_d0 = 0;
  local_cc = 0;
  while (local_cc < 0x10) {
    sprintf(local_78 + local_d0,"%02x",(ulong)local_b8[local_cc]);
    local_cc = local_cc + 1;
    local_d0 = local_d0 + 2;
  }
  local_d0 = 0;
  local_c8 = 0;
  while (local_c8 < 0x10) {
    sprintf(local_58 + local_d0,"%02x",(ulong)local_a8[local_c8]);
    local_c8 = local_c8 + 1;
    local_d0 = local_d0 + 2;
  }
  local_c4 = 0;
  while (local_c4 < 0x1b) {
    acStack56[local_c4] = *(char *)((long)&local_98 + (long)local_c4);
    local_c4 = local_c4 + 1;
  }
  acStack56[27] = local_43;
  acStack56[28] = local_62;
  acStack56[29] = local_62;
  acStack56[30] = local_78[0];
  acStack56[31] = local_5b;
  acStack56[32] = local_43;
  acStack56[33] = local_6a;
  acStack56[34] = local_60;
  acStack56[35] = (undefined)local_ba;
  sVar1 = strlen(param_1);
  if (sVar1 == 0x24) {
    local_c0 = 0;
    while (local_c0 < 0x24) {
      if (param_1[local_c0] != acStack56[local_c0]) {
        uVar2 = 0;
        goto LAB_00101475;
      }
      local_c0 = local_c0 + 1;
    }
    uVar2 = 1;
  }
  else {
    uVar2 = 0;
  }
LAB_00101475:
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return uVar2;
}

flagの先頭から0x1bまでは、local_98 ~ local_80の値がそのまま使えそう。reverse処理してflagの形式に直しておきます。

f:id:kusuwada:20220404143819p:plain

0x1c(28d) ~ 0x24(36d)までの8桁は、処理によって得られた値が入るみたいです。動的解析しないとMOD関数とか見えるしちょっと厳しそう…。動的解析に苦手意識があってあまり得意でないなぁ…。

かしわばさんの、【CTF入門】ELFバイナリのリバースエンジニアリングに入門してみようをじっくり読み込みながらやってみました。これめちゃめちゃわかりやすくて素敵。

まずはradare2でmain関数のアドレスを調べておきます。(実は最初、radare2で動的解析しようとしてて玉砕した)

$ r2 keygenme         
Warning: run r2 with -e io.cache=true to fix relocations in disassembly
[0x00001120]> aaaa
...(omit)...
[0x00001120]> s main

mainのアドレスは0x00001120
次はgdbで開いてデバッグモードで開いています。gdb-pedaが入っていなかったので How To Install GDB Peda? - Bitforestinfo を参考にして入れておきます。

$ gdb ./keygenme 
GNU gdb (Debian 10.1-2) 10.1.90.20210103-git
...(omit)...
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./keygenme...
(No debugging symbols found in ./keygenme)

runして、input待ちのタイミングで Ctrl+C で中断します。

gdb-peda$ run
Starting program: /home/kali/ctf/pico2022/keygenme 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Enter your license key: ^C
Program received signal SIGINT, Interrupt.
[----------------------------------registers-----------------------------------]
RAX: 0xfffffffffffffe00 
RBX: 0x7ffff7cb29a0 --> 0xfbad2288 
RCX: 0x7ffff7bd255e (<__GI___libc_read+14>:     cmp    rax,0xfffffffffffff000)
RDX: 0x400 
RSI: 0x5555555596b0 --> 0x0 
RDI: 0x0 
RBP: 0x7ffff7cb44a0 --> 0x0 
RSP: 0x7fffffffddc8 --> 0x7ffff7b64cdc (<_IO_new_file_underflow+396>:   test   rax,rax)
RIP: 0x7ffff7bd255e (<__GI___libc_read+14>:     cmp    rax,0xfffffffffffff000)
R8 : 0x0 
R9 : 0x7ffff7cb2c00 --> 0x555555559ab0 --> 0x0 
R10: 0x5d (']')
R11: 0x246 
R12: 0x7ffff7cb36c0 --> 0xfbad2a84 
R13: 0xd68 ('h\r')
R14: 0x7ffff7cb38a0 --> 0x0 
R15: 0x7ffff7cb4608 --> 0x7ffff7b66a70 (<_IO_cleanup>:  push   r15)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x7ffff7bd2558 <__GI___libc_read+8>: test   eax,eax
   0x7ffff7bd255a <__GI___libc_read+10>:        jne    0x7ffff7bd2570 <__GI___libc_read+32>
   0x7ffff7bd255c <__GI___libc_read+12>:        syscall 
=> 0x7ffff7bd255e <__GI___libc_read+14>:        cmp    rax,0xfffffffffffff000
   0x7ffff7bd2564 <__GI___libc_read+20>:        ja     0x7ffff7bd25c0 <__GI___libc_read+112>
   0x7ffff7bd2566 <__GI___libc_read+22>:        ret    
   0x7ffff7bd2567 <__GI___libc_read+23>:        nop    WORD PTR [rax+rax*1+0x0]
   0x7ffff7bd2570 <__GI___libc_read+32>:        sub    rsp,0x28
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffddc8 --> 0x7ffff7b64cdc (<_IO_new_file_underflow+396>:  test   rax,rax)
0008| 0x7fffffffddd0 --> 0x0 
0016| 0x7fffffffddd8 --> 0x7ffff7cb44a0 --> 0x0 
0024| 0x7fffffffdde0 --> 0x0 
0032| 0x7fffffffdde8 --> 0x7ffff7cb29a0 --> 0xfbad2288 
0040| 0x7fffffffddf0 --> 0x7ffff7cb44a0 --> 0x0 
0048| 0x7fffffffddf8 --> 0x7ffff7cb29a0 --> 0xfbad2288 
0056| 0x7fffffffde00 --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGINT

ここで info proc mappings をしてadressを表示。

gdb-peda$ info proc mappings
process 192233
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
      0x555555554000     0x555555555000     0x1000        0x0 /home/kali/ctf/pico2022/keygenme
      0x555555555000     0x555555556000     0x1000     0x1000 /home/kali/ctf/pico2022/keygenme
      0x555555556000     0x555555557000     0x1000     0x2000 /home/kali/ctf/pico2022/keygenme
      0x555555557000     0x555555558000     0x1000     0x2000 /home/kali/ctf/pico2022/keygenme
      0x555555558000     0x555555559000     0x1000     0x3000 /home/kali/ctf/pico2022/keygenme
      0x555555559000     0x55555557a000    0x21000        0x0 [heap]

ファイルオフセット0x1000が0x555555555000にマッピングされているようです。

ラッキーなことに、かしわばさんの記事と同じマッピングっぽい!
mainは先程調べた結果 0x00001120 だったので、 0x555555554000 + 0x00001120 = 0x555555555120 にmainがロードされている事になりそう。

次に、BreakPointを貼る場所をGhidraで特定します。先程Ghidra解析済みなので、そのまま Window > Memory Map を選択、右上にある家アイコンから Base Image Address0x555555554000 にセットします。
で、上記decompile済のソースの acStack56[27] ~ acStack56[35] に値を代入している命令のアドレスをチェックし、gdb上でブレークポイントを貼ります。

gdb-peda$ b * 0x5555555553cc
Breakpoint 5 at 0x5555555553cc
gdb-peda$ b * 0x5555555553d3
Breakpoint 6 at 0x5555555553d3
gdb-peda$ b * 0x5555555553da
Breakpoint 7 at 0x5555555553da
gdb-peda$ b * 0x5555555553e1
Breakpoint 8 at 0x5555555553e1
gdb-peda$ b * 0x5555555553e8
Breakpoint 9 at 0x5555555553e8
gdb-peda$ b * 0x5555555553ef
Breakpoint 10 at 0x5555555553ef
gdb-peda$ b * 0x5555555553f6
Breakpoint 11 at 0x5555555553f6
gdb-peda$ b * 0x5555555553fd
Breakpoint 12 at 0x5555555553fd
gdb-peda$ b * 0x555555555407
Breakpoint 13 at 0x555555555407

色々試したので index:5 からの連番になってしまいましたが、特に意味はありません。

さぁ、走らせてみます!

gdb-peda$ run
Starting program: /home/kali/ctf/pico2022/keygenme 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Enter your license key: picoCTF{br1ng_y0ur_0wn_k3y_11111111}
[----------------------------------registers-----------------------------------]
RAX: 0x61 ('a')
RBX: 0x555555555520 (endbr64)
RCX: 0x1 
RDX: 0x5f ('_')
...(omit)...

Breakpoint 5, 0x00005555555553cc in ?? ()

raxには 'a' が入っているみたい!一文字目はaかな?
このままプログラムを走らせるには、continueのcだけでOK。8も自分全部回すと、

gdb-peda$ cContinuing.
[----------------------------------registers-----------------------------------]
RAX: 0x62 ('b')
RBX: 0x555555555520 (endbr64)
...(omit)...
gdb-peda$ c
Continuing.
[----------------------------------registers-----------------------------------]
RAX: 0x7d ('}')
...(omit)...

無事、最後にフラグフォーマットが出てきました👍
先にわかっていた分とつなげて完成です。もうかしわばさんに足を向けて寝れません。

Wizardlike

Do you seek your destiny in these deplorable dungeons? If so, you may want to look elsewhere. Many have gone before you and honestly, they've cleared out the place of all monsters, ne'erdowells, bandits and every other sort of evil foe. The dungeons themselves have seen better days too. There's a lot of missing floors and key passages blocked off. You'd have to be a real wizard to make any progress in this sorry excuse for a dungeon! Download the game.

'w', 'a', 's', 'd' moves your character and 'Q' quits. You'll need to improvise some wizardly abilities to find the flag in this dungeon crawl. '.' is floor, '#' are walls, '<' are stairs up to previous level, and '>' are stairs down to next level.

配布された実行ファイルを見てみます。

$ file game    
game: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=7aa2807541e7aec1342965f13c693f38ae3e2882, for GNU/Linux 3.2.0, stripped

ゲームを実行してみます。

$ ./game

f:id:kusuwada:20220404144100p:plain:w400

テキストベースのダンジョンゲームみたい。どこにflagがあるか、どうやったら取れるのかについては問題文にも書いていません。暫く遊んで階層を進んでみたけど、ずっと同じことの繰り返し。
ghidraでdecompileしてコードを見たところ、10面まであるみたい。が、4面から先には道がなくて普通に遊んでいる分にはこれ以上進めない。
ゲームのチート系問題かな?

ここでヒントを見てみます。

  • Different tools are better at different things. Ghidra is awesome at static analysis, but radare2 is amazing at debugging.

  • With the right focus and preparation, you can teleport to anywhere on the map.

radare2, ghidraを両方使いつつ、map上の任意位置にテレポートする方法を考えるっぽい。とりあえずコードを良く呼んで動作をもう一段深く理解してみることに。

ghidraでdecompileしてもらった結果を、変数名を想像しながら噛み砕いたmainコードがこちら。

undefined8 main(void)

{
  bool isContinue;
  char canGo;
  int win_width;
  int win_height;
  int userInput;
  int idx_w;
  int idx_h;  ushort mapIdx;
  
  isContinue = true;
  generateMap(s_#########_#.......#_......#....._00107740);
  FUN_00101229();
  initscr();
  if (stdscr == 0) {
    win_width = -1;
    win_height = -1;
  }
  else {
    win_width = *(short *)(stdscr + 4) + 1;
    win_height = *(short *)(stdscr + 6) + 1;
  }
  DAT_0011fe98 = win_height;
  DAT_0011fe9c = win_width;
  noecho();
  curs_set(0);

  while (isContinue) {
    // Map作成
    if (currFloor != nextFloor) {  // 階層が変わったときのみ
      if (nextFloor == 1) {
        FUN_00101229();
        generateMap(s_#########_#.......#_......#....._00107740);
        currentPosY = 2;
        currentPosX = 1;
        offsetY = 0;
        offsetX = 0;
        currFloor = 1;
      }
      else {
        if (nextFloor == 2) {
          FUN_00101229();
          generateMap(s_#####._........................._00109e60);
          currentPosY = 1;
          currentPosX = 2;
          offsetY = 0;
          offsetX = 0;
          currFloor = 2;
        }
        else {
          if (nextFloor == 3) {
            FUN_00101229();
            generateMap(s_#################_......._#<...._0010c580);
            currentPosY = 2;
            currentPosX = 1;
            offsetY = 0;
            offsetX = 0;
            currFloor = 3;
          }
          else {
            if (nextFloor == 4) {
              FUN_00101229();
              generateMap(s_..._.._......._.<._####._.#...#._0010eca0);
              currentPosY = 2;
              currentPosX = 1;
              offsetY = 0;
              offsetX = 0;
              currFloor = 4;
            }
            else {
              if (nextFloor == 5) {
                FUN_00101229();
                generateMap(s_########################_#<....._001113c0);
                currentPosY = 2;
                currentPosX = 1;
                offsetY = 0;
                offsetX = 0;
                currFloor = 5;
              }
              else {
                if (nextFloor == 6) {
                  FUN_00101229();
                  generateMap(s_......._.<....._......._......._._00113ae0);
                  currentPosY = 2;
                  currentPosX = 1;
                  offsetY = 0;
                  offsetX = 0;
                  currFloor = 6;
                }
                else {
                  if (nextFloor == 7) {
                    FUN_00101229();
                    generateMap(s_..._.<........._..........._..._._00116200);
                    currentPosY = 2;
                    currentPosX = 1;
                    offsetY = 0;
                    offsetX = 0;
                    currFloor = 7;
                  }
                  else {
                    if (nextFloor == 8) {
                      FUN_00101229();
                      generateMap(s_#########################_#<#..._00118920);
                      currentPosY = 1;
                      currentPosX = 2;
                      offsetY = 0;
                      offsetX = 0;
                      currFloor = 8;
                    }
                    else {
                      if (nextFloor == 9) {
                        FUN_00101229();
                        generateMap(s_..._........<._..###....._.#...#_0011b040);
                        currentPosY = 2;
                        currentPosX = 1;
                        offsetY = 0;
                        offsetX = 0;
                        currFloor = 9;
                      }
                      else {
                        if (nextFloor == 10) {
                          FUN_00101229();
                          generateMap(&DAT_0011d760);
                          currentPosY = 1;
                          currentPosX = 1;
                          offsetY = 0;
                          offsetX = 0;
                          currFloor = 10;
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
    // spaceでMap初期化の描画
    idx_w = 0;
    while (idx_w < win_width) {
      idx_h = 0;
      while (idx_h < win_height) {
        mvprintw(idx_w,idx_h, ' ');  // DAT_00103004 = 0x20 = space
        idx_h = idx_h + 1;
      }
      idx_w = idx_w + 1;
    }
    // MAP描画
    idx_w = 0;
    while (idx_w < win_width) {
      idx_h = 0;
      while (idx_h < win_height) {  // Map範囲内
        if ((((idx_h + offsetY < 100) && (idx_w + offsetX < 100)) &&
            (-1 < idx_h + offsetY)) && (-1 < idx_w + offsetX)) {
          // 壁当たり判定, 0が返ると壁。
          canGo = FUN_00101332(currentPosY, currentPosX, offsetY + idx_h, offsetX + idx_w);
          // 今の位置が壁ではなくて描画済ではない場合
          if ((canGo != '\0') || ((&DAT_DRAWN) [(long)(offsetX + idx_w) * 100 + (long)(idx_h + offsetY)] != '\0')) {
            (&DAT_DRAWN)[(long)(offsetX + idx_w) * 100 + (long)(idx_h + offsetY)] = 1;
            mapIdx = (ushort)(byte)(&DAT_MAP) [(long)(offsetX + idx_w) * 100 + (long)(idx_h + offsetY)];
            // Map描画
            mvprintw(idx_w,idx_h,&mapIdx);
          }
        }
        else { // Map範囲外
          mvprintw(idx_w,idx_h, ' ');  // DAT_00103004 = 0x20 = space
        }
        idx_h = idx_h + 1;
      }
      idx_w = idx_w + 1;
    }
    // @(現在地) 描画
    mvprintw(currentPosX - offsetX,currentPosY - offsetY, '@'); // DAT_00103006 = 0x40 = @
    wrefresh(stdscr);  // 画面クリア
    // 次のアクション
    userInput = wgetch();
    if (userInput == 'Q') {  // 0x51
      isContinue = false;
    }
    else {
      if (userInput == 'w') {  // 0x77
        FUN_0010166b(); // go up
      }
      else {
        if (userInput == 's') {  // 0x73
          FUN_001016e7();  // go down
        }
        else {
          if (userInput == 'a') {  // 0x61
            FUN_00101763();  // go left
          }
          else {
            if (userInput == 'd') {  // 0x64
              FUN_001017df();  // go right
            }
          }
        }
      }
    }
    // 階層移動判定
    if ((&DAT_MAP)[(long)currentPosX * 100 + (long)currentPosY] == '>') {
      nextFloor = nextFloor + 1;
    }
    else {
      if ((&DAT_MAP)[(long)currentPosX * 100 + (long)currentPosY] == '<') {
        nextFloor = nextFloor + -1;
      }
    }
  }
}

ここまでちゃんと見る必要はなかったけど、なんとなく全部解読してみた。最後の10階層目でのmap作成時に参照しているメモリ &DAT_0011d760 に注目してみると(ghidra上で該当コードをクリックし、Bytesのwindowを見る)

f:id:kusuwada:20220404144311p:plain

2e がめっちゃ並んでます。ascii codeだと #。このゲームでは壁です。
これはもしかすると10面のMapがflagになってるのかもしれないと思い、ここから0が並びだすくらいまでのアドレスの値をだーーーーっとコピーしてmemory.txtに貼り付けます。

で、コードの解析より1面あたり100×100というのがわかっているので、その範囲で先程のメモリダンプ結果をascii変換してMapに描画してみます。

f:id:kusuwada:20220404144421p:plain

なんか見えた!けどフラグのお尻だけじゃん…。
気を取り直して、ghidraに戻って、memory先程の上の方、0がとても並ぶところくらいまでをだーーーーーーーーーーーーーーーーーーーーーっとコピーして memory.txtに貼り付けます。

最終的なMap描画スクリプトはこちら。
各面のMapの間に 00 00 00 00 ... が入っているのを発見したので、それを利用した出力ロジックになっております。

with open('memory.txt', 'r') as f:
    data = f.read().split(' ')

x = 0
y = 0
for d in data:
    if int(d,16) == 0:
        print()
        x = 0
        y = 0
    else:
        print(chr(int(d,16)),end='')
        x += 1
        if (x == 100):
            y += 1
            x = 0
            print()

先程のように各面のMapの全貌が現れ、それぞれの面の行動範囲外にflagの断片が書いてあります。ちょっと読みにくいけど、上の面から読み取れる文字をつないでいくと、正しいflagが得られました。