💭

Daily AlpacaHack "Useful Machine" Writeup

に公開

Useful Machine

添付ファイルの確認

$ ls -l
total 8
-rw-r--r-- 1 shiragi shiragi 1086 Dec  7 03:57 program
-rw-r--r-- 1 shiragi shiragi 1713 Dec  7 03:57 vm.py
$ file *
program: data
vm.py:   Python script, ASCII text executable

vm.pyを眺めてみると,独自言語で実装されたプログラムを実行するプログラムとわかった.

そのプログラムコードはprogramというバイナリファイルに書かれているらしい.
バイナリだからリバースするならもうちょっと読みやすくしたいな...

というわけでGPTにPythonに変換するコードを書いてもらった

import sys

# このスクリプトは、vm.py が実行するバイトコード ("program") を
# 同等な処理を行う Python 関数 run(input_string: str) に変換して標準出力に出します。
# 使い方:
#   python bytecode_to_python.py program > decompiled_program.py


def translate(program: bytes) -> str:
    lines: list[str] = []

    # ヘッダ部分: メモリと入力処理の定義
    lines.append("def run(input_string: str):")
    lines.append("    mem = [0] * 256")
    lines.append("    input_index = 0")
    lines.append("")
    lines.append("    def read_char():")
    lines.append("        nonlocal input_index")
    lines.append("        if input_index >= len(input_string):")
    lines.append("            return '\\x00'")
    lines.append("        ch = input_string[input_index]")
    lines.append("        input_index += 1")
    lines.append("        return ch")
    lines.append("")

    # 本体: 各命令をそのままPython文として展開
    if len(program) % 3 != 0:
        raise ValueError(f"Program length {len(program)} is not a multiple of 3")

    ip = 0
    while ip < len(program):
        opcode = program[ip]
        oprand1 = program[ip + 1]
        oprand2 = program[ip + 2]

        # コメントとして元の ip と生の命令を残す
        lines.append(f"    # ip={ip}, opcode={opcode}, oprand1={oprand1}, oprand2={oprand2}")

        if opcode == 0:
            # 入力1文字を読み込み、その ord % 256 をメモリに格納
            lines.append(f"    mem[{oprand1}] = ord(read_char()) % 256")
        elif opcode == 1:
            # 即値を書き込み
            lines.append(f"    mem[{oprand1}] = {oprand2}")
        elif opcode == 2:
            # 別のセルからコピー
            lines.append(f"    mem[{oprand1}] = mem[{oprand2}]")
        elif opcode == 3:
            # 加算 (mod 256)
            lines.append(f"    mem[{oprand1}] = (mem[{oprand1}] + mem[{oprand2}]) % 256")
        elif opcode == 4:
            # 乗算 (mod 256)
            lines.append(f"    mem[{oprand1}] = (mem[{oprand1}] * mem[{oprand2}]) % 256")
        elif opcode == 5:
            # XOR
            lines.append(f"    mem[{oprand1}] = mem[{oprand1}] ^ mem[{oprand2}]")
        elif opcode == 6:
            # 論理NOT (0 なら 1、それ以外なら 0)
            lines.append(f"    mem[{oprand1}] = 0 if mem[{oprand1}] != 0 else 1")
        else:
            raise ValueError(f"Unknown opcode {opcode!r} at ip={ip}")

        ip += 3

    lines.append("")
    lines.append("    # vm.run と同様に、mem[0] == 0 を戻り値にする")
    lines.append("    return mem[0] == 0, mem")

    return "\n".join(lines) + "\n"


def main() -> None:
    if len(sys.argv) < 2:
        path = "program"
    else:
        path = sys.argv[1]

    with open(path, "rb") as f:
        program = f.read()

    src = translate(program)
    sys.stdout.write(src)


if __name__ == "__main__":
    main()

