精华内容
下载资源
问答
  • 最近在B站听伍佰的时候,发现有不少UP主制作了4K画质修复版 Live 视频???? 这些二十多年前的视频,在4K画质下,似乎多了一份莫名的感动… 我后来突然就很好奇,他们是怎么制作的这些视频?毕竟我也收藏有不少老片??...

    最近在B站听伍佰的时候,发现有不少UP主制作了4K画质修复版 Live 视频😘

    这些二十多年前的视频,在4K画质下,似乎多了一份莫名的感动…

    我后来突然就很好奇,他们是怎么制作的这些视频?毕竟我也收藏有不少老片🐶

    做了些研究,阿虚发现实现的方法还不少,比如:

    1. 如今有专业的视频修复公司,比如之前的4K修复版开国大典,其实是花费了600个人的修复团队将24万帧画面一帧一帧上色修复的
    2. 国外一家名为 VideoGorillas 的公司开发了 Bigfoot、SUPER RESOLUTION 等AI修复技术,不过该公司只承接商业合作,并未对外提供可操作的软件
    3. AI影片《老北京100年前影片修复》制作者「大谷Spitzer」分享了他的工作流程:视频补帧使用DAIN(Python),分辨率倍增部分使用 ESRGAN(Python),上色应用了 DeOldify(Jupyter Notebook),此外用 VirtualDub(C++)做了一些老电影降噪处理

    可以看出上面的方法,要么掏出你的人民币,要么有过硬的编程技术,对于我们多数人来说,是没有可操作性的🤦🏻‍♂️

    不过呢,阿虚后续又深入研究了亿下,发现其实也是有我们一般人可学会的流程的🌸🐔

    ▍1 提升视频画质

    大家应该知道,简单来看待视频,其实就是「一帧帧连续的图片」配上对应的「音频」

    那说直白点,如果你想要提升视频分辨率且不怕麻烦的话,完全可以将音画分离,视频导出为一帧一帧,然后用 Waifu2x 等软件将每一帧都放大,最后重新合并画面与音频:https://mp.weixin.qq.com/s/p0ClpJuiNHunkJ7IgyTtCQ

    不过上述方法实在过于麻烦了,好在有这么一家公司—— Topaz

    开发了一款名为 Topaz Video Enhance AI 的软件,可以让我们一键化实现上述步骤

    ● 1.1 Topaz Video Enhance AI简介

    Topaz Video Enhance AI 基于人工智能,喂以数以千计视频的训练其数据模型,可以将模糊视频智能锐化,从素材中推断细节以让视频变得清晰

    最关键的是,不仅功能牛逼,使用上也做到了小白一键化操作!

    Topaz Video Enhance AI 的安装包,请关注微信公众号:阿虚同学,发送以下关键词,后台即会自动回复下载地址了:

    视频清晰化
    

    安装和🆓过程阿虚就略过了,在安装包中阿虚有详细说明

    这里用伍佰1996夏夜晚风演唱会DVD原盘的480P视频做测试:

    ▲点击查看大图

    在右边可以直接快速设置AI模型与想放大的视频倍数:

    ● 1.2 AI模型的选择

    最主要影响视频处理结果的就是AI模型了,所以这里费篇幅多说一点👨‍🏫

    Topaz Video Enhance AI 2.1版的模型大致分为4个系列:Artemis系列,Dione系列,Gaia系列,Theia系列:

    1. Artemis系列比较适合修复画面本身噪点不多,已经被轻微后处理过的视频,比如演唱会DVD原盘这种,视频本身在制作时经过了轻微的磨皮和调试,会有不少特写镜头,这样的视频用Artemis系列修出来,特写镜头清晰的不可思议!

    2. Dione系列是v1.9才推出的模型,算法不算太成熟,虽然能够处理隔行扫描视频,但是当视频中有运动的主体时,运动主体的横纹时常有残留,运动速度越快残留可能性越高。而且当人脸在画面中占比不是很大时,眼睛经常修不好,会有类似水纹的横纹存在!

    3. Gaia系列主要用于修复CG动画和数码制作的动画,颜色不复杂、线条明显的手绘动画用Gaia-CG/采用特别材质,如水彩、丙烯等的动画用Gaia-HQ

    4. Theia系列是可以自己调整参数的模型,但对于多数人来说,自己手动调参的门槛过高,一遍一遍试错其实非常费时间,除非是你实在特别喜欢某个视频非要试出个最完美的参数又另说

    以上评价综合了这几位作者的观点,如果想深入掌握 Topaz Video Enhance AI 建议仔细研读一下这几篇文章:

    ● 1.3 逐行扫描&隔行扫描

    这里简单说一下逐行扫描&隔行扫描

    其实目前大多数视频都是逐行扫描,隔行扫描更多出现在「老视频」

    然后怎么样判断视频是逐行扫描还是隔行扫描呢 ❓ ❓

    如果你是从网上下载的视频文件:文件名标注1080P就是逐行扫描,而隔行扫描是1080i

    更准确一点的确认办法是在视频播放器中,比如 Potplayer

    右键查看「属性」,找到「文件信息」,即可以看到下面这个视频是逐行扫描(progressive)的,而如果是隔行扫描,那显示的单词会是 interlaced

    ● 1.4 处理前后效果对比

    OK,上面你看懂了之后,应该能判断出阿虚的这个「伍佰1996夏夜晚风演唱会DVD原盘的480P的视频」应该选用 Artemis 系列的模型 ⭕

    然后要根据放大前视频的质量来选择AI模型:一般360P、480P建议选低质量模型,720P建议中等质量,1080P则选高质量:

    对于其他设置:

    • 「裁剪以填充帧」这个选项建议关闭
    • 如果是真人视频,颗粒数量和大小推荐「均设置0.8」或者「均设置0」;而动画CG视频则均建议设置为0

    以上都设置好之后,在软件左边设置区(一定建议设置短一点,比如阿虚只设置了8帧的区间),然后点击预览

    然后如果你是第一次使用某个AI模型,将会弹出以下窗口

    因为软件需要联网下载模型,所以请耐心耐心耐心耐心耐心耐心等待!⏳ ⏳ ⏳

    而且模型下载完之后,还需要一帧一帧处理你刚刚选择的区间(所以上面建议各位少设置几帧)

    总之一直耐心等待到出结果,我们就可以预览到视频处理前后的区别:

    ▲点击查看大图

    上图你可能不太看得出区别,你可以在软件右上角,切换画面显示大小

    处理过后,的确是肉眼可见画质有提升

    不过毕竟是后期修复而来的,画面放大到一定程度还是能明显看到处理痕迹的,常见的就比如这种类油画质感

    总而言之,在你预览后对处理效果满意的话,就可以回到软件主界面,在界面右下角「设置视频输出目录」并开始正式处理视频了☕

    ● 1.5 版本选择

    阿虚上文虽然是以2.1版中文版进行的介绍,不过这是为了方面大家理解

    其实加上上文没提到的软件设置界面,整个软件也没多少英文设置!

    ▲软件设置界面

    在掌握用法后其实更建议用最新的2.2版

    2.2版的好处是处理效率更有提升+新增了智能算法选择推荐+不少算法都又有进行更新(比如Artemis、Dinoe)

    ▲也就需要懂几个单词而已

    而且阿虚这边的2.2版省去了2.1版的安装等步骤,直接解压就可用!

    唯一缺点仅是2.2版目前只有英文版!

    ● 1.6 关于处理耗时

    然后必须要说一下,这款软件非常吃电脑配置!

    首先系统要求WIN10(仅限64位),要求最小电脑内存8GB(推荐16GB,且越大越好)

    然后在CPU模式下:推荐英特尔i7或更高(4GHz及以上);AMD则推荐锐龙7或者更高(4GHz及以上)

    如果是GPU模式下:建议显卡显存要有6GB或更多

    方便大家参考,这里稍微列一下不同配置网友处理视频所花费的时间😂:

    • 2.1.1版本,960×464至1920×928,GT940MX显卡平均2-3秒一帧(阿虚说:可以看出版本的提升软件性能大有优化,GT940MX属于排行倒数的显卡了)
    • 我用的是GTX1050Ti,i7-H的,可以用,1080转4K,一个3-4分钟的视频要算7个小时
    • A卡 R9380,720P转换2K,大概1.15秒一帧(阿虚说:约36分钟处理1分钟长的视频)
    • RX5700表示很淦,现在软件无法调用A卡,AMD3600进行720×480转1440×960(200%)16分钟的视频要17小时多
    • 酷睿i9+显卡RTX 3080+内存32g,11分钟720P转1080P要两个小时,平均每帧0.3秒(阿虚说:约9分钟处理1分钟长的视频)

    ▍2 视频补帧

    目前电影普遍是每秒24帧,多数网上的视频则是每秒30帧

    但估计不少人早就是144Hz的电脑屏幕了,甚至手机应该都是120Hz的吧?所以说目前视频的帧率已经远远赶不上广大人民群众的需求了😂

    为了最大程度利用显示屏的性能,我们可以做的便就是对视频进行补帧了

    补帧对视频最明显的体验提升就是在运动画面上:

    ▲左为补帧前,右为补帧后

    光是图片大家不太能体验出来,不过可以想象一下打游戏30帧和60帧的区别,有画面了吗?😂

    ● 2.1 常见视频补帧算法

    关于视频补帧的算法我们目前主流的有几种

    第一种就是英伟达公布的Super SloMo算法,不过遗憾的是论文发布时并没有将代码和数据集公开,所以只能在视频中感受一下它的强大了

    虽然官方没公布算法,但是一位在德州上学的大佬在GitHub上开源了他对 Super-SloMo的 PyTorch实现:https://github.com/avinashpaliwal/Super-SloMo

    但这个算法用的人比较少,而且因为没有Windows打包版,使用门槛也颇高

    第二种就是来自上海交大的一个DAIN插帧算法:https://github.com/baowenbo/DAIN

    虽然也有基于此算法的GUI版本工具Dain-App:https://github.com/BurguerJohn/Dain-App

    不过此算法极度吃显卡及显存资源,低端N卡不建议使用,AMD显卡更是直接不支持

    但好在,我们现在又有了新的补帧算法选择:

    ● 2.2 RIFE算法

    RIFE算法效率更高,重影更低,最重要的是补帧时间大大缩小,原本用DAIN补帧一部25分钟左右的720P动漫一般要十几二十个小时,而RIFE只要四小时左右

    更关键的是基于RIFE算法的带GUI版软件还很多!

    比如 Flowframes:https://nmkd.itch.io/flowframes(这款软件缺点就是全英文界面,且在线安装很慢,优点是要求的驱动版本不高,一般能省去更新驱动的步骤)

    不过相对来说阿虚更推荐有中文界面的 Squirrel-RIFE Video Frame Interpolation:https://github.com/YiWeiHuang-stack/Squirrel-RIFE

    Github目前处于半墙状态,建议了解《Github加速下载教程》

    如果你实在对到 Github下载有困难,可以关注微信公众号:阿虚同学,发送以下关键词,后台即会自动回复这款软件的下载地址:

    视频补帧
    

    软件使用非常简单,一键式操作,拖入视频,设置「视频原始帧率」和「补帧倍率」,再设置好输出文件夹,最后点击「一键补帧」就行了

    但注意,N卡想要使用这款软件,需要所安装的驱动版本大于等于460.89(20201225发布)✅

    你可以直接到NVIDIA官网手动搜索驱动程序自行下载最新版驱动:https://www.nvidia.cn/geforce/drivers/

    如果你搞不懂怎么搜,你可以直接「GEFORCE 驱动程序」来帮你自动更新到最新版N卡驱动:

    ▲使用该软件需要进行注册

    另外A卡虽然也能用该软件,但只能调用CPU进行处理,效率就慢得多了!

    ▍3 播放器插帧

    但如果不是为了制作高清视频收藏或者分享出去,只是为了提升一下本地观看体验的话,其实我们有实时给视频插帧的办法🌝

    ● 3.1 PotPlayer自带

    用 PotPlayer 给视频插帧最简单,成本也低,只需要吃CPU,不怎么需要显卡,可谓懒人最佳的选择🐶

    这款播放器的去广告版可以在储物间(axutongxue.com)4-11栏直接下载

    只需要在视频播放时在 PotPlayer 右键 » 图像处理 » 勾选「倍帧」,即可实现将30帧的视频翻倍到60帧

    我们在 PotPlayer 中按 Tab 键,即可快速查看当前视频信息,可以看到视频原帧率30,在播放时的确有了60帧(因为是实时处理所以会有上下波动)

    不过呢,目前 PotPlayer 最多就能将视频处理成60帧,阿虚专门试过本身就是60帧的视频,再开启 PotPlayer 的倍帧,实际也依然只有60帧,不会变成120帧

    ● 3.2 在Potplayer中开启SVP

    但 PotPlayer 自身的插帧效果一般,真想要体验丝滑的插帧效果,基本上是用SVP

    SVP 这款插帧软件关注微信公众号:阿虚同学,发送以下关键词,后台即会自动回复下载地址:

    视频插帧
    

    具体安装过程阿虚有在压缩包中提供,这里为节省篇幅就略过,这里就说一下怎么在 PotPlayer 中启用 SVP

    在 PotPlayer 中右键 » 选项

    然后找到滤镜 » 全局滤镜优先权 » 添加系统滤镜 » 选择ffdshow raw video filter 并确认

    然后还要点一下 ffdshow raw video filter,选择「强制使用」并点击「应用」

    如果你的电脑有独显,还需要以下操作

    在选项中找到滤镜 » 视频解码器 » 内置解码器/DXVA 设置 » 勾选硬件加速并把DXCA2 Copy-Back 切换为独显,最后点击确定

    以上步骤完成之后,一定要重启电脑 ❗ 重启电脑 ❗ 重启电脑 ❗

    重启之后,如果想要对视频进行补帧,需要先打开SVP

    第一次启动SVP它会自行根据对电脑配置的测试来设置参数,当然你也可以自行进行设置:

    总而言之以上都搞定之后,再启用 PotPlayer 打开视频就能感受补帧效果了!

    如果一切都操作正确的话,影片播放前几秒, PotPlayer 左下角是会出现 SVP 启用的标志的

    好家伙,直接给我补到144帧,把我显示器的配置直接拉满!

    可惜的就是这种补帧后的丝滑体验没办法通过文章展示了,这个真的只有靠自己肉眼才感受的出来了🤷🏻‍♂️

    大家就能靠自己实操后体验了

    对于老视频提升画质、提高帧率阿虚也只是浅显的进行了一下研究,像是如何对黑白视频上色这里就没深入研究了(考虑到应用场景比较少暂时可能也不准备写)

    不过阿虚原来写过一篇对黑白图片上色的文章,感兴趣的话倒是可以去复习一下:https://mp.weixin.qq.com/s/SjSg29iDkfAX7_DNOcSqdw

    展开全文
  • Android 应用和系统优化V1.2

    千次阅读 2017-09-15 22:29:47
    UI可谓是一个应用的脸,所以每一款应用在开发阶段我们的交互、视觉、动画工程师都拼命的想让它变得自然大方美丽,可是现实总是不尽人意,动画和交 互总会觉得开发做出来的应用用上去感觉不自然,没有达到他们心目中...

    一年多年写了一篇简单的软件优化教程,给公司的同事使用。现在应该还不算过时,在过去一年里,在国家脱虚向实运动倡导下,一个个高科技企业如雨后春笋般诞生,对软件的优化和重构的需求也越来越多。早期的 android 开发者曾经十分羡慕C文开发者,C的调试工具是如此之多和丰富,内存,堆栈,CPU,GPU,断点,现在android的系统分析和优化工具也相当多,借助系统自带的开发者工具,android系统也变得日趋成熟和完美。无论是功耗(耗电),还是CPU,还是GPU,还是内存,抑或者是UI显示都有了相应的分析工具做定量的分析,不会处于我的软件似乎变快了,似乎省电了,但又肉眼看不出来的结果。


    Android 应用和系统优化V1.2

    作者:贾治国

    目录

    1应用UI卡顿常见原因
    2.常用工具
    3.工具用法
    Android应用开发性能优化完全分析
    给 App 提速:Android 性能优化总结
    Android系统性能调优工具介绍
    正确使用Android性能分析工具——TraceView
    为什么微博的app在iPhone比Android上流畅
    被忽略的UI检视利器:Hierarchy Viewer
    使用Systrace分析UI性能
    正确使用Android性能分析工具——TraceView
    内存分析工具 MAT 的使用
    使用Android studio分析内存泄露
    Android 编程下的 TraceView 简介及其案例实战
    Google 发布 Android 性能优化典范
    Android MemInfo 各项的意义(转) 
    Android 内存分析工具 - LogCat GC
    Android使用procrank和dumpsysmeminfo分析内存占用情况
    LeakCanary——直白的展现Android中的内存泄露
    LeakCanary 中文使用说明
    如何使用eclipse单独调试android系统的app
    Android上oprofile使用说明
    谁动了我的cpu——oprofile使用札记
    Android性能优化案例研究(上)
    Android性能优化案例研究(下)
    Android代码性能优化












    应用UI卡顿常见原因
    我们在使用App时会发现有些界面启动卡顿、动画不流畅、列表等滑动时也会卡顿,究其原因,很多都是丢帧导致的;通过上面卡顿原理的简单说明我们从应用开发的角度往回推理可以得出常见卡顿原因,如下:
    人为在UI线程中做轻微耗时操作,导致UI线程卡顿;(traceview)
    布局Layout过于复杂,无法在16ms内完成渲染;(HierarchyViewer)
    同一时间动画执行的次数过多,导致CPU或GPU负载过重;(traceview)
    View过度绘制,导致某些像素在同一帧时间内被绘制多次,从而使CPU或GPU负载过重;使用GPU过度绘制分析UI性能(开发者模式)
    View频繁的触发measure、layout,导致measure、layout累计耗时过多及整个View频繁的重新渲染;(HierarchyViewer)
    内存频繁触发GC过多(同一帧中频繁创建内存),导致暂时阻塞渲染操作;(Memory监测)
    冗余资源及逻辑等导致加载和执行缓慢; (HierarchyViewer)
    臭名昭著的ANR; (adb logcat | grep AndroidRuntime)
    影响动画的因素,时间曲线,帧率,触屏响应(systrace)
    大量的数学运算和矩阵变化阻塞了动画线程,优化数学方法,少使用浮点(代码分析)
    使用大量滤镜特效处理图像导致动画阻塞(代码分析)













    常用分析工具
    使用GPU过度绘制分析UI性能(开发者模式,布局优化)
    硬件加速,打开 “Show hardware layers updates” 选项。(开发者模式,变绿未加速)
    使用GPU呈现模式图及FPS考核UI性能(开发者模式,超过十六毫秒优化)
    一些android自带的命令行工具procrank,meminfo分析系统和应用内存
    使用Lint进行资源及冗余UI布局等优化(android studio OR eclipse工具对代码优化)
    android studio device monitor或者DDMS使用Memory监测及GC打印与Allocation Tracker进行UI卡顿分析(android studio device monitor OR eclipse ddms分析)
    使用HierarchyViewer(层级观察器)分析UI性能(命令行,android studio device monitor OR eclipse ddms启动)
    使用Traceview和dmtracedump进行分析优化,系统函数耗时分析(android studio device monitor OR eclipse ddms )
    使用Systrace进行分析优化,动画帧超时分析,提供性能警告信息分析(android studio device monitor OR eclipse ddms)
    使用traces.txt文件进行ANR分析优化(分析阻塞)
    LeakCanary(查找概率性内存泄漏,需要注入APP测试)
    Oprofile,linux下使用,可以分析优化系统底层的文件和函数
    MAT(查找内存泄漏)
    Adb logcat | findstr GC 或者 adb logcat grep GC


















    常用工具及用法(标题前网址为原文地址)

    http://blog.csdn.net/yanbober/article/details/48394201
    Android应用开发性能优化完全分析
    1 背景
    其实有点不想写这篇文章的,但是又想写,有些矛盾。不想写的原因是随便上网一搜一堆关于性能的建议,感觉大家你一总结、我一总结的都说到了很多优化 注意事项,但是看过这些文章后大多数存在一个问题就是只给出啥啥啥不能用,啥啥啥该咋用等,却很少有较为系统的进行真正性能案例分析的,大多数都是嘴上喊 喊或者死记住规则而已(当然了,这话我自己听着都有些刺耳,实在不好意思,其实关于性能优化的优质博文网上也还是有很多的,譬如Google官方都已经推 出了优化专题,我这里只是总结下自的感悟而已,若有得罪欢迎拍砖,我愿挨打,因为我之前工作的一半时间都是负责性能优化)。
    当然了,本文不会就此编辑这么一次,因为技术在发展,工具在强大(写着写着Android Studio 1.4版本都推送了),自己的经验也在增加,所以本文自然不会覆盖所有性能优化及分析;解决的办法就是该文章会长期维护更新,同时在评论区欢迎你关于性能 优化点子的探讨。
    Android应用的性能问题其实可以划分为几个大的模块的,而且都具有相对不错的优化调试技巧,下面我们就会依据一个项目常规开发的大类型来进行一些分析讲解。
    PS:之前呆过一家初创医疗互联网公司,别提性能优化了,老板立完新项目后一个月就要求见到上线成品,这种压迫下谈何性能优化,纯属扯蛋,所以不到三个月时间我主动选择撤了,这种现象后来我一打听发现在很多初创公司都很严重,都想速成却忽略了体验。
    PPPS:本文只是达到抛砖引玉的作用,很多东西细究下去都是值得深入研究的,再加上性能优化本来就是一个需要综合考量的任务,不是说会了本文哪一点就能做性能分析了,需要面面俱到才可高效定位问题原因。
    2 应用UI性能问题分析
    UI可谓是一个应用的脸,所以每一款应用在开发阶段我们的交互、视觉、动画工程师都拼命的想让它变得自然大方美丽,可是现实总是不尽人意,动画和交 互总会觉得开发做出来的应用用上去感觉不自然,没有达到他们心目中的自然流畅细节;这种情况之下就更别提发布给终端用户使用了,用户要是能够感觉出来,少 则影响心情,多则卸载应用;所以一个应用的UI显示性能问题就不得不被开发人员重视。
    2-1 应用UI卡顿原理
    人类大脑与眼睛对一个画面的连贯性感知其实是有一个界限的,譬如我们看电影会觉得画面很自然连贯(帧率为24fps),用手机当然也需要感知屏幕操作的连贯性(尤其是动画过度),所以Android索性就把达到这种流畅的帧率规定为60fps。
    有了上面的背景,我们开发App的帧率性能目标就是保持在60fps,也就是说我们在进行App性能优化时心中要有如下准则:
    换算关系:60帧/秒-----------16ms/帧;

    准则:尽量保证每次在16ms内处理完所有的CPU与GPU计算、绘制、渲染等操作,否则会造成丢帧卡顿问题。
    1
    2
    3
    从上面可以看出来,所谓的卡顿其实是可以量化的,每次是否能够成功渲染是非常重要的问题,16ms能否完整的做完一次操作直接决定了卡顿性能问题。
    当然了,针对Android系统的设计我们还需要知道另一个常识;虚拟机在执行GC垃圾回收操作时所有线程(包括UI线程)都需要暂停,当GC垃圾 回收完成之后所有线程才能够继续执行(这个细节下面小节会有详细介绍)。也就是说当在16ms内进行渲染等操作时如果刚好遇上大量GC操作则会导致渲染时 间明显不足,也就从而导致了丢帧卡顿问题。
    有了上面这两个简单的理论基础之后我们下面就会探讨一些UI卡顿的原因分析及解决方案。
    2-2 应用UI卡顿常见原因
    我们在使用App时会发现有些界面启动卡顿、动画不流畅、列表等滑动时也会卡顿,究其原因,很多都是丢帧导致的;通过上面卡顿原理的简单说明我们从应用开发的角度往回推理可以得出常见卡顿原因,如下:
    人为在UI线程中做轻微耗时操作,导致UI线程卡顿;
    布局Layout过于复杂,无法在16ms内完成渲染;
    同一时间动画执行的次数过多,导致CPU或GPU负载过重;
    View过度绘制,导致某些像素在同一帧时间内被绘制多次,从而使CPU或GPU负载过重;
    View频繁的触发measure、layout,导致measure、layout累计耗时过多及整个View频繁的重新渲染;
    内存频繁触发GC过多(同一帧中频繁创建内存),导致暂时阻塞渲染操作;
    冗余资源及逻辑等导致加载和执行缓慢;
    臭名昭著的ANR;
    可以看见,上面这些导致卡顿的原因都是我们平时开发中非常常见的。有些人可能会觉得自己的应用用着还蛮OK的,其实那是因为你没进行一些瞬时测试和压力测试,一旦在这种环境下运行你的App你就会发现很多性能问题。
    2-3 应用UI卡顿分析解决方法
    分析UI卡顿我们一般都借助工具,通过工具一般都可以直观的分析出问题原因,从而反推寻求优化方案,具体如下细说各种强大的工具。
    2-3-1 使用HierarchyViewer分析UI性能
    我们可以通过SDK提供的工具HierarchyViewer来进行UI布局复杂程度及冗余等分析,如下:
    xxx@ThinkPad:~$ hierarchyviewer   //通过命令启动HierarchyViewer
    1
    选中一个Window界面item,然后点击右上方Hierarchy window或者Pixel Perfect window(这里不介绍,主要用来检查像素属性的)即可操作。
    先看下Hierarchy window,如下:
     
    一个Activity的View树,通过这个树可以分析出View嵌套的冗余层级,左下角可以输入View的id直接自动跳转到中间显示;Save as PNG用来把左侧树保存为一张图片;Capture Layers用来保存psd的PhotoShop分层素材;右侧剧中显示选中View的当前属性状态;右下角显示当前View在Activity中的位置 等;左下角三个进行切换;Load View Hierarchy用来手动刷新变化(不会自动刷新的)。当我们选择一个View后会如下图所示:
     
    类似上图可以很方便的查看到当前View的许多信息;上图最底那三个彩色原点代表了当前View的性能指标,从左到右依次代表测量、布局、绘制的渲 染时间,红色和黄色的点代表速度渲染较慢的View(当然了,有些时候较慢不代表有问题,譬如ViewGroup子节点越多、结构越复杂,性能就越差)。
    当然了,在自定义View的性能调试时,HierarchyViewer上面的invalidate Layout和requestLayout按钮的功能更加强大,它可以帮助我们debug自定义View执行invalidate()和 requestLayout()过程,我们只需要在代码的相关地方打上断点就行了,接下来通过它观察绘制即可。
    可以发现,有了HierarchyViewer调试工具,我们的UI性能分析变得十分容易,这个工具也是我们开发中调试UI的利器,在平时写代码时会时常伴随我们左右。
    2-3-2 使用GPU过度绘制分析UI性能
    我们对于UI性能的优化还可以通过开发者选项中的GPU过度绘制工具来进行分析。在设置->开发者选项->调试GPU过度绘制(不同设备可能位置或者叫法不同)中打开调试后可以看见如下图(对settings当前界面过度绘制进行分析):
     
    可以发现,开启后在我们想要调试的应用界面中可以看到各种颜色的区域,具体含义如下:
     
    颜色    含义       
    无色    WebView等的渲染区域       
    蓝色    1x过度绘制       
    绿色    2x过度绘制       
    淡红色    3x过度绘制       
    红色    4x(+)过度绘制    

    由于过度绘制指在屏幕的一个像素上绘制多次(譬如一个设置了背景色的TextView就会被绘制两次,一次背景一次文本;这里需要强调的是 Activity设置的Theme主题的背景不被算在过度绘制层级中),所以最理想的就是绘制一次,也就是蓝色(当然这在很多绚丽的界面是不现实的,所以 大家有个度即可,我们的开发性能优化标准要求最极端界面下红色区域不能长期持续超过屏幕三分之一,可见还是比较宽松的规定),因此我们需要依据此颜色分布 进行代码优化,譬如优化布局层级、减少没必要的背景、暂时不显示的View设置为GONE而不是INVISIBLE、自定义View的onDraw方法设 置canvas.clipRect()指定绘制区域或通过canvas.quickreject()减少绘制区域等。
    2-3-3 使用GPU呈现模式图及FPS考核UI性能
    Android界面流畅度除过视觉感知以外是可以考核的(测试妹子专用),常见的方法就是通过GPU呈现模式图或者实时FPS显示进行考核,这里我 们主要针对GPU呈现模式图进行下说明,因为FPS考核测试方法有很多(譬如自己写代码实现、第三方App测试、固件支持等),所以不做统一说明。
    通过开发者选项中GPU呈现模式图工具来进行流畅度考量的流程是(注意:如果是在开启应用后才开启此功能,记得先把应用结束后重新启动)在设置 ->开发者选项->GPU呈现模式(不同设备可能位置或者叫法不同)中打开调试后可以看见如下图(对settings当前界面上下滑动列表后 的图表):
     
    当然,也可以在执行完UI滑动操作后在命令行输入如下命令查看命令行打印的GPU渲染数据(分析依据:Draw + Process + Execute = 完整的显示一帧时间 < 16ms):
    adb shell dumpsys gfxinfo [应用包名]
    1
    打开上图可视化工具后,我们可以在手机画面上看到丰富的GPU绘制图形信息,分别展示了StatusBar、NavgationBar、 Activity区域等的GPU渲染时间信息,随着界面的刷新,界面上会以实时柱状图来显示每帧的渲染时间,柱状图越高表示渲染时间越长,每个柱状图偏上 都有一根代表16ms基准的绿色横线,每一条竖着的柱状线都包含三部分(蓝色代表测量绘制Display List的时间,红色代表OpenGL渲染Display List所需要的时间,黄色代表CPU等待GPU处理的时间),只要我们每一帧的总时间低于基准线就不会发生UI卡顿问题(个别超出基准线其实也不算啥问 题的)。
    可以发现,这个工具是有局限性的,他虽然能够看出来有帧耗时超过基准线导致了丢帧卡顿,但却分析不到造成丢帧的具体原因。所以说为了配合解决分析UI丢帧卡顿问题我们还需要借助traceview和systrace来进行原因追踪,下面我们会介绍这两种工具的。
    2-3-4 使用Lint进行资源及冗余UI布局等优化
    上面说了,冗余资源及逻辑等也可能会导致加载和执行缓慢,所以我们就来看看Lint这个工具是如何发现优化这些问题的(当然了,Lint实际的功能是非常强大的,我们开发中也是经常使用它来发现一些问题的,这里主要有点针对UI性能的说明了,其他的雷同)。
    在Android Studio 1.4版本中使用Lint最简单的办法就是将鼠标放在代码区点击右键->Analyze->Inspect Code–>界面选择你要检测的模块->点击确认开始检测,等待一下后会发现如下结果:
     
    可以看见,Lint检测完后给了我们很多建议的,我们重点看一个关于UI性能的检测结果;上图中高亮的那一行明确说明了存在冗余的UI层级嵌套,所以我们是可以点击跳进去进行优化处理掉的。
    当然了,Lint还有很多功能,大家可以自行探索发挥,这里只是达到抛砖引玉的作用。
    2-3-5 使用Memory监测及GC打印与Allocation Tracker进行UI卡顿分析
    关于Android的内存管理机制下面的一节会详细介绍,这里我们主要针对GC导致的UI卡顿问题进行详细说明。
    Android系统会依据内存中不同的内存数据类型分别执行不同的GC操作,常见应用开发中导致GC频繁执行的原因主要可能是因为短时间内有大量频 繁的对象创建与释放操作,也就是俗称的内存抖动现象,或者短时间内已经存在大量内存暂用介于阈值边缘,接着每当有新对象创建时都会导致超越阈值触发GC操 作。
    如下是我工作中一个项目的一次经历(我将代码回退特意抓取的),出现这个问题的场景是一次压力测试导致整个系统卡顿,瞬间杀掉应用就OK了,究其原 因最终查到是一个API的调运位置写错了方式,导致一直被狂调,当普通使用时不会有问题,压力测试必现卡顿。具体内存参考图如下:
     
    与此抖动图对应的LogCat抓取如下:
    //截取其中比较密集一段LogCat,与上图Memory检测到的抖动图对应,其中xxx为应用包名
    ......
    10-06 00:59:45.619 xxx I/art: Explicit concurrent mark sweep GC freed 72515(3MB) AllocSpace objects, 65(2028KB) LOS objects, 80% free, 17MB/89MB, paused 3.505ms total 60.958ms
    10-06 00:59:45.749 xxx I/art: Explicit concurrent mark sweep GC freed 5396(193KB) AllocSpace objects, 0(0B) LOS objects, 75% free, 23MB/95MB, paused 2.079ms total 100.522ms
    ......
    10-06 00:59:48.059 xxx I/art: Explicit concurrent mark sweep GC freed 4693(172KB) AllocSpace objects, 0(0B) LOS objects, 75% free, 23MB/95MB, paused 2.227ms total 101.692ms
    ......
    1
    2
    3
    4
    5
    6
    7
    我们知道,类似上面logcat打印一样,触发垃圾回收的主要原因有以下几种:
    GC_MALLOC——内存分配失败时触发;
    GC_CONCURRENT——当分配的对象大小超过一个限定值(不同系统)时触发;
    GC_EXPLICIT——对垃圾收集的显式调用(System.gc()) ;
    GC_EXTERNAL_ALLOC——外部内存分配失败时触发;
    可以看见,这种不停的大面积打印GC导致所有线程暂停的操作必定会导致UI视觉的卡顿,所以我们要避免此类问题的出现,具体的常见优化方式如下:
    检查代码,尽量避免有些频繁触发的逻辑方法中存在大量对象分配;
    尽量避免在多次for循环中频繁分配对象;
    避免在自定义View的onDraw()方法中执行复杂的操作及创建对象(譬如Paint的实例化操作不要写在onDraw()方法中等);
    对于并发下载等类似逻辑的实现尽量避免多次创建线程对象,而是交给线程池处理。
    当然了,有了上面说明GC导致的性能后我们就该定位分析问题了,可以通过运行DDMS->Allocation Tracker标签打开一个新窗口,然后点击Start Tracing按钮,接着运行你想分析的代码,运行完毕后点击Get Allocations按钮就能够看见一个已分配对象的列表,如下:
     
    点击上面第一个表格中的任何一项就能够在第二个表格中看见导致该内存分配的栈信息,通过这个工具我们可以很方便的知道代码分配了哪类对象、在哪个线 程、哪个类、哪个文件的哪一行。譬如我们可以通过Allocation Tracker分别做一次Paint对象实例化在onDraw与构造方法的一个自定义View的内存跟踪,然后你就明白这个工具的强大了。
    PS一句,Android Studio新版本除过DDMS以外在Memory视图的左侧已经集成了Allocation Tracker功能,只是用起来还是没有DDMS的方便实用,如下图:
     
    2-3-6 使用Traceview和dmtracedump进行分析优化
    关于UI卡顿问题我们还可以通过运行Traceview工具进行分析,他是一个分析器,记录了应用程序中每个函数的执行时间;我们可以打开DDMS 然后选择一个进程,接着点击上面的“Start Method Profiling”按钮(红色小点变为黑色即开始运行),然后操作我们的卡顿UI(小范围测试,所以操作最好不要超过5s),完事再点一下刚才按的那个 按钮,稍等片刻即可出现下图,如下:
     
    花花绿绿的一幅图我们怎么分析呢?下面我们解释下如何通过该工具定位问题:
    整个界面包括上下两部分,上面是你测试的进程中每个线程运行的时间线,下面是每个方法(包含parent及child)执行的各个指标的值。通过上 图的时间面板可以直观发现,整个trace时间段main线程做的事情特别多,其他的做的相对较少。当我们选择上面的一个线程后可以发现下面的性能面板很 复杂,其实这才是TraceView的核心图表,它主要展示了线程中各个方法的调用信息(CPU使用时间、调用次数等),这些信息就是我们分析UI性能卡 顿的核心关注点,所以我们先看几个重要的属性说明,如下:
     
    属性名    含义       
    name    线程中调运的方法名;       
    Incl CPU Time    当前方法(包含内部调运的子方法)执行占用的CPU时间;       
    Excl CPU Time    当前方法(不包含内部调运的子方法)执行占用的CPU时间;       
    Incl Real Time    当前方法(包含内部调运的子方法)执行的真实时间,ms单位;       
    Excl Real Time    当前方法(不包含内部调运的子方法)执行的真实时间,ms单位;       
    Calls+Recur Calls/Total    当前方法被调运的次数及递归调运占总调运次数百分比;       
    CPU Time/Call    当前方法调运CPU时间与调运次数比,即当前方法平均执行CPU耗时时间;       
    Real Time/Call    当前方法调运真实时间与调运次数比,即当前方法平均执行真实耗时时间;(重点关注)    

    有了对上面Traceview图表的一个认识之后我们就来看看具体导致UI性能后该如何切入分析,一般Traceview可以定位两类性能问题:
    方法调运一次需要耗费很长时间导致卡顿;
    方法调运一次耗时不长,但被频繁调运导致累计时长卡顿。
    譬如我们来举个实例,有时候我们写完App在使用时不觉得有啥大的影响,但是当我们启动完App后静止在那却十分费电或者导致设备发热,这种情况我 们就可以打开Traceview然后按照Cpu Time/Call或者Real Time/Call进行降序排列,然后打开可疑的方法及其child进行分析查看,然后再回到代码定位检查逻辑优化即可;当然了,我们也可以通过该工具来 trace我们自定义View的一些方法来权衡性能问题,这里不再一一列举喽。
    可以看见,Traceview能够帮助我们分析程序性能,已经很方便了,然而Traceview家族还有一个更加直观强大的小工具,那就是可以通过dmtracedump生成方法调用图。具体做法如下:
    dmtracedump -g result.png target.trace  //结果png文件  目标trace文件
    1
    通过这个生成的方法调运图我们可以更加直观的发现一些方法的调运异常现象。不过本人优化到现在还没怎么用到它,每次用到Traceview分析就已经搞定问题了,所以说dmtracedump自己酌情使用吧。
    PS一句,Android Studio新版本除过DDMS以外在CPU视图的左侧已经集成了Traceview(start Method Tracing)功能,只是用起来还是没有DDMS的方便实用(这里有一篇AS MT个人觉得不错的分析文章(引用自网络,链接属于原作者功劳)),如下图:
     
    2-3-7 使用Systrace进行分析优化
    Systrace其实有些类似Traceview,它是对整个系统进行分析(同一时间轴包含应用及SurfaceFlinger、 WindowManagerService等模块、服务运行信息),不过这个工具需要你的设备内核支持trace(命令行检查/sys/kernel /debug/tracing)且设备是eng或userdebug版本才可以,所以使用前麻烦自己确认一下。
    我们在分析UI性能时一般只关注图形性能(所以必须选择Graphics和View,其他随意),同时一般对于卡顿的抓取都是5s,最多10s。启动Systrace进行数据抓取可以通过两种方式,命令行方式如下:
    python systrace.py --time=10 -o mynewtrace.html sched gfx view wm
    1
    图形模式:
    打开DDMS->Capture system wide trace using Android systrace->设置时间与选项点击OK就开始了抓取,接着操作APP,完事生成一个trace.html文件,用Chrome打开即可如下图:
     
    在Chrome中浏览分析该文件我们可以通过键盘的W-A-S-D键来搞定,由于上面我们在进行trace时选择了一些选项,所以上图生成了左上方相关的 CPU频率、负载、状态等信息,其中的CPU N代表了CPU核数,每个CPU行的柱状图表代表了当前时间段当前核上的运行信息;下面我们再来看看SurfaceFlinger的解释,如下:
     
    可以看见上面左边栏的SurfaceFlinger其实就是负责绘制Android程序UI的服务,所以SurfaceFlinger能反应出整体 绘制情况,可以关注上图VSYNC-app一行可以发现前5s多基本都能够达到16ms刷新间隔,5s多开始到7s多大于了15ms,说明此时存在绘制丢 帧卡顿;同时可以发现surfaceflinger一行明显存在类似不规律间隔,这是因为有的地方是不需要重新渲染UI,所以有大范围不规律,有的是因为 阻塞导致不规律,明显可以发现0到4s间大多是不需要渲染,而5s以后大多是阻塞导致;对应这个时间点我们放大可以看到每个部分所使用的时间和正在执行的 任务,具体如下:
     
    可以发现具体的执行明显存在超时性能卡顿(原点不是绿色的基本都代表存在一定问题,下面和右侧都会提示你选择的帧相关详细信息或者alert信 息),但是遗憾的是通过Systrace只能大体上发现是否存在性能问题,具体问题还需要通过Traceview或者代码中嵌入Trace工具类等去继续 详细分析,总之很蛋疼。
    PS:如果你想使用Systrace很轻松的分析定位所有问题,看明白所有的行含义,你还需要具备非常扎实的Android系统框架的原理才可以将该工具使用的得心应手。
    2-3-8 使用traces.txt文件进行ANR分析优化
    ANR(Application Not Responding)是Android中AMS与WMS监测应用响应超时的表现;之所以把臭名昭著的ANR单独作为UI性能卡顿的分析来说明是因为 ANR是直接卡死UI不动且必须要解掉的Bug,我们必须尽量在开发时避免他的出现,当然了,万一出现了那就用下面介绍的方法来分析吧。
    我们应用开发中常见的ANR主要有如下几类:
    按键触摸事件派发超时ANR,一般阈值为5s(设置中开启ANR弹窗,默认有事件派发才会触发弹框ANR);
    广播阻塞ANR,一般阈值为10s(设置中开启ANR弹窗,默认不弹框,只有log提示);
    服务超时ANR,一般阈值为20s(设置中开启ANR弹窗,默认不弹框,只有log提示);
    当ANR发生时除过logcat可以看见的log以外我们还可以在系统指定目录下找到traces文件或dropbox文件进行分析,发生ANR后我们可以通过如下命令得到ANR trace文件:
    adb pull /data/anr/traces.txt ./
    1
    然后我们用txt编辑器打开可以发现如下结构分析:
    //显示进程id、ANR发生时间点、ANR发生进程包名
    ----- pid 19073 at 2015-10-08 17:24:38 -----
    Cmd line: com.example.yanbo.myapplication
    //一些GC等object信息,通常可以忽略
    ......
    //ANR方法堆栈打印信息!重点!
    DALVIK THREADS (18):
    "main" prio=5 tid=1 Sleeping
      | group="main" sCount=1 dsCount=0 obj=0x7497dfb8 self=0x7f9d09a000
      | sysTid=19073 nice=0 cgrp=default sched=0/0 handle=0x7fa106c0a8
      | state=S schedstat=( 125271779 68162762 280 ) utm=11 stm=1 core=0 HZ=100
      | stack=0x7fe90d3000-0x7fe90d5000 stackSize=8MB
      | held mutexes=
      at java.lang.Thread.sleep!(Native method)
      - sleeping on <0x0a2ae345> (a java.lang.Object)
      at java.lang.Thread.sleep(Thread.java:1031)
      - locked <0x0a2ae345> (a java.lang.Object)
    //真正导致ANR的问题点,可以发现是onClick中有sleep导致。我们平时可以类比分析即可,这里不详细说明。
      at java.lang.Thread.sleep(Thread.java:985)
      at com.example.yanbo.myapplication.MainActivity$1.onClick(MainActivity.java:21)
      at android.view.View.performClick(View.java:4908)
      at android.view.View$PerformClick.run(View.java:20389)
      at android.os.Handler.handleCallback(Handler.java:815)
      at android.os.Handler.dispatchMessage(Handler.java:104)
      at android.os.Looper.loop(Looper.java:194)
      at android.app.ActivityThread.main(ActivityThread.java:5743)
      at java.lang.reflect.Method.invoke!(Native method)
      at java.lang.reflect.Method.invoke(Method.java:372)
      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:988)
      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783)
    ......
    //省略一些不常关注堆栈打印
    ......
    至此常见的应用开发中ANR分析定位就可以解决了。
    2-4 应用UI性能分析解决总结
    可以看见,关于Android UI卡顿的性能分析还是有很多工具的,上面只是介绍了应用开发中我们经常使用的一些而已,还有一些其他的,譬如Oprofile等工具不怎么常用,这里就不再详细介绍。
    通过上面UI性能的原理、原因、工具分析总结可以发现,我们在开发应用时一定要时刻重视性能问题,如若真的没留意出现了性能问题,不妨使用上面的一 些案例方式进行分析。但是那终归是补救措施,在我们知道上面UI卡顿原理之后我们应该尽量从项目代码架构搭建及编写时就避免一些UI性能问题,具体项目中 常见的注意事项如下:
    布局优化;尽量使用include、merge、ViewStub标签,尽量不存在冗余嵌套及过于复杂布局(譬如10层就会直接异常),尽 量使用GONE替换INVISIBLE,使用weight后尽量将width和heigh设置为0dp减少运算,Item存在非常复杂的嵌套时考虑使用自 定义Item View来取代,减少measure与layout次数等。
    列表及Adapter优化;尽量复用getView方法中的相关View,不重复获取实例导致卡顿,列表尽量在滑动过程中不进行UI元素刷新等。
    背景和图片等内存分配优化;尽量减少不必要的背景设置,图片尽量压缩处理显示,尽量避免频繁内存抖动等问题出现。
    自定义View等绘图与布局优化;尽量避免在draw、measure、layout中做过于耗时及耗内存操作,尤其是draw方法中,尽量减少draw、measure、layout等执行次数。
    避免ANR,不要在UI线程中做耗时操作,遵守ANR规避守则,譬如多次数据库操作等。
    当然了,上面只是列出了我们项目中常见的一些UI性能注意事项而已,相信还有很多其他的情况这里没有说到,欢迎补充。还有一点就是我们上面所谓的 UI性能优化分析总结等都是建议性的,因为性能这个问题是一个涉及面很广很泛的问题,有些优化不是必需的,有些优化是必需的,有些优化掉以后又是得不偿失 的,所以我们一般着手解决那些必须的就可以了。
    3 应用开发Memory内存性能分析优化
    说完了应用开发中的UI性能问题后我们就该来关注应用开发中的另一个重要、严重、非常重要的性能问题了,那就是内存性能优化分析。Android其 实就是嵌入式设备,嵌入式设备核心关注点之一就是内存资源;有人说现在的设备都在堆硬件配置(譬如国产某米的某兔跑分手机、盒子等),所以内存不会再像以 前那么紧张了,其实这句话听着没错,但为啥再牛逼配置的Android设备上有些应用还是越用系统越卡呢?这里面的原因有很多,不过相信有了这一章下面的 内容分析,作为一个移动开发者的你就有能力打理好自己应用的那一亩三分地内存了,能做到这样就足以了。关于Android内存优化,这里有一篇 Google的官方指导文档,但是本文为自己项目摸索,会有很多不一样的地方。
    3-1 Android内存管理原理
    系统级内存管理:
    Android系统内核是基于Linux,所以说Android的内存管理其实也是Linux的升级版而已。Linux在进程停止后就结束该进程, 而Android把这些停止的进程都保留在内存中,直到系统需要更多内存时才选择性的释放一些,保留在内存中的进程默认(不包含后台service与 Thread等单独UI线程的进程)不会影响整体系统的性能(速度与电量等)且当再次启动这些保留在内存的进程时可以明显提高启动速度,不需要再去加载。
    再直白点就是说Android系统级内存管理机制其实类似于Java的垃圾回收机制,这下明白了吧;在Android系统中框架会定义如下几类进程、在系统内存达到规定的不同level阈值时触发清空不同level的进程类型。
     
    可以看见,所谓的我们的Service在后台跑着跑着挂了,或者盒子上有些大型游戏启动起来就挂(之前我在上家公司做盒子时遇见过),有一个直接的 原因就是这个阈值定义的太大,导致系统一直认为已经达到阈值,所以进行优先清除了符合类型的进程。所以说,该阈值的设定是有一些讲究的,额,扯多了,我们 主要是针对应用层内存分析的,系统级内存回收了解这些就基本够解释我们应用在设备上的一些表现特征了。
    应用级内存管理:
    在说应用级别内存管理原理时大家先想一个问题,假设有一个内存为1G的Android设备,上面运行了一个非常非常吃内存的应用,如果没有任何机制的情况下是不是用着用着整个设备会因为我们这个应用把1G内存吃光然后整个系统运行瘫痪呢?
    哈哈,其实Google的工程师才不会这么傻的把系统设计这么差劲。为了使系统不存在我们上面假想情况且能安全快速的运行,Android的框架使 得每个应用程序都运行在单独的进程中(这些应用进程都是由Zygote进程孵化出来的,每个应用进程都对应自己唯一的虚拟机实例);如果应用在运行时再存 在上面假想的情况,那么瘫痪的只会是自己的进程,不会直接影响系统运行及其他进程运行。
    既然每个Android应用程序都执行在自己的虚拟机中,那了解Java的一定明白,每个虚拟机必定会有堆内存阈值限制(值得一提的是这个阈值一般 都由厂商依据硬件配置及设备特性自己设定,没有统一标准,可以为64M,也可以为128M等;它的配置是在Android的属性系统的/system /build.prop中配置dalvik.vm.heapsize=128m即可,若存在dalvik.vm.heapstartsize则表示初始申 请大小),也即一个应用进程同时存在的对象必须小于阈值规定的内存大小才可以正常运行。
    接着我们运行的App在自己的虚拟机中内存管理基本就是遵循Java的内存管理机制了,系统在特定的情况下主动进行垃圾回收。但是要注意的一点就是 在Android系统中执行垃圾回收(GC)操作时所有线程(包含UI线程)都必须暂停,等垃圾回收操作完成之后其他线程才能继续运行。这些GC垃圾回收 一般都会有明显的log打印出回收类型,常见的如下:
    GC_MALLOC——内存分配失败时触发;
    GC_CONCURRENT——当分配的对象大小超过一个限定值(不同系统)时触发;
    GC_EXPLICIT——对垃圾收集的显式调用(System.gc()) ;
    GC_EXTERNAL_ALLOC——外部内存分配失败时触发;
    通过上面这几点的分析可以发现,应用的内存管理其实就是一个萝卜一个坑,坑都一般大,你在开发应用时要保证的是内存使用同一时刻不能超过坑的大小,否则就装不下了。
    3-2 Android内存泄露性能分析
    有了关于Android的一些内存认识,接着我们来看看关于Android应用开发中常出现的一种内存问题—-内存泄露。
    3-2-1 Android应用内存泄露概念
    众所周知,在Java中有些对象的生命周期是有限的,当它们完成了特定的逻辑后将会被垃圾回收;但是,如果在对象的生命周期本来该被垃圾回收时这个 对象还被别的对象所持有引用,那就会导致内存泄漏;这样的后果就是随着我们的应用被长时间使用,他所占用的内存越来越大。如下就是一个最常见简单的泄露例 子(其它的泄露不再一一列举了):
    public final class MainActivity extends Activity {
        private DbManager mDbManager;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

            setContentView(R.layout.activity_main);

            //DbManager是一个单例模式类,这样就持有了MainActivity引用,导致泄露
            mDbManager = DbManager.getInstance(this);
        }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    可以看见,上面例子中我们让一个单例模式的对象持有了当前Activity的强引用,那在当前Acvitivy执行完onDestroy()后,这个Activity就无法得到垃圾回收,也就造成了内存泄露。
    内存泄露可以引发很多的问题,常见的内存泄露导致问题如下:
    应用卡顿,响应速度慢(内存占用高时JVM虚拟机会频繁触发GC);
    应用被从后台进程干为空进程(上面系统内存原理有介绍,也就是超过了阈值);
    应用莫名的崩溃(上面应用内存原理有介绍,也就是超过了阈值OOM);
    造成内存泄露泄露的最核心原理就是一个对象持有了超过自己生命周期以外的对象强引用导致该对象无法被正常垃圾回收;可以发现,应用内存泄露是个相当棘手重要的问题,我们必须重视。
    3-2-2 Android应用内存泄露察觉手段
    知道了内存泄露的概念之后肯定就是想办法来确认自己的项目是否存在内存泄露了,那该如何察觉自己项目是否存在内存泄露呢?如下提供了几种常用的方式:
     
    察觉方式    场景       
    AS的Memory窗口    平时用来直观了解自己应用的全局内存情况,大的泄露才能有感知。       
    DDMS-Heap内存监测工具    同上,大的泄露才能有感知。       
    dumpsys meminfo命令    常用方式,可以很直观的察觉一些泄露,但不全面且常规足够用。       
    leakcanary神器    比较强大,可以感知泄露且定位泄露;实质是MAT原理,只是更加自动化了,当现有代码量已经庞大成型,且无法很快察觉掌控全局代码时极力推荐;或者是偶现泄露的情况下极力推荐。    
    AS的Memory窗口如下,详细的说明这里就不解释了,很简单很直观(使用频率高):
     
    DDMS-Heap内存监测工具窗口如下,详细的说明这里就不解释了,很简单(使用频率不高):
     
    dumpsys meminfo命令如下(使用频率非常高,非常高效,我的最爱之一,平时一般关注几个重要的Object个数即可判断一般的泄露;当然了,adb shell dumpsys meminfo不跟参数直接展示系统所有内存状态):
     
    leakcanary神器使用这里先不说,下文会专题介绍,你会震撼的一B。有了这些工具的定位我们就能很方便的察觉我们App的内存泄露问题,察觉到以后该怎么定位分析呢,继续往下看。
    3-2-3 Android应用内存泄露leakcanary工具定位分析
    leakcanary是一个开源项目,一个内存泄露自动检测工具,是著名的GitHub开源组织Square贡献的,它的主要优势就在于自动化过早 的发觉内存泄露、配置简单、抓取贴心,缺点在于还存在一些bug,不过正常使用百分之九十情况是OK的,其核心原理与MAT工具类似。
    关于leakcanary工具的配置使用方式这里不再详细介绍,因为真的很简单,详情点我参考官方教程学习使用即可。
    PS:之前在优化性能时发现我们有一个应用有两个界面退出后Activity没有被回收(dumpsys meminfo发现一直在加),所以就怀疑可能存在内存泄露。但是问题来了,这两个Activity的逻辑十分复杂,代码也不是我写的,相关联的代码量也 十分庞大,更加郁闷的是很难判断是哪个版本修改导致的,这时候只知道有泄露,却无法定位具体原因,使用MAT分析解决掉了一个可疑泄露后发现泄露又变成了 概率性的。可以发现,对于这种概率性的泄露用MAT去主动抓取肯定是很耗时耗力的,所以决定直接引入leakcanary神器来检测项目,后来很快就彻底 解决了项目中所有必现的、偶现的内存泄露。
    总之一点,工具再强大也只是帮我们定位可能的泄露点,而最核心的GC ROOT泄露信息推导出泄露问题及如何解决还是需要你把住代码逻辑及泄露核心概念去推理解决。
    3-2-4 Android应用内存泄露MAT工具定位分析
    Eclipse Memory Analysis Tools(点我下载)是一个专门分析Java堆数据内存引用的工具,我们可以使用它方便的定位内存泄露原因,核心任务就是找到GC ROOT位置即可,哎呀,关于这个工具的使用我是真的不想说了,自己搜索吧,实在简单、传统的不行了。
    PS:这是开发中使用频率非常高的一个工具之一,麻烦务必掌握其核心使用技巧,虽然Android Studio已经实现了部分功能,但是真的很难用,遇到问题目前还是使用Eclipse Memory Analysis Tools吧。
    原谅我该小节的放荡不羁!!!!(其实我是困了,呜呜!)
    3-2-5 Android应用开发规避内存泄露建议
    有了上面的原理及案例处理其实还不够,因为上面这些处理办法是补救的措施,我们正确的做法应该是在开发过程中就养成良好的习惯和敏锐的嗅觉才对,所以下面给出一些应用开发中常见的规避内存泄露建议:
    Context使用不当造成内存泄露;不要对一个Activity Context保持长生命周期的引用(譬如上面概念部分给出的示例)。尽量在一切可以使用应用ApplicationContext代替Context的 地方进行替换(原理我前面有一篇关于Context的文章有解释)。
    非静态内部类的静态实例容易造成内存泄漏;即一个类中如果你不能够控制它其中内部类的生命周期(譬如Activity中的一些特殊Handler等),则尽量使用静态类和弱引用来处理(譬如ViewRoot的实现)。
    警惕线程未终止造成的内存泄露;譬如在Activity中关联了一个生命周期超过Activity的Thread,在退出Activity 时切记结束线程。一个典型的例子就是HandlerThread的run方法是一个死循环,它不会自己结束,线程的生命周期超过了Activity生命周 期,我们必须手动在Activity的销毁方法中中调运thread.getLooper().quit();才不会泄露。
    对象的注册与反注册没有成对出现造成的内存泄露;譬如注册广播接收器、注册观察者(典型的譬如数据库的监听)等。
    创建与关闭没有成对出现造成的泄露;譬如Cursor资源必须手动关闭,WebView必须手动销毁,流等对象必须手动关闭等。
    不要在执行频率很高的方法或者循环中创建对象,可以使用HashTable等创建一组对象容器从容器中取那些对象,而不用每次new与释放。
    避免代码设计模式的错误造成内存泄露;譬如循环引用,A持有B,B持有C,C持有A,这样的设计谁都得不到释放。
    关于规避内存泄露上面我只是列出了我在项目中经常遇见的一些情况而已,肯定不全面,欢迎拍砖!当然了,只有我们做到好的规避加上强有力的判断嗅觉泄露才能让我们的应用驾驭好自己的一亩三分地。
    3-3 Android内存溢出OOM性能分析
    上面谈论了Android应用开发的内存泄露,下面谈谈内存溢出(OOM);其实可以认为内存溢出与内存泄露是交集关系,具体如下图:
     
    下面我们就来看看内存溢出(OOM)相关的东东吧。
    3-3-1 Android应用内存溢出OOM概念
    上面我们探讨了Android内存管理和应用开发中的内存泄露问题,可以知道内存泄露一般影响就是导致应用卡顿,但是极端的影响是使应用挂掉。前面 也提到过应用的内存分配是有一个阈值的,超过阈值就会出问题,这里我们就来看看这个问题—–内存溢出(OOM–OutOfMemoryError)。
    内存溢出的主要导致原因有如下几类:
    应用代码存在内存泄露,长时间积累无法释放导致OOM;
    应用的某些逻辑操作疯狂的消耗掉大量内存(譬如加载一张不经过处理的超大超高清图片等)导致超过阈值OOM;
    可以发现,无论哪种类型,导致内存溢出(OutOfMemoryError)的核心原因就是应用的内存超过阈值了。
    3-3-2 Android应用内存溢出OOM性能分析
    通过上面的OOM概念和那幅交集图可以发现,要想分析OOM原因和避免OOM需要分两种情况考虑,泄露导致的OOM,申请过大导致的OOM。
    内存泄露导致的OOM分析:
    这种OOM一旦发生后会在logcat中打印相关OutOfMemoryError的异常栈信息,不过你别高兴太早,这种情况下导致的OOM打印异常信息是没有太大作用,因为这种OOM的导致一般都如下图情况(图示为了说明问题数据和场景有夸张,请忽略):
     
    从图片可以看见,这种OOM我们有时也遇到,第一反应是去分析OOM异常打印栈,可是后来发现打印栈打印的地方没有啥问题,没有可优化的余地了,于是就郁闷了。其实这时候你留心观察几个现象即可,如下:
    留意你执行触发OOM操作前的界面是否有卡顿或者比较密集的GC打印;
    使用命令查看下当前应用占用内存情况;
    确认了以上这些现象你基本可以断定该OOM的log真的没用,真正导致问题的原因是内存泄露,所以我们应该按照上节介绍的方式去着手排查内存泄露问题,解决掉内存泄露后红色空间都能得到释放,再去显示一张0.8M的优化图片就不会再报OOM异常了。
    不珍惜内存导致的OOM分析:
    上面说了内存泄露导致的OOM异常,下面我们再来看一幅图(数据和场景描述有夸张,请忽略),如下:
     
    可见,这种类型的OOM就很好定位原因了,一般都可以从OOM后的log中得出分析定位。
    如下例子,我们在Activity中的ImageView放置一张未优化的特大的(30多M)高清图片,运行直接崩溃如下:
    //抛出OOM异常
    10-10 09:01:04.873 11703-11703/? E/art: Throwing OutOfMemoryError "Failed to allocate a 743620620 byte allocation with 4194208 free bytes and 239MB until OOM"
    10-10 09:01:04.940 11703-11703/? E/art: Throwing OutOfMemoryError "Failed to allocate a 743620620 byte allocation with 4194208 free bytes and 239MB until OOM"
    //堆栈打印
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: FATAL EXCEPTION: main
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: Process: com.example.application, PID: 11703
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime: java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.application/com.example.myapplication.MainActivity}: android.view.InflateException: Binary XML file line #21: Error inflating class <unknown>
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2610)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2684)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.app.ActivityThread.access$800(ActivityThread.java:177)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1542)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.os.Handler.dispatchMessage(Handler.java:111)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.os.Looper.loop(Looper.java:194)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.app.ActivityThread.main(ActivityThread.java:5743)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at java.lang.reflect.Method.invoke(Native Method)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at java.lang.reflect.Method.invoke(Method.java:372)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:988)
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:783)
    //出错地点,原因是21行的ImageView设置的src是一张未优化的31M的高清图片
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:  Caused by: android.view.InflateException: Binary XML file line #21: Error inflating class <unknown>
    10-10 09:01:04.958 11703-11703/? E/AndroidRuntime:     at android.view.LayoutInflater.createView(LayoutInflater.java:633)
    通过上面的log可以很方便的看出来问题原因所在地,那接下来的做法就是优化呗,降低图片的相关规格即可(譬如使用BitmapFactory的Option类操作等)。
    PS:提醒一句的是记得应用所属的内存是区分Java堆和native堆的!
    3-3-3 Android应用规避内存溢出OOM建议
    还是那句话,等待OOM发生是为时已晚的事,我们应该将其扼杀于萌芽之中,至于如何在开发中规避OOM,如下给出一些我们应用开发中的常用的策略建议:
    时刻记得不要加载过大的Bitmap对象;譬如对于类似图片加载我们要通过BitmapFactory.Options设置图片的一些采样比率和复用等,具体做法点我参考官方文档,不过过我们一般都用fresco或Glide开源库进行加载。
    优化界面交互过程中频繁的内存使用;譬如在列表等操作中只加载可见区域的Bitmap、滑动时不加载、停止滑动后再开始加载。
    有些地方避免使用强引用,替换为弱引用等操作。
    避免各种内存泄露的存在导致OOM。
    对批量加载等操作进行缓存设计,譬如列表图片显示,Adapter的convertView缓存等。
    尽可能的复用资源;譬如系统本身有很多字符串、颜色、图片、动画、样式以及简单布局等资源可供我们直接使用,我们自己也要尽量复用style等资源达到节约内存。
    对于有缓存等存在的应用尽量实现onLowMemory()和onTrimMemory()方法。
    尽量使用线程池替代多线程操作,这样可以节约内存及CPU占用率。
    尽量管理好自己的Service、Thread等后台的生命周期,不要浪费内存占用。
    尽可能的不要使用依赖注入,中看不中用。
    尽量在做一些大内存分配等可疑内存操作时进行try catch操作,避免不必要的应用闪退。
    尽量的优化自己的代码,减少冗余,进行编译打包等优化对齐处理,避免类加载时浪费内存。
    可以发现,上面只是列出了我们开发中常见的导致OOM异常的一些规避原则,还有很多相信还没有列出来,大家可以自行追加参考即可。
    3-4 Android内存性能优化总结
    无论是什么电子设备的开发,内存问题永远都是一个很深奥、无底洞的话题,上面的这些内存分析建议也单单只是Android应用开发中一些常见的场景而已,真正的达到合理的优化还是需要很多知识和功底的。
    合理的应用架构设计、设计风格选择、开源Lib选择、代码逻辑规范等都会决定到应用的内存性能,我们必须时刻头脑清醒的意识到这些问题潜在的风险与优劣,因为内存优化必须要有一个度,不能一味的优化,亦不能置之不理。
    4 Android应用API使用及代码逻辑性能分析
    在我们开发中除过常规的那些经典UI、内存性能问题外其实还存在很多潜在的性能优化、这种优化不是十分明显,但是在某些场景下却是非常有必要的,所以我们简单列举一些常见的其他潜在性能优化技巧,具体如下探讨。
    4-1 Android应用String/StringBuilder/StringBuffer优化建议
    字符串操作在Android应用开发中是十分常见的操作,也就是这个最简单的字符串操作却也暗藏很多潜在的性能问题,下面我们实例来说说。
    先看下面这个关于String和StringBuffer的对比例子:
    //性能差的实现
    String str1 = "Name:";
    String str2 = "GJRS";
    String Str = str1 + str2;
    //性能好的实现
    String str1 = "Name:";
    String str2 = "GJRS";
    StringBuffer str = new StringBuilder().append(str1).append(str2);
    通过这个例子可以看出来,String对象(记得是对象,不是常量)和StringBuffer对象的主要性能区别在于String对象是不可变 的,所以每次对String对象做改变操作(譬如“+”操作)时其实都生成了新的String对象实例,所以会导致内存消耗性能问题;而 StringBuffer对象做改变操作每次都会对自己进行操作,所以不需要消耗额外的内存空间。
    我们再看一个关于String和StringBuffer的对比例子:
    //性能差的实现
    StringBuffer str = new StringBuilder().append("Name:").append("GJRS");
    //性能好的实现
    String Str = "Name:" + "GJRS";
    1
    2
    3
    4
    在这种情况下你会发现StringBuffer的性能反而没有String的好,原因是在JVM解释时认为
    String Str = "Name:" + "GJRS";就是String Str = "Name:GJRS";,所以自然比StringBuffer快了。
    可以发现,如果我们拼接的是字符串常量则String效率比StringBuffer高,如果拼接的是字符串对象,则StringBuffer比 String效率高,我们在开发中要酌情选择。当然,除过注意StringBuffer和String的效率问题,我们还应该注意另一个问题,那就是 StringBuffer和StringBuilder的区别,其实StringBuffer和StringBuilder都继承自同一个父类,只是 StringBuffer是线程安全的,也就是说在不考虑多线程情况下StringBuilder的性能又比StringBuffer高。
    PS:如果想追究清楚他们之间具体细节差异,麻烦自己查看实现源码即可。
    4-2 Android应用OnTrimMemory()实现性能建议
    OnTrimMemory是Android 4.0之后加入的一个回调方法,作用是通知应用在不同的情况下进行自身的内存释放,以避免被系统直接杀掉,提高应用程序的用户体验(冷启动速度是热启动的 2~3倍)。系统会根据当前不同等级的内存使用情况调用这个方法,并且传入当前内存等级,这个等级有很多种,我们可以依据情况实现不同的等级,这里不详细 介绍,但是要说的是我们应用应该至少实现如下等级:
    TRIM_MEMORY_BACKGROUND
    内存已经很低了,系统准备开始根据LRU缓存来清理进程。这时候如果我们手动释放一些不重要的缓存资源,则当用户返回我们应用时会感觉到很顺畅,而不是重新启动应用。
    可以实现OnTrimMemory方法的系统组件有Application、Activity、Fragement、
    Service、ContentProvider;关于OnTrimMemory释放哪些内存其实在架构阶段就要考虑清楚哪些对象是要常驻内存的,哪些是伴随组件周期存在的,一般需要释放的都是缓存。
    如下给出一个我们项目中常用的例子:
    @Override
    public void onTrimMemory(int level) {
       if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
           clearCache();
       }
    }
    通常在我们代码实现了onTrimMemory后很难复显这种内存消耗场景,但是你又怕引入新Bug,想想办法测试。好在我们有一个快捷的方式来模拟触发该水平内存释放,如下命令:
    adb shell dumpsys gfxinfo packagename -cmd trim value
    1
    packagename为包名或者进程id,value为ComponentCallbacks2.java里面定义的值,可以为80、60、40、20、5等,我们模拟触发其中的等级即可。
    4-3 Android应用HashMap与ArrayMap及SparseArray优化建议
    在Android开发中涉及到数据逻辑部分大部分用的都是Java的API(譬如HashMap),但是对于Android设备来说有些Java的 API并不适合,可能会导致系统性能下降,好在Google团队已经意识到这些问题,所以他们针对Android设备对Java的一些API进行了优化, 优化最多就是使用了ArrayMap及SparseArray替代HashMap来获得性能提升。
    HashMap:
    HashMap内部使用一个默认容量为16的数组来存储数据,数组中每一个元素存放一个链表的头结点,其实整个HashMap内部结构就是一个哈希 表的拉链结构。HashMap默认实现的扩容是以2倍增加,且获取一个节点采用了遍历法,所以相对来说无论从内存消耗还是节点查找上都是十分昂贵的。
    SparseArray:
    SparseArray比HashMap省内存是因为它避免了对Key进行自动装箱(int转Integer),它内部是用两个数组来进行数据存储 的(一个存Key,一个存Value),它内部对数据采用了压缩方式来表示稀疏数组数据,从而节约内存空间,而且其查找节点的实现采用了二分法,很明显可 以看见性能的提升。
    ArrayMap:
    ArrayMap内部使用两个数组进行数据存储,一个记录Key的Hash值,一个记录Value值,它和SparseArray类似,也会在查找时对Key采用二分法。
    有了上面的基本了解我们可以得出结论供开发时参考,当数据量不大(千位级内)且Key为int类型时使用SparseArray替换HashMap 效率高;当数据量不大(千位级内)且数据类型为Map类型时使用ArrayMap替换HashMap效率高;其他情况下HashMap效率相对高于二者。
    4-4 Android应用ContentProviderOperation优化建议
    ContentProvider是Android应用开发的核心组件之一,有时候在开发中需要使用ContentProvider对多行数据进行操 作,我们的做法一般是多次调运相关操作方法,殊不知这种实现方式是非常低性能的,取而代之的做法应该是使用批量操作,具体为了使批量更新、插入、删除数据 操作更加方便官方提供了ContentProviderOperation工具类。所以在我们开发中遇到类似情景时请务必使用批量操作,具体的优势如下:
    所有的操作都在一个事务中执行,可以保证数据的完整性。
    批量操作在一个事务中执行,所以只用打开、关闭一个事务。
    减轻应用程序与ContentProvider间的多次频繁交互,提升性能。
    可以看见,这对于数据库操作来说是一个非常有用的优化措施,烦请务必重视(我们项目优化过,的确有很大提升)。
    4-5 Android应用其他逻辑优化建议
    关于API及逻辑性能优化其实有多知识点的,这里无法一一列出,只能给出一些重要的知识点,下面再给出一些常见的优化建议:
    避免在Android中使用Java的枚举类型,因为编译后不但占空间,加载也费时,完全没有static final的变量好用、高效。
    Handler发送消息时尽量使用obtain去获取已经存在的Message对象进行复用,而不是新new Message对象,这样可以减轻内存压力。
    在使用后台Service时尽量将能够替换为IntentService的地方替换为此,这样可以减轻系统压力、省电、省内存、省CPU占用率。
    在当前类内部尽量不要通过自己的getXXX、setXXX对自己内部成员进行操作,而是直接使用,这样可以提高代码执行效率。
    不要一味的为了设计模式而过分的抽象代码,因为代码抽象系数与代码加载执行时间成正比。
    尽量减少锁个数、减小锁范围,避免造成性能问题。
    合理的选择使用for循环与增强型for循环,譬如不要在ArrayList上使用增强型for循环等。
    哎呀,类似的小优化技巧有很多,这里不一一列举了,自行发挥留意即可。
    5 Android应用移动设备电池耗电性能分析
    有了UI性能优化、内存性能优化、代码编写优化之后我们在来说说应用开发中很重要的一个优化模块—–电量优化。
    5-1 Android应用耗电量概念
    在盒子等开发时可能电量优化不是特别重视(视盒子待机真假待机模式而定),但是在移动设备开发中耗电量是一个非常重要的指标,如果用户一旦发现我们的应用非常耗电,不好意思,他们大多会选择卸载来解决此类问题,所以耗电量是一个十分重要的问题。
    关于我们应用的耗电量情况我们可以进行定长时间测试,至于具体的耗电量统计等请参考此文,同时我们还可以直接通过Battery Historian Tool来查看详细的应用电量消耗情况。最简单常用办法是通过命令直接查看,如下:
    adb shell dumpsys batterystats
    1
    其实我们一款应用耗电量最大的部分不是UI绘制显示等,常见耗电量最大原因基本都是因为网络数据交互、GPS定位、大量内存性能问题、冗余的后台线程和Service等造成。
    5-2 Android应用耗电量优化建议
    优化电量使用情况我们不仅可以使用系统提供的一些API去处理,还可以在平时编写代码时就养成好的习惯。具体的一些建议如下:
    在需要网络的应用中,执行某些操作前尽量先进行网络状态判断。
    在网络应用传输中使用高效率的数据格式和解析方法,譬如JSON等。
    在传输用户反馈或者下载OTA升级包等不是十分紧急的操作时尽量采用压缩数据进行传输且延迟到设备充电和WIFI状态时进行。
    在有必要的情况下尽量通过PowerManager.WakeLock和JobScheduler来控制一些逻辑操作达到省电优化。
    对定位要求不太高的场景尽量使用网络定位,而不是GPS定位。
    对于定时任务尽量使用AlarmManager,而不是sleep或者Timer进行管理。
    尽可能的减少网络请求次数和减小网络请求时间间隔。
    后台任务要尽可能少的唤醒CPU,譬如IM通信的长连接心跳时间间隔、一些应用的后台定时唤醒时间间隔等要设计合理。
    特殊耗电业务情况可以进行弹窗等友好的交互设计提醒用户该操作会耗用过多电量。
    可以看见,上面只是一些常见的电量消耗优化建议。总之,作为应用开发者的我们要意识到电量损耗对于用户来说是非常敏感的,只有我们做到合理的电量优化才能赢得用户的芳心。
    6 Android应用开发性能优化总结
    性能优化是一个很大的话题,上面我们谈到的只是应用开发中常见的性能问题,也是应用开发中性能问题的冰山一角,更多的性能优化技巧和能力不是靠看出 来,而是靠经验和实战结果总结出来的,所以说性能优化是一个涉及面非常广的话题,如果你想对你的应用进行性能你必须对你应用的整个框架有一个非常清晰的认 识。
    当然了,如果在我们开发中只是一味的追求各种极致的优化也是不对的。因为优化本来就是存在风险的,甚至有些过度的优化会直接导致项目的臃肿,所以不要因为极致的性能优化而破坏掉了你项目的合理架构。
    总之一句话,性能优化适可而止,请酌情优化。
    PS:附上Google关于Android开发的一些专题建议视频链接,不过在天朝需要自备梯子哦。








































    http://android.jobbole.com/81944/
    给 App 提速:Android 性能优化总结
    我在几周前的 Droidcon NYC 会议上,做了一个关于 Android 性能优化的报告。
    我花了很多时间准备这个报告,因为我想要展示实际例子中的性能问题,以及如何使用适合的工具去确认它们 。但由于没有足够时间来展示所有的一切,我不得不将幻灯片的内容减半。在本文中,将总结所有我谈到的东西,并展示那些我没有时间讨论的例子。
    你可以在这里观看报告视频。
    幻灯片在这里可以看到。
    现在,让我们仔细查看一些我之前谈过的重要内容 ,但愿我可以非常深入地解释一切。那就先从我在优化时遵循的基本原则开始:
    我的原则
    每当处理或者排查性能问题的时候,我都遵循这些原则:
    持续测量: 用你的眼睛做优化从来就不是一个好主意。同一个动画看了几遍之后,你会开始想像它运行地越来越快。数字从来都不说谎。使用我们即将讨论的工具,在你做改动的前后多次测量应用程序的性能。
    使用慢速设备:如果你真的想让所有的薄弱环节都暴露出来,慢速设备会给你更多帮助。有的性能问题也许不会出现在更新更强大的设备上,但不是所有的用户都会使用最新和最好的设备。
    权衡利弊 :性能优化完全是权衡的问题。你优化了一个东西 —— 往往是以损害另一个东西为代价的。很多情况下,损害的另一个东西可能是查找和修复问题的时间,也可能是位图的质量,或者是应该在一个特定数据结构中存储的大量数据。你要随时做好取舍的准备。
    Systrace
    Systrace 是一个你可能没有用过的好工具。因为开发者不知道要如何利用它提供的信息。
    Systrace 告诉我们当前大致有哪些程序运行在手机上。这个工具提醒我们,手中的电话实际上是一个功能强大的计算机,它可以同时做很多事情。在SDK 工具的最近的更新中,这个工具增强了从数据生成波形图的功能,这个功能可以帮助我们找到问题。让我们观察一下,一个记录文件长什么样子:
     
    你可以用 Android Device Monitor 工具或者用命令行方式产生一个记录文件。在这里可以找到更多的信息。
    我在视频中解释了不同的部分。其中最有意思的就是警报(Alert)和帧(Frame),展示了对搜集数据的分析。让我们观察一个采集到的记录文件,在顶部选择一个警报:
     
    这个警报报告了有一个 View#draw() 调用费时较多。我们得到关于告警的描述,其中包含了关于这个主题的文档链接甚至是视频链接。检查帧下面那一行,我们看到绘制的每一帧都有一个标识,被标成 为绿色、黄色或者红色。如果标识是红色,就说明这帧在绘制时有一个性能问题。让我们选取一个红色的帧:
     
    我们在底部看到所有这帧相关的警报。一共有三个,其中之一是我们之前看到的。让我们放大这个帧并在底部把 “Inflation during ListView recycling” 这个警报报展开:
     
    我们看到这部分一共耗时 32 毫秒,超出了每分钟 60 帧的要求,这种情况下绘制每一帧的时间不能超过 16 毫秒。帧中 ListView 的每一项都有更多的时间信息 —— 每一项耗时 6 毫秒,我们一共有 5 项。其中的描述帮助我们理解这个问题,甚至还提供了一个解决方案。从上面的图中,我们看到所有内容都是可视化的,甚至可以放大“扩展” (“inflate”)片,来观察布局中的哪个视图(“View”)扩展时花了更久的时间。
    另一个帧绘制较慢的例子:
     
    选择一帧后,我们可以按下“m” 键来高亮并观察这部分花了多久。上图中,我们观察到绘制这帧花费了 19 毫秒。展开这帧对应的唯一警报,它告诉我们有一个“调度延迟”。
    调度延迟说明这个处理特定时间片的线程有很长时间没有被 CPU 调度。因此这个线程花了很长时间才完成。选择帧中最长的时间片以便获取更多的详细信息:
     
    墙上时间(Wall Duration)是指从时间片开始到结束所花费的时间。它被称为“墙上时间”,这是因为线程启动后就像观察一个挂钟(去记录这个时间)。
    CPU时间是 CPU 处理这个时间片所花费的实际时间。
    值得注意的是这两个时间有很大的不同。完成这个时间片花了 18 毫秒,但是 CPU 却只花费了 4 毫秒。这有点奇怪,现在是个好机会来抬头看看这整段时间里 CPU 都做了什么:
     
    CPU 的 4 个核心都相当忙碌。
    选择一个com.udinic.keepbusyapp 应用程序中的线程。这个例子中,一个不同的应用程序让 CPU 更加忙碌,而不是为我们的应用程序贡献资源。
    这种特殊场景通常是暂时的,因为其它的应用程序不会总是在后台独占 CPU(对吗?)。
    Traceview
    Traceview 是一个性能分析工具,告诉我们每一个方法执行了多长时间。让我们看一个跟踪文件:
     
    这个工具可以通过 Android Device Monitor 或者从代码中启动。更多信息请参考这里。
    让我们仔细查看这些不同的列:
    名称:此方法的名字,上图中用不同的颜色加以标识。
    CPU非独占时间:此方法及其子方法所占用的 CPU 时间(即所有调用到的方法)。
    CPU独占时间:此方法单独占用 CPU 的时间。
    非独占和独占的实际时间 :此方法从启动那一刻直到完成的时间。和 Systrace 中的“墙上时间”一样。
    调用和递归:此方法被调用的次数以及递归调用的数量。
    每次调用的 CPU 时间和实际时间 :平均每次调用此方法的 CPU 时间和实际时间。另一个时间字段显示了所有调用这个方法的时间总和。
    我打开一个滑动不流畅的应用程序。我启动追踪,滑动了一会然后关掉追踪。找到 getView() 这个方法然后把它展开,我看到下面的结果:
     
    此方法被调用了 12 次,每次调用 CPU 花费的时间是 3 毫秒,但每次调用实际花费的时间是 162 毫秒!这一定有问题……
    查 看了这个方法的子方法,可以看到总体时间都花费在哪些方法上。Thread.join() 占了 98% 左右的非独占实际时间。此方法用在等待其他线程结束。另一个子方法是 Thread.start(),我猜想 getView() 方法启动了一个线程然后等着它执行结束。
    但这个线程在哪里呢?
    因为 getView() 不直接做这件事情,所以 getView() 没有这样的子线程。为找到它 ,我查找一个 Thread.run() 方法,这是生成一个新线程所调用的方法。我追踪这个方法直至找到元凶:
     
    我发现每次调用 BgService.doWork() 方法大约花费 14 毫秒,一共调用了 40 次 。每次 getView() 都有可能不止一次调用它,这就可以解释为什么每次调用 getView() 需要花费这么长时间。此方法让 CPU 长时间处于忙碌状态。再查看一下 CPU 独占 时间,我们看到它                      

    http://blog.csdn.net/innost/article/details/9008691

    Android系统性能调优工具介绍
    Android系统性能调优工具介绍
    在软件开发过程中,想必很多读者都遇到过系统性能问题。而解决系统性能问题的几个主要步骤是:
    测评:对系统进行大量有针对性的测试,以得到合适的测试数据。
    分析系统瓶颈:分析测试数据,找到其中的hotspot(热点,即bottleneck)。
    性能优化:对hotspot相关的代码进行优化。
    由上述步骤可知,性能优化的目标对象是hotspot。如果找到的hotspot并非真正的热点,则性能优化的结果必然是事倍功半甚至竹篮打水一场空。所以,作为Android性能调优相关知识的第一部分,本篇首先将向读者介绍Android平台中三个重要的性能测试工具,它们能很好得帮助开发者找到hotspot。
     
    一Traceview介绍
    1.1  Traceview简介
    Traceview是Android平台特有的数据采集和分析工具,它主要用于分析Android中应用程序的hotspot。Traceview本身只是一个数据分析工具,而数据的采集则需要使用Android SDK中的Debug类或者利用DDMS工具。二者的用法如下:
    开 发者在一些关键代码段开始前调用Android SDK中Debug类的startMethodTracing函数,并在关键代码段结束前调用stopMethodTracing函数。这两个函数运行过 程中将采集运行时间内该应用所有线程(注意,只能是Java线程)的函数执行情况,并将采集数据保存到/mnt/sdcard/下的一个文件中。开发者然 后需要利用SDK中的Traceview工具来分析这些数据。
    借助Android SDK中的DDMS工具。DDMS可采集系统中某个正在运行的进程的函数调用信息。对开发者而言,此方法适用于没有目标应用源代码的情况。DDMS工具中Traceview的使用如图1-1所示。
     
    图1-1  DDMS中Traceview使用示意图
    点击图1-1中所示按钮即可以采集目标进程的数据。当停止采集时,DDMS会自动触发Traceview工具来浏览采集数据。
    下面,我们通过一个示例程序向读者介绍Debug类以及Traceview的使用。
    1.2  Traceview示例分析
    示例程序运行时的界面如图1-2所示:
     
    图1-2  示例界面图
    图1-2中:
    SystraceDemoStringAAA等字样是TraceviewDemo程序启动时ListView最初显示的字符串。
    当用户点击ListView中的某一项时,Traceview将计算对应项当前显示的字符串的MD5值40次,然后用计算得到的MD5字符串替换该项之前显示的内容。其效果如图1-2中的“MD5值“箭头所示。
    该示例的关键代码如图1-3所示:
     
    图1-3示例代码
    由图1-3可知:
    左图中,Debug类的startMethodTracing和stopMethodTracing分别在MainAcvtivity的构造方法和onDestroy函数中调用。
    onCreate函数中我们设置了第一个hotspot,即getStringToShow函数。它将解析一个XML文件,并将解析后的字符串保存到mListItem中以作为ListView的显示内容。
    右图中,当用户点击ListView中的某个Item时,程序在onListItem中将计算MD5值40次,然后用计算结果做为被点击项的新字符串显示。generateMD5中的函数是本示例的第二个hotspot。
    现在,我们用Traceview工具将测试结果文件TraceviewDemo.trace打开。
    Traceview界面比较复杂,其UI划分为上下两个面板,即Timeline Panel(时间线面板)和Profile Panel(分析面板)。图1-4所示为Timeline Panel界面:
     
    图1-4  Traceview Timeline Panel示意图
    图1-4中的Timeline Panel又可细分为左右两个Pane:
    左边Pane显示的是测试数据中所采集的线程信息。由图1-4可知,本次测试数据采集了main线程,两个Binder线程和其它系统辅助线程(例如GC线程等)的信息。
    右边Pane所示为时间线,时间线上是每个线程测试时间段内所涉及的函数调用信息。这些信息包括函数名、函数执行时间等。由图1-4可知,main线程对应行的的内容非常丰富,而其他线程在这段时间内干得工作则要少得多。
    另外,开发者可以在时间线Pane中移动时间线纵轴。纵轴上边将显示当前时间点中某线程正在执行的函数信息。
    现在来看Traceview的Profile Panel界面,如图1-5所示:
     
    图1-5  TraceviewProfile Panel界面
    Profile Panel是Traceview的核心界面,其内涵非常丰富。它主要展示了某个线程(先在Timeline Panel中选择线程)中各个函数调用的情况,包括CPU使用时间、调用次数等信息。而这些信息正是查找hotspot的关键依据。所以,对开发者而言,一定要了解Profile Panel中各列的含义。笔者总结了其中几个重要列的作用,如表1-1所示:
    表1-1  Profile Panel各列作用说明
     
    列名    描述       
    Name    该线程运行过程中所调用的函数名       
    Incl Cpu Time    某函数占用的CPU时间,包含内部调用其它函数的CPU时间       
    Excl Cpu Time    某函数占用的CPU时间,但不含内部调用其它函数所占用的CPU时间       
    Incl Real Time    某函数运行的真实时间(以毫秒为单位),内含调用其它函数所占用的真实时间       
    Excl Real Time    某函数运行的真实时间(以毫秒为单位),不含调用其它函数所占用的真实时间       
    Call+Recur Calls/Total    某函数被调用次数以及递归调用占总调用次数的百分比       
    Cpu Time/Call    某函数调用CPU时间与调用次数的比。相当于该函数平均执行时间       
    Real Time/Call    同CPU Time/Call类似,只不过统计单位换成了真实时间    
    另外,每一个Time列还对应有一个用时间百分比来统计的列(如Incl Cpu Time列对应还有一个列名为Incl Cpu Time %的列,表示以时间百分比来统计的Incl Cpu Time)。
    了解完Traceview的UI后,现在介绍如何利用Traceview来查找hotspot。
    一般而言,hotspot包括两种类型的函数:
    一类是调用次数不多,但每次调用却需要花费很长时间的函数。在示例代码中,它就是hotspot 1。
    一类是那些自身占用时间不长,但调用却非常频繁的函数。在示例代码中,它就是hotspot 2。
    首先,我们来查找hotspot 1。
    在Profile Panel中,选择按Cpu Time/Call进行降序排序(从上之下排列,每项的耗费时间由高到低),得到如图1-6所示的结果:
     
    图1-6  按CPU Time/Call降序排列数据
    图1-6中:
    MainActivity.onCreate是应用程序中的函数,它耗时为4618.684。然后,点击MainActivity.onCreate项,得到箭头所示的小图。
    小图中,Parents一行显示的是MainActivity.onCreate的调用者,本例中它是performCreate函数。这部分代码属于Framework部分。Children行显示的是MainActivity.onCreate调用的子函数。
    在 MainActivity.onCreate调用的子函数中,我们发现getStringsToShow在Incl Cpu Time %一列中占据了63.3%,它是onCreate子函数耗费时间最长的,而且Calls+Recur Calls/Total列显示其调用次数为1,即它仅仅被调用一次了。这个函数是应用程序实现的,所以极有可能是一个潜在的Hotspot。
    另外,由于笔者已经知道getStringsToShow是示例应用自己实现的函数,故在图1-6的大图中,可直接根据MainActivity.getStringsToShow花费了2921.913CPU时间这个信息来确定Hotspot就是它。
    相对来说,类型1的hotspot比较好找,步骤是先按降序对时间项进行排列(可以是时间百分比、真实时间或CPU时间),然后查找耗费时间最多的函数。一般而言,先应对应用程序自己实现的函数进行排查,Framework的函数也有可能是hotspot,但主因一般还是在应用本身(例如设置复杂的界面,导致对应XML解析非常慢)。
    现在,我们来看如何查找类型2的hotspot。
    点击Call/Recur Calls/Total列头,使之按降序排列。关注点放在那些调用频繁并且占用资源较多的函数。图1-7为降序排列的结果图。
     
    图1-7类型2 Hotspot查找过程示意之一
    图1-7所示的运行最频繁的几个函数中,我们发现了几个怀疑点,由图中的1和2箭头标示。
    结 合代码,箭头1所指的函数在代码中实际并不存在。这是因为代码中直接访问了内部类的私有成员,导致java编译器在编译时自动生成了这个函数。这个函数的 调用次数非常多。所以,为了提高效率,我们可以修改内部类成员的访问类型定义为public。不过,该函数的Incl Cpu Time并不高,只有3.2%。
    同样,箭头2所指部分的函数调用次数也很多,达到了5888多次。不过它们占用的时间百分比只有0.9%。
    第一次查找的潜在点被排除后,继续浏览数据,得到如图1-8所示的结果。
     
    图1-8  类型2 Hotspot查找过程示意之二
    在图1-8中:
    红框处有两个重载的MyMD5.getHashString函数调用,它们各运行了368次,而且占用的CPU时间百分比达到了31.8%和53.2%。很显然,这2处调用就有优化的余地,这就是我们所怀疑的hotspot2。
    找到hotspot之后,开发者就需要结合代码来进行对应的优化了。关于Java代码优化,读者可参考如下资料:http://developer.android.com/training/articles/perf-tips.html
    总体而言,Hotspot的查找是一个细致的工作,需要开发者对目标程序的代码,以及Traceview工具都比较熟悉才行。
    1.3  Traceview小结
    Traceview工具是Android平台应用程序性能分析的利器。不过笔者觉得它的UI还是有些复杂。并且使用时感觉流畅度不够好。
    Google官方关于Traceview的介绍可参考以下链接,不过其内容以及较久未更新了。http://developer.android.com/tools/debugging/debugging-tracing.html。
     
    二Systrace介绍
    2.1  Systrace简介
    Systrace是Android4.1中新增的性能数据采样和分析工具。它可帮助开发者收集Android关键子系统(如surfaceflinger、WindowManagerService等Framework部分关键模块、服务)的运行信息,从而帮助开发者更直观的分析系统瓶颈,改进性能。
    Systrace的功能包括跟踪系统的I/O操作、内核工作队列、CPU负载以及Android各个子系统的运行状况等。在Android平台中,它主要由3部分组成:
    内核部分:Systrace利用了Linux Kernel中的ftrace功能。所以,如果要使用Systrace的话,必须开启kernel中和ftrace相关的模块。
    数据采集部分:Android定义了一个Trace类。应用程序可利用该类把统计信息输出给ftrace。同时,Android还有一个atrace程序,它可以从ftrace中读取统计信息然后交给数据分析工具来处理。
    数 据分析工具:Android提供一个systrace.py(python脚本文件,位于Android SDK目录/tools/systrace中,其内部将调用atrace程序)用来配置数据采集的方式(如采集数据的标签、输出文件名等)和收集 ftrace统计数据并生成一个结果网页文件供用户查看。
    从本质上说,Systrace是对Linux Kernel中ftrace的封装。应用进程需要利用Android提供的Trace类来使用Systrace。Android 4.1为系统中的几个关键进程和模块都添加了Systrace功能。以显示系统中重要模块Hwcomposer为例,其代码中使用Systrace的方法如图2-1所示:
     
    图2-1  Hwcomposer模块Systrace使用示例
    图2-1中,应用程序只要通过三个宏就可使用Systrace了:
    定义ATRACE_TAG:Hwcomposer使用了ATRACE_TAG_GRAPHICS,表示它和Graphics相关。
    ATRACE_INIT:用于统计某个变量使用的情况。下文将见到代码中”VSYNC”的统计结果。
    ATRACE_CALL:用于统计函数的调用情况。
    由于篇幅关系,关于Trace使用更多的信息请读者阅读frameworks/native/include/utils/Trace.h或者android.os.Trace类。下面,我们通过一个示例来展示Systrace的使用。
    2.2  Systrace实例
    首先,在PC机上运行如下命令以启动Systrace,如图2-2所示:
     
    图2-2  Systrace操作步骤
    执行上述命令后,将得到一个名为trace.html的文件(trace.html是默认文件名,读者也可在命令行中指定其他文件名)。通过浏览器打开此文件,结果如图2-3所示:
     
    图 2-3  trace.html内容示意
    图2-3中所示的trace.html页面内容和Traceview的Timeline Panel非常类似。图中包含的内容如下:
    由 于在systrace.py中指定了-f -l和-i参数,Systrace将生成CPU频率、负载和状态相关的信息。它们为图2-1中第一个红框所示。由于笔者所测手机CPU为双核,故图中有 CPU 0和CPU 1之分。为行文方便,笔者用CPU N来指代CPU的某个核。
    “CPU N“所示行对应于整个测试时间内,某个核上运行的进程信息。
    “CPU N C-State“所示行为整个测试时间内,某个CPU状态的变化。C-State取值见表2-1。
    “CPU N Clock Frequency”所示行展示了某个CPU运行的频率。通过点击这一行的色块可以查看某个时间点上CPU N的运行频率。
    “cpufreq”: 该行所示内容和CPU交互式频率调节器(Interactive Governor)的工作有关。交互式CPU调节器驱动添加了对CPU频率调节事件的跟踪。感兴趣的读者不妨阅读kernel中的 include/trace/events/cpufreq_interactive.h文件以了解更多的信息。
    图2-1中,CPU信息以下的行就是通过Trace.h提供的宏而添加的统计信息,其中:
    VSYNC:这一行的统计信息来自于图2-1中ATRACE_INIT宏的使用。在Hwcomposer代码中,ATRACE_INIT宏被用于统计VSYNC[1]的Tick-Tack情况(即0,1,0,1交错输出)。VSYNC行显示了每次Tick Tack的时间大概都在16ms左右。
    由 于Framework代码也在显示部分添加了ATRACE_INIT的使用,所以图中 com.example.systracedemo/com.example.systracedemo.MainActivity所示为应用程序占用显 示Buffer的Tick-Tack情况。如果使用时间超过16ms,将导致界面显示迟滞等现象。
    SurfaceFlinger使用了ATRACE_CALL宏,故图中SurfaceFlinger行展示了其函数调用的CPU耗时情况(如箭头1所指,SurfaceFlinger中的onMessageReceived函数的运行信息)。
    在图2-1最下部的方框中,详细显示了当前鼠标在时间线中选择的部分(即SurfaceFlinger中的onMessageReceived)的详细信息。
    表2-1所示为CPU状态取值信息:
    表2-1  CPU状态
     
    C-state    描述       
    C-0    RUN MODE,运行模式。       
    C-1    STANDBY,就位模式,随时准备投入运行       
    C-2    DORMANT,休眠状态,被唤醒投入运行时有一定的延迟       
    C-3    SHUTDOWN,关闭状态,需要有较长的延迟才能进入运行状态,减少耗电    
    2.3  Systrace小结
    总体来说,Systrace比Traceview用途更广泛,它支持对CPU、Native进程甚至Kernel线程进行性能数据采样,可帮助开发者对整个系统的性能情况进行一个详尽的分析。不过其用法比Traceview要复杂,而且还需要对Kernel做一些配置调整。
    Android官方对Systrace也有一些介绍,请读者阅读:
    http://developer.android.com/tools/debugging/systrace.html
    三Oprofile的使用
    3.1  Oprofile简介
    Oprofile是另一个功能更强大的性能数据采集和分析工具,其工作原理如下:
    它利用性能计数器(Performance Counter)或者定时器(针对kernel不支持性能计数器的情况),通过连续的采样获得统计数据,从而对内核和用户空间进程进行性能分析。
    以 性能计数器为例,在系统运行过程中,当某个事件发生时,对应的性能计数器就会自加。当达到计数器的设定值时会产生一个中断。Oprofile驱动利用这个 中断来进行采样统计。通过获取中断发生时PC指针的值以及内核中保存运行的任务的信息等,并把它们转化成对测评有用的数据。
    Oprofile包括内核驱动和用户空间工具两个部分,其中:
    内 核驱动实现了一个oprofilefs虚拟文件系统。它挂载到/dev/oprofile,用来向用户空间报告数据和接收来自用户空间的设置。它是用户空 间进程与内核通信的桥梁。驱动中还包括了与架构相关和通用的驱动,通过它们访问性能计数器寄存器、收集数据后报告给用户空间。守护进程用户从内核接收数据 并保存在磁盘上以备分析使用。
    在用户空间提供了两个工具:oprofiled(作为守护进程在后台通过和/dev/oprofile交互以获取驱动收集的数据)、opcontrol(用户操作的控制工具,它通过读写oprofilefs来控制采样相关的设置)。
    Android默认提供了对Oprofile的支持,其组成包括:
    代码:位于exetrnal/oprofile中。不过,只有编译类型为非user的系统才会使用它。
    四个主要工具,即opcontrol,oprofiled、opreport和opimport。开发者只要使用opcontrol和opreport即可。
    读者应该熟练掌握opcontrol和oprofiled工具的作用,我们此处也总结了它们的用法:
    opcontrol:它用来控制采样过程,比如采样的开始和结束、采样的事件类型和频率等。其内部通过读写oprofilefs来实现。opcontrol的常用选项如表3-1所示:
    表3-1  opcontrol常用选项
     
    opcontrol选项    功能       
    --list-events    列出当前CPU所支持的事件       
    --setup    对测评进行设置,比如关闭旧的守护进程、挂载oprofilefs       
    --vmlinux=    设置将要分析的Android内核镜像文件       
    --callgraph    设置跟踪函数调用的层数       
    --kernel-range=start,end    内核二进制文件起始和结束的虚拟地址       
    --start/--stop    开始/停止采样       
    --event=name:count:unitmask:kernel:user    设置对某事件进行采样。
    Name:事件的名字
    Count:采样时事件发生的次数Unitmask:事件的掩码(CPU支持的事件以及掩码见oprofile的文档)
    Kernel:是否采样内核事件
    User:是否采样用户事件    
    opreport:opreport 是使用采样数据生成报告的工具,可根据用户要求生成不同的报告。一般用法是“opreport [options] [image]”,其中image指定报告需要显示的程序的名字(指程序名字、共享库名字和内核)。image参数可选。不指定它时,opreport将 打印所有进程的报告结果。常用options如表3-2所示:
    表3-2  opreport常用选项
     
    opreprt选项    功能       
    -l    显示函数调用的符号名字       
    -g    以调试的形式打印函数符号,包括函数所在文件及行数等。       
    -c    显示函数调用堆栈       
    -o    报告输出到指定文件    
    另外,Android提供了一个特别的工具opimport_pull。它可把采样数据从手机中pull到PC上,并对数据进行一些简单处理以供opreport使用。所以,在Android平台上,开发者只要使用opimport_pull了就可以了。
    现在,我们来看Oprofile的使用实例。
    3.2  Oprofile实例
    Oprofile的使用大体可以分成以下三步:
    内核加载oprofile驱动(如果该驱动静态编译到内核中,则可略过此步骤)。
    配置采样事件、然后进行采样。
    获取报告、进行分析,针对分析结果进行改进。
    下面分别来看这三个步骤:
    3.2.1  Oprofile内核配置
    如下所示为内核配置的示例,如图3-1所示:
     
    图3-1  Oprofile内核配置示意
    运行Oprofile需要root权限,所以目标设备中最好运行的是userdebug或者engineer版本的Android OS。
    3.2.2  Oprofile用户空间配置
    Oprofile用户空间配置的示例如图3-2所示。假设当前目录为Android源码根目录,并且已经初始化Android编译环境(执行完毕build/envsetup.sh和lunch)。
     
    图3-2  Oprofile用户空间配置示意
    用户空间的配置主要通过执行opcontrol命令来完成。而opcontrol内部是通过往oprofilefs传递对应的控制参数来完成的。例如图3-2中“opcontrol --callgraph=16”命令也可通过“echo 16> /dev/oprofile/backtrace_depth”来实现。
    3.2.3  结果分析
    在上一步中,我们已经获取了测评采样的数据。现在,就可以使用它们来生成采样报告了,方法如图3-3所示:
     
    图3-3  oprofile生成采样报告方法示意
    图3-4为报告的一部分内容:
     
    图3-4  Oprofile测评报告概要
    图3-4中,我们发现libc.so调用的采样数为117299,排第4位。那么libc.so中哪个函数调用次数最多呢?开发者可通过如下命令获取libc.so的更为详细的信息。方法如图3-5所示:
     
    图3-5  opreport使用示例
    执行上述命令后的结果如图3-6所示:
     
    图3-6  Oprofile关于libc的详细结果
    由图3-6可知,memcpy()函数占用最多的CPU资源。所以可以考虑优化memcpy()。
    此处,笔者针对Cortex-A9双核SMP处理器,使用ARM汇编的方法对memcpy进行了优化。优化后的结果如图3-7所示。对比图3-6和图3-7可明显看出,优化后的memcpy对资源的占用降低了2.7个百分点。
     
    图3-7  优化memcpy()后的测试结果
    3.3  Oprofile小结
    在性能分析中,Oprofile无疑是一个使用最广泛、功能最强大的测评工具。对于Android平台开发者来说,它可以采集和分析整个系统的运行状态信息,对于分析查找系统瓶颈进而优化系统具有重大意义。
     
    四  总结
    性能调优向来是一件“高深莫测”的任务,但打破它们神秘面纱的工具就是上文所述的工具了。所以,对有志开展这方面工作的读者而言,首要一步的工作就是先了解各个工具的作用及优缺点。
    除了本文介绍的这三个工具外,Android系统还支持其他一些更有针对性的测试工具,例如用于测评系统整体功能的lmbench,lttng、测试系统启动性能的bootchart、测试文件系统性能的iozone等。由于篇幅关系,笔者就不再一一介绍它们了。
     [1]关于VSYNC的详情,读者可参考http://blog.csdn.net/innost/article/details/8272867,“Android Project Butter分析“一文。




    http://blog.jobbole.com/78995/
    正确使用Android性能分析工具——TraceView
    前面唠叨
    最近公司app中有些列表在滑动的时候会有卡顿现象,我就开始着手解决这些问题,解决问题之前首先要分析列表滑动的性能瓶颈在什么地方。因为之前不会正确使用TraceView这个工具,主要是看不懂TraceView界面下方数据指标的值代表什么意思…以前我用StopWatch类来分析性能,现在觉得弱爆了…不过有些地方StopWatch工具类还是很简单好用的~
    网上可以找了很多博客来介绍这个工具的使用方法,很多都是讲解了一些一些就会的方法,讲一个大概,包括StackOverFlow上我也没有找到很好的讲解TraceView各个数据指标代码什么意思的回答
    因为我要解决列表滑动的卡顿问题,就必须要找到导致卡顿现象的原因,我就在StackOverFlow上找着别人零散的回答慢慢琢磨这个工具的使用方法。现在我学会了,至少能看懂每个指标什么意思,最后发现这个工具实在太强大了!!!
    TraceView界面
    现来看一下整个界面的图,整个界面包括上下两部分,上面是你测试的进程中每个线程的执行情况,每个线程占一行;下面是每个方法执行的各个指标的值
    上面一部分是你测试进程的中每个线程运行的时间线,下图中可以可以看到,主要只有一个main线程在执行,因为我滑动了一下列表,main线程(UI线程)正在进行绘制View呢~
    然后我点击了序号为133的一个方法io.bxbxbai.android.examples.activity.ExpandableLayoutMainActivity$SimpleAdapter.getItemView,就会出现两部分数据:
    Parents
    Children
    Parents表示调用133这个方法的父方法,可以看到序号为130。Children表示方法133调用的其他方法,可以看到有好几个方法。
     
    如何使用TraceView
    因为这次我主要是分析列表滑动卡顿问题,我就讲讲我是怎么使用这个工具的,并且我是怎么分析的。
    使用TraceView主要有两种方式:
    最简单的方式就是直接打开DDMS,选择一个进程,然后按上面的“Start Method Profiling”按钮,等红色小点变成黑色以后就表示TraceView已经开始工作了。然后我就可以滑动一下列表(现在手机上的操作肯定会很卡,因 为Android系统在检测Dalvik虚拟机中每个Java方法的调用,这是我猜测的)。操作最好不要超过5s,因为最好是进行小范围的性能测试。然后再按一下刚才按的按钮,等一会就会出现上面这幅图,然后就可以开始分析了。
    第2种方式就是使用android.os.Debug.startMethodTracing();和android.os.Debug.stopMethodTracing();方法,当运行了这段代码的时候,就会有一个trace文件在/sdcard目录中生成,也可以调用startMethodTracing(String traceName) 设置trace文件的文件名,最后你可以使用adb pull /sdcard/test.trace /tmp 命令将trace文件复制到你的电脑中,然后用DDMS工具打开就会出现第一幅图了
    第一种方式相对来说是一种简单,但是测试的范围很宽泛,第二中方式相对来说精确一点,不过我个人喜欢使用第一种,因为简单,而且它是检测你的某一个操作。因为第二中更适合检测某一个方法的性能,其实也没有那种好,看使用的场景和喜好了。。。
    看懂TraceView中的指标
     
    其实我今年7月份就已经开始使用TraceView工具了,但是当时不懂其中每个指标的含义,就没注意到它强大的地方。看不懂界面下方表格中的指标,这些数据其实一点意义都没有。
    网上包括Android官网也没有对TraceView工具的使用有详细的说明文档,这点确实比较蛋疼。
    纵轴
    TraceView界面下方表格中纵轴就是每个方法,包括了JDK的,Android SDK的,也有native方法的,当然最重要的就是app中你自己写的方法,有些Android系统的方法执行时间很长,那么有很大的可能就是你app中调用这些方法过多导致的。
    每个方法前面都有一个数字,可能是全部方法按照Incl CPU Time 时间的排序序号(后面会讲到)
    点一个方法后可以看到有两部分,一个是Parents,另一个是Children。
    Parent表示调用这个方法的方法,可以叫做父方法
    Children表示这个方法中调用的其他方法,可以叫做子方法
    横轴
     
    横轴上是很多指标,这些指标表示什么意思真的困扰了我很长一段时间。。。
    能够很衡量一个方法性能的指标应该只有时间了吧? 一个方法肯定就是执行时间越短约好咯~~
    1. Incl Cpu Time
    define inclusive : 全包括的
    上图中可以看到0(toplevel) 的Incl Cpu Time 占了100%的时间,这个不是说100%的时间都是它在执行,请看下面代码:
     
    1
    2
    3
    4
    5
    6    public void top() {
        a();
        b();
        c();
        d();
    }    
    Incl Cpu Time表示方法top执行的总时间,假如说方法top的执行时间为10ms,方法a执行了1ms,方法b执行了2ms,方法c执行了3ms,方法d执行 了4ms(这里是为了举个栗子,实际情况中方法a、b、c、d的执行总时间肯定比方法top的执行总时间要小一点)。
    而且调用方法top的方法的执行时间是100ms,那么:
     
            Incl Cpu Time       
    top        10%       
        a    10%       
        b    20%       
        c    30%       
        d    40%    
    从上面图中可以看到:
    toplevel的 Incl Cpu Time 是1110.943,而io.bxbxbai.android.examples.activity.ExpandableLayoutMainActivity$SimpleAdapter.getItemView方法的Incl Cpu Time为12.859,说明后者的Incl Cpu Time % 约为1.2%
    这个指标表示 这个方法以及这个方法的子方法(比如top方法中的a、b、c、d方法)一共执行的时间
    2. Excl Cpu Time
    理解了Incl Cpu Time以后就可以很好理解Excl Cpu Time了,还是上面top方法的栗子:
    方法top 的 Incl Cpu Time 减去 方法a、b、c、d的Incl Cpu Time 的时间就是方法top的Excl Cpu Time 了
    3. Incl Real Time
    这个感觉和Incl Cpu Time 差不多,第7条会讲到。
    4. Excl Real Time
    同上
    5. Calls + Recur Calls / Total
    这个指标非常重要!
    它表示这个方法执行的次数,这个指标中有两个值,一个Call表示这个方法调用的次数,Recur Call表示递归调用次数,看下图:
     
    我选中了一个方法,可以看到这个方法的Calls + Recur Calls 值是14 + 0,表示这个方法调用了14次,但是没有递归调用
    从Children这一块来看,很多方法调用都是13的倍数,说明父方法中有一个判断,但是这不是重点,有些Child方法调用Calls为26,这说明了这些方法被调用了两遍,是不是可能存在重复调用的情况?这些都是可能可以优化性能的地方。
    6. Cpu Time / Call
    重点来了!!!!!!!!!!
     
    这个指标应该说是最重要的,从上图可以看到,133这个方法的调用次数为20次,而它的Incl Cpu Time为12.859ms,那么133方法每一次执行的时间是0.643ms(133这个方法是SimpleAdapter的getItemView方法)
    对于一个adapter的getView方法来说0.643ms是非常快的(因为这个adapter中只有一个TextView,我为了测试用的)
    如果getView方法执行时间很长,那么必然导致列表滑动的时候产生卡顿现象,可以在getView方法的Children方法列表中找到耗时最长的方法,分析出现问题的原因:
    是因为有过多的计算?
    还是因为有读取SD卡的操作?
    还是因为adapter中View太复杂了?
    还是因为需要有很多判断,设置View的显示还是隐藏
    还是因为其他原因…
    7. Real Time / Call
    Real Time 和 Cpu Time 我现在还不太明白它们的区别,我的理解应该是:
    Cpu Time 应该是某个方法占用CPU的时间
    Real Time 应该是这个方法的实际运行时间
    为什么它们会有区别呢?可能是因为CPU的上下文切换、阻塞、GC等原因方法的实际执行时间要比Cpu Time 要稍微长一点。
    总结
    TraceView是一个非常强大的性能分析工具,因为Android 官网对这个工具的使用介绍文档很少,而且一些中文博客中写的也都是抄来抄去,没有讲到底怎么使用。
    最近我在做这方面的性能分析,就慢慢琢磨了这么工具的使用,发现非常强大,写下来总结一下。
    Android的性能分析工具还有很多,比如:
    Eclipse Memory Analyzer Tool 来分析Android app的内存使用
    Dump UI Hierarchy for UI Atomator,分析UI层级
    systrace
    其他

















    http://www.cnblogs.com/zhucai/p/weibo-graphics-performance-analyse.html
    为什么微博的app在iPhone比Android上流畅
    我来说下我所知道的事情。我不知道iOS为什么流畅,但我知道一些Android为什么不流畅的原因。

    首先,就题主所说的问题,我用iPad和小米Pad对比了一下微博滑动滚屏这件事情(2014年8月10日目前微博app最新版本)。正如题主所说,直观感受上明显感觉iOS要流畅、舒服。

    在这件事情上我认为主要是这三个原因:
    速度曲线。
    当你滑动界面然后松手,这时界面会继续滑动,然后速度减小,直到速度为0时停止。iOS下速度减小的这个过程比较慢,尤其是快要停的时候是慢慢停的,视觉上有种很顺滑的感觉;Android下则从松手到停要快很多,相比之下有种戛然而止的感觉。
    从数据/技术角度来看这个事情,我们滑动界面的最终目的不是为了“动”,而是为了“停”,因此只要平滑的到达目的地,似乎越快完成这个过程越好,所以Android的选择是理所当然的。但事实是,大家普遍更喜欢iOS的方式,这样做显得更顺滑、更优雅。
    帧率。
    绝大部分时间两者都能保持60FPS左右的满帧率。但都会有偶尔的掉帧。并且Android上 要比iOS上严重很多。(好吧,比起前两年,已经好太多了。)我前前后后滑动了几十次,iOS在前面遇到1次掉帧,后面就很稳定了。而Android几乎 每滑动一次都会伴随一次掉帧。这完全就是真真实实的卡顿,用户必然会感觉到那一刻的不流畅。Android掉帧的原因我后面再详细分析。
    触摸响应速度。
    从手指碰到触摸屏,到屏幕上显示处理这次触摸产生的画面,是需要时间的。时间越短感觉 越跟手。据说iOS的触摸屏的处理时间要比一般的Android手机快,这不是我的专长,不知道怎么验证。但在软件系统层面,Android的显示机制是 app-->SurfaceFlinger-->Display,这比传统的app-->Display多了一步,主要基于这个原因, 画面最终输出到屏幕要比传统的方式慢一帧(16.7ms)。

    我觉得第1点造成的影响最大,恰恰却是最技术/设备无关的。如果微博app或者Android系统要改变,很容易就可以调得跟iOS一模一样。但正是由于这是产品形态上的差别而不是纯粹技术上的优劣,反倒成了Android最不太可能改变的。

    第2点的影响其次,当然是指在目前这个大部分时候都能满帧的情况下。这方面是Android从早期到现在进步最明显的方面,使用了很多方法来优化帧率。但就算现在Android进化了很多,硬件性能也进化了很多,却仍旧不可能彻底消灭掉帧的情况。

    第3点通俗的讲就是跟手性,跟手性的重要性不言而喻,但现在的差别比较细微,且具体数据我也不清楚。
    我想过一个办法让桌面、微博这种内容和手一起动的应用绘制到屏幕的速度快一帧(16.7ms),其实就是抵消之前提到的慢的那一帧,需要framework层和app层一起配合改动,目前已申请了专利但代码还没进,将来有时间了应该会进到MIUI。感兴趣的可以看看专利:滑动操作响应方法、装置及终端设备。

    最后我来用专业技术分析一下微博app在Android里掉帧的原因。非编程人员可以不看下去了。(这个过程我使用的是小米3高通版+最新版微博app。)

    最初,我认为这种现象很像GC(垃圾回收)导致的,于是打开logcat观察每次卡顿的时候有没有GC发生。结果发现并没有。停下来的时候才会有GC,这说明微博app在滑动过程中控制得不错,在停下来的一刻才去分配内存,使GC不影响帧率。

    然 后我打开“开发者选项”->“GPU呈现模式分析”->“在屏幕上显示为条形图”(好像是4.4才有这个选项,之前的只能在dumpsys里 看),这会在屏幕上直观的显示每帧绘制花费的时间。屏幕上有条基准线大概是16ms,如果超过这条线则很有可能掉帧了。如果下面的蓝色部分很长则说明是软 件draw的部分太费时,那么可以通过traceview来继续分析draw的java代码。(我假定了现在的app都是硬件加速的方式。)如果中间红色 部分很长则说明是OpenGL ES绘制过程太费时。可以用gltrace来分析OpenGL ES的调用过程。
    打开选项后使用微博app,发现虽然偶尔有超基准线,但并不严重,并且卡顿的时候并没有明显异常。

    于是我开始使用systrace,这下就很明显了:
     
    可以发现每隔几帧,就会有一次大间隙,图中我圈了红圈圈的两个地方都明显超过正常的耗时时间。现在问题是,这些地方的代码在干什么呢?是什么花费了这么长时间?
     
    放大后发现都是这两个:obtainView和setupListItem。它们都是在draw之前调用的,这也解释了通过上一步的方法(“GPU呈现模式分析”)为什么没有显示出来,因为那个方法只展示draw里面的时间消耗。
    通过在源码里搜索obtainView和setupListItem,可以发现是AbsListView的obtainView方法和ListView的setupChild方法打下的这个log。

    接下来,我们用traceview来看看这两个方法里面究竟调用了什么代码导致耗时过长。
    跟踪obtainView最终到了这里:
     
    代码混淆了,看不太出来了,也懒得再跟下去了。但随便点了点然后看到:
     
    TextView.setText用了挺多的时间。感觉Android团队应该优化优化这个方法。
    然后再来跟踪setupChild方法:
     
    都是measure占用的时间,并且调用次数比较多。这应该说明了View的数量不少。用hierarchyviewer看了下,的确不少,一条微博有50多个View:
     
    结论:我们最终大致找到了耗时的代码及部分原因。这两个耗时方法应该都是有可能减下来的,但应该不那么容易。
    在Android上实现功能比较容易,但如果默认实现有了性能问题,要想解决就不好说了,有时候要绕很远。










































    http://blog.csdn.net/ddna/article/details/5527072
    【Android工具】被忽略的UI检视利器:Hierarchy Viewer
            Hierarchy Viewer是随AndroidSDK发布的工具,位置在tools文件夹下,名为hierarchyviewer.bat。它是Android自带的非常有用而且使用简单的工具,可以帮助我们更好地检视和设计用户界面(UI),绝对是UI检视的利器,但是好像很少有人提它,难道是因为太简单?
     
    具体来说主要功能有2个:
    1.       从可视化的角度直观地获得UI布局设计结构和各种属性的信息,帮助我们优化布局设计;
    2.       结合debug帮助观察特定的UI对象进行invalidate和requestLayout操作的过程。
     
    1.       基本使用方法
    (1)hierarchyviewer的使用非常简单,启动模拟器或者连接上真机后,启动hierarchyviewer.bat,会看到下面的界面,Devices里列出了可以观察的设备,Windows里列出的是当前选中的设备的可以用来显示View结构的Window:
     
         选中某个想要观察的Window,比如上面列出的com.android.launcher/com.android.launcher.Launcher项,然后点击菜单栏的Load View Hierarchy,就进入Layout View,由于要解析相关Window,所以这个过程要几秒钟,左边列出的是当前窗口的树型布局结构图,右边列出的是当前选中的某个子View的属性信息和在窗口中的位置:
     
        需要注意的是:Layout View列出的View结构是从视图的根节点开始的,比如针对Launcher使用的layout,它的底层基础布局DragLayer实际上是放在一个FrameLayout里的,该FrameLayout又是被PhoneWindow的DecorView管理的。
     
    (2)点击界面左下角类似九宫格的按钮,就进入了Android称之为Pixel Perfect View的界面,这个界面里主要是从细节上观察UI效果:
     
          左边是浏览视图,中间是全局的视图,右边是当前关注的地方的细节放大,是像素级别的,对于观察细节非常有用。
    Refresh Rate用来控制View多久从模拟器或者真机上更新一次视图数据。
    Zoom就是放大局部细节用的,细节显示在最右边的视图上。
    Overlay比较有意思,主要用来测试在当前视图上加载新的图片后的效果,点击Load…选择图片后,可以控制在当前界面上显示的透明读,滑动0%~100%的控件即可。如果选择了Show in Loupe,右侧的放大视图也会将加载的图片的细节结合着透明度显示出来。不过目前这个Overlay做的比较简单,合成的图只能从界面的左下角为原点画出来,不能移动。
     
     
    (3)在Layout View中,选中一个view的图示,点击工具栏的Display View,就可以看到这个view的实际显示效果,可以点选Show Extras,这个功能也比较实用,可以显示出该View中不同元素显示的边界,帮助我们检查是否正确。
     
     
     2.       Hierarchyviewer的invalidate和requestLayout功能
    对于Android的UI来说,invalidate和requestLayout是最重要的过程,Hierarchyviewer提供了帮助我们Debug特定的UI执行invalidate和requestLayout过程的途径,方法很简单,只要选择希望执行这两种操作的View点击按钮就可以。当然,我们需要在例如onMeasure()这样的方法中打上断点。这个功能对于UI组件是自定义的非常有用,可以帮助单独观察相关界面显示逻辑是否正确。














    http://www.tuicool.com/articles/jMfiUjj
    使用Systrace分析UI性能
    原文链接 : Analyzing UI Performance with Systrace
    原文作者 : Android Developers
    译文出自 : 开发技术前线 www.devtf.cn。未经允许,不得转载!
    译者 : desmond1121
    校对者: desmond1121
    开发应用的时候,应该检查它是否有流畅的用户体验,即60fps的帧率。如果由于某种原因丢帧,我们首先要做的就是知道系统在做什么(造成丢帧的原因)。
    Systrace允许你监视和跟踪Android系统的行为(trace)。它会告诉你系统都在哪些工作上花费时间、CPU周期都用在哪里,甚至 你可以看到每个线程、进程在指定时间内都在干嘛。它同时还会突出观测到的问题,从垃圾回收到渲染内容都可能是问题对象,甚至提供给你建议的解决方案。本文 章将介绍如何导出trace以及使用它来优化UI的办法。
    总览
    Systrace可以帮助你分析应用在不同Android系统上的运行情况。它将系统和应用的线程运行情况放置在同一条时间线上分析。你首先需要 收集系统和应用的trace(后面会告诉你怎么做),之后Systrace会帮你生成一份细致、直观的报告,它展示了设备在你监测的这段时间内所发生的事 情。
     图1. 连续滑动应用5秒的Trace,它并没有表现得很完美。
    图1展示了应用在滑动不流畅的时候生成的trace。默认缩放成全局显示,你可以放大到自己所关注的地方。横轴代表着时间线,事件记录按照进程分组,同一个进程内按线程进行纵向拆分,每个线程记录自己的工作。
    在本例中,一共有三个组:Kernel, SurfaceFlinger, App,他们分别以包名为标识。每个应用进程都会包含其中所有线程的记录信号,你可以看到从InputEvent到RenderThread都有。
    生成Trace
    在获取trace之前需要做一些启动工作。首先,设备要求API>=16(Android 4.1),之后通过正常的Debug流程(开启调试、连接工作环境、安装App)连接设备。由于需要记录磁盘活动和内核工作,你可能需要root权限。不 过大部分时候你只要能够正常Debug即可。
    Systrace 可以通过 命令行 或者 图形界面 启动,本篇文章重点介绍通过命令行使用Systrace。
    在Android 4.3及以上的系统中获取trace
    在4.3以上的系统获取Trace步骤:
    保证设备USB连接正常,并可以debug;
    在命令行中设置选项,开启trace,比如:
    $ cd android-sdk/platform-tools/systrace
        $ python systrace.py --time=10 -o mynewtrace.html sched gfx view wm
        $cdandroid-sdk/platform-tools/systrace
        $pythonsystrace.py--time=10-omynewtrace.htmlschedgfxviewwm
     
    在设备上做任何你想让trace记录的操作。
    你可以通过 Systrace选项 来了解更多命令行选项。
    在Android 4.2及以下的系统中获取trace
    在4.2及以下的系统中高效地使用Systrace的话,你需要在配置的时候显式指定要trace的进程种类。一共有这两类种类:
    普通系统进程,比如图形、声音、输入等。(通过tags设置,具体在 Systrace命令行 中有介绍)
    底层系统进程,比如CPU、内核、文件系统活动。(通过options设置,具体在 Systrace命令行 中有介绍)
    你可以通过以下命令行操作来设置tags:
    使用 --set-tags 选项:
    $ cd android-sdk/platform-tools/systrace
        $ python systrace.py --set-tags=gfx,view,wm
        $cdandroid-sdk/platform-tools/systrace
        $pythonsystrace.py--set-tags=gfx,view,wm
     
    重启adb shell来trace这些进程:
    $ adb shell stop
        $ adb shell start
        $adbshellstop
        $adbshellstart
     
    你也可以通过手机上的图形界面设置tags:
    在设备上进入设置> 开发者选项 > 监控 > 启用跟踪(部分手机上没有这个选项);
    选择追踪进程类型,点击确认。
    注意: 在图形界面中设置tag时adb shell不用重新启动。
    在配置完tags后,你可以开始收集操作信息了。
    如何在当前设置下启动trace:
    保证设备的usb连接正常,并且可以正常debug;
    使用低系统等级的命令行选项开启trace,比如:
    $ python systrace.py --cpu-freq --cpu-load --time=10 -o mytracefile.html
    在设备上做任何你想让trace记录的操作。
    你可以通过 Systrace选项 来了解更多命令行选项。
    分析trace报告
    在你获取trace之后你可以在网页浏览器中打开它。这部分内容告诉你怎么通过trace去分析和解决UI性能。
    监视帧数
    每个应用都有一行专门显示frame,每一帧就显示为一个绿色的圆圈。不过也有例外,当显示为黄色或者红色的时候,它的渲染时间超过了16.6ms(即达不到60fps的水准)。’w’键可以放大,看看这一帧的渲染过程中系统到底做了什么。
    提示:你可以按右上角的’?’按钮来查看界面使用帮助。
     图2. Systrace显示长渲染时间的帧
    单击该帧可以高亮它,这时候跟该帧有关的内容会被突出显示。在5.0及以上的系统中,显示工作被拆分成UI线程和Render线程两部分;在5.0以下的系统中,所有的显示工作在UI线程中执行。
    点击单个Frame下面的组件可以看他们所花费的时间。每个事件(比如 performTraversals )都会在你选中的时候显示出它们调用了哪些方法及所用的时间。
    调查警告事件
    Systrace会自动分析事件,它会将任何它认为性能有问题的东西都高亮警告,并提示你要怎么去优化。
     图3. 选择一个被高亮帧,它会显示出检测到的问题(回收ListView消耗时间太长)。
    在你选择类似图三中的问题帧之后,它就会提示你检测出的问题。在这个例子中,它被警告的主要原因是ListView的回收和重新绑定花费太多时间。在Systrace中也会提供一些对应链接,它们会提供更多解释。
    如果你想知道UI线程怎么会花费这么多时间的话,你可以使用 TraceView ,它会告诉你都是哪些函数在消耗时间。
    你可以通过右侧的’Alert’选项卡来查看整个trace过程中发生的所有问题,并进行快速定位。
     图4. 点击Alert选项卡。
    你可以将Alert面板中的问题视为需要处理的bug,很有可能每一次微小的优化能够去除整个应用中的警告!
    应用级别调试
    Systrace并不会追踪应用的所有工作,所以你可以在有需求的情况下自己添加要追踪的代码部分。在Android 4.3及以上的代码中,你可以通过 Trace 类来实现这个功能。它能够让你在任何时候跟踪应用的一举一动。在你获取trace的过程中, Trace.beginSection() 与 Trace.endSection() 之间代码工作会一直被追踪。
    下面这部分代码展示了使用 Trace 的例子,在整个方法中含有两个Trace块。
    public void ProcessPeople() {
        Trace.beginSection("ProcessPeople");
        try {
            Trace.beginSection("Processing Jane");
            try {
               // 待追踪的代码
            } finally {
                Trace.endSection(); // 结束 "Processing Jane"
            }

            Trace.beginSection("Processing John");
            try {
                // 待追踪的代码
            } finally {
                Trace.endSection(); // 结束 "Processing John"
            }
        } finally {
            Trace.endSection(); // 结束 "ProcessPeople"
        }
    }
    publicvoidProcessPeople(){
      Trace.beginSection("ProcessPeople");
      try{
        Trace.beginSection("Processing Jane");
        try{
           // 待追踪的代码
        }finally{
          Trace.endSection();// 结束 "Processing Jane"
        }
        Trace.beginSection("Processing John");
        try{
          // 待追踪的代码
        }finally{
          Trace.endSection();// 结束 "Processing John"
        }
      }finally{
        Trace.endSection();// 结束 "ProcessPeople"
      }
    }
    注意:在Trace是被嵌套在另一个Trace中的时候, endSection() 方法只会结束理它最近的一个 beginSection(String) 。即在一个Trace的过程中是无法中断其他Trace的。所以你要保证 endSection() 与 beginSection(String) 调用次数匹配。
    注意:Trace的begin与end必须在同一线程之中执行!
    当你使用应用级别追踪的时候,你必须通过 -a 或者 -app= 来显式地指定应用包名。可以通过 Systrace指南 查看更多关于它的信息。
    你在评估应用的时候应该开启应用级别跟踪,即使当你没有手动添加 Trace 信号。因为很多库函数里面是有添加Trace信号的(比如 RecyclerView ),它们往往能够提供很多信息。

    http://blog.jobbole.com/78995/
    正确使用Android性能分析工具——TraceView
    2014/10/27 · Android, 开发 · Android
    分享到: 37
    MongoDB集群之分片技术应用
    Hello,移动WEB
    Linux权限管理之特殊权限
    Android高级特效-索引
    原文出处: bxbxbai 的博客(@白瓦力)   欢迎分享原创到伯乐头条
    前面唠叨
    最近公司app中有些列表在滑动的时候会有卡顿现象,我就开始着手解决这些问题,解决问题之前首先要分析列表滑动的性能瓶颈在什么地方。因为之前不会正确使用TraceView这个工具,主要是看不懂TraceView界面下方数据指标的值代表什么意思…以前我用StopWatch类来分析性能,现在觉得弱爆了…不过有些地方StopWatch工具类还是很简单好用的~
    网上可以找了很多博客来介绍这个工具的使用方法,很多都是讲解了一些一些就会的方法,讲一个大概,包括StackOverFlow上我也没有找到很好的讲解TraceView各个数据指标代码什么意思的回答
    因为我要解决列表滑动的卡顿问题,就必须要找到导致卡顿现象的原因,我就在StackOverFlow上找着别人零散的回答慢慢琢磨这个工具的使用方法。现在我学会了,至少能看懂每个指标什么意思,最后发现这个工具实在太强大了!!!
    TraceView界面
    现来看一下整个界面的图,整个界面包括上下两部分,上面是你测试的进程中每个线程的执行情况,每个线程占一行;下面是每个方法执行的各个指标的值
    上面一部分是你测试进程的中每个线程运行的时间线,下图中可以可以看到,主要只有一个main线程在执行,因为我滑动了一下列表,main线程(UI线程)正在进行绘制View呢~
    然后我点击了序号为133的一个方法io.bxbxbai.android.examples.activity.ExpandableLayoutMainActivity$SimpleAdapter.getItemView,就会出现两部分数据:
    Parents
    Children
    Parents表示调用133这个方法的父方法,可以看到序号为130。Children表示方法133调用的其他方法,可以看到有好几个方法。
     
    如何使用TraceView
    因为这次我主要是分析列表滑动卡顿问题,我就讲讲我是怎么使用这个工具的,并且我是怎么分析的。
    使用TraceView主要有两种方式:
    最简单的方式就是直接打开DDMS,选择一个进程,然后按上面的“Start Method Profiling”按钮,等红色小点变成黑色以后就表示TraceView已经开始工作了。然后我就可以滑动一下列表(现在手机上的操作肯定会很卡,因 为Android系统在检测Dalvik虚拟机中每个Java方法的调用,这是我猜测的)。操作最好不要超过5s,因为最好是进行小范围的性能测试。然后再按一下刚才按的按钮,等一会就会出现上面这幅图,然后就可以开始分析了。
    第2种方式就是使用android.os.Debug.startMethodTracing();和android.os.Debug.stopMethodTracing();方法,当运行了这段代码的时候,就会有一个trace文件在/sdcard目录中生成,也可以调用startMethodTracing(String traceName) 设置trace文件的文件名,最后你可以使用adb pull /sdcard/test.trace /tmp 命令将trace文件复制到你的电脑中,然后用DDMS工具打开就会出现第一幅图了
    第一种方式相对来说是一种简单,但是测试的范围很宽泛,第二中方式相对来说精确一点,不过我个人喜欢使用第一种,因为简单,而且它是检测你的某一个操作。因为第二中更适合检测某一个方法的性能,其实也没有那种好,看使用的场景和喜好了。。。
    看懂TraceView中的指标
     
    其实我今年7月份就已经开始使用TraceView工具了,但是当时不懂其中每个指标的含义,就没注意到它强大的地方。看不懂界面下方表格中的指标,这些数据其实一点意义都没有。
    网上包括Android官网也没有对TraceView工具的使用有详细的说明文档,这点确实比较蛋疼。
    纵轴
    TraceView界面下方表格中纵轴就是每个方法,包括了JDK的,Android SDK的,也有native方法的,当然最重要的就是app中你自己写的方法,有些Android系统的方法执行时间很长,那么有很大的可能就是你app中调用这些方法过多导致的。
    每个方法前面都有一个数字,可能是全部方法按照Incl CPU Time 时间的排序序号(后面会讲到)
    点一个方法后可以看到有两部分,一个是Parents,另一个是Children。
    Parent表示调用这个方法的方法,可以叫做父方法
    Children表示这个方法中调用的其他方法,可以叫做子方法
    横轴
     
    横轴上是很多指标,这些指标表示什么意思真的困扰了我很长一段时间。。。
    能够很衡量一个方法性能的指标应该只有时间了吧? 一个方法肯定就是执行时间越短约好咯~~
    1. Incl Cpu Time
    define inclusive : 全包括的
    上图中可以看到0(toplevel) 的Incl Cpu Time 占了100%的时间,这个不是说100%的时间都是它在执行,请看下面代码:
     
    1
    2
    3
    4
    5
    6    public void top() {
        a();
        b();
        c();
        d();
    }    
    Incl Cpu Time表示方法top执行的总时间,假如说方法top的执行时间为10ms,方法a执行了1ms,方法b执行了2ms,方法c执行了3ms,方法d执行 了4ms(这里是为了举个栗子,实际情况中方法a、b、c、d的执行总时间肯定比方法top的执行总时间要小一点)。
    而且调用方法top的方法的执行时间是100ms,那么:
     
            Incl Cpu Time       
    top        10%       
        a    10%       
        b    20%       
        c    30%       
        d    40%    
    从上面图中可以看到:
    toplevel的 Incl Cpu Time 是1110.943,而io.bxbxbai.android.examples.activity.ExpandableLayoutMainActivity$SimpleAdapter.getItemView方法的Incl Cpu Time为12.859,说明后者的Incl Cpu Time % 约为1.2%
    这个指标表示 这个方法以及这个方法的子方法(比如top方法中的a、b、c、d方法)一共执行的时间
    2. Excl Cpu Time
    理解了Incl Cpu Time以后就可以很好理解Excl Cpu Time了,还是上面top方法的栗子:
    方法top 的 Incl Cpu Time 减去 方法a、b、c、d的Incl Cpu Time 的时间就是方法top的Excl Cpu Time 了
    3. Incl Real Time
    这个感觉和Incl Cpu Time 差不多,第7条会讲到。
    4. Excl Real Time
    同上
    5. Calls + Recur Calls / Total
    这个指标非常重要!
    它表示这个方法执行的次数,这个指标中有两个值,一个Call表示这个方法调用的次数,Recur Call表示递归调用次数,看下图:
     
    我选中了一个方法,可以看到这个方法的Calls + Recur Calls 值是14 + 0,表示这个方法调用了14次,但是没有递归调用
    从Children这一块来看,很多方法调用都是13的倍数,说明父方法中有一个判断,但是这不是重点,有些Child方法调用Calls为26,这说明了这些方法被调用了两遍,是不是可能存在重复调用的情况?这些都是可能可以优化性能的地方。
    6. Cpu Time / Call
    重点来了!!!!!!!!!!
     
    这个指标应该说是最重要的,从上图可以看到,133这个方法的调用次数为20次,而它的Incl Cpu Time为12.859ms,那么133方法每一次执行的时间是0.643ms(133这个方法是SimpleAdapter的getItemView方法)
    对于一个adapter的getView方法来说0.643ms是非常快的(因为这个adapter中只有一个TextView,我为了测试用的)
    如果getView方法执行时间很长,那么必然导致列表滑动的时候产生卡顿现象,可以在getView方法的Children方法列表中找到耗时最长的方法,分析出现问题的原因:
    是因为有过多的计算?
    还是因为有读取SD卡的操作?
    还是因为adapter中View太复杂了?
    还是因为需要有很多判断,设置View的显示还是隐藏
    还是因为其他原因…
    7. Real Time / Call
    Real Time 和 Cpu Time 我现在还不太明白它们的区别,我的理解应该是:
    Cpu Time 应该是某个方法占用CPU的时间
    Real Time 应该是这个方法的实际运行时间
    为什么它们会有区别呢?可能是因为CPU的上下文切换、阻塞、GC等原因方法的实际执行时间要比Cpu Time 要稍微长一点。
    总结
    TraceView是一个非常强大的性能分析工具,因为Android 官网对这个工具的使用介绍文档很少,而且一些中文博客中写的也都是抄来抄去,没有讲到底怎么使用。
    最近我在做这方面的性能分析,就慢慢琢磨了这么工具的使用,发现非常强大,写下来总结一下。
    Android的性能分析工具还有很多,比如:
    Eclipse Memory Analyzer Tool 来分析Android app的内存使用
    Dump UI Hierarchy for UI Atomator,分析UI层级
    systrace
    其他
    下图这一条工具栏中有很多性能分析工具~~~
     































    http://blog.csdn.net/aaa2832/article/details/19419679
    [Android Memory] 内存分析工具 MAT 的使用
    1 内存泄漏的排查方法
     
    Dalvik Debug Monitor Server (DDMS) 是 ADT插件的一部分,其中有两项功能可用于内存检查 :
    ·    heap 查看堆的分配情况
    ·    allocation tracker跟踪内存分配情况
    DDMS 这两项功能有助于找到内存泄漏的操作行为。
    Eclipse Memory Analysis Tools (MAT) 是一个分析 Java堆数据的专业工具,用它可以定位内存泄漏的原因。
    工具地址 : https://www.eclipse.org/mat/
     
    1.1 观察 Heap
     
    ·        运行程序,然后进入 DDMS管理界面,如下:
     
    PS : 点击工具栏上的   来更新统计信息
    点击右侧的 Cause GC 按钮或工具栏上的   即可查看当前的堆情况,如下:
     
    主要关注两项数据:
    o    Heap Size 堆的大小,当资源增加,当前堆的空余空间不够时,系统会增加堆的大小,若超过上限 (例如64M,视平台和具体机型而定)则会被杀掉
    o    Allocated 堆中已分配的大小,这是应用程序实际占用的内存大小,资源回收后,此项数据会变小
    ·        查看操作前后的堆数据,看是否有内存泄漏
    对单一操作(比如添加页,删除页)进行反复操作,如果堆的大小一直增加,则有内存泄漏的隐患。
     
    1.2 利用MAT分析内存堆
     
    DDMS 可以将当前的内存 Dump成一个 hprof格式的文件,MAT 读取这个文件后会给出方便阅读的信息,配合它的查找,对比功能,就可以定位内存泄漏的原因。
    ·        获取 hprof文件
    点击工具栏上的   按钮,将内存信息保存成文件。 如果是用 MAT Eclipse 插件获取的 Dump文件,则不需要经过转换,Adt会自动进行转换然后打开。
    ·        转换 hprof文件
    DDMS Dump 出的文件要经过转换才能被 MAT识别,Android SDK提供了这个工具 hprof-conv (位于 sdk/tools下)
    ·    ./hprof-conv xxx-a.hprof xxx-b.hprof
    ·        用 MAT打开转换后的 hprof文件
     
     
    1.3  Histogram 查询
     
    用的最多的功能是 Histogram,点击 Actions下的 Histogram项将得到 Histogram结果:
     
    它按类名将所有的实例对象列出来,可以点击表头进行排序,在表的第一行可以输入正则表达式来匹配结果 :
     
    在某一项上右键打开菜单选择 list objects ->with incoming refs 将列出该类的实例:
     
    它展示了对象间的引用关系,比如展开后的第一个子项表示这个 HomePage(0x420ca5b0)被HomePageContainer(0x420c9e40)中的 mHomePage属性所引用.
    快速找出某个实例没被释放的原因,可以右健 Path to GC Roots-->exclue all phantom/weak/soft etc. reference :
     
    得到的结果是:
     
    从表中可以看出 PreferenceManager -> … ->HomePage这条线路就引用着这个 HomePage实例。用这个方法可以快速找到某个对象的 GC Root,一个存在 GC Root的对象是不会被 GC回收掉的.
     
    1.4  Histogram 对比
     
    为查找内存泄漏,通常需要两个 Dump结果作对比,打开 Navigator History面板,将两个表的 Histogram结果都添加到 Compare Basket中去 :
     
    添加好后,打开 Compare Basket面板,得到结果:
     
    点击右上角的 ! 按钮,将得到比对结果:
     
    注意,上面这个对比结果不利于查找差异,可以调整对比选项:
     
    再把对比的结果排序,就可得到直观的对比结果:
     
    也可以对比两个对象集合,方法与此类似,都是将两个 Dump结果中的对象集合添加到Compare Basket中去对比。找出差异后用 Histogram查询的方法找出 GC Root,定位到具体的某个对象上。
     
    1.5  例子
     
    举例一个典型的分析内存泄漏的过程:
    1.  使用 Heap查看当前堆大小为 23.00M
    2.  添加一个页后堆大小变为 23.40M
    3.  将添加的一个页删除,堆大小为 23.40M
    4.  多次操作,结果仍相似,说明添加/删除页存在内存泄漏 (也应注意排除其它因素的影响)
    5.  Dump 出操作前后的 hprof 文件 (1.hprof,2.hprof),用 mat打开,并得到 histgram结果
    6.  使用 HomePage字段过滤 histgram结果,并列出该类的对象实例列表,看到两个表中的对象集合大小不同,操作后比操作前多出一个 HomePage,说明确实存在泄漏
    7.  将两个列表进行对比,找出多出的一个对象,用查找 GC Root的方法找出是谁串起了这条引用线路,定位结束
    PS :
    ·        很多时候堆增大是 Bitmap引起的,Bitmap在 Histogram中的类型是 byte [],对比两个 Histogram中的 byte[]对象就可以找出哪些 Bitmap有差异
    ·        多使用排序功能,对找出差异很有用
     
     
    2 内存泄漏的原因分析
     
    总结出来只有一条: 存在无效的引用!
    良好的模块设计以及合理使用设计模式有助于解决此问题。
     
     
    3 Tips
     
    ·    使用 android:largeHeap="true"标记 (API Level >= 11)
    在 AndroidManifest.xml中的 Application节点中声明即可分配到更大的堆内存, android:largeHeap标记在 Android系统应用中也有广泛的应用 ,比如 Launcher, Browser这些内存大户上均有使用.
     
     
    http://www.jianshu.com/p/c49f778e7acf

    使用Android studio分析内存泄露
    字数1340 阅读19600 评论19 喜欢87
    This post is a permitted translation of badoo Tech Blog and I add some text and screenshots for android studio users.
    Origin Author: Dmytro Voronkevych
    follow badoo on Tweet
    Translator: Miao1007
    截至androidstudio1.3为止,其内部的MemoryDump功能都很难使用,还是使用MAT更佳。
    Android使用java作为平台开发,帮助了我们解决了很多底层问题,比如内存管理,平台依赖等等。然而,我们也经常遇到OutOfMemoey问题,垃圾回收到底去哪了?
    接下来是一个Handler Leak的例子,它一般会在编译器中被警告提示。
    所需要的工具
    Android Studio 1.1 or higher
    Eclipse MemoryAnalyzer
    示例代码
    public class NonStaticNestedClassLeakActivity extends ActionBarActivity {

      TextView textView;

      public static final String TAG = NonStaticNestedClassLeakActivity.class.getSimpleName();

      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_non_static_nested_class_leak);
        textView = (TextView)findViewById(R.id.textview);
        Handler handler = new Handler();

        handler.postDelayed(new Runnable() {
          @Override public void
            textView.setText("Done");
          }//a mock for long time work
        }, 800000L);


      }
    }
    这是一个非常基础的Activity.注意这个匿名的Runnable被送到了Handler中,而且延迟非常的长。现在我们运行这个Activity,反复旋转屏幕,然后导出内存并分析。
    导入 Memory 到Eclipse MemoryAnalyzer
    使用Androidstudio导出 heap dump
     
    Android Studio dump Memory Analyze
    点击左下角的Android
    选中你的程序的包名
    点击 initiates garbage collection on selected vm
    点击 dump java heap for selected client
    打开MAT,进行分析
    MAT是对java heap中变量分析的一个工具,它可以用于分析内存泄露。
    点击OQL图标
    在窗口输入select * from instanceof android.app.Activity并按Ctrl + F5或者!按钮
    奇迹出现了,现在你发现泄露了许多的activity
    这个真是相当的不容乐观,我们来分析一下为什么GC没有回收它
     
    EMA
    在OQL(Object Query Language)窗口下输入的查询命令可以获得所有在内存中的Activities,这段查询代码是不是非常简单高效呢?
    点击一个activity对象,右键选中Path to GC roots
     
    GC root
     
    Message in looper hold a reference to Activity
    在打开的新窗口中,你可以发现,你的Activity是被this$0所引用的,它实际上是匿名类对当前类的引用。this$0又被callback所引用,接着它又被Message中一串的next所引用,最后到主线程才结束。
    任何情况下你在class中创建非静态内部类,内部类会(自动)拥有对当前类的一个强引用。
    一旦你把Runnable或者Message发送到Handler中,它就会被放入LooperThread的消息队列,并且被保持引用,直到Message被处理。发送postDelayed这样的消息,你输入延迟多少秒,它就会泄露至少多少秒。而发送没有延迟的消息的话,当队列中的消息过多时,也会照成一个临时的泄露。
    尝试使用static inner class来解决
    现在把Runnable变成静态的class
     
    StaticClass
    现在,摇一摇手机,导出内存
     
    StaticClass_memory_analyze
    为什么又出现了泄露呢?我们看一看Activities的引用.
     
    StaticClass_memory_analyze_explained
    看到下面的mContext的引用了吗,它被mTextView引用,这样说明,使用静态内部类还远远不够,我们仍然需要修改。
    使用弱引用 + static Runnable
    现在我们把刚刚内存泄露的罪魁祸首 - TextView改成弱引用。
     
    StaticClassWithWeakRef_code
    再次注意我们对TextView保持的是弱引用,现在让它运行,摇晃手机
    小心地操作WeakReferences,它们随时可以为空,在使用前要判断是否为空.
     
    StaticClassWithWeakRef_memory_analyze
    哇!现在只有一个Activity的实例了,这回终于解决了我们的问题。
    所以,我们应该记住:
    使用静态内部类
    Handler/Runnable的依赖要使用弱引用。
    如果你把现在的代码与开始的代码相比,你会发现它们大不相同,开始的代码易懂简介,你甚至可以脑补出运行结果。
    而现在的代码更加复杂,有很多的模板代码,当把postDelayed设置为一个短时间,比如50ms的情况下,写这么多代码就有点亏了。其实,还有一个更简单的方法。
    onDestroy中手动控制声明周期
    Handler可以使用removeCallbacksAndMessages(null),它将移除这个Handler所拥有的Runnable与Message。
    //Fixed by manually control lifecycle
      @Override protected void onDestroy() {
        super.onDestroy();
        myHandler.removeCallbacksAndMessages(null);
      }
    现在运行,旋转手机,导出内存
     
    removeCallbacks_memory_analyze
    Good!只有一个实例。
    这样写可以让你的代码更加简洁与可读。唯一要记住的就是就是要记得在生命周期onDestory的时候手动移除所有的消息。
    使用WeakHander
    (这个是第三方库,我就不翻译了,大家去Github上去学习吧)
    结论
    在Handler中使用postDelayed需要额外的注意,为了解决问题,我们有三种方法
    使用静态内部Handler/Runnable + 弱引用
    在onDestory的时候,手动清除Message
    使用Badoo开发的第三方的 WeakHandler
    这三种你可以任意选用,第二种看起来更加合理,但是需要额外的工作。第三种方法是我最喜欢的,当然你也要注意WeakHandler不能与外部的强引用共同使用。



    http://www.cnblogs.com/sunzn/p/3192231.html

    Android 编程下的 TraceView 简介及其案例实战
    TraceView 是 Android 平台配备一个很好的性能分析的工具。它可以通过图形化的方式让我们了解我们要跟踪的程序的性能,并且能具体到 method。详细内容参考:Profiling with Traceview and dmtracedump
    TraceView 简介
    TraceView 是 Android 平台特有的数据采集和分析工具,它主要用于分析 Android 中应用程序的 hotspot。TraceView 本身只是一个数据分析工具,而数据的采集则需要使用 Android SDK 中的 Debug 类或者利用 DDMS 工具。二者的用法如下:
    开 发者在一些关键代码段开始前调用 Android SDK 中 Debug 类的 startMethodTracing 函数,并在关键代码段结束前调用 stopMethodTracing 函数。这两个函数运行过程中将采集运行时间内该应用所有线程(注意,只能是 Java 线程)的函数执行情况,并将采集数据保存到 /mnt/sdcard/ 下的一个文件中。开发者然后需要利用 SDK 中的 TraceView 工具来分析这些数据。
    借助 Android SDK 中的 DDMS 工具。DDMS 可采集系统中某个正在运行的进程的函数调用信息。对开发者而言,此方法适用于没有目标应用源代码的情况。
    DDMS 中 TraceView 使用示意图如下,调试人员可以通过选择 Devices 中的应用后点击  按钮 Start Method Profiling(开启方法分析)和点击   Stop Method Profiling(停止方法分析)
     
    开启方法分析后对应用的目标页面进行测试操作,测试完毕后停止方法分析,界面会跳转到 DDMS 的 trace 分析界面,如下图所示:
     
    TraceView 界面比较复杂,其 UI 划分为上下两个面板,即 Timeline Panel(时间线面板)和 Profile Panel(分析面板)。上图中的上半部分为 Timeline Panel(时间线面板),Timeline Panel 又可细分为左右两个 Pane:
    左边 Pane 显示的是测试数据中所采集的线程信息。由图可知,本次测试数据采集了 main 线程,传感器线程和其它系统辅助线程的信息。
    右边 Pane 所示为时间线,时间线上是每个线程测试时间段内所涉及的函数调用信息。这些信息包括函数名、函数执行时间等。由图可知,Thread-1412 线程对应行的的内容非常丰富,而其他线程在这段时间内干得工作则要少得多。
    另外,开发者可以在时间线 Pane 中移动时间线纵轴。纵轴上边将显示当前时间点中某线程正在执行的函数信息。
    上图中的下半部分为 Profile Panel(分析面板),Profile Panel 是 TraceView 的核心界面,其内涵非常丰富。它主要展示了某个线程(先在 Timeline Panel 中选择线程)中各个函数调用的情况,包括 CPU 使用时间、调用次数等信息。而这些信息正是查找 hotspot 的关键依据。所以,对开发者而言,一定要了解 Profile Panel 中各列的含义。下表列出了 Profile Panel 中比较重要的列名及其描述。
     
    TraceView 实战
    了解完 TraceView 的 UI 后,现在介绍如何利用 TraceView 来查找 hotspot。一般而言,hotspot 包括两种类型的函数:
    一类是调用次数不多,但每次调用却需要花费很长时间的函数。
    一类是那些自身占用时间不长,但调用却非常频繁的函数。
    测试背景:APP 在测试机运行一段时间后出现手机发烫、卡顿、高 CPU 占有率的现象。将应用切入后台进行 CPU 数据的监测,结果显示,即使应用不进行任何操作,应用的 CPU 占有率都会持续的增长。
    按照 TraceView 简介中的方法进行测试,TraceView 结果 UI 显示后进行数据分析,在 Profile Panel 中,选择按 Cpu Time/Call 进行降序排序(从上之下排列,每项的耗费时间由高到低)得到如图所示结果:
     
    图中 ImageLoaderTools$2.run() 是应用程序中的函数,它耗时为 1111.124。然后点击 ImageLoaderTools$2.run() 项,得到更为详尽的调用关系图:
     
    上图中 Parents 为 ImageLoaderTools$2.run() 方法的调用者:Parents (the methods calling this method);Children 为 ImageLoaderTools$2.run() 调用的子函数或方法:Children (the methods called by this method)。本例中 ImageLoaderTools$2.run() 方法的调用者为 Framework 部分,而  ImageLoaderTools$2.run() 方法调 用的自方法中我们却发现有三个方法的 Incl Cpu Time % 占用均达到了 14% 以上,更离谱的是 Calls+RecurCalls/Total 显示这三个方法均被调用了 35000 次以上,从包名可以识别出这些方法为测试者自身所实现,由此可以判断 ImageLoaderTools$2.run() 极有可能是手机发烫、卡顿、高 CPU 占用率的原因所在。
    代码验证
    大致可以判断是 ImageLoaderTools$2.run() 方法出现了问题,下面找到这个方法进行代码上的验证:
     
      1 package com.sunzn.app.utils;
      2
      3 import java.io.File;
      4 import java.io.IOException;
      5 import java.io.InputStream;
      6 import java.lang.ref.SoftReference;
      7 import java.util.ArrayList;
      8 import java.util.HashMap;
      9
     10 import android.content.Context;
     11 import android.graphics.Bitmap;
     12 import android.os.Environment;
     13 import android.os.Handler;
     14 import android.os.Message;
     15
     16 public class ImageLoaderTools {
     17
     18     private HttpTools httptool;
     19
     20     private Context mContext;
     21
     22     private boolean isLoop = true;
     23
     24     private HashMap<String, SoftReference<Bitmap>> mHashMap_caches;
     25
     26     private ArrayList<ImageLoadTask> maArrayList_taskQueue;
     27
     28     private Handler mHandler = new Handler() {
     29         public void handleMessage(android.os.Message msg) {
     30             ImageLoadTask loadTask = (ImageLoadTask) msg.obj;
     31             loadTask.callback.imageloaded(loadTask.path, loadTask.bitmap);
     32         };
     33     };
     34
     35     private Thread mThread = new Thread() {
     36
     37         public void run() {
     38
     39             while (isLoop) {
     40
     41                 while (maArrayList_taskQueue.size() > 0) {
     42
     43                     try {
     44                         ImageLoadTask task = maArrayList_taskQueue.remove(0);
     45
     46                         if (Constant.LOADPICTYPE == 1) {
     47                             byte[] bytes = httptool.getByte(task.path, null, HttpTools.METHOD_GET);
     48                             task.bitmap = BitMapTools.getBitmap(bytes, 40, 40);
     49                         } else if (Constant.LOADPICTYPE == 2) {
     50                             InputStream in = httptool.getStream(task.path, null, HttpTools.METHOD_GET);
     51                             task.bitmap = BitMapTools.getBitmap(in, 1);
     52                         }
     53
     54                         if (task.bitmap != null) {
     55                             mHashMap_caches.put(task.path, new SoftReference<Bitmap>(task.bitmap));
     56                             File dir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
     57                             if (!dir.exists()) {
     58                                 dir.mkdirs();
     59                             }
     60                             String[] path = task.path.split("/");
     61                             String filename = path[path.length - 1];
     62                             File file = new File(dir, filename);
     63                             BitMapTools.saveBitmap(file.getAbsolutePath(), task.bitmap);
     64                             Message msg = Message.obtain();
     65                             msg.obj = task;
     66                             mHandler.sendMessage(msg);
     67                         }
     68                     } catch (IOException e) {
     69                         e.printStackTrace();
     70                     } catch (Exception e) {
     71                         e.printStackTrace();
     72                     }
     73
     74                     synchronized (this) {
     75                         try {
     76                             wait();
     77                         } catch (InterruptedException e) {
     78                             e.printStackTrace();
     79                         }
     80                     }
     81
     82                 }
     83
     84             }
     85
     86         };
     87
     88     };
     89
     90     public ImageLoaderTools(Context context) {
     91         this.mContext = context;
     92         httptool = new HttpTools(context);
     93         mHashMap_caches = new HashMap<String, SoftReference<Bitmap>>();
     94         maArrayList_taskQueue = new ArrayList<ImageLoaderTools.ImageLoadTask>();
     95         mThread.start();
     96     }
     97
     98     private class ImageLoadTask {
     99         String path;
    100         Bitmap bitmap;
    101         Callback callback;
    102     }
    103
    104     public interface Callback {
    105         void imageloaded(String path, Bitmap bitmap);
    106     }
    107
    108     public void quit() {
    109         isLoop = false;
    110     }
    111
    112     public Bitmap imageLoad(String path, Callback callback) {
    113         Bitmap bitmap = null;
    114         String[] path1 = path.split("/");
    115         String filename = path1[path1.length - 1];
    116
    117         if (mHashMap_caches.containsKey(path)) {
    118             bitmap = mHashMap_caches.get(path).get();
    119             if (bitmap == null) {
    120                 mHashMap_caches.remove(path);
    121             } else {
    122                 return bitmap;
    123             }
    124         }
    125
    126         File dir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
    127
    128         File file = new File(dir, filename);
    129
    130         bitmap = BitMapTools.getBitMap(file.getAbsolutePath());
    131         if (bitmap != null) {
    132             return bitmap;
    133         }
    134
    135         ImageLoadTask task = new ImageLoadTask();
    136         task.path = path;
    137         task.callback = callback;
    138         maArrayList_taskQueue.add(task);
    139
    140         synchronized (mThread) {
    141             mThread.notify();
    142         }
    143
    144         return null;
    145     }
    146
    147 }
     
    以上代码即是 ImageLoaderTools 图片工具类的全部代码,先不着急去研究这个类的代码实现过程,先来看看这个类是怎么被调用的:
     
     1 ImageLoaderTools imageLoaderTools = imageLoaderTools = new ImageLoaderTools(this);
     2
     3 Bitmap bitmap = imageLoaderTools.imageLoad(picpath, new Callback() {
     4
     5     @Override
     6     public void imageloaded(String picPath, Bitmap bitmap) {
     7         if (bitmap == null) {
     8             imageView.setImageResource(R.drawable.default);
     9         } else {
    10             imageView.setImageBitmap(bitmap);
    11         }
    12     }
    13 });
    14
    15 if (bitmap == null) {
    16     imageView.setImageResource(R.drawable.fengmianmoren);
    17 } else {
    18     imageView.setImageBitmap(bitmap);
    19 }
     
    ImageLoaderTools 被调用的过程非常简单:1.ImageLoaderTools 实例化;2.执行 imageLoad() 方法加载图片。
    在 ImageLoaderTools 类的构造函数(90行-96行)进行实例化过程中完成了网络工具 HttpTools 初始化、新建一个图片缓存 Map、新建一个下载队列、开启下载线程的操作。这时候请注意开启线程的操作,开启线程后执行 run() 方法(35行-88行),这时 isLoop 的值是默认的 true,maArrayList_taskQueue.size() 是为 0 的,在任务队列 maArrayList_taskQueue 中还没有加入下载任务之前这个循环会一直循环下去。在执行 imageLoad() 方法加载图片时会首先去缓存 mHashMap_caches 中查找该图片是否已经被下载过,如果已经下载过则直接返回与之对应的 bitmap 资源,如果没有查找到则会往 maArrayList_taskQueue 中添加下载任务并唤醒对应的下载线程,之前开启的线程在发现 maArrayList_taskQueue.size() > 0 后就进入下载逻辑,下载完任务完成后将对应的图片资源加入缓存 mHashMap_caches 并更新 UI,下载线程执行 wait() 方法被挂起。一 个图片下载的业务逻辑这样理解起来很顺畅,似乎没有什么问题。开始我也这样认为,但后来在仔细的分析代码的过程中发现如果同样一张图片资源重新被加载就会 出现死循环。还记得缓存 mHashMap_caches 么?如果一张图片之前被下载过,那么缓存中就会有这张图片的引用存在。重新去加载这张图片的时候如果重复的去初始化 ImageLoaderTools,线程会被开启,而使用 imageLoad() 方法加载图片时发现缓存中存在这个图片资源,则会将其直接返回,注意这里使用的是 return bitmap; 那 就意味着 imageLoad() 方法里添加下载任务到下载队列的代码不会被执行到,这时候 run() 方法中的 isLoop = true 并且 maArrayList_taskQueue.size() = 0,这样内层 while 里的逻辑也就是挂起线程的关键代码 wait() 永远不会被执行到,而外层 while 的判断条件一直为 true,就这样程序出现了死循环。死循环才是手机发烫、卡顿、高 CPU 占用率的真正原因所在。
    解决方案
    准确的定位到代码问题所在后,提出解决方案就很简单了,这里提供的解决方案是将 wait() 方法从内层 while 循环提到外层 while 循环中,这样重复加载同一张图片时,死循环一出现线程就被挂起,这样就可以避免死循环的出现。代码如下:
     
     1 private Thread mThread = new Thread() {
     2
     3     public void run() {
     4
     5         while (isLoop) {
     6
     7             while (maArrayList_taskQueue.size() > 0) {
     8
     9                 try {
    10                     ImageLoadTask task = maArrayList_taskQueue.remove(0);
    11
    12                     if (Constant.LOADPICTYPE == 1) {
    13                         byte[] bytes = httptool.getByte(task.path, null, HttpTools.METHOD_GET);
    14                         task.bitmap = BitMapTools.getBitmap(bytes, 40, 40);
    15                     } else if (Constant.LOADPICTYPE == 2) {
    16                         InputStream in = httptool.getStream(task.path, null, HttpTools.METHOD_GET);
    17                         task.bitmap = BitMapTools.getBitmap(in, 1);
    18                     }
    19
    20                     if (task.bitmap != null) {
    21                         mHashMap_caches.put(task.path, new SoftReference<Bitmap>(task.bitmap));
    22                         File dir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
    23                         if (!dir.exists()) {
    24                             dir.mkdirs();
    25                         }
    26                         String[] path = task.path.split("/");
    27                         String filename = path[path.length - 1];
    28                         File file = new File(dir, filename);
    29                         BitMapTools.saveBitmap(file.getAbsolutePath(), task.bitmap);
    30                         Message msg = Message.obtain();
    31                         msg.obj = task;
    32                         mHandler.sendMessage(msg);
    33                     }
    34                 } catch (IOException e) {
    35                     e.printStackTrace();
    36                 } catch (Exception e) {
    37                     e.printStackTrace();
    38                 }
    39
    40             }
    41             
    42             synchronized (this) {
    43                 try {
    44                     wait();
    45                 } catch (InterruptedException e) {
    46                     e.printStackTrace();
    47                 }
    48             }
    49
    50         }
    51
    52     };
    53
    54 };
     
    最后再附上代码修改后代码运行的性能图,和之前的多次被重复执行,效率有了质的提升,手机发烫、卡顿、高 CPU 占用率的现象也消失了。
     
    专注移动互联网产品设计研发 分享最新的移动互联网产品和技术








    http://www.oschina.net/news/60157/android-performance-patterns

    Google 发布 Android 性能优化典范
    oschina 发布于: 2015年03月04日 (55评)
    分享到:
    收藏 +696
    3月19日,深圳源创会火热报名中,go>>>»  
     
    2015年伊始,Google发布了关于Android性能优化典范的专题, 一共16个短视频,每个3-5分钟,帮助开发者创建更快更优秀的Android App。课程专题不仅仅介绍了Android系统中有关性能问题的底层工作原理,同时也介绍了如何通过工具来找出性能问题以及提升性能的建议。主要从三个 方面展开,Android的渲染机制,内存与GC,电量优化。下面是对这些问题和建议的总结梳理。
    0)Render Performance
    大多数用户感知到的卡顿等性能问题的最主要根源都是因为渲染性能。从设计师的角度,他们希望App能够有更多的动画,图片等时尚元素来实现流畅的用 户体验。但是Android系统很有可能无法及时完成那些复杂的界面渲染操作。Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染, 如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着程序的大多数操作都必须在16ms内完成。
     
    如果你的某个操作花费时间是24ms,系统在得到VSYNC信号的时候就无法进行正常渲染,这样就发生了丢帧现象。那么用户在32ms内看到的会是同一帧画面。
     
    用户容易在UI执行动画或者滑动ListView的时候感知到卡顿不流畅,是因为这里的操作相对复杂,容易发生丢帧的现象,从而感觉卡顿。有很多原 因可以导致丢帧,也许是因为你的layout太过复杂,无法在16ms内完成渲染,有可能是因为你的UI上有层叠太多的绘制单元,还有可能是因为动画执行 的次数过多。这些都会导致CPU或者GPU负载过重。
    我们可以通过一些工具来定位问题,比如可以使用HierarchyViewer来查找Activity中的布局是否过于复杂,也可以使用手机设置里 面的开发者选项,打开Show GPU Overdraw等选项进行观察。你还可以使用TraceView来观察CPU的执行情况,更加快捷的找到性能瓶颈。
    1)Understanding Overdraw
    Overdraw(过度绘制)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次的UI结构里面,如果不可见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这就浪费大量的CPU以及GPU资源。
     
    当设计上追求更华丽的视觉效果的时候,我们就容易陷入采用越来越多的层叠组件来实现这种视觉效果的怪圈。这很容易导致大量的性能问题,为了获得最佳的性能,我们必须尽量减少Overdraw的情况发生。
    幸运的是,我们可以通过手机设置里面的开发者选项,打开Show GPU Overdraw的选项,可以观察UI上的Overdraw情况。
     
    蓝色,淡绿,淡红,深红代表了4种不同程度的Overdraw情况,我们的目标就是尽量减少红色Overdraw,看到更多的蓝色区域。
    Overdraw有时候是因为你的UI布局存在大量重叠的部分,还有的时候是因为非必须的重叠背景。例如某个Activity有一个背景,然后里面 的Layout又有自己的背景,同时子View又分别有自己的背景。仅仅是通过移除非必须的背景图片,这就能够减少大量的红色Overdraw区域,增加 蓝色区域的占比。这一措施能够显著提升程序性能。
    2)Understanding VSYNC
    为了理解App是如何进行渲染的,我们必须了解手机硬件是如何工作,那么就必须理解什么是VSYNC。
    在讲解VSYNC之前,我们需要了解两个相关的概念:
    Refresh Rate:代表了屏幕在一秒内刷新屏幕的次数,这取决于硬件的固定参数,例如60Hz。
    Frame Rate:代表了GPU在一秒内绘制操作的帧数,例如30fps,60fps。
    GPU会获取图形数据进行渲染,然后硬件负责把渲染后的内容呈现到屏幕上,他们两者不停的进行协作。
     
    不幸的是,刷新频率和帧率并不是总能够保持相同的节奏。如果发生帧率与刷新频率不一致的情况,就会容易出现Tearing的现象(画面上下两部分显示内容发生断裂,来自不同的两帧数据发生重叠)。
     
     
    理解图像渲染里面的双重与三重缓存机制,这个概念比较复杂,请移步查看这里:http://source.android.com/devices/graphics/index.html,还有这里http://article.yeeyan.org/view/37503/304664。
    通常来说,帧率超过刷新频率只是一种理想的状况,在超过60fps的情况下,GPU所产生的帧数据会因为等待VSYNC的刷新信息而被Hold住,这样能够保持每次刷新都有实际的新的数据可以显示。但是我们遇到更多的情况是帧率小于刷新频率。
     
    在这种情况下,某些帧显示的画面内容就会与上一帧的画面相同。糟糕的事情是,帧率从超过60fps突然掉到60fps以下,这样就会发生LAG,JANK,HITCHING等卡顿掉帧的不顺滑的情况。这也是用户感受不好的原因所在。
    3)Tool:Profile GPU Rendering
    性能问题如此的麻烦,幸好我们可以有工具来进行调试。打开手机里面的开发者选项,选择Profile GPU Rendering,选中On screen as bars的选项。
     
    选择了这样以后,我们可以在手机画面上看到丰富的GPU绘制图形信息,分别关于StatusBar,NavBar,激活的程序Activity区域的GPU Rending信息。
     
    随着界面的刷新,界面上会滚动显示垂直的柱状图来表示每帧画面所需要渲染的时间,柱状图越高表示花费的渲染时间越长。
     
    中间有一根绿色的横线,代表16ms,我们需要确保每一帧花费的总时间都低于这条横线,这样才能够避免出现卡顿的问题。
     
    每一条柱状线都包含三部分,蓝色代表测量绘制Display List的时间,红色代表OpenGL渲染Display List所需要的时间,黄色代表CPU等待GPU处理的时间。
    4)Why 60fps?
    我们通常都会提到60fps与16ms,可是知道为何会是以程序是否达到60fps来作为App性能的衡量标准吗?这是因为人眼与大脑之间的协作无法感知超过60fps的画面更新。
    12fps大概类似手动快速翻动书籍的帧率,这明显是可以感知到不够顺滑的。24fps使得人眼感知的是连续线性的运动,这其实是归功于运动模糊的 效果。24fps是电影胶圈通常使用的帧率,因为这个帧率已经足够支撑大部分电影画面需要表达的内容,同时能够最大的减少费用支出。但是低于30fps是 无法顺畅表现绚丽的画面内容的,此时就需要用到60fps来达到想要的效果,当然超过60fps是没有必要的。
    开发app的性能目标就是保持60fps,这意味着每一帧你只有16ms=1000/60的时间来处理所有的任务。
    5)Android, UI and the GPU
    了解Android是如何利用GPU进行画面渲染有助于我们更好的理解性能问题。那么一个最实际的问题是:activity的画面是如何绘制到屏幕上的?那些复杂的XML布局文件又是如何能够被识别并绘制出来的?
     
    Resterization栅格化是绘制那些Button,Shape,Path,String,Bitmap等组件最基础的操作。它把那些组件拆分到不同的像素上进行显示。这是一个很费时的操作,GPU的引入就是为了加快栅格化的操作。
    CPU负责把UI组件计算成Polygons,Texture纹理,然后交给GPU进行栅格化渲染。
     
    然而每次从CPU转移到GPU是一件很麻烦的事情,所幸的是OpenGL ES可以把那些需要渲染的纹理Hold在GPU Memory里面,在下次需要渲染的时候直接进行操作。所以如果你更新了GPU所hold住的纹理内容,那么之前保存的状态就丢失了。
    在Android里面那些由主题所提供的资源,例如Bitmaps,Drawables都是一起打包到统一的Texture纹理当中,然后再传递到 GPU里面,这意味着每次你需要使用这些资源的时候,都是直接从纹理里面进行获取渲染的。当然随着UI组件的越来越丰富,有了更多演变的形态。例如显示图 片的时候,需要先经过CPU的计算加载到内存中,然后传递给GPU进行渲染。文字的显示更加复杂,需要先经过CPU换算成纹理,然后再交给GPU进行渲 染,回到CPU绘制单个字符的时候,再重新引用经过GPU渲染的内容。动画则是一个更加复杂的操作流程。
    为了能够使得App流畅,我们需要在每一帧16ms以内处理完所有的CPU与GPU计算,绘制,渲染等等操作。
    6)Invalidations, Layouts, and Performance
    顺滑精妙的动画是app设计里面最重要的元素之一,这些动画能够显著提升用户体验。下面会讲解Android系统是如何处理UI组件的更新操作的。
    通常来说,Android需要把XML布局文件转换成GPU能够识别并绘制的对象。这个操作是在DisplayList的帮助下完成的。DisplayList持有所有将要交给GPU绘制到屏幕上的数据信息。
    在某个View第一次需要被渲染时,DisplayList会因此而被创建,当这个View要显示到屏幕上时,我们会执行GPU的绘制指令来进行渲 染。如果你在后续有执行类似移动这个View的位置等操作而需要再次渲染这个View时,我们就仅仅需要额外操作一次渲染指令就够了。然而如果你修改了 View中的某些可见组件,那么之前的DisplayList就无法继续使用了,我们需要回头重新创建一个DisplayList并且重新执行渲染指令并 更新到屏幕上。
    需要注意的是:任何时候View中的绘制内容发生变化时,都会重新执行创建DisplayList,渲染DisplayList,更新到屏幕上等一 系列操作。这个流程的表现性能取决于你的View的复杂程度,View的状态变化以及渲染管道的执行性能。举个例子,假设某个Button的大小需要增大 到目前的两倍,在增大Button大小之前,需要通过父View重新计算并摆放其他子View的位置。修改View的大小会触发整个 HierarcyView的重新计算大小的操作。如果是修改View的位置则会触发HierarchView重新计算其他View的位置。如果布局很复 杂,这就会很容易导致严重的性能问题。我们需要尽量减少Overdraw。
     
    我们可以通过前面介绍的Monitor GPU Rendering来查看渲染的表现性能如何,另外也可以通过开发者选项里面的Show GPU view updates来查看视图更新的操作,最后我们还可以通过HierarchyViewer这个工具来查看布局,使得布局尽量扁平化,移除非必需的UI组 件,这些操作能够减少Measure,Layout的计算时间。
    7)Overdraw, Cliprect, QuickReject
    引起性能问题的一个很重要的方面是因为过多复杂的绘制操作。我们可以通过工具来检测并修复标准UI组件的Overdraw问题,但是针对高度自定义的UI组件则显得有些力不从心。
    有一个窍门是我们可以通过执行几个APIs方法来显著提升绘制操作的性能。前面有提到过,非可见的UI组件进行绘制更新会导致Overdraw。例 如Nav Drawer从前置可见的Activity滑出之后,如果还继续绘制那些在Nav Drawer里面不可见的UI组件,这就导致了Overdraw。为了解决这个问题,Android系统会通过避免绘制那些完全不可见的组件来尽量减少 Overdraw。那些Nav Drawer里面不可见的View就不会被执行浪费资源。
     
    但是不幸的是,对于那些过于复杂的自定义的View(重写了onDraw方法),Android系统无法检测具体在onDraw里面会执行什么操作,系统无法监控并自动优化,也就无法避免Overdraw了。但是我们可以通过canvas.clipRect()来 帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠 组件的自定义View来控制显示的区域。同时clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执 行,那些部分内容在矩形区域内的组件,仍然会得到绘制。
     
    除了clipRect方法之外,我们还可以使用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。做了那些优化之后,我们可以通过上面介绍的Show GPU Overdraw来查看效果。
    8)Memory Churn and performance
    虽然Android有自动管理内存的机制,但是对内存的不恰当使用仍然容易引起严重的性能问题。在同一帧里面创建过多的对象是件需要特别引起注意的事情。
    Android系统里面有一个Generational Heap Memory的模型,系统会根据内存中不同 的内存数据类型分别执行不同的GC操作。例如,最近刚分配的对象会放在Young Generation区域,这个区域的对象通常都是会快速被创建并且很快被销毁回收的,同时这个区域的GC操作速度也是比Old Generation区域的GC操作速度更快的。
     
    除了速度差异之外,执行GC操作的时候,任何线程的任何操作都会需要暂停,等待GC操作完成之后,其他操作才能够继续运行。
     
    通常来说,单个的GC并不会占用太多时间,但是大量不停的GC操作则会显著占用帧间隔时间(16ms)。如果在帧间隔时间里面做了过多的GC操作,那么自然其他类似计算,渲染等操作的可用时间就变得少了。
    导致GC频繁执行有两个原因:
    Memory Churn内存抖动,内存抖动是因为大量的对象被创建又在短时间内马上被释放。
    瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,也会触发GC。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加 Heap的压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题。
     
    解决上面的问题有简洁直观方法,如果你在Memory Monitor里面查看到短时间发生了多次内存的涨跌,这意味着很有可能发生了内存抖动。
     
    同时我们还可以通过Allocation Tracker来查看在短时间内,同一个栈中不断进出的相同对象。这是内存抖动的典型信号之一。
    当你大致定位问题之后,接下去的问题修复也就显得相对直接简单了。例如,你需要避免在for循环里面分配对象占用内存,需要尝试把对象的创建移到循 环体之外,自定义View中的onDraw方法也需要引起注意,每次屏幕发生绘制以及动画执行过程中,onDraw方法都会被调用到,避免在onDraw 方法里面执行复杂的操作,避免创建对象。对于那些无法避免需要创建对象的情况,我们可以考虑对象池模型,通过对象池来解决频繁创建与销毁的问题,但是这里 需要注意结束使用之后,需要手动释放对象池中的对象。
    9)Garbage Collection in Android
    JVM的回收机制给开发人员带来很大的好处,不用时刻处理对象的分配与回收,可以更加专注于更加高级的代码实现。相比起Java,C与C++等语言 具备更高的执行效率,他们需要开发人员自己关注对象的分配与回收,但是在一个庞大的系统当中,还是免不了经常发生部分对象忘记回收的情况,这就是内存泄 漏。
    原始JVM中的GC机制在Android中得到了很大程度上的优化。Android里面是一个三级Generation的内存模型,最近分配的对象 会存放在Young Generation区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到Old Generation,最后到Permanent Generation区域。
     
    每一个级别的内存区域都有固定的大小,此后不断有新的对象被分配到此区域,当这些对象总的大小快达到这一级别内存区域的阀值时,会触发GC的操作,以便腾出空间来存放其他新的对象。
     
    前面提到过每次GC发生的时候,所有的线程都是暂停状态的。GC所占用的时间和它是哪一个Generation也有关系,Young Generation的每次GC操作时间是最短的,Old Generation其次,Permanent Generation最长。执行时间的长短也和当前Generation中的对象数量有关,遍历查找20000个对象比起遍历50个对象自然是要慢很多 的。
    虽然Google的工程师在尽量缩短每次GC所花费的时间,但是特别注意GC引起的性能问题还是很有必要。如果不小心在最小的for循环单元里面执 行了创建对象的操作,这将很容易引起GC并导致性能问题。通过Memory Monitor我们可以查看到内存的占用情况,每一次瞬间的内存降低都是因为此时发生了GC操作,如果在短时间内发生大量的内存上涨与降低的事件,这说明 很有可能这里有性能问题。我们还可以通过Heap and Allocation Tracker工具来查看此时内存中分配的到底有哪些对象。
    10)Performance Cost of Memory Leaks
    虽然Java有自动回收的机制,可是这不意味着Java中不存在内存泄漏的问题,而内存泄漏会很容易导致严重的性能问题。
    内存泄漏指的是那些程序不再使用的对象无法被GC识别,这样就导致这个对象一直留在内存当中,占用了宝贵的内存空间。显然,这还使得每级Generation的内存区域可用空间变小,GC就会更容易被触发,从而引起性能问题。
    寻找内存泄漏并修复这个漏洞是件很棘手的事情,你需要对执行的代码很熟悉,清楚的知道在特定环境下是如何运行的,然后仔细排查。例如,你想知道程序 中的某个activity退出的时候,它之前所占用的内存是否有完整的释放干净了?首先你需要在activity处于前台的时候使用Heap Tool获取一份当前状态的内存快照,然后你需要创建一个几乎不这么占用内存的空白activity用来给前一个Activity进行跳转,其次在跳转到 这个空白的activity的时候主动调用System.gc()方法来确保触发一个GC操作。最后,如果前面这个activity的内存都有全部正确释 放,那么在空白activity被启动之后的内存快照中应该不会有前面那个activity中的任何对象了。
     
    如果你发现在空白activity的内存快照中有一些可疑的没有被释放的对象存在,那么接下去就应该使用Alocation Track Tool来仔细查找具体的可疑对象。我们可以从空白activity开始监听,启动到观察activity,然后再回到空白activity结束监听。这样操作以后,我们可以仔细观察那些对象,找出内存泄漏的真凶。
     
    11)Memory Performance
    通常来说,Android对GC做了大量的优化操作,虽然执行GC操作的时候会暂停其他任务,可是大多数情况下,GC操作还是相对很安静并且高效的。但是如果我们对内存的使用不恰当,导致GC频繁执行,这样就会引起不小的性能问题。
    为了寻找内存的性能问题,Android Studio提供了工具来帮助开发者。
    Memory Monitor:查看整个app所占用的内存,以及发生GC的时刻,短时间内发生大量的GC操作是一个危险的信号。
    Allocation Tracker:使用此工具来追踪内存的分配,前面有提到过。
    Heap Tool:查看当前内存快照,便于对比分析哪些对象有可能是泄漏了的,请参考前面的Case。
    12)Tool – Memory Monitor
    Android Studio中的Memory Monitor可以很好的帮组我们查看程序的内存使用情况。
     
     
     
    13)Battery Performance
    电量其实是目前手持设备最宝贵的资源之一,大多数设备都需要不断的充电来维持继续使用。不幸的是,对于开发者来说,电量优化是他们最后才会考虑的的事情。但是可以确定的是,千万不能让你的应用成为消耗电量的大户。
    Purdue University研究了最受欢迎的一些应用的电量消耗,平均只有30%左右的电量是被程序最核心的方法例如绘制图片,摆放布局等等所使用掉的,剩下的 70%左右的电量是被上报数据,检查位置信息,定时检索后台广告信息所使用掉的。如何平衡这两者的电量消耗,就显得非常重要了。
    有下面一些措施能够显著减少电量的消耗:
    我们应该尽量减少唤醒屏幕的次数与持续的时间,使用WakeLock来处理唤醒的问题,能够正确执行唤醒操作并根据设定及时关闭操作进入睡眠状态。
    某些非必须马上执行的操作,例如上传歌曲,图片处理等,可以等到设备处于充电状态或者电量充足的时候才进行。
    触发网络请求的操作,每次都会保持无线信号持续一段时间,我们可以把零散的网络请求打包进行一次操作,避免过多的无线信号引起的电量消耗。关于网络请求引起无线信号的电量消耗,还可以参考这里http://hukai.me/android-training-course-in-chinese/connectivity/efficient-downloads/efficient-network-access.html
    我们可以通过手机设置选项找到对应App的电量消耗统计数据。我们还可以通过Battery Historian Tool来查看详细的电量消耗。
     
    如果发现我们的App有电量消耗过多的问题,我们可以使用JobScheduler API来对一些任务进行定时处理,例如我们可以把那些任务重的操作等到手机处于充电状态,或者是连接到WiFi的时候来处理。
    关于JobScheduler的更多知识可以参考http://hukai.me/android-training-course-in-chinese/background-jobs/scheduling/index.html
    14)Understanding Battery Drain on Android
    电量消耗的计算与统计是一件麻烦而且矛盾的事情,记录电量消耗本身也是一个费电量的事情。唯一可行的方案是使用第三方监测电量的设备,这样才能够获取到真实的电量消耗。
    当设备处于待机状态时消耗的电量是极少的,以N5为例,打开飞行模式,可以待机接近1个月。可是点亮屏幕,硬件各个模块就需要开始工作,这会需要消耗很多电量。
    使用WakeLock或者JobScheduler唤醒设备处理定时的任务之后,一定要及时让设备回到初始状态。每次唤醒无线信号进行数据传递,都会消耗很多电量,它比WiFi等操作更加的耗电,详情请关注http://hukai.me/android-training-course-in-chinese/connectivity/efficient-downloads/efficient-network-access.html
     
    修复电量的消耗是另外一个很大的课题,这里就不展开继续了。
    15)Battery Drain and WakeLocks
    高效的保留更多的电量与不断促使用户使用你的App来消耗电量,这是矛盾的选择题。不过我们可以使用一些更好的办法来平衡两者。
    假设你的手机里面装了大量的社交类应用,即使手机处于待机状态,也会经常被这些应用唤醒用来检查同步新的数据信息。Android会不断关闭各种硬 件来延长手机的待机时间,首先屏幕会逐渐变暗直至关闭,然后CPU进入睡眠,这一切操作都是为了节约宝贵的电量资源。但是即使在这种睡眠状态下,大多数应 用还是会尝试进行工作,他们将不断的唤醒手机。一个最简单的唤醒手机的方法是使用PowerManager.WakeLock的API来保持CPU工作并 防止屏幕变暗关闭。这使得手机可以被唤醒,执行工作,然后回到睡眠状态。知道如何获取WakeLock是简单的,可是及时释放WakeLock也是非常重 要的,不恰当的使用WakeLock会导致严重错误。例如网络请求的数据返回时间不确定,导致本来只需要10s的事情一直等待了1个小时,这样会使得电量 白白浪费了。这也是为何使用带超时参数的wakelock.acquice()方法是很关键的。但是仅仅设置超时并不足够解决问题,例如设置多长的超时比 较合适?什么时候进行重试等等?
    解决上面的问题,正确的方式可能是使用非精准定时器。通常情况下,我们会设定一个时间进行某个操作,但是动态修改这个时间也许会更好。例如,如果有 另外一个程序需要比你设定的时间晚5分钟唤醒,最好能够等到那个时候,两个任务捆绑一起同时进行,这就是非精确定时器的核心工作原理。我们可以定制计划的 任务,可是系统如果检测到一个更好的时间,它可以推迟你的任务,以节省电量消耗。
     
    这正是JobScheduler API所做的事情。它会根据当前的情况与任务,组合出理想的唤醒时间,例如等到正在充电或者连接到WiFi的时候,或者集中任务一起执行。我们可以通过这个API实现很多免费的调度算法。
    从Android 5.0开始发布了Battery History Tool,它可以查看程序被唤醒的频率,又谁唤醒的,持续了多长的时间,这些信息都可以获取到。
    请关注程序的电量消耗,用户可以通过手机的设置选项观察到那些耗电量大户,并可能决定卸载他们。所以尽量减少程序的电量消耗是非常有必要的。
    稿源:hukai.me


    Android MemInfo 各项的意义(转) 
    可以使用adb shell dumpsys meminfo -a <process id>/<process name>来查看一个进程的memory。截图如下:
     
    Naitve Heap Size: 从mallinfo usmblks获得,代表最大总共分配空间
    Native Heap Alloc: 从mallinfo uorblks获得,总共分配空间
    Native Heap Free: 从mallinfo fordblks获得,代表总共剩余空间 
    Native Heap Size 约等于Native Heap Alloc + Native Heap Free
    mallinfo是一个C库, mallinfo 函数提供了各种各样的通过C的malloc()函数分配的内存的统计信息。
    Dalvik Heap Size:从Runtime totalMemory()获得,Dalvik Heap总共的内存大小。
    Dalvik Heap Alloc: Runtime totalMemory()-freeMemory() ,Dalvik Heap分配的内存大小。
    Dalvik Heap Free:从Runtime freeMemory()获得,Dalvik Heap剩余的内存大小。
    Dalvik Heap Size 约等于Dalvik  Heap Alloc + Dalvik  Heap Free
    OtherPss, include Cursor,Ashmem, Other Dev, .so mmap, .jar mmap, .apk mmap, .ttf mmap, .dex mmap, Other mmap, Unkown统计信息都可以在process的smap文件看到。
    Objects and SQL 信息都是从Android Debug信息中获得。
    其他类型               smap 路径名称          描述
    Cursor                  /dev/ashmem/Cursor  Cursor消耗的内存(KB)
    Ashmem               /dev/ashmem            匿名共享内存用来提供共享内存通过分配一个多个进程
                                                             可以共享的带名称的内存块
    Other dev             /dev/                        内部driver占用的在 “Other dev”                                                 
    .so mmap             .so                            C 库代码占用的内存
    .jar mmap            .jar                           Java 文件代码占用的内存
    .apk mmap           .apk                           apk代码占用的内存
    .ttf mmap              .ttf                           ttf 文件代码占用的内存
    .dex mmap             .dex                         Dex 文件代码占用的内存
    Other mmap                                          其他文件占用的内存

    But as to what the difference is between "Pss", "PrivateDirty", and "SharedDirty"... well now the fun begins.
    A lot of memory in Android (and Linux systems in general) is actually shared across multiple processes. So how much memory a processes uses is really not clear. Add on top of that paging out to disk (let alone swap which we don't use on Android) and it is even less clear.
    Thus if you were to take all of the physical RAM actually mapped in to each process, and add up all of the processes, you would probably end up with a number much greater than the actual total RAM.
    The Pss number is a metric the kernel computes that takes into account memory sharing -- basically each page of RAM in a process is scaled by a ratio of the number of other processes also using that page. This way you can (in theory) add up the pss across all processes to see the total RAM they are using, and compare pss between processes to get a rough idea of their relative weight.
    The other interesting metric here is PrivateDirty, which is basically the amount of RAM inside the process that can not be paged to disk (it is not backed by the same data on disk), and is not shared with any other processes. Another way to look at this is the RAM that will become available to the system when that process goes away (and probably quickly subsumed into caches and other uses of it).
    That is pretty much the SDK APIs for this. However there is more you can do as a developer with your device.
    Using adb, there is a lot of information you can get about the memory use of a running system. A common one is the command "adb shell dumpsys meminfo" which will spit out a bunch of information about the memory use of each Java process, containing the above info as well as a variety of other things. You can also tack on the name or pid of a single process to see, for example "adb shell dumpsys meminfo system" give me the system process:
    ** MEMINFO in pid 890 [system] **
                        native   dalvik    other    total
                size:    10940     7047      N/A    17987
           allocated:     8943     5516      N/A    14459
                free:      336     1531      N/A     1867
               (Pss):     4585     9282    11916    25783
      (shared dirty):     2184     3596      916     6696
        (priv dirty):     4504     5956     7456    17916

     Objects
               Views:      149        ViewRoots:        4
         AppContexts:       13       Activities:        0
              Assets:        4    AssetManagers:        4
       Local Binders:      141    Proxy Binders:      158
    Death Recipients:       49
     OpenSSL Sockets:        0

     SQL
                heap:      205          dbFiles:        0
           numPagers:        0   inactivePageKB:        0
        activePageKB:        0
    The top section is the main one, where "size" is the total size in address space of a particular heap, "allocated" is the kb of actual allocations that heap thinks it has, "free" is the remaining kb free the heap has for additional allocations, and "pss" and "priv dirty" are the same as discussed before specific to pages associated with each of the heaps.
    If you just want to look at memory usage across all processes, you can use the command "adb shell procrank". Output of this on the same system looks like:
      PID      Vss      Rss      Pss      Uss  cmdline
      890   84456K   48668K   25850K   21284K  system_server
     1231   50748K   39088K   17587K   13792K  com.android.launcher2
      947   34488K   28528K   10834K    9308K  com.android.wallpaper
      987   26964K   26956K    8751K    7308K  com.google.process.gapps
      954   24300K   24296K    6249K    4824K  com.android.phone
      948   23020K   23016K    5864K    4748K  com.android.inputmethod.latin
      888   25728K   25724K    5774K    3668K  zygote
      977   24100K   24096K    5667K    4340K  android.process.acore
    ...
       59     336K     332K      99K      92K  /system/bin/installd
       60     396K     392K      93K      84K  /system/bin/keystore
       51     280K     276K      74K      68K  /system/bin/servicemanager
       54     256K     252K      69K      64K  /system/bin/debuggerd
    Here the Vss and Rss columns are basically noise (these are the straight-forward address space and RAM usage of a process, where if you add up the RAM usage across processes you get an ridiculously large number).
    Pss is as we've seen before, and Uss is Priv Dirty.
    Interesting thing to note here: Pss and Uss are slightly (or more than slightly) different than what we saw in meminfo. Why is that? Well procrank uses a different kernel mechanism to collect its data than meminfo does, and they give slightly different results. Why is that? Honestly I haven't a clue. I believe procrank may be the more accurate one... but really, this just leave the point: "take any memory info you get with a grain of salt; often a very large grain."
    Finally there is the command "adb shell cat /proc/meminfo" that gives a summary of the overall memory usage of the system. There is a lot of data here, only the first few numbers worth discussing (and the remaining ones understood by few people, and my questions of those few people about them often resulting in conflicting explanations):
    MemTotal:         395144 kB
    MemFree:          184936 kB
    Buffers:             880 kB
    Cached:            84104 kB
    SwapCached:            0 kB
    MemTotal is the total amount of memory available to the kernel and user space (often less than the actual physical RAM of the device, since some of that RAM is needed for the radio, DMA buffers, etc).
    MemFree is the amount of RAM that is not being used at all. The number you see here is very high; typically on an Android system this would be only a few MB, since we try to use available memory to keep processes running
    Cached is the RAM being used for filesystem caches and other such things. Typical systems will need to have 20MB or so for this to avoid getting into bad paging states; the Android out of memory killer is tuned for a particular system to make sure that background processes are killed before the cached RAM is consumed too much by them to result in such paging.
















    Android 内存分析工具 - LogCat GC

    D/dalvikvm: , , ,

    一、GC_Reason 触发垃圾回收的回收的集中原因:
     
    类型    描述       
    GC_CONCURRENT    内存使用将满时,并发的进行垃圾回收。       
    GC_FOR_MALLOC    当内存已满应用尝试分配内存时会出触发垃圾回收,所以系统会停止应用进行垃圾整理       
    GC_HPROF_DUMP_HEAP    当创建HPROF文件分析内存时触发垃圾收集。       
    GC_EXPLICIT    显示的垃圾收集,例如当你调用gc() (应该避免调用,而是交由系统处理)       
    GC_EXTERNAL_ALLOC    只会在API 10以下版本触发。新版都只会在Dalvik Heap上分配。    



    二、Amount freed 回收的内存大小

    三、Heap stats 空闲内存比例和(活跃对象总数/内存大小)

    四、External memory stats API 10以下内存分配大小

    五、Pause time 越大的堆暂停时间越长,并发会显示两个暂停:一个是回收开始时间,另外一个是回收结束时间


    例子: D/dalvikvm(27235): GC_FOR_ALLOC freed 836K, 27% free 9653K/13116K, paused 101ms, total 104ms
     
    LOG信息    描述       
    freed 836K    此次回收836K       
    27% free    可用内存空间27%       
    9653K/13116K     活跃对象与总大小具体指       
    paused 101ms    暂停进行垃圾回收用时101ms       
    total 104ms    总用时104ms    

     
    其他 external 0K/0K,表示可用外部内存/外部内存总量 paused 2ms+2ms,第一个时间值表示markrootset的时间,第二个时间值表示第二次mark的时间。 如果触发原因不是GC_CONCURRENT,这一行为单个时间值,表示垃圾收集的耗时时间。
    可以通过在LogCat通过 “GC_” 关键字 + TAG 两项过滤

    参考资料: https://developer.android.com/tools/debugging/debugging-memory.html#LogMessages





































    http://www.it165.net/pro/html/201406/16404.html

    Android使用procrank和dumpsysmeminfo分析内存占用情况
    如果你想查看所有进程的内存使用情况,可以使用命令procrank、dumpsys meminfo查看,当然也只可以过滤出某个进程如:dumpsys meminfo | grep -i phone
    先来看下procrank
     
    view sourceprint?
    01.sh-4.2# procrank
    02.PID      Vss      Rss      Pss      Uss  cmdline
    03.1078   59840K   59708K   42125K   39344K  com.csr.BTApp
    04.2683   59124K   59040K   37960K   33032K  com.android.launcher
    05.1042   51572K   51488K   35686K   33604K  android.process.acore
    06.782   32808K   32748K   16775K   14716K  system_server
    07.667   20560K   17560K   12739K    8940K  /system/bin/surfaceflinger
    08.851   30124K   30036K   12085K    7996K  com.android.systemui
    09.2999   27680K   27596K    9929K    7040K  com.baidu.input
    10.959   20764K   20676K    5522K    3788K  com.android.phone
    11.3468   21892K   21800K    4591K    1920K  com.apical.dreamthemetime
    12.982   19880K   19792K    4438K    2644K  com.csr.csrservices
    13.668   19592K   19480K    3525K    1360K  zygote
    14.670    2960K    2960K    2407K    2356K  /system/bin/mediaserver
    15.663    1784K    1784K    1209K    1116K  /system/bin/synergy_service
    16.756    3404K    1348K    1133K    1124K  /usr/bin/gpsexe
    17.669    1468K    1468K     959K     928K  /system/bin/drmserver
    18.675     692K     692K     692K     692K  /bin/sh
    19.758    1060K    1060K     630K     604K  /system/bin/audiotransfer
    20.3482     656K     652K     456K     444K  procrank
    21.664     664K     664K     403K     392K  /system/bin/netd
    22.658     584K     584K     331K     320K  /system/bin/vold
    23.666     548K     548K     270K     256K  /system/bin/rild
    24.671     416K     412K     212K     204K  /system/bin/dbus-daemon
    25.673     336K     332K     170K     164K  /system/bin/keystore
    26.1     164K     164K     144K     144K  /init
    27.674     152K     152K     136K     136K  /sbin/adbd
    28.662     312K     312K     112K     104K  /system/bin/dvdd
    29.672     328K     324K     109K     100K  /system/bin/installd
    30.657     268K     264K     102K      96K  /system/bin/servicemanager
    31.649      84K      84K      84K      84K  /sbin/ueventd
    32.665     260K     256K      83K      76K  /system/bin/debuggerd
    33.------   ------  ------
    34.195031K  163724K  TOTAL
    35.
    36.RAM: 480380K total, 3624K free, 732K buffers, 299788K cached, 264844K shmem, 7632K slab
    从以上打印可以看出,一般来说内存占用大小有如下规律:VSS >= RSS >= PSS >= USS
    VSS - Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)是单个进程全部可访问的地址空间
    RSS - Resident Set Size 实际使用物理内存(包含共享库占用的内存)是单个进程实际占用的内存大小,对于单个共享库, 尽管无论多少个进程使用,实际该共享库只会被装入内存一次。
    PSS - Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)
    USS - Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)USS 是一个非常非常有用的数字, 因为它揭示了运行一个特定进程的真实的内存增量大小。如果进程被终止, USS 就是实际被返还给系统的内存大小。
    USS 是针对某个进程开始有可疑内存泄露的情况,进行检测的最佳数字。怀疑某个程序有内存泄露可以查看这个值是否一直有增加
    使用dumpsys meminfo查看内存信息
    view sourceprint?
    01.sh-4.2# dumpsys meminfo
    02.Applications Memory Usage (kB):
    03.Uptime: 3353863 Realtime: 3353850
    04.
    05.Total PSS by process:
    06.41743 kB: com.csr.BTApp (pid 1078)
    07.36924 kB: com.android.launcher (pid 2683)
    08.35452 kB: android.process.acore (pid 1042)
    09.16094 kB: system (pid 782)
    10.11609 kB: com.android.systemui (pid 851)
    11.8564 kB: com.baidu.input (pid 2999)
    12.5298 kB: com.android.phone (pid 959)
    13.4443 kB: com.apical.dreamthemetime (pid 4448)
    14.4203 kB: com.csr.csrservices (pid 982)
    15.4130 kB: com.apical.apicalradio (pid 4518)
    16.
    17.Total PSS by OOM adjustment:
    18.16094 kB: System
    19.16094 kB: system (pid 782)
    20.21110 kB: Persistent
    21.11609 kB: com.android.systemui (pid 851)
    22.5298 kB: com.android.phone (pid 959)
    23.4203 kB: com.csr.csrservices (pid 982)
    24.36924 kB: Foreground
    25.36924 kB: com.android.launcher (pid 2683)
    26.85759 kB: Perceptible
    27.41743 kB: com.csr.BTApp (pid 1078)
    28.35452 kB: android.process.acore (pid 1042)
    29.8564 kB: com.baidu.input (pid 2999)
    30.4443 kB: A Services
    31.4443 kB: com.apical.dreamthemetime (pid 4448)
    32.4130 kB: Background
    33.4130 kB: com.apical.apicalradio (pid 4518)
    34.
    35.Total PSS by category:
    36.56020 kB: Dalvik
    37.30214 kB: Other dev
    38.27716 kB: Native
    39.24504 kB: Cursor
    40.13198 kB: Unknown
    41.7723 kB: Other mmap
    42.6895 kB: .so mmap
    43.1232 kB: .apk mmap
    44.888 kB: .dex mmap
    45.36 kB: .ttf mmap
    46.34 kB: Ashmem
    47.0 kB: .jar mmap
    48.
    49.Total PSS: 168460 kB
    打印某个程序内存信息,把包名写上,如:com.android.launcher
    view sourceprint?
    01.sh-4.2# dumpsys meminfo com.android.launcher
    02.Applications Memory Usage (kB):
    03.Uptime: 4497753 Realtime: 4497741
    04.
    05.** MEMINFO in pid 2683 [com.android.launcher] **
    06.Shared  Private     Heap     Heap     Heap
    07.Pss    Dirty    Dirty     Size    Alloc     Free
    08.------   ------   ------   ------   ------   ------
    09.Native     2144      988     2040     8636     5124     1699
    10.Dalvik     9481     8292     8644    13639    13335      304
    11.Cursor        0        0        0                         
    12.Ashmem        2        4        0                         
    13.Other dev        4       20        0                         
    14..so mmap      922     1892      292                         
    15..jar mmap        0        0        0                         
    16..apk mmap       90        0        0                         
    17..ttf mmap        0        0        0                         
    18..dex mmap      300        0        0                         
    19.Other mmap     1634       16      112                         
    20.Unknown     1830      580     1772                         
    21.TOTAL    16407    11792    12860    22275    18459     2003
    22.
    23.Objects
    24.Views:      146         ViewRootImpl:        1
    25.AppContexts:      374           Activities:        1
    26.Assets:        4        AssetManagers:        4
    27.Local Binders:       13        Proxy Binders:       23
    28.Death Recipients:        1
    29.OpenSSL Sockets:        0
    30.
    31.SQL
    32.heap:       59          MEMORY_USED:       59
    33.PAGECACHE_OVERFLOW:        1          MALLOC_SIZE:       46
    34.
    35.DATABASES
    36.pgsz     dbsz   Lookaside(b)          cache  Dbname
    37.1      179             55         2/11/2  launcher.db
    38.
    39.Asset Allocations
    40.zip:/system/app/Launcher2.apk:/resources.arsc: 340K
    41.zip:/system/app/MediaCenter.apk:/resources.arsc: 604K
    Android程序内存被分为2部分:native和dalvik,dalvik就是我们平常说的java堆,我们创建的对象是在这里面分配的,而bitmap是直接在native上分配的,对于内存的限制是 native+dalvik 不能超过最大限制。Android程序内存一般限制在16M,当然也有24M的。
    从上信息对于分析内存泄露,内存溢出都有极大的作用,从以上信息可以看到该应用程序占用的native和dalvik,当TOTAL 16407 11792 12860 22275 18459 2003超过内存最大限制时会出现OOM错误。
    dumpsys能做的事还有很多
    view sourceprint?
    1.dumpsys [options]
    2.meminfo 显示内存信息
    3.cpuinfo 显示CPU信息
    4.account 显示accounts信息
    5.activity 显示所有的activities的信息
    6.window 显示键盘,窗口和它们的关系
    7.wifi 显示wifi信息
     






    http://blog.csdn.net/watermusicyes/article/details/46333925
    LeakCanary——直白的展现Android中的内存泄露
    之前碰到的OOM问题,终于很直白的呈现在我的眼前:我尝试了MAT,但是发现不怎么会用。直到今天终于发现了这个新工具:
    当我们的App中存在内存泄露时会在通知栏弹出通知:
     
    当点击该通知时,会跳转到具体的页面,展示出Leak的引用路径,如下图所示:
     
    LeakCanary 可以用更加直白的方式将内存泄露展现在我们的面前。
    以下是我找到的学习资料,写的非常棒:
    1、LeakCanary: 让内存泄露无所遁形
    2、LeakCanary 中文使用说明
    AndroidStudio (官方)上使用LeakCanary 请移步:
    https://github.com/square/leakcanary
    Eclipse 上使用LeakCanary 请移步我的:
    https://github.com/SOFTPOWER1991/LeakcanarySample-Eclipse
    android studio (自己弄的)上使用LeakCanary也可以看这个:
    leakcanarySample_androidStudio
    工程包括:
    LeakCanary库代码
    LeakCanaryDemo示例代码
    使用步骤:
    将LeakCanary import 入自己的工程
    添加依赖:
    compile project(':leakcanary')
    在Application中进行配置
    public class ExampleApplication extends Application {

      ......
      //在自己的Application中添加如下代码
    public static RefWatcher getRefWatcher(Context context) {
        ExampleApplication application = (ExampleApplication) context
                .getApplicationContext();
        return application.refWatcher;
    }

      //在自己的Application中添加如下代码
    private RefWatcher refWatcher;

    @Override
    public void onCreate() {
        super.onCreate();
        ......
            //在自己的Application中添加如下代码
        refWatcher = LeakCanary.install(this);
        ......
    }

    .....
    }
    在Activity中进行配置
    public class MainActivity extends AppCompatActivity {

        ......
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
                //在自己的应用初始Activity中加入如下两行代码
            RefWatcher refWatcher = ExampleApplication.getRefWatcher(this);
            refWatcher.watch(this);

            textView = (TextView) findViewById(R.id.tv);
            textView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    startAsyncTask();
                }
            });

        }

        private void async() {

            startAsyncTask();
        }

        private void startAsyncTask() {
            // This async task is an anonymous class and therefore has a hidden reference to the outer
            // class MainActivity. If the activity gets destroyed before the task finishes (e.g. rotation),
            // the activity instance will leak.
            new AsyncTask<Void, Void, Void>() {
                @Override
                protected Void doInBackground(Void... params) {
                    // Do some slow work in background
                    SystemClock.sleep(20000);
                    return null;
                }
            }.execute();
        }


    }
    在AndroidMainfest.xml 中进行配置,添加如下代码
            <service
                android:name="com.squareup.leakcanary.internal.HeapAnalyzerService"
                android:enabled="false"
                android:process=":leakcanary" />
            <service
                android:name="com.squareup.leakcanary.DisplayLeakService"
                android:enabled="false" />

            <activity
                android:name="com.squareup.leakcanary.internal.DisplayLeakActivity"
                android:enabled="false"
                android:icon="@drawable/__leak_canary_icon"
                android:label="@string/__leak_canary_display_activity_label"
                android:taskAffinity="com.squareup.leakcanary"
                android:theme="@style/__LeakCanary.Base" >
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />

                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>














    http://www.liaohuqiu.net/cn/posts/leak-canary-read-me/
    LeakCanary 中文使用说明
    10 May 2015

    LeakCanary
    Android 和 Java 内存泄露检测。
    “A small leak will sink a great ship.” - Benjamin Franklin
    千里之堤, 毁于蚁穴。 -- 《韩非子·喻老》
     
    demo
    一个非常简单的 LeakCanary demo: https://github.com/liaohuqiu/leakcanary-demo
    开始使用
    在 build.gradle 中加入引用,不同的编译使用不同的引用:
     dependencies {
       debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
       releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
     }
    在 Application 中:
    public class ExampleApplication extends Application {

      @Override public void onCreate() {
        super.onCreate();
        LeakCanary.install(this);
      }
    }
    这样,就万事俱备了! 在 debug build 中,如果检测到某个 activity 有内存泄露,LeakCanary 就是自动地显示一个通知。
    为什么需要使用 LeakCanary?
    问得好,看这个文章LeakCanary: 让内存泄露无所遁形
    如何使用
    使用 RefWatcher 监控那些本该被回收的对象。
    RefWatcher refWatcher = {...};

    // 监控
    refWatcher.watch(schrodingerCat);
    LeakCanary.install() 会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄露的 activity。
    public class ExampleApplication extends Application {

      public static RefWatcher getRefWatcher(Context context) {
        ExampleApplication application = (ExampleApplication) context.getApplicationContext();
        return application.refWatcher;
      }

      private RefWatcher refWatcher;

      @Override public void onCreate() {
        super.onCreate();
        refWatcher = LeakCanary.install(this);
      }
    }
    使用 RefWatcher 监控 Fragment:
    public abstract class BaseFragment extends Fragment {

      @Override public void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
        refWatcher.watch(this);
      }
    }
    工作机制
    RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。
    然后在后台线程检查引用是否被清除,如果没有,调用GC。
    如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。
    在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。
    得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄露。
    HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。
    引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。
    如何复制 leak trace?
    在 Logcat 中,你可以看到类似这样的 leak trace:
    In com.example.leakcanary:1.0:1 com.example.leakcanary.MainActivity has leaked:

    * GC ROOT thread java.lang.Thread.<Java Local> (named 'AsyncTask #1')
    * references com.example.leakcanary.MainActivity$3.this$0 (anonymous class extends android.os.AsyncTask)
    * leaks com.example.leakcanary.MainActivity instance

    * Reference Key: e71f3bf5-d786-4145-8539-584afaecad1d
    * Device: Genymotion generic Google Nexus 6 - 5.1.0 - API 22 - 1440x2560 vbox86p
    * Android Version: 5.1 API: 22
    * Durations: watch=5086ms, gc=110ms, heap dump=435ms, analysis=2086ms
    你甚至可以通过分享按钮把这些东西分享出去。
    SDK 导致的内存泄露
    随着时间的推移,很多SDK 和厂商 ROM 中的内存泄露问题已经被尽快修复了。但是,当这样的问题发生时,一般的开发者能做的事情很有限。
    LeakCanary 有一个已知问题的忽略列表,AndroidExcludedRefs.java,如果你发现了一个新的问题,请提一个 issue 并附上 leak trace, reference key, 机器型号和 SDK 版本。如果可以附带上 dump 文件的 链接那就再好不过了。
    对于最新发布的 Android,这点尤其重要。你有机会在帮助在早期发现新的内存泄露,这对整个 Android 社区都有极大的益处。
    开发版本的 Snapshots 包在这里: Sonatype's snapshots repository。
    leak trace 之外
    有时,leak trace 不够,你需要通过 MAT 或者 YourKit 深挖 dump 文件。
    通过以下方法,你能找到问题所在:
    查找所有的 com.squareup.leakcanary.KeyedWeakReference 实例。
    检查 key 字段
    Find the KeyedWeakReference that has a key field equal to the reference key reported by LeakCanary.
    找到 key 和 和 logcat 输出的 key 值一样的 KeyedWeakReference。
    referent 字段对应的就是泄露的对象。
    剩下的,就是动手修复了。最好是检查到 GC root 的最短强引用路径开始。
    自定义
    UI 样式
    DisplayLeakActivity 有一个默认的图标和标签,你只要在你自己的 APP 资源中,替换以下资源就可。
    res/
      drawable-hdpi/
        __leak_canary_icon.png
      drawable-mdpi/
        __leak_canary_icon.png
      drawable-xhdpi/
        __leak_canary_icon.png
      drawable-xxhdpi/
        __leak_canary_icon.png
      drawable-xxxhdpi/
        __leak_canary_icon.png
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
      <string name="__leak_canary_display_activity_label">MyLeaks</string>
    </resources>
    保存 leak trace
    DisplayLeakActivity saves up to 7 heap dumps & leak traces in the app directory. You can change that number by providing R.integer.__leak_canary_max_stored_leaks in your app:
    在 APP 的目录中,DisplayLeakActivity 保存了 7 个 dump 文件和 leak trace。你可以在你的 APP 中,定义 R.integer.__leak_canary_max_stored_leaks 来覆盖类库的默认值。
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
      <integer name="__leak_canary_max_stored_leaks">20</integer>
    </resources>
    上传 leak trace 到服务器
    你可以改变处理完成的默认行为,将 leak trace 和 heap dump 上传到你的服务器以便统计分析。
    创建一个 LeakUploadService, 最简单的就是继承 DisplayLeakService :
    public class LeakUploadService extends DisplayLeakService {
      @Override
      protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {
        if (!result.leakFound || result.excludedLeak) {
          return;
        }
        myServer.uploadLeakBlocking(heapDump.heapDumpFile, leakInfo);
      }
    }
    请确认 release 版本 使用 RefWatcher.DISABLED:
    public class ExampleApplication extends Application {

      public static RefWatcher getRefWatcher(Context context) {
        ExampleApplication application = (ExampleApplication) context.getApplicationContext();
        return application.refWatcher;
      }

      private RefWatcher refWatcher;

      @Override public void onCreate() {
        super.onCreate();
        refWatcher = installLeakCanary();
      }

      protected RefWatcher installLeakCanary() {
        return RefWatcher.DISABLED;
      }
    }
    自定义 RefWatcher:
    public class DebugExampleApplication extends ExampleApplication {
      protected RefWatcher installLeakCanary() {
        return LeakCanary.install(app, LeakUploadService.class);
      }
    }
    别忘了注册 service:
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        >
      <application android:name="com.example.DebugExampleApplication">
        <service android:name="com.example.LeakUploadService" />
      </application>
    </manifest>


    demo
    一个非常简单的 LeakCanary demo: https://github.com/liaohuqiu/leakcanary-demo























    如何使用eclipse单独调试android系统的app
    修改系统的app的时候,如果需要编译整个工程,那的确很慢。

    虽然强大的make功能可以方便的使用mm只编译修改了的那个应用。

    单独的一个应用跑起来才够快。

    做法是这样:

    一次编译过后(比如你改了framework,有新的修改在里面)
    把out/target/common/obj/java_libraries/
    里面的相关jar考出来作为工程的jar source library。
    主要是core_intermediates,ext_intermediates,framework_intermediates,google-framework_intermediates
    这4个。就够用了。
























    http://www.2cto.com/kf/201207/140247.html
    Android上oprofile使用说明
    1.     目的
    本文介绍了oprofile的功能并基于Android 2.1介绍了使用oprofile的基本方法和步骤。本文的读者为软件开发人员和白盒测试人员。
    2.     oprofile简介
    Oprofile是用于Linux的若干种评测和性能监控工具中的一种,它可以工作在不同的体系结构上,包括IA32、IA64、AMD Athlon系列及ARM等。Oprofile包含在Linux 2.5和更高版本的内核中,也包含在大多数较新的Linux发行版本中,在Android中已经集成了Oprofile。
    oprofile以很低的开销对系统中所有运行的代码(包括kernel、kernel模块、库、应用程序)进行函数级别的性能分析(function-level profiling),跟踪占用CPU高的函数的调用信息,从而判断程序中哪些地方存在需要优化的性能瓶颈。
    oprofile支持两种采样(sampling)方式:基于事件的采样(event based)和基于时间的采样(time based)。
    基于事件的采样是oprofile只记录特定事件(比如L2 cache miss)的发生次数,当达到用户设定的定值时oprofile就记录一下(采一个样)。这种方式需要CPU内部有性能计数器(performance counter)。
    基于时间的采样是oprofile借助OS时钟中断的机制,每个时钟中断oprofile都会记录一次(采一次样),又分为RTC模式(RTC mode,适用于2.2/2.4内核)和定时器中断模式(timer interrupt mode,适用于2.6以上内核)。引入定时器采样模式的目的在于,提供对没有性能计数器的CPU的支持,其精度相对于基于事件的采样要低,并且因为要借 助OS时钟中断的支持,对禁用中断的代码oprofile不能对其进行分析。
    高通QSD8K处理器具备性能计数器,但当前发布的软件版本只支持定时器模式。
    在Android上,oprofile分为target端和host端两部分。target端运行在设备上,主要包括一个内核模块 (oprofile.ko)和一个用户空间的守护进程(oprofiled),前者负责访问性能计数器或者注册基于时间采样的函数(使用 register_timer_hook注册之,使时钟中断处理程序最后执行profile_tick时可以访问之),并采样置于内核的缓冲区内;后者在 后台运行,负责从内核空间收集数据,写入文件。host端运行在PC上,包括一组后处理工具用于从原始采样数据生成可读的分析报告。
    3.     oprofile使用方法
    本节基于Android 2.1 (Eclair)介绍oprofile的使用方法,在Android 1.6 (Donut)和Android 2.2 (Froyo)上的使用方法与之相似。
    在使用oprofile之前,首先必须确保烧到手机上的系统具有root权限,否则无法使用oprofile,并确保/data下有足够的可用空间用于保存采样数据(一般几十MB足够)。依次按照如下步骤操作。
    3.1   步骤一:安装target端
    Android源代码中已经包含了移植好的oprofile源代码,kernel缺省配置选项也已经enable了对oprofile的支持。在将 oprofile的target端安装到手机之前,请首先在eng模式下编译源代码,然后按照如下步骤将所需文件复制到手机上:
    1. 内核模块oprofile.ko:
    在PC上运行:
    source build/envsetup.sh
    choosecombo
    adb push $ANDROID_PRODUCT_OUT/obj/KERNEL_OBJ/arch/arm/oprofile/oprofile.ko /data
    2. 守护进程oprofiled和控制程序opcontrol:
    如果手机上烧的system.img是eng模式编出来的,里面已经包含了oprofiled和opcontrol(在/system/xbin/),这一步可以跳过;如果是user模式编出来的system.img则不包含这两个文件,请运行:
    adb push $ANDROID_PRODUCT_OUT/system/xbin/oprofiled /system/xbin
    adb push $ANDROID_PRODUCT_OUT/system/xbin/opcontrol /system/xbin
    3. elf内核映像文件:
    如果要对内核进行profiling,需要将压缩的elf内核映像文件vmlinux复制到手机上,运行:
    adb push $ANDROID_PRODUCT_OUT/obj/KERNEL_OBJ/arch/arm/boot/compressed/vmlinux /data
    该vmlinux必须与手机上实际运行的内核一致。
    3.2   步骤二:计算内核虚拟地址范围
    要对内核进行profiling,需要计算内核开始和结束地址:
    运行:
    adb pull /proc/kallsyms .
    在文件kallsyms中查找_text,其数值即为kernel start地址;查找_etext,其数值即为kernel end地址。
    3.3   步骤三:将CPU设置在保持最高频率运行
    为确保oprofile采样结果的准确性和一致性,在采样开始之前需将CPU设置在保持最高频率运行,为此在adb shell中运行:
    mkdir /data/debug
    mount -t debugfs debugfs /data/debug
    echo 1 > /data/debug/nohlt
    echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
    3.4    步骤四:配置oprofile
    在adb shell中运行:
    insmod /data/oprofile.ko timer=1
    opcontrol --setup
    oprofiled --session-dir=/data/oprofile --vmlinux=/data/vmlinux --kernel-range=start,end --events=CPU_CYCLES:255:0:50000:0:1:1 --separate-lib=1 --separate-kernel=1
    说明:
    其中--kernel-range中的start和end即为上述步骤二中获得的kernel start地址和kernel end地址。
    3.5   步骤五:开始采样
    运行需要进行profiling的应用程序或场景,然后在adb shell中运行:
    opcontrol --start
    此时oprofile已开始采样,可在adb shell中运行:
    opcontrol --status
    随时查看oprofile运行状态和已采集的样本数。
    3.6   步骤六:停止采样
    当采集的样本数足够多的时候(根据经验一般数千即可),在adb shell中运行:
    opcontrol --stop
    3.7   步骤七:上传数据和生成分析结果
    在PC上运行:
    source build/envsetup.sh
    choosecombo
    cp $ANDROID_PRODUCT_OUT/obj/KERNEL_OBJ/vmlinux $ANDROID_PRODUCT_OUT/symbols
    python $ANDROID_BUILD_TOP/external/oprofile/opimport_pull ~/oprofile-result
    此时手机上的采样数据经转换后存在PC上~/oprofile-result中,运行以下命令显示分析结果报告:
    cd ~/oprofile-result
    $OPROFILE_EVENTS_DIR/bin/opreport --session-dir=. -p $ANDROID_PRODUCT_OUT/symbols -d -l
    (注:在Android 2.1 (Eclair)中执行上面的命令出错,需将opreport换成自己编译的 0.9.4版本的opreport,编译步骤参见本文附录。)
    注意:执行以上步骤时若遇到如下出错信息:
    opreport: error while loading shared libraries: libbfd-2.18.0.20080103.so: cannot open shared object file: No such file or directory
    可运行如下命令予以解决:
    ln -s /usr/lib/libbfd.so /usr/lib/libbfd-2.18.0.20080103.so
    3.8   重新运行oprofile
    上述步骤一至七完成了一次完整的oprofile采样和分析过程,若要再次运行oprofile,需复位oprofile并清理上一次的采样数据,在adb shell中运行:
    opcontrol --shutdown
    rm /data/oprofile
    umount /data/debug
    rm -rf /data/debug
    然后重新执行上述步骤三至七进行下一次的采集和分析。
    4.     简化oprofile使用步骤的脚本
    从上文的介绍可以看到oprofile的使用比较复杂,为简化使用步骤,特编写若干脚本,只需按照如下步骤运行这些脚本即完成oprofile的各项操作:
    1.     工作环境准备:①在eng模式下编译源代码;②为使用oprofile创建一个工作目录(例如~/oprofile),将下面附的压缩包 oprofile_cmd.zip解开到该目录下,并进入到该目录下;③按照前文3.2节的方法获取kernel start地址和end地址填入op.sh;④确保烧到手机上的系统具有root权限,且所编译的内核源代码与手机上实际运行的内核一致;⑤确保运行过 choosecombo;
    2.     安装target端:用数据线连接手机和PC,在PC上的oprofile工作目录下运行:
    ./prepare.sh
    (注:若系统是Android 1.6 (Donut),则运行./prepare.sh donut)
    3.     采样:运行需要进行profiling的应用程序或场景,然后在adb shell中运行:
    cd /data
    sh op.sh
    此时可运行opcontrol --status查看oprofile运行状态和已采集的样本数,当采集的样本数足够多的时候(根据经验一般数千即可),在adb shell中运行:
    opcontrol --stop
    4.     上传数据:在PC上的oprofile工作目录下运行:
    ./import.sh
    5.     显示分析结果:在PC上的oprofile工作目录下运行:
    ./report.sh
    6.     重新运行oprofile:在adb shell中运行:
    cd /data
    sh opclean.sh
    然后重新执行上述步骤3至5。
    5.     oprofile分析结果实例
    下面是在Android 2.1 (Eclair)上对一个运行alpha animation的应用程序进行oprofile分析的结果:
    CPU: CPU with timer interrupt, speed 0 MHz (estimated)
    Profiling through timer interrupt
              TIMER:0|
      samples|      %|
    ------------------
         1769 98.4418 app_process
                  TIMER:0|
          samples|      %|
        ------------------
             1299 73.4313 libskia.so
              386 21.8202 vmlinux
               39  2.2046 libdvm.so
               17  0.9610 libc.so
                9  0.5088 libui.so
                4  0.2261 libbinder.so
                4  0.2261 libsurfaceflinger.so
                4  0.2261 libutils.so
                3  0.1696 libandroid_runtime.so
                1  0.0565 libGLES_android.so
                1  0.0565 gralloc.qsd8k.so
                1  0.0565 libm.so
                1  0.0565 libstdc++.so
           13  0.7234 rild
                  TIMER:0|
          samples|      %|
        ------------------
                9 69.2308 vmlinux
                4 30.7692 linker
            6  0.3339 vmlinux
            4  0.2226 cnd
                  TIMER:0|
          samples|      %|
        ------------------
                2 50.0000 linker
                1 25.0000 cnd
                1 25.0000 vmlinux
            2  0.1113 adbd
                  TIMER:0|
          samples|      %|
        ------------------
                2 100.000 vmlinux
            1  0.0556 init
                  TIMER:0|
          samples|      %|
        ------------------
                1 100.000 vmlinux
            1  0.0556 port-bridge
                  TIMER:0|
          samples|      %|
        ------------------
                1 100.000 vmlinux
            1  0.0556 opcontrol
                  TIMER:0|
          samples|      %|
        ------------------
                1 100.000 vmlinux
     
    samples  %        image name               app name                 symbol name
    554      30.8292  libskia.so               app_process              S32A_Opaque_BlitRow32_neon
    456      25.3756  libskia.so               app_process              S32A_D565_Blend_neon(unsigned short*, unsigned int const*, int, unsigned int, int, int)
    128       7.1230  vmlinux                  app_process              __memzero
    113       6.2883  libskia.so               app_process              memset_128_loop
    78        4.3406  libskia.so               app_process              memset_128_loop
    65        3.6171  vmlinux                  app_process              _spin_unlock_irqrestore
    20        1.1130  vmlinux                  app_process              get_page_from_freelist
    18        1.0017  libskia.so               app_process              Sprite_D32_S32::blitRect(int, int, int, int)
    17        0.9460  vmlinux                  app_process              v7wbi_flush_user_tlb_range
    16        0.8904  vmlinux                  app_process              free_hot_cold_page
    15        0.8347  libskia.so               app_process              Sprite_D16_S32_BlitRowProc::blitRect(int, int, int, int)
    13        0.7234  vmlinux                  app_process              _spin_unlock_irq
    12        0.6678  libdvm.so                app_process              dalvik_inst
    12        0.6678  libskia.so               app_process              SkDraw::drawPaint(SkPaint const&) const
    11        0.6121  vmlinux                  app_process              __dabt_usr
    11        0.6121  vmlinux                  app_process              v7_dma_flush_range
    10        0.5565  libskia.so               app_process              S32A_D565_Opaque_Dither(unsigned short*, unsigned int const*, int, unsigned int, int, int)
    ...
    从以上结果可以看出,在运行alpha animation的过程中,在oprofile采样数据中2D图形库libskia.so占了最高的比例,达73.4313%,具体是 libskia.so中的函数S32A_Opaque_BlitRow32_neon和S32A_D565_Blend_neon分别占了最高的前二名, 说明在这一过程中最影响性能的瓶颈是在skia库中的这几个函数。
    附录  关于在Android 1.6 (Donut)上使用oprofile的注意事项
    Android 1.6 (Donut)中自带的opimport和opreport(位于$OPROFILE_EVENTS_DIR/bin/)是64位的,只能在安装了64位Linux系统的PC上运行,要在32位系统的PC上运行,解决方法是自己编译0.9.4版本的opimport和opreport(因Android 1.6 (Donut)中自带的oprofile是0.9.4版本的)。0.9.4版本oprofile源代码附在下面:(略)
     
    编译步骤如下:
    source build/envsetup.sh
    choosecombo
    tar -zxvf oprofile-0.9.4.tar.gz
    cd oprofile-0.9.4
    ./configure --with-linux=$ANDROID_PRODUCT_OUT/obj/KERNEL_OBJ
    make
    make install
    或者采用Android 2.1 (Eclair)以上版本中自带的opimport和opreport(版本为0.9.5)也可以。
    此外Android 1.6 (Donut)中的脚本$ANDROID_BUILD_TOP/external/oprofile/opimport_pull有问题,可替换成Android 2.1 (Eclair)以上版本中的此文件。
    作者:迷糊







































    http://www.cnblogs.com/bangerlee/archive/2012/08/30/2659435.html
    谁动了我的cpu——oprofile使用札记
    引言
    cpu无端占用高?应用程序响应慢?苦于没有分析的工具?
    oprofile利用cpu硬件层面提供的性能计数器(performance counter),通过计数采样,帮助我们从进程、函数、代码层面找出占用cpu的"罪魁祸首"。下面我们通过实例,了解oprofile的具体使用方法。
     
    常用命令
    使用oprofile进行cpu使用情况检测,需要经过初始化、启动检测、导出检测数据、查看检测结果等步骤,以下为常用的oprofile命令。
    初始化
    opcontrol --no-vmlinux : 指示oprofile启动检测后,不记录内核模块、内核代码相关统计数据
    opcontrol --init : 加载oprofile模块、oprofile驱动程序
    检测控制
    opcontrol --start : 指示oprofile启动检测
    opcontrol --dump : 指示将oprofile检测到的数据写入文件
    opcontrol --reset : 清空之前检测的数据记录
    opcontrol -h : 关闭oprofile进程
    查看检测结果
    opreport : 以镜像(image)的角度显示检测结果,进程、动态库、内核模块属于镜像范畴
    opreport -l : 以函数的角度显示检测结果
    opreport -l test : 以函数的角度,针对test进程显示检测结果
    opannotate -s test : 以代码的角度,针对test进程显示检测结果
    opannotate -s /lib64/libc-2.4.so : 以代码的角度,针对libc-2.4.so库显示检测结果
     
    opreport输出解析
    正如以上命令解析所言,不加参数的opreport命令从镜像的角度显示cpu的使用情况:
     
    linux # opreport
    CPU: Core 2, speed 2128.07 MHz (estimated)
    Counted CPU_CLK_UNHALTED events (Clock cycles when not halted) with a unit mask of 0x00 (Unhalted core cycles) count 100000
    CPU_CLK_UNHALT.........|
      samples |           %|
    ------------------------
       31645719     87.6453      no-vmlinux  
        4361113     10.3592      libend.so
           7683      0.1367      libpython2.4.so.1.0
           7046      0.1253      op_test
            ⋯⋯
     
    以上列表按以下形式输出:
                  samples |                            %|
    -----------------------------------------------------
         镜像内发生的采样次数     采样次数所占总采样次数的百分比      镜像名称
     
    因我们在初始化时执行了"opcontrol --no-vmlinux"命令,指示oprofile不对模块和内核进行检测,因而在探测结果中,模块和内核一同显示成no-vmlinux镜像。输出 中,libend.so和libpython2.4.so.1.0均为动态库,op_test为进程。以上采样数据表明,检测时间内,cpu主要执行内核 和模块代码,用于执行libend.so库函数的比重亦较大,达到10%左右。
     
    进一步地,我们可以查看到进程、动态库中的每个函数在检测时间内占用cpu的情况:
     
    linux # opreport -l
     samples           %        image name        app name         symbol name
    31645719     87.4472        no-vmlinux      no-vmlinux         /no-vmlinux
     4361113     10.3605         libend.so       libend.so             endless
        7046      0.1253           op_test         op_test                main
        ⋯⋯
     
    以上输出显示消耗cpu的函数为libend.so库中的endless函数,以及op_test程序中的main函数。
     
    进行oprofile初始化时,若我们执行opcontrol --vmlinux=vmlinux-`uname -r`,指定oprofile对内核和内核模块进行探测,在执行opreport查看检测结果时,内核和内核模块就不再显示为no-vmlinux,而是 内核和各个内核模块作为单独的镜像,显示相应cpu占用情况。
     
    使用opannotate从代码层看cpu占用情况
    以上介绍了使用oprofile的opreport命令,分别从进程和函数层面查看cpu使用情况的方法。看到这里,有的同学可能有这样的疑问:使 用opreport,我找到了消耗cpu的进程A,找到了进程A中最消耗cpu的函数B,进一步地,是否有办法找到函数B中最消耗cpu的那一行代码呢?
     
    oprofile中的opannotate命令可以帮助我们完成这个任务,结合具备调试信息的程序、带有debuginfo的动态 库,opannotate命令可显示代码层面占用cpu的统计信息。下面我们通过几个简单的程序实例,说明opannotate命令的使用方法。
     
    首先,我们需要一个消耗cpu的程序,该程序代码如下:
     
    //op_test.c
    extern void endless();
    int main()
    {
      int i = 0, j = 0;
      for (; i < 10000000; i++ )
         {
               j++;
         }
      endless();
      return 0;
    }
     
    该程序引用了外部函数endless,endless函数定义如下:
     
    //end.c
    void endless()
    {
      int i = 0;
      while(1)
         {
             i++;
         }
    }
     
    endless函数同样很简单,下面我们将定义了endless函数的end.c进行带调试信息地编译,并生成libend.so动态库文件:
    linux # gcc -c -g -fPIC end.c
    linux # gcc -shared -fPIC -o libend.so end.o
    linux # cp libend.so /usr/lib64/libend.so
    接着,带调试信息地编译op_test.c,生成op_test执行文件:
    linux # gcc -g -lend -o op_test op_test.c
     
    之后,我们开启oprofile进行检测,并拉起op_test进程:
    linux # opcontrol --reset
    linux # opcontrol --start
    linux # ./op_test &
    在程序运行一段时间后,导出检测数据,使用opannotate进行结果查看:
     
    linux # opcontrol --dump
    linux # opannotate -s op_test
    /*
     * Total samples for file : "/tmp/lx/op_test.c"
     *
     * 7046  100.00
     */
                   : int main()
                   :{ /*main total : 7046  100.000 */
                  :    int i = 0, j = 0;
    6447   91.4987 :    for (; i < 10000000; i++ )
                   :    {
     599    8.5013 :          j++;
                   :    }
                 :    endless();
                 :    return 0;
                   :}
     
    以上输出表明,在op_test程序的main函数中,主要消耗cpu的是for循环所在行代码,因该段代码不仅包含变量i的自增运算,还将i与10000000进行比较。
     
    下面显示对自编动态库libend.so的检测结果:
     
    linux # opannotate -s /usr/lib64/libend.so
    /*
     * Total samples for file : "/tmp/lx/end.c"
     *
     * 4361113  100.00
     */
                     : void endless()
                     : {
                    :     int i = 0;
                    :     while(1)
                     :     {
      25661   0.6652 :          i++;
    4335452  99.3348 :     }
                     : }
     
     
    查看c库代码占用cpu情况
    以上使用opannotate,分别查看了应用程序代码、自编动态库代码的cpu占用情况,对于c库中的代码,我们是否也能查看其消耗cpu的情况呢?
     
    在使用oprofile查看c库代码信息前,需要安装glibc的debuginfo包,安装debuginfo包之后,我们即可以通过opannotate查看到c库代码,以下展示了malloc底层实现函数_int_malloc的部分代码:
     
    linux # opannotate -s /lib64/libc-2.4.so
    /*
     ----------------malloc---------------------
     */
                    :Void_t *
                    :_int_malloc( mstate av, size_t bytes )
                    :{  /* _int_malloc total: 118396  94.9249 */
                         ⋯⋯
                    :       assert((fwd->size & NON_MAIN_ARENA) == 0);
    115460  92.5709 :       while((unsigned long)(size) < (unsigned long)(fwd->size)) {
      1161   0.9308 :            fwd = fwd->fd;
                    :            assert((fwd->size & NON_MAIN_ARENA) == 0);
                    :        }
                    :}
     
     
    在进行程序性能调优时,根据oprofile检测到的c库代码占用cpu的统计信息,可以判别程序性能瓶颈是否由c库代码引起。若oprofile 检测结果显示cpu被过多地用于执行c库中的代码,我们可进一步地采用修改c库代码、升级glibc版本等方法解决c库引发的应用程序性能问题。
     
    小结
    本文介绍了使用oprofile工具从进程、函数和代码层面检测cpu使用情况的方法,对于代码层面,分别介绍了查看程序代码、自编动态库代码以及 gblic代码cpu统计情况的方法,中间过程使用到opcontrol、opreport、opannotate三个常用的oprofile命令。
     
    当系统出现cpu使用率异常偏高情况时,oprofile不但可以帮助我们分析出是哪一个进程异常使用cpu,还可以揪出进程中占用cpu的函数、 代码。在分析应用程序性能瓶颈、进行性能调优时,我们可以通过oprofile,得出程序代码的cpu使用情况,找到最消耗cpu的那部分代码进行分析与 调优,做到有的放矢。另外,进行程序性能调优时,我们不应仅仅关注自己编写的上层代码,也应考虑底层库函数,甚至内核对应用程序性能的影响。
     
    关于oprofile工具可用于分析的场景,本文仅介绍了cpu使用情况一种,我们还可以通过oprofile查看高速缓存的利用率、错误的转移预 测等信息,"opcontrol --list-events"命令显示了oprofile可检测到的所有事件,更多的oprofile使用方法,请参看oprofile manual。
















    http://blog.csdn.net/mr_raptor/article/details/44937619
    Android性能优化案例研究(上)
    2015-04-08 10:42 663人阅读 评论(0) 收藏 举报
    这 是Google的Android开发工程师Romain Guy刊登在个人Blog上的一篇文章。Romain Guy 作为Android图形渲染和系统优化的专家,是Android 4.1中的“黄油项目”开发者之一。这篇译文将分为上下两个部分,上部分将通过一个实际的例子来展示如何利用现有的工具来定位Android应用程序的性 能瓶颈,下部分将提供一些有效的方法来解决性能问题。希望能给读者和开发者带来启发和借 鉴。
    Falcon Pro
    最近我在我的Nexus4上安装了Falcon Pro(下 图),一个新款的推特(Twitter)客户端。我觉得这款应用真的很赞,但我也注意到一些使用时的瑕疵:似乎在划屏滚动主界面的时间轴时,帧率并不能很 稳定。于是我利用我每天工作中所使用的工具和方法对此稍加研究,很快发现了Falcon Pro不能达到其应有性能的一些原因。
    我这篇文章的主旨在于告诉你如何在一个应用中追踪和定位性能问题,甚至在没有它的源代码的情况下。你所要做的只是要获得最新的Android4.2SDK(最新的ADT工具可以帮你轻而易举的完成此事)。我强烈推荐你“能够”去下载这款有待研究的应用。不幸的是,Falcon Pro是一款付费应用,我因此只能提供一些文件的链接以便你能对照我的分析。
    说说关于性能优化
    Android4.1通过“黄油项目”将 焦点放在性能优化上,并且它也引入了一些性能分析的工具,比如systrace。Android4.2并没有提供像systrace那样显著的工具,但也 为你的工具集增加了一些很有用的功能。你将会在接下来的篇幅中发现到它们。(黄油计划 Project Butter,是Google在 Android 4.1 Jellybean版本开始启动的Android性能提升计划,其寓意为“像黄油一样顺滑”,该项目主要针对Android长期以来的饱受诟病的运行流畅 度问题,通过底层的优化,确保系统设备能达到60fps的帧刷新率,从而大大提高用户体验的流畅性——译者注)
    性能分析通常是一项复杂的任务,它需要大量的经验,需要对工具,硬件,API等方面的深入理解。这些经验让我在这只要几分钟就可以做出分析(你可以在我12月1日的推特(Twitter)上看到它的实况转播。)而你可能得试上几次后才能对此得心应手。
    证实我的疑问
    记 忆中关于性能优化最重要的一件事就是通过量化来验证你的工作。即使对我而言,Falcon Pro在的Nexus4上有着很明显的丢帧现象,我仍然得用实际的数据来证明。因此我将这款应用安装到Nexus7上,因为Nexus7比Nexus4性 能更强大,同时Nexus7在性能分析上也有着比Neux4更有意思的优势,关于这一点,我将在稍后加以讨论。
    这 款应用安装到Nexus7上也没有出现多大差别,我仍然能看到丢帧的现象甚至还略差。为了量化这个问题,我决定使用“Profile GPU rendering”(GPU渲染分析),一款Android4.1所引入的工具。你可以在“设置”应用的“开发者选项”中找到这个工具。
    如果开发者选项在你的Android4.2设备上不可见,你可以在“关于手机”或者“关于桌面选择”的界面底部,点击“版本号”七次。
    我这篇文章的主旨在于告诉你如何在一个应用中追踪和定位性能问题,甚至在没有它的源代码的情况下。你所要做的只是要获得最新的Android4.2SDK(最新的ADT工具可以帮你轻而易举的完成此事)。我强烈推荐你“能够”去下载这款有待研究的应用。不幸的是,Falcon Pro是一款付费应用,我因此只能提供一些文件的链接以便你能对照我的分析。
    当这个选项打开,系统将会记录画每个窗口绘画最后128帧所需要的时间。在使用这个工具前,你得先杀掉这个应用(Android未来的版本将会去掉这个要求)。
    方法:
    除非特别需要,在为这个分析做每一次测量时,需缓慢的滚动主界面的时间轴,让其滚动一段像素,使其能展现额外的条目。
    在重新启动这个应用并滚动时间轴主界面时,我在终端上运行了下面这个命令:
    $ adb shell dumpsys gfxinfo com.jv.falcon.pro
    在 产生的日志中,你会发现一段标记为“Profile”的毫秒量级的数据。这段数据包含了一个有三列数据的表,应用的每个window(窗口)都有一个这样 的表。为了使用这个数据,你可以简单的将这个表拷到你最喜欢的电子制表软件中,从而生成一个数据堆叠的列图。以下这个图就是我的测量结果。
    查看大图
    每一列给出了每一帧花在渲染上的时间估计:
    “Draw”是指Java层用在创建“display lists”(显示列表)上的时间。它表明运行例如View.onDraw(Canvas)需要多少时间。
    “Process”是指Android 2D渲染引擎用在执行“display lists”上的时间。你的UI层级(hierarchy)中的View数量越多,需要执行的绘画命令就越多。
    “Execute”是指将一帧图像交给合成器(compositor)的时间。这部分占用的时间通常比较少
    提醒:
    要以60fps的帧率进行平滑的渲染,每一帧所占用的时间需要少于16ms。
    关于“Execute”:
    如 果Excute花费很多时间,这就意味着你跑在了系统绘图流水线的前面。Android在运行状态时最多可以用3块缓存,如果此时你的应用还需要一块缓 存,那应用就会被阻塞直到三块中的一块缓存被释放。这种情况的发生一般有两个原因。第一个原因是你的应用在Dalvik(java虚拟机)端画的太快,而 在它的Display list在GPU端执行太慢。第二个原因是你的应用花费太多时间在前几帧的渲染上,一旦流水线满了,它就跟不上,直到动画的完成。这些是我们想在下一个版 本的Android改进的地方。
    以上这个图明显的证实了我的疑虑:这个应用在大部分时间运行良好,但某些时候会发生丢帧。
    进一步研究
    我们收集的数据显示这个应用有时绘图时间过长,但盖棺定论还为时过早。帧率也会被未调度的帧或者错过调度的帧的影响。例如,如果应用总是在16ms内完成一次绘图,但有时在帧与帧之间需要完成很长的任务,它就会因此错过一帧。
    Systrace是一个很简单的工具去检查Falcon Pro是否存在这个问题。这个工具是系统级的,额外开销很低。它的时间统计是合理准确的,能给你一个整个系统运行的概况,包括你的应用。
    开启Systrace,可以到开发者选项中选择“启动跟踪”,弹出一个对话框,会让你选择你想测量哪些方面的性能。我们只关注“Graphics”和“View”。
    注意:
    不要忘记关掉之前的GPU渲染分析选项。
    使用systrace时,可以打开终端,在Android SDK的tools/systrace目录下,运行systrace.py:
    $./systrace.py
    这个工具默认会记录5秒钟内发生的事件。我简单的向上和向下滚动时间轴,得到了一个用HTML文档展现的结果图。
    技巧:
    浏览systrace的文档图,可以使用键盘上的WASD键去移动和缩放。W键是将鼠标所处位置进行放大。
    systrace 的文档图显示了很多有意思的信息。例如,它可以显示一个进程是否被调度,是在哪个CPU上调度。如果你放大最后一行(叫做 10440:m.jv.falcon.pro),你可以看到这个应用正在做什么。如果你点击一个“performTraversals”块,你可以看到这 个应用花在输出一帧图像上面多长时间。
    大多数的performTraversals显示在16ms临界值以下,但有一些需要更多的时间,因此也证实了之前的猜测。(在935毫秒处放大可以看到这个块。)
    更 有意思的是,你可以看到这个应用有时错过一帧是因为它没有管理调度一个draw的操作。在270ms处放大,找到占用25ms的 “deliverInputEvent”块。这个块表明这个应用用了25ms来处理一个触摸事件。考虑到这个应用是使用ListView,很有可能是这个 适配器(adapter)出了问题,等会我们再来探讨这个。Systrace很有用的地方不仅在于证实这个应用花在绘图的时间上太长,也在于帮我们找到另 一个潜在的性能瓶颈。它很有用但也有局限。它只能提供高层级的数据,我们必须转向其他工具来理解此时究竟在运行什么。
    可视化重绘
    绘 图性能问题有很多根本的原因,但共同的一点是重绘(overdraw)。重绘发生在每次应用让系统在某个画好的地方上面再画别的。想一个最简单的应用:一 个白色背景的窗口(window),上面是一个按钮。当系统要画这个按钮时,它要画在已经画好的白色背景的上面。这就是重绘。重绘是必然的,但太多的重绘 就是个问题。设备的数据传输带宽是有限的,当重绘使得你的应用需要更多的带宽时,性能就会下降。不同的设备能够承担的重绘的代价是不同的。
    最佳的准则是重绘的最大次数不能超过两次。这就意味着你可以在屏幕画第一次,然后在这个屏幕上再画第二次,最后在其中某些像素上再画第三次。
    重绘的存在通常表明有这些问题:太多的View,复杂的层级,更长的inflation时间等等。
    Android提供了三个工具来帮助辨别和解决重绘问题:Hierachy Viewer,Tracer for OpenGL和Show GPU overdraw。前两个可以在ADT工具或者独立的monitor工具中找到,最后一个是在开发者选项的一部分。
    Show GPU Overdraw会在屏幕上画不同的颜色来辨别重绘发生在哪儿,重绘了几次。现在就开启它并且别忘了先杀掉你的应用(将来版本的Android会去掉这个要求)。
    在我们查看Falcon Pro之前,让我们先看看当打开Show GPU overdraw,“设置”应用是什么样子。
    如果你记得每种颜色所表示的含义,你就能很容易的知道结果是什么:
    没有颜色就表示没有重绘。每个像素只画了一次。在这个例子里,你可以看到背景是完全无色的。
    蓝色:表示重绘了一次。每个像素只画了两次。大块的蓝色是可以接受的。(如果整个window是蓝色的,你就可以使用一个图层(layer)。)
    绿色:表示重绘了两次。每个像素画了三次。中等尺寸的绿色方块是可以接受的,但你最好尝试做出优化。
    红色:表示重绘了三次。这个像素被画了四次。很小尺寸的红色方块是可以接受的。
    黑色:表示重绘了四次及以上。这个像素被画了五次及以上。这个是错的,需要解决。
    基于这些信息,你可以看到“设置”应用表现地很好,不需要额外的改进。只有在切换时有一点点红块,但不需要我们再做什么工作了。
    透明像素:
    再 仔细看看之前的截图。每一个图标都画成了蓝色。你可以看出位图(bitmap)中透明像素是解决了重绘的问题。透明像素必须由GPU处理,开销是昂贵的。 Android为了避免在图层(layer)和9-patches上绘画透明像素,做了优化,所以你只要考虑位图就行了。
    重绘和GPU:
    有 两种移动GPU架构。第一个使用延迟渲染,比如ImaginationTech的SGX系列。这种架构允许GPU在某些特定的场景下检查和处理重绘。(如 果你混合透明和不透明的像素,它有可能不起作用。) 第二钟架构使用及时渲染,它被NVIDIA的TegraGPU采用。这种架构不能为你优化重绘,这就是为什么我喜欢在Nexus7上测试(Nexus7使 用Tegra3)。这两种架构各有优劣。但这已经超出了本文的主题。仅仅只要知道两者都可以工作的很好就行了。现在就让我们看一下Falcon Pro…
    截图上有大量的红色!最感兴趣的却是列表的背景是绿色的。这就显示在应用程序开始描绘它的内容前已经发生了两次重绘。我们这里所看到问题很有可能是和使用了许多全屏图片背景相关。但要解决这个问题通常是很繁琐的。
    译者注:下篇作者将会带来如何解决性能问题的方法和思路,敬请期待!
    英文原文:Android Performance Case Study  编译:ImportNew - 孙立
    译文地址:  http://www.importnew.com/3784.html
    【如需转载,请在正文中标注并保留原文链接、译文链接和译者等信息,谢谢合作!】
    Android性能优化案例研究(下)
    2015-04-08 10:42 586人阅读 评论(0) 收藏 举报
    译者前言:在Android性能优化案例研究(上)中,作者Romain Guy将Falcon Pro这款应用作为例子,通过Android现有的工具追踪和分析了其隐藏的性能问题(重绘)。下篇作者将会带来如何解决此类问题的方法和思路。
    去掉冗余的图层
    为 了去掉重绘我们必须首先理解它从哪里产生的。这就轮到Hierarchy Viewer和Tracer for OpenGL大显身手的时候了。Hierarchy Viewer是ADT工具(或者monitor)的一部分,可以被用作对视图层级进行快速解读。在处理布局问题时特别有用,对于性能问题也很适用。
    重要:
    Hierarchy Viewer默认只能在非加密设备使用,例如工程机,工程平板或者模拟器。为了能够在任何手机上使用Hierarchy Viewer,你得在你的应用中添加ViewServer,这是一个开源库。
    在 ADT(或者monitor)中打开Hierarchy Viewer的全景图,选择window标签。这个界面就会粗体高亮的显示当前设备运行的窗口,通常就是你想要研究的那个应用。选中它再点击工具栏的 Load按钮(它更像蓝色方块组成的树)。加载这棵树需要一段时间,所以请耐心等待。当这棵树加载完成你就可以看到如下图所示的画面。
    现 在视图的层级已经加载到工具里,我们也可以将其转换为PhotoShop文档。只要点击工具栏的第二个按钮,工具提示说:“Capture the window layers [..]”。Adobe Photoshop本身不是必须的,因为生成的文档可以被其他工具兼容,例如Pixelmator, The GIMP等等。你们可以下载我所生成的PSD文件。
    PhotoShop文档可以显示每个视图的每个图层。一个图层可以标记为可见或者不可见,这是取决于View.getVisibility()返回的结果。每一个图层命名在一个视图的后面,如果视图的android:id存在则使用android:id,或者使用它的类名。我曾经开始添加对于组(group)的支持用于视图树的重建…我其实应该早点把这个功能做完。
     
    通过检查每个图层的列表,我们可以快速的辨别至少一种重绘的源头:多个全屏的背景。第一个就是第一个图层,叫做DecorView。这个view是由Android框架生成的,包含了皮肤主题指定的背景。这个默认的背景在应用中是不可见的,因此它可以被安全的去掉。
    从DecorView向上滚动,你可以看到一个LinearLayout,它包含另一个全屏的背景。它和DecorView的背景是一回事,所以它也是不需要的。唯一可见且肯定存在的背景属于一个名叫id/tweet_list_container的view
    去掉桌面背景:
    定 义在你的主题皮肤里的背景通常是当系统启动你的应用时用来创建预览窗口的。千万不要设置它为空(null),除非你的应用是透明的。相反,设置它为你想要 的颜色或者图片,或者在onCreate()里调用getWindow().setBackgroundDrawable(null)来去掉它
    进一步去掉重绘
    用 Photoshop的文档图来理解应用是怎么创建的是很有用的。但是用来去掉小范围的重绘有点难度。现在我们就必须转向Tracer for OpenGL。同样在ADT(或者monitor)中打开它的视图,点击工具栏的箭头图标,输入你应用的包名和你主要的Activity的名字,然后选择 一个目的文件,点击Trace。
    一句建议:
    OpenGL traces抓取的数据量很大。为了让数据量较小,同时也利于更快速抓取。请去掉“all the Data Collection Options”选项。
     
    Activity名字:
    在应用启动时可以通过logcat获得包名和Activity名字。这就是为什么我可以知道在Tracer for OpenGL输入这些名字。
    当启动并运行这个应用时,打开前两个选项:
    Collect Framebuffer contents on eglSwapBuffers()
    Collect Framebuffer contents on glDraw*()
    第一个选项可以方便的快速找到你感兴趣的帧,第二个选项可以让我们看到每一帧是如何通过一步步绘图命令建立起来的。第二个选项就是解决重绘的关键。
     
    随着这两个选项的开启,我开始滚动屏幕。抓取每一帧需要很长时间(也许要30秒),所以我推荐你可以先简单的下载我抓取的trace文件。你可以通过Tracer for OpenGL工具栏的第一个按钮打开这个文件。
    trace 文件一旦加载完成,你就可以看到每一帧发生给GPU的每一个GL命令。如果你下载了我的文件,你跳到第21帧。当一帧被选中后,你就可以看到Frame Summary选项卡中呈现的模样。此外,你还可以点击高亮为蓝色的drawing命令,这样你就可以在Details选项卡中获得当前帧的状态细节。
     
    相继的点击前三个绘图命令,你就可以看到在PhotoShop里面已经得到鉴定的问题:全屏的背景被画了三次。
    通过深入研究这个trace文件,我们可以找到更多优化的地方。当去画一个消息内容条目时,ImageView被用来画头像。这个ImageView先画了一个背景然后再画头像:
     
    如果你看得再仔细点你就会注意到背景只是用来作为图片的边框。这就意味着在位于头像的黑色方块产生了重绘。那块9格图(9-patch)完全被头像覆盖了。
    解决这个问题的有一个很简单的方法就是让这块9格图设为透明。Android的2D渲染引擎已经在9格图上做了优化。这个简单的方法就可以去掉重绘。
    有趣的是,同样的问题也发生在内嵌的媒体内容上。头像很小所以它们的重绘不是个大问题。但内嵌的媒体内容却可以占据屏幕的大片区域,这个问题就严重了。可以用同样的方法去解决它。
     
     
    未来的优化:
    我希望Android的2D渲染流水线能够自动的检测和修正重绘。我们已经有了一些想法但还不能做出承诺。
    扁平化View的层级
    现在重绘已经基本考虑过了。让我们重新回到Hierarchy Viewer吧。通过研究这棵UI树,我们可以尽量去鉴别哪些View不是必须的。去掉View,特别是去掉ViewGroups,不仅可以提供帧率,也可以节省内存,加快启动时间等等。
    看一眼Falcon Pro的View的层级树就可以发现一些ViewGroups是在同一个子节点上。ViewGroups通常不是必须的,也很容易去掉。下面这个截图显示至少有两个节点是可以去掉的。
     
    也 有一些冗余的View可以去掉。比如每一个消息条目都包一个叫做id/listElementBottom的RelativeLayout。这个布局包含 了作者的名字,推特消息,已经发布了多长时间和一个图标。名字和消息用了两个不同的TextView,其实只需要一个TextView用不同的风格来显示 就行了。时间和图标用了一个TextView和一个ImageView,其实两者可以用一个TextView,然后用可视化绑定到TextView上。
    左边滑动的界面用了若干不同的LinearLayout+TextView+ImageView来显示标签和图标。他们都可以通过一个TextView来代替。
    如何扁平化你的界面:
    我在2009年的Google I/O大会上做了一篇题为优化你的UI的演讲,里面介绍了这其中的技术细节。
    关于输入事件?
    还记得我们在看systrace时找到一段处理很慢的触摸事件?现在可以看看这个问题。理解这个问题最佳的工具就是traceview。
    traceview 是Dalvik性能解析工具,它可以测量一个应用在每个方法调用上花费的事件。在ADT或者monitor里打开DDMS,在设备选项卡里选择你应用所在 的进程,然后点击“start method profiling”按钮(三个箭头和一个红色的圈),你就可以使用traceview。
    当启动了traceview后,我滚动应用的界面,然后点击那个按钮结束跟踪。你也可以下载我的跟踪文件。结果如下图所示。
     
    点击条目21:ViewRootImpl.draw(),高亮它所花的时间。表的最后一列表明这个方法的和在它的子类里平均的调用时间。如果你仔细看看高亮的时间轴,你可以注意到帧与帧之间的差距。
    用 一个简单的方法来查看差距里面到底发生了什么,可以放大他们开始的阶段,然后点击你找到的红色的块。你可以跟着调用链来找到你能认出的方法。在我的例子 里,我跟踪了一个大概占用了0.5毫秒的Pattern.compileImpl方法,一直到跟DBListAdapter.bindView。
    很 显然这个应用将同一个正则表达式编译了好几次,每一次滚动屏幕都伴随着一个条目的绑定。TraceView显示bindView平均占用了38毫秒,而其 中56%的时间花在了解析HTML文本上。似乎可以将这个步骤放在后台运行而不去阻塞UI线程,而正则表达式不应该每次都需要重新编译。
    现在轮到你了!
    我保留了最后一个跟踪文件作为测验。这个应用有两个滑动的菜单,可以左右滑动时间轴。Show GPU overdraws高亮了滑动时大量的绘图。我用Tracer for OpenGL抓取了滑动时的若干帧。下载我的trace文件,然后看看你是否能找到重绘的原因(去看第34号帧)。
    提示:
    应 用应该调用View.setLayerType()来使用硬件图层(hardware layer)来简化绘图。大量的背景可以使用9格图来做优化。裁剪也很有效。最后,也许可以将一个颜色过滤器(colofilter)设置在画笔 (paint)上,然后传给setLayerType(),这样可以帮助去掉最后一个绘图命令。
    我向你们展示了大量可以优化你们应用的工具。我其实还可以花费大量的时间来描述用这些工具处理这些问题的技术方法,但这样文章就会变成长篇大论。你们可以去参考Android开发者的的官方文档和所有Google I/O上Android的演讲(ppt和视频都是免费可取得)。
    英文原文:Android Performance Case Study  编译:ImportNew - 孙立
    译文地址:  http://www.importnew.com/4065.html
    【如需转载,请在正文中标注并保留原文链接、译文链接和译者等信息,谢谢合作!】


















    http://blog.csdn.net/mr_raptor/article/details/44937779
    Android性能优化
     随 着技术的发展,智能手机硬件配置越来越高,可是它和现在的PC相比,其运算能力,续航能力,存储空间等都还是受到很大的限制,同时用户对手机的体验要求远 远高于PC的桌面应用程序。以上理由,足以需要开发人员更加专心去实现和优化你的代码了。选择合适的算法和数据结构永远是开发人员最先应该考虑的事情。同 时,我们应该时刻牢记,写出高效代码的两条基本的原则:(1)不要做不必要的事;(2)不要分配不必要的内存。
    我从去年开始接触Android开发,以下结合自己的一点项目经验,同时参考了Google的优化文档和网上的诸多技术大牛给出的意见,整理出这份文档。
     
    1.      内存优化
    Android系统对每个软件所能使用的RAM空间进行了限制(如:Nexus one 对每个软件的内存限制是24M),同时Java语言本身比较消耗内存,dalvik虚拟机也要占用一定的内存空间,所以合理使用内存,彰显出一个程序员的素质和技能。

    1)       了解JIT
    即 时编译(Just-in-time Compilation,JIT),又称动态转译(Dynamic Translation),是一种通过在运行时将字节码翻译为机器码,从而改善字节码编译语言性能的技术。即时编译前期的两个运行时理论是字节码编译和动 态编译。Android原来Dalvik虚拟机是作为一种解释器实现,新版(Android2.2+)将换成JIT编译器实现。性能测试显示,在多项测试 中新版本比旧版本提升了大约6倍。
    详细请参考http://hi.baidu.com/cool_parkour/blog/item/2802b01586e22cd8a6ef3f6b.html
     
    2)       避免创建不必要的对象
    就 像世界上没有免费的午餐,世界上也没有免费的对象。虽然gc为每个线程都建立了临时对象池,可以使创建对象的代价变得小一些,但是分配内存永远都比不分配 内存的代价大。如果你在用户界面循环中分配对象内存,就会引发周期性的垃圾回收,用户就会觉得界面像打嗝一样一顿一顿的。所以,除非必要,应尽量避免尽力 对象的实例。下面的例子将帮助你理解这条原则:
    当 你从用户输入的数据中截取一段字符串时,尽量使用substring函数取得原始数据的一个子串,而不是为子串另外建立一份拷贝。这样你就有一个新的 String对象,它与原始数据共享一个char数组。 如果你有一个函数返回一个String对象,而你确切的知道这个字符串会被附加到一个StringBuffer,那么,请改变这个函数的参数和实现方式, 直接把结果附加到StringBuffer中,而不要再建立一个短命的临时对象。
    一个更极端的例子是,把多维数组分成多个一维数组:
    int 数组比Integer数组好,这也概括了一个基本事实,两个平行的int数组比 (int,int)对象数组性能要好很多。同理,这试用于所有基本类型的组合。如果你想用一种容器存储(Foo,Bar)元组,尝试使用两个单独的 Foo[]数组和Bar[]数组,一定比(Foo,Bar)数组效率更高。(也有例外的情况,就是当你建立一个API,让别人调用它的时候。这时候你要注 重对API接口的设计而牺牲一点儿速度。当然在API的内部,你仍要尽可能的提高代码的效率)
    总体来说,就是避免创建短命的临时对象。减少对象的创建就能减少垃圾收集,进而减少对用户体验的影响。
     
    3)       静态方法代替虚拟方法
    如果不需要访问某对象的字段,将方法设置为静态,调用会加速15%到20%。这也是一种好的做法,因为你可以从方法声明中看出调用该方法不需要更新此对象的状态。
     
    4)       避免内部Getters/Setters
    在源生语言像C++中,通常做法是用Getters(i=getCount())代替直接字段访问(i=mCount)。这是C++中一个好的习惯,因为编译器会内联这些访问,并且如果需要约束或者调试这些域的访问,你可以在任何时间添加代码。
    而在Android中,这不是一个好的做法。虚方法调用的代价比直接字段访问高昂许多。通常根据面向对象语言的实践,在公共接口中使用Getters和Setters是有道理的,但在一个字段经常被访问的类中宜采用直接访问。
    无JIT时,直接字段访问大约比调用getter访问快3倍。有JIT时(直接访问字段开销等同于局部变量访问),要快7倍。
     
    5)       将成员缓存到本地
    访问成员变量比访问本地变量慢得多,下面一段代码:
    [java] view plaincopy
    for(int i =0; i <this.mCount; i++)  { 
    dumpItem(this.mItems); 


     
        
    最好改成这样:
    [java] view plaincopy
    int count = this.mCount; 
    Item[] items = this.mItems; 
    for(int i =0; i < count; i++)  { 
           dumpItems(items); 


     
        
    另一个相似的原则是:永远不要在for的第二个条件中调用任何方法。如下面方法所示,在每次循环的时候都会调用getCount()方法,这样做比你在一个int先把结果保存起来开销大很多。
    [java] view plaincopy
    for(int i =0; i < this.getCount(); i++) { 
    dumpItems(this.getItem(i)); 

     
        
    同样如果你要多次访问一个变量,也最好先为它建立一个本地变量,例如:
    [java] view plaincopy
    protected void drawHorizontalScrollBar(Canvas canvas, int width, int height) { 
    if(isHorizontalScrollBarEnabled()) { 
    intsize = mScrollBar.getSize(false); 
    if(size <=0) { 
           size = mScrollBarSize; 

    mScrollBar.setBounds(0, height - size, width, height); 
    mScrollBar.setParams(computeHorizontalScrollRange(), computeHorizontalScrollOffset(), computeHorizontalScrollExtent(),false); 
    mScrollBar.draw(canvas); 



    这里有4次访问成员变量mScrollBar,如果将它缓存到本地,4次成员变量访问就会变成4次效率更高的栈变量访问。
    另外就是方法的参数与本地变量的效率相同。
     
    1)       对常量使用static final修饰符
    让我们来看看这两段在类前面的声明:
    [java] view plaincopy
    static int intVal = 42; 
    static String strVal = "Hello, world!"; 

    必 以其会生成一个叫做clinit的初始化类的方法,当类第一次被使用的时候这个方法会被执行。方法会将42赋给intVal,然后把一个指向类中常量表的 引用赋给strVal。当以后要用到这些值的时候,会在成员变量表中查找到他们。 下面我们做些改进,使用“final”关键字:
    [java] view plaincopy
    static final int intVal = 42; 
    static final String strVal = "Hello, world!"; 

    现在,类不再需要clinit方法,因为在成员变量初始化的时候,会将常量直接保存到类文件中。用到intVal的代码被直接替换成42,而使用strVal的会指向一个字符串常量,而不是使用成员变量。
    将一个方法或类声明为final不会带来性能的提升,但是会帮助编译器优化代码。举例说,如果编译器知道一个getter方法不会被重载,那么编译器会对其采用内联调用。
    你也可以将本地变量声明为final,同样,这也不会带来性能的提升。使用“final”只能使本地变量看起来更清晰些(但是也有些时候这是必须的,比如在使用匿名内部类的时候)。
     
    2)       使用改进的For循环语法
    改 进for循环(有时被称为for-each循环)能够用于实现了iterable接口的集合类及数组中。在集合类中,迭代器让接口调用hasNext() 和next()方法。在ArrayList中,手写的计数循环迭代要快3倍(无论有没有JIT),但其他集合类中,改进的for循环语法和迭代器具有相同 的效率。下面展示集中访问数组的方法:
    [java] view plaincopy
    static class Foo { 
            int mSplat; 
        } 
        Foo[] mArray = ... 
      
        public void zero() { 
            int sum = 0; 
            for (int i = 0; i < mArray.length; ++i) { 
                sum += mArray[i].mSplat; 
            } 
        } 
      
        public void one() { 
            int sum = 0; 
            Foo[] localArray = mArray; 
            int len = localArray.length; 
      
            for (int i = 0; i < len; ++i) { 
                sum += localArray[i].mSplat; 
            } 
        } 
      
        public void two() { 
            int sum = 0; 
            for (Foo a : mArray) { 
                sum += a.mSplat; 
            } 



    在zero()中,每次循环都会访问两次静态成员变量,取得一次数组的长度。
    在one()中,将所有成员变量存储到本地变量。
    two() 使用了在java1.5中引入的foreach语法。编译器会将对数组的引用和数组的长度保存到本地变量中,这对访问数组元素非常好。但是编译器还会在每 次循环中产生一个额外的对本地变量的存储操作(对变量a的存取)这样会比one()多出4个字节,速度要稍微慢一些。
     
    3)       避免使用浮点数
    通常的经验是,在Android设备中,浮点数会比整型慢两倍,在缺少FPU和JIT的G1上对比有FPU和JIT的Nexus One中确实如此(两种设备间算术运算的绝对速度差大约是10倍)
    从速度方面说,在现代硬件上,float和double之间没有任何不同。更广泛的讲,double大2倍。在台式机上,由于不存在空间问题,double的优先级高于float。
    但即使是整型,有的芯片拥有硬件乘法,却缺少除法。这种情况下,整型除法和求模运算是通过软件实现的,就像当你设计Hash表,或是做大量的算术那样,例如a/2可以换成a*0.5。
     
    4)       了解并使用类库
    选择Library中的代码而非自己重写,除了通常的那些原因外,考虑到系统空闲时会用汇编代码调用来替代library方法,这可能比JIT中生成的等价的最好的Java代码还要好。
               i.    当你在处理字串的时候,不要吝惜使用String.indexOf(),String.lastIndexOf()等特殊实现的方法。这些方法都是使用C/C++实现的,比起Java循环快10到100倍。

               ii.    System.arraycopy方法在有JIT的Nexus One上,自行编码的循环快9倍。

               iii.    android.text.format包下的Formatter类,提供了IP地址转换、文件大小转换等方法;DateFormat类,提供了各种时间转换,都是非常高效的方法。
    详细请参考http://developer.android.com/reference/android/text/format/package-summary.html

               iv.    TextUtils类
    对于字符串处理Android为我们提供了一个简单实用的TextUtils类,如果处理比较简单的内容不用去思考正则表达式不妨试试这个在android.text.TextUtils的类,详细请参考http://developer.android.com/reference/android/text/TextUtils.html

                v.    高性能MemoryFile类。
    很多人抱怨Android处理底层I/O性能不是很理想,如果不想使用NDK则可以通过MemoryFile类实现高性能的文件读写操作。
    MemoryFile 适用于哪些地方呢?对于I/O需要频繁操作的,主要是和外部存储相关的I/O操作,MemoryFile通过将 NAND或SD卡上的文件,分段映射到内存中进行修改处理,这样就用高速的RAM代替了ROM或SD卡,性能自然提高不少,对于Android手机而言同 时还减少了电量消耗。该类实现的功能不是很多,直接从Object上继承,通过JNI的方式直接在C底层执行。
    详细请参考http://developer.android.com/reference/android/os/MemoryFile.html
    在此,只简单列举几个常用的类和方法,更多的是要靠平时的积累和发现。多阅读Google给的帮助文档时很有益的。
     
    5)       合理利用本地方法
    本 地方法并不是一定比Java高效。最起码,Java和native之间过渡的关联是有消耗的,而JIT并不能对此进行优化。当你分配本地资源时(本地堆上 的内存,文件说明符等),往往很难实时的回收这些资源。同时你也需要在各种结构中编译你的代码(而非依赖JIT)。甚至可能需要针对相同的架构来编译出不 同的版本:针对ARM处理器的GI编译的本地代码,并不能充分利用Nexus One上的ARM,而针对Nexus One上ARM编译的本地代码不能在G1的ARM上运行。
    当你想部署程序到存在本地代码库的Android平台上时,本地代码才显得尤为有用,而并非为了Java应用程序的提速。
     
    6)       复杂算法尽量用C完成
    复杂算法尽量用C或者C++完成,然后用JNI调用。但是如果是算法比较单间,不必这么麻烦,毕竟JNI调用也会花一定的时间。请权衡。
     
    7)       减少不必要的全局变量
    尽量避免static成员变量引用资源耗费过多的实例,比如Context。Android提供了很健全的消息传递机制(Intent)和任务模型(Handler),可以通过传递或事件的方式,防止一些不必要的全局变量。
     
    8)       不要过多指望gc
    Java的gc使用的是一个有向图,判断一个对象是否有效看的是其他的对象能到达这个对象的顶点,有向图的相对于链表、二叉树来说开销是可想而知。所以不要过多指望gc。将不用的对象可以把它指向NULL,并注意代码质量。同时,显示让系统gc回收,例如图片处理时,

    [java] view plaincopy
    if(bitmap.isRecycled()==false) { //如果没有回收 
         bitmap.recycle(); 


     
        
     
    9)       了解Java四种引用方式
    JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
                         i.    强引用(StrongReference)
    强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

                         ii.    软引用(SoftReference)
    如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

                       iii.    弱引用(WeakReference)
    在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

                        iv.    虚引用(PhantomReference)
    顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
    了解并熟练掌握这4中引用方式,选择合适的对象应用方式,对内存的回收是很有帮助的。
    详细请参考http://blog.csdn.net/feng88724/article/details/6590064
                
    10)     使用实体类比接口好
    假设你有一个HashMap对象,你可以将它声明为HashMap或者Map:
    [java] view plaincopy
    Map map1 = new HashMap(); 
    HashMap map2 = new HashMap(); 

    哪个更好呢?
    按 照传统的观点Map会更好些,因为这样你可以改变他的具体实现类,只要这个类继承自Map接口。传统的观点对于传统的程序是正确的,但是它并不适合嵌入式 系统。调用一个接口的引用会比调用实体类的引用多花费一倍的时间。如果HashMap完全适合你的程序,那么使用Map就没有什么价值。如果有些地方你不 能确定,先避免使用 Map,剩下的交给IDE提供的重构功能好了。(当然公共API是一个例外:一个好的API常常会牺牲一些性能)
     
    11)     避免使用枚举
    枚举变量非常方便,但不幸的是它会牺牲执行的速度和并大幅增加文件体积。例如:
    [java] view plaincopy
    public class Foo { 
           public enum Shrubbery { GROUND, CRAWLING, HANGING } 


    会 产生一个900字节的.class文件(Foo$Shubbery.class)。在它被首次调用时,这个类会调用初始化方法来准备每个枚举变量。每个枚 举项都会被声明成一个静态变量,并被赋值。然后将这些静态变量放在一个名为”$VALUES”的静态数组变量中。而这么一大堆代码,仅仅是为了使用三个整 数。
    这样:Shrubbery shrub =Shrubbery.GROUND;会引起一个对静态变量的引用,如果这个静态变量是final int,那么编译器会直接内联这个常数。
    一方面说,使用枚举变量可以让你的API更出色,并能提供编译时的检查。所以在通常的时候你毫无疑问应该为公共API选择枚举变量。但是当性能方面有所限制的时候,你就应该避免这种做法了。
    有些情况下,使用ordinal()方法获取枚举变量的整数值会更好一些,举例来说:
    [java] view plaincopy
    for(int n =0; n < list.size(); n++) { 
           if(list.items[n].e == MyEnum.VAL_X) { 
                  // do something 
           } else if(list.items[n].e == MyEnum.VAL_Y) { 
                  // do something 
           } 


    替换为:
    [java] view plaincopy
    int valX = MyEnum.VAL_X.ordinal(); 
    int valY = MyEnum.VAL_Y.ordinal(); 
    int count = list.size(); 
    MyItem items = list.items(); 
    for(int n =0; n < count; n++) { 
           intvalItem = items[n].e.ordinal(); 
           if(valItem == valX) { 
                  // do something 
           } else if(valItem == valY) { 
                  // do something 
           } 


    会使性能得到一些改善,但这并不是最终的解决之道。
     
    12)     在私有内部内中,考虑用包访问权限替代私有访问权限
    [java] view plaincopy
    public class Foo { 
               public class Inner { 
                    public void stuff() { 
                           Foo.this.doStuff(Foo.this.mValue); 
                    } 
               } 
               private int mValue; 
               public void run() { 
                    Inner in = new Inner(); 
                    mValue = 27; 
                    in.stuff(); 
               } 
               private void doStuff(int value) { 
                     System.out.println("value:"+value); 
               } 


    需要注意的关键是:我们定义的一个私有内部类(Foo$Inner),直接访问外部类中的一个私有方法和私有变量。这是合法的,代码也会打印出预期的“Value is 27”。
    但问题是,虚拟机认为从Foo$Inner中直接访问Foo的私有成员是非法的,因为他们是两个不同的类,尽管Java语言允许内部类访问外部类的私有成员,但是通过编译器生成几个综合方法来桥接这些间隙的。
    [java] view plaincopy
    /*package*/static int Foo.access$100(Foo foo) { 
    return foo.mValue; 

    /*package*/static void Foo.access%200(Foo foo,int value) { 
           foo.duStuff(value); 


    内部类会在外部类中任何需要访问mValue字段或调用doStuff方法的地方调用这些静态方法。这意味着这些代码将直接存取成员变量表现为通过存取器方法访问。之前提到过存取器访问如何比直接访问慢,这例子说明,某些语言约会定导致不可见的性能问题。
    如果你在高性能的Hotspot中使用这些代码,可以通过声明被内部类访问的字段和成员为包访问权限,而非私有。但这也意味着这些字段会被其他处于同一个包中的类访问,因此在公共API中不宜采用。
     
    13)     将与内部类一同使用的变量声明在包范围内
    请看下面的类定义:
    [java] view plaincopy
    public class Foo { 
           private class Inner { 
               void stuff() { 
                   Foo.this.doStuff(Foo.this.mValue); 
               } 
           } 
      
           private int mValue; 
           public void run() { 
               Inner in = new Inner(); 
               mValue = 27; 
               in.stuff(); 
           } 
      
           private void doStuff(int value) { 
               System.out.println("Value is " + value); 
           } 


    这其中的关键是,我们定义了一个内部类(Foo$Inner),它需要访问外部类的私有域变量和函数。这是合法的,并且会打印出我们希望的结果Value is 27。
    问题是在技术上来讲(在幕后)Foo$Inner是一个完全独立的类,它要直接访问Foo的私有成员是非法的。要跨越这个鸿沟,编译器需要生成一组方法:
    [java] view plaincopy
    /*package*/ static int Foo.access$100(Foo foo) { 
        return foo.mValue; 

    /*package*/ static void Foo.access$200(Foo foo, int value) { 
        foo.doStuff(value); 


     
        
    内 部类在每次访问mValueg和gdoStuffg方法时,都会调用这些静态方法。就是说,上面的代码说明了一个问题,你是在通过接口方法访问这些成员变 量和函数而不是直接调用它们。在前面我们已经说过,使用接口方法(getter、setter)比直接访问速度要慢。所以这个例子就是在特定语法下面产生 的一个“隐性的”性能障碍。
    通 过将内部类访问的变量和函数声明由私有范围改为包范围,我们可以避免这个问题。这样做可以让代码运行更快,并且避免产生额外的静态方法。(遗憾的是,这些 域和方法可以被同一个包内的其他类直接访问,这与经典的OO原则相违背。因此当你设计公共API的时候应该谨慎使用这条优化原则)。
     
    14)     缓存
    适量使用缓存,不要过量使用,因为内存有限,能保存路径地址的就不要存放图片数据,不经常使用的尽量不要缓存,不用时就清空。
     
    15)     关闭资源对象
    对SQLiteOpenHelper,SQLiteDatabase,Cursor,文件,I/O操作等都应该记得显示关闭。
                
    2.      视图优化
    1)       View优化
                           i.    减少不必要的View以及View的嵌套层次。
    比如实现一个listview中常用的layout,可以使用RelativeLayout减少嵌套,要知道每个View的对象会耗费1~2k内存,嵌套层次过多会引起频繁的gc,造成ANR。
                         ii.    通过HierarchyViewer查看布局结构
    利用HierarchyViewer来查看View的结构:~/tools/hierarchyviewer,能很清楚地看到RelativeLayout下面的扁平结构,这样能加快dom的渲染速度。
    详细请参考http://developer.android.com/guide/developing/tools/hierarchy-viewer.html
                       iii.    通过Layoutopt优化布局
    通过Android sdk中tools目录下的layoutopt 命令查看你的布局是否需要优化。详细请参考http://apps.hi.baidu.com/share/detail/34247942
                
    2)       多线程解决复杂计算
    占用CPU较多的数据操作尽可能放在一个单独的线程中进行,通过handler等方式把执行的结果交于UI线程显示。特别是针对的网络访问,数据库查询,和复杂的算法。目前Android提供了AsyncTask,Hanlder、Message和Thread的组合。
    对于多线程的处理,如果并发的线程很多,同时有频繁的创建和释放,可以通过concurrent类的线程池解决线程创建的效率瓶颈。
    另外值得注意的是,应用开发中自定义View的时候,交互部分,千万不要写成线程不断刷新界面显示,而是根据TouchListener事件主动触发界面的更新。
     
    3)       布局用Java完成比XML快
    一 般情况下对于Android程序布局往往使用XML文件来编写,这样可以提高开发效率,但是考虑到代码的安全性以及执行效率,可以通过Java代码执行创 建,虽然Android编译过的XML是二进制的,但是加载XML解析器的效率对于资源占用还是比较大的,Java处理效率比XML快得多,但是对于一个 复杂界面的编写,可能需要一些套嵌考虑,如果你思维灵活的话,使用Java代码来布局你的Android应用程序是一个更好的方法。
     
    4)       对大型图片进行缩放
    图 片读取是OOM(Out of Memory)的常客,当在Android手机上直接读取4M的图片时,死神一般都会降临,所以导致往往自己手机拍摄的照片都不能直接读取。对大型图片进 行缩放有,处理图片时我们经常会用到BitmapFactory类,android系统中读取位图Bitmap时分给虚拟机中图片的堆栈大小只有8M。用 BitmapFactory解码一张图片时,有时也会遇到该错误。这往往是由于图片过大造成的。这时我们需要分配更少的内存空间来存储。 BitmapFactory.Options.inSampleSize设置恰当的inSampleSize可以使BitmapFactory分配更少的 空间以消除该错误。Android提供了一种动态计算的,如下:
     
    读取图片之前先查看其大小:
    [java] view plaincopy
    BitmapFactory.Options opts = new BitmapFactory.Options(); 
    opts.inJustDecodeBounds = true; 
    Bitmap bitmap = BitmapFactory.decodeFile(imageFile, opts); 

    使用得到的图片原始宽高计算适合自己的smaplesize

    [java] view plaincopy
    BitmapFactory.Options opts = new BitmapFactory.Options(); 
     opts.inSampleSize = 4 ;// 4就代表容量变为以前容量的1/4 
     Bitmap bitmap = BitmapFactory.decodeFile(imageFile, opts); 

     
        
                       
    对于过时的Bitmap对象一定要及时recycle,并且把此对象赋值为null。

    [java] view plaincopy
    bitmap.recycle(); 
    bitmap = null; 

     
        
                
    5)       合理使用ViewStub
    ViewStub 是一个隐藏的,不占用内存空间的视图对象,它可以在运行时延迟加载布局资源文件。当ViewStub可见,或者调用 inflate()函数时,才会加载这个布局资源文件。 该ViewStub在加载视图时在父容器中替换它本身。因此,ViewStub会一直存在于视图中,直到调用setVisibility(int) 或者inflate()为止。ViewStub的布局参数会随着加载的视图数一同被添加到ViewStub父容器。同样,你也可以通过使用 inflatedId属性来定义或重命名要加载的视图对象的Id值。所以我们可以使用ViewStub延迟加载某些比较复杂的layout,动态加载 View,采用ViewStub 避免一些不经常的视图长期握住引用。
    详细请参考http://developer.android.com/reference/android/view/ViewStub.html
     
    6)       针对ListView的性能优化
                        i.    复用convertView。

                        ii.    在getItemView中,判断convertView是否为空,如果不为空,可复用。如果couvertview中的view需要添加 listerner,代码一定要在if(convertView==null){}之外。

                       iii.    异步加载图片,item中如果包含有web image,那么最好异步加载。

                        iv.    快速滑动时不显示图片
    当 快速滑动列表时(SCROLL_STATE_FLING),item中的图片或获取需要消耗资源的view,可以不显示出来;而处于其他两种状态 (SCROLL_STATE_IDLE 和SCROLL_STATE_TOUCH_SCROLL),则将那些view显示出来。

                          v.    item尽可能的减少使用的控件和布局的层次;背景色与cacheColorHint设置相同颜色;ListView中item的布局至关重要,必须尽可 能的减少使用的控件,布局。RelativeLayout是绝对的利器,通过它可以减少布局的层次。同时要尽可能的复用控件,这样可以减少 ListView的内存使用,减少滑动时gc次数。ListView的背景色与cacheColorHint设置相同颜色,可以提高滑动时的渲染性能。

                          vi.    getView优化
    ListView中getView是性能是关键,这里要尽可能的优化。getView方法中要重用view;getView方法中不能做复杂的逻辑计算,特别是数据库和网络访问操作,否则会严重影响滑动时的性能。优化如下所示:
    [java] view plaincopy
    @Override 
    public View getView(int position, View convertView, ViewGroup parent) { 
           Log.d("MyAdapter", "Position:" + position + "---" + String.valueOf(System.currentTimeMillis())); 
           final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 
           View v = inflater.inflate(R.layout.list_item_icon_text, null); 
           ((ImageView) v.findViewById(R.id.icon)).setImageResource(R.drawable.icon); 
           ((TextView) v.findViewById(R.id.text)).setText(mData[position]); 
           return v; 


     
        
    建议改为:
    [java] view plaincopy
    @Override 
    public View getView(int position, View convertView, ViewGroup parent) { 
           Log.d("Adapter", "Position:" + position + " : " + String.valueOf(System.currentTimeMillis())); 
           ViewHolder holder; 
           if (convertView == null) { 
                  final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 
                  convertView = inflater.inflate(R.layout.list_item_icon_text, null); 
                  holder = new ViewHolder(); 
                 holder.icon = (ImageView) convertView.findViewById(R.id.icon); 
                 holder.text = (TextView) convertView.findViewById(R.id.text); 
                 convertView.setTag(holder); 
           } else { 
                 holder = (ViewHolder) convertView.getTag(); 
           } 
                  holder.icon.setImageResource(R.drawable.icon); 
                  holder.text.setText(mData[position]); 
                  return convertView; 
           } 
      
           static class ViewHolder { 
                   ImageView icon; 
                   TextView text; 
           } 


    以上是Google IO大会上给出的优化建议,经过尝试ListView确实流畅了许多。使用1000条记录,经测试第一种方式耗时:25211ms,第二种方式耗时:16528ms。
     
    7)       其他
                       i.    分辨率适配
    -ldpi,-mdpi, -hdpi配置不同精度资源,系统会根据设备自适应,包括drawable, layout,style等不同资源。

                       ii.    尽量使用dp(density independent pixel)开发,不用px(pixel)。

                       iii.    多用wrap_content, fill_parent

                       iv.    抛弃AbsoluteLayout

                       v.    使用9patch(通过~/tools/draw9patch.bat启动应用程序),png格式

                       vi.    采用<merge> 优化布局层数;采用<include >来共享布局。

                       vii.    将Acitivity中的Window的背景图设置为空。getWindow().setBackgroundDrawable(null);android的默认背景是不是为空。

                       viii.    View中设置缓存属性.setDrawingCache为true。
     
    3.      网络优化
    1)       避免频繁网络请求
    访 问server端时,建立连接本身比传输需要跟多的时间,如非必要,不要将一交互可以做的事情分成多次交互(这需要与Server端协调好)。有效管理 Service 后台服务就相当于一个持续运行的Acitivity,如果开发的程序后台都会一个service不停的去服务器上更新数据,在不更新数据的时候就让它 sleep,这种方式是非常耗电的,通常情况下,可以使用AlarmManager来定时启动服务。如下所示,第30分钟执行一次。
    [java] view plaincopy
    AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 
    Intent intent = new Intent(context, MyService.class); 
    PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0); 
    long interval = DateUtils.MINUTE_IN_MILLIS * 30; 
    long firstWake = System.currentTimeMillis() + interval; 
    am.setRepeating(AlarmManager.RTC,firstWake,  interval,  pendingIntent); 

    2)       数据压缩
    传输数据经过压缩 目前大部门网站都支持GZIP压缩,所以在进行大数据量下载时,尽量使用GZIP方式下载,可以减少网络流量,一般是压缩前数据大小的30%左右。

    [java] view plaincopy
    HttpGet request = new HttpGet("http://example.com/gzipcontent"); 
    HttpResponse resp = new DefaultHttpClient().execute(request); 
    HttpEntity entity = response.getEntity(); 
    InputStream compressed = entity.getContent(); 
    InputStream rawData = new GZIPInputStream(compressed); 

     
        
     
    3)       使用线程池
    线程池,分为核心线程池和普通线程池,下载图片等耗时任务放置在普通线程池,避免耗时任务阻塞线程池后,导致所有异步任务都必须等待。
     
    4)       选择合适的数据格式传输形式

       

    其中Tree Parse 是DOM解析 Event/Stream是SAX方式解析。
    很 明显,使用流的方式解析效率要高一些,因为DOM解析是在对整个文档读取完后,再根据节点层次等再组织起来。而流的方式是边读取数据边解析,数据读取完 后,解析也就完毕了。在数据格式方面,JSON和Protobuf效率明显比XML好很多,XML和JSON大家都很熟悉。
    从上面的图中可以得出结论就是尽量使用SAX等边读取边解析的方式来解析数据,针对移动设备,最好能使用JSON之类的轻量级数据格式为佳。
     
    1)       其他
    设置连接超时时间和响应超时时间。Http请求按照业务需求,分为是否可以缓存和不可缓存,那么在无网络的环境中,仍然通过缓存的HttpResponse浏览部分数据,实现离线阅读。
     
    2.      数据库相关
    1)       相对于封装过的ContentProvider而言,使用原始SQL语句执行效率高,比如使用方法rawQuery、execSQL的执行效率比较高。
    2)       对于需要一次性修改多个数据时,可以考虑使用SQLite的事务方式批量处理。
    3)       批量插入多行数据使用InsertHelper或者bulkInsert方法
    4)       有些能用文件操作的,尽量采用文件操作,文件操作的速度比数据库的操作要快10倍左右。
     
    3.      性能测试
    对于Android平台上软件的性能测试可以通过以下几种方法来分析效率瓶颈,目前Google在Android软件开发过程中已经引入了多种测试工具包,比如Unit测试工程,调试类,还有模拟器的Dev Tools都可以直接反应执行性能。
    1)       在模拟器上的Dev Tools可以激活屏幕显示当前的FPS,CPU使用率,可以帮助我们测试一些3D图形界面的性能。
    2)       一般涉及到网络应用的程序,在效率上和网速有很多关系,这里需要多次的调试才能实际了解。
    3)       对于逻辑算法的效率执行,我们使用Android上最普遍的,计算执行时间来查看:

    [java] view plaincopy
    long start = System.currentTimeMillis(); 
    // do something 
    long duration = System.currentTimeMillis() - start; 

    最终duration保存着实际处理该方法需要的毫秒数。
    4)       gc效率跟踪,如果你执行的应用比较简单,可以在DDMS中查看下Logcat的VM释放内存情况,大概模拟下那些地方可以缓存数据或改进算法的。
    5)       线 程的使用和同步,Android平台上给我们提供了丰富的多任务同步方法,但在深层上并没有过多的比如自旋锁等高级应用,不过对于Service和 appWidget而言,他们实际的产品中都应该以多线程的方式处理,以释放CPU时间,对于线程和堆内存的查看这些都可以在DDMS中看到。
    6)       利用traceview和monkey等工具测试应用。
    7)       利用layoutopt和ninepatch等工具优化视图。
     
    4.      结束语
    本 文给出了一些Android 移动开发中常见的优化方法,多数情况下利用这些优化方法优化后的代码,执行效率有明显的提高,内存溢出情况也有所改善。在实际应用中对程序的优化一定要权 衡是否是必须的,因为优化可能会带来诸如增加BUG,降低代码的可读性,降低代码的移植性等不良效果。
    希望不要盲目优化,请先确定存在问题,再进行优化。并且你知道当前系统的性能,否则无法衡量你进行尝试所得到的性能提升。
    希望本文能给大家切实带来帮助。欢迎就上述问题进一步交流。如有发现错误或不足,请斧正。



    展开全文
  •  修复建议 代码层最佳防御sql漏洞方案:使用预编译sql语句查询和绑定变量。  (1)使用预编译语句,使用PDO需要注意不要将变量直接拼接到PDO语句中。所有的查询语句都使用数据库提供的参数化查询接口,参数化的...

    1.SQL注入

      漏洞描述

      Web程序中对于用户提交的参数未做过滤直接拼接到SQL语句中执行,导致参数中的特殊字符破坏了SQL语句原有逻辑,攻击者可以利用该漏洞执行任意SQL语句,如查询数据、下载数据、写入webshell、执行系统命令以及绕过登录限制等。

      修复建议

    代码层最佳防御sql漏洞方案:使用预编译sql语句查询和绑定变量。

      (1)使用预编译语句,使用PDO需要注意不要将变量直接拼接到PDO语句中。所有的查询语句都使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到SQL语句中。当前几乎所有的数据库系统都提供了参数化SQL语句执行接口,使用此接口可以非常有效的防止SQL注入攻击。

      (2)对进入数据库的特殊字符(’”<>&*;等)进行转义处理,或编码转换。

      (3)确认每种数据的类型,比如数字型的数据就必须是数字,数据库中的存储字段必须对应为int型。

      (4)数据长度应该严格规定,能在一定程度上防止比较长的SQL注入语句无法正确执行。

      (5)网站每个数据层的编码统一,建议全部使用UTF-8编码,上下层编码不一致有可能导致一些过滤模型被绕过。

      (6)严格限制网站用户的数据库的操作权限,给此用户提供仅仅能够满足其工作的权限,从而最大限度的减少注入攻击对数据库的危害。

      (7)避免网站显示SQL错误信息,比如类型错误、字段不匹配等,防止攻击者利用这些错误信息进行一些判断。

      (8)过滤危险字符,例如:采用正则表达式匹配union、sleep、and、select、load_file等关键字,如果匹配到则终止运行。

    2.XSS

      漏洞描述

      1、Web程序代码中对用户提交的参数未做过滤或过滤不严,导致参数中的特殊字符破坏了HTML页面的原有逻辑,攻击者可以利用该漏洞执行恶意HTML/JS代码、构造蠕虫、篡改页面实施钓鱼攻击、以及诱导用户再次登录,然后获取其登录凭证等。

      2、XSS攻击对Web服务器本身虽无直接危害,但是它借助网站进行传播,对网站用户进行攻击,窃取网站用户账号身份信息等,从而也会对网站产生较严重的威胁。

     

    XSS攻击可导致以下危害:

      1、钓鱼欺骗:最典型的就是利用目标网站的反射型跨站脚本漏洞将目标网站重定向到钓鱼网站,或者通过注入钓鱼JavaScript脚本以监控目标网站的表单输入,甚至攻击者基于DHTML技术发起更高级的钓鱼攻击。

      2、网站挂马:跨站时,攻击者利用Iframe标签嵌入隐藏的恶意网站,将被攻击者定向到恶意网站上、或弹出恶意网站窗口等方式,进行挂马。

      3、身份盗用:Cookie是用户对于特定网站的身份验证标志,XSS攻击可以盗取用户的cookie,从而利用该cookie盗取用户对该网站的操作权限。

      4、盗取网站用户信息:当窃取到用户cookie从而获取到用户身份时,攻击者可以盗取到用户对网站的操作权限,从而查看用户隐私信息。

      5、垃圾信息发送:在社交网站社区中,利用XSS漏洞借用被攻击者的身份发送大量的垃圾信息给特定的目标群。

      6、劫持用户Web行为:一些高级的XSS攻击甚至可以劫持用户的Web行为,从而监视用户的浏览历史、发送与接收的数据等等。

      7、XSS蠕虫:借助XSS蠕虫病毒还可以用来打广告、刷流量、挂马、恶作剧、破坏数据、实施DDoS攻击等。

      修复建议

    xss漏洞本质上是一种html注入,也就是将html代码注入到网页中。那么其防御的根本就是在将用户提交的代码显示到页面上时做好一系列的过滤与转义

      (1)过滤输入的数据,对例如:“ ‘ ”,“ “ ”,” < “,” > “,” on* “,script、iframe等危险字符进行严格的检查。这里的输入不仅仅是用户可以直接交互的输入接口,也包括HTTP请求中的Cookie中的变量,HTTP请求头部中的变量等。

      (2)不仅验证数据的类型,还要验证其格式、长度、范围和内容。

      (3)不仅在客户端做数据的验证与过滤,关键的过滤步骤在服务端进行。

      (4)对输出到页面的数据进行相应的编码转换,如HTML实体编码、JS编码等。对输出的数据也要检查,数据库里的值有可能会在一个大网站的多处都有输出,即使在输入做了编码等操作,在各处的输出点时也要进行检查。

    3.XXE

      漏洞详情

      参考:https://www.freebuf.com/company-information/165769.html

    4.CSRF

      漏洞描述

      CSRF是跨站请求伪造,不攻击网站服务器,而是冒充用户在站内的正常操作。通常由于服务端没有对请求头做严格过滤引起的。CSRF会造成密码重置,用户伪造等问题,可能引发严重后果。绝大多数网站是通过 cookie 等方式辨识用户身份,再予以授权的。所以要伪造用户的正常操作,最好的方法是通过 XSS 或链接欺骗等途径,让用户在本机(即拥有身份 cookie 的浏览器端)发起用户所不知道的请求。CSRF攻击会令用户在不知情的情况下攻击自己已经登录的系统。

      修复建议

      1、验证请求的Referer是否来自本网站,但可被绕过。

      2、在请求中加入不可伪造的token,并在服务端验证token是否一致或正确,不正确则丢弃拒绝服务。

    5.SSRF

      漏洞描述

      SSRF(Server-Side Request Forgery,服务器端请求伪造):通俗的来说就是我们可以伪造服务器端发起的请求,从而获取客户端所不能得到的数据。SSRF漏洞形成的原因主要是服务器端所提供的接口中包含了所要请求的内容的URL参数,并且未对客户端所传输过来的URL参数进行过滤。这个漏洞造成的危害有:

      (1)、可以对外网、服务器所在内网、本地进行端口扫描,获取一些服务的banner信息;

      (2)、攻击运行在内网或本地的应用程序(比如溢出);

      (3)、对内网Web应用进行指纹识别,通过访问默认文件实现;

      (4)、攻击内外网的Web应用,主要是使用Get参数就可以实现的攻击(比如Struts2漏洞利用,SQL注入等);

      (5)、利用File协议读取本地文件。

      修复建议

      1、禁用不需要的协议,只允许HTTP和HTTPS请求,可以防止类似于file://, gopher://, ftp:// 等引起的问题。

      2、白名单的方式限制访问的目标地址,禁止对内网发起请求

      3、过滤或屏蔽请求返回的详细信息,验证远程服务器对请求的响应是比较容易的方法。如果web应用是去获取某一种类型的文件。那么在把返回结果展示给用户之前先验证返回的信息是否符合标准。

      4、验证请求的文件格式

      5、禁止跳转

      6、限制请求的端口为http常用的端口,比如 80、443、8080、8000等

      7、统一错误信息,避免用户可以根据错误信息来判断远端服务器的端口状态。

    6.任意命令/代码执行

      漏洞描述

      命令或代码执行漏洞是指代码未对用户可控参数做过滤,导致直接带入执行命令和代码,通过漏洞执行恶意构造的语句,执行任意命令或代码。攻击者可在服务器上执行任意命令,读写文件操作等,危害巨大。

      修复建议

      1、严格过滤用户输入的数据,禁止执行非预期系统命令。

      2、减少或不使用代码或命令执行函数

      3、客户端提交的变量在放入函数前进行检测

      4、减少或不使用危险函数

    7.任意文件上传

      漏洞描述

      文件上传漏洞通常由于代码中对文件上传功能所上传的文件过滤不严或web服务器相关解析漏洞未修复而造成的,如果文件上传功能代码没有严格限制和验证用户上传的文件后缀、类型等,攻击者可通过文件上传点上传任意文件,包括网站后门文件(webshell)控制整个网站。

      修复建议

      1、对上传文件类型进行验证,除在前端验证外在后端依然要做验证,后端可以进行扩展名检测,重命名文件,MIME类型检测以及限制上传文件的大小等限制来防御,或是将上传的文件其他文件存储服务器中。

      2、严格限制和校验上传的文件,禁止上传恶意代码的文件。同时限制相关上传文件目录的执行权限,防止木马执行。

      3、对上传文件格式进行严格校验,防止上传恶意脚本文件;

      4、严格限制上传的文件路径。

      5、文件扩展名服务端白名单校验。

      6、文件内容服务端校验。

      7、上传文件重命名。

      8、隐藏上传文件路径。

    8.目录穿越/目录遍历

      漏洞描述

      文件下载或获取文件显示内容页面由于未对传入的文件名进行过滤,利用路径回溯符../跳出程序本身的限制目录,来下载或显示任意文件。

      修复建议

      对传入的文件名参数进行过滤,并且判断是否是允许获取的文件类型,过滤回溯符../。

    9.文件包含

      漏洞描述

      本地文件包含是指程序在处理包含文件的时候没有严格控制。利用这个漏洞,攻击者可以先把上传的文件、网站日志文件等作为代码执行或直接显示出来,或者包含远程服务器上的恶意文件,进而获取到服务器权限。

      修复建议

      1、严格检查变量是否已经初始化。

      2、对所有输入提交可能包含的文件地址,包括服务器本地文件及远程文件,进行严格的检查,参数中不允许出现./和../等目录跳转符。

      3、严格检查文件包含函数中的参数是否外界可控。

    10.弱口令

      漏洞描述

      由于网站用户帐号存在弱口令,导致攻击者通过弱口令可轻松登录到网站中,从而进行下一步的攻击,如上传webshell,获取敏感数据。

      另外攻击者利用弱口令登录网站管理后台,可执行任意管理员的操作。

      修复建议

      1、强制用户首次登录时修改默认口令,或是使用用户自定义初始密码的策略;

      2、完善密码策略,信息安全最佳实践的密码策略为8位(包括)以上字符,包含数字、大小写字母、特殊字符中的至少3种。

      3、增加人机验证机制,限制ip访问次数。

    11.暴力破解

      漏洞描述

      由于没有对登录页面进行相关的人机验证机制,如无验证码、有验证码但可重复利用以及无登录错误次数限制等,导致攻击者可通过暴力破解获取用户登录账号和密码。

      修复建议

      1、如果用户登录次数超过设置的阈值,则锁定帐号(有恶意登录锁定帐号的风险)

      2、如果某个 IP登录次数超过设置的阈值,则锁定IP

      3、增加人机验证机制

      4、验证码必须在服务器端进行校验,客户端的一切校验都是不安全的。

    12.越权访问

      漏洞描述

      由于没有对用户访问角色的权限进行严格的检查及限制,导致当前账号可对其他账号进行相关操作,如查看、修改等。对低权限对高权限账户的操作为纵向越权,相同权限账户之间的操作成为横向越权也称水平越权。

      修复建议

      1、对用户访问角色的权限进行严格的检查及限制。

      2、在一些操作时可以使用session对用户的身份进行判断和控制

    13.未授权访问

      漏洞描述

      由于没有对网站敏感页面进行登录状态、访问权限的检查,导致攻击者可未授权访问,获取敏感信息及进行未授权操作。

      修复建议

      1、页面进行严格的访问权限的控制以及对访问角色进行权限检查。

      2、可以使用session对用户的身份进行判断和控制。

    14.列目录

      漏洞描述

      由于web服务器配置不当,开启了目录浏览,攻击者可获得服务器上的文件目录结构,获取敏感文件。

      修复建议

      1、通过修改配置文件,禁止中间件(如IIS、apache、tomcat)的文件目录索引功能

      2、设置目录访问权限

    15.PHP反序列化

    漏洞描述

      php反序列化漏洞也叫PHP对象注入,形成原因为程序未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行、文件操作、执行数据库操作等不可控后果。这一类攻击在java、python等面向对象语言中均存在。

    可参考:https://www.freebuf.com/articles/web/167721.html

      修复建议

      1、对传入的对象进行严格的过滤检查

      2、在反序列化过程执行的文件读写、命令或代码执行函数中是否有用户可控的参数。

    16.http slow拒绝服务攻击

      漏洞描述

      按照设计,HTTP协议要求服务器在处理之前完全接收请求。 如果HTTP请求没有完成,或者传输速率非常低,服务器会保持其资源忙于等待其余数据。如果服务器保持太多的资源请求和处理,这将造成一个拒绝服务。严重者一台主机即可让web运行缓慢甚至是崩溃。

      修复建议

      对于 Apache 可以做以下优化(其他服务器原理相同):

      1、设置合适的 timeout 时间(Apache 已默认启用了 reqtimeout 模块),规定了 Header 发送的时间以及频率和 Body 发送的时间以及频率

      2、增大 MaxClients(MaxRequestWorkers):增加最大的连接数。根据官方文档,两个参数是一回事,版本不同,MaxRequestWorkers was called MaxClients before version 2.3.13.Theold name is still supported.

      3、默认安装的 Apache 存在 Slow Attack 的威胁,原因就是虽然设置的 timeoute,但是最大连接数不够,如果攻击的请求频率足够大,仍然会占满Apache的所有连接

    17.CRLF注入

      漏洞描述

      CRLF 是“回车 +换行”(\r\n)的简称。在 HTTP 协议中,HTTPHeader 与 HTTP Body 是用两个 CRLF 符号进行分隔的,浏览器根据这两个 CRLF 符号来获取 HTTP 内容并显示。因此,一旦攻击者能够控制 HTTP 消息头中的字符,注入一些恶意的换行,就能注入一些会话 Cookie 或者 HTML 代码。

      修复建议

      1、过滤 \r 、\n 及其各种编码的换行符,避免输入的数据污染到其他 HTTP 消息头。

    18.LDAP注入

      漏洞描述

      由于Web 应用程序没有对用户发送的数据进行适当过滤和检查,攻击者可修改LDAP 语句的结构,并且以数据库服务器、Web 服务器等的权限执行任意命令,许可权可能会允许查询、修改或除去 LDAP 树状构造内任何数据。

      修复建议

      对用户的输入内容进行严格的过滤。

    19.URL 跳转

      漏洞描述

      有的Web 应用程序中使用URL参数中的地址作为跳转链接的功能 ,攻击者可实施钓鱼、恶意网站跳转等攻击。

      修复建议

      1、在进行页面跳转前校验传入的URL是否为可信域名。

      2、白名单规定跳转链接

    20.明文传输

      漏洞描述

      用户登录过程中使用明文传输用户登录信息,若用户遭受中间人攻击时,攻击者可直接获取该用户登录账户,从而进行进一步渗透。

      修复建议

      1、用户登录信息使用加密传输,如密码在传输前使用安全的算法加密后传输,可采用的算法包括:不可逆hash算法加盐(4位及以上随机数,由服务器端产生);安全对称加密算法,如AES(128、192、256位),且必须保证客户端密钥安全,不可被破解或读出;非对称加密算法,如RSA(不低于1024位)、SM2等。

      2、使用https来保证传输的安全。

    21.网页木马

      漏洞描述

      经渗透测试发现目标站点存在webshell,攻击者可直接爆破口令使用木马,非常低成本的进行恶意操作。

      修复建议

      1、确认并删除木马文件,并进行本地文件漏洞扫描排查是否还存在有其他木马。

      2、发现并及时修复已存在的漏洞。

      3、通过查看日志、服务器杀毒等安全排查,确保服务器未被留下后门

    22.备份文件泄露

      漏洞描述

      网站备份文件或、敏感信息文件存放在某个网站目录下,攻击者可通过文件扫描等方法发现并下载该备份文件,导致网站敏感信息泄露。

      修复建议

      1、不在网站目录下存放网站备份文件或敏感信息的文件。

      2、如需存放该类文件,请将文件名命名为难以猜解的无规则字符串。

    23.敏感信息泄露

      漏洞描述

      在页面中或者返回的响应包中泄露了敏感信息,通过这些信息,给攻击者渗透提供了非常多的有用信息。

      修复建议

      1、如果是探针或测试页面等无用的程序建议删除,或者修改成难以猜解的名字。

      2、不影响业务或功能的情况下删除或禁止访问泄露敏感信息页面。

      3、在服务器端对相关敏感信息进行模糊化处理。

      4、对服务器端返回的数据进行严格的检查,满足查询数据与页面显示数据一致。

    24.短信/邮件轰炸

      漏洞描述

      由于没有对短信或者邮件发送次数进行限制,导致可无限次发送短信或邮件给用户,从而造成短信轰炸,进而可能被大量用户投诉,从而影响公司声誉。

      修复建议

      在服务器限制发送短信或邮件的频率,如同一账号1分钟只能发送1次短信或邮件,一天只能发送3次。

    25.phpinfo信息泄漏

      漏洞描述

      Web站点的某些测试页面可能会使用到PHP的phpinfo()函数,会输出服务器的关键信息,造成服务器信息泄露,为攻击提供有利的信息。

      修复建议

      1、删除phpinfo 函数。

      2、若文件无用可直接删除。 

    26.IIS短文件名泄露漏洞

      漏洞描述

      Internet Information Services(IIS,互联网信息服务)是由微软公司提供的基于运行Microsoft Windows的互联网基本服务。 Microsoft IIS在实现上存在文件枚举漏洞,攻击者可利用此漏洞枚举网络服务器根目录中的文件。危害:攻击者可以利用“~”字符猜解或遍历服务器中的文件名,或对IIS服务器中的.Net Framework进行拒绝服务攻击。

      攻击者可通过该漏洞尝试获取网站服务器文件的文件名,达到获取更多信息来入侵服务器的目的。

      修复建议

      修改Windows配置,关闭短文件名功能。

      1.关闭NTFS 8.3文件格式的支持。该功能默认是开启的,对于大多数用户来说无需开启。

      2.如果是虚拟主机空间用户,可采用以下修复方案:

      1)修改注册列表HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\NtfsDisable8dot3NameCreation的值为1(此修改只能禁止NTFS8.3格式文件名创建,已经存在的文件的短文件名无法移除)。

      2)如果你的web环境不需要asp.net的支持你可以进入Internet 信息服务(IIS)管理器 --- Web 服务扩展 - ASP.NET 选择禁止此功能。

      3)升级net framework 至4.0以上版本。

      3.将web文件夹的内容拷贝到另一个位置,比如D:\www到D:\www.back,然后删除原文件夹D:\www,再重命名D:\www.back到D:\www。如果不重新复制,已经存在的短文件名则是不会消失的。

    27.应用程序错误信息泄露

      漏洞描述

      攻击者可通过特殊的攻击向量,使web服务器出现500、403等相关错误,导致信息泄漏如绝对路径、webserver版本、源代码、sql语句等敏感信息,恶意攻击者很有可能利用这些信息实施进一步的攻击。

      修复建议

      1、自定义错误页面或使用统一的错误页面提示。

    28.Apache Tomcat默认文件

      漏洞描述

      Apache Tomcat默认样例文件没有删除或限制访问,可能存在cookie、session伪造,进行后台登录操作

      修复建议

      1、删除样例文件

      2、限制文件访问权限

    29.Crossdomain.xml 配置不当

      漏洞描述

      网站根目录下的 crossdomain.xml 文件指明了远程Flash 是否可以加载当前网站的资源(图片、网页内容、Flash等)。如果配置不当,可能导致遭受跨站请求伪造(CSRF)攻击。

      修复建议

      对于不需要从外部加载资源的网站,在 crossdomain.xml 文件中更改allow-access-from的domain属性为域名白名单。

    30.目标服务器启用了不安全 HTTP 方法

      漏洞描述

      目标服务器启用了不安全的传输方法,如PUT、TRACE、DELETE、MOVE等,这些方法表示可能在服务器上使用了 WebDAV,由于dav方法允许客户端操纵服务器上的文件,如上传、修改、删除相关文件等危险操作,如果没有合理配置dav,有可能允许未授权的用户对其进行利用,修改服务器上的文件。

      修复建议

      1、关闭不安全的传输方法,只开启POST、GET方法。

      2、如果服务器不使用 WebDAV 可直接禁用,或为允许webdav的目录配置严格的访问权限,如认证方法,认证需要的用户名,密码。

    31.weblogic SSRF服务器请求伪造

      漏洞描述

      目标存在weblogic SSRF服务器请求伪造漏洞。WebLogic是用于开发、集成、部署和管理大型分布式Web应用、网络应用和数据库应用的Java应用服务器。SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下,SSRF攻击的目标是从外网无法访问的内部系统。(正是因为它是由服务端发起的,所以它能够请求到与它相连而与外网隔离的内部系统)。Weblogic中间件默认带有“UDDI 目录浏览器”且为未授权访问,通过该应用,可进行无回显的SSRF请求。攻击者可利用该漏洞对企业内网进行大规模扫描,了解内网结构,并可能结合内网漏洞直接获取服务器权限。

      修复建议

      1、若不影响业务则可删除uddiexplorer文件夹

      2、限制uddiexplorer应用只能内网访问

    32.Apache Struts2 远程代码执行漏洞(S2-019)

      漏洞描述

      Apache Struts2的“Dynamic MethodInvocation”机制是默认开启的,仅提醒用户如果可能的情况下关闭此机制,如果未关闭此机制将导致远程代码执行漏洞,远程攻击者可利用此漏洞在受影响应用上下文中执行任意代码。

      修复建议

      1、目前厂商已经发布了升级补丁以修复这个安全问题,请到厂商的主页下载。

      2、或者手工设置struts.xml文件<constantname="struts.enable.DynamicMethodInvocation"value="false"/>

    33.Apache Struts2 远程代码执行漏洞(S2-037)

      漏洞描述

      Apache Struts2在使用REST插件时,攻击者可以绕过动态方法执行的限制,调用恶意表达式执行远程代码。

      修复建议

      建议用户到官方获取最新补丁或者最新版本程序。

    34.Apache Struts2 DevMode 远程代码执行漏洞

      漏洞描述

      为了便于开发人员调试程序,Struts2提供了一个devMode模式,可以方便查看程序错误以及日志等信息。当Struts2中的devMode模式设置为true时,存在严重远程代码执行漏洞。如果WebService 启动权限为最高权限时,可远程执行任意命令,包括关机、建立新用户、以及删除服务器上所有文件等等。

      修复建议

      建议用户到官方获取最新补丁或者最新版本程序。

      或者将struts.properties中的devMode设置为false,或是在struts.xml中添加如下代码: <constant name="struts.devMode"value="false"/>。

    35.Apache Struts2 远程代码执行漏洞(S2-045)

      漏洞描述

      Apache Struts2的Jakarta Multipartparser插件存在远程代码执行漏洞,漏洞编号为CVE-2017-5638。攻击者可以在使用该插件上传文件时,修改HTTP请求头中的Content-Type值来触发该漏洞,导致远程执行代码。

      修复建议

      检测方式查看web目录下/WEB-INF/lib/目录下的struts-core.x.x.jar ,如果这个版本在Struts2.3.5 到 Struts2.3.31 以及 Struts2.5 到 Struts2.5.10之间则存在漏洞。

      1、建议用户到官方获取最新补丁或者最新版本程序。

      2、更新至Strusts2.3.32或者Strusts2.5.10.1,或使用第三方的防护设备进行防护。

      3、临时解决方案:删除commons-fileupload-x.x.x.jar文件(会造成上传功能不可用)。

      4、修改WEB-INF/classes目录下的配置

      在WEB-INF/classes目录下的struts.xml中的struts 标签下添加

      <constantname=”struts.custom.i18n.resources”value=”global”/>;

      在WEB-INF/classes/目录下添加global.properties,文件内容如下:

      struts.messages.upload.error.InvalidContentTypeException=1

    36.Apache Struts2 远程代码执行漏洞(S2-033)

      漏洞描述

      Apache Struts2在开启动态方法调用(DynamicMethod Invocation)的情况下,攻击者使用REST插件调用恶意表达式可以远程执行代码。

      修复建议

      1、用户到官方获取最新补丁或者最新版本程序。

      2、或者在允许的情况下禁用动态方法调用(Dynamic Method Invocation),修改Struts2的配置文件struts.xml,将struts.enable.DynamicMethodInvocation设置为“false”。

    37.redis相关漏洞修复

    补充来自博客园:@梦猩魂

    完善来自博客园:@我超怕的

    漏洞描述

    redis默认没有口令可能会造成未授权访问造成信息泄露,若redis为高权限账户运行,可能导致服务器权限丢失等。

    安全建议

    1.禁用一些高危命令

      常见如:flushdb,flushall,config,keys 等

    2.以低权限运行 Redis 服务

    3.为 Redis 添加密码验证

    4.禁止外网访问 Redis

    5.保证 authorized_keys 文件的安全

     

    展开全文
  • 模糊测试--强制性安全漏洞发掘

    万次阅读 2015-03-12 11:41:50
    http://blog.csdn.net/lxt889/article/details/18546373 ...--George W....模糊测试的概念至少已经流传了20年,但是直到最近才引起广泛的关注。安全漏洞困扰了许多流行的客户端应用程序,包括Microsoft的Intern

    http://blog.csdn.net/lxt889/article/details/18546373


    前 言

    我知道"人类和鱼类能够和平共处" 。

    --George W. Bush, 2000年9月29日

    简介

    模糊测试的概念至少已经流传了20年,但是直到最近才引起广泛的关注。安全漏洞困扰了许多流行的客户端应用程序,包括Microsoft的Internet Explorer、Word和Excel,它们中的许多漏洞在2006年通过模糊测试技术被发现。模糊测试技术的有效应用产生了许多新的工具和日益广泛的影响。本书是第一部公开发表的关于这一主题的专著,这一尴尬事实同时也预示着未来人们将会对模糊测试有更浓厚的兴趣。

    多年来,我们参与了许多有关安全漏洞的研究工作,并且在日常工作中使用了各种不同的模糊测试技术,从不成熟的、凭借个人嗜好的项目到高端的商业产品,都用到过模糊测试。每一位作者都曾参与开发过自用版本的和公开发行版本的模糊器。这本书凝聚了我们以往的实践经验和正在进行的研究项目所花费的心血,我们希望读者能够从中获益。

    目标读者

    安全性领域的书籍和文章通常由这一领域的研究者所写,以方便该领域的其它研究者参考。我们坚信,只要安全性领域的研究小组把解决安全性问题视为其唯一责任,那么安全性问题的数量和严重程度就会随着时间的推移而继续增长。因此,我们付出巨大的努力以使本书能够服务于更多的读者,既包括模糊测试的新手也包括早已对本领域有所了解的读者。

    假设我们只是将开发完成的应用程序提交给一个安全小组,然后让他们在产品发布之前对其进行一个快速审核,相信这样的过程能够产生安全的应用程序显然是不现实的。当开发者或QA组的组员说:"安全根本不是问题--我们有个安全小组关心这件事呢",如此这般,日子就会一天一天的过去。安全性必须被融入软件开发生命周期(SDLC),而不是到了最后才草率处理。

    让开发组和QA组把注意力集中在安全性问题上可能是个离谱的要求,特别是对那些以往没有这么做的开发组和QA组来说尤其如此。我们认为模糊测试是一种独一无二的安全漏洞发掘方法学,由于它能够被高度自动化,因此学习和掌握这种方法学的读者可以相当广泛。我们希望经验丰富的安全领域的研究者可从本书获得有价值的东西,同样希望开发人员和QA人员从中获益。模糊测试可以并且应该是任何完整SDLC的一部分,不仅在测试阶段需要考虑,在开发阶段也同样需要考虑。缺陷被发现得越及时,修补缺陷的成本就越低。

    预备知识

    模糊测试是一个广泛的主题。尽管本书中会介绍一些不专属于模糊测试的背景内容,但是我们仍然假设读者应该拥有这一领域的预备知识。在学习本书之前,读者至少应该对程序设计和计算机网络有一定的基本了解。模糊测试是关于自动化安全测试的,这本书的内容自然要包括如何构造自动工具。我们有目的地选择了多种编程语言来完成这个任务。语言的选择是根据具体任务的,这也说明了模糊测试可以用多种方法实现。当然,没有必要一一罗列所用到的所有编程语言的背景知识,但是介绍一两种语言无疑会帮助读者从这些章节中获益。

    本书自始至终都贯穿着对各种安全漏洞的详细描述并讨论如何通过模糊测试来识别这些漏洞。然而,定义或剖析安全漏洞本身的性质并不是本书的目标。一些优秀的书籍是专门讨论这一主题的。如果需要寻找一部关于软件安全漏洞的初级读本,可以参看Greg Hoglund 、Gray McGraw所著的《Exploiting Software》和Jack Koziol、David Litchfiel等的《Shellcoder's Handbook》,它们都是极好的参考读物。

    学习方法

    如何最好地利用本书,这取决于读者的背景和目的。如果你是一位模糊测试的初学者,我们推荐你按顺序逐章消化理解,因为本书的内容进行了有目的的编排,前面先介绍一些必要的背景信息,随后转入高级主题。反之,如果你已经在使用各种模糊测试工具方面花费了一些时间,那么请不要犹豫,可以直接进入感兴趣的主题,因为本书的不同逻辑章节的划分大致上是相互独立的。

    本书的第一部分主要介绍不同的、具体的模糊测试类型,这些模糊测试类型将在随后的章节中逐一讨论。如果读者对模糊测试比较陌生,可以考虑把这一部分作为必读章节。模糊测试可以被作为多种目标下的安全性测试方法学,不过这些目标下的方法学都遵循相同的基本原则。在第一部分,我们试图将模糊测试定义为一种安全漏洞发掘方法学并详细介绍相关的知识,不考虑这种方法学运用于何种目的。

    第二部分关注模糊测试的各种相关应用目标。每种目标的介绍跨越了两到三章。最前面的一章介绍每类目标的背景信息,随后的章节集中介绍这些目标下的模糊测试自动化,详细阐述如何针对这种目标构造模糊器。当认为有必要分别介绍Windows平台和Unix平台下的模糊器工具时,这两个主题分别安排在有关自动化的两章。例如,以第11章"文件格式模糊测试"为例,该章详细描述有关模糊文件分析器的内容,第12章"文件格式模糊测试:Unix平台上的自动化测试"则深入介绍基于Unix的文件模糊器的实用程序设计,第13章"文件格式模糊测试:Windows平台上的自动化测试"讲解运行在Windows环境中的文件格式模糊器如何构造。

    第三部分讨论模糊测试领域的高级主题。对于那些已经牢固掌握模糊测试背景知识的读者,可以直接跳入第三部分,不过大部分读者很可能需要先了解第一部分和第二部分,然后再学习第三部分。第三部分关注的是近年来浮现出的新技术,这些技术刚刚得到实施,但是未来将成为安全漏洞发掘的高级工具可以利用的模糊测试技术。

    最后,在第四部分,我们将总结学习过本书后所得到的收获,然后深入洞察未来的发展方向。尽管模糊测试并不是一个新概念,但是这一领域仍然有足够的发展空间,并且我们希望本书将为未来的研究空间注入一丝灵感。

    少许幽默

    写书是一件严肃认真的工作,尤其是对诸如模糊测试这样的复杂主题。这就是说,我们希望尽量给随后的读者(实际上这些人可能比写书的人更重要)带来一些乐趣,同时也尽最大的努力让写作的过程更愉快。出于这样的考虑,我们决定在每一章的开头引用美国第43届总统George W. Bush (别名 Dubya)的一段话。不论你的政治倾向或信仰是什么,没人能够否定Bush先生在过去几年中所炮制出的一些引文,这些引文甚至能够写满一年的日历!我们从中挑选了一些最喜欢的引文与读者分享,希望读者和我们得到同样的快乐。读完本书后,读者会发现模糊测试可以被应用于各种不同的目标,显然也可以应用到对英语的模糊测试。

    关于封面

    有时,安全漏洞经常被称为"鱼"(例如,可以参看DailyDave安全性邮件列表中关于"TheL Word & Fish"的提示线索)。这是一个有用的类比,在讨论安全性和安全漏洞时可以被应用到这个问题的各个方面。这一领域的研究者可以被比作钓鱼者。对应用程序的汇编代码实施逆向工程、逐行分析查找安全漏洞,这样的人可被比作"深海钓鱼者"。同许多其它的审核手段相比,模糊测试充其量只是海面搜索,并且通常只对"容易抓的鱼"更有效。此外,大灰熊是一个著名的"模糊动物",当然也是强大的动物。这些场景组合在一起,就构成了本书的封面,其中有一个模糊的动物,正在捕捉一条鱼,后者代表一个安全漏洞。

    配套站点:WWW.FUZZING.ORG

    站点fuzzing.org绝对是本书不可分割的一部分,并不仅仅起到补充资源的作用。除了包含本书出版后的勘误表外,该站点还是书中所有源代码和工具的一个中央资源仓库。经过一段时间的努力,我们打算让fuzzing.org从一个以图书为中心的资源站点发成为模糊测试这一学科的资源、工具和信息的有价值的社区。我们欢迎你们提出反馈信息,以帮助我们让该站点成为一个有价值的、开放的知识库。

    目录

    作者序
    译者序
    前 言
    第一部分
    第1章 安全漏洞发掘方法学
    1.1 白盒测试
    1.1.1 源代码评审
    1.1.2 工具和自动化
    1.1.3 优点和缺点
    1.2 黑盒测试
    1.2.1 人工测试
    1.2.2 自动测试或模糊测试
    1.2.3 优点和缺点
    1.3 灰盒测试
    1.3.1 二进制审核
    1.3.2 自动化的二进制审核
    1.3.3 优点和缺点
    1.4 小结
    1.5 脚注
    第2章 什么是模糊测试
    2.1 模糊测试的定义
    2.2 模糊测试的历史
    2.3 模糊测试阶段
    2.4 模糊测试的局限性和期望
    2.4.1 访问控制缺陷
    2.4.2 设计逻辑不良
    2.4.3 后门
    2.4.4 内存破坏
    2.4.5 多阶段安全漏洞
    2.5 小结
    第3章 模糊测试方法和模糊器类型
    3.1 模糊测试方法
    3.1.1 预先生成测试用例
    3.1.2 随机方法
    3.1.3  协议变异人工测试
    3.1.4 变异或强制性测试
    3.1.5 自动协议生成测试
    3.2 模糊器类型
    3.2.1 本地模糊器
    3.2.2 远程模糊器
    3.2.3 内存模糊器
    3.2.4 模糊器框架
    3.3 小结
    第4章 数据表示和分析
    4.1 什么是协议
    4.2 协议域
    4.3 简单文本协议
    4.4 二进制协议
    4.5 网络协议
    4.6 文件格式
    4.7 常见的协议元素
    4.7.1 名字-值对
    4.7.2 块标识符
    4.7.3 块长度
    4.7.4 校验和
    4.8 小结
    第5章 有效模糊测试的需求
    5.1 可重现性和文档记录
    5.2 可重用性
    5.3 过程状态和过程深度
    5.4 跟踪、代码覆盖和度量
    5.5 错误检测
    5.6 资源约束
    5.7 小结
    第二部分
    第6章 自动化测试和测试数据生成
    6.1 自动化测试的价值
    6.2 有用的工具和库
    6.2.1ETHEREAL /WIRESHARK 
    6.2.2LIBDASM 和LIBDISASM 
    6.2.3LIBNET /LIBNETNT 
    6.2.4LIBPCAP 
    6.2.5METRO PACKET LIBRARY 
    6.2.6PTRACE
    6.2.7PYTHON EXTENSIONS
    6.3 编程语言的选择
    6.4 测试数据生成和模糊启发式
    6.4.1 整型值
    6.4.2 字符串重复
    6.4.3 字段分隔符
    6.4.4 格式化字符串
    6.4.5 字符翻译
    6.4.6 目录遍历
    6.5 小结
    第7章 环境变量和参数的模糊测试
    7.1 本地化模糊测试介绍
    7.1.1 命令行参数
    7.1.2 环境变量
    7.2 本地化模糊测试准则
    7.3 寻找目标程序
    7.4 本地化模糊测试方法
    7.5 枚举环境变量
    7.6 自动化的环境变量测试
    7.7 检测问题
    7.8 小结 
    第8章 环境变量和参数的模糊测试:自动化
    8.1 iFUZZ本地化模糊器的特性
    8.2 iFUZZ的开发
    8.3 iFUZZ的开发语言
    8.4 实例研究
    8.5 益处和改进的余地
    8.6 小结
    第9章 Web应用程序和服务器模糊测试
    9.1 什么是Web应用程序模糊测试
    9.2 目标应用
    9.3 测试方法
    9.3.1 建立目标环境
    9.3.2 输入
    9.4 漏洞
    9.5 异常检测
    9.6 小结
    第10章 Web应用程序和服务器的模糊测试:自动化
    10.1 Web应用模糊器
    10.2 WebFuzz的特性
    10.2.1 请求
    10.2.2 模糊变量
    10.2.3 响应
    10.3 必要的背景知识
    10.3.1 识别请求
    10.3.2 漏洞检测
    10.4 WebFuzz的开发
    10.4.1 开发方法
    10.4.2 开发语言的选择
    10.4.3 设计
    10.5 实例研究
    10.5.1 目录遍历
    10.5.2 溢出
    10.5.3 SQL注入
    10.5.4 XSS脚本
    10.6 益处和改进的余地
    10.7 小结
    第11章 文件格式模糊测试
    11.1 目标应用
    11.2 方法
    11.2.1 强制性或基于变异的模糊测试
    11.2.2 智能强制性或基于生成的模糊测试
    11.3 输入
    11.4 漏洞
    11.4.1 拒绝服务
    11.4.2 整数处理问题
    11.4.3 简单的栈和堆溢出
    11.4.4 逻辑错误
    11.4.5 格式化字符串
    11.4.6 竞争条件
    11.5 漏洞检测
    11.6 小结 
    第12章 文件格式模糊测试:UNIX平台上的自动化测试
    12.1 NOTSPIKEFILE和SPIKEFILE
    12.2 开发方法
    12.2.1 异常检测引擎
    12.2.2 异常报告(异常检测)
    12.2.3 核心模糊测试引擎
    12.3 有意义的代码片段
    12.3.1 通常感兴趣的UNIX信号
    12.3.2 不太感兴趣的UNIX信号
    12.4 僵死进程
    12.5 使用的注意事项
    12.5.1 ADOBE ACROBAT
    12.5.2 REALNETWORKS REALPLAYRE
    12.6 实例研究:REALPLAYERREALPIX格式化字符串漏洞
    12.7 语言
    12.8 小结
    第13章 文件格式模糊测试:Windows平台上的自动化测试
    13.1 Windows文件格式漏洞
    13.2 FileFuzz的特性
    13.2.1 创建文件
    13.2.2 应用程序执行
    13.2.3 异常检测
    13.2.4 保存的审核
    13.3 必要的背景知识
    13.4 FileFuzz的开发
    13.4.1 开发方法
    13.4.2 开发语言的选择
    13.4.3 设计
    13.5 实例研究
    13.6益处和改进的余地
    13.7 小结
    第14章 网络协议模糊测试
    14.1 什么是网络协议模糊测试
    14.2 目标应用
    14.2.1APPLEGATE
    14.2.2 网络层
    14.2.3 传输层
    14.2.4 会话层
    14.2.5 表示层
    14.2.6 应用层
    14.3 测试方法
    14.3.1强制性或基于变异的模糊测试
    14.3.2 智能强制性模糊测试和基于生成的模糊测试
    14.3.3 修改的客户端变异模糊测试
    14.4 错误检测
    14.4.1 人工方法(基于调试器)
    14.4.2 自动化方法(基于代理)
    14.4.3 其它方法
    14.5 小结
    第15章 网络协议模糊测试:UNIX平台上的自动化测试
    15.1 使用SPIKE进行模糊测试
    15.1.1 选择测试目标
    15.1.2  协议逆向工程
    15.2 SPIKE 101
    15.2.1 模糊测试引擎
    15.2.2 通用的基于行的TCP模糊器
    15.3 基于块的协议建模
    15.4 SPIKE的额外特性
    15.4.1 特定于协议的模糊器
    15.4.2 特定于协议的模糊测试脚本
    15.4.3 通用的基于脚本的模糊器
    15.5 编写SPIKENMAP模糊器脚本
    15.6 小结
    第16章 网络协议模糊测试:Windows平台上的自动化测试
    16.1 ProtoFuzz的特性
    16.1.1 包结构
    16.1.2 捕获数据
    16.1.3 解析数据
    16.1.4 模糊变量
    16.1.5 发送数据
    16.2 必要的背景知识
    16.2.1 错误检测
    16.2.2 协议驱动程序
    16.3 ProtoFuzz的开发
    16.3.1 开发语言的选择
    16.3.2 包捕获库
    16.3.3 设计
    16.4 实例研究
    16.5 益处和改进的余地
    16.6 小结
    第17章 Web浏览器模糊测试
    17.1 什么是Web浏览器模糊测试
    17.2 目标
    17.3 方法
    17.3.1 测试方法
    17.3.2 输入
    17.4 漏洞
    17.5 错误检测
    17.6 小结
    第18章 Web浏览器的模糊测试:自动化
    18.1 组件对象模型的背景知识
    18.1.1 在Nutshell中的发展历史
    18.1.2 对象和接口
    18.1.3 ActiveX
    18.2 模糊器的开发
    18.2.1 枚举可加载的ActiveX控件
    18.2.2 属性,方法,参数和类型
    18.2.3 模糊测试和监视
    18.3 小结
    第19章 内存数据的模糊测试
    19.1 内存数据模糊测试的概念及实施该测试的原因
    19.2 必需的背景知识
    19.3 究竟什么是内存数据模糊测试
    19.4 目标
    19.5 方法:变异循环插入
    19.6 方法:快照恢复变异
    19.7 测试速度和处理深度
    19.8 错误检测
    19.9 小结
    第20章 内存数据的模糊测试:自动化
    20.1 所需要的特性集
    20.2 开发语言的选择
    20.3 Windows调试API
    20.4 将其整合在一起
    20.4.1如何实现在特定点将"钩子"植入目标进程的需求
    20.4.2如何来处理进程快照和恢复
    20.4.3如何来选择植入钩子的点
    20.4.4如何对目标内存空间进行定位和变异
    20.5你的新的最好的朋友PYDBG
    20.6 一个构想的示例
    20.7 小结
    第三部分
    第21章 模糊测试框架
    21.1 模糊测试框架的概念
    21.2 现有框架
    21.2.1 ANTIPARSER
    21.2.2 DFUZ
    21.2.3 SPIKE
    21.2.4 PEACH
    21.2.5 通用模糊器(GeneralPurpose Fuzzer)
    21.2.6 AUTODAF?
    21.3 定制模糊器的实例研究:SHOCKWAVEFLASH
    21.3.1 SWF文件的建模
    21.3.2 生成有效的数据
    21.3.3 对环境进行模糊测试
    21.3.4 测试方法
    21.4模糊测试框架SULLEY
    21.4.1 SULLEY目录结构
    21.4.2 数据表示
    21.4.3 会话
    21.4.4
    21.4.5 一个完整的实例分析
    21.5 小结
    第22章 自动化协议解析
    22.1 模糊测试存在的问题是什么
    22.2 启发式技术
    22.2.1 代理模糊测试
    22.2.2 改进的代理模糊测试
    22.2.3 反汇编启发式规则
    22.3 生物信息学
    22.4 遗传算法
    22.5 小结
    第23章 模糊器跟踪
    23.1 我们究竟想要跟踪什么
    23.2 二进制代码可视化和基本块
    23.2.1 CFG
    23.2.2 CFG示例
    23.3 构造一个模糊器跟踪器
    23.3.1 刻画目标特征
    23.3.2 跟踪
    23.3.3 交叉引用
    23.4 对一个代码覆盖工具的分析
    23.4.1 PSTALKER设计概览
    23.4.2 数据源
    23.4.3 数据探查
    23.4.4 数据捕获
    23.4.5局限性
    23.4.6 数据存储
    23.5 实例研究
    23.5.1 测试策略
    23.5.2 测试方法
    23.6 益处和改进的余地
    23.7 小结
    第24章 智能故障检测
    24.1 基本的错误检测方法
    24.2 我们所要搜索的内容
    24.3 选择模糊值时的注意事项
    24.4 自动化的调试器监视
    24.4.1 一个基本的调试器监视器
    24.4.2 一个更加高级的调试器监视器
    24.5 
    24.6 动态二进制插装
    24.7 小结
    第四部分
    第25章 汲取的教训
    25.1 软件开发生命周期
    25.1.1 分析
    25.1.2 设计
    25.1.3 编码
    25.1.4 测试
    25.1.5 维护
    25.1.6 在SDLC中实现模糊测试
    25.2 开发者
    25.3 QA研究者
    25.4 安全问题研究者
    25.5 小结
    第26章 展望
    26.1 商业工具
    26.1.1 安全性测试工具beSTORM
    26.1.2 BREAKINGPOINT系统BPS-1000
    26.1.3 CODENOMICON
    26.1.4 GLEG PROTOVER PROFESSIONAL
    26.1.5 安全性测试工具MU-4000
    26.1.6 SECURITY INNOVATION HOLODECK
    26.2 发现漏洞的混合方法
    26.3 集成的测试平台
    26.4 小结

    作者序

    安全漏洞是研究安全问题的生命线。无论是执行渗透测试、评价新产品还是审核关键构件的源代码--安全漏洞都驱动着我们的决策、让我们有理由花费时间,并且很多年来一直影响着我们的选择。

    源代码审核是一种白盒测试技术,这是一种很长时间以来都流行的软件产品安全漏洞检测方法。这种方法需要审核者了解编程概念和产品功能的每一个细节,深入洞察产品的运行环境。除此之外,源代码审核还有一个显而易见的缺陷--必须首先要获得产品的源代码。

    幸运的是,除了白盒技术外,我们还可以使用不需要访问源代码的黑盒技术。模糊测试就是黑盒技术中的一种可选的方法,这种方法对于发掘那些用审核方法无法发现的产品关键安全漏洞方面被证明是成功的。模糊测试是这样的一个过程:向产品有意识地输入无效数据以期望触发错误条件或引起产品的故障。这些错误条件可以指导我们找出那些可挖掘的安全漏洞。

    模糊测试没有实际的执行规则。它是一种技术,测试的结果是这种技术的成功性的唯一度量。任意一个给定的产品都可能接受无限的输入。模糊测试技术旨在预测产品中可能存在的编程错误以及什么样的输入可能会触发错误。正因为如此,与其说它是一门科学,不如说它是一种技术。

    模糊测试可以简单到就是随意敲打键盘来输入随机数据。我的一个朋友有个3岁的儿子,它就是用这么简单的手段发现了Mac SO X操作系统的屏幕界面锁定功能中的一个漏洞。我的朋友锁定了屏幕界面然后到厨房找酒喝。当他回来的时候,他的儿子已经设法成功地解除了锁定,并且打开了浏览器,所用的方法正是随意敲打键盘。

    过去的几年里,我用模糊测试技术和模糊工具在大量的软件中发现了数百个漏洞。2003年12月,我编写了一个简单的程序向一个远程服务发送随机UDP 包流。结果这个程序发现了Microsoft WINS服务器的两个新的漏洞。该程序后来又帮助我在其它产品中找出了少量的缺陷。最后的结果证明,用简单的随机UPD包流能够发现计算机协会的多个产品中的漏洞,包括Norton Ghost管理服务和OS X操作系统的一个公共服务。

    模糊器对发现网络协议以及其它的许多产品都有效。在2006年的第一季度,我精心设计了3个不同的浏览器模糊工具,结果发现了多种浏览器中的缺陷。2006年第二季度,我又编写了一个Active X模糊器(AxMan),仅在Microsoft的产品中就发现了超过100个缺陷。这些缺陷许多都是在"Month of Browser Bugs"项目中形成的,结果导致该项目组又进一步开发了"Metasploit"框架中的模块。在最初开发AxMan后的接近一年的时间里,我还利用模糊测试发现了AxMan本身所包含的一些漏洞。模糊器真是一个能够不断赐予我们新礼物的工具。

    本书是一部真正让我们有理由相信模糊测试是一门技术的专著。书中所介绍的内容涵盖了对新产品执行模糊测试以及创建有效的模糊工具所需要的全部知识。有效模糊测试的关键在于明确对什么样的产品使用什么样的测试数据,以及需要什么工具来操纵、监控和管理模糊测试过程。本书的作者是模糊测试技术的先锋,在阐明模糊测试的复杂过程方面作出了卓越贡献。

    第1章 安全漏洞发掘方法学

    "Internet高速公路将会越来越少么?"

    --George W. Bush, Concord, N. H., 2004年8月5日

    如果询问任何一位有成就的安全领域的研究者,问他如何发现漏洞,很可能会得到一大堆答案。为什么?因为可用于安全性测试的方法太多,每种方法都有自己的优点和缺点。没有一种方法是绝对正确的,也没有一种方法能够揭示一个给定目标下所有可能的漏洞。在较高的层次上,有三种主要的方法用来发现安全漏洞:白盒测试、黑盒测试和灰盒测试。这些方法之间的差别是由测试者可得到的资源决定的。白盒测试是一个极端,它需要对所有资源进行充分地访问。这包括访问源代码、设计规约,甚至有可能还要访问程序员本人。黑盒测试是另一个极端,它几乎不需要知道软件的内部细节,很大程度上带有盲目测试的味道。远程Web应用的Pen测试是黑盒测试的一个好例子,它不需要访问程序源码。位于两个极端中间的是灰盒测试,它的定义因询问的人的不同而不同。就我们的应用目的而言,灰盒测试需要访问编译后得到的二进制代码,或许还要访问一些基本的文档。

    本章将考察漏洞发掘的各种不同的高层和低层方法,起点是白盒测试,你以前可能听说过这种方法也被称为玻璃、透明或半透明测试。之后我们再定义黑盒测试和灰盒测试,模糊测试可能就属于后两者。我们将阐述每种方法的利弊,这些方法的利弊将成为本书后面介绍模糊测试时所需要的背景知识。模糊测试只是漏洞发掘的一种方法,了解其它可选的实用方法也是相当重要的。

    1.1 白盒测试

    作为一种测试方法学,模糊测试(fuzzing)主要属于黑盒测试和灰盒测试领域。尽管如此,我们还是要考察另一种软件开发人员可选的漏洞发掘方法,即白盒测试(whit box testing)。

    1.1.1 源代码评审

    源代码评审既可以人工完成也可以在自动化工具的辅助下完成。假设计算机程序通常包含数万到数十万行代码,那么单纯的人工评审一般情况下是不可行的。自动化工具是一个宝贵的资源,它能够减少长时间对着代码行阅读而带来的繁重任务,但是自动化工具只能识别出可能的漏洞或可疑的代码片段。检测出的问题是否有效,仍然需要人工分析。

    让源代码分析工具产生有用的结果,必须要克服许多障碍。对这些复杂问题的剖析已经超出了本书的范围,让我们考虑如下的一个C程序代码段,这段程序只是简单地将文本"test"拷贝到一个10字节的字符数组。

    代码

    下面,考虑同样的代码示例,该段代码示例经过了修改,允许用户的输入被拷贝到字符数组。

    代码

    上面的两段代码都使用了strcpy()例程将数据拷贝到一个基于栈的缓冲区。在C/C++编程中,一般不推荐使用strcpy()库函数,因为它缺少对目的缓冲区的边界检查。因此,如果程序员不小心自己编写代码而增加了边界检查代码,便可能会发生缓冲区溢出,从而将数据置于目的容器的边界之外。第一个代码例子不会发生缓冲区溢出,因为字符串"test"(包括后跟的null终止标记)的长度总是固定值5,因此小于目标缓冲区的10字节。第二个例子中的场景中可能会也可能不会发生缓冲区溢出,这要取决于用户在命令行中输入的参数数据长度。这里的关键问题是用户是否能够控制输入给有可能发生漏洞的函数的参数长度。

    一次彻底的代码评审可能会将strcpy()所在的一行标记为"可能产生漏洞"。尽管如此,还是需要跟踪该函数的实际输入参数,以理解可被人为利用的执行条件是否真正存在。

    这并不是说源代码评审对安全领域的研究者不是一个有价值的技术工具。源代码评审应该在可获得代码的任何时候进行。然而,是否要进行源代码评审取决于你的角色和观点,通常人们不需要在如此细节的层次上访问被测对象。

    人们经常不正确地假设白盒测试是比黑盒测试更有效的方法。对软件的何种审视能够比访问源代码更好、更精确呢?不过无论如何都不要忘记,对源代码来说,你看到的东西并不一定是实际执行的东西。软件构建过程在从源代码到汇编代码的转换中可能会发生很大的改变。因为这个原因和其它的原因,不能说一种测试方法就一定比另一种测试方法更好。它们仅仅是不同的方法,通常会揭示不同类型的漏洞。为了达到充分覆盖的目的,有必要结合多种测试方法。

    微软源代码泄露

    为了说明源代码评审未必一定优于黑盒测试,让我们考虑和分析发生在2004年2月的一个事件。在没有事先告戒的情况下,有传闻说Microsoft Windows NT 4.0和Windows 2000操作系统提供的部分源代码文档被传播到互联网上。Microsoft后来确认这些文档是真实的操作系统代码。许多公司当时感到极度紧张,因为这一泄露事件将导致上述两个操作系统产生大量的安全漏洞。但是他们所担心的事情没有发生。事实上,直到今天,只有很少的安全漏洞被归因于当时的源代码泄露。其中的一个安全漏洞是CVE-2004-0566,其具体细节是当操作系统在处理位图文件时会产生一个整数值溢出1。有趣的是,Microsoft否认这个安全漏洞的发现,声称其所属的软件公司在一次内部审核中早已经发现了这个问题2。为什么我们没有发现大量的安全漏洞?不是说源代码评审可以发现一切吗?事实是源代码评审尽管是应用程序或操作系统安全性审核的一个重要部分,但是由于代码的规模和复杂性,源代码评审难以充分地进行。此外,反汇编分析技术也可以发现大部分源代码中的问题。例如,让我们考察一下TinyKRNL3和ReactOS4项目,它们的目的都是为应用软件提供与Microsoft Windows内核及其操作系统的兼容性。这些项目的开发者并没有访问Microsoft的内核源代码,但是仍然能够在一定程度上创建项目,提供一个兼容Windows的环境。在审核Windows操作系统时,不大可能会直接访问Windows源码,相反,上述项目的源代码可以被作为解释Windows汇编代码的指南。

    1.1.2 工具和自动化

    源代码分析工具一般可以分为三类--编译时检查器、源代码浏览器或自动源代码审核工具。编译时检查器在源代码编译时查找漏洞。此类工具通常与编译器集成在一起,但是主要查找与安全性有关的问题而不是应用程序的功能问题。Microsoft Visual C++的/analyze编译选项是一个编译时检查器的例子。Microsoft还提供了PREfast for Drivers6,它能够检测针对驱动程序开发的不同类型的漏洞,而编译器可能检测不到这些漏洞。

    源代码浏览器是专门设计用于辅助人工检查和评审源代码的软件工具。这类工具允许评审者执行代码的高级搜索、枚举代码以及在代码的交叉引用位置之间导航。例如,一位评审者可能会使用这样的工具来定位所有strcpy()调用的位置,力图识别可能的缓冲区溢出漏洞。Cscope7和Linux Cross-Reference8是当前流行的源代码浏览器。

    源代码自动审核工具用于扫描源代码以及自动识别可能的关注区域。同大多数安全性工具一样,源代码自动审核工具既有商业工具也有开源的自由软件解决方案。除此之外,此类工具倾向于关注具体的编程语言,因此如果目标软件使用了不同的编程语言的话,可能需要多种源代码自动审核工具。在商业软件工具方面,有来自Fortify6、Coverity10、KlockWork11、Gramma Tech12和其它供应商的产品。一些流行的自由软件工具列于表1.1,其中包括它们审核的语言以及支持的平台。

    表1.1 源代码审核的自由软件工具

    名称

    语言

    平台

    下载

    RATS(Rough Auditing Tool for Security)

    C、C++、Perl、PHP、Python

    UNIX、Win32

    http://www.fortifysoftware.com/security-resource.jsp

      本表中其余内容同原文,全部不需要翻译。

     

     

     

     

     

     

     

     

     

     

     

     

    重要的是要记住,没有任何自动化工具能够完全替代有经验的安全研究员的技能。它们只不过是工具,能够将繁重的分析源代码的任务进行流水化和自动完成,有助于节省时间和解决一些问题。这些工具生成的报告仍然必须由具备经验的分析员评审,以识别出伪问题,也需要有开发人的员评审以实际实施问题的修复。例如,下面的一段代码输出是由Rough Auditing Tool for Security(RATS)自动生成的,被分析的代码是本章前面所举的存在安全漏洞隐患的程序示例。分析结果可以表明这样一个事实:该段程序使用了固定长度缓冲区并且对strcpy()的使用可能是不安全的。但是,分析结果没有肯定地说确实存在一个漏洞,只是提醒用户注意存在可能不安全的代码区域,但是该区域是否真的不安全,则需要由他(她)决定。

    代码

    1.1.3 优点和缺点

    如同前面提到的,发现安全漏洞不存在唯一正确的方法学。那么如何选择一种合适的方法学呢?当然,有时的决策是由我们自己确定的。例如,如果我们不能访问目标软件的源代码,那么就不可能进行白盒测试。这对大部分安全研究者和用户都是一样的,尤其是那些购买了Windows环境的商业软件的人来说。那么白盒测试有什么优点呢?

    覆盖能力:由于白盒测试能够获得所有源代码,因此代码评审允许完全的覆盖。所有可能的代码路径都可以被审核,以发现可能的漏洞。当然,这个过程也可能导致误报的出现,因为一些代码路径可能在代码执行的时候不可达。

    代码评审并不总是可行的。即使能够执行代码评审,也应该和其它漏洞发掘方法学结合使用。源代码分析有下面一些缺点:

    复杂性:源代码分析工具是不完善的,有可能报告出伪问题。因此,这类工具产生的错误报告并不是游戏的结束。这些结果必须经过有经验的程序员的评审,以识别出那些能够合理代表安全性漏洞的问题。重要的软件项目典型地要包含数十万行代码,工具所产生的报告可能相当冗长并且需要大量的时间阅读。

    可用性:由于白盒测试能够获得所有源代码,因此代码评审允许完全的覆盖。尽管许多UNIX项目是开放源代码的,并且允许源代码被评审,但是这种情况在Win32环境下比较罕见,对于商业软件这种情况则通常不存在。如果不能访问源代码,那么白盒测试则根本不能作为一个测试选项。

    1.2 黑盒测试

    黑盒测试意味着只能了解外部观察到的东西。作为终端用户,可以控制输入,从一个黑盒子的一端提供输入,从盒子的另一端观察输出结果,但是并不知道被控目标的内部工作细节。在远程访问Web应用和Web服务时这种情形最常见。我们可以采用超文本置标语言(HTML)或可扩展置标语言(XML)请求的形式产生输入并观察生成的Web页面或返回值,但是并不知道幕后发生了什么。

    让我们考虑另一个例子。假设你在某个时候购买了一个应用软件,例如Microsoft Office,通常你只能得到一个经过编译后得到的二进制程序,并不能得到用于构建这个应用程序的源代码。在这种情况下,你的观点将决定测试方法的盒子的颜色。如果你不打算采用逆向工程技术,那么黑盒测试是有效的。反之,就应该采用灰盒测试方法,灰盒测试将在下一节讨论。

    1.2.1 人工测试

    仍然以Web应用为例,人工测试可能要包括使用一个标准的Web浏览器来浏览一个Web站点的层次结构,同时冗长乏味地在所观察到的感兴趣的区域输入可能导致危险的各种输入数据。在审核的最初阶段,这个技术可能只是被零星地应用,例如,通过给各种各样的参数增加引用,以期望揭示出SQL注入漏洞。

    在没有自动化工具辅助的情况下通过人工测试应用程序来查找漏洞,这通常并不是一个可行的方法(除非公司雇佣了一大批实习生)。这种方法能够带来益处的场景是它被用于扫除(sweeping)多个应用程序中的类似的一个漏洞时。当多个程序员在不同的项目中犯了相同的错误时,就需要利用这种扫除技术。例如,在一个专门的LDAP(Lightweight Directory Access Protocol)服务器中发现了一个缓冲区溢出,并且通过测试其它LDAP服务器发现同样的安全漏洞。如果我们假设程序之间经常共享代码或者同一个程序员参加过多个开发项目,那么这种情况就并非不经常出现。

    扫除技术

    CreateProcess()是Microsoft Windows 应用程序编程接口(API)提供的一个函数。正如它的名字所暗示的那样,CreateProcess()将发起运行一个新进程及其主线程13。该函数的原型是如下定义的:

    代码

    如果lpApplicationName参数被赋予一个NULL值,那么被发起执行的进程将是lpCommandLine参数中第一个空白符号分隔之后的值,这是人们都知道并且记录在文档中的该函数的行为。考虑下面的例子中对CreateProcess()函数的调用:

    代码

    在上情形下,CreateProcess()函数将试图反复发起每一个空格符之后的值所代表的进程:

    代码

    这种尝试会一直进行,直到发现了一个可执行文件的名字或者所有的选择都已经被穷尽。因此,如果一个可执行文件program.exe位于C:\路径下,带有类似上述结构中对CreateProcess()不安全调用的应用程序将会执行该program.exe程序。这为攻击者提供了一个便利的机会,他们会试图让实际要执行的程序不可达并执行另外一个程序。

    2005年10月发布的一个安全性咨询报告中列举了好几个流行的应用程序中采用了不安全的CareateProcess()调用。这些问题的发现得益于一次成功的但是却非常简单的扫除技术实践。如果你想要利用该扫除技术发现相同的安全漏洞,那么可以拷贝和重新命名一个简单的应用程序,例如notepad.exe,然后将它置于c:\路径下。然后正常使用计算机。如果先前拷贝的这个应用程序突然在预期之外被执行了,那么就很可能发现了一个因不安全调用CreateProcess()而引起的漏洞。

    1.2.2 自动测试或模糊测试

    模糊测试在很大程度上是一种强制性的技术,因此它缺乏"优雅性",它的目标是简单加有效。模糊测试这个术语将在第2章"什么是模糊测试"中详细定义和解释。简单地说,模糊测试包括把能够想到的所有东西都抛给被测目标然后监视结果。大部分软件都应该被设计成带有具体输入并且足够健壮,在接收到异常输入的时候能够完美地恢复。考虑如下所示的一个简单的Web表单。

    合理的假设是Name域应该接受一个字符串值,Age域应该接受一个整数值。如果用户偶然改变了两个域的实际输入范围并且在Age域输入了一个字符串后会发生什么呢?字符串值会被自动转换为基于ASCII码的整数值吗?是否会显示一条错误报告消息?应用程序会崩溃吗?模糊测试试图通过一个自动化的过程来回答这个问题。研究者不需要知道应用程序的内部工作细节,因此执行的是黑盒测试。要做的就是站在后面向目标投掷石头,等待玻璃被打破的声音。就这个意义而言,模糊测试可被归结为黑盒测试。然而,在本书中,我们将说明,为什么强制性的模糊测试可以变得更像"外科手术",从而让石头每次的飞行路线更直接并且更真实。因此,模糊测试也可以应用在灰盒测试中。

    Microsoft使用模糊测试吗?

    答案是肯定的。Microsoft2005年3月发布的可信计算安全开发生命周期(SDL)文档15清楚地阐明了Microsoft考虑将模糊测试作为一种在软件发布之前寻找安全漏洞的关键技术。SDL是一个关于在软件开发生命周期中嵌入安全性的倡议,认为安全性是参与开发过程的每一个人的职责。模糊测试在SDL中被提及为是一类安全性测试工具,应该在软件实现阶段加以利用。实际上,这个文档中说到:"尽管对模糊测试的强调只是最近才被加入到SDL,但是到目前所取得的效果却非常鼓舞人心"。

    1.2.3 优点和缺点

    黑盒测试尽管并不总是最好的方法,但却总是一个可选的方法。黑盒测试的优点包括以下几个方面:

    可用性:黑盒测试总是可以应用的,甚至在源代码可用的情况下应用黑盒测试也是有益处的。

    可重现性:由于黑盒测试的实施不需要事先对目标做出假设,以文件传输协议(FTP)服务器为例,黑盒测试可以很容易地被定制成能够测试任何其它FTP服务器

    简单性:一些测试方法,例如代码逆向工程(RCE)等,需要专业的技巧,而黑盒测试只是测试的一个最基本层次,它能够在不十分了解应用程序内部细节的情况下执行。尽管如此,事实上,尽管通过简单地使用测试工具就能够发现诸如拒绝服务攻击这样的安全漏洞,但是判断一次简单的应用程序崩溃是否能够被关联到某些让人更感兴趣的地方(例如代码的执行),则通常需要对软件的深入理解。

    尽管黑盒测试容易使用,但是也有一些缺点。黑盒测试的缺点包括:

    覆盖能力:如何确定测试何时结束以及测试的有效性程度,这是黑盒测试面临的最大挑战之一。这个问题将在本书的第23章"模糊器跟踪"中详细讨论。

    理解力:黑盒测试最适合那些安全漏洞由一个单独的输入向量所引起的场景。然而,复杂的攻击需要多种攻击向量,其中的一些攻击将目标应用程序置入一种脆弱状态,其它攻击向量进一步触发漏洞。此类攻击需要深刻理解应用程序的底层逻辑,并且典型地需要通过人工代码评审和RCE才能发现漏洞。

    1.3 灰盒测试

    灰盒测试的定义是在白盒测试和黑盒分析之间浮动的,我们给它下的定义是首先它包括了黑盒测试审核,此外还包括通过逆向工程(RE)获得的结果,逆向工程也被称为逆向代码工程(RCE)。源代码是宝贵的资源,它应该容易被阅读并且支持人们理解软件具体功能的操作细节。此外,它还隐含地提示了具体的功能所预期需要的输入,以及该具体功能的预期输出。即使缺少源代码这样的重要资源,也不意味着我们会失去一切。分析编译后得到的汇编指令能够帮助阐明类似的故事,但是要付出更多的努力。在汇编代码层次上进行安全评估而不是在源代码层次上进行安全评估,这种安全评估典型地被称作二进制审核(binary auditing)。

    1.3.1 二进制审核

    RCE通常与二进制审核这个词汇被当作同义词而使用,但就我们的目的而言,我们将RCE作为一类子方法,以使它区别于完全自动化的方法。RCE的最终目标是决定一个编译后得到的二进制应用程序的底层功能。尽管将一个二进制文件完全转换回它的源代码形式是不可能的,但是有可能对汇编指令序列应用逆向工程,从而得到一种位于源代码和构成二进制代码的机器码之间的一种表示形式。通常,这种表示是汇编语言和应用程序代码流的图形表示之间的一种中间形式。

    一旦二进制文件被转换成人可读的形式,这样的代码就可以被评审以查找其中可能包含漏洞之处,这在很大程度上与源代码的评审是类似的。就源代码评审而言,找到可能存在漏洞的代码片段并不是游戏的结束。还必须决定一个终端用户是否能够影响脆弱的代码片段。遵循这样的逻辑,二进制审核也被人们称为是一种"从里向外"的技术:研究者首先识出反汇编结果中令其感兴趣的可能存在的漏洞,然后反向追溯到源代码中以确定漏洞是否可以被别人所利用。

    逆向工程(reverseengineering)是一种外科手术式的技术,它利用诸如反汇编器、反编译器或调试器这样的工具。反汇编器将难以被辨认的机器码解析为某种更适合人理解的汇编码。有各种自由的反汇编器软件可供使用,但是出于重要的RCE工作的要求,我们很可能需要花费一些投资来购买DataRescue的Interactive Disassembler(IDA) Pro16。IDA是一个运行在Windows、UNIX和MacOS平台上的商用反汇编器,能够解析很多不同机器体系架构下的二进制代码。


    同反汇编器类似,反编译器可对二进制代码执行静态分析并将其转换为人可读的源代码形式。反编译器试图产生更高级的语言组件,例如条件和循环结构,而不是直接将机器码转变为汇编指令。反编译器不可能完全恢复产生最初的源代码,因为包含在源代码中的一些信息,例如注释、变量名、函数名甚至基本结构在编译之后都不再被保存。对于将源代码直接翻译成机器码的编译型语言(例如C和C++),它们的反编译器在本质上存在一些限制。对于那些将源代码编译为某种中间形式的字节码的语言,如C#,由于其字节码中包含更多的信息,因此对这些语言的反编译通常要成功的多。

    与反汇编器和反编译器不同,调试器通过打开或附加到一个目标程序并采用动态分析技术来监控目标程序的执行。调试器能够显示应用程序正在运行时CPU寄存器的内容和内存状态。Win32平台下的流行调试器包括OllyDbg18,其运行时的一个屏幕快照。此外还有Microsoft WinDbg(也被人称做"wind bag")19。WinDbg是Windows软件调试工具包20中的一部分,可从Microsoft的网站上免费下载。OllyDbg是一个由Oleh Yuschuk开发的调试器,用户友好性稍好于WinDbg。这两个调试器都允许用户创建自定制的扩展功能组件,有许多第三方插件可用于扩展OllyDbg的功能21。UNIX环境下也有各种各样的调试器,GNU Project Debugger22(GDB)是最流行的也是最容易被移植的调试器。GDB是一个命令行调试器,许多UNIX/Linux产品中都包含这个调试器。

    1.3.2 自动化的二进制审核

    最近出现了少量的工具,这些工具试图对RCE的过程实现自动化,以识别二进制应用程序中潜在的安全漏洞。这些工具既有商业软件,也有自由软件,它们或者是IDA Pro的插件或者是单独开发的应用程序。表1.2列举了它们中的一小部分。

    表1.2 自动化二进制审核工具

    名称

    开发商

    许可类型

    说明

    LogiScan

    LogicLibrary

    商业软件

    LogicLibrary在2004年9月收购了BugScan,此后对后者的二进制审核工具的品牌重新进行了命名,并将其包含到Logidex SDA管理方案中。

    BugScam

    Halvar Flake

    自由软件

    BugScam是一个IDA Pro的IDC脚本集合,它枚举了二进制文件中的函数调用,以识别出对各种不同库函数可能的不安全调用。该应用程序主要是在BugScan的基础上采用“哄骗”的手段发现安全漏洞。

    Inspector

    HB Gary

    商业软件

    Inspector是一个RCE管理系统,它统一了来自各种不同RCE工具的输出,例如IDA Pro和OllyDbg。

    SecurityReview

    Veracode

    商业软件

    VeraCode的产品将一个二进制分析套件直接集成进开发环境,类似于源代码分析套件的功能,如Coverity。在二进制代码层次上进行分析,使得VeraCode能够检测出一些问题,这些问题与“所看到的并不一定是所执行的”有关。

    BinAudit

    SABRE Security

    商业软件

    BinAudit在本书出版之前还没有正式发布。尽管如此,根据SABRE Securite的Web站点上的介绍,它是一个IDA Pro的插件,被设计用来识别一些安全漏洞,例如数组访问越界、内存破坏、内存泄露等。

    1.3.3 优点和缺点

    如前所述,灰盒测试在某种程度上是一种混合的解决方案,它包括传统的黑盒测试,外加通过RCE获得的洞察结果。同其它方法一样,灰盒测试也有自己的优点和缺点。灰盒测试的优点有:

    可用性:除了远程Web服务和应用程序之外,应用程序的二进制版本一般总是可用的。

    覆盖:从灰盒测试分析获得的信息可被用于帮助和改进采用黑盒测试的模糊测试技术。

    灰盒测试同时也有以下的缺点:

    复杂性:RCE是一个专业的技能,因此它需要的资源有可能不可用,从而导致这种方法无法使用。

    1.4 小结

    在最高的层次上,安全漏洞发掘方法可被分为白盒、黑盒和灰盒测试方法三大类。测试者可以获得的资源决定了这三种方法的差别。白盒测试需要使用所有可用的资源,包括源代码,而黑盒测试只访问软件的输入和观察到的输出结果。介于两者之间的是灰盒测试,它在黑盒测试的基础上通过对可用的二进制文件的逆向工程而获得了额外的分析信息。

    白盒测试包括各种不同的源代码分析方法。可以人工完成也可以通过利用自动化工具完成,这些自动化工具包括编译时检查器、源代码浏览器或自动源代码审核工具。

    在执行黑盒测试时,源代码是不可用的。仅仅通过黑盒测试来执行模糊测试,这被认为带有盲目性。尽管能够生成输入,也可以监控输入所对应的响应,但是没有办法获知和分析目标软件的内部状态。基于灰盒测试的模糊测试类似于基于黑盒测试的灰盒测试,但是增加了由RCE获得的数据。模糊测试企图反复为应用程序提供非预期的输入,同时监控可能产生的异常结果。本书的剩余部分将将关注模糊测试,将其作为一种发现安全漏洞的方法。

    第2章 什么是模糊测试

    "他们不正确地低估我。"

    --George W. Bush,Bentonville,Ark,2000年11月6日

    在主流的词典中,模糊测试(fuzzing)这个术语并不存在,同时有许多别名,对许多读者来说它很可能是一个完全陌生的名词。模糊测试是一个宽泛的研究领域,同时也是一种令人激动的软件安全性分析方法。贯穿本书,我们将深入讨论不同的模糊测试方法的细节和目标。不过在详细介绍模糊测试之前,我们先要给模糊测试下一个定义,考察模糊测试的历史,阐述模糊测试的各个阶段,最后再指出模糊测试的局限性。

    2.1 模糊测试的定义

    如果在你的词典中查找"fuzzing"这个词,恐怕很可能找不到任何能够为安全性领域的研究者所用的定义。Wisconsin-Madison大学的一个研究项目中首次公开提及了这个术语,现在这个术语已经被广为采纳,成为一种完整的软件测试方法学的代名词。在学术界,与模糊测试最接近的专业术语可能是边界值分析(boundary value analysis, BVA)1,在边界值分析中,需要考察给定输入的合法值的边界,然后以边界内和边界外的合法和非法值来创建测试。

    BVA可以帮助确保异常处理机制能够过滤掉非预期的值,同时允许最大范围的可接受的输入值。模糊测试类似于VBA,但是在执行模糊测试时,我们并不仅仅唯一关注边界值,而是需要额外关注任何可能引发未定义或不安全行为的输入。

    出于本书的写作目的,我们将模糊测试定义成是一种通过提供非预期的输入并监视异常结果来发现软件故障的方法。模糊测试典型地是一个自动的或半自动的过程,这个过程包括反复操纵目标软件并为其提供处理数据。当然,这是一个相当泛化的定义,但是这个定义抓住了模糊测试的最基本概念。所有的模糊器都可分被分为两大类:基于变异的模糊器,这种模糊器对已有数据样本应用变异技术以创建测试用例;基于生成的模糊器,这种模糊器通过对目标协议或文件格式建模的方法从头开始产生测试用例。这两大类模糊器分别有各自的优点和缺点。在本书的第3章"模糊测试方法和模糊器类型"中,我们将对模糊测试方法学做更进一步的分类并讨论每一类方法的好处和易犯的错误。

    如果你是一位模糊测试的新手,那么可以将模糊测试类比为如何闯进一幢房子。假设你在当前的职业生涯中遭受了挫折,进而转入从事违法犯罪活动。为了破门进入某人的家,假设采用纯白盒的方法,那么在实施破门之前应该能够得到对所有关于这个家的充分信息。这些信息可能要包括房屋设计图、各种锁的制造商列表、房屋建筑材料的详情,等等。尽管这种方法有独一无二的优点,但是也并非万无一失而没有短处。在这种方法下,执行破门的过程中,你要做的不是在实际执行时去检查房屋的设计而是要对房屋的设计执行静态分析。让我们打个比方,例如,事先研究表明起居室的侧面窗户代表了一个弱点,可以砸破这面窗户然后破门而入,如果是这样的话,那你肯定不希望到时候有一个拿着鸟枪的愤怒房主站在里面正在等着你。另一方面,如果采用一种纯的黑盒测试方法来完成破门的话,那么应该在黑夜的掩盖下逐步靠近这个房子,安静地尝试所有的门和窗户是否有漏洞,向房子内窥视以决定哪里可能是最好的突破口。最后,如果选择采用模糊测试来完成突破的话,便可以不必研究设计图更不用人工测试各种锁。要做的就是让找枪和破门而入的过程自动化--这就是强制性的漏洞发掘!

    2.2 模糊测试的历史

    我们所知道的对模糊测试的最早的引用可以追溯到1989年。Barton Miller教授(被很多人认为是模糊测试之"父")和他的高级操作系统课开发和使用了一个原始的模糊器,用来测试UNIX应用程序的健壮性2。测试的关注点并不是评价系统的安全性,而是验证全部代码的质量和可靠性。尽管在该项研究的过程中提及到了安全性考虑的问题,但是在对setuid应用程序的测试中并没有专门强调这一点。1995年的研究发现模糊测试让setuid应用程序的可靠性得到了改进,但是该应用程序仍然被认为"具有较高的失效概率"。

    Miller的小组所用的模糊测试方法是非常粗糙的。如果应用程序崩溃或者挂起,那么就认为测试失败,否则就认为测试通过。所用的方法是简单地向目标应用程序抛出随机字符串,这是一种纯黑盒的方法。尽管这种方法看上去可能过于简单,但是不要忘了,在那个时候甚至还没有人听说过模糊测试的概念。

    大约在1999年,Oulu大学开始了它们的PROTOS测试集的开发工作。通过首先分析协议规约然后产生违背规约或者很有可能让协议实现体无法正确处理的报文,各种各样的测试集先后被开发出来。一开始时,产生这样的测试集需要耗费相当大的努力,但是一旦产生之后,这些测试集就可以用来测试多个供应商的产品。这个例子属于白盒和黑盒混合的测试方法,标志着模糊测试发展历程中的一个重要里程碑,由于后来证明大量的故障是用这样的方法发现的。

    2002年Microsoft为PROTOS3提供了资金支持,2003年PROTOS组的成员发起了Codenomicon,这是一个专门致力于设计和产生商业用模糊测试集的公司。该公司今天的产品仍然基于最初的Oulu测试集,不过又包括了一些图形用户界面、用户支持以及通过健康特征检测而实现的故障检测能力,此外还增加了其它一些特征4。关于Codenomicon和其它商业模糊测试解决方案的更多信息,请阅读本书的第26章"展望"。

    随着PROTOS在2002年逐渐成熟,DaveAitel发布了一个开放源代码的模糊器,名为SPIKE5,采用了GNU的许可(GPL)。Aitel的模糊器实现了一种基于块的方法6,目的是为了测试基于网络的应用程序。SPIKE采用了一种比Miller的模糊器更高级的方法,最显著的特征是包括了描述可变长数据块的能力。此外,SPIKE不仅能够生成随机数据,而且包括了一个数据值的库,库中的数据值有较大可能性让编写较差的应用程序产生故障。SPIKE还包括一组预定义的函数,这些函数能够产生常见的协议和数据格式,其中包括Sun RPC和Microsoft RPC。它们是两个常见的通信协议,过去发现的许多安全漏洞都起源于它们。作为第一个允许用户无痛创建自定制模糊器的公开框架,SPIKE的发布标志着一个重要的里程碑。这个框架在本书中将被反复提及多次,包括本书的第21章"模糊测试框架"。
    大致在SPIKE被发布的几乎同一时间,Aitel还发布了一个名为sharefuzz的本地UNIX模糊器。与Miller的模糊器不同,Sharefuzz以环境变量为目标而不是以命令行参数为模糊测试目标。Sharefuzz还采用了一个有用的技术来促进模糊测试过程。它使用共享库来挂起函数调用,这些函数调返回环境变量值,它们是一些长字符串,为的是发现缓冲区溢出漏洞。

    在SPIKE发布之后的大多数模糊测试技术是以不同类型的具体模糊测试工具出现的。Michal Zalewski7(又名lacamtuf)于2004年发布的mangleme8关注Web浏览器的模糊测试,mangleme是一个公共网关接口(CGI)脚本,可用来持续产生畸形的、在一个目标Web浏览器中被反复刷新的HTML文件。随后很快又出现了几个以Web浏览器为测试目标的模糊器。H.D. Moore和Aviv Raff开发了Hamachi9,用来对动态HTML脚本进行模糊测试以查找其实现缺陷,他们两个后来与Matt Murphy和Thierry Zoller合作又推出了CSSDIE10,这是一个对重叠样式表进行解析的模糊器。

    文件模糊测试的兴起始于2004年。那一年Microsoft发布了MS04-28安全公告,详细描述了负责处理JPEG文件的引擎中的一个缓冲区溢出漏洞。尽管这并不是第一个被发现的文件格式方面的安全漏洞,但是它还是吸引了人们的注意力,因为许多流行的Microsoft应用程序都共享这段有漏洞的代码。文件格式安全漏洞对网络的保护者也提出了挑战。尽管相当数量的类似漏洞在随后几年相继出现,但是如果我们仅仅阻止有可能存在漏洞的文件格式进入公司的网络,那样显然是不切实际的。图像和媒体文件构成了Internet流量中相当大的一部分。如果Web离开了图像的话,那将是多么令人振奋的新闻?此外,这些漏洞中的大多数在不久后就会折磨Microsoft的Office文档文件--这些文件类型对任何企业都至关重要。文件格式安全漏洞最后成为基于变异的模糊测试要发现的主要侯选漏洞,因为漏洞的样品容易被获得,并且文件格式可以在监视目标应用程序的故障的同时被快速地连续变异。我们在Black Hat USA Briefings in 200512上做了陈述并发布了一系列同时基于变异和生成的工具,用于对文件格式进行模糊测试,包括FileFuzz、SPIKEfile和notSPIKEfile13。

    2005年,一个名为Mu Security的公司开始开发一种硬件模糊装置,其目的是让网络中传输的协议数据发生变异14。这个商业产品供应商的出现,恰好吻合了当前人们关注模糊测试的潮流。我们开始看到日益增加的可用的商业模糊测试解决方案,同时伴随着模糊测试技术的自由软件解决方案的出现。另外,大量的开发者和安全领域的研究人员开始对模糊测试感兴趣,一个明显的证据是他们创建了模糊测试邮件列表15,该邮件列表由Gadi Evron维护。只有时间才能说明未来将会有什么样的创新在等待着我们。

    ActiveX模糊测试在2006年开始流行,当年David Zimmer发布了COMRaider,H.D.Moore发布了AxMan16。这两个工具关注那些在访问Microsoft Internet Exploer浏览器时能够被Web应用程序实例化的ActiveX控件。此类应用程序中的可被人远程利用的漏洞代表了一种高风险,因为此类应用程序用户群体十分庞大。就象人们最后发现的那样,ActiveX控件是一种优秀的模糊测试目标,由于其中包括了接口、函数原型和实际的控制逻辑,从而允许对其实施自动化的智能测试。有关ActiveX和浏览器的模糊测试将在第17章"Web浏览器模糊测试"和第18章"Web浏览器模糊测试:自动化"中做更进一步的研究。

    尽管模糊测试在历史上还有许多其它里程碑和标志节点,不过我们认为前面介绍的内容已经足够绘制一幅综合的历史发展路线图。模糊测试的简要发展历史的图形表示参见图2.1。

    译文:

    Oulu University begins work on PROTOS Test Suites:Oulu大学开始PROTOS测试集的工作

    SPIKE demonstrated at BH USA by Dave Aitel:DaveAitel现在BH USA展示SPIKE

    FileFuzz, SPIKEfile, notSPIKEfile released at BH USA:FileFuzz,SPIKEfile, notSPIKEfile在BH USA发布

    Commercial Fuzzers(i.e. Codenomicon, Mu Security, etc.) introduced:商业模糊器(例如Codenomicon,Mu Security等)被引入市场

    COMRider(David Zimmer) and AxMan(H.D. Moore) ActiveX fuzzersreleased:COMRider(David Zimmer)和AxMan(H.D. Moore)等ActiveX模糊器发布

    Professor Barton Miller uses Fuzzing to test Robustness of UNIXApps at UW-Madison 1989:Barton Miller教授在UW-Madison使用模糊测试方法来测试UNIX应用程序的健壮性

    Oulu Univer releases PROTOS SNMP Test Suite:Oulu大学发布PROTOSSN MP测试集

    Mangleme released by Michael Zalewski(aka lcamtuf):MichaelZalewski(又名lcamtuf)发布Mangleme

    Hamachi and CSSDIE released by HD Moore, Aviv Raff, et. al:HD Moore、AvivRaff等发布Hamachi和CSSDIE

    注:上述文字中未包括年号。

    尽管到目前为止取得了一定的进展,但模糊测试这项技术仍然是襁褓中的婴儿。到现在为止大部分工具仍然是相对较小的项目,由几个人组成的小组甚至是一名程序员维护。仅仅在最近几年,一些早期的研究成果才开始步入商业发展阶段。尽管这些发展让人们对模糊测试已经成为独立的研究领域增加了信心,但是也预示着模糊测试未来将会有许多革新和发展,随着未来的几年人们对这一领域投入的日益增加,模糊测试还会达到新的里程碑。

    在下一节,我们将考察执行一次完整的模糊测试审核所要经历的几个不同阶段。

    2.3 模糊测试阶段

    模糊测试方法的选择依赖不同的因素,可能有很大的变化。没有一种绝对正确的模糊测试方法。模糊测试方法的选择完全取决于目标应用程序、研究者的技能,以及需要测试的数据所采用的格式。然而,无论要对什么进行测试,也不论确定选择了哪种方法,模糊测试总要经历几个基本的阶段。

    1.识别目标。在没有考虑清楚目标应用程序的情况下,不可能对模糊测试工具或技术作出选择。如果是在安全审核的过程中对内部开发的应用程序进行模糊测试,目标应用程序的选择应该小心谨慎。相反,如果是对第三方应用程序进行安全漏洞的发掘研究,这种选择就有一定的灵活性。在识别目标应用程序的时候,需要考察开发商过去被发现的安全漏洞的相关历史。这可以通过浏览安全漏洞收集站点来完成,例如SecurityFocus17或Secunia18。一个开发商如果在过去的安全漏洞历史记录方面表现不佳,那么很可能有比较差的编码习惯,最终将会导致更多的安全漏洞被进一步发现。选择了目标应用程序之后,还必须选择应用程序中具体的目标文件或库。如果确要选择目标文件或库,应该选择那些被多个应用程序共享的库,因为这些库的用户群体较大,出现安全漏洞的风险也相应较高。

    2.识别输入。几乎所有可被人利用的漏洞都是因为应用程序接受了用户的输入并且在处理输入数据时没有首先清除非法数据或执行确认例程。枚举输入向量对模糊测试的成功至关重要。未能定位可能的输入源或预期的输入值对模糊测试将产生严重的限制。在查找输入向量同时应该运用水平思考。尽管有一些输入向量是很明显的,但是其它一些则难以捉摸。最后要说的是,任何从客户端发往目标应用程序的输入都应该被认为是输入向量。这些输入包括消息头、文件名、环境变量、注册键值等等。所有这些都应该被认为是输入向量,因此都应该是可能的模糊测试变量。

    3.生成模糊测试数据。一旦识别出输入向量,模糊测试就必须被生成。如何使用预先确定的值、如何变异已有的数据或动态生成数据,这些决策将取决于目标应用程序及其数据格式。不管选择了哪种方法,这个过程中都应该引入自动化。

    4.执行模糊测试数据。这一步与前一步并行进行,在这一步,模糊测试成为一个动词。执行过程可能包括发送数据包给目标应用程序、打开一个文件或发起一个目标进程。同样,这个过程中的自动化也是至关重要的。没有自动化,我们便无法执行真正的模糊测试。

    5.监视异常。在模糊测试过程中,一个至关紧要但却被经常忽视的步骤是对故障或异常的监视过程。举一个例子,如果我们没有办法准确指出是哪一个数据包引起崩溃的话,那么向目标Web服务器发送10000个模糊测试数据包并最终导致服务器崩溃便是一次无用的努力。监视可以采用多种形式并且应该不依赖目标应用程序和所选择的模糊测试类型。

    6.确定可利用性。一旦故障被识别,因审核的目标不同,还可能需要确定所发现的bug是否可被进一步利用。这典型地是一个人工过程,需要具备安全领域的专业知识。因此执行这一步的人可能不是最初执行模糊测试的人。

    不管采用什么类型的模糊测试,所有上述阶段都应该被考虑到,只有确定可利用性这一步有可能例外。各个阶段的顺序和侧重点可依据研究者的目标而改变。尽管模糊测试非常强大,但绝不意味着它对任何被测软件都将发现百分之百的错误。在下一节,我们将罗列一些bug的类型,这些类型的bug很有可能在"雷达的监视下飞行"。

    2.4 模糊测试的局限性和期望

    模糊测试本质上有助于发现目标软件中一些类型的弱点。同时它对发现某些类型的漏洞也存在一定的局限性。在这一节里,我们将给出几类典型的漏洞,这些漏洞无法被模糊器所发现。

    2.4.1 访问控制缺陷

    一些应用程序需要分层的权限来支持多个帐户或用户级别。例如,考虑一个在线日历系统,它可能通过一个Web前端被访问。该应用程序可能指定一名管理员来控制谁被允许登录到系统。可能还存在另一个专门的用户组,该组能够创建日历。所有其它用户可能只有读的权限。最基本的访问控制形式是确保常规用户不能执行管理员的任务。

    模糊器可能会发现日历软件系统中的一个故障,该故障允许一个攻击者获得对系统的完全控制。在较低的层次上,软件故障通常是跨越不同目标的,因此相同的逻辑可被用于检测不同的目标。

    在测试过程中模糊器可能会成功地访问只有管理员才能访问的功能,但是它却只拥有常规用户的访问身份。这种访问控制机制被绕过的事实并不大可能会被检测出来。为什么?因为我们必须考虑到模糊器并不理解应用程序的逻辑这样一个事实。模糊器并没有办法知道管理员的访问区域是否能被常规用户所访问。在模糊器中实现应用程序逻辑感知能力并不是不可行,但是要付出极大的代价,并且在测试其它应用程序时如果不做大的修改便很可能无法被重用。

    2.4.2 设计逻辑不良

    模糊器不是用来识别设计逻辑不良的最好的工具。例如,考虑在Veritas Backup Exec中发现的漏洞,该漏洞允许攻击者远程访问Windows Server的注册表,包括创建、修改和删除注册表键值19。这样的访问几乎不会立即让系统的安全发生妥协。这个漏洞的发现归功于一个侦听服务,该侦听服务在一个传输控制协议(TCP)端点上实现了一个远程过程调用(RPC)接口,该接口不需要认证但是仍然接受操作注册表的命令。这并不是一个认证问题,因为根据软件设计规约,该过程根本不需要认证。但是,这却是一个不恰当的设计决策,该决策很有可能源自一个因为某个假设而引起的失效,该假设认为攻击者愿意花时间破解用来描述目标例程的Microsoft Interface Description Language (IDL)并随后编写了一个自定制的实用程序用来与服务器通信。

    尽管模糊器可能会发现通过RPC连接而提供的数据确实存在解析问题,从而导致某种形式的底层故障,但是却不能确定所展示出的功能是否不安全。这个观点在本书第18章"Web浏览器模糊测试:自动化"讨论ActiveX的模糊测试时将起到重要作用,因为许多控件只为攻击者提供操作方法,例如创建和执行文件的方法。检测这些设计上的bug还需要专门的考虑。

    2.4.3 后门

    对于一个模糊器来说,如果受到限制或不知道关于目标应用程序的结构信息,它的后门(backdoor)看上去和其它目标应用程序的逻辑没有什么区别,例如一个登录屏幕界面。它们都是简单的输入变量,接收身份认证秘钥。不仅如此,除非给予模糊器组的信息来让它认可成功的登录,它将没有办法通过使用一个硬编码的密码来识别一个成功的等录企图。例如,在对密码域进行模糊测试时,模糊器典型地可以发现输入的异常,但是却不能发现随机猜测的硬编码的密码。

    2.4.4 内存破坏

    内存破坏问题通常会导致目标进程崩溃。这种类型的问题可以根据与拒绝服务类似的症状而加以识别。然而,一些内存破坏问题被目标应用程序很好地屏蔽掉了,因此简单的模糊器永远不会发现它们。例如,一个格式字符串漏洞可能不会被发现,除非为目标应用程序附加一个调试器。格式字符串漏洞通常归结于机器代码层的违例指令。例如,在一个X86的Linux平台的机器中,如果格式字符串中带有一个%n的攻击字符特征,那么经常会在执行下列指令时发生故障:

    代码

    如果模糊器向带有某种格式触发字符串(例如这里或那里的%n)的进程发送随机数据,exx寄存器将在许多情况下不包含一个可写的地址,而是包含一些来自栈的垃圾字。在这种情况下,该指令将会触发一个段违例信号SIGSEGV。如果应用程序有一个SIGSEGV信号处理器,那么它将简单地杀死这个进程然后派生一个新的进程。对于某些应用程序,信号处理器甚至会企图允许过程继续执行而不需要重新启动。对于一个格式字符串漏洞,这实际上是可能的(尽管我们极力反对这么做),因在SIGSEGV信号没有发生前内存实际上并没有遭到破坏。因此,如果我们的模糊器没有检测到它又如何呢?这个进程不会崩溃,相反它会恰当通过信号处理器来处理信号,因此这里并不存在安全问题,对吗?但是,不要忘记,对格式字符串的成功利用将造成这样一个结果:在内存中写入数据并随后使用这些受到控制的内存区域获得对进程的控制权。信号处理器不能阻挡格式字符串被他人精确地利用,同样精确地利用也可能不会引起任何故障。

    因对,在模糊测试目标的监视范围不同的情况下,模糊器也可能捕捉不到这种情况。

    2.4.5 多阶段安全漏洞

    可利用性并非总意味着简单地对一个弱点的攻击。复杂的攻击通常涉及连续利用若干个漏洞来让机器妥协。也许一个初始的缺陷让机器接受了未被授权的访问,随后的缺陷让这种未授权被进一步放大。模糊测试对识别单个的缺陷是有用的,但是通常对那些由小的漏洞链而组成的大缺陷价却并没有多少价值,对那些让人不感兴趣的多重输入向量而引起的攻击也是如此。

    2.5 小结

    尽管模糊测试的发展已经有一段时间,但是这种方法学它仍然未被安全研究领域之外的人们所广泛理解。出于此种原因,你很可能会发现,给模糊测试所下的定义的版本数与你询问的人数一样多。我们就本书的目的而定义了模糊测试的概念,介绍了其简要发展历史,为模糊测试定下了未来的期望,下面是该深入研究模糊测试的类型和具体方法的时候了。

    第3章 模糊测试方法和模糊器类型

    "太多的好文档从企业中流失,太多的妇产医生不能与全国的妇女进行爱的实践。"

    --George W. Bush, Poplar Bluff, Mo,2004年9月6日

    模糊测试定义了一种发现漏洞的综合方法。然而,在模糊测试这把大伞下,有各种不同的具体方法来实现模糊测试方法学。本章开始对模糊测试进行详细剖析,考察各种不同的具体的模糊测试方法。我们还将考察不同类型的模糊测试如何实现这些方法以及如何利用这些方法来发现具体目标中的漏洞。本书的第二部分将对每种模糊器类型做进一步的专门探讨。

    3.1 模糊测试方法

    前一章曾提到,所有模糊器都属于两大类中的一类。基于变异的模糊器(mutation-based fuzzer)对已有数据样本应用变异技术来创建测试用例,基于生成的模糊器(generation-basedfuzzer)通过对目标协议或文件格式建模来从头创建测试用例。在这一节,我们将对这两类模糊器进一步划分子类。当然,子类的划分并没有公认的结果,但是鉴于本书是第一部关于模糊测试的专著,我们把模糊器分为下面介绍的5类。

    3.1.1 预先生成测试用例

    如前一章所述,这是PROTOS框架采用的方法。测试用例的开发始于对一个专门规约的研究,其目的是为了理解所有被支持的数据结构和每种数据结构可接受的值范围。硬编码的数据包或文件随后被生成,以测试边界条件或迫使规约发生违例。这些测试用例可用于检验目标系统实现规约的精确程度。创建这些测试用例需要事先完成大量的工作,但是其优点是在测试相同协议的多个实现或文件格式时用例能够被一致地重用。

    预先生成测试用例的一个缺点是这种方式下的模糊测试存在固有的局限性。由于没有引入随机机制,一旦测试用例表中的用例被用完,模糊测试只能结束。

    3.1.2 随机方法

    这种方法是迄今为止最低效的,但是它可被用于快速发现目标软件是否是彻头彻尾的恐怖代码。随机方法只是简单地大量产生伪随机数据给目标软件,希望得到最好的或最坏的结果,这取决于测试者所持的观点是什么。下面是这种方法的一个简单例子,它总是让人感到幽默,不过却很有效:

    代码

    上面的一行命令从Linuxurandom设备读取随机数据,然后用netcat命令将这些数据传输到一个目标地址及其端口。While循环确保这一过程持续进行,直至用户中断该过程。

    信不信由你,影响任务关键软件的漏洞在过去就是使用这样的技术发现的。这是多么令人窘迫!真正的随机模糊测试最困难之处在于如何逆向搜索服务器崩溃时软件发生异常的起因。这种搜索可能是一个令人痛苦的过程,需要回溯跟踪可能引起服务器崩溃的大约500000个随机字节。你可能希望利用探测器捕获通信数据来帮助减少跟踪范围。此外,在调试器和反汇编器上还将花费大量的时间。使用这个技术拆分栈需要痛苦的调试过程,尤其是调用栈可能遭到破坏。与某企业应用系统和网络监视实用程序相连的调试器的一个屏幕快照,被测系统刚刚在一次模糊测试攻击中发生崩溃。你能决定故障的原因吗?很可能无法确定原因,除非你是一个占星术士。精确地指出问题的起因还需要更多的研究工作。这里需要注意的是,一些数据还可能事先经过了混淆以毁灭罪证。

    3.1.3 协议变异人工测试

    按理说,人工协议测试应该比/dev/urandom方法在技术复杂性封面更加初级。人工协议测试中不需要自动化的模糊器。事实上,研究者本人就是模糊器。在加载了目标应用程序后,研究者仅仅通过输入不恰当的数据来试图让服务器崩溃或使其产生非预期的行为。这就是他的模糊测试方法,但是在过去毫无疑问是有效的。这种方法有它的优点,优点当然是分析员本人在审核过程中可以发挥他的历史经验和"天生直觉"。此类模糊测试方法最常用来测试Web应用程序

    3.1.4 变异或强制性测试

    这里所说的强制性,是指模糊器从一个有效的协议或数据格式样本开始,持续不断地打乱数据包或文件中的每一个字节、字、双字或字符串。这是一个相当早期的方法,因为这种方法几乎不需要事先对被测件进行任何研究,实现一个基本的强制性模糊器也相对简单直接。模糊器要做的全部事情就是修改数据然后传递它。当然,在故障检测过程中可以引入许多"铃声和口哨声",还可以引入登录功能等作为辅助工具,这种工具一般可以在很短的时间内完成开发。

    此种方法带有几分低效性,因为许多CUP周期被浪费在数据生成上,并且这些数据并不能立刻得到解释。然而,这些问题所带来的挑战在一定程度上可以得到缓解,因为测试数据生成和发送的全过程都可以被自动化完成。使用强制性方法的代码覆盖依赖于已知的经过测试的良好的数据包或文件。大部分协议规约或文件定义都比较复杂,即使对它进行表面的测试覆盖也需要相当数量的样本。强制性文件格式模糊器的例子包括FileFuzz和notSPIKEfile,分别对应Windows和Linux平台。

    3.1.5 自动协议生成测试

    自动协议生成测试是一种更高级的强制性测试方法。在这种方法中,需要进行前期的研究,首先要理解和解释协议规约或文件定义。然而,与前一种方法不同,这种方法并不创建硬编码的测试用例,而是创建一个描述协议规约如何工作的文法。为了达到这个目的,需要识别数据包或文件中的静态部分和动态部分,后者代表了可被模糊的变量。之后,模糊器动态解析这些模板,生成模糊测试数据,然后向被测目标发送模糊后产生的包或文件。这种方法的成功依赖研究者的能力,研究者需要指出规约中最容易导致目标软件在解析时发生故障的位置。此类模糊器的例子包括SPIKE和SPIKEfile。这两个模糊器都以SPIKE脚本来描述目标协议或文件格式,并使用一个模糊引擎来创建模糊后的数据。这种方法的不足之处是需要耗费一定的时间来产生文法或数据格式的定义。

    3.2 模糊器类型

    前面定义了几种不同的模糊测试方法或手段,下面让我们看一些具体的模糊器类型。模糊器的不同类型取决于被测的目标程序。不同的目标需要采用不同的模糊测试方法并且每种模糊测试方法都有其适合的模糊器。

    在这一节,我们将简要考察一些不同的模糊器类型。本书的后面会逐一介绍每一类模糊器的细节,包括它们的背景知识、自动化技术和开发样例。

    3.2.1 本地模糊器

    在UNIX世界中,setuid应用程序允许一个普通用户临时获得更高的权限。这一功能使该应用程序成为一个显然的模糊测试目标,因为setuid应用程序中的任何漏洞都将使一个用户永久提升权限并执行他或他自己选择的程序。对setuid进行模糊测试有两个不同的被测目标。第一个目标是命令行参数的模糊测试,关注的是在命令行中如何向setuid传递变形的参数。第二个目标也是传递变形的参数,不过采用不同的方式。第二种目标下的变形参数是通过UNIX shell环境传递的。下面让我们分别了解命令行和环境变量的模糊测试。

    命令行模糊器

    当一个应用程序开始运行时,通常要处理用户传递给它的命令行参数。来看下面的例子,这个例子在第1章"安全漏洞发掘方法学"中也曾使用过,该例子说明了命令行参数栈溢出的最简单形式。

    代码

    如果这个程序真的是setuid,可以用本地模糊器对其测试。这里的问题是在源代码不可获得的情况下安全研究者如何才能在setuid应用程序中找到更难以发现的bug。如果你听到解决这个问题最好的办法是模糊测试,应该别感到吃惊。

    下面是一些有用的命令行模糊器:

    warlock的clfuzz1。一个命令行模糊器,可用于测试应用程序中的格式字符串和缓冲区溢出漏洞。

    Adam Greene的iFUZZ1。一个命令行模糊器,可用于测试应用程序中的格式字符串和缓冲区溢出漏洞。包括对若干个不同参数的模糊选项,并可根据应用程序的"使用"帮助信息智能地产生模糊测试数据从而对应用程序执行测试。

    环境变量模糊器

    另一类本地模糊器涉及到环境变量向量,它也以setuid应用程序为目标。考虑下面的应用程序,该程序不安全地使用了来自用户环境中的值:

    代码

    有好几种有效的方式可以对一个应用程序中的环境变量的使用进行模糊测试,但是并没有多少自动化的支持工具存在,尽管这个过程很简单。许多安全研究者试图自行拼凑脚本来完成这任务,这也是公开发布的支持工具较少的部分原因。充其量,本地环境变量模糊器并不是复杂到难以开发,因此许多这样的工具被开发并公开发行。下面是一些有用的环境变量模糊器:

    Dave Aitel的Sharefuzz3。第一个公开发布的可用的环境变量模糊器,它中途拦截对getenv函数的调用消息并且返回恶意数据。

    Adam Greene的iFUZZ4。尽管它主要是一个命令行模糊器,iFUZZ包还包括了基本的环境变量模糊能力。iFUZZ使用与Sharefuzz相同的方法,并且比后者在使用过程中更容易被定制。

    在本书的第7章"环境变量和参数模糊测试"和第8章"环境变量和参数模糊测试:自动化"中,我们将详细讨论本地模糊测试和setuid的有关概念。

    文件格式模糊器

    大量的应用程序必须在某个时间点处理文件的输入输出,不论是客户端还是服务器端都是如此。例如,反病毒网关通常需要解析压缩了的文件以诊断其中包含的内容。另一个例子是Office Productivity软件套件,它需要打开文档。当这些类型的应用程解析到恶意编撰的文件时,就有理由怀疑被分析的软件存在安全漏洞。

    文件格式模糊测试的目的即在于此。文件格式模糊器动态创建变形了的文件,然后利用目标应用程序发起和处理这些文件。尽管文件模糊测试的具体方法严格讲与其它类型的模糊器存在不同,但是它们的总体思路是一致的。一些有用的文件格式模糊测试工具包括:

    Michael Sutton的FileFuzz5。一个基于Windows图形用户界面(GUI)的文件格式模糊测试工具。

    Adam Greene的notSPIKEfile和SPIKEfile6。基于UNIX的文件格式模糊测试工具,其中的一个基于SPIKE,另一个则不基于SPIKE。

    Cody Pierce的PAIMEIfilefuzz5。类似于FileFuzz,它是另一个基于Windows GUI的文件格式模糊测试工具,构建于PaiMei逆向工程框架的基础之上,本书的后面将对PaiMei做更详细的介绍。

    文件格式模糊测试的内容将在本书的第11章"文件格式模糊测试"、第12章"文件格式模糊测试:UNIX平台上的自动化测试"和第13章"文件格式模糊测试:Windows平台上的自动化测试"等章节详细介绍。

    3.2.2 远程模糊器

    远程模糊器以侦听一个网络接口的软件为测试目标。基于网络的应用程序很可能是模糊测试最重要的测试目标。随着Ineternet的发展,几乎所有的公司现在都会公开发布一些可被访问的服务器来提供Web页、e-mail、域名系统(DNS)解析服务,等等。这类软件中的漏洞为攻击者提供了访问敏感数据或对更可信的服务器发起攻击的机会。

    网络协议模糊器

    网络协议模糊器可以被分为两个主要的类:以简单协议为测试目标的模糊器和以复杂协议为测试目标的模糊器。下面将分别介绍这两类模糊器的主要特征。

    简单协议

    简单协议通常只有简单的认证或根本没有认证。它们通常基于可打印的ASCII文本,而不是二进制数据。简单协议不包括长度或校验和字段。此外,典型的简单协议应用程序并不包含很多的状态。

    简单协议的一个例子是FTP。在FTP协议中,所有的受控信道中的通信多是以普通ASCII码文本的形式传输的。就认证而言,FTP只需要简单文本形式的用户名和密码认证。

    复杂协议

    复杂协议典型地要包含二进制数据,偶尔包含人可读的ASCII码字符串。认证可能需要通过加密或某中形式的混淆来实现,其中可能包括多个复杂的状态。

    复杂协议的一个例子是Microsoft远程调用(MSRPC)协议。MSRPC是一个二进制协议,在数据传输之前需要执行若干个步骤才能建立起一个通信信道。协议需要长度描述域和分解域。总体上讲,它不是一个很容易实现模糊测试的协议。下面是几个网络协议模糊测试的有用工具:

    Aitel的SPIKE8。SPIKE是第一个公开发布的模糊测试框架。它包括好几种流行协议的预生成的模糊测试脚本,同时也可以被作为一个API来使用。

    Michael Eddington的Peach9。一个用Python编写的跨平台的模糊测试框架。它非常灵活并且可用来对几乎所有网络目标应用程序进行模糊测试。

    网络协议模糊测试的内容将在本书的第14章"网络协议模糊测试"、第12章"网络协议模糊测试:UNIX平台上的自动化测试"和第13章"网络协议模糊测试:Windows平台上的自动化测试"等章节详细介绍。

    Web应用模糊器

    Web应用程序已经成为访问后端服务的一种流行的便捷方式,这些后端服务包括电子邮件和计帐系统等。随着Web2.0的到来(先不论它究竟是什么),诸如文字处理等传统的桌面应用程序也开始向Web上转移10。

    在对Web应用程序进行模糊测试时,研究者主要考察Web应用所特有的漏洞,例如SQL注入、交叉站点脚本(XSS)等。这使得模糊器必须具备通过超文本传输协议(HTTP)进行通信以及通过捕获网络响应来分析安全漏洞的能力。下面是一些有用的Web应用模糊测试工具:

    OWASP的WebScarab11。一个开放源代码的Web应用程序审核套件,具有模糊测试能力。

    SPI Dynamics的SPI Fuzzer12。一个商业的HTTP和Web应用程序模糊器,它包含在WebInspect漏洞扫描器之内。

    Codenomicon的Codenomicon HTTP Test Tools13。一个商业用HTTP测试套件。

    Web应用模糊器在本书的第9章"Web应用和服务器模糊测试"和第10章"Web应用和服务器模糊测试:Windows平台上的自动化测试"等章节详细介绍。

    Web浏览器模糊器

    尽管Web浏览器模糊器在技术上只是一类特殊的文件格式模糊器,但是由于基于Web的应用程序的普及性,我们认为应该将它专门列为一类。Web浏览器模糊器通常利用HTML的功能来使模糊测试的过程自动化。例如,lcamtuf的mangleme实用程序是最先公开可用的浏览器模糊测试支持工具之一,它利用 标签以自动的方式连续加载测试用例。这种Web浏览器具有的独特特征使得客户端的模糊器能够完全被自动化,并且不需要对应用程序进行任何复杂的包装。而对其它客户端的浏览器,这种包装则是典型的需求。

    Web浏览器模糊测试不仅仅限于对HTML文本进行解析,还有许多其它的测试目标。例如,See-Ess-Ess-Die模糊测试工具能够对重叠样式表进行解析,另一个模糊测试工具COM Raider则关注那些能够被加载进Microsoft Internet Explore的组件对象模型(COM)对象。其它可模糊测试的元素包括图形和服务器应答头域。下面是几个有用的Web浏览器模糊测试工具:

    lcamtuf的mangleme14。第一个公开可用的HTML模糊器。它是一个CGI脚本,可反复发送打乱了的HTML数据到一个浏览器。

    H.D. Moore和Aviv Raff的DOM-Hanoi15。一个DHTML模糊器。

    H.D. Moore和Aviv Raff的Hamachi16。另一个DHTML模糊器。

    H.D. Moore、Aviv Raff、Matt Murphy和Thierry Zoller的CSSDIE17。一个CSS模糊器。

    David Zimmer的COM Raider17。一个易于使用、GUI驱动的COM对象(ActiveX控件)模糊器。

    COM和ActiveX对象的模糊测试将在第17章"Web浏览器模糊测试"和第18章"Web浏览器模糊测试:自动化"中详细介绍。CSS和图形文件的模糊测试并没有在Web浏览器的模糊测试部分专门介绍,但是其中给出的基本概念对CSS和图形文件模糊测试同样适用。

    3.2.3 内存模糊器

    有些时候在测试过程中存在一些困难阻碍了模糊测试的快速有效进行。这时候内存模糊测试就可能会派上用场。内存模糊测试的基本思想是简单的,但是开发一个合适的实现工具却不是一件容易的事。有一种实现方法涉及到冻结进程并记录它的快照,以及快速将故障数据注入进程的输入解析例程。在每个测试用例被执行之后,以前所记录的快照被存储下来并且新的数据被注入。上述过程反复进行直至所有测试用例都被穷尽。和其它模糊测试方法一样,内存模糊测试也有优点和缺点。它的优点包括:

    速度。不仅没有网络带宽需求,并且任何位于接收离线数据包和实际解析数据包之间的不重要的代码都可以被删去,从而使得测试性能有所提高。

    捷径。有时协议程序使用了定制的加密或压缩算法或者其中充满了校验和确认代码。内存模糊器能够记录下解压、解密、或校验和确认之后某一时间点上的内存快照,而不是花费大量的时间来测试所有的过程,减少了测试的工作量。

    内部模糊测试的缺点是:

    假像。由于原始数据被注入到进程地址空间,不仅没有网络带宽需求,并且任何位于接收离线数据包和实际解析数据包之间的不重要的代码都可以被删去,从而使得测试性能有所改进。

    重现。尽管引起一个异常可能会展示出一个可被人利用的漏洞,但是研究者仍然需要在进程之外远程创建异常。这可能会花费不少时间。

    复杂。正如我们在第19章"内存模糊测试"和第20章"内存模糊测试:自动化"中将要看到的,这种模糊测试方法的实现极为复杂。

    3.2.4 模糊器框架

    模糊测试框架可用于对不同目标的模糊测试。模糊测试框架其实就是一个通用的模糊器或模糊器库,它简化了许多不同类型的测试目标的数据表示。到目前为止前面提到的许多模糊器实际上是模糊测试框架,包括SPIKE和Peach。

    典型地,模糊测试框架包括一个库用来产生模糊测试字符串或者通常能够导致解析例程出现问题的数据值。此外,典型的模糊测试框架还包括一组例程以简化网络和磁盘输入输出。模糊测试框架还应该包括某种脚本类语言,以便在创建具体的模糊器时使用。模糊测试框架是流行的,但它绝对不是完全充分的解决方案。使用或设计一个模糊测试框架也有它的优点或缺点。优点包括:

    可重用性。如果创建了一个真正通用的模糊测试框架,这个框架便可以在测试不同目标软件时被反复使用多次。

    群体介入。大项目的群体参与比小项目的群体参与更容易,并且大项目可以被反复使用多次,小项目只有相对狭小的使用空间,例如专门协议的模糊器。

    模糊测试框架有下列缺点:

    局限性。即使对于仔细设计的框架,研究者也很可能遇到不适合该框架的测试目标。这通常令人沮丧。

    复杂性。获得一个通用的模糊测试接口是好事,但是却要花费时间来学习如何驱动这样的框架。有时掉转车轮重新开辟模糊测试接口可能在时间上会更高效,但这样做可能会破坏原来的框架。

    开发时间。对于一个具体的协议来说,设计一个完全通用和可重用的模糊测试框架要比单独为其开发一个程序所需要的工作量大得多。这些工作量的差异几乎总是会在开发时间上有所体现。

    一些研究者强烈地认为框架对任何重要的模糊器来说是最好的方法,而另一些人则认为并非如此,每一个模糊器都应该在具体环境下创建。这两种思路的实验验证以及取舍显然要取决读者自己。

    3.3 小结

    在执行模糊测试的时候,应该考虑到应用本章所讨论的各种方法的价值。通常情况下,组合各种不同的方法可能会收到最好的效果,因为一种方法可能是另一种方法的补充。

    一个模糊器可能要关注不同的目标软件,正因为这样,可能需要多种不同类型的模糊器。在开始学习消化各种各样的模糊器类型背后隐藏的基本概念的同时,应该设想一下如何去实现它们。这将有利于在阅读本后面要介绍的有关实现的内容时给出中肯的评价。

    第4章 数据表示和分析

    "我的工作是,诸如,考虑能立即考虑到的之外的事情"

    --George W. Bush, Poplar Bluff, Mo,2004年9月6日

    计算机在内部和外部通信的各个方面都要使用协议。这些协议构成了数据传输和数据处理的基础结构。如果要进行成功的模糊测试,必须首先对目标软件使用的协议有所理解。有了理解之后才能够针对协议中最有可能引起异常条件的部分进行测试。此外,为了访问到协议中的脆弱部分,在进行模糊测试之前我们还需要提供合法的协议数据。了解开放的和专有的协议必然会使得安全漏洞的发掘更为高效。

    4.1 什么是协议

    协议对通信来说是必须的。有时候一些协议被定义成标准,其它一些时候协议只是人们遵循的事实上的标准。口语就是一种事实上的标准的典型例子。尽管没有正式的文档定义说话的规则,但是人们一般都能够听懂别人在说什么,听者是沉默的,但是它可以找到机会给出回答。同样,你不会没有理由地简单地走到一个人面前滔滔不绝讲出你的名字、住址甚至其它至关重要的个人信息(更不会说出你的生日和身份证号)。相反,个人信息需要以元数据(metadata)开始。例如"我的名字是John Simith,我住在Cambers大街345号"在这些信息中,说者和听者都应该遵循一种有意义的数据通信方式。在计算领域,协议绝对至关重要。计算机本身并没有智能。更不能奢望它还有直觉。因此,严格地定义协议对通信和数据处理是必不可少的。

    百科全书对协议的定义是:"在两个计算端点之间建立或控制连接、通信或数据传输的约定或标准"1。这些端点可以是两个单独的计算机,也可以是一个计算机中两个单独的点。例如,从内存读取数据时,计算机必须访问硬盘上的存储区,通过数据总线将数据转移到内存,然后在将其传送至处理器。在每个端点,数据必须具有某中形式,以便发送方和接收方都能够恰当地处理数据。在最底层,数据不就是一堆比特位的集合。只有在某个上下文中理解,这些比特集合才具有含义。如果发送方和接收方端点不能就上下文达成一致,那么被传输的数据就是无意义的。

    机器间的通信也依赖协议。这种通信协议的一个常见的例子就是Internet。Internet是许许多多个分层的协议集合体。位于美国的一台家用桌面计算机可以通过连续的协议对数据进行包装,然后将数据通过Internet传送到中国的另一台桌面计算机。只有理解了相同的协议,接收方计算机才能解包数据并解释它。在上面的特定场景下,中国政府很可能中途拦截了被传输的数据,按照自己的理解解除数据的包装并对其进行了解释。这是因为参与通信的两个机器都能理解这种协议,从而使得双方的通信成为可能。在两个端点之间存在数量众多的路由器,这些路由器也必须理解协议的某些方面以便合理地对数据执行路由转发。

    尽管协议起到了一个非常通用的作用,但是却可以采取各种不同的具体形式。一些协议是人可读的,并且以简单文本形式表达。其它一些协议则是以二进制形式传输数据,这些协议的数据格式不适合人们直接理解。诸如表示GIF图象或Microsoft Excel表单的文件格式便是二进制协议的典型例子。

    4.2 协议域

    在设计一个协议的时候要作出许多决策,其中最关键的决策之一是协议中的数据如何被分隔为不同的成分。发送方和接收方的机器都需要知道如何解释数据中的个体元素,这些个体元素是由协议定义的。有三种典型的方法可用来应付这个问题:定长域、变长域和分隔域。简单文本协议通常以回车等字符作为分隔符。当客户端或服务器端在接收到的数据中解析到一个回车符,就表明此处是某个命令的结尾。以字符分隔的域在一些文件格式中也常常被使用,例如以逗号分隔的值(CSV)文件,用于表示二维数组数据,这些数据可以被诸如Microsoft Execl这样的电子表单程序所读取。XML是使用字符分隔的域的另一个例子,不过它不是以简单的单字符来表明一个域的结束,XML利用多个字符来分隔文件。例如,XML元素都定义在两个尖括号(<和>)之间,元素属性的名字[nd]值用等号(=)分隔。

    定长域预先定义了每个域使用的字节数。这种方法常常在网络协议的头域中被使用,例如以太网、Internet协议(IP)传输控制协议(TCP)和用户数据报文协议(UDP),因为这些协议的头域每次都需要高度结构化的数据。例如一个以太网数据包总是以标识目的媒体访问控制(MAC)地址的6个字节开始,后面跟着额外的定义源MAC地址的6个字节。

    为优化目的而选择域

    能够规定某些域的大小及位置的优点是规约的作者通过选择灵活的字节排列可以有助于处理器的优化。例如,IPv4头中的许多域方便地按照32位边界来排列。正如Intel Optimization Manual中的第3.41节中所述的,不按排列顺序读取数据对Pentium处理器来说相当耗费资源的。

    未对准的访问对Pentium处理器家族需要耗费三个CPU周期。在PentiumPro和PentiumII处理器上,一个跨越了缓存线的未对准的访问需要耗费6至9个CPU周期。数据缓存单元(DCU) 阵列是一个内存访问装置,它跨越了32个字节的线边界。未对准的访问可能会让DCU阵列使Pentium Pro和Pentium II处理器停止运行。为了获得最佳性能,应该确保超过32字节的数据结构和数组按照32字节的数组元素对齐,并且这种对数据结构和数组元素的访问模式不能破坏对齐规则2。

    相反,对一些精简指令集计算(RISC)体系结构,例如SPARC,在执行不对齐的内存访问时将会彻底失效,抛出致命的总线错误异常。

    当数据不太结构化时,可变长域是人们想要的。这种方法通常被用于媒体文件。一个带有很多可选的头和数据成分的图象或视频文件格式可能会相当复杂。可变长域为协议开发者提供了灵活性,同时能够创建一个有效的、高效利用内存的协议。可变长域并不是为特定的数据元素设置固定的字节数,所有这些可能都不需要,这种域通常前面是一个表明域的类型的标识符,然后是一个数据值用来说明域中随后包含的数据元素的所占的字节数大小。

    4.3 简单文本协议

    简单文本协议(plaintext protocol)这个术语指的是通信数据的数据类型大部分可被归属于可打印的ASCII字符。这些字符包括所有数字、大小写字母、百分号和美元符,还有回车(\r,十六进制数0x0D),换行符(\n,十六进制数0x0A)、制表符(\t, 十六进制数0x09),以及空格(十六进制数0x20),等等。

    简单文本协议的设计目的是让人们可以读懂它。简单文本协议通常不如对应的二进制协议效率高,因为它属于内存密集协议,但是有许多场合需要我们设计这种人可读的协议数据。文件传输协议(FTP)的控制信道就是简单文本协议的一个例子。另一方面,数据传输信道能够传输二进制数据。FTP用于上载和下载数据到远程机器上。FTP控制的通信数据是人可读的,这样一个事实使得通信可以通过命令行工具实现。

    代码

    在上面的例子中,使用了流行的Netcat3工具来人工连接到一个MicrosoftFTP服务器。尽管还有许多可用的其它FTP客户端工具,不过Netcat允许我们完全控制发送到服务器的请求命令并显示出协议的文本性质。在这个例子中,所有黑体文本是发送到服务器的请求,常规文本则代表服务器的响应。我们可以清楚地看到这里所有的请求和响应都是人可读的。这使得人们可以准确的看到当时发生了什么,并且能够调试和解决出现的问题。

    4.4 二进制协议

    二进制协议对于人们来说理解起来更困难,因为这种协议传输的数据不是可读文本,你看到的是原始的字节流。如果对协议不理解,协议中的数据包可能就没有专门的意义。对于模糊测试来说,如果测试目标是协议中的某个位置的话,理解协议的结构是必须的。

    为了更好的说明如何才能获得对一个二进制协议的恰当的理解以产生一个模糊器,让我们考察一个特征丰富的协议,这个协议现在每天被数百万人所使用,可用来与朋友们进行AOL即时消息(AIM)对话,对话的同时可能他们都在工作。特别地,我们将考察登录会话期间发送和接收的数据包。

    在详细研究AIM协议的具体内容时,有必要讨论一下开发二进制协议所用的一些基本的构造块。AIM是一个专有协议4的例子。尽管没有官方的文档,但是有关该协议的结构的大量细节我们都可以获得,这归功于其他人对该协议所做的逆向工程的努力。AIM的官方名称也叫作OSCAR(for Open System for Communication in Realtime)。逆向工程考虑到可替换的客户端,例如GAIM5和Trillian6。此外,一些网络协议分析器如Wireshark能够对包结构完全解码。这是很重要的一点。如果试图构件一个以专有协议为目标的模糊器,绝不要试图对协议实施逆向工程来重头构建模糊器。尽管这种做法在一些情况下是必要的,但是也有一些情况下你对协议或文件格式的细节感兴趣,其它人也是如此。应该利用你的好朋友Google来搜索资料,了解别人已经为你做了什么。对于文件格式模糊器,可以尝试在站点Wotsit.org获取信息,这是一个非常优秀的、集官方和非官方为一体的有关专有和开放协议文档的网站。

    下面让我们看一看AIM的认证或登录过程。理解协议的第一步是捕捉一些原始数据然后将这些数据分解为更有意义的结构。对于AIM来说,我们是幸运的,因为Wireshark就已经能够理解协议的结构。现在让我们略过开始的一些有关握手的数据包,转到用户登陆有关的数据点。其中显示了Wireshark输出的初始数据包,其中显示出AIM客户的用户名被发送到AIM服务器。可以看到,协议有两个单独的层次构成。高层包括AOL即时消息的头,它标识出被发送的数据的类型。在这种情况下,该消息头说明这个数据包包含来自AIM Signon家族(0x0017)和Signon子家族(0x0006)的数据。

    在低层,我们可以看到位于AIMSignon之下的数据和登录的消息头。在此处,只有infotype(0x0001)、buddyname(fuzzing is cool)和buddyname的长度字节数(13)被发送。这是一个使用可变长域的例子。注意buddyname的长度后面紧跟着它本身。前面提到过,二进制协议普遍中使用数据块。数据块以一个字节数开头,说明该数据块的数据长度,后面是实际数据(buddyname)。有时字节数的值也包括这个值本身所占的字节数,其它一些时候,这个值只表示后面所跟的实际数据所占的字节数。对于模糊测试而言,数据块是一个重要的概念。如果创建一个向数据块注入数据的模糊器,那么就必须要仔细调整数据块的长度,否则接收方应用程序将无法理解数据包中其余的内容。诸如SIPKE7这样的网络协议模糊器在设计时要注意这一点.

    响应初始请求的数据包,服务器提供了一个质疑值(challenge value, 3740020309)。AIM客户使用这个质疑值生成一个密码的杂凑值。这里要再一次注意的是,此处处理的是一个数据块,质疑值的前面还有该值所占的字节数。

    一旦质疑值被接收,客户就会再一次响应,返回screen name,但是此时还包括密码的杂凑值。包括在登录密钥中的还有执行登录的客户一些细节信息,用来帮助服务器识别哪个客户有能力登录。这些值以名字[nd]值对的形式提交,这是二进制协议中又一种常见的数据结构。在这里例中使用的是client id string(name)的形式,它与AOL即时消息同时被提交,版本号5.5.3591/WIN32。同样,前面还包括一个说明值域长度的长度值。

    4.5 网络协议

    前面介绍的FTP和AIM协议都是网络协议的例子。Internet上到处都是网络协议,并且哪里都不缺少这种协议。网络协议包括数据传输、路由、电子邮件、流媒体、即时消息以及其它更多形式的通信协议等,种类之多超乎你的想象。正如一位智者所言"关于标准,最好的事情就是有太多的标准可供选择"。网络协议标准也不例外。

    网络协议是如何被开发的呢?这个问题的答案很大程度上依赖于协议是专有的还是开放的。专有协议是被封闭的小组开发的,这个小组属于某一个公司,协议主要用于专门的产品,其维护也是由这个公司来控制。不管怎么说,专有协议都有它的固有优点,因为开发者只需要在一个小的群体范围内就标准达成一致就可以了。另一方面,Internet协议在本质上是开放的,因此需要在大量不同的群体之间达成一致。总体上讲,Internet协议是由Internet Engineering Task Force(IETF)8开发和维护的。IETF对Internet标准的提案有一个冗长的发布和获取反馈的过程,首先要发布"Request for Comment(RFC)",它们是描述协议和供同行进行评审的公开文档。随后是发布评论意见和修改意见,之后RFC才能被IEFT采纳为Internet标准。

    4.7 常见的协议元素

    为什么要介绍这一部分背景知识呢?因文件格式或网络协议的结构不同,需要相应地调整对其进行模糊测试的方法。对数据结构了解的越多,就越容易关注那些容易引发异常条件的模糊测试对象。下面就让我们简单学习协议中的常见元素。

    4.7.1 名字-值对

    不论是二进制协议还是普通文本协议,其中的数据通常以名字-值对(例如size=12)的形式表示,,不过这种情况对普通文本尤其适用。回过头看一看前面提到的Content.xml文件,在整个XML文件中都可以看到名字-值对。作为一个一般规则,在处理名字-值对的时候,至多可能通过对其中的值部分进行模糊测试而识别出潜在的漏洞。

    4.7.2 块标识符

    块标识符是标识二进制数据类型的数字值。它的后面可能跟着可变的或固定长度的数据。在前面讨论过的AIM的例子中,AIM Sinon的头域中包含infotype(0x001)块标记。它的值定义了其后所跟的数据类型,在该例中是buddyname。模糊测试可被用于识别一些未被记录为文档的块标识符,这些标识符可嫩接受格外的数据类型,它们都可以被模糊测试。

    4.7.3 块长度

    块的长度在前面曾经提到过,它通常包含诸如名字-值对这样的数据,名字-值对的前面通常是一到过个字节用来说明该区域的数据类型以及后面所跟的变长域的数据长度。在模糊测试的时候,可以试着修数据块的长度值,以使它大于或小于其后所跟的数据的实际长度,同时监视测试结果。这种测试策略是缓冲区溢出或不满的一个常见的来源。作为可选的另一种方案,当对块内的数据进行模糊测试时,应确保这个长度值得到了相应的调整,以确保应用程序能够恰当地识别数据。

    4.7.2 校验和

    一些文件格式在整个文件中嵌入了校验和,以帮助应用程序识别可能由于各种原因而遭到破坏的数据。校验和并不一定要被实现为一种安全保护机制,因为文件可能会因为各种不同的原因而被拨坏,但是校验和却可以影响模糊测试的结果,因为应用程序通常在遇到不正确的校验和时会放弃文件处理过程。PNG图象格式所采用的文件类型是利用校验和的一个典型例子。在碰到校验和时,至关紧要的是让模糊器考虑到校验和并重新计算和重新写入校验和以确保目标应用程序能够恰当地处理文件。

    4.8 小结

    尽管对文件和网络通信可以采用强制性的模糊测试,然而更高效的做法是只对目标应用程序中有可能导致安全隐患的部分实施测试。虽然获得对目标应用程序的理解需要付出一定的前期努力,但是这些努力一般是值得的,尤其是在开放和专有协议的文档充足的情况下。经验有助于识别协议中最佳的目标位置,本章介绍的内容突出说明了历史上发现的应用程序中可能存在漏洞的某些弱点区域。在第22章"自动化协议解析"中,将详细介绍一些可用于解析协议结构的自动化技术。

    第5章 有效模糊测试的需求

    "你教一个孩子阅读,他或她将能够通过一个文化测试"

    --George W. Bush, Townsend, TN, 2001年2月21日

    在前一章,我们介绍了各种不同的模糊器类型和不同的模糊测试方法。本章要讨论的是进行有效和高效的模糊测试所需要的技巧和技术。一些明显的因素,例如测试计划和模糊器的可重用性应该在开发模糊器之前就予以考虑。这样的考虑有助于保证未来的工作可以依据和构建在当前工作的基础之上。使模糊器更复杂的一些更多的特征,例如过程状态和深度、跟踪和度量、错误检测以及约束条件等,本章也进行了分析讨论。在本书后面的第二部分,我们还将介绍几种模糊器的测试目标以及如何开发自动化工具来支持这些目标。在阅读本章的过程中,应该尽量记住本章介绍的一些概念,因为这些概念在构建新模糊器甚至在构建本书后面第12章"文件格式模糊测试:UNIX平台下的自动化"中要介绍的模糊测试框架的过程中将起到重要的作用。尽管当前有许多在售的商用模糊器产品,但是它们中间没有一个能够完全满足本章所描述的有效模糊测试的所有需求。

    5.1 可重现性和文档记录

    对模糊测试工具的一个明显需求是它应该具备重现测试结果的能力,测试结果既包括单个的测试也包括测试序列的测试结果。这个需求对测试者与它人或小组交流测试结果至关重要。作为一个模糊测试者,应该能够为模糊测试工具提供一个引起问题的测试例数,并且心里要明白被观察到的目标行为在不同的测试运行之间应该保持严格的一致。让我们考虑一个假设的场景:假设你正在测试Web服务器的能力,检验它是否能够处理非法的POST数据,在对其进行模糊测试时,向其发送的第50个测试用例引起服务崩溃,进而发现了一个可能被人利用的内存破坏条件。重新启动Web服务器并重传引起问题的那个数据给目标应用程序之后却什么也没有发现。难道这是假象吗?当然不是:计算机是确定性的计算工具,它本身没有随机的概念。前面的崩溃问题是由于输入的组合而产生的。或许前面输入的数据包首先让服务器进入了某个状态,最后第50个测试用例触发了内存破坏条件。没有进一步的分析,不具备系统的方法来重新回放整个测试过程,我们就无法进一步界定问题的起因。

    即使不是强制规定要编写文档,各种各样的测试结果的记录也很有用途,这是信息共享时代的一个基本需求。在国际外包开发1的大趋势下,安全性测试者通常不可能直接走到与软件问题有关的开发者身边。外包如此普及,以至于计算机科学的学生都知道如何去利用它2。各种各样的通信障碍,包括时区、语言和通信媒体等,要求我们尽可能以清晰简洁的形式对信息打包,这一点非常重要。有组织的开发和记录文档所带来的工作负担不应该是完全的手工劳作。一个好的模糊测试工具应该产生和存储那些容易被分析和引用的日志信息。

    我们应该考虑到前面讨论的模糊器如何满足软件问题的可重现性,如何自动记录日志和文档。还要考虑到如何根据这些目标改进模糊器的实现。

    5.2 可重用性

    从较大规模开发的角度考虑,如果我们正在构建一个文件格式模糊工具,那么我们不希望每次测试一种新的文件格式时必须要重新开发整个工具。我们可以构建出一些可重用的特征,以便未来在测试另一种不同的文件格式时能够节省时间。我们举一个例子,假设我们的目标是开发一个JPEG文件格式模糊测试工具,用来查找Microsoft Paint中的bug。事先我们考虑到并且知道需要重用现在的某些工作,那么很可能会决定将该工具分为三个构件。

    JPEG文件生成器负责不停地生成变异了的JPEG文件。目标程序运行器是工具的前端,它负责周而复始地产生图象,每次都使用不同的参数让Microsoft Paint加载新生成的图象。最后,系统结构中还包括一个错误检测引擎,它负责监视Microsoft Paint的异常条件的每个实例。将系统结构分为三个构件允许我们改变测试集以适应其它文件格式,因为只需要改动生成器即可。

    如果在较小规模上考虑,我们开发的许多构造块应该可以在不同的模糊测试项目之间被重用。例如,考虑一下e-mail地址。这种基本的字符串格式很常见,包括在简单邮件传输协议(SMTP)事务,登录屏幕和VoIP初始会话会话协议(SIP)中:

    在上述每种情况下,e-mail地址都是一个有用的域,因为我们确定该域将被解析并且有可能被分隔为各种各样的构件(例如用户和网域)。如果我们打算花时间列举email地址可能引起问题的表示形式,那么如果让这些表示在所有的模糊器中被重用岂不是很好吗?

    我们应该考虑如抽象或使模糊器被模块化,根据前面的分析思路让使模糊器具有可重用性。

    5.3 过程状态和过程深度

    为了牢固掌握过程状态和过程深度的概念,下面举一个许多人都很熟悉的例子:ATM银行业务。考虑下面的一个简单的状态图。

    在一次典型的ATM交易中,人走到机器前(非常小心谨慎以防有人跟踪),插入他的卡,输入PIN,接着是按照一系列屏幕菜单的提示进行操作,选择希望提取的金额,然后是取出钱,最后是结束交易。同样的状态概念和状态转移的概念也适用于软件。稍后我们还要举一个这方面的具体例子。我们将过程状态(process state)定义为目标过程在任意给定的时间所处的具体状态。动作可以使状态之间发生转移,例如插卡或选择提取金额的动作。过程进行的深入程度被称为过程深度(process depth)。例如,描述一个取款金额发生在比输入一个PIN更深的过程深度上。

    再举一个和安全更相关的例子,考虑一个secure shell (SSH)服务器。在连接到服务器之前,服务器处于初始状态。在认证过程中,服务器处于认证状态。一旦服务器成功通过了对一个用户的认证,那么服务器就处在已认证状态。

    过程深度是达到一个具体的状态而所需要的"向前"步骤的具体度量。仍以SSH服务器为例。

    在过程中,已认证状态比认证状态"更深",因为已认证状态需要在认证状态的基础上执行更多的子步骤。在模糊器的设计中,过程状态和过程深度是能够引起重要复杂情况的重要概念。下面是一个引起复杂情况的例子。为了对一个SMTP服务器的e-mail地址的MAIL FROM谓词参数进行模糊测试,就不得不连接到服务器并发出一个HELLO或EHLO命令。如图5.4所示,底层的SMTP实现可能采用同一个函数来处理MAIL FROM命令,不论最开始使用的命令是什么。

    在函数1是唯一定义的用来处理MAILFROM数据的函数。还可以采用另一种可选设计方案,其中的SMTP实现包含了两个单独的例程用来处理MAIL FROM数据,这两个例程的选择取决于初始的命令。

    这实际上是一个真实的案例。2006年9月7日,一个有关与IpswitchCollaboration Suite捆绑的SMTP的远程攻击栈溢出漏洞的安全公告被发布。该漏洞的产生是因为在解析email地址时遇到了其中包含位于字符@和:之间的长字符串。该分析例程只有在客户发起连接时输入了以EHLO开头的会话信息时才可达。在设计模糊器时,应该留心类似的、可能的逻辑拆分。为了获得充分的覆盖,我们在设计模糊器时对email地址进行了两次完全变异,第一是通过对EHLO变异,第二次是对HELO变异。如果沿这种思路获得更多的逻辑拆分并分别对其变异的话会产生什么结果呢?不要忘记完全充分的覆盖所需要的逻辑循环拆分是呈指数规模递增的。

    在学习后面要介绍的各种各样的模糊器时,应该考虑到如何处理过程深度的变化和逻辑拆分。

    5.4 跟踪、代码覆盖和度量

    代码覆盖(codecoverage)是一个术语,它是指模糊器能够让被测目标系统达到或执行的过程状态的数量。直到写作本书为止,我们现在还不知道有哪种公开或商业可用的模糊测试技术具有跟踪和记录代码覆盖率的能力。这是分析员未来在这一领域进行开拓性研究的一个重要概念。质量保证(QA)小组可以利用代码覆盖提升对测试水平的信心。比方说,如果你是一位Web服务器产品的QA领导,在发布经过测试的产品时,你对产品达到零失效和90%的代码覆盖率比对产品达到零失效和25%的代码覆盖率很可能更有信心。软件漏洞的研究者也可以受益于代码覆盖率分析,通过对代码覆盖率的分析他们可以识别出一些应该对软件进行的必要修改之处,以使软件的代码被覆盖到一些不容易被他人注意的程序状态。代码覆盖率这个重要的概念将在本书的第23章"模糊器跟踪"中详细讨论。

    在阅读本书后面对各种不同模糊器的介绍过程中,建议读者考虑一些创造性的方法来确定代码的覆盖率并分析这种方法的优点。在模糊测试时,人们总是会问"如何开始?",注意另一个问题"什么时候停止?"与这个问题同样重要。

    5.5 错误检测

    生成和传输可能引起异常的数据只是模糊测试战役的一半。战役的另一半是精确地确定错误何时出现。在写作本书的时候,大部分可用的模糊器对这个问题还处于"盲目"状态,因为这些模糊器并不知道被测目标如何对输入数据产生反应。一些商业解决方案在两次发送恶意请求之间加进了"ping"或"存活性"检查机制,用来控制和判定被测目标是否仍然正常履行功能。术语"ping"在这里只是随意地被使用,它的意思是指应该生成已知的良好响应的任何一个事务。此外还有其它一些解决方案,这些方案构建在日志输出分析的基础上。它们包括监视由单独的应用程序维护的ASCII文本日志或者查询系统日志,例如查询Windows Event Viewer中的日志。

    这些错误检测方法的优点是它们中的大部分可以容易地在不同的平台和体系架构之间移植。然而,这些方法在能够检测到的错误种类方面严重受限。例如,它们中没有一种方法能够检测到发生在Window应用程序内同时被结构化异常处理例程(SHE)所处理的错误。

    错误检测的下一代技术是使用轻量级的调试客户端来检测目标系统中的异常条件何时发生。例如,本书第二部分将要介绍的FileFuzz工具包括了一个用户定制的、开放源代码的Microsoft Windows调试客户端。利用这种类型的工具的负面作用是必须为测试的每一个目标平台开发一个调试支持工具。例如,如果想要在Mac OX、Microsoft Windows和Gentoo Linux上测试三个SMTP服务器,就很可能需要开发两个甚至三个不同的监视客户端。此外,由于测试目标所限,有可能无法或无法及时构建这样的调试客户端。再例如,如果测试一个硬件VoIP电话,那么就可能必须重新走到对测试实施控制或对日志实施监控的老路上去,因为硬件解决方案不不支持调试,因此可能需要另外的专用工具。

    如果进一步向前展望,错误检测的万能药很可能是诸如Valgrind6和Dynamo Rio7这样的动态二进制插装/翻译平台。在这样的平台上,有可能在软件开发时就进行错误检测而不是在错误发生后再检测它。如果朝着"5000英尺"的距离展望,基于DBI的调试引擎能够非常专业地在底层对目标软件执行分析和插装。这种底层的控制能够支持内存泄露检查、缓冲区溢出检查和非越界检查,等等。回顾前面讨论可重现性时所举的内存破坏的例子,在该例子的情形中,轻量级的调试客户端能够告诉我们内存破坏是何时被触发的。回顾那个例子我们可以知道,当时的场景是许多数据包被发送给目标服务,结果第50个测试用例引起服务崩溃。在一个诸如Valgrind这样的平台上,我们有可能检测初始的内存破坏,它发生在触发异常之前的早期的测试用例的执行中。这种方法能够节省数个小时甚至许多天的模糊过程和错误跟踪。

    5.6 资源约束

    各种各样的非技术因素都可能对模糊测试带来限制,例如预算和时间期限。这些因素在设计和规划阶段必须被牢记。例如,你可能在模糊测试之前的最后一分钟产生试水前的恐慌感,因为没有人哪怕是很简单地检查过你投资50万美元所开发的产品的安全性。安全性在太多情况下成为软件开发周期(SDLC)过程中事后的追悔,在增加新特征或达到了产品发布期限时的追悔。如果我们希望开发安全的软件,那么安全性应该被我们"培养"而不是被我们"突击"。这种培养包括对SDLC做出根本性的改变以确保安全性在开发过程的每一个阶段都被恰当地考虑。换句话说,我们承认软件是在真实世界中而不是在一个乌托邦的国度中被开发的,后者的资源是充足的,缺陷是稀少的。因此,在阅读本书的过程中,同样重要的是心中应该对各种不同的技术做一个分类,思考哪些技术可以运用在时间和资源有限的情形下,哪些技术只能应用在梦想"最终"理想的模糊器套件的过程中。此外,还要考虑应该在SDLC的哪个阶段实现这样的工具,以及谁负责这个过程。

    5.7 小结

    本章的主要目的是介绍一些用来开发特征丰富的模糊器所要考虑的一些基本思想和创造性见解。当你在开发、比较和使用模糊测试技术时,如果再次阅读本章的内容,就会变得小心谨慎。未来的几章将介绍和分析各种不同的模糊测试解决方案,届时读者应该考虑本章所介绍的一些需求目标如何在相应的背景下得到实现和改进。

    第6章 自动化测试和测试数据生成

    "我们的敌人富有创新精神并且足智多谋,我们也一样。他们总想找到新的方法来损害我们的国家和人民,而我们不会。"

    --George W. Bush, Washington, DC,2004年8月5日

    模糊测试在很大程度上可以采用自动化的方式来实施。模糊测试相对于其他的软件测试方法而言,其核心价值体现在能够将大量的手工测试转换为高度的自动化测试。生成单个测试用例是一项费力和枯燥的工作,而某些任务则非常适合于计算机来完成。模糊器的核心竞争力就是它在人工干预最小的情况下,生成有用测试数据的能力。本章关注于自动化测试的不同方面,包括语言的选择、有用的构造块、以及为了有效的测试目标软件,在测试数据生成过程中如何选择适当模糊值的重要技巧。

    6.1 自动化测试的价值

    尽管自动化测试的优点是显而易见的,这里为了清晰起见,再从两个方面回顾一下。首先,我们根据人工计算的能力来强调其价值,然后再分析其可重用性。针对于早期的计算机而言,计算时间是非常宝贵的,事实上,它比人工计算的时间还要宝贵;因此,在这些系统上编程是一个非常枯燥乏味的手工过程。程序员要操作磁带、转换开关、并且要手工输入十进制或者二进制的机器操作码。随着科技的发展,程序员的聪明才智不断得以体现,计算时间更加充裕,使得成本代价的平衡发生了改变。目前,这种趋势仍在继续,并且在日益流行的高级编程语言如Java,.NET和Python中得到了印证。这些编程语言牺牲了一些计算时间,而却为程序员提供了更加简便的开发环境,以及更加迅速的开发转换时间。

    鉴于此,尽管对于程序分析员而言,他和基于socket的daemon程序进行交互并且手工在程序漏洞中输入数据以发现软件缺陷是十分可行的,但是最好将人工时间花费在其他任务上。当比较模糊测试与人工审核工作如源代码审查和二进制审核时,也可以得出同样的结论。人工审核方法需要高级分析员花费大量的时间,而前一种方法即模糊测试,则可以或多或少的由任何人来实施。最后,自动化应当作为减少高级程序分析员工作量的第一步,使其能够象关注其它测试方法那样来发现缺陷。
    接下来,强调一下可重用的必要性。下面两个关键因素奠定了可重用性的重要性:

    如果我们能够为一个FTP服务创建一个可重用的测试过程,那么我们就可以使用同一个测试过程很方便的测试其它版本的程序,即便是完全不同的FTP服务也可以。否则,需要浪费大量的时间来为每一个FTP服务重新设计并实现一个新的模糊器。

    如果一个非寻常的事件序列在目标程序中触发了一个缺陷,那么我们必须要再次产生整个序列以限制造成异常的特定结果。程序分析员需要创建不同的测试用例,因为它们不具备科学的可重用性。

    简而言之, 测试数据生成和再生成、缺陷监测等耗时耗力的工作最适合于实现自动化。象大多数计算任务一样,对模糊测试而言,幸运的是我们可以利用许多已有的工具和库来实现自动化。

    6.2 有用的工具和库

    大多数模糊器的开发者将会发现他们从头开始创建工具,尽管已经有了大量的公共可用的模糊器脚本 。幸运的是,许多工具和库可以在模糊器的设计和实现阶段为你提供帮助。本节列举了一些这样的工具和库(按字母顺序排列)。

    6.2.1ETHEREAL /WIRESHARK

    Wireshark(Ethereal项目的一个分支) 是一个流行的开源网络嗅探器和协议分析器。尽管依赖于其中的一个库并不是必要的,但毫无疑问,该工具可以在创建模糊器的研究和调试阶段提供帮助。Wireshark所提供的许多开源的分析器也经常作为参考来发挥作用。被识别为一个拥有可用分析器的被捕获通信会被显示为一系列的字段/值对,这些字段/值对是和一组原始字节相对应的。在开始人工协议分析之前,通常是首先认真地查看一下Wireshark的分析结果。对于一个可用分析器的快速列表而言,可以参阅Wireshark的版本控制库,特别是epan/dissectors 目录。

    6.2.2 LIBDASM和LIBDISASM

    Libdasm和libdisasm都是免费和开源的反汇编程序库,你可以将它们嵌入到你的工具中,以用于从二进制流开发AT&T和Intel的语法解析器。Libdasm是用C语言编写的,而libdisasm则是用Perl编写的。在libdasm中包含一个Python的接口。尽管反汇编程序对于创建网络通信是不必要的,但是当在通信的终端进行自动化的错误检测时它是很重要的。上述两个库在本书中被广泛使用,尤其是在第12章"文件格式模糊测试:UNIX平台上的自动化测试",第19章"内存数据的模糊测试",第20章"内存数据的模糊自动化测试",第23章"模糊器跟踪"以及第24章"智能的故障检测"中。

    6.2.3 LIBNET/LIBNETNT

    Libnet是一个针对创建和注入低层网络包数据的免费、开源的高层API。该库隐藏了在生成IP和链接层通信时包含的许多复杂的细节,并且始终提供了跨不同平台的可移植性。如果你正在编写一个网络栈的模糊器,那么可能会对该库产生兴趣。

    6.2.4 LIBPCAP

    LibPCAP以及和微软Windows相兼容的WinPCAP 都是免费、开源的高层库,能够简化跨UNIX和微软Windows平台的网络捕获和分析工具的创建。许多的网络协议分析工具,例如前面提到的Wireshark都是依靠该库创建的。

    6.2.5 METROPACKET LIBRARY

    Metro Packet Library是一个C#库,它提供了一个与IPv4、TCP、UDP和Internet控制消息协议(ICMP)相交互的抽象接口。该库有助于创建包嗅探器和网络分析工具。对于该库的讨论和应用请见第16章"网络协议模糊测试:Windows平台上的自动化测试"。

    6.2.6 PTRACE

    在UNIX平台上进行调试,大部分是在系统调用ptrace()(进程跟踪)的帮助下完成的。一个进程可以利用ptrace()来控制寄存器状态、内存、执行过程、并且捕获另一个进程产生的信号。这种机制用于实现第8章"环境变量和参数的模糊测试:自动化"和第12章"文件格式模糊测试:UNIX平台上的自动化测试"中所讨论的工具。

    6.2.7 PYTHONEXTENSIONS

    在创建模糊器时可以使用不同的Python扩展。示例扩展包括Pcapy,Scapy和PyDbg。Pcapy 是针对LibPCAP/WinPCAP的Python扩展接口,以使得Python脚本能够捕获网络通信。Scapy 是一个功能强大的、简便的包操作扩展,它既可以通过交互方式使用也可以作为一个库使用。Scapy能够创建和解析许多不同种类的协议。PyDbg 是一个纯Python的微软Windows32位的调试器,是一个方便灵活的进程工具。PyDbg库是PaiMei 逆向工程框架的一个子集,该框架将在本书的第19、20、23和24章中使用。

    这里所列出的某些库可以用于多种语言。而另外一些则要受到很大的限制。当决定采用何种语言来开发模糊器时,特定的需求以及能够帮助实现这些需求的可用的库是必须要考虑在内的因素。

    6.3 编程语言的选择

    许多人认为模糊器编程语言的选择问题是一个笃信性的争论,他们固执的坚持其选择而不论所要解决的问题是什么,而其他人则遵循"对于正确的任务使用正确的工具"的准则。我们倾向于后一种做法。在本书中,你将会发现源代码是采用不同的语言所编写的。在本书的编写过程中花费了大量努力以尽可能多的包含可重用的实际的代码示例。无论你个人的喜好如何,我们鼓励你在选择一种语言用于特定任务之前,先思考所给定的任何语言的优劣之处。

    在最高的层次上,你将会在开发的起点上发现编程语言存在一个根本的不同:解释型语言和编译型语言。在低层上,编译型语言如C和C++提供了准确和直接访问底层构件的功能。例如前面提到的Libnet库,就是在此低层的接口。在高层上,解释型语言如Python和Ruby提供了更快的开发时间,并且不需要重复编译就可以进行修改。针对高层语言提供低层功能的库通常是存在的。如果模糊器既要求灵活性又要注重代码质量,那么模糊器将会用所有的语言来编写。依赖于特定的任务和对各种语言的熟悉程度,从shell脚本到Java,从PHP到C#,肯定有一种语言最适合所面临的任务。

    6.4 测试数据生成和模糊启发式

    如何生成测试数据的实现方法只是待解决问题的一部分,同等重要的是决定生成什么样的测试数据。例如,考虑如果我们正在创建一个模糊器以分析一个IMAP服务的健壮性。在许多IMAP请求动作和构造器中,需要加以研究的是命令继续请求(Command Continuation Request,CCR),其引自RFC3501 的描述如下:

    7.5服务器响应--命令继续请求

    命令继续请求响应是通过一个"+"标记来指示的,而不是一个标签。这种形式的响应意味着服务器已做好准备以接收来自客户端的命令继续请求。响应的剩余部分是一行文本。

    该响应用在AUTHENTICATE命令中,将服务器数据传递给客户端,并请求额外的客户端数据。如果命令的参数是一个文本,那么也会用到该响应。

    客户端不允许发送文本的字节,除非服务器表明它希望这样做。这就允许服务器来处理命令并且逐行的抛出错误。该命令的剩余部分,包括终止命令的CRLF,后面跟着文本的字节。如果有任何额外的命令参数,则文本字节的后面跟着一个空格和参数。

    根据RFC,任何以{数字}(用黑体突出显示)形式结尾的命令指明了在后续行中接续的命令的剩余字节数。这是模糊测试的主要目标,但是应该使用什么值来测试该字段呢?所有可能的数字值都能够被测试吗?目标daemon程序可能会接收最大的32位整数值范围内的所有整数,其大小为0xFFFFFFFF(4,294,967,295)。如果模糊器能够每秒钟执行一个测试用例,那么将会花费大于136年的时间来完成测试!就算我们将模糊器加足马力运行,使其每秒钟可以运行100个测试用例,那么仍然需要大约500天的时间来完成测试。按照这个速率,当我们结束测试时,IMAP协议可能将早已过时。很明显,不能检查整个范围内的所有数据,必须要选择一个智能子集或者可能整数值的一个代表集。

    在模糊字符串或者模糊数据列表中所包含的特定的潜在危险值,被称作模糊启发式(fuzz heuristics)。下面学习几个将要包含在智能库中的数据类型的类别。

    6.4.1 整型值

    在新的改进的整型测试用例列表中,选择两个极限边界(0和0xFFFFFFFF)测试用例是很明显的。还能增加其它什么值呢?可能所提供的数字被用作内存分配程序的大小参数。通常额外空间被指定的大小所包含以容纳一个头、尾或者终止空字节。

    由于相同的原因,被指定的大小可能会被之前的值减去以进行分配。当目标程序知道它不准备将所有的指定数据拷贝到新分配的缓冲区时,就会发生这种情况。记住整型值的溢出(加法运算导致结果超出32位整数的最大范围)和下溢(减法运算导致结果小于0)可能会导致在下面的代码行中出现潜在的安全问题,因此应当谨慎的包含边界附近的测试用例,例如0xFFFFFFFF-1,0xFFFFFFFF-2,0xFFFFFFFF-3…,和1,2,3,4等等。

    类似的,对所指定的大小同样可以应用乘法运算。例如,考虑如果提供的数据被转换为Unicode的情形。这就要求指定的大小去乘以2。另外,还应当包括额外的两个字节以确保空终止符。

    为了在此以及类似的情形下触发一个整数的溢出,我们还应当包含下列边界附近的测试用例:0xFFFFFFFF/2,0xFFFFFFFF/2-1,0xFFFFFFFF/2-2等。那么被3除或被4除所得边界附近的测试用例是什么情形呢?在这种情况下,对16位整数(0xFFFF)而言,同样包含前面所列出的边界附近的测试用例又如何呢?那么对8位整数(0xFF)又如何呢?下面将这些问题与其它一些巧妙的选择列在一起。

    在上面的列表中,MAX32表示32位整数(0xFFFFFFFF)的最大值,MAX16表示16位整数(0xFFFF)的最大值,MAX8表示8位整数(0xFF)的最大值,随机选择的范围16作为一个合理的数量。依赖于不同的时间和结果,可以增加该范围值。如果在目标协议中有数百个整型字段,那么额外的启发式整数将上百倍的增加测试用例的总个数。

    采用有趣的方法可以获得这些所选择的"启发式"整数。启发式只不过是"技巧性猜测"的一种风趣的说法。更加高级的用户可以在反汇编程序的作用下研究二进制代码,并且通过研究内存分配和数据复制程序中的交叉引用来抽取潜在的感兴趣的整数值。这个过程也可以实现自动化,第22章"自动化协议解析"中阐述了相关的概念。

    智能数据集的必要性

    2005年9月1日,一则名为"NovellNetMail IMAPD 命令继续请求堆溢出" 的安全性公告同相应的提供商的补丁被一起公开发布。这个公告详细阐述了该缺陷,它允许远程未授权的攻击者以执行任意代码,进而全面危及整个系统的安全。

    所描述的缺陷存在于CCR的处理过程中,作为用户所指定的大小值被直接用做名为MMalloc()的自定义内存分配函数中的一个参数。

    MMalloc()函数在分配内存之前对所提供的值执行一步小的数学操作。攻击者可以指定一个引发整数溢出的恶意数字,从而导致一个小的内存块被分配。所提供的原始的较大的值将在后面的memcpy()中被使用。

    该指令序列将把攻击者所提供的数据拷贝到分配堆区域的边界之外,并且随意的覆盖堆区域,最终破坏整个系统。

    尽管Novell针对此问题的补丁成功的解决了这个特定的问题,但是它并没有解决所有可能的攻击路径。对于Novell而言,不幸的是,IMAP程序将会把人工可读的数字形式转换为等价的整型值。例如,"-1"转换为0xFFFFFFFF,"-2"转换为0xFFFFFFFE等。

    上述这个问题后来被一个独立的研究者所发现,Novell于2006年12月22日发布了针对该问题的补丁 。

    6.4.2 字符串重复

    我们已经讨论了如何在模糊测试数据集中包含一些巧妙的整数,但是对于字符串而言,情形又如何呢?下面从经典 的"长字符串"开始讨论:

    当我们对字符串进行模糊测试时,希望包含一些额外的字符序列。应当首先包含其它的ASCII字符,例如"B"。这是非常重要的,例如由于不同的行为,堆结构中"A"的ASCII值被"B"的ASCII值覆盖,这将在微软Windows操作系统中触发堆溢出。另外一个重要的原因是,实际上软件要特别的搜索并阻止A的长字符串。显然,某些提供商已经明白了使用长字符串AAAAAAAAAAAAAAAAA的强大功能。

    6.4.3 字段分隔符

    同样需要包含非字母字符,包括空格和制表符。这些字符通常被用做字段分隔符和终止符。将它们随机的包含到所生成的模糊测试字符串中,可以提高将被被测协议进行部分复制的机会,进而增加被模糊测试的代码的数量。例如考虑相关的简单HTTP协议,下面列出了一些非字母字符:

    其中有多少个可以被识别为HTTP字段分隔符呢?

    你可能会注意到的第一个样式就是这里有许多换行分隔符,通过序列0x0d 0x0a在字节层次上被表达。每个单独的行后面都使用不同的分隔符。例如在第一行,可以看到有字符空格( ),前斜线(/)和点(.)被使用以分隔响应代码。在后续行上都使用了一个共同的分隔符冒号(:)来分隔不同的项,例如Content-Type,Server和Date和它们相对应的值。进一步研究可以发现逗号(,),等号(=),分号(;)和短划线(-)也被用来分隔字段。

    当生成模糊字符串时,很重要的一点就是要包括象前面所列出的不同字段分隔符所分隔的很长的字符串。另外,增加分隔符的长度同样也是很重要的。例如,考虑在2003年被发现的发送邮件头的处理漏洞 。此漏洞需要一个包含<>字符的长字符串来触发破坏可利用内存的行为。同样考虑下面的代码片段,它引发了重复字符串分隔符解析的漏洞。

    该含有漏洞的解析器处理一个字符串,期望找到冒号分隔符的一个单个实例。当分隔字符被找到时,保存指向该字符串索引的指针。否则,以1来递增变量length。当循环结束时,将所计算的变量length减去1以为空终止符保留空间,然后针对找到的分隔符再减去1,将它与目标字符缓冲区的大小相比较,以确保存在足够的空间来执行后面的strcpy()函数调用。该处理逻辑字符串如name:pedram amini能被正确处理。对于此字符串,解析器将计算出length的值为16,意味着没有足够的空间,于是就跳过函数strcpy()的执行。如果字符串是name::::::::::::::::::::::pedram,情形又怎样呢?在此情况下,计算得到length的值将变为10,于是可以通过条件检查,开始执行strcpy()函数。然而,在strcpy()中所使用的该字符串的实际长度却是32,因此将触发一个栈溢出。

    6.4.4 格式化字符串

    格式化字符串是相对较为容易被发现的一类错误,模糊器所生成的测试数据应当包括这类字符串。格式化字符串漏洞可以用任意格式化字符串标记来揭露,例如%d,将显示一个十进制数,而%08x将显示一个十六进制数。在进行模糊测试时,格式化字符串标记最好选择%s或者%n(或者二者均选)。这些标记更有利于导致可检测错误的发生,例如内存访问违规。大多数格式化字符串标记都将导致从栈中读取内存,通常这并不会触发漏洞代码中的错误。因为栈被解除引用,%s标记将导致发生大量的内存读取操作以寻找一个表明字符串终止的空字节。在大多数情况下,%n标记提供了最大的可能性来触发一个错误。启发式模糊数据列表中应当包括%s%n的长序列。

    利用格式化字符串漏洞的关键

    尽管所有其它的格式化字符串标记,包括%d,%x和%s都将导致从栈中读取内存的操作的发生,但%n格式化字符串却具有唯一性,因为它还导致写内存。这是在代码执行中利用格式化字符串漏洞的关键需求。尽管其它被拒绝的格式化字符串标记可以被用来从漏洞软件中"泄露"潜在的关键信息,而利用%n标记则是通过格式化字符串直接写内存的唯一方法。正是因为这个原因,微软决定实现一个控制机制,以在printf函数类中完全支持%n格式化字符串标记。相应的API函数是_set_printf_count_output() ,若该函数被赋予一个非零值则提供对%n的支持,若赋予零则取消对%n的支持。实际上,%n在产品代码中很少被使用,因此默认的设置都不提供对它的支持。

    6.4.5 字符翻译

    需要关注的另外一个问题是字符转换和翻译,特别是关于字符扩展。例如,十六进制数0xFE和0xFF在UTF16下被扩展为四个字符。在解析代码中对这样的字符扩展缺乏有效的处理,是存在漏洞的一个重要方面。字符转换同样可能会被不正确的实现,尤其是在处理很少被看到或使用的边界数据时。例如微软的Internet浏览器,在从UTF-8转换为Unicode时就被该问题所影响 。该问题的症结在于,当转换程序为存储转换后的缓冲区而确定动态分配存储区的大小时,没有正确的处理5字节和6字节的UTF-8字符。然而,实际的数据拷贝程序却正确的处理了5字节和6字节UTF-8字符,因此就导致了基于堆的缓冲区溢出。启发式模糊数据列表中应当包含这些以及其它类似的恶意字符序列。

    6.4.6 目录遍历

    目录遍历漏洞会影响网络daemon程序以及Web应用程序。通常的一个误解是认为目录遍历漏洞只限于影响Web应用程序。目录遍历漏洞确实在Web应用程序中很常见,然而,这种攻击方式同样也会出现在私有网络协议和其它应用方面。根据2006年Mitre CVE的统计,尽管随着时间的发展,目录遍历漏洞在某种程度上有所减少,但其仍然是在软件应用程序中所发现的第五大类漏洞 。这些统计包括Web应用程序和传统的客户端/服务器应用程序。可以在开源漏洞数据库(OSVDB)中看到多年来所收集到的许多目录遍历漏洞的例子 。

    例如,考虑ComputerAssociates公司的BrightStor ARCserve备份软件。BrightStor通过caloggerd程序和TCP协议为用户提供了注册接口,尽管该协议没有被公开,通过基本的包分析可以发现注册文件名实际上是在网络数据流中被指定的。用目录遍历修饰符给文件名加上前缀就允许一个攻击者指定一个任意的文件,将任意的注册消息写入到该文件中。当注册程序以超级用户的身份运行时,该漏洞就可以被发现。例如在UNIX系统中,一个新的超级用户可以添加到/etc/passwd文件中。在编写本书时,仍然存留着一个在安全补丁发布前而被了解和掌握的漏洞,计划于2007年7月公开发布。启发式模糊数据列表中还应当包含如../../和..\...\等的目录遍历修饰符。

    类似于目录遍历漏洞,命令注入漏洞通常与Web应用程序有关,特别是CGI脚本。再次重申,认为这类错误只与Web应用程序有关是一个普遍的误解,因为它们可以通过公开的私有协议来影响网络daemon程序。任何目标程序,无论Web应用程序还是网络daemon程序,向形如exec()或system()的API调用传递未经过滤或者过滤不正确的用户数据,都将潜在的暴露一个命令注入漏洞。

    在正常情况下,系统路径被服务器所接收,路径下面所列出的文件是确定的,并且列表将被返回给客户端。然而由于缺乏对输入进行过滤,因此可以指定特定的字符以允许执行额外的命令。对于UNIX系统而言,这些字符包括:&&,;和|。例如,参数var/lib;rm-rf/转换为ls/var/lib;rm-rf,该命令可以给被影响系统的管理者带来严重的问题。启发式模糊数据列表中同样应当包含这些字符。

    6.5 小结

    在本章的开始,讨论了自动化测试的必要性,同时简要列举并描述了可以用来简化开发的不同库和工具。其中,对某些库作了更为详细的解释,并应用到了本书第2和第3部分的自定义工具的开发中。针对数字、字符串和二进制序列,如何选择智能和高效的模糊数据是本章所讨论的一个核心问题。这些知识将在后续章节中得到应用并加以扩展。

    在后续章节中,我们将讨论不同的模糊测试目标程序,包括Web应用程序,私有命令行应用,网络服务程序等等。在阅读后续章节时要记住本章所讨论的概念。在模糊器的开发过程中可以应用所介绍的不同库和工具。思考一下本章所讨论的智能模糊数据,以及将来会向模糊数据列表中添加哪些值。

    第7章 环境变量和参数的模糊测试

    "这样的外交策略令人感到一丝沮丧。"

    --George W. Bush,引自2002年4月23日《 NewYork Daily News》

    可以证明,本地化模糊测试是最简单的一种模糊测试。尽管许多攻击者和研究者将通过发现远程和客户端的漏洞来获取更加重要的结果,但本地私有程度的增强仍然是一个重要的研究课题。即使利用一个远程攻击来获得对目标机器的访问,也通常要使用本地化攻击作为一种辅助攻击手段来获取所需要的私有信息。

    7.1 本地化模糊测试介绍

    用户可以通过两种主要的方法将变量引入到程序中。除了显而易见的标准输入设备,通常是键盘之外,命令行参数和进程-环境变量也可以表示输入变量。我们首先采用命令行参数的方式来实现模糊测试。

    7.1.1 命令行参数

    除了最纯粹的Windows用户之外,其他人可能都或多或少的遇到过需要命令行参数的程序。命令行参数被传递给一个程序,并通过在C程序中的main函数中声明的argv指针来寻址。变量argc也要传递给main函数,它表示了传递给程序的参数的个数,另外还要加上1,因为它所激活的程序名被作为一个参数来计算。

    7.1.2 环境变量

    用户将变量引入到程序中的另外一种方法是使用环境变量。每个进程都包含由环境变量组成的所谓的环境。环境变量是全局变量,它定义了应用程序的行为。用户可以设置或取消环境变量,但通常都在软件包的安装过程中或被管理员设置为典型值。大多数的命令解释器将导致所有的新进程都继承当前的环境。command.com shell是Windows中的命令解释器的一个例子。UNIX系统通常有多个命令解释器如sh,csh,ksh以及bash。

    通常使用的一些环境变量的例子包括HOME,PATH,PSI和USER。这些值分别保存了用户的主目录,当前可执行的搜索路径,命令提示符以及当前的用户名。这些特别的变量是很标准的;然而许多其它的公共变量,包括软件提供商所创建的那些变量,只能用于其应用程序的操作中。当一个应用程序需要某个特定变量的信息时,它只需使用getenv函数,该函数将变量名指定为参数。尽管Windows进程和和UNIX应用程序以相同的方式来包含环境变量,这里主要关注于UNIX系统,因为Windows没有setuid应用程序的概念,该应用程序可以被无特权的用户所启动,并在执行过程中获取私有信息。

    用户可以使用export命令来操纵该列表中的每个变量。在理解了命令行参数和环境变量的用法之后,就可以开始讨论对它们进行模糊测试的一些基本准则。

    7.2 本地化模糊测试准则

    环境变量模糊测试和命令行模糊测试的基本思想很简单:如果一个环境变量或者命令行选项包含一个非预期值,那么当该值被接收之后,应用程序将如何做出响应呢?当然,这里只对非正常运行的授权应用程序感兴趣。这是因为本地化模糊测试要求对机器进行本地访问。因此,只需要有限的值就可以使应用程序崩溃,你必须要自己执行拒绝服务的攻击。如果一个环境变量发生了溢出而导致被多个用户共享的系统或应用程序崩溃,那么就会带来一些风险。然而,我们最感兴趣的是在授权的应用程序中寻找一个缓冲区溢出,而该溢出将允许一个受限用户来提升其特权。寻找授权目标程序将在本章后面的"寻找目标程序"一节中进行讨论。

    许多应用程序都进行了这样的设计,即当其被激活时,能够从用户接收命令行参数。然后,应用程序便使用此数据来决定它应当进行的操作。几乎在所有UNIX系统中都能找到的'su'应用程序就是一个极好的例子。当用户不使用任何参数来激活应用程序时,它将会对根用户进行鉴别;然而,如果用户指定了一个不同于第一个参数的用户名,那么它将会切换到此用户,而不是根用户。

    考虑下面的C语言代码,它简要的说明了su命令如何根据不同的参数来采取不同的行为。

    命令行参数和环境变量是将变量引入到程序中的两种最基本的方式。对其进行模糊测试的思想很简单。当我们通过命令行向程序输入错误数据时将会怎样呢?这种行为会导致安全风险吗?

    7.3 寻找目标程序

    当执行本地化模糊测试时,通常在系统中只有少量的所需要的二进制目标程序。这些程序在执行时具有更高的优先级。在基于UNIX的系统中,这些程序很容易被识别,因为它们包含有setuid或者setgid位集。

    setuid 和setgid位表明当一个程序运行时,它可以要求提升其特权。对于setuid位,进程将拥有文件的所有者所具有的特权,而不是程序执行者的特权。在setgid位的情况下,进程将拥有文件的组所有者具有的特权。例如,成功的开发一个有setuid根用户和setgid组用户的程序,可能会生成具有这些特权的一个shell。

    使用find命令来构建setuid二进制代码的列表是很简单的,该命令是UNIX和与UNIX类似的操作系统中的一个标准工具。下面的命令可以完整的列出系统中所有的setuid二进制代码。它应当作为根来运行,以防止文件系统的读取错误。

    find命令是一个强有力的工具,它可以被用来发现特定的文件类型,设备驱动程序以及文件系统中的目录。在上面的例子中,我们只使用了find命令所支持的一小部分选项。第一个参数指明了我们将要搜索整个系统以及在根目录/下面的所有文件。type选项告诉find命令我们只对文件感兴趣。这意味着不会返回符号链接,目录或者设备驱动程序。-perm选项描述了我们所感兴趣的特权。-o选项的使用允许find命令使用逻辑操作符或or。如果一个二进制文件含有setgid位或者setuid位集,那么其值将为true,并且打印出该文件的路径。总之,该命令将搜索所有含有setuid位(4)或者setgid(2)位集的规则文件。

    7.3.1 UNIX文件许可说明

    在UNIX系统中,文件许可模型允许三种基本的不同类型的访问:读、写和执行。对每个文件而言,同样也存在着三种许可访问集,它们分别属于用户、组以及其它类型。在任意给定的条件下,实际上只有这些许可当中的一个被应用。例如,如果你拥有一个文件,则将会被用到的许可集就是该用户。如果你不拥有该文件,而组拥有该文件,那么组许可将会被应用。对于所有其它的情形,其它类型的许可将会被应用。

    在这个例子中,用户dude拥有该文件。那么该用户的许可包括读文件和执行文件。当然,由于该用户拥有文件,因此他或她可以任意修改这些许可。

    如果用户组中的其它成员试图访问该文件,则他或她将只能够执行该文件,而不能读取该文件。试图对文件的读取将因为无效的许可而失败。最后,所有其它的用户将被拒绝访问,他们不能读、写或者执行该文件。

    在UNIX系统中,存在一种特殊的方式以描述完全的文件许可。在此系统下,许可以八进制的形式被表达。也就是说,每一个许可组合都被赋予一个从0到7的值。读标志的八进制值是4,写标志的八进制值是2,执行标志的八进制值则为1。然后将这些值组合起来就得到了所有的许可。例如,如果一个文件允许用户、组和其它用户进行读和写操作,那么其值就为666。被dude所拥有的示例文件将被表达为值510--用户(5)=读(4)+执行(1),组(1)=执行(1),其它用户(0)=空。

    第4列代表特殊标志如setuid和setgid位。setuid位被表达为4,而setgid位则被表达为2。因此,含有setuid和setgid位的文件可能具有值为6755的许可。如果特殊标志列被省略,则将被假定为0,因此将不再具有任何扩展许可。

    7.4 本地化模糊测试方法

    环境变量和命令行参数可以由用户很方便的提供,由于它们几乎总是简单的ASCII字符串,因此在实际中可以对目标程序实施某些基本的手工测试。最简单的测试可能就是将HOME变量设置为一个长字符串,然后运行目标应用程序以查看其输出。使用Perl可以非常迅速的实现该测试,而Perl在大多数UNIX系统中都是默认可用的。

    这是一种非常基本的方法来测试应用程序,以查看其是否能够处理一个长的HOME变量。然而,这个例子假定你已经知道了应用程序使用了HOME变量。如果不知道应用程序使用了何种变量,情况又如何呢?如何来确定应用程序使用的是哪种变量呢?

    7.5 枚举环境变量

    至少可以采用两种自动化方法来确定应用程序使用的是什么环境变量。如果系统支持库的预加载,你就可以钩住getenv库调用。提供一个新的getenv函数以实现标准getenv函数的功能,并且也将调用注册到一个文件,那么就可以有效的记录应用程序需要的所有变量。该方法的一个扩展将在本章后面的"自动化的环境变量模糊测试"一节中做更加详细的介绍。

    7.5.1 GNU调试器(GDB)方法

    可以使用的另外一种方法需要一个调试器的支持。使用GDB,可以在getenv函数的内部设置一个断点,并且输出第一个参数。使用GDB脚本在Solaris10上进行自动化测试的例子。

    如果你对GDB中使用的命令还不熟悉的话,下面将简要对其进行介绍:

    break命令在一个特定的函数或地址设置一个断点。这里,使用break命令以在对getenv进行调用时,中断应用程序的执行。

    command命令指定了当遇到断点时将会发生的特定的行为。在这种情况下,让程序停止执行,然后使用x/s将i0寄存器的值作为一个字符串来输出。在SPARC系统上,i0寄存器中保存了被输入的该函数的第一个参数。

    下一个命令r是继续执行程序,因此我们不必在每个断点之后都通知程序继续执行。可以使用run命令的一个快捷键来开始程序的执行。

    使用这种方法,可以立即看到被/usr/bin/id程序所请求的11个环境变量的列表。注意,该方法应当在所有的系统上都能使用;然而,需要改变正在解除引用的寄存器的名字,因为不同的体系结构对它们的寄存器使用不同的记忆方法。例如,在x86架构下可能会输出$eax寄存器,在SPARC系统中输出$i0寄存器,而对于MIPS则输出$a0寄存器。

    现在,我们已经掌握了目标程序将会使用什么样的变量,下面将研究采用更加自动化的方式对它们进行测试。

    7.6 自动化的环境变量测试

    回顾一下在上一节"枚举环境变量"中简要提到的使用库预加载的技术,这对于自动化的模糊测试也是有用的。为了检索一个环境变量的值,必须要激活getenv函数。如果我们钩住getenv函数,并且对调用它的所有函数返回长字符串,则甚至不需要知道所使用的变量的列表;只需要通过截取所有对getenv的调用来模糊化每个变量。当执行一个快速检查以发现不安全的环境变量使用时,这是很有用的。

    下面的函数是getenv函数的一个简化实现。它使用了指向环境起始点的全局变量environ,该段代码简单的遍历environ数组,以查看被请求的值是否存在于environ数组中。如果存在,则返回一个指向该值的指针;如果不存在,则返回NULL以表明该变量没有被设置。

    7.6.1 库预加载

    我们要讨论的下一个主题是库的预加载。库预加载本质上是通过使用操作系统链接程序来用用户提供的函数替换函数,从而提供了一种简便的方法来钩住函数。尽管该方法在不同的系统之间存在着差异,但基本思想是一致的。通常,用户为其所提供的库的路径设定一个特定的环境变量。这样,当程序执行时该库就被加载。如果该库包含程序中符号的副本,那么将用这些副本来替换原始符号。当提到符号时,这里主要是指函数。例如,如果一个用户使用strcpy函数创建了一个库,并且在运行二进制代码时将其预加载,那么二进制代码将调用用户版本的strcpy函数,而不是系统的strcpy函数。这样做是为了许多合理的目的,例如包装调用以实现切片和审核,同样对发现漏洞也具有一些作用。考虑包装getenv程序或完全替换该程序;该程序被用来请求环境变量的值。

    下面的函数是getenv的一个简单替换,可以被用来发现简单的长字符串问题。可以通过使用库的预加载技术来强制该函数改写实际的getenv。

    很容易看出,该函数对每一个变量请求都会返回一个长字符串。这里完全没有使用environ数组,因为我们不关心所返回的正确值。

    Dave Aitel的GPL sharefuzz工具使用了该方法,该工具被用来发现setuid程序中的大量漏洞。为了初始化该模糊测试,只需要将C代码编译到一个共享库中,并使用用户操作系统库的预加载功能(假定操作系统具有此功能)。对于Linux而言,可以通过如下的两步操作来完成。

    当/usr/bin/target执行时,所有对getnev的调用将使用修改后的getnev函数。

    7.7 检测问题

    现在,你已经熟悉了本地化模糊测试的基本方法,下面你需要进一步了解当目标程序中发生感兴趣的异常行为时,如何来识别所发生的问题,这在很多时候都是显而易见的。例如,程序可能会崩溃,并输出"段错误"或者其它一些严重的信号消息。

    然而,由于我们的最终目标是实现自动化,因此不能依赖于用户手工识别错误。为了实现自动化,我们需要一种可靠的、可编程的方法。至少存在两种比较好的方法来实现此目的。最简单的一种方法是检查应用程序的返回代码。在现代的UNIX和Linux系统中,如果一个应用程序因为一个未处理的信号而终止,那么shell的返回代码将等于128加上该信号数字。例如,一个段错误将导致shell接收十进制数为139的返回代码,这是因为SIGSEGV的值等于11。如果应用程序因为一个非法指令而终止,那么shell将接收一个值为132的返回代码,因为SIGILL的值为4。这其中的逻辑很简单:如果shell关于应用程序的返回代码是132或者139,那么就标志着一个可能的错误。

    你可能也会考虑到异常中断信号。由于在新版本的glibc中所介绍的更加严格的堆检查,使得SIGABRT更加引人注意。异常中断是这样一个信号,它可以在进程中产生以终止进程的执行并输出核心信息。尽管在特殊情况下进程可能在堆破坏处发生中断,但是有许多巧妙的方法可以将其进行扩展。

    如果你使用攻击性的sehll脚本来执行模糊测试,那么使用shell的返回代码将会变得有意义。然而,例如你正在使用一个用C或其它语言编写的适当的模糊器,那么可能就要使用wait或者waitpid函数了。在这种方式下进行模糊测试的一般方法是,在前面是由fork和一个execve函数构成的子进程,后面跟着wait或者waitpid函数构成的父进程。当使用正确时,就可以通过使用wait或者waitpid来检查所返回的状态,从而很简便的确定子进程是否崩溃。下一章将介绍从本地化模糊测试工具iFUZZ中所简化的一个代码片段,以说明该方法的使用。

    如果你关注于捕获可能被应用程序处理的信号(没有被前面所讲的方法所发现),那么除了钩住signal程序之外,至少还有一种可选的方法。你将要使用系统的调试API连接到该进程,并且在信号处理器被激活之前,截获它所接收的信号。对大多数的UNIX操作系统而言,你将会使用到ptrace。通常的方法是一个fork和一个ptrace以及execve构成父进程,后面跟着waitpid和ptrace构成的子进程,形成一个循环,以持续不断的监视进程的执行情况,截获并分析可能产生的信号。每当waitpid在子进程中返回时,就意味着程序已经接收到了一个信号,或者程序已经终止运行。此时,必须要检查由waitpid所返回的状态,以确定到底发生了什么事件。同时,必须要显式的告知应用程序继续执行,并且在大多数情况下都传递信号。该操作也可以使用ptrace来完成。在SPIKEfile和notSPIKEfile中的实现可以作为此通用方法的一个参考。这两种工具被用来实现文件的模糊测试,将在第12章"文件格式模糊测试:UNIX平台上的自动化测试"中对它们做详细的介绍。下一章提供了一个代码片段用以说明该方法。

    在大多数情况下,ptrace方法对于本地化模糊测试而言是足够的。很少的setuid UNIX应用程序为象SIGSEGV和SIGILL等的信号大量的使用信号处理器。同样,一旦你开始使用ptrace,那么就意味着这些代码没有必要在不同的操作系统和体系架构中具有兼容性。可以考虑一下如果你正在创建一个不需修改就可以运行在不同平台上的应用程序的情形。

    在下一章中,提出了一个简单的命令行模糊器的实现,它被设计为只在具有C编译器的UNIX系统中进行编译和运行。该工具也包含一个针对getenv钩子函数的共享库模糊器。

    7.8 小结

    尽管发现本地化漏洞不太受重视,但是对于查找一个私有化的错误而言仍然是非常有价值的。本章阐述了自动化发现这些类型漏洞的一些不同的方法,在下一章中,我们将具体实现其中的某些方法,以实际发现一些错误。

    第8章 环境变量和参数的模糊测试:自动化

    "大规模杀伤武器已经存在于某些地方。"

    --George W. Bush, Wahington, DC, 2004年3月24日

    本章介绍了针对本地化应用程序实施模糊测试的一个程序,即iFUZZ。这里的主要测试目标是在第7章"环境变量和参数的模糊测试"中所讨论的setuid UNIX程序中的命令行参数和环境变量。在本章中,将讨论iFUZZ的特性,解释其设计决策,并且讨论iFUZZ如何被用来发现IBM AIX5.3中的大量的本地化漏洞。

    8.1 iFUZZ本地化模糊器的特性

    iFUZZ包含你能够想象得到的关于本地化模糊器的一些特性。这些特性分别是,存在一个引擎以自动化的处理不同的二进制目标代码,为了简化错误再现而输出C语言触发器的功能,实现模糊测试的一些不同的方法以及模块化实现等。在iFUZZ的这些特性中,最方便的一个是它可以不经修改就能够运行在几乎任何UNIX或者类似于UNIX的操作系统中。它已经在IRIX,HP-UX,QNX,MacOS X和AIX系统中发现了一些漏洞。该模糊器针对每一种类型的模糊测试都具有不同的模块:

    Argv fuzzer模块。前两个模块是非常类似的,因此可以放在一起来说明。它们将可执行程序的argv[0]和argv[1]的值进行模糊化。这两个模块的基本用法很简单:指定目标应用程序的全路径并使其开始运行。然后它将试图使用不同长度的字符串以及特殊格式的字符串。实际所使用的字符串依赖于模糊字符串数据库,可以用终端用户所提供的简单字符串作为补充。

    Single-option/multioption fuzzer(单选/多选模糊器)模块。你可以将此模块认为是一个闭塞的模糊器;也就是说,用户并不向该模块提供有关目标应用程序的任何信息,并且该模块也不关心。它只需要在目标程序中为每一个可能的选项抛出一个字符串值。

    FUZZSTRING是从iFUZZ内部的模糊测试字符串数据库中取出的一个字符串。这是发现简单的和选项相关的问题的一种快速方法,但是不能发现更加复杂的问题,例如需要多个选项值的那些问题。

    getopt模糊器。该模块需要从用户方面得到某些信息。也就是说,它需要知道应用程序通过getopt所使用的选项字符串,这样,它就可以将应用程序将要使用的选项进行模糊化。尽管该模糊化可能是非常耗费时间的,但是它比其它的模糊化类型更加彻底。使用象这样的一个模块,你可能会发现比使用其它的模糊器更加复杂的一些问题。例如,考虑一个应用只有当被设置了调试和冗余选项,并且提供了一个长的-f参数时,该应用程序才会触发一个缓冲区溢出。

    基于所输出的使用信息,我们可以确定针对该应用程序的getopt字符串极有可能形如f:o:vds。这意味着选项f和o是作为一个参数,而选项v,d和s只是作为转换。这是如何得知的呢?根据getopt的使用手册:

    选项参数是一个字符串,它指定了对应用程序而言是有效的选项字符。该字符串中的一个选项字符的后面可以跟着一个冒号(':'),以指明它获得了所需要的参数。如果一个选项字符的后面跟着两个冒号('::'),则表面其参数是可选的;这就是一个GNU扩展。

    如果使用f:o:vds以getopt模糊方式来运行iFUZZ,那么将会很快的发现我们所描述的漏洞。由于iFUZZ也会随机的向一个现存的文件提供路径以作为模糊字符串,你甚至可以发现需要其中某个选项而成为有效文件的那些漏洞。由于iFUZZ所采用的设计方法,使得能够较容易的对它进行创造性使用。也可以向字符串数据库中添加一些其它的有效字符串,例如用户名、主机名、有效的X显示等等。你所能进行的创造性工作越多,那么你的模糊器的功能就越强大。

    iFUZZ的内部也包含有一个简单的可预加载的模糊器。它包含你期望返回的、未修改的一个变量数组,但却模糊化其它所有的数据。它只是对Dave Aitel的共享模糊器增加一些额外的功能,如果需要的话,它允许你从实际变量中返回数据。它实际上不是iFUZZ的一个核心构件,更象是一个快速执行的工具。

    iFUZZ内部同样包含一个简单的getopt钩子,它可以被预加载以从目标二进制代码中输出getopt选项字符串。尽管这是有用的,但实际上它是个只有一行代码的C程序,只是为了便于使用而被包含。当使用该钩子时,要记住某些应用程序使用它们自己的函数来解析命令行选项。对于这些应用程序而言,必须要基于使用函数的输出来手工的构建getopt字符串。

    8.2 iFUZZ的开发

    前面列出了iFUZZ工具所具有的特性,下面将要介绍一些具体的实现细节。为了全面的理解iFUZZ的构造过程,建议读者从fuzzing.org下载并分析该工具的源代码,这里只是重点阐述一些关键功能,并分析一些特定的设计决策。

    8.2.1 开发方法

    iFUZZ所采用的开发方法是模块化和可扩展化。这使得iFUZZ能够很方便的确定应用程序的基本功能,而不用在前面所讲的各种模糊测试方法上花费大量的时间。一旦主引擎被开发完成,实际上就开始了各个模块的开发。

    在主引擎和帮助函数开发完成之后,第一个被开发的模块是argv[0]模糊器。一个测试集可以在若干个不同的操作系统,包括QNX,Linux和AIX上被使用,该测试集是针对argv[0]溢出漏洞的二进制代码、格式化字符串以及被安全编写的应用程序的。该测试集被用来消除任何的小错误并确定准确的输出。

    作为一个开发者,你并不总是具有以前存在的具有漏洞的二进制代码的知识,以将其用于你的测试集中,因此你可以创建自己的一些具有漏洞的应用程序例子。

    可以将该段代码进行编译并放进一个包含一些已知的安全二进制代码的测试集中,以发现漏报和误报漏洞的问题。

    将格式化字符串作为一种攻击手段

    格式化字符串漏洞已经存在一段时间了,而它们曾经被认为是没有危害的。1999年,在经过了对proftpd1.2.opre6的安全性审核之后,Tymm Twillman向BugTraq邮件列表 发布了一个研究细节,即该问题允许攻击者使用格式化字符串漏洞来改写内存。该漏洞是由snprint()函数所导致的,该函数不使用格式化字符串而传递用户输入 。

    接下来被实现的模块是单选项模糊器。尽管它的基本思想也很简单,但将其用于许多商用UNIX系统时是非常有效的。它的设计思想很简单,并且不具有实际的挑战性。使用一个简单的循环来遍历整个字母表,每次使用被选择的字符作为程序的一个选项。该选项的参数取自于模糊数据库。

    当这些简单模块被实现之后,iFUZZ就可以发现一些漏洞了,但是却发现不了任何更加复杂的漏洞,如那些需要多个选项才能被发现的漏洞。这就引入了接下来的两个模块的开发,即多选项和getopt模糊器。

    多选项模糊器的设计思想很简单;它实际上是在一个单选模糊测试循环内嵌套了另外一个单选模糊器。可以在命令行中指定在任意时间所使用的选项的最大数量。所指定的选项越多,则循环的深度就越大。

    为了使多选模糊器更加有效,并且提高执行的效率,getopt模糊器应运而生。它与多选模糊器基本上是相同的,所不同的是它不再试图将所有值作为选项,而是给出了对应用程序而言有意义的值的子集。尽管该技术降低了模糊器功能的全面性,但却提高了其执行速度,并且能够更快的发现漏洞。

    我们设计了两种简单的方法来捕获异常。第一种方法将不会捕获已处理的信号,但正如在上一章中所提到的,在UNIX下只有很少的应用程序会处理我们想要捕获的信号。

    Fork,Execute和Wait方法

    下面列出了fork,execute和wait方法的一个简单的实现:

    fork,Ptrace/Execute和Wait/Ptrace方法

    下面的C代码片段说明了如何来控制并监视一个进程,以找到感兴趣的信号,即使这些信号被应用程序在内部所处理。这个例子是从notAPIKEfile和SPIKEfile所共享的代码中截取的一个片段,文件模糊测试工具notAPIKEfile和SPIKEfile将在第12章"文件格式模糊测试:UNIX平台上的自动化测试"中被讨论。

    8.3 iFUZZ的开发语言

    开发iFUZZ所选用的是C语言。尽管可以琢磨出一些选用C语言不恰当的理由,但是不能掩盖这样一个事实,即C语言被选中主要是因为我对它情有独钟并且经常使用它。

    然而,C语言除了成为我的首选语言之外,还具有某些优点。例如,大多数的UNIX系统,即使是早期的系统,也都安装有C编译器组件。但对于Python或者Ruby等语言可能就不是这样。Perl语言可能也会作为一个选择,因为它在UNIX系统中有着非常广泛的应用。然而,Perl代码非常难以维护。

    在开发的时候,为象这样的项目选择一种如Python或Perl等的语言所带来的益处是非常明显的。使用这些脚本语言,可以极大的缩短开发时间。

    最终,由于我们对C语言的熟悉程度,以及想要开发一个不象用其它脚本语言构造的更小的模糊器那样的攻击性模糊器,使得选定了C作为开发语言。

    8.4 实例研究

    使用iFUZZ,可以识别IBMAIX5.3中超过50个的本地化漏洞,每个漏洞对于彻底测试者或黑客解密者而言都具有不同程度的有用性。这些漏洞中的绝大多数都可以被简单的发现,它们只是隐藏在argv[0]和argv[1]的后面。然而,有一些漏洞更加有趣,并且发现的难度更大。当我们提到难度时,并不是意味着找到这些漏洞需要任何程度的技巧。实际上意味着当准备iFUZZ命令行选项以及等待iFUZZ的输出结果时,需要更加的耐心。下面的两个漏洞(发生在一个setuid根二进制代码中)说明了iFUZZ在系统中作为本地化模糊器所具有的作用和有效性:

    尽管因为开发此代码需要访问printq组,使得这个例子中的目标二进制代码并没有对系统的安全性带来严重的威胁,但只是为了触发其中的漏洞,所以仍将其作为一个示例。

    为了发现这些问题,阅读了该二进制代码的手册,并且为iFUZZ生成了一个getopt字符串。所使用的getopt字符串是a:A:d:D:p:q:Q:s:r:w:v:

    下面列出的第一个命令的输出显示了该二进制代码的位置和许可:

    经过一长段的运行时间之后,iFUZZ将会输出至少两个该二进制代码中有趣的和可能开发的漏洞。在前面所展示的漏洞中,LONGSTRING表示一个大约有20000个字符的字符串,X表示任意合理的字符串,例如字符串x自身。X必须要和长字符串一起使用来到达存在漏洞的代码。

    尽管发现象前面那样的触发难度较大的漏洞是很有意思的,但通过遍历iFUZZ的所有基本模块来查看能够发现多少个简单的错误同样是很有趣的。在AIX5.3的setuid二进制代码中以argv[0],argv[1]或者单选模式来运行iFUZZ,那么你将会花费许多时间并发现足够多的堆溢出错误,栈溢出错误以及格式化字符串问题。自从这些漏洞被作者和其它一些独立的研究者使用iFUZZ发现之后,其中的许多漏洞已经向IBM做了报告,并得到了修复,这些研究者都使用了类似的模糊测试的概念来发现这些漏洞。

    8.5 益处和改进的余地

    在阅读本章之后,希望你对iFUZZ中有用的东西产生一些自己的想法。同样重要的是你已经认识到了iFUZZ的一些弱点。下面列出的是在iFUZZ的开发之后,所做的一些观察。

    首先,iFUZZ除了使用一些硬编码的休眠值来试图控制加载之外,并没有考虑到使系统崩溃的可能性。这将是一个很好的特性,即它能够分析系统加载以查看该休眠值是否需要增加以阻止系统挂起或者出现漏报和误报的问题,而这些问题在过载的情况下可能会发生。

    期望有一种特别的选项,它可以确定触发系统崩溃所需要的近似字符串长度或者绝对的最小值。这是一个微不足道的特性,但可以在很多情况下节省时间。

    iFUZZ所不具备的另外一个小的特性是在系统中自动定位所有的setuid和setgid二进制代码的能力。这是另外一个可以节省时间的特性。

    从一个给定的二进制代码中解析选项的能力将是一个重要的特性,实现该特性是很重要的。如果用户可以指定一个二进制代码,并且iFUZZ可以自动的确定应用程序需要哪些标志和选项,那么将会生成一个智能的完整的模糊器,并且需要最少的用户交互。当然,这并不是一个简单的实现,因为许多应用程序使用许多不同的格式。即使该特性得到了一个相当完整的实现,那么包含一种新类型使用函数的应用程序也不能被当前的实现代码所解析。也就是说,将该特性设置为可扩展的,这样用户就可以为他们所遇到的不同应用程序来添加新的格式。

    我们所认为的微不足道、无价值的实现就是C代码生成能力。在系统崩溃后能够生成C语言程序就意味着漏洞可以被手工的及时的再生。所生成的代码甚至可能会被用做发掘漏洞的基础。这将节省漏洞发掘者的时间,特别是当为触发漏洞而编写单独的C程序的时间开始增加时,对含有许多漏洞的脆弱系统进行模糊测试非常节省时间。

    8.6 小结

    以前,远程漏洞被给予了太多的关注,现在,客户端漏洞和本地系统的安全问题相对来说存在着更多的研究空间。通过实现一个本地化模糊器,就可以使用很少的研究时间来揭露这些研究内容以及更加复杂的漏洞,这也证明了模糊测试是非常有价值的。

    第9章 Web应用程序和服务器模糊测试

    "我是低期望的主人。"

    --George W. Bush, 于"aboard Air Force One", 2003年6月4日

    现在,我们从本地化模糊测试转向对客户机-服务器体系架构进行模糊测试。特别的,我们主要关注于Web应用程序和Web服务器的模糊测试。正如我们所讨论的,对一个Web应用程序进行模糊测试也可以发现其底层的Web服务器中的漏洞,但是为了简便起见,我们将这种类型的模糊测试只作为Web应用程序的模糊测试。尽管其基本概念与前面所讨论的网络模糊测试基本上是一致的,但我们还是要做出一些改动。首先,Web应用程序的输入数据量很大,并且输入经常处于不明显的位置,因此需要对输入向量的组成进行重新定义。其次,需要改进漏洞检测机制以能够捕获并分析Web应用程序所产生的错误消息,以揭露一个漏洞条件。最后,为了使Web应用程序的模糊测试实用化,需要建立一种通过网络发送输入的体系架构来弥补性能上的损失。

    9.1 什么是Web应用程序模糊测试

    Web应用程序模糊测试是一种特殊形式的网络协议模糊测试。网络协议模糊测试(在第14章"网络协议模糊测试"中所讨论)将任意的网络包进行变异,而Web应用模糊测试则特别关注于遵循HTTP规范的包。鉴于Web应用在当今世界的广泛应用和具有的重要性,我们将Web应用模糊测试作为一种单独的方法来论述。目前,相对于传统的软件产品被直接安装到本地计算机上而言,提供商更多的是通过Web将软件作为一种服务而发布。Web应用程序可以部署在使用它们的实体上,也可以部署在第三方的机器上。当一个第三方机器被包含时,通常称之为应用服务提供者(Application Service Provider,ASP)模型。

    Web应用为软件提供商和终端用户都提供了许多的优越性。为提供商带来了利润并允许及时的更新,包括安全性修复。终端用户不再需要下载并应用补丁,因为应用程序被驻留在一个中心服务器上。从一个终端用户的视角来看,这也意味着减少了应用程序的维护开销,因为维护工作是由ASP来完成的。即委托ASP来维护适当的安全性以保护你的私有数据免受攻击。因此,Web应用的安全性应当作为关注的一个重点问题。

    微软LIVE

    微软在Web应用不断增强的重要性方面下了很大的赌注。尽管其传统的产品是GUI应用程序如微软Office,但在2005年11月,微软总裁比尔 盖茨宣布发布了两个基于Web的应用:WindowsLive和OfficeLive 。WindowsLive是针对个人服务的一组集合,而OfficeLive则面向小的商业应用。微软将通过广告和订购模型来从这两个服务中获取利益。服务的Live集最早出现在2002年的Xbox Live的发布中,Xbox Live是Xbox视频游戏控制台的一个在线社区和市场。

    开发Web应用的编程语言和开发环境的用户友好性都在不断的提高。因此,现在许多公司都在开发他们自己的Web应用以在企业内部的互联网络中使用,并作为一种手段来方便与商业伙伴和客户之间的交流。现在,任何公司不论其规模大小,实际上都可以被认为是一个软件开发者。由于技术所带来的先进性,甚至一个人的公司也有能力开发并部署Web应用。然而,尽管开发一个Web应用相对较为容易,但开发一个安全的Web应用就不那么容易了。当一个Web应用被软件开发实力不是很强的公司所开发时,那么该应用在部署之前很难通过严格的安全性测试。不幸的是,测试Web应用安全性的那些易于使用并且免费的程序在技术上要落后于所开发的Web程序。本书的目标之一就是要改变这种状况。下面对Web应用程序开发过程中常用的一些技术做了简要介绍。

    Web应用相关技术

    CGI

    公共网关接口(CommonGateway Interface,CGI)最早是由美国国家超级计算机应用中心(National Center for Supercomputing Applications,NCSA)在1993年为NCSAHTTPd Web服务器而开发的一个标准。CGI定义了数据应当如何在Web客户端和代表Web服务器来处理该请求的应用程序之间进行传递 。尽管CGI可以使用任何一种语言,但绝大多数都是用Perl来实现的。

    PHP

    超文本预处理器(HypertextPreprocessor,PHP) 是一种经常被用来开发Web应用的流行的脚本语言。解释器的应用范围很广以允许PHP可以应用在大多数的操作系统上。PHP脚本可以被直接嵌入到HTML页中以处理需要动态生成的Web页部分。

    Flash

    Flash最初是由FutureWave软件公司所开发的,该公司于1996年12月被Macromedia公司所收购 。在这里所谈及的技术中,Flash在某种程度上具有唯一性,因为它要求安装独立的客户端软件。而提到的其它大多数技术则只需要一个Web浏览器。

    Macromedia通过将Macromedia Flash播放器变为免费可用的,从而能够扩大Macromedia Flash的被接受范围。Flash使用一种被称为ActionScript的语言来处理大多数的交互特性。尽管Flash主要是一种客户端技术,而Macromedia Flash远程化则允许Flash播放器和Web服务器应用之间进行交互 。Macromedia自身也于2005年被AdobeSystem公司所收购 。

    JavaScript

    Netscape于1995年的后期创建了Javascript,作为一种手段以允许在Web页中处理动态内容 。Javascript可以被应用为一种客户端或者服务器端技术。当应用在客户端时,Javascript被嵌入到发送给Web浏览器的HTML中,并且直接被浏览器解释。Javascript也可以被应用为一种服务器端技术,Web服务器在创建动态内容时使用该技术。其它的服务器端技术如微软的ASP.Net(见下面)对包含Javascript提供相应的支持。

    Java

    Java是James Gosling在Sun Microsystems的创造性产物。它最初的名字是Oak,被设计用来在嵌入式系统中运行。后来被用做了基于Web的技术,当发现Oak已经被注册了商标之后,它的名字也被改为了Java 。Java是编译型语言和解释型语言的一个交叉产物。Java的源代码被编译成字节码,然后字节码又被运行在目标平台上的Java虚拟机进行解释。这种机制使得Java具备了平台无关性。它经常被Web服务器使用以发布复杂的交互式应用。

    ASP.Net

    .Net是一种开发平台,而不是一种语言。它包含一个公共语言运行时(Common Language Runtime,CLR)环境,该环境可以被包括Visual Basic和C#在内的许多语言所使用。微软于2002年引入了.Net,并将其作为包括Web应用在内的许多应用程序的开发平台。它和Java具有类似性,因为其源代码也被编译为称作公共中间语言(Common Intermediate Language,CIL)的一个中间字节代码,然后该中间代码再被虚拟机所解释 。可以使用具有语言无关性的ASP.Net来设计Web应用。而ASP.Net应用程序则可以使用任意与.Net兼容的语言来编写。

    9.2 目标应用

    Web应用模糊测试不仅可以发现Web应用自身的漏洞,而且还可以发现其底层任何构件中存在的漏洞,包括可能和Web应用集成在一起的Web服务器和数据库服务器。虽然Web应用所包含的应用程序类型非常广泛,但仍然存在着一个公共的分类体系。下面就列出了这样的分类情况,并且给出了特定应用中存在的漏洞的一个示例,这些漏洞可以使用模糊测试技术被潜在的识别:

    Web邮件

    微软OutlookWeb Access Cross-Site脚本漏洞

    http://www.idefense.com/intelligence/vulnerabilities/display.php?id=261

    讨论板

    phpBB组 phpBB任意文件揭露漏洞

    http://www.idefense.com/intelligence/vulnerabilities/display.php?id=204

    Wikis

    Tikiwiki tiki-user_preferences命令注入漏洞

    http://www.idefense.com/intelligence/vulnerabilities/display.php?id=335

    Web日志

    WordPress Cookie cache_lastpostdate变量任意PHP代码执行

    http://www.osvdb.org/18672

    企业资源计划(ERP)

    SAP Web应用服务器sap-exiturl Header HTTP响应断开

    http://www.osvdb.org/20714

    日志分析

    AWStats远程命令执行漏洞

    http://www.idefense.com/intelligence/vulnerabilities/display.php?id=185

    网络监视

    IpSwitch WhatsUp Professional 2005(SPI)SQL注入漏洞

    http://www.idefense.com/intelligence/vulnerabilities/display.php?id=268

    多提供商Cacti远程文件包含漏洞

    http://www.idefense.com/intelligence/vulnerabilities/display.php?id=265

    以上并不是所有Web应用类型的一个完整的列表,但是它对通过Web来发布自身应用的那些应用程序类型提供了一种说明,并通过实例阐述了它们可能会具有的漏洞类型。

    9.3 测试方法

    在开始对一个Web应用进行模糊测试之前,必须首先要建立目标环境,然后为目标环境选择输入向量。Web应用在这两方面都具有一些特有的挑战。按照设计,Web应用的体系架构可以被部署在多个网络机器上。当在开发环境中部署这样一个应用时,尽管具备了必要的可测量性,但却带来了模糊测试中性能上的下降。除此之外,Web应用的输入可以采用多种方式来掩饰,这些都可导致漏洞的产生。因此,当定义模糊测试所需要的输入时,必须要采取更加灵活的方法。

    9.3.1 建立目标环境

    模糊测试通常要求能够快速、连续的向目标程序发送大量的输入。一个单一输入的事件序列包括:在本地产生输入,将输入发送到目标应用,允许目标应用对输入进行处理,监视输出结果。因此,运行一个模糊器所需要的时间也就由该序列中运行速度最慢的环节所决定。当对一个本地应用进行模糊测试时,整个测试过程的瓶颈是CPU周期以及硬件的读/写时间。在现代计算机硬件速度的支持下,这些时间可以减少到最小,因此模糊测试对于研究漏洞是一个可行的方法。

    对于Web应用模糊测试而言,瓶颈点在于由模糊器向目标应用所进行的网络包的传输。考虑一下从远程地点加载一个Web页的过程。当浏览一个Web页时,页面的显示速度是由以下三个速度来决定的:你的计算机,该页所在服务器,以及位于二者之间的Internet连接。你只能控制这三个变量中的第一个变量,即你自己的计算机。因此,当对一个Web应用进行模糊测试时,通过去除其它两个变量来提高网络通信的速度是非常重要的。在可能的情况下,与其让目标应用在一个远程服务器上运行,不如将其部署到本地,这样包就不需要通过网络进行传输了。大多数的桌面操作系统如WindowsXP 或Linux都具有内嵌的Web服务器,因此,通常的选择是将目标应用安装并配置在本地机器上。

    当对一个Web应用进行模糊测试时,一个虚拟机(VM)应用如VMWare 或微软的虚拟机 也是一个很有用的工具。使用一个虚拟机,当模糊器在本地运行时,被测目标Web应用可以运行在一个VM实例中。这种方法提供了在本地运行目标应用所不具备的一些优越性。首先,目标应用所消耗的大量的资源可以通过VM来更好的管理。这就确保了在模糊测试过程不会消耗掉所有的机器资源。其次,引起系统崩溃和拒绝服务攻击的网络通信将不会影响模糊器所运行的本地机器。最后,这些正是我们试图要发现的异常,如果机器持续加锁那么将很难做到这一点。

    9.3.2 输入(1)

    在开始模糊测试之前,首先必须要确定Web应用所提供的不同输入,因为这些输入将成为模糊测试活动的目标。请看下面的问题:你认为输入应当包含什么呢?很明显,Web表单中的数据字段可以作为输入,但是URL自身或者发送给Web服务器的cookies能否作为输入呢?Web请求中的头部又如何呢?答案是所有这些内容构成了输入。按照我们的观点,一个输入被认为是发送给Web服务器并被Web服务器解释的所有信息。

    我们将马上对所有可能的输入进行分类,但首先让我们来分析一下产生Web请求的过程,以便更好的理解其机理。多数人使用Web浏览器如微软的Internet Explorer或Mozilla Firefox来访问一个Web页。当一个Web浏览器配置好之后,只需简单的在地址栏中输入URL地址就可以访问Web页的内容了。

    然而,Web浏览器隐藏了当请求一个Web页时所发生的许多实际活动。为了更好的理解所有这些活动,让我们手工的请求一个Web页。可以通过使用包含在大多数现代操作系统中的telnet程序来完成请求。telnet连接到你所选择的一个主机和接口,然后开始发送和接收TCP包。

    下面来分析一下该请求。首先,启动telnet程序并向它提供两个参数:服务器名(fuzzing.org)和要连接的端口(80)。按照默认设置,telnet将连接到TCP端口23。然而,由于我们是手工的向Web服务器发送一个请求,因此需要强制它使用TCP端口80。接下来的代码行则表示了HTTP协议所要求的最小化请求。首先,将使用的请求方法(GET)告知给服务器。本章的后面将详细介绍不同的请求方法。然后发送正在请求的路径和/或Web页。在这种情况下,我们请求服务器所请求的默认Web页(/),而不是请求一个特定的Web页。同样在该行中,向Web服务器指明了所要使用的HTTP协议的版本(HTTP/1.1)。下一行指定了主机,这在HTTP1.0中是可选的,但在HTTP1.1中是强制使用的 。当该请求被提交时(注意需要使用两个回车来完成请求),服务器将会返回类似于下面所示的响应:

    所接收到的响应是Web页的HTML源代码,前面的一系列头信息向浏览器提供了有关响应的额外信息。如果你将该响应的HTML部分保存到一个文件中,并且用一个Web浏览器将其打开,那么你将会看到所显示的页面,该页面同简单的使用URL所获取的页面是一样的。然而,假定使用的是相对链接而不是绝对链接,那么其中可能会丢失一些图像。

    我们已经提到,前面这个请求代表了HTTP协议所要求的最小格式化请求。那么还能向Web服务器提供哪些其它输入呢?为了回答这个问题,首先来分析使用Internet Explorer Web浏览器所发送的请求。使用Ethereal,我们可以发现下面的请求:

    所有其它额外的头信息的含义是什么呢?HTTP协议定义了许多头信息,而每个头信息则有一些可以接受的值。HTTP/1.1协议的详细内容参见RFC2616的第176页--超文本传输协议(Hypertext Transfer Protocol,HTTP/1.1) 。这里并不试图阐述整个文档,而只对前面所遇到的一些示例头信息做一解释:

    Accept:*/*

    Accept头信息指定了可以在响应中使用的媒体类型。在上面的例子中,表示所有的媒体类型(*/*)都是可以接受的。然而,我们可以限制响应只接受特定的媒体类型如text/html或者image/jpeg。

    Accept-Language:en-us

    Accept-Language允许用户指定可以在响应中使用的自然语言的类型。在上面的例子中,要求响应使用美国英语。RFC1766中定义了语言标签的正确的格式,即识别不同的各种标签 。

    Accept-Encoding:gzip,deflate

    该头信息同样是为响应指定一种可接受的格式,它定义了可接受的编码模式。在上面的例子中,我们所发送的请求表明可以使用gzip 或者deflate 编码算法。

    User-Agent:Moailla/4.0(compatible;MSIE 6.0;
    Windows NT 5.1;SV1;.NET CLR 1.1.4322;.NET CLR 2.0.50727)

    User-Agent定义了发出请求的客户端(Web浏览器)。这对服务器而言是非常重要的,因为它允许对响应进行剪裁以适应不同浏览器所支持的不同功能。此例中的User-Agent定义了在WindowsXP SP2上运行的微软Internet Explorer6.0。

    Host:www.google.com

    该头信息定义了为被请求的Web页提供服务的主机和端口。如果没有包含端口,那么服务器将使用默认的端口。这个字段对于服务器而言是很重要的,因为单一的IP地址上可以有多个主机名被提供。

    Connection:Keep-Alive

    Connection头信息允许客户端指定连接所需要的不同选项。一个持久的连接允许多个请求被发送,而不用为每个请求打开一个单独的连接。Connection:close意味着一旦响应被发送出去,连接就应当立即被关闭。

    Cookie :PREF=ID=32b1c6fa8e9e9a7a:FF=4:LD=en:
    NR=10:TM=1130820854:LM=1135410309:5=b9I4GWDAtc2pmXBF

    Cookies可以在计算机的本地硬盘或者内存中被保存一段指定的时间,当当前任务完成之后它们将被抛弃。Cookies允许服务器识别请求者。这使得一个网站可以跟踪用户的上网习惯,并且为用户潜在的定制Web页。如果针对一个特定网站的cookie存在于本地,那么浏览器将在发送请求时将其提交给服务器。

    现在,我们已经研究了一个请求的例子,这更加有利于定义对Web应用进行模糊测试可用的不同输入。正如前面提到的,发送给Web服务器的请求数据的任何部分都可以被认为是一个输入。在高的层次上,这包括请求方法,请求的统一资源定位符(Uniform Resource Identifier,URI),HTTP协议版本,所有的HTTP头以及发送的数据。在下面的节中,我们将分析每个组成部分,并为每个成分确定合适的模糊测试变量:

    方法(Method)

    GET和POST方法是请求一个Web页最常用的两个方法。这两个方法使用名-值对的方式从Web服务器请求特定的内容。可以将它们认为是一个Web请求中的变量。GET方法在请求URI中向Web服务器提交名-值对。例如,http://www.google.com/search?as_q=security&num=10将向google搜索引擎提交一个请求,以告知它我们想要搜索的词为security(as_q=security),并且在每个返回页中显示10个 搜索结果(num=10)。当使用GET方法时,这些名-值对以字符?开头被追加到URI的后面,每个不同的名-值对之间用字符&隔开。

    名-值对也可以使用POST方法被提交。当使用POST方法时,名-值对作为HTTP头被提交,后面跟着其它标准HTTP头。使用POST方法的一个优点是可以发送任意大小的值。尽管HTTP规范并没有特别的限制URI的总长度,但是Web服务器和Web浏览器通常对此施加了一定的限制。如果一个URI超过了预期的长度,那么Web服务器将返回一个414(请求URI过长)状态。使用POST方法的缺点是URI不能被共享以使其他人访问一个动态生成的Web页。例如,人们经常通过定位在一个特定搜索之后生成的Google地图URI来共享地图和方位。猜想一下下面的地图将你引导到何处?

    http://maps.google.com/maps?hl=en&q=1600+Pennsylvanis+Ave&near=20500

    正如所提到的,在Web服务器请求中还可以使用其它的一些方法。下面简要描述了其它的有效方法:

    HEAD:类似于GET方法,但是服务器只返回响应头,并不返回被请求Web页的HTML内容。

    PUT:允许用户向Web服务器上载数据。尽管该方法并没有被广泛的支持,但是当PUT方法被支持而没有被正确的实现时,就已经发现了相关的漏洞。例如,微软的安全公告MS05-006 中提到的安全问题正是这样一个漏洞。该漏洞允许未授权的用户使用PUT方法向微软的SharePoint服务器上载数据 。

    DELETE:允许用户请求从Web服务器中删除一个资源。同样,尽管该方法没有被广泛的实现,但也应当对其进行完善的测试。不正确的使用该方法可能会导致攻击者通过删除Web服务器上的资源来拒绝合法用户的访问。

    TRACE:允许客户端提交一个请求并返回到发送请求的客户端。这对于调试网络连接性问题是很有用的,因为你可以查看到实际被发送给服务器的请求的结构。在2003年1月,WhiteHat Security的Jeremiah Grossman发表了名为"跨站点跟踪(Cross-Site Tracing,XST)"的白皮书 ,该书披露了客户端脚本如何被利用以允许恶意的Web服务器获取对第三方Web服务器中cookie值的访问权,而该第三方服务器支持TRACE方法。同样,测试应当识别出这样的情形,即TRACE在何处被隐式的支持。

    CONNECT:被保留以供一个代理使用,可以动态的转换为一个通道。

    OPTIONS:允许客户端询问Web服务器以确定服务器所支持的标准的和私有的方法。漏洞扫描者可以利用该方法以确定潜在的漏洞方法是否被实现。

    OPTIPONS方法也可以被用来确定一个Web服务器是否正在运行Internet信息服务器(Internet Information Services)的一个版本,该版本易受到WebDAV中的一个严重漏洞的影响,而WebDAV是为了更加便于Web内容的发布而对HTTP协议所做的一组扩展。该漏洞在MS03-007(Windows构件中未检查的缓冲区可能导致服务器的安全问题)中有详细的描述。当发出一个OPTIONS*HTTP/1.0请求时,支持OPTIONS选项的服务器将返回下面的请求,识别WebDAV是否可用 。下面列出的服务器响应表明WebDAV是不可用的,因为在Public头中没有列出任何的WebDAV扩展:

    接下来的响应包含了完整的头信息,在该头信息中包含了WebDAV所提供的扩展。当WebDAV在运行有微软IIS5.0的服务器上可用时,就表明了如果还没有实现适当的补丁,那么服务器就可能会存在MS03-007中的漏洞,

    请求URI(Request-URI)

    在传递了方法(method)之后,客户端将向Web服务器传递请求URI。请求URI的目的是定位被请求的资源(Web页)。它可以采用绝对URI的格式(例如http://www.target.com/page.html),或者相对URI的格式(如/page.html)。另外,除了定位服务器上的一个特定的资源之外,服务器自身也可以被定位。这是通过使用字符*来实现的,OPTIONS方法也需要这种实现。

    当对请求URI进行模糊测试时,URI的每个部分都可以被模糊化。以下面的相对路径为例来说明:

    该路径可以被划分为以下的组成部分:

    每个单独的项都可以而且应当被模糊化。其中一些项应当用已知的值来模糊化,这些值会导致前面所描述的一些漏洞,而另外一些项则应当采用随机值以确定服务器是否很好的处理了非预期的请求。随机值应当也包括大批量的数据以确定当服务器解析并解释数据时,是否发生缓冲区溢出。下面将分别研究每个单独的项:

    路径(Path)。当对路径项进行模糊测试时,最常被发现的漏洞就是缓冲区溢出和目录遍历攻击。缓冲区溢出通常在发送大量数据的时候被识别出,而目录遍历则可以通过发送连续的../字符序列来发现。在请求被解码之前,应当使用不同的编码模式以绕过输入验证程序。

    缓冲区溢出示例(Bufferoverflow example)。对于Macromedia JRun5发布之前的JRun4 Web服务器而言,在接收一个过长路径时易出现基于栈的缓冲区溢出 。当路径长度超过大约65536个字符时,该漏洞将被触发。这也就说明了当进行模糊测试时,包含超长变量的必要性。

    目录遍历示例(Directorytraversal example)。3Com的网络监视器应用中的一个漏洞是目录遍历漏洞的一个经典示例 。该应用使得Web服务器在21700端口监听TCP协议,发现在网络监视器5.0.2和早期版本中,一个包含连续../字符序列的简单URL将允许用户遍历webroot目录。应当注意到,类似这样的漏洞实际上是服务器中的漏洞,而不是应用程序中的漏洞。

    页(Page)。对公共页名进行模糊测试,可能会发现那些没有受到正确保护的页,以及导致缓冲区溢出漏洞的页。

    缓冲区溢出示例(Bufferoverflow example)。当过长的请求被发送给扩展名为.htr,.stm和.idc的文件时,微软IIS4.0将面临基于栈的缓冲区溢出的危险。该漏洞在微软的安全公告MS99-019中有详细的描述 ,并引起了许多公开的研究。

    信息泄漏示例(Informationleakage example)。"3Com OfficeConnect Wireless 11g Access Point"包含一个漏洞,即可以在基于Web的管理界面上访问敏感Web页,而不用提供适当的授权认证 。因此,请求一个页如/main/config.bin将显示该页的内容,而不是一个注册提示,并且包括管理者的用户名和密码等信息。Nikto 就是搜索此类漏洞的应用程序的一个例子,它通过向Web服务器发送重复请求来发现公共的以及可能不安全的Web页。

    扩展(Extension)。与本地文件一样,Web页的文件扩展名通常也指明了生成Web页所采用的技术。Web页文件扩展名的例子包括*.html(超文本标记语言,HyperTextMarkup Language),*.asp(动态服务器页,Active Server Page)和*.php(超文本预处理器,Hypertext Preprocessor)。当请求具有未知扩展名的Web页时,就已经在Web服务器中发现了漏洞。

    9.3.2 输入(2)

    名字(Name)。对公共名字项进行模糊测试可以发现发送给服务器的未定义的变量。同样,如果应用程序没有足够的错误处理能力,那么发送非期望的变量可能会导致应用程序失效。

    值(Value)。对值项进行模糊测试所应采用的合适方法要依赖于Web应用所期望的变量类型。例如,如果合法请求中的名-值对是length=50,那么就可以用超长值或者小数字值来对值项进行模糊化。例如,如果提交的长度小于数据的实际长度,那么就会怎样呢?当值为零或甚至为负数时情况又怎样呢?一个比应用程序设计的能处理的最大值还大的值可能会导致应用程序崩溃或者发生整数溢出。模糊测试可以被用来找到这些问题的答案。如果值项期望的是一个字符串内容,那么就试图发送非期望字符串值以查看应用程序能否很好的进行处理。然后使用逐渐增多的大量数据,以查看是否发生缓冲区溢出或运行崩溃的情况。

    分隔符(Separator)。在不同的项之间作为分隔符的那些字符(/,=,&,.,:等)也应当被作为模糊测试的目标。这些字符被Web服务器或应用程序解析,如果服务器不能处理非预期值,那么不正确的错误处理就可以导致漏洞的发生。

    协议(Protocol)

    数字变量可以被用来模糊化HTTP协议的版本号,以提交支持或不支持的HTTP协议版本,以发现某些服务器漏洞。当前最常用的HTTP协议的版本是HTTP/1.1,HTTP协议被划分为主版本和次版本(HTTP/[major].[minor])。

    头(Headers)

    所有的请求的头都应该被模糊化。

    因此,这里有三个可能被模糊化的变量:名字(name),值(value)和分隔符(:)。名字应当使用已知的合法值来模糊化,以确定Web应用是否支持未公开的头信息。不同HTTP协议所支持的头信息的列表可以在下面的RFC中找到:

    RFC 1945-超文本传输协议-HTTP/1.0

    RFC 2616-超文本传输协议-HTTP/1.1

    值变量可以被模糊化以确定应用程序是否能够正确的处理非预期值。

    堆溢出示例

    2006年1月,iDefense实验室发布了在Novell SUSE Linux企业级服务器9中被远程发现的堆溢出漏洞的细节 。该漏洞可以通过简单的提交一个POST请求被触发,只需在该请求的Content-Length头中包含一个负数即可。可以触发该漏洞的一个请求的例子如下所示:

    Cookies

    Cookies被存储在本地,当一个请求被发送给cookie以前所存在的那个Web服务器时,cookie将作为一个HTTP头被提交。一个cookie的格式如下所示:

    同样,名字(name),值(value)和分隔符(:)应当被模糊化。同其它值一样,模糊值也应当受到在合法请求中提交的变量类型的影响。

    发布数据(PostData)

    正如前面所提到的,名-值对既可以使用GET方法在请求URI内被提交给Web服务器,也可以使用POST方法作为独立的HTTP头提交给Web服务器。数据通过下面的格式被发送:

    缓冲区溢出示例

    iDefense实验室的Greg MacManus在与流行的Linksys WRT54G无线路由器一起使用的Web服务器中发现了一个缓冲区溢出漏洞 。他发现向apply.cgi发送一个内容长度超过10000字节的POST请求将会导致缓冲区溢出。尽管这样的漏洞通常很难在一个嵌入的Web服务器中发现,但是设备固件的开源特性,以及使用开发工具来修改固件的有效性,可以有助于来开发漏洞发掘代码。

    识别输入

    现在我们已经知道了Web应用请求的结构,并且了解了这些请求中可以作为单独输入变量以及潜在的模糊测试目标的各个不同的组成部分。下一步就要执行搜索以识别所有不同的合法输入值。这可以通过手工的方式来完成,也可以用自动化的方法实现。不论采用哪一种方法,我们要尽可能完整的覆盖输入值。当研究应用程序时,我们的目标就是要列出下面所有的输入:

    Web页

    目录

    页所支持的方法

    Web表单

    名-值对

    隐藏字段

    头信息

    Cookies

    识别前面所提到的输入最简单也是效率最低的方法就是使用一个Web浏览器打开一个Web页,然后查看该页的源代码,在源代码中你可以寻找包含在Web表单中的输入。同时要确保搜索隐藏字段(输入类型="hidden"),因为懒散的应用程序开发者有些时候会将隐藏字段作为通过隐匿来实现安全性的一种手段。他们可能会假定你不会对这些字段进行测试,因为它们在页面上是不可见的。这显然是一种非常弱的控制方法,因为隐藏字段总是可以在Web页的源代码中被查看到。

    当使用Web浏览器来识别输入时,你将不能看到请求/响应的头信息。例如,你将不会看到被发送给应用服务器的cookies的结构,因为它们被包含在HTTP头中,并且被自动的传递给浏览器。为了查看原始HTTP请求,你可以使用一个网络协议分析器如Wireshark 。

    即使使用一个Web浏览器和嗅探器,一旦Web应用中的页面超过一定数量,那么手工识别所有可能的输入将是不可行的。幸运的是,可以使用一个Web蜘蛛来自动化的跟踪遍历Web应用程序。Web蜘蛛(或者Web爬虫)是一个应用程序,它可以识别出一个Web页内部的所有超链接,遍历这些链接,再发现额外的超链接并遍历,重复执行此过程直到所用可能的Web页都被访问。同时,它还可以发布对安全研究者而言非常重要的信息,如前面所提到的输入信息。幸运的是,目前有许多功能卓越的Web蜘蛛,它们是免费并且开源的。一个简单而功能强大的Web蜘蛛是wget工具 。尽管它最初是为UNIX操作系统而设计的,但wget的端口同样也适应于win32平台 。我们要推荐的另外一个免费Web蜘蛛包含在WebScarab中 。WebScarab项目是开放Web应用安全项目(Open Web Application Security Project,OWASP)所提供的一组工具的集合,它们对于分析Web应用非常有用。在WebScarab众多的工具中,有一个Web蜘蛛可以被用来自动化的识别Web应用中所有的URL。另外一个有用的WebScarab工具是一个代理服务器,可以用于手工的Web页的审核。如果设定你的Web浏览器以使用WebScarab代理,则在你运行应用程序时,它将记录下所有的原始请求/响应。它甚至包含一个非常基本的Web模糊器,在下一章中,我们将讨论如何创建一个功能更强大的模糊器。

    9.4 漏洞

    Web应用易于遭受许多类型漏洞的攻击,所有这些漏洞都可以通过模糊测试来加以识别。下面列出了标准的漏洞分类:

    拒绝服务(Denial-of-service,DoS):DoS攻击对Web应用而言是一个很重要的威胁。尽管DoS攻击并不使攻击者访问目标程序,但是也拒绝访问你的公司的外部入口站点,并且一个收入源的失去可能会导致实质上的金融损失。

    跨站点编写脚本(Cross-sitescripting,XSS):根据Mitre的统计,XSS漏洞占2006年新发现漏洞的21.5% 。,这使得XSS漏洞成为了最普遍的漏洞,而不仅仅限于Web应用,但是在所有的应用程序中,XSS漏洞曾经被认为是比安全威胁更加严重的问题,但随着钓鱼式攻击(phishing attack)的爆炸性增长,这种情况已经完全被改变了。XSS漏洞允许一个攻击者控制Web浏览器中客户端的行为,因此成为了执行这类攻击的攻击者的有用工具。

    SQL注入(SQL injection):在Web应用的漏洞中,SQL注入不仅是最普遍的一种漏洞之一,而且是最严重的一种漏洞之一。2006年的Mitre统计表明SQL注入two spot,占新发现漏洞的14%。这种情况的造成,一方面是由于在关系型数据库的助推下动态网页的快速增长,另一方面则是仍然在大多数教科书里所讲述的不安全的SQL编码实践。有一些误解认为SQL注入只限于攻击提供有读取数据库记录途径的应用,因此只是一种机密风险。随着被大多数关系型数据库所支持的功能强大的存储过程的出现,SQL注入将成为更大的风险,它可以导致对后端可信系统的全面威胁。

    目录遍历/弱访问控制(Directorytraversal/Weak access control):目录遍历曾经是相当普遍的,但有幸的是现在已基本消除。另一方面,弱访问控制仍然是一个存在的威胁,因为它没有使开发者忘记一个受限制的页或者目录,以致不能正确的应用适当的访问控制。这也是Web应用应当被持续不断进行审核的许多原因之一,并且这种审核不仅应当在产品进入市场之前进行,而且应当贯穿于产品的整个生命周期。一个曾经安全的Web应用可能会由于对服务器所做的错误的配置变更突然变得不安全,这种变更即删除或者弱化访问控制。

    弱认证(Weakauthentication):采用认证模式没有什么缺点,如果没有正确的实现认证,那么一切都可能会不安全。弱的密码保护是实施强有力攻击的一个漏洞,在明文本中传递信任信息使得它们在无线网络中易于被捕获。尽管这些错误通过基本的努力很容易被发现,但它可以带来更大的威胁,即当理解了应用程序的业务逻辑以确保开发者没有错误的在应用程序中放置一个可以绕过认证控制的入口的时候。

    弱任务管理(Weaksession management):由于HTTP协议是一个无状态协议,因此状态管理的一些形式应有必要能够区分并发用户,并且对每个及所有的页不需要输入认证信任。无论唯一任务标记是通过cookies进行传递的,还是在URI内部或者页数据中传递的,标记本身都不再重要,重要的是它们是如何被结构化的和被保护的。Cookies应当是足够随机的,以使强制性漏洞攻击变得不可行。另外,它们必须要经常过期以提防重复攻击。

    缓冲区溢出(Bufferoverflow):缓冲区溢出攻击在Web应用中的普遍性比不上它在桌面和服务器应用中的普遍性。这个原因主要归结于Web应用开发所通常使用的语言如C#或者Java,这些语言所具有的内存管理控制功能减少了缓冲区溢出的风险。但这并不是说,在对Web应用进行模糊测试时就可以忽略缓冲区溢出。很可能一个应用程序将用户提供的输入传递给用C或C++编写的一个独立的应用,而该应用易于受到缓冲区溢出的影响。除此之外要记住,当进行模糊测试时你至少有两个目标:Web应用程序和Web服务器。在另一方面,Web服务器通常用易发生缓冲区溢出问题的语言编写。

    不正确支持的HTTP方法(Improperlysupported HTTP method):通常情况下,Web应用处理GET和POST请求。然而,正如前面所讨论的,还有许多其它的和RFC兼容的以及第三方的方法。如果这些方法没有被正确的实现,那么它们就可以允许攻击者操纵服务器上的数据,或者通过后续攻击获取有价值的信息。因此,模糊测试应当被用来识别所有支持的方法,以确定它们是否被正确的实现。

    远程命令执行(Remotecommand exection):Web服务器可能简单的将用户提供的输入传递给其它的应用或者操作系统本身。如果这些输入没有经过适当的验证,那么就会使攻击者直接执行目标系统上的命令。PHP和Perl应用通常特别易于受到这样的攻击。

    远程代码注入(Remotecode injection):同样,这个漏洞也易于攻击PHP应用。不良的编码习惯,包括允许未验证的用户输入被传递给如include()或require()的一个方法,这将允许本地或者远程的PHP代码被包含进来。当用户输入被盲目地传递给这些方法时,它就允许攻击者将他们自己的PHP代码注入到目标应用程序中。据Mitre的2006年统计,此类漏洞占新发现漏洞的9.5%。

    漏洞库(Vulnerablelibraries):开发者通常会错误的信任包含在应用程序中的第三方库。一个Web应用中所包含的任何代码,无论是自己编写的,还是从第三方经过预编译获取的,都可能是存在潜在漏洞的代码,都应当在安全性测试中进行相同程度的审查。至少应当检查相关文档以确保所包含的库没有已知的漏洞。除此之外,应当对第三方库执行与自编库相同水平的模糊测试以及其它的安全性测试

    HTTP响应分解(HTTP response splitting):HTTP响应分解第一次被广泛的认知,是随着Sanctum Inc.的白皮书"分解和征服"的发布 。当一个用户可以向响应的头信息中注入一个CRLF序列时,该攻击就变得可行。接着,它会使攻击者能够篡改Web服务器所提供的响应,并导致一系列不同的攻击,包括使Web代理和浏览器cache遭到破坏。

    跨站点请求伪造(CrossSite Requset Forgery,CSRF):CSRF攻击很难防护,并且是一种不断增长的威胁。当一个被攻击者有一个活动任务时,如果一个攻击者能够使被攻击者去执行某些动作,如点击一个可能存在于e-mail消息中的链接,那么一个CSRF漏洞就会存在。例如,在一个银行的网站上,一个资金转帐请求可能会通过提交一个Web表单来完成,而该表单中包含有账户信息和转帐金额。只有该账户的所有者才能实现转帐,并且只有该账户的所有者可以使用他或她的信用信息进行注册。然而,如果此人已经注册,并且不知不觉的点击了一个链接使得相同的Web表单数据被发送给银行的站点,那么在被攻击者无察觉的情况下就会发生相同的转帐操作。为了防护CSRF攻击,Web站点通常要求用户在执行一项敏感操作如资金转帐之前,进行重复确认或执行某些手工操作。Web站点也开始在Web表单中实现当前(一次)值,以验证表单的来源。

    尽管这里并没有完整的列出所有可能的Web应用漏洞,但它说明了Web应用程序易于受到许多漏洞的攻击。同时它也说明了尽管影响本地应用的某些漏洞同样也会影响Web应用,但有许多攻击是专门针对Web应用的。为了通过模糊测试来发现Web应用的漏洞,必须要通过HTTP协议来提交输入,并且必须要使用与对本地应用进行模糊测试不同的机制来发现错误。要了解完整的Web应用漏洞的情况,我们推荐参阅Web应用安全协会的威胁分类项目。

    9.5 异常检测

    当对Web应用进行模糊测试时,异常检测是该类模糊测试中非常具有挑战性的一项工作。如果我们让一个模糊器连夜运行,并且发现应用程序因为它所接收到的多于10000个请求中的一个而崩溃,那么这样的结果对我们没有什么意义。我们可能会知道存在着一个漏洞条件,但是我们无法将它重现。因此,下面列出的数据是很重要的,可以被用来识别潜在的漏洞条件。

    HTTP状态码。当一个Web服务器响应一个请求时,它包含一个三位数字码以确定请求的状态。状态码的完整列表可以在RFC2616-超文本传输协议-HTTP/1.1 的第十部分找到。这些状态码可以提供线索以确定哪些模糊请求需要进一步加以研究。例如,Internet服务器错误的状态码500可能会提示前面的模糊请求导致了服务器的错误。同样,未授权错误的状态码401则提示一个被请求的页是存在的,但是是受密码保护的。

    Web服务器错误消息。Web应用可能被设计成直接在Web页的内容中显示一个错误消息。使用正则表达式来解析包含在响应中的HTML可以揭露这些消息。

    中断连接。如果其中一个模糊输入导致Web服务器冻结或崩溃,那么后续的请求将不能被成功的连接到服务器。因此,模糊测试工具应当维护日志文件以记录毁坏通道和失效连接何时发生。当检查日志文件并识别出一个故障连接的字符串,那么就立即查看日志条目之前的请求以确定故障。

    日志文件。大多数Web服务器都可以被配置以记录不同类型的错误。同样,这些日志可以提供线索以发现哪些模糊请求导致问题的发生,而该问题可导致一个漏洞条件的产生。日志文件所面临的挑战是它们不能被直接的绑定到导致错误的请求中。将请求和错误日志相连接的一个可选方法是将攻击计算机和目标计算机的时钟进行同步,并查看请求和日志条目的时间戳,而这将会限制可能的错误数量。它不能准确的、确定的将一个模糊请求和它所导致的错误绑定在一起。

    事件日志。事件日志类似于Web服务器日志,因为它们也不能直接绑定到请求,但是可以通过时间戳被关联到一个特定的范围。事件日志被微软的Windows操作系统所使用,并且可以使用事件查看应用程序来访问。

    调试器。鉴别已处理和未处理异常的最好方法,是在模糊测试之前将目标应用连接到一个调试器。错误处理机制将阻止由模糊测试所导致的许多错误的明显标记,但是这些错误通常可以通过使用一个调试器来发现。寻找已处理的异常同未处理的异常是同等重要的,因为某些特定类型的错误如空指针解除引用以及格式化字符串虽然会导致已处理的异常,但通过给定适合的输入值,它们也可以发现漏洞。这里所面临的最大挑战就是确定哪个请求导致了异常的产生。调试器在对Web应用进行模糊测试方面具有一定的局限性,因为虽然它们可以发现服务器异常,但却不能发现应用层的异常。

    9.6 小结

    在本章中,我们定义了Web应用和Web服务器模糊测试,并且学习了如何使用它来发现Web应用中的漏洞。这要求全面的了解Web浏览器是如何提交请求的,以及Web服务器是如何对请求作出响应的。通过这些知识,我们就可以确定所有的能够被终端用户控制和操纵的那些巧妙的输入。可以通过这些输入来进行模糊测试,并通过监视潜在的异常以发现漏洞。在下一章中,我们将创建一个Web应用模糊器以实现测试过程的自动化。

    第10章 Web应用程序和服务器的模糊测试:自动化

    "对我们来说最重要的事情是发现'Osama bin Laden(奥萨姆o本o拉登)',这是我们的头等大事,在找到他之前我们不会停歇。"

    --George W. Bush, Washington DC,2001年9月13日

    "我不知道'bin Laden'在哪里。我没有任何想法并且实际上也不关心。它并不是那么重要。它不不是我们的头等大事。"

    --George W. Bush, Washington DC,2002年3月13日

    前面我们已经讨论了如何对Web应用程序进行模糊测试,现在可以讨论有关模糊测试的理论了。在本章中,我们将利用前面章节所学的知识,通过开发一个图形化的Web应用模糊器WebFuzz来应用这些知识。我们首先开始介绍该应用程序的设计,并确定我们所面对的独特的挑战。然后,我们就可以选择一种合适的开发平台来开始构建模糊器。一旦开发完成,我们的工作并没有结束。当构建一个漏洞发掘工具时,你永远不能认为工作已经完成直到你已经使用它发现了漏洞。你不会生产一辆汽车而不对它进行驾驶测试。因此,我们将遍历一些不同类型的已知的Web应用漏洞,以确定WebFuzz是否能够发现这些漏洞。

    10.1 Web应用模糊器

    Web应用模糊测试并不是一个新的概念。目前存在有许多不同的模糊器并且数量还在增加。下面列出了一些主流的、免费的、以及商用的Web应用模糊器。

    SPIKE代理 。SPIKE代理是由DaveAitel用Python语言开发的、基于浏览器的Web模糊器。它执行一个代理动作,捕获Web浏览器请求,然后允许你为一个目标Web站点运行一系列预定义的审核,以识别不同的漏洞如SQL注入、缓冲区溢出和XSS。由于建立在一个开源的架构之上,SPIKE代理可以被扩展以对不同的目标应用进行模糊测试。SPIKE代理并不是一个专门的模糊器;实际上它是漏洞扫描器和模糊器的组合体。

    WebScarab 。开放Web应用安全项目(Open Web Application Security Project,OWASP)为测试Web应用的安全性提供了许多不同可用的工具,这其中就包括WebScarab。尽管它是一个全面的Web应用安全测试工具,但它也包含一个基本的模糊器,可以向应用程序的参数注入模糊值。

    SPI模糊器 。SPI模糊器是SPI工具集的一个构件,而SPI工具集是WebInspect应用的一部分。WebInspect是SPIDynamics公司开发的一个商用工具,被设计用来为Web应用程序的测试提供一套完整的工具。

    Codenomicon HTTP测试工具 。Codenomicon为包括HTTP在内的各种协议产生模糊测试集。

    beSTORM 。类似于Codenomicon,beSTORM也是一个可以处理包括HTTP在内的不同Internet协议的模糊器。

    WebFuzz的开发受到了商用工具SPI模糊器的启发。SPI模糊器是一个简单但设计很精巧的图形化Web应用模糊器,它向用户提供了对模糊测试所使用的原始HTTP请求的完整的控制。它要求用户具备基本的HTTP协议的知识以开发测试并生成结果。同样,也需要使用这些知识来解释响应以识别那些需要进一步研究的问题。

    SPI模糊器的主要缺点是它只能作为一个昂贵的商用应用程序中的一个构件来使用。我们基于SPI模糊器的基本知识,来创建一个功能受到一些限制但是开源的工具来满足WebFuzz的特定需求。和大多数模糊器一样,WebFuzz不是一个易于使用以及完全用于安全性的一个工具。它只是一个将以前手工完成的工作进行自动化处理的工具。你作为终端用户可以利用该工具来开发有效的测试并分析测试结果。该工具应当被视为一个研究的起点,而不是最终的解决方案。

    象本书中所开发的所有工具一样,WebFuzz是一个开源的应用程序。因此,它提供了一个可以而且应当被进一步创建的架构。我们鼓励你对其添加新的功能并修复错误,但最重要的是,我们希望你能和其他人来共享你所做的改进工作。本章的剩余部分详细描述了WebFuzz的开发过程,并通过研究不同的实例来说明其所具有的功能以及受到的限制。可以从本书的Web站点www.fuzzing.org处下载WebFuzz。

    10.2 WebFuzz的特性

    在开始创建我们自己的模糊器之前,首先回顾一下上一章所学到的HTTP协议工作机理的有关知识,并利用这些知识来确定我们的模糊器所需要的特性。

    10.2.1 请求

    让我们从头开始讨论。你无法对一个Web应用进行模糊测试,除非你有一种方式来向它发送请求。在通常情况下,我们使用一个Web浏览器同Web服务器进行通信。毕竟它知道如何利用HTTP进行通信,并且能够将所有杂乱的细节信息组装成一个HTTP请求。然而,当进行模糊测试时,我们需要这些细节信息。我们希望能发挥主观能动性,来改变请求的所有方面,因此,我们选择了将原始请求暴露给终端用户,并允许对其中的任何部分进行模糊化。

    该请求包括以下字段:

    主机。目标机器的名字和IP地址是所需要的一个字段。我们不能对一个Web应用进行模糊测试,而不让WebFuzz知道向何处发送请求。这不是一个可以进行模糊测试的字段。

    端口。尽管Web应用在默认情况下运行在TCP端口80,然而它们也可以方便的运行在其它任意的TCP端口。实际上,通常将Web应用设计为提供一个基于Web的管理控制台,以使其运行在一个可选的端口而不干扰主要的Web服务器。同主机名一样,端口字段也是告诉WebFuzz向何处发送请求,同时,它也不是一个可以进行模糊测试的字段。

    超时。由于我们的目的是发送非标准的Web请求,因此,目标应用程序通常不会以一种及时的方式来作出响应。因此,我们包含了一个用户定义的、以毫秒计的超时值。当记录一个请求的响应超时时,这种记录是非常重要的,因为它可以指明我们的请求使得目标程序离线,从而导致一个潜在的DoS漏洞。

    请求头。这是操作执行的起点。当使用一个Web浏览器时,终端用户可以控制目标主机、端口以及请求URI,但却不能控制所有不同的头信息。我们有意将请求的所有组成部分保持在一个单独的可写的文本字段中,因为我们希望终端用户能够控制请求的各个方面。可以通过简单的将所需要的请求敲进请求头字段中来手工的创建一个请求。另外,如果你倾向于使用点击的方法,那么也可以使用图10.3中上下文菜单中所提供的标准头列表来拼凑一个头。最后一个选项是如果默认Web页需要一个基本请求,那么允许从上下文菜单中选择默认头。

    10.2.2 模糊变量

    模糊变量是指在请求中将要被模糊数据所替代的那些区域。正如所讨论的,用户对发送给Web服务器的原始请求有着完整的控制权。因此,模糊变量被直接添加到原始请求中,并通过在方括号中的变量名(例如[Overflow])来识别。当设计创建模糊变量的函数时,我们可以将它们划分为两个基本类型:静态列表或生成变量。静态列表是指从一个预定义的数据列表中取出的模糊变量。静态列表的一个示例是用来识别XSS漏洞的模糊数据。可能导致XSS漏洞的不同输入的一个预定义列表(例如<script>alert('XSS')</script>)被编译,并且每次将数据注入到请求的一行中。静态列表通常被作为外部ASCII文本文件来维护,因此用户不需要再编译应用程序就可以修改变量。另一方面,生成模糊变量是根据预定义的算法而创建的,而该算法可能允许接收用户的输入。溢出变量是生成模糊变量的一个例子。一个溢出允许用户定义溢出所使用的文本,并定义文本的长度和其重复次数。

    为了更加有效,我们可能希望在一个单一的请求中定义多个模糊变量。尽管你可能经常想要在同一时间动态的改变两个或多个变量(例如变量的长度值以及它所描述的内容),出于简化的原因,我们选择了一次只处理一个变量,但在一个单一请求中包含多个模糊变量是可能的。WebFuzz将处理所遇到的第一个模糊变量,而忽略其它的变量。一旦第一个变量被完全模糊化,那么它将从原始请求中被删除,因此WebFuzz就可以转向处理后续变量。下面的WebFuzz请求说明了一个请求,该请求被设计为通过一次攻击而识别多个公共漏洞。

    10.2.3 响应

    WebFuzz捕获所有的响应结果并将其以原始格式保存。通过捕获完整的原始响应,我们就可以用不同的格式来灵活的显示响应。特别的,我们能够让用户来显示原始结果或者在Web浏览器的控制下查看HTML。一个原始响应,在Web浏览器中的相同数据。这是非常重要的,因为现存漏洞的线索具有不同的形式。例如,头信息可能包含一个状态码(例如500-内部错误)以提示发生了一个DoS。同样,Web页本身可能也会向用户显示错误消息以提示可能发生了SQL注入。在这种情况下,当一个浏览器解释HTML时,错误将易于被查看。

    10.3 必要的背景知识

    HTTP在两个方面提出了独特的挑战,一是确定如何对通信进行最佳的监视,更重要的是确定当异常发生时如何来识别这些异常。接下来,我们将研究Web浏览器内部所执行的一些操作情况。

    10.3.1 识别请求

    WebFuzz要求用户创建一个原始的HTTP请求,但是对于一个给定的Web应用,如何来确定合适的请求呢?哪些Web页是存在的?这些Web页可以接收哪些变量?这些变量应当如何被传递?在上一章中,我们讨论了如何手工的识别输入,以及如何通过使用嗅探器、Web蜘蛛和代理来识别输入。在继续讨论之前,需要介绍一个Web浏览器插件,该插件在使用WebFuzz时以及需要为单一Web页确定原始输入时都能够应用。LiveHTTPHeaders项目 为确定基于Mozilla的Web浏览器中的原始HTTP请求提供了一个方便的工具。LiveHTTPHeaders被用来显示Firefoxsidebar中的所有请求及其相应的响应。使用这种方法的一个突出优点是,一个请求可以被捕获并直接剪切并粘贴到WebFuzz以简化模糊请求的创建。还有一些其它不同的浏览器插件,例如作为Firefox的扩展的Tamper Data 和Firebug ,以及针对Internet Explorer的浏览器插件Fiddler ,但LiveHTTPHeaders因其简便性而成为首选。

    10.3.2 漏洞检测

    正如前面所讨论的,从目标应用程序所返回的响应可以提供不同的线索以指示最初模糊请求所带来的影响。WebFuzz被设计为向用户提供检测数据,但是需要用户来对响应进行解析。尽管手工评审来自于Web服务器的所有响应是不切实际的,但希望响应的特定部分能够提示出一个异常条件的发生。然后用户就可以识别相关的请求。这是可能的,因为所有的响应,不论是一个原始响应还是HTML格式的响应,都可以使用相关的请求通过从响应窗口选择合适的选项来查看。

    当运行WebFuzz时,下面所列出的信息可能会提示一个漏洞条件的存在:

    HTML状态码

    嵌入在响应中的错误消息

    嵌入在响应中的用户输入

    性能下降

    请求超时

    WebFuzz错误消息

    已处理和未处理的异常

    下面分别来研究一下每个信息,以便更好的理解它们为什么能够有助于识别漏洞。

    HTML状态码

    我们已经提到HTML状态码是一个非常重要的信息,因为它们对初始请求的成功或失败提供了一种快捷的可视化的指示。因此,WebFuzz解析原始响应以识别状态码,然后该状态码被单独显示在一个表中以详细描述所有的响应。使用此信息,用户可以迅速的识别出应当被进一步详细研究的那些响应。

    嵌入在响应中的错误消息

    从设计来讲,Web服务器在动态生成的Web页中可以包含错误消息。当一个Web服务器被不正确的部署到一个可以进行调试的环境时,情况尤其如此。多次被提到的一个典型的错误消息是一个类似于显示"密码错误"验证错误,而不是"用户名或密码错误"。当对一个Web应用的注册界面进行强制性漏洞发掘时,第一个消息将使你知道用户名存在但密码是错误的。这将未知变量的数量从两个(用户名和密码)减少为一个(密码),并且极大的增加了获取访问的几率。当识别Web注入攻击时,应用程序错误消息也可以非常有用。

    嵌入在响应中的用户输入

    当动态生成的Web页包含用户提供的数据时,那么就可能后存在XSS漏洞。Web应用的设计者需要过滤用户输入以确保不会发生这样的攻击,但是不正确的过滤也是一个很普遍的问题。因此,识别WebFuzz所提供的HTML响应中的数据就表示应用程序应当被测试以检测XSS漏洞。

    性能下降

    尽管一个最高级别的应用程序崩溃使得一个DoS攻击易于被发现,但是这类漏洞通常会更加精巧。在通常情况下,性能下降将会意味着应用程序易于受到DoS攻击。一个请求超时是识别性能下降的一种方法,但在进行模糊测试时,性能监视器也应当被使用以识别诸如CPU过载或内存使用等问题。

    请求超时

    正如所提到的,请求超时不应当被忽略,因为它们可能会指示一个暂时或者永久的DoS条件。

    WebFuzz错误消息

    WebFuzz有它自己的错误处理方法,当在特定函数执行失败时将弹出错误消息。例如,如果目标服务器由于之前的一个模糊请求而离线,则WebFuzz可能会发出一个错误消息以表明它不能连接到目标服务器。这也意味着发生了一个DoS攻击。

    已处理和未处理的异常

    当对Web应用进行模糊测试时,既可以发现应用程序本身的漏洞,也可以发现应用程序所运行的Web服务器的漏洞。因此,监视服务器的状态也是非常重要的。尽管Web服务器所返回的响应提供了发现潜在漏洞的机会,但是它们并没有给出完整的解决方法。模糊请求很可能会导致已处理和未处理的异常,而当输入被微小改变之后,这些异常将可能导致漏洞条件。因此,这里推荐在模糊测试过程中为目标服务器设置一个单独的调试器,以识别这些异常。本书所讨论的其它工具如FileFuzz和COMRaider,都包含有内嵌的调试功能。然而,对于Web应用的模糊测试而言,这并不是需要的。一个WebFuzz不需要重复部署并破坏一个应用程序。这次,我们向一个单独的Web应用发送一系列模糊请求,该应用连续运行并响应所有的请求,除非遇到导致DoS条件的输入。

    10.4 WebFuzz的开发

    到目前为止我们已经具备了足够的理论知识,下面可以进行具体的模糊器的开发了。我们将详细介绍WebFuzz的开发过程。

    10.4.1 开发方法

    在进行WebFuzz的设计时,我们的目标是创建一个对Web应用进行模糊测试的用户友好的工具。由于假定我们的目标用户具有一定的HTTP知识,因此没有必要创建一个只是简单进行点击操作的工具。相反,我们期望设计一种能为终端用户提供对请求结构具有最大操纵灵活性的工具。我们也希望以一种方式来传送响应数据,而该方式可以简化潜在漏洞的发现。

    10.4.2 开发语言的选择

    为了使WebFuzz具有更好的用户友好性,那么最好是创建一个GUI应用程序。基于两个主要的原因选择了C#作为开发语言。首先,C#允许我们非常方便的设计一个看起来相当专业的GUI应用程序。其次,C#提供了一些类以帮助实现发送/接收等网络通信功能。使用C#的一个缺点是使我们主要依赖于Windows平台。然而,当对Web应用进行模糊测试时,我们没有必要对运行在与模糊器相同机器上的目标应用进行模糊测试。因此,设计一个基于Windows的工具并没有限制我们只能对基于Windows的目标应用进行模糊测试。

    10.4.3 设计

    如果将所有的代码都拿出来进行讲述,那将会使读者疲倦不堪。然而,WebFuzz模糊器的主要功能被包含在一些基本的类中,我们将对每个类中的关键功能做重点说明。请记住,本书开发的所有应用程序的完整源代码在本书的网站上都可以得到。

    TcpClient类

    C#提供了一个WebClient类,该类中包含了处理HTTP请求和响应的函数。它将生成和处理必要的网络通信所需要的大部分代码进行了封装,能够极大的简化应用程序的开发。它甚至包含函数以处理WebFuzz所需要的许多功能,如访问HTTP响应的头。在稍微低一些的层次上,C#提供了HttpWebRequest和HttpWebResponse类。这些类需要进行稍多的编码工作,但同时也可使用更高级的功能如使用代理的能力。WebFuzz应当使用这些类中的哪些类呢?答案是都不使用。取而代之,我们选择使用为任意类型的TCP通信(不只是HTTP)而设计的TcpClient类。这样,它就缺少了其它Web类所封装的一些功能。为什么要这样做呢?是我们乐于编写没用的代码吗?不是,只是必须要这样做。

    当编写模糊器时,一个最大的挑战就是你试图以一种超常规的方法来编写代码。因此,那些标准的类和函数可能就不适合你的需要。我们的目的是要完整的控制原始HTTP请求,但不幸的是各种各样的Web类并没有为我们提供这样的控制粒度。

    这个简单的代码示例是要求使用WebClient类发送一个定制的Web请求。我们创建了一个基本的GET请求,并且只加入了一个定制头(blah:blah)。然而,当嗅探所生成的实际通信时,我们发现的请求被发送了。


    你可能会注意到在实际的请求中,两个额外的头也被加入了,即主机(Host)和连接(Connection)。因为这个原因,使得我们不能使用通常的类。我们必须要牺牲一定的使用便利性,以换取对该过程进行低层次完整的控制。在我们的例子中,为WebFuzz的网络通信部分使用了TcpClient类。

    异步Sockets

    网络通信可以使用同步或者异步sockets来实现。尽管使用异步sockets需要多做一些额外工作,但在WebFuzz中使用它们是经过深思熟虑后作出的决定,因为这种方式更好的处理了在使用模糊器时可能会出现的预期的网络问题。

    同步sockets采用阻止化的工作方式。这意味着当遇到一个请求或响应时,主线程将停止执行,并且在继续执行之前要等待该通信执行完毕。使用一个模糊器,我们将尽力的试图去导致异常条件的发生,某些异常可能会导致性能下降或者使目标应用完全离线。我们不希望WebFuzz在等待可能永远也不会发生的通信时成为无响应的程序。异步sockets可以使我们避免此问题,因为它们采用的是非阻止化的工作方式。异步sockets发出一个单独的线程来处理此类通信,并且激活一个回调函数以指示通信何时完成。这就允许其它事件继续执行。

    下面来分析一下WebFuzz中的网络通信代码,以便更好的理解异步sockets的概念:

    在创建了一个典型的TCPClient和NetworkStream之后,我们激活了stream中的BeginWrite()方法。BeginWrite()方法使用下面5个参数 :

    byte[] array。包含写入到网络流的数据的一个缓冲区。

    int offset。缓冲区中开始发送数据的位置。

    int numBytes。写入的最大字节数。

    AsyncCallback userCallback。通信完成时将被激活的回调函数。

    object stateObject。区分此异步写请求与其它请求的一个对象。

    AsyncWaitHandle.WaitOne()将导致侦听线程被阻止,直到请求被成功的发送。

    当我们编写完对网络流的请求之后,就可以接收到从服务器返回的结果。

    在这时,我们再次使用一个异步sockets,但这次它被用来接收从目标应用所返回的响应。我们现在激活BeginRead()方法,该方法含有同BeginWrite()方法相同的参数,但这次,我们使用OnReadComplete()作为回调函数:

    在OnReadComplete()的开头创建了一个计时器(readTimeout),当到达用户定义的超时时,该计时器将调用ReadDone.Set()。这就允许我们确保如果读取操作失败则该线程可能处于不活动状态,并且为终端用户提供一种方法以控制超时的长度。然后将所接收到的响应追加到缓冲区。在这时,我们需要决定是否应当继续等待以后的数据。这项工作可以通过确定是否有字节被接收来完成。如果有字节被接收,我们通过再次激活BeginRead()来继续等待以后的数据。如果没有字节被接收,则销毁该线程并继续执行。

    生成请求

    在我们发送一个请求之前,必须要首先确定发送的内容。此内容显然是从用户创建请求的请求头窗口获得的,但是每个模糊变量[xxx]必须被实际的模糊数据所替代。一旦用户在btnRequest_Click()方法中点击请求按钮,那么该过程就开始执行。

    当我们生成请求以后,就开始执行一个循环,该循环连续的解析用户提供的数据直到在请求中遇到模糊变量。然后转到一个case语句,以确定对每个模糊变量执行何种操作。

    来自于静态列表(SQL,XSS和方法)的那些模糊变量,创建了一个新的Read()类,并且将包含模糊变量的ASCII文本文件的名字传递给构造器。另一方面,生成的变量(溢出,遍历和格式化)实例化一个新的Generate()类,并传递将要被重复的字符串、该字符串的总长度以及该字符串将要被传递的次数。

    接收响应

    当响应被接收时,WebFuzz通过将请求、原始响应、HTML响应、主机名和路径添加到单独的字符串数组来将它们进行保存。另外,识别的信息包括状态码、主机名以及被添加到一个ListView控制的请求。通过这种方法,当模糊测试执行完毕后,你可以简单的点击ListView控件中合适的响应,则详细信息将被显示在一系列的被标签化的超文本框以及Web浏览器控件中。

    正如所提到的,WebFuzz并不是一个只能执行简单点击操作的漏洞扫描器。它被设计为允许高级用户来对一个HTTP请求的目标部分进行模糊化。该工具的完整源代码和二进制代码可在网站www.fuzzing.org找到。

    10.5 实例研究

    现在我们已经基本了解了WebFuzz的创建原因和构造方法,下面将讨论一些更加重要的内容,来看看它在实际应用中是否能够有效的执行。

    10.5.1 目录遍历

    当一个用户可以进入Web根目录,并且可以访问并未想通过Web应用传递的文件和文件夹时,就会存在目录遍历。这种类型的漏洞造成了一种泄密风险,但也可能升级而导致威胁整个系统的安全,而这取决于可以被访问的文件。例如,考虑这种情况,即目录遍历将允许一个攻击者获取密码文件。即使该文件是加密的,那么当它被检索之后仍有机会获取其中的密码,因此攻击者稍后会再次登录,使用有效的认证信任连接到服务器。

    遍历通常包括发送一系列的../字符以遍历更高层的目录。遍历字符通常是被编码的URL以绕过基本的检测过滤器。如果目录是可浏览的那么这就是所有需要做的工作,但是通常还需要追加一个存在文件的名字。当测试一个目录遍历攻击时,建议将服务器上默认存在的文件的名字进行追加。例如,在一个Windows系统中,boot.ini和win.ini就是很好的追加选择,因为它们是ASCII文件,同时当它们在所有现代的Windows操作系统在存在时,非常容易被识别。

    让我们来看在TrendMicro控制管理器 中的一个简单的目录遍历攻击的例子,该攻击是由rptserver.asp页中的IMAGE参数的不正确的输入验证所引起的。我们将使用WebFuzz向rptserver.asp页发送一个GET请求,但是我们将用模糊变量[Traversal]后面跟着一个已知的文件(在这里是win.ini)来替换正确的IMAGE参数。我们可以从中看出目录遍历实际上是在一个单一的遍历之后被暴露的。


    让我们来看另外一个例子,但这次我们将变换一种方式。Ipswitch Imail Web日历中的一个漏洞 提示我们为了发现一个目录遍历,有时必须要请求某些不存在的内容。在这种情况下,我们发现当遍历被传递到一个不存在的JSP页时,目录遍历漏洞是存在的。我们再次使用WebFuzz来进行测试。

    为了测试该特殊的漏洞,我们将向服务器传递一个GET方法,其后面跟着遍历目录并以公共boot.ini文件作为结尾,以请求不存在的blah.asp Web页。

    10.5.2 溢出

    尽管缓冲区溢出在Web应用中相对较少出现,但就像在控制台和GUI应用中一样,缓冲区溢出同样可以存在于Web应用和服务器中,并且存在的原因也是相同的:用户提供的输入没有被恰当的过滤,以致允许一个非预期长度的数据在一个固定大小的缓冲区中溢出。溢出具有很大的危险性,因为它可能导致任意的代码执行。然而,当进行模糊测试时,最可能指示发生一个溢出的情况是应用程序崩溃,并且跟着一个包含超长字符串的请求。通过在模糊测试的执行过程中将一个调试器连接到目标应用,就有可能确定溢出是否只是一个DoS攻击,还是可以被扩展以允许远程代码执行。

    为了说明WebFuzz在检测溢出漏洞中的应用,我们将利用一个简单的例子。考虑PMSoftware的Simple Web Server中的一个溢出 。在这个例子中,我们所需要做的所有工作就是向服务器发送一个长的GET请求。

    最初,除了产生一个404-页没有找到的错误之外,没有发生其它任何事情。但是在第10号请求之后,WebFuzz将不再接收任何响应,为什么呢?

    当我们查看了SimpleWeb Server所运行的服务器之后,很快就得到了问题的答案,因为弹出消息被显示。

    这不是一个好的迹象(至少对SimpleWeb Server而言不是)。当该错误消息框被关闭后,应用程序也就被关闭了。至少我们是受到了一个DoS攻击。那么是否还有机会使代码执行呢?从中可以得到结果,相关联的调试器显示EIP已经被控制,因此代码可能还会执行。如果你是第一次学习缓冲区溢出,那么不要太兴奋。这是最好的一种情形,实际中所遇到的情况并总是这样乐观。

    10.5.3 SQL注入

    当用户提供的数据可以影响到达后端关系型数据库的SQL请求时,SQL注入攻击就会发生。再强调一次,不正确过滤的用户数据是该漏洞的罪魁祸首。当进行模糊测试时,应用程序的错误消息提供了强有力的线索,以指明可能发生了SQL注入。

    Ipswitch Whatsup Professional(SPI) 的注册界面中的用户名字段中的一个SQL注入攻击,允许攻击者通过改变管理者密码而绕过安全性检查。但是我们是如何发现这一情况的?使用LiveHTTPHeaders,我们就可以很容易的看到用户名和密码参数通过一个POST请求被传递到login.asp页。

    现在我们知道了该请求的正确格式,可以加上一个模糊请求。

    注册失败的标准错误消息如下:当试图进行注册生发生了一个错误:非法的用户名。然而,当我们运行WebFuzz时,发现在特定的响应中,其它格式的错误消息中所显示的错误消息被显示。你可以从错误消息的内容中清楚的看出用户提供的数据准备被提交给数据库。

    然而,这并不是一个直接的SQL注入攻击。这里并没有使用普通的认证躲避技术如'or 1=1  ,因此我们必须要实际设计一个合理的UPDATE查询来改变管理者的密码。在这时,我们需要知道数据库模式的细节信息以构造特定于目标应用的被裁剪的SQL注入攻击,但是从哪里可以得到这些信息呢?通过快速的Google搜索可以找到答案。首先,Ipswitch本身就提供了可以下载的模式 。

    这很好,但在创建模糊器的百忙之中,仍然需要做出一些努力来提出一个有意义的查询。不用担心:Ipswitch已经为我们提供了所需要的查询。Ipswitch知识库 实际上为我们提供的命令,可以将管理者用户的密码重设为默认值。

    Ipswitch所提供的这个查询被用于一个命令行工具,以帮助那些因为偶然忘记密码而不能进入应用程序的管理者。但是他们并没有认识到,该查询同样能够很好的用于SQL注入攻击。

    问题/难题:我忘记了Web界面中默认管理者用户的密码。如何才能将它重设呢?

    答案/解决方法:找到一个SQL注入漏洞,然后使用下面的命令…。

    10.5.4 XSS脚本

    XSS是无处不在的。按照Mitre在2006所做的统计,XSS占所有新漏洞的21.5% ,你可以很容易的发现这些漏洞。实际上,sla.ckers.org在它们的论坛上维护着一个不断增长的XSS漏洞列表,并且让人感到失望的是 ,许多大公司也出现在列表当中。

    同大多数Web应用漏洞一样,XSS也是由于不正确的输入验证所造成的。存在漏洞的应用程序接收用户输入,并且不经过任何过滤就将它们嵌入到动态页中。因此,一个攻击者就可以将客户端脚本如JavaScript注入到被请求的页中。然后,这将会允许攻击者控制所显示Web页的内容,或者代表被攻击者执行某些动作。

    为了分析一个XSS模糊测试的例子,我们将从一个已知的存在漏洞的Web页开始。SPI Dynamics在http://zero.webappsecurity.com上部署了一个存在漏洞的Web应用,为了测试WebInspect,默认的注册页包含两个Web表单。一个是发送给login1.asp页的注册表单,另外一个是向rootlogin.asp页发送数据的表单。第二个表单包含一个XSS漏洞。在该表单的输入字段中,标签显示为Last Name的txtName字段的内容,将在被请求的rootlogin.asp页中被返回。由于用户输入没有经过任何验证而被显示,因此应用程序将受到XSS的攻击。

    测试存在XSS漏洞的一个普遍方法是输入一个简单的JavaScript代码片段和一个报警函数,该报警函数将导致产生一个弹出窗口。这是一个快速而简单的测试,它将生成一个简单的可视队列使得客户端JavaScript脚本可以被注入到目标页中。

    在网页上显示了一个弹出窗口。然而,当开始进行模糊测试后,这并不是一个实际可用的漏洞检测机制,因为它要求我们要坐在那并观察结果。而模糊测试最大的优越性就是它可以自动化的运行,因此我们可以离开机器,而当我们返回时就可以得到测试结果。

    幸运的是,由于我们可以将代码注入到页中,因此有多种选择。那么注入JavaScript,并且当执行成功时将通过电话通知我们又如何呢?这是可以做到的,但是我们可以采取一种更加简单的方法。客户端脚本不一定必须是JavaScript,它可以是浏览器能够解释的任意脚本语言。HTML如何呢?它也是一种客户端脚本语言,并且它比JavaScript更不易于通过黑名单进行过滤,因此在某种程度上它提供了一种更强大的XSS测试。一个HTML IMG标签为我们提供了非常简单的电话通知功能。我们所需要做的所有工作就是使用模糊器注入一个HTML IMG标签,从一个本地web服务器请求一个虚假页。当模糊测试完成后检查服务器日志,如果发现了该请求,那么就表明该目标应用存在XSS漏洞。我们需要立即将一个适当的模糊变量添加到WebFuzz中。这就是WebFuzz为什么被设计为从纯文本文件中抽取模糊变量的原因。添加新变量非常简单,并且不需要再次编译应用程序。为了执行测试,我们将下面的行添加到xssinjection.txt文件。

    这只是下面图像请求的一个URL编码版本,该图像请求试图从我们的本地web服务器中检索一个虚假页。

    当检查我们的Web服务器日志文件时,将会看到什么呢?

    执行失败!当WebFuzz将图像请求注入到rootlogin.asp页时,产生了404-页没有找到的日志条目,同时也从正面证明了该页存在XSS漏洞。

    10.6 益处和改进的余地

    WebFuzz所带来的好处来自于它提供给终端用户的控制层次。也就是说,用户可以完全控制请求的所有方面。然而,这同时也需要对HTTP有较深的理解,以生成一个有意义的、更加奏效的请求。该工具的请求部分可以被改进的一个方面是允许更加复杂的模糊变量的组合。例如,可以添加依赖变量以使每次能够添加多于一个的变量,并且一个变量值由另一个变量的值所决定。

    这里有许多改进的余地以提高WebFuzz的自动化程度,使得允许完整的覆盖整个应用程序。这可以通过添加蜘蛛程序的功能来实现,该程序可以被初始化运行以识别所有可能的请求及其结构。将WebFuzz中的该功能输出到一个Web浏览器扩展也将是有价值的,因为它允许当在网络中遇到特定的请求时,将这些请求进行模糊化。最后,我可以预见将来能够对识别潜在漏洞的请求进行自动化的检测。例如,一个解析器引擎可以扫描一个原始响应以寻找象用户提供的数据那样的标识符,响应中的这些标识符可以指示XSS漏洞的存在。

    现在我们已经使测试工具开始运转了,下面的工作就要靠你了。提高该工具的性能,并且通过将你所做的改进提交给我们来使其他人共享改进,这样我们就可以将该工具提升到一个新的版本。

    10.7 小结

    Web应用向我们展示了一些独特的挑战工作,但它们对模糊测试而言却是确定的合适的目标应用。我们已经阐述了在发布你的Web应用程序之前,使用模糊测试来发现其中漏洞的价值。WebFuzz只是一个概念上的试验,但即使是在其原始版本状态,它也可以发现漏洞。我们希望你获取并仔细研究WebFuzz的源代码,将它创建为一个更加有效的工具。

    第11章 文件格式模糊测试

    "如果这是专政,可能会令人很不舒服,但我就是一个独裁者。"

    --George W. Bush, Washington DC,2000年12月19日

    文件格式模糊测试是一种针对特别定义的目标应用的特殊的模糊测试方法,这些目标应用通常是客户端应用。其中的例子包括媒体播放器、Web浏览器以及office办公套件。然而,目标应用也可以是服务器程序,如防病毒网关扫描器、垃圾邮件过滤器以及常用的邮件服务程序。文件模糊测试的最终目标是发现应用程序解析特定类型文件的缺陷。

    大量的客户端文件格式解析漏洞在2005年和2006年被发现,其中许多是被进行恶意攻击的团体所发现的,因为在典型的漏洞发掘过程之前,发现了许多在安全补丁发布前而被了解和利用的漏洞信息。eEye安全研究组开展了很好的工作以将这些披露的漏洞详细的描述在他们的Zero-Day跟踪器中 。这里有大量的事实表明这些漏洞中的绝大部分是通过文件格式模糊测试而发现的。这种类型的错误远没有灭绝的迹象,这使得文件格式模糊测试成为一个非常有趣并且热门的研究课题。

    在本章中,我们提出了实现文件模糊测试的不同方法,同时也讨论了特定目标应用接收输入的不同方式。最后,我们说明了文件模糊器将会遇到的普遍的漏洞,并且给出了在实际中检测这些漏洞的建议。当然,所要做的第一项工作就是选择一个合适的目标应用。

    11.1 目标应用

    就像传统类型的模糊测试一样,使用文件格式模糊测试可以发现许多不同类型的漏洞。同样,这里也有许多不同类型的发掘场景。例如,有些情形要求一个攻击者向用户发送一个恶意文件,并且让他或她手工的将文件打开。而其它的情形可能只要求一个用户浏览被攻击者控制的Web页。最后,有些场景能够通过简单的经由邮件服务器或防病毒网关发送一个恶意的邮件而触发。最后一个场景就是表11.1提到的微软的Exchange TNEF漏洞,同时该表也列出了其它文件格式漏洞的例子。

    表11.1 含有已发现的文件格式漏洞的常用应用程序类型和示例

    应用分类

    漏洞名

    查询网站

    Office办公套件

    微软HLINK.DLL

    超链接对象库缓

    冲区溢出漏洞

    http://www.tippingpoint.com/security
    /advisories/TSRT-06-10.html

    防病毒扫描器

    卡巴斯基防病毒引

    擎CHM文件解

    析器缓冲区溢出漏洞

    http://www.idefense.com/intelligence
    /vulnerabilities/display.php?id=318

    媒体播放器

    Winamp m3u解析

    栈溢出漏洞

    http://www.idefense.com/intelligence
    /vulnerabilities/display.php?id=377

    Web浏览器

    向量标记语言中

    的漏洞可以允许

    远程代码执行

    http://www.microsoftcom/technet
    /security/Bulletin/MS06-055.mspx

    存档工具

    WinZip MIME解

    析器缓冲区溢出

    漏洞

    http://www.idefense.com/intelligence
    /vulnerabilities/display.php?id=76

    邮件服务器

    微软Exchange TNEF

    解码器漏洞

    http://www.microsoftcom/technet
    /security/Bulletin/MS06-003.mspx

    你将会发现大多数的目标应用都属于这些分类中的某一个类别。某些应用则由于它们的次要功能而同时属于多个分类。例如,许多防病毒扫描器也包括解压缩文件的库,以允许它们作为存档工具。还有一些内容扫描器声称可以为情色内容分析图像文件。这些程序也可以被认为是图像查看器 。应用程序共享公共库是很普遍的,在这种情况下一个单独的漏洞可以影响多个应用。例如,考虑一下在微软的安全公告MS06-055中详细描述的漏洞,它既可以影响Internet Explorer也可以影响Outlook。11.2 方法

    文件格式模糊测试与其它类型的模糊测试是不同的,因为它通常是在一个主机上完整的执行。当执行Web应用或网络协议模糊测试时,你很可能至少会使用两个系统,即一个目标系统和一个模糊器所运行的系统。通过能够在一个单独的机器上进行模糊测试而使性能得到了提高,这使得文件格式模糊测试成为一种很有吸引力的漏洞发现方法。

    对于基于网络的模糊测试而言,在目标应用中何时发生一个有趣的条件通常是很明显的。在许多情况下,服务器将关闭或者立即崩溃,并且将不能够再连接。而对于文件模糊测试而言,主要是在对客户端应用进行模糊测试时,模糊器将会继续重新开始运行并且销毁目标应用,因此如果不使用适当的监视机制,那么模糊器将可能不会识别出一个崩溃情形。这是文件格式模糊测试比网络模糊测试更加复杂的一个应用领域。对于文件格式模糊测试而言,模糊器通常必须要监视目标应用的每次执行以发现异常。这通常可以通过使用一个调试库来动态的监视目标应用中已处理和未处理的异常来实现,同时要将结果记为日志以便于以后的评审。从较高的层次上看,一个典型的文件模糊器的工作过程如下:

    1.通过变异或者生成来准备一个测试用例(后者使用的更多)。

    2.部署目标应用并指示其加载测试用例。

    3.监视目标应用以发现错误,通常是使用一个调试器。

    4.当发现一个错误时,记录日志。同样,如果经过一段时间之后没有错误被发现,手工销毁目标应用。

    5.重复上述过程。

    文件格式模糊测试可以通过生成和变异两种方法来实现。尽管这两种方法在实践应用中都很有效,但显然变异或"强有力"的方法更加易于实现。尽管生成方法或"智能强有力"模糊测试的实现要花费更多的时间,但它们可能将发现用其它的更原始的强有力方法所不能发现的一些漏洞。

    11.2.1 强制性或基于变异的模糊测试

    使用强制性模糊测试方法,你需要首先收集目标文件类型的一些不同的例子。你所能找到的不同文件越多,那么你的测试将越彻底。然后模糊器将作用在这些文件上,创建它们的变异文件,并通过目标应用解析器将它们进行发送。这些变异文件可以具有任意形式,这取决于在模糊器中所选择的方法。你可以使用的一个方法是用数据字节来替换字节。例如,遍历文件并用0xff替换每个字节。你也可以为多个字节执行此操作,例如两字节或四字节。你同样可以向文件中插入数据,而不只是覆盖字节。在测试字符串值时,这是一种非常有用的方法。然而,当向文件中插入数据时,需要注意可能会扰乱文件中的偏移量。这将会严重的破坏对代码的覆盖,因为某些解析器将会快速的检测一个无效文件并且退出。

    校验和也可以破坏强制性解析器。由于任何的字节改变都将使校验和变为无效,因此解析应用程序很可能将退出,并且在一个潜在的漏洞代码片段能够被到达之前提供一个错误消息。该问题的解决方法,一是转换到智能模糊测试,这将在下一节中进行讨论。或者是停止在目标软件中的校验。停止软件的校验并不是一项简单的工作,通常需要进行逆向工程的大量工作。

    一旦该方法被实现之后,为什么非常便于使用呢?答案很简单。因为终端用户不需要具备文件格式的任何知识,并且也不需要了解其工作机理。如果他们可以使用常用的搜索引擎或者通过搜索他们的本地系统来找到一些示例文件的话,他们就可以通过自己的研究来进行操作,直到模糊器发现某些感兴趣的内容。

    这种模糊测试方法也具有一些缺点。首先,它是一种效率不高的方法,因此需要花费较多的时间来完成对一个单一文件的模糊测试。例如,对于一个基本的word文档的测试。即使是一个空文档,其大小也大约有20KB。每次对一个字节进行模糊化,那么将需要创建并部署20480个独立的文件。假定每个文件的处理需要2秒钟,那么总共将要花费11个小时,而这只是对一个单一字节值的处理。那么处理其它254个字节将会怎样呢?可以通过使用多线程模糊器在某种程度上回避这个问题,但它却说明了单纯的变异模糊测试具有不高的效率。简化这种模糊测试的另外一种方法是只关注文件中最有可能产生期望结果的那些部分,如文件头和字段头。

    强有力模糊测试的主要缺点是几乎总是有许多的功能将会被遗漏掉,除非你以某种方式收集到包含每个和所有可能特性的一个示例文件集。大多数的文件格式都是很复杂的,并且包含大量的变换。当度量代码覆盖率时,你将会发现将一些示例文件使用于一个应用中并不像另外一种情形那样彻底的执行应用,这另外的情形就是用户真正的理解了文件格式,并且手工准备了一些针对文件类型的信息。这个问题可以使用文件模糊测试中的生成方法来解决,也即我们所说的智能强有力模糊测试。

    11.2.2 智能强制性或基于生成的模糊测试

    使用智能强制性模糊测试,你必须要首先花费一些精力来实际的研究文件规范。一个智能模糊器仍然是一个模糊测试引擎,因此仍然执行一个强制性攻击。然而,它依赖来自于用户的配置文件,以使测试过程更加智能。这些文件通常包含描述文件类型语言的元数据。可以将这些模板想象为数据结构的列表,它们的位置及其可能的取值都是相互关联的。在一个实现的层面上看,这些模板可以用许多不同的格式来表示。

    如果一个没有任何公开文档的文件格式被选作用于测试,那么你作为一个研究者,则必须要在创建一个模板之前对格式化规范进行深入的研究。这可能需要逆向工程的知识,但首先要使用你的好朋友Google来查看一下是否已经有其他人为你做了这项工作。一些Web站点,如Wotsit的Format ,就提供了很好的官方和非官方的文件格式文档的服务。一个可选的作为补充的方法是比较文件类型的这些例子以发现一些模式,并且剖析一些被使用的数据类型。记住,一个智能模糊器的有效性直接相关于你对文件格式的理解以及采用一种通用方法将其描述给所使用的模糊器的能力。在第12章"文件格式模糊测试:UNIX平台上的自动化测试"中,当创建SPIKEfile时,我们将展示智能强有力模糊器的一个示例实现。

    一旦一个目标应用和测试方法被确定,下一步就是针对所选定的目标应用,研究如何生成适当的输入向量。

    11.3 输入

    选择了一个目标应用之后,下一步就是枚举出该应用所支持的文件类型和不同向量的扩展以使这些文件被解析。可用的格式化规范也应当被收集并评审。即使在你只希望执行一个简单的强有力测试的情况下,具有相关的文件格式知识仍然是很有用的。关注于更加复杂的文件类型会更加有利,因为实现一个恰当的解析器将会是更加复杂的,因此发现一个漏洞的机会就会增加。

    让我们通过一个例子,来看一下如何收集输入。存档工具WinRAR 是一个免费使用的流行的存档工具。了解WinRAR将处理何种文件的一个简单方法是浏览WinRAR网站。在该网站的主页上,你将会看到它所支持的文件类型的一个列表。这些类型包括zip,rar,tar,gz,ace,uue以及其它的一些类型。

    现在你已经知道了WinRAR所能处理的文件类型的列表,下一步就必须选择一个目标应用。有些时候,选择一个目标应用的最好方法就是寻找关于每个文件类型的信息,然后选择其中最复杂的一种类型。这里所假设的前提条件是复杂性通常会导致编码错误。例如,使用一些长标记值和用户提供的偏移量的文件类型可能会比基于静态偏移量和静态长度字段的简单文件类型更加具有吸引力。当然,一旦你掌握了模糊测试,你将会发现该规则也有许多例外的情况。在理想情况下,模糊器将最终面向所有可能的文件类型;第一个选择并不是十分重要,然而它总是有利于在针对特定应用的第一个模糊测试集中发现有趣的行为。

    11.4 漏洞

    当解析一个不规则文件时,一个编写的很差的应用程序易于受到不同类别漏洞的攻击。本节讨论其中的一些漏洞类别:

    DoS(应用崩溃或挂起)

    整数处理问题

    简单的栈/堆溢出

    逻辑错误

    格式化字符串

    竞争条件

    11.4.1 拒绝服务

    尽管DoS问题在客户端应用中并不是十分有趣,但你需要记住我们也可以面向服务器应用,而该应用必须要保持对安全性和生产力的可用性。当然,这包括邮件服务器和内容过滤器。在实践应用中,导致文件解析代码中的DoS问题的一些常见原因包括越界读取、无限循环和空指针解除引用。

    导致无限循环的一个通常错误是相信文件中制定其它块的位置的偏移量值。如果应用程序不能保证该偏移量使当前块向前移动,那么就会发生一个无限循环并导致应用程序无限制的重复处理同一个块或多个块。以前在ClamAV中已经有许多该类问题的实例。

    11.4.2 整数处理问题

    整数溢出和"有符号"问题在二进制文件解析中是非常普遍的。

    这个例子说明了一个典型的导致内存破坏的整数溢出情况。如果文件为value值指定了最大的无符号32位整数(0xFFFFFFFF),于是第[2]行中的allocation_size变量由于一个整数溢出而被赋值为0。在第[3]行,该代码将导致用大小为0的值来调用内存分配函数。在这时,指针buffer将指向一个未分配的内存块。在第[4]行和第[5]行,应用程序执行循环并将个数等于size的初始值的大量数据复制到所分配的缓冲区中,从而导致一个内存破坏。

    这种特殊情形并不总是能够被发现。它的可发现性取决于应用程序使用堆的方式。简单的覆盖内存中的堆通常不足以获得对应用程序的控制权。必须要采取一些操作使得被覆盖的堆数据能够被使用。在某些情况下,类似于此的整数溢出在堆内存被使用之前将导致一个非堆相关的崩溃的发生。

    当然这只是一个例子,说明了在解析二进制数据时整数如何被不正确的使用。整数的错误使用还表现在其它的许多方面,包括经常提到的无符号整数比较错误。

    如果size值是一个负数,那么该段代码将产生一个基于栈的溢出。这是因为在第[4]行的比较中,size(在第[1]行中定义)和MAX_ITEMS(在第[0]行定义)都被作为有符号数来处理,例如,-1小于512。在后面,当size在第[7]行的函数中被用做复制的边界变量时,它是被作为无符号数来处理的。例如,此时的值-1将被解释为42949672954294967295。当然,并不能保证可以发现这类情况,但在很大情况下依赖于readx_from_file函数的实现方式,可以通过监视变量以及将寄存器保存到栈的方法来发现此情况。

    11.4.3 简单的栈和堆溢出

    该问题已经被很好的理解,并且在以前已经出现过许多次了。一个典型的场景如下:一个固定大小的缓冲区被分配,无论它是位于栈中还是位于堆中。稍后,当从文件复制超过缓冲区大小的数据时,没有执行任何边界检查。在某些情况下,虽然会试图进行一些边界检查,但这些检查也是不正确的。当复制操作发生时,内存被破坏,通常还会导致任意代码执行。关于该类漏洞的更加详细的信息,请参阅"Shell编码手册:发掘并利用安全漏洞" 。

    11.4.4 逻辑错误

    依赖于文件格式的设计,可利用的逻辑错误可能会存在。尽管在对文件格式漏洞的研究过程中,我们没有亲自发现任何逻辑错误,但该类漏洞的一个极好的例子就是MS06-001所描述的微软的WMF漏洞 。该漏洞并不是源自于一个典型的溢出。实际上,它不要求任何类型的内存破坏,它允许一个攻击者直接执行用户提供的位置无关的代码。

    11.4.5 格式化字符串

    尽管大部分格式化字符串漏洞已经不存在了,特别是在开源软件中,但仍然有必要提及它们。当我们讲大部分不存在时,之所以这样说是因为并不是所有的程序员都能象US-CERT的程序员那样关注安全,US-CERT的程序员建议为了保证你的软件的安全性,不应当使用"%n"格式化字符串指定符 。

    但情况更加严重,在我们亲自进行的实验中,当对文件进行模糊测试时确实发现了一些与格式化字符串相关的问题。其中的某些问题已经在Adobe 和RealNetworks 中发现。发现格式化字符串问题的许多乐趣来自于能够使用漏洞以泄露内存,从而帮助发现问题。不幸的是,由于使用非格式化文件的客户端攻击,你很少能够被提供这样的机会。

    11.4.6 竞争条件

    尽管人们通常不认为文件格式漏洞是由于竞争条件所引起的,但是有一些人是这样认为的,并且可能还会有更多的人这样认为。这种类型漏洞的主要目标应用是复杂的多线程应用。我们反对只面向一个特定的产品,但是微软的Internet Explorer是这里所想到的第一个应用程序。Internet Explorer使用非初始化内存以及其它线程正在使用的内存而导致的漏洞,将可能会被不断的发现。

    11.5 漏洞检测

    当对文件格式进行模糊测试时,通常要产生目标应用的许多实例。其中一些将会不确定的挂起并且必须要被销毁掉,一些将会发生执行崩溃,而另外一些将会主动的退出。所面对的挑战是需要确定一个已处理和未处理的异常何时发生,以及该异常何时被发现。一个模糊器可以利用一些源信息以发现一个进程的更多信息:

    事件日志。事件日志被微软的Windows操作系统所使用,它可以通过使用事件查看器应用程序来访问。事件日志对于我们来说并不是十分有用,因为当我们在模糊测试的过程中部署了成百上千个进程时,它很难将一个事件日志条目同一个特定的进程相关联。

    调试器。识别未处理和已处理异常的最好方法是在进行模糊测试之前将一个调试器关联到目标应用。错误处理将阻止由模糊测试而引起的许多错误的明显征兆的出现,但是这些错误通常可以使用调试器来发现。还有许多比使用调试器来进行错误检测更加先进的技术,其中的一些技术将在第24章"智能故障检测"中介绍。

    返回代码。捕获并测试应用程序的返回代码,虽然它不如使用调试器那样准确并提供详细的信息,但它是一种非常快速而有力的确定应用程序终止原因的方法。至少在UNIX平台上,它可以通过返回代码来确定是什么信号导致了应用程序终止。

    调试API。不使用第三方的调试器,而在模糊器中实现某些基本的调试功能通常是可行并且有效的。例如,对于一个进程而言我们所感兴趣的是它被终止的原因,当前的指令是什么,寄存器的状态以及栈指针或某些寄存器区域的内存值等。通常实现这些功能都很简单,并且在分析崩溃原因以查找漏洞的时间节省方面不具有应用价值。在下一章中,我们将研究该内容,并提出一个在微软Windows平台上的名为PyDbg的简单可复用的调试器创建框架,该框架是PaiMei 逆向工程架构的一部分。

    一旦一个给定的测试用例被确定用来导致一个错误,则除了保存触发错误的实际文件之外,还必须要保存被你所选择的任何错误监视方法所收集的信息。同样,在你的记录中保存测试用例的元数据也是非常重要的。例如,如果模糊器使用第42个模糊值正在对文件中的第8个变量字段进行模糊测试,那么该文件就可以被命名为文件-8-42。在某些情况下,我们可能会释放一个内核文件并且也将其保存。如果一个模糊器使用调试API来捕获信号的话就可以实现这一点。关于此的特定实现细节可以在下两章中找到。

    11.6 小结

    尽管文件格式模糊测试是一种定义面较窄的方法,但是仍然有许多的目标应用和大量的攻击方法。我们不仅讨论了传统的客户端文件格式漏洞,而且涉及了一些远程服务的情形,如防病毒网关和邮件服务器。强调的重点放在了阻止和发现通过TCP/IP进行的基于网络的攻击,文件格式对于深入剖析内部网络段仍然是一种有价值的手段。

    第12章 文件格式模糊测试:UNIX平台上的自动化测试

    "我是指挥官(当然,我不需要对此解释),我不需要解释为什么这么说。这就是当总统的有趣之处。"

    --George W. Bush, 引自Bob Woodward的"Bush at War"

    文件格式漏洞既可以在客户端被利用,如Web浏览器和office套件,也可以在服务器端被利用,如邮件扫描器和防病毒网关等。关于在客户端利用的问题,受影响客户端的广泛使用与该问题的严重程度直接相关。例如,影响微软的Internet Explorer的一个HTML解析漏洞,在严重性被考虑之前是被最为广泛使用的漏洞之一。相反,局限于UNIX平台的客户端漏洞因其受限暴露而不被关注。

    然而,在本章中我们将介绍两个模糊器,即notSPIKEfile和SPIKEfile,它们分别实现了基于变异的文件格式模糊测试和基于生成的文件格式模糊测试。我们将讨论这两种工具所具有的特性及其不足。然后,进一步深入到工具的开发过程中,阐述这些工具的开发方法并分析一些核心的代码片段。我们也将论述一些基本的UNIX概念,如感兴趣和不感兴趣的信号,以及僵死过程。最后,我们将介绍这两种工具的应用,并解释选择所用开发语言的原因。

    12.1 NOTSPIKEFILE和SPIKEFILE

    被开发用来说明在UNIX上进行文件模糊测试的两个工具是notSPIKEfile和SPIKEfile。正如它们的名字所示,它们一个是基于SPIKE ,而另一个则不基于SPIKE。下面列出了它们在实现中所提供的一些关键特性:

    集成了最小化的调试器,能够检测已处理和未处理的信号,并清空内存和寄存器状态。

    使用用户所指定的在销毁目标进程之前的延迟来实现完全的自动化模糊测试。

    针对目标二进制代码和ASCII可打印数据类型的两个不同的启发式模糊数据库。

    以导致问题的历史数据值为基础,实现简单的易扩展的启发式模糊数据库。

    12.1.1 缺少的特性

    notSPIKEfile和SPIKEfile和中缺少了一些特性,而这些特性对于使用这两种工具的某些用户来说可能是有用的。下面列出了这些所缺少的特性:

    可移植性。由于集成调试器被创建工作在x86上,并且使用Linux ptrace,那么它在其它体系架构或操作系统中将不能工作。然而,增加兼容性是非常简单的,并且提供了其它调试和解析库的可用性。

    智能加载监视。尽管用户可以立即指定对多少进程进行模糊测试,但是当前非常需要用户来确定系统是否过载。

    显然,在开发过程当中做出了一些折衷考虑,正如这些缺少的特性所描述的那样。工具的开发者通过声明将改进留作"读者的一个练习"而利用一个古老而隐蔽的工具。让我们来看一下在这些工具的创建过程中所实施的开发活动。

    12.2 开发方法

    本章的焦点不在于如何使用一个模糊器,而在于如何实现一个模糊器。本节描述文件格式模糊器在Linux平台上的设计和开发细节。在文件格式模糊器的开发阶段,我们可以放心的假设我们的模糊器将在与目标应用相同的系统中执行。我们的设计包括三个不同的功能构件。

    12.2.1 异常检测引擎

    这部分代码负责确定目标应用在何时说明某些未定义的潜在的不安全行为。如何实现这一点呢?这里有两个基本的方法。第一个方法相当简单,它只是简单的监视被应用程序所接收的任意信号。这就允许模糊器检测将导致一个无效内存引用的错误,如缓冲区溢出。当然,该方法将不会捕获诸如元字符注入或逻辑缺陷等行为。例如,考虑一个应用程序将部分由攻击者提供的值传递给UNIX系统函数。这将允许一个攻击者通过使用shell元字符来执行任意程序代码,而不会引起任何类型的内存访问违规。尽管这里存在漏洞以允许一个攻击者危及主机的安全性,但该安全性完全不包括内存破坏。因此,最后所作出的决定是我们并不对这样的错误感兴趣,因为检测这些错误所需要付出的实现努力要远大于所得到的回报。

    对于那些想要发现这些逻辑错误的人来说,一个好的方法是钩住C库函数(LIBC),并监视被不安全的传递给函数调用的那些由模糊器提供的值,这些函数调用包括open,creat,system等。

    对于我们的设计而言,我们决定简单的使用系统的ptrace调试接口来检测信号。尽管通过简单的等待应用程序返回来确定一个信号导致了应用程序的终止是不重要的,但还要做一些工作来检测被应用程序在内部进行处理的那些信号。正因为如此,异常检测引擎所采用的开发方法要严重的依赖于系统的调试接口,在这种情况下就是ptrace系统调用。

    12.2.2 异常报告(异常检测)

    在发现异常的过程中,一个好的模糊器应当对所发生情况的有用信息做出报告。至少应当报告所产生的信号。在理想情况下,更加详细的信息如恶意指令,CPU寄存器状态以及一个栈的清空也应当包含在报告中。本章所实现的这两个模糊器都能够生成这样的详细报告。为了获取这些所需要的、低层次的信息,我们必须在系统调用ptrace的帮助下来收集这些信息。并且如果我们希望向用户报告这些信息,那么还要利用一个库来解析指令。该库应当能够将任意数据的一个缓冲区转换为x86指令的字符串表示。因其性能的良好性我们选择了libdisasm库 ,它提供了一个非常简单的接口,并且被看做是Google搜索引擎的最佳选择。为了提供其实用性,libdisasm包括了一些示例,我们可以从其中剪切和粘贴代码,极大的提高了使用的便利性。

    12.2.3 核心模糊测试引擎

    文件格式模糊器的核心是,它控制着使用什么样的异常数据以及在哪里使用这些数据。notSPIKEfile中实现此功能的代码与SPIKEfile中实现此功能的代码是不同的,因为SPIKEfile是利用已经存在的SPIKE代码来实现此功能的。但其余部分二者是非常相似的。

    毫无疑问,你可能已经猜到了,SPIKEfile使用了与SPIKE相同的模糊测试引擎。模糊测试引擎需要一个模板,即描述文件格式的一个SPIKE脚本。然后,SPIKE将使用有效数据和无效数据的组合来智能的生成该格式的不同变体。

    在notSPIKEfile中,模糊测试引擎的局限性更大。用户必须要向模糊器提供一个有效的目标文件。然后,引擎使用一个模糊值数据库来对该文件的不同部分进行变异。这些值被划分为二进制和字符串两种类型。二进制值可以是任意长度以及任意值,但通常被用来表示公共整数字段的大小。字符串值正如其名字所示,只是表示字符串数据。这些字符串数据可以包括长或短的字符串,特定格式的字符串以及其它所用种类的异常值,如文件路径、URL和你能想到的任意其它类型的不同字符串。为了得到这些类型的一个完整列表,可以SPIKE 和notSPIKEfile的源代码,但作为一个入门,表12.1为一些有效的模糊字符串提供了一个简明的列表,并给出了简短的解释。虽然它和完整的列表相差很多,但它有助于你理解什么类型的输入应当被考虑。

    表12.1 一些常用的模糊字符串及其含义

    字符串

    含义

    “A”x10000

    长字符串,可能导致缓冲区溢出

    “%n%n”x5000

    含有百分号的长字符串,可能导致缓冲区溢出或触发格式化字符串漏洞

    HTTP://+”A”x10000

    有效的URL格式,可能触发URL解析代码中的缓冲区溢出

    “A”x5000+”@”+”A”5000

    有效的邮件地址格式,可能导致邮件地址解析代码中的缓冲区溢出

    0x20000000,0x40000000

    0x80000000,0xffffffff

    一些可能触发整数溢出的整数。在此可以充分发挥你的创造性。考虑一下代码malloc(user_count*sizeof(struct blah));也考虑一下没有对溢出和下溢进行检查就递增或递减整数的代码。

    “../”x5000+”AAAA”

    可能触发路径或URL地址解析代码中的溢出

    实际上,这里对你可以使用的模糊字符串的数量并没有限制。然而需要注意的是,任意类型的值可能会要求进行特殊的解析,或者是产生一个应当被很好表示的异常条件。造成使用模糊器错过和发现一个错误的不同结果的原因,可能就是由于一下简单的操作如将.html追加到一个长模糊字符串的末尾。

    12.3 有意义的代码片段

    在这两个模糊测试工具中被共享的大部分核心功能都在下面进行了分析。例如,首先强调一下对子进程进行派生和跟踪的基本方法。

    主进程或父进程开始于为目标应用派生一个新的进程。该新进程或子进程使用ptrace调用来表明它将通过发出PTRACE_TRACEME请求来被其父进程跟踪。然后,该子进程继续目标应用,并知道其父进程会象任何执行良好的进程一样监视其是否会执行任何不恰当的操作。

    作为父进程,模糊器能够接收发送给子进程的所有信号,因为子进程使用了PTRACE_TRACEME请求。父进程甚至会接收对执行函数家族的任何调用的一个信号。父循环是简单明了并且作用强大的。模糊器循环接收发送给子进程的所有信号。模糊器根据信号和子进程的状态而采取不同的行为。

    例如,如果进程中断运行,这意味着程序还没有退出,但是它接收到了一个信号并等待父进程做出判断以允许它继续执行。如果该信号表示了一个内存破坏问题,那么模糊器将把信号传递给子进程,并假定它能够销毁子进程并报告结果。如果该信号本质上并没有什么问题或者只是一个很平常的信号,那么模糊器将把它传递给应用程序而不执行任何监视。

    模糊器同时要检查子进程是否真的被中断,或者是其它的非关联情形。你可能会问,如果应用程序崩溃那么会发生什么情况呢?我们是否应当对此给予关注呢?好的,由于我们在应用程序实际中断运行之前,已经截取了所有感兴趣的信号,我们知道应用程序中断运行是因为一个自然的行为或者是一个不感兴趣的信号。这就强调了你理解应当对哪些信号感兴趣是非常重要的。对于那些寻找DoS漏洞的人来说,他们可能会对一个浮点数异常感兴趣。而另外一些人则可能只对真正的内存破坏问题感兴趣。那么其他人可能会等待一个异常中断信号,该信号已经成为了新版本GLIBC的堆破坏的一个指示器。已经表明,在某些情况下这些堆破坏检查可以通过暂时解决该问题来执行任意代码 。

    在查看了处理特定信号的这些代码之后,可能会产生这样的疑问,即为什么对特定信号的处理方法不同于处理其它信号的方法呢?下面是在对漏洞进行研究的环境中,针对UNIX信号的一个解释。

    12.3.1 通常感兴趣的UNIX信号

    表12.2提供了漏洞研究者在模糊测试过程中可能认为是感兴趣的信号的一个列表,同时给出了感兴趣的原因。

    表12.2 在UNIX平台上执行模糊测试的一些感兴趣的信号

    感兴趣的信号名

    含义

    SIGSEGV

    无效的内存引用。成功模糊测试的最常见结果。

    SIGILL

    非法指令。它是内存破坏所产生的一个可能的

    副作用,但是相对很少发生。它通常是由于程

    序计数器被破坏或在数据和指令中间停止而引起的。

    SIGSYS

    错误的系统调用。它也是内存破坏产生的一个

    可能的副作用,但是相对也很少发生(实际上

    是非常少发生)。造成产生该信号的原因与

    SIGILL是相同的。

    SIGBUS

    总线错误。通常是由于某些形式的内存破坏,

    或者是不正确的访问内存。由于它们的排列需求,

    使得在RISC机器上经常产生此信号。在大多数

    的RISC实现中,非排列的存储和加载将产生

    SIGBUS信号。

    SIGABRT

    由异常中断的函数调用所生成。这是非常有意

    思的,因为当GLIBC发现一个堆破坏时,它将

    产生异常中断。

    12.3.2 不太感兴趣的UNIX信号

    相对于表12.2,表12.3描述了在模糊测试过程中经常产生,但对于一个漏洞研究者而言不太感兴趣的一些信号。

    表12.3 在UNIX平台上执行模糊测试的一些不太感兴趣的信号

    不感兴趣的信号名

    含义

    SIGCHLD

    一个子进程已经退出

    SIGKILL,SIGTERM

    进程被销毁

    SIGFPE

    浮点数异常,如被0除

    SIGALRM

    计时器过期

    我们已经介绍了SIGCHLD信号,现在可以讨论如何对一个常见的场景进行处理,而该场景是由于对该信号的不正确处理而导致的。下一节解释了什么是一个僵死进程,以及如何正确的处理子进程以使僵死进程不被生成。

    12.4 僵死进程

    一个僵死进程是这样一个进程,它是从一个父进程中分离出来并且完全执行(即已经退出)的进程,但是其父进程没有通过调用wait或waitpid来得到其状态。当这种情况发生时,整个进程的信息仍然不确定的保留在内核中,直到父进程对其发出请求。在这时,这些信息被释放,进程才真正执行完成。从我们的模糊器中分离出的一个进程的通常的生命周期。

    当编写一个使用fork来生成子进程的模糊器时,必须要确保父进程使用wait或waitpid接收进程完成的所有状态信息。如果错过了一个进程的完成,那么将得到一个僵死进程。

    早期版本的notSPIKEfile含有一些错误,会导致活动进程的数量缓慢减少,直到模糊器到达一个死锁的状态。例如,假定用户一次使用8个进程来对应用程序进行模糊测试。随着时间的推移,活动进程慢慢的就会减少到1个。这是由于工具开发者的两个粗心错误而造成的。最初的设计完全依赖于SIGCHLD信号,该信号在子进程执行完毕后被发送给进程。使用此设计,就会错过某些SIGCHLD信号。同样在有些地方,当子进程执行完毕后,开发者没有成功的减少活动进程的数量,从而导致模糊测试进程的运行速度越来越慢。

    曾经提到过,这些错误很好解决。当使用WNOHANG标志使阻止变为非阻止时,所有的wait和waitpid调用都可能导致问题的发生。当它们返回时,检查所返回的状态以查看一个进程是否真正的执行完成。如果执行完成,那么活动进程的数量总是被递减。

    使用SPIKEfile,不能立刻部署多个应用程序从而极大的简化设计和实现。我们不需要担心错过了SIGCHLD信号,因为每次都有一个信号要返回来。

    由于SPIKEfile是基于SPIKE的,我们只需要向其中添加一个或两个文件,已使它能够处理文件的输入和输出而不只是传输其输入和输出。通过快速查看一下SPIKE是如何针对TCP/IP进行实现的,就能够简单的提供文件支持。

    通过将此文件添加到Makefile,那么曾经使用过SPIKE的任何人现在都可以象把文件作为终端那样简单的使用它。如果你对哪些文件被添加到SPIKE中感兴趣,那么表12.4就提供了相关的信息。

    表12.4 为生成SPIKEfile而对SPIKE所做的一些更改

    文件名

    目的

    filestuff.c

    包含打开和写文件的程序

    util.c

    包含在notSPIKEfile和SPIKEfile中所共享的许多代码。包含ptrace函数,主F_execmon函数以及其它的一些有用的函数。

    generic_file_fuzz.c

    这是主要的SPIKEfile源代码。它包含main函数

    include/filestuff.c

    filestuff.c的一个头文件

    Libdsasm

    当发生崩溃的情况生,被用来解析x86指令的库

    12.5 使用的注意事项

    如果你计划对一个不能直接部署的应用程序使用SPIKEfile或notSPIKEfile,那么需要以某种方式来暂时解决这个问题。这些类型应用程序的典型例子包括Adobe Acrobat Reader和RealNetworks RealPlayer。

    在通常情况下,对于这些应用程序而言,你实际所运行的程序是一个shell脚本函数。shell脚本建立起环境以使真正的二进制代码可以正确的运行。因此,应用程序就可以包含它们自身共享库的副本。由于包含了它们自身对公共共享库的副本,就增加了该产品的可移植性。尽管这样做是为了给用户的使用带来方便,但对于我们要实现的目的而言,它反而使情况变得更加糟糕。例如,如果我们指定shell脚本作为模糊测试的文件,那么当其运行时我们将不会被关联到实际的二进制文件,而被关联到shell的一个实例。这将会使我们的信号捕获变得没有意义。这有一个示例,说明了在Acrobat Reader和RealPlayer中的情况。对于诸如此类的其它应用程序而言,思想都是类似的。

    12.5.1 ADOBEACROBAT

    对Acrobat而言,你必须首先要使用-DEBUG选项来运行acroread。这将会进入到一个被设置了正确环境的shell脚本,以直接激活实际的acroread二进制代码,该代码通常存在于$PREFIX/Adobe/Acrobat7.0/Reader/intellinux/bin/acroread中。尽管它没有被公开化,并且不存在有用的函数,但是可以通过简单的阅读acroread shell脚本来确定此信息。你现在可以对此二进制代码进行模糊测试而不会发生任何问题。该方法被notSPIKEfile所使用以发现Adobe Acrobat Reader UnixAppOpenFilePerform的缓冲区溢出漏洞。

    12.5.2REALNETWORKS REALPLAYRE

    正如下面的输出所显示的,realplay命令实际上是一个shell脚本。我们也看到实际的二进制应用程序被称作realplay.bin。

    对于RealPlayer而言,你只需要将shell环境变量HELIX_PATH设置为RealPlayer的安装路径。然后就可以直接对包含在RealPlayer中的二进制文件realplay.bin进行模糊测试了。通过简单的阅读realplayshell脚本就可以确定此信息。在notSPIKEfile中使用此方法,就可以发现RealNetworksRealPlayer/HelixPlayer RealPix中的格式化字符串漏洞。

    12.6 实例研究:REALPLAYER REALPIX格式化字符串漏洞

    我们现在将要讲述如何使用notSPIKEfile来发现RealPlayer中的一个漏洞,而该漏洞于2005年9月被修补。第一步是任意选择RealPlayer所支持的一种文件格式。当然在这个例子中,选择了RealPix格式。在经过大量的搜索之后,一些示例RealPix文件被编译,并被用于notSPIKEfile模糊测试的基础文件。

    这只是一个很小的RealPlayer文件的一个框架示例。如果在RealPlayer中加载该文件,那么将不会显示任何内容,因为它只包含一个头。我们将通过notSPIKEfile来运行该文件以测试头解析代码。

    我们使用-t选项告诉工具对每个RealPlayer的激活要延续3秒钟。同时,使用-d选项告诉该工具在它销毁一个空闲进程和启动一个新进程之间要等待1秒钟。使用-m选项来指定部署RealPlayer的3个并发实例,并使用-r选项告诉工具对从0字节开始的整个文件进行模糊测试。我们还使用-S选项来指定字符串模糊测试的模式,并使用-s选项指定SIGKILL信号来终止空闲进程。最后,告诉工具为模糊测试文件名所采取的一种格式,并指定示例文件的文件名sample1.rp,并告诉工具如何执行RealPlayer以使它解析我们的文件。一切准备就绪。notSPIKEfile的输出通过报告一些崩溃信息来表明发现了某些类型的漏洞。

    我们将当前目录中的文件列出,可以看到文件FUZZY-sample1.rp-0x28ab156b-dump.txt被创建。当我们查看该文件时,就可以看到一个生成的描述崩溃时进程状态的详细报告。同时也可以看到导致崩溃的文件的名字。在这个例子中,它被保存为12288-FUZZY-sample1.rp。该文件可以被用来再现崩溃。当查看该文件时,我们得到了该问题可能是什么的一个很好的提示。

    由于%n字符的出现,我们立刻怀疑是一个格式化字符串漏洞导致了崩溃的发生。当在GDB中部署RealPlayer二进制文件时,我们的怀疑得到了证实。

    我们真的在RealPlayer对timeformat的选项的处理中发现了一个格式化字符串漏洞。关于如何利用该漏洞进行攻击的问题给读者留作练习思考。

    12.7 语言

    这些工具之所以采用C语言编写,是由于以下一些逻辑原因。首先,就像盐和胡椒粉的关系一样,C和Linux一直能够并且还将能够相互配合很好的进行工作。每个先进的Linux分布式系统都有一个C编译器,并且可能将会一直拥有,因为Linux的内核就是用C语言编写的。这里不需要任何特殊的库来将ptrace接口暴露给我们的应用程序,我们完全有自由来执行所需要的任务。

    这些工具使用C语言编写的另外一个原因是由于SPIKE就是用C编写的。由于至少有一个工具扩展的使用了SPIKE代码,同时这两个工具需要共享某些代码如异常处理和报告代码,因此采用不同的语言来实现相同的功能是不明智的。

    12.8 小结

    在可靠的文件模糊化测试的工具的支持下,客户端漏洞发掘工作变成了只是选择一个目标应用并且辅以耐心。不论你是选择编写自己的模糊器还是从其它的模糊器进行扩展,那么所花费的时间都是非常值得的。

    第13章 文件格式模糊测试:Windows平台上的自动化测试

    "发现那些伤害我们的人,并阻止他们的伤害,这是我们国家的利益所在。"

    --George W. Bush, Washington DC,2005年4月28日

    在上一章中,我们讨论了在UNIX平台上实现文件格式模糊测试。现在我们要转向发现Windows应用程序中的文件格式漏洞。尽管所有的概念都是一致的,但我们还是要强调一些重要的不同点。首先,Windows编程在本质上都是进行图形化的工具设计,因此我们将脱离上一章所提到的命令行应用,为脚本程序创建一个独特的GUI应用。同时还要花费时间来识别作为模糊测试目标的适当的文件格式,当工作在Windows环境时这是一个非常重要的决定,提供了针对给定文件类型对Windows中默认应用的严重依赖性。最后,我们为曾经提到的发现漏洞条件的挑战提供一种解决方法。

    13.1 Windows文件格式漏洞

    尽管文件格式漏洞也可以影响服务器,但它们更多的是影响客户端应用。文件格式漏洞的出现表明客户端漏洞的重要性正在不断的增加。网络管理者关注于资源以使协同网络免受网络层漏洞的攻击。同时,软件提供商也意识到了由服务器端漏洞所导致的威胁,这些努力联合起来减少了在流行应用程序和操作系统中的严重的服务器端漏洞数量,这些漏洞过去曾导致快速扩张的蠕虫并导致非常严重的损害。然而,对于客户端的情况却大不一样。客户端漏洞在过去的几年中一直在增长,并使它们成为了攻击的目标如钓鱼式攻击和身份窃取。

    文件格式漏洞给企业带来的独特的风险。尽管这些漏洞并不会使它们自己变为快速增长的蠕虫或直接危及网络的安全,但是它们在很多方面是难以被防护的。Internet网络从设计初衷上讲,是为了促进信息的共享。Web站点上充满了电影、图像、音乐以及文档,这使它变成了信息和娱乐的一个巨大的资源库。所有这些内容都要求文件要公开的免费的共享。我们从来没有将图片和电子表格这样的文件认为是恶意的,因为它们是不能自己执行的。然而正如前面所讨论的,当它们被一个漏洞应用所解释时就可能导致漏洞的发生。我们如何来避免此类威胁呢?网络管理者是否应当在防火墙阻止所有的内容呢?如果Web站点上只是基于文本的内容,那么它将会使人感到厌烦。我们真的想回到使用基于文本的浏览器如Lynx来浏览Web站点的时代吗?当然不是,但我们需要认识到由文件格式漏洞所带来的威胁。

    Windows平台更加易于受到文件格式漏洞的影响,因为它具有用户友好的特性。文件类型是与默认将要处理它们的应用程序关联在一起的。这就允许一个用户通过简单的双击文件就可以观看电影或阅读文档。甚至没有必要知道何种应用程序能够提供特定的文件类型以查看或聆听它们。想象一下由应用程序中所发现的一个文件格式漏洞带来的风险,该漏洞默认运行在Windows操作系统中。那么成百上千的终端用户将会立刻受到影响。下面列出了影响微软Windows的一些严重的文件格式漏洞。

    影响微软Windows的严重的文件格式漏洞

    MS04-028,JPEG处理(GDI+)中的缓冲区溢出,可以导致代码执行

    2004年9月,微软发布了一个安全公告,详细描述了一个缓冲区溢出漏洞,该漏洞发生在当GDI+图形设备接口所提供的JPEG图像包含一个无效大小的内容时。JPEG文件中的内容的前缀是0xFFFE字节值,后面跟着针对后续内容的一个2字节大小。内容的最小有效大小是2字节,因为内容大小包含被大小值自身所使用的2个字节。我们发现一个0或1的大小值将由于一个整数下溢而导致一个堆溢出。因为GDI+库(gdiplus.dll)被多个应用程序所使用,因此该漏洞影响了大量的Windows应用程序。在该安全公告发布之后,很快就出现了公开的漏洞利用代码。

    MS05-009,PNG处理中的漏洞,可以允许远程代码执行

    许多软件提供商,包括微软都曾开发了包含缓冲区溢出漏洞的应用程序,这些漏洞发生在当渲染包含有大块tRNS的可移植的网络图形(Portable Network Graphics,PNG)图像时,tRNS被用于图像的透明化处理。该漏洞影响了Windows消息处理器和MSN消息处理器,并且导致微软阻止含有漏洞的客户端访问它们的即时消息网络,直到适当的补丁被应用为止。

    MS06-001,图形渲染引擎中的漏洞,可以允许远程代码执行

    在2005年的假日期间,有报道称Web站点对恶意的Windows元文件(WindowsMeta File,WMF)文件进行攻击将会导致一个问题,即当使用Internet Explorer查看页面时将发生代码的执行。随后微软根据所发生漏洞的级别,在2006年的早期被迫发布了针对该问题的一个生命周期之外的补丁。该漏洞后来被证明是由于一个设计错误而导致的。WMF文件包含记录以允许参数被传递给特定的Windows图形设备接口(Graphics Device Interface,GDI)库的调用。其中的一个调用Escape及其子命令SETABORTPROC,实际上允许任意可执行代码被调用。稍后将说明该漏洞的细节信息已经被秘密的卖到了4000美元 。

    eBay网中的Excel漏洞拍卖

    2005年12月8日,一个由自称为"fearwall"的黑客发起的拍卖在eBay网上进行,该拍卖提供了微软Excel中的一个漏洞的详细信息 。该拍卖引起了媒体的重点关注,并且很快被eBay网取消,删除该拍卖的原因是eBay网引用了一条禁止非法活动 的政策。

    FileFuzz被创建用来将识别文件格式漏洞的过程实现自动化。FileFuzz要实现的目标包含三个方面。首先,应用程序应当是直观且用户友好的。其次,它应当既可以实现模糊化文件创建的自动化,也能够实现被设计用来解释这些文件的应用程序执行过程的自动化。第三,它应当组合有调试功能,以确保已处理和未处理的异常都能被识别。为了满足这些目标,我们选用微软的.NET平台来设计该应用程序。这就允许我们使用C#来创建图形化的前端,而其它的一些后端构件如调试器仍然用C语言编写。

    13.2 FileFuzz的特性

    FileFuzz被设计为通过使用一种简单而有效的强有力漏洞发掘方法来识别文件格式漏洞。FileFuzz破坏被正确格式化的文件,并将它们部署到被设计用来解释这些文件的应用程序中,然后查看是否发生任何问题。这种方法可能不是非常好,但它可以正确的工作。当开发FileFuzz时,我们对它非常容易的发现文件格式中潜在的漏洞条件感到非常的惊讶。

    FileFuzz的开发经历了3个不同的阶段。首先,它创建将要被模糊化的文件。它通过获得一个合法的用户提供的文件并基于所提供的目录,来进行有计划的连续的变异,并保存结果文件的方法来实现此功能。其次,变异后的文件一个接一个的被部署到目标应用中。这些文件被重复部署,并且结果进程最终基于用户定义的一个超时而被销毁。最后,内嵌的调试功能监视进程以识别可能发生的已处理和未处理异常。当这些事件被识别时,它们被记录下来并报告给终端用户。下面对每个阶段进行更加详细的介绍。

    13.2.1 创建文件

    FileFuzz采用了一种强制性方法来进行模糊测试。这意味着我们需要具有以下功能,即可以从一个有效的文件中进行读取,覆盖该文件的特定部分以及保存改变后的文件,这样它就可以被负责解释该文件的应用程序来读取。因为这个过程将被重复执行成百上千次,因此必须将它实现自动化。

    FileFuzz允许使用4个单独的方法(所有字节,范围,深度和匹配)来进行文件的变异,这些这些方法可以被划分为如下的类别:

    二进制文件

    宽度

    所有字节

    范围

    深度

    ASCII文本文件

    匹配

    可以看到,FileFuzz可以处理二进制文件格式和ASCII文本文件格式。在二进制文件中,采用了两个单独的方法,名为宽度和深度。为了区分宽度和深度,我们将使用钻井取油来进行类比。如果你正在一个广阔的区域内寻找石油,那么你不可能随意就开始钻井。你必须要首先使用一些不同的技术来确定那些最有可能包含石油的位置。可能你会研究地图,分析岩石的构造或者使用地面探测雷达。不管使用什么方法,一旦你发现这些感兴趣的位置,你就可以进行试探性的钻探以发现最佳的位置。

    对于文件格式模糊测试,我们也采用了一种类似的方法。首先,我们使用宽度方法来发现感兴趣的位置,然后使用深度方法来确定是否找到了需要的内容。宽度方法意味着覆盖文件中的所有字节或者一个特定的区域。通过连续的将区域内的字节值改变为一个预定义的值来生成单独的文件,直到整个区域都被覆盖。一旦该操作完成,这些文件被一个接一个的部署以确定这些变化是否导致了任何异常。

    有时候我们会比较幸运,不再需要进行其它操作。在有些时候,异常结果是很有趣的,并且终端用户可以控制崩溃,因为被引入的变异值在寄存器中是明显可见的。但在大多数情况下,并不会这么简单。一个崩溃将会发生并且位置可能是有趣的,但如果终端用户拥有控制崩溃的任何方法,那么寄存器的值将不是清晰可见的。在这种情况下,就该转向使用深度方法了。一旦感兴趣的字节位置在文件中被识别,我们将关注这些位置并使用深度方法来为该位置试验所有可能的字节值。当我们查看所生成的崩溃时,就可以了解能够控制该崩溃的程度。如果不论提供什么字节值,异常发生的位置和寄存器值都保持一致的话,那么就表明没有对该异常的控制权。然而,如果崩溃发生在不同的位置或寄存器的值连续的改变,那么很显然我们对所产生的异常至少造成了一些影响,而这种影响基于对文件进行变异时所使用的值。

    FileFuzz也可以处理ASCII文本文件。它是采用如下的方法来进行处理的,即允许终端用户首先选择一个标识将要被覆盖位置的字符串,然后请求三个单独的输入来确定被用来变异文件的输入。终端用户必须提供一个字符串值及其长度,以及它将要被增加的次数。下面来看一个例子。

    典型的,我们想要将值覆盖。假定我们要用连续的10个A字符来覆盖值。在这种情况下,我们首先将所发现的值设置为=字符,它标识了值的开始。然后将替换值设置为AX10X10。这将生成10个变异文件,即当一个=字符被发现时为每个实例使用覆盖值。所生成的10个文件将在值的位置包含从10到100个A字符。

    13.2.2 应用程序执行

    一旦模糊文件被创建,我们需要将它们部署到应用程序中。例如,如果我们已经将*.doc文件变异,然后想要将它们部署到微软的Word程序中。FileFuzz使用来自于Windows API的CreateProcess()函数来实现此功能,因此我们可以使用FileFuzz来部署应用程序,只要能够确定相同的应用程序如何从命令行被部署,并且包含可能传递给应用程序的必要标志。我们将在本章的后面详细的介绍如何识别这些信息。

    在文件格式模糊测试中,我们需要将同一个应用程序部署成百上千次。如果我们让所有前面已经执行过的进程开始运行,那么将会很快用完可用内存。因此,执行阶段并没有完成,直到在一个预定义的时间间隔之后该进程也被销毁为止。我们允许终端用户对此进行控制,方法是通过在执行标签下包含一个毫秒字段,以标识在需要时被强制销毁之前进程所允许被运行的总次数。

    13.2.3 异常检测

    正如在上一章中所提到的,检测部分是模糊测试的一个核心构件。如果整夜运行一个模糊器,当醒来时发现可以真正的使应用程序崩溃,但是你不知道是这1000个输入中的哪个导致了崩溃的发生,那么这样有什么好处呢?在开始测试之前你是不会得到答案的。文件格式模糊测试提供了许多检测异常的选项,但有一个是例外的。对初学者而言,可以从较低的技术层面入手,只是简单的观察模糊测试过程以识别错误窗口或应用程序和系统的崩溃。如果你喜欢"Watching Paint Dry",那么就可以采用这种方法。如果你倾向于将它提高一步,那么可以检查日志文件以确定是否发生了一个问题。这些日志文件既包括应用程序的日志文件,也包括系统日志文件,如被Windows事件查看器所维护的那些文件。该方法存在的问题是输入(模糊文件)和输出(日志事件)没有相互关联。将二者进行关联的唯一方法就是使用时间戳,但它并不是十分完美。

    对于大多数类型的模糊测试而言,识别异常的最好方法是使用一个调试器。调试器的优点是它既可以识别出已处理的异常,也能够识别出未处理的异常。Windows具有强大的异常处理功能,并且通常可以从它所读取的模糊文件中进行恢复。然而,识别这些异常情形是非常重要的,因为在文件异常处的一个小的改变就可以结束创建一个不可恢复或可利用的条件。

    调试器在文件格式模糊测试过程中的使用,并不像它在其它类型模糊测试中那样直接。在这种情况下,我们无法手工的将一个调试器关联到目标应用程序并使模糊器运行。这是因为模糊器要经常的部署并销毁目标应用程序。因此,它同样也会销毁调试器。为此,我们需要利用一个调试API并且在模糊器中直接创建调试器。采用这种方法,模糊器就可以执行目标应用,并且自己关联到一个调试器,然后销毁目标应用。因此,模糊器将在每次目标应用被部署时来监视异常。

    对于FileFuzz而言,我们创建了crash.exe,它实际上是一个被GUI应用执行然后部署目标应用的单机版调试器。它是一个完全的单机命令行应用程序,同时由于FileFuzz是一个开源项目,因此你可以免费在你自己的模糊测试项目中使用crash.exe。

    13.2.4 保存的审核

    在FileFuzz中,可以通过手工的方式来进行审核,方法是选择要进行模糊测试的目标应用并确定模糊文件应当如何被生成和保存。另外一种方法是,可以使用以前所保存的审核来填充所有需要的字段以立即启动模糊测试。带有一些已保存的审核信息的应用程序,可以通过主界面中的文件类型下拉菜单进行访问。然而,终端用户也可以创建他们自己的已保存的审核并将它们添加到下拉菜单中,而不用重新编译应用程序。这可能是由于菜单是在运行时通过解析target.xml文件而动态生成的。

    包含一个动态生成的下拉菜单是特意做出的一个设计决定。尽管FileFuzz是一个开源应用,但它假定大多数的用户将不具有编程经验,或通过额外编码来扩展功能的兴趣。使用动态菜单的方法就允许大多数终端用户以相对更加友好的方式来扩展功能。XML文件的结构说明了很重要的一点:尽可能使你的应用程序更加直观和用户友好。尽管文档是开发过程中的一个关键构件,但不要期望用户首先去查看文档。用户所期望的是直观的应用程序,可以简单直接的进行使用。

    请看前面所示的XML文件。描述性的XML标签是自解释的。终端用户可以通过简单的添加并定制一个额外的<test>块来添加一个审核。尽管创建直观的应用程序并不是忽略文档的一个合理理由,但它是一个重要的设计概念。现代编程语言允许实现用户友好的应用程序,因为它们负责控制底层代码如网络通信和图形显示。创建一个为终端用户提供良好体验的应用程序可以节省大量的时间。这是因为假如你正在设计一个安全的应用程序,但这并不意味着你需要具备博士学位来操作该程序。

    13.3 必要的背景知识

    Windows和UNIX平台上的文件格式模糊测试具有相同的基础,但是Windows环境中默认应用的重要性可以增加该类漏洞所造成的风险的级别。因此在创建模糊器之前,我们需要花费一些时间来研究一下该问题,以更好的理解如何来识别具有高风险的目标应用。

    13.3.1 识别目标应用

    微软Windows允许用户将默认的应用程序指定为特定的文件类型。这就增强了操作系统的可用性,因为当一个文件被双击时,它允许应用程序自动的开始运行。当为目标应用识别文件格式时,这是应当被牢记的很重要的一点。如果在应用程序中发现了一个缓冲区溢出,但该应用程序不会被用做查看一个特定的文件类型,那么此时的风险是比较小的。但如果在应用程序中发现了一个相同的漏洞,但该应用默认要打开一个常用的交易文件,那么此时就具有很大的风险。例如,考虑在JPEG图像格式中的一个溢出。象所有的图形格式一样,存在许多可用的应用程序可以来展现图像,但在一个给定的操作系统中默认只有一个与该文件类型相关联的应用程序。在Windows XP中,默认的JPEG图像查看器是Windows Picture和Fax Viewer。因此,在Windows Picture和Fax Viewer中发现溢出所带来的风险要大于使用从Download.com下载的免费图像查看器发现相同溢出的风险。为什么呢?如果一个默认的Windows应用程序被发现含有漏洞,那么立刻就会有数百万的机器受到该漏洞的影响。

    Windows Explorer

    我们如何确定在Windows环境中哪些应用程序将展现一个特定的文件类型呢?一个简单的方法是双击文件,然后查看是哪个应用程序被启动。这对于快速检查是很好的,但它不能帮助我们确定被执行以启动应用程序的准确的指令。但进行模糊测试时,这些指令对我们来说是非常重要的,因为我们需要一种方法来使目标应用自动化的连续运行。当你对几千个文件进行模糊测试时,只是快速的双击是不实际的。

    Windows Explorer是一种快速而简便的方法来识别与文件类型相关联的应用程序,并且可以识别被用来启动应用程序的命令行参数。让我们使用Explorer来证明JPEG文件是与Windows Picture和Fax Viewer相关联的。更重要的是,让我们来确定如何能够在FileFuzz中重复启动模糊化的JPEG文件以识别漏洞。

    第一步需要从WindowsExplorer的菜单中选择Tools[el]文件夹选项,然后,选择文件类型标签。

    该界面本身包含有大量的信息。拖动已注册文件类型列表框中的滑块,可以直观的看到与特定应用程序相关联的所有文件扩展名。这些文件类型可以成为很好的模糊测试目标应用,因为在默认应用中所识别的漏洞可以通过下面的方法被利用,即将文件发送给被攻击者然后确认他们双击该文件。尽管这是很重要的,但垃圾邮件证明终端用户已经习惯于点击这些文件,因此它代表了一种合理的利用漏洞的场景。

    在这时,根据所列出的标签,我们已经知道了与JPEG文件类型相关联的应用程序,但是我们不知道操作系统实际上是如何启动该应用程序的。幸运的是,我们可以通过较简单的操作就可以得到这些信息。

    当处理文件类型时,Windows包含了动作的概念。动作允许在给定唯一的命令行选项的情况下,采用一种不同的方法或使用不同的应用程序来打开文件。我们的目的是想要知道Windows是如何打开JPEG文件的,因此我们关注于打开动作。

    Windows最终揭晓了我们所寻找的秘密。在被用来执行动作域的应用程序中,我们看到Windows Picture和Fax Viewer并不是一个可执行的应用程序。实际上,它是使用rundll32.exe来启动的一个动态链接库(dynamic-link library,DLL)。在Windows Picture和FaxViewer中启动一个图像文件的完整命令行参数如下所示。

    不仅WindowsPicture和FaxViewer出乎我们的意料不是一个可执行的应用,同时我们可以看到Windows也期望提供ImageView_Fullscreen参数。如果你在命令行提示符下运行该行代码,并用一个合法的JPEG文件名来替换%1,那么你就会看到该文件象预期的那样在Windows Picture和Fax Viewer中被展现。这是一个关键的概念。如果我们能够确定在一个给定的应用程序中展现文件的适当的命令行参数的话,那么就可以使用FileFuzz来测试漏洞。我们现在将同一个命令行拷贝到FileFuzz中执行标签的应用程序和参数字段中。唯一需要做的就是将代表目标文件的%1改变为一个{0},因为这是FIleFuzz所期望的格式。有一个建议:即当遇到诸如命令行中的空格和引号时,Windows过于挑剔。因此要保证准确的复制命令,以避免以后繁琐的调试工作。

    Windows注册表

    尽管WindowsExplorer在90%的情况下都可以展示特定文件类型及与之相关联的应用程序之间的关系,但是我们还会遇到这种情况,即关联关系是存在的但在Windows Explorer中并没有显示出来。例如,考虑*.cbo文件类型。CBO文件被微软的交互式训练应用程序所使用,并默认被Windows XP的特定分布式应用所包含,例如许多Dell机器上所带有的应用。如果你的机器上包含有微软的交互式训练程序,那么你将会注意到CBO文件类型并没有被包含在Windows Explorer的文件类型列表中,在Windows Explorer中,一个CBO文件被显示为一个铅笔图标,并且当被双击时负责启动微软的交互式训练应用程序。这表明什么呢?我们如何来确定命令行参数,以启动Windows Explorer中没有的一个文件类型呢?为此,我们需要使用Windows注册表。首先,检查一下\HKEY_CLASSED_ROOT\.XXX注册表键的默认字段的值,其中xxx是相关联的文件扩展名。该值为我们提供了用来启动文件类型的应用程序名。接着,确定与同一个描述性名字相一致的HKEY_CLASSES_ROOT\注册表键。在…\shell\open\command键中,你将会发现用来启动与该文件扩展名相关联的应用程序的命令行参数的详细信息。

    13.4 FileFuzz的开发

    现在我们已经初步了解了在Windows平台上进行文件格式模糊测试的一些独特方面,下面就可以开始创建一个模糊器了。我们将FileFuzz的设计过程进行详细的分解与分析,然后,通过使用FileFuzz来识别微软安全公告MS04-028中详细描述的被广泛宣传的JPEG GDI+漏洞,来对FileFuzz的开发做出总结。

    13.4.1 开发方法

    对于创建FileFuzz而言,我们想要开发一个用户友好的且图形化的应用程序,该程序允许终端用户不需要学习一系列的命令行参数知识就可以执行模糊测试。即尽可能的开发一个简单易用的、直观的应用程序。我们的目标还包括要开发一个能够特别适应Windows平台上的文件格式模糊测试特点的工具,因此,跨平台功能并不是一个必需的特性。

    13.4.2 开发语言的选择

    针对给定的目标,我们再次选择了.Net作为开发平台。这使得我们可以非常容易的创建一个GUI前端,以节省出时间集中关注于项目的功能方面。GUI界面和所有的文件创建功能都是使用C#语言实现的。对于调试功能的实现,我们转向使用了C语言,因为它允许我们更加简便和直接的与Windows API进行交互。.Net平台能够适应此设计决策,因为它允许项目包含多个编程语言只要这些语言与.Net框架相兼容即可。

    13.4.3 设计

    我们已经对如何来设计FileFuzz进行了足够的讨论。现在可以深入到具体实现的一些特定部分了。在这里对所有的代码进行描述是不切实际的,但我们要强调一些更加重要的代码段。为了全面理解FileFuzz是如何被设计的,我们建议读者从www.fuzzing.org处下载源代码进行分析。

    创建文件

    FileFuzz需要能够适应任何Windows文件格式,因此对于二进制文件和ASCII文本文件需要采用不同的方法。Read.cs文件包含阅读有效文件的所有代码,而Write.cs文件则处理模糊文件的创建。

    从源文件中读取

    FileFuzz采用了一种强制性方法来进行模糊测试,因此我们从读取已知的源文件开始。该合理文件中的数据被读取并保存以用于随后创建模糊文件时进行变异。幸运的是,.Net框架相对来说简化了诸如从文件读取这类任务。我们在读取并创建二进制文件和ASCII文本文件时采用了不同的方法。为了实现此目的,我们利用BinaryReader类来读取二进制文件并将数据保存到一个字节数组中。读取ASCII文本文件非常类似于读取二进制文件,只是使用的是StreamReader类。另外,我们将结果保存到一个字符串变量中,而不是一个字节数组中。

    sourceArray将用于支持所读取的二进制文件的字节数组,而sourceString将被用做保存一个ASCII文本文件的内容。

    写入模糊文件

    现在文件已经被读取了,我们需要对它们进行变异并保存结果文件,以在目标应用程序中启动它们。正如所提到的,FileFuzz基于文件变异所采用的方法将文件的创建划分为如下四种类型:

    所有字节

    范围

    深度

    匹配

    我们在一个单独的Write类中对所有不同的方法进行处理,但是对构造器进行重载以处理每一个不同的场景。对于二进制文件类型,我们使用BinaryWrite类将新字节数组写入一个文件中,该文件将在执行阶段被用来对目标应用进行模糊测试。在另一方面,对于ASCII文本文件,利用StreamWrite类将字符串变量写到磁盘上。

    应用程序执行

    负责启动执行目应用程序的代码存在于Main.cs文件中,如下一个代码段所示。然而,当你查看该段代码时,会发现它相对来说很简单,这是因为该段代码本身实际上并不负责启动目标应用程序;而它实际上是负责启动内嵌的调试器,该调试器接着再处理目标应用程序的执行。稍后将对crash.exe调试器进行详细的讨论。

    首先,对Process类的一个新的实例进行初始化。在executeApp()函数中,我们利用一个循环以启动前面所创建的各个模糊文件。在循环的每次执行过程中,为要创建的进程设置属性,包括要执行的进程的名字,正如所提到的,该名字将会一直是crash.exe而不论什么被模糊化,因为命令行应用程序crash.exe接着将要启动目标应用。在这时,控制权被转交给crash.exe,并且结果最终由crash.exe通过标准输出和标准错误来返回,并显示在rtbLog超文本框中,该文本框是FileFuzz的主输出窗口。

    异常检测

    正如所提到的,FileFuzz包括以crash.exe形式存在的调试功能,它是用C语言编写的一个单机调试器,利用了内嵌在Windows API中的调试功能。同时它也利用了帮助解析代码的开源库libdasm。从列出的下一个代码段中可以看到,首先进行了一个检查以确保为crash.exe至少传递了3个参数。在FileFuzz所使用的这些参数是被进行模糊测试的应用程序的名字及其路径、在强制销毁目标应用程序之前的等待时间以及额外增加的一个命令行参数,即将被解释的模糊文件的名字。在该检查之后,将等待时间值从一个字符串类型转换为一个整型,完整的命令行参数被创建并保存到一个字符数组中。然后,使用CreateProcess命令并设置DEBUG_PROCESS标志来启动目标应用程序。

    在这时,crash.exe就可以监视并记录异常了。在下一个代码片段中,我们可以看到只要超时值没有过期,我们就可以一直监视调试事件。当遇到一个异常时,我们获得存在问题的线程的句柄并且确定所发生异常的类型。使用一个case语句来判断我们所感兴趣的三种类型的异常:内存访问违规,被0除的错误以及栈溢出。然后输出相关的调试信息以帮助终端用户确定该异常是否值得进行深入的分析。通过利用libdasm库,我们得到了发生异常的位置,恶意的操作码以及程序崩溃时寄存器的值。

    被crash.exe所识别的异常的细节信息被返回到GUI并显示给终端用户。我们希望该信息将为终端用户提供一个快速的可视化的队列以帮助识别重要的崩溃。需要进行深入研究的崩溃包括发生在恶意指令中的异常,这时寄存器包括用户控制的或用户影响的输入,并且该异常将允许shellcode获得代码执行流的控制权。

    13.5 实例研究

    现在我们已经开发了一个文件格式模糊器,我们将使用它来测试一个已知的漏洞以验证我们的设计。我们对文件格式漏洞的兴趣主要来自于微软所发布的安全公告MS04-028-"JPEG处理中的缓冲区溢出可能导致代码执行" 。该公告引起了广泛的关注,因为它清晰的表明了客户端漏洞的破坏性是多么的大。这里有一个在许多流行的客户端应用中可以利用的缓冲区溢出,它被默认安装并且影响了大量的用户。其结果是公开利用漏洞的迅速出现,尽管依赖于某些社会工程,但这使它们自己成为了攻击目标,以及不加选择的钓鱼式攻击和身份窃取。

    漏洞本身存在于gdiplus.dll库中,该库被许多应用程序所使用,包括微软的Office,Internet Explorer和Windows Explorer。JPEG格式允许内容被嵌入到图像自身中。内容的前面是0xFFFE字节序列,后面跟着一个16位的字值,该值表明了内容的总大小。该大小包括为大小所使用的两个字节和以内容自身作为结尾的头。

    FileFuzz能够识别这个漏洞吗?让我们来探查一下。我们将使用一个合理的图像文件,然后手工的或使用一个图像编辑器来添加一个注释。如果你选择为自己来复制这个例子,那么要确保你正在使用Windows的一个漏洞版本。我们的结果是使用Windows XP SP1来生成的。当创建测试文件时,我们将使用一个非常简单的图像,在这个例子中是一个1х1的白色像素。为什么用这么简单的图像呢?正如所提到的,强制性模糊测试的效率较低。我们想要关注于图像头,而不是图像本身,因此我们坚持使用一个几乎是不存在的图像。作为结果,我们以一个大小只有631字节的文件作为结尾,因此就可以在一个合理的时间帧内对所有的字节进行强制性测试。在添加了"fuzz"注释之后,当我们使用一个十六进制编辑器查看该文件时,就可以看到如下的字节序列。

    在能够开始模糊测试之前,我们想要知道在默认情况下是哪个Windows XP应用程序负责展现JPEG图像。幸运的是,我们在本章的前面已经确定了是Windows Picture和Fax Viewer负责完成此任务,它们使用下面的命令行参数来启动JPEG图像。

    在这时,我们就可以启动FileFuzz并开始模糊测试过程。FileFuzz针对JPEG文件有一个内嵌的审核器,它可以通过文件类型下拉菜单进行访问。但为了说明FileFuzz的功能特性,我们将从头开始介绍。

    我们从Create选项卡开始,设定适当的选项以生成一系列变化了的JPEG文件,而生成的基础是一个合理的并内嵌有前面生成的注释的JPEG文件。我们将Create选项卡中的所有选项设置为如下的值:

    源文件。C:\ProgramFiles\FileFuzz\Attack\test.jpg。是一个合理的JPEG文件。

    目标目录。C:\fuzz\jpg\。目标目录是所生成的模糊文件将要被保存到的目录。

    要覆盖的字节。00 x 2。根据漏洞的详细信息 ,如果我们将漏洞的长度值设置为0x00或0x01,那么就会发生溢出。注释的长度为0或1个字节是不可能的,因为两个字节的大小被包含在整个长度中。因此我们使用一个字值0x0000来模糊化该文件,以期望通过覆盖注释的大小值来触发溢出。

    范围。范围=150-170。在我们所创建的测试文件中,大小值起始于160字节.为了安全起见,我们将150到170字节之间的范围进行模糊化。

    一旦设定好所有的选项,就可以点击Create按钮来生成文件。下面来研究一下Execute选项卡。在该选项卡中我们需要告诉FileFuzz如何来启动WindowsPicture和FaxViewer。Execute选项卡中的设置值如下所示:

    应用程序。Rundll32.exe。由于WindowsPicture和FaxViewer实际上是一个DLL,因此我们所使用的应用程序是run32.exe3,它将被用来启动DLL文件。

    参数。C:\WINDOWS\system32\shimgvw.dll,ImageView_Fullscreen{0}。所传递的参数包括WindowsPicture和FaxViewer的完整的路径(shimgvw,dll),ImageView_Fullscreen参数,{0}是针对将要打开的模糊文件的名字的一个占位符。

    起始文件。150.我们所创建的第一个文件。

    结束文件。170.我们所创建的最后一个文件。

    毫秒数。2000。WindowsPicture和FaxViewer被强制关闭之前所允许运行的时间长度。

    在这时,我们就准备就绪了。当点击Execute按钮时,我们可以看到WindowsPicture和FaxViewer被重复的启动和退出。这种情况将会发生21次,因为这里有21个模糊文件被打开和解释。当执行结束时,我们看到FileFuzz已经发现了一些异常。然而,我们所感兴趣的异常发生在当160.jpg文件被启动时,如下面的示例输出所示。该异常之所引起我们的兴趣,是因为160字节是JPEG注释大小的起始位置,并且文件160.jpg已经用0x0000将初始值覆盖。

    13.6 益处和改进的余地

    FileFuzz为采用一种强制性方法来对文件格式进行模糊测试提供了基本的功能。使用其GUI界面和内嵌的调试功能,就可以通过使用一个已知的文件进行快速的审核。也就是说,存在着很大的改进余地。

    对于初学者而言,可以开发更加全面的审核功能。正如所指出的,每次只能执行一个审核。例如,当对一个二进制文件类型执行宽度方法时,每一次只能使用一个单一的字节值(如0xFFFFFFFF)对字节范围执行一次单一的扫描。如果另外一个值被使用,那么进程就必须使用新的值来重新运行。一个更加全面的审核功能将允许多个值被选择,或者是将要被测试的值的范围。可能有一些智能化的功能将被增加,一旦字节位置被识别它就首先来执行一个宽度方法然后自动的转向执行一个深度方法,以发现更多数量和更多类型的异常。

    FileFuzz被有意的设计为一个强制性模糊器,因为文件格式模糊测试使其成为了更加简单的方法。然而,这并不是说智能化的模糊测试功能不能被增加。可能除了已经存在的Create选项卡以外,还会增加一个新的Create-Intelligent选项卡,该选项卡将会成为一个Create-Brute Force选项卡。这个新的选项卡将包含一个完整的智能化模糊测试功能集,它将需要终端用户来为特定文件类型的结构开发一个模板,而不是使用一个已经存在的文件作为起点。该方法需要用户进行更多的前端工作,但同时也将允许他或她更好的识别将要进行模糊测试的文件的特定区域,并确定如何对它们进行模糊测试。模板的创建可能要在研究模板文件格式的规范文档之后来进行,但是FileFuzz也可以包含内嵌的示例模板。

    智能化的异常处理将有助于清除许多不太可能会导致漏洞条件的异常。通过嵌入在分析crash.exe的输出时所应用的规则,特定的异常可以基于以下的一些因素而被忽略,这些因素包括崩溃的内存位置处的操作码,寄存器值和栈的条件。另外一种可选的方法是,与其忽略特定的结果,不如将它们加以强调。

    简而言之,这里有着很大的改进余地。我们的目标只是简单的使该工具可以运转,而剩下的完善工作就要靠你自己了。

    13.7 小结

    文件格式漏洞在过去的几年中给微软带来了很多的麻烦。无论该漏洞是出现在多媒体文件还是文档文件中,它们都是大量存在的并且被攻击者所利用。这并不是说微软是唯一一个与该类漏洞进行斗争的提供商,只是表明微软一直是错误发掘者所感兴趣的漏洞发掘目标。我们希望随着近来对文件格式漏洞的关注程度的提高,软件提供商将会在他们的开发周期中加入模糊测试,以在生成产品之前捕获这些漏洞。

    第14章 网络协议模糊测试

    "我拥有一个木材公司吗?这对我来说是一个新消息。是否需要一些木材?"

    --George W. Bush,第二届总统论坛,St.Louis,MO,2004年10月8日

    模糊测试是威斯康星大学为UNIX工具中的命令行setuid引入随机参数值时提出的。尽管它最初是用于命令行参数,但模糊测试这个术语现在通常被认为是应用于网络协议领域,这是有合理理由的。网络协议模糊测试对安全研究者来说是最感兴趣的模糊测试类型,因为所发现的漏洞通常具有最高级别的危险程度。不需要有效的身份认证就可以到达或者不需要目标用户的任何交互就可以利用的一个远程的可利用漏洞是所发现漏洞的一个缩影,如果你将同它们进行交互那将是最好的。

    客户端漏洞,例如影响微软的InternetExplorer的漏洞,通过在创建被恶意代码感染并控制的与Internet相连的计算机网络时被利用。贪婪的渔夫撒下一张大网,期望能够捕获尽可能多的鱼。在大多数时候,这些被捕获的鱼是宽度网络上所连接的个人桌面计算机。在一个网络daemon程序中的服务器端漏洞对于创建一个被恶意代码感染并控制的与Internet相连的计算机网络同样是非常有用的,但是以这种方式来利用这些漏洞是对潜在资源的浪费。从一个攻击者的角度来看,拥有象后端数据库或企业级Web服务器这样的软件,那么就给数据的窃取提供了很大的机会,就像从一个可信的平台上执行进一步的攻击一样。在本章中,我们将介绍网络协议模糊测试,并提出一些由这种流行的模糊测试类型所带来的独特的特性和挑战。同时,我们还将研究一些网络协议漏洞。

    14.1 什么是网络协议模糊测试

    相对于其它类型的模糊测试而言,网络协议模糊测试要求识别攻击的界面,变异或生成包含错误的模糊值,然后将这些模糊值传递给一个目标应用,并监视目标应用以发现错误。非常的简单,如果你的模糊器通过某种形式的socket与其目标应用相通信,那么该模糊器就是一个网络协议模糊器。

    基于网络的模糊测试中的socket通信构件带来了一个有趣的变化,因为它对测试的吞吐量造成了一个瓶颈。将文件格式模糊测试、命令行参数模糊测试以及环境变量模糊测试的传输速度与网络协议模糊测试的传输速度进行比较,那么就像是一辆阿斯顿·马丁DB9和谢奥·米托罗进行比赛一样。

    现有的网络协议模糊器倾向于采用两种风格。其中一些采用了通用体系结构以能够对不同的协议进行模糊测试。这类工具的代表如SPIKE 和ProtoFuzz,我们在第16章"网络协议模糊测试:Windows平台上的自动化测试"中从头创建的一个模糊器也是属于这种类型。SPIKE是最广为人知的一个模糊器,在下一章中将对它进行更加详细的介绍。另外一种类型的网络协议模糊器包括哪些被设计为面向特定协议的模糊器。这种类型模糊器的例子包括ircfuzz ,dhcpfuzz 以及Infigo FTPStress模糊器 。直接从它们的名字你就可以猜出每个所提到的模糊器被设计为面向哪种协议。特定于协议的模糊器通常面向于较小的目标脚本和应用程序,但其体系结构开发的工作量却较大。在第21章"模糊测试的体系结构"中,我们将讨论相对于特定的单机工具而言,创建和使用模糊测试体系结构的价值。下面,让我们来看一些网络协议模糊测试目标的例子。

    微软感染了一个病毒

    对于世界上大多数非妄想狂和精神分裂症的人来说,新千年的到来是一个值得庆祝的时刻。我们享受了历史上最大的新年庆祝宴会,从2000年问题的惊恐中恢复了过来,并且看到世界并没有灭亡。但对于微软来说却很不幸,自从新千年到来后的几年中,在大多数主流的微软产品中发现了大量的服务器端漏洞。正如所证明的,多数问题将带来漏洞利用代码的快速开发和公开发布。另外,其中的一些漏洞将被快速扩张的恶意代码所利用,从而导致给世界范围内的公司带来实质性的金融损失,同时也给世界各地的系统管理者敲响了警钟。

    让我们来看一些著名的影响微软的蠕虫以及在世界范围内被广泛传播的漏洞。由这些蠕虫所造成的名誉上的损害是促使微软于2002年开始建立可信计算组织的直接原因之一 ,该措施从根本上改变了微软在软件开发生命周期中解决安全问题的方法。

    Code Red。在一个IIS Web服务器,即Internet服务器应用程序编程接口(Internet Server Application Programming Interface,ISAPI)扩展中的一个缓冲区溢出,于2001年6月18日被发现 。尽管针对该问题的补丁的可用性持续了将近一个月,但在同年的7月13日又发现了一个蠕虫,它利用该缺陷并使用消息"HELLO!Welcome tohttp://www.worm.com!Hacked By Chinese! "来破坏含有漏洞的服务器上的Web站点。在感染之后,该蠕虫将休眠20到27天,然后将试图对不同的固定的IP地址发动一个DoS攻击,这其中包括whitehouse.gov的IP地址。

    Slammer。SQL Slammer蠕虫利用了两个单独的漏洞,它们分别被微软的安全公告MS02-039 和MS02-061 在微软的SQL Server和桌面引擎中所识别。当该蠕虫在2003年1月25日第一次出现时,它用了大概10分钟的时间感染了最初的75000个受攻击者 。该蠕虫所利用的缓冲区溢出漏洞(MS02-039)已被微软在6个月之前发现并添加了补丁,但仍然有大量的含有漏洞的服务器在使用。

    Blaster。2003年8月11日,18岁的JeffreyLee Parson将他所制造的蠕虫传播给了全世界,该蠕虫 利用了在WindowsXP和Windows2000中的一个DCOM远程过程调用(RemoteProcedure Call,RPC)的缓冲区溢出 。同样,该问题的补丁也是早已经就可用的。该蠕虫可以通过一个SYN的溢出对windowsupdate.com执行一个分布式的DoS攻击。由于制造并传播了该蠕虫,Parson被判了18个月的监禁,3年保释下的监管以及100小时的社团劳动服务 。

    以上所列出的漏洞还很不全面,但它说明了历史上一些重要的、存在于微软的服务器端的漏洞,而该漏洞可能导致生成快速传播的蠕虫。任何网络漏洞都可以通过网络模糊测试来潜在的发现。

    14.2 目标应用

    这里有成百上千的目标应用可以选择,它们都可以暴露出远程可利用的网络协议解析漏洞。在表14.1中,我们列出了已知的网络漏洞的一小部分示例,以强调一些常用的目标分类。

    表14.1 含有漏洞的应用程序的常用类型和以前所发现的漏洞的示例

    应用程序分类

    漏洞名称

    参考信息

    邮件服务器

    Sendmail远程信号

    处理漏洞

    http://xforce.iss.net/xforce

    /alerts/id/216

    数据库服务器

    MySQL绕过验

    证漏洞

    http://archives.neohapsis.com

    /archives/vulnwatch/2004
    -q3/0001.html

    基于RPC的服务

    RPC DCOM缓冲区

    溢出漏洞

    http://www.microsoft.com
    /technet/security/bulletin/
    MS03-026.mspx

    远程访问服务

    OpenSSH远程挑

    战漏洞

    http://bvlive01.iss.net/issEn/
    delivery/xforce/alertdetail.
    jsp?oid=20584

    多媒体服务器

    RealServer../

    DESCRIBE漏洞

    http://www.service.real.
    com/help/faq/security/
    rootexploit082203.html

    备份服务器

    CA BrightStor

    ARCserve备份

    消息引擎缓冲

    区溢出漏洞

    http://www.zerodayinitiative

    .com/advisories/ZDI-07-003.html

    这六个类别构成了目标应用的很好的集合。然而,任何接受进入连接的应用程序或服务都可以作为一个潜在的目标应用。这包括硬件设备、网络打印机、PDA以及蜂窝电话等。任何具备接受网络通信功能的软件都可以使用网络模糊测试技术来进行审核。

    为了对典型的目标应用有一个更加深入的了解,我们对开放系统互连基础参考模型(Open Systems Interconnection,OSI模型) 中所定义的7层结构的每一层都提供了示例,OSI模型如图14.1所示。尽管在任何单一的网络技术中没有被直接的实现,但当剖析网络技术以说明特定的功能应当被放在哪一层时,OSI模型通常被作为一个参考点来使用。从理论上来讲,网络协议模糊测试可以面向7层OSI模型中的任何一层。在实际测试中,你将会发现模糊器面向除了第一层物理层之外的所有层。由于每一层都被提出,因此下面就提供了与各层相关的一个历史上的漏洞。

    14.2.1 数据链路层

    与数据链路层相关的技术包括以太网框架和802.11框架。该层的漏洞是很有趣的,因为对此低层次网络层的处理通常是在操作系统内核中实现的。最近,该层中的一个漏洞是被Mitre在CVE-2006-3507中识别的 。在该漏洞中,一个攻击者可以危及一个机场中支持无线的Mac OS系统的安全,因为存在有多个基于栈的缓冲区溢出漏洞。该溢出发生在内核的环境中,可以被利用以彻底的破坏被影响的系统。这里存在的一个有趣的需求是,攻击者必须要位于他或她所面向的无线网络的范围之内。如果你曾经在咖啡店中连接过公共无线网,并且你的Mac OS系统曾经发生过故障,那么你可能会谨慎的查看房间并确认网络环境。

    APPLEGATE

    2006年,在拉斯维加斯的Black Hat简报中,当安全问题研究者Jon"Johnny Cache"Ellch和David Maynor发布了一段录像,以说明通过一个设想的无线网络驱动器中的漏洞对Apple Macbook所造成的远程破坏时,引起了很大的争论 。接下来所发生的媒体的狂暴行为包括从所有卷入的团体中大量的揭发隐私,也包括Apple的指控即研究者从没有将该漏洞的足够多的详细信息进行共享,以允许他们来证实该声明。Apple的拥护者也加入到了这场争论中,有情形表明该漏洞利用了一个第三方的驱动程序,而不是Apple的开发者所编写的程序。

    最后,Apple发布了一系列的补丁,包括CVE-2006-3507,但继续主张该漏洞来自于BlackHat说明所引发的内部审核。发现此缺陷的荣誉始终没有被授予Maynor和Ellch。在这个过度戏剧化的一系列连锁事件中,最有可能的是我们将可能永远不会知道事情的真相。

    14.2.2 网络层

    第3层即网络层,它包括IP协议和Internet控制消息协议(InternetControl Message Protocol,ICMP)。尽管TCP/IP协议最为常见的实现已经在前些年经过了很好的测试,但是漏洞仍然可以在该层被发现。同时值得一提的是,Windows Vista包含一个完全被重写的网络栈,这表明它可能是模糊测试的一个很好的目标。TCP/IP协议中的最近被发现的一个漏洞被描述在MS06-032中,即"TCP/IP中的漏洞可以允许远程代码执行" 。内核中出现的漏洞是由于不正确的解析IPv4源路由选项而引起的。这个不正确的处理导致了一个缓冲区溢出,而该溢出可以被远程攻击所利用以获得对内核级的访问。

    14.2.3 传输层

    再上面的一层是第4层即传输层,该层包括TCP和UDP。正如前面所提到的,大多数的TCP/IP实现已经经过了很好的测试,但过去该层仍然存在有问题。该层漏洞的一个突出的例子是陈旧的利用带外TCP包的"Winnuke"攻击 。可以证明,Winnuke攻击是到目前为止最简单的远程内核DoS攻击。远程的使一个受影响的系统崩溃所需要做的所有工作就是使用TCP紧急指针集来传输一个TCP包。该指针可以使用任意socket API来简单的设置,方法是通过简单的将包指定为包含带外数据。

    14.2.4 会话层

    OSI模型中的第5层即会话层,该层包含两个非常特别的协议,这两个协议都实现了远程过程调用。这些技术是DCE/RPC(微软的MSRPC)和ONC RPC,后者也被称为Sun RPC。这两个协议分别是针对Windows和UNIX系统而实现的。在过去,通过使用这些技术发现了大量严重的漏洞。其中最为严重的一个漏洞是在微软的安全公告MS04-011 中被添加了补丁,该漏洞被Sasser 蠕虫使用而得到了疯狂的传播。该漏洞存在于二进制文件lsass.exe中,该文件暴露了默认在所有当前版本的Windows操作系统中已被注册的RPC端点。特别的,该漏洞的产生是由于DaRolerUpgradeDownLevelServer函数中的一个缓冲区溢出,并且该漏洞可以被远程利用。

    14.2.5 表示层

    在第6层所采用的技术中,使其更加有利于进行模糊测试的一个很好的例子是在Sun RPC中所使用的外部数据表示(eXternal Data Representation)XDR。在以前所发现的与XDR相关的漏洞中,一个非常好的例子是Neel Mehta所发现的xdr_array整数溢出 。该发现的核心是对一个整数溢出的滥用。如果一个攻击者为一个数组指定了大量的实体,那么可能分配了一个不够大的缓冲区,然后随着数据的写入而发生了溢出。攻击者可以利用这个内存破坏来危及底层系统的安全。

    14.2.6 应用层

    第7层是应用层,它对于网络协议模糊测试而言,是最常被想起和作为测试目标的OSI层。该层包括许多常用的协议,如FTP,SMTP,HTTP,DNS以及许多其它标准的私有的协议。历史上,在该层被发现的漏洞要多于在其它层被发现的漏洞。第7层是经常被模糊测试的层,因为对常用的协议而言,这里有许多不同的实现。

    本章的剩余部分重点关注于第7层,但是应当记住,任何层上的软件都会存在实现问题,并且在对一个目标执行彻底的审核时不应当忽略这些软件。

    14.3 测试方法

    对大多数层而言,可用的模糊测试方法与第11章"文件格式模糊测试"所提出的方法是相同的。从高层次而言,我们可以使用强制性或智能的方法来进行文件格式模糊测试和网络模糊测试。然而,当应用于基于网络的模糊测试时,这些方法的使用上存在着一些不同。

    14.3.1 强制性或基于变异的模糊测试

    在文件格式模糊测试的环境下,强制性模糊测试要求模糊测试者获得目标文件格式的有效示例。然后模糊器采用不同的方法将这些文件进行变异,并将每个测试用例提供给一个目标应用。在网络模糊测试的环境下,模糊测试者通常使用一个嗅探器来捕获有效的协议通信,既可以在运行之前静态的捕获也可以在运行时动态的捕获。然后模糊器将所捕获的数据进行变异,并在目标应用上进行使用。当然,情况并不总是这么简单。例如,考虑一下实现了对基本的重复攻击进行防护的任何协议。在这种情况下,简单的强有力模糊测试除了对诸如验证进程的会话初始化代码之外,将不会对其它任何代码有效。

    一个简单的基于变异的模糊器应用失败的另外一个例子是当其试图对嵌入到校验和中的协议进行模糊测试时。除非校验和字段被模糊器动态的更新,否则所传输的数据在执行任何深度解析之前将被目标软件所丢弃。在这种情况下,测试用例就被浪费了。基于变异的模糊测试在文件模糊测试领域应用得很好,但在通常情况下,下一个模糊测试方法在网络协议模糊测试中应用的更好。

    14.3.2 智能强制性模糊测试和基于生成的模糊测试

    对于智能强制性模糊测试而言,你必须要首先花费一些时间来实际的研究一下协议的规范。一个智能模糊器仍然是一个模糊测试引擎,因此仍然会执行一个强制性攻击。然而,这要依赖于用户的配置文件,以使测试过程更加智能化。这些文件通常包含描述协议语言的元数据。

    当对一个不重要的网络协议如FTP中的控制通道进行模糊测试时,智能模糊测试通常是最佳的方法。描述每个协议动词(USER,PASS,CWD等)的一个简单的语法以及对每个动词参数的数据类型的描述,将允许进行一个非常彻底的测试。基于生成的模糊器的开发可以构建在一个已经存在的框架之上,如PEACH ,或者从头开始创建。在后一种情况下,所开发的特定于某种协议的模糊器在大多数情况下不能被应用于测试其它的协议。使用这种方法,通常的设计思想被抛弃,从而简化了测试过程的设计和实现。

    这里有许多公开的、可用的面向一个单一协议的测试工具,但是许多测试工具仍然是私有的,因为它们实质上可以看做是一个被加载的工具。这就是说,当一个开发者发布一个通用的模糊器时,无论它是基于变异的还是基于生成的,终端用户都需要进行一些研究来发现漏洞。使用一个特定于某种协议的模糊器,那么需要做的所有工作就是选择一个测试目标。如果终端用户使用该模糊器来测试与开发者所测试的相同的目标,那么将会有相同的错误被发现,这时,工具对于最初的开发者而言将具有不高的价值。如果发现了从来没有被该模糊器测试过的目标,并且该目标使用相同的协议,那么情况将不会是这样。发布这样一个工具的决定要归结于对规范的个人理解以及错误消除和共享研究的想法。

    14.3.3 修改的客户端变异模糊测试

    对模糊测试常见的一个批评是随着所开发的模糊器的复杂性的增加,其复杂性开始接近于开发一个完整客户端的复杂性。因此为什么不降低其复杂性呢?有一个前面没有提到过的特殊的模糊测试类型,该方法是这样一种技术,即客户端-服务器通信中的客户端源代码(当可用时)被修改以创建一个模糊器。实际上,该方法并不是在一个模糊器中实现一个完整的协议,而是该模糊器被嵌入到一个已经使用了所希望的语言的应用程序中。这样做是具有优越性的,因为模糊器可以访问已经存在的、生成有效的协议数据所需要的程序,并且将模糊器的开发者的工作量减少到最小。

    尽管我们不知道有采用这种方法的任何公开发布的模糊测试工具,但是已经按照这种方式进行了一些开发工作。其中的一个例子是由GOBBLES所编写的sshutuptheo OpenSSH漏洞利用工具,它利用了主流SSH服务器中的一个验证前整数溢出。

    这种方法特别适应于类似于SSH这样的复杂协议。然而,开发者必须要意识到客户端可能会具有的、将会影响代码覆盖的任何限制条件。例如,如果选择了SSH,并且所选择的客户端只支持版本1的SSH,那么对版本2的SSH的细节将不会有代码覆盖。当然,它所具有的另外一个缺点是一个合理的客户端可能会被进行大量的修改以覆盖许多测试用例。

    14.4 错误检测

    错误检测是模糊测试过程中的一个关键组成部分,因此第24章"智能故障检测"专门对此进行了讨论。尽管在该章之前不会涉及到高级的错误检测方法,但我们可以来看一些应用于网络协议模糊测试的基本技术。在网络模糊测试过程中检测错误的难度完全依赖于测试目标。例如,考虑一个网络daemon程序发生崩溃, 并且在任意错误发生之后停止接收连接。很明显,这从基本的错误检测的观点来看是一个很重要的行为。当服务器发生崩溃时,简单的假定是最后一个测试用例导致了它崩溃。这可以完全由模糊器来完成,而不需要在目标机器上的软件代理或人工评审的任何帮助。

    让我们考虑目标应用能够处理错误、或者生成一个单独进程以及独立维护监听socket的另外一个场景。在理论上,我们可以触发一个可利用的漏洞,当然并不知道我们在目标机器上应用了某种形式的深度评审。可以安全的假设在大多数模糊测试过程中,我们可以访问目标机器。下面来看一些我们可以用来在目标机器上检测错误的基本的方法。

    14.4.1 人工方法(基于调试器)

    在可以对一个机器进行本地访问时,监视异常最简单的方法就是将一个调试器关联到一个进程。该调试器可以检测到何时发生了一个异常,并允许用户决定采取什么动作。使用Ollydbg,Windgb,IDA和GDB都可以实现这种方法。这里所存在的问题是,哪个测试用例或测试引起的序列导致了该行为。

    14.4.2 自动化方法(基于代理)

    考虑设计一种方法,以替代人工的调试过程。不是使用一个调试器应用,模糊测试者编写了一个特定于运行在目标机器上的目标平台的调试代理。该代理有两项任务。第一项工作就是在目标进程中监视异常。第二项工作是同远程系统中的模糊器进行通信。这允许将数据和错误检测关联起来。该方法的缺点是开发者需要为将被测试的每个平台创建一个代理。同样,这个概念将在第24章中进行详细的讨论。

    14.4.3 其它方法

    尽管当执行网络模糊测试时,调试器可能会成为检测异常的最有价值的工具,但是不要忽略其它可用的方法。应用程序和操作系统日志可能会提供有关已发生的问题的信息,这里的挑战是将问题与可靠的模糊测试用例进行关联。同时,要确保对系统的性能降低进行监视,因为这可能表明了某些隐藏的问题。性能下降可以包括增加的CPU利用率或者内存的耗尽。导致一个无限循环的测试用例可能永远不会触发一个异常,但同时会导致一个DoS。其中的要点很简单:尽管调试器对于发现漏洞而言是奇特的工具,但是你也不能忽略其它的方法。

    14.5 小结

    网络协议模糊测试可能是最广为人知和最广泛被利用的模糊测试类别,并且有许多不同的方法来实现网络协议模糊测试。它的流行是受一些因素的共同影响,并不只是因为它可以发现高风险、远程的验证前漏洞。除此之外,它是一种成熟的模糊测试类型,因为存在许多的公开的可用的工具可以帮助安全问题研究者。在了解了网络模糊测试的一些常用方法之后,就可以讨论基于网络的模糊器的具体实现了。

    第15章 网络协议模糊测试:UNIX平台上的自动化测试

    "我认为我们已经达成一致,过去已经结束。"

    --George W. Bush,会见John McCain时的讲话,达拉斯早间新闻,2000年5月10日

    尽管微软的Windows在桌面操作系统领域居于统治地位,但UNIX系统仍然在服务器平台的选择中占据着大部分市场。例如Web服务器Apache,广泛的运行于UNIX系统上,并且根据最新的NetCraft调查 ,它在微软所有的IIS产品中保持着将近30分的领先优势。影响主流UNIX系统的漏洞都非常重要,并且具有深远的影响。Internet上有许多产品运行于基于UNIX的DNS,邮件和Web服务。例如,考虑一个在伯克利的Internet域名(Berkeley Internet Name Domain,BIND)服务器中所发现的缺陷,它可以被利用以破坏大部分的Internet通信。尽管无法确切的知道以前所发现的漏洞有百分之多少是通过模糊测试发现的,但可以肯定的是有许多的漏洞是在一个模糊器的帮助下才发现的。

    在本章中,我们并不从头开发一个定制的基于UNIX的模糊器。相反,我们将提出并利用由SPIKE模糊测试框架提供的可用的脚本编写接口,SPIKE是在本书中经常被提到的一个开源模糊器。由于在本章中,我们不关注于模糊测试框架开发的实际编码问题,我们将自始至终的使用SPIKE来对一个非开放源代码的应用的模糊测试进行一次走查。

    15.1 使用SPIKE进行模糊测试

    为了说明使用SPIKE进行模糊测试的过程,让我们从头到尾的分析一个完整的例子,首先要选择一个测试目标,研究目标协议,在一个SPIKE脚本中描述该协议,然后设置该模糊器开始工作。

    15.1.1 选择测试目标

    为了我们要达到的目的,这里选择一个具有下列所描述特性的目标软件:

    目标软件应当被普遍的使用。

    目标软件应当可以通过演示或评估来方便的获得。

    目标软件应当使用一个公开的或易于被逆向工程的协议。

    这里存在着许多可能的目标软件,但是出于此例子的目的,我们选择了Novell NetMail ,它是一个实现了许多公开协议的企业级邮件和日历系统,并且满足我们需要的所有准则。特别的,我们面向的是NetMail网络消息应用协议(Networked Messaging Application Protocol,NMAP)。NMAP被没有被普遍存在的网络扫描和侦测工具Nmap 所扰乱。那么NetMail NMAPM究竟是什么意思呢?我们并不知道准确的定义,因此要感谢Novell热心的共享了下面的定义:

    缩写NMAP代表网络消息应用协议。它是一个基于文本的IP协议,由Internet Assigned Numbers Authority(IANA)在端口689所注册,并使用NIMS代理来进行通信。当与NDS eDirectory的分布式特性相结合时,该协议就允许NIMS代理运行在不同的服务器上(甚至是不同的平台)来进行操作,就好像它们是在同一个服务器上一样。当消息服务的需求增加时,不是用一个更大的服务器来替换该服务器,NMAP允许额外的服务器被添加到这个"簇"中。每一个版本的NIMS都提供了NMAP协议的RFC类型的文档 。

    NMAP看起来象是一个构建在TCP之上的、Novell所开发的基于文本的私有协议。将一个私有协议作为测试目标既有优点也有缺点。其不足的一面是,我们所开发的模糊器在测试该协议之外似乎没有其它的用处。然而有利的一面是,一个自定义的协议意味着长期存在的、经过试验的真实的解析库是不可用的。协议提供商必须要自己编写协议解析器,较少的人评审过相关代码,因此它可能会包含大量的漏洞。

    Novell是很热心的,它通过其Web站点提供了NetMail的免费90天的试用版 ,因此在实验室环境中下载并安装目标软件是非常容易的。在安装之后,我们使用Novell提供的补丁来更新软件。这是一个关键的步骤以确保我们不会重复发现旧的错误(如果发现了一些)。指出你在一次审核中将该步骤错过了一个星期,那么将是相当沮丧的。下一步就是要研究NMAP协议的相关细节。

    15.1.2  协议逆向工程

    在我们可以描述针对SPIKE的NMAP协议之前,显然需要先将其理解透彻。有许多方法可以处理该步骤。可能最明显的方法就是在我们的实验室环境中监视合理的NMAP通信。该方法实际上要比你所想象的要复杂一些。当处理一些复杂的企业级软件时,有时候生成你所寻找的通信是很困难的。另外一种方法是基于其它的工作进行构建。可以通过Google查询一下其他人对任意给定的协议了解的如何。查看在开源Wireshark(以前的Ethereal)嗅探器中是否存在ygie协议解码器也是一个不错的方法。跳转到Wireshark 版本控制库,特别是epan\dissectors 目录中,并且查看是否能够找到你的协议。

    另外一个也是最少被使用的方法就是简单的与NMAP daemon程序本身进行通信,以查看它是否为我们提供任何信息。为此,我们需要首先确定应用程序使用哪个端口与客户端进行通信。假定文档中并没有公开的说明NMAP绑定到TCP的689端口,那么我们可以在微软TCPView (以前的Sys Internals)工具的帮助下手工的得到此信息。简单的启动TCPView程序,就会立即发现nmapd.exe在TCP的689端口进行监听。我们可以使用一个基本的TCP连接工具如netcat 或者甚至是Windows telnet命令来连接到daemon程序。采用一个大胆的猜想,我们发出HELP命令,然后欣喜而又惊讶的看到,它为我们提供了daemon将接受的命令的一个列表。

    前一段所进行的工作并不差。利用我们所收集的信息再往前进一步,即把nmapd.exe的二进制文件加载到我们的解析器IDA Pro中。按Shift+F12键以浏览图的字符串数据库。然后我们就可以定位在该图中所显示的有用响应的位置。即以ASCII文本"1000"开始的字符串。现在,我们依据字符串的内容而不是其地址对字符串数据库进行分类,然后滑动到以"1000"开始的行。在这里,我们发现了被NMAP服务器所支持的所有命令及其期望语法。

    在评审了不同的命令及其所描述的语法之后,针对有用输出的符号变得清晰起来。尽管命令的某些参数与其方法是不一致的,但在大多数情况下使用了如下的原型:

    <argument>。这种类型的参数是必需的。如果它没有被指定,那么命令将执行失败。

    [argument]。这种类型的参数是可选的。

    {CONSTANT1|CONSTANT2|CONSTANT3}。这种类型的参数是必需的,并且必须为后面的命令从一组常量字符串中选取。每个可能的取值用字符|分隔。

    同时,任何没有包含在角括号中的字符串应当被作为一个文字字符串来处理。包含在角括号中的任何字符串应当被作为一个变量来处理。这些规则对于存在语法描述嵌套的命令同样适用。

    该原型指定了这个命令的后面将总是跟着字符串值SYS或USER。由于变量Username存在于字符串USER后面的角括号中,因此它是必需的,但只有当字符串USER被选择时才如此。如果字符串SYS被选择,那么就不需要Username参数。最后,作为该命令的最后一个参数,变量Password总是必需的。PASS命令的协议可能是这个协议中最复杂的命令,但并没有太多的关于此的评论。因此,我们选择了一个非常友好的协议进行模糊测试。

    由于我们所选取的示例命令是处理验证的,因此我们可以立刻猜到该daemon程序至少需要为其某些功能进行验证。模糊测试者可能对此感兴趣,也可能对此不感兴趣,这要依赖于测试的范围。例如,对于一个小的研究团队,他们为了满足最终的开发期限只对应用的关键状态进行了测试(预验证),那么后验证命令就很可能被忽略。然而,对于一个关注于应用程序所有状态的完整测试而言,当模糊器进行完预验证状态之后,它需要确保执行了正确的验证。

    假定我们对后验证状态的模糊测试感兴趣,我们必须确定如何成功的连接到服务器。我们可以看到有一些不同的方法可以连接到服务器。一个用户可以在FTP协议中分别使用USER和PASS命令来连接到服务器。验证的第三种类型允许其它的NMAP代理使用PASS命令中的SYS标识来验证它们自身,而不是使用USER标识。

    通过查看我们使用IDA发现的其它命令及其便捷的详细的描述,我们就可以理解该协议的工作方式。在最低层的公共命名中,每个命令都以一个单独的ASCII字符串开始,即我们所指的verb。这就告诉了daemon程序我们需要采取哪种动作。对每个命令而言,下一个构成部分就是一个空格分隔符。在空格之后,每个命令需要一个新行(如果动词不需要任何参数)或者特定于动词的参数。该参数的格式可以从我们在IDA中定位的字符串中得到。

    了解了关于NMAP协议的这些基本知识以后,我们就可以开始构建我们的SPIKE NMAP模糊器了。

    15.2 SPIKE 101

    在第21章"模糊测试框架"中所介绍的其它可用的模糊测试体系结构中,简要的涉及了SPIKE。但在这里,我们将对其进行深入的分析。由于我们将使用SPIKE来对该协议进行模糊测试,因此下面的学习是很重要的,即熟悉SPIKE的模糊测试引擎的工作机理及其通用的、可编脚本的、基于行的TCP模糊器的工作机理。

    15.2.1 模糊测试引擎

    SPIKE通过迭代变量和模糊字符串来进行工作。考虑一下作为协议当中的字段的变量,如用户名、密码和命令等。这些变量及其在数据流中的位置将是特定于每个目标的。模糊字符串是那些能够潜在的触发一个错误的不同的字符串和二进制数据的库,基于以往导致其它软件发生问题的特定序列的经验来仔细的选择模糊字符串库中的元素。例如,考虑一个非常基本的模糊字符串,如一个包含64000个连续A字符的ASCII字符串。该模糊字符串可能会替换协议中的一个用户名变量,从而触发一个预验证缓冲区溢出。记住术语模糊"字符串"有一些令人误解。一个模糊字符串可以是任意的数据类型,甚至是类似于二进制数据的一个XDR编码的数组。

    15.2.2 通用的基于行的TCP模糊器

    我们这里所要创建的模糊器被称为通用的基于行的TCP模糊器。该应用的代码存在于SPIKE源代码包中的line_send_tcp.c文件中。该模糊器是关于SPIKE的一个非常简单而又功能强大的模糊器。它将处理一个SPIKE脚本,将该脚本中的每个变量进行模糊化,并试图为每一次新的模糊测试重复连接到主机。这些变量根据它们在模糊脚本中的位置来进行模糊化。这意味着在大多数带有验证的协议中,与验证有关的信息将首先被SPIKE测试。

    脚本语言所采用的实现方法允许用户编写脚本以直接访问SPIKE API函数。为了提供关于如何编写脚本的基础知识,下面首先列出了可能会调用的一些函数:

    s_string(char * instring)。该函数将向SPIKE添加一个固定的字符串。该字符串将永远不会改变。

    s_string_variable(unsigned char *variable)。该函数将向SPIKE添加一个变量字符串。当该变量被处理时,这个字符串将被模糊字符串所替代。

    s_binary(char * instring)。向SPIKE添加二进制数据。该数据将永远不会被改变。

    s_xdr_string(unsigned cahr *astring)。该函数将向SPIKE添加一个XDR类型的字符串。也就是说,它将包含一个4字节长度的标签,并将用0来扩展4倍长度。该字符串将永远不会改变。

    s_int_variable(int defaultvalue, int type)。该函数将向SPIKE添加一个整数。为了调用s_int_variable(),类型值应取如下之一:

    Binary Big Endian。最高有效位元(Most significant bit,MSB)整数,4字节。

    ASCII。标记十进制数的一个ASCII格式。

    One Byte。一个字节整数。

    Binary Little Endian HalfWord。最低有效位元(Least significant bit,LSB)位整数,2字节。

    Binary Big Endian HalfWord。MSB整数,2字节。

    Zero X ASCII Hex。以0x开头的ASCII格式的十六进制数。

    ASCII Hex。一个ASCII格式的十六进制数。

    ASCII Unsigned。一个ASCII格式的无符号十进制数。

    Intel Endian Word。LSB整数,4字节。

    由于当我们将SPIKE作为一个编写脚本的主机使用时,并没有使用一个C预处理器,因此我们需要知道该类型的整数值。

    到现在为止所讨论的SPIKE的功能对于开发一个NMAP模糊器而言已经是足够的了。然而,让我们再来研究一下SPIKE在模糊测试领域的主要功能,基于块的协议表达。

    15.3 基于块的协议建模

    尽管我们选择了一个简单的基于文本的协议来进行测试,但是SPIKE通过使用其基于块的模糊测试功能可以支持更加复杂的协议。使用基于块的模糊测试允许动态的创建具有有效字段长度的包。例如,考虑带有包结构的一个协议,该结构的前缀是一个用户名字段和所指定的用户名的长度。我们希望当对用户名进行模糊测试时,模糊器可以自动的更新该长度字段。这就确保了模糊字符串被正确的传递给了用户名解析器。s_block_start()和s_block_end()函数允许你对这样的协议进行建模。

    为了有效的利用这些函数,你只需要在字段被度量之前和之后进行声明。然后,当实际长度必须要被插入到数据流中时,使用若干个blocksize函数中的一个。它们类似于前面所讨论的整数字段,并且被详细的定义。注意到这些blocksizes中的一些是变量,这意味着将使用无效的blocksizes来执行一系列的模糊测试。对长度字段是否进行模糊测试要取决于你所做的决定。下面列出了在SPIKE中可以使用的所有不同类型的blocksizes的一个较完整列表。

    15.4 SPIKE的额外特性

    SPIKE不只是一个模糊器,它实际上是一个完整的模糊测试框架,该框架包含了大量有用函数的API,可以帮助我们简化定制模糊器的创建。它还包含许多特定于协议以及特定于应用程序的模糊器和模糊测试脚本。下面是除了SPIKE引擎和通信代码之外,SPIKE所提供的一些其它功能。

    15.4.1 特定于协议的模糊器

    SPIKE包含一小部分以前所编写的特定于协议的模糊器,如下所示:

    HTTP模糊器

    微软RPC模糊器

    X11模糊器

    Citrix模糊器

    Sun RPC模糊器

    在大多数情况下,这些模糊器只是作为如何使用SPIKE的示例来使用。这是因为它们已经存在了一些时间,并且在你可能感兴趣的每个目标中都运行良好。

    15.4.2 特定于协议的模糊测试脚本

    同时包含在SPIKE中的还有一些脚本,这些脚本可以插入到SPIKE所包含的许多通用模糊器的某个当中。这些脚本如下所示:

    CIFS

    FTP

    H.323

    IMAP

    Oracle

    Microsoft SQL

    PPTP

    SMTP

    SSL

    POP3

    15.4.3 通用的基于脚本的模糊器

    正如前面所提到的,在SPIKE中有许多采用脚本作为输入的通用模糊器。下面的通用模糊器可以在SPIKE中被找到:

    TCP监听(客户端)模糊器

    TCP/UDP发送模糊器

    行缓冲的TCP发送模糊器

    15.5 编写SPIKE NMAP模糊器脚本

    再回到我们的NetMail目标应用,下面所列出的SPIKE示例脚本完全是根据HELP输出消息以及来自于IDA Pro的字符串列表来创建的。该脚本首先对实际验证命令本身进行了模糊测试,以寻找可能的预验证错误。如果提供了一个正确的用户名和密码,那么该模糊脚本将继续对后验证命令进行模糊测试。

    接着,我们将所选择的一个调试器关联到目标系统的NMAP进程,并通过SPIKE运行我们所开发的脚本。我们使用SPIKE的通用的基于行的TCP模糊器,并使用下面的命令行选项来执行名为nmap.spk的脚本:

    在执行一小段时间之后,我们的模糊器就发现了一个可利用的栈溢出。

    右上角的窗格显示了程序崩溃时的寄存器值。寄存器EBP(栈结构指针),EBX,ESI,EDI以及更重要的EIP(指令指针)都被十六进制值0x41或者ASCII字符A所改写。右下角的窗格显示了栈结构,我们可以清楚的看到它已经被一个由A组成的长字符串所改写。左上角的窗格中通常显示被执行的指令的列表,但由于当前指令指针处于地址0x41414141,因此该窗格是空的。没有内存页被映射到该地址,因此在解析窗口中没有显示任何内容。接下来,我们必须要查到是哪个动词-参数对导致了该崩溃,这样我们就可以将它再现,甚至可以利用它。

    有许多方法可以做到这一点。其中最简单的一个方法是利用SPIKE中内嵌的一个特性。如果SPIKE不能连接到目标,那么它通常会崩溃。通过检查SPIKE崩溃之前的最后一个输出,我们就可以确定是哪个测试用例导致了NetMail NMAP中的崩溃。然而,由于我们和一个调试器相关联,因此目标进程将不会崩溃,而是挂起,并且SPIKE的崩溃特性将不会出现。因此,为了能够重现崩溃,我们重新启动该进程,允许它在没有监视的情况下运行(不关联到一个调试器),并依赖于SPIKE崩溃以及受影响的NMAP daemon程序。

    该方法对于追查触发NMAP中错误的测试用例而言,无疑是最基本的一种技术。更加科学的方法包括使用一个嗅探器来监视所有的网络通信。NMAP将崩溃,并且不再能够对请求做出响应。但在另一方面,SPIKE将继续传输测试用例。通过定位最后一次请求响应的传输,我们就可以确定是哪个测试用例可能触发了该错误。

    回到最初所描述的向后追查的技术,来自于SPIKE的最后一次成功的传输标记在"模糊变量5:1"行中。这告诉我们最后一次成功的连接是关于模糊变量5。发送给该进程的最后一个模糊字符串是1。为了确定哪一个模糊变量是模糊变量5,我们只要简单的加载我们的的SPIKE脚本,然后从第0行开始统计包含有"变量"这一词的行的个数。在遇到CREA命令中的只能用于后验证的一个动词参数时停止计数。接下来,我们必须要确定为模糊字符串1即CREA命令的参数使用了什么值。

    有许多方法可以实现这一点。一种方法是如前面所提到的使用一个嗅探器。另外一种可以采用的方法是向模糊测试程序line_send_tcp.c中添加一个printf()函数,以命令应用程序输出当前的模糊字符串,然后再重新运行模糊器。不论使用哪一种方法,我们都发现导致错误的字符串就是"CREA<longstring>"。我们立刻就可以重新创建这个后验证崩溃。需要做的所有工作就是让用户连接到服务器并发送一个恶意的CREA命令参数。这并不很困难:为发现远程漏洞只要花费很少的时间。这可能会使你产生这样的疑问,为什么许多提供商在正式发布他们的软件之前不应用这种类型的测试呢?如果我们想要继续对NMAP进行模糊测试,那么可以在关于CREA命令的SPIKE脚本中删除此部分,以使我们不再用一个已知的问题来破坏测试目标。

    15.6 小结

    当考虑编写你自己的模糊器时,首先对已经存在的模糊器以及模糊测试的体系结构进行评估是非常重要的。对于一个简单的测试目标如NMAP协议而言,从头开发一个模糊器将会得到很小的收获。只要花费几个小时的时间,我们就可以掌握一些SPIKE脚本,以有效的执行NMAP daemon代码。但是要记住,你所得到的结果的质量将反映出开发模糊器所花费的时间。在这种特殊的情况下,我们追求的是绝对的最小化。我们的选择是连接到服务器,发出一些命令请求,然后退出。在NMAP中还可以发现许多其它的函数可以被添加到脚本中,以进行更多的测试。

    第16章 网络协议模糊测试:Windows平台上的自动化测试

    "我无法想象"Osama bin Laden"等人如何理解光明节的乐趣。"

    --George W. Bush,白宫烛台点火仪式,Washington DC,2001年12月10日

    尽管UNIX系统在服务器领域可能居于统治地位,但在全世界范围内所安装的更多的操作系统是微软的Windows操作系统,这也使它成为了易受攻击的目标。影响Windows桌面的漏洞经常在创建现有的许多被恶意代码感染并控制的与Internet相连的计算机网络时被利用。考虑一下Slammer蠕虫 ,它利用了微软SQLServer中的一个缓冲区溢出,并被作为说明通过网络导致Windows漏洞的威力的一个示例。该漏洞被描述在2002年7月24微软发布的安全公告MS02-039中 ,而Slammer蠕虫在2003年1月25日被肤浅的加以描述。该蠕虫实际上并没有有效载荷,它只是简单的利用被感染的主机来扫描并传播到其它被感染的机器 。尽管该蠕虫缺乏有效载荷,但这种入侵式的扫描生成了足够的通信以导致对Internet、信用卡的处理的破坏,以及在某些情况下造成了对蜂窝电话网络可用性的破坏。令人感兴趣的是甚至在4年之后,Slammer蠕虫仍然位于最常见的五种通信生成事件之内  。显然,在Windows中所暴露的一个网络漏洞具有深远的含义。

    在前面的章节中,我们利用了一个已经存在的模糊器框架SPIKE,来构建了在UNIX环境中面向Novell NetMail NMAP daemon程序的一个协议模糊器。在本章中,我们采用一种不同的方法,从头至尾的构建一个简单的基于Windows的、具有GUI界面以及用户友好的模糊器。尽管名为ProtoFuzz的最终产品将只提供最基本的功能,但是它提供了一个很好的扩展平台,并且为模糊器的创建提供了一些不同的视角。下面首先来讨论一下该模糊器所具有的特性。

    16.1 ProtoFuzz的特性

    在我们开始开发该工具之前,必须要首先考虑一下所需要的以及所期望的特性。在最基本的层面上,一个协议模糊器只是简单的在目标上传输变异的包。因此,只要模糊器能够生成并发送数据包,那么它就满足要求了。然而,如果ProtoFuzz具备理解想要被模糊的包的结构的功能,那么将会更好。下面让我们对这个基本的需求进行扩展。

    16.1.1 包结构

    在我们的模糊器可以发送一个数据包之前,需要了解如何来创建一个包。对于现有的模糊器而言,在模糊测试中采用下面的三种基本方法之一来组装包:

    技巧性的测试集。PROTOS测试集 及其配套的商业工具Codenomicon ,都是将用于模糊测试的数据包的结构进行硬编码。象这样来创建测试集是很浪费时间的,因为它需要分析一个协议的规范并开发成百上千个硬编码的测试用例。

    生成模糊器。正如我们在前面的章节中所看到的,象SPIKE这样的工具需要用户创建一个描述数据包结构的模板。然后,模糊器就负责在运行时生成并传输单个的测试用例。

    变异模糊器。如果不是从头创建一个包,那么一个可选的方法是从一个已知的好的包开始,然后连续的对该包中的部分进行变异。尽管在强制性测试模式下,该包中的每一个字节都可以被变异,但该方法通常不适用于协议模糊测试,因为该方法的效率不高,并且会生成与网络协议不一致的包,甚至会生成无法到达目标的包。可以通过借鉴协议模板方法的一些技巧来修改该方法。该模糊器从一个已知的好包开始工作,然后用户通过识别应当被作为测试目标的那部分数据来创建一个模板。该方法将被ProtoFuzz所利用。

    没有一个单独的方法要优越于其它方法,每个方法都有其各自的适用情形。ProtoFuzz之所以选择包变异方法,就是因为它所具有的简洁性。以一个可以从被观察的网络通信中获取的已知的好包开始,就允许用户可以立即开始模糊测试,而不用花费大量的时间来研究协议并创建生成模板。

    16.1.2 捕获数据

    由于选择了包变异作为模糊测试的方法,那么ProtoFuzz应当能够在混乱模式下捕获网络数据,就像一个嗅探器一样。我们将在ProtoFuzz中创建一个网络协议分析器,这样,它就可以捕获从目标应用进出的通信,然后我们就可以选择单独的包来进行模糊测试。为了实现此功能,我们将利用一个已存在的包捕获库,该库将在"ProtoFuzz的开发"一节中做详细的介绍。

    16.1.3 解析数据

    尽管这不是绝对需要的,但我们想要ProtoFuzz除了捕获数据之外,还能够以一种易于理解的形式将所捕获的包的内容展现出来。这将有助于用户识别包中适于进行模糊测试的部分。网络包数据只是一系列遵循特定模式的字节。以一种用户易读的格式来显示数据要求利用一个捕获库,而该库能够理解包结构并将其分解为组成数据流的不同的协议头和数据段。许多人对Wireshark所使用的表现格式很熟悉,因此我们将其作为显示格式的基础。

    这个来自于Wireshark的特定的屏幕快照将段在三个窗格中进行了显示。最上面的窗格列出了被单个捕获的包。选择其中的一个包将把该包的内容加载到下面的两个窗格中。中间的窗格显示了将该包分解为单独的字段。在这个例子中,我们可以看到Wireshark比较熟悉AOL实例消息协议,因为它成功的将TCP包的整个数据段进行了解析。最后,底层的窗格显示了被选择包的原始十六进制和ASCII字节。

    16.1.4 模糊变量

    一旦网络数据被观测并捕获,我们想要允许用户来确定其中将为模糊测试而进行变异的部分。为此,我们将使用一种非常简单的格式,即使用开始和结束标签将包的十六进制表示部分包含起来。不同的标签将被用来表示运行时模糊变量的不同类型。这种简单的格式将允许用户可视化的识别模糊变量,并且允许ProtoFuzz在解析包结构时来识别将要被模糊数据所替代的区域。

    [XX]强有力模糊变量。被方括号包围起来的字节将使用所有可能的字节值来模糊化。因此,一个单独的字节将被模糊256次,而一个字值(两个字节)将被模糊65536次。

    <XX>字符串模糊变量。也可以使用用户控制的文本文件中(String.txt)预定义的变量长度字符串的十六进制表示来模糊化字节。这种类型的模糊测试通常在包的数据部分来进行,而不是在需要一个已定义结构的头字段中。

    下面的模板说明了同时具有强有力模糊变量和字符串模糊变量的TCP包。

    16.1.5 发送数据

    协议库在发送数据时通常采用两种不同的方法。首先,它们具有一系列的函数允许你设定数据的特定部分,如目标IP和MAC地址。然而,包结构的大部分内容都是基于适当的RFC已经给你构建好的。一个.NET框架类如HttpRequest,就是这种类的一个示例。该类允许你定义诸如方法和URL等的属性,但大多数的HTTP头和所有的以太网,TCP以及IP头都是已经给你创建好的。另外一种可选的方法是创建一个原始数据包以使单个字节可以被指定,并且由程序员来确保包要遵循定义在适当RFC中的结构。尽管第二种方法需要研究者多做一些工作,但是它提供了更细的控制粒度,将使模糊协议头具有同数据段一样的功能。

    16.2 必要的背景知识

    在开始讨论ProtoFuzz的开发过程之前,我们应当首先强调一些重要的背景知识。

    16.2.1 错误检测

    对任何类型的模糊测试而言,在目标应用中检测错误等同于发现漏洞。没有一个完美的方法来实现这一点,但某些方法要优于其它的方法。例如,在协议模糊测试中存在的不同于文件模糊测试的一个障碍是,模糊器和目标应用可能将被分隔为两个系统。当对网络协议进行模糊测试时,将一个调试器关联到目标应用是一个好的开端,它将允许你查明已处理和未处理的异常,否则的话这些异常将可能不会被识别。当使用调试器时,你仍然要面临将模糊包与它们所生成的异常进行关联的挑战。尽管不是十分的安全,但可做的工作包括在每个包之后发送某种形式的探针,以确保目标应用仍然是可响应的。例如,你可以向目标应用发送一个ping请求,并确保在发送下一个模糊包之前接收到一个回应。这不是非常的完美,因为一个异常可能已经发生,但并不影响目标应用响应ping的能力。然而,你可以针对目标应用来定制探针。

    性能下降

    除了监视错误之外,我们也可以监视目标应用所发生的性能下降问题。例如,考虑一个在目标应用的处理逻辑中导致一个无限循环的病态数据包。在这种情况下,没有实际的错误被生成,但是我们却具备了一个DoS条件。在目标机器上运行性能监视器,如在微软的管理控制台中可用的Performance Logs and Alerts或者System Monitor snap-ins,可以帮助识别这些条件。

    请求超时和非预期响应

    并不是所有被传输的模糊包都将引起一个响应。然而,对大多数包而言都会产生一个响应,监视响应以确保目标应用仍然正常的运转是非常重要的。再进一步,也可以对响应包的内容进行解析。采用这种方法,你不仅能够识别何时响应没有被接收,而且还可以识别包含非预期数据的响应。

    16.2.2 协议驱动程序

    许多包捕获库需要使用一个定制的协议驱动程序。为ProtoFuzz所选择的Metro Packet Library 包括ndisprot.inf,它是微软作为其驱动程序开发工具包的一部分而提供的网络驱动程序接口规范(Network Driver Interface Specification,NDIS)协议驱动程序的一个示例。NDIS对于网络适配器而言是一个有效的API,并且为应用程序提供了发送和接收原始以太网包的能力。在ProtoFuzz可以被使用之前,该驱动程序必须被手工的安装和启动,这可以通过在命令行运行net start ndisprot来实现。使用这样一个需要定制的驱动程序的库的缺点是,该驱动程序可能无法处理所有类型的网络适配器。例如对于Metro而言,该驱动程序将不能处理一个无线适配器。

    16.3 ProtoFuzz的开发

    现在已经有一些运行的很好的协议模糊器。如果我们准备创建一个新的模糊器,那么为了避免开发一个重复的项目,就需要提供其它模糊器所不具备的一些功能。由于许多现有的模糊器都是从命令行开始运行的,因此我们面向下面的目标来使ProtoFuzz不同于其它模糊器:

    直观性。掌握ProtoFuzz的使用应当不需要一个长的学习曲线。一个终端用户应当可以掌握该工具的基础用法,而不用手工输入或记住令人费解的命令行选项。

    易于使用。一旦ProtoFuzz被启动,它就应当立即开始运转。我们将利用前面所捕获的数据包的结构来创建模糊测试模板,而不需要用户来创建繁琐的模板以定义数据包的结构。

    可以访问所有的协议层。一些网络模糊器关注于包内的数据,而不是协议头本身。ProtoFuzz应当能够对一个包的任何以及所有部分进行模糊化,包括从以太网头到TCP/UDP数据。

    我们创建ProtoFuzz的目标肯定不是要替代现有的工具。它是为在Windows环境中进行网络模糊测试来构建一个基础平台,该平台既可以作为一个学习的工具,也可以作为一个开源项目被任何感兴趣的团体来进行扩展。

    16.3.1 开发语言的选择

    对于我们的基于Windows的文件格式模糊测试工具FileFuzz而言,我们选择了C#和微软的.NET框架来创建GUI驱动的网络协议模糊器。当开发GUI应用程序时,.NET平台可以处理许多繁琐的事务,以使你可以关注于应用程序的业务逻辑,对于我们的情形而言,就是关注于破坏业务的逻辑。.NET应用要求用户首先要安装.NET框架。然而,基于.NET的应用的不断流行也正在使这一点变得有些不方便,因为大多数系统已经具有可用的库。

    16.3.3 设计

    我们已经了解了足够的理论知识,下面就可以进行编码了。同样,ProtoFuzz的所有源代码可以从www.fuzzing.org网站上获取。我们不准备分析代码的所有部分,但是在下面的几节中,我们将强调一些非常重要的代码段。

    网络适配器

    我们希望ProtoFuzz能够捕获将被用来生成模糊模板的通信。为此,用户必须要首先选择将被置为混合模式以嗅探通信的网络适配器。我们不要求用户提供一个适配器名或者他们手工发现的标识符,我们将向用户提供一个简单的下列菜单,通过这个菜单,他们可以选择系统中任何活动的网络适配器。

    首先,我们初始化了一个新的NdisProtocolDriverInterface,然后使用用户应当已经手工安装的ndisprot网络适配器来调用OpenDevice()函数。如果该适配器不可用,一个SystemException异常将被捕获,用户将被提示安装并启动该适配器。一旦该调用成功的执行完,我们就得到了一个NetworkAdapter数组,该数组包含了所有可用的网络适配器的信息。利用该数组,我们使用一个简单的foreach循环来遍历该数组,并将AdapterName属性添加到组合列表框控件中。

    捕获数据

    一旦我们打开了一个适配器,就可以将它置于混合模式并开始捕获通信。

    我们在一开始创建了一个二维数组(capturePackets),它包含一个被捕获包的数组以及一个在该包中的字节的数组。然后调用BindAdapter()函数将所选择的网络适配器绑定到前面所初始化的NdisProtocolDriverInterface(driver)。在这时,我们在一个单独的线程中调用capturePacket。为此部署一个单独的线程是非常重要的,这样当包被捕获时GUI就不会加锁。

    为了得到一步被捕获包的字节数组,只需要简单的调用ReceivePacket()函数。然后只要将它们添加到capturePacket数组中即可。

    解析数据

    因为我们采用两种独立的方法来显示被捕获的包,因此对数据的解析是一个包含两个步骤的过程。为了与Wireshark所使用的设计保持一致,当一个用户选择了一个被捕获的包时,该包内容的一个概要将显示在一个树形控件中,同时,原始字节被显示在一个超文本框控件中。采用这种方法,用户就具有了一个清晰的易于理解的总体概览,但仍然控制着单个字节以用于模糊测试。我们试图通过当树形控件中的一行选项被选中时,在红色文本框中突出显示相关联的原始字节来将两个窗口关联起来。

    被捕获包的概要视图被显示在中间的树形窗格中。创建树形控件的代码包含在packetTvwDecode()函数中,该函数将代码分隔为不同的块以解析下面每个头:以太网,TCP,UDP,IP,ARP和ICMP。

    可以看到,Ethernet802_3类使用提供给构造器的capPacket中的字节数组来进行了初始化,并且树形控件中显示的每个节点就是该类的一个属性。下面的代码片段是一个基本的循环,以在一个网格窗格中显示原始包字节。该循环每行打印16个字节。

    模糊变量

    对于强制性模糊测试而言,模糊变量通过将字节包围在方括号中([])来表达,而对于基于字符串的模糊测试而言,模糊变量则通过将字节包围在尖括号中(<>)来表达。当发送包时,代码简单的读取原始包并寻找成对的括号。当遇到这些模糊变量时,在发送包之前模糊数据将替代原始数据。

    十六进制编码和解码

    最后一个关心的问题是对被用于显示捕获数据包内容的字节表示进行编码和解码。.NET框架提供了ToString("X")函数以将一个字节数组转换为一个十六进制字符串,但是它并没有提供类似的函数将一个字符串转换为一个字节数组 。因为这个原因,所以添加了HexEncoding类,该类的大部分内容借鉴自www.codeproject.com站点中的"将十六进制字符串转换为字节数组,或者从字节数组转换为十六进制字符串"。

    16.4 实例研究

    现在我们已经构建了一个网络模糊器,下面应该确保它能够正常的工作。ProtoFuzz是一个一次执行单个任务的工具,它获取一个包并在将它发送给目标之前重复对它进行变异。例如,在发送导致崩溃的包之前,它不能发送一系列被设计的初始包以使目标进入到一个漏洞状态。

    在这个例子中,我们需要向SMTP服务器发送一些请求。我们需要建立起初始的TCP连接,并且为粗体所示的每个HELO,MAIL FROM和RCPT TO命令发送一个请求。初始请求必须要将服务器设置到准备接收RCPT TO命令,并且长字符串最终将导致溢出的状态。类似于此的一个漏洞更加适合于采用如SPIKE这样的工具,允许创建脚本来定义包结构,并且允许发送多个请求。