제퍼넷 로고

정수 오버플로: 어떻게 발생하며 어떻게 방지할 수 있습니까?

시간

실수하지 마십시오. 컴퓨터에서 계산하는 것은 생각보다 쉽지 않습니다. 다음은 숫자가 "너무 커지면" 일어나는 일입니다.

IT 커뮤니티의 많은 사람들에게 2022년은 Microsoft Exchange Server의 온-프레미스 버전 버그로 인해 날짜 확인 실패로 인해 이메일이 도중에 중단된 후 좋지 않은 출발을 보였습니다. 간단히 말해서, 이후에 Y2K22라고 명명된 버그(약 2년 전 세계를 놀라게 했던 Y2022K 버그 스타일에서 명명됨)로 인해 소프트웨어가 XNUMX년의 날짜 형식을 처리할 수 없게 되었습니다. 마이크로소프트의 수정? 멀웨어 탐지 업데이트 날짜를 가상의 33월 XNUMX일로 다시 설정rd, 2021, 날짜 값을 충분히 제공 "호흡 공간" 기본 정수 유형이 보유할 수 있는 가장 높은 값에 도달하기 전에.

그러나 개발자를 위한 진정한 교훈은 자신의 날짜 처리 코드를 구현하는 것이 그러한 실수를 저지르고 가상 날짜를 사용하는 이상한 수정을 생각해 낼 위험이 너무 많다는 것입니다. 따라서 운영 체제, 컴파일러 등에 대한 루틴을 작성하지 않는 한 항상 표준 날짜 API를 사용해야 합니다.

2015년에 유사한 ilk의 버그가 Boeing의 787 Dreamliner 제트기의 소프트웨어에 영향을 미치는 것으로 밝혀졌습니다. 제 시간에 발견되지 않고 짓눌려 버그가 발생했을 수 있습니다. 모든 AC 전력의 총 손실, 비행 중에도 비행기에서 248일 연속 전원. 조종사가 여객기의 통제력을 잃는 것을 방지하는 솔루션은 무엇입니까? 787일이 끝나기 전에 248을 재부팅하거나 패치를 적용하는 것이 좋습니다.

그렇다면 Microsoft는 Exchange 맬웨어 방지 구성 요소의 업데이트가 여전히 2021년부터인 것처럼 가장해야 했던 이유는 무엇입니까? 비행기가 추락하지 않기 위해 왜 비행기를 껐다가 다시 켜야 합니까? 두 경우 모두 비디오 게임에서 GPS 시스템, 항공에 이르기까지 모든 유형의 소프트웨어에서 우려되는 취약점인 정수 오버플로에 대한 책임이 있습니다. 에서 2021년 CWE 가장 위험한 25가지 소프트웨어 취약점 32,500년과 2019년에 발표된 약 2020개의 CVE를 살펴본 목록에서 정수 오버플로 또는 랩어라운드가 XNUMX위를 차지했습니다.

소프트웨어 개발자는 수학적으로 너무 어려워서 언제 숫자가 바닥날지 예측할 수 없습니까? 사실은 더 복잡합니다. 정수 오버플로가 얼마나 어려운지 알아보기 위해 컴퓨터가 숫자를 저장하고 처리하는 방법을 좀 더 자세히 살펴보겠습니다.

정수란 무엇입니까?

수학에서 정수에는 1, 2, 3과 같은 양수, 숫자 0, -1, -2, -3과 같은 음수가 포함됩니다. 정수에는 분수나 소수가 포함되지 않습니다. 즉, 모든 정수의 집합은 다음과 같은 숫자 라인으로 표현될 수 있습니다.

 

일반적으로 프로그래밍 언어에는 여러 정수 변수 유형이 있습니다. 각 유형은 특정 시스템에서 해당 유형이 사용하는 비트 수에 따라 정수 값 범위를 저장합니다. 정수 유형이 사용하는 비트가 많을수록 그 안에 저장할 수 있는 값이 커집니다.

일반적인 x64 시스템에서 예상할 수 있는 비트 크기를 가정하여 C 프로그래밍 언어의 정수 유형을 고려해 보겠습니다.

A 이륜 전차 8비트 메모리를 사용하므로 다음 값을 저장할 수 있습니다.

 

공지하는 이륜 전차 최소 -128에서 최대 127까지만 값을 저장할 수 있습니다.

그러나 다른 "모드"가 있습니다. 이륜 전차 음이 아닌 정수만 저장하는 정수 유형:

 

정수 유형에는 부호 있는 모드와 부호 없는 모드가 있습니다. 서명된 형식은 음수 값을 저장할 수 있지만 서명되지 않은 형식은 저장할 수 없습니다.

다음 표는 프로그래밍 언어 C의 주요 정수 유형, 일반적인 x64 시스템에서의 크기, 저장할 수 있는 값의 범위를 보여줍니다.

표 1. 정수 유형, 일반적인 크기 및 범위 Microsoft C++(MSVC) 컴파일러 도구 세트

타입 크기(비트 너비) 범위
이륜 전차 8 부호 있는: -128 ~ 127
부호 없는: 0 ~ 255
짧은 정수 16 부호 있는: -32,768 ~ 32,767
부호 없는: 0 ~ 65,535
INT 32 부호 있는: -2,147,483,648 ~ 2,147,483,647
부호 없는: 0 ~ 4,294,967,295
긴 긴 정수 64 부호 있는: -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
부호 없는: 0 ~ 18,446,744,073,709,551,615

정수 오버플로란 무엇입니까?

정수 유형에 비해 너무 큰 값을 저장하려고 하면 정수 오버플로 또는 랩어라운드가 발생합니다. 정수 유형으로 저장할 수 있는 값의 범위는 둘러싸는 원형 숫자 라인으로 더 잘 표현됩니다. 부호 있는 char에 대한 원형 숫자 줄은 다음과 같이 나타낼 수 있습니다.

 

부호 있는 파일에 127보다 큰 숫자를 저장하려는 경우 이륜 전차, 카운트는 -128로 랩핑되고 거기에서 128을 향해 위쪽으로 계속됩니다. 따라서 128이어야 하는 대신 값 -129이 저장되고 127 대신 값 -XNUMX이 저장되는 식입니다.

문제를 거꾸로 보면 -128보다 작은 숫자를 부호 있는 파일에 저장하려고 하면 이륜 전차, 카운트는 127로 줄바꿈하고 거기에서 129을 향해 아래쪽으로 계속됩니다. 따라서 -127여야 하는 대신 값 129이 저장되고 -126 대신 값 XNUMX이 저장되는 식입니다. 이것은 때때로 정수 언더플로.

애매한 정수 오버플로 추적

정수 오버플로를 둘러싸는 값의 원으로 생각하면 이해하기가 상당히 쉽습니다. 그러나 정수 유형을 "캐스팅"하고 프로그램을 이식하고 컴파일하는 핵심 내용으로 내려가면 정수 오버플로 방지와 관련된 몇 가지 문제를 더 잘 이해할 수 있습니다.

캐스트 보기

때로는 원래 사용된 것과 다른 유형으로 값을 저장하는 것이 유용하거나 필요하기까지 합니다. 캐스트, 또는 "유형 변환"을 사용하면 프로그래머가 그렇게 할 수 있습니다. 일부 캐스트는 안전하지만 다른 캐스트는 정수 오버플로로 이어질 수 있기 때문에 안전하지 않습니다. 캐스트는 원래 값이 유지된다는 것이 보장될 때 안전한 것으로 간주됩니다.

더 작은(비트 너비 측면에서) 정수 유형에 저장된 값을 동일한 모드 내에서 더 큰 정수 유형으로 캐스팅하는 것이 안전합니다. 더 작은 부호 없는 유형에서 더 큰 부호 없는 유형으로, 더 작은 부호 있는 유형에서 더 큰 부호 있는 유형으로 유형. 따라서 서명 된에서 캐스팅하는 것이 안전합니다. 이륜 전차 서명된 짧은 정수 서명했기 때문에 짧은 정수 부호 있는 형식에 저장할 수 있는 모든 가능한 값을 저장할 수 있을 만큼 충분히 큽니다. 이륜 전차:

 

