프로그램 검증 기술로 Heartbleed 장르의 버그가 발생하지 않을 수 있습니까?


9

Heartbleed 버그와 관련하여 Bruce Schneier는 4 월 15 일 Crypto-Gram에 'Catastrophic'이 올바른 단어라고 썼습니다. 1에서 10까지는 11입니다. ' 몇 년 전에 특정 운영 체제의 커널이 최신 프로그램 확인 시스템으로 엄격하게 검증되었다는 것을 읽었습니다. 따라서 Heartbleed 장르의 버그가 오늘날 프로그램 검증 기술의 적용을 통해 발생하는 것을 막을 수 있습니까? 아니면 이것이 여전히 비현실적이거나 심지어는 불가능합니까?


2
다음 은 J. Regehr의이 질문에 대한 흥미로운 분석입니다.
Martin Berger

답변:


6

가장 간결한 방법으로 귀하의 질문에 답변하기 위해 –이 버그는 공식적인 검증 도구에 의해 잠재적으로 발견되었을 수 있습니다. 실제로, "전송 된리스 비트 크기보다 큰 블록을 보내지 마십시오"속성은 대부분의 사양 언어 (예 : LTL)로 형식화하기가 매우 간단합니다.

문제는 (공식적인 방법에 대한 일반적인 비판) 사용하는 사양은 사람이 작성한다는 것입니다. 실제로 공식적인 방법은 버그 찾기 문제를 버그 찾기에서 버그 정의로 전환합니다. 이것은 어려운 작업입니다.

또한, 상태 폭발 문제로 인해 소프트웨어를 공식적으로 검증하는 것은 매우 어렵다. 이 경우, 국가 폭발을 피하기 위해 여러 번 있기 때문에 특히 관련이 있습니다. 예를 들어, "모든 요청 뒤에 100000 단계 내에서 보조금이 나옵니다"라고 말하려는 경우 매우 긴 공식이 필요하므로 "모든 요청 뒤에는 보조금이 뒤 따릅니다"라는 공식으로 요약합니다.

따라서 heartbleed 경우, 요구 사항을 공식화하려고 시도하는 중에도 해당 경계가 추상화되어 동일한 동작이 발생합니다.

요약하면, 공식적인 방법을 사용하여이 버그를 피할 수 있었지만,이 속성을 미리 지정해 놓은 사람이 있었을 것입니다.


5

Klocwork 또는 Coverity와 같은 상용 프로그램 검사기는 Heartbleed를 찾을 수 있었을 수 있습니다. Heartbleed는 검사하기 위해 설계된 주요 문제 중 하나 인 "경계 검사 오류를 잊어 버렸습니다". 그러나 훨씬 간단한 방법이 있습니다. 버퍼 오버런이없는 것으로 잘 테스트 된 불투명 한 추상 데이터 형식을 사용하십시오.

C 프로그래밍에 사용할 수있는 많은 "안전한 문자열"추상 데이터 유형이 있습니다. 내가 가장 익숙한 것은 Vstr 입니다. 저자 James Antill 은 왜 자체 생성자 / 팩토리 메소드 와 함께 문자열 추상 데이터 유형이 필요하고 C에 대한 다른 문자열 추상 데이터 유형 목록 이 필요한지에 대해 큰 토론을했습니다 .


2
Coverity가 Heartbleed를 찾지 못했습니다 . John Regehr 의이 분석 을 참조하십시오 .
Martin Berger

좋은 링크! 그것은 프로그램의 검증이 잘못 설계되었거나 존재하지 않는 추상화를 보완 할 수 없다는 이야기의 진정한 도덕을 보여줍니다.
방황 논리

2
프로그램 확인의 의미에 따라 다릅니다. 정적 분석을 의미한다면, 쌀 정리의 직접적인 결과로 항상 근사치입니다. 대화식 정리 전문가에서 전체 동작을 확인하면 프로그램이 해당 사양을 충족한다는 주철 보증을받을 수 있지만 매우 힘든 작업입니다. 그리고 당신은 여전히 ​​당신의 사양이 틀릴 수도 있다는 문제에 직면 해 있습니다 (예 : Ariane 5 폭발 참조).
Martin Berger

