Анализ прошивки и механизма загрузки rootfs операционной системы FortiOS 8.0

I. Введение.

В современной индустрии сетевой безопасности решения от Fortinet (в частности, линейка FortiGate) занимают одну из лидирующих позиций. Они широко применяются как в корпоративном сегменте, так и в критической инфраструктуре. Однако статистика инцидентов показывает, что данные устройства регулярно становятся объектами атак, зачастую с использованием 0-day уязвимостей. Это стимулирует интерес сообщества к независимому аудиту безопасности и поиску уязвимостей в прошивке данных устройств.

Ключевым этапом любого исследования, целью которого является поиск уязвимостей (Vulnerability Research), является получение доступа к файловой системе устройства. Однако, в последних версиях прошивок Fortinet, файл, представляющий собой файловую систему (rootfs), поставляется в зашифрованном виде. Попытки анализа данного файла с использованием стандартных инструментов или утилит для распаковки (например, binwalk), в данном случае, не дают никакого результата.

Так как сам файл образа файловой системы не содержит никакой информации о методе своего шифрования, единственным вектором для анализа становится механизм его загрузки — ядро Linux. В этой статье мы опишем полный цикл исследования: от подготовки бинарного файла ядра для работы в дизассемблере до восстановления алгоритмов шифрования, преодоления обфускации ключей и реализации инструмента для дешифровки и распаковки файловой системы.

II. Первичный анализ.

В качестве объекта исследования был выбран образ прошивки FortiOS 8.0 для виртуализации KVM. После распаковки архива мы получаем файл образа диска boot.qcow2. Данный образ можно смонтировать, либо разархивировать (например, с помощью 7-zip), после чего мы получаем доступ к загрузочному разделу.

Рисунок 1 - Загрузочный раздел FortiOS 8.0

В первую очередь нас интересуют три ключевых файла:

  • extlinux.conf — конфигурационный файл загрузчика.
  • rootfs.gz — образ файловой системы.
  • vmlinuz — сжатое ядро Linux.

Первым делом заглянем в extlinux.conf:

DEFAULT vmlinuz ramdisk_size=870400  crashkernel=192M softlockup_panic=0 hung_task_panic=0 initrd=/rootfs.gz tpm_tis.interrupts=0 pcie_aspm=off

Параметр initrd=/rootfs.gz указывает ядру использовать этот файл как начальную файловую систему в оперативной памяти.

Теперь попробуем классифицировать rootfs.gz стандартными утилитами. Команда file выдает неожиданный результат:

$ file rootfs.gz
rootfs.gz: OpenPGP Public Key

Идентификация файла размером ~150 МБ как "OpenPGP Public Key" является очевидным ложным срабатыванием, вызванным, вероятно, совпадением сигнатур в заголовке или специфичной структурой данных.

Запуск binwalk для поиска вложенных файловых систем или архивов также не дает успешного результата:

$ binwalk rootfs.gz

DECIMAL       HEXADECIMAL     DESCRIPTION
---------------------------------------------------------------------------
18353219      0x1180C43       MySQL MISAM compressed data file Version 3
58858910      0x3821D9E       mcrypt 2.5 encrypted data, algorithm: "",
                              keysize: 8789 bytes, mode: "B",

Утилита обнаруживает лишь случайные сигнатуры глубоко внутри файла (MySQL, mcrypt), игнорируя начало файла. Это типичная картина для зашифрованных данных с высокой энтропией.

Если посмотреть содержимое файла в hex-редакторе, то мы не увидим ничего, кроме случайного шума:

Рисунок 2 - Содержимое rootfs.gz

Как уже было сказано ранее, информацию о том как расшифровать rootfs.gz, мы можем получить только из ядра vmlinuz. Однако анализ "сырого" сжатого ядра в дизассемблере не самый эффективный подход. Существует утилита vmlinux-to-elf, которая позволяет распаковать ядро, восстановить имена функций, и секции, что сильно упростит будущий анализ.

$ vmlinux-to-elf vmlinuz vmlinuz.elf

[+] Kernel successfully decompressed in-memory (the offsets that
    follow will be given relative to the decompressed binary)
[+] Version string: Linux version 6.1.62 (root@2de49d193207) (clang 
    version 14.0.6 (https://github.com/llvm/llvm-project.git 
    fcd3695c47ca861e38f9098199c92b117b0d9cd9), LLD 14.0.6) # SMP 
[+] Guessed architecture: x86_64 successfully in 0.51 seconds
[+] Found kallsyms_token_table at file offset 0x014d79d8
[+] Found kallsyms_token_index at file offset 0x014d7d58
[+] Found kallsyms_markers at file offset 0x014b83a0
[+] Found kallsyms_names at file offset 0x01433b88 (42630 symbols)
[+] Found kallsyms_num_syms at file offset 0x01433b80
[i] Negative offsets overall: 100 %
[i] Null addresses overall: 0 %
[+] Found kallsyms_offsets at file offset 0x0140a160
[+] Successfully wrote the new ELF kernel to vmlinuz1.elf

На выходе мы получаем полноценный ELF-файл (vmlinuz.elf), готовый к загрузке в дизассемблер.

III. Общий алгоритм

Загрузив полученный vmlinuz.elf в дизассемблер, мы можем приступать к анализу.  Опираясь на опыт анализа предыдущих версий прошивок Fortinet, мы знаем, что непосредственная расшифровка файловой системы ранее выполнялась в функции fgt_verify_decrypt. Поэтому первым делом мы обратились к поиску именно этой функции в восстановленном ELF-файле.

Рисунок 3 - Начало функции fgt_verify_decrypt

Беглый осмотр fgt_verify_decrypt подтверждает, что она по-прежнему отвечает за расшифровку rootfs. В качестве аргументов функция принимает указатели на Key (ключ) и IV (вектор инициализации). Здесь используется AES в режиме ECB (строка 14). Но к детальному разбору алгоритма этой функции мы вернемся позже (см. Раздел VI). Сейчас наша главная задача — понять, откуда берутся криптографические ключи, передаваемые в эту функцию.

Для этого перейдем к родительской функции - fgt_verify_initrd.

Рисунок 4 — Декомпилированный код fgt_verify_initrd

Алгоритм работы этой функции, напоминает проверку цифровой подписи, и, если проверка прошла успешно, вызывается функция fgt_verify_decrypt (строка 35).

Мы видим, что цифровая подпись занимает последние 256 байт initrd (а как мы выяснили из файла extlinux.conf: initrd - это файл rootfs.gz). Сначала вычисляется SHA-256 хэш от всего initrd, исключая последние 256 байт (строка 17).

Интересующие нас аргументы (key и seed_iv), помимо fgt_verify_decrypt, также передаются в fgt_verifier_rsa_verify (строка 20).

Заглянув внутрь fgt_verifier_rsa_verify, мы видим активное использование библиотеки MPI (Multi-Precision Integers), например функции mpi_powm, которая выполняет возведение в степень по модулю.

Рисунок 5 - декомпилированный код fgt_verifier_rsa_verify

Здесь мы видим расшифровку подписи (строка 17), проверку структуры расшифрованных данных (строки 20-24) и извлечение ключей из подписи. А в конце происходит сравнение вычисленного ранее SHA-256 с данными внутри цифровой подписи (строки 33-34). 

Имя функции и алгоритм позволяют утверждать, что используется RSA, однако теперь мы встретились со следующей проблемой: для расшифровки (проверки) подписи нам необходим публичный ключ, константы которого передаются через проприетарную структуру, которую мы назвали “fgt_verifier_ctx

Если мы посмотрим на псевдокод функции fgt_verifier_initrd, то увидим, что адрес на эту структуру передается в функцию fgt_verifier_open

Рисунок 6 - декомпилированный код fgt_verifier_open

Эта функция является конструктором для структуры fgt_verifier_ctx, здесь мы видим начальную инициализацию членов структуры (строки 12-16) и запись необходимых нам значений (строки 30 и 35).

Помимо этого, мы видим вызов fgt_verifier_pub_key, за которым следует asn1_ber_decoder. Наличие декодера ASN.1 — это мощная подсказка. Она говорит о том, что искомый публичный ключ должен быть сформирован в стандарте X.509 или PKCS (формат DER/BER). Однако беглый поиск по строкам и константам не выдает никаких похожих структур в открытом виде. Поэтому переходим к анализу fgt_verifier_pub_key.

IV. Дешифровка публичного ключа

Функция fgt_verifier_pub_key, выполняет подготовку ключей и расшифровку публичного ключа RSA. Исходя из отладочных сообщений (строка 25) и первого аргумента функции crypto_alloc_tfm_node (строка 22), мы можем сделать вывод, что здесь используется алгоритм ChaCha20.

Для начала подготавливаются параметры для шифра — Ключ (Key) и Вектор Инициализации (IV). Однако вместо хранения явных данных, используется массив seed размером 32 байта.

Рисунок 7 - Инициализация key и iv в fgt_verifier_pub_key

Алгоритм генерации Cipher Key (строки 11-13) выглядит следующим образом:

  1. Берутся данные из массива seed, начиная со смещения 5 (27 байт).
  2. К ним добавляются первые 5 байт массива seed.
  3. От полученной последовательности вычисляется хеш SHA-256.
  4. Результат используется как 256-битный ключ для шифра ChaCha20.

Алгоритм генерации IV (строки 19-21) выглядит следующим образом:

  1. Аналогичная операция выполняется со смещением 2: берутся 30 байт, начиная со 2-го байта, и к ним добавляются первые 2 байта seed.
  2. Результат хэширования SHA-256 используется как IV.

Из анализа структуры skcipher_request мы находим источник - адрес, где лежит зашифрованный ключ (строки 52,62) и узнаем длину этого ключа (строка 65).

Рисунок 8 - подготовка skcipher_request

После настройки контекста шифрования происходит вызов функции шифрования (строка 72), которая преобразует зашифрованный массив ciphered_key в открытый public_key.

Восстановив этот алгоритм и владея извлеченными данными из исполняемого файла (seed.bin и encrypted_key.bin), мы можем написать скрипт для дешифрования публичного ключа:

import hashlib
import struct
from Crypto.Cipher import ChaCha20


with open("seed.bin", "rb") as f:
   seed_data = f.read(32)
with open("encrypted_key.bin", "rb") as f:
   enc_data = f.read(270)
  
key_input = seed_data[5:32] + seed_data[0:5]
chacha_key = hashlib.sha256(key_input).digest()

iv_input = seed_data[2:32] + seed_data[0:2]
iv_hash = hashlib.sha256(iv_input).digest()

print(f"Key SHA256: {chacha_key.hex()}")
print(f"IV SHA256:  {iv_hash.hex()}")

kernel_iv = iv_hash[:16]
counter_bytes = kernel_iv[:4]
nonce_bytes = kernel_iv[4:]
counter_val = struct.unpack("<I", counter_bytes)[0]

cipher = ChaCha20.new(key=chacha_key, nonce=nonce_bytes)
cipher.seek(counter_val * 64)
decrypted = cipher.decrypt(enc_data)

with open("decrypted_rsa_pub.der", "wb") as f:
   f.write(decrypted)

Результат работы скрипта сохраняется в файл decrypted_rsa_pub.der. Проверка его содержимого подтверждает, что это корректная структура ASN.1 (DER), содержащая публичный ключ RSA-2048. 

$ openssl rsa -RSAPublicKey_in -inform DER -in decrypted_rsa_pub.der -text

Public-Key: (2048 bit)
Modulus:
    00:b7:9a:8c:86:fb:ac:f0:1a:bf:a6:8b:8b:74:de:
    c1:87:cb:8d:d7:ea:df:93:cc:81:ce:29:17:21:3f:
    a5:d1:df:7d:68:50:94:fb:3e:e9:b5:ef:e1:b9:b4:
    a2:48:81:6c:cb:f6:17:4b:52:2b:1f:0b:96:98:19:
    60:45:7a:64:47:ae:0a:b3:9d:96:7b:aa:6b:cf:bc:
    59:72:b6:72:f4:27:b0:20:a0:b9:4f:a2:90:6f:2a:
    10:0b:c2:4c:fc:e8:11:b8:a0:de:58:ae:d8:40:cc:
    e4:db:03:9c:44:25:a8:77:bd:0b:93:4d:95:be:06:
    23:a4:5c:63:47:08:d6:f9:fe:a9:56:27:1c:a4:04:
    45:38:d4:68:08:36:44:8e:70:27:6d:b2:15:36:d2:
    34:4a:16:94:1b:51:79:4e:2c:93:4b:10:ef:cf:b4:
    ca:8d:3d:6c:55:93:9e:78:84:bc:82:5f:a0:2f:e5:
    45:b4:eb:a4:28:90:ad:c5:e3:02:40:97:4e:64:33:
    96:37:12:02:15:d1:3c:aa:03:fe:35:5c:65:66:a0:
    51:01:1d:8c:f3:2c:40:58:99:7e:97:33:8a:93:17:
    bf:33:65:8d:57:90:ec:bd:12:cd:b3:68:ca:f2:35:
    fb:4f:f9:f8:4d:51:03:5b:8d:6c:07:21:26:8a:2f:
    f3:01
Exponent: 65537 (0x10001)
writing RSA key
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt5qMhvus8Bq/pouLdN7B
h8uN1+rfk8yBzikXIT+l0d99aFCU+z7pte/hubSiSIFsy/YXS1IrHwuWmBlgRXpk
R64Ks52We6prz7xZcrZy9CewIKC5T6KQbyoQC8JM/OgRuKDeWK7YQMzk2wOcRCWo
d70Lk02VvgYjpFxjRwjW+f6pViccpARFONRoCDZEjnAnbbIVNtI0ShaUG1F5TiyT
SxDvz7TKjT1sVZOeeIS8gl+gL+VFtOukKJCtxeMCQJdOZDOWNxICFdE8qgP+NVxl
ZqBRAR2M8yxAWJl+lzOKkxe/M2WNV5DsvRLNs2jK8jX7T/n4TVEDW41sByEmii/z
AQIDAQAB
-----END PUBLIC KEY-----

Теперь мы можем перейти к анализу fgt_verifier_rsa_verify и расшифровке цифровой подписи 

V. Анализ схемы RSA-KEM и извлечение ключей

Анализ псевдокода функции fgt_verifier_rsa_verify показывает, что внутри реализуется не просто верификация подписи, а механизм инкапсуляции ключа (KEM — Key Encapsulation Mechanism). 

Рисунок 9 — Псевдокод fgt_verifier_rsa_verify

Код выполняет операцию «сырого» RSA-дешифрования с помощью функции mpi_powm. Полученный результат представляет собой блок данных размером 256 байт.

Самое интересное кроется в структуре этого дешифрованного блока. Стандартные библиотеки (например, OpenSSL или PyCryptodome) при верификации подписи ожидают найти внутри структуру ASN.1 с идентификатором хэш-алгоритма (OID). Однако Fortinet использует этот контейнер для безопасной передачи ключевой информации (Cryptographic key material), необходимой для дешифровки основного файла.

Структура дешифрованного блока выглядит следующим образом:

Рисунок 10 - Структура RSA-KEM

  1. Padding (PKCS#1 v1.5): Блок начинается со стандартного заголовка 0x00 0x01, за которым следуют байты заполнения 0xFF и нулевой байт-разделитель 0x00.
  2. Cryptographic Key Material: Сразу после разделителя располагаются данные, необходимые для дешифровки rootfs:
    • AES IV: 16 байт вектора инициализации.
    • AES Key: 32 байта ключа шифрования.
  3. Hash: Завершает блок 32-байтный SHA-256 хэш, вычисленный от зашифрованного тела rootfs.

Здесь кроется важный нюанс: именно из-за использования собственной схемы хранения ключевой информации (Key Material вместо ASN.1) стандартные криптографические библиотеки не могут обработать этот контейнер. Они успешно обрабатывают Padding, но не могут интерпретировать содержимое.

Для извлечения ключевой информации нам необходимо реализовать «ручную» обработку RSA-блока: выполнить математическую операцию, проверить корректность Padding и извлечь данные по смещениям.

import hashlib
from Crypto.PublicKey import RSA
from Crypto.Util.number import bytes_to_long, long_to_bytes


with open("decrypted_rsa_pub.der", "rb") as f:
   pub_key_der = f.read()
   rsa_key = RSA.importKey(pub_key_der)
with open("rootfs.gz", "rb") as f:
   data = f.read()
   rootfs_data = data[:-256]
   signature = data[-256:]

# Raw RSA Decrypting
sig_int = bytes_to_long(signature)
decrypted_int = pow(sig_int, rsa_key.e, rsa_key.n)
decrypted_bytes = long_to_bytes(decrypted_int)

# Check PKCS#1 v1.5 padding
if decrypted_bytes[0] != 0x01:
   print(f"[FAIL] decrypted_bytes[0]: {decrypted_bytes[:5].hex()}")
   exit()

if decrypted_bytes[0xAE] != 0x00:
   print(f"[FAIL] decrypted_bytes[0xAE]: {hex(decrypted_bytes[0xAE])}")
   exit()

# Get key material
payload_iv = decrypted_bytes[0xAF: 0xAF + 16]
payload_key = decrypted_bytes[0xBF: 0xBF + 32]
embedded_hash = decrypted_bytes[0xDF: 0xDF + 32]

print("-" * 40)
print(f"PAYLOAD KEY (Hex): {payload_key.hex()}")
print(f"PAYLOAD IV  (Hex): {payload_iv.hex()}")
print("-" * 40)
print(f"SHA256 Hash:       {embedded_hash.hex()}")
print(f"SHA256 Hash rootfs:       {hashlib.sha256(rootfs_data).hexdigest()}")

with open("aes_key.bin", "wb") as f:
   f.write(payload_key)
with open("aes_iv.bin", "wb") as f:
   f.write(payload_iv)

Результат работы скрипта подтверждает целостность данных - хэши совпадают.  

$ python step_2.py 

----------------------------------------

PAYLOAD KEY (Hex): 003de30f5928159d5824c6031d66756317b053240588a86830a14d4c62eb3290

PAYLOAD IV  (Hex): 1609010a35a4b8a9f97aa7f0b15f1a11

----------------------------------------

SHA256 Hash:       73b6197ce681dc70b696d2bbd79583c35cc73a765f839ed562a0d54307525a6a

SHA256 Hash rootfs:       73b6197ce681dc70b696d2bbd79583c35cc73a765f839ed562a0d54307525a6a

Мы успешно извлекли aes_key.bin и aes_iv.bin. Осталось лишь применить стандартный AES-256 и получить файловую систему, но здесь Fortinet приготовил финальную ловушку.

VI. Собственная криптография и дешифрование rootfs.

Впервые посмотрев на функцию fgt_verify_decrypt в глаза бросается то, используется AES в режиме ECB, однако при детальном рассмотрении алгоритма расшифровки, оказывается, что он используется для получения гаммы (keystream), а не для расшифровки rootfs.

Рисунок 11 - Цикл расшифровки rootfs

Таким образом, здесь используется алгоритм AES-CTR, для которого характерно использование “Counter Block”, который шифруется алгоритмом AES-ECB (строка 83) для получения гаммы, и выполняется XOR между зашифрованным блоком данных и гаммой (строки 92-93).

Для расшифровки мы можем воспользоваться утилитой openssl, предварительно вырезав последние 256 байт файла rootfs.gz.

$ openssl enc -d -aes-256-ctr -in rootfs_enc.bin 
-out rootfs_decrypted.gz 
-K 003de30f5928159d5824c6031d66756317b053240588a86830a14d4c62eb3290    
-iv 1609010a35a4b8a9f97aa7f0b15f1a11

$ file rootfs_decrypted.gz
rootfs_decrypted.gz: XZ compressed data, checksum CRC32

$ xz -d rootfs_decrypted.gz 
xz: rootfs_decrypted.gz: Compressed data is corrupt

Итак, мы получили XZ архив, казалось бы, достаточно его разархивировать и получить доступ к внутренним файлам прошивки, но вместо успеха, мы получили сообщение об ошибке. 

При более внимательном изучении алгоритма fgt_verify_decrypt, мы заметили одно очень важное расхождение со стандартным алгоритмом. В стандартном AES-CTR после шифрования Counter Block (для получения гаммы), он увеличивается на единицу. Это необходимо для следующего блока шифрования, чтобы гамма была всегда разная. Но здесь реализован свой алгоритм динамического расчета инкремента на основе AES_IV, полученного из цифровой подписи. Из-за этого верно был расшифрован первый блок данных (так как начальное значение Counter block такое же, как и в стандартной реализации AES-CTR) и утилита file смогла верно определить формат файла.

Рисунок 12 - псевдокод вычисления инкремента.

Анализировать данный алгоритм смысла нет, единственное, на что стоит обратить внимание - в результате инкремент будет в диапазоне** от 1 до 15** (строка 45). Таким образом, для итогового дешифрования нам нужно: либо реализовать алгоритм вычисления счетчика на python, либо перебрать все возможные значения инкремента. Мы пошли по первому пути, но даже при использовании второго подхода, результат получается за приемлемое время - около 2-3 минут.

import struct
from Crypto.Cipher import AES


def rol1(val, n):
   return ((val << n) & 0xFF) | (val >> (8 - n))


def calculate_increment_c_style(iv_bytes):
   data = list(iv_bytes)
   chk = 11
   acc_v11 = 0
   iter_v12 = 0
   
   for i in range(16):
       xor_val = (data[i] ^ (acc_v11 & 0xFF)) & 0xFF
       rol_res = rol1(xor_val, 3)
       
       v14 = (11 * iter_v12) & 0xFFFFFFFF  # 32-bit int
       iter_v12 += 1
       acc_v11 = (rol_res + v14) & 0xFFFFFFFF
       chk = (chk - 11) & 0xFFFFFFFF
       
   res_inc_step = (rol_res - chk) & 0xF
   return res_inc_step if res_inc_step != 0 else 1
   
with open("aes_key.bin", "rb") as f:
   key = f.read()
with open("aes_iv.bin", "rb") as f:
   iv_bytes = f.read()
with open("rootfs.gz", "rb") as f:
   raw_data = f.read()s
   ciphertext = raw_data[:-256]

increment = calculate_increment_c_style(iv_bytes)

cipher = AES.new(key, AES.MODE_ECB)

iv_low = struct.unpack("<Q", iv_bytes[:8])[0]
iv_high = struct.unpack("<Q", iv_bytes[8:])[0]

print(f"iv_bytes: {iv_bytes}")
print(f"INCREMENT: {increment}")

output = bytearray()
total_len = len(ciphertext)
num_blocks = (total_len + 15) // 16

curr_iv_high = iv_high

for i in range(num_blocks):
   block_in = struct.pack("<Q", iv_low) + struct.pack("<Q", curr_iv_high)
   ks = cipher.encrypt(block_in)

   chunk = ciphertext[i * 16: (i + 1) * 16]
   for j in range(len(chunk)):
       output.append(chunk[j] ^ ks[j])
       
   # Update IV
   curr_iv_high = (curr_iv_high + increment) & 0xFFFFFFFFFFFFFFFF
with open("rootfs_decrypted.xz", "wb") as f:
   f.write(output)

В результате мы получаем файл rootfs_decrypted.xz:

$ xz -d rootfs_decrypted.xz

$ file rootfs_decrypted   
rootfs_decrypted: Linux rev 1.0 ext4 filesystem data, UUID=ffb6851c-a619-49f4-9181-8a6fef23f6b4 (extents) (64bit) (large files) (huge files)

Полученный образ файловой системы мы можем либо смонтировать, либо разархивировать по аналогии с boot.qcow2 из начала статьи.

Рисунок 13 - Расшифрованное содержимое rootfs.gz

VII. Вывод

В ходе данного исследования мы прошли путь от анализа бинарного файла ядра до полной расшифровки файловой системы устройства Fortinet. Процесс показал, что вендор активно использует концепцию «Security by Obscurity», выстраивая защиту в несколько эшелонов:

  • Обфускация ключей: Публичный ключ RSA не хранится в открытом виде, а скрыт с помощью алгоритма ChaCha20, параметры для которого генерируются динамически из статических данных в памяти.
  • Нестандартный крипто-контейнер: Использование механизма RSA-KEM с собственной структурой данных вместо стандартной подписи делает невозможным использование общедоступных инструментов анализа и библиотек.
  • Модификация алгоритмов: Изменение логики инкремента счетчика в режиме AES-CTR ломает совместимость со стандартными реализациями AES, требуя от исследователя глубокого анализа алгоритма генерации гаммы.

Подобные модификации стандартов существенно усложняют первичный анализ и отсеивают автоматизированные сканеры, однако не обеспечивают криптографической стойкости против исследователя, имеющего доступ к исполняемому коду (ядру). Поскольку устройство обязано уметь самостоятельно расшифровывать свою файловую систему для загрузки, алгоритм и ключи неизбежно присутствуют в системе.

Paranoid Security Как злоумышленники используют подписанные драйверы для захвата инфраструктуры. Разбор техники BYOVD. 10 октября
Vulnerability Research Как злоумышленники используют подписанные драйверы для захвата инфраструктуры. Разбор техники BYOVD.
Paranoid Security Анализ обновлений Microsoft Patch Tuesday – Сентябрь 2025 9 сентября
MS Patch Tuesday Анализ обновлений Microsoft Patch Tuesday – Сентябрь 2025
Paranoid Security Автоматизация поиска уязвимостей с помощью angr 06 сентября
angr Автоматизация поиска уязвимостей с помощью angr