Как найти 0day

Решил написать о часто встречающейся ошибке в сетевых приложениях и сетевых устройствах. Постараюсь объяснить проблему, на примере Linux стека. И буду больше рассуждать Абстрактно, пытаясь объяснить принцип. Ведь все приложения разные, хоть суть одна. (Переносят биты туда-сюда. ) И к каждому сетевому приложению, или к сетевому устройству нужен свой подход, чтоб исправить допущенные в нём ошибки.


Проблема заключается в тех маленьких порциях данных, которыми обмениваются системы. Точнее не в самих этих порциях, а в том, как они далее собираются и обрабатываются.

Например, одна система отправляет другой системе фразу «Hello World !»

Как это можно сделать? Да как угодно. Выбирается любой протокол передачи данных: TCP, MPTCP, UDP. Данные разбиваются на части, которые мы назовём пакетами. И отправляются на конечную систему. (Как именно? Да это абсолютно не важно. )

Важно то, что(и как) конечная система будет делать с полученными данными. Если строка «Hello World !» была разбита на два пакета:

«Hello » - Это содержимое первого пакета.

«World !» - А это, уже второй пакет.

То после того, как сетевое устройство начнёт принимать пакеты (они могут быть обработаны самим сетевым устройством) при удачной обработке, они будут переданы далее в сетевой стек Операционной Системы (ОС). (О проблеме на этом этапе будет написано далее. )

Если пакеты попали в Сетевой Стек, то ОС обрабатывает данные пакета последовательно. Это значит, что часть пакета будет указывать, как пакет был доставлен до системы. И мы эту часть, смело пропускаем т.к. это Не важно. Отбрасываем всю часть рассуждений вплоть до важного момента — до сборки данных на транспортном уровне.

В конечном итоге, система должна понять какова правильная последовательность пакетов. Ведь, до системы мог сначала дойти пакет

«World !» - второй пакет.

А затем

«Hello » - первый пакет.

И если, ОС не определяла правильную последовательность пакетов, то до приложения пользовательского уровня дойдёт такая строка: «World !Hello ». Разумеется такой подход большинство не устраивает и поэтому протоколы поддерживают номера последовательностей (а также временные метки, но это всё частности, мы будем говорить только о «номерах последовательностей» и назовём их «seq»).

Мы уже оперируем такими важными вещами: Данные и seq. Добавим к нашим знаниям, также «Размер Данных». Данных и их размера, достаточно, чтоб произвести их отправку через сокет. Но зачем тогда упоминать seq? Тут нужно осознать, что можно предугадать состояние второй системы, которая принимает наш «Hello World !»

Смотрим на примере Linux и протокола TCP ( Код привожу из ядра Linux-5.10.7 ) В файле net/ipv4/tcp_ipv4.c есть функция tcp_v4_fill_cb, которая прольёт свет на происходящее на конечной системе. А точнее, нас интересует

TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin +
				    skb->len - th->doff * 4);

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

Например: «Hello » - Первый пакет. Размер 6 байт.

Первый пакет, имеет seq = 0x1 и end_seq = 0x7

«World !» - Второй пакет. Размер 7 байт.

Второй пакет, имеет seq = 0x7 и end_seq = 0xe

Тогда данные собeирутся в строку «Hello World !»

А если второй пакет, имеет seq = 0x4 и end_seq = 0xb

Тогда данные собeрутся в строку «Hello ld !»

Символы «Wor» Будут отброшены. (Это происходит при копировании данных из пакетов, при чтении сокета ) После копирования данных. Через функцию tcp_rcv_nxt_update файла net/ipv4/tcp_input.c указывается значение rcv_nxt в структуре протокола struct tcp_sock. Это значение, будет равняться end_seq после копирования всех пакетов в общий буфер (после чтения данных из сокета). (Первое значение rcv_nxt будет установлено сразу после tcp рукопожатия)

Короче говоря: сначала приходят пакеты и передвигается значение end_seq. Далее данные копируются в общий буфер, и передвигается значение copied_seq структуры struct tcp_sock. И после того, как данные из пакетов (буферов сокетов) будут скопированы в общий буфер, произойдёт установка rcv_nxt в end_seq. И итерация повторяется.

