ゼオスTTのブログ

気が向いた時に、主にプログラミング関係の記事を書くつもり。しかし気が向かない。

Kaspersky Industrial CTF Quals 2017 write-up

Harekazeで参加。2450点で32位。

以下、自分が解いた or ある程度取り組んだ問題について。

[RE 300] Backdoor Pi

ルート以下を丸ごとZIPで固めたものが渡される。
怪しいファイルを探すところから始めろということか・・・(forensicかよ)。

/bin/backが「/binにある」かつ「更新日時が新しい」といかにも怪しいため、fileしてみると、

$ file /bin/back
/bin/back: python 2.7 byte-compiled

デコンパイルしてみると、

import sys
import os
import time
from flask import Flask
from flask import request
from flask import abort
import hashlib

def check_creds(user, pincode):
    if len(pincode) <= 8 and pincode.isdigit():
        val = '{}:{}'.format(user, pincode)
        key = hashlib.sha256(val).hexdigest()
        if key == '34c05015de48ef10309963543b4a347b5d3d20bbe2ed462cf226b1cc8fff222e':
            return 'Congr4ts, you found the b@ckd00r. The fl4g is simply : {}:{}'.format(user, pincode)
    return abort(404)

app = Flask(__name__)

@app.route('/')
def hello():
    return '<h1>HOME</h1>'

@app.route('/backdoor')
def backdoor():
    user = request.args.get('user')
    pincode = request.args.get('pincode')
    return check_creds(user, pincode)

if __name__ == '__main__':
    app.run(threaded=True, host='0.0.0.0', port=3333)

pincodeは約108通りなので、userさえ分かれば全探索で解ける。

pi/homeに唯一あったディレクトリ)やadminを試してみたがヒットしない。

ユーザ一覧といえば、/etc/passwd

$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/bin/sh
man:x:6:12:man:/var/cache/man:/bin/sh
lp:x:7:7:lp:/var/spool/lpd:/bin/sh
mail:x:8:8:mail:/var/mail:/bin/sh
news:x:9:9:news:/var/spool/news:/bin/sh
uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh
proxy:x:13:13:proxy:/bin:/bin/sh
www-data:x:33:33:www-data:/var/www:/bin/sh
backup:x:34:34:backup:/var/backups:/bin/sh
list:x:38:38:Mailing List Manager:/var/list:/bin/sh
irc:x:39:39:ircd:/var/run/ircd:/bin/sh
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh
nobody:x:65534:65534:nobody:/nonexistent:/bin/sh
libuuid:x:100:101::/var/lib/libuuid:/bin/sh
pi:x:1000:1000:,,,:/home/pi:/bin/bash
sshd:x:101:65534::/var/run/sshd:/usr/sbin/nologin
ntp:x:102:104::/home/ntp:/bin/false
statd:x:103:65534::/var/lib/nfs:/bin/false
messagebus:x:104:106::/var/run/dbus:/bin/false
usbmux:x:105:46:usbmux daemon,,,:/home/usbmux:/bin/false
lightdm:x:106:109:Light Display Manager:/var/lib/lightdm:/bin/false
avahi:x:107:110:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/bin/false
b4ckd00r_us3r:x:1001:1004::/home/b4ckd00r_us3r:/bin/bash

b4ckd00r_us3rがものすごく怪しい。

$ python solve_backdoor_pi.py b4ckd00r_us3r
Congr4ts, you found the b@ckd00r. The fl4g is simply : b4ckd00r_us3r:12171337

ということで、KLCTF{b4ckd00r_us3r:12171337}

[Fun 150] Old School

なんか渡される。

$ file old_school.bin
old_school.bin: NES ROM image (iNES): 2x16k PRG, 1x8k CHR [V-mirror]

NESが分からないので調べてみると、Nintendo Entertainment Systemの略で、いわば海外版のファミコンらしい。
ROM配布サイト的なものが多数見つかったが、違法な臭いがプンプンする。著作権とか著作権とか著作権とか。

さて、VirtuaNESなるエミュレータが見つかったので、それで起動。

f:id:zeosutt:20171010020054p:plain

パックマンだった。

f:id:zeosutt:20171010020636p:plain

とりあえず1面をクリアしてみると、

f:id:zeosutt:20171010020343p:plain

出ましたASCII。WELCOMEだそう。
2面もクリアしてやろうと思ったが、なんとこの数字のオブジェクトも壁扱いで、一部のドットが食えない。

正攻法では解けないみたい(CTFだし当然)なので、メモリを眺めていると、

f:id:zeosutt:20171010022303p:plain

ROMの領域に、2面に対応していそうな部分を発見。
周囲のアドレスにもそれっぽい(各面に対応していそうな)部分があったが、上記画像に77がないように、一部欠損している。

アドレス順に並べると、次のようになる。

FLAG AT WELCOE YOU WILL FIND THE THE LEVEL EIGH? /NVxy9?は多分Tだが、484となっている)

8面にフラグがあるっぽいので、楽して8面に飛ぼうと試行錯誤するも、うまく行かない。
しかし、その過程において、0x0060の値がx座標、0x0062の値がy座標であることが分かる。
仕方ないので、必要に応じてそれらの値を書き換えることで、8面まで地道にクリアしていくことに。

結局、各面のメッセージは以下の通り。

1. 
2. WELCOME
3. YOU WILL
4. FIND THE
5. FLAG AT
6. THE LEVEL
7. EIGH?
8. /nXNuVxy9

パッと見、/nXNuVxy9短縮URLとかのあれっぽい。
ということで、まずは https://goo.gl/nXNuVxy9 にアクセスしてみたが、存在しない。
次に、「海外といえばPastebinだよねー」と https://pastebin.com/raw/nXNuVxy9 にアクセスしてみると、見事にヒット。

S0xGQ1RGe1czXzRMTF9MMFYzX1IzVFIwfQ==

はい。

$ echo S0xGQ1RGe1czXzRMTF9MMFYzX1IzVFIwfQ== | base64 -d
KLFCTF{W3_4LL_L0V3_R3TR0}

しかし、これをそのまま提出してもWA。

問題文の

Concat answer to KLCTF prefix

を参考に幾つか試してみたところ、KLCTFW3_4LL_L0V3_R3TR0 が正解だった。マジ何がしたいのか分からないんすけど・・・。

全チーム中3番目に解けて喜んでいたが、この問題が全問題中最低点数という悲しい事実。

ちなみに - その1

普通なら「SCORE」や「HIGH SCORE」と表示される部分が「PASTE」や「BIN」となっていて、一応それがPastebinを示唆していたっぽい。
答えが分かってからそれがヒントだったと分かるやつ。

guessingはクソ。

ちなみに - その2

メモリ上で484となっていた部分は、表示上も484となっていた。

f:id:zeosutt:20171010032948p:plain

T84なので、おそらくゴミがくっ付いただけで、この面のメッセージは予想通りEIGHT

[Crypto 800] Bad computations

最高点の問題のくせに、多くのチームが通していたため挑戦。

渡されたcrypt.pyの識別子を分かりやすくrename&一部関数の計算量を落とすと、

from random import choice
from sys import argv
from base64 import b64encode
from math import sqrt

b = 22

def gen_prime_list(first, last):
    prime_list = []
    for a in range(first, last + 1):
        for i in range(2, a):
            if (a % i) == 0:
                break
        else:
            prime_list.append(a)

    return prime_list

def sieve(n):
    is_prime = [True for _ in range(n)]

    for i in range(2, int(sqrt(n)) + 1):
        if is_prime[i]:
            for j in range(i * i, n, i):
                is_prime[j] = False

    prime_list = [i for i in range(2, n) if is_prime[i]]

    return prime_list

def xgcd(a, b):
    if a == 0:
        return (b, 0, 1)
    else:
        g, y, x = xgcd(b % a, a)
        return (g, x - (b // a) * y, y)

def modinv(a, m):
    g, x, y = xgcd(a, m)
    if g != 1:
        raise Exception('Oops! Error!')
    else:
        return x % m

def L(u, n):
    return (u - 1) // n

if __name__ == '__main__':
    print("Key cryptor v1.0")

    if len(argv) != 2:
        print("Start script like: python crypt.py <YourOwnPasswordString>")

    if (not str(argv[1]).startswith("KLCTF{")) or (not str(argv[1]).endswith("}")):
        print("Error! Password must starts with KLCTF")
        exit()

    p = choice(gen_prime_list(100, 1000))
    q = choice(gen_prime_list(200, 1000))

    print("Waiting for encryption...")

    n = p * q
    g = None
    for i in range(n + 1, n * n):
        if ((i % p) == 0) or ((i % q) == 0) or ((i % n) == 0):
            continue

        g = i
        break

    if g is None:
        print("Error! Can't find g!")
        exit()

    lamb = (p - 1) * (q - 1)
    mu = modinv(L(pow(g, lamb, n * n), n), n) % n

    rc = sieve(n - 1)
    if len(rc) == 0:
        print("Error! Candidates for r not found!")
        exit()

    if p in rc:
        rc.remove(p)
    if q in rc:
        rc.remove(q)

    r = choice(rc)

    password = [ord(x) for x in argv[1][6:-1]]
    dcew = (pow(g, b, (n * n)) * pow(r, n, (n * n))) % (n * n)

    for i in range(len(password)):
        password[i] = (((pow(g, password[i], (n * n)) * pow(r, n, (n * n))) % (n * n)) * dcew) % (n * n)
        password[i] = (L(pow(password[i], lamb, (n * n)), n) * mu) % n

    password = b64encode(bytearray(password))
    print(str(password)[2:-1])

passwordをいじっている部分の詳細はよく分からないが、「入力長と出力(b64encode()する前)長は同じ」、「入力のi文字目は出力のi文字目にしか影響しない」ことは容易に分かる。
さらに、ちょっと実験してみると、awに、bxに、_uにと、変換により+22されることが分かる(変換式に現れるbの値と同じなので、つまりはそういう式なのだろう)。

また、問題文には以下のようにある。

The answer for this task is the password the encrypted version of which: hnd/goJ/e4h1foWDhYOFiIZ+f3l1e4R5iI+Gin+FhA==

よって、次のように解ける。

from base64 import b64decode

tmp = b64decode('hnd/goJ/e4h1foWDhYOFiIZ+f3l1e4R5iI+Gin+FhA==');
print(bytearray([c - 22 for c in tmp]))
$ python solve_crypt.py
bytearray(b'paillier_homomorphic_encryption')

ということで、KLCTF{paillier_homomorphic_encryption}

paillier homomorphic encryptionでググってみると、Paillier cryptosystem - Wikipediaが出てきたので、これを実装したものなのかな??

何にせよ、これのどこが800点・・・?

[RE 400] Smart Heater (part 1)(解けてない)

何回かサーバが落ちてた。その上、アクセスできても問題ファイルが見つからず。
そんな中、http://94.130.149.88/backup/backup.tarを見つけてくれたst98さんに感謝(プロ!)。

解凍すると、cliindex.phpmainServerの3ファイル。

解析すると、次のことが分かる。

  • mainServer./pass.confから読み込んだ文字列を/dev/shm/shared_memoryに書き込む

  • cliはポート2323でlistenして、接続を受けるとパスワードを要求する。このときのパスワードは/dev/shm/shared_memoryから読み込んだ文字列。そして、このパスワードがフラグ

しかし、./pass.confまたは/dev/shm/shared_memoryの値を手に入れる方法が分からず、フラグ獲得には至らなかった。

にしても、問題ファイルを探させる必要はあったのか・・・?

[RE 700] BlaBlaMan(解けてない)

PE32+が渡される。同じものがリモートで動いている。

ローカルで実行してみると、

Talk to me: 541ECF0D483B4B570A1EFF6102C88E970F0FDFA8F17FED6B2E342815FE912314

のように64桁のランダムな16進数が表示され、入力待ちになる。
少し時間が経つか、適当な値(表示されている16進数でも)を入力すると、何の表示もなく終了する。

解析すると、次のことが分かる。

  • seed.txtから読み込んだ数値と_time64()の返り値とを掛け算し、それでsrand()している(多分、seedを予想する解法を防ぐため)
  • 入力として64桁の16進数を期待している
  • data.txtには16進数が4つ書かれている
  • 表示されている16進数と入力の16進数を先頭から16桁ずつ、4回に分けて処理している。各処理は以下の通り
ans := 表示されている16進数(から抜き出した16桁)
x := 入力(から抜き出した16桁)
a := data.txtの1つ目の値
b := data.txtの2つ目の値
c := data.txtの3つ目の値
d := data.txtの4つ目の値

tmp := ax^3 - bx^2 + cx - d (mod 2^64)
if (tmp !≡ ans (mod 2^64)) {
    tmpを表示して終了
}
  • 適当な値を入力した際に何も表示されなかったのは、data.txtがなかったから
  • 4処理分一致すると、flag.txtの内容が表示される

ということで、表示されたtmpを基にa、b、c、dの値をあらかじめ求めておけば、あとはそれを使って合同方程式を解くことでフラグが得られる。

そして、各値は以下のように求まる。

a = 0x23A79C805D1296DD
b = 0x88A4495017D18AAA
c = 0x91358369274ACD98
d = 0xFB3491CCD0195483

上の合同方程式の解は存在しない場合もあるので、4処理分全てに解があるような16進数が得られなかった場合は再接続の必要がある。

と、ここまでは分かったのだが、肝心の合同方程式の解き方が分からず、フラグ獲得には至らなかった。

あと一歩だったのにー! 悔しい。

追記

SMTソルバで解けるらしい。

ふるかわさん、ありがとうございます!