wargame narnia writeup

wyzsk發表於2020-08-19
作者: litao3rd · 2015/04/09 11:02

前言


這一期的wargame難度明顯比之前的leviathan要高,而且已經涉及到相對完善的Linux溢位相關知識了。但是在overthewire上這才居然只是2/10的難度,看來我差得很遠啊。

narnia首頁,有如下提示,使用初始賬號和密碼登陸到目標機器,關於本wargame的所有檔案都在/narnia資料夾下面。Let's go

To login to the first level use:

Username: narnia0
Passowrd: narnia0
Data for the levels can be found in /narnia/.

level 0


narnia_level0_login.png

從目標機器的資料夾中我們可以看到,每個 level 都給了原始碼和編譯後的可執行檔案,每個可執行檔案都有set-uid許可權。只要溢位該可執行檔案,得到下一個 level 的 shell,就可以在/etc/narnia_pass/資料夾下面得到下一個 level 的密碼了。

首先正常執行一下narnia0這個檔案,看看有什麼提示沒有。

narnia_level0_run.png

從執行結果來看,應該是溢位緩衝區,然後修改棧中的另外一個自動變數,以此來過後面的邏輯判斷。從下面的原始碼檔案也可以看到,我的猜想是正確的。

#!c
#include <stdio.h>
#include <stdlib.h>

int main(){
        long val=0x41414141;
        char buf[20];

        printf("Correct val's value from 0x41414141 -> 0xdeadbeef!\n");
        printf("Here is your chance: ");
        scanf("%24s",&buf);

        printf("buf: %s\n",buf);
        printf("val: 0x%08x\n",val);

        if(val==0xdeadbeef)
                system("/bin/sh");
        else {
                printf("WAY OFF!!!!\n");
                exit(1);
        }

        return 0;
}

可以看到,輸入緩衝區buf,和待溢位變數都在main函式的棧幀中,這題很簡單,只要正確構造輸入就可以了。一開始,我只是構造了這樣的一個輸入:

narnia_level0_cmd1

很明顯,明明正確溢位修改了val的值,但是沒有得到想要的 shell,後來,發現原來是管道輸出給了程式之後,就會自動關閉了,造成程式返回的 shell 無法開啟。於是,修改shellcode如下,發現正確得到了密碼。

narnia_level0_crack

level 1


執行可執行程式,可以得到一個很有用的提示如下。

narnia_level1_login

好像只要把shellcode放到正確的環境變數中就可以了,從程式程式碼中,看到環境變數EGG的地址被作為函式指標呼叫了。

#!c
#include <stdio.h>

int main(){
        int (*ret)();

        if(getenv("EGG")==NULL){
                printf("Give me something to execute at the env-variable EGG\n");
                exit(1);
        }

        printf("Trying to execute EGG!\n");
        ret = getenv("EGG");
        ret();

        return 0;
}

構造如下的帶有shellcode的環境變數就可以正確溢位了。關於環境變數的地址計算,很多溢位類書籍都會提到,上百度google一下就可以得到想要的方法。

narnia_level1_crack

level 2


narnia_level2_login

從程式執行結果來看,似乎需要給一個輸入作為main函式的引數,估計是透過這個輸入來做為溢位點。

#!c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char * argv[]){
        char buf[128];

        if(argc == 1){
                printf("Usage: %s argument\n", argv[0]);
                exit(1);
        }
        strcpy(buf,argv[1]);
        printf("%s", buf);

        return 0;
}

這也是一個很基礎的溢位題目,正確的覆蓋main函式的返回地址就可以了。解法如下圖所示。我在這裡,將shellcode放在了EGG這個環境變數中,所以只要使用EGG的地址覆蓋main函式的返回地址就可以了。

narnia_level2_crack

level 3


從這道題目開始,難度開始慢慢加大了,不過依然都在控制之內。

narnia_level3_login

