暴力破解Android锁屏口令

JellyBean开始,Android的锁屏口令以hash形式存放,口令通常是4位数字(对于多位复杂口令方法也是一样的),暴力破解完全可行

锁屏口令的hash存放在/data/system/password.key,形如

1136656D5C6718C1DEFC71B431B2CB5652A8AD550E20BDCF52B00002C8DF35C963B71298

共72个字符,包含Sha1和MD5两个hash,参考Android源码

前40位是Sha1,后32位是MD5,计算(口令+salt)得到hash

salt的存放位置为/data/system/locksettings.db,使用sqlite3打开数据库,输入

就得到形如3582477098377895419的salt值了,最后将其转化为小写的16进制64位整数31b783f0b0c95dfb

有了这些信息用就可以用hashcat跑了,用MD5部分(0E20BDCF52B00002C8DF35C963B71298)爆破的指令为

 

壳里的日子(已完结)

大学三年级,我有幸参与了一个大学生创新计划项目《高强度软件防护》。当时是我们专业的指导教授roser老师找到我和同专业的几个学生,向我们介绍了这样一个计划对于我们来说是怎样一个机会。在信息对抗技术专业学习了两年多的时间,自己也发现在课程的安排上,的确更多的还是一些专业基础的内容,涉及到比较高层特别是软件方面的内容比较少。

而这一次这个项目我知道是学习信息安全技术的一个很好的机会,因此我当即就决定参与这样一个项目。我和班级内的5个人组成了一个创新小组,虽然对于软件加壳保护的内容我也没有实践过,但理论方面的内容以及软件破解的的一些调试过程我还是了解过相当一段时间的,为了能带领大家尽快进入状态,我担任了组长。

在计划开始之初和老师谈论有关计划的的时候,自信满满可以很好的体现我当时的状态,自己也不明白当时怎么会那么充满自信。小组内的成员水品参差不齐,有的MFC的程序都没怎么编过,有的连32汇编都没听说过。根据以上情况,对于整个创新计划的目的,我就明确了下来—-未必一定要设计出多么猛的软件,我可以多花一些时间等待其他组员的进度,也可以经常把我学到的东西教授给大家,但最重要的是每个人都能真正学到一些东西,多少并不重要。

创新计划开始后由于时至期末,大家都忙着考试,因此开始的阶段,计划一直处于停滞的状态,基本都是查阅一些资料而已。我一直希望能在网上找到一些例子,最好是高级语言编写的,毕竟汇编代码看起来总让人比较乏味,但这样的例子着实不多。

到了寒假,我分配给大家的任务主要还是分模块查资料,有的antidump,有的antidebug,以及反拷贝等内容,进一步了解壳的概念。因为加壳保护这个概念说起来似乎很容易,但稍微深想一下,壳代码究竟怎么样才能附加到被保护的程序上,就会让人比较疑惑了。我接触过软件破解一段时间,但真正写壳可完全是另一码事,头一次感到有点蒙。以前买过看雪的一本《加密与解密》,一直没有仔细翻阅过,这次回到家,我开始从PE文件结构看起,不过复杂的结构体看了前边忘后面,真的是有点烦,索性就跳过这部分继续跳跃性前进,中国革命的道路不也是这么走出来的吗。别说是软件保护,就是32位汇编,PE文件结构学校都没有讲过一点。软件安全,网络安全技术,或者说是安全类技术,相信很多在校学生都有感触,老师能教给你的很有限,多半是基础的过时的东西。真正要学都是靠自学的,哪个黑客是学校教出来的啊~~

经过一段时间的研究,我大致明白了软件壳保护的机制。对于常规的可执行文件(PE文件),系统都会以一个相当复杂但规整说明性极强的方式组织并存贮在硬盘上。当然运行的时候硬盘上的PE文件也会以几乎相同的格式加载到内存当中。而代码或数据以及资源这些2进制信息都会以区段的形式存放,总的来说一个PE文件包括有头部说明部分,包括文件类型说明,文件格式大小的信息,入口点信息(程序第一句要执行的代码位置),区段的偏移位置,输入函数表等,再来就是代码,可能是一段,也可能几段,然后是数据,以及资源(图片,声音,按钮,图标等都是资源)。这些内容以前也有所涉及,只是从来没有比较系统完整的全貌。而软件加壳保护,说简单了就是修改程序入口点到你写的壳代码部分(当然要添加到该PE文件空闲的部分),然后在壳代码执行完毕后再jmp回真正的入口点。这样做可以使得调试器,反汇编器等逆向分析程序的过程变得复杂。当然各种反调试代码也可以加入到壳代码中,或加入相应的定制的功能(时间限制,注册验证等内容)。对于PE文件也可以进行变换,将原来的一些说明信息,标志位等修改,在壳内再复原,这样就不能简单的跳过壳的部分了。一旦说起来,这就是一堆的生疏概念,越是向下研究,生疏的就越多。

