1. UniqueProcessId 구하기.
* PEPROCESS+0x80(오프셋이 항상 이 값 보다는 크므로) 부터 +PAGE_SIZE까지
Traverse한다.
* PEPROCESS는 IoGetCurrentProcess()를 이용하고 PID는 PsGetCurrentProcessId()를 이용하여 구한다.
* for문으로 4의 배수에 맞추어 검색하여 PID와 같은 값이 발견되면 그 뒤에(뒤에 바로 ActiveProcessLinks가 나오므로) ActiveProcessLinks의 포인터가 Valid한 VA인지 확인하고, 다시 내 EPROCESS구조체로 link되는지 체크하여 정확하게 찾아낸다.
대략적인 C 코드는 아래와 같습니다.
VOID CalcPIDOffset(VOID)
{
PEPROCESS Proc = IoGetCurrentProcess();
HANDLE PID = PsGetCurrentProcessId();
PVOID ThisEntry = NULL, NextEntry = NULL;
int i = 0;
for(i = 0x80; i < PAGE_SIZE - 8; i += 4)
{
if(*(PHANDLE)((PCHAR)Proc + i) == PID)
{
// ActiveProcessLinks Offset is PID Offset+4
NextEntry = *(PVOID *)((PCHAR)Proc + i + 8);
if(MmIsAddressValid(NextEntry))
{
ThisEntry = *(PVOID *)NextEntry;
if((PCHAR)ThisEntry == (PCHAR)Proc + i + 4)
{
// Probably, this is PID offset
PID_OFFSET = i;
return;
}
}
}
}
PID_OFFSET = 0x1ec; // Exception case, so, put Offset as WinNT/2003(generic).
}
* 모든 상황에 대처할 수 있도록, 예외 처리로는 NT/2003의 PID_OFFSET을 대입하게 하였습니다.
2. Process Name Offset 구하기.
PEPROCESS의 프로세스 이름 Offset을 따내는 방법입니다. 아래와 같이 딸 수 있습니다.
VOID CalcProcessNameOffset(VOID)
{
PEPROCESS SysProc = PsInitialSystemProcess;
int i = 0;
for(i = 0; i < PAGE_SIZE; i++)
{
if(strncmp((PCHAR)SysProc + i, "System", 6) == 0)
{
IMAGE_NAME_OFFSET = i;
return;
}
}
}
* PsInitialSystemProcess의 PEPROCESS를 트레버싱하여 System 문자열과 일치하는 offset을 찾습니다.
3. Handle Table Offset 구하기.
PEPROCESS의 Handle Table Offset을 구하는 법입니다. Handle Table이란 EPROCESS의
아래 필드를 의미합니다.
lkd> dt !_EPROCESS 8a2407f8
ntdll!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x06c ProcessLock : _EX_PUSH_LOCK
+0x070 CreateTime : _LARGE_INTEGER 0x0
+0x078 ExitTime : _LARGE_INTEGER 0x0
+0x080 RundownProtect : _EX_RUNDOWN_REF
+0x084 UniqueProcessId : 0x00000004
+0x088 ActiveProcessLinks : _LIST_ENTRY [ 0x8a10d2e8 - 0x8056a558 ]
+0x090 QuotaUsage : [3] 0
+0x09c QuotaPeak : [3] 0
+0x0a8 CommitCharge : 7
+0x0ac PeakVirtualSize : 0x759000
+0x0b0 VirtualSize : 0x1d0000
+0x0b4 SessionProcessLinks : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x0bc DebugPort : (null)
+0x0c0 ExceptionPort : (null)
+0x0c4 ObjectTable : 0xe1001e00 Ptr32 _HANDLE_TABLE
...
이 것을 구하는 방법은 크게 두가지를 생각해볼 수 있습니다.
1. 다른 필드로부터 델타(Delta; 오프셋 차이값)을 계산하여 그 값을 가산함으로써 오프셋을 얻는 방법.
-> 이 방법을 사용하려면 많은 테스트가 이루어져야 하며, 불안정하기 때문에 여기서는 사용하지 않겠습니다. 이 방법을 사용하려면 모든 버젼에서 해당 차이값이 동일하다는 확실한 근거가 있어야합니다.
2. 해당 필드의 구조를 이용하는 방법.
-> _HANDLE_TABLE은 아래와 같은 구조로 되어있습니다.
lkd> dt !_HANDLE_TABLE 0xe1001e00
ntdll!_HANDLE_TABLE
+0x000 TableCode : 0xe1890001
+0x004 QuotaProcess : (null)
+0x008 UniqueProcessId : 0x00000004
+0x00c HandleTableLock : [4] _EX_PUSH_LOCK
+0x01c HandleTableList : _LIST_ENTRY [ 0xe18640d4 - 0x8056b848 ]
+0x024 HandleContentionEvent : _EX_PUSH_LOCK
+0x028 DebugInfo : (null)
+0x02c ExtraInfoPages : 0
+0x030 FirstFree : 0x570
+0x034 LastFree : 0xc58
+0x038 NextHandleNeedingPool : 0x1000
+0x03c HandleCount : 387
+0x040 Flags : 0
+0x040 StrictFIFO : 0y0
보이십니까? UniqueProcessId를 0x08번지에 가지고 있는걸 볼 수 있습니다.
이를 이용하여 핸들 테이블 Offset을 구할 수 있고, 그 오프셋으로부터의 Delta값을 이용하여 다른 필드도 구할 수 있을겁니다.(아까도 당부했지만, Delta값을 이용한 방법은 여러 OS에서 테스트를 해봐야합니다.)
이를 이용한 C 코드는 아래와 같습니다.
VOID CalcHandleTableOffset(VOID)
{
PEPROCESS Proc = IoGetCurrentProcess();
HANDLE PID = PsGetCurrentProcessId();
PVOID GuessHandleTable = NULL;
int i = 0;
for(i = 0; i < PAGE_SIZE; i+=4)
{
GuessHandleTable = *(PVOID *)((PCHAR)Proc + i);
if(MmIsAddressValid(GuessHandleTable))
{
if(*(PHANDLE)((PCHAR)GuessHandleTable + 0x08) == PID)
{
HANDLE_TABLE_OFFSET = i;
return;
}
}
}
}
4. ActiveProcessLinks
UniqueProcessId Offset + 4를 하면 ActiveProcessLinks Offset(LIST_ENTRY)가 나옵니다.
이는 NT 계열에서는 모두 동일(NT4~Vista에서 필자가 확인)하기 때문에, 사용해도 됩니다.
ActiveProcessLinks_Offset = PID_OFFSET + 4;
5. DebugPort
주로 프로세스가 디버깅중인지 커널단에서 강력하게 확인할 때 사용되어집니다.
디버거가 활성화 되어있다면 이 값은 NULL이 아닌 값이 되므로 체크할 때 사용됩니다.
오프셋은 핸들 테이블의 오프셋 - 0x08으로 아래와 같이 쓸 수 있습니다.
DEBUG_PORT_OFFSET = HANDLE_TABLE_OFFSET - 0x08;
**********************************************************************
이 부분은 논외이지만, 심심하므로(?) 내가 원하는 Process의 PEPROCESS를 얻어오는 방법에 대해서 간단하게 생각해봅시다.
우선 Native API를 이용하는 방법으로 PsLookupProcessByProcessId()를 이용해볼 수 있습니다. 이 함수는 PspCidTable에서 Process를 찾아내어 반환하는 역할을 합니다. 따라서 가장 저 수준의 역할을 하는 Native API로 이를 후킹하면 프로세스 접근을 강력하게 막을수도 있을것입니다. 또한 기존 핸들로부터 얻을수도 있을겁니다. 이는 NtDuplicateObject와 NtQuerySystemInformation을 이용하면 됩니다. 그 후, ObReferenceObjectByHandle을 하면 얻을 수 있을겁니다.
(윈도우에서는 CSRSS.exe가 모든 프로세스에 대한 핸들을 하나씩 가지고 있으므로 이를 이용하면 NtOpenProcess를 거치지 않고도 열 수 있습니다.)
이번엔 커널 구조체를 이용하는 방법들로, 우선 ActiveProcessLinks를 Traversing 하는 방법이 있겠습니다. 가장 보편적인 방법이며, 윈도우가 이 방법을 사용하고 있습니다. 또한 보통 프로세스를 숨길때는 이를 끊어서 목록에서 안보이게 합니다.
이 것 외에도 Handle Table의 Link를 이용하거나, 직접 PspCidTable의 포인터를 구해내어(PsLookupProcessByProcessId를 traverse) 직접 찾아내거나, 그 테이블을 조작해서 핸들을 얻어낸 후 ObReferenceObjectByHandle()을 한다면 PEPROCESS를 얻을 수 있을겁니다.