본문 바로가기

My Study/Programming&Theory

SSDT Hooking

단기간에 공부해야 할 일이 생겨서..ㅜㅜ 급하게 급하게 하고 있습니다...;;
도움 주신 건우형(Sone) 감사해요~~!
========================================================
이번에 해볼 후킹은 Windows 내부 커널에 있는 SSDT을 후킹하는 것에 대해서 알아보겠습니다.
먼저 SSDT가 어느 부분인지 간단한 그림으로 설명해 보겠습니다. ( 크게 보려면 클릭 )

파워포인트를 사용해 힘들게 그려봤습니다.
DeleteFileW함수를 쭉 따라가면서 그린 그림입니다.
환경은 Windows XP 32bit입니다.
그림 설명
UserMode에서 ZwOpenFile함수는 eax에 0x74를 넣고 KiFastSystemCall함수를 호출합니다.
이때 eax는 KiServiceTable에서 원하는 함수를 찾기위해 Index값으로 사용됩니다.

그리고 KiFastSystemCall에서는 현재의 스택포인터를 edx에 넣고 sysenter명령어를 사용합니다.
현재의 스택 포인터를 edx에 넣는 이유는 sysenter명령이 수행되면 커널로 들어가게 되는데 커널에서 사용하는 스택으로 갈아타기 위해 UserMode에서 사용하던 스택포인터를 저장해 놓는 것입니다. 

sysenter명령어를 수행하면 msr레지스터에서 0x176번째있는 SYSENTER_EIP_MSR레지스터에 있는 값으로 건너뒤게 됩니다. 바로 그 값이 KiFastCallEntry함수 주소 입니다. 아마 들어오시면 주소가 0x8.....로 되어 있으며 커널영역으로 들어온 것을 확인하실 수 있습니다.

그리고 KiFastCallEntry에서는 특정 루틴을 수행하는데 바로 우리가 원하는 함수 주소를 얻어오는 루틴을 수행합니다.
GetFuncAddress라는 루틴입니다. ( 함수가 아닙니다... 그냥 따로 빼논 것 뿐입니다.. )

해당 루틴에서는 KeServiceDescriptorTable 주소를 가져옵니다.
그리고 KeServiceDescriptorTable구조체 첫번째 멤버인 KiServiceTable에는 함수주소들이 쭉~ 나열되어 있습니다.
KiServiceTable를 SSDT라고 합니다. ( System Service Dispatch Table )
이제 KiServiceTable값에서 아까 User영역에서 가져온 0x74인덱스를 사용해서 원하는 함수를 구할 것입니다.
그냥 C언어로 표기해보면 위 그림에 있는 것처럼 KiServiceTable[eax*4] 값을 가져옵니다.

바로 그 주소가 NtOpenFile함수이지요.
주소를 구해왔으니 Call을 합니다...

일단은 이렇게 작동을 하게 됩니다.

그러면 우리 목표인 SSDT후킹! 바로 KiServiceTable을 후킹하는 것입니다.
원리는 간단합니다. 
KiServiceTable에 있는 후킹할 함수 주소부분을 제가 만든 함수 주소로 바꿔버리면 되는 것이지요.
그림으로 보시겠습니다.

위 그림과 같이 NtOpenFile주소가 있던 부분에 제가 만든 함수 주소 부분으로 바꿉니다.
그러면 KiServiceTable[eax*4]해서 주소를 얻어올 때 MyNtOpenFile주소를 얻어갈 것입니다.
후킹이 성공 된것이죠.

그리고 유저영역에서 DeleteFile함수를 호출하면 커널에서는 NtOpenFile함수가 아닌 MyNtOpenFile함수를 호출합니다.
이제 해당 함수의 인자를 주물럭 하던지 리턴 값을 주물럭 하던지 맘대로 하면 됩니다.
내부적으로 NtOpenFile함수만 정상적으로 호출 시켜주면 되는 것이지요.

뭐 원리는 유저모드에서 하는 후킹이랑 다른건 없으니 길게 설명은 안하겠습니다.

