кода выглядит так: сохраняем несколько
Классический алгоритм внедрения shell- кода выглядит так: сохраняем несколько байт перехватываемой функции и ставим jump на свой thunk, который делает что задумано, выполняет сохраненные байты и передает управление оригинальной функции, которая может вызываться как по jump, так и по call (подробнее этот вопрос рассмотрен в статье "crackme, прячущий код на API-функциях", опубликованной в Хакере).
Самое сложное — выбрать место для размещения thunk'а. Это должна быть память доступная всем процессам, а такой памяти в нашем распоряжении нет! Мы знаем, что "подопытная" библиотека отображается на адресное пространство каждого процесса, но это пространство уже занято! Наскрести пару десятков байт, отведенных под выравнивание, вполне реально, только нам этого не хватает! Приходится хитрить.
Прежде всего мы можем разместить код перехватчика в какой-нибудь "ненужной" функции, например, gets, а в начало всех перехватываемых функций внедрить… нет, не jump (в этом случае перехватчик не сможет определить откуда пришел вызов), а call gets! Внутри gets, перехватчик выталкивает из стека адрес возврата, уменьшает его на длину команды call (в 32-разрядном режиме — 5 байт) и получает искомый указатель на функцию.
Рисунок 1 установка hook'а на функцию write, в начало которой внедрена команда перехода на gets (E8h 4Bh 73h F9h FFh), пусть вас не смущает тот факт, что HT-редактор показывает gets под именем _IO_gets, функция gets имеет несколько синонимов и это — один из них
Зная указатель, можно определить имя функции — в этом нам поможет функция dladdr из GNU Extensions. В 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;
*((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-строку (только не забудьте потом вытолкнуть обратно — некоторые забывают, в результате чего имеют несбалансированный стек и получают segmentation fault).
// начало 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 вызывает "свои" подфункции по относительным адресам. При перемещении ее тела на другое место они изменяться и… здравствуй, segmentation fault!
Рисунок 3 результат работы кода, "впрыснутого" в gets (звездочки и точки идут косяками за счет буферизации)