源代码:
https://github.com/haidragon/linux-inject
注入的代码中动态加载所用到的函数。代码为调用 dlopen启动一个动态库。
原理:
通过 ptrace 附加他,到目标内存中找一块可以执行区域,把代码注入到那里。同时保存原来的数据。用于执行完后恢复现场。关键点是注入的代码最后一个字节为 int 3.产生一个异常。这个异常的作用用来恢复现场。
命令为 到目录下 make
另外开一个终端到相同目录下
运行 ./sample-target
第一个终端运行:
sudo ./inject -n sample-target sample-library.so
关键代码
#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"
// 这是将实际注入目标进程的代码。
// 此代码负责将共享库加载到目标中进程的地址空间。首先,它调用malloc()来分配缓冲区。
// 保存要加载的库的文件名。
// 然后,它调用__libc_dlopen_mode(),libc实现dlopen(),加载所需的共享库。最后,它调用free()来释放包含库名称。
// 每次需要给注射器传递目标进程的中相关信息,原理是用的int $3异常
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:
//
// rdi = address of malloc() in target process
// rsi = address of free() in target process
// rdx = address of __libc_dlopen_mode() in target process
// rcx = size of the path to the shared library we want to load
// save addresses of free() and __libc_dlopen_mode() on the stack for later use
asm(
// rsi is going to contain the address of free(). it's going to get wiped
// out by the call to malloc(), so save it on the stack for later
"push %rsi \n"
// same thing for rdx, which will contain the address of _dl_open()
"push %rdx"
);
// call malloc() from within the target process
asm(
// save previous value of r9, because we're going to use it to call malloc()
"push %r9 \n"
// now move the address of malloc() into r9
"mov %rdi,%r9 \n"
// choose the amount of memory to allocate with malloc() based on the size
// of the path to the shared library passed via rcx
"mov %rcx,%rdi \n"
// now call r9; malloc()
"callq *%r9 \n"
// after returning from malloc(), pop the previous value of r9 off the stack
"pop %r9 \n"
// break in so that we can see what malloc() returned
"int $3"
);
// call __libc_dlopen_mode() to load the shared library
asm(
// get the address of __libc_dlopen_mode() off of the stack so we can call it
"pop %rdx \n"
// as before, save the previous value of r9 on the stack
"push %r9 \n"
// copy the address of __libc_dlopen_mode() into r9
"mov %rdx,%r9 \n"
// 1st argument to __libc_dlopen_mode(): filename = the address of the buffer returned by malloc()
"mov %rax,%rdi \n"
// 2nd argument to __libc_dlopen_mode(): flag = RTLD_LAZY
"movabs $1,%rsi \n"
// call __libc_dlopen_mode()
"callq *%r9 \n"
// restore old r9 value
"pop %r9 \n"
// break in so that we can see what __libc_dlopen_mode() returned
"int $3"
);
// call free() to free the buffer we allocated earlier.
//
// Note: I found that if you put a nonzero value in r9, free() seems to
// interpret that as an address to be freed, even though it's only
// supposed to take one argument. As a result, I had to call it using a
// register that's not used as part of the x64 calling convention. I
// chose rbx.
asm(
// at this point, rax should still contain our malloc()d buffer from earlier.
// we're going to free it, so move rax into rdi to make it the first argument to free().
"mov %rax,%rdi \n"
// pop rsi so that we can get the address to free(), which we pushed onto the stack a while ago.
"pop %rsi \n"
// save previous rbx value
"push %rbx \n"
// load the address of free() into rbx
"mov %rsi,%rbx \n"
// zero out rsi, because free() might think that it contains something that should be freed
"xor %rsi,%rsi \n"
// break in so that we can check out the arguments right before making the call
"int $3 \n"
// call free()
"callq *%rbx \n"
// restore previous rbx value
"pop %rbx"
);
// 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.
}
//仅仅用作计算偏移
void injectSharedLibrary_end()
{
}
int main(int argc, char** argv)
{
if(argc < 4)
{
usage(argv[0]);
return 1;
}
//./jnject -n exec libxxxx.so
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;
//通过进程名称找到 pid
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);
}
//pid
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;
//获取 pid
int mypid = getpid();
//获取lib 基址
long mylibcaddr = getlibcaddr(mypid);
//获取相关函数地址
long mallocAddr = getFunctionAddress("malloc");
long freeAddr = getFunctionAddress("free");
long dlopenAddr = getFunctionAddress("__libc_dlopen_mode");
//计算相对偏移
long mallocOffset = mallocAddr - mylibcaddr;
long freeOffset = freeAddr - mylibcaddr;
long dlopenOffset = dlopenAddr - mylibcaddr;
//获取目标进程
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));
//找一个可以执行的段
long addr = freespaceaddr(target) + sizeof(long);
//现在我们有一个复制代码的地址,将目标的RIP设置为它。
//我们必须提前2字节,因为RIP是由当前指令的大小递增的,并且在开始注入函数时的指令总是恰好是2字节长。
regs.rip = addr + 2;
//通过加载它们将参数传递给我的函数注入
//进入正确的寄存器。请注意,这肯定只会起作用。
//x64,因为它依赖于x64调用约定,其中
//参数通过寄存器RDI、RSI、RDX、RCX、R8和R9传递。
//在CujSudidLabValuy()中查看注释以获取更多细节。
regs.rdi = targetMallocAddr;
regs.rsi = targetFreeAddr;
regs.rdx = targetDlopenAddr;
regs.rcx = libPathLength;
//设置寄存器的值
ptrace_setregs(target, ®s);
//计算函数大小
size_t injectSharedLibrary_size = (intptr_t)injectSharedLibrary_end - (intptr_t)injectSharedLibrary;
//也可以找出RET指令在哪里结束
intptr_t injectSharedLibrary_ret = (intptr_t)findRet(injectSharedLibrary_end) - (intptr_t)injectSharedLibrary;
// 备份我们想要修改的地址所使用的任何数据
char* backup = malloc(injectSharedLibrary_size * sizeof(char));
ptrace_read(target, addr, backup, injectSharedLibrary_size);
// 设置一个缓冲区来保存我们将要注入目标进程的代码
char* newcode = malloc(injectSharedLibrary_size * sizeof(char));
memset(newcode, 0, injectSharedLibrary_size * sizeof(char));
// 拷贝代码injectSharedLibrary()
memcpy(newcode, injectSharedLibrary, injectSharedLibrary_size - 1);
// 覆盖最后一个字节为INT 3用于产生异常
newcode[injectSharedLibrary_ret] = INTEL_INT3_INSTRUCTION;
//注入
ptrace_write(target, addr, newcode, injectSharedLibrary_size);
//让目标运行我们的注入代码。
ptrace_cont(target);
//此时,目标应该运行malloc()。检查它的返回值,看看它是否成功,如果没有,就保释出来。
struct user_regs_struct malloc_regs;
memset(&malloc_regs, 0, sizeof(struct user_regs_struct));
ptrace_getregs(target, &malloc_regs);
//malloc()的返回值
unsigned long long targetBuf = malloc_regs.rax;
if(targetBuf == 0)
{
fprintf(stderr, "malloc() failed to allocate memory\n");
//恢复现场
restoreStateAndDetach(target, addr, backup, injectSharedLibrary_size, oldregs);
free(backup);
free(newcode);
return 1;
}
//写入路径名称 作为__libc_dlopen_mode 的参数
ptrace_write(target, targetBuf, libPath, libPathLength);
//继续执行目标 call __libc_dlopen_mode.
//重新运行 eip已经指向下一条
ptrace_cont(target);
//检查 rax 看dlopen是否调用成功
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.rax;
//如果 rax返回0 说明__libc_dlopen_mode加载失败,因此直接 恢复原有的状态
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;
}
//检查动态库
if(checkloaded(target, libname))
{
printf("\"%s\" successfully injected\n", libname);
}
else
{
fprintf(stderr, "could not inject \"%s\"\n", libname);
}
//再检查下返回值
ptrace_cont(target);
//恢复原有的状态
restoreStateAndDetach(target, addr, backup, injectSharedLibrary_size, oldregs);
free(backup);
free(newcode);
return 0;
}
utils.c
#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
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
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;
}
//proc/[pid]/exe为实际运行程序的符号链接
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
* 找到一个可执行的内存区域,我们可以使用它来运行代码。
*
* args:
* - pid_t pid: pid of process to inspect
*/
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;
}
unsigned char* findRet(void* endAddr)
{
unsigned char* retInstAddr = endAddr;
while(*retInstAddr != INTEL_RET_INSTRUCTION)
{
retInstAddr--;
}
return retInstAddr;
}
//接收参数
void usage(char* name)
{
printf("usage: %s [-n process-name] [-p pid] [library-to-inject]\n", name);
}
ptrace.c
#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
*
*/
//重新运行 eip已经指向下一条
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);
// 确保目标进程在停止后接收SIGLAFT。
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);
}
https://blog.51cto.com/haidragon/2135226