[하루한줄] CVE-2024-55030 : NASA fPrime의 Queue Overflow로 인한 DoS 취약점
URL
https://visionspace.com/remote-code-execution-and-critical-vulnerabilities-in-nasa-fprime-v3-4-3/
Target
- fPrime ≤ v3.4.3
Explain
Background
fPrime은 우주 비행 어플리케이션과 임베디드 시스템을 위한 가벼운 개발 및 배포 소프트웨어 프레임워크입니다. 시스템은 통신 포트로 연결된 컴포넌트들로 구성되어 있으며, 이벤트와 명령을 통해 컴포넌트 간 통신이 이루어집니다. 컴포넌트는 다른 요소들과 접촉(또는 통신)하기 위한 입/출력 포트를 제공하여 이벤트를 알리거나 명령을 주고받으며 통신합니다.
Command Dispatcher Service 역시 fprime 프레임워크 내의 한 컴포넌트로서, 외부로부터 명령을 받아 내부 컴포넌트로 중계하는 역할을 수행합니다. 이때, Command Dispatcher Service로 전달되는 외부로부터의 명령은 아래 그림과 같이 지상 시스템(Ground System)으로부터 전달됩니다. 지상 시스템은 비행 소프트웨어가 탑재된 시스템(e.g., 위성)과 외부에서 통신하고 제어하는 역할을 담당합니다.
Root Cause
Command Dispatcher Service에서 외부로부터 유입되는 다량의 명령을 처리할 때, 내부 명령 큐(m_queue
또는 m_seqCmdBuffQueue
)가 오버플로우되는 문제가 발생합니다. 이 서비스는 외부 소스(e.g., 지상 시스템)로부터 들어오는 명령을 수신하여 해당 명령을 처리할 목적지 컴포넌트로 중계하는 역할을 수행합니다.
이 문제는 pipeline/standard.py
의 send_command
함수를 무한 루프[1]로 수정하여 명령을 반복적으로 빠르게 전송하는 방식으로 재현되었습니다. 이처럼 비정상적으로 많은 명령이 짧은 시간 내에 유입되면, Command Dispatcher Service는 이를 처리하기 위한 내부 큐에 부담을 받게 됩니다.
def send_command(self, command, args):
"""Sends commands to the encoder and history.
:param command: command id from dictionary to get command template
:param args: arguments to process
"""
if isinstance(command, str):
command_template = self.dictionaries.command_name[command]
else:
command_template = self.dictionaries.command_id[command]
cmd_data = fprime_gds.common.data_types.cmd_data.CmdData(
tuple(args), command_template
)
cmd_data.time = fprime.common.models.serialize.time_type.TimeType()
cmd_data.time.set_datetime(datetime.datetime.now(), 2)
while 1: // [1] Infinite Loop
self.coders.send_command(cmd_data)
루트 커즈는 수신된 명령을 내부 큐에 넣는 로직에서 발생합니다. CommandDispatcherComponentBase::seqCmdBuff_handlerBase()
함수 내에서 수신된 명령 메시지(msg
)를 큐에 전송하기 위해 다음과 같은 코드가 사용됩니다:
Os::Queue::QueueBlocking _block = Os::Queue::QUEUE_NONBLOCKING; // [2]
Os::Queue::QueueStatus qStatus = this->m_queue.send(msg, 0, _block); // 메시지를 큐에 전송 시도
FW_ASSERT(
qStatus == Os::Queue::QUEUE_OK,
static_cast<FwAssertArgType>(qStatus)
);
이 코드에서 큐 전송 방식은 Os::Queue::QUEUE_NONBLOCKING
으로 설정[2]되어 있어, 큐가 가득 찼을 때 블록하지 않고 즉시 상태를 반환합니다. 내부적으로 this->m_queue.send()
는 bareSendNonBlock()
를 호출하고[3],
Queue::QueueStatus Queue::send(const Fw::SerializeBufferBase &buffer, NATIVE_INT_TYPE priority, QueueBlocking block) {
const U8* msgBuff = buffer.getBuffAddr();
NATIVE_INT_TYPE buffLength = buffer.getBuffLength();
return this->send(msgBuff, buffLength, priority, block);
}
Queue::QueueStatus Queue::send(const U8* buffer, NATIVE_INT_TYPE size, NATIVE_INT_TYPE priority, QueueBlocking block) {
//Check if the handle is null or check the underlying queue is null
/* ... */
//Send to the queue
if (QUEUE_NONBLOCKING == block) { //[3]
return bareSendNonBlock(handle, buffer, size, priority);
}
return bareSendBlock(handle, buffer, size, priority);
}
이는 다시 실제 버퍼 큐 구현의 push()
메서드를 호출하여 데이터를 큐에 삽입하려고 시도합니다[4].
Queue::QueueStatus bareSendNonBlock(BareQueueHandle& handle, const U8* buffer, NATIVE_INT_TYPE size, NATIVE_INT_TYPE priority) {
/ * ... */
Queue::QueueStatus status = Queue::QUEUE_OK;
bool success = queue.push(buffer, size, priority); // [4]
if(!success) { // push가 실패했다면 (e.g., 큐가 가득 찼다면)
status = Queue::QUEUE_FULL; // 상태를 QUEUE_FULL로 설정
}
return status; // 최종 상태 반환
}
만약 큐가 가득 찼다면, push()
는 false
를 반환하고, 이는 bareSendNonBlock()
함수를 거쳐 Os::Queue::QUEUE_FULL
(오류 코드 8) 상태로 변환되어 m_queue.send()
의 반환 값인 qStatus
에 저장됩니다[5].
문제는 바로 그다음 줄의 FW_ASSERT
구문에서 발생합니다. 이 어설션은 qStatus
가 Os::Queue::QUEUE_OK
와 같을 것이라고 단정합니다. 그러나 큐 오버플로우로 인해 qStatus
가 Os::Queue::QUEUE_FULL
(8)이 되면, 이 어설션 조건이 거짓이 되어 어설션 실패가 발생합니다.
Os::Queue::QueueBlocking _block = Os::Queue::QUEUE_NONBLOCKING;
Os::Queue::QueueStatus qStatus = this->m_queue.send(msg, 0, _block);
FW_ASSERT( // 전송 상태에 대한 어설션 체크
qStatus == Os::Queue::QUEUE_OK, // [5] 상태가 QUEUE_OK (성공)인지 확인
static_cast<FwAssertArgType>(qStatus) // 실패 시 상태 값 출력
);
어설션 자체는 디버깅 도구이지만, 기본 시스템 설정에서 어설션 실패 시 sigabort
신호를 보내며 프로세스를 강제 종료하도록 되어 있습니다. 따라서 과도한 명령 전송으로 인한 큐 오버플로우가 어설션 실패를 유발하고, 궁극적으로 비행 소프트웨어 프로세스가 비정상 종료되는 서비스 거부(DoS) 상태로 이어지는 것입니다.
정리하면, 큐가 오버플로우 되었을 때의 핸들링을 어설션이 아닌 차단하거나 드롭하거나 훅하는 등으로 적절히 설정해줘야하는데 기본 설정으로 어설션을 적용하여 큐가 꽉차면 프로그램이 꺼지는 취약점이었습니다.
Reference
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.