SECCON CTF 2022 Finals - Heptarchy writeup

SECCON CTF 2022の国内決勝にチームAERO SANITYで参加して、4位でした。

以前はチームdodododoとして参加していたのですが、チームメンバーの半数(1人)がCTF運営側になってしまったので、今回は会社の同僚を誘ってやってきました。

Heptarchy

様々な言語のバイナリを手でデコンパイルして、どれだけ本来のソースコードに似ているかを競う、King of the Hillの問題です。

提出したソースコードは5分毎に評価され、チームのなかで最もバイナリのdiffスコア(何かのアルゴリズムをベースにしているとのことですがブラックボックス)が少なかったチームへの得点比重が大きくなるようにポイントが加算されます(1位なら20点、2位なら18点といったもの)。

問題自体は、1時間ごとに以下の言語のバイナリが合計7問出題されました。

基本的な方針としては、未提出だと0点なので最初に空のmain関数でもよいので提出しておいて得点を稼ぐ、そしてとにかく早くデコンパイルするということです(1言語につき12回しか評価されない)。

C

ひとまずIDA Proでデコンパイルした結果を適当に貼り付け、ほぼそのまま提出しました。即座に分かるようなところに関しては修正できたのですが、細かいところは適当のままです。

最終順位: 4, diff: 920

#include <stdio.h>
#include <stdlib.h>

typedef long int __int64;

typedef unsigned char _BYTE;
typedef unsigned long long _QWORD;

__int64 myers_diff(__int64 a1, __int64 a2, __int64 a3, __int64 a4)
{
  __int64 result; // rax
  _QWORD *v7; // [rsp+20h] [rbp-30h]
  __int64 v8; // [rsp+28h] [rbp-28h]
  __int64 j; // [rsp+30h] [rbp-20h]
  __int64 i; // [rsp+38h] [rbp-18h]
  __int64 k; // [rsp+40h] [rbp-10h]
  __int64 v12; // [rsp+48h] [rbp-8h]

  if ( a2 > 0x3FFFFFFFFFFFFFFELL || a4 > 0x3FFFFFFFFFFFFFFELL )
    __assert_fail("sa < LONG_MAX/2 && sb < LONG_MAX/2", "/tmp/main.c", 9u, "myers_diff");
  v8 = a2 + a4;
  if ( (unsigned long long)(a2 + a4) > 0x7FFFFFFFFFFFFFELL )
//  if ( (unsigned __int64)(a2 + a4) > 0x7FFFFFFFFFFFFFELL )
    __assert_fail("max < (LONG_MAX/2-1)/sizeof(ssize_t)", "/tmp/main.c", 0xBu, "myers_diff");
  v7 = calloc(2 * v8 + 1, 8uLL);
  for ( i = 0LL; ; ++i )
  {
    result = i;
    if ( i > v8 )
      break;
    for ( j = -i; j <= i; j += 2LL )
    {
      if ( j != -i && (j == i || v7[v8 - 1 + j] >= v7[v8 + 1 + j]) )
        v12 = v7[v8 - 1 + j] + 1LL;
      else
        v12 = v7[v8 + 1 + j];
      for ( k = v12 - j; v12 < a2 && k <= a4 && *(_BYTE *)(v12 + a1) == *(_BYTE *)(k + a3); ++k )
        ++v12;
      v7[v8 + j] = v12;
      if ( v12 >= a2 && k >= a4 )
        return i;
    }
  }
  return result;
}

__int64 get_size(FILE *a1)
{
  __int64 v2; // [rsp+18h] [rbp-8h]

  fseek(a1, 0LL, 2);
  v2 = ftell(a1);
  fseek(a1, 0LL, 0);
  return v2;
}

