|
| 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, ®s) < 0) { |
| 93 | + LOGD("getregs error"); |
| 94 | + return -1; |
| 95 | + } |
| 96 | + memcpy(&original_regs, ®s, 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, ®s) < 0) { |
| 175 | + LOGD("call target mmap error"); |
| 176 | + return -1; |
| 177 | + } |
| 178 | + //得到mmap分配的内存地址 |
| 179 | + uint8_t *target_mmap_base = ptrace_retval(®s); |
| 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 *)¶ms[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(®s); |
| 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,®s) < 0){ |
| 285 | + LOGD("call target dlopen error"); |
| 286 | + return -1; |
| 287 | + } |
| 288 | + //取返回结果 |
| 289 | + void * target_so_handle = ptrace_retval(®s); |
| 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,®s) < 0) { |
| 314 | + LOGD("call target dlsym error"); |
| 315 | + return -1; |
| 316 | + } |
| 317 | + |
| 318 | + void * hook_func_addr = ptrace_retval(®s); |
| 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, ®s) < 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,®s) < -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