是HSCTF2020新生赛的PC逆向部分的wp,尽量往详细写啦,希望能给师傅们提供良好的复现体验。
本次Reverse基本都为送分题,旨在科普和引导逆向入门=v=
已经疯狂砍了很多难度啦,H4ve fuN w1th C7F_r3vEr5e!!
0x01 Funny_game
多维度送分题(x)
显而易见的俄罗斯方块,打块拿flag。
Ⅰ. 暴力打穿
打到2020分自行结束游戏后在屏幕右侧出flag。花个十几分钟大概就能出(
p.s. 一定要在刚好2020分的时候结束游戏才能出flag哦,不然也没有flag出来的(所谓打得好不如打得巧
自己打出来的反面教材↓(。)
Ⅱ. 修改程序逻辑
在ida里按F5出伪代码,看到程序逻辑,
patch一下把if改成if not / 2020改成0 / …
(如果ida有装keypatch这个插件的话就能直接通过更改汇编语句从而patch机器码;方法见->IDA7.0安装keypatch和findcrypt-yara插件)
↑ ① 相当于把if改成if not
↑ ② 把2020改成0
然后狂敲enter,1s拿flag。
Ⅲ. 定位到getflag()逆向求解
查看到程序逻辑以后双击getflag(),或者直接在函数栏搜索flag定位到关键函数。
getflag()程序逻辑:
逆向写出exp:
1 | arr=[0x0000006E, 0x00000094, 0x00000061, 0x0000006F, 0x0000009B, 0x00000071, 0x0000009D, 0x00000051, 0x0000009C, 0x0000006D, 0x00000067, 0x00000061, 0x00000067, 0x0000006E, 0x0000009D, 0x00000096, 0x00000096, 0x00000099, 0x00000067, 0x0000006F, 0x0000005C, 0x00000095, 0x0000006D, 0x00000067, 0x00000069, 0x00000093, 0x00000096, 0x0000007C, 0x00000067, 0x00000069, 0x0000009C, 0x00000085] |
p.s. v1-v9在空间上是连续的,所以实际上这是一个长度为33(末尾有’\0’)的char型数组。
0x02 Magic_switch
还是送分题(linux下可直接打开
Ⅰ. 随手乱糊
其实根据xor的特性,只要1-8每个数字都敲一次就能出flag(比如12345678)。当时出题的时候就在想会不会有队伍在wp里一脸懵逼地写“随便打就出来”的,没想到还真有哈哈哈。
Ⅱ. 修改程序逻辑
可以看到程序逻辑是只要所有灯的状态都是1的时候,才能跳出while(1)
死循环。
同funny_game,可以通过修改程序逻辑,把if(v6)
改成if(!v6)
,然后程序运行时在while执行的第一次就能跳出循环获得flag,不过完全没必要(。
有patch的功夫不如直接乱糊2333。
Ⅲ. 定位到关键函数逆向求flag
按惯例按快捷键shift+F12
,查看strings窗口,可以看到一个fake_flag,顾名思义是假flag(这一段没什么用,目的只是提一下IDA的常规操作罢辽)。
点进flag以后发现flag不是直接存储的,而是在某个函数里生成(?)的。
进到反汇编窗口,右键flag数组,查询引用情况。
then可以看到一个神秘的secret函数。
secret()的函数逻辑如下。
在题目里,可以看到flag[]为全局变量(函数内没有声明/存储在bss段/在ida里显示变量名字),全局变量默认初始化为0。而任何数异或0都为它本身,故flag[i]^=v2[i]
实际上等同于flag[i]=v2[i]
。
一个简单xor,解密思路跟funny_game相同,写出exp:
1 | v2=[14,16,35,28,15,42,14,16,29,60,53,12,35,46,116,15,92,56,42,19,3,20,28,37,6,19,13,20,56,6,20,27,20] |
这也说明了xor其实只是基操并非考点,考点还是ida的操作和阅读C代码的能力= =
0x03 Not_A_Sudoku
Not A Sudoku,不是一个数独,不要看到81和sudo的数组名就想到数独啦(
但跟数独也不是毫无关系的x。这是一个神秘古老而冷门(误)的逻辑游戏,名为数织Nonograms 【数织 - 在线解谜游戏】(hint也给了这个网站的数独网址,往下翻一下更多解谜游戏就有),应用数织原理的手游也有Two Eyes、PicrossLUNA系列等。
看到伪代码,知道程序逻辑是输入一个长度为81的二进制数字串,且需要通过check(v4,0)
和check(v4,1)
。
这里不能通过动态调试的方法获取flag,因为这里的flag其实是用户输入进去的,它只负责测试对不对。
也不能用angr,因为分支太多会爆内存。
所以我们只能通过静态分析check()
函数来判断怎样的flag才能通过检查。
双击C()
可以看到其功能是return x==0? 1:9;
,本来还想科普一波三目运算符的没想到ida给简化了。
关于xor,有0^1=1, 1^0=0
,即,将参数取反。(逆向科普有认真看的想必对这一条不会陌生!xor还有更多有趣的特性等待你们去挖掘~)
回到check()
,a2=0时的函数可以简化为:
1 | for(int i=0;i<9*9;i+=9){ //i为行数 |
于是就很容易可以看出,这是在检验一个9 * 9的图里每一行中有多少个连续的1块,每个1块里有多少个1。同理可得,check(v4,1)
的作用是检验一个9 * 9的图里每一列中有多少个连续的1块,每个1块里有多少个1。这些数据都存放在sudo[]
里,并以-1
作为每一行/列的界限。
这就是数织游戏的原理。
所以,我们可以画出这样的数织空图:
按照数织的玩法,我们填出下面的图(是一个很简单的9*9非常规数织,熟练的人很快就能填出来了)。
和尚玩家直呼眼熟(x)
填完以后,按照题目要求,把每一个格子的状态打下来就好了(有色为1,无色为0),然后能够得到flag:
flag{001111000011111100110001110111111110111111111011111111000011110000001110000111100}
0x04 Maze
其实这道题是出题的时候所有题里面最先有思路的一道,并且也花了点心思往里塞了很多科普(可惜写的时候忽略了防输入其他字符的default,不过还是做到就是赚到hhh。
如果认真看过CTF Wiki的人(或者攻防世界里reverse新手区刷完的人),看到这个题目应该有条件反射吧0v0
那就直接maze套路走起吧
1)在内存中布置一张“地图”
不同于常规的maze题目,本题的地图没有直接储存在全局变量中,而是由CreateMap()
生成。
而这一串关键代码
1 | do{ |
其实跟十进制转二进制的代码特别像(论熟悉进制转换的必要性
1 | int num,pos=1,binary[16]={0}; |
所以其实就是把num数组里的数(先转成无符号数再)转成二进制按行存到地图里。
然后可以得到16*16的地图:
2)将用户输入限制在少数几个字符范围内
可以看到,除了格式flag{}
以外,其余的字符都由h
、j
、k
、l
组成,表示在迷宫内的上下左右移动。
那么,为什么选这四个字母来作为上下左右而不是常规的↑↓←→
或者wasd
呢?
因为在文本编辑器vim里,让光标上下左右移动的快捷键分别就是h(左)、j(下)、k(上)、l(右)。(掌握vim快捷键是linux端快速敲码的第一步噢0v0
3)一般只有一个迷宫入口和一个迷宫出口
在迷宫画出来后,可以轻松看到唯一的出入口(入口在map[13][0]、出口在map[13][15])。因为有限制长度为59,所以很大可能是迷宫的最短路径。
在地图中用黄色标记表示(红色为混淆点):
也可以很明显地看到,全部为0的格子连起来其实是一个Sloth的文字押 (cy2谱面传统艺能,假装是音游元素)。
跟着黄色路线一直走记下flag即可。
flag{llllkkkhhhkkkkkkkkklllljjjjllljjljjjjjjjlllkkkklljjjl}
0x05 Base64
这个名字,显然就是跟base64编码有密切联系呀= =
不知道的赶紧去看base64的简介和源码(pia飞。
某种程度来说也是一道送分题。
函数名也这么明显了hhh,超级友好(当然如果函数名不明显的话也可以通过查看加密函数,发现跟base64源码差不多)。
点进base64_char
数组,能看到编码表,发现跟原版base64的表不同。
- 本题:
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/
- 原版编码:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
所以是一个base64换表题,随便找一个在线网站利用已知密文src
直接base64换表解密即可。
flag{dedf73ea-7252-4892-b882-9992708f0f62}
0x06 Bytecode
python字节码人工反编译,在网上能搜到很多死磕字节码的教程(比如hint给的 死磕python字节码 或者 python 字节码死磕),最需要的是耐心hhhh。
然后可以逆出源码
1 | def main(): |
简单逆向,主要的点在字节码反编译部分,只要把源码逆出来就能写出脚本啦,刚好能帮助入门python。
就是切片那一块有一点点坑,注意是前到后不到的就好。
本来还有一层加密的,为了降低难度删掉了。
1 | arr1=[51, 42, 67, 2, 100, 48, 94, 29, 25, 26, 9, 43, 25, 21, 53, 11, 11, 91, 0, 12, 14, 19, 122, 0, 44, 26, 58, 26, 28, 24, 50, 3, 93, 21] |
0x07 Deliver_TEA_to_dalao
其实名字有一点点暗示 给大佬递茶.jpg ,毕竟TEA这么明显的大写,在搜索引擎里能查到TEA加密;并且很明显的可以看到,在代码保护上运用了smc技术(热身赛有好好看wp的队肯定能发现)。
SMC(self-Modifying Code 自修改代码),就是在真正执行某一段代码时,程序会对自身的该段代码进行自修改,只有在修改后的代码才是可汇编、可执行的。在程序未对该段代码进行修改之前,在静态分析状态下,均是不可读的字节码,IDA之类的反汇编器无法识别程序的正常逻辑。
大概逻辑如图:
Ⅰ. 通过patch伪修正
代码逻辑看起来比较简单,就是把输入的字符串v21经过一个加密函数以后跟已知字符串做对比而已。
然而看到加密函数的伪代码&&反汇编却发现乱七八糟的。
这时候我们可以猜测它被smc技术保护,实锤就是在上面一个不起眼(?)的ignoreMe
函数里把加密函数所在的地址进行了xor。
也就是说,程序目前的encrypt_0v0
部分的机器码是有问题的,需要通过这个xor函数以后程序才能正常进行。
那我们可以通过IDA自带的脚本功能进行patch,将那一部分的函数“修正”,让改函数能正常反编译,给我们呈现出这个部分的伪代码。
注意,这里只是伪修正,只改这里然后将patch的部分应用到输入文件的话程序是无法运行的,需要把xor函数nop(全部patch成90)才能消除影响(不过在这里正常运行似乎没什么必要x)。
我们找到encrypt_0v0
部分,看到函数起始地址是0x401448,加密长度是189。
然后写出patch的exp:
1 | def patch(start,end,key): |
点击File->Script file,导入脚本运行。
可以看到encrypt_0v0
函数发生了变化,同时消息窗口有“189 Byte has been changed”字样。
点击Edit->Patch program->Apply patches to input file,将patch的部分应用到输入文件中(便于让ida重新分析)。
关掉,重新用ida打开,按F5
进行反编译可得:
是显而易见的TEA加密形式(比如特殊常数0x9E3779B9或者0x61C88647)。
这里key是[v4,v5,v6,v7]=[0x33221100,0x77665544,0xBBAA9988,0xFFEEDDCC]
。
明文是flag,密文是v8~v12转成的char数组:{0xa8,0xa,0xe4,0xe3,0x13,0x5c,0xfa,0x8,0xd,0x5c,0xe1,0x90,0x25,0x12,0x76,0x36,0x51,0x10,0xc4,0x11,0xf6,0xd8,0xf8,0x82}
。
为什么不是{0xe3,0xe4,0x0a,0xa8,...}
呢,这就要看它们在内存中的存储方式(小端序)了,具体自行搜索。
于是exp就可以写了:
1 |
|
Ⅱ. 动态调试直接看函数
对付smc,还可以考虑动态调试。
环境是IDA pro v7.0,高版本可以直接调试32位程序。
因为是32位程序,所以在ida目录下找到.\dbgsrv\win32_remote.exe
,并打开:
然后用ida打开文件,点击Debugger->Select debugger选择:
然后点击Debugger->Process options调整设置(将Hostname设置成my ip就好):
断点设在call ignoreMe
后面,此时encrypt_0v0
已被处理,是正常函数。
然后按F9
,开始调试,调试开始后程序暂停在断点处。
点开encrypt_0v0
,(按空格从图形界面转到文字)。
选中这一部分的汇编代码,按c
强转成代码(->Force->Yes)。将还包括在函数体内的语句按d
再按c
。
然后选中刚才的地址区段,按p
生成函数。
最后按F5
反编译得到:
就可以看到加密函数了,后续解密步骤同Ⅰ。
其实这里从强转成代码开始的步骤同样适用于Ⅰ,只是相比之下直接重新打开ida让其重新分析更方便而已hhh。
0xff EOF…?
一点乱七八糟的碎碎念,看完wp的可以关掉啦~
这次新生赛是我CTF生涯中第一次出题(很开心能混迹在大佬云集的筹备委员会中0v0),在pc逆向这边除了Base64以外背了其他六道题的锅,感觉是一次很宝贵的经历。
所谓“出题才能学到更多”是真的。以前做题的时候,在ida里总是会点进一些看起来操作很多好像很有用的函数,但实际上有很多都是没有用的初始函数(编译源码的时候自动生成、ida反编译的时候也算上了的那种),都是在自己出完题目然后试图从选手的角度做的时候发现的。
以及比如像smc这种有点进阶考点的,本来做题也不是很懂原理(吧),就是单纯地走一个找函数patch回去的流程,然后出题的时候就单纯地按自己的想法写了(.data段没有执行的权限hhhh 一开始还把函数当成数组写到.data段里了xswl),然后就失败了x,幸好有大佬可以求助=v=。
还有就是写wp的时候每道题会去想一些别的路,争取把各个思路&工具都科普出去(于是迫使自己接触一些平时不常用的工具)。比较遗憾的是没有合适的题目可以用来写ida利用wsl2远程调试linux程序
和angr/pintool的使用
吧。前者是没有出到用动态调试性价比更高的linux题目,后者是那些可以走爆破的题目分支多容易爆内存(像这个东西也是从去年vm的wp里学到的,就是想把这些宝藏工具传承下去)。
出题的时候说不要变成萌新劝退赛+要放点有意思的题目,于是就有了Funny_game和Magic_switch这两道以游戏为载体的题目。Funny_game本来是打算出成贪吃蛇的,不过考虑到去年的杂项就有贪吃蛇(偶然看到我们队去年的wp发现的),就想着不要出重复的了。刚好那段时间在等俄罗斯方块手游的N测(卑微打块人已经在小程序上打了很久的马拉松了),于是就拿别人的俄罗斯方块程序改了一下,加了个出flag的函数。说起来那么简单,实际上还是得把别人的程序看懂了才知道能在哪里加啊qvq 给写这个源程序的大佬磕头。一定要等于2020那里是我故意的哈哈哈,因为读程序的时候发现2020其实很容易达到,又为了契合年份(这六个题的所有常数都是有意义的嘿嘿,甚至连smc那个异或的0x56也是有意义的 cy2的某首歌 ),于是就搞了个等于而不是大于等于。这个题出完以后我还闲着无聊打了几盘,没有保留功能不然就能凑tetris了。至于Magic_switch,参考了一下攻防世界逆向新手区里面game的玩法,但源码是自己撸的,感觉提升了一波写游戏代码的能力。
Not_A_Sudoku的原意其实是真的想出一道数独的,源于某次比赛逆出来的py源码排版超级乱(一堆lambda看到眼花),然后被某个大佬发现是数独检验,直接当数独题做了。出题的时候就想搞一个类似的,不过后来想了想觉得数织的检验代码比数独优雅好多啊,写源码的话还不如去写数织的呢(所以这道题就叫Not_A_Sudoku了,顺便还能科普一下冷门的数织游戏。至于谜面,一目了然就是本命禅雅塔的“乱”了hhh。
Maze真的是我最早有思路的题目,源于数据结构课本里用深搜/栈走迷宫的图(上课没怎么听讲实锤了,以及不用队列走迷宫这个点真的很想让我吐槽),then一整节数据结构课上都在想构造这个maze的谜面hhh,以及一个埋成sloth彩蛋的文字押,下课就迅速掏出电脑把生成地图的常数数组写进去。像是把数字转成十六位二进制当成数组和hjkl控制光标上下左右这些点也是想科普的点吧hhh。
Bytecode的出题本意是想让萌新们趁机入门python和一些反编译的操作,感觉比起汇编语法来说python字节码这个更容易理解一点,毕竟没涉及到寄存器操作之类的,甚至还保留了变量名。这个题真不难,就是考一点耐心和细心而已,欣慰的是还挺多队伍做出来了(自我感觉这个题比数织的要复杂,不知道为什么数织的解这么少XD)。原本还有一个foo4是维吉尼亚密码的加密,后来被嫌太难就删了。
预期中Deliver_TEA_to_dalao应该是最难的题,因为考虑到大部分人没接触过smc这种保护,没想到热身赛直接就放了个smc的题,那其实难度就大大降低(在热身赛的wp那里我连patch的脚本都给了),主要难点变成了TEA这个加密算法的识别了(对差点就做出来的TEA一直念念不忘),当然手写逆算法也不难hhh。
出完题以后觉得pc逆向这里防ak有点悬,没想到真就没防住被ak了(向大佬低头)。出题其实也是在另一个角度上的对基础知识的巩固吧,wp也写得很爽没有交作业的感觉,出题人表示体验极佳,下次一定再来 /Doge。