<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>How to solve?</title><description>Home</description><link>https://ma4the.github.io/</link><language>en</language><item><title>Codegate CTF 2026 Writeup (PhantomCore)</title><link>https://ma4the.github.io/posts/rev-phantomcore-codegate2026/</link><guid isPermaLink="true">https://ma4the.github.io/posts/rev-phantomcore-codegate2026/</guid><description>I see dead code.</description><pubDate>Fri, 10 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Challenge&lt;/strong&gt;: &lt;a href=&quot;https://github.com/sajjadium/ctf-archives/tree/main/ctfs/Codegate/2026/Quals/rev/PhantomCore&quot;&gt;PhantomCore - Codegate CTF 2026 Quals&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;  ┌──────────────┐      ioctl       ┌──────────────┐      MMIO       ┌──────────────┐
  │  gpuctl-cli  │ ───────────────▶ │  gpuctl.ko   │ ──────────────▶ │  phantom-gpu │
  │  (userspace) │    /dev/gpuctl   │   (kernel)   │      BAR0       │    (QEMU)    │
  └──────────────┘                  └──────────────┘                 └──────┬───────┘
                                                                            │
                      ┌─────────────────────────────────────────────────────┴─────┐
                      │                                                           │
                      ▼                                                           ▼
            ┌─────────────────┐                                      ┌────────────────────┐
            │   INIT PATH     │                                      │   RUNTIME PATH     │
            │                 │                                      │                    │
            │  blob @ 0xb0a2d8│                                      │  UPLOAD_SHADER     │
            │       ↓         │                                      │       ↓            │
            │  xorshift32     │                                      │  DISPATCH          │
            │(seed=0xfe170e07)│                                      │       ↓            │
            │       ↓         │                                      │  Validate + Exec   │
            │  CRC + LZ4      │                                      │       ↓            │
            │       ↓         │                                      │  opcode → hash     │
            │  0x308 bytes ───┼──────────────────────────────────────▶  → handler table   │
            │  (state blob)   │                                      │       ↓            │
            └─────────────────┘                                      │  regs/stack/DMA    │
                                                                     └───────────────────
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PhantomCore는 커스텀 QEMU PCI 장치(&lt;code&gt;phantom-gpu&lt;/code&gt;) 안에 구현된 VM을 분석해서, 도합 6개의 gate 조건을 모두 통과하는 shader bytecode를 작성하고, firmware blob에 숨겨진 hidden witness(레지스터 값 + buf1 32바이트)를 복원하여 최종 flag를 얻는 리버싱 문제이다.&lt;/p&gt;
&lt;p&gt;풀이는 크게 두 단계로 나뉜다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Gate 통과&lt;/strong&gt;: checker가 요구하는 6-bit 조건을 모두 만족하는 shader + buffer 조합을 찾는다&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flag 복원&lt;/strong&gt;: gate를 통과해도 success path가 읽는 추가 입력(reg0/reg3/reg4/reg5, buf1 raw 32바이트)이 정확해야만 올바른 flag가 출력된다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;아래는 리버싱 자체보다는 전체적인 구조와 흐름에 중점을 두고 작성한 라업이다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 전체 아키텍처&lt;/h2&gt;
&lt;h3&gt;1-1. 배경: GPU와 CPU의 관계&lt;/h3&gt;
&lt;p&gt;&amp;lt;div style=&quot;display: flex; gap: 10px; justify-content: center;&quot;&amp;gt;
&amp;lt;img src=&quot;/images/phantomcore/1.png&quot; style=&quot;width: 48%;&quot; /&amp;gt;
&amp;lt;img src=&quot;/images/phantomcore/2.png&quot; style=&quot;width: 48%;&quot; /&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;GPU는 CPU와 PCIe 버스로 연결된 별도 물리 장치다. CPU는 자신의 DRAM을, GPU는 별도 VRAM을 갖고 있고, 둘 사이의 데이터 이동은 PCIe를 통한 DMA로 이루어진다.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;display: flex; gap: 10px; justify-content: center;&quot;&amp;gt;
&amp;lt;img src=&quot;/images/phantomcore/3.png&quot; style=&quot;width: 48%;&quot; /&amp;gt;
&amp;lt;img src=&quot;/images/phantomcore/4.png&quot; style=&quot;width: 48%;&quot; /&amp;gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;Driver가 GPU와 통신하는 방법은 2가지다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MMIO (control plane)&lt;/strong&gt;: CPU가 BAR0에 매핑된 GPU 레지스터에 r/w해서 명령과 설정을 전달한다. 데이터 이동 없이 신호만 전달.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DMA (data plane)&lt;/strong&gt;: GPU가 PCIe Bus Master가 되어 CPU 개입 없이 Host Memory ↔ Device Memory 사이에서 직접 데이터를 전송한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;방향이 반대라서 분리된다. MMIO는 CPU → device, DMA는 device → RAM.&lt;/p&gt;
&lt;h3&gt;1-2. QEMU에서의 구현&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;참고&lt;/strong&gt;: &lt;a href=&quot;https://wiki.qemu.org/images/f/f6/PCIvsPCIe.pdf&quot;&gt;PCI vs PCIe - QEMU Wiki&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/images/phantomcore/5.png&quot; alt=&quot;5.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;기존 PCI는 CPU, Bridge/Memory Controller, Bus, 그리고 그 위에 연결된 장치들로 구성된 shared bus 구조였다. Host Bridge(노스브릿지)가 CPU/DMA와 버스를 연결하며, bus arbitration을 통해 버스 사용 권한이 결정된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/phantomcore/6.png&quot; alt=&quot;6.png&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;참고&lt;/strong&gt;: &lt;a href=&quot;https://www.owalle.com/2021/12/09/qemu-pci/&quot;&gt;QEMU PCI 장치 구현 - owalle&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 문제에서는 -machine q35를 사용하여 PCIe 기반 플랫폼을 에뮬레이션한다. PCIe에서는 Root Complex가 기존의 Host Bridge 및 메모리 컨트롤러 역할을 통합하며, 별도의 노스브릿지 칩이 존재하지 않는다. 또한 shared bus 대신 각 장치가 Root Complex와 point-to-point 링크로 연결되는 구조를 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/sys/bus/pci/devices&lt;/code&gt;로 확인한 장치 목록:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[0000:00] pcie.0 (Root Complex = q35 MCH)
    ├── 00:00.0  Intel 82G33 Host Bridge    vendor=8086:29c0  class=0x060000
    ├── 00:01.0  phantom-gpu (Accelerator)  vendor=1de5:7001  class=0x0b4000  &amp;lt;- DRIVER=gpuctl
    ├── 00:1f.0  ICH9 LPC Controller        vendor=8086:2918  class=0x060100
    ├── 00:1f.2  ICH9 SATA Controller       vendor=8086:2922  class=0x010601
    └── 00:1f.3  ICH9 SMBus Controller      vendor=8086:2930  class=0x0c0500
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;bus 구조에서 별도의 bridge나 switch가 존재하지 않으므로, 문제 환경은 [PCI Express Topology] 그림과 같이 Root Complex에서 Endpoint로 직접 연결되는 단일 PCIe 토폴로지임을 알 수 있고, 또  [PCI System Block Diagram]의 Graphics 위치에 대응되는 장치가 phantom-gpu라고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;한편 phantom-gpu는 BAR0을 통해 MMIO 영역이 매핑되어 있고,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BAR0: 0xfebfe000 ~ 0xfebfefff (4KB MMIO 영역)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이는 해당 디바이스의 레지스터 접근을 위한 메모리 공간을 의미한다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;참고&lt;/strong&gt;: &lt;a href=&quot;https://velog.io/@qct8377/QEMU-beginning&quot;&gt;QEMU 시작하기 - velog&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;QEMU 프로세스 메모리:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/phantomcore/7.png&quot; alt=&quot;7.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;실제 GPU는 PCIe로 연결된 외부 물리 장치지만, phantom-gpu는 QEMU 프로세스 내부에서 C 코드로 구현된 가상 디바이스다. 물리적으로 분리된 VRAM이 여기서는 같은 Guest RAM의 일부 영역으로 표현되고, &lt;code&gt;dma_memory_read/write()&lt;/code&gt;를 통해 접근한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;QEMU 추상화 레이어:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/phantomcore/8.png&quot; alt=&quot;8.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;QEMU 내부 모듈:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TCG&lt;/strong&gt;: Guest x86_64 명령어를 JIT으로 번역해서 Host CPU에서 실행하는 에뮬레이션 엔진&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MMU&lt;/strong&gt;: Guest 가상주소 → 물리주소 변환. BAR0 MMIO 접근 시 MemoryRegion 디스패치로 콜백&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;QDEV&lt;/strong&gt;: PCI 장치 에뮬레이션 프레임워크. phantom-gpu가 여기 등록됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;문제 환경은 &lt;code&gt;enable-kvm&lt;/code&gt; 없이 순수 TCG 모드로 실행된다. 실행 중 MMIO 주소(BAR0 영역)를 건드리면 해당 주소에 매핑된 MemoryRegion 콜백이 트리거돼서 phantom-gpu handler로 진입하는 구조다.&lt;/p&gt;
&lt;p&gt;참고로 QEMU 공식 PCI 디바이스 구현 예제는 &lt;a href=&quot;https://github.com/qemu/qemu/blob/master/hw/misc/edu.c&quot;&gt;&lt;code&gt;hw/misc/edu.c&lt;/code&gt;&lt;/a&gt;에서 확인할 수 있다.&lt;/p&gt;
&lt;p&gt;phantom-gpu는 공식과 유사하게 BAR0 MemoryRegion 등록, DMA 접근, 인터럽트 처리 구조를 따르며, 그 위에 shader VM이 추가된 형태로 보인다.&lt;/p&gt;
&lt;h3&gt;1-3. phantom-gpu vs. 실제 GPU&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/phantomcore/9.png&quot; alt=&quot;9.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;실제 GPU Compute Core는 병렬 하드웨어지만, phantom-gpu VM executor는 단순히 instruction을 하나씩 읽고 실행하는 순차적인 소프트웨어 interpreter다. 별도의 MMU도 없고, memory isolation도 없다. firmware blob을 디코드해서 device 내부 struct에 S-box, constants, key material 등을 세팅하고, 이게 곧 VM 실행에 필요한 모든 global state가 된다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 문제 파일 분석&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;├── bios/
│   ├── bios-256k.bin          &amp;lt;- 필요
│   ├── efi-e1000e.rom         &amp;lt;- 필요X (-nic none)
│   ├── efi-virtio.rom         &amp;lt;- 필요X
│   ├── kvmvapic.bin           &amp;lt;- 필요X (KVM 사용 안함, only TCG)
│   ├── linuxboot_dma.bin      &amp;lt;- 필요X
│   └── vgabios-stdvga.bin     &amp;lt;- 필요X (-vga none)
├── bzImage                    # Guest 리눅스 커널
├── initramfs.cpio.gz          # Guest 루트파일시스템
├── qemu-system-x86_64         # 커스텀 QEMU 바이너리 &amp;lt;- 분석 대상
└── run.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;bios/&lt;/code&gt; 디렉토리에 6개 파일이 있지만 실제로 필요한 것은 &lt;code&gt;bios-256k.bin&lt;/code&gt; 하나뿐이다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;-machine q35&lt;/code&gt;가 BIOS를 요구하고, 이것이 Guest Linux 커널 부팅 전 POST 과정을 담당한다. 나머지 5개 파일은 원본 run.sh에 &lt;code&gt;-nic none&lt;/code&gt;, &lt;code&gt;-vga none&lt;/code&gt; 옵션만 추가하면 전혀 필요가 없다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;initramfs 풀기:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir initramfs_root &amp;amp;&amp;amp; cd initramfs_root
gzip -dc ../initramfs.cpio.gz | cpio -idmv
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;.
├── bin
│   ├── busybox
│   ├── cat -&amp;gt; busybox
│   ├── chmod -&amp;gt; busybox     ├── hexdump -&amp;gt; busybox
│   ├── cp -&amp;gt; busybox        ├── insmod -&amp;gt; busybox
│   ├── dd -&amp;gt; busybox        ├── ls -&amp;gt; busybox
│   ├── dmesg -&amp;gt; busybox     ├── mount -&amp;gt; busybox
│   ├── echo -&amp;gt; busybox      ├── od -&amp;gt; busybox
│   ├── grep -&amp;gt; busybox      └── xxd -&amp;gt; busybox
├── init
├── lib
│   └── modules
│       └── gpuctl.ko        &amp;lt;- 커널 모듈
└── usr
    └── local
        └── bin
            └── gpuctl-cli   &amp;lt;- 유저랜드 CLI
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;init&lt;/code&gt; 스크립트가 &lt;code&gt;gpuctl.ko&lt;/code&gt;를 &lt;code&gt;insmod&lt;/code&gt;하고, &lt;code&gt;/dev/gpuctl&lt;/code&gt; 캐릭터 디바이스를 생성한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mkdir -p /dev/pts
mount -t devpts none /dev/pts
mount -t tmpfs none /tmp

export PATH=/bin:/sbin:/usr/local/bin

# Load phantom-gpu driver
insmod /lib/modules/gpuctl.ko
sleep 0.5

echo &quot;================================================&quot;
echo &quot;  PhantomCore&quot;
echo &quot;  /dev/gpuctl is ready&quot;
echo &quot;  Use gpuctl-cli to interact with the device&quot;
echo &quot;================================================&quot;

# Drop to shell
export PATH=/bin:/sbin:/usr/local/bin
export HOME=/tmp
cd /tmp
setsid cttyhack sh

poweroff -f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;run.sh 실행:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;================================================
  PhantomCore
  /dev/gpuctl is ready
  Use gpuctl-cli to interact with the device
================================================
BusyBox v1.36.1 (Ubuntu 1:1.36.1-6ubuntu3.1) built-in shell (ash)
Enter &apos;help&apos; for a list of built-in commands.

