p1s1lver 发表于 2023-4-7 18:40

Android 自动化分析之 Activity 对应 am 命令提取

编写 Android 自动化代码时,不想进行太多的 UI 操作,通常使用 start activity 的方式 (等价于 am 命令) 直接跳转到对应界面,如何快速获得这个 am 命令呢?

这里封装了 hook 逻辑和转换逻辑,本质上是对 Activity 对应的 Intent 对象进行了规则的提取和转换,这里我绘制了一张逻辑图,只需要 hook 之后点击界面即可获得这个界面的 am 启动命令。
有一种特殊情况,是界面本身通过 Fragment 实现而不是 Activity 实现,这种方式本身就不能通过 am 命令实现。



就是,只要结果输出了,那么就是可用的 am 命令。
记得删除其中的一些无用的东西,如带有时间戳信息的可能会被 onCreate 方法中的逻辑判断为过期,从而崩溃;
也可以替换其中的一些东西,比如一个 app 的搜索页,可以替换为默认关键词,从而可以节省一步 set_text





function intentToAmCommand(intent) {
    let amCommand = "am start"
    let action = intent.getAction()
    let categories = intent.getCategories()
    let type = intent.getType()
    let component = intent.getComponent()
    let flags = intent.getFlags()
    let extras = intent.getExtras()

    if (action !== null) {
      amCommand += ` -a ${action}`
    }
    if (categories && categories.length > 0) {
      categories.forEach(category => {
            amCommand += ` -c ${category}`
      })
    }
    if (type !== null) {
      amCommand += ` -t ${type}`
    }
    if (component !== null) {
      amCommand += ` -n ${component.getPackageName()}/${component.getClassName()}`
    }
    if (flags !== 0) {
      amCommand += ` -f ${flags}`
    }
    if (extras !== null) {
      amCommand += ` -e`
      let iter = extras.keySet().iterator()
      while (iter.hasNext()) {
            let key = iter.next()
            let value = extras.getString(key)
            amCommand += ` "${key}" "${value}"`
      }
    }
    return amCommand
}

function logIntentAm(intent) {
    // 打印对应 am 命令
    console.log(`[*] am command: ${intentToAmCommand(intent)}`)// 后续自行删改
}

function hookOnCreate() {
    let Activity = Java.use("android.app.Activity");
    console.log("[*] hooked Activity onCreate method");
    Activity.onCreate.overload('android.os.Bundle').implementation = function (arg1) {
      let theIntent = this.getIntent()
      logIntentAm(theIntent)
      return this.onCreate(arg1)
    };
}

setTimeout(() => {
    Java.perform(() => {
      console.log("----- FRIDA start mocking -----");
      try { hookOnCreate(); } catch (err) { console.error(err.stack); }
      console.log("----- FRIDA finish mocking ----");
    })
}, 2000)

p1s1lver 发表于 2023-4-8 11:32

本帖最后由 p1s1lver 于 2023-4-8 12:01 编辑

不少的自动化工具中会自己封装 startActivity 方法,这些方法相较于 am 命令,不会将大部分时间都放在虚拟机初始化上,性能更高,但是对应的 IntentFilter 机制的原理是没有变的,以如下代码为例:

构造一个 JavaScript 函数打印出构造的 python 语句字符串如打开开发者选项设置是 device.start_activity(action="android.settings.APPLICATION_DEVELOPMENT_SETTINGS")

这个 start_activity 方法的参数规则对应 am 命令的规则如下:
- action 参数对应于 am 命令中的 -a (action)
- category 参数对应 am 命令中的 -c (category)
- component 参数对应 am 命令中的 -n (component)
- extras 参数(dict) 对应 am 命令中的 -e 参数

现在在代码中也封装了对应的 python 语句的打印,这样就省去所有的步骤了,有就是能用,没有就是非 activity 启动,打印的代码及如何删除无用逻辑举例如下图:



代码如下:
function constructAmCommand(action, categories, component, extras) {
    let amCommand = "am start"
    if (action !== null) {
      amCommand += ` -a ${action}`
    }
    if (categories && categories.length > 0) {
      categories.forEach(category => {
            amCommand += ` -c ${category}`
      })
    }
    if (component !== null) {
      amCommand += ` -n ${component.getPackageName()}/${component.getClassName()}`
    }
    if (extras !== null) {
      amCommand += ` -e`
      let iter = extras.keySet().iterator()
      while (iter.hasNext()) {
            let key = iter.next()
            let value = extras.getString(key)
            amCommand += ` "${key}" "${value}"`
      }
    }
    return amCommand
}


function constructPythonStatement(action, categories, component, extras) {
    let params_dict = {}
    if (action !== null) {
      params_dict["action"] = action
    }
    if (categories && categories.length > 0) {
      params_dict["categories"] = categories
    }
    if (component !== null) {
      params_dict["component"] = `${component.getPackageName()}/${component.getClassName()}`
    }
    if (extras !== null) {
      let extras_params_dict = {}
      let iter = extras.keySet().iterator()
      while (iter.hasNext()) {
            let key = iter.next()
            let value = extras.getString(key)
            if (value !== null) {
                extras_params_dict = value
            }
      }
      params_dict["extras"] = extras_params_dict
    }
    let params = Object.entries(params_dict).map(() => `${key}=${JSON.stringify(value)}`).join(", ")
    let pythonStatement = `device.start_activity(${params})`
    console.log(pythonStatement)
}

