以下の内容はhttps://daisuke20240310.hatenablog.com/entry/picoctf2024reより取得しました。


picoCTF 2024:Reverse Engineeringの全7問をやってみた(Windowsプログラムの3問は後日やります)

前回 は、picoCTF の picoCTF 2024 のうち、Binary Exploitation をやってみました。全10問のうち、Hard の 1問目は解けず、2問目は後回しになりました。Hard 問題は、いきなりレベルが上がった気がします。

今回は、引き続き、picoCTF の picoCTF 2024 のうち、Reverse Engineering というカテゴリの全7問をやっていきたいと思います。Medium が 7問です。

それでは、やっていきます。

はじめに

「セキュリティ」の記事一覧です。良かったら参考にしてください。

セキュリティの記事一覧
・第1回:Ghidraで始めるリバースエンジニアリング(環境構築編)
・第2回:Ghidraで始めるリバースエンジニアリング(使い方編)
・第3回:VirtualBoxにParrotOS(OVA)をインストールする
・第4回:tcpdumpを理解して出力を正しく見れるようにする
・第5回:nginx(エンジンエックス)を理解する
・第6回:Python+Flask(WSGI+Werkzeug+Jinja2)を動かしてみる
・第7回:Python+FlaskのファイルをCython化してみる
・第8回:shadowファイルを理解してパスワードを解読してみる
・第9回:安全なWebアプリケーションの作り方(徳丸本)の環境構築
・第10回:Vue.jsの2.xと3.xをVue CLIを使って動かしてみる(ビルドも行う)
・第11回:Vue.jsのソースコードを確認する(ビルド後のソースも見てみる)
・第12回:徳丸本:OWASP ZAPの自動脆弱性スキャンをやってみる
・第13回:徳丸本:セッション管理を理解してセッションID漏洩で成りすましを試す
・第14回:OWASP ZAPの自動スキャン結果の分析と対策:パストラバーサル
・第15回:OWASP ZAPの自動スキャン結果の分析と対策:クロスサイトスクリプティング(XSS)
・第16回:OWASP ZAPの自動スキャン結果の分析と対策:SQLインジェクション
・第17回:OWASP ZAPの自動スキャン結果の分析と対策:オープンリダイレクト
・第18回:OWASP ZAPの自動スキャン結果の分析と対策:リスク中すべて
・第19回:CTF初心者向けのCpawCTFをやってみた
・第20回:hashcatの使い方とGPUで実行したときの時間を見積もってみる
・第21回:Scapyの環境構築とネットワークプログラミング
・第22回:CpawCTF2にチャレンジします(クリア状況は随時更新します)
・第23回:K&Rのmalloc関数とfree関数を理解する
・第24回:C言語、アセンブラでシェルを起動するプログラムを作る(ARM64)
・第25回:機械語でシェルを起動するプログラムを作る(ARM64)
・第26回:入門セキュリhttps://github.com/SECCON/SECCON2017_online_CTF.gitティコンテスト(CTFを解きながら学ぶ実践技術)を読んだ
・第27回:x86-64 ELF(Linux)のアセンブラをGDBでデバッグしながら理解する(GDBコマンド、関連ツールもまとめておく)
・第28回:入門セキュリティコンテスト(CTFを解きながら学ぶ実践技術)のPwnable問題をやってみる
・第29回:実行ファイルのセキュリティ機構を調べるツール「checksec」のまとめ
・第30回:setodaNote CTF Exhibitionにチャレンジします(クリア状況は随時更新します)
・第31回:常設CTFのksnctfにチャレンジします(クリア状況は随時更新します)
・第32回:セキュリティコンテストチャレンジブックの「Part2 pwn」を読んだ
・第33回:セキュリティコンテストチャレンジブックの「付録」を読んでx86とx64のシェルコードを作った
・第34回:TryHackMeを始めてみたけどハードルが高かった話
・第35回:picoCTFを始めてみた(Beginner picoMini 2022:全13問完了)
・第36回:picoCTF 2024:Binary Exploitationの全10問をやってみた(Hardの1問は後日やります)
・第37回:picoCTF 2024:Reverse Engineeringの全7問をやってみた(Windowsプログラムの3問は後日やります) ← 今回

picoCTF の公式サイトは以下です。英語のサイトですが、シンプルで分かりやすいので困らずに進めることができます。

picoctf.com

それでは、やっていきます。

picoCTF 2024:Reverse Engineering

ポイントの低い順にやっていきます。

packer(100ポイント)

Medium の問題です。バイナリファイル(out)が 1つダウンロードできます。

packer問題
packer問題

packer という名前の問題ですが、そういえば、以下の記事で読んだ付録の 1 に、パッカーというバイナリがあると出てきてました。

daisuke20240310.hatenablog.com

まずは、簡単に表層解析を行います。strip の表示がありませんね、No Symbols なので、stripされてそうですが、こういうのは初めてです。RELRO が無効(PLT、GOT の両方が書き込み可能)、スタック実行可能、プログラムのアドレスのランダム化が無効です。

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

$ checksec --file=out
RELRO     STACK CANARY     NX          PIE     RPATH  RUNPATH  Symbols     FORTIFY  Fortified  Fortifiable  FILE
No RELRO  No canary found  NX enabled  No PIE  N/A    N/A      No Symbols  N/A      0          0            out

なんか変なので、試しに、upxコマンドを実行してみます。やはり解凍できました。

$ upx -d out
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2024
UPX 4.2.2       Markus Oberhumer, Laszlo Molnar & John Reiser    Jan 3rd 2024

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
    877724 <-    336520   38.34%   linux/amd64   out

Unpacked 1 file.

もう一度、表層解析を行います。普通の見え方になりました。RELRO が Partial RELRO に変化しました。

$ file out
out: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=36bf0fdfd791fee2c1cc45dff9ddb2a4f48f6d53, for GNU/Linux 3.2.0, not stripped

$ checksec --file=out
RELRO          STACK CANARY     NX          PIE     RPATH  RUNPATH  Symbols       FORTIFY  Fortified  Fortifiable  FILE
Partial RELRO  No canary found  NX enabled  No PIE  N/A    N/A      1879 Symbols  N/A      0          22           out

実行できるかどうか、やってみます。なるほど、正しいパスワードを入力すればフラグが得られるということでしょうか。でも、サーバ接続が無いので、逆コンパイルすると分かってしまうので、それも変ですかね。

$ ./out
Enter the password to unlock this file: AAA
You entered: AAA

Access denied

続いて、静的解析として、Ghidra でソースコードを眺めてみます。スタティックリンクなのでサイズが大きいです。

main関数です。というか、main関数だけのようです。

0x66(f)が見えると、、、ん!?と見てしまいますが、今回は picoCTF なので、以下の数値を気にする必要がありますね。直接の値は無いようです。

$ python -c 'print("picoCTF".encode("utf-8").hex())'
7069636f435446`

では、普通に見ていきます。

最初の for文あたりのアセンブラを見てたんですが、逆コンパイルにない内容が結構あります。最初の div命令(div rsi)は、RDX:RAX / ESI(115 / 16) で、商が RAX:7、あまりが RDX:3 になりますが、何に使ってるのか分かりません。次の命令の乗算(imul rax, rax, 0x10)は、第2オペランドの RAX と定数を乗算して、上位32bitを RDX、下位32bitを第1オペランドの RAX に格納します。

と、真面目に読もうとしましたが、疲れてきて、下の方を見ると答えがあるのに気づきました(笑)。Password correct, please see flag: 7069636f4354467b5539585f556e5034636b314e365f42316e345269 33535f39343130343638327d というやつです。途中にスペースがあるので注意が必要ですが、これを ASCIIコード にするだけでした。全体像を見てから取り掛かる必要がありますね。

undefined8 main(void)
{
  size_t sVar1;
  char *pcVar2;
  int iVar3;
  undefined *puVar4;
  long in_FS_OFFSET;
  undefined auStack_a8 [8];
  size_t local_a0;
  undefined8 local_98;
  char *local_90;
  undefined8 local_88;
  undefined8 local_80;
  undefined8 local_78;
  undefined8 local_70;
  undefined8 local_68;
  undefined8 local_60;
  undefined8 local_58;
  undefined8 local_50;
  undefined8 local_48;
  undefined8 local_40;
  undefined8 local_38;
  undefined8 local_30;
  undefined4 local_28;
  long local_20;
  
  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  local_a0 = 100;
  local_98 = 99;
  for (puVar4 = auStack_a8; puVar4 != auStack_a8; puVar4 = puVar4 + -0x1000) {
    *(undefined8 *)(puVar4 + -8) = *(undefined8 *)(puVar4 + -8);
  }
  *(undefined8 *)(puVar4 + -8) = *(undefined8 *)(puVar4 + -8);
  local_88 = 0x6636333639363037;
  local_80 = 0x6237363434353334;
  local_78 = 0x6635383539333535;
  local_70 = 0x3433303565363535;
  local_68 = 0x6534313362363336;
  local_60 = 0x3133323466353633;
  local_58 = 0x3936323534336536;
  local_50 = 0x3933663533353333;
  local_48 = 0x3433303331333433;
  local_40 = 0x6437323338333633;
  local_38 = 0;
  local_30 = 0;
  local_28 = 0;
  local_90 = puVar4 + -0x70;
  *(undefined8 *)(puVar4 + -0x78) = 0x401eee;
  printf("Enter the password to unlock this file: ");
  pcVar2 = local_90;
  sVar1 = local_a0;
  *(undefined8 *)(puVar4 + -0x78) = 0x401f0f;
  fgets(pcVar2,(int)sVar1,(FILE *)stdin);
  pcVar2 = local_90;
  *(undefined8 *)(puVar4 + -0x78) = 0x401f2a;
  printf("You entered: %s\n",pcVar2);
  pcVar2 = local_90;
  sVar1 = local_a0;
  *(undefined8 *)(puVar4 + -0x78) = 0x401f47;
  iVar3 = strncmp(pcVar2,(char *)&local_88,sVar1);
  if (iVar3 == 0) {
    *(undefined8 *)(puVar4 + -0x78) = 0x401f57;
    puts(
        "Password correct, please see flag: 7069636f4354467b5539585f556e5034636b314e365f42316e345269 33535f39343130343638327d"
        );
    *(undefined8 *)(puVar4 + -0x78) = 0x401f63;
    puts((char *)&local_88);
  }
  else {
    *(undefined8 *)(puVar4 + -0x78) = 0x401f71;
    puts("Access denied");
  }
  if (local_20 == *(long *)(in_FS_OFFSET + 0x28)) {
    return 0;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

FactCheck(200ポイント)

先ほどと同じ Medium の問題ですが、先ほどは 100ポイントで、200ポイントです。バイナリファイル(bin)が 1つダウンロードできます。何か不都合があって、バイナリとフラグが変更されたようですが、新しいバイナリでも問題なく解決できるということなので進めていきます。

FactCheck問題
FactCheck問題

表層解析を行います。何となく strings も実行してみましたが、フラグが少し見えています。バイナリエディタで見てみます。うーん、アンダースコアの後は 0(NULL文字)なので、途中までしかないようです。

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

$ checksec --file=bin
RELRO       STACK CANARY  NX          PIE          RPATH     RUNPATH     Symbols     FORTIFY  Fortified  Fortifiable  FILE
Full RELRO  Canary found  NX enabled  PIE enabled  No RPATH  No RUNPATH  83 Symbols  N/A      0          0            bin

$ strings bin | grep pico
picoCTF{wELF_d0N3_mate_

Ghidra で見てみます。C++ っぽいです。長い main関数だけです。さすがに読む気にならないので、GDB で動かします。最後の方にブレークポイントを設定してやってみるとうまくいきませんね。

仕方ないので、ni でポチポチ進めます。途中までは、スタック上にフラグが少しずつ出来上がっていく感じでしたが、最後の方で、突然、スタック上に見えていたフラグが見えなくなりました。GDB なので画面をスクロールして、見えなくなる直前のフラグを提出するとクリアでした。

undefined8 main(void)
{
  char cVar1;
  char *pcVar2;
  long in_FS_OFFSET;
  allocator<char> local_249;
  basic_string<> local_248 [32];
  basic_string local_228 [32];
  basic_string<> local_208 [32];
  basic_string local_1e8 [32];
  basic_string local_1c8 [32];
  basic_string local_1a8 [32];
  basic_string local_188 [32];
  basic_string local_168 [32];
  basic_string<> local_148 [32];
  basic_string local_128 [32];
  basic_string<> local_108 [32];
  basic_string<> local_e8 [32];
  basic_string local_c8 [32];
  basic_string<> local_a8 [32];
  basic_string local_88 [32];
  basic_string local_68 [32];
  basic_string<> local_48 [40];
  long local_20;
  
  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  std::allocator<char>::allocator();
                    /* try { // try from 001012cf to 001012d3 has its CatchHandler @ 00101975 */
                    /* } // end try from 001012cf to 001012d3 */
  std::__cxx11::basic_string<>::basic_string
            ((char *)local_248,(allocator *)"picoCTF{wELF_d0N3_mate_");
  std::allocator<char>::~allocator(&local_249);
  std::allocator<char>::allocator();
                    /* try { // try from 0010130a to 0010130e has its CatchHandler @ 00101996 */
                    /* } // end try from 0010130a to 0010130e */
  std::__cxx11::basic_string<>::basic_string((char *)local_228,(allocator *)&DAT_0010201d);
  std::allocator<char>::~allocator(&local_249);
  std::allocator<char>::allocator();
                    /* try { // try from 00101345 to 00101349 has its CatchHandler @ 001019b1 */
                    /* } // end try from 00101345 to 00101349 */
  std::__cxx11::basic_string<>::basic_string((char *)local_208,(allocator *)&DAT_0010201f);
  std::allocator<char>::~allocator(&local_249);
  std::allocator<char>::allocator();
                    /* try { // try from 00101380 to 00101384 has its CatchHandler @ 001019cc */
                    /* } // end try from 00101380 to 00101384 */
  std::__cxx11::basic_string<>::basic_string((char *)local_1e8,(allocator *)&DAT_00102021);
  std::allocator<char>::~allocator(&local_249);
  std::allocator<char>::allocator();
                    /* try { // try from 001013bb to 001013bf has its CatchHandler @ 001019e7 */
                    /* } // end try from 001013bb to 001013bf */
  std::__cxx11::basic_string<>::basic_string((char *)local_1c8,(allocator *)&DAT_00102023);
  std::allocator<char>::~allocator(&local_249);
  std::allocator<char>::allocator();
                    /* try { // try from 001013f6 to 001013fa has its CatchHandler @ 00101a02 */
                    /* } // end try from 001013f6 to 001013fa */
  std::__cxx11::basic_string<>::basic_string((char *)local_1a8,(allocator *)&DAT_00102025);
  std::allocator<char>::~allocator(&local_249);
  std::allocator<char>::allocator();
                    /* try { // try from 00101431 to 00101435 has its CatchHandler @ 00101a1d */
                    /* } // end try from 00101431 to 00101435 */
  std::__cxx11::basic_string<>::basic_string((char *)local_188,(allocator *)&DAT_00102027);
  std::allocator<char>::~allocator(&local_249);
  std::allocator<char>::allocator();
                    /* try { // try from 0010146c to 00101470 has its CatchHandler @ 00101a38 */
                    /* } // end try from 0010146c to 00101470 */
  std::__cxx11::basic_string<>::basic_string((char *)local_168,(allocator *)&DAT_00102029);
  std::allocator<char>::~allocator(&local_249);
  std::allocator<char>::allocator();
                    /* try { // try from 001014a7 to 001014ab has its CatchHandler @ 00101a53 */
                    /* } // end try from 001014a7 to 001014ab */
  std::__cxx11::basic_string<>::basic_string((char *)local_148,(allocator *)&DAT_0010202b);
  std::allocator<char>::~allocator(&local_249);
  std::allocator<char>::allocator();
                    /* try { // try from 001014e2 to 001014e6 has its CatchHandler @ 00101a6e */
                    /* } // end try from 001014e2 to 001014e6 */
  std::__cxx11::basic_string<>::basic_string((char *)local_128,(allocator *)&DAT_00102029);
  std::allocator<char>::~allocator(&local_249);
  std::allocator<char>::allocator();
                    /* try { // try from 0010151d to 00101521 has its CatchHandler @ 00101a89 */
                    /* } // end try from 0010151d to 00101521 */
  std::__cxx11::basic_string<>::basic_string((char *)local_108,(allocator *)&DAT_0010202d);
  std::allocator<char>::~allocator(&local_249);
  std::allocator<char>::allocator();
                    /* try { // try from 00101558 to 0010155c has its CatchHandler @ 00101aa4 */
                    /* } // end try from 00101558 to 0010155c */
  std::__cxx11::basic_string<>::basic_string((char *)local_e8,(allocator *)&DAT_0010202f);
  std::allocator<char>::~allocator(&local_249);
  std::allocator<char>::allocator();
                    /* try { // try from 00101593 to 00101597 has its CatchHandler @ 00101abf */
                    /* } // end try from 00101593 to 00101597 */
  std::__cxx11::basic_string<>::basic_string((char *)local_c8,(allocator *)&DAT_00102031);
  std::allocator<char>::~allocator(&local_249);
  std::allocator<char>::allocator();
                    /* try { // try from 001015ce to 001015d2 has its CatchHandler @ 00101ada */
                    /* } // end try from 001015ce to 001015d2 */
  std::__cxx11::basic_string<>::basic_string((char *)local_a8,(allocator *)&DAT_00102033);
  std::allocator<char>::~allocator(&local_249);
  std::allocator<char>::allocator();
                    /* try { // try from 00101606 to 0010160a has its CatchHandler @ 00101af5 */
                    /* } // end try from 00101606 to 0010160a */
  std::__cxx11::basic_string<>::basic_string((char *)local_88,(allocator *)&DAT_00102027);
  std::allocator<char>::~allocator(&local_249);
  std::allocator<char>::allocator();
                    /* try { // try from 0010163e to 00101642 has its CatchHandler @ 00101b0d */
                    /* } // end try from 0010163e to 00101642 */
  std::__cxx11::basic_string<>::basic_string((char *)local_68,(allocator *)&DAT_00102023);
  std::allocator<char>::~allocator(&local_249);
  std::allocator<char>::allocator();
                    /* try { // try from 00101676 to 0010167a has its CatchHandler @ 00101b25 */
                    /* } // end try from 00101676 to 0010167a */
  std::__cxx11::basic_string<>::basic_string((char *)local_48,(allocator *)&DAT_00102035);
  std::allocator<char>::~allocator(&local_249);
                    /* try { // try from 00101699 to 0010185f has its CatchHandler @ 00101b3d */
  pcVar2 = (char *)std::__cxx11::basic_string<>::operator[]((ulong)local_208);
  if (*pcVar2 < 'B') {
    std::__cxx11::basic_string<>::operator+=(local_248,local_c8);
  }
  pcVar2 = (char *)std::__cxx11::basic_string<>::operator[]((ulong)local_a8);
  if (*pcVar2 != 'A') {
    std::__cxx11::basic_string<>::operator+=(local_248,local_68);
  }
  pcVar2 = (char *)std::__cxx11::basic_string<>::operator[]((ulong)local_1c8);
  cVar1 = *pcVar2;
  pcVar2 = (char *)std::__cxx11::basic_string<>::operator[]((ulong)local_148);
  if ((int)cVar1 - (int)*pcVar2 == 3) {
    std::__cxx11::basic_string<>::operator+=(local_248,local_1c8);
  }
  std::__cxx11::basic_string<>::operator+=(local_248,local_1e8);
  std::__cxx11::basic_string<>::operator+=(local_248,local_188);
  pcVar2 = (char *)std::__cxx11::basic_string<>::operator[]((ulong)local_168);
  if (*pcVar2 == 'G') {
    std::__cxx11::basic_string<>::operator+=(local_248,local_168);
  }
  std::__cxx11::basic_string<>::operator+=(local_248,local_1a8);
  std::__cxx11::basic_string<>::operator+=(local_248,local_88);
  std::__cxx11::basic_string<>::operator+=(local_248,local_228);
  std::__cxx11::basic_string<>::operator+=(local_248,local_128);
                    /* } // end try from 00101699 to 0010185f */
  std::__cxx11::basic_string<>::operator+=(local_248,'}');
  std::__cxx11::basic_string<>::~basic_string(local_48);
  std::__cxx11::basic_string<>::~basic_string((basic_string<> *)local_68);
  std::__cxx11::basic_string<>::~basic_string((basic_string<> *)local_88);
  std::__cxx11::basic_string<>::~basic_string(local_a8);
  std::__cxx11::basic_string<>::~basic_string((basic_string<> *)local_c8);
  std::__cxx11::basic_string<>::~basic_string(local_e8);
  std::__cxx11::basic_string<>::~basic_string(local_108);
  std::__cxx11::basic_string<>::~basic_string((basic_string<> *)local_128);
  std::__cxx11::basic_string<>::~basic_string(local_148);
  std::__cxx11::basic_string<>::~basic_string((basic_string<> *)local_168);
  std::__cxx11::basic_string<>::~basic_string((basic_string<> *)local_188);
  std::__cxx11::basic_string<>::~basic_string((basic_string<> *)local_1a8);
  std::__cxx11::basic_string<>::~basic_string((basic_string<> *)local_1c8);
  std::__cxx11::basic_string<>::~basic_string((basic_string<> *)local_1e8);
  std::__cxx11::basic_string<>::~basic_string(local_208);
  std::__cxx11::basic_string<>::~basic_string((basic_string<> *)local_228);
  std::__cxx11::basic_string<>::~basic_string(local_248);
  if (local_20 == *(long *)(in_FS_OFFSET + 0x28)) {
    return 0;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

WinAntiDbg0x100(200ポイント)

Medium の 200ポイントの問題です。バイナリファイル(WinAntiDbg0x100.zip)が 1つダウンロードできます。パスワード付きの ZIPファイルで、パスワードを入力すると解凍できます。Windows の CUIプログラムのようです。config.bin というファイルも含まれていました。

WinAntiDbg0x100問題
WinAntiDbg0x100問題

表層解析を行います。Windowsプログラムですね。

$ file WinAntiDbg0x100.exe 
WinAntiDbg0x100.exe: PE32 executable (console) Intel 80386, for MS Windows, 5 sections

$ checksec --file=WinAntiDbg0x100.exe
Error: Not an ELF file: WinAntiDbg0x100.exe: PE32 executable (console) Intel 80386, for MS Windows, 5 sections

$ strings WinAntiDbg0x100.exe | grep pico

とりあえず、実行してみます。まず最初にデバッガを起動する必要があると言ってるでしょうか。

$ ./WinAntiDbg0x100.exe


        _            _____ _______ ______
       (_)          / ____|__   __|  ____|
  _ __  _  ___ ___ | |       | |  | |__
 | '_ \| |/ __/ _ \| |       | |  |  __|
 | |_) | | (_| (_) | |____   | |  | |
 | .__/|_|\___\___/ \_____|  |_|  |_|
 | |
 |_|
  Welcome to the Anti-Debug challenge!
### To start the challenge, you'll need to first launch this program using a debugger!

Windowsプログラムは全然分からないので、後回しにします。

Classic Crackme 0x100(300ポイント)

Medium の問題です。更新されたバイナリファイル(crackme100)が 1つダウンロードできます。また、最後はサーバを起動して実行する必要があるようです。

Classic Crackme 0x100問題
Classic Crackme 0x100問題

表層解析を行います。stringsコマンドでフラグが見えてますが、ローカルファイル用のフラグということでしょうか。

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

$ checksec --file=crackme100
RELRO          STACK CANARY     NX          PIE     RPATH     RUNPATH     Symbols     FORTIFY  Fortified  Fortifiable  FILE
Partial RELRO  No canary found  NX enabled  No PIE  No RPATH  No RUNPATH  40 Symbols  No       0          1            crackme100

$ strings crackme100 | grep pico
picoCTF{sample_flag}

まず、実行してみます。正しいパスワードを入力する必要がありそうです。

$ ./crackme100
Enter the secret password: aaa
FAILED!

Ghidra を使って、ソースを見ていきます。なんか正統派な問題って感じです。

二重ループのところを読み解いてみます。外側は 3回、内側は 配列変数の output の文字数なので 50回実行されそうです。

検討が長くなりそうなので、ソースコードの下に書いていきます。

int main(void)
{
  uint uVar1;
  int iVar2;
  size_t sVar3;
  char input [51];
  char output [51];
  int random2;
  int random1;
  char fix;
  int secret3;
  int secret2;
  int secret1;
  int len;
  int i_1;
  int i;
  
  output[0] = 'k';
  output[1] = 'g';
  output[2] = 'x';
  output[3] = 'm';
  output[4] = 'w';
  output[5] = 'p';
  output[6] = 'b';
  output[7] = 'p';
  output[8] = 'u';
  output[9] = 'q';
  output[10] = 't';
  output[0xb] = 'o';
  output[0xc] = 'r';
  output[0xd] = 'z';
  output[0xe] = 'a';
  output[0xf] = 'p';
  output[0x10] = 'j';
  output[0x11] = 'h';
  output[0x12] = 'f';
  output[0x13] = 'm';
  output[0x14] = 'e';
  output[0x15] = 'b';
  output[0x16] = 'm';
  output[0x17] = 'c';
  output[0x18] = 'c';
  output[0x19] = 'v';
  output[0x1a] = 'w';
  output[0x1b] = 'y';
  output[0x1c] = 'c';
  output[0x1d] = 'y';
  output[0x1e] = 'v';
  output[0x1f] = 'e';
  output[0x20] = 'w';
  output[0x21] = 'p';
  output[0x22] = 'x';
  output[0x23] = 'i';
  output[0x24] = 'h';
  output[0x25] = 'e';
  output[0x26] = 'i';
  output[0x27] = 'f';
  output[0x28] = 'v';
  output[0x29] = 'n';
  output[0x2a] = 'u';
  output[0x2b] = 'q';
  output[0x2c] = 's';
  output[0x2d] = 'r';
  output[0x2e] = 'g';
  output[0x2f] = 'e';
  output[0x30] = 'x';
  output[0x31] = 'l';
  output[0x32] = '\0';
  setvbuf(stdout,(char *)0x0,2,0);
  printf("Enter the secret password: ");
  __isoc99_scanf(&DAT_00402024,input);
  i = 0;
  sVar3 = strlen(output);
  for (; i < 3; i = i + 1) {
    for (i_1 = 0; i_1 < (int)sVar3; i_1 = i_1 + 1) {
      uVar1 = (i_1 % 0xff >> 1 & 0x55U) + (i_1 % 0xff & 0x55U);
      uVar1 = ((int)uVar1 >> 2 & 0x33U) + (uVar1 & 0x33);
      iVar2 = ((int)uVar1 >> 4) + input[i_1] + -0x61 + (uVar1 & 0xf);
      input[i_1] = (char)iVar2 + (char)(iVar2 / 0x1a) * -0x1a + 'a';
    }
  }
  iVar2 = memcmp(input,output,(long)(int)sVar3);
  if (iVar2 == 0) {
    printf("SUCCESS! Here is your flag: %s\n","picoCTF{sample_flag}");
  }
  else {
    puts("FAILED!");
  }
  return 0;
}

ループの内側の 4行を詳しく見ます。

1行目は、演算子の優先順位を正しく見る必要があるので括弧を付けます。また、i_1 は 0 から 49 をとるので、% 0xff は無視できます。

uVar1 = (((i_1 % 0xff) >> 1) & 0x55U) + ((i_1 % 0xff) & 0x55U);

よって、以下のように簡単にできます。

uVar1 = ((i_1 >> 1) & 0x55U) + (i_1 & 0x55U);

うーん、このやり方は無謀でした。やめます。

4行のうち、input 以外は値が決まっていることと、i は 4行に出てこないこと、ある input の計算に、他の input が関係しないことが分かります。つまり、ある input の場合に、この 4行を 3回連続でやった結果と同じです。プログラムで ASCIIコードを総当たりで計算するのがいいかもしれません。英小文字だけでいけそうですし。

Pythonスクリプトを実装します。

C言語から、Python に変換するだけでした。これを実行すると、正しいパスワードが表示されます。サーバで同じパスワードを入力すると、フラグが表示されました。

import os, sys

output = "kgxmwpbpuqtorzapjhfmebmccvwycyvewpxiheifvnuqsrgexl"

ret = []
for i_1, out in enumerate(output):
    
    tmps = [ aa for aa in range(0x21, 0x7f) ]
    #print( tmps )
    
    flag = False
    for tmp in tmps:
        input = tmp
        for ii in range(3):
            
            uVar1 = ((((i_1 % 0xff) >> 1)) & 0x55) + ((i_1 % 0xff) & 0x55)
            uVar1 = ((uVar1 >> 2) & 0x33) + (uVar1 & 0x33)
            iVar2 = (uVar1 >> 4) + input - 0x61 + (uVar1 & 0xf)
            input = (iVar2 & 0xff) - ((iVar2 // 0x1a) & 0xff) * (0x1a) + 0x61
        
        #print( f"out={out}, ord(out)={ord(out)}" )
        
        if input == ord( out ):
            ret.append( chr(tmp) )
            flag = True
            break
    
    assert flag, f"fail, ret={ret}"

print( f"ret={''.join(ret)}" )

やってみます。

$ python crackme100.py
ret=kdugtjvgrknflqrdgb`d_sdqwmnmtmjptjr`bv`tpelejfuprc

$ ./crackme100
Enter the secret password: kdugtjvgrknflqrdgb`d_sdqwmnmtmjptjr`bv`tpelejfuprc
SUCCESS! Here is your flag: picoCTF{sample_flag}

サーバに対して実施するとフラグが表示されます。

weirdSnake(300ポイント)

Medium の問題です。更新されたバイナリファイル(snake)が 1つダウンロードできます。

weirdSnake問題
weirdSnake問題

表層解析を行います。テキストファイルでした。

$ file snake 
snake: ASCII text

エディタで開いてみました。うーん、なんでしょうか。特徴的な名前(UNPACK_SEQUENCE)で、Web検索してみたところ、Python の公式サイトがヒットしました。Python のバイトコードの逆アセンブラらしいです。このアセンブラから Pythonスクリプトを構築する感じでしょうか。順番に見ていきます。

解析が長くなりそうなので、ソースコードの下に書いていきます。

  1           0 LOAD_CONST               0 (4)
              2 LOAD_CONST               1 (54)
              4 LOAD_CONST               2 (41)
              6 LOAD_CONST               3 (0)
              8 LOAD_CONST               4 (112)
             10 LOAD_CONST               5 (32)
             12 LOAD_CONST               6 (25)
             14 LOAD_CONST               7 (49)
             16 LOAD_CONST               8 (33)
             18 LOAD_CONST               9 (3)
             20 LOAD_CONST               3 (0)
             22 LOAD_CONST               3 (0)
             24 LOAD_CONST              10 (57)
             26 LOAD_CONST               5 (32)
             28 LOAD_CONST              11 (108)
             30 LOAD_CONST              12 (23)
             32 LOAD_CONST              13 (48)
             34 LOAD_CONST               0 (4)
             36 LOAD_CONST              14 (9)
             38 LOAD_CONST              15 (70)
             40 LOAD_CONST              16 (7)
             42 LOAD_CONST              17 (110)
             44 LOAD_CONST              18 (36)
             46 LOAD_CONST              19 (8)
             48 LOAD_CONST              11 (108)
             50 LOAD_CONST              16 (7)
             52 LOAD_CONST               7 (49)
             54 LOAD_CONST              20 (10)
             56 LOAD_CONST               0 (4)
             58 LOAD_CONST              21 (86)
             60 LOAD_CONST              22 (43)
             62 LOAD_CONST              17 (110)
             64 LOAD_CONST              22 (43)
             66 LOAD_CONST              23 (88)
             68 LOAD_CONST               3 (0)
             70 LOAD_CONST              24 (67)
             72 LOAD_CONST              25 (104)
             74 LOAD_CONST              26 (125)
             76 LOAD_CONST              14 (9)
             78 LOAD_CONST              27 (78)
             80 BUILD_LIST              40
             82 STORE_NAME               0 (input_list)

  2          84 LOAD_CONST              28 ('J')
             86 STORE_NAME               1 (key_str)

  3          88 LOAD_CONST              29 ('_')
             90 LOAD_NAME                1 (key_str)
             92 BINARY_ADD
             94 STORE_NAME               1 (key_str)

  4          96 LOAD_NAME                1 (key_str)
             98 LOAD_CONST              30 ('o')
            100 BINARY_ADD
            102 STORE_NAME               1 (key_str)

  5         104 LOAD_NAME                1 (key_str)
            106 LOAD_CONST              31 ('3')
            108 BINARY_ADD
            110 STORE_NAME               1 (key_str)

  6         112 LOAD_CONST              32 ('t')
            114 LOAD_NAME                1 (key_str)
            116 BINARY_ADD
            118 STORE_NAME               1 (key_str)

  9         120 LOAD_CONST              33 (<code object <listcomp> at 0x7ffb38066d40, file "snake.py", line 9>)
            122 LOAD_CONST              34 ('<listcomp>')
            124 MAKE_FUNCTION            0
            126 LOAD_NAME                1 (key_str)
            128 GET_ITER
            130 CALL_FUNCTION            1
            132 STORE_NAME               2 (key_list)

 11     >>  134 LOAD_NAME                3 (len)
            136 LOAD_NAME                2 (key_list)
            138 CALL_FUNCTION            1
            140 LOAD_NAME                3 (len)
            142 LOAD_NAME                0 (input_list)
            144 CALL_FUNCTION            1
            146 COMPARE_OP               0 (<)
            148 POP_JUMP_IF_FALSE      162

 12         150 LOAD_NAME                2 (key_list)
            152 LOAD_METHOD              4 (extend)
            154 LOAD_NAME                2 (key_list)
            156 CALL_METHOD              1
            158 POP_TOP
            160 JUMP_ABSOLUTE          134

 15     >>  162 LOAD_CONST              35 (<code object <listcomp> at 0x7ffb38066df0, file "snake.py", line 15>)
            164 LOAD_CONST              34 ('<listcomp>')
            166 MAKE_FUNCTION            0
            168 LOAD_NAME                5 (zip)
            170 LOAD_NAME                0 (input_list)
            172 LOAD_NAME                2 (key_list)
            174 CALL_FUNCTION            2
            176 GET_ITER
            178 CALL_FUNCTION            1
            180 STORE_NAME               6 (result)

 18         182 LOAD_CONST              36 ('')
            184 LOAD_METHOD              7 (join)
            186 LOAD_NAME                8 (map)
            188 LOAD_NAME                9 (chr)
            190 LOAD_NAME                6 (result)
            192 CALL_FUNCTION            2
            194 CALL_METHOD              1
            196 STORE_NAME              10 (result_text)
            198 LOAD_CONST              37 (None)
            200 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x7ffb38066d40, file "snake.py", line 9>:
  9           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                12 (to 18)
              6 STORE_FAST               1 (char)
              8 LOAD_GLOBAL              0 (ord)
             10 LOAD_FAST                1 (char)
             12 CALL_FUNCTION            1
             14 LIST_APPEND              2
             16 JUMP_ABSOLUTE            4
        >>   18 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x7ffb38066df0, file "snake.py", line 15>:
 15           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                16 (to 22)
              6 UNPACK_SEQUENCE          2
              8 STORE_FAST               1 (a)
             10 STORE_FAST               2 (b)
             12 LOAD_FAST                1 (a)
             14 LOAD_FAST                2 (b)
             16 BINARY_XOR
             18 LIST_APPEND              2
             20 JUMP_ABSOLUTE            4
        >>   22 RETURN_VALUE

1行目(左端の列の番号が Pythonスクリプトの行番号)は、40個の要素を持つ、input_list という名前のリストを作っているようです。括弧内が値です。

2行目から 6行目までは、おそらくセットで考えた方がよさそうです。以下でしょうか。

key_str = 'J'
key_str += '_'
key_str += 'o'
key_str += '3'
key_str += 't'

試しに、リストの作成と、上のコードの逆アセンブラを求めてみます。以下のようになりました。うーん、近いけど、ちょっと違いますね。まぁ、でもこれで仮決めします。

$ python -m dis hello_world.py
  0           0 RESUME                   0

  3           2 BUILD_LIST               0
              4 LOAD_CONST               0 ((4, 54, 41, 0, 112, 32, 25, 49, 33, 3, 0, 0, 57, 32, 108, 23, 48, 4, 9, 70, 7, 110, 36, 8, 108, 7, 49, 10, 4, 86, 43, 110, 43, 88, 0, 67, 104, 125, 9, 78))
              6 LIST_EXTEND              1
              8 STORE_NAME               0 (input_list)

  5          10 LOAD_CONST               1 ('J')
             12 STORE_NAME               1 (key_str)

  6          14 LOAD_NAME                1 (key_str)
             16 LOAD_CONST               2 ('_')
             18 BINARY_OP               13 (+=)
             22 STORE_NAME               1 (key_str)

  7          24 LOAD_NAME                1 (key_str)
             26 LOAD_CONST               3 ('o')
             28 BINARY_OP               13 (+=)
             32 STORE_NAME               1 (key_str)

  8          34 LOAD_NAME                1 (key_str)
             36 LOAD_CONST               4 ('3')
             38 BINARY_OP               13 (+=)
             42 STORE_NAME               1 (key_str)

  9          44 LOAD_NAME                1 (key_str)
             46 LOAD_CONST               5 ('t')
             48 BINARY_OP               13 (+=)
             52 STORE_NAME               1 (key_str)
             54 LOAD_CONST               6 (None)
             56 RETURN_VALUE

9行目は関数の生成で関数の実体は、下の方にある Disassembly から始まるところだと思います。同じく、15行目も関数の生成だと思います。

関数というか、リスト内包表記でしょうか。結果が key_list に入り、引数が key_str です。関数の実体を踏まえると、key_list = [ord(char) for char in key_str] のようになりそうです。

11行目は、if len(key_list) < len(input_list): だと思います。

12行目は、key_list.extend(key_list) でしょうか?

15行目もリスト内包表記と考えると、結果が result に入り、zip(input_list, key_list) の形で、関数の実体を踏まえると、result = [a ^ b for a, b in zip(input_list, key_list)]

最後の 18行目は、結果が result_text に入り、join関数を使って、result_text = ''.join(map(chr, result)) でしょうか。

以下のようになりました。

input_list = [4, 54, 41, 0, 112, 32, 25, 49, 33, 3, 0, 0, 57, 32, 108, 23, 48, 4, 9, 70, 7, 110, 36, 8, 108, 7, 49, 10, 4, 86, 43, 110, 43, 88, 0, 67, 104, 125, 9, 78]
key_str = 'J'
key_str += '_'
key_str += 'o'
key_str += '3'
key_str += 't'

key_list = [ord(char) for char in key_str]

if len(key_list) < len(input_list):
    key_list.extend(key_list)

result = [a ^ b for a, b in zip(input_list, key_list)]

result_text = ''.join(map(chr, result))

print(result_text)

これを逆アセンブルします。

$ python -m dis hello_world.py
  0           0 RESUME                   0

  1           2 BUILD_LIST               0
              4 LOAD_CONST               0 ((4, 54, 41, 0, 112, 32, 25, 49, 33, 3, 0, 0, 57, 32, 108, 23, 48, 4, 9, 70, 7, 110, 36, 8, 108, 7, 49, 10, 4, 86, 43, 110, 43, 88, 0, 67, 104, 125, 9, 78))
              6 LIST_EXTEND              1
              8 STORE_NAME               0 (input_list)

  2          10 LOAD_CONST               1 ('J')
             12 STORE_NAME               1 (key_str)

  3          14 LOAD_NAME                1 (key_str)
             16 LOAD_CONST               2 ('_')
             18 BINARY_OP               13 (+=)
             22 STORE_NAME               1 (key_str)

  4          24 LOAD_NAME                1 (key_str)
             26 LOAD_CONST               3 ('o')
             28 BINARY_OP               13 (+=)
             32 STORE_NAME               1 (key_str)

  5          34 LOAD_NAME                1 (key_str)
             36 LOAD_CONST               4 ('3')
             38 BINARY_OP               13 (+=)
             42 STORE_NAME               1 (key_str)

  6          44 LOAD_NAME                1 (key_str)
             46 LOAD_CONST               5 ('t')
             48 BINARY_OP               13 (+=)
             52 STORE_NAME               1 (key_str)

  9          54 LOAD_CONST               6 (<code object <listcomp> at 0x7fba221673c0, file "hello_world.py", line 9>)
             56 MAKE_FUNCTION            0
             58 LOAD_NAME                1 (key_str)
             60 GET_ITER
             62 PRECALL                  0
             66 CALL                     0
             76 STORE_NAME               2 (key_list)

 11          78 PUSH_NULL
             80 LOAD_NAME                3 (len)
             82 LOAD_NAME                2 (key_list)
             84 PRECALL                  1
             88 CALL                     1
             98 PUSH_NULL
            100 LOAD_NAME                3 (len)
            102 LOAD_NAME                0 (input_list)
            104 PRECALL                  1
            108 CALL                     1
            118 COMPARE_OP               0 (<)
            124 POP_JUMP_FORWARD_IF_FALSE    21 (to 168)

 12         126 LOAD_NAME                2 (key_list)
            128 LOAD_METHOD              4 (extend)
            150 LOAD_NAME                2 (key_list)
            152 PRECALL                  1
            156 CALL                     1
            166 POP_TOP

 15     >>  168 LOAD_CONST               7 (<code object <listcomp> at 0x7fba22177130, file "hello_world.py", line 15>)
            170 MAKE_FUNCTION            0
            172 PUSH_NULL
            174 LOAD_NAME                5 (zip)
            176 LOAD_NAME                0 (input_list)
            178 LOAD_NAME                2 (key_list)
            180 PRECALL                  2
            184 CALL                     2
            194 GET_ITER
            196 PRECALL                  0
            200 CALL                     0
            210 STORE_NAME               6 (result)

 18         212 LOAD_CONST               8 ('')
            214 LOAD_METHOD              7 (join)
            236 PUSH_NULL
            238 LOAD_NAME                8 (map)
            240 LOAD_NAME                9 (chr)
            242 LOAD_NAME                6 (result)
            244 PRECALL                  2
            248 CALL                     2
            258 PRECALL                  1
            262 CALL                     1
            272 STORE_NAME              10 (result_text)

 20         274 PUSH_NULL
            276 LOAD_NAME               11 (print)
            278 LOAD_NAME               10 (result_text)
            280 PRECALL                  1
            284 CALL                     1
            294 POP_TOP
            296 LOAD_CONST               9 (None)
            298 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x7fba221673c0, file "hello_world.py", line 9>:
  9           0 RESUME                   0
              2 BUILD_LIST               0
              4 LOAD_FAST                0 (.0)
        >>    6 FOR_ITER                17 (to 42)
              8 STORE_FAST               1 (char)
             10 LOAD_GLOBAL              1 (NULL + ord)
             22 LOAD_FAST                1 (char)
             24 PRECALL                  1
             28 CALL                     1
             38 LIST_APPEND              2
             40 JUMP_BACKWARD           18 (to 6)
        >>   42 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x7fba22177130, file "hello_world.py", line 15>:
 15           0 RESUME                   0
              2 BUILD_LIST               0
              4 LOAD_FAST                0 (.0)
        >>    6 FOR_ITER                10 (to 28)
              8 UNPACK_SEQUENCE          2
             12 STORE_FAST               1 (a)
             14 STORE_FAST               2 (b)
             16 LOAD_FAST                1 (a)
             18 LOAD_FAST                2 (b)
             20 BINARY_OP               12 (^)
             24 LIST_APPEND              2
             26 JUMP_BACKWARD           11 (to 6)
        >>   28 RETURN_VALUE

うーん、if のところがおかしいです。while に変えてみると、いい感じになってきました。

実行してみます。惜しい感じです。細かいところで間違えてそうです。

$ python hello_world.py
NiF3jF^wJ_V]ok:2M1K;Mne7"a1Dkt  7::

逆算でいきます。p(0x70)になるために、4 と J(0x4A)を使っていますが、結果は N(0x4E)になっています。4 はおそらく正しくて、J は怪しいです。4 と何かの XOR が 0x70 になるには、0x74(t)が必要です。key_str の最後が t です。key_str の作り方が間違っていそうです。

key_str を以下に修正しました。これだと、先頭に t がきます。

key_str = 'J'
key_str = '_' + key_str
key_str = 'o' + key_str
key_str = '3' + key_str
key_str = 't' + key_str

実行します。p だけ合ってます(笑)。

$ python hello_world.py
pF_:T*^~It3V&ckV
                s]KW&se[_]DJ7[V

key_str のところは、今は、BAINARY_OP (+) となってますが、問題文では、BAINARY_ADD なんですよね、ここが違いそうです。いや、J_t は、左オペランドですが、o3 は右オペランドになってます!

それを修正すると、、、フラグゲットです!300ポイントなのにだいぶ苦労しました。

Pythonスクリプトの最終版です。

input_list = [4, 54, 41, 0, 112, 32, 25, 49, 33, 3, 0, 0, 57, 32, 108, 23, 48, 4, 9, 70, 7, 110, 36, 8, 108, 7, 49, 10, 4, 86, 43, 110, 43, 88, 0, 67, 104, 125, 9, 78]
key_str = 'J'
key_str = '_' + key_str
key_str = key_str + 'o'
key_str = key_str + '3'
key_str = 't' + key_str


key_list = [ord(char) for char in key_str]
print(key_list)
while len(key_list) < len(input_list):
    key_list.extend(key_list)


result = [a ^ b for a, b in zip(input_list, key_list)]


result_text = ''.join(map(chr, result))

print(result_text)

実行します。

$ python snake.py
[116, 95, 74, 111, 51]
picoCTF{N0t_sO_coNfus1ng_sn@ke_1a73777f}

WinAntiDbg0x200(300ポイント)

Medium の 300ポイントの問題です。バイナリファイル(WinAntiDbg0x200.zip)が 1つダウンロードできます。パスワード付きの ZIPファイルで、パスワードを入力すると解凍できます。Windows の CUIプログラムのようです。config.bin(上の WinAntiDbg0x100 と同じファイル名なので注意です!)というファイルも含まれていました。

WinAntiDbg0x200問題
WinAntiDbg0x200問題

一応、表層解析を行います。やはり、Windows の CUIプログラムのようです。

$ file WinAntiDbg0x200.exe
WinAntiDbg0x200.exe: PE32 executable (console) Intel 80386, for MS Windows, 5 sections

Windowsプログラムは後回しにします。

WinAntiDbg0x300(400ポイント)

Medium の 400ポイントの問題です。バイナリファイル(WinAntiDbg0x300.zip)が 1つダウンロードできます。パスワード付きの ZIPファイルで、パスワードを入力すると解凍できます。Windowsプログラムのようです。config.bin(上の WinAntiDbg0x100 や WinAntiDbg0x200 と同じファイル名なので注意です!)というファイルと、WinAntiDbg0x300.pdb も含まれていました。

WinAntiDbg0x300問題
WinAntiDbg0x300問題

一応、表層解析を行います。Windows の GUIプログラムのようです。

$ file WinAntiDbg0x300.exe
WinAntiDbg0x300.exe: PE32 executable (GUI) Intel 80386, for MS Windows, UPX compressed, 3 sections

$ file WinAntiDbg0x300.pdb
WinAntiDbg0x300.pdb: MSVC program database ver 7.00, 4096*215 bytes

Windowsプログラムは後回しにします。

おわりに

今回は、picoCTF の picoCTF 2024 のうち、Reverse Engineering というカテゴリの全7問のうち、Windowsプログラムの 3問を除き、4問をやりました。4問とも解けたので良かったです。

次は、picoCTF 2024 の General Skills か、Web Exploitation に挑戦してみたいと思います。

最後になりましたが、エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

今回は以上です!

最後までお読みいただき、ありがとうございました。




以上の内容はhttps://daisuke20240310.hatenablog.com/entry/picoctf2024reより取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14