All Articles

UIUCTF 2023 Writeup

7/1 から開催されていた UIUCTF 2023 に 0nePadding で参加して 124位 / 818 チームでした。

image-20230705210351400

今回は Rev と OSINT を解きましたが、課題感の残るコンテストでした。

とりあえず Rev の問題の Writeup を 2 問分書きます。

vmwhere1(Rev)

問題バイナリとして vm という ELF ファイルと、 program というバイナリファイルが与えられます。

Ghidra で解析すると vm は program のバイトデータを読み取り、そのバイト値に応じた処理を行っていることがわかります。

実際のデコンパイル結果は以下のようになっていました。

undefined8 check(byte *param_1,int param_2)

{
  byte *pbVar1;
  byte bVar2;
  byte bVar3;
  int iVar4;
  byte *pbVar5;
  uint local_24;
  byte *local_20;
  byte *local_18;
  
  pbVar5 = (byte *)malloc(0x1000);
  local_20 = param_1;
  local_18 = pbVar5;
  while( true ) {
    if ((local_20 < param_1) || (param_1 + param_2 <= local_20)) {
      printf("Program terminated unexpectedly. Last instruction: 0x%04lx\n",
             (long)local_20 - (long)param_1);
      return 1;
    }
    pbVar1 = local_20 + 1;
    switch(*local_20) {
    case 0:
      return 0;
    case 1:
      local_18[-2] = local_18[-2] + local_18[-1];
      local_18 = local_18 + -1;
      local_20 = pbVar1;
      break;
    case 2:
      local_18[-2] = local_18[-2] - local_18[-1];
      local_18 = local_18 + -1;
      local_20 = pbVar1;
      break;
    case 3:
      local_18[-2] = local_18[-2] & local_18[-1];
      local_18 = local_18 + -1;
      local_20 = pbVar1;
      break;
    case 4:
      local_18[-2] = local_18[-2] | local_18[-1];
      local_18 = local_18 + -1;
      local_20 = pbVar1;
      break;
    case 5:
      local_18[-2] = local_18[-2] ^ local_18[-1];
      local_18 = local_18 + -1;
      local_20 = pbVar1;
      break;
    case 6:
      local_18[-2] = local_18[-2] << (local_18[-1] & 0x1f);
      local_18 = local_18 + -1;
      local_20 = pbVar1;
      break;
    case 7:
      local_18[-2] = (byte)((int)(uint)local_18[-2] >> (local_18[-1] & 0x1f));
      local_18 = local_18 + -1;
      local_20 = pbVar1;
      break;
    case 8:
      iVar4 = getchar();
      *local_18 = (byte)iVar4;
      local_18 = local_18 + 1;
      local_20 = pbVar1;
      break;
    case 9:
      local_18 = local_18 + -1;
      putchar((uint)*local_18);
      local_20 = pbVar1;
      break;
    case 10:
      *local_18 = *pbVar1;
      local_18 = local_18 + 1;
      local_20 = local_20 + 2;
      break;
    case 0xb:
      if ((char)local_18[-1] < '\0') {
        pbVar1 = pbVar1 + CONCAT11(*pbVar1,local_20[2]);
      }
      local_20 = pbVar1;
      local_20 = local_20 + 2;
      break;
    case 0xc:
      if (local_18[-1] == 0) {
        pbVar1 = pbVar1 + CONCAT11(*pbVar1,local_20[2]);
      }
      local_20 = pbVar1;
      local_20 = local_20 + 2;
      break;
    case 0xd:
      local_20 = pbVar1 + (long)CONCAT11(*pbVar1,local_20[2]) + 2;
      break;
    case 0xe:
      local_18 = local_18 + -1;
      local_20 = pbVar1;
      break;
    case 0xf:
      *local_18 = local_18[-1];
      local_18 = local_18 + 1;
      local_20 = pbVar1;
      break;
    case 0x10:
      local_20 = local_20 + 2;
      bVar2 = *pbVar1;
      if ((long)local_18 - (long)pbVar5 < (long)(ulong)bVar2) {
        printf("Stack underflow in reverse at 0x%04lx\n",(long)local_20 - (long)param_1);
      }
      for (local_24 = 0; (int)local_24 < (int)(uint)(bVar2 >> 1); local_24 = local_24 + 1) {
        bVar3 = local_18[(int)(local_24 - bVar2)];
        local_18[(int)(local_24 - bVar2)] = local_18[(int)~local_24];
        local_18[(int)~local_24] = bVar3;
      }
      break;
    default:
      printf("Unknown opcode: 0x%02x at 0x%04lx\n",(ulong)*local_20,(long)local_20 - (long)param_1);
      return 1;
    case 0x28:
      FUN_00101370(param_1,pbVar5,local_18,(long)pbVar1 - (long)param_1);
      local_20 = pbVar1;
    }
    if (local_18 < pbVar5) break;
    if (pbVar5 + 0x1000 < local_18) {
      printf("Stack overflow at 0x%04lx\n",(long)local_20 - (long)param_1);
      return 1;
    }
  }
  printf("Stack underflow at 0x%04lx\n",(long)local_20 - (long)param_1);
  return 1;
}

