HOME FORUMS MEMBERS RECENT POSTS LOG IN  
× Авторизация
Имя пользователя:
Пароль:
Нет аккаунта? Регистрация
Баннер 1   Баннер 2

ANTICHAT — форум по информационной безопасности, OSINT и технологиям

ANTICHAT — русскоязычное сообщество по безопасности, OSINT и программированию. Форум ранее работал на доменах antichat.ru, antichat.com и antichat.club, и теперь снова доступен на новом адресе — forum.antichat.xyz.
Форум восстановлен и продолжает развитие: доступны архивные темы, добавляются новые обсуждения и материалы.
⚠️ Старые аккаунты восстановить невозможно — необходимо зарегистрироваться заново.
Вернуться   Форум АНТИЧАТ > БЕЗОПАСНОСТЬ И УЯЗВИМОСТИ > Безопасность и Анонимность > Защита ОС: вирусы, антивирусы, файрволы.
   
Ответ
 
Опции темы Поиск в этой теме Опции просмотра

  #1  
Старый 23.11.2025, 16:25
Marylin
Постоянный
Регистрация: 01.09.2019
Сообщений: 378
Провел на форуме:
145166

Репутация: 0
По умолчанию

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

1. Источник угрозы
2. Переполнение буферов в стеке
3. Переполнение буферов в куче
4. Уязвимость в спецификаторах printf()
5. Выводы

1. Источник угрозы

Из имеющих уязвимости переполнения буфера языков, ключевыми являются C и C++, поскольку их рантайм msvcrt.dll работает с памятью более небрежно, чем многие интерпретируемые языки. Но даже если код и написан на безопасном Python, он всё-равно использует написанные на сишке подключаемые модули, а потому по прежнему может быть уязвим. Чтобы понять природу переполнения, нужно разобраться с выделением памяти в программе – в официальных источниках оно числится как «Memory Allocation».

Как правило память под буфер можно выделить или в стеке (резервируется во время компиляции), или-же в куче «Heap» уже во время выполнения. Атаки на стек встречаются чаще, т.к. при вызове функции в нём всегда лежит «адрес возврата» обратно к вызывающему, модифицировав который можно передать управление на произвольный участок кода. Поскольку куча не хранит Return-адресов, то здесь гораздо сложнее запустить свой сплойт или шелл – эта память содержит лишь данные программы. Однако куча имеет тенденцию расширяться динамически, и если прогер использовал это свойство вызовом функции
Код:
realloc()
, то перехват управления всё-таки возможен, о чём пойдёт речь далее. В силу того, что переполнение буферов в стеке представляет наибольший интерес, рассмотрим как именно оно работает.

2. Переполнение буферов в стеке

Причина, по которой «BufferOverflow» стало серьёзной проблемой заключается в отсутствии проверки границ во многих функциях управления памятью C и C++. Обычно пользовательский буфер переполняется, когда код зависит от внешних входных данных (например ввод с клавиатуры), или когда он имеет зависимости за пределами прямой видимости стекового фрейма, который отображён на рисунке ниже (архитектура IA32). Рассмотрим запрашивающий имя следующий код на ассемблере FASM:

C-подобный:


Код:
format   pe console
include
'win32ax.inc'
;
//-------
.
code
main
:
stdcall  myProc
,
0x10
,
0
;
// Вызов процедуры!
cinvoke  getch
       cinvoke  exit
,
0
;
//------ ПРОЦЕДУРА -----------------
proc  myProc  arg1
,
arg2
locals
  var1  dd
0x12345678
;
//
,
addr buff
        ret
endp
;
//-------
section
'.idata'
import data readable
library  msvcrt
,
'msvcrt.dll'
import   msvcrt
,
printf
,
'printf'
,
gets
,
'gets'
,
\
                 getch
,
'_getch'
,
exit
,
'exit'
Для вызова процедур и функций предусмотрена инструкция ассемблера
Код:
CALL
, которая неявно (т.е. аппаратно где-то под катом) сначала помещает в стек адрес-возврата в виде следующей за собой инструкции, и лишь потом передаёт управление на вход в функцию. В свою очередь, выход из отработанной функи осуществляется инструкцией
Код:
RET
, которая снимает адрес-возврата со стека и переходит по нему, продолжая тем самым программу. Таким образом, инструкции call и ret всегда ходят парой. Так будет выглядеть стек на входе в представленную выше процедуру «myProc»:

