SEH Overwrite

Analysis/Technique 2018. 5. 29. 17:15

SEH : Structured Exception Handling의 약자로, Windows를 위한 네이티브 예외 처리 메커니즘이다. 각 스레드마다 독립적으로 설치되고 처리된다.

 __try, __except, __finally 구문을 사용하여 SEH를 설치하고 해제할 수 있다. __try{} 선언은 컴파일러에서 정의된 EH_prolog 함수를 호출하는데, 이 함수는 스택에 _EXCEPTION_REGISTRATION_RECORD를 할당하고 SEH 링크드 리스트의 헤드에 레코드를 추가한다. (출처)

[SEH 설치 디스어셈블 모습]

 어셈블리 코드를 확인해 보면 익셉션 핸들러 함수 주소를 먼저 push하고, FS:[0x00]에 접근하여 그 값을 Push하는 것을 볼 수 있다. _EXCEPTION_REGISTRATION_RECORD는 다음과 같은 구조를 갖고 있다.

typedef struct _EXCEPTION_REGISTRATION_RECORD

{

     PEXCEPTION_REGISTRATION_RECORD Next;

     PEXCEPTION_DISPOSITION Handler;

} EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;


 Stack은 높은 주소에서 낮은 주소로 할당되므로 Handler 함수를 먼저 push하였고, 두번째로 push한 값은 Next 값이 되는데 이는 이 레코드에서 처리하지 못했을 경우 처리될 다음 _EXCEPTION_REGISTRATION_RECORD의 주소이다. 위에서 언급한 FS:[0x00]에는 TIB(Thread Information Block) 또는 TEB(Thread Environment Block)이라고 불리는 구조체가 존재한다.

 

[TIB 내용. 출처]

 그림과 같이 FS[0x00]에 할당된 TIB는 첫번째 인자로 현재 SEH 프레임의 주소, 즉 링크드 리스트의 헤더 주소를 갖고 있다.

 

[SEH Chain]

 _EXCEPTION_REGISTRATION_RECORD__try 문의 중첩 정도에 따라 그림과 같은 링크드 리스트 구조를 갖고 스택에 할당된다. 먼저 pExceptionHandler 함수가 호출되고, 해당 핸들러에서 익셉션을 처리하지 못할 경우 pNextSEHRecord를 참조하여 다음 핸들러를 호출한다. 이를 계속 반복하다가 pNextSEHRecord0xFFFFFFFF가 할당되어 있는 디폴트 핸들러인 UnhandledExceptionHandler까지 도달할 경우 해당 익셉션을 커널로 넘겨 프로세스를 재개하거나 종료시킨다. 

 

 

[SEH in Stack]

 함수가 Call 될 때 자동으로 스택에 저장되는 ret과 함수 프롤로그에서 자동으로 생성되는 SFP 이후에 함수 내부에서 __try 구문을 사용했다는 가정 하에 스택 프레임에는 위와 같은 구조로 _EXCEPTION_REGISTRATION_RECORD가 쌓일 것이다. Buffer에 원하는 만큼 데이터를 집어넣어서 BOF를 일으킬 수 있다면 가장 쉬운 공격 방법은 역시 ret을 덮어씌워서 공격하는 것이지만, 이는 보통 Stack cookie 또는 Stack canary라고 불리는 보호 기법에 의해 공격이 여의치 않다.

 

[Stack Canary]

 Stack Canary는 고전적인 BOF 방지 기법 중 하나로, retSFP 뒤에 랜덤한 값을 생성하여 설정한 후 함수가 끝날 때 검사하여 값이 바뀌었을 경우 탐지하고 강제로 종료시켜버리는 보호 기법이다. 이런 상황에서는 Canary를 덮어쓰고도 공격이 가능하게 하기 위해 SEH Overwrite를 이용할 수 있다.

 

[Basic SEH Overwrite]

 가장 간단하게 SEH Overwrite를 사용하는 방법은 위와 같다. pExceptionHandler를 덮어쓰기 위해 Canary 값을 변경했으므로 함수 에필로그 직전에 Canary를 검사하는 루틴을 지나가는 순간 프로그램이 종료되어 버린다. 따라서 SEH Overwrite가 제대로 기능하도록 하기 위해서 Canary 검사가 진행되기 전에 다른 Exception이 발생하도록 만들어야 한다. 이 방법은 여러 가지가 있지만 대표적으로는 위 그림처럼 파라미터로 넘어온 포인터까지 Dummy 값으로 덮어씌우고 이를 사용했을 때 Exception이 발생하도록 할 수 있겠다.

 위처럼 단순하게 스택에서 _EXCEPTION_REGISTRATION_RECORDpExceptionHandlerShellcode의 주소로 덮어쓰는 방법을 쓸 수 있으면 좋겠지만, 이 방법은 Microsoft에서 SafeSEH라는 보호 기법을 도입하면서 불가능하게 되었다. 이 보호 기법은 pExceptionHandler 내부에 스택 주소가 들어가거나, MS에서 핸들러로 등록한 주소가 아닌데 kernel32.dll MS에서 지정한 모듈 주소가 들어갈 경우 실행되지 않도록 한다. 따라서 이를 우회하기 위해서는 다른 방식을 사용해야 한다.


typedef EXCEPTION_DISPOSITION (*PEXCEPTION_ROUTINE) ( 

    IN PEXCEPTION_RECORD ExceptionRecord, 

    IN ULONG EstablisherFrame

    IN OUT PCONTEXT ContextRecord, 

    IN OUT PDISPATCHER_CONTEXT DispatcherContext 

);


 위의 함수 정의부는 pExceptionHandler 함수의 프로토타입이다. SEH 처리가 시작되면 pExceptionHandler 주소를 위의 인자들과 함께 Call하게 되는 것인데, 여기서 중요한 부분은 두번째 인자인 EstablisherFrame이다. EstablisherFrame은 이 pExceptionHandler 함수를 호출한 _EXCEPTION_REGISTRATION_RECORD 구조체의 주소를 갖게 된다. , 익셉션 발생 후 SEH 처리가 정상적으로 실행될 경우 다음과 같은 스택 상황이 만들어진다.


[정상적으로 실행된 SEH]

 여기서 주의 깊게 봐야 할 부분은 바로 ESP+8에 위치한 EstablisherFrame 값이 우리가 직접 변조 가능한 pNextSEHRecord를 가리키고 있다는 점이다. 만약 pExceptionHandler를 변조하여 Pop-Pop-Ret 가젯의 주소로 바꿔놓는다면 위 스택에서 ret이 먼저 Pop되고, ExceptionRecordPop되고, EstablisherFrameret되면서 자연스럽게 EIP가 스택의 pNextSEHRecord를 가리킬 것이다.

 

[변조된 SEH]

 위에서 적은 대로 pExceptionHandler &ppr로 덮어씌우고 추가적인 작업을 더 해서 Exploit을 완성한 모습이다. ppr 가젯에 의해 EIP&pNextSEHRecord를 가리키게 되면서 최대 4바이트의 임의의 OPCode를 실행시킬 수 있다. 여기서 사용할 수 있는 가장 간단하면서 강력한 OPCodeshort relative-jmp, EB XX2바이트짜리 점프이다

[Negative Short JMP. 출처]

 이 명령은 [현재 주소] + [Second Byte Value(XX)] + 2 주소로 점프하게 된다여기서 +2가 붙은 이유는 이 명령 자체의 크기가 2이기 때문이다. 예를 들어 0x00120100이라는 스택 주소에 EB 9A 90 90 이라는 OPCode가 들어갈 경우 0x9Asigned int8 형에서 -102와 같으므로 -102 + 2 하여 현재 EIP에서 -100의 주소로 점프한다. 이를 잘 이용하면 위의 버퍼에 미리 입력해 둔 NOP Sled + Shellcode로 점프하는 것이 가능하다.


블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

댓글을 달아 주세요