The Universal Loader for Go

As promised in the previous post, Go Assembly on the arm64, I have been working on a very special project for the past couple months, and I’m very pleased to announce the Universal Loader!

This Golang library provides a consistent interface across all platforms for loading shared libraries from memory and without using CGO. When I say “all platforms”, I mean Linux, Windows, and OSX including the new M1 Apple chip.

Until someone tells me differently, I am claiming that this is the very first loader to work on the M1, Golang or not. I haven’t tried it myself yet, but it will likely also work on any POSIX system, like BSD variants, without changes. If you try this on a new platform, let me know!

Additionally, the Linux backend of the loader does not use memfd, and I believe this is the first Golang loader to do that as well. The Linux ramdisk implementation memfd is very easy to use, but it’s also relatively easy to detect.

Consistent Interface

On all platforms, this is a basic example of how to use Universal to load a shared library from memory and call an exported symbol inside it:

Just pass in your library as a byte array, call LoadLibrary on it and give it a name, then you can Call() any exported symbol in that library with whatever arguments you want.

All you have to do differently on a different platform is load the right type of library for your platform. On OSX, you would load myLibrary.dyld, on Linux, and on Windows myLibrary.DLL.

Check out the examples/ folder in the repo and the tests for more details.

Algorithms and References

The Linux and Windows implementations are very straight-forward and based on the same basic algorithms that have been widely used for years, and I used malisal’s excellent repo as a reference, with some minor changes.

For the OSX loader, I referred heavily to MalwareUnicorn’s wonderful training, but I did have to make a few updates. For one thing, dyld is not guaranteed to be the next image in memory after the base image.

Also have to give a heartful thank you to C-Sto and lesnuages, who contributed code to the Windows and OSX loaders, respectively.

Last but not least, this library makes heavy use of our own fork of the debug library, so this would not have been possible without contributions over the years from the whole Binject crew, and it’s a perfect example of the power of the tools we’ve made.

This isn’t the first tool to come out of our work in Binject, and it definitely will not be the last.

Once you get locked into a serious m*****e collection, the tendency is to push it as far as you can.

PT_NOTE to PT_LOAD Injection in ELF

We’ve recently checked in a new injection method for ELF executables into binjection, based on the PT_NOTE segment. This method of infection is both really handy (read: easy to implement) and has a high success rate on ET_EXEC binaries without breaking the binary.

Understanding the techniques of binary injection is useful to both the penetration tester and the reverse engineer who is analyzing files for malware. For the pentester, having a common file such as /bin/ls execute arbitrary code each time it is run is useful in establishing and maintaining persistence. A reverse engineer who notices that an ELF binary has 3 PT_LOAD segments and no PT_NOTE segment can safely assume that the binary at the very least is suspect, and should be evaluated further.

The breakdown of the algorithm

Most ET_EXEC binaries (but not golang! [1]) have a PT_NOTE segment that can be entirely overwritten without affecting program execution. The same segment can also be transformed into a loadable segment, where executable instructions can be placed.

We will go through the algorithm next, step by step. If you need a primer on the basics of ELF files and their headers, read this article before proceeding. When you’re ready, open the binjection source code and follow along.

1. Open the ELF file to be injected:

2. Save the original entry point, e_entry:

3. Parse the program header table, looking for the PT_NOTE segment:

4. Convert the PT_NOTE segment to a PT_LOAD segment:

5. Change the memory protections for this segment to allow executable instructions:

6. Change the entry point address to an area that will not conflict with the original program execution.

We will use 0xc000000. Pick an address that will be sufficiently far enough beyond the end of the original program’s end so that when loaded it does not overlap. 0xc000000 will work on most executables, but to be safe you should check before choosing.

7. Adjust the size on disk and virtual memory size to account for the size of the injected code:

8. Point the offset of our converted segment to the end of the original binary, where we will store the new code:

9. Patch the end of the code with instructions to jump to the original entry point:

10. Add our injected code to the end of the file:

11. Write the file back to disk, over the original file:

-= We’re done! =-

Notes on system and interpreter differences of PT_NOTE

The ELF ABI specification doesn’t describe a way in which this segment must be used, therefore it is inevitable that the researcher will find binaries that use the NOTE segment in unique ways. These are sometimes system specific
(BSD varieties have a lot of possible notes), and sometimes original language specific, as in Go binaries.

Final Thoughts

PT_NOTE injection of ET_EXEC ELF binaries is a solid and tested method for the pentester. It is fast and relatively easy to implement, and will work on a high percentage of Linux binaries without modification to the algorithm.

It is probably the easiest of the injection algorithms to start out with, for those new to this area.

Understanding this method of injection is valuable to the budding reverse engineer, an easy red flag to look for in the first of many injection methods that you will encounter over time.

ELF injection is an art-form, and to people on either side of the aisle who enjoy this type of binary analysis and manipulation, the myriad of tricks to implement and detect these methods provide a vast playground with nearly
endless possibilities.

I hope that this article will be useful to those attempting to begin and advance their work in this area.

All that we are is the result of what we have thought.
— Buddhist quote

[1] golang binaries do a unique thing with the PT_NOTE segment. It contains data that must be present during execution, therefore this injection method will not work on them… more on this to follow…