Проблема в том, что большинство функций рантайма для хранения временных значений всегда используют локальную память функции именно в стеке, что влечёт за собой катастрофические последствия. С одной строны это удобно, т.к. на выходе из функции компилятор сам очищает лок.память предотвращая её утечку (не нужен постоянный контроль). Но с другой стороны программист не может заранее предугадать, буфер какого именно размера потребуется в стековой памяти, и резервирует разумно-ограниченное её кол-во как на рис.выше (пусть будет) 32 байта. Например если код запрашивает имя юзера, оно может иметь длину 4-байта «Вася», а может принадлежать Лоуренсу Уоткинсу с длинною в 2.253 слов. Здесь уже прогерам приходится садиться на шпагат, и выбирать золотую середину.

Как результат, ввод 44-байтной строки в выделенный зелёным лок.буфер приведёт к тому, что мы пробьём дно буфера 0-31, и затерём сначала обе переменные, а затем и адрес-возврата из функции, подсунув вместо него указатель на свой шелл. Единственная проблема здесь в том, как из копируемой в буфер строки сформировать фиктивный адрес-возврата, т.к. клавиатурный ввод поддерживает только ASCII-коды печатных символов, и создать из них HEX-адрес не так просто (ситуацию усугубляет, если в адресе присутствует двоичный нуль). Однако при наличии смекалки и эту проблему можно решить (см.ниже).

Чтобы предотвратить модификацию адреса-возврата при переполнении буфера, инженеры Microsoft предлагают нам 2 основных решения – это замена уязвимой функции
Код:
gets()
на безопасную и способную ограничивать длину ввода
Код:
fgets()
, а так-же вставку перед адресом-возврата контрольного слова «Canary word», что на жаргоне звучит как «Канарейка». Это может быть любое значение размером DWORD, главное чтобы оно было уникальным при каждом вызове функции (например текущее время, или такты процессора rdtsc), иначе взломщик сможет легко найти и восстановить его. Теперь на выходе из функции мы сможем проверить значение канарейки и если оно изменилось, возможно это попытка переполнения буфа в стеке взломщиком. Позиция контрольного слова выделена на рис.ниже жёлтым:


3. Переполнение буферов в куче

Куча «Heap» – это область динамической памяти программы для выделения блоков произвольного размера. Для временных буферов рекомендуется выделять память именно в куче, а не в стеке, т.к. в ней будет отсутствовать уязвимый адрес-возврата. Тогда какой интерес представляет куча для взломщиков? Чтобы ответить на этот вопрос, нужно рассмотреть способ динамического распределения памяти.

В основе управления хипом лежит функция
Код:
malloc()
, и сопутствующие ей:
Код:
calloc()
,
Код:
realloc()
, и
Код:
free()
.

Код:
void * malloc ( size ) ;
Выделяет Size-байт из кучи, и возвращает указатель на выделенный блок, или нуль в случае ошибки.

Код:
void * calloc ( size, num ) ;
Аналогично malloc(), только забивает выделенный блок памяти нулями, чего не делает предыдущая.
Size = размер блока, Num = их кол-во. Эти аргументы можно переставить местами.

Код:
void * realloc ( ptr, size ) ;
Позволяет динамически изменить размер выделенного блока.
Ptr = указатель на выделенную ранее память, Size = новый размер блока.

Код:
void free ( ptr ) ;
Освобождает выделенную malloc(), calloc(), или realloc() память.
Ptr = указатель на блок, который необходимо освободить.

Суть в том, что при выделении памяти с помощью
Код:
malloc()
, диспетчер вообще не обращается к ядру ОС – вместо этого используются «Метаданные» для отслеживания свободных и занятых участков, которые располагаются там-же, сразу за выделенным блоком памяти. Эти метаданные использует затем функция
Код:
realloc()
, когда потребуется динамически расширить или освободить приёмный буфер.

Сами метаданные хранят всего 2 указателя для организации связанного списка – это линк на метаданные предыдущего свободного блока памяти, и линк на метаданные следующего блока. Чтобы разобраться с форматом служебных данных, можно последовательно запросить 2 блока памяти подряд, и посмотреть на результат в отладчике:

C-подобный:


