最近在开发一些东西的时候遇到了一些比较奇特的需求用到了该姿势,就顺势学习了一波,在一些情景下,我们需要无进程启动一些程序,此时线程注入就非常好用了,此处介绍下linux下的简单线程注入姿势
适用场景
- 无进程运行程序
- 动态打补丁(替换函数)
- 调试器,逆向软件开发
- 程序辅助器?可能dll注入更多些2333
一般目标
Tips
- 在没有特殊手段的情况下,我们是无法用两个调试器同时调试同一个进程的
- 我们只有在拥有对该进程的相应权限的时候才可以注入进程
注入手法
我们都知道在windows下我们可以用dll注入来进行线程注入,那么在Linux下,我们也可以用类似dll注入的方法,即共享库so注入来实现功能
那么我们在linux下该如何进行so注入呢?
0x00 LD_PRELOAD
在载入so文件的时候,_init_初始化是先于main函数运行的,因此我们可以通过LD_PRELOAD来载入一个写有_init_的.so库文件
例如
1 2 3 4 5 6 7 8 9 10 11
| #include <stdio.h> #include <dlfcn.h>
void _init() { printf("inject success!\n"); }
|
和我们想要注入的程序
1 2 3 4 5 6 7 8
| #include <stdlib.h>
int main() { printf("This is my program!"); }
|
然后我们编译一下
1 2 3
| gcc -fPIC -shared myso.c -c -o myso.o ld -shared -ldl myso.o -o myso.so gcc test.c -o test
|
之后再运行程序时输入
1
| LD_PRELOAD=./myso.so ./test
|
结果:
1 2 3
| ~/inject/Ld : LD_PRELOAD=./myso.so ./test inject success! This is my program!
|
想要取消也很简单
0x01 ld.so.preload
我们通过篡改预处理文件就可以达到我们想要的效果
我们将我们的恶意so文件写入ld.so.preload即可
0x02 strace
strace常用来跟踪进程执行时的系统调用和所接收的信号,因此我们也可以通过strace来注入进程,举个例子:
1
| strace -f -p pid -o /tmp/.log -e trace=read,write -s 1024
|
这里也解释一下所用到参数的意思
-f 指可以追踪进程fork出来的进程
-p 指定进程pid号
-o 将strace的输出写入指定文件
-e trace=read,write 跟踪进程读写的系统调用
-s 指定输出的字符串的最大长度.默认为32
关于更多参数,可以到strace 跟踪进程中的系统调用处查询
0x2 ptrace
利用ptrace注入的过程总结一下就是:
- 获取内存读写权限
- 使用dlopen(libc_dlopen_mode)函数载入so文件
- 调用so中的函数
获取进程内存读写权限
ptrace提供了一种使父进程得以监视和控制其它进程的方式,它还能够改变子进程中的寄存器和内核映像,因而可以实现断点调试和系统调用的跟踪(我们的注入就是基于这个
ptrace函数定义如下:
1 2 3 4
| #include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
|
其中第一个参数可以选择
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| PTRACE_TRACEME, PTRACE_ATTACH, PTRACE_SEIZE, PTRACE_INTERRUPT, PTRACE_KILL, PTRACE_PEEKTEXT,PTRACE_PEEKDATA PTRACE_PEEKUSER PTRACE_POKETEXT ,PTRACE_POKEDATA PTRACE_POKEUSER PTRACE_GETREGS,PTRACE_GETFPREGS, PTRACE_SETREGS,PTRACE_SETFPREGS PTRACE_SETREGSET (since Linux 2.6.34) PTRACE_GETSIGINFO (since Linux 2.3.99-pre6) PTRACE_SETSIGINFO (since Linux 2.3.99-pre6) PTRACE_PEEKSIGINFO (since Linux 3.10) PTRACE_GETSIGMASK (since Linux 3.11) PTRACE_SETSIGMASK (since Linux 3.11) PTRACE_CONT PTRACE_SYSCALL, PTRACE_SINGLESTEP PTRACE_DETACH etc.
|
更详细的内容可以在这里
或者这里 查询
当然我们也可以直接打开我们的terminal,然后 man ptrace
这里我们只举例几个我们会用到的几个参数
1 2 3 4 5 6 7
| PTRACE_ATTACH,PTRACE_TRACEME PTRACE_CONT PTRACE_PEEKTEXT, PTRACE_PEEKDATA, PTRACE_PEEKUSR PTRACE_POKETEXT,PTRACE_POKEDATA, PTRACE_POKEUSR PTRACE_GETREGS PTRACE_SETREGS PTRACE_DETACH, PTRACE_KILL
|
那么我们现在暂时拥有了内存的读写权限,现在就该向我们的进程中注入代码了
获得libc_dlopen_mode函数地址
首先我们需要了解两个个函数,dlopen()和dlsym(),函数定义如下:
1 2 3
| #include <dlfcn.h> void * dlopen( const char * pathname, int mode); void* dlsym(void* handler, const char* symbol);
|
这个函数的作用就是打开一个动态链接库,并且返回动态链接库的句柄
mode参数是so文件的打开方式我们这里只用到RTLD_LAZY,即在dlopen返回前,对于动态库中的未定义的符号不执行解析,也就是延迟绑定,关于其他的参数各位师傅感兴趣的话可以自行了解
而通过dlsym()函数我们可以从句柄中取出我们所需要用的函数来调用
那么即然我们已经获取了进程内存的读写权限,那么我们现在就直接开始调用dlopen函数搞事吧:)
这里要提及一句,dlopen并不是每一个进程都有的,但是_libc_dlopen_mode是默认包含的,所以在dlopen不能使用的时候,我们可以选择调用_libc_dlopen_mode
该函数定义如下:
1
| void * __libc_dlopen_mode (const char *name, int mode)
|
为便于通用性,这里我们就拿_libc_dlopen_mode作为实例来进行进程注入
那么现在我们确定了需要使用的函数,下一步要做的就是确定该函数在进程中的内存地址,这里有两种方法,这里都介绍一下
cat /proc/$pid/maps 得到基址之后根据偏移来得到
用类似于pwn中常用的ret2dl-resolve技巧来寻找
我们再来看一下elf是如何通过重定位来找到函数地址的,
这里贴一段来自一个师傅对最核心的代码和分析(具体来源有点找不到了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| _dl_fixup(struct link_map *l, ElfW(Word) reloc_arg) { const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset); const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)]; assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL); value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0); return elf_machine_fixup_plt (l, result, reloc, rel_addr, value); }
|
具体实现过程师傅们可以看一下我之前的关于ret2dl的文章或者其他师傅的一些文章
当然,此处安利<<程序员的自我修养—链接、装载与库>>
那么现在我们就可以通过类似的方法来获取__libc_dlopen_mode的地址啦
因为不是pwn题,所以我们需要做的并不是那么麻烦,在可以获得进程的内存读写权限的时候,我们只需要遍历一下link_map和相关链表就可以完成_libc_dlopen_mode的符号解析从而获取地址
此时我们需要的就是调用了
函数调用
那么我们该如何调用函数呢?
如果直接修改eip指针有可能会导致程序崩坏,因此我们在这里可以寻找一片nop内存进行注入,
我们可以选择调用mmap函数来将我们的文件写入到进程中,之后通过干扰重定位或者注入eip指针来调用我们所需要的函数,这里看一下mmap函数的定义:
1 2
| #include <sys/mman.h> void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
|
其他的参数想必大家都很熟悉,因此这里我只列出prot的一些参数选择
1 2 3 4
| PROT_EXEC PROT_READ PROT_WRITE PROT_NONE
|
而这个是可以通过or符号来组合选择的
比如 “PROT_READ | PROT_EXEC | PROT_WRITE”,可以 弄出一块可读可写可执行的区域,之后完成我们所需要做的操作即可
比如调用某个函数,或者替换某个函数
但是如果没有特殊需求(比如某某触发条件时),只需要执行我们想要执行的程序时,更简单的方法是通过共享对象构造函数来完成,也就是
1
| __attribute __((constructor))装饰器
|
在c++中,对于一个类我们可以通过编写构造函数来完成类中某些元素的初始化,在写好构造函数后,我们所创建的每一个对象都会自动调用构造函数来做一些初始化的操作,而共享对象构造函数也十分类似
共享库可以在加载时自动调用__attribute __((constructor))装饰器来加载我们所写的代码,如:
1 2 3 4 5 6 7 8
| #include <stdio.h> #include <system> void __attribute__((constructor)) test(void)
/* gcc -fPIC -shared myso.c -c -o myso.o ld -shared -ldl myso.o -o myso.so */
|
so文件的构造函数效果是输出时间,那么便于观看效果,我们写一个程序来检测效果(因为注入的线程会中断原本的程序流畅,因此建议另起一个线程来调用
1 2 3 4 5 6 7 8 9
| #include <stdio.h> #include <dlfcn.h> int main() { dlopen("./myso.so", RTLD_LAZY); }
|
然后运行一下,运行结果:
1 2
| ~/inject/attribute ./test Tue Nov 19 07:41:11 PST 2019
|
成功:)
这时如果有人测试
LD_PRELOAD=./myso.so ./test
的话,那么恭喜你,效果十分显著,具体效果师傅们可以自己试试(手动滑稽
另起线程的示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <stdio.h> #include <unistd.h> #include <pthread.h> void* test(void* a) { while (1) { system("date"); sleep(2); } } void __attribute__((constructor)) pthread_test(void) { pthread_t my_pthread; pthread_create(&my_pthread, NULL, test, NULL); }
|
而如果想替换函数,这有一篇文章讲的很好,虽然有些老了
1
| https://www.freebuf.com/articles/system/6388.html
|
最后的最后,向大家推荐几款比较好用的进程注入工具:
linux-inject
, saruman
, vegule
, cub3
, vlany