Comments
Забыли четко сказать, что будет работать только под рутом:

$ ls -l /proc/176/mem
-rw------- 1 root root 0 дек 14 23:07 /proc/176/mem

Будет работать при правах на отладку. Файлом /proc/[pid]/mem владеет тот, кто запускал процесс, а права доступа проверяются как по владению, так и праву отлаживать этот процесс. Короче, всё сложно. Даже если вы рут, то ядро может отказать в чтении этого файла. Подробнее написано в proc(5) (раздел «/proc/[pid]/mem») и ptrace(2) (раздел «Ptrace access mode checking»).

Файлом /proc/[pid]/mem владеет тот, кто запускал процесс, а права доступа проверяются как по владению, так и праву отлаживать этот процесс

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

Есть еще более быстрый вариант с process_vm_readv/process_vm_writev, в рамках задачи это конечно не критично. С правами тоже самое — нужен PTRACE_MODE_ATTACH_REALCREDS.

Да, так по идее будет быстрее: можно забрать к себе весь ELF-образ из нескольких кусочков за одно обращение к ядру, описав эти куски в iovec, вместо того, чтобы делать по отдельному read для каждого куска.

Если к процессу удалось приаттачиться через ptrace то этот файл становится вполне себе читаемым.
Вот это если все и портит. Непривилегированный код не может такие действия в отношении всего, что выходит за рамки своей песочницы ~ собственных процессов.
Почему же так категорично, "всё портит"? Мы же не вирус пишем, так что очень даже не всё. А внутри своей песочницы, в частности как тут в статье в качестве примера говорилось «показывать содержимое полей ввода с паролями там, где разработчики не предусмотрели такую возможность», очень даже можно. А большего для дома, для семьи и не надо.
Для домашней песочницы гораздо больше подойдет один из следующих вариантов:
— патч в исходники тулзы с полем;
— добавление своего кода в секцию .init бинарника;
— менеджер паролей наконец.

Свое время нужно ценить все-таки.
Свое время нужно ценить все-таки.

Золотые слова! И именно поэтому не подходит ни патч в исходники тулзы с полем, ни добавление своего кода в секцию .init бинарника — на это уйдёт гораздо больше времени, чем на подобную тулзу. Против менеджера паролей ничего не скажу, тут да, время реально сэкономится.
Патч с подменой конструктора в эльфе и уж тем более патч исходников будут явно быстрее разбирательства с адресным пространством произвольного процесса. Первое можно делать в автоматизированном режиме если ассемблер не будет изменяться в дальнейшем. А вот с каждым новым «полем со звёздочками» придётся разбираться во многом вручную.

Даже патч не нужен, ведь можно LD_PRELOAD воспользоваться. Но это требует перезапуска приложения, а у автора неявное условие (иначе его метод не оправдан) — патчить уже исполняющийся процесс, остановить который нет возможности.

Все верно, но вменяемых практических задач, для которых атака на процесс обоснована так и не названо. Поскольку принцип изоляции адресных пространств и общая логика работы MMU взялись не на пустом месте — это именно атака. Коль скоро метод применим либо под root-ом, либо в песочнице, то и условие о невозможности перезапуска процесса требует пояснения.
Да. Я вероломно атаковал свою собственную машину. Чтоб знала, кто тут хозяин! :)

Если серьёзно, то это всё «ради науки»:
Однако, мне было интересно, как [CreateRemoteThread] может быть реализован.
Применять его в «практических задачах» — не интересно. Диалог с паролем — просто красивый повод и первоначальная идея для иллюстрации.

Я вполне себе осознаю, что хоть и технически это возможно, но практически, если очень надо, то проще будет взять LD_PRELOAD и перезапустить процесс. А потом поступить сознательно, реализовать нужную функциональность, и отправить сопровождающим патч, чтобы проблема могла решиться не только у меня на машине, но у других.
то и условие о невозможности перезапуска процесса требует пояснения.

Невозможно не в смысле "технически невозможно", а в смысле "тут крупные бабки крутятся, секунда простоя — твоя зарплата"

А почему функции mmap, ptrace, snprintf и другие не пришлось искать также, как и dlopen? Или вопрос наоборот — если все эти функции не пришлось искать и за нас их нашел компилятор шелл-кода, то зачем искать те первые две? Почему их нельзя найти также?

ptrace(), snprintf(), и прочее используются в загрузчике inject-thread — отдельном процессе, который загружает шелл-код в целевой процесс. Там эти функции у себя дома; их найдёт загрузик операционной системы, когда он загружает inject-thread на исполнение. (Да, как-то много загрузчиков в этом предложении.)


Точно так же не надо вручную искать вот эти все gtk_entry_get_input_purpose() в полезной нагрузке — их найдёт динамический загрузчик, живущий в целевом процессе. Он у себя дома и знает, как это делается.


Вручную необходимо находить только функции, непосредственно используемые в шелл коде. Вот он-то в целевом процессе «лишний» и не знает, где что находится.

