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}

 

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