Skip to content

Commit

Permalink
Merge pull request #819 from tlsfuzzer/bleichenbacher-updates
Browse files Browse the repository at this point in the history
Bleichenbacher test updates (#819)
  • Loading branch information
tomato42 committed Jun 9, 2023
2 parents 8cb1c0e + 6871359 commit 00ff2c6
Show file tree
Hide file tree
Showing 8 changed files with 1,718 additions and 100 deletions.
13 changes: 13 additions & 0 deletions docs/source/timing-analysis.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ file:
isolated_cores=2-10
no_balance_cores=2-10
.. note::

You should isolate both processors of the hyper-threaded pair (or quadruple,
or octect...). Use a tool like ``lscpu -p`` to identify which Linux CPUs
run on which physical cores.

Then apply the profile:

.. code:: bash
Expand Down Expand Up @@ -102,6 +108,13 @@ And the general requirements to collect and analyse timing results:
data points, using them will only make tlsfuzzer run the tests at a higher
frequency.

.. note::
RHEL-8 doesn't respect the QUICKACK setting on the C API. The users need
to declare the loopback as a quickack route, otherwise the time between
packets will be counted as zero. Use a command like
``ip route change local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1 quickack 1``
to enable it.

Testing theory
==============

Expand Down
1,407 changes: 1,407 additions & 0 deletions scripts/test-bleichenbacher-timing-pregenerate.py

Large diffs are not rendered by default.

249 changes: 161 additions & 88 deletions scripts/test-bleichenbacher-timing.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from tlsfuzzer.helpers import SIG_ALL, RSA_PKCS1_ALL


version = 13
version = 16


def help_msg():
Expand Down Expand Up @@ -78,6 +78,8 @@ def help_msg():
print(" --static-enc Re-use once generated RSA ciphertext. This may make the")
print(" timing signal weaker or stronger depending on implementation.")
print(" By default ciphertexts that have padding will be randomised.")
print(" --test-set Execute a pre-selected subset of tests.")
print(" Available: 'raw decrypted value'")
print(" --help this message")


Expand All @@ -101,6 +103,7 @@ def main():
cipher = CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA
affinity = None
reuse_rsa_ciphertext = False
test_set = None

argv = sys.argv[1:]
opts, args = getopt.getopt(argv,
Expand All @@ -110,7 +113,8 @@ def main():
"no-sni",
"repeat=",
"cpu-list=",
"static-enc"])
"static-enc",
"test-set="])
for opt, arg in opts:
if opt == '-h':
host = arg
Expand Down Expand Up @@ -159,6 +163,8 @@ def main():
elif opt == '--help':
help_msg()
sys.exit(0)
elif opt == "--test-set":
test_set = arg
else:
raise ValueError("Unknown option: {0}".format(opt))

Expand All @@ -167,6 +173,45 @@ def main():
else:
run_only = None

if run_only and test_set:
raise ValueError("Can't specify test set and individual tests together")

if test_set == "raw decrypted value":
if reuse_rsa_ciphertext:
run_only = set((
"control - fuzzed pre master secret 1",
"control - fuzzed pre master secret 2",
"control - fuzzed pre master secret 3",
"too short PKCS padding - 1 bytes - 0",
"too short PKCS padding - 1 bytes - 1",
"too short PKCS padding - 1 bytes - 2",
"too short PKCS padding - 1 bytes - 3",
"too short PKCS padding - 4 bytes - 0",
"too short PKCS padding - 4 bytes - 1",
"too short PKCS padding - 4 bytes - 2",
"too short PKCS padding - 4 bytes - 3",
"too short PKCS padding - 8 bytes - 0",
"too short PKCS padding - 8 bytes - 1",
"too short PKCS padding - 8 bytes - 2",
"too short PKCS padding - 8 bytes - 3",
"very short PKCS padding (40 bytes short) - 0",
"very short PKCS padding (40 bytes short) - 1",
"very short PKCS padding (40 bytes short) - 2",
"very short PKCS padding (40 bytes short) - 3",
))
else:
run_only = set((
"control - fuzzed pre master secret 1",
"control - fuzzed pre master secret 2",
"control - fuzzed pre master secret 3",
"too short PKCS padding - 1 bytes",
"too short PKCS padding - 4 bytes",
"too short PKCS padding - 8 bytes",
"very short PKCS padding (40 bytes short)",
))
elif test_set is not None:
raise ValueError("Unrecognised test set name: {0}".format(test_set))

cln_extensions = {ExtensionType.renegotiation_info: None}
if is_valid_hostname(host) and not no_sni:
cln_extensions[ExtensionType.server_name] = \
Expand Down Expand Up @@ -240,34 +285,42 @@ def main():

