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 |  | 
主函数opcode
现在就可以把main_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"""
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为
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)
那么这里就是对输入进行简单异或
子函数二
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 |  | 
与子函数结合一起的python代码为
| 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 |  | 
对应python 代码
| 1 |  | 
整体加密
将所有加密流程整合起来,其中KEY也是通过调试拿到
| 1 |  | 
经过对比,发现就是present魔改加密,在github上有原始present解密,进行相应修改
| 1 |  | 
总结
- 确定寄存器、栈等变量
- 对操作进行翻译,把opcode和汇编对应起来
- 在翻译时候结合动态调试,确定输入、密钥等变量存放的位置
- 边翻译边调试,确定猜想
- 转换对应的高级代码
file_encryptor
异常处理:
题目用了SEH进行反调试,在tls函数和main函数中均看到了故意触发异常的指令即memory[0]=42

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

因此本题主要逻辑在except块中,要么把相关异常处理全部patch掉,然后进行反汇编,要么就摁看汇编。
这里选择摁看汇编,在patch掉tls的反调试之后,转去看main的except块

除此之外还发现了新的反调试指令,即这里地址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是对密钥做一些处理,跳过不看

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

下断点调试
在IDA输出窗口也看见又加载了一些dll文件

路径信息
看到路径信息

拼接一个文件夹路径

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

继续跟入sub_A513E0

流程还是很清楚的,就是找文件、打开文件然后读文件,最后进行加密,再写文件,即这里的ifu语句
| 1 |  | 
GetProcAddress
即现在的v6是加密函数。而v6又是由前面的函数得到,这里v6重命名为encrypt_func

跟入dword_40541C数组查看

发现是个dll文件。
其实到这里程序流程就很清晰了,先把资源段中的dll文件解密,然后加载进程序中,然后获得路径,通过路径得到文件,再使用sub_A51BB0得到dll中的加密文件函数,然后进行加密,最后写文件。
不难推测,这里的sub_A51BB0就是GetProcAddres函数,第二个参数就是加密函数的序号。那么这里有一个trick就是如果在这个地方得到的是解密函数,那后面不就相当于自行解密了吗
查看一下提取出来的PE文件的导出表

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

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


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

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

flag{7sa963fa-91a6-4371-bl7b-225102y789a0}
总结
- 使用简单的SEH异常进行干扰,可以全部nop掉,也可以摁看汇编
- 使用简单的基于栈帧的花指令
- GetProcAddress的理解与使用,重点在于通过不同函数导出序号可以得到不同函数地址