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,比较好读(

可以看到这两个分别是ZwAllocateirtuaZwFreeVirtualMem

回到函数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 += 2i64;
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 += 2i64;
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;
// 4次一循环,经过测试这个(a3 ^ (a1 + a2)) % 256 == a4 % 256,即v15就是color
v16 = (a3 ^ (a2 * a1)) % 256 - (a4 >> 8) % 256;
// (a3 ^ (a2 * a1)) % 256 == (a4 >> 8) % 256
v17 = (float)(v37 - (float)(2 * (v16 + a2) - 1)) / v37;
// v37 = 845 高度, 由此可以猜测a2为高度
v18 = (float)(v37 - (float)(2 * a2 + 99)) / v37;
v19 = (float)((float)(2 * (v16 + a1) - 1) - v36) / v36;
// v36 = 1540 宽度,由此可以猜测a1为宽度
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 struct


def 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')
# vm init
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
# vm start
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) # swap
print(nop_ins_offset) # remove dump

最后输出结果如下:

以上图为例,在绘制黄色方块的时,在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]

# remove swap
for j in nop_offset:
idaapi.patch_bytes(vmcode+j*4, b"\x90"*9*4)

# remove dup
for k in nop_offset2:
idaapi.patch_bytes(vmcode+k*4, b"\x90"*3*4)
print("Done")

先patch一下GetTickCount

然后执行一下脚本

最后结果如下: