Advanced IP Tracing Techniques: A Technical Deep Dive for Threat Hunters

Comprehensive methodologies for tracking scammers, threat actors, and malicious infrastructure


The cat-and-mouse game between defenders and attackers has never been more complex. With threat actors leveraging cloud infrastructure, bulletproof hosting, fast-flux DNS, and rotating command-and-control (C2) servers, traditional IP tracing methods are insufficient. This comprehensive guide explores both established techniques and cutting-edge approaches for attribution and infrastructure hunting.

Github repo: https://github.com/Bodhi8/threattracer


Understanding the Attribution Challenge

IP addresses represent the most volatile element in threat intelligence. According to Datadog’s threat intelligence documentation, IP addresses often change reputations within a 24-hour cycle due to their dynamic nature in residential and mobile networks.

The 2025 IBM X-Force Threat Intelligence Index reveals critical shifts:

  • 30% of intrusions now begin with valid account credentials
  • 84% increase in infostealer malware delivered via phishing
  • Threat actors increasingly abuse cloud hosting services for campaigns

These trends mean IP-based tracking must be combined with behavioral, cryptographic, and infrastructure-level analysis.

The Pyramid of Pain

When we use techniques that fingerprint adversary infrastructure and tools rather than easily-changed IOCs like IPs and domains, we move higher on David Bianco’s Pyramid of Pain — increasing the cost for attackers to evade detection.

/\
                   /  \
                  / TTP \           ← Hardest to change
                 /--------\
                /  Tools   \
               /------------\
              / Network/Host \
             /  Artifacts     \
            /------------------\
           /  Domain Names      \
          /----------------------\
         /     IP Addresses       \  ← Easiest to change
        /--------------------------\

Passive DNS Analysis

Passive DNS (pDNS) collects historical DNS resolution data without querying authoritative servers directly. This provides invaluable intelligence about domain-to-IP relationships over time.

How Passive DNS Works

Unlike active DNS queries, passive DNS sensors observe DNS traffic at recursive resolvers, collecting:

  • Domain names queried
  • Resolved IP addresses
  • First-seen and last-seen timestamps
  • Record types (A, AAAA, CNAME, MX, NS, TXT)

Key Use Cases

1. Infrastructure Pivoting

When you have a known malicious IP, passive DNS reveals all domains that have ever pointed to it:

# Example: Query passive DNS for historical resolutions
# Using Farsight DNSDB API
curl -H "X-API-Key: $DNSDB_API_KEY" \
  "https://api.dnsdb.info/lookup/rdata/ip/192.0.2.1?limit=100"

2. Tracking Domain Migrations

Threat actors frequently rotate infrastructure. Passive DNS shows when domains moved between IPs:

example-malware.com
├── 2024-01-15: 192.0.2.1 (Hosting Provider A)
├── 2024-03-22: 198.51.100.5 (Bulletproof Host B)
└── 2024-06-10: 203.0.113.42 (Cloud Provider C)

3. Identifying Shared Infrastructure

Multiple campaigns often share infrastructure. Passive DNS reveals these connections:

# Python example: Find domains sharing an IP
import requests

def find_cohosted_domains(ip_address, api_key):
    """Query passive DNS for all domains resolving to an IP"""
    headers = {"X-API-Key": api_key}
    url = f"https://api.passivetotal.org/v2/dns/passive?query={ip_address}"
    
    response = requests.get(url, headers=headers)
    results = response.json()
    
    domains = set()
    for record in results.get('results', []):
        domains.add(record.get('resolve'))
    
    return domains

# Usage
cohosted = find_cohosted_domains("192.0.2.1", API_KEY)
print(f"Found {len(cohosted)} domains sharing this IP")

Passive DNS Tools and Services

Commercial Options:

  • Farsight DNSDB — Largest passive DNS database
  • PassiveTotal (RiskIQ) — Integrated with threat intel
  • SecurityTrails — Historical DNS records with freemium tier

Free/Freemium Options:

  • VirusTotal — Limited historical data
  • DNSDumpster — Good for quick lookups
  • Spamhaus Passive DNS — Focus on abuse data

Red Flags in Passive DNS Data

When analyzing passive DNS records, watch for:

  • Rapid IP rotation — Multiple IPs in short timeframes suggests fast-flux
  • Low TTL values — Often indicates dynamic infrastructure
  • Shared nameservers — Multiple suspicious domains using same NS
  • Recently registered domains — Combined with suspicious patterns
  • Privacy/proxy registrations — WHOIS privacy across multiple domains

TLS Fingerprinting Deep Dive

TLS fingerprinting identifies clients and servers based on their cryptographic negotiation patterns. Because these patterns are determined by the application’s TLS implementation, they persist even when IPs and domains change.

JA3: Client Fingerprinting

JA3 creates fingerprints from the TLS Client Hello packet by extracting:

  1. SSL/TLS Version
  2. Accepted Cipher Suites
  3. List of Extensions
  4. Elliptic Curves (Supported Groups)
  5. Elliptic Curve Point Formats

JA3 Calculation

import hashlib

def calculate_ja3(client_hello):
    """
    Calculate JA3 fingerprint from TLS Client Hello
    
    JA3 = MD5(SSLVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats)
    """
    # Extract fields from Client Hello
    ssl_version = str(client_hello.version)
    
    # Cipher suites as dash-separated decimals
    ciphers = "-".join([str(c) for c in client_hello.cipher_suites])
    
    # Extensions as dash-separated decimals (excluding GREASE values)
    extensions = "-".join([str(e) for e in client_hello.extensions 
                          if not is_grease(e)])
    
    # Elliptic curves
    curves = "-".join([str(c) for c in client_hello.supported_groups])
    
    # EC point formats
    ec_formats = "-".join([str(f) for f in client_hello.ec_point_formats])
    
    # Build JA3 string
    ja3_string = f"{ssl_version},{ciphers},{extensions},{curves},{ec_formats}"
    
    # Calculate MD5 hash
    ja3_hash = hashlib.md5(ja3_string.encode()).hexdigest()
    
    return ja3_string, ja3_hash

def is_grease(value):
    """Check if value is a GREASE (random) value to be excluded"""
    grease_values = [0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a, 
                    0x5a5a, 0x6a6a, 0x7a7a, 0x8a8a, 0x9a9a,
                    0xaaaa, 0xbaba, 0xcaca, 0xdada, 0xeaea, 0xfafa]
    return value in grease_values

Known Malware JA3 Signatures

Trickbot

  • JA3: 6734f37431670b3ab4292b8f60f29984
  • JA3S: 623de93db17d313345d7ea481e7443cf

Emotet

  • JA3: 4d7a28d6f2263ed61de88ca66eb011e3
  • JA3S: 80b3a14bccc8598a1f3bbe83e71f735f

Cobalt Strike

  • JA3: 72a589da586844d7f0818ce684948eea
  • JA3S: Various depending on configuration

Dridex

  • JA3: 51c64c77e60f3980eea90869b68c58a8

IcedID

  • JA3: a0e9f5d64349fb13191bc781f81f42e1

JA3S: Server Response Fingerprinting

JA3S fingerprints how servers respond to specific clients. The key insight: C2 servers always respond to their malware client in exactly the same way.

def calculate_ja3s(server_hello):
    """
    Calculate JA3S from TLS Server Hello
    
    JA3S = MD5(SSLVersion,Cipher,Extensions)
    """
    ssl_version = str(server_hello.version)
    cipher = str(server_hello.cipher_suite)
    extensions = "-".join([str(e) for e in server_hello.extensions])
    
    ja3s_string = f"{ssl_version},{cipher},{extensions}"
    ja3s_hash = hashlib.md5(ja3s_string.encode()).hexdigest()
    
    return ja3s_string, ja3s_hash

Combining JA3 + JA3S creates a fingerprint of the entire cryptographic negotiation, providing high-confidence identification even when infrastructure changes.

JARM: Active Server Fingerprinting

Unlike passive JA3/JA3S observation, JARM actively probes servers by sending 10 specially crafted TLS Client Hello packets and analyzing the responses.

How JARM Works

  1. Send 10 different TLS Client Hello packets with varying TLS versions, cipher suites, and extensions
  2. Capture Server Hello responses
  3. Extract from each response: TLS version selected, cipher suite selected, and extensions returned
  4. Concatenate and hash the results into a 62-character fingerprint

JARM Fingerprint Structure

The 62-byte fingerprint consists of:

  • First 30 bytes: Fuzzy hash of TLS version and cipher usage
  • Last 32 bytes: SHA-256 of extension usage

Calculating JARM

# Using the official JARM tool
git clone https://github.com/salesforce/jarm.git
cd jarm
python jarm.py target.com

# Output example:
# Domain: target.com
# JARM: 2ad2ad16d2ad2ad00042d42d00042ddb04deffa1705e2edc44cae1ed24a4da

Known C2 JARM Signatures

Cobalt Strike (default) 07d14d16d21d21d00042d41d00041de5fb3038e22f33f93c9a4ce6880e8b63

Metasploit 07d14d16d21d21d07c42d41d00041d24a458a375eef0c576d23a7bab9a9fb1

Mythic 2ad2ad0002ad2ad00042d42d000000ad9bf51cc3f5a1e29eecb81d0c7b06eb

Sliver 2ad2ad16d2ad2ad22c42d42d00042d58c7162162b6a603d3d90a2b76865b53

Important Caveat: JARM fingerprints identify TLS implementation characteristics, not the specific application. A Cobalt Strike JARM is actually Java’s JARM fingerprint. Always correlate with additional indicators.

JA4+: Next-Generation Fingerprinting

JA4+ addresses JA3’s weaknesses (cipher reordering, GREASE values) with a modular, human-readable approach.

JA4 Format: a_b_c

t13d1516h2_8daaf6152771_b0da82dd1658

Breaking this down:

  • t13d — TLS 1.3, with SNI (d=domain present)
  • 15 — Number of cipher suites
  • 16 — Number of extensions
  • h2 — ALPN (h2=HTTP/2, h1=HTTP/1.1)
  • 8daaf6152771 — Truncated SHA256 of sorted ciphers
  • b0da82dd1658 — Truncated SHA256 of sorted extensions

JA4+ Suite Components

  • JA4 — TLS client fingerprinting
  • JA4S — TLS server fingerprinting
  • JA4H — HTTP client fingerprinting
  • JA4L — Light fingerprint (latency/timing)
  • JA4X — X.509 certificate fingerprinting
  • JA4SSH — SSH session fingerprinting

Partial Matching with JA4

JA4’s modular format enables partial matching:

# Track actor who randomizes ciphers but keeps consistent extensions
def track_by_ja4_ac(ja4_fingerprint):
    """Extract a+c parts, ignoring cipher variations"""
    parts = ja4_fingerprint.split('_')
    if len(parts) == 3:
        return f"{parts[0]}_{parts[2]}"  # a_c only
    return None

# Example: GreyNoise tracks internet scanners this way
# Even with changing ciphers, the a_c fingerprint stays consistent

Internet-Wide Scanning and C2 Hunting

Internet scanning platforms index billions of devices, enabling proactive threat hunting for malicious infrastructure.

Core Platforms

Shodan — Device metadata, banners, screenshots

  • Example query: http.favicon.hash:-1234567

Censys — Deep TLS/certificate analysis

  • Example query: services.jarm.fingerprint:07d14d*

FOFA — Strong Asia-Pacific coverage

  • Example query: icon_hash="-1234567"

ZoomEye — Chinese infrastructure visibility

  • Example query: iconhash:"-1234567"

Netlas — Emerging alternative platform

  • Example query: jarm:"2ad2ad16d2ad..."

Hunting Cobalt Strike Infrastructure

Cobalt Strike servers have identifiable characteristics. Here are four methods:

Method 1: Default SSL Certificate Serial

# Shodan query
ssl.cert.serial:146473198

# Censys query
services.tls.certificates.leaf.parsed.serial_number:"146473198"

Method 2: JARM Fingerprint

# Shodan
ssl.jarm:"07d14d16d21d21d00042d41d00041de5fb3038e22f33f93c9a4ce6880e8b63"

# Censys
services.jarm.fingerprint:"07d14d16d21d21d00042d41d00041de5*"

Method 3: HTTP Response Characteristics

# Look for default 404 response
http.html:"404 Not Found" http.headers.content-length:0

# Beacon configuration indicators
http.html_hash:363333310

Method 4: Certificate Field Analysis

