Conf42 Golang 2024 - Online

How we almost secured our project by writing more tests

Abstract

The whole test coverage feature introduced a better visibility in Go projects. Let’s discover how our experiment used these tests for generating Seccomp profiles, exploring both successes and limitations encountered, explaining the approach we had along the way.

Alessio on GitHub

Summary

  • Alessio Veggie is a software engineer, full time cat food opener for his furry friend. Today he will talk about security by testing and how he tries to improve the security of his go projects just by writing more tests.
  • Code coverage is a metric that can help developers understand how much of their source code is tested and how much is not. With Golang it was first time introduced in version 1.2, April 2013, and this support was specifically for unit tests. Now with new grid support for the integration test, the coverage percentage of projects sensibly increased.
  • The Europe probe overwrite the return address of the probed function with an address of a trampoline. U probes can be attached to a specific offset in the binary function. The use of LibfDo makes the project even more distributable.

Transcript

This transcript was autogenerated. To make changes, submit a PR.
Hi, hello everyone and welcome to my talk. Today I'm gonna talk about security by testing and how I try to improve the security of my go projects just by writing more tests. Before starting the talk, I will give you a little introduction about myself. So, my name is Alessio Veggie. I'm a software engineer, full time cat food opener for my furry friend. And jokes apart, I'm passionate about reading and taking long walks on the social media. You can find me on GitHub, LinkedIn, Twitter and so on with this unique account and this amazing avatar. So let's start the talk first concept to know is about code coverage code coverage is a metric that can help developers understanding how much of their source code is tested and how much is not. It is mostly used when writing a unit test, but not only related to them, and it's expressed as a percentage. Let's go a bit more in depth with code coverage. With Golang it was first time introduced in version 1.2, April 2013, if I remember well, and this support was specifically for unit tests. Here's the link to the announcement. And the story continued after almost ten years with version 1.20 with a new grid support for the integration test. So this means that combining the unit test with the integration test coverage, the coverage percentage of our projects sensibly increased thanks to the last feature. During this time. Also, the go community introduced some nice tools, like for example the go to cover that gives you the ability to see the to render the source code with its coverage in an HTML page and so on. So that's, that's what happened. Another important concept is about Secomp profile. First of all, Seccomp is a security feature that resides on the Linux kernel and the second profile is basically list of syscalls that you can use to put them into a rule and attach this rule to a program by defining if these syscalls could be executed or not by the program itself. So it works as a firewall for system calls. Additionally, it is extensively used in the kubernetes ecosystem, especially when you enable the second default feature flag. So this means that all the workloads that you are going to run will be, will be attached to this default profile. That is a default second profile that have inside the most dangerous syscalls that you should never use in your run. Let's see now, the main idea that I had during the tasks, so the main thought that I had was to create to generate a second profile as an artifact at the end of a test pipeline. Why specifically the test pipeline because in the test pipeline, we are testing, as much as we can, our test our code through the test that we write. And depending on the percentage that cover our, our source code, we can rely on them for a security profile. So if we are testing the 70% of our code, the syscalls that we are going to extract on the test pipeline will be reliable for the 70%. So that was the main idea. Additionally, this second profile could be used in different ways. The, the most common that came into my mind was to use it through an init container. When we deploy our application, the init container basically injected the second by downloading it from the artifact registry and store it into the Kubernetes node in order to allow the application container to load it and run with it. And there's the example. So the init container basically download it and store it under Barlib Kubelet second, and then the container, in this case the NginX container for example, uses the security context second profile of type localhost by referring to the second profile itself, but with the localhost profile. So now that we have the main concept in our mind, let me explain you what was the step that I followed and how I tried to achieve this goal. So in this image, you can see basically a call graph of an example program. So having that in mind and having all the tests that are rendered can basically understand what is missing and what is not. So we have a general overview about the potential syscall that you are missing if you are not testing the binary. So in order to extract the syscalls from the test with the integration test, that was the easiest part. What we have to do is to build a binary, provide some script to check for the expected result, general scripts for the integration test. An example that I really appreciate is the test script suite. And basically we can run our binary by using strace or pair for whatever you want. So in this case we can extract, we can collect all the executed syscalls from the binary by testing all the possible branch that the executioner has. So this was the first part, but it was not enough in my opinion, because we were missing the unit test that are such a big part of the test pipeline. So how I tried to extract also the syscalls from the unit test, first of all, it's quite simple to say, but it was a bit more complicated. First of all, the gotest command, we have to think that the gotest command compile and run the test binary all at once. So when we run go tests, we are basically compiling the test and then automatically run with the same command. At first glance we couldn't do strace of go test because we were going to hook, we were going to trace calls that were not related to our specific test, but syscalls that were included in the go test command that were out of school. So what we could do at first I thought that we could for example compile only the test binary without running it using go test, but running it separately. So if we basically type gotest c, we could compile the test binary and run it by ourselves. The problem in this case is that even doing strace about of the test binary, we could include some noise that are not related to our specific function that we are testing, because for example we can include some function that load some test data from, from the, from the environment. So as in as an example we could open a file, read the file content, and this syscalls will be collected by strace. But we don't need that, we should try to find a way to avoid this kind of noise. My personal idea, I don't know if it's the good one, but it seems to work, at least in some cases. But the main idea was to create using a BPF to define a trace point that basically start tracing the syscalls. When a probe that is attached to the function that we want to trace inform us that the execution of the function started and we can stop the trace point when the uert probe inform us that the execution of the function is finished. So we have a range of time that we can rely on in order to collect the syscholes that derives from our function. An additional information is that Arpun was based on go BPF at the beginning, but then I moved to Libpfgo and I will explain later why I took this decision. So let's see the juice part. So in order to run harpoon what we have to do, we should basically build the test binary first as we were trying to do before. So just typing gotest c followed by the package pop that host our function. So in this case we are going to build the test binary. So as a result we are going to have a binary that will execute the test where our function is located. Consequently, we have to extract from this binary the symbol name of the function that we want to trace. So in the example I typed the myfunction. So myfunction is the function that we want to trace and we are using objdump since followed by the name of the binary test by graphing for the function name. In this case we are going to find the symbol of the function name. So as you can see in the example below, so we are doing objdump to test binary of mine. It was an independent project from this talk and we are graphing for the function interface exist. So interface exists is a function that I've created in my personal project, a go function that we can find on the binary on the test binary with the following name. So GitHub.com allegra 91 forwardctlash packages iptables dot interface exists. So once we have the symbol, the symbol name of the function, we can use arpun in order to extract the syscalls from the binary. So by typing this command arpun fm to specifying the the symbol name of the function followed by the command for the test binary. So as I explained before, what is going to happen with arpun is that arpun will take the elf binary. So binary test and we'll attach a u probe and a u ref probe to the main dot. Do something in this case, but whatever is the name of the symbol or of the function and will, after it attached these probes to the function, will basically run the binary and the trace point will be informed by the u probe and the u rh probe about the starting and the xd and the end of the function itself. So in this limited range of time we are going to extract the syscalls. And here it is an example. So by typing arfun fn followed by the function, the symbol name of the function, followed by the command of the test binary. So iptables test. So here's the list of the functions that are related only to the function in interface exists. Let's see now the worst part of this of this project, everything was looking nice, but at some point I realized that not all the things were working properly. Main thing that was not working properly was that the u rat probe sometime was not informing the program that the function was returned. What was the problem? Basically the Europe probe overwrite the return address of the probed function with an address of a trampoline. This trampoline is basically pointing to our EPF program. So once the EBBF program is executed and after it sends the instruction pointer should restore tool to point to the next instruction. But this thing doesn't happen all the time, since the stack in the go binaries dynamically changes due to the garbage collector, for example. So this kind of behavior could cause the program corruption. So I had to find a way to solve this issue, and luckily for me I found this workaround on Internet. So the workaround consists in starting from the fact that the U probes could be attached to a specific offset. So we don't need to attach the U probes only on the symbol function names, but we can do the same to a specific offset in the function, in the binary function. So in order to simulate the URET probe, what you what we could do is of adding for each rEt instruction that is inside the function, a uprobe that basically simulates the functioning of a UART probe. So instead of attaching one single u ret probe in the function, we can add the one u probe for each ret instruction within the function that we are tracing, and the rest of the function is the same. So benefits of moving to lib bpf go so at the beginning when I introduced arkun, I mentioned the thing that was based on go bpf, that is a library from the iovisor organization and that is using bc under the hood. So now I decided to move to lib bpfgo, mainly because libpfgo gives you the ability of attaching the u probes to specific offset, and this thing was not supported by go bpf. So we can basically simulate the functioning of our UART probe by attaching the U probes to the RET instructions, thanks to Libbypfgo. And the use of LibfDo makes the project even more distributable, easily distributable because the LibPF Go is a library that is a wrapper of Lib BPF. So Lib BPF gives us the ability to write the program with Cory. Cory means compile once runs everywhere. We don't have to build the binary every time we run the application as we did before with the Go BPF that was based on BCC that needs GCC as a dependency to do this thing. So this time we can simply compile the binary the first time from the pipeline, for example, and then distribute the entire program through the repository. So the talk is almost finished. I just want to point you to some links that helped me a lot understanding this problem and links from which I learned a lot. I also want to thank some people that helped me doing this project. Thank you for your attention and I hope you enjoyed the talk.
...

Alessio Greggi

Software Engineer

Alessio Greggi's LinkedIn account Alessio Greggi's twitter account



Awesome tech events for

Priority access to all content

Video hallway track

Community chat

Exclusive promotions and giveaways