于是我决定,先从《加密与解密》中的这个已给出的的例子开始,完整的再现一下,借此了解一下整个流程。看雪的书中给出了的例子主要是一个压缩壳,也就是对于被处理的PE文件进行信息压缩,去掉一些冗余,以比较简单的形式记录有关信息,再对各个区段进行编码实现数据体积减小。许多开源的壳的压缩都采用了APLIB这个压缩引擎,这次看雪的例子也不例外。首先是对原始文件的各项属性的记录,特别是输入表的部分,系统给出的输入表的结构虽然规整,但对于必要的信息表达并不需要如此,因此我可以自己构造一个输入表的形式来存储这些信息。对于资源中的图标,在压缩的时候要跳过,这样被处理的文件才会有图表,否则就是一个蓝边的白框,难看极了。将压缩的数据添加到壳内,然后修改壳的一些属性信息,入口点,资源大小等。壳其实还是用32位会编写的,编译也用的MASM32 来进行,但VC这个环境里提供了兼容并包的方式来组织一个工程。你完全可以在shell.asm上添加上编译环境的信息,然后以统一的VC工程形式编译整个工程。这样编译完的Shell.asm就成了一段机器码,在VC下就当他是bytes数据好了,然后修改不同位置的信息就达到了修改对应变量的目的,再将这些bytes数据添加到新生成的PE文件中就好了。新的PE文件在运行时,会运行壳代码,进行解压缩,并一步步还原原始数据,添加相关表,说明符,标志位,各区段等到内存的正确位置,然后就开始运行原来的程序代码了,壳的任务就此完成了。

MFC用起来还是很方便的,因此我将看雪原来win32的代码添加到我的MFC写的程序中,刚开始的部分,读PE文件等这样的内容,我都是每一句仔细分析研读,然后才写入我的程序中。越往后发现这样太好时间和精力了,而且也不是能简单的理解那些操作了,索性就开始从一句一句的粘贴到了一个一个函数的粘贴。这样的工作很快就接近了尾声,编译运行,选一个PE文件进行处理,然后就是运行看结果了。这与以往的程序不同,在中间的编辑过程中几乎不能停下来看看阶段性成果,只有完全编好后才能开结果,而这样的话一点出现问题就很难调试找问题了,因为之前未验证的代码量实在太庞大了。而且壳代码也无法用VC环境来调试,因此代码都是在被处理PE文件中,只能借助OLLDBG这样的第三方调试器来进行。幸运的是,这个程序编译通过后,运行完全正确。寒假也几乎结束了,经过这么一次壳的实验过程,我也大致了解了一个壳的运作编写流程了。

回到学校,和其他组员交流了一下,很遗憾其他人都没有什么进展,资料搞了一些但零零散散毫无组织,这样的资料的价值等于零。毫无组织的信息,为了能解读必须要话比资料本身理解更长的时间来分类归档,因此这些资料我至今都压在垃圾箱里。虽然很遗憾,但也多少在我的预料之中,而且我也并不感觉到太失望。毕竟,他们的基础并不怎么样。为了能让大家开始学起来,我决定让他们先编写一些小程序,熟悉一下MFC,就编一个简单文件读取的窗口程序吧。从这个为期一周的例子里,我就看到了每个人对一个程序的理解程度,借此我也强调了编程规范性的问题,特别是合作编程时接口的设定问题。并不是骄傲,也不是自负,但程序编写我真的是进行过太多了,大大小小的程序,从C到C++,MASM,C#.Net,javascript,VB等我都编过不少。所以我相信我的建议特别是这些基本的建议,应该都是比较正确的。

这是当时第一次会议的记录:
“小组开会时间:2月21日8点到9点
参加人员:WYS LSG HT WGX roser(导师)
会议内容:
这是本学期第一次小组讨论有关软件防护项目的内容,首先大家对假期资料查找学习后各自的收获进行了总结。发现的问题是,最初的分组工作十分不切合实际,将这个项目按反调试反跟踪,反dump 等空荡荡的词语进行分工,让每个人并不能很有效的了解要做的工作。
我在假期中,尝试根据书中的一个例子进行一个壳例程的编写(基本就是复制别人的代码),感受一下一个壳的大概步骤,希望能对接下来的工作起一个方向导引的作用,但相应的资料查得并不多。
WYS假期里查找了有关反跟踪的资料,虽然打印了出来,但他自己说,这资料虽然看了两次,但好像还是没有看得太懂,这当中涉及到的许多知识因为都没怎么接触过因此,效果并不明显。
LSG查了有关加密的内容,但其个人认为资料上所说大部分为概念性的内容,比如密码学上的一些思想。因此他并没有实际动手进行相关的软件上的编写测试。
HT查了反dump方面的资料,由于它之前的相关基础较好,因此对于所查资料,概括得比较明晰,同样他也在假期看了我的那本《加密与解密第三版》中的例程,并发表了相应的看法。
朱伯宇还没返校
由于一个壳的编写工作比较复杂,想分得很开比较困难,因此我们打算在编写的过程中不断进行工作的分配。
昨晚讨论的结果是,由于大部分工作环境在win32vc6.0下进行,加上大家之前也接触过一点mfc的例子的,为了让大家熟悉一下环境。第一项任务是每个人编写一个mfc下的窗口,选择文件后读取相应路径,显示文件内容的小程序。目的只有一个,熟悉工作环境,了解相应变量添加,函数调用规则等。为接下来的编写热身。
接下来的计划大致如下,由于pe文件的结构分析是一个加壳软件通常必要的过程,因此打算合力编写一个pe分析软件,借此让大家熟悉pe文件结构,在Vc下完成,但合力来写得话,我的想法是每个人首先拿到相同的一个窗口原程序,在此基础上添加比如5个窗口类函数,调用每一个函数就可以在信息栏显示一个部分的pe文件分析结果..”
于是接下大家开始编写这个读取文件的窗口了,即使是这么简单的程序,还是暴露出了甚多问题。

第二次会议记录如下:
“时间:2月28日晚上
人员:ZBY,LSG,WYS,WGX
HT有事没能参与讨论
本次讨论了之前的 mfc程序的编写,大家在显示文件内容的文本框的编写时遇到了多行显示的问题。但经过相应属性的调整,解决了该问题。同时,在对比各个人编写的程序时,也发现了大家对变量命名比较随便。因此接下来的程序编写,在程序中各个成员变量及全局变量都要采用更严格的命名规则,例如成员变量m_strpathname要能看出变量所处的域。
对比了大家的软件,最终决定采用ZBY的对话框进一步完成下面的工作。同时要制定接下来pe文件头的编写时用到的全局变量。ZBY负责将WGX和他的程序进行对比,完善之前的工作。
LSG接下来完成dos头的读,ZBY完成nt_header中imagefileheader的读,WYS完成nt_header中imageoptionalheader32的读,HT来查接下要工作要进行的部分,下一次讨论他来主持。我对各个变量进行制定,同时进行输入表的函数读取进行研究。”

革命都是以一个催化剂式事件为导火索的开始的,我们的项目在进行了这一个阶段后,似乎也有些停滞了,大家的兴趣似乎也并不高涨。我本人来说,兴趣也稍微有些下降,主要是要研究的内容实在很头疼,壳注入文件的方式我一直不能灵活掌握,这意味着我脱离不了现成的例子,独自开发。缓慢的前进步伐让我也有些反感,这从来都不是我希望发生在自己身上的。

挑战杯的到来正好充当了这一角色,为了能拿我们的项目参加挑战杯,我们决定夜以继日开始赶进度,很多基础性的核心工作都是在这一阶段完成的。大家的热情也调动起来了,周末时我们会去信息楼6002一天都在里面集体编程,这景象煞是有震慑力。后来为了不影响教室里的其他人,我们就去了创新计划同时批到的一个专属实验室。

说起这个实验室还真是有不少故事,计划开始不久,就得到了这样一个远在戊区的小实验室,20平米左右吧,初来乍到,脏乱差来形容一点不夸张。我们花了一天时间来打扫整个屋子,当时干的热火朝天的,好像从高中毕业以来就没搞过比较大型的扫除工作。回想以前的高中生活或是初中生活,搞大扫除都是比较愉快的回忆,通常这都代表一部分人可以出去自由活动,当然劳动本身做好了也会让自己心旷神怡于整洁的环境~~几个让我至今仍有很深回忆的同学我都能马上想到他们打扫卫生的样子哈~~有点跑题了。除了打扫工作,添置桌椅也是不知实验室的一部分,从远远地4号楼搬大号桌椅过来可一点也不轻松,当时我们租了两辆板车,我一直是坐在司机的位置,其他人在后面推,感觉好极了。就这样折腾了3,4回终于将一件能待人的屋子搞出来了。

通常临近周末的晚上,我们组都会在这个实验室搞到一两点钟才回宿舍。经过一番筛选,我选择了Bambam作为我们软件架构的底层支持,也就是说,我们的软件是以Bambam为核心编制的,在上面扩展相应的功能,这样可以大大缩短工程开发时间。Bambam是我在网上找到的一个用高级语言(C++)写成的壳,这样的例子真的不多,这个壳是BedRock根据BigBoot的一篇文章写成的,但由于时间比较紧迫,我也并没有话很多时间来读相应的代码(如果读懂的话应该会很有价值)。但Bambam本身是一款压缩壳,并不能很好的完成我们的anti工作,虽然他的壳本身还真是用高级语言写生成的dll加载,似乎更有优越性,但当时看来反而更不易懂了。为此我结合看雪的例子,还是采用了32位汇编来写shell部分的代码,然后混合编译。为了能搭建这一平台还真是花了不少功夫,可以说整个软件最关键的部分就是这里了。这里稍微回忆一下当时的过程,我们的操作要在Bambam处理之前进行,也就是相当于Bambam压缩了一个我们加了一层壳的软件。对于原始的Pe文件,首先由ZBY添加一个区段然后传给我一新区段的地址,并把原始入口点改为新区段的地址,然后把原始的入口点存好也传给我,这一部分的接口算是整个程序设计的最合理的一部分了。

我的工作就是要在新区段里写入壳代码,这部分我花了很长的时间才想明白究竟怎样做到,一个Shell.asm究竟如何变成机器码存入到这个区段中,我又是如何实现的修改这个asm中的变量的,本来想起来很明白的事,到了真正编程的时候却怎么也想不通该怎么让编译器完成我的要求。这个关卡我花了很久的时间才逐渐思考明白,直到今天我还记着刚刚想通时我向身边人讲解的那一幕,Incredible~~

编译器编译C代码很好理解,将它汇编成obj文件,里面就是机器码,反汇编就对应于汇编代码。当然连接后就成了带有很多说明信息的.exe可执行文件。而32汇编代码呢,汇编写好后,32位汇编器只要做很少的工作将一些宏展开,做一些处理就变成了机器码,可以送入连接器了。但两者结合起来会怎么样呢,如何用C来修改汇编代码中的变量,你可能会说申请为全局变量就好了,的确~~那如何像读取字符串一样将汇编代码读到内存再写入新的区段呢,从例子看来,似乎汇编代码就像是一个数据数组被改来改去,然后被写入到新的区段中。经过我一段时间的思考和不断地尝试(这方面的介绍太少了,特别是这些技巧性实际操作),发现了如下的事实:我们知道汇编写代码的时候可以定义一些标识符,用来做jmp,但对于真正的机器码级别来说,这些标识符就是一些地址(比如0x00400210)。在Vc环境下C代码和汇编代码同时编译(当然汇编代码的编译setting里要设置上masm32.exe的位置,和编译的参数)。但与此同时,我可以在汇编代码中设置一些标志位,而C访问这些标志位可以直接访问到汇编代码的类逻辑地址(比如0x00400210),因此我就可以在汇编代码的开始和结束设置上标志位,然后像读取字符串一样将机器码读出来。当然一旦读出了这些字符我们就可以任意修改了,不要说是某个变量,就是代码也没问题,然后再将这些内容写入到壳所在的区段里,就完成了壳代码的添加。

以上提到的过程,说起来好像就是上面短短的十几行,但我真正想到确实花了不少的时间。过了这样一个卡,我完成了壳代码添加基础,当然很多细节还没有完全处理掉。Api函数地址的添加,可移植性,以及所有的anti,当然一切都有了基础了。底层的接口或者说平台我已经搭建好了,接下来只要添加代码就好了,不再需要考虑这个代码和添加之间的关系了。

壳代码的添加就是一个漫长的过程了,当时为了赶着参加挑战杯,大家相继添加了一些代码,我当时主要利用findwindow,实现了对ollydbg的ANTI-DEBUG,也就是查找到这个窗口后,就关闭它。这里涉及到的一个关键问题,就是API函数的调用。在此之前写过的一些VC程序,用到的函数都是由MFC封装的,并没有直接看到API的使用,当然这个概念我一直是有的,API是操作系统提供给用户使用的一些函数。因为Windows并不想让你知道或者说没必要让你知道它底层的工作方式,因此提供了这样一些接口函数帮助你实现有关的功能。API也就是application interface,应用程序接口。这些函数被保存在系统的动态链接库中,比如MessageBoxA这个函数就位于user32.dll中,系统执行这个api的方式分成两种,一种是静态挂接,另一种是动态加载。汇成一句就是,系统加载动态链接库到内存,在从中找到user32.dll中的MessageBoxA后,将参数通过堆栈传过去。总体来说我看的资料还是比较少,有时实在懒得汪洋大海里四处搜寻就索性通过自己的不断实验摸索解决这些问题,api的调用就是根据一些小例子不断摸索出来的。虽然其实没什么深奥的,但没做过就是觉得有难度。

列举了几种常见的ollydbg版本的窗口名称和类名作为查找的对象,保证了大部分版本的Ollydbg都能被检测到,其实就是到刚刚我还有添加了两种新的类名作为查找的的对象。Findwindow可以对窗口名和类名进行查找,一旦找到,说明该程序在运行,就可以给该窗口发WM_DESTROY消息让他中止。另外一种反调试就是用另一个现成的API函数IsDebuggerPresent()来检测调试器的存在,虽然这个方法也比较容易对付,ollydbg对其有天生的免疫,但出于完整性考虑还是实现了这样一个功能。

为了使壳程序功能扩展更为方便,我为壳定制了一个大概的结构,开始部分为api函数动态加载,也就是加载所需要的动态链接库,然后从动态链接库中找到所需要的函数的地址存入定义好的数据单元等待以后调用。壳末尾部分,主要用于各类数据的存放,包括库函数的名称,api函数的名称等,而功能部分则主要在这两者之间进行扩展。这样一个壳的模型就搭建好了,扩展功能的话,尽量用api实现,因此查找有用的api就十分必要了。另外api名称的确定也很关键,很多资料文献并没有按照api的原始名称给出,比如MessageBoxA这函数很多文献上就写成MessageBox,但实际上并不存在这样一个函数。这里借助一个api函数查询助手的小软件实现对各类api名称的准确查询。

挑战杯展示期间,我们的作品就大致是这样一个状态了,可以压缩,内部还有我们自己写的壳,功能上有一个简单的反调试,和反拷贝功能,以及一个简单的注册认证功能。每一个功能都十分简陋。但总算是一个完整的程序了,挑战杯的校内预选赛算是相当的扯了,说是有专家来查看,等了一天也没见个人影,后来听说是来过个人但根本就不是搞这方面的人,哪看得懂啊,妄我用了那么多时间耗在那该死的挑战杯报告上,真是浪费感情,荒唐至极啊。
不过总算因为这次准备,开发的进度进展了不少,核心的一些问题解决了不少。接下来有空的时间我就不断扩充壳的功能了,首先是反调试方法的扩充,除了将IsDebuggerPresent改写为三句等效汇编代码,再来就是调用一些其他的调试器检测函数进行检测,以及int3陷阱的she检测法。扩展了这些功能的过程中,时不时也出现了一些小问题,比如编写时,VC注入代码时,我发现计算的汇编代码的偏移量并不正确,只能是重新编译几次,又调整了几次汇编代码的位置才好。到现在也还是不太清楚,什么原因导致的这样的偏移量计算错误。在调试的时候,用watch方式观察变量里的偏移明明是正确的,但一赋值就变了。现在每次改写,只能祈祷不出现问题才好,十分怀疑是VC环境的BUG。

