利用篇ptrace系统调用linux可以完成进程运行时代码注入功能
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/user.h>
#include <wait.h>
#include <time.h>
#include "ptrace.h"
/*
* ptrace_attach()
*
* Use ptrace() to attach to a process. This requires calling waitpid() to
* determine when the process is ready to be traced.
*
* args:
* - int pid: pid of the process to attach to
*
*/
void ptrace_attach(pid_t target)
{
int waitpidstatus;
if(ptrace(PTRACE_ATTACH, target, NULL, NULL) == -1)
{
fprintf(stderr, "ptrace(PTRACE_ATTACH) failed\n");
exit(1);
}
if(waitpid(target, &waitpidstatus, WUNTRACED) != target)
{
fprintf(stderr, "waitpid(%d) failed\n", target);
exit(1);
}
}
/*
* ptrace_detach()
*
* Detach from a process that is being ptrace()d. Unlike ptrace_cont(), this
* completely ends our relationship with the target process.
*
* args:
* - int pid: pid of the process to detach from. this process must already be
* ptrace()d by us in order for this to work.
*
*/
void ptrace_detach(pid_t target)
{
if(ptrace(PTRACE_DETACH, target, NULL, NULL) == -1)
{
fprintf(stderr, "ptrace(PTRACE_DETACH) failed\n");
exit(1);
}
}
/*
* ptrace_getregs()
*
* Use ptrace() to get a process' current register state. Uses REG_TYPE
* preprocessor macro in order to allow for both ARM and x86/x86_64
* functionality.
*
* args:
* - int pid: pid of the target process
* - struct REG_TYPE* regs: a struct (either user_regs_struct or user_regs,
* depending on architecture) to store the resulting register data in
*
*/
void ptrace_getregs(pid_t target, struct REG_TYPE* regs)
{
if(ptrace(PTRACE_GETREGS, target, NULL, regs) == -1)
{
fprintf(stderr, "ptrace(PTRACE_GETREGS) failed\n");
exit(1);
}
}
/*
* ptrace_cont()
*
* Continue the execution of a process being traced using ptrace(). Note that
* this is different from ptrace_detach(): we still retain control of the
* target process after this call.
*
* args:
* - int pid: pid of the target process
*
*/
void ptrace_cont(pid_t target)
{
struct timespec* sleeptime = malloc(sizeof(struct timespec));
sleeptime->tv_sec = 0;
sleeptime->tv_nsec = 5000000;
if(ptrace(PTRACE_CONT, target, NULL, NULL) == -1)
{
fprintf(stderr, "ptrace(PTRACE_CONT) failed\n");
exit(1);
}
nanosleep(sleeptime, NULL);
// make sure the target process received SIGTRAP after stopping.
checktargetsig(target);
}
/*
* ptrace_setregs()
*
* Use ptrace() to set the target's register state.
*
* args:
* - int pid: pid of the target process
* - struct REG_TYPE* regs: a struct (either user_regs_struct or user_regs,
* depending on architecture) containing the register state to be set in the
* target process
*
*/
void ptrace_setregs(pid_t target, struct REG_TYPE* regs)
{
if(ptrace(PTRACE_SETREGS, target, NULL, regs) == -1)
{
fprintf(stderr, "ptrace(PTRACE_SETREGS) failed\n");
exit(1);
}
}
/*
* ptrace_getsiginfo()
*
* Use ptrace() to determine what signal was most recently raised by the target
* process. This is primarily used for to determine whether the target process
* has segfaulted.
*
* args:
* - int pid: pid of the target process
*
* returns:
* - a siginfo_t containing information about the most recent signal raised by
* the target process
*
*/
siginfo_t ptrace_getsiginfo(pid_t target)
{
siginfo_t targetsig;
if(ptrace(PTRACE_GETSIGINFO, target, NULL, &targetsig) == -1)
{
fprintf(stderr, "ptrace(PTRACE_GETSIGINFO) failed\n");
exit(1);
}
return targetsig;
}
/*
* ptrace_read()
*
* Use ptrace() to read the contents of a target process' address space.
*
* args:
* - int pid: pid of the target process
* - unsigned long addr: the address to start reading from
* - void *vptr: a pointer to a buffer to read data into
* - int len: the amount of data to read from the target
*
*/
void ptrace_read(int pid, unsigned long addr, void *vptr, int len)
{
int bytesRead = 0;
int i = 0;
long word = 0;
long *ptr = (long *) vptr;
while (bytesRead < len)
{
word = ptrace(PTRACE_PEEKTEXT, pid, addr + bytesRead, NULL);
if(word == -1)
{
fprintf(stderr, "ptrace(PTRACE_PEEKTEXT) failed\n");
exit(1);
}
bytesRead += sizeof(word);
ptr[i++] = word;
}
}
/*
* ptrace_write()
*
* Use ptrace() to write to the target process' address space.
*
* args:
* - int pid: pid of the target process
* - unsigned long addr: the address to start writing to
* - void *vptr: a pointer to a buffer containing the data to be written to the
* target's address space
* - int len: the amount of data to write to the target
*
*/
void ptrace_write(int pid, unsigned long addr, void *vptr, int len)
{
int byteCount = 0;
long word = 0;
while (byteCount < len)
{
memcpy(&word, vptr + byteCount, sizeof(word));
word = ptrace(PTRACE_POKETEXT, pid, addr + byteCount, word);
if(word == -1)
{
fprintf(stderr, "ptrace(PTRACE_POKETEXT) failed\n");
exit(1);
}
byteCount += sizeof(word);
}
}
/*
* checktargetsig()
*
* Check what signal was most recently returned by the target process being
* ptrace()d. We expect a SIGTRAP from the target process, so raise an error
* and exit if we do not receive that signal. The most likely non-SIGTRAP
* signal for us to receive would be SIGSEGV.
*
* args:
* - int pid: pid of the target process
*
*/
void checktargetsig(int pid)
{
// check the signal that the child stopped with.
siginfo_t targetsig = ptrace_getsiginfo(pid);
// if it wasn't SIGTRAP, then something bad happened (most likely a
// segfault).
if(targetsig.si_signo != SIGTRAP)
{
fprintf(stderr, "instead of expected SIGTRAP, target stopped with signal %d: %s\n", targetsig.si_signo, strsignal(targetsig.si_signo));
fprintf(stderr, "sending process %d a SIGSTOP signal for debugging purposes\n", pid);
ptrace(PTRACE_CONT, pid, NULL, SIGSTOP);
exit(1);
}
}
/*
* restoreStateAndDetach()
*
* Once we're done debugging a target process, restore the process' backed-up
* data and register state and let it go on its merry way.
*
* args:
* - pid_t target: pid of the target process
* - unsigned long addr: address within the target's address space to write
* backed-up data to
* - void* backup: a buffer pointing to the backed-up data
* - int datasize: the amount of backed-up data to write
* - struct REG_TYPE oldregs: backed-up register state to restore
*
*/
void restoreStateAndDetach(pid_t target, unsigned long addr, void* backup, int datasize, struct REG_TYPE oldregs)
{
ptrace_write(target, addr, backup, datasize);
ptrace_setregs(target, &oldregs);
ptrace_detach(target);
}
#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <string.h>
#include <unistd.h>
#include <dlfcn.h>
#include "utils.h"
/*
* findProcessByName()
*
* Given the name of a process, try to find its PID by searching through /proc
* and reading /proc/[pid]/exe until we find a process whose name matches the
* given process.
*
* args:
* - char* processName: name of the process whose pid to find
*
* returns:
* - a pid_t containing the pid of the process (or -1 if not found)
*
*/
pid_t findProcessByName(char* processName)
{
if(processName == NULL)
{
return -1;
}
struct dirent *procDirs;
DIR *directory = opendir("/proc/");
if (directory)
{
while ((procDirs = readdir(directory)) != NULL)
{
if (procDirs->d_type != DT_DIR)
continue;
pid_t pid = atoi(procDirs->d_name);
int exePathLen = 10 + strlen(procDirs->d_name) + 1;
char* exePath = malloc(exePathLen * sizeof(char));
if(exePath == NULL)
{
continue;
}
sprintf(exePath, "/proc/%s/exe", procDirs->d_name);
exePath[exePathLen-1] = '\0';
char* exeBuf = malloc(PATH_MAX * sizeof(char));
if(exeBuf == NULL)
{
free(exePath);
continue;
}
ssize_t len = readlink(exePath, exeBuf, PATH_MAX - 1);
if(len == -1)
{
free(exePath);
free(exeBuf);
continue;
}
exeBuf[len] = '\0';
char* exeName = NULL;
char* exeToken = strtok(exeBuf, "/");
while(exeToken)
{
exeName = exeToken;
exeToken = strtok(NULL, "/");
}
if(strcmp(exeName, processName) == 0)
{
free(exePath);
free(exeBuf);
closedir(directory);
return pid;
}
free(exePath);
free(exeBuf);
}
closedir(directory);
}
return -1;
}
/*
* freespaceaddr()
*
* Search the target process' /proc/pid/maps entry and find an executable
* region of memory that we can use to run code in.
*
* args:
* - pid_t pid: pid of process to inspect
*
* returns:
* - a long containing the address of an executable region of memory inside the
* specified process' address space.
*
*/
long freespaceaddr(pid_t pid)
{
FILE *fp;
char filename[30];
char line[850];
long addr;
char str[20];
char perms[5];
sprintf(filename, "/proc/%d/maps", pid);
fp = fopen(filename, "r");
if(fp == NULL)
exit(1);
while(fgets(line, 850, fp) != NULL)
{
sscanf(line, "%lx-%*lx %s %*s %s %*d", &addr, perms, str);
if(strstr(perms, "x") != NULL)
{
break;
}
}
fclose(fp);
return addr;
}
/*
* getlibcaddr()
*
* Gets the base address of libc.so inside a process by reading /proc/pid/maps.
*
* args:
* - pid_t pid: the pid of the process whose libc.so base address we should
* find
*
* returns:
* - a long containing the base address of libc.so inside that process
*
*/
long getlibcaddr(pid_t pid)
{
FILE *fp;
char filename[30];
char line[850];
long addr;
char perms[5];
char* modulePath;
sprintf(filename, "/proc/%d/maps", pid);
fp = fopen(filename, "r");
if(fp == NULL)
exit(1);
while(fgets(line, 850, fp) != NULL)
{
sscanf(line, "%lx-%*lx %*s %*s %*s %*d", &addr);
if(strstr(line, "libc-") != NULL)
{
break;
}
}
fclose(fp);
return addr;
}
/*
* checkloaded()
*
* Given a process ID and the name of a shared library, check whether that
* process has loaded the shared library by reading entries in its
* /proc/[pid]/maps file.
*
* args:
* - pid_t pid: the pid of the process to check
* - char* libname: the library to search /proc/[pid]/maps for
*
* returns:
* - an int indicating whether or not the library has been loaded into the
* process (1 = yes, 0 = no)
*
*/
int checkloaded(pid_t pid, char* libname)
{
FILE *fp;
char filename[30];
char line[850];
long addr;
char perms[5];
char* modulePath;
sprintf(filename, "/proc/%d/maps", pid);
fp = fopen(filename, "r");
if(fp == NULL)
exit(1);
while(fgets(line, 850, fp) != NULL)
{
sscanf(line, "%lx-%*lx %*s %*s %*s %*d", &addr);
if(strstr(line, libname) != NULL)
{
fclose(fp);
return 1;
}
}
fclose(fp);
return 0;
}
/*
* getFunctionAddress()
*
* Find the address of a function within our own loaded copy of libc.so.
*
* args:
* - char* funcName: name of the function whose address we want to find
*
* returns:
* - a long containing the address of that function
*
*/
long getFunctionAddress(char* funcName)
{
void* self = dlopen("libc.so.6", RTLD_LAZY);
void* funcAddr = dlsym(self, funcName);
return (long)funcAddr;
}
/*
* findRet()
*
* Starting at an address somewhere after the end of a function, search for the
* "ret" instruction that ends it. We do this by searching for a 0xc3 byte, and
* assuming that it represents that function's "ret" instruction. This should
* be a safe assumption. Function addresses are word-aligned, and so there's
* usually extra space at the end of a function. This space is always padded
* with "nop"s, so we'll end up just searching through a series of "nop"s
* before finding our "ret". In other words, it's unlikely that we'll run into
* a 0xc3 byte that corresponds to anything other than an actual "ret"
* instruction.
*
* Note that this function only applies to x86 and x86_64, and not ARM.
*
* args:
* - void* endAddr: the ending address of the function whose final "ret"
* instruction we want to find
*
* returns:
* - an unsigned char* pointing to the address of the final "ret" instruction
* of the specified function
*
*/
unsigned char* findRet(void* endAddr)
{
unsigned char* retInstAddr = endAddr;
while(*retInstAddr != INTEL_RET_INSTRUCTION)
{
retInstAddr--;
}
return retInstAddr;
}
/*
* usage()
*
* Print program usage and exit.
*
* args:
* - char* name: the name of the executable we're running out of
*
*/
void usage(char* name)
{
printf("usage: %s [-n process-name] [-p pid] [library-to-inject]\n", name);
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/user.h>
#include <wait.h>
#include "utils.h"
#include "ptrace.h"
/*
* injectSharedLibrary()
*
* This is the code that will actually be injected into the target process.
* This code is responsible for loading the shared library into the target
* process' address space. First, it calls malloc() to allocate a buffer to
* hold the filename of the library to be loaded. Then, it calls
* __libc_dlopen_mode(), libc's implementation of dlopen(), to load the desired
* shared library. Finally, it calls free() to free the buffer containing the
* library name. Each time it needs to give control back to the injector
* process, it breaks back in by executing an "int $3" instruction. See the
* comments below for more details on how this works.
*
*/
void injectSharedLibrary(long mallocaddr, long freeaddr, long dlopenaddr)
{
// here are the assumptions I'm making about what data will be located
// where at the time the target executes this code:
//
// ebx = address of malloc() in target process
// edi = address of __libc_dlopen_mode() in target process
// esi = address of free() in target process
// ecx = size of the path to the shared library we want to load
// for some reason it's adding 1 to esi, so subtract 1 from it
asm("dec %esi");
// call malloc() from within the target process
asm(
// choose the amount of memory to allocate with malloc() based on the size
// of the path to the shared library passed via ecx
"push %ecx \n"
// call malloc
"call *%ebx \n"
// copy malloc's return value (i.e. the address of the allocated buffer) into ebx
"mov %eax, %ebx \n"
// break back in so that the injector can get the return value
"int $3"
);
// call __libc_dlopen_mode() to load the shared library
asm(
// 2nd argument to __libc_dlopen_mode(): flag = RTLD_LAZY
"push $1 \n"
// 1st argument to __libc_dlopen_mode(): filename = the buffer we allocated earlier
"push %ebx \n"
// call __libc_dlopen_mode()
"call *%edi \n"
// break back in so that the injector can check the return value
"int $3"
);
// call free() on the previously malloc'd buffer
asm(
// 1st argument to free(): ptr = the buffer we allocated earlier
"push %ebx \n"
// call free()
"call *%esi"
);
// we already overwrote the RET instruction at the end of this function
// with an INT 3, so at this point the injector will regain control of
// the target's execution.
}
/*
* injectSharedLibrary_end()
*
* This function's only purpose is to be contiguous to injectSharedLibrary(),
* so that we can use its address to more precisely figure out how long
* injectSharedLibrary() is.
*
*/
void injectSharedLibrary_end()
{
}
int main(int argc, char** argv)
{
if(argc < 4)
{
usage(argv[0]);
return 1;
}
char* command = argv[1];
char* commandArg = argv[2];
char* libname = argv[3];
char* libPath = realpath(libname, NULL);
char* processName = NULL;
pid_t target = 0;
if(!libPath)
{
fprintf(stderr, "can't find file \"%s\"\n", libname);
return 1;
}
if(!strcmp(command, "-n"))
{
processName = commandArg;
target = findProcessByName(processName);
if(target == -1)
{
fprintf(stderr, "doesn't look like a process named \"%s\" is running right now\n", processName);
return 1;
}
printf("targeting process \"%s\" with pid %d\n", processName, target);
}
else if(!strcmp(command, "-p"))
{
target = atoi(commandArg);
printf("targeting process with pid %d\n", target);
}
else
{
usage(argv[0]);
return 1;
}
int libPathLength = strlen(libPath) + 1;
int mypid = getpid();
long mylibcaddr = getlibcaddr(mypid);
// find the addresses of the syscalls that we'd like to use inside the
// target, as loaded inside THIS process (i.e. NOT the target process)
long mallocAddr = getFunctionAddress("malloc");
long freeAddr = getFunctionAddress("free");
long dlopenAddr = getFunctionAddress("__libc_dlopen_mode");
// use the base address of libc to calculate offsets for the syscalls
// we want to use
long mallocOffset = mallocAddr - mylibcaddr;
long freeOffset = freeAddr - mylibcaddr;
long dlopenOffset = dlopenAddr - mylibcaddr;
// get the target process' libc address and use it to find the
// addresses of the syscalls we want to use inside the target process
long targetLibcAddr = getlibcaddr(target);
long targetMallocAddr = targetLibcAddr + mallocOffset;
long targetFreeAddr = targetLibcAddr + freeOffset;
long targetDlopenAddr = targetLibcAddr + dlopenOffset;
struct user_regs_struct oldregs, regs;
memset(&oldregs, 0, sizeof(struct user_regs_struct));
memset(®s, 0, sizeof(struct user_regs_struct));
ptrace_attach(target);
ptrace_getregs(target, &oldregs);
memcpy(®s, &oldregs, sizeof(struct user_regs_struct));
// find a good address to copy code to
long addr = freespaceaddr(target) + sizeof(long);
// now that we have an address to copy code to, set the target's eip to it.
regs.eip = addr;
// pass arguments to my function injectSharedLibrary() by loading them
// into the right registers. see comments in injectSharedLibrary() for
// more details.
regs.ebx = targetMallocAddr;
regs.edi = targetDlopenAddr;
regs.esi = targetFreeAddr;
regs.ecx = libPathLength;
ptrace_setregs(target, ®s);
// figure out the size of injectSharedLibrary() so we know how big of a buffer to allocate.
size_t injectSharedLibrary_size = (intptr_t)injectSharedLibrary_end - (intptr_t)injectSharedLibrary;
// also figure out where the RET instruction at the end of
// injectSharedLibrary() lies so that we can overwrite it with an INT 3
// in order to break back into the target process. gcc and clang both
// force function addresses to be word-aligned, which means that
// functions are padded at the end after the RET instruction that ends
// the function. as a result, even though in theory we've found the
// length of the function, it is very likely padded with NOPs, so we
// still need to do a bit of searching to find the RET.
intptr_t injectSharedLibrary_ret = (intptr_t)findRet(injectSharedLibrary_end) - (intptr_t)injectSharedLibrary;
// back up whatever data used to be at the address we want to modify.
char* backup = malloc(injectSharedLibrary_size * sizeof(char));
ptrace_read(target, addr, backup, injectSharedLibrary_size);
// set up the buffer containing the code to inject into the target process.
char* newcode = malloc(injectSharedLibrary_size * sizeof(char));
memset(newcode, 0, injectSharedLibrary_size * sizeof(char));
// copy the code of injectSharedLibrary() to a buffer.
memcpy(newcode, injectSharedLibrary, injectSharedLibrary_size - 1);
// overwrite the RET instruction with an INT 3.
newcode[injectSharedLibrary_ret] = INTEL_INT3_INSTRUCTION;
// copy injectSharedLibrary()'s code to the target address inside the
// target process' address space.
ptrace_write(target, addr, newcode, injectSharedLibrary_size);
// now that the new code is in place, let the target run our injected code.
ptrace_cont(target);
// at this point, the target should have run malloc(). check its return
// value to see if it succeeded, and bail out cleanly if it didn't.
struct user_regs_struct malloc_regs;
memset(&malloc_regs, 0, sizeof(struct user_regs_struct));
ptrace_getregs(target, &malloc_regs);
unsigned long targetBuf = malloc_regs.eax;
if(targetBuf == 0)
{
fprintf(stderr, "malloc() failed to allocate memory\n");
restoreStateAndDetach(target, addr, backup, injectSharedLibrary_size, oldregs);
free(backup);
free(newcode);
return 1;
}
// if we get here, then malloc likely succeeded, so now we need to copy
// the path to the shared library we want to inject into the buffer
// that the target process just malloc'd. this is needed so that it can
// be passed as an argument to __libc_dlopen_mode later on.
// read the current value of eax, which contains malloc's return value,
// and copy the name of our shared library to that address inside the
// target process.
ptrace_write(target, targetBuf, libPath, libPathLength);
// now call __libc_dlopen_mode() to attempt to load the shared library.
ptrace_cont(target);
// check out what the registers look like after calling
// __libc_dlopen_mode.
struct user_regs_struct dlopen_regs;
memset(&dlopen_regs, 0, sizeof(struct user_regs_struct));
ptrace_getregs(target, &dlopen_regs);
unsigned long long libAddr = dlopen_regs.eax;
// if eax is 0 here, then dlopen failed, and we should bail out cleanly.
if(libAddr == 0)
{
fprintf(stderr, "__libc_dlopen_mode() failed to load %s\n", libname);
restoreStateAndDetach(target, addr, backup, injectSharedLibrary_size, oldregs);
free(backup);
free(newcode);
return 1;
}
// now check /proc/pid/maps to see whether injection was successful.
if(checkloaded(target, libname))
{
printf("\"%s\" successfully injected\n", libname);
}
else
{
fprintf(stderr, "could not inject \"%s\"\n", libname);
}
// as a courtesy, free the buffer that we allocated inside the target
// process. we don't really care whether this succeeds, so don't
// bother checking the return value.
ptrace_cont(target);
// at this point, if everything went according to plan, we've loaded
// the shared library inside the target process, so we're done. restore
// the old state and detach from the target.
restoreStateAndDetach(target, addr, backup, injectSharedLibrary_size, oldregs);
free(backup);
free(newcode);
return 0;
}