~ # gpuctl-cli
Usage: gpuctl-cli &amp;lt;command&amp;gt; [args...]
  upload   &amp;lt;shader.bin&amp;gt;              Upload shader bytecode
  setbuf   &amp;lt;slot&amp;gt; &amp;lt;file.bin&amp;gt;         Set buffer slot (0-3)
  dispatch [max_steps]               Dispatch shader execution
  run      &amp;lt;shader.bin&amp;gt; [buf0] [buf1]  One-stop upload+set+dispatch
  log                                Read last execution log
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;3. gpuctl-cli 리버싱 (Userland)&lt;/h2&gt;
&lt;p&gt;먼저 gpuctl-cli를 보자.
IDA에서 &lt;code&gt;&quot;Usage: %s &amp;lt;command&amp;gt;&quot;&lt;/code&gt; 문자열 xref로 main 함수(&lt;code&gt;sub_401BA0&lt;/code&gt;)를 찾았다.&lt;/p&gt;
&lt;p&gt;함수를 보면 upload / setbuf / dispatch / log는 각각 ioctl 한 번 호출하는 단순 wrapper고, run 명령은 저 전부를 한번에 해준다는 것을 알 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int cli_main(int argc, char **argv) {
    if (argc &amp;lt;= 1) {
        print_usage();
        return 1;
    }
    int fd = open(&quot;/dev/gpuctl&quot;, O_RDWR);
    if (fd &amp;lt; 0) {
        perror(&quot;open(/dev/gpuctl)&quot;);
        return 1;
    }
    const char *cmd = argv[1];

    if (!strcmp(cmd, &quot;upload&quot;)) {
        if (argc != 3) error(&quot;upload: need &amp;lt;shader.bin&amp;gt;&quot;);
        else rc = upload_shader(fd, argv[2]);
    }
    else if (!strcmp(cmd, &quot;setbuf&quot;)) {
        if (argc &amp;lt;= 3) error(&quot;setbuf: need &amp;lt;slot&amp;gt; &amp;lt;file&amp;gt;&quot;);
        else {
            slot = parse_uint(argv[2]);
            if (slot &amp;gt; 3) error(&quot;slot must be 0-3&quot;);
            else rc = set_buffer_from_file(fd, slot, argv[3]);
        }
    }
    else if (!strcmp(cmd, &quot;dispatch&quot;)) {
        max_steps = (argc == 2) ? 0x100000 : parse_uint(argv[2]);
        out = calloc(1, 0x4000);
        rc = dispatch(fd, max_steps, out);
        free(out);
    }
    else if (!strcmp(cmd, &quot;run&quot;)) {
        if (argc == 2) error(&quot;run: need at least &amp;lt;shader.bin&amp;gt;&quot;);
        else {
            upload_shader(fd, argv[2]);
            if (argc &amp;gt;= 4) set_or_clear(fd, 0, argv[3]);  // buf0
            else           clear_buf(fd, 0);
            if (argc &amp;gt;= 5) set_or_clear(fd, 1, argv[4]);  // buf1
            else           clear_buf(fd, 1);
            clear_buf(fd, 2);   // buf2: 16KB zeroed  ← flag output 영역
            clear_buf(fd, 3);   // buf3: 16KB zeroed  ← stack 영역
            out = calloc(1, 0x4000);
            rc = dispatch(fd, 0x100000, out);
            if (!rc &amp;amp;&amp;amp; read_log(fd, &amp;amp;logrec, 8) &amp;gt; 0) {
                print_log(logrec);
                if (logrec.status == 8)   // GATE_TRIGGERED
                    dump_buf2_as_flag(out);
            }
            free(out);
        }
    }
    else if (!strcmp(cmd, &quot;log&quot;)) {
        if (read_log(fd, &amp;amp;logrec, 8) &amp;lt; 0) perror(&quot;read(log)&quot;);
        else print_log(logrec);
    }
    else {
        print_unknown_command(cmd);
        print_usage();
        rc = 1;
    }
    close(fd);
    return rc;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;유저인 우리는 &lt;code&gt;run&lt;/code&gt;명령어를 써서 shader, buf0, buf1을 올리면 된다. buf2(flag output)와 buf3(stack)은 &lt;code&gt;run&lt;/code&gt;이 알아서 16KB zeroed 버퍼로 할당해준다.&lt;/p&gt;
&lt;p&gt;코드 중에서 특히 &lt;code&gt;logrec.status == 8&lt;/code&gt; 체크가 눈에 띄는데, 나중에 알게 되겠지만 이게  &lt;code&gt;GATE_TRIGGERED&lt;/code&gt; 상태이다. 이 조건을 만족해야만 buf2가 flag로 dump된다.&lt;/p&gt;
&lt;p&gt;또 ioctl 코드는 upload=&lt;code&gt;0x40105001&lt;/code&gt;, setbuf=&lt;code&gt;0x40105002&lt;/code&gt;, dispatch=&lt;code&gt;0xC0185003&lt;/code&gt;이다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. gpuctl.ko 리버싱 (Kernel Module)&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;참고 자료&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ctf.re/kernel/pcie/tutorial/dma/mmio/tlp/2024/03/26/pcie-part-2/&quot;&gt;PCIe Part 2: DMA, MMIO, TLP - ctf.re&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yairgadelov.me/pcie-bar0-and-dma-explained-with-qemu/&quot;&gt;PCIe BAR0 and DMA Explained with QEMU - Yair Gadelov&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;다음으로 gpuctl.ko의 구조를 이해하려면 PCIe DMA의 표준 흐름을 먼저 알아야 한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/phantomcore/10.png&quot; alt=&quot;10.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;위 그림과 다르게 phantom-gpu에서는 물리적 PCIe TLP가 없다. &lt;code&gt;iowrite32(bar0+0x30)&lt;/code&gt;이 guest MMIO trap을 유발하면, QEMU MemoryRegion 콜백이 &lt;code&gt;dma_memory_read(cmd_ring_dma)&lt;/code&gt;로 guest RAM을 직접 읽는다. driver 관점에서의 인터페이스는 동일하지만 PCIe fabric 없이 동작한다.&lt;/p&gt;
&lt;p&gt;gpuctl.ko는 이 흐름을 그대로 구현한 순수 전달 레이어다. PCIe driver의 전형적인 패턴 — DMA buffer 할당 → device에 주소 전달 → doorbell로 시작 → interrupt로 완료 수신 — 을 크게 &lt;strong&gt;초기화(probe)&lt;/strong&gt;, &lt;strong&gt;커맨드 전달(ioctl)&lt;/strong&gt;, &lt;strong&gt;완료 수신(IRQ)&lt;/strong&gt; 세 파트로 구현한다.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;4-1. init_modul에서 probe까지&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;unk_11A0&lt;/code&gt;이 &lt;code&gt;pci_driver&lt;/code&gt; 구조체다. IDA에서 해당 주소를 보면:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__int64 init_module() {
    return pci_register_driver(&amp;amp;pci_driver_struct, &amp;amp;_this_module, &quot;gpuctl&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;.data:11B0  dq offset &quot;gpuctl&quot;          → .name
.data:11B8  dq offset id_table          → .id_table  (vendor=1DE5, device=7001)
.data:11C0  dq offset sub_650           → .probe
.data:11C8  dq offset sub_30            → .remove
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pci_register_driver&lt;/code&gt;가 이 구조체를 커널에 등록하면, 커널이 PCI 버스를 스캔하다가 vendor=&lt;code&gt;1DE5&lt;/code&gt;, device=&lt;code&gt;7001&lt;/code&gt;인 장치(phantom-gpu)를 발견하면 &lt;code&gt;.probe&lt;/code&gt; = &lt;code&gt;sub_650&lt;/code&gt;함수를 자동 호출한다.&lt;/p&gt;
&lt;h3&gt;4-2. probe(sub_650) — PCI 초기화 및 DMA 할당&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;PCI 활성화 및 BAR0 매핑&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pci_enable_device(pdev);
pci_request_regions(pdev, &quot;gpuctl&quot;);
gd-&amp;gt;bar0 = pci_iomap(pdev, 0, 4096);

// Signature 검증
sig = ioread32(gd-&amp;gt;bar0 + 0x00);
// sig == 0x5048544D (&quot;PHTM&quot;) 이어야 함
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pci_iomap&lt;/code&gt;이 BAR0 물리주소를 커널 가상주소로 매핑한다. 이후 &lt;code&gt;ioread32/iowrite32&lt;/code&gt;로 이 주소를 읽고 쓰면 MMIO 접근이 발생하고, QEMU의 MemoryRegion 콜백이 트리거된다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;DMA buffer 할당 및 주소 전달&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;PCIe에서 device가 host RAM에 직접 접근하려면, 먼저 OS가 contiguous physical memory를 잡아주고 그 물리주소를 device에 알려줘야 한다.
이때, 주소를 BAR0에 기록한다고 데이터 전송이 시작되는 게 아니고 device에게 나중에 여기를 읽으라고 위치를 알려주는 것뿐이다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// OS에 DMA buffer 할당 요청
gd-&amp;gt;cmd_ring = dma_alloc_attrs(&amp;amp;pdev-&amp;gt;dev, 0x1000, &amp;amp;gd-&amp;gt;cmd_ring_dma, ...);
gd-&amp;gt;log_ring = dma_alloc_attrs(&amp;amp;pdev-&amp;gt;dev, 0x1000, &amp;amp;gd-&amp;gt;log_ring_dma, ...);
gd-&amp;gt;shader   = dma_alloc_attrs(&amp;amp;pdev-&amp;gt;dev, 0x2000, &amp;amp;gd-&amp;gt;shader_dma,   ...);
for (i = 0; i &amp;lt; 4; i++)
    gd-&amp;gt;slot[i] = dma_alloc_attrs(&amp;amp;pdev-&amp;gt;dev, 0x4000, &amp;amp;gd-&amp;gt;slot_dma[i], ...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;dma_alloc_attrs&lt;/code&gt;의 세 번째 인자(&lt;code&gt;&amp;amp;gd-&amp;gt;cmd_ring_dma&lt;/code&gt;)가 물리주소를 받아오는 포인터다. device는 이 물리주소로 guest RAM에 직접 접근한다.&lt;/p&gt;
&lt;p&gt;그 다음 이 주소들을 BAR0 레지스터에 기록해서 phantom-gpu에 알려준다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// DMA 물리주소 → BAR0에 기록
iowrite32(lower_32_bits(gd-&amp;gt;cmd_ring_dma), gd-&amp;gt;bar0 + 0x20);  // cmd_ring lo
iowrite32(upper_32_bits(gd-&amp;gt;cmd_ring_dma), gd-&amp;gt;bar0 + 0x24);  // cmd_ring hi
iowrite32(lower_32_bits(gd-&amp;gt;log_ring_dma), gd-&amp;gt;bar0 + 0x40);
iowrite32(upper_32_bits(gd-&amp;gt;log_ring_dma), gd-&amp;gt;bar0 + 0x44);
// slot 0~3도 동일하게 0x50~0x6C에 기록
iowrite32(0, gd-&amp;gt;bar0 + 0x30);  // doorbell 초기화
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이것이 PCIe DMA의 Source Address 프로그래밍 단계다. phantom-gpu는 이 주소들을 내부적으로 저장해두고, 이후 커맨드가 올 때 해당 guest 메모리 영역을 직접 읽는다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;IRQ 등록 및 Firmware init&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// MSI interrupt handler 등록
request_threaded_irq(irq, sub_5E0, NULL, 0x80, &quot;gpuctl&quot;, gd);

// Firmware init trigger
iowrite32(1, gd-&amp;gt;bar0 + 0x100);
_mm_mfence();

// QEMU가 firmware blob 디코드 완료할 때까지 polling
for (i = 0; i &amp;lt; 100; i++) {
    if (ioread32(gd-&amp;gt;bar0 + 0x104) == 1) break;
    udelay(4295);
}

pci_set_master(pdev);          // device가 DMA 주체가 될 수 있음
cdev_add(&amp;amp;gd-&amp;gt;cdev, ...);      // /dev/gpuctl 생성
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;bar0+0x100&lt;/code&gt;에 1을 쓰는 것이 firmware init doorbell이다. QEMU가 이 write를 받아 내부 firmware blob을 디코드하고 gate 상수들을 세팅한 뒤, &lt;code&gt;bar0+0x104&lt;/code&gt;를 1로 바꿔 완료를 알린다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;4-3. ioctl handler (sub_370) - 커맨드 전달&lt;/h3&gt;
&lt;p&gt;gpuctl-cli가 &lt;code&gt;/dev/gpuctl&lt;/code&gt;에 ioctl을 보내면 이 함수가 받는다. &lt;code&gt;sub_370&lt;/code&gt;이 ioctl handler인 건 probe에서 &lt;code&gt;cdev_init(gd-&amp;gt;cdev, &amp;amp;gpuctl_fops)&lt;/code&gt;로 등록한 파일 ops의 &lt;code&gt;.unlocked_ioctl&lt;/code&gt; 필드에 이 함수가 들어있기 때문이다.&lt;/p&gt;
&lt;p&gt;이 함수는 세 가지 커맨드를 처리한다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mutex_lock(&amp;amp;gd-&amp;gt;lock);
switch (cmd) {

case 0x40105001:  // UPLOAD_SHADER
    copy_from_user(gd-&amp;gt;shader, user_ptr, size);   // user → DMA buffer
    push_cmd(gd, CMD_UPLOAD, size, shader_dma);   // cmd ring에 기록 + doorbell
    wait_event(gd-&amp;gt;wq, gd-&amp;gt;irq_done);             // IRQ 올 때까지 대기
    break;

case 0x40105002:  // SET_BUF
    copy_from_user(gd-&amp;gt;slot[slot], user_ptr, size);
    push_cmd(gd, CMD_SETBUF, slot|(size&amp;lt;&amp;lt;32), slot_dma[slot]);
    wait_event(gd-&amp;gt;wq, gd-&amp;gt;irq_done);
    break;

case 0xC0185003:  // DISPATCH
    push_cmd(gd, CMD_DISPATCH, max_steps, 0);
    wait_event(gd-&amp;gt;wq, gd-&amp;gt;irq_done);
    copy_to_user(user_out, gd-&amp;gt;slot[2], min(out_size, 0x4000));  // 결과 반환
    break;
}
mutex_unlock(&amp;amp;gd-&amp;gt;lock);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 때  &lt;code&gt;push_cmd&lt;/code&gt;(&lt;code&gt;sub_160&lt;/code&gt;)의 역할이 중요하다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. cmd ring의 다음 슬롯에 64바이트 커맨드 기록 (DMA buffer에 씀)
memcpy(&amp;amp;cmd_ring[tail], &amp;amp;cmd, 64);

// 2. doorbell write → QEMU MMIO write handler 트리거
iowrite32(next_idx, gd-&amp;gt;bar0 + 0x30);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cmd_ring&lt;/code&gt;은 streaming DMA ring buffer 패턴으로 동작한다. driver가 tail(Producer index)을 올리며 커맨드를 채우고, phantom-gpu가 head(Consumer index)를 소비한다.&lt;/p&gt;
&lt;p&gt;이때 &lt;strong&gt;DMA buffer를 먼저 채운 뒤 doorbell로 device를 깨우는 것&lt;/strong&gt;이 PCIe DMA의 표준적인 순서이고, &lt;code&gt;iowrite32(next_idx, bar0+0x30)&lt;/code&gt;이 그 신호라고 할 수 있다. doorbell write 이후로는 phantom-gpu는 DMA로 &lt;code&gt;cmd_ring&lt;/code&gt;을 읽어 커맨드를 처리한다.&lt;/p&gt;
&lt;h3&gt;4-5. IRQ handler (sub_5E0) - 완료 통보&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;irqreturn_t irq_handler(int irq, void *gd) {
    u32 status = ioread32(gd-&amp;gt;bar0 + 0x14);   // completion status 확인
    if (status) {
        gd-&amp;gt;log_record = *(u64 *)gd-&amp;gt;log_ring; // log 읽기
        gd-&amp;gt;has_log = 1;
        iowrite32(status, gd-&amp;gt;bar0 + 0x18);    // IRQ ACK
        wake_up(&amp;amp;gd-&amp;gt;wq);                       // ioctl handler 깨움
    }
    return IRQ_HANDLED;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DISPATCH 완료 후 phantom-gpu가 MSI를 발생시키면 이 ISR이 깨어난다. &lt;code&gt;wait_event&lt;/code&gt;로 block 중이던 ioctl handler를 깨우고, 이후 &lt;code&gt;copy_to_user&lt;/code&gt;로 결과(slot2 = flag output 영역)가 user 쪽으로 전달된다.&lt;/p&gt;
&lt;h3&gt;4-6. 전체 흐름&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;gpuctl-cli
    │ ioctl(fd, DISPATCH, ...)
    ▼
sub_370 (ioctl handler)
    ├─ copy_from_user → DMA buffer (slot[0], slot[1])
    ├─ push_cmd
    │     ├─ cmd_ring[tail]에 커맨드 기록  ← streaming ring buffer
    │     └─ iowrite32(next_idx, bar0+0x30)  ← doorbell
    │                   │
    │        실제 HW:    └─ PCIe TLP → device DMA engine → host RAM 읽기
    │        QEMU:      └─ MMIO trap → MemoryRegion 콜백
    │                        └─ dma_memory_read(cmd_ring_dma)  ← guest RAM 직접
    │                                    │
    │                      shader validation + VM execution
    │                      MSI interrupt 발생
    │                   │
    └─ wait_event ◀─────┘
                        │
                    sub_5E0 (IRQ handler)
                        ioread32(bar0+0x14)   completion status
                        iowrite32(bar0+0x18)  ACK
                        wake_up(&amp;amp;gd-&amp;gt;wq)
                        │
    ioctl handler 재개 ◀┘
    copy_to_user(slot[2]) → flag output 반환
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;앞서 설명했지만 gpuctl.ko는 모든 요청을 DMA 버퍼에 복사하고 doorbell을 울릴 뿐인 순수한 전달 레이어다. 진짜 분석해야할 대상은 &lt;code&gt;qemu-system-x86_64&lt;/code&gt; 바이너리이다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. QEMU phantom-gpu 리버싱&lt;/h2&gt;
&lt;p&gt;qemu의 경우는 우선 IDA에서 &lt;code&gt;&quot;phantom-gpu&quot;&lt;/code&gt; 문자열 xref를 추적해서 가장 주요한 함수들을 찾았다:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;함수&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sub_4277B0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PCI class init; &lt;code&gt;&quot;Phantom GPU (CTF Challenge Device)&quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sub_427D10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;device realize; MMIO region 등록, BAR 활성화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sub_427C10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MMIO &lt;strong&gt;read&lt;/strong&gt; handler; signature/version 반환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;sub_428AB0&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;MMIO write handler&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;sub_428AB0&lt;/code&gt;는 약 6천 줄짜리 함수로, firmware init / DMA command 처리 / shader validation / VM execution / post-gate success path 까지 핵심 로직들이 전부 들어있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/phantomcore/11.png&quot; alt=&quot;11.png&quot; /&gt;
&lt;em&gt;&amp;lt;center&amp;gt;GPU 아키텍처 관점에서의 동작&amp;lt;/center&amp;gt;&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;5-1. Firmware Blob 추출 및 디코드&lt;/h3&gt;
&lt;p&gt;앞서 언급한 함수인 &lt;code&gt;sub_428AB0&lt;/code&gt;에서 &lt;code&gt;offset == 0x100&lt;/code&gt; 분기가 firmware init 경로다. &lt;code&gt;0xB0A2D8&lt;/code&gt;의 embedded blob을 참조한다.&lt;/p&gt;
&lt;p&gt;firmware 디코딩 체인은 단순하다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;; ① xorshift32 — 0x428FE8
mov  edx, 0FE170E07h          ; seed
loc_428FE8:
    mov  eax, edx
    shl  eax, 0Dh             ; state ^= state &amp;lt;&amp;lt; 13
    xor  eax, edx
    mov  edx, eax
    shr  edx, 11h             ; state ^= state &amp;gt;&amp;gt; 17
    xor  eax, edx
    mov  edx, eax
    shl  edx, 5               ; state ^= state &amp;lt;&amp;lt; 5
    xor  edx, eax
    xor  [rcx], dl             ; blob[i] ^= low_byte(state)
    add  rcx, 1
    cmp  rcx, rbp              ; 0x2E1 bytes
    jnz  loc_428FE8

; ② CRC32 — 0x42901C
mov  eax, 0FFFFFFFFh           ; CRC init
loc_429028:
    movzx edx, byte ptr [rsi]
    xor   edx, eax
    shr   eax, 8
    movzx edx, dl
    xor   eax, [rcx+rdx*4]    ; table @ 0x19EEF00
    cmp   rsi, rbp
    jnz   loc_429028
cmp  eax, 0AEF1393Ah          ; expected CRC

; ③ LZ4 raw block — 0x429080
movzx ebx, byte ptr [rdx]     ; token byte
    shr  al, 4                 ; literal_length = token &amp;gt;&amp;gt; 4
    cmp  al, 0Fh               ; if 0xF → extended length loop
    jz   loc_429522
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;아래 스크립트로 firmware blob을 추출할 수 있다.&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;extract.py&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
import struct, zlib, sys, lz4.block
from pathlib import Path

qemu = Path(&quot;qemu-system-x86_64&quot;).read_bytes()
enc = bytearray(qemu[0xB0A2D8 : 0xB0A2D8 + 0x2E1])

# xorshift32 decrypt (seed=0xFE170E07)
state = 0xFE170E07
for i in range(len(enc)):
    state ^= (state &amp;lt;&amp;lt; 13) &amp;amp; 0xFFFFFFFF
    state ^= state &amp;gt;&amp;gt; 17
    state ^= (state &amp;lt;&amp;lt; 5) &amp;amp; 0xFFFFFFFF
    state &amp;amp;= 0xFFFFFFFF
    enc[i] ^= (state &amp;amp; 0xFF)

# CRC32 verify — pre-final XOR value == 0xAEF1393A
crc_raw = zlib.crc32(bytes(enc)) ^ 0xFFFFFFFF &amp;amp; 0xFFFFFFFF
assert crc_raw == 0xAEF1393A, f&quot;CRC mismatch: {crc_raw:#x} vs 0xAEF1393A&quot;

# LZ4 raw block decompress -&amp;gt; firmware blobs
firmware = lz4.block.decompress(bytes(enc), uncompressed_size=0x308)
Path(&quot;firmware_decompressed.bin&quot;).write_bytes(firmware)
print(f&quot;OK: {len(firmware)} bytes → firmware_decompressed.bin&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;5-2. Firmware → Device State 복사&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;LZ4 압축해제 이후의 &lt;code&gt;0x308&lt;/code&gt;바이트 blob은 device state 구조체의 여러 오프셋으로 복사된다. IDA에서 추적한 &lt;code&gt;mov&lt;/code&gt;/&lt;code&gt;rep movsq&lt;/code&gt; 어셈으로 구조를 복원하면 이와 같다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  typedef struct PhantomGPUState {
  uint8_t  _pci_header[0xC00];

  uint8_t  key_seed[8];
  uint8_t  blob_copy[0x308];

  uint64_t blob_tail_qword;

  uint8_t  trace_pattern[8];
  uint32_t gate_reg7;
  uint32_t gate_buf2_dw0;
  uint32_t gate_crc_buf1;
  uint32_t gate_xor_r0r3;

  uint32_t post_f28;
  uint32_t post_f2c;

  uint64_t dispatch_table[256];

  uint8_t  _dma_addrs[];

  uint32_t reg[16];
  uint32_t pc;
  uint32_t sp;
  uint32_t cmp_flag;
  
  uint8_t  active_shader[0x2000];

  uint8_t  trace_ring[16];
  uint32_t trace_count;

  uint32_t gate_armed;
  uint32_t vm_status;
  uint32_t fault_code;

} PhantomGPUState;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;5-3. Phase 1 Transform — Grammar A / Grammar B&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;firmware blob → gate 상수 파생 과정을 Phase 1 Transform이라고 명명하겠다. 이때 IDA &lt;code&gt;0x429240&lt;/code&gt;~&lt;code&gt;0x429472&lt;/code&gt; 구간에 두 가지 유의미한 패턴이 반복된다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Grammar A (stream XOR) — trace pattern 생성&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0x429240&lt;/code&gt;~&lt;code&gt;0x429261&lt;/code&gt; 루프:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;loc_429240:
    lea  edx, [ecx+eax]         ; edx = (seed + i) — seed는 blob[0x08]의 하위 바이트
    movzx edx, dl               ; dl = (seed + i) &amp;amp; 0xFF
    movzx edx, byte ptr [rdi+rdx+0D0Ch]  ; dev[0xD0C + dl] = sbox[dl]
    xor   dl, [rax+0C07h]       ; ^ blob[i-1] (dev[0xC08+i-1])
    mov   [rax+0F0Fh], dl       ; → dev[0xF10+i-1] (trace pattern)
    add   rax, 1
    cmp   rax, rsi              ; 8회 반복
    jnz   loc_429240
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pseudo-code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Grammar A: sbox byte XOR
for (i = 0; i &amp;lt; 8; i++)

dev[0xF10 + i] = sbox[(seed + i) &amp;amp; 0xFF] ^ blob[i];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;seed&lt;/code&gt;는 &lt;code&gt;blob[0x08:0x0C]&lt;/code&gt;의 하위 바이트(= &lt;code&gt;dev[0xC10]&lt;/code&gt;), &lt;code&gt;sbox&lt;/code&gt;는 &lt;code&gt;dev[0xD0C]&lt;/code&gt; = &lt;code&gt;blob[0xFC:0x1FC]&lt;/code&gt;이다. 그리고 결과가 &lt;code&gt;50 11 21 40 20 11 51 41&lt;/code&gt; — gate bit 0의 trace 기대값이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Grammar B (pack_perm XOR) — 32-bit 상수 생성&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;0x42926B&lt;/code&gt;~&lt;code&gt;0x4292C1&lt;/code&gt;이 첫 번째 Grammar B 블록이다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mov  edx, [rbx+0C18h]       ; edx = selector (blob 내 4바이트 값)
lea  eax, [rdx+1]           ; idx+1
lea  ecx, [rdx+2]           ; idx+2
movzx eax, byte ptr [rbx+rax+0D0Ch]  ; sbox[(selector+1) &amp;amp; 0xFF]
movzx ecx, byte ptr [rbx+rcx+0D0Ch]  ; sbox[(selector+2) &amp;amp; 0xFF]
shl  eax, 8                 ; byte1 &amp;lt;&amp;lt; 8
shl  ecx, 10h               ; byte2 &amp;lt;&amp;lt; 16
or   eax, ecx
movzx ecx, dl               ; sbox[(selector+0) &amp;amp; 0xFF]
movzx ecx, byte ptr [rbx+rcx+0D0Ch]
add  edx, 3
movzx edx, byte ptr [rbx+rdx+0D0Ch]  ; sbox[(selector+3) &amp;amp; 0xFF]
or   eax, ecx               ; | byte0
shl  edx, 18h               ; byte3 &amp;lt;&amp;lt; 24
or   eax, edx               ; = pack_perm 결과
xor  eax, [rbx+0C14h]       ; blob에서 가져온 상수랑 XOR
mov  [rbx+0F18h], eax       ; → dev[0xF18] (gate_reg7)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pseudo-code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Grammar B: 4-byte S-box lookup → u32 → XOR
uint32_t pack_perm(uint8_t *sbox, uint32_t idx) {
    return sbox[(idx+0) &amp;amp; 0xFF]        |
          (sbox[(idx+1) &amp;amp; 0xFF] &amp;lt;&amp;lt; 8)  |
          (sbox[(idx+2) &amp;amp; 0xFF] &amp;lt;&amp;lt; 16) |
          (sbox[(idx+3) &amp;amp; 0xFF] &amp;lt;&amp;lt; 24);
}

dev[0xF18] = pack_perm(sbox, blob_dword(selector_offset)) ^ blob_dword(xor_offset);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 패턴이 6번 반복되면서 gate 상수 6개를 생성한다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// selector 오프셋 → XOR 상수 → 저장 주소
dev[0xF18] = pack_perm(sbox, blob[0x10]) ^ blob[0x0C]   // gate_reg7    = 0xA5A5A5A5
dev[0xF1C] = pack_perm(sbox, blob[0x18]) ^ blob[0x14]   // gate_buf2    = 0xC0DEF00D
dev[0xF20] = pack_perm(sbox, blob[0x20]) ^ blob[0x1C]   // gate_crc     = 0x2A8DED84
dev[0xF24] = pack_perm(sbox, blob[0x58]) ^ blob[0x54]   // gate_xor     = 0xCC99E897
dev[0xF28] = pack_perm(sbox, blob[0x28]) ^ blob[0x24]   // post_f28     = 0x13579BDF
dev[0xF2C] = pack_perm(sbox, blob[0x30]) ^ blob[0x2C]   // post_f2c     = 0x2468ACE0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;대부분 &lt;code&gt;blob[0x0C]&lt;/code&gt;부터 8바이트 간격으로 순차 배치되지만, f24(gate_xor)만 &lt;code&gt;blob[0x54]/blob[0x58]&lt;/code&gt;로 떨어져 있다.&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;5-4. Dispatch Table 초기화&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Phase 1 Transform 직후 dispatch table이 초기화된다. &lt;code&gt;unk_1719400&lt;/code&gt;에 15개 &lt;code&gt;(opcode, handler)&lt;/code&gt; 쌍이 정적으로 정의되어 있고, firmware blob의 scramble 값으로 최종 slot 위치를 결정한다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 0x4294C2 — dispatch table 초기화 루프
for (int i = 0; i &amp;lt; 15; i++) {
    uint8_t  opcode  = static_table[i].opcode;      // unk_1719400
    void    *handler = static_table[i].handler;
    uint8_t  slot    = (13 * opcode + scramble_data[opcode] + blob[0x304]) &amp;amp; 0xFF;
    dev-&amp;gt;dispatch_table[slot] = handler;
}
// scramble_data = blob[0x1FC:0x2FC], blob[0x304] = global constant
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 scramble이 없으면 opcode → handler 매핑을 알 수 없으므로, firmware 디코딩이 VM 분석의 선행 조건이다.&lt;/p&gt;
&lt;p&gt;디코딩 후 복원한 ISA — 15개 opcode, 4바이트 고정 길이 (&lt;code&gt;byte0&lt;/code&gt;=opcode, &lt;code&gt;byte1&lt;/code&gt;=Rd, &lt;code&gt;byte2&lt;/code&gt;=Rs/Rb/buf, &lt;code&gt;byte3&lt;/code&gt;=imm/offset):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Opcode&lt;/th&gt;
&lt;th&gt;Mnemonic&lt;/th&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x01&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MOV Rd, Rs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reg[Rd] = reg[Rs]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x02&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LI Rd, imm8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reg[Rd] = sign_extend(imm8)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ADD Rd, Rs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reg[Rd] += reg[Rs]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x11&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;XOR Rd, Rs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reg[Rd] ^= reg[Rs]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x12&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AND Rd, Rs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reg[Rd] &amp;amp;= reg[Rs]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x13&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OR Rd, Rs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reg[Rd] |= reg[Rs]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LOAD Rd, [buf, Rb+off]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reg[Rd] = dma_buf[buf][(reg[Rb]+off)*4]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x21&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;STORE Rs, [buf, Rb+off]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dma_buf[buf][(reg[Rb]+off)*4] = reg[Rs]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x30&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CMP Rd, Rs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;flag = (reg[Rd] == reg[Rs])&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x31&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BEQ off8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;if flag: PC += off*4&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x32&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BNE off8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;if !flag: PC += off*4&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x40&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CALL off8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SP--; stack[SP]=PC+4; gate_check(); PC+=off*4&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x41&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;if checker()==0x3F → GATE_TRIGGERED; else PC=pop()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x50&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PUSH Rs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SP--; stack[SP] = reg[Rs]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x51&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POP Rd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;reg[Rd] = stack[SP]; SP++&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;&lt;strong&gt;5-5. Shader Validator (&lt;code&gt;sub_428450&lt;/code&gt;)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;dispatch 전에 업로드된 shader를 검사한다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int validate_shader(PhantomGPUState *dev) {
    uint8_t *shader = dev-&amp;gt;active_shader;
    int size = dev-&amp;gt;shader_size;

    // 1. 크기/정렬
    if (size == 0 || (size &amp;amp; 3) || size &amp;gt; 0x1803)
        return FAIL;

    // 2. opcode 유효성 — 모든 instruction을 순회
    for (int pc = 0; pc &amp;lt; size; pc += 4) {
        uint8_t op = shader[pc];
        if (op &amp;gt; 0x51)                  // switch 82 cases, default → FAIL
            return 7;
        // jump table: 0x01,0x02,0x10~0x13,0x20,0x21,0x30~0x32,0x40,0x41,0x50,0x51만 허용
        // 나머지 → return 7
    }

    // 3. BFS reachability 마킹 (sub_4275B0)
    //    PC=0부터 시작, CALL/BEQ/BNE target을 따라가며 reachable 블록 마킹
    mark_reachable(shader, size);  // sub_4275B0

    // 4~5. reachable 블록에 대해서만 prologue/epilogue 검사
    for (each reachable instruction) {
        if (op == 0x41) {              // RET
            // RET 직전 2개 명령이 POP(0x51)이어야 함
            // 추가로 POP의 대상 레지스터가 r15, r14 순이어야 함
            word_m2 = read_insn(pc - 8);  // byte[pc-12..pc-9]
            word_m1 = read_insn(pc - 4);  // byte[pc-8..pc-5]
            if ((word_m2 &amp;amp; 0xFF) != 0x51) return 4;
            if ((word_m1 &amp;amp; 0xFF) != 0x51) return 4;
            // word_m1의 byte1 == 0x0E (r14) 확인
            // word_m2의 byte1 == 0x0F (r15) 확인
        }
        if (op == 0x40) {              // CALL
            // CALL target의 첫 2개 명령이 PUSH r15(0x50 0x0F) + MOV r15,r14(0x01 0x0F 0x0E)
            target = pc + offset*4;
            word0 = read_insn(target);
            word1 = read_insn(target + 4);
            if ((word0 &amp;amp; 0xFF) != 0x50) return 4;   // PUSH
            if ((word0 &amp;gt;&amp;gt; 8 &amp;amp; 0xF) != 0xF) return 4; // r15
            if ((word1 &amp;amp; 0xFF) != 0x01) return 4;   // MOV
            if ((word1 &amp;gt;&amp;gt; 8 &amp;amp; 0xF) != 0xF) return 4; // Rd=r15
            if ((word1 &amp;gt;&amp;gt; 16 &amp;amp; 0xF) != 0xE) return 4; // Rs=r14
        }
    }
    return 0;  // pass
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 검증 4, 5는 &lt;code&gt;sub_4275B0&lt;/code&gt;(BFS)이 reachable로 마킹한 블록에만 적용된다. BFS가 도달하지 못한 블록은 prologue/epilogue 검사를 완전히 건너뛴다.&lt;/p&gt;
&lt;p&gt;이걸로 추후 gate 검증 우회를 할 수 있다.&lt;/p&gt;
&lt;h3&gt;5-6. VM Executor Loop &amp;amp; Fault Codes&lt;/h3&gt;
&lt;p&gt;dispatch 시작 시 &lt;code&gt;memset(dev + 0x1730, 0, 0x20a8)&lt;/code&gt;로 레지스터/PC/SP/trace ring을 전부 초기화한 뒤, 아래 루프를 실행한다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while (steps &amp;lt; max_steps) {
    if (pc &amp;gt;= shader_size) { fault = FAULT_BAD_PC; break; }

    uint8_t opcode = shader[pc];
    uint8_t slot = (13 * opcode + scramble_data[opcode] + blob[0x304]) &amp;amp; 0xFF;
    handler_fn *handler = dev-&amp;gt;dispatch_table[slot];

    if (!handler) { fault = FAULT_BAD_OPCODE; break; }

    trace_ring[trace_count++ % 16] = opcode;
    handler(dev, &amp;amp;shader[pc]);

    if (dev-&amp;gt;fault_code) break;
    steps++;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;dispatch_table[256]&lt;/code&gt;은 함수 포인터 배열로, 5-4에서 초기화된 15개 slot만 실제 handler가 채워져 있고 나머지는 NULL이다. 유효하지 않은 opcode가 들어오면 NULL handler → &lt;code&gt;FAULT_BAD_OPCODE&lt;/code&gt;로 즉시 종료된다.&lt;/p&gt;
&lt;p&gt;각 handler 내부 함수에서는 에러 조건을 만나면 &lt;code&gt;dev-&amp;gt;fault_code&lt;/code&gt;(offset &lt;code&gt;0x379C&lt;/code&gt;)에 상수를 쓰는데, 이 패턴을 추적해서 fault code를 식별할 수 있다:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;코드&lt;/th&gt;
&lt;th&gt;이름&lt;/th&gt;
&lt;th&gt;트리거 조건&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;정상 종료&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;FAULT_OOB&lt;/td&gt;
&lt;td&gt;DMA 버퍼 범위 초과 (LOAD/STORE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;FAULT_STACK_OVER&lt;/td&gt;
&lt;td&gt;SP ≤ 0 (PUSH)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;FAULT_STACK_UNDER&lt;/td&gt;
&lt;td&gt;스택 비어있음 (POP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;FAULT_BAD_OPCODE&lt;/td&gt;
&lt;td&gt;NULL handler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;FAULT_BAD_PC&lt;/td&gt;
&lt;td&gt;PC 범위 초과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;STEPS_EXHAUST&lt;/td&gt;
&lt;td&gt;최대 스텝 소진&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;HALTED&lt;/td&gt;
&lt;td&gt;shader 끝 도달&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;GATE_TRIGGERED&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;checker == 0x3F → success path&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;요약:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/phantomcore/12.png&quot; alt=&quot;12.png&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. Gate 검증&lt;/h2&gt;
&lt;h3&gt;6-1. Gate 조건 분석&lt;/h3&gt;
&lt;p&gt;RET handler(&lt;code&gt;sub_428370&lt;/code&gt;)가 매 RET 실행 시 &lt;code&gt;sub_4280D0&lt;/code&gt;(checker)을 호출한다. 반환값이 정확히 &lt;code&gt;0x3F&lt;/code&gt;(6비트 모두 set)이면 &lt;code&gt;vm_status = 8&lt;/code&gt; (GATE_TRIGGERED).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bit 0 (0x01) — Trace History&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;최근 8개 opcode == 50 11 21 40 20 11 51 41
(PUSH XOR STORE CALL LOAD XOR POP RET)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Bit 1 (0x02) — Register Check&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;reg[7] == 0xA5A5A5A5
reg[0] ^ reg[3] == 0xCC99E897
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Bit 2 (0x04) — buf2 First Dword&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*(u32*)buf2 == 0xC0DEF00D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Bit 3 (0x08) — buf1 CRC32&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CRC32(buf1[0:16]) == 0x2A8DED84
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;gate는 buf1 앞 16바이트의 CRC만 검사한다. 바이트 자체는 고정하지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bit 4 (0x10) — Gate Armed&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CALL 실행 시: reg[1] == 0x47415445 (&quot;ETAG&quot;) + 직전 4개 trace == [LOAD, AND, STORE, CALL]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Bit 5 (0x20) — Shader CRC32&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;reg[2] == CRC32(전체 shader bytecode)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;dead code 바이트도 CRC 계산에 포함된다.&lt;/p&gt;
&lt;h3&gt;6-2. Gate 검증 우회&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;문제:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Bit 0 history &lt;code&gt;50 11 21 40 20 11 51 41&lt;/code&gt;에서 RET 직전은 &lt;code&gt;0x51&lt;/code&gt;(POP) 하나이고 그 앞은 &lt;code&gt;0x11&lt;/code&gt;(XOR)이다. 그런데 validator는 &lt;strong&gt;reachable RET 직전 두 명령이 POP 두 개여&lt;/strong&gt;야 한다고 강제한다. reachable code만으로는 두 조건을 동시에 만족할 수 없다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Validator가 dead code 블록에 대해서 CALL prologue/RET epilogue 규칙을 검사하지 않는다는 점을 이용할 수 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reachable 함수 내에서 STORE로 slot3(stack)의 return address를 dead block PC로 덮어쓴다&lt;/li&gt;
&lt;li&gt;Validator 규칙을 만족하는 POP + POP + RET로 함수를 종료한다&lt;/li&gt;
&lt;li&gt;RET가 덮어쓴 주소를 팝하여 dead block으로 점프한다&lt;/li&gt;
&lt;li&gt;Dead block에서 정확히 &lt;code&gt;50 11 21 40 20 11 51 41&lt;/code&gt; history를 만들 수 있다&lt;/li&gt;
&lt;li&gt;Dead block의 마지막 RET에서 checker 호출 → &lt;code&gt;0x3F&lt;/code&gt; → GATE_TRIGGERED&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Dead Block Return Address Overwrite&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;gpuctl-cli run&lt;/code&gt;이 slot3을 16KB(SP=4096)로 할당한다. CALL이 return address를 &lt;code&gt;stack[4095]&lt;/code&gt;에 push하는데, STORE의 offset(byte3)은 signed 8-bit이라 base가 0이면 &lt;code&gt;stack[15]&lt;/code&gt;까지만 도달 가능하다.&lt;/p&gt;
&lt;p&gt;이 때 CALL 전에 &lt;code&gt;r14 = 4080&lt;/code&gt;을 미리 로드하면 된다. 함수 prologue의 &lt;code&gt;MOV r15, r14&lt;/code&gt;가 &lt;code&gt;r15 = 4080&lt;/code&gt;을 만들고, &lt;code&gt;STORE r9, [buf3, r15+15]&lt;/code&gt;가 &lt;code&gt;stack[4095]&lt;/code&gt;를 정확히 덮는다. validator는 함수 바이트코드 자체를 검사하므로, 동일한 바이트코드가 입력값(r14)에 따라 다른 위치를 overwrite하는 것까지는 막지 못한다.&lt;/p&gt;
&lt;h3&gt;6-3. Shader 레이아웃&lt;/h3&gt;
&lt;p&gt;최종 shader는 reachable region과 dead block 두 영역으로 구성된다. Reachable 영역에서 gate bit 1~5를 세팅한 뒤, return address overwrite로 dead block에 진입하여 bit 0(trace history)까지 만족시킨다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/phantomcore/13.png&quot; alt=&quot;13.png&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. Flag 복원&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;GATE_TRIGGERED&lt;/code&gt;까지 성공해도 buf2에 올바른 flag가 바로 나오지는 않는다. Gate 조건은 레지스터·버퍼의 &lt;strong&gt;관계&lt;/strong&gt;(XOR, CRC 등)만 검사하므로 통과 가능한 입력 조합은 무수히 많지만, success path는 각 값을 직접 사용해 AES key를 파생한다. 정확한 입력이 아니면 의미 있는 출력이 나올 수 없다.&lt;/p&gt;
&lt;h3&gt;7-1. Gate 검사 범위&lt;/h3&gt;
&lt;p&gt;Gate가 &lt;strong&gt;고정하는 값:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;trace8 — 8바이트 일치&lt;/li&gt;
&lt;li&gt;reg7 — 값 일치&lt;/li&gt;
&lt;li&gt;reg0 ^ reg3 — XOR 결과만 (개별값은 자유)&lt;/li&gt;
&lt;li&gt;buf2[0:4] — 값 일치&lt;/li&gt;
&lt;li&gt;CRC32(buf1[:16]) — CRC만 (raw bytes는 자유)&lt;/li&gt;
&lt;li&gt;reg2 — CRC32(shader) 일치&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Gate가 &lt;strong&gt;검사하지 않는 값:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;reg0, reg3 개별값&lt;/li&gt;
&lt;li&gt;reg4, reg5&lt;/li&gt;
&lt;li&gt;buf1 전체 32바이트 (앞 16바이트는 CRC만, 뒤 16바이트는 자유)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7-2. Success Path가 실제로 읽는 것&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// sub_428AB0 success path, 0x4299d6..
v132 = f28 ^ reg0                                // reg0 개별값 직접 씀
v134 = f2c ^ ((reg4 + reg5) &amp;amp; 0xFFFFFFFF)        // reg4, reg5 직접 씀
v135 = ROL32(reg3, 7) ^ v132                     // reg3 개별값 직접 씀
msg  = trace8 || pack_le32(v135) || pack_le32(v134) || buf1[16:32]
rounds = expand_round_material(buf1[0:16])        // buf1 전반 raw bytes 직접 씀
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Gate를 통과하더라도, 위 코드에서 직접 참조하는 &lt;strong&gt;reg0, reg3, reg4, reg5의 개별값&lt;/strong&gt;과 &lt;strong&gt;buf1 32바이트의 정확한 내용&lt;/strong&gt;을 별도로 복원해야 올바른 flag를 얻을 수 있다.&lt;/p&gt;
&lt;h3&gt;7-4. Success Path 분석&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;buf1[0:32]  ──┐
trace8      ──┤
reg0~reg5   ──┼──▶ compress_states() ──▶ S0 (AES key), S1 (CTR counter)
f28, f2c    ──┘
                         ↓
              AES-128-CTR(S0, S1) ──▶ keystream (78B)
                                           │
              fixed_blob (78B) ──XOR───────┘──▶ flag (buf2 output)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;compress_states&lt;/code&gt; 메인 로직은 아래와 같다:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;buf1[0:16]&lt;/code&gt;에서 round material 24개 생성 (MurmurHash3 fmix 변형)&lt;/li&gt;
&lt;li&gt;GF(2^8) MixColumns (standard + custom matrix) + Feistel 12라운드&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fold16&lt;/code&gt; (&lt;code&gt;sub_4279B0&lt;/code&gt;): 16B → 64bit keyed digest (seed &lt;code&gt;0xDEADBEEFCAFEBABE&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;역방향 Feistel 8라운드 + 최종 GF MixColumns → S0(AES key), S1(CTR counter)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;compress_states 함수는 역연산이 불가능한 일방향 함수이다. 다만 올바른 입력값만 넣으면 VM 내부에서 알아서 연산되므로, 자세히 보지 않겠다.&lt;/p&gt;
&lt;h3&gt;7-6. Hidden Witness&lt;/h3&gt;
&lt;p&gt;자, Gate를 통과해도 flag가 나오지 않으니 이제 success path가 기대하는 정확한 입력값을 찾아야 한다. 그리고 다행히도 firmware blob 0x308바이트 중 아직 해석이 덜 된 영역이 남아있다.&lt;/p&gt;
&lt;p&gt;여기부터는 약간의 게싱이 필요한데,&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/phantomcore/14.png&quot; alt=&quot;14.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hidden Register&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Firmware blob &lt;code&gt;0x034..0x054&lt;/code&gt; 영역을 Grammar B (pack_perm XOR) 방식으로 디코드하면:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;th&gt;용도&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x12345678&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;reg0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0xDEADBEEF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;reg3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x11111111&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;reg4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x22222222&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;reg5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;검증: &lt;code&gt;0x12345678 ^ 0xDEADBEEF = 0xCC99E897&lt;/code&gt; = gate bit 1의 &lt;code&gt;reg0 ^ reg3&lt;/code&gt; 기대값과 정확히 일치한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hidden buf1&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Firmware blob &lt;code&gt;blob[0x5C:0x80]&lt;/code&gt; 영역:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[0x5C:0x7C] = raw 32바이트 (Grammar A로 인코딩된 데이터)
[0x7C:0x80] = selector dword (0x0000009D)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 구조가 trace8 디코드에 사용된 Grammar A 블록과 완전히 동일한 &lt;code&gt;[raw data] + [selector dword]&lt;/code&gt; 포맷이다. Grammar A를 적용하면:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;decoded[i] = blob[0x5C + i] ^ sbox[(0x9D + i) &amp;amp; 0xFF]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;PHTM_CTF_KEY_128SECONDHALF_DATA!&lt;/code&gt; 라는 의미있는 문자열이 나온다!&lt;/p&gt;
&lt;p&gt;이처럼 trace block과 동일한 &lt;code&gt;[raw data] + [selector dword]&lt;/code&gt; , 의미있는 ASCII 문자열이 나온 데다가 앞 16바이트 CRC32 = &lt;code&gt;0x2A8DED84&lt;/code&gt; = gate 상수 f20과 정확히 일치하는 점으로 미루어보았을 때 이게 의도된  buf1 값이라는 걸 알 수 있다.&lt;/p&gt;
&lt;p&gt;이때 hidden reg 및 buf1은 QEMU success path에 의해 참조되진 않는다. Hidden marker는 runtime 중에 소비되는 정답이 아니라 숨겨진 값이다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. Solve&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/phantomcore/15.png&quot; alt=&quot;15.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/phantomcore/16.png&quot; alt=&quot;16.png&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>LA CTF 2026 Writeup (Reflection)</title><link>https://ma4the.github.io/posts/rev-reflection-lactf2026/</link><guid isPermaLink="true">https://ma4the.github.io/posts/rev-reflection-lactf2026/</guid><description>I can keep digging. I could pull us down to the center of the earth.</description><pubDate>Fri, 13 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. Challenge Overview&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;reflection&lt;/code&gt;이라는 Rust 바이너리가 주어진다. 실행하면 인자와 무관하게 항상 panic 메시지가 출력된다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ./reflection
thread &apos;main&apos; panicked at src/main.rs:2:5:
Come close to me and I&apos;ll make you regret it.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

$ ./reflection a
thread &apos;main&apos; panicked at src/main.rs:2:5:
Come close to me and I&apos;ll make you regret it.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;어떤 값을 넘기든 출력은 동일하다. 디컴파일러에서 &lt;code&gt;reflection::main&lt;/code&gt;을 확인하면 무조건 &lt;code&gt;panic!()&lt;/code&gt;만 호출한다. &lt;code&gt;.text&lt;/code&gt; 어디에도 플래그 검증 로직이 없고, 바이너리가 입력을 어떻게 받는지도 알 수 없다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/reflection/0.png&quot; alt=&quot;0.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/reflection/1.jpg&quot; alt=&quot;1.jpg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;여기서 드는 두 가지 의문:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;검증 로직은 어디에 있는가?&lt;/li&gt;
&lt;li&gt;바이너리는 플래그를 어떻게 읽는가?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;.text&lt;/code&gt;에 검증 코드가 없다면, 검증은 메타데이터에 숨겨져 있어야 한다. Rust에서 &lt;code&gt;panic!()&lt;/code&gt;은 스택 Unwinding을 트리거하며, 이 과정에서 &lt;code&gt;.eh_frame&lt;/code&gt;(임의의 DWARF 바이트코드를 포함할 수 있는 섹션)을 읽는다. 여기가 다음으로 살펴볼 곳이다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;참고:&lt;/strong&gt; 후에 기술하겠지만, 플래그는 실제로 커맨드라인 인자로 전달된다. &lt;code&gt;./reflection &quot;lactf{...}&quot;&lt;/code&gt; 정확한 로직은 DWARF 표현식을 분석해야 알 수 있으며, 해당 표현식은 Rust 내부 전역 변수에서 &lt;code&gt;argc&lt;/code&gt;와 &lt;code&gt;argv&lt;/code&gt;를 직접 읽는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;2. Background — Stack Unwinding, .eh_frame&lt;/h2&gt;
&lt;h3&gt;2.1 Rust Panic: Unwind vs. Abort&lt;/h3&gt;
&lt;p&gt;참고: https://os.phil-opp.com/freestanding-rust-binary/&lt;/p&gt;
&lt;p&gt;Rust 프로그램이 &lt;code&gt;panic!()&lt;/code&gt;을 만나면, 컴파일 시점에 &lt;code&gt;Cargo.toml&lt;/code&gt;로 설정된 두 가지 전략 중 하나가 실행된다:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Abort 모드&lt;/strong&gt; (&lt;code&gt;panic = &quot;abort&quot;&lt;/code&gt;): 프로세스가 즉시 종료된다. 스택 정리도, 소멸자 호출도 없다. 더 단순하고 바이너리 크기가 작아 임베디드, 베어메탈 환경에서 흔히 사용된다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[profile.release]
panic = &quot;abort&quot;   # 그냥 프로세스를 죽임
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Unwind 모드&lt;/strong&gt; (기본값): 런타임이 콜 스택을 역방향으로 순회하며, 살아있는 각 지역 변수의 소멸자를 호출하고, &lt;code&gt;catch_unwind&lt;/code&gt;를 통한 복구를 허용한다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::panic;

let result = panic::catch_unwind(|| {
    panic!(&quot;something went wrong&quot;);
});
// result는 Err — 프로그램은 계속 실행됨
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unwinding에는 각 스택 프레임에 대한 메타데이터가 필요하다 — 구체적으로 DWARF Call Frame Information이 포함된 &lt;code&gt;.eh_frame&lt;/code&gt; 섹션이다. 이 메타데이터는 Unwinder한테 레지스터 복원 방법과 이전 프레임을 찾는 방법을 알려준다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;이 챌린지 바이너리는 기본 Unwind 모드를 사용한다.&lt;/strong&gt; 다음을 통해 확인할 수 있다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;panic 출력에 백트레이스 힌트(&lt;code&gt;RUST_BACKTRACE=1&lt;/code&gt;)가 포함되어 있으며, 이는 Unwinding에서만 동작함&lt;/li&gt;
&lt;li&gt;바이너리에 큰 &lt;code&gt;.eh_frame&lt;/code&gt; 섹션이 존재함&lt;/li&gt;
&lt;li&gt;panic 도중 &lt;code&gt;libgcc_s.so.1&lt;/code&gt;의 &lt;code&gt;_Unwind_RaiseException&lt;/code&gt;이 호출됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, &lt;code&gt;panic!()&lt;/code&gt;이 Unwinding을 트리거하고, Unwinding이 &lt;code&gt;.eh_frame&lt;/code&gt; DWARF 표현식을 평가하므로, 출제자는 해당 표현식 안에 완전한 검증 프로그램을 숨겨놓은 것이다!&lt;/p&gt;
&lt;h3&gt;2.2 Unwinding 동작 원리: 2-Phase 메커니즘&lt;/h3&gt;
&lt;p&gt;참고: gcc/libgcc/unwind.inc(https://github.com/gcc-mirror/gcc/tree/master/libgcc)&lt;/p&gt;
&lt;p&gt;Rust &lt;code&gt;panic!&lt;/code&gt; 이 발생하면, Unwinder는 콜 스택을 역방향으로 순회하며 각 프레임의 저장된 레지스터를 복원하고 정리 코드를 실행한다. Linux x86-64에서 Unwinder는 &lt;code&gt;libgcc_s.so.1&lt;/code&gt;에 위치한다. 진입점은 &lt;code&gt;_Unwind_RaiseException()&lt;/code&gt;이며, 2단계로 동작한다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// libgcc/unwind.inc 요약 
_Unwind_RaiseException(exc) {
    // Phase 1 — 검색:
    //   프레임을 위로 순회하며 핸들러(catch_unwind)를 찾음.
    //   아직 아무것도 변경하지 않음. 표현식은 실행되지만 결과는 폐기됨.
    while (1) {
        uw_frame_state_for(&amp;amp;context, &amp;amp;fs);   // FDE 찾기, CFI 파싱
        if (fs.personality)
            personality(..._UA_SEARCH_PHASE...);  // 질의: 정리? 캐치?
        uw_update_context(&amp;amp;context, &amp;amp;fs);     // CFI 규칙 적용
    }

    cur_context = this_context;  // ★ 초기 상태로 리셋

    // Phase 2 — 정리:
    //   처음부터 프레임을 다시 순회.
    //   각 프레임에서: 레지스터 복원, 정리 코드 실행.
    //   핸들러 프레임에 도달하면 점프.
    while (1) {
        uw_frame_state_for(&amp;amp;context, &amp;amp;fs);
        if (fs.personality)
            personality(..._UA_CLEANUP_PHASE...);
        uw_update_context(&amp;amp;context, &amp;amp;fs);     // ★ 표현식 실행, RIP 결정
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;핵심 함수는 &lt;code&gt;uw_update_context()&lt;/code&gt;이다. &lt;code&gt;.eh_frame&lt;/code&gt;의 CFI 규칙을 읽고 이전 프레임의 레지스터 값을 계산한다. 규칙에 DWARF 표현식이 포함되어 있으면, libgcc 내부의 스택 기반 바이트코드 VM인 &lt;code&gt;execute_stack_op()&lt;/code&gt;이 해당 표현식을 평가한다. 두 Phase 모두 동일한 프레임을 순회하므로, 모든 DWARF 표현식은 &lt;strong&gt;두 번&lt;/strong&gt; 실행된다(각 Phase에서 한 번씩.) 그러나 Phase 1은 이후 컨텍스트를 리셋하므로 Phase 2의 결과만 주의깊게 보면 된다.&lt;/p&gt;
&lt;h3&gt;2.3 .eh_frame section: CIE, FDE&lt;/h3&gt;
&lt;p&gt;참고: https://refspecs.linuxfoundation.org/LSB_3.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html&lt;/p&gt;
&lt;p&gt;.eh_frame 섹션은 하나 이상의 Call Frame Information(CFI) 레코드를 포함해야 한다.&lt;/p&gt;
&lt;p&gt;레코드 개수는 섹션 헤더에 명시된 섹션 크기에 따라 결정된다.&lt;/p&gt;
&lt;p&gt;각 CFI 레코드는 다음으로 구성된다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;하나의 Common Information Entry (CIE)&lt;/li&gt;
&lt;li&gt;그 뒤에 하나 이상의 Frame Description Entry (FDE)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CIE와 FDE는 모두 주소 단위 크기의 경계에 정렬(aligned)되어야 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CIE (Common Information Entry)&lt;/strong&gt; — 여러 함수가 공유하는 템플릿:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Usage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Augmentation String&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;기능 플래그: &lt;code&gt;&quot;zR&quot;&lt;/code&gt; (기본), &lt;code&gt;&quot;zPLR&quot;&lt;/code&gt; (personality 함수 포함)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Code Align&lt;/code&gt;ment factor&lt;/td&gt;
&lt;td&gt;명령어 정렬 인수 (x86-64에서 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Data Align&lt;/code&gt;ment factor&lt;/td&gt;
&lt;td&gt;스택 슬롯 인수 (x86-64에서 -8)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Return Address Register&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;복귀 주소 레지스터 (16 = RIP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Initial Instructions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;모든 FDE가 상속하는 기본 CFI 규칙&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;FDE (Frame Description Entry)&lt;/strong&gt; — 함수당 하나:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Usage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PC Begin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;함수 시작 주소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PC Range&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;함수 크기(바이트)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Call Frame Instructions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;스택 프레임 변화를 기술하는 CFI 바이트코드&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&quot;/images/reflection/2.png&quot; alt=&quot;2.png&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2.4 두 가지 바이트코드 시스템: DW_CFA vs. DW_OP&lt;/h3&gt;
&lt;p&gt;참고: https://dwarfstd.org/doc/DWARF5.pdf&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;DWARF is a debugging information file format used by many compilers and debuggers to support source level debugging. It addresses the requirements of a number of procedural languages, such as C, C++, and Fortran, and is designed to be extensible to other languages. DWARF is architecture independent and applicable to any processor or operating system. It is widely used on Unix, Linux and other operating systems, as well as in stand-alone environments.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;DWARF는 두 종류의 바이트코드 시스템을 포함한다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DWARF Expressions (DW_OP_*)&lt;/strong&gt; — 스택 기반 표현식 평가 언어&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Call Frame Instructions (DW_CFA_*)&lt;/strong&gt; — 레지스터 복원 규칙을 생성하는 상태 머신&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;구분&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;DWARF Expression&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;DW_CFA Instruction&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;정의&lt;/td&gt;
&lt;td&gt;스택 기반 표현식 평가기&lt;/td&gt;
&lt;td&gt;콜 프레임 기술 언어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모델&lt;/td&gt;
&lt;td&gt;스택 기반 가상 평가기&lt;/td&gt;
&lt;td&gt;복원 규칙을 생성하는 상태 머신&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;목적&lt;/td&gt;
&lt;td&gt;값/주소 계산&lt;/td&gt;
&lt;td&gt;CFA 및 레지스터 복원 규칙&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;대부분의 CFI 명령어는 콜 프레임 상태 복원 방법을 기술하는 규칙 테이블을 단순히 업데이트한다. 그러나 특정 DW_CFA 명령어는 &lt;strong&gt;DWARF 표현식&lt;/strong&gt;을 피연산자로 내장할 수 있다:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CFI Instruction&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DW_CFA_def_cfa_expression&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;표현식 E를 평가하여 CFA를 계산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DW_CFA_expression(reg)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;표현식 E가 레지스터 &lt;code&gt;reg&lt;/code&gt;이 저장된 주소를 계산&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DW_CFA_val_expression(reg)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;표현식 E가 레지스터 &lt;code&gt;reg&lt;/code&gt;의 값을 계산&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;uw_update_context()&lt;/code&gt; (Unwinder 함수)가 이러한 명령어를 만나면, &lt;code&gt;libgcc/unwind-dw2.c&lt;/code&gt;의 &lt;code&gt;execute_stack_op()&lt;/code&gt; — 산술, 메모리 접근, 제어 흐름, 레지스터 읽기를 지원하는 스택 기반 DWARF VM — 을 호출한다.&lt;/p&gt;
&lt;p&gt;이 챌린지에서 출제자는 &lt;strong&gt;12,888바이트짜리 검증 프로그램&lt;/strong&gt;을 &lt;code&gt;DW_CFA_val_expression&lt;/code&gt; 안에 심었다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 숨겨진 코드 찾기&lt;/h2&gt;
&lt;h3&gt;3.1 Expressions 스캔&lt;/h3&gt;
&lt;p&gt;참고: https://man7.org/linux/man-pages/man1/readelf.1.html&lt;/p&gt;
&lt;p&gt;readelf의 &lt;code&gt;--debug-dump=frames&lt;/code&gt; 옵션을 이용해 보면,&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ readelf --debug-dump=frames ./reflection | grep expression
  DW_CFA_def_cfa_expression (DW_OP_breg7 (rsp): 8; ...)
  DW_CFA_val_expression: r16 (rip) (DW_OP_GNU_encoded_addr: ... (Unknown location op 0x5))
  DW_CFA_val_expression: r2 (rcx) (DW_OP_const8u: 342697)
  DW_CFA_val_expression: r12 (r12) (DW_OP_const8u: 4469372305074626438)
  DW_CFA_val_expression: r16 (rip) (DW_OP_GNU_encoded_addr: ... (Unknown location op 0x1))
  DW_CFA_val_expression: r13 (r13) (DW_OP_const8u: 5860663257725425333)
  DW_CFA_expression: r8 (r8) (DW_OP_GNU_encoded_addr: fmt:42 addr:0000000000000024)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;6개 FDE에 걸쳐 7개의 DWARF 표현식이 있는 것을 알 수 있다. 바이너리의 대부분의 FDE는 단순한 CFI 규칙만 갖고 있다. 이 7개가 의심스럽다.&lt;/p&gt;
&lt;p&gt;이때 readelf가 두 개의 &lt;code&gt;r16 (rip)&lt;/code&gt; 표현식을 파싱하지 못하고 &lt;code&gt;(Unknown location op)&lt;/code&gt;으로 보고하고 있는데, 이는 readelf가 지원하지 않는 &lt;code&gt;funcrel|uleb128&lt;/code&gt; 인코딩의 &lt;code&gt;DW_OP_GNU_encoded_addr&lt;/code&gt; (0xF1)을 사용하기 때문이다. 0xF1 이후 바이트를 잘못 파싱하고 포기한다.&lt;/p&gt;
&lt;p&gt;실제 표현식은 115바이트와 12,888바이트로, 일반 CFI에 비해 훨씬 크다. raw 바이트는 다른 방법으로 추출해야 한다.&lt;/p&gt;
&lt;h3&gt;3.2 GDB 디버깅 — Expressions 추출&lt;/h3&gt;
&lt;p&gt;참고: gcc/libgcc/unwind-dw2.의 execute_stack_op() 함수(https://github.com/gcc-mirror/gcc/tree/705d87ba8e987cb8e0ca8b6d400c0aff8739eb23/libgcc)&lt;/p&gt;
&lt;p&gt;readelf가 이 표현식들을 완전히 파싱할 수 없으므로, 런타임에서 추출해야 한다.&lt;/p&gt;
&lt;p&gt;핵심 인사이트: &lt;strong&gt;libgcc의 &lt;code&gt;execute_stack_op()&lt;/code&gt;은 raw 표현식 바이트를 인자로 받는다&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// libgcc/unwind-dw2.c
static _Unwind_Word
execute_stack_op(const unsigned char *op_ptr,   // 표현식 시작
                 const unsigned char *op_end,   // 표현식 끝
                 struct _Unwind_Context *context,
                 _Unwind_Word initial)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 함수에 브레이크포인트를 걸면 실행되는 모든 DWARF 표현식을 검사할 수 있다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ gdb ./reflection
(gdb) set args &quot;AAAA&quot;
(gdb) start                          # libgcc_s.so.1 로드
(gdb) break execute_stack_op         # DWARF 표현식 VM 진입점
(gdb) continue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;브레이크포인트에 도달할 때마다, &lt;code&gt;op_end - op_ptr&lt;/code&gt;로 정확한 표현식 크기를 알 수 있고, &lt;code&gt;x/&amp;lt;size&amp;gt;xb $rdi&lt;/code&gt;로 원시 바이트를 덤프할 수 있다. Python GDB 스크립트(&lt;code&gt;reflection_dump.py&lt;/code&gt;)로 이를 자동화한다: 각 호출마다 &lt;code&gt;inferior.read_memory()&lt;/code&gt;로 원시 바이트를 읽고, &lt;code&gt;.bin&lt;/code&gt; 파일로 저장하며, 컨텍스트 레지스터 상태를 기록한다.&lt;/p&gt;
&lt;p&gt;2-phase Unwinding으로 인해, 각 표현식은 두 번 실행된다 (Phase 1 + Phase 2). &lt;code&gt;op_ptr&lt;/code&gt; 주소로 중복을 제거하고, Phase 2 실행만 유지하도록 스크립트를 작성했다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;reflection_dump.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;
reflection_dump.py
LACTF reflection - DWARF expression dump + analysis

Usage: gdb -x reflection_dump.py ./reflection
Output: expr_p{1,2}_N_{size}b.bin  (Phase_CallNumber_Size)

Examples:
  expr_p1_1_9b.bin      Phase 1, call #1, 9 bytes (RCX constant)
  expr_p1_3_115b.bin    Phase 1, call #3, 115 bytes (trigger)
  expr_p2_7_12888b.bin  Phase 2, call #7, 12888 bytes (main VM)
&quot;&quot;&quot;

import gdb
import struct
import os

FLAG_INPUT = &quot;lactf{si&quot; + &quot;A&quot; * 38 + &quot;}&quot;  # 8 + 38 + 1 = 47
OUTDIR = &quot;./reflection_exprs&quot;
PRINT_LIMIT = 256  # Max bytes for terminal output (truncates to head/tail if exceeded)
PRINT_HEAD = 128
PRINT_TAIL = 64

call_num = 0
phase = 1
phase1_ctx = None
seen = {}  # op_ptr -&amp;gt; (call_num, size, data, ctx_addr) deduplication map

def read_bytes(addr, size):
    &quot;&quot;&quot;Read raw bytes from memory&quot;&quot;&quot;
    inferior = gdb.selected_inferior()
    return bytes(inferior.read_memory(addr, size))

def get_reg(ctx_addr, idx):
    &quot;&quot;&quot;Return context register (by_value, value)&quot;&quot;&quot;
    try:
        bv = int(gdb.parse_and_eval(
            f&quot;((struct _Unwind_Context*){ctx_addr:#x})-&amp;gt;by_value[{idx}]&quot;))
        val = int(gdb.parse_and_eval(
            f&quot;(unsigned long)((struct _Unwind_Context*){ctx_addr:#x})-&amp;gt;reg[{idx}]&quot;))
        return bv, val
    except:
        return -1, 0

def get_cfa(ctx_addr):
    try:
        return int(gdb.parse_and_eval(
            f&quot;(unsigned long)((struct _Unwind_Context*){ctx_addr:#x})-&amp;gt;cfa&quot;))
    except:
        return 0

def hexdump(data, addr, limit=None):
    &quot;&quot;&quot;Pretty hex dump. Shows only head/tail if limit is exceeded.&quot;&quot;&quot;
    if limit and len(data) &amp;gt; limit:
        # Head section
        for i in range(0, PRINT_HEAD, 16):
            chunk = data[i:i+16]
            hex_str = &quot; &quot;.join(f&quot;{b:02x}&quot; for b in chunk)
            ascii_str = &quot;&quot;.join(chr(b) if 32 &amp;lt;= b &amp;lt; 127 else &quot;.&quot; for b in chunk)
            print(f&quot;  {addr+i:012x}  {hex_str:&amp;lt;48s}  {ascii_str}&quot;)
        print(f&quot;  ... ({len(data) - PRINT_HEAD - PRINT_TAIL} bytes omitted) ...&quot;)
        # Tail section
        start = len(data) - PRINT_TAIL
        start = (start // 16) * 16
        for i in range(start, len(data), 16):
            chunk = data[i:i+16]
            hex_str = &quot; &quot;.join(f&quot;{b:02x}&quot; for b in chunk)
            ascii_str = &quot;&quot;.join(chr(b) if 32 &amp;lt;= b &amp;lt; 127 else &quot;.&quot; for b in chunk)
            print(f&quot;  {addr+i:012x}  {hex_str:&amp;lt;48s}  {ascii_str}&quot;)
    else:
        for i in range(0, len(data), 16):
            chunk = data[i:i+16]
            hex_str = &quot; &quot;.join(f&quot;{b:02x}&quot; for b in chunk)
            ascii_str = &quot;&quot;.join(chr(b) if 32 &amp;lt;= b &amp;lt; 127 else &quot;.&quot; for b in chunk)
            print(f&quot;  {addr+i:012x}  {hex_str:&amp;lt;48s}  {ascii_str}&quot;)

def save_bin(data, filename):
    &quot;&quot;&quot;Save as binary file&quot;&quot;&quot;
    path = os.path.join(OUTDIR, filename)
    with open(path, &quot;wb&quot;) as f:
        f.write(data)
    return path

def save_context(ctx_addr, filename):
    &quot;&quot;&quot;Save context registers as JSON-like text&quot;&quot;&quot;
    regs = {}
    for idx, name in [(2, &quot;rcx&quot;), (8, &quot;r8&quot;), (12, &quot;r12&quot;), (13, &quot;r13&quot;), (16, &quot;rip&quot;)]:
        bv, val = get_reg(ctx_addr, idx)
        regs[name] = {&quot;by_value&quot;: bv, &quot;value&quot;: val}
        # Dereference if by_value=0 and value is non-zero
        if bv == 0 and val != 0:
            try:
                deref = int(gdb.parse_and_eval(f&quot;*(unsigned long*){val:#x}&quot;))
                regs[name][&quot;deref&quot;] = deref
            except:
                pass
    regs[&quot;cfa&quot;] = get_cfa(ctx_addr)

    path = os.path.join(OUTDIR, filename)
    with open(path, &quot;w&quot;) as f:
        for name, info in regs.items():
            if isinstance(info, dict):
                bv = info[&quot;by_value&quot;]
                val = info[&quot;value&quot;]
                deref = info.get(&quot;deref&quot;)
                line = f&quot;{name}: by_value={bv} val={val:#018x}&quot;
                if deref is not None:
                    line += f&quot; *val={deref:#018x}&quot;
                f.write(line + &quot;\n&quot;)
            else:
                f.write(f&quot;{name}: {info:#018x}\n&quot;)
    return path

class ExprBreakpoint(gdb.Breakpoint):
    def stop(self):
        global call_num, phase, phase1_ctx, seen

        try:
            op_ptr = int(gdb.parse_and_eval(&quot;(unsigned long)$rdi&quot;))
            op_end = int(gdb.parse_and_eval(&quot;(unsigned long)$rsi&quot;))
            ctx = int(gdb.parse_and_eval(&quot;(unsigned long)$rdx&quot;))
            initial = int(gdb.parse_and_eval(&quot;(unsigned long)$rcx&quot;))
        except:
            return True

        call_num += 1
        size = op_end - op_ptr

        # Phase detection
        if call_num == 1:
            phase1_ctx = ctx
            phase = 1
        elif ctx != phase1_ctx and phase == 1:
            phase = 2

        # Read raw bytes
        data = read_bytes(op_ptr, size)

        # Dedup check: overwrite if same op_ptr (Phase 2 takes precedence)
        is_new = op_ptr not in seen
        seen[op_ptr] = {
            &quot;call_num&quot;: call_num,
            &quot;phase&quot;: phase,
            &quot;size&quot;: size,
            &quot;data&quot;: data,
            &quot;ctx&quot;: ctx,
            &quot;initial&quot;: initial,
            &quot;op_ptr&quot;: op_ptr,
        }

        # Output
        dup_tag = &quot;&quot; if is_new else &quot; [overwrite]&quot;
        print()
        print(f&quot;{&apos;=&apos;*60}&quot;)
        print(f&quot;  #{call_num}  |  Phase {phase}  |  {size} bytes{dup_tag}&quot;)
        print(f&quot;{&apos;=&apos;*60}&quot;)

        # Register summary
        for idx, name in [(2, &quot;RCX&quot;), (8, &quot;R8&quot;), (12, &quot;R12&quot;), (13, &quot;R13&quot;)]:
            bv, val = get_reg(ctx, idx)
            tag = &quot;val&quot; if bv == 1 else &quot;ptr&quot; if bv == 0 else &quot;?&quot;
            extra = &quot;&quot;
            if bv == 0 and val != 0:
                try:
                    deref = int(gdb.parse_and_eval(f&quot;*(unsigned long*){val:#x}&quot;))
                    extra = f&quot; → {deref:#x}&quot;
                except:
                    pass
            print(f&quot;  {name:&amp;gt;3}: [{tag}] {val:#x}{extra}&quot;)
        print(f&quot;  CFA: {get_cfa(ctx):#x}&quot;)
        print()

        # Hex dump
        hexdump(data, op_ptr, limit=PRINT_LIMIT)

        # Extra info for 115-byte expressions
        if size == 115:
            bv_rcx, val_rcx = get_reg(ctx, 2)
            if bv_rcx == 1 and val_rcx != 0:
                print()
                try:
                    argc = int(gdb.parse_and_eval(f&quot;*(unsigned long*){val_rcx - 89:#x}&quot;))
                    argv_ptr = int(gdb.parse_and_eval(f&quot;*(unsigned long*){val_rcx - 81:#x}&quot;))
                    argv1 = int(gdb.parse_and_eval(f&quot;*(unsigned long*){argv_ptr + 8:#x}&quot;))
                    argv1_s = gdb.execute(f&quot;x/s {argv1:#x}&quot;, to_string=True).split(&quot;:&quot;, 1)[1].strip()
                    print(f&quot;  argc={argc}, argv[1]={argv1_s}&quot;)
                except:
                    pass

        print(f&quot;{&apos;=&apos;*60}&quot;)

        return False  # Auto-continue

def main():
    os.makedirs(OUTDIR, exist_ok=True)
    # Clean up existing files
    for f in os.listdir(OUTDIR):
        os.remove(os.path.join(OUTDIR, f))

    print(f&quot;\n[*] LACTF reflection expression dumper&quot;)
    print(f&quot;[*] Input: {FLAG_INPUT}&quot;)
    print(f&quot;[*] Output: {OUTDIR}/&quot;)
    print()

    gdb.execute(f&apos;set args &quot;{FLAG_INPUT}&quot;&apos;)
    gdb.execute(&quot;set pagination off&quot;)
    gdb.execute(&quot;set confirm off&quot;)
    gdb.execute(&quot;start&quot;)

    bp = ExprBreakpoint(&quot;execute_stack_op&quot;)
    bp.silent = True

    print(f&quot;\n[*] Continuing to panic...\n&quot;)
    gdb.execute(&quot;continue&quot;)

    # Save only deduplicated expressions
    print(f&quot;\n{&apos;=&apos;*60}&quot;)
    print(f&quot;  {call_num} total calls, {len(seen)} unique expressions&quot;)
    print(f&quot;  Output directory: {OUTDIR}/&quot;)
    print()

    # Sort by execution order (call_num)
    sorted_exprs = sorted(seen.values(), key=lambda x: x[&quot;call_num&quot;])

    for i, info in enumerate(sorted_exprs, 1):
        size = info[&quot;size&quot;]
        bin_name = f&quot;expr_{i}_{size}b.bin&quot;
        ctx_name = f&quot;expr_{i}_{size}b_ctx.txt&quot;

        # Save .bin
        save_bin(info[&quot;data&quot;], bin_name)

        # Save context (Phase 2 values)
        save_context(info[&quot;ctx&quot;], ctx_name)

        # Description labels
        labels = {9: &quot;constant assign&quot;, 4: &quot;address encoding&quot;, 115: &quot;1st validation&quot;, 12888: &quot;2nd validation VM&quot;}
        label = labels.get(size, f&quot;{size}b&quot;)

        fpath = os.path.join(OUTDIR, bin_name)
        print(f&quot;  {bin_name:&amp;gt;30s}  ({os.path.getsize(fpath):&amp;gt;6d} bytes)&quot;
              f&quot;  #{info[&apos;call_num&apos;]:&amp;gt;2d}  @{info[&apos;op_ptr&apos;]:#x}  {label}&quot;)

    print(f&quot;{&apos;=&apos;*60}&quot;)

main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이때 1차 검증 로직에 따라 올바른 47바이트 입력(&lt;code&gt;lactf{si&lt;/code&gt; + 38*&lt;code&gt;A&lt;/code&gt; + &lt;code&gt;}&lt;/code&gt;)을 주고 덤프하면, Phase 2 실행 순서로 &lt;strong&gt;6개의 expressions가&lt;/strong&gt; 나타난다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#  크기       op_ptr 주소       설명
1    9 bytes  0x55555559e567   val_expression(RCX) = 상수 대입
2    4 bytes  0x5555555a4ea9   expression(R8) = 주소 인코딩
3  115 bytes  0x55555559cbf1   val_expression(RIP) — 1차 검증
4    9 bytes  0x5555555a1ba8   val_expression(R12) = 상수 대입
5    9 bytes  0x5555555a4e10   val_expression(R13) = 상수 대입
6  12888 bytes 0x5555555a1bb5  val_expression(RIP) — 2차 검증 VM
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;표현식 #4~#6은 115바이트 사전 검사(#3)를 통과할 때만 나타난다.&lt;/p&gt;
&lt;p&gt;입력이 잘못되면, Unwinding은 정상 복귀 경로를 타고 이 표현식들은 실행되지 않는다. 이 경우 프로세스는 총 6번의 호출(Phase당 3개 × 2 Phase, 표현식 #1~#3만)로 종료된다.&lt;/p&gt;
&lt;h3&gt;3.3 Expression 체인&lt;/h3&gt;
&lt;p&gt;정리하면, 전체 로직은 6개 표현식이 순서대로 실행되면서, 앞의 표현식이 설정한 레지스터 값을 뒤의 표현식이 읽는 구조다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❶ val_expression(RCX) = 0x53AA9         → ARGV 전역 변수 접근 기준점
❷ expression(R8) = 0x555555590d94       → .text 영역 주소. 역참조하면 0x0f10478b4808578b
                                           (코드 바이트를 해시 상수 겸 XOR 키로 재활용)
❸ val_expression(RIP) [115B]            → 1차 검증: argc, strlen, 해시, &apos;}&apos; 확인
                                           통과 시 RIP를 리다이렉트 → ❹❺❻ 실행
                                           실패 시 정상 복귀 → ❹❺❻ 미실행
❹ val_expression(R12) = 0x3E06666E84F11F86  → 2차 VM용 상수 키
❺ val_expression(R13) = 0x5155422E88E856B5  → 2차 VM용 상수 키
❻ val_expression(RIP) [12,888B]         → 2차 검증: 38개 블록으로 나머지 플래그 검증
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;❹❺❻은 같은 FDE에 속해 있어 Unwinder가 해당 FDE를 처리할 때 세 표현식이 함께 실행된다. ❸의 1차 검증이 실패하면 이 FDE에 도달하지 않으므로 ❹❺❻은 실행되지 않는다.&lt;/p&gt;
&lt;p&gt;나의 경우는 lactf{test} 같은 문자열을 줬을 때 ❸까지만 멈춘 걸 보고 해당 표현식 바이트를 덤프해서 로직을 분석하고 초기 입력 검증 조건을 알아냈다.&lt;/p&gt;
&lt;h3&gt;3.4 115바이트 입력값 검증&lt;/h3&gt;
&lt;p&gt;이게 표현식 ❸의 Phase2 115바이트 덤프이다&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;============================================================
  #9  |  Phase 2  |  115 bytes [overwrite]
============================================================
  RCX: [val] 0x5555555a7aa9
   R8: [ptr] 0x555555590d94 → 0xf10478b4808578b
  R12: [ptr] 0x7fffffffdf80 → 0x0
  R13: [ptr] 0x7fffffffdf88 → 0x1000
  CFA: 0x7fffffffe050

  55555559cbf1  f1 41 06 06 08 ff 1a 3d 1c 0a 05 0a 16 52 16 1c   .A.....=.....R..
  55555559cc01  16 13 12 06 32 2e 28 56 00 23 08 06 23 08 06 12   ....2.(V.#..#...
  55555559cc11  30 16 12 06 08 ff 1a 30 29 28 09 00 23 01 16 23   0......0)(..#..#
  55555559cc21  01 16 2f ed ff 17 08 4b 27 08 64 2e 16 06 30 31   ../....K&apos;.d...01
  55555559cc31  1c 40 25 22 0e dc 66 8d a7 9d 87 cc 0c 33 24 27   .@%&quot;..f......3$&apos;
  55555559cc41  58 2e 21 16 31 1c 06 08 ff 1a 08 7d 2e 21 28 08   X.!.1......}.!(.
  55555559cc51  00 f1 41 e1 b7 0e 2f 03 00 38 1c 06 2f 04 00 13   ..A.../..8../...
  55555559cc61  38 1c 06                                          8..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 &lt;strong&gt;바이너리가 플래그를 읽는 방식&lt;/strong&gt;이 드러난다. 사전 검사 표현식은 표현식 ❶에서 &lt;code&gt;0x53AA9&lt;/code&gt;로 설정된 RCX 기준 상대 주소를 계산하는 것으로 시작한다. GDB에서:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) x/gx (0x5555555a7aa9 - 89)
0x5555555a7a50 &amp;lt;std::sys::args::unix::imp::ARGC.0&amp;gt;: 0x0000000000000002

(gdb) x/gx (0x5555555a7aa9 - 81)
0x5555555a7a58 &amp;lt;std::sys::args::unix::imp::ARGV.0&amp;gt;: 0x00007fffffffe268

(gdb) set $argv = *(unsigned long*)(0x5555555a7aa9 - 81)
(gdb) x/s *(char**)($argv + 8)
0x7fffffffe516: &quot;lactf{test}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 표현식은 Rust 런타임이 내부적으로 저장하는 raw 전역 변수에서 &lt;code&gt;argv&lt;/code&gt;를 읽는다. &lt;strong&gt;이로써 플래그가 &lt;code&gt;./reflection &quot;lactf{...}&quot;&lt;/code&gt;로 전달된다는 것이 확실해졌다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;DWARF op코드가 &lt;code&gt;*(RCX-89) == 2&lt;/code&gt; (카운트가 2인지 확인)와 &lt;code&gt;*(*(RCX-81)+8)&lt;/code&gt; (포인터 배열을 따라가 두 번째 요소에 접근)이라는 접근 패턴을 보여주었다. 이것이 argc/argv처럼 보였고, GDB가 해당 주소에서 &lt;code&gt;ARGC.0&lt;/code&gt;과 &lt;code&gt;ARGV.0&lt;/code&gt;이라는 심볼 이름을 보여줌으로써 확인되었다.&lt;/p&gt;
&lt;p&gt;사전 검사는 115바이트 DWARF 표현식을 통해 네 가지 조건을 검증한다:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;블록 A — argc 검사 (오프셋 0–24):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GNU_encoded_addr(funcrel, 6) → 0x3046   ; NOP 패딩 바이트 주소
deref → *(0x3046) &amp;amp; 0xFF = 0x66
lit13; minus → 0x66 - 13 = 89           ; ARGC까지의 오프셋 계산
reg2 (RCX); minus → RCX - 89 = ARGC_addr
deref → *(ARGC_addr) = argc
lit2; ne → (argc ≠ 2)?
bra(+86) → argc ≠ 2이면 탈출
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;블록 B — argv[1] 포인터 (오프셋 25–33):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plus_uconst(8); deref → *(ARGC_addr + 8) = ARGV_ptr
plus_uconst(8); deref → *(ARGV_ptr + 8) = argv[1] = flag_ptr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;블록 C — strlen 루프 (오프셋 34–52):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LOOP:
  dup; deref → 현재 바이트
  const1u(0xFF); and → 8비트로 마스킹
  lit0; eq → 널 검사
  bra(+9) → 널이면 탈출
  plus_uconst(1) → ptr++
  swap; plus_uconst(1); swap → count++
  skip(-19) → 루프 복귀
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;블록 D — 3중 검증 (오프셋 53–96):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;검증 1: (count ^ 75) != 100 → strlen ≠ 47 → 실패
검증 2: (first8_le + 0x0000FFFFFFFFFFFF) ^ 0x66643CED3C6B36E0 != R8 → 해시 실패
검증 3: last_byte != 0x7D (&apos;}&apos;) → 실패
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;성공/실패 분기 (오프셋 97–114):&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;모든 검사 통과 → RIP = GNU_encoded_addr(funcrel, 236513) = 0x3CC21 → 가상 프레임
검사 실패 → RIP = *(CFA-8) = 정상 복귀 주소 → 종료
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;처음 8바이트에 대한 해시 검사를 역산할 수 있다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;first8 + 0x0000FFFFFFFFFFFF = R8_val ^ 0x66643CED3C6B36E0
       = 0x0f10478b4808578b ^ 0x66643CED3C6B36E0
       = 0x69747B667463616B

first8 = 0x69747B667463616B - 0x0000FFFFFFFFFFFF
       = 0x69737B667463616C

LE → ASCII: 6C 61 63 74 66 7B 73 69 = &quot;lactf{si&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이로써 플래그 접두사가 확인되고, 위치 0–7이 &lt;code&gt;lactf{si&lt;/code&gt;임을 알 수 있다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 12KB DWARF VM 리버싱&lt;/h2&gt;
&lt;h3&gt;4.1 바이트코드 추출 및 디코딩&lt;/h3&gt;
&lt;p&gt;참고: https://github.com/gcc-mirror/gcc/tree/705d87ba8e987cb8e0ca8b6d400c0aff8739eb23/libgcc&lt;/p&gt;
&lt;p&gt;메인 표현식은 가상 프레임 FDE의 12,888바이트 &lt;code&gt;val_expression(RIP)&lt;/code&gt;이다.&lt;/p&gt;
&lt;p&gt;마찬가지로 앞서 작성한 GDB 덤프 스크립트로 통째로 덤프했다.&lt;/p&gt;
&lt;p&gt;이후 &lt;code&gt;libgcc/unwind-dw2.c&lt;/code&gt;의 &lt;code&gt;execute_stack_op()&lt;/code&gt;과 &lt;code&gt;libgcc/unwind-pe.h&lt;/code&gt;를 기반으로 한 커스텀 디코더(&lt;code&gt;dwarf_decoder.py&lt;/code&gt;)가 이를 9,195개의 명령어로 디스어셈블했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dwarf_decoder.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
&quot;&quot;&quot;
DWARF Expression VM Decoder
============================
Decodes DW_OP_* opcodes from raw .eh_frame bytecode,
including GNU extension DW_OP_GNU_encoded_addr (0xF1).

Based on libgcc/unwind-dw2.c execute_stack_op() and libgcc/unwind-pe.h

Usage:
  python3 dwarf_decoder.py &amp;lt;dwarf_vm.bin&amp;gt;
&quot;&quot;&quot;
import sys, struct
from collections import Counter

# x86-64 register name mapping
REG_NAMES = {
    0:&quot;rax&quot;, 1:&quot;rdx&quot;, 2:&quot;rcx&quot;, 3:&quot;rbx&quot;, 4:&quot;rsi&quot;, 5:&quot;rdi&quot;, 6:&quot;rbp&quot;, 7:&quot;rsp&quot;,
    8:&quot;r8&quot;, 9:&quot;r9&quot;, 10:&quot;r10&quot;, 11:&quot;r11&quot;, 12:&quot;r12&quot;, 13:&quot;r13&quot;, 14:&quot;r14&quot;, 15:&quot;r15&quot;, 16:&quot;rip&quot;
}

def regname(n):
    return REG_NAMES.get(n, f&quot;r{n}&quot;)

# ═══════════════════════════════════════════════════
#  LEB128 readers
# ═══════════════════════════════════════════════════

def read_uleb128(data, pos):
    result = shift = 0
    while pos &amp;lt; len(data):
        b = data[pos]; pos += 1
        result |= (b &amp;amp; 0x7f) &amp;lt;&amp;lt; shift; shift += 7
        if not (b &amp;amp; 0x80): break
    return result, pos

def read_sleb128(data, pos):
    result = shift = 0; b = 0
    while pos &amp;lt; len(data):
        b = data[pos]; pos += 1
        result |= (b &amp;amp; 0x7f) &amp;lt;&amp;lt; shift; shift += 7
        if not (b &amp;amp; 0x80): break
    if shift &amp;lt; 64 and (b &amp;amp; 0x40): result |= -(1 &amp;lt;&amp;lt; shift)
    if result &amp;gt;= (1 &amp;lt;&amp;lt; 63): result -= (1 &amp;lt;&amp;lt; 64)
    return result, pos

# ═══════════════════════════════════════════════════
#  GNU encoded address decoder
# ═══════════════════════════════════════════════════

MOD_NAMES = {0x00:&quot;abs&quot;, 0x10:&quot;pcrel&quot;, 0x20:&quot;textrel&quot;, 0x30:&quot;datarel&quot;, 0x40:&quot;funcrel&quot;, 0x50:&quot;aligned&quot;}
FMT_NAMES = {0x00:&quot;ptr&quot;, 0x01:&quot;uleb&quot;, 0x02:&quot;u2&quot;, 0x03:&quot;u4&quot;, 0x04:&quot;u8&quot;,
             0x09:&quot;sleb&quot;, 0x0a:&quot;s2&quot;, 0x0b:&quot;s4&quot;, 0x0c:&quot;s8&quot;}

def decode_encoded_addr(data, pos):
    &quot;&quot;&quot;Decode DW_OP_GNU_encoded_addr per libgcc/unwind-pe.h&quot;&quot;&quot;
    enc = data[pos]; pos += 1
    fmt = enc &amp;amp; 0x0f; modifier = enc &amp;amp; 0x70; indirect = enc &amp;amp; 0x80

    if   fmt == 0x00: val = int.from_bytes(data[pos:pos+8], &apos;little&apos;); pos += 8
    elif fmt == 0x01: val, pos = read_uleb128(data, pos)
    elif fmt == 0x02: val = int.from_bytes(data[pos:pos+2], &apos;little&apos;); pos += 2
    elif fmt == 0x03: val = int.from_bytes(data[pos:pos+4], &apos;little&apos;); pos += 4
    elif fmt == 0x04: val = int.from_bytes(data[pos:pos+8], &apos;little&apos;); pos += 8
    elif fmt == 0x09: val, pos = read_sleb128(data, pos)
    elif fmt == 0x0a: val = int.from_bytes(data[pos:pos+2], &apos;little&apos;, signed=True); pos += 2
    elif fmt == 0x0b: val = int.from_bytes(data[pos:pos+4], &apos;little&apos;, signed=True); pos += 4
    elif fmt == 0x0c: val = int.from_bytes(data[pos:pos+8], &apos;little&apos;, signed=True); pos += 8
    else: val = 0

    enc_str = f&quot;{MOD_NAMES.get(modifier,&apos;?&apos;)}|{FMT_NAMES.get(fmt,&apos;?&apos;)}&quot;
    if indirect: enc_str += &quot;|indirect&quot;
    return val, enc_str, pos

# ═══════════════════════════════════════════════════
#  Main decoder
# ═══════════════════════════════════════════════════

def decode_dwarf_expr(data, func_base=0x3cc20):
    &quot;&quot;&quot;Decode DWARF expression bytecode into (offset, name, description, metadata) list.&quot;&quot;&quot;
    pos = 0; insns = []

    while pos &amp;lt; len(data):
        off = pos; op = data[pos]; pos += 1

        # DW_OP_lit0..lit31 (0x30..0x4f)
        if 0x30 &amp;lt;= op &amp;lt;= 0x4f:
            v = op - 0x30
            insns.append((off, f&quot;lit{v}&quot;, f&quot;push {v}&quot;, []))
        # DW_OP_reg0..reg31 (0x50..0x6f)
        elif 0x50 &amp;lt;= op &amp;lt;= 0x6f:
            r = op - 0x50
            insns.append((off, f&quot;reg{r}&quot;, f&quot;push {regname(r)}&quot;, []))
        # DW_OP_breg0..breg31 (0x70..0x8f)
        elif 0x70 &amp;lt;= op &amp;lt;= 0x8f:
            r = op - 0x70; sv, pos = read_sleb128(data, pos)
            insns.append((off, f&quot;breg{r}&quot;, f&quot;push {regname(r)}+({sv})&quot;, []))

        elif op == 0x03:  # DW_OP_addr
            v = int.from_bytes(data[pos:pos+8], &apos;little&apos;); pos += 8
            insns.append((off, &quot;addr&quot;, f&quot;push 0x{v:x}&quot;, []))
        elif op == 0x06:
            insns.append((off, &quot;deref&quot;, &quot;pop a; push *(uint64*)a&quot;, []))
        elif op == 0x08:
            v = data[pos]; pos += 1
            insns.append((off, &quot;const1u&quot;, f&quot;push {v} (0x{v:02x})&quot;, []))
        elif op == 0x09:
            v = struct.unpack_from(&apos;b&apos;, data, pos)[0]; pos += 1
            insns.append((off, &quot;const1s&quot;, f&quot;push {v}&quot;, []))
        elif op == 0x0a:
            v = int.from_bytes(data[pos:pos+2], &apos;little&apos;); pos += 2
            insns.append((off, &quot;const2u&quot;, f&quot;push {v} (0x{v:04x})&quot;, []))
        elif op == 0x0b:
            v = int.from_bytes(data[pos:pos+2], &apos;little&apos;, signed=True); pos += 2
            insns.append((off, &quot;const2s&quot;, f&quot;push {v}&quot;, []))
        elif op == 0x0c:
            v = int.from_bytes(data[pos:pos+4], &apos;little&apos;); pos += 4
            insns.append((off, &quot;const4u&quot;, f&quot;push 0x{v:08x}&quot;, []))
        elif op == 0x0e:
            v = int.from_bytes(data[pos:pos+8], &apos;little&apos;); pos += 8
            insns.append((off, &quot;const8u&quot;, f&quot;push 0x{v:016x}&quot;, []))
        elif op == 0x0f:
            v = int.from_bytes(data[pos:pos+8], &apos;little&apos;, signed=True); pos += 8
            insns.append((off, &quot;const8s&quot;, f&quot;push {v}&quot;, []))
        elif op == 0x10:
            v, pos = read_uleb128(data, pos)
            insns.append((off, &quot;constu&quot;, f&quot;push {v} (0x{v:x})&quot;, []))
        elif op == 0x11:
            v, pos = read_sleb128(data, pos)
            insns.append((off, &quot;consts&quot;, f&quot;push {v}&quot;, []))

        elif op == 0x12: insns.append((off, &quot;dup&quot;,   &quot;push TOS&quot;, []))
        elif op == 0x13: insns.append((off, &quot;drop&quot;,  &quot;pop TOS&quot;, []))
        elif op == 0x14: insns.append((off, &quot;over&quot;,  &quot;push stack[-2]&quot;, []))
        elif op == 0x15:
            idx = data[pos]; pos += 1
            insns.append((off, &quot;pick&quot;, f&quot;push stack[-{idx+1}]&quot;, []))
        elif op == 0x16: insns.append((off, &quot;swap&quot;,  &quot;swap TOS and TOS-1&quot;, []))
        elif op == 0x17: insns.append((off, &quot;rot&quot;,   &quot;rotate top 3: [a,b,c]-&amp;gt;[c,a,b]&quot;, []))
        elif op == 0x19: insns.append((off, &quot;abs&quot;,   &quot;TOS = |TOS|&quot;, []))

        elif op == 0x1a: insns.append((off, &quot;and&quot;,   &quot;pop b,a; push a &amp;amp; b&quot;, []))
        elif op == 0x1b: insns.append((off, &quot;div&quot;,   &quot;pop b,a; push a / b&quot;, []))
        elif op == 0x1c: insns.append((off, &quot;minus&quot;, &quot;pop b,a; push a - b&quot;, []))
        elif op == 0x1d: insns.append((off, &quot;mod&quot;,   &quot;pop b,a; push a % b&quot;, []))
        elif op == 0x1e: insns.append((off, &quot;mul&quot;,   &quot;pop b,a; push a * b&quot;, []))
        elif op == 0x1f: insns.append((off, &quot;neg&quot;,   &quot;TOS = -TOS&quot;, []))
        elif op == 0x20: insns.append((off, &quot;not&quot;,   &quot;TOS = ~TOS&quot;, []))
        elif op == 0x21: insns.append((off, &quot;or&quot;,    &quot;pop b,a; push a | b&quot;, []))
        elif op == 0x22: insns.append((off, &quot;plus&quot;,  &quot;pop b,a; push a + b&quot;, []))
        elif op == 0x23:
            v, pos = read_uleb128(data, pos)
            insns.append((off, &quot;plus_uconst&quot;, f&quot;TOS += {v}&quot;, []))
        elif op == 0x24: insns.append((off, &quot;shl&quot;,   &quot;pop b,a; push a &amp;lt;&amp;lt; b&quot;, []))
        elif op == 0x25: insns.append((off, &quot;shr&quot;,   &quot;pop b,a; push a &amp;gt;&amp;gt; b (unsigned)&quot;, []))
        elif op == 0x26: insns.append((off, &quot;shra&quot;,  &quot;pop b,a; push a &amp;gt;&amp;gt; b (signed)&quot;, []))
        elif op == 0x27: insns.append((off, &quot;xor&quot;,   &quot;pop b,a; push a ^ b&quot;, []))

        elif op == 0x28:  # DW_OP_bra (conditional branch)
            soff = int.from_bytes(data[pos:pos+2], &apos;little&apos;, signed=True); pos += 2
            tgt = pos + soff
            insns.append((off, &quot;bra&quot;, f&quot;if TOS!=0 goto {tgt} (offset {soff:+d})&quot;, [(&quot;target&quot;, tgt)]))

        elif op == 0x29: insns.append((off, &quot;eq&quot;, &quot;pop b,a; push (a == b)&quot;, []))
        elif op == 0x2a: insns.append((off, &quot;ge&quot;, &quot;pop b,a; push (a &amp;gt;= b)&quot;, []))
        elif op == 0x2b: insns.append((off, &quot;gt&quot;, &quot;pop b,a; push (a &amp;gt; b)&quot;, []))
        elif op == 0x2c: insns.append((off, &quot;le&quot;, &quot;pop b,a; push (a &amp;lt;= b)&quot;, []))
        elif op == 0x2d: insns.append((off, &quot;lt&quot;, &quot;pop b,a; push (a &amp;lt; b)&quot;, []))
        elif op == 0x2e: insns.append((off, &quot;ne&quot;, &quot;pop b,a; push (a != b)&quot;, []))

        elif op == 0x2f:  # DW_OP_skip (unconditional branch)
            soff = int.from_bytes(data[pos:pos+2], &apos;little&apos;, signed=True); pos += 2
            tgt = pos + soff
            insns.append((off, &quot;skip&quot;, f&quot;goto {tgt} (offset {soff:+d})&quot;, [(&quot;target&quot;, tgt)]))

        elif op == 0x90:  # DW_OP_regx
            v, pos = read_uleb128(data, pos)
            insns.append((off, &quot;regx&quot;, f&quot;push {regname(v)} (reg {v})&quot;, []))
        elif op == 0x91:  # DW_OP_fbreg
            sv, pos = read_sleb128(data, pos)
            insns.append((off, &quot;fbreg&quot;, f&quot;push CFA+({sv})&quot;, []))
        elif op == 0x93:  # DW_OP_piece
            v, pos = read_uleb128(data, pos)
            insns.append((off, &quot;piece&quot;, f&quot;piece({v} bytes)&quot;, []))
        elif op == 0x9f:
            insns.append((off, &quot;stack_value&quot;, &quot;TOS is the value (not addr)&quot;, []))

        elif op == 0xf1:  # DW_OP_GNU_encoded_addr
            val, enc_str, pos = decode_encoded_addr(data, pos)
            if &quot;funcrel&quot; in enc_str:
                resolved = func_base + val
                insns.append((off, &quot;GNU_encoded_addr&quot;, f&quot;({enc_str}, {val}) -&amp;gt; 0x{resolved:x}&quot;, []))
            elif &quot;datarel&quot; in enc_str:
                insns.append((off, &quot;GNU_encoded_addr&quot;, f&quot;({enc_str}, {val}) -&amp;gt; dbase+{val}&quot;, []))
            else:
                insns.append((off, &quot;GNU_encoded_addr&quot;, f&quot;({enc_str}, {val})&quot;, []))

        elif op == 0x00:
            insns.append((off, &quot;nop&quot;, &quot;&quot;, []))
        else:
            insns.append((off, f&quot;UNKNOWN_0x{op:02x}&quot;, &quot;???&quot;, []))

    return insns

# ═══════════════════════════════════════════════════
#  Main
# ═══════════════════════════════════════════════════

def main():
    if len(sys.argv) != 2:
        print(f&quot;Usage: python3 {sys.argv[0]} &amp;lt;dwarf_vm.bin&amp;gt;&quot;)
        sys.exit(1)

    data = open(sys.argv[1], &quot;rb&quot;).read()
    print(f&quot;Loaded {len(data)} bytes of DWARF expression bytecode\n&quot;)

    insns = decode_dwarf_expr(data, func_base=0x3cc20)

    # Collect branch targets for annotation
    targets = {v for _, _, _, meta in insns for k, v in meta if k == &quot;target&quot;}

    # Print disassembly
    print(f&quot;{&apos;OFF&apos;:&amp;gt;5}  {&apos;OPCODE&apos;:&amp;lt;22} {&apos;DESCRIPTION&apos;}&quot;)
    print(&quot;=&quot; * 80)
    for off, name, desc, _ in insns:
        prefix = &quot;&amp;gt;&amp;gt;&amp;gt;&quot; if off in targets else &quot;   &quot;
        print(f&quot;{prefix}{off:5d}  {name:&amp;lt;22} {desc}&quot;)

    print(f&quot;\nTotal: {len(insns)} instructions decoded from {len(data)} bytes&quot;)

    # Top opcodes
    counts = Counter(name.split(&apos;(&apos;)[0] for _, name, _, _ in insns)
    print(f&quot;\nTop opcodes:&quot;)
    for name, count in counts.most_common(20):
        print(f&quot;  {name}: {count}&quot;)

if __name__ == &quot;__main__&quot;:
    main()

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;디코더는 바이트코드에서 사용된 모든 DW_OP Op코드를 처리하며, 다양한 인코딩 형식(funcrel|uleb128, datarel|udata4 등)의 GNU 확장 &lt;code&gt;DW_OP_GNU_encoded_addr&lt;/code&gt;(0xF1)도 포함한다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ python3 dwarf_decoder.py dwarf_vm.bin
Loaded 12888 bytes of DWARF expression bytecode

  OFF  OPCODE                 DESCRIPTION
================================================================================
     0  GNU_encoded_addr       (datarel|uleb, 8) → dbase+8
     3  breg2                  push rcx+(-81)
     6  deref                  pop a; push *(uint64*)a
     7  plus                   pop b,a; push a + b
     8  deref                  pop a; push *(uint64*)a
     9  const2u                push 5377 (0x1501)
    ...
 12884  GNU_encoded_addr       (funcrel|uleb, 14000) → 0x402d0

Total: 9195 instructions decoded from 12888 bytes
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 제약 조건 요약&lt;/h3&gt;
&lt;p&gt;12,888바이트 VM은 38개의 검증 블록으로 구성된다. 각 블록은 결과로 0 또는 1을 반환하고, 전체가 AND되어 하나라도 실패하면 최종 결과는 0이다.&lt;/p&gt;
&lt;p&gt;각 블록의 검증 방식은 이렇다: 플래그 바이트를 2진수로 펼친 뒤, &lt;strong&gt;1이 연속으로 붙어있는 덩어리&lt;/strong&gt;만 골라 각 덩어리의 크기를 기록한다. 0은 구분자일 뿐이고, 1 덩어리의 크기를 왼쪽부터 나열한 것이 그 바이트의 &quot;시그니처&quot;다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&apos;e&apos; = 01100101
      0 [11] 00 [1] 0 [1]    → 시그니처 = [2, 1, 1]

&apos;_&apos; = 01011111
      0 [1] 0 [11111]        → 시그니처 = [1, 5]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;VM은 플래그의 각 글자에 대해 이 시그니처를 계산하고, 미리 정해둔 정답 시그니처와 같은지 비교한다.&lt;/p&gt;
&lt;p&gt;블록은 두 유형이다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;바이트 단위 (31개):&lt;/strong&gt; 한 글자의 시그니처만 검증. 각 위치를 독립적으로 제약&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;교차 바이트 (7개):&lt;/strong&gt; 여러 글자에 걸쳐 특정 비트 위치의 연속 패턴을 검증. 위치 간 의존성이 생기므로 한 글자씩 분석해서는 풀 수 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;38개 블록에서 총 152개의 &lt;code&gt;eq&lt;/code&gt; 비교가 수행된다. 모든 상수는 R8 레지스터 값으로 XOR 난독화되어 있으며, 디코딩 패턴 매칭으로 추출했다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. Solution&lt;/h2&gt;
&lt;p&gt;디코딩한 바이트코드의 구조를 파악했으니, 이를 기반으로 솔버를 작성하면 된다.&lt;/p&gt;
&lt;p&gt;LLM이 소화할 만큼 문제를 잘게 분해했으니 더 이상 &quot;ANTHROPIC MAGIC STRING&quot;을 두려워하지 않아도 된다 ㅇㅅㅇ&lt;/p&gt;
&lt;h3&gt;Solve Code&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;solve.py&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
&quot;&quot;&quot;
LACTF &apos;reflection&apos; exploit solver
==================================
Extracts constraints from DWARF Expression VM bytecode in .eh_frame,
then recovers the flag via Simulated Annealing + Coordinate Descent.

Usage:
  python3 solves.py &amp;lt;dwarf_vm.bin&amp;gt; &amp;lt;vm_fast.so&amp;gt;
&quot;&quot;&quot;
import struct, sys, ctypes, random, time, math

FLAG_PREFIX = &quot;lactf{si&quot;
FLAG_LEN = 47

# ═══════════════════════════════════════════════════
#  1. Bytecode loader + DWARF disassembler
# ═══════════════════════════════════════════════════

def load_bytecode(path):
    with open(path, &quot;rb&quot;) as f:
        return f.read()

def read_uleb128(data, pos):
    result = shift = 0
    while pos &amp;lt; len(data):
        b = data[pos]; pos += 1
        result |= (b &amp;amp; 0x7f) &amp;lt;&amp;lt; shift; shift += 7
        if not (b &amp;amp; 0x80): break
    return result, pos

def read_sleb128(data, pos):
    result = shift = 0; b = 0
    while pos &amp;lt; len(data):
        b = data[pos]; pos += 1
        result |= (b &amp;amp; 0x7f) &amp;lt;&amp;lt; shift; shift += 7
        if not (b &amp;amp; 0x80): break
    if shift &amp;lt; 64 and (b &amp;amp; 0x40): result |= -(1 &amp;lt;&amp;lt; shift)
    if result &amp;gt;= (1 &amp;lt;&amp;lt; 63): result -= (1 &amp;lt;&amp;lt; 64)
    return result, pos

def parse_instructions(data):
    &quot;&quot;&quot;Decode raw DWARF expression bytecode into (offset, mnemonic, operand) list.&quot;&quot;&quot;
    pos = 0; ops = []
    while pos &amp;lt; len(data):
        off = pos; op = data[pos]; pos += 1
        if   0x30 &amp;lt;= op &amp;lt;= 0x4f: ops.append((off, &apos;lit&apos;, op - 0x30))
        elif 0x50 &amp;lt;= op &amp;lt;= 0x6f: ops.append((off, &apos;reg&apos;, op - 0x50))
        elif 0x70 &amp;lt;= op &amp;lt;= 0x8f:
            _, pos = read_sleb128(data, pos); ops.append((off, &apos;breg&apos;, op - 0x70))
        elif op == 0x06: ops.append((off, &apos;deref&apos;, None))
        elif op == 0x08: v = data[pos]; pos += 1; ops.append((off, &apos;const1u&apos;, v))
        elif op == 0x09: v = struct.unpack_from(&apos;b&apos;, data, pos)[0]; pos += 1; ops.append((off, &apos;const1s&apos;, v))
        elif op == 0x0a: v = int.from_bytes(data[pos:pos+2], &apos;little&apos;); pos += 2; ops.append((off, &apos;const2u&apos;, v))
        elif op == 0x0e: v = int.from_bytes(data[pos:pos+8], &apos;little&apos;); pos += 8; ops.append((off, &apos;const8u&apos;, v))
        elif op == 0x10: v, pos = read_uleb128(data, pos); ops.append((off, &apos;constu&apos;, v))
        elif op == 0x12: ops.append((off, &apos;dup&apos;, None))
        elif op == 0x13: ops.append((off, &apos;drop&apos;, None))
        elif op == 0x14: ops.append((off, &apos;over&apos;, None))
        elif op == 0x15: v = data[pos]; pos += 1; ops.append((off, &apos;pick&apos;, v))
        elif op == 0x16: ops.append((off, &apos;swap&apos;, None))
        elif op == 0x17: ops.append((off, &apos;rot&apos;, None))
        elif op == 0x1a: ops.append((off, &apos;and&apos;, None))
        elif op == 0x1b: ops.append((off, &apos;div&apos;, None))
        elif op == 0x1c: ops.append((off, &apos;minus&apos;, None))
        elif op == 0x1d: ops.append((off, &apos;mod&apos;, None))
        elif op == 0x1e: ops.append((off, &apos;mul&apos;, None))
        elif op == 0x1f: ops.append((off, &apos;neg&apos;, None))
        elif op == 0x20: ops.append((off, &apos;not&apos;, None))
        elif op == 0x21: ops.append((off, &apos;or&apos;, None))
        elif op == 0x22: ops.append((off, &apos;plus&apos;, None))
        elif op == 0x23: v, pos = read_uleb128(data, pos); ops.append((off, &apos;plus_uconst&apos;, v))
        elif op == 0x24: ops.append((off, &apos;shl&apos;, None))
        elif op == 0x25: ops.append((off, &apos;shr&apos;, None))
        elif op == 0x26: ops.append((off, &apos;shra&apos;, None))
        elif op == 0x27: ops.append((off, &apos;xor&apos;, None))
        elif op == 0x28:
            soff = int.from_bytes(data[pos:pos+2], &apos;little&apos;, signed=True); pos += 2
            ops.append((off, &apos;bra&apos;, pos + soff))
        elif op == 0x29: ops.append((off, &apos;eq&apos;, None))
        elif op == 0x2a: ops.append((off, &apos;ge&apos;, None))
        elif op == 0x2b: ops.append((off, &apos;gt&apos;, None))
        elif op == 0x2c: ops.append((off, &apos;le&apos;, None))
        elif op == 0x2d: ops.append((off, &apos;lt&apos;, None))
        elif op == 0x2e: ops.append((off, &apos;ne&apos;, None))
        elif op == 0x2f:
            soff = int.from_bytes(data[pos:pos+2], &apos;little&apos;, signed=True); pos += 2
            ops.append((off, &apos;skip&apos;, pos + soff))
        elif op == 0x90: v, pos = read_uleb128(data, pos); ops.append((off, &apos;regx&apos;, v))
        elif op == 0xf1:
            enc = data[pos]; pos += 1; fmt = enc &amp;amp; 0x0f
            if   fmt == 0x01: v, pos = read_uleb128(data, pos)
            elif fmt == 0x02: v = int.from_bytes(data[pos:pos+2], &apos;little&apos;); pos += 2
            elif fmt == 0x03: v = int.from_bytes(data[pos:pos+4], &apos;little&apos;); pos += 4
            elif fmt == 0x04: v = int.from_bytes(data[pos:pos+8], &apos;little&apos;); pos += 8
            else: v = 0
            ops.append((off, &apos;encoded_addr&apos;, (enc, enc &amp;amp; 0x70, v)))
        elif op == 0x00: ops.append((off, &apos;nop&apos;, None))
        else: ops.append((off, f&apos;unk_{op:02x}&apos;, None))
    return ops

# ═══════════════════════════════════════════════════
#  2. Constraint extraction
# ═══════════════════════════════════════════════════
R8 = 0x8578b  # constant key stored in r8

def xr(X, shift):
    &quot;&quot;&quot;XOR decode: X ^ ((r8 &amp;gt;&amp;gt; shift) &amp;amp; 0xFF)&quot;&quot;&quot;
    return X ^ ((R8 &amp;gt;&amp;gt; shift) &amp;amp; 0xFF)

def bit_groups_of(byte_val):
    &quot;&quot;&quot;Return list of consecutive 1-bit group lengths (MSB to LSB).&quot;&quot;&quot;
    if byte_val == 0: return []
    groups = []; p = 7
    while p &amp;gt;= 0 and not (byte_val &amp;amp; (1 &amp;lt;&amp;lt; p)): p -= 1
    while p &amp;gt;= 0:
        cnt = 0
        while p &amp;gt;= 0 and (byte_val &amp;amp; (1 &amp;lt;&amp;lt; p)): cnt += 1; p -= 1
        if cnt &amp;gt; 0: groups.append(cnt)
        while p &amp;gt;= 0 and not (byte_val &amp;amp; (1 &amp;lt;&amp;lt; p)): p -= 1
    return groups

def find_constraint_eq_offsets(ops):
    &quot;&quot;&quot;Find bytecode offsets of all &apos;eq&apos; ops that follow the XOR-decode pattern.&quot;&quot;&quot;
    eq_offsets = set()
    for i in range(len(ops)):
        off, name, operand = ops[i]
        if name not in (&apos;const1u&apos;, &apos;lit&apos;) or i + 7 &amp;gt;= len(ops): continue
        n1 = ops[i+1]
        if not ((n1[1] == &apos;regx&apos; and n1[2] == 8) or (n1[1] == &apos;reg&apos; and n1[2] == 8)): continue
        n2 = ops[i+2]
        if n2[1] not in (&apos;lit&apos;, &apos;const1u&apos;): continue
        if ops[i+3][1] != &apos;shr&apos;: continue
        if ops[i+4][1] != &apos;const1u&apos; or ops[i+4][2] != 255: continue
        if ops[i+5][1] != &apos;and&apos;: continue
        if ops[i+6][1] != &apos;xor&apos;: continue
        if ops[i+7][1] == &apos;eq&apos;:
            eq_offsets.add(ops[i+7][0])
    return eq_offsets

def find_block_ranges(ops):
    &quot;&quot;&quot;Detect constraint block boundaries via rot/drop/drop/and sequences.&quot;&quot;&quot;
    markers = []
    for i in range(len(ops) - 3):
        if (ops[i][1] == &apos;rot&apos; and ops[i+1][1] == &apos;drop&apos; and
            ops[i+2][1] == &apos;drop&apos; and ops[i+3][1] == &apos;and&apos;):
            markers.append(ops[i][0])
    markers.sort()
    ranges = []
    prev = 9
    for m in markers:
        ranges.append((prev, m))
        prev = m + 4
    return ranges

def extract_per_byte_constraints(ops, block_ranges):
    &quot;&quot;&quot;From single-deref blocks, extract {flag_pos: expected_bit_groups}.&quot;&quot;&quot;
    per_byte = {}
    for bstart, bend in block_ranges:
        block_ops = [(o, n, p) for o, n, p in ops if bstart &amp;lt;= o &amp;lt; bend]
        if sum(1 for _, n, _ in block_ops if n == &apos;deref&apos;) &amp;gt; 1:
            continue  # skip cross-byte blocks

        brs, gcs = [], []
        for i in range(len(ops)):
            off, name, operand = ops[i]
            if not (bstart &amp;lt;= off &amp;lt; bend): continue
            if name not in (&apos;const1u&apos;, &apos;lit&apos;) or i + 7 &amp;gt;= len(ops): continue
            X = operand
            n1 = ops[i+1]
            if not ((n1[1] == &apos;regx&apos; and n1[2] == 8) or (n1[1] == &apos;reg&apos; and n1[2] == 8)): continue
            n2 = ops[i+2]
            if n2[1] in (&apos;lit&apos;, &apos;const1u&apos;): shift = n2[2]
            else: continue
            if ops[i+3][1] != &apos;shr&apos;: continue
            if ops[i+4][1] != &apos;const1u&apos; or ops[i+4][2] != 255: continue
            if ops[i+5][1] != &apos;and&apos;: continue
            if ops[i+6][1] != &apos;xor&apos;: continue
            val = xr(X, shift)
            if   ops[i+7][1] == &apos;plus&apos;: brs.append(val)
            elif ops[i+7][1] == &apos;eq&apos;:   gcs.append(val)

        if brs:
            per_byte[brs[0]] = gcs
    return per_byte

def compute_per_byte_candidates(per_byte):
    &quot;&quot;&quot;Build candidate char set for each flag position from per-byte constraints.&quot;&quot;&quot;
    candidates = {}
    for pos in range(FLAG_LEN):
        if pos &amp;lt; len(FLAG_PREFIX):
            candidates[pos] = [ord(FLAG_PREFIX[pos])]
        elif pos == FLAG_LEN - 1:
            candidates[pos] = [ord(&apos;}&apos;)]
        elif pos in per_byte:
            expected = per_byte[pos]
            cands = [v for v in range(32, 127) if bit_groups_of(v) == expected]
            candidates[pos] = cands if cands else list(range(32, 127))
        else:
            candidates[pos] = list(range(32, 127))
    return candidates

# ═══════════════════════════════════════════════════
#  3. C VM interface (ctypes wrapper)
# ═══════════════════════════════════════════════════
class CVM:
    def __init__(self, so_path, bytecode, constraint_offsets, r8=R8):
        self.lib = ctypes.CDLL(so_path)
        self.bc = (ctypes.c_uint8 * len(bytecode))(*bytecode)
        self.bc_len = len(bytecode)
        self.r8 = ctypes.c_uint64(r8)
        self.n_con = len(constraint_offsets)
        c_offsets = (ctypes.c_int * self.n_con)(*sorted(constraint_offsets))
        self.lib.setup_constraints(c_offsets, self.n_con)

    def run_quick(self, flag_bytes):
        &quot;&quot;&quot;Execute VM with given flag -&amp;gt; (result, total_passes)&quot;&quot;&quot;
        c_flag = (ctypes.c_uint8 * len(flag_bytes))(*flag_bytes)
        c_out = ctypes.c_int(0)
        passes = self.lib.vm_run_quick(
            self.bc, self.bc_len,
            c_flag, len(flag_bytes),
            self.r8, ctypes.byref(c_out)
        )
        return c_out.value, passes

# ═══════════════════════════════════════════════════
#  4. Coordinate Descent solver
# ═══════════════════════════════════════════════════
def solve_coordinate_descent(vm, candidates, n_constraints, max_rounds=20):
    &quot;&quot;&quot;Greedy per-position optimization: pick the value that maximizes pass count.&quot;&quot;&quot;
    flag = [candidates[p][0] for p in range(FLAG_LEN)]
    _, best_score = vm.run_quick(flag)

    for rnd in range(max_rounds):
        improved = False
        positions = list(range(len(FLAG_PREFIX), FLAG_LEN - 1))
        random.shuffle(positions)

        for pos in positions:
            orig = flag[pos]; best_val = orig
            for v in candidates[pos]:
                if v == orig: continue
                flag[pos] = v
                _, score = vm.run_quick(flag)
                if score &amp;gt; best_score:
                    best_score = score; best_val = v; improved = True
            flag[pos] = best_val

        result, _ = vm.run_quick(flag)
        flag_str = &apos;&apos;.join(chr(b) for b in flag)
        print(f&quot;  [CD round {rnd+1}] score={best_score}/{n_constraints} flag={flag_str}&quot;)
        if result != 0:
            return flag, True
        if not improved:
            break

    return flag, False

# ═══════════════════════════════════════════════════
#  5. Simulated Annealing solver
# ═══════════════════════════════════════════════════
def solve_simulated_annealing(vm, candidates, n_constraints,
                               max_iter=2000000, T_start=8.0, T_end=0.001):
    &quot;&quot;&quot;SA: probabilistically accepts worse moves at high T to escape local optima.&quot;&quot;&quot;
    mutable = [p for p in range(len(FLAG_PREFIX), FLAG_LEN - 1) if len(candidates[p]) &amp;gt; 1]

    # random init
    flag = [random.choice(candidates[p]) if len(candidates[p]) &amp;gt; 1 else candidates[p][0]
            for p in range(FLAG_LEN)]
    _, cur_score = vm.run_quick(flag)
    best_score = cur_score; best_flag = list(flag)
    log_interval = max_iter // 20

    for i in range(max_iter):
        T = T_start * (T_end / T_start) ** (i / max_iter)

        pos = random.choice(mutable)
        old_val = flag[pos]
        new_val = random.choice(candidates[pos])
        if new_val == old_val: continue

        flag[pos] = new_val
        result, new_score = vm.run_quick(flag)

        if result != 0:
            print(f&quot;  [SA iter {i}] FOUND! score={new_score}/{n_constraints}&quot;)
            return flag, True

        delta = new_score - cur_score
        if delta &amp;gt; 0 or random.random() &amp;lt; math.exp(delta / max(T, 1e-10)):
            cur_score = new_score
            if new_score &amp;gt; best_score:
                best_score = new_score; best_flag = list(flag)
        else:
            flag[pos] = old_val

        if i % log_interval == 0 and i &amp;gt; 0:
            flag_str = &apos;&apos;.join(chr(b) for b in best_flag)
            print(f&quot;  [SA {i//1000}K/{max_iter//1000}K] T={T:.3f} best={best_score}/{n_constraints} &quot;
                  f&quot;flag={flag_str}&quot;)

    return best_flag, False

def solve_sa_with_restarts(vm, candidates, n_constraints,
                            n_restarts=10, iter_per_restart=2000000):
    &quot;&quot;&quot;Run SA multiple times with random restarts.&quot;&quot;&quot;
    best_overall = 0; best_flag = None

    for r in range(n_restarts):
        print(f&quot;\n  --- SA restart {r+1}/{n_restarts} ---&quot;)
        flag, found = solve_simulated_annealing(
            vm, candidates, n_constraints,
            max_iter=iter_per_restart, T_start=6.0, T_end=0.001
        )
        if found: return flag, True

        _, score = vm.run_quick(flag)
        if score &amp;gt; best_overall:
            best_overall = score; best_flag = list(flag)
        flag_str = &apos;&apos;.join(chr(b) for b in flag)
        print(f&quot;  [SA restart {r+1}] score={score}/{n_constraints} &quot;
              f&quot;(best={best_overall}) flag={flag_str}&quot;)

        # if very close, brute-force one position at a time
        if score &amp;gt;= n_constraints - 2:
            print(f&quot;  [*] Near-solution ({score}/{n_constraints}), trying single-pos bruteforce...&quot;)
            for pos in range(len(FLAG_PREFIX), FLAG_LEN - 1):
                for v in range(32, 127):
                    trial = list(flag); trial[pos] = v
                    result, _ = vm.run_quick(trial)
                    if result != 0:
                        print(f&quot;  FOUND via pos {pos} = &apos;{chr(v)}&apos;&quot;)
                        return trial, True

    return best_flag, False

# ═══════════════════════════════════════════════════
#  6. Main
# ═══════════════════════════════════════════════════
def main():
    if len(sys.argv) != 3:
        print(f&quot;Usage: python3 {sys.argv[0]} &amp;lt;dwarf_vm.bin&amp;gt; &amp;lt;vm_fast.so&amp;gt;&quot;)
        sys.exit(1)

    bin_path = sys.argv[1]
    so_path  = sys.argv[2]

    # --- Parse bytecode ---
    data = load_bytecode(bin_path)
    ops  = parse_instructions(data)
    print(f&quot;[*] Loaded {len(data)} bytes -&amp;gt; {len(ops)} instructions&quot;)

    # --- Extract constraints ---
    block_ranges  = find_block_ranges(ops)
    eq_offsets    = find_constraint_eq_offsets(ops)
    n_constraints = len(eq_offsets)
    per_byte      = extract_per_byte_constraints(ops, block_ranges)
    candidates    = compute_per_byte_candidates(per_byte)
    print(f&quot;[*] Blocks: {len(block_ranges)}, Constraints: {n_constraints}, Per-byte: {len(per_byte)}&quot;)

    # --- Per-position candidate summary ---
    print(f&quot;\n[*] Per-position candidates:&quot;)
    fixed = 0
    for pos in range(len(FLAG_PREFIX), FLAG_LEN - 1):
        cands = candidates[pos]
        if len(cands) == 1:
            print(f&quot;    pos {pos:&amp;gt;2}: FIXED -&amp;gt; &apos;{chr(cands[0])}&apos;&quot;)
            fixed += 1
        elif len(cands) &amp;lt;= 10:
            cand_str = &apos; &apos;.join(f&quot;&apos;{chr(c)}&apos;&quot; for c in cands)
            print(f&quot;    pos {pos:&amp;gt;2}: [{len(cands):&amp;gt;2}] {cand_str}&quot;)
        else:
            print(f&quot;    pos {pos:&amp;gt;2}: [{len(cands):&amp;gt;2}] (unconstrained)&quot;)
    n_search = (FLAG_LEN - 1) - len(FLAG_PREFIX)
    print(f&quot;    Fixed: {fixed}, Need search: {n_search - fixed}&quot;)

    # --- Init C VM ---
    vm = CVM(so_path, data, eq_offsets)

    # --- Phase 1: Simulated Annealing ---
    print(f&quot;\n[*] Phase 1: Simulated Annealing&quot;)
    random.seed(42)
    flag, found = solve_sa_with_restarts(vm, candidates, n_constraints,
                                          n_restarts=10, iter_per_restart=2000000)
    if found:
        print(f&quot;\n{&apos;=&apos;*60}\n  FLAG: {&apos;&apos;.join(chr(b) for b in flag)}\n{&apos;=&apos;*60}&quot;)
        return

    # --- Phase 2: Coordinate Descent polish ---
    print(f&quot;\n[*] Phase 2: Coordinate Descent&quot;)
    flag, found = solve_coordinate_descent(vm, candidates, n_constraints)
    if found:
        print(f&quot;\n{&apos;=&apos;*60}\n  FLAG: {&apos;&apos;.join(chr(b) for b in flag)}\n{&apos;=&apos;*60}&quot;)
        return

    _, best_score = vm.run_quick(flag)
    print(f&quot;\n[-] Not solved. Best: {best_score}/{n_constraints}&quot;)
    print(f&quot;    {&apos;&apos;.join(chr(b) for b in flag)}&quot;)

if __name__ == &quot;__main__&quot;:
    t0 = time.time()
    main()
    print(f&quot;\n[*] Elapsed: {time.time()-t0:.1f}s&quot;)

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vm_emulator.c&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/*
 * DWARF Expression VM  –  C accelerator for the solver
 *
 * Exports:
 *   setup_constraints(offsets, n)  – register &apos;eq&apos; bytecode offsets
 *   vm_run_quick(data, bc_len, flag, flag_len, r8, out_result)
 *       -&amp;gt; returns total constraint passes; *out_result = final TOS
 *
 * Build:
 *   gcc -O3 -shared -fPIC -o vm_fast2.so vm_fast2.c
 */

#include &amp;lt;stdint.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

/* ── Tunables ─────────────────────────────────────── */
#define MAX_STACK       4096
#define MAX_STEPS       2000000
#define MAX_CONSTRAINTS 256
#define MEM_SIZE        128

/* ── Helpers ──────────────────────────────────────── */

static inline int64_t to_signed(uint64_t v) { return (int64_t)v; }

static int read_uleb128(const uint8_t *d, int len, int p, uint64_t *out) {
    uint64_t r = 0; int s = 0;
    while (p &amp;lt; len) {
        uint8_t b = d[p++];
        r |= (uint64_t)(b &amp;amp; 0x7f) &amp;lt;&amp;lt; s; s += 7;
        if (!(b &amp;amp; 0x80)) break;
    }
    *out = r; return p;
}

static int read_sleb128(const uint8_t *d, int len, int p, int64_t *out) {
    int64_t r = 0; int s = 0; uint8_t b = 0;
    while (p &amp;lt; len) {
        b = d[p++];
        r |= (int64_t)(b &amp;amp; 0x7f) &amp;lt;&amp;lt; s; s += 7;
        if (!(b &amp;amp; 0x80)) break;
    }
    if (s &amp;lt; 64 &amp;amp;&amp;amp; (b &amp;amp; 0x40)) r |= -(1LL &amp;lt;&amp;lt; s);
    *out = r; return p;
}

/* ── Constraint offset -&amp;gt; index map ───────────────── */

static int con_map[16384];   /* offset -&amp;gt; constraint index (-1 = none) */
static int n_con = 0;

void setup_constraints(const int *offsets, int n) {
    n_con = n;
    memset(con_map, 0xFF, sizeof(con_map));
    for (int i = 0; i &amp;lt; n &amp;amp;&amp;amp; i &amp;lt; MAX_CONSTRAINTS; i++)
        if (offsets[i] &amp;gt;= 0 &amp;amp;&amp;amp; offsets[i] &amp;lt; 16384)
            con_map[offsets[i]] = i;
}

/* ── Flat memory (flag bytes mapped at flag_ptr) ──── */

static uint8_t mem[MEM_SIZE];

static uint64_t mem_read_u64(uint64_t addr, uint64_t base) {
    uint64_t v = 0;
    for (int i = 0; i &amp;lt; 8; i++) {
        int idx = (int)(addr + i - base);
        uint8_t b = (idx &amp;gt;= 0 &amp;amp;&amp;amp; idx &amp;lt; MEM_SIZE) ? mem[idx] : 0;
        v |= (uint64_t)b &amp;lt;&amp;lt; (i * 8);
    }
    return v;
}

/* ── VM execution ─────────────────────────────────── */

int vm_run_quick(const uint8_t *data, int bc_len,
                 const uint8_t *flag, int flag_len,
                 uint64_t r8, int *out_result)
{
    const uint64_t flag_ptr = 0x400000ULL;

    /* Load flag into flat memory */
    memset(mem, 0, MEM_SIZE);
    for (int i = 0; i &amp;lt; flag_len &amp;amp;&amp;amp; i &amp;lt; MEM_SIZE; i++)
        mem[i] = flag[i];

    uint64_t stk[MAX_STACK];
    int sp = 0;
    stk[sp++] = flag_ptr;            /* initial TOS = pointer to flag */
    int pos = 9;                      /* skip 9-byte prologue */
    int steps = 0, passes = 0;

    while (pos &amp;lt; bc_len) {
        if (++steps &amp;gt; MAX_STEPS) { *out_result = -1; return passes; }

        int pc = pos;
        uint8_t op = data[pos++];

        /* ── Compact opcode ranges ── */
        if (op &amp;gt;= 0x30 &amp;amp;&amp;amp; op &amp;lt;= 0x4f) { stk[sp++] = (uint64_t)(op - 0x30);  continue; }  /* lit0..lit31  */
        if (op &amp;gt;= 0x50 &amp;amp;&amp;amp; op &amp;lt;= 0x6f) { stk[sp++] = ((op-0x50)==8) ? r8 : 0; continue; }  /* reg0..reg31  */
        if (op &amp;gt;= 0x70 &amp;amp;&amp;amp; op &amp;lt;= 0x8f) {                                                     /* breg0..breg31 */
            int64_t dummy; pos = read_sleb128(data, bc_len, pos, &amp;amp;dummy);
            stk[sp++] = 0; continue;
        }

        switch (op) {

        /* ── Memory / constants ── */
        case 0x06: { uint64_t a = stk[--sp]; stk[sp++] = mem_read_u64(a, flag_ptr); break; }        /* deref    */
        case 0x08: { stk[sp++] = data[pos++]; break; }                                               /* const1u  */
        case 0x09: { int8_t v = (int8_t)data[pos++]; stk[sp++] = (uint64_t)(int64_t)v; break; }     /* const1s  */
        case 0x0a: { uint16_t v = data[pos]|((uint16_t)data[pos+1]&amp;lt;&amp;lt;8); pos+=2; stk[sp++]=v; break; }/* const2u */
        case 0x0e: { uint64_t v=0; for(int i=0;i&amp;lt;8;i++) v|=(uint64_t)data[pos+i]&amp;lt;&amp;lt;(i*8); pos+=8; stk[sp++]=v; break; } /* const8u */
        case 0x10: { uint64_t v; pos=read_uleb128(data,bc_len,pos,&amp;amp;v); stk[sp++]=v; break; }        /* constu   */

        /* ── Stack manipulation ── */
        case 0x12: stk[sp]=stk[sp-1]; sp++; break;                                                   /* dup   */
        case 0x13: sp--; break;                                                                       /* drop  */
        case 0x14: stk[sp]=stk[sp-2]; sp++; break;                                                   /* over  */
        case 0x15: { uint8_t i=data[pos++]; stk[sp]=stk[sp-1-i]; sp++; break; }                     /* pick  */
        case 0x16: { uint64_t t=stk[sp-1]; stk[sp-1]=stk[sp-2]; stk[sp-2]=t; break; }              /* swap  */
        case 0x17: { uint64_t a=stk[sp-1],b=stk[sp-2],c=stk[sp-3];                                 /* rot   */
                     stk[sp-1]=b; stk[sp-2]=c; stk[sp-3]=a; break; }

        /* ── Arithmetic ── */
        case 0x1a: { uint64_t b=stk[--sp],a=stk[--sp]; stk[sp++]=a&amp;amp;b; break; }                     /* and   */
        case 0x1b: { uint64_t b=stk[--sp],a=stk[--sp]; int64_t sb=to_signed(b);                    /* div   */
                     stk[sp++]=sb?(uint64_t)(to_signed(a)/sb):0; break; }
        case 0x1c: { uint64_t b=stk[--sp],a=stk[--sp]; stk[sp++]=a-b; break; }                     /* minus */
        case 0x1d: { uint64_t b=stk[--sp],a=stk[--sp]; stk[sp++]=b?a%b:0; break; }                 /* mod   */
        case 0x1e: { uint64_t b=stk[--sp],a=stk[--sp]; stk[sp++]=a*b; break; }                     /* mul   */
        case 0x1f: { uint64_t a=stk[--sp]; stk[sp++]=(uint64_t)(-to_signed(a)); break; }           /* neg   */
        case 0x20: { uint64_t a=stk[--sp]; stk[sp++]=~a; break; }                                   /* not   */
        case 0x21: { uint64_t b=stk[--sp],a=stk[--sp]; stk[sp++]=a|b; break; }                     /* or    */
        case 0x22: { uint64_t b=stk[--sp],a=stk[--sp]; stk[sp++]=a+b; break; }                     /* plus  */
        case 0x23: { uint64_t v; pos=read_uleb128(data,bc_len,pos,&amp;amp;v);                              /* plus_uconst */
                     stk[sp-1]+=v; break; }
        case 0x24: { uint64_t b=stk[--sp],a=stk[--sp]; stk[sp++]=a&amp;lt;&amp;lt;(b&amp;amp;63); break; }               /* shl   */
        case 0x25: { uint64_t b=stk[--sp],a=stk[--sp]; stk[sp++]=a&amp;gt;&amp;gt;(b&amp;amp;63); break; }               /* shr   */
        case 0x26: { uint64_t b=stk[--sp],a=stk[--sp];                                              /* shra  */
                     stk[sp++]=(uint64_t)(to_signed(a)&amp;gt;&amp;gt;(b&amp;amp;63)); break; }
        case 0x27: { uint64_t b=stk[--sp],a=stk[--sp]; stk[sp++]=a^b; break; }                     /* xor   */

        /* ── Branching ── */
        case 0x28: { int16_t off=(int16_t)(data[pos]|((uint16_t)data[pos+1]&amp;lt;&amp;lt;8)); pos+=2;           /* bra   */
                     if(stk[--sp]) pos+=off; break; }
        case 0x2f: { int16_t off=(int16_t)(data[pos]|((uint16_t)data[pos+1]&amp;lt;&amp;lt;8)); pos+=2;           /* skip  */
                     pos+=off; break; }

        /* ── Comparisons ── */
        case 0x29: { uint64_t b=stk[--sp],a=stk[--sp];                                              /* eq — constraint check */
                     int ok=(a==b); stk[sp++]=ok;
                     if(pc&amp;lt;16384 &amp;amp;&amp;amp; con_map[pc]&amp;gt;=0 &amp;amp;&amp;amp; ok) passes++;
                     break; }
        case 0x2a: { uint64_t b=stk[--sp],a=stk[--sp]; stk[sp++]=(to_signed(a)&amp;gt;=to_signed(b)); break; } /* ge */
        case 0x2b: { uint64_t b=stk[--sp],a=stk[--sp]; stk[sp++]=(to_signed(a)&amp;gt; to_signed(b)); break; } /* gt */
        case 0x2c: { uint64_t b=stk[--sp],a=stk[--sp]; stk[sp++]=(to_signed(a)&amp;lt;=to_signed(b)); break; } /* le */
        case 0x2d: { uint64_t b=stk[--sp],a=stk[--sp]; stk[sp++]=(to_signed(a)&amp;lt; to_signed(b)); break; } /* lt */
        case 0x2e: { uint64_t b=stk[--sp],a=stk[--sp]; stk[sp++]=(a!=b); break; }                       /* ne */

        /* ── Register extended ── */
        case 0x90: { uint64_t v; pos=read_uleb128(data,bc_len,pos,&amp;amp;v);                              /* regx  */
                     if      (v==8)  stk[sp++]=r8;
                     else if (v==12) stk[sp++]=0x3E06666E84F11F86ULL;   /* r12 constant */
                     else if (v==13) stk[sp++]=0x5155422E88E856B5ULL;   /* r13 constant */
                     else stk[sp++]=0; break; }

        /* ── GNU encoded address ── */
        case 0xf1: { uint8_t enc=data[pos++]; uint8_t fmt=enc&amp;amp;0x0f; uint8_t mod=enc&amp;amp;0x70;
                     uint64_t v=0;
                     if      (fmt==0x01) { pos=read_uleb128(data,bc_len,pos,&amp;amp;v); }
                     else if (fmt==0x02) { v=data[pos]|((uint64_t)data[pos+1]&amp;lt;&amp;lt;8); pos+=2; }
                     else if (fmt==0x03) { for(int i=0;i&amp;lt;4;i++) v|=(uint64_t)data[pos+i]&amp;lt;&amp;lt;(i*8); pos+=4; }
                     else if (fmt==0x04) { for(int i=0;i&amp;lt;8;i++) v|=(uint64_t)data[pos+i]&amp;lt;&amp;lt;(i*8); pos+=8; }
                     stk[sp++] = (mod==0x40) ? 0x3cc20ULL+v : v;       /* funcrel -&amp;gt; base+v */
                     break; }

        case 0x00: break;                                                                             /* nop  */
        default:   *out_result = -2; return passes;
        }

        if (sp &amp;lt; 0 || sp &amp;gt;= MAX_STACK) { *out_result = -3; return passes; }
    }

    *out_result = (sp &amp;gt; 0) ? (int)stk[sp-1] : -4;
    return passes;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;최종적으로 정리하면, 풀이에 쓰인 코드는 크게 네가지 아래와 같다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dwarf_decoder.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DWARF 표현식 디스어셈블러 (GNU 확장 지원 DW_OP_* 디코더)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;solve.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;메인 솔버: 바이트코드 파싱 + 제약 조건 추출 + C VM 인터페이스 + SA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vm_emulator.c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;C VM 가속기 (libgcc &lt;code&gt;execute_stack_op&lt;/code&gt; 재구현)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reflection_dump.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;자동 표현식 추출을 위한 GDB Python 스크립트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Flag&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;./solve.py dwarf_vm.bin vm.so    
[*] Loaded 12888 bytes -&amp;gt; 9195 instructions
[*] Blocks: 38, Constraints: 152, Per-byte: 31

[*] Per-position candidates:
    pos  8: FIXED -&amp;gt; &apos;k&apos;
    pos  9: [ 4] &apos;5&apos; &apos;e&apos; &apos;i&apos; &apos;j&apos;
    pos 10: FIXED -&amp;gt; &apos;_&apos;
    pos 11: [95] (unconstrained)
    pos 12: [95] (unconstrained)
    pos 13: [ 4] &apos;5&apos; &apos;e&apos; &apos;i&apos; &apos;j&apos;
    pos 14: [ 3] &apos;;&apos; &apos;s&apos; &apos;v&apos;
    pos 15: [95] (unconstrained)
    pos 16: [95] (unconstrained)
    pos 17: [ 3] &apos;;&apos; &apos;s&apos; &apos;v&apos;
    pos 18: FIXED -&amp;gt; &apos;_&apos;
    pos 19: [ 7] &apos;1&apos; &apos;2&apos; &apos;4&apos; &apos;a&apos; &apos;b&apos; &apos;d&apos; &apos;h&apos;
    pos 20: [ 5] &apos;3&apos; &apos;6&apos; &apos;c&apos; &apos;f&apos; &apos;l&apos;
    pos 21: [ 5] &apos;9&apos; &apos;:&apos; &apos;q&apos; &apos;r&apos; &apos;t&apos;
    pos 22: FIXED -&amp;gt; &apos;u&apos;
    pos 23: [ 7] &apos;1&apos; &apos;2&apos; &apos;4&apos; &apos;a&apos; &apos;b&apos; &apos;d&apos; &apos;h&apos;
    pos 24: [ 5] &apos;3&apos; &apos;6&apos; &apos;c&apos; &apos;f&apos; &apos;l&apos;
    pos 25: [ 5] &apos;3&apos; &apos;6&apos; &apos;c&apos; &apos;f&apos; &apos;l&apos;
    pos 26: [ 3] &apos;=&apos; &apos;y&apos; &apos;z&apos;
    pos 27: FIXED -&amp;gt; &apos;_&apos;
    pos 28: [ 7] &apos;1&apos; &apos;2&apos; &apos;4&apos; &apos;a&apos; &apos;b&apos; &apos;d&apos; &apos;h&apos;
    pos 29: FIXED -&amp;gt; &apos;_&apos;
    pos 30: [ 2] &apos;8&apos; &apos;p&apos;
    pos 31: FIXED -&amp;gt; &apos;u&apos;
    pos 32: FIXED -&amp;gt; &apos;m&apos;
    pos 33: [ 7] &apos;1&apos; &apos;2&apos; &apos;4&apos; &apos;a&apos; &apos;b&apos; &apos;d&apos; &apos;h&apos;
    pos 34: [ 4] &apos;5&apos; &apos;e&apos; &apos;i&apos; &apos;j&apos;
    pos 35: [ 5] &apos;9&apos; &apos;:&apos; &apos;q&apos; &apos;r&apos; &apos;t&apos;
    pos 36: [95] (unconstrained)
    pos 37: [95] (unconstrained)
    pos 38: [ 4] &apos;5&apos; &apos;e&apos; &apos;i&apos; &apos;j&apos;
    pos 39: [ 5] &apos;3&apos; &apos;6&apos; &apos;c&apos; &apos;f&apos; &apos;l&apos;
    pos 40: [ 4] &apos;5&apos; &apos;e&apos; &apos;i&apos; &apos;j&apos;
    pos 41: [ 5] &apos;9&apos; &apos;:&apos; &apos;q&apos; &apos;r&apos; &apos;t&apos;
    pos 42: [ 4] &apos;5&apos; &apos;e&apos; &apos;i&apos; &apos;j&apos;
    pos 43: [95] (unconstrained)
    pos 44: [ 5] &apos;3&apos; &apos;6&apos; &apos;c&apos; &apos;f&apos; &apos;l&apos;
    pos 45: [ 4] &apos;5&apos; &apos;e&apos; &apos;i&apos; &apos;j&apos;
    Fixed: 8, Need search: 30

[*] Phase 1: Simulated Annealing

  --- SA restart 1/10 ---
  [SA 300K/2000K] T=1.627 best=130/152 flag=lactf{siki_&amp;lt;lj;Qms_hltublfz_a_8um2e:G*e6ere43e}
  [SA 500K/2000K] T=0.682 best=143/152 flag=lactf{sike_this_es_actu1lly_a_pumb5rWBef5:5.c5}
  [SA 600K/2000K] T=0.441 best=146/152 flag=lactf{sike_tiesSa;_af9udlly_a_pumber_re6e:5n3e}
  [SA 900K/2000K] T=0.120 best=147/152 flag=lactf{sike_tae;[a;_af9udlly_a_pumber_re6e:enc5}
  [SA 1100K/2000K] T=0.050 best=147/152 flag=lactf{sike_tae;[a;_af9udlly_a_pumber_re6e:enc5}
  [SA 1200K/2000K] T=0.032 best=147/152 flag=lactf{sike_tae;[a;_af9udlly_a_pumber_re6e:enc5}
  [SA 1300K/2000K] T=0.021 best=147/152 flag=lactf{sike_tae;[a;_af9udlly_a_pumber_re6e:enc5}
  [SA 1400K/2000K] T=0.014 best=147/152 flag=lactf{sike_tae;[a;_af9udlly_a_pumber_re6e:enc5}
  [SA 1600K/2000K] T=0.006 best=147/152 flag=lactf{sike_tae;[a;_af9udlly_a_pumber_re6e:enc5}
  [SA 1700K/2000K] T=0.004 best=147/152 flag=lactf{sike_tae;[a;_af9udlly_a_pumber_re6e:enc5}
  [SA 1800K/2000K] T=0.002 best=147/152 flag=lactf{sike_tae;[a;_af9udlly_a_pumber_re6e:enc5}
  [SA 1900K/2000K] T=0.002 best=147/152 flag=lactf{sike_tae;[a;_af9udlly_a_pumber_re6e:enc5}
  [SA restart 1] score=147/152 (best=147) flag=lactf{sike_tae;[a;_af9udlly_a_pumber_re6e:enc5}

  --- SA restart 2/10 ---
  [SA 100K/2000K] T=3.884 best=125/152 flag=lactf{siki_Qhe;;B;_b39u16fy_h_8umb5q[ej6et56le}
  [SA 200K/2000K] T=2.514 best=125/152 flag=lactf{siki_Qhe;;B;_b39u16fy_h_8umb5q[ej6et56le}
  [SA 300K/2000K] T=1.627 best=130/152 flag=lactf{sike_de5s_hs_h39u1fly_h_pumb5:[&amp;gt;j6itja6j}
  [SA 400K/2000K] T=1.053 best=136/152 flag=lactf{sik5_tai;[%s_aftual6y_a_puma5:Wb56e:e.ce}
  [SA iter 535145] FOUND! score=152/152

============================================================
  FLAG: lactf{sike_this_is_actually_a_pumber_reference}
============================================================
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;최종 검증을 위해 바이너리에 플래그를 넣으면…&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/reflection/4.png&quot; alt=&quot;4.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;catch_unwind 핸들러가 panic을 받고, expression이 0이 아닌 값을 반환하여 프로그램이 정상 종료된다!&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 실행 흐름&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;./reflection &quot;lactf{...}&quot;
│
├─ main() → catch_unwind 설정
│    └─ __rust_begin_short_backtrace()
│         └─ reflection::main()
│              └─ panic!(&quot;Come close to me and I&apos;ll make you regret it.&quot;)
│                   └─ panic_fmt() → panic_with_hook() → rust_panic()
│                        └─ _Unwind_RaiseException() — 여기서 검증 시작
│
├─ [Phase 1] 검색: 프레임 순회, main()의 catch_unwind 핸들러 발견
│    └─ 표현식이 실행되지만 컨텍스트는 이후 리셋됨
│
├─ [Phase 2] 정리: 프레임을 다시 순회, DWARF 표현식 실행
│    ├─ ❶ panic_with_hook FDE → RCX = 0x53AA9 (ARGV 전역 변수 근처)
│    ├─ ❷ panic_handler FDE → R8 = 코드 영역 주소 (해시 값)
│    ├─ ❸ __rust_begin_short_backtrace FDE → 115B 표현식:
│    │      argc==2? strlen==47? hash(first8)? last==&apos;}&apos;?
│    │      ├─ 통과 → RIP = 0x3CC21 (가상 프레임 진입)
│    │      └─ 실패 → RIP = 정상 복귀 (검증 건너뜀)
│    ├─ ❹ 가상 프레임 FDE → R12 = 0x3E06666E84F11F86 (상수 키)
│    ├─ ❺ 가상 프레임 FDE → R13 = 0x5155422E88E856B5 (상수 키)
│    └─ ❻ 가상 프레임 FDE → 12,888B 표현식:
│           38개 블록이 비트 그룹 시그니처로 나머지 문자 검증
│           ├─ 모두 통과 → VM이 0이 아닌 값 반환 → RIP = 성공 경로
│           └─ 하나라도 실패 → VM이 0 반환 → RIP = 실패 경로
│
└─ catch_unwind가 결과를 받음 → 프로그램 종료
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unwinder는 CFI를 신뢰해 레지스터를 복원하는데, 바로 이 점이 이러한 조작을 가능하게 한다. 와!&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;7. Conclusion&lt;/h2&gt;
&lt;p&gt;요즘 많은 문제들이 LLM으로 쉽게 풀리다 보니 허무할 때도 있는데, Anti-LLM 문자열(?)이 들어있는 리버싱 문제는 처음이라 신박했다. (&lt;s&gt;내가 먼저 써먹으려고 했는데!&lt;/s&gt;)&lt;/p&gt;
&lt;p&gt;아무튼 재미있었다.&lt;/p&gt;
</content:encoded></item><item><title>0 CTF 2025 Writeup (1Zwasm)</title><link>https://ma4the.github.io/posts/rev-1zwasm-0ctf2026/</link><guid isPermaLink="true">https://ma4the.github.io/posts/rev-1zwasm-0ctf2026/</guid><description>Is WASM a good language?</description><pubDate>Mon, 05 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Challenges: https://github.com/sajjadium/ctf-archives/tree/main/ctfs/0CTF/2025/rev/1Zwasm&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;1. Challenge Overview&lt;/h2&gt;
&lt;p&gt;우선 문제 바이너리를 열어보니 UPX로 패킹돼있었고, 툴로 언패킹이 불가능했다.&lt;/p&gt;
&lt;p&gt;GDB로 붙어봤는데 이번엔 디버깅이 안 됐다. &lt;code&gt;set follow-fork-mode child&lt;/code&gt; 명령어를 써서 트레이싱 해봤더니, &lt;code&gt;memfd_create()&lt;/code&gt; 시스콜이 호출됨을 알 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1ZWasm/1.png&quot; alt=&quot;1.png&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(gdb) set follow-fork-mode child
(gdb) r
Starting program: /home/noonchi/ctf/1Zwαsm

Catchpoint 1 (call to syscall memfd_create), 0x00007ffff7ffe3c7 in ?? ()
(gdb) c
Continuing.

...

[Attaching after process 108066 fork to child process 108619]
[New inferior 2 (process 108619)]
process 108619 is executing new program: /proc/108619/exe
Enter the flag:hey

:(
[Inferior 2 (process 108619) exited normally]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;2. 바이너리 언패킹 및 덤프&lt;/h2&gt;
&lt;p&gt;UPX 패커가 실행할 때 &lt;code&gt;memfd_create()&lt;/code&gt; 시스콜로 파일 두 개를 생성하고 있어서 런타임에 덤프를 떴다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pgrep -f 1Zw | xargs -I{} sh -c &apos;echo &quot;=== PID {} ===&quot; &amp;amp;&amp;amp; ls -la /proc/{}/fd/ 2&amp;gt;/dev/null&apos;
=== PID 65619 ===
합계 0
dr-x------ 2 noonchi noonchi 5 12월 20 15:39 .
dr-xr-xr-x 9 noonchi noonchi 0 12월 20 15:30 ..
lrwx------ 1 noonchi noonchi 64 12월 20 15:39 0 -&amp;gt; /dev/pts/16
lrwx------ 1 noonchi noonchi 64 12월 20 15:39 1 -&amp;gt; /dev/pts/16
lrwx------ 1 noonchi noonchi 64 12월 20 15:39 2 -&amp;gt; /dev/pts/16
lrwx------ 1 noonchi noonchi 64 12월 20 15:39 3 -&amp;gt; &apos;/memfd:chall (deleted)&apos;
lrwx------ 1 noonchi noonchi 64 12월 20 15:39 4 -&amp;gt; &apos;/memfd:chall_wasm (deleted)&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;두 개의 파일이 메모리에 올라와 있다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;파일 1. &lt;code&gt;chall&lt;/code&gt;&lt;/strong&gt;: toywasm 런타임 (WASM 인터프리터)
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cat /proc/65619/fd/3 &amp;gt; chall.bin&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;파일 2. &lt;code&gt;chall_wasm&lt;/code&gt;&lt;/strong&gt;: Rust로 작성된 WASM 모듈
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cat /proc/65619/fd/4 &amp;gt; chall_wasm.bin&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;chall&lt;/code&gt;은 디컴파일러로 열면 UPX로 패킹돼있다고 뜨고, &lt;code&gt;chall_wasm&lt;/code&gt;은 WASM 바이너리로 인식은 되지만 코드가 깨져 보인다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;3. WASM 바이너리 분석&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;chall_wasm&lt;/code&gt; 바이너리를 &lt;code&gt;wasm-objdump&lt;/code&gt;로 보면, &lt;code&gt;END&lt;/code&gt;를 비롯한 여러 opcode가 손상된 것을 알 수 있다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wasm-objdump -h chall_wasm.bin

chall_wasm.bin:       file format wasm 0x1
0000253: error: init expression must end with END opcode
000028b: error: unfinished section (expected end: 0x2e1)
000e8f2: error: ref.null type must be a reference type
module name: &amp;lt;chall-7a8478e4c9508fe0.wasm&amp;gt;

Sections:

     Type start=0x0000000b end=0x0000008c (size=0x00000081) count: 18
   Import start=0x0000008f end=0x00000146 (size=0x000000b7) count: 5
 Function start=0x00000149 end=0x00000237 (size=0x000000ee) count: 236
    Table start=0x00000239 end=0x0000023e (size=0x00000005) count: 1
   Memory start=0x00000240 end=0x00000243 (size=0x00000003) count: 1
   Global start=0x00000245 end=0x00000253 (size=0x0000000e) count: 2
   Export start=0x00000255 end=0x00000276 (size=0x00000021) count: 3
     Elem start=0x00000278 end=0x000002e1 (size=0x00000069) count: 1
     Code start=0x000002e5 end=0x0000e253 (size=0x0000df6e) count: 236
     Data start=0x0000e256 end=0x00011490 (size=0x0000323a) count: 2
   Custom start=0x00011493 end=0x00011537 (size=0x000000a4) &quot;target_features&quot;
   Custom start=0x0001153a end=0x000153f7 (size=0x00003ebd) &quot;name&quot;
   Custom start=0x000153fa end=0x000154ba (size=0x000000c0) &quot;producers&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;마찬가지로 &lt;code&gt;wasm-objdump&lt;/code&gt;로 확인한 함수 심볼 등으로 추측컨대, 전체 흐름이 대충 **&quot;입력 문자[i] → apply_sbox_layer(???) → SHA1 → 타겟 해시[i]와 비교&quot;**이고, 구체적인 로직을 알아내서 역연산을 해야 한다는 걸 알 수 있다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;함수&lt;/th&gt;
&lt;th&gt;이름&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;func[6]&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;EntryPoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;func[11]&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chall::main&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;메인 함수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;func[12]&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chall::crypto::apply_sbox_layer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;S-box 치환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;func[23]&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sha1::compress::compress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SHA1 압축&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;..&lt;/td&gt;
&lt;td&gt;등등...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;추가로 데이터 세그먼트에서 정적 데이터를 추출했다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;플래그 포맷&lt;/strong&gt;: &lt;code&gt;0ops{...}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;타겟 해시 테이블&lt;/strong&gt;: 해시 40개&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;S-box 테이블&lt;/strong&gt;: 16바이트
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0e 04 0d 01 02 0f 0b 08 03 0a 06 0c 05 09 00 07&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;4. toywasm 런타임 식별&lt;/h2&gt;
&lt;p&gt;덤프 뜬 &lt;code&gt;chall&lt;/code&gt;을 실행해보니 &lt;a href=&quot;https://github.com/yamt/toywasm&quot;&gt;toywasm&lt;/a&gt;이 원형임을 알 수 있었다.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./chall
Usage:
        toywasm [OPTIONS] [--] &amp;lt;MODULE&amp;gt; [WASI-ARGS...]
Options:
        --allow-unresolved-functions
        --disable-jump-table
        --disable-localtype-cellidx
        --disable-resulttype-cellidx
        --invoke FUNCTION[ FUNCTION_ARGS...]
        --load MODULE_PATH
        --max-frames NUMBER_OF_FRAMES
        --max-memory MEMORY_LIMIT_IN_BYTES
        --max-stack-cells NUMBER_OF_CELLS
        --repl
        --repl-prompt STRING
        --print-build-options
        --print-stats
        --timeout TIMEOUT_MS
        --version
        --wasi
        --wasi-dir HOST_DIR[::GUEST_DIR]
        --wasi-env NAME=VAR
Examples:
        Run a wasi module
                toywasm --wasi module
        Load a module and invoke its function
                toywasm --load module --invoke &quot;func arg1 arg2&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;원본 toywasm의 오픈소스 코드를 뜯어보니, opcode를 테이블로 해석하고 각 instruction handler로 넘기는 파싱 로직이 보였다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1ZWasm/2.png&quot; alt=&quot;2.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;toywasm &lt;code&gt;expr.c&lt;/code&gt; — opcode 디스패치 테이블&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;이쯤에서 이 문제는 &lt;strong&gt;커스텀 opcode를 정의한 VM-like 문제&lt;/strong&gt;가 아닐까 하는 생각을 했다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 커스텀 Opcode 가설&lt;/h2&gt;
&lt;h3&gt;근거 1. 모든 236개 함수가 &lt;code&gt;0x55&lt;/code&gt;로 끝남&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Code 섹션을 WASM 포맷대로 파싱해서 각 함수 body의 마지막 1바이트 값을 확인해본 결과 &lt;strong&gt;모두 &lt;code&gt;0x55&lt;/code&gt;로 끝남&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;표준 WASM에서는 &lt;code&gt;0x0b&lt;/code&gt; (END)로 끝나야 하기 때문에, &lt;code&gt;0x55 → 0x0b&lt;/code&gt; 이렇게 치환된 게 아닐까 추측&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;근거 2. 바이트 빈도 분포가 비정상적임&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0x38&lt;/code&gt;: 7193회 (원래 &lt;code&gt;f32.store&lt;/code&gt;인데 이렇게 많을 수 없음)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x45&lt;/code&gt;: 4441회 (원래 &lt;code&gt;i32.eqz&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x55&lt;/code&gt;: 1459회 (원래 &lt;code&gt;i64.gt_u&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x0b&lt;/code&gt;: 227회 (원래 &lt;code&gt;end&lt;/code&gt;인데 너무 적음!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기서 명백한 사실은, &lt;strong&gt;커스텀 인터프리터는 커스텀 WASM 바이너리의 바이트코드를 올바르게 해석하고 실행하고 있다&lt;/strong&gt;는 것이다.&lt;/p&gt;
&lt;p&gt;즉, 멋대로 뒤섞인 opcode를 표준 opcode 테이블로 디스어셈블하면 니모닉이 틀리는 게 당연하고, 디스패처 역할을 하는 함수를 찾고 매핑 테이블을 알아내서 패치만 해주면 일반 디컴파일러로도 해석이 가능해질 것이다.&lt;/p&gt;
&lt;p&gt;이 가설을 세우고 언패킹된 toywasm을 덤프 떠서 실제 사용되는 opcode 핸들러를 추적하고자 했다.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 코어 덤프를 통한 Opcode 매핑 추출&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;./chall --wasi ./chall_wasm&lt;/code&gt; 으로 실행한 뒤 프로세스 메모리 맵을 확인:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat /proc/16222/maps
555576ff5000-555577016000 rw-p 00000000 00:00 0                          [heap]
7cf924fec000-7cf9250fc000 rw-p 00000000 00:00 0
7cf925200000-7cf925228000 r--p 00000000 fc:00 2118957                    /usr/lib/x86_64-linux-gnu/libc.so.6
7cf925228000-7cf9253b0000 r-xp 00028000 fc:00 2118957                    /usr/lib/x86_64-linux-gnu/libc.so.6
...
7cf925572000-7cf9255b3000 r-xs 00000000 00:01 6031                       /memfd:upx (deleted)
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;앞서 말했듯 &lt;code&gt;chall&lt;/code&gt;은 UPX로 패킹돼있기 때문에 런타임에 덤프를 떠야 한다. &lt;code&gt;gcore -o core &amp;lt;PID&amp;gt;&lt;/code&gt;로 전체 덤프를 뜨면 대강 이런 구조:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│  core.??? (2.2MB) - 프로세스 메모리 스냅샷
│
│  내용:
│  ├── libc.so.6          (~1.5MB) - 시스템 라이브러리
│  ├── ld-linux.so        (~200KB) - 동적 링커
│  ├── libm.so            (~500KB) - 수학 라이브러리
│  ├── memfd:upx          (266KB) &amp;lt;- 언패킹된 toywasm
│  ├── 힙, 스택
│  └── WASM 메모리
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;바이너리를 분석해 opcode 디스패처 함수를 찾는다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1ZWasm/3.png&quot; alt=&quot;3.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;코어 덤프 뜰 때마다 베이스 주소는 달라질 수 있기 때문에 &lt;code&gt;i64.eqz&lt;/code&gt; 같은 문자열 검색해서 xref를 찾아서 타고 올라가다 보면 찾을 수 있다. (사실 &lt;code&gt;expr.c&lt;/code&gt; 핸들러를 찾는 게 목표니 기존 &lt;code&gt;expr.c&lt;/code&gt; 소스코드에 있던 &lt;code&gt;&quot;Unimplemented instructions&quot;&lt;/code&gt; 같은 문자열 기준으로 찾으면 더 쉽다)&lt;/p&gt;
&lt;p&gt;추출한 opcode 매핑은 아래와 같다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 전체 Opcode 매핑 테이블
# base=0x7cf099725fe0 stride=32
0x00 i64.eqz          0x01 i64.sub          0x02 i64.load16_s
0x03 i64.gt_u         0x04 nop              0x05 i64.const
0x06 select           0x07 else             0x08 i64.load16_u
0x09 i64.ge_u         0x0a i64.load8_u      0x0b f64.lt
0x0c i32.gt_s         0x0d i32.lt_s         0x0e i32.load8_u
0x0f i32.rem_u        0x10 i64.store        0x11 br
0x12 f32.ne           0x13 i64.load8_s      0x14 i32.or
0x15 f32.lt           0x16 i32.add          0x17 i32.clz
0x18 i32.shr_u        0x1a f32.eq           0x1b i64.load32_u
0x1c global.get       0x1d i32.eqz          0x20 select_t
0x21 i32.div_s        0x22 i32.shr_s        0x23 i32.store8
0x24 i32.gt_u         0x25 i32.store        0x26 i64.clz
0x28 local.set        0x29 i64.le_s         0x2a f32.const
0x2b i64.le_u         0x2d f64.ge           0x2e if
0x2f i32.rotl         0x30 i32.eq           0x31 i64.load
0x33 i64.store8       0x34 f32.ge           0x35 i64.eq
0x36 global.set       0x37 i32.xor          0x38 local.get
0x39 drop             0x3a i64.gt_s         0x3d table_set
0x3e call             0x3f memory.size       0x40 f32.gt
0x41 i64.ne           0x42 local.tee        0x43 f64.ne
0x44 f64.store        0x45 i32.const        0x46 i32.ctz
0x47 i32.mul          0x48 i32.shl          0x49 return
0x4a f32.le           0x4c i32.load8_s      0x4d br_if
0x4e i32.popcnt       0x4f i32.load16_u     0x50 i32.sub
0x51 i32.lt_u         0x53 table_get        0x54 br_table
0x55 end              0x56 i64.lt_s         0x57 call_indirect
0x58 i64.lt_u         0x59 i32.load         0x5a i32.load16_s
0x5b block            0x5c unreachable       0x5d i64.mul
0x5e f64.le           0x5f i32.le_u         0x62 f64.const
0x63 f32.store        0x64 f32.load         0x65 f64.eq
0x66 i64.ctz          0x67 i32.ne           0x68 i64.add
0x69 i64.popcnt       0x6a i64.div_s        0x6b i32.ge_u
0x6d i32.rotr         0x6f i32.rem_s        0x70 memory.grow
0x71 i64.store16      0x72 i32.ge_s         0x73 i32.div_u
0x74 i64.store32      0x75 f64.load         0x76 i64.ge_s
0x77 i32.and          0x78 i64.load32_s     0x7a loop
0x7c i32.store16      0x7e f64.gt           0x7f i32.le_s
0x80 ref.null         0x81 fd               0x82 f32.convert_i32_s
0x84 i64.rem_s        0x85 i64.extend8_s    0x86 f64.ceil
0x87 i64.xor          0x88 f32.convert_i32_u 0x8a i64.trunc_f64_u
0x8e i64.extend_i32_u 0x90 f64.convert_i32_s 0x91 f32.neg
0x93 i64.trunc_f64_s  0x99 i64.shr_s        0x9b f32.convert_i64_u
0x9c f64.div          0x9e f32.copysign      0x9f f32.sub
0xa0 f64.floor        0xa3 f64.convert_i64_u 0xa5 f32.demote_f64
0xa7 i64.rotr         0xa8 f64.sub           0xaa i64.extend16_s
0xac f64.trunc        0xae i64.or            0xb1 i32.trunc_f32_u
0xb2 i64.shr_u        0xb3 i32.reinterpret_f32 0xb5 ref.is_null
0xb6 f64.min          0xb8 f64.add           0xb9 f64.neg
0xbb f32.div          0xbc i64.rotl          0xbd f64.copysign
0xbe f32.nearest      0xbf f64.reinterpret_i64 0xc1 ref.func
0xc2 f64.mul          0xc4 f64.convert_i64_s 0xc5 i32.extend16_s
0xc9 f32.trunc        0xcb f32.max           0xcc i64.extend_i32_s
0xcd f32.ceil         0xcf i64.trunc_f32_u   0xd2 f32.add
0xd3 f64.max          0xd4 f32.floor         0xd5 f32.abs
0xd7 f32.sqrt         0xd9 i32.trunc_f32_s   0xda i64.trunc_f32_s
0xdb i64.rem_u        0xdc i64.div_u         0xe0 f64.nearest
0xe1 f32.min          0xe2 i64.extend32_s    0xe3 f64.convert_i32_u
0xe4 i32.trunc_f64_s  0xe8 fc                0xec i64.shl
0xee f64.abs          0xf0 i32.extend8_s     0xf1 i64.reinterpret_f64
0xf4 f32.reinterpret_i32 0xf5 i32.trunc_f64_u 0xf8 f32.convert_i64_s
0xf9 f64.sqrt         0xfa i64.and           0xfb i32.wrap_i64
0xfc f64.promote_f32  0xfd f32.mul
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실제 WASM &lt;a href=&quot;https://pengowray.github.io/wasm-ops/&quot;&gt;표준 테이블&lt;/a&gt;과 비교해보면, 표준 WASM에선 &lt;code&gt;end&lt;/code&gt;가 항상 &lt;code&gt;0x0B&lt;/code&gt;인데 여기선 &lt;code&gt;end&lt;/code&gt;가 &lt;code&gt;0x55&lt;/code&gt;로 매핑돼 있고, 반대로 표준 테이블에서 &lt;code&gt;i64.sub&lt;/code&gt;는 &lt;code&gt;0x7D&lt;/code&gt;인데 추출 테이블에선 &lt;code&gt;0x01&lt;/code&gt;이 &lt;code&gt;i64.sub&lt;/code&gt;로 되어있는 식이다.&lt;/p&gt;
&lt;h3&gt;정리&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;chall&lt;/code&gt;을 실행하면 커스텀된 toywasm이 메모리 상에 풀린 상태로 동작함&lt;/li&gt;
&lt;li&gt;그 상태에서 프로세스 코어덤프를 뜨면, toywasm의 opcode → mnemonic 문자열 포인터 테이블도 같이 메모리에 남음&lt;/li&gt;
&lt;li&gt;core 바이너리 리버싱해서 opcode 핸들러 함수와 매핑 테이블을 찾을 수 있음&lt;/li&gt;
&lt;li&gt;(덤) 테이블 베이스를 잡고 IDA Python으로 0..255를 순회해서 &lt;code&gt;0x?? mnemonic&lt;/code&gt;을 출력하는 식으로 자동화도 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;7. Opcode 패치 및 디컴파일&lt;/h2&gt;
&lt;p&gt;알아낸 매핑대로 WASM 바이너리를 패치했다. 아래는 WASM을 포맷대로 파싱해서 opcode 위치만 치환하는 코드이다:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Opcode 패치 스크립트
import argparse
import os
import re


def uleb(d: bytes, o: int) -&amp;gt; tuple[int, int]:
    r = 0
    s = 0
    i = 0
    while True:
        b = d[o + i]
        r |= (b &amp;amp; 0x7F) &amp;lt;&amp;lt; s
        i += 1
        if b &amp;lt; 0x80:
            return r, i
        s += 7
        if i &amp;gt; 16:
            raise ValueError(&quot;uleb too long&quot;)


def read_uleb_bytes(d: bytes, o: int) -&amp;gt; tuple[bytes, int]:
    i = 0
    while True:
        b = d[o + i]
        i += 1
        if b &amp;lt; 0x80:
            return d[o : o + i], i
        if i &amp;gt; 16:
            raise ValueError(&quot;uleb bytes too long&quot;)


def read_sleb_bytes(d: bytes, o: int) -&amp;gt; tuple[bytes, int]:
    i = 0
    while True:
        b = d[o + i]
        i += 1
        if b &amp;lt; 0x80:
            return d[o : o + i], i
        if i &amp;gt; 16:
            raise ValueError(&quot;sleb bytes too long&quot;)


def encode_uleb(n: int) -&amp;gt; bytes:
    out = bytearray()
    while True:
        b = n &amp;amp; 0x7F
        n &amp;gt;&amp;gt;= 7
        if n:
            out.append(b | 0x80)
        else:
            out.append(b)
            return bytes(out)


def parse_sections(wasm: bytes) -&amp;gt; list[tuple[int, int, int]]:
    if wasm[:4] != b&quot;\0asm&quot;:
        raise ValueError(&quot;not wasm&quot;)
    o = 8
    secs: list[tuple[int, int, int]] = []
    while o &amp;lt; len(wasm):
        sid = wasm[o]
        o += 1
        sz, n = uleb(wasm, o)
        o += n
        secs.append((sid, o, o + sz))
        o += sz
    return secs


def sec_range(secs: list[tuple[int, int, int]], sid: int) -&amp;gt; tuple[int, int] | None:
    for s, a, b in secs:
        if s == sid:
            return a, b
    return None


def load_opdump(path: str) -&amp;gt; dict[int, str]:
    enc2name: dict[int, str] = {}
    with open(path, &quot;r&quot;, encoding=&quot;utf-8&quot;, errors=&quot;ignore&quot;) as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith(&quot;#&quot;):
                continue
            k, name = line.split(None, 1)
            enc2name[int(k, 16)] = name.strip()
    return enc2name


_RE_INSN = re.compile(r&apos;INSTRUCTION\(\s*(0x[0-9a-fA-F]+)\s*,\s*&quot;([^&quot;]+)&quot;&apos;)


def load_std_maps(repo_root: str) -&amp;gt; tuple[dict[str, int], dict[str, int]]:
    def parse_hdr(p: str) -&amp;gt; dict[str, int]:
        m: dict[str, int] = {}
        with open(p, &quot;r&quot;, encoding=&quot;utf-8&quot;, errors=&quot;ignore&quot;) as f:
            for ln in f:
                mo = _RE_INSN.search(ln)
                if mo:
                    m[mo.group(2)] = int(mo.group(1), 0)
        return m

    base_p = os.path.join(repo_root, &quot;toywasm&quot;, &quot;lib&quot;, &quot;insn_list_base.h&quot;)
    fc_p = os.path.join(repo_root, &quot;toywasm&quot;, &quot;lib&quot;, &quot;insn_list_fc.h&quot;)
    if not (os.path.exists(base_p) and os.path.exists(fc_p)):
        raise SystemExit(&quot;need toywasm headers: toywasm/lib/insn_list_base.h + insn_list_fc.h&quot;)

    std = parse_hdr(base_p)
    std_fc = parse_hdr(fc_p)
    if &quot;table.grew&quot; in std_fc:
        std_fc[&quot;table.grow&quot;] = std_fc.pop(&quot;table.grew&quot;)
    return std, std_fc


# encoded 0xfc subopcode -&amp;gt; mnemonic
FC_ENC2NAME: dict[int, str] = {
    7: &quot;i64.trunc_sat_f32_u&quot;,
    39: &quot;memory.copy&quot;,
    85: &quot;memory.fill&quot;,
}


def rewrite_init_expr(buf: bytes, enc2name: dict[int, str], std: dict[str, int]) -&amp;gt; tuple[bytes, int]:
    out = bytearray()
    i = 0
    while i &amp;lt; len(buf):
        op = buf[i]
        i += 1
        name = enc2name.get(op)
        if not name:
            raise ValueError(f&quot;unknown encoded opcode 0x{op:02x} in init-expr&quot;)
        if name in (&quot;fc&quot;, &quot;fd&quot;):
            raise ValueError(&quot;prefix in init-expr not supported&quot;)

        out.append(std[name])
        if name in (&quot;i32.const&quot;, &quot;i64.const&quot;):
            raw, n = read_sleb_bytes(buf, i)
            i += n
            out += raw
        elif name == &quot;f32.const&quot;:
            out += buf[i : i + 4]
            i += 4
        elif name == &quot;f64.const&quot;:
            out += buf[i : i + 8]
            i += 8
        elif name == &quot;global.get&quot;:
            raw, n = read_uleb_bytes(buf, i)
            i += n
            out += raw
        elif name == &quot;ref.null&quot;:
            out.append(buf[i])
            i += 1
        elif name == &quot;ref.func&quot;:
            raw, n = read_uleb_bytes(buf, i)
            i += n
            out += raw

        if name == &quot;end&quot;:
            return bytes(out), i
    raise ValueError(&quot;init-expr didn&apos;t end with end&quot;)


def rewrite_code(code: bytes, enc2name: dict[int, str], std: dict[str, int], std_fc: dict[str, int]) -&amp;gt; bytes:
    out = bytearray()
    i = 0
    while i &amp;lt; len(code):
        op = code[i]
        i += 1
        name = enc2name.get(op)
        if not name:
            raise ValueError(f&quot;unknown encoded opcode 0x{op:02x} at +0x{i-1:x}&quot;)

        if name == &quot;fd&quot;:
            raise ValueError(&quot;0xfd(SIMD) not supported&quot;)

        if name == &quot;fc&quot;:
            enc_sub, _ = uleb(code, i)
            _raw, n = read_uleb_bytes(code, i)
            i += n
            sub_name = FC_ENC2NAME.get(enc_sub)
            if not sub_name:
                raise ValueError(f&quot;unknown encoded 0xfc subopcode {enc_sub}&quot;)
            std_sub = std_fc.get(sub_name)
            if std_sub is None:
                raise ValueError(f&quot;unknown std 0xfc mnemonic {sub_name!r}&quot;)

            out.append(0xFC)
            out += encode_uleb(std_sub)

            # fc immediates
            if 0x00 &amp;lt;= std_sub &amp;lt;= 0x07:
                pass
            elif sub_name == &quot;memory.init&quot;:
                raw, n = read_uleb_bytes(code, i); i += n; out += raw
                raw, n = read_uleb_bytes(code, i); i += n; out += raw
            elif sub_name == &quot;data.drop&quot;:
                raw, n = read_uleb_bytes(code, i); i += n; out += raw
            elif sub_name == &quot;memory.copy&quot;:
                raw, n = read_uleb_bytes(code, i); i += n; out += raw
                raw, n = read_uleb_bytes(code, i); i += n; out += raw
            elif sub_name == &quot;memory.fill&quot;:
                raw, n = read_uleb_bytes(code, i); i += n; out += raw
            elif sub_name == &quot;table.init&quot;:
                raw, n = read_uleb_bytes(code, i); i += n; out += raw
                raw, n = read_uleb_bytes(code, i); i += n; out += raw
            elif sub_name == &quot;elem.drop&quot;:
                raw, n = read_uleb_bytes(code, i); i += n; out += raw
            elif sub_name == &quot;table.copy&quot;:
                raw, n = read_uleb_bytes(code, i); i += n; out += raw
                raw, n = read_uleb_bytes(code, i); i += n; out += raw
            elif sub_name in (&quot;table.grow&quot;, &quot;table.size&quot;, &quot;table.fill&quot;):
                raw, n = read_uleb_bytes(code, i); i += n; out += raw
            else:
                raise ValueError(f&quot;unsupported 0xfc mnemonic {sub_name!r}&quot;)
            continue

        out.append(std[name])

        # immediates: length-only
        if name in (&quot;block&quot;, &quot;loop&quot;, &quot;if&quot;):
            b = code[i]
            if b in {0x40, 0x7F, 0x7E, 0x7D, 0x7C, 0x7B, 0x70, 0x6F}:
                out.append(b); i += 1
            else:
                raw, n = read_sleb_bytes(code, i); i += n; out += raw
        elif name in (&quot;br&quot;, &quot;br_if&quot;, &quot;call&quot;, &quot;local.get&quot;, &quot;local.set&quot;, &quot;local.tee&quot;,
                       &quot;global.get&quot;, &quot;global.set&quot;, &quot;table_get&quot;, &quot;table_set&quot;):
            raw, n = read_uleb_bytes(code, i); i += n; out += raw
        elif name == &quot;call_indirect&quot;:
            raw, n = read_uleb_bytes(code, i); i += n; out += raw
            out.append(code[i]); i += 1
        elif name == &quot;br_table&quot;:
            raw_cnt, n = read_uleb_bytes(code, i)
            cnt, _ = uleb(code, i)
            i += n
            out += raw_cnt
            for _ in range(cnt + 1):
                raw, n = read_uleb_bytes(code, i); i += n; out += raw
        elif name == &quot;select_t&quot;:
            raw_cnt, n = read_uleb_bytes(code, i)
            cnt, _ = uleb(code, i)
            i += n
            out += raw_cnt
            out += code[i : i + cnt]
            i += cnt
        elif &quot;.load&quot; in name or &quot;.store&quot; in name:
            raw, n = read_uleb_bytes(code, i); i += n; out += raw
            raw, n = read_uleb_bytes(code, i); i += n; out += raw
        elif name in (&quot;memory.size&quot;, &quot;memory.grow&quot;):
            out.append(code[i]); i += 1
        elif name in (&quot;i32.const&quot;, &quot;i64.const&quot;):
            raw, n = read_sleb_bytes(code, i); i += n; out += raw
        elif name == &quot;f32.const&quot;:
            out += code[i : i + 4]; i += 4
        elif name == &quot;f64.const&quot;:
            out += code[i : i + 8]; i += 8
        elif name == &quot;ref.null&quot;:
            out.append(code[i]); i += 1
        elif name == &quot;ref.func&quot;:
            raw, n = read_uleb_bytes(code, i); i += n; out += raw

    return bytes(out)


def rewrite_module(wasm: bytes, enc2name: dict[int, str],
                   std: dict[str, int], std_fc: dict[str, int]) -&amp;gt; bytes:
    secs = parse_sections(wasm)
    out = bytearray(wasm)

    # globals (6)
    gr = sec_range(secs, 6)
    if gr:
        gs, ge = gr
        p = gs
        gcount, n = uleb(out, p); p += n
        for _ in range(gcount):
            p += 2
            rw, used = rewrite_init_expr(bytes(out[p:ge]), enc2name, std)
            out[p : p + used] = rw
            p += used

    # elem (9) - active segments(flags 0/2)만
    er = sec_range(secs, 9)
    if er:
        es, ee = er
        p = es
        segcnt, n = uleb(out, p); p += n
        for _ in range(segcnt):
            flags, n = uleb(out, p); p += n
            if flags not in (0, 2):
                raise ValueError(f&quot;unsupported elem flags {flags}&quot;)
            if flags == 2:
                _, n = uleb(out, p); p += n
            rw, used = rewrite_init_expr(bytes(out[p:ee]), enc2name, std)
            out[p : p + used] = rw
            p += used
            cnt, n = uleb(out, p); p += n
            for _j in range(cnt):
                _, n = uleb(out, p); p += n

    # data (11)
    dr = sec_range(secs, 11)
    if dr:
        ds, de = dr
        p = ds
        segcnt, n = uleb(out, p); p += n
        for _ in range(segcnt):
            flags, n = uleb(out, p); p += n
            if flags &amp;amp; 0x02:
                _, n = uleb(out, p); p += n
            if (flags &amp;amp; 0x01) == 0:
                rw, used = rewrite_init_expr(bytes(out[p:de]), enc2name, std)
                out[p : p + used] = rw
                p += used
            sz, n = uleb(out, p); p += n
            p += sz

    # code (10)
    cr = sec_range(secs, 10)
    if not cr:
        raise ValueError(&quot;no code section&quot;)
    cs, _ce = cr
    p = cs
    fcnt, n = uleb(out, p); p += n
    for fi in range(fcnt):
        body_size, nsz = uleb(out, p); p += nsz
        bs = p
        be = p + body_size
        body = bytes(out[bs:be])
        q = 0
        lgc, nn = uleb(body, q); q += nn
        for _ in range(lgc):
            _, nn = uleb(body, q); q += nn
            q += 1
        rw = rewrite_code(body[q:], enc2name, std, std_fc)
        if len(rw) != len(body) - q:
            raise ValueError(f&quot;code size changed at func {fi}; need section rebuild&quot;)
        out[bs + q : be] = rw
        p = be

    return bytes(out)


def main() -&amp;gt; int:
    ap = argparse.ArgumentParser()
    ap.add_argument(&quot;--wasm&quot;, required=True)
    ap.add_argument(&quot;--opdump&quot;, required=True)
    ap.add_argument(&quot;--out&quot;, required=True)
    args = ap.parse_args()

    repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    std, std_fc = load_std_maps(repo_root)
    enc2name = load_opdump(args.opdump)
    wasm = open(args.wasm, &quot;rb&quot;).read()
    out = rewrite_module(wasm, enc2name, std, std_fc)
    open(args.out, &quot;wb&quot;).write(out)
    print(&quot;[ok]&quot;, args.out, &quot;size&quot;, len(out))
    return 0


if __name__ == &quot;__main__&quot;:
    raise SystemExit(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/nneonneo/ghidra-wasm-plugin&quot;&gt;Ghidra Wasm plugin&lt;/a&gt;을 설치하고 패치된 바이너리를 열어보면:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1ZWasm/4.png&quot; alt=&quot;4.png&quot; /&gt;&lt;/p&gt;
&lt;p&gt;코드가 보인다!&lt;/p&gt;
&lt;p&gt;제일 궁금했던 &lt;code&gt;apply_sbox_layer&lt;/code&gt; 함수 로직은 &lt;strong&gt;64비트 값을 4비트 조각으로 쪼개 S-box 테이블(&lt;code&gt;DAT_ram_00100abc&lt;/code&gt;)로 바꾼 뒤 다시 64비트로 합치는&lt;/strong&gt; 로직이었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1ZWasm/5.png&quot; alt=&quot;5.png&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;8. 솔브 코드&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import hashlib

HASHES = [
    &quot;4462fc5ac9dda45e1f956bfa9159c148906ea2eb&quot;,
    &quot;50afeeeba0e2674462f21f8286bb8fa20f91e882&quot;,
    &quot;271d1c2edfb9d5c6506be73c695958b035a2fbb4&quot;,
    &quot;82ef770e9de4f8ad66071b95e69cc80998cc6154&quot;,
    &quot;5a6778089ec6ecf9abc84348af33e761e30f0368&quot;,
    &quot;c839f2e7c4701924709bc0b9fa339cb7d361102e&quot;,
    &quot;e93f555173093854f0588ae859de9caf686213d4&quot;,
    &quot;3e0bd1fb0cc18b23941f3a015a0826071f9526a6&quot;,
    &quot;1df3f80cd3b0c34360f9e7c56378fba862d05e0b&quot;,
    &quot;6c144bf262c8e54719cd1e056ca6ede306161182&quot;,
    &quot;983b5bc62c686e987e252de3548cd17ce2f0c998&quot;,
    &quot;64e63929dc2c52e93a26ea72c6c5d00db839d2c4&quot;,
    &quot;afb7023a362d94e0bfdb33e2a8dced45ae87aa0b&quot;,
    &quot;c5b02ef1d8f7a31950ab219e6258355fd81a218e&quot;,
    &quot;58511d70859435ea1e4a4a64ebab7aff01e8ccad&quot;,
    &quot;2f58d45d2c6cdf5421d41af2b08778abf0e3d525&quot;,
    &quot;deeb5ed18ce5d39b0f0be9a7f9ccd9bdf8221753&quot;,
    &quot;8bae2558d08c0bc17e80192fbc398436e6934d55&quot;,
    &quot;b6a03cd5819d84f9babf9e80291b08c6901cfa5d&quot;,
    &quot;2d91a20a20fcaeb0ae60b5189b810bdf8481b1d7&quot;,
    &quot;5a930b172e356b7e4d9e3e0560863ca7794a87d8&quot;,
    &quot;72f676227f09ea59b4c176786b43d5638709e594&quot;,
    &quot;4acf5137e8896d3de00cb2fe97cbbbd5c00ae443&quot;,
    &quot;b6f116748b1a85fd0514666b220f3acd73c05863&quot;,
    &quot;a5522b2b6e176d64dc1d064264d43704e31b7325&quot;,
    &quot;1ea1c9dea1b08dc9cb3834d66e4ac48c28e3cf30&quot;,
    &quot;f5b378b0e1eed620d0522de40427a59ecb1d1b06&quot;,
    &quot;b6d3b5ebc1b4ed96d33702e18796846d4a07996c&quot;,
    &quot;c4e45262d2f681f87ca7624dab7589df0daec44e&quot;,
    &quot;fd1a1247655fccb5ccf1689f99789a30e69440fa&quot;,
    &quot;4a7008477bfd6c0bb38b44cbfe1f578230f09c34&quot;,
    &quot;2eebddd42c73f4df038bcedf24d2a9fda3e24b96&quot;,
    &quot;416a237b97883bf34bcbf14f9b84bc8111d369dd&quot;,
    &quot;814daf9e0d4106a254fe39a4019338377db99300&quot;,
    &quot;c70544086b038f41391a84815e6a3cb5c8e7405a&quot;,
    &quot;f36ef8a4827781edca736f121fab55d61ad2a902&quot;,
    &quot;e737b3e6a9d2f5d36c930e0f825813ae1f86e2ee&quot;,
    &quot;2789186ff38370bb23b45775f9bcf8b3bfb72ddc&quot;,
    &quot;001c5fcd727e9e8a1af79b5ef966771b5b5008ef&quot;,
    &quot;d685f409ea233df5eba3658a4547ddd78485d2fa&quot;,
]

SBOX = [0x0e, 0x04, 0x0d, 0x01, 0x02, 0x0f, 0x0b, 0x08, 0x03, 0x0a, 0x06, 0x0c, 0x05, 0x09, 0x00, 0x07]
INV_SBOX = [0] * 16
for i, v in enumerate(SBOX):
    INV_SBOX[v] = i

MASK32 = 0xFFFFFFFF
MASK64 = (1 &amp;lt;&amp;lt; 64) - 1

C1 = (-0x4508329480000000) &amp;amp; MASK64
C2 = (-0x77fccd388a106529) &amp;amp; MASK64
C0 = 0x225d686a
M32 = 0x1e35a7bd
ROUND_CONSTS = [
    0x6bdfe07b,
    0xaf92329f,
    0x32553697,
    0x45b21761,
    0xa45b1a4b,
    0xff580251,
    0xd3f278fc,
    0x0945406a,
    0x9826331a,
    0x3d0b5dce,
    0x52b238a8,
    0x3c67752c,
    0xeb51d479,
    0xf3c6b787,
    0x7f7d729b,
]

SHIFT = (C1 &amp;amp; -C1).bit_length() - 1
K1 = C1 &amp;gt;&amp;gt; SHIFT
MOD_K1 = 1 &amp;lt;&amp;lt; (64 - SHIFT)
INV_K1 = pow(K1, -1, MOD_K1)
MOD31 = 1 &amp;lt;&amp;lt; 31
INV_C2_31 = pow(C2 % MOD31, -1, MOD31)

def g(u8):
    return (SBOX[u8 &amp;gt;&amp;gt; 4] &amp;lt;&amp;lt; 17) | (SBOX[u8 &amp;amp; 0xF] &amp;lt;&amp;lt; 13)

def f32(u):
    return ((u * M32) ^ g(u &amp;amp; 0xFF)) &amp;amp; MASK32

def inv_sbox_layer(x):
    out = 0
    for i in range(16):
        out |= INV_SBOX[(x &amp;gt;&amp;gt; (4 * i)) &amp;amp; 0xF] &amp;lt;&amp;lt; (4 * i)
    return out

def bswap64(x):
    return int.from_bytes(x.to_bytes(8, &quot;little&quot;), &quot;big&quot;)

def inv_mix(p1, u):
    t0 = ((p1 &amp;gt;&amp;gt; SHIFT) * INV_K1) % MOD_K1
    a = (t0 * C2) &amp;amp; MASK64
    k = (u - (a &amp;gt;&amp;gt; 33)) &amp;amp; (MOD31 - 1)
    k = (k * INV_C2_31) % MOD31
    return (t0 + (k &amp;lt;&amp;lt; (64 - SHIFT))) &amp;amp; MASK64

def recover_words():
    table = {hashlib.sha1(v.to_bytes(2, &quot;little&quot;)).hexdigest(): v for v in range(65536)}
    return [table[h] for h in HASHES]

def recover_block(out64):
    b = out64.to_bytes(8, &quot;little&quot;)
    u = b[7] | (b[6] &amp;lt;&amp;lt; 8) | (b[5] &amp;lt;&amp;lt; 16) | ((b[4] &amp;amp; 0x7F) &amp;lt;&amp;lt; 24)
    p1 = ((b[4] &amp;amp; 0x80) &amp;lt;&amp;lt; 24) | (b[3] &amp;lt;&amp;lt; 32) | (b[2] &amp;lt;&amp;lt; 40) | (b[1] &amp;lt;&amp;lt; 48) | (b[0] &amp;lt;&amp;lt; 56)

    t = inv_mix(p1, u)
    v = inv_sbox_layer(t)
    A = v &amp;amp; MASK32
    B = (v &amp;gt;&amp;gt; 32) &amp;amp; MASK32

    A ^= f32(B ^ ROUND_CONSTS[-1])
    for i in range(len(ROUND_CONSTS) - 2, -1, -1):
        if i % 2 == 0:
            A ^= f32(B ^ ROUND_CONSTS[i])
        else:
            B ^= f32(A ^ ROUND_CONSTS[i])

    H = B ^ f32(A ^ C0)
    p1 = (A &amp;amp; 0x80000000) | (H &amp;lt;&amp;lt; 32)
    u = A &amp;amp; 0x7FFFFFFF
    y = inv_mix(p1, u)

    x = bswap64(inv_sbox_layer(y))
    return x.to_bytes(8, &quot;little&quot;)

def main():
    w = recover_words()
    data = b&quot;&quot;.join(
        recover_block(w[i] | (w[i + 1] &amp;lt;&amp;lt; 16) | (w[i + 2] &amp;lt;&amp;lt; 32) | (w[i + 3] &amp;lt;&amp;lt; 48)) for i in range(0, len(w), 4)
    )
    print(data.rstrip(b&quot;\x00&quot;).decode())

if __name__ == &quot;__main__&quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Flag&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;0ops{𝞃ĥ|ß_ǐ$_Ʀ³αIıу_Ą_Ślmp1ȩ_Ŵǟ5ɱ_cHªIIεnɠ𝛆（）}&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;9. 결론&lt;/h2&gt;
&lt;p&gt;한번 코드가 노출되면 아무리 어려운 로직이어도 LLM이 쓱싹 풀어줄 수도 있는데, 여기에 포렌식 요소를 더해서 스크램블된 opcode를 패치해 바이너리를 복원하게끔 하는 출제 의도가 좋았다.&lt;/p&gt;
&lt;p&gt;물론 CTF 서버에서 어떤 분이 LLM이 잘 풀어주더라며 코멘트 하기도 했지만, 적어도 원샷에 풀 수 있는 문제는 아니었다고 생각한다.&lt;/p&gt;
&lt;p&gt;더 디깅하고 싶은데 시간이 없는 관계로 이 정도만 적을게요 그럼 20000&lt;/p&gt;
</content:encoded></item></channel></rss>