7. Введение в Windows API

Введение в Windows API

Введение

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

Типы данных Windows

В Windows API есть множество типов данных, отличных от хорошо известных (например, int, float). Типы данных документированы и могут быть просмотрены здесь.

Ниже приведены некоторые общие типы данных:

DWORD dwVariable = 42;
SIZE_T sVariable = sizeof(int);
void* pVariable = NULL; // Это то же самое, что и PVOID
PVOID pVariable = &SomeData;
HANDLE hFile = CreateFile(...);
HMODULE hModule = GetModuleHandle(...);

LPCSTR  lpcString   = "Привет, мир!";
PCSTR   pcString    = "Привет, мир!";

LPSTR   lpString    = "Привет, мир!";
PSTR    pString     = "Привет, мир!";

LPCWSTR     lpwcString  = L"Привет, мир!";
PCWSTR      pcwString   = L"Привет, мир!";

LPWSTR  lpwString = L"Привет, мир!";
PWSTR   pwString  = L"Привет, мир!";
wchar_t     wChar = L'A';
wchar_t*    wcString = L"Привет, мир!";
PVOID Pointer = malloc(100);
// Pointer = Pointer + 10; // недопустимо
Pointer = (ULONG_PTR)Pointer + 10; // допустимо

Указатели на типы данных

API Windows позволяет разработчику объявлять тип данных напрямую или указатель на тип данных. Это отражено в именах типов данных, где типы данных, начинающиеся с буквы "P", представляют указатели на фактический тип данных, тогда как те, которые не начинаются с буквы "P", представляют сами типы данных.

Это станет полезным позже при работе с API Windows, которые имеют параметры, являющиеся указателями на тип данных. Приведенные ниже примеры показывают, как связаны "P" типы данных и их эквиваленты без указателя.

Функции ANSI и Unicode

Большинство функций API Windows имеют две версии, заканчивающиеся либо на "A", либо на "W". Например, есть функции CreateFileA и CreateFileW. Функции, заканчивающиеся на "A", предназначены для работы с ANSI, тогда как функции, заканчивающиеся на "W", представляют Юникод или "Wide".

Основное отличие, которое следует учитывать, заключается в том, что функции ANSI принимают в качестве параметров данные ANSI, а функции Unicode принимают данные в формате Юникод. Например, первый параметр для CreateFileA - это LPCSTR, указатель на константную строку из 8-битных символов Windows ANSI. С другой стороны, первый параметр для CreateFileW - это LPCWSTR, указатель на константную строку из 16-битных символов Юникода.

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

char str1[] = "maldev"; // 7 байтов (maldev + нулевой байт).

wchar str2[] = L"maldev"; // 14 байтов, каждый символ занимает 2 байта (нулевой байт также занимает 2 байта)

Входные и выходные параметры

API Windows имеют входные и выходные параметры. Входной параметр (IN) - это параметр, передаваемый в функцию и используемый для ввода. Выходной параметр (OUT) - это параметр, используемый для возврата значения вызывающему коду. Выходные параметры часто передаются по ссылке через указатели.

Например, фрагмент кода ниже показывает функцию HackTheWorld, которая принимает указатель на целое число и устанавливает его значение равным 123. Это считается выходным параметром, поскольку параметр возвращает значение.


BOOL HackTheWorld(OUT int* num){

    // Устанавливаем значение num равным 123
    *num = 123;

    // Возвращаем логическое значение
    return TRUE;
}

int main(){


    int a = 0;

    // 'HackTheWorld' вернет true
    // 'a' будет содержать значение 123
    HackTheWorld(&a);
}

Имейте в виду, что использование ключевых слов IN или OUT служит для упрощения понимания ожидаемых параметров функции и того, что она делает с этими параметрами. Однако стоит отметить, что исключение этих ключевых слов не влияет на то, считается ли параметр выходным или входным.

Пример Windows API

Теперь, когда основы Windows API изложены, в этом разделе рассмотрим использование функции CreateFileW.

Найти справочник API

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

Анализ типа возврата и параметров

