Ricera CTF 2023 writeup

2023/4/22に行なわれたRicerca CTF 2023にdodododoで参加して、2位でした。

dodododoでは普段CTFに参加するときは、Google Docsにドキュメントを用意しておき、どの問題を解こうとしているかなどの進捗状況を共有できるようにしています。大したものはないのですが、せっかくなので中身を晒しつつ、writeupを書いていきます。

解いた問題

crackme

Google Docsの内容:

[solved] crackme
RicSec{U_R_h1y0k0_cr4ck3r!}

何も詳細は書いていません。warmup問題のようなやるだけの問題はflagを書いて終わっています。

N1pp0n-Ich!_s3cuR3_p45$w0rDとstrcmpしている部分を見かけたので、そのまま求められるパスワードとして入力したところ、flagが出力されました。

Cat Cafe

Google Docsの内容:

[solved] Cat Cafe
/img?f=..././flag.txt

RicSec{directory_traversal_is_one_of_the_most_common_vulnearbilities}

.replace("../", "")../を置き換えるようになっていますが、再帰的な置換ではないので...././../になるというものです。

BOFSec

Google Docsの内容:

[solved] BOFSec
b'A' * 0x101 + b'\n'

RicSec{U_und3rst4nd_th3_b4s1c_0f_buff3r_0v3rfl0w}

これもwarmup問題なので、かなり簡略に書いています。そのまま送信するだけです。

tinyDB

Google Docsの内容:

[solved] tinyDB
clearするタイミングでadmin自体のパスワードが********************************になる

RicSec{j4v45cr1p7_15_7000000000000_d1f1cul7}

デバッグの際に適当にconsole.logを仕込んでいると、userDB.sizeが10より大きくなったときに走る処理によって、adminのパスワードが********************************になることに気づきました。以下のコードを読んだときは単にレスポンスの内容にだけ影響するものと思っていましたが、Mapのkeyとしているauthの参照自体を書き換えているのでそのまま書き換えられてしまいますね。

  let auth = {
    username: username ?? "admin",
    password: password ?? randStr(),
  };
  if (!userDB.has(auth)) {
    userDB.set(auth, "guest");
  }

  if (userDB.size > 10) {
    // Too many users, clear the database
    auth.username = "admin";
    auth.password = getAdminPW();
    userDB.set(auth, "admin");
    auth.password = "*".repeat(auth.password.length);
  }

こういうタイプの問題は手元で動かしたらすぐにわかってしまうという意味で、package.jsonなど実際に動かすのに必要なファイルを配布していないのだろうと思うのですが、とはいえこれが本質ではないとは思うので他の問題のように、Dockerなりですぐ動く状態のものを配布してもらいたいところです……

NEMU

Google Docsの内容:

[solved] NEMU
reg自体の元々のサイズはint32_tだけど各命令ではuint64_tで読み書きするので4バイト分はみでるというバグ

https://gist.github.com/akiym/a4b816c93c3b201cca4ed35368e6f6e4

RicSec{me0w_i_am_n3mu_n3mu_c4tt0}

上に書いてあるバグを利用することで、add命令の先頭のコードを書き換えることができます。ただし、書き換えられるのは一部なので任意のコードを実行できるようにするにはバイト数が足りません。

スタック上にはacc, r3, r2と12バイト分並んだ、自由に操作できる部分があるのでそれらに対してシェルコードを読み込むstagerを仕込んでおきます。あとは書き換えたadd命令からその部分へジャンプすることで、自由にシェルコードを実行できるようになります。

tic tac toe?

Google Docsの内容:

[solved] tic tac toe?
いろいろ崩壊してるマルバツゲーム

  | a | b | c |
--|---|---|---|
1 | 0 | 3 | 6 |
--|---|---|---|
2 | 1 | 4 | 7 |
--|---|---|---|
3 | 2 | 5 | 8 |
--|---|---|---|

盤面からfork-exitでexitcodeとして何か計算してチェックしてるっぽい

https://gist.github.com/akiym/037b9347ff6e43f17ca373daf730a728
mainから0x1590のところはこういうかんじだと思ったのだけどunsat

RicSec{t1c_t4c_t03_1s_3x1t1ng_g4m3}

