All Articles

1337UP CTF 2023 Writeup

2023 年 11 月 17 日から開催されていた 1337UP CTF に 0nePadding で参加し、最終順位 32 位でした。

image-20231119161025504

問題数が非常に多かったので解ききれなかった問題が多いですが、簡単に Writeup を書いていきます。

もくじ

Obfuscation(Rev)

I think I made my code harder to read. Can you let me know if that’s true?

問題バイナリを解凍すると、難読化された問題バイナリのソースコードと暗号化されたデータ output が存在していました。

難読化されたソースコードを一度コンパイルしてから Ghidra でデコンパイルしてみると、main 関数で以下のような処理をしていることがわかりました。

undefined8 main(int param_1,long param_2)

{
  int __n;
  FILE *pFVar1;
  undefined8 uVar2;
  char *__s;
  
  if (param_1 != 2) {
    printf("Not enough arguments provided!");
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  pFVar1 = fopen(*(char **)(param_2 + 8),"r");
  if (pFVar1 == (FILE *)0x0) {
    perror("Error opening file");
    uVar2 = 0xffffffff;
  }
  else {
    __n = o_0b97aabd0b9aa9e13aa47794b5f2236f(pFVar1);
    __s = (char *)malloc((long)(__n + 1));
    if (__s == (char *)0x0) {
      perror("Memory allocation error");
      fclose(pFVar1);
      uVar2 = 0xffffffff;
    }
    else {
      fgets(__s,__n,pFVar1);
      fclose(pFVar1);
      o_e5c0d3fd217ec5a6cd022874d7ffe0b9(__s,__n);
      pFVar1 = fopen("output","wb");
      if (pFVar1 == (FILE *)0x0) {
        perror("Error opening file");
        uVar2 = 0xffffffff;
      }
      else {
        fwrite(__s,(long)__n,1,pFVar1);
        fclose(pFVar1);
        free(__s);
        uVar2 = 0;
      }
    }
  }
  return uVar2;
}

この中で呼び出されている o_e5c0d3fd217ec5a6cd022874d7ffe0b9 関数は以下のようなコードでした。

void o_e5c0d3fd217ec5a6cd022874d7ffe0b9(long param_1,int param_2)
{
  int i;
  
  if (param_2 != 0x18) {
                    /* WARNING: Subroutine does not return */
    __assert_fail("o_8ce986b6b3a519615b6244d7fb2b62f8 == 24","chall.c",5,__PRETTY_FUNCTION__.0);
  }
  for (i = 0; i < 0x18; i = i + 1) {
    *(byte *)(param_1 + i) =
         *(byte *)(param_1 + i) ^
         (byte)*(undefined4 *)(o_a8d9bf17d390687c168fe26f2c3a58b1 + ((ulong)(long)i % 400) * 4) ^
         0x37;
  }
  return;
}

この結果から、*(byte *)(param_1 + i) ^ (byte)*(undefined4 *)(o_a8d9bf17d390687c168fe26f2c3a58b1 + ((ulong)(long)i % 400) * 4) ^ 0x37 の箇所で Flag を暗号化した結果が output ファイルに記録されていることがわかります。

そのため、以下の Solver で Flag を取得できました。

import struct

obs = b'\x2a\x00\x00\x00\x4d\x00\x00\x00\x03\x00\x00\x00\x08\x00\x00\x00\x45\x00\x00\x00\x56\x00\x00\x00\x3c\x00\x00\x00\x63\x00\x00\x00\x32\x00\x00\x00\x4c\x00\x00\x00\x0f\x00\x00\x00\x0e\x00\x00\x00\x29\x00\x00\x00\x57\x00\x00\x00\x2d\x00\x00\x00\x3d\x00\x00\x00\x10\x00\x00\x00\x32\x00\x00\x00\x14\x00\x00\x00\x05\x00\x00\x00\x0d\x00\x00\x00\x21\x00\x00\x00\x3e\x00\x00\x00\x46\x00\x00\x00\x46\x00\x00\x00\x4d\x00\x00\x00\x1c\x00\x00\x00\x55\x00\x00\x00\x52\x00\x00\x00\x1a\x00\x00\x00\x1c\x00\x00\x00\x20\x00\x00\x00\x38\x00\x00\x00\x16\x00\x00\x00\x15\x00\x00\x00\x30\x00\x00\x00\x26\x00\x00\x00\x2a\x00\x00\x00\x62\x00\x00\x00\x14\x00\x00\x00\x2c\x00\x00\x00\x42\x00\x00\x00\x15\x00\x00\x00\x37\x00\x00\x00\x62\x00\x00\x00\x11\x00\x00\x00\x14\x00\x00\x00\x5d\x00\x00\x00\x63\x00\x00\x00\x36\x00\x00\x00\x15\x00\x00\x00\x2b\x00\x00\x00\x50\x00\x00\x00\x63\x00\x00\x00\x40\x00\x00\x00\x62\x00\x00\x00\x37\x00\x00\x00\x03\x00\x00\x00\x5f\x00\x00\x00\x10\x00\x00\x00\x38\x00\x00\x00\x3e\x00\x00\x00\x2a\x00\x00\x00\x53\x00\x00\x00\x48\x00\x00\x00\x17\x00\x00\x00\x47\x00\x00\x00\x3d\x00\x00\x00\x5a\x00\x00\x00\x0e\x00\x00\x00\x21\x00\x00\x00\x2d\x00\x00\x00\x54\x00\x00\x00\x19\x00\x00\x00\x18\x00\x00\x00\x60\x00\x00\x00\x4a\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5c\x00\x00\x00\x19\x00\x00\x00\x21\x00\x00\x00\x24\x00\x00\x00\x06\x00\x00\x00\x1a\x00\x00\x00\x0e\x00\x00\x00\x25\x00\x00\x00\x21\x00\x00\x00\x64\x00\x00\x00\x03\x00\x00\x00\x1e\x00\x00\x00\x01\x00\x00\x00\x1f\x00\x00\x00\x1f\x00\x00\x00\x56\x00\x00\x00\x5c\x00\x00\x00\x3d\x00\x00\x00\x56\x00\x00\x00\x51\x00\x00\x00\x26\x00\x00\x00'
obs = [struct.unpack("<I", obs[i:i+4])[0] for i in range(0,len(obs),4)]

with open("output", "rb") as f:
    data = f.read()

for i in range(0x18):
    print(chr(data[i]^0x37^obs[i]),end="")

# INTIGRITI{Z29vZGpvYg==}

FlagChecker(Rev)

Can you beat this FlagChecker?

問題バイナリとして与えられた Rust のソースコードは以下の通りでした。

use std::io;

fn check_flag(flag: &str) -> bool {
    flag.as_bytes()[18] as i32 * flag.as_bytes()[7] as i32 & flag.as_bytes()[12] as i32 ^ flag.as_bytes()[2] as i32 == 36 &&
    flag.as_bytes()[1] as i32 % flag.as_bytes()[14] as i32 - flag.as_bytes()[21] as i32 % flag.as_bytes()[15] as i32 == -3 &&
    flag.as_bytes()[10] as i32 + flag.as_bytes()[4] as i32 * flag.as_bytes()[11] as i32 - flag.as_bytes()[20] as i32 == 5141 &&
    flag.as_bytes()[19] as i32 + flag.as_bytes()[12] as i32 * flag.as_bytes()[0] as i32 ^ flag.as_bytes()[16] as i32 == 8332 &&
    flag.as_bytes()[9] as i32 ^ flag.as_bytes()[13] as i32 * flag.as_bytes()[8] as i32 & flag.as_bytes()[16] as i32 == 113 &&
    flag.as_bytes()[3] as i32 * flag.as_bytes()[17] as i32 + flag.as_bytes()[5] as i32 + flag.as_bytes()[6] as i32 == 7090 &&
    flag.as_bytes()[21] as i32 * flag.as_bytes()[2] as i32 ^ flag.as_bytes()[3] as i32 ^ flag.as_bytes()[19] as i32 == 10521 &&
    flag.as_bytes()[11] as i32 ^ flag.as_bytes()[20] as i32 * flag.as_bytes()[1] as i32 + flag.as_bytes()[6] as i32 == 6787 &&
    flag.as_bytes()[7] as i32 + flag.as_bytes()[5] as i32 - flag.as_bytes()[18] as i32 & flag.as_bytes()[9] as i32 == 96 &&
    flag.as_bytes()[12] as i32 * flag.as_bytes()[8] as i32 - flag.as_bytes()[10] as i32 + flag.as_bytes()[4] as i32 == 8277 &&
    flag.as_bytes()[16] as i32 ^ flag.as_bytes()[17] as i32 * flag.as_bytes()[13] as i32 + flag.as_bytes()[14] as i32 == 4986 &&
    flag.as_bytes()[0] as i32 * flag.as_bytes()[15] as i32 + flag.as_bytes()[3] as i32 == 7008 &&
    flag.as_bytes()[13] as i32 + flag.as_bytes()[18] as i32 * flag.as_bytes()[2] as i32 & flag.as_bytes()[5] as i32 ^ flag.as_bytes()[10] as i32 == 118 &&
    flag.as_bytes()[0] as i32 % flag.as_bytes()[12] as i32 - flag.as_bytes()[19] as i32 % flag.as_bytes()[7] as i32 == 73 &&
    flag.as_bytes()[14] as i32 + flag.as_bytes()[21] as i32 * flag.as_bytes()[16] as i32 - flag.as_bytes()[8] as i32 == 11228 &&
    flag.as_bytes()[3] as i32 + flag.as_bytes()[17] as i32 * flag.as_bytes()[9] as i32 ^ flag.as_bytes()[11] as i32 == 11686 &&
    flag.as_bytes()[15] as i32 ^ flag.as_bytes()[4] as i32 * flag.as_bytes()[20] as i32 & flag.as_bytes()[1] as i32 == 95 &&
    flag.as_bytes()[6] as i32 * flag.as_bytes()[12] as i32 + flag.as_bytes()[19] as i32 + flag.as_bytes()[2] as i32 == 8490 &&
    flag.as_bytes()[7] as i32 * flag.as_bytes()[5] as i32 ^ flag.as_bytes()[10] as i32 ^ flag.as_bytes()[0] as i32 == 6869 &&
    flag.as_bytes()[21] as i32 ^ flag.as_bytes()[13] as i32 * flag.as_bytes()[15] as i32 + flag.as_bytes()[11] as i32 == 4936 &&
    flag.as_bytes()[16] as i32 + flag.as_bytes()[20] as i32 - flag.as_bytes()[3] as i32 & flag.as_bytes()[9] as i32 == 104 &&
    flag.as_bytes()[18] as i32 * flag.as_bytes()[1] as i32 - flag.as_bytes()[4] as i32 + flag.as_bytes()[14] as i32 == 5440 &&
    flag.as_bytes()[8] as i32 ^ flag.as_bytes()[6] as i32 * flag.as_bytes()[17] as i32 + flag.as_bytes()[12] as i32 == 7104 &&
    flag.as_bytes()[11] as i32 * flag.as_bytes()[2] as i32 + flag.as_bytes()[15] as i32 == 6143
}

fn main() {
    let mut flag = String::new();
    println!("Enter the flag: ");
    io::stdin().read_line(&mut flag).expect("Failed to read line");
    let flag = flag.trim();

    if check_flag(flag) {
        println!("Correct flag");
    } else {
        println!("Wrong flag");
    }
}

入力文字が Flag に一致するかどうか検証をしているようですが、実装的に Z3 で解くことが想定されていそうだということがわかります。

そこで、以下の Solver を使用して Flag を取得しました。

from z3 import *
flag = [BitVec(f"flag[{i}]", 8) for i in range(22)]
s = Solver()

for i in range(22):
    s.add(And(
        (flag[i] >= 0x21),
        (flag[i] <= 0x7e)
    ))

# INTIGRITI{
s.add(flag[0] == ord("I"))
s.add(flag[1] == ord("N"))
s.add(flag[2] == ord("T"))
s.add(flag[3] == ord("I"))
s.add(flag[4] == ord("G"))
s.add(flag[5] == ord("R"))
s.add(flag[6] == ord("I"))
s.add(flag[7] == ord("T"))
s.add(flag[8] == ord("I"))
s.add(flag[9] == ord("{"))
s.add(flag[21] == ord("}"))

s.add(flag[18] * flag[7] & flag[12] ^ flag[2] == 36)
s.add(flag[1] % flag[14] - flag[21] % flag[15] == -3)
s.add(flag[10] + flag[4] * flag[11] - flag[20] == 5141)
s.add(flag[19] + flag[12] * flag[0] ^ flag[16] == 8332)
s.add(flag[9] ^ flag[13] * flag[8] & flag[16] == 113)
s.add(flag[3] * flag[17] + flag[5] + flag[6] == 7090)
s.add(flag[21] * flag[2] ^ flag[3] ^ flag[19] == 10521)
s.add(flag[11] ^ flag[20] * flag[1] + flag[6] == 6787)
s.add(flag[7] + flag[5] - flag[18] & flag[9] == 96)
s.add(flag[12] * flag[8] - flag[10] + flag[4] == 8277)
s.add(flag[16] ^ flag[17] * flag[13] + flag[14] == 4986)
s.add(flag[0] * flag[15] + flag[3] == 7008)
s.add(flag[13] + flag[18] * flag[2] & flag[5] ^ flag[10] == 118)
s.add(flag[0] % flag[12] - flag[19] % flag[7] == 73)
s.add(flag[14] + flag[21] * flag[16] - flag[8] == 11228)
s.add(flag[3] + flag[17] * flag[9] ^ flag[11] == 11686)
s.add(flag[15] ^ flag[4] * flag[20] & flag[1] == 95)
s.add(flag[6] * flag[12] + flag[19] + flag[2] == 8490)
s.add(flag[7] * flag[5] ^ flag[10] ^ flag[0] == 6869)
s.add(flag[21] ^ flag[13] * flag[15] + flag[11] == 4936)
s.add(flag[16] + flag[20] - flag[3] & flag[9] == 104)
s.add(flag[18] * flag[1] - flag[4] + flag[14] == 5440)
s.add(flag[8] ^ flag[6] * flag[17] + flag[12] == 7104)
s.add(flag[11] * flag[2] + flag[15] == 6143)

while s.check() == sat:
    m = s.model()
    for c in flag:
        print(chr(m[c].as_long()),end="")
    print("")
    break

# INTIGRITI{tHr33_Z_FTW}

Anonymous(Rev)

Anonymous has hidden a message inside this exe, can you extract it?

問題バイナリとして与えられたファイルを ILSpy で解析すると以下のコードを取得できました。

image-20231118104419037

このプログラムでは、プログラム内にハードコードされたアイコンの中から特定の条件を満たす文字列を抜き出して Base64 デコードを行っていることがわかります。

そのため、アイコンの中央あたりの適当な文字列をいくつかコピーして Base64 デコードを行ったところ、Flag を取得することができました。

image-20231118104402809

imPACKful(Rev)

This program seems to be compressed but still can be executed, I wonder what could cause that..

問題バイナリとして与えられたファイルは UPX でパッキングされていたため、まずはこれを解凍します。

image-20231118104558414

しかし、解凍したプログラムをデコンパイルしても、このプログラム自体は何の処理も行わないようでした。

image-20231118110146619

そこで、回答したプログラムのセクションを調べてみたところ、{N3v3R} というセクションが登録されており、これが Flag になりました。

image-20231118110137371

Can We Fix It(Rev)

We have extracted this payload that some malware tried to dump.. Seems dumping it directly from memory debased it so it can’t run. Can you fix it?

問題バイナリとして与えられたファイルは何らかの理由で実行できない PE ファイルでした。

PE ファイルとして Windows がロードできなくなっているようでしたので、以下の画像と CFF Exporer の結果を比較しながら解析を進めていきました。

image-20231118150807553

参考:PE Format - Win32 apps | Microsoft Learn

最終的に、.text セクションのアラインメントが不適切なことが PE バイナリを実行できない原因だということがわかりました。

image-20231118155837570

そのため、.text セクションの VA を 0x1000 に変更してファイルを保存し、正しい Flag を取得することに成功しました。

image-20231118155912481

Virtual RAM(Rev)

I wonder what the old man is talking about

問題バイナリとしてゲームボーイの ROM イメージが与えられます。

image-20231118161636073

この ROM イメージは BGB GameBoy Emulator を使用して実行し、デバッグやチートを行うことができます。

参考:BGB GameBoy Emulator (current version: BGB 1.5.10)

参考:[Reverse] WPI CTF 2022 - PokemonRematch | TeamRocketIST - Portuguese CTF Team

image-20231118220252864

ゲーム開始画面の老人に話しかけると、VRAM を確認するように指示されます。

image-20231118220310677

そこで VRAM の情報を確認したところ、画面外のタイルに Flag 文字列が埋まっていることがわかりました。

image-20231118220325629

これを切り貼りしてつなげてみると以下の文字列を取得できました。

img

この Flag が INTIGRITI{H3r0_0F_tIM3} としか読めず中々通せなかったところ、チームメンバーから母音は全部数字なのではという鋭い指摘を受け、正しい Flag が INTIGRITI{H3r0_0F_t1M3} であることを特定しました。

image-20231118220400153

Crack Me If You Can(Rev)

Can you slay goliath?

問題バイナリとして与えられた exe を調べると .Net プログラムということがわかります。

image-20231119151731960

実行してみると、謎の入力を求める GUI が起動します。

image-20231119151941971

ILSpy でデコンパイルしてみると、中々複雑そうな関数が複数定義されていました。

image-20231119152052196

Main 関数は暗号化されたバイトデータを復号して executingAssembly でメソッドを取得する方式を取っており、このままでは解析が困難そうです。

image-20231119152411152

そこで、ExtremeDumper を使用して実行中のプロセスから復号されたバイナリを抽出します。

image-20231119154338120

取得したバイナリを ILSpy で解析してみると、プログラム起動時に表示される GUI のコードを参照することができるようになりました。

image-20231119154629810

この Main クラスのコードを読むと、入力した文字列と事前定義された enc というバイト列を unkownMethod 関数に与えて得られる出力が WhatAreYouDoingToChallenge という文字列に一致するかを検証していることがわかります。

private void Button1_Click(object sender, EventArgs e)
{
    string @string = Encoding.UTF8.GetString(Resources.enc);
    if (Operators.CompareString(unkownMethod(TextBox1.Text, @string), "WhatAreYouDoingToChallenge", TextCompare: false) == 0)
    {
        Interaction.MsgBox("You Solve It", MsgBoxStyle.Information, "Nice");
    }
}

public string unkownMethod(string textToScramble, string password)
{
    StringBuilder stringBuilder = new StringBuilder(textToScramble.Length);
    int num = checked(textToScramble.Length - 1);
    for (int i = 0; i <= num; i = checked(i + 1))
    {
        int index = i % password.Length;
        char c = textToScramble[i];
        c = Strings.ChrW(c ^ password[index]);
        stringBuilder.Append(c);
    }
    return stringBuilder.ToString();
}

unkownMethod 関数の中ではさらに checked という関数が呼び出されています。

checked 関数のコードは ILSpy のデコンパイル結果内には含まれていませんが、その後の処理を見る限り、単に入力文字と enc を XOR しているだけですので無視しても問題なさそうです。

以下の Solver で Flag を取得できました。

ans = "WhatAreYouDoingToChallenge"
with open("enc", "rb") as f:
    enc = f.read()

for i in range(len(ans)):
    print(chr(ord(ans[i])^enc[i%len(enc)]),end="")
    
# intigriti{You_Are_Amazing}

Escape(Game Hacking)

Your trapped inside a box. Can you escape it and do the reverse to get the flag?

問題バイナリとして与えられたファイルは、Unity 製のゲームプログラムでした。

image-20231119111623490

これを起動すると、四方を壁に囲まれた謎のオブジェクトが表示されます。

とりあえずこの壁から脱出すれば Flag を取得できるようです。

image-20231119111733734

まず初めに、CheatEngine を使用して移動時の座標の格納されるメモリ領域を特定して改ざんすればよいと考えましたが、残念ながら試行錯誤を繰り返したものの対象のメモリ領域は見つかりませんでした。

次に、別のアプローチとしてゲームのオブジェクトをいじることを考えます。

参考:Reverse engineering unity game | tripoloski blog

Unity のデフォルトの設定では <Project>\Managed\Assembly-CSharp.dll に .Net プログラムとしてコンパイルされたプログラムが配置されているので、これを ILSpy で解析します。

モジュールを探すと MyCharacterController というクラスが存在していました。

image-20231119131415199

ここのモジュールをパッチして、「オブジェクトの開始位置を壁の外にする」、「壁のオブジェクトを削除する」、「壁の衝突判定をなくしてすり抜けられるようにする」、「ジャンプで壁を越えられるようにする」などのチートを行うことで Flag を取得できそうです。

コードの改ざんのため、プログラムを ILSpy ではなく dnSpy で開きなおします。(ILSpy でもパッチができるのかもしれませんが、メニューが見当たりませんでしたので dnSpy を使います)

ここで、キャラクタオブジェクトの UpdateVelocity メソッドを見ると、ジャンプの高さが制御されていることがわかりました。

image-20231119145012962

そこで、以下の行を追加して強制的に高高度でジャンプをするようにコードを変更した後、コンパイルを行います。

image-20231119150144740

image-20231119150320357

最後に Save All からファイルを保存してゲームを起動します。

image-20231119150344304

これで普通にジャンプを行うと、超高度のジャンプとなり壁を越えられることがわかりました。

image-20231119150122233

壁を越え、Map 上の Flag を取得できました。

image-20231119145857562

Over the Wire 1(Warmup)

I’m not sure how secure this protocol is but as long as we update the password, I’m sure everything will be fine

問題バイナリとして与えられた pcap を WireShark で開くと、FTP でファイルのやり取りが行われていることがわかりました。

そのため、まずはこのファイルをバイナリデータとして保存します。

image-20231117231323002

FTP でやり取りされていたファイル(flag.zip)はパスワード付き ZIP ファイルでしたので、続いてファイルを解凍するためのパスワードを探していきます。

FTP でファイル転送を行った際のパスワードは 5up3r_53cur3_p455w0rd_2022 でしたので、このパスワードを使用して解凍を試みましたが失敗しました。

220 pyftpdlib 1.5.9 ready.
USER cat
331 Username ok, send password.
PASS 5up3r_53cur3_p455w0rd_2022
230 Login successful.
SYST
215 UNIX Type: L8
PORT 192,168,16,131,179,47
200 Active data connection established.
LIST
125 Data connection already open. Transfer starting.
226 Transfer complete.
TYPE I
200 Type set to: Binary.
PORT 192,168,16,131,203,181
200 Active data connection established.
RETR flag.zip
125 Data connection already open. Transfer starting.
226 Transfer complete.
PORT 192,168,16,131,132,11
200 Active data connection established.
RETR reminder.txt
125 Data connection already open. Transfer starting.
226 Transfer complete.
PORT 192,168,16,131,162,139
200 Active data connection established.
RETR README.md
125 Data connection already open. Transfer starting.
226 Transfer complete.
QUIT
221 Goodbye.

さらにパケットを読むと、FTP 経由で以下のようなメッセージを含むファイルを受信していることがわかりました。

image-20231119162229252

このヒントを元に、パスワードを 5up3r_53cur3_p455w0rd_2023 に変更した結果、ZIP ファイルを解凍して Flag を取得できました。

Over the Wire 2(Warmup)

問題バイナリとして与えられた pcap ファイルを解凍すると、SMTP 経由で画像をやり取りしていることがわかりました。

image-20231117232037966

1 枚目は以下の画像でしたが、こちらは一通り調べても不審な点は見つかりませんでした。

image-20231117232149565

そこで、さらにパケットを追ってみると、別のメールでも画像を送付していることがわかりました。

image-20231117232800928

そこで、次はこちらの画像を調べていきます。

image-20231117232750719

zsteg でファイルを解析すると、Flag を取得できました。

image-20231117232841225

まとめ

久しぶりにたくさんの問題を解くことができて楽しかったです。

パッキングされた .Net バイナリの問題やゲームプログラムの解析など新しい学びもありとてもよかったです。