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

查看 76|回复 4
作者:p1s1lver   
编写 Android 自动化代码时,不想进行太多的 UI 操作,通常使用 start activity 的方式 (等价于 am 命令) 直接跳转到对应界面,如何快速获得这个 am 命令呢?
这里封装了 hook 逻辑和转换逻辑,本质上是对 Activity 对应的 Intent 对象进行了规则的提取和转换,这里我绘制了一张逻辑图,只需要 hook 之后点击界面即可获得这个界面的 am 启动命令。
有一种特殊情况,是界面本身通过 Fragment 实现而不是 Activity 实现,这种方式本身就不能通过 am 命令实现。


image_1680860454809_0.png (52.54 KB, 下载次数: 0)
下载附件
2023-4-7 18:38 上传

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


image_1680848776242_0.png (46.46 KB, 下载次数: 0)
下载附件
2023-4-7 18:38 上传

[JavaScript] 纯文本查看 复制代码
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
    OP
      

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


    Snipaste_2023-04-07_18-48-41.png (7.41 KB, 下载次数: 0)
    下载附件
    2023-4-7 18:48 上传

    p1s1lver
    OP
      

    原理上,基于 IntentFilter 机制的 am 命令启动 activity 只需要 -a -c -n -es -ei 这些参数即可,其中 -es 和 -ei 参数可以直接简化为 -e 参数 (不显式指定是 String 还是 Int 类型),所以修改了一下代码,减少对 -t 和 -f 参数的判断
    [JavaScript] 纯文本查看 复制代码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 的信息,这些信息也应该删除,否则被逻辑检测到往往会闪退,如下
    [Shell] 纯文本查看 复制代码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"
    就应该被简化为下面的命令格式:
    [Shell] 纯文本查看 复制代码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 相关信息被检测到然后闪退,后者是可以直接打开对应的搜索页面的。
    p1s1lver
    OP
      

    不少的自动化工具中会自己封装 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 启动,打印的代码及如何删除无用逻辑举例如下图:


    Snipaste_2023-04-08_11-31-57.png (98.97 KB, 下载次数: 0)
    下载附件
    2023-4-8 11:32 上传

    代码如下:
    [JavaScript] 纯文本查看 复制代码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[key] = value
                }
            }
            params_dict["extras"] = extras_params_dict
        }
        let params = Object.entries(params_dict).map(([key, value]) => `${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)
    QAQ666   

    写的真是详细,图文并茂,谢谢!
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部