標題:首屆北京大學信息安全綜合能力競賽writeup

taibeihacker

Moderator

签到​

題目內容是一個pdf 文件,用Adobe Acrobat 打開,看到其中包含一些特殊符號。
在編輯模式下,查看得到其字體為Wingdings,這是一個裝飾字體,文本內容其實是ASCII 碼。文本範圍是超出頁面的,resize 之後復制出其內容,給出了兩行文字:
這是柵欄密碼,得到flag 為flag{Have_A_Great_Time@GeekGame_v1!}。
fa{aeAGetTm@ekaev!
lgHv__ra_ieGeGm_1}

小北问答 Remake​

北京大學燕園校區有理科1 號樓到理科X 號樓,但沒有理科(X+1) 號及之後的樓。 X 是?在Google Earth 中搜索,存在理科5 號樓,但沒有理科6 號樓。故答案為5。
上一屆(第零屆)比賽的總註冊人數有多少?在北京大學新聞網中找到報導北京大學舉辦首屆信息安全綜合能力競賽,得到「本次大賽共有407 人註冊參賽」,故答案為407。
geekgame.pku.edu.cn 的HTTPS 證書曾有一次忘記續期了,發生過期的時間是?搜索「ssl cert database」,找到網站crt.sh。在該網站上搜索geekgame.pku.edu.cn,並根據題目給出的正則表達式尋找過期時間秒數以3 結尾的證書,得到證書4362003382。其過期時間為Jul 11 00:49:53 2021 GMT,將時區換為UTC+8,得到2021-07-11T08:49:53+08:00。
2020 年DEFCON CTF 資格賽簽到題的flag 是?找到2020 年DEFCON CTF 資格賽的網站是OOO DEF CON CTF Quals,打開第一題welcome-to-dc2020-quals,下載welcome.txt,獲得flag 為
OOO{this_is_the_welcome_flag}。
在大小為672328094 * 386900246 的方形棋盤上放3 枚(相同的)皇后且它們互不攻擊,有幾種方法?在The On-Line Encyclopedia of Integer Sequences中搜索「3 queens」,沒有直接找到的通解,但有一篇相關的文章Number of ways to place 3 nonattacking queens on an n X n board.裡面給出了通解的表達式:
任意$m\times n$棋盘上的3皇后问题

代入數據計算得2933523260166137923998409309647057493882806525577536。這裡直接用Mathematica 計算了。
上一屆(第零屆)比賽的“小北問答1202” 題目會把所有選手提交的答案存到SQLite 數據庫的一個表中,這個表名叫?在第零屆比賽的GitHub 倉庫geekgame-0th中查找,在src/choice/game/db.py中得到表名叫submits。
國際互聯網由許多個自治系統(AS)組成。北京大學有一個自己的自治系統,它的編號是?在中國AS 自治系統號碼中查找Peking University,找到編號AS59201。另一個搜索結果CNGI-BJ-IX3-AS-AP CERNET2 IX at Peking University, CN 不是正確答案。
截止到2021 年6 月1 日,完全由北京大學信息科學技術學院下屬的中文名稱最長的實驗室叫?在信息科學技術學院2021 年招生指南中找名字最長的實驗室,為「區域光纖通信網與新型光通信系統國家重點實驗室」。

共享的机器​

這題提到了「未來的機器」,是第零屆比賽的題目。通過閱讀「未來的機器」的Writeup,得知需要人腦解釋執行代碼,反推flag。猜測這題是類似的。
首先需要了解以太坊智能合約的機制。智能合約創建時需要提供一段Solidity 程序的字節碼,並且此後無法再修改。每次向該智能合約發起交易時,提供的交易信息和交易的發起者將作為程序的輸入,程序的運行結果將可以存儲到區塊鏈中,也可以通過revert提前終止,拒絕交易。程序運行時將可以訪問memory和storage。 memory類似RAM,程序終止後被銷毀,而storage是區塊鏈上的持久化存儲。
原題提供了一個bitaps 中的鏈接,可以看到2021-10-22 和2021-11-07 兩筆關鍵交易,其中2021-10-22 的交易是創建此合約。其它有很多失敗的交易,都是2021-11-14 之後的。這時題目已經發布了,因此這些失敗的交易應當不是題目的一部分。
除此之外,bitaps 上並沒有提供更詳細的信息。搜索其它CTF 競賽中有關以太坊智能合約的題目Writeup,發現了Etherscan網站,可以通過其Parity Trace 功能查看交易詳情。更令人激動的是,Etherscan 自帶Decompile Bytecode 功能,打開題目中所給的智能合約後,可利用這一功能,查看得到反編譯的源碼:
#
# Panoramix v4 Oct 2019
# Decompiled source of ropsten:0xa43028c702c3B119C749306461582bF647Fd770a
#
# Let's make the world open source
#
def storage:
stor0 is addr at storage 0
stor1 is uint256 at storage 1
stor2 is uint256 at storage 2
stor3 is uint256 at storage 3
def _fallback() payable: # default function
revert
def unknown7fbf5e5a(uint256 _param1, uint256 _param2) payable:
require calldata.size - 4=64
if stor0 !=caller:
if stor0 !=tx.origin:
if stor1 !=sha3(caller):
if stor1 !=sha3(tx.origin):
revert with 0, 'caller must be owner'
stor2=_param1
stor3=_param2
def unknownded0677d(uint256 _param1) payable:
require calldata.size - 4=32
idx=0
s=0
while idx 64:
idx=idx + 1
s=s or (Mask(256, -4 * idx, _param1) 4 * idx) + (5 * idx) + (7 * Mask(256, -4 * idx, stor2) 4 * idx) % 16 4 * idx
continue
if stor3 !=0:
revert with 0, 'this is not the real flag!'
return 1
這裡得到了兩個函數,但調用關係並不明朗。用另一個在線工具Online Solidity Decompiler 反編譯,得到了另一種表示,兩者可以互相參照。 \footnote {Online Solidity Decompiler 的反編譯結果篇幅較長,且可以在線查看,就不貼在文中了。其中重要的部分會在後文給出。 }
Online Solidity Decompiler 的結果中存在一些goto,但跳轉的地址仍在函數內部,因此還是可以比較輕鬆地理清控制流。經過分析,發現第一個函數必須由owner 發起交易才能正常返回,其作用是修改storage[2]和storage[3]。第二個函數實際上運行了一個64 次的循環,循環中不斷用或運算修改變量var0,並且用到了storage[2]存儲的數據。循環後將var0的運算結果與storage[3]比較,兩者不相同則輸出this is not the real flag!換言之,需要找出一個初始的var0,使其運算後與storage[3]相同。這個var0很可能就是我們需要的flag。
這一部分的Solidity 代碼提取出來是
var arg0=msg.data[0x04:0x24];
var var0=0x00;
var var1=0x00;
while (var10x40) {
var0=var0 | (((arg0 var1 *0x04) + var1 *0x05 + (storage[0x02] var1 *0x04) *0x070x0f) var1 *0x04);
var1 +=0x01;
}
if (var0==storage[0x03]) { return0x01; }
注意到位運算的優先級,最終被左移var1 *0x04位的內容提前經過了\0x0f運算,換言之,一次循環中var0只會至多被改變4 位,並且每次循環改變的位是互不干擾的。這使得整個運算過程是可逆的。
此外我們還需要知道storage[2]和storage[3]的值。這可以通過查看2021-11-07 的交易獲得。
查看Internal transaction的信息

這樣,就可以把反推var0的邏輯用Python 實現出來了。
stor2=0x15eea4b2551f0c96d02a5d62f84cac8112690d68c47b16814e221b8a37d6c4d3
stor3=0x293edea661635aabcd6deba615ab813a7610c1cfb9efb31ccc5224c0e4b37372
res=0
flag=[]
for i in range(0x40):
target=stor3 i * 40x0f
for ans in range(0x10):
if ans + i * 5 + (stor2 i * 4) * 70x0f==target:
flag.insert(0, ans)
print(''.join([chr(flag * 16 + flag[i + 1]) for i in range(0, len(flag), 2)]))
得到flag 為flag{N0_S3cReT_ON_EThEreuM}。

翻车的谜语人​

題目提供了一個pcap 格式的抓包數據。用Charles 打開,可以看出這是與Jupyter 交互的流量。
用Charles查看pcap

這裡可以直接把Jupyter Notebook 的內容恢復出來。
import zwsp_steg
from Crypto.Random import get_random_bytes
import binascii
def genflag():
return 'flag{%s}' % binascii.hexlify(get_random_bytes(16)).decode()
flag1=genflag()
flag2=genflag()
key=get_random_bytes(len(flag1))
def xor_each(k, b):
assert len(k)==len(b)
out=[]
for i in range(len(b)):
out.append(b ^ k)
return bytes(out)
encoded_flag1=xor_each(key, flag1.encode())
encoded_flag2=xor_each(key, flag2.encode())
with open('flag1.txt', 'wb') as f:
f.write(binascii.hexlify(encoded_flag2))
從Jupyter Notebook 的輸出可以知道key為
b'\x1e\xe0[u\xf2\xf2\x81\x01U_\x9d!yc\x8e\xce[X\r\x04\x94\xbc9\x1d\xd7\xf8\xde\xdcd\xb2Q\xa3\x8a?\x16\xe5\x8a9'
而encoded_flag1則是根據flag1與key的異或運算得到。根據異或運算的性質,將encoded_flag1與key再進行一次異或就能夠還原出flag1。
接下來搜索flag1,可以在流量中找到讀取flag1.txt文件的內容。
用Charles读取flag1.txt

由此可以還原出flag1:
flag1='788c3a1289cbe5383466f9184b07edac6a6b3b37f78e0f7ce79bece502d63091ef5b7087bc44'
flag1=binascii.unhexlify(flag1)
print(''.join([chr(flag1 ^ key) for i in range(len(flag1))]))
對於flag2,搜索後發現Jupyter 工作區存在大小為2935226 字節的7zip 文件,其內容可以完全dump 出來。但是這個壓縮文件有密碼,必須繼續挖掘。這時Charles 給出的HTTP 流量數據已經提取不到更多有用的信息,轉而使用Wireshark。果不其然,在Wireshark 中發現了Jupyter Notebook 的WebSocket 協議數據幀。
Wireshark发现命令行操作

這些WebSocket 數據幀完整地記錄了命令行操作。可以看到You ちゃん先是用pip安裝了stego-lsb,然後將flag2.txt隱寫進了ki-ringtrain.wav,最後把wav 用7za壓縮。壓縮時設置了密碼,其命令行參數為
-p'Wakarimasu! `date` `uname -nom` `nproc`'
7za的輸出裡顯示了CPU 型號為i7-10510U,這是一個4C8T 的U,故nproc輸出為8。 \footnote {如果關了超線程應該就是4? }uname -o顯然為GNU/Linux,uname -m則為x86_64。 uname -n為主機名,通過命令提示符的回顯得到you-kali-vm。
命令提示符中的you-kali-vm

至於date的輸出需要一定的猜測,因為主機的時區和語言還沒確定。並且date本身也有幾種風格的輸出,例如
Sat Nov 06 07:44:16 CST 2021
Sat 06 Nov 2021 07:44:16 AM GMT
執行命令的時間相對第一個數據幀的偏移是,推測出時間大約是2021 年11 月6 日15:44:16。當然,這是存在誤差的,實際試密碼時把都試了。比較幸運,正確的密碼手工試出來了,不然需要寫腳本遍歷不同的時區和語言:
Wakarimasu! Sat 06 Nov 2021 03:44:15 PM CST you-kali-vm x86_64 GNU/Linux 8
解壓得到wav 文件,再用stegolsb提取出隱寫的信息,正是encoded_flag2。
pip3 install stego-lsb
stegolsb wavsteg -r -i flag2.wav -o flag2.txt --bytes 76 -n 1
使用前文類似的方法,恢復出flag2 即可。

叶子的新歌​

首先用ffprobe查看mp3 文件的元信息,得到兩個關鍵的提示:
album : Secret in Album Cover!
TRACKTOTAL : aHR0cDovL2xhYi5tYXh4c29mdC5uZXQvY3RmL2xlZ2FjeS50Ynoy
這是兩個分支,後文分別敘述。

夢は時空を越えて​

用binwalk看到Album Cover是png 圖片,提取之。
Album Cover

這個圖片看上去非常正常。首先猜測是圖片大小存在問題,於是對PNG 頭進行CRC32 校驗,無異常。進而懷疑使用了隱寫技術,上StegSolve。使用LSB,提取RGB 三個通道的最低位,二進制解碼後三個大字「PNG」映入眼簾。說明思路正確,提取出一張圖片。
LSB隐写的二维码

這是一個二維碼,但並不是常見的QR 碼。扔到Google 圖片中搜索,發現其名為Aztec 碼。手機下載掃碼軟件Scandit,得到內容Gur frperg va uvfgbtenz.看上去是凱撒密碼,隨手找了一個在線工具,解碼得到The secret in histogram.
這個Aztec 碼的灰度分佈看上去就不太對勁,不過Photoshop 的直方圖不太能放大,於是用Python 腳本輸出直方圖。
from PIL import Image
import numpy as np
im=Image.open('aztec.png')
cluster=np.zeros(shape=(256))
for i in range(1000):
for j in range(1000):
cluster[im.getpixel((i, j))] +=1
img=Image.new(mode='RGB', size=(256 + 40, 50 + 10), color=(255, 255, 255))
pixels=img.load()
for i in range(len(cluster)):
if cluster 0:
for j in range(50):
pixels[i + 20, j + 5]=(0, 0, 0)
img.save('histogram.png')
直方圖如下圖所示。
二维码图片的直方图

這個直方圖怎麼看也是一個條形碼,繼續掃碼得到xmcp.ltd/KCwBa。訪問後得到一大串Ook。這是一個Brainfuck 的方言,在Ook! Programming Language - Esoteric Code Decoder, Encoder, Translator執行後得到flag 為
flag{y0u_h4ve_f0rgott3n_7oo_much}。
StegSolve 的UI 在macOS 上會有問題,可以用其他程序替代,例如zsteg 或StegOnline。

夢と現の境界​

另一個分支,aHR0cDovL2xhYi5tYXh4c29mdC5uZXQvY3RmL2xlZ2FjeS50YnoyBase64 解碼得到http://lab.maxxsoft.net/ctf/legacy.tbz2。下載解壓得到To_the_past.img。 macOS 上直接掛載磁盤鏡像,得到MEMORY.ZIP和NOTE.TXT。 NOTE.TXT中提示密碼是:賓馭令詮懷馭榕喆藝藝賓庚藝懷喆晾令喆晾懷。搜索後得知這是人民幣冠號密碼,解碼得到72364209117514983984。用該密碼解壓MEMORY.ZIP,獲得新的提示和兩個二進製文件,left.bin和right.bin。先用binwalk,沒有掃到有用的信息。提示中有「紅白機」「找不同滴神」,故使用vbindiff比較,發現確實可以找不同,但應該用最長公共子串比較,而不是按位比較。這裡偷懶了,寫了一個比較簡單的腳本,稍微處理了一下edge case,不過對於某些極端輸入會有bug。
with open('left.bin', 'rb') as f:
lbuf=f.read()
with open('right.bin', 'rb') as f:
rbuf=f.read()
lpointer=0
rpointer=0
common=[]
lonly=[]
ronly=[]
allonly=[]
while lpointe
 
返回
上方