shellcode简介
1 SehllCode
shellcode是指一段原始可执行代码,通常只是一个二进制数据块;对它进行分析时需要手动选择解析方式。shellcode的不能通过Windows加载器执行,编写者需要手动执行Windows加载器在普通程序启动时所执行的如下动作:
- 对shellcode进行内存布局,如果它不能被加载到首选的内存位置, 那么需要应用地址重定向
- 加载需要的库, 并解决外部依赖
shellcode这个名宇来源于攻击者通常会使用这段代码来获得被攻陷系统上交互式shell的访问权限。
2 位置无关代码(PIC)
位置无关代码(PIC) 是指不使用硬编码地址来寻址指令或数据的代码。shellcode是位置无关代码,它被加载到的内存中的位置是不确定的,shellcode必须确定对代码和数据的内存访问都使用PIC技术。下面为几种常见的x86代码与数据访问类型, 以及它们是否是PIC代码:

2.1 识别执行位置
Shellcode在以位置无关的方式访问数据时, 需要解引用一个基址指针,用它加上或减去偏移值, 将使它安全访问shellcode中包含的数据。因为x86指令集不提供相对EIP的数据访问寻址,而仅对控制流指令提供EIP相对寻址, 所以, 一个通用寄存器必须首先载入当前指令指针值(EIP), 作为基址指针来使用。在x86系统上的指令指针不能被软件直接访问,`mov eax, eip这条指令是非法的;shellcode使用两种普遍的技术解决这个问题: call/pop指令和fnstenv指令。
2.2 call/pop指令获取EIP
当一个call指令被执行时, 处理器将call后面的指令的地址压到栈上, 然后转到被请求的位置进行执行。这个函数执行完后, 会执行一个ret指令, 将返回地址弹出到栈的顶部, 并将它载入指令指针寄存器(EIP)中。这样做的结果是执行刚好返回到call后面的指令。shellcode可以通过在一个call指令后面立刻执行pop指令滥用这种通常约定, 这会将紧跟call后面的地址载入指定寄存器中。示例代码如下:

2.3 fnstenv指令获取EIP
x87浮点单元(FPU)在普通x86架构中提供了一个隔离的执行环境。它包含一个单独的专用寄存器集合,当一个进程正在使用FPU执行浮点运算时,这些寄存器需要由操作系统在上下文切换时保存。结构体FpuSaveState
用于保存FPU状态
struct FpuSaveState
{
uint32_t control_word;
uint32_t status_word;
uint32_t tag_word;
uint32_t fpu_instruction_pointer;
uint16_t fpu_instruction_selecter;
uint16_t fpu_opcode;
uint32_t fpu_operand_pointer;
uint16_t fpu_operand_selector;
uint16_t reserved;
}

3 符号解析
shellcode执行时通常要通过API和系统进行交互。shellcode不能使用Windows加载器来加载需要的库,只能自己解决依赖问题。为了完成这个任务,shellcode经常使用LoadlibraryA
和GetProcAddress
函数。这两个API都在kernel32.dll
里,所以shellcode必须
- 搜索
kernel32.dll
的位置 - 解析
kernel32.dll
,并找到上述API
要找到kernel32.dll
的基地址,我们需要跟踪下图所示的一些数据结构,找到库后通过对及解析可以获取到相应API的地址,一般是通过strcmp函数对比函数名来获取。

3.1 使用散列过的函数名
上述算法对每一个导出名字执行strcmp, 直到它找到正确的那个为止。这要求shellcode所使用的每一个APl函数的全名, 以ASCll字符串形式被包含进来。当这段shellcode的大小受限时, 这些字符串可能使这段shellcode的大小超过限制。解决这个问题的方法是计算出每个符号字符串的散列值, 并用这个结果与保存在shellcode中的预先计算的值进行比较。散列函数不需要很复杂;只需要保证在每个被shellcode使用的DLL中 这些散列值是独一无二的就可以了。在不同DLL的符号之间及shellcode不使用的符号之间的散列冲突是可以接受的。
4 shellcode编码
为了在shellcode被触发时能够被执行,它被加载到程序地址空间中的某个位置。当与一个漏洞 利用组合使用时,意味着shellcode必须在漏洞利用发生前存在,或是与漏洞利用数据一起被传递给 程序。例如,如果一个程序正在对输入数据执行一些基本的过滤,那么这个shellocde必须绕过这个 过滤,否则它就不会存在于这个脆弱进程的内存空间中。这意味着shellcode通常必须看起来像是合 法数据,这样才能被一个脆弱程序所接受。
例如不安全的字符串函数strcpy和strcat, 这两个函数都没有对要写的数据设置最大长度。如果一个程序使用这些函数中的某个读取或复制恶意数据到一个长度固定的缓冲区中,这些数据能够轻易地超过缓冲区的大小, 并导致一个缓冲区溢出攻击。这些函数将字符串作为以(0x00)字节结尾的字符数组看待。一个攻击者想要复制到这个缓冲区的shellcode必须看起来像是有效数据, 这意味着它不能在中间有任何NULL(0x00)字节, 这个NULL(0x00)字节会提前终止这个字符串复制操作。
程序可能对shellcode必须传递给它的数据执行额外的正确性检查, 比如下面这些:
- 所有字节都是可打印的(小千0x80) ASCII字节
- 所有字节都是字母数字组合的(A到Z, a到Z, 或0到9)
要克服漏洞程序的过滤限制, 几乎所有的shellcode都需要对主要载荷进行编码, 从而绕过漏洞 程序的过滤, 然后再通过插入一个解码器将编码过的载荷转换为可执行代码。