用printf()調用實現web服務器

一個小夥伴轉發了一個可能咱們都知道的Jeff Dean的笑話。每次我讀到這個列表的時候,這一部分就會跳出來:html

Jeff Dean有次用一句printf()實現了一個web服務器,而其餘工程師添加了數千行註釋可是仍然不能徹底弄清楚它是如何工做的。而這個程序正是現在的Google Search首頁。linux

使用一句printf調用來實現一個web服務器是頗有可能的,可是我還沒發現其餘人作到。因此此次我讀到這個列表時,我決定實現它。這裏是它的代碼,一個純粹單一的printf調用,沒有任何附加的變量或者宏(不用擔憂,我將會解釋這段代碼是如何工做的)。git

#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 );
    }

這段代碼只能在 Linux amd64 編譯器(gcc版本是4.8.2(Debian 4.8.2-16))的系統上運行,編譯命令以下:github

gcc -g web1.c -O webserver

可能有些人會這樣猜想:我用一個特殊格式的字符串來做弊。這段代碼可能不能在你的機器上運行,由於我對兩個地址使用了硬編碼。web

下面這個版本是更加用戶友好化的(更容易改變),可是你仍舊要改變兩個值:FUNCTION_ADDR和DESTADDR,稍後我會解釋:shell

#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 );
    }

我將解釋這段代碼如何經過一系列簡短的C編碼來工做。第一段代碼將解釋如何不使用函數調用,就能運行另外一段代碼。看看下面這段簡單的代碼:數據庫

#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;
     }

你能夠編譯它,可是它可能不能在你的系統上運行,你須要按以下步驟來作:編程

編譯這段代碼:ubuntu

gcc run-finalizer.c -o run-finalizer

檢查fini_array的地址segmentfault

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

你須要一個最新版的GCC來編譯才能發現它,舊版本的GCC使用不一樣的存儲終結器原理。

改變代碼中ADDR的值爲正確的地址。

從新編譯代碼

運行它

如今你就會看到你的屏幕上輸出「hello world」,而它其實是如何運行的呢?:

依據Chapter 11 of Linux Standard Base Core Specification 3.1(譯註:Linux標準基礎核心規範3.1第11章)

 .fini_array
這部分保存了一個函數指針數組,它貢獻出一個終止數組給這個可執行的或可共享的、包含這個部分的對象。

爲了讓hello函數被調用而不是調用默認的處理函數,咱們要重寫這個數組。若是嘗試編譯這個web服務器代碼,ADDR的值以一樣的方式獲取(使用objdump)。

好了,如今咱們清楚瞭如何經過覆蓋一個肯定的地址來執行一個函數,還須要知道如何使用printf來覆蓋一個地址。能夠找到不少關於利用格式化字符串漏洞的教程,可是我將給出一個簡短的解釋。

printf函數有這樣一個特性,使用「%n」格式可讓咱們知道有多少個字符輸出。

#include <stdio.h>

    int main(){
        int count;
        printf("AB%n", &count);
        printf("\n%d characters printed\n", count);
    }

能夠看到輸出以下:

AB
    2 characters printed

固然咱們用任何計數指針的地址來重寫這個地址。可是爲了用一個大數值來覆蓋地址,須要輸出大量的文本。幸運的是,有另一個格式化字符串「%hn」做用於short而不是int。每次能夠用2個字節排列成一個咱們須要的4字節值來覆蓋這個值。

試着用兩個printf調用放置咱們須要的a¡值(在這個例子中是指「hello」函數的指針)到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;
    }

導入的行是:

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都只是函數地址的一半,能夠構造一個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」函數,可是其實咱們是能夠調用任何函數的(或者任何地址)。我寫過一個就像web服務器的shellcode(譯註:填充數據),可是它只是輸出「Hello world」。如下是我寫的填充數據:

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函數而後插入這個填充數據,這段代碼將會被調用。

這段代碼其實就是一個字符串,因此能夠給它添加「%*c%hn%*c%hn」格式化字符串。這個字符串還未命名,因此須要在編譯後找到它的地址,而爲了得到這個地址,咱們須要編譯這段代碼,而後反彙編它:

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

其實只須要關心這行:

mov    $0x4005c8,%edi

這就是咱們須要的地址:

#define FUNCTION_ADDR ((uint64_t)0x4005c8 + 12)

+12是很是必要的,由於咱們的填充數據是從12個字符長度的「%*c%hn%*c%hn」字符串後面開始的。

若是你的對填充數據很好奇,其實它是由如下的C代碼建立的:

#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<h1>Hello world!</h1>"; 
        if (fork()==0) {
            write(cfd, s, strlen(s));
            shutdown(cfd, SHUT_RDWR);
            close(cfd);
        }   
    }
    return 0;
    }

我作了額外的工做(即便在這個例子中並非十分必要的)來移除這個填充數據中的全部NUL字符(由於我沒有從X86-64上的Shellcodes數據庫中找到一個NUL字符)。

Jeff Dean曾經使用一個printf()調用實現了一個web服務器。其餘的工程師添加了數千行的註釋,可是仍然沒有弄清楚它是如何工做的。而這個程序正是現在的****Google Search****首頁

這給讀者留下了一道練習題,若是要評測web服務器,能夠處理Google search的負載。

這部分的代碼能夠從這裏得到。

對於認爲這樣作是無用的人:它確實是沒有用的。我只是碰巧喜歡這種挑戰,而它爲如下主題更新了個人記憶和知識:編寫填充代碼(已經不少年沒有寫過了),AMD64裝配(調用慣例,寄存器保護等等),系統調用,objdump,fini_array(最近一次我檢測的時候,GCC依然使用.dtors),printf格式化利用,gdb技巧(例如將內存塊寫入文件),還有低階的socket編程(過去幾年中我使用過boost)。

更新:Ubuntu增長了一個安全特性,這個特性提供了在最終的ELF表區域中只讀重定位,爲了可以在ubuntu中運行這個例子,在編譯的時候添加如下命令行:

-Wl,-z,norelro

好比:

gcc -Wl,-z,norelro test.c

原文 Implementing a web server in a single printf() call
翻譯 伯樂在線 - 欣仔

相關文章
相關標籤/搜索