前半部分はチームメンバーが書いていて、後半のgistのURLを貼っているところから自分が書いています。

gistのrevisionsを見ると、最初は解析した結果をz3のスクリプトに落とし込んでいるところが間違っています。途中でexit codeって8bitだな、とか細かいミスに気づいて直したところsatだったので、flagを求める処理に突っ込んで終わりです。

funnylfi

Google Docsの内容:

[solved] funnylfi
f!ile:// みたいなかんじでscheme_detectorはbypassできるけど、RicSecのWAFがある

gopher protocol + uwsgiで何かと思ったけど、_が消されてしまうのでUWSGI_FILEを作れない
というか%が使えないんだった

?url=˚f!ile://«/var/www/flag˚

競技時間もほぼ終盤になった頃、チームメンバーから˚を使うとコマンド内にスペースを含められることを教えてもらいました。レスポンス中にRicSecが含まれると怒られる(flagはRicSec{...}という形式なので、flagをそのまま出力できない)ので、解法的にはRangeのリクエストだろうと推測して、curl-rオプションを使う方法はないかを探しました。

curl-rオプションは通常であれば-r 0-100のように使いますが、-r0とした場合でもwarningは出るものの-r0-と同様に動くことがわかります。つまり、引数の中に-r2のようにオプションを指定することができれば、RicSecが含まれる先頭部分を捨ててflagを出力させることができるはずです。

色々試したところ、b'xn-- file:///var/www/flag -r2a'.decode('idna')の結果が' file://«/var/www/flag 'だったので、˚f!ile://«/var/www/flag˚を入力したところb'xn-- file:///var/www/flag -r2a476lwa'と変換され、無事に-r2が指定できました。

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であれば、最初に解いたチームがほぼ独占してポイントを稼ぐのが恒例だった)、かなりよい問題だったと思いました。

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

IDA Proで独自VM問を読む

この記事は、CTF Advent Calendar 2021の4日目です。

adventar.org

CTFで出題されるreversingの問題のひとつに、独自VMと呼ばれる、独自に実装されたVirtual Machineの上で動くバイトコード(プログラム)の解析を行うものがあります。

この問題ではVM自体のバイナリを読み、どのような命令があるのか、どのような挙動なのかを把握するのはもちろん、最終的にはそのVM上で動くバイトコード自体を読み解く必要があり、かなり根気がいります。

VMのバイナリはいつも通りIDA Proで解析すればよいのですが、その後のバイトコードの解析にはもちろんIDA Proが使えず、簡易的なディスアセンブラを実装し、テキストエディタでメモを書きながら読んでいく必要があります。関数呼び出しや分岐があればあるほど読みにくくなっていき、解析にかなりの時間を費すことになります。

そこでIDA Proのプラグインを自分で書いて、独自VM問のバイトコードをIDA Proで読めるようにしてしまおうというのがこの記事での本題です。

IDA Proのprocessor moduleを書く

IDA Proではprocessor moduleを書くことで独自のアーキテクチャを定義し読み込むことができます。

IDA Pro本体に同梱されているprocessor moduleのほとんどはC++で実装されていますが、CTFで対象となるのは小規模なバイナリなので書きやすさを優先して今回はPythonで実装しました。 独自のprocessor moduleを書く際のテンプレートや、Pythonで書かれたprocessor moduleもいくつか同梱されているので、それらを参考に書いていきます。

次に過去に出題された独自VM問のために作ったprocessor moduleを紹介します。

例題1: baby-a-fallen-lap-ray - DEFCON 2021 Quals

github.com

よくわからないマシン(エミュレータ)上で動くVM上で動くバイトコードのreversingです。解析自体はかなりつらいです。

作ったprocessor module: https://github.com/akiym/ida-ctf-vm-chall-reversing/blob/0a499f8427eb09241f7c4b861314aa87c17e6230/procs/fallen-lap-ray.py

f:id:akiym:20211204190203p:plain f:id:akiym:20211204190217p:plain

実のところ、IDA Proでバイトコードを読むというアイディアは以下のwriteupからいただきました(元記事ではBinary Ninjaを使っています)。ありがとうございます。

zackorndorff.com

例題2: EmojiVM - HITCON CTF 2019

github.com

この問題はスタックマシン型のVMなのですが、自分の中で解析方法が定まっておらずprocessor moduleでスタックの状態をエミュレーションしながら、その結果をコメントに追記していく形にしました。あまりIDA Proでの解析の恩恵を得られなかった例です。

作ったprocessor module: https://github.com/akiym/ida-ctf-vm-chall-reversing/blob/0a499f8427eb09241f7c4b861314aa87c17e6230/procs/emojivm.py

f:id:akiym:20211204191405p:plain

まとめ

IDA Proを使って独自VM問のバイトコードの解析ができるようprocessor moduleを書いてみました。今後出題される問題でも今回書いたスクリプトを少し書き換えれば応用可能なので、また使う機会があるかもしれません。

実際のところ、IDA Proで読めたからといってそこで終わりではなく、ここからまた時間をかけて人間が読む作業は残っています。 2020 Plug-In Contest – Hex Rays にbfというbrainfuckをHex-Rays decompilerでデコンパイルするというプラグインがあったので、解析補助のためにデコンパイラを実装できなくはないのかもしれませんが、1つのVM問に対する実装量がかなり多くなるはずで、現実的には人間が読むほうが早いということになりそうではあります。

今後も問題を解く上で書いたprocessor moduleは以下のリポジトリに追加していく予定です。面白かった独自VM問の過去問がありましたらIDA Proで読もうと思いますので是非 @akiym まで教えてください。

github.com

PerlでもgRPCで通信したい

まずはじめに、2021/2時点でgRPCがサポートされている言語にはPerlは含まれていなく、公式にはサポートされていません。 現時点でと言ったものの将来的にもサポートされることがないだろうことからPerlでgRPCを扱うのは茨の道といえるでしょう。

おとなしくgRPC transcodingしてHTTP REST APIで叩きましょう、というのがほぼ答えなのですがCPANに公開されているライブラリを使ってどこまでできるのかを検証するのがこの記事の目的です。

題材

gRPCで通信といっても、サーバとクライアントのどちらをPerlで実装するかという話になりますが、今回実装するのはクライアントです。 他の言語で書かれたマイクロサービスからPerlと通信することを想定して、手軽な例としてGAPIC Showcaseのサーバと通信することにしてみます。

github.com

google.showcase.v1beta1 packageにはいくつかのserviceが提供されていますが、その中でもEcho serviceの各メソッドを呼び出してみることを題材とします。 protoファイルに定義されたスキーマには、単純にリクエストを投げてレスポンスが返ってくるだけのEchoメソッドやサーバストリーミング、クライアントストリーミング、双方向ストリーミングなど形式で通信を行うメソッドが用意されています。

ちなみにIdentity serviceなどでも試したかったのですがproto3のoptional fieldが使われているため見送りました。

PerlからProtocol Buffersを扱う

protoファイルを元にメッセージのエンコード/デコードを行うためにGoogle::ProtocolBuffer::Dynamicを使います。 ほかにもモジュールは世に存在しているのですが、proto2にしか対応していない、メンテナンスされていないことから選択肢としては実質このモジュールしかありません。

metacpan.org

Google::ProtocolBuffer::Dynamicはその名前の通り、スタブコードを事前に生成しておくのではなく、protoファイルを読み込んで動的にインタフェースを生成します。

gRPCでクライアント通信をする場合はオプションを渡すことでGrpc::XSが内部で使われるようになります。Grpc::XSはCPANTSの結果を見るとMETA.ymlが存在しなくDevel::CheckLibの依存が漏れていたりなどと少し不安ではありますがGoogle::ProtocolBuffers::Dynamicからはこのモジュールを使うしかありません。

試してみる

PerlからgRPCで通信はできそうということがわかったので、実際にコードを書いて試してみます。今回書いたコードの全体は以下のリポジトリで公開しています。

github.com

まずはprotocコマンドを使ってスタブコードを生成します。このスタブコードというのはprotoファイルに定義されたメッセージの生成やメソッドの呼び出しを行えるようにするためのクライアント用に生成されたコードです。

