The container is almost working, the only thing we have left is to add the binary execution inside our code.
The execve
syscall
When Linux is told to execute a software, whether it's a binary or a text script (if the first line is #!<interpreter>
), behind the curtains it calls the execve
that takes 3 arguments:
- The paths of the script / binary
- The arguments to pass to the executable
- The environment variables to set
The linux kernel will then replace the current process with the executable. This is why we needed to clone our main process before calling this syscall.
Check out the execve
syscall documentation here
Everything is already set up for our execution, so we only have to call the syscall inside the child
function of src/child.rs
:
use execve;
use CString;
The only tricky bit here is to tell Rust that we are using CString
as it has to have some compatibilities with raw C.
In case of success, the execve
function will never return as the process will
be replaced by the executable
We'll want also to check that the command is provided by the user and valid, so in src/cli.rs
let's modify the parse_args
functions to add this check:
Testing
The implementation was straightforward, let's test this out :
# ...
Aouch, that doesn't look good we got ENOENT
! When we check the execve documentation, we can read:
ENOENT The file pathname or a script or ELF interpreter does not exist.
Why that ? We copied the binary inside the mount point, every file is there to allow execution !
... Are they ?
Dynamically linked binaries
When a program is compiled, it requires a lot of dependencies, even for a small Hello World, such as the standard library of the programming language or the bindings to the underlying operating system.
It is possible to compile a binary statically, but it produces a big file, and 5 software compiled this way would embed in each of them 5 chunks of software that could have been shared.
This is why most binaries uses shared libraries. In Linux, you can see the libraries that are dynamically linked to an binary by executing the command
Here we can see that our /bin/bash
executable depends on 4 files located at the root of the system.
Compile a test binary statically
We will deal with this dynamic binaries problems later, for now we want to try our execve
, so let's try it with a static binary which only requires the binary file to work ! Let's create a little crate inside our repository, and compile it into a statically built binary blob.
The process may look a little bit esoteric, but we are simply telling the compiler to bundle the dynamic dependencies inside the binary instead of referencing them, and as it depend on the system, we compile against a specific target.
cargo new --bin testbin
cd testbin
RUSTFLAGS="-C target-feature=+crt-static" cargo build --target="x86_64-unknown-linux-gnu"
cp target/debug/testbin ../mountdir/
cd ..
To know more about the linking process in Rust, check out this doc
Now that we have a static binary, we can test out our container properly
[2022-08-23T07:58:45Z INFO crabcan] Args { debug: true, command: "/testbin", uid: 0, mount_dir: "./mountdir/" }
[2022-08-23T07:58:45Z DEBUG crabcan::container] Linux release: 5.13.0-52-generic
[2022-08-23T07:58:45Z DEBUG crabcan::container] Container sockets: (3, 4)
[2022-08-23T07:58:45Z DEBUG crabcan::resources] Restricting resources for hostname triangular-coffee-39
[2022-08-23T07:58:45Z DEBUG crabcan::hostname] Container hostname is now triangular-coffee-39
[2022-08-23T07:58:45Z DEBUG crabcan::mounts] Setting mount points ...
[2022-08-23T07:58:45Z DEBUG crabcan::mounts] Mounting temp directory /tmp/crabcan.yOXBrf4FO8v0
[2022-08-23T07:58:45Z DEBUG crabcan::mounts] Pivoting root
[2022-08-23T07:58:45Z DEBUG crabcan::mounts] Unmounting old root
[2022-08-23T07:58:45Z DEBUG crabcan::namespaces] Setting up user namespace with UID 0
[2022-08-23T07:58:45Z DEBUG crabcan::namespaces] Child UID/GID map done, sending signal to child to continue...
[2022-08-23T07:58:45Z DEBUG crabcan::container] Creation finished
[2022-08-23T07:58:45Z DEBUG crabcan::container] Container child PID: Some(Pid(2354182))
[2022-08-23T07:58:45Z DEBUG crabcan::container] Waiting for child (pid 2354182) to finish
[2022-08-23T07:58:45Z INFO crabcan::namespaces] User namespaces set up
[2022-08-23T07:58:45Z DEBUG crabcan::namespaces] Switching to uid 0 / gid 0...
[2022-08-23T07:58:45Z DEBUG crabcan::capabilities] Clearing unwanted capabilities ...
[2022-08-23T07:58:45Z DEBUG crabcan::syscalls] Refusing / Filtering unwanted syscalls
[2022-08-23T07:58:45Z DEBUG syscallz] seccomp: setting action=Errno(1) syscall=chmod comparators=[Comparator { arg: 1, op: MaskedEq, datum_a: 2048, datum_b: 2048 }]
...
[2022-08-23T07:58:45Z DEBUG syscallz] seccomp: loading policy
[2022-08-23T07:58:45Z INFO crabcan::child] Container set up successfully
[2022-08-23T07:58:45Z INFO crabcan::child] Starting container with command /testbin and args ["/testbin"]
Hello, world!
[2022-08-23T07:58:45Z DEBUG crabcan::container] Finished, cleaning & exit
[2022-08-23T07:58:45Z DEBUG crabcan::container] Cleaning container
[2022-08-23T07:58:45Z DEBUG crabcan::resources] Cleaning cgroups
[2022-08-23T07:58:45Z DEBUG crabcan::errors] Exit without any error, returning 0
Yipee ! We got our Hello, world!, our container is now able to launch executables
Patch for this step
The code for this step is available on github litchipi/crabcan branch “step15”.
The raw patch to apply on the previous step can be found here
Mounting additional paths
The last bit we need for a useful container is to allow some dynamic binaries to be used inside our container. We could simply copy everything each time, and it can be better in terms of security, but we'll use a lighter method for now, let's mount directories inside our container !
The idea is simple, when creating the container, we pass a list of directory of the hosts and where they should appear inside the container. Then we "mount" them and can freely access to them.
Let's add a parameter to the CLI for these additional paths to mount, in src/cli.rs
:
These arguments will be used to have a pair of from_dir -> to_dir
, and pass them to the container configuration. In src/container.rs
:
Let's add this new parameter in the function ContainerOpts::new
of the file src/config.rs
:
Then, they are passed as an additional argument of the setmountpoint
function and used to create the mountpoint and perform the mount
operation. In the file src/mounts.rs
:
Security Note: You do not want all mounts to be read/write, especially when sharing system directories. An additionalMS_RDONLY
flag should be added for them. (You could have-a
for read/write directories, and-r
for read only ones)
Finally, let's pass the new additional parameter to the setmountpoint
inside the setup_container_configurations
function, in src/child.rs
:
Testing
Now we can finally test our container with additional paths passed using multiple pairs of the form from:to
, like so:
syscall=chmod comparators=[Comparator ]
&
And voilà ! We just called a dynamically linked binary from inside our container !
Patch for this step
The code for this step is available on github litchipi/crabcan branch “step16”.
The raw patch to apply on the previous step can be found here
Closing thoughts
If you have to remember only one thing of this part, remember that YOU SHOULD NOT USE THIS TOY PROJECT FOR ANYTHING SERIOUS, use one of the many containerisation solutions instead
From there to Docker
It may seams like we arrived to a very minimalist state of a container, but once you can add files and execute binaries, you can do pretty much anything from there. This environment is already enough to execute a web service, or as a compilation sandbox, but it lacks severall things to get to the level comparable to Docker:
- Network isolation: Creating fake interfaces, bind them to the real networkhardware, but isolated enough to not have any bad surprises
- Port forwarding: Executing a web service on port 80 of the container andredirect the data to whatever local port you wish
- Good security: Security is hard because it has so many pitfalls.
- Container generation from file: A
Crabfile
with a weird syntax to builda container from text
And many more things that I am forgetting.
End of this tutorial
This was the last post of this tutorial that took way too long to write. Feel free to reach to me if you have any question or remark, or leave a comment using your Github Account.
I tried to keep it beginer-friendly in every aspects, as what I love to do when learning a language is build something real with it.
The code of this tutorial is a Rust rewrite from this tutorial, you should go and take a look at least to it as it's filled with comments, experiments, remarks and interesting thoughts that are not present here.
I will credit you for any contribution, error rectification, and any content you think could be added to the text of any of these posts.
Take care, happy coding <3