pwnable.kr [Toddler's Bottle] - input

Mom? how can I pass my input to a computer program?

ssh input2@pwnable.kr -p2222 (pw:guest)

這題流程相對較長,考查Linux編程的基本功(筆者做到這題不禁感嘆自己基本功還是欠了不少火候)。
在一開始,嘗試寫Python腳本去完成驗證,但stage 2關于stdio的驗證卻苦無思路。
這里感謝werew在他的writeup中提供的解決思路,這才豁然開朗。
參考鏈接:https://werewblog.wordpress.com/2016/01/11/pwnable-kr-input/

關于file descriptor maping的內容我另外做了整理,
詳見 UNIX 下 C 實現 Pipe Descriptors 映射

先放上題目源碼:

/* ssh input2@pwnable.kr -p2222 (pw:guest) */

int main(int argc, char* argv[], char* envp[]){
    printf("Welcome to pwnable.kr\n");
    printf("Let's see if you know how to give input to program\n");
    printf("Just give me correct inputs then you will get the flag :)\n");

    // argv
    if(argc != 100) return 0;
    if(strcmp(argv['A'],"\x00")) return 0;
    if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;
    printf("Stage 1 clear!\n"); 

    // stdio
    char buf[4];
    read(0, buf, 4);
    if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;
    read(2, buf, 4);
        if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;
    printf("Stage 2 clear!\n");
    
    // env
    if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
    printf("Stage 3 clear!\n");

    // file
    FILE* fp = fopen("\x0a", "r");
    if(!fp) return 0;
    if( fread(buf, 4, 1, fp)!=1 ) return 0;
    if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;
    fclose(fp);
    printf("Stage 4 clear!\n"); 

    // network
    int sd, cd;
    struct sockaddr_in saddr, caddr;
    sd = socket(AF_INET, SOCK_STREAM, 0);
    if(sd == -1){
        printf("socket error, tell admin\n");
        return 0;
    }
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons( atoi(argv['C']) );
    if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
        printf("bind error, use another port\n");
            return 1;
    }
    listen(sd, 1);
    int c = sizeof(struct sockaddr_in);
    cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
    if(cd < 0){
        printf("accept error, tell admin\n");
        return 0;
    }
    if( recv(cd, buf, 4, 0) != 4 ) return 0;
    if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;
    printf("Stage 5 clear!\n");

    // here's your flag
    system("/bin/cat flag");    
    return 0;
}

一共分為五個驗證步驟,先一個個來看。

Stage 1 argv

第一步比較常規,argv需要傳遞100個參數,即實際為argv[101](argv[0]指向執行的路徑,argv[100]為NULL)。

    /* stage 1 */
    char *argv[101] = {0};
    for(int i = 1; i<100; ++i)
        argv[i] = "a";
    argv[0] = "/home/input2/input";
    argv['A'] = "\x00";
    argv['B'] = "\x20\x0a\x0d";
    argv[100] = NULL;

Stage 2 stdio

可以根據上面的鏈接查看我整理的關于file descriptor maping的帖子,個中內容不再贅述。主要原理就是利用fork()創建一個當前進程的復刻,父進程和子進程分開進行兩個操作:使用dup2()函數將管道重定向,將所需的內容寫入相應的pipe。按題設要求我們需要重定向0-stdin和2-stderr。

