2015/5/19 18:08:45
上一节我们介绍了基本shellcode的编写,采用的是exit()、setreuid()和execve()三个系统调用,实际中当然是根据自己的需要来选择合适的系统调用了,系统调用号需要查看syscalltable,参数的压入也是采取类似的方式,处理好堆栈,编写汇编代码并不十分困难。
这一节我们要来介绍下shellcode的编码,那么为什么要对shellcode进行编码呢?大致原因有以下几个:
- 避免出现Bad字符,如\x00、\xa9等;
- 避开IDS或其他网络检测器的检测;
- 遵循字符串过滤器;
接下来,我们来简单介绍一种shellcode编码的方式。
一、简单的XOR编码
计算机中一种常见的位运算是XOR运算,即“按位异或”。当初自己记忆这个运算时还费了一些功夫才和“按位与|或”运算区分开。异或运算的本质是判断对应二进制位是否相同,若不同-->异-->True;若相同-->同-->False。因此可以说XOR运算是判断对应二进制位不同的运算。
XOR运算有着很好的运算特性,即一个数与一个数XOR两次会得到自身:
0 XOR 0 = 0 0 XOR 1 = 1 1 XOR 1 = 0 1 XOR 0 = 1 101 XOR 100 = 001 001 XOR 100 = 101
我们利用这个特点可以构造shellcode编码和基本的加密,当然,密钥(上例中的0x100)自然要硬编码进shellcode了。
二、JMP/CALL XOR解码器
既然对shellcode编码,那么也就意味着要解码。我们的模型大概是下面的样子:
[decoder][encoded shellcodes]
一般来说,如果解码器需要知道自己的位置,这样就可以计算出编码的shellcode的位置开始解码。确定解码器位置通常被称作GETPC,方法有许多种,今天我们来介绍其中的一种:JMP/CALL。
JMP/CALL的思想是:
- JMP指令跳转到CALL指令;
- 该CALL指令位于编码的shellcode之前;
- CALL指令会创建一个新栈,因此将当前的EIP指针压栈(即编码shellcode的起始地址);
- CALL调用的过程将压栈的地址弹出保存到寄存器中;
- 利用保存的寄存器进行shellcode解码;
- JMP到shellcode处执行;
看起来复杂,我们看看下面的汇编代码就明白了,注意每条汇编语句后的执行顺序标号,可以帮助大家理解整个流程:
global _start _start: jmp short call_point ;1. JMP to CALL begin: pop esi ;3.将栈中的shellcode地址弹出保存到寄存器esi中便于后续解码 xor ecx, ecx ;4.清空ecx mov cl, 0x0 ;5.shellcode长度设为0 short_xor: xor byte[esi], 0x0 ;6.0x0是这里的编码key inc esi ;7.ESI指针递增,遍历所有的shellcode字节 loop short_xor ;8.循环直到shellcode解码完毕 jmp short shellcodes ;9.跳过CALL直接到达shellcode段 call_point: call begin ;2.CALL begin过程,同时将当前的EIP即shellcode的起始地址压入栈中 shellcodes: ;10.执行解码后的shellcode 这里放置解码后的shellcode
整体的流程就是这样,只要仔细留心逻辑顺序和shellcode地址的压栈弹栈,JMP/CALL并不难理解。
Refer: Gray Hat Hacking: The Ethical Hacker's Handbook, Third Edition