自动化工具 基于 Xposed Hook 技术,实现 Mock 及 Fuzzing 自动化容错测试工具,代号 XMonkey

zhangzhao_lenovo · 2018年06月25日 · 最后由 zhangzhao_lenovo 回复于 2019年10月25日 · 10568 次阅读
本帖已被设为精华帖!

打造一款具有创造力且高度可复用的 android 自动化测试工具 (qq 群 :608824162)

主页入口 请点我
https://github.com/zhangzhao4444/XMonkey

优势

  1. UI 自动化与 Fuzz 结合,边遍历边测容错!
  2. 支持多种 Fuzz,如 Intent fuzz、Json fuzz、text fuzz、provider mock、gps mock 等
  3. 多个黑科技,如防跳出、崩溃捕获、弹窗屏蔽、权限绕过、ssl 绕过等

如何使用

  1. 安装 Xposed Installer(见#1 楼
    http://repo.xposed.info/module/de.robv.android.xposed.installer
  2. 安装 Xmonkey,在 xposed 中激活 xmonkey,然后重启手机 
  3. 启动 Xmonkey,配置策略,选择待测 app,start test,悬浮窗点击开始跑 monkey(亦可不跑 monkey 手动触发 fuzz)
  4. monkey 过程中点开悬浮窗即暂停 monkey,回到 Xmonkey 可查看 activity 覆盖率、崩溃堆栈

各种 Fuzz

前提 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. 屏蔽系统弹窗
为何需要屏蔽弹窗?出于以下几点原因:

  1. monkey 过程中减少系统干扰,以防跳出待测 app,如系统更新升级、键盘选择、权限提示等。
  2. app 崩溃或 anr 时并不希望弹出窗干扰。
    如何做:
    以 app anr 系统弹 无响应窗为例,crashApplication 中给 mUihandler 发 msg

    new 一个 appnotresponedingdialog

    经过一系列跟进找到该 appnotresponedingdialog 实际是 dialog 的子类,所以我们的思路是 hook dialog,重写 show func
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 方案:

  1. hook App 端,UncaughtExceptionHandler 的 uncaughtException
  2. hook 系统端,crashApplication
// 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:

  1. 修正弹窗 show 的 hook,改为默认自动选择确定(考虑默认确定的话也会影响其他非崩溃的弹窗,未知情况较多故舍弃)
  2. 仅完成 app 端崩溃处理,然后做截断,阻止其系统崩溃处理(担心会影响 app 自身的崩溃流程,没有详细研究)
  3. 系统崩溃处理链上寻找如何绕过等待超时
    跟进下 crashApplication 的弹窗以及超时逻辑 其中调用 mUihander.sendMessage 来创建崩溃弹窗,并阻塞等待用户选择 但往上看,如果存在 activityController 时,可以将崩溃流程引入 activityController,从而绕过这个崩溃弹窗。于是我们现在需要做的是构建一个 activityController,并把它 set 到 ActivityManager 中。
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. 其他诸多难点,篇幅原因略去

todo

  1. 支持非 root
  2. okhttp3

其他

原生 6.0 经过上千次调试,测试通过!

(android 7 测试通过,暂时 android 8.0 以上有 bug,正在修改)

支持 virtualxposed (仅部分 hook 测试通过,暂时仅支持 monkey、text fuzz、静态 intent fuzz)

update

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 38 条回复 时间 点赞

关于 刷 xposed

建议用 google 原生机,比如 nexus 系列。其他厂商不建议刷机容易砖

  1. 刷 Android 原生 6.0
    下载 img https://developers.google.com/android/images#bullhead
    我这里是 6.0.1 (MTC20K) Link

    4a950470af6c1e0111cfa8efbd77422928b88d01800dd2fadc6f8eeeae1b97a9
    进 fastboot,执行 flash-all.bat

  2. 刷 twrp
    下载对应的 twrp.img
    进 fastboot,执行 fastboot flash recovery twrp.img
    刷入成功后,可以在 fastboot 状态下,选择” recovery mode” 来进入 twrp

  3. 刷 xposed framework
    下载对应版本的 fw https://forum.xda-developers.com/showthread.php?t=3034811
    我这里是 xposed-v89-sdk23-arm64.zip
    adb push xposed-v89-sdk23-arm64.zip /sdcard
    点击 twrp 上 install 选该 zip 包,刷
    重启等待进系统

  4. 进系统安装 xposed installer
    XposedInstaller_3.1.5.apk

  5. 刷 supersu
    下对应的 su http://www.supersu.com/download
    我这里是 SuperSU-v2.82-201705271822.zip
    adb push SuperSU-v2.82-201705271822.zip /sdcard
    点击 twrp 上 install 选该 zip 包,刷
    重启等待进系统

你终于又出手了.江湖又是一场腥风血雨😆

楼主想法很不错,想问下目前使用效果如何?
相对你们之前之前的一些方案效果提升多少呢?

testly 回复

目前是 动态 Intent 、Json 的 Fuzz 造成的 App 崩溃的情况比较多。之前容错有一些半自动化的工具,这次的想法是结合全自动跑,但 App 崩溃较多可能入口就挂了。

作为看客,对楼主的精神、实践能力跟精神佩服。

思寒_seveniruby 将本帖设为了精华贴 06月25日 12:34
思寒_seveniruby 将本帖设为了精华贴 06月25日 12:34

思路超赞,我之前是用代理和自动化完成这个效果的,你这个方法独辟蹊径,非常不错,扩展性也很强。

厉害,只是做过简单的 Xposed hook 技术篡改服务端的响应消息,但没有你这么灵活而且还是自动化测试!

普通手机装得上吗 xposed installer

装 xposed 需要 root,建议在原生机上刷 root

还需要 root 还需要 xposed 条件多了点 不便捷啊。 VirtualXposed 不需要 root 吧 可以考虑支持这个。

老马 回复

virtualxposed 的正在弄了。😎

楼主原创的 idea 很不错,但是个人觉得方案侵入性太强、跨平台也存在潜在问题。

乾行 回复

跨平台? 已对几个版本做了兼容。 且这种 App 容错的 6.0 上和 7、8 差异不大,找准一个系统跑就行

期待楼主 virtualxposed 的版本,膜拜楼主

2018.6.26 update
1.修正 微信 pm.getPackageInfo 异常,对于微信 getActivitys 返回 null
https://hk.saowen.com/a/e5aea65017573d8c103f8822745c663c876fcebbca44e91851888508502d8dc7
2.修正 intent fuzz ,startService 时缺少对应 permission 异常

这回是真的不明觉厉了- - 。。。。

牛逼

zhangzhao 大神一出手就是精品。
也非常期待大神能一起分享一下这个工具的产出效果如何。

惭愧,实不敢当!大神们都去搞 AI 了。

2018.7.4 update

  1. 兼容 android 7
  2. 支持 virtualxposed (仅部分 hook 测试通过,暂时仅支持 monkey、text fuzz、静态 intent fuzz)
  3. SharedPreferences 重构为多进程间共享
仅楼主可见
26楼 已删除

用的不是 java 语言写的吧?

圈圈 回复

不是 java

@ 楼主 可以使用 virtualxposed 不需要刷机

king.yu 回复

virtualxposed 并非是 xposed ,实际上研究完略失望

需要 root?安装在 android 7.1.1 的 oppo 设备上,闪退,无法使用呢。

Smile小馨888 回复

是的 需要 root

装了 xposed,在下载里面搜了 xmonkey,都没有可下载的模块啊,设定里面更改了版本,只看到一个叫 1 的模块,下载不了,且提示不是有效的 apk,这有能用吗???? 有那位大哥用了的,麻烦告知下怎么解决,注意:其他的模块我下载是可以的

jierong01 回复

刚看到 你应该是下错地方了。 xmonkey 的安装包在 github 上。

simple 专栏文章:[精华帖] 社区历年精华帖分类归总 中提及了此贴 12月13日 14:44

xposed 和 root 都有了,还是闪退,7.1.1

hodor592 回复

加我 qq

申请加群了

simple [精彩盘点] TesterHome 社区 2018 年 度精华帖 中提及了此贴 01月07日 12:08

楼主是不是可以把 Xposed 更新成 太极框架了,无需 root 也可以运行。

https://github.com/taichi-framework/TaiChi/wiki/taichi-magisk-zh

在路上 回复

我调研一下,不过初步看上去本质就是 virtualxposed

LZ, 这个一定要刷机的吗

阁主夫人 回复

是的

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册