음소거: Outlook에서 RCE를 달성하기 위한 취약점 연결: 2부
서론
이 블로그 시리즈의 1부에서설명한 취약점을 이용하면 Outlook의 알림 소리 기능을 악용하고 표적에서 사용자 지정 사운드 파일을 다시 한 번 재생할 수 있었습니다. 이 기능을 활용해 완전한 원격 코드 실행(RCE)으로 전환하기 위해, Windows에서 사운드 파일 구문 분석의 취약점을 찾기 시작했습니다.
공격표면
Outlook에서 재생하는 사운드 파일은 WAV( Waveform Audio File ) 포맷입니다. 이 파일은 사운드 파일 경로를 수신하는 PlaySound 함수를 통해 재생됩니다. PlaySound는 파일을 로드하고 구문 분석한 다음 soundOpen을 호출해 waveOutOpen같은 다양한 파형 함수를 호출합니다.
WAV 파일은 여러 오디오 코덱을 위한 컨테이너(또는 래퍼) 역할을 합니다. 코덱은 데이터 스트림(예: 이미지, 비디오 또는 오디오)을 인코딩하거나 디코딩하는 프로그램이나 코드입니다. 일반적으로 코덱은 샘플링된 아날로그 신호를 표현하는 간단한 방법인 PCM( Pulse-Code Modulation ) 인코딩을 사용합니다.
취약점을 검색할 수 있는 세 가지 주요 공격표면이 있습니다.
WAV 포맷 구문 분석
Audio Compression Manager
다양한 오디오 코덱
WAV 포맷 구문 분석
WAV 포맷 구문 분석은 winmm.dll(Windows 멀티미디어 API를 구축하는 라이브러리)의 soundInitWavHdr 함수 내부에 구축되어 있습니다. 이 함수의 공격표면은 크지 않고 검토가 완료된 것으로 보이며 취약점은 발견되지 않았습니다.
Audio Compression Manager란 무엇일까요?
ACM(Audio Compression Manager)은 WAV 파일에 사용된 코덱이 단순한 PCM 인코딩을 사용하지 않아서 사용자 지정 디코더로 디코딩해야 하는 경우를 처리하는 코드입니다. 이러한 디코더는 확장자가 .acm인 파일로 구축됩니다. 널리 사용되는 예 중 하나는 l3codeca.acm으로 구축된 MP3 코덱입니다. 각 코덱은 ACM을 통해 등록되는 드라이버(커널 모드 드라이버와는 다르지만 기능은 비슷함)에 의해 처리됩니다.
MP3를 PCM으로 변환하거나 그 반대로 변환하는 등 변환이 필요할 때마다 ACM이 작동하여 변환을 관리합니다. PCM이 사용되지 않는 WAV 파일을 사용하는 경우, 파일 자체에 지정된 코덱이 존재하고 변환을 처리할 수 있는지 확인하기 위해 ACM을 쿼리합니다(그림 1).
각 ACM 드라이버는 acmdStreamSize 및 acmdStreamOpen같은 함수를 구축해야 합니다. 첫 번째 함수는 변환 출력에 필요한 크기(바이트 단위)를 반환하고, 두 번째 함수는 스트림 구조체를 생성하며 디코딩 콜백 함수와 같은 적절한 필드를 설정합니다.
ACM의 공격표면은 그리 크지 않습니다. 하지만 이 블로그 게시물의 뒷부분에서 설명하겠지만 해당 코드에서 취약점을 발견할 수 있었습니다.
다양한 오디오 코덱
마지막 공격표면의 경우, 기본적으로 설치되는 오디오 코덱이 서로 다릅니다. 코덱은 두 가지 방법을 사용하여 WAV 파일에 지정됩니다.
FORMAT 청크의 wFormatTag
wFormatTag가 WAVE_FORMAT_EXTENSIBLE인 경우 SubFormat이 오디오 코덱의 GUID를 보유합니다.
사용 가능한 코덱 목록은 부록에 설명되어 있습니다.
오디오 신호 처리의 기초
코드를 살펴보기 전에 오디오 신호 처리의 몇 가지 기본 사항에 대해 알아보세요. 이러한 개념에 이미 익숙하다면 다음 섹션으로 건너뛰셔도 좋습니다.
우리가 소리를 들을 때 실제로 듣는 것은 전송 매체를 통해 전파되는 진동입니다. 소리는 이러한 진동파가 귀에 포착되어 뇌에서 인식되는 것을 말합니다.
오디오 신호는 연속적인 파형입니다. 이를 디지털로 처리하려면 아날로그 신호에서 디지털 신호로 변환해야 합니다(예: ADC 컨버터 사용). 이 변환을 아날로그 신호의 이산화라고 합니다. 이를 위해 아날로그 신호를 균일한 간격의 데이터 포인트( 샘플이라고 함)로 여러 번 샘플링합니다. 샘플링 속도 (샘플링 빈도라고도 함)에 따라 초당 채취하는 샘플 수가 결정됩니다. 샘플링 속도가 높을수록 더 많은 세부 정보를 캡처할 수 있지만, 더 많은 저장 공간과 처리가 필요합니다.
샘플링 속도 외에도 각 샘플에 사용되는 비트 수인 샘플 크기가있습니다. 다시 한 번 말하지만, 샘플 크기가 클수록 샘플의 품질이 더 좋아지거나 원음에 더 가깝습니다. 샘플 크기는 일반적으로 샘플당 16비트 또는 24비트입니다.
오디오 코덱은 심리 음향 모델을 기반으로 합니다. 심리 음향학은 인간의 청각 시스템이 다양한 소리를 어떻게 인식하는지에 대한 소리 인식과 청각학에 대한 과학적 리서치입니다. 예를 들어, 인간의 청각 범위는 20Hz에서 20,000Hz 사이입니다. 따라서 파일 크기를 더욱 줄이기 위해 오디오 코덱은 사람이 들을 수 없는 주파수를 제거할 수 있습니다. 또한 오디오 코덱은 신호의 볼륨이 사람의 귀가 들을 수 있을 정도로 강하지 않은 경우 신호를 제거할 수 있습니다. 예를 들어, 20Hz 사운드는 60데시벨 미만이면 들리지 않습니다.
심리 음향 모델의 예로는 조용한 신호가 시끄러운 신호와 주파수나 시간이 가까울 때 마스킹하는 것 등 더 많은 예가 있습니다.
신호의 분석, 해석 및 수정은 신호를 서브밴드(신호의 특정 부분을 더 자세히 검사하고 조작할 수 있는 별개의 구성요소 대역)로 나누는 필터 뱅크를 사용하여 수행됩니다. 일반적으로 사용되는 필터 뱅크는 DCT 및 다상 필터 뱅크입니다.
이러한 기본 지식을 염두에 두고 다양한 코덱을 리서치할 수 있습니다.
첫 번째 시도: 샘플 버퍼에 대한 범위를 벗어난 쓰기
MP3가 다른 코덱에 비해 훨씬 더 복잡하기 때문에 당사는 먼저 MP3를 살펴보기로 했습니다. 대부분의 다른 코덱은 다소 단순한 변환만 수행하지만 MP3는 디코딩 과정에서 여러 단계를 거칩니다(그림 2).
MP3 오디오 데이터는 일련의 프레임으로 구성되며, 각 프레임은 오디오의 작은 세그먼트를 나타냅니다. 프레임은 헤더와 오디오 데이터로 구성됩니다. 오디오 데이터는 Huffman 코딩을 사용하여 압축됩니다. 각 프레임은 채널당(모노/스테레오) 정확히 1,152개의 주파수 영역 샘플을 나타냅니다. 이는 각각 576개의 샘플로 구성된 두 개의 청크로 분할됩니다. 각 프레임에는 사이드 정보라고 하는 디코딩과 관련된 정보도 포함되어 있습니다 (그림 3).
MP3 디코딩 프로세스의 일부로 수행되는 대부분의 작업은 복잡하며, 이론적으로는 미묘한 취약점을 찾기에 흥미롭고 멋진 장소가 될 수 있지만 실제로는 많은 작업(예: 수정된 DCT, 다상 필터뱅크, 앨리어스 감소)이 576개 샘플에 대한 값을 지속적으로 보유하는 버퍼에서 수행됩니다. 따라서 여기서 범위를 벗어난 쓰기 취약점을 찾는 것은 타당하지 않습니다. 한 가지 흥미로운 점은 Huffman 디코딩이며, 이는 576개 샘플 버퍼와 달리 보다 동적인 데이터에서 자연스럽게 작동하기 때문입니다.
MP3 Huffman 디코딩
입자 단위(576개 샘플)의 Huffman 코딩은 (바이너리 트리 대신) 코드 테이블을 사용하여 구축됩니다. 0에서 22,050까지의 총 주파수 범위(Nyquist 주파수)는 다음과 같이 5개의 영역으로 나뉩니다(그림 4).
1, 2, 3. "큰 값"의 세 영역 — 값이 -8,206에서 8,206 사이인 샘플
4. "count1 영역" — 값 -1, 0 또는 1의 4중 영역
5. "rzero 영역" — 높은 주파수 값은 진폭이 낮은 것으로 간주되므로 코딩할 필요가 없으며, 이 값은 0과 같습니다.
각 영역에는 고유한 Huffman 테이블이 있으며(0 영역 제외), 따라서 서로 다른 주파수의 샘플은 다르게 코딩됩니다.
샘플을 지역으로 분류하는 것은 다음 변수에 따라 달라집니다.
big_values — 큰 값 영역에 있는 총 샘플 수를 지정합니다
region0_count 및 region1_count — big_values를 하위 지역으로 분할하고, big_values에서 그 합을 빼면 region2의 샘플 수가 산출됩니다.
part2_3length — 스케일 팩터(파트 2)에 사용되는 비트 수와 Huffman 코딩(파트 3)에 사용되는 비트 수를 지정합니다.
576개의 샘플을 디코딩하는 과정은 다음과 같이 진행됩니다.
big_values 영역의 샘플을 디코딩합니다
count1 영역의 샘플을 디코딩합니다
처리된 비트 수가 part2_3length보다 크면 사실상 모든 인풋 데이터를 디코딩한 것이고, 심지어 데이터를 오버리드한 것이므로 total_samples_read에서 4를 뺍니다(즉, 이러한 입력 비트는 버립니다).
샘플이 남아 있으면 0으로 채웁니다(0 영역이 형성됩니다).
표 1의 이 로직은 의사 코드로 표시되어 있습니다.
total_samples_read = 0;
// Decode big_values region
[redacted for brevity]
// Decode count1 region [1]
for (int i = 0; i < count1; i++) {
samples[total_samples_read++] = huff_decode(bitstream, count1_huff_table);
}
if (bits_processed > part2_3length) [2] {
// Overread. Throw last 4 samples
total_samples_read -= 4;
}
// Fill rzero region with zeros
for (int i = total_samples_read; i < 576; i++) {
samples[total_samples_read++] = 0; [3]
}
표 1: Huffman 디코딩 로직의 의사 코드
안타깝게도 이 코드에는 특정 엣지 케이스 하나가 누락되어 정수 언더플로우가 발생합니다.
Big values 영역 크기가 0입니다
Count1 영역 크기가 0입니다
Part2_3length가 0입니다
Bits_processed가 0보다 큽니다
이 특정 사례에서는 bits_processed가 part2_3length보다 클 것입니다(스케일 팩터 디코딩 중 디코딩 프로세스 전에 0이 아닌 값에 도달). 따라서 이 코드는 마지막 4개의 샘플을 ‘던지게’ 됩니다. 코드가 샘플을 처리하지 않았기 때문에 total_samples_read는 0입니다. 여기에는 언더플로우가 발생하고 코드에서는 -4개의 샘플을 처리한 것으로 간주합니다. 이제 다음과 같이 0 영역을 채웁니다.
버퍼 포인터를 &samples[total_samples_read]로 설정합니다. 이것은 샘플 버퍼 앞의 16바이트를 가리킵니다.
쓰기 크기를 576 - total_samples_read = 576 - (-4) = 580개 정수로 설정합니다.
따라서 샘플 버퍼 바로 앞에 값이 0인 범위를 벗어난 쓰기가 발생합니다. 깔끔하죠!
그렇다면 왜 이 취약점은 CVE를 받지 못했을까요? 샘플 버퍼는 구조체의 일부이고, 샘플 버퍼 바로 앞의 필드가 스케일팩터 배열이기 때문입니다. 이 배열은 우리가 이미 제어하고 있는 필드가 있는 배열이기 때문에 별다른 영향을 미치지 않습니다.
코드가 샘플을 역양자화한 후에도 동일한 취약점이 발생합니다. 이번에는 0 영역을 다시 한 번 역양자화된 버퍼로 채웁니다. 역양자화된 버퍼 앞에 무엇이 있는지 궁금하신가요? Huffman 디코딩된 샘플 버퍼입니다. 이것은 앞서 본 것과 동일한 샘플 버퍼이며, 우리가 제어할 수 있습니다. 따라서 다시 한 번 말하지만, 실제 영향은 없습니다.
이러한 범위를 벗어난 쓰기는 MP3 디코더(WAV와 .mp3 파일을 통해 모두 접속 가능)에 여전히 존재하며, Microsoft에 따르면 향후 수정될 수 있다고 합니다. 코덱을 반전시키는 동안 영향력 있는 취약점은 발견되지 않았지만, 디코더가 수행하는 여러 가지 복잡한 작업에 숨어 있는 취약점이 있을 수 있다고 생각합니다.
두 번째 시도: IMA ADPCM 코덱의 정수 오버플로우
다음 시도는 imaadp32.acm에 구축된 IMA ADPCM 코덱입니다. 이제 알다시피 ACM은 서로 다른 코덱 간의 변환을 관리할 것입니다. 코덱을 등록하려면 코드가 ACM 함수를 구축해야 합니다. 이러한 함수 중 하나는 대상 버퍼에 필요한 바이트 수를 반환하는 acmStreamSize입니다.
IMA ADPCM 코덱은 입력 페이로드의 크기(cbSrcLength), 정렬(nBlockAlign, 블록당 샘플(wSamplesPerBlock, 표 2)을 기반으로 대상 버퍼 크기를 계산합니다.
(cbSrcLength / pwfxSrc->nBlockAlign) *(pwfxSrc->wSamplesPerBlock * pwfxDst->nBlockAlign)
표 2: 버퍼 크기 계산
곱하기 전에 이 코드는 계산이 정수 오버플로우를 초래하지 않는지 확인합니다(표 3).
SrcNumberOfBlocks = cbSrcLength / pwfxSrc->WaveFormat.nBlockAlign;
v14 = pwfxSrc->wSamplesPerBlock * pwfxDst->nBlockAlign;
if ( 0xFFFFFFFF / v14 < SrcNumberOfBlocks )
return ERROR_OVERFLOW;
IsThereRemainder = cbSrcLength % pwfxSrc->WaveFormat.nBlockAlign;
if ( IsThereRemainder )
++SrcNumberOfBlocks;
DstBufferLengthInBytes = v14 * SrcNumberOfBlocks;
표 3: 정수 오버플로우를 방지하기 위한 계산 검사
이 검사만으로는 오버플로우를 방지하기에 충분하지 않습니다. cbSrcLength / pwfxSrc->nBlockAlign의 나눗셈에 나머지가 있는 경우, 코드는 곱셈에 사용되는 (cbSrcLength / pwfxSrc->nBlockAlign)의 결과를 증가시킵니다. 오버플로우 검사는 이 증가를 다루지 않습니다. 따라서 사용자 정의 값을 지정해 대상 버퍼 길이를 오버플로우할 수 있습니다.
이를 위해서는 cbSrcLength , 즉 pwfxSrc->nBlockAlign으로나눴을 때 나머지가 있는 값을 제공해야 합니다.
표 4는 정수 오버플로우를 유발하는 값의 예를 보여줍니다.
cbSrcLength = 0x71c71c72
pwfxSrc->nBlockAlign = 8
pwfxSrc->wSamplesPerBlock = 9
pwfxDst->nBlockAlign = 2
표 4: 정수 오버플로우를 유발하는 값의 예
결과적으로 대상 버퍼는 0xE 바이트가 되지만 대상 버퍼는 훨씬 더 커야 합니다.
이는 범위를 벗어난 쓰기로 이어질 수 있는 정수 오버플로우처럼 보이지만, 디코딩 함수는 할당된 버퍼 이후에 쓰기가 발생하지 않도록 올바르게 확인하며, 버퍼가 올바른 크기로 할당되었다고 가정하지 않습니다.
따라서 여러 샘플을 제공하지만 실제로는 대상 버퍼가 가득 차면 코드가 중지됩니다. AD PCM 코덱( msadp32.acm에서 구축됨)에서도 동일한 동작이 발생합니다.
세 번째 시도: ACM의 정수 오버플로우(CVE-2023-36710)
마지막으로 ACM 코드에서 멋진 취약점을 발견했습니다. WAV 파일을 재생하는 과정의 일부로, ACM 관리자의 mapWavePrepareHeader 함수( msacm32.drv에 구축)가 호출됩니다.
이 함수에는 정수 오버플로우 취약점이 있습니다. 이 함수는 드라이버의 콜백을 호출하는 acmStreamSize를호출합니다. 이 함수는 대상 버퍼에 필요한 크기를 반환한다는 점을 기억하세요. 이 크기를 받은 후 mapWavePrepareHeader는 오버플로우를 확인하지 않고 176바이트(대상 버퍼를 진행할 스트림 헤더의 크기)를 추가합니다. 이 추가 결과는 GlobalAlloc으로 전달됩니다(그림 6).
이것은 악용 가능한 문제입니다. GlobalAlloc이 큰 버퍼 대신 아주 작은 버퍼를 할당하게 하려면 acmStreamSize가 0xffffff50에서 0xffffffff 사이의 값을 반환하도록 하면 됩니다. 이 할당 후에는
구조체 크기와 소스 같은 스트림 헤더와 대상 버퍼 포인터 및 크기의 범위를 벗어난 쓰기가 두 번 발생할 수 있습니다. 이러한 값은 부분적으로 제어할 수 있습니다.
코덱의 디코딩된 값. 이 값은 완전히 제어할 수 있습니다.
취약점을 트리거하려면 디코딩할 때 0xffffff50보다 크거나 같은 크기의 WAV 샘플을 제공해야 합니다. 이는 쉽게 달성할 수 있는 것처럼 보이지만 일부 코덱에서는 달성하지 못할 수도 있다는 것을 실험을 통해 알게 되었습니다. 예를 들어 MP3 코덱의 경우, 계산의 일부로 1,152 또는 576(프레임당 샘플 수)을 곱해야 합니다. 이 계산의 결과는 결코 원하는 범위에 속하지 않습니다.
마지막으로, 당사는 IMA ADP 코덱을 사용하여 취약점을 트리거하는 데 성공했습니다. 파일 크기는 약 1.8GB입니다. 계산에 수학 제한 연산을 수행함으로써 IMA ADP 코덱을 사용하여 가능한 최소 파일 크기가 1GB라는 결론을 내릴 수 있습니다.
동적으로 악용을 제작할 수 있는 스크립팅 엔진이 있으면 이러한 취약점을 더 쉽게 악용할 수 있습니다. Windows Media Player에는 스크립팅 엔진이 없기 때문에 악용이 더 어려울 수 있습니다. 크리스 에반스(Chris Evans)가"악용 발전: Linux 데스크톱을 겨냥한 스크립트리스 제로데이 악용"블로그 게시물에서 설명한 것처럼 악용이 여전히 가능할 수도 있습니다. 하지만 Outlook 애플리케이션(또는 다른 인스턴트 메시징 애플리케이션)에서 이 취약점이 성공적으로 악용될 가능성이 더 높습니다.
요약
이 블로그 시리즈에서는 인터넷에서 악용된 취약점으로부터 시작된 리서치를 다루었습니다. (아직 읽지 않았다면 1부를 읽어보세요.) 그 후 우회 방법을 찾는 리서치를 계속했고, 마침내 제로 클릭 RCE 체인을 달성하기 위해 연결할 수 있는 동반 취약점을 발견했습니다. 이러한 취약점은 수정되었지만 공격자들은 원격으로 악용할 수 있는 유사한 공격표면과 취약점을 계속 찾고 있습니다.
현재로서는 Akamai가 조사한 Outlook의 공격표면이 여전히 존재하며, 새로운 취약점이 발견되어 악용될 수 있습니다. Microsoft는 PidLidReminderFileParameter 속성이 포함된 메일을 삭제하도록 Exchange에 패치를 적용했지만, 이 방어 조치를 우회할 가능성을 배제할 수는 없습니다.
부록
이 사이트에는 Microsoft의 Media Foundation에서 사용할 수 있는 모든 미디어 종류과 코덱이 나열되어 있습니다.
테스트 결과, 실제로는 다음 코덱만 WAV를 통해 사용할 수 있는 것으로 나타났습니다.
1 — PCM
2 — ADPCM
6 — A-LAW
7 — U-LAW
11 — IMA ADPCM
31 — GSM 6.10
55 — MPEG-1 Audio Layer III(MP3)
00000003_0000_0010_8000_00aa00389b71 — IEEE Float
00000008-0000-0010-8000-00aa00389b71 — DTS Audio
00000092-0000-0010-8000-00aa00389b71 — Dolby Digital
00000164-0000-0010-8000-00aa00389b71 — Microsoft WMA