Created
March 5, 2020 14:14
-
-
Save ulidtko/35cc082a27c348501e5e8305f2aecd77 to your computer and use it in GitHub Desktop.
TLS1.2 RSA-AES key exchange decryption example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#/usr/bin/env python3 | |
""" | |
An exploration of TLS_RSA_WITH_AES_256_CBC_SHA key exchange in TLS v1.2 | |
-- using https://cryptography.io/ | |
[1]: https://lowleveldesign.org/2016/03/09/manually-decrypting-https-request/ | |
[2]: https://www.acunetix.com/blog/articles/establishing-tls-ssl-connection-part-5/ | |
[RFC5246]: https://tools.ietf.org/html/rfc5246 | |
""" | |
__author__ = "Max <[email protected]>" | |
# pylint: disable=line-too-long | |
from binascii import hexlify, unhexlify | |
from dataclasses import dataclass | |
from itertools import chain, islice | |
from pprint import pprint | |
from cryptography.hazmat.backends import default_backend | |
from cryptography.hazmat.primitives import serialization, hashes, hmac | |
from cryptography.hazmat.primitives.asymmetric import padding | |
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes | |
@dataclass | |
class RsaKexParams: | |
""" RSA key exchange parameters -- extract these from your Wireshark dump """ | |
clientrandom: bytes | |
serverrandom: bytes | |
encrypted_premaster: bytes | |
#-- the first TLS-encrypted record, 64 bytes, "Handshake Protocol: Finished" | |
handshake_finished: bytes | |
# -- -- -- extract from wireshark -- -- -- -- -- -- -- -- -- -- -- -- -- # | |
# TEST1 = RsaKexParams( | |
# clientrandom=unhexlify('e1:52:d4:9c:3c:f3:25:d7:4f:82:0c:9f:0e:30:10:87:77:c0:4d:18:64:e2:c5:62:85:15:1e:60:bf:af:10:1b'.replace(':', '')), | |
# serverrandom=unhexlify('7c:6a:74:59:ca:ac:2d:95:82:9c:4e:a9:31:60:00:d8:12:18:82:73:46:9c:ad:51:44:4f:57:4e:47:52:44:01'.replace(':', '')), | |
# encrypted_premaster=unhexlify('a3a7ba01e79d19010649431e19c0f0933e377656cd0662f4afd1d96470187e5d6536927ed2ff4240e3176d53a38b92cfb7d1cbcc1ba37e255241e2a5edb5f99b97bed355a5af858d413fa391b87b0e0e2457c7af5c612f654c40cd34452e29373126259700cb7429cbcf329c760c0c90d97eccd1fb7b83999a1a8abe008f58f4f224ee8a1a5eba4b2073b028a034ff2c5caba7145d30d88c8231eaf6a036bcfcad388145eac50d3756f681c72467b78cde8e4eae19097034566a8b16d67685a78753db4926c1f1288b8d97f78651d351787fb9caa0a69a36a90bb9f9255e3ddbe73d52012bcf9c6465e6f1d34a226c3cd38e3cb4d8754de2b7152ade36156f3e') | |
# ) | |
#-- modem-kex-wtf.1.pcapng | |
TEST1 = RsaKexParams( | |
clientrandom=unhexlify('5e5fbf3a57ccbd4d5ce3951556d54773039005541d7a64847718cfa52ce46006'), | |
serverrandom=unhexlify('f402e9748ccaf418ad4ee9d34a5921a5237e5d442108b83a444f574e47524401'), | |
encrypted_premaster=unhexlify('54bc7b307a2cdb53474afb85ea1881bb75f5dde51bae37ce8137c8f7730830f6444acce9e3dd1ca3b5c2b968a03ec4583168a37175cb6a3471f26f9b737ceba6840dee2b0fcbe49a1d645d01da3af91caf1e0971e49558794c07fd7411c19bf93576d171ac0e7f7c75dee8e93d738cb3185d595530cb5ab53cad167cfcc2ea82759f73b51d3e824b135c459499c67d5cafe5233f834fcfc2aa67a28036bb5730d2177b35882da1a36943be393afd31d5996b78cda3710847a85f9dd7b455751e0944f2b18dfb04481b59c7908d063bcf7e6aac64c478e41a3cca91abb0439bf28a048b639d066e9edfdcc191bc07292d7c6197ce2bef1425427faa2130c9f0f4'), | |
handshake_finished=unhexlify('b66d95cb60c0631ecdb24cb913afeff4bb00b0d6ef058df298cf5e7b4bda2ce1ceca3a7af9f1182ac4c567de181dc1ff45cc7898a7ad92b9638b0adc31efaa6b') | |
) | |
#-- modem-kex-wtf.2.pcapng | |
TEST2 = RsaKexParams( | |
clientrandom=unhexlify('5e5ff3ce5f2cc1111387165a947cd3a0f5e16386f87483c32b5657e963675e6e'), | |
serverrandom=unhexlify('c5b8197b542240a463ea9deaec2d474142a173a89ad0f0d7444f574e47524401'), | |
encrypted_premaster=unhexlify('2c44c3fec4ea69894a6d10248597ddc9596f02394d45d4591d0fd53d3ebdd220e11ef6145c601b8faa5725ccb7c878dce82749b134242423bb45c0c4336526ad5321f216b7a6e9267f509497e4a2f088cda5d43ff17d55b849878816b4ee74e2b6d5f4700b58b7b66f8f21da06a000b642b2fd5e285a694f25099e523814a8027d0a25d35020b6a04c36bb969259559598674fd9d245cfa99eee312090e0ebc1b4190f6fab74b9c45692fd817ce9788439c3d4aaa49b2e6464849d17e3575c6540de965196b72fb858e414ac5605f6714f7b969ef755f06c32a4b456ab76cd675c9c57e39be21f1402a0478c9d7cfc03d391d2bcebdcd557a1fa66e89963e3db'), | |
handshake_finished=unhexlify('af0757a141108c423243693673d735ac12c9499d5e602d79d54163d19150bdd1ae40f2caa4128846e644d44740b717ef6869b6e3073664d860a80da9f33504a9') | |
) | |
# TEST = RsaKexParams( | |
# clientrandom=unhexlify(''), | |
# serverrandom=unhexlify(''), | |
# encrypted_premaster=unhexlify(''), | |
# handshake_finished=unhexlify('') | |
# ) | |
SERVER_PRIVKEY_PATH = '/tmp/ftpkey.pem' | |
# -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # | |
# a few clarity wrappers for the Y U NO FUNCTIONAL crypto apis. | |
def hmac_sha256(key, bmsg): | |
digest = hmac.HMAC(key, hashes.SHA256(), backend=default_backend()) | |
digest.update(bmsg) | |
return digest.finalize() | |
def hmac_sha1(key, bmsg): | |
digest = hmac.HMAC(key, hashes.SHA1(), backend=default_backend()) | |
digest.update(bmsg) | |
return digest.finalize() | |
def hmac_md5(key, bmsg): | |
digest = hmac.HMAC(key, hashes.MD5(), backend=default_backend()) | |
digest.update(bmsg) | |
return digest.finalize() | |
def aes256cbc_decrypt(key, iv, msg): | |
decryptor = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()).decryptor() | |
return decryptor.update(msg) + decryptor.finalize() | |
# -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # | |
def tls12_prf_sha256(secret: bytes, label: str, seed: bytes, length: int) -> bytes: | |
""" https://tools.ietf.org/html/rfc5246#section-5 """ | |
def p_sha256(secret, seed): | |
a = seed | |
while True: | |
a = hmac_sha256(secret, a) | |
yield hmac_sha256(secret, a + seed) | |
chunk_generator = p_sha256(secret, label.encode('ascii') + seed) | |
return bytes(islice(chain.from_iterable(chunk_generator), length)) | |
def tls10_prf(secret: bytes, label: str, seed: bytes, length: int) -> bytes: | |
""" https://tools.ietf.org/html/rfc2246#section-5 """ | |
def p_md5(secret, seed): | |
a = seed | |
while True: | |
a = hmac_md5(secret, a) | |
yield hmac_md5(secret, a + seed) | |
def p_sha1(secret, seed): | |
a = seed | |
while True: | |
a = hmac_sha1(secret, a) | |
yield hmac_sha1(secret, a + seed) | |
halflen = (len(secret) + 1) // 2 | |
s1, s2 = secret[:halflen], secret[-halflen:] | |
chunks_md5 = p_md5(s1, label.encode('ascii') + seed) | |
chunks_sha1 = p_sha1(s2, label.encode('ascii') + seed) | |
bytes_generator = (x ^ y for x, y in zip(chain.from_iterable(chunks_md5), | |
chain.from_iterable(chunks_sha1))) | |
return bytes(islice(bytes_generator, length)) | |
# -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- # | |
def blog_test(): | |
clientrandom = unhexlify('56 d9 7d 9d be c6 ea 92 23 c1 5f d6 3d fd 22 24 90 13 f2 34 aa 04 ed cc 30 8a f6 c5 09 f8 60 a5'.replace(' ', '')) | |
serverrandom = unhexlify('16 62 fe 6f 2c 14 06 b2 73 a5 e3 76 a6 25 4a 81 86 06 d4 75 9a b7 ef b7 97 a1 e5 59 f4 dd 04 7f'.replace(' ', '')) | |
premaster = unhexlify("03 01 82 BB 54 5E 61 A2 7C D2 B0 BD 59 9E E8 6A 41 9C 63 0C 4B B9 E1 25 DB 9F 9E 08 F4 D4 41 F6 1F 3B 1E C1 3E 88 BA DD 38 AE E3 73 E7 88 FC AB".replace(' ', '')) | |
master_secret_expected = unhexlify("27 90 3D 7D 3F 40 B9 0E 64 84 40 87 A5 8D FF F0 1A 1D D7 6C 96 81 8E F9 62 4F 38 AF 38 60 9E 52 D3 18 0F E8 07 3D 4A 81 EC 28 26 44 22 66 C8 CF".replace(' ', '')) | |
master_secret_computed = tls10_prf(premaster, "master secret", clientrandom + serverrandom, 48) | |
# print("[BLOG TEST] computed master_secret:") | |
# pprint(hexlify(master_secret_computed)) | |
# print("[BLOG TEST] expected master_secret:") | |
# pprint(hexlify(master_secret_expected)) | |
assert master_secret_computed == master_secret_expected | |
def tohex(m: bytes) -> str: | |
return hexlify(m).decode('ascii') | |
def tohex_abbrev(m: bytes, detail: int = 4) -> str: | |
nlead, nlast = detail, detail * 3 // 4 | |
return hexlify(m).decode('ascii') if len(m) <= 32 \ | |
else "{}..{}".format(hexlify(m[:nlead]).decode('ascii'), hexlify(m[-nlast:]).decode('ascii')) | |
def run_single(kex: RsaKexParams): | |
print() | |
print("### Parameters dump ###") | |
print() | |
print(f" - server PEM key path: {SERVER_PRIVKEY_PATH}") | |
print(f" - client random: {kex.clientrandom}") | |
print(f" - server random: {kex.serverrandom}") | |
print() | |
rsa_privkey = serialization.load_pem_private_key(open(SERVER_PRIVKEY_PATH, 'rb').read(), password=None, backend=default_backend()) | |
premaster = rsa_privkey.decrypt(kex.encrypted_premaster, padding.PKCS1v15()) | |
print("=== PreMaster Secret ===") | |
print(f" encrypted: {tohex_abbrev(kex.encrypted_premaster, 8)}") | |
print(f" decrypted: {tohex_abbrev(premaster, 8)}") | |
print() | |
master_secret = tls12_prf_sha256(premaster, "master secret", kex.clientrandom + kex.serverrandom, 48) | |
print("=== PRF-derived master secret [48] ===") | |
print(master_secret) | |
print() | |
derived_material = tls12_prf_sha256(master_secret, "key expansion", kex.serverrandom + kex.clientrandom, 136) | |
client_mac_key = derived_material[0:20] | |
server_mac_key = derived_material[20:40] | |
client_aes_key = derived_material[40:72] | |
server_aes_key = derived_material[72:104] | |
client_aes_iv = derived_material[104:120] | |
server_aes_iv = derived_material[120:136] | |
print("=== PRF-derived expansion parts ===") | |
print(f" [20] client MAC key: {hexlify(client_mac_key)}") | |
print(f" [32] client AES key: {hexlify(client_aes_key)}") | |
print(f" [16] client AES IV : {hexlify(client_aes_iv)}") | |
print(f" [20] server MAC key: {hexlify(server_mac_key)}") | |
print(f" [32] server AES key: {hexlify(server_aes_key)}") | |
print(f" [16] server AES IV : {hexlify(server_aes_iv)}") | |
print() | |
hello_tls = aes256cbc_decrypt(client_aes_key, client_aes_iv, kex.handshake_finished) | |
print("--- Handshake Finished block ---") | |
print(f" encrypted: {tohex_abbrev(kex.handshake_finished, 8)}") | |
print(f" decrypted: {tohex(hello_tls)}") | |
print(" (%d bytes)" % len(hello_tls)) | |
if len(hello_tls) == 64: | |
print(f" ?? {tohex(hello_tls[ 0:16])}") | |
print(f" {tohex(hello_tls[16:32])}") | |
print(f" hmac {tohex(hello_tls[32:52])}") | |
print(f" pad {tohex(hello_tls[52:64])}") | |
print() | |
def main(): | |
blog_test() | |
run_single(TEST1) | |
#-- output: | |
# d1ca1243cbadb256f3a3b95f144ad790 1400000c4ff05a3d6aed00845104a34 bab033f4518b1de38d91a65b828f3132ed6378aef 0b0b0b0b0b0b0b0b0b0b0b0b | |
run_single(TEST2) | |
#-- output: | |
# 68363641725ccdc27639f618a92460bb 1400000cc242cd2d2d2dd91445e2b98 f418033cf277c97110ffb984b17f3e6e74da5318e 0b0b0b0b0b0b0b0b0b0b0b0b | |
if __name__ == "__main__": | |
import sys | |
sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment