Swiss Hacking Challenge 2023 - hsb

Posted on Apr 23, 2023

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.