Native IoT Hardening in Your BSP with RunSafe Alkemist: Part 1

Published

Native IoT Hardening in Your BSP with RunSafe Alkemist: Part 1

DISLCAIMERS:

  1. I AM NOT A SECURITY EXPERT AND NOTHING IN THIS SERIES OF BLOG POSTS SHOULD BE (MIS)CONSTRUED AS SECURITY ADVICE. I AM NOT RESPONSIBLE FOR ANY VULNERABILITIES IN YOUR SYSTEM.
  2. THIS SERIES OF BLOG POSTS ARE ONLY FOR EDUCATIONAL PURPOSES AND ARE NOT MEANT TO AID ANYONE IN ANY ILLEGAL ACTIVITY. I AM NOT RESPONSIBLE FOR ANY ILLEGAL ACTIVITY THAT’S CONDUCTED AS A RESULT OF ANYONE USING THE INFORMATION IN THESE POSTS. IF YOU DO NOT AGREE, LEAVE THIS SITE IMMEDIATELY.

I was recently contacted by RunSafe Security, Inc. to try out their Alkemist software suite. Two things happened after reading through their blog post (https://runsafesecurity.com/blog/5-minute-memory-threat-immunization-for-yocto-build-environments/). First, it has whet my appetite to learn more about the different types of exploits that are generally employed to try to penetrate a system (it’s amazing that buffer overflow attacks are still the general root of most exploitation techniques). I will capture my findings and experiments in future posts. Second, and the focus of this blog post, is to understand how seamlessly Alkemist integrates into a board support package (BSP) and the level of customization it offers. The next blog post will actually deploy a BSP with the Alkemist customization and quantify its impact on running binaries on an embedded system.

The steps outlined on RunSafe’s website (https://alkemist.runsafesecurity.com/deploy/lfr/yocto) are very straightforward. They allow you to see how it integrates with a standard poky Yocto build running from the zeus branch. To satisfy my curiosity about how it works and what it does (“Talk is cheap, show me the code”), I dug deeper into the recipes of the meta-lfr layer, which is responsible for the hardening.

I was impressed by the simplicity, since the layer leverages some key features in Yocto. At a high level, the meta-lfr layer simply creates symbolic links to the Alkemist linker (called the “TrapLinker”) and the original cross-linker, such that when Bitbake calls the original linker, it is actually calling the TrapLinker. TrapLinker hardens the binary by adding the appropriate sections into the original executable to perform the function randomization and by modifying the entry point of the binary to point to the randomization library. Then, TrapLinker calls the original linker to complete the binary generation process.

Let’s take a look at the meta-lfr layer in some more detail. First, this is the directory structure of the layer itself:

meta-lfr$ tree -d .
.
├── classes
├── conf
├── recipes-devtools
│   ├── binutils
│   ├── gcc
│   ├── lfr
│   │   └── files
│   └── python
└── recipes-extended
        └── sysklogd

“classes” contains selfrandomize.bbclass, which contains a set of helper functions to ensure that TrapLinker is built before any of the actual binaries (since it’s needed for hardening) and to set up the appropriate compiler and linker flags. selfrandomize.bbclass also contains the following snippet, which is probably the most important:

python () {
		if d.getVar('LFR_REPLACE_LD', False) == "1":
           d.appendVar('OVERRIDES', ':lfr-replace-ld')
        else:
           d.appendVar('OVERRIDES', ':lfr-keep-ld')
}

The above snippet checks to see if the “LFR_REPLACE_LD” is set in the global Bitbake data store. If it is set, then Bitbake appends “:lfr-replace-ld” to the “OVERRIDES” variable. If not, then “:lfr-keep-ld” is appended. Recall that if a particular string is added to OVERRIDES, then any recipe step that contains the particular string will be executed instead of the standard step. This means that if a particular recipe has a step do_xxx_lfr-replace-ld, then that “overloaded” step will be executed instead of the standard “do_xxx” step.

The meta-lfr layer, and specifically the “recipes-devtools/binutils/binutils-cross_%.bbappend” recipe, provides an excellent example of this overloading process. Recall that creating a recipe with the “bbappend” extension will add the steps in the “bbappend” recipe to the original recipe :

do_install_append_lfr-keep-ld () {
         cd ${D}${bindir}
         # We create links from ld.original to ld,
         # so TrapLinker can call the original linker via the suffix
         for l in ${TARGET_PREFIX}ld*; do
            ln -s $l $l.original
         done
 }

do_install_append_lfr-replace-ld () {
         cd ${D}${bindir}
         # Rename all aliases of the various linker binaries,
         # appending a ".original" suffix for LFR
         for l in ${TARGET_PREFIX}ld*; do
            mv $l $l.original
         done
 }

DEPENDS_append_lfr-replace-ld = " virtual/lfr-traplinker-native lfr-traplinker-cross-${TARGET_ARCH} "

Both “do_install_append_lfr-keep-ld” and “do_install_append-lfr-replace-ld” are added as install candidates to the binutils. Now, when “LFR_REPLACE_LD” is set, then the overloaded “do_install_append_lfr-replace-ld” step will be called when installing binutils.

The redirection of the compile-time cross-linker is done in the recipes-devtools/lfr/lfr-traplinker-cross_1.0.bb recipes-devtools/gcc/gcc-cross_%.bbappend recipes. Since the gcc-cross_%.bbappend simply references the add-symlinks.inc recipe in the same directory, we look at add-symlinks.inc instead:

# gcc-cross installs binutils symlinks into `/usr/libexec/gcc`
# Mainly `ld` -> `/usr/bin/${TARGET_PREFIX}ld
# We need to add `.original` versions of these symlinks for LFR
do_install_append () {
	cd ${D}${libexecdir}/gcc/${TARGET_SYS}/${BINV}
    for x in ld ld.bfd ld.gold lld; do
    	y=${TARGET_PREFIX}$x
        if [ -h $x ]; then
        	ln -sf ${BINRELPATH}/$y.original $x.original
        fi
        if [ -h $y ]; then
        	ln -sf ${BINRELPATH}/$y.original $y.original
        fi
	done
}

This recipe adds an install step to simply create symlinks from the original linker binaries to the name of the linker appended with “.original”, for the TrapLinker to invoke. And, if we take a look at recipes-devtools/lfr/lfr-traplinker-cross_1.0.bb, we see the following do_install step:

do_install_append_lfr-replace-ld () {
     # If we renamed `ld`&friends to `ld.original`, then create our own
     # symlinks from `ld` to `traplinker`
     cd ${D}${bindir}
     for x in ld ld.bfd ld.gold; do
         lnr lfr/traplinker ${TARGET_PREFIX}$x
     done
}

The above snippet simply creates a symlink to the TrapLinker from the original linker. This way, when gcc calls the original linker during the build process, it’s actually calling TrapLinker. Then, TrapLinker will then call the original gcc linker to complete the binary generation.

The TrapLinker binaries are retrieved using the meta-lfr layer configuration file and the lfr-package.inc recipe. lfr-package.inc is included in the lfr-package_1.0.bb recipe, which is responsible for installing the requisite LFR libraries and python scripts in the final rootfs, and in the lfr-traplinker-package-native-1.0.bb recipe, which is responsible for installing the binaries into the Yocto build system.

Finally, a very straightforward mechanism is provided to disable use of the TrapLinker for certain binaries. An example is provided in “recipes-extended/sysklogd/sysklogd_%.bbappend”:

LFR_DISABLE = "1"
TARGET_CC_ARCH_remove = "-fPIC"

My only complaint would be that it would become tedious to disable Alkemist for the majority of binaries that exist on an embedded system and only enable it for the single application that is the focus of that embedded system. This criticism is based on my own experience. A test team that does its job properly will NOT release a BSP to production unless they have thoroughly tested anything that has changed from the previous release.  Given that Alkemist would fundamentally change the majority of the binaries included in the BSP, the test team would most likely not accept this change. Instead, it would be preferable if there was an option to only enable Alkemist for a single (or a few) binaries.

(NB: After reaching out to RunSafe about this perceived deficiency, they said that support for hardening individual binaries while keeping the rest of the BSP untouched will be added in a future release.)

To summarize, RunSafe’s Alkemist system is a straightforward way to harden binaries that are deployed to an IoT device. Their seamless integration into the Yocto build system makes it easy to use in any BSP. However, their propensity to require users to enable it across the entire BSP and selectively disable it for certain binaries may lead to substantial testing overhead. Instead, it would be preferable to selectively enable Alkemist for certain binaries while keeping it disabled for the rest of the BSP, which will be added in a future release.

In the next post, we’ll include Alkemist in an actual BSP, deploy it on a target IoT device, and evaluate the performance on a running system.

Leave a comment

Your email address will not be published.