1、概述
有幸参与了看雪2017CTF防御方题目设计,由于各方面的缘故,结果题目设计有点“糙”,惹得各位大侠诸多不满!在此向各位攻击方选手表达深深的歉意,确实由于设计时的准备和平时积累的缘故,题目没有太多的新意,并且设计过程中还出现的各种问题,引得大家的争议,表示一定努力做得更好。在此也同时深深感谢看雪逆向版版主netwind的信任与帮助,初次设计,考虑不周,还望谅解!最后唯有努力不断进步,加强研究,破解更多的题目,交流更多的技术。
在参加看雪2017ctf比赛中,中间也参与了攻击方,很遗憾中间只完成了第一个题目后由于其他任务便中途中断了,等到自己的题目,被鄙视了一番,深深的歉意,13题时用符号执行,但是由于路径的缘故,符号执行求解失败,最后没有搞定。搞到14题的时候,搞了版本绕过了壳以及调试检测,dump出了内容,但是后面算法部分还是没有完整跟出来(这两天不是周末,投入也不足)。最后看了15题,搞了半天,后面根据题目中的一些字符串,搜索到其使用了lua虚拟机,由于对lua不熟,最后又查询相关的资料,最终搞定。因此准备将自己查找的内容以及相关的理解给总结出来,以便自己以后遇到这类题目能更加顺手的处理。文中的叙述内容参考了其他博客或者网站内容,由于调研的不充分,其中如果有错误的地方请各位大牛指正。
题目和2016年的看雪CTF的第二题相似,利用了lua的虚拟机,将算法的核心算法隐藏在了lua字节码文件中,然后编译成lua的字节码,通过动态加载并调用相关函数执行加密后再进一步进行相关的异或操作,最后与固定的值进行比较。题目算法很简单,就是两次简单的异或操作,关键将算法代码进行了隐藏,核心就是定位JIT字节码以及最后比较的位置。因此解密的关键就是要对lua虚拟机的源码进行相关的识别,并找到相关的位置,从内存中提取出lua字节码,通过反编译得到相关的算法,由于对lua字节码的也不太熟悉,因此花费了较多的时间。虽然题目算法相对来说比较简单,但是通过分析题目,自己学到了很多东西,对lua的相关知识有了更一步的了解。最后更是坚定自己的信心,只要去研究,认真去搞,就一定能搞干掉它。
2、lua字节码及其反编译
2.1 lua脚本以及lu字节码文件
由于题目中涉及到了lua字节码,这里结合自己做这个题目是查阅的相关资料,对lua脚本语言以及在攻防对抗中的应用进行简单的分析。lua是一种简洁、轻量、可扩展的脚本语言,源自于葡萄牙语中月亮的意思。lua设计的目标是使其成为一种能够很容易嵌入到其他语言中的脚本语言,其使用ANSI C语言的方式编写并且以源代码的形式开放,因此可以很方便的嵌入到其他脚本语言中,目前Lua语言经常被嵌入到C语言中,其以简洁以及性能高效而被广泛使用,尤其在游戏外挂中,由于其简单快捷,更是被各种游戏外挂作者使用。
上面对Lua脚本语言进行了简单的概述,下面接着介绍Lua脚本、Lua字节码以及Lua虚拟机。由于Lua是一种脚本语言,因此其需要加载到Lua虚拟机中进行执行。Lua脚本是一种利用Lua语法编写的脚本语言,其以后缀.lua结束,并以明文的形式存在,利用记事本等工具便可以直接打开查看。下是本题目中提取得到的Lua脚本:
从图中可以看到Lua也是也函数为单位,函数之间可以被相互调用。而Lua字节码文件则是将lua脚本进行编译后得到的字节码文件,其编译后得到的字节码能够被lua虚拟机解释执行(实际上lua脚本在实际使用时也会被编译成字节码形式再被虚拟机解释执行)。字节码是指在虚拟机内部被定义为具有特定含义的功能字节,虚拟机能翻译这些特定的字节码到相应的语言信息并执行与之语意信息相符号的汇编指令。目前以字节码形式脚本主要以两种形式存在,分别是luac和luaJIT。其中luac是原始的版本。而LuaJIT则是lua的另外一个实现脚本,其JIT代表的是just-in-time,也就是解释执行。LuaJIT相对于luac来说更加高效一些,lua脚本文件编译成luac和luajit字节码文件后都带有一定的格式,下图分别是luac和luajit两种字节码文件的格式开头:
上面分别是luac和luajit两种字节码文件的开始标志。上图是luac格式文件的开始,一般以固定标志\x1B\x4C\x75开始,而第五位一般是版本信息,这里表示是luc 5.1,在一些题目中,经常会将开始的字节码进行修改,以让一些反编译工具进行反编译时失败,这时一般需要分析程序,看程序中的提示符来决定使用的是哪个版本的lua虚拟机。下图是luajit的格式文件,其开始固定为\x1B\x4C\4A。
2.2 lua虚拟机
一般每种语言都有自己opcode(operation code,“操作码”),相当于每种语言都有了自己的“汇编语言”,虚拟机就是可以理解这些opcode并将其转化为各个平台处理器所能够识别的机器指令。 lua虚拟机用于解释执行lua字节码,将字节码翻译成处理器能够识别的机器指令并执行,搭建了字节码到处理器指令集之间的桥梁。下图是脚本语言的运行流程:
其中parser用于对外部输入进行词法和语法分析,然后再转换为opcode字节码,最后虚拟机对opcode字节码进行解释执行。对于lua语言,目前虚拟机主要有两种,一种是原生的虚拟机,还有一种是luajit虚拟机。在前面解释了luajit虚拟机所识别的字节码文件和luac的不同。lua虚拟机是开源的,由巴西里约热内卢天主教大学所设计的。它于2003年发布最初版本lua1.0。目前最新的版本是5.3,而release版本是Lua 5.3.4,目前在lua的官方网站上可以下载到lua虚拟机源文件的各个版本https://www.lua.org/ftp/ 。由于lua虚拟机版本不同,其生成的luac字节码文件也不同,在2.1节中描述了luac字节码文件的第五个字节表示了lua虚拟机版本。去年看雪CTF2016比赛的第二个题目http://bbs.pediy.com/thread-213712.htm 中,其使用了lua5.3.3版本的虚拟机,但是却将luac字节码文件的开头修成了ls 1.1,并且修改了虚拟机源代码,导致被修改的虚拟机能够识别别修改后的字节码文件,而一般的反编译工具却无法准确识别因而导致反编译失败。这种情况下一般是需要攻击者通过分析程序的代码,由于将luac嵌入到c代码中执行时,创作者一般都需要将lua虚拟机编译到目标程序中,由于lua虚拟机调用函数时会有很多的字符串,因此可以分析程序通过查看相关的字符串来获取其真正所使用的lua虚拟机版本,然后将错误的字节码开头修改成正确的后方可进行反编译。
另外一种广泛使用的lua虚拟机是luaJIT,其优势是性能优越,同时支持ffi,能方便的集成到C语言中实现。LuaJIT虚拟机字节码文件的开始是以"\x1B\x4C\x4A"作为开始的,在2.1节中已经描述。目前最新的luajit虚拟机版本是LuaJIT 2.1.0-beta3 ,在The LuaJIT Project这个网站上(http://luajit.org/ )可以下载到luaJIT虚拟机的各个版本。在今年的题目中其使用的就是LuaJIT 2.1.0-beta3的虚拟机版本,在这里作者并没有修改Lua虚拟机,使得题目相对比较容易做出来。
该节对Lua虚拟机进行了简单的介绍,关于Lua虚拟机源码解读的文章,网上有很多,CSDN上有个专栏是探索Lua5.2的内部实现(http://blog.csdn.net/yuanlin2008/article/category/1307277 ),自己还没完整读过。关于luaJIT的源码阅读,这里找到一个简单的LuaJIT源码分析的Pdf(https://www.gitbook.com/book/gaoxiaojun/luajit-source-code-analysis/details ),但是其没有叙述完整,只描述了bytecode部分内容。
2.3 lua字节码文件反编译
在进行程序分析中,大部分程序不是直接将lua脚本嵌入到程序中,而是将脚本文件编译成字节码文件,然后通过lua虚拟机的luaL_loadbuffer 来进行加载,而且很多程序都会将lua字节码文件进行加密或者部分opcode的修改,导致常规工具解密失败或者无法解密。因此在实际分析时一般是定位到lualL_loadbuffer函数(该函数用于动态加载lua字节码文件),然后从内存中直接dump出lua字节码文件,再利用相应的工具进行反编译最后得到lua脚本文件,这部分可以参考看雪CTF2016第二题http://www.10tiao.com/html/523/201611/2458280475/1.html 。在实际应用中一些较复杂的时可能会修改lua虚拟机的源代码,从而使得提取lua字节码文件比较困难,常见的是 很多外挂会将lua字节码文件进行加密后存放在文件中,在使用时动态解密后再加载,这时可以在luaL_loadbuffer时对内存进行dump从而获取相应的字节码文件。更加复杂的情况是外挂作者会对lua虚拟机的源码进行修改,从而使得直接dump会失败,比如对luaL_loadbuffer的源码进行修改,在luaL_loadbuffer函数里面进行解密后再进行加载,这时不能直接在函数外层进行dump,而应该进入到luaL_loaderbuffer函数进行等解密完成后再进行dump。一些还会对虚拟机源码中的关于字节码处理的代码进行修改,使得某个关键字节码对应的语言信息发生改变,从而使得常规的反编译工具反编译失败,这种情况下也得分析lua虚拟机的源码,找到作者修改的位置,并在字节码文件中将相应的字节码替换为原来的正确字节码,从而使得普通反编译工具能够反编译成功。关于如何获取luaL字节码,可以查看看雪论坛的另外这篇文章“浅析android手游lua脚本的加密与解密 ”http://bbs.pediy.com/thread-216969.htm ,其详细进行了描述,也分享了案例,这里有很多内容可以参考。下面简单叙述下常规情况下如何对lua字节码文件进行反编译。目前对原生luac字节码文件进行反编译的工具一般是使用luacdec(http://luaforge.net/projects/luadec51/ )这个工具,,该工具用于对lua5.1的bytecode进行反编译。其一般命令如下:
而针对luaJIT生成的字节码文件,一般是利用luajit的命令行工具,将字节码转换成可读的模式。在实际使用中对luaJIT字节码文件进行反编译一般是使用luajit-decomp这个工具来对luaJIT字节文件进行反编译,该工程项目文件github地址为如下:https://github.com/bobsayshilol/luajit-decomp 。该工程可以对luac和luajit等字节码文件都进行反编译,但是该工程默认的lua版本是5.1,而luajit的版本为2.0.2,因此在实际使用中,如果需要反编译不同版本的lua字节码文件,则需要下载相应版本的lua和luajit的源码,进行编译后替换掉原文件夹中的lua51.dll和luajit.exe。
上图是一个修改过的对luajit-decomp进行修改过的工程,用于对lua字节码进行反编译,并且已经编译了用于解密2.0.2,2.0.3和2.1.0三个版本的字节码dll和exe,分别放在2.0.2,2.0.3,2.1.0三个文件夹下,如果反编译时需要用到哪个版本的luajit.exe 进行反编译,则直接在对对应的目录下将相应的luaJit.exe和lua51.dll拷贝并替换到主目录下的文件即可。工具被放在了附件中,具体下载见附件,使用时在data文件夹中包含三个文件,其中在luajit中存放luajit字节码文件:
放置完成后,直接运行decoder.exe,则会进行批量反编译,将luaJit下面的所有的字节码文件进行反编译,最后在data文件夹的out目录中生成相应反编译后的文件。如果出现解密失败,那么就必须要考虑上面提到的问题,是否是版本不匹配,作者是否是修改了luajit虚拟机的源代码,导致字节码文件中的一些字节被修改或者替换,从而需要进行修正后在进行解密。该工具引用自看雪文章“浅析android手游lua脚本的加密与解密 ”作者所提供。在附件中也会提供相应的下载,解密本题目时也是使用了该工具。
3、题目分析和解题思路
3.1 题目分析
现在该轮到题目分析了,这个题目放出来两个小时就被id为“风间仁”的选手搞定,如本次比赛组织者“netwind”所说“已经到了在用意念做题”的地步,膜拜一下。我想攻击者这么牛,也是经过很多磨炼和无数的努力才能达到此登峰造极的地步。所谓“台上一分钟,台下十年功”,做任何事情都不容易,但只要努力就肯定能战胜。
下面开始分享一下自己的解题过程,拿到题目直接运行,随便输入序列号,提示Wrong!
于是按照常规思路开始上调试器,拖到IDA里面静态分析,发现被加壳,开始跟踪分析用普通的OllyICE被提示检测到调试器:
于是换上HawkOD,以及strongOD等插件,反调试被搞定,于是脱壳,试了所有能够尝试的各种脱壳手段,发现不行,于是单步跟着走,发现调用VirtualAlloc申请空间,然后利用拷贝数据到申请空间中,最后有利用VitualProtect改变代码段的权限,将数据写入到.txt代码段中,跟踪到0x00401000等位置处被解密出来,以为字节能搞定,跟着程序继续走,但是到后面又跑丢了或者程序异常,搞了半天也没搞定,于是放弃纠缠。换思路,不再纠缠于于脱壳,直接将程序运行起来,然后附加上HawkOD,这时一切正常,看了一下0x00401000等位置处的相关代码,发现也和之前分析到的解密后代码相同,初步判定解密完成。为了方便了解程序流程,还是从0x00401000出开始进行了内存dump,拖到ida中,发现还是很大部分函数能够解析,但后面分析发现这些dump出来的内容对分析程序帮助不大,这种方法在对付一些已经过了反调试,但是无法进行完整脱壳时还有很有用的,在实际分析中,经常会遇到很多壳无法完整的跟踪或者脱掉,那么就等程序运行起来后,在附加上调试器,然后直接从内存中dump出代码,拖到ida里面查看,很多时候我们只需要分析到程序的核心跳转流程就行了,并并不需要完整的脱壳。
于是开始在调试器里面分析,大体思路就是利用ALT+M在,然后ctrl+B在内存中搜索相关的字符串,比如输入的字符串,或者提示Wrong!等字符,然后设置硬件内存断点,查看程序访问的位置,经过多次调试分析,总共设置了20多个断点,大概分析到一些关键代码位置,但是刚开始分析了很久还是不明白,由于刚开始不对Lua不熟悉,经常跟踪到lua的虚拟机中,然后半天被绕晕,不知所云。下面大概截取一些在分析过程中通过设置断点定位到的关键代码位置:
位置1:0x40103d,写入luaJIT字节码文件,在这里向esp+0x36位置写入字节码问文件内容,esp+0x30中存放的是字节码文件开始的六个字节,其开始为\x1B\x4C\x4A
写入完成后总共有0x275个字节的字节码文件,写入完成后内存中的字节码文件内容如下所示:
但是由于不熟悉LuaJIT字节码文件,依然没看明白是啥东西,尴尬!然后接着分析:
在0x4021CF
在写入字节码文件中,后面的函数分别是完成了LuaJIT虚拟机初始化以及Loal_loadbuffer加载luajit字节码文件等,入上图所示。其中在0x004021CF处调用函数sub_00412270是加载了luaJIT的相关库,并完成了初始化等工作。开始不熟悉,跟进了该函数,走了半天,也正是该函数里面的内容给予了自己的提示,因此遇到不明白的就多跟着程序走,多走几次,总会看出点东西来,程序使用了混淆技术,走了很久终于发现了一些有意思的东西,在0x42a088的Call EDI会被调用多次,里面进去后会出现很多有意思的字符串。后面经过和源码确认是此处循环多次执行是加载了多个lua虚拟机的核心的库。。
第一次进入后出现了一些关键字符串,如下所示
有Lua 5.1 coroutine,这些信息本来可以作为提示了其可能使用了Lua 虚拟机,但是由于不熟悉了,还没是没有识别出来,但是后面经过分析其使用的是Luajit beta 2.1.0的虚拟机,这个函数多次被调用,第二次调用相关信息如下:
有大量的lua字符串出现,这次开始觉得有点可疑,应该是使用了什么工程的源代码。这个给一个自己以前分析的经验,一般在程序中看到有很多有意义的字符串,但是又不像是作者自己写的,那多半是用了什么开源库,之前自己分析一个加密软件时,发现其使用了很多crypt先关的字符串,利用字符串在google进行搜索,确认了是使用了openssl库,一般确认了使用什么开源库时,分析就简单了,这是只需要找到源码,然后对应分析知道其调用的函数,最后分析就方便很多了。同样的思路,直接上谷歌,关键词crackme 加lua,一搜索,出现的居然是看雪CTF2016第二题的信息,初略看了下,知道了lua在crackme中的应用,但是自己去年没做啊。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
上传的附件: