In this challenge, we are given a file called lowdown.sage. The challenge implements a custom signature scheme. If we are able to forge a signature for a given message, we get the flag. The challenge first generates the key and the random message to sign:

def makey(k):
    while True:
        g = random_matrix(F, k)
        if g.is_invertible():
            ng = 1 << 192 # g.order()
            break
    r, a = [randint(2, ng - 2) for _ in '01']
    gg = g ** r
    pkey, skey = (g, gg ** a), r
    return(pkey, skey)

global F, k
F = GF(256)
k = 10
pkey, skey = makey(k)

msg = ''.join([string.printable[randint(0, 85)] for _ in range(40)]).encode()

It picks a random matrix $g \in GL(10, F)$ (invertible 10$\times$10 matrix with elements on the field $F$) and generates two random numbers $r$ and $a$. The private key is the random number $r$ and the public key is composed by the matrix $g$ and the matrix $g^{ra}$.

Note that elements of the field $F$ are represented as polynomials of degree 7 with binary coefficients:

sage: [F.random_element() for _ in range(5)]
[z8^6 + z8^5 + z8^3 + 1,
 z8^6 + z8 + 1,
 z8^7 + z8^6 + z8^4 + z8^3,
 z8^7 + z8^5 + z8^4 + z8^3 + z8^2 + z8 + 1,
 z8^5 + z8^4 + z8^2]

Then, in the main loop, we have:

while True:
		pr("| Options: \n|\t[G]et the flag \n|\t[P]ublic Key \n|\t[S]ign a message \n|\t[V]erify signature \n|\t[Q]uit")
		ans = sc().decode().lower().strip()

We can sign short messages:

elif ans == 's':
    pr(border, 'Send your message to sign: ')
    _msg = sc().strip()
    if len(_msg) >= 10:
        die(border, 'Sorry, I sign only short messages! :/')
    _s, _t = sign(pkey, skey, _msg)
    pr(border, f's = {M2i(_s)}')
    pr(border, f't = {M2i(_t)}')

We can verify the signature for any message:

elif ans == 'v':
    pr(border, 'Send your signature to verify: ')
    _sgn = sc().split(b',')
    try:
        _s, _t = [int(_) for _ in _sgn]
        _sgn = (Hinv(_s, k), Hinv(_t, k))
        pr(border, 'Send your message: ')
        _msg = sc().strip()
        if verify(_sgn, pkey, _msg):
            pr(border, 'Your message successfully verified :)')
        else:
            pr(border, 'Verification failed :(')
    except:
        pr(border, 'Try to send valid signature!')
        continue

We can get the public key:

elif ans == 'p':
    _g, _ga = pkey
    pr(border, f'g  = {M2i(_g)}')
    pr(border, f'ga = {M2i(_ga)}')

And we can get the flag, supplying a valid signature for the random message:

if ans == 'g':
    pr(border, 'You should send the valid signature for my given message!')
    pr(border, f'Message = {msg}')
    pr(border, 'Send the signature of the above message: ')
    _sgn = sc().split(b',')
    try:
        _s, _t = [int(_) for _ in _sgn]
        sgn = (Hinv(_s, k), Hinv(_t, k))
        if verify(sgn, pkey, msg) and str(_s).startswith('13') and str(_t).startswith('37'):
            pr(border, f'Congratulation! You got the flag!')
            die(border, f'flag = {flag}')
        else:
          pr(border, 'Your signature is not correct!')
    except:
        die(border, 'Exiting...')

Besides the signature being correct, the s value needs to start with 13 and the t value needs to start with 37.

The challenge has several functions to transform a matrix into a integer and the other way around. These are just helper functions:

def h(a):
    if a == 0:
        return 0
    else:
        g = F.gen()
        for _ in range(1, 256):
            if g ** _ == a:
                return _

def H(M):
    assert M.nrows() == M.ncols()
    k, _H = M.nrows(), []
    for i in range(k):
        for j in range(k):
            _h = h(M[i, j])
            _H.append(bin(_h)[2:].zfill(8))
    return ''.join(_H)

