반응형

강병탁 window31@empal.com, www.window31.com|바이너리 취약점 분석 업무를 하고 있다. 안티 크래킹/안티 디버깅 엔진 개발을 다년간 해왔으며 시스템 프로그래밍과 리버스 엔지니어링에 관심이 많다. 악성 코드나 해킹툴이 내부에 담고 있는 천재적인 알고리즘에 감탄하며 오늘도 IDA를 돌린다.


루트킷은 반드시 커널 레벨에서만 가동돼야 한다는 고정관념이 있다. 유저 레벨에서 아무리 숨겨 보았자 커널에서 쉽게 발견할 수 있다는 점에서 기능상의 한계를 많이 지적하는 편이다. 하지만 루트킷을 단순히 ‘최고의 은닉’ 기술에만 국한시켜서는 안 된다. 은닉이라는 기본 테마가 ‘무엇으로부터의 은닉’ 이라는 구체적 명제에 목매이지 않는다면, 유저 레벨 루트킷도 훌륭한 투명인간이 될 수 있다. 예를 들어 유저 레벨의 루트킷은 IceSword 같은 대부분의 루트킷 디텍터에 쉽게 발견되는 편이다. 하지만 그 같은 커널 드라이버를 이용하는 루트킷 디텍터를 사용할 수 없는 경우, 그리고 루트킷을 사용할 목적지가 드라이버와 무관한 장소일 경우라면 유저 레벨 루트킷도 충분한 가치를 발산할 수 있다. 더욱이 루트킷 하면 드라이버라고 생각하는데, 유저 레벨 루트킷은 드라이버를 설치하지 않고 숨겼다는 점에서 어떤 면에서는 더욱 훌륭하다고 볼 수 있다.

그리고 유저 레벨 루트킷은 “절대로 발견할 수 없도록 숨긴다”라는 목적보다는 그 기술이나 구현론 자체에 더욱 중점을 둬야 한다고 본다. 유저 레벨 루트킷을 구현하려면 API Hooking이나 각종 시스템 지식들이 다양하게 요구된다. “절대 발견할 수 없도록 숨긴다”에 너무 치중하지만 않는다면, 유저 레벨 루트킷을 연구하면서 얻을 수 있는 시스템 개발적 지식은 아주 많으리라 생각된다. 또한 유저 레벨의 루트킷은 굳이 루트킷 구현이 아닌 다른 여러 곳에서도 이용될 수 있는 알고리즘도 있기 때문에 얻을 수 있는 것은 더욱 많다고 생각된다. 따라서 루트킷을 커널에서만 구현해야 한다는 관념에 너무 얽매일 필요는 없으며, 유저 레벨에서 구현할 수 있는 루트킷은 어떤 것이 있고, 구조는 어떠한지 살펴볼 필요가 있다.
 
API Hooking

유저 레벨 루트킷으로써의 가장 기본임과 동시에 모든 부분이 되기도 하는 기법이 바로 API Hooking이다. Ring3에서는 시스템에 전역적인 영향을 끼치는 커널 구조체를 컨트롤 할 수 없으므로 은닉을 위해서는 기본적으로 후킹 기법을 필수적으로 사용한다. 그리고 타겟이 되는 API는 사용자가 공격자의 코드나 바이너리를 발견하지 못하도록 모듈을 찾는 관련 함수가 직접적인 대상이 된다. 예를 들면 파일을 찾을 때 FindNext File() 이나 프로세스를 찾을 때의 Process32Next() 같은 API가 대표적인 경우이다. 따라서 유저 레벨 루트킷은 이와 같은 API를 후킹하는 작업을 필수적으로 진행하므로 먼저 API Hooking에 대해서 살펴볼 필요가 있다. API Hooking에는 기본적으로 두 가지 기법으로 나누어진다. 첫째는 IAT Hook, 둘째는 Inline Hook(Overwrite Hook)이다. 여기서 사실 IAT Hook은 루트킷을 구현하는데 있어서 실전에서는 그다지 많이 쓰이지 않는 편이다. 왜냐하면 요즘 필드에 릴리즈되는 바이너리들은 암호화를 위해 대부분 프로텍팅·패킹이 돼서 배포되고 있는데, 프로텍터나 패커들은 API Call의 정보를 숨기거나 다른 보안상의 이유로 IAT를 망가뜨리거나 여러 갈래 꼬아놓은 자체 IAT를 사용하고 있다. 그래서 이 경우는 IAT가 잘 구해지지 않기 때문에 해당 테이블을 후킹 할 수 없고, 당연히 내가 원하는 은닉 기술을 활성화 할 수 없다. 따라서 IAT와는 무관하게 궁극적으로 DLL의 엔트리 포인트로 이동하는 부분을 노린, Inline Hook 방법을 많이 사용한다.

