| 描述 | # CFE-SB-USERDATALEN-UNDERFLOW
## Summary
`CFE_SB_GetUserDataLength()` returns `TotalMsgSize - HdrSize` without checking that the CCSDS-declared total message size is at least as large as the cFE Software Bus header size. A CCSDS command with a secondary header present and a declared size of 7 bytes can produce an 8-byte command-header size, causing unsigned underflow to a huge `size_t` user-data length.
This is a safe source-chain/model package only. It does not transmit packets, inject Software Bus traffic, or provide exploitation logic.
## Target
- Product: NASA cFS/cFE
- Version checked: cFS `v7.0.1`, cFE `v7.0.1`
- Component: cFE Software Bus (`CFE_SB`)
- Primary file: `src/cFS/cfe/modules/sb/fsw/src/cfe_sb_util.c`
- Public-history anchor: cFS/cFE `CFE_SB` has public CVE history through `CVE-2026-5475`.
## Source Chain
- `src/cFS/cfe/modules/msg/fsw/src/cfe_msg_ccsdspri.c:379` computes total packet size from the CCSDS length field plus `CFE_MSG_SIZE_OFFSET`.
- `src/cFS/cfe/modules/sb/fsw/src/cfe_sb_msg_id_util.c:92` returns `sizeof(CFE_MSG_CommandHeader_t)` for command packets with secondary headers.
- `src/cFS/cfe/modules/sb/fsw/src/cfe_sb_util.c:77` reads `TotalMsgSize` from the packet header.
- `src/cFS/cfe/modules/sb/fsw/src/cfe_sb_util.c:78` derives `HdrSize`.
- `src/cFS/cfe/modules/sb/fsw/src/cfe_sb_util.c:80` returns `TotalMsgSize - HdrSize` without a lower-bound check.
- `src/cFS/cfe/modules/msg/fsw/src/cfe_msg_sechdr_checksum.c:44` computes checksum over the CCSDS-declared length, so a short declared command can be checksum-valid under the same length rule.
- `src/cFS/cfe/modules/msg/fsw/src/cfe_msg_integrity.c:73` default verification accepts messages unless mission-specific verification is added.
## Safe PoC Result
The PoC constructs a structured CCSDS command model:
- Primary Header: command type, secondary-header-present bit, APID `1`, standalone sequence, length field `0`.
- Secondary Header: function-code byte chosen so the cFS XOR checksum over the declared 7 bytes is valid; checksum byte exists in the local buffer but is not included by the declared length.
- Declared total size: `7`.
- Modeled command header size: `8`.
- 64-bit result: `7 - 8 -> 18446744073709551615`.
- 32-bit result: `7 - 8 -> 4294967295`.
Evidence JSON: `analysis/reports/rce_lpe/cfe_sb_userdatalen_underflow_20260615.json`.
## Impact
If an application or downstream handler calls `CFE_SB_GetUserDataLength()` on such a malformed short command and uses the returned value for copy/parse bounds, it can turn a small malformed message into a huge trusted length. This is most directly a memory-safety primitive or denial-of-service risk in consumers of the public API.
## Caveats
- This package does not show unauthenticated RCE, LPE, or a complete exploit chain.
- Default cFE command dispatch may reject many specific command handlers through exact expected-length checks.
- The finding is strongest as a public API safety issue in a CVE-history component, not as a proven standalone mission compromise.
## Suggested Fix
- In `CFE_SB_GetUserDataLength()`, return `0` or an error-compatible sentinel if `CFE_MSG_GetSize()` fails, `HdrSize == 0`, or `TotalMsgSize < HdrSize`.
- In `CFE_SB_SetUserDataLength()`, check `DataLength > CFE_MISSION_SB_MAX_SB_MSG_SIZE - HdrSize` before addition.
- Consider enforcing a minimum message size in default verification for command/telemetry packets with secondary headers.
### poc.py
```
#!/usr/bin/env python3
"""
Safe model for CAND-CFE-SB-USERDATALEN-UNDERFLOW.
This script does not transmit packets, inject Software Bus traffic, or exploit a
target. It verifies the original cFS/cFE source anchors and models the arithmetic
in CFE_SB_GetUserDataLength() for a CCSDS command whose declared packet size is
smaller than the cFE command header size.
"""
from __future__ import annotations
import argparse
import json
from datetime import datetime, timezone
from pathlib import Path
CANDIDATE = "CAND-CFE-SB-USERDATALEN-UNDERFLOW"
CFE_MSG_SIZE_OFFSET = 7
CFE_MSG_COMMAND_HEADER_SIZE = 8
CFE_MISSION_SB_MAX_SB_MSG_SIZE = 32768
def find_line(source: str, needle: str) -> int:
for index, line in enumerate(source.splitlines(), start=1):
if needle in line:
return index
return -1
def find_line_after(source: str, anchor: str, needle: str) -> int:
lines = source.splitlines()
start_index = 0
for index, line in enumerate(lines):
if anchor in line:
start_index = index
break
for index, line in enumerate(lines[start_index:], start=start_index + 1):
if needle in line:
return index
return -1
def cfs_checksum(packet: bytes, declared_size: int) -> int:
checksum = 0xFF
for value in packet[:declared_size]:
checksum ^= value
return checksum & 0xFF
def build_checksum_valid_short_command() -> dict[str, object]:
packet = bytearray(CFE_MSG_COMMAND_HEADER_SIZE)
# CCSDS primary header, big endian:
# version=0, type=command, secondary-header-present=1, APID=1
packet[0] = 0x18
packet[1] = 0x01
# Sequence flags=standalone, sequence count=0
packet[2] = 0xC0
packet[3] = 0x00
# CCSDS length field = 0 => CFE_MSG_GetSize() reports 7 bytes.
packet[4] = 0x00
packet[5] = 0x00
declared_size = CFE_MSG_SIZE_OFFSET
# Command secondary header starts at byte 6. Because checksum computation is
# driven by the declared size, a 7-byte command includes the function-code
# byte but not the checksum byte. Choose the function code so the cFS XOR
# checksum over the declared bytes is already valid.
fcn_code = 0xFF
for value in packet[:6]:
fcn_code ^= value
packet[6] = fcn_code & 0xFF
packet[7] = 0x00
return {
"packet_hex": packet.hex(),
"declared_size": declared_size,
"actual_buffer_size": len(packet),
"function_code_for_valid_declared_checksum": packet[6],
"checksum_over_declared_size": cfs_checksum(packet, declared_size),
"checksum_valid_under_cfe_rule": cfs_checksum(packet, declared_size) == 0,
}
def model_get_user_data_length(total_msg_size: int, header_size: int, bits: int) -> int:
mask = (1 << bits) - 1
return (total_msg_size - header_size) & mask
def model_set_user_data_length(data_length: int, header_size: int, bits: int) -> dict[str, object]:
mask = (1 << bits) - 1
total = (header_size + data_length) & mask
return {
"data_length": data_length,
"wrapped_total_msg_size": total,
"passes_max_msg_size_check": total <= CFE_MISSION_SB_MAX_SB_MSG_SIZE,
"would_call_CFE_MSG_SetSize": CFE_MSG_SIZE_OFFSET <= total <= (0xFFFF + CFE_MSG_SIZE_OFFSET),
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--workspace-root", default=".", help="Workspace root containing src/cFS")
parser.add_argument(
"--output",
default="analysis/reports/rce_lpe/cfe_sb_userdatalen_underflow_20260615.json",
)
parser.add_argument(
"--log",
default="submission_packages/CAND-CFE-SB-USERDATALEN-UNDERFLOW/crash_evidence.log",
)
parser.add_argument("--expect-crash", action="store_true", help="Return success only if the marker condition is present")
args = parser.parse_args()
root = Path(args.workspace_root)
sb_util_path = root / "src" / "cFS" / "cfe" / "modules" / "sb" / "fsw" / "src" / "cfe_sb_util.c"
sb_hdr_path = root / "src" / "cFS" / "cfe" / "modules" / "sb" / "fsw" / "src" / "cfe_sb_msg_id_util.c"
msg_size_path = root / "src" / "cFS" / "cfe" / "modules" / "msg" / "fsw" / "src" / "cfe_msg_ccsdspri.c"
checksum_path = root / "src" / "cFS" / "cfe" / "modules" / "msg" / "fsw" / "src" / "cfe_msg_sechdr_checksum.c"
integrity_path = root / "src" / "cFS" / "cfe" / "modules" / "msg" / "fsw" / "src" / "cfe_msg_integrity.c"
sb_util = sb_util_path.read_text(encoding="utf-8", errors="replace")
sb_hdr = sb_hdr_path.read_text(encoding="utf-8", errors="replace")
msg_size = msg_size_path.read_text(encoding="utf-8", errors="replace")
checksum_source = checksum_path.read_text(encoding="utf-8", errors="replace")
integrity_source = integrity_path.read_text(encoding="utf-8", errors="replace")
anchors = {
"get_user_data_length_function": find_line(sb_util, "size_t CFE_SB_GetUserDataLength"),
"get_size_call": find_line_after(
sb_util,
"size_t CFE_SB_GetUserDataLength",
"CFE_MSG_GetSize(MsgPtr, &TotalMsgSize);",
),
"header_size_call": find_line_after(
sb_util,
"size_t CFE_SB_GetUserDataLength",
"HdrSize = CFE_SB_MsgHdrSize(MsgPtr);",
),
"unchecked_subtract": find_line_after(
sb_util,
"size_t CFE_SB_GetUserDataLength",
"return TotalMsgSize - HdrSize;",
),
"set_user_data_lengt
``` |
|---|