2023春秋杯网络安全联赛冬季赛

Last updated on January 27, 2024 am

coos,file_encryptor

coos

整体分析

程序逻辑主函数逻辑很清晰,接受输入,在函数sub_47118B对输入进行加密,判断。

其中开头的两个函数sub_4712D0和函数sub_47128A进行了一些初始化操作,将vm的寄存器给置零等,在这里先把寄存器给重新命名以便审计

不断跟进sub_47118B加密函数,开头也是些初始化的操作,都是与输入无关的,可以直接跳过不看。

一直拉到最下面可以看到真正开始加密的地方

不断跟进sub_4112DF

就看到真正虚拟机的样子了。

但是这题有几点不一样的地方,题目文件是32位的,但这里虚拟机却是64位的。跟进sub_41141A,就可以发现这里把64位拆成两个32位,后续所有的操作中都是如此。即通过32位数组的形式实现64位的操作。

还有一点在于这里的虚拟机更像是虚拟函数,因为这里面嵌套了虚拟机,根据opcode的不同实现不同的功能。

在后面三个操作码的地方,再次调用了三个函数,每个函数又会再次调用这套虚拟机。

即有一个主的main_opcode操作码数组,还有三个sub_opcode的子操作码数组

相关变量重命名如图

翻译opcode

第一步还是先把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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def get_code(opcode, rip):
code = {1: ["mov r1,r2", 1],
2: ["mov r1,r3", 1],
3: ["mov r1,r4", 1],
4: ["mov r2,r1", 1],
5: ["mov r2,r3", 1],
6: ["mov r2 r4", 1],
7: ["mov r3,r1", 1],
8: ["mov r3,r2", 1],
9: ["mov r3,r4", 1],
10: ["mov r4,r1", 1],
11: ["mov r4,r2", 1],
12: ["mov r4,r3", 1],
13: [f"mov r4,0x{(opcode[rip + 1])[2:]}", 2],
14: ["nop", 1],
15: ["nop", 1],
16: ["nop", 1],
17: [f"shl r1,{(opcode[rip + 1])}", 2],
18: ["pop r3", 1],
19: ["nop", 1],
20: [f"shr r2 {(opcode[rip + 1])}", 2],
21: ["pop r4", 1],
22: ["nop", 1],
23: ["nop", 1],
24: ["add r1,r3", 1],
25: ["push r1", 1],
28: ["push r1", 1],
26: [f"shr r1 {opcode[rip + 1]}", 2],
30: [f"shr r1 {opcode[rip + 1]}", 2],
27: ["nop", 1],
29: ["nop", 1],
31: ["nop", 1],
32: ["nop", 1],
33: ["nop", 1],
34: ["j__dynamic_initializer_for__lang::GlobalStorage::sm_instance__()", 1],
35: ["pop r2", 1],
36: ["add r2,r1", 1],
37: ["pop r1", 1],
38: [f"xor r1,{opcode[rip + 1]}", 2],
39: ["push r3", 1],
40: [f"xor r1,r2", 1],
41: [f"xor r2,{opcode[rip + 1]}", 2],
42: ["add r2,r3", 1],
43: ["xor r2,r3", 1],
44: ["nop", 2],
45: ["add r1,r2", 1],
47: ["nop", 2],
48: ["cmp", 0],
49: ["nop", 2],
50: ["nop", 1],
51: ["nop", 2],
52: ["nop", 1],
53: ["nop", 1],
54: ["nop", 1],
55: [f"and r1,{opcode[rip + 1]}", 2],
56: ["nop", 1],
57: [f"mov r3,{opcode[rip + 1]}", 2],
58: [f"mov r1,{opcode[rip + 1]}", 2],
59: ["nop", 1],
60: ["nop", 1],
61: ["shl r3,r1", 1],
62: ["nop", 2],
63: ["nop", 1],
64: ["nop", 1],
65: ["nop", 2],
66: [f"mov r6 {opcode[rip + 1]}", 2],
67: ["mov r2,r5", 1],
68: ["shr r2,r1", 1],
69: ["mov r2,dword_47E000[r1]", 1],
70: [f"shl r4,{opcode[rip + 1]}", 2],
71: ["shl r2,r4", 1],
72: ["add r6,r2", 1],
73: ["add r3,1", 1],
74: ["shr r2,r3", 1],
75: ["mov r2,dword_47E080[r3]", 1],
76: ["shl r1,r2", 1],
77: ["add r6,r1", 1],
78: ["push r5\n"
"\t push dword_4803A8\n"
"\t call vm(sub_code_1,5)\n"
"\t mov r5,r1", 1],
79: ["mov r3,0\n"
"\t call vm(sub_code_2,243)\n"
"\t mov r5,r6", 1],
80: ["mov r6,0\n"
"\t mov r3,0\n"
"\t call vm(sub_code_3,579)\n"
"\t mov r5,r6\n"
"\t check dword_4805C8 == 31", 1],
}

print(opcode[rip], '\t', code[opcode[rip]][0])
rip += code[opcode[rip]][1]
return rip

主函数opcode

现在就可以把main_opcode的操作给打出来看看

1
2
3
4
5
r = 0
while 1:
r = get_code(main_opcode, r)
if r >= len(main_opcode):
break
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
"""
57 mov r3,0

78 push r5
push KEY[key_index]
key_index+=1
call vm(sub_code_1,5)
mov r5,r1
79 mov r3,0
call vm(sub_code_2,243)
mov r5,r6
80 mov r6,0
mov r3,0
call vm(sub_code_3,579)
mov r5,r6
check key_index == 31
73 add r3,1

78 push r5
push KEY[key_index]
key_index+=1
call vm(sub_code_1,5)
mov r5,r1
79 mov r3,0
call vm(sub_code_2,243)
mov r5,r6
80 mov r6,0
mov r3,0
call vm(sub_code_3,579)
mov r5,r6
check key_index == 31
73 add r3,1
…………
"""

简单分析一下,不难发现,这里其实就是一个pattern重复31次,pattern为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"""
78 push r5
push KEY[key_index]
key_index+=1
call vm(sub_code_1,5)
mov r5,r1
79 mov r3,0
call vm(sub_code_2,243)
mov r5,r6
80 mov r6,0
mov r3,0
call vm(sub_code_3,579)
mov r5,r6
check key_index == 31
73 add r3,1
"""

那么这里r3就应该充当一个rounds的变量,下一步就是继续查看剩下三个子操作的功能,,在虚拟机开始之前可以打个断点,看看输入会被放在哪便于审计。

然后就是把三个子函数给打出来

子函数一

37 pop r1
35 pop r2
40 xor r1,r2
38 xor r1,51

结合子函数之前的操作

78 push r5
push KEY[key_index]
key_index+=1
call vm(sub_ code_1,5)

那么这里就是对输入进行简单异或

子函数二

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
"""
66 mov r6 0 r6: sum = 0
57 mov r3,0 r3: rounds = 0

2 mov r1,r3
17 shl r1,2 r1: rounds << 2
67 mov r2,r5 r2: enc = input ^ key
68 shr r2,r1 r2: enc >> (rounds << 2)
1 mov r1,r2 r1: enc >> (rounds << 2)
55 and r1,15 r1: (enc >> (rounds << 2)) & 15
69 mov r2,arr1[r1] r2: arr1[((enc) >> (rounds << 2)) & 15]
12 mov r4,r3 r4: rounds
70 shl r4,2 r4: rounds << 2
71 shl r2,r4 r2: (arr1[((enc) >> (rounds << 2)) & 15]) << (rounds << 2)
72 add r6,r2 r6: sum += (arr1[((enc) >> (rounds << 2)) & 15]) << (rounds << 2)
73 add r3,1 r3: rounds += 1

2 mov r1,r3
17 shl r1,2 r1: rounds << 2
67 mov r2,r5 r2: enc = input ^ key
68 shr r2,r1 r2: enc >> (rounds << 2)
1 mov r1,r2 r1: enc >> (rounds << 2)
55 and r1,15 r1: (enc >> (rounds << 2)) & 15
69 mov r2,arr1[r1] r2: arr1[((enc) >> (rounds << 2)) & 15]
12 mov r4,r3 r4: rounds
70 shl r4,2 r4: rounds << 2
71 shl r2,r4 r2: arr1[((enc) >> (rounds << 2)) & 15]
72 add r6,r2 r6: sum += (arr1[((enc) >> (rounds << 2)) & 15]) << (rounds << 2)
73 add r3,1 rounds += 1
………………
"""

