0x00 题 这里有一个画了flag的小程序,可好像出了点问题,flag丢失了,需要把它找回来。
要求:
1、不得直接patch系统组件实现绘制(如:直接编写D3D代码绘制flag),只能对题目自身代码进行修改或调用。
2、找回的flag需要和预期图案(包括颜色)一致,如果绘制结果存在偏差会扣除一定分数。
3、赛后需要提交找回flag的截图 和解题代码或文档 进行评分。
0x01 解 运行程序会发现ACE的蓝色logo,过几秒后会自动消失
上IDA,直接看看WinMain(https://learn.microsoft.com/zh-cn/windows/win32/learnwin32/winmain--the-application-entry-point)
可以看到创建了一个互斥体(CreateMutexA),保证程序单例运行
之后是创建窗口
先D3D入个门吧:https://www.cnblogs.com/skiwnchiwns/p/10343190.html
之后这里能找到D3D的初始化过程的一步
所以大概可以判定这是D3D的初始化函数
看里面包着的函数sub_140001090
可以注意到这里有两个xmmword,里面的内容可以看一下
第一个无法解析成字符串,但是第二个可以,同时可以注意到下面还有一个xmmword可以解析成字符串
由于IDA的特性,这里有点问题,可以u转unk然后d转byte,比较好读(
可以看到这两个分别是ZwAllocateirtua和ZwFreeVirtualMem
回到函数sub_140001090
可以看到后面又strcpy了ntdll和lMemory
由此判断这里是动态获取ZwAllocateVirtualMemory和ZwFreeVirtualMem的函数地址
参考:https://learn.microsoft.com/zh-cn/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress
之后是用memcpy写了点东西
这里有两段memcpy,浅看了一下,第一个开头很多90,即nop,想到了nop雪橇
关于Nop雪橇:https://www.cnblogs.com/nice-to-meet-you/p/11240228.html
所以这可能是一段shellcode,而第二个memcpy则没看出什么,根据大哥的WP这里应该也是一段shellcode
两段shellcode之间的(即0x53C48B48这些)是填充指令
之后有个GetTickCount
这里是检测运行时间是否超过4s,如果超过则调用上面提到的ZwFreeVirtualMem清空shellcode
(所以为什么会导致打开之后几秒图案消失)
接下来dump这个shellcode下来看看干了什么
dump下来之后浏览一下,可以发现这里有一段相对比较长的地址
结合一下源程序的这里
动调找到这个v6+11249(0x2BF1)的地址
可以发现一模一样
所以这个v37就是这个D3DCompile函数了,可以把这个v37当函数看
之后还可以在sub_650中找到一大堆看不懂的东西
根据WP,这个sub_650是shellcode的入口,是D3D的一些shader初始化
不过这里挺乱的,可以把它变得好读一点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 cbuffer ConstantBuffer : register (b0){matrix World;matrix View;matrix Projection;} struct VS_OUTPUT { float4 Pos : SV_POSITION; float4 Color : COLOR0; }; VS_OUTPUT VS (float4 Pos : POSITION, float4 Color : COLOR) { VS_OUTPUT output = (VS_OUTPUT)0 ; output.Pos = mul(Pos, World); output.Pos = mul(output.Pos, View); output.Pos = mul(output.Pos, Projection); output.Color = Color; return output; } float4 PS (VS_OUTPUT input) : SV_Target { return input.Color; }
后面也可以看到v54被v37调用
所以可以得知这个地方拿v54去编译了
往下翻翻,还能看到这段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct VSOut { float4 Col : COLOR;float4 Pos : SV_POSITION; }; VSOut VS (float4 Col : COLOR, float4 Pos : POSITION) { VSOut Output;Output.Pos = Pos; Output.Col = Col; return Output; } float4 PS (float4 Col : COLOR) : SV_TARGET { return Col; }
后面和v54差不多,也是调用了D3DCompile
之后调了个函数
感觉很是重要
根据WP说,整个sub_650是动态编译shader,完毕后执行虚拟机
而这个sub_420就是虚拟机了
关于VM可以看看这个:https://bbs.kanxue.com/thread-267670.htm
很多case啊(
可以注意到这个unk_1301,里面很多这个
一眼操作码
接下来就是搞清楚各个case的作用了
先从case5和case6开始,因为这两个都调用了sub_0
根据sub_0的参数,很明显case5和case6的第五个传入的参数有问题
这里把float改成int
然后就能识别了
这个数很奇怪,莫名其妙就来这么一个数
结合一下flag,或许和绘图有关,那么猜测可能是颜色
试试
bingo,看来这两个就是绘图了
case6就是绘制ACE的logo,那么case5就是黄色的flag了
然后再分析别的
根据一般vm的操作,一个虚拟机应该有寄存器,寄存器分High和Low两个部分处理,在这个vm中可以看出v15经常被使用,所以可以作为寄存器来看
这里有两个函数
LODWORD是取DWORD的低16位
HIDWORD是取DWORD的高16位
考虑到v15经常取高位和低位进行操作,因此合理认为v15实际上是一个数组,其[0]和[1]分别是两个寄存器
因此后面优化观看体验时有必要将其设置为数组(
然后这个unk_1301里面有很多操作数,所以可以看成是代码,即vmcode(_DWORD[])
这个v7则用于指定使用vmcode的那一个,类似于指针,所以可以看做是相对地址IP
而这个result,则可以看作是从
重命名,以便更好地查看
这下好看多了
一个个分析吧
先是case 0
1 2 3 4 case 0 : result = reg[1 ]; reg[0 ] += reg[1 ]; goto LABEL_10;
一眼add
然后是case 1
1 2 3 4 case 1 : result = reg[1 ]; reg[0 ] -= reg[1 ]; goto LABEL_10;
一眼sub
然后是case 2
1 2 3 4 5 6 case 2 : v11 = (int )vmcode[i_p + 1 ]; i_p += 2 i64; result = (unsigned int )reg[v11]; reg[vmcode[i_p]] = result; goto LABEL_10;
这个case2的vmcode应该有双操作数
v11存了vmcode的其中一个值
然后vmcode[ip+2]相当于取了vmcode的另一个值
这里应该是将reg[v11]的值给了reg[vmcode[ip+2]]中
应该是mov
然后是case 3
1 2 3 4 5 6 case 3 : v12 = vmcode[i_p + 1 ]; i_p += 2 i64; result = (__int64)vmcode; reg[vmcode[i_p]] = v12; goto LABEL_10;
这里v12存了vmcode的一个值,然后将它赋值给了reg[vmcode[ip+2]]
这里有必要提一下lea和mov的区别了
lea是“load effective address”的缩写,简单的说,lea指令可以用来将一个内存地址直接赋给目的操作数,例如:
lea eax,[ebx+8]就是将ebx+8这个值直接赋给eax,而不是把ebx+8处的内存地址里的数据赋给eax。
而mov指令则恰恰相反,例如:
mov eax,[ebx+8]则是把内存地址为ebx+8处的数据赋给eax。
所以这个应该是lea
最后看看case 4
1 2 3 4 5 6 7 8 9 case 4 : ++i_p; v13 = reg[0 ]; v14 = reg[0 ] * (reg[1 ] + 1 ); reg[0 ] = vmcode[i_p] ^ 0x414345 ; result = (unsigned int )((reg[0 ] ^ (reg[1 ] + v13)) % 256 + (((reg[0 ] ^ (v13 * reg[1 ])) % 256 + (((reg[0 ] ^ (reg[1 ] + v14)) % 256 ) << 8 )) << 8 )); reg[1 ] = result; goto LABEL_10;
看不懂,应该是什么计算
这个时候发现case5和case6有点问题,应该是reg的大小不够,试着将reg大小增加一下
这里增加到32bit(即4Byte)
至此,VM的指令大体是解决了
接下来就要看看这个绘制了
这里贴一个D3D入门教程:https://blog.csdn.net/u014038143/article/details/82730776
在青色位置下断点,然后观察各参数的变化
这里贴一个绘制函数的部分参数解析(根据上面的教程和上下文可以推测出来)
1 2 3 4 5 6 7 8 9 10 11 __int64 __fastcall sub_0 ( int x, int y, int a3, int a4, float color, ID3D11DeviceContext *a6, ID3D11Buffer *buffer, ID3D10InputLayout *inputlayout, ID3D10VertexShader *vertexshader, __int64 pixelshader)
在绘制的函数中有几个比较重要的值
1 2 3 4 5 6 7 8 9 10 v15 = a5 + (a3 ^ (a1 + a2)) % 256 - a4 % 256 ; v16 = (a3 ^ (a2 * a1)) % 256 - (a4 >> 8 ) % 256 ; v17 = (float )(v37 - (float )(2 * (v16 + a2) - 1 )) / v37; v18 = (float )(v37 - (float )(2 * a2 + 99 )) / v37; v19 = (float )((float )(2 * (v16 + a1) - 1 ) - v36) / v36; v20 = (float )((float )(2 * a1 + 99 ) - v36) / v36;
看来长宽就是在这一段里面了,而黄色旗子无法绘制就是与a1和a2有关
先写个解释器,看看VM干了什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 import structdef read_int_array (file_name ): with open (file_name, 'rb' ) as f: data = f.read() return struct.unpack('i' * (len (data) // 4 ), data) a = read_int_array('./vmcode.txt' ) ip = 0 regs = [0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 50 , 50 ] nop_offset = [] nop_ins_offset = [] blue_cnt = 0 reg4_is_set = 0 reg5_is_set = 0 yellow_cnt = 0 while ip < 0x1301 : code = a[ip] print (hex (ip)[2 :].zfill(5 ) + " " , end='' ) if (code == 0 ): print (f"add regs[0] regs[1]" ) regs[0 ] += regs[1 ] ip += 1 elif (code == 1 ): print (f"sub regs[0] regs[1]" ) regs[0 ] -= regs[1 ] ip += 1 elif (code == 2 ): print (f"mov regs[{a[ip+2 ]} ] regs[{a[ip+1 ]} ]" ) if (blue_cnt == 0 ): if (a[ip+2 ] == 3 ): nop_offset.append(ip) if (a[ip+2 ] == 4 ): if (reg4_is_set): nop_ins_offset.append(ip) reg4_is_set = 1 if (a[ip+2 ] == 5 ): if (reg5_is_set): nop_ins_offset.append(ip) reg5_is_set = 1 regs[a[ip+2 ]] = regs[a[ip+1 ]] ip += 3 elif (code == 3 ): print (f"lea regs[{a[ip+2 ]} ] {hex (a[ip+1 ])} " ) regs[a[ip+2 ]] = a[ip+1 ] ip += 3 elif (code == 4 ): print (f"cal coordinate " , end='| ' ) val1 = regs[0 ] v14 = regs[0 ] * (regs[1 ] + 1 ) regs[0 ] = a[ip+1 ] ^ 0x414345 op = ((regs[0 ] ^ (regs[1 ] + val1)) % 256 + (((regs[0 ] ^ (val1 * regs[1 ])) % 256 + (((regs[0 ] ^ (regs[1 ] + v14)) % 256 ) << 8 )) << 8 )) regs[1 ] = op ip += 2 print ("CalRes:(" , hex (regs[0 ]), hex (regs[1 ]), ")" ) elif (code == 5 ): print (f"draw yellow block" , yellow_cnt, end=' | ' ) print ("args: x:" , hex (regs[4 ]), "y:" , hex (regs[5 ]), hex (regs[6 ]), hex (regs[7 ]), "\n" ) reg4_is_set = 0 reg5_is_set = 0 yellow_cnt += 1 ip += 1 elif (code == 6 ): print (f"draw blue block" , blue_cnt, end=' | ' ) print ("args: x:" , hex (regs[4 ]), "y:" , hex (regs[5 ]), hex (regs[6 ]), hex (regs[7 ]), "\n" ) blue_cnt += 1 ip += 1 elif (code == 7 ): print (f"retn" ) break else : print (f" unknown code {hex (code)} " ) break print (nop_offset) print (nop_ins_offset)
最后输出结果如下:
以上图为例,在绘制黄色方块的时,在cal coordinate操作前后相比起绘制青色方块时会多出一些操作
由于绘制第0个蓝色方块时与后面绘制有所偏差,所以以后面绘制为准,将绘制黄色方块的操作与后面绘制蓝色方块的操作进行比较
上面取了两个典型的差异,可见
1.黄色方块绘制时会将regs[0]和regs[1]的位置进行互换
2.绘制某些黄色方块时会让regs[0]的值减去0x1f4然后再放入regs[5]中
3.绘制某些黄色方块时会让regs[0]的值减去0x3e8然后再放入regs[4]中
所以写补丁要去除上述差异,补丁应该执行的操作有:
1.禁止互换
2.替换0x3e8和0x1f4,使其成为0x00
上面的解释器中有写出哪些地方属于swap,哪些地方属于存在0x1f4和0x3e8的情况
据此可以写出补丁(IDA)
1 2 3 4 5 6 7 8 9 10 11 12 vmcode = get_name_ea_simple("vmcode" ) nop_offset = [30 , 121 , 161 , 252 , 361 , 460 , 514 ] nop_offset2 = [13 , 69 , 100 , 190 , 210 , 285 , 305 , 350 , 394 , 449 , 493 ] for j in nop_offset: idaapi.patch_bytes(vmcode+j*4 , b"\x90" *9 *4 ) for k in nop_offset2: idaapi.patch_bytes(vmcode+k*4 , b"\x90" *3 *4 ) print ("Done" )
先patch一下GetTickCount
然后执行一下脚本
最后结果如下: