N0PSctf Writeup
まずはこちらをご覧ください.↓
HoriK氏からの命を受けましてWriteupを書いていきたいと思います.
BabyTopia
The emperor
Ahoye! Here are the crypto newbies! Today, we are learning the basics of cryptography! Here is an encrypted message for you, try to decipher it. Learning this will help you on the day you will face CrypTopia.
Note: The flag format is B4BY{DECODEDMESSAGE}.
与えられたファイルの中を見ると変な文字列が書いていた.
$ cat challenge.txt Ea, kag pqoapqp uf tgt? Ftqz, tqdq ue ftq rxms: UVGEFPQOAPQPMOMQEMDOUBTQDITUOTYMWQEYQMBDARQEEUAZMXODKBFATQDA
シーザー暗号と見立てCyberChefで変換してみる.
ROT14で見えましたね.ということで,
Flag : B4BY{IJUSTDECODEDACAESARCIPHERWHICHMAKESMEAPROFESSIONALCRYPTOHERO}
Unknown File
Hello young trainees! Today, we are studying digital forensics! This may be useful if one day you have to face PwnTopia...
Here is a file, you have to find a way to read its content. Good luck!
まず,拡張子のないファイルが渡されますので,fileコマンドでファイルの種類を確認します.
$ file challenge challenge: PDF document, version 1.6, 1 pages (zip deflate encoded
すると,pdfファイルということが分かったのでmvコマンドで拡張子を付けpdfファイルを開きます.
$ mv challenge challenge.pdf
Flag : B4BY{h1dD3n_PDF!}
Read the Bytes!
Look who's there! New students! Fine, this time we will focus on reverse engineering. This could help you against PwnTopia one day!
I give you now a Python program and its output. Try to understand how it works!
provide code
from flag import flag # flag = b"XXXXXXXXXX" for char in flag: print(char) # 66 # 52 # 66 # 89 # 123 # 52 # 95 # 67 # 104 # 52 # 114 # 97 # 67 # 55 # 51 # 114 # 95 # 49 # 115 # 95 # 74 # 117 # 53 # 116 # 95 # 52 # 95 # 110 # 85 # 109 # 56 # 51 # 114 # 33 # 125
125がmaxで,おそらくASCIIに変換するのだろう.
ということで,コード書きます.
# soloved.py ascii_codes = [ 66, 52, 66, 89, 123, 52, 95, 67, 104, 52, 114, 97, 67, 55, 51, 114, 95, 49, 115, 95, 74, 117, 53, 116, 95, 52, 95, 110, 85, 109, 56, 51, 114, 33, 125 ] characters = [] for code in ascii_codes: character = chr(code) characters.append(character) flag = ''.join(characters) print(flag)
$ python3 solved.py B4BY{4_Ch4raC73r_1s_Ju5t_4_nUm83r!}
Falg : B4BY{4_Ch4raC73r_1s_Ju5t_4_nUm83r!}
Tak Tak
Aloha querid@s! ^^
It's finally time to dive into the fascinating world of OSINT, that magical word that excites digital detectives and gives regular folks the chills. Today, we’ll be starting with the baaasic stuff: just a bit of reverse image searching to warm up!
Now, story time: Alice has stumbled into N0PStopia and found herself in a surreal place, lined entirely with chairs. 🪑
Will you be able to figure out where this tunnel is, when it opened, and, just for fun, how many chairs are in there? It seemed like it stretches into infinity.. x)
Flag format : B4BY{Place-Location_Opening-Date_chairs-number}
Example : B4BY{Lectures-Tower-N0PSTopia_June-1st-2025_505}
Note : Make sure all the parts of your flag (even country names) are in English.
Google Lensに突っ込むと,「Designmuseum Denmark」というところにヒットする.
そこからいろいろ探していくと以下のサイトからflagに必要な情報が得られる.
Flag : B4BY{Designmuseum-Danmark_June-7th-2024_125}
N0PStopia
G-Bee-S
There is a bee living in N0PStopia, named Valentine. She loves waterlilies, and lots of them are located around her beehive. However, she needs some help to find a good way to visit all of them while tralling the shortest distance, can you help her? She does not need the best path, but at least one that is good enough.
Note: Each line is the coordinates of a waterlily. The beehive is located in (0,0), and the path has to start from there and end there. Each flower has to be visited one time, but not more. When submitting, you have to send the order of flowers to be visited. For instance, if your path is to visit the 2nd flower, then the 3rd, then the 1st, you will send 0 2 3 1 0, where 0 represents the beehive. In order to be valid, your path has to have a total length of less than 1400.
Author: algorab
nc 0.cloud.chals.io 13055
provide text file
-62 -67 -8 44 44 17 -91 -74 -56 18 96 -19 45 -67 -28 62 94 69 48 52 -11 64 -95 -57 -2 79 34 40 -5 24 -35 -50 -40 72 -25 -4 -75 -98 6 98 -87 -37 -63 99 -96 86 28 65 -87 26 53 -2 -98 7 69 -71 18 41 -84 51 -80 -10 50 39 13 -89 4 35 31 95 84 -50 86 -82 32 -21 -36 -22 34 -77 -77 -78 -92 -2 72 -54 88 -29 1 -14 -82 97 -16 -70 -19 96 -41 41 -24 -87
問題の概要
- 原点 (0, 0) を含む座標リストが与えられます.
- これらの全てを1回ずつ訪問し,出発地点に戻るという巡回セールスマン問題(TSP)を解く必要があります.
- 経路の長さが1400未満であれば,フラグが与えられます.
- 原点 (0, 0) を含む座標リストが与えられます.
解法 TSPを解くにはいろいろありますが,今回はgreedyと2-optを使って解きました.
以下,solverです.
# solved.py import math import pexpect coordinates = [ (-62, -67), (-8, 44), (44, 17), (-91, -74), (-56, 18), (96, -19), (45, -67), (-28, 62), (94, 69), (48, 52), (-11, 64), (-95, -57), (-2, 79), (34, 40), (-5, 24), (-35, -50), (-40, 72), (-25, -4), (-75, -98), (6, 98), (-87, -37), (-63, 99), (-96, 86), (28, 65), (-87, 26), (53, -2), (-98, 7), (69, -71), (18, 41), (-84, 51), (-80, -10), (50, 39), (13, -89), (4, 35), (31, 95), (84, -50), (86, -82), (32, -21), (-36, -22), (34, -77), (-77, -78), (-92, -2), (72, -54), (88, -29), (1, -14), (-82, 97), (-16, -70), (-19, 96), (-41, 41), (-24, -87) ] points = [(0, 0)] + coordinates def dist(p1, p2): return math.hypot(p1[0] - p2[0], p1[1] - p2[1]) def greedy_tsp(points): n = len(points) unvisited = list(range(1, n)) path = [0] while unvisited: last = path[-1] next_city = min(unvisited, key=lambda i: dist(points[last], points[i])) path.append(next_city) unvisited.remove(next_city) path.append(0) return path def total_length(path): return sum(dist(points[path[i]], points[path[i+1]]) for i in range(len(path)-1)) def two_opt(path, points): best = path[:] improved = True while improved: improved = False for i in range(1, len(path) - 2): for j in range(i + 1, len(path) - 1): if j - i == 1: continue new_path = path[:i] + path[i:j][::-1] + path[j:] if total_length(new_path) < total_length(best): best = new_path improved = True path = best return best path = greedy_tsp(points) path = two_opt(path, points) length = total_length(path) print("Path:", path) print("Total length:", length) if length < 1400: answer = " ".join(map(str, path)) conn = pexpect.spawn("nc 0.cloud.chals.io 13055", timeout=10) conn.expect("Enter the path Valentine should follow:") conn.sendline(answer) conn.interact() else: print("Total length exceeds 1400")
$ python3 solved.py Path: [0, 45, 18, 39, 16, 47, 50, 1, 41, 19, 4, 12, 21, 31, 42, 27, 25, 5, 49, 17, 8, 11, 2, 15, 34, 29, 14, 10, 32, 3, 26, 38, 33, 40, 7, 28, 37, 43, 36, 44, 6, 9, 24, 35, 20, 13, 48, 22, 46, 23, 30, 0] Total length: 1377.6346187253687 >>> 0 45 18 39 16 47 50 1 41 19 4 12 21 31 42 27 25 5 49 17 8 11 2 15 34 29 14 10 32 3 26 38 33 40 7 28 37 43 36 44 6 9 24 35 20 13 48 22 46 23 30 0 The length of your path is 1377.6346187253687. Well done! Thanks to you, Valentine can visit all the waterlillies <3 Here is your flag: N0PS{w4t3rl1ll13s_f0r_v4l3nt1n3}
Flag : N0PS{w4t3rl1ll13s_f0r_v4l3nt1n3}
Musical Encounter
n00psy takes you to a nature walk in N0PStopia. On your way, you meet a group of dancing skeletons! What an amazing adventure to see and hear. I wonder what they talk about...
与えられた動画には何もなかったので音声を見てみる.
そのためにはwav形式の音声ファイルにしないといけないので,ffmpegで音声を抽出する.
ffmpeg -i dancing_skeletons.mp4 -vn -acodec pcm_s16le -ar 44100 -ac 1 audio.wav
このaudio.wav
をwindowsのaudaciryで見てみる.
波形では何も見えないのでスペクトログラムで見てみるとFlagが見えます.
Flag : N0PS{c4rT5s0N8z}
CrypTopia
Break My stream
CrypTopia is testing their next gen encryption algorithm. We believe that the way they implemented it may have a flaw...
Author: algorab
nc 0.cloud.chals.io 31561
provide code
import os class CrypTopiaSC: @staticmethod def KSA(key, n): S = list(range(n)) j = 0 for i in range(n): j = ((j + S[i] + key[i % len(key)]) >> 4 | (j - S[i] + key[i % len(key)]) << 4) & (n-1) S[i], S[j] = S[j], S[i] return S @staticmethod def PRGA(S, n): i = 0 j = 0 while True: i = (i+1) & (n-1) j = (j+S[i]) & (n-1) S[i], S[j] = S[j], S[i] yield S[((S[i] + S[j]) >> 4 | (S[i] - S[j]) << 4) & (n-1)] def __init__(self, key, n=256): self.KeyGenerator = self.PRGA(self.KSA(key, n), n) def encrypt(self, message): return bytes([char ^ next(self.KeyGenerator) for char in message]) def main(): flag = b"XXX" key = os.urandom(256) encrypted_flag = CrypTopiaSC(key).encrypt(flag) print("Welcome to our first version of CrypTopia Stream Cipher!\nYou can here encrypt any message you want.") print(f"Oh, one last thing: {encrypted_flag.hex()}") while True: pt = input("Enter your message: ").encode() ct = CrypTopiaSC(key).encrypt(pt) print(ct.hex()) if __name__ == "__main__": main()
問題概要
- サーバはランダムな鍵でフラグを暗号化し,平文を入力すると暗号文を返してくれる.
- 鍵 (key) は内部的にランダム生成され,KSA/PRGAでキーストリームが生成される.
- 入力したメッセージに対して同じキーストリームで暗号化が行われる.
脆弱性はどこか
- この実装の最大の問題点は,一度生成されたキーストリームを繰り返し使っていることである.つまり,フラグの暗号化にも,ユーザが入力した任意の平文の暗号化にも同じキーストリームが使われているため,次の式が成り立つ
ciphertext = plaintext ^ keystream
- この実装の最大の問題点は,一度生成されたキーストリームを繰り返し使っていることである.つまり,フラグの暗号化にも,ユーザが入力した任意の平文の暗号化にも同じキーストリームが使われているため,次の式が成り立つ
攻撃方法
1. サーバから encrypted_flag を受け取る.
2. 同じ長さの "A" * len(flag) を送って暗号文を取得.
3. "A" = 0x41 なので,暗号文と0x41をXORすればキーストリームが手に入る.
4. そのキーストリームを使って,encrypted_flag を復号する.
# solved.py from pwn import remote r = remote('0.cloud.chals.io', 31561) r.recvuntil(b"Oh, one last thing: ") enc_flag = bytes.fromhex(r.recvline().strip().decode()) # 入力プロンプトの待ち受け r.recvuntil(b"Enter your message: ") r.sendline(b"A" * len(enc_flag)) # 受信行を取得して暗号文に変換 cipher_hex = r.recvline().strip() try: cipher = bytes.fromhex(cipher_hex.decode()) except ValueError: print(f"[!] Unexpected server response: {cipher_hex}") exit(1) # keystreamを復元 keystream = bytes([c ^ 0x41 for c in cipher]) # flagを復元 flag = bytes([enc_flag[i] ^ keystream[i] for i in range(len(enc_flag))]) print(f"Flag: {flag.decode()}")
$ python3 solved.py [+] Opening connection to 0.cloud.chals.io on port 31561: Done Flag: N0PS{u5u4L_M1sT4k3S...} [*] Closed connection to 0.cloud.chals.io port 31561
Flag : N0PS{u5u4L_M1sT4k3S...}
WebTopia
Press Me If U Can
-"Stop chasing me!" said the button.
-"We can't. It's our job." they answered.
Trapped in WebTopia on a page with no way out, except for the fleeing button. Can you break free?
flagをgetするにはPress Meと書いているボタンを押す必要がある.
しかし,マウスカーソルを近づけるとPress Meが離れていくので押すことができない.
そこで,このボタンを動かないようにして押せるようにする.
script.js
const btn = document.querySelector("button"); const OFFSET = 100; const testEdge = function (property, axis) { if (endPoint[property] <= 0) { endPoint[property] = axis - OFFSET; } else if (endPoint[property] >= axis) { endPoint[property] = OFFSET; } }; let endPoint = { x: innerWidth / 2, y: innerHeight * 2 / 3 }; addEventListener("mousemove", (e) => { const btnRect = btn.getBoundingClientRect(); const angle = Math.atan2(e.y - endPoint.y, e.x - endPoint.x); const distance = Math.sqrt( Math.pow(e.x - endPoint.x, 2) + Math.pow(e.y - endPoint.y, 2) ); if (distance <= OFFSET) { endPoint = { x: OFFSET * -Math.cos(angle) + e.x, y: OFFSET * -Math.sin(angle) + e.y }; } btn.style.left = endPoint.x + "px"; btn.style.top = endPoint.y + "px"; btn.disabled = true; testEdge("x", innerWidth); testEdge("y", innerHeight); }); // Select all pupils const pupils = document.querySelectorAll('.pupil'); // Add an event listener for mouse movement document.addEventListener('mousemove', (event) => { const { clientX: mouseX, clientY: mouseY } = event; // Adjust each pupil position pupils.forEach((pupil) => { const eye = pupil.parentElement; // Get the bounding box of the eye const { left, top, width, height } = eye.getBoundingClientRect(); // Calculate the center of the eye const eyeCenterX = left + width / 2; const eyeCenterY = top + height / 2; // Calculate the offset for the pupil based on the eye center const dx = mouseX - eyeCenterX; const dy = mouseY - eyeCenterY; // Normalize the movement within a range const maxOffsetX = width * 0.25; // Adjust range for horizontal movement const maxOffsetY = height * 0.25; // Adjust range for vertical movement const offsetX = Math.max(-maxOffsetX, Math.min(maxOffsetX, dx * 0.1)); const offsetY = Math.max(-maxOffsetY, Math.min(maxOffsetY, dy * 0.1)); // Set the pupil position pupil.style.transform = `translate(${offsetX}px, ${offsetY}px)`; }); });
ボタンが逃げる動作はbtn
に対する操作なので,btn
が参照しているDOMノードを置き換えることで,スクリプト側のbtn
は「過去の要素」を見続ける.
つまり,ボタンを外見そのままに差し替えればスクリプトの制御から外れる.
Google Chrome のDev ToolsにあるConsoleで以下のスクリプトを書く.
let old_element = document.querySelector("button"); // 元のボタンを取得 let new_element = old_element.cloneNode(true); // ボタンを複製 old_element.parentNode.replaceChild(new_element, old_element); // DOM上で入れ替え
こうするとボタンが止まり,押すことができる.
押したらflagが表示される.
Flag : N0PS{W3l1_i7_w4S_Ju5T_F0r_Fun}