基于Zeek UID与JA4+指纹的恶意流量威胁狩猎实践

2 阅读8分钟

JA4+ 的强大之处在于,它将分析焦点从“数据包在说什么”转向了“数据包是如何构建的”。在一切都加密、攻击者在几秒内就能轮换IP和域名的时代,JA4+为网络流量提供了稳定的指纹。通过将TCP和TLS握手转换为模块化的 $a\_b\_c$ 格式,它使我们能够识别连接背后的特定软件、库或工具。即使攻击者更改了加密密钥或隐藏在新的代理后面,指纹识别仍然有效。JA4+本质上是将看似随机的网络数据噪声,转化为清晰、可搜索且易于人类阅读和机器追踪的特征。再加上Zeek强大的检测引擎,实时威胁检测成为可能。

今天,我们正在实验一个独立的Zeek实现以及个人编写的Python脚本,以查找已知恶意JA4+指纹的可能匹配项。这是一项正在进行中的工作,Zeek的连接ID(UID)是这些脚本的核心。有望更快、更简洁地揭示额外的威胁情报。

更多信息请参阅FoxIO的GitHub:github.com/FoxIO-LLC/j…

以下是一些初始数据,您可能觉得重要:

thedr@gallifrey:~/zeek$ zeek -v
zeek version 8.0.4
thedr@gallifrey:~/zeek$ cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.3 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.3 LTS (Jammy Jellyfish)"
thedr@gallifrey:~/zeek$ tshark -v
TShark (Wireshark) 4.6.2.
2023-10-03-Pikabot-infection-with-Cobalt-Strike.pcap

此Zeek实例以独立模式运行。为了让Zeek正常运行,我们需要安装Tshark。我将会发布另一篇文章,展示如何通过CLI在新的虚拟机上安装Zeek和Wireshark。Zeek没有GUI。当Zeek处理一个PCAP文件时,它会根据PCAP中观察到的流量输出到一个文件夹中的多个文件。

Zeek对 2023–10–03-Pikabot-infection-with-Cobalt-Strike.pcap 的输出

我将展示一个从我追踪相关恶意软件最喜欢的网站获取的文件:Malware-Traffic-Analysis.net

请注意!这是真实的恶意软件。除非您清楚自己在做什么,否则切勿导出或操作此PCAP文件。

[图片:2023–10–03 Pikabot infection with Cobalt Strike]

我选择了 2023–10–03-Pikabot-infection-with-Cobalt-Strike.pcap,因为我确实知道Cobalt Strike有多个JA4+指纹。这是一个很好的示例。

让我们实时运行它。

thedr@gallifrey:~/pcap/pikabot$ zeek -r ../2023-10-03-Pikabot-infection-with-Cobalt-Strike.pcap ../../zeek/
## ../xxxxx 告诉shell请查找上一个父目录
## ../../xxxx 请回溯2层目录
thedr@gallifrey:~/pcap/pikabot$ ls
conn.log     files.log     ldap.log         packet_filter.log  ssl.log
dce_rpc.log  http.log      ldap_search.log  smb_files.log      weird.log
dns.log      kerberos.log  ocsp.log         smb_mapping.log    x509.log

现在,您看到了Zeek为我们创建的所有不同的日志文件。这就是Zeek如此特别的原因。它将所有重要信息分组汇总。现在轮到我自己开发的Python脚本出场了,它不仅可以提取已处理的JA4+指纹,还可以将其与我的本地数据库进行比较。

thedr@gallifrey:~/pcap/pikabot$ python3 ~/petes_python/ja4_threat_hunter_v7.py
================================================================================
[*] Analysis Complete
[*] Total connections checked: 280
[*] Malicious connections detected: 203
================================================================================

================================================================================
Finding #1
================================================================================
Connection ID (UID): Clj9x532tezTvr9dbe
Log Source: ssl.log
Timestamp: 2023-10-03 16:11:39 UTC

[PRIMARY DETECTION]
JA4S Match: t120300_c030_52d195ce1d92
    Application: Cobalt Strike v4.9.1 beacon
    Notes: Cobalt Strike v4.9.1 over winhttp

[RELATED DATA]
JA4T: 65535_2-1-3-1-1-4_1460_8 (from conn.log)
JA4TS: 64240_2-1-1-4-1-3_1396_7 (from conn.log)

[CONNECTION DETAILS]
10.1.2.147:49388 -> 179[.]60.149[.]244:443

