
CVE-2020-8597 (pppd EAP)의 합성 CWE-120 스택 버퍼 오버플로 변형: CodeQL 정적 분석 대상
pppd의 함수 이름, 호출 깊이, 디스패치 구조를 전혀 공유하지 않는 프로그램에서 CVE-2020-8597 — pppd EAP rhostname 스택 버퍼 오버플로(CWE-120) — 의 버그 클래스를 재현한, 의도적으로 취약한 약 170줄짜리 C 프로그램으로, CodeQL 정적 분석 타깃으로 사용됩니다.
목표는 일반성 테스트입니다. pppd 버그를 잡기 위해 작성된 CodeQL 쿼리가 수정 없이 이 프로그램에서도 발동해야 합니다. 그렇다면 그 쿼리는 원본 코드의 형태가 아니라 버그 클래스를 표현한 것입니다.
이 프로그램은 의도적으로 안전하지 않으며 분석 목적으로만 존재합니다. 배포하지 마십시오. 이것이 모방하는 버그는 공개된 것입니다(CVE-2020-8597, 2020년 공개).
공격자로부터 유래한 길이가 고정 크기 버퍼로 복사되며, 그 길이를 버퍼의 크기와 연관시키는 가드가 없습니다.
모든 경우에 경계 검사는 존재합니다 — 다만 중요한 두 값 사이의 관계를 맺는 데 실패할 뿐입니다. 이를 잘못하는 방법은 정확히 두 가지이며, 이 프로그램에는 각각 하나씩 들어 있습니다:
sizeof(dest)가 아니라 수신된 프레임에 대해 검사됩니다. 오버리드는 막지만 오버라이트에는 아무것도 하지 않습니다. (handle_hello)sizeof(dest)를 지칭합니다 — 정확히 버퍼 경계처럼 보입니다 — 하지만 복사 길이로 사용되는 변수가 아닌 다른 변수를 제약합니다. (handle_stat)두 번째가 더 어려운 경우이며, 이것이 pppd의 죽은 vallen >= len + sizeof(rhostname) 검사입니다: 목적지 크기를 언급하면서 복사 길이가 아닌 무언가를 제약하는 비교입니다. "여기 어떤 비교가 sizeof(dest)를 언급하는가?" 만 묻는 쿼리는 이에 의해 침묵됩니다.
| pppd / CVE-2020-8597 | 이 프로젝트 | |
|---|---|---|
| 소스 | PPP fd에 대한 read() | UDP 소켓에 대한 recvfrom() |
| 디스패치 | 전역 struct protent *protocols[], 프로토콜 번호에 대한 선형 매칭 | 파일 로컬 const struct frame_op ops[], 1바이트 태그에 대한 선형 매칭 |
| 싱크까지의 깊이 | get_input → (*input) → eap_input → eap_request | dispatch_frame → (*handle) → handle_hello |
| 목적지 | char rhostname[256] | char name[64] |
| 잘못된 가드 | vallen이 패킷 len에 의해 제한됨 | vlen이 프레임 plen에 의해 제한됨 |
둘 다 이것을 grep이 아닌 데이터 흐름으로 만드는 한 가지 속성을 유지합니다: 소스와 싱크 사이에 있는 함수 포인터 테이블을 통한 간접 호출입니다.
| 핸들러 | 줄 | 존재하는 검사 | 판정 |
|---|---|---|---|
handle_hello() | :80의 싱크 | vlen > plen - 2 — 올바른 값, 잘못된 경계 | 발동해야 함 |
handle_echo() | :106의 복사 | vlen >= sizeof(buf) — 둘 다 올바름 | 침묵을 유지해야 함 — 네거티브 컨트롤 |
handle_stat() | :145의 싱크 | hlen >= sizeof(report) — 올바른 경계, 잘못된 값 | 발동해야 함 |
handle_stat이 판별 사례입니다. 그 검사는 sizeof(report)를 지칭하므로, 목적지 크기를 언급하는 어떤 비교든 받아들이는 쿼리는 이를 가드된 것으로 취급하고 버그를 놓칩니다. 이를 잡으려면 검사되는 값과 복사 길이로 사용되는 값을 비교해야 합니다 — 전역 값 번호 매기기(global value numbering). 쿼리에서 그것을 제거하면 이 핸들러는 거짓 음성이 되지만 다른 모든 지점은 판정을 유지합니다.
하나의 UDP 데이터그램 = 하나의 프레임:
[ type : 1 ] [ length : 2, big-endian ] [ value : length bytes ]
type 0x01 → hello, 0x02 → echo, 0x03 → stat. 선언된 길이가 65에서 ~2045 사이인 hello 프레임은 name[64]를 오버플로합니다. stat 프레임은 대신 두 개의 1바이트 길이를 전달합니다 — 헤더 길이와 본문 길이 — 그리고 헤더 길이가 무엇을 말하든 32를 초과하는 본문 길이는 report[32]를 오버플로합니다.
make # gcc -Wall -Wextra -O0 -g -o tlv_server tlv_server.c
Linux/POSIX (BSD 소켓). 경고 없이 깨끗하게 빌드됩니다.
CodeQL은 실제 컴파일을 추적하므로, 클린 상태에서 빌드하십시오:
make clean
codeql database create db --language=cpp --command="make"
# or, without the clean step:
codeql database create db --language=cpp --command="make -B"
그런 다음 db에 대해 Part 3 쿼리를 실행하십시오. handle_hello의 memcpy와 handle_stat의 memcpy를 보고하고, handle_echo에 대해서는 침묵을 유지해야 합니다. 쿼리와 실행 지침은 codeql/에 있습니다.
핸들러가 추가될 때마다 소스가 변경되므로 데이터베이스를 다시 빌드하십시오 — CodeQL은 database create 시점의 코드를 스냅샷하며, 기존 db/는 새 코드를 보지 못합니다.