后来其他人又添加了反DUMP的代码,为了区分这些模块,使得程序加壳时可以有选择加载这些模块,我就对每个程序模块进行了标号区别,这样在VC将壳代码写入的时候,就根据界面选择的结果有选择的添加代码,没有被选中的,为了偏移量计算的正确就将代码用NOP填充。功能逐渐完善了,剩下的工作就是修修补补了。但由于每一次修补之间都拖沓了很长时间,因此常常提笔忘字,得花一些时间来回想一些工作方法。

后来,注册算法这部分确实改了不少。思考了一下最开始提出反拷贝的概念,感觉一开始理解的还是有偏差,并不是说要固定的将一个程序放在某一台电脑上,而不让它运行于其他的电脑上,而是说注册码不通用,对于一台电脑上注册通过后,并不能保证其他的电脑也能用此注册码工作。这就需要在注册时考虑注册名,注册码和机器码,三者保证注册软件的唯一性。机器码的提取常见的有硬盘序列号和网卡MAC以及CPU序列号等,但由于一开始没有找到有效地提取机器码的方式,我就是用SYSTEMINFO的摘要作为机器码,从一定程度上说,这也保证了一定程度上杜绝了不同两台机器共用一套注册码。

注册码的输入当然是用dosstub实现了,这也是我们的加壳软件的一大创新点,虽然应用上看似多次一举,但至少提出了一个新思路,算是抛砖引玉吧。也就是说注册码的输入利用了PE程序里开始的一段16位的dos代码实现,因此对于不知情的人很难找到这段代码,也就无从了解注册点的位置进行突破。而注册的验证,除了对三者的的运算,特别的在比较阶段,没有对比的过程,而是将运算结果与入口点进行运算,这样既保证了入口点的加密处理,又保证了注册的验证。也就是说注册不正确,程序就运行不正常,但究竟怎么样运行才正常是无法推算出来的,保证了注册算法的不可逆行。这主要是依据了一个常数来处理这个入口点,而这个常数只有注册码设计者才知道。用户名确定后,注册码的目的就是让机器码和用户名以及注册码经过运算达到这一常数0x14c09002,当然应用中,这个常数是可以改变的,应对不同的产品也可以定制不同的常数。

这样大部分的工作就几乎完成了,虽然说起来只有短短的几个自然段,但实际经历的时间大概有5个月吧,中间也是断断续续。后来无意中想到的不能让程序每次注册失败就导致程序运行失败,这样总还是不太美观,因此又结合了SEH异常处理方法,增加了一个线程级别的异常处理函数,主要应对与注册失败导致程序返回到错误的内存地址后,不至于弹出一个错误对话框,而是弹出我预定的一个MessageBox注册失败提示。

做到这里,已经是这个学期了,刚好赶上操作系统的课程的学习,用到了不少多线程编程。正好考虑到了壳内反调试这一环节的弊端,反调试只在程序运行开始阶段存在,一旦被绕过,就会导致后期反调试无效,而且绕过也并不难,只要运行程序后在附加调试器就成功绕过了。为此,要为反调试单独设立一个线程来保证被保护程序整个生命周期的安全。说的很繁琐,其实就是用createthread函数将反调试部分作为一个线程函数来执行,保证程序执行期间抑制保持反调试状态。想不到一开始提到的多线程反调试技术就这样简单的实现了,也不知道自己理解的对不对,但效果还是起到了。好像很多技术,我都是仅仅听过一个名字,也没有查什么资料就按照自己的理解开始做了,估计细节上很难保证和官方的想法一样。经过整个软件的编写,我发现更多的时候我倾向于自己的实验测试而不是网上大段的资料,也不知道是对还是错。

后期对程序的改进,功能上的确多了不少,但都不是特别有难度,在程序代码结构上我又做过几次调整,力求大段的汇编代码不至于看起来晦涩难懂。也易于扩展各部分的代码。整个加壳程序的编写,目前算是一个告一段落的时刻了,这期间有关于代码的生成,编译过程,PE文件结构,线程,API,动态库这些系统级别的封装好的概念,我理解了很多,对于今后从事软件安全的话有很大的帮助,也为我编写各类程序开拓了视野,提供了思路。遇到问题也多了很多解决方案。That’s all