linux动态库编写与注入
动态库编写
加载时、结束时运行
通过__attribute__((constructor))
修饰的函数在该动态库被加载时进行调用。同样__attribute__((destructor))
修饰的函数会在程序正常退出时调用。其原理是将该函数的地址加入至.init_array
或.fini_array
段中,ld加载时就会调用该函数。
一般来说constructor
会更加常用,可以在里面动态地inline hook某些函数或读取/修改某些内存的值等等。
#include <stdio.h>
__attribute__((constructor))
void constructor() {
puts("Library Loaded...");
}
__attribute__((destructor))
void constructor() {
puts("Library Removed...");
}
通过同名覆盖实现hook函数
这种方式只能用于LD_PRELOAD
的方式加载。这边拿hook libc的函数举例,由于指定的动态库会在libc等函数之前加载,因此如果该动态库与libc中有同名函数,则会有限调用我们写的函数。然后在此函数中通过ld提供的dlsym
函数加载真实的函数地址,这样就可以在调用真实的函数前后我们进行任意的修改。例如下面是一个能够检测调用libc堆操作的代码。
里面复制了一些libc源码中的宏,以便读取堆块的大小。
#define _GNU_SOURCE
#include<stdio.h>
#include<stdint.h>
#include<dlfcn.h>
#include<stdlib.h>
#define unlikely(x) __builtin_expect(!!(x),0)
#define TRY_LOAD_HOOK_FUNC(name) if(unlikely(!g_sys_##name)){ \
g_sys_##name=(sys_##name##_t)dlsym(RTLD_NEXT,#name); \
}
#define __log(format,...) fprintf(stderr,"\33[35m\033[1m[@]"format"\33[0m",##__VA_ARGS__)
//---------------malloc.c-----------------
#ifndef INTERNAL_SIZE_T
# define INTERNAL_SIZE_T size_t
#endif
#define SIZE_SZ (sizeof (INTERNAL_SIZE_T))
#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size;
INTERNAL_SIZE_T mchunk_size;
struct malloc_chunk* fd;
struct malloc_chunk* bk;
struct malloc_chunk* fd_nextsize;
struct malloc_chunk* bk_nextsize;
};
typedef struct malloc_chunk* mchunkptr;
#define chunk2mem(p) ((void*)((char*)(p) + 2*SIZE_SZ))[菜单]
#define mem2chunk(mem) ((mchunkptr)((char*)(mem) - 2*SIZE_SZ))
#define PREV_INUSE 0x1
#define prev_inuse(p) ((p)->mchunk_size & PREV_INUSE)
#define IS_MMAPPED 0x2
#define chunk_is_mmapped(p) ((p)->mchunk_size & IS_MMAPPED)
#define NON_MAIN_ARENA 0x4
#define chunk_main_arena(p) (((p)->mchunk_size & NON_MAIN_ARENA) == 0)
#define SIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
#define chunksize(p) (chunksize_nomask (p) & ~(SIZE_BITS))
#define chunksize_nomask(p) ((p)->mchunk_size)
#define next_chunk(p) ((mchunkptr) (((char *) (p)) + chunksize (p)))
#define prev_size(p) ((p)->mchunk_prev_size)
#define prev_chunk(p) ((mchunkptr) (((char *) (p)) - prev_size (p)))
//---------------end-----------------
typedef void* (*sys_malloc_t)(size_t size);
static sys_malloc_t g_sys_malloc=NULL;
void* malloc(size_t size){
TRY_LOAD_HOOK_FUNC(malloc);
void *p=g_sys_malloc(size);
__log("malloc: size=0x%zX, ptr=%p\n",size,p);
return p;
}
typedef void* (*sys_calloc_t)(size_t num,size_t size);
static sys_calloc_t g_sys_calloc=NULL;
void* calloc(size_t num,size_t size){
TRY_LOAD_HOOK_FUNC(calloc);
void *p=g_sys_calloc(num,size);
__log("calloc: num=0x%zX, size=0x%zX, ptr=%p\n",num,size,p);
return p;
}
typedef void* (*sys_realloc_t)(void *old, size_t new_size);
static sys_realloc_t g_sys_realloc=NULL;
void* realloc(void *old, size_t new_size){
TRY_LOAD_HOOK_FUNC(realloc);
void *p=g_sys_realloc(old,new_size);
__log("realloc: old_ptr=%p, new_ptr=%p, new_size=0x%zX\n",old,p,new_size);
return p;
}
typedef void (*sys_free_t)(void *ptr);
static sys_free_t g_sys_free = NULL;
void free(void *ptr){
TRY_LOAD_HOOK_FUNC(free);
g_sys_free(ptr);
if(ptr){
__log("free: size=0x%zX, ptr=%p\n",chunksize(mem2chunk(ptr)),ptr);
}else{
__log("free: Null pointer\n");
}
}
动态库注入
在Linux中,让一个进程加载指定的动态链接库(.so文件)的方法比windows少得多,局限性也较大。
运行前指定
这种方式适用于在运行前加上指定环境变量或者直接修改ELF可执行文件,可以在程序运行最开始时运行某段代码,或执行函数hook等。
preload
preload参数可以让ld加载程序前加载指定动态库。有两种方式实现
LD_PRELOAD
是ld.so加载程序时会看的一个环境变量。可以指定一个或多个动态库(用空格或者分号隔开)。该环境变量里的动态库会按从左到右的顺序加载。但是如果程序中可以通过检测该环境变量来反注入。- 使用ld的
--preload
参数,这种方式可以在程序内部通过读取/proc/self/cmdline
探测到。
LD_PRELOAD=./injection.so ./target
/lib64/ld-linux-x86-64.so.2 --preload ./injection.so ./target
Patchelf
使用NixOS/patchelf工具,直接修改可执行文件,可以通过--add-needed
强制添加一个依赖,就不用每次启动都加上环境变量或者用ld启动。这种方式会修改文件,如果程序有签名校验等操作,就难以绕过。
patchelf --add-needed injection.so ./target
运行时注入
运行时注入大概dlsym分为以下几步:先让程序停下来,然后将shellcode写入rip指向的位置,最后让程序恢复执行。
根据这个原理,重构了MordragT/injex,并增加了一些新功能,放在ri-char/injex。
让程序停下来
Ptrace
ptrace是设计给调试器使用的。因此可以通过ptrace附加到目标进程上,控制目标进程是否执行。
局限性:
- 需要root权限或者给
/proc/sys/kernel/yama/ptrace_scope
写入1 - 如果目标进程上已经被ptrace了,就无法使用这种方式。例如ptrace反调试,先自己启一个进程调试关键进程,这样其他进程都调不上来了
由于其局限性较大,所以一般不采用该方式,在injex中也未实现该种方式。
Signal
ptrace会在attach时给目标进程发送SIGSTOP
信号,那我们也可以直接给目标进程发送该信号。当一个进程收到该信号后会进入Stopped
状态,并且无法屏蔽该信号。当收到SIGCONT
信号时,该进程才会继续执行。
DavidBuchanan314/dlinject中默认的方式就是这种方式。
kill -SIGSTOP $pid
kill -SIGCONT $pid
CGroups
CGroups是linux内核中给进程用于限制、统计、隔离各类系统资源的特性。他有两个不兼容的版本v1以及v2。我们可以创建一个CGroup,并将目标进程放入CGroup中,然后可以冻结这个CGroup,来实现暂停进程的功能。但是这种方式需要root权限。
DavidBuchanan314/dlinject中也实现了该方式,但是它针对的是CGroups v1。在v2中,实现的bash脚本大致如下需要(root权限):
export PID=xxx
# 暂停程序
mkdir /sys/fs/cgroup/injex_$PID # 创建名为injex_$PID的cgroup
echo $PID > /sys/fs/cgroup/injex_$PID/cgroup.procs # 将目标进程加入至injex_$PID
echo 1 > /sys/fs/cgroup/injex_$PID/cgroup.freeze # 将此cgroup冻结
# 查看是否已经处于冻结状态
cat /sys/fs/cgroup/injex_$PID/cgroup.events
# 恢复执行
echo $PID > /sys/fs/cgroup/cgroup.procs # 将目标进程放回默认cgroup
rmdir /sys/fs/cgroup/injex_$PID # 将创建的进程组删掉
写入shellcode
将程序暂停下来后可以通过/proc/pid/syscall
文件来获取目标进程此时的rip
和rsp
寄存器。然后向rip
指向的地址写入shellcode。然是如果在这段shellcode中直接调用_dl_open
,那么加载进来的动态库构造函数可能会破坏当前进程的栈,因此先备份一遍栈上内容,然后调用加载动态库,加载后将备份的内容恢复。
那如何将备份的内容恢复呢,当然可以再次让程序停下来,但这样成本太高,通常我们把要恢复的内容、以及恢复内容的代码也写入目标进程,然后直接让目标进程执行。
所以shellcode分为两段,第一段shellcode用mmap系统调用开辟空间,然后通过读取文件或者打开共享内存的方式加载第二段shellcode。第二段shellcode里面先调用_dl_open
加载动态库,然后再恢复寄存器、栈等信息,最后跳回原来的程序继续执行。具体的代码可以参考dlinject.py。在实际编写代码中还遇到一个小坑,就是解析ld.so
的符号的时候没有找到_dl_open
,甚至还换了一个解析ELF文件的库,后来发现是因为ld.so
的调试文件是分离的,应当单独加载调试文件,然后解析符号位置,调试文件位置可以参考GDB的加载顺序。
修改目标进程的内存有三种方式,他们都和ptrace需要相同的权限:有CAP_SYS_PTRACE或和目标进程是同一用户并且系统允许普通用户调试。
通过/proc/pid/mem
proc文件系统中将任意进程的内存都映射到了/proc/{pid}/mem
文件,可以通过直接读写这个文件来访问、修改目标进程的内存。
通过ptrace
Ptrace通过PTRACE_PEEKDATA和PTRACE_POKEDATA功能可以向目标进程读取和写入一个字的内容。但是一次只能读写一个字,效率很低,所以一般也不采用这种方式。
process_vm_readv/writev系统调用
process_vm_readv和process_vm_writev系统调用可以不经过内核内存进行跨进程内存读写。源码位于mm/process_vm_access.c。但是实际测试中,由于想要写入的段没有写权限,所以只能读不能写,。
总结
总结下来,想要动态注入还是需要CAP_SYS_PTRACE比较方便,但一般都是root权限直接注入就行了。还有需要改进的地方有:shellcode中调用的ld的_ld_open
可能被hook,因此可以自己写如一个加载器在shellcode中。强对抗环境下,进程可以直接在/proc/self/maps
看到可疑映射,可以想办法让加载出来的空间伪装成堆栈。