2022HWS硬件安全冬令营 X DASCTF Jan


做完了re,简单总结下本次的比赛,以为是个小比赛,也没奖,冬令营很多人估计也不会去,没想到这么多人来打,且人均全栈。。。

基本上考的最多的就是去花指令和vm动调了,算法部分都不是太难,毕竟有两道是vm题型,还有道是加了控制流平坦化。之前也总结过vm的专题文章,主要是调试和明白变量所带的含义。

快过春节了,新年快乐。

BabyVM

先去花,只有jnz,jz,E8一种花,patch掉E8就行,但是比较多,可以考虑写个idapython脚本。

之后可以在这个位置找到vm的初始化。

可以看到对于vm的初始化可以看出,这个vm还是比较复杂,甚至有EIP和跳转之类的东西,而不是简单的一条流水线。

然后到了主函数。

可以看到opcode是3个8字节的数为一组,一共24,相当于第一个数是case语句的选择,第二个数通过调试可以知道,代表的是30字节table的index/2,第三个数就是操作的数据。

我们打印出所有的opcode。

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

//flag{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}
#include<stdio.h>

int main()
{
unsigned char opcode1[];
unsigned char opcode2[];
unsigned char opcode3[];
unsigned char opcode4[];
int i,k=0;
for(i=0;i<1583;i+=24)
{
printf("%x : ",k++);
printf("%x, ",*(__int64*)(&opcode1[i]));
printf("%llx, ",*(__int64*)(&opcode1[i+8]));
printf("%llx, ",*(__int64*)(&opcode1[i+16]));

printf("\n");
}

printf("\n\n\n");
k=0;
for(i=0;i<191;i+=24)
{
printf("%x : ",k++);
printf("%x, ",*(__int64*)(&opcode2[i]));
printf("%llx, ",*(__int64*)(&opcode2[i+8]));
printf("%llx, ",*(__int64*)(&opcode2[i+16]));

printf("\n");
}

printf("\n\n\n");

k=0;
for(i=0;i<887;i+=24)
{
printf("%x : ",k++);
printf("%x, ",*(__int64*)(&opcode4[i]));
printf("%llx, ",*(__int64*)(&opcode4[i+8]));
printf("%llx, ",*(__int64*)(&opcode4[i+16]));

printf("\n");
}

printf("\n\n\n");
k=0;
for(i=0;i<1836;i+=24)
{
printf("%x : ",k++);
printf("%x, ",*(__int64*)(&opcode3[i]));
printf("%llx, ",*(__int64*)(&opcode3[i+8]));
printf("%llx, ",*(__int64*)(&opcode3[i+16]));

printf("\n");
}

printf("\n\n\n");
}

然后来看看vm内部如何处理的。

opcode3,注释已经标里面了。

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

#打印input flag:
0 : 12, 0, 0,
1 : 12, 1, 1,
2 : 12, 2, 2,
3 : 12, 3, 3,
4 : 12, 6, 6,
5 : 12, 7, 7,
6 : 1, 0, 69,
7 : 1, 1, 6e,
8 : 1, 2, 70,
9 : 1, 3, 75,
a : 1, 6, 74,
b : 1, 7, 20,
c : 18, 0, ffffffffffffffff,
d : 18, 1, ffffffffffffffff,
e : 18, 2, ffffffffffffffff,
f : 18, 3, ffffffffffffffff,
10 : 18, 6, ffffffffffffffff,
11 : 18, 7, ffffffffffffffff,
12 : 1, 0, 66,
13 : 1, 1, 6c,
14 : 1, 2, 61,
15 : 1, 3, 67,
16 : 1, 6, 3a,
17 : 1, 7, 20,
18 : 18, 0, ffffffffffffffff,
19 : 18, 1, ffffffffffffffff,
1a : 18, 2, ffffffffffffffff,
1b : 18, 3, ffffffffffffffff,
1c : 18, 6, ffffffffffffffff,
1d : 18, 7, ffffffffffffffff,
1e : 12, 1, 1,
#循环getchar(),长度为0x26
1f : 17, 0, ffffffffffffffff, //getchar
20 : 5, 0, ffffffffffffffff, //push to add1+偏移
21 : 7, 1, 1, //i++
22 : 1a, 1, 26, //0x26
23 : 1e, 1f, ffffffffffffffff, //决定是否跳转到1f
24 : 19, ffffffffffffffff, ffffffffffffffff,

这段opcode设计到一些重要的case语句。

case 0x17: 读取一个字符。

case 5: push flag[i] 到addr+偏移的地址处。

case 7: 类似于i++之类的操作,i存放在table的某个位置,忘了。

case 0x1A: 比较i是否等于0x26,从而改变伪标志寄存器,也就是那4个byte。

case 0x1E: 根据伪标志寄存器决定是否跳转,第一个参数就是代表要跳转的地方。

调试可知,前面很大一部分都是在读数据,然后打印,然后后面部分就是在循环getchar()。

sub_C22AB0函数断下来,opcode4,计算部分了。

0 : 6, 0, ffffffffffffffff,            //pop form add1+偏移 到T[0],注意相当于是从最下面开始pop的,也就是先pop的 '}'

1 : 1a, 0, 7d,
2 : 1c, 12, ffffffffffffffff,        //等于7d则跳到12
#failed。
3 : 1, 0, 77,
4 : 1, 1, 72,
5 : 1, 2, 6f,
6 : 1, 3, 6e,
7 : 1, 6, 67,
8 : 1, 7, 21,
9 : 18, 0, ffffffffffffffff,
a : 18, 1, ffffffffffffffff,
b : 18, 2, ffffffffffffffff,
c : 18, 3, ffffffffffffffff,
d : 18, 6, ffffffffffffffff,
e : 18, 7, ffffffffffffffff,
f : 1, 0, a,
10 : 18, 0, ffffffffffffffff,
11 : 19, ffffffffffffffff, ffffffffffffffff,
#开始循环,不断pop出0x19个数据,也就是flag{},花括号里面的部分,并且push到add2+偏移。
12 : 1, 8, 100,
13 : 1a, 8, e1,
14 : 1e, 19, ffffffffffffffff,
15 : 6, 0, ffffffffffffffff,            //pop form add1+偏移
16 : 4, 8, 0,                            //push to add2+偏移
17 : 9, 8, 1,
18 : 1d, 13, ffffffffffffffff,            //是否跳转到13

#比较前5个字符是否是flag{
19 : 6, 0, ffffffffffffffff,
1a : 1a, 0, 7b,
1b : 1f, 3, ffffffffffffffff,            //是否跳转到3,也就是failed
1c : 6, 0, ffffffffffffffff,
1d : 1a, 0, 67,
1e : 1f, 3, ffffffffffffffff,
1f : 6, 0, ffffffffffffffff,
20 : 1a, 0, 61,
21 : 1f, 3, ffffffffffffffff,
22 : 6, 0, ffffffffffffffff,
23 : 1a, 0, 6c,
24 : 1f, 3, ffffffffffffffff,
25 : 6, 0, ffffffffffffffff,
26 : 1a, 0, 66,
27 : 1f, 3, ffffffffffffffff,    
#开始循环加密和比较。        
28 : 12, 9, 9,                     //异或清零T[2*9]
29 : 1, a, e1,
2a : 3, 7, 9,                    //pop form add2+偏移,获取enc[i]
2b : 3, 6, a,                    //pop form add2+偏移,获取flag[i]
2c : 11, 6, 42,                    //flag[i]^42
2d : d, 6, 2,                    //flag[i]<<2,存放在T[6*2]
2e : 1b, 6, 7,                    //比较flag[i]和enc[i]从而改变标志寄存器
2f : 1f, 3, ffffffffffffffff,        
30 : 7, 9, 1,
31 : 7, a, 1,
32 : 1a, 9, 20,                    //判断循环是否结束
33 : 1e, 2a, ffffffffffffffff,    //是否跳转到2a
#success
34 : 1, 0, 63,
35 : 1, 1, 6f,
36 : 1, 2, 72,
37 : 1, 3, 72,
38 : 1, 6, 65,
39 : 1, 7, 63,
3a : 18, 0, ffffffffffffffff,
3b : 18, 1, ffffffffffffffff,
3c : 18, 2, ffffffffffffffff,
3d : 18, 3, ffffffffffffffff,
3e : 18, 6, ffffffffffffffff,
3f : 18, 7, ffffffffffffffff,
40 : 1, 0, 74,
41 : 1, 1, 6c,
42 : 1, 2, 79,
43 : 1, 3, 21,
44 : 1, 6, a,
45 : 18, 0, ffffffffffffffff,
46 : 18, 1, ffffffffffffffff,
47 : 18, 2, ffffffffffffffff,
48 : 18, 3, ffffffffffffffff,
49 : 18, 6, ffffffffffffffff,
4a : 19, ffffffffffffffff, ffffffffffffffff,
4b : 0, 0, 0,
4c : 0, 4d0000000000, 402a100000072000,

