]> git.ipfire.org Git - thirdparty/strongswan.git/commitdiff
simaka-message: Prevent buffer overflow when generating messages
authorTobias Brunner <tobias@strongswan.org>
Thu, 9 Jul 2026 10:03:58 +0000 (12:03 +0200)
committerTobias Brunner <tobias@strongswan.org>
Fri, 24 Jul 2026 06:47:38 +0000 (08:47 +0200)
A malicious (authenticated) server could send a long AT_NEXT_PSEUDONYM
or AT_NEXT_REAUTH_ID attribute (max. 1016 bytes) that the client then
attempts to return in an AT_IDENTITY attribute that could overflow the
fixed-sized stack buffer `out_buf` by a few bytes (writing into the
`encr_buf` buffer).

This refactoring first calculates the length required to encode the
attributes (with and without encryption) and accounts for the overhead
necessary for AT_ENCR_DATA, AT_PADDING, and AT_MAC (some limits are
enforced and error handling is added where appropriate).  Then correctly
sized buffers are allocated and the attributes are encoded pretty much
as before.  Except that the encoding of AT_RES is corrected (it actually
worked fine for up to 31 bytes, so in practice it wasn't a problem),
AT_PADDING is not added if data is already block-aligned, and it corrects
handling of AT_NOTIFICATION (they are never sent encrypted according to
the RFCs, but protected with a MAC).

Fixes: f8330d03953b ("Added a libsimaka library with shared message handling code for EAP-SIM/AKA")
src/libsimaka/simaka_message.c

index 9c5363e41fa9ddef9aee50689d4d1966e6749719..97f0954f7aa389e57a182f1c5c6245d2ed02ccad 100644 (file)
@@ -263,12 +263,21 @@ METHOD(simaka_message_t, add_attribute, void,
 }
 
 /**
- * Error handling for unencrypted attributes
+ * Determine if an attribute is only sent encrypted
  */
-static bool not_encrypted(simaka_attribute_t type)
+static bool is_encrypted_attribute(simaka_attribute_t type)
 {
-       DBG1(DBG_LIB, "received unencrypted %N", simaka_attribute_names, type);
-       return FALSE;
+       switch (type)
+       {
+               case AT_NONCE_S:
+               case AT_NEXT_PSEUDONYM:
+               case AT_NEXT_REAUTH_ID:
+               case AT_COUNTER:
+               case AT_COUNTER_TOO_SMALL:
+                       return TRUE;
+               default:
+                       return FALSE;
+       }
 }
 
 /**
@@ -320,15 +329,17 @@ static bool parse_attributes(private_simaka_message_t *this, chunk_t in)
                }
                hdr = (attr_hdr_t*)in.ptr;
 
+               if (is_encrypted_attribute(hdr->type) && !this->encrypted)
+               {
+                       DBG1(DBG_LIB, "received unencrypted %N", simaka_attribute_names,
+                                hdr->type);
+                       return FALSE;
+               }
+
                switch (hdr->type)
                {
                        /* attributes without data */
                        case AT_COUNTER_TOO_SMALL:
-                               if (!this->encrypted)
-                               {
-                                       return not_encrypted(hdr->type);
-                               }
-                               /* FALL */
                        case AT_ANY_ID_REQ:
                        case AT_PERMANENT_ID_REQ:
                        case AT_FULLAUTH_ID_REQ:
@@ -343,11 +354,6 @@ static bool parse_attributes(private_simaka_message_t *this, chunk_t in)
                        }
                        /* attributes with two bytes data */
                        case AT_COUNTER:
-                               if (!this->encrypted)
-                               {
-                                       return not_encrypted(hdr->type);
-                               }
-                               /* FALL */
                        case AT_CLIENT_ERROR_CODE:
                        case AT_SELECTED_VERSION:
                        case AT_NOTIFICATION:
@@ -363,11 +369,6 @@ static bool parse_attributes(private_simaka_message_t *this, chunk_t in)
                        /* attributes with an additional actual-length in bits or bytes */
                        case AT_NEXT_PSEUDONYM:
                        case AT_NEXT_REAUTH_ID:
-                               if (!this->encrypted)
-                               {
-                                       return not_encrypted(hdr->type);
-                               }
-                               /* FALL */
                        case AT_RES:
                        case AT_IDENTITY:
                        case AT_VERSION_LIST:
@@ -394,11 +395,6 @@ static bool parse_attributes(private_simaka_message_t *this, chunk_t in)
                        }
                        /* attributes with two reserved bytes, 16 bytes length */
                        case AT_NONCE_S:
-                               if (!this->encrypted)
-                               {
-                                       return not_encrypted(hdr->type);
-                               }
-                               /* FALL */
                        case AT_AUTN:
                        case AT_NONCE_MT:
                        case AT_IV:
@@ -464,15 +460,8 @@ static bool parse_attributes(private_simaka_message_t *this, chunk_t in)
                        case AT_PADDING:
                                break;
                        case AT_NOTIFICATION:
-                               if (this->p_bit)
-                               {       /* remember P bit for MAC verification */
-                                       this->p_bit = !!(data.ptr[0] & 0x40);
-                               }
-                               else if (!this->encrypted)
-                               {
-                                       DBG1(DBG_LIB, "found P-bit 0 notify in unencrypted message");
-                                       return FALSE;
-                               }
+                               /* remember P bit for MAC verification */
+                               this->p_bit &= !!(data.ptr[0] & 0x40);
                                /* FALL */
                        default:
                                add_attribute(this, hdr->type, data);
@@ -607,245 +596,365 @@ METHOD(simaka_message_t, verify, bool,
        return TRUE;
 }
 
