プログラム系統備忘録ブログ

記事中のコードは自己責任の下でご自由にどうぞ。

sknbCTF 2025 Rev全5問write-up

sknbCTF 2025へ参加しました。そのwrite-up記事です。

コンテスト概要

2025/11/22(土) 12:00 +09:00 - 11/23(日) 12:00 +09:00の24時間開催でした。他ルールはトップページから引用します:

# CTFルール
1. flagフォーマットはsknb{flag}です
2. チームサイズに制限はありません
3. スコアは動的に計算されます
4. DoS攻撃を含む大会インフラへの攻撃及び必要以上の負荷をかける行為は禁止されています
5. チームは単独で参加する必要があり、他チームとのflagや解法の共有は禁止されています

# Generalルール
1. 全ての人に敬意を持ちコミュニケーションを行ってください。嫌がらせや差別は許容されません
2. CTFが終了するまでチーム内以外での解法及びそれに繋がりかねない情報の共有は禁止されています
3. 上記のルールに違反した場合、もしくは運営が不適切と判断した場合失格となる可能性があります
4. 全ての問題に関する運営の決定は最終的なものです
5. 当CTFで発生した損害について、運営は一切の責任を負いません
6. 何よりも、楽しんでください!

ヘルプが必要な場合や問題に不備がある場合はチケットを開き運営に連絡してください

(英語版表記は省略します)

結果

Revジャンルを全問正解できました!

環境

主にWindowsのWSL2(Ubuntu 24.04)を使って取り組みました。

Windows(ホスト)

c:\>ver

Microsoft Windows [Version 10.0.26200.7171]

c:\>wsl -l -v
  NAME            STATE           VERSION
* Ubuntu-24.04    Running         2
  Ubuntu-22.04    Stopped         2

c:\>

他ソフト

  • IDA Version 9.2.250908 Windows x64 (64-bit address size)

WSL2(Ubuntu 24.04)