同样这里是一个pattern重复16次,这里arr1可以通过调试拿到

1
arr1 = [2, 1, 7, 4, 8, 15, 14, 3, 13, 10, 0, 9, 11, 6, 5, 12]

与子函数结合一起的python代码为

1
2
3
4
5
6
7
8
def calc_sum_1(state,key_index):
sum_ = 0
rounds = 0
state = state ^ KEY[key_index] ^ 51
for i in range(16):
sum_ += (arr1[(state >> (rounds << 2)) & 15]) << (rounds << 2)
rounds += 1
return sum_

子函数三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"""
66 mov r6 0 r6: total = 0
57 mov r3,0 r3: rounds = 0

67 mov r2,r5 r2: sum
74 shr r2,r3 r2: sum >> rounds
1 mov r1,r2 r1: sum >> rounds
55 and r1,1 r1: (sum >> rounds) & 1
75 mov r2,arr2[r3] r2: arr2[rounds]
76 shl r1,r2 r1: ((sum >> rounds) & 1) << arr2[rounds]
77 add r6,r1 r6: total += ((sum >> rounds) & 1) << arr2[rounds]
73 add r3,1

67 mov r2,r5
74 shr r2,r3
1 mov r1,r2
55 and r1,1
75 mov r2,arr2[r3]
76 shl r1,r2
77 add r6,r1
73 add r3,1
…………
"""

同样一个pattern重复64次,arr2由调试拿到

1
2
3
arr2 = [63, 47, 31, 15, 62, 46, 30, 14, 61, 45, 29, 13, 60, 44, 28, 12, 59, 43, 27, 11, 58, 42, 26, 10, 57, 41, 25, 9,
56, 40, 24, 8, 55, 39, 23, 7, 54, 38, 22, 6, 53, 37, 21, 5, 52, 36, 20, 4, 51, 35, 19, 3, 50, 34, 18, 2, 49, 33,
17, 1, 48, 32, 16, 0]

对应python 代码

1
2
3
4
5
6
7
def calc_sum_2(sum_1):
total = 0
rounds = 0
for i in range(64):
total += ((sum_1 >> rounds) & 1) << p_box[rounds]
rounds += 1
return total

整体加密

将所有加密流程整合起来,其中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
43
44
45
46
47
48
arr1 = [2, 1, 7, 4, 8, 15, 14, 3, 13, 10, 0, 9, 11, 6, 5, 12]
arr2 = [63, 47, 31, 15, 62, 46, 30, 14, 61, 45, 29, 13, 60, 44, 28, 12, 59, 43, 27, 11, 58, 42, 26, 10, 57, 41, 25, 9,
56, 40, 24, 8, 55, 39, 23, 7, 54, 38, 22, 6, 53, 37, 21, 5, 52, 36, 20, 4, 51, 35, 19, 3, 50, 34, 18, 2, 49, 33,
17, 1, 48, 32, 16, 0]

KEY = [7596569225765413736, 14892793705278438672, 14897626102458855899, 15127138055849948981, 17864553494577085546,
1138107802150484470, 8911548945514081943, 3235953879216500034, 11010657345162096808, 14479365304648704483,
12150232467342833339, 12487849248796427385, 13416156134946011488, 5920119153271060313, 8053781650739871525,
9558394794771259844, 4339208423645057876, 16476906405380659427, 11912452293618884519, 1406977800464934583,
2088792745124906531, 12099790537768180319, 2503949348962038751, 1807992343561743431, 1910441527462264642,
10680376501786139932, 3656171727515946310, 8846275897411559763, 15016467168181810528, 14129026293696237890,
127218696325272760, 14650765194520711557]

def calc_sum_1(state):
sum_ = 0
rounds = 0
for i in range(16):
sum_ += (arr1[(state >> (rounds << 2)) & 15]) << (rounds << 2)
rounds += 1
return sum_


def calc_sum_2(sum_1):
total = 0
rounds = 0
for i in range(64):
total += ((sum_1 >> rounds) & 1) << arr2[rounds]
rounds += 1
return total


def encrypt(input_):
key_index = 0
s = 0
r5 = input_
for i in range(31):
s = r5 ^ KEY[key_index] ^ 51
s = calc_sum_1(s)
s = calc_sum_2(s)
r5 = s
key_index += 1

return s ^ 14650765194520711557 ^ 51


a = 0x6161616161616161
print(hex(encrypt(a)))

经过对比,发现就是present魔改加密,在github上有原始present解密,进行相应修改

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
Sbox = [2, 1, 7, 4, 8, 15, 14, 3, 13, 10, 0, 9, 11, 6, 5, 12]
Sbox_inv = [Sbox.index(x) for x in range(16)]

PBox = [63, 47, 31, 15, 62, 46, 30, 14, 61, 45, 29, 13, 60, 44, 28, 12, 59, 43, 27, 11, 58, 42, 26, 10, 57, 41, 25, 9,
56, 40, 24, 8, 55, 39, 23, 7, 54, 38, 22, 6, 53, 37, 21, 5, 52, 36, 20, 4, 51, 35, 19, 3, 50, 34, 18, 2, 49, 33,
17, 1, 48, 32, 16, 0]
PBox_inv = [PBox.index(x) for x in range(64)]
def addRoundKey(state, roundkey):
return state ^ roundkey ^ 51

def sBoxLayer(state):
"""SBox function for encryption

Input: 64-bit integer
Output: 64-bit integer"""

output = 0
for i in range(16):
output += Sbox[(state >> (i * 4)) & 0xF] << (i * 4)
return output


def sBoxLayer_dec(state):
"""Inverse SBox function for decryption

Input: 64-bit integer
Output: 64-bit integer"""
output = 0
for i in range(16):
output += Sbox_inv[(state >> (i * 4)) & 0xF] << (i * 4)
return output


def pLayer(state):
"""Permutation layer for encryption

Input: 64-bit integer
Output: 64-bit integer"""
output = 0
for i in range(64):
output += ((state >> i) & 0x01) << PBox[i]
return output


def pLayer_dec(state):
"""Permutation layer for decryption

Input: 64-bit integer
Output: 64-bit integer"""
output = 0
for i in range(64):
output += ((state >> i) & 0x01) << PBox_inv[i]
return output


def string2number(i):
""" Convert a string to a number

Input: string (big-endian)
Output: long or integer
"""
return int(i, 16)


def number2string_N(i, N):
"""Convert a number to a string of fixed size

i: long or integer
N: length of string
Output: string (big-endian)
"""
s = '%0*x' % (N * 2, i)
return s


class Present:

def __init__(self, rounds=32):
self.rounds = rounds
self.roundkeys = [7596569225765413736, 14892793705278438672, 14897626102458855899, 15127138055849948981,
17864553494577085546,
1138107802150484470, 8911548945514081943, 3235953879216500034, 11010657345162096808,
14479365304648704483,
12150232467342833339, 12487849248796427385, 13416156134946011488, 5920119153271060313,
8053781650739871525,
9558394794771259844, 4339208423645057876, 16476906405380659427, 11912452293618884519,
1406977800464934583,
2088792745124906531, 12099790537768180319, 2503949348962038751, 1807992343561743431,
1910441527462264642,
10680376501786139932, 3656171727515946310, 8846275897411559763, 15016467168181810528,
14129026293696237890,
127218696325272760, 14650765194520711557]


def decrypt(self, state):
"""Decrypt 1 block (8 bytes)

Input: ciphertext block as raw string
Output: plaintext block as raw string
"""
for i in range(self.rounds - 1):
state = addRoundKey(state, self.roundkeys[-i - 1])
state = pLayer_dec(state)
state = sBoxLayer_dec(state)
decipher = addRoundKey(state, self.roundkeys[0])
return decipher

def get_block_size(self):
return 8


test = Present()
enc = [11246785761149773209, 16090349977178840068, 13524222093275577792, 10243890929873528779]
for k in range(4):
src = test.decrypt(enc[k])
for i in range(8):
print(chr((src >> (8 * i)) & 0xff), end='')

# a9d99caef9ae999a299129c91299fc95

总结

  1. 确定寄存器、栈等变量
  2. 对操作进行翻译,把opcode和汇编对应起来
  3. 在翻译时候结合动态调试,确定输入、密钥等变量存放的位置
  4. 边翻译边调试,确定猜想
  5. 转换对应的高级代码

file_encryptor

异常处理:

题目用了SEH进行反调试,在tls函数和main函数中均看到了故意触发异常的指令即memory[0]=42

