转自CSDN《程序员杂志》作者:小淅,火点
由于微信等即使移动通讯工具的迅速发展,其取代传统电话短信,成为人们互相沟通、传播消息的主流手段,因此保护微信聊天记录存在巨大的潜在需求。本文将在不修改原始微信应用的前提下,给出如何为微信增加隐藏好友聊天记录的功能介绍、技术细节以及遇到的挑战。
广大的安卓手机用户都熟悉各大主流安全软件的“私密联系人”功能,可以把一些联系人通话记录、短信内容等保存在一个特殊的地方,输入密码才可看到。鲶鱼团队对这个功能的评价是:鸡肋!请问,你每天使用传统短信应用发了几条消息?而你使用微信发了多少消息?在为数不多的使用传统短信的情况下,又有多少是具有高度私密性需求的呢?因此,私密联系人的想法是好的,但用错了地方。
前段时间上映的电影《北京爱情故事》很多人都看过,王学兵饰演的出轨男吴峥以为把小三儿名字改成“孙总”就万事大吉了?错!暧昧短信、大尺度视频都赤裸裸的躺在微信聊天记录里,把老婆想的太没智商。这年头,治标不治本是不行的。
鲶鱼团队在某次集体观摩了该电影后,大受启发,突发一个奇想,把“私密联系人”功能用于微信,让用户可以随时隐藏微信好友聊天记录,让保护隐私的功能真真正正的被用在用户有强烈需求的地方。对于青少年,不再担心微信中的聊天记录被父母发现;对于恋人,不再郁闷一言一行都被对方检查;对于一个社会关系“复杂”的商务人士,他的商业机密不会被竞争对手窥探,他的“私人生活”不会被家中老小知晓;对于一个普通人,他终于可以在别人借手机的时候,不用担心别人“无意”看到他跟爱人的甜蜜聊天了。
在互联网信息爆炸的时代,人终将没有因此。鲶鱼团队只想在有限的空间,为大家做到最好的隐私保护。微信以迅雷不及掩耳之势取代传统短信和电话,让沟通随时随地的进行。因此,保护微信聊天记录,建立专属自己的微信密友,大有必要!
微信密友功能由两部分组成:鲶鱼汇应用和微信应用。这里需要强调一点:密友功能绝不会修改微信应用,因此,鲶鱼团队提供的apk仅仅是“鲶鱼汇”而已,用户可以通过各种应用市场或官网下载微信应用。
图1 鲶鱼汇应用
鲶鱼汇应用完成两个功能:
1.开启/关闭微信密友功能
2.自动静默关注微信公众号“鲶鱼”。
启动微信密友功能后,微信中即可拥有以下所有功能:
1. 密友的隐藏与显示
2. 设置密友名单
3. 根据口令进入“显示密友模式”
4. 摇一摇退出“显示密友模式”
5. 用户自定义口令
功能演示:
进入微信好友聊天界面à长按某个好友会话列表 -> 选择“隐藏该聊天” -> 摇一摇,被选择成隐藏的聊天记录就从界面上消失了。如图2所示。
图2 微信密友演示
也许一部分读者的第一反应是:这是修改了微信应用吗?这里鲶鱼团队给出一个明确的答复:没有!鲶鱼团队的这款微信密友功能并不会修改微信原始应用,也不是一个插件。用户仅仅需要下载一个鲶鱼汇应用,在其中开启微信密友功能即可。
微信密友功能是基于对微信内部编码逻辑有了一定的了解的基础上实施起来的。怎么了解微信的内部构造?答案是对微信应用进行逆向分析。这里需要读者对逆向分析,Java的Hook技术和注入技术有一定的了解。这里只做简单描述,详细技术细节可关注后续文章。
逆向分析本质上就是对一个apk进行反编译并分析其代码逻辑。
Inject即为为注入,是指攻击者将自己编写的一段代码强行运行在另一个进程中的行为。
Hook即为钩子技术,通过在关键函数中插入判断代码,借此控制此关键函数的运行流程。
从根出发:熟悉android的读者自然会联想到,会话列表中某些会话的消失和出现对应于UI层来说就是ListView中某些Item的无和有(或者是不可见和可见)。这就是整个思路线索的根。
顺藤摸瓜:有了根,就要抽丝剥茧出一条有价值的线索。
ListView -> Adapter -> Cursor ->ContentResolver -> ContentProvider -> SQLiteDatabase
在UI上体现出来的某些ListView的Item有和无,可以通过在上述这条线索上的任何一点进行技术处理来达到。下面将一一介绍。
方案一:修改ListView,将需要隐藏的Item设置成GONE,如图3绿色部分所示。
方案二:修改Adapter,对Cursor行记录进行筛选,过滤某些行;修改Cursor,对查询到的Cursor进行包装或修改,过滤Cursor中的某些数据,如图3蓝色部分所示。
方案三:修改调用ContentResolver.query()/ContentProvider.query()/SQLiteDatabase.query()的参数,如图3黄色部分所示。
图3 隐藏方案线索
方案比较
方案 |
局限性 |
方案一 |
ListView的Item重用机制导致在此方案中需要特别考虑被setVisiblity(View.GONE)的item何时再被设置成VISIBLE. |
方案二 |
对Cursor的行记录进程筛选会牵扯到大量的数据库拷贝,有一定程度的性能影响 |
方案三 |
若微信应用不使用ContentResolver,ContentProvider,则该方案失效 |
踩点安钩:上述的三种方案都可以实现,到底在哪里做手脚比较合适?
通过逆向分析微信应用,发现一个“讨巧”的地方:微信的Adapter中存在一个方法func(List list),该方法的参数是一个String类型的List,List中的元素为微信好友唯一标示的username,该方法的作用是设置黑名单,使list中的好友不显示在好友对话列表中。
举个例子:逆向分析微信得到的这个List中原本只有一个元素,就是“floatbottle”(漂流瓶),也就是说大家的好友都包括“漂流瓶”,只不过微信把它隐藏了而已。
有了这个巧妙的函数,给微信设置黑名单就轻而易举了。
精确勿差:下面给出思路梳理,如图4所示。
1) 通过工具(比如monitor等)找到微信好友会话界面的MainUIactivity。
2) 通过该MainUI activity对象,拿到其中使用的ListView对象。
3) 通过ListView对象拿到其中的Adapter对象
4) 有了Adapter,可根据Adapter中使用的某些方法,比如setCursor()等跟踪到Cursor对象
5) 跟踪Cursor对象从哪里被query出来
6) 通过query()找到对应的SELECTION语句
图4 流程梳理
因地制宜,具体问题具体分析。
上述给出了理论上的思路和逻辑分析方法,结合反编译微信得到的有用信息,鲶鱼团队决定通过微信本身提供的Adapter中设置黑名单方法func(List list)来设置密友名单。利用这个方法func,可以将微信密友名单作为黑名单设置进去,再触发更新cursor,更新adapter,最后触发ListView的重绘,从而更新UI,使密友会话在UI界面不显示。过程图5所示。
同理,将密友名单从黑名单List中删除,触发更新UI,密友即可显示。
图5隐藏密友操作原理
设置密友操作如下:长按某个好友会话,在弹出的菜单中单击“隐藏该聊天”,则该好友被设定为密友,且在退出“显示密友模式”后,该密友聊天会被隐藏。如图2所示。
这里涉及到三个地方的修改:
1) 修改会话列表中某个item的长按点击事件。
2) 修改菜单(长按item后弹出的为菜单,需要为菜单新添加一个“隐藏该聊天”选项)。
3) 为菜单中新添加的“隐藏该聊天”选项添加响应事件。
使用Hook技术中替换OnItemLongClickListener对象的方法即可。
密友聊天记录是用户私密的信息,通常情况下是不会显示在微信聊天首页的。因此,若想跟密友进行聊天,需要凭借用户设定的口令进入“显示密友模式”,做法是:在微信会话页面的搜索框中输入口令(默认口令为“菠萝菠萝蜜”,用户可以根据设置口令规则自定义口令,下文中有介绍)后自动进入“显示密友模式”。
原理:拿到微信会话页面搜索框中输入文字EditText对象,为该对象添加一个TextWatcher,当EditText中的内容有任何改变时,TextWatcher中的方法会被调用,在该方法中可以对EditText内容进行匹配,若为口令,则启动显示密友模式。
退出“显示密友模式”需要快、隐、稳,也就是用户跟密友聊天完了以后,可以快速的、神不知鬼不觉的让聊天记录消失。
鲶鱼团队特别的使用了摇一摇来完成退出“显示密友模式”。目的何在?于千千万万个酷炫方法中,为何只取“摇一摇”独饮?原因在于微信的“摇一摇”深入人心,人尽皆知!
“摇一摇”功能是基于加速度感应器,通过上报X,Y,Z三轴的加速度来实现功能,刚开始简单的实现为判断某个轴的加速度是否达到阈值,结果发现效果很不理想,最大的原因是不同型号的手机的感应器灵敏度都不一样,这个阈值就很难设定了,还有更坑爹的是热门某米手机,同一个型号的灵敏度都差别非常大。
转念去试了一下微信的摇一摇,每个手机都体验非常棒,而且是除了斜45度角上下摇特定手势,其他手势一般都不容易触发,后来又试了其他几个比较热的app的摇一摇,不比不知道,微信的摇一摇真的是用户体验最好的,甩开其他产品几条街,肃然起敬,果然是一个伟大的产品,每一个细节都做到极致。
于是就地取材,逆向了微信的算法,果然精妙,每次计算是对X,Y,Z轴的有个加权因子修正,Y轴权重最大,阈值也不是固定的,而是根据手机灵敏度在一个范围内动态调整,由于我们不需要这么灵敏,所以对阈值稍许做了调整。
有了优良的算法,接下来的事情就水到渠成了。在微信MainUI activity的onStart()和onStop()中添加SensorEventListener,监视手机“摇一摇”状态,上手体验果然非常好。当微信处于前台且手机摇一摇触发时,根据新的黑名单触发刷新UI,隐藏密友会话列表。
这是一款保护用户隐藏的应用,必须得让用户能够自定义口令进入显示密友模式。我们的想法是一切都在微信中搞点,不用再到鲶鱼汇中设置了,头脑风暴之后,我们想到通过一个公众号发消息的入口设置。方法是在微信公众号“鲶鱼”中输入“口令#旧口令#新口令”,可重置口令。初口令为“菠萝菠萝蜜”。
原理:拿到EditText对象和“发送”按钮对象,hook按钮对象的onClickListener方法,在方法中,判断EditText内容,若内容与设置口令模板匹配,则保存新口令,并将该设置口令语句删除。
现在引入新的问题,如果让用户搜索并关注这个公众号实在麻烦,更麻烦的是微信允许同名,筛选更痛苦。所以最好是开启微信密友,就自动关注公众号“鲶鱼”,如何做到呢?
1) 启动关注页面activity
首先找到的微信账号关注页面的activity,鲶鱼汇应用通过startActivity(Intent intent)来启动该页面。在intent中将公众号“鲶鱼”的信息填入其中,包括“鲶鱼”在微信server端固定的一串标识,“鲶鱼”的简介文字等。也就是鲶鱼汇应用构造出一个微信应用在启动它的关注页面activity时需要用到的一模一样的intent,并用它去startActivity启动关注账号页面。
2) 静默实现关注
从图示可以看到,关注页面下方此时为绿色的“关注”按钮。静默调用该按钮的onClick事件,达到“关注”的效果。
3) 使该activity不可见
接下来,在关注界面activity的onResume方法中调用finish()方法将自己结束掉。该activity并不会显示在前台,用户自然感知不到关注页面。
Hack APP犹如戴着镣铐在跳舞,各种限制会阻碍Hack的进展、产品的通用性、适配性等等。要想尽一切办法,让舞蹈跳得精彩动人!
微信应用的代码经过混淆,这是毋庸置疑的。这就完了吗?当然不是!微信大版本的升级,同一个版本的不同编译版本等等,都会造成相同的方法/类的混淆名不一样,如果在注入代码中用了混淆名,基本上宣告失败,即便是一个小版本更新,注入代码就很可能无法工作了。因此,微信密友功能的最大难点在于更混淆名斗争,适配不同微信版本,也就是尽可能的在各种不一样的混淆名中找出线索,做到自适应。好比如明明我知道美女在哪里,就是不能直接搭讪,一定要通过熟人介绍。下面是笔者总结的小技巧,可供参考:
1) 尽量使用未被混淆的类名/方法名/变量名等。比如通过AndroidMenifest.xml的四大组件方法入口;通过SDKAPI的调用、继承、实现等;通过资源寻找相关代码(如利用monitor工具等)。
2) 通过特征找到混淆的类名/方法名/变量名。比如,假设有一个View中有方法为setAdapter,那么可以考虑这个view是否为ListView。
即便是这样,也依然无法100%解决问题,带着镣铐跳舞大多还是痛苦的。
微信密友涉及到两个进程:鲶鱼进程和微信进程。鲶鱼进程在启动阶段会将一段代码(dex文件)注入到微信进程中,而重复注入导致微信加载多次dex文件,致使系统功能异常。因此,如何保证这两个进程任意一方挂掉并重启后,不会重复注入微信进程?
图6 鲶鱼进程与微信进程
解决方案是在鲶鱼进程的SharedPreferences保存上次注入微信的进程ID,同时互相传递binder,通过linkToDeath监听对方进程挂掉的消息。
1. 当鲶鱼挂掉,有两种情况:进程异常或者卸载。
如果是进程异常,则重启后读取SharedPreferences保存的上次注入微信的进程ID,如果进程ID没有变化,则不注入。
如果是卸载,微信进程则会重启自己,意味着微信密友功能消失。
2. 当微信进程挂掉后,鲶鱼进程则会监听微信进程启动,重新注入,然后保存新的进程ID到本地的SharedPreferences
首次进入微信聊天页面在activity的onPostResume()中,需要拿到该activity对象中使用的ListView对象,但ListView是异步加载的,即onPostResume()运行完成的时候ListView对象仍没有生成。
解决方案:将寻找ListView对象的逻辑写成一个Runnable,发现找不到ListView对象时就再post自己,知道找到或条件不满足时结束。
即时移动聊天工具的出现,迅速的取代了传统的短信、电话和邮件,这些社交工具中的聊天消息承载了从生活、情感、金融、商务等各方面的个人重要信息,而这些信息在网络大爆炸的当下,是即为不安定的定时炸弹。当各家安全软件还在着眼于诸如私密通讯录等功能时,鲶鱼团队做了一个大胆的尝试和推测:微信密友。相信它更贴近社交人的生活圈,更服务于百姓,更好的保护个人隐私。