$ cat /proc/version
Linux version 6.6.87.2-microsoft-standard-WSL2 (root@439a258ad544) (gcc (GCC) 11.2.0, GNU ld (GNU Binutils) 2.37) #1 SMP PREEMPT_DYNAMIC Thu Jun  5 18:30:46 UTC 2025
$ cat /etc/os-release
PRETTY_NAME="Ubuntu 24.04.3 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04.3 LTS (Noble Numbat)"
VERSION_CODENAME=noble
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=noble
LOGO=ubuntu-logo
$ python3 --version
Python 3.12.3
$ python3 -m pip show pip | grep Version:
Version: 24.0
$ python3 -m pip show pwntools | grep Version:
Version: 4.15.0
$ python3 -m pip show numpy | grep Version:
Version: 1.26.4
$ python3 -m pip show opencv-python | grep Version:
Version: 4.9.0.80
$ python3 -m pip show tqdm | grep Version:
Version: 4.66.4
$ g++ --version
g++ (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ docker --version
Docker version 29.0.2, build 8108357
$

解けた問題

一部の問題では、日本語と英語の両方で問題文が記述されていました。本記事では日本語版の問題文のみを引用します。

[Rev, medium] GIF-Zipper (78 teams solves, 100 points)

GIFファイルを暗号化してみた!秘密の情報を隠したし誰も見ないで欲しいな...

配布ファイルとして、次のファイルがありました:

./Debug/flag.gifzip:    ASCII text, with very long lines (65512), with no line terminators
./Debug/GIF-zipper.exe: PE32+ executable (console) x86-64, for MS Windows, 11 sections
./Debug/GIF-zipper.pdb: MSVC program database ver 7.00, 4096*763 bytes

EXEファイルのみならず、珍しくPDBファイルも与えられています!また、どうやらDebugビルドのようです。DebugビルドかつPDBファイルによるシンボル情報があるということで、IDAでの逆コンパイル結果にもローカル変数名が含まれており、元コードとかなり近い内容に見受けられました:

__int64 __fastcall main()
{
  char *v0; // rdi
  __int64 i; // rcx
  std::ostream *v3; // rax
  std::ostream *v4; // rax
  std::ostream *v5; // rax
  char v6; // [rsp+30h] [rbp+0h] BYREF
  int width[8]; // [rsp+34h] [rbp+4h] BYREF
  int height[8]; // [rsp+54h] [rbp+24h] BYREF
  int channels[9]; // [rsp+74h] [rbp+44h] BYREF
  unsigned __int8 *data; // [rsp+98h] [rbp+68h]
  std::ofstream outfile; // [rsp+C0h] [rbp+90h] BYREF
  int y; // [rsp+1E4h] [rbp+1B4h]
  int x; // [rsp+204h] [rbp+1D4h]
  int index; // [rsp+224h] [rbp+1F4h]
  unsigned __int8 r; // [rsp+244h] [rbp+214h]
  unsigned __int8 g; // [rsp+264h] [rbp+234h]
  unsigned __int8 b; // [rsp+284h] [rbp+254h]
  unsigned int v18; // [rsp+424h] [rbp+3F4h]

  v0 = &v6;
  for ( i = 166; i; --i )
  {
    *(_DWORD *)v0 = 0xCCCCCCCC;
    v0 += 4;
  }
  j___CheckForDebuggerJustMyCode(&_3E79CF4D_GIF_zipper_cpp);
  data = j_stbi_load("flag.gif", width, height, channels, 3);
  if ( data )
  {
    std::ofstream::__autoclassinit2(&outfile, 0x108u);
    std::ofstream::ofstream(&outfile, "flag.gifzip", 2, 64);
    if ( std::ofstream::is_open(&outfile) )
    {
      v3 = (std::ostream *)std::ostream::operator<<(&outfile, (unsigned int)width[0]);
      v4 = std::operator<<<std::char_traits<char>>(v3, ",");
      v5 = (std::ostream *)std::ostream::operator<<(v4, (unsigned int)height[0]);
      std::operator<<<std::char_traits<char>>(v5, ",");
      for ( y = 0; y < height[0] && y != height[0] - 1; ++y )
      {
        for ( x = 0; x < width[0]; ++x )
        {
          index = 3 * (x + width[0] * y);
          r = data[index];
          g = data[index + 1];
          b = data[index + 2];
          std::ostream::operator<<(&outfile, r << (g + 16) << (b + 8));
          if ( x < width[0] - 1 )
            std::operator<<<std::char_traits<char>>(&outfile, ", ");
        }
        if ( y < height[0] - 2 )
          std::operator<<<std::char_traits<char>>(&outfile, ", ");
      }
    }
    std::ofstream::close(&outfile);
    remove("flag.gif");
    j_stbi_image_free(data);
    v18 = 0;
    std::ofstream::`vbase destructor'(&outfile);
    return v18;
  }
  else
  {
    std::operator<<<std::char_traits<char>>(std::cerr, "Failed to load image\n");
    return 1;
  }
}

入力画像の各ピクセルを、RGB成分を加工して数値出力する内容のようです。最初はr << (g + 16) << (b + 8)箇所が逆算不能に思えたので困っていましたが、加工結果のflag.gifzipファイルを見ると各ピクセルは2種類の値のみらしいことが分かりました。画像として復元してみるコードを書きました:

#!/usr/bin/env python3

import collections.abc

import cv2
import numpy as np


def number_stream() -> collections.abc.Iterator[int]:
    with open("flag.gifzip") as f:
        data = f.read()
    for line in data.splitlines():
        for d in line.split(","):
            d = d.strip()
            if not d:
                continue
            yield int(d)


generator = number_stream()

# 「for ( y = 0; y < height[0] && y != height[0] - 1; ++y )」ループなので1行少ない
width = next(generator)
height = next(generator) - 1

print(width)
print(height)

image = np.zeros((height, width, 3), np.uint8)

for y in range(height):
    for x in range(width):
        rgb = next(generator)
        assert rgb == 0x38C000 or rgb == 0x1000000
        for c in range(3):
            image[y, x, c] = 0xFF if rgb == 0x38C000 else 0

cv2.imwrite("result.png", image)

実行結果は次の画像でした:

QRコードでした。最近のWindowsではSnipping ToolsでQRコードを読み込めて便利です:

QRコードの読み込み結果を提出すると正解でした: sknb{n0t-zip_6ut-hidd3n-1ittl3?}

[Rev, easy] Fubukkit (54 teams solves, 100 points)

マイクラのプラグイン問に慣れよう! https://www.spigotmc.org/resources/ultimateadvancementapi-1-15-1-21-10.95585/ 1.21.5で動作確認済み(固まっても許して~)

配布ファイルとして、Fubukkit-1.0-SNAPSHOT.jarがありました:

$ file *
Fubukkit-1.0-SNAPSHOT.jar: Zip archive data, at least v2.0 to extract, compression method=deflate
$

申し訳ないですが、私はMinecraftを持っていません。skylot/jadx: Dex to Java decompilerjadx-gui-1.5.3-with-jre-win.zip版をダウンロードして実行して、配布ファイルを逆コンパイルして眺めていると、AdvancementManager型の次の処理が見つかりました:

    // 前略
    private void generateAdvancements() {
        this.tab = this.API.createAdvancementTab("chal");
        RootAdvancement root = new RootAdvancement(this.tab, "root", new AdvancementDisplay(Material.GRASS_BLOCK, "sknb chal!!!!", AdvancementFrameType.TASK, true, false, 0.0f, 7.5f, new String[]{"/flag <flag>"}), "textures/block/yellow_wool.png");
        BaseAdvancement v0_0 = new CustomAdvancement("0_0", new AdvancementDisplay(Material.CLAY_BALL, "hi!", AdvancementFrameType.TASK, false, false, 1.0f, 0.0f, new String[0]), root);
        BaseAdvancement v0_1 = new CustomAdvancement("0_1", new AdvancementDisplay(Material.COAL, "hi!", AdvancementFrameType.TASK, false, false, 1.0f, 1.0f, new String[0]), root);
        // 中略
        BaseAdvancement v43_15 = new CustomAdvancement("43_15", new AdvancementDisplay(Material.NETHERITE_INGOT, "hi!", AdvancementFrameType.TASK, false, false, 44.0f, 15.0f, new String[0]), v42_7);
        BaseAdvancement goal = new CustomAdvancement("goal", new AdvancementDisplay(head(), "Correct!", AdvancementFrameType.CHALLENGE, true, false, 45.0f, 7.5f, new String[0]), v43_13);
        this.tab.registerAdvancements(root, new BaseAdvancement[]{v0_0, v1_0, /* 中略 */, v43_15, goal});
    }
    // 中略
    private byte[] split(byte[] bytes) {
        byte[] res = new byte[bytes.length * 2];
        for (int i = 0; i < bytes.length; i++) {
            res[2 * i] = (byte) ((bytes[i] & 240) >> 4);
            res[(2 * i) + 1] = (byte) (bytes[i] & 15);
        }
        return res;
    }
    // 中略
    public void test(final Player player, String flag) {
        // 中略
        final byte[] data = split(flag.getBytes(StandardCharsets.UTF_8));
        if (44 < data.length) {
            fail(player);
            return;
        }
        tryGrant(player, "chal:root");
        player.sendMessage(String.valueOf(ChatColor.YELLOW) + "You can view your progress in the Advancements screen.");
        Bukkit.getScheduler().runTaskTimer(this.plugin, new Runnable() { // from class: io.github.koufu193.fubukkit.AdvancementManager.1
            private int index = 0;

            @Override // java.lang.Runnable
            public void run() {
                try {
                    if (this.index < data.length) {
                        if (!AdvancementManager.this.tryGrant(player, "chal:%d_%d".formatted(Integer.valueOf(this.index), Byte.valueOf(data[this.index])))) {
                            AdvancementManager.this.fail(player);
                            return;
                        }
                        player.playSound(player, Sound.BLOCK_NOTE_BLOCK_BELL, 2.0f, 1.0f);
                    } else if (this.index == data.length) {
                        if (!AdvancementManager.this.tryGrant(player, "chal:goal")) {
                            AdvancementManager.this.fail(player);
                        } else {
                            player.playSound(player, Sound.ENTITY_FIREWORK_ROCKET_TWINKLE_FAR, 2.0f, 1.0f);
                            player.sendTitle(String.valueOf(ChatColor.GREEN) + "Correct!", "", 5, 40, 5);
                            player.sendMessage(String.valueOf(ChatColor.GREEN) + "Correct!");
                        }
                    } else {
                        Bukkit.getScheduler().cancelTasks(AdvancementManager.this.plugin);
                    }
                    this.index++;
                } catch (Exception e) {
                    Bukkit.broadcastMessage("An error occurred");
                    Bukkit.getScheduler().cancelTasks(AdvancementManager.this.plugin);
                }
            }
        }, SLEEP_TICKS, SLEEP_TICKS);
    }
    // 後略

どうやら、「初期化処理のgenerateAdvancementsメソッドで実績のような何かの木構造を構築して、フラグ入力をsplit関数でnibble単位に分割してから、実績のようなものを獲得していく」流れのようです。実績のようなものの1つにgoalとあるので、goalへ到達する入力が正解フラグのようでした。

木構造をgoalからrootへ辿って、正解となる入力を求めるソルバーを書きました。木構造を構築するJavaコードが700行もあるので、ソルバーでも中略します:

#!/usr/bin/env python3
import re

# jadxでの逆コンパイル結果をコピペ
code = """
        RootAdvancement root = new RootAdvancement(this.tab, "root", new AdvancementDisplay(Material.GRASS_BLOCK, "sknb chal!!!!", AdvancementFrameType.TASK, true, false, 0.0f, 7.5f, new String[]{"/flag <flag>"}), "textures/block/yellow_wool.png");
        BaseAdvancement v0_0 = new CustomAdvancement("0_0", new AdvancementDisplay(Material.CLAY_BALL, "hi!", AdvancementFrameType.TASK, false, false, 1.0f, 0.0f, new String[0]), root);
        BaseAdvancement v0_1 = new CustomAdvancement("0_1", new AdvancementDisplay(Material.COAL, "hi!", AdvancementFrameType.TASK, false, false, 1.0f, 1.0f, new String[0]), root);
        # 中略
        BaseAdvancement v43_15 = new CustomAdvancement("43_15", new AdvancementDisplay(Material.NETHERITE_INGOT, "hi!", AdvancementFrameType.TASK, false, false, 44.0f, 15.0f, new String[0]), v42_7);
        BaseAdvancement goal = new CustomAdvancement("goal", new AdvancementDisplay(head(), "Correct!", AdvancementFrameType.CHALLENGE, true, false, 45.0f, 7.5f, new String[0]), v43_13);
"""
code_lines = code.splitlines()


def search_target_list():
    target_list = []
    target = "goal"
    while True:
        # print(target)

        for index, line in enumerate(code_lines):
            pos = line.find(target)
            if pos < 0:
                continue

            print(line)
            m = re.search(r"(v[0-9_]+)\);$", line)
            if m is None:
                return target_list[::-1]
            assert m
            target = m.group(1)
            target_list.append(target)
            break


def get_index(name: str) -> int:
    m = re.search(r"^v([0-9]+)_([0-9]+)$", name)
    assert m
    return int(m.group(2))


target_list = search_target_list()
print(f"{len(target_list) = }")
for i in range(0, len(target_list), 2):
    upper = get_index(target_list[i])
    lower = get_index(target_list[i + 1])
    print(chr(upper << 4 | lower), end="")
print()

実行しました:

$ ./solve.py
        BaseAdvancement goal = new CustomAdvancement("goal", new AdvancementDisplay(head(), "Correct!", AdvancementFrameType.CHALLENGE, true, false, 45.0f, 7.5f, new String[0]), v43_13);
        BaseAdvancement v43_13 = new CustomAdvancement("43_13", new AdvancementDisplay(Material.QUARTZ, "hi!", AdvancementFrameType.TASK, false, false, 44.0f, 13.0f, new String[0]), v42_7);
        BaseAdvancement v42_7 = new CustomAdvancement("42_7", new AdvancementDisplay(Material.EMERALD, "hi!", AdvancementFrameType.TASK, false, false, 43.0f, 7.0f, new String[0]), v41_4);
        BaseAdvancement v41_4 = new CustomAdvancement("41_4", new AdvancementDisplay(Material.GOLD_INGOT, "hi!", AdvancementFrameType.TASK, false, false, 42.0f, 4.0f, new String[0]), v40_7);
        BaseAdvancement v40_7 = new CustomAdvancement("40_7", new AdvancementDisplay(Material.EMERALD, "hi!", AdvancementFrameType.TASK, false, false, 41.0f, 7.0f, new String[0]), v39_9);
        BaseAdvancement v39_9 = new CustomAdvancement("39_9", new AdvancementDisplay(Material.PRISMARINE_SHARD, "hi!", AdvancementFrameType.TASK, false, false, 40.0f, 9.0f, new String[0]), v38_6);
        BaseAdvancement v38_6 = new CustomAdvancement("38_6", new AdvancementDisplay(Material.LAPIS_LAZULI, "hi!", AdvancementFrameType.TASK, false, false, 39.0f, 6.0f, new String[0]), v37_11);
        BaseAdvancement v37_11 = new CustomAdvancement("37_11", new AdvancementDisplay(Material.GLOWSTONE_DUST, "hi!", AdvancementFrameType.TASK, false, false, 38.0f, 11.0f, new String[0]), v36_6);
        BaseAdvancement v36_6 = new CustomAdvancement("36_6", new AdvancementDisplay(Material.LAPIS_LAZULI, "hi!", AdvancementFrameType.TASK, false, false, 37.0f, 6.0f, new String[0]), v35_11);
        BaseAdvancement v35_11 = new CustomAdvancement("35_11", new AdvancementDisplay(Material.GLOWSTONE_DUST, "hi!", AdvancementFrameType.TASK, false, false, 36.0f, 11.0f, new String[0]), v34_6);
        BaseAdvancement v34_6 = new CustomAdvancement("34_6", new AdvancementDisplay(Material.LAPIS_LAZULI, "hi!", AdvancementFrameType.TASK, false, false, 35.0f, 6.0f, new String[0]), v33_1);
        BaseAdvancement v33_1 = new CustomAdvancement("33_1", new AdvancementDisplay(Material.COAL, "hi!", AdvancementFrameType.TASK, false, false, 34.0f, 1.0f, new String[0]), v32_6);
        BaseAdvancement v32_6 = new CustomAdvancement("32_6", new AdvancementDisplay(Material.LAPIS_LAZULI, "hi!", AdvancementFrameType.TASK, false, false, 33.0f, 6.0f, new String[0]), v31_2);
        BaseAdvancement v31_2 = new CustomAdvancement("31_2", new AdvancementDisplay(Material.IRON_INGOT, "hi!", AdvancementFrameType.TASK, false, false, 32.0f, 2.0f, new String[0]), v30_6);
        BaseAdvancement v30_6 = new CustomAdvancement("30_6", new AdvancementDisplay(Material.LAPIS_LAZULI, "hi!", AdvancementFrameType.TASK, false, false, 31.0f, 6.0f, new String[0]), v29_15);
        BaseAdvancement v29_15 = new CustomAdvancement("29_15", new AdvancementDisplay(Material.NETHERITE_INGOT, "hi!", AdvancementFrameType.TASK, false, false, 30.0f, 15.0f, new String[0]), v28_5);
        BaseAdvancement v28_5 = new CustomAdvancement("28_5", new AdvancementDisplay(Material.REDSTONE, "hi!", AdvancementFrameType.TASK, false, false, 29.0f, 5.0f, new String[0]), v27_15);
        BaseAdvancement v27_15 = new CustomAdvancement("27_15", new AdvancementDisplay(Material.NETHERITE_INGOT, "hi!", AdvancementFrameType.TASK, false, false, 28.0f, 15.0f, new String[0]), v26_6);
        BaseAdvancement v26_6 = new CustomAdvancement("26_6", new AdvancementDisplay(Material.LAPIS_LAZULI, "hi!", AdvancementFrameType.TASK, false, false, 27.0f, 6.0f, new String[0]), v25_7);
        BaseAdvancement v25_7 = new CustomAdvancement("25_7", new AdvancementDisplay(Material.EMERALD, "hi!", AdvancementFrameType.TASK, false, false, 26.0f, 7.0f, new String[0]), v24_7);
        BaseAdvancement v24_7 = new CustomAdvancement("24_7", new AdvancementDisplay(Material.EMERALD, "hi!", AdvancementFrameType.TASK, false, false, 25.0f, 7.0f, new String[0]), v23_15);
        BaseAdvancement v23_15 = new CustomAdvancement("23_15", new AdvancementDisplay(Material.NETHERITE_INGOT, "hi!", AdvancementFrameType.TASK, false, false, 24.0f, 15.0f, new String[0]), v22_5);
        BaseAdvancement v22_5 = new CustomAdvancement("22_5", new AdvancementDisplay(Material.REDSTONE, "hi!", AdvancementFrameType.TASK, false, false, 23.0f, 5.0f, new String[0]), v21_4);
        BaseAdvancement v21_4 = new CustomAdvancement("21_4", new AdvancementDisplay(Material.GOLD_INGOT, "hi!", AdvancementFrameType.TASK, false, false, 22.0f, 4.0f, new String[0]), v20_7);
        BaseAdvancement v20_7 = new CustomAdvancement("20_7", new AdvancementDisplay(Material.EMERALD, "hi!", AdvancementFrameType.TASK, false, false, 21.0f, 7.0f, new String[0]), v19_9);
        BaseAdvancement v19_9 = new CustomAdvancement("19_9", new AdvancementDisplay(Material.PRISMARINE_SHARD, "hi!", AdvancementFrameType.TASK, false, false, 20.0f, 9.0f, new String[0]), v18_6);
        BaseAdvancement v18_6 = new CustomAdvancement("18_6", new AdvancementDisplay(Material.LAPIS_LAZULI, "hi!", AdvancementFrameType.TASK, false, false, 19.0f, 6.0f, new String[0]), v17_11);
        BaseAdvancement v17_11 = new CustomAdvancement("17_11", new AdvancementDisplay(Material.GLOWSTONE_DUST, "hi!", AdvancementFrameType.TASK, false, false, 18.0f, 11.0f, new String[0]), v16_6);
        BaseAdvancement v16_6 = new CustomAdvancement("16_6", new AdvancementDisplay(Material.LAPIS_LAZULI, "hi!", AdvancementFrameType.TASK, false, false, 17.0f, 6.0f, new String[0]), v15_11);
        BaseAdvancement v15_11 = new CustomAdvancement("15_11", new AdvancementDisplay(Material.GLOWSTONE_DUST, "hi!", AdvancementFrameType.TASK, false, false, 16.0f, 11.0f, new String[0]), v14_6);
        BaseAdvancement v14_6 = new CustomAdvancement("14_6", new AdvancementDisplay(Material.LAPIS_LAZULI, "hi!", AdvancementFrameType.TASK, false, false, 15.0f, 6.0f, new String[0]), v13_5);
        BaseAdvancement v13_5 = new CustomAdvancement("13_5", new AdvancementDisplay(Material.REDSTONE, "hi!", AdvancementFrameType.TASK, false, false, 14.0f, 5.0f, new String[0]), v12_7);
        BaseAdvancement v12_7 = new CustomAdvancement("12_7", new AdvancementDisplay(Material.EMERALD, "hi!", AdvancementFrameType.TASK, false, false, 13.0f, 7.0f, new String[0]), v11_2);
        BaseAdvancement v11_2 = new CustomAdvancement("11_2", new AdvancementDisplay(Material.IRON_INGOT, "hi!", AdvancementFrameType.TASK, false, false, 12.0f, 2.0f, new String[0]), v10_6);
        BaseAdvancement v10_6 = new CustomAdvancement("10_6", new AdvancementDisplay(Material.LAPIS_LAZULI, "hi!", AdvancementFrameType.TASK, false, false, 11.0f, 6.0f, new String[0]), v9_11);
        BaseAdvancement v9_11 = new CustomAdvancement("9_11", new AdvancementDisplay(Material.GLOWSTONE_DUST, "hi!", AdvancementFrameType.TASK, false, false, 10.0f, 11.0f, new String[0]), v8_7);
        BaseAdvancement v8_7 = new CustomAdvancement("8_7", new AdvancementDisplay(Material.EMERALD, "hi!", AdvancementFrameType.TASK, false, false, 9.0f, 7.0f, new String[0]), v7_2);
        BaseAdvancement v7_2 = new CustomAdvancement("7_2", new AdvancementDisplay(Material.IRON_INGOT, "hi!", AdvancementFrameType.TASK, false, false, 8.0f, 2.0f, new String[0]), v6_6);
        BaseAdvancement v6_6 = new CustomAdvancement("6_6", new AdvancementDisplay(Material.LAPIS_LAZULI, "hi!", AdvancementFrameType.TASK, false, false, 7.0f, 6.0f, new String[0]), v5_14);
        BaseAdvancement v5_14 = new CustomAdvancement("5_14", new AdvancementDisplay(Material.DIAMOND, "hi!", AdvancementFrameType.TASK, false, false, 6.0f, 14.0f, new String[0]), v4_6);
        BaseAdvancement v4_6 = new CustomAdvancement("4_6", new AdvancementDisplay(Material.LAPIS_LAZULI, "hi!", AdvancementFrameType.TASK, false, false, 5.0f, 6.0f, new String[0]), v3_11);
        BaseAdvancement v3_11 = new CustomAdvancement("3_11", new AdvancementDisplay(Material.GLOWSTONE_DUST, "hi!", AdvancementFrameType.TASK, false, false, 4.0f, 11.0f, new String[0]), v2_6);
        BaseAdvancement v2_6 = new CustomAdvancement("2_6", new AdvancementDisplay(Material.LAPIS_LAZULI, "hi!", AdvancementFrameType.TASK, false, false, 3.0f, 6.0f, new String[0]), v1_3);
        BaseAdvancement v1_3 = new CustomAdvancement("1_3", new AdvancementDisplay(Material.COPPER_INGOT, "hi!", AdvancementFrameType.TASK, false, false, 2.0f, 3.0f, new String[0]), v0_7);
        BaseAdvancement v0_7 = new CustomAdvancement("0_7", new AdvancementDisplay(Material.EMERALD, "hi!", AdvancementFrameType.TASK, false, false, 1.0f, 7.0f, new String[0]), root);
len(target_list) = 44
sknb{bukkit_wo_bakkit}
$

フラグを入手できました: sknb{bukkit_wo_bakkit}

[Rev, medium] 風が吹けば桶屋が儲かる (46 teams solves, 176 points)

微小な変化の詰め合わせ

日本語タイトルの問題です。配布ファイルとして、1264個ものclassファイルがありました!

$ file *
Chal0000.class: compiled Java class data, version 67.0
中略
Chal1263.class: compiled Java class data, version 67.0
$ sha256sum *.class | cut -d' ' -f 1 | sort | uniq -c | wc -l
1264
$

SHA256がすべて異なるというわけで、問題文通りに各種ファイルには微小な変化があるようでした。さて、classファイルのままではよく分からないので逆コンパイルをしたくなりました。調べると、上述のskylot/jadx: Dex to Java decompilerに含まれている、jadx-1.5.3.zip側を使うとCUIでのHeadlessな逆コンパイルができました:

$ time /path/to/jadx-1.5.3/bin/jadx Chal0000.class
INFO  - loading ...
INFO  - processing ...
INFO  - done
/path/to/jadx-1.5.3/bin/jadx Chal0000.class  2.77s user 0.40s system 162% cpu 1.956 total
$ find Chal0000 -type f
Chal0000/sources/defpackage/Chal0000.java
$

1個のclassファイルを約3秒でJavaコードへ逆コンパイルできました。逆コンパイル結果は次のようなコードでした:

package defpackage;

import java.io.IOException;
import java.util.Arrays;

/* loaded from: Chal0000.class */
public class Chal0000 {
    private static final byte[] enc = {65, 11, -48, 103, 77, 19, -80, -1};

    public static void main(String[] strArr) throws IOException {
        System.out.print("msg:");
        byte[] nBytes = System.in.readNBytes(8);
        int[] iArr = {120, 187, 228, 254, 207, 89, 242, 108};
        for (int i = 0; i < 8; i++) {
            int i2 = i;
            nBytes[i2] = (byte) (nBytes[i2] ^ ((byte) ((iArr[i] >> 3) | (iArr[i] << 5))));
        }
        for (int i3 = 0; i3 < 8; i3++) {
            int i4 = i3;
            nBytes[i4] = (byte) (nBytes[i4] + ((byte) iArr[(i3 + 1) % 8]));
        }
        if (Arrays.equals(enc, nBytes)) {
            System.out.println("Correct");
        } else {
            System.out.println("Incorrect");
        }
    }
}

1つのclassファイルが、8バイトのCrackMeを表すようです。いくつかの逆コンパイル結果を見比べると、次の4箇所に変化が起きるらしいことが分かりました:

  • enc配列の各種値。
  • iArr配列の各種値。
  • nBytes[i2] =行の、iArr[i]をRotateRight8するビット数。上の例で3です。
  • nBytes[i4] =行の、i3 +右辺の数値。上の例では1です。

逆コンパイル済みの各種Javaコードを読み込み、変化箇所4箇所をパースして、正解となる入力を逆算するソルバーを書きました:

#!/usr/bin/env python3

import ast
import pathlib

import tqdm


def rotate_right_8(value: int, bit: int) -> int:
    assert 0 <= value < 256
    v = (value >> bit) | (value << (8 - bit))
    return v & 0xFF


# "{1, 2, 3}" -> [1, 2, 3]
def parse_java_array(code: str) -> list[int]:
    return ast.literal_eval(code.replace("{", "[").replace("}", "]"))


def extract(code: str, line_prefix: str, delimiter: str) -> str:
    pos = code.find(line_prefix)
    assert pos >= 0
    postDelimiter = code.find(delimiter, pos + len(line_prefix))
    assert postDelimiter >= 0
    return code[pos + len(line_prefix) : postDelimiter]


def solve_one_class(javaFilePath: pathlib.Path) -> bytes:
    with javaFilePath.open() as f:
        code = f.read()

    enc = parse_java_array(extract(code, "private static final byte[] enc = ", ";"))
    iArr = parse_java_array(extract(code, "int[] iArr = ", ";"))
    ror_bit = int(
        extract(
            code,
            "nBytes[i2] = (byte) (nBytes[i2] ^ ((byte) ((iArr[i] >> ",
            ") | (iArr[i] << ",
        )
    )
    i3_added = int(
        extract(
            code, "nBytes[i4] = (byte) (nBytes[i4] + ((byte) iArr[(i3 + ", ") % 8]));"
        )
    )

    nBytes = bytearray()
    for i in range(8):
        tmp = enc[i] - (iArr[(i + i3_added) % 8])
        while tmp < 0:
            tmp += 256
        tmp %= 256
        tmp ^= rotate_right_8(iArr[i], ror_bit)
        nBytes.append(tmp)

    tqdm.tqdm.write(str(nBytes))
    return nBytes


total = bytearray()
for index in tqdm.trange(0, 1264):
    fileIndex = f"{index:04d}"
    path = pathlib.Path(f"Chal{fileIndex}/sources/defpackage/Chal{fileIndex}.java")
    total.extend(solve_one_class(path))

with open("result.png", "wb") as f:
    f.write(total)

1264個全部の逆コンパイルが終わるまでの約40分待ってから、ソルバーを実行しました:

$ time ./solve.py
bytearray(b'\x89PNG\r\n\x1a\n')
bytearray(b'\x00\x00\x00\rIHDR')
中略
bytearray(b'\x00\x00\x00IEND\xae')
bytearray(b'B`\x82\x00\x00\x00\x00\x00')
100%|███████████████████████████████| 1264/1264 [00:03<00:00, 327.52it/s]
./solve.py  0.47s user 0.35s system 20% cpu 3.968 total

約4秒で処理が終わりました。処理結果にPNGIHDRIENDがあることからPNG形式の内容で、全体を結合した結果は次の画像でした:

QRコードを読み取ることでもフラグ文字列が手に入る、優しさに溢れた内容でした: sknb{now_you_can_say_bye_bye_bytecode_73c1d0}

[Rev, easy] from-one-to-two (30 teams solves, 366 points)

二次元へLet's gooooo!!!

配布ファイルとして、mainバイナリとprogram.txtがありました:

$ file *
main:        ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=7fff80a1fa1ed7ededeff1bc926dcb3c15d1b507, for GNU/Linux 3.2.0, not stripped
program.txt: Unicode text, UTF-8 text, with CRLF line terminators
$

配布ファイル全体としてはフラグチェッカーでした:

$ ./main program.txt
input:test
no
$

program.txtは、何か矢印を大量に使ったコードのようでした:

↠↠↠↠↠↠↠↠↠⇣⇣⇠⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠
⇣⇠↠⇢⇢⇢⇢⇢⇣⇣⇣⇣⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇡⇠⇓⇠⇣⇠↡⇠↡⇠⇓⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇣⇠⇡
⇣⇡⇡⇠⇣⇠⇠↞⇣⇣⇣⇣↟⇣↟⇣↟⇣↟⇣...........................................................................................................⇡⇣↟⇓⇡↡⇡↡⇡⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇡
⇣⇡⇢⇡↡⇢⇣⇡↡⇣⇣⇣↟⇣↟⇣↟⇣↟⇣..........#.................#.............................................####.....####.....####....######.⇡⇣↟⇣⇡↡⇡↡⇡⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇡
⇣⇡⇡↡↔⇣⇡↕⇠⇣⇣⇣↟⇣↟⇣↟⇣↟⇣..........#.................#..........####...#######..#######...........#....#...#....#...#....#...#......⇡⇣↟⇣⇡↡⇡↡⇡⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇡
⇣⇡⇡⇢⇣⇢⇣⇣↟⇣⇣⇣↟⇣↟⇣↟⇣↟⇣..........#.................#.........#....#.....#.....#......................#...#....#........#...#......⇡⇣↟⇣⇡↡⇡↡⇡⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇡
⇣⇡⇡↡↔⇣⇠⇣↟⇣⇣⇣↟⇣↟⇣↟⇣↟⇣..#####...#...##...#.####...######...#...........#.....#.....................#....#....#.......#....#####..⇡⇣↟⇣⇡↡⇡↡⇡⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇡
⇣⇡⇡⇢⇣⇢⇣⇣↟⇣⇣⇣↟⇣↟⇣↟⇣↟⇣.#........#..#.....##....#..#.....#..#...........#.....#####................#.....#....#......#..........#.⇡⇣↟⇣⇡↡⇡↡⇡⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇡
⇣⇡⇡↡↔⇣⇠⇣⇡⇣⇣⇣↟⇣↟⇣↟⇣↟⇣..####....###......#.....#..#.....#..#...........#.....#...................#......#....#.....#...........#.↟⇣↟⇣⇡↡⇡↡⇡⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇡
⇣⇡⇡⇢⇣⇣⇣⇠⇡⇣↠⇣↟⇣↟⇣↟⇣↟⇣......#...#..#.....#.....#..#.....#...#....#.....#.....#..................#.......#....#....#.......#....#.↟⇣↟⇣⇡↡⇡↡↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇡
⇣⇡⇡↡↔⇣⇣↠⇡⇢⇑⇣↟⇣↟⇣↟⇣↟⇣.#####....#...##...#.....#..######.....####......#.....#.................######....####....######....####..↟⇣↟⇣⇡↡⇡↡↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇣↟⇠↟⇠↟⇡
⇢⇡⇡⇢⇣⇣⇣⇠⇡⇣⇠⇠↟⇠↟⇠↟⇠↟⇣...........................................................................................................↟⇠↟⇠⇡⇠⇡⇠↟⇠↟⇠↟⇠↟⇠↟⇠↟⇠↟⇠↟⇠↟⇠↟⇠⇢⇢⇢⇢↟⇡
⇢⇣⇡⇠↔⇣⇣⇣⇡⇠⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇢⇢⇢⇢⇢↡⇡↞↞↞⇡
⇡⇢⇢⇢⇢⇣⇣⇡⇠⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇢↕⇢⇢⇢⇢⇡⇡
⇡⇢⇢⇣⇠⇣⇣⇢⇢⇡⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇣↠⇣↠↡⇣⇡
⇡↕↞⇣↟⇣⇣⇡⇣⇠⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇡
⇡⇣↟⇣↟⇣⇣⇡⇣⇡⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇡
⇡⇣↟⇣↟⇣⇣⇡⇣⇡⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇡
⇡⇣↟⇣↟⇣⇣⇡⇣⇡⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣↟⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣↟⇣↞⇡
⇡⇣↟⇣↟⇣⇣⇡⇣⇡⇣⇡⇣↟⇢⇣⇣⇡⇣↟⇢⇣⇣⇡⇣↟⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡↡↟⇢⇣⇣⇡⇣↟⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣↟⇢⇣⇣⇡⇣↟⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣↟⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣↟⇢⇣⇣⇡⇣↟⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣↟⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣↟⇢⇣⇣⇡⇣↟⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣↟⇢⇣⇣⇡↡↟⇢⇣⇡
⇡⇣↟⇣↟⇣⇣⇡⇣⇡⇣⇡↡↟⇣↞⇣⇡↡↟⇣↞⇣⇡↡↟⇣↞⇣⇡⇣↟⇣↞⇣⇡↡↟⇣↞⇣⇡↡↟⇣↞⇣⇡⇣⇡⇣↞⇣⇡↡↟⇣↞⇣⇡↡↟⇣↞⇣⇡⇣⇡⇣↞⇣⇡↡↟⇣↞⇣⇡⇣↟⇣↞⇣⇡⇣⇡⇣↞⇣⇡↡↟⇣↞⇣⇡↡↟⇣↞⇣⇡⇣⇡⇣↞⇣⇡↡↟⇣↞⇣⇡⇣↟⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡↡↟⇣↞⇣⇡↡↟⇣↞⇣⇡⇣⇡⇣↞⇣⇡↡↟⇣↞⇣⇡↡↟⇣↞⇡
⇡⇣↟⇣↟⇣⇣⇡⇣⇡⇣⇡↡↟⇢⇣⇣⇡↡↟⇢⇣⇣⇡↡↟⇢⇣⇣⇡↡↟⇢⇣⇣⇡↡↟⇢⇣⇣⇡↡↟⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡↡↟⇢⇣⇣⇡↡↟⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡↡↟⇢⇣⇣⇡↡↟⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡↡↟⇢⇣⇣⇡↡↟⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡↡↟⇢⇣⇣⇡↡↟⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡↡↟⇢⇣⇣⇡↡↟⇢⇣⇣⇡⇣⇡⇢⇣⇣⇡↡↟⇢⇣⇣⇡↡↟⇢⇣⇡
⇡⇣↟⇣↟⇣⇣⇡⇣⇡⇣⇡↡↟⇣↞⇣⇡↡↟⇣↞⇣⇡↡↟⇣↞⇣⇡↡↟⇣↞⇣⇡↡↟⇣↞⇣⇡↡↟⇣↞⇣⇡⇣⇡⇣↞⇣⇡↡↟⇣↞⇣⇡↡↟⇣↞⇣⇡⇣⇡⇣↞⇣⇡↡↟⇣↞⇣⇡↡↟⇣↞⇣⇡⇣⇡⇣↞⇣⇡↡↟⇣↞⇣⇡↡↟⇣↞⇣⇡⇣⇡⇣↞⇣⇡↡↟⇣↞⇣⇡↡↟⇣↞⇣⇡⇣⇡⇣↞⇣⇡⇣⇡⇣↞⇣⇡↡↟⇣↞⇣⇡↡↟⇣↞⇣⇡⇣⇡⇣↞⇣⇡↡↟⇣↞⇣⇡↡↟⇣↞⇡
⇡⇣↟⇣↟⇣⇣⇡⇣⇡⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡⇣↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡⇣↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡⇣↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡⇣↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡⇣↟⇣⇣⇣⇡⇣↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡⇣↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇡
⇡⇣↟↡↟↞⇣⇡⇣⇡⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇡
⇡⇣↟↠⇣⇢↠⇡⇣⇡⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣⇡↡↟⇣⇣⇣↟↡↟⇣⇣⇡
⇡⇣↟⇠⇠↠⇓⇢⇓⇡⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣⇡⇢↕⇣⇣⇣↟⇢↕⇣⇣⇣↟⇢↕⇣⇣⇡
⇡⇢⇢⇢⇢⇡⇢↟.⇡⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣↟⇣↞⇣⇣⇣↟⇣↞⇣⇣⇣↟⇣↞⇣⇣⇡
⇡⇠⇠⇠⇠↞↞↞↞↞⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣↟⇢⇣⇣⇣⇣↟⇢⇣⇣⇣⇣↟⇢⇣⇣⇣⇣↟⇢⇣⇣⇣⇡
⇣⇠⇣⇠⇠⇣⇠⇣⇠⇡⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣⇡⇣↞⇣⇣⇣↟⇣↞⇣⇣⇣↟⇣↞⇣⇣⇣↟⇣↞⇣⇣⇣↟⇣↞⇣⇣⇣↟⇣↞⇣⇣⇡
⇣⇡⇢⇣⇡⇠⇡⇠⇡⇡⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣⇡⇢⇣⇣⇣⇣↟⇢⇣⇣⇣⇣↟⇢⇣⇣⇣⇣↟⇢⇣⇣⇣⇣↟⇢⇣⇣⇣⇣↟⇢⇣⇣⇣⇣↟⇢⇣⇣⇣⇡
⇣⇡⇣⇠⇢⇢⇢⇢⇡⇡⇣⇡↡↞⇣⇣⇣⇡↡↞⇣⇣⇣⇡↡↞⇣⇣⇣⇡↡↞⇣⇣⇣⇡↡↞⇣⇣⇣⇡↡↞⇣⇣⇣⇡↡↞⇣⇣⇣⇡↡↞⇣⇣⇣⇡↡↞⇣⇣⇣⇡↡↞⇣⇣⇣⇡↡↞⇣⇣⇣⇡↡↞⇣⇣⇣⇡↡↞⇣⇣⇣⇡↡↞⇣⇣⇣⇡↡↞⇣⇣⇣⇡↡↞⇣⇣⇣⇡↡↞⇣⇣⇣⇡↡↞⇣⇣⇣↟↡↞⇣⇣⇣↟↡↞⇣⇣⇣↟↡↞⇣⇣⇣↟↡↞⇣⇣⇣↟↡↞⇣⇣⇣↟↡↞⇣⇣⇣↟↡↞⇣⇣⇡
⇣⇡⇢⇣⇡⇣⇠↞↞⇡⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇡
⇣⇡⇣⇠⇡⇣↠↠↟⇡⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇡
⇣⇡⇢⇣⇡⇣⇡⇢⇢⇡⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇡
⇣⇡⇣⇠⇡⇣⇡↕↞↞⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇡
⇣⇡⇢⇣⇡⇣⇑⇠⇢⇡⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇡
⇣⇡⇣⇠⇡⇣⇢⇣⇡↞⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇡
⇣⇡⇣⇢⇡⇣⇡⇣⇢⇡⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇡
⇣⇡⇣⇡⇣⇠⇡⇠⇡↞⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇡
⇣⇡⇣⇡⇣⇣⇠↔⇢⇡⇣⇡↡⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇡
⇣⇡⇣⇡⇣⇣↟⇡⇠⇠⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇡
⇣⇡⇣⇡⇣⇢↕⇠↠⇡⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇡
⇣⇡⇣⇡⇣⇣⇢⇣⇡⇠⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇡
⇣⇡⇣⇡⇣⇣⇣↞↠⇡⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟↡⇣⇣⇣⇣↟⇣⇣⇣⇣⇡
⇣⇡⇣⇡⇣⇢⇢⇣⇡⇠⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇡
⇣⇡⇣⇡⇣⇡⇣↞↠⇡⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇡
⇣⇡⇣⇡⇣⇡⇢⇣⇡⇠⇣⇡⇣⇣⇣⇣⇣⇡⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇣↟⇣⇣⇣⇣⇡
⇣⇡⇣⇡⇣⇣↞↞↠⇡⇢⇡⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇢↟⇣⇠⇠⇠⇡
⇣⇡⇠⇡⇣⇣⇢⇠⇡⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇠⇡
⇢⇢⇢⇡⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇢⇡

mainバイナリをIDAで開いて解析すると、次の内容でした:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int result; // eax
  int dwCurrentLine; // ebx
  int dwArrayMemorySize256[258]; // [rsp+10h] [rbp-840h] BYREF
  int dwX; // [rsp+418h] [rbp-438h] MAPDST
  int dwY; // [rsp+41Ch] [rbp-434h]
  wchar_t wstrSize256[256]; // [rsp+420h] [rbp-430h] BYREF
  size_t qwStrSize; // [rsp+820h] [rbp-30h]
  int wcharCurrent; // [rsp+82Ch] [rbp-24h]
  __FILE *fpRead; // [rsp+830h] [rbp-20h]
  int charInputted; // [rsp+838h] [rbp-18h]
  int dwMemoryIndex; // [rsp+83Ch] [rbp-14h]

  setlocale(6, &locale);
  if ( argc > 1 )
  {
    fpRead = fopen(argv[1], "r");
    if ( fpRead )
    {
      while ( fgetws(wstrSize256, 256, fpRead) )
      {
        qwStrSize = wcslen(wstrSize256);
        if ( qwStrSize && wstrSize256[qwStrSize - 1] == '\n' )
          wstrSize256[qwStrSize - 1] = 0;
        dwCurrentLine = g_lineCount;
        g_pwstrProgramArraySize256[dwCurrentLine] = wcsdup(wstrSize256);
        ++g_lineCount;
      }
      fclose(fpRead);
      dwX = 0;
      dwY = 0;
      memset(dwArrayMemorySize256, 0, 0x400u);
      dwMemoryIndex = 0;
      printf("input:");
      fflush(stdout);
      while ( 1 )
      {
labelVerifyCondition:
        if ( dwY < 0 || dwY >= g_lineCount || dwX < 0 || dwX >= (int)wcslen(g_pwstrProgramArraySize256[dwY]) )
          return 0;
        wcharCurrent = g_pwstrProgramArraySize256[dwY][dwX];
        if ( wcharCurrent == '.' )              // end
          return 0;
        if ( wcharCurrent >= '.' && wcharCurrent <= 8675 && wcharCurrent >= 8596 )
          break;
labelInvalidInstruction:
        puts("Invalid inst");
      }
      switch ( wcharCurrent )
      {
        case 8596:                              // ↔
          if ( dwArrayMemorySize256[dwMemoryIndex] )
            --dwX;
          else
            ++dwX;
          goto labelVerifyCondition;
        case 8597:                              // ↕
          if ( dwArrayMemorySize256[dwMemoryIndex] )
            --dwY;
          else
            ++dwY;
          goto labelVerifyCondition;
        case 8606:                              // ↞
          --dwMemoryIndex;
          --dwX;
          if ( dwMemoryIndex >= 0 )
            goto labelVerifyCondition;
          fwrite("stack underflow\n", 1u, 0x10u, stderr);
          result = 1;
          break;
        case 8607:                              // ↟
          ++dwArrayMemorySize256[dwMemoryIndex];
          --dwY;
          goto labelVerifyCondition;
        case 8608:                              // ↠
          ++dwMemoryIndex;
          ++dwX;
          if ( dwMemoryIndex <= 255 )
            goto labelVerifyCondition;
          fwrite("stack overflow\n", 1u, 0xFu, stderr);
          result = 1;
          break;
        case 8609:                              // ↡
          --dwArrayMemorySize256[dwMemoryIndex];
          ++dwY;
          goto labelVerifyCondition;
        case 8657:                              // ⇑ , input
          charInputted = getchar();
          if ( charInputted == -1 || charInputted == 10 )
            charInputted = 255;
          dwArrayMemorySize256[dwMemoryIndex] = charInputted;
          --dwY;
          goto labelVerifyCondition;
        case 8659:                              // ⇓ , output
          putchar(dwArrayMemorySize256[dwMemoryIndex]);
          fflush(stdout);
          ++dwY;
          goto labelVerifyCondition;
        case 8672:                              // ⇠
          --dwX;
          goto labelVerifyCondition;
        case 8673:                              // ⇡
          --dwY;
          goto labelVerifyCondition;
        case 8674:                              // ⇢
          ++dwX;
          goto labelVerifyCondition;
        case 8675:                              // ⇣
          ++dwY;
          goto labelVerifyCondition;
        default:
          goto labelInvalidInstruction;
      }
    }
    else
    {
      perror(argv[1]);
      return 1;
    }
  }
  else
  {
    fprintf(stderr, "Usage: %s <program>\n", *argv);
    return 1;
  }
  return result;
}

どうやらBrainFuckの2D版のようなプログラムのようでした。

さて、program.txtを真面目に解析するのは大変に思いました。GDBのPythonAPIを使って、とりあえず分岐命令を実行する際にトレース出力するコードを書きました:

#!/usr/bin/env python3
# gdb -q -x trace.py main

import gdb

# Python API利用コードのデバッグ時に役立ちます
gdb.execute("set python print-stack full")

INPUT_FILENAME = "input.txt"
# ↓呼び出し元が作ってください
# with open(INPUT_FILENAME, "w") as f:
#     f.write("sknb{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}")

# piebaseコマンドは実行後にのみ動作します
gdb.execute(f"starti program.txt < {INPUT_FILENAME}")
# gdb.execute("starti program.txt")


# pwndbgの機能を使います
def get_va(rva: int) -> int:
    output = gdb.execute(f"piebase {hex(rva)}", to_string=True)
    return int(output.split(" = ")[1], 0)


dwArrayMemorySize256 = -0x840
dwX = -0x438
dwY = -0x434
dwMemoryIndex = -0x14


def set_dprintf(rva: int):
    gdb.execute(
        rf"""dprintf *{hex(get_va(rva))},"(%3d,%3d) memory[%3d]:%4d\n", {{unsigned int}}($rbp+{dwX}), {{unsigned int}}($rbp+{dwY}), {{unsigned int}}($rbp+{dwMemoryIndex}), $rax """
    )


set_dprintf(0x15EB)  # ↔
set_dprintf(0x15B3)  # ↕


gdb.execute("continue")
gdb.execute("quit")

実行結果を眺めても全然ピンときませんでした。しばらく困っていると、「入力文字列が正しければ正しいほどトレース出力結果が長くなるのでは?先頭から1文字ずつ特定できるのでは?」との発想が思いつきました。実装しました:

#!/usr/bin/env python3

import subprocess

import tqdm

flag = ""
INPUT_FILENAME = "input.txt"

for _ in tqdm.trange(64, leave=False):
    tuple_list = []
    for c in tqdm.trange(0x20, 0x7F, leave=False):
        current = flag + chr(c)
        with open(INPUT_FILENAME, "w") as f:
            f.write(current)
        completed_process = subprocess.run(
            ["gdb", "-q", "-x", "trace.py", "main"],
            capture_output=True,
            text=False,
        )
        # print(completed_process.stdout)
        tuple_list.append((len(completed_process.stdout), current))
        # print(tuple_list[-1])
    tuple_list.sort(reverse=True)
    tqdm.tqdm.write(str(tuple_list[1]))
    tqdm.tqdm.write(str(tuple_list[0]))
    flag = tuple_list[0][1]
    if "}" in flag:
        break

実行しました:

$ time ./solve.py
(5414, '~')
(10112, 's')
(10112, 's~')
(14999, 'sk')
(14999, 'sk~')
(19346, 'skn')
(19346, 'skn~')
(25367, 'sknb')
(25367, 'sknb~')
(30821, 'sknb{')
(30821, 'sknb{~')
(33224, 'sknb{n')
(33224, 'sknb{n~')
(39056, 'sknb{n4')
(39056, 'sknb{n4~')
(45077, 'sknb{n4r')
(45077, 'sknb{n4r~')
(47723, 'sknb{n4rr')
(47723, 'sknb{n4rr~')
(54122, 'sknb{n4rr0')
(54122, 'sknb{n4rr0~')
(59765, 'sknb{n4rr0w')
(59766, 'sknb{n4rr0w~')
(62655, 'sknb{n4rr0w_')
(62655, 'sknb{n4rr0w_~')
(69621, 'sknb{n4rr0w_4')
(69621, 'sknb{n4rr0w_4~')
(76776, 'sknb{n4rr0w_4r')
(76776, 'sknb{n4rr0w_4r~')
(79908, 'sknb{n4rr0w_4rr')
(79908, 'sknb{n4rr0w_4rr~')
(87441, 'sknb{n4rr0w_4rr0')
(87441, 'sknb{n4rr0w_4rr0~')
(94056, 'sknb{n4rr0w_4rr0w')
(94056, 'sknb{n4rr0w_4rr0w~')
(97431, 'sknb{n4rr0w_4rr0w_')
(97431, 'sknb{n4rr0w_4rr0w_~')
(100887, 'sknb{n4rr0w_4rr0w_5')
(100887, 'sknb{n4rr0w_4rr0w_5~')
(109176, 'sknb{n4rr0w_4rr0w_50')
(109176, 'sknb{n4rr0w_4rr0w_50~')
(117654, 'sknb{n4rr0w_4rr0w_50r')
(117654, 'sknb{n4rr0w_4rr0w_50r~')
(121353, 'sknb{n4rr0w_4rr0w_50rr')
(121353, 'sknb{n4rr0w_4rr0w_50rr~')
(130209, 'sknb{n4rr0w_4rr0w_50rr0')
(130209, 'sknb{n4rr0w_4rr0w_50rr0~')
(140550, 'sknb{n4rr0w_4rr0w_50rr0w')
(140550, 'sknb{n4rr0w_4rr0w_50rr0w~')
(141469, 'sknb{n4rr0w_4rr0w_50rr0w}')
./solve.py  3016.30s user 1393.76s system 106% cpu 1:08:50.22 total
$ ./main program.txt
input:sknb{n4rr0w_4rr0w_50rr0w}
yes
$

プログラム1回あたりのトレース実行に必要な秒数は、最初は1回1.3秒ほどでしたが、最後の方は1回3秒ほどになっていました。ともかく全体として1時間強でフラグを入手できました: sknb{n4rr0w_4rr0w_50rr0w}

[Rev, easy] kufvm (25 teams solves, 408 points)

KoUFuVM

配布ファイルとして、問題本体のmainchall.binと、Pwn問題用のDockerfile等がありました:

$ file *
Dockerfile:  ASCII text
chall.bin:   data
compose.yml: ASCII text
flag.txt:    ASCII text, with no line terminators
main:        ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ca89b36b93ae719fb3065b3b5c060fbb96399ce5, for GNU/Linux 3.2.0, not stripped
run.sh:      POSIX shell script, ASCII text executable
$

本Revジャンルの問題として扱うのは、mainバイナリとchall.binのみでした。配布ファイル全体としてはフラグチェッカーでした:

$ ./main chall.bin
flag:test
incorrect!
$

mainバイナリをIDAで開いて解析しました:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stderr, 0, 2, 0);
  if ( argc != 2 )
  {
    printf("Usage:%s <path_to_program>", *argv);
    exit(-1);
  }
  vm(argv[1]);
  return 0;
}

void __fastcall vm(const char *pStrVmFileName)
{
  _BYTE byteCodeArraySize0x400[1036]; // [rsp+10h] [rbp-430h] BYREF
  int dwInputted; // [rsp+41Ch] [rbp-24h]
  int operand2; // [rsp+420h] [rbp-20h]
  int operand1; // [rsp+424h] [rbp-1Ch]
  unsigned int opcode; // [rsp+428h] [rbp-18h]
  unsigned int dwInstruction; // [rsp+42Ch] [rbp-14h]
  FILE *fpVm; // [rsp+430h] [rbp-10h]
  int dwByteCodeIndex; // [rsp+43Ch] [rbp-4h]

  memset(byteCodeArraySize0x400, 0, 0x400u);
  dwByteCodeIndex = 0;
  fpVm = fopen(pStrVmFileName, "rb");
  if ( !fpVm )
  {
    printf("Unable to open program");
    exit(-1);
  }
  fread(byteCodeArraySize0x400, 1u, 0x400u, fpVm);
  while ( 1 )
  {
    dwInstruction = *(_QWORD *)&byteCodeArraySize0x400[dwByteCodeIndex];
    opcode = HIBYTE(dwInstruction);
    operand1 = (dwInstruction >> 12) & 0xFFF;
    operand2 = dwInstruction & 0xFFF;
    switch ( HIBYTE(dwInstruction) )
    {
      case 0u:
        byteCodeArraySize0x400[operand1] += byteCodeArraySize0x400[operand2];
        goto labelGoNextInstruction;
      case 1u:
        byteCodeArraySize0x400[operand1] ^= byteCodeArraySize0x400[operand2];
        goto labelGoNextInstruction;
      case 2u:
        if ( byteCodeArraySize0x400[operand1] )
          goto labelGoNextInstruction;
        dwByteCodeIndex = operand2;
        break;
      case 3u:
        dwInputted = getchar();
        if ( dwInputted >= 0 )
          byteCodeArraySize0x400[operand1] = dwInputted;
        else
          byteCodeArraySize0x400[operand1] = '\n';
        goto labelGoNextInstruction;
      case 4u:
        putchar((char)byteCodeArraySize0x400[operand1]);
        goto labelGoNextInstruction;
      case 5u:
        byteCodeArraySize0x400[(char)byteCodeArraySize0x400[operand1]] = byteCodeArraySize0x400[operand2];
labelGoNextInstruction:
        dwByteCodeIndex += 4;
        break;
      case 6u:
        return;
      default:
        printf("Invalid inst:%d %d %d", opcode, operand1, operand2);
        exit(-1);
    }
  }
}