然后是關于execve,這里使用該函數創建input進程:
在父進程中fork一個子進程,在子進程中調用exec函數啟動新的程序。exec函數一共有六個,其中execve為內核級系統調用,其他(execl,execle,execlp,execv,execvp)都是調用execve的庫函數。

  • 頭文件: unistd.h
  • 函數定義: int execve(const char *filename, char *const argv[ ], char *const envp[ ]);
  • 返回值: 函數執行成功時沒有返回值,執行失敗時的返回值為-1.
  • 函數說明: execve()用來執行參數filename字符串所代表的文件路徑,第二個參數是利用數組指針來傳遞給執行文件,并且需要以空指針(NULL)結束,最后一個參數則為傳遞給執行文件的新環境變量數組。
    /* stage 2 */
    int pipe_stdin[2] = {-1, -1};
    int pipe_stderr[2] = {-1, -1};
    pid_t pid_child;
    if ( pipe(pipe_stdin) < 0 || pipe(pipe_stderr) < 0 )
    {
        perror("Cannot create the pipe.");
        exit(1);
    }

    #define STDIN_READ   pipe_stdin[0]
    #define STDIN_WRITE  pipe_stdin[1]
    #define STDERR_READ  pipe_stderr[0]
    #define STDERR_WRITE pipe_stderr[1]
    if ( ( pid_child = fork() ) < 0 )   // do not forget the ()!
    {
        perror("Cannot create fork child.");
        exit(1);
    }

    if( pid_child == 0 )
    {
        /* child proc */
        sleep(1); //wait to pipe link 0,2
        close(STDIN_READ);
        close(STDERR_READ);
        write(STDIN_WRITE, "\x00\x0a\x00\xff", 4);
        write(STDERR_WRITE, "\x00\x0a\x02\xff", 4);
    }
    else
    {
        /* parent proc */
        close(STDIN_WRITE);
        close(STDERR_WRITE);
        dup2(STDIN_READ, 0);  //dup to 0-stdin
        dup2(STDERR_READ, 2); //dup to 2-stderr
        printf("start execve input.\n");
        execve("/home/input2/input", argv, envp);  //envp see stage 3
        perror("Fail to execute the program");
        exit(1);
    }
    printf("pipe link.\n");

Stage 3 envp

getenv("\xde\xad\xbe\xef")意為查看名為"\xde\xad\xbe\xef"的環境變量的值,這里我們只需傳遞一個環境變量,名為"\xde\xad\xbe\xef",值為"\xca\xfe\xba\xbe"即可。

/* stage 3 */
char *envp[2] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe", NULL};

Stage 4 file

按要求創建文件寫入內容即可。需要注意的是對于文件的操作需要在execve函數被調用之前完成,因為input進程不會等待當前進行對該文件的創建和寫入,需要保證在執行驗證之前完成文件的構造。

    /* stage 4 */  
    // ! : file open before execve , or the check will fail 
    FILE *fp = fopen("\x0a", "wb"); // wb,w are similar in linux but differ in win
    if(!fp)                         //see \x0d\x0a in win and \x0a in linux
    {
        perror("Cannot open file.");
        exit(1);
    }
    printf("open file success.\n");
    fwrite("\x00\x00\x00\x00", 4, 1, fp);
    fclose(fp);

Stage 5 network

通過socket完成linux下進程間的通信,和在windows下基本類似。
具體可見我關于windows下的socket通信的簡單實例。http://blog.csdn.net/qq_19550513/article/details/54965653
唯一區別在于windows下需要額外對winsock信息 WSAData 利用 WSAStartup() 函數進行初始化操作。

    /* stage 5 */
    sleep(2); // wait the server start
    int sockfd;
    char buf[10] = {0}; // buf to be sent
    int len;            // len of avail buf
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;  
    servaddr.sin_port = htons(9999);  // port in argv['C'] 
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //local
    if( (sockfd = socket(PF_INET, SOCK_STREAM, 0)) < 0 )  
    {  
        perror("socket error.");  
        exit(1);  
    }  
    if ( connect(sockfd, (struct sockaddr*) &servaddr, sizeof(servaddr)) < 0 )
    {
        perror("connect error.");
        exit(1);
        }
    printf("socket connect.\n");
    strcpy(buf, "\xde\xad\xbe\xef");
    len = strlen(buf);
    send(sockfd, buf, len, 0);
    close(sockfd);  

Final Stage

整合后,ssh登錄后在/tmp目錄下我們自己的目錄,利用vi寫入c文件,再用gcc編譯生成可執行文件。
到這里還沒有結束,因為只有在當前目錄guest才有寫的權限,而flag路徑為/home/input2/flag。
這里我們需要一個soft link指向flag文件,在當前目錄 ln -s /home/input2/flag flag 即可。注意不能創建hard link,因為guest對flag文件本身是沒有讀寫權限的。
c文件內容如下:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h> 
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main()
{
    /* stage 1 */
    char *argv[101] = {0};
    for(int i = 1; i<100; ++i)
        argv[i] = "a";
    argv[0] = "/home/input2/input";
    argv['A'] = "\x00";
    argv['B'] = "\x20\x0a\x0d";
    argv['C'] = "9999"; //server port
    argv[100] = NULL;

    /* stage 3 */
    char *envp[2] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe", NULL};

    /* stage 4 */  // ! : file open before execve , or the check will fail 
    FILE *fp = fopen("\x0a", "wb"); // wb,w are similar in linux but differ in win
    if(!fp)                         //see \x0d\x0a in win and \x0a in linux
    {
        perror("Cannot open file.");
        exit(1);
    }
    printf("open file success.\n");
    fwrite("\x00\x00\x00\x00", 4, 1, fp);
    fclose(fp);
    
    /* stage 2 */
    int pipe_stdin[2] = {-1, -1};
    int pipe_stderr[2] = {-1, -1};
    pid_t pid_child;
    if ( pipe(pipe_stdin) < 0 || pipe(pipe_stderr) < 0 )
    {
        perror("Cannot create the pipe.");
        exit(1);
    }

    #define STDIN_READ   pipe_stdin[0]
    #define STDIN_WRITE  pipe_stdin[1]
    #define STDERR_READ  pipe_stderr[0]
    #define STDERR_WRITE pipe_stderr[1]
    if ( ( pid_child = fork() ) < 0 )   // do not forget the ()!
    {
        perror("Cannot create fork child.");
        exit(1);
    }

    if( pid_child == 0 )
    {
        /*child proc*/
        sleep(1); //wait to pipe link 0,2
        close(STDIN_READ);
        close(STDERR_READ);
        write(STDIN_WRITE, "\x00\x0a\x00\xff", 4);
        write(STDERR_WRITE, "\x00\x0a\x02\xff", 4);
    }
    else
    {
        /*parent proc*/
        close(STDIN_WRITE);
        close(STDERR_WRITE);
        dup2(STDIN_READ, 0);  //dup to 0-stdin
        dup2(STDERR_READ, 2); //dup to 2-stderr
        printf("start execve input.\n");
        execve("/home/input2/input", argv, envp);
            perror("Fail to execute the program");
            exit(1);
    }
    printf("pipe link.\n");

    /* stage 5 */
    sleep(2); // wait the server start
    int sockfd;
    char buf[10] = {0}; // buf to be sent
    int len;            // len of avail buf
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;  
    servaddr.sin_port = htons(9999);  // port in argv['C'] 
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //local
    if( (sockfd = socket(PF_INET, SOCK_STREAM, 0)) < 0 )  
    {  
        perror("socket error.");  
        exit(1);  
    }  
    if ( connect(sockfd, (struct sockaddr*) &servaddr, sizeof(servaddr)) < 0 )
    {
        perror("connect error.");
        exit(1);
        }
    printf("socket connect.\n");
    strcpy(buf, "\xde\xad\xbe\xef");
    len = strlen(buf);
    send(sockfd, buf, len, 0);
    close(sockfd);  

    return 0;
}

最后的運行情況如下:

input2@ubuntu:/tmp/umiade$ ./setinput 
open file success.
start execve input.
Welcome to pwnable.kr
Let's see if you know how to give input to program
Just give me correct inputs then you will get the flag :)
Stage 1 clear!
pipe link.
Stage 2 clear!
Stage 3 clear!
Stage 4 clear!
socket connect.
Stage 5 clear!
Mommy! I learned how to pass various input in Linux :)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • linux資料總章2.1 1.0寫的不好抱歉 但是2.0已經改了很多 但是錯誤還是無法避免 以后資料會慢慢更新 大...
    數據革命閱讀 12,239評論 2 33
  • 一、溫故而知新 1. 內存不夠怎么辦 內存簡單分配策略的問題地址空間不隔離內存使用效率低程序運行的地址不確定 關于...
    SeanCST閱讀 7,883評論 0 27
  • Linux 進程管理與程序開發 進程是Linux事務管理的基本單元,所有的進程均擁有自己獨立的處理環境和系統資源,...
    JamesPeng閱讀 2,512評論 1 14
  • 過去中診一直講“司外揣內”,現在我們有條件、有能力直觀的看到,是否有必要繼續“揣”呢?為什么還要“揣”呢!——內鏡...
    聽聽益生閱讀 255評論 0 0
  • 在看這本書之前,很慶幸自己認識到了自己,了解了自己,說明我還是有得救。五年十年二十年乃至更久,積習一定要改。就像俞...
    我便如此閱讀 290評論 0 0