一些重要的case语句。
case 6:pop 语句,从add1+偏移地址处,取数据提到T[0],是反着提的。

case 0x1C: 和1E差不多,根据伪标志寄存器决定是否跳转,第一个参数就是代表要跳转的地方。

case 0x11:^42

case 0xd: <<2

case 3:pop form add2+偏移

case 4:push to add2+偏移

所以加密就是(flag[i]^0x42)<<2

1
2
3
4
5
6
7
8
9
10
11
12

enc=[0x000000000000009C, 0x00000000000001C0, 0x00000000000001D8, 0x00000000000001D4,
0x00000000000001D4, 0x00000000000001E8, 0x00000000000001C8, 0x0000000000000098,
0x00000000000001C8, 0x00000000000001C0, 0x00000000000001EC, 0x000000000000008C,
0x00000000000001D4, 0x000000000000008C, 0x00000000000001EC, 0x00000000000001EC,
0x00000000000001C0, 0x00000000000001C0, 0x00000000000001D8, 0x00000000000001D4,
0x000000000000009C, 0x00000000000001D0, 0x00000000000001D0, 0x00000000000001D0,
0x00000000000001D4, 0x00000000000001E8, 0x00000000000001D0, 0x00000000000001EC,
0x00000000000001C8, 0x00000000000001C8, 0x00000000000001E8, 0x000000000000008C]
for i in enc:
print(chr((i>>2) ^0x42),end='')
#e247780d029a7a992247e6667869008a

虽然这道题加密部分并不是很难,但是这种vm的格式却让我眼前一亮,不仅有EIP,更是模拟出来了C语言中的for循环,不再是简简单单的流水线式opcode。

EasyVM

做题时的笔记没了,g。

开头一个NtCurrentPeb()->BeingDebugged,简单绕过下就行。

然后会引发一个异常。

断下来后我们就可以看到大概的逻辑了。

sub_401000函数,vm结构体,vm指令处理函数。

struct vm {
    _DWORD* functions;
    _DWORD* EIP;
    _DWORD eax;
    _DWORD ebx;
    _DWORD ecx;
    _DWORD edx;
    _DWORD* enc;
    _DWORD 忘了;
    _DWORD* flag;
}


c++类实现的,还是动调慢慢分析一轮吧,实际上需要分析两轮,函数对EIP的修改都不一样,opcode很少,加密部分在循环,基本上只用到eax,ebx,ecx三个个寄存器。ecx作为index。

加密逻辑就是

flag[0]^0xee^0
flag[1]^0xee^flag[0]
flag[2]^0xee^flag[1]
···
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