mainバイナリはかなり単純なVMで、1-instructionは32-bit固定長、opcodeも7種類のみでした。

Revジャンルの問題は、本問題を除いた4問が最初から取り組める状態であり、本問題がコンテスト開始から約9時間後に公開された問題でした。本問題公開時では他の問題を解き終わっていたため、「もしかしたら本問題も、風が吹けば桶屋が儲かる問題同様に、トレース実行での出力長を使って1文字ずつ求めれるかも?」と考えました。しかし約80分実行してみるも、sknb{This_is_rev_flag!_congrats!!!!_167~b~~~(以降同様にチルダループ)と最後の方だけ失敗している様子でした。

困ったので、真面目にchall.binバイナリ読解を始めました。VM問題を解くときにいつも恐れているのが、実行中に実行内容を自己書き換えすることです。ただ本問題は幸いにして、実行内容領域とデータ領域が明確に分かれていました。chall.bin読解メモです:

  • data[0 : 4] :: 命令。0xa7(=167)へjmpする
  • data[4 : 63] :: rodataのはず。correct/incorrect文字列や、その他定数?
  • data[63 : 105] :: data。getchar結果の格納や、correct/incorrect判定用。
  • data[111 : 151+4] :: 命令。最後のcorrect/incorrect表示とreturn命令箇所。 11命令分。
  • data[155] :: 初期値0。これが非0になると、最後の111/119へのjmp含めて色々スキップされそう。ある種の番兵? → Jmp系がJZだけだから、単なる数値0があるとJMP代わりで便利という話かも。
  • data[156] :: 数値1。「getchar格納先index」の+=用途
  • data[157] :: 数値0xFF。
  • data[158 : 161] :: (未調査)
  • data[161] :: getchar格納先index。初期値0なものの「data[161] += data[165];」があるので実質0x3F以降。pwn時には文字を多く入力すればcorrect/incorrect用命令を上書きできます。
  • data[162 : 167] :: (未調査)
  • data[167 : 787+4] :: 各種命令。最後に111か119へjmp。
  • data[787+4 : ] :: 未使用。0埋め。

命令箇所がread-onlyの場合のVM問題で私がしばしばやることが、C言語ソースコードへ変換することです。そうすると、コンパイラや逆コンパイラの最適化により読みやすくなったり、またangrでのシンボリック実行ができる場合があります。chall.bin内容をC言語コードへ変換するスクリプトを書きました(読解メモ作成にも大いに役立ちました):

#!/usr/bin/env python3

import sys

import pwn

with open("chall.bin", "rb") as fin:
    byte_code = fin.read()

# print(f"{hex(len(byte_code)) = }")


# 0~4: jmp
# 111-155: print result and return
# 155-167: data
# 167- code
def is_in_data_section(index: int) -> bool:
    return (4 <= index < 111) or (155 <= index < 167)


OFFSET_MAIN = 167
print(f"""
#include <stdio.h>

    unsigned char data[] = {{{",".join(map(str, byte_code[:OFFSET_MAIN]))}}};
int main() {{
    int dwInputted;


    goto label_{OFFSET_MAIN};
""")


def convert_one_instruction(i):
    instruction = pwn.u32(byte_code[i : i + 4])
    opcode = instruction >> 24
    operand1 = (instruction >> 12) & 0xFFF
    operand2 = (instruction >> 0) & 0xFFF
    print(f"label_{i}: ", end="")
    print(
        f"[{i:03x}] {opcode = :02x}, {operand1 = :03x}, {operand2 = :03x}",
        file=sys.stderr,
    )
    match opcode:
        case 0:
            assert is_in_data_section(operand1)
            if is_in_data_section(operand2):
                print(f"""data[{operand1}] += data[{operand2}];""")
            else:
                print(f"""data[{operand1}] += {byte_code[operand2]};""")
        case 1:
            assert is_in_data_section(operand1)
            if is_in_data_section(operand2):
                print(f"""data[{operand1}] ^= data[{operand2}];""")
            else:
                print(f"""data[{operand1}] ^= {byte_code[operand2]};""")
        case 2:
            assert is_in_data_section(operand1)
            print(f"""if(data[{operand1}] == 0) {{ goto label_{operand2}; }}""")
        case 3:
            assert is_in_data_section(operand1)
            print(
                f"""dwInputted = getchar(); data[{operand1}] = (dwInputted >= 0 ? dwInputted : 10);"""
            )
        case 4:
            assert is_in_data_section(operand1)
            print(f"""putchar((char)data[{operand1}]);""")
        case 5:
            print(f"""data[data[{operand1}]] = data[{operand2}];""")
        case 6:
            print("""return 0;""")


for i in range(111, 155, 4):
    convert_one_instruction(i)
for i in range(OFFSET_MAIN, 791, 4):
    convert_one_instruction(i)

print("""
    puts("Must not come here!");
    return 1;
}
""")

./convert.py > converted.c実行でC言語ソースコードを作成しました。今回はgcc -g -O0 converted.cでの最適化無しビルドの場合が、逆コンパイル結果が一番読みやすかったです。

お試しで、とりあえずangrでのシンボリック実行を試しました:

#!/usr/bin/env python3
#
import logging

import angr

logging.getLogger("angr").setLevel("WARN")
project = angr.Project("./a.out", load_options={"auto_load_libs": False})

state = project.factory.entry_state(
    args=[project.filename],
    add_options={angr.options.ZERO_FILL_UNCONSTRAINED_MEMORY},
)
simgr = project.factory.simulation_manager(state)

simgr.explore(
    find=lambda s: b"flag:correct!" in s.posix.dumps(1),
    avoid=lambda s: b"flag:incorrect!" in s.posix.dumps(1),
)

if len(simgr.found) == 0:
    raise Exception("Not found...")

solution = simgr.found[0]
print(solution.posix.dumps(0))

実行しました:

$ time docker run --rm -it --mount type=bind,src=.,dst=/app/,readonly -w /app angr/angr:9.2.184 /home/angr/.venv/bin/python3 solve_rev_by_angr.py
b'sknb{This_is_rev_flag!_congrats!!!!_167a14}\n'
docker run --rm -it --mount type=bind,src=.,dst=/app/,readonly -w /app     0.03s user 0.06s system 0% cpu 2:23.23 total
$ ./main chall.bin
flag:sknb{This_is_rev_flag!_congrats!!!!_167a14}
correct!
$

フラグを入手できました: sknb{This_is_rev_flag!_congrats!!!!_167a14}

感想

  • Revジャンルを全問正解できて満足です!
  • jadxがGUI版もありCUIのHeadless版もありで、とても便利です!
  • 上述の通り、Revジャンルのkufvm問題には、対応するPwnジャンルの問題がありました。
    • 私の想像するVMジャンルのPwn問題は、「VM実行機のバイナリは同一で、入力VMコードを自由に与える形式」でした。しかし今回は、VM実行機のバイナリのみならず、chall.binも同一のままのPwn問題でした。そのためchall.binの脆弱性を付く必要がありました!面白い形式です!
    • この都合で、chall.binのcorrect/incorrect表示の命令が、フラグ入力読み込み領域の直後に存在したようです。
    • ただし残念ながら、私はPwn問題を解けませんでした……。VM命令が単純すぎて何をするにしても間接参照が必須という不便な点や、入力に使えないバイトがあるらしい点があり、実装しきれませんでした……。
  • 最近、angrをdocker経由で実行するようになりました。angrを使うだけならdockerコマンド単体で完結するので便利です。他パッケージが必要な場合でも、少しDockerfileを書けばおそらくできるでしょう。
    • angrは依存先パッケージをバージョン固定でインストールする必要があるため、pip全体でバージョンの整合性を取るのが困難だったためです。そのため、いつの間にかz3-solver関連のパッケージがうまく動かないことがよく起こってました。angrをdocker実行に変更し、pipとしてはangrを導入しないことで、pipでのバージョンの整合性が取りやすくなりました。
  • Snipping ToolsでQRコードを読めることを最近知りました。もっと早く知りたかったです!