Статья Hello world в виде шелл-кода: Особенности написания шелл-кодов

Приветствую всех читателей этой статьи и посетителей <Codeby.net> 🖐

Хочу рассказать о шелл-кодах и особенностях их написания вручную. Вам понадобятся знания ассемблера на базовом уровне. Рассмотрим как пишут шелл-коды без инструментов, которые могут их автоматически создать. Вредоносные шелл-коды писать не будем! Будем писать псевдо шелл-коды для простоты и понимания. Если эта статья и её формат вам понравиться, тогда расскажу о вредоносных шелл-кодах :)
Написание шелл-кода будет показано для архитектуры x86. Алгоритм не сильно отличается для архитектуры x64. Для практики я рекомендую вам установить Linux в VirtualBox или VMware. Так же можно экспортировать готовый образ виртуальной машины.

План:
Теория: Что такое шелл-код и системные вызовы
Практика: Сравниваем программу на ассемблере и языке Си. Делаем hello world в виде шелл-кода

1611680387761.png


Что такое шелл-код и системные вызовы

Шелл-код — это двоичный исполняемый код, который выполняет определенную задачу. Например: Передать управление (/bin/sh ) или даже выключить компьютер. Шелл-код пишут на языке ассемблер с помощью опкодов (Например: \x90 означает команду:nop ).

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

У системных вызовов есть кода. Например, функция printf() использует системный вызов write() с кодом 4.
Машины с архитектурой x86: Системные вызовы определены в файле /usr/include/i386-linux-gnu/asm/unistd_32.h
Машины с архитектурой x64: Системные вызовы определены в файле /usr/include/x86_64-linux-gnu/asm/unistd_64.h
с объяснениями.

Проверим существование системных вызовов на практике

Напишем программу на языке Си, печатающую строку BUG.

Код:
C:
#include <stdio.h>

void main(void) { printf("BUG"); }

Компиляция: gcc printf_prog.c -o printf_prog

Проверим наличие системных вызовов с помощью команды: strace ./printf_prog

