隐藏的OK——HackTM Final 某逆向writeup

  • A+
所属分类:逆向工程

隐藏的OK——HackTM Final 某逆向writeup

1

前言

大家好,我就是那个pwn不动就做逆向的pwn手——Anciety。这次给大家带来不久前刚刚结束的HackTM决赛中的一道逆向题,也想通过这道逆向题给大家分享一些我逆向的经验(当然,也可能是反向经验——越听越不会)。


这次的题目叫 demoscenes ,虽然不是特别复杂,但我还是从中有所收获,希望我的做题过程可以给大家带来一些启发。


2

题目介绍

题目描述如下:


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——HackTM Final 某逆向writeup


我就这样在旁边傻傻的看了他一分钟。

隐藏的OK——HackTM Final 某逆向writeup

好的,没有OK,不太OK——显然这是句废话,要是直接有OK还要我做啥?


所以我们的目标就是找到这么个“OK!” ,我觉得应该是这样的。


3

逆向开始

接下来就进入了令人期待的IDA打开环节,打开之后发现函数也不是很多,大概也就...


隐藏的OK——HackTM Final 某逆向writeup


好像还是有点多。再看一眼 main函数:


隐藏的OK——HackTM Final 某逆向writeup

有点厉害,这一堆sub该如何是好?


4

第一步

首先我们总结一下已经知道的东西:

1.这是一个GUI程序,是画了图像的,要首先认识到和我们在CTF当中一般会逆向的命令行程序的区别。

2.GUI程序应该不会是自己从头写的。

3.函数很多。

基本可以推断:这肯定是一个用了开源软件的程序(如果闭源就太恐怖了)。


所以我们第一步需要找到这个开源软件,那么这应该是个什么开源软件呢?


这时,就应该按下shift+f12,打开你的字符串了:

隐藏的OK——HackTM Final 某逆向writeup

大多数时候,正常的软件都不会针对防逆向去做太多的处理,一般在字符串中会包含大量的跟程序有关的信息,所以其实看字符串应该是工业界逆向的老套路了。先看字符串,再看其他的。


从这个字符串中我们可以看出用的库是allegro5,别问我怎么看出来的,看不出来,出门左转度娘。


5

第二步

现在我们知道用了什么软件(框架)了。在正常情况下,下一步我所需要做的事情,就是去打开他的官网,然后点开tutorial,看看里边的框架都怎么用,于是我找到了这样的示例代码:

隐藏的OK——HackTM Final 某逆向writeup

我发现一个惊人的问题,那就是,其实allegro处理事件是需要自己写一个while true的,回顾刚刚的main 函数,我发现,似乎我们没有while true?

隐藏的OK——HackTM Final 某逆向writeup

也就是说,整个程序没有接收任何输入?那么问题来了,我们怎么输入东西让他检测呢?


6

理解程序

这里可能就是题目的第一个槛了,找不到输入的地方,和其他的“妖艳贱货”好不一样,这可怎么办?没办法,只能强行理解一下程序了,然而理解也没这么容易,由于allegro被静态链接了,很有可能导致逆了半天,发现是库函数,所以我们需要分辨库函数和非库函数。


这个时候可以选几种方法:

1.用Binary AI,将动态链接库(有符号)和其进行匹配。或者其他基于签名的匹配方式,例如FLIRT。

2.其中有的字符串对应log函数,log函数可能会有函数名。


第一种我们之前介绍过,如果不了解的可以去尝试一下。第二种主要是这样的函数:

隐藏的OK——HackTM Final 某逆向writeup


这样的函数明显就是日志函数啦!最后一个就是函数名嘛,所以这样就可以推出不少库函数了。这时我可以通过xref这个日志函数,去找到大多数带有日志的函数名,如果有兴趣可以写个IDAPython脚本去做这个事情,并不是很麻烦。


不过说了这么多,其实……我什么也没干!因为我发现,如果我动态调试,场景之间我是有可能断下来的,大概就断在这个地方:

隐藏的OK——HackTM Final 某逆向writeup

这时,在显示完 PRESENTS "PLATONIC" 之后,就会停下来了!那么很明显,中间的地方一定和整个字符串的展示有关,而且他是一个循环,然后这个字符串的展示又是一个动画,这说明什么?肯定是每一个循环一帧嘛对吧。反正我也不知道对不对,我大概就是这么猜了,接下来就开始进入了漫长的调试阶段……


中间的调试阶段太漫长,就不细说了,调试过程大致如下:

