2023 年 11 月 17 日から開催されていた 1337UP CTF に 0nePadding で参加し、最終順位 32 位でした。
問題数が非常に多かったので解ききれなかった問題が多いですが、簡単に Writeup を書いていきます。
もくじ
- Obfuscation(Rev)
- FlagChecker(Rev)
- Anonymous(Rev)
- imPACKful(Rev)
- Can We Fix It(Rev)
- Virtual RAM(Rev)
- Crack Me If You Can(Rev)
- Escape(Game Hacking)
- Over the Wire 1(Warmup)
- Over the Wire 2(Warmup)
- まとめ
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 で解析すると以下のコードを取得できました。
このプログラムでは、プログラム内にハードコードされたアイコンの中から特定の条件を満たす文字列を抜き出して Base64 デコードを行っていることがわかります。
そのため、アイコンの中央あたりの適当な文字列をいくつかコピーして Base64 デコードを行ったところ、Flag を取得することができました。
imPACKful(Rev)
This program seems to be compressed but still can be executed, I wonder what could cause that..
問題バイナリとして与えられたファイルは UPX でパッキングされていたため、まずはこれを解凍します。
しかし、解凍したプログラムをデコンパイルしても、このプログラム自体は何の処理も行わないようでした。
そこで、回答したプログラムのセクションを調べてみたところ、{N3v3R} というセクションが登録されており、これが Flag になりました。
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 の結果を比較しながら解析を進めていきました。
参考:PE Format - Win32 apps | Microsoft Learn
最終的に、.text セクションのアラインメントが不適切なことが PE バイナリを実行できない原因だということがわかりました。
そのため、.text セクションの VA を 0x1000 に変更してファイルを保存し、正しい Flag を取得することに成功しました。
Virtual RAM(Rev)
I wonder what the old man is talking about
問題バイナリとしてゲームボーイの ROM イメージが与えられます。
この ROM イメージは BGB GameBoy Emulator を使用して実行し、デバッグやチートを行うことができます。
参考:BGB GameBoy Emulator (current version: BGB 1.5.10)
参考:[Reverse] WPI CTF 2022 - PokemonRematch | TeamRocketIST - Portuguese CTF Team
ゲーム開始画面の老人に話しかけると、VRAM を確認するように指示されます。
そこで VRAM の情報を確認したところ、画面外のタイルに Flag 文字列が埋まっていることがわかりました。
これを切り貼りしてつなげてみると以下の文字列を取得できました。
この Flag が INTIGRITI{H3r0_0F_tIM3}
としか読めず中々通せなかったところ、チームメンバーから母音は全部数字なのではという鋭い指摘を受け、正しい Flag が INTIGRITI{H3r0_0F_t1M3}
であることを特定しました。
Crack Me If You Can(Rev)
Can you slay goliath?
問題バイナリとして与えられた exe を調べると .Net プログラムということがわかります。
実行してみると、謎の入力を求める GUI が起動します。
ILSpy でデコンパイルしてみると、中々複雑そうな関数が複数定義されていました。
Main 関数は暗号化されたバイトデータを復号して executingAssembly でメソッドを取得する方式を取っており、このままでは解析が困難そうです。
そこで、ExtremeDumper を使用して実行中のプロセスから復号されたバイナリを抽出します。
取得したバイナリを ILSpy で解析してみると、プログラム起動時に表示される GUI のコードを参照することができるようになりました。
この 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 製のゲームプログラムでした。
これを起動すると、四方を壁に囲まれた謎のオブジェクトが表示されます。
とりあえずこの壁から脱出すれば Flag を取得できるようです。
まず初めに、CheatEngine を使用して移動時の座標の格納されるメモリ領域を特定して改ざんすればよいと考えましたが、残念ながら試行錯誤を繰り返したものの対象のメモリ領域は見つかりませんでした。
次に、別のアプローチとしてゲームのオブジェクトをいじることを考えます。
参考:Reverse engineering unity game | tripoloski blog
Unity のデフォルトの設定では <Project>\Managed\Assembly-CSharp.dll
に .Net プログラムとしてコンパイルされたプログラムが配置されているので、これを ILSpy で解析します。
モジュールを探すと MyCharacterController というクラスが存在していました。
ここのモジュールをパッチして、「オブジェクトの開始位置を壁の外にする」、「壁のオブジェクトを削除する」、「壁の衝突判定をなくしてすり抜けられるようにする」、「ジャンプで壁を越えられるようにする」などのチートを行うことで Flag を取得できそうです。
コードの改ざんのため、プログラムを ILSpy ではなく dnSpy で開きなおします。(ILSpy でもパッチができるのかもしれませんが、メニューが見当たりませんでしたので dnSpy を使います)
ここで、キャラクタオブジェクトの UpdateVelocity メソッドを見ると、ジャンプの高さが制御されていることがわかりました。
そこで、以下の行を追加して強制的に高高度でジャンプをするようにコードを変更した後、コンパイルを行います。
最後に Save All からファイルを保存してゲームを起動します。
これで普通にジャンプを行うと、超高度のジャンプとなり壁を越えられることがわかりました。
壁を越え、Map 上の Flag を取得できました。
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 でファイルのやり取りが行われていることがわかりました。
そのため、まずはこのファイルをバイナリデータとして保存します。
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 経由で以下のようなメッセージを含むファイルを受信していることがわかりました。
このヒントを元に、パスワードを 5up3r_53cur3_p455w0rd_2023
に変更した結果、ZIP ファイルを解凍して Flag を取得できました。
Over the Wire 2(Warmup)
問題バイナリとして与えられた pcap ファイルを解凍すると、SMTP 経由で画像をやり取りしていることがわかりました。
1 枚目は以下の画像でしたが、こちらは一通り調べても不審な点は見つかりませんでした。
そこで、さらにパケットを追ってみると、別のメールでも画像を送付していることがわかりました。
そこで、次はこちらの画像を調べていきます。
zsteg でファイルを解析すると、Flag を取得できました。
まとめ
久しぶりにたくさんの問題を解くことができて楽しかったです。
パッキングされた .Net バイナリの問題やゲームプログラムの解析など新しい学びもありとてもよかったです。