这是 ja4_threat_hunter_v7.py。这实际上是一个全面的JA4+威胁狩猎工具,覆盖了整套JA4指纹(TLS、HTTP、SSH、X.509和TCP),而不仅仅是SSL/TLS指纹。数据库 /home/thedr/db/ja4+_db.json 包含了我们想要检测的已知恶意指纹(ja4_fingerprintja4s_fingerprintja4h_fingerprint 等)。

Zeek上的JA4X目前无法工作。 我使用了FoxIO提供的Python脚本中的 JA4.py,从Zeek出发进一步识别额外的JAX证书基础设施。

这不是一个实时数据库。我从 Ja4db.com 下载了文件。未来,我将修改脚本以使用API,以便所有潜在的指纹都能使用最新的威胁情报。

thedr@gallifrey:~/pcap/pikabot$ cat ~/petes_python/ja4_threat_hunter_v7.py
#!/usr/bin/env python3
"""JA4 Threat Hunter v4 - Compare Zeek JA4 fingerprints against known database
Works with FoxIO-style database format (array of objects)
Usage: python3 ja4_threat_hunter_v4.py /path/to/zeek/logs
"""

import json
import gzip
import sys
import os
from pathlib import Path
from collections import defaultdict
from datetime import datetime

# JA4 field mappings for each log type
# Primary fingerprints (can trigger detections)
PRIMARY_FIELDS = {
    'ssl.log': ['ja4', 'ja4s'],
    'http.log': ['ja4h'],
    'ssh.log': ['ja4ssh'],
    'x509.log': ['ja4x']
}

# All other logs that contain related data
RELATED_LOGS = {
    'conn.log': ['ja4t', 'ja4ts'],  # TCP fingerprints - always related to any connection
    'ssl.log': ['ja4', 'ja4s'],     # TLS fingerprints
    'x509.log': ['ja4x']            # Certificate fingerprints
}

def load_malicious_db(db_path):
    """Load the malicious JA4 database (FoxIO format - array of objects)"""
    try:
        with open(db_path, 'r') as f:
            db_array = json.load(f)
        # Convert array format to lookup dictionaries for fast searching
        fingerprint_db = {
            'ja4': {}, 'ja4s': {}, 'ja4h': {},
            'ja4ssh': {}, 'ja4x': {}, 'ja4t': {}, 'ja4ts': {}
        }
        for entry in db_array:
            if entry.get('ja4_fingerprint'):
                fingerprint_db['ja4'][entry['ja4_fingerprint']] = entry
            if entry.get('ja4s_fingerprint'):
                fingerprint_db['ja4s'][entry['ja4s_fingerprint']] = entry
            if entry.get('ja4h_fingerprint'):
                fingerprint_db['ja4h'][entry['ja4h_fingerprint']] = entry
            if entry.get('ja4ssh_fingerprint'):
                fingerprint_db['ja4ssh'][entry['ja4ssh_fingerprint']] = entry
            if entry.get('ja4x_fingerprint'):
                fingerprint_db['ja4x'][entry['ja4x_fingerprint']] = entry
            if entry.get('ja4t_fingerprint'):
                fingerprint_db['ja4t'][entry['ja4t_fingerprint']] = entry
            if entry.get('ja4ts_fingerprint'):
                fingerprint_db['ja4ts'][entry['ja4ts_fingerprint']] = entry
        return fingerprint_db
    except FileNotFoundError:
        print(f"[!] Database not found: {db_path}")
        sys.exit(1)
    except json.JSONDecodeError:
        print(f"[!] Invalid JSON in database: {db_path}")
        sys.exit(1)

def parse_zeek_log(log_path):
    """Parse a Zeek log file (handles both plain and gzipped)"""
    # ... 函数体见原文 ...
    pass

def extract_ja4_fingerprints(log_entry, ja4_fields):
    """Extract JA4 fingerprints from a log entry"""
    # ... 函数体见原文 ...
    pass

def check_against_db(fingerprints, malicious_db):
    """Check if any fingerprints match the malicious database"""
    # ... 函数体见原文 ...
    pass

def find_log_files(directory):
    """Find all Zeek log files recursively"""
    # ... 函数体见原文 ...
    pass

def convert_ts_to_utc(timestamp_str):
    """Convert Zeek timestamp to UTC format"""
    # ... 函数体见原文 ...
    pass