int main(int argc, const char **argv)
{
  __int64 v4; // rax
  void *v5; // [rsp+10h] [rbp-30h]
  void *ptr; // [rsp+18h] [rbp-28h]
  __int64 n; // [rsp+20h] [rbp-20h]
  __int64 size; // [rsp+28h] [rbp-18h]
  FILE *v9; // [rsp+30h] [rbp-10h]
  FILE *stream; // [rsp+38h] [rbp-8h]

    if ( argc <= 2 )
  {
    printf("Usage: %s <file1> <file2>\n", *argv);
    return 1;
  }
  else
  {
    stream = fopen(argv[1], "r");
    if ( stream )
    {
      v9 = fopen(argv[2], "r");
      if ( v9 )
      {
        size = get_size(stream);
        n = get_size(v9);
        ptr = malloc(size);
        if ( ptr )
        {
          v5 = malloc(n);
          if ( fread(ptr, 1uLL, size, stream) == size && fread(v5, 1uLL, n, v9) == n )
          {
            v4 = myers_diff(ptr, size, v5, n);
            printf("%ld", v4);
          }
          free(v5);
          free(ptr);
          fclose(v9);
          fclose(stream);
          return 0;
        }
        else
        {
          fclose(v9);
          fclose(stream);
          return 1;
        }
      }
      else
      {
        perror(argv[2]);
        fclose(stream);
        return 1;
      }
    }
    else
    {
      perror(argv[1]);
      return 1;
    }
  }
}

C++

それっぽく戻します。IDA Proでデコンパイルした結果をそのままコンパイルできるかんじではないので、流用しつつも丁寧にclassなどに戻していきます。実は手動デコンパイルが適当で、動かすとsegmentation faultで落ちます。

最終順位: 1, diff: 3528

#include <iomanip>
#include <iostream>
#include <string>
#include <vector>

typedef long long __int64;

typedef unsigned char _BYTE;
typedef unsigned long long _QWORD;

class RC4 {
    std::vector<unsigned char> a1;
public:
    RC4(std::string const &a2) {
      unsigned char *result; // rax
      char v3; // r12
      unsigned long long v4; // rax
      __int64 v5; // rbx
      __int64 v6; // rax
      int j; // [rsp+14h] [rbp-1Ch]
      int i; // [rsp+18h] [rbp-18h]
      unsigned char v9; // [rsp+1Fh] [rbp-11h]

      std::vector<unsigned char>(a1);
      a1.resize(256);
      for ( i = 0; i <= 255; ++i )
      {
        result = &a1[i];
        *result = i;
      }
      v9 = 0;
      for ( j = 0; j <= 255; ++j )
      {
        v3 = a1[j];
        v4 = a2.size();
        v9 += v3 + a2[j%v4];
        v5 = a1.at(v9);
        v6 = a1.at(j);
        std::swap(v6, v5);
      }
    }
    unsigned char* encrypt(std::string const &a2) {
      unsigned long long v2; // rax
      __int64 v3; // rbx
      __int64 v4; // rax
      char v5; // r12
      _BYTE v6; // rax
      unsigned char *v8; // [rsp+10h] [rbp-20h]
      int i; // [rsp+18h] [rbp-18h]
      unsigned char v10; // [rsp+1Eh] [rbp-12h]
      unsigned char v11; // [rsp+1Fh] [rbp-11h]

      v11 = 0;
      v10 = 0;
      v2 = a2.size();
      v8 = new unsigned char[v2];
      for ( i = 0; i < a2.size(); ++i )
      {
        v10 += a1[++v11];
        v3 = a1.at(v10);
        v4 = a1.at(v11);
        std::swap(v4, v3);
        v5 = a1[v11];
        v6 = a1[v10];
        v3 = a1[v5 + v6];
        v8[i] = a2[i] ^ v3;
      }
      return v8;
    }
};

int main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rdx
  __int64 v4; // rbx
  unsigned int v5; // eax
  unsigned int v6; // eax
  __int64 v7; // rax
  unsigned long long v8; // rbx
  RC4 *v10; // [rsp+0h] [rbp-80h] BYREF
  unsigned char *v13; // [rsp+60h] [rbp-20h]
  int i; // [rsp+6Ch] [rbp-14h]

  std::string v12;
  std::string v11;
  std::cout << "Key: ";
  std::cin >> v12;
  std::cout << "Plaintext: ";
  std::cin >> v11;
  v10 = new RC4(v12);
  std::cout << "Ciphertext: " << std::hex << std::setfill('0');
  v13 = v10->encrypt(v11);
  for ( i = 0; ; ++i )
  {
    v8 = i;
    if ( v8 >= v11.size() )
      break;
    std::cout << std::setw(2) << v13[i];
  }
  std::cout << std::endl;
  if ( v13 )
      delete v13;
  return 0;
}

Rust

まったく同じコードを想像するのが難しいので動かしたときの挙動を把握しつつ、それっぽく書きました。

最終順位: 1, diff: 14342

use std::io;
use std::io::Write;

fn get_player_hand(u: i32) -> i32 {
    print!("Player {} Hand [Rock/Paper/Scissors]: ", u);
    io::stdout().flush().unwrap();
    let mut x = String::new();
    io::stdin().read_line(&mut x)
        .expect("I/O error");

    let y = x.trim().to_lowercase();
    match &*y {
        "rock" => 0,
        "paper" => 1,
        "scissors" => 2,
        _ => panic!("Invalid hand")
    }
}

fn main() {
    let p1 = get_player_hand(1);
    let p2 = get_player_hand(2);
    let x = (p1 - p2).rem_euclid(3);

    let display = match x {
        0 => "Draw!",
        1 => "Player 1 wins!",
        2 => "Player 2 wins!",
        _ => "Invalid hand"
    };
    println!("{}", display)
}

Go

channelやgoroutineを使ったコードであり、アセンブリを読むのが大変な部類なのですが、デバッグメッセージもあり、難易度としてはかなり優しくなっています。 ある程度ほぼ同じだろうというコードは書けたのですが、その時点で他チームはもっとよいスコアを叩き出していました……

最終順位: 3, diff: 11716

package main

import (
    "fmt"
    "os"
)

var counter int

func shrinker(c chan int, quit chan int) {
    var elem int
    for ;; {
        elem = 0
        elem = <-c
        if elem == 1 {
            break
        }
        if (elem & 1) == 0 {
            counter++
            elem /= 2
        }
        c <- elem
    }
    quit <- 0
}

func expander(c chan int, quit chan int) {
    var elem int
    for ;; {
         elem = 0
         elem = <-c
         if elem == 1 {
             break
         }
         if (elem & 1) != 0 {
             counter++
             elem = 3 * elem + 1
         }
         c <- elem
    }
    quit <- 0
}

func main() {
    var number int
    fmt.Fprint(os.Stdout, "Number: ")
    fmt.Fscan(os.Stdin, &number)
    if number <= 0 {
        fmt.Fprintln(os.Stdout, "Invalid number")
        return
    }
    fmt.Fprintln(os.Stdout, "[DEBUG] quit := make(chan int)")
    quit := make(chan int)
    fmt.Fprintln(os.Stdout, "[DEBUG] c := make(chan int)")
    c := make(chan int)
    fmt.Fprintln(os.Stdout, "[DEBUG] go shrinker(c, quit)")
    go shrinker(c, quit)
    fmt.Fprintln(os.Stdout, "[DEBUG] go expander(c, quit)")
    go expander(c, quit)

    c <- number
    <-quit

    fmt.Fprintln(os.Stdout, counter)
}

Python

pycを読みたくなかったのでuncompyle6などを試そうとするものの、python自体のバージョンが3.12.0a3であり、おそらく対応していません(この状況、何度もCTFで見ます)。バイトコードを読むのが嫌でどうにか試していたのですが、それっぽいのが書けず苦戦していました。 かなり後半のほうにpycのバイナリ自体をpythonから呼んでしまえば、バイナリ自体のdiffは少なくまったく同じ挙動にできることに気がつきました。

最終順位: 3, diff: 274

