출처 : http://zack-textcube.blogspot.com/2010/04/2.html


이번엔 프로세스를 숨기는 방법에 대해 말씀드리려고 합니다. 

윈도우에는 유저영역과 커널영역이 있습니다. 

SDK를 이용해서 만들어지는 모든 응용 프로그램은 유저 영역에서만 

다루어집니다. 

아니, 윈도우에서는 응용 프로그램이 커널 영역을 건드리는 것을 막아 놓았습니다. 

사실, 핸들이라는 것을 통해서 응용 프로그램은 간접적으로 커널 영역을 건드리는데요, 

커널을 만지지 않고서는 아무 작업도 할 수 없기 때문에 

윈도우가 고안한 방법일 겁니다. 

커널을 직접 건드리기 위해선 DDK라는 것을 통해 시스템 프로그램 내지는 

드라이버(확장자가 .sys입니다)를 만드는게 보통인데요,

제가 설명하고자 하는 방법은 SDK만을 이용한 방법입니다. 

프로세스를 숨기는 기본적인 원리는 이렇습니다. 

윈도우에는 커널영역이 있다고 했죠?

그리고 그 영역에 접근하기 위한 핸들이 있다고 했습니다. 

각각의 핸들에는 대응되는 오브젝트가 있습니다. 

예를 들어 윈도우 핸들에는 윈도우 정보를 담고 있는 구조체가, 

프로세스 핸들에는 프로세스 정보를 담고 있는 구조체가 있습니다. 

이 프로세스의 정보를 담은 구조체의 이름은 EPROCESS입니다. 

EPROCESS에는 엄청나게 많은 멤버가 있는데요, 그 중 이 테마에 중요한 것은

ActiveProcessLinks라는 멤버 하나 뿐입니다. 

이름에서 대략 눈치채셨겠지만, 프로세스들은 연결리스트 구조로 연결되어있습니다. 

때문에 목표하는 프로세스를 연결리스트에서 끊기만 하면 감쪽같이 

목록에서 사라지게 되지요. 

혹시 CPU 사용 권한을 잃게 될 까 걱정하지 않아도 됩니다. 왜냐면, 

작업은 쓰레드를 기반으로 이루어지기 때문에, 프로세스는 이름일 뿐입니다. 

윈도우즈의 버전에 따라 ActiveProcessLinks의 오프셋값이 다른데요, 

XP의 경우 구조체의 시작 번지로 부터 0x088만큼 떨어진(오프셋된) 위치에 

ActiveProcessLinks가 있습니다. 이 멤버의 자료형은 LIST_ENTRY인데, 이는 

SDK플랫폼에도 정의가 되어있는 구조체로, 다음 두 멤버를 가집니다. 

PLIST_ENTRY Flink;
PLIST_ENTRY Blink;

그러므로, 
ActiveProcessLinks.Filnk->Blink = ActiveProcessLinks.Blink;
ActiveProcessLinks.Bilnk->Flink = ActiveProcessLinks.Flink;
의 작업을 거쳐주면 되는 겁니다. 

그럼 문제는 EPROCESS의 주소를 어떻게 알아내는가 인데요..

바로 여기에 여러가지 테크닉이 존재합니다. 

어떤 방법을 사용하든지 Native API를 사용하게 될 텐데요, 

이는 SDK에 정의되어있지 않으므로, GetProcAddress를 통해 직접 주소를 얻어내야 합니다. 

제가 설명할 방법에 쓰이는 API함수는 다음 두 개입니다. 

NTSTATUS __stdcall ZwQuerySystemInformation(IN SYSTEM_INFORMATION_CLASS SystemInformationClass, IN OUT PVOID SystemInformation, IN ULONG SystemInformationLength, OUT PULONG ReturnLength OPTIONAL );

NTSTATUS __stdcall ZwSystemDebugControl(IN SYSDBG_COMMAND SysDbgChunks, IN OUT PVOID pQueryBuff, DWORD dwSize, DWORD, DWORD, NTSTATUS *pResult);

NTSTATUS는 DDK에서 쓰이는 자료형으로, SDK에서 사용하려면 LONG형을 typedef해야 합니다. 

