Post

Memory Descriptor List

MDL (Memory Descriptor List)

가상 메모리 주소는 항상 연속적인 반면, 물리 메모리 주소는 연속성이 보장되지 않습니다. MDL은 이러한 불연속적인 물리 메모리 주소를 추적하는데 사용됩니다.

아래는 MSDN에서 설명하는 MDL에 대한 내용입니다.

1
An I/O buffer that spans a range of contiguous virtual memory addresses can be spread over several physical pages, and these pages can be discontiguous. The operating system uses a memory descriptor list (MDL) to describe the physical page layout for a virtual memory buffer.

MDL structure (wdm.h)

MDL은 다음과 같은 구조로 구성되어 있습니다.

1
2
3
4
5
6
7
8
9
10
typedef struct _MDL {
  struct _MDL      *Next;
  CSHORT           Size;
  CSHORT           MdlFlags;
  struct _EPROCESS *Process;
  PVOID            MappedSystemVa;
  PVOID            StartVa;
  ULONG            ByteCount;
  ULONG            ByteOffset;
} MDL, *PMDL;

해당 구조체는 일부분만 공개되어 있으며, 나머지는 불투명하여 NextMdlFlags를 제외한 멤버에는 직접적으로 엑세스하는 것을 권장하지 않습니다.

1
2
3
An MDL structure is semi-opaque. Your driver should directly access only the Next and MdlFlags members of this structure. For a code example that uses these two members, see the following Example section.

The remaining members of an MDL are opaque. Do not access the opaque members of an MDL directly. 

대신에 다음과 같이 일부 멤버에 대한 매크로 기능을 제공합니다.

1
2
3
4
5
6
7
Instead, use the following macros, which the operating system provides to perform basic operations on the structure:

MmGetMdlVirtualAddress returns the virtual memory address of the I/O buffer that is described by the MDL.

MmGetMdlByteCount returns the size, in bytes, of the I/O buffer.

MmGetMdlByteOffset returns the offset within a physical page of the beginning of the I/O buffer.

해당 구조체에서 사용되는 멤버들은 다음과 같습니다. IoAllocateMdl을 통해 생성된 메모리를 덤프하여 분석하였습니다.

1
2
3
4
5
6
7
8
9
10
11
12
8: kd> dt_mdl ffffAD0D9CB755B0
nt!_MDL
   +0x000 Next             : (null) 
   +0x008 Size             : 0n72
   +0x00a MdlFlags         : 0n11
   +0x00c AllocationProcessorNumber : 5
   +0x00e Reserved         : 0
   +0x010 Process          : (null) 
   +0x018 MappedSystemVa   : 0xffffbe80`c5420680 Void
   +0x020 StartVa          : 0xfffff807`27963000 Void
   +0x028 ByteCount        : 0x2000
   +0x02c ByteOffset       : 0x680

Next

MDL 체인에 대한 포인터 입니다. list 구조로 구성되어 있으며, 연결되어 있는 Next PMDL에 대한 주소 값을 나타냅니다.

Size

할당된 MDL의 크기 입니다. 해당 값에 구조체 크기만큼 제외하면 나머지는 저장된 물리 메모리의 개수 입니다.

현재 수집된 덤프 기준으로 계산해보면, 현재 MDL에 저장된 물리 메모리 주소는 3개입니다.

1
2
3
PMDL.Size(0x48) - sizeof(MDL, 0x30) = 0x18

0x18 / 0x08 = 0x03

MdlFlags

할당된 MDL 메모리 페이지의 상태를 나타냅니다. 현재 수집된 덤프 기준으로 0x0B(MDL_MAPPED_TO_SYSTEM_VA, MDL_PAGES_LOCKED, MDL_ALLOCATED_FIXED_SIZE) 값을 가지고 있습니다.

Process

EPROCESS 주소 입니다. 어떠한 경우에 사용되는지 확인하지 못했습니다.

MappedSystemVa

MmMapLockedPagesSpecifyCache를 통해 매핑 시 전달받는 가상 메모리 주소입니다. 해당 메모리는 공유 메모리와 같은 개념이며, 해당 메모리를 변경 시 Virtual Memory 및 Physical Memory에도 변경사항이 동일하게 적용됩니다.

해당 메모리 값은 Physical Memory, Virtual Memroy와 동일합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// MappedSystemVa
8: kd> db 0xffffbe80`c5420680
ffffbe80`c5420680  48 83 ec 28 41 b8 4e 6f-6e 65 e8 81 39 65 00 48  H..(A.None..9e.H
ffffbe80`c5420690  83 c4 28 c3 cc cc cc cc-cc cc cc cc cc cc cc cc  ..(.............
ffffbe80`c54206a0  65 4c 8b 04 25 88 01 00-00 41 0f 0d 48 78 41 8b  eL..%....A..HxA.
ffffbe80`c54206b0  50 78 8b ca 81 c9 00 00-00 ff 8b c2 f0 41 0f b1  Px...........A..
ffffbe80`c54206c0  48 78 75 ea c1 ea 18 8b-c2 c3 cc cc cc cc cc cc  Hxu.............
ffffbe80`c54206d0  cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc  ................
ffffbe80`c54206e0  65 4c 8b 04 25 88 01 00-00 c1 e1 18 81 c9 ff ff  eL..%...........
ffffbe80`c54206f0  ff 00 41 0f 0d 48 78 41-8b 40 78 8b d1 23 d0 f0  ..A..HxA.@x..#..

// Physical Address
8: kd> db /p 2b63000+680
00000000`02b63680  48 83 ec 28 41 b8 4e 6f-6e 65 e8 81 39 65 00 48  H..(A.None..9e.H
00000000`02b63690  83 c4 28 c3 cc cc cc cc-cc cc cc cc cc cc cc cc  ..(.............
00000000`02b636a0  65 4c 8b 04 25 88 01 00-00 41 0f 0d 48 78 41 8b  eL..%....A..HxA.
00000000`02b636b0  50 78 8b ca 81 c9 00 00-00 ff 8b c2 f0 41 0f b1  Px...........A..
00000000`02b636c0  48 78 75 ea c1 ea 18 8b-c2 c3 cc cc cc cc cc cc  Hxu.............
00000000`02b636d0  cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc  ................
00000000`02b636e0  65 4c 8b 04 25 88 01 00-00 c1 e1 18 81 c9 ff ff  eL..%...........
00000000`02b636f0  ff 00 41 0f 0d 48 78 41-8b 40 78 8b d1 23 d0 f0  ..A..HxA.@x..#..

// Virtual Address
8: kd> db ExAllocatePool
fffff807`27963680  48 83 ec 28 41 b8 4e 6f-6e 65 e8 81 39 65 00 48  H..(A.None..9e.H
fffff807`27963690  83 c4 28 c3 cc cc cc cc-cc cc cc cc cc cc cc cc  ..(.............
fffff807`279636a0  65 4c 8b 04 25 88 01 00-00 41 0f 0d 48 78 41 8b  eL..%....A..HxA.
fffff807`279636b0  50 78 8b ca 81 c9 00 00-00 ff 8b c2 f0 41 0f b1  Px...........A..
fffff807`279636c0  48 78 75 ea c1 ea 18 8b-c2 c3 cc cc cc cc cc cc  Hxu.............
fffff807`279636d0  cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc  ................
fffff807`279636e0  65 4c 8b 04 25 88 01 00-00 c1 e1 18 81 c9 ff ff  eL..%...........
fffff807`279636f0  ff 00 41 0f 0d 48 78 41-8b 40 78 8b d1 23 d0 f0  ..A..HxA.@x..#..

StartVa

대상 가상 메모리의 페이지 시작 주소입니다. ByteOffset만큼 더하면 IoAllocateMdl 호출 시 전달한 VirtualAddress 값하고 동일합니다.

1
2
3
4
5
8: kd> db 0xfffff807`27963000+680
fffff807`27963680  48 83 ec 28 41 b8 4e 6f-6e 65 e8 81 39 65 00 48  H..(A.None..9e.H

8: kd> db ExAllocatePool
fffff807`27963680  48 83 ec 28 41 b8 4e 6f-6e 65 e8 81 39 65 00 48  H..(A.None..9e.H

ByteCount

ByteCount는 대상 메모리의 크기 입니다. IoAllocateMdl 호출 시 전달되는 Length 값하고 동일합니다.

ByteOffset

ByteOffset은 해당 주소의 오프셋 값입니다. 페이지 시작 주소로부터 해당 값을 더하면 대상 메모리 주소가 됩니다.

Physical Address

MDL에서 처리하는 실제 메모리 주소는 다음과 같이 PMDL 끝에 저장되어 있습니다. Size 크기를 통해 저장된 물리 메모리 주소의 개수를 파악할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
8: kd> dt_mdl ffffAD0D9CB755B0
nt!_MDL
   +0x000 Next             : (null) 
   +0x008 Size             : 0n72
   +0x00a MdlFlags         : 0n11
   +0x00c AllocationProcessorNumber : 5
   +0x00e Reserved         : 0
   +0x010 Process          : (null) 
   +0x018 MappedSystemVa   : 0xffffbe80`c5420680 Void
   +0x020 StartVa          : 0xfffff807`27963000 Void
   +0x028 ByteCount        : 0x2000
   +0x02c ByteOffset       : 0x680

8: kd> dqs ffffAD0D9CB755B0+30 L? 3
ffffad0d`9cb755e0  00000000`00002b63
ffffad0d`9cb755e8  00000000`00002b64
ffffad0d`9cb755f0  00000000`00002b65

해당 물리 메모리와 가상 메모리를 비교해보면 동일합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
8: kd> db /p 2b63000+680
00000000`02b63680  48 83 ec 28 41 b8 4e 6f-6e 65 e8 81 39 65 00 48  H..(A.None..9e.H
00000000`02b63690  83 c4 28 c3 cc cc cc cc-cc cc cc cc cc cc cc cc  ..(.............
00000000`02b636a0  65 4c 8b 04 25 88 01 00-00 41 0f 0d 48 78 41 8b  eL..%....A..HxA.
00000000`02b636b0  50 78 8b ca 81 c9 00 00-00 ff 8b c2 f0 41 0f b1  Px...........A..
00000000`02b636c0  48 78 75 ea c1 ea 18 8b-c2 c3 cc cc cc cc cc cc  Hxu.............
00000000`02b636d0  cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc  ................
00000000`02b636e0  65 4c 8b 04 25 88 01 00-00 c1 e1 18 81 c9 ff ff  eL..%...........
00000000`02b636f0  ff 00 41 0f 0d 48 78 41-8b 40 78 8b d1 23 d0 f0  ..A..HxA.@x..#..

8: kd> db ExAllocatePool
fffff807`27963680  48 83 ec 28 41 b8 4e 6f-6e 65 e8 81 39 65 00 48  H..(A.None..9e.H
fffff807`27963690  83 c4 28 c3 cc cc cc cc-cc cc cc cc cc cc cc cc  ..(.............
fffff807`279636a0  65 4c 8b 04 25 88 01 00-00 41 0f 0d 48 78 41 8b  eL..%....A..HxA.
fffff807`279636b0  50 78 8b ca 81 c9 00 00-00 ff 8b c2 f0 41 0f b1  Px...........A..
fffff807`279636c0  48 78 75 ea c1 ea 18 8b-c2 c3 cc cc cc cc cc cc  Hxu.............
fffff807`279636d0  cc cc cc cc cc cc cc cc-cc cc cc cc cc cc cc cc  ................
fffff807`279636e0  65 4c 8b 04 25 88 01 00-00 c1 e1 18 81 c9 ff ff  eL..%...........
fffff807`279636f0  ff 00 41 0f 0d 48 78 41-8b 40 78 8b d1 23 d0 f0  ..A..HxA.@x..#..

Inline hooking in the kernel

커널 함수는 기본적으로 Read Only 페이지 속성을 가지고 있으며, 이를 변경하기 위해서는 다음과 같이 CR0의 WP를 비활성화해야 합니다. 아래 코드는 UnknownCheats에 게시된 예제 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include "nt.h"
#include "hook.h"
 
/// <summary>
/// Place an inline hook on given address in any driver
/// </summary>
/// <param name="HookData">Pointer to a HOOK_DATA structure</param>
/// <returns>Hook status code</returns>
HOOK_STATUS PlaceDriverInlineHook(PHOOK_DATA HookData)
{
	/*
		mov rax, 0h
		jmp rax
		mov rax, 0h
		call rax
	*/
	BYTE shellcode[24] = { 0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0, 0x48, 0xB8, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xD0 };
 
	// Check if any of the given addresses are invalid
	if (!MmIsAddressValid(HookData->Address) || !MmIsAddressValid(HookData->HookAddress) || !MmIsAddressValid(HookData->OriginalAssemblyAddress))
		return HOOK_INVALID_PARAMETER;
 
	// Copy the addresses into the shellcode
	RtlCopyMemory(shellcode + 0x02, &HookData->HookAddress, sizeof(HookData->HookAddress));
	RtlCopyMemory(shellcode + 0x0E, &HookData->OriginalAssemblyAddress, sizeof(HookData->OriginalAssemblyAddress));
 
	if (HookData->DisableWP)
	{
		// Flip the 16th bit of cr0 to 0
		ULONG cr0 = __readcr0();
		cr0 = cr0 & ~0x10000;
		__writecr0(cr0);
	}
 
	// Copy the shellcode to the destination
	RtlCopyMemory(HookData->Address, &shellcode, sizeof(shellcode));
 
	if (HookData->DisableWP)
	{
		// Flip the 16th bit of cr0 to 1
		ULONG cr0 = __readcr0();
		cr0 = cr0 | 0x10000;
		__writecr0(cr0);
	}
 
	HookData->ReturnAddress = (DWORD64)HookData->Address + 0xC;
 
	return HOOK_STATUS_SUCCESS;
}

또는 다음과 같이 MDL을 이용하면 더욱 간단하고, 안전하게 보호된 메모리를 변경할 수 있습니다.

1
2
3
4
5
6
7
8
9
First of all, you shouldn't use CR0 because it disables WP only on current core, but scheduler can replace your thread to another core at any moment. To disable WP on all cores you should call it in interprocessor interruption by KeIpiGenericCall. Or, if you really want to do this and wants to prevent cores switching, just set your thread affinity to only one core by KeSetSystemAffinityThread, disable interrupts or raise IRQL.

Anyway, all of this are VERY BAD techniques for writing readonly memory.
To change readable memory you should to do this:
1. Allocate new MDL for specified address by IoAllocateMdl
2. Lock pages in RAM to prevent swapping by MmProbeAndLockPages
3. Map locked pages with RW-rights by MmMapLockedPagesSpecifyCache
4. Write your hook to mapped memory
5. MmUnmapLockedPages/MmUnlockPages/IoFreeMdl

IoAllocateMdl

가장 먼저 IoAllocateMdl를 통해 가상 메모리에 대한 MDL을 할당합니다.

1
2
3
4
5
6
7
PMDL IoAllocateMdl(
  [in, optional]      __drv_aliasesMem PVOID VirtualAddress,
  [in]                ULONG                  Length,
  [in]                BOOLEAN                SecondaryBuffer,
  [in]                BOOLEAN                ChargeQuota,
  [in, out, optional] PIRP                   Irp
);

MmProbeAndLockPages

MmProbeAndLockPages를 통해 해당 페이지가 스와핑 또는 페이지 아웃되지 않도록 잠금합니다.

1
2
3
4
5
void MmProbeAndLockPages(
  [in, out] PMDL            MemoryDescriptorList,
  [in]      KPROCESSOR_MODE AccessMode,
  [in]      LOCK_OPERATION  Operation
);

MmMapLockedPagesSpecifyCache

MmMapLockedPagesSpecifyCache를 통해 VA와 PA를 MappedSystemVa에 매핑합니다. 이는 공유 메모리와 같은 개념입니다. 해당 메모리를 수정 시 변경된 사항이 VA 및 PA에 전부 반영됩니다.

1
2
3
4
5
6
7
8
PVOID MmMapLockedPagesSpecifyCache(
  [in]           PMDL                                                                          MemoryDescriptorList,
  [in]           __drv_strictType(KPROCESSOR_MODE / enum _MODE,__drv_typeConst)KPROCESSOR_MODE AccessMode,
  [in]           __drv_strictTypeMatch(__drv_typeCond)MEMORY_CACHING_TYPE                      CacheType,
  [in, optional] PVOID                                                                         RequestedAddress,
  [in]           ULONG                                                                         BugCheckOnFailure,
  [in]           ULONG                                                                         Priority
);

References

This post is licensed under CC BY 4.0 by the author.