# Hunt for Cobalt Strike by certificate patterns
def hunt_cobalt_strike_certs(censys_api):
    """Hunt for CS servers using certificate characteristics"""
    queries = [
        # Default self-signed pattern
        'services.tls.certificates.leaf.parsed.subject.common_name:"Major Cobalt Strike"',
        
        # Known serial in certificate
        'services.tls.certificates.leaf.parsed.serial_number:"146473198"',
        
        # Suspicious self-signed with specific validity period
        'services.tls.certificates.leaf.parsed.issuer.common_name:/^localhost$/'
    ]
    
    results = []
    for query in queries:
        results.extend(censys_api.search(query))
    
    return deduplicate(results)

Hunting Open Directories

Threat actors often expose staging servers with open directory listings:

# Shodan queries for open directories with malicious tools
http.html:"Index of /" http.html:"mimikatz"
http.html:"Index of /" http.html:"beacon"
http.html:"Index of /" http.html:".exe" http.html:".ps1"

# Censys approach
services.http.response.body:"Index of /" AND 
services.http.response.body:("mimikatz" OR "beacon.exe" OR "payload")

Building Complex Queries

Combine multiple indicators for high-fidelity detection:

def build_hunt_query(indicators):
    """Build complex Shodan query from multiple indicators"""
    query_parts = []
    
    if indicators.get('jarm'):
        query_parts.append(f'ssl.jarm:"{indicators["jarm"]}"')
    
    if indicators.get('cert_serial'):
        query_parts.append(f'ssl.cert.serial:{indicators["cert_serial"]}')
    
    if indicators.get('favicon_hash'):
        query_parts.append(f'http.favicon.hash:{indicators["favicon_hash"]}')
    
    if indicators.get('html_hash'):
        query_parts.append(f'http.html_hash:{indicators["html_hash"]}')
    
    if indicators.get('asn'):
        query_parts.append(f'asn:AS{indicators["asn"]}')
    
    # Exclude known good infrastructure
    if indicators.get('exclude_orgs'):
        for org in indicators['exclude_orgs']:
            query_parts.append(f'-org:"{org}"')
    
    return ' '.join(query_parts)

# Example: Hunt for suspicious C2 infrastructure
query = build_hunt_query({
    'jarm': '2ad2ad16d2ad2ad00042d42d00042d',
    'exclude_orgs': ['Microsoft', 'Amazon', 'Google'],
})

Favicon Hash Analysis

Favicons provide surprisingly effective fingerprinting because:

  • Threat actors often reuse default icons
  • Phishing sites clone target company favicons
  • C2 frameworks ship with identifiable favicons

MurmurHash3 Calculation

Shodan uses MurmurHash3 on base64-encoded favicon data:

import mmh3
import requests
import base64
import re

def calculate_favicon_hash(url):
    """
    Calculate Shodan-compatible favicon hash
    
    Important: Shodan doesn't hash raw bytes, but a modified
    base64-encoded version with specific newline formatting
    """
    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()
        
        # Step 1: Base64 encode
        b64_data = base64.b64encode(response.content)
        
        # Step 2: Convert to string
        b64_string = b64_data.decode('utf-8')
        
        # Step 3: Insert newlines every 76 characters (RFC 2045)
        # AND add trailing newline
        with_newlines = re.sub("(.{76})", "\\1\n", b64_string, 0, re.DOTALL)
        if not with_newlines.endswith('\n'):
            with_newlines += '\n'
        
        # Step 4: Calculate MurmurHash3
        favicon_hash = mmh3.hash(with_newlines)
        
        return favicon_hash
        
    except Exception as e:
        print(f"Error: {e}")
        return None

# Calculate hash for a target
hash_value = calculate_favicon_hash("https://example.com/favicon.ico")
print(f"Shodan query: http.favicon.hash:{hash_value}")

One-Liner Alternative

python3 -c "
import mmh3,requests,codecs,sys
r=requests.get(sys.argv[1])
print(mmh3.hash(codecs.encode(r.content,'base64')))
" "https://example.com/favicon.ico"

Known C2 Favicon Hashes

Cobalt Strike (default): -1018312697

  • Query: http.favicon.hash:-1018312697

Metasploit: -127886975

  • Query: http.favicon.hash:-127886975

Empire: 298876636

  • Query: http.favicon.hash:298876636

Mythic: 1982692064

  • Query: http.favicon.hash:1982692064

Hunting Phishing Infrastructure

Use favicon hashes to find phishing sites impersonating legitimate brands:

def hunt_phishing_sites(legitimate_favicon_url, excluded_orgs):
    """Find potential phishing sites using cloned favicons"""
    
    # Calculate hash of legitimate site's favicon
    target_hash = calculate_favicon_hash(legitimate_favicon_url)
    
    # Build Shodan query
    query = f'http.favicon.hash:{target_hash}'
    
    # Exclude legitimate infrastructure
    for org in excluded_orgs:
        query += f' -org:"{org}"'
    
    # Exclude hosting on legitimate domains
    query += ' -hostname:*.microsoft.com'
    query += ' -hostname:*.office.com'
    
    return query

# Hunt for fake Microsoft login pages
query = hunt_phishing_sites(
    "https://c.s-microsoft.com/favicon.ico",
    ["Microsoft Corporation", "Microsoft Azure"]
)

# Add behavioral indicators
query += ' http.html:"Sign in"'  # Login page indicator

Cross-Platform Searching

Different platforms use the same MurmurHash3 algorithm:

Shodan: http.favicon.hash:1234567

Censys: services.http.response.favicons.hashes.murmur3:1234567

FOFA: icon_hash="1234567"

ZoomEye: iconhash:"1234567"


SSL/TLS Certificate Intelligence

TLS certificates provide rich attribution data through fields, issuers, validity periods, and transparency logs.

Certificate Transparency (CT) Logs

CT logs are append-only, publicly auditable ledgers of all issued certificates. Over 2.5 billion certificates have been logged since 2013.

Monitoring CT Logs for Threat Hunting

# Using crt.sh API to find certificates for a domain
curl "https://crt.sh/?q=%.example.com&output=json" | jq .

# Find certificates with specific patterns
curl "https://crt.sh/?q=admin.%25&output=json" | \
  jq '.[] | select(.name_value | contains("admin"))'

Python CT Log Monitoring

import requests
from datetime import datetime, timedelta

def monitor_ct_logs(domain, days_back=30):
    """Monitor CT logs for certificates issued to a domain"""
    url = f"https://crt.sh/?q=%.{domain}&output=json"
    
    response = requests.get(url)
    certificates = response.json()
    
    recent = []
    cutoff = datetime.now() - timedelta(days=days_back)
    
    for cert in certificates:
        entry_time = datetime.strptime(
            cert['entry_timestamp'], 
            '%Y-%m-%dT%H:%M:%S.%f'
        )
        
        if entry_time > cutoff:
            recent.append({
                'id': cert['id'],
                'issuer': cert['issuer_name'],
                'common_name': cert['common_name'],
                'name_value': cert['name_value'],
                'not_before': cert['not_before'],
                'not_after': cert['not_after'],
                'entry_timestamp': cert['entry_timestamp']
            })
    
    return recent

# Monitor for suspicious certificates
suspicious = monitor_ct_logs("yourcompany.com")
for cert in suspicious:
    print(f"New cert: {cert['common_name']} issued by {cert['issuer']}")

Certificate Field Analysis

Key fields for threat hunting:

def analyze_certificate(cert_data):
    """Extract attribution-relevant certificate fields"""
    analysis = {
        # Subject fields
        'subject_cn': cert_data.get('subject', {}).get('common_name'),
        'subject_org': cert_data.get('subject', {}).get('organization'),
        
        # Issuer fields
        'issuer_cn': cert_data.get('issuer', {}).get('common_name'),
        'issuer_org': cert_data.get('issuer', {}).get('organization'),
        
        # Validity
        'not_before': cert_data.get('validity', {}).get('start'),
        'not_after': cert_data.get('validity', {}).get('end'),
        
        # Fingerprints
        'sha256': cert_data.get('fingerprint_sha256'),
        'serial': cert_data.get('serial_number'),
        
        # SANs (Subject Alternative Names)
        'sans': cert_data.get('extensions', {}).get('subject_alt_name', {}).get('dns_names', [])
    }
    
    # Red flags
    red_flags = []
    
    # Self-signed certificate
    if analysis['subject_cn'] == analysis['issuer_cn']:
        red_flags.append('self_signed')
    
    # Suspicious patterns in common name
    if analysis['subject_cn'] and any(x in analysis['subject_cn'].lower() 
                                      for x in ['localhost', 'test', 'example']):
        red_flags.append('suspicious_cn')
    
    analysis['red_flags'] = red_flags
    
    return analysis