(아시는 분은 알겠지만, 참고로 예기해 드립니다. IN, OUT, OPTIONAL은 아무 의미없는 #define으로, 인자가 입력인지, 출력인지, 그리고 사용하지 않으므로 NULL을 넣어도 되는가 등을 예기해주는 겁니다)

SYSTEM_INFORMATION_CLASS 는 enum으로 정의된 자료형으로, 여러가지 값이 

정의되어 있습니다. 어떤 분이 찾아내셨는지는 모르겠지만;;

그 목록에 나와있지 않은 값들 중에서 16을 넣게 되면, 재미있는 일이 벌어지는데요, 

이걸 이용할 것입니다. 사용할 값은 16뿐이므로 SYSTEM_INFORMATION_CLASS를 

다음과 같이 정의해서 사용하면 됩니다. 

typedef enum _SYSTEM_INFORMATION_CLASS
{
SystemHandleInformation = 16
} SYSTEM_INFORMATION_CLASS;

너무 길어지네요;; 2부에서 계속합니다.

 

 

SystemHandleInformation이란 이름에서 알 수 있듯, 

16은 시스템 상에 로드되어있는 모든 핸들에 대한 정보를 얻어오게 합니다. 

그 정보는 다음과 같은 구조체의 배열에 저장됩니다. 

typedef struct _SYSTEM_HANDLE_INFORMATION
{
ULONG ProcessId;
UCHAR ObjectTypeNumber;
UCHAR Flags;
USHORT Handle;
PVOID Object;
ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION;

눈여겨 보아야 할 것은 첫번째, 두번째, 다섯번째 인자인데요, 

첫번째 인자는 이 핸들을 소유한 프로세스의 아이디입니다. 두번째 인자는 

이 핸들의 타입인데요, 핸들에는 여러 종류가 있으니 이를 구분해 주는 겁니다. 

윈도우 핸들, 쓰레드 핸들 등 많은 종류가 있지만, 프로세스는 5번입니다. 

즉, 이 핸들이 우리가 찾고자 하는 프로세스인지 아닌지 조사하려면, 

우선 첫번째 인자와 찾고자 하는 프로세스의 아이디를 조사하고, 

두번째 인자가 5인지를 조사하면 되는 겁니다. 

그리고 대망의 다섯번째 인자는 바로 EPROCESS구조체의 주소입니다. 

이제 ZwQuerySystemInformation의 각 인자에 대해 설명드리겠습니다. 

첫번째 인자는 어떤 종류의 정보를 얻어올 것인가로, 16을 입력할 경우 시스템 핸들 정보를

얻어온다고 했습니다. 

두번째 인자는 정보를 입력받을 버퍼의 포인터입니다. 

세번째 인자는 버퍼의 크기입니다. 

네번째 인자는 정보의 크기입니다. 

시스템 상에 핸들이 얼마나 많은지 모르기 때문에, 적당한 버퍼의 크기를 알 수가 없습니다. 

그래서 1바이트 부터 시작해서 계속 2씩 곱해가면서 적당한 사이즈를 찾는데요, 

버퍼의 크기가 부적절한 경우 ZwQuerySystemInformation은 

STATUS_INFO_LENGTH_MISMATCH라는 값을 반환합니다. 

이 값 역시 DDK에서 사용하는 매크로로, ((NTSTATUS)0xC0000004L)와 같이 정의하면 됩니다. 

사이즈가 적당치 않은 경우는 버퍼를 free하고 크기를 두배로 늘린 뒤 다시 시도하는 

반복문을 돌려서, 적당한 크기를 찾습니다. 

크기가 충분해서 Query Information이 성공하면, 

ZwQuerySystemInformation은 0이상의 값을 반환합니다. 

네번째 인자는 OPTIONAL이므로 그냥 0을 주면 됩니다. 

아무튼 이렇게 해서 얻어낸 정보의 앞 4바이트는 배열의 인자 개수를 나타냅니다. 

다시말하면 시스템상에 존재하는 핸들의 숫자입니다. 

이제 이 숫자만큼 루프를 돌면서 원하는 핸들을 찾아내면 됩니다. 

헥헥.; 여기까지가 EPROCESS의 주소를 알아내는 방법입니다. 

이제 다 끝난게 아닌가 하시겠지만, 사실 더 있습니다. 

이렇게 얻어낸 EPROCESS의 주소는 선형 주소라는 것으로, 직접 접근할 수가 없습니다. 

주소에는 물리 주소, 선형 주소, 논리 주소 이렇게 3개가 있는데요, 

보통 그냥 사용해 왔던 포인터 변수는 논리 주소입니다. 

선형 주소는 가상 주소라고도 하는데요, ZwSystemDebugControl은 여기서 쓰입니다. 

이녀석도 첫번째 인자로 enum자료형을 받는데요, 여기서 사용할 값은 

8, 9 두 개이므로, 다음과같이 정의해서 쓰면 됩니다. 

typedef enum _SYSDBG_COMMAND
{
SysDbgCopyMemoryChunks_0 = 0x08, SysDbgCopyMemoryChunks_1 = 0x09
} SYSDBG_COMMAND;

두번째 인자는 데이터를 받을 구조체의 포인터입니다. 

이 구조체는 MEMORY_CHUNKS라는 구조체로, 다음과 같이 정의됩니다. 

typedef struct _MEMORY_CHUNKS
{
PVOID pVirtualAddress;
PVOID pBuffer;
DWORD dwBufferSize;
} MEMORY_CHUNKS, *PMEMORY_CHUNKS;

첫번째 멤버로 접근하고자 하는 메모리의 주소를 입력하고, 

두번째 멤버와 세번째 멤버에 데이터를 저장할 버퍼의 주소와 그 크기를 입력합니다. 

가상메모리를 읽는 경우(SysDbgCopyMemoryChunks_0), 

이 버퍼로 가상 메모리의 데이터가 쓰여집니다. 

가상메모리를 쓰는 경우(SysDbgCopyMemoryChunks_1), 

이 버퍼에 쓰인 내용이 가상 메모리에 쓰여집니다. 

한편 ZwSystemDebugControl의 세번째 인자는 읽고자 하는 데이터의 크기입니다. 

포인터를 읽을 것이기 때문에 여기에는 4를 지정하면 됩니다. 

네번째와 다섯번째 인자는 여기서 중요하지 않기 때문에 그냥 비워두었습니다. 

그리고 여섯번째 인자로 성공의 여부가 들어오게 됩니다. 

이렇게 해서 가상메모리를 읽거나 쓸 수가 있습니다. 

그럼 3부에서 계속하겠습니다.

 

 

이제 진짜 끝인가 하시겠지만 사실 조금 더 있습니다. 

가상메모리는 그냥 읽고 쓸 수 없습니다. 이걸 읽고 쓰려면 그에 맞는 권한(Privilege)

를 획득해야 하는데, 이 권한을 조절할 줄 알면 많은 것을 할 수 있습니다(^^훗)

이 권한을 통해 할 수 있는 일에 비해 이를 얻는 방법은 매우 쉽습니다. 

Native API따위를 이용하지 않아도 됩니다.

이부분은 인터넷에 자료가 많이 있으므로 소스만 올리겠습니다.

TOKEN_PRIVILEGES priv = { 1, {0, 0, SE_PRIVILEGE_ENABLED} };
LookupPrivilegeValue(0, lpName, &priv.Privileges[0].Luid);
HANDLE hToken;
OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES, &hToken);
AdjustTokenPrivileges(hToken, FALSE, &priv, sizeof(TOKEN_PRIVILEGES), 0, 0);
CloseHandle(hToken);

권한에는 여러 종류가 있는데, 그 중에는 셧다운 권한, 디버그 권한 등이 있습니다. 

이름에서 예측할 수 있지만, 셧다운 권한은 컴퓨터를 시스템 종료할 수 있는 권한입니다. 

이를 획득한 후 ExitWindows와 같은 함수를 이용하면 간단히 시스템 종료를 할 수 있습니다. 

중요한 것은 디버그 권한입니다. 

이 권한은 매우 강력한 권한으로, 이 권한을 획득해야만 가상메모리를 읽거나 쓸 수 있습니다. 

(여담이지만)추가로, 이 권한을 얻으면 작업관리자도 종료하지 못하는 중대한 

프로세스들을 죽일 수 있습니다. 

이러한 프로세스들이 그냥은 Terminate되지 않는 이유는 OpenProcess함수가 NULL을

리턴하기 때문인데요, 그 이유는 PROCESS_ALL_ACCESS권한으로 프로세스를 열려면

디버그 권한이 필요하기 때문입니다. 

따라서 디버그 권한을 얻은 뒤 OpenProcess를 하면 정상적으로 핸들을 얻게 되고, 

TerminateProcess도 정상적으로 동작합니다. 

이 방법을 이용하면 smss.exe, winlogon.exe등을 죽일 수가 있는데, 

실험해 본 결과 이럴 경우 블루스크린이 뜨면서 바로 재부팅됩니다;;

+ Recent posts