Зоопарк переполняющихся буферов Крис Касперски aka мыщъх Спецвыпуск Xakep, номер #045, стр. 045-008-3 Код #4. Пример, демонстрирующий атаку типа "POKE" f() { char buf[MAX_BUF_SIZE]; int a; int *b; ... gets(buf); ... *b = a; } Правда, его мощь будет неполной без функции PEEK, позволяющей читать произвольные ячейки, так как зачастую целевой адрес записи не известен, и, чтобы не блуждать впотьмах, неплохо бы увидеть "живой" дамп уязвимой программы. Это можно сделать так: Код #5. Пример, демонстрирующий атаку типа "PEEK" f() { char buf[MAX_BUF_SIZE]; int *b; ... gets(buf); ... printf("%x\n", *b); } Индексы представляют собой разновидность указателей. Можно сказать, что это относительные указатели, отсчитываемые от определенной"базы", которой, как правило, является начало переполняющегося буфера. Рассмотрим следующий пример и сравним его с кодом #4, чтобы выяснить, есть ли между ними разница. При вычислении эффективного адреса Си просто складывает указатель с индексом: addr = (p+b). Варьируя b, мы можем получить любой addr, и p нам не помешает. Правда, тут есть одно но. Сказанное справедливо лишь по отношению к индексам типа двойного слова, а дальнобойность байтовых индексов очень даже ограничена! Код #6. Пример, демонстрирующий атаку на индексы f() { int *p; char buf[MAX_BUF_SIZE]; int a; int b; ... gets(buf); ... p[b] = a; } От индексов рукой подать до целочисленного переполнения, суть которого можно понять из следующего примера: Код #7. Пример, демонстрирующий целочисленное переполнение DWORD sum(DWORD a, DWORD b) { return a + b; } Если сумма a и b равна или превышает 1.00.00.00.00h, то произойдет переполнение разрядной сетки и результат вычислений окажется усечен. Со знаковыми переменными еще интереснее: сумма двух положительных чисел зачастую оказывается меньше нуля (достаточно лишь затереть старший бит – на архитектуре x86 он и есть знаковый). Вычисления с преобразованием типа – вообще полный швах: a = (DWORD) (byte b – byte c). Если b > c, то небольшое по модулю отрицательное число превратится в очень большое положительное, и если оно используется в индексном выражении, а проверки выхода за границы массива отсутствуют – произойдет его катастрофическое переполнение (на этом, кстати говоря, и была основа легендарная атака teardrop). Остальные типы переполнений чрезвычайно мало распространены, и поэтому мы не будем их рассматривать. Три континента: стек, данные и куча Переполняющиеся буфера могут располагаться в одном из трех мест адресного пространства процесса: стеке (автоматической памяти), сегменте данных (хотя в 9x/NT это никакой не сегмент) и куче (динамической памяти). Наиболее распространено стековое переполнение, хотя его значимость сильно преувеличена. Дно стека варьируется от одной операционной системы к другой, а высота вершины зависит от характера предыдущих запросов к программе, поэтому абсолютный адрес автоматических переменных атакующему практически никогда не известен. С другой стороны, автоматические буфера привлекательны тем, что в непосредственной близости с их концом лежит адрес возврата из функции (абсолютный, конечно), и если его затереть, то управление получит совсем другая ветка программы! Проще всего подсунуть адрес уже существующей функции, сложнее – передать управление непосредственно на сам переполняющийся буфер. Это можно сделать несколькими путями. Первый – найти в памяти инструкцию JMP ESP и передать ей управление, а она передаст его на вершину карда стека, чуть ниже которого расположен shell-код. Шансы дойти до shell-кода, преодолев весь мусор на дороге, достаточно невелики, но они все-таки есть. Второй путь: если размеры переполняющегося буфера превышают непостоянство его размещения в памяти, перед shell-кодом можно расположить длинную цепочку команд-пустышек (NOP'ов) и передать управление на середину (авось не промажет!). Этот способ использовал червь Love San, известный тем, что чаще всего он мазал и ронял машину, не производя заражения. И, наконец, третий вариант: если атакующий может воздействовать на статические буфера, расположенные в сегменте данных (а их адрес постоянен), то передать сюда управление не составит труда. Ведь shell-код и не подписывался располагаться именно в переполняющемся буфере – он может быть где угодно. Правда, не факт, что при переполнении буфера функция доживет до возращения, ведь все располагающиеся за его концом переменные окажутся искажены! Кстати говоря, помимо адреса возврата там гнездятся полчища прочих служебных структур, но рассказать о них в рамках журнальной статьи нет никакой возможности. |