Angstrom CTF 2018 write-up

 3月16日から22日にかけて行われたAngstrom CTF 2018に参加してきました!開始早々ドタバタあったそうですが、典型的?な問題が多く、とても勉強になりました。得点は1435点でした。

angstromCTF

150点以上の問題はほぼ解けていませんのでご注意ください(m´・ω・`)m ゴメン…

---MISC---

・Waldo2

同じjpgファイルがたくさん入っているが、どれかは必ず何かが違うはずなのでフォルダ内をサイズ順でソートすると、waldo339.jpgだけ異様にサイズが小さい。これはただのテキストファイルで、拡張子をtxtにして開くとフラグがある。

actf{r3d_4nd_wh1t3_str1p3s}

・That's Not My Name...

リンク先のgettysburg.pdfは、読み込もうとしてもエラーで読み込めない。ひとまずダウンロードしてバイナリエディタで開いてみると、先頭がPKとなっており、Zip形式の圧縮ファイルとわかる。

f:id:verliezer93764:20180323113136j:plain

解凍すると_rels、docProps、Wordというフォルダと[Content_Types].xmlというファイルが中に入っている。実はこれはZipファイルというよりもWordファイルで、TrIDに投げると画像のような結果が出る。

f:id:verliezer93764:20180323113803p:plain

拡張子をdocxにして開くと、フラグがある。

f:id:verliezer93764:20180323112842p:plain

actf{thanks_mr_lincoln_but_who_even_uses_word_anymore}

・File Transfer

与えられたpcapファイルをWireSharkで開くと、23個目のパケットでjpeg画像を受け取ったことがわかる。[ファイル]->[オブジェクトをエクスポート]->[HTTP]->[保存]にてf2J0Qiというファイルを保存し、拡張子をjpgにして開くと、フラグが手に入る。

f:id:verliezer93764:20180323114741j:plain

actf{0ver_th3_w1re}

・gif

プリン(ポケモン)のjif画像が渡される。サイズが大きいのでバイナリエディタで開いてみると、pngファイルは16進で89 50 4E 47から始まりAE 42 60 68で終わるが、これらがあちことでみられる。つまり渡されたファイルは多くのpngファイルが連結されているものだと考えられるので、これを分解する必要がある。下記のpythonスクリプトで分解した。


#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import struct

infile=open("jiggs/jiggs.gif","rb")
basebintext=infile.read()
infile.close()

infile=open("jiggs/jiggs.gif","rb")
outfile=open("jiggs/1.png","wb")
i=1

final_four_bytes=["ae","42","60","82"]
latest_four_bytes=["","","",""]

for c in basebintext:

    #書き込み先ファイルに元画像から抽出した値を入れていく
    outfile.write(infile.read(1))

    #読み込んだ最新の4バイトをlatest_four_bytesに入れる
    for j in range(0,3):
        latest_four_bytes[j]=latest_four_bytes[j+1]
    latest_four_bytes[3]=str(format(c,'x'))

    
    #latest_four_bytesがPNGの終端4バイトと一致したら
    #書き込み先を新たなファイルに変える
    if latest_four_bytes==final_four_bytes:
        outfile.close()
        i+=1
        outfile=open("jiggs/"+str(i)+".png","wb")

infile.close()
outfile.close()


 これにより、いくつかのpngファイルが得られるが、上のスクリプトで分解を行っていた場合には5.pngにフラグが書かれている。

f:id:verliezer93764:20180323125916p:plain

actf{thats_not_how_you_make_gifs}

(もっといいやり方があったのかな?)

---CRYPTO---

・Warmup

ヒントを見ると、アフィン暗号らしい。Affine Cipherで、a=19,b=12としてDecryptすると、フラグが得られる。

actf{it_begins}

・Back to Base-ics

遷移先のページには次の内容が書かれている。

 Part 1: 011000010110001101110100011001100111101100110000011011100110010101011111011101000111011100110000010111110110011000110000

Part 2: 165 162 137 145 151 147 150 164 137 163 151 170 164 63 63
Part 3: 6e5f7468317274797477305f733178
Part 4: dHlmMHVyX25vX20wcmV9

 

Flag is the concatenation of the four decoded parts.

Part1,2,3はASCIIコード変換機を使って、2進数、8進数、16進数からASCII文字に変換する。Part4はhttp://www.convertstring.com/ja/EncodeDecode/Base64Decodeを使ってBase64の状態からデコードする。part1から「actf{0ne_tw0_f0」、part2から「ur_eight_sixt33」、part3から「n_th1rtytw0_s1x」、part4から「tyf0ur_no_m0re}」が得られるので、フラグは

actf{0ne_tw0_f0ur_eight_sixt33n_th1rtytw0_s1xtyf0ur_no_m0re}

・XOR

遷移先のページには、以下の内容が書かれている。

fbf9eefce1f2f5eaffc5e3f5efc5efe9fffec5fbc5e9f9e8f3eaeee7

題名や問題文から、これは平文Pを1Byteずつに分割し、それぞれに対し固定の1Byteの鍵KとのXORを計算し、接合したものだとわかる。よって、その鍵Kを見つけて同様にXORを計算すれば平文Pを復元できる。鍵が短いので、ブルートフォースアタックで復号を試みる(先頭が「actf{」になるのを見越して鍵を復元してもよい)。


#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys

ciphertext="fbf9eefce1f2f5eaffc5e3f5efc5efe9fffec5fbc5e9f9e8f3eaeee7"
i=1

fixed_ciphertext=[]
ch=""

for c in ciphertext:
    ch+=c

    if i%2==0:
        fixed_ciphertext.append(ch)
        ch=""

    i+=1

i=0
plaintextnum=[]
plaintext=""
plaintext_list=[]

for i in range(255):
    
    success_flag = True

    #暗号文Cと鍵候補iでXORをとる
    for s in fixed_ciphertext:
        asc = int(s, 16) ^ int(i)

        #XORの結果がASCIIの範囲外に
        #あった場合、次の鍵を試す
        if 32 >= asc or 127 <= asc:
            success_flag = False
            break

        plaintextnum.append(asc)

    #XORの結果のすべての文字がASCIIの
    #範囲にあれば、平文候補に加える。
    if success_flag:
        
        for num in plaintextnum:
            plaintext+=chr(num)

        plaintext_list.append(plaintext)
        plaintext=""

    plaintextnum=[]

if len(plaintext_list)==0:
    print("Failed to decrypt this ciphertext... :(")
else:
    print("Succeeded in decrypting and candidates of plaintext are below.")
    print("--------------------------------------------------------------")

    #すべての平文候補を表示する
    for text in plaintext_list:
        print(text)

    print("--------------------------------------------------------------")

input("Put ENTER to end the program...")

 結果

f:id:verliezer93764:20180323151011p:plain

actf{hope_you_used_a_script}

・Intro to RSA

 p、qが与えられているので、C^d mod nを計算することによって平文Pを復元することができる。ただし、Cもdも非常に大きいので、復号するためのC^d mod nの計算には工夫が必要。http://post1.s105.xrea.com/を参照しよう。


#拡張ユークリッド互除法「もどき」
#しかもこのコード専用
def bad_expanded_gcd(e,en):
    k=1
    while True:
        if (k*en+1)%e==0:
            return (k*en+1)//e, k
        k+=1

#solve (base^exp) mod n
def solve_exp_mod(base,exp,n):

    expbin=format(exp,'b')
    expbintext=str(expbin)
    
    plaintext=1
    
    for c in expbintext:
        if c=='1':
            plaintext=plaintext*plaintext*base%n
        else:
            plaintext=plaintext*plaintext%n

    return plaintext
            
def main():
    p=169524110085046954319747170465105648233168702937955683889447853815898670069828343980818367807171215202643149176857117014826791242142210124521380573480143683660195568906553119683192470329413953411905742074448392816913467035316596822218317488903257069007949137629543010054246885909276872349326142152285347048927
    q=170780128973387404254550233211898468299200117082734909936129463191969072080198908267381169837578188594808676174446856901962451707859231958269401958672950141944679827844646158659922175597068183903642473161665782065958249304202759597168259072368123700040163659262941978786363797334903233540121308223989457248267
    e=65537
    c=4531850464036745618300770366164614386495084945985129111541252641569745463086472656370005978297267807299415858324820149933137259813719550825795569865301790252501254180057121806754411506817019631341846094836070057184169015820234429382145019281935017707994070217705460907511942438972962653164287761695982230728969508370400854478181107445003385579261993625770566932506870421547033934140554009090766102575218045185956824020910463996496543098753308927618692783836021742365910050093343747616861660744940014683025321538719970946739880943167282065095406465354971096477229669290277771547093476011147370441338501427786766482964
    
    n=p*q
    en=(p-1)*(q-1)
    
    d,k=bad_expanded_gcd(e,en)

    # P = C^d mod n
    plaintext=solve_exp_mod(c,d,n)
    
    print("Plain text is ...")
    print("-- dec version --")
    print(plaintext)
    print("-- hex version --")
    print(format(plaintext,'x'))
    
if __name__=="__main__":
    main()

 実行結果

f:id:verliezer93764:20180323154505p:plain

16進数での出力結果をASCIIコード変換機に投げるとフラグを獲得。

actf{rsa_is_reallllly_fun!!!!!!}

・ofb

 ブロック暗号は、平文を固定長のブロックに分けて、それぞれの平文ブロックに対し暗号化をし暗号ブロックを作るということを行い、最後に暗号ブロックを結合して暗号文を完成させるという暗号方式。そしてそれぞれのブロックをどのように暗号化するか、各ブロックを暗号化させる鍵をどう生成するかによってECBモード、CBCモード、CTSモード、CFBモード、OFBモード、CTRモードがある。先ほどの問題のXORによる暗号化はある意味ではECBモードといえる。ECBモードは暗号化を通じて平文ブロックと暗号文ブロックを一対一対応させるもので、実装は簡単だが、使うべきではない。

OFBモードは、つぎのような暗号化手順である。

f:id:verliezer93764:20180323171447p:plain

ただし、数値Aを数値Bで2回XORすると数値Aに戻るという特性から、構造上は復号も同じようにできる。

さて、問題ではpythonによる暗号化プログラムと、それにより暗号化されたファイルflag.png.encが与えられる。

暗号化プログラム(encrypt.py)は下の通り。


import struct

def lcg(m, a, c, x):
	return (a*x + c) % m

m = pow(2, 32)

with open('lcg') as f:
	a = int(f.readline())
	c = int(f.readline())
	x = int(f.readline())

d = open('flag.png').read()
d += '\x00' * (-len(d) % 4)
d = [d[i:i+4] for i in range(0, len(d), 4)]

e = ''
for i in range(len(d)):
	e += struct.pack('>I', x ^ struct.unpack('>I', d[i])[0])
	x = lcg(m, a, c, x)

with open('flag.png.enc', 'w') as f:
	f.write(e)
	f.close()


まずは、上のプログラムのflag.pngとflag.png.encを入れ替える。これで復号のベースはできる。そして、次は初期化ベクトルと暗号化関数Fを明らかにする。関数Fは、lcg関数で行われている。これは線形合同法という、n+1回目の出力がX[n+1]=(a*X[n]+c) mod mで定義されている実行速度を重視した疑似乱数生成方法である。その中で、パラメータa,c,x,mについては、m以外はlcgという外部ファイルから読み込んでいる。よって、a,c,xを求めることができればこの問題を解くことができる。ところで、フラグファイルがpngならば、先頭16バイトが89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52で始まっているはずなので、flag.png.encの最初の16バイト18 9A 6D 45 89 A2 9C 3F 4E B6 9D 78 B3 6F 89 27と照らし合わせると、上図でいうK0,K1,K2,K3は

K0=89 50 4E 47 XOR 18 9A 6D 45=91CA2302(16)=2445943554(10)、

K1=0D 0A 1A 0A XOR 89 A2 9C 3F=84A88635(16)=2225636917(10)、

K2=00 00 00 0D XOR 4E B6 9D 78=4EB69D75(16)=1320590709(10)、

K3=49 48 44 52 XOR B3 6F 89 27=FA27CD75(16)=4196912501(10)

であることがわかる。また、線形合同法から

a*K0+c≡K1 (mod m)

a*K1+c≡K2 (mod m)

a*K2+c≡K3 (mod m)

すなわち

a*(K1-K0)≡(K2-K1) (mod m)

a*(K2-K1)≡(K3-K2) (mod m)

が成立する。具体的に表すと、

220306637*a≡905046208 (mod 2^32)

905046208*a≡1418645504 (mod 2^32)

である。

文字が一つになったので、aの値を求めることができる。長いブルートフォースアタックの末、a=3204287424と求まった。

次に、cを求める。cは線形合同法において足される役目となっているので、法mより大きくする必要はなく、c<=2^32である。

先ほどaが求まったので、a*K0+c≡K1 (mod m)にaを代入し、具体的には

7837506169896064896+c≡2225636917 (mod 2^32)からcの値を求めると、c=1460809397である。

最後に、先ほどの定義でいうX[0]の値についてであるが、上のencrypt.pyではlcgファイルから読み取るxの値を線形合同法で攪乱する前にXOR処理「e += struct.pack('>I', x ^ struct.unpack('>I', d[i])[0])」を行っているので、x=K0=2445943554である。以上から、lcgという名前のファイルを作り、a,c,xの順に3204287424、1460809397、2445943554と一行ずつ書いてencrypt.py(改変後)を走らせると、flag.pngが復元され、フラグが手に入る。(python2系で実行した)

参考:decrypt.py


import struct

def lcg(m, a, c, x):
	return (a*x + c) % m

m = pow(2, 32)

a = 3204287424
c = 1460809397
x = 2445943554

d = open('flag.png.enc').read()
d += '\x00' * (-len(d) % 4)
d = [d[i:i+4] for i in range(0, len(d), 4)]

e = ''
for i in range(len(d)):
	e += struct.pack('>I', x ^ struct.unpack('>I', d[i])[0])
	x = lcg(m, a, c, x)

with open('flag.png', 'w') as f:
	f.write(e)
	f.close()

f:id:verliezer93764:20180323182725p:plain

actf{pad_rng}

 

---WEB---

・get me

遷移先ページのsubmitを押すとHey, you're not authorized!と言われるが、URLに「http://web.angstromctf.com:3005/?auth=false」とauth=falseとなっているので、auth=trueにすると、フラグが表示される。

actf{why_did_you_get_me}

・Sequel

ログインページのUsername、Passwordのいずれかのフォームに「'」を入力するとエラーメッセージが表示される。仮にログインのための条件文が「user ' (入力値1) ' について、password=' (入力値2) 'が正しいか」というような構造をしていると、「'」を入力した場合構文エラーが起こってしまう。さて、usernameの欄に「admin」、passwordの欄に「' or '1'='1」と打つと、パスワード照合部分で条件が「password=' ' or '1'='1 '」となるが、'1'='1'なので、この条件は真になってしまう。その結果、adminとしてのログインが通る。

actf{sql_injection_more_like_prequel_injection}

・Source Me 2

ソースを見ると以下のようなJavaScriptがある。


    var checkLogin = function () {
        var password = document.getElementById("password").value;
        if (document.getElementById("uname").value != "admin"){
            console.log(uname);
            document.getElementById("message").innerHTML = "Only admin user allowed!";
            return;
        } else {
            var passHash = md5(password);
            if (passHash == "bdc87b9c894da5168059e00ebffb9077"){
                window.location = "./login.php?user=admin&pass=" + password;
            } else {
                document.getElementById("message").innerHTML = "Incorrect Login";
            }
        }
        return;
    }

つまり、usernameはadminで、passwordのmd5ハッシュ値がbdc87b9c894da5168059e00ebffb9077でなければならない。

Best MD5 & SHA1 Decrypter - Hash Toolkitにて逆変換すると、password1234が返ってくる。従って、Usernameの欄にはadminを、Passwordの欄にはpassword1234を入力すればよい。

actf{md5_hash_browns_and_pasta_sauce}

・MadLibs

あるWebサービスに遷移する。いろいろやっていると、なにやらソースコードを手に入れられる。


from flask import Flask, render_template, render_template_string, send_from_directory, request
from jinja2 import Environment, FileSystemLoader
from time import gmtime, strftime
template_dir = './templates'
env = Environment(loader=FileSystemLoader(template_dir))


madlib_names = ["The Tale of a Person","A Random Story"]
story_fields = {
    "The Tale of a Person":['Author Name','Adjective','Noun','Verb'],
    "A Random Story":['Author Name','Adjective','Noun','Any first name','Verb']
    }

app = Flask(__name__)
app.secret_key = open("flag.txt").read()

@app.route("/",methods=["GET"])
def home():
    return render_template("home.html",libs=madlib_names)
    
@app.route("/form/",methods=["GET"])
def madlib(templatename):
    global madlib_names
    if templatename in madlib_names:  
        return render_template("home.html",libs=madlib_names,title=templatename,fields=story_fields[templatename])
    else:
        error_message = 'The MadLib with title "' + templatename + '" could not be found.'
        return render_template("home.html",libs=madlib_names,message=error_message)

@app.route("/result/",methods=["POST"])
def output(templatename):
    
    if templatename not in madlib_names:    
        return "Template not found."
    
    inpValues = []
    for i in range(len(story_fields[templatename])):
        if not request.form[str(i+1)]: 
            return "All form fields must be filled"
        else:
            inpValues.append(request.form[str(i+1)][:24])
        
    authorName = inpValues.pop(0)[:12]
    try:
        comment = render_template_string('''This MadLib with title %s was created by %s at %s''' % (templatename, authorName, strftime("%Y-%m-%d %H:%M:%S", gmtime())))
    except:
        comment = "Error generating comment."
    return render_template("_".join(templatename.lower().split())+".html",libtitle=templatename,footer=comment, libentries=inpValues)
    

@app.route("/get-source", methods=["GET","POST"])
def source():
    return send_from_directory('./','app.py')

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

 Flaskとは、Pythonで使うことのできる軽量のWebフレームワークらしい。まず目に入るのは、appのsecret_keyという変数?にフラグが入れられているということ。ただ何をすればいいのかよくわからないので調べていたところ、Server-Side-Templating-Injectionなるものを知った。Flaskを使う際、テンプレートエンジンにjinja2というものを使うが、それは{{と}}で囲まれたコードを実行してしまうらしい。たとえば、{{7 * 7}}を表示させようとすると、単に49が表示される。また、app.secret_key=(フラグ文書)となっていたが、これはconfigという辞書形式のグローバル変数に保存されているらしい。そこで、とりあえず入力フォーム全てに{{config}}を入力すると、下図のようになった。なぜauthorNameにだけconfigが代入されたのかわからないし、もともと問題は解けたけれど、全体的に理解できてない。

f:id:verliezer93764:20180323231614j:plain

actf{wow_ur_a_jinja_ninja}

・File Storer

 何にも手掛かりがなかったので、ヒントを見ると"Can't solve it? Git gud."といわれた。Git gudというのは上手くなれみたいな意味らしいが、Gitというワードが出て何かあるのかなと思っていろいろ調べると、/.gitというディレクトリがあるようだ。

---RE---

IDAのFree版を使った。

・Rev1

32bitELFが渡される。これをIDAに読み込ませ、下図の分岐に注目する。見にくくなってしまったが、画面の一番上でebp+var_54というアドレスからs3cret_pa55wordという文字列を代入している。また、fgetsにより、ebp+var_4Cというアドレスから入力値を代入していく。

f:id:verliezer93764:20180324100021p:plain

そして、図の選択した部分で、eaxすなわちebp+var_4Cの中身の値(入力値)とebp+var_54の中身の値(s3cret_pa55word)とをstrcmpにより比較し、等しければ右側に、等しくなければ左に分岐する。もちろん、等しい場合、つまりs3cret_pa55wordを入力すればフラグが表示される。

・Rev2

32bitELFファイルが渡される。この問題は2段階で構成されている。まずはLevel1。

f:id:verliezer93764:20180324101305p:plain

分岐前のcmp命令により、eaxの値と11D7(16)すなわち4567(10)の値が等しければよいので、まずは4567を入力する。

次は、Level2。今回は数値を二つ入れる。

f:id:verliezer93764:20180324101852p:plain

f:id:verliezer93764:20180324101911p:plain

2枚目の最後の分岐が右側であればフラグが表示される。

最初の入力をA、次の入力をBとするならば、次の条件を満たさなければならない。

1.B<=99

2.9<B

3.A<=99

4.9<A

5.A<=B

6.B*A=0x6D67=3431(10)

これらを満たすのは、A=47、B=73である。よって、"47 73"と入力すればよい。

・Rev3

 32bitELFファイルが渡される。今回は、コマンドライン引数を1つ指定しなければならない。IDAに読み込ませると、"egzloxi|ixw]dkSe]dzSzccShejSi^3q"という文字列が気になったので、コマンドライン引数に指定するも、当然うまくいかない。

f:id:verliezer93764:20180324104304j:plain

ltraceしてみると、入力値がどうやら改造されているらしい。

abc -> ehg

abcdefghijklmn -> ehgjilk^]`_bad

f:id:verliezer93764:20180324104909p:plain

しかしながら、改造前の文字と改造後の文字は一対一対応をしているらしかったので、変換テーブルを作ってみると、"egzloxi|ixw]dkSe]dzSzccShejSi^3q"はフラグ文字列を変換したものだということが分かった。

f:id:verliezer93764:20180324105312p:plain

actf{reversing_aint_too_bad_eh?}

 

---BINARY---

まさか今回pwn系の問題に挑戦するとは思わなかった(もっと先になるかと思っていた)。

・Accumulator

渡されたC言語ソースコードは以下の通り。


#include <stdlib.h>
#include <stdio.h>

int main(){

	int accumulator = 0;
	int n;
	while (accumulator >= 0){
		printf("The accumulator currently has a value of %d.\n",accumulator);
		printf("Please enter a positive integer to add: ");

		if (scanf("%d",&n) != 1){
			printf("Error reading integer.\n");
		} else {
			if (n < 0){
				printf("You can't enter negatives!\n");
			} else {
				accumulator += n;
			}
		}
	}
	gid_t gid = getegid();
	setresgid(gid,gid,gid);
	
	printf("The accumulator has a value of %d. You win!\n", accumulator);
	system("/bin/cat flag");

}

int型変数accumulatorが0以上である間、フラグは表示されないが、負数を加えることもできない。しかし2の補数表現を使っているので、accumulatorを数値的に2147483648以上4294967293以下にしてしまえば、accumulatorは負数扱いされる。ということで、足す数も負数扱いされないように、例えばまず10を足してから2147483640を足してやればwhileループを抜けてフラグが表示される。

 

Cookie jar

渡されたC言語ソースコードは以下の通り。


#include <stdio.h>
#include <stdlib.h>

#define FLAG "----------REDACTED----------"

int main(int argc, char **argv){
  
	gid_t gid = getegid();
	setresgid(gid, gid, gid);

	int numCookies = 0;

	char buffer[64];

	puts("Welcome to the Cookie Jar program!\n");
	puts("In order to get the flag, you will need to have 100 cookies!\n");
	puts("So, how many cookies are there in the cookie jar: ");
	fflush(stdout);
	gets(buffer);

	if (numCookies >= 100){
		printf("Congrats, you have %d cookies!\n", numCookies);
		printf("Here's your flag: %s\n", FLAG);
	} else {
		printf("Sorry, you only had %d cookies, try again!\n",numCookies);
	}
		
	return 0;
}

普通にやれば変数numCookiesの値を変えられないのでフラグは見られない。しかし基本的なバッファオーバーフロー脆弱性がある。char型配列bufferにはchar型64個ぶんの領域しか確保されていないので、それ以上書き込むとnumCookiesの領域まで踏み込んでしまい、numCookiesの値が書き換えられてしまう。例えば次のように入力すれば、フラグを手に入れることができる。"z"はASCII Codeで122番目なので、うまくnumCookiesの値を書き換えることができている。

f:id:verliezer93764:20180324113054p:plain

 

・Number Guess

渡されたC言語ソースコードは以下の通り。


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>


char *flag = "REDACTED";
char buf[50];

int main(int argc, char **argv) {

	
	puts("Welcome to the number guessing game!");
	puts("Before we begin, please enter your name (40 chars max): ");
	fflush(stdout);
	fgets(buf, 40, stdin);
	buf[strlen(buf)-1] = '\0';
		
	strcat(buf, "'s guess: ");	
	puts("I'm thinking of two random numbers (0 to 1000000), can you tell me their sum?");
	
	srand(time(NULL));
	int rand1 = rand() % 1000000;
	int rand2 = rand() % 1000000;

	printf(buf);
	fflush(stdout);
	int guess;
	char num[8];
	fgets(num,8,stdin);
	sscanf(num,"%d",&guess);

	if (guess == rand1+rand2){
		printf("Congrats, here's a flag: %s\n", flag);
	} else {
		printf("Sorry, the answer was %d. Try again :(\n", rand1+rand2); 
	}
	fflush(stdout);
	return 0;
}

疑似乱数を使っているのが、シードを書き換えるのは無理そう。しかし簡単なフォーマットストリング攻撃を使うことができる。標準入力で"%x %x %x %x"などと入れると、各変数のアドレスが表示されてしまう。とりあえずrand1とrand2の値を知りたいので"%d %d %d %d %d %d %d %d %d %d"と入力すると、

f:id:verliezer93764:20180324114949p:plain

答えが3つめの値と9つめの値の和になっていることがわかる。よって、先ほどの"%d %d %d ..."をyour nameとして入力し、出力の3つめの値と9つめの値の和をとればフラグを手に入れることができる。

 

・Rop to the Top

渡されたC言語ソースコードは以下の通り。


#include <stdlib.h>
#include <string.h>
#include <stdio.h>

void the_top(){

	system("/bin/cat flag");

}

void fun_copy(char *input){

	char destination[32];
	strcpy(destination, input);
	puts("Done!");
}

int main (int argc, char **argv){
	gid_t gid = getegid();
	setresgid(gid,gid,gid);

	if (argc == 2){
		puts("Now copying input...");
		fun_copy(argv[1]);
	} else {
		puts("Usage: ./rop_to_the_top32 ");
	}

	return 0;
}

the_topという関数を実行してくれればフラグを見せてくれるが、実行してくれない。ROP攻撃というものがある。サブルーチンに入ったとき、終了時に呼び出し元のアドレスへ制御が戻るようにするためにリターンアドレスが設定されるが、それをバッファオーバーフローによって書き換えることでサブルーチン終了時に自由な場所へ制御を移してやろうという攻撃である。ためしに、gdb上でコマンドライン引数をAAAAAAAA...と設定し、プログラムを走らせてやると次のようになる。

f:id:verliezer93764:20180324120030p:plain

Segmentation fault.や0x41414141 in ?? () と表示されている。これは、プログラム上で存在しないアドレスに移動しようとしたためで、i r コマンドでレジスタの内容を確認すると、eip、すなわち次実行される命令のアドレスが0x41414141に設定されてしまっている。

さて、このリターンアドレスをthe_top関数の先頭アドレスに書き換えたい。IDAで調べてみると、これが0x080486dbであることがわかる。また、AAAAA...のどの位置で書き換えが行われているのか調べるためにコマンドライン引数をAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUに設定してみると、リターンアドレスは0x51414150となるが、'P'=0x50、'Q'=0x51なので、'PAAQ'の部分でリターンアドレスの書き換えが起こせることがわかる。

f:id:verliezer93764:20180324121501p:plain

よって、リトルエンディアンに気を付けて、引数にAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAA\xdb\x86\x04\x08を指定すると、the_top関数が実行される。しかし、gdb上では権限の問題でフラグを見ることができなかったので、次のようにgdb外で通常の状態で実行した。(どうやって16進の状態で引数を渡せるのか知らなかったので悪戦苦闘した)

f:id:verliezer93764:20180324122149p:plain

actf{strut_your_stuff}

 

読んでくれてありがとう!

NeverLAN CTF 2018 write-up (18/02/28更新)

23日から26日にかけて行われたNeverLAN CTFというオンラインCTFに参加しました。

neverlanctf.com

僕は去年12月にCTFの存在と、その楽しさを知り、CpawCTF、ksnctfといった常設のCTFに挑戦してきましたが、オンラインCTFとしてはこのNeverLAN CTFが初めての経験です。情報を学び始めたのは去年からで、主に基本情報処理技術者試験の勉強をしていたので得点は約10000pt満点中2841pt、順位はNon-Studentの参加者中で80位、というまだまだこれからな結果でした。しかしながらたくさん知ったことがありとても熱中して楽しくできたので、備忘録的にwrite-upを載せておきます。

高得点問題はほぼ解けていないので期待はしないでください(´;ω;`)

ヒントが解放されているので、でき次第更新していきます。

---Cryptography---

・I have a message for you

問題文には二進数が書かれている。これをASCIIに変換すると、

 Tmh5b2IgdmZzIGEgY2N5IGR2IHZqaGV6bXYgYnltdyBkZWNyIHhmZ3YgaCBkZmhwcmdjIHZvZXF0aiBkb21tIGdjIGhrcnNkZHcgcnIgemJ1IGdoa3FiIHB6IG5jbHhxZHd0cHVxcg==
が得られる。さらにこれをbase64デコードすると、
Nhyob vfs a ccy dv vjhezmv bymw decr xfgv h dfhprgc voeqtj domm gc hkrsddw rr zbu ghkqb pz nclxqdwtpuqr
となる。スキュタレー暗号かなと思いpythonでこの文字列を任意の太さの棒に巻き付けたときに読める文字列を出力するプログラムを書いてみたが、特に知っている単語はみられなかった。。
 
・Picture Words
換字式暗号。与えられる画像にはいろんな記号が記されているが、同じ記号に特定のアルファベットを割り当てて、できた文字列をhttps://quipqiup.com/に入力すると解読してくれる。
 
---Scripting---

・basic math

・more basic math

・even more basic math with some junk

下のpythonでなんとかなる。numbers.txtは数が並んでいるページの内容を写したもの。ただし三問目は、テキストエディタであらかじめ空白やコンマを取り外す。途中に数字ではない偽の"101"があるので注意。

sum=0
for line in open("numbers.txt","r"):
    sum+=int(line)
print(sum)

 

 ---Reversing---

※まだアセンブリ言語を目で追う程度しかできないので、この分野の更新はないです。

・commitment issues

ダウンロードしたものをバイナリエディタで開くともろにflagがある。stringsコマンドやcatコマンドでも見れるはず。CryptoでいうROT13みたいな典型的な入門問題?

 

---Interweb---

Ajax_not_sorp

Ajaxについては全く知らなかったが、「ソースを表示」させると、おそらくuser欄に入力された場合の処理を書いてある部分、26行目に

$.ajax('webhooks/get_username.php',{
とある。どうやらここからユーザ名をとって照合しているようなので、URLに/webhooks/get_username.phpを加えると、移動先のページにはMrCleanと書かれている。
そしておそらくuser欄に入力された場合の処理を書いてある部分、43行目に
$.ajax('webhooks/get_pass.php?username='+$('#name').val(),{
とある。先に得た"MrClean"も使いURLに/webhooks/get_pass.php?username=MrCleanを加えると、移動先のページにフラグが書かれている。
 
・the_red_or_blue_pill
移動先のページのURLにパラメータredまたはblueを付加したそれぞれの場合にのみページ上部に文章が表示される。よくわからなかったがページ内の文章の文意から「赤と青を一緒にやるな!!」というのが読み取れたので、パラメータをred && blueにすると、blueの場合の文章が表示された。そこでいろいろやっていたらflagが出てきた。今やってみるとなぜか出てこないのでもう少し研究します…
 
ajax_not_borax
MD5がなんとかと。先ほどのAjax_not_sorpと似ているが、ユーザとパスワードの照合部分がそれぞれソース上では

// For element with id='name', when a key is pressed run this function
      $('#name').on('keypress',function(){
        // get the value that is in element with id='name'
        var that = $('#name');
        $.ajax('webhooks/get_username.php?username='+that.val(),{
        }).done(function(data){ // once the request has been completed, run this function
            data = data.replace(/(\r\n|\n|\r)/gm,""); // remove newlines from returned data
            if(data==MD5(that.val())){ // see if the data matches what the user typed in
              that.css('border', '1px solid green'); // if it matches turn the border green
              $('#output').html('Username is correct'); // state that the user was correct
            }else{ // if the user typed in something incorrect
              that.css('border', ''); // set input box border to default color
              $('#output').html('Username is incorrect'); // say the user was incorrect
            }
          }
        );
      });
      // dito ^ but for the password input now
      $('#pass').on('keypress', function(){
        var that = $('#pass');
        $.ajax('webhooks/get_pass.php?username='+$('#name').val(),{
        }).done(function(data){
            data = data.replace(/(\r\n|\n|\r)/gm,""); // remove newlines from data
            if(MD5(data)==MD5(that.val())){
              that.css('border', '1px solid green');
              $('#output').html(data);
            }else{
              that.css('border', '');
              $('#output').html('Password is incorrect');
            }
          }
        );
      });

詳しく見ると、

if(data==MD5(that.val())){ // see if the data matches what the user typed in
if(MD5(data)==MD5(that.val())){
の行で照合している。そこで、まずuserを調べるために「検証」(ChromeではDev Tools)を開き、上の囲いの行にブレークポイントをおく。すると停止時点でdataの値にはc5644ca91d1307779ed493c4dedfdcb7という値が入っている。これはMD5ハッシュ値なのでMD5変換(MD5),MD5逆変換(MD5Reverse)にて逆変換を試みるとtideadeという結果が返ってくる。これがuserフォームに入力すべきusernameである。こんどはパスワードについて調べる。同様に下の囲いの行にブレークポイントを置いてpassword欄に文字を打つと、ブレークポイントで停止した時点でdataの値にはZmxhZ3tzZDkwSjBkbkxLSjFsczlISmVkfQ==が入っている。base64エンコードされているようなのでデコードしてみるとflagが出てくる。
 
・Das_blog
 ログインページのソース上部に次のコメントがある。このようにテストアカウントのメモをうっかり残してしまうことはときどきあるらしい。
<!-- Development test account: user: JohnsTestUser, pass: AT3stAccountForT3sting -->
 これでログインをすると、You are now logged in as JohnsTestUser with permissions userと表示される。
ここで、いまURLに/login.phpとあるのでそれを消去してログインページからトップページに戻ると、当初とは表示が異なっている。しかしながらJohnsTestUserは"user"権限であり、「特殊な権限」がないと投稿された内容を見ることができないらしい。そこで、cookieを確認すると、最後のほうにpermissions=userとある。それをBurpSuiteなどでpermissions=adminに書き換えてやって更新すると、admin権限のユーザとみなされ、flagが表示される。
 
 ・tik-tik-boom
Purvestaさんの地域の時間が表示されている。文章によると、23:59に何かが起こるらしい。23分59秒きっかりに何かするのは無謀なので、23時59分だろう。ソースを見ると、
<span hidden>username and password did not match: admin hahahaN0one1s3verGett1ngTh1sp@ssw0rd</span>
というspanがある。そしてusernameとpasswordという2つのcookieがある。おそらく23時59分にそのcookieをusername=admin,password=hahahaN0one1s3verGett1ngTh1sp@ssw0rdに書き換えてやって更新すると何かが起こる(未検証)。正解者が解いた時間もその23時59分に偏っていたのであっているはず。
(追記)何も起こりませんでした。。
だが、どうやら時間を書き換えて正解した強者がいるらしい。それができるのがプロだよなあ。
 
---Network---
・Fuzzy Packets
与えられたpcapngファイルをWiresharkで開くと、ICMPがずら~っと並んでいる。まずすべてのパケットのペイロード部分にはThis is not the flag you're looking forとあるので探索対象から外す。次に、これは2つのipアドレス間の通信なのでたとえばsourceを172.26.13.112、destinationを192.241.233.138でフィルタリングし方向を固定してみる。ここでいろんなパケットを見ると、値が変化しているのがチェックサム、そしてcodeの部分だけである。そしてそのcodeの部分は0,1,...と不規則に変化している。「この0と1を並べていくとasciiコードが出てくるのでは」と推理する。Linuxを起動し、以下のようにScapyで解析を試みる。使ったのはkali Linux 2018.1。

f:id:verliezer93764:20180228101451p:plain

ASCIIコードならば8bitのうち先頭bitが0なのであっていそう。このASCIIコードを文字に変換するとflagが手に入る。

 

---Passwords---

・Encoding != Hashing

与えられたpcapファイルをWiresharkで開くとたくさんのパケットが並んでおりやりづらいので、とりあえずプロトコル階層を見る。そこで極めて少ない数の通信しかない種類を確認すると、IPv6でのUDPIPv4でのSSDPDNS、HTTPなどがみられるが、まずはHTTPが怪しいとみる。HTTPでフィルタリングして少し探すと10464番のHTTPヘッダで認証している様子が見られる。詳しく見るとこれはBASIC認証で、認証の際入力したidやパスワードを暗号化せずに平文のままネットワークを通過させてしまう危険な認証である。そこでflagが得られる。

 

・Zip Attack

パスワードで暗号化されたZipファイルと、それに含まれているという1つのjpegファイルが渡される。

既知平文攻撃というものがある。これはある暗号化Zipファイルに入っているあるファイルAについて、それと同一の暗号化されていないファイルA(インターネット上に公開されているものも含む:以下「既知ファイル」)が存在すればその暗号化Zipファイルを開くことができるという攻撃である。

pkcrackは、それを行うツールである。使用方法は以下の通り。

./pkcrack -C [暗号化されたZipファイル] -c [暗号化されたZipファイル内にある既知ファイル] -P [(後述)] -p [既知ファイル] -d [出力zipファイル]
なお、ディレクトリ階層に注意。

f:id:verliezer93764:20180228110542p:plain

さて、上記の公式に則ってやってみると、エラーが出てきてしまった。サイズは同じなのに…?

調べると、「問題のzipファイルを上げた出題者のOSの圧縮方式と自分のOSの圧縮方式が同じでないといけない」らしい。ためしに問題Zipファイルと、自分で画像を圧縮して作ったZipファイルについて、zipinfoしてみた。

f:id:verliezer93764:20180228111210p:plain

defXとdefNで異なっている。これらが何なのかはもう少し調べたいが、これが原因だろう。

また、-Pオプションで、既知ファイルをZip化した自作のZipファイルを指定すると、これが解決できる、というような記事をみた。そして、Zipファイルを作る際に「圧縮レベル」を指定できるようだ。0が圧縮しないでただzip化するだけ、9が最高圧縮率だという。

 その圧縮レベルを調整すれば、問題ファイルと同じ圧縮方式ができるかもしれない、と思ってレベル別に10個のzipファイルを作った。すると、レベル9で作った自作zipファイルについて、上記の公式に-Pオプション指定したとき、うまくいった。

f:id:verliezer93764:20180228110652p:plain

 成功すると、パスワードなしの状態で、-dで指定したdecrypted.zipが生成され、そこにencrypted.zipの内容がコピーされている。flag.txtを見て終了。個人的には一番きつかった。

 

・The WIFI Network

与えられたpcapファイルは、WPA2-PSKの4way-handshakeの様子らしい。

ヒントを見てしまったが、hashcatというツールを使うと、このハンドシェイクの通信と辞書ファイルによってパスワードを割り出してしまうという。

まず、https://hashcat.net/cap2hccapx/でこのpcapファイルをhccapxという独自の形式に変換してもらう。

つぎに、kali linuxにはデフォルトでhashcatが入っていたので、次のようなコマンドを打つ。

hashcat -a 0 -m 2500 neverlan.hccapx [辞書ファイル]

 ただこの「辞書ファイル」が厄介で、/var/share/dictにあったデフォルトの辞書ファイルやJohnTheRipperで使ったそこそこ大きいはずのOpenwallのフリー版辞書ファイルでも結果は出ず、いろんなデモの動画で使われていたrockyou.txtという辞書ファイルでやっとパスワードが出た。時間がかかるので、解析中はほかの問題を解くとよいかも。

 

---Trivia---

大文字にする、略称などいろんな答えが期待できたのでおもったよりきつかった。How far can you go?というどこかでみたような問題がわからなかった。

 

---Blast from the Past---

唯一全部解けた問題。

Cookie_monster

移動先のページにはHe's my favorite Red guyと書かれており、Red_Guy's_nameというcookieにNameGoesHereという値が入っている。そこをElmoに書き換えるとフラグをゲット。

 

・Siths use Ubuntu (Part 1 of 3)

Ubuntuに侵入されたらしい。問題文には"You've got to figure out how they keep getting in even though we've changed the password."とある。まずは与えられたovaファイルをダウンロードしてVirtualBoxなどから開く。Things-I-should-doというテキストファイルがあるが、関係ないファイル(出題者がスターウォーズ好きなのが次の問題の答えと合わせてわかる)。

part2でもpart3でもログファイルを使うが、実際侵入されるときには改竄対象にされるらしいので注意。

 lastコマンドを使うと、最近のログイン履歴を見ることができる。

f:id:verliezer93764:20180228122551p:plain

このうち

kyrolen  pts/18  172.16.164.128 Sta Feb 25 12:52 - 17:24 (04:31)
が外部(172.16.164.128)からの不正侵入。
この時間に何があったかを/var/log配下のauth.log.1で確認すると、確かに12時52分に"Accepted password for kyrolen from 172.16.164.128"とある。その後、12:52:45にcatコマンドで/etc/crontabを覗いているのが気になる。cronは定められたコマンドを定期的に実行し、どの日時にどのcron(ファイル)を実行するかを記入するのがcrontabである。定期的に特定のプログラムを実行するように侵入者が設定しようとしているかもしれない。

f:id:verliezer93764:20180228125433p:plain

 crontabには、以下の内容が書いてあった。一番下のファイルは、5分毎に実行するようになっており、最も怪しい。

f:id:verliezer93764:20180228125803p:plain

そこで、/etc/init.d/rebelsを覗くとflagが書かれている。 

f:id:verliezer93764:20180228125816p:plain

シェルスクリプトについては知らないので、ここの内容はもう少し調べてみます。

nc.traditionalと-p 443から、何かを送信するのかなとは感じる。

 

・Siths use Ubuntu (Part 2 of 3)

かつてのパスワードを求めろ、ということでJohnTheRipperを使う。shadowファイルには提示されている/etc/shadow.backupを使う。

参考:/etc/passwdのクラックツール『John The Ripper』を使ってみた | 俺的備忘録 〜なんかいろいろ〜

 

・Siths use Ubuntu (Part 3 of 3)

問題文には"You've got to figure out how they broke in."つまりはどうやって侵入したかを問うている。認証関係のログファイルauth.log.1を再び見てみると、先ほどの12時52分の"Accepted password for kyrolen from 172.16.164.128"の上には大量のログイン失敗のログが並んでいる。そのなかにflagが紛れ込んでいた。ひょっとしたら1問目を解いているときに見つかるかもしれない。

f:id:verliezer93764:20180228141420p:plain