«C» — язык UNIX. В этой статье мы будем эксплуатировать уязвимость переполнения буфера в языке C. Мы все знаем язык C за его скорость, но большая мощь сопряжена с большой ответственностью. Управление памятью является ключевым моментом при написании программы, защищенной от утечек.

Уязвимость переполнения буфера существует с первых дней появления компьютеров и существует до сих пор. Различные интернет-черви используют для своего распространения уязвимости, связанные с переполнением буфера. Эта уязвимость полностью зависит от базы знаний программиста. Вы должны увидеть муху при выделении памяти в C. В этой статье я кратко расскажу о выделении памяти, которое играет ключевую роль при использовании этой уязвимости.

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

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

Чтобы увидеть, как работает эта эксплуатация, мы рассмотрим небольшую программу на C, которая поможет нам визуализировать эту уязвимость.

#include<stdio.h>
#include<string.h>
int main(int argc, char *argv[]){
int value = 5; 
char buffer_one[8], buffer_two[8];
strcpy(buffer_one, "one");
strcpy(buffer_two, "two");
printf("[BEFORE] buffer_two is at %p and contains \'%s\'\n", buffer_two,buffer_two);
printf("[BEFORE] buffer_one is at %p and contains \'%s\'\n", buffer_one,buffer_one);
printf("[BEFORE] value is at %p and is %d (0x%08x)\n",&value , value ,value);
printf("\n [STRCPY] copying %d bytes into buffer_two \n\n", strlen(argv[1]));
strcpy(buffer_two, argv[1]);
printf("[AFTER] buffer_two is at %p and contains \'%s\'\n", buffer_two,buffer_two);
printf("[AFTER] buffer_one is at %p and contains \'%s\'\n", buffer_one,buffer_one);
printf("[AFTER] value is at %p and is %d (0x%08x)\n",&value , value ,value);
}

Я предполагаю, что у вас есть общее представление об указателях и строках в C. Для сравнения, у нас есть 3 разные переменные, две с 8-байтовым массивом символов и одна с целым числом.

int value = 5; 
char buffer_one[8], buffer_two[8];

Затем мы копируем несколько строк «один» и «два» в ранее выделенный нами буфер.

strcpy(buffer_one, "one");
strcpy(buffer_two, "two");

Затем есть несколько операторов печати, чтобы распечатать текущую ячейку памяти этих трех переменных.

printf("[BEFORE] buffer_two is at %p and contains \'%s\'\n", buffer_two,buffer_two);
printf("[BEFORE] buffer_one is at %p and contains \'%s\'\n", buffer_one,buffer_one);
printf("[BEFORE] value is at %p and is %d (0x%08x)\n",&value , value ,value);

Настоящая уязвимость эксплуатируется в этой строке:

strcpy(buffer_two, argv[1]);

Здесь мы копируем все, что находится внутри argv[1], независимо от емкости переменной, а именно buffer_two. Все должно работать нормально, пока размер argv[1] меньше восьми байтов, но если встречается что-то большее, память переполняется. Давайте скомпилируем и запустим эту программу, чтобы увидеть переполнение в действии.

Компиляция

gcc -o overflow <your .c file with the code>

Выполняется (без переполнения)

./overflow 1234

Программа работает как положено. Он копирует все, что мы передали в качестве аргумента командной строки, например 1234, в buffer_two. Самое интересное происходит, когда мы передаем 9 байтов. Мы все знаем, что в C каждый символ воспринимается как байт. Итак, если мы передаем 9 символов, память должна переполниться.

Выполняется (с переполнением)

./overflow 123456789

Поздравляем! ты хакер (смеется).

Вы можете видеть, что 1, 2, 3, 4, 5, 6, 7 и 8 остаются такими, какие они есть, но 9 переполняется и достигает другой области памяти, то есть в buffer_one.

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

Этот эффект проявляется из-за структуры C FILO (First In Last Out) при выделении памяти. Чтобы углубиться в это, мы можем обратиться к сегментации памяти.

Как правило, память скомпилированных программ делится на пять сегментов.

  1. Текст
  2. Данные
  3. БСС
  4. куча
  5. Куча

Когда программа запускается, EIP (указатель инструкции, т. е. тип регистра) устанавливается на первую инструкцию в текстовом сегменте. Этот сегмент защищен, и права на запись не разрешены.

Сегменты data и BSS сегменты используются для хранения глобальных и статических переменных. Сегмент данных содержит все инициализированные глобальные и статические переменные, тогда как BSS содержит все неинициализированные.

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

Сегмент стека — это еще один сегмент памяти переменного размера, который служит временным блокнотом для хранения локальных функций. Этот стек содержит кадры стека. Сегмент стека может содержать много кадров стека. В общем случае кадры стека представляют собой абстрактную структуру данных, которая соответствует порядку FILO.

В нашем предыдущем примере мы видели переполнение на buffer_one. Так как мы определили buffer_two после buffer_one, любое переполнение перехватывается buffer_one из-за механизма упорядочивания FILO. В памяти буфер_два располагается перед буфером_один.

Какой вариант использования?

После большого обсуждения переполнения буфера вы можете задаться вопросом, как выглядит его использование. Чтобы продемонстрировать, что мы напишем небольшую программу на C для аутентификации пользователя с помощью сравнения строк.

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int check_authentication(char *password){
int auth_flag = 0;
char password_buffer[16];
strcpy(password_buffer, password);
if(strcmp(password_buffer, "sabin" ) == 0)
 auth_flag = 1;
if(strcmp(password_buffer, "sharma") == 0)
 auth_flag = 1;
return auth_flag;
}
int main(int argc , char *argv[])
{
if(argc < 2)
{
 printf("Usage: %s <password>\n", argv[0]);
 exit(0);
}
if(check_authentication(argv[1])){
 printf("\n-======================-\n");
 printf(" Access Granted.\n");
}
else{
 printf("\n Access Denied. \n");
}
}

Итак, у нас есть простая программа, которая аутентифицирует пользователя на основе переданной строки. Единственный способ получить доступ — знать пароль, т. е. «сабин» или «шарма».

int check_authentication(char *password){
int auth_flag = 0;
char password_buffer[16];
strcpy(password_buffer, password);
if(strcmp(password_buffer, "sabin" ) == 0)
 auth_flag = 1;
if(strcmp(password_buffer, "sharma") == 0)
 auth_flag = 1;
return auth_flag;
}

В этой функции в качестве параметра передается указатель символа. Сначала мы инициализируем переменную, а именно auth_flag, и устанавливаем для нее значение 0. После этого мы создаем переменную, а именно password_buffer емкостью 16 байт. После этого мы копируем значение из указателя, переданного в локальную переменную, а именно password_buffer. Наконец, мы сравниваем эту скопированную строку с некоторой строкой. Если они совпадают, мы устанавливаем auth_flag в 1 и возвращаем auth_flag.

if(check_authentication(argv[1])){
 printf("\n-======================-\n");
 printf(" Access Granted.\n");
}
else{
 printf("\n Access Denied. \n");
}

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

Эта программа работает нормально, но есть одна загвоздка. Что, если мы передадим 33 символа только для того, чтобы создать переполнение.

Компиляция

gcc -o auth_overflow <your .c file with the code>

Выполняется (без переполнения)

./auth_overflow sabin

Выполняется (без переполнения)

./auth_overflow randomstring

Выполняется (без переполнения)

./auth_overflow sharma

Так что позвольте хакеру внутри вас включиться. Что произойдет, если мы передадим 33 символа?

Выполняется (с переполнением)

./auth_overflow 1234567890ABCDEFGHIJKLMNOPQRS

Как видите, без правильного пароля нам предоставляется доступ. Вы можете задаться вопросом, как это произошло. Это та же самая FILO-логика сегментации памяти. Как мы ранее определили наш строковый буфер после целочисленной переменной. В памяти строковый буфер находится перед целочисленной переменной. Следовательно, значение внутри целочисленной переменной перезаписывается переполняющей строкой или символами. Значение внутри целочисленной переменной всегда положительно из-за преобразования строки в значения ASCII.

Примечание: [если утверждения с положительным целым числом всегда истинны.]

Таким образом, значение переполнения устанавливает, если условие истинно, и наш доступ предоставляется.

Подождите, подождите, здесь начинается самое интересное. Если вы понимаете сегментацию памяти, эту уязвимость можно устранить, просто изменив объявление переменной.

Отсюда

int auth_flag = 0;
char password_buffer[16];

К этому

char password_buffer[16];
int auth_flag = 0;

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

Если вас интересуют алгоритмы, вам следует ознакомиться с другими моими статьями: Молекулярно-динамическое моделирование твердых сфер — приоритетная очередь в действии с Java, Создайте свою собственную игру на основе ИИ, Контроллер вентилятора NVIDIA для Linux (сделай сам). ».

Кто такой Сабин Шарма?