so注入的主要思路是:

  1. 找到目标进程
  2. 使用ptrace跟踪目标进程,保存目标进程的状态
  3. 获得mmap、dlopen、dlsym等库函数在目标进程中的偏移地址
  4. 调用mmap在目标进程内部申请一段内存空间
  5. 将我们的ShellCode写入到刚才申请的内存空间中
  6. dlopen打开Shellcode,dlsym调用Shellcode库的函数
  7. 恢复原so状态,detach

核心点

ptrace

1
2
3
4
5
6
7
8
#include <sys/ptrace.h>

long ptrace(enum _ptrace_request request,pid_t pid,void * addr ,void *data);

1). enum __ptrace_request request:指示了ptrace要执行的命令。
2). pid_t pid: 指示ptrace要跟踪的进程。
3). void *addr: 指示要监控的内存地址。
4). void *data: 存放读取出的或者要写入的数据。

简单点,ptrace是个系统调用,功能是提供一个进程(父进程)监控和管理另一个进程(子进程)的能力,并允许父进程检查和替换子进程的内核镜像(包括寄存器)的值。

基本原理是: 当使用了ptrace跟踪后,所有发送给被跟踪的子进程的信号(除了SIGKILL),都会被转发给父进程,而子进程则会被阻塞,这时子进程的状态就会被系统标注为TASK_TRACED。而父进程收到信号后,就可以对停止下来的子进程进行检查和修改,然后让子进程继续运行。

ptrace命令

1). 用PTRACE_ATTACH或者PTRACE_TRACEME 建立进程间的跟踪关系。
2). PTRACE_PEEKTEXT, PTRACE_PEEKDATA, PTRACE_PEEKUSR等读取子进程内存/寄存器中保留的值。
3). PTRACE_POKETEXT, PTRACE_POKEDATA, PTRACE_POKEUSR等把值写入到被跟踪进程的内存/寄存器中
4). 用PTRACE_CONT,PTRACE_SYSCALL, PTRACE_SINGLESTEP控制被跟踪进程以何种方式继续运行。
5). PTRACE_DETACH, PTRACE_KILL 脱离进程间的跟踪关系。

TIPS:

1. 进程状态TASK_TRACED用以表示当前进程因为被父进程跟踪而被系统停止。
2. 如在子进程结束前,父进程结束,则trace关系解除。
3. 利用attach建立起来的跟踪关系,虽然ps看到双方为父子关系,但在"子进程"中调用getppid()仍会返回原来的父进程id。
4. 不能attach到自己不能跟踪的进程,如non-root进程跟踪root进程。
5. 已经被trace的进程,不能再次被attach。
6. 即使是用PTRACE_TRACEME建立起来的跟踪关系,也可以用DETACH的方式予以解除。
7. 因为进入/退出系统调用都会触发一次SIGTRAP,所以通常的做法是在第一次(进入)的时候读取系统调用的参数,在第二次(退出)的时候读取系统调用的返回值。但注意execve是个例外。
8. 程序调试时的断点由int 3设置完成,而单步跟踪则可由ptrace(PTRACE_SINGLESTEP)实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
switch(pid = fork())  
{
case -1:
return -1;
case 0: //子进程
ptrace(PTRACE_TRACEME,0,NULL,NULL);
execl("./HelloWorld", "HelloWorld", NULL);
default: //父进程
wait(&val); //等待并记录execve
if(WIFEXITED(val))
return 0;
syscallID=ptrace(PTRACE_PEEKUSER, pid, ORIG_EAX*4, NULL);
printf("Process executed system call ID = %ld/n",syscallID);
ptrace(PTRACE_SYSCALL,pid,NULL,NULL);
}

在上面的程序中,fork出的子进程先调用了ptrace(PTRACE_TRACEME)表示子进程让父进程跟踪自己。然后子进程调用execl加载执行了HelloWorld。

