> For the complete documentation index, see [llms.txt](https://yutewiyof.gitbook.io/intro-rev-ida-pro/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://yutewiyof.gitbook.io/intro-rev-ida-pro/chast-20.md).

# Часть 20

[\[Используемые материалы\]](https://github.com/yutewiyof/intro-rev-ida-pro/tree/e1367e11cc661f3d69c02ae5f733b7dd168bc5ab/.gitbook/assets/files/20.zip)

## Уязвимости.

В этой главе, мы поговорим об уязвимоcтях и как анализировать самые простые из них.

## Что же такое уязвимость ?

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

Уязвимости - это результат ошибок или просчётов в дизайне системы. Хотя, в более широком смысле, они также могут быть результатом самих технологических ограничений, потому что, в принципе, не существует 100% безопасной системы. Таким образом, существуют теоретические и настоящие уязвимости.

То же самое правило применяется и к программам. Уязвимая программа - это такая программа, которая имеет ошибки или изъяны в программном коде и в зависимости от типа ошибок, эти ошибки могут быть эксплуатированы. Из-за этих ошибок может быть запущен вредоносный код в вышеупомянутой программе, но также и аутентификация может завершиться ошибкой и позволит совершать действия, которые не позволены пользователю, провоцировать сбои, поднимать привилегии, и т.д.

Конечно, на уровне ошибок повреждения памяти самые простые - это переполняющиеся буфера.

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

Самый простой тип переполняющегося буфера - это переполнение стека, которое случается, когда происходит переполнение зарезервированного буфера в стеке.

В исходном коде простой программы на **C**, буфер может быть представлен так:

**char buf\[xxx];**

Где **XXX** - это размер буфера. В нашем случае, это буфер в стеке длиной **0x300** байт.

![](/files/-LjqtHgLMh9TXMNF2aqv)

Очевидно, эта программа ничего не делает, но мы всё равно компилируем её и загружаем в **IDA**.

Она приложена к этому туториалу, как файл **COMPILADO\_1.EXE**.

![](/files/-LjqtHgN7t9MdcuAUfH3)

Уже знаем, что нам нужно искать ссылки на аргументы **ARGC** или **ARGV**. Так мы можем попасть в функцию **MAIN** в консольной программе.

Делая двойной щелчок на них, я прибываю сюда:

![](/files/-Ljttz2C8Yy2mXnrhnC9)

Теперь поищем перекрёстные ссылки с помощью клавиши **X**.

![](/files/-LjqtHgRFLP13NuijepP)

Ища перекрёстные ссылки, попадаем в известный блок, который вызывает функцию **MAIN**. В нашем случае, давайте дадим ему другое имя. Может быть такое, потому что блок ничего не делает.

![](/files/-Ljttz2YoLcUCOvp1nEN)

Если войдём в эту функцию, увидим, что она ничего не резервирует, потому что функция не использует буфер, и при выходе она возвращает нуль в регистре **EAX**.

![](/files/-LjqtHgVhNMWM15AcT2j)

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

![](/files/-LjqtHgXANakKThlFLmX)

Сейчас, мы используем функцию **GETS\_S**, для того, чтобы пользователь ввёл что-нибудь в консоли и этот ввод сохранился в буфер, чтобы это значение можно было бы использовать позже в программе.

![](/files/-LjqtHgZVSEPI8xAzJEK)

Видим, что вышеупомянутая функция имеет два аргумента - буфер и максимальный размер того, что Вы можете ввести, для того, чтобы буфер не переполнился. Очевидно, в примере нет переполнения, потому что размер, который копируется, не превышает размера созданного буфера в **0x300** байт.

![](/files/-LjqtHgaURhe6qqRtVj3)

Видно, что не существует возможности переполнить буфер, так как то, что я буду вводить будет меньше или равно **0x300** байт.

Давайте посмотрим на эту программу в **IDA**. Файл приложен к данному туториалу с именем **COMPILADO**\_**2.EXE**.

![](/files/-LjqtHgcIS7zdnnc-8cy)

Я компилировал файл с символами, и **IDA** также нашла символы на моей машине.

Так выглядит намного лучше. Уже появилась функция **MAIN** c её аргументами и переменными.

Давайте немного проанализируем этот код в **IDA**.

![](/files/-Ljttz3Q08loKxdAWVS8)

Я делаю двойной щелчок на любой переменной или аргументе и попадаю в статическое представление стека.

Здесь у нас есть три аргумента - **ENVP**, **ARGV** и **ARGC**, которые программа не использует внутри функции **MAIN**.

Они помещаются в стек перед вызовом **MAIN**.

![](/files/-LjqtHggJQ1qQGRVjjr1)

Программа помещает три аргумента в стек перед тем как сделать **ВЫЗОВ** функции **MAIN**.

Программа сохраняет **АДРЕС ВОЗВРАТА**, который является адресом, который знает куда нужно вернуться программе после выхода из **CALL**. В нашем случае, адрес возврата имеет значение **0x401200**, если не будет рандомизации.

![](/files/-LjqtHgid7dEjfl5RYgn)

Программа будет возвращаться сюда, после выполнения функции **MAIN**. Поэтому она должна сохранить значение **0x401200** в стеке чуть выше **3**-х аргументов.

![](/files/-LjqtHgkkcsbmzjqx781)

Затем программа начинает выполнять функцию **MAIN**. Первое, что делает программа — это исполняет инструкцию **PUSH EBP**.

![](/files/-Ljttz42fesb5gDl9GyV)

Эта инструкция сохраняет в стеке значение **EBP**, которое использовалось функцией, которая вызвала функцию **MAIN**, чуть выше **АДРЕСА ВОЗВРАТА 0x401200**. Мы не знаем, какое значение оно может иметь, потому что оно меняется при каждом запуске программы, но сохраненный **EBP** или **STORED EBP** - это отец этой функции.

![](/files/-Ljttz4BfwtZzyGYoX-3)

Здесь, **EBP** будет сохраняться в стек, выше адреса возврата.

Следующая инструкция, которая исполняется - такая.

![](/files/-LjqtHgqBgn3vkczjK9h)

Она устанавливает **EBP** как базу в этой функции и выравнивает его с **ESP**. Это одинокая инструкция **MOV**. Она меняет значение **EBP**, а не стек.

Затем, следующая инструкция **SUB ESP**, **0x304** передвигает указатель **ESP** вверх памяти, резервируя место для локальных переменных и буферов в стеке, которые расположены выше **STORED EBP** и **ESP** будет работать в функции основанной на **EBP**. Чуть выше резервируется место.

![](/files/-Ljttz4WCjN_vzFlHaw2)

Здесь, мы видим зарезервированное пространство для переменных и буферов. Чуть ниже находится **S** (**STORED EBP**).

Первая переменная, которая почти всегда находится сразу после **S** - это **CANARY**, которая нужна для защиты стека. В нашем случае переменная называется **VAR\_4**.

![](/files/-Ljttz4fMPgdJGvJ7y8h)

Здесь видим, что программа читает значение **\_\_SECURITY\_COOKIE**, которое является случайным значением, которое создаётся каждый раз, когда запускается программа. Это значение **XOR**ится с помощью **EBP** и сохраняется в переменную **VAR\_4** как мы уже видели ранее. Переименовываем её в **CANARY**.

![](/files/-Ljttz4qFHWkxURitlIC)

Выше **CANARY** находится буфер **BUF**. Давайте посмотрим его в статическом представлении стека.

![](/files/-Ljttz52mVFFoxE_sEmn)

Когда я вижу пустое пространство в статическом представлении стека, я предполагаю, что это может быть буфер. Поэтому мы делаем правый щелчок на этих байтах **BUF** и выбираем **ARRAY**.

![](/files/-LjqtHh-o1eIjewgOUvB)

Видно, что его размер равен **768** \* **1** байт, т.к. это длина каждого элемента. Следовательно, размер буфера равен **768**, который в **HEX** представлении равен **0x300**.

![](/files/-Ljttz5JU2knguKh1ELJ)

Поэтому, мы соглашаемся с **IDA** и получается буфер **BUF**, который уже определен как буфер из **0x300** байт в **HEX** или **768** в десятичном виде.

Здесь, есть вызов функции **GETS\_S** и два её аргумента: максимальный размер **0x300** и другой её аргумент - адрес буфера, который получается через инструкцию **LEA**.

![](/files/-LjqtHh3E_Htx9LII18o)

Поэтому, мы проверяем, что размер буфера **BUF** равен **0x300** байт и он вмещает максимум **0x300** байт, который мы вводим через функцию **GETS\_S**.

Очевидно, что если бы мы могли переполнить буфер, копируя больше чем **0x300** байт, мы переписали бы переменные **CANARY, STORED EBP** и **АДРЕС ВОЗВРАТА**, которые находятся чуть ниже **БУФЕРА**.

![](/files/-LjqtHh5O28hpst7tzMm)

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

![](/files/-LjqtHh7b6T0csNGipoR)

Очевидно, часто пользователь не знает размер данных, которые будут скопированы. Если это так, размер должен быть хорошо проверен, что этот **SIZE** не будет больше, чем размер буфера.

![](/files/-Ljttz5teTN0pKvGNu8q)

Здесь видим буфер из **0x10** байт или **16** байт в десятичном виде и пользователь имеет возможность ввести размер буфера через функцию **GETS\_S**. Очевидно, не существует никакой проверки этого максимального значения, поэтому, если я компилирую и запускаю этот файл (**COMPILADO\_3.EXE**).

![](/files/-Ljttz64h1Urxzej8FVv)

Видно, что теперь в программе есть **БАГ** и она стала уязвима. Если откроем её в **IDA**, даже если мы не имеем исходного кода, то увидим.

![](/files/-Ljttz6EgvX9Gp3EQXno)

Как и раньше это переменная **CANARY**. Чуть выше есть буфер **BUF**. Давайте посмотри его длину в статическом представление стека.

![](/files/-LjqtHhFAKCoX1RHRx7v)

Размер буфера равен **16** байт \* **1**, так как это размер одного элемента. Поэтому размер буфера в **HEX** равен **0x10**.

![](/files/-LjqtHhHqBrIFN1lj2EB)

Другими словами, если бы мы смогли скопировать более **16** байтов в буфер, произошло бы переполнение и перезапись переменных **CANARY**, **STORED EBP** и **АДРЕС ВОЗВРАТА**.

Посмотрим размер переменной.

![](/files/-LjqtHhJKH8SQdJfj3GD)

Видно, что после вывода с помощью функции **PRINTF** сообщения **PLEASE ENTER YOUR NUMBER**, вызывается функция **SCANF\_S** для ввода значений с клавиатуры, которые сохраняется в переменной, размер которой равен **DWORD** и передает размер переменной с помощью инструкции **LEA** в регистр **EAX**.

Давайте посмотрим описание функции **SCANF\_S**.

![](/files/-LjqtHhLta6rtvrwvgp2)

Функция **scanf\_s** читает данные из стандартного потока **stdin**, и пишет данные по адресу, который указан в **аргументе**. Каждый **аргумент** должен быть указателем на переменную типа, который соответствует спецификатору типа в **формате**. Если копирование происходит между строками, которые перекрываются, поведение не определено.

Другими словами, это как противоположность функции **PRINTF**. Только вместо печати с форматом, формат вводится с консоли с форматом в буфер. В нашем случае, формат **%d** интерпретирует данные как десятичное число.

Таким образом, когда Вы вызываете функцию **GETS**\_**S** использую этот размер, который я напечатал, программа будет копировать заданное количество байт и если размер больше чем **0x10**, буфер будет переполнен.

![](/files/-Ljttz6xLQZEgH2Yzb_Q)

Возможным решением этой проблемы будет проверять размер введённых данных перед тем как копировать ввод пользователя в буфер.

![](/files/-LjqtHhPGPDV43M2NFQe)

Было бы неплохо проанализировать программу, чтобы убедиться, что это решение делает её неуязвимой или оставляет её такой же уязвимой. Эта программа называется **VULNERABLE\_O\_NO.EXE**, и мы будем обсуждать её в следующей главе.

До встрече в следующей **21**-й главе друзья.

Автор оригинального текста — Рикардо Нарваха.

Перевод и адаптация на английский язык — IvinsonCLS.

Перевод и адаптация на русский язык — Яша Яшечкин.

Перевод специально для форума системного и низкоуровневого программирования - WASM.IN

28.10.2017

[**Источник: ricardonarvaja.info**](http://ricardonarvaja.info/WEB/IDA%20DESDE%20CERO/CURSO%20DE%20IDA%20TUTES/20-INTRODUCCION%20AL%20REVERSING%20CON%20IDA%20PRO%20DESDE%20CERO.docx)
