오늘 'Paradox Conference 2011'에서 윈도우즈 물리 메모리 포렌식과 관련하여 발표를 하고 왔습니다. 아래는 발표자료 다운로드 링크입니다.

Download


재미있었는지는 모르겠지만, 경청해주신 많은 분들께 감사드립니다.



윈도우즈 시스템에서의 물리 메모리는 일반적으로 win32dd 또는 mdd 등의 도구를 이용하여 수집합니다. 이러한 도구들은 여러가지 방법을 이용하여 물리 메모리에 접근하게 되는데, 이번 포스팅은 윈도우즈 시스템에서 물리 메모리에 접근하여 이를 덤프하는 방법에 대한 글입니다.



1. \Device\PhysicalMemory를 이용한 방법

'\Device\PhysicalMemory' 개체는 물리 메모리에 접근하기 위해 사용되는 개체이며, 가장 일반적으로 사용되는 방법입니다. 이 방법을 사용하는 대표적인 도구가 바로 mdd입니다. (win32dd는 이 방법 및 앞으로 설명될 다른 방법들을 옵션으로 제공합니다.)

mdd는 '\Device\PhysicalMemory' 섹션의 핸들을 커널레벨에서 얻어온 뒤, 이를 유저레벨에서 특정 메모리주소에 맵핑 후 접근하는 방식을 취하고 있습니다. win32dd는 커널레벨에서 모든 작업을 한다는 점이 다릅니다.

아래는 mdd의 커널 드라이버 소스 중 일부입니다.

UNICODE_STRING usPhysicalMemory;
OBJECT_ATTRIBUTES oa;
 
RtlInitUnicodeString( &usPhysicalMemory, L"\\Device\\PhysicalMemory"); 

InitializeObjectAttributes( &oa, &usPhysicalMemory,
        OBJ_CASE_INSENSITIVE,
        NULL, NULL );   
status = ZwOpenSection( &hPhysicalMemory, SECTION_MAP_READ, &oa );
if( !NT_SUCCESS(status) )
{
  DbgPrint("Failed to open %wZ! Status %p\n", usPhysicalMemory, status);
  hPhysicalMemory = NULL;
  goto done;
}
DbgPrint("Opened section handle %p in driver\n", hPhysicalMemory);
 
*(HANDLE*)pIrp->AssociatedIrp.SystemBuffer = hPhysicalMemory;
pIrp->IoStatus.Information = 4;
status = 0;


mdd는 위의 코드를 이용하여 물리 메모리의 핸들을 얻어온 뒤, MapViewOfFile() 함수를 이용하여 가상 주소에 맵핑하여 이를 파일에 쓰는 작업을 합니다.


2. MmGetPhysicalMemoryRanges()를 이용한 방법

다음 방법은 undocumented kernel function인 MmGetPhysicalMemoryRanges()를 이용한 방법입니다. 이 함수는 PHYSICAL_MEMORY_RANGE 구조체 배열의 시작 주소를 리턴하는데, 그 원형은 다음과 같습니다.

typedef struct _PHYSICAL_MEMORY_RANGE {
    PHYSICAL_ADDRESS BaseAddress;
    LARGE_INTEGER NumberOfBytes;
} PHYSICAL_MEMORY_RANGE, *PPHYSICAL_MEMORY_RANGE;

NTKERNELAPI
PPHYSICAL_MEMORY_RANGE
MmGetPhysicalMemoryRanges (
    VOID
    );

PHYSICAL_MEMORY_RANGE의 BaseAddress와 NumberOfBytes 필드를 MmMapIoSpace() 함수의 파라메터로 이용하여 가상 주소로  맵핑할 수 있습니다. MmMapIoSpace() 함수의 원형은 다음과 같습니다.


PVOID 
MmMapIoSpace( 
    IN PHYSICAL_ADDRESS PhysicalAddress,
    IN ULONG NumberOfBytes,
    IN MEMORY_CACHING_TYPE CacheType 
    );

유의하실 점은, PHYSICAL_MEMORY_RANGE 구조체의 마지막 노드는 BaseAddress와 NumberOfBytes 필드가 null이라는 것입니다.


3. MmMapMemoryDumpMdl()을 이용한 방법

MmMapMemoryDumpMdl() 함수는 undocumented kernel function이며 크래시 덤프를 위해 사용되는 함수입니다. win32dd가 기본적으로 이 함수를 이용하며, 그 원형은 다음과 같습니다.

typedef struct _MDL {
    struct _MDL *Next;
    CSHORT Size;
    CSHORT MdlFlags;
    struct _EPROCESS *Process;
    PVOID MappedSystemVa;
    PVOID StartVa;
    ULONG ByteCount;
    ULONG ByteOffset;
} MDL, *PMDL;

NTKERNELAPI
VOID
MmMapMemoryDumpMdl (
    __inout PMDL MemoryDumpMdl
    );

MDL 구조체는 특정 물리 메모리 주소를 서술하기 위한 구조체이며, MmMapMemoryDumpMdl()의 입출력 파라메터로 사용됩니다. 가상 주소로의 맵핑을 원하는 Page Frame Number를 MDL 구조체의 뒤의 4바이트에 넣으면, MappedSystemVa 필드에 맵핑된 주소가 리턴되어 출력됩니다. 이를 이용하여 함수를 작성해보면 다음과 같습니다.


typedef struct _MY_MDL {
 MDL Mdl;
 DWORD_PTR PageFrameNumber;
} MY_MDL, *PMY_MDL;

PVOID GetMappedAddr(
        IN ULONG PageFrameNumber
        )
{
 MY_MDL mymdl;
 mymdl.Mdl.Next = NULL;
 mymdl.Mdl.Size = 0x20; // MDL size 0x1C + 4 bytes
 mymdl.Mdl.MappedSystemVa = NULL;
 mymdl.Mdl.StartVa = NULL;
 mymdl.Mdl.ByteCount = SIZE_MEM_PAGE; // 4096 bytes
 mymdl.Mdl.ByteOffset = 0;
 mymdl.PageFrameNumber = PageFrameNumber;

 MmMapMemoryDumpMdl(&mymdl.Mdl);

 return mymdl.Mdl.MappedSystemVa;
}

입력을 위한 물리 페이지 번호의 범위는 ZwQuerySystemInformation 함수를 이용하여 구합니다. 참고로 리턴되는 주소는 항상 0xFFBF0000를 가리키며, 이는 크래시 덤프 드라이버를 위해 예약된 주소와 일치입니다.

유의해야 할 부분은, 위 주소를 ZwWriteFile 함수로 직접 접근해 파일로 출력해서는 안되며, ExAllocatePoolWithTag 등의 함수로 할당 받은 영역으로 memcpy로 복사 후 이용해야 한다는 점입니다. (직접 접근하는 순간 BSOD를...)



맺음말 : 얼마전 제가 공개한 Fastdd는 바로 MmMapMemoryDumpMdl() 함수를 이용하였습니다. 제가 테스트 해본 결과로는 지금까지 공개된 윈도우즈 물리 메모리 덤프도구 중에서는 가장 빠릅니다. 필요하신 분들의 많은 활용이 있었으면 합니다.