ふるつき

v(*'='*)v かに

高専セキュリティコンテストに参加した

 高専セキュリティコンテストに参加しました。insecureというチームで、thrust2799、kyumina、mi_24vと一緒に参加して、3位でした。楽しいコンテストでした。

f:id:Furutsuki:20161127194028j:plain

f:id:Furutsuki:20161206121411p:plain

Slack

 参加者を集めるSlackが開きました。これが今回の最大の成功だと思っていて、Slackでは他高専高専生や運営と楽しく盛り上がることができました。

 Slackではemojiやreactionの追加や自由な書き込みが許可されて、 自己紹介や雑談に花が咲きました。ここで、カジュアルに書き込みができる雰囲気が作られたのはとても良いと思いました。

 Slackではもちろん運営からの連絡もあって、競技環境の事前公開(本番環境と近い部分がある)が公開されて接続テストをしたり、問題の予告がでたりしました。

当日まで

 接続テストがありました。オンライン参加のチームにはとても重要なことでした。接続は事前に申告した高専のネットワークからしかできず、テスト前とかあってぎりぎりになりましたが、ちゃんと接続が確認できました。

 さらに、その環境には問題の予告が置いてありました。今回の問題はIoT関連ということで、IoTカー、というものに焦点があたっていました。サーバにはiotcarというシェルスクリプト)と、iotcar.pyというpythonスクリプトが置いてあり、どちらも所有者がrootになっており、読み書き実行の権限は基本的にありませんでしたが、iotcarは、実行可能フラグとスティッキービットsetuid ビットが立っていました(実行時に、所有者の権限で実行される)。

iotcar.pyはIoTカーを模倣したスクリプトで、UDPで適当なパケットを送りつけると、それにあわせた動作をする、かわりに /etc/iotcar 以下に結果を吐き出します。事前環境では、「アクセルの踏み込み量」のみを操作可能でした。

./iotcar でパケットを待ち受けた状態で、 0xf0, 0x40, 0x01, 0x10, 0x04, 0xff, 0xff, 0x01, 0x02, 0xf7 のようなパケットを送りつけると /etc/iotcar/motor に 0xffffが書き込まれることを確認しました(パケット中の0xff, 0xffを書き換えるとmotorに書き込まれる値も変わった)。

これと、IoTカーの仕様のようなものが公開されていたので、こんなスクリプトを組んで準備していました。

import socket
import array
import argparse
import sys

command_help = """
command list

sportsmode   sports mode         0-255 center is 128
handle       handle degree       0-255 center is 128
brake        brake               0-255
accel        accel               0-255
charge       battery charge      0: charge mode, 1: run mode
hybrid       energy mode         0: hybrid, 1: electron, 2: gasoline
gear         gear                0|N: neutral, 1-5: n speed 254|R: back, 255|P: parking
engine       power on off        0|off: power off, 1|on: power on

window                           0|close: close, 1|open: open
windowlock                       0|off: free, 1|on: locked
temp         aircon temperature  0-45
airmode      aircon run mode     0|off: off, 1: cooler, 2: heater, 3: wind only
loof         ????                0|off: off, 1|on: on
kumoridome                       0|off: off, 1|on: on
wiper                            0|off: off, 1|on: on
doorlock                         0|off: free, 1|on: locked
door         open/close          0|close: close, 1|open: open
hazard       hazard lamp         0|off: off, 1|on: on
light        small light         0|off: off, 1|on: on
navi                             0|off: off, 1|on: on
audio                            0|off: off, 1|on: on
highbeam     highbeam light      0|off: off, 1|on: on
craction     1sec craction       0: dummy value
ABS          smooth brake aid    0|off: off, 1|on: on
4WD                              0|off: off, 1: auto, 2|on: on
"""

commandlist = {
        'sportsmode': [0x10, 1],
        'handle':     [0x10, 2],
        'brake':      [0x10, 3],
        'accel':      [0x10, 4],
        'charge':     [0x10, 5],
        'hybrid':     [0x10, 6],
        'gear':       [0x10, 7],
        'engine':     [0x10, 8],

        'window':     [0x20, 1],
        'windowlock': [0x20, 2],
        'temp':       [0x20, 3],
        'airmode':    [0x20, 4],
        'loof':       [0x20, 5],
        'kumoridome': [0x20, 6],
        'wiper':      [0x20, 7],
        'doorlock':   [0x20, 8],
        'door':       [0x20, 9],
        'hazard':     [0x20, 10],
        'light':      [0x20, 11],
        'navi':       [0x20, 12],
        'audio':      [0x20, 13],
        'highbeam':   [0x20, 14],
        'headlamp':   [0x20, 15],
        'cracshon':   [0x20, 16],
        'ABS':        [0x20, 17],
        '4WD':        [0x20, 18],
}

