一个人转发了一个我们可能都知道的JeffDean笑话。每次我阅读这个列表时,都会弹出这部分内容:JeffDean曾经用一个printf()实现了一个Web服务器,其他工程师添加了数千行注释,但仍然无法完全弄清楚它是如何工作的。工作。而这个程序就是今天的谷歌搜索主页。可以通过单个printf调用来实现Web服务器,但我还没有看到其他人这样做过。所以这次当我读到这份清单时,我决定实施它。这是它的代码,一个纯粹的单一printf调用,没有任何额外的变量或宏(别担心,我会解释这段代码是如何工作的)。#includeintmain(intargc,char*argv[]){printf("%*c%hn%*c%hn""\xeb\x3d\x48\x54\x54\x50\x2f\x31\x2e\x30\x20\x32""\x30\x30\x0d\x0a\x43\x6f\x6e\x74\x65\x6e\x74\x2d""\x74\x79\x70\x65\x3a\x74\x65\x78\x74\x2f\x68\x74""\x6d\x6c\x0d\x0a\x0d\x0a\x3c\x68\x31\x3e\x48\x65""\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x21\x3c\x2f""\x68\x31\x3e\x4c\x8d\x2d\xbc\xff\xff\xff\x48\x89""\xe3\x48\x83\xeb\x10\x48\x31\xc0\x50\x66\xb8\x1f""\x90\xc1\xe0\x10\xb0\x02\x50\x31\xd2\x31\xf6\xff""\xc6\x89\xf7\xff\xc7\x31\xc0\xb0\x29\x0f\x05\x49""\x89\xc2\x31\xd2\xb2\x10\x48\x89\xde\x89\xc7\x31""\xc0\xb0\x31\x0f\x05\x31\xc0\xb0\x05\x89\xc6\x4c""\x89\xd0\x89\xc7\x31\xc0\xb0\x32\x0f\x05\x31\xd2""\x31\xf6\x4c\x89\xd0\x89\xc7\x31\xc0\xb0\x2b\x0f""\x05\x49\x89\xc4\x48\x31\xd2\xb2\x3d\x4c\x89\xee""\x4c\x89\xe7\x31\xc0\xff\xc0\x0f\x05\x31\xf6\xff""\xc6\xff\xc6\x4c\x89\xe7\x31\xc0\xb0\x30\x0f\x05""\x4c\x89\xe7\x31\xc0\xb0\x03\x0f\x05\xeb\xc3",((((unsignedlongint)0x4005c8+12)>>16)&0xffff),0,0x00000000006007D8+2,(((unsignedlongint)0x4005c8+12)&0xffff)-((((unsignedlongint)0x4005c8+12)>>16)&0xffff),0,0x00000000006007D8);}此代码只能在LinuxAMD64独有编译器的系统中使用(gcc版本为4.8.2(Debian4.8.2-16))运行在电脑上,编译命令如下:gcc-gweb1.c-Owebserver有些人可能会这样猜:我用特殊格式的字符串来欺骗这段代码可能无法在你的机器上运行,因为我使用了两个地址硬编码。下面的版本比较人性化(更容易改),但是还是得改两个值:FUNCTION_ADDR和DESTADDR,后面会解释:#include#include#include#defineFUNCTION_ADDR((uint64_t)0x4005c8+12)#defineDESTADDR0x00000000006007D8#definea(FUNCTION_ADDR&0xffff)#defineb((FUNCTION_ADDR>>16)&0xffff)intmain(intargc,char"*argv(])%*{cprintf%hn%c%hn""\xeb\x3d\x48\x54\x54\x50\x2f\x31\x2e\x30\x20\x32""\x30\x30\x0d\x0a\x43\x6f\x6e\x74\x65\x6e\x74\x2d""\x74\x79\x70\x65\x3a\x74\x65\x78\x74\x2f\x68\x74""\x6d\x6c\x0d\x0a\x0d\x0a\x3c\x68\x31\x3e\x48\x65""\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x21\x3c\x2f""\x68\x31\x3e\x4c\x8d\x2d\xbc\xff\xff\xff\x48\x89""\xe3\x48\x83\xeb\x10\x48\x31\xc0\x50\x66\xb8\x1f""\x90\xc1\xe0\x10\xb0\x02\x50\x31\xd2\x31\xf6\xff""\xc6\x89\xf7\xff\xc7\x31\xc0\xb0\x29\x0f\x05\x49""\x89\xc2\x31\xd2\xb2\x10\x48\x89\xde\x89\xc7\x31""\xc0\xb0\x31\x0f\x05\x31\xc0\xb0\x05\x89\xc6\x4c""\x89\xd0\x89\xc7\x31\xc0\xb0\x32\x0f\x05\x31\xd2""\x31\xf6\x4c\x89\xd0\x89\xc7\x31\xc0\xb0\x2b\x0f""\x05\x49\x89\xc4\x48\x31\xd2\xb2\x3d\x4c\x89\xee""\x4c\x89\xe7\x31\xc0\xff\xc0\x0f\x05\x31\xf6\xff""\xc6\xff\xc6\x4c\x89\xe7\x31\xc0\xb0\x30\x0f\x05""\x4c\x89\xe7\x31\xc0\xb0\x03\x0f\x05\xeb\xc3",b,0,DESTADDR+2,a-b,0,DESTADDR);我将通过一系列简短的C代码解释这段代码的工作原理。第一段代码将解释如何在不使用函数调用的情况下运行另一段代码。看看这段简单的代码:#include#include#defineADDR0x00000000600720voidhello(){printf("helloworld\n");}intmain(intargc,char*argv[]){(*((unsignedlongint*)ADDR))=(unsignedlongint)你好;您可以编译它,但它可能无法在您的系统上运行,您需要执行以下步骤:1.编译这段代码:gccrun-finalizer.c-orun-finalizer2.检查fini_array的地址objdump-h-j.fini_arrayrun-finalizer然后从中找到VMA:run-finalizer:fileformatelf64-x86-64Sections:IdxNameSizeVMALMAFileoffAlgn18.fini_array0000000800000000006007200000000000600720000007202**3CONTENTS,ALLOC,LOAD,DATA你需要一个ThelatestversionofGCCmustbecompiledtofindit.旧版本的GCC使用不同的存储终止符原则。3.将代码中ADDR的值改成正确的地址。4.重新编译代码5.运行#p#现在您将在屏幕上看到“helloworld”输出,但它实际上是如何工作的呢?:根据第11章的LinuxStandardBaseCoreSpecification3.1。为了调用hello函数而不是调用默认处理程序,我们必须覆盖此数组。如果您尝试编译此Web服务器代码,将以相同的方式(使用objdump)获取ADDR的值。好了,既然我们知道了如何通过覆盖某个地址来执行一个函数,我们还需要知道如何使用printf来覆盖一个地址。你可以找到很多关于利用格式字符串漏洞的教程,但我会给出一个简短的解释。printf函数有这样一个特点,使用“%n”格式可以让我们知道输出了多少个字符。#includeintmain(){intcount;printf("AB%n",&计数);printf("\n%dcharactersprinted\n",count);}可以看到输出如下:AB2charactersprinted当然我们使用任意计数指针的地址来覆盖这个地址。但是为了覆盖大值的地址,需要输出大量的文本。幸运的是,还有另一种格式字符串“%hn”适用于短裤而不是整数。这个值每次可以用2个字节覆盖,排列成我们需要的4个字节的值。尝试将我们在两次printf调用中需要的a?defineFUNCTION_ADDR((uint64_t)hello)#defineDESTADDR0x0000000000600948voidhello(){printf("\n\n\n\nhelloworld\n\n");}intmain(intargc,char*argv[]){shorta=FUNCTION_ADDR&0xffff;shortb=(FUNCTION_ADDR>>16)&0xffff;printf("a=%04xb=%04x\n",a,b);fflush(标准输出);uint64_t*p=(uint64_t*)DESTADDR;printf("之前:%08lx\n",*p);fflush(标准输出);printf("%*c%hn",b,0,DESTADDR+2);fflush(stdout);printf("after1:%08lx\n",*p);fflush(stdout);printf("%*c%hn",a,0,DESTADDR);fflush(标准输出);printf("after2:%08lx\n",*p);fflush(标准输出);返回0;}导入的行是:shorta=FUNCTION_ADDR&0xffff;shortb=(FUNCTION_ADDR>>16)&0xffff;printf("%*c%hn",b,0,DESTADDR+2);printf("%*c%hn",a,0,DESTADDR);a和b只是函数地址的一半可以构造一个长度为a和b的字字符串传递给printf,但是我选择使用格式“%*”,可以通过参数控制输出的长度。例如,这段代码:printf("%*c",10,'A');会在A后面输出9个空格,所以一共输出10个字符。如果我们只想使用一个printf,我们需要考虑到b字节已经打印完了,我们需要再打印一个b-a字节(这个计数器是累加的)。printf("%*c%hn%*c%hn",b,0,DESTADDR+2,b-a,0,DESTADDR);目前我们正在调用这个“hello”函数,但实际上我们可以调用任何函数(或任何地址)。我写了一个shellcode就像一个网络服务器,但它只输出“Helloworld”。下面是我写的填充数据:unsignedcharhello[]="\xeb\x3d\x48\x54\x54\x50\x2f\x31\x2e\x30\x20\x32""\x30\x30\x0d\x0a\x43\x6f\x6e\x74\x65\x6e\x74\x2d""\x74\x79\x70\x65\x3a\x74\x65\x78\x74\x2f\x68\x74""\x6d\x6c\x0d\x0a\x0d\x0a\x3c\x68\x31\x3e\x48\x65""\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x21\x3c\x2f""\x68\x31\x3e\x4c\x8d\x2d\xbc\xff\xff\xff\x48\x89""\xe3\x48\x83\xeb\x10\x48\x31\xc0\x50\x66\xb8\x1f""\x90\xc1\xe0\x10\xb0\x02\x50\x31\xd2\x31\xf6\xff""\xc6\x89\xf7\xff\xc7\x31\xc0\xb0\x29\x0f\x05\x49""\x89\xc2\x31\xd2\xb2\x10\x48\x89\xde\x89\xc7\x31""\xc0\xb0\x31\x0f\x05\x31\xc0\xb0\x05\x89\xc6\x4c""\x89\xd0\x89\xc7\x31\xc0\xb0\x32\x0f\x05\x31\xd2""\x31\xf6\x4c\x89\xd0\x89\xc7\x31\xc0\xb0\x2b\x0f""\x05\x49\x89\xc4\x48\x31\xd2\xb2\x3d\x4c\x89\xee""\x4c\x89\xe7\x31\xc0\xff\xc0\x0f\x05\x31\xf6\xff""\xc6\xff\xc6\x4c\x89\xe7\x31\xc0\xb0\x30\x0f\x05""\x4c\x89\xe7\x31\xc0\xb0\x03\x0f\x05\xeb\xc3";如果去掉hello函数插入这个padding数据,这段代码会被调用#p#这段代码其实是一个字符串,所以可以在里面加上"%*c%hn%*c%hn"格式化字符串,这个字符串没有命名,所以需要找到它的地址编译后,为了得到这个地址,我们需要编译这段代码然后反汇编它:objdump-dwebserver00000000004004fd:4004fd:55push%rbp4004fe:4889e5mov%rsp,%rbp400501:4883ec20sub$0x20,%rsp400505:897dfcmov%edi,-0x4(%rbp)400508:488975f0mov%rsi,-0x10(%rbp)40050c:c70424d8076000movl$0x6007d8,(%rsp)400513:41b900000000mov$0x0,%r9d400519:41b894050000mov$0x594,%r8d40051f:b9da076000mov$0x6007da,%ecx400524:ba00000000mov$0x0,%edx400529:be40000000mov$0x40,%esi40052e:bfc8054000mov$0x4005c8,%edi400533:b800000000mov$0x0,%eax400538:e8a3feffffcallq4003e040053d:c9leaveq40053e:c3retq40053f:90nop其实只需要关心这一行:mov$0x4005c8,%edi这是我们需要的地址:#defineFUNCTION_ADDR((uint64_t)0x4005c8+12)+12是非常必要的,因为我们填充数据是在字符串“%*c%hn%*c%hn”之后开始的,长度为12个字符。如果您对填充数据感到好奇,它实际上是由以下C代码创建的:#include#include#include#include#include#include#include#include#include#include#includeintmain(intargc,char*argv[]){intsockfd=socket(AF_INET,SOCK_STREAM,0);structsockaddr_inserv_addr;bzero((char*)&serv_addr,sizeof(serv_addr));serv_addr.sin_family=AF_INET;serv_addr.sin_addr.s_addr=INADDR_ANY;serv_addr.sin_port=htons(8080);绑定(sockfd,(structsockaddr*)&serv_addr,sizeof(serv_addr));听(sockfd,5);while(1){intcfd=accept(sockfd,0,0);char*s="HTTP/1.0200\r\nContent-type:text/html\r\n\r\nHelloworld!
";如果(fork()==0){写(cfd,s,strlen(s));关机(cfd,SHUT_RDWR);关闭(差价合约);}}返回0;删除此填充数据中的任何NUL字符(因为我没有从X86-64上的Shellcodes数据库中找到单个NUL字符)。JeffDean曾经使用单个printf()调用实现了一个Web服务器。其他工程师已经添加了数千行注释,但仍然没有弄清楚它是如何工作的。而这个程序就是今天的GoogleSearch主页。如果您想对可以处理Google搜索负载的Web服务器进行基准测试,这就给读者留下了一个练习。此部分的代码可在此处获得。对于那些认为这没用的人:它确实是。我恰好喜欢这种挑战,它刷新了我对以下主题的记忆和知识:编写shim代码(多年未编写)、AMD64汇编(调用约定、寄存器保护等)、系统调用、objdump,fini_array(我上次检查时,GCC仍然使用.)。更新:Ubuntu添加了一个安全特性,在最终的ELF表区域提供只读重定位,为了能够在ubuntu中运行这个例子,编译时添加如下命令行:-Wl,-z,norelro例如:gcc-Wl,-z,norelrotest.c原文链接:tinyhack翻译:伯乐在线-信子翻译链接:http://blog.jobbole.com/64252/