There's no function, given the PID of a target process, to inject code directly. In order to load code from a library, you need to A) map the library into memory B) handle relocations (i.e., connect up that library's references to functions in already-loaded libraries like libc) C) start executing it. Step A can be done from a process itself with mmap, but there isn't a function to map memory into another process. Step B is just some modifications to values in memory. Step C involves somehow changing the instruction pointer. All of these are easy to do if you're running code as the process itself - in particular, if you can get control of the instruction pointer, you can avoid doing steps A and B manually by just instructing the program to use the existing libc dlopen() function, which mmaps a library and handles relocations.

So we need to be able to change the instruction pointer of the process and coax it into calling dlopen(). The functionality available to you is:

- You can use ptrace to stop the process, read and write the saved registers, read and write its memory, resume the process, or resume the process until the next instruction (single step) or next syscall. It's the API used by debuggers. If you run `print dlopen("file.so")` from GDB, this is exactly what GDB will do: it'll use ptrace to make up a stack frame to call dlopen with the arguments you specify and then hit a breakpoint, and GDB will print the result. Another good example of using this to inject code for non-malicious purposes is https://github.com/nelhage/reptyr , which lets you tell another process to switch terminals (e.g., move it into a screen/tmux session).

- You can use /proc/$pid/mem or the process_vm_writev syscall to modify the stack but not the instruction pointer, because the stack is in memory. That puts you effectively in the same position as an exploit writer: you have to create a stack frame that returns to custom code somehow, or uses return-oriented programming to call the function you want to call. Then when the program returns from its current stack frame, it'll call your shellcode or ROP gadget.

- You can use /proc/$pid/mem or the process_vm_writev syscall to modify the code at the instruction pointer, even though you can't modify the instruction pointer itself. That's what this program does - it backs up a segment of instructions about to be executed, overwrites it with shellcode, and restores the instructions once the shellcode is done.

ptrace, /proc/$pid/mem, and process_vm_readv/writev have permission checks. On most systems, you have to be running as the same user as the program you're trying to mess with. On systems including the "Yama" Linux security module - notably including Ubuntu - you also have to be an ancestor process of the process you're trying to attach to, which is a security measure intended to make it harder for malware to steal secrets out of other programs you're running, without preventing debuggers etc. from working. See https://www.kernel.org/doc/Documentation/security/Yama.txt for details.

(Again, this is the same interface as a debugger uses. If you're not prevented from debugging your own programs, yes, you can inject malicious code into them without limitation - and it's generally easier to use a debugger than to write your own custom injector to do it.)