转载:实用 FRIDA 进阶 --- objection :内存漫游、hook anywhere、抓包

转载:实用FRIDA进阶:内存漫游、hook anywhere、抓包:https://www.anquanke.com/post/id/197657

Frida Hook Android 常用方法:https://blog.csdn.net/zhy025907/article/details/89512096

实用FRIDA进阶:脱壳、自动化、高频问题:https://www.anquanke.com/post/id/197670

frida github 地址:https://github.com/frida/frida
objection github:https://github.com/sensepost/objection
objection pypi:https://pypi.org/project/objection/

本章中我们进一步介绍,大家在学习和工作中使用 Frida 的实际高频场景,比如:

  • 动态查看 安卓应用程序 在当前内存中的状态
  • 指哪儿就能 hook 哪儿
  • 比如 脱壳,
  • 还有使用 Frida 来自动化获取参数、返回值等数据,
  • 主动调用 API 获取签名结果 sign 等。。。

最后介绍一些经常遇到的高频问题解决思路,希望可以切实地帮助到读者。

Objection 简单使用

Frida hook工具 --- objection 使用:https://blog.csdn.net/wang_624/article/details/115601098

更多命令行参数可以查看 cli.py 文件得到https://github.com/sensepost/objection/blob/e7eb1d9b769edf6a98870c75a6d2a6123b7346fd/objection/console/cli.py

使用命令

pip install objection  # 安装
objection --help   # 查看帮助
help frida         # 不知道当前命令的作用时,进入objection后就在命令前加 help 会有提示


objection -g 包名 explore     # 注入进程,如果objection没有找到进程,会以spwan方式启动进程
objection -N -h 192.168.1.3 -p 9999 -g 包名 explore    # 指定ip和端口的连接

# spawn启动前就Hook
objection -N -h 192.168.1.3 -p 9999 -g 包名 explore --startup-command "android hooking watch class '包名.类名'"

# spawn启动前就Hook 打印参数、返回值、函数调用栈
objection -N -h 192.168.1.3 -p 9999 -g 包名 explore --startup-command "android hooking watch class_method '包名.类名.方法'  --dump-args --dump-return --dump-backtrace"


android hooking list classes  # 列出内存中所有的类
android hooking search classes 包名           # 在内存中所有已加载的类中搜索包含特定关键词的类
android hooking list class_methods 包名.类名  # 列出类的所有方法

android hooking watch class 包名.类名              # hook类的所有方法
android hooking watch class_method 包名.类名.方法  # 默认会Hook方法的所有重载

# hook方法的参数、返回值和调用栈(–dump-args: 显示参数; --dump-return: 显示返回值; --dump-backtrace: 显示堆栈)
android hooking watch class_method 包名.类名.方法 --dump-args --dump-return --dump-backtrace

android hooking search classes okhttp3
android hooking watch class okhttp3.OkHttpClient --dump-args --dump-return
android hooking watch class_method okhttp3.OkHttpClient.newCall --dump-args --dump-backtrace --dump-return

# 如果只需hook其中一个重载函数 指定参数类型 多个参数用逗号分隔
android hooking watch class_method 包名.类名.方法 "参数1,参数2"

jobs list        # 查看 hook 的任务有多少个
jobs kill jobid  # 把正在 hook 的任务关闭

android heap search instances 包名.类名 --fresh    # 搜索堆中的实例
android heap execute 地址(hashcode的地址) 方法名   # 调用实例的方法

memory list modules              # 枚举内存中所有模块
memory list exports 文件名.so    # 枚举模块中所有导出函数

objection 基本使用 + Wallbreaker

:https://www.52pojie.cn/forum.php?mod=viewthread&tid=1626964

objection 框架__EMPTY
objection -g cn.com.ccccaa.ui exploreobjection注入指定应用
android sslpinning disable过ssl证书认证
android root disable
android hooking list activities查找所有可用activities
android intent launch_activity 类名启动指定的类
android intent launch_service 类名启动指定服务
android hooking generate simple 类名查指定类下面有哪些方法
android hooking list class_methods 类列出类的所有方法
android hooking watch class 类名监视进行某个操作的时候调用了哪些方法(hook类的所有方法)
objection -g com.hd.zhibo explore --startup-command "android hooking watch class_method android.app.AlertDialog.onCreate --dump-args --dump-backtrace --dump-return"hook方法的参数、返回值和调用栈,这种实现方式是启动的时候就hook
android hooking watch class_method android.app.AlertDialog.onCreate --dump-args --dump-return --dump-backtrace具体方法调用之前hook
env应用环境信息
ls
jobs list创建的Hooks列表
jobs kill id
memory list modules查看内存中加载的库
memory list exports libssl.so查看库的导出函数
memory dump all from_base提取整个(或部分)内存
memory dump from_base 0xc935628c 100 memory.dex
memory search "64 65 78 0a 30 33 35 00"暴力搜内存
memory search "aiyou,bucuoo" --string搜索整个内存
memory search "aiyou,bucuoo" --string --offsets-only仅看偏移地址
android hooking list services查看可供开启的服务
android intent launch_service [完整Service名]直接启动指定service
android hooking list classes列出内存中所有的类
android hooking search classes [display]在内存中所有已加载的类中搜索包含特定关键词的类
android hooking search methods [display]在内存中所有已加载的类的方法中搜索包含特定关键词的类
cat .objection/objection.log日志查看
cat objection.log |grep -i http日志筛选
objection -g com.android.settings explore -c "2.txt"运行批量hook
Wallbreaker
objection -g com.android.phone explore -P ~/.objection/plugins使用Wallbreaker
plugin wallbreaker classsearch <pattern>搜索类,根据给的 pattern 对所有类名进行匹配,列出匹配到的所有类名
plugin wallbreaker objectsearch <classname>搜索对象,根据类名搜索内存中已经被创建的实例,列出 handle 和 toString() 的结果。
plugin wallbreaker classdump <classname> [--fullname]ClassDump,输出类的结构, 若加了 --fullname 参数,打印的数据中类名会带着完整的包名。
plugin wallbreaker objectdump <handle> [--fullname]ObjectDump,在 ClassDump 的基础上,输出指定对象中的每个字段的数据。

搜索类:plugin wallbreaker classsearch <pattern>
根据给的 pattern 对所有类名进行匹配,列出匹配到的所有类名。

搜索对象:plugin wallbreaker objectsearch <classname>
根据类名搜索内存中已经被创建的实例,列出 handle 和 toString() 的结果。

ClassDump:plugin wallbreaker classdump <classname> [--fullname]
输出类的结构, 若加了 --fullname 参数,打印的数据中类名会带着完整的包名。

ObjectDump:plugin wallbreaker objectdump <handle> [--fullname]
在 ClassDump 的基础上,输出指定对象中的每个字段的数据。

memory 操作

# 列举加载的 modules,也就是so文件
        memory list modules
        memory list modules --json result.txt

# 列举 so 文件的导出方法
        memory list exports xxx.so

# dump所有内存
        memory dump all /tmp/dump

# dump内存指定地址和大小
        memory dump from_base 0x130b4060 1024 /tmp/dump

# 在内存搜索
        memory search "64 65 78 0a 30 33 35 00"
        memory search "99999999999" --string
        memory search "66 72 ?? ?? 61"

# 修改内存
        memory write 0x130b4060 "99999999999" --string

activity / services / receivers 组件相关

# 列出应用程序具有的组件
        android hooking list activities
        android hooking list services
        android hooking list receivers

# 获取当前activity
        android hooking get current_activity

# 启动某个activity
        android intent launch_activity com.xxx.xxx.Activity

# 启动某个service
        android intent launch_service xxxx

class / method 相关

# 列举所有加载的类
        android hooking list classes

# 查找已加载的类中带有关键词的类
        android hooking search classes xxx

# 查看xx类有哪些方法
        android hooking list class_methods com.xx.xx

# 在内存中所有已加载的类的方法中搜索包含特定关键词的方法
        android hooking search methods xxx

# 查找所有类的实例
        android heap search instances xxx

# 主动调用指定实例的函数
        android heap execute instance_ID function

# hook指定类的所有方法, 会打印该类下的所有调用
        android hooking watch class com.xxx.xxx            

# hook指定方法, 如果有重载会hook所有重载
        android hooking watch class_method com.xxx.xxx.methodName

# 以上命令支持下面参数
# --dump-args : 打印参数
# --dump-backtrace : 打印调用栈
# --dump-return : 打印返回值
# eg:
android hooking watch class_method com.xxx.xxx.methodName --dump-args --dump-backtrace --dump-return
android hooking set return_value com.xxx.xxx.methodName false    # 设置返回值(只支持bool类型)


# 生成某个类的hook js代码
        android hooking generate  simple  com.xxx.xxx

任务管理

# 列举出当前任务
        jobs list

# 关闭任务
        jobs kill [job_id]

文件操作

# 文件基本操作
        ls
        file download
        file upload
        file cat

# 文件服务器,在当前目录下启动一个http server
        file http start/stop/status

抓包

#关闭ssl校验
        android sslpinning disable

其他操作

# 打印出应用程序文件、缓存和其他目录的位置
        env

# 对APP隐藏root
        android root disable 

# 执行命令
        android shell_exec ls 

# 截屏
        android ui screenshot /sdcard/1.png

# 导入外部 js 脚本
        import 1.js 

日志

objection日志文件位于:

        ~/.objection/objection.log

命令历史文件位于:

~/.objection/objection_history
删除 ~/.objection目录后,在下次 objection 启动时会重新创建。

参考:https://github.com/sensepost/objection/wiki/Using-objection

frida 启动

adb forward tcp:27042 tcp:27042

frida -U -l js_okhttp.js     -F com.cdsb.newsreader --no-pause
frida -U -l okhttp_poker.js  -F com.cdsb.newsreader --no-pause
frida -U -l okhttp_poker.js  -F com.huanqiu.news --no-pause
frida -U -l frida_hook_js.js -f com.huanqiu.news --no-pause

objection -g com.app.name explore -P ~/objection/plugins
objection -g com.cdsb.newsreader explore -P objection_plugins

python r0capture.py -U -f com.cdsb.newsreader -v
python r0capture.py -U com.cdsb.newsreader -v -p cdsb.pcap

objection 下所有命令 简单说明

基于 frida 的 objection 及其插件 wallbreaker 命令列表

