关于安卓注入几种方式的讨论,开源注入模块实现
概念
在android系统中,进程之间是相互隔离的,两个进程之间是没办法直接跨进程访问其他进程的空间信息的。那么在android平台中要对某个app进程进行内存操作,并获取目标进程的地址空间内信息或者修改目标进程的地址空间内的私有信息,就需要涉及到注入技术。
通过注入技术可以将指定so模块或代码注入到目标进程中,只要注入成功后,就可以进行访问和篡改目标进程空间内的信息,包括数据和代码。
写这篇文章的初衷:
在我们进行算法还原再或者进行APP的RPC算法调用的时候,都有对APP注入的需求。
只不过目前的工具比较成熟,大家都忽略了注入的这个过程。
随着厂商对常见注入工具Frida、Xposed的特征检测,注入后APP会发生崩溃不可运行的问题。
众所周知:游戏安全对抗领域往往要比常见的应用安全领先多个领域,当然很多大厂也开始上了策略检测注入,但更多的只是风控策略,不会发生闪退(因为不同的厂商对于系统多少有些修改,万一有些系统会注入辅助so,那么会造成很大的误伤)
应用场景:
例如BB企业版、爱加密企业版、360企业版都对frida、xposed等工具进行检测,那么我们就可以手动注入dobby hook 以及支持Java的一些sandhook 来辅助分析,当然分析效率没有frida高,但是不会触发闪退检测策略。(当然本工具后期有打算进一步开发隐藏注入,这对游戏安全是小儿科,但是应用安全隐藏的话效果还是很可观的)
所以本文章首先讨论多种注入方式,并给出开源的面具模块供大家编译使用,注入自己开发的so,或者是调用成品库,进行hook以及高性能的RPC。
本文会罗列出几个常见的注入技术,以及列出使用该原理的工具,并重点讲一下zygote注入的模块开发。
我会详细讲解我比较熟悉的两种注入方式(修改aosp、zygisk),以及简单带过一些可能的注入方式,并后续补充注入材料。
常见的注入方式:
静态注入(重打包,需要过签名检测)
静态注入,静态解析ELF文件,增加一个依赖SO,或新增一个section节(注入代码在section字段),代码节是自己的注入代码,然后修复ELF文件结构。
修改dex,增加静态dex段,system.load 加载自己的so
实现案例:平头哥,一些虚拟xposed框架
| 1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
这种方式的优点:
免root、便于分发、打包速度一般
缺点:
对于签名检测的pass难度比较高
动态注入(基于系统提供的调试API注入)
ptrace注入
**由于Android是基于linux内核的操作系统,所以Android下的注入也是基于Linux下的系统调用函数ptrace()实现的。**即在获得root权限后,通过ptrace()系统调用将stub(桩代码)注入到指定pid的进程中。
常见使用工具:IDA、GDB、LLDB、Frida等常见工具
我们也可以自己写一个ptrace简单的注入so,下面我给出一个项目,感兴趣的大佬可以自己编译进行尝试。
这里进行预告:后面我会自己写一个调试器(基于ptrace),会写出文章进行分享,目前已经在做了。
这里简单附上几篇ptrace的文章,感兴趣的大佬可以尝试。
因为我研究的实在是不多。
Android中的so注入(inject)和挂钩(hook) - For both x86 and arm_安卓hook注入有什么危害-CSDN博客
这种方式的优点:
注入速度快,注入不容易检测到(ptrace注入完成以后直接取消ptrace,在后面检测不到)
缺点:
需要root、有一定的ptrace检测(像ida这样的注入,会在maps扫描到当前正在被调试)
attach方式被ptrace占坑方式搞得不好绕过(ida表示非常难受)。
zygote注入
常见使用工具:xposed 实现工具:Riru(早期)、Zygisk(常用)
zygote注入是属于全局注入的方式,它主要是依赖于fork()子进程方式进行注入的。
目前市面上比较成熟的注入工具xposed就是基于zygote的全局注入。
它有两大优点:主要在于zygote是系统进程,通过系统进程fork出来后它就具备隐蔽性,强大性。
常见的一些工具都是使用Zygisk注入,比如知名的开源项目Zygisk-Il2CppDumper
以及寒冰大佬开发的FrdiaManager 还有Xposed框架都支持Zygsik注入
下面我来讲一下我开发的模块是如何注入自己的so的(本模块是基于**Zygisk-Il2CppDumper项目进行修改,因为作者写的Gradle实在是太好用啦)**
系统注入(修改AOSP源码,进行插桩)
通过修改aosp系统的源码,在app加载之前插桩语句,加载自定义库。
后面会有一个小模块进行讨论。
Zygisk自定义注入so(dex)插件的实现
模块开发前置知识
| 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 |
|
重点就是这几个api, 看注释理解
用最通俗粗略的理解来表示的话:
pre是刚从zygote fork出来没有沙箱限制的时候
postAppSpecialize 相当于app进程启动, 这里可以做自定义dex加载的一些动作
postServerSpecialize 相当于系统服务也就是system server 运行
官方提供了一个https://github.com/topjohnwu/zygisk-module-sample案例,也可以读一读
模块实现细节:
实现原理非常简单:
从app可以访问的路径copy要注入的so到自己的私有目录(因为有selinux的限制)
之后使用dl_open加载目标so
| 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 |
|
这里主要实现了面具模块主要提供的api
| 1 2 3 4 5 6 7 8 |
|
我们的实现主要是这个实现的函数,此时app已经处于沙盒中了,只有app自身的权限
| 1 2 3 4 5 |
|
在这里我们需要修改要注入的包名,不然模块不会进一步注入
主要功能实现
| 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 |
|
复制过程,主要就是把/data/local/tmp/test.so 复制到私有目录,然后修改权限为0755 不然dlopen没法加载
| 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 |
|
尝试打开十次,获取到so的handle
| 1 2 3 4 5 6 7 8 9 10 11 12 |
|
寻找符号并执行
| 1 2 3 4 5 6 7 8 |
|
可以自己写一个自己的函数在用dlsym调用,这里就不多说了
源码导入android studio就可以构建出面具模块了,再次感谢原作者的项目
使用方法:
在编译之前应该修改目标app的包名,如果不修改不会注入(后面会考虑做一个和shamiko一样的黑白名单)初代版本大家先手动修改
编译自己的插件so实现自己的功能
这里需要了解的是dlopen的加载流程。见番外篇。
当然可以修改插件,使用dlsym找到自己函数的符号,手动加载。
我已经实现了JNI_ONLOAD的加载。
移动so到/data/local/tmp目录下 命名为test.so
享受注入!
插件so源码:
| 1 2 3 4 5 |
|
注入效果:
番外一: dlopen的简单解析
libdl.cpp - OpenGrok cross reference for /bionic/libdl/libdl.cpp
之后调用

来到真正的dlopen加载的地方
| 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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
|
这里就是对so的各个段的装载,我们目光聚焦于结尾的部分
在这个函数里有:
| 1 2 3 4 |
|
对DT_INIT和DT_INIT_ARRAY的调用
所以我们dlopen如果成功打开了so,就会对这两个地方调用
所以说插件的入口可以选择在这两个段里 attribute((constructor))
| 1 2 3 4 5 |
|
番外二:定植AOSP进行so的注入
这里我通过修改源码去注入so,so注入的时机我开始的选择是越早越好。
这里选在在handleBindApplication处,创建ContextImpl对象时进行一系列的复制注入操作。
我们流程选择先将需要注入的so放到sd卡目录下,然后判断app为非系统app时进行复制到app目录,注入app等一系列操作。 我们找到源码,目录AOSP/frameworks/base/core/java/android/app/ActivityThread.java,
找到handleBindApplication,定位到”final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);”这一行。
开始加入我们自己的代码:
和上面的实现一样,就是copyso 然后使用system.load即可加载
网上有很多实现,还可以自定义selinux标签,配合系统服务和配套app达到自定义注入
TODO:实战:使用自己写的hook工具分析强检测frida的APP
如果文章反响还不错,我会继续更新一些frida、xposed分析不了的app(被反调试block掉的)
来进一步加深大家对这个框架的使用。
未来的框架的更新方向:
增加第二种注入方式:将插件so打包到框架里,隐藏落地文件的特征。
通过借鉴Riru的注入方式,隐藏注入(对一些厂商管用)
进一步研究完美隐藏方式
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)