社会工程学攻击取证深度分析

0x01 社会工程学攻击概述与取证框架

社会工程学攻击分类体系

社会工程学(Social Engineering)是利用人类心理弱点、认知偏差和行为习惯,通过欺骗、操纵等手段获取敏感信息、访问权限或执行特定动作的攻击方式。与传统技术攻击不同,社会工程学攻击的核心目标是而非系统。

攻击类型分层矩阵:

攻击类型投递渠道目标角色技术复杂度成功概率典型损失
大规模钓鱼(Phishing)邮件/SMS普通员工低(0.1-2%)单账号凭证
鱼叉式钓鱼(Spear Phishing)邮件特定人员中(5-15%)内网入口
鲸钓(Whaling)邮件/电话C-level高管中(10-20%)巨额资金
BEC 商务邮件欺诈邮件财务/采购高(20-40%)平均$150K+
Vishing 语音钓鱼电话/VoIPIT支持/HR中(15-30%)凭据/权限
Smishing 短信钓鱼SMS/iMessage手机用户低(3-5%)恶意App安装
物理社工(Physical SE)现场入侵安保/前台中高(30-50%)物理接入/设备
水坑攻击(Watering Hole)Web站点特定组织成员很高低(1-3%)供应链入口

现代攻击链中的社会工程学定位:

在 APT 攻击全生命周期中,社会工程学通常出现在初始突破阶段(Initial Access),是最常见的边界突破手段。根据 Verizon DBIR 2024 报告,约 68% 的数据泄露涉及人为交互环节,其中钓鱼邮件占据主导。攻击者选择社工路径的根本原因在于:相比零日漏洞利用,人的弱点更稳定、更可预测、且防御成本更高。

MITRE ATT&CK 社会工程学技术映射:

MITRE_ATT&CK_Social_Engineering_Mapping:
  Initial_Access:
    - T1566.001: Spearphishing Attachment(鱼叉式钓鱼附件)
    - T1566.002: Spearphishing Link(鱼叉式钓鱼链接)
    - T1566.003: Spearphishing via Service(通过第三方服务钓鱼)
    - T1566.004: Spearphishing Voice Call(鱼叉式语音通话)
  Credential_Access:
    - T1598.001: Phishing for Credential - Spearphishing Service
    - T1598.002: Spearphishing Attachment (Credential Harvesting)
    - T1598.003: Spearphishing Link (Credential Harvesting)
  Collection:
    - T1534: Internal Spearphishing(内部鱼叉式钓鱼)
  Reconnaissance:
    - T1592: Gather Victim Host Information
    - T1593: Search Open Websites/Databases
    - T1594: Search Victim-Owned Websites
    - T1595: Active Scanning
    - T1596: Search Open Technical Databases
    - T1597: Search Closed Sources
  Resource_Development:
    - T1583.006: Acquire Infrastructure - Web Services
    - T1586: Compromise Accounts
    - T1586.002: Email Accounts
    - T1650: Acquire Access

取证挑战与方法论

社会工程学攻击取证面临三大核心挑战:

证据分散性:一封钓鱼邮件可能涉及的证据分布于邮件网关日志、DNS 记录、WHOIS 历史、CDN 缓存、终端浏览器历史、AD 认证日志、代理服务器日志、防火墙流量等多个独立系统中,这些系统往往属于不同管理部门,证据采集需要跨团队协调。

跨平台性:攻击链可能横跨邮件系统(Exchange/O365/Gmail)、即时通讯(Teams/Slack/WeChat)、电话系统(VoIP/PSTN)、物理门禁、银行系统等多个异构平台,每个平台的日志格式、保留周期、提取方式各不相同。

法律边界:OSINT 调查必须在法律允许范围内进行。对攻击者基础设施的主动探测(端口扫描、漏洞利用)可能触犯计算机犯罪相关法律;跨境调查涉及数据隐私法规(GDPR/CCPA/个保法)冲突;暗网监控需要注意蜜罐合法性问题。

取证方法论四步框架:

FORENSIC_FRAMEWORK = {
    "Phase_1_Timeline_Reconstruction": {
        "goal": "重建完整攻击时间线",
        "steps": [
            "收集所有相关日志源(邮件头/服务器日志/终端日志/网络流量)",
            "统一时间戳为UTC并标注时区偏移",
            "按时间排序构建事件序列",
            "识别时间窗口内的异常行为节点"
        ]
    },
    "Phase_2_Infrastructure_Correlation": {
        "goal": "关联攻击基础设施组件",
        "steps": [
            "提取所有可疑域名/IP/URL",
            "WHOIS + DNS + SSL证书交叉查询",
            "被动DNS数据库验证历史解析关系",
            "构建基础设施拓扑图"
        ]
    },
    "Phase_3_Psychological_Profiling": {
        "goal": "构建攻击者心理画像",
        "steps": [
            "分析话术模板语言特征",
            "识别文化背景暗示(节日/时区/语言习惯)",
            "评估攻击定制化程度",
            "推断攻击者组织能力层级"
        ]
    },
    "Phase_4_Evidence_Gradation": {
        "goal": "证据强度分层评估",
        "levels": [
            "确认恶意(Confirmed Malicious): 可直接证明恶意意图的证据",
            "高度可疑(Highly Suspicious): 强烈暗示恶意但需佐证的证据",
            "需要关注(Requires Attention): 存在异常但无法直接归因的指标"
        ]
    }
}

0x02 钓鱼邮件基础设施溯源

邮件头深度分析

邮件头是钓鱼邮件事后取证的第一个切入点,包含完整的邮件路由信息、认证结果和内容元数据。

完整路由追踪方法:

以一封典型的鱼叉式钓鱼邮件为例,从 Received 头部逐层逆序解析:

完整邮件头示例分析:

Return-Path: <no-reply@micros0ft-security.com>
Delivered-To: target@company.com
Received: from mail.company.com (10.0.1.25) by exchange.company.com
    with SMTP; Mon, 15 Jan 2026 08:23:41 +0800
Received: from mx-relay.external.net (203.0.113.50) by mail.company.com
    with ESMTPS; Mon, 15 Jan 2026 08:23:39 +0800
Received: from [198.51.100.23] (helo=sender.campaign-server.io)
    by mx-relay.external.net with SMTP; Mon, 15 Jan 2026 00:23:35 +0000
From: "Microsoft Security Team" <no-reply@micros0ft-security.com>
Reply-To: security-alerts@ms-support-verify.com
Subject: Urgent: Your Account Requires Immediate Verification
X-Mailer: PHPMailer v6.8.0
X-Originating-IP: [198.51.100.23]

关键取证要点:

  • 最顶部的 Received 是最接近收件方的跳,最底部是原始发送方
  • HELO/EHLO hostname 与实际 IP 不匹配是重要红旗指标
  • Reply-ToFrom 地址不一致是高概率钓鱼特征
  • X-Originating-IP 可能泄露真实发送 IP(但常被清除或伪造)
  • X-Mailer 字段可关联已知攻击工具指纹

延迟分析技术:

每跳之间的时间差可以揭示中继服务器的地理位置分布和转发模式:

import email
from datetime import datetime
import re

def parse_email_headers(raw_headers):
    msg = email.message_from_string(raw_headers)
    received_headers = msg.get_all('Received')
    
    hops = []
    for header in reversed(received_headers):
        timestamp_match = re.search(r';\s*(.+)$', header)
        if timestamp_match:
            time_str = timestamp_match.group(1).strip()
            try:
                parsed_time = email.utils.parsedate_to_datetime(time_str)
                hops.append({
                    'hop_raw': header.split(';')[0].strip(),
                    'timestamp': parsed_time,
                    'timezone': parsed_time.tzinfo
                })
            except Exception:
                continue
    
    analysis = []
    for i in range(1, len(hops)):
        delay = (hops[i]['timestamp'] - hops[i-1]['timestamp']).total_seconds()
        analysis.append({
            'from_hop': i-1,
            'to_hop': i,
            'delay_seconds': delay,
            'anomaly': True if abs(delay) > 30 or delay < 0 else False
        })
    
    return analysis

X-Header 分析清单:

X-Header取证价值常见伪造方式
X-Originating-IP原始发送IP可能被中间件删除或修改
X-Mailer发件客户端软件攻击工具签名识别
X-Spam-Status反垃圾引擎判定绕过SPF后可能标记clean
X-SES-OutgoingAWS SES发送标识确认云服务滥用
X-Microsoft-AntispamExchange Online过滤结果反映微软信誉评分
X-EOPAttributedMessageEOP处理标记区分外部/内部消息
X-MS-Exchange-Organization-AuthAs认证身份Internal vs Anonymous

发件人域名分析

WHOIS 历史查询深度分析:

使用多种工具交叉验证域名注册信息:

whois micros0ft-security.com
Domain Name: MICROS0FT-SECURITY.COM
Registry Domain ID: 2845671234_DOMAIN_COM-vrsn
Registrar WHOIS Server: whois.namecheap.com
Registrar: NameCheap, Inc.
Creation Date: 2025-12-28T09:15:00Z
Updated Date: 2025-12-28T09:15:00Z
Expiry Date: 2027-12-28T09:15:00Z

Registrant Organization: PrivacyProtect.org
Registrant State/Province: MA
Registrant Country: US

取证分析要点:

  • 注册日期距离攻击发生仅 18 天 → 新注册域名,高风险指标
  • 使用隐私保护服务 → 无法直接获取注册人信息
  • 选择低价注册商(NameCheap)→ 常见于短期攻击基础设施
  • 有效期仅 2 年 → 预期使用时间短暂

DNS 记录深度分析:

dig micros0ft-security.com ANY +noall +answer

micros0ft-security.com. 300 IN A     198.51.100.23
micros0ft-security.com. 300 IN MX 10 mail.micros0ft-security.com.
micros0ft-security.com. 300 IN TXT  "v=spf1 include:_spf.mailgun.org ~all"
mail._domainkey.micros0ft-security.com. 300 IN TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSq..."

dig micros0ft-security.com NS +short
ns1.suspended-domain.com.
ns2.suspended-domain.com.

dig _dmarc.micros0ft-security.com TXT +short
"v=DMARC1; p=none; rua=mailto:dmarc@micros0ft-security.com"

DNS 配置取证结论:

DNS 记录观察结果风险等级说明
A Record指向VPS IP🔴 高非企业级基础设施
SPF使用Mailgun🟡 中合法服务可被滥用
DKIM已配置🟢 低提高邮件可信度
DMARCp=none🔴 高未启用拒绝策略
NSsuspended-domain.com🔴 极高域名曾被暂停/滥用

相似域名检测(Homoglyph / Typosquatting):

import dns.resolver
from itertools import permutations

def generate_typosquat_variants(domain):
    name, tld = domain.rsplit('.', 1)
    variants = set()
    
    for i in range(len(name)):
        variants.add(name[:i] + name[i+1:])
    
    char_substitutions = {
        'o': '0', '0': 'o', 'l': '1', '1': 'l',
        'e': '3', 'a': '@', 's': 'z', 'rn': 'm',
        'cl': 'd', 'vv': 'w'
    }
    for original, replacement in char_substitutions.items():
        if original in name:
            variants.add(name.replace(original, replacement))
    
    for i in range(len(name) - 1):
        variants.add(name[:i] + name[i+1:i+2] + name[i:i+1] + name[i+2:])
    
    for sep in ['-', '.']:
        for i in range(1, len(name)):
            variants.add(name[:i] + sep + name[i:])
    
    homoglyph_map = {
        'a': 'а', 'c': 'ϲ', 'e': 'е', 'o': 'ο',
        'p': 'р', 's': 'ѕ', 'x': 'х'
    }
    for orig, hglyph in homoglyph_map.items():
        if orig in name:
            variants.add(name.replace(orig, hglyph))
    
    return [v + '.' + tld for v in variants]

def check_domain_resolved(domains):
    resolved = {}
    resolver = dns.resolver.Resolver()
    resolver.nameservers = ['8.8.8.8']
    resolver.lifetime = 3
    
    for domain in domains:
        try:
            answers = resolver.resolve(domain, 'A')
            ips = [rdata.address for rdata in answers]
            resolved[domain] = ips
        except Exception:
            resolved[domain] = None
    return resolved

邮件模板分析

HTML 结构指纹提取:

from bs4 import BeautifulSoup
import hashlib

def extract_template_fingerprint(html_content):
    soup = BeautifulSoup(html_content, 'html.parser')
    
    structure_hash = hashlib.sha256()
    for tag in soup.descendants:
        if tag.name:
            structure_hash.update(tag.name.encode())
    fingerprint = structure_hash.hexdigest()[:16]
    
    styles = soup.find_all(['style', 'link'], rel='stylesheet')
    style_content = ''.join(s.get_text() for s in soup.find_all('style'))
    style_hash = hashlib.md5(style_content.encode()).hexdigest()[:12]
    
    images = [img.get('src', '') for img in soup.find_all('img')]
    tracking_pixels = [src for src in images if 'track' in src.lower() or 'pixel' in src.lower()]
    
    links = [a.get('href', '') for a in soup.find_all('a')]
    suspicious_links = [l for l in links if l.startswith('http') and 'target' not in str(soup.find('a', href=l))]
    
    return {
        'structure_fingerprint': fingerprint,
        'style_fingerprint': style_hash,
        'tracking_pixels': tracking_pixels,
        'suspicious_links': suspicious_links,
        'total_images': len(images),
        'total_links': len(links),
        'form_count': len(soup.find_all('form')),
        'iframe_count': len(soup.find_all('iframe'))
    }

图片 EXIF 数据分析:

exiftool phishing_logo.png

ExifTool Version Number         : 12.70
File Name                       : phishing_logo.png
File Size                       : 45 KB
File Modification Date/Time     : 2025:12:20 14:32:18+08:00
File Type                       : PNG
Image Width                     : 200
Image Height                    : 56
Software                        : Adobe Photoshop 25.3
Creator Tool                    : Canva
Color Space                     : sRGB

EXIF 中暴露了 Canva 作为设计工具 → 攻击者使用了在线设计平台制作钓鱼素材,这意味着 Canva 平台上可能存在该攻击者的账户和活动记录。

落地页取证

页面源码分析与表单提交目标提取:

import requests
from bs4 import BeautifulSoup
import re

def analyze_landing_page(url):
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
    }
    response = requests.get(url, headers=headers, allow_redirects=False, timeout=15)
    
    redirect_chain = [url]
    while response.is_redirect or response.status_code in [301, 302, 303, 307, 308]:
        next_url = response.headers.get('Location')
        redirect_chain.append(next_url)
        response = requests.get(next_url, headers=headers, allow_redirects=False, timeout=15)
    
    final_html = response.text
    soup = BeautifulSoup(final_html, 'html.parser')
    
    forms = []
    for form in soup.find_all('form'):
        form_data = {
            'action': form.get('action', ''),
            'method': form.get('method', 'GET').upper(),
            'fields': [],
            'javascript_submit': bool(re.search(r'\.submit\(\)|\.click\(\)', final_html))
        }
        for inp in form.find_all(['input', 'textarea', 'select']):
            form_data['fields'].append({
                'name': inp.get('name', ''),
                'type': inp.get('type', 'text'),
                'id': inp.get('id', ''),
                'placeholder': inp.get('placeholder', ''),
                'autocomplete': inp.get('autocomplete', '')
            })
        forms.append(form_data)
    
    scripts = []
    for script in soup.find_all('script'):
        src = script.get('src', '')
        if src:
            scripts.append({'external': src})
        elif script.string:
            scripts.append({'inline': script.string[:500]})
    
    meta_tags = {}
    for meta in soup.find_all('meta'):
        name = meta.get('name', meta.get('property', ''))
        content = meta.get('content', '')
        if name:
            meta_tags[name] = content
    
    return {
        'final_url': response.url,
        'redirect_chain': redirect_chain,
        'status_code': response.status_code,
        'server_header': response.headers.get('Server', ''),
        'forms': forms,
        'scripts': scripts,
        'meta_tags': meta_tags,
        'tls_info': {
            'issuer': str(response.raw.connection.sock.getpeercert().get('issuer', '')),
            'subject': str(response.raw.connection.sock.getpeercert().get('subject', ''))
        } if hasattr(response.raw, 'connection') and response.raw.connection else None
    }

Wayback Machine 历史查询:

curl -s "https://archive.org/wayback/available?url=suspicious-site.com" | python3 -m json.tool

{
    "archived_snapshots": {
        "closest": {
            "available": true,
            "url": "https://web.archive.org/web/20260110/https://suspicious-site.com",
            "timestamp": "20260110",
            "status": "200"
        }
    }
}

检查页面是否曾经存在合法内容后被替换为钓鱼页面:
curl -s "https://web.archive.org/web/*/suspicious-site.com/*" | grep -oP 'datetime="[^"]*"'

Python 脚本:邮件头自动解析与基础设施提取

#!/usr/bin/env python3
import email
import re
import json
import hashlib
from datetime import datetime, timezone
import dns.resolver
import requests

class PhishingEmailAnalyzer:
    def __init__(self):
        self.resolver = dns.resolver.Resolver()
        self.resolver.nameservers = ['8.8.8.8', '1.1.1.1']
        self.resolver.lifetime = 5
        self.extracted_iocs = {
            'domains': set(),
            'ips': set(),
            'urls': set(),
            'emails': set(),
            'hashes': set()
        }
    
    def parse_full_headers(self, raw_message):
        msg = email.message_from_string(raw_message)
        
        auth_results = {}
        for header_name in ['Authentication-Results', 'X-Microsoft-Antispam', 
                            'X-SES-Outgoing', 'X-Original-Sender']:
            value = msg.get(header_name)
            if value:
                auth_results[header_name] = value
        
        received_chain = []
        for header in msg.get_all('Received', []):
            received_chain.append(header)
        
        hop_details = []
        for i, hop in enumerate(reversed(received_chain)):
            ip_match = re.findall(r'\[(\d+\.\d+\.\d+\.\d+)\]', hop)
            host_match = re.search(r'from\s+(\S+)', hop)
            time_match = re.search(r';\s*(.+)$', hop)
            
            hop_info = {
                'sequence': i + 1,
                'raw': hop.strip(),
                'ips': ip_match,
                'hostname': host_match.group(1) if host_match else '',
                'timestamp': time_match.group(1).strip() if time_match else ''
            }
            hop_details.append(hop_info)
            for ip in ip_match:
                self.extracted_iocs['ips'].add(ip)
        
        sender_analysis = {
            'from': msg.get('From', ''),
            'reply_to': msg.get('Reply-To', ''),
            'return_path': msg.get('Return-Path', ''),
            'envelope_from_mismatch': msg.get('Reply-To', '') != msg.get('From', '')
        }
        
        url_pattern = re.compile(r'https?://[^\s<>"\'`)]+')
        email_pattern = re.compile(r'[\w.+-]+@[\w-]+\.[\w.-]+')
        
        body = ''
        if msg.is_multipart():
            for part in msg.walk():
                ct = part.get_content_type()
                if ct in ['text/html', 'text/plain']:
                    body += part.get_payload(decode=True).decode('utf-8', errors='ignore')
        else:
            body = msg.get_payload(decode=True).decode('utf-8', errors='ignore')
        
        for match in url_pattern.finditer(body):
            self.extracted_iocs['urls'].add(match.group(0))
        for match in email_pattern.finditer(body):
            self.extracted_iocs['emails'].add(match.group(0))
        
        display_urls = []
        anchor_pattern = re.compile(r'<a[^>]*href=["\']([^"\']+)["\'][^>]*>(.*?)</a>', re.DOTALL)
        for match in anchor_pattern.finditer(body):
            real_url = match.group(1)
            display_text = re.sub(r'<[^>]+>', '', match.group(2)).strip()
            if display_text and 'http' in display_text:
                display_urls.append({
                    'display_text': display_text,
                    'real_url': real_url,
                    'mismatch': display_text != real_url
                })
        
        return {
            'authentication': auth_results,
            'hop_chain': hop_details,
            'sender': sender_analysis,
            'display_url_mismatches': display_urls,
            'iocs': {k: list(v) for k, v in self.extracted_iocs.items()}
        }
    
    def enrich_domains(self):
        enrichment = {}
        for domain in list(self.extracted_iocs['domains']):
            info = {'domain': domain}
            
            try:
                whois_resp = requests.get(f'https://www.whoisxmlapi.com/whoisserver/WhoisService?domain={domain}&outputFormat=json', timeout=10)
                info['whois_available'] = whois_resp.status_code == 200
            except Exception:
                info['whois_available'] = False
            
            try:
                ns_records = self.resolver.resolve(domain, 'NS')
                info['nameservers'] = [str(r) for r in ns_records]
            except Exception:
                info['nameservers'] = []
            
            try:
                mx_records = self.resolver.resolve(domain, 'MX')
                info['mx_records'] = [(str(r.exchange), r.preference) for r in mx_records]
            except Exception:
                info['mx_records'] = []
            
            try:
                spf_record = self.resolver.resolve(domain, 'TXT')
                spf_texts = [str(r).strip('"') for r in spf_record]
                info['spf'] = [t for t in spf_texts if 'v=spf1' in t]
            except Exception:
                info['spf'] = []
            
            enrichment[domain] = info
        return enrichment
    
    def generate_report(self, header_analysis, domain_enrichment):
        report = {
            'analysis_timestamp': datetime.now(timezone.utc).isoformat(),
            'email_authentication': header_analysis['authentication'],
            'routing_chain': header_analysis['hop_chain'],
            'sender_analysis': header_analysis['sender'],
            'display_url_deception': header_analysis['display_url_mismatches'],
            'infrastructure_enrichment': domain_enrichment,
            'extracted_iocs': header_analysis['iocs'],
            'risk_indicators': [],
            'attribution_hints': []
        }
        
        if header_analysis['sender']['envelope_from_mismatch']:
            report['risk_indicators'].append('Reply-To 与 From 地址不一致')
        
        for mismatch in header_analysis['display_url_mismatches']:
            if mismatch['mismatch']:
                report['risk_indicators'].append(f'URL显示欺骗: 显示"{mismatch["display_text"]}" → 实际"{mismatch["real_url"]}"')
        
        for domain, info in domain_enrichment.items():
            if info.get('nameservers'):
                for ns in info['nameservers']:
                    if any(suspect in ns.lower() for suspect in ['suspended', 'parked', 'expired']):
                        report['risk_indicators'].append(f'{domain} 使用可疑NS: {ns}')
        
        return report

0x03 BEC 商务邮件欺诈取证

BEC 攻击模式分类

BEC(Business Email Compromise) 是社会工程学攻击中经济损失最大的一类。FBI IC3 报告显示,2024 年 BEC 造成的全球损失超过 $29 亿,平均每起事件损失 $150,000 以上。

四大 BEC 攻击模式对比:

攻击模式伪装对象目标部门典型话术平均响应时间要求
CEO FraudCEO/CFO财务/会计“紧急转账,保密”1-2小时
Vendor Fraud供应商应付账款“更换收款账户”24-48小时
Attorney Impersonation法务/外部律师法务部/管理层“机密并购项目”数小时
IT Support FraudIT管理员全员“密码即将过期”即时

邮件账户入侵检测

邮箱规则异常检测 — 隐藏转发规则取证:

PowerShell 检查 O365 邮箱异常规则

Get-InboxRule -Mailbox victim@company.com | Select-Object Name, Enabled, Description, Priority | Format-List

异常规则特征
Name        : "Process Incoming Mail"伪装正常名称
Enabled     : True
Description : "将来自特定发件人的邮件标记为已读并移至RSS Feeds文件夹"
              "同时将副本转发至 external@gmail.com"
Priority    : -1最高优先级确保最先执行

查找隐藏转发规则的命令
Get-InboxRule -Mailbox victim@company.com | Where-Object {$_.ForwardTo -ne $null -or $_.RedirectTo -ne $null} | Format-List *

登录日志异常分析:

Azure AD Sign-in Logs 异常查询:

SigninLogs
| where UserPrincipalName == "victim@company.com"
| project TimeGenerated, IPAddress, Location, DeviceDetail, ClientAppUsed, ConditionalAccessStatus, ResultDescription
| order by TimeGenerated desc

异常检测维度:
- Impossible Travel: 短时间内从两个地理上不可达的位置登录
- 新 IP/国家首次出现
- 异常设备指纹(之前从未出现的浏览器/OS组合)
- 非工作时间登录模式突变
- ConditionalAccessStatus 变化(绕过MFA尝试)

OAUTH Token 滥用检测:

import msal
import requests
from datetime import datetime

class O365TokenAuditor:
    def __init__(self, tenant_id, client_id, client_secret):
        self.authority = f"https://login.microsoftonline.com/{tenant_id}"
        self.app = msal.ConfidentialClientApplication(
            client_id, authority=self.authority, client_credential=client_secret
        )
        self.graph_base = "https://graph.microsoft.com/v1.0"
    
    def audit_oauth_consent(self):
        token_result = self.app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
        access_token = token_result['access_token']
        
        headers = {'Authorization': f'Bearer {access_token}'}
        
        apps_response = requests.get(
            f"{self.graph_base}/servicePrincipals",
            headers=headers,
            params={"$filter": "tags/any(t:t eq 'WindowsAzureActiveDirectoryIntegratedApp')"}
        ).json()
        
        suspicious_apps = []
        for app in apps_response.get('value', []):
            oauth_perms = app.get('oauth2PermissionScopes', [])
            delegated_perms = app.get('oauth2PermissionGrants', [])
            
            high_risk_scopes = ['Mail.Read', 'Mail.ReadWrite', 'Mail.Send',
                               'Files.Read.All', 'MailboxSettings.ReadWrite']
            
            for perm in delegated_perms:
                scope_list = perm.get('scope', '').split()
                risky = [s for s in scope_list if s in high_risk_scopes]
                if risky:
                    suspicious_apps.append({
                        'app_id': app.get('appId'),
                        'app_display_name': app.get('displayName'),
                        'publisher': app.get('publisherName'),
                        'risky_scopes': risky,
                        'consent_time': perm.get('startTime', 'unknown')
                    })
        
        return suspicious_apps
    
    def check_mailbox_delegation(self, user_upn):
        token_result = self.app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
        headers = {'Authorization': f'Bearer {token_result["access_token"]}'}
        
        delegates = requests.get(
            f"{self.graph_base}/users/{user_upn}/mailboxSettings",
            headers=headers
        ).json()
        
        return delegates

财务流程分析

付款指令变更追溯:

在 BEC 案件中,财务流程的篡改取证是关键。取证人员需要还原完整的付款变更链路:

def trace_payment_instruction_change(internal_emails, bank_statements):
    timeline = []
    
    for email_record in internal_emails:
        subject = email_record['subject']
        keywords = ['bank', 'account', 'wire', 'payment', 'invoice', 'vendor',
                     'change', 'update', 'new account', 'replacement']
        
        if any(kw in subject.lower() for kw in keywords):
            timeline.append({
                'timestamp': email_record['received_time'],
                'event_type': 'potential_bank_change_request',
                'sender': email_record['sender'],
                'recipient': email_record['recipient'],
                'subject': subject,
                'has_attachment': bool(email_record.get('attachments')),
                'urgency_indicators': count_urgency_words(email_record['body']),
                'secrecy_requests': count_secrecy_words(email_record['body'])
            })
    
    for txn in bank_statements:
        matching_events = [e for e in timeline 
                         if abs((e['timestamp'] - txn['process_time']).total_seconds()) < 86400]
        if matching_events:
            txn['correlated_email_event'] = matching_events
    
    return sorted(timeline, key=lambda x: x['timestamp'])

def count_urgency_words(text):
    urgency_terms = ['urgent', 'immediately', 'asap', 'critical', 'confidential',
                     'do not share', 'secret', 'time-sensitive', 'rush', 'priority']
    text_lower = text.lower()
    return sum(1 for term in urgency_terms if term in text_lower)

def count_secrecy_words(text):
    secrecy_terms = ['do not tell', 'keep quiet', 'between us', 'not announce',
                     'merger preparation', 'board approval pending', 'internal only']
    text_lower = text.lower()
    return sum(1 for term in secrecy_terms if term in text_lower)

邮件通信模式异常检测

def detect_bec_communication_anomalies(mailbox_logs, baseline_period_days=30):
    import numpy as np
    
    normal_patterns = {}
    for user, logs in mailbox_logs.items():
        recent = logs[-baseline_period_days:]
        baseline = logs[:-baseline_period_days]
        
        baseline_external_ratio = np.mean([log['is_external'] for log in baseline])
        recent_external_ratio = np.mean([log['is_external'] for log in recent])
        
        baseline_outbound_hour = {}
        for log in baseline:
            hour = log['sent_time'].hour
            baseline_outbound_hour.setdefault(hour, 0)
            baseline_outbound_hour[hour] += 1
        
        recent_outbound_hour = {}
        for log in recent:
            hour = log['sent_time'].hour
            recent_outbound_hour.setdefault(hour, 0)
            recent_outbound_hour[hour] += 1
        
        thread_injection_candidates = []
        for log in recent:
            if log['in_reply_to'] and log['thread_position'] == 0:
                original_thread_starter = find_original_sender(log['in_reply_to'], logs)
                if original_thread_starter != log['sender'] and log['is_external']:
                    thread_injection_candidates.append(log)
        
        anomalies = {
            'external_ratio_spike': recent_external_ratio - baseline_external_ratio,
            'off_hours_spike': sum(recent_outbound_hour.get(h, 0) for h in range(0, 7)),
            'thread_injections': thread_injection_candidates
        }
        normal_patterns[user] = anomalies
    
    return normal_patterns

0x04 话术工程与心理操纵分析

社会工程学六大原则与攻击映射

Robert Cialdini 提出的六大影响力原则在社会工程学攻击中有明确的对应模式:

Cialdini原则攻击应用模式典型话术示例取证语言特征
互惠(Reciprocity)提供"免费"资源诱导回报“您的专属优惠”/“会员特权”过度承诺性语言
承诺与一致(Commitment)从小请求逐步升级“请先确认身份…现在需要验证码…”渐进式要求递增
权威(Authority)冒充高管/监管/IT“CEO指示”/“合规部门要求”正式称谓+命令句式
社会证明(Social Proof)暗示他人已完成相同操作“其他同事都已更新”/“全公司都在使用”群体性表述
喜好(Liking)建立好感后再提要求使用目标社交信息拉近距离个性化引用+赞美语
稀缺(Scarcity)制造紧迫感和错失恐惧“24小时内失效”/“最后机会”时间限制+量化倒数

语言特征分析方法论

紧急性语言模式检测:

def analyze_urgency_patterns(text):
    urgency_categories = {
        'time_constraint': [
            r'(?:立即|马上|即刻)[^,。.]*?(?:处理|操作|回复|确认)',
            r'(?:within|in)\s+\d+\s+(?:minutes|hours|days)',
            r'by\s+(?:EOD|COB|end of day|tomorrow)',
            r' ASAP\b', r' urgent(?:ly)?\b', r' immediately\b'
        ],
        'threat_of_loss': [
            r'(?:账户|账号|account).*?(?:将被|will be).*?(?:锁定|关闭|suspend)',
            r'(?:penalty|罚款|处罚)',
            r'(?:legal action|法律诉讼)',
            r'(?:permanent|永久).*?(?:damage|损害|loss)'
        ],
        'authority_invocation': [
            r'(?:CEO|CTO|CFO|总裁|总经理).*?(?:要求|指示|命令|request|order)',
            r'(?:compliance|合规|regulatory|监管).*?(?:require|必须|需要)',
            r'(?:法务|legal|lawyer|律师).*?(?:advice|建议|opinion|意见)'
        ],
        'secrecy_demand': [
            r'(?:保密|不要告诉|confidential|do not share)',
            r'(?:between us|你我之间)',
            r'(?:do not (?:disclose|announce|inform))',
            r'(?:仅限|only for|restricted)'
        ]
    }
    
    results = {}
    for category, patterns in urgency_categories.items():
        matches = []
        for pattern in patterns:
            found = re.findall(pattern, text, re.IGNORECASE)
            matches.extend(found)
        results[category] = {
            'count': len(matches),
            'examples': matches[:5],
            'severity': 'high' if len(matches) >= 3 else 'medium' if len(matches) >= 1 else 'low'
        }
    
    overall_score = sum(r['count'] for r in results.values())
    results['overall_manipulation_score'] = min(overall_score / 10, 1.0)
    
    return results

翻译痕迹检测 — 多语言攻击识别:

当攻击者使用机器翻译生成中文钓鱼邮件时,常留下以下痕迹:

  • 中式英语反向翻译的语法结构
  • 不适用于中文语境的敬语/称谓混用
  • 特定短语在不同段落间的一致性差异(分段翻译导致)
  • 英文标点符号残留(如半角逗号在中文语境中)
  • 直译导致的语义不通顺
def detect_translation_artifacts(text):
    indicators = {
        'mixed_scripts': bool(re.search(r'[\u4e00-\u9fff].*?[a-zA-Z]{3,}|[a-zA-Z]{3,}.*?[\u4e00-\u9fff]', text)),
        'inconsistent_honorifics': bool(re.search(r'您.*你|你.*您', text)),
        'english_punctuation': bool(re.search(r'[\u4e00-\u9fff]\s*[,;!?]\s*[\u4e00-\u9fff]', text)),
        'calque_phrases': bool(re.search(r'根据我们的记录|per our records|按照我们的信息', text, re.IGNORECASE)),
        'unnecessary_formality': bool(re.search(r'我们诚挚地通知您|we hereby inform you', text, re.IGNORECASE))
    }
    
    detected = sum(1 for v in indicators.values() if v)
    return {
        'artifacts_detected': detected,
        'translation_likelihood': 'very_high' if detected >= 3 else 'high' if detected >= 2 else 'possible' if detected >= 1 else 'unlikely',
        'details': indicators
    }

攻击者心理画像构建

基于话术分析的心理学建模方法:

ATTACKER_PROFILING_MODEL = {
    "linguistic_background": {
        "native_language": "基于翻译痕迹判断",
        "education_level": "基于词汇复杂度和语法规范性",
        "cultural_context": "基于节日引用、时间表达习惯、称谓方式"
    },
    "organizational_capability": {
        "reconnaissance_depth": "基于对个人信息的掌握程度",
        "template_quality": "基于视觉设计和文案质量",
        "infrastructure_sophistication": "基于域名选择和服务器配置水平"
    },
    "psychological_techniques": {
        "primary_leverage": "主要利用的心理原则",
        "emotional_targets": ["恐惧", "好奇", "贪婪", "服从", "同情"],
        "escalation_pattern": "从小要求到大要求的递进模式"
    },
    "operational_security": {
        "anonymity_level": "身份信息保护措施",
        "reusability": "基础设施复用程度",
        "counter_forensics": "反取证意识强度"
    }
}

Vishing 与 Smishing 取证

Vishing 通话录音分析:

import speech_recognition as sr
from pydub import AudioSegment

def analyze_vishing_recording(audio_file_path):
    audio = AudioSegment.from_file(audio_file_path)
    
    duration = len(audio) / 1000
    sample_rate = audio.frame_rate
    channels = audio.channels
    
    caller_profile = {
        'duration_seconds': duration,
        'accent_detection': analyze_accent(audio),
        'background_noise': detect_background_environment(audio),
        'voice_gender': detect_voice_gender(audio),
        'estimated_age_range': estimate_age_from_voice(audio),
        'language_dialect': detect_dialect(audio),
        'voip_indicators': detect_voip_artifacts(audio),
        'caller_id_spoof': None
    }
    
    recognizer = sr.Recognizer()
    with sr.AudioFile(audio_file_path) as source:
        audio_data = recognizer.record(source)
        try:
            transcript = recognizer.recognize_google(audio_data, language='zh-CN')
            caller_profile['transcript'] = transcript
            caller_profile['manipulation_analysis'] = analyze_urgency_patterns(transcript)
        except sr.UnknownValueError:
            caller_profile['transcript'] = 'Unable to transcribe'
    
    return caller_profile

def detect_voip_artifacts(audio):
    spectral_centroid = compute_spectral_centroid(audio)
    codec_signatures = {
        'g711': check_g711_codec(audio),
        'g729': check_g729_codec(audio),
        'opus': check_opus_codec(audio),
        'amr': check_amr_codec(audio)
    }
    active_codecs = [k for k, v in codec_signatures.items() if v]
    is_voip = len(active_codecs) > 0
    
    jitter_level = measure_jitter(audio)
    packet_loss_estimate = estimate_packet_loss(audio)
    
    return {
        'is_voip': is_voip,
        'codec': active_codecs,
        'jitter_ms': jitter_level,
        'packet_loss_pct': packet_loss_estimate
    }

0x05 OSINT 调查技术

被动信息收集

域名/IP 情报平台对比:

平台核心能力API限制最佳用途付费需求
Shodan互联网设备扫描100次/月(免费)开放端口/服务指纹高级搜索需付费
Censys全网TLS证书+主机250次/月(免费)证书关联分析批量查询需付费
FOFA中国资产测绘有限免费国内目标分析大量API需付费
VirusTotal恶意文件/URL分析4次/分钟(免费)IOC验证Intelligence功能需付费
URLscan.io网页截图+DOM分析有限免费钓鱼页面可视化私有扫描需付费
ThreatCrowd威胁情报聚合无限制多维度IOC关联完全免费

Shodan 查询实战示例:

shodan search "http.html:'Microsoft Office' country:CN port:443" --fields ip_str,port,org,hostnames

shodan host 198.51.100.23
218.xxx.xxx.xxx
Hostnames: sender.campaign-server.io
Organization: XYZ Hosting LLC
OS: Linux
Ports: 22, 80, 443, 8080

shodan search "http.title:'Document Sharing Portal'" --fields ip_str,port,http.title

证书透明度(Certificate Transparency)日志分析:

import requests
import json

def query_ct_logs(domain):
    url = f"https://crt.sh/?q=%25.{domain}&output=json"
    response = requests.get(url, timeout=30)
    certificates = response.json()
    
    cert_analysis = {}
    for cert in certificates:
        issuer = cert.get('issuer_name', '')
        not_before = cert.get('not_before', '')
        common_name = cert.get('common_name', '')
        name_value = cert.get('name_value', '')
        
        subdomains = name_value.split('\n')
        for sd in subdomains:
            sd_clean = sd.strip().lower()
            if sd_clean not in cert_analysis:
                cert_analysis[sd_clean] = []
            cert_analysis[sd_clean].append({
                'issuer': issuer,
                'issued_date': not_before,
                'cn': common_name
            })
    
    historical_subdomains = set()
    for sd, certs in cert_analysis.items():
        historical_subdomains.add(sd)
        if len(certs) > 1:
            print(f"[!] {sd} 有多张证书记录 → 可能频繁续期/重新申请")
    
    suspicious_tlds = ['.xyz', '.top', '.tk', '.pw', '.cc', '.su']
    for sd in historical_subdomains:
        if any(sd.endswith(tld) for tld in suspicious_tlds):
            print(f"[!] 可疑TLD子域名: {sd}")
    
    return cert_analysis

被动 DNS 分析:

使用 PassiveTotal / SecurityTrails / CirclPDNS 查询历史解析:

curl -s "https://api.passivetotal.org/v2/dns/passive" \
  -u "$PT_USERNAME:$PT_API_KEY" \
  -d '{"query": "198.51.100.23"}' | python3 -m json.tool

输出示例:
{
    "results": [
        {"resolve": "campaign-server.io", "firstSeen": "2025-12-01", "lastSeen": "2026-01-15", "recordType": "A"},
        {"resolve": "micros0ft-security.com", "firstSeen": "2025-12-28", "lastSeen": "2026-01-20", "recordType": "A"},
        {"resolve": "secure-login-verify.net", "firstSeen": "2026-01-05", "lastSeen": "2026-01-22", "recordType": "A"},
        {"resolve": "office365-update.org", "firstSeen": "2026-01-10", "lastSeen": "2026-01-25", "recordType": "A"}
    ]
}

同一IP承载多个仿冒域名 → 确认为集中化钓鱼基础设施

工具链整合

theHarvester 综合收集:

theHarvester -d company.com -b all -f output/company_recon.html

模块覆盖范围:
- baidu/bing/duckduckgo/yahoo: 搜索引擎枚举
- crtsh/certspotter: 证书透明度
- github-codesearch: GitHub代码搜索
- hackertarget/rapidDNS: 子域名暴力枚举
- linkedin/linkedin_links: LinkedIn人员收集
- twitter: Twitter账号发现
- urlscan: URL扫描历史
- vulners: 漏洞信息关联

SpiderFoot 自动化 OSINT:

docker run -p 5001:5001 -v spiderfoot_data:/root/.spiderfoot spiderfoot/spiderfoot:latest

SpiderFoot 内置模块类别:
- DNS: 子域名枚举/反向DNS/区域传输
- Email: 邮箱泄露检查/PwnedDB查询
- Social: 社交媒体账号发现/用户名枚举
- Darknet: Tor市场搜索/暗网泄露监控
- Threat Intel: VirusTotal/Shodan/AbuseIPDB集成
- Geo: IP地理定位/GPS坐标解析

Python 自动化 OSINT 框架

#!/usr/bin/env python3
import requests
import json
import dns.resolver
import socket
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from collections import defaultdict

class AutomatedOSINT:
    def __init__(self, target_domain, api_keys=None):
        self.target = target_domain
        self.api_keys = api_keys or {}
        self.results = defaultdict(dict)
        self.ioc_set = set()
    
    def passive_dns_lookup(self):
        resolvers = [
            ('securitytrails', f"https://api.securitytrails.com/v1/history/{self.target}/dns/a"),
            ('circl_pdns', f"https://www.circl.lu/pdns/query/{self.target}")
        ]
        
        for provider, url in resolvers:
            try:
                headers = {}
                if provider == 'securitytrails':
                    headers['APIKEY'] = self.api_keys.get('securitytrails', '')
                
                resp = requests.get(url, headers=headers, timeout=15)
                if resp.status_code == 200:
                    self.results['passive_dns'][provider] = resp.json()
            except Exception as e:
                self.results['errors'].append(f'{provider}: {str(e)}')
    
    def certificate_transparency_scan(self):
        url = f"https://crt.sh/?q=%25.{self.target}&output=json"
        try:
            resp = requests.get(url, timeout=30)
            certs = resp.json()
            subdomains = set()
            issuers = defaultdict(int)
            
            for cert in certs:
                names = cert.get('name_value', '').split('\n')
                for name in names:
                    cleaned = name.strip().lower().lstrip('*.')
                    if cleaned and '.' in cleaned:
                        subdomains.add(cleaned)
                
                issuer_cn = cert.get('issuer_ca_id', 'unknown')
                issuers[str(issuer_cn)] += 1
            
            self.results['cert_transparency'] = {
                'subdomains': sorted(subdomains),
                'certificate_count': len(certs),
                'unique_issuers': dict(issuers),
                'oldest_cert': min(c.get('not_before', '9999') for c in certs) if certs else None,
                'newest_cert': max(c.get('not_before', '0000') for c in certs) if certs else None
            }
        except Exception as e:
            self.results['errors'].append(f'crt.sh: {str(e)}')
    
    def shodan_host_query(self, ip_address):
        if 'shodan' not in self.api_keys:
            return
        url = f"https://api.shodan.io/shodan/host/{ip_address}?key={self.api_keys['shodan']}"
        try:
            resp = requests.get(url, timeout=15)
            if resp.status_code == 200:
                data = resp.json()
                self.results['shodan'] = {
                    'ip': data.get('ip_str'),
                    'hostnames': data.get('hostnames', []),
                    'org': data.get('org', ''),
                    'os': data.get('os', ''),
                    'ports': data.get('ports', []),
                    'vulns': data.get('vulns', []),
                    'country': data.get('country_name', ''),
                    'city': data.get('city', '')
                }
                for hostname in data.get('hostnames', []):
                    self.ioc_set.add(hostname)
        except Exception as e:
            self.results['errors'].append(f'shodan: {str(e)}')
    
    def subdomain_enum(self):
        wordlist = ['mail', 'ftp', 'vpn', 'remote', 'webmail', 'admin', 'portal',
                    'api', 'dev', 'staging', 'test', 'owa', 'exchange', 'autodiscover',
                    'login', 'sso', 'auth', 'cloud', 'cdn', 'static', 'assets']
        
        found = []
        def resolve_subdomain(sub):
            fqdn = f"{sub}.{self.target}"
            try:
                ip = socket.gethostbyname(fqdn)
                return fqdn, ip
            except socket.gaierror:
                return fqdn, None
        
        with ThreadPoolExecutor(max_workers=20) as executor:
            futures = {executor.submit(resolve_subdomain, sub): sub for sub in wordlist}
            for future in as_completed(futures):
                fqdn, ip = future.result()
                if ip:
                    found.append({'fqdn': fqdn, 'ip': ip})
                    self.ioc_set.add(ip)
        
        self.results['subdomains'] = found
    
    def generate_osint_report(self):
        tasks = [
            self.passive_dns_lookup,
            self.certificate_transparency_scan,
            self.subdomain_enum
        ]
        
        threads = []
        for task in tasks:
            t = threading.Thread(target=task)
            threads.append(t)
            t.start()
        
        for t in threads:
            t.join(timeout=60)
        
        return {
            'target': self.target,
            'timestamp': __import__('datetime').datetime.utcnow().isoformat(),
            'findings': dict(self.results),
            'ioc_summary': {
                'domains': [ioc for ioc in self.ioc_set if not ioc.replace('.','').isdigit()],
                'ips': [ioc for ioc in self.ioc_set if ioc.replace('.','').isdigit()]
            }
        }

0x06 社交媒体取证

平台取证方法论

Twitter/X 账号取证:

使用 snscrape 或 twint 提取公开推文及元数据:

snscrape --jsonl twitter-user @suspect_account > tweets.jsonl

Python 分析脚本:
import json

tweets = []
with open('tweets.jsonl', 'r') as f:
    for line in f:
        tweets.append(json.loads(line))

metadata_extraction = {
    'account_created': tweets[0]['date'] if tweets else None,
    'total_tweets': len(tweets),
    'posting_schedule': analyze_posting_frequency(tweets),
    'geotagged_posts': [t for t in tweets if t.get('coordinates')],
    'mentioned_accounts': extract_mentions(tweets),
    'hashtags_used': extract_hashtags(tweets),
    'url_shorteners': extract_shortened_urls(tweets),
    'image_metadata': extract_image_urls(tweets)
}

图片地理定位技术(Geolocation):

from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS
import requests

def extract_gps_from_image(image_path):
    image = Image.open(image_path)
    exif_data = image._getexif()
    
    if not exif_data:
        return None
    
    gps_info = {}
    for tag_id, value in exif_data.items():
        tag_name = TAGS.get(tag_id, tag_id)
        if tag_name == 'GPSInfo':
            for gps_tag_id, gps_value in value.items():
                gps_tag_name = GPSTAGS.get(gps_tag_id, gps_tag_id)
                gps_info[gps_tag_name] = gps_value
    
    def convert_gps(value):
        d = float(value[0])
        m = float(value[1])
        s = float(value[2])
        return d + m/60.0 + s/3600.0
    
    lat = convert_gps(gps_info['GPSLatitude'])
    lon = convert_gps(gps_info['GPSLongitude'])
    if gps_info['GPSLatitudeRef'] == 'S':
        lat = -lat
    if gps_info['GPSLongitudeRef'] == 'W':
        lon = -lon
    
    return {'latitude': lat, 'longitude': lon, 'altitude': gps_info.get('GPSAltitude')}

def reverse_geocode(lat, lon):
    url = f"https://nominatim.openstreetmap.org/reverse?format=json&lat={lat}&lon={lon}"
    headers = {'User-Agent': 'ForensicGeoLoc/1.0'}
    resp = requests.get(url, headers=headers, timeout=10)
    return resp.json().get('address', {})

def analyze_image_background(image_path):
    from PIL import Image
    img = Image.open(image_path)
    width, height = img.size
    
    regions = {
        'sky': img.crop((0, 0, width, height//4)),
        'mid_ground': img.crop((0, height//4, width, height//2)),
        'foreground': img.crop((0, height//2, width, height))
    }
    
    dominant_colors = {}
    for region_name, region_img in regions.items():
        small = region_img.resize((50, 50))
        pixels = list(small.getdata())
        avg_color = tuple(sum(c)//len(pixels) for c in zip(*pixels))
        dominant_colors[region_name] = avg_color
    
    return dominant_colors

社交关系图谱构建:

import networkx as nx
import matplotlib.pyplot as plt

def build_social_graph(accounts_data):
    G = nx.DiGraph()
    
    for account in accounts_data:
        username = account['username']
        G.add_node(username, 
                   created=account['created_at'],
                   followers=account['followers_count'],
                   verified=account.get('verified', False))
        
        for mention in account.get('mentions', []):
            G.add_edge(username, mention['username'],
                      interaction_type=mention['type'],
                      frequency=mention['count'])
        
        for follower in account.get('follows', []):
            if not G.has_edge(username, follower):
                G.add_edge(username, follower, interaction_type='follow')
    
    centrality = nx.degree_centrality(G)
    betweenness = nx.betweenness_centrality(G)
    
    hubs = sorted(centrality.items(), key=lambda x: x[1], reverse=True)[:10]
    bridges = sorted(betweenness.items(), key=lambda x: x[1], reverse=True)[:10]
    
    return {
        'graph': G,
        'node_count': G.number_of_nodes(),
        'edge_count': G.number_of_edges(),
        'key_hubs': hubs,
        'bridge_accounts': bridges,
        'communities': nx.community.louvain_communities(G.to_undirected())
    }

即时通讯平台取证

Telegram 频道/群组取证:

import telethon
from telethon.sync import TelegramClient

async def telegram_channel_forensics(api_id, api_hash, channel_username):
    async with TelegramClient('forensic_session', api_id, api_hash) as client:
        entity = await client.get_entity(channel_username)
        
        channel_info = {
            'id': entity.id,
            'title': entity.title,
            'username': entity.username,
            'date_created': entity.date,
            'members_count': getattr(entity, 'participants_count', 'hidden'),
            'description': getattr(entity, 'about', ''),
            'linked_chat': getattr(entity, 'linked_chat_id', None)
        }
        
        messages = []
        async for message in client.iter_messages(entity, limit=1000):
            msg_data = {
                'id': message.id,
                'date': message.date.isoformat(),
                'text': message.text,
                'views': message.views,
                'forwards': message.forwards,
                'media': message.media is not None,
                'edited': message.edit_date.isoformat() if message.edit_date else None,
                'via_bot': message.via_bot.username if message.via_bot else None,
                'fwd_from': message.fwd_from.from_id if message.fwd_from else None
            }
            messages.append(msg_data)
        
        admins = []
        try:
            async for admin in client.iter_participants(entity, filter=telethon.tl.types.ChannelParticipantsAdmins):
                admins.append({
                    'id': admin.id,
                    'username': admin.username,
                    'role': admin.participant_rank
                })
        except Exception:
            pass
        
        return {
            'channel_info': channel_info,
            'messages': messages,
            'admins': admins,
            'message_frequency': analyze_message_frequency(messages)
        }

Discord 服务器取证:

import discord
import asyncio
from datetime import datetime

class DiscordForensicBot(discord.Client):
    def __init__(self, guild_id):
        intents = discord.Intents.all()
        super().__init__(intents=intents)
        self.guild_id = guild_id
        self.forensic_data = {
            'guild_info': None,
            'channels': {},
            'members': [],
            'roles': [],
            'audit_log': []
        }
    
    async def collect_guild_forensics(self):
        guild = self.get_guild(self.guild_id)
        
        self.forensic_data['guild_info'] = {
            'name': guild.name,
            'id': guild.id,
            'owner_id': guild.owner_id,
            'created_at': guild.created_at.isoformat(),
            'member_count': guild.member_count,
            'boost_level': guild.premium_tier,
            'verification_level': str(guild.verification_level),
            'features': guild.features
        }
        
        for role in guild.roles:
            self.forensic_data['roles'].append({
                'name': role.name,
                'id': role.id,
                'permissions': list(role.permissions),
                'member_count': len(role.members),
                'color': str(role.color),
                'hoisted': role.hoist,
                'mentionable': role.mentionable,
                'created_at': role.created_at.isoformat()
            })
        
        async for entry in guild.audit_logs(limit=500):
            self.forensic_data['audit_log'].append({
                'action': str(entry.action),
                'user': str(entry.user),
                'target': str(entry.target),
                'reason': entry.reason,
                'created_at': entry.created_at.isoformat(),
                'changes': str(entry.changes.before) + ' -> ' + str(entry.changes.after)
            })
        
        await self.close()

0x07 深度伪造检测

Deepfake 技术分类与检测方法

图像深度伪造检测流水线:

import cv2
import numpy as np
from PIL import Image

class DeepfakeDetector:
    def error_level_analysis(self, image_path, quality=95):
        img = cv2.imread(image_path)
        resized = cv2.resize(img, (512, 512))
        
        encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), quality]
        _, compressed = cv2.imencode('.jpg', resized, encode_param)
        decompressed = cv2.imdecode(compressed, cv2.IMREAD_UNCHANGED)
        
        ela = cv2.absdiff(resized, decompressed)
        ela_normalized = cv2.normalize(ela, None, 0, 255, cv2.NORM_MINMAX)
        
        mean_error = np.mean(ela)
        std_error = np.std(ela)
        
        regions = self.segment_into_grid(ela_normalized, grid_size=(4, 4))
        anomaly_regions = []
        global_mean = np.mean(ela_normalized)
        global_std = np.std(ela_normalized)
        
        for idx, region in enumerate(regions):
            region_mean = np.mean(region)
            z_score = (region_mean - global_mean) / (global_std + 1e-6)
            if abs(z_score) > 2.0:
                anomaly_regions.append({
                    'region_index': idx,
                    'mean_intensity': float(region_mean),
                    'z_score': float(z_score),
                    'anomaly_type': 'likely_manipulated' if z_score > 0 else 'likely_original'
                })
        
        return {
            'mean_ela_error': float(mean_error),
            'std_ela_error': float(std_error),
            'anomaly_regions': anomaly_regions,
            'manipulation_probability': min(len(anomaly_regions) / 16.0, 1.0)
        }
    
    def frequency_domain_analysis(self, image_path):
        img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
        img_float = np.float32(img)
        
        dft = cv2.dft(img_float)
        magnitude_spectrum = cv2.magnitude(dft[:, :, 0], dft[:, :, 1])
        magnitude_log = np.log1p(magnitude_spectrum)
        
        h, w = magnitude_log.shape
        center_y, center_x = h // 2, w // 2
        
        low_freq_mask = np.zeros_like(magnitude_log)
        cv2.circle(low_freq_mask, (center_x, center_y), min(h, w) // 8, 1, -1)
        high_freq_energy = np.sum(magnitude_log * (1 - low_freq_mask))
        total_energy = np.sum(magnitude_log)
        
        high_freq_ratio = high_freq_energy / (total_energy + 1e-6)
        
        artifacts = self.detect_periodic_artifacts(magnitude_log)
        
        return {
            'high_frequency_ratio': float(high_freq_ratio),
            'periodic_artifacts_detected': artifacts['detected'],
            'artifact_frequencies': artifacts.get('frequencies', []),
            'synthetic_likelihood': 'high' if high_freq_ratio > 0.6 or artifacts['detected'] else 'low'
        }
    
    def facial_consistency_check(self, image_path):
        face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
        img = cv2.imread(image_path)
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        faces = face_cascade.detectMultiScale(gray, 1.1, 4)
        
        consistency_results = {}
        
        if len(faces) > 0:
            x, y, w, h = faces[0]
            face_roi = gray[y:y+h, x:x+w]
            
            left_eye_region = face_roi[0:h//3, 0:w//2]
            right_eye_region = face_roi[0:h//3, w//2:]
            
            left_eye_hist = cv2.calcHist([left_eye_region], [0], None, [256], [0, 256])
            right_eye_hist = cv2.calcHist([right_eye_region], [0], None, [256], [0, 256])
            
            eye_symmetry = cv2.compareHist(left_eye_hist, right_eye_hist, cv2.HISTCMP_CORREL)
            
            consistency_results['eye_symmetry'] = float(eye_symmetry)
            consistency_results['face_aspect_ratio'] = w / h if h > 0 else 0
            consistency_results['face_count'] = len(faces)
            
            skin_tone_regions = [
                face_roi[h//3:2*h//3, w//4:3*w//4],
                face_roi[2*h//3:, w//4:3*w//4]
            ]
            skin_colors = [np.mean(r) for r in skin_tone_regions if r.size > 0]
            if len(skin_colors) >= 2:
                consistency_results['skin_tone_variance'] = float(np.std(skin_colors))
        
        return consistency_results

音频深度伪造检测:

import librosa
import numpy as np
from scipy import signal

class AudioDeepfakeDetector:
    def __init__(self, audio_path):
        self.audio_path = audio_path
        self.y, self.sr = librosa.load(audio_path, sr=22050)
    
    def spectral_analysis(self):
        spectral_centroid = librosa.feature.spectral_centroid(y=self.y, sr=self.sr)
        spectral_rolloff = librosa.feature.spectral_rolloff(y=self.y, sr=self.sr)
        spectral_bandwidth = librosa.feature.spectral_bandwidth(y=self.y, sr=self.sr)
        zero_crossing_rate = librosa.feature.zero_crossing_rate(self.y)
        
        centroid_mean = float(np.mean(spectral_centroid))
        rolloff_mean = float(np.mean(spectral_rolloff))
        bandwidth_mean = float(np.mean(spectral_bandwidth))
        
        synthetic_indicators = {
            'abnormally_smooth_spectrum': float(np.std(spectral_centroid)) < 100,
            'missing_high_frequency_detail': rolloff_mean < 4000,
            'unnatural_bandwidth': bandwidth_mean < 500,
            'zero_crossing_anomaly': float(np.std(zero_crossing_rate)) < 0.01
        }
        
        return {
            'spectral_centroid_mean': centroid_mean,
            'spectral_rolloff_mean': rolloff_mean,
            'spectral_bandwidth_mean': bandwidth_mean,
            'synthetic_likelihood': sum(1 for v in synthetic_indicators.values() if v),
            'indicators': synthetic_indicators
        }
    
    def breathing_pattern_analysis(self):
        hop_length = 512
        energy = librosa.feature.rms(y=self.y, hop_length=hop_length)[0]
        
        silent_frames = energy < np.percentile(energy, 10)
        breathing_candidates = []
        
        in_breath = False
        breath_start = 0
        for i, is_silent in enumerate(silent_frames):
            if is_silent and not in_breath:
                breath_start = i
                in_breath = True
            elif not is_silent and in_breath:
                breath_duration_frames = i - breath_start
                breath_duration_sec = breath_duration_frames * hop_length / self.sr
                if 0.1 < breath_duration_sec < 3.0:
                    breathing_candidates.append({
                        'frame': breath_start,
                        'duration_seconds': breath_duration_sec,
                        'time_seconds': breath_start * hop_length / self.sr
                    })
                in_breath = False
        
        regular_intervals = []
        for i in range(1, len(breathing_candidates)):
            interval = breathing_candidates[i]['time_seconds'] - breathing_candidates[i-1]['time_seconds']
            regular_intervals.append(interval)
        
        interval_regularity = np.std(regular_intervals) if regular_intervals else float('inf')
        
        return {
            'breathing_events_found': len(breathing_candidates),
            'average_interval': float(np.mean(regular_intervals)) if regular_intervals else None,
            'interval_regularity_std': float(interval_regularity),
            'too_regular': interval_regularity < 0.3 if regular_intervals else False
        }
    
    def environmental_noise_consistency(self):
        noise_frames = librosa.effects.split(self.y, top_db=30)
        
        if len(noise_frames) < 2:
            return {'consistent': True, 'note': 'insufficient_noise_segments'}
        
        noise_spectra = []
        for start, end in noise_frames:
            segment = self.y[start:end]
            if len(segment) > 0:
                spectrum = np.abs(np.fft.rfft(segment))
                noise_spectra.append(spectrum)
        
        correlations = []
        for i in range(len(noise_spectra)):
            for j in range(i+1, len(noise_spectra)):
                min_len = min(len(noise_spectra[i]), len(noise_spectra[j]))
                corr = np.corrcoef(noise_spectra[i][:min_len], noise_spectra[j][:min_len])[0, 1]
                correlations.append(corr)
        
        avg_correlation = float(np.mean(correlations)) if correlations else 0
        
        return {
            'noise_segments': len(noise_frames),
            'average_noise_correlation': avg_correlation,
            'consistent_environment': avg_correlation > 0.8,
            'possible_splicing': avg_correlation < 0.3 and len(noise_frames) > 3
        }

Deepfake 检测工具对比:

工具/平台检测类型准确率部署方式适用场景
Deepware Scanner视频换脸92%+Web API快速视频验证
Hive Moderation图像/视频 AI生成95%+API多模态内容审核
Microsoft Video Authenticator视频深度伪造90%+SDK企业级集成
Sensity AI多类型深度伪造93%+API+Dashboard持续监控
Intel FakeCatcher视频真实性96%+SDK实时检测
GetReal面部操纵检测91%+Web快速筛查

0x08 物理社会工程学取证

物理入侵取证方法

监控录像分析取证:

物理社会工程学取证的核心在于将物理世界的证据与数字世界的证据进行关联。监控录像分析是首要步骤:

import cv2
import os
from datetime import datetime

class PhysicalSEForensics:
    def __init__(self, cctv_directory, access_log_path):
        self.cctv_dir = cctv_directory
        self.access_logs = self.parse_access_logs(access_log_path)
    
    def parse_access_logs(self, log_path):
        logs = []
        with open(log_path, 'r') as f:
            for line in f:
                parts = line.strip().split(',')
                if len(parts) >= 4:
                    logs.append({
                        'timestamp': datetime.strptime(parts[0], '%Y-%m-%d %H:%M:%S'),
                        'badge_id': parts[1],
                        'door_id': parts[2],
                        'access_result': parts[3],
                        'photo_ref': parts[4] if len(parts) > 4 else None
                    })
        return logs
    
    def detect_tailgating(self, video_path, fps=30):
        cap = cv2.VideoCapture(video_path)
        frame_count = 0
        person_detections = []
        
        person_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_fullbody.xml')
        
        while cap.isOpened():
            ret, frame = cap.read()
            if not ret:
                break
            
            frame_count += 1
            if frame_count % fps == 0:
                persons = person_cascade.detectMultiScale(frame, 1.1, 3)
                
                if len(persons) > 1:
                    badge_time = self.find_closest_access_event(frame_count / fps)
                    if badge_time:
                        person_detections.append({
                            'timestamp_seconds': frame_count / fps,
                            'person_count': len(persons),
                            'badge_swipe_count': 1,
                            'tailgating_suspected': len(persons) > 1,
                            'closest_badge_event': badge_time
                        })
        
        cap.release()
        return person_detections
    
    def find_closest_access_event(self, video_timestamp):
        video_dt = self.base_time + __import__('datetime').timedelta(seconds=video_timestamp)
        closest = None
        min_diff = float('inf')
        
        for log in self.access_logs:
            diff = abs((log['timestamp'] - video_dt).total_seconds())
            if diff < min_diff:
                min_diff = diff
                closest = log
        
        return closest if min_diff < 5 else None
    
    def correlate_physical_digital(self, suspicious_events):
        correlations = []
        
        for event in suspicious_events:
            time_window = __import__('datetime').timedelta(minutes=5)
            event_time = event.get('timestamp')
            
            matching_digital = []
            for log in self.access_logs:
                if abs((log['timestamp'] - event_time).total_seconds()) <= time_window.total_seconds():
                    matching_digital.append(log)
            
            correlations.append({
                'physical_event': event,
                'matching_access_logs': matching_digital,
                'badge_used': event.get('badge_id'),
                'anomaly': len(matching_digital) > 1 or event.get('tailgating_suspected')
            })
        
        return correlations

设备投放取证

USB 投放设备分析:

USB 设备取证检查流程:

1. 物理检查:
   - 记录设备外观、品牌、型号、序列号
   - 拍照固定证据
   - 检查是否有物理损坏或改装痕迹

2. 写保护后连接取证工作站:
   dmesg | tail -20
   lsusb -v
   fdisk -l /dev/sdX

3. 创建磁盘镜像:
   dd if=/dev/sdX of=/evidence/usb_image.dd bs=4M status=progress
   sha256sum /evidence/usb_image.dd > /evidence/usb_image.dd.sha256

4. 挂载分析:
   mount -o ro /evidence/usb_image.dd /mnt/usb_analysis
   find /mnt/usb_analysis -type f -exec file {} \;
   find /mnt/usb_analysis -type f -exec md5sum {} \;

5. 检查恶意配置:
   - autorun.inf / autorun.sh 自动执行文件
   - 伪装的固件更新文件
   - BadUSB HID 注入脚本
   - 隐藏的分区或数据区

恶意充电站检测:

import usb.core
import usb.util

def detect_malicious_charging_station():
    devices = usb.core.find(find_all=True)
    
    suspicious_devices = []
    for dev in devices:
        device_info = {
            'vendor_id': hex(dev.idVendor),
            'product_id': hex(dev.idProduct),
            'manufacturer': usb.util.get_string(dev, dev.iManufacturer) if dev.iManufacturer else 'unknown',
            'product': usb.util.get_string(dev, dev.iProduct) if dev.iProduct else 'unknown',
            'serial': usb.util.get_string(dev, dev.iSerialNumber) if dev.iSerialNumber else 'unknown',
            'device_class': dev.bDeviceClass,
            'interfaces': []
        }
        
        for cfg in dev:
            for intf in cfg:
                device_info['interfaces'].append({
                    'class': intf.bInterfaceClass,
                    'subclass': intf.bInterfaceSubClass,
                    'protocol': intf.bInterfaceProtocol
                })
        
        is_data_device = any(
            intf['class'] in [0x08, 0x03, 0x02]
            for intf in device_info['interfaces']
        )
        is_known_charger = device_info['device_class'] == 0x00 and len(device_info['interfaces']) == 0
        
        if is_data_device:
            device_info['risk'] = 'HIGH - USB charging port with data capabilities detected'
            suspicious_devices.append(device_info)
        elif not is_known_charger:
            device_info['risk'] = 'MEDIUM - Unusual device characteristics'
            suspicious_devices.append(device_info)
    
    return suspicious_devices

物理证据保全与数字证据关联

物理社工取证的独特挑战在于物理证据与数字证据的时空关联。以下框架用于系统化地建立这种关联:

物理证据类型采集方法关联数字证据时间关联窗口
门禁刷卡记录门禁系统导出VPN登录/网络接入日志±5分钟
监控录像DVR/NVR提取终端登录事件/文件访问日志±10分钟
USB设备投放物理扣押+镜像USB连接事件/驱动安装日志±30分钟
伪装身份访客记录前台登记簿内部系统账号创建/权限变更±24小时
无线接入点设备扣押WLC日志/DHCP分配记录±1小时
物理文件/便签拍照+扣押对应数字文档/密码使用记录无固定窗口

0x09 内部威胁关联分析

内部威胁与社会工程学关联模型

社会工程学攻击的最终目标往往是建立内部协助者(insider),无论是被欺骗的无意内部人员还是被策反的恶意内部人员。内部威胁取证需要建立以下关联模型:

员工行为异常检测框架:

import numpy as np
from datetime import datetime, timedelta

class InsiderThreatDetector:
    def __init__(self, user_activity_logs, baseline_days=90):
        self.logs = user_activity_logs
        self.baseline_days = baseline_days
    
    def detect_access_anomalies(self, user_id):
        user_logs = [l for l in self.logs if l['user_id'] == user_id]
        
        baseline_logs = [l for l in user_logs 
                        if (datetime.now() - l['timestamp']).days > self.baseline_days]
        recent_logs = [l for l in user_logs 
                      if (datetime.now() - l['timestamp']).days <= self.baseline_days]
        
        baseline_resources = set(l['resource_accessed'] for l in baseline_logs)
        recent_resources = set(l['resource_accessed'] for l in recent_logs)
        
        new_resources = recent_resources - baseline_resources
        sensitive_keywords = ['password', 'credential', 'secret', 'key', 'token',
                            'financial', 'hr', 'salary', 'customer_data', 'pii']
        
        sensitive_new_access = []
        for resource in new_resources:
            if any(kw in resource.lower() for kw in sensitive_keywords):
                sensitive_new_access.append(resource)
        
        baseline_hours = {}
        for log in baseline_logs:
            hour = log['timestamp'].hour
            baseline_hours.setdefault(hour, 0)
            baseline_hours[hour] += 1
        
        recent_hours = {}
        for log in recent_logs:
            hour = log['timestamp'].hour
            recent_hours.setdefault(hour, 0)
            recent_hours[hour] += 1
        
        off_hours_baseline = sum(baseline_hours.get(h, 0) for h in [0,1,2,3,4,5,23])
        off_hours_recent = sum(recent_hours.get(h, 0) for h in [0,1,2,3,4,5,23])
        
        return {
            'new_sensitive_resources': sensitive_new_access,
            'off_hours_baseline': off_hours_baseline,
            'off_hours_recent': off_hours_recent,
            'off_hours_spike': off_hours_recent > off_hours_baseline * 2,
            'total_new_resources': len(new_resources)
        }
    
    def detect_data_exfiltration(self, user_id):
        user_logs = [l for l in self.logs if l['user_id'] == user_id]
        
        large_transfers = [l for l in user_logs 
                         if l.get('transfer_size_mb', 0) > 100]
        
        external_uploads = [l for l in user_logs 
                          if l.get('destination_type') == 'external' 
                          and l.get('action') in ['upload', 'send', 'share']]
        
        archive_creations = [l for l in user_logs 
                           if l.get('action') in ['compress', 'zip', 'encrypt']
                           and l.get('file_count', 0) > 10]
        
        usb_copies = [l for l in user_logs 
                     if l.get('destination_type') == 'usb' 
                     and l.get('action') == 'copy']
        
        printing_events = [l for l in user_logs 
                         if l.get('action') == 'print'
                         and l.get('page_count', 0) > 50]
        
        exfil_indicators = {
            'large_transfers': len(large_transfers),
            'external_uploads': len(external_uploads),
            'archive_creations': len(archive_creations),
            'usb_copies': len(usb_copies),
            'bulk_printing': len(printing_events),
            'total_indicators': len(large_transfers) + len(external_uploads) + 
                              len(archive_creations) + len(usb_copies) + len(printing_events)
        }
        
        return exfil_indicators
    
    def detect_social_engineering_susceptibility(self, user_id):
        user_logs = [l for l in self.logs if l['user_id'] == user_id]
        
        phishing_clicks = [l for l in user_logs 
                         if l.get('event_type') == 'phishing_link_clicked']
        password_resets = [l for l in user_logs 
                         if l.get('event_type') == 'password_reset' 
                         and l.get('trigger') == 'user_request']
        unusual_grants = [l for l in user_logs 
                        if l.get('event_type') == 'permission_grant'
                        and l.get('authorized_by') != 'admin']
        
        return {
            'phishing_click_count': len(phishing_clicks),
            'self_service_resets': len(password_resets),
            'unauthorized_grants': len(unusual_grants),
            'susceptibility_score': min((len(phishing_clicks) * 3 + 
                                        len(password_resets) * 2 + 
                                        len(unusual_grants) * 5) / 20, 1.0)
        }

UEBA 应用与 Sigma 规则

内部威胁检测 Sigma 规则集:

title: Suspicious Off-Hours Access to Sensitive Resources
id: a1b2c3d4-5678-90ab-cdef-111111111111
status: experimental
description: 检测非工作时间对敏感资源的异常访问行为
author: SOC Team
date: 2026/07/02
logsource:
    category: process_creation
    product: windows
detection:
    selection:
        ParentImage|endswith:
            - '\powershell.exe'
            - '\cmd.exe'
            - '\wscript.exe'
            - '\cscript.exe'
        CommandLine|contains:
            - 'password'
            - 'credential'
            - 'export'
            - 'download'
            - 'compress'
    timeframe:
        field: Timestamp
        condition:
            lt: '06:00:00'
            gt: '22:00:00'
    condition: selection and timeframe
level: high
tags:
    - attack.exfiltration
    - attack.credential_access
    - insider_threat
title: Unusual Volume of File Access by Single User
id: a1b2c3d4-5678-90ab-cdef-222222222222
status: experimental
description: 检测单用户在短时间内访问大量文件的行为
author: SOC Team
date: 2026/07/02
logsource:
    category: file_event
    product: windows
detection:
    selection:
        TargetFilename|contains:
            - '\\Finance\\'
            - '\\HR\\'
            - '\\Legal\\'
            - '\\Confidential\\'
    condition: selection | count(TargetFilename) by User > 100
    timeframe: 1h
level: medium
tags:
    - attack.collection
    - insider_threat

离职员工风险评估

def assess_departing_employee_risk(employee_id, departure_type, access_history):
    risk_factors = {
        'departure_type': {
            'voluntary': 3,
            'involuntary': 7,
            'retirement': 1,
            'termination_for_cause': 9
        },
        'access_level': {
            'standard': 2,
            'elevated': 5,
            'admin': 8,
            'domain_admin': 10
        },
        'data_sensitivity': {
            'general': 1,
            'confidential': 5,
            'restricted': 8,
            'top_secret': 10
        },
        'recent_behavior': {
            'normal': 0,
            'policy_violations': 4,
            'access_anomalies': 6,
            'data_exfil_indicators': 9
        }
    }
    
    score = 0
    score += risk_factors['departure_type'].get(departure_type, 5)
    
    max_access = max(access_history, key=lambda x: x.get('access_level_score', 0))
    score += risk_factors['access_level'].get(max_access.get('access_level', 'standard'), 2)
    
    if access_history[-30:]:
        recent_anomalies = [a for a in access_history[-30:] if a.get('anomaly')]
        score += min(len(recent_anomalies) * 2, 10)
    
    total_score = min(score / 30, 1.0)
    
    return {
        'employee_id': employee_id,
        'risk_score': total_score,
        'risk_level': 'critical' if total_score > 0.8 else 'high' if total_score > 0.6 else 'medium' if total_score > 0.3 else 'low',
        'recommended_actions': get_recommended_actions(total_score)
    }

def get_recommended_actions(risk_score):
    if risk_score > 0.8:
        return ['immediate_access_revocation', 'escort_from_premises', 'device_confiscation', 'full_audit_log_review']
    elif risk_score > 0.6:
        return ['access_revocation_within_24h', 'manager_notification', 'data_access_audit']
    elif risk_score > 0.3:
        return ['standard_offboarding', 'knowledge_transfer_session', 'monitoring_during_notice_period']
    else:
        return ['standard_offboarding_procedure']

0x0A 证据强度分层与关联分析

证据强度分类体系

社会工程学取证中的证据需要按可靠性进行分层,以便在报告和法律诉讼中准确表述:

证据层级定义示例法律效力报告标注
L1-确认恶意可直接证明恶意意图和行为钓鱼邮件源码、攻击者自白、C2通信日志直接证据🔴 CONFIRMED MALICIOUS
L2-高度可疑强烈暗示恶意但需佐证的间接证据域名注册信息异常、基础设施关联、话术模式匹配间接证据/情况证据🟠 HIGHLY SUSPICIOUS
L3-需要关注存在异常但无法直接归因的弱信号异常登录时间、新设备指纹、非典型通信模式需进一步调查🟡 REQUIRES ATTENTION
L4-信息性提供上下文但无直接证明价值WHOIS隐私保护、通用基础设施、公开工具使用背景信息🔵 INFORMATIONAL

多源证据关联方法

from collections import defaultdict
from datetime import datetime

class EvidenceCorrelator:
    def __init__(self):
        self.evidence_items = []
        self.correlation_graph = defaultdict(set)
    
    def add_evidence(self, evidence_type, source, indicator, timestamp, confidence_level):
        item = {
            'id': len(self.evidence_items),
            'type': evidence_type,
            'source': source,
            'indicator': indicator,
            'timestamp': timestamp,
            'confidence': confidence_level,
            'correlated_with': set()
        }
        self.evidence_items.append(item)
        return item['id']
    
    def correlate_by_indicator(self):
        indicator_map = defaultdict(list)
        for item in self.evidence_items:
            indicator_map[item['indicator']].append(item['id'])
        
        for indicator, ids in indicator_map.items():
            if len(ids) > 1:
                for i in range(len(ids)):
                    for j in range(i+1, len(ids)):
                        self.evidence_items[ids[i]]['correlated_with'].add(ids[j])
                        self.evidence_items[ids[j]]['correlated_with'].add(ids[i])
                        self.correlation_graph[ids[i]].add(ids[j])
                        self.correlation_graph[ids[j]].add(ids[i])
    
    def correlate_by_time_window(self, window_minutes=60):
        sorted_items = sorted(self.evidence_items, key=lambda x: x['timestamp'])
        
        for i in range(len(sorted_items)):
            for j in range(i+1, len(sorted_items)):
                time_diff = (sorted_items[j]['timestamp'] - sorted_items[i]['timestamp']).total_seconds()
                if time_diff > window_minutes * 60:
                    break
                if sorted_items[i]['source'] != sorted_items[j]['source']:
                    self.evidence_items[sorted_items[i]['id']]['correlated_with'].add(sorted_items[j]['id'])
                    self.evidence_items[sorted_items[j]['id']]['correlated_with'].add(sorted_items[i]['id'])
    
    def generate_attribution_assessment(self):
        clusters = self._find_connected_components()
        
        assessments = []
        for cluster in clusters:
            cluster_items = [self.evidence_items[i] for i in cluster]
            
            l1_count = sum(1 for i in cluster_items if i['confidence'] == 'L1')
            l2_count = sum(1 for i in cluster_items if i['confidence'] == 'L2')
            
            if l1_count >= 2:
                attribution_confidence = 'HIGH'
            elif l1_count >= 1 and l2_count >= 3:
                attribution_confidence = 'MEDIUM-HIGH'
            elif l2_count >= 3:
                attribution_confidence = 'MEDIUM'
            else:
                attribution_confidence = 'LOW'
            
            assessments.append({
                'cluster_size': len(cluster),
                'evidence_types': list(set(i['type'] for i in cluster_items)),
                'l1_evidence_count': l1_count,
                'l2_evidence_count': l2_count,
                'attribution_confidence': attribution_confidence,
                'indicators': list(set(i['indicator'] for i in cluster_items))
            })
        
        return assessments
    
    def _find_connected_components(self):
        visited = set()
        components = []
        
        def dfs(node, component):
            visited.add(node)
            component.append(node)
            for neighbor in self.correlation_graph.get(node, set()):
                if neighbor not in visited:
                    dfs(neighbor, component)
        
        for i in range(len(self.evidence_items)):
            if i not in visited:
                component = []
                dfs(i, component)
                if len(component) > 1:
                    components.append(component)
        
        return components

IOC 汇总与 STIX/TAXII 共享格式

import json
from datetime import datetime

def generate_stix_bundle(iocs, attack_description):
    stix_objects = []
    
    bundle_id = f"bundle--{__import__('uuid').uuid4()}"
    
    indicator_objects = []
    for ioc in iocs:
        indicator = {
            'type': 'indicator',
            'spec_version': '2.1',
            'id': f"indicator--{__import__('uuid').uuid4()}",
            'created': datetime.utcnow().isoformat() + 'Z',
            'modified': datetime.utcnow().isoformat() + 'Z',
            'name': ioc.get('name', 'SE Attack IOC'),
            'description': ioc.get('description', ''),
            'pattern': ioc.get('stix_pattern', ''),
            'pattern_type': 'stix',
            'valid_from': datetime.utcnow().isoformat() + 'Z',
            'labels': ioc.get('labels', ['malicious-activity']),
            'confidence': ioc.get('confidence', 50)
        }
        indicator_objects.append(indicator)
        stix_objects.append(indicator)
    
    attack_pattern = {
        'type': 'attack-pattern',
        'spec_version': '2.1',
        'id': f"attack-pattern--{__import__('uuid').uuid4()}",
        'created': datetime.utcnow().isoformat() + 'Z',
        'name': 'Social Engineering Attack',
        'description': attack_description,
        'external_references': [
            {
                'source_name': 'mitre-attack',
                'external_id': 'T1566',
                'url': 'https://attack.mitre.org/techniques/T1566/'
            }
        ]
    }
    stix_objects.append(attack_pattern)
    
    bundle = {
        'type': 'bundle',
        'id': bundle_id,
        'objects': stix_objects
    }
    
    return bundle

0x0B 自动化检测与防御

邮件网关检测规则

SpamAssassin 自定义规则集:

/etc/spamassassin/local.cf 追加规则:

header SE_URGENCY_LANGUAGE Subject =~ /(?:urgent|immediate|immediate action|required now|expiring soon)/i
score SE_URGENCY_LANGUAGE 2.0
describe SE_URGENCY_LANGUAGE Subject contains urgency language common in SE attacks

header SE_DISPLAY_URL_MISMATCH eval:check_display_url_mismatch()
score SE_DISPLAY_URL_MISMATCH 3.5
describe SE_DISPLAY_URL_MISMATCH Display URL differs from actual link destination

header SE_NEW_DOMAIN eval:check_sender_domain_age()
score SE_NEW_DOMAIN 2.5
describe SE_NEW_DOMAIN Sender domain registered within last 30 days

header SE_HOMOGYPH_DOMAIN eval:check_homoglyph_domain()
score SE_HOMOGYPH_DOMAIN 4.0
describe SE_HOMOGYPH_DOMAIN Sender domain uses homoglyph characters

header SE_REPLY_MISMATCH eval:check_reply_to_from_mismatch()
score SE_REPLY_MISMATCH 1.5
describe SE_REPLY_MISMATCH Reply-To differs from From address

header SE_SECRECY_LANGUAGE Body =~ /(?:do not (?:tell|share|disclose)|confidential|between us|keep this quiet)/i
score SE_SECRECY_LANGUAGE 2.0
describe SE_SECRECY_LANGUAGE Body contains secrecy demands

header SE_MX_MAILGUN Received =~ /mailgun\.org|sendgrid\.net|amazonses\.com/i
score SE_MX_MAILGUN 0.5
describe SE_MX_MAILGUN Email sent via transactional email service

header SE_NO_DMARC_REJECT eval:check_dmarc_policy_none()
score SE_NO_DMARC_REJECT 1.0
describe SE_NO_DMARC_REJECT Sender domain has DMARC p=none policy

Exchange Online Transport Rules:

New-TransportRule -Name "SE Detection - Urgency + External" `
    -FromScope "NotInOrganization" `
    -SubjectOrBodyContainsWords @("urgent","immediately","ASAP","within 24 hours","account will be closed","immediate verification") `
    -SubjectOrBodyContainsWords @("click here","verify now","confirm identity","update information") `
    -SetSCL 7 `
    -PrependSubject "[SE ALERT] " `
    -GenerateIncidentReport "soc@company.com" `
    -IncidentReportContent "OriginalMail,Headers"

New-TransportRule -Name "SE Detection - New Domain + Financial Keywords" `
    -FromScope "NotInOrganization" `
    -SenderDomainIs (Get-NewlyRegisteredDomains) `
    -SubjectOrBodyContainsWords @("wire transfer","bank account","payment","invoice","update bank","change account") `
    -SetSCL 9 `
    -RedirectMessageTo "finance-security@company.com" `
    -GenerateIncidentReport "soc@company.com"

New-TransportRule -Name "SE Detection - Display Name Spoofing" `
    -FromScope "NotInOrganization" `
    -SenderAddressMatchesPatterns @(Get-ExecNames) `
    -SetSCL 6 `
    -PrependSubject "[SPOOF ALERT] " `
    -GenerateIncidentReport "soc@company.com"

域名相似度检测算法

import math
from collections import Counter

def levenshtein_distance(s1, s2):
    if len(s1) < len(s2):
        return levenshtein_distance(s2, s1)
    if len(s2) == 0:
        return len(s1)
    
    previous_row = range(len(s2) + 1)
    for i, c1 in enumerate(s1):
        current_row = [i + 1]
        for j, c2 in enumerate(s2):
            insertions = previous_row[j + 1] + 1
            deletions = current_row[j] + 1
            substitutions = previous_row[j] + (c1 != c2)
            current_row.append(min(insertions, deletions, substitutions))
        previous_row = current_row
    
    return previous_row[-1]

def jaro_winkler_similarity(s1, s2):
    if s1 == s2:
        return 1.0
    
    len1, len2 = len(s1), len(s2)
    if len1 == 0 or len2 == 0:
        return 0.0
    
    match_distance = max(len1, len2) // 2 - 1
    if match_distance < 0:
        match_distance = 0
    
    s1_matches = [False] * len1
    s2_matches = [False] * len2
    
    matches = 0
    transpositions = 0
    
    for i in range(len1):
        start = max(0, i - match_distance)
        end = min(i + match_distance + 1, len2)
        
        for j in range(start, end):
            if s2_matches[j] or s1[i] != s2[j]:
                continue
            s1_matches[i] = True
            s2_matches[j] = True
            matches += 1
            break
    
    if matches == 0:
        return 0.0
    
    k = 0
    for i in range(len1):
        if not s1_matches[i]:
            continue
        while not s2_matches[k]:
            k += 1
        if s1[i] != s2[k]:
            transpositions += 1
        k += 1
    
    jaro = (matches / len1 + matches / len2 + (matches - transpositions / 2) / matches) / 3
    
    prefix = 0
    for i in range(min(len1, len2, 4)):
        if s1[i] == s2[i]:
            prefix += 1
        else:
            break
    
    winkler = jaro + prefix * 0.1 * (1 - jaro)
    
    return winkler

def detect_suspicious_domains(target_domain, observed_domains):
    target_name = target_domain.split('.')[0]
    target_tld = '.'.join(target_domain.split('.')[1:])
    
    results = []
    for domain in observed_domains:
        obs_name = domain.split('.')[0]
        obs_tld = '.'.join(domain.split('.')[1:])
        
        edit_dist = levenshtein_distance(target_name, obs_name)
        jw_sim = jaro_winkler_similarity(target_name, obs_name)
        
        homoglyph_chars = sum(1 for a, b in zip(target_name, obs_name) if a != b and ord(a) != ord(b))
        
        risk_score = 0
        if edit_dist <= 2:
            risk_score += 40
        if jw_sim > 0.85:
            risk_score += 30
        if homoglyph_chars > 0:
            risk_score += 20
        if obs_tld != target_tld:
            risk_score += 10
        
        results.append({
            'domain': domain,
            'edit_distance': edit_dist,
            'jaro_winkler': jw_sim,
            'homoglyph_chars': homoglyph_chars,
            'risk_score': min(risk_score, 100),
            'classification': 'confirmed_squat' if risk_score >= 70 else 'likely_squat' if risk_score >= 40 else 'monitor'
        })
    
    return sorted(results, key=lambda x: x['risk_score'], reverse=True)

SOAR 集成方案

playbook: SE_Incident_Response
trigger:
    - condition: email_alert
    - condition: user_report

steps:
    step_1_extract_iocs:
        action: extract_email_headers
        input: "{{trigger.raw_email}}"
        output: ioc_list
    
    step_2_enrich_domains:
        action: domain_enrichment
        input: "{{step_1_extract_iocs.domains}}"
        tools:
            - whois_lookup
            - dns_resolution
            - vt_query
            - urlscan_submit
        output: enriched_domains
    
    step_3_check_reputation:
        action: reputation_check
        input: "{{step_2_enrich_domains}}"
        sources:
            - virustotal
            - abuseipdb
            - spamhaus
            - googlesafebrowsing
        output: reputation_results
    
    step_4_classify_threat:
        action: threat_classification
        input:
            email_analysis: "{{step_1_extract_iocs}}"
            domain_analysis: "{{step_2_enrich_domains}}"
            reputation: "{{step_3_check_reputation}}"
        rules:
            - if: "reputation.malicious_count >= 2"
              then: "CRITICAL"
            - if: "domain_analysis.new_domain and email_analysis.urgency_detected"
              then: "HIGH"
            - if: "domain_analysis.homoglyph_detected"
              then: "HIGH"
            - default: "MEDIUM"
        output: threat_level
    
    step_5_containment:
        action: conditional
        condition: "{{step_4_classify_threat.threat_level}} in ['CRITICAL', 'HIGH']"
        then:
            - action: block_sender_domain
              target: email_gateway
            - action: quarantine_similar_emails
              search: "{{step_1_extract_iocs.template_fingerprint}}"
            - action: reset_affected_user_credentials
              users: "{{trigger.affected_users}}"
            - action: create_incident_ticket
              system: servicenow
              priority: "{{step_4_classify_threat.threat_level}}"
    
    step_6_notification:
        action: notify
        channels:
            - slack: "#security-incidents"
            - email: "soc-team@company.com"
        template: se_incident_summary

Sigma 规则集:社会工程学检测

title: Suspicious Email Forwarding Rule Created
id: b1c2d3e4-5678-90ab-cdef-333333333333
status: experimental
description: 检测邮箱中创建的可疑转发规则,常见于BEC攻击后的持久化
author: SOC Team
date: 2026/07/02
logsource:
    product: microsoft365
    service: exchange
    category: audit
detection:
    selection:
        Operation: New-InboxRule
        Parameters|contains:
            - 'ForwardTo'
            - 'RedirectTo'
            - 'ForwardAsAttachmentTo'
    filter_internal:
        ForwardTo|endswith: '@company.com'
    condition: selection and not filter_internal
level: high
tags:
    - attack.collection
    - attack.t1114.003
    - bec
    - social_engineering
title: Phishing Kit Infrastructure Detection
id: c2d3e4f5-6789-01ab-cdef-444444444444
status: experimental
description: 通过Web服务器指纹检测已知的钓鱼工具包基础设施
author: SOC Team
date: 2026/07/02
logsource:
    category: proxy
    product: web_proxy
detection:
    selection_url:
        cs-uri|contains:
            - '/.well-known/captcha/'
            - '/gate.php'
            - '/login.php?cmd='
            - '/process.php?token='
    selection_headers:
        c-useragent|contains:
            - 'Evilginx'
            - 'Modlishka'
            - 'Muraena'
    selection_response:
        sc-status: 200
        cs-uri|contains|all:
            - 'login'
            - 'verify'
            - 'update'
            - 'secure'
    condition: selection_url or selection_headers or selection_response
level: high
tags:
    - attack.credential_access
    - attack.t1566
    - phishing
    - infrastructure

0x0C 公开案例分析

案例一:2023 MGM Resorts 社会工程学攻击

攻击概述:

2023 年 9 月,MGM Resorts International 遭受了一次大规模社会工程学攻击,导致其赌场运营系统瘫痪超过 3 天,造成约 $1 亿美元的损失。攻击者为 Scattered Spider(又名 UNC3944)组织。

攻击链重建:

时间线(UTC):
2023-09-10 14:00 - 攻击者通过LinkedIn收集MGM员工信息
2023-09-10 16:30 - 发现目标IT员工的个人信息
2023-09-10 17:00 - 致电MGM IT服务台,冒充该员工
2023-09-10 17:15 - 成功重置员工MFA/密码(Vishing攻击)
2023-09-10 18:00 - 使用窃取的凭证访问Okta管理控制台
2023-09-10 19:30 - 创建新的管理员账户
2023-09-10 21:00 - 部署LockBit 3.0勒索软件
2023-09-11 02:00 - 酒店系统(房卡/POS/预订)全面瘫痪
2023-09-13 08:00 - MGM宣布系统中断,拒绝支付赎金
2023-09-17 - 系统开始逐步恢复

取证发现:

取证维度发现证据强度
社工入口Vishing攻击IT服务台,利用公开个人信息验证身份🔴 L1-确认恶意
凭证窃取通过社会工程学重置合法员工凭证🔴 L1-确认恶意
横向移动通过Okta管理控制台获取广泛系统访问权限🔴 L1-确认恶意
数据外传通过Mega.nz和Tor网络外传150GB数据🟠 L2-高度可疑
OSINT来源攻击者使用的员工信息来自LinkedIn公开资料🟠 L2-高度可疑

IOC 汇总:

domains:
    - scattered-spider-c2.xyz
    - lockbit-pay-onion.link
ips:
    - 185.229.191.xx (C2)
    - 45.141.152.xx (exfiltration)
email_addresses:
    - fake_it_support@tempmail.org
phone_numbers:
    - VoIP numbers used for vishing
tools_used:
    - Social engineering toolkit
    - LockBit 3.0 ransomware
    - Cobalt Strike beacons

经验教训:

  1. IT 服务台的 MFA 重置流程缺乏多因素身份验证 — 仅凭个人信息即可重置
  2. LinkedIn 等社交平台上的员工信息过于详细,降低了攻击者的侦察成本
  3. Okta 管理控制台缺乏额外的访问控制层(如硬件安全密钥)
  4. 缺乏对特权账户的异常行为监控
  5. 事件响应计划中未充分考虑完全系统瘫痪的场景

案例二:2020 Twitter 比特币欺诈事件

攻击概述:

2020 年 7 月 15 日,多个高知名度 Twitter 账号(包括 Elon Musk、Jeff Bezos、Barack Obama、Kanye West 等)被黑客入侵并发布比特币欺诈推文。攻击者通过社会工程学手段获取了 Twitter 内部工具访问权限。

攻击链重建:

时间线(UTC):
2020-07-14 - 攻击者在多个 Discord/Telegram 频道宣传"Twitter内部工具访问"
2020-07-14 - 一名Twitter员工收到伪装为IT支持的短信
2020-07-14 20:00 - 员工点击钓鱼链接,输入公司VPN凭证
2020-07-14 20:30 - 攻击者使用窃取的VPN凭证访问Twitter内网
2020-07-14 21:00 - 访问Twitter内部支持工具(God Mode / Customer Experience)
2020-07-14 21:30 - 利用内部工具重置目标账号密码
2020-07-15 00:00 - 开始在被盗账号上发布比特币欺诈推文
2020-07-15 01:00 - Twitter检测到异常并禁用相关账号
2020-07-15 02:00 - 开始全面调查

取证发现:

取证维度发现证据强度
初始入口鱼叉式钓鱼短信(Smishing)针对Twitter员工🔴 L1-确认恶意
凭证窃取克隆的Twitter VPN登录页面🔴 L1-确认恶意
内部工具滥用使用合法内部工具执行未授权操作🔴 L1-确认恶意
Discord招募攻击者在Discord上招募内部协助者🟠 L2-高度可疑
加密货币追踪比特币钱包地址关联到已知诈骗集群🟠 L2-高度可疑

IOC 汇总:

domains:
    - twitter-verify-support.com (VPN phishing page)
    - twitter-login-secure.net (secondary phishing)
ips:
    - 104.248.xx.xx (phishing hosting)
bitcoin_addresses:
    - bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh
    - 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
discord_servers:
    - Server ID: 7xxxxxxxxx (recruitment server)
telegram_channels:
    - @twitter_leaked_access
internal_tools_abused:
    - God Mode (account management)
    - Customer Experience (account recovery)

经验教训:

  1. 内部工具(God Mode)的访问控制过于宽松,缺乏最小权限原则
  2. 员工对社会工程学攻击(特别是 Smishing)的安全意识不足
  3. VPN 登录页面缺乏有效的钓鱼检测机制
  4. 内部工具的操作审计和异常检测存在延迟
  5. 高知名度账号缺乏额外的保护层级
  6. 攻击者利用社交媒体平台本身作为攻击渠道,形成闭环

0x0D 参考资料

  1. Cialdini, R.B. (2009). Influence: Science and Practice. Pearson Education. - 社会工程学六大原则的理论基础,理解攻击者如何利用人类心理弱点。

  2. Hadnagy, C. (2018). Social Engineering: The Science of Human Hacking. Wiley. - 全面的社会工程学攻击方法论和防御策略参考。

  3. Verizon. (2024). 2024 Data Breach Investigations Report (DBIR). https://www.verizon.com/business/resources/reports/dbir/ - 年度数据泄露调查报告,包含社会工程学攻击统计数据。

  4. MITRE. (2024). ATT&CK Enterprise Matrix - Social Engineering Techniques. https://attack.mitre.org/tactics/TA0001/ - MITRE ATT&CK 框架中社会工程学相关技术的官方映射。

  5. FBI IC3. (2024). Business Email Compromise (BEC) - The $29 Billion Scam. https://www.ic3.gov/Media/PDF/2024/2024IC3Report.pdf - FBI 互联网犯罪投诉中心关于 BEC 攻击的年度报告。

  6. Anti-Phishing Working Group (APWG). (2024). Phishing Activity Trends Report. https://apwg.org/trendsreports/ - APWG 季度钓鱼攻击趋势分析报告。

  7. NIST. (2023). SP 800-61r2 Computer Security Incident Handling Guide. https://csrc.nist.gov/publications/detail/sp/800-61/rev-2/final - NIST 事件处理指南,包含社会工程学事件响应流程。

  8. SANS Institute. (2024). SEC504: Hacker Tools, Techniques, and Incident Handling. https://www.sans.org/cyber-security-courses/hacker-tools-incident-handling/ - SANS 安全培训课程中关于社会工程学取证的方法论。

  9. MGM Resorts International. (2023). SEC 8-K Filing - Cybersecurity Incident Disclosure. https://www.sec.gov/ - MGM 关于 2023 年社会工程学攻击事件的官方披露文件。

  10. Twitter. (2020). Update on Recent Account Compromises. https://blog.twitter.com/en_us/topics/company/2020/update-on-recent-account-compromises - Twitter 关于 2020 年大规模账号被盗事件的官方声明。

  11. Europol. (2024). Internet Organised Crime Threat Assessment (IOCTA). https://www.europol.europa.eu/activities-services/main-reports/european-union-internet-organised-crime-threat-assessment-iocta-2024 - 欧盟刑警组织关于互联网有组织犯罪的威胁评估。

  12. CrowdStrike. (2024). Global Threat Report 2024 - Social Engineering as Initial Access Vector. https://www.crowdstrike.com/resources/reports/global-threat-report/ - CrowdStrike 全球威胁报告中关于社会工程学作为初始访问向量的分析。