Кажется понятно. Неплохо бы добавить в статью диаграмму потока, вроде такой: https://gojs.net/latest/samples/sequenceDiagram.html. 4 столбца с активностями:


  • Injector, ищет библиотеки и функции, используемые вспомогательным недо-потоком
  • Вспомогательный недо-поток, выполняет в адресном пространстве целевого процесса создание рабочего потока и погибает
  • Рабочий поток с нагрузкой, делает то, что нам нужно
  • Поток(и) приложения

В отличие от прототипа диаграммы, последние 3 процесса обвести рамочкой — адресное пространство целевого процесса.


Точно так же не надо вручную искать вот эти все gtk_entry_get_input_purpose() в полезной нагрузке — их найдёт динамический загрузчик, живущий в целевом процессе

Полагаю, динамический загрузчик нельзя использовать в недо-потоке, потому что это небезопасно (т.к. поток неполноценный)? В противном случае он бы мог сам найти нужные функции

Отличная идея про диаграмму последовательности исполнения! Мне показалось, что она выйдёт большой и непонятной, потому и лень было рисовать — и да! она вышла большой, но надеюсь не настолько непонятной. Теперь бы придумать, куда её вставить в статью.


Полагаю, динамический загрузчик нельзя использовать в недо-потоке, потому что это небезопасно (т.к. поток неполноценный)?

Можно, и он используется. Когда недопоток вызывает dlopen(), там как раз отрабатывает динамический загрузчик. Это формально не вполне безопасно, потому что недопоток всё же «недо-»: он хоть и исполняется независимо, но, например, thread-local память у него не своя личная, а позаимствованная у главного потока приложения, к которому подключился отладчик. Это теоретически может что-то сломать в pthread_create() и в синхронизации, так как главный поток мы при этом отпускаем. (Но по крайней мере у меня не падает, гы-гы.)


Отдельный недопоток нужен потому, что вот этим всем dlopen() и компании нужен стек — и много стека. Кроме того, они могут быть не reentrant, не готовы к тому, что кто-то вызовет dlopen() из dlopen(): например, захватят какой-то нерекурсивный мьютекс. Наконец, System V ABI не позволяет вызывать функции с произвольным состоянием стека: нужны вот эти все адреса возврата, правильное выравнивание, сохранение затираемых регистров, и прочее. Системые вызовы можно делать откуда попало, а вот для функций проще сделать отдельный поток со своим стеком.

Круто! Я так понимаю, разные потоки вы обозначили разными цветами, чтобы показать и какой поток это делает, и где исполняемый им код живет? Просто получилось, что столбцы с активностями теперь обозначают разное — где-то подписано, что это поток исполнения, а где-то мы видим, что это просто место в какой-то библиотеке (ведь наверное не может же у libc.so быть свой поток). Вообщем, не хватает на диаграмме объяснениц цветов активностей и что представляет собой каждый столбец. Плюс, для проформы, можно подписать, что по вертикали отложено время, идет сверху-вниз.


Можно, и он используется. Когда недопоток вызывает dlopen(), там как раз отрабатывает динамический загрузчик.

Думаю, здесь я смогу написать небольшое дополнение, почему же все-таки понадобился ручной поиск dlopen. В нагрузке мы использовали фунцию gtk_entry_get_input_purpose. Адрес этой функции станет известен программе (коду в payload.so), когда его загрузят динамическим загрузчиком. Или, если payload.so сам является исполняемой программой, когда его загрузят статическим загрузчиком. Но чтобы позвать динамический загрузчик, нужно вызвать функцию dlopen, адрес которой в обычной ситуации находится во время загрузки всего приложения (того, в которое мы будем внедряться, или payload.so, если мы саму ее запускаем, как приложение). Но в нашем случае приложение уже загружено и в новом загружаемом (а фактически — просто копируемом) нами извне коде никто этот адрес не пропишет. Поэтому придется прописать его самим, выполнив для этого часть работы штатного загрузчика.

Хорошая статья. Спасибо.
Способов применения этого метода тысяча и одна штучка. Конечно нужен рут/дебаг, для вредоносов подойдёт редко, но для разрабов самое то, особенно если приходится использовать чужую прогу без исходников с непонятным поведением.
Или же, например, можно добавить функционал контролирующий сложность пароля(вместо qwerty требовать Dgh@#5`as_5d8 длинной не менее 10 символов). Костыльно конечно, в продакшн пускать такое не стоит, но для мелкой конторы или личного использования костыль сойдёт.

strip по умолчанию не вырезает динамические символы, потому что они нужны для загрузки. Его можно попросить, конечно, но в результате получится нерабочий бинарник.

Хабр — торт! Спасибо за статью.


Такой вопрос — а если бы этой статьи не было, а мне хотелось бы узнать, как внедрять свои потоки в чужой процесс, что вообще надо было бы читать? Ведь в жизнь не догадаешься, как скрутить вместе все эти технологии.

Присоединяюсь, с удовольствием почитал и пришел в комментарии сказать, что Хабр — снова торт. ilammy, спасибо!
Only those users with full accounts are able to leave comments. Log in, please.