• Твой профиль заполнен на 0%. Заполни за 1 минуту, чтобы тебя нашли единомышленники и работодатели. Заполнить →

Статья Обнаружение CVE в ядре Linux: автоматизация поиска незакрытых уязвимостей в backport-патчах

Серверный блейд, наполовину извлечённый из тёмной стойки, с янтарным текстом об уязвимости на ЖК-панели. Тёплый свет лампы и холодное бирюзовое свечение монитора на матовом металле.


Когда в upstream-ядре закрывают use-after-free или переполнение буфера, исправление попадает в mainline за дни. А вот в вашем RHEL 8 с ядром 4.18 или Ubuntu 22.04 LTS с 5.15 этот патч может застрять на недели - или прийти кастрированным. Я видел ситуации, когда вендор формально закрывал CVE, а attack surface оставался нетронутым. Автоматизированное обнаружение CVE в ядре Linux на уровне backport-патчей - единственный способ не полагаться на слепое доверие к вендору дистрибутива. Покажу конкретные техники и инструменты, которыми пользуюсь в ежедневной работе с downstream-деревьями, объясню, почему стандартный grep по changelog бесполезен, и дам пошаговый алгоритм для вашего собственного pipeline.

Почему backport-патчи - слепая зона безопасности ядра​

Модель разработки ядра Linux устроена так: баг фиксится в mainline (ветка Линуса), затем мейнтейнеры stable-веток (Грег Кроа-Хартман и команда) cherry-pick'ают коммит в 6.6.x, 6.1.x и т.д. Дальше вендоры дистрибутивов забирают эти фиксы и адаптируют под свои ядра, которые могут отличаться от upstream на тысячи патчей.

На каждом этапе что-то ломается:
  • Неполный cherry-pick. Upstream-фикс состоит из трёх коммитов, в stable попали два. Третий «зависит от рефакторинга, которого нет в старой ветке». В итоге CVE формально закрыт, но attack surface остаётся.
  • Конфликт при адаптации. Backport требует ручного разрешения merge-конфликтов. Разработчик может неверно адаптировать логику - патч компилируется, проходит тесты, но дыру не затыкает.
  • Задержка. По данным исследователей FixMorph (ACM ISSTA 2021), около 8% всех коммитов в mainline бэкпортируются в старые stable-ветки, но между исходным фиксом и появлением backport проходит больше месяца. В этом окне система голая.
Конкретный пример: - use-after-free в nf_tables (CWE-416, CVSS 7.8 HIGH, вектор CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H). Уязвимость в nft_verdict_init(), которая допускала положительные значения как drop error, приводя к double-free в nf_hook_slow(). Исправление появилось в mainline коммитом f342de4e2f33e0e39165d8639387aa6c19dff660, затем перенесено в stable-ветки 5.15.149, 6.1.76 и 6.6.15. Но между upstream-фиксом и backport в 5.15 прошло несколько недель - а рабочий эксплоит с 99.4% успешностью уже был публичен.

С точки зрения MITRE ATT&CK такие задержки - прямой путь к (T1068): атакующий берёт известную kernel-уязвимость и поднимает привилегии, пока downstream-дистрибутив ждёт backport.

Почему grep по CVE-номеру в changelog недостаточен​

Первое, что делает начинающий инженер - запускает grep CVE-2024-1086 /usr/share/doc/linux-image-[I]/changelog[/I]. Иногда это работает. Чаще - нет.

Коммиты без CVE в сообщении​

Ядро Linux стало собственной CNA (CVE Numbering Authority) только в феврале 2024 года. До этого момента огромная часть security-фиксов попадала в stable без какого-либо упоминания CVE. Коммит выглядел как «nf_tables: fix verdict handling in nft_verdict_init()» - и всё. Идентификатор присваивался позже, иногда через месяцы, когда исследователь или NVD-аналитик связывал коммит с уязвимостью.

После создания kernel CNA картина изменилась: ежедневно присваиваются десятки CVE-идентификаторов коммитам, которые раньше никто не считал security-фиксами. Прозрачности прибавилось, но появилась другая головная боль - лавина CVE, в которой нужно быстро определять, какие уже закрыты вашим downstream-ядром.

Backport меняет commit hash​

git cherry-pick создаёт новый коммит с новым SHA. Upstream-коммит f342de4e2f33 в вашем downstream-дереве будет иметь совершенно другой хэш. Если вендор не добавил строку (cherry picked from commit f342de4e...) в commit message - связь теряется.

Патч адаптирован, а не скопирован​

При backport меняются контекстные строки, имена переменных (из-за отсутствия промежуточных рефакторингов), пути к файлам. Чистый diff между upstream-патчем и downstream-версией покажет расхождения, даже если логика идентична.

Вывод: нужна автоматизация, которая работает на уровне семантики кода, а не текстового поиска.

Linux backport уязвимости: техники автоматического обнаружения​

Разберём основные подходы - от простых git-команд до LLM-агентов.

Метод 1: git patch-id для точного сопоставления​

git patch-id вычисляет хэш содержимого патча, игнорируя контекстные строки, номера строк и whitespace. Это позволяет сопоставить upstream-коммит с cherry-pick'нутым downstream-коммитом, даже если SHA отличается.
Bash:
# Получаем patch-id upstream-коммита (фикс CVE-2024-1086)
git -C linux-upstream show f342de4e2f33 | git patch-id --stable
# Результат: <patch-id-hash> f342de4e2f33e0e39165d8639387aa6c19dff660

# Получаем patch-id всех коммитов в downstream-ветке за период
git -C linux-downstream log --format="%H" v5.15.148..v5.15.149 | \
  while read commit; do
    git show "$commit" | git patch-id --stable
  done | grep "<patch-id-hash>"
Если patch-id совпал - backport идентичен по содержимому (не считая контекста). Самый надёжный метод для «чистых» cherry-pick'ов.

Ограничение: если backport потребовал адаптации (changed context, renamed symbols), patch-id будет другим. Тут нужны инструменты посерьёзнее.

Метод 2: kernel-backport-checker - автоматизация поиска CVE через git​

kernel-backport-checker - command-line утилита, которая определяет, какие CVE закрыты backport-коммитами в конкретном git-репозитории, фильтруя по версии ядра.

Принцип работы:
  1. Загружает маппинг CVE-to-commit из публичных баз (NVD, kernel.org security advisories)
  2. Для каждого CVE находит upstream-коммит(ы)
  3. Ищет соответствующие cherry-pick'и в целевом дереве через patch-id и commit message parsing
  4. Формирует отчёт: какие CVE закрыты, какие - нет
Bash:
# Пример использования (концептуальный - сверяйте с docs)
git clone https://github.com/hardenedlinux/kernel-backport-checker
cd kernel-backport-checker

# Проверка ветки 5.15.y на наличие backport для конкретных CVE
./check_backports.py \
  --repo /path/to/linux-stable \
  --branch linux-5.15.y \
  --kernel-version 5.15.148 \
  --cve-list CVE-2024-1086,CVE-2024-26592,CVE-2024-26594
Ни один из конкурентов в RU SERP не упоминает инструментов для автоматической проверки backport-покрытия - так что запоминайте.

Метод 3:

Coccinelle (spatch) - инструмент семантического патчинга для C-кода. Позволяет писать «семантические шаблоны» (SmPL) для поиска паттернов в коде ядра. Лично я использую его, когда нужно проверить, применена ли конкретная логика фикса, а не конкретный текст патча.

Пример: CVE-2024-1086 исправлялась добавлением проверки на положительные значения в nft_verdict_init(). Пишем SmPL-правило для детектирования отсутствия этой проверки:
C:
// detect_cve_2024_1086.cocci
// Ищем nft_verdict_init без проверки на NF_DROP с положительным error
@@
expression E;
@@

 nft_verdict_init(...)
 {
   ...
-  // Отсутствие проверки: data->verdict.code & NF_VERDICT_MASK
+  // После фикса здесь должна быть валидация verdict code
   ...
 }
Запуск:
Bash:
spatch --sp-file detect_cve_2024_1086.cocci \
  --dir /path/to/kernel-source/net/netfilter/ \
  --include-headers
Coccinelle оперирует абстрактным синтаксическим деревом C-кода, а не текстом. Поэтому он найдёт проблемный паттерн даже если переменные переименованы или код перемещён внутри файла. Для статического анализа ядра Linux в downstream-деревьях - незаменимая штука.

Метод 4: cve-bin-tool для бинарного анализа​

Когда доступа к исходникам downstream-ядра нет (проприетарный embedded-дистрибутив, appliance-прошивка), бинарный анализ - единственный вариант. cve-bin-tool от Intel проверяет бинарные файлы на наличие известных уязвимых версий библиотек и компонентов:
Bash:
pip install cve-bin-tool

# Сканирование образа firmware
cve-bin-tool --sbom-type cyclonedx --sbom-file sbom.json /path/to/firmware/

# Фильтрация по ядерным CVE
cve-bin-tool /path/to/vmlinuz --product linux_kernel
Ограничение: cve-bin-tool работает на уровне версий пакетов, а не патчей. Он скажет «linux_kernel 5.15.148 имеет CVE-2024-1086», но не скажет, применён ли вендорский backport. Для полной картины нужно комбинировать с source-level анализом.

Автоматическое создание backport-патчей: FixMorph и PortGPT​

FixMorph: AST-трансформация для автоматического backporting​

FixMorph и PortGPT - инструменты для создания backport-патчей, а не для обнаружения незакрытых CVE. Они нужны на следующем этапе: когда pipeline из предыдущего раздела выявил отсутствующий backport и надо его сгенерировать.
FixMorph, разработанный для автоматического переноса патчей из mainline в старые stable-ветки, работает в три этапа:
  1. Извлечение синтаксических правок между исходной и пропатченной версией файла
  2. Локализация точки применения в целевой ветке через version control history и clone detection
  3. Адаптация трансформации через анализ alignment между AST mainline и target, включая namespace-адаптации и импорт отсутствующих зависимостей
По данным исследования (ACM ISSTA 2021), FixMorph корректно бэкпортирует 75.1% патчей из 350 тестовых случаев. Три четверти - неплохо, но оставшиеся 25% могут содержать как раз те критические фиксы, где адаптация нетривиальна. Инструмент доступен как Docker-образ:
Bash:
docker pull rshariffdeen/fixmorph:issta21
# Или сборка из исходников
git clone https://github.com/rshariffdeen/fixmorph
docker build -t rshariffdeen/fixmorph .
FixMorph использует LLVM/Clang frontend для анализа AST - привязан к C-коду. Для ядра это идеально, но Go или C++ проекты мимо.

PortGPT: LLM-агент для backporting​

Согласно исследованию на arxiv (2024), PortGPT - LLM-агент на базе GPT-4o, который автоматизирует backporting, имитируя рабочий процесс живого разработчика. Отличия от rule-based подходов:
  • Доступ к git history: агент самостоятельно исследует историю коммитов, чтобы понять, когда функция была переименована или перемещена
  • Compiler feedback loop: если backport не компилируется, PortGPT анализирует ошибки и корректирует патч
  • Per-hunk adaptation: каждый «кусок» (hunk) патча адаптируется отдельно с учётом контекста целевой ветки
Цифры: 89.15% успеха на 1815 тестовых случаях (включая 1465 Linux kernel CVE из набора TSBPORT и 350 из набора FixMorph). На 146 сложных случаях (C, C++, Go) - 62.33%. Девять патчей, сгенерированных PortGPT, были приняты в Linux kernel community.

Но есть нюанс. Как отмечает один из авторов исследования Чжаоян Ли: «В репозиториях с плохой или непоследовательной историей коммитов - например, с неполными сообщениями или squashed-коммитами - производительность PortGPT может снизиться из-за отсутствующей или вводящей в заблуждение контекстной информации». А в downstream-деревьях многих вендоров история коммитов куда грязнее, чем в mainline Linux.

CVE detection kernel: пошаговый практический pipeline​

Вот конкретный алгоритм, которым я пользуюсь для проверки downstream-ядра на наличие незакрытых CVE. Подход комбинирует несколько методов, чтобы минимизировать false negatives.
📚 Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме

Этот пятишаговый pipeline - основа автоматизированного аудита безопасности ядра для downstream-дистрибутивов. Он ловит как точные cherry-pick'и, так и адаптированные backport'ы.

Анализ безопасности ядра Linux: реальные примеры незакрытых backport​

Чтобы показать, зачем всё это нужно на практике, разберём несколько CVE из верифицированных данных NVD, где backport-процесс создаёт реальные дыры.

ksmbd: каскад уязвимостей с разными сроками backport​

Серия CVE в модуле ksmbd - типичная история: несколько связанных уязвимостей закрываются в разных stable-ветках в разное время.

CVEТипCVSSCWEИсправлено в mainlineBackport в stable
CVE-2024-26592UAF (race condition)7.8 HIGHCWE-4166.86.1.75, 6.6.14, 6.7.2
CVE-2023-52440Heap buffer overflow (slub)7.8 HIGHCWE-1196.55.15.145, 6.1.53, 6.4.16 (версии stable требуют верификации по git.kernel.org)
CVE-2024-26594OOB read7.1 HIGHCWE-1256.86.1.75, 6.6.14, 6.7.2
CVE-2023-52442Пропуск проверки session/tree id (авторская оценка: CWE-863 Incorrect Authorization)5.5 MEDIUMНет данных6.55.15.x, 6.1.x, 6.4.x
CVE-2023-52441OOB access7.8 HIGHCWE-1196.55.15.145, 6.1.53, 6.4.16

