Windows in-memory injection is commonplace in current toolsets, there are quite a few methods to do it, and most of them are documented pretty well. Linux in-memory injection is essentially the same, however, not seen in toolsets quite as much. That is why, for this post, I am going to cover four different open-source methods that do in-memory injection, talk about how they work, give the benefits and pitfalls of the tools (as I see them), and provide a basic proof of concept script to help find them. Links to all the tools and methods covered will be located in the references section below. Hopefully, some of you will find it useful for both Red and Blue Team uses.
This tool works by using ptrace to inject shellcode that will call dlopen, loading a path to a shared library on disk and running it inside the binary where it is injected.
How it works
First, the code gets the address of malloc/free/ and __libc_dlopen_mode from libc.so inside of the target process identifier (PID). The address is derived from the address of the functions from the injection binary, subtracting the base address of libc, then adding those offsets to the base libc address of the target binary. After that, it attaches to the target process, gets the register state, sets register values that the shellcode being injected uses, and writes into the target process. Once that is all taken care of, they resume the target process to run the injected code and a breakpoint, then it restores the registers and detaches from the process. To write a shared object that can be used for it, just specify a ‘constructor’ function that runs before anything else, which starts a thread and then returns. To do this, prepend __attribute__((constructor)) to the front of a function definition like ‘void init_function(void)’. This will be the same for the other methods as well.
This method is one of the most common ones I have seen. The benefits of this are that it is a great tool that works on x86, x86_64, and ARM architectures, so it is very portable, and writing a new implementation for different architectures would not be too difficult (provided you like assembly). One of the main downfalls of this method is that the shared object will have to be on disk, which may not be a deal breaker for you, but since the shared object will be on disk, there is a chance that it could be detected and reverse engineered. For any Blue Teamers reading, you can check processes for shared objects that are loaded within them by parsing out the Maps file for the PID.
View of Maps
This is what shows up when deleted:
This tool uses the same method to inject into a process but is unique in writing a whole shared object into memory, then calling a ‘ReflectiveLoader’ function similar to a reflective DLL injection on Windows.
How it works
Instead of typing all the same stuff from Linux Inject, I am just going to reference it. They do all of the same steps, but instead of writing in shellcode to dlopen the shared object, they write in shellcode to mmap a section of memory to store the shared object, parsing out the ReflectiveLoader offset, then have the program call the reflective loader. The reflective loader then processes all of the relocations and loads what the libraries will need to function properly. It gets dlopen, dlsym, dlclose, calloc, and mprotect, and uses those functions to load itself into memory and call the constructor to execute.
The benefit of this method is that the injected shared object will never be on disk. The injector source code from the repo reads the shared object from disk, but with some modification, you can have it pull from the network or read it from an encrypted section. This is by far the most useful version I have seen so far. The downside of this method is that the memory is marked as RWX, which can be changed in the code but would take some development time. Other than that, from the list of injection methods included here, this seems to have the fewest drawbacks.
View of Maps
Injection with GDB
This method is pretty great. To inject with this method, all you need is a shared object and GDB installed on the system. Link to the original blog post about this is in the references section.
How it works
This method works by attaching to a process with GDB and having it use __libc_dlopen_mode() to load the shared object in the process. This method is rather simple to use and is a great portable version of injection.
This method has the benefit of not needing another binary to do the injection, just using native tools to inject a shared object into a processes memory. It also has the benefit of being extremely portable, so it should work on all architectures, as long as you can compile a shared object. The downside of this method is the same as Linux Inject, where the shared object is on disk and the same detection will find this as well.
From a shell, just run the following, which will attach to the process with GDB, and load your library:
echo 'print __libc_dlopen_mode("/tmp/sample_library.so", 2)' | gdb -p <PID>
This is another simple method and works by preloading the shared object into the memory of a process at runtime. This may not count as memory injection, but it is a way to load shared objects into a process, as long as you can restart the process. This one will not have a ‘how it works’ section because to use it you simply start the process and the library runs in that process.
This method has the same issue as Linux Inject and GDB Injection, where the shared object is on disk, but also has the problem of the binary being restarted. It also has the same benefit of the GDB Injection method, where it will be present on Linux systems without introduction of a new tool.
Overarching Concerns for Red and Blue
So, there are the four different methods to run a shared object inside of a process. Red Teams should consider that any fork ‘fork()’ you do in the thread will show a duplicate process in the process list. Additionally, whenever you spawn a process, it will show up in the process list as well, so to hide it completely, it will take a little more development effort to write. Blue Teams will need to consider while trying to detect, that they need to verify shared objects loaded into processes and check for RWX memory. When verifying the shared objects, its useful to know what packages are installed, and what is meant to be running on the system. These checks will require analysis to determine if it is a false positive or a real positive, which is where knowing what is installed and what should be running is vital. It is possible to write code to avoid RWX memory, which makes it a lot harder to find, but it is a nice starting point. Additionally, SELinux makes it a lot harder because it can protect processes from having execmem and execheap permissions, so I highly recommend looking into SELinux policies and how they work. Below you will find a simple script to do what you want, whether using it as- is or adding in code to filter out some false positives.
This quick script will check for RWX memory sections, deleted shared objects, and shared objects that don’t belong to a package on both Redhat and Debian. To run, just run the script as root with no arguments for Debian or Redhat, as the first argument for Redhat. Keep in mind that this is very rough and could be optimized.
import sys import os #Keeping track of items pids =  sharedobjects = set() cleansharedobjects = set() rwxMemory =  distro = "debian" if __name__ == "__main__": if len(sys.argv) > 1: if "red" in sys.argv.lower(): distro = "redhat" for item in os.listdir("/proc/"): try: #See if it's a PID number. testval = int(item) pids.append(item) except: continue for item in pids: fin = None try: fin = open("/proc/%s/maps"%(item), "r") except: continue data = fin.read() fin.close() for line in data.split("\n"): #Add rwx and shared object lines if ".so" in line: sharedobjects.add(line.replace("%s "%(line.split(" /")), "")) if "rwx" in line: rwxMemory.append("%s:%s"%(item,line)) #Show the deleted shared objects can be a sign if a shared object was deleted # after injecting, also has false positives if the server was updated and not # restarted. print("List of deleted shared objects:") for line in sharedobjects: if "deleted" in line: cleansharedobjects.add(line.split(" (deleted")) print("\t%s"%(line)) else: cleansharedobjects.add(line.strip()) #Create and run the verification command (redhat and Debian based distros) commandline = "dpkg -S %s 2>&1" if distro == "redhat": commandline = "rpm -qf %s" fin = os.popen(commandline%(" ".join(cleansharedobjects))) results = fin.read().split("\n") fin.close() print("Shared objects that don't belong:") for line in results: if "no path" in line or "not owned" in line: print("\t%s"%(line)) #Show identifiers for RWX memory permissions. print("RWX lines (NOTE: False positives for anything using scripting languages):") for line in rwxMemory: try: exefile = os.readlink("/proc/%s/exe"%(line.split(":"))) print("\t%s:%s"%(exefile, line)) except: print("\tNOT FOUND:%s"%(line)) print("DONE")
Example output from the script on a system with python running (libraries installed through pip). Note that if you are running on a clean system, you will see a few false positives, but they can be ignored if you know you installed those packages.
Author: Kevin Haubris
Kevin has 5+ years of experience in information security positions for both public and private sector focusing on both offensive and defensive security. During this time, he focused mostly on software development, pen testing, adversarial emulation, and reverse engineering.