從程式執行結果來看,似乎將某個檔案作為引數傳給程式,然後程式開啟輸出到/dev/null這個裝置中去。

#!c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char **argv){

        int  ifd,  ofd;
        char ofile[16] = "/dev/null";
        char ifile[32];
        char buf[32];

        if(argc != 2){
                printf("usage, %s file, will send contents of file 2 /dev/null\n",argv[0]);
                exit(-1);
        }

        /* open files */
        strcpy(ifile, argv[1]);
        if((ofd = open(ofile,O_RDWR)) < 0 ){
                printf("error opening %s\n", ofile);
                exit(-1);
        }
        if((ifd = open(ifile, O_RDONLY)) < 0 ){
                printf("error opening %s\n", ifile);
                exit(-1);
        }

        /* copy from file1 to file2 */
        read(ifd, buf, sizeof(buf)-1);
        write(ofd,buf, sizeof(buf)-1);
        printf("copied contents of %s to a safer place... (%s)\n",ifile,ofile);

        /* close 'em */
        close(ifd);
        close(ofd);

        exit(1);
}

很神奇的事情,字元陣列char ofile[16] = "/dev/null";居然可以這樣初始化,我記得當年的譚老師的課本里不是這樣寫的啊。。。。

從原始碼來看,之前的猜測是對的。我一開始的想法是,重定向/dev/null裝置到某個檔案,這樣,將密碼的儲存檔案作為引數傳給程式,程式將密碼檔案中的內容輸出到我重定向的目標檔案中,就可以正確得到了。後來google了半天,沒有找到有效的重定向的方法。另闢蹊徑,我想到可以溢位緩衝區的內容,修改輸出檔案路徑,這樣就可以將結果輸出到某個檔案中去了。我構造了/tmp/narnia3/AAAAAAAAAAAAAAAAAAA/tmp/pass這個路徑下的一個檔案,該檔案被軟連結到密碼檔案。同時,該字串被作為引數輸入給了程式,/tmp/pass這部分子串被溢位到輸出檔案路徑的儲存緩衝區中,這樣輸入是密碼檔案的一個軟連結,輸出是/tmp/pass這樣的一個檔案。具體操作如下圖所示,因為待溢位程式具有set-uid許可權,所以執行是的有效使用者是下一個 level,注意/tmp下資料夾的訪問許可權問題。

narnia_level3_crack

level 4


narnia_level4_login

這一關程式執行居然沒有輸出,看來只能透過分析原始碼來找溢位點了。

#!c
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>

extern char **environ;

int main(int argc,char **argv){
        int i;
        char buffer[256];

        for(i = 0; environ[i] != NULL; i++)
                memset(environ[i], '\0', strlen(environ[i]));

        if(argc>1)
                strcpy(buffer,argv[1]);

        return 0;
}

從程式碼來看,程式清空了所有的環境變數,這樣在環境變數中存放shellcode的方法不可用了,不過,程式將輸入的main函式引數無限制複製到了buffer中,這樣就給了我們緩衝區溢位的漏洞,很基礎的一個緩衝區溢位題目,只要將shellcode放入到棧中,然後正確覆蓋函式返回地址就可以了。在猜測shellcode地址的時候,可能是因為棧偏移的問題,導致gdb中的棧地址和shell中執行時的實際地址有所偏移,不過新增一些NOP Sled就可以了。結果如下圖所示。

narnia_level4_crack

level 5


這一關從程式執行來看,也是透過溢位修改某個變數的值,但是從原始碼看,並不是簡單的溢位就可以修改了。

narnia_level5_login

#!c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char **argv){
        int i = 1;
        char buffer[64];

        snprintf(buffer, sizeof buffer, argv[1]);
        buffer[sizeof (buffer) - 1] = 0;
        printf("Change i's value from 1 -> 500. ");

        if(i==500){
                printf("GOOD\n");
                system("/bin/sh");
        }

        printf("No way...let me give you a hint!\n");
        printf("buffer : [%s] (%d)\n", buffer, strlen(buffer));
        printf ("i = %d (%p)\n", i, &i);
        return 0;
}

