In the first part we looked at creating a new kernel object type. In the second part we implemented creation of new DataStack objects and opening existing objects by name. In this part, we’ll implement the main functionality of a DataStack, that makes a DataStack what it is.
첫 번째 파트에서는 새로운 커널 객체 유형을 생성하는 방법을 살펴봤습니다. 두 번째 파트에서는 새 데이터스택 객체를 생성하고 기존 객체를 이름으로 여는 방법을 구현했습니다. 이번 파트에서는 데이터스택을 구성하는 데이터스택의 주요 기능을 구현해 보겠습니다.
Before we get on with that, there is one correction I must make. A good comment from Luke (@lukethezoid) on X said that although the code returns a 32-bit HANDLE
to 32-bit callers, it’s not nearly enough because all pointers passed using NtDeviceIoControlFile
/DeviceIoControl
would be wrong – 32-bit pointers would be passed to the 64-bit kernel and that would cause a crash unless we do some work. For example, a UNICODE_STRING
provided by a 32-bit client has a different binary layout than a 64-bit one. Wow64 processes (32-bit x86 running on x64) have two versions of NtDll.Dll in their address space. One is the “native” 64-bit, and the other is a special 32-bit variant.
그 이야기를 시작하기 전에 한 가지 정정해야 할 것이 있습니다. 코드가 32비트 호출자에게 32비트 HANDLE을
반환하지만, NtDeviceIoControlFile/DeviceIoControl을
사용하여 전달된 모든 포인터가 잘못되어 32비트 포인터가 64비트 커널로 전달되고 일부 작업을 하지 않으면 충돌이 발생할 수 있다는 Luke(@lukethezoid)의 좋은 의견에 따라 충분하지 않다고 말했습니다. 예를 들어 32비트 클라이언트에서 제공하는 UNICODE_STRING은
64비트와 다른 바이너리 레이아웃을 가지고 있습니다. Wow64 프로세스(x64에서 실행되는 32비트 x86)의 주소 공간에는 두 가지 버전의 NtDll.dll이 있습니다. 하나는 "기본" 64비트이고 다른 하나는 특수 32비트 변형입니다.
I say “special” because it’s not the same NtDll.Dll you would find on a true 32-bit system. This is because this special DLL knows it’s on a 64-bit system, and provides conversions for parameters (pointer and structures) before invoking the “real” 64-bit DLL API.
"특별하다"고 말하는 이유는 실제 32비트 시스템에서 찾을 수 있는 NtDll.dll이 아니기 때문입니다. 이 특별한 DLL은 64비트 시스템임을 인식하고 "실제" 64비트 DLL API를 호출하기 전에 매개변수(포인터 및 구조체)에 대한 변환을 제공하기 때문입니다.
Here is a snapshot from Process Explorer showing a 32-bit process with two NtDll.Dll files – the native 64-bit loaded into a high address, while the other loaded into a low address (within the first 4GB of address space):
다음은 프로세스 탐색기의 스냅샷으로, 네이티브 64비트는 높은 주소에 로드되고 다른 하나는 낮은 주소(처음 4GB의 주소 공간 내)에 로드된 두 개의 NtDll.dll 파일로 구성된 32비트 프로세스를 보여줍니다:
The changes required to support Wow64 processes are not difficult to make, but not too interesting, either, so I won’t be implementing them. Instead, 32-bit clients will be blocked from using DataStacks.
Wow64 프로세스를 지원하는 데 필요한 변경 사항은 어렵지 않지만, 그다지 흥미롭지도 않기 때문에 구현하지 않을 것입니다. 대신 32비트 클라이언트는 DataStacks를 사용할 수 없도록 차단됩니다.
We should block on the kernel side for sure, and also in user mode to fail faster. In the kernel, we could use something like this in IRP_MJ_CREATE
or IRP_MJ_DEVICE_CONTROL
handles:
커널 쪽에서 확실히 차단하고 사용자 모드에서도 더 빨리 실패하도록 해야 합니다. 커널에서는 IRP_MJ_CREATE
또는 IRP_MJ_DEVICE_CONTROL
핸들에서 이와 같은 것을 사용할 수 있습니다:
if (IoIs32bitProcess(Irp)) { status = STATUS_NOT_IMPLEMENTED; // complete the request... } |
In user mode, we can prevent DataStack.Dll from loading in the first place in Wow64 processes:
사용자 모드에서는 Wow64 프로세스에서 DataStack.dll이 처음부터 로드되지 않도록 할 수 있습니다:
BOOL APIENTRY DllMain( HMODULE hModule, DWORD reason, LPVOID ) { switch (reason) { case DLL_PROCESS_ATTACH: // C++ 17 if ( BOOL wow; IsWow64Process(GetCurrentProcess(), &wow) && wow) return FALSE; //... |
Note, however, that on true 32-bit systems everything should work just fine, as user mode and kernel mode are bitness-aligned.
그러나 실제 32비트 시스템에서는 사용자 모드와 커널 모드가 비트 단위로 정렬되므로 모든 것이 정상적으로 작동합니다.
Implementing the Actual Data Stack
실제 데이터 스택 구현하기
Now we’re ready to focus on implementing the stack functionality – push, pop, and clear.
이제 푸시, 팝, 지우기 등 스택 기능을 구현하는 데 집중할 준비가 되었습니다.
We’ll start from user mode, and gradually move to kernel mode. First, we want nice APIs for clients to use that have “Win32” conventions like so:
사용자 모드에서 시작하여 점차 커널 모드로 이동하겠습니다. 먼저, 클라이언트가 사용할 수 있는 "Win32" 규칙을 따르는 멋진 API가 필요합니다:
BOOL WINAPI PushDataStack(_In_ HANDLE hDataStack, _In_ const PVOID buffer, _In_ DWORD size); BOOL WINAPI PopDataStack(_In_ HANDLE hDataStack, _Out_ PVOID buffer, _Inout_ DWORD * size); BOOL WINAPI ClearDataStack(_In_ HANDLE hDataStack); |
The APIs return BOOL
to indicate success/failure, and GetLastError
could be used to get additional information in case of an error. Let’s start with push:
API는 성공/실패를 나타내는 BOOL을
반환하며, 오류 발생 시 추가 정보를 얻기 위해 GetLastError를
사용할 수 있습니다. 푸시부터 시작하겠습니다:
BOOL WINAPI PushDataStack( HANDLE hDataStack, const PVOID buffer, DWORD size) { auto status = NtPushDataStack(hDataStack, buffer, size); if (!NT_SUCCESS(status)) SetLastError(RtlNtStatusToDosError(status)); return NT_SUCCESS(status); } |
Nothing to it. Call NtPushDataStack
and update the last error is needed. NtPushDataStack
just packs the arguments in a helper structure and sends to the driver, just like we did with CreateDataStack
and OpenDataStack
:
아무것도 없습니다. NtPushDataStack을
호출하고 마지막 오류를 업데이트해야 합니다. NtPushDataStack은
CreateDataStack
및 OpenDataStack에서
했던 것처럼 인수를 헬퍼 구조에 패킹하여 드라이버로 전송합니다:
NTSTATUS NTAPI NtPushDataStack(_In_ HANDLE DataStackHandle, _In_ const PVOID Item, _In_ ULONG ItemSize) { DataStackPush data; data.DataStackHandle = DataStackHandle; data.Buffer = Item; data.Size = ItemSize; IO_STATUS_BLOCK ioStatus; return NtDeviceIoControlFile(g_hDevice, nullptr , nullptr , nullptr , &ioStatus, IOCTL_DATASTACK_PUSH, &data, sizeof (data), nullptr , 0); } |
Now we switch to the kernel side. The DeviceIoControl
handler just forwards the arguments to the “real” NtPushDataStack
:
이제 커널 측으로 전환합니다. DeviceIoControl
핸들러는 인수를 "실제" NtPushDataStack으로
전달하기만 하면 됩니다:
case IOCTL_DATASTACK_PUSH: { auto data = (DataStackPush*)Irp->AssociatedIrp.SystemBuffer; if (dic.InputBufferLength < sizeof(*data)) { status = STATUS_BUFFER_TOO_SMALL; break; } status = NtPushDataStack(data->DataStackHandle, data->Buffer, data->Size); break; } |
Now we’re getting to the interesting parts. First, we need to check if the arguments make sense:
이제 흥미로운 부분으로 넘어갑니다. 먼저 인수가 타당한지 확인해야 합니다:
NTSTATUS NTAPI NtPushDataStack( HANDLE DataStackHandle, const PVOID Item, ULONG ItemSize) { if (ItemSize == 0) return STATUS_INVALID_PARAMETER_3; if (!ARGUMENT_PRESENT(Item)) return STATUS_INVALID_PARAMETER_2; |
If the pushed data size is zero or the pointer to the data is NULL
, then it’s an error. The ARGUMENT_PRESENT
macro returns true if the given pointer is not NULL
. Notice that we can return specific “invalid parameter” error based on the parameter index. Unfortunately, user mode callers always get a generic ERROR_INVALID_PARAMETER
regardless. But at least kernel callers can benefit from the extra detail.
푸시된 데이터 크기가 0이거나 데이터에 대한 포인터가 NULL이면
오류입니다. 주어진 포인터가 NULL이
아닌 경우 ARGUMENT_PRESENT
매크로는 참을 반환합니다. 매개변수 인덱스에 따라 특정 "유효하지 않은 매개변수" 오류를 반환할 수 있다는 점에 유의하세요. 안타깝게도 사용자 모드 호출자는 이와 관계없이 항상 일반 ERROR_INVALID_PARAMETER를
반환합니다. 하지만 적어도 커널 호출자는 추가 세부 정보를 통해 이점을 얻을 수 있습니다.
Next, we need to get to the DataStack object corresponding to the given handle; in fact, the handle could be bad, or not point to a DataStack object at all. This is a job for ObReferenceObjectByHandle
or one of its variants:
다음으로, 주어진 핸들에 해당하는 DataStack 객체로 이동해야 하는데, 실제로 핸들이 잘못되었거나 아예 DataStack 객체를 가리키지 않을 수도 있습니다. 이 작업은 ObReferenceObjectByHandle
또는 그 변형 중 하나에 대한 작업입니다:
DataStack* ds; auto status = ObReferenceObjectByHandleWithTag(DataStackHandle, DATA_STACK_PUSH, g_DataStackType, ExGetPreviousMode(), DataStackTag, ( PVOID *)&ds, nullptr ); if (!NT_SUCCESS(status)) return status; |
ObReferenceObjectByHandleWithTag
attempts to retrieve the object pointer given a handle, a type object, and access. The added tag provides a simple way to “track” the caller taking the reference. I’ve defined DataStackTag
to be used for dynamic memory allocations as well (we’ll do very soon):
ObReferenceObjectByHandleWithTag
는 핸들, 타입 객체 및 액세스가 주어진 객체 포인터를 검색하려고 시도합니다. 추가된 태그는 참조를 받는 호출자를 '추적'하는 간단한 방법을 제공합니다. 동적 메모리 할당에도 DataStackTag를
사용하도록 정의했습니다(곧 추가할 예정입니다):
const ULONG DataStackTag = 'ktsD' ; |
It’s the same kind of tag typically provided to allocation functions. You can read the tag from right to left (that’s how it would be displayed in tools as we’ll see later) – “Dstk”, kind of short for “Data Stack”.
일반적으로 할당 함수에 제공되는 것과 같은 종류의 태그입니다. 태그는 오른쪽에서 왼쪽으로 읽을 수 있으며(나중에 살펴보겠지만 도구에 표시되는 방식입니다), "데이터 스택"의 줄임말인 "Dstk"입니다.
The function also checks if the handle has the required access mask (DATA_STACK_PUSH
in this case), and will fail if the handle is not powerful enough. ExGetPreviousMode
is provided to the function to indicate who is the original caller – for kernel callers, any access will be granted (the access mask is not really used).
이 함수는 또한 핸들에 필요한 액세스 마스크(
이 경우DATA_STACK_PUSH
)가 있는지 확인하고, 핸들이 충분히 강력하지 않은 경우 실패합니다. 원래 호출자가 누구인지 표시하기 위해 함수에 ExGetPreviousMode가
제공되며, 커널 호출자의 경우 모든 액세스 권한이 부여됩니다(액세스 마스크는 실제로 사용되지 않음).
With the object in hand, we can do the work, delegated to a separate function, and then not to forget to dereference the object or it will leak:
개체를 손에 들고 별도의 함수에 위임하여 작업을 수행한 다음 개체를 참조 해제하는 것을 잊지 않으면 개체가 유출될 수 있습니다:
status = DsPushDataStack(ds, Item, ItemSize); ObDereferenceObjectWithTag(ds, DataStackTag); return status; |
Technically, we don’t have to use a separate function, but it mimics how the kernel works for complex objects: there is another layer of implementation that works with the object directly (no more handles involved).
기술적으로는 별도의 함수를 사용할 필요는 없지만 복잡한 객체에 대해 커널이 작동하는 방식을 모방한 것으로, 객체와 직접적으로 작동하는 또 다른 구현 계층이 있습니다(더 이상 핸들이 개입되지 않음).
The DataStack
structure mentioned in the previous parts holds the data for the implementation, and will be treated as a classic C structure – no member functions just to mimic how the Windows kernel is implemented – almost everything in C, not C++:
앞부분에서 언급한 DataStack
구조는 구현을 위한 데이터를 담고 있으며, 윈도우 커널이 구현되는 방식을 모방하기 위한 멤버 함수가 없는 고전적인 C 구조로 취급되며, 거의 모든 것이 C++가 아닌 C로 구현됩니다:
struct DataStack { LIST_ENTRY Head; FAST_MUTEX Lock; ULONG Count; ULONG MaxItemCount; ULONG_PTR Size; ULONG MaxItemSize; ULONG_PTR MaxSize; }; struct DataBlock { LIST_ENTRY Link; ULONG Size; UCHAR Data[1]; }; |
The “stack” will be managed by a linked list (the classic LIST_ENTRY
and friends) implementation provided by the kernel. We’ll push by adding to the tail and pop by removing from the tail.
'스택'은 커널에서 제공하는 링크된 목록(클래식 리스트_엔트리와
친구들) 구현에 의해 관리됩니다. 꼬리에 추가하여 푸시하고 꼬리에서 제거하여 팝할 것입니다.
(Clearly, it would be just as easy to implement a queue, rather than, or in addition to, a stack.) We need a lock to prevent corruption of the list, and a fast mutex will do the job. Count
stores the number of items currently on the data stack, and Size
has the total size in bytes. MaxSize
and MaxItemCount
are initialized when the DataStack is created and provide some control over the limits of the data stack. Values of zero indicate no special limit.
(물론 스택 대신 또는 스택과 함께 큐를 구현하는 것이 훨씬 더 쉬울 것입니다.) 목록의 손상을 방지하기 위해 잠금이 필요하며, 빠른 뮤텍스가 이 작업을 수행합니다. Count는
현재 데이터 스택에 있는 항목의 수를 저장하고, Size는
총 크기를 바이트 단위로 저장합니다. MaxSize와
MaxItemCount는
데이터 스택이 생성될 때 초기화되며 데이터 스택의 한계를 어느 정도 제어할 수 있습니다. 0 값은 특별한 제한이 없음을 나타냅니다.
The second structure, DataBlock
is the one that holds the actual data, along with its size, and of course a link to the list. Data[1]
is just a placeholder, where the data is going to be copied to, assuming we allocate the correct size.
두 번째 구조인 DataBlock은
실제 데이터와 그 크기, 그리고 목록에 대한 링크를 담고 있는 구조입니다. Data[1]
은 올바른 크기를 할당한다고 가정할 때 데이터가 복사될 자리 표시자일 뿐입니다.
We’ll start the push implementation in an optimistic manner, and allocate the DataBlock
structure with the required size based on the data provided:
낙관적인 방식으로 푸시 구현을 시작하고 제공된 데이터에 따라 필요한 크기의 DataBlock
구조를 할당합니다:
NTSTATUS DsPushDataStack(DataStack* ds, PVOID Item, ULONG ItemSize) { auto buffer = (DataBlock*)ExAllocatePool2(POOL_FLAG_PAGED | POOL_FLAG_UNINITIALIZED, ItemSize + sizeof (DataBlock), DataStackTag); if (buffer == nullptr ) return STATUS_INSUFFICIENT_RESOURCES; |
We use the (relatively) new ExAllocatePool2
API to allocate the memory block (ExAllocatePoolWithTag
is deprecated from Windows version 2004, but you can use it with an old-enough WDK or if you turn off the deprecation warning). We allocate the buffer uninitialized, as we’ll copy the data to it very soon, so no need for zeroed buffer.
메모리 블록을 할당하기 위해 (비교적) 새로운 ExAllocatePool2
API를 사용합니다(ExAllocatePoolWithTag는
Windows 버전 2004에서 더 이상 사용되지 않지만, 충분히 오래된 WDK를 사용하거나 사용 중단 경고를 해제하면 사용할 수 있습니다). 곧 데이터를 복사할 것이므로 버퍼를 초기화하지 않은 상태로 할당하므로 버퍼를 0으로 만들 필요가 없습니다.
Technically, we allocate one extra byte beyond what we need, but that’s not a big deal. Now we can copy the data from the client’s provided buffer, being careful to probe user-mode buffers under exception protection:
기술적으로는 필요한 것보다 1바이트가 더 할당되지만 큰 문제는 아닙니다. 이제 예외 보호가 적용되는 사용자 모드 버퍼를 조심하면서 클라이언트가 제공한 버퍼에서 데이터를 복사할 수 있습니다:
auto status = STATUS_SUCCESS; if (ExGetPreviousMode() != KernelMode) { __try { ProbeForRead(Item, ItemSize, 1); memcpy (buffer->Data, Item, ItemSize); } __except (EXCEPTION_EXECUTE_HANDLER) { ExFreePool(buffer); return GetExceptionCode(); } } else { memcpy (buffer->Data, Item, ItemSize); } buffer->Size = ItemSize; |
If an exception occurs because of a bad user-mode buffer, we can return the exception code and that’s it. Note that ProbeForRead
does not work for kernel addresses, and cannot prevent crashes – this is intentional. Only user-mode buffers are out of control from the kernel’s side.
잘못된 사용자 모드 버퍼로 인해 예외가 발생하면 예외 코드를 반환하면 됩니다. ProbeForRead는
커널 주소에 대해서는 작동하지 않으며 충돌을 방지할 수 없다는 점에 유의하세요. 사용자 모드 버퍼만 커널 측에서 제어할 수 없습니다.
Now we can add the item to the stack if the limits are not violated, while updating the DataStack’s stats:
이제 제한을 위반하지 않은 경우 항목을 스택에 추가하는 동시에 데이터스택의 통계를 업데이트할 수 있습니다:
ExAcquireFastMutex(&ds->Lock); do { if (ds->MaxItemCount == ds->Count) { status = STATUS_NO_MORE_ENTRIES; break ; } if (ds->MaxItemSize && ItemSize > ds->MaxItemSize) { status = STATUS_NOT_CAPABLE; break ; } if (ds->MaxSize && ds->Size + ItemSize > ds->MaxSize) { status = STATUS_NOT_CAPABLE; break ; } } while ( false ); if (NT_SUCCESS(status)) { InsertTailList(&ds->Head, &buffer->Link); ds->Count++; ds->Size += ItemSize; } ExReleaseFastMutex(&ds->Lock); if (!NT_SUCCESS(status)) ExFreePool(buffer); return status; |
First, we acquire the fast mutex to prevent data races. I opted not to use any C++ RAII type here to make things as clear as possible – we have to be careful not to return before releasing the fast mutex. Next, the do
/while
non-loop is used to check if any setting is violated, in which case the status is set to some failure. The status values I chose may not look perfect – the right thing to do is create new NTSTATUS
values that would be specific for DataStacks, but I was too lazy. The interested reader/coder is welcome to do it right.
먼저, 데이터 경합을 방지하기 위해 빠른 뮤텍스를 확보합니다. 빠른 뮤텍스를 해제하기 전에 반환되지 않도록 주의해야 하므로 가능한 한 명확하게 하기 위해 여기서는 C++ RAII 유형을 사용하지 않기로 했습니다. 다음으로, do/while
논루프를 사용하여 설정 위반 여부를 확인하고, 이 경우 상태를 일부 실패로 설정합니다. 제가 선택한 상태 값은 완벽하지 않을 수 있습니다. 데이터스택에 맞는 새로운 NTSTATUS
값을 만드는 것이 옳지만, 제가 너무 게을렀습니다. 관심 있는 독자/코더가 올바르게 작성해 주시면 감사하겠습니다.
Inserting the item involves calling InsertTailList
, and then just updating the item count and total byte size. If anything fails, we are careful to free the buffer to prevent a memory leak. This is for push.
항목을 삽입하려면 InsertTailList를
호출한 다음 항목 수와 총 바이트 크기를 업데이트하기만 하면 됩니다. 실패할 경우 메모리 누수를 방지하기 위해 버퍼를 해제하도록 주의합니다. 이것은 푸시용입니다.
Popping Items 팝핑 항목
The pop operation works along similar lines. In this case, the client asks to pop an item but needs to provide a large-enough buffer to store the data.
팝 작업도 비슷한 방식으로 작동합니다. 이 경우 클라이언트는 항목 팝을 요청하지만 데이터를 저장할 수 있는 충분한 용량의 버퍼를 제공해야 합니다.
We’ll use an additional size pointer argument, that on input indicates the buffer’s size, and on output indicates the actual item size. First, the “Win32” API:
입력 시에는 버퍼의 크기를 나타내고 출력 시에는 실제 항목 크기를 나타내는 추가 크기 포인터 인수를 사용할 것입니다. 먼저, "Win32" API입니다:
BOOL WINAPI PopDataStack( HANDLE hDataStack, PVOID buffer, DWORD * size) { auto status = NtPopDataStack(hDataStack, buffer, size); if (!NT_SUCCESS(status)) SetLastError(RtlNtStatusToDosError(status)); return NT_SUCCESS(status); } |
Just delegating the work to the native API, which forwards to the kernel with a helper structure:
헬퍼 구조로 커널에 전달되는 네이티브 API에 작업을 위임하기만 하면 됩니다:
NTSTATUS NTAPI NtPopDataStack(_In_ HANDLE DataStackHandle, _In_ PVOID Buffer, _Inout_ PULONG ItemSize) { DataStackPop data; data.DataStackHandle = DataStackHandle; data.Buffer = Buffer; data.Size = ItemSize; IO_STATUS_BLOCK ioStatus; return NtDeviceIoControlFile(g_hDevice, nullptr , nullptr , nullptr , &ioStatus, IOCTL_DATASTACK_POP, &data, sizeof (data), nullptr , 0); } |
This should be expected by now. On the kernel side, things are more interesting. First, get the object based on the handle, then send it to the lower-layer function if successful:
이쯤 되면 예상할 수 있는 일입니다. 커널 쪽에서는 상황이 더 흥미롭습니다. 먼저 핸들을 기반으로 객체를 가져온 다음 성공하면 하위 계층 함수로 보냅니다:
NTSTATUS NTAPI NtPopDataStack( HANDLE DataStackHandle, PVOID Buffer, PULONG BufferSize) { if (!ARGUMENT_PRESENT(BufferSize)) return STATUS_INVALID_PARAMETER_3; ULONG size; if (ExGetPreviousMode() != KernelMode) { __try { ProbeForRead(BufferSize, sizeof ( ULONG ), 1); size = *BufferSize; } __except (EXCEPTION_EXECUTE_HANDLER) { return GetExceptionCode(); } } else { size = *BufferSize; } if (!ARGUMENT_PRESENT(Buffer) && size != 0) return STATUS_INVALID_PARAMETER_2; DataStack* ds; auto status = ObReferenceObjectByHandleWithTag(DataStackHandle, DATA_STACK_POP, g_DataStackType, ExGetPreviousMode(), DataStackTag, ( PVOID *)&ds, nullptr ); if (!NT_SUCCESS(status)) return status; status = DsPopDataStack(ds, Buffer, size, BufferSize); ObDereferenceObjectWithTag(ds, DataStackTag); return status; } |
The input buffer size is extracted, being careful to probe the user mode pointer. The real work is done in DsPopDataStack
. First, take the lock. Second, see if the data stack is empty – if so, no pop operation possible. If the input size is zero, return the size of the top element:
입력 버퍼 크기가 추출되며, 사용자 모드 포인터를 조사하는 데 주의해야 합니다. 실제 작업은 DsPopDataStack에서
이루어집니다. 먼저 잠금을 가져옵니다. 둘째, 데이터 스택이 비어 있는지 확인합니다(비어 있다면 팝 작업이 불가능합니다). 입력 크기가 0이면 최상위 요소의 크기를 반환합니다:
NTSTATUS DsPopDataStack(DataStack* ds, PVOID buffer, ULONG inputSize, ULONG * itemSize) { ExAcquireFastMutex(&ds->Lock); __try { if (inputSize == 0) { // // return size of next item // __try { if (ds->Count == 0) { // // stack empty // *itemSize = 0; } else { auto top = CONTAINING_RECORD(ds->Head.Blink, DataBlock, Link); *itemSize = top->Size; } return STATUS_SUCCESS; } __except (EXCEPTION_EXECUTE_HANDLER) { return GetExceptionCode(); } } |
The locking here works differently than the push implementation by using a __finally
block, which is the one releasing the fast mutex. This ensures that no matter how we leave the __try
block, the lock will be released for sure.
여기서 잠금은 빠른 뮤텍스를 해제하는 __finally
블록을 사용하여 푸시 구현과 다르게 작동합니다. 이렇게 하면 __try
블록을 어떻게 떠나더라도 잠금이 확실히 해제됩니다.
The CONTAINING_RECORD
macro is used correctly to get to the item from the link (LIST_ENTRY
). Technically, in this case we could just make a simple cast, as the LIST_ENTRY
member is the first in a DataBlock
. Notice how we get to the top item: Head.Blink
, which points to the tail (last) item.
링크(LIST_ENTRY
)에서 항목으로 이동하기 위해 CONTAINING_RECORD
매크로가 올바르게 사용됩니다. 기술적으로는 이 경우 LIST_ENTRY
멤버가 DataBlock의
첫 번째 멤버이므로 간단한 형변환을 하면 됩니다. 최상위 항목에 도달하는 방법을 주목하세요: Head.Blink가
맨 아래(마지막) 항목을 가리키고 있습니다.
If the data stack is empty, we place zero in the item size pointer and return an error (abusing yet another existing error):
데이터 스택이 비어 있으면 항목 크기 포인터에 0을 배치하고 오류를 반환합니다(또 다른 기존 오류를 남용합니다):
if (ds->Count == 0) { __try { *itemSize = 0; } __except (EXCEPTION_EXECUTE_HANDLER) { return GetExceptionCode(); } return STATUS_PIPE_EMPTY; } |
If manage to get beyond this point, then there is an item, and we need to remove it, copy the data to the client’s buffer (if it’s big enough), and free the kernel’s copy of the buffer:
이 지점을 넘어가면 항목이 있으므로 이를 제거하고 클라이언트 버퍼에 데이터를 복사한 다음(충분히 큰 경우) 커널의 버퍼 복사본을 비워야 합니다:
auto link = RemoveTailList(&ds->Head); NT_ASSERT(link != &ds->Head); auto item = CONTAINING_RECORD(link, DataBlock, Link); __try { *itemSize = item->Size; if (inputSize < item->Size) { // // buffer too small // reinsert item // InsertTailList(&ds->Head, link); return STATUS_BUFFER_TOO_SMALL; } else { memcpy (buffer, item->Data, item->Size); ds->Count--; ds->Size -= item->Size; ExFreePool(item); return STATUS_SUCCESS; } } __except (EXCEPTION_EXECUTE_HANDLER) { return GetExceptionCode(); } } __finally { ExReleaseFastMutex(&ds->Lock); } |
The call to RemoveTailList
removes the top item from the list. The next assert verifies the list wasn’t empty before the removal (it can’t be as we dealt with that case in the previous code section). Remember, that if a list is empty calling RemoveTailList
or RemoveHeadList
returns the head’s pointer.RemoveTailList를
호출하면 목록에서 최상위 항목이 제거됩니다. 다음 어서트는 제거하기 전에 목록이 비어 있지 않았는지 확인합니다(이전 코드 섹션에서 이 경우를 다루었으므로 비어 있을 수 없습니다). 목록이 비어 있으면 RemoveTailList
또는 RemoveHeadList를
호출하면 헤드의 포인터를 반환한다는 점을 기억하세요.
If the client’s buffer is too small, we reinsert the item back and bail. Otherwise, we copy the data, update the data stack’s stats and free our copy of the item.
클라이언트의 버퍼가 너무 작으면 항목을 다시 삽입한 다음 해제합니다. 그렇지 않으면 데이터를 복사하고 데이터 스택의 통계를 업데이트한 후 아이템의 복사본을 해제합니다.
Cleanup 정리
The stack clear operation is relatively straightforward of them all. Here is the kernel part that matters:
스택 지우기 작업은 그중에서도 비교적 간단합니다. 다음은 중요한 커널 부분입니다:
NTSTATUS DsClearDataStack(DataStack* ds) { ExAcquireFastMutex(&ds->Lock); LIST_ENTRY* link; while ((link = RemoveHeadList(&ds->Head)) != &ds->Head) { auto item = CONTAINING_RECORD(link, DataBlock, Link); ExFreePool(item); } ds->Count = 0; ds->Size = 0; ExReleaseFastMutex(&ds->Lock); return STATUS_SUCCESS; } |
We take the lock, and then go over the list, removing and freeing each item. Finally, we update the stats to zero items and zero bytes.
잠금을 해제하고 목록을 살펴본 다음 각 항목을 제거하고 해제합니다. 마지막으로 통계를 0개의 항목과 0바이트로 업데이트합니다.
Testing 테스트
Here is one way to test – having an executable run twice, the first instance pushes some items, and the second one popping items. main
creates a data stack with a name. If it’s a new object, it assumes the role of “pusher”. Otherwise, it assumes the role of “popper”:
실행 파일을 두 번 실행하여 첫 번째 인스턴스는 일부 항목을 푸시하고 두 번째 인스턴스는 항목을 팝하는 테스트 방법은 다음과 같습니다. main은
이름을 가진 데이터 스택을 생성합니다. 새 객체인 경우 "푸셔" 역할을 맡습니다. 그렇지 않으면 "팝퍼" 역할을 맡습니다:
int main() { HANDLE hDataStack = CreateDataStack( nullptr , 0, 100, 10 << 20, L"MyDataStack" ); if (!hDataStack) { printf ( "Failed to create data stack (%u)\n" , GetLastError()); return 1; } printf ( "Handle created: 0x%p\n" , hDataStack); if (GetLastError() == ERROR_ALREADY_EXISTS) { printf ( "Opened an existing object... will pop elements\n" ); PopItems(hDataStack); } else { Sleep(5000); PushString(hDataStack, "Hello, data stack!" ); PushString(hDataStack, "Pushing another string..." ); for ( int i = 1; i <= 10; i++) { Sleep(100); PushDataStack(hDataStack, &i, sizeof (i)); } } CloseHandle(hDataStack); return 0; } |
When creating a named object, if GetLastError
returns ERROR_ALREADY_EXISTS
, it means a handle is returned to an existing object. In our current implementation, this actually won’t work. We have to fix the CreateDataStack
implementation like so:
명명된 객체를 생성할 때 GetLastError가
ERROR_ALREADY_EXISTS를
반환하면 기존 객체에 대한 핸들이 반환된다는 의미입니다. 현재 구현에서는 실제로 작동하지 않습니다. CreateDataStack
구현을 다음과 같이 수정해야 합니다:
HANDLE hDataStack; auto status = NtCreateDataStack(&hDataStack, &attr, maxItemSize, maxItemCount, maxSize); if (NT_SUCCESS(status)) { const NTSTATUS STATUS_OBJECT_NAME_EXISTS = 0x40000000; if (status == STATUS_OBJECT_NAME_EXISTS) { SetLastError(ERROR_ALREADY_EXISTS); } else { SetLastError(0); } return hDataStack; } |
After calling NtCreateDataStack
we fix the returned “error” if the kernel returns STATUS_OBJECT_NAME_EXISTS
. Now the previous will work correctly.NtCreateDataStack을
호출한 후 커널이 STATUS_OBJECT_NAME_EXISTS를
반환하면 반환된 "오류"를 수정합니다. 이제 이전 방법이 올바르게 작동합니다.
PushString
is a little helper to push strings:푸시스트링은
문자열을 푸시하는 작은 도우미입니다:
bool PushString( HANDLE h, std::string const & text) { auto ok = PushDataStack(h, ( PVOID )text.c_str(), ( ULONG )text.length() + 1); if (!ok) printf ( "Error in PushString: %u\n" , GetLastError()); return ok; } |
Finally, PopItems
does some popping:
마지막으로 팝아이템이
톡톡 튀는 기능을 제공합니다:
void PopItems( HANDLE h) { BYTE buffer[256]; auto tick = GetTickCount64(); while (GetTickCount64() - tick < 10000) { DWORD size = sizeof (buffer); if (!PopDataStack(h, buffer, &size) && GetLastError() != ERROR_NO_DATA) { printf ( "Error in PopDataStack (%u)\n" , GetLastError()); break ; } if (size) { printf ( "Popped %u bytes: " , size); if (size > sizeof ( int )) printf ( "%s\n" , ( PCSTR )buffer); else printf ( "%d\n" , *( int *)buffer); } Sleep(300); } } |
Not very exciting, but is good enough for this simple test. Here is some output, first from the “pusher” and then the “popper”:
그다지 흥미롭지는 않지만 이 간단한 테스트에는 충분합니다. 다음은 '푸셔'와 '팝퍼'의 출력입니다:
E:\Test>DSTest.exe
Handle created: 0x00000000000000F8
E:\Test>DSTest.exe
Handle created: 0x0000000000000104
Opened an existing object... will popup elements
Popped 4 bytes: 2
Popped 4 bytes: 5
Popped 4 bytes: 8
Popped 4 bytes: 10
Popped 4 bytes: 9
Popped 4 bytes: 7
Popped 4 bytes: 6
Popped 4 bytes: 4
Popped 4 bytes: 3
Popped 4 bytes: 1
Popped 26 bytes: Pushing another string...
Popped 19 bytes: Hello, data stack!
What’s Next? 다음 단계는 무엇인가요?
Are we done? Not quite. Astute readers may have noticed a little problem. What happens if a DataStack object is destroyed (e.g., the last handle to it is closed), but the stack is not empty? That memory will leak, as we have no “desctructor”.
다 끝났나요? 아직은 아닙니다. 눈치 빠른 독자라면 작은 문제를 눈치챘을지도 모릅니다. 데이터스택 객체가 소멸되었지만(예: 마지막 핸들이 닫힌 경우) 스택이 비어 있지 않은 경우 어떻게 될까요? "소멸자"가 없으므로 해당 메모리가 누수됩니다.
Running the “pusher” a few times without a second process that pops items results in a leak. Here is my PoolMonX tool showing the leak:
항목을 팝업하는 두 번째 프로세스 없이 "푸셔"를 몇 번 실행하면 누수가 발생합니다. 다음은 누수를 보여주는 PoolMonX 도구입니다:
Notice the “Dstk” tag and the number of allocations being higher that deallocations.
"Dstk" 태그와 할당 취소보다 할당 횟수가 더 많은 것을 확인할 수 있습니다.
Another feature we are missing is the ability to wait on a DataStack until data is available, if the stack is empty, maybe by calling the WaitForSingleObject
API. It would be nice to have that.
또 다른 기능은 스택이 비어 있는 경우 데이터가 사용할 수 있을 때까지 기다리는 기능으로, 아마도 WaitForSingleObject
API를 호출하여 데이터 스택에서 기다릴 수 있는 기능입니다. 이 기능이 있으면 좋을 것 같습니다.
Yet another missing element is the ability to query DataStack objects – how much memory is being used, how many items, etc.
또 다른 누락된 요소는 사용 중인 메모리 양, 항목 수 등 데이터스택 객체를 쿼리할 수 있는 기능입니다.
We’ll deal with these aspects in the next part.
다음 파트에서 이러한 측면을 다루겠습니다.
Pingback에서: 커널 객체 유형 구현 개선하기 (4부) - Pavel Yosifovich