def send(address, port, command, value, key, v2=0):
        server_address = (address, port)
        max_size = 10

        key1 = (key & 0xff00) >> 8
        key2 = key & 0x00ff

        subid, cmdid = commandlist[command]

        if value == "close" or value == "off":
                value = 0
        elif value == "open" or value == "on":
                value = 1
                if command == '4WD':
                        value = 2
        else:
                value = int(value)

        print("{} with ({}, {})".format(command, value, v2))

        client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        client.sendto(array.array("B", [0xf0, 0x40, 0x01, subid, cmdid, value, v2, key1, key2, 0xf7]), server_address)
        client.close()

if __name__ == '__main__':
        parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)

        parser.add_argument('-a', '--addr', default='localhost', type=str)
        parser.add_argument('-p', '--port', default=52782, type=int)
        parser.add_argument('command', type=str, help=command_help)
        parser.add_argument('value')
        parser.add_argument('-v2', default=0)
        parser.add_argument('key', type=str, help="0xkey1key2")

        args = vars(parser.parse_args())


        if not args['command'] in commandlist:
                print(helpmsg)
                sys.exit()

        try:
                args['key'] = int(args['key'], 16)
        except:
                print("key is not hex value")
                sys.exit()
        send(args['addr'], args['port'], args['command'], args['value'], args['key'], args['v2'])

それから、前日には競技説明がありました。これの告知が無くて(サイトには確かにあるって書いてあったので私達がわるいんだけど)、うちのチームはみんなして競技説明をすっぽかしました。競技説明があとからでも見られて助かりました。

当日

テスト前というのに学校に出ていって、開会式を待ちました。開会式では、ちょっとヒントっぽい話(実際に問題を解くときに活躍することはなかった)があったり、問題が予定より大幅に減った(ジャンルが6 -> 3になって、「環境設定」や「暗号通信」がなくなった)ことを告知されました。

 さらに、今回の問題形式が 問題文なし であることや、他チーム、スコアサーバへの攻撃の禁止を言い渡されました。

競技形式

  各チームは、自らに割り当てられたサーバの「脆弱性を修正」します。5分に1度チェックスクリプトが走って(こういうの何ていうんだっけ)、脆弱性の修正が確認されたら得点が入ります。得点は脆弱性ごとに100, 200, 300の点数がつけられており、点数を得られるのは一度だけです(継続していたら点数が増えるとかはない)。

 脆弱性にはジャンルが設けられており、 「安全でないWebインタフェース」「不十分な認証/認可/バックドア」「危険なネットワークサービス」のそれぞれに100, 200, 300点問題が1つずつありました。合計1800点です。

競技

とりあえずssh接続をしました。ホームディレクトリにはscuserが所有者となった iotcar.py だけが置かれていました。

iotcar.pyの修正

iotcarを操作するpacketが運営から飛んできそうだなーとおもっててそれを捕まえないことには……と思ってたので、iotcarを実行しようとしました。 python iotcar.py な感じです。するとpythonシンタックスエラーを吐いたので修正をはじめました。このときに、 debug option とかいうヤバそうな文字列を見かけたので、関連箇所を全部コメントアウトしておきました。

iotcar.pyが修正される

参加者から質問が飛んで、修正版 iotcar.py が配布されました(ここで参加者が質問できる雰囲気良い)。私の努力は無に帰して、 pyhon iotcar.py が正常に起動することを確認した後は、 nohup python iotcar.py & しておきました。

 そして、このまましばらく待つことにしました。ちょっと暇だったので netstat -antu とかしました。ここで telnetftp が生きてることがわかりました。 thrustがこのあたりの処理を買って出てくれたので任せました。

backdoorユーザを発見する

kyuminaの画面を見たのか何だったか、 backdoor とかいうユーザが存在することを知りました。 /home/backdoor を訪問すると、READMEと、 PASSWORD_is_backdoor みたいなファイルがありました。

とりあえず README を読むと 「このユーザは開発用です」 みたいなことが書いてありました。とりあえずこのユーザを無効化することにしました。 telnet localhost して backdoorにbackdoorで入ると入れたので、 passwd して パスワードを _backdoor_ にかえておきました。これで何かしらの点数が入ったはずです。

Webインタフェースを見る

thrustがapacheが動いてるらしいことを教えてくれて、みたら「デフォルトのページ」「ファイラでphpinfo吐いてて、/etc/passwd吐いてるファイルであるところのindex_old.php」「adminディレクトリ(認証がかかってる)」などがありました。これらは全部www-dataの持ち物で、エディット出来ませんでした。

