Часть 20
Last updated
Last updated
В этой главе, мы поговорим об уязвимоcтях и как анализировать самые простые из них.
В компьютерной безопасности, слово уязвимость относится к слабости в системе, позволяющей атакующему нарушать конфиденциальность, целостность, доступность, контроль доступа, согласование системы или её данных и приложений.
Уязвимости - это результат ошибок или просчётов в дизайне системы. Хотя, в более широком смысле, они также могут быть результатом самих технологических ограничений, потому что, в принципе, не существует 100% безопасной системы. Таким образом, существуют теоретические и настоящие уязвимости.
То же самое правило применяется и к программам. Уязвимая программа - это такая программа, которая имеет ошибки или изъяны в программном коде и в зависимости от типа ошибок, эти ошибки могут быть эксплуатированы. Из-за этих ошибок может быть запущен вредоносный код в вышеупомянутой программе, но также и аутентификация может завершиться ошибкой и позволит совершать действия, которые не позволены пользователю, провоцировать сбои, поднимать привилегии, и т.д.
Конечно, на уровне ошибок повреждения памяти самые простые - это переполняющиеся буфера.
Они случается, когда программа резервирует область памяти или буфер, чтобы хранить данные и по какой-либо причине не проверяет соответствующим образом размер данных, которые нужно скопировать и переполняет буфер, копируя больше зарезервированного размера, перезаписывая переменные, аргументы и указатели, которые находятся в памяти.
Самый простой тип переполняющегося буфера - это переполнение стека, которое случается, когда происходит переполнение зарезервированного буфера в стеке.
В исходном коде простой программы на C, буфер может быть представлен так:
char buf[xxx];
Где XXX - это размер буфера. В нашем случае, это буфер в стеке длиной 0x300 байт.
Очевидно, эта программа ничего не делает, но мы всё равно компилируем её и загружаем в IDA.
Она приложена к этому туториалу, как файл COMPILADO_1.EXE.
Уже знаем, что нам нужно искать ссылки на аргументы ARGC или ARGV. Так мы можем попасть в функцию MAIN в консольной программе.
Делая двойной щелчок на них, я прибываю сюда:
Теперь поищем перекрёстные ссылки с помощью клавиши X.
Ища перекрёстные ссылки, попадаем в известный блок, который вызывает функцию MAIN. В нашем случае, давайте дадим ему другое имя. Может быть такое, потому что блок ничего не делает.
Если войдём в эту функцию, увидим, что она ничего не резервирует, потому что функция не использует буфер, и при выходе она возвращает нуль в регистре EAX.
Мы должны использовать буфер, для того, чтобы программа закончила резервировать пространство.
Сейчас, мы используем функцию GETS_S, для того, чтобы пользователь ввёл что-нибудь в консоли и этот ввод сохранился в буфер, чтобы это значение можно было бы использовать позже в программе.
Видим, что вышеупомянутая функция имеет два аргумента - буфер и максимальный размер того, что Вы можете ввести, для того, чтобы буфер не переполнился. Очевидно, в примере нет переполнения, потому что размер, который копируется, не превышает размера созданного буфера в 0x300 байт.
Видно, что не существует возможности переполнить буфер, так как то, что я буду вводить будет меньше или равно 0x300 байт.
Давайте посмотрим на эту программу в IDA. Файл приложен к данному туториалу с именем COMPILADO_2.EXE.
Я компилировал файл с символами, и IDA также нашла символы на моей машине.
Так выглядит намного лучше. Уже появилась функция MAIN c её аргументами и переменными.
Давайте немного проанализируем этот код в IDA.
Я делаю двойной щелчок на любой переменной или аргументе и попадаю в статическое представление стека.
Здесь у нас есть три аргумента - ENVP, ARGV и ARGC, которые программа не использует внутри функции MAIN.
Они помещаются в стек перед вызовом MAIN.
Программа помещает три аргумента в стек перед тем как сделать ВЫЗОВ функции MAIN.
Программа сохраняет АДРЕС ВОЗВРАТА, который является адресом, который знает куда нужно вернуться программе после выхода из CALL. В нашем случае, адрес возврата имеет значение 0x401200, если не будет рандомизации.
Программа будет возвращаться сюда, после выполнения функции MAIN. Поэтому она должна сохранить значение 0x401200 в стеке чуть выше 3-х аргументов.
Затем программа начинает выполнять функцию MAIN. Первое, что делает программа — это исполняет инструкцию PUSH EBP.
Эта инструкция сохраняет в стеке значение EBP, которое использовалось функцией, которая вызвала функцию MAIN, чуть выше АДРЕСА ВОЗВРАТА 0x401200. Мы не знаем, какое значение оно может иметь, потому что оно меняется при каждом запуске программы, но сохраненный EBP или STORED EBP - это отец этой функции.
Здесь, EBP будет сохраняться в стек, выше адреса возврата.
Следующая инструкция, которая исполняется - такая.
Она устанавливает EBP как базу в этой функции и выравнивает его с ESP. Это одинокая инструкция MOV. Она меняет значение EBP, а не стек.
Затем, следующая инструкция SUB ESP, 0x304 передвигает указатель ESP вверх памяти, резервируя место для локальных переменных и буферов в стеке, которые расположены выше STORED EBP и ESP будет работать в функции основанной на EBP. Чуть выше резервируется место.
Здесь, мы видим зарезервированное пространство для переменных и буферов. Чуть ниже находится S (STORED EBP).
Первая переменная, которая почти всегда находится сразу после S - это CANARY, которая нужна для защиты стека. В нашем случае переменная называется VAR_4.
Здесь видим, что программа читает значение __SECURITY_COOKIE, которое является случайным значением, которое создаётся каждый раз, когда запускается программа. Это значение XORится с помощью EBP и сохраняется в переменную VAR_4 как мы уже видели ранее. Переименовываем её в CANARY.
Выше CANARY находится буфер BUF. Давайте посмотрим его в статическом представлении стека.
Когда я вижу пустое пространство в статическом представлении стека, я предполагаю, что это может быть буфер. Поэтому мы делаем правый щелчок на этих байтах BUF и выбираем ARRAY.
Видно, что его размер равен 768 * 1 байт, т.к. это длина каждого элемента. Следовательно, размер буфера равен 768, который в HEX представлении равен 0x300.
Поэтому, мы соглашаемся с IDA и получается буфер BUF, который уже определен как буфер из 0x300 байт в HEX или 768 в десятичном виде.
Здесь, есть вызов функции GETS_S и два её аргумента: максимальный размер 0x300 и другой её аргумент - адрес буфера, который получается через инструкцию LEA.
Поэтому, мы проверяем, что размер буфера BUF равен 0x300 байт и он вмещает максимум 0x300 байт, который мы вводим через функцию GETS_S.
Очевидно, что если бы мы могли переполнить буфер, копируя больше чем 0x300 байт, мы переписали бы переменные CANARY, STORED EBP и АДРЕС ВОЗВРАТА, которые находятся чуть ниже БУФЕРА.
Но это не тот случай. Это пример хорошего буфера, который записывается правильно.
Очевидно, часто пользователь не знает размер данных, которые будут скопированы. Если это так, размер должен быть хорошо проверен, что этот SIZE не будет больше, чем размер буфера.
Здесь видим буфер из 0x10 байт или 16 байт в десятичном виде и пользователь имеет возможность ввести размер буфера через функцию GETS_S. Очевидно, не существует никакой проверки этого максимального значения, поэтому, если я компилирую и запускаю этот файл (COMPILADO_3.EXE).
Видно, что теперь в программе есть БАГ и она стала уязвима. Если откроем её в IDA, даже если мы не имеем исходного кода, то увидим.
Как и раньше это переменная CANARY. Чуть выше есть буфер BUF. Давайте посмотри его длину в статическом представление стека.
Размер буфера равен 16 байт * 1, так как это размер одного элемента. Поэтому размер буфера в HEX равен 0x10.
Другими словами, если бы мы смогли скопировать более 16 байтов в буфер, произошло бы переполнение и перезапись переменных CANARY, STORED EBP и АДРЕС ВОЗВРАТА.
Посмотрим размер переменной.
Видно, что после вывода с помощью функции PRINTF сообщения PLEASE ENTER YOUR NUMBER, вызывается функция SCANF_S для ввода значений с клавиатуры, которые сохраняется в переменной, размер которой равен DWORD и передает размер переменной с помощью инструкции LEA в регистр EAX.
Давайте посмотрим описание функции SCANF_S.
Функция scanf_s читает данные из стандартного потока stdin, и пишет данные по адресу, который указан в аргументе. Каждый аргумент должен быть указателем на переменную типа, который соответствует спецификатору типа в формате. Если копирование происходит между строками, которые перекрываются, поведение не определено.
Другими словами, это как противоположность функции PRINTF. Только вместо печати с форматом, формат вводится с консоли с форматом в буфер. В нашем случае, формат %d интерпретирует данные как десятичное число.
Таким образом, когда Вы вызываете функцию GETS_S использую этот размер, который я напечатал, программа будет копировать заданное количество байт и если размер больше чем 0x10, буфер будет переполнен.
Возможным решением этой проблемы будет проверять размер введённых данных перед тем как копировать ввод пользователя в буфер.
Было бы неплохо проанализировать программу, чтобы убедиться, что это решение делает её неуязвимой или оставляет её такой же уязвимой. Эта программа называется VULNERABLE_O_NO.EXE, и мы будем обсуждать её в следующей главе.
До встрече в следующей 21-й главе друзья.
Автор оригинального текста — Рикардо Нарваха.
Перевод и адаптация на английский язык — IvinsonCLS.
Перевод и адаптация на русский язык — Яша Яшечкин.
Перевод специально для форума системного и низкоуровневого программирования - WASM.IN
28.10.2017