[5] libwebrtc derives send encodings from an unclamped remote layer count
No remote peer required: the page writes its own WebRTC offer, and picks the count.
High. Encoding vector의 길이는 페이지가 setRemoteDescription에 넘기는 SDP에 의해 결정되며, consumer 진입점에서의 유일한 bounds 검증은 shipping build에서 컴파일되어 빠지는 debug 전용 assertion뿐이었습니다. Out-of-bounds write라는 것이 이 commit이 제시하는 프레이밍이며, WebKit은 producer 단에서 값을 잘라내고 해당 assertion을 release check로 승격시키는 방식으로 이를 해결했습니다.
Simulcast는 하나의 WebRTC sender가 동일한 video track을 여러 해상도로 동시에 발행할 수 있게 하는 기능이며, 이때 encoding 개수는 SDP — 즉 setRemoteDescription에 전달되는 텍스트 형태의 session description — 에서 비롯됩니다. Answering 측에서는 pc/sdp_offer_answer.cc의 offer/answer 처리 로직이 전달받은 description에 선언된 receive layer들을 local sender의 send-encoding 목록으로 변환하는데, layer 하나당 encoding 하나씩 대응시킵니다. 이후 하위 단계인 video-coding 설정 계층에서는 이 목록을 VideoCodec 구조체로 복사하며, 이 구조체의 per-stream 배열인 SimulcastStream simulcastStream[kMaxSimulcastStreams]는 고정된 3개 원소짜리 inline 배열입니다. 즉 encoding 개수는 3을 넘지 않는다는 전제가 깔려 있습니다.
관전 포인트: 어떤 페이지든 별도의 remote peer 없이도 setRemoteDescription을 통해 스스로에게 조작된 SDP offer를 전달할 수 있으며, 이를 통해 local WebContent process가 구성하는 send encoding 개수를 임의로 결정할 수 있습니다. 결과적으로 공격자가 선택한 개수가 video encoder 설정 경로의 3-slot 고정 inline storage로 그대로 흘러들어가게 됩니다.
Patch Details
GetSendEncodingsFromRemoteDescription()에 trim 로직이 추가되어, 도출되는 encoding vector가 simulcast.receive_layers()의 원소 수만큼 무한정 늘어나는 대신 kMaxSimulcastStreams로 상한이 걸리게 되었습니다. 별도로 modules/video_coding/video_codec_initializer.cc에서는 VideoCodecInitializer::SetupCodec()의 진입점 bound가 RTC_DCHECK_LE에서 RTC_CHECK_LE로 승격되었습니다.
Remote에서 전달된 개수 값이 고정 용량의 inline storage로 그대로 흘러들어갔고, 유일한 진입점 bound는 debug 전용 assertion으로만 표현되어 있던 패턴입니다.
Background
SDP와 simulcast 속성. Session description은 media stream과 그 파라미터를 a= 속성 라인 형태로 선언합니다. a=rid:<id> recv는 peer가 수신할 의향이 있는 restriction identifier를 선언하고, a=simulcast:recv는 이 rid들 중 어떤 것들이 simulcast layer를 구성하는지 나열합니다. 이때 ;는 서로 다른 layer를 구분하고, ,는 하나의 layer 안에서의 대안을 구분합니다.
SDP에서 sender parameter까지. pc/sdp_offer_answer.cc는 negotiate된 description을 RTP sender 및 receiver parameter로 변환합니다. addTrack 이후에는 remote description이 선언한 receive layer들이 local sender의 send encoding — 즉 std::vector<RtpEncodingParameters> — 이 됩니다.
Encoding에서 encoder 설정까지. 이 encoding들은 VideoEncoderConfig::number_of_streams와 simulcast_layers를 설정하며, 이 값들이 다시 VideoCodecInitializer::SetupCodec()에 전달되는 std::vector<VideoStream> streams의 크기를 결정합니다. 이 함수는 function-local VideoCodec을 생성하는데, 여기서 SimulcastStream simulcastStream[kMaxSimulcastStreams]는 api/video/video_codec_constants.h에 따라 3개로 고정된 inline 배열입니다.
RTC_DCHECK와 RTC_CHECK의 차이. RTC_DCHECK_LE는 debug 전용이라 shipping build에서는 컴파일되어 빠지지만, RTC_CHECK_LE는 조건 없이 항상 동작하며 실패 시 process를 중단시킵니다. 하나를 다른 하나로 승격시키면 조용한 위반이 항상 동일하게 재현되는 crash로 바뀌게 되며, 이 점이 producer 측 수정과 별개로 이 승격 자체가 의미를 갖는 이유입니다.
Analysis
근본 원인은 producer 단에서의 clamp 누락입니다. GetSendEncodingsFromRemoteDescription()은 local sender의 encoding 목록을 오직 전달받은 simulcast description의 개수만으로 도출했습니다. simulcast.receive_layers()의 항목 하나당 RtpEncodingParameters 하나씩 누적하는 구조였고, 이 누적 루프에는 종료 조건이 없었습니다. 설정 계층이 전제하는 invariant는 send encoding 개수가 kMaxSimulcastStreams를 넘지 않는다는 것입니다. Commit message에 따르면 이 상한은 addTransceiver 경로에서는 이미 적용되어 있었지만("we trim them ... like done for addTransceiver"), remote SDP에서 도출되는 경로에는 적용되어 있지 않았습니다. 다만 제공된 context에는 addTransceiver encoding 경로 자체가 포함되어 있지 않으므로, 이 비교의 절반은 commit message의 내용을 그대로 인용한 것입니다.
Before: After:
page SDP (N recv layers) page SDP (N recv layers)
+-> GetSendEncodingsFromRemote +-> GetSendEncodingsFromRemote
N encodings, unclamped min(N, kMaxSimulcastStreams)
+-> VideoEncoderConfig +-> VideoEncoderConfig
number_of_streams = N number_of_streams <= 3
+-> SetupCodec() +-> SetupCodec()
RTC_DCHECK_LE (debug) RTC_CHECK_LE
+-> VideoCodec +-> VideoCodec
simulcastStream[3] simulcastStream[3]
Trigger를 구성하는 방법 자체는 단순합니다. 페이지가 setRemoteDescription에 전달하는 SDP offer에 a=rid:<id> recv 라인 N개와, ;로 구분된 N개의 simulcast layer를 나열하는 a=simulcast:recv 라인을 담으면 N개의 receive layer가 만들어집니다. 참고로 regression test는 ; 구분자만 사용하는데, 이 경우 각 rid가 하나의 layer 내 대안이 아니라 독립된 layer로 취급됩니다. addTrack 이후 이 N개 layer는 sender의 send encoding이 되고, N이라는 값이 그대로 number_of_streams까지 전달됩니다.
Write가 실제로 어디에서 발생하는지는 정확히 짚어둘 필요가 있습니다. 제공된 trunk 소스에서 SetupCodec()의 per-stream 루프는 const size_t num_streams = std::min(streams.size(), static_cast<size_t>(kMaxSimulcastStreams));로 bound가 걸려 있고, numberOfSimulcastStreams 할당 역시 동일한 std::min을 사용합니다. 즉 제공된 context 안에서의 이 루프는 simulcastStream[2]를 넘어서는 인덱싱을 하지 않으며, 여기서 overflow가 발생하는 write 지점은 확인되지 않습니다. 따라서 out-of-bounds write에 도달하려면 제공된 context에 포함되어 있지 않은 별도의 write 지점이 필요합니다. Commit message가 원래 반영 지점으로 명시한 branch(Originally-landed-as: 305413.820@safari-7624-branch)의 SetupCodec() 변형이거나, untrimmed encoding vector를 사용하는 또 다른 consumer일 가능성이 있습니다.
제공된 context를 통해 확인되는 사실은 세 가지입니다. Producer가 길이 제한이 없는 vector를 만들어낸다는 점, SetupCodec()에 걸린 유일한 진입점 bound가 shipping build에는 없는 debug 전용 assertion이었다는 점, 그리고 WebKit이 이 상황을 producer 단 trim과 consumer 단 assertion의 release check 승격을 모두 필요로 할 만큼 심각하게 판단했다는 점입니다. GetSendEncodingsFromRemoteDescription()의 trim이 근본 원인에 대한 수정이며, assertion 승격은 남아있을 수 있는 잔여 케이스를 조용한 write 대신 항상 동일하게 재현되는 abort로 바꾸는 역할을 합니다.
이 취약점은 WebContent process 내부에서 session-description 텍스트와 고정 레이아웃의 encoder 설정 상태 사이의 경계를 약화시킵니다. 이는 스스로 offer를 구성하여 setRemoteDescription + addTrack + createAnswer를 호출할 수 있는 모든 페이지가 도달 가능한 surface입니다.
Audit directions
- Remote에서 협상된 개수 값이 고정 배열 크기를 결정하는 패턴. SDP 속성 라인의 개수를 세어 도출되는 값은 모두 공격자가 제어할 수 있으며, 그 하위에 위치한 inline
[kMax...]배열이 최종 목표가 됩니다.pc/sdp_offer_answer.cc안의 다른GetSendEncodings*/Get*FromRemoteDescriptionhelper들을 살펴보고, 각 누적 루프에 종료 bound가 있는지 확인할 필요가 있습니다. 검토 시 눈여겨볼 패턴은receive_layers()형태의 컬렉션을 대상으로push_back을 수행하면서 상한 표현식이 없는for루프입니다. - Bound를 debug 전용 assertion 하나로만 표현하는 패턴.
RTC_DCHECK_LE와 그에 상응하는ASSERT계열은 invariant를 문서화할 뿐 shipping build에서 이를 강제하지 않으므로, index bound가 오직 이 형태로만 표현되어 있다면 production 환경에서는 사실상 무제한인 셈입니다. Video-coding 설정 계층에서 개수를kMax상수와 비교하는RTC_DCHECK형태를 검색해볼 필요가 있습니다. - Producer/consumer 간 clamp 비대칭. 공유된 consumer로 들어가는 여러 경로 중 하나만 clamp를 적용하고 나머지는 그렇지 않다면, consumer가 전제하는 가정은 절반의 caller에 대해 조용히 깨져 있는 셈입니다.
RtpEncodingParametersvector를 생성하는 모든 producer를 나열하고, 이미 clamp가 적용된addTransceiver경로와 비교해볼 필요가 있습니다.
Translating the WebXRSystem security newsletter section into Korean per the specified style guide.