用代码狩猎 — 使用yara-python构建自动化签名检测器

如果一条规则是一名战士,那么用Python挥舞的YARA就是一支军团。

超越命令行,迈向真正自动化的第一步。

>

本文涵盖内容

  • 为何使用yara-python而非CLI,以及何时需要它
  • 从库安装到规则编译、文件、内存和字节扫描的整个过程
  • 目录递归扫描和处理匹配结果的简洁模式
  • 使用回调函数和外部变量(externals)动态控制规则的技术
  • 生产环境中常见的陷阱和规避方法

引言 — 超越CLI,迈向自动化

yara命令是分析师手中熟悉的利器,但它也有无法触及的领域。当每天有数千个样本需要自动分类,或者需要将检测结果导入SIEM管道,或者需要按进程ID跟踪内存转储中的特定模式时——在所有这些情况下,分析师最终都会编写代码。

yara-python是YARA的官方Python绑定,它将用C编写的YARA引擎的所有功能作为Python对象公开。从编译、缓存、回调、外部变量到内存扫描,在CLI中笨拙或不可能完成的任务,现在只需一行方法调用即可实现。

yara-python的核心组成

该库的使用流程很简单。

  • yara.compile() — 编译规则,生成可重用的Rules对象。它可以接受文件、字符串或多个源。
  • rules.match() — 接受文件路径、字节数据或进程PID作为扫描目标。
  • Match对象 — 匹配结果返回一个包含规则名称、元数据、匹配字符串和偏移量的对象。

只要掌握这三点,就可以构建任何自动化。

实践 — 用代码构建猎手

第0步. 环境准备

# YARA引擎必须已经安装
sudo apt install -y yara                # Debian / Ubuntu
brew install yara                       # macOS

# 安装Python绑定
pip install yara-python

# 验证安装
python -c "import yara; print(yara.__version__)"

第1步. 创建检测目标样本文件

与之前一样,我们使用无害的虚拟文件。这次,我们将直接用Python脚本生成它们。

# create_sample.py
SAMPLE = """#!/bin/bash
# Internal task runner v1.0
TASK_ID=ACME-EDU-2026
echo "Starting backup process..."
curl -s http://example-edu-lab.local/healthcheck
echo "Token: EDULAB_SIGNATURE_TOKEN_42"
exit 0
"""

with open("suspicious_sample.txt", "w", encoding="utf-8") as f:
    f.write(SAMPLE)
print("[+] sample file created")

第2步. 编写YARA规则

我们将保留规则文件不变,但这次我们将以两种方式在Python中处理它——加载外部.yar文件和直接编译源字符串。

// edulab_detector.yar
rule EDULAB_Suspicious_Script
{
    meta:
        author      = "주군의 보안 강의실"
        description = "EDULAB 식별자와 토큰을 포함한 셸 스크립트 탐지"
        date        = "2026-05-23"
        severity    = "medium"

    strings:
        $magic = "#!/bin/bash"
        $id    = "ACME-EDU-2026" ascii
        $token = "EDULAB_SIGNATURE_TOKEN_42" ascii

    condition:
        filesize < 10KB
        and $magic at 0
        and all of ($id, $token)
}

第3步. 编写第一个扫描器

我们将核心流程:编译 → 扫描 → 输出结果,放入一个文件中。

# scanner_basic.py
import yara

# (1) 编译规则 — 从文件加载
rules = yara.compile(filepath="edulab_detector.yar")

# (2) 扫描目标文件
matches = rules.match(filepath="suspicious_sample.txt")

# (3) 输出结果
if not matches:
    print("[-] No matches.")
else:
    for m in matches:
        print(f"[!] Rule matched: {m.rule}")
        print(f"    Tags     : {m.tags}")
        print(f"    Meta     : {m.meta}")
        for s in m.strings:
            for inst in s.instances:
                offset = inst.offset
                data   = inst.matched_data.decode("utf-8", errors="replace")
                print(f"    {s.identifier} @ 0x{offset:x}  ->  {data!r}")

如果正常运行,将显示以下输出。

[!] Rule matched: EDULAB_Suspicious_Script
    Tags     : []
    Meta     : {'author': '주군의 보안 강의실', ...}
    $magic @ 0x0  ->  '#!/bin/bash'
    $id @ 0x35  ->  'ACME-EDU-2026'
    $token @ 0xa9  ->  'EDULAB_SIGNATURE_TOKEN_42'

版本注意:yara-python 4.3.0之后,match.strings已成为StringMatch对象列表,每个对象包含identifier和instances。在之前的版本(3.x)中,它是一个(offset, identifier, data)元组列表。编写代码前请务必检查yara.__version__。

第4步. 直接从字符串编译规则

这在无需配置文件即可在代码中动态创建和测试规则时非常有用。它在CI管道或单元测试中尤其出色。

# scanner_inline.py
import yara

RULE_SOURCE = """
rule QuickHexSig {
    meta:
        description = "Detects bash shebang via hex pattern"
    strings:
        $hex = { 23 21 2F 62 69 6E 2F 62 61 73 68 }
    condition:
        $hex at 0
}
"""