-METHOD(simaka_message_t, generate, bool,
-       private_simaka_message_t *this, chunk_t sigdata, chunk_t *gen)
+/**
+ * Encode the given attribute into buf. If buf is NULL, the required size is
+ * returned.
+ */
+static bool encode_attribute(simaka_attribute_t type, chunk_t data,
+                                                        chunk_t *buf, size_t *len)
 {
-       /* buffers large enough for messages we generate */
-       char out_buf[1024], encr_buf[512];
-       enumerator_t *enumerator;
-       chunk_t out, encr, data, *target, mac = chunk_empty;
-       simaka_attribute_t type;
-       attr_hdr_t *hdr;
-       uint16_t len;
-       signer_t *signer;
+       attr_hdr_t *hdr = NULL;
 
-       call_hook(this, FALSE, TRUE);
-
-       out = chunk_create(out_buf, sizeof(out_buf));
-       encr = chunk_create(encr_buf, sizeof(encr_buf));
-
-       /* copy header */
-       memcpy(out.ptr, this->hdr, sizeof(hdr_t));
-       out = chunk_skip(out, sizeof(hdr_t));
+       if (buf)
+       {
+               hdr = (attr_hdr_t*)buf->ptr;
+               hdr->type = type;
+       }
 
-       /* encode attributes */
-       enumerator = create_attribute_enumerator(this);
-       while (enumerator->enumerate(enumerator, &type, &data))
+       switch (type)
        {
-               /* encrypt this attribute? */
-               switch (type)
+               /* attributes without data */
+               case AT_COUNTER_TOO_SMALL:
+               case AT_ANY_ID_REQ:
+               case AT_PERMANENT_ID_REQ:
+               case AT_FULLAUTH_ID_REQ:
                {
-                       case AT_NONCE_S:
-                       case AT_NEXT_PSEUDONYM:
-                       case AT_NEXT_REAUTH_ID:
-                       case AT_COUNTER:
-                       case AT_COUNTER_TOO_SMALL:
-                               target = &encr;
-                               break;
-                       case AT_NOTIFICATION:
-                               /* P bit not set, encrypt */
-                               if (!(data.ptr[0] & 0x40))
-                               {
-                                       target = &encr;
-                                       break;
-                               }
-                               /* FALL */
-                       default:
-                               target = &out;
+                       if (data.len)
+                       {
+                               return invalid_length(type);
+                       }
+                       if (!buf)
+                       {
+                               *len = 4;
                                break;
+                       }
+                       hdr->length = 1;
+                       memset(buf->ptr + 2, 0, 2);
+                       *buf = chunk_skip(*buf, 4);
+                       break;
                }
-
-               hdr = (attr_hdr_t*)target->ptr;
-               hdr->type = type;
-
-               /* encode type specific */
-               switch (type)
+               /* attributes with two bytes data */
+               case AT_COUNTER:
+               case AT_CLIENT_ERROR_CODE:
+               case AT_SELECTED_VERSION:
+               case AT_NOTIFICATION:
                {
-                       /* attributes without data */
-                       case AT_COUNTER_TOO_SMALL:
-                       case AT_ANY_ID_REQ:
-                       case AT_PERMANENT_ID_REQ:
-                       case AT_FULLAUTH_ID_REQ:
+                       if (data.len != 2)
                        {
-                               hdr->length = 1;
-                               memset(target->ptr + 2, 0, 2);
-                               *target = chunk_skip(*target, 4);
-                               break;
+                               return invalid_length(type);
                        }
-                       /* attributes with two bytes data */
-                       case AT_COUNTER:
-                       case AT_CLIENT_ERROR_CODE:
-                       case AT_SELECTED_VERSION:
-                       case AT_NOTIFICATION:
+                       if (!buf)
                        {
-                               hdr->length = 1;
-                               memcpy(target->ptr + 2, data.ptr, 2);
-                               *target = chunk_skip(*target, 4);
+                               *len = 4;
                                break;
                        }
-                       /* attributes with an additional actual-length in bits or bytes */
-                       case AT_NEXT_PSEUDONYM:
-                       case AT_NEXT_REAUTH_ID:
-                       case AT_IDENTITY:
-                       case AT_VERSION_LIST:
-                       case AT_RES:
-                       {
-                               uint16_t len, padding;
+                       hdr->length = 1;
+                       memcpy(buf->ptr + 2, data.ptr, 2);
+                       *buf = chunk_skip(*buf, 4);
+                       break;
+               }
+               /* attributes with an additional actual-length in bits or bytes */
+               case AT_NEXT_PSEUDONYM:
+               case AT_NEXT_REAUTH_ID:
+               case AT_IDENTITY:
+               case AT_VERSION_LIST:
+               case AT_RES:
+               {
+                       uint16_t actual_len, padding;
 
-                               len = htons(data.len);
-                               if (type == AT_RES)
-                               {       /* AT_RES uses length encoding in bits */
-                                       len *= 8;
-                               }
-                               memcpy(target->ptr + 2, &len, sizeof(len));
-                               memcpy(target->ptr + 4, data.ptr, data.len);
-                               hdr->length = data.len / 4 + 1;
-                               padding = (4 - (data.len % 4)) % 4;
-                               if (padding)
-                               {
-                                       hdr->length++;
-                                       memset(target->ptr + 4 + data.len, 0, padding);
-                               }
-                               *target = chunk_skip(*target, hdr->length * 4);
+                       if (data.len > UINT8_MAX * 4 - 4)
+                       {
+                               return invalid_length(type);
+                       }
+                       if (!buf)
+                       {
+                               *len = round_up(4 + data.len, 4);
                                break;
                        }
-                       /* attributes with two reserved bytes, 16 bytes length */
-                       case AT_NONCE_S:
-                       case AT_NONCE_MT:
-                       case AT_AUTN:
+                       actual_len = data.len;
+                       if (type == AT_RES)
+                       {       /* AT_RES uses length encoding in bits */
+                               actual_len *= 8;
+                       }
+                       htoun16(buf->ptr + 2, actual_len);
+                       memcpy(buf->ptr + 4, data.ptr, data.len);
+                       hdr->length = data.len / 4 + 1;
+                       padding = pad_len(data.len, 4);
+                       if (padding)
+                       {
+                               hdr->length++;
+                               memset(buf->ptr + 4 + data.len, 0, padding);
+                       }
+                       *buf = chunk_skip(*buf, hdr->length * 4);
+                       break;
+               }
+               /* attributes with two reserved bytes, 16 bytes length */
+               case AT_NONCE_S:
+               case AT_NONCE_MT:
+               case AT_AUTN:
+               {
+                       if (data.len != 16)
                        {
-                               hdr->length = 5;
-                               memset(target->ptr + 2, 0, 2);
-                               memcpy(target->ptr + 4, data.ptr, data.len);
-                               *target = chunk_skip(*target, 20);
+                               return invalid_length(type);
+                       }
+                       if (!buf)
+                       {
+                               *len = 20;
                                break;
                        }
-                       /* attributes with two reserved bytes, variable length */
-                       case AT_RAND:
+                       hdr->length = 5;
+                       memset(buf->ptr + 2, 0, 2);
+                       memcpy(buf->ptr + 4, data.ptr, data.len);
+                       *buf = chunk_skip(*buf, 20);
+                       break;
+               }
+               /* attributes with two reserved bytes, variable length */
+               case AT_RAND:
+               {
+                       if (data.len % 4 || data.len > UINT8_MAX * 4 - 4)
                        {
-                               hdr->length = 1 + data.len / 4;
-                               memset(target->ptr + 2, 0, 2);
-                               memcpy(target->ptr + 4, data.ptr, data.len);
-                               *target = chunk_skip(*target, data.len + 4);
+                               return invalid_length(type);
+                       }
+                       if (!buf)
+                       {
+                               *len = 4 + data.len;
                                break;
                        }
-                       /* attributes with no reserved bytes, 14 bytes length */
-                       case AT_AUTS:
+                       hdr->length = 1 + data.len / 4;
+                       memset(buf->ptr + 2, 0, 2);
+                       memcpy(buf->ptr + 4, data.ptr, data.len);
+                       *buf = chunk_skip(*buf, data.len + 4);
+                       break;
+               }
+               /* attributes with no reserved bytes, 14 bytes length */
+               case AT_AUTS:
+               {
+                       if (data.len != 14)
                        {
-                               hdr->length = 4;
-                               memcpy(target->ptr + 2, data.ptr, data.len);
-                               *target = chunk_skip(*target, 16);
+                               return invalid_length(type);
+                       }
+                       if (!buf)
+                       {
+                               *len = 16;
                                break;
                        }
-                       default:
+                       hdr->length = 4;
+                       memcpy(buf->ptr + 2, data.ptr, data.len);
+                       *buf = chunk_skip(*buf, 16);
+                       break;
+               }
+               default:
+               {
+                       if (!buf)
                        {
-                               DBG1(DBG_LIB, "no rule to encode %N, skipped",
-                                        simaka_attribute_names, type);
+                               *len = 0;
                                break;
                        }
+                       DBG1(DBG_LIB, "no rule to encode %N, skipped",
+                                simaka_attribute_names, type);
+                       break;
                }
        }
-       enumerator->destroy(enumerator);
+       return TRUE;
+}
 
-       /* encrypt attributes, if any */
-       if (encr.len < sizeof(encr_buf))
+METHOD(simaka_message_t, generate, bool,
+       private_simaka_message_t *this, chunk_t sigdata, chunk_t *gen)
+{
+       enumerator_t *enumerator;
+       chunk_t out_buf, out, encr_buf, encr, data, *target, mac;
+       simaka_attribute_t type;
+       attr_hdr_t *hdr;
+       size_t out_len = sizeof(hdr_t), encr_len = 0, attr_len;
+       size_t iv_len = 0, encr_pad = 0, mac_bs = 0;
+       bool notify_no_p_bit = FALSE, add_mac = FALSE, success = FALSE;
+       crypter_t *crypter = NULL;
+       signer_t *signer = NULL;
+       rng_t *rng = NULL;
+
+       call_hook(this, FALSE, TRUE);
+
+       enumerator = create_attribute_enumerator(this);
+       while (enumerator->enumerate(enumerator, &type, &data))
        {
-               chunk_t iv;
-               size_t bs, padding;
-               crypter_t *crypter;
-               rng_t *rng;
+               if (!encode_attribute(type, data, NULL, &attr_len))
+               {
+                       enumerator->destroy(enumerator);
+                       return FALSE;
+               }
+               if (!attr_len)
+               {
+                       continue;
+               }
+               if (is_encrypted_attribute(type))
+               {
+                       encr_len += attr_len;
+               }
+               else
+               {
+                       if (type == AT_NOTIFICATION)
+                       {
+                               notify_no_p_bit |= !(data.ptr[0] & 0x40);
+                       }
+                       out_len += attr_len;
+               }
+       }
+       enumerator->destroy(enumerator);
 
+       if (encr_len)
+       {
                crypter = this->crypto->get_crypter(this->crypto);
-               bs = crypter->get_block_size(crypter);
-               iv.len = crypter->get_iv_size(crypter);
+               if (!crypter)
+               {
+                       DBG1(DBG_LIB, "%N message requires encryption, but no crypter found",
+                                simaka_subtype_names, this->hdr->subtype);
+                       return FALSE;
+               }
+               rng = this->crypto->get_rng(this->crypto);
+               if (!rng)
+               {
+                       DBG1(DBG_LIB, "%N message requires RNG, but none found",
+                                simaka_subtype_names, this->hdr->subtype);
+                       return FALSE;
+               }
+               iv_len = crypter->get_iv_size(crypter);
+               encr_pad = pad_len(encr_len, crypter->get_block_size(crypter));
+               /* AT_PADDING */
+               encr_len += encr_pad;
+               if (encr_len > UINT8_MAX * 4 - 4)
+               {
+                       DBG1(DBG_LIB, "encrypted attributes exceed maximum length");
+                       return FALSE;
+               }
+               /* AT_IV */
+               out_len += 4 + iv_len;
+               /* AT_ENCR_DATA */
+               out_len += 4 + encr_len;
+       }
+
+       switch (this->hdr->subtype)
+       {
+               case SIM_CHALLENGE:
+               case AKA_CHALLENGE:
+               case SIM_REAUTHENTICATION:
+                 /* AKA_REAUTHENTICATION: */
+                       add_mac = TRUE;
+                       break;
+               case SIM_NOTIFICATION:
+                 /* AKA_NOTIFICATION: */
+                       add_mac = notify_no_p_bit;
+                       break;
+               default:
+                       break;
+       }
+
+       if (add_mac)
+       {
+               signer = this->crypto->get_signer(this->crypto);
+               if (!signer)
+               {
+                       DBG1(DBG_LIB, "%N message requires AT_MAC, but no signer found",
+                                simaka_subtype_names, this->hdr->subtype);
+                       return FALSE;
+               }
+               mac_bs = signer->get_block_size(signer);
+               /* AT_MAC */
+               out_len += 4 + mac_bs;
+       }
+
+       if (out_len > UINT16_MAX)
+       {
+               DBG1(DBG_LIB, "EAP-SIM/AKA message exceeds maximum length");
+               return FALSE;
+       }
+
+       out = out_buf = chunk_alloc(out_len);
+       encr = encr_buf = chunk_alloc(encr_len);
+
+       /* copy header, set length */
+       memcpy(out.ptr, this->hdr, sizeof(hdr_t));
+       htoun16(out.ptr + 2, out.len);
+
+       out = chunk_skip(out, sizeof(hdr_t));
+
+       /* encode attributes */
+       enumerator = create_attribute_enumerator(this);
+       while (enumerator->enumerate(enumerator, &type, &data))
+       {
+               target = is_encrypted_attribute(type) ? &encr : &out;
+               encode_attribute(type, data, target, NULL);
+       }
+       enumerator->destroy(enumerator);
+
+       if (encr_len)
+       {
+               chunk_t iv;
 
                /* add AT_PADDING attribute */
-               padding = bs - ((sizeof(encr_buf) - encr.len) % bs);
-               if (padding)
+               if (encr_pad)
                {
                        hdr = (attr_hdr_t*)encr.ptr;
                        hdr->type = AT_PADDING;
-                       hdr->length = padding / 4;
-                       memset(encr.ptr + 2, 0, padding - 2);
-                       encr = chunk_skip(encr, padding);
+                       hdr->length = encr_pad / 4;
+                       memset(encr.ptr + 2, 0, encr_pad - 2);
                }
-               encr = chunk_create(encr_buf, sizeof(encr_buf) - encr.len);
 
                /* add IV attribute */
                hdr = (attr_hdr_t*)out.ptr;
                hdr->type = AT_IV;
-               hdr->length = iv.len / 4 + 1;
+               hdr->length = iv_len / 4 + 1;
                memset(out.ptr + 2, 0, 2);
                out = chunk_skip(out, 4);
 
-               rng = this->crypto->get_rng(this->crypto);
-               if (!rng->get_bytes(rng, iv.len, out.ptr))
+               iv = chunk_create(out.ptr, iv_len);
+               if (!rng->get_bytes(rng, iv.len, iv.ptr))
                {
-                       return FALSE;
+                       goto failed;
                }
-
-               iv = chunk_clonea(chunk_create(out.ptr, iv.len));
                out = chunk_skip(out, iv.len);
 
                /* inline encryption */
-               if (!crypter->encrypt(crypter, encr, iv, NULL))
+               if (!crypter->encrypt(crypter, encr_buf, iv, NULL))
                {
-                       return FALSE;
+                       goto failed;
                }
 
                /* add ENCR_DATA attribute */
                hdr = (attr_hdr_t*)out.ptr;
                hdr->type = AT_ENCR_DATA;
-               hdr->length = encr.len / 4 + 1;
+               hdr->length = encr_buf.len / 4 + 1;
                memset(out.ptr + 2, 0, 2);
-               memcpy(out.ptr + 4, encr.ptr, encr.len);
-               out = chunk_skip(out, encr.len + 4);
+               memcpy(out.ptr + 4, encr_buf.ptr, encr_buf.len);
+               out = chunk_skip(out, encr_buf.len + 4);
        }
 
-       /* include MAC ? */
-       signer = this->crypto->get_signer(this->crypto);
-       switch (this->hdr->subtype)
+       if (add_mac)
        {
-               case SIM_CHALLENGE:
-               case AKA_CHALLENGE:
-               case SIM_REAUTHENTICATION:
-                 /* AKA_REAUTHENTICATION: */
-               /* TODO: Notifications without P bit */
-               {
-                       size_t bs;
-
-                       bs = signer->get_block_size(signer);
-                       hdr = (attr_hdr_t*)out.ptr;
-                       hdr->type = AT_MAC;
-                       hdr->length = bs / 4 + 1;
-                       memset(out.ptr + 2, 0, 2 + bs);
-                       mac = chunk_create(out.ptr + 4, bs);
-                       out = chunk_skip(out, bs + 4);
-                       break;
-               }
-               default:
-                       break;
-       }
-
-       /* calculate message length */
-       out = chunk_create(out_buf, sizeof(out_buf) - out.len);
-       len = htons(out.len);
-       memcpy(out.ptr + 2, &len, sizeof(len));
+               hdr = (attr_hdr_t*)out.ptr;
+               hdr->type = AT_MAC;
+               hdr->length = mac_bs / 4 + 1;
+               memset(out.ptr + 2, 0, 2 + mac_bs);
+               mac = chunk_create(out.ptr + 4, mac_bs);
 
-       /* generate MAC */
-       if (mac.len)
-       {
-               data = chunk_cata("cc", out, sigdata);
-               if (!signer->get_signature(signer, data, mac.ptr))
+               if (!signer->get_signature(signer, out_buf, NULL) ||
+                       !signer->get_signature(signer, sigdata, mac.ptr))
                {
-                       return FALSE;
+                       goto failed;
                }
        }
 
        call_hook(this, FALSE, FALSE);
 
-       *gen = chunk_clone(out);
-       return TRUE;
+       *gen = out_buf;
+       success = TRUE;
+
+failed:
+       chunk_clear(&encr_buf);
+       if (!success)
+       {
+               chunk_free(&out_buf);
+       }
+       return success;
 }
 
 METHOD(simaka_message_t, destroy, void,