このコードを見ると、Hex が 0x08 のときに getchar 関数が呼び出され、入力値を 1 文字ずつ取得していることがわかります。

そこで、program から 0x08 の命令を抽出したところ、以下のように周期性があることがわかりました。

image-20230703183714428

以降のバイト列を追ってみると、0xf の処理で入力値を保存した後、0x7 と 0x5 の処理で変換を行い、ハードコードされているバイト値と XOR した後 0xc の処理で 1 文字ずつ検証していることがわかります。

ここまで特定できれば、あとは動的解析で総当たりを試すことができます。

以下のスクリプトで 0xc の条件式を満たすような文字を先頭から 1 文字ずつ特定していくことで Flag を取得できました。

# gdb -x solver.py
import gdb
from pprint import pprint

# pprint(dir(gdb))
BINDIR = "/home/ubuntu/Hacking/CTF/2023/UIUCTF/Rev/vmwhere1/"
BIN = "chal"

gdb.execute('file {}/{}'.format(BINDIR, BIN))
# gdb.execute('b *{}'.format(0x555555555587))
gdb.execute('b *{}'.format(0x55555555569f))

flag = "uiuctf{" + "A"*150
for i in range(150):
    for j in range(0x21,0x126):
        flag = flag[:i] + chr(j) + "A"*(30-i)
        with open("in.txt", "w") as f:
            f.write(flag)
        
        gdb.execute("run program < in.txt")
        gdb.execute("continue {}".format(51+i))
        res = int(gdb.parse_and_eval("$al"))
        if res == 0:
            print(flag)
            print(chr(j),flag)            
            break
        else:
            continue

print(flag)
gdb.execute('quit')

静的解析でちゃんと解くパターン

vmwhere1 については最低限 0xc で 1 文字ずつ検証しているという点さえ特定できれば Flag 取得まで行けましたが、そのまま進んでしまうと vmwhere2 で詰みます。

そのため、vmwhere1 の時点でもう少しバイナリの動作を理解するために深掘りしていきます。

vm の動作を理解するには、0x28 の処理で呼び出される以下の関数がヒントになっています。

void FUN_00101370(undefined8 param_1,long param_2,long param_3,undefined8 param_4)
{
  int local_c;
  
  param_3 = param_3 + -1;
  printf("Program counter: 0x%04lx\n",param_4);
  printf("Stack pointer: 0x%04lx\n",param_3 - param_2);
  puts("Stack:");
  for (local_c = 0; local_c < 0x10; local_c = local_c + 1) {
    if (-1 < (param_3 - param_2) - (long)local_c) {
      printf("0x%04lx: 0x%04x\n",(param_3 - param_2) - (long)local_c,
             (ulong)*(byte *)(param_3 - local_c));
    }
  }
  return;
}

実際に適当に Stack に値を積んだ後に 0x28 の処理を呼び出すバイナリを作成して実行してみると、以下のような出力を得ることができます。

ここでわかるのは、param_3 として与えている変数がスタックポインタになっているという点です。

echo -n -e "\x0a\x21\x0a\x22\x0a\x23\x0a\x24\x28" > test
./chal test
>
Program counter: 0x0009
Stack pointer: 0x0003
Stack:
0x0003: 0x0024
0x0002: 0x0023
0x0001: 0x0022
0x0000: 0x0021
Program terminated unexpectedly. Last instruction: 0x0009

スタックポインタとプログラムカウンタを特定できたので、あとは必要な処理を読み解いていきます。

0x8 で Flag の 1 文字を読み込んだ後の処理は以下のようになっていました。

08 0f 0a 04 07 05 05 0f 0a 72 05 0c 00 03 0d 04 0d 0e

とりあえず 0xc までの処理を Python に落とし込んでみると以下のようになりました。

op = prog[pic]

if op == 1:
    a = stack.pop()
    b = stack.pop()
    stack.append(a+b)
    pic += 1

if op == 5:
    a = stack.pop()
    b = stack.pop()
    n = a ^ b
    stack.append(n)
    pic += 1

if op == 7:
    a = stack.pop()
    b = stack.pop()
    n = b >> (a & 0x1f)
    stack.append(n)
    pic += 1

if op == 8:
    c = flag[seek]
    seek += 1
    stack.append(c)
    pic += 1

if op == 0xa:
    data = prog[pic+1]
    stack.append(data)
    pic += 2

if op == 0xf:
    stack.append(stack[-1])
    pic += 1

順番を置き換えると以下のようになります。

  • 入力値を 1 文字取得し、その値を Stack に追加
  • 同じ値をもう 1 つ Stack に追加
  • 0x4 を Stack に追加
  • 0x4 & 0x1f した値で入力値を右シフトして Stack に格納(入力文字の上位ビットのみを取得)
  • 上記の値と入力値を XOR して Stack に格納(上位ビットはそのまま、下位ビットのみ XOR される)
  • 入力値の取得前に Stack に積んであった値(初期値は 0x00 ) と上記の値を XOR
  • 上記の結果と同じ値をもう 1 つ Stack に追加
  • 0x72 を Stack に追加(この値は毎回変わり、次の検証にも使用される)
  • 0x72 と先ほどの演算結果を XOR して Stack に追加
  • 1 つ前の演算結果(入力値の取得前に Stack に積んであった値(0x00) と上記の値を XOR) が 0 かどうかを判定し、0 なら正解とする

実際にこの処理を演算してみると、以下のように 1 文字目が u の場合は正解の 0 が返却されることがわかります。

image-20230703220020748

以上の処理は、0a 72 で Stack に積む値以外は共通で実行されます。

つまり、各文字ごとにハードコードされた検証用の値を抽出することでただしい Flag を逆算することができそうです。

image-20230703220312435

以下のようなスクリプトでこれらの Key を取得できます。

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

pattern = b"\x05\x0f\x0a"
key = []
for i in range(3, len(prog)):
    if prog[i-3:i] == pattern:
        key.append(prog[i])

print(key)

そして、解析した結果を元に以下の Solver を実行することで正しい結果を取得できました。

chr((a>>4)^a) で Flag を復号できているのは、正しい入力値の上位ビットはそのままにして、下位ビットを上位ビットで XOR した値を使用しているためです。

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

pattern = b"\x05\x0f\x0a"
key = []
for i in range(3, len(prog)):
    if prog[i-3:i] == pattern:
        key.append(prog[i])

# print([hex(i) for i in key])

flag = ""
key = key[::-1]
for i in range(len(key)-1):
    k = key[i]
    a = k ^ key[i+1]
    flag += chr((a>>4)^a)
print("c" + flag[::-1])

# ciuctf{ar3_y0u_4_r3al_vm_wh3r3_(gpt_g3n3r4t3d_th1s_f14g)}

vmwhere2(Rev)

問題バイナリとして vmwhere1 とほぼ同じバイナリと新しい program ファイルが与えられます。

vmwhere1 とは異なり、ELF ファイルには以下の 2 つの処理が追加されていました。

case 0x11:
  local_30 = sp[-1];
  for (j = 0; j < 8; j = j + 1) {
    (sp + -1)[j] = local_30 & 1;
    local_30 = local_30 >> 1;
  }
  sp = sp + 7;
  current_pic = next_pic;
  break;

case 0x12:
  local_2f = 0;
  for (k = 7; -1 < k; k = k + -1) {
    local_2f = local_2f << 1 | (sp + -8)[k] & 1;
  }
  sp[-8] = local_2f;
  sp = sp + -7;
  current_pic = next_pic;
  break;

0x11 の処理の方は、入力された文字を 1 bit ずつ Stack に積んでいく処理であることがわかります。

また、0x12 の方は 0x11 で格納した bit 列を逆順に並べ替えて再格納しているようです。

その他の処理は見た感じ vmwhere1 と同じのようなので、次に program の方を見ていきます。