而在父进程中则使用wait系统调用等待子进程的中断或结束。子进程因为设置了PTRACE_TRACEME而在执行系统调用被系统停止(设置为TASK_TRACED),这时父进程被唤醒,使用ptrace(PTRACE_PEEKUSER,pid,…)分别去读取子进程执行的系统调用ID(放在ORIG_EAX中)以及系统调用返回时的值(放在EAX中)。然后使用ptrace(PTRACE_SYSCALL,pid,…)指示子进程运行到下一次执行系统调用的时候(进入或者退出),直到子进程退出为止。

wait指令会让父进程挂起,等待子进程的中断或结束

断点原理

断点是大家在调试程序时常用的一个功能,如break linenumber,当执行到linenumber那一行的时候被调试程序会停止,等待debugger的进一步操作。
断点的实现原理,就是在指定的位置插入断点指令,当被调试的程序运行到断点的时候,产生SIGTRAP信号。该信号被gdb捕获并进行断点命中判定,当gdb判断出这次SIGTRAP是断点命中之后就会转入等待用户输入进行下一步处理,否则继续。

断点的设置原理: 在程序中设置断点,就是先将该位置的原来的指令保存,然后向该位置写入int 3。当执行到int 3的时候,发生软中断,内核会给子进程发出SIGTRAP信号,当然这个信号会被转发给父进程。然后用保存的指令替换int3,等待恢复运行。
断点命中判定:gdb把所有的断点位置都存放在一个链表中,命中判定即把被调试程序当前停止的位置和链表中的断点位置进行比较,看是断点产生的信号,还是无关信号。

单步调试

单步跟踪就是指在调试程序的时候,让程序运行一条指令/语句后就停下。GDB中常用的命令有next, step, nexti, stepi。单步跟踪又常分为语句单步(next, step)和指令单步(如nexti, stepi)。

在linux上,指令单步可以通过ptrace来实现。调用ptrace(PTRACE_SINGLESTEP,pid,…)可以使被调试的进程在每执行完一条指令后就触发一个SIGTRAP信号,让GDB运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
child = fork();  
if(child == 0) {
execl("./HelloWorld", "HelloWorld", NULL);
}
else {
ptrace(PTRACE_ATTACH,child,NULL,NULL);
while(1){
wait(&val);
if(WIFEXITED(val))
break;
count++;
ptrace(PTRACE_SINGLESTEP,child,NULL,NULL);
}
printf("Total Instruction number= %d/n",count);
}

1、找到目标进程

1
2
3
4
5
6
7
8
pid_t target_pid;

// PS命令能找到的进程
target_pid = find_pid_of("/system/bin/logwrapper");
if (-1 == target_pid) {
printf("Can't find the process\n");
return -1;
}

找到目标进程的实现方式为:通过proc文件系统来一一比较。

在Linux上,proc是一个伪文件系统,提供了访问内核数据的方法,一般挂载在“/proc”目录

每个正在运行着的进程都proc下都会有个目录,目录名为pid。

例如init进程,它的pid为1,所以对应/proc/1/,这个目录下保存的就是Init进程相关的数据。

找到目标进程的逻辑:

  • 打开proc目录
  • 读取该目录,并转为dirent
  • 读取dirent->d_name值,并转为int号,也就是pid号
  • 在循环遍历每一个读取到的pid目录下的cmdline,用\0’分割其中的字符串得到进程的args[],拿到args[0],也就是进程的绝对路径
  • 匹配进程名是否一致

dirent:是一个结构体,

1
2
3
4
5
6
7
8
struct dirent
{
long d_ino; /* inode number 索引节点号 */
off_t d_off; /* offset to this dirent 在目录文件中的偏移 */
unsigned short d_reclen; /* length of this d_name 文件名长 */
unsigned char d_type; /* the type of d_name 文件类型 */
char d_name [NAME_MAX+1]; /* file name (null-terminated) 文件名,最长256字符 */
}

