mirror of
https://github.com/donnaskiez/ac.git
synced 2024-11-21 22:24:08 +01:00
refactor the callbacks
This commit is contained in:
parent
45a23b7177
commit
1e2de300c2
7 changed files with 200 additions and 172 deletions
|
@ -6,9 +6,6 @@
|
|||
#include "pool.h"
|
||||
#include "thread.h"
|
||||
|
||||
CALLBACK_CONFIGURATION configuration = { 0 };
|
||||
|
||||
STATIC
|
||||
VOID
|
||||
ObPostOpCallbackRoutine(
|
||||
_In_ PVOID RegistrationContext,
|
||||
|
@ -18,7 +15,6 @@ ObPostOpCallbackRoutine(
|
|||
|
||||
}
|
||||
|
||||
STATIC
|
||||
OB_PREOP_CALLBACK_STATUS
|
||||
ObPreOpCallbackRoutine(
|
||||
_In_ PVOID RegistrationContext,
|
||||
|
@ -46,9 +42,14 @@ ObPreOpCallbackRoutine(
|
|||
LPCSTR process_creator_name;
|
||||
LPCSTR target_process_name;
|
||||
LPCSTR protected_process_name;
|
||||
PCALLBACK_CONFIGURATION configuration = NULL;
|
||||
|
||||
KeAcquireGuardedMutex(&configuration.mutex);
|
||||
GetCallbackConfigStructure(&configuration);
|
||||
|
||||
if (!configuration)
|
||||
return OB_PREOP_SUCCESS;
|
||||
|
||||
KeAcquireGuardedMutex(&configuration->mutex);
|
||||
GetProtectedProcessId(&protected_process_id);
|
||||
GetProtectedProcessEProcess(&protected_process);
|
||||
|
||||
|
@ -110,7 +111,7 @@ ObPreOpCallbackRoutine(
|
|||
|
||||
end:
|
||||
|
||||
KeReleaseGuardedMutex(&configuration.mutex);
|
||||
KeReleaseGuardedMutex(&configuration->mutex);
|
||||
return OB_PREOP_SUCCESS;
|
||||
}
|
||||
|
||||
|
@ -404,68 +405,4 @@ EnumerateProcessListWithCallbackFunction(
|
|||
process_list_entry = process_list_entry->Flink;
|
||||
|
||||
} while (process_list_entry != process_list_head->Blink);
|
||||
}
|
||||
|
||||
NTSTATUS
|
||||
InitiateDriverCallbacks()
|
||||
{
|
||||
NTSTATUS status;
|
||||
|
||||
/*
|
||||
* This mutex ensures we don't unregister our ObRegisterCallbacks while
|
||||
* the callback function is running since this might cause some funny stuff
|
||||
* to happen. Better to be safe then sorry :)
|
||||
*/
|
||||
KeInitializeGuardedMutex(&configuration.mutex);
|
||||
|
||||
OB_CALLBACK_REGISTRATION callback_registration = { 0 };
|
||||
OB_OPERATION_REGISTRATION operation_registration = { 0 };
|
||||
|
||||
operation_registration.ObjectType = PsProcessType;
|
||||
operation_registration.Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE;
|
||||
operation_registration.PreOperation = ObPreOpCallbackRoutine;
|
||||
operation_registration.PostOperation = ObPostOpCallbackRoutine;
|
||||
|
||||
callback_registration.Version = OB_FLT_REGISTRATION_VERSION;
|
||||
callback_registration.OperationRegistration = &operation_registration;
|
||||
callback_registration.OperationRegistrationCount = 1;
|
||||
callback_registration.RegistrationContext = NULL;
|
||||
|
||||
status = ObRegisterCallbacks(
|
||||
&callback_registration,
|
||||
&configuration.registration_handle
|
||||
);
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
{
|
||||
DEBUG_ERROR("failed to launch obregisters with status %x", status);
|
||||
return status;
|
||||
}
|
||||
|
||||
//status = PsSetCreateProcessNotifyRoutine(
|
||||
// ProcessCreateNotifyRoutine,
|
||||
// FALSE
|
||||
//);
|
||||
|
||||
//if ( !NT_SUCCESS( status ) )
|
||||
// DEBUG_ERROR( "Failed to launch ps create notif routines with status %x", status );
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
VOID
|
||||
UnregisterCallbacksOnProcessTermination()
|
||||
{
|
||||
DEBUG_LOG("Process closed, unregistering callbacks");
|
||||
KeAcquireGuardedMutex(&configuration.mutex);
|
||||
|
||||
if (configuration.registration_handle == NULL)
|
||||
{
|
||||
KeReleaseGuardedMutex(&configuration.mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
ObUnRegisterCallbacks(configuration.registration_handle);
|
||||
configuration.registration_handle = NULL;
|
||||
KeReleaseGuardedMutex(&configuration.mutex);
|
||||
}
|
|
@ -19,13 +19,6 @@ typedef struct _OPEN_HANDLE_FAILURE_REPORT
|
|||
|
||||
}OPEN_HANDLE_FAILURE_REPORT, * POPEN_HANDLE_FAILURE_REPORT;
|
||||
|
||||
typedef struct _CALLBACKS_CONFIGURATION
|
||||
{
|
||||
PVOID registration_handle;
|
||||
KGUARDED_MUTEX mutex;
|
||||
|
||||
}CALLBACK_CONFIGURATION, * PCALLBACK_CONFIGURATION;
|
||||
|
||||
//handle access masks
|
||||
//https://learn.microsoft.com/en-us/windows/win32/procthread/process-security-and-access-rights
|
||||
#define PROCESS_CREATE_PROCESS 0x0080
|
||||
|
@ -58,14 +51,12 @@ ExUnlockHandleTableEntry(
|
|||
IN PHANDLE_TABLE_ENTRY HandleTableEntry
|
||||
);
|
||||
|
||||
STATIC
|
||||
VOID
|
||||
ObPostOpCallbackRoutine(
|
||||
_In_ PVOID RegistrationContext,
|
||||
_In_ POB_POST_OPERATION_INFORMATION OperationInformation
|
||||
);
|
||||
|
||||
STATIC
|
||||
OB_PREOP_CALLBACK_STATUS
|
||||
ObPreOpCallbackRoutine(
|
||||
_In_ PVOID RegistrationContext,
|
||||
|
@ -89,10 +80,4 @@ EnumerateProcessHandles(
|
|||
_In_ PEPROCESS Process
|
||||
);
|
||||
|
||||
NTSTATUS
|
||||
InitiateDriverCallbacks();
|
||||
|
||||
VOID
|
||||
UnregisterCallbacksOnProcessTermination();
|
||||
|
||||
#endif
|
||||
|
|
153
driver/driver.c
153
driver/driver.c
|
@ -13,9 +13,6 @@
|
|||
/*
|
||||
* This structure is strictly for driver related stuff
|
||||
* that should only be written at driver entry.
|
||||
*
|
||||
* Note that the lock isnt really needed here but Im using one
|
||||
* just in case c:
|
||||
*/
|
||||
|
||||
#define MAXIMUM_APC_CONTEXTS 10
|
||||
|
@ -30,6 +27,7 @@ typedef struct _DRIVER_CONFIG
|
|||
UNICODE_STRING registry_path;
|
||||
SYSTEM_INFORMATION system_information;
|
||||
PVOID apc_contexts[MAXIMUM_APC_CONTEXTS];
|
||||
PCALLBACK_CONFIGURATION callback_config;
|
||||
KGUARDED_MUTEX lock;
|
||||
|
||||
}DRIVER_CONFIG, * PDRIVER_CONFIG;
|
||||
|
@ -51,6 +49,131 @@ typedef struct _PROCESS_CONFIG
|
|||
DRIVER_CONFIG driver_config = { 0 };
|
||||
PROCESS_CONFIG process_config = { 0 };
|
||||
|
||||
#define POOL_TAG_CONFIG 'conf'
|
||||
|
||||
NTSTATUS
|
||||
EnableCallbackRoutinesOnProcessRun()
|
||||
{
|
||||
NTSTATUS status;
|
||||
|
||||
KeAcquireGuardedMutex(&driver_config.lock);
|
||||
KeAcquireGuardedMutex(&driver_config.callback_config->mutex);
|
||||
|
||||
OB_CALLBACK_REGISTRATION callback_registration = { 0 };
|
||||
OB_OPERATION_REGISTRATION operation_registration = { 0 };
|
||||
|
||||
operation_registration.ObjectType = PsProcessType;
|
||||
operation_registration.Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE;
|
||||
operation_registration.PreOperation = ObPreOpCallbackRoutine;
|
||||
operation_registration.PostOperation = ObPostOpCallbackRoutine;
|
||||
|
||||
callback_registration.Version = OB_FLT_REGISTRATION_VERSION;
|
||||
callback_registration.OperationRegistration = &operation_registration;
|
||||
callback_registration.OperationRegistrationCount = 1;
|
||||
callback_registration.RegistrationContext = NULL;
|
||||
|
||||
status = ObRegisterCallbacks(
|
||||
&callback_registration,
|
||||
&driver_config.callback_config->registration_handle
|
||||
);
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
{
|
||||
DEBUG_ERROR("failed to launch obregisters with status %x", status);
|
||||
goto end;
|
||||
}
|
||||
|
||||
//status = PsSetCreateProcessNotifyRoutine(
|
||||
// ProcessCreateNotifyRoutine,
|
||||
// FALSE
|
||||
//);
|
||||
|
||||
//if ( !NT_SUCCESS( status ) )
|
||||
// DEBUG_ERROR( "Failed to launch ps create notif routines with status %x", status );
|
||||
|
||||
end:
|
||||
KeReleaseGuardedMutex(&driver_config.callback_config->mutex);
|
||||
KeReleaseGuardedMutex(&driver_config.lock);
|
||||
return status;
|
||||
}
|
||||
|
||||
STATIC
|
||||
NTSTATUS
|
||||
AllocateCallbackStructure()
|
||||
{
|
||||
KeAcquireGuardedMutex(&driver_config.lock);
|
||||
|
||||
driver_config.callback_config =
|
||||
ExAllocatePool2(POOL_FLAG_NON_PAGED, sizeof(CALLBACK_CONFIGURATION), POOL_TAG_CONFIG);
|
||||
|
||||
if (!driver_config.callback_config)
|
||||
{
|
||||
KeReleaseGuardedMutex(&driver_config.lock);
|
||||
return STATUS_MEMORY_NOT_ALLOCATED;
|
||||
}
|
||||
|
||||
/*
|
||||
* This mutex ensures we don't unregister our ObRegisterCallbacks while
|
||||
* the callback function is running since this might cause some funny stuff
|
||||
* to happen. Better to be safe then sorry :)
|
||||
*/
|
||||
KeInitializeGuardedMutex(&driver_config.callback_config->mutex);
|
||||
KeReleaseGuardedMutex(&driver_config.lock);
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
/*
|
||||
* The question is, What happens if we attempt to register our callbacks after we
|
||||
* unregister them but before we free the pool? Hm.. No Good.
|
||||
*
|
||||
* Okay to solve this well acquire the driver lock aswell, we could also just
|
||||
* store the structure in the .data section but i ceebs atm.
|
||||
*
|
||||
* This definitely doesn't seem optimal, but it works ...
|
||||
*/
|
||||
STATIC
|
||||
VOID
|
||||
CleanupDriverCallbacksOnDriverUnload()
|
||||
{
|
||||
/* UnRegisterCallbacksOnProcessTermination holds the driver lock, so must acquire it after */
|
||||
UnregisterCallbacksOnProcessTermination();
|
||||
KeAcquireGuardedMutex(&driver_config.lock);
|
||||
ExFreePoolWithTag(driver_config.callback_config, POOL_TAG_CONFIG);
|
||||
KeAcquireGuardedMutex(&driver_config.lock);
|
||||
}
|
||||
|
||||
VOID
|
||||
UnregisterCallbacksOnProcessTermination()
|
||||
{
|
||||
KeAcquireGuardedMutex(&driver_config.lock);
|
||||
KeAcquireGuardedMutex(&driver_config.callback_config->mutex);
|
||||
|
||||
if (driver_config.callback_config->registration_handle)
|
||||
{
|
||||
ObUnRegisterCallbacks(driver_config.callback_config->registration_handle);
|
||||
driver_config.callback_config->registration_handle = NULL;
|
||||
}
|
||||
|
||||
KeReleaseGuardedMutex(&driver_config.callback_config->mutex);
|
||||
KeReleaseGuardedMutex(&driver_config.lock);
|
||||
}
|
||||
|
||||
/*
|
||||
* Can return a null value if we attempt to read the value is underway whilst we are
|
||||
* freeing the structure, hence the use of the 2 locks.
|
||||
*/
|
||||
VOID
|
||||
GetCallbackConfigStructure(
|
||||
_Out_ PCALLBACK_CONFIGURATION* CallbackConfiguration
|
||||
)
|
||||
{
|
||||
KeAcquireGuardedMutex(&driver_config.lock);
|
||||
KeAcquireGuardedMutex(&driver_config.callback_config->mutex);
|
||||
*CallbackConfiguration = driver_config.callback_config;
|
||||
KeReleaseGuardedMutex(&driver_config.callback_config->mutex);
|
||||
KeReleaseGuardedMutex(&driver_config.lock);
|
||||
}
|
||||
|
||||
/*
|
||||
* The driver config structure holds an array of pointers to APC context structures. These
|
||||
* APC context structures are unique to each APC operation that this driver will perform. For
|
||||
|
@ -580,6 +703,15 @@ InitialiseDriverConfigOnDriverEntry(
|
|||
return status;
|
||||
}
|
||||
|
||||
status = AllocateCallbackStructure();
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
{
|
||||
DEBUG_ERROR("AllocateCallbackStructure failed with status %x", status);
|
||||
FreeDriverConfigurationStringBuffers();
|
||||
return status;
|
||||
}
|
||||
|
||||
DEBUG_LOG("Motherboard serial: %s", driver_config.system_information.motherboard_serial);
|
||||
DEBUG_LOG("Drive 0 serial: %s", driver_config.system_information.drive_0_serial);
|
||||
|
||||
|
@ -592,8 +724,8 @@ InitialiseProcessConfigOnProcessLaunch(
|
|||
)
|
||||
{
|
||||
NTSTATUS status;
|
||||
PDRIVER_INITIATION_INFORMATION information;
|
||||
PEPROCESS eprocess;
|
||||
PDRIVER_INITIATION_INFORMATION information;
|
||||
|
||||
information = (PDRIVER_INITIATION_INFORMATION)Irp->AssociatedIrp.SystemBuffer;
|
||||
|
||||
|
@ -636,18 +768,21 @@ DriverUnload(
|
|||
_In_ PDRIVER_OBJECT DriverObject
|
||||
)
|
||||
{
|
||||
DEBUG_LOG("Unloading driver...");
|
||||
//PsSetCreateProcessNotifyRoutine( ProcessCreateNotifyRoutine, TRUE );
|
||||
//QueryActiveApcContextsForCompletion();
|
||||
|
||||
/* dont unload while we have active APC operations */
|
||||
//while ( !FreeAllApcContextStructures() )
|
||||
// YieldProcessor();
|
||||
while (FreeAllApcContextStructures() == FALSE)
|
||||
YieldProcessor();
|
||||
|
||||
/* This is safe to call even if the callbacks have already been disabled */
|
||||
//UnregisterCallbacksOnProcessTermination();
|
||||
CleanupDriverCallbacksOnDriverUnload();
|
||||
|
||||
//CleanupDriverConfigOnUnload();
|
||||
//IoDeleteDevice( DriverObject->DeviceObject );
|
||||
CleanupDriverConfigOnUnload();
|
||||
IoDeleteDevice(DriverObject->DeviceObject);
|
||||
|
||||
DEBUG_LOG("Driver unloaded");
|
||||
}
|
||||
|
||||
VOID
|
||||
|
|
|
@ -26,6 +26,13 @@ typedef struct _SYSTEM_INFORMATION
|
|||
|
||||
}SYSTEM_INFORMATION, * PSYSTEM_INFORMATION;
|
||||
|
||||
typedef struct _CALLBACKS_CONFIGURATION
|
||||
{
|
||||
PVOID registration_handle;
|
||||
KGUARDED_MUTEX mutex;
|
||||
|
||||
}CALLBACK_CONFIGURATION, * PCALLBACK_CONFIGURATION;
|
||||
|
||||
NTSTATUS InitialiseProcessConfigOnProcessLaunch(
|
||||
_In_ PIRP Irp
|
||||
);
|
||||
|
@ -86,4 +93,15 @@ TerminateProtectedProcessOnViolation();
|
|||
VOID
|
||||
ClearProcessConfigOnProcessTermination();
|
||||
|
||||
NTSTATUS
|
||||
EnableCallbackRoutinesOnProcessRun();
|
||||
|
||||
VOID
|
||||
UnregisterCallbacksOnProcessTermination();
|
||||
|
||||
VOID
|
||||
GetCallbackConfigStructure(
|
||||
_Out_ PCALLBACK_CONFIGURATION* CallbackConfiguration
|
||||
);
|
||||
|
||||
#endif
|
|
@ -1414,10 +1414,11 @@ WCHAR PROTECTED_FUNCTIONS[EPT_PROTECTED_FUNCTIONS_COUNT][EPT_MAX_FUNCTION_NAME_L
|
|||
|
||||
/*
|
||||
* For whatever reason MmGetSystemRoutineAddress only works once, then every call
|
||||
* thereafter fails. So will be storing the routine addresses in arrays.
|
||||
* thereafter fails. So will be storing the routine addresses in arrays since they
|
||||
* dont change once the kernel is loaded.
|
||||
*/
|
||||
UINT64 CONTROL_FUNCTION_ADDRESSES[EPT_CONTROL_FUNCTIONS_COUNT];
|
||||
UINT64 PROTECTED_FUNCTION_ADDRESSES[EPT_PROTECTED_FUNCTIONS_COUNT];
|
||||
UINT64 CONTROL_FUNCTION_ADDRESSES[EPT_CONTROL_FUNCTIONS_COUNT] = { 0 };
|
||||
UINT64 PROTECTED_FUNCTION_ADDRESSES[EPT_PROTECTED_FUNCTIONS_COUNT] = { 0 };
|
||||
|
||||
STATIC
|
||||
NTSTATUS
|
||||
|
@ -1446,9 +1447,6 @@ InitiateEptFunctionAddressArrays()
|
|||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
/*
|
||||
* This maybe needs to be dispatched from a system thread rather then a user mode thread.
|
||||
*/
|
||||
NTSTATUS
|
||||
DetectEptHooksInKeyFunctions()
|
||||
{
|
||||
|
@ -1483,23 +1481,13 @@ DetectEptHooksInKeyFunctions()
|
|||
control_time_sum += instruction_time;
|
||||
}
|
||||
|
||||
DEBUG_LOG("Control time sum: %llx", control_time_sum);
|
||||
|
||||
if (control_time_sum == 0)
|
||||
{
|
||||
DEBUG_ERROR("Control time is null");
|
||||
return STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
|
||||
control_average = control_time_sum / (EPT_CONTROL_FUNCTIONS_COUNT - control_fails);
|
||||
|
||||
DEBUG_LOG("Control average: %llx", control_average);
|
||||
|
||||
if (control_average == 0)
|
||||
{
|
||||
DEBUG_ERROR("Control average time is null");
|
||||
return STATUS_UNSUCCESSFUL;
|
||||
}
|
||||
return STATUS_ABANDONED;
|
||||
|
||||
for (INT index = 0; index < EPT_PROTECTED_FUNCTIONS_COUNT; index++)
|
||||
{
|
||||
|
|
|
@ -163,7 +163,7 @@ DeviceControl(
|
|||
goto end;
|
||||
}
|
||||
|
||||
status = InitiateDriverCallbacks();
|
||||
status = EnableCallbackRoutinesOnProcessRun();
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
DEBUG_ERROR("InitiateDriverCallbacks failed with status %x", status);
|
||||
|
@ -338,18 +338,7 @@ DeviceControl(
|
|||
|
||||
case IOCTL_CHECK_FOR_EPT_HOOK:
|
||||
|
||||
/*
|
||||
* No need to wait on this thread as if it fails, program will be shut.
|
||||
*/
|
||||
status = PsCreateSystemThread(
|
||||
&handle,
|
||||
PROCESS_ALL_ACCESS,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
DetectEptHooksInKeyFunctions,
|
||||
NULL
|
||||
);
|
||||
status = DetectEptHooksInKeyFunctions();
|
||||
|
||||
if (!NT_SUCCESS(status))
|
||||
DEBUG_ERROR("DetectEpthooksInKeyFunctions failed with status %x", status);
|
||||
|
|
|
@ -43,19 +43,40 @@ DWORD WINAPI Init(HINSTANCE hinstDLL)
|
|||
|
||||
while (!GetAsyncKeyState(VK_DELETE))
|
||||
{
|
||||
int seed = (rand() % 3);
|
||||
int seed = (rand() % 10);
|
||||
|
||||
std::cout << "Seed: " << seed << std::endl;
|
||||
|
||||
switch (seed)
|
||||
{
|
||||
case 0:
|
||||
kmanager.CheckForAttachedThreads();
|
||||
kmanager.EnumerateHandleTables();
|
||||
break;
|
||||
case 1:
|
||||
kmanager.CheckForHiddenThreads();
|
||||
kmanager.PerformIntegrityCheck();
|
||||
break;
|
||||
case 2:
|
||||
kmanager.ScanPoolsForUnlinkedProcesses();
|
||||
break;
|
||||
case 3:
|
||||
kmanager.VerifySystemModules();
|
||||
break;
|
||||
case 4:
|
||||
kmanager.ValidateProcessModules();
|
||||
break;
|
||||
case 5:
|
||||
kmanager.RunNmiCallbacks();
|
||||
break;
|
||||
case 6:
|
||||
kmanager.CheckForAttachedThreads();
|
||||
break;
|
||||
case 7:
|
||||
//kmanager.InitiateApcStackwalkOperation();
|
||||
break;
|
||||
case 8:
|
||||
kmanager.CheckForHiddenThreads();
|
||||
break;
|
||||
case 9:
|
||||
kmanager.CheckForEptHooks();
|
||||
break;
|
||||
}
|
||||
|
@ -65,51 +86,6 @@ DWORD WINAPI Init(HINSTANCE hinstDLL)
|
|||
std::this_thread::sleep_for(std::chrono::seconds(10));
|
||||
}
|
||||
|
||||
//while (!GetAsyncKeyState(VK_DELETE))
|
||||
//{
|
||||
// int seed = (rand() % 10);
|
||||
|
||||
// std::cout << "Seed: " << seed << std::endl;
|
||||
|
||||
// switch (seed)
|
||||
// {
|
||||
// case 0:
|
||||
// kmanager.EnumerateHandleTables();
|
||||
// break;
|
||||
// case 1:
|
||||
// kmanager.PerformIntegrityCheck();
|
||||
// break;
|
||||
// case 2:
|
||||
// kmanager.ScanPoolsForUnlinkedProcesses();
|
||||
// break;
|
||||
// case 3:
|
||||
// kmanager.VerifySystemModules();
|
||||
// break;
|
||||
// case 4:
|
||||
// kmanager.ValidateProcessModules();
|
||||
// break;
|
||||
// case 5:
|
||||
// kmanager.RunNmiCallbacks();
|
||||
// break;
|
||||
// case 6:
|
||||
// kmanager.CheckForAttachedThreads();
|
||||
// break;
|
||||
// case 7:
|
||||
// kmanager.InitiateApcStackwalkOperation();
|
||||
// break;
|
||||
// case 8:
|
||||
// kmanager.CheckForHiddenThreads();
|
||||
// break;
|
||||
// case 9:
|
||||
// kmanager.CheckForEptHooks();
|
||||
// break;
|
||||
// }
|
||||
|
||||
// kmanager.MonitorCallbackReports();
|
||||
|
||||
// std::this_thread::sleep_for(std::chrono::seconds(10));
|
||||
//}
|
||||
|
||||
fclose(stdout);
|
||||
fclose(stdin);
|
||||
FreeConsole();
|
||||
|
|
Loading…
Reference in a new issue