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

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

打造一款具有创造力且高度可复用的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

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

关于 刷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很不错,但是个人觉得方案侵入性太强、跨平台也存在潜在问题。

qianxing 回复

跨平台? 已对几个版本做了兼容。 且这种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上。

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