import base64
enc=[ 0xBE, 0x36, 0xAC, 0x27, 0x99, 0x4F, 0xDE, 0x44, 0xEE, 0x5F,
0xDA, 0x0B, 0xB5, 0x17, 0xB8, 0x68, 0xC2, 0x4E, 0x9C, 0x4A,
0xE1, 0x43, 0xF0, 0x22, 0x8A, 0x3B, 0x88, 0x5B, 0xE5, 0x54,
0xFF, 0x68, 0xD5, 0x67, 0xD4, 0x06, 0xAD, 0x0B, 0xD8, 0x50,
0xF9, 0x58, 0xE0, 0x6F, 0xC5, 0x4A, 0xFD, 0x2F, 0x84, 0x36,
0x85, 0x52, 0xFB, 0x73, 0xD7, 0x0D, 0xE3]
print(hex(0xBE^0xee),end=',')
for i in range(1,len(enc)):
print(hex(enc[i-1]^enc[i]^0xee),end=',')
table=[0x50,0x66,0x74,0x65,0x50,0x38,0x7f,0x74,0x44,0x5f,0x6b,0x3f,0x50,0x4c,0x41,0x3e,0x44,0x62,0x3c,0x38,0x45,0x4c,0x5d,0x3c,0x46,0x5f,0x5d,0x3d,0x50,0x5f,0x45,0x79,0x53,0x5c,0x5d,0x3c,0x45,0x48,0x3d,0x66,0x47,0x4f,0x56,0x61,0x44,0x61,0x59,0x3c,0x45,0x5c,0x5d,0x39,0x47,0x66,0x4a,0x34,0x0]
key=[0xa,0xb,0xc,0xd]

for i in range(len(table)):
table[i]=table[i]^key[i%4]
print("\n")
flag=base64.b64decode(bytes(table))
print(flag)
#flag{2586dc76-98d5-44e2-ad58-d06e6559d82a}

babyre

先去花,这里有两种花,一种和前面的差不多,还有种如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

.text:00401513 push eax
.text:00401514 push ecx
.text:00401515 call sub_4010A0
.text:0040151A add eax, 15h
.text:0040151D push eax
.text:0040151E pop eax
.text:0040151F mov ecx, 0FFFFFFFFh
.text:00401524 xor eax, ecx
.text:00401526 push eax
.text:00401527 pop ecx
.text:00401528 not ecx
.text:0040152A push ecx
.text:0040152B retn
.text:0040152B ; ---------------------------------------------------------------------------
.text:0040152C db 7Eh ; ~
.text:0040152D db 0FFh
.text:0040152E db 15h
.text:0040152F ; ---------------------------------------------------------------------------
.text:0040152F pop ecx
.text:00401530 pop eax

解决方法,全部patch即可。

之后就可以看到流程了,加了平坦化控制流,尝试去了下没去掉,今天日常访问大佬博客Mason,看到了个github链接https://github.com/moliam/llvm-fla-cracker

然后在有数据处理的地方打上断点。

加密一

1
2
3
4
5
6
7
8
9

case 399047545:
v35 = ~flag[i] & v35 | ~v35 & flag[i];
v36 = ~flag[i];
v23 = -1395711487;
break;

flag[v33] = ~flag[v33] & v35 | ~v35 & flag[v33];

就是将32byte转为8DWORD,然后全部异或产生一个值,然后和flag[i]挨着异或,翻译下就是

1
2
3
4
5
6
7
8
9
10
11

flag=[0x6463626b,0x6867666b,0x6c6b6a6e,0x706f6e39,0x74737271,0x78777675,0x42417a79,0x46454443]
temp=0
for i in range(8):
temp=flag[i]^temp
print(hex(temp))

for i in range(8):
flag[i]=flag[i]^temp
print(hex(flag[i]),end=',')

解的话爆破就行,异或的特性,可以看做单字节单字节的,得到异或的4byte数据。

1
2
3
4
5
6
7
8
9
10
11

enc=[0x6660656b,0x35613568,0x3161306e,0x633d6039,0x32666535,0x3063306c,0x3060606b,0x603d336b]
xor=[]
for k in range(4):
for i in range(0,127):
a=0
for j in range(8):
a^=((enc[j]>>8*k)&0xff)^i
if(a==i):
xor.append(hex(i))
print(xor)