API Hooking을 위한 사전 준비 지식

유저 레벨에서 API Hooking을 위해서는 반드시 해당 프로세스에 DLL을 주입해야 한다(물론 DLL없이 빈 번지를 할당하여 코드를 삽입하고 리모트 스레드를 돌리는 방법도 있지만, 그 방법 역시 지금부터 설명할 세 가지 방법 중, 3번에 해당하는 것이기 때문에 별도 설명은 생략한다). DLL을 주입하는 방법에는 세 가지 방법이 있다. 첫째는 메시지 후킹, 둘째는 AppInit_DLLs 이용, 셋째는 CreateRemoteThread 이다. 이중 첫째는 윈도우가 없는 프로세스는 DLL을 집어넣을 수 없고, 둘째의 AppInit_ DLLs은 다른 프로그램이 사용하는 경우도 있으니 루트킷 구현이 목적인 API Hooking 방법으로는 적절한 방법이 아니다. 따라서 모든 프로세스의 DLL을 집어넣기 위한 방법으로는 셋째인 CreateRemoteThread가 가장 적합하다. 이 기법은 대부분 OpenProcess -> VirtualAllocEx -> WriteProcess Memory -> CreateRemoteThread 의 패턴을 사용한다. API Hooking에 이용되는 가장 표준적인 방법이기도 하다.

다음은 어느 API를 후킹해야 루트킷이 되는지 기능별로 살펴볼 필요가 있다. 목적은 자신을 발견할 수 없게 하는 법이 우선이다. 따라서 모듈이나 프로그램을 발견하기 위해서는 파일을 찾거나 메모리를 뒤지는 방법을 생각할 수 있는데 루트킷은 그것을 저지해야 하는 기능이 반드시 포함돼야 한다. 즉 모듈을 찾을 때에는 프로세스 검색, 파일 검색 더불어 서비스 리스트 검색까지 세 가지 방법이 있으며 루트킷은 이 세 가지 검색 방법을 모두 무력화 시켜야 한다.

프로세스 숨김

프로세스 리스트에서 공격자가 만든 바이너리가 보이지 않아야 하는 것이 첫 번째 과제이다. 프로세스를 나열하는 방법은 대표적으로 툴헬프32와 PSAPI가 있다. 따라서 루트킷은 유저 레벨에서 프로세스 리스트를 링크에서 제외시키기 위하여, Pro cess32Next()와 OpenProcess()를 후킹해야 한다. Open Process()만 후킹하면, 프로세스의 핸들만 얻을 수 없다 뿐이지, 툴헬프로는 PID와 프로세스 이름까지는 구해지기 때문에 유저 레벨에선 반드시 Process32Next()까지 처리해야 한다. 자신의 핸들이나 PID를 구해 놓고, OpenProcess()나 Process32Next() 후킹 함수에서 자신을 찾으려 할 때, 자신의 정보를 숨기는 방법이다. 만약 DLL을 가진 경우는 Module32Next()도 같은 방법으로 처리해 준다.

서비스 숨김

바이러스나 백도어를 찾을 때 서비스에 등록됐는지 살펴보는 경우가 많다. 그 때 루트킷이 서비스에 등록이 되어 있는 모습이 쉽게 발견되면 안 되므로, 서비스 리스트를 뽑을 때 그 목록에서 제거할 필요가 있다. 서비스 리스트는 EnumServices StatusEx 이라는 API를 사용한다. 루트킷은 이 API를 후킹하여 자신의 링크를 끊는다.

파일 숨김

탐색기나 내 컴퓨터 등에서 파일 리스트가 보이지 않아야 한다. 원리는 프로세스 리스트 때와 비슷하며 타겟이 되는 API는 FindNextFile 이다. 유저 레벨 루트킷은 이 API도 놓치면 안 된다. 이렇게 유저 레벨 루트킷을 구현하기 위한 세 가지 처리 부분을 살펴보았다. 프로세스, 서비스, 파일 리스트에서 자신을 숨긴다면, 별다른 루트킷 디텍터의 도움을 받지 않는 이상 해당 모듈을 찾기는 결코 쉽지 않다. 커널 드라이버를 설치할 필요도 없고, EXE와 DLL만으로 루트킷을 구현할 수 있다. 이것이 유저 레벨의 루트킷이다.

유저 레벨에서 루트킷 검출 방법

그렇다면 이번엔 유저 레벨에서 루트킷을 찾는 방법에 대해 알아보자. 물론 유저 레벨에서의 검출 방법은 커널 레벨에 비해 그 활용성이 지극히 제한적이고, 기법도 다양하지 않은 편이다. 그러나 방법이 전혀 쓸모없는 것도 아니다. 또한 이런 방법이 반드시 루트킷을 찾을 때만 사용되는 기술은 아니라는 점에서 다른 여러 시스템 프로그래밍에 활용할 수 있는 부분도 있다. 그렇다면 유저 레벨에서의 검출 방법에는 어떤 것이 있는지 살펴보자.

csrss.exe 의 스레드 리스트 이용

윈도우 시스템 프로세스 중 하나인 csrss.exe를 활용하면 유저 레벨에서도 루트킷을 감지할 수 있다. csrss.exe는 Client Server Run-time SubSystem의 약자로 윈도우 시스템을 가동시키는 핵심 프로세스이다. 이 프로세스는 윈도우에서 실행되는 모든 프로세스의 오브젝트를 관장하고 있다. 따라서 csrss.exe의 스레드 리스트를 구하면 루트킷 프로세스까지 검출이 가능하다.

후킹 원복화

후킹 되어 있는 API를 직접 원복화 시켜 버리는 방법이다. 물론 툴로 제공되는 경우도 있고 직접 VirtualProtect()와 Write ProcessMemory()를 이용하여 구현할 수도 있다. 지금은 디버거를 직접 붙여서 강제로 원복화를 시켜 보도록 하겠다. <화면 4>를 보자. 현재 FindNextFile이 h.dll 로 후킹 된 상태이다.

이 상태에서 이 코드를 원복화 시켜버리자. FindNextFile의 엔트리 코드는 mov edi, edi 로 시작하며 그 이후의 코드까지 포함하여 후킹 된 5바이트를 OpCode로 표현하면 0x8B, 0xFF, 0x55, 0x8B, 0xEC이다. 이 코드로 엔트리를 덮어써 버리자. <화면 5>가 그 내용이다.

그 후킹된 코드는 사라지고 오리지날 상태의 엔트리 포인트가 된다(<화면 6> 참조). 이제 FindNextFile의 후킹은 제거했다. 따라서 무결 상태의 FindNextFile을 사용할 수 있을 것이며 루트킷의 DLL을 거치치 않게 되므로 파일 검색이 가능하다. 이런 식으로 API Hooking을 원복화 시켜서 무력화 할 수 있다.

PID 연속 대입 방법

아무리 루트킷이 프로세스를 숨겨도 PID는 당연히 있기 마련이다. 그리고 PID는 0xC번부터 0xFFFF번까지 존재한다(4번 PID는 시스템이 사용한다). 따라서 PID를 4씩 늘려가며 연속해서 프로세스의 핸들을 오픈해 본다면, 현재 프로세스 리스트엔 없지만 PID가 있는 경우가 나타날 수 있다. 이 경우는 100% 루트킷의 프로세스이다. PID Brute Force 라는 기법이며, 단점은 루트킷이 프로세스의 핸들조차 얻지 못하도록 처리해 놓았을 때는 이 방법으로 검출이 불가능하다는 점이다.

유저 레벨 루트킷은 커널 하층부에 또 하나의 시스템이 더 들어올 수 있다는 약점이 있으므로 한계가 있고, 또 모든 프로세스를 통제해야 한다는 점에서 커널 루트킷보다 오히려 더 까다로울 수도 있다. 하지만 유저 레벨 루트킷은 반드시 루트킷 구현의 목적이 아닌 시스템 프로그래밍의 진수를 보여주는 여러 가지 기법을 사용한다. DLL Injection과 API Hooking, 그리고 각종 오브젝트의 활용적인 부분까지 유저 레벨 쪽의 Windows Internals에 대한 지식을 많이 함축하고 있다. 따라서 유저 레벨의 루트킷은 그 기능의 강력함의 여부를 떠나 기술 그 자체, 구현 알고리즘 자체에 우선순위를 두어 평가하는 것도 좋지 않을까 생각한다.

참고자료

1. Rootkit - http://www.rootkit.com

2. Anti-Rootkit - http://antirootkit.com/

3. Rootkits : Subverting the Windows Kernel - by Greg Hoglund, Jamie Butler

+ Recent posts