def Hinv(m, k):
    B = bin(m)[2:].zfill(8 * k**2)
    g = F.gen()
    _H = [int(B[8*i:8*i + 8], 2) for i in range(k**2)]
    _M = [0 if _h == 0 else g ** _h for _h in _H]
    M = Matrix(F, [[a for a in _M[k*i:k*i + k]] for i in range(k)])
    return M

def M2i(M):
    _H = H(M)
    return int(_H, 2)

Now, for the signature scheme (the fun part), these are the sign and verify functions:

def random_oracle(msg):
    _h = sha1(msg).digest()
    return bytes_to_long(_h)

def sign(pkey, skey, msg):
	  g, ga = pkey
    ng = 1 << 192 # g.order()
    _h = random_oracle(msg)
    assert _h <= ng
    _g = g ** skey
    n = randint(2, ng - 2)
    s, t = ga * (_g.inverse()) ** (n * _h), _g ** n
    return (s, t)

def verify(sgn, pkey, msg):
    _, ga = pkey
    s, t = sgn
    _h = random_oracle(msg)
    return s * t ** _h == ga

The signature generation first computes _h, which is the sha1 hash of the message (not random at all despite the name of the function). It generates a random number n and computes the signature as the pair $(s, t)$ given by:

$$s = g^{ra} \left(g^{-r}\right)^{n H(m)} = g^{ra - rnH(m)}$$

$$t = \left(g^{r}\right)^n = g^{rn}$$

And the verify function checks that

$$s t^{H(m)} = g^{ra}$$

Verifying the correctness of the scheme is pretty straightforward: suppose that $(s, t)$ is a valid signature for a message $m$, generated with that algorithm. Then,

$$s t^{H(m)} = g^{ra - rnH(m)} \cdot \left(g^{rn}\right)^{H(m)} = g^{ra - rnH(m) + rnH(m)} = g^{ra}$$

However, note that the signature is completely broken: given a public key $g^{ra}$ and a message $m$, all of the parameters in the verification equation are either known or controlled by an attacker: $s, t, H(m), g^{ra}$. If an attacker computes a random matrix $t$, then he can choose $s$ to be:

$$s = g^{ra} t^{-H(m)}$$

and the signature would be a valid one. The strategy in this challenge is to generate random matrices $t$ and compute the corresponding matrix $s$ until they satisfy the 13 and 37 restrictions:

while True:
    max_tries = 100
    try:
        r.sendlineafter(b"[Q]uit\n", b"P", timeout=5)

        r.recvuntil(b"g  = ", timeout=5)
        g = Hinv(int(r.recvline(timeout=5).strip()))

        r.recvuntil(b"ga = ", timeout=5)
        ga = Hinv(int(r.recvline(timeout=5).strip()))

        r.sendlineafter(b"[Q]uit\n", b"G", timeout=5)

        r.recvuntil(b"Message = b", timeout=5)
        msg = r.recvline(timeout=5).strip()[1:-1]
        print(f"Message to sign: {msg}")
        r.recvline(timeout=5)

    except (EOFError, TimeoutError, PwnlibException):
        r.close()
        continue
    _h = random_oracle(msg)
    
    s = Matrix(F, k)
    while not str(M2i(s)).startswith("13") and max_tries > 0:
        t = random_matrix(F, k)
        while not str(M2i(t)).startswith("37"):
            t = random_matrix(F, k)

        s = ga*(t**(-_h))
        max_tries -= 1
	
    r.sendline(f"{M2i(s)},{M2i(t)}".encode())
    r.interactive()
    r.close()

When executing that:

$ python get_flag.py
[+] Starting local process './lowdown.sage': pid 1076973
Message to sign: b"1+i:twQZmW3>=vvy5:WC@B.XSKc8a')e)FCZMSyl"
[*] Switching to interactive mode
 Congratulation! You got the flag!
 flag = flag{flag_for_testing}
 Exiting...
[*] Got EOF while reading in interactive