转载:实用FRIDA进阶:内存漫游、hook anywhere、抓包:实用FRIDA进阶:内存漫游、hook anywhere、抓包 - 安全客,安全资讯平台
frida github 地址:https://github.com/frida/frida
objection github:https://github.com/sensepost/objection
objection pypi:objection · PyPI
本章中我们进一步介绍,大家在学习和工作中使用 Frida 的实际高频场景,比如:
最后介绍一些经常遇到的高频问题解决思路,希望可以切实地帮助到读者。
Frida hook工具 --- objection 使用:Frida hook工具---objection 使用_ST0new的博客-CSDN博客
更多命令行参数可以查看 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其中一个重载函数 指定参数类型 多个参数用逗号分隔
android hooking watch class_method 包名.类名.方法 "参数1,参数2"
# hook方法的参数、返回值和调用栈(–dump-args: 显示参数; --dump-return: 显示返回值; --dump-backtrace: 显示堆栈)
android hooking watch class_method 包名.类名.方法 --dump-args --dump-return --dump-backtrace
jobs list # 查看 hook 的任务有多少个
jobs kill jobid # 把正在 hook 的任务关闭
android heap search instances 包名.类名 --fresh # 搜索堆中的实例
android heap execute 地址(hashcode的地址) 方法名 # 调用实例的方法
memory list modules # 枚举内存中所有模块
memory list exports 文件名.so # 枚举模块中所有导出函数
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
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
( :基于frida的objection及其插件wallbreaker 命令列表 - 小天儿 - 博客园 )
cd
commands
clear
history
save
env
evaluate
exit
file
cat
download
upload
http
frida
import
ios
bundles
cookies
heap
hooking
info
jailbreak
keychain
monitor
nsurlcredentialstoragte
nsuserdefaults
pasteboard
plist
sslpinning
ui
jobs
kill
list
ls
memory
list
exports xx.so
modules
search
write
dump
all
from_base
android
clipboard
monitor
deoptimize
heap
evaluate
execute
print
fields
methods
search
instances xxx --fresh
hooking
generate
class
simple
get
current_activity
set
return_value
list
activities
class_loaders
class_methods
classes
receivers
services
search
classes
methods
watch
class
class_method
intent
launching_activity
launching_service
keystore
list
clear
watch
proxy
set
root
disable
simulate
shell_exec
sslpinning
disable
ui
FLAG_SECURE
screenshot
ping
plugin
load
pwd
reconnect
rm
sqlite
connect
Frida 只是提供了各种 API 供我们调用,在此基础之上可以实现具体的功能,比如禁用证书绑定之类的脚本,就是使用 Frida 的各种 API 来组合编写而成。于是有大佬将各种常见、常用的功能整合进一个工具,供我们直接在命令行中使用,这个工具便是objection。objection 功能强大,命令众多,而且不用写一行代码,便可实现诸如内存搜索、类和模块搜索、方法hook打印参数返回值调用栈等常用功能,是一个非常方便的,逆向必备、内存漫游神器。
安装命令:pip3 install objection
objection 的界面及命令如图所示。
objection 是基于 frida 的命令行 hook 工具,可以让你不写代码, 敲几句命令就可以对 java 函数的高颗粒度 hook, 还支持 RPC 调用。
objection 目前只支持 Java层的 hook,但是 objection 有提供插件接口,可以自己写 frida 脚本去定义接口,
比如葫芦娃大佬的脱壳插件,实名推荐: FRIDA-DEXDump
官方仓库: objection
首先介绍几个基本操作:
连接逍遥模拟器,需要先进入模拟器所在目录,使用目录中 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.Browser
Found 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
以下只是写了一部分指令和功能, 详细的功能需要合理运用 空格 和 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 指令
//堆内存中搜索指定类的实例, 可以获取该类的实例id
search 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 false
Spawn 方式 Hook
objection -g packageName explore --startup-command '[obejection_command]'
activity 和 service 操作
android hooking list activities //枚举activity
android intent launch_activity [activity_class] //启动activity
android hooking list services //枚举services
android 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
From:不写一行代码探索应用行为——使用objection-小北的自留地
这里拿 XCTF 的三个题目做演示,分别是mobile进阶区的第3题、第8题和第17题。
在手机上启动 frida-server,并且点击启动 "设置" 图标,手机进入设置的界面,首先查看一下 "设置" 应用的包名。
# frida-ps -U|grep -i setting
7107 com.android.settings
13370 com.google.android.settings.intelligence
再使用 objection 注入 "设置" 应用。
# objection -g com.android.settings explore
启动 objection之后,会出现提示它的 logo,这时候不知道输入啥命令的话,可以按下空格,有提示的命令及其功能出来;
再按空格选中,又会有新的提示命令出来,这时候按回车就可以执行该命令,
见下图 2-2 执行的应用环境信息命令 env 和 frida-server 版本信息命令。
运行命令 memory list modules,效果如下图2-3所示。内存中加载的库
运行命令 memory list exports libssl.so,效果如下图2-4所示。 libssl.so 库的导出函数
当结果太多,终端无法全部显示的时候,可以将结果导出到文件中,然后使用其他软件查看内容,见下图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 的导出函数
命令是 memory dump all from_base,这部分内容与下文脱壳部分有重叠,我们在脱壳部分介绍用法。
命令是 memory search --string --offsets-only,这部分也与下文脱壳部分有重叠,我们在脱壳部分详细介绍用法。
我们查看AOSP源码关于设置里显示系统设置的部分,发现存在着 DisplaySettings类,可以在堆上搜索是否存在着该类的实例。
首先在手机上点击进入 "显示" 设置,然后运行命令:android heap search instances com.android.settings.DisplaySettings
并得到相应的实例地址:
查看源码得知 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
可见结果被直接打印了出来。
也可以在找到的实例上直接编写 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
这个功能其实非常厉害,可以即时编写、出结果、即时调试自己的代码,不用再:编写→注入→操作→看结果→再调整,而是直接出结果。
直接上代码,想要进入显示设置,可以在任意界面直接运行以下代码进入显示设置:
# android intent launch_activity com.android.settings.DisplaySettings
(agent) Starting activity com.android.settings.DisplaySettings...
(agent) Activity successfully asked to start.
可以使用 android hooking list 命令来查看当前可用的 activities,然后使用上述命令进行调起。
# android hooking list activities
com.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
也可以先使用 android hooking list services 查看可供开启的服务,
然后使用 android intent launch_service com.android.settings.bluetooth.BluetoothPairingService 命令来开启服务。
很多新手在学习 Frida 的时候,遇到的第一个问题就是:无法找到正确的类及子类,无法定位到实现功能的准确的方法,无法正确的构造参数、继而进入正确的重载。
这时候可以使用 Frida 进行动态调试,来确定以上具体的名称和写法,最后写出正确的hook代码。
执行命令:android hooking list classes
# android hooking list classes
sun.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
void
Found 11885 classes
执行命令: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.EGLDisplay
Found 144 classes
在内存中所有已加载的类的方法中搜索包含特定关键词的方法,上文中可以发现,内存中已加载的类就已经高达11885
个了,那么他们的方法一定是类的个数的数倍,整个过程会相当庞大和耗时,见下图2-6。
# android hooking search methods display
内存中搜索所有的方法
当搜索到了比较关心的类之后,就可以直接查看它有哪些方法,
比如:我们想要查看 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.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.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)
列出的方法与源码相比对之后,发现是一模一样的。
上文中在列出类的方法时,还直接把参数也提供了,也就是说我们可以直接动手写 hook 了,既然上述写 hook 的要素已经全部都有了,objection 这个 "自动化" 工具,当然可以直接生成代码。
自动生成 hook 代码的命令:android hooking generate simple com.android.settings.DisplaySettings
# android hooking generate simple com.android.settings.DisplaySettings
Java.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);
}
});
生成的代码大部分要素都有了,只是参数貌似没有填上,还是需要我们后续补充一些,看来还是无法做到完美。
上述操作均是基于在内存中直接枚举搜索,已经可以获取到大量有用的静态信息,我们再来介绍几个方法,可以获取到执行时动态的信息,当然、同样地,不用写一行代码。
我们以手机连接蓝牙耳机播放音乐为例,看看手机蓝牙接口的动态信息。
使用 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 类中的多个方法。
在这些方法中,我们对哪些方法感兴趣,就可以查看哪些个方法的参数、返回值和调用栈,比如想看 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()这个方法的;虽然这个方法没有参数,大家可以再找个有参数的试一下。
objection 的 help 中指出,在 hook 给出的单个方法的时候,会 hook 它的所有重载。
# help android hooking watch class_method
Command: android hooking watch class_method
Usage: android hooking watch class_method
(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.login
android 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-backtrace
android 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个。在设置界面随意进出几个子设置界面,可以看到命中很多次该方法的不同重载,每次参数的值也都不同,
见下图。 方法重载的参数和值都不同
葫芦娃 github:https://github.com/hluwa?tab=repositories
Wallbreaker:从内存里面进行 逆向
Wallbreaker 是一个有用的工具,用于实时分析 Java 堆,由frida提供支持。提供一些命令从内存中搜索对象或类,并精美地可视化目标的真实结构。
想知道真实的数据内容吗?项目清单?地图条目?想知道接口的实现吗?尝试一下!你所看到的就是你得到的!
使用方法:参看 github:https://github.com/hluwa/Wallbreaker
From:[原创] 使用 Wallbreaker 快速分析 Java 类/对象结构-Android安全-看雪论坛-安全社区|安全招聘|bbs.pediy.com
Wallbreaker
取自 wikipedia
上对《三体》"破壁者"的翻译。
wallbreaker 是一个超级懒人(我)为了减少编写重复性垃圾代码而产生的一个工具,主要作用是将内存中 Java 类或对象的结构数据进行可视化。
就像介个亚子:
目前我是比较喜欢以 objection
插件的形式来使用,本来我也想自己写交互式控制台,但我觉得 objection
已经写得挺好,直接上车就好了,所以暂时不打算自己实现了。
开发的时候就使用 ipython
或者写 testcase
调试。
wallbreaker
到自己的插件目录:git clone https://github.com/hluwa/Wallbreaker ~/.objection/plugins/Wallbreaker-P
参数带着插件启动 objection:objection -g com.app.name explore -P ~/.objection/plugins然后就可以愉快的使用 wallbreaker 的几个命令了:
DEMO:Wallbreaker: Quick view object struct in Android - asciinema
前文中介绍的 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:
return
f = open(jobfile, 'w')
export = {}
export['match_regex'] = self.app.match_regex_list
export['black_regex'] = self.app.black_regex_list
tree = {}
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'] = tree
f.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:
return
f = 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)
我们来完整的演示一遍,比如现在看java.io.File
类的所有方法,我们可以这样操作,首先是精准匹配:
可以看到 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。当然这些 “缺点” 无非也就是加几行代码的事情,整个工具非常不错,值得用于辅助分析。
我们拿到一个app,做的第一件事情往往是先抓包来看,它发送和接收了哪些数据。收包发包是一个app的命门,企业为用户服务过程中最为关键的步骤——注册、流量商品、游戏数据、点赞评论、下单抢票等行为,均通过收包发包来完成。如果对收包发包的数据没有校验,黑灰产业可以直接制作相应的协议刷工具,脱离app本身进行实质性业务操作,为企业和用户带来巨大的损失。
抓包方法
JustTrustMePlus:https://github.com/Mocha-L/JustTrustMePlus
自识别类名 自动化Hook JustTrustMe 升级版:[原创]自识别类名 自动化Hook JustTrustMe 升级版-Android安全-看雪论坛-安全社区|安全招聘|bbs.pediy.com
重点说明手机抓包
APP 抓包和微信小程序抓包-Charles 的精简使用教程:APP 抓包和微信小程序抓包-Charles 的精简使用教程_Andy's Blog-CSDN博客
解决安卓手机 charles 抓包网络请求 https 抓包证书认证不通过:解决安卓手机charles抓包网络请求https抓包证书认证不通过 - 吴先雨 - 博客园
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() 只有可以重新发送的请求
坑、坑、坑、坑、坑、坑:
okhtpfind.dex
内包含了 更改了包名的okio
以及Gson
)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(); // 获取classloader
Java.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")
//URL
var 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 = true
if (null == requestBody) {
hasRequestBody = false
}
//Headers
var 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 = defChatset
var contentType = requestBody[M_reqbody_contentType]()
if (null != contentType) {
var appcharset = contentType[M_contentType_charset](defChatset);
if (null != appcharset) {
charset = appcharset;
}
}
//LOG Request Body
try {
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;
}
//URL
logString.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]()
//Headers
var 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")
}
//Body
var 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 = defChatset
var contentType = responseBody[M_rspBody_contentType]()
if (null != contentType) {
var appcharset = contentType[M_contentType_charset](defChatset)
if (null != appcharset) {
charset = appcharset
}
}
//newResponse
var 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) * 150
newSB.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 common
JavaStringWapper = 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 OKHTTP
OkHttpClientWapper = Java.use(Cls_OkHttpClient)
ResponseBodyWapper = Java.use(Cls_ResponseBody)
BufferWapper = Java.use(Cls_okio_Buffer)
//Start Hook
OkHttpClientWapper[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.$className
if (!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 = loader
console.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).class
if (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").class
var 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 抓包
由上所述,抓包是每一位安全工程师必须掌握的技能。而抓包一般又分为以下两种情形:
使用 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的所有流量也是从抓包电脑走的,可以抓到所有的包。
在抓包的对抗上体现的也是两个原则,一是理解的越成熟思路越多,二是对抗的战场越深上层越无法防御。
从防护的强度来看,Https 的强度是远远大于 Http 的;从大型分布式 C/S 架构的设计来看,如果服务器数量非常多、app版本众多,app在实现Https的策略上通常会采取客户端校验服务器证书的策略,如果服务器数量比较少,全国就那么几台、且app版本较少、对app版本管控较为严格,app在实现Https的策略时会加上服务器校验客户端证书的策略。
接下来我们具体分析每一种情况。
对于Http的抓包,只要在电脑的Charles上配置好Socks5服务器,手机上用Postern开启VPN连上电脑上的Charles的Socks5服务器,所有流量即可导出到Charles上。当然使用BurpSuite也是一样的道理。至于具体的操作步骤网上文档浩如烟海,读者可以自行取阅。
一般大型app、服务器数量非常多的,尤其还配置了多种CDN在全国范围、三网内进行内容分发和加速分发的,通常app里绝大多数内容都是走的Http。
当然他们会在最关键的业务上,比如用户登录时,配置Https协议,来保证最基本的安全。
这时候我们抓 app 的 Http 流量的时候一切正常,图片、视频、音乐都直接下载和转储。
但是作为用户要登录的时候,就会发现抓包失败,这时候开启 Charles 的 SSL 抓包功能,手机浏览器输入Charles的证书下载地址chls.pro/ssl,下载证书并安装到手机中。
注意在高版本的安卓上,用户安装的证书并不会安装到系统根证书目录中去,需要
root
手机后将用户安装的证书移动到系统根证书目录中去,具体操作步骤网上非常多,这里不再赘述。
当 Charles 的证书安装到系统根目录中去之后,系统就会信任来自Charles的流量包了,我们的抓包过程就会回归正常。
当然,这里还是会有读者疑惑,为什么导入Charles的证书之后,app抓包就正常了呢?
这里我们就需要理解一下应用层 Https 抓包的根本原理,
见下图2-15(会话层Socket抓包并不是这个原理,后文会介绍Socket抓包的根本原理)。
有了 Charles 置于中间之后,本来 C/S 架构的通信过程会 “分裂” 为两个独立的通信过程,app本来验证的是服务器的证书,服务器的证书手机的根证书是认可的,直接内置的;但是分裂成两个独立的通信过程之后,app验证的是Charles的证书,它的证书手机根证书并不认可,它并不是由手机内置的权威根证书签发机构签发的,所以手机不认,然后app也不认;所以我们要把Charles的证书导入到手机根证书目录中去,这样手机就会认可,如果app没有进行额外的校验(比如在代码中对该证书进行校验,也就是SSL pinning系列API,这种情况下一小节具体阐述)的话,app也会直接认可接受。
既然 app 客户端会校验服务器证书,那么服务器可不可能校验app客户端证书呢?答案是肯定的。
在许多业务非常聚焦并且当单一,比如行业应用、银行、公共交通、游戏等行业,C/S架构中服务器高度集中,对应用的版本控制非常严格,这时候就会在服务器上部署对app内置证书的校验代码。
上一小节中已经看到,单一通信已经分裂成两个互相独立的通信,这时候与服务器进行通信的已经不是app、而是Charles了,所以我们要将app中内置的证书导入到Charles中去。
这个操作通常需要完成两项内容:
找到证书文件很简单,一般 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。
上文中我们还有一种情况没有分析,就是客户端并不会默认信任系统根证书目录中的证书,而是在代码里再加一层校验,这就是证书绑定机制——SSL pinning
,如果这段代码的校验过不了,那么客户端还是会报证书错误。
遇到这种情况的时候,我们一般有三种方式,当然目标是一样的,都是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:如果还有一些情况没有覆盖的话,可以来看看大佬的代码
应该可以覆盖到目前已知的所有种类的证书绑定了。
当我们在使用 Charles 进行抓包的时候,会发现针对某些 IP 的数据传输一直显示 CONNECT,无法 Complete,显示 Sending request body,并且数据包大小持续增长,这时候说明我们遇到了Socket端口通信。
Socket 端口通信运行在会话层,并不是应用层,Socket抓包的原理与应用层Http(s)有着显著的区别。准确的说,Http(s)抓包是真正的“中间人”抓包,而Socket抓包是在接口上进行转储;Http(s)抓包是明显的将一套C/S架构通信分裂成两套完整的通信过程,而Socket抓包是在接口上将发送与接收的内容存储下来,并不干扰其原本的通信过程。
对于安卓应用来说,Socket
通信天生又分为两种Java
层Socket
通信和Native
层Socket
通信。
Java
层:使用的是java.net.InetAddress
、java.net.Socket
、java.net.ServerSocket
等类,与证书绑定的情形类似,也可能存在着自定义框架的Socket
通信,这时候就需要具体情况具体分析,比如谷歌的protobuf
框架等;Native
层:一般使用的是C Socket API
,一般hook
住send()
和recv()
函数可以得到其发送和接受的内容抓包方法分为三种,接口转储、驱动转储和路由转储:
outputStream.write
下hook
,把内容存下来看看,可能是经过压缩、或加密后的包,毕竟是二进制,一切皆有可能;tcpdump
将经过网口驱动时的数据包转储下来,再使用Wireshark
进行分析;jnettop
,观察实时进过的流量和IP
,可以使用WireShark
实时抓包,也可以使用tcpdump
抓包后用WireShark
分析。