7. Введение в Windows API
Введение в Windows API
Введение
Windows API предоставляет разработчикам способ взаимодействия их приложений с операционной системой Windows. Например, если приложению необходимо отобразить что-то на экране, изменить файл или запросить информацию из реестра, все эти действия можно выполнить с помощью Windows API. Windows API очень хорошо документирован Microsoft и может быть просмотрен здесь.
Типы данных Windows
В Windows API есть множество типов данных, отличных от хорошо известных (например, int, float). Типы данных документированы и могут быть просмотрены здесь.
Ниже приведены некоторые общие типы данных:
DWORD
- 32-разрядное беззнаковое целое число, как на 32-разрядных, так и на 64-разрядных системах, используется для представления значений от 0 до (2^32 - 1).
DWORD dwVariable = 42;
size_t
- Используется для представления размера объекта. На 32-разрядных системах это 32-разрядное беззнаковое целое число, представляющее значения от 0 до (2^32 - 1). На 64-разрядных системах это 64-разрядное беззнаковое целое число, представляющее значения от 0 до (2^64 - 1).
SIZE_T sVariable = sizeof(int);
VOID
- Указывает на отсутствие конкретного типа данных.
void* pVariable = NULL; // Это то же самое, что и PVOID
PVOID
- 32-разрядный или 4-байтовый указатель любого типа данных на 32-разрядных системах. Альтернативно, 64-разрядный или 8-байтовый указатель любого типа данных на 64-разрядных системах.
PVOID pVariable = &SomeData;
HANDLE
- Значение, которое указывает на определенный объект, управляемый операционной системой (например, файл, процесс, поток).
HANDLE hFile = CreateFile(...);
HMODULE
- Дескриптор модуля. Это базовый адрес модуля в памяти. Примером модуля может быть файл DLL или EXE.
HMODULE hModule = GetModuleHandle(...);
LPCSTR/PCSTR
- Указатель на константную строку из символов Windows 8-битного ANSI (ANSI). Буква "L" означает "long", что происходит от 16-битного периода программирования Windows, но в настоящее время не влияет на тип данных, но соглашение об именах все еще существует. Буква "C" означает "constant" или "read-only variable". Оба этих типа данных эквивалентныconst char*
.
LPCSTR lpcString = "Привет, мир!";
PCSTR pcString = "Привет, мир!";
LPSTR/PSTR
- То же самое, что иLPCSTR
иPCSTR
, единственное отличие в том, чтоLPSTR
иPSTR
не указывают на константную переменную, а вместо этого указывают на читаемую и записываемую строку. Оба этих типа данных эквивалентныchar*
.
LPSTR lpString = "Привет, мир!";
PSTR pString = "Привет, мир!";
LPCWSTR/PCWSTR
- Указатель на константную строку из символов Windows 16-битного юникода (Unicode). Оба этих типа данных эквивалентныconst wchar*
.
LPCWSTR lpwcString = L"Привет, мир!";
PCWSTR pcwString = L"Привет, мир!";
PWSTR/LPWSTR
- То же самое, что иLPCWSTR
иPCWSTR
, единственное отличие в том, что 'PWSTR' и 'LPWSTR' не указывают на константную переменную, а вместо этого указывают на читаемую и записываемую строку. Оба этих типа данных эквивалентныwchar*
.
LPWSTR lpwString = L"Привет, мир!";
PWSTR pwString = L"Привет, мир!";
wchar_t
- То же самое, что иwchar
, используется для представления широких символов.
wchar_t wChar = L'A';
wchar_t* wcString = L"Привет, мир!";
ULONG_PTR
- Представляет беззнаковое целое число, которое имеет такой же размер, как указатель в указанной архитектуре, то есть на 32-битных системахULONG_PTR
будет иметь размер 32 бита, а на 64-битных системах - 64 бита. В течение этого курсаULONG_PTR
будет использоваться для обработки арифметических выражений, содержащих указатели (например, PVOID). Перед выполнением любой арифметической операции указатель будет приводиться к типуULONG_PTR
. Этот подход используется для избегания прямой манипуляции указателями, что может привести к ошибкам компиляции.
PVOID Pointer = malloc(100);
// Pointer = Pointer + 10; // недопустимо
Pointer = (ULONG_PTR)Pointer + 10; // допустимо
Указатели на типы данных
API Windows позволяет разработчику объявлять тип данных напрямую или указатель на тип данных. Это отражено в именах типов данных, где типы данных, начинающиеся с буквы "P", представляют указатели на фактический тип данных, тогда как те, которые не начинаются с буквы "P", представляют сами типы данных.
Это станет полезным позже при работе с API Windows, которые имеют параметры, являющиеся указателями на тип данных. Приведенные ниже примеры показывают, как связаны "P" типы данных и их эквиваленты без указателя.
PHANDLE
- то же самое, что иHANDLE*
.
PSIZE_T
- то же самое, что иSIZE_T*
.
PDWORD
- то же самое, что иDWORD*
.
Функции 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:
5
- ERROR_ACCESS_DENIED
2
- ERROR_FILE_NOT_FOUND
87
- ERROR_INVALID_PARAMETER
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