Comments 27
$ 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.
— патч в исходники тулзы с полем;
— добавление своего кода в секцию .init бинарника;
— менеджер паролей наконец.
Свое время нужно ценить все-таки.
Свое время нужно ценить все-таки.
Золотые слова! И именно поэтому не подходит ни патч в исходники тулзы с полем, ни добавление своего кода в секцию .init бинарника — на это уйдёт гораздо больше времени, чем на подобную тулзу. Против менеджера паролей ничего не скажу, тут да, время реально сэкономится.
Даже патч не нужен, ведь можно LD_PRELOAD
воспользоваться. Но это требует перезапуска приложения, а у автора неявное условие (иначе его метод не оправдан) — патчить уже исполняющийся процесс, остановить который нет возможности.
Если серьёзно, то это всё «ради науки»:
Однако, мне было интересно, как [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 символов). Костыльно конечно, в продакшн пускать такое не стоит, но для мелкой конторы или личного использования костыль сойдёт.
Хабр — торт! Спасибо за статью.
Такой вопрос — а если бы этой статьи не было, а мне хотелось бы узнать, как внедрять свои потоки в чужой процесс, что вообще надо было бы читать? Ведь в жизнь не догадаешься, как скрутить вместе все эти технологии.
CreateRemoteThread для Linux