とりあえず認証が突破できる気がすると思ったので .htpasswd を落としてきて、ローカルの環境で john .htpasswd したら一瞬で adminのパスワードが password であることがわかりました。

 そういうことでadminとして入ると、「IoTカーのWebコントローラ」みたいなのがありました。「status」「update」「date」「manual」くらいの項目があり、statusは/etc/iotcar以下のファイルを読んで、IoTカーの状態を表示してくれる部分のようでしたが、/etc/iotcar以下には未だ何もなかったので(kyuminaがiotcar.pyまわりをやってくれてたけどなにもわからなかった)、意味なし太郎でした。

updateはなんかヤバそうで、ファイルのuploadフォームがありました。まずこれのソースコードを読むと(読む権限はあった)、「ファイルがアップロードされたらfilesディレクトリに移して、unzipして、出てきたディレクトリに移動し、sh command.sh する」ようなphpがありました。

「あっこれ使える」と思い、 /var/www/html/.htpasswdを .htpasswd_に書き換え、新しい.htpasswdをつくるようなシェルスクリプトを書いて、command.shとして保存し、適当なディレクトリに埋めてzip圧縮したものをuploadしました。

 すると、サーバがInternal ServerErrorを吐きました。みると、 /var/www/htmlに.htpasswdMがありました。Windows環境でつくったシェルスクリプトだったので、改行コードの\rがファイル名として認識されたようです。

.htpasswdMをなんとかしようとしたのですが、頼みの綱のWebインタフェースは500だし、.htpasswd自体への書き込み権限はないしで、積んだと思い、「環境のリセット」をお願いしました。

 環境のリセットは運営が用意した救済策で「どうしても積んだと思ったら申告してください。15分程度で環境をリセットします」ときいていました。

 ここから大変な時間が始まります……

2時間待つ

 15分まつ、とのことでしたが、30分待っても環境は復活せず、ちょっとはやいお昼を食べたり、Twitterをしたりしていました。その間にも、他のチームが続々とリセット申告を行いました。運営が「ちょっと、Azureのフォームが重たいので、時間かかってます」ということには、私たちはガルパン鑑賞会を始めていました。

 結局、2時間まちました。

再開

 競技時間がのこり2時間を切った頃に、やっと復旧して、競技を再開しました。他のチームも結構待たされてて、運営も「root権限でどうにかなりそうな事態ならリセットではなくその旨を申告してみてください」というアナウンスをしていました。

 私はもっかいおんなじことをしてInternal Server Errorを吐かせ、「/var/www/html以下をリセットしてください!」とお願いしました。

 今度は改行コードをちゃんと修正して、adminのパスワードをpasswordからadminpasswordに変えました。多分これでも点が入ったと思います。

date.php

date.phpは「システムの時刻を設定できます」とのことでしたがよくわからなかったのでソースコードを覗きました。すると「POSTにshというパラメータがあったら、 system('date +s "' . $_POST['sh'] . '"')する」という雰囲気のコードが有り、やばそうでした。

とりあえず、これを用いて、 index_old.phpをindex_hoge.phpにしたり(入力に "; mv /var/www/html/index_old.php /var/www/html/index_hoge.php; echo "OK などをあたえる)、date.phpをhogedate.phpにしたりしていましたが、このあたりでは点が入らないようでした。

thrustのお手伝い

thrustくんが、「ftpでログインしたさきに、you_must_delete_thisみたいなファイルがある」ということを教えてくれて、それがdeleteされていなかったので、どうしたもんかなと思っていましたが、そういえばと思い ftp localhost して anonymous ユーザでパスワードなしでログインすると案の定入れたので、 delete それ しました。多分これでも点数がはいったはず。

その後行き詰まって

admin ディレクトリを hoge に変えたり、やばいphpを軒並み名前変更したりしていましたが、得点が入りませんでした。

  index.htmlがデフォルトのままでいろんなパスとかバレてるのが良くないのかな、とか、Options Indexesが良くないのか、とおもい修正もしてみましたが一向に点は入らず……。

おわり

 時間いっぱいおしまいになりました。insecureは900点で 3位でした。1位は 1700点のSIGSEGVで、 2位は 1200点の Harekazeでした。序盤に600点くらいとったあと、300点しか取れなくて、サイバー甲子園でもこんな感じだったぞと思いました。

終わって

 SIGSEGVがなんであんなに点取れてん、という話になって「開始30分でrootがとれたので」と言われて「!?!?!?!?!」となりました。さらに話を聞くと、「$HOMEに root所有者のsticky bitsetuid bitありのiotcarが存在して、こいつは、 0777 root所有の iotcar.py を実行してるだけだから iotcar.pyに/etc/sudoersを編集するコード書けばok」とのことでした。

 このへんから話の雲行きが怪しくなって、というのも「あーなるほど」というチームと「私らのチームそんなiotcarとか言うファイル無いんですけど」というチームが現れました(私らは後者です)。

