Различия

Здесь показаны различия между двумя версиями данной страницы.

Ссылка на это сравнение

articles:linux.hook-sub2 [2017/09/05 02:55] (текущий)
Строка 1: Строка 1:
 +====== linux.hook-sub2 ======
 +<​sub>​{{linux.hook-sub2.odt|Original file}}</​sub>​
 +
 +====== шприц для *bsd или функции на игле ======
 +
 +крис касперски ака мыщъх, nofuckinge-mail
 +
 +**внедриться в адресное пространство чужого процесса — проще простого! в первой части статьи мы показали как сконструировать универсальный шприц-баян,​ теперь остается только замутить тот фармацевтический ****### магический**** раствор,​ который будет введен внутрь чужеродных клеток машинного кода. навстречу нашей вакцине тут же устремится батальон иммунных тел, готовых сожрать ее в один момент (и ведь сожрут же!), но мыщщъх знает один хитрый рецепт…**
 +
 +===== введение =====
 +
 +Классический алгоритм внедрения shell-кода выглядит так: сохраняем несколько байт перехватываемой функции и ставим jump на свой thunk, который делает что задумано,​ выполняет сохраненные байты и передает управление оригинальной функции,​ которая может вызываться как по jump, так и по call (подробнее этот вопрос рассмотрен в статье "​crackme,​ прячущий код на API-функциях",​ опубликованной в Хакере).
 +
 +Самое сложное — выбрать место для размещения thunk'​а. Это должна быть память доступная всем процессам,​ а такой памяти в нашем распоряжении нет! Мы знаем, что "​подопытная"​ библиотека отображается на адресное пространство каждого процесса,​ но это пространство уже занято! Наскрести пару десятков байт, отведенных под выравнивание,​ вполне реально,​ только нам этого не хватает! Приходится хитрить.
 +
 +Прежде всего мы можем разместить код перехватчика в какой-нибудь "​ненужной"​ функции,​ например,​ gets, а в начало всех перехватываемых функций внедрить… нет, не jump (в этом случае перехватчик не сможет определить откуда пришел вызов),​ а call gets! Внутри gets, перехватчик выталкивает из стека адрес возврата,​ уменьшает его на длину команды call (в 32-разрядном режиме — 5 байт) и получает искомый указатель на функцию.
 +
 +{{linux.hook-sub2_Image_0.png}}
 +
 +Рисунок 1 установка hook'​а на функцию write, в начало которой внедрена команда перехода на gets (E8h 4Bh 73hF9hFFh), пусть вас не смущает тот факт, что HT-редактор показывает gets под именем _IO_gets, функция gets имеет несколько синонимов и это — один из них
 +
 +Зная указатель,​ можно определить имя функции — в этом нам поможет функция dladdr из GNUExtensions. В POSIX она не входит,​ но поддерживается практически всеми UNIX'​ами,​ так что на этот счет можно не волноваться. (//​**Примечание**////:​ напоминаем,​ что при внедрении в ////​gets////,​ равно как и любую другую функцию,​ мы можем пересекать границы страниц,​ поскольку за концом текущей страницы наверняка находится совсем посторонняя область памяти! если же возникает необходимость модифицировать функцию ////​gets////​ целиком,​ необходимо найти все принадлежащие ей страницы,​ тем же самым методом,​ которым мы нашли первую из них//).
 +
 +{{linux.hook-sub2_Image_1.png}}
 +
 +Рисунок 2 функция dladdr в действительности реализована в libc.so (впрочем,​ в старых версиях это было не так), где она называется _dl_addr
 +
 +Проблема в том, что dladdr находится в библиотеке libdl.x.so, которой может и не быть в памяти конкретно взятого процесса,​ а если она там есть, то хрен знает по какому адресу загружена. Некоторые хакеры утверждают,​ что в thunk-коде можно использовать только прямые вызовы ядра через интерфейс INT 80h, а все остальные функции недоступны. На самом деле это не так! Как показывает дизассемблер,​ dladdr это всего лишь "​обертка"​ вокруг _dl_addr, реализованной в libc.so.x, а она-то доступна наверняка! Вот только на базовый адрес загрузки закладываться ни в коем случае нельзя и вызов должен быть относительным.
 +
 +Простейшая подпрограмма генерации относительного вызова выглядит так:
 +
 +unsigned char buf_code[]={0xE8,​ 0x0, 0x0, 0x0,0x0}; // call 00000h
 +
 +call_r(char *lib_name, char *from, char *to, int delta)
 +
 +{
 +
 +unsigned char *base, *from, *to;
 +
 +
 +
 +base = dlopen(lib_name,​RTLD_NOW);​ if (!base) return -1;
 +
 +from = dlsym(base,​from);​ if (!from) return -1;
 +
 +to  = dlsym(base,​to);​ if (!to) return -1;
 +
 +
 +
 +*((unsigned int*)&​buf_code[1]) = to - from - sizeof(buf_code) - delta;
 +
 +return 666;
 +
 +}
 +
 +Листинг 1 подпрограмма генерирует относительный вызов и помещает его в глобальный буфер buf_code, lib_name – имя хакаемой библиотеки,​ from – имя функции,​ из которой будет осуществляться вызов (например,​ gets), to – имя функции,​ которую нужно вызывать (например,​ write), delta – смещение инструкции call от начала thunk-кода
 +
 +Функция call_r вызывается из программы-инсталлятора (например,​ нашей mem.c) и генерирует относительный вызов call по адресу from на адрес to. Она может использоваться для вызова любых функций,​ а не только _dl_addr.
 +
 +Модернизируем программу mem.c и отпатчим функцию gets так, чтобы она выводила символ "​*"​ на экран. Мы будем вызывать функцию write из библиотеки libc со следующими параметрами:​ write(1,&"​*",​1). Обратите внимание на конструкцию &"​*"​ — мы заталкиваем в стек символ "​*"​ и передаем функции его указатель. А что еще остается делать?​ Сегмент данных ведь недоступен! Приходится использовать стек! При желании туда можно затолкать не только один символ,​ но и ASCIIZ-строку (только не забудьте потом вытолкнуть обратно — некоторые забывают,​ в результате чего имеют несбалансированный стек и получают segmentationfault).
 +
 +// начало thunk-кода
 +
 +// заталкиваем в стек аргументы функции write,
 +
 +// но саму функцию еще не вызываем,​ т.к. не знаем ее адреса
 +
 +unsigned char buf_pre[]={0x6A,​0x2A,/​* push 2Ah*/
 +
 +0x8B,​0xDC,/​* mov ebx,esp*/
 +
 +0x33,​0xC0,/​* xor eax,eax*/
 +
 +0x40,/* inc eax*/
 +
 +0x50,/* push eax*/
 +
 +0x53,/* push ebx*/
 +
 +0x50/* push eax*/
 +
 +};
 +
 +// сюда записывается сгенерированный относительный вызов функции write
 +
 +unsigned char buf_code[]={0xE8,​0x0,​0x0,​0x0,​0x0};​
 +
 +// конец thunk-кода
 +
 +// выталкиваем аргументы из стека вместе с символом "​*"​
 +
 +// и возвращаемся по ret
 +
 +unsigned char buf_post[]={
 +
 +0x83,​0xC4,​0x10,/​* add esp,10 */
 +
 +0xC3/* ret */
 +
 +};
 +
 +// буфер в который будет записан собранный thunk-код в следующей последовательности:​
 +
 +// buf_pre + buf_code + buf_post
 +
 +unsigned char buf_dst[sizeof(buf_pre)+sizeof(buf_code)+sizeof(buf_post)];​
 +
 +// генерируем относительный вызов write
 +
 +call_r("​libc.so.6",​ "​gets",​ "​write",​ sizeof(buf_pre));​
 +
 +// собираем thunk-код
 +
 +memcpy(buf_dst,​buf_pre,​sizeof(buf_pre));​
 +
 +memcpy(buf_dst + sizeof(buf_pre),​ buf_code, sizeof(buf_code));​
 +
 +memcpy(buf_dst + sizeof(buf_pre) + sizeof(buf_code),​ buf_post, sizeof(buf_post));​
 +
 +
 +
 +// ПАТЧИМ
 +
 +//​-------------------------------------------------------------------------
 +
 +
 +
 +// ставим C3h (ret) или восстанавливаем стандартный пролог обратно
 +
 +//if (page_buf[((unsigned int)p)%PAGE_SIZE]==0xC3)
 +
 +//​page_buf[((unsigned int)p)%PAGE_SIZE] = 0x55;
 +
 +//else page_buf[((unsigned int)p)%PAGE_SIZE] = 0xC3;
 +
 +// копируем thunk-код поверх функции gets
 +
 +memcpy(&​page_buf[((unsigned int)p)%PAGE_SIZE],​
 +
 +buf_dst,​sizeof(buf_dst));​
 +
 +Листинг 2 модернизированный вариант программы mem.c, внедряющий в начало gets вызов write(1,&"​*",​1);​
 +
 +Компилируем программу и убеждаемся,​ что она работает,​ вплотную приближая нас к созданию полноценного перехватчика. Чуть-чуть усложнив thunk-код,​ мы сможем не только загаживать экран, но и сохранять log в файл!
 +
 +Программировать в машинных кодах очень неудобно и возникает естественное желание задействовать Си и другие языки высокого уровня. И это вполне возможно! Поскольку thunk код вызывается в контексте вызывавшего его процесса,​ он может загружать свои собственные динамические библиотеки,​ вызывая dlopen/​dlsym. На машинном коде пишется лишь крохотный загрузчик,​ а основной код перехватчика сосредотачивается в динамической библиотеке,​ которую можно написать и на Си.
 +
 +Кстати говоря,​ отказываться от функции gets совершенно необязательно и мы можем перенести ее функционал в нашу динамическую библиотеку! Только переносить необходимо именно функционал (то есть переписывать функцию заново),​ а не пытаться копировать код — gets вызывает "​свои"​ подфункции по относительным адресам. При перемещении ее тела на другое место они изменяться и… здравствуй,​ segmentationfault!
 +
 +{{linux.hook-sub2_Image_2.png}}
 +
 +Рисунок 3 результат работы кода, "​впрыснутого"​ в gets (звездочки и точки идут косяками за счет буферизации)
 +
 +===== перехват функций не во сне, а наяву =====
 +
 +Самое сложное в перехвате — это определить границы машинных инструкций,​ поверх которых записывается команда перехода на перехватчик (он же thunk, расположенный в нашем случае в теле функции gets). По-хорошему,​ для решения этой задачи требуется написать мини-дизассемблер,​ но… это же сколько всего писать придется! А можно ли без него обойтись?​ Можно!
 +
 +В начале большинства библиотечных функций расположен стандартный пролог вида PUSH EBP/​MOV EBP,​ESP/​SUB ESP,​XXXh (55h/​89h E5h/​ 83h ECh XXh),​ дающий нам пять байт — необходимый минимум для внедрения! Встречаются и другие,​ слегка видоизмененные прологи,​ например:​ PUSH EBP/​MOV EBP,​ESP/​PUSH EDI/​PUSH ESI (55h/​89h E5h/​ 57h/​ 56h);​ PUSH EBP/​MOV EAX,​ 0FFFFFFFFh/​MOV EBP,​ ESP (55h/​B8h FFh FFh FFh FFh/​89h E5h);​ PUSH EBP/​XOR EAX,​ EAX/​MOV EBP,​ESP (55h/​31h C0h/​89h E5h). Хороший перехватчик должен их учитывать.
 +
 +{{linux.hook-sub2_Image_3.png}}
 +
 +Рисунок 4 функция gettimer с прологом далеким от стандартного
 +
 +Таким образом,​ наш перехватчик должен проверить первые 5 байтов перехватываемой функции и, если они совпадают со стандартным (или слегка оптимизированным) прологом,​ скопировать этот пролог в свое тело и выполнить его перед передачей управления оригинальной функции. А куда его можно скопировать?​ Сегмент данных,​ как уже говорилось,​ нам недоступен,​ стек трогать нельзя (перед передачей управления на функции он должен быть восстановлен),​ а сегмент кода запрещен от модификации.
 +
 +Существует по меньше мере три решения:​ во-первых,​ мы можем вызывать функцию mprotect, присвоив кодовой странице атрибут writable, (но это некрасиво),​ во-вторых,​ трогать стек все-таки можно: забрасываем пролог на верхушку,​ забрасываем туда же копию всех аргументов (а сколько у функции аргументов?​ да хрен его знает, вот и приходится копировать с запасом) и передаем ей управление как ни в чем не бывало (но это уже не просто "​некрасиво",​ это вообще уродство). В-третьих,​ мы можем поступить так:
 +
 +// "​коллекция"​ разнообразных прологов для сравнения
 +
 +unsigned char prolog_1[]={0x55h,​0x89,​0xE5,​0x83,​0xEC};​
 +
 +unsigned char prolog_2[]={0x55,​0x89,​0xE5,​0x57,​0x56};​
 +
 +// буфер в который будет записан сгенерированный код
 +
 +unsigned char buf_code[1024];​
 +
 +// определяем адрес перехватываемой функции
 +
 +p = msym(base, fnc_name);
 +
 +// если в начале перехватываемой функции расположен prolog_1
 +
 +// внедряем в ее начало call на prepare_prolog_1
 +
 +if (!memcmp(p,​prolog_1,​sizeof(prolog_1))
 +
 +call_r(base,​ fnc_name, "​gets",​ 0);
 +
 +// если в начале перехватываемой функции расположен prolog_2
 +
 +// внедряем в ее начало call на prepare_prolog_2
 +
 +if (!memcmp(p,​prolog_1,​sizeof(prolog_2))
 +
 +call_r(base,​fnc_name,"​gets",​ offset prapare_prolog_2-offset prepare_prolog_1);​
 +
 +Листинг 3 фрагмент программы-инсталлятора,​ анализирующей пролог перехватываемой функции и устанавливающей обработчик с соответствующим прологом
 +
 +; // заносим номер "​нашего"​ пролога в регистр EAX,
 +
 +; // чтобы перехватчик знал какой ему пролог эмулировать
 +
 +; // ВНИМАНИЕ! этот код засирает EAX и не работает на fastcall-функциях,​
 +
 +; // для поддержки которых регистры трогать нельзя,​ а номер пролога класть на стек,
 +
 +; // восстанавливая его перед передачей управления оригинальной функции
 +
 +prepare_prolog_1:​
 +
 +MOVEAX, 0x1
 +
 +JMP short do_begin
 +
 +
 +
 +prepare_prolog_2:​
 +
 +MOV EAX, 0x2
 +
 +JMP short do_begin
 +
 +
 +
 +prepare_prolog_n:​
 +
 +MOV EAX, 0x2
 +
 +JMP do_begin
 +
 +do_begin:
 +
 +// ОСНОВНОЙ КОД ПЕРЕХВАТЧИКА
 +
 +// ДЕЛАЕМ ЧТО ЗАДУМАНО
 +
 +// [ESP+4]+5 содержит адрес вызванной функции
 +
 +// это поможет нам отличить перехваченные функции друг от друга
 +
 +
 +
 +
 +
 +
 +
 +// ПЕРЕДАЧА УПРАВЛЕНИЯ ПЕРЕХВАЧЕННОЙ ФУНКЦИИ
 +
 +// С ЭМУЛЯЦИЕЙ ЕЕ "​РОДНОГО"​ ПРОЛОГА
 +
 +DEC EAX
 +
 +JZ prolog_1
 +
 +DEC EAX
 +
 +JZ prolog_2
 +
 +
 +
 +
 +
 +prolog_1: ; // эмулируем выполнение пролога типа PUSHEBP/​MOVEBP,​ESP/​SUBESP,​XXX
 +
 +PUSH EBP
 +
 +MOV  EBP,ESP
 +
 +SUB ESP, byte ptr [EAX]; берем XXh изпамяти
 +
 +INCEAX; на след. машинную команду
 +
 +JMP EAX
 +
 +
 +
 +prolog_2: ;// эмулируемвыполнениепрологатипа PUSB EBP/MOV EBP,​ESP/​PUSH EDI/PUSH ESI
 +
 +PUSH EBP
 +
 +MOV EBP, ESP
 +
 +PUSH EDI
 +
 +PUSH ESI
 +
 +JMPEAX
 +
 +Листинг 4 базовый код перехватчика (расположенный в gets), поддерживающий несколько различных прологом
 +
 +Программа-инсталлятор анализирует пролог перехватываемой функции и, в зависимости от результата,​ внедряет в ее начало либо call prepare_prolog_1 либо call prepare_prolog_2,​ где prepare_prolog_x – метка, расположенная внутри thunk-кода,​ помещенного нами в функцию gets. Команда call занимает 5 байт и потому в аккурат накладывается на команду SUB ESP,​XXh так, что XXh оказывается прямо за ее концом. Поэтому,​ сохранять XXh в теле самого перехватчика не нужно!!! Команда SUB ESP, byte ptr [EAX],​ вызываемая из thunk-кода эмулирует выполнение SUB ESP,​XXh на ура!
 +
 +Приведенный пример портит регистр EAX и работает только с cdecl и stdcall функциями. Перехват fastcall-функции,​ передающих аргументы через EAX, по этой схеме невозможен. Однако,​ оригинальный EAX можно сохранять в стеке и восстанавливать непосредственно перед передачей управления перехваченной функции,​ но в этом случае JMP EAX придется заменить на RETN, а на верхушку стека предварительно положить адрес для перехода.
 +
 +Вот, собственно говоря,​ и все. Скелет перехватчика успешно собран и готов к работе. Остается дописать "​боевую начинку"​. Это может быть и логгер,​ протоколирующий вызовы,​ и анти-протектор,​ блокирующий вызовы некоторых функций (например,​ удаление файла),​ и макро-машина,​ "​подсовывающая"​ функциям клавиатурного ввода готовые данные и… да все что угодно!
 +
 +===== >>>​ врезка проблемы стабильности =====
 +
 +Сконструированный нами перехватчик довольно капризен по натуре и периодически падает без всяких видимых причин. Почему это происходит?​ Рассмотрим функцию func со стандартным прологом вида PUSH EBP/​MOV EBP,​ESP. Допустим,​ процесс A выполнил команду PUSH EBP и только собирался приступить к выполнению MOV EBP,​ESP как был прерван системным планировщиком и управление получил наш процесс B, осуществляющий перехват функции func путем внедрения в ее начало инструкции call. Когда процесс A возобновит свое выполнение,​ команды MOV EBP ESP там уже и не окажется,​ а будет торчать хвостовая часть от call при выполнении которой все пойдет в разнос. Конечно,​ вероятность такого события исчезающе мала, но в особо ответственных случаях с ней все-таки стоит считаться.
 +
 +{{linux.hook-sub2_Image_4.png}}
 +
 +Рисунок 5 стандартный пролог функции getaddrinfo,​ испорченный несвоевременным переключением планировщика
 +
 +Чтобы "​обезопасить"​ перехват,​ необходимо сократить длину внедряемой инструкции до одного байта, но… таких инструкций просто нет! То есть как это нет? А INT 03h (CCh) на что? Традиционно она используется для организации точек останова и на прикладном уровне защищенного режима возбуждает исключение,​ которое легко перехватить из ядра, а точнее из загружаемого модуля. Об этом уже писалось в статье "​HandlingInterruptDescriptorTableforfunandprofit",​ опубликованной в 59 номере phrack, так что не будем повторяться.
 +
 +Заметим,​ что CCh конфликтует с некоторыми защитными механизмами и, естественно,​ с отладчиками,​ поэтому лучше внедрять не INT 03h, а какую-нибудь "​запрещенную"​ однобайтовую команду типа CLI (FAh), возбуждающую исключение,​ которое мы будем отлавливать.
 +
 +===== >>>​ врезка точки останова =====
 +
 +Отладчики могут устанавливать программные точки останова на библиотечные функции,​ внедряя в их начало команду INT 03h (CCh). В этом случае наш перехватчик не сможет распознать пролог,​ что не есть хорошо.
 +
 +{{linux.hook-sub2_Image_5.png}}
 +
 +Рисунок 6 стандартный пролог функции lchmod, искаженной программной точной останова (CCh), установленной отладчиком
 +
 +Выход очевиден — сравнивать только 2й, 3й, 4й и 5й байты пролога,​ игнорируя 1й байт. А что делать с точкой останова?​ Если записать call поверх нее, то она будет затерта и отладчик потеряет контроль за функцией,​ что в некоторых случаях неприемлемо и тогда необходимо внедряться со 2го байта, но в этом случае команда call полностью затрет SUB ESP,​XXh и XXh придется сохранять где-то в другом месте.
 +
 +===== заключение =====
 +
 +Механизмы перехвата API-функций под windows хорошо исследованы и предложить радикально новый трюк довольно трудно. UNIX-системы исследованы намного хуже и таят множество нераскрытых возможностей,​ притягивающий хакеров и прочих творческих людей. Описанный мыщъх'​ем способ — не единственный и, вероятно,​ не самый удобный,​ к тому же до "​промышленного применения"​ ему еще расти и расти. Тем не менее, в мыщъхиных утилитах,​ написанный на скорый хвост, он вполне нормально работает — как под linux'​ом,​ так и под BSD.
 +
 +