'ActiveProcessLinks'에 해당되는 글 1건

  1. 2011.08.29 [Malware Analysis] DKOM(Direct Kernel Object Manipulation) 기법을 이용한 RootKit (2)

보통 악성코드가 사용하는 루트킷 기법은 디바이스 드라이버 파일을 이용하여 동작을 하지만 DKOM(Direct Kernel Object Manipulation) 기법을 이용하여 사용자 레벨에서 직접 커널에 접근하여 커널 오브젝트를 수정하는 것이 가능하다. 해당 기법을 이용하여 자신의 프로세스 목록을 숨기는 악성코드가 있어 정리하고자 한다.

 

윈도우 시스템에서 프로세스 정보는 각 프로세스 별로 EPROCESS 라는 구조체로 관리된다. 구조체의 구성은 [그림 1]과 같은 형태로 이루어진다.

[그림 1]

이 중 DKOM을 이용한 프로세스를 숨기는 기법에서는 +0x84에 위치한 UniqueProcessId와 +0x88에 위치한 ActiveProcessLinks 멤버를 주로 사용한다. UniqueProcessId는 각 프로세스 별로 부여된 PID를 의미하여 ActiveProcessLinks는 환형 구조로 구성된 프로세스 링크에 대한 이중 링크드 리스트를 의미한다. ActiveProcessLinks는 다시 [그림 2]와 같은 구조체 형식으로 이루어져 있다.

[그림 2]

Flink는 이전 프로세스의 EPROCESS의 Flink를 가르키며 Blink는 다음 프로세스 EPROCESS의 Blink를 가르킨다. 프로세스의 연결 관계를 그림으로 살펴보면 [그림 3]과 같다.

[그림 3]

[그림 3]과 같이 시스템 상에 PsActiveProcessHead 변수가 존재하며 PsActiveProcessHead에서부터 이중 링크드 리스트 형태로 환형구조가 이루어진다. 따라서 중간에 링크를 끊어주거나 변경하게 되면 해당 프로세스는 사용자에게 보이지 않게 된다.

   

먼저 [그림 4]과 같이 자신의 프로세스의 권한을 상승시킨다.

[그림 4]

   

이후 [그림 5]와 같이 ZwQuerySystemInformation 함수를 이용하여 시스템의 특정 정보를 얻어온다. InfoType을 보면 시스템 모듈 정보를 얻어오려는 것을 확인할 수 있다. 얻어온 정보는 아래 구조체와 같은 형식으로 버퍼에 기록되며 첫번째 모듈은 항상 ntkrnlpa.exe이다. 검색 결과 커널이미지는 CPU의 종류에 따라서 다른 파일이 로드된다고 한다.

- NTOSKRNL.EXE : 1 CPU

- NTKRNLMP.EXE : N CPU SMP(Symmetric multipeocessing)

- NTKRNLPA.EXE : 1 CPU, PAE(Physical address extension)

- NTKRPAMP.EXE : N CPU SMP, PAE

[그림 5]

typedef struct _SYSTEM_MODULE_INFORMATION { 
 ULONG Reserved[2];
 PVOID Base; // 모듈의 기본주소
 ULONG Size; //모듈의 사이즈
 ULONG Flags; // 모듈 상태를 나타내는 플래그들의 비트배열
 USHORT Index; // 모듈의 인덱스 번호
 USHORT Unknown;
 USHORT LoadCount; // 모듈에 대한 참조 갯수
 USHORT ModuleNameOffset; // ImageName에서 경로를 제외한 파일이름의 오프셋
 CHAR ImageName[256]; //모듈의 이름
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;

   

ntkenlpa.exe에 대한 정보가 메모리에 로드된 모습은 다음과 같으며 현재 커널에 ntkrnlpa.exe가 로드되어 있는 주소(0x804D9000) 를 확인할 수 있다. 실제로 해당 주소 확인시 [그림 6]과 같이 ntkenlpa.exe가 로드되어 있는 것을 확인할 수 있다.

   

00A94988 84 00 00 00 78 01 A9 00 00 00 00 00 00 90 4D 80 ?..x?.....륪€

00A94998 00 69 1F 00 00 40 00 0C 00 00 00 00 01 00 12 00 .i..@........

00A949A8 5C 57 49 4E 44 4F 57 53 5C 73 79 73 74 65 6D 33 \WINDOWS\system3

00A949B8 32 5C 6E 74 6B 72 6E 6C 70 61 2E 65 78 65 00 00 2\ntkrnlpa.exe..

   

[그림 6]

이어서 [그림 7]와 같이 ntkenlpa.exe를 로드한다.

[그림 7]

[그림 8]와 같이 PsInitialSystemProcess 함수의 주소를 얻어온다. PsInitialSystemProcess 함수는 "system " 프로세스에 대한 EPROCESS 구조체의 주소를 반환한다.

[그림 8]

[그림 5]에서 구한 ntkenlpa.exe의 커널 상의 주소와 PsInitialSystemProcess 함수의 RVA 값을 더해 커널상의 PsInitialSystemProcess 주소를 얻어온다. PsInitialSystemProcess 주소를 얻었으므로 이제 커널 상에 존재하는 "system" 프로세스의 EPROCESS 구조체의 주소를 알 수 있다.

[그림 9]

 "system" 프로세스의 EPROCESS 구조체의 주소를 알았으므로 UniqueProcessId와 ActiveProcessLinks 값을 이용해 현재 프로세스(숨김 대상)의 PID를 찾아간다. [그림 10]는 ActiveProcessLinks를 이용하여 다음 프로세스로 이동하면서 UniqueProcessId가 현재 프로세스의 ID와 일치하는지 비교하는 부분이다. 0x0042FBFA 부분에서 현재 프로세스의 ID와 UniqueProcessId를 비교하는 것을 볼 수 있다.

[그림 10]

[그림 11]

   

ActiveProcessLinks를 따라 이동하다가 현재 프로세스(숨김 대상)의 PID와 동일한 UniqueProcessId를 갖는 프로세스를 찾게되면 ActiveProcessLinks를 변형하게 된다. 현재 테스트 환경에서의 악성코드와 앞뒤로 링크되어 있는 프로세스의 관계는 [그림 12]와 같다.

   

[그림 12]

먼저 ollydbg.exe의 FLINK를 변경한다. 변경된 후의 값을 보면 [그림 13]과 같이 cmd.exe 프로세스의 ActiveProcessLinks의 값으로 변경된 것을 확인할 수 있다.

   

[그림 13]

이어서 cmd.exe의 BLINK를 변경한다. 해당 값은 ollydbg.exe의 ActiveProcessLinks를 가르키는 것을 확인할 수 있다.

   

[그림 14]

따라서 위의 과정을 거친 후의 세 프로세스의 연결 관계를 보면 [그림 15]과 같다.

[그림 15]

그림에서 보는 것과 같이 ollydbg.exe의 Flink는 cmd.exe를 가르키도록, cmd.exe의 BLink는 ollydbg.exe를 가르키도록 변경된다. 따라서 악성코드인 igfxcp32.exe는 프로세스 목록에서 나타나지 않게 된다.

   

또한 igfxcp32.exe의 ActiveProcessLinks는 Flink는 csrss.exe를 Blink는 smss.exe를 가르키도록 변경된다. [그림16 ]은 original ActiveProcessLinks이며 [그림 17]은 변조된 igfxcp32.exe의 ActiveProcessLinks의 값이다.

[그림 16]

[그림 17]

   

실제로 커널의 값을 읽어 오거나 변경할 때는 ZwSystemDebugControl 함수를 사용한다. ZwSystemDebugControl 함수의 원형은 다음과 같다.

NTSYAPI NTSTATUS NTAPI ZwSystemDebugControl (
IN SYSDBG_COMMAND SysDbgChunks,  // Read, Write 여부
IN OUT PMEMORY_CHUNKS pQueryBuff, // Query 내용이 저장된 버퍼
DWORD dwSize, // 버퍼의 크기
DWORD,
DWORD,
NTSTATUS *pResult // 성공 여부
};

   

가상 메모리 공간에서 커널 데이터를 읽거나 쓸 때는 아래와 같은 구조로 해당 값에 접근이 가능하다. ZwSystemDebugControl의 첫번째 파라미터인 SysDbgChunks의 값에 따라 메모리에서 내용을 읽을 것인지 또는 쓸 것인지 결정된다. 예제 소스를 보면 아래와 같은 구조로 사용자 레벨에서 커널 레벨의 값에 접근하여 내용을 읽거나 변경한다.

   

//가상 메모리에 데이터를 쓰거나 읽을 때 사용하는 구조체이다
typedef struct _MEMORY_CHUNKS
{
 PVOID pVirtualAddress;
 PVOID pBuffer;
 DWORD dwBufferSize;
} MEMORY_CHUNKS, *PMEMORY_CHUNKS;
.....
//ZwSystemDebugControl의 첫 번째 인자
typedef enum _SYSDBG_COMMAND
{
 SysDbgCopyMemoryChunks_0 = 0x08,  //가상 메모리로부터 데이터를 읽을 때
 SysDbgCopyMemoryChunks_1 = 0x09,  //가상 메모리에 데이터를 쓸 때
} SYSDBG_COMMAND;
.....
//가상 메모리 공간을 읽어온다
BOOL ReadVirtualMemory(PVOID pAddress, PVOID pBuffer, DWORD dwBufferSize)
{
 NTSTATUS result;
 
 //메모리 청크 구조체
 MEMORY_CHUNKS QueryBuff;
 QueryBuff.pVirtualAddress = pAddress; //가상 메모리 주소
 QueryBuff.pBuffer = pBuffer;   //버퍼의 주소
 QueryBuff.dwBufferSize = dwBufferSize; //버퍼의 크기
 
 //가상 메모리 공간을 읽어서 버퍼에 기록한다
 ZwSystemDebugControl(SysDbgCopyMemoryChunks_0, &QueryBuff,
  sizeof(MEMORY_CHUNKS), NULL, 0, &result);
 
 return NT_SUCCESS(result);
}
 
//가상 메모리 공간에 데이터를 쓴다
BOOL WriteVirtualMemory(PVOID pAddress, PVOID pBuffer, DWORD dwBufferSize)
{
 NTSTATUS result;
 
 //메모리 청크 구조체
 MEMORY_CHUNKS QueryBuff;
 QueryBuff.pVirtualAddress = pAddress; //가상 메모리 주소
 QueryBuff.pBuffer = pBuffer;   //버퍼의 주소
 QueryBuff.dwBufferSize = dwBufferSize; //버퍼의 크기
 
 //버퍼로 부터 값을 읽어서 가상 메모리 공간에 기록한다
 ZwSystemDebugControl(SysDbgCopyMemoryChunks_1, &QueryBuff,
  sizeof(MEMORY_CHUNKS), NULL, 0, &result);
 
 return NT_SUCCESS(result);
}

참고문헌 : http://www.winapi.co.kr/ApiBoard/tblpds/HideProcess.cpp

신고
Posted by By. PHR34K