You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
709 lines
16 KiB
709 lines
16 KiB
/*++
|
|
|
|
Copyright (c) 2001 Microsoft Corporation
|
|
|
|
Module Name:
|
|
|
|
injecter.cxx
|
|
|
|
Abstract:
|
|
|
|
Injecter
|
|
|
|
Author:
|
|
|
|
Larry Zhu (LZhu) December 1, 2001 Created
|
|
|
|
Environment:
|
|
|
|
User Mode
|
|
|
|
Revision History:
|
|
|
|
--*/
|
|
|
|
#include "precomp.hxx"
|
|
#pragma hdrstop
|
|
|
|
#include "injecter.hxx"
|
|
|
|
DWORD
|
|
AdjustDebugPriv(
|
|
VOID
|
|
)
|
|
{
|
|
HANDLE hToken = NULL;
|
|
DWORD dwErr = 0;
|
|
TOKEN_PRIVILEGES newPrivs = {0};
|
|
|
|
if (!OpenProcessToken(
|
|
GetCurrentProcess(),
|
|
TOKEN_ADJUST_PRIVILEGES,
|
|
&hToken))
|
|
{
|
|
dwErr = GetLastError();
|
|
goto Cleanup;
|
|
}
|
|
|
|
if (!LookupPrivilegeValue(
|
|
NULL,
|
|
SE_DEBUG_NAME,
|
|
&newPrivs.Privileges[0].Luid
|
|
))
|
|
{
|
|
dwErr = GetLastError();
|
|
goto Cleanup;
|
|
}
|
|
|
|
newPrivs.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
|
|
newPrivs.PrivilegeCount = 1;
|
|
|
|
if (!AdjustTokenPrivileges(
|
|
hToken,
|
|
FALSE,
|
|
&newPrivs,
|
|
0,
|
|
NULL,
|
|
NULL
|
|
))
|
|
{
|
|
dwErr = GetLastError();
|
|
goto Cleanup;
|
|
}
|
|
|
|
Cleanup:
|
|
|
|
if (hToken)
|
|
{
|
|
CloseHandle(hToken);
|
|
}
|
|
|
|
return dwErr;
|
|
}
|
|
|
|
static DWORD
|
|
WorkerFunc(
|
|
IN REMOTE_INFO* pInfo
|
|
)
|
|
{
|
|
HINSTANCE hDll = NULL;
|
|
PFuncRunIt_t pFuncRunit = NULL;
|
|
DWORD dwErr = -1;
|
|
|
|
hDll = pInfo->pFuncLoadLibrary(pInfo->szDllName);
|
|
|
|
if (hDll != NULL)
|
|
{
|
|
pFuncRunit = (PFuncRunIt_t) pInfo->pFuncGetProcAddress(
|
|
hDll,
|
|
pInfo->szProcName
|
|
);
|
|
|
|
if (pFuncRunit != NULL)
|
|
{
|
|
dwErr = pFuncRunit(
|
|
pInfo->cbParameters,
|
|
pInfo->Parameters
|
|
);
|
|
}
|
|
else
|
|
{
|
|
dwErr = -3;
|
|
}
|
|
|
|
//
|
|
// ERROR_NO_MORE_USER_HANDLES unload repeatedly
|
|
// ERROR_SERVER_HAS_OPEN_HANDLES no unload at all
|
|
// others unload once
|
|
//
|
|
|
|
if (ERROR_SERVER_HAS_OPEN_HANDLES != dwErr)
|
|
{
|
|
pInfo->pFuncFreeLibrary(hDll);
|
|
|
|
//
|
|
// unload abandoned dll
|
|
//
|
|
|
|
if (ERROR_NO_MORE_USER_HANDLES == dwErr)
|
|
{
|
|
while (pInfo->pFuncFreeLibrary(hDll))
|
|
{
|
|
// repeat FreeLibrary unit it fails
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dwErr = -2;
|
|
}
|
|
|
|
return dwErr;
|
|
}
|
|
|
|
static VOID
|
|
MarkerFunc(
|
|
VOID
|
|
)
|
|
{
|
|
// empty
|
|
}
|
|
|
|
DWORD
|
|
InjectDllToProcess(
|
|
IN HANDLE hProc,
|
|
IN PCSTR pszDllFileName,
|
|
IN ULONG cbParameters,
|
|
IN VOID* pvParameters
|
|
)
|
|
{
|
|
DWORD dwErr = ERROR_SUCCESS;
|
|
|
|
VOID* pRemoteAlloc = NULL;
|
|
HANDLE hRemoteThread = NULL;
|
|
HINSTANCE hKernel32 = NULL;
|
|
HINSTANCE hDll = NULL;
|
|
REMOTE_INFO* pRemInfo = NULL;
|
|
ULONG cbRemInfo = 0;
|
|
|
|
ULONG ulFuncSize = 0;
|
|
ULONG ulBytesToAlloc = 0;
|
|
CHAR szDllPath[MAX_PATH] = {0};
|
|
SIZE_T dwBytesWritten = 0;
|
|
DWORD dwIgnored;
|
|
ULONG cbPadding = sizeof(ULONG);
|
|
|
|
cbRemInfo = sizeof(REMOTE_INFO) + cbParameters | sizeof(VOID*); // add a safe zone
|
|
pRemInfo = (REMOTE_INFO*) new CHAR [cbRemInfo];
|
|
|
|
if (!pRemInfo)
|
|
{
|
|
dwErr = ERROR_OUTOFMEMORY;
|
|
}
|
|
|
|
RtlZeroMemory(pRemInfo, cbRemInfo);
|
|
pRemInfo->cbParameters = cbParameters;
|
|
|
|
hKernel32 = LoadLibraryA("Kernel32");
|
|
if (!hKernel32)
|
|
{
|
|
dwErr = GetLastError();
|
|
goto Cleanup;
|
|
}
|
|
|
|
pRemInfo->pFuncLoadLibrary = (PFuncLoadLib_t) GetProcAddress(hKernel32, "LoadLibraryA");
|
|
if (!pRemInfo->pFuncLoadLibrary)
|
|
{
|
|
dwErr = GetLastError();
|
|
goto Cleanup;
|
|
}
|
|
|
|
pRemInfo->pFuncGetProcAddress = (PFuncGetProcAddr_t) GetProcAddress(hKernel32, "GetProcAddress");
|
|
if (!pRemInfo->pFuncGetProcAddress)
|
|
{
|
|
dwErr = GetLastError();
|
|
goto Cleanup;
|
|
}
|
|
|
|
pRemInfo->pFuncFreeLibrary = (PFuncFreeLib_t) GetProcAddress(hKernel32, "FreeLibrary");
|
|
if (!pRemInfo->pFuncFreeLibrary)
|
|
{
|
|
dwErr = GetLastError();
|
|
goto Cleanup;
|
|
}
|
|
|
|
if (0 == GetModuleFileNameA(NULL, szDllPath, sizeof (szDllPath)))
|
|
{
|
|
dwErr = GetLastError();
|
|
goto Cleanup;
|
|
}
|
|
|
|
strcpy(strrchr (szDllPath, '\\') + 1, pszDllFileName);
|
|
strncpy(pRemInfo->szDllName, szDllPath, sizeof(pRemInfo->szDllName));
|
|
|
|
strncpy(pRemInfo->szProcName, REMOTE_DLL_ENTRY, sizeof(pRemInfo->szProcName));
|
|
|
|
pRemInfo->cbParameters = cbParameters;
|
|
memcpy(&pRemInfo->Parameters, pvParameters, pRemInfo->cbParameters);
|
|
|
|
ulFuncSize = (ULONG) ((ULONG_PTR) MarkerFunc - (ULONG_PTR) WorkerFunc);
|
|
cbPadding = sizeof(void*) - ulFuncSize % sizeof(void*);
|
|
ulBytesToAlloc = ulFuncSize + cbPadding + cbRemInfo;
|
|
|
|
DebugPrintf(SSPI_LOG, "InjectDllToProcess dllname %s, ulBytesToAlloc %#x, cbRemInfo %#x\n",
|
|
pszDllFileName, ulBytesToAlloc, cbRemInfo);
|
|
DebugPrintf(SSPI_LOG, "pInfo->pFuncLoadLibrary %p\n", pRemInfo->pFuncLoadLibrary);
|
|
DebugPrintf(SSPI_LOG, "pInfo->pFuncGetProcAddress %p\n", pRemInfo->pFuncGetProcAddress);
|
|
DebugPrintf(SSPI_LOG, "pInfo->pFuncFreeLibrary %p\n", pRemInfo->pFuncFreeLibrary);
|
|
|
|
DebugPrintf(SSPI_LOG, "pInfo->szDllName %s\n", pRemInfo->szDllName);
|
|
|
|
DebugPrintf(SSPI_LOG, "pInfo->szProcName %s\n", pRemInfo->szProcName);
|
|
|
|
DebugPrintf(SSPI_LOG, "pInfo->cbParameters %#x, pInfo->Parameters %p\n",
|
|
pRemInfo->cbParameters, pRemInfo->Parameters);
|
|
DebugPrintHex(SSPI_LOG, "Parameters", pRemInfo->cbParameters, pRemInfo->Parameters);
|
|
|
|
pRemoteAlloc = VirtualAllocEx(
|
|
hProc,
|
|
NULL,
|
|
ulBytesToAlloc,
|
|
MEM_COMMIT,
|
|
PAGE_READWRITE
|
|
);
|
|
if (pRemoteAlloc == NULL)
|
|
{
|
|
dwErr = GetLastError();
|
|
goto Cleanup;
|
|
}
|
|
|
|
if (!WriteProcessMemory(
|
|
hProc,
|
|
pRemoteAlloc,
|
|
(PVOID) WorkerFunc,
|
|
ulFuncSize + cbPadding,
|
|
&dwBytesWritten
|
|
))
|
|
{
|
|
dwErr = GetLastError();
|
|
goto Cleanup;
|
|
}
|
|
|
|
if (!WriteProcessMemory(
|
|
hProc,
|
|
((UCHAR*) pRemoteAlloc) + ulFuncSize + cbPadding,
|
|
pRemInfo,
|
|
cbRemInfo,
|
|
&dwBytesWritten
|
|
))
|
|
{
|
|
dwErr = GetLastError();
|
|
goto Cleanup;
|
|
}
|
|
|
|
DebugPrintf(SSPI_LOG,
|
|
"InjectDllToProcess pRemoteAlloc %p, pRemInfo %p\n",
|
|
pRemoteAlloc, ((UCHAR*) pRemoteAlloc) + ulFuncSize + cbPadding);
|
|
|
|
hRemoteThread = CreateRemoteThread(
|
|
hProc,
|
|
NULL,
|
|
0,
|
|
(PTHREAD_START_ROUTINE) pRemoteAlloc, // start address
|
|
((UCHAR*) pRemoteAlloc) + ulFuncSize + cbPadding, // parameter
|
|
0, // creation flags
|
|
&dwIgnored
|
|
);
|
|
if (!hRemoteThread)
|
|
{
|
|
dwErr = GetLastError();
|
|
goto Cleanup;
|
|
}
|
|
|
|
//
|
|
// run Init()
|
|
//
|
|
|
|
dwErr = WaitForSingleObject(hRemoteThread, INFINITE);
|
|
|
|
if (dwErr != ERROR_SUCCESS)
|
|
{
|
|
DebugPrintf(SSPI_ERROR, "WaitForSingleObject failed with %#x\n", dwErr);
|
|
dwErr = GetLastError();
|
|
}
|
|
|
|
Cleanup:
|
|
|
|
if (hKernel32)
|
|
{
|
|
FreeLibrary(hKernel32);
|
|
}
|
|
if (hRemoteThread)
|
|
{
|
|
CloseHandle(hRemoteThread);
|
|
}
|
|
|
|
if (pRemoteAlloc)
|
|
{
|
|
VirtualFreeEx(hProc, pRemoteAlloc, 0, MEM_RELEASE);
|
|
}
|
|
|
|
if (pRemInfo)
|
|
{
|
|
delete [] pRemInfo;
|
|
}
|
|
|
|
return dwErr;
|
|
}
|
|
|
|
DWORD
|
|
FindPid(
|
|
IN PCSTR pszImageFileName
|
|
)
|
|
{
|
|
NTSTATUS Status ;
|
|
|
|
PSYSTEM_PROCESS_INFORMATION pSystemInfo = NULL;
|
|
PSYSTEM_PROCESS_INFORMATION pWalk = NULL;
|
|
ANSI_STRING AnsiProcessName = {0};
|
|
UNICODE_STRING ProcessName = {0};
|
|
DWORD ulPid = 0;
|
|
|
|
DebugPrintf(SSPI_LOG, "FindPid looking for %s\n", pszImageFileName);
|
|
|
|
pSystemInfo = new SYSTEM_PROCESS_INFORMATION[1024];
|
|
|
|
if ( !pSystemInfo )
|
|
{
|
|
DebugPrintf(SSPI_ERROR, "FindPid out of memory\n");
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
Status = NtQuerySystemInformation(
|
|
SystemProcessInformation,
|
|
pSystemInfo,
|
|
sizeof( SYSTEM_PROCESS_INFORMATION ) * 1024,
|
|
NULL
|
|
);
|
|
|
|
if ( !NT_SUCCESS( Status ) )
|
|
{
|
|
DebugPrintf(SSPI_ERROR, "NtQuerySystemInformation failed with %#x\n", Status);
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
RtlInitAnsiString(&AnsiProcessName, pszImageFileName);
|
|
Status = RtlAnsiStringToUnicodeString(&ProcessName, &AnsiProcessName, TRUE);
|
|
|
|
if ( !NT_SUCCESS( Status ) )
|
|
{
|
|
DebugPrintf(SSPI_ERROR, "RtlAnsiStringToUnicodeString failed with %#x\n", Status);
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
pWalk = pSystemInfo ;
|
|
|
|
while ( RtlCompareUnicodeString( &pWalk->ImageName, &ProcessName, TRUE ) != 0 )
|
|
{
|
|
if ( pWalk->NextEntryOffset == 0 )
|
|
{
|
|
pWalk = NULL ;
|
|
break;
|
|
}
|
|
|
|
pWalk = (PSYSTEM_PROCESS_INFORMATION) ((PUCHAR) pWalk + pWalk->NextEntryOffset );
|
|
}
|
|
|
|
if ( !pWalk )
|
|
{
|
|
delete [] pSystemInfo;
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
ulPid = PtrToUlong( pWalk->UniqueProcessId );
|
|
|
|
delete [] pSystemInfo;
|
|
|
|
return ulPid;
|
|
}
|
|
|
|
void
|
|
DisplayUsage(
|
|
IN PCSTR pszApp,
|
|
IN OPTIONAL PCSTR pszArgs
|
|
)
|
|
{
|
|
DebugPrintf(SSPI_ERROR, "\n\nUsage: %s <injectee.dll> [-p<PID>|-n<process name>] %s\n\n\n", pszApp, pszArgs ? pszArgs : "");
|
|
}
|
|
|
|
DWORD
|
|
Init(
|
|
IN PCSTR pszFileName,
|
|
IN ULONG argc,
|
|
IN PCSTR* argv,
|
|
IN PCSTR pszDllName,
|
|
IN PCSTR pszRunItProcName,
|
|
IN PCSTR pszInitProcName,
|
|
OUT ULONG* pcbParameters,
|
|
OUT VOID** ppvParameters
|
|
)
|
|
{
|
|
DWORD dwErr = ERROR_SUCCESS;
|
|
|
|
HINSTANCE hDll = NULL;
|
|
PFuncRunIt_t pFuncRunIt = NULL;
|
|
PFuncInit_t pFuncInit = NULL;
|
|
|
|
BOOLEAN bUseDefaulInitHandler = FALSE;
|
|
|
|
hDll = LoadLibraryA(pszDllName);
|
|
if (!hDll)
|
|
{
|
|
dwErr = GetLastError();
|
|
goto Cleanup;
|
|
}
|
|
pFuncRunIt = (PFuncRunIt_t) GetProcAddress(hDll, pszRunItProcName);
|
|
if (!pFuncRunIt)
|
|
{
|
|
dwErr = GetLastError();
|
|
goto Cleanup;
|
|
}
|
|
pFuncInit = (PFuncInit_t) GetProcAddress(hDll, pszInitProcName);
|
|
if (!pFuncInit)
|
|
{
|
|
dwErr = GetLastError();
|
|
DebugPrintf(SSPI_WARN, "Init failed to locate %s %#x\n", REMOTE_DLL_INIT, dwErr);
|
|
if (dwErr == ERROR_PROC_NOT_FOUND)
|
|
{
|
|
bUseDefaulInitHandler = TRUE;
|
|
}
|
|
else
|
|
{
|
|
goto Cleanup;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dwErr = pFuncInit(
|
|
argc,
|
|
argv,
|
|
pcbParameters,
|
|
ppvParameters
|
|
);
|
|
if (dwErr == ERROR_CONTINUE)
|
|
{
|
|
bUseDefaulInitHandler = TRUE;
|
|
}
|
|
else if (dwErr == ERROR_INVALID_PARAMETER)
|
|
{
|
|
DisplayUsage(pszFileName, (PSTR) (*ppvParameters));
|
|
goto Cleanup;
|
|
}
|
|
else if (dwErr != ERROR_SUCCESS)
|
|
{
|
|
DebugPrintf(SSPI_ERROR, "Init faield with %#x\n", dwErr);
|
|
goto Cleanup;
|
|
}
|
|
}
|
|
|
|
if (bUseDefaulInitHandler)
|
|
{
|
|
dwErr = InitDefaultHandler(argc, argv, pcbParameters, ppvParameters);
|
|
}
|
|
else
|
|
{
|
|
dwErr = ERROR_SUCCESS;
|
|
}
|
|
|
|
Cleanup:
|
|
|
|
if (hDll)
|
|
{
|
|
FreeLibrary(hDll);
|
|
}
|
|
|
|
return dwErr;
|
|
}
|
|
|
|
int
|
|
InitDefaultHandler(
|
|
IN ULONG argc,
|
|
IN PCSTR* argv,
|
|
OUT ULONG* pcbParameters,
|
|
OUT VOID** ppvParameters
|
|
)
|
|
{
|
|
DWORD dwErr = ERROR_SUCCESS;
|
|
|
|
CHAR Parameters[REMOTE_PACKET_SIZE] = {0};
|
|
ULONG cbBuffer = sizeof(Parameters);
|
|
VOID* pvParameters = NULL;
|
|
ULONG cbParameters = 0;
|
|
|
|
DebugPrintf(SSPI_LOG, "InitDefaultHandler is used\n");
|
|
|
|
*pcbParameters = 0;
|
|
*ppvParameters = NULL;
|
|
|
|
for (ULONG i = 0; i < (ULONG) argc; i++)
|
|
{
|
|
cbBuffer -= _snprintf(Parameters + sizeof(Parameters) - cbBuffer, cbBuffer, "%s", argv[i]);
|
|
cbBuffer--; // add a null
|
|
}
|
|
|
|
cbBuffer--; // the last null of multi-sz string
|
|
|
|
cbParameters = sizeof(Parameters) - cbBuffer;
|
|
|
|
pvParameters = new CHAR[cbParameters];
|
|
|
|
if (pvParameters)
|
|
{
|
|
memcpy(pvParameters, Parameters, cbParameters);
|
|
|
|
*ppvParameters = pvParameters;
|
|
pvParameters = NULL;
|
|
|
|
*pcbParameters = cbParameters;
|
|
}
|
|
else
|
|
{
|
|
dwErr = ERROR_OUTOFMEMORY;
|
|
goto Cleanup;
|
|
}
|
|
|
|
Cleanup:
|
|
|
|
if (pvParameters)
|
|
{
|
|
delete [] pvParameters;
|
|
}
|
|
|
|
return dwErr;
|
|
}
|
|
|
|
int __cdecl
|
|
main(
|
|
IN int argc,
|
|
IN CHAR* argv[]
|
|
)
|
|
{
|
|
DWORD dwErr = ERROR_SUCCESS;
|
|
|
|
DWORD dwIgnored;
|
|
HANDLE hLsassProc = NULL;
|
|
HANDLE hReceiveThread = NULL;
|
|
|
|
ULONG ulPid = 0;
|
|
VOID* pvParameters = NULL;
|
|
ULONG cbParamerter = 0;
|
|
|
|
ULONG ulStart = 2;
|
|
PSTR pszProcess = "lsass.exe";
|
|
|
|
if (argc >= 3)
|
|
{
|
|
if ((argv[2][0] == '-') || (argv[2][0] == '/'))
|
|
{
|
|
if (argv[2][1] == 'n')
|
|
{
|
|
ulStart++;
|
|
pszProcess = argv[2] + 2 * sizeof(char);
|
|
}
|
|
else if (argv[2][1] == 'p')
|
|
{
|
|
ulStart++;
|
|
ulPid = strtol(argv[2] + 2 * sizeof(char), NULL, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
/* allow the user to override settings with command line switches */
|
|
for (i = 1; i < argc; i++)
|
|
{
|
|
if ((*argv[i] == '-') || (*argv[i] == '/'))
|
|
{
|
|
switch (tolower(*(argv[i]+1)))
|
|
{
|
|
case 'd':
|
|
fEncode = 0;
|
|
break;
|
|
case 'e':
|
|
fEncode = 1;
|
|
break;
|
|
case 'i':
|
|
fFixedStyle = 0;
|
|
break;
|
|
case 'f':
|
|
pszFileName = argv[i] + 2;
|
|
break;
|
|
case 'h':
|
|
case '?':
|
|
default:
|
|
Usage(argv[0]);
|
|
}
|
|
}
|
|
else
|
|
Usage(argv[0]);
|
|
}
|
|
#endif 0
|
|
|
|
if ((argc >= 2) && (ulPid == 0))
|
|
{
|
|
ulPid = FindPid(pszProcess);
|
|
}
|
|
|
|
if (!ulPid)
|
|
{
|
|
DisplayUsage(argv[0], NULL);
|
|
DebugPrintf(SSPI_ERROR, "%s failed: pszProcess is \"%s\", ulPid is %#x\n", argv[0], pszProcess, ulPid);
|
|
goto Cleanup;
|
|
}
|
|
|
|
DebugPrintf(SSPI_LOG, "%s is injecting %s with pid %#x(%d)\n", argv[0], pszProcess, ulPid, ulPid);
|
|
|
|
dwErr = Init(argv[0],
|
|
argc - ulStart,
|
|
(PCSTR*) reinterpret_cast<PSTR*>(argv + ulStart),
|
|
argv[1],
|
|
REMOTE_DLL_ENTRY,
|
|
REMOTE_DLL_INIT,
|
|
&cbParamerter,
|
|
&pvParameters);
|
|
if (dwErr != ERROR_SUCCESS)
|
|
{
|
|
DebugPrintf(SSPI_ERROR, "Init failed with error %#x\n", dwErr);
|
|
goto Cleanup;
|
|
}
|
|
|
|
dwErr = AdjustDebugPriv();
|
|
|
|
if (dwErr != ERROR_SUCCESS)
|
|
{
|
|
DebugPrintf(SSPI_ERROR, "EnableDebugPriv failed with error %#x\n", dwErr);
|
|
goto Cleanup;
|
|
}
|
|
|
|
hLsassProc = OpenProcess(MAXIMUM_ALLOWED, FALSE, ulPid);
|
|
|
|
if (!hLsassProc)
|
|
{
|
|
DebugPrintf(SSPI_ERROR, "OpenProcess pid %#x failed with last error %#x\n", ulPid, GetLastError());
|
|
goto Cleanup;
|
|
}
|
|
|
|
dwErr = InjectDllToProcess(
|
|
hLsassProc,
|
|
argv[1],
|
|
cbParamerter,
|
|
pvParameters
|
|
);
|
|
if (dwErr != ERROR_SUCCESS)
|
|
{
|
|
DebugPrintf(SSPI_ERROR, "InjectDllToProcess failed with status %#x, dwErr %#x\n", dwErr);
|
|
goto Cleanup;
|
|
}
|
|
|
|
Cleanup:
|
|
|
|
if (pvParameters)
|
|
{
|
|
delete [] pvParameters;
|
|
}
|
|
|
|
if (hLsassProc)
|
|
{
|
|
CloseHandle(hLsassProc);
|
|
}
|
|
|
|
if (hReceiveThread)
|
|
{
|
|
CloseHandle(hReceiveThread);
|
|
}
|
|
|
|
return ERROR_SUCCESS;
|
|
}
|
|
|
|
|