Hooking libc using Go shared libraries
Alastair O’Neill gave a talk at the BSides Manchester security
conference in August about userland rootkits that use the
LD_PRELOAD mechanism. Most of these rootkits are written in C. I knew
that as of version 1.5, Go supports a build mode for shared libraries
and having seen the talk, I wondered if I could write something similar
in Go and learn something about
LD_PRELOAD, cgo and building Go shared
libraries in the process.
Disclaimer: I’m not a security researcher and the code here is purely an experiment for educational purposes.
How LD_PRELOAD rootkits work
LD_PRELOAD rootkit works by implementing alternative versions
of functions provided by the libc library that many Unix binaries
link to dynamically. Using these ‘hooks’, the rootkit can evade
detection by modifying the behaviour of the functions and bypassing
authentication mechanisms, e.g. PAM, to provide an attacker with a
backdoor such as an SSH login using credentials configured by the
For example, the Azazel rootkit hooks into the fopen function and conceals evidence of network activity or files related to the rootkit. If there is nothing to hide, Azazel invokes the original libc function so that the application behaves as normal from the user’s perspective.
LD_PRELOAD to hook into other libraries is an old trick and can
usefully be used for debugging applications, especially when you
don’t have access to an application’s source code.
Shared C libraries in Go
Go 1.5 introduced new execution modes, or ‘build modes’, including
the ability to build Go packages into a shared C library by passing the
-buildmode=c-shared flag to the Go tool.
This means that non-Go programs can invoke functions from a Go package that has been compiled into a shared C library.
The shared library is created using the Go tool:
We can dynamically link to the
.so shared object file we have
created using the
LD_PRELOAD environment variable, for example:
cgo: Calling C from Go and vice-versa
For our Go functions to be visible to C programs, we have to export them by adding a cgo comment directly above the Go function:
cgo lets Go packages call C code and export Go functions to be called by C code; you can find out more in the Go blog article.
We also have to import the
Overriding a libc function
To override the behaviour of a libc function, we export our function so that it is visible to C programs as above:
Note that we must match the function signature of the original libc
function. You can see that we’re using the types provided by the
In the body of our function, we could re-implement the original libc function, however it’s probably easier for us just to call the original libc function. We do that by dynamically linking to the original libc library, and invoking the original function from inside our wrapper function.
There’s a dynamic library loader for Go on GitHub called
can open the libc library using
We can then using
dl.Sym() to load a symbol (in our case, a function)
from libc into a pointer. Here, we load the symbol for the
function into a pointer named
Next, we invoke the original
strrchr function and return its return
value in our wrapper function. The whole wrapper function looks like
Writing a simple remote shell in Go
We now know how to hook into a libc function. For fun, let’s try writing a simple ‘backdoor’ shell server in Go that binds to a port and accepts arbitrary commands whenever the libc function is invoked.
For this, we’ll use the
net/textproto package, part of the Go
standard library, which “implements generic support for text-based
request/response protocols in the style of HTTP, NNTP, and SMTP”.
First, we bind to a TCP port using
net.Listen() from the
This providers us with a
Listener on which we can accept connections:
Whenever a connection is accepted, we call
handleConnection() in a
goroutine, which allows us to handle multiple connections concurrently.
backdoor() function looks like this:
handleConnection(), we create a buffered I/O reader to read from
the connection using
bufio.NewReader(). We then pass the buffered I/O
textproto.NewReader(), which provides convenience methods
for reading from a text-based protocol connection, such as
We then pass the line we read from the connection to
bash, as a command and write the output back to the connection.
handleConnection() function looks like this:
Starting the remote shell when the libc function is called
Let’s try starting the remote shell whenever our target application tries to invoke a given libc function.
We’re going to start the remote shell server in a goroutine, so it can continue working in the background while we invoke the origin libc function being called.
In this example, I’m hooking into the
strrchr() libc function, simply
because it’s used early on by top and it has a simple function signature
that’s easy to implement in cgo. If this were a rootkit, you might be
stat(), but for our example,
Testing it out
To compile the shared C library, use
go build with the
If we set the
LD_PRELOAD environment variable to use our shared
library and invoke
top under Linux, we can then connect to the remote
You should be able to send commands so long as the top process is running.
The code presented here was an experiment I wrote for fun but it highlights how easy it is to write a server using a text-based protocol in Go and the power of Go shared libraries, for example the ability to start a goroutine from a C application.
You can find find the example code in full on GitHub.