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 |
|
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 |
|
与子函数结合一起的python代码为
1 |
|
子函数三
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
的理解与使用,重点在于通过不同函数导出序号可以得到不同函数地址