Теперь рассмотрим следующее. Что будет если первый пакет: seq = 0xfffffff0 и размер 0x64 (100) то

TCP_SKB_CB(skb)->end_seq = (0xfffffff0  + 0x1 + 0 +  0x140 - 0x40);
(если  doff = 5 в tcp заголовке)

TCP_SKB_CB(skb)->end_seq = 0xfffffff0 + 0x65 = 0x100000055

Т. е. произошло переполнение, и результат end_seq = 0x55 (85 в десятичной) при этом rcv_nxt = 0xfffffff0 , т. к. ещё копирования данных из сокета не произошло.

Здесь возникает ситуация, когда end_seq < seq ( 0x55 < 0xfffffff0 )

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

Например такой пакет будет корректен. seq = 0xfffffc6e размером в 1000 байт (end_seq = 0x56 ) В этом нет ничего интересного, если не помнить о флаге URG . В Википедии сказано: 16-битовое значение положительного смещения от порядкового номера в данном сегменте. Это поле указывает порядковый номер октета, которым заканчиваются важные (urgent) данные.

И вот тут то, и находятся возможные ошибки. Всё зависит от того, как сетевое устройство обрабатывает urg пакеты. Ошибки могут быть в реализации драйверов. Или в аппаратной части. (при TCP Offload Engine )

При больших значениях seq (близких к целочисленному переполнению), возможности регулировать размер буфера сокета (размером данных пакета), возможностью указать смещение для urg в диапазоне 0 - 0xffff,
установкой значения rcv_nxt. Можно влиять на дальнейшее поведение системы. (на уровне ядра) Добавлю ещё такую информацию. Она не будет лишней.

Проверки номеров последовательностей пакетов, базируются на функциях: after и before. Которые проверяют, где находится число в последовательности "до" указанного числа, либо "после" или "равно".

Вот результаты работы функций с некоторыми числами, как пример.

inline bool before(__u32 seq1, __u32 seq2)
{
   return (__s32)(seq1-seq2) < 0;
}
#define after(seq2, seq1) 	before(seq1, seq2)

after( 0x80000000,  0x7fffffff ) = 1   
after( 0x7fffffff,  0x80000000 ) = 0   
after( 0x80000000,  0x80000000 ) = 0  

before( 0x80000000,  0x7fffffff ) = 0   
before( 0x7fffffff,  0x80000000 ) = 1   
before( 0x80000000,  0x80000000 ) = 0  

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

В большинстве случаев, с помощью данных функций определяется, как следует поступить с пакетом. Если оказывается, что end_seq имеет номер меньший или равный последнему принятому пакету, то такой пакет отбрасывается.

На примере end_seq < seq ( 0x55 < 0xfffffff0 )

В тех случаях, когда происходят математические действия над двумя и более параметрами на которые можно влиять, и сравнение их с каким_то_числом, то

if(TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq >= какое_то_число) {
Можно_попасть_сюда } else { или_сюда }

if(0x55 - 0xfffffff0 >= какое_то_число)
if(0xffffff9 >=  какое_то_число)

Привожу пример кода из файла drivers/net/ethernet/chelsio/inline_crypto/chtls/chtls_cm.c который может содержать ошибку.

В функция handle_urg_ptr присутствует код.

if (skb && tp->copied_seq - ULP_SKB_CB(skb)->seq >= skb->len)
			chtls_free_skb(sk, skb);

В нем присутствуют три переменных: copied_seq, seq, skb->len на которые можно влиять.

На самом деле нет (из-за особенностей работы устройства, нельзя влиять на ULP_SKB_CB(skb)->seq ). Еслиб можно было влиять на (skb)->seq, тогда можно подобрать такие значения seq, copied и skb->len при которых, сработает очистка буфера сокета и код драйвера, продолжит свою работу. ( Обрабатывая пакет, из уже освобождённой памяти )

Далее, рассмотрим такой пример: в файле chtls_cm.c