vmwhere2 の program では、vmwhere1 とは異なりすべての文字入力を受け取ってから Flag の検証を行っているようです。

まずは、先ほど同様 0x08 で文字入力を受けとったあとの処理を見てみます。

image-20230703233629823

1 文字目の入力を受け取る処理は以下のようになっていました。

先ほどと比べるとかなり多いですね。

08 11 0a ff 10 09 10 08 0a 00 10 02 0f 0a ff 05 0c 00 04 0e 0d 00 04 0e 0d 00 16 10 02 10 02 0c 00 07 0e 0a 01 01 0d 00 01 0e 0f 0f 01 01 0d ff d9 0e

各処理について整理するために、オペコードとオペランドで整列してみました。

08
11
0a ff
10 09
10 08
0a 00
10 02
0f
0a ff
05
0c 00 04 
0e 
0d 00 04
0e 0d 00 16 
10 02 
10 02 
0c 00 07 0e 0a 01 01 0d 00 01 
0e 
0f 
0f 
01 
01 
0d ff d9
0e
  • 受け取った入力値は、0x11 の処理で bit 列として Stack に格納される
  • Stack に 0xff が積まれる
  • 0x10 の処理で 0x9 を引数として Stack の操作を行う
  • 連続して 0x10 の処理で 0x8 を引数として Stack の操作を行う
  • 0x0 を Stack に積む
  • 0x10 の処理を 0x2 を引数として呼び出す
  • Stack の最上位の値をもう一つ Stack に積む
  • 0xff を Stack に積む
  • Stack の上 2 つを XOR する
  • 2 つ分処理を進めて 0xd の処理に進む
  • 0x10 の処理を 0x2 を引数をして 2 回行う
  • 0xe の処理まで進める
  • Stack にいくつかの値おを積んだ後、0xd の処理で -0x25 の位置までプログラムカウンタを戻す((0x55555555a535+0xffffffffffffffd9) & 0xffffffffffffffff の演算を行う)

ここまで読んだものの、以降の処理量が膨大過ぎて処理を追うのを諦めました。

仕方がないので、バイナリのコードを再実装して Print デバッグをしながら挙動を確認することにしました。

実装力が足りず、自分が書いた Python スクリプトでは動作を完全には再現することができなかったので C 言語で書くことにしました。

作成したコードは以下の通りです。

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

void main()
{
    char bVar1;
    char bVar2;
    int iVar3;
    char *stack;
    char local_30;
    char local_2f;
    uint i;
    int j;
    int k;
    char *current_pic;
    char *sp;
    char *next_pic;

    FILE *f;
    void *prog;
    long f_size;

    // Read file
    f = fopen("program","r");
    if (f == (FILE *)0x0) {
        prog = (void *)0x0;
    }
    else {
        fseek(f,0,2);
        f_size = ftell(f);
        rewind(f);
        prog = malloc(f_size);
        if (prog == (void *)0x0) {
            prog = (void *)0x0;
        }
        else {
            fread(prog,1,f_size,f);
            fclose(f);
        }
    }

    // Run program
    stack = (char *)malloc(0x1000);
    current_pic = prog;
    int start = (int)current_pic;
    sp = stack;
    while(1) {
    if ((current_pic < prog) || (prog + f_size <= current_pic)) {
        printf("Program terminated unexpectedly. Last instruction: 0x%04lx\n",
                (long)current_pic - (long)prog);
        return 1;
    }
    printf("[0x%x] OP:0x%x  ", current_pic - start, *current_pic);
    next_pic = current_pic + 1;
    switch(*current_pic) {
    case 0:
        return 0;
    case 1:
        printf("ADD %x, %x = %x\n", sp[-2], sp[-1], sp[-2] + sp[-1]);
        sp[-2] = sp[-2] + sp[-1];
        sp = sp + -1;
        current_pic = next_pic;
        break;
    case 2:
        printf("SUB %x, %x = %x\n", sp[-2], sp[-1], sp[-2] - sp[-1]);
        sp[-2] = sp[-2] - sp[-1];
        sp = sp + -1;
        current_pic = next_pic;
        break;
    case 3:
        printf("AND %x, %x = %x\n", sp[-2], sp[-1], sp[-2] & sp[-1]);
        sp[-2] = sp[-2] & sp[-1];
        sp = sp + -1;
        current_pic = next_pic;
        break;
    case 4:
        printf("OR %x, %x = %x\n", sp[-2], sp[-1], sp[-2] | sp[-1]);
        sp[-2] = sp[-2] | sp[-1];
        sp = sp + -1;
        current_pic = next_pic;
        break;
    case 5:
        printf("XOR %x, %x = %x\n", sp[-2], sp[-1], sp[-2] ^ sp[-1]);
        sp[-2] = sp[-2] ^ sp[-1];
        sp = sp + -1;
        current_pic = next_pic;
        break;
    case 6:
        printf("LSH %x, %x = %x\n", sp[-2], (sp[-1] & 0x1f), sp[-2] >> (sp[-1] & 0x1f));
        sp[-2] = sp[-2] << (sp[-1] & 0x1f);
        sp = sp + -1;
        current_pic = next_pic;
        break;
    case 7:
        printf("RSH %x, %x = %x\n", sp[-2], (sp[-1] & 0x1f), sp[-2] << (sp[-1] & 0x1f));
        sp[-2] = (char)((int)(uint)sp[-2] >> (sp[-1] & 0x1f));
        sp = sp + -1;
        current_pic = next_pic;
        break;
    case 8:
        iVar3 = getchar();
        *sp = (char)iVar3;
        printf("READ %c\n", *sp);
        sp = sp + 1;
        current_pic = next_pic;
        break;
    case 9:
        sp = sp + -1;
        printf("PUTCHAR %c\n", *sp);
        // putchar((uint)*sp);
        current_pic = next_pic;
        break;
    case 10:
        printf("PUSH %x\n", *next_pic);
        *sp = *next_pic;
        sp = sp + 1;
        current_pic = current_pic + 2;
        break;
    case 0xb:
        if ((char)sp[-1] < '\0') {
            next_pic = next_pic + (*next_pic << 8 | current_pic[2]);
        }
        current_pic = next_pic;
        current_pic = current_pic + 2;
        printf("JPM %x\n", current_pic);
        break;
    case 0xc:
        if (sp[-1] == 0) {
            next_pic = next_pic + (*next_pic << 8 | current_pic[2]);
        }
        current_pic = next_pic;
        current_pic = current_pic + 2;
        printf("JPM %x\n", current_pic);
        break;
    case 0xd:
        current_pic = next_pic + (*next_pic << 8 | current_pic[2]) + 2;
        printf("JPM %x\n", current_pic);
        break;
    case 0xe:
        printf("POP\n");
        sp = sp + -1;
        current_pic = next_pic;
        break;
    case 0xf:
        printf("DUP %x\n", sp[-1]);
        *sp = sp[-1];
        sp = sp + 1;
        current_pic = next_pic;
        break;
    case 0x10:
        printf("REVERSE TOP %x\n", *next_pic);
        current_pic = current_pic + 2;
        bVar1 = *next_pic;
        if ((long)sp - (long)stack < (long)(ulong)bVar1) {
            printf("Stack underflow in reverse at 0x%04lx\n",(long)current_pic - (long)prog);
        }
        for (i = 0; (int)i < (int)(uint)(bVar1 >> 1); i = i + 1) {
            bVar2 = sp[(int)(i - bVar1)];
            sp[(int)(i - bVar1)] = sp[(int)~i];
            sp[(int)~i] = bVar2;
        }
        break;
    case 0x11:
        printf("SPLIT BYTE TO BITS\n");
        local_30 = sp[-1];
        for (j = 0; j < 8; j = j + 1) {
        (sp + -1)[j] = local_30 & 1;
        local_30 = local_30 >> 1;
        }
        sp = sp + 7;
        current_pic = next_pic;
        break;
    case 0x12:
        printf("POP 8 VALUES, NEW VALUE = LSB OF LAST 8\n");
        local_2f = 0;
        for (k = 7; -1 < k; k = k + -1) {
        local_2f = local_2f << 1 | (sp + -8)[k] & 1;
        }
        sp[-8] = local_2f;
        sp = sp + -7;
        current_pic = next_pic;
        break;
    default:
        printf("Unknown opcode: 0x%02x at 0x%04lx\n",(ulong)*current_pic,
                (long)current_pic - (long)prog);
        return 1;
    }
    if (sp < stack) break;
    if (stack + 0x1000 < sp) {
        printf("Stack overflow at 0x%04lx\n",(long)current_pic - (long)prog);
        return 1;
    }
    }
    printf("Stack underflow at 0x%04lx\n",(long)current_pic - (long)prog);
    return 1;
}

これをコンパイルして実行すると、最終的にオフセット 0xbed の命令まで実行し、Incorrect password! という文字列を出力してくれました。

image-20230705184531728

これで、実際に実行される問題プログラムの挙動を追うことができそうです。

この Print デバッグ結果から、先ほどまで追っていた 1 文字取得後の処理を抜き出すと以下のようになります。

[0x74] OP:0xa  PUSH 0
[0x76] OP:0x8  READ u
[0x77] OP:0x11  SPLIT BYTE TO BITS
[0x78] OP:0xa  PUSH ffffffff
[0x7a] OP:0x10  REVERSE TOP 9
[0x7c] OP:0x10  REVERSE TOP 8
[0x7e] OP:0xa  PUSH 0
[0x80] OP:0x10  REVERSE TOP 2
[0x82] OP:0xf  DUP 0
[0x83] OP:0xa  PUSH ffffffff
[0x85] OP:0x5  XOR 0, ffffffff = ffffffff
[0x86] OP:0xc  JPM f117f519
[0x89] OP:0xe  POP
[0x8a] OP:0xd  JPM f117f521
[0x91] OP:0x10  REVERSE TOP 2
[0x93] OP:0x10  REVERSE TOP 2
[0x95] OP:0xc  JPM f117f52f
[0x9f] OP:0xe  POP
[0xa0] OP:0xf  DUP 0
[0xa1] OP:0xf  DUP 0
[0xa2] OP:0x1  ADD 0, 0 = 0
[0xa3] OP:0x1  ADD 0, 0 = 0
[0xa4] OP:0xd  JPM f117f510
[0x80] OP:0x10  REVERSE TOP 2
[0x82] OP:0xf  DUP 1
[0x83] OP:0xa  PUSH ffffffff
[0x85] OP:0x5  XOR 1, ffffffff = fffffffe
[0x86] OP:0xc  JPM f117f519
[0x89] OP:0xe  POP
[0x8a] OP:0xd  JPM f117f521
[0x91] OP:0x10  REVERSE TOP 2
[0x93] OP:0x10  REVERSE TOP 2
[0x95] OP:0xc  JPM f117f528
[0x98] OP:0xe  POP
[0x99] OP:0xa  PUSH 1
[0x9b] OP:0x1  ADD 0, 1 = 1
[0x9c] OP:0xd  JPM f117f530
[0xa0] OP:0xf  DUP 1
[0xa1] OP:0xf  DUP 1
[0xa2] OP:0x1  ADD 1, 1 = 2
[0xa3] OP:0x1  ADD 1, 2 = 3
[0xa4] OP:0xd  JPM f117f510
[0x80] OP:0x10  REVERSE TOP 2
[0x82] OP:0xf  DUP 1
[0x83] OP:0xa  PUSH ffffffff
[0x85] OP:0x5  XOR 1, ffffffff = fffffffe
[0x86] OP:0xc  JPM f117f519
[0x89] OP:0xe  POP
[0x8a] OP:0xd  JPM f117f521
[0x91] OP:0x10  REVERSE TOP 2
[0x93] OP:0x10  REVERSE TOP 2
[0x95] OP:0xc  JPM f117f528
[0x98] OP:0xe  POP
[0x99] OP:0xa  PUSH 1
[0x9b] OP:0x1  ADD 3, 1 = 4
[0x9c] OP:0xd  JPM f117f530
[0xa0] OP:0xf  DUP 4
[0xa1] OP:0xf  DUP 4
[0xa2] OP:0x1  ADD 4, 4 = 8
[0xa3] OP:0x1  ADD 4, 8 = c
[0xa4] OP:0xd  JPM f117f510
[0x80] OP:0x10  REVERSE TOP 2
[0x82] OP:0xf  DUP 1
[0x83] OP:0xa  PUSH ffffffff
[0x85] OP:0x5  XOR 1, ffffffff = fffffffe
[0x86] OP:0xc  JPM f117f519
[0x89] OP:0xe  POP
[0x8a] OP:0xd  JPM f117f521
[0x91] OP:0x10  REVERSE TOP 2
[0x93] OP:0x10  REVERSE TOP 2
[0x95] OP:0xc  JPM f117f528
[0x98] OP:0xe  POP
[0x99] OP:0xa  PUSH 1
[0x9b] OP:0x1  ADD c, 1 = d
[0x9c] OP:0xd  JPM f117f530
[0xa0] OP:0xf  DUP d
[0xa1] OP:0xf  DUP d
[0xa2] OP:0x1  ADD d, d = 1a
[0xa3] OP:0x1  ADD d, 1a = 27
[0xa4] OP:0xd  JPM f117f510
[0x80] OP:0x10  REVERSE TOP 2
[0x82] OP:0xf  DUP 0
[0x83] OP:0xa  PUSH ffffffff
[0x85] OP:0x5  XOR 0, ffffffff = ffffffff
[0x86] OP:0xc  JPM f117f519
[0x89] OP:0xe  POP
[0x8a] OP:0xd  JPM f117f521
[0x91] OP:0x10  REVERSE TOP 2
[0x93] OP:0x10  REVERSE TOP 2
[0x95] OP:0xc  JPM f117f52f
[0x9f] OP:0xe  POP
[0xa0] OP:0xf  DUP 27
[0xa1] OP:0xf  DUP 27
[0xa2] OP:0x1  ADD 27, 27 = 4e
[0xa3] OP:0x1  ADD 27, 4e = 75
[0xa4] OP:0xd  JPM f117f510
[0x80] OP:0x10  REVERSE TOP 2
[0x82] OP:0xf  DUP 1
[0x83] OP:0xa  PUSH ffffffff
[0x85] OP:0x5  XOR 1, ffffffff = fffffffe
[0x86] OP:0xc  JPM f117f519
[0x89] OP:0xe  POP
[0x8a] OP:0xd  JPM f117f521
[0x91] OP:0x10  REVERSE TOP 2
[0x93] OP:0x10  REVERSE TOP 2
[0x95] OP:0xc  JPM f117f528
[0x98] OP:0xe  POP
[0x99] OP:0xa  PUSH 1
[0x9b] OP:0x1  ADD 75, 1 = 76
[0x9c] OP:0xd  JPM f117f530
[0xa0] OP:0xf  DUP 76
[0xa1] OP:0xf  DUP 76
[0xa2] OP:0x1  ADD 76, 76 = ec
[0xa3] OP:0x1  ADD 76, ffffffec = 62
[0xa4] OP:0xd  JPM f117f510
[0x80] OP:0x10  REVERSE TOP 2
[0x82] OP:0xf  DUP 0
[0x83] OP:0xa  PUSH ffffffff
[0x85] OP:0x5  XOR 0, ffffffff = ffffffff
[0x86] OP:0xc  JPM f117f519
[0x89] OP:0xe  POP
[0x8a] OP:0xd  JPM f117f521
[0x91] OP:0x10  REVERSE TOP 2
[0x93] OP:0x10  REVERSE TOP 2
[0x95] OP:0xc  JPM f117f52f
[0x9f] OP:0xe  POP
[0xa0] OP:0xf  DUP 62
[0xa1] OP:0xf  DUP 62
[0xa2] OP:0x1  ADD 62, 62 = c4
[0xa3] OP:0x1  ADD 62, ffffffc4 = 26
[0xa4] OP:0xd  JPM f117f510
[0x80] OP:0x10  REVERSE TOP 2
[0x82] OP:0xf  DUP 1
[0x83] OP:0xa  PUSH ffffffff
[0x85] OP:0x5  XOR 1, ffffffff = fffffffe
[0x86] OP:0xc  JPM f117f519
[0x89] OP:0xe  POP
[0x8a] OP:0xd  JPM f117f521
[0x91] OP:0x10  REVERSE TOP 2
[0x93] OP:0x10  REVERSE TOP 2
[0x95] OP:0xc  JPM f117f528
[0x98] OP:0xe  POP
[0x99] OP:0xa  PUSH 1
[0x9b] OP:0x1  ADD 26, 1 = 27
[0x9c] OP:0xd  JPM f117f530
[0xa0] OP:0xf  DUP 27
[0xa1] OP:0xf  DUP 27
[0xa2] OP:0x1  ADD 27, 27 = 4e
[0xa3] OP:0x1  ADD 27, 4e = 75
[0xa4] OP:0xd  JPM f117f510
[0x80] OP:0x10  REVERSE TOP 2
[0x82] OP:0xf  DUP ffffffff
[0x83] OP:0xa  PUSH ffffffff
[0x85] OP:0x5  XOR ffffffff, ffffffff = 0
[0x86] OP:0xc  JPM f117f51d
[0x8d] OP:0xe  POP
[0x8e] OP:0xd  JPM f117f537
[0xa7] OP:0xe  POP
[0xa8] OP:0x8  READ i

かなり長いです。

僕はここで完全に詰んだのですが、ctf-writeups/uiuctf23/rev/vmwhere2 を参考にしたところ、以下のような Base3 エンコーディングを行う Python スクリプトに書き起こすことができるようです。

result = 0
for i in range(7, -1, -1):
	if (x >> i) & 1 == 1:
		result = (result + 1) * 3
	else:
		result = result * 3
	result = result % 256
return result

なるほどたしかに、0xa0 から 0x9c までのループ構造を見抜けば、そんな処理に見えなくもないような気がする・・・?(しない)

[0xa0] OP:0xf  DUP 1
[0xa1] OP:0xf  DUP 1
[0xa2] OP:0x1  ADD 1, 1 = 2
[0xa3] OP:0x1  ADD 1, 2 = 3
[0xa4] OP:0xd  JPM f117f510

[0x80] OP:0x10  REVERSE TOP 2
[0x82] OP:0xf  DUP 1
[0x83] OP:0xa  PUSH ffffffff
[0x85] OP:0x5  XOR 1, ffffffff = fffffffe
[0x86] OP:0xc  JPM f117f519
[0x89] OP:0xe  POP
[0x8a] OP:0xd  JPM f117f521

[0x91] OP:0x10  REVERSE TOP 2
[0x93] OP:0x10  REVERSE TOP 2
[0x95] OP:0xc  JPM f117f528

[0x98] OP:0xe  POP
[0x99] OP:0xa  PUSH 1
[0x9b] OP:0x1  ADD 3, 1 = 4
[0x9c] OP:0xd  JPM f117f530

しばらく出力結果とにらめっこしたものの、残念ながら処理をすっきり理解することはできませんでした。

ただし、こうして変換した入力値は最終的にどこかの値と比較することになるだろうという予測が立ちます。

実際に 1 文字目に “u” を入力した場合の演算結果である 0x75 と比較を行っている箇所を見つけました。

以下のように入力値を変更してみると XOR の結果が変わることから、最終的にこのあたりで Flag の検証を行っているだろうということがわかります。

image-20230705203219584

そこで、バイナリの 0xb90 あたりからハードコードされた Key を探してみます。

image-20230705204000304

コードを見てわかった通り Flag 文字は逆順に埋め込まれているので、一番下の 0x75 が 1 文字目との比較を行う Key になっています。

ここまでわかったので、以下のように適当な文字列を突っ込んだところ、すべての Key と思われる箇所を特定することができました。(0x67 と比較を行っている値がすべて Key になります。)

image-20230705204415010

ここから Key を抽出できたので、Python で各文字と変換後の値のテーブルを作って Flag を取得できるようにします。

以下の Solver を作成しました。

import subprocess

key = [0xc6,0x8b,0xd9,0xcf,0x63,0x60,0xd8,0x7b,0xd8,0x60,0xf6,0xd3,0x7b,0xf6,0xd8,0xc1,0xcf,0xd0,0xf6,0x72,0x63,0x75,0xbe,0xf6,0x7f,0xd8,0x63,0xe7,0x6d,0xf6,0x63,0xcf,0xf6,0xd8,0xf6,0xd8,0x63,0xe7,0x6d,0xb4,0x88,0x72,0x70,0x75,0xb8,0x75]
key = key[::-1]
table = {}

for i in range(0x21, 0xfd):
    cmd = 'echo "{}" | ./a.out | grep 0xb90 | cut -d " " -f 5'.format(chr(i))
    result = subprocess.run(cmd, capture_output=True, shell=True)
    table[result.stdout.decode().replace(",\n", "")] = chr(i)

for k in key:
    print(table[hex(k).replace("0x", "")], end="")

これを実行すると Flag を取得できます。

image-20230705210305482

まとめ

今回はかなり純粋な Rev 力の不足を痛感しました。

先日の Google CTF もそうでしたが、Writeup を読みこんでも解けないような問題に最近頻繁にぶつかります。

今解けないような問題を解いて上位を目指すためには、ツールの使い方などではなく、純粋にバイナリを読んで挙動を理解できる力をきたえていかないといけないなと痛感しています。