打造一款具有创造力且高度可复用的 android 自动化测试工具 (qq 群 :608824162)
主页入口 请点我
https://github.com/zhangzhao4444/XMonkey
前提 Enable Injection(启动注入)
*Provider mock: 对 provider 的 query 接口进行 hook,根据传入的 uri 类型进行对应的伪造以及 null。如 image、audio
findAndHookMethod("android.content.ContentResolver", loader, "query",Uri::class.java,..,object : XC_MethodHook() {
@Throws(Throwable::class)
override fun afterHookedMethod(param: XC_MethodHook.MethodHookParam?) {
val uri = param.args[0] as Uri
val cursor = param.result as Cursor
ContentsProcessor.solve(context, uri, cursor)
}
})
*Json fuzz:对 HttpURLConnectionImpl 及 okhttp3 的 OkHttpClient 进行 hook,对服务端返回做延迟、非 json、空 json、null、xss 等 fuzz(支持 https fuzz)
......
if (Build.VERSION.SDK_INT >=19){
findAndHookMethod("com.android.okhttp.internal.http.HttpURLConnectionImpl", loader, "getOutputStream", hook)
findAndHookMethod("com.android.okhttp.internal.http.HttpURLConnectionImpl", loader, "getInputStream", hook)
}else{
findAndHookMethod("libcore.net.http.HttpURLConnectionImpl", loader, "getOutputStream", RequestHook)
}
val hook= object : XC_MethodHook() {
@Throws(Throwable::class)
override fun afterHookedMethod(param: XC_MethodHook.MethodHookParam?) {
......
val code = urlConn.responseCode
if (code == 200) {
val body = AppUtil.getBytesByInputStream(result)
oldrespose = AppUtil.getStringByBytes(body)
fakerespose = FuzzingDroid.getResposedValue()
var fakeresult:InputStream? = null
when(fakerespose){
"" -> fakeresult = AppUtil.strToInputSteam("")
"{}" -> fakeresult = AppUtil.strToInputSteam("{}")
"1=1" -> fakeresult = AppUtil.strToInputSteam("{1=1}")
......
}
上面例子 app 对 json 返回容错不严谨造成了 app crash
logcat 可看 mock 情况,红色命中 mock,绿色未命中
*Text fuzz
对界面中控件遍历,对 edit 的进行 fuzz 填充,如非法字符、超长、空 string、null、xss 等
fun fillTextFuzz(context: Context, views: List<View>) {
for (i in views.indices) {
val et = views[i] as EditText
val value = InjectorManager.mockInput(et.inputType)
et.setText(value)
}
}
logcat 可看 mock 情况
*Gps Mock(注意与 Json Mock 互斥)
对 gps 相关模块进行 hook 并返回伪造的 location
override fun hook(loader: ClassLoader) {
findAndHookMethod("android.telephony.TelephonyManager", loader, "getCellLocation", object : XC_MethodHook() {
@Throws(Throwable::class)
override fun afterHookedMethod(param: XC_MethodHook.MethodHookParam?) {
val gsmCellLocation = GsmCellLocation()
gsmCellLocation.setLacAndCid(lac, cid)
param?.result = gsmCellLocation
}
})
* 静态 Intent Fuzz(注意与动态 Intent Fuzz 互斥)
start test 后对所有 exported 的组件遍历进行 intent fuzz 攻击,如 null、非法序列化 Intent 等
静态 Intent fuzz 过程中,无需手工或 monkey 操作,等待 弹出 fuzz over 提示时表示完成测试。(测试过程中可能会引起多次崩溃)
activities?.forEach {
if (it.exported){
Logger.notice(context,"start activity {${it.name}} with Null Intent" )
val intent = Intent()
intent.component = ComponentName(packageName, it.name)
context.startActivity(intent)
Thread.sleep(8 * 1000)
}
}
* 动态 Intent Fuzz(注意与静态 Intent Fuzz 互斥)
运行时 hook Intent、bundle 组件相关接口,实现运行过程中劫持并畸变 intent,如空、非法、null、空数组、非法序列等
findAndHookMethod(bundle, loader, "getBoolean",String::class.java, Boolean::class.javaPrimitiveType, object : XC_MethodHook() {
@Throws(Throwable::class)
override fun afterHookedMethod(param: MethodHookParam?) {
val value = FuzzingDroid.getBooleanValue()
Logger.log("Bundle:Mock key-boolean: ${param!!.args[0]}-{ new=$value old=${param.result} }")
param.result = value
}
})
findAndHookMethod(intent, loader, "getData", object : XC_MethodHook() {
@Throws(Throwable::class)
override fun afterHookedMethod(param: MethodHookParam?) {
val value = FuzzingDroid.getDataValue()
Logger.log("Intent:Mock data-uri: data-{ new=$value old=${param?.result} }")
param?.result = value
}
})
该攻击畸变程度较高,app 可能出现大量崩溃,建议单独使用。
Hook 的方法很容易,但难点是从一系列路径中寻找到一个合适 Hook 的点。
a. Monkey 如何跨进程注入
XMonkey 中事件注入是通过调用 Instrumentation.sendPointerSync 来实现的。
但是当调 Instrumentation.sendPointerSync 执行跨进程点击时,会报如下错误:
由错误可知:Premission denied,inject event from a to b。权限拒绝了
(https://www.cnblogs.com/slgkaifa/p/6727510.html)
如何解决?初步思路是 hook 某个关键点将 uid a 篡改为 b 以此绕过权限检查。
重点是这个 nativeInjectEvent,如果返回 INPUT_EVENT_INJECTION_SUCCEEDED 则注入成功,继续跟进这个 native func
最终执行 hasInjectionPermission 来进行权限检查,并当 uid==0 也就是 root 时返回 policyFlags |= POLICY_FLAG_TRUSTED,从而后续进一步完成事件注入。
结论方案:选取这个关键的 native func 入口作为 hook 点 伪造 uid 为 0,从而绕过系统的权限检查
findAndHookMethod("com.android.server.input.InputManagerService", loader, "nativeInjectInputEvent",
Int::class.javaPrimitiveType, android.view.InputEvent ::class.java, Int::class.javaPrimitiveType, Int::class.javaPrimitiveType, Int::class.javaPrimitiveType, Int::class.javaPrimitiveType, Int::class.javaPrimitiveType, object : XC_MethodHook(){
@Throws(Throwable::class)
override fun beforeHookedMethod(param: XC_MethodHook.MethodHookParam?) {
if (param == null) return
if("${param.args[3]}" == UID) param.args[3] = 0
}
})
注:该 uid 为被测 app 的 id,且这个 hook 需要重启手机生效,所以测试时选取完被测 app 后需要重启一次手机来完成跨进程注入。另注不同 framework 对应的这个 nativeInjectInputEvent func 不同。
b. 屏蔽系统弹窗
为何需要屏蔽弹窗?出于以下几点原因:
val hook = object :XC_MethodReplacement(){
@Throws(Throwable::class)
override fun replaceHookedMethod(param: MethodHookParam): Any? {
return null
}
}
hookAllMethods(Dialog::class.java, "show", hook)
......
注: 将 show func 替换为空实现,凡是 Dialog 的子类均会受此影响。此 hook 也需重启手机生效,故更改 “屏蔽系统弹窗” 选项后需重启。另注:该屏蔽同样会影响安装包提示及权限提示。
c. 崩溃捕获
上图是大致的崩溃处理流程,左侧是 app 端处理崩溃,右侧则是系统端。由此可以得出存在两种 Hook 方案:
// UncaughtExceptionHandler
findAndHookMethod(classHandler, "uncaughtException", Thread::class.java, Throwable::class.java, object : XC_MethodHook() {
@Throws(Throwable::class)
override fun beforeHookedMethod(param: MethodHookParam) {
.....
}
})
// Handle Crash
findAndHookMethod("com.android.server.am.ActivityManagerService", loader, "handleApplicationCrash", IBinder::class.java, ApplicationErrorReport.CrashInfo::class.java, object : XC_MethodHook() {
@Throws(Throwable::class)
override fun beforeHookedMethod(param: XC_MethodHook.MethodHookParam) {
......
}
})
将以上两种方案均实现后调试,都可以正常 hook 到 app 的崩溃,但因为屏蔽了系统崩溃弹窗,在系统处理流程上存在等待该弹窗用户反馈的一个超时时延(大于 10 秒),显然这是无法接受的。如何处理?
最初的思路是以下 3 种,权衡之后选了 3:
internal class myActivityController: IActivityController.Stub() {
@Throws(RemoteException::class)
override fun appCrashed(processName: String, pid: Int, shortMsg: String, longMsg: String, timeMillis: Long, stackTrace: String): Boolean {
......
}
...
}
fun setActivityController(activityController: IActivityController?) {
try {
......
val amClass = Class.forName("android.app.ActivityManagerNative")
val getDefault = amClass.getMethod("getDefault")
am = getDefault.invoke(null)
attemptSetController(am, activityController)
} catch (e: Throwable) {
}
}
@Throws(Throwable::class)
fun attemptSetController(am: Any, activityController: IActivityController?) {
var setMethod: Method
setMethod = am.javaClass.getMethod("setActivityController", IActivityController::class.java)
setMethod.invoke(am, activityController)
}
用反射的方式进行 set。 最后一点 因为做这个 set 需要 root 权限,故我们需要 hook checkpermission 来绕过
findAndHookMethod("android.app.ActivityManager", loader, "checkComponentPermission", String::class.java, ..., object : XC_MethodHook() {
@Throws(Throwable::class)
override fun afterHookedMethod(param: MethodHookParam?) {
if (android.Manifest.permission.SET_ACTIVITY_WATCHER == param!!.args[0]) {
val re = param.result as Int
param.setResult(0)
}
}
当 permission 为 setActivityController 时 篡改 uid 为 root 从而绕过 checkpermission
d. 其他诸多难点,篇幅原因略去
原生 6.0 经过上千次调试,测试通过!
(android 7 测试通过,暂时 android 8.0 以上有 bug,正在修改)
支持 virtualxposed (仅部分 hook 测试通过,暂时仅支持 monkey、text fuzz、静态 intent fuzz)