Talos Vulnerability Report

TALOS-2024-1928

libigl readMSH out-of-bounds read vulnerability

May 28, 2024
CVE Number

CVE-2024-24583,CVE-2024-24584

SUMMARY

Multiple out-of-bounds read vulnerabilities exist in the readMSH functionality of libigl v2.5.0. A specially crafted .msh file can lead to an out-of-bounds read. An attacker can provide a malicious file to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

libigl v2.5.0

PRODUCT URLS

libigl - https://libigl.github.io/

CVSSv3 SCORE

4.3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N

CWE

CWE-125 - Out-of-bounds Read

DETAILS

libigl is a C++ geometry processing library that is designed to be simple to integrate into projects using a header-only construction for the code base. This library is widely utilized in industries ranging from Triple-A game development to 3D printing, and it can be found in many applications that require the geometry processing of various file formats.

When loading a .msh file using the readMSH function, an assumption is made that the number of entries in the ElementData section is in sync with the entries in the Element section. However if too many elements are in the Element section that should have associated entries in the ElementDatabut that are missing, it is possible to trigger an an out-of-bound read. This can affect both Triangular and Tetrahedral elements returned in the TriF and TetF paraneters provided to the readMSH function.

As the libigl is a library that can be used in many different scenarios, we can assume that an out-of-bound read could be exploited in the context of a privilege-escalation where the library is being used by a higher privlege service to load a file and return the result to the user. As such, the data returned by the readMSH function could be leveraged to leak heap data. Leaked data could be used to bypass ASLR or access other sensitive information.

CVE-2024-24583 - MshLoader::ELEMENT_TRI

The out-of-bound-read can be seen in the code as follow:

IGL_INLINE  bool  igl::readMSH(
  const std::string &msh,
  Eigen::Matrix<double,Eigen::Dynamic,Eigen::Dynamic,EigenMatrixOptions> &X,
  Eigen::Matrix<int,Eigen::Dynamic,Eigen::Dynamic,EigenMatrixOptions> &Tri,
  Eigen::Matrix<int,Eigen::Dynamic,Eigen::Dynamic,EigenMatrixOptions> &Tet,
  Eigen::VectorXi &TriTag,
  Eigen::VectorXi &TetTag,
  std::vector<std::string>     &XFields,
  std::vector<Eigen::Matrix<double,Eigen::Dynamic,Eigen::Dynamic,EigenMatrixOptions>> &XF,
  std::vector<std::string>     &EFields,
  std::vector<Eigen::Matrix<double,Eigen::Dynamic,Eigen::Dynamic,EigenMatrixOptions>> &TriF,
  std::vector<Eigen::Matrix<double,Eigen::Dynamic,Eigen::Dynamic,EigenMatrixOptions>> &TetF)
{
    try 
    {
        igl::MshLoader _loader(msh);

           ...

        int n_tri_el=0;
        int n_tet_el=0;
        auto n_tri_el_=element_counts.find(igl::MshLoader::ELEMENT_TRI);
        auto n_tet_el_=element_counts.find(igl::MshLoader::ELEMENT_TET);
        if(n_tri_el_!=std::end(element_counts)) 
            n_tri_el=n_tri_el_->second;
        if(n_tet_el_!=std::end(element_counts)) 
            n_tet_el=n_tet_el_->second;

        Tri.resize(n_tri_el,3);
        Tet.resize(n_tet_el,4);
        TriTag.resize(n_tri_el);
        TetTag.resize(n_tet_el);
        size_t el_start = 0;
[0]     TriF.resize(_loader.get_element_fields().size());
[0b]    TetF.resize(_loader.get_element_fields().size());
        for(size_t i=0;i<_loader.get_element_fields().size();++i)
        {
[1]         TriF[i].resize(n_tri_el,_loader.get_element_fields_components()[i]);
[1b]        TetF[i].resize(n_tet_el,_loader.get_element_fields_components()[i]);
        }
        EFields = _loader.get_element_fields_names();
        int i_tri = 0;
        int i_tet = 0;

[2]     for(size_t i=0;i<_loader.get_elements_lengths().size();++i)
        {
[3]         if(_loader.get_elements_types()[i]==MshLoader::ELEMENT_TRI )
            {

                ...

                for(size_t j=0;j<_loader.get_element_fields().size();++j)
                    for(size_t k=0;k<_loader.get_element_fields_components()[j];++k)
[4]                     TriF[j](i_tri,k) = _loader.get_element_fields()[j][_loader.get_element_fields_components()[j]*i+k];

                ++i_tri;
[3b]        } else if(_loader.get_elements_types()[i]==MshLoader::ELEMENT_TET ) {

                ...

                for(size_t j=0;j<_loader.get_element_fields().size();++j)
                    for(size_t k=0;k<_loader.get_element_fields_components()[j];++k)
 [4b]                   TetF[j](i_tet,k) = _loader.get_element_fields()[j][_loader.get_element_fields_components()[j]*i+k];

                ++i_tet;
            } else {
                ...
            }

          ...
        }

        ...
    } catch(const std::exception& e) {
        std::cerr << e.what() << std::endl;
        return false;
    }
    return true;
}

