├── Bleichenbacher_Oracle ├── Oracle │ ├── Bleichenbacher.py │ └── __init__.py ├── PKCS │ ├── Formatting.py │ └── __init__.py ├── TypeChecking │ ├── Annotations.py │ └── __init__.py └── main.py ├── Parity_Oracle ├── Oracle │ ├── Parity.py │ └── __init__.py └── main.py ├── README.md └── beamer.pdf /Bleichenbacher_Oracle/Oracle/Bleichenbacher.py: -------------------------------------------------------------------------------- 1 | from TypeChecking.Annotations import typecheck 2 | from Crypto.PublicKey import RSA 3 | from Crypto.Cipher import PKCS1_v1_5 4 | 5 | 6 | class Oracle(): 7 | """ 8 | Bleichebacher's oracle implementing methods available to eve. 9 | """ 10 | 11 | @typecheck 12 | def __init__(self): 13 | """ 14 | Setup keys, secret message and encryption/decryption schemes. 15 | """ 16 | self._key = RSA.generate(1024) 17 | self._pkcs = PKCS1_v1_5.new(self._key) 18 | self._secret = b'This is how Daniel Bleichenbachers adaptive chosen-ciphertext attack works...' 19 | self._pkcsmsg = self._pkcs.encrypt(self._secret) 20 | 21 | @typecheck 22 | def get_n(self) -> int: 23 | """ 24 | Returns the public RSA modulus. 25 | """ 26 | return self._key.n 27 | 28 | @typecheck 29 | def get_e(self) -> int: 30 | """ 31 | Returns the public RSA exponent. 32 | """ 33 | return self._key.e 34 | 35 | @typecheck 36 | def get_k(self) -> int: 37 | """ 38 | Returns the length of the RSA modulus in bytes. 39 | """ 40 | return (self._key.size() + 1) // 8 41 | 42 | @typecheck 43 | def eavesdrop(self) -> bytes: 44 | return self._pkcsmsg 45 | 46 | @typecheck 47 | def decrypt(self, ciphertext: bytes) -> bool: 48 | """ 49 | Modified decrypt method for demonstration purposes. 50 | See 'Cipher/PKCS1-v1_5.py' for the correct version. 51 | 52 | :param ciphertext: Ciphertext that contains the message to recover. 53 | :return: True iff the decrypted message is correctly padded according to PKCS#1 v1.5; otherwise False. 54 | """ 55 | 56 | # Step 1 57 | if len(ciphertext) != self.get_k(): 58 | raise ValueError("Ciphertext with incorrect length.") 59 | 60 | # Step 2a (OS2IP), 2b (RSADP), and part of 2c (I2OSP) 61 | m = self._key.decrypt(ciphertext) 62 | 63 | # Complete step 2c (I2OSP) 64 | em = b"\x00" * (self.get_k() - len(m)) + m 65 | 66 | # Step 3 (modified) 67 | sep = em.find(b"\x00", 2) 68 | 69 | # TODO: Justify oracle strength... --> for testing purposes 70 | 71 | if not em.startswith(b'\x00\x02'): 72 | return False 73 | 74 | #if not em.startswith(b'\x00\x02') or sep < 10: # typically sep position will be checked 75 | # return False 76 | 77 | # Step 4 (modified) 78 | return True 79 | -------------------------------------------------------------------------------- /Bleichenbacher_Oracle/Oracle/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duesee/bleichenbacher/e13104542e00556adfea960a7acad2dd184da3b8/Bleichenbacher_Oracle/Oracle/__init__.py -------------------------------------------------------------------------------- /Bleichenbacher_Oracle/PKCS/Formatting.py: -------------------------------------------------------------------------------- 1 | from TypeChecking.Annotations import typecheck 2 | 3 | 4 | @typecheck 5 | def os2ip(octets: bytes) -> int: 6 | """ 7 | Octet-String-to-Integer primitive 8 | PKCS #1 Version 1.5 (RFC2313) 9 | """ 10 | return int.from_bytes(octets, 'big') 11 | 12 | 13 | @typecheck 14 | def i2osp(i: int, k: int) -> bytes: 15 | """ 16 | Integer-to-Octet-String primitive 17 | PKCS #1 Version 1.5 (RFC2313) 18 | """ 19 | return i.to_bytes(k, byteorder='big') 20 | -------------------------------------------------------------------------------- /Bleichenbacher_Oracle/PKCS/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duesee/bleichenbacher/e13104542e00556adfea960a7acad2dd184da3b8/Bleichenbacher_Oracle/PKCS/__init__.py -------------------------------------------------------------------------------- /Bleichenbacher_Oracle/TypeChecking/Annotations.py: -------------------------------------------------------------------------------- 1 | def typecheck(f): 2 | """ 3 | Taken from "Python 3 - Das umfassende Handbuch" 4 | """ 5 | def decorated(*args, **kws): 6 | for i, name in enumerate(f.__code__.co_varnames): 7 | argtype = f.__annotations__.get(name) 8 | 9 | # Only check if annotation exists and it is as a type 10 | if isinstance(argtype, type): 11 | # First len(args) are positional... 12 | if i < len(args): 13 | if not isinstance(args[i], argtype): 14 | raise TypeError("Positional argument {0} has type {1} but expected was type {2}.".format(i, type(args[i]), argtype)) 15 | # ...after that check keywords 16 | elif name in kws: 17 | if not isinstance(kws[name], argtype): 18 | raise TypeError("Keyword argument '{0}' has type {1} but expected was type {2}.".format(name, type(kws[name]), argtype)) 19 | 20 | res = f(*args, **kws) 21 | restype = f.__annotations__.get('return') 22 | 23 | # Check return type 24 | if isinstance(restype, type): 25 | if not isinstance(res, restype): 26 | raise TypeError("Return value has type {0} but expected was type {1}.".format(type(res), restype)) 27 | 28 | return res 29 | 30 | return decorated 31 | -------------------------------------------------------------------------------- /Bleichenbacher_Oracle/TypeChecking/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duesee/bleichenbacher/e13104542e00556adfea960a7acad2dd184da3b8/Bleichenbacher_Oracle/TypeChecking/__init__.py -------------------------------------------------------------------------------- /Bleichenbacher_Oracle/main.py: -------------------------------------------------------------------------------- 1 | from TypeChecking.Annotations import typecheck 2 | from Oracle.Bleichenbacher import Oracle 3 | from PKCS.Formatting import os2ip, i2osp 4 | from sys import stdout 5 | 6 | 7 | @typecheck 8 | def extended_gcd(aa: int, bb: int) -> tuple: 9 | """ 10 | http://rosettacode.org/wiki/Modular_inverse#Python 11 | """ 12 | lastremainder, remainder = abs(aa), abs(bb) 13 | x, lastx, y, lasty = 0, 1, 1, 0 14 | while remainder: 15 | lastremainder, (quotient, remainder) = remainder, divmod(lastremainder, remainder) 16 | x, lastx = lastx - quotient * x, x 17 | y, lasty = lasty - quotient * y, y 18 | return lastremainder, lastx * (-1 if aa < 0 else 1), lasty * (-1 if bb < 0 else 1) 19 | 20 | 21 | @typecheck 22 | def modinv(a: int, m: int) -> int: 23 | """ 24 | http://rosettacode.org/wiki/Modular_inverse#Python 25 | """ 26 | g, x, y = extended_gcd(a, m) 27 | if g != 1: 28 | raise ValueError 29 | return x % m 30 | 31 | 32 | @typecheck 33 | def interval(a: int, b: int) -> range: 34 | return range(a, b + 1) 35 | 36 | 37 | @typecheck 38 | def ceildiv(a: int, b: int) -> int: 39 | """ 40 | http://stackoverflow.com/a/17511341 41 | """ 42 | return -(-a // b) 43 | 44 | 45 | @typecheck 46 | def floordiv(a: int, b: int) -> int: 47 | """ 48 | http://stackoverflow.com/a/17511341 49 | """ 50 | return a // b 51 | 52 | 53 | @typecheck 54 | def bleichenbacher(oracle: Oracle): 55 | """ 56 | Bleichenbacher's attack 57 | 58 | Good ideas taken from: 59 | http://secgroup.dais.unive.it/wp-content/uploads/2012/11/Practical-Padding-Oracle-Attacks-on-RSA.html 60 | """ 61 | 62 | k, n, e = oracle.get_k(), oracle.get_n(), oracle.get_e() 63 | 64 | B = pow(2, 8 * (k - 2)) 65 | B2 = 2 * B 66 | B3 = B2 + B 67 | 68 | @typecheck 69 | def pkcs_conformant(c_param: int, s_param: int) -> bool: 70 | """ 71 | Helper-Function to check for PKCS conformance. 72 | """ 73 | pkcs_conformant.counter += 1 74 | return oracle.decrypt(i2osp(c_param * pow(s_param, e, n) % n, k)) 75 | 76 | pkcs_conformant.counter = 0 77 | 78 | cipher = os2ip(oracle.eavesdrop()) 79 | 80 | assert(pkcs_conformant(cipher, 1)) 81 | 82 | c_0 = cipher 83 | set_m_old = {(B2, B3 - 1)} 84 | i = 1 85 | 86 | s_old = 0 87 | while True: 88 | if i == 1: 89 | s_new = ceildiv(n, B3) 90 | while not pkcs_conformant(c_0, s_new): 91 | s_new += 1 92 | 93 | elif i > 1 and len(set_m_old) >= 2: 94 | s_new = s_old + 1 95 | while not pkcs_conformant(c_0, s_new): 96 | s_new += 1 97 | 98 | elif len(set_m_old) == 1: 99 | a, b = next(iter(set_m_old)) 100 | found = False 101 | r = ceildiv(2 * (b * s_old - B2), n) 102 | while not found: 103 | for s in interval(ceildiv(B2 + r*n, b), floordiv(B3 - 1 + r*n, a)): 104 | if pkcs_conformant(c_0, s): 105 | found = True 106 | s_new = s 107 | break 108 | r += 1 109 | 110 | set_m_new = set() 111 | for a, b in set_m_old: 112 | r_min = ceildiv(a * s_new - B3 + 1, n) 113 | r_max = floordiv(b * s_new - B2, n) 114 | for r in interval(r_min, r_max): 115 | new_lb = max(a, ceildiv(B2 + r*n, s_new)) 116 | new_ub = min(b, floordiv(B3 - 1 + r*n, s_new)) 117 | if new_lb <= new_ub: # intersection must be non-empty 118 | set_m_new |= {(new_lb, new_ub)} 119 | 120 | print("Calculated new intervals set_m_new = {} in Step 3".format(set_m_new)) 121 | 122 | if len(set_m_new) == 1: 123 | a, b = next(iter(set_m_new)) 124 | if a == b: 125 | print("Calculated: ", i2osp(a, k)) 126 | print("Calculated int: ", a) 127 | print("Success after {} calls to the oracle.".format(pkcs_conformant.counter)) 128 | return a 129 | 130 | i += 1 131 | s_old = s_new 132 | set_m_old = set_m_new 133 | 134 | 135 | @typecheck 136 | def bleichenbacher_simulation(k, n, m): 137 | """ 138 | Bleichenbacher's attack 139 | 140 | Good ideas taken from: 141 | http://secgroup.dais.unive.it/wp-content/uploads/2012/11/Practical-Padding-Oracle-Attacks-on-RSA.html 142 | """ 143 | 144 | B = pow(2, 8 * (k - 2)) 145 | B2 = 2 * B 146 | B3 = B2 + B 147 | 148 | print("---------------") 149 | print("k", k) 150 | print("n", n) 151 | print("m", m) 152 | print("B2", B2) 153 | print("B3-1", B3-1) 154 | print("---------------") 155 | 156 | @typecheck 157 | def pkcs_conformant(s: int, m: int) -> bool: 158 | """ 159 | Helper-Function to check for PKCS conformance with unencrypted m. 160 | """ 161 | pkcs_conformant.counter += 1 162 | return B2 <= ((s * m) % n) < B3 # Chained comparisons are allowed in Python... :-) 163 | 164 | pkcs_conformant.counter = 0 165 | 166 | """ 167 | Step 1: Blinding. 168 | Can be skipped if c is already PKCS conforming (i.e., when c is an encrypted message). 169 | In that case, we set s_0 = 1. 170 | """ 171 | 172 | print("Starting with Step 1") 173 | 174 | assert(pkcs_conformant(1, m)) 175 | 176 | s_0 = 1 # Since we know that c_0 is PKCS-conformant, we don't need to find a new s in the first step. 177 | m_0 = m * s_0 % n # Mathematically trivial - only for explanation purposes... 178 | set_m_old = {(B2, B3 - 1)} 179 | i = 1 180 | 181 | for v in set_m_old: 182 | stdout.write(str(v)) 183 | stdout.write(";") 184 | 185 | print("") 186 | 187 | s_old = 0 188 | while True: 189 | """ 190 | Step 2: Searching for PKCS conforming messages. 191 | """ 192 | 193 | print("Starting with Step 2") 194 | 195 | if i == 1: 196 | """ 197 | Step 2.a: Starting the search. 198 | If i = 1, then search for the smallest positive integer s_1 \geq n/(3B), 199 | such that the ciphertext c_0*(s_1)^e mod n is PKCS conforming. 200 | """ 201 | 202 | print("Starting with Step 2.a") 203 | 204 | s_new = ceildiv(n, B3) # Explanation follows... 205 | while not pkcs_conformant(s_new, m_0): 206 | s_new += 1 207 | 208 | print("Found s_new = {} in Step 2.a".format(s_new)) 209 | 210 | elif i > 1 and len(set_m_old) >= 2: 211 | """ 212 | Step 2.b: Searching with more than one interval left. 213 | If i > 1 and the number of intervals in M_{i−1} is at least 2, then search for the 214 | smallest integer s_i > s_{i−1}, such that the ciphertext c_0*(s_i)^e mod n is PKCS conforming. 215 | """ 216 | 217 | print("Starting with Step 2.b") 218 | 219 | s_new = s_old + 1 220 | while not pkcs_conformant(s_new, m_0): 221 | s_new += 1 222 | 223 | print("Found s_new = {} in Step 2.b".format(s_new)) 224 | 225 | elif len(set_m_old) == 1: 226 | """ 227 | Step 2.c: Searching with one interval left. 228 | If M_{i−1} contains exactly one interval (i.e., M_{i−1} = {[a, b]}), 229 | then choose small integer values r_i, s_i such that 230 | 231 | r_i \geq 2 * (bs_{i-1} - 2B) / n 232 | 233 | and 234 | 235 | (2B + r_i*n) / b \leq s_i < (3B + r_i*n) / a, 236 | 237 | until the ciphertext c_0*(s_i)^e mod n is PKCS conforming. 238 | """ 239 | 240 | print("Starting with Step 2.c") 241 | 242 | a, b = next(iter(set_m_old)) 243 | found = False 244 | r = ceildiv(2 * (b * s_old - B2), n) 245 | while not found: 246 | for s in interval(ceildiv(B2 + r*n, b), floordiv(B3 - 1 + r*n, a)): 247 | if pkcs_conformant(s, m_0): 248 | found = True 249 | s_new = s 250 | break 251 | r += 1 252 | 253 | print("Found s_new = {} in Step 2.c".format(s_new)) 254 | 255 | """ 256 | Step 3: Narrowing the set of solutions. 257 | After s_i has been found, the set M_i is computed as 258 | 259 | M_i = \bigcup_{(a, b, r)} { [max(a, [2B+rn / s_i]), min(b, [3B-1+rn / s_i])] } 260 | 261 | for all [a, b] \in M_{i-1} and (as_i - 3B + 1)/(n) \leq r \leq (bs_i - 2B)/(n). 262 | """ 263 | 264 | print("Starting with Step 3") 265 | 266 | set_m_new = set() 267 | for a, b in set_m_old: 268 | r_min = ceildiv(a * s_new - B3 + 1, n) 269 | r_max = floordiv(b * s_new - B2, n) 270 | 271 | print("Found new values for r and a = {}, b = {} -- {} <= r <= {}".format(a, b, r_min, r_max)) 272 | 273 | for r in interval(r_min, r_max): 274 | new_lb = max(a, ceildiv(B2 + r*n, s_new)) 275 | new_ub = min(b, floordiv(B3 - 1 + r*n, s_new)) 276 | if new_lb <= new_ub: # intersection must be non-empty 277 | set_m_new |= {(new_lb, new_ub)} 278 | 279 | for v in set_m_new: 280 | stdout.write(str(v)) 281 | stdout.write(";") 282 | 283 | print("") 284 | 285 | """ 286 | Step 4: Computing the solution. 287 | If M_i contains only one interval of length 1 (i.e., M_i = {[a, a]}), 288 | then set m = a(s_0)^{−1} mod n, and return m as solution of m \equiv c^d (mod n). 289 | Otherwise, set i = i + 1 and go to step 2. 290 | """ 291 | 292 | print("Starting with Step 4") 293 | 294 | if len(set_m_new) == 1: 295 | a, b = next(iter(set_m_new)) 296 | if a == b: 297 | print("Original: ", hex(m)) 298 | print("Calculated: ", hex(a)) 299 | print("Success after {} calls to the oracle.".format(pkcs_conformant.counter)) 300 | return a 301 | 302 | i += 1 303 | #print("Intervals retry", set_m_new) 304 | print("Going back to step 2") 305 | s_old = s_new 306 | set_m_old = set_m_new 307 | 308 | print("No luck for set_m_new = {} in Step 4".format(set_m_new)) 309 | 310 | 311 | @typecheck 312 | def run_tests(): 313 | """ 314 | Tests to validate the algorithm. 315 | """ 316 | tests = [ 317 | #{"k": 4, "p": 10007, "q": 10037}, 318 | #{"k": 8, "p": 1000000007, "q": 1000000009}, 319 | #{"k": 16, "p": 15309168720959725921, "q": 12819619822143804367}, 320 | #{"k": 32, "p": 313115142601654954062569328755831304743, "q": 255336707253239299888475776540791782543}, 321 | #{"k": 128, "p": 13244628888829977635820637741951867212791935321427723006722254687460420690588887758810928307766709750393036804634467753421052044765639091747501507784485213, "q": 11583722350869317111812024666321163171121150655325205224246499810906481321757898291274629991421369420603573230614994104724581115295807013606297300412089473}, 322 | {"k": 256, "p": 167230636094866282461211664159158428279902699551992447152002026086321450931356694020366071860452874936312114743689156451204955885905421523426919810372766672329365549589557666294538091910136590569719360771818561583316969574158815755289605442067349031803550793473499645742177046498728201139371344588674279678939, "q": 153746015991426629737627279764483089360526745536038013938043722107713599542535853191396561623967198520570205780805436781605500829408932188321165836162114053101365153759076284383142329005938490083741368370827739032497065588280706602245858167496933285469691492972704468586486254156236192774677901155398324342253} 323 | ] 324 | 325 | for test in tests: 326 | n = test["p"] * test["q"] 327 | B = pow(2, 8 * (test["k"] - 2)) 328 | B2 = 2 * B 329 | m = b"ABCD" 330 | m = B2 + int(m, 16) 331 | bleichenbacher_simulation(test["k"], n, m) 332 | 333 | 334 | if __name__ == "__main__": 335 | oracle = Oracle() 336 | #run_tests() 337 | bleichenbacher(oracle), oracle.get_k() 338 | -------------------------------------------------------------------------------- /Parity_Oracle/Oracle/Parity.py: -------------------------------------------------------------------------------- 1 | from Crypto.PublicKey import RSA 2 | 3 | 4 | 5 | class Oracle(): 6 | """ 7 | Parity Oracle implementing all methods available to eve. 8 | """ 9 | 10 | def __init__(self): 11 | """ 12 | Constructor. 13 | """ 14 | self.rsa = RSA.generate(2048) 15 | 16 | def get_n(self): 17 | """ 18 | Returns the public RSA modulus. 19 | """ 20 | return self.rsa.n 21 | 22 | def get_e(self): 23 | """ 24 | Returns the public RSA exponent. 25 | """ 26 | return self.rsa.e 27 | 28 | def encrypt(self, plaintext): 29 | """ 30 | Returns the encrypted plaintext. 31 | """ 32 | return self.rsa.encrypt(plaintext, None)[0] 33 | 34 | def decrypt(self, ciphertext): 35 | """ 36 | Returns true if the decrypted plaintext is even - false otherwise. 37 | """ 38 | return (self.rsa.decrypt(ciphertext) % 2 == 0) 39 | -------------------------------------------------------------------------------- /Parity_Oracle/Oracle/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duesee/bleichenbacher/e13104542e00556adfea960a7acad2dd184da3b8/Parity_Oracle/Oracle/__init__.py -------------------------------------------------------------------------------- /Parity_Oracle/main.py: -------------------------------------------------------------------------------- 1 | from Oracle.Parity import Oracle 2 | from decimal import * 3 | from math import ceil, floor, log 4 | 5 | 6 | if __name__ == "__main__": 7 | """ 8 | http://secgroup.dais.unive.it/wp-content/uploads/2012/11/Practical-Padding-Oracle-Attacks-on-RSA.html 9 | """ 10 | oracle = Oracle() 11 | 12 | e, n = oracle.get_e(), oracle.get_n() 13 | 14 | c = oracle.encrypt(123456789123456789123456789) 15 | 16 | enctwo = pow(2, e, n) 17 | 18 | lb = Decimal(0) 19 | ub = Decimal(n) 20 | 21 | k = int(ceil(log(n, 2))) # n. of iterations 22 | getcontext().prec = k # allows for 'precise enough' floats 23 | 24 | for i in range(1, k + 1): 25 | c = (c * enctwo) % n # Adapting c... 26 | 27 | nb = (lb + ub) / 2 28 | 29 | if oracle.decrypt(c): 30 | ub = nb 31 | else: 32 | lb = nb 33 | 34 | print("{:>4}: [{}, {}]".format(i, int(lb), int(ub))) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bleichenbachers "Million Message Attack" on RSA 2 | 3 | This repo contains two python projects to demonstrate Daniel Bleichenbacher's million message attack against RSA encryption and PKCS #1 padding. 4 | 5 | ## PKCS #1 Padding Oracle 6 | 7 | The bleichenbacher oracle is implemented in `Bleichenbacher_Oracle`. 8 | 9 | ## RSA Parity Oracle 10 | 11 | The effects of leaking the LSB (the Parity-Bit) of an RSA plaintext is demonstrated via `Parity_Oracle`. 12 | 13 | The code is mostly taken from [Practical-Padding-Oracle-Attacks-on-RSA](http://secgroup.dais.unive.it/wp-content/uploads/2012/11/Practical-Padding-Oracle-Attacks-on-RSA.html). Since we find it very helpful to understand the Bleichenbacher oracle, it is included here as well. 14 | 15 | # Good to know 16 | 17 | * The `python-crypto` package is needed. Please install it. 18 | * Text in Python 3 is always Unicode and represented by the `str` type. Binary data is represented by the `bytes` type. Please see http://eli.thegreenplace.net/2012/01/30/the-bytesstr-dichotomy-in-python-3 for further information. 19 | * We had some trouble with type errors during development. Thus, many functions are type-checked via a custom decorator and special annotations. (We found this pattern to be very useful in many scenarios.) 20 | 21 | # Literature 22 | 23 | * "Chosen Ciphertext Attacks Against Protocols Based on the RSA Encryption Standard PKCS #1" by Daniel Bleichenbacher. 24 | * [Practical-Padding-Oracle-Attacks-on-RSA](http://secgroup.dais.unive.it/wp-content/uploads/2012/11/Practical-Padding-Oracle-Attacks-on-RSA.html) 25 | -------------------------------------------------------------------------------- /beamer.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/duesee/bleichenbacher/e13104542e00556adfea960a7acad2dd184da3b8/beamer.pdf --------------------------------------------------------------------------------