Обратите внимание: CVE-2024-26592 (race condition в ksmbd_tcp_new_connection, CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) и CVE-2024-26594 (невалидированный mech token) попали в stable 6.1.75, а CVE-2023-52440 (переполнение буфера в ksmbd_decode_ntlmssp_auth_blob) - в 6.1.53, то есть намного раньше.

Без автоматизированного сканирования вы эту картину не увидите - вендорский changelog может не упоминать половину этих CVE по номерам.

USB-audio и UVC: уязвимости из каталога CISA KEV​

CVE-2024-53150 (OOB read в USB-audio clock source parsing, CVSS 7.1 HIGH, CWE-125) и CVE-2024-53197 (OOB access в USB-audio для устройств Extigy и Mbox, CVSS 7.8 HIGH, CWE-787) были внесены CISA в каталог активно эксплуатируемых уязвимостей в апреле 2025 года. Обе связаны с тем, что bogus USB-устройство может предоставить некорректные значения bLength или bNumConfigurations, провоцируя out-of-bounds доступ.

Эти уязвимости особенно интересны в контексте техники (T1200, initial-access) при физическом доступе к машине - воткнул специально подготовленную флешку и атакуешь ядро напрямую.

Автоматизированный аудит безопасности ядра: сборка CI/CD pipeline​

Для интеграции проверок в CI/CD объединяем всё вышеописанное в единый скрипт:
Python:
#!/usr/bin/env python3
"""
kernel_cve_audit.py - автоматизированный аудит backport-патчей
Пример для демонстрации концепции
"""

import subprocess
import json
import sys
from pathlib import Path

class KernelCVEAuditor:
    def __init__(self, upstream_repo, downstream_repo, branch):
        self.upstream = Path(upstream_repo)
        self.downstream = Path(downstream_repo)
        self.branch = branch
        self._downstream_index = None

    def build_patchid_index(self):
        """Индексируем все patch-id в downstream (batch-режим)"""
        # Batch: один pipe вместо N вызовов git show
        log_proc = subprocess.Popen(
            ["git", "-C", str(self.downstream),
             "log", "-p", self.branch],
            stdout=subprocess.PIPE
        )
        pid_result = subprocess.run(
            ["git", "patch-id", "--stable"],
            stdin=log_proc.stdout,
            capture_output=True, text=True
        )
        log_proc.stdout.close()
        log_proc.wait()
        index = {}
        for line in pid_result.stdout.strip().split('\n'):
            if not line:
                continue
            parts = line.split()
            if len(parts) >= 2:
                patch_id, commit_hash = parts[0], parts[1]
                index[patch_id] = commit_hash
        self._downstream_index = index
        return index

    def check_cve(self, cve_id, upstream_commit):
        """Проверяем наличие backport для конкретного CVE"""
        # Метод 1: patch-id matching
        show = subprocess.run(
            ["git", "-C", str(self.upstream),
             "show", upstream_commit],
            capture_output=True, text=True
        )
        pid = subprocess.run(
            ["git", "patch-id", "--stable"],
            input=show.stdout,
            capture_output=True, text=True
        )
        if pid.stdout.strip():
            patch_id = pid.stdout.strip().split()[0]
            if patch_id in self._downstream_index:
                return {
                    "cve": cve_id,
                    "status": "FIXED",
                    "method": "patch-id exact match",
                    "downstream_commit":
                        self._downstream_index[patch_id]
                }

        # Метод 2: поиск cherry-pick reference
        result = subprocess.run(
            ["git", "-C", str(self.downstream), "log",
             "--grep", f"cherry picked from commit {upstream_commit[:12]}",
             "--oneline", self.branch],
            capture_output=True, text=True
        )
        if result.stdout.strip():
            return {
                "cve": cve_id,
                "status": "FIXED",
                "method": "cherry-pick reference",
                "downstream_commit":
                    result.stdout.strip().split()[0]
            }

        # Метод 3: поиск по затронутым файлам
        # (требует дополнительной верификации)
        changed_files = subprocess.run(
            ["git", "-C", str(self.upstream),
             "diff-tree", "--no-commit-id", "--name-only",
             "-r", upstream_commit],
            capture_output=True, text=True
        )
        return {
            "cve": cve_id,
            "status": "NOT_FOUND",
            "method": "all methods exhausted",
            "affected_files":
                changed_files.stdout.strip().split('\n'),
            "action": "MANUAL_REVIEW_REQUIRED"
        }


