本文主要是介绍(gaffe23/linux-inject) Github项目分析-linux之SO注入,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
原始链接 (gaffe23/linux-inject)项目分析-linux之SO注入
序言
原始项目: https://github.com/gaffe23/linux-inject
自己修改过的项目: https://github.com/redqx/linux-inject2
一个9年前(base 2024)的项目, 没怎么更新过,但项目在9年前来说也是写得非常棒的一个项目,
放到现在有些代码已经过时了,导致无法运行成功
fork: 224
start: 1.1k
time: 2024/8/31
x86_64 分析
项目原理
- injector使用 ptrace attach 目标进程(target), 然后target停下来
- injector寻找一块可执行rwx的空间
(/proc/{pid}/maps)
, 写入shellcode, 同时备份写入的空间为backup - injector获取target的寄存器环境,同时备份寄存器环境old_regs
- injector修改寄存器 rip指向shellcode,
- injector通过ptrace让程序跑起来, 先执行shellcode的malloc()函数,然后停下来
- injector获取寄存器环境, 通过rax获取malloc的返回值, rax = mem_buf_libpath,
- injector通过ptrace往mem_buf_libpath写入so文件的路径
- injector继续让target执行,执行shellcode的dlopen,然后停下来
- injector获取寄存器环境, 通过rax获取dlopen的返回值, 非0则加载成功
- 此刻target已经加载了so
- injector让target继续执行shellcode的free(), 释放malloc()的mem_buf_libpath, 然后停下来
- injector 恢复old_regs, 恢复backup的指令, 然后通过detach继续让target跑起来
具体分析
injector传参方式有2种,
way1: 指定process_name, 通过process_name寻找目标进程的pid
way2: 直接传入pid
流程如下
injector获取so的绝对路径
char* libPath = realpath(libname, NULL); // 通过libcname获取libso的完整路径
injector获取process_pid
if(!strcmp(command, "-n")) // 通过进程名去获取进程的pid{processName = commandArg;target_pid = findProcessByName(processName);if(target_pid == -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"))//直接从参数获取pid{target_pid = atoi(commandArg);printf("targeting process with pid %d\n", target_pid);}else{usage(argv[0]);return 1;}
获取injector的libc.so.6的基地址
long mylibcaddr = getlibcaddr(mypid); //
原理是通过/proc/{pid}/maps读取libc.so.6在内存中的布局
7f688cd83000-7f688cda9000 r--p 00000000 08:20 34128 /usr/lib/x86_64-linux-gnu/libc.so.6
7f688cda9000-7f688cf00000 r-xp 00026000 08:20 34128 /usr/lib/x86_64-linux-gnu/libc.so.6
7f688cf00000-7f688cf55000 r--p 0017d000 08:20 34128 /usr/lib/x86_64-linux-gnu/libc.so.6
7f688cf55000-7f688cf59000 r--p 001d1000 08:20 34128 /usr/lib/x86_64-linux-gnu/libc.so.6
7f688cf59000-7f688cf5b000 rw-p 001d5000 08:20 34128 /usr/lib/x86_64-linux-gnu/libc.so.6
可用读取到7f688cd83000
就是libc.so.6在内存中的基地址
然后获取injector中, malloc, dlopen, free函数的偏移
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");//无法直接获取 __libc_dlopen_mode()long dlopenAddr = getFunctionAddress("dlopen");// use the base address of libc to calculate offsets for the syscalls// we want to uselong libc_mallocOffset = mallocAddr - mylibcaddr;long libc_freeOffset = freeAddr - mylibcaddr;long libc_dlopenOffset = dlopenAddr - mylibcaddr;
然后获取target中, malloc, dlopen, free 函数的真实地址
long remote_LibcAddr = getlibcaddr(target_pid);long remote_MallocAddr = remote_LibcAddr + libc_mallocOffset;long remote_FreeAddr = remote_LibcAddr + libc_freeOffset;long remote_DlopenAddr = remote_LibcAddr + libc_dlopenOffset;
injector attach 目标进程,
target在被attach后,会停下来, 发送一个signal消息给injector
injector等待这个消息,等待后, 获取此刻target寄存器环境, 备份为old_regs
ptrace_attach(target_pid); //附加调试目标进程,并等待子进程的停止, 目标进程收到被调试的信息后,会停下来ptrace_getregs(target_pid, &oldregs);//获取当前target寄存器信息memcpy(®s, &oldregs, sizeof(struct user_regs_struct));
寻找一块可写入的rwx内存,待会往里面写入shellcode
现在找到那块内存,然后让regs.rip指向这块内存
同时给一些寄存器赋值 rdi,rsi,rdx,rcs…这些寄存器是给shellcode传参
// find a good address to copy code to// long addr = freespaceaddr(target) + sizeof(long); // 寻找第一块可写的内存,一般是代码段long remote_mem_rwx = freespaceaddr(target_pid) + 0xf00 ; //直接放远一点// now that we have an address to copy code to, set the target's rip to// it. we have to advance by 2 bytes here because rip gets incremented// by the size of the current instruction, and the instruction at the// start of the function to inject always happens to be 2 bytes long.regs.rip = remote_mem_rwx + 2; //实际执行的地方是 rip - 2, 所以我们指向的地方得是rip + 2// pass arguments to my function injectSharedLibrary() by loading them// into the right registers. note that this will definitely only work// on x64, because it relies on the x64 calling convention, in which// arguments are passed via registers rdi, rsi, rdx, rcx, r8, and r9.// see comments in injectSharedLibrary() for more details.regs.rdi = remote_MallocAddr;regs.rsi = remote_FreeAddr;regs.rdx = remote_DlopenAddr;regs.rcx = libPathLength;if(regs.rsp&0xf){//高版本Linux中, 调用dlopen或者__libc_dlopen_mode前,保证rsp是16的倍数regs.rsp = regs.rsp - 8;//这个点卡了我2天,草!cao!}ptrace_setregs(target_pid, ®s);printf("[inject]: change target process status\n");
寻找内存的原理依然是读取 /proc/{pid}/maps
通常寻找的第一块rx内存是代码段,
55ca4511c000-55ca4514b000 r--p 00000000 08:20 16699 /..
55ca4514b000-55ca4520f000 r-xp 0002f000 08:20 16699 /..
55ca4520f000-55ca45248000 r--p 000f3000 08:20 16699 /..
55ca45248000-55ca4524c000 r--p 0012b000 08:20 16699 /..
55ca4524c000-55ca45255000 rw-p 0012f000 08:20 16699 /..
关于为什么regs.rip = remote_mem_rwx + 2;
作者本来写的就是 +2
我不信邪,写regs.rip = remote_mem_rwx;
后来发现因为这个问题,引起了很大莫名其妙的问题…困扰很久…
后来通过调试,发现rip会指向addr - 2的地方执行,尽管rip指向的是addr
所以我们要让rip指向我们要执行的地方,那么就让rip往后多指2字节
关于为什么rsp - 8
后来发现,调用dlopen或者__libc_dlopen_mode
都需要让rsp是16的倍数
以前做pwn题,在调用system()前也要保证rsp是16的倍数
然后往找到的内存中写入shellcode, 并备份写入前已有的字节码
// 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. note that on x64,// gcc and clang both force function addresses to be word-aligned,// which means that functions are padded with NOPs. as a result, even// though we've found the length of the function, it is very likely// padded with NOPs, so we need to actually search 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_pid, remote_mem_rwx, backup, injectSharedLibrary_size);// set up a buffer to hold the code we're going 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, (char*)injectSharedLibrary + 4, injectSharedLibrary_size);/*
.text:0000563A66E30F44 55 push rbp
.text:0000563A66E30F45 48 89 E5 mov rbp, rsp ; 跳过这几个字节
.text:0000563A66E30F48 56 push rsi
.text:0000563A66E30F49 52 push rdx
.text:0000563A66E30F4A 41 51 push r9
.text:0000563A66E30F4C 49 89 F9 mov r9, rdi
.text:0000563A66E30F4F 48 89 CF mov rdi, rcx
.text:0000563A66E30F52 41 FF D1 call r9*/// 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_pid, remote_mem_rwx, newcode, injectSharedLibrary_size);//写入shellcode
shellcode内容如下
// void injectSharedLibrary(long mallocaddr, long freeaddr, long dlopenaddr) // 这几个参数没用
void injectSharedLibrary()
{// 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 useasm(// 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");// char* lib_soname = malloc( xx_length )asm("push %r9 \n" // save previous value of r9, because we're going to use it to call malloc()"mov %rdi,%r9 \n" // r9 = malloc()"mov %rcx,%rdi \n" // rdi = lenght(libso_name)"callq *%r9 \n" // call malloc()"pop %r9 \n" //pop the previous value of r9 off the stack"int $3" // 暂停一下,injector处理一下新开辟的内容rax, 往内存rax中写入libso_name);//继续运行 f9// call __libc_dlopen_mode() to load the shared library// __libc_dlopen_mode()无法直接通过dlsym()找到, 在静态的libc.so.6中也无法直接找到, __libc_dlopen_mode()好像是3个参数// 所以换位dlopen打开吧....asm(// get the address of __libc_dlopen_mode() off of the stack so we can call it"pop %rdx \n" // rdx = dlopen"push %r9 \n" // as before, save the previous value of r9 on the stack"mov %rdx,%r9 \n" //r9 = dlopen"mov %rax,%rdi \n" // rax = rdi = lib_soname"movabs $1,%rsi \n" // rsi = 1 = RTLD_LAZY"callq *%r9 \n" // call dlopen_mode"pop %r9 \n" // restore old r9 value"int $3" //暂停,让injector处理一下);// 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.// 下面的free()函数感觉不一定要释放,^-^...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" // rdi = rax = dlopen() ????"pop %rsi \n" // rsi = free()"push %rbx \n" // save previous rbx value"mov %rsi,%rbx \n" // rbx = rsi = free"xor %rsi,%rsi \n" // zero out rsi, because free() might think that it contains something that should be freed// break in so that we can check out the arguments right before making the call"int $3 \n" // 修改rdi为libso_path"callq *%rbx \n"// call free()"pop %rbx"// restore previous rbx value);//最后停止asm("int $3 \n");// 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.
}
大概功能就是
char* mem_libpath = malloc(xxx_len);//injector会往mem_libpath写入内容
void* handle = dlopen(mem_libpath,RTLD_LAZY)
free(mem_libpath)
我修改了void injectSharedLibrary()
函数声明类型
原始的是void injectSharedLibrary(long mallocaddr, long freeaddr, long dlopenaddr)
原始的声明会让injectSharedLibrary多出一些字节码,反正我们也用不到那些参数
在shellcode写入后, 寄存器参数也准备好后
就让target跑起来
ptrace_f9(target_pid,1); // 准备执行shellcode,参数已经放入寄存器中, 同时等待target的int中断
//ptrace(PTRACE_CONT,...
target第一段跑的shellcode代码如下
// 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 useasm(// 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");// char* lib_soname = malloc( xx_length )asm("push %r9 \n" // save previous value of r9, because we're going to use it to call malloc()"mov %rdi,%r9 \n" // r9 = malloc()"mov %rcx,%rdi \n" // rdi = lenght(libso_name)"callq *%r9 \n" // call malloc()"pop %r9 \n" //pop the previous value of r9 off the stack"int $3" // 暂停一下,injector处理一下新开辟的内容rax, 往内存rax中写入libso_name);
可用看到最后有一个int3, 这个是为了停下来,让injector去处理
第一段shellcode执行的功能就是 char* libso_path = malloc (xxx)
在target执行了int3后,停下来,发消息给injector
injector处理如下,
大概就是获取malloc的返回值rax,然后写入内容
// 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(®s, 0, sizeof(struct user_regs_struct));ptrace_getregs(target_pid, ®s);//unsigned long long remote_malloc_buf = regs.rax;//获取malloc函数返回地址, 虽然在x64下,long是8字节, 但regs.rax却是long long类型printf("[inject]: get remote addr for malloc() = %x\n",remote_malloc_buf);if(remote_malloc_buf == 0){fprintf(stderr, "malloc() failed to allocate memory\n");restoreStateAndDetach(target_pid, remote_mem_rwx, 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 rax, which contains malloc's return value,// and copy the name of our shared library to that address inside the// target process.ptrace_write(target_pid, remote_malloc_buf, libPath, libPathLength);//往remote_malloc_buf写入libpath
然后让target继续跑
ptrace_f9(target_pid,1);
target会执行dlopen,打开libso
// call __libc_dlopen_mode() to load the shared library// __libc_dlopen_mode()无法直接通过dlsym()找到, 在静态的libc.so.6中也无法直接找到, __libc_dlopen_mode()好像是3个参数// 所以换位dlopen打开吧....asm(// get the address of __libc_dlopen_mode() off of the stack so we can call it"pop %rdx \n" // rdx = dlopen"push %r9 \n" // as before, save the previous value of r9 on the stack"mov %rdx,%r9 \n" //r9 = dlopen"mov %rax,%rdi \n" // rax = rdi = lib_soname"movabs $1,%rsi \n" // rsi = 1 = RTLD_LAZY"callq *%r9 \n" // call dlopen_mode"pop %r9 \n" // restore old r9 value"int $3" //暂停,让injector处理一下);
target执行了dlopen后,停下来,交给injector处理
// check out what the registers look like after calling dlopen. //struct user_regs_struct dlopen_regs;memset(®s, 0, sizeof(struct user_regs_struct));ptrace_getregs(target_pid, ®s);unsigned long long remote_libso_addr = regs.rax; //获取dlopen函数返回值// if rax is 0 here, then __libc_dlopen_mode failed, and we should bail// out cleanly.printf("[inject]: get remote %s at =%x\n",libname,remote_libso_addr);if(remote_libso_addr == 0){fprintf(stderr, "__libc_dlopen_mode() failed to load %s\n", libname); //查看有没有加载成功restoreStateAndDetach(target_pid, remote_mem_rwx, backup, injectSharedLibrary_size, oldregs);free(backup);free(newcode);return 1;}// now check /proc/pid/maps to see whether injection was successful.if(checkloaded(target_pid, libname)){printf("[inject]: \"%s\" successfully injected\n", libname);}else{fprintf(stderr, "could not inject \"%s\"\n", libname);}
injector会通过dlopen的返回值确定是否加载成功
然后再读取/proc/{pid}/maps
再次检测是否有加载成功
加载成功后,继续让target跑起来
ptrace_f9(target_pid,1);
target会执行free()
// 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.// 下面的free()函数感觉不一定要释放,^-^...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" // rdi = rax = dlopen() ????"pop %rsi \n" // rsi = free()"push %rbx \n" // save previous rbx value"mov %rsi,%rbx \n" // rbx = rsi = free"xor %rsi,%rsi \n" // zero out rsi, because free() might think that it contains something that should be freed// break in so that we can check out the arguments right before making the call"int $3 \n" // 修改rdi为libso_path"callq *%rbx \n"// call free()"pop %rbx"// restore previous rbx value);
值得注意的是,在target里面,以当前shellcode的状况,我们已经搞丢了malloc开辟的内存地址
于是在执行call free之前,target停下来,把参数rdi修改为libso_path
// 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_getregs(target_pid, ®s);regs.rdi = remote_libso_addr; //free(rdi = remote_libso_addr)ptrace_setregs(target_pid, ®s);ptrace_f9(target_pid,1); //执行到最后的int3
ps: 原项目并没有这样处理,他没有执行调用free,虽然在shellcode中写了free的调用
然后继续让target跑起来
ptrace_f9(target_pid,1);
target会执行完free(libso_path),然后停下来
//最后停止asm("int $3 \n");
这样差不多,libso就加载完毕了
于是inject 恢复之前的old_regs寄存器环境,然后恢复被覆盖的字节码backup
void restoreStateAndDetach(pid_t target, unsigned long remote_mem_rwx, void* backup, int datasize, struct REG_TYPE oldregs)
{ptrace_write(target, remote_mem_rwx, backup, datasize);//恢复之前的指令ptrace_setregs(target, &oldregs);//恢复之前的寄存器ptrace_detach(target);//detach
}
大概执行了detach, target会继续运行吧…QAQ
这样…一个libso的注入差不多就这样了
这篇关于(gaffe23/linux-inject) Github项目分析-linux之SO注入的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!