Следующим шагом будет просмотр параметров функции вместе с типом данных, возвращаемым функцией. В документации указано: "Если функция выполнена успешно, возвращается открытый дескриптор указанного файла, устройства, именованного канала или почтового слота". Следовательно, CreateFileW возвращает тип данных HANDLE для созданного объекта.

Кроме того, обратите внимание, что параметры функции являются входными (IN) параметрами. Это означает, что функция не возвращает данные из параметров, так как все они являются входными параметрами. Следует помнить, что ключевые слова в квадратных скобках, такие как IN, OUT и OPTIONAL, предназначены исключительно для справки разработчиков и не имеют фактического значения.

HANDLE CreateFileW(
  [in]           LPCWSTR               lpFileName,
  [in]           DWORD                 dwDesiredAccess,
  [in]           DWORD                 dwShareMode,
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  [in]           DWORD                 dwCreationDisposition,
  [in]           DWORD                 dwFlagsAndAttributes,
  [in, optional] HANDLE                hTemplateFile
);

Использование функции

Приведенный ниже пример кода демонстрирует использование функции CreateFileW. Он создает текстовый файл с именем maldev.txt на рабочем столе текущего пользователя.

// Для хранения дескриптора файла используется тип данных Handle
// INVALID_HANDLE_VALUE - просто инициализирует переменную
Handle hFile = INVALID_HANDLE_VALUE;

// Полный путь к создаваемому файлу.
// Двойной обратный слеш необходим для экранирования символа одиночного обратного слеша в C
LPCWSTR filePath = L"C:\\Users\\maldevacademy\\Desktop\\maldev.txt";

// Вызов CreateFileW с указанием пути к файлу
// Дополнительные параметры берутся непосредственно из документации
hFile = CreateFileW(filePath, GENERIC_ALL, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

// Если CreateFileW завершилась ошибкой, она возвращает INVALID_HANDLE_VALUE
// GetLastError() - другая функция Windows API, возвращающая код ошибки предыдущей выполненной функции WinAPI
if (hFile == INVALID_HANDLE_VALUE){
    printf("[-] Функция API CreateFileW завершилась ошибкой : %d\n", GetLastError());
    return -1;
}

Windows API Debugging Errors

When functions fail they often return a non-verbose error. For example, if CreateFileW fails it returns INVALID_HANDLE_VALUE which indicates that a file could not be created. To gain more insight as to why the file couldn't be created, the error code must be retrieved using the GetLastError function.

Once the code is retrieved, it needs to be looked up in Windows's System Error Codes List. Some common error codes are translated below:

Windows Native API Debugging Errors

Recall from the Windows Architecture module, NTAPIs are mostly exported from ntdll.dll. Unlike Windows APIs, these functions cannot have their error code fetched via GetLastError. Instead, they return the error code directly which is represented by the NTSTATUS data type.

NTSTATUS is used to represent the status of a system call or function and is defined as a 32-bit unsigned integer value. A successful system call will return the value STATUS_SUCCESS, which is 0. On the other hand, if the call failed it will return a non-zero value, to further investigate the cause of the problem, one must check Microsoft's documentation on NTSTATUS values.

The code snippet below shows how error checking for system calls is done.

NTSTATUS STATUS = NativeSyscallExample(...);
if (STATUS != STATUS_SUCCESS){
    // printing the error in unsigned integer hexadecimal format
    printf("[!] NativeSyscallExample Failed With Status : 0x%0.8X \n", STATUS);
}

// NativeSyscallExample succeeded

NT_SUCCESS Macro

Another way to check the return value of NTAPIs is through the NT_SUCCESS macro shown here. The macro returns TRUE if the function succeeded, and FALSE it fails.

#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)

Below, is an example of using this macro

NTSTATUS STATUS = NativeSyscallExample(...);
if (!NT_SUCCESS(STATUS)){
    // printing the error in unsigned integer hexadecimal format
    printf("[!] NativeSyscallExample Failed With Status : 0x%0.8X \n", STATUS);
}

// NativeSyscallExample succeeded