if __name__ == "__main__":
    auditor = KernelCVEAuditor(
        upstream_repo="/src/linux-upstream",
        downstream_repo="/src/linux-downstream",
        branch="ubuntu/focal"
    )
    print("[*] Building patch-id index...")
    auditor.build_patchid_index()

    cves_to_check = [
        ("CVE-2024-1086", "f342de4e2f33e0e39165d8639387aa6c19dff660"),
        # Добавьте другие CVE и коммиты
    ]

    results = []
    for cve_id, commit in cves_to_check:
        result = auditor.check_cve(cve_id, commit)
        results.append(result)
        status_marker = "+" if result["status"] == "FIXED" else "!"
        print(f"[{status_marker}] {result['cve']}: "
              f"{result['status']} ({result['method']})")

    # Экспорт результатов
    with open("audit_report.json", "w") as f:
        json.dump(results, f, indent=2)
Этот скрипт прикручивается к CI через cron-задачу или webhook при обновлении NVD-фида. На выходе - JSON-отчёт с чётким разделением на FIXED и NOT_FOUND, где последние требуют ручного разбора.

Сравнение инструментов для kernel CVE сканирования​

ИнструментПодходТочностьЯзык/форматПокрытиеСтоимость
git patch-idХэш содержимогоВысокая (только exact match)CLI / bashТолько чистые cherry-pickБесплатно
kernel-backport-checkerpatch-id + NVD mappingСредняяPython / CLILinux kernel CVEБесплатно
coccinelle (spatch)Семантический анализ ASTВысокаяSmPL / CЛюбой C-код ядраБесплатно
FixMorphAST-трансформация75.1% на 350 патчахC / DockerLinux kernel (C)Бесплатно
PortGPTLLM-агент (GPT-4o)89.15% на 1815 случаяхPython / APIC, C++, GoТребует API GPT-4o
cve-bin-toolБинарный анализ версийНизкая (version-level)Python / CLIШирокое (не только ядро)Бесплатно
CVE ScanSBOM + kernel configСредняя-ВысокаяSaaS / On-premiseYocto, Buildroot, ZephyrКоммерческий

Моя рекомендация - комбинация: patch-id для быстрого скрининга, coccinelle для верификации сложных случаев, и cve-bin-tool / CVE Scan для embedded-систем без доступа к исходникам.

Типичные ловушки при поиске уязвимостей в патчах ядра​

Опыт работы с downstream-деревьями RHEL и Ubuntu LTS научил меня нескольким вещам, которые автоматика регулярно пропускает:

Многокоммитный фикс. CVE закрывается серией из 3-5 коммитов. Вендор бэкпортирует 4 из 5 - patch-id для каждого совпадает, аудит показывает «FIXED». Но пятый коммит, с ключевой проверкой, отсутствует. Решение: парсить Fixes: тег в upstream-коммитах и проверять всю цепочку.

Backport вносит регрессию. Патч адаптирован, компилируется, но из-за отсутствия промежуточного рефакторинга работает криво. Coccinelle тут помогает частично - можно написать правило, проверяющее наличие конкретных guard conditions, но полная верификация требует runtime-тестирования (syzkaller, kselftest).

CVE присвоен задним числом. Коммит abc123 попал в stable-5.15 полгода назад как «bugfix». Вчера ему присвоили CVE. Ваш аудит-скрипт его не проверяет, потому что на момент последнего запуска CVE не существовал. Решение: регулярное обновление CVE-to-commit маппинга и полное пересканирование.

Заключение​

Обнаружение CVE в ядре Linux на уровне backport-патчей - не разовая акция, а конвейер, который должен крутиться постоянно. Десятки CVE-идентификаторов от kernel CNA каждый день, задержки backport'ов в stable-ветках и неизбежные ошибки адаптации делают ручной аудит физически невозможным.

Комбинация git patch-id для быстрого скрининга, coccinelle для семантической верификации и Python-скриптов для оркестрации NVD-фидов закрывает большинство сценариев. Для масштабных проектов стоит присмотреться к FixMorph (AST-based, 75.1% точность) или PortGPT (LLM-based, 89.15%) - особенно если ваша команда поддерживает собственный downstream-форк ядра.

Главное правило: никогда не доверяйте changelog. Проверяйте патчи на уровне кода - автоматически, регулярно и с понятным fallback на ручной анализ. Прогоните pipeline из шага 5 на своём ядре прямо сейчас - если в отчёте вылезет хоть один MISS с CVSS > 7.0, считайте, что статья себя окупила.
 
Последнее редактирование модератором:
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab