Swiss Hacking Challenge 2023 - hsb
Information
Challenge category: crypto
Challenge Description
Do you usually screw up your crypto implementation? You’re not alone, most companies do! But fear not, our newest HSM is the end of all your worries of being hacked. You can use it as a black box for your crypto operations and never need to worry about storing your keys again!
Files
We are given a hsb.zip
file
Source code
#!/usr/bin/env python3
from Crypto.PublicKey import RSA
from inspect import signature
from secrets import choice
from secret import FLAG
RSA_LEN = 256
TYPE_USER = b"\x01"
TYPE_INTERNAL = b"\x02"
def b2i(b: bytes) -> int:
return int.from_bytes(b, "big")
def i2b(i: int) -> bytes:
return i.to_bytes((i.bit_length() + 7) // 8, "big")
def get_random_bytes(l: int):
alph = list(range(1, 256))
return b"".join([bytes([choice(alph)]) for _ in range(l)])
def pad(p: bytes) -> bytes:
return get_random_bytes(RSA_LEN - len(p) - 2) + b"\x00" + p
def unpad(p: bytes) -> bytes:
pad_end = 1
while pad_end < len(p) and p[pad_end] != 0:
pad_end += 1
return p[pad_end + 1:]
class HSM:
def __init__(self):
self.vendor = "Cybersecurity Competence Center"
self.model = "Perfection v2.1"
self.rsa = None
self.running = False
def info(self):
print(f"Vendor: {self.vendor}\nModel: {self.model}")
def stop(self):
if not self.running:
print("HSM is already stopped.")
return
self.running = False
def gen_key(self):
bits = RSA_LEN * 8
self.rsa = RSA.generate(bits)
print(f"Generated new RSA-{bits} keys")
def sign(self, m: int):
m_pad = int.from_bytes(pad(i2b(m)), "big")
sig = pow(m_pad, self.rsa.d, self.rsa.n)
print(f"Signature: {sig}")
def verify(self, sig: int, m: int):
recovered = b2i(unpad(pow(sig, self.rsa.e, self.rsa.n).to_bytes(RSA_LEN, "big")))
if recovered == m:
print("Valid signature.")
else:
print("Invalid signature.")
def _enc(self, m: bytes):
c = pow(int.from_bytes(pad(m), "big"), self.rsa.e, self.rsa.n)
print(f"Ciphertext: {c}")
def enc(self, m: int):
self._enc(TYPE_USER + i2b(m))
def dec(self, c: int):
m = unpad(pow(c, self.rsa.d, self.rsa.n).to_bytes(RSA_LEN, "big"))
t, m = m[:1], b2i(m[1:])
if t == TYPE_USER:
print(f"Plaintext: {m}")
else:
print("Cannot decrypt internal secrets")
def export_secret(self):
self._enc(TYPE_INTERNAL + FLAG.encode())
def run(self):
self.running = True
options = [self.info, self.stop, self.gen_key, self.sign, self.verify, self.enc, self.dec, self.export_secret]
while self.running:
print("Available operations:")
for i, opt in enumerate(options):
print(f"\t[{i}] {opt.__name__}")
print()
try:
opt = int(input("Enter selected option: "))
print()
if opt > 2 and not self.rsa:
print("No RSA key available. Use gen_key() first.")
else:
fn = options[opt]
args = []
for i in range(len(signature(fn).parameters)):
try:
args.append(int(input(f"input {i}: ")))
except ValueError as e:
print("Invalid input format, must be integer")
raise e
fn(*args)
except (ValueError, IndexError):
print("Invalid option")
pass
print()
if __name__ == "__main__":
HSM().run()
Built-in security mechanisms
When using export_secret
, it gets prepended with TYPE_INTERNAL
. The dec
function checks for this and doesn’t decrypt the flag.
Exploitation
If we compare the dec
and the sign
function, we notice something:
def dec(self, c: int):
m = unpad(pow(c, self.rsa.d, self.rsa.n).to_bytes(RSA_LEN, "big"))
t, m = m[:1], b2i(m[1:])
if t == TYPE_USER:
print(f"Plaintext: {m}")
else:
print("Cannot decrypt internal secrets")
def sign(self, m: int):
m_pad = int.from_bytes(pad(i2b(m)), "big")
sig = pow(m_pad, self.rsa.d, self.rsa.n)
print(f"Signature: {sig}")
The sign
function does exactly the same mathematical operations on the input as the dec
function but doesn’t validate the first byte. (That’s how RSA signatures should work but in this case this leaks the internal secret).
This means we can connect to the SHC instance now and get the internal secret:
$ nc chall.m0unt41n 1337
Available operations:
[0] info
[1] stop
[2] gen_key
[3] sign
[4] verify
[5] enc
[6] dec
[7] export_secret
Enter selected option: 2
Generated new RSA-2048 keys
---
Enter selected option: 7
Ciphertext: 174725772169222287195938703911231582182695539384---
---
Enter selected option: 3
input 0: 174725772169222287195938703911231582182695539384---
Signature: 9473692820722258446903447676992090720825282959538
490362350263894338500686440950606540162344224395897213368029
279968508221332989258562095946594392178931649648643570655628
922861582230636982646331154487805623442300470844020672829309
523901576468216964671144360811622931648161097925125266476082
028949669995884614205296121751844135893079651582428072661904
184494676864525256527172369202644596749005954956413636771564
790063335840900196399766878579577184281925448478962212350974
990896397430295856890539804909143038611450889521153946054453
560318637176602847500659085628603111998100896762399217088374
2736741763252318023139197
Flag
We can convert the “signature” to bytes again:
def i2b(i: int) -> bytes:
return i.to_bytes((i.bit_length() + 7) // 8, "big")
def unpad(p: bytes) -> bytes:
pad_end = 1
while pad_end < len(p) and p[pad_end] != 0:
pad_end += 1
return p[pad_end + 1:]
print(unpad(i2b(94736928207222584469034476769920---)))
We get the flag: shc2023{sur3ly_n0_0n3_w0uld_b3_s0_stup1d_1n_r34l1ty_r1gh7?}
Conclusion
In hindsight, this challenge was very easy. However it took me very long to figure out that the sign
function basically can do RSA decryption, I tried very weird stuff before that.