Есть функция chtls_recv_data в ней присутствует код

	if (unlikely(hdr->urg))
		handle_urg_ptr(sk, tp->rcv_nxt + ntohs(hdr->urg));
	if (unlikely(tp->urg_data == TCP_URG_NOTYET &&
		     tp->urg_seq - tp->rcv_nxt < skb->len))
		tp->urg_data = TCP_URG_VALID |
			       skb->data[tp->urg_seq - tp->rcv_nxt];

Сразу можно обратить внимание на вызов handle_urg_ptr(sk, tp->rcv_nxt + ntohs(hdr->urg)); Где находятся два аргумента: tp->rcv_nxt + ntohs(hdr->urg) .

rcv_nxt можно предсказать, а hdr->urg установить в пакете. Таким образом, передав какое_то_число в функцию handle_urg_ptr для дальнейшей обработки. Но это не так критично, как следующий код.

if (unlikely(tp->urg_data == TCP_URG_NOTYET &&
		     tp->urg_seq - tp->rcv_nxt < skb->len))
		tp->urg_data = TCP_URG_VALID |
			       skb->data[tp->urg_seq - tp->rcv_nxt];

Тут снова видим tp->urg_seq - tp->rcv_nxt < skb->len , два числи на которые может воздействовать отправитель пакетов, сравниваются с значением размера данных сокета, который также указывается отправителем (через количество отправляемых данных). А далее skb->data[tp->urg_seq - tp->rcv_nxt];

Снова два значения, устанавливаемые удалённо tp->urg_seq - tp->rcv_nxt. Обратятся, к данным по индексу - ???

tp->urg_seq - tp->rcv_nxt < skb->len 
Данное условие создаёт иллюзию, что при обращении к массиву skb->data

urg_seq - tp->rcv_nxt  Будут находиться, внутри массива данных сокета.
И не могут быть больше skb->len, ведь чуть выше делалась проверка 
tp->urg_seq - tp->rcv_nxt < skb->len


В суровой реальности индекс массива может быть
skb->data[0xffffffff];
skb->data[0xfffffff0];
skb->data[0xffffff00]; и тд.

Это приводит к чтению памяти, по указному индексу. Либо к зависанию системы.

Далее ещё один пример.

Но уже обработки данных пакета на сетевом устройстве (которую, я разобрать не могу). Можно предположить, что внутри устройства есть теже проблемы, что и у большого числа сетевых устройств(и программ). Это, проблема с проверками порядковых номеров в протоколах пакетов.

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

Когда устройство делает аппаратную обработку tcp пакетов, то предполагается что взаимодействие клиент/сервер происходит, по стандартному сценарию. Когда все действия систем перекладываются на сетевой стек. Все данные имеют, порядковые номера указываемые сетевой подсистемой (временные метки, размер пакетов и тд.). Но если, пакеты сформированы вне "системы" (вручную - что обычно не делают), то есть огромная вероятность, что что-то пойдёт не так. Ведь, когда программист тестирует сетевое приложение (или аппаратуру), то он не выходит из стандартной схемы тестирования. Берёт приложение, или пишет его сам. Создаёт сокет и шлёт данные. Как именно шлются эти данные, программисту не важно. Все действия по доставке данных, перекладываются на ОС. В этом проблема. Люди больше концентрируют внимание на том, какие именно данные шлются, а не на том, как они отправляются.

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

А когда тестируется сетевое устройство, то после ошибки обработки данных (или не ошибки) в устройстве. Оно может передать в драйвер не корректные данные. Которые по сути, никак не могли появиться. (в теории) (Но если вспомнить о проблемах проверки порядковых номеров. То вероятность обнаружить ошибку, резко возрастает)

Вот пример: в файле chtls_main.c

Есть функция chtls_recv , она работает с пакетом который уже был обработан сетевым устройством.

void chtls_recv(struct chtls_dev *cdev,
		       struct sk_buff **skbs, const __be64 *rsp)
{
	struct sk_buff *skb = *skbs;
	unsigned int opcode;
	int ret;

	opcode = *(u8 *)rsp;   // Тут находится ОШИБОЧНЫЙ ОПКОД который установило устройство

	__skb_push(skb, sizeof(struct rss_header));
	skb_copy_to_linear_data(skb, rsp, sizeof(struct rss_header));

	ret = chtls_handlers[opcode](cdev, skb); // Вызов функции по, не правильному опкоду
	if (ret & CPL_RET_BUF_DONE)
		kfree_skb(skb);
}

После обработки пакета устройством, сетевая карта передаёт драйверу опкод операции которую должна выполнить система.

opcode = *(u8 *)rsp; Содержит опкод инструкции, которую будет выполнять система.

Но если, передать устройству определённые пакеты, которые пройдут все проверки по значениям seq и дополнить их urg, то устройство выставит определённый опкод для выполнения, который вообще не должен был никогда выставляться.

В данном примере

ret = chtls_handlers[opcode](cdev, skb);

Будет выполнена функция по адресу NULL. Что приведёт к зависанию системы.

И такие ошибки остаются не замеченными ГОДАМИ.

Также скажу, что пытался рассказать о проблеме на примере TCP и флага urg. Но точно такие же проблемы, можно обнаружить и при сборке UDP пакетов (везде. не важно, драйвера или приложения прикладного уровня, или сетевые устройства). Сколько сетевых устройств подвержено данной проблеме, не знаю. Думаю, что очень не мало.

Проверял всего одно устройство (которое было на руках). Результат, удалённый вызов нулевого указателя в ядре. Отказ в обслуживании. ( + доступ к памяти системы, в определённых диапазонах) Писал производителю о проблемах, но им это не интересно, проигнорировали и выпустили новый флагман) с теми же проблемами.

На этом пока всё.

Реклама
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее

Комментарии 3

    +1
    Классная, познавательная статья, о потрошках Linux, торт!
    Вот, разве что, не самый широкий круг программистов сталкиваются с сетевым стеком на таком уровне (драйвера да embedded кмк). Сам люблю и уважаю подобные ковыряния, но кроме как на голом энтузиазме не доводилось лично, увы
      +2

      конечно может быть дело в том, что на часах 4 утра, но, по-моему, статья ужасная.
      если читаешь англоязычные статьи, то там всегда чётко соблюдается структура, в частности в начале статьи всегда есть abstract.


      Аннотация – это мини-версия статьи. Американский национальный институт стандартов говорит: «Хорошо подготовленная аннотация позволяет читателям быстро и точно определить основное содержание документа, определить ее соответствие их интересам и, таким образом, решить, нужно ли им читать статью целиком» (ANSI 1979). Поэтому крайне важно, чтобы аннотация была написана четко.
        0
        Скорее всего, это 4 утра.
        Сама эта статья, это мини выжимка того, что следовало бы написать. Именно поэтому я не вдаюсь в подробности работы даже tcp стека. И использую такие слова, как «это Не важно» и пропускаю кучу ВАЖНЫХ моментов. Пишу «и тд.»
        Пишу так:
        Например такой пакет будет корректен. seq = 0xfffffc6e размером в 1000 байт

        И не описываю подробно, почему такие значения корректны, да и примеры не привожу, которые по сути то нужны.
        Не привожу примеры значений end_seq rcv_nxt при этапе tcp-рукопожатия! А следовало бы. Там на «втором шаге» (при отправке на сервер второго пакета c флагом ack) раздолье для действий. (Да и при закрытии соединения тоже)
        Следовало бы, пройтись по каждому этапу хотябы сетевой обработки tcp. Вот тут "Как мы раскрыли 24-летний баг в ядре Linux" Рассматривается что-то похожее.
        Рассмотреть как можно действовать с временными метками. Ведь они тоже ручками подсчитываются.И в зависимости от их значений, идёт разная обработка пакетов.

        Можно было рассмотреть mptcp с его коллизиями в самом протоколе. (он использует tcp стек, но также является «обёрткой» над… в общем, не важно.)

        Хотя, может Вы и правы.
        Можно пример? В пару строк. Подходящей аннотация для этой статьи. (Яб добавил их в начале, для структуры)

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое