Google CTF 真的难。。。拼了老命也才能勉强比赛完后做了一题而且(沉迷reverse的第二弹。。。。)
Google CTF – Moon
首先运行,发现是一个exe界面,而且长的很像游戏:
然后看到下面的SDL,明白了这是一个用SDL引擎写的游戏。。我们用ida打开以后,定位到main函数下:
这里关注一个函数:
SDL_PollEvent
这个函数是SDL中常常用于事件分发的函数,我们输入字符串将会在这里被卡住。根据MFC逆向的经验,如果真的存在flag的比较过程的话,那么关于键盘中数据读入的过程很可能会包含我们的flag匹配。于是这里我们用x86-64进行调试,首先定位到我们可能发生字符串读入的函数:
经过测试,这段内容是在我们输入了字符串之后会卡住,所以可以认为这个地方可能是处理字符串的内容,我们直接跟进去看:
看起来是给指定位置读取字符串而已。于是我们尝试动态调试跟踪一下:
这段给了我们一些基本的信息:
- 4ca080这个全局变量的位置很关键,此处存放了存放ascii的位置
- 4ca088处存放的是当前读入了多少个字符串
- 4ca090处存放了当前读入的字符串的ascii
然后再反复测试中发现,当读入八个字符串之后,存放在原先地址的前面的8个字符串就会被抹去,只剩下后面的八个字符串:
但是我们会发现,长度依然是在计算的。同时,我们输入的字符串的来到了另一个地址
同时我们发现,我们最多能够输入的字符串数量为32,正好符合一个判断语句;
这段逻辑应该就和这个加密密切相关了…
我们进入这个函数,发现一开始调用了
glUseProgram(program)
这个做过开发都会知道,是使用我们制定的着色器函数的意思。这里指定的着色器函数内容如下:
紧接着调用了这个函数:
GLboolean glIsBuffer(GLuint buffer);
//判断是否是缓冲区对象
同样的,判断了指定位置上(还是我们的6df1000)是否为缓冲区。缓冲区是用来存放顶点信息的。
之后就是比较常见的数据绑定,接下来有一个有意思的函数:
glMapBuffer
glMapBuffer用来将一个缓冲区对象中的数据映射为客户端中的地址空间。这段的意思就是我们能够取得此时的顶点信息(也有可能是进行写入操作),这里显然和我们将要处理的数据很有关系:
void *glMapBuffer(
GLenum target,
GLenum access);
target用于指定缓冲区的类型,而access指定这段缓冲内容是否可读可写。
之后会开始往我们得到的缓冲区地址+0x200中写入数据:
可以看到,这里是以一个int的长度写入的数据。之后我们会来到一个充满了xmm0的位置,里面同样的会将缓冲区地址中写入数据:
最后我们会释放掉这部分缓冲区的指针:
然后我们会开始调用一个神奇的函数:
glDispatchCompute()
函数会把工作组发送到计算管线上,其原型如下:
void glDispatchCompute(GLuint num_groups_x, GLuint num_groups_y, GLuint num_groups_z);
这段内容会将会开始在xyz三个维度上进行工作计算。计算的内容就是我们之前读入的着色器。程序在这里分别传入了8,8,1;
第二天早上
今天早上测试的时候逻辑完全不一样了额。。。。首先我们反复的执行了这个函数:
然后我们跟踪进去,发现有一个函数不断的产生新的字符串:
于是想到,接下来的目的主要有两个:
-
搞清楚下面这个函数的参数意义
generate_string((__int64)&gen_str, (__int64)"%.8x", v19, v18);
-
找到产生的字符串对之后产生了什么影响.
由于这里错过了第一个内容,所以我们优先跟踪我们产生的字符串去了哪里:
发现不久之后,就对这个参数进行了减去0x1010101的处理,然后将其自身取反之后and 0x80808080,如果有做过相关练习的话,会知道这一段其实是在测量字符串的长度。
然后我们会传入我们之前产生过的字符串的地址指针以及这次产生的字符出,以及长度。
同时,从这个地方我们能够看出此处存在一个结构体:
1 | struct{ |
后来发现,这个玩意儿没那么简单。。。我们可以看到循环的条件:
起始地址如上,然后我们的终点是:
这就不好处理了。。。如果我们完全跟踪的话,怕是要到地老天荒。。。所以我们这里直接跳过看看。
不过为了实现之前的目的1,我们决定跟如generate看看发生了什么,这里首先记忆一下,我们输入的字符串内容存在这里:
总的来说,就是一个随机产生字符串的过程。。
然后我们完成这段内容复制之后,发现原先存储字符串的地址变成了这个:
估计是原先地址不够大的原因。然后我们继续走:
这段内容将往ds:4ca060中存放的地址中存放的字符串与我们产生的字符串本身进行比较。字符串内容为:
1 | 30c7ead97107775969be4ba00cf5578f1048ab1375113631dbb6871dbe35162b1c62e982eb6a7512f3274743fb2e55c818912779ef7a34169a838666ff3994bb4d3c6e14ba2d732f14414f2c1cb5d3844935aebbbe3fb206343a004e18a092daba02e3c0969871548ed2c372eb68d1af41152cb3b61f300e3c1a8246108010d282e16df8ae7bff6cb6314d4ad38b5f9779ef23208efe3e1b699700429eae1fa93c036e5dcbe87d32be1ecfac2452ddfdc704a00ea24fbc2161b7824a968e9da1db756712be3e7b3d3420c8f33c37dba42072a941d799ba2eebbf86191cb59aa49a80ebe0b61a79741888cb62341259f62848aad44df2b809383e09437928980f |
在完成了比较之后,会将之前的空间释放掉:
之后就回到开始,开始处理输入字符串。
总的流程下来,算是找到了判断逻辑:
这段中,如果right__or_not = 2的时候,屏幕上会绘制GOOD,否则的话绘制NOPE.那么我们返回来看这个判断条件,发现还是和之前的加密算法密切相关的。。。不过也算好,终于也是找到了判断逻辑了。。。
于是接下来开始分析逻辑:首先找到我们输入字符串的存储位置:
然后继续深入,知道这段内容:
这段内容关键内容即为r9寄存器中的内容,这个内容直接影响了后面的数据的变化,那么这个数据
测试之后得到,这个位置的rdx中的数据终会转换成字符串存储在内存中。于是现在反向查找数字的产生位置:
可以发现,后面的处理不过是将这段内容进行了字符串转换存储罢了。也就是说,关键还是这段内容是怎么产生的。
然后就能够回到昨晚那段内容了。。。接着分析 :
glClipControl
接下来是这个函数,这个用的比较少啊,是用来控制裁剪坐标系的。感觉应该和我们的输入没什么关系才对。。
发现在这个函数之后,会发生将所有mmap其实地址中的数据+1的事情:
原因有点不清楚。。然后调用了下列函数:
glMemoryBarrierByRegion
该函数会定义一个defines a barrier ordering memory transactions
,我这里理解就是顺序内存交换的障碍。。。不知道用来做啥的。。
后面能够开到,再次申请了空间:
而且显然,由于我们之前把这段内容释放掉了,所以是利用了我们之前的空间。
关键是这里用了一个函数我之前没用过。。。也没怎么听过。。。只能靠找规律了。。。。
输入数据:
12345678901234567890123456789014
12345678901234567890123456789015
可以发现,最后一个数字会影响每一行的数字,使得每两行中的同一列发生相同的变化规律。
这样做不行。。。找不到本质。。。。然后我回头问了一下同学,发现其实是
glClientWaitSync
这个函数在起作用,其作用就是【让同步对象执行】,然后我们看到函数:
glFenceSync
这个则是创建一个同步对象:
隔了几天之后
我为什么放弃了。。。明明差一点就找到了啊啊啊啊啊
就在gl_init里面。。。我还自称做过OpenGL开发。。。这点东西都忘了。。。。
glCreateProgram
这个函数会创建一个着色器对象,然后成功之后,一般就会调用函数就是:
glCreateShader
这段就是传入着色器了啊啊啊!!!我之前的思路也是这么写的啊啊啊!!!为什么放弃了!!!
最后可以找到非常复杂的加密逻辑:
1 |
|
怕是可以当成crypto出了。。。这里扯到了一点关于算数着色器的内容,这里可以见另一篇博客算数着色器
这段逻辑其实也非常的麻烦,我们一点点来分析:
1 | layout(local_size_x=8,local_size_y=8)in; |
之前的着色器是为了普通的染色用的,第三段才是重点。首先一开始生命的是当前计算着色器中局部工作组的大小,为8*8(*1)(由于着色器本身为3维上,但是由于不声明的话默认是2维的,z轴默认值为1)。然后是声明此时多个管道中进行共享的变量。从大小和命名上可以猜测,password应该是我们输入的字符串,state应该是GPU状态之类的,hash就是一个特定的值的hash。
1 | uint idx=gl_GlobalInvocationID.x+gl_GlobalInvocationID.y*8; |
这段是main函数中的内容,第一句话的意思是【确定当前在全局工作组中的下标】。由state大小不难知道,全局工作组的大小为8*8。并且这里会将每两个赋值,都是将我们的输入的字符串传入并且进行hash运算,我们深入看一下算法:
1 | uint hash_alpha(uint p){ |
这里能够看到,此时的p(也就是我们数组中的值)会同一个calc调用,然后会返回一个vec3。区别在于alpha返回的是[0],而beta返回的是[1]。
1 | vec3 calc(uint p){ |
这段就是hash处理的过程。这个函数会将传入的数据作为弧度,然后会强行算一个旋转矩阵,就是
[cos r, - sin r]
[sin r, cos r]
然后这个矩阵乘上(1024,0,0)(相当于这个1024向量将要转动p度),并且将结果加上向量(2048,2048)
结合一下前面,大致意思就是【将传入的数据作为角度,计算当前向量旋转后的值,并且在idx为单数的时候设置其答案为x值,idx为双数的时候设置其值为y】。最后会在extend中进行数据扩展。
1 | uint i; |
最后这段首先是根据下标,按6 bit为一组与idx进行了异或,然后在password中的值每个值取出来进行,进行循环移位,同时将当前值与一个魔数异或。最后将final和刚刚得到的魔数按照8bit扩展成32bit后的数字机型异或。最后将这个数字存入hash中。
可以猜测,这个hash就是我们需要的答案。
虽然可以考虑使用暴力破解的方法,但注意到这个h的值是由所有的password一起确定的,就很难一次性确认出来。。。。后来在一个大佬的github上看到了一个非常聪明的思路:
https://hxp.io/blog/33/Google%20CTF%202017:%20reversing%20%22moon%22/
这里搬运一下;
函数的关键在于:
1 | for (i=0;i<32;i++){ |
这段内容,我们会将所有的password都读出来,并且进行运算最后得到h。会发现这个过程和idx完全无关。换句话说,每个管线中都将执行一遍这个过程,得到相同的数据。然后由于flag的形式一定是CTF{xxxx},所以第一个字符必然是C,于是我们按照前面的思路完全重现一下加密的逻辑,代码如下:
1 | # -*- coding:-utf-8 -*- |
得到的输出与之前提到的一大串数据的开头四个进行异或,正好得到
1 | 0x6f6f6f6f |
由此可见,h的最终值就是0x6f。接下来直接写个爆破脚本跑数据看看:
1 | # -*- coding:-utf-8 -*- |
得到flag:
CTF{OpenGLMoonMoonG0esT0TheMoon}
挖草这个Google ctf逼的我把OpenGL又复习了一遍。。。。。