solution

$m$ がすべて Your OTP for transaction #731337 in ABCXYZ Bank is ?????????. の形をしていることをgeussingすればあとはCoppersmithの定理やるだけ。気付けない場合は平文が似通ってることを信じてCoppersmith’s Short Pad Attack。

note

sageの起動が遅い問題はserverとして建てれば解決することに気付いた

implementation

main.py

#!/usr/bin/env python3
import argparse
import ast
import sys
import telnetlib
import Crypto.PublicKey.RSA
import Crypto.Util.number

parser = argparse.ArgumentParser()
parser.add_argument('host', nargs='?', default='206.189.92.209')
parser.add_argument('port', nargs='?', default=31333, type=int)
parser.add_argument('--sage-host', default='localhost')
parser.add_argument('--sage-port', default=8000, type=int)
args = parser.parse_args()

def solve(sage_tn):
    def coppersmiths_short_pad_attack(n, e, c1, c2):
        sage_tn.write((' '.join(map(str, [ n, e, c1, c2 ])) + '\n').encode())
        result = ast.literal_eval(sage_tn.read_until(b'\n').decode())
        if result:
            m1, m2 = result
            return m1

    with telnetlib.Telnet(args.host, args.port) as tn:

        def test_the_otp():
            tn.read_until(b'Choose one:')
            tn.write(b'1\n')
            tn.read_until(b'otp should be:')
            otp = int(tn.read_until(b'\n').strip())
            tn.read_until(b'encrypted dat:')
            encrypted_dat = tn.read_until(b'\n').strip()
            tn.read_until(b'decrypted dat:')
            decrypted_dat = tn.read_until(b'\n').strip()
            return otp, encrypted_dat, decrypted_dat

        def get_the_public_key():
            tn.read_until(b'Choose one:')
            tn.write(b'2\n')
            BEGIN = b'-----BEGIN PUBLIC KEY-----'
            END = b'-----END PUBLIC KEY-----'
            tn.read_until(BEGIN)
            key = BEGIN + tn.read_until(END)
            return Crypto.PublicKey.RSA.importKey(key)

        def get_flag(callback):
            tn.read_until(b'Choose one:')
            tn.write(b'3\n')
            tn.read_until(b'encrypted dat:')
            encrypted_dat = tn.read_until(b'\n').strip()
            tn.read_until(b'send me otp to get flag >>> ')
            otp = callback(encrypted_dat)
            tn.write(str(otp).encode() + b'\n')
            return tn.read_until(b'\n').decode().strip()

        pubkey = get_the_public_key()
        e = pubkey.e
        n = pubkey.n
        print('[*] e =', e)
        print('[*] n =', hex(n))
        assert e == 3

        for _ in range(0):
            otp, encrypted_dat, decrypted_dat = test_the_otp()
            c = int(encrypted_dat, 16)
            m = Crypto.Util.number.bytes_to_long(decrypted_dat)
            print('[*] otp =', hex(otp))
            print('[*] c =', hex(c))
            print('[*] m =', hex(m))
            assert pow(m, e, n) == c

        def solve_otp(encrypted_dat, cs=[]):
            c1 = int(encrypted_dat, 16)
            print('[*] c =', hex(c1))
            for c2 in cs:
                m1 = coppersmiths_short_pad_attack(n, e, c1, c2)
                if m1:
                    msg = Crypto.Util.number.long_to_bytes(m1).decode()
                    otp = int(msg.split()[-1].rstrip('.'))
                    print('[*] otp =', otp)
                    return otp
            else:
                cs += [ c1 ]
                return 0

        while True:
            try:
                result = get_flag(solve_otp)
            except EOFError:
                break
            print('[+]', result)
            if not result.startswith('Sorry, '):
                return result

with telnetlib.Telnet(args.sage_host, args.sage_port) as tn:
    while True:
        flag = solve(sage_tn=tn)
        if flag:
            print('[+]', flag)
            break

coppersmiths_short_pad_attack.sage

http://inaz2.hatenablog.com/entry/2016/01/20/022936 から借りてきたものを修正

#!/usr/bin/env sage
# http://inaz2.hatenablog.com/entry/2016/01/20/022936

import sys

def short_pad_attack(c1, c2, e, n):
    PRxy.<x,y> = PolynomialRing(Zmod(n))
    PRx.<xn> = PolynomialRing(Zmod(n))
    PRZZ.<xz,yz> = PolynomialRing(Zmod(n))

    g1 = x^e - c1
    g2 = (x+y)^e - c2

    q1 = g1.change_ring(PRZZ)
    q2 = g2.change_ring(PRZZ)

    h = q2.resultant(q1)
    h = h.univariate_polynomial()
    h = h.change_ring(PRx).subs(y=xn)
    h = h.monic()

    kbits = n.nbits()//(2*e*e)
    diff = h.small_roots(X=2^kbits, beta=0.5)[0]  # find root < 2^kbits with factor >= n^0.5

    return diff

def related_message_attack(c1, c2, diff, e, n):
    PRx.<x> = PolynomialRing(Zmod(n))
    g1 = x^e - c1
    g2 = (x+diff)^e - c2

    def gcd(g1, g2):
        while g2:
            g1, g2 = g2, g1 % g2
        return g1.monic()

    return -gcd(g1, g2)[0]


def main():
    n, e, c1, c2 = map(ZZ, raw_input().split())
    nbits = n.nbits()
    kbits = nbits//(2*e*e)

    try:
        diff = short_pad_attack(c1, c2, e, n)
        m1 = related_message_attack(c1, c2, diff, e, n)
        m2 = m1 + diff

        assert pow(m1, e, n) == c1
        assert pow(m2, e, n) == c2
        print [ m1, m2 ]
    except:
        print []
    sys.stdout.flush()

if __name__ == '__main__':
    while True:
        main()