Изображение сгенерировано нейросетью Midjourney
Изображение сгенерировано нейросетью Midjourney

Python, Java, C++, Delphi, PHP — именно эти разные языки программирования использовались в этот раз для создания виртуальной машины криптомата-банкомата, которую должны были испытать на прочность участники в рамках конкурса $NATCH, проходившего на Positive Hack Days 12, чтобы победить. Весь код от начала и до конца был написан с помощью ChatGPT и показал себя исключительно хорошо. Мы пересмотрели концепцию конкурса и использовали систему репортов. Помимо стандартных задач (обхода киоска, повышения привилегий и обхода AppLocker), в этом году участников ждали новые нестандартные задания, о которых читайте в этой статье.

@nikaleksey, @drd0c, @s0x28 — победители конкурса по взлому банкоматов 3.0
@nikaleksey, @drd0c, @s0x28 — победители конкурса по взлому банкоматов 3.0

Немного про конкурс и статистику

В этом году банкомат выдавал участникам специальные банкноты, которые можно было обменять на мерч: футболки и сувенирные банковские карточки.

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

Конкурсный банкомат на PHDays 12
Конкурсный банкомат на PHDays 12

В чат конкурса за два дня вступило более 100 человек (после конкурса некоторые вышли, но история все помнит).

Количество подписчиков в чате конкурса
Количество подписчиков в чате конкурса

Победители разделили в соответствии с финальным скорбордом выигрыш в 50 000 рублей. Таблица с очками участников выглядела так:

Таблица с очками участников
Таблица с очками участников

Вы не поверите, но первые три результата были одинаковыми, поэтому итоговое решение пришлось принимать нам, как организаторам.

????@drd0c — 25 000 рублей + рюкзак

????@s0x28 — 12 500 рублей + рюкзак

????@nikaleksey — 12 500 рублей + рюкзак

Все участники шли нога в ногу, было бы обидно отдать третье место тому, кто его не заслужил, поэтому бронзы в этом году не было. Репорты @drd0c были, без сомнения, интереснее, именно поэтому мы признали его победителем конкурса.

В этом году мы добавили механизм обновления виртуальной машины: прямо во время конкурса участники могли запустить файл updater.py, после чего скачивался архив update.zip с новыми тасками.

Демонстрация процесса обновления криптомата-банкомата
Демонстрация процесса обновления криптомата-банкомата

Разбор некоторых заданий

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

Обход режима киоска № 1

Первый киоск выглядел так: вы видите интерфейс криптомата, там есть QR-код, который можно отсканировать. В нем закодирован bitcoin-кошелек, на который нужно отправлять биткоины, чтобы криптомат впоследствии выдавал купюры. Публичный и приватный ключ в дальнейшем можно использовать, чтобы управлять своими средствами, доступными на кошельке — по факту для конкурса был написан настоящий bitcoin-кошелек, доступный в виртуалке. Настоящие криптоматы работают именно по такому принципу, а под капотом у них, как правило, Windows.

Интерфейс криптомата
Интерфейс криптомата

Кстати, сами кошельки хранились в файле wallets.txt в таком виде, который вы можете увидеть на скриншоте ниже.

Кошельки в файле wallets.txt
Кошельки в файле wallets.txt

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

А теперь перейдем к киоску: иногда в банкоматах доступен сервисный режим киоска (сделано это для удобства персонала, обслуживающего банкомат), например, клавиатура может включаться и выключаться всего лишь с помощью одной клавиши. Первый способ обхода киоска в этом году заключался именно в этом. Каждый раз, когда пользователь нажимал клавишу u или г, клавиатура вдруг начинала работать. Звучит просто, но на самом деле, если не знать, то это сложно заметить.

import sys
import keyboard
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget
from PyQt5.QtWebEngineWidgets import QWebEngineView

class KioskApp(QMainWindow):
    def __init__(self):
        super().__init__()

        # Настройка окна
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
        self.showFullScreen()

        # Создание и настройка веб-браузера
        browser = QWebEngineView()
        browser.settings().setDefaultTextEncoding("utf-8")
        browser.load(QUrl("http://localhost:8000/create_wallet"))
        self.setCentralWidget(browser)

        # Создание серого слоя
        self.gray_layer = QWidget(self)
        self.gray_layer.setStyleSheet("background-color: rgba(128, 128, 128, 20);") # 1% прозрачности
        self.gray_layer.setGeometry(0, 0, self.width(), self.height())

        # Переключатель состояния клавиш (блокировка по умолчанию)
        self.keys_enabled = True
        self.toggle_keys()

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_U or (event.text() == 'г' and event.modifiers() == Qt.NoModifier):
            self.toggle_keys()
        elif self.keys_enabled:
            super().keyPressEvent(event)

    def toggle_keys(self):
        self.keys_enabled = not self.keys_enabled

        if self.keys_enabled:
            keyboard.unhook_all()
            self.gray_layer.hide()
        else:
            for key in keyboard.all_modifiers:
                keyboard.block_key(key)
            for i in range(1, 255):
                if keyboard.key_to_scan_codes(i) != keyboard.key_to_scan_codes("u") and keyboard.key_to_scan_codes(i) != 33:
                    keyboard.block_key(i)
            self.gray_layer.show()

    def closeEvent(self, event):
        # Разблокировать клавиши при закрытии приложения
        keyboard.unhook_all()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    kiosk = KioskApp()
    kiosk.show()

    sys.exit(app.exec_())

Обход режима киоска № 2

 Второй киоск выглядел так:

Интерфейс киоска
Интерфейс киоска

Он был доступен в публичном обновлении, о котором мы объявляли в телеграм-чате. Основная функциональность кода (представлен ниже) заключается в создании окна, содержащего кнопку kiosk bypass, и перехвате событий клавиатуры. При нажатии кнопка перемещается случайным образом внутри окна. Если она нажимается более 100 раз, то окно закрывается и перехват клавиатуры отключается.

Бонус для хаброчитателей: сознательно отдаю вам код приложения киосков в полном и неизменном виде, чтобы вы могли изучить, как именно может быть написано приложение киоска на разных языках и как все это работает. Одна из моих идей на будущее — сделать такой киоск, который не сможет взломать ни один хакер. 

#include <Windows.h>
#include <cstdlib>

HHOOK keyboardHook;
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
void DisableKeyboard();

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
void MoveButtonRandomly(HWND hwnd);

HWND buttonHandle; // Глобальная переменная для хранения дескриптора кнопки
int clickCount = 0; // Глобальная переменная для подсчета нажатий

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    // Регистрация класса окна
    WNDCLASS wc = {0};
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.hbrBackground = (HBRUSH)(COLOR_BACKGROUND);
    wc.lpszClassName = TEXT("FullScreenWindow");
    RegisterClass(&wc);

    // Создание окна
    HWND hwnd = CreateWindowEx(
        0,
        TEXT("FullScreenWindow"),
        TEXT(""),
        WS_POPUP,
        0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN),
        NULL, NULL, hInstance, NULL);

    // Создание кнопки "Выход из режима киоска"
    buttonHandle = CreateWindow(
        TEXT("BUTTON"),
        TEXT("kiosk bypass"),
        WS_VISIBLE | WS_CHILD,
        10, 10, 200, 50,
        hwnd, NULL, hInstance, NULL);

    // Установка окна поверх всех остальных окон
    SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);

    // Отображение окна
    ShowWindow(hwnd, SW_SHOW);
    UpdateWindow(hwnd); // Обновление содержимого окна и его отображение

    // Установка перехватчика клавиатуры
    keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, NULL, 0);
    if (keyboardHook == NULL) {
        // Обработка ошибки при установке перехватчика клавиатуры
    }

    // Запуск цикла обработки сообщений
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    // Выход из программы
    DisableKeyboard(); // Отключение клавиатуры перед выходом

    return 0;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
        case WM_COMMAND:
            if (reinterpret_cast<HWND>(lParam) == buttonHandle) // Проверка, что сообщение пришло от кнопки
            {
                MoveButtonRandomly(hwnd);
                clickCount++;
                if (clickCount >= 100)
                {
                    DestroyWindow(hwnd); // Закрытие окна
                    DisableKeyboard(); // Отключение клавиатуры
                }
            }
            break;

        case WM_DESTROY:
            PostQuitMessage(0);
            return 0;
    }

    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

void MoveButtonRandomly(HWND hwnd)
{
    RECT rect;
    GetClientRect(hwnd, &rect);

    // Генерация случайных координат для перемещения кнопки
    int newX = rand() % (rect.right - rect.left - 200) + rect.left;
    int newY = rand() % (rect.bottom - rect.top - 50) + rect.top;

    // Перемещение кнопки на новые координаты
    SetWindowPos(buttonHandle, NULL, newX, newY, 0, 0, SWP_NOSIZE | SWP_NOZORDER);
}

LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    if (nCode >= 0)
    {
        if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN)
        {
            // Предотвращение обработки событий клавиатуры
            return 1;
        }
    }

    return CallNextHookEx(keyboardHook, nCode, wParam, lParam);
}

void DisableKeyboard()
{
    UnhookWindowsHookEx(keyboardHook);
}

Повышение привилегий — менеджер паролей

Вполне реальная ситуация, когда на криптомате может быть найден менеджер паролей. Для банкомата, конечно, спорно, но в нашем деле может быть все, что угодно. На виртуальной машине можно было найти password_manager, написанный на Python с использованием фреймворка Flask. Сам он просто лежал в папке (неактивный), его еще нужно было найти, а потом запустить. Привилегии можно было повысить, попав в аккаунт администратора (в браузере Chrome были сохранены креды). По факту это — готовый менеджер паролей, в который специально заложены уязвимости. Если чуть-чуть его доделать, то можно безопасно хранить пароли онлайн и быть уверенным, что они в безопасности.

Менеджер паролей в виртуальной машине
Менеджер паролей в виртуальной машине

Код прикладывать не буду, иначе эта статья растянется на 200 страниц (можете скачать виртуальную машину и немного пореверсить). Не стал в этот раз собирать исходный код в EXE-файл, чтобы участникам было проще все это исследовать и изучать. В этом менеджере паролей много уязвимостей: от хранения паролей в открытом виде до XSS.

Файл базы данных, открытый через блокнот
Файл базы данных, открытый через блокнот

Интересные репорты от участников

В виртуальную машину было заложено более 15 уязвимостей. Расскажу про несколько крутых репортов, которые прислали нам участники. Например, @s0x28 нашел удаленную читалку файлов. Нарушитель, находившийся в одной сети с криптоматом-банкоматом, мог даже не обходя киоск получать содержимое любого файла — в нашем случае содержимое wallets.txt.

Удаленное чтение файлов
Удаленное чтение файлов

А @nikaleksey успешно эксплуатировал XSS-уязвимость в приложении кошелька. Выглядело это примерно так:

Эксплуатация XSS-уязвимости в приложении кошелька
Эксплуатация XSS-уязвимости в приложении кошелька

Сам метод эксплуатации останется секретом — в этом вам еще предстоит разобраться.

И про CVE забывать не стоит: в виртуальной машине успешно отрабатывали несколько эксплойтов ко всем известным CVE (например, CVE-2020-0796) — об этом почти одновременно сообщили cразу несколько участников.

PS C:\Users\kiosk\AppData\Local\Programs\Python\Python310> .\cve-2020-0796-local.exe
-= CVE-2020-0796 LPE =-
by @danigargu and @dialluvioso_

Successfully connected socket descriptor: 192
Sending SMB negotiation request...
Finished SMB negotiation
Found kernel token at 0xffffb88770dd1060
Sending compressed buffer...
SEP_TOKEN_PRIVILEGES changed
Injecting shellcode in winlogon...
Success! ;)

Тем, кому не удалось побывать на PHDays и понаблюдать за перипетиями состязания вживую, предлагаю посмотреть ролик, который создали участники.

Почувствовали напряженную атмосферу? Именно так взламываются настоящие банкоматы! И это только малая доля от того количества репортов, которые нам прислали!

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

Выводы

Хочу сказать большое спасибо ребятам, которые из года в год участвуют в этом конкурсе и помогают делать его лучше. Также благодарю новых участников. В этом году формат был по-настоящему экспериментальным. Он собрал много отзывов (в основном положительных), но нам есть над чем поработать. В следующем году ждем вас снова на PHDays и на нашем конкурсе банкоматов, который будет еще лучше — v 4.0 на подходе!

Постоянным читателям, конечно, тоже большое спасибо! Есть тут те, кто читает разбор конкурса уже третий год подряд?

Разборы конкурса с предыдущих PHDays:

 Всем хорошего настроения!

Юрий Ряднина

Старший специалист по анализу защищенности банковских систем, Positive Technologies. Он также ведет крутой канал о багхантинге. Советуем подписаться ???????? https://t.me/bughunter_circuit

Комментарии (0)