cmdline:存储的是main方法用的参数,也就是args,args[0]就是进程的绝对路径

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int find_pid_of(const char *process_name)    
{
int id;
pid_t pid = -1;
DIR* dir;
FILE *fp;
char filename[32];
char cmdline[256];

struct dirent * entry;

if (process_name == NULL)
return -1;

dir = opendir("/proc");
if (dir == NULL)
return -1;

while((entry = readdir(dir)) != NULL) {
id = atoi(entry->d_name);
if (id != 0) {
sprintf(filename, "/proc/%d/cmdline", id);
fp = fopen(filename, "r");
if (fp) {
fgets(cmdline, sizeof(cmdline), fp);
fclose(fp);

if (strcmp(process_name, cmdline) == 0) {
/* process found */
pid = id;
break;
}
}
}
}

closedir(dir);
return pid;
}

2、ptrace跟踪目标进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (ptrace_attach(target_pid) == -1)   
{
goto exit;
}

int ptrace_attach(pid_t pid)
{
if (ptrace(PTRACE_ATTACH, pid, NULL, 0) < 0) {
perror("ptrace_attach");
return -1;
}

int status = 0;
// 挂起父进程,等待子进程中断或结束
waitpid(pid, &status , WUNTRACED);

return 0;
}

3、保存目标进程寄存器的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (ptrace_getregs(target_pid, &regs) == -1) 
{
goto exit;
}

// 将读取到的寄存器值存放在regs中
int ptrace_getregs(pid_t pid, struct pt_regs * regs)
{
if (ptrace(PTRACE_GETREGS, pid, NULL, regs) < 0) {
perror("ptrace_getregs: Can not get register values");
return -1;
}

return 0;
}

/* save original registers */
memcpy(&original_regs, &regs, sizeof(regs));

4、获得mmap、dlopen等库函数在目标进程中的偏移地址

1
2
3
4
5
6
7
const char *linker_path = "/system/bin/linker";   

mmap_addr = get_remote_addr(target_pid, libc_path, (void *)mmap);
dlopen_addr = get_remote_addr( target_pid, linker_path, (void *)dlopen );
dlsym_addr = get_remote_addr( target_pid, linker_path, (void *)dlsym );
dlclose_addr = get_remote_addr( target_pid, linker_path, (void *)dlclose );
dlerror_addr = get_remote_addr( target_pid, linker_path, (void *)dlerror );

在计算偏移地址时,有个小TIPS,一个相同的库对于不同的进程来说,它的偏移量是恒定的。

也就是说,A进程载入了mmap,B进程也载入了mmap,那么mmap在A进程中的偏移等于相对于B进程的偏移。

B.mmap地址 = B_lib库地址 + (A.mmap地址 -A_lib库地址)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void* get_remote_addr(pid_t target_pid, const char* module_name, void* local_addr)    
{
void* local_handle, *remote_handle;

local_handle = get_module_base(-1, module_name);
remote_handle = get_module_base(target_pid, module_name);

DEBUG_PRINT("[+] get_remote_addr: local[%x], remote[%x]\n", local_handle, remote_handle);

void * ret_addr = (void *)((uint32_t)local_addr + (uint32_t)remote_handle - (uint32_t)local_handle);

#if defined(__i386__)
if (!strcmp(module_name, libc_path)) {
ret_addr += 2;
}
#endif
return ret_addr;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void* get_module_base(pid_t pid, const char* module_name)    
{
FILE *fp;
long addr = 0;
char *pch;
char filename[32];
char line[1024];

if (pid < 0) {
/* self process */
snprintf(filename, sizeof(filename), "/proc/self/maps", pid);
} else {
snprintf(filename, sizeof(filename), "/proc/%d/maps", pid);
}

fp = fopen(filename, "r");

if (fp != NULL) {
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, module_name)) {
pch = strtok( line, "-" );
addr = strtoul( pch, NULL, 16 );

if (addr == 0x8000)
addr = 0;

break;
}
}

fclose(fp) ;
}

return (void *)addr;
}

5、调用mmap在目标进程申请内存空间

构建mmap调用需要的参数

1
2
3
4
5
6
7
8
9
10
11
12
/* call mmap */    
parameters[0] = 0; // addr
parameters[1] = 0x4000; // size
parameters[2] = PROT_READ | PROT_WRITE | PROT_EXEC; // prot
parameters[3] = MAP_ANONYMOUS | MAP_PRIVATE; // flags
parameters[4] = 0; //fd
parameters[5] = 0; //offset