( :https://www.cnblogs.com/ningskyer/articles/14611822.html )

!           # 执行操作系统的命令(注意:不是在所连接device上执行命令)
android     # 执行指定的 Android 命令
        clipboard
                monitor
        deoptimize    # Force the VM to execute everything in the interpreter
        heap
                evaluate    # 在 Java 类中执行 JavaScript 脚本。
                execute     # 在 Java 类中执行 方法。android heap execute 实例ID 实例方法
                print
                search
                        instances  # 在当前Android heap上搜索类的实例。android heap search instances 类名
        hooking
                generate
                        class    #  A generic hook manager for Classes
                        simple   #  Simple hooks for each Class method
                get
                        current_activity    #  获取当前 前景(foregrounded) activity
                list
                        activities    # 列出已经登记的 Activities
                        class_loaders # 列出已经登记的 class loaders 
                        class_methods # 列出一个类上的可用的方法
                        classes       # 列出当前载入的所有类
                        receivers     # 列出已经登记的 BroadcastReceivers
                        services      # 列出已经登记的 Services
                search
                        classes 关键字    # 搜索与名称匹配的Java类
                        methods 关键字    # 搜索与名称匹配的Java方法
                set
                        return_value    # 设置一个方法的返回值。只支持布尔返回
                watch
                        class           # Watches for invocations of all methods in a class
                        class_method    # Watches for invocations of a specific class method
        intent
                launch_activity    # 使用Intent启动Activity类
                launch_service     # Launch a Service class using an Intent
        keystore
                clear    # 清除 Android KeyStore
                list     # 列出 Android KeyStore 中的条目
                watch    # 监视 Android KeyStore 的使用
        proxy    
                set      # 为应用程序设置代理
        root
            disable    # 试图禁用 root 检测
            simulate   # 试图模拟已经 root 的环境
        shell_exec     # 执行shell命令
        sslpinning
                disable    # 尝试禁用 SSL pinning 在各种 Java libraries/classes
        ui
            FLAG_SECURE    # Control FLAG_SECURE of the current Activity
            screenshot     # 在当前 Activity 进行截图
cd          # 改变当前工作目录
commands        
        clear    # 清除当前会话命令的历史记录
        history  # 列出当前会话命令历史记录
        save     # 将在此会话中运行的所有惟一命令保存到一个文件中
env         # 打印环境信息
evaluate    # 执行 JavaScript。( Evaluate JavaScript within the agent )
exit        # 退出
file
        cat         # 打印文件内容
        download    # 下载一个文件
        http        
                start    # Start's an HTTP server in the current working directory
                status   # Get the status of the HTTP server
                stop     # Stop's a running HTTP server
        upload           # 上传一个文件
frida       # 获取关于 frida 环境的信息
import      # 从完整路径导入 frida 脚本并运行
ios         执行指定的 ios 命令
        bundles
        cookies
        heap
        hooking
        info
        jailbreak
        keychain
        monitor
        nsurlcredentialstorage
        nsuserdefaults
        pasteboard
        plist
        sslpinning
        ui
jobs
        kill    # 结束一个任务。这个操作不会写在卸载或者退出当前脚本
        list    # 列出当前所有的任务
ls              # 列出当前工作目录下的文件
memory
        dump
                all 文件名                      # Dump 当前进程的整个内存
                from_base 起始地址 字节数 文件  # 将(x)个字节的内存从基址转储到文件
        list
                exports    # List the exports of a module. (列出模块的导出)
                modules    # List loaded modules in the current process. (列出当前进程中已加载的模块)
        search    # 搜索模块。用法:memory search "<pattern eg: 41 41 41 ?? 41>" (--string) (--offsets-only)
        write     # 将原始字节写入内存地址。小心使用!
ping        # ping agent
plugin      
        load    # 载入插接
pwd             # 打印当前工作目录
reconnect       # 重新连接 device
rm              # 从 device 上删除文件 
sqlite          # sqlite 数据库命令
        connect  # 连接到SQLite数据库文件
ui
        alert    # 显示警报消息,可选地指定要显示的消息。(目前iOS崩溃)

1. 内存漫游

Frida 只是提供了各种 API 供我们调用,在此基础之上可以实现具体的功能,比如禁用证书绑定之类的脚本,就是使用 Frida 的各种 API 来组合编写而成。于是有大佬将各种常见、常用的功能整合进一个工具,供我们直接在命令行中使用,这个工具便是objectionobjection 功能强大,命令众多,而且不用写一行代码,便可实现诸如内存搜索、类和模块搜索、方法hook打印参数返回值调用栈等常用功能,是一个非常方便的,逆向必备、内存漫游神器。

安装命令:pip3 install objection

objection 的界面及命令如图所示。

objection 是基于 frida 的命令行 hook 工具,可以让你不写代码, 敲几句命令就可以对 java 函数的高颗粒度 hook, 还支持 RPC 调用

objection 目前只支持 Java层的 hook,但是 objection 有提供插件接口,可以自己写 frida 脚本去定义接口,

比如葫芦娃大佬的脱壳插件,实名推荐: https://github.com/hluwa/FRIDA-DEXDump

官方仓库: https://github.com/sensepost/objection

1.1 获取基本信息

首先介绍几个基本操作:

  • 键入命令之后,回车执行;
  • help: 不知道当前命令的效果是什么,在当前命令前加 help 比如:help env,回车之后会出现当前命令的解释信息;
  • 按空格: 不知道输入什么就按空格,会有提示出来,上下选择之后再按空格选中,又会有新的提示出来;
  • jobs: 作业系统很好用,建议一定要掌握,可以同时运行 多项 ( hook ) 作业

简单使用

  • 启动 Frida-server,并转发端口 ( adb forward tcp:27042 tcp:27042 )
  • 附加需要调试的 app,进入交互界面 ( objection -g [packageName] explore )

连接逍遥模拟器,需要先进入模拟器所在目录,使用目录中 adb.exe 命令执行:adb.exe connect 127.0.0.1:21503

可以使用该 env 命令枚举与所讨论的应用程序相关的其他有趣目录:env

可以使用以下 file download 命令从远程文件系统中下载文件:file download [file] [outfile]

com.opera.mini.native on (samsung: 6.0.1) [usb] # file download fhash.dat fhash.dat
Downloading /data/user/0/com.opera.mini.native/cache/fhash.dat to fhash.dat

可以列出 app 具有的所有avtivity:android hooking list activities

com.opera.mini.native on (samsung: 6.0.1) [usb] # android hooking list activities
com.facebook.ads.AudienceNetworkActivity
com.google.android.gms.ads.AdActivity
com.google.android.gms.auth.api.signin.internal.SignInHubActivity
com.google.android.gms.common.api.GoogleApiActivity
com.opera.android.AssistActivity
com.opera.android.MiniActivity
com.opera.android.ads.AdmobIntentInterceptor
com.opera.mini.android.BrowserFound 8 classes

启动指定 avtivity:android intent launch_activity [class_activity]

com.opera.mini.native on (samsung: 6.0.1) [usb] # android intent launch_activity 
com.facebook.ads.AudienceNetworkActivity
Launching Activity: com.facebook.ads.AudienceNetworkActivity...

RPC 调用命令:curl -s "http://127.0.0.1:8888/rpc/invoke/androidHookingListActivities"

$ curl -s "http://127.0.0.1:8888/rpc/invoke/androidHookingListActivities"
["com.reddit.frontpage.StartActivity","com.reddit.frontpage.IntroductionActivity", ... snip ...]- RPC调用执行脚本:`url -X POST -H "Content-Type: text/javascript" http://127.0.0.1:8888/script/runonce -d "@script.js"`$ cat script.js
{send(Frida.version);
}[{"payload":"12.8.0","type":"send"}]

RPC WIKI:https://github.com/sensepost/objection/wiki/API

API 介绍

以下只是写了一部分指令和功能, 详细的功能需要合理运用 空格 和 help


Memory 指令memory list modules               //枚举当前进程模块memory list exports [lib_name]    //查看指定模块的导出函数memory list exports libart.so --json /root/libart.json //将结果保存到json文件中memory search --string --offsets-only                  //搜索内存android heap 指令//堆内存中搜索指定类的实例, 可以获取该类的实例idsearch instances search instances com.xx.xx.class//直接调用指定实例下的方法android heap execute [ins_id] [func_name]//自定义frida脚本, 执行实例的方法android heap execute [ins_id]android 指令android root disable   //尝试关闭app的root检测android root simulate  //尝试模拟root环境android ui screenshot [image.png]    //截图android ui FLAG_SECURE false         //设置FLAG_SECURE权限内存漫游android hooking list classes    //列出内存中所有的类//在内存中所有已加载的类中搜索包含特定关键词的类android hooking search classes [search_name] //在内存中所有已加载的方法中搜索包含特定关键词的方法android hooking search methods [search_name] //直接生成hook代码android hooking generate simple [class_name]hook 方式/*hook指定方法, 如果有重载会hook所有重载,如果有疑问可以看--dump-args : 打印参数--dump-backtrace : 打印调用栈--dump-return : 打印返回值*/android hooking watch class_method com.xxx.xxx.methodName --dump-args --dump-backtrace --dump-return//hook指定类, 会打印该类下的所有调用android hooking watch class com.xxx.xxx//设置返回值(只支持bool类型)android hooking set return_value com.xxx.xxx.methodName falseSpawn 方式 Hookobjection -g packageName explore --startup-command '[obejection_command]'activity 和 service 操作android hooking list activities                   //枚举activityandroid intent launch_activity [activity_class]   //启动activityandroid hooking list services                     //枚举servicesandroid intent launch_service [services_class]    //启动services任务管理器jobs list            // 查看任务列表jobs kill [task_id]  // 关闭任务关闭 app 的 ssl 校验android sslpinning disable监控系统剪贴板// 获取Android剪贴板服务上的句柄并每5秒轮询一次用于数据。 // 如果发现新数据,与之前的调查不同,则该数据将被转储到屏幕上。help android  clipboard执行命令行help android shell_exec [command]

插件编写 : objection pluging:https://github.com/sensepost/objection/wiki/Plugins

不写一行代码探索应用行为 --- 使用 objection

From:https://www.y4f.net/77651.html

这里拿 XCTF 的三个题目做演示,分别是mobile进阶区的第3题、第8题和第17题。

示例:以安卓 内置应用 "设置" 演示基本用法

在手机上启动 frida-server,并且点击启动 "设置" 图标,手机进入设置的界面,首先查看一下 "设置" 应用的包名。

# frida-ps -U|grep -i setting7107  com.android.settings
13370  com.google.android.settings.intelligence

再使用 objection 注入 "设置" 应用。

# objection -g com.android.settings explore

启动 objection之后,会出现提示它的 logo,这时候不知道输入啥命令的话,可以按下空格,有提示的命令及其功能出来;
再按空格选中,又会有新的提示命令出来,这时候按回车就可以执行该命令,

见下图 2-2 执行的应用环境信息命令 env 和 frida-server 版本信息命令。

1.2 提取内存信息

1.2.1 查看内存中加载的库( memory list modules )

运行命令 memory list modules,效果如下图2-3所示。内存中加载的库

1.2.2 查看库的导出函数 ( memory list exports libssl.so )

运行命令 memory list exports libssl.so,效果如下图2-4所示。 libssl.so 库的导出函数

1.2.3 将结果保存到 json文件中

当结果太多,终端无法全部显示的时候,可以将结果导出到文件中,然后使用其他软件查看内容,见下图2-5。

# memory list exports libart.so --json /root/libart.json  
Writing exports as json to /root/libart.json...
Wrote exports to: /root/libart.json

使用 json 格式保存的 libart.so 的导出函数

1.2.4 提取整个(或部分)内存( memory dump all from_base )

命令是 memory dump all from_base,这部分内容与下文脱壳部分有重叠,我们在脱壳部分介绍用法。

1.2.5 搜索整个内存( memory search --string --offsets-only )

命令是 memory search --string --offsets-only,这部分也与下文脱壳部分有重叠,我们在脱壳部分详细介绍用法。


1.3 内存堆 (heap上的搜索执行

1.3.1 在堆 (heap)上搜索实例

我们查看AOSP源码关于设置里显示系统设置的部分,发现存在着 DisplaySettings类,可以在堆上搜索是否存在着该类的实例。

首先在手机上点击进入 "显示" 设置,然后运行命令:android heap search instances com.android.settings.DisplaySettings

并得到相应的实例地址:

1.3.2 调用实例的方法

查看源码得知 com.android.settings.DisplaySettings类 有一个 getPreferenceScreenResId()方法,这样就可以直接调用该实例的 getPreferenceScreenResId()方法,

后文也会介绍在objection中直接打印类的所有方法的命令

用 excute 命令:android heap execute 0x2526 getPreferenceScreenResId

Handle 0x2526 is to class com.android.settings.DisplaySettings
Executing method: getPreferenceScreenResId()
2132082764

可见结果被直接打印了出来。

1.3.3 在实例上执行 js 代码

也可以在找到的实例上直接编写 js 脚本,输入android heap evaluate 0x2526 命令后,会进入一个迷你编辑器环境,

  • 输入 console.log("evaluate result:"+clazz.getPreferenceScreenResId()) 这串脚本,
  • 按ESC退出编辑器,然后按回车,即会开始执行这串脚本,输出结果。
# android heap evaluate 0x2526                                          
(The handle at `0x2526` will be available as the `clazz` variable.)console.log("evaluate result:"+clazz.getPreferenceScreenResId()) JavaScript capture complete. Evaluating...
Handle 0x2526 is to class com.android.settings.DisplaySettings
evaluate result:2132082764

这个功能其实非常厉害,可以即时编写、出结果、即时调试自己的代码,不用再:编写→注入→操作→看结果→再调整,而是直接出结果。

1.4 启动 activity 或 service

1.4.1 直接启动 activity

直接上代码,想要进入显示设置,可以在任意界面直接运行以下代码进入显示设置:

# android intent launch_activity com.android.settings.DisplaySettings                      
(agent) Starting activity com.android.settings.DisplaySettings...
(agent) Activity successfully asked to start.

1.4.2 查看当前可用的 activity

可以使用 android hooking list 命令来查看当前可用的 activities,然后使用上述命令进行调起。

# android hooking list activitiescom.android.settings.ActivityPicker
com.android.settings.AirplaneModeVoiceActivity
com.android.settings.AllowBindAppWidgetActivity
com.android.settings.AppWidgetPickActivity
com.android.settings.BandMode
com.android.settings.ConfirmDeviceCredentialActivity
com.android.settings.CredentialStorage
com.android.settings.CryptKeeper$FadeToBlack
com.android.settings.CryptKeeperConfirm$Blank
com.android.settings.DeviceAdminAdd
com.android.settings.DeviceAdminSettings
com.android.settings.DisplaySettings
com.android.settings.EncryptionInterstitial
com.android.settings.FallbackHome
com.android.settings.HelpTrampoline
com.android.settings.LanguageSettings
com.android.settings.MonitoringCertInfoActivity
com.android.settings.RadioInfo
com.android.settings.RegulatoryInfoDisplayActivity
com.android.settings.RemoteBugreportActivity
com.android.settings.RunningServices
com.android.settings.SetFullBackupPassword
com.android.settings.SetProfileOwner
com.android.settings.Settings
com.android.settings.Settings
com.android.settings.Settings$AccessibilityDaltonizerSettingsActivity
com.android.settings.Settings$AccessibilitySettingsActivity
com.android.settings.Settings$AccountDashboardActivity
com.android.settings.Settings$AccountSyncSettingsActivity
com.android.settings.Settings$AdvancedAppsActivity

1.4.3 直接启动 service

也可以先使用 android hooking list services 查看可供开启的服务,

然后使用 android intent launch_service com.android.settings.bluetooth.BluetoothPairingService 命令来开启服务。

2. Frida hook anywhere

很多新手在学习 Frida 的时候,遇到的第一个问题就是:无法找到正确的类及子类,无法定位到实现功能的准确的方法,无法正确的构造参数、继而进入正确的重载

这时候可以使用 Frida 进行动态调试,来确定以上具体的名称和写法,最后写出正确的hook代码。


2.1 objection(内存漫游)

2.1.1 列出内存中所有的类

执行命令:android hooking list classes

# android hooking list classessun.util.logging.LoggingSupport
sun.util.logging.LoggingSupport$1
sun.util.logging.LoggingSupport$2
sun.util.logging.PlatformLogger
sun.util.logging.PlatformLogger$1
sun.util.logging.PlatformLogger$JavaLoggerProxy
sun.util.logging.PlatformLogger$Level
sun.util.logging.PlatformLogger$LoggerProxy
voidFound 11885 classes

2.1.2 内存中搜索包含特定关键词的类

执行命令:android hooking search classes 关键字。在内存中所有已加载的类中搜索包含特定关键词的类。

示例( 搜索包含关键 display 的 类 ):android hooking search classes display     

# android hooking search classes display
[Landroid.hardware.display.WifiDisplay;
[Landroid.icu.impl.ICUCurrencyDisplayInfoProvider$ICUCurrencyDisplayInfo$CurrencySink$EntrypointTable;
[Landroid.icu.impl.LocaleDisplayNamesImpl$CapitalizationContextUsage;
[Landroid.icu.impl.LocaleDisplayNamesImpl$DataTableType;
[Landroid.icu.number.NumberFormatter$DecimalSeparatorDisplay;
[Landroid.icu.number.NumberFormatter$SignDisplay;
[Landroid.icu.text.DisplayContext$Type;
[Landroid.icu.text.DisplayContext;
[Landroid.icu.text.LocaleDisplayNames$DialectHandling;
[Landroid.view.Display$Mode;
[Landroid.view.Display;
android.app.Vr2dDisplayProperties
android.hardware.display.AmbientBrightnessDayStats
android.hardware.display.AmbientBrightnessDayStats$1
android.hardware.display.BrightnessChangeEvent
com.android.settings.wfd.WifiDisplaySettings$SummaryProvider
com.android.settings.wfd.WifiDisplaySettings$SummaryProvider$1
com.android.settingslib.display.BrightnessUtils
com.android.settingslib.display.DisplayDensityUtils
com.google.android.gles_jni.EGLDisplayImpl
javax.microedition.khronos.egl.EGLDisplayFound 144 classes

2.1.3 内存中搜索所有的方法

在内存中所有已加载的类的方法中搜索包含特定关键词的方法,上文中可以发现,内存中已加载的类就已经高达11885个了,那么他们的方法一定是类的个数的数倍,整个过程会相当庞大和耗时,见下图2-6。

# android hooking search methods display

内存中搜索所有的方法

2.1.4 列出指定类的所有方法

当搜索到了比较关心的类之后,就可以直接查看它有哪些方法,

比如:我们想要查看 com.android.settings.DisplaySettings 类 有哪些方法,就可以执行命令:android hooking list class_methods com.android.settings.DisplaySettings

# android hooking list class_methods com.android.settings.DisplaySettings                                                                                                                        
private static java.util.List<com.android.settingslib.core.AbstractPreferenceController> com.android.settings.DisplaySettings.buildPreferenceControllers(android.content.Context,com.android.settingslib.core.lifecycle.Lifecycle)
protected int com.android.settings.DisplaySettings.getPreferenceScreenResId()
protected java.lang.String com.android.settings.DisplaySettings.getLogTag()
protected java.util.List<com.android.settingslib.core.AbstractPreferenceController> com.android.settings.DisplaySettings.createPreferenceControllers(android.content.Context)
public int com.android.settings.DisplaySettings.getHelpResource()
public int com.android.settings.DisplaySettings.getMetricsCategory()
static java.util.List com.android.settings.DisplaySettings.access$000(android.content.Context,com.android.settingslib.core.lifecycle.Lifecycle)Found 7 method(s)

列出的方法与源码相比对之后,发现是一模一样的。

2.1.5 自动生成 hook 代码

上文中在列出类的方法时,还直接把参数也提供了,也就是说我们可以直接动手写 hook 了,既然上述写 hook 的要素已经全部都有了,objection 这个 "自动化" 工具,当然可以直接生成代码。

自动生成 hook 代码的命令:android hooking generate  simple  com.android.settings.DisplaySettings

# android hooking generate  simple  com.android.settings.DisplaySettingsJava.perform(function() {var clazz = Java.use('com.android.settings.DisplaySettings');clazz.getHelpResource.implementation = function() {//return clazz.getHelpResource.apply(this, arguments);}
});Java.perform(function() {var clazz = Java.use('com.android.settings.DisplaySettings');clazz.getLogTag.implementation = function() {//return clazz.getLogTag.apply(this, arguments);}
});Java.perform(function() {var clazz = Java.use('com.android.settings.DisplaySettings');clazz.getPreferenceScreenResId.implementation = function() {//return clazz.getPreferenceScreenResId.apply(this, arguments);}
});

生成的代码大部分要素都有了,只是参数貌似没有填上,还是需要我们后续补充一些,看来还是无法做到完美。

2.2 objection(hook)

上述操作均是基于在内存中直接枚举搜索,已经可以获取到大量有用的静态信息,我们再来介绍几个方法,可以获取到执行时动态的信息,当然、同样地,不用写一行代码。

2.2.1 hook类的所有方法

我们以手机连接蓝牙耳机播放音乐为例,看看手机蓝牙接口的动态信息。

  • 首先我们将手机连接上我的蓝牙耳机(一加蓝牙耳机OnePlus Bullets Wireless 2),并可以正常播放音乐;
  • 然后我们按照上文的方法,搜索一下与蓝牙相关的类,搜到一个高度可疑的类:android.bluetooth.BluetoothDevice
  • 运行命令,hook 这个类:# android hooking watch class android.bluetooth.BluetoothDevice

使用 jobs list 命令可以看到 objection 为我们创建的 Hooks 数为 57也就是将 android.bluetooth.BluetoothDevice类 下的所有方法都 hook了。这时候我们在 设置→声音→媒体播放到上进行操作,在蓝牙耳机与“此设备”之间切换时,会命中这些hook之后,此时objection就会将方法打印出来,会将类似这样的信息“吐”出来:

com.android.settings on (google: 9) [usb] # (agent) [h0u5g7uclo] Called
android.bluetooth.BluetoothDevice.getService()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.isConnected()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getService()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getAliasName()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getAlias()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getName()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.equals(java.lang.Object)
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getService()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.isConnected()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getService()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getAliasName()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getAlias()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getName()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.equals(java.lang.Object)
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.equals(java.lang.Object)
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getBatteryLevel()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.equals(java.lang.Object)
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getBatteryLevel()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.equals(java.lang.Object)
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getBondState()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getAliasName()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getAlias()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getName()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getBatteryLevel()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.equals(java.lang.Object)
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getBatteryLevel()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.equals(java.lang.Object)
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getBondState()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getAliasName()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getAlias()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getName()
(agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getService()

可以看到我们的切换操作,调用到了 android.bluetooth.BluetoothDevice 类中的多个方法。

2.2.2 hook 方法的参数、返回值、调用栈

在这些方法中,我们对哪些方法感兴趣,就可以查看哪些个方法的参数、返回值和调用栈,比如想看 getName()方法,则运行以下命令:# android hooking watch class_method android.bluetooth.BluetoothDevice.getName --dump-args --dump-return --dump-backtrace

注意最后加上的三个选项 --dump-args --dump-return --dump-backtrace,为我们成功打印出来了我们想要看的信息,其实返回值 Return Value 就是 getName()方法的返回值,我的蓝牙耳机的型号名字 OnePlus Bullets Wireless 2;从调用栈可以反查如何一步一步调用到 getName()这个方法的;虽然这个方法没有参数,大家可以再找个有参数的试一下。

2.2.3 hook 方法的所有重载

objection 的 help 中指出,在 hook 给出的单个方法的时候,会 hook 它的所有重载

# help android hooking watch class_method
Command: android hooking watch class_methodUsage: android hooking watch class_method <fully qualified class method> <optional overload>(optional: --dump-args) (optional: --dump-backtrace)(optional: --dump-return)Hooks a specified class method and reports on invocations, together with
the number of arguments that method was called with. This command will
also hook all of the methods available overloads unless a specific    
overload is specified. If the --include-backtrace flag is provided, a full stack trace that 
lead to the methods invocation will also be dumped. This would aid in 
discovering who called the original method.     Examples: android hooking watch class_method com.example.test.loginandroid hooking watch class_method com.example.test.helper.executeQuery android hooking watch class_method com.example.test.helper.executeQuery "java.lang.String,java.lang.String"android hooking watch class_method com.example.test.helper.executeQuery --dump-backtraceandroid hooking watch class_method com.example.test.login --dump-args --dump-return

那我们可以用 File 类的构造器来试一下效果。

# android hooking watch class_method java.io.File.$init --dump-args

可以看到 objection 为我们 hook 了 File 构造器的所有重载,一共是6个。在设置界面随意进出几个子设置界面,可以看到命中很多次该方法的不同重载,每次参数的值也都不同,

见下图。 方法重载的参数和值都不同

2.3 objection 插件 --- Wallbreaker

葫芦娃 github:https://github.com/hluwa?tab=repositories

Wallbreaker:从内存里面进行 逆向

Wallbreaker 是一个有用的工具,用于实时分析 Java 堆,由frida提供支持。提供一些命令从内存中搜索对象或类,并精美地可视化目标的真实结构。

想知道真实的数据内容吗?项目清单?地图条目?想知道接口的实现吗?尝试一下!你所看到的就是你得到的!

使用方法:参看 github:https://github.com/hluwa/Wallbreaker

使用 Wallbreaker 快速分析 Java 类/对象结构

From:https://bbs.pediy.com/thread-260110.htm

Wallbreaker 取自 wikipedia 上对《三体》"破壁者"的翻译。

wallbreaker 是一个超级懒人(我)为了减少编写重复性垃圾代码而产生的一个工具,主要作用是将内存中 Java 类或对象的结构数据进行可视化。

就像介个亚子:

应用场景

如何使用

目前我是比较喜欢以 objection 插件的形式来使用,本来我也想自己写交互式控制台,但我觉得 objection 已经写得挺好,直接上车就好了,所以暂时不打算自己实现了。
开发的时候就使用 ipython 或者写 testcase 调试。

  1. 安装 objection:pip3 install objection
  2. 下载 wallbreaker 到自己的插件目录:git clone https://github.com/hluwa/Wallbreaker ~/.objection/plugins/Wallbreaker
  3. 启动 frida-server,使用 -P 参数带着插件启动 objection:objection -g com.app.name explore -P ~/.objection/plugins

然后就可以愉快的使用 wallbreaker 的几个命令了:

  • 搜索 plugin wallbreaker classsearch <type-pattern>
        根据给的 pattern 对所有类名进行匹配,列出匹配到的所有类名。[return all matched class]
  • 搜索 实例对象:plugin wallbreaker objectsearch <instance-class-name>
        根据类名搜索内存中已经被创建的实例,列出 handle 和 toString() 的结果。 [return all matched object-handle and toString]
  • ClassDump,输出类的结构:plugin wallbreaker classdump <class-name> [--fullname]
        输出类的结构, 若加了 --fullname 参数,打印的数据中类名会带着完整的包名。
            [
               pretty print class structure: fields declare, static field value, methods declare.
                  set --fullname to display package name of type name.
            ]
  • ObjectDump:plugin wallbreaker objectdump <object-handle> [--fullname] [--as-class class-name]
        在 ClassDump 的基础上,输出指定对象中的每个字段的数据。
            [
               pretty print object structure: fields declare and value, methods declare.
                  set --fullname to display package name of type name;
                  set --as-class to cast instance type(super class, not interface).
               if instance is a collection or map, dump all entries.
            ]

DEMO:https://asciinema.org/a/XZf8yLWJylCKJfcaYzcKlNbIy

2.3 ZenTracer(hook)

前文中介绍的 objection 已经足够强大,优点是 hook 准确、粒度细。这里再推荐个好友自己写的批量 hook 查看调用轨迹的工具ZenTracer ( https://github.com/hluwa/ZenTracer ),可以更大范围地 hook,帮助读者辅助分析。

# pyenv install 3.8.0
# git clone https://github.com/hluwa/ZenTracer
# cd ZenTracer
# pyenv local 3.8.0
# python -m pip install --upgrade pip
# pip install PyQt5
# pip install frida-tools
# python ZenTracer.py

上述命令执行完毕之后,会出现一个 PyQt 画出来的界面,如图 2-10 所示。

点击 Action之后,会出现匹配模板(Match RegEx)和过滤模板(Black RegEx)。匹配就是包含的关键词,过滤就是不包含的关键词,见下图2-11。其代码实现就是

通过如下的代码实现,hook 出来的结果需要通过匹配模板进行匹配,并且筛选剔除掉过滤模板中的内容。

var matchRegEx = {MATCHREGEX};
var blackRegEx = {BLACKREGEX};
Java.enumerateLoadedClasses({onMatch: function (aClass) {for (var index in matchRegEx) {// console.log(matchRegEx[index]);// 通过匹配模板进行匹配if (match(matchRegEx[index], aClass)) {var is_black = false;for (var i in blackRegEx) {//如果也包含在过滤模板中,则剔除if (match(blackRegEx[i], aClass)) {is_black = true;log(aClass + "' black by '" + blackRegEx[i] + "'");break;}}if (is_black) {break;}log(aClass + "' match by '" + matchRegEx[index] + "'");traceClass(aClass);}}},onComplete: function () {log("Complete.");}
});

通过下述代码实现的模糊匹配和精准匹配:

function match(ex, text) {if (ex[1] == ':') {var mode = ex[0];if (mode == 'E') {ex = ex.substr(2, ex.length - 2);return ex == text;} else if (mode == 'M') {ex = ex.substr(2, ex.length - 2);} else {log("Unknown match mode: " + mode + ", current support M(match) and E(equal)")}}return text.match(ex)
}

通过下述代码实现的导入导出调用栈及观察结果:

def export_onClick(self):jobfile = QFileDialog.getSaveFileName(self, 'export', '', 'json file(*.json)')if isinstance(jobfile, tuple):jobfile = jobfile[0]if not jobfile:returnf = open(jobfile, 'w')export = {}export['match_regex'] = self.app.match_regex_listexport['black_regex'] = self.app.black_regex_listtree = {}for tid in self.app.thread_map:tree[self.app.thread_map[tid]['list'][0].text()] = gen_tree(self.app.thread_map[tid]['list'][0])export['tree'] = treef.write(json.dumps(export))f.close()def import_onClick(self):jobfile = QFileDialog.getOpenFileName(self, 'import', '', 'json file(*.json)')if isinstance(jobfile, tuple):jobfile = jobfile[0]if not jobfile:returnf = open(jobfile, 'r')export = json.loads(f.read())for regex in export['match_regex']: self.app.match_regex_list.append(regex), self.app.match_regex_dialog.setupList()for regex in export['black_regex']: self.app.black_regex_list.append(regex), self.app.black_regex_dialog.setupList()for t in export['tree']:tid = t[0: t.index(' - ')]tname = t[t.index(' - ') + 3:]for item in export['tree'][t]:put_tree(self.app, tid, tname, item)

示例 ( ZenTracer ):hook java.io.File类的所有方法

我们来完整的演示一遍,比如现在看java.io.File类的所有方法,我们可以这样操作,首先是精准匹配:

  • 点击打开 "设置" 应用;
  • 选择 Action → Match RegEx
  • 输入E:java.io.File,点击add,然后关闭窗口
  • 点击 Action → Start

可以看到 java.io.File 类的所有方法都被 hook 了,并且像 java.io.File.createTempFile 方法的所有重载也被 hook 了。

1. 在 "设置" 应用上进行操作,打开几个子选项的界面之后,观察方法的参数和返回值;

2. 导出 json 来观察方法的调用树,选择 File → Export json,导出为 tmp.json,使用 vscode 来 format Document 之后,效果如下:

{"match_regex": ["E:java.io.File"],"black_regex": [],"tree": {"2 - main": [{"clazz": "java.io.File","method": "exists()","args": [],"child": [],"retval": "false"},{"clazz": "java.io.File","method": "toString()","args": [],"child": [{"clazz": "java.io.File","method": "getPath()","args": [],"child": [],"retval": "/data/user/0/com.android.settings"}],"retval": "/data/user/0/com.android.settings"},{"clazz": "java.io.File","method": "equals(java.lang.Object)","args": ["/data/user/0/com.android.settings"],"child": [{"clazz": "java.io.File","method": "toString()","args": [],"child": [{"clazz": "java.io.File","method": "getPath()","args": [],"child": [],"retval": "/data/user/0/com.android.settings"}],"retval": "/data/user/0/com.android.settings"},{"clazz": "java.io.File","method": "compareTo(java.io.File)","args": ["/data/user/0/com.android.settings"],"child": [{"clazz": "java.io.File","method": "getPath()","args": [],"child": [],"retval": "/data/user_de/0/com.android.settings"},{"clazz": "java.io.File","method": "getPath()","args": [],"child": [],"retval": "/data/user/0/com.android.settings"}],"retval": "48"}],"retval": "false"},

3. 点击 Action→Stop,再点击 Action→Clean,本次观察结束。

也可以使用模糊匹配模式,比如输入M:java.io.File之后,会将诸如 java.io.FileOutputStream 类的诸多方法也都 hook上,见下图2-14。

ZenTracer 的目前已知的缺点,无法打印调用栈,无法 hook 构造函数,也就是 $init。当然这些 “缺点” 无非也就是加几行代码的事情,整个工具非常不错,值得用于辅助分析。

3. Frida 用于抓包

我们拿到一个app,做的第一件事情往往是先抓包来看,它发送和接收了哪些数据。收包发包是一个app的命门,企业为用户服务过程中最为关键的步骤——注册、流量商品、游戏数据、点赞评论、下单抢票等行为,均通过收包发包来完成。如果对收包发包的数据没有校验,黑灰产业可以直接制作相应的协议刷工具,脱离app本身进行实质性业务操作,为企业和用户带来巨大的损失。

抓包方法

  • 移动应用安全基础篇:APP抓包姿势总结:https://www.freebuf.com/column/207041.html
  • Android硬核https抓包, 在多个app亲测ok, 自定义ssl也无用:https://www.52pojie.cn/thread-1405917-1-1.html
  • 安卓应用层抓包通杀脚本:https://github.com/r0ysue/r0capture
            安卓应用层协议/框架通杀抓包:实战篇:https://www.sohu.com/a/445865909_120045376
  • 安卓应用层抓包通杀脚本发布:https://bbs.pediy.com/thread-264283.htm
                         frida_ssl_logger:https://github.com/NetCapture/frida_ssl_logger
  • 安卓强制代理。例如:proxydroid (代理机器人) :https://github.com/madeye/proxydroid   apk下载:http://www.xfdown.com/soft/120355.html

JustTrustMePlus:https://github.com/Mocha-L/JustTrustMePlus

自识别类名 自动化Hook JustTrustMe 升级版:https://bbs.pediy.com/thread-254114.htm

重点说下手机抓包

  1. JustTrustMePlus 是升级版能够对抗 okhttp 混淆加密
  2. 对于某些 apk 无法使用 charles 抓包(我也不知道原因)
  3. 手机端抓包可以使用 httpCarry

APP 抓包和微信小程序抓包-Charles 的精简使用教程:https://blog.csdn.net/liqing0013/article/details/83010531

解决安卓手机 charles 抓包网络请求 https 抓包证书认证不通过:https://www.cnblogs.com/wuxianyu/p/14271564.html

Android 使用 Wireshark 抓包:https://github.com/lasting-yang/frida_bypass_ssl_example/tree/master/Android/android_wireshark_tls

frida hook 住 okhttp 实现抓包:https://github.com/siyujie/OkHttpLogger-Frida

① 首先将 okhttpfind.dex 拷贝到 /data/local/tmp/ 目录下。执行命令启动 frida -U -l okhttp_poker.js -f com.example.demo --no-pause 可追加 -o [output filepath]保存到文件。注意: -f 参数是不管 app 启动没有,都直接启动 app,如果 app 已经启动,则可以使用 -F 参数,直接附加到 已经启动的 app 上

(  )

② 调用函数开始执行

  • find() 要等完全启动并执行过网络请求后再进行调用
  • hold() 要等完全启动再进行调用
  • history() & resend() 只有可以重新发送的请求

坑、坑、坑、坑、坑、坑:

  • 1. https://github.com/siyujie/OkHttpLogger-Frida:这个使用 -F 参数可以成功抓到包。( okhtpfind.dex 内包含了 更改了包名的okio以及Gson
  • 2. https://bbs.pediy.com/thread-252129.htm:这个使用 -F 没法抓到包,使用 -f 参数可以抓到包。( 这个没有 更改包名 okio以及Gson

打印堆栈:

function showStacks() {send(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
}

在 okhttp_poker.js 文件中的 printerRequest 函数中添加打印堆栈,就可以在打印抓包时候,把堆栈调用也打印出来

运行截图:

抓包结果:

okhttp_poker.js

function showStacks() {send(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
}function hook_okhttp3(classLoader) {Java.perform(function () {console.log("开始执行注入");var ByteString = classLoader.use("com.android.okhttp.okio.ByteString");console.log("com.android.okhttp.okio.ByteString ---> load");var Buffer = classLoader.use("com.android.okhttp.okio.Buffer");console.log("com.android.okhttp.okio.Buffer ---> load");var Interceptor = classLoader.use("okhttp3.Interceptor");console.log("okhttp3.Interceptor ---> load");var MyInterceptor = Java.registerClass({name: "okhttp3.MyInterceptor",implements: [Interceptor],methods: {intercept: function (chain) {var request = chain.request();try {console.log("#################################################################################")showStacks();console.log("\n")console.log("MyInterceptor.intercept onEnter:", request, "\nrequest headers:\n", request.headers());var requestBody = request.body();var contentLength = requestBody ? requestBody.contentLength() : 0;if (contentLength > 0) {var BufferObj = Buffer.$new();requestBody.writeTo(BufferObj);try {console.log("\nrequest body String:\n", BufferObj.readString(), "\n");} catch (error) {try {console.log("\nrequest body ByteString:\n", ByteString.of(BufferObj.readByteArray()).hex(), "\n");} catch (error) {console.log("error 1:", error);}}}} catch (error) {console.log("error 2:", error);}console.log("#################################################################################")console.log("\n")var response = chain.proceed(request);try {console.log("MyInterceptor.intercept onLeave:", response, "\nresponse headers:\n", response.headers());var responseBody = response.body();var contentLength = responseBody ? responseBody.contentLength() : 0;if (contentLength > 0) {console.log("\nresponsecontentLength:", contentLength, "responseBody:", responseBody, "\n");var ContentType = response.headers().get("Content-Type");console.log("ContentType:", ContentType);if (ContentType.indexOf("video") == -1) {if (ContentType.indexOf("application") == 0) {var source = responseBody.source();if (ContentType.indexOf("application/zip") != 0) {try {console.log("\nresponse.body StringClass\n", source.readUtf8(), "\n");} catch (error) {try {console.log("\nresponse.body ByteString\n", source.readByteString().hex(), "\n");} catch (error) {console.log("error 4:", error);}}}}}}} catch (error) {console.log("error 3:", error);}console.log("#################################################################################")console.log("\n")return response;}}});var ArrayList = classLoader.use("java.util.ArrayList");var OkHttpClient = classLoader.use("okhttp3.OkHttpClient");console.log(OkHttpClient);OkHttpClient.$init.overload('okhttp3.OkHttpClient$Builder').implementation = function (Builder) {console.log("OkHttpClient.$init:", this, Java.cast(Builder.interceptors(), ArrayList));this.$init(Builder);};var MyInterceptorObj = MyInterceptor.$new();var Builder = classLoader.use("okhttp3.OkHttpClient$Builder");console.log(Builder);Builder.build.implementation = function () {this.interceptors().clear();//var MyInterceptorObj = MyInterceptor.$new();this.interceptors().add(MyInterceptorObj);var result = this.build();return result;};Builder.addInterceptor.implementation = function (interceptor) {this.interceptors().clear();//var MyInterceptorObj = MyInterceptor.$new();this.interceptors().add(MyInterceptorObj);return this;//return this.addInterceptor(interceptor);};console.log("hook_okhttp3...");});
}Java.perform(function() {var application = Java.use("android.app.Application");application.attach.overload('android.content.Context').implementation = function(context) {var result = this.attach(context); // 先执行原来的attach方法var classloader = context.getClassLoader(); // 获取classloaderJava.classFactory.loader = classloader;hook_okhttp3(Java.classFactory);}});

okhttp_poker.js

/**
使用说明
首先将 okhttpfind.dex 拷贝到 /data/local/tmp/ 目录下
例:frida -U -l okhttp_poker.js -f com.example.demo --no-pause
接下来使用okhttp的所有请求将被拦截并打印出来;
扩展函数:find()                                         检查是否使用了Okhttp & 是否可能被混淆 & 寻找okhttp3关键类及函数switchLoader(\"okhttp3.OkHttpClient\")         参数:静态分析到的okhttpclient类名hold()                                         开启HOOK拦截history()                                      打印可重新发送的请求				resend(index)                                  重新发送请求		备注 : okhtpfind.dex 内包含了 更改了包名的okio以及Gson,以及Java写的寻找okhttp特征的代码。
okhttpfind.dex 源码链接 https://github.com/siyujie/okhttp_find原理:由于所有使用的okhttp框架的App发出的请求都是通过RealCall.java发出的,那么我们可以hook此类拿到request和response,
也可以缓存下来每一个请求的call对象,进行再次请求,所以选择了此处进行hook。*/
var Cls_Call = "okhttp3.Call";
var Cls_CallBack = "okhttp3.Callback";
var Cls_OkHttpClient = "okhttp3.OkHttpClient";
var Cls_Request = "okhttp3.Request";
var Cls_Response = "okhttp3.Response";
var Cls_ResponseBody = "okhttp3.ResponseBody";
var Cls_okio_Buffer = "okio.Buffer";
var F_header_namesAndValues = "namesAndValues";
var F_req_body = "body";
var F_req_headers = "headers";
var F_req_method = "method";
var F_req_url = "url";
var F_rsp$builder_body = "body";
var F_rsp_body = "body";
var F_rsp_code = "code";
var F_rsp_headers = "headers";
var F_rsp_message = "message";
var F_rsp_request = "request";
var M_CallBack_onFailure = "onFailure";
var M_CallBack_onResponse = "onResponse";
var M_Call_enqueue = "enqueue";
var M_Call_execute = "execute";
var M_Call_request = "request";
var M_Client_newCall = "newCall";
var M_buffer_readByteArray = "readByteArray";
var M_contentType_charset = "charset";
var M_reqbody_contentLength = "contentLength";
var M_reqbody_contentType = "contentType";
var M_reqbody_writeTo = "writeTo";
var M_rsp$builder_build = "build";
var M_rspBody_contentLength = "contentLength";
var M_rspBody_contentType = "contentType";
var M_rspBody_create = "create";
var M_rspBody_source = "source";
var M_rsp_newBuilder = "newBuilder";//----------------------------------
var JavaStringWapper = null;
var JavaIntegerWapper = null;
var JavaStringBufferWapper = null;
var GsonWapper = null;
var ListWapper = null;
var ArrayListWapper = null;
var ArraysWapper = null;
var CharsetWapper = null;
var CharacterWapper = null;var OkioByteStrngWapper = null;
var OkioBufferWapper = null;var OkHttpClientWapper = null;
var ResponseBodyWapper = null;
var BufferWapper = null;
var Utils = null;
//----------------------------------
var CallCache = []
var hookedArray = []
var filterArray = ["JPG", "jpg", "PNG", "png", "WEBP", "webp", "JPEG", "jpeg", "GIF", "gif",".zip", ".data"]function buildNewResponse(responseObject) {var newResponse = null;Java.perform(function () {try {var logString = JavaStringBufferWapper.$new()logString.append("").append("\n");logString.append("┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────").append("\n");newResponse = printAll(responseObject, logString)logString.append("└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────").append("\n");logString.append("").append("\n");console.log(logString)} catch (error) {console.log("printAll ERROR : " + error);}})return newResponse;
}function printAll(responseObject, logString) {try {var request = getFieldValue(responseObject, F_rsp_request)printerRequest(request, logString)} catch (error) {console.log("print request error : ", error.stack)return responseObject;}var newResponse = printerResponse(responseObject, logString)return newResponse;
}function printerRequest(request, logString) {var defChatset = CharsetWapper.forName("UTF-8")//URLvar httpUrl = getFieldValue(request, F_req_url)logString.append("| URL: " + httpUrl).append("\n")logString.append("|").append("\n")logString.append("| Method: " + getFieldValue(request, F_req_method)).append("\n")logString.append("|").append("\n")var requestBody = getFieldValue(request, F_req_body);var hasRequestBody = trueif (null == requestBody) {hasRequestBody = false}//Headersvar requestHeaders = getFieldValue(request, F_req_headers)var headersList = headersToList(requestHeaders)var headersSize = getHeaderSize(headersList)logString.append("| Request Headers: ").append("" + headersSize).append("\n")if (hasRequestBody) {var requestBody = getWrapper(requestBody)var contentType = requestBody[M_reqbody_contentType]()if (null != contentType) {logString.append("|   ┌─" + "Content-Type: " + contentType).append("\n")}var contentLength = requestBody[M_reqbody_contentLength]()if (contentLength != -1) {var tag = headersSize == 0 ? "└─" : "┌─"logString.append("|   " + tag + "Content-Length: " + contentLength).append("\n")}}if (headersSize == 0) {logString.append("|     no headers").append("\n")}for (var i = 0; i < headersSize; i++) {var name = getHeaderName(headersList, i)if (!JavaStringWapper.$new("Content-Type").equalsIgnoreCase(name) && !JavaStringWapper.$new("Content-Length").equalsIgnoreCase(name)) {var value = getHeaderValue(headersList, i)var tag = i == (headersSize - 1) ? "└─" : "┌─"logString.append("|   " + tag + name + ": " + value).append("\n")}}var shielded = filterUrl(httpUrl.toString())if (shielded) {logString.append("|" + "     File Request Body Omit.....").append("\n")return;}logString.append("|").append("\n")if (!hasRequestBody) {logString.append("|" + "--> END ").append("\n")} else if (bodyEncoded(headersList)) {logString.append("|" + "--> END  (encoded body omitted > bodyEncoded)").append("\n")} else {logString.append("| Request Body:").append("\n")var buffer = BufferWapper.$new()requestBody[M_reqbody_writeTo](buffer)var reqByteString = getByteString(buffer)var charset = defChatsetvar contentType = requestBody[M_reqbody_contentType]()if (null != contentType) {var appcharset = contentType[M_contentType_charset](defChatset);if (null != appcharset) {charset = appcharset;}}//LOG Request Bodytry {if (isPlaintext(reqByteString)) {logString.append(splitLine(readBufferString(reqByteString, charset), "|   ")).append("\n")logString.append("|").append("\n")logString.append("|" + "--> END ").append("\n")} else {logString.append(splitLine(hexToUtf8(reqByteString.hex()), "|   ")).append("\n")logString.append("|").append("\n");logString.append("|" + "--> END  (binary body omitted -> isPlaintext)").append("\n")}} catch (error) {logString.append(splitLine(hexToUtf8(reqByteString.hex()), "|   ")).append("\n")logString.append("|").append("\n");logString.append("|" + "--> END  (binary body omitted -> isPlaintext)").append("\n")}}logString.append("|").append("\n");
}function printerResponse(response, logString) {var newResponse = null;try {var defChatset = CharsetWapper.forName("UTF-8")var request = getFieldValue(response, F_rsp_request)var url = getFieldValue(request, F_req_url)var shielded = filterUrl(url.toString())if (shielded) {logString.append("|" + "     File Response Body Omit.....").append("\n")return response;}//URLlogString.append("| URL: " + url).append("\n")logString.append("|").append("\n")logString.append("| Status Code: " + getFieldValue(response, F_rsp_code) + " / " + getFieldValue(response, F_rsp_message)).append("\n")logString.append("|").append("\n")var responseBodyObj = getFieldValue(response, F_rsp_body)var responseBody = getWrapper(responseBodyObj)var contentLength = responseBody[M_rspBody_contentLength]()//Headersvar resp_headers = getFieldValue(response, F_rsp_headers)var respHeadersList = headersToList(resp_headers)var respHeaderSize = getHeaderSize(respHeadersList)logString.append("| Response Headers: ").append("" + respHeaderSize).append("\n")if (respHeaderSize == 0) {logString.append("|     no headers").append("\n")}for (var i = 0; i < respHeaderSize; i++) {var tag = i == (respHeaderSize - 1) ? "└─" : "┌─"logString.append("|   " + tag + getHeaderName(respHeadersList, i) + ": " + getHeaderValue(respHeadersList, i)).append("\n")}//Bodyvar content = "";var nobody = !hasBody(response, respHeadersList)if (nobody) {logString.append("| No Response Body : " + response).append("\n")logString.append("|" + "<-- END HTTP").append("\n")} else if (bodyEncoded(respHeadersList)) {logString.append("|" + "<-- END HTTP (encoded body omitted)").append("\n")} else {logString.append("| ").append("\n");logString.append("| Response Body:").append("\n")var source = responseBody[M_rspBody_source]()var rspByteString = getByteString(source)var charset = defChatsetvar contentType = responseBody[M_rspBody_contentType]()if (null != contentType) {var appcharset = contentType[M_contentType_charset](defChatset)if (null != appcharset) {charset = appcharset}}//newResponsevar mediaType = responseBody[M_rspBody_contentType]()var newBody = null;try {newBody = ResponseBodyWapper[M_rspBody_create](mediaType, rspByteString.toByteArray())} catch (error) {newBody = ResponseBodyWapper[M_rspBody_create](mediaType, readBufferString(rspByteString, charset))}var newBuilder = null;if ("" == M_rsp_newBuilder) {var ResponseBuilderClazz = response.class.getDeclaredClasses()[0]newBuilder = Java.use(ResponseBuilderClazz.getName()).$new(response)} else {newBuilder = response[M_rsp_newBuilder]()}var bodyField = newBuilder.class.getDeclaredField(F_rsp$builder_body)bodyField.setAccessible(true)bodyField.set(newBuilder, newBody)newResponse = newBuilder[M_rsp$builder_build]()if (!isPlaintext(rspByteString)) {logString.append("|" + "<-- END HTTP (binary body omitted)").append("\n");}if (contentLength != 0) {try {var content = readBufferString(rspByteString, charset)logString.append(splitLine(content, "|   ")).append("\n")} catch (error) {logString.append(splitLine(hexToUtf8(rspByteString.hex()), "|   ")).append("\n")}logString.append("| ").append("\n");}logString.append("|" + "<-- END HTTP").append("\n");}} catch (error) {logString.append("print response error : " + error).append("\n")if (null == newResponse) {return response;}}return newResponse;
}/*** hex to string*/
function hexToUtf8(hex) {try {return decodeURIComponent('%' + hex.match(/.{1,2}/g).join('%'));} catch (error) {return "hex[" + hex + "]";}
}/***/
function getFieldValue(object, fieldName) {var field = object.class.getDeclaredField(fieldName);field.setAccessible(true)var fieldValue = field.get(object)if (null == fieldValue) {return null;}var FieldClazz = Java.use(fieldValue.$className)var fieldValueWapper = Java.cast(fieldValue, FieldClazz)return fieldValueWapper
}
/***/
function getWrapper(javaobject) {return Java.cast(javaobject, Java.use(javaobject.$className))
}/***/
function headersToList(headers) {var gson = GsonWapper.$new()var namesAndValues = getFieldValue(headers, F_header_namesAndValues)var jsonString = gson.toJson(namesAndValues)var namesAndValuesList = Java.cast(gson.fromJson(jsonString, ListWapper.class), ListWapper)return namesAndValuesList;
}function getHeaderSize(namesAndValuesList) {return namesAndValuesList.size() / 2
}function getHeaderName(namesAndValuesList, index) {return namesAndValuesList.get(index * 2)
}
function getHeaderValue(namesAndValuesList, index) {return namesAndValuesList.get((index * 2) + 1)
}function getByHeader(namesAndValuesList, name) {var nameString = JavaStringWapper.$new(name)Java.perform(function () {var length = namesAndValuesList.size()var nameByList = "";do {length -= 2;if (length < 0) {return null;}// console.log("namesAndValuesList: "+namesAndValuesList.$className)nameByList = namesAndValuesList.get(JavaIntegerWapper.valueOf(length).intValue())} while (!nameString.equalsIgnoreCase(nameByList));return namesAndValuesList.get(length + 1);})
}function bodyEncoded(namesAndValuesList) {if (null == namesAndValuesList) return false;var contentEncoding = getByHeader(namesAndValuesList, "Content-Encoding")var bodyEncoded = contentEncoding != null && !JavaStringWapper.$new("identity").equalsIgnoreCase(contentEncoding)return bodyEncoded}function hasBody(response, namesAndValuesList) {var request = getFieldValue(response, F_rsp_request)var m = getFieldValue(request, F_req_method);if (JavaStringWapper.$new("HEAD").equals(m)) {return false;}var Transfer_Encoding = "";var respHeaderSize = getHeaderSize(namesAndValuesList)for (var i = 0; i < respHeaderSize; i++) {if (JavaStringWapper.$new("Transfer-Encoding").equals(getHeaderName(namesAndValuesList, i))) {Transfer_Encoding = getHeaderValue(namesAndValuesList, i);break}}var code = getFieldValue(response, F_rsp_code)if (((code >= 100 && code < 200) || code == 204 || code == 304)&& response[M_rspBody_contentLength] == -1&& !JavaStringWapper.$new("chunked").equalsIgnoreCase(Transfer_Encoding)) {return false;}return true;
}function isPlaintext(byteString) {try {var bufferSize = byteString.size()var buffer = NewBuffer(byteString)for (var i = 0; i < 16; i++) {if (bufferSize == 0) {console.log("bufferSize == 0")break}var codePoint = buffer.readUtf8CodePoint()if (CharacterWapper.isISOControl(codePoint) && !CharacterWapper.isWhitespace(codePoint)) {return false;}}return true;} catch (error) {// console.log(error)// console.log(Java.use("android.util.Log").getStackTraceString(error))return false;}
}function getByteString(buffer) {var bytearray = buffer[M_buffer_readByteArray]();var byteString = OkioByteStrngWapper.of(bytearray)return byteString;
}function NewBuffer(byteString) {var buffer = OkioBufferWapper.$new()byteString.write(buffer)return buffer;
}function readBufferString(byteString, chatset) {var byteArray = byteString.toByteArray();var str = JavaStringWapper.$new(byteArray, chatset)return str;
}function splitLine(string, tag) {var newSB = JavaStringBufferWapper.$new()var newString = JavaStringWapper.$new(string)var lineNum = Math.ceil(newString.length() / 150)for (var i = 0; i < lineNum; i++) {var start = i * 150;var end = (i + 1) * 150newSB.append(tag)if (end > newString.length()) {newSB.append(newString.substring(start, newString.length()))} else {newSB.append(newString.substring(start, end))}newSB.append("\n")}var lineStr = "";if (newSB.length() > 0) {lineStr = newSB.deleteCharAt(newSB.length() - 1).toString()}return lineStr
}/*** */
function alreadyHook(str) {for (var i = 0; i < hookedArray.length; i++) {if (str == hookedArray[i]) {return true;}}return false;
}/*** */
function filterUrl(url) {for (var i = 0; i < filterArray.length; i++) {if (url.indexOf(filterArray[i]) != -1) {// console.log(url + " ?? " + filterArray[i])return true;}}return false;
}function hookRealCall(realCallClassName) {Java.perform(function () {console.log(" ...........  hookRealCall  : " + realCallClassName)var RealCall = Java.use(realCallClassName)if ("" != Cls_CallBack) {//异步RealCall[M_Call_enqueue].overload(Cls_CallBack).implementation = function (callback) {// console.log("-------------------------------------HOOK SUCCESS 异步--------------------------------------------------")var realCallBack = Java.use(callback.$className)realCallBack[M_CallBack_onResponse].overload(Cls_Call,Cls_Response).implementation = function(call, response){var newResponse = buildNewResponse(response)this[M_CallBack_onResponse](call,newResponse)}this[M_Call_enqueue](callback)realCallBack.$dispose}}//同步  RealCall[M_Call_execute].overload().implementation = function () {// console.log("-------------------------------------HOOK SUCCESS 同步--------------------------------------------------")var response = this[M_Call_execute]()var newResponse = buildNewResponse(response)return newResponse;}})
}/*** check className & filter*/
function checkClass(name) {if (name.startsWith("com.")|| name.startsWith("cn.")|| name.startsWith("io.")|| name.startsWith("org.")|| name.startsWith("android")|| name.startsWith("kotlin")|| name.startsWith("[")|| name.startsWith("java")|| name.startsWith("sun.")|| name.startsWith("net.")|| name.indexOf(".") < 0|| name.startsWith("dalvik")) {return false;}return true;
}/**
* print request history
*/
function history() {Java.perform(function () {try {console.log("")console.log("History Size : " + CallCache.length)for (var i = 0; i < CallCache.length; i++) {var call = CallCache[i]if ("" != M_Call_request) {console.log("-----> index[" + i + "]" + " >> " + call[M_Call_request]())} else {console.log("-----> index[" + i + "]" + "    ????  M_Call_execute = \"\"")}console.log("")}console.log("")} catch (error) {console.log(error)}})
}/**
* resend request
*/
function resend(index) {Java.perform(function () {try {console.log("resend >> " + index)var call = CallCache[index]if ("" != M_Call_execute) {call[M_Call_execute]()} else {console.log("M_Call_execute = null")}} catch (error) {console.log("Error : " + error)}})
}/*** 开启HOOK拦截*/
function hold() {Java.perform(function () {//Utils = Java.use("com.singleman.okhttp.Utils")//Init commonJavaStringWapper = Java.use("java.lang.String")JavaStringBufferWapper = Java.use("java.lang.StringBuilder")JavaIntegerWapper = Java.use("java.lang.Integer")GsonWapper = Java.use("com.singleman.gson.Gson")ListWapper = Java.use("java.util.List")ArraysWapper = Java.use("java.util.Arrays")ArrayListWapper = Java.use("java.util.ArrayList")CharsetWapper = Java.use("java.nio.charset.Charset")CharacterWapper = Java.use("java.lang.Character")OkioByteStrngWapper = Java.use("com.singleman.okio.ByteString")OkioBufferWapper = Java.use("com.singleman.okio.Buffer")//Init OKHTTPOkHttpClientWapper = Java.use(Cls_OkHttpClient)ResponseBodyWapper = Java.use(Cls_ResponseBody)BufferWapper = Java.use(Cls_okio_Buffer)//Start HookOkHttpClientWapper[M_Client_newCall].overload(Cls_Request).implementation = function (request) {var call = this[M_Client_newCall](request)try {CallCache.push(call["clone"]())} catch (error) {console.log("not fount clone method!")}var realCallClassName = call.$classNameif (!alreadyHook(realCallClassName)) {hookedArray.push(realCallClassName)hookRealCall(realCallClassName)}return call;}})
}function switchLoader(clientName) {Java.perform(function () {if ("" != clientName) {try {var clz = Java.classFactory.loader.findClass(clientName)console.log("")console.log(">>>>>>>>>>>>>  ", clz, "  <<<<<<<<<<<<<<<<")} catch (error) {console.log(error)Java.enumerateClassLoaders({onMatch: function (loader) {try {if (loader.findClass(clientName)) {Java.classFactory.loader = loaderconsole.log("")console.log("Switch ClassLoader To : ", loader)console.log("")}} catch (error) {// console.log(error)}},onComplete: function () {console.log("")console.log("Switch ClassLoader Complete !")console.log("")}})}}Java.openClassFile("/data/local/tmp/okhttpfind.dex").load()})
}/*** find & print used location*/
function find() {Java.perform(function () {ArraysWapper = Java.use("java.util.Arrays")ArrayListWapper = Java.use("java.util.ArrayList")var isSupport = false;var clz_Protocol = null;try {var clazzNameList = Java.enumerateLoadedClassesSync()if (clazzNameList.length == 0) {console.log("ERROR >> [enumerateLoadedClasses] return null !!!!!!")return}for (var i = 0; i < clazzNameList.length; i++) {var name = clazzNameList[i]if (!checkClass(name)) {continue}try {var loadedClazz = Java.classFactory.loader.loadClass(name);if (loadedClazz.isEnum()) {var Protocol = Java.use(name);var toString = ArraysWapper.toString(Protocol.values());if (toString.indexOf("http/1.0") != -1&& toString.indexOf("http/1.1") != -1&& toString.indexOf("spdy/3.1") != -1&& toString.indexOf("h2") != -1) {clz_Protocol = loadedClazz;break;}}} catch (error) {}}if (null == clz_Protocol) {console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~ 寻找okhttp特征失败,请确认是否使用okhttp ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")return}//enum values >> Not to be confused with!var okhttp_pn = clz_Protocol.getPackage().getName();var likelyOkHttpClient = okhttp_pn + ".OkHttpClient"try {var clz_okclient = Java.use(likelyOkHttpClient).classif (null != clz_okclient) {console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 未 混 淆 (仅参考)~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")isSupport = true;}} catch (error) {console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 被 混 淆 (仅参考)~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")isSupport = true;}} catch (error) {console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~未使用okhttp~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")isSupport = false;}if (!isSupport) {console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~ 寻找okhttp特征失败,请确认是否使用okhttp ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")return}var likelyClazzList = ArrayListWapper.$new()for (var i = 0; i < clazzNameList.length; i++) {var name = clazzNameList[i]if (!checkClass(name)) {continue}try {var loadedClazz = Java.classFactory.loader.loadClass(name);likelyClazzList.add(loadedClazz)} catch (error) {}}console.log("likelyClazzList size :" + likelyClazzList.size())if (likelyClazzList.size() == 0) {console.log("Please make a network request and try again!")}console.log("")console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Start Find~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")console.log("")try {var OkHttpFinder = Java.use("com.singleman.okhttp.OkHttpFinder")OkHttpFinder.getInstance().findClassInit(likelyClazzList)console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Find Result~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")var OkCompatClazz = Java.use("com.singleman.okhttp.OkCompat").classvar fields = OkCompatClazz.getDeclaredFields();for (var i = 0; i < fields.length; i++) {var field = fields[i]field.setAccessible(true);var name = field.getName()var value = field.get(null)console.log("var " + name + " = \"" + value + "\";")}console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Find Complete~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")} catch (error) {console.log(error)//console.log(Java.use("android.util.Log").getStackTraceString(error))}})
}/***/
function main() {Java.perform(function () {Java.openClassFile("/data/local/tmp/okhttpfind.dex").load()var version = Java.use("com.singleman.SingleMan").class.getDeclaredField("version").get(null)console.log("");console.log("------------------------- OkHttp Poker by SingleMan [" + version + "]------------------------------------");console.log("API:")console.log("   >>>  find()                                         检查是否使用了Okhttp & 是否可能被混淆 & 寻找okhttp3关键类及函数");console.log("   >>>  switchLoader(\"okhttp3.OkHttpClient\")           参数:静态分析到的okhttpclient类名");console.log("   >>>  hold()                                         开启HOOK拦截");console.log("   >>>  history()                                      打印可重新发送的请求");console.log("   >>>  resend(index)                                  重新发送请求");console.log("----------------------------------------------------------------------------------------");})
}setImmediate(main)

http 抓包

  • 正常代理来抓
  • 强制代理抓
  • JustTrustMe 来抓
  • JustTrustMePlus 来抓
  • frida_ssl_logger 来抓
  • r0capture 来抓
  • 硬核 https 抓包


3.1 推荐抓包环境

由上所述,抓包是每一位安全工程师必须掌握的技能。而抓包一般又分为以下两种情形:

  • 应用层:Http(s)协议抓包。
            如果是抓应用层Http(s),推荐的专业工具是BurpSuite,如果只是想简单的抓包、用的舒服轻松,也可以使用花瓶(Charles)。
            不推荐使用 fiddle,因为它无法导入客户端证书(p12、Client SSL Certificates),对于服务器校验客户端证书的情况无法 Bypass;
  • 会话层:Socket 端口通信抓包。
            如果是会话层抓包,则选择 tcpdump 和 WireShark 相组合的方式。

使用 jnettop 还可以实时查看流量走势和对方IP地址,更为直观和生动。

在手机上设置代理时,推荐使用 VPN 来将流量导出到抓包软件上,而不是通过给 WIFI 设置 HTTP 代理的方式。使用 VPN 可以同时抓到 Http(s) 和 Socket 的包,且不管其来自 Java层还是so层。我们常用的代理软件是老牌的 Postern,开 VPN 服务通过连接到开启 Socks5 服务端的抓包软件,将流量导出去。 

当然有些应用会使用 System.getProperty("http.proxyHost")、System.getProperty("http.proxyPort"); 这两个API来查看当前系统是否挂了VPN,这时候只能用 Frida 或 Xposed 来 hook 这个接口、修改其返回值,或者重打包来 nop 掉。当然还有一种最为终极、最为强悍的方法,那就是制作路由器,抓所有过网卡的包。

制作路由器的方法也很简单,给笔记本电脑装 Kali Linux,eth0口插网线上网,wlan0口使用系统自带的热点功能,手机连上热点上网。史上最强,安卓应用是无法对抗的。

另外,曾经有人问我,像这样的一个场景如何抓包:

问:最近在分析手机搬家类软件的协议,不知道用什么去抓包,系统应用,不可卸载那种。搬家场景:两台手机打开搬家软件,一台会创建热点,另一台手机连接该热点后,通过搬家软件传输数据。求大佬指点抓包方法。

这个场景是有点难度的,我们把开热点的手机假设为A,连接热点的手机假设为B。另外准备一台抓包电脑,连接上A开的热点。在B上安装VPN软件Postern,服务器设置为抓包电脑,这样B应该可以正常连接到A,B的所有流量也是从抓包电脑走的,可以抓到所有的包。

在抓包的对抗上体现的也是两个原则,一是理解的越成熟思路越多,二是对抗的战场越深上层越无法防御。

3.2 Http(s) 多场景分析

从防护的强度来看,Https 的强度是远远大于 Http 的;从大型分布式 C/S 架构的设计来看,如果服务器数量非常多、app版本众多,app在实现Https的策略上通常会采取客户端校验服务器证书的策略,如果服务器数量比较少,全国就那么几台、且app版本较少、对app版本管控较为严格,app在实现Https的策略时会加上服务器校验客户端证书的策略。

接下来我们具体分析每一种情况。

3.2.1 Http 抓包

对于Http的抓包,只要在电脑的Charles上配置好Socks5服务器,手机上用Postern开启VPN连上电脑上的Charles的Socks5服务器,所有流量即可导出到Charles上。当然使用BurpSuite也是一样的道理。至于具体的操作步骤网上文档浩如烟海,读者可以自行取阅。

一般大型app、服务器数量非常多的,尤其还配置了多种CDN在全国范围、三网内进行内容分发和加速分发的,通常app里绝大多数内容都是走的Http。

当然他们会在最关键的业务上,比如用户登录时,配置Https协议,来保证最基本的安全。

3.2.2 Https客户端校验服务器

这时候我们抓 app 的 Http 流量的时候一切正常,图片、视频、音乐都直接下载和转储。

但是作为用户要登录的时候,就会发现抓包失败,这时候开启 Charles 的 SSL 抓包功能,手机浏览器输入Charles的证书下载地址chls.pro/ssl,下载证书并安装到手机中。

注意在高版本的安卓上,用户安装的证书并不会安装到系统根证书目录中去,需要root手机后将用户安装的证书移动到系统根证书目录中去,具体操作步骤网上非常多,这里不再赘述。

当 Charles 的证书安装到系统根目录中去之后,系统就会信任来自Charles的流量包了,我们的抓包过程就会回归正常。

当然,这里还是会有读者疑惑,为什么导入Charles的证书之后,app抓包就正常了呢?

应用层 Https 抓包的根本原理

这里我们就需要理解一下应用层 Https 抓包的根本原理,

见下图2-15(会话层Socket抓包并不是这个原理,后文会介绍Socket抓包的根本原理)。

有了 Charles 置于中间之后,本来 C/S 架构的通信过程会 “分裂” 为两个独立的通信过程,app本来验证的是服务器的证书,服务器的证书手机的根证书是认可的,直接内置的;但是分裂成两个独立的通信过程之后,app验证的是Charles的证书,它的证书手机根证书并不认可,它并不是由手机内置的权威根证书签发机构签发的,所以手机不认,然后app也不认;所以我们要把Charles的证书导入到手机根证书目录中去,这样手机就会认可,如果app没有进行额外的校验(比如在代码中对该证书进行校验,也就是SSL pinning系列API,这种情况下一小节具体阐述)的话,app也会直接认可接受。

3.3.3 Https服务器校验客户端

既然 app 客户端会校验服务器证书,那么服务器可不可能校验app客户端证书呢?答案是肯定的。

在许多业务非常聚焦并且当单一,比如行业应用、银行、公共交通、游戏等行业,C/S架构中服务器高度集中,对应用的版本控制非常严格,这时候就会在服务器上部署对app内置证书的校验代码。

上一小节中已经看到,单一通信已经分裂成两个互相独立的通信,这时候与服务器进行通信的已经不是app、而是Charles了,所以我们要将app中内置的证书导入到Charles中去。

这个操作通常需要完成两项内容:

  1. 找到证书文件
  2. 找到证书密码

找到证书文件很简单,一般 apk 进行解包,直接过滤搜索后缀名为 p12 的文件即可,一般常用的命令为 tree -NCfhl |grep -i p12,直接打印出 p12 文件的路径,当然也有一些 app 比较 “狡猾”,比如我们通过搜索 p12 没有搜到证书,然后看 jadx 反编译的源码得出它将证书伪装成 border_ks_19 文件,我们找到这个文件用 file 命令查看果然不是后缀名所显示的 png 格式,将其改成 p12 的后缀名尝试打开时要求输入密码,可见其确实是一个证书,见下图2-17。

想要拿到密码也很简单,一般在 jadx 反编译的代码中或者so库拖进IDA后可以看到硬编码的明文;也可以使用下面这一段脚本,直接打印出来,终于到了Frida派上用场的时候。

function hook_KeyStore_load() {Java.perform(function () {var StringClass = Java.use("java.lang.String");var KeyStore = Java.use("java.security.KeyStore");KeyStore.load.overload('java.security.KeyStore$LoadStoreParameter').implementation = function (arg0) {printStack("KeyStore.load1");console.log("KeyStore.load1:", arg0);this.load(arg0);};KeyStore.load.overload('java.io.InputStream', '[C').implementation = function (arg0, arg1) {printStack("KeyStore.load2");console.log("KeyStore.load2:", arg0, arg1 ? StringClass.$new(arg1) : null);this.load(arg0, arg1);};console.log("hook_KeyStore_load...");});
}

打印出来的效果如下图2-18,直接将密码打印了出来。

当然其实也并不一定非要用 Frida,用 Xposed 也可以,只是 Xposed 很久不更新了,最近流行的大趋势是 Frida

有了证书和密码之后,就可以将其导入到抓包软件中,在 Charles中是位于 Proxy→SSL Proxy Settings→Client Certificates→Add 添加新的证书,输入指定的域名或IP使用指定的证书即可,见下图2-19。

3.3 SSL Pinning Bypass

上文中我们还有一种情况没有分析,就是客户端并不会默认信任系统根证书目录中的证书,而是在代码里再加一层校验,这就是证书绑定机制——SSL pinning,如果这段代码的校验过不了,那么客户端还是会报证书错误。

Https客户端代码校验服务器证书

遇到这种情况的时候,我们一般有三种方式,当然目标是一样的,都是hook住这段校验的代码,使这段判断的机制失效即可。

方法1:hook 住 checkServerTrusted,将其所有重载都置空;

function hook_ssl() {Java.perform(function() {var ClassName = "com.android.org.conscrypt.Platform";var Platform = Java.use(ClassName);var targetMethod = "checkServerTrusted";var len = Platform[targetMethod].overloads.length;console.log(len);for(var i = 0; i < len; ++i) {Platform[targetMethod].overloads[i].implementation = function () {console.log("class:", ClassName, "target:", targetMethod, " i:", i, arguments);//printStack(ClassName + "." + targetMethod);}}});
}

方法2:使用 objection,直接将 SSL pinning 给 disable 掉

# android sslpinning disable

方法3:如果还有一些情况没有覆盖的话,可以来看看大佬的代码

  • 目录 ObjectionUnpinningPlus 增加了 ObjectionUnpinning 没覆盖到的锁定场景.(objection)
    • 使用方法1 attach : frida -U com.example.mennomorsink.webviewtest2 —no-pause -l hooks.js
    • 使用方法2 spawn : python application.py com.example.mennomorsink.webviewtest2
    • 更为详细使用方法:参考我的文章 Frida.Android.Practice(ssl unpinning) 实战ssl pinning bypass 章节 .
  • ObjectionUnpinningPlus hook list:
    • SSLcontext(ART only)
    • okhttp
    • webview
    • XUtils(ART only)
    • httpclientandroidlib
    • JSSE
    • network_security_config (android 7.0+)
    • Apache Http client (support partly)
    • OpenSSLSocketImpl
    • TrustKit

应该可以覆盖到目前已知的所有种类的证书绑定了。

3.4 Socket 多场景分析

当我们在使用 Charles 进行抓包的时候,会发现针对某些 IP 的数据传输一直显示 CONNECT,无法 Complete,显示 Sending request body,并且数据包大小持续增长,这时候说明我们遇到了Socket端口通信。

Socket 端口通信运行在会话层,并不是应用层,Socket抓包的原理与应用层Http(s)有着显著的区别。准确的说,Http(s)抓包是真正的“中间人”抓包,而Socket抓包是在接口上进行转储;Http(s)抓包是明显的将一套C/S架构通信分裂成两套完整的通信过程,而Socket抓包是在接口上将发送与接收的内容存储下来,并不干扰其原本的通信过程。

对于安卓应用来说,Socket通信天生又分为两种JavaSocket通信和NativeSocket通信。

  • Java层:使用的是java.net.InetAddressjava.net.Socketjava.net.ServerSocket等类,与证书绑定的情形类似,也可能存在着自定义框架的Socket通信,这时候就需要具体情况具体分析,比如谷歌的protobuf框架等;
  • Native层:一般使用的是C Socket API,一般hooksend()recv()函数可以得到其发送和接受的内容

抓包方法分为三种,接口转储、驱动转储和路由转储:

  • 接口转储:比如给outputStream.writehook,把内容存下来看看,可能是经过压缩、或加密后的包,毕竟是二进制,一切皆有可能;
  • 驱动转储:使用tcpdump将经过网口驱动时的数据包转储下来,再使用Wireshark进行分析;
  • 路由转储:自己做个路由器,运行jnettop,观察实时进过的流量和IP,可以使用WireShark实时抓包,也可以使用tcpdump抓包后用WireShark分析。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/495256.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

谷歌李飞飞:我们依旧站在人工智能研究的起点

来源&#xff1a;机器人大讲堂摘要&#xff1a;8 年来&#xff0c;在 ImageNet 数据集的训练下&#xff0c;人工智能对于图像识别的准确度整整提高了 10 倍&#xff0c;甚至超越了人类视觉本身。但李飞飞认为&#xff0c;我们对于人工智能的研究仍在起点上。说起人工智能&#…

Android Intent 用法总结

From&#xff1a;https://www.jianshu.com/p/67d99a82509b Android 中提供了 Intent 机制来协助应用间的交互与通讯&#xff0c;Intent 负责对应用中一次操作的动作、动作涉及数据、附加数据进行描述&#xff0c;Android 则根据此 Intent 的描述&#xff0c;负责找到对应的组件…

FRIDA - API使用篇:rpc、Process、Module、Memory 使用方法及示例

官方 API (JavaScript API)&#xff1a;https://frida.re/docs/javascript-api/ From&#xff1a; ( FRIDA-API使用篇 )&#xff1a;https://www.anquanke.com/post/id/195215 前言 在这篇文章中来对其官方的一些非常常用的 API 进行学习。所谓工欲善其事&#xff0c;必先利其…

Entity Framework 实体关系总结(转)

通过 Entiy Framework实践系列文章&#xff0c;理了理 Entity Framework 的实体关系。 为什么要写文章来理清这些关系&#xff1f;“血”的教训啊&#xff0c;刚开始使用 Entity Framework 的时候&#xff0c;由于没有静下心来认真理清关系&#xff0c;走了一些"痛不欲生&…

技术架构分析:攻克Dota2的OpenAI-Five

来源&#xff1a;CreateAMind摘要&#xff1a;OpenAI昨日发布研究成果&#xff0c;宣布Dota2 5v5在限定条件下&#xff08;英雄阵容固定&#xff0c;部分道具和功能禁用&#xff09;战胜人类半职业选手。本文主要对其模型技术架构做一些分析总结。一、 模型输入与输出模型的输入…

Redis基础-Redis概念及常见命令

1.nosql数据库 NoSQL数据库是一种提供了非关系型数据存储的数据库系统&#xff0c;与传统的关系型数据库&#xff08;如SQL数据库&#xff09;不同。NoSQL数据库的特点是灵活性高&#xff0c;能够处理结构化、半结构化或非结构化数据。它们通常用于大数据和实时Web应用。NoSQL数…

Java 高级特性 --- 反射

From&#xff1a;Java 高级特性 --- 反射&#xff1a;https://www.jianshu.com/p/9be58ee20dee From&#xff1a;Java 基础之 --- 反射&#xff08;非常重要&#xff09;&#xff1a;https://blog.csdn.net/sinat_38259539/article/details/71799078 From&#xff1a;Java 高级…

G20国家科技竞争力大盘点,中国科研创新表现突出,人工智能变道超车

来源&#xff1a;科睿唯安中国科学院文献情报中心和科睿唯安6月25日在北京联合发布了《G20国家科技竞争格局之辩》系列报告&#xff0c;报告分为总体篇及人工智能专题篇&#xff08;下文有重点介绍&#xff09;&#xff0c;聚焦G20国家的科研产出规模、学术影响力、领域分布、国…

Java中泛型 Class<T>、T与Class<?>、 Object类和Class类、 object.getClass() 和 Object.class

From&#xff1a;Java中泛型 Class<T>、T 与 Class<?>、 Object类 和 Class类、 object.getClass() 和 Object.class &#xff1a;https://www.cnblogs.com/zhaoyanhaoBlog/p/9362267.html Class<T>和 Class<?>类型 有什么区别&#xff1a;https://…

智能驾驶是否会“运动式”发展

来源&#xff1a;中国科学网最近&#xff0c;无人驾驶车发生撞人致死事故再度引发公众恐慌。在近日举行的全球人工智能技术大会上&#xff0c;中国工程院院士李德毅表示&#xff0c;不管是无人驾驶还是有人驾驶&#xff0c;事故总是有的。实际上人类才是第一马路杀手&#xff0…

Java学习之java高级特性

From&#xff1a;https://blog.csdn.net/w252064/article/details/79923999 [Java高级特性详解]&#xff1a;https://blog.csdn.net/qq_37977176/article/details/78941649 菜鸟教程 之 Java 教程&#xff1a;https://www.runoob.com/java/java-tutorial.html 本部分内容主要…

任正非亲自指导下拍的视频,事关中国的未来

来源&#xff1a;华为中美贸易战开打之后&#xff0c;中国产业未来的出路在哪里&#xff1f;华为的答案是基础研究与基础教育。这则由华为创始人兼CEO任正非亲自指导下拍摄的视频&#xff0c;呼吁社会重视基础教育&#xff0c;让教师成为最伟大的职业&#xff0c;成为优秀青年的…

《科学》杂志做了一个清单,告诉你今年 10 个最重要的科技突破

来源&#xff1a;网络大数据摘要&#xff1a;著名杂志《科学》最近列出了一份清单&#xff0c;来告诉你哪些科技突破在 2015 年是最重要的。《科学》是美国科技促进会出版的一份学术期刊杂志&#xff0c;主要发布的内容是各种学术研究&#xff0c;以及相关的科学新闻和观点&…

[进阶] --- Python3 异步编程详解(史上最全篇)

[进阶] - Python3 异步编程详解&#xff1a;https://blog.csdn.net/lu8000/article/details/45025987 参考&#xff1a;http://aosabook.org/en/500L/a-web-crawler-with-asyncio-coroutines.html 木风卜雨&#xff1a;https://blog.csdn.net/lu8000 1 什么是异步编程 1.1 阻…

AI+医疗:基于模型的医疗应用大规模分析 | 腾讯AI Lab学术论坛演讲

来源&#xff1a;腾讯AI实验室摘要&#xff1a;3月15日&#xff0c;腾讯AI Lab第二届学术论坛在深圳举行&#xff0c;聚焦人工智能在医疗、游戏、多媒体内容、人机交互等四大领域的跨界研究与应用。3月15日&#xff0c;腾讯AI Lab第二届学术论坛在深圳举行&#xff0c;聚焦人工…

Python 并行编程

参考&#xff1a;python-parallel-programming-cookbook-cn&#xff1a;https://python-parallel-programmning-cookbook.readthedocs.io/zh_CN/latest/ 第一章 认识并行计算和Python 1. 介绍2. 并行计算的内存架构3. 内存管理4. 并行编程模型5. 如何设计一个并行程序6. 如何评…

自动驾驶技术之——虚拟场景数据库研究

来源&#xff1a;智车科技摘要&#xff1a;驾驶场景数据是智能网联汽车研发与测试的基础数据资源&#xff0c;是评价智能网联汽车功能安全的重要“案例库”与“习题集”&#xff0c;是重新定义智能汽车等级的关键数据依据。驾驶场景测试用例主要通过虚拟仿真环境及工具链进行复…

C++ 数据指针(-)

C指针探讨 &#xff08;一&#xff09;数据指针 指针&#xff0c;在C/C语言中一直是很受宠的&#xff1b;几乎找不到一个不使用指针的C/C应用。用于存储数据和程序的地址&#xff0c;这是指针的基本功能。用于指向整型数&#xff0c; 用整数指针(int*)&#xff1b;指向浮点数用…

【进阶】 --- 多线程、多进程、异步IO实用例子

【进阶】 --- 多线程、多进程、异步IO实用例子&#xff1a;https://blog.csdn.net/lu8000/article/details/82315576 python之爬虫_并发&#xff08;串行、多线程、多进程、异步IO&#xff09;&#xff1a;https://www.cnblogs.com/fat39/archive/2004/01/13/9044474.html Py…

何恺明CVPR演讲:深入理解ResNet和视觉识别的表示学习(41 PPT)

来源&#xff1a;专知摘要&#xff1a;今年CVPR 2018上&#xff0c;刚获得“TPAMI”年轻研究员奖的Facebook的Kaiming He做了一个叫“Learning Deep Representations for Visual Recognition”的讲座。在今年CVPR 2018上&#xff0c;刚获得“TPAMI”年轻研究员奖的Facebook的Ka…