Символьное выполнение (Z3 SMT Solver)

Модуль символьного выполнения для проверки достижимости путей в анализе помеченных данных. Использует Z3 SMT-решатель для фильтрации нереализуемых путей, снижая уровень ложных срабатываний до 1-3%.

Обзор

Когда анализ помеченных данных (анализ заражения данных) находит путь от источника к приёмнику, путь может проходить через ветвления с противоречивыми условиями (например, if (x > 10) и далее if (x < 5) для той же переменной). Символьное выполнение проверяет реализуемость таких путей, кодируя условия ветвлений как ограничения Z3 и вызывая решатель.

Ключевые характеристики: - Опциональный компонент — корректно деградирует при отсутствии Z3 - Дополнительные затраты: ~20-25% времени, ~25-30% памяти - Максимум 20 ограничений на путь (настраивается) - Рекурсивный нисходящий анализатор выражений условий - Поддержка целочисленных сравнений, булевой логики, арифметики, моделей функций, проверок NULL - Определение направления ветки через анализ преемников CFG - Настраиваемые таймауты решателя (500 мс для LIA, 2 с для UF-запросов)

Установка

pip install z3-solver

Z3 включён в requirements.txt. Если Z3 недоступен во время выполнения, система записывает предупреждение и возвращает консервативные оценки (все пути считаются реализуемыми).

Архитектура

TaintPropagator (enable_symbolic_execution=True)
    
    ├─ find_taint_paths()    необработанные пути
    
    └─ PathConstraintTracker.filter_infeasible_paths()
        
        ├─ _collect_constraints(start, end)
           ├─ Обход рёбер CFG, извлечение условий из CDG
           └─ _determine_branch_direction() через преемников CFG
        
        ├─ _parse_condition(code, z3_vars, state)
           └─ Рекурсивный спуск: or  and  not  comparison  additive  atom
               ├─ Переменные: z3.Int('x')
               ├─ Функции: strlen(s)  новая Int с ограничениями домена
               └─ NULL: z3.Bool('ptr_is_null')
        
        └─ z3.Solver().check()  (с таймаутом)
            ├─ sat      путь реализуем (достоверность 0.9)
            ├─ unsat    путь нереализуем, ОТФИЛЬТРОВАТЬ (достоверность 0.95)
            └─ unknown  консервативно, сохранить путь (достоверность 0.4)

Справочник API

PathConstraint

@dataclass
class PathConstraint:
    node_id: int       # ID узла CPG (точка ветвления)
    condition: str     # Выражение условия (например, "x > 100")
    location: str      # Расположение файл:строка
    negated: bool      # True для else-ветки

SymbolicExecutionConfig

@dataclass
class SymbolicExecutionConfig:
    enabled: bool = True
    max_constraints: int = 20
    solver_timeout_ms: int = 500        # Запросы QF_LIA
    solver_timeout_uf_ms: int = 2000    # Запросы с неинтерпретируемыми функциями
    max_parse_depth: int = 10           # Ограничение глубины рекурсивного спуска
    enable_function_models: bool = True # Модели strlen, sizeof, len
    enable_arithmetic: bool = True      # +, -, * в условиях

PathConstraintTracker

class PathConstraintTracker:
    def __init__(self, cpg_service, max_constraints: int = 20,
                 config: Optional[SymbolicExecutionConfig] = None):
        """
        Аргументы:
            cpg_service: экземпляр CPGQueryService для запросов к графу
            max_constraints: максимум ограничений на путь (по умолчанию: 20).
                Пути с большим числом ограничений считаются реализуемыми.
            config: необязательная конфигурация таймаутов и функций решателя.
        """

    def is_path_feasible(self, start_node: int, end_node: int) -> Tuple[bool, float]:
        """
        Проверить реализуемость пути между двумя узлами.

        Возвращает:
            (is_feasible, confidence) — достоверность от 0.0 до 1.0
        """

    def filter_infeasible_paths(
        self, paths: List[Any], min_confidence: float = 0.8
    ) -> List[Any]:
        """
        Отфильтровать нереализуемые пути из списка.

        Аргументы:
            paths: список объектов путей (должны иметь атрибут .path_nodes —
                   список ID узлов; первый элемент = источник, последний = приёмник)
            min_confidence: минимальная достоверность (по умолчанию: 0.8)

        Возвращает:
            Отфильтрованный список — только реализуемые пути
        """

    def get_statistics(self) -> Dict[str, Any]:
        """
        Получить статистику решателя.

        Возвращает:
            Словарь с ключами:
            - z3_available (bool): установлен ли Z3
            - max_constraints (int): максимум ограничений на путь
            - solver_timeout_ms (int): таймаут LIA-запросов
            - solver_timeout_uf_ms (int): таймаут UF-запросов
            - enable_function_models (bool): модели функций включены
            - enable_arithmetic (bool): арифметика включена
            - supported_functions (list): известные модели функций
        """

Использование

Прямой вызов API

from src.analysis.dataflow.symbolic_execution import PathConstraintTracker

tracker = PathConstraintTracker(cpg_service, max_constraints=20)

# Проверка одного пути
is_feasible, confidence = tracker.is_path_feasible(node_123, node_456)
if not is_feasible and confidence > 0.9:
    print("Путь нереализуем — вероятно ложное срабатывание")

# Фильтрация пакета путей
feasible_paths = tracker.filter_infeasible_paths(all_paths, min_confidence=0.8)

Интеграция с анализом помеченных данных

from src.analysis.dataflow.taint_analysis import TaintPropagator

propagator = TaintPropagator(
    cpg_service,
    enable_symbolic_execution=True  # Включает Z3-фильтрацию
)

# Символьное выполнение применяется автоматически
paths = propagator.find_taint_paths(
    sources=["recv", "getenv"],
    sinks=["strcpy", "sprintf"]
)
# paths уже отфильтрованы — нереализуемые пути удалены

Поддерживаемые типы ограничений

Ограничение Пример Кодирование в Z3
Целочисленное (переменная и константа) x > 100 z3.Int('x') > 100
Целочисленное (переменная и переменная) a > b z3.Int('a') > z3.Int('b')
Равенство x == 0 z3.Int('x') == 0
Неравенство x != 5 z3.Int('x') != 5
Булев флаг flag z3.Int('flag') != 0
Логическое И x > 0 && y < 10 z3.And(x > 0, y < 10)
Логическое ИЛИ x > 0 \|\| y < 10 z3.Or(x > 0, y < 10)
Логическое НЕ !flag z3.Not(flag != 0)
Арифметика x + 1 > 10 z3.Int('x') + 1 > 10
Модель функции strlen(s) > 10 z3.Int('strlen_s') > 10strlen_s >= 0)
Проверка NULL ptr == NULL z3.Bool('ptr_is_null') == True
Проверка nullptr ptr != nullptr z3.Bool('ptr_is_null') == False

Не поддерживается: сравнения с плавающей точкой, строковые операции, межпроцедурные ограничения.

Модели функций

Известные чистые функции моделируются как неинтерпретируемые функции с ограничениями домена:

Функция Ограничение домена Языки
strlen, strnlen, wcslen result >= 0 C/C++
sizeof result > 0 C/C++
len, length result >= 0 Go, Python, Java
size, count, cap result >= 0 Go, C++ STL
abs result >= 0 Все

Одна и та же функция с тем же аргументом использует одну Z3-переменную, что позволяет обнаруживать противоречия (например, strlen(s) > 10 && strlen(s) < 5 → нереализуемо).

Уровни достоверности

Результат решателя Реализуем? Достоверность Действие
sat Да 0.9 Сохранить путь
unsat Нет 0.95 Удалить (ложное срабатывание)
unknown Предположительно да 0.4 Сохранить (консервативно)
Z3 недоступен Предположительно да 0.5 Сохранить (деградированный режим)
Слишком много ограничений Предположительно да 0.3 Сохранить (пропуск анализа)

Конфигурация

Через config.yaml:

symbolic_execution:
  enabled: true
  max_constraints: 20
  solver_timeout_ms: 500
  solver_timeout_uf_ms: 2000
  max_parse_depth: 10
  enable_function_models: true
  enable_arithmetic: true

Влияние на производительность

Метрика Без Z3 С Z3 Разница
Время анализа Базовое +20-25% Нагрузка решателя
Память Базовая +25-30% Внутренние структуры Z3
Ложные срабатывания ~12% ~1-3% Нереализуемые пути удалены

Ограничения

  1. Облегчённый анализ — не полноценный символьный исполнитель; отслеживает только условия ветвлений
  2. Без плавающей точки — нет поддержки float/double (замедление Z3 в 100-1000 раз)
  3. Без строковых операций — нет strcmp, равенства строк (замедление Z3 в 50-500 раз)
  4. Максимум 20 ограничений — длинные пути считаются реализуемыми
  5. Без межпроцедурного анализа — ограничения собираются в пределах одной функции
  6. Только линейная арифметикаvar * const поддерживается, var * var — нет

Связанная документация


Модуль: src/analysis/dataflow/symbolic_execution.py Последнее обновление: март 2026