src=b'\xb8\r\r\n\x00\x00\x00\x00\xd3b\xd3c\xf2\x03\x00\x00\xe3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\xf3\x92\x01\x00\x00\x97\x00d\x00d\x01l\x00Z\x00d\rd\x02\x84\x01Z\x01d\x03\x84\x00Z\x02e\x03d\x04k\x02\x00\x00\x00\x00r\xb5\x02\x00e\x02d\x05\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00Z\x04\x02\x00e\x02d\x05\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00Z\x05\x02\x00e\x02d\x05\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00Z\x06e\x04e\x05z\x05\x00\x00e\x06z\x05\x00\x00Z\x07d\x06Z\x08e\tj\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00e\x0bd\x07\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00j\x19\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xab\x00\x00\x00\x00\x00\x00\x00\x00\x00d\x08\xab\x02\x00\x00\x00\x00\x00\x00\x00\x00Z\re\re\x07k\x04\x00\x00\x00\x00r\x12\x02\x00e\x0ed\t\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x00e\x0fd\n\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x00e\x10e\re\x08e\x07\xab\x03\x00\x00\x00\x00\x00\x00\x00\x00Z\x11\x02\x00e\x0ed\x0b\x02\x00e\x12e\x11\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00\x9b\x00\x9d\x02\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00e\x04d\nz\n\x00\x00e\x05d\nz\n\x00\x00z\x05\x00\x00e\x06d\nz\n\x00\x00z\x05\x00\x00Z\x13\x02\x00e\x10e\x08d\x0ce\x13\xab\x03\x00\x00\x00\x00\x00\x00\x00\x00Z\x14\x02\x00e\x10e\x11e\x14e\x07\xab\x03\x00\x00\x00\x00\x00\x00\x00\x00Z\x15e\re\x15k\x02\x00\x00\x00\x00s\x02J\x00\x82\x01d\x01S\x00d\x01S\x00)\x0e\xe9\x00\x00\x00\x00Nc\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x03\x00\x00\x00\xf3l\x01\x00\x00\x97\x00|\x00d\x01k\x02\x00\x00\x00\x00s\x06|\x00d\x02k\x02\x00\x00\x00\x00r\x02d\x03S\x00|\x00d\x04z\x01\x00\x00d\x05k\x02\x00\x00\x00\x00r\x02d\x06S\x00d\x05|\x00d\x04z\n\x00\x00}\x03}\x02|\x03d\x04z\x01\x00\x00d\x05k\x02\x00\x00\x00\x00r\x14|\x03d\x04z\x16\x00\x00}\x03|\x02d\x04z\r\x00\x00}\x02|\x03d\x04z\x01\x00\x00d\x05k\x02\x00\x00\x00\x00r\x01\x8c\x14t\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|\x01\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00D\x00]f\x00\x00}\x04t\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00j\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00d\x01|\x00d\x04z\n\x00\x00\xab\x02\x00\x00\x00\x00\x00\x00\x00\x00}\x05t\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|\x05|\x03|\x00\xab\x03\x00\x00\x00\x00\x00\x00\x00\x00}\x06|\x06d\x04k\x02\x00\x00\x00\x00s\t|\x00|\x06z\n\x00\x00d\x04k\x02\x00\x00\x00\x00r\x01\x8c=t\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|\x02d\x04z\n\x00\x00\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00D\x00]\x15\x00\x00}\x04|\x06|\x06z\x05\x00\x00|\x00z\x06\x00\x00}\x06|\x00|\x06z\n\x00\x00d\x04k\x02\x00\x00\x00\x00s\x01\x8c\x15\x01\x00\x8cd\x04\x00\x01\x00d\x06S\x00\x04\x00d\x03S\x00)\x07N\xe9\x02\x00\x00\x00\xe9\x03\x00\x00\x00T\xe9\x01\x00\x00\x00r\x02\x00\x00\x00F)\x04\xda\x05range\xda\x06random\xda\trandrange\xda\x03pow)\x07\xda\x01n\xda\x01k\xda\x01r\xda\x01s\xda\x01_\xda\x01a\xda\x01xs\x07\x00\x00\x00       \xfa\x0c/tmp/main.py\xda\x07isPrimer\x13\x00\x00\x00\x03\x00\x00\x00s\x07\x01\x00\x00\x80\x00\xd8\x07\x08\x88A\x82v\x80v\x90\x11\x90a\x92\x16\x90\x16\xd8\x0f\x13\x88t\xd8\t\n\x88Q\x89\x15\x90!\x8a\x1a\x88\x1a\xd8\x0f\x14\x88u\xe0\x0b\x0c\x88a\x90\x01\x89c\x80q\x80A\xd8\n\x0b\x88a\x89%\x901\x8a*\x88*\xd8\x08\t\x88a\x89\x07\x88\x01\xd8\x08\t\x88Q\x89\x06\x88\x01\xf0\x05\x00\x0b\x0c\x88a\x89%\x901\x8a*\x88*\xf8\xf5\x08\x00\x0e\x13\x901\x8cX\xf0\x00\n\x05\x19\xf1\x00\n\x05\x19\x88\x01\xdd\x0c\x12\xd7\x0c\x1c\xd1\x0c\x1c\x98Q\xa0\x01\xa0!\xa1\x03\xd4\x0c$\x88\x01\xdd\x0c\x0f\x90\x01\x901\x90a\x8cL\x88\x01\xd8\x0b\x0c\x90\x01\x8a6\x886\x90Q\x98\x11\x91U\x98a\x92Z\x90Z\xd8\x0c\x14\xdd\x11\x16\x90q\x98\x11\x91s\x94\x1a\xf0\x00\x05\t\x19\xf1\x00\x05\t\x19\x88A\xd8\x10\x11\x90A\x91\x05\x98\x01\x91\t\x88A\xd8\x0f\x10\x901\x89u\x98\x01\x8az\x88z\xf8\xd8\x10\x15\x90\x05\xf0\x07\x05\t\x19\xf0\n\x00\x14\x19\x905\x905\xf0\x15\n\x05\x19\xf0\x18\x00\x0c\x10\x884\xf3\x00\x00\x00\x00c\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x03\x00\x00\x00\xf3f\x00\x00\x00\x97\x00\t\x00t\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00j\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00d\x01|\x00z\x03\x00\x00d\x01|\x00d\x01z\x00\x00\x00z\x03\x00\x00\xab\x02\x00\x00\x00\x00\x00\x00\x00\x00}\x01t\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|\x01\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00r\x02|\x01S\x00\x8c1)\x02Nr\x06\x00\x00\x00)\x03r\x08\x00\x00\x00r\t\x00\x00\x00r\x13\x00\x00\x00)\x02\xda\x04bits\xda\x01ps\x02\x00\x00\x00  r\x12\x00\x00\x00\xda\x08getPrimer\x18\x00\x00\x00\x1c\x00\x00\x00s6\x00\x00\x00\x80\x00\xd8\n\x0e\xdd\x0c\x12\xd7\x0c\x1c\xd1\x0c\x1c\x98Q\xa0\x04\x99W\xa0a\xa8$\xa8q\xa9&\xa1k\xd4\x0c2\x88\x01\xdd\x0b\x12\x901\x8c:\x88:\xd8\x13\x14\x88H\xf0\x07\x00\x0b\x0fr\x14\x00\x00\x00\xda\x08__main__\xe9\x00\x01\x00\x00i\x01\x00\x01\x00z\x06Text: \xda\x03bigz\x08Too longr\x06\x00\x00\x00z\x08Cipher: \xe9\xff\xff\xff\xff)\x01\xe9\n\x00\x00\x00)\x16r\x08\x00\x00\x00r\x13\x00\x00\x00r\x18\x00\x00\x00\xda\x08__name__r\x17\x00\x00\x00\xda\x01qr\r\x00\x00\x00r\x0b\x00\x00\x00\xda\x01e\xda\x03int\xda\nfrom_bytes\xda\x05input\xda\x06encode\xda\x01m\xda\x05print\xda\x04exitr\n\x00\x00\x00\xda\x01c\xda\x03hex\xda\x03phi\xda\x01d\xda\x02mm\xa9\x00r\x14\x00\x00\x00r\x12\x00\x00\x00\xfa\x08<module>r.\x00\x00\x00\x01\x00\x00\x00s-\x01\x00\x00\xf0\x03\x01\x01\x01\xd8\x00\r\x80\r\x80\r\x80\r\xf0\x04\x17\x01\x10\xf0\x00\x17\x01\x10\xf0\x00\x17\x01\x10\xf0\x00\x17\x01\x10\xf02\x04\x01\x15\xf0\x00\x04\x01\x15\xf0\x00\x04\x01\x15\xf0\x0c\x00\x04\x0c\x88z\xd2\x03\x19\xd0\x03\x19\xd8\x08\x10\x88\x08\x90\x13\x8c\r\x80A\xd8\x08\x10\x88\x08\x90\x13\x8c\r\x80A\xd8\x08\x10\x88\x08\x90\x13\x8c\r\x80A\xd8\x08\t\x88!\x89\x03\x88A\x89\x05\x80A\xd8\x08\r\x80A\xd8\x08\x0b\x8f\x0e\x89\x0e\x90u\x90u\x98X\x94\x7f\xd7\x17-\xd1\x17-\xd4\x17/\xb0\x15\xd4\x087\x80A\xd8\x07\x08\x881\x82u\x80u\xd8\x08\r\x88\x05\x88j\xd4\x08\x19\xd0\x08\x19\xd8\x08\x0c\x88\x04\x88Q\x8c\x07\x88\x07\xe0\x08\x0b\x88\x03\x88A\x88q\x90!\x8c\x0c\x80A\xd8\x04\t\x80E\xd0\n\x1d\x90S\x90S\x98\x11\x94V\xd0\n\x1d\xd0\n\x1d\xd4\x04\x1e\xd0\x04\x1e\xe0\x0b\x0c\x88Q\x893\x90\x11\x901\x91\x13\x89+\x90q\x98\x11\x91s\xd1\n\x1b\x80C\xd8\x08\x0b\x88\x03\x88A\x88r\x903\x8c\x0f\x80A\xd8\t\x0c\x88\x13\x88Q\x90\x01\x901\x8c\x1c\x80B\xe0\x0b\x0c\x90\x02\x8a7\x887\x80N\x80N\x887\x887\xf0%\x00\x04\x1a\xd0\x03\x19r\x14\x00\x00\x00'
import marshal
exec(marshal.loads(src[16:]))

