2022年3月15日~3月29日に開催された中高生向けのCTF大会、picoCTFの[Reverse Engineering]分野のwriteupです。 その他のジャンルについてはこちらを参照。
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
がスクリプトになってるみたいです。
最後のexec
をprint
に書き換えて実行すると
$ 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の形式に直しておきます。
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 Address
を 0x555555554000
にセットします。
で、上記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
テキストベースのダンジョンゲームみたい。どこに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を見る)
2e
がめっちゃ並んでます。ascii codeだと #
。このゲームでは壁です。
これはもしかすると10面のMapがflagになってるのかもしれないと思い、ここから0
が並びだすくらいまでのアドレスの値をだーーーーっとコピーしてmemory.txt
に貼り付けます。
で、コードの解析より1面あたり100×100というのがわかっているので、その範囲で先程のメモリダンプ結果をascii変換してMapに描画してみます。
なんか見えた!けどフラグのお尻だけじゃん…。
気を取り直して、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が得られました。