→ 12/5 に連絡があり、やっぱり環境がちがうケースが存在しました。どうやら二種類の環境が存在していたようです。それぞれで別の部門としてランク分けされました。

scapyで大きなpcapファイルを読む

scapyを使ってpcapファイルから何か操作をする場合にはまず

packets = rdpcap(filename)

とすると思いますが、何万パケットとあるようなpcapファイルを読み始めるとそれだけでファンが猛回転し、時間がかかり、心臓が死にます。

そこで

Reading a huge PCAP or PCAPNG file

に従って

packets = PcapReader(filename)

などとすると、ジェネレータを用いてメモリ節約してくれるのでファンがぐおんぐおん回ったりせず心に安寧が訪れます。

但し、これはパケットを順番に処理していく場合にしか使えないので悪しからず

サイバー甲子園に参加した

まとめ

サイバー甲子園に参加しました

なんか会場入りしたら格好良い本がおいてあって最高でした。

CTFが始まって、私はinsecureという一人チームでした。

解いた問題のWrite-Upは後に述べるとしてCTF中どうだったかと書くと、序盤は自明問題をさくさく解けたので2位か3位で「これいけるじゃんね最高」とかなってましたが、そこから詰まって結局2000ptsとれずにつらい思いをして終わりました。

結果は五位で、優勝したチームと2位のチームと3位のチームと4位のチームに負けました。4位のチームに負けたのと2000ptsボーダを超えられなかったのがとてもつらいです。

でもひとりで頑張ったので一人で頑張ったで賞として京都府警の課長賞(詳細な賞名はわからない)をいただきました。身に余る光栄です。なんかこれは履歴書にも書けるそうなんですが、どのくらいすごいんでしょうね。

f:id:Furutsuki:20161112203018j:plain

それから懇親会に出ました。産技高専の人と喋ったり、沖縄の人と喋ったり、優勝したチームの人と喋ったりしました。懇親会は楽しいのでいいですね。

それから帰りました。有意義な一日でした。

Write Up

自明問題しか解けていない上、自明問題の中にも解けてないものがあったのでつらいです。解説をきいたら、これ三問くらいは解けて当然じゃ~んみたいな感じでした。stegsolveの名前をど忘れしたやつとSESSIONがmd5になってるのに気が付かないやつと、icmpのttlが変わってるのに気がついてないやつ。

それから、binaryが一問も解けてないので弱点が露呈した感じなりました。

Assembler Tanka

かの有名なアセンブラ短歌です。SECCON{57577}と吐くアセンブラ短歌を渡されて、これを実行できるというサイトの存在まで教えられたのでそこに投げるだけです。アセンブラ短歌すげぇ

gokai?

base64 -dを5回やるとSECCON{BASE64}が得られるような文字列が渡されました。

very easy

16進数が幾つかスペース区切りで渡されるので "".join(map(lambda x: chr(int(x, 16)), それ)) みたいにするとフラグになります

decode the flag

暗号化鍵と暗号化済みファイルが渡されるので、 "cat それ | openssl {} -d -k '鍵'".format(暗号化形式) みたいなのを回したら解けます。

gettheflag

なんどかHTTP通信をしているpcapがわたされるので strings それ | grep true みたいな感じにすると縦読みのflagが出ます

megrep

バイナリエディタで開くとSECCON{bsdbanner}というbitmap artが表示されます

x2.txt

ファイルをダウンロードしてきて "".join(map(lambda x: chr(ord(x)/2), open(それ).read()) すると解けます

decode the trapezoid QR code

f:id:Furutsuki:20161112210803p:plain

QRコードGIMPで補正してやると読めるようになります。

sum primes

エラトステネスとかで適当にたくさんの素数を生成してやって sum(primes[12344:31337]) とかすると解けるはずでしたが採点側がみすっていて時間を食いました。エラトステネスに自身がないときはどこかから素数表をもってくるだけです

blacked out PDF

黒塗りのPDFなので読めます

blacked out PDF again

同上ですが、今度はコピープロテクトがかかっています。でも私のViewerではコピープロテクトを無視していたらしく実質 blacked out PDFです

how much a fine?

なんか犯罪行為と犯罪名称を結びつける問題です。一発で解けますが、せいぜい20回かそこらためしたらおわりです。

acronym

googleです。

感想

解説をきいたら3問程度は解ける問題だったのでつらいです。