本文是对大犇蒸米spark安卓动态调试七种武器之离别钩 – Hooking的实践记录以及知识整理!原文请戳链接.

被测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
#include<stdlib.h>
int count = 0;
void print()
{
printf("hello,%d\n",count);
sleep(1);
}
int main(int argc, char const *argv[])
{
while(1){
print();
count++;
}
return 0;
}

注入方法都是通过ptrace实现的.
本文代码在github.

调用系统so库中的函数

目标函数是libc.so中的sleep函数.
正常情况是每输出一次暂停一秒,现在我们让它暂停10秒.

总体思路

  • 获取目标进程sleep函数地址
  • 在目标进程内执行sleep函数

如何获取函数地址

  • 已知条件: 本进程的基址、目标进程的基址、本进程中sleep函数的地址(当然,这些已知条件也是需要获得的 :p)
    /proc/<pid>/maps文件中存储的是进程内存映射详情,我们可以在这个文件中查询进程中so的基址;
    sleep函数在本进程中的地址直接可以获得(void*)

  • 求解: 目标进程中sleep函数地址

  • 计算: 本进程sleep地址 - 本进程基址 + 目标进程基址

获取so库的加载基址

打开/proc/<pid>/maps文件找到基址.

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
void* get_module_base(int pid, const char* module_name)
{
FILE *f; //文件指针
long addr = 0; //模块地址
char filename[32]; //maps路径
char *pch;
char line[1024]; //每行
if(pid == 0){
snprintf(filename, sizeof(filename), "/proc/self/maps");
} else {
snprintf(filename, sizeof(filename), "/proc/%d/maps", pid);
}
f = fopen(filename, "r");
if(f != NULL){
while(fgets(line,sizeof(line),f)){
if(strstr(line, module_name)) { //找到该行是否含有module_name
pch = strtok(line,"-"); //分割出基址字符串
addr = strtoul(pch,NULL,0x10); //转换为16进制数
if(addr == 0x8000) //32位linux程序中默认的text加载地址为0x08408000,64位的改为0x00400000,此时计算base地址就没什么用了
addr = 0;
break;
}
}
fclose(f);
}
return (void*)addr;
}

计算目标进程中sleep函数地址

1
2
3
4
5
6
7
8
9
10
11
12
13
long get_remote_addr(int target_pid, const char* module_name, void* local_addr)
{
void* local_handle = get_module_base(0,module_name);
void* remote_handle = get_module_base(target_pid,module_name);
printf("local_handle:%p remote_handle:%p\n", local_handle, remote_handle);
//计算公式
long remote_addr = (long)((uint32_t)local_addr - (uint32_t)local_handle + (uint32_t)remote_handle);
printf("remote_addr:%p\n", remote_addr);
return remote_addr;
}

如何执行sleep函数

  • 设置函数参数,如果参数个数小于等于4,参数按顺序放入R0~R4寄存器中;如果参数个数大于4,多余的部分需要入栈.
  • 设置pc寄存器的值,设置当前指令集标志位.
  • 应用以上寄存器的修改使之生效.
  • 等待函数执行.
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
//目标进程id,参数地址,参数个数,寄存器地址
int ptrace_call(int pid, long addr, long *params, uint32_t params_num, struct pt_regs* regs)
{
uint32_t i;
for (i = 0; i < params_num && i < 4; i++) { //设置少于4个的参数
regs->uregs[i] = params[i];
}
//设置多于4个的参数
if (i < params_num) {
regs->ARM_sp -= (params_num - i) * long_size; //抬高栈顶指针(分配空间)
writeData(pid, (long)regs->ARM_sp, (char*)&params[i], (params_num - i) * long_size); //写入
}
regs->ARM_pc = addr; //设置pc
if (regs->ARM_pc & 1) { //判断是否是Thumb指令
regs->ARM_pc &= (~1u); //Thumb的pc最后一位总是0
regs->ARM_cpsr |= CPSR_T_MASK; //T标志位为1
} else { //arm
regs->ARM_cpsr &= ~CPSR_T_MASK; //T标志位为0
}
regs->ARM_lr = 0; //为了使sleep函数执行完毕后产生“内存访问错误”,这样我们就知道什么时候执行完了
if(ptrace_setregs(pid,regs)==-1 || ptrace_continue(pid)==-1){ //目标进程继续执行
return -1;
}
int stat = 0; //WUNTRACED表示如果pid进程进入暂停状态,那么waitpid函数立即返回
waitpid(pid,&stat,WUNTRACED); //等待sleep函数执行,等待过程中本进程暂停执行
printf("%d\n", stat);
while (stat != 0xb7f) { //0xb7f表示目标进程进入暂停状态
printf("%d\n", stat);
if (ptrace_continue(pid) == -1) {
return -1;
}
waitpid(pid,&stat,WUNTRACED);
}
return 0;
}