Here we can see the TriF vector of matrix being resized as a vector containing as many matrices as there are element fields (aka ElementData) at [0] and then each matrix within it is resized to be a n_tri_el by k element where k is the number of components within a field [1]. Then the function interates over all the elements at [2] and when a given element at offset i is of type ELEMENT_TRI [3] the associated TriF entries will be populated [4]. However to do so, the function will retrive a given field j (for all element_fields) and get the associated components using the formula field[num_component*i + k] (with num_component = _loader.get_element_fields_components()[j] and field=_loader.get_element_fields()[j] for clarity). This assumes the field vector is an array of num_components by num_elements. However nothing in the creation of this vector enforces this assumption (see below [5] within igl::MshLoader::parse_element_field where the vector is resized with values provided by user input in the $ElementData section), and if the .msh file does not contain the right number of elements within a given $ElementData entry, this assumption will be false, leading to an out-of-bound read beyond the end of the field vector.

IGL_INLINE void igl::MshLoader::parse_element_field(std::ifstream& fin) {
    size_t num_string_tags;
    size_t num_real_tags;
    size_t num_int_tags;

    ...

    for (size_t i=0; i<num_int_tags; i++)
        fin >> int_tags[i];

    if (num_string_tags <= 0 || num_int_tags <= 2) {
        throw std::runtime_error("Invalid file format");
    }
    std::string fieldname = str_tags[0];
    int num_components = int_tags[1];
    int num_entries = int_tags[2];
[5] std::vector<Float> field(num_entries*num_components);

    ...

    m_element_fields_names.push_back(fieldname);
    m_element_fields.push_back(field);
    m_element_fields_components.push_back(num_components);
}

CVE-2024-24584 - MshLoader::ELEMENT_TET

The out-of-bound-read can be seen in the code as follow:

