您好,登錄后才能下訂單哦!
這篇文章將為大家詳細(xì)講解有關(guān)如何用一個(gè)printf()調(diào)用實(shí)現(xiàn)一個(gè)web服務(wù)器,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對(duì)相關(guān)知識(shí)有一定的了解。
一個(gè)小伙伴轉(zhuǎn)發(fā)了一個(gè)可能我們都知道的Jeff Dean的笑話。每次我讀到這個(gè)列表的時(shí)候,這一部分就會(huì)跳出來:
Jeff Dean有次用一句printf()實(shí)現(xiàn)了一個(gè)web服務(wù)器,而其他工程師添加了數(shù)千行注釋但是仍然不能完全弄清楚它是如何工作的。而這個(gè)程序正是如今的Google Search首頁。
使用一句printf調(diào)用來實(shí)現(xiàn)一個(gè)web服務(wù)器是很有可能的,但是我還沒發(fā)現(xiàn)其他人做到。所以這次我讀到這個(gè)列表時(shí),我決定實(shí)現(xiàn)它。這里是它的代碼,一個(gè)純粹單一的printf調(diào)用,沒有任何附加的變量或者宏(不用擔(dān)心,我將會(huì)解釋這段代碼是如何工作的)。
#include <stdio.h> int main(int argc, 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", ((((unsigned long int)0x4005c8 + 12) >> 16) & 0xffff), 0, 0x00000000006007D8 + 2, (((unsigned long int)0x4005c8 + 12) & 0xffff)- ((((unsigned long int)0x4005c8 + 12) >> 16) & 0xffff), 0, 0x00000000006007D8 ); }
這段代碼只能在獨(dú)有Linux AMD64位編譯器(gcc版本是4.8.2(Debian 4.8.2-16))的系統(tǒng)上運(yùn)行,編譯命令如下:
gcc -g web1.c -O webserver
可能有些人會(huì)這樣猜測(cè):我用一個(gè)特殊格式的字符串來作弊。這段代碼可能不能在你的機(jī)器上運(yùn)行,因?yàn)槲覍?duì)兩個(gè)地址使用了硬編碼。
下面這個(gè)版本是更加用戶友好化的(更容易改變),但是你仍舊要改變兩個(gè)值:FUNCTION_ADDR和DESTADDR,稍后我會(huì)解釋:
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #define FUNCTION_ADDR ((uint64_t)0x4005c8 + 12) #define DESTADDR 0x00000000006007D8 #define a (FUNCTION_ADDR & 0xffff) #define b ((FUNCTION_ADDR >> 16) & 0xffff) int main(int argc, 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" , b, 0, DESTADDR + 2, a-b, 0, DESTADDR ); }
我將解釋這段代碼如何通過一系列簡(jiǎn)短的C編碼來工作。第一段代碼將解釋如何不使用函數(shù)調(diào)用,就能運(yùn)行另一段代碼。看看下面這段簡(jiǎn)單的代碼:
#include <stdlib.h> #include <stdio.h> #define ADDR 0x00000000600720 void hello() { printf("hello world\n"); } int main(int argc, char *argv[]) { (*((unsigned long int*)ADDR))= (unsigned long int)hello; }
你可以編譯它,但是它可能不能在你的系統(tǒng)上運(yùn)行,你需要按如下步驟來做:
1.編譯這段代碼:
gcc run-finalizer.c -o run-finalizer
2.檢查fini_array的地址
objdump -h -j .fini_array run-finalizer
然后從中找到VMA:
run-finalizer: file format elf64-x86-64 Sections: Idx Name Size VMA LMA File off Algn 18 .fini_array 00000008 00000000006007200000000000600720 00000720 2**3 CONTENTS, ALLOC, LOAD, DATA
你需要一個(gè)GCC來編譯才能發(fā)現(xiàn)它,舊版本的GCC使用不同的存儲(chǔ)終結(jié)器原理。
3. 改變代碼中ADDR的值為正確的地址。
4.重新編譯代碼
5.運(yùn)行它
現(xiàn)在你就會(huì)看到你的屏幕上輸出“hello world”,而它實(shí)際上是如何運(yùn)行的呢?:
依據(jù)Chapter 11 of Linux Standard Base Core Specification 3.1(譯注:Linux標(biāo)準(zhǔn)基礎(chǔ)核心規(guī)范3.1第11章)
.fini_array
這部分保存了一個(gè)函數(shù)指針數(shù)組,它貢獻(xiàn)出一個(gè)終止數(shù)組給這個(gè)可執(zhí)行的或可共享的、包含這個(gè)部分的對(duì)象。
為了讓hello函數(shù)被調(diào)用而不是調(diào)用默認(rèn)的處理函數(shù),我們要重寫這個(gè)數(shù)組。如果嘗試編譯這個(gè)web服務(wù)器代碼,ADDR的值以同樣的方式獲取(使用objdump)。
好了,現(xiàn)在我們清楚了如何通過覆蓋一個(gè)確定的地址來執(zhí)行一個(gè)函數(shù),還需要知道如何使用printf來覆蓋一個(gè)地址??梢哉业胶芏嚓P(guān)于利用格式化字符串漏洞的教程,但是我將給出一個(gè)簡(jiǎn)短的解釋。
printf函數(shù)有這樣一個(gè)特性,使用“%n”格式可以讓我們知道有多少個(gè)字符輸出。
#include <stdio.h> int main(){ int count; printf("AB%n", &count); printf("\n%d characters printed\n", count); }
可以看到輸出如下:
AB 2 characters printed
當(dāng)然我們用任何計(jì)數(shù)指針的地址來重寫這個(gè)地址。但是為了用一個(gè)大數(shù)值來覆蓋地址,需要輸出大量的文本。幸運(yùn)的是,有另外一個(gè)格式化字符串“%hn”作用于short而不是int。每次可以用2個(gè)字節(jié)排列成一個(gè)我們需要的4字節(jié)值來覆蓋這個(gè)值。
試著用兩個(gè)printf調(diào)用放置我們需要的a¡值(在這個(gè)例子中是指“hello”函數(shù)的指針)到fini_array:
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #define FUNCTION_ADDR ((uint64_t)hello) #define DESTADDR 0x0000000000600948 void hello() { printf("\n\n\n\nhello world\n\n"); } int main(int argc, char *argv[]) { short a= FUNCTION_ADDR & 0xffff; short b = (FUNCTION_ADDR >> 16) & 0xffff; printf("a = %04x b = %04x\n", a, b);fflush(stdout); uint64_t *p = (uint64_t*)DESTADDR; printf("before: %08lx\n", *p); fflush(stdout); printf("%*c%hn", b, 0, DESTADDR + 2 );fflush(stdout); printf("after1: %08lx\n", *p); fflush(stdout); printf("%*c%hn", a, 0, DESTADDR);fflush(stdout); printf("after2: %08lx\n", *p); fflush(stdout); return 0; }
導(dǎo)入的行是:
short a= FUNCTION_ADDR & 0xffff; short b = (FUNCTION_ADDR >> 16) & 0xffff; printf("%*c%hn", b, 0, DESTADDR + 2 ); printf("%*c%hn", a, 0, DESTADDR);
a和b都只是函數(shù)地址的一半,可以構(gòu)造一個(gè)a和b長(zhǎng)度的字符串傳入printf,但是我選擇使用“%*”這個(gè)格式,它可以通過參數(shù)來控制輸出的長(zhǎng)度。
例如這段代碼:
printf("%*c", 10, 'A');
將會(huì)在A后面輸出9個(gè)空格,所以一共輸出10字符。
如果只想用一個(gè)printf,就需要考慮到b字節(jié)已經(jīng)被打印,而我們又需要打印另一個(gè)b-a字節(jié)(這個(gè)計(jì)數(shù)器是累加的)。
printf("%*c%hn%*c%hn", b, 0, DESTADDR + 2, b-a, 0, DESTADDR );
目前我們是調(diào)用這個(gè)“hello”函數(shù),但是其實(shí)我們是可以調(diào)用任何函數(shù)的(或者任何地址)。我寫過一個(gè)就像web服務(wù)器的shellcode(譯注:填充數(shù)據(jù)),但是它只是輸出“Hello world”。以下是我寫的填充數(shù)據(jù):
unsigned char hello[] = "\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函數(shù)然后插入這個(gè)填充數(shù)據(jù),這段代碼將會(huì)被調(diào)用。
這段代碼其實(shí)就是一個(gè)字符串,所以可以給它添加“%*c%hn%*c%hn”格式化字符串。這個(gè)字符串還未命名,所以需要在編譯后找到它的地址,而為了獲得這個(gè)地址,我們需要編譯這段代碼,然后反匯編它:
objdump -d webserver
00000000004004fd <main>: 4004fd: 55 push %rbp 4004fe: 48 89 e5 mov %rsp,%rbp 400501: 48 83 ec 20 sub $0x20,%rsp 400505: 89 7d fc mov %edi,-0x4(%rbp) 400508: 48 89 75 f0 mov %rsi,-0x10(%rbp) 40050c: c7 04 24 d8 07 60 00 movl $0x6007d8,(%rsp) 400513: 41 b9 00 00 00 00 mov $0x0,%r9d 400519: 41 b8 94 05 00 00 mov $0x594,%r8d 40051f: b9 da 07 60 00 mov $0x6007da,%ecx 400524: ba 00 00 00 00 mov $0x0,%edx 400529: be 40 00 00 00 mov $0x40,%esi 40052e: bf c8 05 40 00 mov $0x4005c8,%edi 400533: b8 00 00 00 00 mov $0x0,%eax 400538: e8 a3 fe ff ff callq 4003e0 <printf@plt> 40053d: c9 leaveq 40053e: c3 retq 40053f: 90 nop
其實(shí)只需要關(guān)心這行:
mov $0x4005c8,%edi
這就是我們需要的地址:
#define FUNCTION_ADDR ((uint64_t)0x4005c8 + 12)
+12是非常必要的,因?yàn)槲覀兊奶畛鋽?shù)據(jù)是從12個(gè)字符長(zhǎng)度的“%*c%hn%*c%hn”字符串后面開始的。
如果你的對(duì)填充數(shù)據(jù)很好奇,其實(shí)它是由以下的C代碼創(chuàng)建的:
#include<stdio.h> #include<string.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h> #include<sys/stat.h> #include<sys/socket.h> #include<arpa/inet.h> #include<netdb.h> #include<signal.h> #include<fcntl.h> int main(int argc, char *argv[]) { int sockfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in serv_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); bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); listen(sockfd, 5); while (1) { int cfd = accept(sockfd, 0, 0); char *s = "HTTP/1.0 200\r\nContent-type:text/html\r\n\r\n<h2>Hello world!</h2>"; if (fork()==0) { write(cfd, s, strlen(s)); shutdown(cfd, SHUT_RDWR); close(cfd); } } return 0; }
我做了額外的工作(即使在這個(gè)例子中并不是十分必要的)來移除這個(gè)填充數(shù)據(jù)中的所有NUL字符(因?yàn)槲覜]有從X86-64上的Shellcodes數(shù)據(jù)庫中找到一個(gè)NUL字符)。
Jeff Dean曾經(jīng)使用一個(gè)printf()調(diào)用實(shí)現(xiàn)了一個(gè)web服務(wù)器。其他的工程師添加了數(shù)千行的注釋,但是仍然沒有弄清楚它是如何工作的。而這個(gè)程序正是如今的Google Search首頁。
這給讀者留下了一道練習(xí)題,如果要評(píng)測(cè)web服務(wù)器,可以處理Google search的負(fù)載。
這部分的代碼可以從這里獲得。
對(duì)于認(rèn)為這樣做是無用的人:它確實(shí)是沒有用的。我只是碰巧喜歡這種挑戰(zhàn),而它為以下主題更新了我的記憶和知識(shí):編寫填充代碼(已經(jīng)很多年沒有寫過了),AMD64裝配(調(diào)用慣例,寄存器保護(hù)等等),系統(tǒng)調(diào)用,objdump,fini_array(最近一次我檢測(cè)的時(shí)候,GCC依然使用.dtors),printf格式化利用,gdb技巧(例如將內(nèi)存塊寫入文件),還有低階的socket編程(過去幾年中我使用過boost)。
更新:Ubuntu增加了一個(gè)安全特性,這個(gè)特性提供了在最終的ELF表區(qū)域中只讀重定位,為了能夠在ubuntu中運(yùn)行這個(gè)例子,在編譯的時(shí)候添加以下命令行:
-Wl,-z,norelro
比如:
gcc -Wl,-z,norelro test.c
關(guān)于如何用一個(gè)printf()調(diào)用實(shí)現(xiàn)一個(gè)web服務(wù)器就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。