P.W.N. CTF 2018

City RSA - Points:557

Challenge

I know that frank that a**hole ate the last cookie, but no one believes me.

He wrote himself a service to sign messages, I found part of the source.

It runs here:

nc rsa.uni.hctf.fun 4422

Can you get me the message "YES, I did eat the last cookie" signed by his service?

I added a verifier for you using his public key:

nc checker.uni.hctf.fun 31337

Analysis

The sender message is signed by RSA in main.c

    printf("Enter message:");
    .....
    /* Sign via RSA */
    city_rsa_init(&cfg, p_str, q_str, p_inv_str, d_str, e_str, d_p_str, d_q_str);
    //city_print_config(&cfg);
    city_rsa_sign(&cfg, result, input_hex);
    printf("The signature of your message is: 0x%s\n", result);

The receiver verify message by main.py

#!/usr/bin/env python3

pubkey = {
        'e': 0x10001,
        'N': 0x98ac865ef6a31313e50fb37853ce96804cb2d864e2a4d14bf7cca85a444a40b453de7c3ae8416e8976cd1cac7f548a43fe8c2eb3d4cfcd3808cf9458c0c87bf4c037d515d22d1299b72e79fcd4a1d1531789cb3013031fb0e28fdfe73f090027b3b3428cacef6dbf7823d5da8d3158101e0c07e707224d451fcbb3114ab85a925bcb7faf9b317bbbddba81285ab93f0ee5f968b258f4675e9d893ec7f0e8379b67527d78fe920ab201cb3a6459d4f3902754b36e3264db7727c6d32e014593c39991f54c7b034d69b986616a39454c85d9e032afa853a6e12fea06472ed3573707da3df9ca7ce8d2c3b820e745da6e3cc523789f858d98645ea042bb54b463d3
}


def main():
        ct = int(input("Input signed message:"), 16)
        msg = pow(ct, pubkey["e"], pubkey["N"])
        msg = int.to_bytes(msg, 128, "big")

        if b"YES, I did eat the last cookie" in msg:
                flag = open("/opt/flag.txt").read()
                print(flag)
        else:
            print("Nope, sorry.")


if __name__ == '__main__':
        main()

If signed message contains 'YES, I did eat the last cookie', we can have flag. However, the service block us from getting the message signed directly by checking if 'YES' is inside the message.

RSA Blind Attack?

Apparently, we can use RSA blind attack to forge the signed message. However, the input length and characters are restricted by c api. Only 30 characters can be input and the maximum value of character have to be under 0x7f.

The blind attack attack can bypass the check by

cmd ( mod N )

cmdrd ( mod N )

ccr1 ( mod N )

But, we cannot send

mdrd to service and get signed.

Exploit

Since we cannot use RSA blind attack directly, we can try to factor the forged message and use modular multiplicative inverse to find all our ingredients.

find possible prime factors

use sage to find prime factors.

msg = 'YES, I did eat the last cookie'
import binascii
for a in string.ascii_letters:
    tmp = msg+a
    v = int(binascii.hexlify(tmp),16)
    factor(v)

after few minues, we have a good candidate.

2 * 3 * 5 * 313 * 379 * 33311 * 15239591 * 2184081073 * 454801932889 * 4706963699932381 * 18672853823680873391

then, we try to send those factors into sign service with different multiplier to control our characters are under 0x7f.

(1, 2)
(1, 3)
(1, 5)
(1, 313)
(3, 379)
(2, 33311)
(8, 15239591)
(12, 2184081073)
(73, 454801932889)
(21, 4706963699932381)
(31, 18672853823680873391L)

Here is the final exploit.

from pwn import *
import binascii
from Crypto.Util.number import *
import gmpy

msg = 'YES, I did eat the last cookie'
N = 0x98ac865ef6a31313e50fb37853ce96804cb2d864e2a4d14bf7cca85a444a40b453de7c3ae8416e8976cd1cac7f548a43fe8c2eb3d4cfcd3808cf9458c0c87bf4c037d515d22d1299b72e79fcd4a1d1531789cb3013031fb0e28fdfe73f090027b3b3428cacef6dbf7823d5da8d3158101e0c07e707224d451fcbb3114ab85a925bcb7faf9b317bbbddba81285ab93f0ee5f968b258f4675e9d893ec7f0e8379b67527d78fe920ab201cb3a6459d4f3902754b36e3264db7727c6d32e014593c39991f54c7b034d69b986616a39454c85d9e032afa853a6e12fea06472ed3573707da3df9ca7ce8d2c3b820e745da6e3cc523789f858d98645ea042bb54b463d3
def getsign(m):
    context.log_level = 'WARN'
    p = remote('rsa.uni.hctf.fun', 4422)
    dmsg = long_to_bytes(m)
    p.sendline(dmsg)
    p.recvuntil('The signature of your message is: ')
    m = int(p.recvline(), 16)
    p.close()
    return m

x100 = gmpy.invert(getsign(0x100), N)

d = [2, 3, 5, 313, 379, 33311, 15239591, 2184081073, 454801932889, 4706963699932381, 18672853823680873391]
d = [2, 3, 5, 313, 379, 33311, 15239591, 2184081073, 454801932889, 4706963699932381, 18672853823680873391]
mods = {}
flag = 1
for v in d:
    for i in range(1, 0xff):
        m = long_to_bytes(v*i)
        found = True
        for n in m:
            if ord(n) > 0x79:
                found = False
                break
        if found:
            print(i, v)
            if i == 1:
                mods[v] = (getsign(v<<8) * int(x100))%N
                flag *= mods[v]
            else:
                tmp = (getsign(v*i<<8) * int(x100))%N
                if i in mods:
                    flag *= (tmp*gmpy.invert(mods[i], N)) % N
                else:
                    iinvert = (getsign(i<<8) * int(x100))%N
                    flag *= (tmp*gmpy.invert(iinvert, N)) % N
            break
print(hex(flag%N))
print(pow(flag%N, 0x10001, N))

Send the signed message to receiver to get the flag.

% nc checker.uni.hctf.fun 31337
Input signed message:0x85a8277792bcd656724e92dad50ee5ed31de54ecf746849a0605be3afd27c75492196d5cbc35957f85d0f962aa520aab50b83025797f1f7c3f5ae84b1e66976a520d5d98c4b88eb19078ec78c7d07ad5149e382ffba52d4e9d2ad40718545c3a1296b3026324d0e6a12141363ef272e42139438cd180a94b350818cd39c24f2c6d0241d80e01febf305f0866111816ff1be8823a66eed866545d1dab69ba0748d17b5344a75ea3d340bc6b2e0ca6db3c3db2bc7e22a0ce3b8ea06851ce2be7a16ff3931d5e8cb8b01b757aa6c975e98a7fb36acd1eb8b96210affeaa638b93fa9fc68960544a3b7df88d1a496b107125465e479e99d87b47a825aed9b54095d4
flag{https://www.youtube.com/watch?v=MpMBETNC-44#C0ngr4tz}

It is an excellent challenge, good job.