Pivoting on Certificate Fields

def pivot_on_certificate(censys_client, cert_fingerprint):
    """Find related infrastructure using certificate pivots"""
    pivots = {}
    
    # Get certificate details
    cert = censys_client.get_certificate(cert_fingerprint)
    
    # Pivot 1: Same serial number
    serial = cert.get('parsed', {}).get('serial_number')
    if serial:
        pivots['same_serial'] = censys_client.search(
            f'services.tls.certificates.leaf.parsed.serial_number:"{serial}"'
        )
    
    # Pivot 2: Same issuer (skip common CAs)
    issuer = cert.get('parsed', {}).get('issuer', {}).get('common_name')
    if issuer and 'Let\'s Encrypt' not in issuer:
        pivots['same_issuer'] = censys_client.search(
            f'services.tls.certificates.leaf.parsed.issuer.common_name:"{issuer}"'
        )
    
    # Pivot 3: Same subject organization
    org = cert.get('parsed', {}).get('subject', {}).get('organization')
    if org:
        pivots['same_org'] = censys_client.search(
            f'services.tls.certificates.leaf.parsed.subject.organization:"{org}"'
        )
    
    return pivots

ASN and BGP Analysis

Autonomous System Numbers (ASNs) identify networks and their relationships, providing valuable context for attribution.

Understanding ASNs for Threat Intelligence

def analyze_asn(ip_address):
    """Extract ASN information for an IP address"""
    import ipwhois
    
    obj = ipwhois.IPWhois(ip_address)
    results = obj.lookup_rdap()
    
    asn_info = {
        'asn': results.get('asn'),
        'asn_cidr': results.get('asn_cidr'),
        'asn_country_code': results.get('asn_country_code'),
        'asn_description': results.get('asn_description'),
        'asn_registry': results.get('asn_registry'),
        'network_name': results.get('network', {}).get('name'),
    }
    
    return asn_info

Bulletproof Hosting Indicators

Certain ASNs are known for hosting malicious infrastructure. Key signals:

  • High volume of abuse reports
  • Lax or non-existent abuse response
  • Known associations with criminal forums
  • Hosting in jurisdictions with weak enforcement
  • Frequent appearance in threat intel feeds

ASN Pivoting Workflow

1. Start with known malicious IP
          ↓
2. Identify ASN (e.g., AS12345)
          ↓
3. Query all prefixes announced by ASN
          ↓
4. Scan prefixes for similar infrastructure
          ↓
5. Identify related C2 servers
          ↓
6. Map threat actor's hosting preferences

Useful ASN Resources

Regional Internet Registries:

  • ARIN (North America)
  • RIPE (Europe, Middle East)
  • APNIC (Asia-Pacific)
  • LACNIC (Latin America)
  • AFRINIC (Africa)

Lookup Tools:

  • Hurricane Electric BGP Toolkit (bgp.he.net)
  • PeeringDB
  • BGPView

AI-Enhanced Attribution

Machine learning and graph-based analysis are transforming threat attribution.

Graph Neural Networks for Infrastructure Mapping

Research shows AI-driven correlation engines:

  • Reduce false correlations by 43%
  • Accelerate attribution analysis by 60%
# Conceptual example: Graph-based infrastructure analysis
import networkx as nx

def build_infrastructure_graph(iocs):
    """
    Build a graph connecting related infrastructure
    
    Nodes: IPs, domains, certificates, ASNs
    Edges: Relationships (resolves_to, shares_cert, same_asn, etc.)
    """
    G = nx.Graph()
    
    for ioc in iocs:
        # Add IP node
        G.add_node(ioc['ip'], type='ip', asn=ioc.get('asn'))
        
        # Add domain nodes and edges
        for domain in ioc.get('domains', []):
            G.add_node(domain, type='domain')
            G.add_edge(ioc['ip'], domain, relationship='resolves')
        
        # Add certificate node and edge
        if ioc.get('cert_fingerprint'):
            G.add_node(ioc['cert_fingerprint'], type='certificate')
            G.add_edge(ioc['ip'], ioc['cert_fingerprint'], 
                      relationship='presents_cert')
        
        # Add JARM node
        if ioc.get('jarm'):
            G.add_node(ioc['jarm'], type='jarm')
            G.add_edge(ioc['ip'], ioc['jarm'], relationship='jarm_match')
    
    return G

def find_clusters(G):
    """Identify infrastructure clusters (likely same actor)"""
    communities = nx.community.louvain_communities(G)
    return list(communities)

LLM-Assisted Threat Intelligence

Large Language Models are increasingly used for:

  • Threat report summarization
  • IOC extraction from unstructured text
  • Pattern recognition across campaigns
  • Attribution hypothesis generation

Building an Automated Hunting Pipeline

Combine all techniques into an automated workflow.

Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│                     IOC Input (IP, Domain)                       │
└────────────────────────────┬────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Enrichment Layer                             │
│     Passive DNS | ASN/BGP | WHOIS | Threat Intel Feeds          │
└────────────────────────────┬────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Fingerprint Collection                       │
│     JARM Scan | JA3/JA4 | Favicon Hash | Certificate Analysis   │
└────────────────────────────┬────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Pivoting & Discovery                         │
│     Shodan | Censys | FOFA | CT Logs                            │
└────────────────────────────┬────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Correlation & Analysis                       │
│     Graph Analysis (NetworkX/Neo4j) | ML Clustering             │
└────────────────────────────┬────────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Output                                       │
│     MISP Export | OpenCTI | STIX Bundle | Alert/Report          │
└─────────────────────────────────────────────────────────────────┘

Implementation Example

#!/usr/bin/env python3
"""Automated C2 Hunting Pipeline"""

import asyncio
from dataclasses import dataclass
from typing import List, Optional

@dataclass
class InfrastructureIOC:
    ip: str
    domains: List[str] = None
    jarm: Optional[str] = None
    favicon_hash: Optional[int] = None
    cert_sha256: Optional[str] = None
    asn: Optional[str] = None
    confidence: float = 0.0
    tags: List[str] = None

class C2HuntingPipeline:
    def __init__(self, config):
        self.shodan_api = ShodanClient(config['shodan_key'])
        self.censys_api = CensysClient(config['censys_id'], config['censys_secret'])
        self.pdns_api = PassiveDNSClient(config['pdns_key'])
        
    async def enrich_ioc(self, ip: str) -> InfrastructureIOC:
        """Full enrichment of a single IP"""
        ioc = InfrastructureIOC(ip=ip)
        
        # Parallel enrichment
        tasks = [
            self.get_passive_dns(ip),
            self.get_jarm_fingerprint(ip),
            self.get_shodan_data(ip),
            self.get_censys_data(ip),
            self.get_asn_info(ip)
        ]
        
        results = await asyncio.gather(*tasks, return_exceptions=True)
        
        # Merge results
        pdns, jarm, shodan, censys, asn = results
        
        ioc.domains = pdns.get('domains', []) if pdns else []
        ioc.jarm = jarm
        ioc.asn = asn.get('asn') if asn else None
        
        if shodan:
            ioc.favicon_hash = shodan.get('http', {}).get('favicon', {}).get('hash')
        
        if censys:
            ioc.cert_sha256 = censys.get('tls', {}).get('certificate', {}).get('fingerprint_sha256')
            
        return ioc
    
    async def hunt_related(self, ioc: InfrastructureIOC) -> List[InfrastructureIOC]:
        """Find related infrastructure using multiple pivots"""
        related = []
        
        # JARM pivot
        if ioc.jarm:
            matches = await self.shodan_api.search(f'ssl.jarm:"{ioc.jarm}"')
            for match in matches:
                if match['ip_str'] != ioc.ip:
                    related.append(InfrastructureIOC(
                        ip=match['ip_str'],
                        jarm=ioc.jarm,
                        tags=['jarm_pivot']
                    ))
        
        # Favicon pivot
        if ioc.favicon_hash:
            matches = await self.shodan_api.search(
                f'http.favicon.hash:{ioc.favicon_hash}'
            )
            for match in matches:
                if match['ip_str'] != ioc.ip:
                    related.append(InfrastructureIOC(
                        ip=match['ip_str'],
                        favicon_hash=ioc.favicon_hash,
                        tags=['favicon_pivot']
                    ))
        
        # Certificate pivot
        if ioc.cert_sha256:
            matches = await self.censys_api.search(
                f'services.tls.certificates.leaf.fingerprint_sha256:{ioc.cert_sha256}'
            )
            for match in matches:
                if match['ip'] != ioc.ip:
                    related.append(InfrastructureIOC(
                        ip=match['ip'],
                        cert_sha256=ioc.cert_sha256,
                        tags=['cert_pivot']
                    ))
        
        return related

# Usage
async def main():
    config = {
        'shodan_key': 'your_key',
        'censys_id': 'your_id',
        'censys_secret': 'your_secret',
        'pdns_key': 'your_key'
    }
    
    pipeline = C2HuntingPipeline(config)
    
    # Start from seed IP
    seed_ioc = await pipeline.enrich_ioc("192.0.2.1")
    print(f"Enriched: JARM={seed_ioc.jarm[:20]}... ASN={seed_ioc.asn}")
    
    # Find related infrastructure
    related = await pipeline.hunt_related(seed_ioc)
    print(f"Found {len(related)} related hosts")

if __name__ == "__main__":
    asyncio.run(main())

Nuclei Templates for C2 Detection

Project Discovery’s Nuclei provides templated scanning:

# cobalt-strike-c2.yaml
id: cobalt-strike-c2

info:
  name: Cobalt Strike C2 - Detect
  author: security-researcher
  severity: high
  description: Detects Cobalt Strike C2 via SSL certificate
  tags: ssl,c2,cobalt-strike

ssl:
  - address: "{{Host}}:{{Port}}"
    
    matchers:
      - type: dsl
        dsl:
          - 'contains(serial,"08:BB:00:EE")'
    
    extractors:
      - type: json
        json:
          - ".serial"

Operational Security Considerations

When conducting threat hunting, protect yourself.

Attribution Protection

Use isolated infrastructure:

  • Dedicated VMs for research
  • Separate network segments
  • VPN/Tor for queries

Avoid alerting adversaries:

  • Passive DNS over active queries
  • Rate-limit scans
  • Use cached data when possible

Credential hygiene:

  • Unique API keys per project
  • Rotate keys after investigations
  • Never reuse threat intel platform credentials

Legal Boundaries

  • Passive techniques (DNS, Shodan/Censys queries): Generally legal
  • Active scanning: Verify authorization
  • Accessing systems: Never without explicit permission
  • Data handling: Follow GDPR/CCPA for PII

Documentation

Maintain detailed records for:

  • Legal defensibility
  • Team knowledge sharing
  • Pattern recognition over time
  • Incident response support

Conclusion

Modern IP tracing requires a multi-layered approach:

  1. Passive DNS for historical infrastructure mapping
  2. TLS fingerprinting (JA3/JA4/JARM) for behavioral identification
  3. Internet scanning (Shodan/Censys) for proactive discovery
  4. Certificate analysis for cryptographic attribution
  5. ASN/BGP analysis for network-level context
  6. AI/ML for pattern recognition and clustering

No single technique provides definitive attribution. The key is correlation across multiple data sources to build high-confidence assessments of threat actor infrastructure.

The most effective threat hunters combine:

  • Technical depth in fingerprinting techniques
  • Automation for scale
  • Creativity in pivot strategies
  • Operational discipline for safety

References & Further Reading

  1. IBM X-Force 2025 Threat Intelligence Index
  2. Salesforce Engineering: TLS Fingerprinting with JA3 and JA3S
  3. FoxIO: JA4+ Network Fingerprinting
  4. Censys: A Beginner’s Guide to Tracking Malware Infrastructure
  5. Project Discovery: C2 Server Hunting with Nuclei Templates
  6. Certificate Transparency: https://certificate.transparency.dev/

What IP tracing techniques have you found most effective? Share your experiences in the comments below.

About the Author

Brian Curry is a Kansas City-based Data Scientist, AI expert, and founder of Vector1 Research, where he explores the frontier of Artificial Intelligence, Causal Inference, Agentic Systems, and Economic Modeling.

His work focuses on building intelligent systems that reveal the causal structures driving performance across marketing, content, and economics. He is the creator of PyCausalSim (causal discovery through simulation), Papilon (complex system optimization), Daedalus (causal and economic analysis), and Memory-Node Encapsulation (data structures for artificial episodic memory).

Contact: brian@vector1.ai


Tags: #Cybersecurity #ThreatIntelligence #ThreatHunting #OSINT #MalwareAnalysis #C2Hunting #TLSFingerprinting #Infosec

Related research