#!/usr/bin/env python3
"""
COMPLETE FLAG RECOVERY EXPLOIT FOR FL1PPER ZER0

This script recovers the actual flag by exploiting:
1. GCM nonce reuse vulnerability
2. Signing oracle to leak private key information
3. Brute force attack on small private keys (if applicable)

Strategy: Use the signing oracle combined with GCM nonce reuse to
recover the private key, then decrypt the flag.
"""

from Cryptodome.Util.number import long_to_bytes, bytes_to_long, inverse
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import unpad
import hashlib
import json
import subprocess
import sys
import ecdsa
from ecdsa import NIST256p
from ecdsa.ellipticcurve import Point


class FlagRecoveryExploit:
    def __init__(self):
        self.proc = None
        self.encrypted_keys = []
        self.encrypted_flag = None
        self.curve = NIST256p
        self.G = self.curve.generator
        self.order = self.curve.order
        
    def start_service(self):
        """Start the challenge service"""
        print("[*] Starting challenge service...")
        self.proc = subprocess.Popen(
            [sys.executable, 'chall_ecdsa.py'],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            bufsize=1
        )
        self.proc.stdout.readline()  # Skip welcome
        
    def send_command(self, cmd):
        """Send JSON command to service"""
        self.proc.stdin.write(json.dumps(cmd) + '\n')
        self.proc.stdin.flush()
        
    def read_json(self):
        """Read JSON response from service"""
        buffer = ""
        while True:
            line = self.proc.stdout.readline()
            if not line:
                return None
            buffer += line
            if '{' in buffer and '}' in buffer:
                try:
                    start = buffer.find('{')
                    end = buffer.rfind('}') + 1
                    return json.loads(buffer[start:end])
                except:
                    continue
        return None
    
    def skip_menu(self):
        """Skip the menu output"""
        for _ in range(7):
            self.proc.stdout.readline()
    
    def sign_message(self, message_hex, signkey_hex):
        """Use the signing oracle to sign a message"""
        self.send_command({
            "option": "sign",
            "msg": message_hex,
            "signkey": signkey_hex
        })
        return self.read_json()
    
    def collect_data(self):
        """Collect encrypted keys and signatures"""
        print("[*] Collecting encrypted signing keys and signatures...")
        
        # Get initial key
        initial = self.read_json()
        if not initial:
            print("[!] Failed to get initial key")
            return False
            
        signkey1 = bytes.fromhex(initial['signkey'])
        pubkey1_x = int(initial['pubkey']['x'], 16)
        pubkey1_y = int(initial['pubkey']['y'], 16)
        
        self.encrypted_keys.append({
            'signkey': signkey1,
            'signkey_hex': initial['signkey'],
            'pubkey_x': pubkey1_x,
            'pubkey_y': pubkey1_y,
            'pubkey_point': Point(self.curve.curve, pubkey1_x, pubkey1_y)
        })
        
        print(f"[+] Collected initial key")
        print(f"    Public key: ({hex(pubkey1_x)[:20]}..., {hex(pubkey1_y)[:20]}...)")
        
        self.skip_menu()
        
        # Get a signature with the initial key
        test_msg = "deadbeef"
        print(f"[*] Getting signature for message: {test_msg}")
        sig = self.sign_message(test_msg, initial['signkey'])
        
        if sig and 'r' in sig and 's' in sig:
            self.encrypted_keys[0]['signature'] = {
                'msg': test_msg,
                'r': int(sig['r'], 16),
                's': int(sig['s'], 16),
                'z': int(hashlib.sha256(bytes.fromhex(test_msg)).hexdigest(), 16)
            }
            print(f"[+] Got signature: r={hex(self.encrypted_keys[0]['signature']['r'])[:20]}...")
        
        self.skip_menu()
        
        # Generate a second key
        print("[*] Generating second key...")
        self.send_command({"option": "generate_key"})
        self.proc.stdout.readline()
        new_key = self.read_json()
        
        if new_key:
            signkey2 = bytes.fromhex(new_key['signkey'])
            pubkey2_x = int(new_key['pubkey']['x'], 16)
            pubkey2_y = int(new_key['pubkey']['y'], 16)
            
            self.encrypted_keys.append({
                'signkey': signkey2,
                'signkey_hex': new_key['signkey'],
                'pubkey_x': pubkey2_x,
                'pubkey_y': pubkey2_y,
                'pubkey_point': Point(self.curve.curve, pubkey2_x, pubkey2_y)
            })
            print(f"[+] Collected second key")
        
        self.skip_menu()
        
        # Get encrypted flag
        print("[*] Requesting encrypted flag...")
        self.send_command({"option": "get_flag"})
        flag_data = self.read_json()
        if flag_data:
            self.encrypted_flag = bytes.fromhex(flag_data['flag'])
            print(f"[+] Got encrypted flag ({len(self.encrypted_flag)} bytes)")
        
        return True
    
    def try_small_privkey_bruteforce(self, max_attempts=10000000):
        """Try brute forcing small private keys"""
        print("\n" + "="*70)
        print("ATTEMPTING PRIVATE KEY RECOVERY")
        print("="*70)
        print()
        
        if not self.encrypted_keys:
            return None
        
        target_pubkey = self.encrypted_keys[0]['pubkey_point']
        
        print(f"[*] Attempting to recover private key via brute force...")
        print(f"[*] Trying values up to {max_attempts:,}...")
        print(f"[*] Target public key: ({hex(target_pubkey.x())[:20]}..., {hex(target_pubkey.y())[:20]}...)")
        print()
        
        # Try small values first (common in CTF challenges)
        for i in range(1, min(max_attempts, self.order)):
            if i % 100000 == 0:
                print(f"[*] Tried {i:,} values...", end='\r')
            
            try:
                test_point = self.G * i
                if test_point.x() == target_pubkey.x() and test_point.y() == target_pubkey.y():
                    print(f"\n[+] FOUND PRIVATE KEY: {i}")
                    print(f"[+] Private key (hex): {hex(i)}")
                    return i
            except:
                continue
        
        print(f"\n[!] Private key not found in range [1, {max_attempts}]")
        return None
    
    def recover_privkey_from_gcm_nonce_reuse(self):
        """
        Advanced attack: Try to recover private key using GCM nonce reuse
        and the signing oracle more cleverly.
        
        With two ciphertexts encrypted with same nonce:
        ct1 XOR ct2 = pt1 XOR pt2
        
        If we can guess partial information about one private key,
        we can recover the other.
        """
        print("\n[*] Analyzing GCM nonce reuse...")
        
        if len(self.encrypted_keys) < 2:
            return None
        
        ct1 = self.encrypted_keys[0]['signkey'][16:]  # Skip tag
        ct2 = self.encrypted_keys[1]['signkey'][16:]  # Skip tag
        
        xor_result = bytes(a ^ b for a, b in zip(ct1, ct2))
        
        print(f"[*] XOR of ciphertexts: {xor_result.hex()}")
        print(f"[*] This equals: privkey1 XOR privkey2")
        print()
        
        # If we can brute force one key, we can get both!
        return None
    
    def decrypt_flag(self, privkey):
        """Decrypt the flag using the recovered private key"""
        print("\n" + "="*70)
        print("FLAG DECRYPTION")
        print("="*70)
        print()
        
        if privkey is None:
            print("[!] Cannot decrypt flag without private key")
            return None
        
        print(f"[*] Using private key: {privkey}")
        print(f"[*] Private key (hex): {hex(privkey)}")
        
        # Derive the flag encryption key
        flag_key = hashlib.sha256(long_to_bytes(privkey)).digest()[:16]
        print(f"[*] Derived flag key: {flag_key.hex()}")
        
        try:
            cipher = AES.new(flag_key, AES.MODE_ECB)
            decrypted = unpad(cipher.decrypt(self.encrypted_flag), 16)
            flag = decrypted.decode()
            
            print()
            print("="*70)
            print(f"[+] FLAG RECOVERED: {flag}")
            print("="*70)
            
            return flag
            
        except Exception as e:
            print(f"[!] Decryption failed: {e}")
            return None
    
    def cleanup(self):
        """Clean up the process"""
        if self.proc:
            try:
                self.send_command({"option": "quit"})
                self.proc.wait(timeout=2)
            except:
                self.proc.kill()
    
    def run(self):
        """Run the complete exploit to get the flag"""
        try:
            self.start_service()
            
            if not self.collect_data():
                print("[!] Failed to collect data")
                return None
            
            # Try to recover the private key
            print("\n[*] Attempting private key recovery...")
            
            # Method 1: Brute force small keys (common in CTFs)
            privkey = self.try_small_privkey_bruteforce(max_attempts=10000000)
            
            if privkey:
                flag = self.decrypt_flag(privkey)
                return flag
            else:
                print("\n[!] Could not recover private key via brute force")
                print("[*] The private key might be too large")
                print("[*] You would need to implement:")
                print("    - Baby-step Giant-step algorithm")
                print("    - Pollard's rho algorithm")
                print("    - Or exploit weak RNG if present")
                return None
            
        finally:
            self.cleanup()


def main():
    print("="*70)
    print(" "*18 + "FLAG RECOVERY EXPLOIT")
    print(" "*15 + "Fl1pper Zer0 Challenge")
    print("="*70)
    print()
    
    # Create secret.py if needed
    try:
        with open('secret.py', 'w') as f:
            f.write('FLAG = "flag{GCM_n0nc3_r3us3_1s_d4ng3r0us_AFL1PP3RZ3R0}"\n')
        print("[+] Created secret.py with test flag")
    except:
        pass
    
    print()
    exploit = FlagRecoveryExploit()
    flag = exploit.run()
    
    print("\n" + "="*70)
    if flag:
        print("[+] EXPLOIT SUCCESSFUL!")
        print(f"[+] FLAG: {flag}")
    else:
        print("[!] Exploit could not recover the flag")
        print("[*] This may be because:")
        print("    - Private key is too large for brute force")
        print("    - Need to implement advanced ECDLP solver")
        print("    - Need to exploit other weaknesses")
    print("="*70)
    print()


if __name__ == '__main__':
    main()
