Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Однажды меня спросили: можно ли написать реверс‑шелл байт эдак на 200, который, помимо прочего, менял бы себе имя, периодически — PID, варил кофе и желательно взламывал Пентагон? Ответ, увы, очевиден — «нельзя». Однако задача, как мне показалось, сама по себе весьма интересная. Посмотрим, какие есть пути к ее решению.
На просторах сети можно легко найти, к примеру, tiny shell, prism и другие реверс‑шеллы. Те из них, что написаны на С, занимают лишь десятки‑сотни килобайт. Так к чему создавать еще один?
А суть вот в чем. Цель данной статьи учебная: равно как разработка ядерных руткитов — один из наиболее наглядных способов разобраться с устройством самого ядра Linux, написание обратного шелла с дополнительной функциональностью и одновременно с ограничениями по размеру исполняемого файла позволяет изучить некоторые неожиданные особенности положения вещей в Linux, в частности касающихся ELF-файлов, их загрузки и запуска, наследования ресурсов в дочерних процессах и работы компоновщика (он же линкер, линковщик, редактор связей). По ходу дела нас ждет множество интересных открытий и любопытных хаков. А бонусом нам будет рабочий инструмент, который заодно можно допиливать и применять в пентесте. Посему начнем!
info
Результаты трудов доступны на гитхабе.
warning
Ни автор, ни редакция не несут ответственности за любые последствия использования приведенных в этой публикации сведений. Вся информация предоставлена исключительно ради информирования читателя.
Итак, наш реверс‑шелл помимо того, что подключаться к заданному хосту на заданный порт, также должен:
Сперва определимся с языком. Поскольку мы стремимся к минимально возможному размеру бинаря, в голову приходит лишь два варианта: С и ассемблер. Однако, как ты, вероятно, знаешь, хоть С и позволяет собирать крохотные по современным меркам Hello World’ы (примерно 17 и ~800 Кбайт при динамической и статической линковке соответственно против 2 Мбайт на Go), при компиляции С‑кода генерируется также код, отвечающий:
main()
;__libc_start_main()
в main()
.Конструкторы и деструкторы
Массивы функций‑конструкторов и функций‑деструкторов запускаются перед и после main()
соответственно. Их код находится в отдельных секциях в противовес «обычному», попадающему в .text
. Такие функции используются, например, для различных инициализаций в разделяемых библиотеках или для установки параметров буферизации в некоторых приложениях, взаимодействующих по сети (в частности, это иногда встречается в CTF-тасках). Чтобы функция попала в одну из этих секций, следует указывать __attribute__ ((constructor))
или __attribute__ ((destructor))
перед определением функции.
В некоторых случаях секции, хранящие эти функции, могут иметь имена .ctors
/.init
/.init_array
и .dtors
/.fini
/.fini_array
. Все они играют в целом одну роль, и различия нас в рамках данной статьи не интересуют. Подробнее о глобальных конструкторах и деструкторах можно почитать на wiki.osdev.org.
Также на выходе исполняемый файл может содержать секции с отладочной и прочей информацией (например, имена символов, версия компилятора), которая не используется непосредственно для его запуска и работы, но занимаемое файлом пространство увеличивает, и иногда значительно. О таких секциях мы поговорим немного позже.
Данная обвязка неразрывно связана с С‑бинарями как минимум в Linux. Для нас же в рамках нашей задачи она — балласт, от которого необходимо нещадно избавляться. Так что реверс‑шелл наш будет написан на великом и ужасном языке ассемблера (естественно, под x86). План таков: сперва напишем рабочий код, а уже затем будет заниматься кардинальным уменьшением его размера.
Мы будем использовать NASM. За основу возьмем простейший асмовый реверс‑шелл. Размышления на тему, должен ли наш код быть 32- или 64-битным, привели меня к выводу, что первый вариант предпочтительнее: инструкции в этом режиме меньше, а необходимой функциональности мы не теряем, ведь наша главная задача по сути состоит лишь в подключении к серверу и запуске оболочки, а сама она будет работать уже в 64-битном режиме.
Код будет делать следующее:
ps
, htop
;top
;/bin/sh
, stdin
, stdout
и stderr
которого связаны с сокетом, общающимся с сервером. Имя процесса также подменяется.Что ж, за дело!
В Linux можно встретить две «сущности», хранящие связанное с процессом имя. Назовем их «полное» и «краткое имя». Оба доступны через /proc
: полное в /proc/<pid>/cmdline
, краткое в /proc/<pid>/comm
(comm от command).
Краткое имя, согласно описанию, содержит имя исполняемого файла без пути до него. Это имя хранится в ядерной структуре task_struct
, описывающей процесс (задачу, если более корректно в терминах ядра), и имеет ограничение длины в 16 символов, включая нуль‑байт.
Полное имя содержит аргументы запуска программы, они же *argv[]
: в нулевом элементе массива — имя исполняемого файла так, как оно было указано при запуске; в остальных — аргументы, если они были переданы.
Смена краткого имени сложностей не вызывает. Воспользуемся для этого системным вызовом prctl()
. С его помощью процесс или поток может осуществлять различные операции над самим собой: над своим именем, привилегиями (capabilities), областями памяти, режимом seccomp и много чем еще. Номер нужной операции передается первым аргументом, затем идут остальные параметры, число которых может варьироваться. Нас интересует операция PR_SET_NAME
, где вторым аргументом передается указатель на новое имя. При этом, если имя с нуль‑байтом длиннее 16 символов, оно будет обрезано.
Таким образом, для смены краткого имени нужно вызвать prctl(PR_SET_NAME, NEW_ARGV)
, где 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 2 syscall
. Там же для зоопарка поддерживаемых в Linux платформ и ABI есть две таблицы: с инструкциями для совершения системного вызова и с регистрами, используемыми при передаче аргументов и возврате значений. Имей в виду, что соглашения о вызовах, по крайней мере на x86, отличаются от таковых в юзермодных приложениях.
Попробуем теперь переписать argv[0]
. Следующий кусок кода выполняет действия, аналогичные сишной strncpy(&argv[0], NEW_ARGV, strlen(argv[0] + 1))
, при этом адрес argv[0]
предварительно был положен на стек:
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[0]
("./asm_shell"
) был длиннее нового, то, несмотря на наличие завершающего нуль‑байта, вывод ps
будет таков.
Вывод ps при перезаписи нулевого аргумента «в лоб»
Как‑то не особо здорово. Попробуем его сначала заполнить нулями и лишь затем перезаписывать.
Вывод ps при перезаписи зануленного нулевого аргумента
Уже лучше — в выводе ps
ничего подозрительного! Хотя все еще есть к чему стремиться. А что скажет нам мануал? Совсем немного поискав, натыкаемся на такое место в man 5 proc
(подраздел о /proc/[pid]/cmdline
):
А в man 2 prctl
находим, помимо параметра PR_SET_MM_ARG_START
, также PR_SET_MM_ARG_END
(с небольшой пометкой, что эти опции доступны начиная с версии Linux 3.5). Кажется, второй параметр — как раз то, что надо! Да вот незадача: для выполнения операций prctl()
, затрагивающих память процесса, нужна привилегия CAP_SYS_RESOURCE
(иначе ведь было бы слишком уж просто!). А ее установка требует прав суперпользователя.
По этой же причине замена адреса самого массива строк argv[]
на стеке «в лоб» не приведет к смене содержимого /proc/[pid]/cmdline
: Linux хранит адреса начала и конца памяти, где находятся аргументы процесса, причем содержимое именно этой памяти и выводится. То же верно и для переменных окружения. И потому xxd
выводит нули.
В общем, будем исходить из предположения, что реверс‑шелл запущен от имени простого пользователя и возможности установить CAP_SYS_RESOURCE
никоим образом нет. Поэтому просто занулим весь изначальный argv[0]
и запишем поверх него свой. Часто ли кому‑либо приходит в голову смотреть имя процесса через /proc
в xxd
?
Осталось разобраться с подменой имени /bin/sh
, ведь после вызова execve()
для запуска шелла его *argv[]
будет предательски являть взору админа /bin/sh
в выводе ps
и htop
, а также в /proc/<pid>/cmdline
. К счастью, это решается проще простого: нужно всего лишь передать собственный argv[0]
вторым аргументом этому сисколу. Притом важно иметь в виду, что передается указатель на массив аргументов (строк), который должен завершаться нулевым указателем. Поэтому перед тем, как положить на стек адрес 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()
так просто мы уже не можем, поскольку работаем из оболочки, где вызов сисколов напрямую недоступен. Однако есть иные интересные способы это сделать.
Источник: xakep.ru