if (ptrace_call_wrapper(target_pid, "mmap", mmap_addr, parameters, 6, &regs) == -1)
{
goto exit;
}
1
2
3
4
5
6
7
8
9
10
11
12
int ptrace_call_wrapper(pid_t target_pid, const char * func_name, void * func_addr, long * parameters, int param_num, struct pt_regs * regs)     
{
DEBUG_PRINT("[+] Calling %s in target process.\n", func_name);
if (ptrace_call(target_pid, (uint32_t)func_addr, parameters, param_num, regs) == -1)
return -1;

if (ptrace_getregs(target_pid, regs) == -1)
return -1;
DEBUG_PRINT("[+] Target process returned from %s, return value=%x, pc=%x \n",
func_name, ptrace_retval(regs), ptrace_ip(regs));
return 0;
}

这里做个平台判断,区分arm和x86,他们的实现方式不一样。下面只介绍arm

  • 首先将前面构建的mmap参数放入寄存器当中,对于arm来说,R0~R3是存放参数的,对于超过4位的参数一律放在栈中
  • 将目标进程的PC寄存器指向mmap的函数地址,重置CPSR状态寄存器,重置LR寄存器
  • 将配置完的寄存器应用到进程中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#if defined(__arm__)    
int ptrace_call(pid_t pid, uint32_t addr, long *params, uint32_t num_params, struct pt_regs* regs)
{
uint32_t i;
// 参数先放寄存器中,R0~R3
for (i = 0; i < num_params && i < 4; i ++) {
regs->uregs[i] = params[i];
}

// 多于4位的放在栈中
if (i < num_params) {
regs->ARM_sp -= (num_params - i) * sizeof(long) ;
ptrace_writedata(pid, (void *)regs->ARM_sp, (uint8_t *)&params[i], (num_params - i) * sizeof(long));
}

// 将ARM的PC寄存器指向addr,也就是上面传来的需要调用的函数地址
regs->ARM_pc = addr;

// 判断ARM当前所处的状态是thumb还是arm,它们的区别在于thumb是16位,arm是32位。属于arm的子集
// 然后重置CPSR程序状态寄存器
if (regs->ARM_pc & 1) {
/* thumb */
regs->ARM_pc &= (~1u);
regs->ARM_cpsr |= CPSR_T_MASK;
} else {
/* arm */
regs->ARM_cpsr &= ~CPSR_T_MASK;
}

// 重置LR寄存器
// LR寄存器存放的是子程序的返回地址
regs->ARM_lr = 0;

// 设置寄存器的值
if (ptrace_setregs(pid, regs) == -1
|| ptrace_continue(pid) == -1) {
printf("error\n");
return -1;
}

int stat = 0;
waitpid(pid, &stat, WUNTRACED);
while (stat != 0xb7f) {
if (ptrace_continue(pid) == -1) {
printf("error\n");
return -1;
}
waitpid(pid, &stat, WUNTRACED);
}

return 0;
}

#elif defined(__i386__)
long ptrace_call(pid_t pid, uint32_t addr, long *params, uint32_t num_params, struct user_regs_struct * regs)
{
regs->esp -= (num_params) * sizeof(long) ;
ptrace_writedata(pid, (void *)regs->esp, (uint8_t *)params, (num_params) * sizeof(long));

long tmp_addr = 0x00;
regs->esp -= sizeof(long);
ptrace_writedata(pid, regs->esp, (char *)&tmp_addr, sizeof(tmp_addr));

regs->eip = addr;

if (ptrace_setregs(pid, regs) == -1
|| ptrace_continue( pid) == -1) {
printf("error\n");
return -1;
}

int stat = 0;
waitpid(pid, &stat, WUNTRACED);
while (stat != 0xb7f) {
if (ptrace_continue(pid) == -1) {
printf("error\n");
return -1;
}
waitpid(pid, &stat, WUNTRACED);
}

return 0;
}
#else
#error "Not supported"
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int ptrace_writedata(pid_t pid, uint8_t *dest, uint8_t *data, size_t size)    
{
uint32_t i, j, remain;
uint8_t *laddr;

union u {
long val;
char chars[sizeof(long)];
} d;

j = size / 4;
remain = size % 4;

laddr = data;

for (i = 0; i < j; i ++) {
memcpy(d.chars, laddr, 4);
ptrace(PTRACE_POKETEXT, pid, dest, d.val);

dest += 4;
laddr += 4;
}

if (remain > 0) {
d.val = ptrace(PTRACE_PEEKTEXT, pid, dest, 0);
for (i = 0; i < remain; i ++) {
d.chars[i] = *laddr ++;
}

ptrace(PTRACE_POKETEXT, pid, dest, d.val);
}

return 0;
}
1
2
3
4
5
6
7
8
9
10
// 设置寄存器
int ptrace_setregs(pid_t pid, struct pt_regs * regs)
{
if (ptrace(PTRACE_SETREGS, pid, NULL, regs) < 0) {
perror("ptrace_setregs: Can not set register values");
return -1;
}

return 0;
}

写入Shellcode

首先拿到mmap映射的基地址,方便后续操作

1
2
3
4
5
6
7
8
9
10
11
12
map_base = ptrace_retval(&regs);   

long ptrace_retval(struct pt_regs * regs)
{
#if defined(__arm__)
return regs->ARM_r0;
#elif defined(__i386__)
return regs->eax;
#else
#error "Not supported"
#endif
}

将我们的Shellcode写入到目标进程中

1
ptrace_writedata(target_pid, map_base, library_path, strlen(library_path) + 1);

6、执行Shellcode

构建dlopen的执行参数,打开shellcode

1
2
3
4
5
6
7
parameters[0] = map_base;       
parameters[1] = RTLD_NOW| RTLD_GLOBAL;

if (ptrace_call_wrapper(target_pid, "dlopen", dlopen_addr, parameters, 2, &regs) == -1)
{
goto exit;
}

拿到shellcode的句柄,找到shellcode中目标方法的偏移地址,执行dlsym加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void * sohandle = ptrace_retval(&regs); 

#define FUNCTION_NAME_ADDR_OFFSET 0x100
ptrace_writedata(target_pid, map_base + FUNCTION_NAME_ADDR_OFFSET, function_name, strlen(function_name) + 1);
parameters[0] = sohandle;
parameters[1] = map_base + FUNCTION_NAME_ADDR_OFFSET;

if (ptrace_call_wrapper(target_pid, "dlsym", dlsym_addr, parameters, 2, &regs) == -1)
goto exit;

void * hook_entry_addr = ptrace_retval(&regs);
DEBUG_PRINT("hook_entry_addr = %p\n", hook_entry_addr);

#define FUNCTION_PARAM_ADDR_OFFSET 0x200
ptrace_writedata(target_pid, map_base + FUNCTION_PARAM_ADDR_OFFSET, param, strlen(param) + 1);
parameters[0] = map_base + FUNCTION_PARAM_ADDR_OFFSET;

if (ptrace_call_wrapper(target_pid, "hook_entry", hook_entry_addr, parameters, 1, &regs) == -1)
goto exit;

7、恢复原so的状态,detach

1
2
3
4
5
6
7
8
9
10
11
printf("Press enter to dlclose and detach\n");    
getchar();
parameters[0] = sohandle;

if (ptrace_call_wrapper(target_pid, "dlclose", dlclose, parameters, 1, &regs) == -1)
goto exit;

/* restore */
ptrace_setregs(target_pid, &original_regs);
ptrace_detach(target_pid);
ret = 0;
1
2
3
4
5
6
7
8
9
int ptrace_detach(pid_t pid)    
{
if (ptrace(PTRACE_DETACH, pid, NULL, 0) < 0) {
perror("ptrace_detach");
return -1;
}

return 0;
}

完结,撒花


参考链接:

Ptrace 详解:https://blog.csdn.net/ginray/article/details/60628251

Linux Ptrace 详解: https://blog.csdn.net/u012417380/article/details/60470075

本文代码来自网络,地址没找到了,注释自己写的