D

パスワードを入力し"Make D-lang Great Again!"と比較するというプログラムの動作は理解できるのですが、そもそも空のmain関数を提出しても0点のままであり、他のチームも同じように0点続きだったので何かしらの罠があったのだと思います。

ptraceの有無やパスワードの正解不正解の挙動が正しくないと問答無用で0点にするのでは、という罠が用意されているような予想をして色々試していたのですが、挙動自体を同じように実装したとしても0点のままでわからず、途中で諦めました……

最終順位: 5, diff: 9999999999

import std.stdio;
import std.string;
import core.stdc.stdlib;
import std.algorithm;

extern(C) long ptrace(int a, int b, int c, int d);

bool check_password(string password)
{
    return false;
}

void main()
{
    if (ptrace(0, 0, 1, 0)) {
        exit(1);
    }
    write("Password: ");
    string password = strip(readln('\n'));
    if (check_password(password)) {
        writeln("Correct!");
    } else {
        writeln("Wrong...");
    }
}

wasm

emscriptenでwasmに変換されたcのコードを直します。 実際にコンパイルされる際には、-O1の最適化が効いており、中身がほぼない関数などはインライン展開されてしまい、元のバイナリに比べると大きなdiffが発生してしまうので、関数に__attribute__ ((noinline))をつけてあげるとインライン展開を抑制するのがポイントです。この状態でmain関数とほぼ空のbruteforce関数だけ実装すればdiffがかなり小さくなりました。

