linux.hook-sub2

шприц для *bsd или функции на игле

крис касперски ака мыщъх, nofuckinge-mail

внедриться в адресное пространство чужого процесса — проще простого! в первой части статьи мы показали как сконструировать универсальный шприц-баян, теперь остается только замутить тот фармацевтический ### магический раствор, который будет введен внутрь чужеродных клеток машинного кода. навстречу нашей вакцине тут же устремится батальон иммунных тел, готовых сожрать ее в один момент (и ведь сожрут же!), но мыщщъх знает один хитрый рецепт…

Классический алгоритм внедрения shell-кода выглядит так: сохраняем несколько байт перехватываемой функции и ставим jump на свой thunk, который делает что задумано, выполняет сохраненные байты и передает управление оригинальной функции, которая может вызываться как по jump, так и по call (подробнее этот вопрос рассмотрен в статье «crackme, прячущий код на API-функциях», опубликованной в Хакере).

Самое сложное — выбрать место для размещения thunk'а. Это должна быть память доступная всем процессам, а такой памяти в нашем распоряжении нет! Мы знаем, что «подопытная» библиотека отображается на адресное пространство каждого процесса, но это пространство уже занято! Наскрести пару десятков байт, отведенных под выравнивание, вполне реально, только нам этого не хватает! Приходится хитрить.

Прежде всего мы можем разместить код перехватчика в какой-нибудь «ненужной» функции, например, gets, а в начало всех перехватываемых функций внедрить… нет, не jump (в этом случае перехватчик не сможет определить откуда пришел вызов), а call gets! Внутри gets, перехватчик выталкивает из стека адрес возврата, уменьшает его на длину команды call (в 32-разрядном режиме — 5 байт) и получает искомый указатель на функцию.

Рисунок 1 установка hook'а на функцию write, в начало которой внедрена команда перехода на gets (E8h 4Bh 73hF9hFFh), пусть вас не смущает тот факт, что HT-редактор показывает gets под именем _IO_gets, функция gets имеет несколько синонимов и это — один из них

Зная указатель, можно определить имя функции — в этом нам поможет функция dladdr из GNUExtensions. В POSIX она не входит, но поддерживается практически всеми UNIX'ами, так что на этот счет можно не волноваться. (Примечание: напоминаем, что при внедрении в gets, равно как и любую другую функцию, мы можем пересекать границы страниц, поскольку за концом текущей страницы наверняка находится совсем посторонняя область памяти! если же возникает необходимость модифицировать функцию gets целиком, необходимо найти все принадлежащие ей страницы, тем же самым методом, которым мы нашли первую из них).

Рисунок 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; *1); собираем 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[2); Листинг 2 модернизированный вариант программы mem.c, внедряющий в начало gets вызов write(1,&«*»,1); Компилируем программу и убеждаемся, что она работает, вплотную приближая нас к созданию полноценного перехватчика. Чуть-чуть усложнив thunk-код, мы сможем не только загаживать экран, но и сохранять log в файл! Программировать в машинных кодах очень неудобно и возникает естественное желание задействовать Си и другие языки высокого уровня. И это вполне возможно! Поскольку thunk код вызывается в контексте вызывавшего его процесса, он может загружать свои собственные динамические библиотеки, вызывая dlopen/dlsym. На машинном коде пишется лишь крохотный загрузчик, а основной код перехватчика сосредотачивается в динамической библиотеке, которую можно написать и на Си. Кстати говоря, отказываться от функции gets совершенно необязательно и мы можем перенести ее функционал в нашу динамическую библиотеку! Только переносить необходимо именно функционал (то есть переписывать функцию заново), а не пытаться копировать код — gets вызывает «свои» подфункции по относительным адресам. При перемещении ее тела на другое место они изменяться и… здравствуй, segmentationfault! Рисунок 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). Хороший перехватчик должен их учитывать. Рисунок 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 при выполнении которой все пойдет в разнос. Конечно, вероятность такого события исчезающе мала, но в особо ответственных случаях с ней все-таки стоит считаться. Рисунок 5 стандартный пролог функции getaddrinfo, испорченный несвоевременным переключением планировщика Чтобы «обезопасить» перехват, необходимо сократить длину внедряемой инструкции до одного байта, но… таких инструкций просто нет! То есть как это нет? А INT 03h (CCh) на что? Традиционно она используется для организации точек останова и на прикладном уровне защищенного режима возбуждает исключение, которое легко перехватить из ядра, а точнее из загружаемого модуля. Об этом уже писалось в статье «HandlingInterruptDescriptorTableforfunandprofit», опубликованной в 59 номере phrack, так что не будем повторяться. Заметим, что CCh конфликтует с некоторыми защитными механизмами и, естественно, с отладчиками, поэтому лучше внедрять не INT 03h, а какую-нибудь «запрещенную» однобайтовую команду типа CLI (FAh), возбуждающую исключение, которое мы будем отлавливать. ===== »> врезка точки останова ===== Отладчики могут устанавливать программные точки останова на библиотечные функции, внедряя в их начало команду INT 03h (CCh). В этом случае наш перехватчик не сможет распознать пролог, что не есть хорошо. Рисунок 6 стандартный пролог функции lchmod, искаженной программной точной останова (CCh), установленной отладчиком Выход очевиден — сравнивать только 2й, 3й, 4й и 5й байты пролога, игнорируя 1й байт. А что делать с точкой останова? Если записать call поверх нее, то она будет затерта и отладчик потеряет контроль за функцией, что в некоторых случаях неприемлемо и тогда необходимо внедряться со 2го байта, но в этом случае команда call полностью затрет SUB ESP,XXh и XXh придется сохранять где-то в другом месте. ===== заключение ===== Механизмы перехвата API-функций под windows хорошо исследованы и предложить радикально новый трюк довольно трудно. UNIX-системы исследованы намного хуже и таят множество нераскрытых возможностей, притягивающий хакеров и прочих творческих людей. Описанный мыщъх'ем способ — не единственный и, вероятно, не самый удобный, к тому же до «промышленного применения» ему еще расти и расти. Тем не менее, в мыщъхиных утилитах, написанный на скорый хвост, он вполне нормально работает — как под linux'ом, так и под BSD.

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
2)
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