Перехват системных вызовов в linux под x86-64
В интернете опубликовано множество статей по перехвату системных вызовов под x32. В рамках решения одной задачи появилась необходимость в перехвате системных вызовов под архитектурой x86-64 при помощи загружаемого модуля ядра. Приступим:
Перехват системных вызовов
Поиск адреса таблицы системных вызовов
Первый вариант: можно найти через таблицу дескрипторов прерываний (IDT), IDT — служит для связи обработчика прерывания с номером прерывания. В защищённом режиме адрес в физической памяти и размер таблицы прерываний определяется 80-битным регистром IDTR.В защищённом режиме элементом IDT является шлюз прерывания длиной 10 байт, содержащий сегментный (логический) адрес обработчика прерывания, права доступа и др. Нам такой метод не интересен, т.к. мы получим адрес обработчика, который сделан для совместимости с х32
Второй вариант, более интересен.
Для начала не большой экскурс: MSR – machine state register это набор регистров процессоров Интел, используемых в семействе x86 и x86-64 процессоров. Эти регистры предоставляют возможность контролировать и получать информацию о состоянии процессора. Все MSR регистры доступны только для системных функций и не доступны из пользовательских программ. Нас в частности интересует следующий регистр:MSR_LSTAR — 0xc0000082 (long mode SYSCALL target)
(полный список можно посмотреть в /usr/include/asm/msr-index.h).
В этом регистре хранится адрес обработчика прерываний для x86-64.
Получить адрес можно следующим образом:
int i, lo, hi;
asm volatile(«rdmsr» : «=a» (lo), «=d» (hi) : «c» (MSR_LSTAR));
system_call = (void*)(((long)hi <<32) | lo); Далее найдем адрес самой таблицы. Перейдем на только что полученный адрес и найдем в памяти последовательность \xff\x14\xc5(эти магические числа берутся, если посмотреть на код ядра, в частности, на код функции system_call, в которой происходит вызов обработчика из искомой). Считав следующие за ней 4 байта, мы получим адрес таблицы системных вызовов syscall_table. Зная ее адрес, мы можем получить содержимое этой таблицы (адреса всех системных функций) и изменить адрес любого системного вызова, перехватив его.
код для нахождения адреса таблицы системных вызовов:
unsigned char *ptr;
for (ptr=system_call, i=0; i if (ptr[0] == 0xff && ptr[1] == 0x14 && ptr[2] == 0xc5)
return (void*)(0xffffffff00000000 | *((unsigned int*)(ptr+3)));
ptr++;
>
Подмена на адреса новых системных вызовов
- Отключаем защиту памяти
- Переписываем адрес на адрес нашего обработчика
- Включаем защиту памяти
Этих знаний достаточно для подмены системных вызовов в Linux x86-64. Надеюсь кому-нибудь это будет полезным.
Спасибо за внимание.
Перехват системных вызовов с помощью ptrace
ptrace (от process trace) — системный вызов в некоторых unix-подобных системах (в том числе в Linux, FreeBSD, Max OS X), который позволяет трассировать или отлаживать выбранный процесс. Можно сказать, что ptrace дает полный контроль над процессом: можно изменять ход выполнения программы, смотреть и изменять значения в памяти или состояния регистров. Стоит оговориться, что никаких дополнительных прав при этом мы не получаем — возможные действия ограничены правами запущенного процесса. К тому же, при трассировке программы с setuid битом, этот самый бит не работает — привилегии не повышаются.
В статье будет показано, как перехватывать системные вызовы на примере ОС Linux.
1. Немного о ptrace
- request — это действие, которое необходимо осуществить, например PTRACE_CONT, PTRACE_PEEKTEXT
- pid — индентификатор трассируемого процесса
- addr и data зависят от request‘а
- PTRACE_SINGLESTEP — пошаговое выполнение программы, управление будет передаваться после выполнения каждой инструкции; такая трассировка достаточна медленна
- PTRACE_SYSCALL — продолжить выполнение программы до входа или выхода из системного вызова
- PTRACE_CONT — просто продолжить выполнение программы
2. Просмотр системных вызовов
Напишем программу для вывода списка системных вызовов, используемых программой (простенький аналог утилиты strace).
Итак, для начала необходимо сделать fork — родительский процесс будет отлаживать дочерний:
int main( int argc, char *argv[]) pid_t pid = fork();
if (pid)
parent(pid);
else
child();
return 0;
>
В дочернем процессе все просто — начинаем трассировку с PTRACE_TRACEME и запускаем нужную программу:
void child() ptrace(PTRACE_TRACEME, 0, 0, 0);
execl( «/bin/echo» , «/bin/echo» , «Hello, world!» , NULL);
perror( «execl» );
>
При выполнении execl трассируемый процесс остановится, передав свое новое состояние родительскому. Поэтому родительский процесс сначала должен подождать запуска программы с помощью waitpid (можно просто wait, так как дочерний процесс всего один):
Чтобы как-то различать системные вызовы и другие остановки (например SIGTRAP), предусмотрен специальный параметр PTRACE_O_TRACESYSGOOD — при остановке на системном вызове родительский процесс получит в статусе SIGTRAP | 0x80:
Теперь можно в цикле выполнять PTRACE_SYSCALL до выхода из программы, и смотреть значение регистра eax для определения номера системного вызова. Для этого используем PTRACE_GETREGS. Следует отметить, что регистр eax в момент остановки заменен, и поэтому необходимо использовать сохраненный state.orig_eax:
struct user_regs_struct state;
ptrace(PTRACE_SYSCALL, pid, 0, 0);
waitpid(pid, &status, 0);
// skip after syscall
ptrace(PTRACE_SYSCALL, pid, 0, 0);
waitpid(pid, &status, 0);
>
Запустив программу, увидим нечто подобное:
.
SYSCALL 6 at b783a430
SYSCALL 197 at b783a430
SYSCALL 192 at b783a430
SYSCALL 4 at b783a430
Hello, world!
SYSCALL 6 at b783a430
SYSCALL 91 at b783a430
SYSCALL 6 at b783a430
SYSCALL 252 at b783a430
Как видно, после системного вызова №4 (а это sys_write) выводится наш текст.
3. Перехват системного вызова
Попробуем теперь перехватить вызов, и сделать что-нибудь хорошее. Системный вызов write выглядит так:
- ebx: fd — файловый дескриптор (номер)
- ecx: buf — указатель на текст для вывода
- edx: n — количество байт
// sys_write
if (state.orig_eax == 4) char * text = ( char *)state.ecx;
ptrace(PTRACE_POKETEXT, pid, ( void *)(text+7), 0x72626168); //habr
ptrace(PTRACE_POKETEXT, pid, ( void *)(text+11), 0x00000a21); //!\n
>
.
SYSCALL 6 at 00556416
SYSCALL 197 at 00556416
SYSCALL 192 at 00556416
SYSCALL 4 at 00556416
Hello, habr!
SYSCALL 6 at 00556416
SYSCALL 91 at 00556416
SYSCALL 6 at 00556416
SYSCALL 252 at 00556416
Таким образом, мы перехватили системный вызов sys_write в программе /bin/echo для вывода своего текста. Это всего лишь простой пример использования ptrace. С его помощью также можно легко делать дампы памяти (это, кстати, очень помогает при решении линуксовых крэкмисов), устанавливать breakpoint’ы (с помощью PTRACE_SINGLESTEP или заменой интсрукции на 0xCC), анализировать регистры/переменные и многое другое. ptrace очень полезен, например, когда до проблемного участка кода быстро не добраться — если в отладчике приходится многократно прыгать, подменять данные, а потом программа умирает и приходится все делать заново; если же написать программу для отладки ptrace’ом — все эти действия необходимо описать только один раз, и они будут выполняться автоматически. Конечно, в некоторых отладчиках можно писать скрипты — но по возможностям они навернякак уступают.
UPD: забыл выложить полный исходник
Перехват системных вызовов в linux под x86-64
В интернете опубликовано множество статей по перехвату системных вызовов под x32. В рамках решения одной задачи появилась необходимость в перехвате системных вызовов под архитектурой x86-64 при помощи загружаемого модуля ядра. Приступим:
Перехват системных вызовов
Поиск адреса таблицы системных вызовов
Первый вариант: можно найти через таблицу дескрипторов прерываний (IDT), IDT — служит для связи обработчика прерывания с номером прерывания. В защищённом режиме адрес в физической памяти и размер таблицы прерываний определяется 80-битным регистром IDTR.В защищённом режиме элементом IDT является шлюз прерывания длиной 10 байт, содержащий сегментный (логический) адрес обработчика прерывания, права доступа и др. Нам такой метод не интересен, т.к. мы получим адрес обработчика, который сделан для совместимости с х32
Второй вариант, более интересен.
Для начала не большой экскурс: MSR – machine state register это набор регистров процессоров Интел, используемых в семействе x86 и x86-64 процессоров. Эти регистры предоставляют возможность контролировать и получать информацию о состоянии процессора. Все MSR регистры доступны только для системных функций и не доступны из пользовательских программ. Нас в частности интересует следующий регистр:MSR_LSTAR — 0xc0000082 (long mode SYSCALL target)
(полный список можно посмотреть в /usr/include/asm/msr-index.h).
В этом регистре хранится адрес обработчика прерываний для x86-64.
Получить адрес можно следующим образом:
int i, lo, hi;
asm volatile(«rdmsr» : «=a» (lo), «=d» (hi) : «c» (MSR_LSTAR));
system_call = (void*)(((long)hi <<32) | lo); Далее найдем адрес самой таблицы. Перейдем на только что полученный адрес и найдем в памяти последовательность \xff\x14\xc5(эти магические числа берутся, если посмотреть на код ядра, в частности, на код функции system_call, в которой происходит вызов обработчика из искомой). Считав следующие за ней 4 байта, мы получим адрес таблицы системных вызовов syscall_table. Зная ее адрес, мы можем получить содержимое этой таблицы (адреса всех системных функций) и изменить адрес любого системного вызова, перехватив его.
код для нахождения адреса таблицы системных вызовов:
unsigned char *ptr;
for (ptr=system_call, i=0; i if (ptr[0] == 0xff && ptr[1] == 0x14 && ptr[2] == 0xc5)
return (void*)(0xffffffff00000000 | *((unsigned int*)(ptr+3)));
ptr++;
>
Подмена на адреса новых системных вызовов
- Отключаем защиту памяти
- Переписываем адрес на адрес нашего обработчика
- Включаем защиту памяти
Этих знаний достаточно для подмены системных вызовов в Linux x86-64. Надеюсь кому-нибудь это будет полезным.
Спасибо за внимание.