如何注入

  • 保存寄存器的值
  • 获得sleep函数地址
  • 执行sleep函数
  • 恢复寄存器的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void inject(int pid)
{
struct pt_regs old_regs,regs;
long sleep_addr;
//保存寄存器
ptrace(PTRACE_GETREGS, pid, NULL, &old_regs);
memcpy(&regs, &old_regs, sizeof(regs));
long parameters[1];
parameters[0] = 10;
sleep_addr = get_remote_addr(pid, "libc.so", (void*)sleep);
ptrace_call(pid,sleep_addr,parameters,1,&regs);
//恢复寄存器
ptrace(PTRACE_SETREGS, pid, NULL, &old_regs);
}

主函数

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
#include<stdio.h>
#include<string.h> // strstr,strtok
#include<stdlib.h> //strtoul
#include<stdint.h> //uint32_t
#include<unistd.h> //sleep
#include<sys/ptrace.h>
#include<linux/wait.h> // WUNTRACED
#include<time.h>
int main(int argc, char* argv[])
{
if(argc != 2){
printf("usage: %s <pid to be traced>\n",argv[0]);
return 1;
}
int pid = atoi(argv[1]);
if(0 != ptrace(PTRACE_ATTACH, pid, NULL, NULL)){
printf("attach failed.");
return 1;
}
inject(pid);
ptrace(PTRACE_DETACH, pid, NULL, NULL);
return 0;
}

番外

/proc/<pid> 文件夹

文件 内容
cmdline 命令行全名(加参数变量)和 ps 命令中的command 列结果一样
cwd 进程的工作目录 和 (pwdx PID) 结果相同
environ 进程的环境变量
exe 一般是/bin/ 的链接
fd 进程打开的文件描述fu .用ls -l 可以查看具体的文件 (可以用lsof -p PID)
status 进程的相关状态
task 该目录下是进程所包含的线程(note: ps 可以查看线程)
mounts 进程挂载点
maps 进程内存映射详情

关于pc寄存器

arm.pdf 中的A2.4.3 Register 15 and the program counter有这样一段话:
是关于指令集在pc寄存器上的表现的.

Reading the program counter
When an instruction reads the PC, the value read depends on which instruction set it comes from:
• For an ARM instruction, the value read is the address of the instruction plus 8 bytes. Bits [1:0] of this
value are always zero, because ARM instructions are always word-aligned.
• For a Thumb instruction, the value read is the address of the instruction plus 4 bytes. Bit [0] of this
value is always zero, because Thumb instructions are always halfword-aligned.

关于CPSR寄存器

31 30 29 28 27 26 25 24 23 20 19 16 15 10 9 8 7 6 5 4 0
N Z C V Q Res J RESERVED GE[3:0] RESERVED E A I F T M[4:0]

其中J和T标记位代表当前指令集:

J T Instruction set
0 0 ARM
0 1 Thumb
1 0 Jazelle
1 1 RESERVED

关于waitpid

详细介绍可看官方文档.

参数status

wait函数调用过后,status指针指向可以被宏解析的值,这些宏在ndk目录下platforms/android-21/arch-arm/usr/include/sys/wait.h文件中定义.

高2字节用于表示导致子进程的退出或暂停状态信号值(WTERMSIG),低2字节表示子进程是退出(0x0)还是暂停(0x7f)状态(WEXITSTATUS)。
如:0xb7f就表示子进程为暂停状态,导致它暂停的信号量为11即sigsegv错误。
关于错误代码的文档可看这里,
定义在ndk目录下platforms/android-21/arch-arm/usr/include/asm/signal.h中.

其中两个宏:
WEXITSTATUS(*statusPtr):
if the child process terminates normally, this macro evaluates to the lower 8 bits of the value passed to the exit or _exit function or returned from main.
WTERMSIG(*statusPtr)
if the child process ends by a signal that was not caught, this macro evaluates to the number of that signal.

参数options

指定了waitpid的额外行动.选项有:

WNOHANG:
告诉waitpid不等程序中止立即返回status信息.
正常情况是当主进程对子进程使用了waitpid,主进程就会阻塞直到waitpid返回status信息;如果指定了WNOHANG选项,主进程就不会阻塞了.
如果还没有可用的status信息,waitpid返回0.

WUNTRACED:
告诉waitpid,如果子进程进入暂停状态或者已经终止,那么就立即返回status信息,正常情况是紫禁城终止的时候才返回.
如果是被ptrace的子进程,那么即使不提供WUNTRACED参数,也会在子进程进入暂停状态的时候立即返回。
对于使用ptrace_cont运行的子进程,它会在3种情况下进入暂停状态:①下一次系统调用;②子进程退出;③子进程的执行发生错误。

总结

程序中的0xb7f就表示子进程进入了暂停状态,且发送的错误信号为11(SIGSEGV),它表示试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。
当子进程执行完注入的函数后,由于我们在前面设置了regs->ARM_lr = 0,它就会返回到0地址处继续执行,这样就会产生SIGSEGV了.

调用自定义so库中的函数

  • 保存当前寄存器的状态
  • 获取目标程序的mmap, dlopen, dlsym, dlclose函数地址
  • 调用mmap分配空间保存参数信息
  • 调用dlopen加载so库
  • 调用dlsym找到目标函数地址
  • 执行目标函数
  • 调用dlclose卸载so库
  • 恢复寄存器的状态

保存当前寄存器的状态

1
2
3
struct pt_regs old_regs,regs;
ptrace(PTRACE_GETREGS, pid, NULL, &old_regs);
memcpy(&regs,&old_regs,sizeof(regs));

获取目标程序的mmap, dlopen, dlsym, dlclose函数地址

1
2
3
4
5
long mmap_addr,dlopen_addr,dlsym_addr,dlclose_addr;
mmap_addr = get_remote_addr(pid, libc_path, (void*)mmap);
dlopen_addr = get_remote_addr(pid, libc_path, (void*)dlopen);
dlsym_addr = get_remote_addr(pid, libc_path, (void*)dlsym);
dlclose_addr = get_remote_addr(pid, libc_path, (void*)dlclose);

调用mmap分配空间保存参数信息

mmap的原型如下:

1
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

参数 描述
addr 映射的起始地址,为0表示由系统决定映射的起始地址
length 映射的长度
prot 映射的内存保存属性,不能与文件的打开模式冲突
flags 指定映射对象的类型,映射选项和映射页是否可以共享
fd 有效的文件描述符,一般是由open()函数返回;其值也可以设置为-1,此时需要指定flags参数中的MAP_ANON,表明进行的是匿名映射
offset 被映射对象内容的起点

这里我们需要的调用语句是mmap(0,0x4000,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_ANONYMOUS|MAP_PRIVATE,0,0),
PROT_EXEC表示可执行.
PROT_READ表示可读.
PROT_WRITE表示可写.
MAP_PRIVATE表示建.立一个写入时拷贝的私有映射.内存区域的写入不会影响到原文件.这个标志和以上标志是互斥的,只能使用其中一个.
MAP_ANONYMOUS表示匿名映射,映射区不与任何文件关联.
则:

1
2
3
4
5
6
7
8
9
10
11
long parameters[10];
parameters[0] = 0; //构造参数
parameters[1] = 0x4000;
parameters[2] = PROT_READ | PROT_WRITE | PROT_EXEC;
parameters[3] = MAP_ANONYMOUS | MAP_PRIVATE;
parameters[4] = 0;
parameters[5] = 0;
ptrace_call(pid,mmap_addr,parameters,6,&regs);
//调用结束后获得r0中保存的返回值
ptrace(PTRACE_GETREGS,pid,NULL,&regs);
long mapping_base = regs.ARM_r0;

调用dlopen加载so库

原型:

1
void *dlopen(const char *filename, int flags);
参数 描述
filename so库名
flags 打开方式

这里我们需要的调用语句是dlopen(so_path,RTLD_NOW | RTLD_GLOBAL),
RTLD_NOW表示需要在dlopen返回前,解析出所有未定义符号,如果解析不出来在dlopen会返回NULL;
RTLD_GLOBAL表示动态库中定义的符号可被其后打开的其它库解析.
则:

1
2
3
4
5
6
writeData(pid, mapping_base, so_path, strlen(so_path)+1); //将库名字符串放入目标进程空间
parameters[0] = mapping_base;
parameters[1] = RTLD_NOW | RTLD_GLOBAL;
ptrace_call(pid, dlopen_addr, parameters, 2, &regs);
ptrace(PTRACE_GETREGS,pid,NULL,&regs); //调用结束后获得r0中保存的返回值
long handle = regs.ARM_r0;

调用dlsym找到目标函数地址

原型:

1
void *dlsym(void *handle, const char *symbol);
参数 描述
handle so库的基址
symbol 函数名地址

这里我们需要的调用语句是dlsym(handle, function_name),则:

1
2
3
4
5
6
writeData(pid, mapping_base, function_name, strlen(function_name)+1);
parameters[0] = handle;
parameters[1] = mapping_base;
ptrace_call(pid, dlsym_addr, parameters, 2, &regs);
ptrace(PTRACE_GETREGS,pid,NULL,&regs); //调用结束后获得r0中保存的返回值
long function_addr = regs.ARM_r0;

执行目标函数

先写段c程序编译为so文件:

1
2
3
4
5
6
#include<stdio.h>
void hello(char *str)
{
printf("hello %s\n",str);
}

则:

1
2
3
writeData(pid, mapping_base, function_parameters, strlen(function_parameters)+1);
parameters[0] = mapping_base;
ptrace_call(pid, function_addr, parameters, 1, &regs);

调用dlclose卸载so库

原型:

1
int dlclose(void *handle);

则:

1
2
parameters[0] = handle;
ptrace_call(pid,dlclose_addr,parameters,1,&regs);

恢复寄存器的状态

1
ptrace(PTRACE_SETREGS,pid,NULL,&old_regs);

Reference

安卓动态调试七种武器之离别钩 – Hooking

Android中的so注入(inject)和挂钩(hook) - For both x86 and arm

Android注入完全剖析

http://segmentfault.com/a/1190000000606904

Android下so注入汇总