Содержание статьи
И снова: зачем?
На просторах сети можно легко найти, к примеру, tiny shell, prism и другие реверс‑шеллы. Те из них, что написаны на С, занимают лишь десятки‑сотни килобайт. Так к чему создавать еще один?
А суть вот в чем. Цель данной статьи учебная: равно как разработка ядерных руткитов — один из наиболее наглядных способов разобраться с устройством самого ядра Linux, написание обратного шелла с дополнительной функциональностью и одновременно с ограничениями по размеру исполняемого файла позволяет изучить некоторые неожиданные особенности положения вещей в Linux, в частности касающихся ELF-файлов, их загрузки и запуска, наследования ресурсов в дочерних процессах и работы компоновщика (он же линкер, линковщик, редактор связей). По ходу дела нас ждет множество интересных открытий и любопытных хаков. А бонусом нам будет рабочий инструмент, который заодно можно допиливать и применять в пентесте. Посему начнем!
info
Результаты трудов доступны на гитхабе.
warning
Ни автор, ни редакция не несут ответственности за любые последствия использования приведенных в этой публикации сведений. Вся информация предоставлена исключительно ради информирования читателя.
Определяемся с ТЗ
Итак, наш реверс‑шелл помимо того, что подключаться к заданному хосту на заданный порт, также должен:
- изменять собственное имя при запуске — так мы будем менее заметны;
- периодически менять идентификатор процесса — так мы будем менее уловимы;
- иметь минимально возможный размер — так будет интереснее.
Сперва определимся с языком. Поскольку мы стремимся к минимально возможному размеру бинаря, в голову приходит лишь два варианта: С и ассемблер. Однако, как ты, вероятно, знаешь, хоть С и позволяет собирать крохотные по современным меркам Hello World’ы (примерно 17 и ~800 Кбайт при динамической и статической линковке соответственно против 2 Мбайт на Go), при компиляции С‑кода генерируется также код, отвечающий:
- за запуск глобальных конструкторов и деструкторов, если они есть;
- за корректную передачу аргументов в
main(
;) - за передачу управления затем из
__libc_start_main(
в) main(
.)
Конструкторы и деструкторы
Массивы функций‑конструкторов и функций‑деструкторов запускаются перед и после main(
соответственно. Их код находится в отдельных секциях в противовес «обычному», попадающему в .
. Такие функции используются, например, для различных инициализаций в разделяемых библиотеках или для установки параметров буферизации в некоторых приложениях, взаимодействующих по сети (в частности, это иногда встречается в CTF-тасках). Чтобы функция попала в одну из этих секций, следует указывать __attribute__ ((
или __attribute__ ((
перед определением функции.
В некоторых случаях секции, хранящие эти функции, могут иметь имена .
/.
/.
и .
/.
/.
. Все они играют в целом одну роль, и различия нас в рамках данной статьи не интересуют. Подробнее о глобальных конструкторах и деструкторах можно почитать на wiki.osdev.org.
Также на выходе исполняемый файл может содержать секции с отладочной и прочей информацией (например, имена символов, версия компилятора), которая не используется непосредственно для его запуска и работы, но занимаемое файлом пространство увеличивает, и иногда значительно. О таких секциях мы поговорим немного позже.
Данная обвязка неразрывно связана с С‑бинарями как минимум в Linux. Для нас же в рамках нашей задачи она — балласт, от которого необходимо нещадно избавляться. Так что реверс‑шелл наш будет написан на великом и ужасном языке ассемблера (естественно, под x86). План таков: сперва напишем рабочий код, а уже затем будет заниматься кардинальным уменьшением его размера.
Кодим
Мы будем использовать NASM. За основу возьмем простейший асмовый реверс‑шелл. Размышления на тему, должен ли наш код быть 32- или 64-битным, привели меня к выводу, что первый вариант предпочтительнее: инструкции в этом режиме меньше, а необходимой функциональности мы не теряем, ведь наша главная задача по сути состоит лишь в подключении к серверу и запуске оболочки, а сама она будет работать уже в 64-битном режиме.
Код будет делать следующее:
- при запуске реверс‑шелл меняет свой первый аргумент запуска — эти аргументы отображаются в
ps
,htop
; - также меняет краткое имя — оно отображается утилитой
top
; - затем пробует подключиться к серверу. При неудаче создается дочерний процесс, завершается родительский, выжидается тайм‑аут, после чего попытка подключения повторяется;
- при успешном подключении запускается
/
,bin/ sh stdin
,stdout
иstderr
которого связаны с сокетом, общающимся с сервером. Имя процесса также подменяется.
Что ж, за дело!
Что в имени тебе?
В Linux можно встретить две «сущности», хранящие связанное с процессом имя. Назовем их «полное» и «краткое имя». Оба доступны через /
: полное в /
, краткое в /
(comm от command).
Краткое имя, согласно описанию, содержит имя исполняемого файла без пути до него. Это имя хранится в ядерной структуре task_struct
, описывающей процесс (задачу, если более корректно в терминах ядра), и имеет ограничение длины в 16 символов, включая нуль‑байт.
Полное имя содержит аргументы запуска программы, они же *argv[
: в нулевом элементе массива — имя исполняемого файла так, как оно было указано при запуске; в остальных — аргументы, если они были переданы.
Смена краткого имени сложностей не вызывает. Воспользуемся для этого системным вызовом prctl(
. С его помощью процесс или поток может осуществлять различные операции над самим собой: над своим именем, привилегиями (capabilities), областями памяти, режимом seccomp и много чем еще. Номер нужной операции передается первым аргументом, затем идут остальные параметры, число которых может варьироваться. Нас интересует операция PR_SET_NAME
, где вторым аргументом передается указатель на новое имя. При этом, если имя с нуль‑байтом длиннее 16 символов, оно будет обрезано.
Таким образом, для смены краткого имени нужно вызвать prctl(
, где NEW_ARGV
содержит адрес нового имени. Для этого используем следующий код:
mov eax, 0xac ; NR_PRCTL mov ebx, 15 ; PR_SET_NAME mov ecx, NEW_ARGV int 0x80 ; syscall interrupt ...NEW_ARGV: db "s0l3g1t", 0
info
Много полезной информации о системных вызовах можно найти в man
. Там же для зоопарка поддерживаемых в Linux платформ и ABI есть две таблицы: с инструкциями для совершения системного вызова и с регистрами, используемыми при передаче аргументов и возврате значений. Имей в виду, что соглашения о вызовах, по крайней мере на x86, отличаются от таковых в юзермодных приложениях.
Попробуем теперь переписать argv[
. Следующий кусок кода выполняет действия, аналогичные сишной strncpy(
, при этом адрес argv[
предварительно был положен на стек:
mov edi, [esp] ; edi = &argv[0] mov esi, NEW_ARGV mov ecx, _start - NEW_ARGV ; ecx = strlen(NEW_ARGV) + NULL-byte _name_loop: movsb ; edi[i] = esi[i] ; i+=1 loop _name_loop ...NEW_ARGV: db "s0l3g1t", 0_start:...
Этот адрес помещается в регистр edi
(destination index register). В регистр esi
(source index register) отправляется адрес устанавливаемого нами имени "s0l3g1t"
, а в ecx
— его длина, включая нулевой байт. Однако оказывается, что если изначальный argv[
("./
) был длиннее нового, то, несмотря на наличие завершающего нуль‑байта, вывод ps
будет таков.
Как‑то не особо здорово. Попробуем его сначала заполнить нулями и лишь затем перезаписывать.
Уже лучше — в выводе ps
ничего подозрительного! Хотя все еще есть к чему стремиться. А что скажет нам мануал? Совсем немного поискав, натыкаемся на такое место в man
(подраздел о /
):
Furthermore, a process may change the memory location that this file refers via prctl(2) operations such as PR_SET_MM_ARG_START.
А в man
находим, помимо параметра PR_SET_MM_ARG_START
, также PR_SET_MM_ARG_END
(с небольшой пометкой, что эти опции доступны начиная с версии Linux 3.5). Кажется, второй параметр — как раз то, что надо! Да вот незадача: для выполнения операций prctl(
, затрагивающих память процесса, нужна привилегия CAP_SYS_RESOURCE
(иначе ведь было бы слишком уж просто!). А ее установка требует прав суперпользователя.
По этой же причине замена адреса самого массива строк argv[
на стеке «в лоб» не приведет к смене содержимого /
: Linux хранит адреса начала и конца памяти, где находятся аргументы процесса, причем содержимое именно этой памяти и выводится. То же верно и для переменных окружения. И потому xxd
выводит нули.
В общем, будем исходить из предположения, что реверс‑шелл запущен от имени простого пользователя и возможности установить CAP_SYS_RESOURCE
никоим образом нет. Поэтому просто занулим весь изначальный argv[
и запишем поверх него свой. Часто ли кому‑либо приходит в голову смотреть имя процесса через /
в xxd
?
Осталось разобраться с подменой имени /
, ведь после вызова execve(
для запуска шелла его *argv[
будет предательски являть взору админа /
в выводе ps
и htop
, а также в /
. К счастью, это решается проще простого: нужно всего лишь передать собственный argv[
вторым аргументом этому сисколу. Притом важно иметь в виду, что передается указатель на массив аргументов (строк), который должен завершаться нулевым указателем. Поэтому перед тем, как положить на стек адрес NEW_ARGV
, туда кладется 0:
xor eax, eax push dword 0x0068732f ; push "/sh" push dword 0x6e69622f ; push /bin (="/bin/sh") mov ebx, esp ; ebx = ptr to "/bin/sh" into ebx push edx ; edx = 0x00000000mov edx, esp ; **envp = edx = ptr to NULL addresspush ebx ; pointer to /bin/shpush 0push NEW_ARGV mov ecx, esp ; ecx points to shell's argv[0] ( &NEW_ARGV ) mov al, 0xb int 0x80 ; execve("/bin/sh", &{ NEW_ARGV, 0 }, 0)
Но сменить при этом и краткое имя через prctl(
так просто мы уже не можем, поскольку работаем из оболочки, где вызов сисколов напрямую недоступен. Однако есть иные интересные способы это сделать.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»