CVE-2024-24583,CVE-2024-24584
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.
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
libigl - https://libigl.github.io/
4.3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CWE-125 - Out-of-bounds Read
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 ElementData
but 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.
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);
}
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);
}
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
Discovered by Philippe Laulheret of Cisco Talos.