N0PSctf 2024
N0PSCTF 2024
CRYPTOGRAPHY WRITEUP
Author:
Pham Quoc Trung
Used Language:
Python3
Problem Solving:
Crypto Rookie
Description:
Hey Rookie, decode this and I'll give you a cookie!
STSABEAOE OEIEALGSETRHNCOI MMFITTAK
The flag's format is N0PS{DECODEDMESSAGE}
.
Solution:
Đưa vào một trang Cipher Identifier, ta sẽ ra được đây là Rail-fence Cipher.
Đưa vào tool để decode và mình ra được flag.

Flag: N0PS{SOMETIMESAFLAGISBETTERTHANACOOKIE}
Broken OTP
Description:
i heard OTP it the most secure encryption algorithm ever
sc nopsctf-broken-otp.chals.io
Attachments:
main.py
import random
secret = 'XXXXXXXXXXXXXXXXXXXX'
PINK = 118
RED = 101
YELLOW = 97
GREEN = 108
BLACK = __builtins__
PURPLE = dir
e = getattr(BLACK, bytes([RED, PINK, YELLOW, GREEN]).decode())
g = e(''.__dir__()[4].strip('_')[:7])
b = g(BLACK, PURPLE(BLACK)[92])
i = g(BLACK, PURPLE(BLACK)[120])
t = ['74696d65', '72616e646f6d', '5f5f696d706f72745f5f', '726f756e64', '73656564']
d = lambda x: b.fromhex(x).decode()
fb = g(i, PURPLE(i)[-6])
_i = lambda x: e(d(t[2]))(x)
s = lambda: g(BLACK,d(t[3]))(g(_i(d(t[0])), d(t[0]))()) + fb(secret.encode())
r = g(_i(d(t[1])), d(t[4]))
def kg(l):
return bytes([random.randint(0,255) for i in range(l)])
def c(p):
k = kg(len(p))
return bytes([k[i] ^ p[i] for i in range(len(p))]).hex()
if __name__ == '__main__':
r(s())
print("Welcome to our encryption service.")
choice = input("Choose between:\n1. Encrypt your message.\n2. Get the encrypted secret.\nEnter your choice: ")
match choice:
case "1":
message = input("Please enter the message you wish to encrypt: ")
print(f"Your encrypted message is: {c(message.encode())}")
case "2":
print(f"The secret is: {c(secret.encode())}")
case _:
print("Invalid option!")
Dockerfile
FROM ubuntu:latest
WORKDIR /app
RUN apt-get update && apt-get install ncat python3.12 -y
COPY ./src/main.py ./main.py
EXPOSE 1234
CMD ncat -l -p 1234 --sh-exec "python3.12 /app/main.py" --keep-open
Recon:
Sau khi thử decode e
bằng tay, mình ra được hàm eval()
. Có vẻ đây là một chương trình bị obfuscate. Các bạn có thể deobfuscate bằng cách chạy từng dòng code để xem output (nhớ sử dụng python 3.12 vì kết quả sẽ khác nhau với mỗi phiên bản python, đây là lí do ta có Dockerfile)
Tuy nhiên, mình sẽ sử dụng GPT để deobfuscate hộ mình:

Bỏ qua vấn đề về chính tả các thứ thì GPT làm mọi thứ khá tốt. Ở đây mình thấy được chương trình dạng như sau
import random
import time
# Define the secret key
secret = 'YourSecretKeyHere'
# Helper function to convert hex to string
def hex_to_str(hex_str):
return bytes.fromhex(hex_str).decode()
# Function to generate random bytes
def kg(length):
return bytes([random.randint(0, 255) for _ in range(length)])
# Function to perform XOR encryption
def encrypt(message, key):
return bytes([message[i] ^ key[i] for i in range(len(message))]).hex()
# Main execution flow
if __name__ == '__main__':
# Generate the seed using the current time
seed = round(time.time()) + int(secret.encode().hex(), 16)
random.seed(seed)
print("Welcome to our encryption service.")
choice = input("Choose between:\n1. Encrypt your message.\n2. Get the encrypted secret.\nEnter your choice: ")
match choice:
case "1":
message = input("Please enter the message you wish to encrypt: ")
key = kg(len(message))
encrypted_message = encrypt(message.encode(), key)
print(f"Your encrypted message is: {encrypted_message}")
case "2":
key = kg(len(secret))
encrypted_secret = encrypt(secret.encode(), key)
print(f"The secret is: {encrypted_secret}")
case _:
print("Invalid option!")
Ở đây ta chỉ nhìn vào logic của chương trình (vì đây là code gen minh họa nên chắc sẽ không chạy được). Có thể thấy đây chỉ đơn giản là chương trình mã hóa bằng cách gen ra một keystream bytes random.randint(0, 255)
và xor với message tùy ý (option 1) hoặc secret (option 2). Xem chừng có vẻ khá là secure cho tới khi mình thấy dòng này:
seed = round(time.time()) + int(secret.encode().hex(), 16)
random.seed(seed)
Seed của hàm random
sẽ được set bằng cách lấy time hiện tại của hệ thống tính theo giây + secret ở dạng số nguyên. Do secret là không đổi và time là tính theo giây, nếu mình có thể kết nối tới 2 lần trong cùng 1 giây, cả 2 lần đó đều sẽ có cùng 1 seed.
Khi đó, mình sẽ dùng option số 1 với 1 message dài và lấy ra keystream đó bằng cách xor plaintext với ciphertext thu được. Vì cùng 1 seed nên keystream này sẽ có đoạn từ [0:len(secret)] trùng với keystream tạo ra ở option 2. Cuối cùng, ta chỉ cần xor ciphertext của secret với đoạn keystream này là sẽ ra được flag.
import threading # To make sure I can connect 2 times in 1 second
from pwn import *
def connect1(output, index):
conn = remote("nopsctf-broken-otp.chals.io", 443, ssl=True)
conn.recvline()
conn.recvline()
conn.recvline()
conn.recvline()
conn.sendline(b'1')
plaintext = b'A' * 19 # I found it so I change to make it more beautiful only
conn.sendline(plaintext)
ciphertext = conn.recvline().decode().strip()[92:]
output[index] = xor(plaintext, bytes.fromhex(ciphertext)).hex()
def connect2(output, index):
conn = remote("nopsctf-broken-otp.chals.io", 443, ssl=True)
conn.recvline()
conn.recvline()
conn.recvline()
conn.recvline()
conn.sendline(b'2')
flag_encrypted = conn.recvline().decode().strip()[34:]
output[index] = flag_encrypted
output = [None, None]
thread1 = threading.Thread(target=connect1, args=(output, 0))
thread2 = threading.Thread(target=connect2, args=(output, 1))
thread1.start()
thread2.start()
thread1.join()
thread2.join()
key = bytes.fromhex(output[0])
flag_encrypted = bytes.fromhex(output[1])
flag = xor(key, flag_encrypted)
print("Flag:", flag)
Thật ra có thể gửi
\x00
thay vìA
, khi đó ciphertext trả về sẽ chính là key luôn.
Flag: N0PS{0tP_k3Y_r3u53}
Side Channel
Description:
Your goal is to extract the key used for the encryption of the captured data.
All blocks have been encrypted in Electronic Codebook (ECB) mode.
All data blocks are in hexadecimal, byte 0 first, byte 15 last. First column: 128 bits input block, second column: 128 bits encrypted block, third column: encryption time.
The key length is 128 bits.
The format of the flag is N0PS{key}
where key is the 32 digits hexadecimal representation of the secret key you found, byte 0 first, byte 15 last, with everything in capital letters.
If the correct key was 0102030405060708090A0B0C0D0E0F the following command would output the first ciphertext bloc in ta.dat: printf "$(sed -n 's/ .*//;s/\(..\)/\\x\1/gp;q' ta.dat)" | openssl enc -aes-128-ecb -nosalt -nopad -K 0102030405060708090A0B0C0D0E0F | od -v -A none -tx1
Hint 1: https://en.wikipedia.org/wiki/Timing_attack
Hint 2: http://www.ee.unb.ca/cgi-bin/tervo/calc2.pl?num=42&den=16&f=m&p=36&d=1&y=1&m=1
Attachments:
Solution:
<Not Yet>
Last updated