以下のコマンドを実行するとGrpcSandbox::PBというPerlのパッケージが作られます。 生成されたコードにはシリアライズされたデータとgRPCとPerlのパッケージの紐付けが含まれており、実際にserviceを呼び出す際にはGrpcSandbox::PB::Google::Showcase::V1beta1::Echoパッケージを参照するといった形で行ないます。

% protoc \
    -Ithird_party/gapic-showcase/schema/api-common-protos \
    -Ithird_party/gapic-showcase/schema \
    --perl-gpd_out=package=GrpcSandbox.PB:lib \
    --perl-gpd_opt=client_services=grpc_xs \
    third_party/gapic-showcase/schema/google/showcase/v1beta1/echo.proto \
    $(find third_party/gapic-showcase/schema/api-common-protos/google -name '*.proto') \
    $(find /usr/local/include/google -name '*.proto')

ここで注意する点としては、GAPIC Showcaseが依存しているapi-common-protosgoogle.protobuf packageのprotoファイルも読み込む必要があることです。必要に応じてprotoファイルのinclude pathも指定します。

余談ですがprotocの挙動としては--perl-gpd_xxxというオプションが渡されることでGoogle::ProtocolBuffer::Dynamicの提供するproto-gen-perl-gpdというコマンドが呼ばれるようになります。 このコマンド同士のやり取りにもProtocol Buffersが使われており、オプションやprotoファイルの一覧がCodeGeneratorRequestとして渡されていたりします。

Echoメソッドの実装

クライアントライブラリを提供するという形でgRPCで通信するメソッドを実装していきます。 適宜protoファイルを見ながら読んでもらえると理解しやすいと思います。

まずは以下のコードのようにしてEcho serviceへのコネクションを作成します。 コード中にでてくる $self->service はこれを指します。

my $service = GrpcSandbox::PB::Google::Showcase::V1beta1::Echo->new(
    'gapic-showcase:7469',
    credentials => Grpc::XS::ChannelCredentials::createInsecure(),
);

serviceにあるメソッドの呼び出しは->Echoのように同じ名前で呼び出す形に対応します。 google.showcase.v1beta1 packageのEchoRequestに対応するパッケージはGrpcSandbox::PB::Google::Showcase::V1beta1::EchoRequestです。

Echoメソッドは単一(Unary)リクエストなので、呼び出し後は->waitを使ってEchoResponseに対応するオブジェクトを取得します。メッセージのフィールドの値はget_というprefixをつけて取り出すことができます。

sub echo {
    my ($self, $content) = @_;

    my $req = GrpcSandbox::PB::Google::Showcase::V1beta1::EchoRequest->new({
        content => $content,
    });
    my $call = $self->service->Echo(argument => $req);
    my $res = $call->wait;
    return $res->get_content;
}

Expand, Collectメソッドの実装

Expandメソッドは複数のレスポンスを受け取り(サーバストリーミング)、Collectメソッドは複数のリクエストを送ります(クライアントストリーミング)。

sub expand {
    my ($self, $content) = @_;

    my $req = GrpcSandbox::PB::Google::Showcase::V1beta1::ExpandRequest->new({
        content => $content,
    });
    my $call = $self->service->Expand(argument => $req);
    my @res = $call->responses;
    return [map { $_->get_content } @res];
}

sub collect {
    my ($self, @contents) = @_;

    my $call = $self->service->Collect();
    for my $content (@contents) {
        my $req = GrpcSandbox::PB::Google::Showcase::V1beta1::EchoRequest->new({
            content => $content,
        });
        $call->write($req);
    }
    my $res = $call->wait;
    return $res->get_content;
}

Chatメソッドの実装

Chatメソッドは双方向ストリーミングを行ないます。1つずつリクエストを送ってはレスポンスを受け取るという形にしてみました。

sub chat {
    my ($self, @contents) = @_;

    my @res;
    my $call = $self->service->Chat();
    for my $content (@contents) {
        my $req = GrpcSandbox::PB::Google::Showcase::V1beta1::EchoRequest->new({
            content => $content,
        });
        $call->write($req);
        my $res = $call->read;
        push @res, $res->get_content;
    }
    $call->writesDone;
    return \@res;
}

Waitメソッドの実装

Waitメソッドは待ち時間を受け取りますが、即座にgoogle.longrunning.Operationを返します。google.longrunning.Operations serviceが実装されているのでGetOperationメソッドを定期的に呼び出し、そのoperationが終了したかどうかを確認するようにしてみました。

google.longrunning.Operationのresponseフィールドはgoogle.protobuf.Anyなので自分でWaitResponseにデコードする必要があります。

ちなみにgoogle.longrunning.Operationの詳しい仕様に関してはGoogle AIPsのAIP-151: Long-running operationsにあります。

sub wait {
    my ($self, $content, $ttl) = @_;

    my $req = GrpcSandbox::PB::Google::Showcase::V1beta1::WaitRequest->new({
        success => { content => $content },
        ttl     => { seconds => $ttl },
    });
    my $call = $self->service->Wait(argument => $req);
    my $res = $call->wait;
    while (1) {
        my ($res, $done) = $self->_get_operation($res->get_name);
        if ($done) {
            my $wait_res = GrpcSandbox::PB::Google::Showcase::V1beta1::WaitResponse->decode($res->get_value);
            return $wait_res->get_content;
        }
        sleep 1;
    }
}

sub _get_operation {
    my ($self, $name) = @_;

    my $operations_service = GrpcSandbox::PB::Google::Longrunning::Operations->new(
        $self->{server},
        credentials => $self->{credentials},
    );
    my $req = GrpcSandbox::PB::Google::Longrunning::GetOperationRequest->new({
        name => $name,
    });
    my $call = $operations_service->GetOperation(argument => $req);
    my $res = $call->wait;
    return $res->get_response, $res->get_done;
}

Blockメソッドの実装

Blockメソッドは受け取った待ち時間分、実際にsleepしてレスポンスを返すというサーバ側の実装になっていますが、ここではあまり関係ないのでエラーを返すときの例として紹介します。

実は->waitwantarrayでコンテキストに応じて返り値が変わるようになっており、リストコンテキストで受け取る場合にはレスポンスとstatusを返します。 このstatusというのはGrpc::XSの実装によるとcode, details, metadataというキーを持つhashrefが返され、このキーの順番にgoogle.rpc.Statusのcode, message, detailsに対応します(details→messageなので注意)。

sub block_error {
    my ($self, $delay, $content) = @_;

    my $req = GrpcSandbox::PB::Google::Showcase::V1beta1::BlockRequest->new({
        response_delay => { seconds => $delay },
        error          => {
            code    => GrpcSandbox::PB::Google::Rpc::Code::UNKNOWN,
            message => 'unknown error',
        },
    });
    my $call = $self->service->Block(argument => $req);
    my ($res, $status) = $call->wait;
    return {
        code    => $status->{code},
        details => $status->{details},
    };
}

最後にgRPCサーバと通信するテストを書きました。 動かすと裏で立ち上がっているGAPIC Showcaseのコンテナに対して通信します。

% docker-compose exec app bash
root@c271acf8b99d:/app# perl t/echo_service.t
# Subtest: echo
    ok 1
    ok 2
    1..2
ok 1 - echo
# Subtest: expand
    ok 1
    1..1
ok 2 - expand
# Subtest: collect
    ok 1
    1..1
ok 3 - collect
# Subtest: chat
    ok 1
    1..1
ok 4 - chat
# Subtest: paged_expand
    ok 1
    ok 2
    ok 3
    ok 4
    1..4
ok 5 - paged_expand
# Subtest: wait
    ok 1
    1..1
ok 6 - wait
# Subtest: block
    ok 1
    ok 2
    1..2
ok 7 - block
1..7
gapic-showcase_1  | 2021/02/06 19:36:44 Received Unary Request for Method: /google.showcase.v1beta1.Echo/Echo
gapic-showcase_1  | 2021/02/06 19:36:44     Request:  content:"hello"
gapic-showcase_1  | 2021/02/06 19:36:44     Returning Response: content:"hello"
gapic-showcase_1  | 2021/02/06 19:36:44
gapic-showcase_1  | 2021/02/06 19:36:44 Received Unary Request for Method: /google.showcase.v1beta1.Echo/Echo
gapic-showcase_1  | 2021/02/06 19:36:44     Request:  content:"world"
gapic-showcase_1  | 2021/02/06 19:36:44     Returning Response: content:"world"
gapic-showcase_1  | 2021/02/06 19:36:44
gapic-showcase_1  | 2021/02/06 19:36:44 Server Stream for Method: /google.showcase.v1beta1.Echo/Expand
gapic-showcase_1  | 2021/02/06 19:36:44     Receiving Message:  content:"hello world"
gapic-showcase_1  | 2021/02/06 19:36:44
gapic-showcase_1  | 2021/02/06 19:36:44 Server Stream for Method: /google.showcase.v1beta1.Echo/Expand
gapic-showcase_1  | 2021/02/06 19:36:44     Sending Message:  content:"hello"
gapic-showcase_1  | 2021/02/06 19:36:44
gapic-showcase_1  | 2021/02/06 19:36:44 Server Stream for Method: /google.showcase.v1beta1.Echo/Expand
gapic-showcase_1  | 2021/02/06 19:36:44     Sending Message:  content:"world"
<snip>

まとめ

これでPerlでgRPCでの一通りの通信はできました。思った以上にGoogle::ProtocolBuffers::DynamicとGrpc::XSの出来は良く、gRPC Transcodingに頼らずにgRPCで通信するのも選択肢としてはありかもしれません。

ただし動的なスタブコードを使って実装していくのは大変で、protoファイルを見ながら対応しているパッケージをちまちまと書いていく必要がありました。

Goでのprotoc-gen-goを使ったスタブコード生成をしてエディタの補完が効く快適な開発体験と比べると、PerlでもgRPCで通信するのはやっぱり辛いけどなんとかできる状態にはなっています。(プロダクションで使っている事例があれば教えてください)

OpenSSLはどこにいる

この記事は、はてなエンジニア Advent Calendarの15日目です。

qiita.com

OpenSSLに依存しているモジュールをインストールしようと思ったときにライブラリが見つからなくて困ることがあります。

例えばmacOS上でhomebrewを使ってOpenSSLをインストールした場合は /usr/local/opt/openssl以下に配置されるため、モジュールのインストール時にインクルードパスとライブラリパスを指定しないといけません。 cpanmであれば--configure-args経由でビルドする際の引数を渡すことができますが、コンパイラにどのように渡されるかはMakefile.PLを読む必要があります。大変ですね。

そこで作ったのが、いいかんじにOpenSSLのパスを取ってきてくれるモジュールCrypt::OpenSSL::Guessです。

metacpan.org

これはNet::SSLeayで使われているOpenSSLのパス解決部分をモジュール化したものです。このモジュールは現在Crypt::OpenSSL::RSACrypt::OpenSSL::Randomで使われています。

使い方はMakefile.PL内に以下のように書くだけです。

use ExtUtils::MakerMaker;
use Crypt::OpenSSL::Guess;
 
WriteMakefile(
    # ...
    LIBS => ['-lssl -lcrypto ' . openssl_lib_paths()],
    INC  => openssl_inc_paths(),
);

実装は素直で、パスを順番に探して見つかったものを返すというものです。 この中に含まれていない場合でも簡単にパスを指定できるように、環境変数OPENSSL_PREFIXに設定できるようにしています。

sub find_openssl_prefix {
    my ($dir) = @_;
 
    if (defined $ENV{OPENSSL_PREFIX}) {
        return $ENV{OPENSSL_PREFIX};
    }
 
    my @guesses = (
        '/home/linuxbrew/.linuxbrew/opt/openssl/bin/openssl' => '/home/linuxbrew/.linuxbrew/opt/openssl', # LinuxBrew openssl
        '/usr/local/opt/openssl/bin/openssl' => '/usr/local/opt/openssl', # OSX homebrew openssl
        '/usr/local/bin/openssl'         => '/usr/local', # OSX homebrew openssl
        '/opt/local/bin/openssl'         => '/opt/local', # Macports openssl
        '/usr/bin/openssl'               => '/usr',
        '/usr/sbin/openssl'              => '/usr',
        '/opt/ssl/bin/openssl'           => '/opt/ssl',
        '/opt/ssl/sbin/openssl'          => '/opt/ssl',
        '/usr/local/ssl/bin/openssl'     => '/usr/local/ssl',
        '/usr/local/openssl/bin/openssl' => '/usr/local/openssl',
        '/apps/openssl/std/bin/openssl'  => '/apps/openssl/std',
        '/usr/sfw/bin/openssl'           => '/usr/sfw', # Open Solaris
        'C:\OpenSSL\bin\openssl.exe'     => 'C:\OpenSSL',
        'C:\OpenSSL-Win32\bin\openssl.exe'        => 'C:\OpenSSL-Win32',
        $Config{prefix} . '\bin\openssl.exe'      => $Config{prefix},           # strawberry perl
        $Config{prefix} . '\..\c\bin\openssl.exe' => $Config{prefix} . '\..\c', # strawberry perl
        '/sslexe/openssl.exe'            => '/sslroot'# VMS, openssl.org
        '/ssl$exe/openssl.exe'           => '/ssl$root', # VMS, HP install
    );
 
    while (my $k = shift @guesses
           and my $v = shift @guesses) {
        if ( -x $k ) {
            return $v;
        }
    }
    (undef, $dir) = check_no_path()
       and return $dir;
 
    return;
}

PerlにはThere's more than one way to do it(やり方はいくつもある)というモットーがありますが、それに続くbut sometimes consistency is not a bad thing either(ときには共通のやり方があっても悪くはない)という言葉があるように、各モジュールが行っていたOpenSSLのパスの解決の仕組みを共通のやり方で出来るようにモジュール化したという話でした。

UCSB iCTF 2018 - fantasticiot, hero_text_adventure

attack and deference形式のCTFにオンラインで参加した。dodododoは19位。

f:id:akiym:20180318184839p:plain

sshできるサーバが1台与えられて、そこで8つのサービスを正しく動かしつつ、攻撃と防御を行う。
flagは運営からサービスが正しく動いているかどうかの確認と一緒に送られてくる。例えば秘密のメモアプリみたいなのがあったとして、正規の方法だとメモの閲覧にはパスワードが必要だけど、サービスに残された脆弱性を使うと任意のメモを見られるになればflagを入手して提出することで攻撃できる。
サービスはxinetd経由で起動するようになっていて、サービスの脆弱性を潰すためバイナリを書き換えたり、途中にバリデーション用のスクリプトを挟んだりしてflagを持ち出されないように防御できる。

このCTF専用のサービス一覧/参加チーム取得やフラグ提出用のクライアントがあって、それを使うと手軽に攻撃の自動化ができる。 事前にサンプル が渡されたのだけど、結構ミスっていて困っていた。事前にテストして欲しい……

fantasticiot

% file ./fantasticiot
./fantasticiot: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=baa3e0fa48be4715a10784c2dd5e7d3adb5d9181, not stripped

冷蔵庫サービス。static linkされていて一瞬ぎょっとするけど、中身は単純にJSON経由で冷蔵庫に入れたりすることができるサービス。flagはsetflagで保存されたflag。

  • setflag
    • {"token": "A", "flag": "B", "id": "2", "service": "flag", "op": "setflag"}
    • flag/1 に書き込む
  • getflag
    • {"token": "A", "id": "1", "service": "flag", "op": "getflag"}
    • flag/1 から取得する
    • ただし正しいtokenである必要がある
  • addfridge
    • {"content": "AAAAAAAAAA", "item": "1", "service": "fridge", "op": "addfridge"}
    • fridge/1 に書き込む
  • getfridge
    • {"item": "1", "service": "fridge", "op": "getfridge"}
    • fridge/1 から取得する

脆弱性

  • getfridgeの際にserviceを flag に指定することでディレクトリを変更できる
  • getfridgeの際にitemを ../flag/1 にすることでディレクトリを変更できる

こういう雑なバリデータを挟むことで回避できる。

import sys
import json

src = sys.stdin.readline()

try:
    data = json.loads(src)
    if data['op'] == 'getfridge':
        assert data['service'] == 'fridge'
        assert '/' not in data['item']
    sys.stdout.write(src)
except:
    pass
  • strncmpにbackdoorが仕掛けられている

static linkされているのはこういうことだったのか!上位チームでも意外と気づかなかったチームが結構いた。

f:id:akiym:20180318184751p:plain

つまり、getflagの際にtokenを victor に指定すればtokenチェックが問答無用に突破可能。