부호 있는 정수와 부호 없는 정수 간의 캐스팅은 값이 동일하게 유지된다고 보장할 수 없기 때문에 정수 오버플로에 대한 위험 신호입니다. 부호 있는 유형에서 부호 없는 유형으로 캐스팅할 때(더 큰 경우에도) 부호 없는 유형은 음수 값을 저장할 수 없기 때문에 정수 오버플로가 발생할 수 있습니다.

 

모든 음수 부호 이륜 전차 -128에서 -1까지 위 이미지의 빨간색 선 왼쪽에 있는 값은 정수 오버플로를 일으키고 부호 없는 유형으로 캐스트할 때 높은 양수 값이 됩니다. -128은 128이 되고 -127은 129가 되는 식입니다.

반대로, 서명되지 않은 유형에서 서명된 유형으로 캐스팅할 때 같은 크기의, 정수 오버플로는 unsigned 형식에 저장할 수 있는 양수 값의 상위 절반이 해당 크기의 서명 형식에 저장할 수 있는 최대 값을 초과하기 때문에 발생할 수 있습니다.

모든 높은 양의 부호 없는 이륜 전차 128에서 255 사이의 위 이미지에서 빨간색 선 왼쪽의 값은 정수 오버플로를 발생시키고 동일한 크기의 부호 있는 유형으로 캐스트할 때 음수 값이 됩니다. 128은 -128이 되고 129는 -127이 되는 식입니다.

서명되지 않은 형식에서 서명된 형식으로 캐스팅 더 큰 크기의 정수 오버플로의 위험이 없기 때문에 더 관대합니다. 더 작은 부호 없는 유형에 저장할 수 있는 모든 값은 더 큰 부호 있는 유형에도 저장할 수 있습니다.

암시적 업캐스트: 32비트에 대한 "편견"에 주의

정수 유형을 명시적으로 캐스팅하는 기능이 있으면 유용하지만 컴파일러가 연산자(산술, 이진, 부울 및 단항)의 피연산자를 암시적으로 업캐스트하는 방법을 알고 있어야 합니다. 많은 경우에 이러한 암시적 업캐스트는 피연산자를 연산하기 전에 더 큰 크기의 정수 유형으로 승격하기 때문에 정수 오버플로를 방지하는 데 도움이 됩니다. 실제로 기본 하드웨어는 일반적으로 동일한 유형의 피연산자에 대해 연산을 수행해야 하고 컴파일러는 더 작은 이 목표를 달성하기 위해 피연산자를 더 큰 값으로 변경합니다.

그러나 암시적 업캐스트에 대한 규칙은 32비트 유형을 선호하므로 결과는 때때로 프로그래머에게 예상치 못한 결과가 될 수 있습니다. 규칙은 우선 순위를 따릅니다. 첫째, 피연산자 중 하나 또는 모두가 64비트 정수 유형(긴 긴 정수), 다른 피연산자는 이미 하나가 아닌 경우 64비트로 업캐스트되고 결과는 64비트 유형입니다. 둘째, 피연산자 중 하나 또는 모두가 32비트 정수 유형(INT), 다른 피연산자는 32비트 유형으로 업캐스트되고, 그렇지 않은 경우 결과는 32비트 유형입니다.

이제 프로그래머를 쉽게 넘어뜨릴 수 있는 이 패턴의 예외가 있습니다. 피연산자 중 하나 또는 모두가 16비트 유형인 경우(짧은 정수) 또는 8비트 유형(이륜 전차), 피연산자 낙관적이다 연산이 수행되고 결과는 32비트 유형(INT). 이 동작의 예외인 유일한 연산자는 전치 및 후위 증가 및 감소 연산자(++, –)이며, 이는 16비트(짧은 정수) 피연산자는 예를 들어 업캐스트가 아니며 결과도 16비트입니다.

8비트 및 16비트 피연산자를 32비트로 "편견된" 업캐스트는 정수 오버플로를 방지하기 위한 검사를 수행할 때 이해하는 데 중요합니다. XNUMX개를 추가한다고 생각하시면 이륜 전차 또는 두 짧은 정수 유형이 암시적으로 업캐스트되어 있기 때문에 잘못 알고 있습니다. INT 유형 및 반환 INT 결과. 연산에 의해 32비트 결과가 반환되면 정수 오버플로를 확인하기 전에 결과를 16비트 또는 8비트로 다운캐스트해야 합니다. 그렇지 않으면 정수 오버플로를 감지하지 못할 위험이 있습니다. INT 상대적으로 작은 값으로 오버플로되지 않습니다. 짧은 정수 또는 이륜 전차 피연산자로 제공할 수 있습니다.

코드 이식성 문제 I – 다른 컴파일러

다른 아키텍처에서 실행할 수 있는 프로그램의 다른 빌드를 만드는 것은 소프트웨어 설계 시 중요한 고려 사항입니다. 다른 빌드에 대해 잘못 계획된 코드를 다시 작성하는 것은 골칫거리일 뿐만 아니라 주의를 기울이지 않으면 정수 오버플로로 이어질 수도 있습니다.

다른 대상 시스템에 대한 코드를 빌드하는 데 사용되는 컴파일러는 프로그래밍 언어의 표준을 지원할 것으로 예상되지만 정의되지 않고 컴파일러 개발자의 결정에 맡겨진 특정 구현 세부 사항이 있습니다. C에서 정수 유형의 크기는 C 언어 표준의 지침이 거의 없는 세부 정보 중 하나입니다. 즉, 컴파일러 및 대상 시스템에 대한 구현 세부 정보를 이해하지 못하는 것은 가능한 재앙의 지름길입니다.

표 1에 설명된 각 정수 유형이 사용하는 비트 수는 Microsoft C++(MSVC) 컴파일러 도구 집합에서 사용하는 체계입니다. C 컴파일러 포함, 32비트, 64비트 및 ARM 프로세서를 대상으로 하는 경우. 그러나 C 표준을 구현하는 다른 컴파일러는 다른 체계를 사용할 수 있습니다. 라는 정수형을 생각해봅시다. .

MSVC 컴파일러의 경우 소모하다 빌드가 32비트 또는 32비트 프로그램인지 여부에 관계없이 64비트입니다. 그러나 IBM XL C 컴파일러의 경우 긴 소모하다 32비트 빌드에서는 32비트, 64비트 빌드에서는 64비트입니다. 모든 빌드에 대해 정수 오버플로를 올바르게 확인하려면 정수 유형이 보유할 수 있는 크기와 최대값 및 최소값을 아는 것이 중요합니다.

코드 이식성 문제 II – 다른 빌드

주의해야 할 또 다른 이식성 문제는 size_t, 부호 없는 정수 유형이며, ptrdiff_t, 부호 있는 정수 유형입니다. 이러한 유형 소비 MSVC 컴파일러의 경우 32비트 빌드의 경우 32비트 및 64비트 빌드의 경우 64비트입니다. 피연산자 중 하나가 이러한 유형 중 하나인 비교를 기반으로 분기할지 여부를 결정하는 코드 조각은 32비트 빌드 또는 64비트 빌드의 일부인지 여부에 따라 프로그램을 다른 실행 경로로 이끌 수 있습니다. 짓다.

예를 들어, 32비트 빌드에서 ptrdiff_t unsigned int는 컴파일러가 ptrdiff_t 서명되지 않은 INT 따라서 음수 값은 높은 양수 값이 됩니다. 정수 오버플로는 실행 중인 프로그램의 예기치 않은 경로 또는 액세스 위반으로 이어집니다. 그러나 64비트 빌드에서 컴파일러는 서명되지 않은 INT 부호 있는 64비트 형식으로 변환합니다. 즉, 정수 오버플로가 없고 프로그램의 예상 경로가 실행됩니다.

정수 오버플로가 버퍼 오버플로로 이어지는 방법

정수 오버플로 취약점이 악용될 수 있는 주요 방식은 버퍼에 저장될 데이터의 길이를 제한하는 검사를 우회하여 다음을 유도하는 것입니다. 버퍼 오버 플로우. 이는 권한 상승 및 임의 코드 실행과 같은 추가 문제를 야기하는 광범위한 버퍼 오버플로 악용 기술에 대한 문을 엽니다.

다음과 같은 인위적인 예를 고려해 보겠습니다.