Hack The Box(HTB) - CTF Try Out : FlagCasino Walkthrough
FlagCasino Walkthrough
進入到 Reversing 類型的 CTF,這一題顯示如下
又是一題僅有檔案沒有機器的 CTF
下載後看來是個執行檔
執行後是個機器人要你下注,輸入數字、?、help 等都直接跳出
測試到 flag 的格式 HTB{.........} 發現終於有回應了
看來就是輸入正確的 flag 就過關了
解法一 : 慢慢一個一個數字與大小寫英文輸入測試,答案就出來了
但我如果知道 flag 是甚麼的話,幹嘛還要一一測試
這太浪費時間了,無法這麼做
解法二 : 寫一個程式自動去跑將答案跑出來
不過到底有幾碼這是個問題,所以看來還是要拆解程式看看有無甚麼可以發現的
先分析一下檔案,是個 ELF shared library 檔案
使用 ghidra 建立專案匯入檔案 casino
用 Open With --> CodeBrowser 開啟該檔案
在 Decompile: main 視窗查看邏輯
整段 Code 如下
undefined8 main(void)
{
int iVar1;
char local_d;
uint local_c;
puts("[ ** WELCOME TO ROBO CASINO **]");
puts(
" , ,\n (\\____/)\n (_oo_)\n (O)\n __||__ \\)\n []/______\\[] /\n / \\______/ \\/\n / /__\\\n(\\ /____\\\n---------------------"
);
puts("[*** PLEASE PLACE YOUR BETS ***]");
local_c = 0;
while( true ) {
if (0x1c < local_c) {
puts("[ ** HOUSE BALANCE $0 - PLEASE COME BACK LATER ** ]");
return 0;
}
printf("> ");
iVar1 = __isoc99_scanf(&DAT_001020fc,&local_d);
if (iVar1 != 1) break;
srand((int)local_d);
iVar1 = rand();
if (iVar1 != *(int *)(check + (long)(int)local_c * 4)) {
puts("[ * INCORRECT * ]");
puts("[ *** ACTIVATING SECURITY SYSTEM - PLEASE VACATE *** ]");
/* WARNING: Subroutine does not return */
exit(-2);
}
puts("[ * CORRECT *]");
local_c = local_c + 1;
}
/* WARNING: Subroutine does not return */
exit(-1);
}
1 : 顯示歡迎畫面與下注提示,只是氣氛用。
puts("[ ** WELCOME TO ROBO CASINO **]");
puts(/* 機器人 ASCII Art 省略 */);
puts("[*** PLEASE PLACE YOUR BETS ***]");
2. 迴圈控制變數
uint round = 0; // local_c:目前進行到第幾輪(0-based)
char input; // local_d:玩家每回輸入 1 byte
3. 主迴圈 : 28 個迴圈,所以總數字看來有 29 個
依據 flag 格式來看 HTB{.......},扣除前後與大括號,中間有 24 個數值
while (1) {
if (round > 0x1c) { // 0x1c == 28
puts("[ ** HOUSE BALANCE $0 - PLEASE COME BACK LATER ** ]");
return 0; // 全 28 輪都過關才會走到這裡 → 成功
}
printf("> "); // 提示 → 玩家輸入一個字元
if (__isoc99_scanf("%c", &input) != 1)
exit(-1); // 讀取失敗就直接結束
srand((int)input); // **重設 RNG 種子為剛輸入的單一字節**
int r = rand(); // 拿第一筆隨機數
if (r != check[round]) { // check[] 是全域 int[28] 對照表
puts("[ * INCORRECT * ]");
puts("[ *** ACTIVATING SECURITY SYSTEM - PLEASE VACATE *** ]");
exit(-2); // 只要有一輪不對 → 立即 Game Over
}
puts("[ * CORRECT * ]"); // 這一輪比對成功
round++; // 進到下一輪
}
glibc 的 rand()
第一次呼叫就會使用
所以對於 固定種子 結果唯一,方便把「輸入字元」→「檢查值」做 1-對-1 映射。
4. 關卡條件 :
共 28 輪 |
0 … 0x1c(28)全部對才跳出迴圈顯示成功句子 → 旗標。 |
每輪重新 srand() |
把原本 31-bit LCG 的「長期狀態」打斷成 28 個獨立的小挑戰;只要列舉 0-255 就能找到使 rand() 輸出匹配的種子。 |
Input = char |
scanf("%c") 會讀一個 signed char(-128…127),再轉成 int 給 srand() 。攻擊時要注意正負號對應。 |
結論 :
程式其實是「28 次 猜種子 遊戲」:你輸入 1 byte,程式用它重設亂數種子並比對 rand()
的輸出。因為 byte 空間只有 256 種,對照表又硬編在檔案裡,所以可以脫機爆破出每一輪要輸的字元,拼起來就是最終的 HTB flag。
寫一段 Python Code 如下自動去跑
#!/usr/bin/env python3
import ctypes, struct
libc = ctypes.CDLL("libc.so.6")
libc.srand.argtypes = [ctypes.c_uint]
libc.rand.restype = ctypes.c_int
# 28 個 int,直接從檔案 offset 0x3080 讀出
table = struct.unpack("<28I", open("casino", "rb").read()[0x3080:0x3080+28*4])
flag = []
for target in table:
for c in range(256): # 全 1-byte 種子
libc.srand(ctypes.c_uint(ctypes.c_int8(c).value).value)
if libc.rand() == target: # 唯一對應
flag.append(chr(c))
break
print("flag =", "".join(flag) + "}")
取名叫 "python_run"
執行後獲得 flag : HTB{r4nd_1s_v3ry_pr3d1ct4bl3}
整題白話說明如下
遊戲規則 :
-
每回合 給機器人一張字卡(上面印 1 個字元,像 H、T、B…)。
-
機器人收到字卡後,會把字卡藏進口袋,立刻做一次心算,得出一個很大的數字。
-
牠把這個數字跟自己事先準備好的「答案表」做比對:
-
如果吻合,牠說:「✔️ 對了,再來下一張!」
-
不吻合就直接把你趕走。
-
-
一共要連續猜對 28 次,才能拿到獎品──旗標(flag)。
重點:機器人心算的規則沒有變,只跟「你給的那張字卡」有關。
破綻在哪裡?
-
機器人心算用的公式是一個固定且簡單的算式。
-
最要命的是,牠的「答案表」就直接放在機器人身體裡,沒有鎖!
(我們把檔案打開,就能看到 28 個對答案。) -
既然心算公式固定+答案表也偷看得到,
那麼 每一張字卡該用哪個字母,就可以事先算出來。
如何破解 :
-
打開機器人 → 把「28 格答案表」抄下來。
-
做張對照表
-
把 256 種可能字卡 (所有字元) 一張張塞給機器人試算。
-
找到「哪張字卡 → 算出來會對應答案表的第 1 格」。
-
重複 28 次,得到 28 張正確字卡。
-
依據程式來看
新數字 = (紙條 × 1 103 515 245 + 12 345) 再取餘數
作者把 28 個印表機號碼(對應 28 輪)直接貼在牆上,長這樣(節錄前 5 筆):
輪次 | 牆上的號碼 |
---|---|
1 | 608 905 406 |
2 | 183 990 277 |
3 | 286 129 175 |
4 | 128 959 393 |
5 | 1 795 081 523 |
... | ... (共 28 個) |
我們的任務:找到「哪張紙條 → 印出該號碼」;把它循序送進程式就通關。
開始對照──逐格試 256 張紙條就夠了
為什麼只要 256 張?
-
程式每一輪都用 1 個位元組(byte) 當種子。
-
一個 byte 只有 256 種可能值(–128 ~ 127)。
-
可以把它想成 256 張編號 0~255 的紙條。
-
操作流程(以第 1 輪為例)
-
拿紙條 #0 → 塞給機器人 → 得到號碼 1。
-
比對牆上:不是 608 905 406 → 換下一張。
-
反覆到 紙條 #72 時,印出的號碼終於是 608 905 406!
-
那就記下:第 1 輪正確紙條 = #72。
因為「印表機公式」在 0~255 這個小範圍內不會重複,所以 一定至多只會有 1 張紙條對得上。
只要 256 次以內必能找到。
全部 28 輪
把這樣的「盲盒試紙條」流程做 28 次,就得到一條表格:
輪次 | 對上的紙條編號 | 轉成字元 (ASCII) |
---|---|---|
1 | 72 | H |
2 | 84 | T |
3 | 66 | B |
4 | 123 | { |
5 | 114 | r |
... | ... | ... |
28 | 125 | } |
把字元串起來,就是旗標
依序排列剛才 28 個字元:
合體後得到最終答案:
結論 :
-
低熵種子
-
只用 1 byte 當種子,搜尋空間極小(256 種)。
-
-
公式已公開
-
rand()
的 LCG 參數(1 103 515 245 與 12 345)是眾所皆知、寫在手冊裡的常數。
-
-
答案直接外洩
-
把 28 個目標數字硬寫在程式的資料區,不經任何加密。
-
只要同時滿足以上三點,就像出考試題目卻把答案抄在黑板上──自然人人都能把正確輸入反推回來。
Python Code 只是把「牆上貼的 28 個號碼」丟回同一台固定公式的印表機,逐張試 256 張紙條,找出哪張能列印出對應號碼;把 28 張紙條的 ASCII 字元串起來,就拿到 flag
留言
張貼留言