def exp(s, flag_id):
    payload = {
        "service": "flag",
        "op": "getflag",
        "id": flag_id,
        "token": "victor",
    }
    s.send(json.dumps(payload) + '\n')
    return json.loads(s.recvuntil('\n'))['flag']

hero_text_adventure

% file ./hero_text_adventure
./hero_text_adventure: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=98aac0bf05483d6328b1640625404c533c785999, not stripped
% ./hero_text_adventure
Hello superhero, are you continuing your previous adventure? (y/N)
n
Welcome to the beta test of the Marvel Super Hero text adventure!
Enter your name:
test
Pick a character
1) Star Lord
2) Dr Strange
3) wanda maximoff
4) Hulk
5) Thor
1
test, what would you like to do?
1) BATTLE!
2) buy weapon
3) equip weapon
4) give weapon a new name
5) load game
6) exit
6
Exiting. Save game? (y/N)
y
enter a password to protect your save file
secretpassword!
id of your adventure: 7906397325484763355

アドベンチャーゲーム。戦ったり、武器を買ったり、データをロード/セーブできる。flagはセーブされているデータのname部分。

脆弱性

  • weaponのhandlerのアドレス書き換え
    • 1) BATTLE! のときに任意のアドレスに対して飛べるようになる
    • readlineの最初(0x400C8C)に飛ぶことで、bssのプレイヤー名のあるアドレス(0x603240)以降の書き換えができる
    • そこから装備中の武器のアドレスを書き換えると、 4) give weapon a new name から任意のアドレスへの書き込みができるようになる
    • GOTにあるstrcmpをstrchrに書き換えるとデータのロード時のパスワードチェックで、strcmpが strchr("real_password", "input") になり、これはNULLを返すので任意のデータをロードできるようになりflagが入手できる

exploit: hero_text_adventure · GitHub

防御に関しては、give weapon a new nameのときのreadlineが36バイト読み込むのを32バイトだけ読み込むようにバイナリを変更した。

感想

自分の現在の順位よりも上のチームにしか攻撃できない(flagの提出もできず、サーバへのアクセスも遮断されている)ようになっていて珍しいルールだった。よくあるA&D形式のCTFだと防御がボロボロだと上位チームに攻撃されて、点数が減る一方で最後までやる気がなくなってしまうのだけど、これならまだ最後までできる。
一見よいルールに見えるのだけど、順位をわざと下げて攻撃対象を増やして、一度に大量の得点を取得することが可能で(もちろん攻撃が塞がれてなければだけど)割と入れ替わりが激しく、崩壊していた気がする。

今回はRuCTFEのようにVPNの提供はなく、サーバが与えられて手元から他チームに攻撃するときはsocks経由でやってくれ、というかんじだったので運営のことを考えると準備の手間をあまり考える必要なく、これなら手軽にA&D形式のCTFを開催できるのではという気がした。
ただチェックシステムを準備するのが大変という話はありそうだけど、ちょっと調べたらRuCTFEで使われているコードが公開されていた。A&D形式のCTF増えて欲しい。

github.com

YAPC::Okinawaで「Perlコーディングテクニック2018」という話をしました

speakerdeck.com

最近の便利Perl情報や好きなモジュールの話をしました。 トーク応募したときには話したかった細かい話題がいくつかあったのですが、20分では収まらなかったのもありクラスビルダやクラスローダ、バリデータ、Type::Tinyの話になりました。

トークの中で紹介した、拙作のSmart::Args::TypeTinyが結構好きで最近よく使っています。

Smart::Args::TypeTiny - We are smart, smart for you - metacpan.org

詳しい使い方やSmart::Argsと比較したときのメリットをあまり説明できなかったのですが、Smart::Args::TypeTinyについては昨年 id:papix さんが紹介している記事があります。(ありがとうございます!!!)

papix.hatenablog.com

自分のトーク後の id:shoichikaji さんのトークの冒頭では、手元環境ではstricturesのPERL_STRICTURES_EXTRAを設定しているという話がありました。便利ですね。

strictures - turn on strict and make most warnings fatal - metacpan.org

参加者の皆様並び運営スタッフの皆様お疲れ様でした。次回も楽しみにしています。