All Articles

TUCTF CTF 2023 Writeup

2023 年の 12 月 2 日ごろに開催されていた TUCTF に 0nePadding で参加しました。

image-20231206011830510

簡単に Writeup を書いていきます。

もくじ

What Are You Doing In My Swamp?(Forensic)

This challenge is like ogres, it has layers

問題バイナリとして与えられたファイルはマジックナンバーが削られていて画像ファイルとして参照できなかったため、JPG のマジックナンバーを手動で追加しました。

image-20231205212004575

その結果、以下のような画像を参照できるようになりました。

image-20231206002452193

ここからヒントがなかったので少々悩みましたが、最終的に layers というパスワードを使用して steghide を使用したところ以下のメッセージを取得できました。

image-20231205212222591

あとはこの GFXGU{LtIvh_zIv_oRpv_lmrOmh} を Atbash Cipher にかけると正しい Flag を取得できました。

State of the Git(Forensic)

All the cool kids are embracing state of the art IAC technology, and we are rushing to catch up! We have a new system that we are testing out, but we are not sure how secure it is. Can you check it out for us?

問題バイナリとして .git ディレクトリといくつかのコードを含むアーカイブファイルが与えられます。

与えられたリポジトリを Github に push してみると、592 もの commit が存在していることがわかります。

image-20231205234751287

ほとんどの commit のコミットメッセージは delete や add 、release、fix などから始まっています。

image-20231205234844701

github 上で見るとブランチは 1 つしか出ませんでした。

これは私が完全にやらかしてましたが、ローカルリポジトリ内の main ブランチした github 上に push していなかったために、いくら github 上でコードを調べても Flag が見つからなくなってしまっていました。

image-20231205235638066

ローカルブランチを確認するといくつかのブランチが存在していることがわかります。

image-20231205235829838

ローカルブランチを release に変更した上で git log を grep してみると、1 つだけおかしなコミットメッセージが見つかりました。

image-20231205235959885

この commit を調査すると、password に Base64 エンコードされた Flag が埋め込まれていました。

$ git --no-pager show c7ac66f
commit c7ac66fbd99c8850706c99b27475222f8dfe3d29
Author: MrLadas <97653268+MrLadas@users.noreply.github.com>
Date:   Tue Nov 21 23:28:37 2023 +0000

    jqnljvtngtrtpqdkvccfpqyskwnayzgdhurvuwdkxjtcldzhjcksiaagimzdyoflpodbgzfimxumbouesdkivaolamntydqtmwwj

diff --git a/terraform.tfstate b/terraform.tfstate
new file mode 100644
index 0000000..955de90
--- /dev/null
+++ b/terraform.tfstate
@@ -0,0 +1,103 @@
+{
+  "version": 4,
+  "terraform_version": "1.6.4",
+  "serial": 3,
+  "lineage": "0d21a79d-34f7-89e7-57f4-9266570147f4",
+  "outputs": {},
+  "resources": [
+    {
+      "mode": "managed",
+      "type": "droplet",
+      "name": "ctfd-dev-01",
+      "provider": "provider[\"registry.terraform.io/digitalocean/digitalocean\"]",
+      "instances": [
+        {
+          "schema_version": 0,
+          "attributes": {
+            "arch": "amd64",
+            "bwlimit": 0,
+            "clone": null,
+            "clone_storage": null,
+            "cmode": "tty",
+            "console": true,
+            "cores": 1,
+            "cpulimit": 0,
+            "cpuunits": 1024,
+            "description": "",
+            "features": [],
+            "force": false,
+            "full": null,
+            "hagroup": "",
+            "hastate": "",
+            "hookscript": "",
+            "hostname": "ctfd-dev-01",
+            "id": "aws/ctfd-dev-01",
+            "ignore_unpack_errors": false,
+            "lock": "",
+            "memory": 512,
+            "mountpoint": [],
+            "nameserver": "",
+            "network": [
+              {
+                "bridge": "vmbr0",
+                "firewall": true,
+                "gw": "192.168.1.1",
+                "gw6": "",
+                "hwaddr": "BC:24:11:15:79:0A",
+                "ip": "192.168.5.250/16",
+                "ip6": "",
+                "mtu": 0,
+                "name": "eth0",
+                "rate": 0,
+                "tag": 0,
+                "trunks": "",
+                "type": "veth"
+              }
+            ],
+            "onboot": true,
+            "ostemplate": "",
+            "ostype": "ubuntu",
+            "password": "VFVDVEZ7NzNycjRmMHJtX1M3QTczLTF5XzUzY3IzNzV9Cg==", // ZG9wX3YxXzA3ZmJjODgwY2YwNTNhOTE5Nzk4MDdkZmFhZjhhZDVjOTg4MGFiYWUxZjhkZjJjY2VjZTk2Njk0MmFmNDE0MDgK < Change this before going !
+            "pool": "Production",
+            "protection": false,
+            "restore": false,
+            "rootfs": [
+              {
+                "acl": false,
+                "quota": false,
+                "replicate": false,
+                "ro": false,
+                "shared": false,
+                "size": "8G",
+                "storage": "do-block-storage",
+                "volume": "do-block-storage:ctfd-dev-01/rootfs"
+              }
+            ],
+            "searchdomain": "",
+            "ssh_public_keys": null,
+            "start": true,
+            "startup": "",
+            "swap": 512,
+            "tags": "",
+            "template": false,
+            "timeouts": null,
+            "tty": 2,
+            "unique": false,
+            "unprivileged": true,
+            "unused": []
+          },
+          "sensitive_attributes": [
+            [
+              {
+                "type": "get_attr",
+                "value": "password"
+              }
+            ]
+          ],
+          "private": "sdkawewgfjfakqpwoqpretwenfwejweahwhuqhewdfhewf"
+        }
+      ]
+    }
+  ],
+  "check_results": null
+}

これをデコードすると Flag を取得できます。

$ echo VFVDVEZ7NzNycjRmMHJtX1M3QTczLTF5XzUzY3IzNzV9Cg== | base64 -d
TUCTF{73rr4f0rm_S7A73-1y_53cr375}

Table Encryption(Crypto)

You can’t crack my file! I am the exclusive owner of the encryption key!

問題バイナリとして table_encryption.xml.enc という名前の暗号化されたバイナリファイルが与えられます。

ファイル名以外にヒントがない状況でしたので、とりあえず XML のプロローグで XOR を取ってみると元のキーを復元することができました。

あとは以下の通り復元した XOR キーで暗号化を解除すると Flag を取得できました。

image-20231203193120254

Custom ECB Cipher(Crypto)

I have designed a simple algorithm which I believe it’s secure. I am confident you cannot compromise my message with the x value.

問題バイナリとして以下の Python スクリプトが与えられます。

from Crypto.Util import number

flag = b"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

def convert(msg):
    msg = msg ^ msg >> x
    msg = msg ^ msg << 13 & 275128763
    msg = msg ^ msg << 20 & 2186268085
    msg = msg ^ msg >> 14
    return msg

def transform(message):
    assert len(message) % 4 == 0
    new_message = b''
    for i in range(int(len(message) / 4)):
        block = message[i * 4 : i * 4 +4]
        block = number.bytes_to_long(block)
        block = convert(block)
        block = number.long_to_bytes(block, 4)
        new_message += block
    return new_message

c = transform(flag[6:-1]).hex()
print('c =', c)
'''
c = e34a707c5c1970cc6375181577612a4ed07a2c3e3f441d6af808a8acd4310b89bd7e2bb9
'''

Flag の括弧内の文字列を 4 バイトごとに分割して convert 関数に送り、暗号化されたバイト列が c となっているようです。

また、x は不明ですが convert 関数の引数が 4 バイトになることは確定しているので、ここは 32 未満のいずれかの値になると予想できます。

当初は convert 関数の処理を下から順に逆算して最後に x の総当たりを試せばよいのではないかと考えていましたが、and 演算は破壊的な演算なので復号無理では?などとなり解けませんでした。

Writeup を読むと、どうもこの convert 関数の演算は破壊的ではなく、簡単に復号できるようでした。

まず最後の行から見ていきます。

以下のコードは、メッセージの下位 18 bit をそのメッセージ自身の上位 18 bit で XOR するコードです。

msg = msg ^ msg >> 14

あまり直感的では内容に思えますが、どうやらこのような「元のメッセージをシフトして XOR する」ような処理をおこなった場合、同じ演算を繰り返すことで最終的に元のメッセージを復元できるタイミングがあるようです。

実際に計算の過程をトレースしてみるとメッセージが 32bit の場合は msg = msg ^ msg >> 14 を 4 回行うと元の値に戻ることがわかりました。

image-20231205231617829

そして、次の AND を含む計算も、(なぜか)破壊的な演算になっておらず、同じ演算をもう一度繰り返すことで元の値に復元できることを確かめられます。

image-20231205232017122

image-20231205232046122

これによって、あとは x の値をブルートフォースするだけで元のメッセージを復元できるようになります。

以下の Solver で Flag を復元できました。

from Crypto.Util import number

def inverse(msg,x):
    for i in range(3):
        msg = msg ^ msg >> 14

    for i in range(1):
        msg = msg ^ msg << 20 & 2186268085
    
    for i in range(1):
        msg = msg ^ msg << 13 & 275128763
    
    for i in range(3):
        msg = msg ^ msg >> x

    return msg

data = number.long_to_bytes(0xe34a707c5c1970cc6375181577612a4ed07a2c3e3f441d6af808a8acd4310b89bd7e2bb9)

for x in range(32):
    flag = b""
    for i in range(0,len(data),4):
        c = number.bytes_to_long(data[i:i+4])
        flag += number.long_to_bytes(inverse(c,x))
    
    print(flag)

まとめ

今回は Rev が全く解けなかったので精進します。。