IGL_INLINE  bool  igl::readMSH(
  const std::string &msh,
  Eigen::Matrix<double,Eigen::Dynamic,Eigen::Dynamic,EigenMatrixOptions> &X,
  Eigen::Matrix<int,Eigen::Dynamic,Eigen::Dynamic,EigenMatrixOptions> &Tri,
  Eigen::Matrix<int,Eigen::Dynamic,Eigen::Dynamic,EigenMatrixOptions> &Tet,
  Eigen::VectorXi &TriTag,
  Eigen::VectorXi &TetTag,
  std::vector<std::string>     &XFields,
  std::vector<Eigen::Matrix<double,Eigen::Dynamic,Eigen::Dynamic,EigenMatrixOptions>> &XF,
  std::vector<std::string>     &EFields,
  std::vector<Eigen::Matrix<double,Eigen::Dynamic,Eigen::Dynamic,EigenMatrixOptions>> &TriF,
  std::vector<Eigen::Matrix<double,Eigen::Dynamic,Eigen::Dynamic,EigenMatrixOptions>> &TetF)
{
    try 
    {
        igl::MshLoader _loader(msh);

           ...

        int n_tri_el=0;
        int n_tet_el=0;
        auto n_tri_el_=element_counts.find(igl::MshLoader::ELEMENT_TRI);
        auto n_tet_el_=element_counts.find(igl::MshLoader::ELEMENT_TET);
        if(n_tri_el_!=std::end(element_counts)) 
            n_tri_el=n_tri_el_->second;
        if(n_tet_el_!=std::end(element_counts)) 
            n_tet_el=n_tet_el_->second;

        Tri.resize(n_tri_el,3);
        Tet.resize(n_tet_el,4);
        TriTag.resize(n_tri_el);
        TetTag.resize(n_tet_el);
        size_t el_start = 0;
[0]     TriF.resize(_loader.get_element_fields().size());
[0b]    TetF.resize(_loader.get_element_fields().size());
        for(size_t i=0;i<_loader.get_element_fields().size();++i)
        {
[1]         TriF[i].resize(n_tri_el,_loader.get_element_fields_components()[i]);
[1b]        TetF[i].resize(n_tet_el,_loader.get_element_fields_components()[i]);
        }
        EFields = _loader.get_element_fields_names();
        int i_tri = 0;
        int i_tet = 0;

[2]     for(size_t i=0;i<_loader.get_elements_lengths().size();++i)
        {
[3]         if(_loader.get_elements_types()[i]==MshLoader::ELEMENT_TRI )
            {

                ...

                for(size_t j=0;j<_loader.get_element_fields().size();++j)
                    for(size_t k=0;k<_loader.get_element_fields_components()[j];++k)
[4]                     TriF[j](i_tri,k) = _loader.get_element_fields()[j][_loader.get_element_fields_components()[j]*i+k];

                ++i_tri;
[3b]        } else if(_loader.get_elements_types()[i]==MshLoader::ELEMENT_TET ) {

                ...

                for(size_t j=0;j<_loader.get_element_fields().size();++j)
                    for(size_t k=0;k<_loader.get_element_fields_components()[j];++k)
 [4b]                   TetF[j](i_tet,k) = _loader.get_element_fields()[j][_loader.get_element_fields_components()[j]*i+k];

                ++i_tet;
            } else {
                ...
            }

          ...
        }

        ...
    } catch(const std::exception& e) {
        std::cerr << e.what() << std::endl;
        return false;
    }
    return true;
}

Here we can see the TetF vector of matrix being resized as a vector containing as many matrices as there are element fields (aka ElementData) at [0b] and then each matrix within it is resized to be a n_tri_el by k element where k is the number of components within a field [1b]. Then the function iterates over all the elements at [2] and when a given element at offset i is of type ELEMENT_TET [3b] the associated TetF entries will be populated [4b]. However to do so, the function will retrive a given field j (for all element_fields) and get the associated components using the formula field[num_component*i + k] (with num_component = _loader.get_element_fields_components()[j] and field=_loader.get_element_fields()[j] for clarity). This assumes the field vector is an array of num_components by num_elements. However nothing in the creation of this vector enforces this assumption (see below [5] within igl::MshLoader::parse_element_field where the vector is resized with values provided by user input in the $ElementData section), and if the .msh file does not contain the right number of elements within a given $ElementData entry, this assumption will be false, leading to an out-of-bound read beyond the end of the field vector.

IGL_INLINE void igl::MshLoader::parse_element_field(std::ifstream& fin) {
    size_t num_string_tags;
    size_t num_real_tags;
    size_t num_int_tags;

    ...

    for (size_t i=0; i<num_int_tags; i++)
        fin >> int_tags[i];

    if (num_string_tags <= 0 || num_int_tags <= 2) {
        throw std::runtime_error("Invalid file format");
    }
    std::string fieldname = str_tags[0];
    int num_components = int_tags[1];
    int num_entries = int_tags[2];
[5] std::vector<Float> field(num_entries*num_components);

    ...

    m_element_fields_names.push_back(fieldname);
    m_element_fields.push_back(field);
    m_element_fields_components.push_back(num_components);
}
TIMELINE

2023-11-22 - Initial Vendor Contact
2023-11-28 - Initial Vendor Contact
2023-11-30 - Request for confirmation
2023-12-11 - Advisories sent
2024-02-07 - Four more advisories sent, after the initial two
2024-02-27 - Request for status update
2024-04-10 - Request for status update
2ß24-05-15 - Request for status update via Github issue, no reply
2024-05-28 - Public Release

Credit

Discovered by Philippe Laulheret of Cisco Talos.