Windows OS 에서 사용되는 메모리 보호 기법인 SafeSEH, DEP 과 이에 대한 우회 기법인 RTL, ROP에 대해 정리합니다.
SafeSEH는 Windows OS 에서 구조적 예외 처리(Structured Exception Handling, SEH)기반 공격을 방어하기 위해 도입된 메모리 보호 기법이다. 이 기법은 예외가 발생했을 때 예외 처리 핸들러를 실행하기 전에 핸들러 주소의 유효성을 검증하는 방식으로 작동한다.
예외가 발생하면 ntdll.dll 의 kiUserExeptionDispatcher 함수가 호출되며, 이 함수는 다음과 같은 검증 과정을 수행한다.
SEH는 Windows 에서 제공하는 예외 처리 메커니즘이로, 하드웨어와 소프트웨어의 예외를 처리하기 위한 구조이다. 하드웨어 예외는 잘못된 접근이다 0으로 나누기와 같은 프로세서 개입으로 발생하며, 소프트웨어 예외는 try-catch 구문과 같은 사용자 정의 예외로 발생한다.
SEH는 연결 리스트 형태로 구성되어 있어, 예외가 발생하면 가장 최근에 등록된 핸들러부터 순차적으로 처리된다. 이러한 구조적 특성 때문에 공격자는 SEH 체인을 조작하여 악의적인 코드 실행을 시도할 수 있다.
SafeSEH는 강력한 보호 기법이지만 몇 가지 한계점을 가지고 있다.
POP/POP/RET
대신 jmp[esp+xx], call[esp+xx]
등의 코드를 이용하여 우회할 수 있다.Immunity Debugger나 OllyDbg와 같은 디버거에서 mona 모듈을 이용하면 SafeSEH 옵션이 적용되지 않은 모듈을 쉽게 확인할 수 있다.
!mona modules // SafeSEH 옵션 상태 확인
!mona seh -m [모듈이름] // 특정 모듈에서 POP/POP/RET 가젯 검색
예외가 발생하면 프로그램의 실행 흐름이 NTDLL로 넘어가게 된다. NTDLL로 넘어간 후에 스택을 확인하면 ESP-16, ESP+8, ESP+20, ESP+28. 등에 Next SEH주소의 값이 존재한다. 따라서 jmp esp+20, call esp+8 등의 명령어도 SEH Overwrite Exploit을 작성하는 데 활용될 수 있다.
SafeSEH의 한계를 보완하기 위해 개발된 SEHOP는 더 강화된 보호 기법이다. SEHOP는 Linked List 형태인 SEH 체인의 무결성을 검증하며, 마지막 SEH 레코드가 적절한 값 (pNextSEHRecord에는 0xFFFFFFFF, pExceptionHandler에는 Ntdll!FinalExceptionHandler)을 가지고 있는지 확인한다.
하지만 SEHOP도 SEH 체인을 조작하는 방식으로 우회가 가능하다는 한계를 가지고 있다.
DEP는 메모리 영역을 실행 가능과 실행 불가로 구분한다. 실행 권한이 없는 영역에서는 어떤 코드도 실행되지 않도록 하여 공격자가 쉘코드를 삽입 후 실행하는 전통적인 버처 오버플로우 공격을 차단한다.
Windows XP SP2 이상 버전에서는 NX-bit(No Excution)이라는 하드웨어 기능을 통해 DEP를 구현하고 있으며, 이는 페이지 단위로 실행 권한을 제어한다.
DEP는 강력한 보호 기법이지만 다음과 같은 방법으로 우회가 가능하다.
RTL은 DEP를 우회하기 위한 대표적인 기법으로, EIP를 직접 쉘코드를 변조하지 않고 이미 실행 권한이 존재하는 라이브러리 함수 주소로 변조하는 기법이다. 이 방법은 쉘코드를 직접 입력하는 대신 이미 존재하는 함수를 이용하므로, DEP가 적용된 환경에서도 공격이 가능하다.
RTL의 핵심 아이디어는 다음과 같다.
1. 실행 가능한 코드 영역 활용: 공유 라이브러리의 함수 주소를 RET 주소로 덮어쓴다.
2. 함수 인자 조작: 스택 프레임을 변조해 함수 호출 시 필요한 인자를 전달한다.
일반적인 버퍼 오버플로우 공격과 유사하게, RTL은 RET주소를 목표 함수 주소로 덮어쓴다. 그러나 추가적으로 인자 전달을 위해 스택 레이아웃을 특수하게 구성한다.
예를 들어, system("/bin/sh")를 실행하기 위한 페이로드 구조는 다음과 같다.
[버퍼(256바이트)] [SFP(4바이트)] [RET(system())] [dummy(4바이트)] [인자(/bin/sh 주소)]
여기서 dummy 값은 system() 함수의 반환 주소 영역으로, 공격 성공 후 프로그램 종료를 위해 exit() 함수 주소를 넣을 수도 있다.
RTL은 함수를 한 번만 호출할 수 있다는 제한이 있는데, 체이닝을 통해 다중 함수 호출과 복합한 로직 실행이 가능해진다.
RTL 체이닝의 핵심 아이디어는 다음과 같다.
예를 들어 "/bin/sh" 문자열이 메모리에 없을 경우, strcpy()로 .bss 영역에 문자열을 구성한 후 system()을 호출하는 등의 단계적 접근이 필요하다.
전형적인 RTL 체이닝 페이로드 구조는 다음과 같다.
[버퍼 오버플로우] [SFP] [함수1 주소] [가젯 주소] [인자1] [인자2] [함수2 주소] [가젯 주소] ...
payload = b"A"*264 # 버퍼 채우기
payload += p32(strcpy_addr) # 첫 번째 함수
payload += p32(pop_pop_ret) # 가젯
payload += p32(dest_addr) + p32(src1) # 인자
payload += p32(strcpy_addr) # 두 번째 함수
payload += p32(pop_pop_ret)
payload += p32(dest_addr+4) + p32(src2)
payload += p32(system_addr) # 최종 함수
payload += b"AAAA" # dummy
payload += p32(dest_addr)!
ROP는 DEP 우회 기법 중 가장 강력한 방법으로, 프로그램 또는 라이브러리 내에 이미 존재하는 코드 조각(가젯)을 연결하여 공격자를 위한 임의의 로직을 구성하는 기법이다. 각 가젯은 RET 명령어로 종료되며, 이것들을 체인으로 연결하여 Turing-complete 연산이 가능하다.
VirtualProtect는 윈도우 API 함수로, 특정 메모리 영역의 보호 속성을 변경할 수 있게 해준다. 함수 원형은 다음과 같다.
BOOL WINAPI VirtualProtect(
_In_ LPVOID lpAddress, // 권한을 변경할 메모리 영역의 시작 주소
_In_ SIZE_T dwSize, // 변경할 크기
_In_ DWORD flNewProtect, // 변경할 속성 값
_Out_ PDWORD lpflOldProtect // 이전 값 저장 (쓰기 가능한 영역)
);
DEP 우회를 위해서는 PAGE_EXECUTE_READWRITE(0x40) 값을 사용하여 메모리 영역에 실행 권한을 부여한다.
ROP 가젯은 다음과 같은 특성을 가진다.
신뢰성 있는 가젯을 찾기 위한 조건은 다음과 같다.
VirtualProtect를 이용한 DEP 우회는 다음과 같은 단계로 구현할 수 있다.
구현을 위해 먼저 ROP 가젯을 찾아야 한다.
!mona modules // ASLR이 적용되지 않은 모듈 찾기
!mona rop -m example // example.dll에서 ROP 가젯 추출
VirtualProtect 함수 주소에 접근하는 방법은 다음과 같다.
// IAT(Import Address Table)에서 VirtualProtect 함수 주소 찾기
pop eax // eax에 VirtualProtect IAT 주소 넣기
mov eax, [eax] // eax에 실제 VirtualProtect 함수 주소 넣기
ROP 체인 구성 예시는 다음과 같다.
// 필요한 가젯들
pop ecx // 매개변수 1 설정 (lpAddress)
pop edx // 매개변수 2 설정 (dwSize)
pop ebx // 매개변수 3 설정 (flNewProtect)
pop esi // 매개변수 4 설정 (lpflOldProtect)
pushad // 레지스터 값들을 스택에 저장
jmp esp // 쉘코드로 점프
최종 페이로드 구조는 다음과 같다.
[쓰레기값 * N] // 버퍼 채우기
[ROP 체인] // VirtualProtect 호출 및 매개변수 설정
[VirtualProtect 주소]
[JMP ESP 주소] // 쉘코드로 점프할 가젯
[쉘코드 주소] // lpAddress
[0x20000] // dwSize (128KB)
[0x40] // flNewProtect (PAGE_EXECUTE_READWRITE)
[쓰기 가능한 주소] // lpflOldProtect
[NOP 슬라이드] // 안정성 향상
[쉘코드] // 실행할 악성 코드