def main():
    if len(sys.argv) < 2:
        print("Usage: python3 ja4_threat_hunter_v6.py <zeek_log_directory>")
        sys.exit(1)
    log_dir = sys.argv[1]
    db_path = "/home/thedr/db/ja4+_db.json"
    malicious_db = load_malicious_db(db_path)
    log_files = find_log_files(log_dir)

    if not log_files['primary']:
        print(f"[!] No JA4-related log files found in {log_dir}")
        sys.exit(1)

    # PHASE 1: Scan primary logs for matches
    total_checked = 0
    findings = []
    matched_uids = set()

    for log_path, log_type in log_files['primary']:
        for entry in parse_zeek_log(log_path):
            fingerprints = extract_ja4_fingerprints(entry, PRIMARY_FIELDS[log_type])
            if fingerprints:
                total_checked += 1
                matches = check_against_db(fingerprints, malicious_db)
                if matches:
                    uid = entry.get('uid', 'Unknown')
                    matched_uids.add(uid)
                    findings.append({
                        'log_file': log_path,
                        'log_type': log_type,
                        'log_entry': entry,
                        'matches': matches,
                        'uid': uid,
                        'related': []
                    })

    # PHASE 2: Find related data
    if matched_uids:
        uid_related = defaultdict(list)
        for log_path, log_type in log_files['related']:
            for entry in parse_zeek_log(log_path):
                uid = entry.get('uid', '')
                if uid in matched_uids:
                    fingerprints = extract_ja4_fingerprints(entry, RELATED_LOGS[log_type])
                    if fingerprints:
                        uid_related[uid].append({
                            'log_type': log_type,
                            'fingerprints': fingerprints,
                            'entry': entry
                        })
        for finding in findings:
            if finding['uid'] in uid_related:
                finding['related'] = uid_related[finding['uid']]

    # Report findings
    # ... 输出报告部分见原文 ...
    pass

if __name__ == "__main__":
    main()

该脚本分两个阶段工作:

  1. 扫描主要日志,与我们的恶意数据库进行匹配。
  2. 对于任何找到的匹配项,它会使用连接UID从其他日志中提取相关的JA4数据。

一旦匹配到恶意指纹,我们就可以利用Zeek的连接ID进行数据透视,了解有关这些连接的其他重要信息。这就是Zeek的便利之处。

[图片:输出截图]

因此,即使确切的JA4S指纹和IP完全相同,也确认了203个不同的匹配项。这是TLS服务器问候(Server Hello)。客户端正在与恶意服务器通信。每隔一秒左右,就会发出一个信标。观察这个过滤后的PCAP,可以看到唯一变化的是客户端机器上的“临时”端口。这显示了连接大约每秒发出一次信标。

[图片:Wireshark捕获显示JA4S服务器问候]

请记住,对于JA4与JA4S、JA4T与JA4TS,需要捕获握手过程以及TLS客户端和服务器问候才能正确地给连接打指纹。客户端上用于C2的应用程序使用的TLS库可能是恶意的,但无法从看到的流量中确认。我假设那是Cobalt客户端在与C2服务器通信。我在这个Zeek实例上使用JA4X遇到了困难,但我可以转向Wireshark捕获或使用FoxIO专用的Python脚本来提取X509信息。我们想知道这个PCAP中的其他哪些流量可能是恶意的。JA4X不识别X509证书中的内容,而是识别其结构——证书的制作方式。这是可重现的识别信息。证书可能完全不同,但如果它们以相同的方式制作,我们就知道它们是由同一个恶意行为者(同一个应用程序)制作的。

如果Zeek的JA4X插件可用,这可以通过连接ID完成。这里,让我们看看FoxIO提供的这个Python脚本,用于从PCAP中提取X509证书。

thedr@gallifrey:~/foxio_python/ja4-sparse-clone/python$ cat ja4x_out.json | grep -i -a2 -b10 '179[.]60.149[.]244'
## 这是具有已知Cobalt Strike JA4S指纹的IP地址
## 我们想要找到这里连接的任何其他指纹

[FoxIO GitHub 带有 JA4+ 指纹示例]

这是我最喜欢JA4+的一点。我们可以基于其他指纹进行数据透视。