1
@MartinBerger : Coverity가 지금 찾습니다 .
Reinstate Monica-M. Schröder

4

런타임 바운드 검사와 퍼징의 조합을 " 프로그램 검증 기술  " 로 간주하면  이 특정 버그가 발견되었을 수 있습니다. .

적절한 퍼징은 memcpy(bp, pl, payload);메모리 블록의 한계를 넘어서 악명 높은 현재 읽기를 야기 할 것 pl입니다. 런타임 바운드 검사는 원칙적으로 이러한 액세스를 잡을 수 있으며 실제로는이 경우 특정 버전의 디버그 버전조차도 malloc매개 변수를 바운드 검사 memcpy하여 작업을 수행했을 것입니다 (여기서는 MMU를 망칠 필요가 없습니다) . 문제는 각 종류의 네트워크 패킷에 대해 퍼징 테스트를 수행하는 데 노력이 필요하다는 것입니다.


1
일반적으로 IIRC 인 OpenSSL의 경우 저자는 자체 내부 메모리 관리를 구현하여 memcpy원래 시스템에서 요청한 (대형) 영역의 실제 경계에 도달 할 가능성이 훨씬 적었 습니다 malloc.
윌리엄 가격

예. 버그가 발생한 시점의 memcpy(bp, pl, payload)OpenSSL의 malloc경우 시스템이 아닌 OpenSSL의 교체에 사용 된 범위를 확인해야했습니다 malloc. 이것은 바이너리 수준에서 자동 바운드 검사를 배제합니다 (적어도 malloc대체에 대한 깊은 지식없이 ). 토큰을 대체하는 C 매크로 malloc나 OpenSSL이 사용한 대체물을 사용하여 소스 레벨 마법사를 사용하여 재 컴파일해야합니다 . memcpy매우 영리한 MMU 트릭 을 제외하고 는 같은 것이 필요합니다 .
fgrieu

4

보다 엄격한 언어를 사용한다고해서 목표 ​​게시물이 구현을 올바르게 수행하는 것에서 스펙을 올바르게 얻는 것으로 이동하는 것이 아닙니다. 매우 잘못되었지만 논리적으로 일관성있는 것을 만드는 것은 어렵습니다. 이것이 컴파일러가 많은 버그를 잡는 이유입니다.

형식 시스템이 실제로 의미하는 것을 의미하지 않기 때문에 일반적으로 공식화되는 포인터 산술은 소리가 나지 않습니다. 가비지 수집 언어 (추상화를위한 일반적인 접근 방식)로 작업하면이 문제를 완전히 피할 수 있습니다. 또는 어떤 종류의 포인터를 사용하는지에 대해 더 구체적으로 지정할 수 있으므로 컴파일러는 일관성이 없거나 서면으로 올바르게 증명할 수없는 것을 거부 할 수 있습니다. 이것은 Rust와 같은 일부 언어의 접근 방식입니다.

생성 된 형식은 증명과 동일하므로이를 잊어 버린 형식 시스템을 작성하면 모든 종류의 문제가 발생합니다. 우리가 타입을 선언 할 때 실제로 변수에 무엇이 있는지에 대한 진실을 주장하고 있다고 가정합니다.

  • int * x; // 거짓 주장. x가 존재하고 int를 가리 키지 않습니다.
  • int * y = z; // z가 int를 가리키는 것으로 입증 된 경우에만 true
  • * (x + 3) = 5; // (x + 3)이 x와 동일한 배열의 int를 가리키는 경우에만 true
  • int c = a / b; // b가 0이 아닌 경우에만 "nonzero int b = ...;"
  • 널 입력 가능 int * z = NULL; // nullable int *는 int *와 같지 않습니다
  • int d = * z; // z는 nullable이므로 거짓 주장
  • if (z! = NULL) {int * e = z; } // z가 null이 아니기 때문에 Ok
  • 자유 (y); int w = * y; // y는 더 이상 w에 존재하지 않으므로 거짓 주장

이 세계에서 포인터는 null 일 수 없습니다. NullPointer 역 참조는 존재하지 않으며 포인터가 널 (null) 여부를 검사 할 필요가 없습니다. 대신, "nullable int *"는 값을 null 또는 포인터로 추출 할 수있는 다른 유형입니다. 이것은 널이 아닌 가정이 시작 되는 시점에서 예외를 로그하거나 널 브랜치로 내려가는 것을 의미합니다.

이 세계에서는 범위를 벗어난 배열 오류도 존재하지 않습니다. 컴파일러가 범위 내에 있음을 증명할 수 없으면 컴파일러가이를 입증 할 수 있도록 다시 작성하십시오. 만약 그것이 불가능하다면, 그 지점에서 가정을 수동으로 넣어야합니다; 컴파일러는 나중에 모순을 찾을 수 있습니다.

또한 초기화되지 않은 포인터를 가질 수 없으면 초기화되지 않은 메모리에 대한 포인터가 없습니다. 해제 된 메모리에 대한 포인터가 있으면 컴파일러가이를 거부해야합니다. Rust에는 이러한 종류의 증명을 기대할 수 있도록 다양한 포인터 유형이 있습니다. 독점적으로 소유 한 포인터 (즉, 별칭 없음), 불변의 구조에 대한 포인터가 있습니다. 기본 스토리지 유형은 변경할 수 없습니다.

또한 입력 표면 영역을 정확히 예상되는 것으로 제한하기 위해 프로토콜 (인터페이스 멤버 포함)에 대해 잘 정의 된 문법을 적용하는 문제도 있습니다. "정확성"에 대한 것은 다음과 같습니다. 1) 정의되지 않은 모든 상태를 제거 합니다. 2) 논리적 일관성을 유지 합니다. 거기에 도달하기 어려운 것은 (정확성의 관점에서) 극도로 나쁜 툴링을 사용하는 것과 관련이 있습니다.

이것이 바로 두 가지 최악의 관행이 전역 변수와 고 토스 인 이유입니다. 이러한 것들로 인해 사후 / 불변 / 불변 조건이 발생하지 않습니다. 또한 유형이 그렇게 효과적인 이유이기도합니다. 유형이 강해짐에 따라 (실제 값을 사용하기 위해 종속 유형을 사용함) 구조적 정확성 증명 자체에 접근합니다. 일치하지 않는 프로그램이 컴파일에 실패합니다.

바보 같은 실수가 아니라는 점을 명심하십시오. 또한 영리한 침입자로부터 코드 기반을 방어하는 것에 관한 것입니다. "공식적으로 지정된 프로토콜 준수"와 같은 중요한 속성에 대한 설득력있는 기계 생성 증거없이 제출을 거부해야하는 경우가 있습니다.



1

자동화 된 / 공식 소프트웨어 검증은 유용하며 어떤 경우에는 도움이 될 수 있지만 다른 사람들이 지적했듯이 은색 총알이 아닙니다. OpenSSL은 오픈 소스라는 점에서 취약하지만 상업 및 산업 전반에서 널리 사용되고 널리 사용되며 릴리스 전에 동료 검토를하지 않아도됩니다 (프로젝트에 유료 개발자가 있는지 궁금합니다). 결함은 기본적으로 릴리스 후 코드 검토를 통해 발견되었으며 코드는 릴리스 전으로 검토되었습니다 (내부 코드 검토를 수행 한 사람을 추적 할 방법은 없지만). Heartbleed (많은 것들 중에서도)가있는 "교육 가능한 순간"은 기본적으로 매우 민감한 코드를 릴리스하기 전에 이상적으로 더 나은 코드 검토입니다. 아마도 OpenSSL은 이제 더 면밀히 조사 될 것입니다.

출처를 자세히 설명하는 미디어에서 더 많은 bkg :

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.