前言
大家好,我就是那个pwn不动就做逆向的pwn手——Anciety。这次给大家带来不久前刚刚结束的HackTM决赛中的一道逆向题,也想通过这道逆向题给大家分享一些我逆向的经验(当然,也可能是反向经验——越听越不会)。
这次的题目叫 demoscenes ,虽然不是特别复杂,但我还是从中有所收获,希望我的做题过程可以给大家带来一些启发。
题目介绍
题目描述如下:
After his terrible exprience as a camboy at the quals, trupples turned to other ways of making art, and he stumbled across demoscening. Here's a cute but technically terrible piece of /art/! I swear I remember it drawing a big "OK!", but I can't get it to do that anymore...
题目下载之后会有一个 .exe文件,我怀着忐忑的心情点开了他,出于对比赛方的信任,我甚至都没有使用沙箱:
我就这样在旁边傻傻的看了他一分钟。
好的,没有OK,不太OK——显然这是句废话,要是直接有OK还要我做啥?
所以我们的目标就是找到这么个“OK!” ,我觉得应该是这样的。
逆向开始
接下来就进入了令人期待的IDA打开环节,打开之后发现函数也不是很多,大概也就...
好像还是有点多。再看一眼 main函数:
有点厉害,这一堆sub该如何是好?
第一步
首先我们总结一下已经知道的东西:
1.这是一个GUI程序,是画了图像的,要首先认识到和我们在CTF当中一般会逆向的命令行程序的区别。
2.GUI程序应该不会是自己从头写的。
3.函数很多。
基本可以推断:这肯定是一个用了开源软件的程序(如果闭源就太恐怖了)。
所以我们第一步需要找到这个开源软件,那么这应该是个什么开源软件呢?
这时,就应该按下shift+f12,打开你的字符串了:
大多数时候,正常的软件都不会针对防逆向去做太多的处理,一般在字符串中会包含大量的跟程序有关的信息,所以其实看字符串应该是工业界逆向的老套路了。先看字符串,再看其他的。
从这个字符串中我们可以看出用的库是allegro5,别问我怎么看出来的,看不出来,出门左转度娘。
第二步
现在我们知道用了什么软件(框架)了。在正常情况下,下一步我所需要做的事情,就是去打开他的官网,然后点开tutorial,看看里边的框架都怎么用,于是我找到了这样的示例代码:
我发现一个惊人的问题,那就是,其实allegro处理事件是需要自己写一个while true的,回顾刚刚的main 函数,我发现,似乎我们没有while true?
也就是说,整个程序没有接收任何输入?那么问题来了,我们怎么输入东西让他检测呢?
理解程序
这里可能就是题目的第一个槛了,找不到输入的地方,和其他的“妖艳贱货”好不一样,这可怎么办?没办法,只能强行理解一下程序了,然而理解也没这么容易,由于allegro被静态链接了,很有可能导致逆了半天,发现是库函数,所以我们需要分辨库函数和非库函数。
这个时候可以选几种方法:
1.用Binary AI,将动态链接库(有符号)和其进行匹配。或者其他基于签名的匹配方式,例如FLIRT。
2.其中有的字符串对应log函数,log函数可能会有函数名。
第一种我们之前介绍过,如果不了解的可以去尝试一下。第二种主要是这样的函数:
这样的函数明显就是日志函数啦!最后一个就是函数名嘛,所以这样就可以推出不少库函数了。这时我可以通过xref这个日志函数,去找到大多数带有日志的函数名,如果有兴趣可以写个IDAPython脚本去做这个事情,并不是很麻烦。
不过说了这么多,其实……我什么也没干!因为我发现,如果我动态调试,场景之间我是有可能断下来的,大概就断在这个地方:
这时,在显示完 PRESENTS "PLATONIC" 之后,就会停下来了!那么很明显,中间的地方一定和整个字符串的展示有关,而且他是一个循环,然后这个字符串的展示又是一个动画,这说明什么?肯定是每一个循环一帧嘛对吧。反正我也不知道对不对,我大概就是这么猜了,接下来就开始进入了漫长的调试阶段……
中间的调试阶段太漫长,就不细说了,调试过程大致如下:
1.我走到了sub_140034b4里,看到函数里出现了许多可显示字符:
2.这个sub_140034b4函数的最后有这样的操作:
这是对全局变量的一个赋值。然后我尝试修改了这个函数的调用参数,发现修改调用参数会让字符串的位置发生变化!其实,这个函数的副作用(也就是实际做的操作)就是赋值了这个全局变量而已,那么我就能基本确定这个全局变量和在屏幕上显示的位置有关系了,所以给他命名为 screen_buffer。
好了,字符串的显示我明白了,难道是要调字符串位置才能出现OK吗?挺奇怪的,因为这样和flag没啥关系呀?
但是这一系列的分析给了我很大的帮助,将screen_buffer确定了,另外,我也大概确定显示的过程会写成什么样子,接下来我就可以去找类似的函数啦!
多面体函数
基于上面的分析,我很快找到了我所怀疑的函数,这应该是和后面的多面体渲染有关,sub_14001b4c。
函数比较大,1150行,其中有几个点让我对他是渲染多面体的主要函数深信不疑:
1.对screen_buffer的操作(嗯?这不就很像清空屏幕)
2.大量的重复操作(赋值)
3.神奇的case
以上几点让我有点怀疑,最关键的是:我调了一下
我在switch v426这里断了下来,然后修改了v426的值,发现确实修改的值会影响什么图像。然后我就发现了更奇怪的事情:
这里有一个 default case ,是走不过去的,也不报错,但是后面又会用到:
(这里的 case 6 也是 v426,没有发生修改)
也就是说,有对 case 6 的处理,但走不过去!
这就令人值得怀疑了,怀着忐忑的心情,我将他设置了一下,得到了下面这个图像:
另外,如果是7就直接会黑掉了,没有图像,说明只有6是特殊case。再仔细看看,会发现case 6的处理函数 sub_140001128 里是有对 screen_buffer 的处理的!
接下来我继续动态调试,sub_140001128函数参数发生变化,会导致图像发生变化(嗯,挺显然的,毕竟动了 screen_buffer)。
之后静态看就可以发现,这个的参数和全局变量 Buffer 有关,然后xref 一下 Buffer 里的内容一看,就发现了这个:
终于,我们要的字符串有了!
再随便玩一下就可以发现,这个图像和字符串是相关的,于是我又静态地逆了一下(猜一下,然后调试确认就好了)。最后的结果:
参数:draw_line(x1, y1, x2, y2, val),其实 val 始终是 1,另外 x1 也一定和 x2 相等。
所以这个题的终极目标:调整 Buffer ,从而调整 y1, y2 的值,x 是不变的,最后画一个OK!出来。由于开头是定的 HackTM{ 结尾是 },所以给我们定了一个基本的位置,这样就不容易错了。
我用下面的脚本模拟了一下这个过程(其中还包含一些测试,例如确认和默认时候的图像是否一样):
from PIL import Image, ImageDraw
INITIAL = "Hello world my name is trupples welcome to"
FLAG = r"HackTM{B0rnD3c@desTooLa7eForDemo$cen3F4me}"
# (x1, idx, y1, x2, idx, y2)
data = [
(44, 34, 37, 44, 34, 40),
(45, 35, 42, 45, 35, 45),
(50, 40, 37, 50, 40, 50),
(17, 7, 72, 17, 7, 76),
(28, 18, 56, 28, 18, 68),
(15, 5, 73, 15, 5, 78),
(27, 17, 23, 0x1B, 17, 38),
(43, 33, 51, 0x2B, 33, 55),
(41, 31, 29, 0x29, 31, 33),
(20, 10, 28, 0x14, 10, 32),
(42, 32, 103, 0x2A, 32, 107),
(46, 36, 87, 0x2E, 36, 88),
(49, 39, 29, 0x31, 39, 30),
(11, 1, 44, 0xB, 1, 54),
(25, 15, 38, 0x19, 15, 43),
(18, 8, 103, 0x12, 8, 107),
(22, 12, 100, 0x16, 12, 104),
(47, 37, 84, 0x2F, 37, 85),
(50, 40, 51, 0x32, 40, 54),
(24, 14, 87, 0x18, 14, 91),
(34, 24, 37, 0x22, 24, 54),
(42, 32, 113, 0x2A, 32, 117),
(30, 20, 27, 0x1E, 20, 28),
(25, 15, 50, 0x19, 15, 55),
(17, 7, 85, 0x11, 7, 89),
(49, 39, 44, 0x31, 39, 45),
(46, 36, 102, 0x2E, 36, 104),
(51, 41, 28, 0x33, 41, 29),
(22, 12, 87, 0x16, 12, 91),
(23, 13, 52, 0x17, 13, 56),
(44, 34, 50, 0x2C, 34, 54),
(23, 13, 39, 0x17, 13, 43),
(47, 37, 68, 0x2F, 37, 69),
(36, 26, 27, 0x24, 26, 44),
(38, 28, 75, 0x26, 28, 81),
(13, 3, 32, 0xD, 3, 46),
(21, 11, 83, 0x15, 11, 87),
(51, 41, 13, 0x33, 41, 14),
(39, 29, 41, 0x27, 29, 49),
(24, 14, 74, 0x18, 14, 78),
(21, 11, 70, 0x15, 11, 74),
(37, 27, 30, 0x25, 27, 34),
(16, 6, 15, 0x10, 6, 19),
(45, 35, 28, 0x2D, 35, 30),
(40, 30, 38, 0x28, 30, 42),
(48, 38, 86, 0x30, 38, 87),
(40, 30, 32, 0x28, 30, 36),
(51, 41, 15, 0x33, 41, 24),
(33, 23, 83, 0x21, 23, 100),
(41, 31, 37, 0x29, 31, 41),
(19, 9, 24, 0x13, 9, 28),
(32, 22, 41, 0x20, 22, 42),
(19, 9, 37, 0x13, 9, 41),
(31, 21, 62, 0x1F, 21, 63),
(18, 8, 90, 0x12, 8, 94),
(29, 19, 30, 0x1D, 19, 40),
(16, 6, 28, 0x10, 6, 32),
(12, 2, 41, 0xC, 2, 53),
(30, 20, 32, 0x1E, 20, 38),
(10, 0, 71, 10, 0, 77),
(15, 5, 61, 0xF, 5, 66),
(49, 39, 31, 0x31, 39, 40),
(29, 19, 27, 0x1D, 19, 28),
(35, 25, 68, 0x23, 25, 85),
(20, 10, 41, 0x14, 10, 45),
(28, 18, 54, 0x1C, 18, 55),
(26, 16, 37, 0x1A, 16, 53),
(14, 4, 55, 14, 4, 70),
(43, 33, 39, 43, 33, 43),
]
def gen_empty(im):
draw = ImageDraw.Draw(im)
for (x1, idx1, y1, x2, idx2, y2) in data:
draw.line((x1, y1, x2, y2), fill=128)
with open('demo.png', 'wb') as f:
im.save(f)
def gen_initial(im):
draw = ImageDraw.Draw(im)
for (x1, idx1, y1, x2, idx2, y2) in data:
assert(idx1 == idx2)
val = ord(INITIAL[idx1])
draw.line((x1, (y1 + val) % 128, x2, (y2 + val) % 128), fill=128)
with open('initial.png', 'wb') as f:
im.save(f)
def gen_flag(im):
draw = ImageDraw.Draw(im)
for (x1, idx1, y1, x2, idx2, y2) in data:
assert(idx1 == idx2)
assert(x1 == x2)
print('-------------')
print((x1, y1, x2, y2))
delta = abs(y1 - y2)
val = ord(FLAG[idx1])
y1 = (y1 + val) % 128
y2 = (y2 + val) % 128
assert (delta == abs(y1 - y2)), f'num {idx1} ({flag[idx1]})'
print((x1, y1, x2, y2))
draw.line((x1, y1, x2, y2), fill=128, width=1)
with open('ok.png', 'wb') as f:
im.save(f)
def main():
with Image.new('1', (127, 63)) as im:
gen_flag(im)
if __name__ == '__main__':
main()
至于提取 flag ,当然是一个字母一个字母试啦(其实可以大概根据相对位置去试)。
总结
题目本身不算很难,其中主要有几个关键:
-
识别用户业务代码和库函数:这里我运气比较好,直接调出来了,不然可能需要匹配,甚至对照代码去看,从而避免逆到库函数。
-
猜测函数功能:相似的函数可能有相似的功能,这里我们找到了字符串的渲染函数,多面体的渲染函数也应该和他类似,这样来思考功能。
-
猜测函数功能2:正向思考函数可能需要怎样的功能,比如draw_line,5个参数,进去之后发现是对screen_buffer的调整,应该就和渲染相关,再结合看到的图像,应该是画的线(因为没涉及到其他的几何运算了),之后确认一下就好了。
(画OK真有趣)
往期精彩
星阑科技
微信号|StarCrossCN
本文始发于微信公众号(星阑科技):隐藏的OK——HackTM Final 某逆向writeup
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论