然后第二部分就是变表base加密,3个为一组,共11轮,除了第一组,每次都进行base64加密的时候,都会将后面还没base64加密的数据先异或一个长度为三的key,key由上一轮base64加密中table[index]中的index组成。翻译如下。

1
2
3
4
5
6
7
8

key=[0]*3
for r in range(10):
key[0] = flag[r * 3] >> 2
key[1] = (flag[r * 3] & 0x3) << 4 | (flag[r * 3 + 1] >> 4)
key[2]=(flag[r * 3 + 1] & 0xf) << 2 | (flag[r * 3 + 2] >> 6)
for i in range((r+1)*3,33):
flag[i]=flag[i]^key[i%3]

解的话可以采用z3,如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

from z3 import *
enc=[0x6b,0x65,0x60,0x7c,0x5e,0x20,0x64,0x6,0x43,0x2c,0x52,0x5,0x2e,0x56,0x1,0x7f,0x26,0x41,0x65,0x13,0x51,0x2a,0x53,0x0,0x7b,0x75,0x5c,0x3e,0x49,0x1a,0x3c,0x66,0x50]


s = Solver()
flag = [BitVec('flag[%d]' % i, 8) for i in range(33)]

key=[0]*3
for r in range(10):
key[0] = flag[r * 3] >> 2
key[1] = (flag[r * 3] & 0x3) << 4 | (flag[r * 3 + 1] >> 4)
key[2]=(flag[r * 3 + 1] & 0xf) << 2 | (flag[r * 3 + 2] >> 6)
for i in range((r+1)*3,33):
flag[i]=flag[i]^key[i%3]
for i in range(33):
s.add(flag[i]==enc[i])

print(s.check())
m=s.model()
print(m)

或者采用下面的方式,先求出所有的key,然后以同样的方式去异或。

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

import base64

# 解变表base
str1 = 'Fi9X/fxX6Q6JBfUfBM1V/y6V6PcPjMaQLl9IuttFuH68'
string1 = 'QVEJAfHmUYjBac+u8Ph5n9Od16FrICL/X0GvtM4qk7T2z3wNSsyoebilxWKgZpRD'
string2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

for i in range(len(str1)):
print(hex(string1.index(str1[i])), end=',')
print("\n")
# get key1
for i in range(len(str1)):
if ((i + 1) % 4 == 0):
continue
print(hex(string1.index(str1[i])), end=',')
enc = base64.b64decode(str1.translate(str.maketrans(string1, string2)))

for i in enc:
print(hex(i), end=',')

enc = [0x6b, 0x65, 0x60, 0x7c, 0x5e, 0x20, 0x64, 0x6, 0x43, 0x2c, 0x52, 0x5, 0x2e, 0x56, 0x1, 0x7f, 0x26, 0x41, 0x65,
0x13, 0x51, 0x2a, 0x53, 0x0, 0x7b, 0x75, 0x5c, 0x3e, 0x49, 0x1a, 0x3c, 0x66, 0x50]

key1 = [0x1a, 0x36, 0x15, 0x1f, 0x5, 0x38, 0x19, 0x0, 0x19, 0xb, 0x5, 0x8, 0xb, 0x25, 0x18, 0x1f, 0x32, 0x19, 0x19,
0x11, 0xd, 0xa, 0x25, 0xc, 0x1e, 0x37, 0x15, 0xf, 0x24, 0x24, 0xf, 0x6, 0x19]

key = [0] * 3
for r in range(10):
key[0] = key1[r * 3]
key[1] = key1[r * 3 + 1]
key[2] = key1[r * 3 + 2]
print(key)
for i in range((r + 1) * 3, 33):
enc[i] = enc[i] ^ key[i % 3]

flag = ''
xor = [0xd, 0x6, 5, 0x53]
for i in range(32):
flag += chr(enc[i] ^ xor[i % 4])
print(flag)
#fce5e3dfc6db4f808ccaa6fcffecf583