Wargame chill chill¶
Để tránh vấn đề bản quyền và bị soi bởi cộng đồng toxic nào đó thì mình sẽ không để đề ở đây.
Đã giải:
Bounded Noise (đã viết lời giải)
Forbiden Fruit
Noise Free
Too Many Errors
Pad-Thai
Paper Plane
Bound noise¶
import random
import json
from sage.all import *
FLAG = b"crypto{?????????????????????????????????????????}"
def keygen(secret, q, erange=range(2)):
n, m = len(secret), len(secret) ** 2
A = Matrix(GF(q), m, n, lambda i, j: randint(0, q - 1))
e = vector(random.choices(erange, k=m), GF(q))
b = (A * secret) + e
return A, b, e
flag_int = int.from_bytes(FLAG, "big")
q = 0x10001
secret_key = []
while flag_int:
secret_key.append(flag_int % q)
flag_int //= q
secret_key = vector(GF(q), secret_key)
A, b, e = keygen(secret_key, q)
with open("output.txt", "w") as f:
json.dump({"A": str(list(A)), "b": str(b)}, f)
Đây là một bài LWE cơ bản.
Giả sử flag là một số nguyên \(M\). Chọn số nguyên tố \(q = 65537\) và biểu diễn \(M\) ở dạng cơ số \(q\) thì ta được vector
Secret sẽ là vector \((s_0, \ldots, s_{n-1})\) và mình kí hiệu ngắn gọn là \(\bm{s}\).
Chọn ngẫu nhiên ma trận \(\bm{A}\) kích thước \(m \times n\) với các phần tử thuộc \(\mathbb{F}_q\), trong đó \(n\) là độ dài vector \(\bm{s}\) và \(m = n^2\).
Sau đó chọn ngẫu nhiên vector \(\bm{e}\) độ dài \(m\) với các phần tử thuộc \(\{ 0, 1 \}\). Tính vector \(\bm{b} = \bm{A} \cdot \bm{s} + \bm{e}\).
Khi đó public key là ma trận \(\bm{A}\) và vector \(\bm{b}\). Ta cần tìm flag là secret key.
import json
import numpy as np
from sage.all import GF
js = json.loads(open("output.txt").read())
A = eval(js["A"])
b = eval(js["b"])
q = 0x10001
# https://github.com/jvdsn/crypto-attacks/blob/master/attacks/lwe/arora_ge.py
def attack(q, A, b, E, S=None):
"""
Recovers the secret key s from the LWE samples A and b.
More information: "The Learning with Errors Problem: Algorithms" (Section 1)
:param q: the modulus
:param A: the matrix A, represented as a list of lists
:param b: the vector b, represented as a list
:param E: the possible error values
:param S: the possible values of the entries in s (default: None)
:return: a list representing the secret key s
"""
m = len(A)
n = len(A[0])
gf = GF(q)
pr = gf[tuple(f"x{i}" for i in range(n))]
gens = pr.gens()
f = []
for i in range(m):
p = 1
for e in E:
p *= (b[i] - sum(A[i][j] * gens[j] for j in range(n)) - e)
f.append(p)
if S is not None:
# Use information about the possible values for s to add more polynomials.
for j in range(n):
p = 1
for s in S:
p *= (gens[j] - s)
f.append(p)
s = []
for p in pr.ideal(f).groebner_basis():
assert p.nvariables() == 1 and p.degree() == 1
s.append(int(-p.constant_coefficient()))
return s
s = attack(q, A, b, range(2))
flag = 0
for i in s[::-1]:
flag = flag * q + i
print(int.to_bytes(flag, flag.bit_length() // 8 + 1, 'big'))
[TODO] Cần hiểu cách thực hiện của đoạn code kia.
Forbiden fruit¶
from Crypto.Cipher import AES
from Crypto.Util.number import long_to_bytes, bytes_to_long
from sage.all import *
import requests
import json
FLAG = b'crypto{}'
KEY = b'deadbeaf00112233'
IV = b'dddduuuupppp'
assert len(IV) == 12
def decrypt(nonce, ciphertext, tag, associated_data):
ciphertext = bytes.fromhex(ciphertext)
tag = bytes.fromhex(tag)
header = bytes.fromhex(associated_data)
nonce = bytes.fromhex(nonce)
if header != b'CryptoHack':
return {"error": "Don't understand this message type"}
cipher = AES.new(KEY, AES.MODE_GCM, nonce=nonce)
encrypted = cipher.update(header)
try:
decrypted = cipher.decrypt_and_verify(ciphertext, tag)
except ValueError as e:
return {"error": "Invalid authentication tag"}
if b'give me the flag' in decrypted:
return {"plaintext": FLAG.encode().hex()}
return {"plaintext": decrypted.hex()}
def encrypt(plaintext):
plaintext = bytes.fromhex(plaintext)
header = b"CryptoHack"
cipher = AES.new(KEY, AES.MODE_GCM, nonce=IV)
encrypted = cipher.update(header)
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
if b'flag' in plaintext:
return {
"error": "Invalid plaintext, not authenticating",
"ciphertext": ciphertext.hex(),
}
return {
"nonce": IV.hex(),
"ciphertext": ciphertext.hex(),
"tag": tag.hex(),
"associated_data": header.hex(),
}
def xor(a, b):
return bytes(x^y for x, y in zip(a, b))
def byte_to_pol(block):
n = int.from_bytes(block, 'big')
nn = list(map(int, bin(n)[2:].zfill(128)))
pol = sum(j*x**i for i, j in enumerate(nn))
return F128(pol)
def pol_to_byte(element):
coeff = element.polynomial().coefficients(sparse=False)
coeff = coeff + [0] * (128 - len(coeff))
num = int(''.join(list(map(str, coeff))), 2)
return int.to_bytes(num, 16, 'big')
F = GF(2)['x']
x = F.gen()
F128 = GF(2**128, 'x', modulus=x**128 + x**7 + x**2 + x + 1)
PR = PolynomialRing(F128, 'z')
z = PR.gen()
pt0 = "01" * 16
pt1 = "02" * 16
rq = requests.get("https://aes.cryptohack.org/forbidden_fruit/encrypt/" + pt0)
res0 = json.loads(rq.text)
rq = requests.get("https://aes.cryptohack.org/forbidden_fruit/encrypt/" + pt1)
res1 = json.loads(rq.text)
tag0 = byte_to_pol(bytes.fromhex(res0["tag"]))
tag1 = byte_to_pol(bytes.fromhex(res1["tag"]))
ct0 = byte_to_pol(bytes.fromhex(res0["ciphertext"]))
ct1 = byte_to_pol(bytes.fromhex(res1["ciphertext"]))
H2 = (tag0 + tag1) * (ct0 + ct1)**(-1)
pt2 = b"give me the flag"
P = byte_to_pol(bytes.fromhex(pt0)) + byte_to_pol(pt2)
C = P + ct0
T = tag0 + P * H2
nonce = res0["nonce"]
ciphertext = pol_to_byte(C).hex()
tag = pol_to_byte(T).hex()
associated_data = res0["associated_data"]
rq = requests.get(f"https://aes.cryptohack.org/forbidden_fruit/decrypt/{nonce}/{ciphertext}/{tag}/{associated_data}")
print(bytes.fromhex(json.loads(rq.text)["plaintext"]))
Noise free¶
from utils import listener
from sage.all import *
FLAG = b"crypto{????????????????????????}"
# dimension
n = 64
# plaintext modulus
p = 257
# ciphertext modulus
q = 0x10001
V = VectorSpace(GF(q), n)
S = V.random_element()
def encrypt(m):
A = V.random_element()
b = A * S + m
return A, b
class Challenge:
def __init__(self):
self.before_input = "Would you like to encrypt your own message, or see an encryption of a character in the flag?\n"
def challenge(self, your_input):
if 'option' not in your_input:
return {'error': 'You must specify an option'}
if your_input['option'] == 'get_flag':
if "index" not in your_input:
return {"error": "You must provide an index"}
self.exit = True
index = int(your_input["index"])
if index < 0 or index >= len(FLAG) :
return {"error": f"index must be between 0 and {len(FLAG) - 1}"}
self.exit = True
A, b = encrypt(FLAG[index])
return {"A": str(list(A)), "b": str(int(b))}
elif your_input['option'] == 'encrypt':
if "message" not in your_input:
return {"error": "You must provide a message"}
self.exit = True
message = int(your_input["message"])
if message < 0 or message >= p:
return {"error": f"message must be between 0 and {p - 1}"}
self.exit = True
A, b = encrypt(message)
return {"A": str(list(A)), "b": str(int(b))}
return {'error': 'Unknown action'}
import builtins; builtins.Challenge = Challenge # hack to enable challenge to be run locally, see https://cryptohack.org/faq/#listener
listener.start_server(port=13411)
from pwn import remote, process, context
import numpy as np
import json
from sage.all import *
# context.log_level = "Debug"
pr = remote("socket.cryptohack.org", 13411)
# pr = process(["python", "13411.py"])
# dimension
n = 64
# plaintext modulus
p = 257
# ciphertext modulus
q = 0x10001
pr.recvline()
A = []
B = []
for m in range(p):
d = {"option": "encrypt", "message": m}
pr.sendline(json.dumps(d).encode())
dj = json.loads(pr.recvline().strip().decode())
a = eval(dj["A"])
b = eval(dj["b"])
if len(A) == 0:
A = np.array(a)
B.append(b - m)
else:
At = np.vstack([A, a])
if np.linalg.matrix_rank(At) == np.linalg.matrix_rank(A) + 1:
A = np.vstack([A, a])
B.append(b - m)
if np.linalg.matrix_rank(A) == n:
break
A = matrix(GF(q), A)
B = vector(GF(q), B)
S = A.inverse() * B
print(B)
print(S)
flag = []
for index in range(p):
d = {"option": "get_flag", "index": index}
pr.sendline(json.dumps(d).encode())
dj = json.loads(pr.recvline().strip().decode())
a = eval(dj["A"])
b = eval(dj["b"])
flag.append((b - vector(GF(q), a) * S) % q)
print(bytes(flag))
pr.close()
Too Many Errors¶
from Crypto.Random.random import getrandbits
import random
from utils import listener
SEED = getrandbits(32)
FLAG = b'crypto{????????????????????}'
q = 127
class Challenge():
def __init__(self):
self.before_input = f"Welcome to the LWE sample generator! Retrieve a sample using the 'get_sample' option, or reset the distribution using the 'reset' option.\n"
self.rand = random.Random(SEED)
def challenge(self, your_input):
if not "option" in your_input:
return {"error": "You must send an option to this server"}
elif your_input["option"] == "reset":
self.rand.seed(SEED)
return {"success": "The distribution has been reset"}
elif your_input["option"] == "get_sample":
a = []
for i in range(len(FLAG)):
a.append(self.rand.randint(0, q - 1))
e = self.rand.randint(-1, 1)
self.rand.seed(getrandbits(32))
if self.rand.randint(0, 1):
a[self.rand.randint(0, len(a) - 1)] = self.rand.randint(0, q - 1)
b = 0
for (i, j) in zip(a, FLAG):
b += i * j
b += e
b %= q
return {"a": a, "b": b}
else:
return {"error": "Invalid option"}
import builtins; builtins.Challenge = Challenge # hack to enable challenge to be run locally, see https://cryptohack.org/faq/#listener
"""
When you connect, the 'challenge' function will be called on your JSON
input.
"""
listener.start_server(port=13390)
from pwn import remote, context
import json
# context.log_level = 'Debug'
q = 127
flag = [-1] * 28
flag = b'crypto{f4ult_4ttack5_0n_lw3}'
flag = list(flag)
while True:
ff = [0 if f == -1 else f for f in flag]
print(bytes(ff))
if all(i > 0 for i in flag): break
pr = remote("socket.cryptohack.org", "13390")
pr.recvline()
# Phase 1
js = { "option": "get_sample" }
pr.sendline(json.dumps(js).encode())
ds = json.loads(pr.recvline().strip().decode())
a1 = ds["a"]
b1 = ds["b"]
# Reset
js = { "option": "reset" }
pr.sendline(json.dumps(js).encode())
pr.recvline()
# Phase 2
js = { "option": "get_sample" }
pr.sendline(json.dumps(js).encode())
ds = json.loads(pr.recvline().strip().decode())
a2 = ds["a"]
b2 = ds["b"]
while all(i == j for i, j in zip(a1, a2)):
# Reset
js = { "option": "reset" }
pr.sendline(json.dumps(js).encode())
pr.recvline()
# Phase 2
js = { "option": "get_sample" }
pr.sendline(json.dumps(js).encode())
ds = json.loads(pr.recvline().strip().decode())
a2 = ds["a"]
b2 = ds["b"]
for i in range(len(a1)):
if a1[i] != a2[i] and flag[i] == 0:
a = (a1[i] - a2[i]) % q
b = (b1 - b2) % q
f = (b * pow(a, -1, q)) % q
flag[i] = f
break
pr.close()
print(bytes(flag))
Pad Thai¶
# challenge.py
from Crypto.Util.Padding import unpad
from Crypto.Cipher import AES
from os import urandom
FLAG = "Hello, World"
class Challenge:
def __init__(self):
self.before_input = "Let's practice padding oracle attacks! Recover my message and I'll send you a flag.\n"
self.message = urandom(16).hex()
self.key = urandom(16)
def get_ct(self):
iv = urandom(16)
cipher = AES.new(self.key, AES.MODE_CBC, iv=iv)
ct = cipher.encrypt(self.message.encode("ascii"))
return {"ct": (iv+ct).hex()}
def check_padding(self, ct):
ct = bytes.fromhex(ct)
iv, ct = ct[:16], ct[16:]
cipher = AES.new(self.key, AES.MODE_CBC, iv=iv)
pt = cipher.decrypt(ct) # does not remove padding
try:
unpad(pt, 16)
except ValueError:
good = False
else:
good = True
return {"result": good}
def check_message(self, message):
if message != self.message:
self.exit = True
return {"error": "incorrect message"}
return {"flag": FLAG}
#
# This challenge function is called on your input, which must be JSON
# encoded
#
def challenge(self, msg):
if "option" not in msg or msg["option"] not in ("encrypt", "unpad", "check"):
return {"error": "Option must be one of: encrypt, unpad, check"}
if msg["option"] == "encrypt": return self.get_ct()
elif msg["option"] == "unpad": return self.check_padding(msg["ct"])
elif msg["option"] == "check": return self.check_message(msg["message"])
# 13421.py
#!/usr/bin/env python3
from Crypto.Util.Padding import unpad
from Crypto.Cipher import AES
from os import urandom
from utils import listener
FLAG = 'crypto{?????????????????????????????????????????????????????}'
class Challenge:
def __init__(self):
self.before_input = "Let's practice padding oracle attacks! Recover my message and I'll send you a flag.\n"
self.message = urandom(16).hex()
self.key = urandom(16)
def get_ct(self):
iv = urandom(16)
cipher = AES.new(self.key, AES.MODE_CBC, iv=iv)
ct = cipher.encrypt(self.message.encode("ascii"))
return {"ct": (iv+ct).hex()}
def check_padding(self, ct):
ct = bytes.fromhex(ct)
iv, ct = ct[:16], ct[16:]
cipher = AES.new(self.key, AES.MODE_CBC, iv=iv)
pt = cipher.decrypt(ct) # does not remove padding
try:
unpad(pt, 16)
except ValueError:
good = False
else:
good = True
return {"result": good}
def check_message(self, message):
if message != self.message:
self.exit = True
return {"error": "incorrect message"}
return {"flag": FLAG}
#
# This challenge function is called on your input, which must be JSON
# encoded
#
def challenge(self, msg):
if "option" not in msg or msg["option"] not in ("encrypt", "unpad", "check"):
return {"error": "Option must be one of: encrypt, unpad, check"}
if msg["option"] == "encrypt": return self.get_ct()
elif msg["option"] == "unpad": return self.check_padding(msg["ct"])
elif msg["option"] == "check": return self.check_message(msg["message"])
import builtins; builtins.Challenge = Challenge # hack to enable challenge to be run locally, see https://cryptohack.org/faq/#listener
listener.start_server(port=13421)
# solve.py
import random
from pwn import remote, context
import json
from challenge import Challenge
is_local = False
def xor(a, b):
return bytes(x^y for x, y in zip(a, b))
if is_local: # Local
challenge = Challenge()
first_message = []
second_message = []
S = list(b'0123456789abcdef')
opt = { "option": "encrypt" }
ct = challenge.challenge(opt)["ct"]
ct = bytes.fromhex(ct)
# First message
print("Find first message")
iv, ct = ct[:16], ct[16:]
c1 = ct[:16]
for x in range(1, 17):
queries = 0
payload = [x] * x
payload = [0] * (16 - x) + payload
payload = xor(iv, payload)
for i in S:
iv_ = [0] * (15 - len(first_message)) + [i] + first_message
iv_ = xor(iv_, payload)
opt2 = { "option": "unpad", "ct": (iv_ + c1).hex() }
result = challenge.challenge(opt2)["result"]
if result == True:
print(f"Found char {chr(i)} at index {x}")
first_message = [i] + first_message
break
print(bytes(first_message))
# assert challenge.message[:16].encode() == bytes(first_message)
assert len(first_message) == 16
# Second part
print("\nFind second part")
iv = c1
c1 = ct[16:]
for x in range(1, 17):
queries = 0
payload = [x] * x
payload = [0] * (16 - x) + payload
payload = xor(iv, payload)
ff = False
for i in S:
iv_ = [0] * (15 - len(second_message)) + [i] + second_message
iv_ = xor(iv_, payload)
opt2 = { "option": "unpad", "ct": (iv_ + c1).hex() }
result = challenge.challenge(opt2)["result"]
if result == True:
second_message = [i] + second_message
break
print(bytes(second_message))
assert len(second_message) == 16
assert challenge.message[16:].encode() == bytes(second_message)
flag = challenge.challenge({ "option": "check", "message": (bytes(first_message + second_message)).decode()})["flag"]
print(flag)
else: # Remote
context.log_level = 'Debug'
pr = remote("socket.cryptohack.org", "13421")
pr.recvline()
first_message = []
second_message = []
S = list(b'0123456789abcdef')
opt = { "option": "encrypt" }
pr.sendline(json.dumps(opt).encode())
ct = json.loads(pr.recvline().strip())["ct"]
ct = bytes.fromhex(ct)
# First message
print("Find first message")
iv, ct = ct[:16], ct[16:]
c1 = ct[:16]
for x in range(1, 17):
queries = 0
payload = [x] * x
payload = [0] * (16 - x) + payload
payload = xor(iv, payload)
for i in S:
iv_ = [0] * (15 - len(first_message)) + [i] + first_message
iv_ = xor(iv_, payload)
opt2 = { "option": "unpad", "ct": (iv_ + c1).hex() }
pr.sendline(json.dumps(opt2).encode())
result = json.loads(pr.recvline().strip())["result"]
if result == True:
print(f"Found char {chr(i)} at index {x}")
first_message = [i] + first_message
break
print(bytes(first_message))
# assert challenge.message[:16].encode() == bytes(first_message)
assert len(first_message) == 16
# Second part
print("\nFind second part")
iv = c1
c1 = ct[16:]
for x in range(1, 17):
queries = 0
payload = [x] * x
payload = [0] * (16 - x) + payload
payload = xor(iv, payload)
ff = False
for i in S:
iv_ = [0] * (15 - len(second_message)) + [i] + second_message
iv_ = xor(iv_, payload)
opt2 = { "option": "unpad", "ct": (iv_ + c1).hex() }
pr.sendline(json.dumps(opt2).encode())
result = json.loads(pr.recvline().strip())["result"]
if result == True:
second_message = [i] + second_message
break
print(bytes(second_message))
assert len(second_message) == 16
# assert challenge.message[16:].encode() == bytes(second_message)
print(bytes(first_message + second_message))
opt3 = { "option": "check", "message": bytes(first_message + second_message).decode("ascii") }
pr.sendline(json.dumps(opt3).encode())
print(pr.recvline())
pr.close()
# b'{"flag": "crypto{if_you_ask_enough_times_you_usually_get_what_you_want}"}\n'
Paper Plane¶
# solve.py
import requests
def xor(a, b):
return bytes(x^y for x, y in zip(a, b))
c0 = bytes.fromhex("1e1cb70d4aaf8f74d845e8f85f636a22")
ciphertext = bytes.fromhex("dd2d25c1e61648b6a1cbab88466e832e1758163b68165cf8c5efd21af5682bd5")
m0 = bytes.fromhex("c8dd40be54e559068842351c72207dfd")
flag1 = b'crypto{h3ll0_t3l'
flag2 = b'3gr4m}\n\n\n\n\n\n\n\n\n\n'
print(flag1 + flag2)
exit(0)
flag = b'\n' * 10
idx = len(flag)
flag = list(b'\x00'*(16-len(flag)) + flag)
c0 = ciphertext[:16]
ciphertext = ciphertext[16:]
m0 = flag1
for x in range(idx, 16):
payload = bytes([x+1]) * x
payload = b'\x00' * (16 - len(payload)) + payload
padding = xor(c0, xor(flag, payload))[16-x:]
for i in range(256):
c0_ = c0[:(15-x)] + bytes([i]) + padding
assert len(c0_) == 16
rq = requests.get(f"https://aes.cryptohack.org/paper_plane/send_msg/{ciphertext.hex()}/{m0.hex()}/{c0_.hex()}")
if 'received' in rq.text:
f = c0[-(x+1)] ^ i ^ (x+1)
if f > 127:
print(f"Something wrong: {f}")
exit(0)
print(f"Found character {i} at index {x}")
flag[15-x] = f
break
print(i)
if i == 256:
print("Wrong padding")
exit(0)
print(bytes(flag))