bkcrack
是一个zip明文攻击
工具,点击这里访问仓库地址。本教程翻译自bkcrack
的示例教程,在翻译过程中,我对示例教程做了一些修改,并增加一些内容。
仓库的example
目录里包含了一个示例文件secrets.zip
,本教程将使用这个压缩包来演示zip明文攻击
。
教程使用的环境是MacOS
系统,如果你使用的是Windows
系统,可以用PowerShell
来执行命令。
首先让我们康康压缩包里面有什么东西,使用终端进入example
目录,执行以下命令:
1 | $ zipinfo secrets.zip |
可以看到,secrets.zip
压缩里包含了两个文件:advice.jpg
和spiral.svg
。
第5列为Bx
,开头的大写字母B
代表文件被加密了,需要密码才能解压。
第6列的defN
代表advice.jpg
被压缩,stor
代表spiral.svg
未压缩。
defN
是deflated (normal)
的缩写,表示压缩方式是deflated
,压缩类型是normal
。stor
是none (stored)
的缩写,表示文件未压缩,只进行存储。
要进行破解的话,我们必须要知道至少12个字节的明文。通常知道的连续明文越多,破解的速度就越快。
我们可以从spiral.svg
文件的扩展名推测出这是一个xml
文件,那么文件的内容很可能以字符串<?xml version="1.0"
开头。假如我们的猜想正确的话,这20个字节的明文已经绰绰有余了。
幸运的是,压缩包里的spiral.svg
文件并未进行压缩,所以可以直接进行明文攻击。
让我们先把推测的明文保存到文件plain.txt
中备用:
1 | $ echo -n '<?xml version="1.0" ' > plain.txt |
执行后example
目录下会生成一个plain.txt
文本,里面保存着我们推测的明文,加上-n
参数是为了让文本末尾不产生换行符。
准备工作就绪,现在可以进行明文攻击了。
执行以下命令:
1 | $ ../bkcrack -C secrets.zip -c spiral.svg -p plain.txt |
命令的格式为:bkcrack -C 加密的压缩包 -c 存在明文的文件 -p 存储了明文的文本
稍等一会就能得到3个密钥:
1 | bkcrack 1.3.1 - 2021-08-16 |
通过开始时间和结束时间可以算出耗时为2分钟53秒。
此外,根据《ZIP文件格式规范》第6.1.6
点中所述,压缩包在存储的文件数据之前添加了一个12字节的加密标头,加密标头的最后一个字节是文件CRC
值的最高位字节。
可以通过以下命令来获取spiral.svg
文件的CRC
值:
1 | $ zipinfo -v secrets.zip spiral.svg | grep CRC |
所以我们知道了文件内容的前一个字节(即偏移量-1处)是0xA9
。
通过以下命令使用附加信息进行攻击:
1 | $ ../bkcrack -C secrets.zip -c spiral.svg -p plain.txt -x -1 A9 |
附加信息的格式为:-x 偏移值 十六进制字节数据
执行后同样可以得到3个密钥:
1 | bkcrack 1.3.1 - 2021-08-16 |
可以算出耗时为3分钟,这里多了一个字节的明文,但是破解速度稍微慢了几秒。
前面说了知道的连续的明文越多,破解的速度就越快,是相对而言的。如果附加的明文有很多个字节的话,那么破解速度就能加快许多。
一旦我们有了3个密钥,我们就可以从压缩包中恢复出原始文件。
假设zip压缩包里的所有文件都使用了相同的密钥,那么我们可以使用新密码来将secret.zip
存储为一个新的加密压缩包。
例如使用easy
作为新密码,执行命令:
1 | $ ../bkcrack -C secrets.zip -k c4490e28 b414a23d 91404b31 -U secrets_with_new_password.zip easy |
命令格式为:bkcrack -C 加密的压缩包 -k 3个密钥 -U 新的压缩包 新密码
执行后会得到一个secrets_with_new_password.zip
压缩包,使用easy
密码就可以解压出所有文件。
我们也可以选择一一提取文件。
首先提取spiral.svg
文件:
1 | $ ../bkcrack -C secrets.zip -c spiral.svg -k c4490e28 b414a23d 91404b31 -d spiral_deciphered.svg |
由于压缩包中的spiral.svg
文件未压缩,因此执行后得到的spiral_deciphered.svg
便是原文件。
再提取advice.jpg
文件:
1 | $ ../bkcrack -C secrets.zip -c advice.jpg -k c4490e28 b414a23d 91404b31 -d advice_deciphered.deflate |
因为advice.jpg
是通过deflate
算法压缩的,所以提取到的advice_deciphered.deflate
也是压缩过的,我们需要解压它,为此,tools
目录中提供了一个Python
脚本,执行命令:
1 | $ python3 ../tools/inflate.py < advice_deciphered.deflate > very_good_advice.jpg |
执行后便可以得到解压的very_good_advice.jpg
图片。
假设zip压缩包里没有包含未压缩的文件spiral.svg
,而全部文件都是压缩过的,那该怎么办呢?
我们可以通过advice.jpg
文件的扩展名知道这是一张图片,由于图片的文件头前面一部分字节是固定的值,因此我们可以将这些字节作为明文来使用。
但问题是,这个文件被压缩过了,文件的内容已经发生了改变。想进行明文攻击的话,就必须要知道文件压缩后的内容变成了什么。但是如果不知道原来整个文件的内容,基本不可能推出文件压缩后的内容。
如果可以在网上轻松找到原始文件,例如压缩包里包含了某个.dll
文件,在别的地方可以下载到,就可以拿来使用。然后,可以使用各种压缩软件和压缩级别对其进行压缩,以尝试生成正确的明文。
假如我们在网上下载到了very_good_advice.jpg
图片,下面演示如何通过该图片来获取密钥。
执行命令压缩图片:
1 | $ zip very_good_advice.jpg.zip very_good_advice.jpg |
执行后生成very_good_advice.jpg.zip
压缩包,查看压缩包信息:
1 | $ zipinfo very_good_advice.jpg.zip |
可以看到该压缩包也是使用deflate
算法压缩的。
执行命令:
1 | $ ../bkcrack -C secrets.zip -c advice.jpg -P very_good_advice.jpg.zip -p very_good_advice.jpg |
命令格式为:bkcrack -C 加密的压缩包 -c 加密的压缩文件 -P 未加密的压缩包 -p 未加密的压缩文件
结果显示无法找到密钥,可能是因为压缩包的压缩率不同,导致压缩后的压缩包数据不同,因此使用这些错误的明文是无法破解的。
zip压缩率有0 ~ 9
一共10个级别,其中0
代表未压缩存储,1
代表压缩速度最快(文件最大),9
代表压缩速度最慢(文件最小),默认的压缩级别是6
。
编写一个Python
脚本遍历10个级别进行破解:
1 | #coding:utf-8 |
执行结果为:
1 | Level 0: |
无法获取到密钥,说明secrets.zip
这个压缩包应该不是使用MacOS
系统的zip
命令创建的。
接下来使用7z
来测试,7z
有1 ~ 9
一共9个压缩级别,执行以下Python
代码:
1 | #coding:utf-8 |
执行结果如下:
1 | Level 1: |
可见,使用压缩级别9
可以成功获取到密钥。
我本来以为我已经过了折腾博客的年纪了,没想到最近还是把博客从WordPress
迁移到了Hexo
,主要有以下几个原因:
最近,我用了10多年的PHP
主机商宣布停运了,突然感到有些失落,我已经经历过太多网站的关停了,包括在我博客留下足迹的众多个人博客,现在还能打开的博客寥寥无几。
我就在思考,如果我有一天去世了,我的空间到期后也会被关闭,域名到期也会被回收,所以我希望我的博客在我大限到来之后能够再运行久一点。
因此我打算将WordPress
博客迁移到静态博客Hexo
,因为静态博客可以托管在GitHub Pages
里,如果GitHub
不倒闭的话,我的博客也不会关闭。
人终有一死,但GitHub
永存不朽。
如果正在看这篇文章的你所在的年份是2121
年之后的话,说明100年后我的博客还存在,至少证明我现在的决定是正确的。
Markdown
写作我一直幻想WordPress
的编辑器有一天能够支持Markdown
,可惜每次更新后都没看到希望,而且编辑器也变得越来越非主流。
我使用了第三方的Markdown
插件,但是插件有时会把Markdown
格式的文章又转换成html
编码,导致文章很难编辑。
而Hexo
是为程序员而生的,只需用Markdown
格式编写文章,Hexo
框架会自动渲染,非常丝滑优雅。
我很久以前就想使用静态博客了,但是考虑到静态博客无法进行评论,不能将WordPress
的评论迁移过去,所以还是放弃了。
经过多年的发展,市面上已经涌现出很多优秀的开源评论系统,如beaudar
, valine
, twikoo
, waline
, minivaline
, disqus
, disqusjs
, gitalk
, vssue
, livere
, isso
, hashover
等。
经过对比,我选择了使用waline
评论系统,官网为 https://waline.js.org 。
众所周知,国内访问GitHub Pages
的速度非常慢,有时甚至无法访问。
不过目前已经有别的网站托管服务商可以使用,比如优秀的Vercel
,可以看这篇介绍文章:
《Vercel是什么神仙网站?》
主站可以使用Vercel
部署,而GitHub Pages
用来作为博客的备份。
博客迁移过程主要参考了这篇文章:《WordPress迁移到Hexo填坑记录》 。
该文章将博客的评论迁移到valine
评论系统,我使用的是waline
,因为waline
是基于valine
开发的,所以迁移的过程大致相同。
登录WordPress
后台,点击工具 - 导出 - 下载导出的文件,可以得到WordPress.2021-08-15.xml
文件,存放到hexo/source/
目录下。
使用命令安装文章转换工具:
1 | npm install hexo-migrator-wordpress --save |
使用以下命令将WordPress
文章转成Hexo
的文章:
1 | hexo migrate wordpress WordPress.2021-08-15.xml --paragraph-fix --import-image --skipduplicate |
转换后文章保存在hexo/source/_posts
目录下,由于我之前有一半文章是使用富文本编辑器写的,所以转换后Markdown
格式有点问题,因此我手动把所有文章都修整了一遍。
我的WordPress
文章的固定链接以前一直使用/%category%/%postname%.html
,感觉还是一开始考虑得不够周到,导致现在出现了一点麻烦。
当初使用WordPress
建站的时候,我觉得文章链接加上分类目录的话会更加清晰,能够方便分类。但是我大部分文章都会存放在多个分类里,而分类目录是WordPress
随机添加的。
而Hexo
的博客系统使用目录分类比较麻烦,考虑再三,长痛不如短痛,将Hexo
的链接去掉了分类目录,只保留了文章名。
这样会导致旧的链接跳进来后找不到文章,所以我先在WordPress
做了301
重定向,等搜索引擎更新链接后,再开放Hexo
博客。
我的WordPress
里有1400
多条评论,这些评论必须要保留下来。
新的评论系统使用了waline
+MySQL
部署,所以迁移过程和上面的文章教程类似,用脚本将评论导入到MySQL
数据库即可。
但是我发现了那篇教程的脚本代码有bug
:有些评论的父级id赋值有误,会导致有些评论加载不出来。
比如A发表了一条评论,B评论了A,这时B的pid
和rid
都是A。
如果C又评论了B,那么C的pid
是B,但是rid
是A。rid
是评论的根id,如果设置错误的话,C的评论就无法显示出来。
这个bug
我已经修复了,文章后面会贴出代码。
文章的浏览数也是必须保留的,所幸的是,waline
也自带了统计文章浏览数的功能。
和评论的迁移一样,也是将浏览数批量保存到MySQL
数据库。
我使用了macOS
的MAMP
集成环境运行MySQL
,再使用Python
执行脚本,参考代码如下:
1 | #coding:utf-8 |
跟去年一样,浏览店铺满15秒可以获得大量的喵币,每天可以重复做几十次,但是这个过程有点无聊。
程序员最讨厌的就是做重复的事了,如果有一件事要重复做两次以上,程序员就往往会考虑编写代码来自动完成这些事。
如果在越狱的iOS或者安卓系统上,可以使用按键精灵
软件来自动领取喵币,不过能否在没越狱的iOS手机上做到呢?
iOS 13
系统自带的快捷指令
应用能够执行自动化操作,或许能够创建一个类似于按键精灵
的流程来自动领取喵币,于是我研究了一下,最终成功实现了自动化的流程:
注意:该流程在
iOS 14
的iPhone 11
上完美运行,在iOS 13.3
的iPhone SE
上偶尔会中断,具体原因不明,其它机型和系统没有进行测试。
整个研究的过程并不是很顺利,解决了很多个大坑才得以完成,下面就来介绍一下我使用的方法。
按键精灵
最重要的功能是能够自动点击屏幕,遗憾的是快捷指令
应用里并没有提供自动点击屏幕的功能,不过iOS 13
系统自带的语音控制
功能就能够做到这一点。
我在不久之前看过一个视频教程:《iOS 13自带王者荣耀一键换键功能,相当于辅助外挂》,这个视频介绍了在语音控制
里添加点击手势的操作步骤,建议先观看视频学习。
正常来说,我们要对手机淘宝
添加的点击手势是:在游戏界面点击去浏览按钮进入店铺界面,等待读条15秒任务完成后,再点击店铺右上角的关闭按钮退出店铺界面。
注意:语音控制的手势最多只能录制5个操作,最长只能录制10秒。
但是由于语音控制
的手势最多只能录制10秒,而读条等待的时间却超过了15秒,所以不能在一个手势里录制这些操作。那么我们不妨换个思路,先在店铺界面等待读条完成,再开始使用语音控制
执行点击手势。
修改后的手势操作为:点击右上角的关闭按钮返回到游戏界面,等待两秒后再点击去浏览按钮进入下一个店铺界面,这样只需花费不到5秒的时间便可以完成操作。
以下是在手机淘宝
里创建点击手势的操作步骤:
1、打开手机的设置 - 辅助功能 - 语音控制,开启语音控制
功能。
注意:如果是第一次开启
语音控制
功能,需要连接上WiFi,因为系统要下载几百M的语音数据包,不连接WiFi的话会导致开启失败。开启成功后,状态栏会出现一个蓝色的麦克风图标。
2、开启语音控制
后,进入自定命令 - 自定 - 创建新命令,输入命令短语。
注意:短语只能使用英文,但是使用单音节的词容易误识别,于是我使用了
evening
这个命令短语。
3、点击操作 - 运行自定手势,由于要点击的区域被导航栏挡到了,所以要点击隐藏控制项按钮进入全屏状态。
4、先点击点击右上角关闭按钮的位置❶,等待两秒后点击去浏览按钮的位置❷,并保存录制结果。
5、现在可以进入手机淘宝的店铺界面测试一下,对着手机的麦克风说出evening
,系统就会自动执行点击操作。
前面介绍了用人类的声音控制手机执行点击操作的方法,但是这样的“人工智能”并不是我们想要的,如果能够让手机自动执行语音控制
就再好不过了。
好在快捷指令
应用里有朗读文本
的功能,能够让手机朗读出evening
,从而自动执行语音控制
。
接下来打开快捷指令
应用,新建一个自动化流程:
1、在自动化
界面点击创建个人自动化按钮。
2、自动化的触发条件选择打开App。
3、选择打开手机淘宝
应用,再点击下一步。
4、添加两个快捷指令:等待8秒后,朗读evening
。
注意:打开
手机淘宝
自动执行快捷指令时,手机顶部会出现运行快捷指令的通知,通知大概需要6秒后才会消失。为了不出现误操作,等待8秒钟的容错率比较高。
5、展开朗读文本
的选项,语言
选择英语(英国)
,再点击下一步。
注意:默认的Siri语音可能会播不出声音,所以需要换成英国的发音。
6、不要选中运行前询问
,这样打开手机淘宝
后,系统才会自动执行快捷指令,否则需要手动确认才能执行。
7、这时可以打开手机淘宝
,系统会自动朗读文本,语音控制
也会自动执行。
注意:朗读文本需要使用手机扬声器播放,手机的音量可以调大一点,以便让语音控制识别到。
前面两步教程实现了以下的自动化操作:打开手机淘宝 -> 等待8秒 -> 朗读evening
-> 语音控制
执行操作 -> 退出店铺界面 -> 进入下一个店铺界面。
那么怎样才能让以上的操作一直循环执行呢?很不巧的是,快捷指令
依然没有循环执行的功能,但是还是有办法来解决这个问题的。
因为快捷指令
里面有打开应用(跳转应用)的指令,假设有应用A和B,那么可以创建两条自动化流程,第一条是打开应用A时跳转到B应用,第二条是打开B应用时跳转到A应用,这样两个应用就会不断互相跳转,相当于命令一直循环执行了。
所以我们可以在朗读evening
后,等待十几秒,再跳转到另一个应用,另一个应用再跳回手机淘宝
。当回到手机淘宝
后,又自动执行了手机淘宝
的快捷指令,这样快捷指令就能够一直循环执行。
随便找一个比较少用到的应用来做为工具人,我用的是系统自带的Apple TV
,也就是电视
应用。
注意:本教程使用
iOS 13
系统截图,应用名字叫电视
,iOS 14
里应用名字改为了视频
。
下面是实现循环执行的步骤:
1、编辑手机淘宝
的快捷指令,追加两条快捷指令:等待15秒钟,打开电视
应用。
注意:15秒是一个比较合适的时间,虽然和上面的8秒加起来让整个流程的等待时间达到了23秒,但是店铺界面之间的跳转以及语音控制的操作也是需要花费时间的,加起来花费的时间也要20多秒。
2、创建一个新的自动化流程,触发条件为打开电视
应用。
3、添加两个快捷指令:等待1秒后,打开手机淘宝
应用。
4、当然,运行前询问
也不要选中。
5、至此,所有的快捷指令就创建完成了,打开手机淘宝
后,系统便会循环执行快捷指令。
目前两个应用在互相跳转,永远不会停止,那么如何才能让快捷指令停止下来呢?下面介绍几种方法。
1、在这个教程里,快捷指令有十多秒的等待时间,只要在这段时间里,打开快捷指令
应用,将启用此自动化
关闭就行了。
2、如果快捷指令的等待时间比较短,来不及进行关闭的操作,那么可以将应用强制关闭,再马上锁屏。锁屏状态下的快捷指令会执行失败,所以能够打断循环执行的链条。
注意:单纯的锁屏是没用的,只要应用存活,屏幕解锁后又自动执行快捷指令了。
3、如果快捷指令执行的速度太快,来不及强制关闭应用,那么可以考虑强制重启手机。
4、也可以在快捷指令
里编写计次器,达到指定的执行次数后就中断执行流程。
快捷指令的功能很强大,除了自动领活动奖励,还可以做很多东西,比如自动签到、定时执行任务等,以后有机会的话再写一些新的教程。
]]>小萝卜头来到了下水道,下水道里面有一些错综复杂的水管,如图所示(红色箭头表示进水口和出水口):
其中出水口的水流进了蓄水池,而小萝卜头打算进入蓄水池里,所以只能想办法关闭这个出水口。
水管上面总共有11个阀门,而下水道里面只能找到3个扳手,每个扳手能关掉一个阀门。
那么问题来了:你能帮小萝卜头关闭3个阀门,让出水口流不出水吗?
我们遇到了新类型的游戏,这个游戏可以看成一个连通图,阀门就是连通图的节点,那么问题可以转化为:如何通过去掉3个节点,让进水口和出水口不相通?
如果用X
来表示进水口,用Y
表示出水口,用A
~ K
来标记11个阀门,结果如图所示:
那么如何来构建这张连通图呢?
首先要找出图中所有相连的节点,可以用元组(X, Y)
来表示节点X
和节点Y
相连。
找出所有两两相连的节点后,就可以用这些相连的节点来构建一个连通图。
以进水口节点X
为例,与节点X
相连的节点为[G, H, K, B]
,如图所示:
那么可以用一个元组数组来表示节点X
的连通关系:
1 | link_valves = [ |
假如接下来要寻找节点G
的连通关系,由于G
与X
已经相连了,已经存在(X, G)
,那么连通数组就可以不必加入(G, X)
。
当然,如果要加入也是没关系的,不加入的话看起来比较简洁。
由于水管错综复杂,要找出所有节点的连通关系是一件很麻烦的事情,所以只能使用人工智能
来寻找了。
这是一个纯体力活,耗费了两个打怪的时间才找完所有节点,节点的连通数组如下所示:
1 | link_valves = [ |
由于阀门节点要记录与其相连的阀门,所以不能够用基础数据类型来表示,定义一个阀门类比较方便。
阀门类使用阀门名字来初始化(阀门名字为英文字母):
1 | #coding:utf-8 |
测试代码将阀门B
与阀门A
相连,执行结果为:
1 | A(0) : ['B'] |
进水口和出水口可以视为特殊的阀门,即不可关闭,永远可以让水通过。
游戏类可以维护一个字典,以便快速根据阀门名来找到阀门对象。
新建一个游戏类,用相连的阀门节点数组初始化游戏:
1 | #coding:utf-8 |
代码执行结果为:
1 | A(0) : ['D', 'C', 'I', 'J', 'F', 'G'] |
如果要判断两个节点是否相通,需要使用递归来进行判断。
由于我们目前已经知道进水口X
和出水口Y
是相连通的,但是阀门X
连接的节点为['G', 'H', 'K', 'B']
,里面没有Y
。所以要遍历每一个连接的阀门,判断某个阀门是否和Y
连通。
假如能够找到某条相通的路径,例如X -> B -> Y
,就说明X
和Y
是相通的。如果遍历完所有路径都找不到,就说明两个阀门不相通。
判断两个阀门是否相通的深度优先遍历算法如下所示:
1 | #coding:utf-8 |
执行结果为:
1 | True |
说明进水口和出水口是相通的。
首先实现一下判断游戏是否完成的方法,如果进水口和出水口不相通,就相当于出水口被关闭了,那么可以写出下面的方法:
1 | #coding:utf-8 |
这个游戏可以使用回溯算法来求解答案,回溯算法跟深度优先搜索算法差不多。
如果广度优先搜索算法是孙悟空用分身走迷宫的话,那么深度优先搜索算法就是猪八戒走迷宫,猪八戒不能分身,只能先沿着迷宫某个岔路口一直走到最深处(深度优先的含义),如果发现是死路,再选择别的岔路口去尝试。
同理,这个游戏先尝试关闭前3个阀门,判断是否完成游戏。如果没完成游戏,就把第3个阀门打开,关闭第4个阀门,这样一直尝试下去,直到游戏完成为止。
回溯算法如下所示:
1 | #coding:utf-8 |
执行结果为:
1 | ['A', 'B', 'H'] |
将[A, B, H]
这3个阀门关闭的话,就能完成游戏,如图所示:
原文再续,书接上回。
机器人小萝卜头从牢房出来后,遇到了一个丢失了小狗的阿姨。
阿姨附近有一个起重电磁铁,小萝卜头打算使用起重电磁铁把铁箱子吸上去,不过需要先打开开关才能使用起重电磁铁。
电磁铁的开关有6个箭头,左边3个,右边3个,中间隔了一个空格。(注:游戏里使用的是上下箭头,而本文章使用左右箭头,讲解比较方便)
箭头移动的规则和玻璃珠跳棋的规则类似。
箭头只能移动到空格里,而且只能往箭头朝向的方向移动,不能够后退。
箭头移动的方式有两种,一种是把箭头移动到相邻的空格(例如上图第3个位置的箭头往前走一步):
另一种是跳过相邻的箭头后进入后面的空格,但是最多只能跳过一个箭头(例如上图倒数第3个位置的箭头往前走两步):
将左右两边的箭头互换位置,就能打开开关,如图所示:
那么新的问题来了,你能帮小萝卜头找出打开开关的最少移动步骤吗?
如果用1
来表示方向朝右的箭头,用-1
表示方向朝左的箭头,0
表示空格,那么游戏可以用一个数组来表示:
1 | [1, 1, 1, 0, -1, -1, -1] |
用1
和-1
来表示箭头是有原因的,因为方向朝右的箭头要往数组右边移动,所以下标要+1。而方向朝左的箭头往数组左边移动,下标要-1。箭头的值即为箭头移动时下标的偏移量,这会给代码增加一点便利性。
当箭头移动时,只需要交换数组里元素的值就行了。
新建一个游戏类,用数组初始化游戏:
1 | #coding:utf-8 |
代码执行结果为:
1 | [1, 1, 1, 0, -1, -1, -1] |
再给游戏类添加一个移动箭头的方法,参数为要移动的箭头的下标。
先把箭头当前所在的下标加上箭头的值,就得到箭头移动的新下标,如果这个下标所在的格子是空格,就说明可以移动。如果这个格子不是空格,就把新下标再加上该箭头的值得到新的下标,再继续判断。
因为箭头最多只能移动两个格子的位置,所以只需要判断两次。
1 | #coding:utf-8 |
执行结果为:
1 | [1, 1, 1, 0, -1, -1, -1] |
可见箭头的移动结果是正确的。
首先实现一下判断游戏是否完成的方法,如果游戏完成的话,中间的格子肯定是空格,左半部分全部都是方向朝左的箭头,那么可以写出下面的方法:
1 | #coding:utf-8 |
沿用上一个游戏的套路,这个游戏也继续使用广度优先搜索算法来求解答案。
那么怎么获取该游戏的操作步骤呢?
因为只有距离空格左右两边两个格子以内的箭头才能够移动,所以只要判断空格周围的4个箭头就行了。
获取空格左边两格内方向朝右的箭头,加上空格右边两格内方向朝左的箭头作为操作步骤。
可以写一个方法来获取空格周围的箭头下标:
1 | #coding:utf-8 |
广度优先搜索算法算法如下所示:
1 | #coding:utf-8 |
执行结果为:
1 | [2, 4, 5, 3, 1, 0, 2, 4, 6, 5, 3, 1, 2, 4, 3] |
数字代表每次移动的箭头下标,如2
表示移动第三个格子的箭头,其它类推。
需要移动15步才能打开开关,移动步骤如下:
题目中空格左右两边只有3个箭头,人脑也能比较容易思考出游戏解法。
那么如果每边的箭头不止3个,而是有4个、5个或更多个,怎样才能算出最少的移动步数呢?
先使用下面的代码打印出当每边有i
个箭头时,最少的移动次数:
1 | #coding:utf-8 |
执行结果为:
1 | 0 => 0 |
从结果可以看出:
如果每边有0个箭头,这时不用移动箭头就完成游戏,移动次数为0。
如果每边有1个箭头,最少需要移动3次。
如果每边有2个箭头,最少需要移动8次。
如果每边有3个箭头,最少需要移动15次,跟当前游戏相同。
观察结果,可以推出以下式子:
1 | f(0) = 0 |
求一下数列的通项式:
1 | 由于: |
可以很方便地算出,当每边有3个箭头时,所需移动次数为3 * (3 + 2) = 15
。
我们的机器人小萝卜头(robot)经历了千辛万苦,终于进入了监狱的第三个牢房。
牢房的柜子里可能藏着好东西,但是柜子的门上安装了一个密码锁,需要先打开密码锁才能开柜子。
密码锁由12个点组成,其中有6个绿点和6个红点。
密码锁上面还有3个转盘,每个转盘边上都有6个点。
转盘可以按顺时针或逆时针的方向旋转,当转盘旋转时,转盘上的6个点会跟着转盘一起转动。
如果将6个绿色的点转到中央的三角形区域,密码锁就能打开,如下图所示:
那么问题来了:你能帮小萝卜头找出打开密码锁的最少旋转步骤吗?
如果用数字1来表示绿点,数字0表示红点,那么游戏可以表示为:
1 | 1 0 1 |
若将这12个数字按从左到右、从上到下的顺序保存到一个数组里,则当前游戏状态可以表示为:
1 | [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0] |
那么当转盘转动时,怎么改变这个数组的值呢?
可以考虑先记录下每个转盘上的点在数组里的下标,然后根据下标来移动元素的值。
先看一下所有点在数组里的下标:
1 | 0 1 2 |
由上图可知,3个转盘边上的点的下标分别为:
1 | 第1个转盘:[0, 1, 5, 8, 7, 3] |
注意,上面转盘的点坐标是按顺时针方向获取的,以便进行旋转的操作。
只要能够模拟转盘的转动,就能编写自动求解答案的算法了。
要找出游戏的解法,首先要模拟游戏的玩法,下面就用python来实现一下这个游戏。
新建一个游戏类,使用dots数组来初始化游戏:
1 | #coding:utf-8 |
代码执行结果为:
1 | 1 0 1 |
由结果可见,游戏类能够正常打印出点的位置。
再给游戏类添加一个转动转盘的方法:
1 | #coding:utf-8 |
如果将第一个转盘按顺时针方向转动一下,将会成为下面的样子:
转动前:
转动后:
测试一下旋转的方法,以下代码将第一个转盘按顺时针方向转动:
1 | dots = [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0] |
执行结果为:
1 | 0 1 1 |
可见结果的数值和图片是相对应的,至此游戏的代码就编写完成了。
我们想写算法自动求解游戏,最终肯定要判断游戏是否完成的,所以可以先考虑一下游戏的完成条件。
当结果如下图所示时,就说明游戏完成了:
1 | 0 1 0 |
将上图转换成数组,结果为:
1 | [0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0] |
所以只要判断游戏状态是否等于上面的数组就行了:
1 | #coding:utf-8 |
要找出最少的旋转步骤,可以使用广度优先搜索算法来求解答案。
广度优先搜索就像孙悟空走迷宫一样,比如孙悟空走到迷宫的三岔口,就会拔出猴毛变成三个分身,每个分身进入一个分叉口。每个分身分别到达下一个分叉口后,又变出和分叉口一样多的分身进入每个分叉口。这样当其中某个分身最先到达迷宫终点的时候,这个分身所走过的路径就是最短的。
这个转盘游戏也是一样,游戏有3个转盘,每个转盘有两个旋转方向,所以总共有6种转法。每种转法相当于一个分叉口,把初始游戏按每种转法旋转后得到6个结果,每个结果也分别转6次得到6个子结果,这样不断转下去,子子孙孙无穷匮也。当某个子结果完成的时候,这个子结果所转动的次数就是最少的。
算法如下所示:
1 | #coding:utf-8 |
执行结果为:
1 | [(0, True), (1, False), (2, True), (1, False)] |
其中(0, True)
表示将第1个转盘进行顺时针旋转,(1, False)
表示将第2个转盘进行逆时针旋转,其它的同理。
也就是说,最少需要转动4次才能解开密码锁,旋转过程为:
多次打开密码锁可以发现,密码锁的初始状态不是固定的,还可能出现另一种状态:
修改一下初始化数组的值,如下所示:
1 | dots = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1] |
执行结果为:
1 | [(0, True), (1, True), (2, False), (0, False), (0, False)] |
在这种初始状态下,需要旋转5次才能完成游戏,旋转过程为:
1、第一个答案是B的问题是哪一个:
A.第2题 B.第3题 C.第4题 D.第5题 E.第6题2、唯一的连续两个具有相同答案的问题是:
A.第2,3题 B.第3,4题 C.第4,5题 D.第5,6题 E.第6,7题3、本问题答案和哪一个问题的答案相同:
A.第1题 B.第2题 C.第4题 D.第7题 E.第6题4、答案是A的问题的个数是:
A.0个 B.1个 C.2个 D.3个 E.4个5、本问题答案和哪一个问题的答案相同:
A.第10题 B.第9题 C.第8题 D.第7题 E.第6题6、答案是A的问题的个数和答案是什么的问题的个数相同:
A.B B.C C.D D.E E.以上都不是7、按照字母顺序,本问题的答案和下一个问题的答案相差几个字母(注:A和B相差一个字母):
A.4个 B.3个 C.2个 D.1个 E.0个8、答案是元音字母的问题的个数是(注:A和E是元音字母):
A.2个 B.3个 C.4个 D.5个 E.6个9、答案是辅音字母的问题的个数是:
A.一个质数 B.一个阶乘数 C.一个平方数 D.一个立方数 E.5的倍数10、本问题的答案是:
A.A B.B C.C D.D E.E这10道题的答案为: 。
以下是用python代码写的求解算法:
1 | #coding:utf-8 |
执行结果为:CDEBEEDCBA
点击链接查看第9集的魔术表演:《数学与魔术:油与水》
利用数学原理表演的魔术,只要严格按照魔术师的步骤表演,结果都一定能成功。
所以很多看似不可能完成的事情,背后往往有着不为人知的技巧,而这正是魔术令人着迷的地方。
接下来就详细说一下我对这个魔术表演的推理过程。
魔术师在表演过程中做了以下几个步骤:
整个表演过程中随机性的选择很多,所以看起来不太好推导,但是如果把步骤精简一下的话,看起来就清晰多了。
假如在很久很久以前,有一个魔术师发明了一个魔术,魔术的步骤是①②⑩,做完这3个步骤后,魔术就完美结束了,因为经过第②步操作后,红蓝牌的朝向自然就不同了。
也就是说,第③到⑨之间操作的步骤,并不会改变牌的朝向,而只会打乱牌的顺序。
那么把步骤③加进去呢?结果还是一样,牌的顺序乱了,但牌的朝向不变。
如果把步骤④加进去呢?奇数张牌的朝向不变,但是偶数张牌的朝向变了。
如果再把步骤⑧⑨加进去呢?⑧是将奇数的牌和偶数的牌分成两叠,⑨是将其中一叠翻过来。
如果观众选择的是偶数牌叠,那么就会和步骤④互相抵消,牌的朝向又还原了。
如果观众选择的是奇数牌叠,和步骤④合起来相当于把所有牌翻了过来,那么红蓝牌的朝向依然不同。
如果把步骤⑤加进去呢?切牌就是将牌分成两叠再交换位置。
更多切牌的知识可以看这篇文章:《数学与魔术:心灵支配配对实验》。
如果切的两叠牌都是偶数张,那么牌的奇偶性不变,原来奇数位置的牌依然在奇数的位置。
如果切的两叠牌都是奇数张,那么偶数张牌和奇数张牌的位置互换了,虽然原来的奇数张牌变成了偶数张牌,但是牌的朝向依然不变,所以对魔术的结果依然没影响。
现在只剩下第⑥个步骤了,观众随机的选择,会让牌的朝向发生变化,这是最能迷惑人的地方。
因为做完步骤②后,魔术是可以结束的,也就是说牌的朝向都是正确的,可以将10张牌的朝向记为:
1 | a = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] |
注意,数组记录的是牌正确的朝向,而不是牌的正反面。红色的牌背面朝上,黑色的牌正面朝上,都是正确的朝向,所以都记为1
。
做完第⑤个步骤后,奇数张牌和偶数张牌朝向不同,如果将错误的朝向记为0
,那么这10张牌的朝向可以表示为(下标0
为牌顶):
1 | a = [1, 0, 1, 0, 1, 0, 1, 0, 1, 0] |
假如现在开始做步骤⑥了,魔术师将牌分成两部分,顶部4张牌和底部6张牌:
1 | top = [a[0], a[1], a[2], a[3]] = [1, 0, 1, 0] |
如果观众选择将顶部4张牌翻面,底部6张牌不翻面,那么结果为:
1 | # top是先翻面放下去的,bottom就成了牌顶 |
可见奇数张牌和偶数张牌的朝向依然不变,如果观众选择顶部4张牌不翻面,底部6张牌翻面,结果也是一样的。
如果魔术师将顶部3张翻面和底部7张不翻面的话,会导致结果异常:
1 | a = [a[3], a[4], a[5], a[6], a[7], a[8], a[9], !a[2], !a[1], !a[0]] |
视频里也可以看出魔术师有数牌的痕迹,每次数2
、4
或6
张,所以魔术师只要保证每次拿的牌是偶数就行了,无论观众怎么选择,牌的朝向都不会改变。
点击链接查看第7集的魔术表演:《数学与魔术:心灵支配配对实验》
切牌是指将一副牌任意分成两部分,再将两部分牌进行交换,切牌后牌的相对顺序依然保持不变。
比如有5张牌,顺序为1 2 3 4 5
,如果把牌看成首尾相接的圆环的话,那么5
的下一张牌就是1
。
将牌顶的两张牌切到牌底,顺序变成了3 4 5 1 2
,可以看到5
的下一张牌还是1
。
无论切了多少次牌,牌的相对顺序依然不变。
魔术师将ESP卡片分成两叠,每叠有5张卡片,让观众把4个筹码任意放在两叠卡片上,每叠卡片有多少个筹码就从顶部拿多少张卡片到底部。
因为这是数学魔术,所以无论观众怎么放筹码,最终都会成功的。
怎样才能找出两叠卡片的相对顺序呢?假设一开始第一叠卡片翻开了,第二叠卡片的顺序还未知:
注意魔术师移动卡片时,每次都是把顶部的卡片移到底部,相当于每次都切1张牌。
那么如果第一叠卡片移动3次,第二叠卡片移动1次的话,结果会变成:
由结果可知,卡片B
为波浪卡
。
而筹码总共有5种摆放方法:
1 | 第一叠卡片的筹码数:0 1 2 3 4 |
只要把这5种情况都试一遍,就能得出第二叠卡片的排列顺序:
可见第一叠卡片和第二叠卡片的顺序相反。
为什么两叠卡片的顺序相反时有这个特性呢?
假设每叠卡片的数量为n
,筹码数是n-1
。
如果第一叠卡片放了k
个筹码,则移动k
张卡片后,卡片顶部是第k
张(下标从0开始)。
第二叠卡片移动了n-1-k
次,卡片顶部是第n-1-k
张。
由于两叠卡片顺序相反,那么第一叠卡片的第i
张和第二叠卡片的第n-1-i
张相同(下标从0开始)。
所以第一叠卡片的第k
张和第二叠卡片的第n-1-k
张相同。
因为整个过程都是通过切牌的方式移动卡片的,所以当拿走顶部的卡片后,剩下的卡片的相对顺序不变,两叠卡片依然是逆序的,因此能够继续表演下去。
一开始魔术师随便切了几次牌,观众也切过了牌,魔术师再从顶部一张一张数了5张卡片到桌子上形成第一叠卡片,剩下的卡片直接放在桌子上形成第二叠卡片。
这时桌子上两叠卡片的顺序是相反的,也就是说一开始两叠卡片的顺序是相同的,第一叠卡片在数牌的时候顺序反过来了。
表演前只需把两叠卡片按相同的顺序排序叠在一起就可以了。
]]>最近使用Mac版QQ时弹出了一个版本更新提示,说最新版v5.1.2
正式版已在官网发布,于是我便更新了。
不过更新完我就后悔了,因为我发现5.x
版本有些功能用不了:
1、不能选择忙碌状态:
要知道,我每次上QQ都会把状态设为忙碌,这样才能假装成很忙的样子。
2、不能发送本地图片,只能发送文件:
从上图可以看到,发送图片的按钮不见了。虽然可以把图片拖到聊天窗口发送,但操作还是感觉很麻烦。
看来,我只能自己给给QQ加上这两个功能了。
用Interface Inspector附加QQ的进程,然后定位到选择状态的按钮,如下图所示:
可以很明显地看出,菜单列表里有一个忙碌
的菜单项,后面有个划了一条斜线的眼睛,表示该菜单项是隐藏状态。
因此可以猜想程序员只是把该菜单项隐藏了,具体作用应该还保留着,如果将该菜单项设置为不隐藏的话,那么就很有可能可以使用忙碌的功能了。
那么怎样才能获取到这个菜单列表呢?首先我们可以看到状态按钮的类名是OnlineStateImagePopUpButton
,父类为NSPopUpButton
,查看NSPopUpButton
类的头文件,可以发现该类有一个itemArray
的属性,能够获取到菜单列表:
1 | @interface NSPopUpButton : NSButton |
也就是说,如果能获取到状态按钮对象的话,就能获取到菜单列表了。由图片可知,状态按钮在MQAIOSelfInfoViewController2
视图控制器里。
用class-dump
获取QQ的头文件,找到MQAIOSelfInfoViewController2.h
,部分内容如下:
1 | @interface MQAIOSelfInfoViewController2 : NSViewController <NSMenuDelegate> |
可以看到该视图控制器有一个_statusPopUpButton
属性,看名字基本可以肯定是状态按钮了。
那么我们可以hook该视图控制器的viewDidLoad
方法,这时状态按钮已经创建完毕,通过遍历状态按钮的菜单列表,将每个菜单的hidden
属性设为NO
。
按照《使用EasySIMBL为Mac应用加载插件》教程里的方法安装EasySIMBL模板,然后用Xcode新建一个EasySIMBL插件工程,工程名为QQPlugin
:
然后hook-[MQAIOSelfInfoViewController2 viewDidLoad]
方法,代码如下所示:
1 | @implementation NSObject (MQAIOSelfInfoViewController2BusyStatus) |
编译后重新运行QQ,已经可以看到忙碌状态菜单了:
选择忙碌状态后,登录另一个QQ查看我当前的状态,确实变成忙碌了:
用Interface Inspector
定位到聊天框的工具栏,一共有7个按钮,这些按钮都没有设置隐藏属性:
可以看到聊天界面的工具栏也是7个按钮:
也就是说,如果要增加发送图片的按钮的话,就只能自己创建一个新的按钮,再加到工具栏上面了。
至于点击按钮会调用什么方法,可以通过逆向旧版本的QQ来获得。下面介绍一种方法,可以比较快速获取到按钮调用的方法:
首先下载4.x
版本的QQ,然后打开插件工程,按command + shift + ,快捷键打开Edit scheme
界面,Executable
那一项选择4.x
版本QQ的路径:
然后再按command + R运行插件工程,Xcode会自动运行QQ。
登录QQ后随便进入一个聊天窗口,接着在Xcode的debug工具栏里点击界面调试按钮:
当视图层次界面加载完毕之后,在层次界面里选中QQ聊天框工具栏的发送图片按钮:
在Xcode的右侧边栏可以看到该按钮的属性:
可以看到按钮调用的方法是-[MQAIOChatTootKitViewController onPicture:]
。
查看MQAIOChatTootKitViewController.h
文件,可以看到如下方法:
1 | @interface MQAIOChatTootKitViewController : NSViewController |
可以猜想pictureBtn
就是发送图片的按钮。
在5.x
版QQ的头文件里也可以看到这些方法,把5.x
版本QQ的可执行文件拖到Hopper里分析,然后查看-[MQAIOChatTootKitViewController pictureBtn]
方法的伪代码:
1 | void * -[MQAIOChatTootKitViewController pictureBtn](void * self, void * _cmd) { |
可以看到该方法用懒加载的方式创建了一个按钮,也就是说代码还是保留的,只是没使用而已。那么我们可以先看其它按钮是怎么创建的,就看历史记录按钮好了。
按照《Hopper Disassembler批量导出反编译的伪代码》里的方法,反编译出MQAIOChatTootKitViewController
类所有方法的伪代码。
打开~/ClassDecompiles/QQ/MQAIOChatTootKitViewController.m
文件,搜索historyBtn
,可以很快发现以下代码:
1 | void -[MQAIOChatTootKitViewController setupUI:](void * self, void * _cmd, int arg2) { |
可以看到这些按钮是用自动布局的方法写的,因此可以考虑hook-[MQAIOChatTootKitViewController setupUI:]
方法,在这些按钮添加完毕后,把发送图片的按钮加到最后面。
要先知道前面按钮的数量,才能计算出最后一个按钮的位置。考虑到不同的聊天框可能会出现不同数量的按钮,所以不能写死7个。那么怎么才能获取到这些按钮的数量呢?
看一下前面出现过的图片:
可以看到这些按钮都是加在MQEventForwardView
类的实例对象里的,在MQAIOChatTootKitViewController.m
文件里搜索MQEventForwardView
,可以发现以下代码:
1 | void * -[MQAIOChatTootKitViewController init](void * self, void * _cmd) { |
也就是说,当试图控制器初始化的时候,就将MQEventForwardView
实例对象赋值给了view
属性。那么使用self.view.subviews.count
方法就能获取到按钮的数量了。
最后的示例代码如下:
1 | @implementation NSObject (MQAIOChatTootKitViewControllerSendPicture) |
编译后重启QQ,可以看到出现了发送图片按钮:
点击后也确实可以发送图片了。
插件工程可以在QQPlugin下载。
很惭愧,就做了一点微小的工作,谢谢大家。
]]>不过一点击 高级 菜单按钮,就会提示让我购买游戏,贫穷的我只能逆向一下这个app。
首先运行Interface Inspector,这是一个可以查看Mac应用界面元素结构和属性的软件,功能非常强大,运行后附加上扫雷的进程:
然后展开菜单 游戏 –> 新游戏 –> 高级 ,在右边侧边栏可以看到 高级 菜单栏对象的内存地址是0x101331130
:
接下来用lldb附加扫雷的进程:
1 | Jobs: ~$ lldb -n "Minesweeper Deluxe" |
然后使用以下命令获取点击菜单按钮时调用的方法名:
1 | (lldb) po 0x101331130 |
由此可知,点击菜单按钮会调用-[minesweepermacAppDelegate startNewGameExpert:]
方法,用Hopper可以看到该方法的伪代码为:
1 | void -[minesweepermacAppDelegate startNewGameExpert:](void * self, void * _cmd, void * arg2) { |
由代码可知,当-[GameState fullgame]
方法的返回值为YES
的时候,才可以玩高级级别的游戏。
所以可以编写一个插件来hook这个方法,强制返回YES
值。
按照《使用EasySIMBL为Mac应用加载插件》教程里的方法安装EasySIMBL模板,然后用Xcode新建一个EasySIMBL插件工程,工程名为MinesweeperPlugin
:
然后hook-[GameState fullgame]
方法,代码如下所示:
1 | @implementation NSObject (GameStateHook) |
编译代码后重新运行游戏,就可以进入高级级别游戏了。
扫雷的菜单里还有一个购买安全帽的功能:
在帮助里可以看到安全帽的说明:
使用安全帽
==========
使用 “Alt / Option + 左键点击” 可以保证安全。如果方块下面是雷会被插棋,如果下面不是雷会正常打开。每次会用掉一个安全帽。游戏中随时按下 “Alt / Option“ 键可以看安全帽数量。安全帽用完了可以在商店菜单中添加,也可以通过分享你的成绩获得免费安全帽。
虽然我玩扫雷时不会使用这种作弊的功能,但是在某些情况下还是有用的。比如有时玩到最后会出现2选1的情况,这时如果靠运气点到地雷就太亏了,所以安全帽可以在这种情况下使用。那么顺便把安全帽的数量修改成无限吧。
同理,在Interface Inspector
里查看购买5个安全帽的菜单地址:
再获取菜单调用的方法名:
1 | (lldb) po 0x10112c2c0 |
在Hopper里查看-[minesweepermacAppDelegate buy5Robot:]
方法的伪代码:
1 | void -[minesweepermacAppDelegate buy5Robot:](void * self, void * _cmd, void * arg2) { |
再查看-[InAppPurchaseManager purchase5robot]
方法的伪代码:
1 | void -[InAppPurchaseManager purchase5robot](void * self, void * _cmd) { |
从代码可知,购买的产品的id是com.sg.minesweepermac.robot5
。
由《一种应用内付费(iap)的破解方法》可知,内购的回调方法为paymentQueue:updatedTransactions:
,在Hopper里搜一下这个方法,可以发现这个方法在InAppPurchaseManager
类里。
接下来使用《Hopper Disassembler批量导出反编译的伪代码》里的方法,反编译出InAppPurchaseManager
类所有方法的伪代码。
打开~/ClassDecompiles/Minesweeper Deluxe/InAppPurchaseManager.m
文件,搜索com.sg.minesweepermac.robot5
,可以很快发现以下代码:
1 | - (void)provideContent:(id)arg2 |
可以发现,购买成功后通过不同的产品id来增加不同的安全帽数量,然后通过-[GameState setRobot:]
方法保存安全帽的数量。
也就是说,属性robot
储存了安全帽的数量,如果hook了-[GameState robot]
方法,返回999个安全帽的话,就有用不完的安全帽了。
参考代码如下:
1 | @implementation NSObject (GameStateHook) |
具体工程代码可以在MinesweeperPlugin下载。
编译工程后,重新运行扫雷,安全帽的数量也变成了999个了:
逆向工程最受欢迎的应用场合就是“借鉴”他人的软件功能。
在App Store里面,有不少优秀的App。当我们不知道App中的某个功能是如何实现的时候,逆向工程就能起到关键性的作用,此时使用iOS逆向工程技术就能够对它了解一二。
有些老牌软件的架构设计合理,代码工整规范,实现得非常优雅。我们没有他们那样深厚的技术功底和人才储备,想要借鉴他们使用的高级技术,却又求学无门。在这种情况下,逆向工程就是解决问题的金钥匙。通过逆向那些软件,可以从App中把它们的设计思路抽象出来为我所用,从而提高自己App的精致程度。比如,WhatsApp的稳定性、健壮性出类拔萃,如果我们自己要编写一个IM类App,通过逆向工程技术学习WhatsApp的整体架构与设计思路将是非常有益的。
想要了解app的功能是怎么实现的,最简单的方法就是反编译了。
所幸的是,Hopper Disassembler提供了反编译功能,能够将汇编代码转成伪代码。
想要使用这个功能的话,要先把光标定位到某个函数的汇编代码里:
然后点击菜单的 Window –> Show Pseudo Code Of Procedure 选项,就会弹出一个伪代码窗口:
这个功能虽然强大,但是每次只能反编译一个函数,并不支持批量生成伪代码。
不过Hopper内置了一个Python解析器(这背后一定有肮脏的Python交易),所以我们可以编写Python脚本来实现这个功能。
打开Hopper的帮助文件/Applications/Hopper Disassembler v3.app/Contents/Resources/Hopper.help
,会出现一个窗口:
点击Scripting Reference
选项,可以看到Hopper提供的类和方法:
1 | Class Tag |
搜索关键字Pseudo
,可以发现以反编译的方法:
1 | decompile() |
既然Hopper提供了这个方法,那么实现批量导出伪代码的功能就不难了。
由于代码比较长,所以放在Github里,具体代码可以在https://github.com/poboke/Class-Decompile下载。
1、将下载的Class Decompile.py
文件放到~/Library/Application Support/Hopper/Scripts
目录里。
2、将可执行文件拖到Hopper里,等待分析完成。如果日志框里出现以下文字,就说明分析完成了:
Analysis segment __LINKEDIT
Analysis segment External Symbols
Background analysis ended
3、点击菜单 Scripts –> Class Decompile :
4、Hopper会出现一个弹框,可以选择反编译类型:
5、如果选择反编译单个类的话,会出现以下弹框:
输入某个类名后,点击 OK 按钮就可以反编译出该类的伪代码。
6、反编译出来的伪代码保存在~/ClassDecompiles
目录里。
7、打开反编译的文件,例如CalculatorController.m
,可以看到生成的伪代码:
1 | @implementation CalculatorController |
底部圆形的logo在不停地跳动,点击一下,居然领到了一分钱:
如果不断摇一摇再点击logo,就可以领到很多一分钱。不过手动操作太麻烦了,所以写了个tweak
插件来自动领取。
插件要做的操作大概如下:
当前的微信版本是6.3.10
,iOS设备是越狱后的iPad mini 2
。
这个插件的功能算是很简单的,下面就说一下编写插件的过程:
使用class-dump可以获取微信的头文件。
解压微信.ipa
,将Payload/WeChat.app/WeChat
这个可执行文件拷贝出来,然后在终端执行命令:
1 | class-dump -sSH WeChat -o WeChatHeaders |
在生成的WeChatHeaders
文件夹里可以看到微信的所有类的头文件:
1 | . |
首先进入摇一摇界面:
使用Reveal查看视图控制器的类名:
然后在微信头文件里找到NewYearShakeViewController.h
,搜索关键字shake
,可以搜到一个方法:- (void)OnShake;
。
用电脑打开终端远程连接到iPad,再用Cycript附加微信,调用OnShake
方法:
1 | Jobs: ~$ ssh root@remoteip |
执行命令后界面果然自动摇一摇了,并且跳到了红包详情界面。
在红包详情界面,用Reveal可以看到logo是一个按钮:
在Cycript中使用以下方法可以获取点击按钮时调用的方法:
1 | button = #0x1468358f0 |
可以看到,点击按钮时会调用[NewYearShakeInteractiveLogoView onClickLogoButton:]
方法。
可以在NewYearShakeInteractiveLogoView
初始化后调用点击按钮的方法,就可以实现自动领取了。
这个方法需要传入按钮对象作为参数,在NewYearShakeInteractiveLogoView.h
里可以看到按钮是一个成员变量ImagesAnimationButton *m_logoView;
,所以可以通过[self valueForKey:@"m_logoView"]
方法获取按钮对象。
领到钱后需要退出当前界面,才能继续重新摇一摇。
而点击按钮时会出现金币旋转的动画,然后请求网络领取RMB,领到RMB后再结束动画,也就是说动画执行的时间可能不是固定的。
如果能找到结束动画的方法的话,通过hook这个方法,就可以在动画结束后退出当前界面了。
用Hopper Disassembler
反编译微信的可执行程序,搜索NewYearShakeInteractiveLogoView onClickLogoButton
,按 alt + enter 生成伪代码:
1 | void -[NewYearShakeInteractiveLogoView onClickLogoButton:](void * self, void * _cmd, void * arg2) { |
可以看到调用了onClickLogoEvent
方法,查看该方法的伪代码:
1 | void -[NewYearShakeInteractiveLogoView onClickLogoEvent](void * self, void * _cmd) { |
伪代码里调用了startClickAnimation
方法开始执行动画,那么很可能存在一个停止执行动画的方法。
在NewYearShakeInteractiveLogoView.h
里搜索stop
,可以发现- (void)stopClickAnimation;
方法,基本上就可以确定这是停止动画时调用的方法了。
如果要在view的stopClickAnimation
方法里关闭红包界面的话,就需要通过view获取view所在的视图控制器,可以通过nextResponder
方法来获取。
最后让视图控制器对象调用dismissViewControllerAnimated:completion:
方法来退出红包界面。
使用Theos新建一个tweak插件,代码非常简单:
1 | @interface NewYearShakeViewController : UIViewController |
iPad安装插件后,重新打开微信,微信就会自动领一分钱了:
最后领到了很多一分钱,不知不觉中已经发财了:
这道题的题目是《饥饿游戏》,一进游戏果然是好莱坞大片的即视感:
可以看到场面非常宏伟壮观,而我们的英雄人物就是乔布斯
。
看一下游戏提示:
Connected
Login OK
Use [↑↓←→] ot move around, [space] is the function key.
也就是说:方向键可以移动英雄,空格键是用来触发功能的。
把英雄移动到门的旁边,按空格键可以过关。
地图里有两扇门:
关卡提示:
There is a locked door in front of you, but you don’t have the key.
走到左边的门旁边按空格键,看到提示:
Find key to open this door!
看来需要钥匙才能打开左边的门,但是地图里没看到有钥匙。
在网页源码里搜索关键字Find key
,可以在game.js
文件里发现以下代码:
1 | function onnextdoor() { |
搜一下onfackdoor()
函数,发现按空格就直接调用了,所以是找不到钥匙来开启的。
而onnextdoor()
函数的作用是向服务器发送数据进入下一关,所以在浏览器的控制台里执行onnextdoor()
即可过关。
还有一种方法是修改英雄的坐标,这样英雄就可以穿墙
了,然后走到右边的门按空格键。
地图里有两颗树:
游戏提示:
Hold [space] to cut the tree. When you get 9999 wood, a wooden pickaxe will be automatically generated.
在树的旁边按住空格键可以砍树,获得9999块木材会自动获取一根木制镐。
走到树边砍树:
Cutting Tree…
You get 3 woods,total 3
You get 5 woods,total 8
……
按住一秒会得到一个木材,手动砍树的话肯定是行不通的,看一下js文件,发现如下代码:
1 | if (level == 2 && users[heroname].x < 800) { |
tmp
变量是两次按键之间的毫秒数,所以可以轻易伪造砍了10000颗树,在浏览器控制台输入:
1 | data = JSON.stringify([msg('wood', { |
执行后游戏提示:
You get 10000 woods,total 10008
Get the wooden pickaxe!!!
走到门边通往下一关。
地图里有两堆钻石:
游戏提示:
Mine by hitting [space], When you get 9999 diamonds,the diamond sword will be automatically generated.
猛击空格键,得到9999颗钻石会获得一把钻石剑。
在游戏源码里可以发现以下代码:
1 | if (level == 3 && users[heroname].x < 800) { |
按照第三关的方法在浏览器控制台执行:
1 | data = JSON.stringify([msg('diamond', { |
游戏提示:
Mining too fast, manager kicked you out.
Connection lost
挖掘太快,被服务器踢出去了。
应该是数量太多了,经过试验,每次挖掘的最大数量是50,所以用循环执行200次就行了:
1 | for (var i = 0; i <= 200; i++) { |
执行结果:
Get 50 diamond,total 50
Get 50 diamond,total 100
Get 50 diamond,total 150
……
Get 50 diamond,total 9950
Get 50 diamond,total 10000
Get the diamond sword!!!
通过门可以通向下一关。
终于要打BOSS了:
游戏提示:
At last, the final level! Wave your diamond sword and beat the BOSS.
PS: Short-range weapons can only hurt other players(your hp +1) but cannot harm the BOSS.
PS: Cause 15 point damages to the BOSS to get the flag
PS: Or kill 5 players to get the flag
近距离攻击的武器只能攻击到别的玩家,打不到远处的BOSS。攻击到别的玩家时,别的玩家的hp会减1,自己的hp会加1。
有两种过关方法,一是让BOSS掉15滴血,二是杀死5个玩家。
BOSS是有瞬移技能的,满地图顺机传送,而且隔两三秒就攻击玩家一次:
Attacked by boss
Attacked by boss
Attacked by TFBoys
Attacked by boss
英雄不但会受到BOSS攻击,还会受到其它玩家的攻击,而英雄只有10滴血,往往坚持30多秒就挂了。
地图里有个神秘的箱子和一把远程攻击的弓箭,其实这些都是骗人的,捡物品的时候会调用以下代码:
1 | function onselfkill(argument) { |
看一下按功能键时的代码:
1 | if (level == 4) { |
代码向服务器发送了一个攻击事件和英雄的当前坐标,服务器应该是判断这个坐标周围有没有攻击对象的。如果把这个坐标改成BOSS的坐标的话,那么就可以攻击到BOSS了。
查看js代码,发现有一个boss
变量,所以可以在控制台执行以下代码:
1 | function attact() { |
执行后可以得到过关的key:
]]>Attack:boss,total 1
Attack:boss,total 2
Attacked by boss
Attack:boss,total 3
Attack:boss,total 4
Attack:boss,total 5
Attacked by boss
Attack:boss,total 6
Attack:boss,total 7
Attacked by boss
Attack:boss,total 8
Attack:boss,total 9
Attack:boss,total 10
Attacked by boss
Attack:boss,total 11
Attack:boss,total 12
Attack:boss,total 13
Attacked by boss
Attack:boss,total 14
Attack:boss,total 15
SSCTF{2b3d41dd4b7911dc0fe683d1a0d977ef}
我的电脑里以前装过WiFi万能钥匙,打开一看,发现有可以连接的热点:
先用手机开启一个热点给电脑使用,再打开Charles拦截电脑数据包。
在WiFi列表里选中TP-LINK_FB2BBE
这一列,然后点击 自动连接 按钮,可以截取到以下数据:
1 | { |
由结果可以看出,pwd
字段很有可能就是加密后的密码。如果能破解出原始密码的话,那么手机就可以直接连接WiFi了。
以下就是破解密码的过程:
先按照《使用EasySIMBL为Mac应用加载插件》教程里的方法安装EasySIMBL模板,然后用Xcode新建一个EasySIMBL插件工程,工程名为WifiMasterKeyPlugin
,再将初始化代码改为:
1 | + (instancetype)sharedInstance |
上面的代码能够获取到创建视图的通知,根据通知可以打印出视图的类名。
编译工程后,打开控制台应用,重新运行WiFi万能钥匙,然后点击TP-LINK_FB2BBE
这一列,在控制台里可以看到输出的log:
1 | WiFiMasterKey[30976]: view : <NSTableView: 0x7f8fa3dc3810>, frame : NSRect: {{0, 0}, {537, 862}} |
可以看到创建了一个<WiFiTableSelectedCellView: 0x7f8fa6134df0>
,看类名应该是当前选中的CellView,CellView对象的内存地址为0x7f8fa6134df0
。
接下来用Xcode来动态调试WiFi万能钥匙,点击Xcode的菜单 Debug –> Attach to Process ,选择WiFiMasterKey
进程。
等待进程附加完毕,点击下面的按钮暂停应用:
然后在调试框里输入以下命令打印出CellView的子视图:
(注意:pviews
命令需要先安装chisel才能使用。)
1 | (lldb) po 0x7f8fa6134df0 |
由结果可知, 自动连接 按钮对象的内存地址是0x7f8fa3daf760
,用以下命令可以得到点击按钮时调用的方法名:
1 | (lldb) po [0x7f8fa3daf760 target] |
也就是说,点击按钮时会调用[WiFiTableSelectedCellView autoConnectButtonAction:]
方法。
接下来用动态调试来分析比较麻烦,可以采用静态分析的方法。
将WiFi万能钥匙的可执行文件拖到Hopper Disassembler里进行分析,等待分析完毕后搜索WiFiTableSelectedCellView autoConnectButtonAction:
方法,再按 alt + enter 组合键查看反汇编伪代码,可得到以下结果:
1 | void -[WiFiTableSelectedCellView autoConnectButtonAction:](void * self, void * _cmd, void * arg2) { |
查看queryWiFiMasterKey
方法的伪代码:
1 | void -[WiFiTableSelectedCellView queryWiFiMasterKey](void * self, void * _cmd) { |
再查看queryWiFiMasterKeyFromServer
方法的伪代码:
1 | void -[WiFiTableSelectedCellView queryWiFiMasterKeyFromServer](void * self, void * _cmd) { |
也就是说,点击 自动连接 按钮,会调用[WiFiMasterKeyService queryMasterKey:tag:userInfo:scanWiFiType:success:failure:]
方法向服务器查询加密后的密码,数据请求成功的回调方法是[WiFiTableSelectedCellView queryWiFiMasterKeyFromServer]_block_invoke
,这个block的伪代码是:
1 | void ___57-[WiFiTableSelectedCellView queryWiFiMasterKeyFromServer]_block_invoke(int arg0) { |
最后通过[WiFiTableSelectedCellView parserScanWiFiResult:]
方法来解析返回的数据,所以解密WiFi密码的代码很有可能就在这个方法里。
由一开始可知,加密后的密码字段名是pwd
,因此在parserScanWiFiResult:
方法的伪代码里可以很快发现以下代码:
1 | void -[WiFiTableSelectedCellView parserScanWiFiResult:](void * self, void * _cmd, void * arg2) { |
原来是通过[WiFiKeyAESUtilties ShareKeyAES128Decry:]
方法解密出密码。
因此我们可以通过hook这个方法,把解密后的密码用弹出框显示出来,示例代码如下:
1 | @implementation NSObject (WiFiKeyAESUtiltiesHook) |
具体工程代码可以在WifiMasterKeyPlugin下载。
编译工程后,重新运行WiFi万能钥匙,点击 自动连接 按钮,可以看到弹出一个提示框:
用户想给应用增加功能的话,最快捷的方法就是给应用写插件打补丁。
EasySIMBL(Easy SIMple Bundle Loader)
是macOS
系统的一个工具,能够很方便地把插件注入到目标应用里,从而使应用能按我们想要的方式运行。
EasySIMBL
需要安装到/System/Library/ScriptingAdditions/
路径,安装过程如下:
由于macOS X 10.11
启用了SIP,用户没有权限修改系统目录下的文件,因此安装EasySIMBL
之前要先关闭SIP。
首先重启系统,开机时按住 command + R 不放,直到进入系统恢复模式。
然后点击 实用工具 菜单,打开终端,执行csrutil disable
命令关闭SIP,再重启系统。
点击SIMBL-0.9.9.zip下载文件并解压。
然后打开终端,cd到SIMBL-0.9.9
目录,执行以下命令进行安装:
1 | sudo installer -verbose -pkg SIMBL-0.9.9.pkg -target / |
安装完可以启用SIP,对系统进行保护。
进入系统恢复模式,执行csrutil enable --without debug
,并重启系统。
加上--without debug
参数,才可以让lldb附加进程进行调试。
将插件放在/Library/Application Support/SIMBL/Plugins
目录里,应用启动时会自动加载插件。
以系统自带的计算器应用/Applications/Calculator.app
为例编写一个插件。
1、安装插件模板,地址为 https://github.com/poboke/EasySIMBL-Bundle-Template。
2、使用Xcode创建一个Project,选择SIMBL Bundle
模板。
3、设置工程名字为CalculatorPlugin
。
打开/Applications/Calculator.app/Contents/Info.plist
,可以看到计算器的bundle identifier
为com.apple.calculator
:
下图里的Target App Bundle Id
就是我们要注入的app的包标识符,当app启动时,SIMBL就会把填有该app包标识符的插件注入到该app中。
填写后的工程信息为:
4、工程默认的初始化代码为:
1 |
|
加载插件时会先执行类方法load
的代码,因此可以在该方法里编写初始化的代码。
5、编辑插件的Info.plist
文件,可以看到里面有一个SIMBLTargetApplications
数组:
字段的含义为:
BundleIdentifier
: 要注入的app的包标识符。MaxBundleVersion
: 要注入的app的版本号最大值,默认为99999。MinBundleVersion
: 要注入的app的版本号最小值,默认为0。版本号是指CFBundleVersion
的值:
由计算器的信息可知,目前计算器的版本号是123
,而非10.8
。
处于MinBundleVersion
和MaxBundleVersion
版本号之间的app才能够加载插件。
因为app升级后可能导致插件失效,严重的话可能会导致app闪退。所以为了保证插件能够正常工作,可以使用app当前版本号的值,也就是最大值和最小值都填上123
。
不过这种情况是很少出现的,一般来说使用模板默认的0 ~ 99999
就可以了。
6、工程编译成功后会在/Library/Application Support/SIMBL/Plugins
目录下生成一个CalculatorPlugin.bundle
插件。
7、先运行控制台Console.app
,再运行计算器Calculator.app
。
如果在控制台里能看到++++++++ <CalculatorPlugin: 0x7f980488e970> plugin loaded ++++++++
等字样,就说明我们的插件已经成功注入到计算器里了。
8、接下来就可以使用逆向工程的方法来修改app的逻辑了。
插件模板 https://github.com/poboke/EasySIMBL-Bundle-Template 的Samples
目录里有一个CalculatorPlugin
示例工程。
编译该工程后,点击计算器菜单栏的 计算器 –> 关于计算器 时,会出现一个Hello world
的弹出框:
感兴趣的话可以去看工程里的代码,由于代码比较简单,这里就不多费笔墨了。
使用图形化软件可以更方便地管理插件,mySIMBL是一个开源的macOS插件管理器,它不但可以很方便地安装SIMBL,而且还自带了插件列表,能够下载其他用户分享的插件。
点击这里下载最新的安装包,下载后解压,将mySIMBL.app
移动到应用程序
里。
第一次运行时,会提示用户安装SIMBL,按照上面的方法关闭SIP
,并安装EasySIMBL
。
运行mySIMBL.app
后便可看到本地的插件列表:
Manage
菜单里可以管理本地的插件,把插件拖到插件列表里可以安装插件。点击状态图标可以开启/禁用插件,绿色图标为开启,红色为禁用。Discover
菜单里可以下载其他用户分享的插件。Updates
菜单里可以更新其他用户分享的插件。SIMBL
菜单里可以把某些应用加入黑名单,这些应用启动时就不会加载任何插件。如果你想把自己写的插件分享给别人使用,可以把插件提交到mySIMBL
的插件仓库里,仓库地址为:https://github.com/w0lfschild/macplugins。
首先Fork该插件仓库,将你编译好的插件压缩为zip
格式,放到仓库的bundles
目录里。
编辑packages_v2.plist
文件填写插件信息,再提交Pull Request
就可以啦。
1、这道题的答案是:
A.A B.B C.C D.D2、第5题的答案是:
A.C B.D C.A D.B3、以下选项中哪一题的答案与其他三项不同:
A.第3题 B.第6题 C.第2题 D.第4题4、以下选项中哪两题的答案相同:
A.第1,5题 B.第2,7题 C.第1,9题 D.第6,10题5、以下选项中哪一题的答案与本题相同:
A.第8题 B.第4题 C.第9题 D.第7题6、以下选项中哪两题的答案与第8题相同:
A.第2,4题 B.第1,6题 C.第3,10题 D.第5,9题7、在此十道题中,被选中次数最少的选项字母为:
A.C B.B C.A D.D8、以下选项中哪一题的答案与第1题的答案在字母表中不相邻:
A.第7题 B.第5题 C.第2题 D.第10题9、已知“第1题与第6题的答案相同”与“第X题与第5题的答案相同”的真假性相反,那么X为:
A.第6题 B.第10题 C.第2题 D.第9题10、在此十道题中,ABCD四个字母出现次数最多者与最少者的差为:
A.3 B.2 C.4 D.1这10道题的答案为: 。
以下是用python代码写的求解算法:
1 | #coding:utf-8 |
执行结果为:BCACACDABA
最近Atom编辑器又出了一个插件:atom-miku,装上这个插件后编辑器会出现一个程序员鼓励师Miku,敲代码时Miku会唱歌和跳舞,停止敲代码时Miku的动作就慢了下来,简直是宅男的福音啊,效果如下:
据说有人用了,而且带上耳机听背景音乐,结果第二天就被炒了,理由是上班看视频。
下载查看atom-miku
的源码,发现插件只是在编辑器里面嵌入了一个网页,网址为http://miku-dancing.coding.io
。
当网页加载完毕时,会免费赠送10秒钟的播放时间,如果播放时间消耗完的话,Miku的动作就会变慢,音乐的音量也会变小。这时如果执行js代码control.addFrame(seconds)
方法的话,播放时间就会增加,Miku又重新复活了。
可以说,网页里已经实现了大部分功能,如果要移植到Xcode的话,只需写出以下逻辑就行了:
addFrame()
方法来增加播放时间。于是我便模仿着写了个Xcode
的插件,可以在Alcatraz
上搜索Miku
进行安装:
插件代码下载地址为:https://github.com/poboke/Miku
]]>自从公司启用代码review以来,每个开发者的代码风格渐渐保持一致了,看到规范统一的代码就是觉得比较舒服。
不过Xcode的代码注释功能一直用着很别扭,比如下面的例子:
1 | //注释前: |
可以看到,Xcode只是在行开头加上注释符,注释后的代码缩进对不齐,很难看。这对患有神经感官歇斯底里毛细血管穿梭图鲁西斯症候群(简称强迫症)的人来说,是一件非常痛苦的事情。
在stackoverflow搜了一下,发现有人也提了这个问题,然后有个人回答说:
- 你先按
command + [
把代码往左缩进到最前面- 再按
command + /
注释代码- 最后按
command + ]
把代码往右缩进
结果这条回答被采纳为最差答案。
所以我只能自己写个插件来让注释自动缩进了,首先新建一个Xcode插件工程,比较详细的创建过程可以见《Xcode插件AllTargets开发教程》。
一开始要先整理一下写插件的思路,我们使用快捷键command + /
的时候,Xcode肯定会调用一个方法来注释代码,这个方法实现的功能应该是在代码行最前面加上//
注释符。
而当我们写的代码有缩进的话,缩进的地方都是空格,所以我们可以hook注释的方法,把注释符改为插入到第一个非空格字符前就行了。
接下来就开始查找代码注释的方法了,首先在Xcode的私有类头文件里搜索comment
,可以发现一个比较可疑的方法:- (void)commentAndUncommentCurrentLines:(id)arg1;
,hook这个方法,把参数arg1
打印出来,参考代码如下:
1 |
|
使用command + /
对代码进行注释和反注释,输出结果为:
<NSMenuItem: 0x7f9ec494c8f0 Comment Selection>
<NSMenuItem: 0x7f9ec494c8f0 Uncomment Selection>
由此可知,使用快捷键注释,实际上是调用了Xcode菜单栏点击的方法。而当我们注释代码时,会有log打印,也说明找对了方法。
commentAndUncommentCurrentLines:
这个方法位于DVTSourceTextView
类中,而这个类位于DVTKit.framework
里,接下来打开反汇编软件Hopper Disassembler
,把DVTKit.framework
拖进去进行反汇编。
搜索并选中commentAndUncommentCurrentLines:
方法,按alt + enter
生成伪代码,由于代码太长所以只贴出关键代码:
1 | void -[DVTSourceTextView commentAndUncommentCurrentLines:](void * self, void * _cmd, void * arg2) |
由上面的代码可知,stringByTogglingCommentsInLineRange:
方法返回了注释或反注释后的字符串,接着再搜索并查看该方法的伪代码:
1 | void * -[DVTSourceLanguageService stringByTogglingCommentsInLineRange:](void * self, void * _cmd, struct _NSRange arg2) |
同理,继续查看stringByCommentingString:
方法的伪代码:
1 | void * -[DVTSourceLanguageService stringByCommentingString:](void * self, void * _cmd, void * arg2) |
这个方法的作用是把待注释的代码加上注释符进行注释,这也是Xcode注释代码的具体实现方法,将伪代码翻译成OC代码为:
1 | - (NSString *)stringByCommentingString:(NSString *)commentingString |
因此我们可以hook这个方法,在第一个非空格字符的位置插入注释符。
如果某一行代码是空行的话,就不需要注释了,我看了大部分的编辑器都是这么处理的。
参考代码如下:
1 | - (NSString *)hook_stringByCommentingString:(NSString *)commentingString |
安装插件后,使用效果如下所示:
插件代码下载地址为:https://github.com/poboke/IndentComments
]]>最近微博上在流传一个Atom编辑器的插件:activate-power-mode,装上这个插件后打字会有震屏和火花效果,非常牛逼,效果如下:
据说有人用了,并且还是机械键盘,差点被同事打断手了。
于是我花了几天的下班时间,写了个Xcode版的插件,模仿了这个效果:
插件可以在Alcatraz上搜索ActivatePowerMode
进行安装。
插件代码下载地址为:https://github.com/poboke/ActivatePowerMode
这些功能实现起来也不难,主要是获取光标所在位置的代码颜色花了比较多时间。
我一开始以为代码高亮的颜色是由NSAttributedString
控制的,但是我获取到的属性里只有字体字号等属性,没有NSForegroundColorAttributeName
这个字段,所以只能用别的方法寻找。
用逆向思维思考一下,因为代码高亮是由配色方案管理的,切换配色方案时,代码颜色就会改变。而配色方案是根据单词的类型来设置颜色的,所以猜想可能存在某个方法,可以读取或设置某个范围的文字的颜色,这样才方便配色方案功能的实现。
先用关键字color
在Xcode的私有类头文件里搜索,把搜到的方法名输出到一个文本里。然后再用关键字NSRange
搜索,很快就发现了一个可疑的方法:- (id)colorAtCharacterIndex:(unsigned long long)arg1 effectiveRange:(struct _NSRange *)arg2 context:(id)arg3
。然后再hook这个方法,果然返回了相应的颜色。
代码编辑框一般都是用NSTextView
来实现的,所以要找到NSTextView
的代理方法。
我之前写过一篇文章《Xcode插件AllTargets开发教程》,按照里面的方法,可以找到代码编辑区域视图的类名为IDESourceCodeEditorContainerView
。
该类的头文件的部分代码如下:
1 | //...... |
IDESourceCodeEditor
类里面用到了NSTextViewDelegate
代理,编辑文字时会调用textView:shouldChangeTextInRange:replacementString:
方法,所以可以hook这个方法。
原版的插件是用CoffeeScript
写的,震屏代码如下:
1 | shake: -> |
也就是x轴和y轴随机产生1到3像素的偏移,编辑框的原点坐标移动到这个偏移位置。
经过75毫秒后,再把编辑框的原点坐标改为(0, 0)。
在OC中可以通过修改编辑框的frame值来更改编辑框的位置,时间延迟可以使用dispatch_after
方法。
先看一下原插件的代码:
1 | spawnParticles: (range) -> |
通过spawnParticles
方法随机创建5到15个粒子,保存到粒子数组里,数组上限是500个。
使用createParticle
创建一个粒子,随机产生x轴和y轴的初始速度,y轴的初始速度越大,创建的粒子就跳得越高。
然后定时器每隔一段时间执行,粒子以加速度的方式下落,透明度逐渐减少。
由于我对Mac编程不太熟悉,所以使用了NSView
来创建粒子,不知道有没有更好的方法。
获取光标所在的位置,以便在这个位置喷出火花,花了很多时间才找到这个方法。
可以通过rectArrayForCharacterRange:withinSelectedCharacterRange:inTextContainer:rectCount:
方法来获取光标所在的位置。