一次ELF程序动态解密尝试

在最近的一次出题中,由于pwn水平有限(泪),所以为了提高难度,在pwn题中添加了一些逆向要素。在最近的一次尝试中实现了之前一直有想法,但没有去实现的一个动态解密的思路,写出本文给大家分享一下。

0x01 什么是动态解密

这动态解密其实是我自己取的名字,主要的操作就是让程序在运行过程中对将要执行的代码进行解密,执行完后再加密回去。这样能在很大程度上提高逆向分析的难度。因为当攻击者对程序进行逆向分析时,如果没有解密,它只能看着一块加密的数据发呆。

大概是这个样子:

img

0x02 实现思路

  1. 编写好待加密函数a();
  2. 编写解密函数b(),在b()函数中先解密a,再用函数指针调用a,调用完毕后再加密回去;
  3. 编译;
  4. 编写脚本(我习惯用py),对编译后的ELF对应地址加密、覆写。

这里要注意,当程序被编译完以后,是无法执行的,如果执行一定会报错,这是因为此时函数a还并未加密,如果对未加密的字节进行解密,它的效果等同于对该字节进行加密,因此程序会因为碰到不认识的机器码而报错。

0x03 实际操作

dynamicDecode.c

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>

void a(){
puts("114514");
}

void b(){
char *func_a = (char *)a;//func_p of a();

//let a(); writable
mprotect((u_int64_t)func_a & ~0xfff,0x1000,PROT_EXEC|PROT_WRITE|PROT_READ);
//decode a();
for(int i = 0;i < (int)b - (int)a;i++){
*(func_a + i) = *(func_a + i) ^ 0xfc;
}

//call a();
(*(void (*)())func_a)();

//encode a();
for(int i = 0;i < (int)b - (int)a;i++){
*(func_a + i) = *(func_a + i) ^ 0xfc;
}
//let a(); not writable
mprotect((u_int64_t)func_a & ~0xfff,0x1000,PROT_EXEC|PROT_READ);
return;
}

int main(){
b();
return 0;
}

可以看到程序逻辑不是特别复杂,主要操作就是获取a的指针,给a所在的页写权限,用循环遍历函数a的每个字节,将所有数据逐字节异或一个0xfc,通过函数指针调用,然后再加密回去,关闭写权限。

最后再main调用b,实际上则是解密a,调用a,加密a。

当然,前面也提到,这个程序编译完是无法运行的:

img

理由也很简单,gdb进去一看就知道

img

此时的a是一串莫名其妙的东西。

img

所以现在要做的是对a函数进行加密。

获取a的地址

img

将elf文件拖入ida,查看a函数的起始地址和结束地址 0x1169和0x117f

enc_fun.py

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
ELF_PATH = r'./dynamicDecode'
start_addr = 0x1169
end_addr = 0x1180

#read elf with bytes mode
with open(ELF_PATH,'rb') as fp:
content = fp.read()

#bytes -> hex string
hex_code = content.hex()

#part1
new_elf_hex = hex_code[:start_addr * 2]

#part2 select and encode aim function
for i in range(start_addr * 2,end_addr * 2,2):
now_byte = int(hex_code[i:i+2],16)

#? encode now_byte
now_byte = now_byte ^ 0xfc

if len(hex(now_byte))== 3:
new_byte = '0' + hex(now_byte)[2:]
else:
new_byte = hex(now_byte)[2:]
new_elf_hex = new_elf_hex + new_byte

#part3
new_elf_hex += hex_code[end_addr * 2:]

#hex string -> bytes,write with bytes mod
with open(ELF_PATH + '.encoded','wb') as fp:
fp.write(bytes.fromhex(new_elf_hex))

此脚本的逻辑很简单,主要是将ELF文件转成hex string处理。这里要注意一点,因为一个地址对应一个字节,而一个字节对应的hex string是两个字符(例如‘A’是0x61,那它的hex string就是“61”),所以所有的地址都需要*2。其他的我批注写的应该还算明白,直接看批注就行了。

加密后的执行结果:

img

在ida中的函数a:

img

居然还有几个字节能识别成汇编,不过没什么意义。

0x04 总结

此次尝试主要是实现了我之前一直想要去做的一个想法。当然加密方式可以不必要是简单的异或,也可以是RC4,AES,甚至是RSA。如果想的话,也可以直接用内联汇编写解密函数b,这些都能让函数a的解密难度更上一层楼,而函数a可以是整个程序中的任何一个函数,如果你想可以每个函数都套一层这个,这也就提高了整个二进制文件的逆向难度。