Закрыть
В мире КиберГрамотность Обзоры Угрозы

Разбираемся с Integer Overflow: Переполнение целого числа в libxml2

Разбираемся с Integer Overflow: Переполнение целого числа в libxml2

В повседневной жизни числа кажутся бесконечными. Но внутри нашего устройства числа хранятся в контейнерах фиксированного размера, и когда в результате вычисления получается значение, которое слишком велико или слишком мало для своего контейнера, мы получаем  integer overflow или integer underflow

Именно здесь и возникают integer overflow и integer underflow.

Эти проблемы — не просто курьезы, они могут вызывать логические ошибки, сбои и даже уязвимости в системе безопасности. Давайте разберемся в этом шаг за шагом.

0. Знаковые и беззнаковые целые числа

Разбираемся с Integer Overflow: Переполнение целого числа в libxml2

В программировании есть два основных типа целых чисел: signed (со знаком) и unsigned (без знака).

  • Signed integer (со знаком) может представлять как положительные, так и отрицательные числа (включая ноль). На большинстве систем 32-битный int имеет диапазон от −2,147,483,648 до +2,147,483,647.

Если посмотреть в шестнадцатеричном представлении, то граница у знаковых чисел проходит между:

  • 0x7fffffff (максимальное положительное: 2,147,483,647)
  • и 0x80000000 (минимальное отрицательное: −2,147,483,648)

После достижения максимума значение «переворачивается» в отрицательное и снова растёт вверх, пока не вернётся к −1.

1. Знаковые целые числа (Signed Integers)

Как уже сказано, знаковые целые могут хранить и положительные, и отрицательные значения.

Для 32-битного int диапазон равен: −2,147,483,648 … 2,147,483,647.


1.1 Переполнение знаковых чисел (Signed Integer Overflow)

Переполнение происходит, когда мы превышаем максимальное положительное значение.

Разбираемся с Integer Overflow: Переполнение целого числа в libxml2

Вывод:

Разбираемся с Integer Overflow: Переполнение целого числа в libxml2

Как мы видим, произошел переполнение целого числа с знаком. Значение «перескочило» с максимального на минимальное отрицательное.

1.2 Обратное переполнение (Signed Integer Underflow)

Обратное переполнение возникает, когда мы уходим ниже минимального отрицательного значения.

Разбираемся с Integer Overflow: Переполнение целого числа в libxml2

Вывод:

Разбираемся с Integer Overflow: Переполнение целого числа в libxml2

Здесь значение минимального отрицательного числа «прыгает» в максимально положительное.

2. Беззнаковые целые числа (Unsigned Integers)

Беззнаковые числа хранят только ноль и положительные значения.

Для 32-битного unsigned int диапазон: 0 … 4,294,967,295.


2.1 Переполнение беззнаковых (Unsigned Overflow)

Когда мы превышаем максимум, значение возвращается к нулю.

Разбираемся с Integer Overflow: Переполнение целого числа в libxml2

Вывод:

Разбираемся с Integer Overflow: Переполнение целого числа в libxml2


2.2 Обратное переполнение беззнаковых (Unsigned Underflow)

Если вычесть единицу из нуля, значение перепрыгнет к максимуму.

Разбираемся с Integer Overflow: Переполнение целого числа в libxml2

Вывод:

Разбираемся с Integer Overflow: Переполнение целого числа в libxml2


✨ Итог

  • Signed Integer переполняется из положительных в отрицательные и наоборот.
  • Unsigned Integer переполняется по кругу: от максимума к нулю и от нуля к максимуму.

В реальных библиотеках (например, libxml2) переполнения индексов могут приводить к выходу за границы массива (Out-Of-Bounds) и уязвимостям, позволяющим атакующему повредить память программы.


libxml2: Переполнение целого числа, приводящее к переполнению кучи в xmlRegEpxFromParse

Переполнение буфера в куче существует в функции xmlRegEpxFromParse внутри xmlregexp.c. Уязвимость возникает из-за переполнения целого числа при вычислении индекса в таблице переходов во время компиляции регулярного выражения, используемого для валидации DTD.

Как работает xmlRegEpxFromParse

Когда libxml2 обрабатывает XML-документ с DTD, содержащим описание содержимого (content model), внутри создаётся автомат (state machine). Этот автомат затем компилируется в регулярное выражение функцией xmlRegEpxFromParse.

Разбираемся с Integer Overflow: Переполнение целого числа в libxml2

Чтобы хранить переходы состояний автомата, функция выделяет массив transitions [1]. Далее она проходит по всем состояниям и переходам и заполняет таблицу. Индекс внутри массива вычисляется так:index = stateno * (nbatoms + 1) + atomno + 1

Где:

  • stateno — номер (перенумерованный индекс) текущего состояния автомата.
  • nbatoms — количество уникальных элементов (атомов) в модели содержимого.
  • atomno — номер текущего атома (элемента), по которому строится переход.


Что идёт не так

Все три переменные (stateno, nbatoms, atomno) — это знаковые 32-битные целые числа (int).
Если автомат достаточно большой, произведение stateno * (nbatoms + 1) может превысить максимально допустимое значение 2,147,483,647. Тогда возникает переполнение целого числа: результат «оборачивается» в отрицательное значение.

Этот отрицательный индекс используется для обращения к массиву transitions. В итоге:

  • на строке [2] (xmlregexp.c) происходит чтение из неверного места в памяти,

  • на строке [3] выполняется запись по тому же неверному адресу.

Результат — повреждение кучи (heap corruption).


Пример данных (PoC)

Разбираемся с Integer Overflow: Переполнение целого числа в libxml2

Что здесь происходит:

  • nbatoms = 46341Это количество уникальных имён элементов (a0 … a46340).
  • statenoСоответствует состояниям автомата. Например:
    0 = старт (ожидаем a0)
    1 = после a0 (ожидаем a1)
    2 = после a1 (ожидаем a2)
    ...
    
  • atomnoЭто индекс конкретного элемента (атома), по которому происходит переход. Например:
    atomno 0 → a0
    atomno 1 → a1
    atomno 2 → a2
    ...
    

Ключевой момент:

46341 * (46341 + 1) + 1 = 0x8000c71f

Это значение уже выходит за пределы допустимого диапазона 32-битного int. Именно тут и происходит integer overflow.

Почему проверка выделения памяти не спасает?

Функция xmlRegCalloc2[1] действительно содержит проверки на переполнение, но они касаются только размера выделяемой памяти. Внутри используются типы size_t (64-битные на современных системах).

Однако индексирование в xmlRegEpxFromParse ведётся через 32-битные int, и именно там случается переполнение. То есть:

  • библиотека может успешно выделить блок памяти более 2 ГБ,
  • но расчёт индекса «сломается» из-за переполнения 32-битного int,
  • и мы получаем выход за границы массива (out-of-bounds).

PoC

Разбираемся с Integer Overflow: Переполнение целого числа в libxml2

Инструкции по сборке и запуску PoC:

Разбираемся с Integer Overflow: Переполнение целого числа в libxml2

В итоге мы имеем классическую уязвимость: переполнение целого числа → выход за границы массива → повреждение кучи.


Asan

Разбираемся с Integer Overflow: Переполнение целого числа в libxml2


Источник:

[1] https://project-zero.issues.chromium.org/issues/432450829

Оставить Комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *