mirror of
https://github.com/donnaskiez/ac.git
synced 2024-11-21 22:24:08 +01:00
improve system module val performance
This commit is contained in:
parent
7b15c10ca1
commit
d6bc82f6e9
8 changed files with 675 additions and 258 deletions
|
@ -24,6 +24,7 @@ open source anti cheat (lol) which I made for fun.
|
|||
# planned features
|
||||
|
||||
- Heartbeat
|
||||
- optimise stuff
|
||||
- ntoskrnl integrity checks, or atleast a small subset of the kernel encompasing critical functions
|
||||
- spoofed stack identifier
|
||||
- process module inline hook detection (this would include checking whether the hook is valid, as many legimate programs hook user mode modules such as discord, nvidia overlay etc.)
|
||||
|
|
|
@ -105,18 +105,19 @@ DrvLoadInitialiseDriverConfig(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_ST
|
|||
|
||||
typedef struct _DRIVER_CONFIG
|
||||
{
|
||||
UNICODE_STRING unicode_driver_name;
|
||||
ANSI_STRING ansi_driver_name;
|
||||
UNICODE_STRING device_name;
|
||||
UNICODE_STRING device_symbolic_link;
|
||||
UNICODE_STRING driver_path;
|
||||
UNICODE_STRING registry_path;
|
||||
SYSTEM_INFORMATION system_information;
|
||||
PVOID apc_contexts[MAXIMUM_APC_CONTEXTS];
|
||||
PDRIVER_OBJECT driver_object;
|
||||
PDEVICE_OBJECT device_object;
|
||||
volatile BOOLEAN unload_in_progress;
|
||||
KGUARDED_MUTEX lock;
|
||||
UNICODE_STRING unicode_driver_name;
|
||||
ANSI_STRING ansi_driver_name;
|
||||
UNICODE_STRING device_name;
|
||||
UNICODE_STRING device_symbolic_link;
|
||||
UNICODE_STRING driver_path;
|
||||
UNICODE_STRING registry_path;
|
||||
SYSTEM_INFORMATION system_information;
|
||||
PVOID apc_contexts[MAXIMUM_APC_CONTEXTS];
|
||||
PDRIVER_OBJECT driver_object;
|
||||
PDEVICE_OBJECT device_object;
|
||||
volatile BOOLEAN unload_in_progress;
|
||||
KGUARDED_MUTEX lock;
|
||||
SYS_MODULE_VAL_CONTEXT sys_val_context;
|
||||
|
||||
} DRIVER_CONFIG, *PDRIVER_CONFIG;
|
||||
|
||||
|
@ -431,6 +432,11 @@ end:
|
|||
return status;
|
||||
}
|
||||
|
||||
GetSystemModuleValidationContext(_Out_ PSYS_MODULE_VAL_CONTEXT* Context)
|
||||
{
|
||||
*Context = &driver_config.sys_val_context;
|
||||
}
|
||||
|
||||
_IRQL_requires_max_(APC_LEVEL)
|
||||
_Acquires_lock_(_Lock_kind_mutex_)
|
||||
_Releases_lock_(_Lock_kind_mutex_)
|
||||
|
@ -509,6 +515,12 @@ GetDriverName(_Out_ LPCSTR* DriverName)
|
|||
KeReleaseGuardedMutex(&driver_config.lock);
|
||||
}
|
||||
|
||||
PDEVICE_OBJECT
|
||||
GetDriverDeviceObject()
|
||||
{
|
||||
return driver_config.device_object;
|
||||
}
|
||||
|
||||
_IRQL_requires_max_(APC_LEVEL)
|
||||
_Acquires_lock_(_Lock_kind_mutex_)
|
||||
_Releases_lock_(_Lock_kind_mutex_)
|
||||
|
@ -1303,6 +1315,7 @@ DrvLoadInitialiseDriverConfig(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_ST
|
|||
|
||||
driver_config.unload_in_progress = FALSE;
|
||||
driver_config.system_information.virtualised_environment = FALSE;
|
||||
driver_config.sys_val_context.active = FALSE;
|
||||
|
||||
RtlInitUnicodeString(&driver_config.device_name, L"\\Device\\DonnaAC");
|
||||
RtlInitUnicodeString(&driver_config.device_symbolic_link, L"\\??\\DonnaAC");
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include "common.h"
|
||||
#include "queue.h"
|
||||
#include "modules.h"
|
||||
#include "integrity.h"
|
||||
|
||||
#define DRIVER_PATH_MAX_LENGTH 512
|
||||
#define MOTHERBOARD_SERIAL_CODE_LENGTH 64
|
||||
|
@ -185,4 +186,9 @@ _Releases_lock_(_Lock_kind_mutex_)
|
|||
VOID
|
||||
GetDriverSymbolicLink(_Out_ PUNICODE_STRING DeviceSymbolicLink);
|
||||
|
||||
PDEVICE_OBJECT
|
||||
GetDriverDeviceObject();
|
||||
|
||||
GetSystemModuleValidationContext(_Out_ PSYS_MODULE_VAL_CONTEXT* Context);
|
||||
|
||||
#endif
|
|
@ -1562,246 +1562,578 @@ FindWinLogonProcess(_In_ PPROCESS_LIST_ENTRY Entry, _In_opt_ PVOID Context)
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* todo: for some reason this works flawlessy on my vm, but fails on my real pc...
|
||||
*/
|
||||
NTSTATUS
|
||||
ValidateSystemModules()
|
||||
{
|
||||
NTSTATUS status = STATUS_UNSUCCESSFUL;
|
||||
BOOLEAN bstatus = FALSE;
|
||||
ANSI_STRING ansi_string = {0};
|
||||
UNICODE_STRING path = {0};
|
||||
ULONG section_size = 0;
|
||||
HANDLE section_handle = NULL;
|
||||
PVOID section = NULL;
|
||||
PVOID disk_buffer = NULL;
|
||||
ULONG disk_buffer_size = 0;
|
||||
PVOID disk_hash = NULL;
|
||||
ULONG disk_hash_size = 0;
|
||||
UINT64 disk_text_base = 0;
|
||||
UINT64 memory_text_base = 0;
|
||||
ULONG memory_text_size = 0;
|
||||
PVOID memory_hash = NULL;
|
||||
ULONG memory_hash_size = 0;
|
||||
PVOID memory_buffer = NULL;
|
||||
ULONG memory_buffer_size = 0;
|
||||
ULONG result = 0;
|
||||
PEPROCESS process = NULL;
|
||||
KAPC_STATE apc_state = {0};
|
||||
SYSTEM_MODULES modules = {0};
|
||||
PRTL_MODULE_EXTENDED_INFO module_info = NULL;
|
||||
PIMAGE_SECTION_HEADER disk_text_header = NULL;
|
||||
PIMAGE_SECTION_HEADER memory_text_header = NULL;
|
||||
//NTSTATUS
|
||||
//ValidateSystemModules()
|
||||
//{
|
||||
// NTSTATUS status = STATUS_UNSUCCESSFUL;
|
||||
// BOOLEAN bstatus = FALSE;
|
||||
// ANSI_STRING ansi_string = {0};
|
||||
// UNICODE_STRING path = {0};
|
||||
// ULONG section_size = 0;
|
||||
// HANDLE section_handle = NULL;
|
||||
// PVOID section = NULL;
|
||||
// PVOID disk_buffer = NULL;
|
||||
// ULONG disk_buffer_size = 0;
|
||||
// PVOID disk_hash = NULL;
|
||||
// ULONG disk_hash_size = 0;
|
||||
// UINT64 disk_text_base = 0;
|
||||
// UINT64 memory_text_base = 0;
|
||||
// ULONG memory_text_size = 0;
|
||||
// PVOID memory_hash = NULL;
|
||||
// ULONG memory_hash_size = 0;
|
||||
// PVOID memory_buffer = NULL;
|
||||
// ULONG memory_buffer_size = 0;
|
||||
// ULONG result = 0;
|
||||
// PEPROCESS process = NULL;
|
||||
// KAPC_STATE apc_state = {0};
|
||||
// SYSTEM_MODULES modules = {0};
|
||||
// PRTL_MODULE_EXTENDED_INFO module_info = NULL;
|
||||
// PIMAGE_SECTION_HEADER disk_text_header = NULL;
|
||||
// PIMAGE_SECTION_HEADER memory_text_header = NULL;
|
||||
//
|
||||
// DEBUG_VERBOSE("Beginning to validate system modules.");
|
||||
//
|
||||
// status = GetSystemModuleInformation(&modules);
|
||||
//
|
||||
// if (!NT_SUCCESS(status))
|
||||
// {
|
||||
// DEBUG_ERROR("GetSystemModuleInformation failed with status %s", status);
|
||||
// return status;
|
||||
// }
|
||||
//
|
||||
// /*
|
||||
// * Since ntoskrnl itself is a process, we skip it here - we will validate it elsewhere.
|
||||
// */
|
||||
// for (INT index = 1; index < modules.module_count; index++)
|
||||
// {
|
||||
// module_info = (PRTL_MODULE_EXTENDED_INFO)((UINT64)modules.address +
|
||||
// index * sizeof(RTL_MODULE_EXTENDED_INFO));
|
||||
//
|
||||
// RtlInitAnsiString(&ansi_string, module_info->FullPathName);
|
||||
//
|
||||
// if (!ansi_string.Buffer)
|
||||
// {
|
||||
// DEBUG_ERROR("RtlInitAnsiString failed with status %x", status);
|
||||
// ansi_string.Buffer = NULL;
|
||||
// ansi_string.Length = 0;
|
||||
// ansi_string.MaximumLength = 0;
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// status = RtlAnsiStringToUnicodeString(&path, &ansi_string, TRUE);
|
||||
//
|
||||
// if (!NT_SUCCESS(status))
|
||||
// {
|
||||
// DEBUG_ERROR("RtlAnsiStringToUnicodeString failed with status %x", status);
|
||||
// path.Buffer = NULL;
|
||||
// goto free_iteration;
|
||||
// }
|
||||
//
|
||||
// status = MapDiskImageIntoVirtualAddressSpace(
|
||||
// §ion_handle, §ion, &path, §ion_size);
|
||||
//
|
||||
// if (!NT_SUCCESS(status))
|
||||
// {
|
||||
// DEBUG_ERROR("MapDiskImageIntoVirtualAddressSpace failed with status %x",
|
||||
// status);
|
||||
// goto free_iteration;
|
||||
// }
|
||||
//
|
||||
// status = StoreModuleExecutableRegionsInBuffer(
|
||||
// &disk_buffer, section, section_size, &disk_buffer_size);
|
||||
//
|
||||
// if (!NT_SUCCESS(status))
|
||||
// {
|
||||
// DEBUG_ERROR("StoreModuleExecutableRegionsInBuffer failed with status %x",
|
||||
// status);
|
||||
// goto free_iteration;
|
||||
// }
|
||||
//
|
||||
// /*
|
||||
// * For win32k and related modules, because they are 32bit for us to read the memory
|
||||
// * we need to attach to a 32 bit process. A simple check is that the 32 bit image
|
||||
// * base wont be a valid address, while this is hacky it works. Then we simply attach
|
||||
// * to a 32 bit address space, in our case winlogon, which will allow us to perform
|
||||
// * the copy.
|
||||
// */
|
||||
// if (!MmIsAddressValid(module_info->ImageBase))
|
||||
// {
|
||||
// DEBUG_VERBOSE(
|
||||
// "Win32k related module found, acquiring 32 bit address space...");
|
||||
//
|
||||
// EnumerateProcessListWithCallbackRoutine(FindWinLogonProcess, &process);
|
||||
//
|
||||
// if (!process)
|
||||
// goto free_iteration;
|
||||
//
|
||||
// KeStackAttachProcess(process, &apc_state);
|
||||
//
|
||||
// status = StoreModuleExecutableRegionsInBuffer(&memory_buffer,
|
||||
// module_info->ImageBase,
|
||||
// module_info->ImageSize,
|
||||
// &memory_buffer_size);
|
||||
//
|
||||
// KeUnstackDetachProcess(&apc_state);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// status = StoreModuleExecutableRegionsInBuffer(&memory_buffer,
|
||||
// module_info->ImageBase,
|
||||
// module_info->ImageSize,
|
||||
// &memory_buffer_size);
|
||||
// }
|
||||
//
|
||||
// if (!NT_SUCCESS(status))
|
||||
// {
|
||||
// DEBUG_ERROR("StoreModuleExecutableRegionsInbuffer 2 failed with status %x",
|
||||
// status);
|
||||
// goto free_iteration;
|
||||
// }
|
||||
//
|
||||
// disk_text_base = (UINT64)disk_buffer + sizeof(INTEGRITY_CHECK_HEADER) +
|
||||
// sizeof(IMAGE_SECTION_HEADER);
|
||||
//
|
||||
// memory_text_base = (UINT64)((UINT64)memory_buffer + sizeof(INTEGRITY_CHECK_HEADER) +
|
||||
// sizeof(IMAGE_SECTION_HEADER));
|
||||
//
|
||||
// disk_text_header =
|
||||
// (PIMAGE_SECTION_HEADER)((UINT64)disk_buffer + sizeof(INTEGRITY_CHECK_HEADER));
|
||||
//
|
||||
// memory_text_header =
|
||||
// (PIMAGE_SECTION_HEADER)((UINT64)memory_buffer + sizeof(INTEGRITY_CHECK_HEADER));
|
||||
//
|
||||
// if (!disk_text_base || !memory_text_base || !disk_buffer || !memory_buffer ||
|
||||
// !MmIsAddressValid(disk_buffer) || !MmIsAddressValid(memory_buffer))
|
||||
// {
|
||||
// DEBUG_ERROR("Buffer(s) are null. An error has occured.");
|
||||
// goto free_iteration;
|
||||
// }
|
||||
//
|
||||
// if (disk_text_header->SizeOfRawData != memory_text_header->SizeOfRawData)
|
||||
// {
|
||||
// DEBUG_WARNING("Executable section sizes differ between images.");
|
||||
// goto free_iteration;
|
||||
// }
|
||||
//
|
||||
// status = ComputeHashOfBuffer(
|
||||
// disk_text_base, disk_text_header->SizeOfRawData, &disk_hash, &disk_hash_size);
|
||||
//
|
||||
// if (!NT_SUCCESS(status))
|
||||
// {
|
||||
// DEBUG_ERROR("ComputeHashOfBuffer failed with status %s", status);
|
||||
// goto free_iteration;
|
||||
// }
|
||||
//
|
||||
// status = ComputeHashOfBuffer(memory_text_base,
|
||||
// memory_text_header->SizeOfRawData,
|
||||
// &memory_hash,
|
||||
// &memory_hash_size);
|
||||
//
|
||||
// if (!NT_SUCCESS(status))
|
||||
// {
|
||||
// DEBUG_ERROR("ComputeHashOfBuffer failed with status %x", status);
|
||||
// goto free_iteration;
|
||||
// }
|
||||
//
|
||||
// if (!MmIsAddressValid(memory_hash) || !MmIsAddressValid(disk_hash))
|
||||
// goto free_iteration;
|
||||
//
|
||||
// result = RtlCompareMemory(memory_hash, disk_hash, memory_hash_size);
|
||||
//
|
||||
// if (result = memory_text_header->SizeOfRawData)
|
||||
// DEBUG_VERBOSE("Module executable sections are valid for the module: %s",
|
||||
// module_info->FullPathName);
|
||||
// else
|
||||
// DEBUG_WARNING("Module regions are not valid for module: %s",
|
||||
// module_info->FullPathName);
|
||||
//
|
||||
// free_iteration:
|
||||
//
|
||||
// if (memory_buffer)
|
||||
// ExFreePoolWithTag(memory_buffer, POOL_TAG_INTEGRITY);
|
||||
//
|
||||
// if (memory_hash)
|
||||
// ExFreePoolWithTag(memory_hash, POOL_TAG_INTEGRITY);
|
||||
//
|
||||
// if (disk_buffer)
|
||||
// ExFreePoolWithTag(disk_buffer, POOL_TAG_INTEGRITY);
|
||||
//
|
||||
// if (disk_hash)
|
||||
// ExFreePoolWithTag(disk_hash, POOL_TAG_INTEGRITY);
|
||||
//
|
||||
// free_section:
|
||||
//
|
||||
// if (section_handle)
|
||||
// ZwClose(section_handle);
|
||||
//
|
||||
// if (section)
|
||||
// ZwUnmapViewOfSection(ZwCurrentProcess(), section);
|
||||
//
|
||||
// /*
|
||||
// * Its times like this where you see why allocating all local variables at the
|
||||
// * beginning of the function may not be so ideal...
|
||||
// */
|
||||
// if (path.Buffer)
|
||||
// {
|
||||
// RtlFreeUnicodeString(&path);
|
||||
// path.Buffer = NULL;
|
||||
// path.Length = 0;
|
||||
// path.MaximumLength = 0;
|
||||
// }
|
||||
//
|
||||
// ansi_string.Buffer = NULL;
|
||||
// ansi_string.Length = 0;
|
||||
// ansi_string.MaximumLength = 0;
|
||||
//
|
||||
// section_handle = NULL;
|
||||
// section = NULL;
|
||||
// memory_buffer = NULL;
|
||||
// memory_hash = NULL;
|
||||
// disk_buffer = NULL;
|
||||
// disk_hash = NULL;
|
||||
// }
|
||||
//
|
||||
// if (modules.address)
|
||||
// ExFreePoolWithTag(modules.address, SYSTEM_MODULES_POOL);
|
||||
//
|
||||
// return status;
|
||||
//}
|
||||
|
||||
DEBUG_VERBOSE("Beginning to validate system modules.");
|
||||
STATIC
|
||||
VOID
|
||||
ValidateSystemModule(_In_ PRTL_MODULE_EXTENDED_INFO Module)
|
||||
{
|
||||
NTSTATUS status = STATUS_UNSUCCESSFUL;
|
||||
BOOLEAN bstatus = FALSE;
|
||||
ANSI_STRING ansi_string = {0};
|
||||
UNICODE_STRING path = {0};
|
||||
ULONG section_size = 0;
|
||||
HANDLE section_handle = NULL;
|
||||
PVOID section = NULL;
|
||||
PVOID disk_buffer = NULL;
|
||||
ULONG disk_buffer_size = 0;
|
||||
PVOID disk_hash = NULL;
|
||||
ULONG disk_hash_size = 0;
|
||||
UINT64 disk_text_base = 0;
|
||||
UINT64 memory_text_base = 0;
|
||||
ULONG memory_text_size = 0;
|
||||
PVOID memory_hash = NULL;
|
||||
ULONG memory_hash_size = 0;
|
||||
PVOID memory_buffer = NULL;
|
||||
ULONG memory_buffer_size = 0;
|
||||
ULONG result = 0;
|
||||
PEPROCESS process = NULL;
|
||||
KAPC_STATE apc_state = {0};
|
||||
PIMAGE_SECTION_HEADER disk_text_header = NULL;
|
||||
PIMAGE_SECTION_HEADER memory_text_header = NULL;
|
||||
|
||||
RtlInitAnsiString(&ansi_string, Module->FullPathName);
|
||||
|
||||
if (!ansi_string.Buffer)
|
||||
{
|
||||
DEBUG_ERROR("RtlInitAnsiString failed with status %x", status);
|
||||
return;
|
||||
}
|
||||
|
||||
status = RtlAnsiStringToUnicodeString(&path, &ansi_string, TRUE);
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
{
|
||||
DEBUG_ERROR("RtlAnsiStringToUnicodeString failed with status %x", status);
|
||||
goto end;
|
||||
}
|
||||
|
||||
status =
|
||||
MapDiskImageIntoVirtualAddressSpace(§ion_handle, §ion, &path, §ion_size);
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
{
|
||||
DEBUG_ERROR("MapDiskImageIntoVirtualAddressSpace failed with status %x", status);
|
||||
goto end;
|
||||
}
|
||||
|
||||
status = StoreModuleExecutableRegionsInBuffer(
|
||||
&disk_buffer, section, section_size, &disk_buffer_size);
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
{
|
||||
DEBUG_ERROR("StoreModuleExecutableRegionsInBuffer failed with status %x", status);
|
||||
goto end;
|
||||
}
|
||||
|
||||
/*
|
||||
* For win32k and related modules, because they are 32bit for us to read the
|
||||
* memory we need to attach to a 32 bit process. A simple check is that the
|
||||
* 32 bit image base wont be a valid address, while this is hacky it works.
|
||||
* Then we simply attach to a 32 bit address space, in our case winlogon,
|
||||
* which will allow us to perform the copy.
|
||||
*/
|
||||
if (!MmIsAddressValid(Module->ImageBase))
|
||||
{
|
||||
DEBUG_VERBOSE("Win32k related module found, acquiring 32 bit address space...");
|
||||
|
||||
EnumerateProcessListWithCallbackRoutine(FindWinLogonProcess, &process);
|
||||
|
||||
if (!process)
|
||||
goto end;
|
||||
|
||||
KeStackAttachProcess(process, &apc_state);
|
||||
|
||||
status = StoreModuleExecutableRegionsInBuffer(
|
||||
&memory_buffer, Module->ImageBase, Module->ImageSize, &memory_buffer_size);
|
||||
|
||||
KeUnstackDetachProcess(&apc_state);
|
||||
}
|
||||
else
|
||||
{
|
||||
status = StoreModuleExecutableRegionsInBuffer(
|
||||
&memory_buffer, Module->ImageBase, Module->ImageSize, &memory_buffer_size);
|
||||
}
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
{
|
||||
DEBUG_ERROR("StoreModuleExecutableRegionsInbuffer 2 failed with status %x", status);
|
||||
goto end;
|
||||
}
|
||||
|
||||
disk_text_base =
|
||||
(UINT64)disk_buffer + sizeof(INTEGRITY_CHECK_HEADER) + sizeof(IMAGE_SECTION_HEADER);
|
||||
|
||||
memory_text_base = (UINT64)((UINT64)memory_buffer + sizeof(INTEGRITY_CHECK_HEADER) +
|
||||
sizeof(IMAGE_SECTION_HEADER));
|
||||
|
||||
disk_text_header =
|
||||
(PIMAGE_SECTION_HEADER)((UINT64)disk_buffer + sizeof(INTEGRITY_CHECK_HEADER));
|
||||
|
||||
memory_text_header =
|
||||
(PIMAGE_SECTION_HEADER)((UINT64)memory_buffer + sizeof(INTEGRITY_CHECK_HEADER));
|
||||
|
||||
if (!disk_text_base || !memory_text_base || !disk_buffer || !memory_buffer ||
|
||||
!MmIsAddressValid(disk_buffer) || !MmIsAddressValid(memory_buffer))
|
||||
{
|
||||
DEBUG_ERROR("Buffer(s) are null. An error has occured.");
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (disk_text_header->SizeOfRawData != memory_text_header->SizeOfRawData)
|
||||
{
|
||||
DEBUG_WARNING("Executable section sizes differ between images.");
|
||||
goto end;
|
||||
}
|
||||
|
||||
status = ComputeHashOfBuffer(
|
||||
disk_text_base, disk_text_header->SizeOfRawData, &disk_hash, &disk_hash_size);
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
{
|
||||
DEBUG_ERROR("ComputeHashOfBuffer failed with status %s", status);
|
||||
goto end;
|
||||
}
|
||||
|
||||
status = ComputeHashOfBuffer(
|
||||
memory_text_base, memory_text_header->SizeOfRawData, &memory_hash, &memory_hash_size);
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
{
|
||||
DEBUG_ERROR("ComputeHashOfBuffer failed with status %x", status);
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (!MmIsAddressValid(memory_hash) || !MmIsAddressValid(disk_hash))
|
||||
goto end;
|
||||
|
||||
result = RtlCompareMemory(memory_hash, disk_hash, memory_hash_size);
|
||||
|
||||
if (result = memory_text_header->SizeOfRawData)
|
||||
DEBUG_VERBOSE(
|
||||
"Thread: %lx: Module executable sections are valid for the module: %s",
|
||||
PsGetCurrentThreadId(),
|
||||
Module->FullPathName);
|
||||
else
|
||||
DEBUG_WARNING("Thread: %lx: Module regions are not valid for module: %s",
|
||||
PsGetCurrentThreadId(),
|
||||
Module->FullPathName);
|
||||
|
||||
end:
|
||||
|
||||
if (memory_buffer)
|
||||
ExFreePoolWithTag(memory_buffer, POOL_TAG_INTEGRITY);
|
||||
|
||||
if (memory_hash)
|
||||
ExFreePoolWithTag(memory_hash, POOL_TAG_INTEGRITY);
|
||||
|
||||
if (disk_buffer)
|
||||
ExFreePoolWithTag(disk_buffer, POOL_TAG_INTEGRITY);
|
||||
|
||||
if (disk_hash)
|
||||
ExFreePoolWithTag(disk_hash, POOL_TAG_INTEGRITY);
|
||||
|
||||
if (section_handle)
|
||||
ZwClose(section_handle);
|
||||
|
||||
if (section)
|
||||
ZwUnmapViewOfSection(ZwCurrentProcess(), section);
|
||||
|
||||
if (path.Buffer)
|
||||
RtlFreeUnicodeString(&path);
|
||||
}
|
||||
|
||||
STATIC
|
||||
VOID
|
||||
SystemModuleVerificationDispatchFunction(_In_ PDEVICE_OBJECT DeviceObject,
|
||||
_In_ PSYS_MODULE_VAL_CONTEXT Context)
|
||||
{
|
||||
InterlockedIncrement(&Context->active_thread_count);
|
||||
|
||||
LONG count = InterlockedExchange(&Context->current_count, Context->current_count);
|
||||
LONG max = count + Context->block_size;
|
||||
|
||||
for (; count < max && count < Context->total_count; count++)
|
||||
{
|
||||
if (!InterlockedCompareExchange(
|
||||
&Context->dispatcher_info[count].validated, TRUE, FALSE))
|
||||
{
|
||||
ValidateSystemModule(&Context->module_info[count]);
|
||||
}
|
||||
}
|
||||
|
||||
if (count == Context->total_count)
|
||||
InterlockedExchange(&Context->complete, TRUE);
|
||||
|
||||
InterlockedExchange(&Context->current_count, count);
|
||||
InterlockedDecrement(&Context->active_thread_count);
|
||||
}
|
||||
|
||||
#define VALIDATION_BLOCK_SIZE 25
|
||||
|
||||
/*
|
||||
* Multithreaded delayed priority work items improve 1% lows by 25% and reduces average PC latency
|
||||
* by 10% compared to traditional multithreading. This is important as having high average fps but
|
||||
* low 1% lows just leads to stuttery gameplay which in competitive multiplayer games is simply not
|
||||
* alright. Overall still room for improvement but from a statistical and feel which the gameplay is
|
||||
* much smoother (tested in cs2).
|
||||
*
|
||||
* A potential idea for further improvement is finding the cores with the least cpu usages and
|
||||
* setting the worker threads affinity accordingly.
|
||||
*/
|
||||
STATIC
|
||||
NTSTATUS
|
||||
InitialiseSystemModuleVerificationContext(PSYS_MODULE_VAL_CONTEXT Context)
|
||||
{
|
||||
NTSTATUS status = STATUS_UNSUCCESSFUL;
|
||||
SYSTEM_MODULES modules = {0};
|
||||
PMODULE_DISPATCHER_HEADER dispatcher_array = NULL;
|
||||
|
||||
status = GetSystemModuleInformation(&modules);
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
{
|
||||
DEBUG_ERROR("GetSystemModuleInformation failed with status %s", status);
|
||||
DEBUG_ERROR("GetSystemModuleInformation failed with status %x", status);
|
||||
return status;
|
||||
}
|
||||
|
||||
/*
|
||||
* Since ntoskrnl itself is a process, we skip it here - we will validate it elsewhere.
|
||||
*/
|
||||
for (INT index = 1; index < modules.module_count; index++)
|
||||
dispatcher_array = ExAllocatePool2(POOL_FLAG_NON_PAGED,
|
||||
modules.module_count * sizeof(MODULE_DISPATCHER_HEADER),
|
||||
POOL_TAG_INTEGRITY);
|
||||
if (!dispatcher_array)
|
||||
{
|
||||
module_info = (PRTL_MODULE_EXTENDED_INFO)((UINT64)modules.address +
|
||||
index * sizeof(RTL_MODULE_EXTENDED_INFO));
|
||||
|
||||
RtlInitAnsiString(&ansi_string, module_info->FullPathName);
|
||||
|
||||
if (!ansi_string.Buffer)
|
||||
{
|
||||
DEBUG_ERROR("RtlInitAnsiString failed with status %x", status);
|
||||
ansi_string.Buffer = NULL;
|
||||
ansi_string.Length = 0;
|
||||
ansi_string.MaximumLength = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
status = RtlAnsiStringToUnicodeString(&path, &ansi_string, TRUE);
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
{
|
||||
DEBUG_ERROR("RtlAnsiStringToUnicodeString failed with status %x", status);
|
||||
path.Buffer = NULL;
|
||||
goto free_iteration;
|
||||
}
|
||||
|
||||
status = MapDiskImageIntoVirtualAddressSpace(
|
||||
§ion_handle, §ion, &path, §ion_size);
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
{
|
||||
DEBUG_ERROR("MapDiskImageIntoVirtualAddressSpace failed with status %x",
|
||||
status);
|
||||
goto free_iteration;
|
||||
}
|
||||
|
||||
status = StoreModuleExecutableRegionsInBuffer(
|
||||
&disk_buffer, section, section_size, &disk_buffer_size);
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
{
|
||||
DEBUG_ERROR("StoreModuleExecutableRegionsInBuffer failed with status %x",
|
||||
status);
|
||||
goto free_iteration;
|
||||
}
|
||||
|
||||
/*
|
||||
* For win32k and related modules, because they are 32bit for us to read the memory
|
||||
* we need to attach to a 32 bit process. A simple check is that the 32 bit image
|
||||
* base wont be a valid address, while this is hacky it works. Then we simply attach
|
||||
* to a 32 bit address space, in our case winlogon, which will allow us to perform
|
||||
* the copy.
|
||||
*/
|
||||
if (!MmIsAddressValid(module_info->ImageBase))
|
||||
{
|
||||
DEBUG_VERBOSE(
|
||||
"Win32k related module found, acquiring 32 bit address space...");
|
||||
|
||||
EnumerateProcessListWithCallbackRoutine(FindWinLogonProcess, &process);
|
||||
|
||||
if (!process)
|
||||
goto free_iteration;
|
||||
|
||||
KeStackAttachProcess(process, &apc_state);
|
||||
|
||||
status = StoreModuleExecutableRegionsInBuffer(&memory_buffer,
|
||||
module_info->ImageBase,
|
||||
module_info->ImageSize,
|
||||
&memory_buffer_size);
|
||||
|
||||
KeUnstackDetachProcess(&apc_state);
|
||||
}
|
||||
else
|
||||
{
|
||||
status = StoreModuleExecutableRegionsInBuffer(&memory_buffer,
|
||||
module_info->ImageBase,
|
||||
module_info->ImageSize,
|
||||
&memory_buffer_size);
|
||||
}
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
{
|
||||
DEBUG_ERROR("StoreModuleExecutableRegionsInbuffer 2 failed with status %x",
|
||||
status);
|
||||
goto free_iteration;
|
||||
}
|
||||
|
||||
disk_text_base = (UINT64)disk_buffer + sizeof(INTEGRITY_CHECK_HEADER) +
|
||||
sizeof(IMAGE_SECTION_HEADER);
|
||||
|
||||
memory_text_base = (UINT64)((UINT64)memory_buffer + sizeof(INTEGRITY_CHECK_HEADER) +
|
||||
sizeof(IMAGE_SECTION_HEADER));
|
||||
|
||||
disk_text_header =
|
||||
(PIMAGE_SECTION_HEADER)((UINT64)disk_buffer + sizeof(INTEGRITY_CHECK_HEADER));
|
||||
|
||||
memory_text_header =
|
||||
(PIMAGE_SECTION_HEADER)((UINT64)memory_buffer + sizeof(INTEGRITY_CHECK_HEADER));
|
||||
|
||||
if (!disk_text_base || !memory_text_base || !disk_buffer || !memory_buffer ||
|
||||
!MmIsAddressValid(disk_buffer) || !MmIsAddressValid(memory_buffer))
|
||||
{
|
||||
DEBUG_ERROR("Buffer(s) are null. An error has occured.");
|
||||
goto free_section;
|
||||
}
|
||||
|
||||
if (disk_text_header->SizeOfRawData != memory_text_header->SizeOfRawData)
|
||||
{
|
||||
DEBUG_WARNING("Executable section sizes differ between images.");
|
||||
goto free_iteration;
|
||||
}
|
||||
|
||||
status = ComputeHashOfBuffer(
|
||||
disk_text_base, disk_text_header->SizeOfRawData, &disk_hash, &disk_hash_size);
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
{
|
||||
DEBUG_ERROR("ComputeHashOfBuffer failed with status %s", status);
|
||||
goto free_iteration;
|
||||
}
|
||||
|
||||
status = ComputeHashOfBuffer(memory_text_base,
|
||||
memory_text_header->SizeOfRawData,
|
||||
&memory_hash,
|
||||
&memory_hash_size);
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
{
|
||||
DEBUG_ERROR("ComputeHashOfBuffer failed with status %x", status);
|
||||
goto free_iteration;
|
||||
}
|
||||
|
||||
if (!MmIsAddressValid(memory_hash) || !MmIsAddressValid(disk_hash))
|
||||
goto free_iteration;
|
||||
|
||||
result = RtlCompareMemory(memory_hash, disk_hash, memory_hash_size);
|
||||
|
||||
if (result = memory_text_header->SizeOfRawData)
|
||||
DEBUG_VERBOSE("Module executable sections are valid for the module: %s",
|
||||
module_info->FullPathName);
|
||||
else
|
||||
DEBUG_WARNING("Module regions are not valid for module: %s",
|
||||
module_info->FullPathName);
|
||||
|
||||
free_iteration:
|
||||
|
||||
if (memory_buffer)
|
||||
ExFreePoolWithTag(memory_buffer, POOL_TAG_INTEGRITY);
|
||||
|
||||
if (memory_hash)
|
||||
ExFreePoolWithTag(memory_hash, POOL_TAG_INTEGRITY);
|
||||
|
||||
if (disk_buffer)
|
||||
ExFreePoolWithTag(disk_buffer, POOL_TAG_INTEGRITY);
|
||||
|
||||
if (disk_hash)
|
||||
ExFreePoolWithTag(disk_hash, POOL_TAG_INTEGRITY);
|
||||
|
||||
free_section:
|
||||
|
||||
if (section_handle)
|
||||
ZwClose(section_handle);
|
||||
|
||||
if (section)
|
||||
ZwUnmapViewOfSection(ZwCurrentProcess(), section);
|
||||
|
||||
/*
|
||||
* Its times like this where you see why allocating all local variables at the
|
||||
* beginning of the function may not be so ideal...
|
||||
*/
|
||||
if (path.Buffer)
|
||||
{
|
||||
RtlFreeUnicodeString(&path);
|
||||
path.Buffer = NULL;
|
||||
path.Length = 0;
|
||||
path.MaximumLength = 0;
|
||||
}
|
||||
|
||||
ansi_string.Buffer = NULL;
|
||||
ansi_string.Length = 0;
|
||||
ansi_string.MaximumLength = 0;
|
||||
|
||||
section_handle = NULL;
|
||||
section = NULL;
|
||||
memory_buffer = NULL;
|
||||
memory_hash = NULL;
|
||||
disk_buffer = NULL;
|
||||
disk_hash = NULL;
|
||||
ExFreePoolWithTag(modules.address, SYSTEM_MODULES_POOL);
|
||||
return STATUS_MEMORY_NOT_ALLOCATED;
|
||||
}
|
||||
|
||||
if (modules.address)
|
||||
ExFreePoolWithTag(modules.address, SYSTEM_MODULES_POOL);
|
||||
Context->active_thread_count = 0;
|
||||
Context->active = TRUE;
|
||||
Context->complete = FALSE;
|
||||
Context->dispatcher_info = dispatcher_array;
|
||||
Context->module_info = modules.address;
|
||||
Context->current_count = 0;
|
||||
Context->total_count = modules.module_count;
|
||||
Context->block_size = VALIDATION_BLOCK_SIZE;
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
VOID
|
||||
FreeWorkItems(_In_ PSYS_MODULE_VAL_CONTEXT Context)
|
||||
{
|
||||
for (INT index = 0; index < VERIFICATION_THREAD_COUNT; index++)
|
||||
{
|
||||
if (Context->work_items[index])
|
||||
{
|
||||
IoFreeWorkItem(Context->work_items[index]);
|
||||
Context->work_items[index] = 0ull;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
STATIC
|
||||
VOID
|
||||
FreeModuleVerificationItems(_In_ PSYS_MODULE_VAL_CONTEXT Context)
|
||||
{
|
||||
if (!Context->active_thread_count)
|
||||
{
|
||||
if (Context->module_info)
|
||||
ExFreePoolWithTag(Context->module_info, SYSTEM_MODULES_POOL);
|
||||
if (Context->dispatcher_info)
|
||||
ExFreePoolWithTag(Context->dispatcher_info, POOL_TAG_INTEGRITY);
|
||||
}
|
||||
}
|
||||
|
||||
NTSTATUS
|
||||
SystemModuleVerificationDispatcher()
|
||||
{
|
||||
NTSTATUS status = STATUS_UNSUCCESSFUL;
|
||||
PSYS_MODULE_VAL_CONTEXT context = NULL;
|
||||
PIO_WORKITEM work_item = NULL;
|
||||
|
||||
GetSystemModuleValidationContext(&context);
|
||||
|
||||
if (context->complete)
|
||||
{
|
||||
DEBUG_VERBOSE("System modules integrity check complete. Freeing items.");
|
||||
context->active = FALSE;
|
||||
FreeModuleVerificationItems(context);
|
||||
FreeWorkItems(context);
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
if (!context->active)
|
||||
{
|
||||
DEBUG_VERBOSE("Context not active, generating new one");
|
||||
|
||||
status = InitialiseSystemModuleVerificationContext(context);
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
{
|
||||
DEBUG_ERROR(
|
||||
"InitialiseSystemModuleVerificationContext failed with status %x",
|
||||
status);
|
||||
return status;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FreeWorkItems(context);
|
||||
}
|
||||
|
||||
for (INT index = 0; index < VERIFICATION_THREAD_COUNT; index++)
|
||||
{
|
||||
work_item = IoAllocateWorkItem(GetDriverDeviceObject());
|
||||
|
||||
if (!work_item)
|
||||
continue;
|
||||
|
||||
IoQueueWorkItem(
|
||||
work_item, SystemModuleVerificationDispatchFunction, DelayedWorkQueue, context);
|
||||
|
||||
context->work_items[index] = work_item;
|
||||
}
|
||||
|
||||
DEBUG_VERBOSE("Finished validating modules via dispatcher threads");
|
||||
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
/*
|
||||
* After some further research it appears that performing integrity checks the same way as
|
||||
* validating other modules is not as straight forward as there are many (thousands) of hooks that
|
||||
|
|
|
@ -5,6 +5,62 @@
|
|||
|
||||
#include "common.h"
|
||||
|
||||
typedef struct _MODULE_DISPATCHER_HEADER
|
||||
{
|
||||
volatile UINT32 validated; // if this is > 0, a thread is already using it
|
||||
UINT8 result;
|
||||
|
||||
} MODULE_DISPATCHER_HEADER, *PMODULE_DISPATCHER_HEADER;
|
||||
|
||||
typedef struct _SYSTEM_MODULE_INFORMATION
|
||||
{
|
||||
MODULE_DISPATCHER_HEADER dispatcher_header;
|
||||
RTL_MODULE_EXTENDED_INFO module_information;
|
||||
|
||||
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;
|
||||
|
||||
// #pragma pack(1)
|
||||
// typedef struct VERIFICATION_CONTEXT
|
||||
//{
|
||||
// // PSYSTEM_MODULE_INFORMATION module_info;
|
||||
//
|
||||
//
|
||||
// } VERIFICATION_CONTEXT, *PVERIFICATION_CONTEXT;
|
||||
// #pragma pack()
|
||||
|
||||
#define VERIFICATION_THREAD_COUNT 4
|
||||
|
||||
typedef struct _SYS_MODULE_VAL_CONTEXT
|
||||
{
|
||||
/* Stores the number of actively executing worker threads */
|
||||
volatile LONG active_thread_count;
|
||||
|
||||
/* determines whether a validation is in progress */
|
||||
volatile LONG active;
|
||||
|
||||
/* determines whether a validation is complete */
|
||||
volatile LONG complete;
|
||||
|
||||
/* current count of validated modules */
|
||||
volatile LONG current_count;
|
||||
|
||||
/* total count of modules */
|
||||
UINT32 total_count;
|
||||
|
||||
/* number of modules to validate in a single sweep */
|
||||
UINT32 block_size;
|
||||
|
||||
/* pointer to the buffer containing the system module information */
|
||||
PRTL_MODULE_EXTENDED_INFO module_info;
|
||||
|
||||
/* pointer to the array of dispatcher info used to synchonize threads */
|
||||
PMODULE_DISPATCHER_HEADER dispatcher_info;
|
||||
|
||||
/* array of pointers to work items, used to free work items when complete */
|
||||
PIO_WORKITEM work_items[VERIFICATION_THREAD_COUNT];
|
||||
|
||||
} SYS_MODULE_VAL_CONTEXT, *PSYS_MODULE_VAL_CONTEXT;
|
||||
|
||||
typedef enum _SMBIOS_TABLE_INDEX
|
||||
{
|
||||
SmbiosInformation = 0,
|
||||
|
@ -15,8 +71,8 @@ typedef enum _SMBIOS_TABLE_INDEX
|
|||
} SMBIOS_TABLE_INDEX;
|
||||
|
||||
#define SMBIOS_VMWARE_SERIAL_NUMBER_SUB_INDEX 3
|
||||
#define SMBIOS_NATIVE_SERIAL_NUMBER_SUB_INDEX 4
|
||||
#define SMBIOS_VENDOR_STRING_SUB_INDEX 1
|
||||
#define SMBIOS_NATIVE_SERIAL_NUMBER_SUB_INDEX 4
|
||||
#define SMBIOS_VENDOR_STRING_SUB_INDEX 1
|
||||
|
||||
NTSTATUS
|
||||
GetDriverImageSize(_Inout_ PIRP Irp);
|
||||
|
@ -64,4 +120,7 @@ ValidateNtoskrnl();
|
|||
NTSTATUS
|
||||
GetOsVersionInformation(_Out_ PRTL_OSVERSIONINFOW VersionInfo);
|
||||
|
||||
NTSTATUS
|
||||
SystemModuleVerificationDispatcher();
|
||||
|
||||
#endif
|
||||
|
|
|
@ -444,7 +444,8 @@ DeviceControl(_In_ PDRIVER_OBJECT DriverObject, _Inout_ PIRP Irp)
|
|||
* Currently the validation is buggy, once the validation is better will
|
||||
* probably bugcheck the system.
|
||||
*/
|
||||
status = ValidateSystemModules();
|
||||
//status = ValidateSystemModules();
|
||||
status = SystemModuleVerificationDispatcher();
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
DEBUG_ERROR("ValidateSystemModules failed with status %x", status);
|
||||
|
|
|
@ -33,8 +33,8 @@ ValidateThreadsPspCidTableEntry(_In_ PETHREAD Thread)
|
|||
* is important is because passing in a handle value of 0 which, even though is a valid cid,
|
||||
* returns a non success status meaning we mark it an invalid cid entry even though it is.
|
||||
* To combat this we simply add a little check here. The problem is this can be easily
|
||||
* bypassed by simply modifying the ETHREAD->Cid.UniqueThread identifier.. So while it isnt a
|
||||
* perfect detection method for now it's good enough.
|
||||
* bypassed by simply modifying the ETHREAD->Cid.UniqueThread identifier.. So while it isnt
|
||||
* a perfect detection method for now it's good enough.
|
||||
*/
|
||||
if ((UINT64)thread_id < (UINT64)KeQueryActiveProcessorCount(NULL))
|
||||
return TRUE;
|
||||
|
@ -57,6 +57,19 @@ ValidateThreadsPspCidTableEntry(_In_ PETHREAD Thread)
|
|||
return TRUE;
|
||||
}
|
||||
|
||||
/*
|
||||
* I did not reverse this myself and previously had no idea how you would go about
|
||||
* detecting KiAttachProcess so credits to KANKOSHEV for the explanation:
|
||||
*
|
||||
* https://github.com/KANKOSHEV/Detect-KeAttachProcess/tree/main
|
||||
* https://doxygen.reactos.org/d0/dc9/procobj_8c.html#adec6dc539d4a5c0ee7d0f48e24ef0933
|
||||
*
|
||||
* To expand on his writeup a little, the offset that he provides is equivalent to
|
||||
* PKAPC_STATE->Process. This is where KiAttachProcess writes the process that thread is attaching
|
||||
* to when it's called. The APC_STATE structure holds relevant information about the thread's APC
|
||||
* state and is quite important during context switch scenarios as it's how the thread determines if
|
||||
* it has any APC's queued.
|
||||
*/
|
||||
_IRQL_always_function_min_(DISPATCH_LEVEL) STATIC VOID
|
||||
DetectAttachedThreadsProcessCallback(_In_ PTHREAD_LIST_ENTRY ThreadListEntry,
|
||||
_Inout_opt_ PVOID Context)
|
||||
|
@ -68,12 +81,17 @@ _IRQL_always_function_min_(DISPATCH_LEVEL) STATIC VOID
|
|||
|
||||
GetProtectedProcessEProcess(&protected_process);
|
||||
|
||||
if (protected_process == NULL)
|
||||
if (!protected_process)
|
||||
return;
|
||||
|
||||
apc_state = (PKAPC_STATE)((UINT64)ThreadListEntry->thread + KTHREAD_APC_STATE_OFFSET);
|
||||
|
||||
if (apc_state->Process == protected_process)
|
||||
/*
|
||||
* Just a sanity check even though it doesnt really make sense for internal threads of our
|
||||
* protected process to attach..
|
||||
*/
|
||||
if (apc_state->Process == protected_process &&
|
||||
ThreadListEntry->owning_process != protected_process)
|
||||
{
|
||||
DEBUG_WARNING("Thread is attached to our protected process: %llx",
|
||||
(UINT64)ThreadListEntry->thread);
|
||||
|
@ -92,19 +110,6 @@ _IRQL_always_function_min_(DISPATCH_LEVEL) STATIC VOID
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* I did not reverse this myself and previously had no idea how you would go about
|
||||
* detecting KiAttachProcess so credits to KANKOSHEV for the explanation:
|
||||
*
|
||||
* https://github.com/KANKOSHEV/Detect-KeAttachProcess/tree/main
|
||||
* https://doxygen.reactos.org/d0/dc9/procobj_8c.html#adec6dc539d4a5c0ee7d0f48e24ef0933
|
||||
*
|
||||
* To expand on his writeup a little, the offset that he provides is equivalent to
|
||||
* PKAPC_STATE->Process. This is where KiAttachProcess writes the process that thread is attaching
|
||||
* to when it's called. The APC_STATE structure holds relevant information about the thread's APC
|
||||
* state and is quite important during context switch scenarios as it's how the thread determines if
|
||||
* it has any APC's queued.
|
||||
*/
|
||||
VOID
|
||||
DetectThreadsAttachedToProtectedProcess()
|
||||
{
|
||||
|
|
|
@ -76,9 +76,9 @@ Init(HINSTANCE hinstDLL)
|
|||
|
||||
srand(time(NULL));
|
||||
|
||||
while (!GetAsyncKeyState(VK_DELETE))
|
||||
while (!GetAsyncKeyState(VK_DELETE))
|
||||
{
|
||||
int seed = (rand() % 11);
|
||||
int seed = (rand() % 11);
|
||||
|
||||
std::cout << "Seed: " << seed << std::endl;
|
||||
|
||||
|
|
Loading…
Reference in a new issue