在汇编层面查看相应的异常处理函数,可以很清晰的看到try块和except块,except中括号地址是过滤器地址,决定要不要处理此异常,不过这题都选择了处理,即返回1

因此本题主要逻辑在except块中,要么把相关异常处理全部patch掉,然后进行反汇编,要么就摁看汇编。

这里选择摁看汇编,在patch掉tls的反调试之后,转去看mainexcept

除此之外还发现了新的反调试指令,即这里地址4019F5处的两个call指令,IDA在此处爆红了,一个一个看看。

进入sub_402140

发现很简单,就是让esp指向地址加1。所以这个花指令就是干扰了IDA对栈帧的判断。

这个花指令原理比较简单,在call一个函数的时候,程序会把下一条指令地址给压栈,然后在retn的时候再把这个地址弹出去然后再jmp过去。那么这里的指令add [esp + 0],1实际上就是把栈顶值给加1,也就是把下一条指令地址给加1,也就是说会跳过下一个指令地址。在这题具体体现在跳过了E8这个call指令,而是从83开始继续执行

这个patch也比较简单,全部nop掉即可,包括那个E8花指令

main函数分析

根据函数名字以及IDA的变量注释,可以知道主要逻辑是进行一个资源的查找以及解密,中间进行一些字符串复制等操作,一直看到这一段

这里根据变量名字以及之前几个块的操作不难推测[ebp+var_20]就是数据大小,[ebp+var_1C]就是数据地址,这里就是把从资源段取出的内容进行异或。

提取文件

resource hacker打开查看,果然发现是有东西的

提出来做个异或操作

发现是个新的PE文件,download下来,用IDA打开

看到很多加密相关函数,不难推测这道题就应该是使用这些加密函数进行加密。

加密函数

继续往后看,发现两个关键的函数

其中sub_401320是对密钥做一些处理,跳过不看

截屏2024-01-27 03.27.43

跟入sub_402000发现很多判断语句,从名字上看应该是要获取路径信息

下断点调试

在IDA输出窗口也看见又加载了一些dll文件

路径信息

看到路径信息

拼接一个文件夹路径

一直跟入到sub_4017E0即下图的sub_A517E0(IDA调试会对地址重新处理),然后就可以发现相关文件

继续跟入sub_A513E0

流程还是很清楚的,就是找文件、打开文件然后读文件,最后进行加密,再写文件,即这里的ifu语句

1
2
if ( !v6(*a1, 0, FileSize == 0, 0, lpBuffer, &NumberOfBytesRead)
|| !WriteFile(hFile, lpBuffer, NumberOfBytesRead, &NumberOfBytesRead, 0) )

GetProcAddress

即现在的v6是加密函数。而v6又是由前面的函数得到,这里v6重命名为encrypt_func

跟入dword_40541C数组查看

发现是个dll文件。

其实到这里程序流程就很清晰了,先把资源段中的dll文件解密,然后加载进程序中,然后获得路径,通过路径得到文件,再使用sub_A51BB0得到dll中的加密文件函数,然后进行加密,最后写文件。

不难推测,这里的sub_A51BB0就是GetProcAddres函数,第二个参数就是加密函数的序号。那么这里有一个trick就是如果在这个地方得到的是解密函数,那后面不就相当于自行解密了吗

查看一下提取出来的PE文件的导出表

patch

发现加密函数序号对的上!而且也有解密函数序号,那么只要patch一下即可

然后通过查看这两个函数的参数可以发现解密函数会少一个参数,那么调用他的时候就应该少push一个

可以发现就是少了最后一个参数,那么就需要patch掉第一个push

截屏2024-01-27 03.55.58

然后就让程序正常运行,就会自动解密并写入文件了

截屏2024-01-27 03.56.40

flag{7sa963fa-91a6-4371-bl7b-225102y789a0}

总结

  1. 使用简单的SEH异常进行干扰,可以全部nop掉,也可以摁看汇编
  2. 使用简单的基于栈帧的花指令
  3. GetProcAddress的理解与使用,重点在于通过不同函数导出序号可以得到不同函数地址

2023春秋杯网络安全联赛冬季赛
http://example.com/2024/01/24/ichunqiu2023_winter/
Author
yring
Posted on
January 24, 2024
Licensed under