CTF比赛逆向组程序 Moon.exe 的分析过程
一、 观察
刚拿到这个程序的时候,双击运行发现黑框框一闪而过,什么都没看到,于是就用命令行去执行它,结果只输出“gl3wInit()”, 如图1所示:
图1
用IDA查看程序里面的流程,发现sub_4032C0()这个函数动态加载很多关于OpenGL的函数,总共有1236个,如图2所示:
图2
二、 程序正常运行
中间挣扎了很久,一直无法让程序正常运行起来,最后想到OpenGL绘图可能跟显卡有关,而我的电脑默认使用集成显卡,于是我就使用独立显卡运行这个程序,程序正常运行了,如图3所示:
图3
这是一个OpenGL绘出的一个图形界面,可以直接输入,而输入的字符长度是固定32个字符,随意输入32个字符之后,程序输出一个结果---“Nope”,如图4所示:
图4
三、 查找输入和输出之间的关系
于是用x64dbg加载该程序,并输入32个password字符,然后找到password的处理过程,如图5所示:
图5
然后再找到一个比较函数----memcmp(),这个比较函数比较两个512字节的字符串,其中一个字符串是固定的,只有另外一个字符串会发生变化,这个会变化的字符串和用户输入的32个字节存在某种联系,这种联系也就是密码和另外一个固定的512个字节的联系。如图6所示:
图6
输入了32个“8”之后,产生了一组对应的512字节的数据,如图7所示:
图7
四、 分析
从这些数据中暂时还看不出它们和用户输入的32个字符之间的关系,经过多次调试之后,发现这些数据是经过OpenGL的一段Shader脚本语言产生的,继而去了解Shader脚本语言,找到了关键的函数glGetShaderSource(),该函数是一个加载Shader脚本的函数。在IDA中找到调用这个函数的位置,如图8所示:
图8
这个函数是在0x4017B0这个位置被调用的,然后就在x64dbg中定位到这个位置,如图9所示:
图9
在这个位置上下一个断点,重新加载这个程序,让它直接运行到这个位置之后断下,就可以得到这个函数的实际参数,再对照glGetShaderSource()这个函数的参数,知道他的第四个参数就是Shader 脚本语言的源程序,如图10所示:
图10
五、 查找Shader脚本
而在程序的执行过程中,我们发现这个函数被执行了三次,而且每一次得到的Shader脚本都不一样,Shader脚本代码的地址就保存在RSI 寄存器中,前两次得到的Shader代码比较短,第三次的Shader代码比较长,最长的这部分代码也是最主要的代码,前面的代码是次要的。如图11、图12、图13所示:
图11
图12
图13
六、 数据变换过程
Shader代码在附录中。这部分代码的功能就是把用户输入的32个字符数据转变成64个DWORD类型的数据,是一个变相的hash过程,具体的实现过程可以参考附录中的Shader代码。至于最后为什么是比较两个512字节的数据,而不是比较两个64个DWORD类型的数据,这个过程是在程序中实现的,程序中使用一个循环,把每一个DWORD类型的数据表示成十六进制的形式,然后再把这个DWORD类型的十六进制形式的数据的每一位都拆开,最后把每一位都转换成为一个对应的ASCII字符。例如:一个DWORD类型的数据0x30c7ead9,最后会变成“30c7ead9”这样的一个字符串。也就是说,原来的一个DWORD类型的数据,最后变成了八个字节的ASCII字符串,所以64个DWORD类型的数据,最后就变成了一个64*8=512字节的字符串,所以最后比较的是两个512字节的字符串。中间的转化过程的代码如图14所示:
图14
在IDA中的位置如图15所示:
图15
七、 分析Shader代码并编写逆向程序
分析它最后的Shader代码,可以得知,它是一个类似于hash的的一个算法,把32个字符串password映射到64个DWORD值的hash表上面,虽然每一个哈希值都与前面的每一个password有关,但是它们的关系比较特殊,如图16所示:
图16
这个循环的过程就是利用32个password产生一个结果h,而这个结果h虽然是一个uint类型的变量,但是在这个循环的过程中,它的结果只会落在0到0xFF之间,所以我们可以使用枚举的方法来暴力破解它。此外,这段Shader代码的中间还有一个细节,就是每一个password主要影响两个相邻的hash值,如图17所示:
图17
根据Shader代码的这两个主要的特性,我们就可以使用枚举的方法得到原始的密码,具体的代码请参考附录。最后我们就得到了原始的密码为:
CTF{OpenGLMoonMoonG0esT0TheMoon}
八、 总结
这个程序使用的手段有点特别。
首先,它的数据转换过程不在moon.exe程序中,而是使用Shader脚本语言实现数据的转换,并且转换的过程在GPU中执行,不在CPU中执行,使得逆向的过程中无法看到数据的转换过程,给逆向带来较大的困难。
其次,这个程序使用了OpenGl的一些库函数来实现绘图,使得我们在得到Shader脚本程序的过程中,要学习一些额外的Shader语言的相关知识,才能找Shader脚本的加载函数GlGetShaderSource(),最后才得到了Shader脚本代码。
最后,这个程序使用集成显卡运行不起来,想要看到运行效果,必须使用独立显卡才能看到,这个问题刚开始的时候困扰了我很久,再加上我本来不玩游戏,对这方面的知识了解的不太多,使得它成为我逆向这个程序的第一块屏障。
总之,通过逆向这个程序,中间收获很多,学习到不少新知识,比如关于OpenGL的、Shader脚本语言的,还有一些逆向的思维等。能得到最终的结果,我要感谢我的“导师”--李博士,还有我的队友--王同学,在李博士的指导下和在于队友的探讨过程中,我才克服了重重困难,逆向出最终的结果。
附 录
1. Shader脚本的源代码
#version 430
layout(local_size_x=8,local_size_y=8)in;
layout(std430,binding=0) buffer
shaderExchangeProtocol
{
uintstate[64];
uinthash[64];
uintpassword[32];
};
vec3 calc(uint p)
{
floatr=radians(p);
floatc=cos(r);
floats=sin(r);
mat3m=mat3(c,-s,0.0, s,c,0.0, 0.0,0.0,1.0);
vec3pt=vec3(1024.0,0.0,0.0);
vec3res=m*pt;
res+=vec3(2048.0,2048.0,0.0);
returnres;
}
uint extend(uint e)
{
uinti;
uintr=e^0x5f208c26;
for(i=15;i<31;i+=3)
{
uintf=e<
r^=f;
}
returnr;
}
uint hash_alpha(uint p)
{
vec3res=calc(p);
returnextend(uint(res[0]));
}
uint hash_beta(uint p)
{
vec3res=calc(p);
returnextend(uint(res[1]));
}
void main()
{
uintidx=gl_GlobalInvocationID.x+gl_GlobalInvocationID.y*8;
uintfinal;
if(state[idx]!=1)
{
return;
}
if((idx&1)==0)
{
final=hash_alpha(password[idx/2]);
}
else
{
final=hash_beta(password[idx/2]);
}
uinti;
for(i=0;i<32;i+=6)
{
final^=idx<
}
uinth=0x5a;
for(i=0;i<32;i++)
{
uintp=password[i];
uintr=(i*3)&7;
p=(p<
p&=0xff;
h^=p;
}
final^=(h|(h<<8)|(h<<16)|(h<<24));
hash[idx]=final;
state[idx]=2;
memoryBarrierShared();
}
2. 最终的512字节的数据:
0000000001064A30 33 30 63 3765 61 64 39 37 31 30 37 37 37 35 39 30c7ead971077759
0000000001064A40 36 39 62 6534 62 61 30 30 63 66 35 35 37 38 66 69be4ba00cf5578f
0000000001064A50 31 30 34 3861 62 31 33 37 35 31 31 33 36 33 31 1048ab1375113631
0000000001064A60 64 62 62 36 38 37 31 64 62 65 33 35 31 36 3262 dbb6871dbe35162b
0000000001064A70 31 63 36 3265 39 38 32 65 62 36 61 37 35 31 32 1c62e982eb6a7512
0000000001064A80 66 33 32 3734 37 34 33 66 62 32 65 35 35 63 38 f3274743fb2e55c8
0000000001064A90 31 38 39 3132 37 37 39 65 66 37 61 33 34 31 36 18912779ef7a3416
0000000001064AA0 39 61 38 3338 36 36 36 66 66 33 39 39 34 62 62 9a838666ff3994bb
0000000001064AB0 34 64 33 6336 65 31 34 62 61 32 64 37 33 32 66 4d3c6e14ba2d732f
0000000001064AC0 31 34 34 3134 66 32 63 31 63 62 35 64 33 38 34 14414f2c1cb5d384
0000000001064AD0 34 39 33 3561 65 62 62 62 65 33 66 62 32 30 36 4935aebbbe3fb206
0000000001064AE0 33 34 33 6130 30 34 65 31 38 61 30 39 32 64 61 343a004e18a092da
0000000001064AF0 62 61 30 3265 33 63 30 39 36 39 38 37 31 35 34 ba02e3c096987154
0000000001064B00 38 65 64 3263 33 37 32 65 62 36 38 64 31 61 66 8ed2c372eb68d1af
0000000001064B10 34 31 31 3532 63 62 33 62 36 31 66 33 30 30 65 41152cb3b61f300e
0000000001064B20 33 63 31 6138 32 34 36 31 30 38 30 31 30 64 32 3c1a8246108010d2
0000000001064B30 38 32 65 3136 64 66 38 61 65 37 62 66 66 36 63 82e16df8ae7bff6c
0000000001064B40 62 36 33 3134 64 34 61 64 33 38 62 35 66 39 37 b6314d4ad38b5f97
0000000001064B50 37 39 65 6632 33 32 30 38 65 66 65 33 65 31 62 79ef23208efe3e1b
0000000001064B60 36 39 39 3730 30 34 32 39 65 61 65 31 66 61 39 699700429eae1fa9
0000000001064B70 33 63 30 3336 65 35 64 63 62 65 38 37 64 33 32 3c036e5dcbe87d32
0000000001064B80 62 65 31 6563 66 61 63 32 34 35 32 64 64 66 64 be1ecfac2452ddfd
0000000001064B90 63 37 30 3461 30 30 65 61 32 34 66 62 63 32 31 c704a00ea24fbc21
0000000001064BA0 36 31 62 3738 32 34 61 39 36 38 65 39 64 61 31 61b7824a968e9da1
0000000001064BB0 64 62 37 3536 37 31 32 62 65 33 65 37 62 33 64 db756712be3e7b3d
0000000001064BC0 33 34 32 3063 38 66 33 33 63 33 37 64 62 61 34 3420c8f33c37dba4
0000000001064BD0 32 30 37 3261 39 34 31 64 37 39 39 62 61 32 65 2072a941d799ba2e
0000000001064BE0 65 62 62 6638 36 31 39 31 63 62 35 39 61 61 34 ebbf86191cb59aa4
0000000001064BF0 39 61 38 3065 62 65 30 62 36 31 61 37 39 37 34 9a80ebe0b61a7974
0000000001064C00 31 38 38 3863 62 36 32 33 34 31 32 35 39 66 36 1888cb62341259f6
0000000001064C10 32 38 34 3861 61 64 34 34 64 66 32 62 38 30 39 2848aad44df2b809
0000000001064C20 33 38 33 6530 39 34 33 37 39 32 38 39 38 30 66 383e09437928980f
3. 处理这512字节数据的一个Python脚本:
def GetDataFromString(str):
result= str[-19: -2]
returnresult
pass
def main():
result= "{"
data= ""
count= 0;
try:
f= open("Password.txt")
forline in f:
data= GetDataFromString(line)
var= "0x" + data[0:8] + ", "
result+= var
var= "0x" + data[8:16] + ", "
result+= var
count+= 2;
result= result[:-2] + "}"
print("get%d datas."%count)
print(result)
f.close()
f= open("result.txt", "w")
f.write(result)
f.close()
except:
print("%sconvert to dword error\n"%var)
main()
4. 把脚本处理过后得到的64个DWORD类型的数据转换为原始的32个字符
#include
#include
typedef unsigned int uint;
#define M_PI 3.14159265358979323846
uint state[64];
uint hash[64];
uint password[32]; // 要求的密码
// 正确的 hash 结果
uint CorrectHash[64] = {
0x30C7EAD9,0x71077759, 0x69BE4BA0, 0x0CF5578F,
0x1048AB13,0x75113631, 0xDBB6871D, 0xBE35162B,
0x1C62E982,0xEB6A7512, 0xF3274743, 0xFB2E55C8,
0x18912779,0xEF7A3416, 0x9A838666, 0xFF3994BB,
0x4D3C6E14,0xBA2D732F, 0x14414F2C, 0x1CB5D384,
0x4935AEBB,0xBE3FB206, 0x343A004E, 0x18A092DA,
0xBA02E3C0,0x96987154, 0x8ED2C372, 0xEB68D1AF,
0x41152CB3,0xB61F300E, 0x3C1A8246, 0x108010D2,
0x82E16DF8,0xAE7BFF6C, 0xB6314D4A, 0xD38B5F97,
0x79EF2320,0x8EFE3E1B, 0x69970042, 0x9EAE1FA9,
0x3C036E5D,0xCBE87D32, 0xBE1ECFAC, 0x2452DDFD,
0xC704A00E,0xA24FBC21, 0x61B7824A, 0x968E9DA1,
0xDB756712,0xBE3E7B3D, 0x3420C8F3, 0x3C37DBA4,
0x2072A941,0xD799BA2E, 0xEBBF8619, 0x1CB59AA4,
0x9A80EBE0,0xB61A7974, 0x1888CB62, 0x341259F6,
0x2848AAD4,0x4DF2B809, 0x383E0943, 0x7928980F,
};
struct vec3
{
floatxyz[3];
vec3(floata, float b, float c)
{
xyz[0]= a;
xyz[1]= b;
xyz[2]= c;
}
//重载 += 运算符
voidoperator += (const vec3& res)
{
size_ti = 0;
for(i = 0; i < 3; i++)
{
xyz[i]+= res.xyz[i];
}
}
};
struct mat3
{
floatv[3][3];
mat3(floata0, float a1, float a2, float b0, float b1, float b2, float c0, float c1, floatc2)
{
//这里的保存顺序要注意,是列优先
v[0][0]= a0;
v[0][1]= b0;
v[0][2]= c0;
v[1][0]= a1;
v[1][1]= b1;
v[1][2]= c1;
v[2][0]= a2;
v[2][1]= b2;
v[2][2]= c2;
}
//重载矩阵乘法
vec3operator*(const vec3& res)
{
size_ti = 0;
vec3tmp = vec3(0, 0, 0);
//矩阵(3*3)*矩阵(3*1)
for(i = 0; i < 3; i++)
{
tmp.xyz[i]=
v[i][0]* res.xyz[0] +
v[i][1]* res.xyz[1] +
v[i][2]* res.xyz[2];
}
returntmp;
}
};
float radians(uint p)
{
returnM_PI / 180.0 * p;
}
vec3 calc(uint p)
{
floatr = radians(p);
floatc = cos(r);
floats = sin(r);
mat3m = mat3(c, -s, 0.0, s, c, 0.0, 0.0, 0.0, 1.0);
vec3pt = vec3(1024.0, 0.0, 0.0);
vec3res = m*pt;
res+= vec3(2048.0, 2048.0, 0.0);
returnres;
}
uint extend(uint e)
{
uinti;
uintr = e ^ 0x5f208c26;
for(i = 15; i<31; i += 3)
{
uintf = e << i;
r^= f;
}
returnr;
}
uint hash_alpha(uint p)
{
vec3res = calc(p);
returnextend(uint(res.xyz[0]));
}
uint hash_beta(uint p)
{
vec3res = calc(p);
returnextend(uint(res.xyz[1]));
}
void core(int idx)
{
uintfinalRes;
if(state[idx] != 1)
{
return;
}
if((idx & 1) == 0)
{
finalRes= hash_alpha(password[idx / 2]);
}
else
{
finalRes= hash_beta(password[idx / 2]);
}
uinti;
for(i = 0; i<32; i += 6)
{
finalRes^= idx << i;
}
//这里的 h 的范围 是从 0x0 到 0xFF
//于是枚举所有的 h 的值, 从而忽略其他 password 对 h 值的影响
//uinth = 0x5a;
//for(i = 0; i<32; i++)
//{
// uint p = password[i];
// uint r = (i * 3) & 7;
// p = (p << r) | (p >> (8 - r));
// p &= 0xff;
// h ^= p;
//}
uinth;
uinttmpres = finalRes;
for(i = 0x0; i <= 0xFF; i++)
{
h= i;
tmpres= finalRes;
tmpres^= (h | (h << 8) | (h << 16) | (h << 24));
if(tmpres == CorrectHash[idx])
{
//得到正确地 hash 值
hash[idx]= tmpres;
state[idx]= 2;
break;
}
}
}
void printPass()
{
printf("Passwordis :\n");
for(size_t i = 0; i < 32; i++)
{
printf("%c",password[i]);
}
printf("\n");
}
void printHash()
{
printf("Hashis :\n");
for(size_t i = 0; i < 64; i++)
{
printf("0x%08X", hash[i]);
if((i + 1) % 4 == 0) printf("\n");
}
printf("\n");
}
int main()
{
size_ti = 0;
//初始化
for(i = 0; i < 64; i++)
{
state[i]= 1;
hash[i]= 0;
}
for(int k = 0; k < 32; k++)
{
for(i = 0; i <= 256; i++)
{
password[k]= i;
core(k* 2); // 一个 password 对应两个 hash 值
core(k* 2 + 1);
if(state[k * 2] == 2) // 只要有一个hash值是对的,另外一个也是对的
{
break;
}
}
}
printPass();
printf("\n");
printHash();
printf("\n");
return0;
}
5. 最终结果:
CTF{OpenGLMoonMoonG0esT0TheMoon}