本文是对大犇蒸米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*)¶ms[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(®s, &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,®s);
//恢复寄存器
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(®s,&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,®s);
//调用结束后获得r0中保存的返回值
ptrace(PTRACE_GETREGS,pid,NULL,®s);
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, ®s);
ptrace(PTRACE_GETREGS,pid,NULL,®s); //调用结束后获得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, ®s);
ptrace(PTRACE_GETREGS,pid,NULL,®s); //调用结束后获得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, ®s);
调用dlclose卸载so库 原型:
1
int dlclose(void *handle);
则:
1
2
parameters[0] = handle;
ptrace_call(pid,dlclose_addr,parameters,1,®s);
恢复寄存器的状态 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注入汇总