rules = yara.compile(source=RULE_SOURCE)

# 您也可以直接扫描字节数据 — 无需文件I/O!
with open("suspicious_sample.txt", "rb") as f:
    data = f.read()

for m in rules.match(data=data):
    print(f"[!] {m.rule} matched in-memory buffer.")

`data=`参数在即时检查从内存缓冲区或网络接收到的有效载荷时,成为一个决定性的武器。

第5步. 目录递归扫描器

在实际应用中,您将扫描整个目录树,而不仅仅是一个文件。我们将它与错误处理一起整洁地打包。

# scan_tree.py
import os
import sys
import yara

def build_rules(rule_path: str) -> yara.Rules:
    try:
        return yara.compile(filepath=rule_path)
    except yara.SyntaxError as e:
        print(f"[error] rule syntax error: {e}", file=sys.stderr)
        sys.exit(1)

def scan_tree(rules: yara.Rules, root: str) -> None:
    hit_count = 0
    for dirpath, _, filenames in os.walk(root):
        for name in filenames:
            path = os.path.join(dirpath, name)
            try:
                matches = rules.match(filepath=path, timeout=10)
            except yara.TimeoutError:
                print(f"[warn] timeout: {path}", file=sys.stderr)
                continue
            except (PermissionError, yara.Error) as e:
                print(f"[skip] {path}: {e}", file=sys.stderr)
                continue

            for m in matches:
                hit_count += 1
                print(f"[HIT] {m.rule}  ->  {path}")
    print(f"
[+] Scan complete. {hit_count} hit(s).")

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("Usage: python scan_tree.py <rules.yar> <target_dir>")
        sys.exit(1)
    rules = build_rules(sys.argv[1])
    scan_tree(rules, sys.argv[2])

请注意`timeout`参数。它可以在遇到大型二进制文件或压缩文件时,防止程序无限期挂起。

第6步. 使用回调和外部变量进行精细控制

yara-python的真正魅力在于其回调函数外部变量。通过注册一个在每次匹配发生时都会调用的函数,您可以自然地插入诸如即时发送通知或记录到数据库等副作用。

# scanner_callback.py
import yara

def on_match(data):
    """매칭 발생 시 호출되는 콜백"""
    if data["matches"]:
        print(f"[CALLBACK] Rule '{data['rule']}' fired")
        print(f"           Namespace: {data['namespace']}")
        print(f"           Tags     : {data['tags']}")
        # 在此处自由处理Slack通知、数据库插入、SIEM转发等
    return yara.CALLBACK_CONTINUE

# 声明外部变量以便在规则中引用
RULE = """
rule HighRiskHost {
    condition:
        env == "production" and filesize < 5KB
}
"""

rules = yara.compile(source=RULE, externals={"env": "staging"})

# 外部变量的值可以在扫描时动态替换
rules.match(
    filepath="suspicious_sample.txt",
    externals={"env": "production"},
    callback=on_match,
    which_callbacks=yara.CALLBACK_MATCHES,
)

externals提供了一种优雅的方式,将环境变量、主机标签或用户组等上下文信息注入到规则中。这使得相同的规则可以在生产环境中触发,而在开发环境中保持静默。

⚠️ 注意事项 — 生产环境中的陷阱

  • 编译成本: yara.compile()绝不是轻量级的。如果重复使用相同的规则,应重用已编译的Rules对象,或使用rules.save() / yara.load()将其缓存到磁盘。
  • 线程安全性: Rules对象是线程安全的,但使用同一对象并发调用match()可能会产生内部同步开销。如果需要高性能,请考虑使用multiprocessing。
  • 内存扫描权限: 使用rules.match(pid=1234)检查其他进程的内存需要适当的权限(root、CAP_SYS_PTRACE等)。如果权限不足,将抛出yara.Error。
  • 外部变量类型匹配: 如果传递给externals的值的类型(字符串、整数、浮点数、布尔值)与规则中声明的类型不同,编译将在编译阶段失败。
  • 编码陷阱: 匹配到的字节始终是bytes类型。不要直接打印它们;务必使用.decode(…, errors=”replace”)进行包装,以避免因损坏字符而崩溃。

✅ 总结 — 成为代码猎手

yara-python不仅仅是一个简单的绑定;它是一座将YARA转变为真正自动化系统的桥梁。通过文本接收CLI输出并再次解析的时代已经结束。现在,我们可以将匹配结果作为对象接收,并直接将其送入数据管道。

如果您继续下一步,这些路径将展开:

  • 使用FastAPI / Flask封装,构建内部扫描API
  • 使用CeleryRQ构建分布式扫描队列
  • 结合Volatility3插件,将YARA应用于内存取证
  • 结合VirusTotal API + yara-python,自动化内部威胁情报工作流
  • 使用mkYARAyaraGen等工具自动生成规则 → 通过代码验证的管道

一条规则守护着数百台服务器,代码中的一个函数执行该规则数千次。主宰的狩猎现在不再通过双手,而是通过代码完成。️


Comments

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注