Привет, киберрекруты! Статья посвящена написанию шеллкода и его внедрения в ELF как через уязвимость, так и с добавлением новой секцией данных. ТЗ следующее:
Код:
Написать программу, которая отправляет UDP пакет и его можно отследить в
WireShark.
Дополнительные требования:
1. Работающая программа написанная на языке ассемблер и программа не падает
после выполнения
2. Вставленный шеллкод в скомпилированну программу написанную на C/C++ -
выполняет свою задачу и поток управления восстанавливается (программа не
падает)
3. Вставить в любую существующую программу (у которого есть NX/DEP), шеллкод
выпоняет свою задачу и прога не падает
Из ТЗ, думаю, ясно, что статья будет состоять из четырех частей, одна из которых подготовительная. Начинаем.
Часть 0. Подготовка тест сервера
Python:
from socket import *
host = '127.0.0.1'
port = 4096
addr = (host,port)
Второй шаг - настройка сокета. Он должен работать с IPv4 и принимать UDP-пакеты:
udp_socket = socket(AF_INET, SOCK_DGRAM)
. Затем открываем подключение: udp_socket.bind(addr)
. После этого создаем бесконечный цикл, чтобы постоянно не запускать программу. Скрипт будет принимать пакеты и выводить информацию о подключении:
Python:
while True:
question = input('Do you want to quit? y\\n: ')
if question == 'y': break
print('wait data...')
conn, addr = udp_socket.recvfrom(1024)
print('client addr: ', addr)
Полный код:
Python:
from socket import *
host = '127.0.0.1'
port = 4096
addr = (host,port)
udp_socket = socket(AF_INET, SOCK_DGRAM)
udp_socket.bind(addr)
while True:
question = input('Do you want to quit? y\\n: ')
if question == 'y': break
print('wait data...')
conn, addr = udp_socket.recvfrom(1024)
print('client addr: ', addr)
udp_socket.close()
После запуска вижу такую картину:
Пока забываем про него и переходим к написанию программы на языке ассемблер.
Часть 1. Пишем программу на языке Ассемблер
Программа будет отправлять UDP-пакеты по заданному адресу. Для этого необходимо:
- Открыть сокет
- Подключиться
- Отправить пакет
В самом начале следует обнулить регистры, которые будем использовать. Это необходимо, потому что при внедрении шеллкода как нагрузки, значения регистров будут непредсказуемые:
Код:
xor rax, rax
xor rdi, rdi
xor rsi, rsi
xor rdx, rdx
xor r8, r8
xor r9, r9
xor r10, r10
xor r12, r12
Системный вызов socket имеет такой прототип: int socket(int domain, int type, int protocol);
domain - выбираем протокол, с помошью которого будет произведено соединение. Значение 0x02 соответвует IPv4.
type - выбираем семантику соединения. В нашем случае - UDP.
protocol - так как для данного протока используем единственный сокет, то и значание будет равно нулю.
Системный вызов socket - 0x29 .
Таким образом, первый кусок кода выглядит так:
Код:
xor rax, rax
xor rdi, rdi
xor rsi, rsi
xor rdx, rdx
xor r8, r8
xor r9, r9
xor r10, r10
xor r12, r12
add di, 0x02 ; IPv4
add si, 0x02 ; UDP
add al, 0x29
syscall
Дальше необходимо произвести соединение. Оно необходимо, потому что он соединяет сокет, на который ссылается сокет-дескриптором sockfd к адресу, указанному в addr .
Прототип выглядит так: int connect(int sockfd, const struct sockaddr addr, socklen_t addrlen);
sockfd - файловый дескриптор, который получаем после вызова функции socket
socklen_t addrlen - определяет размер aadr
const struct sockaddr addr - это структура в которой описывается адрес и порт получателя и тип протокола
Начну с создания структуры. Сначала прописывается тип протокола - IPv4 и он равен двум, после порт. В моем случаем порт равен 4096. Дальше адрес - 127.0.0.1. Таким образом, структура будет выглядеть так:
Код:
strct: dw 2 ; AF_INET ipv4
db 0x10,0x00 ; 4096
db 0x7f, 0x00, 0x00, 0x01 ; 127.0.0.1
db 0, 0, 0, 0, 0, 0, 0, 0
Так как в rax лежит сокет-дескриптор, то передаю его регистру rdi. Регистр rsi будет указателем на структуру strct. Длину, равная 32, передам rdx. В результате этот участок кода будет следующим:
Код:
xor rdi, rdi
xor rdx, rdx
xor rsi, rsi
add di, ax
xor r12, r12
add r12, rax
lea rsi, [rel + strct]
add rdx, 0x10
xor rax, rax
add al, 0x2a
syscall
Последняя часть кода - отправка пакета. Осуществляется с помощью функции sendto.
Прототип следующий:
ssize_t sendto(int sockfd, const void buf, size_t len, int flags, const struct sockaddr dest_addr, socklen_t addrlen);
sockfd - файловый дескриптор, который получаем после вызова функции socket
buf - сообщение, которое передаем
len - длина сообщения
flag - будет равен нулю
const struct sockaddr dest_addr - структура, в которой прописан адрес назначения
socklen_t addrlen - длина адреса
Для начала необходимо создать сообщение. Сообщение будет таким: message: db 'shellcode!',0 и его длина соответствено 10.
Суть такая же. Значение rax - передаю его регистру rdi . В rsi указателем передаю сообщение. rdx - длина, r10 - равен нулю, r8 - указателем передаю структуру с адресом и наконец r9 - длина адреса. Номер системного вызова sendto - 0x2c . Последняя часть кода:
Код:
xor rdi, rdi
xor rdx, rdx
add rdi, r12 ; sockfd
lea rsi, [rel + message] ; message
add dx, 0x0A ; len
xor r10, r10 ; flag
lea r8, [rel + strct]; dest addr
xor r9, r9
add r9, 0x10
xor rax, rax
add al, 0x2c
syscall
Чтобы программане падала - сделал петлю ( петля только в бинарном файле ) и только эта часть шеллкода и будет меняться в дальнейшем. В ней ничего сложного, просто на стек кладу адрес, с которого начинает работать бинарь, и возвращаюсь по этому адресу:
Код:
mov rax, 0x401000
push rax
xor rax, rax
ret
Полностью шеллкод выглядит так:
Код:
BITS 64
section .text
global _start
_start:
xor rax, rax
xor rdi, rdi
xor rsi, rsi
xor rdx, rdx
xor r8, r8
xor r9, r9
xor r10, r10
xor r12, r12
add di, 0x02
add si, 0x02
add al, 0x29
syscall
xor rdi, rdi
xor rdx, rdx
xor rsi, rsi
add di, ax
xor r12, r12
add r12, rax
lea rsi, [rel + strct]
add rdx, 0x10
xor rax, rax
add al, 0x2a
syscall
xor rdi, rdi
xor rdx, rdx
add rdi, r12
lea rsi, [rel + message]
add dx, 0x0A
xor r10, r10
lea r8, [rel + strct]
xor r9, r9
add r9, 0x10
xor rax, rax
add al, 0x2c
syscall
mov rax, 0x401000
push rax
xor rax, rax
ret
section .data
message: db 'shellcode!',0
strct: dw 2
db 0x10,0x00
db 0x7f, 0x00, 0x00, 0x01
db 0, 0, 0, 0, 0, 0, 0, 0
Как видно из листинга, ничего сложного. Теперь необходимо скомпилить, запустить, проверить на сервере и в WireShark. Компиляция происходит следующим образом:
Код:
nasm -felf64 -o udp.o udb.asm
ld udp.o -o udp
После запуска на серверной части можно увидеть следующее:
Теперь проврека в сетевой акуле:
Вот и пакет, который передается:
Переходим ко второй части.
Часть 2. Инъекция шеллкода в уязвимую программу
Чтобы инъекция шеллкода в УЯЗВИМУЮ программу прошла успешно, необходимо достаточно учесть один факт - бинарь должен быть без NX/DEP и было бы круто иметь RWX сегмент. Для теста взял задание с сайта
Ссылка скрыта от гостей
, только там были seccomp ограничения, поэтому пришлость патчить программу, чтобы не было лишних проблем. Через checksec , убедился, что NX отключен:Программа просто принимает шеллкод и его исполняет. Реверсить бинарь не имеет смыла, а также писать сплойт. Просто как нагрузку отправляю шеллкод и жду его исполнения. Будет только одна проблема. Она связана с падением программы - сегфолтом. В ТЗ сказано, что не должно такого быть:
Код:
Вставленный шеллкод в скомпилированну программу написанную на C/C++ -
выполняет свою задачу и поток управления восстанавливается (программа не падает)
Поэтому буду пробовать дважды. В первый раз просто загружу шеллкод без изменений, а во второй раз, буду делать поправки. Если бы в эльфаре не было б включенного ASLR, то просто изменил бы значение, которое кладется на стек:
Код:
mov rax, 0x401000
push rax
xor rax, rax
ret
Компилирую программу так:
Код:
nasm udb.asm -o shellcode
и запускаю в отладчике EDB:
Код:
cat shellcode | edb --run ./pcat.elf
call rdx - запускается шеллкод, а попасть мы должны в инструкции дальше, которые идут после отработки шеллкода. Как пашет шеллкод не особо интересно, потому что сама суть программы не менялась. Важно то, что находится на стеке:
Как видно из скрина на стеке лежит адрес возврата из шеллкода, который соответвует адресу инструкции mov eax, 0 . Поэтому поправки будут минимальные - просто уберем адрес, который кладется на стек:
Код:
xor rax, rax
add al, 0x2c
syscall
ret
В итоге полный шеллкод для этого случая такой:
Код:
BITS 64
section .text
global _start
_start:
xor rax, rax
xor rdi, rdi
xor rsi, rsi
xor rdx, rdx
xor r8, r8
xor r9, r9
xor r10, r10
xor r12, r12
add di, 0x02
add si, 0x02
add al, 0x29
syscall
xor rdi, rdi
xor rdx, rdx
xor rsi, rsi
add di, ax
xor r12, r12
add r12, rax
lea rsi, [rel + strct]
add rdx, 0x10
xor rax, rax
add al, 0x2a
syscall
xor rdi, rdi
xor rdx, rdx
add rdi, r12
lea rsi, [rel + message]
add dx, 0x0A
xor r10, r10
lea r8, [rel + strct]
xor r9, r9
add r9, 0x10
xor rax, rax
add al, 0x2c
syscall
ret
section .data
message: db 'shellcode!',0
strct: dw 2
db 0x10,0x00
db 0x7f, 0x00, 0x00, 0x01
db 0, 0, 0, 0, 0, 0, 0, 0
Запускаем и проверяем. Зпуск будет таким: cat shellcode | ./pcat.elf
Ответ сервера:
Из сетевой акулы:
Пакет:
Теперь переходим к последней части.
Часть 3. Инъекция шеллкода в эльф
На мой взгляд это самая интересная часть. Тут тоже есть микроограничение - в программе не должно быть PIE, то бишь отключен ASLR. Как жертву взял так же таск с
Ссылка скрыта от гостей
. Проверяя его выяснил следующее:Не важно, что тут отсутсвуют все защиты, главное, что нет PIE. Как работает инъекция? Суть максимально простая. Выделяем через mmap() память в самом бинаре, после записываем ее как новый сегмент памяти и туда уже вшиваем шеллкод. Почему же так важно, чтобы не было ASLR? Потому что важно знать адреса, где производить инъекцию. В результате будет работать так:
Данная схема была повзаимственна из книги - Практический анализ двоичных файлов. Эндриесс Д.
То есть, сначала отработает инъектированная область, потом уже сама программа. На данный момент имеем такой набор функций:
Однако, после инъекции, появится еще одна....
Чтобы программа вернулась к нормальной отработке необходимо посмотреть адрес _start , потому что с нее начинается работа программы:
В данном случае адрес - 0x400490 . Поэтому и шеллкод изменим - поместим на стек адрес _start :
Код:
mov rax, 0x400490
push rax
xor rax, rax
ret
В резльтате шеллкод будет таким:
Код:
BITS 64
section .text
global _start
_start:
xor rax, rax
xor rdi, rdi
xor rsi, rsi
xor rdx, rdx
xor r8, r8
xor r9, r9
xor r10, r10
xor r12, r12
add di, 0x02
add si, 0x02
add al, 0x29
syscall
xor rdi, rdi
xor rdx, rdx
xor rsi, rsi
add di, ax
xor r12, r12
add r12, rax
lea rsi, [rel + strct]
add rdx, 0x10
xor rax, rax
add al, 0x2a
syscall
xor rdi, rdi
xor rdx, rdx
add rdi, r12
lea rsi, [rel + message]add dx, 0x0A
xor r10, r10
lea r8, [rel + strct]
xor r9, r9
add r9, 0x10
xor rax, rax
add al, 0x2c
syscall
mov rax, 0x400490
push rax
xor rax, rax
ret
section .data
message: db 'shellcode!',0
strct: dw 2
db 0x10,0x00
db 0x7f, 0x00, 0x00, 0x01
db 0, 0, 0, 0, 0, 0, 0, 0
Теперь, как его инъектировать? В тулзе nameless , есть функция инъектирования, поэтому достаточно открыть файл с скопиленным шеллом, указать путь до жертвы и запустить.
Небольшой туториал по nameless
nameless - тулза для конструирования шеллкода и инъектирования его в бинарь.
Запускаем программу nameless - ./nameless , после чего видим такое окно:
После нажимаем на вкладку File->Open - это мы открываем шеллкод. Результат можно проверить в окне, которое подписано как Opened File :
или в окне логирования, которое находится внизу окна:
Так же появится в главном окне байты - скомпиленный шеллкод:
Дальше опять выбираем File->Open File to Inject - это открываем жертву, бинарь, куда инъектирую шеллкод. Результат можно посмотреть в окне с подписью Inject File :
и так же в окне логирования:
Дальше Tools->ELFInject и в окне логирования ожидаем завершения:
Примерно такой должен быть вывод программы.
Теперь проверяем используя IDA Pro:
Как видно из скрина, появилась новая секция данных - start , если перейдем к ней, то увидим наш шеллкод:
Поставлю точку останова на инструкции mov eax, offset _start и запускаю под отладчиком, чтобы проверить, что программа не падает. Дошли до точки останова:
Проверяю через импровизированный сервер ответ:
и через wireshark:
Так же пакет:
Отлично! Шеллкод рабочий, теперь главное, чтобы прога не упала. Дохожу до ret и еще раз на F8 и попадаю в _start :
Теперь в принципе можно отпустить процесс и программа продолжит свое нормальное выполение:
Вложения
Последнее редактирование модератором: