#!/usr/bin/env python3 """Analyze PDF files for malicious content. Parses PDF structure to identify JavaScript, embedded files, launch actions, OpenAction triggers, and other suspicious elements. """ from __future__ import annotations import argparse import hashlib import json import re import sys import zlib from datetime import datetime from pathlib import Path # Suspicious PDF keywords SUSPICIOUS_KEYWORDS = { "high_risk": [ "/JavaScript", "/JS", "/Launch", "/EmbeddedFile", "/OpenAction", "/AA", "/AcroForm", "/XFA", "/RichMedia", "/ObjStm", ], "medium_risk": [ "/URI", "/SubmitForm", "/GoToR", "/GoToE", "/ImportData", "/Action", ], "indicators": [ "/Encrypt", "/JBIG2Decode", "/Colors > 2^24", ], } # JavaScript exploit patterns JS_EXPLOIT_PATTERNS = [ (r"unescape\s*\(", "JavaScript unescape (potential shellcode)"), (r"eval\s*\(", "JavaScript eval (code execution)"), (r"String\.fromCharCode", "Character code conversion"), (r"spray|nop|sled", "Heap spray indicators"), (r"\\u[0-9a-fA-F]{4}", "Unicode escape sequences"), (r"0x[0-9a-fA-F]{8,}", "Long hex values (shellcode)"), (r"app\.doc|app\.alert|util\.printf", "Acrobat API calls"), (r"this\.getField|this\.submitForm", "Form manipulation"), ] def compute_hash(data) -> dict: return hashlib.sha256(data).hexdigest() def extract_pdf_objects(data) -> dict: """Extract PDF objects and their content.""" objects = [] # Find object definitions: N N obj ... endobj obj_pattern = re.compile( rb'(\d+)\s+(\d+)\s+obj\b(.*?)endobj', re.DOTALL ) for match in obj_pattern.finditer(data): obj_num = int(match.group(1)) obj_gen = int(match.group(2)) obj_content = match.group(3) objects.append({ "number": obj_num, "generation": obj_gen, "content": obj_content, "size": len(obj_content), }) return objects def check_keywords(data) -> list: """Check for suspicious PDF keywords.""" findings = [] data_str = data.decode("latin-1", errors="ignore") for severity, keywords in SUSPICIOUS_KEYWORDS.items(): for keyword in keywords: count = data_str.count(keyword) if count > 0: findings.append({ "keyword": keyword, "count": count, "severity": severity, }) return findings def extract_javascript(data) -> dict: """Extract JavaScript from PDF streams.""" js_content = [] # Look for JavaScript in streams js_refs = [m.start() for m in re.finditer(rb'/JavaScript|/JS\s', data)] for ref_pos in js_refs: # Try to find associated stream content stream_match = re.search( rb'stream\r?\n(.*?)\r?\nendstream', data[max(0, ref_pos - 1000):ref_pos + 5000], re.DOTALL ) if stream_match: stream_data = stream_match.group(1) # Try to decompress try: decompressed = zlib.decompress(stream_data) js_content.append(decompressed.decode("utf-8", errors="ignore")) except zlib.error: js_content.append(stream_data.decode("utf-8", errors="ignore")) return js_content def analyze_javascript(js_code) -> list: """Analyze extracted JavaScript for exploit indicators.""" findings = [] for pattern, description in JS_EXPLOIT_PATTERNS: matches = re.findall(pattern, js_code, re.IGNORECASE) if matches: findings.append({ "pattern": description, "match_count": len(matches), "severity": "high", }) return findings def extract_urls(data) -> dict: """Extract URLs from PDF content.""" urls = set() data_str = data.decode("latin-1", errors="ignore") # URI objects uri_matches = re.findall(r'/URI\s*\((https?://[^)]+)\)', data_str) urls.update(uri_matches) # General URL patterns url_matches = re.findall( r'https?://[a-zA-Z0-9\-._~:/?#\[\]@!$&\'()*+,;=%]+', data_str ) urls.update(url_matches) return list(urls) def analyze_pdf(filepath) -> dict: """Main PDF analysis function.""" path = Path(filepath) if not path.exists(): print(f"[!] File not found: {filepath}", file=sys.stderr) sys.exit(1) data = path.read_bytes() # Verify PDF header if not data[:5] in (b"%PDF-", b"\xef\xbb\xbf%P"): print("[!] Warning: File does not start with PDF header", file=sys.stderr) result = { "file": str(filepath), "file_size": len(data), "sha256": compute_hash(data), "timestamp": datetime.now().isoformat(), } # Extract PDF version version_match = re.match(rb'%PDF-(\d+\.\d+)', data[:20]) result["pdf_version"] = version_match.group(1).decode() if version_match else "unknown" # Extract objects objects = extract_pdf_objects(data) result["object_count"] = len(objects) # Check keywords keyword_findings = check_keywords(data) result["suspicious_keywords"] = keyword_findings # Extract JavaScript js_content = extract_javascript(data) result["javascript_found"] = len(js_content) > 0 result["javascript_count"] = len(js_content) js_analysis = [] for js in js_content: js_analysis.extend(analyze_javascript(js)) result["javascript_analysis"] = js_analysis # Extract URLs urls = extract_urls(data) result["urls"] = urls[:50] # Check for embedded files embedded_count = data.count(b"/EmbeddedFile") result["embedded_files"] = embedded_count # Check for encryption result["encrypted"] = b"/Encrypt" in data # Risk assessment risk_score = 0 risk_reasons = [] high_risk_keywords = [k for k in keyword_findings if k["severity"] == "high_risk"] if high_risk_keywords: risk_score += 20 * len(high_risk_keywords) risk_reasons.extend([f"Keyword: {k['keyword']}" for k in high_risk_keywords]) if js_content: risk_score += 25 risk_reasons.append(f"JavaScript present ({len(js_content)} instances)") if js_analysis: risk_score += 30 risk_reasons.extend([f"JS exploit pattern: {a['pattern']}" for a in js_analysis]) if embedded_count: risk_score += 15 risk_reasons.append(f"Embedded files: {embedded_count}") result["risk_assessment"] = { "score": min(risk_score, 100), "level": "critical" if risk_score >= 60 else ("high" if risk_score >= 40 else ("medium" if risk_score >= 20 else "low")), "reasons": risk_reasons, } return result def main() -> None: parser = argparse.ArgumentParser(description="Analyze PDF files for malicious content") parser.add_argument("--input", "--file", help="PDF file to analyze") parser.add_argument("--output", "-o", help="Output file (JSON)") parser.add_argument("--format", choices=["json", "text"], default="text") args = parser.parse_args() result = analyze_pdf(args.file) if args.format == "json": output = json.dumps(result, indent=2) else: output = f"=== PDF Analysis ===\n" output += f"File: {result['file']}\n" output += f"Version: PDF {result['pdf_version']}\n" output += f"SHA256: {result['sha256']}\n" output += f"Objects: {result['object_count']} | Embedded: {result['embedded_files']}\n" output += f"JavaScript: {'YES' if result['javascript_found'] else 'No'}\n" output += f"Encrypted: {'YES' if result['encrypted'] else 'No'}\n\n" risk = result["risk_assessment"] output += f"Risk: {risk['level'].upper()} (score: {risk['score']}/100)\n" for reason in risk["reasons"]: output += f" - {reason}\n" if result["urls"]: output += f"\nURLs ({len(result['urls'])}):\n" for url in result["urls"][:10]: output += f" {url}\n" if args.output: Path(args.output).write_text(output) print(f"[+] Report saved to {args.output}") else: print(output) if __name__ == "__main__": main()