1.我走到了sub_140034b4里,看到函数里出现了许多可显示字符:

隐藏的OK——HackTM Final 某逆向writeup


2.这个sub_140034b4函数的最后有这样的操作:

隐藏的OK——HackTM Final 某逆向writeup


这是对全局变量的一个赋值。然后我尝试修改了这个函数的调用参数,发现修改调用参数会让字符串的位置发生变化!其实,这个函数的副作用(也就是实际做的操作)就是赋值了这个全局变量而已,那么我就能基本确定这个全局变量和在屏幕上显示的位置有关系了,所以给他命名为 screen_buffer


好了,字符串的显示我明白了,难道是要调字符串位置才能出现OK吗?挺奇怪的,因为这样和flag没啥关系呀?


但是这一系列的分析给了我很大的帮助,将screen_buffer确定了,另外,我也大概确定显示的过程会写成什么样子,接下来我就可以去找类似的函数啦!


7

多面体函数

基于上面的分析,我很快找到了我所怀疑的函数,这应该是和后面的多面体渲染有关,sub_14001b4c


函数比较大,1150行,其中有几个点让我对他是渲染多面体的主要函数深信不疑:

1.对screen_buffer的操作(嗯?这不就很像清空屏幕)

隐藏的OK——HackTM Final 某逆向writeup


2.大量的重复操作(赋值)

隐藏的OK——HackTM Final 某逆向writeup


3.神奇的case

隐藏的OK——HackTM Final 某逆向writeup

以上几点让我有点怀疑,最关键的是:我调了一下隐藏的OK——HackTM Final 某逆向writeup


我在switch v426这里断了下来,然后修改了v426的值,发现确实修改的值会影响什么图像。然后我就发现了更奇怪的事情:

隐藏的OK——HackTM Final 某逆向writeup

这里有一个 default case ,是走不过去的,也不报错,但是后面又会用到:

隐藏的OK——HackTM Final 某逆向writeup

(这里的 case 6 也是 v426,没有发生修改)

也就是说,有对 case 6 的处理,但走不过去!


这就令人值得怀疑了,怀着忐忑的心情,我将他设置了一下,得到了下面这个图像:

隐藏的OK——HackTM Final 某逆向writeup

另外,如果是7就直接会黑掉了,没有图像,说明只有6是特殊case。再仔细看看,会发现case 6的处理函数 sub_140001128 里是有对 screen_buffer 的处理的!

隐藏的OK——HackTM Final 某逆向writeup


接下来我继续动态调试,sub_140001128函数参数发生变化,会导致图像发生变化(嗯,挺显然的,毕竟动了 screen_buffer)。


之后静态看就可以发现,这个的参数和全局变量 Buffer 有关,然后xref 一下 Buffer 里的内容一看,就发现了这个:

隐藏的OK——HackTM Final 某逆向writeup

终于,我们要的字符串有了!


再随便玩一下就可以发现,这个图像和字符串是相关的,于是我又静态地逆了一下(猜一下,然后调试确认就好了)。最后的结果:

隐藏的OK——HackTM Final 某逆向writeup

参数: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{[email protected]$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 ,当然是一个字母一个字母试啦(其实可以大概根据相对位置去试)。


8

总结

题目本身不算很难,其中主要有几个关键:

  1. 识别用户业务代码和库函数:这里我运气比较好,直接调出来了,不然可能需要匹配,甚至对照代码去看,从而避免逆到库函数。

  2. 猜测函数功能:相似的函数可能有相似的功能,这里我们找到了字符串的渲染函数,多面体的渲染函数也应该和他类似,这样来思考功能。

  3. 猜测函数功能2:正向思考函数可能需要怎样的功能,比如draw_line,5个参数,进去之后发现是对screen_buffer的调整,应该就和渲染相关,再结合看到的图像,应该是画的线(因为没涉及到其他的几何运算了),之后确认一下就好了。

(画OK真有趣)


往期精彩

Zend Framework 3.1.3 gadget chainZend Framework 3.1.3 gadget chain
Zend Framework 3.1.3 gadget chain
技术文章分享 | 在CTF中享受扫雷的快乐
对 Linux 内核中的 eBPF JIT 漏洞进行 fuzz(译)
清华三创大赛|星阑科技荣获TMT/AI赛道第一名
隐藏的OK——HackTM Final 某逆向writeup

星阑科技

微信号|StarCrossCN

本文始发于微信公众号(星阑科技):隐藏的OK——HackTM Final 某逆向writeup

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: