Skip to content

Commit f4e16fc

Browse files
committed
Create README.md
1 parent 855fd75 commit f4e16fc

File tree

1 file changed

+387
-0
lines changed

1 file changed

+387
-0
lines changed

README.md

+387
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
1+
#<center>Android进程注入</center>
2+
##概述
3+
4+
我们平时所说的代码注入,主要静态和动态两种方式:<br>
5+
静态注入,针对是可执行文件,比如修改ELF,DEX文件等,相关的辅助工具也很多,比如IDA、ApkTool等;<br>
6+
动态注入,也可以叫进程注入,针对是进程,比如修改进程的寄存器、内存值等;<br>
7+
动态跟静态最大的区别是,动态不需要改动源文件,但需要高权限(通常是root权限),而且所需的技术含量更高。
8+
##基本思路
9+
关键点在于让目标进加载自定义的动态库so,当so被加载后,so就可以加载其他模块、dex文件等,具体的注入过程大致如下:<br>
10+
<br>
11+
1) attach上目标进程;<br>
12+
2) 让目标进程的执行流程跳转到mmap函数来分配内存空间;<br>
13+
3) 加载注入so;<br>
14+
4) 最后让目标进程的执行流程跳转到注入的代码执行。<br>
15+
16+
后面会更详细地分析注入过程。
17+
18+
##示例演示
19+
我们准备了2个程序:<br>
20+
<img src="icon.jpg" width="220" />
21+
<br>
22+
Target作为目标程序,Inject注入程序。
23+
###目标程序
24+
<img src="target_app.jpg" width="200" />
25+
<br>
26+
在注入前数字每次增加1,点击启动inject启动注入程序。
27+
####注入程序
28+
<img src="inject_app_new.jpg" width="200" />
29+
<br>
30+
点击inject浮窗按钮,开始执行注入,可以看到数字每次增加2。<br>
31+
查看log日志:<br>
32+
<img src="log.jpg" width="400" />
33+
<br>
34+
<br>
35+
##注入程序分析
36+
###注入代码分析
37+
int hook_entry(char * a) {
38+
LOGD("Hook success, pid = %d\n", getpid());
39+
LOGD("Hello %s\n", a);
40+
void *handle = dlopen(target_path, 2);
41+
if (handle == NULL) {
42+
LOGD("open target so error!\n");
43+
return -1;
44+
}
45+
void *symbol = dlsym(handle, "set_step");
46+
if (symbol == NULL) {
47+
LOGD("get set_step error!\n");
48+
return -1;
49+
}
50+
_set_step = symbol;
51+
LOGD("_set_step addr :%x\n", _set_step);
52+
_set_step(2);
53+
return 0;
54+
}
55+
56+
这段代码就是我们想在目标进程里面执行的代码,代码很简单,做了2件事情:<br>
57+
1.打印调用传参字符串;<br>
58+
2.调用目标进程的set_step函数,让每次增加的数为2。
59+
<br>
60+
编译注入代码为动态库libhooker.so。
61+
62+
<br>
63+
64+
###注入过程详解
65+
66+
我们的inject代码必须运行在root进程,
67+
68+
69+
StringBuffer sb = new StringBuffer();
70+
sb.append("su -c");
71+
sb.append(" ").append(injectPath);//注入程序
72+
sb.append(" ").append("com.ry.target");//目标进程名称
73+
sb.append(" ").append(hookerPath);//注入代码so
74+
sb.append(" ").append("hook_entry");//注入代码入口函数
75+
sb.append(" ").append("hahaha");//注入代码入口函数参数
76+
通过"su -c "启动一个root进程来执行。<br>
77+
下面开始分析具体注入过程。
78+
79+
80+
81+
####一.attach到目标进程
82+
83+
//1.attach
84+
if(ptrace_attach(target_pid) < 0) {
85+
LOGD("attach error");
86+
return -1;
87+
}
88+
89+
####二.获取目标进程寄存器,并复制一份保存,以便在注入完成后恢复目标进程
90+
91+
struct pt_regs regs, original_regs;
92+
if (ptrace_getregs(target_pid, &regs) < 0) {
93+
LOGD("getregs error");
94+
return -1;
95+
}
96+
memcpy(&original_regs, &regs, sizeof(regs));
97+
###三.取目标进程mmap函数地址
98+
99+
void *target_mmap_addr = get_remote_func_address(target_pid, libc_path, (void *) mmap);
100+
LOGD("target mmap address: %x\n", target_mmap_addr);
101+
get\_remote\_func\_address函数位于proccess_util.c中:<br>
102+
103+
/**
104+
* 获取目标进程中函数地址
105+
* */
106+
void* get_remote_func_address(pid_t target_pid, const char* module_name,void* local_addr) {
107+
void* local_handle, *remote_handle;
108+
local_handle = get_lib_adress(-1, module_name);
109+
remote_handle = get_lib_adress(target_pid, module_name);
110+
111+
/*目标进程函数地址= 目标进程lib库地址 + (本进程函数地址 -本进程lib库地址)*/
112+
void * ret_addr = (void *) ((uint32_t) remote_handle + (uint32_t) local_addr - (uint32_t) local_handle);
113+
return ret_addr;
114+
}
115+
为了在目标进程中调用mmap函数,需要得到mmap函数在目标进程中的地址。<br>
116+
一个模块库里的函数地址等于模块库的装载地址加上一个偏移量,所以:<br>
117+
目标进程函数地址= 目标进程lib库地址 + (本进程函数地址 -本进程lib库地址)<br>
118+
mmap函数在/system/lib/libc.so库里面,所以(void*)mmap可以取得inject本身进程的mmap函数的地址,这样其实只要得到动态库的装载地址就能算出目标进程的mmap的地址。一种得到动态库装载地址的方法是分析Linux进程的/proc/pid/maps文件,这个文件包含了进程中所有mmap映射的地址。下面我们写一个获取动态库地址的函数,代码如下
119+
120+
/**
121+
* 获取动态库装载地址
122+
* */
123+
void* get_lib_adress(pid_t pid, const char* module_name) {
124+
FILE *fp;
125+
long addr = 0;
126+
char *pch;
127+
char filename[32];
128+
char line[1024];
129+
130+
if (pid < 0) {
131+
/* self process */
132+
snprintf(filename, sizeof(filename), "/proc/self/maps");
133+
} else {
134+
snprintf(filename, sizeof(filename), "/proc/%d/maps", pid);
135+
}
136+
137+
fp = fopen(filename, "r");
138+
139+
if (fp != NULL) {
140+
while (fgets(line, sizeof(line), fp)) {
141+
//在所有的映射行中寻找目标动态库所在的行
142+
if (strstr(line, module_name)) {
143+
pch = strtok(line, "-");
144+
addr = strtoul(pch, NULL, 16);
145+
146+
if (addr == 0x8000)
147+
addr = 0;
148+
149+
break;
150+
}
151+
}
152+
153+
fclose(fp);
154+
}
155+
156+
return (void *) addr;
157+
}
158+
159+
此函数的功能就是通过遍历/proc/pid/maps文件,来找到目的module_name的内存映射起始地址。
160+
由于内存地址的表达方式是xxxxxxx-xxxxxxx的,所以会在后面使用strtok(line,"-")来分割字符串
161+
如果pid = -1,表示获取本地进程的某个模块的地址,
162+
否则就是pid进程的某个模块的地址。
163+
164+
####四.调用目标进程mmap函数分配一块内存
165+
166+
long parameters[6];
167+
parameters[0] = 0; // addr
168+
parameters[1] = 0x400; // size
169+
parameters[2] = PROT_READ | PROT_WRITE | PROT_EXEC; // prot
170+
parameters[3] = MAP_ANONYMOUS | MAP_PRIVATE; // flags
171+
parameters[4] = 0; //fd
172+
parameters[5] = 0; //offset
173+
174+
if (ptrace_call_wrapper(target_pid, "mmap", target_mmap_addr, parameters, 6, &regs) < 0) {
175+
LOGD("call target mmap error");
176+
return -1;
177+
}
178+
//得到mmap分配的内存地址
179+
uint8_t *target_mmap_base = ptrace_retval(&regs);
180+
LOGD("target_mmap_base: %x\n", target_mmap_base);
181+
182+
mmap函数原型:<br>
183+
184+
void *mmap(void *addr, size_t length, int prot, int flags,
185+
int fd, off_t offset);
186+
187+
准备好参数后,调用ptrace\_call\_wrapper函数:
188+
189+
190+
int ptrace_call_wrapper(pid_t target_pid, const char * func_name, void * func_addr, long * parameters, int param_num, struct pt_regs * regs) {
191+
LOGD("Calling [%s] in target process <%d> \n", func_name,target_pid);
192+
if (ptrace_call(target_pid, (uint32_t)func_addr, parameters, param_num, regs) < 0) {
193+
return -1;
194+
}
195+
196+
if (ptrace_getregs(target_pid, regs) < 0) {
197+
return -1;
198+
}
199+
return 0;
200+
}
201+
总结一下ptrace\_call\_wrapper,它完成两个功能:<br>
202+
一是调用ptrace\_call函数来执行指定函数;<br>
203+
二是调用ptrace\_getregs函数获取所有寄存器的值。
204+
<br>
205+
206+
下面来分析ptrace_call函数:
207+
208+
int ptrace_call(pid_t pid, uint32_t addr, const long *params, uint32_t num_params, struct pt_regs* regs){
209+
uint32_t i;
210+
//前面四个参数用寄存器传递
211+
for (i = 0; i < num_params && i < 4; i ++) {
212+
regs->uregs[i] = params[i];
213+
}
214+
215+
//后面参数放到栈里
216+
if (i < num_params) {
217+
regs->ARM_sp -= (num_params - i) * sizeof(long) ;
218+
ptrace_writedata(pid, (void *)regs->ARM_sp, (uint8_t *)&params[i], (num_params - i) * sizeof(long));
219+
}
220+
221+
//PC指向要执行的函数地址
222+
regs->ARM_pc = addr;
223+
224+
if (regs->ARM_pc & 1) {
225+
/* thumb */
226+
regs->ARM_pc &= (~1u);
227+
regs->ARM_cpsr |= CPSR_T_MASK;
228+
} else {
229+
/* arm */
230+
regs->ARM_cpsr &= ~CPSR_T_MASK;
231+
}
232+
233+
//把返回地址设为0,这样目标进程执行完返回时会出现地址错误,这样目标进程将被挂起,控制权会回到调试进程手中
234+
regs->ARM_lr = 0;
235+
236+
//设置目标进程的寄存器,让目标进程继续运行
237+
if (ptrace_setregs(pid, regs) == -1 || ptrace_continue(pid) == -1) {
238+
return -1;
239+
}
240+
//等待目标进程结束
241+
int stat = 0;
242+
waitpid(pid, &stat, WUNTRACED);
243+
while (stat != 0xb7f) {
244+
if (ptrace_continue(pid) == -1) {
245+
return -1;
246+
}
247+
waitpid(pid, &stat, WUNTRACED);
248+
}
249+
250+
return 0;
251+
}
252+
253+
254+
功能总结:<br>
255+
1,将要执行的指令参数写入寄存器中,个数大于4的话,需要将剩余的指令通过ptrace\_writedata函数写入栈中;<br>
256+
2,修改寄存器,PC指向要执行的函数,lr设置为0,调用ptrace\_setregs设置修改后的寄存器;<br>
257+
3,使用ptrace\_continue函数运行目的进程;<br>
258+
4,等待目标进程执行完毕;<br>
259+
260+
目标进程执行完成后,调用:
261+
262+
uint8_t *target_mmap_base = ptrace_retval(&regs);
263+
ptrace\_retval函数从寄存器里面取值,arm平台函数返回值是存放在ARM_r0的
264+
265+
long ptrace_retval(struct pt_regs * regs) {
266+
return regs->ARM_r0;
267+
}
268+
269+
这一步完成后,我们得到了在目标进程开辟的一块内存地址。target\_mmap\_base指向这块地址的首地址。<br>
270+
271+
####五.调用目标进程dlopen函数加载注入so
272+
273+
//取目标进程dlopen函数地址
274+
void *target_dlopen_addr = get_remote_func_address(target_pid, linker_path, (void *) dlopen);
275+
LOGD("target dlopen address: %x\n", target_dlopen_addr);
276+
277+
//把注入so地址写入目标进程
278+
ptrace_writedata(target_pid, target_mmap_base, library_path,strlen(library_path) + 1);
279+
280+
//准备参数
281+
parameters[0] = target_mmap_base;
282+
parameters[1] = RTLD_NOW | RTLD_GLOBAL;
283+
//通过ptrace调用
284+
if (ptrace_call_wrapper(target_pid, "dlopen", target_dlopen_addr, parameters, 2,&regs) < 0){
285+
LOGD("call target dlopen error");
286+
return -1;
287+
}
288+
//取返回结果
289+
void * target_so_handle = ptrace_retval(&regs);
290+
291+
dlopen函数原型:<br>
292+
293+
void *dlopen(const char *filename, int flag);
294+
295+
这个函数的作用就是把一个so库加载到进程空间中。<br>
296+
297+
我们要调用这个函数,就和上一步调用mmap差不多,首先取得dlopen函数的地址,然后准备好参数,通过ptrace\_call\_wrapper调用执行。<br>
298+
299+
重点在准备参数这里,我们要先把so地址这个字符串写到目标进程里面去,写到哪里呢?就是上一步我们调用mmap得到的target\_mmap\_base。<br>
300+
301+
302+
####六.调用dlsym取注入so库执行函数的地址
303+
304+
//取目标进程dlsym函数的地址
305+
void *target_dlsym_addr = get_remote_func_address(target_pid, linker_path, (void *) dlsym);
306+
LOGD("target dlsym address: %x\n", target_dlsym_addr);
307+
//把函数名称字符串写进目标进程
308+
ptrace_writedata(target_pid, target_mmap_base + FUNCTION_NAME_ADDR_OFFSET,function_name, strlen(function_name) + 1);
309+
310+
parameters[0] = target_so_handle;
311+
parameters[1] = target_mmap_base + FUNCTION_NAME_ADDR_OFFSET;
312+
313+
if (ptrace_call_wrapper(target_pid, "dlsym", target_dlsym_addr, parameters, 2,&regs) < 0) {
314+
LOGD("call target dlsym error");
315+
return -1;
316+
}
317+
318+
void * hook_func_addr = ptrace_retval(&regs);
319+
LOGD("target %s address: %x\n", function_name,target_dlsym_addr);
320+
321+
dlsym函数原型:<br>
322+
323+
void *dlsym(void *handle, const char *symbol);
324+
这个函数可以得到so库中函数的地址。<br>
325+
这一步和调用dlopen差不多,注意准备参数的时候要加上一段偏移量。<br>
326+
####七.调用注入函数
327+
328+
//写入函数需要的参数
329+
ptrace_writedata(target_pid, target_mmap_base + FUNCTION_PARAM_ADDR_OFFSET, param,strlen(param) + 1);
330+
parameters[0] = target_mmap_base + FUNCTION_PARAM_ADDR_OFFSET;
331+
332+
if (ptrace_call_wrapper(target_pid, function_name, hook_func_addr,parameters, 1, &regs) < 0) {
333+
LOGD("call target %s error",function_name);
334+
return -1;
335+
}
336+
337+
一切准备就绪,这一步执行注入入口函数。<br>
338+
####八.调用dlclose卸载注入so
339+
340+
void *target_dlclose_addr = get_remote_func_address(target_pid, linker_path, (void *) dlclose);
341+
parameters[0] = target_so_handle;
342+
343+
if (ptrace_call_wrapper(target_pid, "dlclose", target_dlclose_addr, parameters, 1,&regs) < -1) {
344+
LOGD("call target dlclose error");
345+
return -1;
346+
}
347+
348+
####九.恢复现场
349+
350+
ptrace_setregs(target_pid, &original_regs);
351+
####十.detach
352+
353+
ptrace_detach(target_pid);
354+
##总结
355+
本篇只是分析最基本的注入流程,这是最基本也是最重要的,只有进入到了目标进程才能做接下来的事情。
356+
##示例代码
357+
<https://github.com/yangbean9/injectDemo>
358+
359+
##参考资料
360+
<http://blog.csdn.net/jinzhuojun/article/details/9900105>
361+
<br>
362+
<http://blog.csdn.net/u013234805/article/details/24796515>
363+
364+
365+
366+
367+
368+
369+
370+
371+
372+
373+
374+
375+
376+
377+
378+
379+
380+
381+
382+
383+
384+
385+
386+
387+

0 commit comments

Comments
 (0)