Перехват функций (часть 1): Перехват вызовов функций разделяемых библиотек в Linux

Перехват функций (часть 1): Перехват вызовов функций разделяемых библиотек в Linux

При исследовании приложений на уязвимости в ОС Linux у нас не всегда есть исходный код или документация к этому приложению. Вследствие этого, в подобных ситуациях необходимо использовать методы больше подходящие для исследования черного ящика, когда информация об анатомии приложения получается посредством мониторинга сетевых коммуникаций, вызовов криптографических функций и файлового ввода-вывода.  

Автор: Джастин Кеттнер (Justin Kettner)

При исследовании приложений на уязвимости в ОС Linux у нас не всегда есть исходный код или документация к этому приложению. Вследствие этого, в подобных ситуациях необходимо использовать методы больше подходящие для исследования черного ящика, когда информация об анатомии приложения получается посредством мониторинга сетевых коммуникаций, вызовов криптографических функций и файлового ввода-вывода.

Один из способов мониторинга приложения для получения информации об алгоритме его работы – подцепить отладчик (например, GDB) к процессу и выгрузить содержимое регистров или стека, поскольку срабатывают точки останова во время вызова желаемых функций. Хотя подобный механизм позволяет прекрасно управлять многими вещами, такими как, потоком кода и содержимым регистров, но в тоже время этот процесс более трудоемок по сравнению с перехватом вызовов функций для модификации их поведения.

Перехват вызовов функций охватывает целый ряд техник, суть которых сводится к перехвату вызовов к уже существующим функциям и созданию обертки для изменения поведения этих функций во время их выполнения. В этой статье основное внимание будет уделено перехвату функций в Linux с использованием API динамического загрузчика, который позволяет нам динамически подгружать и выполнять функции из разделяемых библиотек системы во время выполнения и обертывать существующие функции при помощи переменной окружения LD_PRELOAD.

Переменная окружения LD_PRELOAD используется для указания разделяемой библиотеки, которую загрузчик должен загрузить первой. Загрузив нашу разделяемую библиотеку первой, мы сможем перехватывать вызовы функций, а используя API динамического загрузчика, мы можем привязать первоначальную функцию к указателю на функцию и передать исходные аргументы через этот указатель, фактически создав таким образом обертку для вызова функции.
В качестве демонстрации возьмем избитый пример с «hello world». В этом примере мы будем перехватывать функцию puts и изменять ее выходные данные.

Файл helloworld.c:

#include <stdio.h>
#include <unistd.h>
int main()
{
puts(“Hello world!\n”);
return 0;
}

Файл libexample.c:

#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
int puts(const char *message)
{
int (*new_puts)(const char *message);
int result;
new_puts = dlsym(RTLD_NEXT, “puts”);
if(strcmp(message, “Hello world!\n”) == 0)
{
result = new_puts(“Goodbye, cruel world!\n”);
}
else
{
result = new_puts(message);
}
return result;
}

Рассмотрим подробнее файл libexample.c:
  • В строке 5 происходит объявление функции. Для перехвата исходной функции puts мы определяем функцию с точно таким же именем и сигнатурой как у первоначальной функции libc puts.
  • В строке 7 происходит объявление new_puts (указатель на функцию), который будет указывать на первоначальную функцию puts. Как и в предыдущем случае, когда объявлялась перехватывающая функция, эта сигнатура указателя функции должна в точности соответствовать сигнатуре функции puts.
  • В строке 10 происходит инициализация указателя на функцию посредством функции dlsym(). Перечисление RTLD_NEXT сигнализирует API динамического загрузчика, что мы хотим получить экземпляр функции, связанной со вторым аргументом (в нашем случае puts), в следующей загруженной библиотеке.
  • Мы сравниваем аргумент, который передается нашей функции puts, со строкой «Hello world!\n» в строке 12. Если строки совпадают, происходит замена на «Goodbye, cruel world!\n». В противном случае мы просто передаем первоначальное сообщение в функцию puts в строке 14.

Теперь соберем все файлы и протестируем результат:

sigma@ubuntu:~/code$ gcc helloworld.c -o helloworld
sigma@ubuntu:~/code$ gcc libexample.c -o libexample.so -fPIC -shared -ldl -D_GNU_SOURCE
sigma@ubuntu:~/code$

Вначале мы как обычно компилируем файл helloworld.c. Затем мы компилируем libexample.c в разделяемую библиотеку, указывая флаги компиляции –shared и –fPIC, и линкуем с libdl при помощи флага ldl. Флаг -D_GNU_SOURCE указывается для удовлетворения условий #ifdef, которые позволяют нам использовать перечисление RTLD_NEXT. По желанию этот флаг можно убрать, если добавить строку «#define _GNU_SOURCE» где-нибудь вначале файла libexample.c.

После компиляции исходников мы устанавливаем в переменную окружение LD_PRELOAD путь к нашей только что созданной разделяемой библиотеке.

sigma@ubuntu:~/code$ export LD_PRELOAD=”/home/sigma/code/libexample.so”

После установки пути мы готовы к запуску нашего исполняемого файла helloworld. После запуска получаем следующий результат:

sigma@ubuntu:~/code$ ./helloworld
Goodbye, cruel world!
sigma@ubuntu:~/code$

Как и ожидалось, после запуска helloworld происходит перехват функции puts и выводится сообщение «Goodbye, cruel world!» вместо «Hello world!».

Теперь, когда мы познакомились с механизмом перехвата вызовов функций, рассмотрим более практичный пример. Давайте представим, что мы исследуем приложение, которое использует OpenSSL для шифрования конфиденциальных данных. Также предположим, что атака типа «человек посередине» оказалась неудачной. Для получения конфиденциальных данных мы будем перехватывать вызовы к функции SSL_write, которая отвечает за шифрование и передачу данных через сокет. Перехват SSL_write позволит нам записать строку, посылаемую к функции и передать первоначальные параметры, чтобы обойти защиту шифрования и в то же время не нарушить работу приложения.

Давайте взглянем на объявление функции SSL_write:

int SSL_write(SSL *ssl, const void *buf, int num);

Далее приводится код, который я написал для перехвата SSL_write, в файле hook.c:

#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
#include <openssl/ssl.h>
int SSL_write(SSL *context, const void *buffer, int bytes)
{
int (*new_ssl_write)(SSL *context, const void *buffer, int bytes);
new_ssl_write = dlsym(RTLD_NEXT, “SSL_write”);
FILE *logfile = fopen(“logfile”, “a+”);
fprintf(logfile, “Process %d:\n\n%s\n\n\n”, getpid(), (char *)buffer);
fclose(logfile);
return new_ssl_write(context, buffer, bytes);
}

Как видно из объявления функции, она должна возвращать целое значение и принимать три аргумента: указатель на SSL-контекст, указатель на буфер, содержащий строку для шифрования, и количество байтов для записи. Помимо объявления нашей функции, мы определить указатель, который будет связан с первоначальной функцией SSL_write, а затем проинициализируем его при помощи функции dlsym. После этого мы запишем идентификатор процесса (process ID) вызова функции SSL_write и строки, которая передается этой функции.

Теперь скомпилируем наш исходных код в разделяемую библиотеку:

sigma@ubuntu:~/code$ gcc hook.c -o libhook.so -fPIC -shared -lssl -D_GNU_SOURCE
sigma@ubuntu:~/code$

Единственное отличие от предыдущего примера: мы использовали флаг –lssl, который линкует наш код и библиотеку OpenSSL. Теперь установим в переменную LD_PRELOAD путь к только что созданной разделяемой библиотеке libhook:

sigma@ubuntu:~/code$ export LD_PRELOAD=”/home/sigma/code/libhook.so”
sigma@ubuntu:~/code$

После установки значения в переменную LD_PRELOAD мы готовы к перехвату вызовов к функции SSL_write. При тестировании мы будем использовать утилиту curl через протокол HTTPS и будем перехватывать HTTPS-запрос.

sigma@ubuntu:~/code$ curl https://www.netspi.com > /dev/null
% Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed
100 19086 0 19086 0 0 37437 0 –:–:– –:–:– –:–:– 60590
sigma@ubuntu:~/code$

После успешного завершения команды должен создаться лог-файл. Посмотрим на его внутренности:

sigma@ubuntu:~/code$ cat logfile
Process 11423:
GET / HTTP/1.1
User-Agent: curl/7.22.0 (i686-pc-linux-gnu) libcurl/7.22.0 OpenSSL/1.0.1 zlib/1.2.3.4 libidn/1.23 librtmp/2.3
Host: www.netspi.com
Accept: */*
sigma@ubuntu:~/code$

Из примера мы видим, что запрос был записан как обычный текст, а приложение отработало без каких-либо ошибок. В нашем случае для сохранения надежности данных использовалось только шифрование SSL. Также предполагалось, что атаки типа «человек посередине» могли быть осуществлены только на сетевом уровне. Любой подобный механизм, как видно из примера, был бы скомпрометирован.

В этой статье были показаны лишь некоторые примеры использования API динамического загрузчика и переменной окружения LD_PRELOAD. Как только созданная разделяемая библиотека будет загружена в память запущенного процесса, мы могли бы выгружать память процесса, исследовать память во время выполнения процесса или изменять динамические переменные.
Другие способы перехвата вызовов функций в основном используются в user-land руткитах или внутри вредоносных программ. Об этом будет рассказано в следующей статье.

Где кванты и ИИ становятся искусством?

На перекрестке науки и фантазии — наш канал

Подписаться