Код:
main
:
cinvoke  calloc
,
32
,
1
;
// Выделить 2 блока,
push     eax
;
// ...по 32 байта.
cinvoke  calloc
,
32
,
1
cinvoke  free
,
eax
;
// Освободить их!
pop      eax
       cinvoke  free
,
eax

@exit
:
cinvoke  getch
       cinvoke  exit
,
0
При первом вызове функция
Код:
calloc()
вернула мне адрес выделенного блока
Код:
0x005D0E78
, где я обнаружил забитый нулями буфер (выделен жёлтым), дальше 2 дворда назначение которых я так и не понял(возможно это выравнивание Padding), и в хвосте ещё 2 дворда метаданных (выделены зелёным). Судя по хранящихся в этих ячейках данным, предыдущий блок имеет адрес
Код:
0x005D00C4
, а следующий свободный должен распологаться по адресу
Код:
0x005D0EA0
:

Значит продолжаем трейс в отладчике и зовём второй раз
Код:
calloc()
.. Хм, и точно аллокатор выделил нам 32-байтный блок в памяти хипа по адресу
Код:
0x005D0EA0
, в хвосте которых так-же маячат метаданные.

Таким образом, атака на переполнение буфера в куче подразумевает мод именно метаданных. Если вместо указателей на «Backward/Forward Block» мы сможем подсунуть линк на свой шелл, то при сл.вызове
Код:
calloc()
или
Код:
realloc()
он получит управление! При поиске уязвимостей в коде, здесь можно искать не только вызовы
Код:
malloc/calloc()
с последующим вводом с клавиатуры
Код:
gets()
, но и функции копирования блоков из одного места в другое
Код:
memcpy()
.

Реализация переполнения буферов в куче требует тщательного разбора метаданных, а потому этот способ не получил широкого распространения. Единственно возможный вариант защиты от атак подобного рода является ввод с ограничением длины
Код:
fgets()
, вместо дырявой как сито
Код:
gets()
.

4. Фиктивные спецификаторы функции printf()

Команды «peek» и «poke» использовались в Basic для доступа к содержимому ячейки памяти – «peek» читает байт по указанному адресу, а «poke» записывает. Термины устарели, но по привычке используются программистами для обозначения доступа к памяти в целом. Для начала посмотрим на список функций из либы msvcrt.dll (Microsoft VisualC Runtime), которые представляют для нас особый интерес:

Известно, что функции
Код:
printf() + scanf()
первым аргументом ожидают спецификатор ввода/вывода. Например такая конструкция будет расценивать клавиатурный ввод как строку:
Код:
scanf('%s',*buff)
, а
Код:
printf('%X',var)
распечатает второй аргумент в виде HEX-числа.

Проблема в том, что эти функции страдают классическим недержанием кол-ва переданных им спецификаторов, что является огромной дырой в подсистеме безопасности. Они могут принимать произвольное кол-во спецификаторов, и этот факт позволяет нам навязывать фиктивные, по своему усмотрению. Рассмотрим такой пример, где функция
Код:
gets()
читает строку в приёмный буфер(in), далее
Код:
sprintf()
копирует эту строку в буфер вывода(out), и наконец
Код:
printf()
распечатывает содержимое выходного буфа:

C-подобный:


Код:
format   pe console
include
'win32ax.inc'
entry    main
;
//-------
.
data
in_buff    rb
32
;
// Буф для ввода,
out_buff   rb
32
;
// ..и для вывода.
;
//-------
.
code
main
:
stdcall  myProc
,
0x10
cinvoke  getch
       cinvoke  exit
,
0
;
//----- ПРОЦЕДУРА -----------
proc  myProc param1
locals
  var1  dd
0x12345678
;
// Лок.переменные
var2  dd
0x9ABCDEF0
endl
       cinvoke  printf
,

cinvoke  gets
,
in_buff
       cinvoke  sprintf
,
out_buff
,

,
in_buff
       cinvoke  printf
,
out_buff
        ret
endp
;
//-------
section
'.idata'
import data readable
library  msvcrt
,
'msvcrt.dll'
import   msvcrt
,
printf
,
'printf'
,
sprintf
,
'sprintf'
,
\
                 getch
,
'_getch'
,
exit
,
'exit'
,
gets
,
'gets'
..компилим, запускаем, всё исправно работает.



