
Savant Web Server 3.1에서 조작된 HTTP 요청을 통해 2000년대 초반의 원격 메모리 손상을 시연하는 고전적인 스택 기반 버퍼 오버플로우입니다.
Savant Web Server 3.1의 전형적인 스택 기반 버퍼 오버플로우로, 조작된 HTTP 요청을 통해 2000년대 초반의 원격 메모리 손상을 입증합니다. 2025년의 대부분의 버퍼 오버플로우 CVE보다 초보자에게 더 많은 것을 요구하는 2002년의 취약점입니다.
이 저장소는 제가 메모리 손상(memory corruption) 익스플로잇을 가르칠 때 사용하는 자료의 일부입니다. (본업 외에도 여러 사이버 보안 과정에서 강의하며 차세대 리버스 엔지니어를 양성하는 데 기여하고 있습니다.) 수업에서 스택 기반 버퍼 오버플로우를 다룰 때 저는 장난감(toy) 예제가 아닌 실제 사례를 다룹니다. 그리고 기본적인 EIP 덮어쓰기 경험이 있는 학생들에게 도전 과제를 주고 싶을 때 특히 이 CVE를 다시 꺼내 듭니다.
그 이유는 CVE 식별자에 포함된 연도가 익스플로잇의 난이도에 대해 아무것도 알려주지 않기 때문입니다. 대부분의 학생들은 오래된 CVE일수록 더 단순할 것이라고 가정합니다. 단순한 오버플로우, JMP ESP, 끝. 그러나 이 CVE는 그렇지 않다는 것을 증명합니다. 제가 가르치는 2025년의 일부 CVE는 취약점 클래스가 더 단순하거나 제약 조건이 더 적기 때문에 이 CVE보다 근본적으로 더 기초적입니다. CVE-2002-1120은 일부 현대 익스플로잇이 요구하지 않는 것들을 이해해야 합니다: 특정 opcode를 요청의 특정 부분에서 사용할 수 없는 이유, 무조건 점프가 차단되었을 때 항상 실행되는 조건부 점프를 구성하는 방법, 평소에는 shellcode를 놓던 위치에 배치할 수 없을 때 전달하는 방법, 그리고 egghunter가 어떻게 공간 문제를 해결해 익스플로잇을 가능하게 하는지가 그것입니다.
이것은 누군가가 실제로 자신이 무엇을 하고 있는지 이해하고 있는지 확인하고 싶을 때 제가 내주는 CVE입니다.
이 취약점은 2002년에 나온 프리웨어 Windows HTTP 서버인 Savant 3.1에 영향을 줍니다. 2025년에도 연구할 가치가 있는 이유는 연식이 아니라, 이 취약점이 제시하는 제약 조건들의 조합과 이를 우회하는 데 필요한 기법들입니다:
이러한 제약 조건 하나하나는 그 자체로 하나의 교훈입니다.
Savant는 원래 모든 데스크톱 컴퓨터를 웹 서버로 바꾸도록 설계된 프리웨어 오픈소스 Windows HTTP 서버입니다. TCP 포트 80에서 수신 대기하며 표준 HTTP GET 요청을 처리합니다. 오버플로우는 URI 경로를 길이 검증 없이 고정 크기 스택 버퍼에 복사하는 핸들러에서 발생합니다.
주요 기술 세부 사항:
Savant는 수신되는 HTTP GET 요청을 처리하고 URI 경로를 길이를 확인하지 않고 고정 크기 스택 버퍼에 복사합니다. 취약한 로직을 단순화하면 다음과 같습니다:
char uri_buffer[270];
strcpy(uri_buffer, uri_path);
약 271바이트의 URI를 포함한 GET 요청을 보내면 스택에 저장된 리턴 주소가 덮어써집니다. 오버플로우에는 제약이 있습니다. 그보다 훨씬 많은 양을 보내면 EIP를 더 이상 제어할 수 없는 방식으로 크래시가 발생하므로, 사용 가능한 범위는 좁습니다.
이 서버의 두 가지 추가 속성 때문에 익스플로잇은 표준 EIP 덮어쓰기보다 더 복잡합니다: HTTP 메서드 필드도 메모리에 복사되어 실행되며, HTTP 요청 본문은 스택이 아닌 별도의 힙 영역에 저장됩니다. (이 두 속성 모두 익스플로잇 체인의 필수적인 구성 요소가 됩니다.)
충분히 긴 URI를 포함한 GET 요청을 보내면 크래시를 재현할 수 있습니다. 인증은 필요하지 않습니다. Python 예제:
import socket
HOST = '127.0.0.1'
PORT = 80
payload = b"A" * 271
request = (
b"GET /" + payload + b" HTTP/1.1\r\n"
b"Host: 127.0.0.1\r\n"
b"Connection: close\r\n"
b"\r\n"
)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.send(request)
s.close()
디버거에서 실행하면 크래시 시점에 EIP가 사용자 제어 데이터로 덮여 있는 것을 확인할 수 있습니다:
EIP = 41414141
즉, 저장된 리턴 주소가 오버플로우로 인해 손상되었음을 확인할 수 있습니다.
이 저장소의 목표는 크래시를 시연하는 것뿐만 아니라, 표준 EIP 덮어쓰기보다 이 사례를 더 어렵게 만드는 모든 제약 조건과 이를 우회하기 위해 내린 모든 결정을 포함하여 전체 익스플로잇 과정을 단계별로 안내하는 것입니다.
메인 README를 깔끔하게 유지하기 위해, 상세한 익스플로잇 노트, 스크립트 및 디버거 단계는 이 저장소의 Vulnerability 📂 폴더에 정리되어 있습니다.
그곳에서 이 CVE를 익스플로잇하는 데 사용된 전체 워크플로우를 확인할 수 있습니다: