4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / dual_ec_drbg_backdoor_poc.ipynb IPYNB
{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Dual_EC_DRBG Backdoor: A Deep Dive\n",
    "## Educational Proof-of-Concept for Security Professionals\n",
    "\n",
    "---\n",
    "\n",
    "### \u26a0\ufe0f DISCLAIMER\n",
    "\n",
    "**This notebook is for EDUCATIONAL PURPOSES ONLY.**\n",
    "\n",
    "The Dual_EC_DRBG backdoor is a well-documented cryptographic vulnerability (CVE-2014-8610) that was publicly disclosed following the Snowden revelations.\n",
    "\n",
    "---\n",
    "\n",
    "## Table of Contents\n",
    "\n",
    "1. Historical Context\n",
    "2. Mathematical Foundations\n",
    "3. NIST P-256 Curve Parameters\n",
    "4. Dual_EC_DRBG Algorithm\n",
    "5. Implementation: Honest vs Backdoored\n",
    "6. The Attack: State Recovery\n",
    "7. Live Demonstration\n",
    "8. Mitigations and Lessons\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 1. Historical Context: The NSA Backdoor That Shook Cryptography\n",
    "\n",
    "### Timeline of Events\n",
    "\n",
    "| Year | Event | Significance |\n",
    "|------|-------|--------------|\n",
    "| **1997** | NIST initiates AES competition | Beginning of modern crypto standardization |\n",
    "| **2004** | NSA pushes Dual_EC_DRBG into NIST SP 800-90 | The backdoor is planted |\n",
    "| **2006** | NIST SP 800-90 published | Dual_EC_DRBG becomes a standard |\n",
    "| **2007** | Shumow-Ferguson presentation at CRYPTO | First public warning of potential backdoor |\n",
    "| **2013** | Snowden revelations (June) | Documents confirm NSA paid RSA $10M to use Dual_EC |\n",
    "| **2014** | NIST withdraws Dual_EC_DRBG | Official deprecation |\n",
    "| **2015** | Juniper backdoor discovered | Attackers exploited weak Dual_EC implementation |\n",
    "\n",
    "### The Core Problem\n",
    "\n",
    "Dual_EC_DRBG uses elliptic curve points P and Q where **Q = d\u00b7P** for some secret **d**.\n",
    "\n",
    "If you know **d**, you can predict all future outputs after observing just **32 bytes** of output.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 2. Mathematical Foundations\n",
    "\n",
    "### 2.1 Elliptic Curves Basics\n",
    "\n",
    "$$y^2 = x^3 + ax + b \\pmod{p}$$\n",
    "\n",
    "### 2.2 The Backdoor Mechanism\n",
    "\n",
    "```\n",
    "State update: s_{i+1} = phi(s_i * P)\n",
    "Output:       r_i = phi(s_i * Q)  [truncated]\n",
    "```\n",
    "\n",
    "Where Q = d*P (the backdoor relationship)\n",
    "\n",
    "**The attack:**\n",
    "1. Observe r_i (30+ bytes of output)\n",
    "2. Find candidate R where x(R) \u2248 r_i\n",
    "3. Compute: s_{i+1} = phi(d^(-1) * R) = phi(s_i * P)\n",
    "\n",
    "**32 bytes \u2192 complete state compromise \u2192 ALL future output predictable**\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Setup: Import required libraries\n",
    "from ecdsa import NIST256p, ellipticcurve\n",
    "from ecdsa.ellipticcurve import Point\n",
    "import hashlib\n",
    "import os\n",
    "import binascii\n",
    "\n",
    "print('[=] Libraries loaded successfully')\n",
    "print('[+] ecdsa library provides NIST P-256 curve support')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 3. NIST P-256 Curve Parameters\n",
    "\n",
    "These are the official NIST parameters from FIPS 186-4.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# NIST P-256 Curve Parameters\n",
    "# Source: FIPS 186-4, Section D.1.2.3\n",
    "\n",
    "curve = NIST256p\n",
    "\n",
    "# Prime field\n",
    "p = curve.curve.p()\n",
    "\n",
    "# Curve coefficients: y^2 = x^3 - 3x + b\n",
    "a = curve.curve.a()\n",
    "b = curve.curve.b()\n",
    "\n",
    "# Generator point G\n",
    "G = curve.generator\n",
    "\n",
    "# Order of the curve (number of points)\n",
    "n = curve.order\n",
    "\n",
    "# Cofactor (h = 1 for NIST P-256, prime-order curve)\n",
    "h = 1\n",
    "\n",
    "print('[=] NIST P-256 Curve Parameters')\n",
    "print('=' * 60)\n",
    "print(f'Prime p = {hex(p)}')\n",
    "print(f'\\nCurve: y^2 = x^3 + {a}x + {hex(b)}')\n",
    "print(f'\\nGenerator G =\\n  x: {hex(G.x())}\\n  y: {hex(G.y())}')\n",
    "print(f'\\nOrder n = {hex(n)}')\n",
    "print(f'Cofactor h = {h} (always 1 for prime-order curves)')\n",
    "print(f'\\n[+] Curve bit length: {p.bit_length()} bits')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 4. Dual_EC_DRBG Algorithm\n",
    "\n",
    "### Algorithm Specification (NIST SP 800-90)\n",
    "\n",
    "**State Update:**  s_{i+1} = phi(s_i * P)\n",
    "\n",
    "**Output:** r_i = truncate(phi(s_i * Q))\n",
    "\n",
    "**Why insecure with backdoor:** If Q = d\u00b7P, knowing d allows state recovery\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 5. Implementation: Honest vs Backdoored\n",
    "\n",
    "1. **Honest**: Q is randomly chosen (no known discrete log)\n",
    "2. **Backdoored**: Q = d\u00b7P for secret d (NSA scenario)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class DualECDRBG:\n",
    "    \"\"\"Dual_EC_DRBG Implementation using NIST P-256\"\"\"\n",
    "\n",
    "    OUTPUT_BYTES = 30  # Keep 240 bits of 256\n",
    "\n",
    "    def __init__(self, seed: bytes, P: Point, Q: Point, secret_d: int = None):\n",
    "        self.curve = P.curve()\n",
    "        self.P = P\n",
    "        self.Q = Q\n",
    "        self.secret_d = secret_d\n",
    "        self.backdoored = secret_d is not None\n",
    "\n",
    "        # Hash seed to get initial state\n",
    "        seed_hash = hashlib.sha256(seed).digest()\n",
    "        self.state = int.from_bytes(seed_hash, 'big') % n\n",
    "        self.reseed_counter = 0\n",
    "\n",
    "        print(f'[+] DRBG initialized')\n",
    "        print(f'    Mode: {\"BACKDOORED\" if self.backdoored else \"HONEST\"}')\n",
    "        print(f'    P = ({hex(self.P.x())[:20]}..., {hex(self.P.y())[:20]}...)')\n",
    "        print(f'    Q = ({hex(self.Q.x())[:20]}..., {hex(self.Q.y())[:20]}...)')\n",
    "        if self.backdoored:\n",
    "            print(f'    Secret d = {hex(secret_d)[:30]}...')\n",
    "\n",
    "    def _phi(self, point: Point) -> int:\n",
    "        return point.x() % p\n",
    "\n",
    "    def _update_state(self):\n",
    "        point = self.P * self.state\n",
    "        self.state = self._phi(point) % n\n",
    "        self.reseed_counter += 1\n",
    "\n",
    "    def _generate_block(self) -> bytes:\n",
    "        point = self.Q * self.state\n",
    "        x = self._phi(point)\n",
    "        x_bytes = x.to_bytes(32, 'big')\n",
    "        truncated = x_bytes[:self.OUTPUT_BYTES]\n",
    "        self._update_state()\n",
    "        return truncated\n",
    "\n",
    "    def generate(self, num_bytes: int) -> bytes:\n",
    "        output = b''\n",
    "        while len(output) < num_bytes:\n",
    "            output += self._generate_block()\n",
    "        return output[:num_bytes]\n",
    "\n",
    "    def get_state_info(self) -> dict:\n",
    "        return {'state': hex(self.state), 'reseed_counter': self.reseed_counter}\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def create_honest_drbg(seed: bytes) -> DualECDRBG:\n",
    "    \"\"\"Create honest DRBG - Q is random, no discrete log known\"\"\"\n",
    "    P = G\n",
    "    random_scalar = int.from_bytes(os.urandom(32), 'big') % n\n",
    "    Q = P * random_scalar\n",
    "    print('\\n[=== HONEST Dual_EC_DRBG ===]')\n",
    "    return DualECDRBG(seed, P, Q, secret_d=None)\n",
    "\n",
    "def create_backdoored_drbg(seed: bytes, d: int = None) -> DualECDRBG:\n",
    "    \"\"\"Create backdoored DRBG - Q = d*P where d is secret\"\"\"\n",
    "    P = G\n",
    "    if d is None:\n",
    "        d = int.from_bytes(os.urandom(32), 'big') % n\n",
    "    Q = P * d  # The backdoor relationship\n",
    "    print('\\n[=== BACKDOORED Dual_EC_DRBG ===]')\n",
    "    return DualECDRBG(seed, P, Q, secret_d=d)\n",
    "\n",
    "# Test\n",
    "test_seed = b'test_seed_for_demonstration'\n",
    "print('Initializing both DRBG variants...')\n",
    "honest_drbg = create_honest_drbg(test_seed)\n",
    "backdoored_drbg = create_backdoored_drbg(test_seed)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 6. The Attack: State Recovery\n",
    "\n",
    "### The Attack Algorithm\n",
    "\n",
    "Given: Observed output r (30 bytes), secret d where Q = d\u00b7P\n",
    "\n",
    "Goal: Recover state s to predict all future output\n",
    "\n",
    "**Step 1:** Reconstruct candidate points R where x(R) \u2248 r\n",
    "\n",
    "**Step 2:** Compute s\u00b7P = d^(-1) \u00b7 R\n",
    "\n",
    "**Step 3:** The x-coordinate of s\u00b7P is the next state\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class DualECAttacker:\n",
    "    \"\"\"Implements the Dual_EC_DRBG backdoor attack\"\"\"\n",
    "\n",
    "    def __init__(self, P: Point, Q: Point, secret_d: int):\n",
    "        self.P = P\n",
    "        self.Q = Q\n",
    "        self.d = secret_d\n",
    "        self.d_inv = pow(secret_d, -1, n)  # Modular inverse\n",
    "        self.curve = P.curve()\n",
    "        print(f'[+] Attacker initialized with secret d')\n",
    "        print(f'    d^(-1) mod n = {hex(self.d_inv)[:30]}...')\n",
    "\n",
    "    def _recover_candidate_points(self, observed_output: bytes) -> list:\n",
    "        candidates = []\n",
    "        x_bytes = observed_output + b'\\x00\\x00'\n",
    "        x = int.from_bytes(x_bytes, 'big')\n",
    "\n",
    "        for x_candidate in [x, x + 1]:\n",
    "            rhs = (pow(x_candidate, 3, p) + a * x_candidate + b) % p\n",
    "            # Check if quadratic residue using Euler's criterion\n",
    "            if pow(rhs, (p - 1) // 2, p) == 1:\n",
    "                y = pow(rhs, (p + 1) // 4, p)  # Square root\n",
    "                candidates.append(Point(self.curve, x_candidate, y))\n",
    "                if y != 0:\n",
    "                    candidates.append(Point(self.curve, x_candidate, (-y) % p))\n",
    "        return candidates\n",
    "\n",
    "    def attack(self, observed_output: bytes, verification_output: bytes = None) -> dict:\n",
    "        print(f'\\n[=== ATTACK EXECUTION ===]')\n",
    "        print(f'[+] Observed output: {binascii.hexlify(observed_output[:16]).decode()}...')\n",
    "\n",
    "        candidates = self._recover_candidate_points(observed_output)\n",
    "        print(f'[+] Found {len(candidates)} candidate points on curve')\n",
    "\n",
    "        for i, R in enumerate(candidates):\n",
    "            print(f'\\n[+] Testing candidate {i+1}/{len(candidates)}')\n",
    "            print(f'    R = ({hex(R.x())[:30]}..., {hex(R.y())[:30]}...)')\n",
    "\n",
    "            # R = s * Q = s * d * P\n",
    "            # Therefore: s * P = d^(-1) * R\n",
    "            s_times_P = R * self.d_inv\n",
    "            recovered_next_state = s_times_P.x() % n\n",
    "\n",
    "            print(f'    Recovered next state: {hex(recovered_next_state)[:40]}...')\n",
    "\n",
    "            if verification_output:\n",
    "                test_point = self.Q * recovered_next_state\n",
    "                test_x = test_point.x()\n",
    "                test_output = test_x.to_bytes(32, 'big')[:30]\n",
    "\n",
    "                if test_output[:len(verification_output)] == verification_output:\n",
    "                    print(f'    [OK] VERIFIED: Correct state recovered!')\n",
    "                    return {'success': True, 'next_state': recovered_next_state, 'candidate_index': i}\n",
    "                else:\n",
    "                    print(f'    [X] Output mismatch, trying next candidate')\n",
    "\n",
    "        if candidates and not verification_output:\n",
    "            R = candidates[0]\n",
    "            s_times_P = R * self.d_inv\n",
    "            recovered_next_state = s_times_P.x() % n\n",
    "            return {'success': True, 'next_state': recovered_next_state,\n",
    "                    'candidate_index': 0, 'note': 'No verification performed'}\n",
    "\n",
    "        return {'success': False, 'error': 'Could not recover state'}\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def predict_output(self, state: int, num_bytes: int) -> bytes:\n",
    "    \"\"\"Given a state, predict all future output\"\"\"\n",
    "    output = b''\n",
    "    current_state = state\n",
    "\n",
    "    while len(output) < num_bytes:\n",
    "        point = self.Q * current_state\n",
    "        x = point.x()\n",
    "        output += x.to_bytes(32, 'big')[:30]\n",
    "        \n",
    "        # Update state\n",
    "        point = self.P * current_state\n",
    "        current_state = point.x() % n\n",
    "\n",
    "    return output[:num_bytes]\n",
    "\n",
    "# Attach method to class\n",
    "DualECAttacker.predict_output = predict_output\n",
    "print('[+] Attacker class complete with predict_output method')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 7. Live Demonstration\n",
    "\n",
    "### Scenario: Simulating the Juniper Networks Attack\n",
    "\n",
    "In December 2015, Juniper Networks disclosed attackers had:\n",
    "1. Modified ScreenOS to use attacker-controlled Dual_EC parameters\n",
    "2. Changed Q point to one where they knew the discrete log\n",
    "3. Could decrypt all VPN traffic\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# === SCENARIO SETUP ===\n",
    "\n",
    "print('=' * 70)\n",
    "print('SCENARIO: Juniper Networks-style Attack Simulation')\n",
    "print('=' * 70)\n",
    "\n",
    "# Step 1: Attacker generates secret backdoor key\n",
    "print('\\n[PHASE 1: Attacker creates the backdoor]')\n",
    "attacker_secret_d = int.from_bytes(b'NSA_BACKDOOR_KEY_001', 'big') % n\n",
    "print(f'[*] Attacker generates secret d = {hex(attacker_secret_d)[:40]}...')\n",
    "\n",
    "# Step 2: Create the backdoored DRBG\n",
    "victim_seed = os.urandom(32)  # Unknown to attacker\n",
    "print(f'[*] Victim seed (unknown): {binascii.hexlify(victim_seed[:8]).decode()}...')\n",
    "\n",
    "victim_drbg = create_backdoored_drbg(victim_seed, attacker_secret_d)\n",
    "\n",
    "# Victim generates key material\n",
    "print('\\n[PHASE 2: Victim generates random material]')\n",
    "victim_key = victim_drbg.generate(64)\n",
    "print(f'[*] Key material: {binascii.hexlify(victim_key[:16]).decode()}...')\n",
    "victim_nonce = victim_drbg.generate(16)\n",
    "print(f'[*] Nonce: {binascii.hexlify(victim_nonce).decode()}')\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Step 3: Attacker intercepts traffic\n",
    "print('\\n[PHASE 3: Attacker intercepts traffic]')\n",
    "print('[*] Attacker sees 32 bytes of output in TLS handshake')\n",
    "\n",
    "victim_drbg_for_attack = create_backdoored_drbg(victim_seed, attacker_secret_d)\n",
    "\n",
    "observed_output = victim_drbg_for_attack.generate(32)\n",
    "print(f'[*] Intercepted: {binascii.hexlify(observed_output).decode()}')\n",
    "\n",
    "future_output = victim_drbg_for_attack.generate(64)\n",
    "print(f'[*] (Hidden) Future output: {binascii.hexlify(future_output[:16]).decode()}...')\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Step 4: Attacker executes the attack\n",
    "print('\\n[PHASE 4: Attacker executes state recovery]')\n",
    "\n",
    "attacker = DualECAttacker(victim_drbg.P, victim_drbg.Q, attacker_secret_d)\n",
    "\n",
    "verification_data = victim_drbg_for_attack.generate(30)\n",
    "result = attacker.attack(observed_output[:30], verification_data)\n",
    "\n",
    "if result['success']:\n",
    "    print(f'\\n[!] STATE RECOVERY SUCCESSFUL')\n",
    "    print(f'    Recovered state: {hex(result[\"next_state\"])[:50]}...')\n",
    "else:\n",
    "    print('[X] Attack failed')\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Step 5: Predict all future output\n",
    "print('\\n[PHASE 5: Attacker predicts all future output]')\n",
    "\n",
    "if result['success']:\n",
    "    predicted = attacker.predict_output(result['next_state'], 128)\n",
    "    print(f'[*] Predicted: {binascii.hexlify(predicted[:32]).decode()}...')\n",
    "\n",
    "    # Verify\n",
    "    print('\\n[VERIFICATION]')\n",
    "    victim_verify = create_backdoored_drbg(victim_seed, attacker_secret_d)\n",
    "    _ = victim_verify.generate(32)  # Skip first block\n",
    "\n",
    "    actual = victim_verify.generate(128)\n",
    "    print(f'[*] Actual:    {binascii.hexlify(actual[:32]).decode()}...')\n",
    "    print(f'[*] Predicted: {binascii.hexlify(predicted[:32]).decode()}...')\n",
    "\n",
    "    if predicted == actual:\n",
    "        print('\\n[OK] PERFECT MATCH - All future output predicted!')\n",
    "        print('[OK] Attacker can decrypt all traffic!')\n",
    "    else:\n",
    "        print('\\n[X] Mismatch')\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Compare Honest vs Backdoored\n",
    "print('\\n[=== COMPARISON: Honest vs Backdoored ===]\\n')\n",
    "\n",
    "print('Testing HONEST DRBG:')\n",
    "honest_seed = os.urandom(32)\n",
    "honest = create_honest_drbg(honest_seed)\n",
    "honest_output = honest.generate(64)\n",
    "print(f'[*] Output: {binascii.hexlify(honest_output[:16]).decode()}...')\n",
    "print('[*] Without secret relationship, state recovery is hard\\n')\n",
    "\n",
    "print('Testing BACKDOORED DRBG:')\n",
    "backdoor_seed = os.urandom(32)\n",
    "backdoor_d = int.from_bytes(os.urandom(32), 'big') % n\n",
    "backdoored = create_backdoored_drbg(backdoor_seed, backdoor_d)\n",
    "backdoored_output = backdoored.generate(64)\n",
    "print(f'[*] Output: {binascii.hexlify(backdoored_output[:16]).decode()}...')\n",
    "print('[*] With secret d, state recovery is instant\\n')\n",
    "\n",
    "print('[SUMMARY]')\n",
    "print('- Both produce statistically random output')\n",
    "print('- Only backdoored has a master key')\n",
    "print('- Backdoor is undetectable without d')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## 8. Mitigations and Lessons\n",
    "\n",
    "### Never Use\n",
    "- \u274c Dual_EC_DRBG (deprecated 2014)\n",
    "\n",
    "### Use Instead\n",
    "- \u2705 `/dev/urandom` or `getrandom()` (Linux)\n",
    "- \u2705 `BCryptGenRandom()` (Windows)\n",
    "- \u2705 Hash_DRBG, HMAC_DRBG, CTR_DRBG (NIST SP 800-90A Rev 1)\n",
    "\n",
    "### Historical Lessons\n",
    "\n",
    "| Lesson | Application |\n",
    "|--------|-------------|\n",
    "| Backdoors survive for years | Dual_EC standard for 7+ years |\n",
    "| Financial incentives matter | RSA took $10M for weak default |\n",
    "| Open review is essential | Shumow-Ferguson identified issue |\n",
    "\n",
    "**Takeaway**: Cryptographic backdoors let attackers in without leaving a trace.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.12.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}