Теперь посмотрим на реакцию функции
Код:
sprintf()
, если на запрос «Name» навязать ей несколько HEX-спецификаторов (%08Х указывает расширить число до 8 знаков):



Как видим, глупая функция приняла наш ввод за свои спецификаторы, в результате чего прихватила с собой 5 следующих двордов из стека – первые(2) это лок.переменные см.исходник, третий это значение сохранённого на входе в процедуру регистра
Код:
EBP
, четвёртый дворд есть ничто иное как адрес-возврата из процедуры, и наконец последний это аргумент вызова
Код:
stdcall (0x10)
.

Таким образом, взяв в заложники спецификаторы функции
Код:
printf()
, мы можем распечатать дамп памяти как стека так и кучи, чтобы проанализировав найти, например, смещение адреса-возврата в стеке, или произвольного указателя в куче. Другими словами получили аналог 'peek' бейсика. Мотаем это на ус...

Ладно, будем считать, что с чтением памяти определились. Но на практике интерес представляет запись, чтобы модифицировать, например, всё тот-же адрес-возврата. Примечательно, что
Код:
printf()
способна не только печатать вывод в stdout, но и осуществлять (!)запись по указанному в аргументе адресу, для чего предусмотрен спецификатор
Код:
%n
. Поскольку мод буфера осуществляется уже после вывода его на экран, доказательства перезаписи спецификатором(%n) приходится добывать в отладчике. Так мы получим аналог 'poke', детали смотри здесь.

Ну и конечно классический вариант перезаписи ячеек функцией
Код:
gets(buff)
или
Код:
scanf('%s',*buff)
.
Если функция внутри процедуры запрашивает ввод текстовой строки, то не прибегая к лишним усилиям мы можем подсунуть ей HEX-значения в формате ASCII-кодов. Когда с комбинацией
Код:
Alt
мы вводим 10-тичные значения на цифровой клавиатуре, нам становится доступен весь диапазон от
Код:
00h
до
Код:
FFh
. Саму таблицу можно взять например здесь.

Рассмотрим это дело на таком примере, где запрашивается строка со-спецификатором(%s), но для демонстрации мы распечатаем её в HEX.

C-подобный:


Код:
cinvoke  printf
,

cinvoke  gets
,
in_buff
       cinvoke  sprintf
,
out_buff
,

,
in_buff
       cinvoke  printf
,
out_buff
Допустим нам нужен адрес
Код:
0x0014E2F4
, тогда учитывая обратный порядок байт «Little Endian» в процессорах х86 получаем
Код:
0xF4E21400
. Значит вводим последовательность
Код:
244 226 20
с зажатой
Код:
Alt
, и вот он нужный указатель:



5. Выводы

В своей массе, переполнению буферов подвержены только консольные приложения, которые импортируют функции из либы msvcrt.dll (хидер stdio.h). Однако и в оконных приложениях довольно часто встречается WinAPI
Код:
wsprintf()
из библиотеки user32.dll, которая осуществляет форматированный по спецификаторам вывод в буфер. Если таковая обнаружится в софте, это может послужить потенциальным признаком данного рода уязвимости.

Ну и под занавес вот несколько рекомендаций для защиты от переполнений буферов в стеке и куче:

1. Используйте только безопасные функции ввода с ограничением длины типа
Код:
fgets()
, а ещё лучше WinAPI
Код:
Read/WriteConsole()
из либы Kernel32.dll.

2. Используйте «Canaries Word», которые вставляют контрольное слово перед адресом-возврата в стеке, и проверяются перед обращением к нему.

3. Не используйте локальные буферы в стеке, а выделяйте для них память из кучи.

4. Собирайте свои исходники с флагом ASLR (рандомизация адресного пространства), чтобы при каждой загрузке системы менялись адреса базы загрузки образа в память, и соответственно адрес его стека.

5. Сделайте стек неисполняемым установив бит NX (No-eXecute), чтобы плохой парень не вставил шелл-код непосредственно в стек. Для этого нужно включить DEP (Data Execution Protect) в свойствах системы. По умолчанию DEP включён только для системных приложений, так-что нужно взвести галку «..для всех».

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





Здесь присутствуют: 1 (пользователей: 0 , гостей: 1)
 


Быстрый переход




ANTICHAT ™ © 2001- Antichat Kft.