function printAmAndPython(intent) {
    let am_command = constructAmCommand(intent.getAction(), intent.getCategories(), intent.getComponent(), intent.getExtras())
    let python_statement = constructPythonStatement(intent.getAction(), intent.getCategories(), intent.getComponent(), intent.getExtras())
    console.log(am_command)
    console.log(python_statement)
}

function hookOnCreate() {
    var Activity = Java.use("android.app.Activity");
    Activity.onCreate.overload('android.os.Bundle').implementation = function (arg1) {
      var theIntent = this.getIntent()
      printAmAndPython(theIntent)
      constructPythonStatement(theIntent.getAction(), theIntent.getCategories(), theIntent.getComponent(), theIntent.getExtras())
      return this.onCreate(arg1)
    };
}

setTimeout(() => {
    Java.perform(() => {
      console.log("[~] Inject FRIDA ...");
      try {
            hookOnCreate();
      } catch (err) { console.error(err.stack); }
      console.log("[-] FRIDA end mocking.");
    })
}, 2000)

p1s1lver 发表于 2023-4-7 18:48

就 frida 指定运行即可,就下面这种格式

p1s1lver 发表于 2023-4-8 11:03

原理上,基于 IntentFilter 机制的 am 命令启动 activity 只需要 -a -c -n -es -ei 这些参数即可,其中 -es 和 -ei 参数可以直接简化为 -e 参数 (不显式指定是 String 还是 Int 类型),所以修改了一下代码,减少对 -t 和 -f 参数的判断

function intentToAmCommand(intent) {
    let amCommand = "am start"
    let action = intent.getAction()
    let categories = intent.getCategories()
    let component = intent.getComponent()
    let extras = intent.getExtras()

    if (action !== null) {
      amCommand += ` -a ${action}`
    }
    if (categories && categories.length > 0) {
      categories.forEach(category => {
            amCommand += ` -c ${category}`
      })
    }
    if (component !== null) {
      amCommand += ` -n ${component.getPackageName()}/${component.getClassName()}`
    }
    if (extras !== null) {
      amCommand += ` -e`
      let iter = extras.keySet().iterator()
      while (iter.hasNext()) {
            let key = iter.next()
            let value = extras.getString(key)
            amCommand += ` "${key}" "${value}"`
      }
    }
    return amCommand
}

function hookOnCreate() {
    var Activity = Java.use("android.app.Activity");
    Activity.onCreate.overload('android.os.Bundle').implementation = function (arg1) {
      var theIntent = this.getIntent()
      console.log(` am command: ${intentToAmCommand(theIntent)}`)
      return this.onCreate(arg1)
    };
}

setTimeout(() => {
    Java.perform(() => {
      console.log("[~] Inject FRIDA ...");
      try {
            hookOnCreate();
      } catch (err) { console.error(err.stack); }
      console.log("[-] FRIDA end mocking.");
    })
}, 2000)


另外,在 am 命令的输出中,需要删除的不仅仅是 -e 参数中多余的参数,还有参数(比如 url,即 Android deep linking 的 url)本身可能包含了关于时间戳和 navigator 的信息,这些信息也应该删除,否则被逻辑检测到往往会闪退,如下
am start -a com.xxx.xxx.navigator.xxx -n com.xxx.xxx/com.xxx.xxx.module.search.searchResult.xxxSearchResultActivity -e "key_intent_fdsfas_dsdfasdfata" "null" "com.xxx.xxx.navigator.url""xxx://page.xxx/search?q=%E5%A5%B6%E7%B2%89&searchType=default&_input_charset=utf-8&sdfadfp=a1z60.7757874.dsfadsfdfsadsf.5_%E5%A5%B6%E7%B2%89" "asdfasdfasdf" "21108695,7757874," "afsdfadfadsfadsfadf" "null" "fasdfewfaesfgqerg" "null"
就应该被简化为下面的命令格式:
am start -a com.xxx.xxx.navigator.xxx -n com.xxx.xxx/com.xxx.xxx.module.search.searchResult.xxxSearchResultActivity -e "com.xxx.xxx.navigator.url" "xxx://page.xxx/search?q=%E5%A5%B6%E7%B2%89"
前者打开会因为时间戳和 navigator 相关信息被检测到然后闪退,后者是可以直接打开对应的搜索页面的。

QAQ666 发表于 2023-4-8 13:02

sorryzzital 发表于 2023-4-9 15:27

写的真是详细,图文并茂,谢谢!

JordanYang 发表于 2023-4-9 18:49

真的很详细,感谢大佬分享!

MMM2042 发表于 2023-4-9 20:55

感谢分享!很详细

cqymm 发表于 2023-4-10 10:12

这个应该怎么运行啊。有没有大佬教教啊

p1s1lver 发表于 2023-4-10 10:41

xiaoshuis16now 发表于 2023-4-9 23:06
这种方法,相当于是在跑自动化前,需要自己先手动全部界面点击一遍是不?

是的,这样的话自动化就不要写很多 ui 对应的代码了,而是直接进入对应界面。
但正如文中所写,fragment 实现的只能通过 ui 点击运行,不过现在大多数的 app 都是通过 activity 界面运行的
页: [1] 2 3
查看完整版本: Android 自动化分析之 Activity 对应 am 命令提取