Hacking & Security/Kernel2009. 6. 22. 13:11

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를 얻을 수 있을겁니다.

Posted by skensita