それで出力されたPythonコードが以下になる.

   # 前略

    # これは最初だけの処理
    # ip=0, opcode=1, oprand1=0, oprand2=1
    mem[0] = 1

    # ここからパターン処理
    # ip=3, opcode=0, oprand1=1, oprand2=0
    mem[1] = ord(read_char()) % 256
    # ip=6, opcode=3, oprand1=1, oprand2=3
    mem[1] = (mem[1] + mem[3]) % 256
    # ip=9, opcode=1, oprand1=2, oprand2=9
    mem[2] = 9
    # ip=12, opcode=5, oprand1=1, oprand2=2
    mem[1] = mem[1] ^ mem[2]
    # ip=15, opcode=2, oprand1=3, oprand2=1
    mem[3] = mem[1]
    # ip=18, opcode=1, oprand1=2, oprand2=184
    mem[2] = 184
    # ip=21, opcode=3, oprand1=1, oprand2=2
    mem[1] = (mem[1] + mem[2]) % 256
    # ip=24, opcode=6, oprand1=1, oprand2=0
    mem[1] = 0 if mem[1] != 0 else 1
    # ip=27, opcode=4, oprand1=0, oprand2=1
    mem[0] = (mem[0] * mem[1]) % 256
    # ここまでがパターン1回分の処理
    # 後略

コードを眺めてみると,1パターンのコードが繰り返されていることが判明.
多分文字数の分だけ繰り返されている.

最後の部分を見ると

    mem[0] = (mem[0] * mem[1]) % 256
    # ip=1083, opcode=6, oprand1=0, oprand2=0
    mem[0] = 0 if mem[0] != 0 else 1

つまりmem[0]が0でない数字ならmem[0]になってフラグと一致.

その前の行を見ると,mem[0] * mem[1]だから,mem[1]の結果が1文字だけの比較結果.
mem[0]が今までの結果を累積しているメモリだろう.

なんとかmem[0] != 0でない値にするには,プログラム内のmem[0] * mem[1]の演算が行われるときすべてにおいて,mem[1] != 0である必要がある.もし一度でもmem[1] == 0になってしまうと,フラグと一致しないといえるであろう.

てことは,mem[0] != 0(mem[1] != 0でもいい)がtrueになるstepの回数をキーに貪欲法でさがせばOK
よってvm.pyを以下のようにいじった.

solver.py
class VM:
    def __init__(self, program: bytes):
        self.program = program
        self.mem = [0] * 256
        self.ip = 0
        self.input_string = ""
        self.input_index = 0

    def _read_input_char(self) -> str:
        # ch = sys.stdin.read(1)
        # if ch == "":
        #     return "\x00"
        # return ch
        if self.input_index >= len(self.input_string):
            return "\x00"
        ch = self.input_string[self.input_index]
        self.input_index += 1
        return ch


    def step(self) -> bool:
        if self.ip < 0 or self.ip >= len(self.program):
            return False

        code = self.program
        opcode = code[self.ip]
        oprand1 = code[self.ip + 1]
        oprand2 = code[self.ip + 2]
        self.ip += 3
        if opcode == 0:
            ch = self._read_input_char()
            self.mem[oprand1] = ord(ch) % 256
        elif opcode == 1:
            self.mem[oprand1] = oprand2
        elif opcode == 2:
            self.mem[oprand1] = self.mem[oprand2]
        elif opcode == 3:
            self.mem[oprand1] = (self.mem[oprand1] + self.mem[oprand2]) % 256
        elif opcode == 4:
            self.mem[oprand1] = (self.mem[oprand1] * self.mem[oprand2]) % 256
        elif opcode == 5:
            self.mem[oprand1] = self.mem[oprand1] ^ self.mem[oprand2]
        elif opcode == 6:
            self.mem[oprand1] = 0 if self.mem[oprand1] != 0 else 1
        else:
            raise RuntimeError(f"Unknown opcode {opcode!r} at ip={self.ip}")
        return True

    def run(self, input_string: str, max_steps: int = 10_000_000) -> bool:
        self.mem = [0] * 256
        self.ip = 0
        self.input_string = input_string
        self.input_index = 0

        self.zero_count = 0
    
        steps = 0
        while steps < max_steps and self.step():
            steps += 1
            if self.mem[0] != 0:
                self.zero_count += 1
        return self.mem[0] == 0, self.zero_count

def main():
    with open("program", "rb") as f:
        program = f.read()

    vm = VM(program)

    flag = "Alpaca{"
    while True:
        max_step = -1
        max_char = ''
        for c in range(0x20, 0x7f):
            candidate = flag + chr(c) + '}'
            
            ret, steps = vm.run(candidate)
            if ret:
                print(f'\nFound flag: {candidate}')
                return
            if steps > max_step:
                max_step = steps
                max_char = chr(c)
        
        flag += max_char
        print(f'\r{flag}', end='')


if __name__ == "__main__":
    main()

Discussion