Вывод strace
C:
execve("./printf_prog", ["./printf_prog"], 0xbffff330 /* 48 vars */) = 0
brk(NULL)                               = 0x405000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7fcf000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=92992, ...}) = 0
mmap2(NULL, 92992, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7fb8000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/i386-linux-gnu/libc.so.6", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
read(3, "\177ELF\1\1\1\3\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\300\254\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1947056, ...}) = 0
mmap2(NULL, 1955712, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb7dda000
mprotect(0xb7df3000, 1830912, PROT_NONE) = 0
mmap2(0xb7df3000, 1368064, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19000) = 0xb7df3000
mmap2(0xb7f41000, 458752, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x167000) = 0xb7f41000
mmap2(0xb7fb2000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1d7000) = 0xb7fb2000
mmap2(0xb7fb5000, 10112, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb7fb5000
close(3)                                = 0
set_thread_area({entry_number=-1, base_addr=0xb7fd00c0, limit=0x0fffff, seg_32bit=1, contents=0, read_exec_only=0, limit_in_pages=1, seg_not_present=0, useable=1}) = 0 (entry_number=6)
mprotect(0xb7fb2000, 8192, PROT_READ)   = 0
mprotect(0x403000, 4096, PROT_READ)     = 0
mprotect(0xb7ffe000, 4096, PROT_READ)   = 0
munmap(0xb7fb8000, 92992)               = 0
fstat64(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(0x88, 0), ...}) = 0
brk(NULL)                               = 0x405000
brk(0x426000)                           = 0x426000
brk(0x427000)                           = 0x427000
write(1, "BUG", 3BUG)                      = 3
exit_group(3)                           = ?
+++ exited with 3 +++

В конце strace мы можем видеть системный вызов write(1, "BUG", 3BUG). Количество кода для шелл-кода слишком много, если использовать функции. Старайтесь писать небольшие шелл-коды. Так они будут меньше обнаруживаться и вероятность их срабатывания будет больше.

Сравниваем программу на ассемблере и языке Си

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

1611680968874.png


Код на языке Си:
C:
#include <stdio.h>

void main(void) { printf("Hello, world!"); }

Компиляция: gcc hello_world_c.c -o hello_world_c

Код на ассемблере:
C-подобный:
global _start
section .text

_start:
        mov eax, 4 ; номер системного вызова (sys_write)
        mov ebx, 1 ; файловый дескриптор (stdout)
        mov ecx, hello_world ; сообщение hello_world
        mov edx, len_hello ; длина строки hello_world
        int 0x80 ; вызов системного прерывания

        mov eax, 1 ; номер системного вызова (sys_exit)
        xor ebx, ebx ; Обнуляем регистр ebx, чтобы первый аргумент системного вызова sys_exit был равен 0
        int 0x80 ; вызов системного прерывания

hello_world: db "Hello, world!", 10 ; 10 - количество выделенных байт для строки
len_hello: equ $ - hello_world ; вычиляем длину строки. $ указывает на строку hello_world

Получаем объектный файл с помощью nasm: nasm -f elf32 hello_world.asm -o hello_world.o
Объединяем объектный файл в один исполняемый: ld -m elf_i386 hello_world.o -o hello_world

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

Посмотрим на ассемблерный код получившихся файлов с помощью objdump.

Функция main в программе на языке Си:
C-подобный:
    1199:       8d 4c 24 04             lea    ecx,[esp+0x4]
    119d:       83 e4 f0                and    esp,0xfffffff0
    11a0:       ff 71 fc                push   DWORD PTR [ecx-0x4]
    11a3:       55                      push   ebp
    11a4:       89 e5                   mov    ebp,esp
    11a6:       53                      push   ebx
    11a7:       51                      push   ecx
    11a8:       e8 24 00 00 00          call   11d1 <__x86.get_pc_thunk.ax>
    11ad:       05 53 2e 00 00          add    eax,0x2e53
    11b2:       83 ec 0c                sub    esp,0xc
    11b5:       8d 90 08 e0 ff ff       lea    edx,[eax-0x1ff8]
    11bb:       52                      push   edx
    11bc:       89 c3                   mov    ebx,eax
    11be:       e8 6d fe ff ff          call   1030 <printf@plt>
    11c3:       83 c4 10                add    esp,0x10
    11c6:       90                      nop
    11c7:       8d 65 f8                lea    esp,[ebp-0x8]
    11ca:       59                      pop    ecx
    11cb:       5b                      pop    ebx
    11cc:       5d                      pop    ebp
    11cd:       8d 61 fc                lea    esp,[ecx-0x4]
    11d0:       c3                      ret

Ассемблер:
C-подобный:
08049000 <_start>:
8049000:       b8 04 00 00 00          mov    eax,0x4
8049005:       bb 01 00 00 00          mov    ebx,0x1
804900a:       b9 1f 90 04 08          mov    ecx,0x804901f
804900f:       ba 0e 00 00 00          mov    edx,0xe
8049014:       cd 80                   int    0x80
8049016:       b8 01 00 00 00          mov    eax,0x1
804901b:       31 db                   xor    ebx,ebx
804901d:       cd 80                   int    0x80

0804901f <hello_world>:
804901f:       48                      dec    eax
8049020:       65 6c                   gs ins BYTE PTR es:[edi],dx
8049022:       6c                      ins    BYTE PTR es:[edi],dx
8049023:       6f                      outs   dx,DWORD PTR ds:[esi]
8049024:       2c 20                   sub    al,0x20
8049026:       77 6f                   ja     8049097 <hello_world+0x78>
8049028:       72 6c                   jb     8049096 <hello_world+0x77>
804902a:       64 21 0a                and    DWORD PTR fs:[edx],ecx

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

Делаем hello world в виде шелл-кода

Взгляните на листинг программы, написанной на ассемблере. Сначала идут адреса, затем байты, а далее инструкции (8049000: b8 04 00 00 00 mov eax, 0x4). Запишем опкоды инструкций в виде шелл-кода.
Вручную всё делать очень не удобно. Bash нам в помощь: objdump -d ./hello_world|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g' (вместо ./hello_world можно подставить любую другую программу ).

Опкоды ( представлены в читаемом виде )
C:
"\xb8\x04\x00\x00\x00"
"\xbb\x01\x00\x00\x00"

"\xb9\x1f\x90\x04\x08"

"\xba\x0e\x00\x00\x00"
"\xcd\x80\xb8\x01\x00"
"\x00\x00\x31\xdb\xcd"
"\x80\x48\x65\x6c\x6c"
"\x6f\x2c\x20\x77\x6f"
"\x72\x6c\x64\x21\x0a"

Но работать этот шелл-код не будет, так как в нём присутствуют байты \x00 и строка hello_world указана по адресу ( "\xb9\x1f"\x90\x04\x08" - это инструкция mov ecx, 0x8040901f ), а в программе адрес может быть разный из-за механизма защиты . В шелл-коде точных адресов быть не должно. Решим проблему постепенно, начав заменять данные, расположенные по точному адресу, а затем уберём байты \x00.

Убираем точные адреса

1611681224261.png


Строка, которую нам нужно напечатать - Hello, world! Представим её в виде байтов. Утилита xxd нам поможет: echo "Hello, World!" | xxd -pu

Байтовое представление строки Hello, world!: 48656c6c6f2c20576f726c64210a. Для удобства разделим по 4 всю последовательность байтов: 48656c6c 6f2c2057 6f726c64 210a. Байтов в конце недостаточно. Во всех отделённых нами наборов байтов, их по 4, а в последнем всего лишь 2. Добавим любые байты кроме \x00, так как потом добавленные нами байты обрежутся программой. Я выберу байты \x90. Нам нужно расположить байты в порядке: little-enidan ( в обратном порядке ). Получится такая последовательность байт: 90900a21 646c726f 57202c6f 6c6c6548. Это просто байты строки.

Теперь превратим их в инструкции на ассемблере. Тут нам поможет фреймворк с утилитой rasm2.

Получаем опкоды инструкций

Bash:
rasm2 -a x86 -b 32 "push 0x90900a21"
rasm2 -a x86 -b 32 "push 0x646c726f"
rasm2 -a x86 -b 32 "push 0x57202c6f"
rasm2 -a x86 -b 32 "push 0x6c6c6548"
rasm2 -a x86 -b 32 "mov ecx, esp"
Флаг -a x86 -b 32 обозначают вывод для архитектуры x86.


Чтобы передать байты в стек нужна инструкция push. Регистр [/COLOR]esp[COLOR=rgb(97, 189, 109)] указывает на вершину стека. Переместим на значение вершине стека в регистр ecx.

Команда PUSH размещает значение в стеке, т.е. помещает значение в ячейку памяти, на которую указывает регистр ESP, после этого значение регистра ESP увеличивается на 4.

Как будет выглядить код на ассемблере
C-подобный:
push 90900a21
push 646c726f
push 57202c6f
push 6c6c6548
mov ecx, esp

В итоге получаем: 68210a9090 686f726c64 686f2c2057 6848656c6c 89e1. Заменим точный адрес в нашем шелл-коде на новые инструкции.

C:
"\xb8\x04\x00\x00\x00"
"\xbb\x01\x00\x00\x00"

"\x68\x21\x0a\x90\x90"
"\x68\x6f\x72\x6c\x64"
"\x68\x6f\x2c\x20\x57"
"\x68\x48\x65\x6c\x6c"
"\x89\xe1"

"\xba\x0e\x00\x00\x00"
"\xcd\x80\xb8\x01\x00"
"\x00\x00\x31\xdb\xcd"
"\x80\x48\x65\x6c\x6c"
"\x6f\x2c\x20\x77\x6f"
"\x72\x6c\x64\x21\x0a"


Замена нулевых байтов

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

Bash:
echo -ne '\xb8\x04\x00\x00\x00\xbb\x01\x00\x00\x00\x68\x21\x0a\x90\x90\x68\x6f\x72\x6c\x64\x68\x6f\x2c\x20\x57\x68\x48\x65\x6c\x6c\x89\xe1\xba\x0e\x00\x00\x00\xcd\x80\xb8\x01\x00\x00\x00\x31\xdb\xcd\x80\x48\x65\x6c\x6c\x6f\x2c\x20\x77\x6f\x72\x6c\x64\x21\x0a' > test
ndisasm -b32 test

Вывод утилиты ndisasm
C-подобный:
00000000  B804000000        mov eax,0x4
00000005  BB01000000        mov ebx,0x1
0000000A  68210A9090        push dword 0x90900a21
0000000F  686F726C64        push dword 0x646c726f
00000014  686F2C2057        push dword 0x57202c6f
00000019  6848656C6C        push dword 0x6c6c6548
0000001E  89E1              mov ecx,esp
00000020  BA0E000000        mov edx,0xe
00000025  CD80              int 0x80
00000027  B801000000        mov eax,0x1
0000002C  31DB              xor ebx,ebx
0000002E  CD80              int 0x80
00000030  48                dec eax
00000031  656C              gs insb
00000033  6C                insb
00000034  6F                outsd
00000035  2C20              sub al,0x20
00000037  776F              ja 0xa8
00000039  726C              jc 0xa7
0000003B  64210A            and [fs:edx],ecx

Инструкции, содержащие нулевые байты
C-подобный:
00000000  B804000000        mov eax,0x4
00000005  BB01000000        mov ebx,0x1
00000020  BA0E000000        mov edx,0xe
00000027  B801000000        mov eax,0x1

Нам нужно заменить инструкции с нулевыми байтами на другие. Нулевые байты образуются из-за того, что инструкция mov - двухбайтовая, а оставшиеся 2 байта из 4 компилятору нужно заменить нулями. Предлагаю заменить эти инструкции mov на сочетание двухбайтовых инструкций xor и mov.

Ассемблерные инструкции и их опкоды
C-подобный:
xor eax, eax ; \x31\xc0
mov al, 4      ; \xb0\x04

xor ebx, ebx ; \x31\xdb
mov bl, 1      ; \xb3\x01

xor edx, edx ; \x31\xd2
mov dl, 14    ; \xb2\x0e

xor eax, eax ; \x31\xc0
mov al, 1      ; \xb0\x01

Итоговый вариант Hello, World! в виде шелл-кода
C-подобный:
"\x31\xc0\xb0\x04"

"\x31\xdb\xb3\x01"

"\x68\x21\x0a\x90\x90"
"\x68\x6f\x72\x6c\x64"
"\x68\x6f\x2c\x20\x57"
"\x68\x48\x65\x6c\x6c"
"\x89\xe1"

"\x31\xd2\xb2\x0e"

"\xcd\x80"

"\x31\xc0\xb0\x01"

"\x31\xdb\xcd"
"\x80\x48\x65\x6c\x6c"
"\x6f\x2c\x20\x77\x6f"
"\x72\x6c\x64\x21\x0a"

Оформим весь этот набор байтов в виде программы на языке Си.

Код программы
C:
unsigned char hello_world[]=

// Заменённые инструкции

//"\xb8\x04\x00\x00\x00" mov eax,0x4
"\x31\xc0\xb0\x04"

//"\xbb\x01\x00\x00\x00" mov ebx,0x1
"\x31\xdb\xb3\x01"

"\x68\x21\x0a\x90\x90"
"\x68\x6f\x72\x6c\x64"
"\x68\x6f\x2c\x20\x57"
"\x68\x48\x65\x6c\x6c"
"\x89\xe1"

//"\xba\x0e\x00\x00\x00" mov edx,0xe
"\x31\xd2\xb2\x0e"

"\xcd\x80"

//"\xba\x01\x00\x00\x00" mov eax,0x1
"\x31\xc0\xb0\x01"

"\x31\xdb\xcd"
"\x80\x48\x65\x6c\x6c"
"\x6f\x2c\x20\x77\x6f"
"\x72\x6c\x64\x21\x0a";

void main() {
  int (*ret)() = (int(*)())hello_world;
  ret();
}



Компилируем: gcc hello_world_test.c -o hello_world_test -z execstack
Проверяем работоспособность: ./hello_world_test

Довольно долго это всё делать, если вы не хотите делать шелл-код для атаки на определённую компанию.
Существует замечательный инструменты Msfvenom и подобные ему. Msfvenom позволяет делать шелл-код по шаблону и даже закодировать его. Про этот инструмент и про сам metasploit на Codeby.net написано много информации. Про энкодеры информации в интернете тоже достаточно. Например: .
Хочу порекомендовать сайты: и . На этих сайтах вы сможете найти множество шелл-кодов.

Желаю вам удачи и здоровья. Не болейте и 🧠прокачивайте мозги🧠.
 
Последнее редактирование:

Alex Trim

Green Team
12.07.2020
18
11
Может быть 10 - это в десятичной системе счисления 16? Нам нужно 13, но берём 16 для выравнивания до 4 групп по 4 байта?
Hello, world!xxx

Если так, то как функция узнаёт, что вывод надо остановить на знаке ! ?
А. Для этого есть
len_hello: equ $ - hello_world ;

Вот. Теперь понял. Спасибо за ответ.
 
Последнее редактирование:
  • Нравится
Реакции: Mogen

Marylin

Mod.Assembler
Red Team
05.06.2019
295
1 304
Может быть 10 - это в десятичной системе счисления 16? Нам нужно 13, но берём 16 для выравнивания

Не совсем так...
Здесь 10 - это оператор "перевода строки" на новую.
Во-времена MS-DOS для этих целей использовалась пара 0Dh,0Ah (управляющие коды), что означает 0Dh (13) - возврат каретки, и 0Ah (10) - переход на новую строку. В Windows/Linux можно указывать только 10=0Аh, чтобы заставить курсор перейти на нов.строку.

1310.png


hello_world: db "Hello, world!", 10 ;// 10 - количество выделенных байт для строки
len_hello: equ $ - hello_world ;// вычисляем длину строки. $ указывает на строку hello_world

В свою очередь символом $ обозначается "текущий адрес".
Есть ещё $$ - это "адрес начала секции".
Соответственно чтобы вычислить длину строки, достаточно от текущего адреса($) отнять адрес метки hello_world:.
 
  • Нравится
Реакции: Alex Trim и Mogen

Mogen

Red Team
27.08.2019
315
597
Почему здесь число 10? В строке "Hello, world!" 13 символов.
Это опечатка?
Спасибо, что обратил внимание на мою ошибку.
Я не то объяснение написал 🤦🤦🤦
Извиняюсь за неточную информацию.

Не совсем так...
Здесь 10 - это оператор "перевода строки" на новую.
Во-времена MS-DOS для этих целей использовалась пара 0Dh,0Ah (управляющие коды), что означает 0Dh (13) - возврат каретки, и 0Ah (10) - переход на новую строку. В Windows/Linux можно указывать только 10=0Аh, чтобы заставить курсор перейти на нов.строку.

Посмотреть вложение 47736



В свою очередь символом $ обозначается "текущий адрес".
Есть ещё $$ - это "адрес начала секции".
Соответственно чтобы вычислить длину строки, достаточно от текущего адреса($) отнять адрес метки hello_world:.
@Marylin, спасибо за ответ на вопрос @Alex Trim :)
 
  • Нравится
Реакции: Alex Trim

Alex Trim

Green Team
12.07.2020
18
11
Может добавить в статью объяснение необходимости замены нулевых байтов?

Что-то типа: "Поскольку мы передаёт шелл-код в виде строки, то она не может содержать внутри себя нулевые байты, которые обозначают конец строки."
 

Mogen

Red Team
27.08.2019
315
597
Может добавить в статью объяснение необходимости замены нулевых байтов?

Что-то типа: "Поскольку мы передаёт шелл-код в виде строки, то она не может содержать внутри себя нулевые байты, которые обозначают конец строки."
Я вроде писал, что нулевые байты не должны быть в шелл-коде. Но редактировать эту статью я уже не могу
 

vag4b0nd

Green Team
24.05.2017
303
1 470
Занимательная статья. Видел что-то подобное в чтиве . Моё уважение @Mogen за такой отличный материал! Без воды, с толком, с расстановкой. Для начинающих самое то.

П.С.: знаю, что статья прошлогодняя, но сути дела не меняет.
 
  • Нравится
Реакции: Mogen
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!