"stream": 76,
"src": "10.1.2.147",
"dst": "179[.]60.149[.]244",
"srcport": "49404",
"dstport": "443",
"client_ttl": "128",
"server_ttl": "41",
"domain": "zzerxc[.]com",
"JA4.1": "t00d190700_4d06a43e2d88_b4cdb2b2a0c3",
"JA4_r.1": "t00d190700_,,,,,161,162,171,172,187,188,191,192,195,196,199,200,6,7_000a,000b,000d,0017,0023,ff01_52,53,54,25,81,3,27,83,5,4,37,39",
"JA4_o.1": "t00d190700_be9ffe69a4f5_45dadac4372d",
"JA4_ro.1": "t00d190700_196,195,200,199,188,187,192,191,162,161,172,171,7,6,,,,,_0000,000a,000b,000d,0023,0017,ff01_52,53,54,25,81,3,27,83,5,4,37,39",
"JA4X.1": "a373a9f83c6b_7022c563de38_821a8ec155c6",
-- snip --
"server_ttl": "102",
"JA4.1": "t00i190600_4d06a43e2d88_b4cdb2b2a0c3",
"JA4_r.1": "t00i190600_,,,,,161,162,171,172,187,188,191,192,195,196,199,200,6,7_000a,000b,000d,0017,0023,ff01_52,53,54,25,81,3,27,83,5,4,37,39",
"JA4_o.1": "t00i190600_be9ffe69a4f5_2621bef74af5",
"JA4_ro.1": "t00i190600_196,195,200,199,188,187,192,191,162,161,172,171,7,6,,,,,_000a,000b,000d,0023,0017,ff01_52,53,54,25,81,3,27,83,5,4,37,39",
"JA4X.1": "1a59268f55e5_1a59268f55e5_795797892f9c"
######### ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#### "JA4X.1": "1a59268f55e5_1a59268f55e5_795797892f9c"
### 这与PIKABOT证书100%匹配。

攻击者可以轻松更改文件的SHA256哈希值,但他们更难更改其证书颁发机构的模板。JA4X.2和.3允许我们对提供证书的基础设施进行指纹识别,而不仅仅是单个站点。现在我们可以使用JA4X进行数据透视。让我们看看发现了什么。

thedr@gallifrey:~/foxio_python/ja4-sparse-clone/python$ grep -B15 '1a59268f55e5_1a59268f55e5_795797892f9c' ja4x_out.json | grep -E '"dst"|JA4X.1'
"JA4X.1":"a373a9f83c6b_2bab15409345_0ce9ea683d50", "dst":"167[.]86.96[.]3",
"JA4X.1":"1a59268f55e5_1a59268f55e5_795797892f9c"
"JA4X.1":"2bab15409345_2166164053c1_68ccbb907075", "dst":"167[.]86.96[.]3",
"JA4X.1":"1a59268f55e5_1a59268f55e5_795797892f9c", "dst":"79[.]141.175[.]96",
"JA4X.1":"1a59268f55e5_1a59268f55e5_795797892f9c", "dst":"209[.]126.9[.]47",
"JA4X.1":"1a59268f55e5_1a59268f55e5_795797892f9c", "dst":"167[.]86.96[.]3",
"JA4X.1":"1a59268f55e5_1a59268f55e5_795797892f9c", "dst":"79[.]141.175[.]96",
"JA4X.1":"2bab15409345_2166164053c1_68ccbb907075", "dst":"209[.]126.9[.]47",
"JA4X.1":"1a59268f55e5_1a59268f55e5_795797892f9c", "dst":"167[.]86.96[.]3",
"JA4X.1":"1a59268f55e5_1a59268f55e5_795797892f9c", "dst":"79[.]141.175[.]96",
"JA4X.1":"1a59268f55e5_1a59268f55e5_795797892f9c", "dst":"209[.]126.9[.]47",
"JA4X.1":"1a59268f55e5_1a59268f55e5_795797892f9c", "dst":"167[.]86.96[.]3",
"JA4X.1":"1a59268f55e5_1a59268f55e5_795797892f9c"

仅基于JA4X和最初的JA4S匹配,我们已经确定其他多个IP地址也连接到了Cobalt和Pikabot。

这个最初的Pikabot感染和IOC列表归功于Palo Alto Unit 42。

[Unit42 GitHub 链接]

这是展示感染链的LinkedIn帖子。

[LinkedIn 帖子链接]

以下是基于Unit42 GitHub中的原始参考资料。

原始参考资料:

感谢这里所做的所有工作。我利用这些来更好地理解感染链以及JA4+如何在事件发生后用于继续发现更多信息。真正有趣的是,JA4+仅通过对加密流量的指纹识别,就检索到了与此感染直接相连的所有完全相同的IP地址。所以,如果你认为因为流量被加密就无法获取任何信息,那你就大错特错了。

感谢您花时间阅读此文。我不在乎点赞或分享,只希望帮助下一个读者理解。与他人分享你正在学习的东西就足够了。即使你对此完全陌生,你也有能力为社区做出贡献。参与进来,也帮助其他人参与进来。

如有任何问题或想打个招呼,欢迎来我的LinkedIn看看!www.linkedin.com/in/pagaudio… CSD0tFqvECLokhw9aBeRqiHxKuChFS1BHMrXqLpuZRSQopvII++BkDJZmWN7m+/eMmnzI0z1wxbfihqPPShIVArQoOJX66nT221TJ8z1MsdVblDBSVyHbryBqL639KY6