從來看,變數i和緩衝區buffer在棧中相鄰,但是,緩衝區輸入的時候使用了安全的snprintf()函式,這導致不能透過溢位來覆蓋變數i的值,但是snprintf()在呼叫的時候的格式化字串是由使用者作為main()函式的輸入,我們可以控制這個格式化字串,導致了格式化字串漏洞。

narnia_level5_check_vul

驗證的確有格式化字串漏洞,利用這個漏洞,可以讀寫任意地址的值,所以我們構造一個合適的格式化字串,就可以修改變數i的值,得到一個高許可權的shell。具體操作如下圖所示。

narnia_level5_crack

level 6


這一關需要兩個輸入作為main()函式的引數,猜測應該有很明顯的溢位點,難度就在於如何構造合適的溢位字串。

narnia_level6_login

#!c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

extern char **environ;

// tired of fixing values...
// - morla
unsigned long get_sp(void) {
       __asm__("movl %esp,%eax\n\t"
               "and $0xff000000, %eax"
               );
}

int main(int argc, char *argv[]){
        char b1[8], b2[8];
        int  (*fp)(char *)=(int(*)(char *))&puts, i;

        if(argc!=3){ printf("%s b1 b2\n", argv[0]); exit(-1); }

        /* clear environ */
        for(i=0; environ[i] != NULL; i++)
                memset(environ[i], '\0', strlen(environ[i]));
        /* clear argz    */
        for(i=3; argv[i] != NULL; i++)
                memset(argv[i], '\0', strlen(argv[i]));

        strcpy(b1,argv[1]);
        strcpy(b2,argv[2]);
        //if(((unsigned long)fp & 0xff000000) == 0xff000000)
        if(((unsigned long)fp & 0xff000000) == get_sp())
                exit(-1);
        fp(b1);
        exit(1);

從原始碼中可以看到,環境變數和多餘的main()函式引數都被清空了,導致無法在其中安放shellcode。緩衝區b1b2在棧中緊鄰,接下來是一個指向puts()函式的函式指標,於是有了覆蓋這個函式指標的思路。函式指標以b1為引數,進行函式呼叫,於是思路是用system()的地址覆蓋fp的值,然後在緩衝區b1中填充/bin/sh字串,這樣在程式結束的時候就會有system("/bin/sh")這個函式呼叫,得到一個高一級的shell。在程式中,首先strcpy(b1),然後再strcpy(b2),我們在構造帶有/bin/sh這個子串的字串時需要考慮到字串的長度問題,使得字串能夠正常結束。這樣,先使用緩衝區b1溢位覆蓋fp的值,使用system()的地址覆蓋該值,然後使用緩衝區b2溢位往b1中新增/bin/sh這樣的子串,b2的長度需要考慮。實際操作如下。

narnia_level6_crack

level 7


簡單的程式輸出已經提供不了太多的有效資訊了,但是還是可以看到有輸入,就有可能有緩衝區溢位問題。

narnia_level7_login

#!c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

int goodfunction();
int hackedfunction();

int vuln(const char *format){
        char buffer[128];
        int (*ptrf)();

        memset(buffer, 0, sizeof(buffer));
        printf("goodfunction() = %p\n", goodfunction);
        printf("hackedfunction() = %p\n\n", hackedfunction);

        ptrf = goodfunction;
        printf("before : ptrf() = %p (%p)\n", ptrf, &ptrf);

        printf("I guess you want to come to the hackedfunction...\n");
        sleep(2);
        ptrf = goodfunction;

        snprintf(buffer, sizeof buffer, format);

        return ptrf();
}
int main(int argc, char **argv){
        if (argc <= 1){
                fprintf(stderr, "Usage: %s <buffer>\n", argv[0]);
                exit(-1);
        }
        exit(vuln(argv[1]));
}
int goodfunction(){
        printf("Welcome to the goodfunction, but i said the Hackedfunction..\n");
        fflush(stdout);

        return 0;
}
int hackedfunction(){
        printf("Way to go!!!!");
        fflush(stdout);
        system("/bin/sh");

        return 0;
}

這個程式碼有點點長,不過思路還是很清楚的,在vuln()函式中,有我們的輸入,有一個函式指標緊鄰著緩衝區,使用了安全的snprintf()函式來複制我們的輸入到緩衝區中,依然有格式化字串漏洞。不過這題的難度在於,我們輸入的格式化引數沒有列印出來結果,導致我們無法根據輸出來調整輸入的格式化引數,而且由於棧偏移的問題,導致gdb中的地址和shell中實際執行的地址差距很大,基本上不能利用。好字啊程式列印出來了足夠的地址資訊,我們知道修改後的值和待修改的地址。於是,我先構造了含有目的地址和目標長度的格式化字串\x0c\xd5\xff\xff.%134514432d.,然後在該字串後面新增寫入的格式化引數%n,依次嘗試猜測,運氣不錯,猜到了第六個就得到了shell

本來是寫了一個Python指令碼來嘗試爆破的,但是技術太爛,導致指令碼執行的結果不太好,還是人工爆破來做的。其實這裡猜測的風險很大,因為如果字串的儲存地址不是四位元組對齊的話,這樣我們在字串中存放的地址就需要調整偏移,但是因為沒有輸出,導致無法知道這個偏移到底存在與否。好在題目設計得不是太難。

narnia_level7_crack

level 8


既然程式執行已經無法提供太多有效的資訊了,還是直接看程式碼吧。

#!c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// gcc's variable reordering fucked things up
// to keep the level in its old style i am 
// making "i" global unti i find a fix 
// -morla 
int i;

void func(char *b){
        char *blah=b;
        char bok[20];
        //int i=0;

        memset(bok, '\0', sizeof(bok));
        for(i=0; blah[i] != '\0'; i++)
                bok[i]=blah[i];

        printf("%s\n",bok);
}

int main(int argc, char **argv){

        if(argc > 1)
                func(argv[1]);
        else
        printf("%s argument\n", argv[0]);

        return 0;
}

看起來似乎很簡單,只是一個很簡單的緩衝區溢位,但是實際操作的時候發現,變數blah和緩衝區bok在棧中是相鄰的,導致如果輸入的字串太長的話,就會覆蓋blah這個變數,這個變數又是我們的輸入字串的基地址指標,如果被修改了,就無法正確訪問我們輸入的字串。

narnia_level8_run_check

從執行結果來看,輸入太長的字串,都會導致後面的字串沒有複製到緩衝區中,這樣也就無法覆蓋函式的返回地址,溢位失敗。於是改變了思路,既然太長的輸入字串會修改原來的基地址值,那麼就用原來的基地址值再覆蓋回去,這樣就相當於沒有修改了。只需要猜測原來的基地址值就可以了。從程式中可以看到,緩衝區被複制之後,沒有正確的結束符,這樣給了我們列印blah變數原來的值的可能。

narnia_level8_run_check2

緩衝區長度是20,所以輸入長度為20的字串正好可以覆蓋緩衝區,同時又沒有正確的結束符,就可以看到blah變數的值了。在這裡是0xffffd7c7,根據測試,我們輸入的字串長度每增加1,這個基地址值就會減少1,透過計算,就可以得到正確的基地址值了。具體操作過程如下圖所示。

narnia_level8_crack

end of the game


終於結束了這次的wargame,想到這一期的wargame難度只有2/10,我就知道後面還會有更多更好玩的東西。畢竟這裡還有沒涉及到ASLRstack canary等緩衝區溢位保護策略,不過這些在後面的遊戲都會有的。盡請期待~

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章