이제 후킹을 하는 디바이스 드라이버를 작성해 보도록 하겠습니다.
sys파일이 커널에 로드되면 가장먼저 호출되는 DriverEntry부분을 보도록 하겠습니다.
일단 GetServiceNum이라는 함수는 각 OS버전에 맞게 ServiceNum을 가져오는 함수입니다.
해당 sys파일이 어떠한 OS에서 로드되던 전부 동작하도록 하기위함 이지요.

해당 소스는 생략입니다. XP라면 아까 위에서 봤던 0x74를 가져오겠지요. ( 내부적으론 하드코딩.. )
이 값은 나중에 KiServiceTable에서 함수 주소를 찾을 때 사용하게 됩니다.
KeRaiseIrql과 KeLowerIrql함수로 후킹하는 부분을 감싸놓은 이유
IRQL값이 일반적으로 작동할 땐 PASSIVE_LEVEL(0)입니다. 그렇기 때문에 cpu독점을 막기위해 작동하는 스케쥴러에 의해 cpu를 일정시간 사용하면 강제적으로 사용하던 cpu를 뺏겨버립니다. 스위칭이 일어난다는 것이지요. 그렇게 되면 아직 중요 작업을 다 마치지 못했는데 다른 코드에서 해당 메모리로 접근을 해버릴 수 있다는 것입니다. BlueScreen을 맛볼 수도 있습니다.
그렇기 때문에 잠깐 IRQL값을 DISPATCH_LEVEL로 올려주면서 스케쥴러에 영향에서 벗어나게 되는 것입니다.
스케쥴러는 DISPATCH_LEVEL에서 작동하기 때문에 같은 레벨에서 동작 중인 cpu는 스케쥴러 대상에서 빼버립니다.
그러면 스위칭이 일어날 일이 없으니 안전하게 코드를 실행 할 수 있는 것이지요.

이제 MemoryProtect_Off, MemoryProtect_On 코드를 보시겠습니다.
코드를 보시기 전에 하나만 더 알고 가겠습니다. 
SSDT는 물리메모리에 존재하게 됩니다. 
하지만 메모리를 수정시키려면 CR0레지스터를 수정시켜야합니다.

CR0 레지스터는 페이징 메커니즘 활성화, 태스트 전환의 감시, 보조 프로세서의 에뮬레이션, 보호모드 선택에 사용됩니다.
CR0은 32bit로 이루어져 있는데 SSDT후킹을 위해 0번째부터 시작해서 16번째 있는 비트 값(WP)을 수정시켜야합니다.

0 -> Write 가능
1 -> Read-Only

CR0레지스터에서 WP값은 디폴트로 1로 셋팅되어 있습니다. 그렇기 때문에 SSDT후킹을 위해서 0으로 클리어 해줘야합니다.
다음은 코드 입니다.
코드를 보시면 CR0레지스터에서 WP값을 조정하고 있는 것을 보실 수 있습니다.
그리고 Off함수 내부에서 cli명령어는 IF레지스터를 0으로 클리어 해주는 명령어입니다.
IF레지스터가 0이 되면 해당 cpu는 더이상 인터럽트를 받지 않습니다. 즉, IRQL값이 높은 하드웨어 인터럽트에 의해서도 
cpu를 뺏기지 않는 다는 것입니다.

그리고 On마지막에 sti는 다시 IF레지스터를 1로 셋팅하는 명령어 입니다. 인터럽트를 다시 받겠다는 것이지요.
솔직히 IRQL값을 조정하는 함수를 썼는데 왜 또 이 명령어를 썼냐고 질문하시는 분들도 계실탠데..
그냥 안전상 이렇게 했다고 보시면 됩니다. 둘중 하나를 빼고 싶다면 IRQL값을 조정하는 함수를 빼시는게 나을 것 같습니다.

이제 HookFunc함수를 보도록 하겠습니다.
전달된 인자는 이렇습니다.
OldZwOpenFile : SSDT에 있던 원래 NtOpenFile함수 주소 담을 변수
ZwOpenFile_serviceNum : 해당 함수 서비스 넘버입니다. Index값으로 사용되죠.
MyZwOpenFile : 제가 만든 함수입니다.
HookState_ZwOpenFile : 후킹이 됬는지 안됬는지 판별하기 위한 변수입니다.

함수 내부입니다.

InterlockedExchange함수를 사용해 SSDT내부에 있는 NtOpenFile주소와 제가 만든 함수 주소를 바꿉니다.
그리고 리턴 값은 SSDT내부에 있는 NtOpenFile주소입니다. 그 값을 OldFunc에 넣어주고 있습니다.

마지막으로 후킹됬다는 표시를 하기위해 0이었던 HookState값을 1로 셋 해줬습니다.

Interlocked함수를 쓴 이유는 동기화를 위해서 입니다.

여기서 또 궁금증..
KeServiceDescriptorTable주소는 어디서 가져오느냐 인데요.
방법은 전역으로 구조체를 하나 만들고 __declspec(dllimport)를 사용해 주소를 가져옵니다.
코드 입니다.
이제 다 된거 같군요. 

마지막으로 볼 함수는 제가 만든 MyZwOpenFile함수입니다.
제가 해볼 후킹은 특정 파일을 삭제 못하도록 하는 것입니다.
코드를 보시겠습니다.
일단 DesiredAccess변수에는 NtOpenFile함수를 어떠한 이유로 불렀는지에 대한 값이 들어있습니다.
전 삭제할 때 불렀으므로 DELETE값입니다. 하지만 정의된 DELETE값은 0x00010000 이면서..
실제로 커널에서 함수 인자가 어떻게 전달되는지 봐보니 0x00010080 이었습니다. -_-;;
어떠한 값과 OR연산을 해서 나온 값인 거 같습니다.

그래서 DELETE상태이면 if문 안으로 들어옵니다.
그리고 일단 UNICODE인 값을 ANSI값으로 바꿉니다.

그리고 "\\??\\C:\\test.txt"와 비교를 하는데 커널에서는 경로를 사용할 때
앞에 "\??\"가 붙더군요. 함수를 사용해서 변경할 수도 있는데 해당 함수는
위 글에서 사용했습니다. 

아무튼 asFileName.Buffer에는 똑같이 삭제할 파일의 경로가 있겠지요.
경로가 "C:\test.txt"면 NtOpenFile함수를 호출하지 않고 그냥 실패했다는 리턴 값을 리턴해 버리고 끝냅니다.

코드는 이게 전부 입니다.
그리고 드라이버를 언로드 할 땐 후킹했던 부분을 다시 언훅해줘야 합니다.
언훅 코드는 위에 있고 사용방법은 후킹함수와 같습니다.
CR0레지스터 수정시켜주고 해야합니다.

이렇게 만들어진 sys파일을 커널에 로드 시키고 "C:\test.txt" 파일을 삭제하려고 해보겠습니다.

삭제가 안되는군요... ^^; 후킹 성공입니다.

해당 파일이니 한번 테스트 해보세요 ^^;
Windows XP에서는 잘 될것입니다.
제가 쓰는 로더는.. somma님이 만드신 로더.. ~_~;
======================================================================
처음으로 시도해본 커널 후킹이지만.. 정말 어려운거 같습니다. ㅜ_ㅜ 유저모드에서 하는거와 크게 다른 점은..
OS에 대한 지식이 더 많이 필요하다는 점과 유저모드에서 사용하는 함수들을 사용못하니 .. 그게 힘듭니다..

계속 커널 공부를 하며 다른 후킹도 해보고 디바이스도 많이 짜봐야겠습니다...이그.. 일단 책보면서 OS지식 쫌 넓혀야겠군요.

아! 틀린 부분이 있다면 지적부탁드립니다 ^^ 단말보다 쓴말을 좋아하는 저입니다..~!