PspCidTable, ObTypeIndexTable, _HANDLE_TABLE_ENTRY -> ObjectPointerBits
숨겨진 프로세스 탐지 방법을 찾던중 PspCidTable이 가지고 있는 핸들 정보를 이용하여 검색하는 방법을 구현하게 되었다.
인터넷에 검색해보면 몇몇 코드가 나오는데 대부분 XP에서 테스트가 된거라 손볼데가 많았다.
1. PspCidTable 주소 찾기
각 OS마다 PspCidTable 주소를 찾기 위해서 PsLookupProcessByProcessId 함수 내부를 패턴으로 검색해서 찾아야한다. Windows 8부터는 함수 내부에서 PspReferenceCidTableEntry 함수를 call하게 되고, 이곳에 PspCidTable 주소가 있다. 8 이전 OS에서는 PsLookupProcessByProcessId 함수 내부에서 바로 찾을 수 있다.
|
PsLookupProcessByProcessId() / PspReferenceCidTableEntry -> PspCidTable |
_HANDLE_TABLE_ENTRY의 Object 필드가 올바른 오브젝트 주소인지 확인하기 위해 _OBJECT_HEADER의 Type이 PsProcessType이 맞는지 검증을 해야하는데, 전에 글에도 썼듯이 Windows 7부터는 Type(_OBJECT_HEADER)필드가 없고 TypeIndex 값만 존재한다.
ObTypeIndexTable 주소를 찾아 TypeIndex와 계산을 하면 _OBJECT_TYPE 주소가 나오는데, ObTypeIndexTable이 export 되지 않기 때문에 ObGetObjectType() 함수 내부에서 모두 패턴으로 찾아야 한다.
|
ObGetObjectType() -> ObTypeIndexTable |
VISTA이하는 해당 함수가 없으며, _OBJECT_HEADER의 Type필드 값이 바로 Object Type 주소이다.
7이상부터는 ObTypeIndexTable 주소를 미리 구한 뒤에, 아래와 같이 계산하여 PsProcessType 주소와 비교한다.
Object Type Address = ObTypeIndexTable + (TypeIndex * Porinter Size) |
※ 추가
위 계산 과정을 수행하는 ObGetObjectType()이라는 함수가 있으며, 인자로 Object 주소를 넣을 경우 Object Type 주소를 리턴한다.
_OBJECT_HEADER의 TypeIndex를 찾아 ObTypeIndexTable과 연산한 값을 리턴한다.
|
|
ObGetObjectType 내부 코드 |
ex)
3. ObjectPointerBits로 주소 재구성
Windows 8 이상부터는 _HANDLE_TABLE_ENTRY 내에 Object 포인터가 바로 들어있지 않다.
8 / 8.1 x64 OS에서 _HANDLE_TABLE_ENTRY를 보면 아래와 같이 Unlocked, RefCnt, Attributes, ObjectPointerBits라는 4개의 필드가 8byte를 bit단위로 나누어 쓰고 있다.
(x86에서는 RefCnt 필드가 제일 마지막에 있지만 x64에서는 위로 올라와있다.)
|
Windows 7 _HANDLE_TABLE_ENTRY |
|
Windows 8 _HANDLE_TABLE_ENTRY |
|
Windows 8.1 _HANDLE_TABLE_ENTRY |
Windows 8 이전 OS에서는 Object 필드에 최하위 1bit인 Lock bit만 제거해주면 바로 Object 주소를 얻을 수 있었지만, 8 이상부터는 Object의 주소를 구하기 위해 4개의 필드를 이용하여 주소를 재구성 해주어야 한다.
주소를 재구성하는 방법은 한 중국 사이트에서 아래와 같은 공식(?)으로 재구성하는 것을 볼 수 있었다.
Object Address = ((ObjectPointerBits >> 19) & (~0xF) | 0xFFFFF00000000000)
어찌어찌 삽질을 해서 Windows 8 / 8.1 x64에서도 동작이 되도록 수정은 했지만 잘 이해가 가지 않는 부분이 있다.
3-1. SHR 19bit
Windows 8 x64 기준으로 언뜻보면 Unlocked, RefCnt 필드를 제거하고 Attributes, ObjectPointerBits의 값만 사용하기 위한 것으로 보이지만, 정확하게는 20bit를 Shift해야 RefCnt까지 제거된다.
물론 >> 20을 하면 주소 계산이 제대로 안된다. (Windows 8.1도 역시 16bit만..)
3-2. AND (~0xF)
Shift 이후에 ~0xF로 마지막 4bit를 날려버리는데, RefCnt의 1bit와 Attributes의 3bit를 날리는 것 같다.
어떻게 이런 계산 방식이 나오는건지 당최..
3-3. OR 0xFFFFF000`00000000
마지막에 0xFFFFF00000000000로 OR 연산을 해주는데, OS마다 상위 8byte가 제대로 나오지 않는 경우가 있다.
위의 의문점들의 답은 PspReferenceCidTableEntry() 함수 내부를 리버싱해보면 찾을 수 있다.
아래는 해당 함수 내부에서 주소를 재구성하는 코드다.
[Windows 8 x86] nt!PspReferenceCidTableEntry: ... 814796c1 83e7f8 and edi, 0FFFFFFF8h ... [Windows 8 x64] nt!PspReferenceCidTableEntry ... fffff802`a088017d 48c1ff13 sar rdi,13h fffff802`a0880181 4883e7f0 and rdi,0FFFFFFFFFFFFFFF0h ... |
[Windows 8.1 x86] nt!PspReferenceCidTableEntry: ... 8131022c 83e3f8 and edi, 0FFFFFFF8h ... [Windows 8.1 x64] nt!PspReferenceCidTableEntry ... fffff802`57e487cc 48c1ff10 sar rdi,10h fffff802`57e487d0 4883e7f0 and rdi,0FFFFFFFFFFFFFFF0h ... |
x64 코드에서는 SAR(부호비트유지)로 쉬프트를 하고 있었다.
코드에서는 ULONG_PTR 변수형을 사용하고 있어서 Shift(SHR)를 할 경우 밀린 bit만큼 0으로 채워져 0xFFFFF00000000000 값을 다시 OR 연산하는 과정이 필요했었던거로 보인다. 하지만 하드코딩된 값은 언제나 위험..
LONG_PTR로 변수 사용시 OR연산은 필요하지 않다.
x86 코드에서는 0xFFFFFFF8과 AND 연산을 통해 Unlocked, Attributes 3bit를 제거하여 Object 주소를 얻는다.
결국 x86이나 x64나 ObjectPointerBits로 주소 연산을 하는 것을 알 수 있다. 다만 x64에서 & (~0xF) 하는 부분이 좀 이상하긴 하지만 현재로서는 정확하게 설명을 못하겠다.
아래는 Windows 8 계열에서 다시 재구성해본 계산방식이다.
LONG_PTR ObjectPointerBits = 0; PVOID pObj = NULL; #ifdef _WIN64 ObjectPointerBits = (*(PLONG_PTR)pHandleTableEntry & 0xFFFFFFFFFFF00000); pObj = ((ObjectPointerBits >> 16) & (~0xF)); #else pObj = (PVOID)((ULONG_PTR)pHandleTableEntry->Object & (~0x7)); #endif |
참조 사이트
1. http://forum.sysinternals.com/hiding-a-process-pspcidtable_topic15362.html
2. http://bbs.pediy.com/showpost.php?p=1169355&postcount=1