Time for a tiny bit of client-side operational assurance. We're going to package this chapter's code so that it "just works" in the field.
We want our
rcli tool to run on nearly any Linux system, instantly.
Just by copying a single executable file.
No setup, no pulling in libraries with an OS-specific package manager.
We want it to work for every user, every time.
Is this section relevant to red teams?
Potentially. Operational assurance can be thought of as an abstract game played by defenders and attackers. Likewise, native executables can serve different agendas:
Defense: Performant, reliable tools for a range of hosts you manage (e.g. "assets").
Offense: Portable programs amenable to obfuscation1. For hosts owned by your victims (e.g. "targets").
Static binaries are a tried-and-true way to bundle a program and its dependencies. They provide an alternative to dynamic linking, a default which locates and loads dependencies at runtime. Let's briefly visualize the mechanical distinction.
With dynamic linking, multiple processes use the same copy of a shared dependency (e.g. shared library). The shared functions are "resolved" (address to call into is determined) at runtime. Typically that means on the first call a process makes to a shared function, but it can also be when the process is first "loaded" (e.g. the program is started)2. It's common, but not required, for shared libraries to make systems calls - requests to the OS kernel to interact with hardware. Reading or writing a file requires a system call.
Static linking takes all of the executable code needed for a program, including any services typically provided by system libraries, and bakes everything into one larger file. The result is a stand-alone application. No need to resolve anything at runtime. System calls are made directly as necessary.
Are we making an operational tradeoff?
Yes. For defenders, static linking complicates patching. Typically, an OS's package manager keeps system libraries up to date. And individual programs can link against a single, recent copy of the relevant library.
Static linking means each individual program needs to be replaced to keep its dependencies up to date. We lose the ability to manage centralized copies of certain components.
If multiple processes rely on the same dependency, then a statically linked process may also mean duplicated code and thus higher RAM usage.
But static linking is great for portability and isn't readily supported by many programming languages. So let's see how it's done in Rust.
First, we'll verify that
rcli is dynamically linked by default.
crypto_tool/rcli directory, run:
cargo build --release ldd ../target/release/rcli
ldd is a Linux command for printing shared library dependencies - those the OS distribution typically manages.
So that second command will output something like:
linux-vdso.so.1 (0x00007ffc0196f000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f09369b9000) /lib64/ld-linux-x86-64.so.2 (0x00007f0936c8e000) libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f0936996000) libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f093697b000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f0936975000)
Each line represents a shared object (
.so file) that the
rcli tool expects to be present somewhere on the filesystem in order to function.
The second item (line starting with
libc.so.6) is the C standard library.
Recall from this chapter's intro that our
rcli front-end code links against parts of
libc (e.g. for dynamic memory allocation).
Although our RC4 library does not (it's a
To avoid being reliant on the presence of these libraries, we can compile a static binary that will use
musl (a tiny
libc alternative3) instead:
rustup target add x86_64-unknown-linux-musl cargo build --release --target x86_64-unknown-linux-musl
The first command adds a new compilation target4, which is generally specified by a "target triple" in the form
The second command builds
rclias before, but this time for the target triple
Now let's try
ldd again, this time on the
The output should be:
Our second build of the
rcli executable will "just work" on any x86_64 Linux system!
All you need to do is copy over the binary.
If we want to distribute this executable, we should reduce its size by removing debug information (including symbols that allow matching to source code, something a CLI end-user won't need to do).
We can "strip" the binary of this information by adding the following
release profile setting to the workspace's configuration file,
[profile.release] strip = true
The setting applies to any target built with the flag
--release (which enables optimizations).
We could've also used
strip5, a standalone Linux utility, but we leveraged
cargo to more cleanly integrate into the build pipeline.
An Alternative to
muslis a popular way to build small-ish static binaries,
muslhas its quirks. Particularly with regard to performance.
To statically link your platform's standard C runtime ("CRT") instead6:
RUSTFLAGS='-C target-feature=+crt-static' cargo build --release --target x86_64-unknown-linux-gnu
Warning: unlike the
muslroute, the resulting binary might still be dynamic linked against something like
vdso7. You can use
We've demonstrated building a completely free-standing tool. Our binary will run natively, on nearly any client of given OS and ISA8.
That concludes our tour of software assurance! In the next chapter, we'll dig into Rust proper.
hellscape. meme (Archived 2021).
ld.so. Linux manual (Accessed 2022). On Linux, this behavior can be activated by setting the
LD_BIND_NOW environment variable to a non-empty string. The advantage of doing shared function resolution at load-time is slightly more predictable runtime performance. May also be useful for process debugging.
musl libc. Rich Felker and contributors (Accessed 2022).
Platform Support. The Rust Team (Accessed 2022).
strip. Linux manual (Accessed 2022).
RFC 1721. The Rust RFC Book (Accessed 2022).
vdso. Linux manual (Accessed 2022).
Instruction Set Architecture, e.g. x86_64.