conversations["sanity - static non-zero byte in random padding"] = conversation

# create a CKE with PMS the runner doesn't know/use
# (benchmark to measure other tests to)
conversation = Connect(host, port)
node = conversation
ciphers = [cipher]
node = node.add_child(ClientHelloGenerator(ciphers,
extensions=cln_extensions))
node = node.add_child(ExpectServerHello(extensions=srv_extensions))

node = node.add_child(ExpectCertificate())
node = node.add_child(ExpectServerHelloDone())
node = node.add_child(TCPBufferingEnable())
# use too short PMS but then change padding so that the PMS is
# correct length with correct TLS version but the encryption keys
# that tlsfuzzer calculates will be incorrect
node = node.add_child(ClientKeyExchangeGenerator(
padding_subs={-3: 0, -2: 3, -1: 3},
premaster_secret=bytearray([0] * 46),
reuse_encrypted_premaster=reuse_rsa_ciphertext))
node = node.add_child(ChangeCipherSpecGenerator())
node = node.add_child(FinishedGenerator())
node = node.add_child(TCPBufferingDisable())
node = node.add_child(TCPBufferingFlush())
node = node.add_child(ExpectAlert(level,
alert))
node.add_child(ExpectClose())

conversations["fuzzed pre master secret"] = conversation
for i in range(1, 4):
# create a CKE with PMS the runner doesn't know/use
# (benchmark to measure other tests to)
conversation = Connect(host, port)
node = conversation
ciphers = [cipher]
node = node.add_child(ClientHelloGenerator(ciphers,
extensions=cln_extensions))
node = node.add_child(ExpectServerHello(extensions=srv_extensions))

node = node.add_child(ExpectCertificate())
node = node.add_child(ExpectServerHelloDone())
node = node.add_child(TCPBufferingEnable())
# use too short PMS but then change padding so that the PMS is
# correct length with correct TLS version but the encryption keys
# that tlsfuzzer calculates will be incorrect
padding_xors=None
if i == 3:
# for the third control probe make sure that the signal from
# fuzzing is very visible to the server
padding_xors=dict((i, 0) for i in range(120))
node = node.add_child(ClientKeyExchangeGenerator(
padding_subs={-3: 0, -2: 3, -1: 3},
padding_xors=padding_xors,
premaster_secret=bytearray([0] * 46),
reuse_encrypted_premaster=reuse_rsa_ciphertext))
node = node.add_child(ChangeCipherSpecGenerator())
node = node.add_child(FinishedGenerator())
node = node.add_child(TCPBufferingDisable())
node = node.add_child(TCPBufferingFlush())
node = node.add_child(ExpectAlert(level,
alert))
node.add_child(ExpectClose())

conversations["control - fuzzed pre master secret {0}".format(i)] =\
conversation

# set 2nd byte of padding to 3 (invalid value)
conversation = Connect(host, port)
Expand Down Expand Up @@ -760,61 +813,78 @@ def main():

conversations["wrong TLS version (0, 0) in pre master secret"] = conversation

# check if too short PKCS padding is detected
conversation = Connect(host, port)
node = conversation
ciphers = [cipher]
node = node.add_child(ClientHelloGenerator(ciphers,
extensions=cln_extensions))
node = node.add_child(ExpectServerHello(extensions=srv_extensions))

node = node.add_child(ExpectCertificate())
node = node.add_child(ExpectServerHelloDone())
node = node.add_child(TCPBufferingEnable())
# move the start of the padding forward, essentially encrypting two 0 bytes
# at the beginning of the padding, but since those are transformed into a number
# their existence is lost and it just like the padding was too small
node = node.add_child(ClientKeyExchangeGenerator(
padding_subs={1: 0, 2: 2},
reuse_encrypted_premaster=reuse_rsa_ciphertext))
node = node.add_child(ChangeCipherSpecGenerator())
node = node.add_child(FinishedGenerator())
node = node.add_child(TCPBufferingDisable())
node = node.add_child(TCPBufferingFlush())
node = node.add_child(ExpectAlert(level,
alert))
node.add_child(ExpectClose())

conversations["too short PKCS padding"] = conversation

# check if very short PKCS padding doesn't have a different behaviour
conversation = Connect(host, port)
node = conversation
ciphers = [cipher]
node = node.add_child(ClientHelloGenerator(ciphers,
extensions=cln_extensions))
node = node.add_child(ExpectServerHello(extensions=srv_extensions))

node = node.add_child(ExpectCertificate())
node = node.add_child(ExpectServerHelloDone())
node = node.add_child(TCPBufferingEnable())
# move the start of the padding 40 bytes towards LSB
subs = {}
for i in range(41):
subs[i] = 0
subs[41] = 2
node = node.add_child(ClientKeyExchangeGenerator(
padding_subs=subs,
reuse_encrypted_premaster=reuse_rsa_ciphertext))
node = node.add_child(ChangeCipherSpecGenerator())
node = node.add_child(FinishedGenerator())
node = node.add_child(TCPBufferingDisable())
node = node.add_child(TCPBufferingFlush())
node = node.add_child(ExpectAlert(level,
alert))
node.add_child(ExpectClose())

conversations["very short PKCS padding (40 bytes short)"] = conversation
for rep in range(4 if reuse_rsa_ciphertext else 1):
for i in [1, 4, 8]:
# check if too short PKCS padding is detected
conversation = Connect(host, port)
node = conversation
ciphers = [cipher]
node = node.add_child(ClientHelloGenerator(ciphers,
extensions=cln_extensions))
node = node.add_child(ExpectServerHello(extensions=srv_extensions))

node = node.add_child(ExpectCertificate())
node = node.add_child(ExpectServerHelloDone())
node = node.add_child(TCPBufferingEnable())
# move the start of the padding forward, essentially encrypting two 0 bytes
# at the beginning of the padding, but since those are transformed into a number
# their existence is lost and it just like the padding was too small
padding_subs = {}
for j in range(1, 1+i):
padding_subs[j] = 0
padding_subs[i+1] = 2
node = node.add_child(ClientKeyExchangeGenerator(
padding_subs=padding_subs,
reuse_encrypted_premaster=reuse_rsa_ciphertext))
node = node.add_child(ChangeCipherSpecGenerator())
node = node.add_child(FinishedGenerator())
node = node.add_child(TCPBufferingDisable())
node = node.add_child(TCPBufferingFlush())
node = node.add_child(ExpectAlert(level,
alert))
node.add_child(ExpectClose())

suffix = ""
if reuse_rsa_ciphertext:
suffix = " - {0}".format(rep)

conversations["too short PKCS padding - {0} bytes{1}"
.format(i, suffix)] = conversation

for j in range(4 if reuse_rsa_ciphertext else 1):
# check if very short PKCS padding doesn't have a different behaviour
conversation = Connect(host, port)
node = conversation
ciphers = [cipher]
node = node.add_child(ClientHelloGenerator(ciphers,
extensions=cln_extensions))
node = node.add_child(ExpectServerHello(extensions=srv_extensions))

node = node.add_child(ExpectCertificate())
node = node.add_child(ExpectServerHelloDone())
node = node.add_child(TCPBufferingEnable())
# move the start of the padding 40 bytes towards LSB
subs = {}
for i in range(41):
subs[i] = 0
subs[41] = 2
node = node.add_child(ClientKeyExchangeGenerator(
padding_subs=subs,
reuse_encrypted_premaster=reuse_rsa_ciphertext))
node = node.add_child(ChangeCipherSpecGenerator())
node = node.add_child(FinishedGenerator())
node = node.add_child(TCPBufferingDisable())
node = node.add_child(TCPBufferingFlush())
node = node.add_child(ExpectAlert(level,
alert))
node.add_child(ExpectClose())

suffix = ""
if reuse_rsa_ciphertext:
suffix = " - {0}".format(j)

conversations["very short PKCS padding (40 bytes short){0}"
.format(suffix)] = conversation

# check if too long PKCS padding is detected
conversation = Connect(host, port)
Expand Down Expand Up @@ -1022,7 +1092,9 @@ def main():
if some groups of tests are inconsistent, that points to likely
place where the timing leak happens:
- the control test case:
- 'fuzzed pre master secret' - this will end up with random
- 'control - fuzzed pre master secret 1',
'control - fuzzed pre master secret 2', and
'control - fuzzed pre master secret 3' - this will end up with random
plaintexts in record with Finished, most resembling a randomly
selected PMS by the server
- 'random plaintext' - this will end up with a completely random
Expand Down Expand Up @@ -1062,8 +1134,9 @@ def main():
null byte separating padding and encrypted value
- 'no encrypted value' - this sends a null separator, but it's
the last byte of plaintext
- 'too short PKCS padding' - this sends the correct encryption
type in the padding (2), but one byte later than required
- 'too short PKCS padding - 1 bytes', 'too short PKCS padding - 4 bytes',
'too short PKCS padding - 8 bytes' - this sends the correct encryption
type in the padding (2), but one, 4 or 8 bytes later than required
- 'very short PKCS padding (40 bytes short)' - same as above
only 40 bytes later
- 'too long PKCS padding' this doesn't send the PKCS#1 v1.5
Expand Down

0 comments on commit 00ff2c6

Please sign in to comment.