また、wasmを読む際には拙作のidawasm2を使いました。そもそも簡素なプログラムであるため、そこまで活躍はしませんでしたが、便利です。(ちなみにpython 3.10では使えないので、手元ではidapythonrc.pyでpython 3.9を使うように変更して無理矢理使っています)

最終順位: 1, diff: 518

#include <stdio.h>
#include <stdlib.h>

__attribute__ ((noinline)) int bruteforce(char *nums) {
    nums[0] = 6;
    nums[1] = 9;
    nums[2] = 6;
    nums[3] = 3;
    return 1;
}

int main() {
    char *nums = malloc(4);
    puts("[+] Computing...");
    if (bruteforce(nums)) {
        printf("Hit: %d%d%d%d\n", nums[0], nums[1], nums[2], nums[3]);
    }
    puts("[+] Done.");
    free(nums);
    return 0;
}

まとめ

ある処理系の吐くバイナリ自体を読んだことないときには、言語を書きながら吐かれるアセンブリも一緒に読むということはたまにやりますが、実際にちゃんと手動でデコンパイルまでやることはなく、なんとなくこういうコードなんだろうなと思っていたアセンブリがこうだったのか、と理解できたのは面白い体験でした。

ただ、1時間に1言語出題されるというのが忙しすぎるという印象はありました。 そもそもがスピード勝負であり、ある程度デコンパイルできて形になってくるのは結局30分後くらいで、微調整していくも時間は溶け、次の言語のための準備もしないといけないというのがかなり重労働でした。 とはいえ、全体としては一人勝ち状態にならないようにうまくKoHのバランスが取れていたのもあり(例年のSECCONであれば、最初に解いたチームがほぼ独占してポイントを稼ぐのが恒例だった)、かなりよい問題だったと思いました。

運営の皆さん、参加者の皆さんお疲れ様でした。来年も期待しています。