plain RSAに対する攻撃手法を実装してみる
RSAは「単純な素因数分解アルゴリズムを実装してみる」「Msieveを使って大きな数を素因数分解してみる」「YAFUを使って大きな数を素因数分解してみる」で示したような方法により、公開鍵nを素因数分解することができれば秘密鍵dを得ることができる。 一方、平文をそのまま暗号化した場合のRSA(plain RSA)では、nの素因数分解以外にも、特定の条件のもとで暗号文cから平文mを復号できる場合がある。 ここでは、そのようなplain RSAに対する攻撃手法を実装してみる。
環境
Ubuntu 14.04.3 LTS 64bit版
$ uname -a Linux vm-ubuntu64 3.19.0-25-generic #26~14.04.1-Ubuntu SMP Fri Jul 24 21:16:20 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 14.04.3 LTS Release: 14.04 Codename: trusty
GMPYのインストール
Pythonは標準で多倍長整数を扱うことができるため、GNU Multi-Precision Library(GMP)のような多倍長整数ライブラリを用いなくても大きな整数を扱うことができる。 しかし、整数の範囲でのn乗根や拡張ユークリッド互除法、mod nにおける逆元演算を行うにあたっては、GMPが提供する実装を用いると簡単である。
GMPYまたはその後継のGMPY2を用いると、PythonからGMPを使うことができる。 GMPY2はGMPに加えMPFR(多倍長浮動小数点数)やMPC(多倍長複素数)も使うことができるが、GMPの他にこれらのライブラリのインストールも必要となるため、ここではGMPYを用いることとする。
インストールするにはpipを用いて次のようにすればよい。
$ sudo apt-get install python-dev libgmp3-dev $ sudo pip install gmpy
GMPYのドキュメントは下記から参照することができる。
e乗根
eが小さいとき、nのe乗根以下の平文mについては、単純に暗号文cのe乗根を取れば求めることができる。
# root_e.py import sys import gmpy def root_e(c, e, n): bound = gmpy.root(n, e)[0] m = gmpy.root(c, e)[0] return m, bound if __name__ == '__main__': n = 0x00d91f0102279d099a9aa3a819faefef8e39e71075c5ed59275ae33fd16f10c6b120fbc14f2b0e85b09b7372853c22b359fb4b850e0b66da55585e1221bc23d4a84bc0cce1c1f1c080c74520c3f7cb2d041bc2c372ae96a3b9344dc00b00a75873fd339121804b39b74969ceab850a5ce8c65860fa1e7cfafb052e994a832198ece195ee8bb427a04609b69f052b1d2818741604e2d1fc95008961365f0536f1d3d12b11f3b56f55aa478b18cc5e74918869d9ef8935ce29c66ac5abdde9cc44b8a33c4a3c057624bee9bdfeb8e296798c377110e2209b68fc500d872fd847fe0a7b41c6826b4db3645133a497424b5c111fc661e320b024bccf4b8120847fc92d e = 17 m = long(sys.argv[1]) c = pow(m, e, n) print m m, bound = root_e(c, e, n) print "%d (possible solution under %d)" % (m, bound)
$ time python root_e.py 123456789012345678901234567890 123456789012345678901234567890 123456789012345678901234567890 (possible solution under 1824116372372090135441744160659843710) real 0m0.012s user 0m0.008s sys 0m0.000s
Common Modulus Attack
同一の平文mを異なるeで暗号化した暗号文cの組がある場合、拡張ユークリッド互除法を用いて平文mを復号することができる。
# common_modulus_attack.py import sys import gmpy def common_modulus_attack(c1, c2, e1, e2, n): gcd, s1, s2 = gmpy.gcdext(e1, e2) if s1 < 0: s1 = -s1 c1 = gmpy.invert(c1, n) elif s2 < 0: s2 = -s2 c2 = gmpy.invert(c2, n) v = pow(c1, s1, n) w = pow(c2, s2, n) m = (v * w) % n return m if __name__ == '__main__': n = 0x00d91f0102279d099a9aa3a819faefef8e39e71075c5ed59275ae33fd16f10c6b120fbc14f2b0e85b09b7372853c22b359fb4b850e0b66da55585e1221bc23d4a84bc0cce1c1f1c080c74520c3f7cb2d041bc2c372ae96a3b9344dc00b00a75873fd339121804b39b74969ceab850a5ce8c65860fa1e7cfafb052e994a832198ece195ee8bb427a04609b69f052b1d2818741604e2d1fc95008961365f0536f1d3d12b11f3b56f55aa478b18cc5e74918869d9ef8935ce29c66ac5abdde9cc44b8a33c4a3c057624bee9bdfeb8e296798c377110e2209b68fc500d872fd847fe0a7b41c6826b4db3645133a497424b5c111fc661e320b024bccf4b8120847fc92d e1 = 65537 e2 = 257 m = long(sys.argv[1]) c1 = pow(m, e1, n) c2 = pow(m, e2, n) print m print common_modulus_attack(c1, c2, e1, e2, n)
$ time python common_modulus_attack.py 1234567890123456789012345678901234567890 1234567890123456789012345678901234567890 1234567890123456789012345678901234567890 real 0m0.014s user 0m0.012s sys 0m0.000s
Wiener's Attack
Wiener's Attackは、秘密鍵dが小さい(d < 1/3 * n1/4)とき、公開鍵n、eから秘密鍵を求めることができる手法である。 具体的には、適当なkに対してk/dがe/nで近似できることをもとに、e/nを連分数展開したのちこれを途中で打ち切ることによって得られる近似分数の中からdを見つけ出す。 秘密鍵dが小さいとき、ペアとなる公開鍵eは非常に大きくなるため、Wiener's Attackはeが大きい場合に対する手法といえる。
nがkビット、つまりnが約2kであるとしたとき、上の条件式に対応するdのビット数はk/4 - log2(3) = k/4 - 1.58となる。 適当なn=p*qに対しそのようなdをひとつ選び、対応するeを求めるコードを書くと次のようになる。
# weak_e.py import sys import random from fractions import gcd import gmpy def weak_e(): n = 0x00d91f0102279d099a9aa3a819faefef8e39e71075c5ed59275ae33fd16f10c6b120fbc14f2b0e85b09b7372853c22b359fb4b850e0b66da55585e1221bc23d4a84bc0cce1c1f1c080c74520c3f7cb2d041bc2c372ae96a3b9344dc00b00a75873fd339121804b39b74969ceab850a5ce8c65860fa1e7cfafb052e994a832198ece195ee8bb427a04609b69f052b1d2818741604e2d1fc95008961365f0536f1d3d12b11f3b56f55aa478b18cc5e74918869d9ef8935ce29c66ac5abdde9cc44b8a33c4a3c057624bee9bdfeb8e296798c377110e2209b68fc500d872fd847fe0a7b41c6826b4db3645133a497424b5c111fc661e320b024bccf4b8120847fc92d p = 0x00f4df425732311763642a435d135c62e99117e146c4b7c2a2ef3664cee80852e52a3eed7dc1f9ff36b923620562330a8751b61ca33e12c684e47d4296fb002ff708bd0dd999f2340997481b2628526d3caa75eb6ce9f9d6db8f1218dee7f41ee2e7f325ba55545f99cfd453fdac06feca005a7efdd45d9f0a4eafb6770a48929f q = 0x00e2fce6b312912d6c65b07d435b63b541b11b2e9525e83cb7342d046082aaba8adefc0be953328a573dc31b7594c23d6535e16c37bc326de39cc390e1ce550f34ac3b9b6ed75bbf9dc1fc183559779e91558c86f2c74eb2ff6b81382b9acf8223bae022a801dc29737cdcfa75cc370f56021728fe467d17cfe2484437b81c3cb3 phi = (p-1)*(q-1) while True: d = random.getrandbits(2048/4-2) if gcd(d, phi) == 1: break e = gmpy.invert(d, phi) return e if __name__ == '__main__': print hex(weak_e())
$ python weak_e.py 0x470a2650f57fed98dbde75761701a2b2711c668dcaf1f58c1e87bd1ff21b19ca107bbf8ae7cfdd31e991a6900aa2e4f24ab20fa291fb014a7a7dc73df4726a057a222aa331726cf9b9ebb22e8b8812025340ed1bdf882eef353f009cbf20c1be0e6231c8021d63e82f66c94118cefb1fd3c155bede6037f822992b8e37cd6a1b011aec6dfeb63079030e1af7fabf53bb625a7c58aceaa5805b59495989965cd62440acaa326bb90ba5d315845ad295eced02a8aca56f479c7ed97cb8dbb48b89366cb0467fa77ddfccfd09d428bc4aa6f5170e68a7c219b4c8bd032dc13946e2e1ab5d18e41eddd2dad1d8cef5e7f45dcd9ada2c696dc16f7510b155d7b72c35
このeを用いて暗号化した場合について、Wiener's Attackにより秘密鍵dを求め、これを用いて平文を復号するコードを書くと次のようになる。 このコードでは、「二次方程式の解p、qがともに整数であること」と「判別式Dが平方数であること」が必要十分の関係にあることを利用して、後者を満たすk、dの組を特定している。
# wieners_attack.py import sys import gmpy def continued_fraction(n, d): """ 415/93 = 4 + 1/(2 + 1/(6 + 1/7)) >>> continued_fraction(415, 93) [4, 2, 6, 7] """ cf = [] while d: q = n // d cf.append(q) n, d = d, n-d*q return cf def convergents_of_contfrac(cf): """ 4 + 1/(2 + 1/(6 + 1/7)) is approximately 4/1, 9/2, 58/13 and 415/93 >>> list(convergents_of_contfrac([4, 2, 6, 7])) [(4, 1), (9, 2), (58, 13), (415, 93)] """ n0, n1 = cf[0], cf[0]*cf[1]+1 d0, d1 = 1, cf[1] yield (n0, d0) yield (n1, d1) for i in xrange(2, len(cf)): n2, d2 = cf[i]*n1+n0, cf[i]*d1+d0 yield (n2, d2) n0, n1 = n1, n2 d0, d1 = d1, d2 def wieners_attack(e, n): cf = continued_fraction(e, n) convergents = convergents_of_contfrac(cf) for k, d in convergents: if k == 0: continue phi, rem = divmod(e*d-1, k) if rem != 0: continue s = n - phi + 1 # check if x^2 - s*x + n = 0 has integer roots D = s*s - 4*n if D > 0 and gmpy.is_square(D): return d if __name__ == '__main__': n = 0x00d91f0102279d099a9aa3a819faefef8e39e71075c5ed59275ae33fd16f10c6b120fbc14f2b0e85b09b7372853c22b359fb4b850e0b66da55585e1221bc23d4a84bc0cce1c1f1c080c74520c3f7cb2d041bc2c372ae96a3b9344dc00b00a75873fd339121804b39b74969ceab850a5ce8c65860fa1e7cfafb052e994a832198ece195ee8bb427a04609b69f052b1d2818741604e2d1fc95008961365f0536f1d3d12b11f3b56f55aa478b18cc5e74918869d9ef8935ce29c66ac5abdde9cc44b8a33c4a3c057624bee9bdfeb8e296798c377110e2209b68fc500d872fd847fe0a7b41c6826b4db3645133a497424b5c111fc661e320b024bccf4b8120847fc92d e = 0x470a2650f57fed98dbde75761701a2b2711c668dcaf1f58c1e87bd1ff21b19ca107bbf8ae7cfdd31e991a6900aa2e4f24ab20fa291fb014a7a7dc73df4726a057a222aa331726cf9b9ebb22e8b8812025340ed1bdf882eef353f009cbf20c1be0e6231c8021d63e82f66c94118cefb1fd3c155bede6037f822992b8e37cd6a1b011aec6dfeb63079030e1af7fabf53bb625a7c58aceaa5805b59495989965cd62440acaa326bb90ba5d315845ad295eced02a8aca56f479c7ed97cb8dbb48b89366cb0467fa77ddfccfd09d428bc4aa6f5170e68a7c219b4c8bd032dc13946e2e1ab5d18e41eddd2dad1d8cef5e7f45dcd9ada2c696dc16f7510b155d7b72c35 m = long(sys.argv[1]) c = pow(m, e, n) print m d = wieners_attack(e, n) print "found d: %d" % d m = pow(c, d, n) print m
$ time python wieners_attack.py 1234567890123456789012345678901234567890 1234567890123456789012345678901234567890 found d: 1275083233037721186926973395245184092260517180012174852353846980727795785319103669819820390556486356639260897479178672619948490889728830117728813637277349 1234567890123456789012345678901234567890 real 0m0.087s user 0m0.080s sys 0m0.004s
Håstad's Broadcast Attack
同一の平文mを異なる公開鍵nで暗号化した暗号文cをe個得られるとき、中国の剰余定理を用いてmを求めることができる。 同報通信あるいは同一平文の問題とも呼ばれる。
# hastads_broadcast_attack.py import sys import random import gmpy def random_prime(bits): while True: n = random.getrandbits(bits) if gmpy.is_prime(n): return n def chinese_remainder(pairs): N = 1 for a, n in pairs: N *= n result = 0 for a, n in pairs: m = N/n d, r, s = gmpy.gcdext(n, m) if d != 1: raise "Input not pairwise co-prime" result += a*s*m return result % N, N def hastads_broadcast_attack(e, pairs): x, n = chinese_remainder(pairs) return gmpy.root(x, e)[0] if __name__ == '__main__': m = long(sys.argv[1]) e = 17 print "collecting e=%d pairs of (c, n)" % e pairs = [] for i in xrange(e): p = random_prime(1024) q = random_prime(1024) n = p*q c = pow(m, e, n) pairs.append((c, n)) print m print hastads_broadcast_attack(e, pairs)
$ time python hastads_broadcast_attack.py 1234567890123456789012345678901234567890 collecting e=17 pairs of (c, n) 1234567890123456789012345678901234567890 1234567890123456789012345678901234567890 real 0m1.847s user 0m1.844s sys 0m0.000s
その他の攻撃手法
他の攻撃手法としては、LLL格子基底縮小アルゴリズムによるCoppersmithの定理を応用した手法が挙げられる。 具体的には、上位bitが等しい二つの平文に対するFranklin-Reiter Related Message AttackおよびCoppersmith’s Short Pad Attack、d < n0.292となるような小さなdに対するBoneh-Durfee Attackがある。
関連リンク
- Twenty Years of Attacks on the RSA Cryptosystem
- 公開鍵暗号 - RSA - 基礎 - ₍₍ (ง ˘ω˘ )ว ⁾⁾ < 暗号楽しいです
- 公開鍵暗号 RSA - Common Modulus Attack, 秘密鍵からの素因数分解 - ₍₍ (ง ˘ω˘ )ว ⁾⁾ < 暗号楽しいです
- 公開鍵暗号 - RSA - Wiener's Attack - ₍₍ (ง ˘ω˘ )ว ⁾⁾ < 暗号楽しいです
- v0id s3curity: Volga CTF Quals 2013 - Crypto 200 - [Team xbios]
- HackYou 2014 - Crypto 400 - CRYPTONET - codezen.fr