/var/log/Sawada.log

SAINO中毒患者の備忘録。

Defense against Game Hack with Ghidra へ参加してきました

はじめに

株式会社NinjastarsさんによるGhidraのハンズオンへ参加してきました.

f:id:takuzoo3868:20190416232413g:plain
https://www.gizmodo.jp/2018/09/tunnel-tokyo-sega-summy-co-working-space.html

品川でのお仕事を終えてから会場へGO.

会場は大崎にあるSEGA SAMMYグループのTUNNEL TOKYO.おしゃれ😳

f:id:takuzoo3868:20190416233242p:plain

目次です.今回は三部構成でした.

Game Securityの課題 by 高橋さん

近年のスマホゲーム・eSPORTsの発展により,市場の成長が大きいゲーム産業.
しかし,対策コストと被害額の相関が特にゲームは見えにくく,チート対策専門のエンジニアを雇うにはそれなりの規模をもった会社でないと厳しいのが現状というお話.ゲームのチート行為と言えば,

  • 内部メモリの書き換えによるアイテム取得・ステータス改ざん
  • 自動Botを活用したアカウントのRMT行為
  • より普及の為に改造ツールの流通

が昨今の問題例として挙げられます.1つ目については小中の頃にGame Shark(PAR)とか流行ったよなぁと思い出しました.

ゲーム業界で想定される最悪のケースとして,チートツールのカジュアル化により若年層のユーザーがチーターへ変化,対策として予定になかったメンテの増加,世界観崩壊によるプレイヤーの減少が流れとして示されていました.チート技術に明るくなくても,チート行為ができてしまうことをカジュアルチートと呼ぶのは初めて知りました.企業としたら開発投資を回収できないまま,折角作ったゲームが倒れてしまうと困るわけです.

だからこそ,ゲームアプリの脆弱性診断が重要になってきます.それに相反するようにGame Securityは 普通のソフトウェアと仕組みが異なるため,

  • 脆弱性調査も専門性が更に高く,中々人材育成の機会に恵まれていない
  • 被害額などリスクが不透明で可視化しにくい
  • ゲームセキュリティはガイドラインが定まっていない

といった課題が生じているとの事でした. 事例にAndroid Republicが出てきたり,可視化ツールPacifistaを開発中と聞いた時は突っ込んだ内容かな...と思いましたが,セールストークが多めで残念. それでも,Game Securityを専門とするNinjastarsさんは尖ってて面白い会社だなと思いました.インターンとかやらないかなぁ...

Traing Ghidraで脆弱性診断 by 齊藤さん

github.com

NSAが公開したGhidraを使ったハンズオン.
eagle0wl氏のcrackme問題集から01と04を扱い,Ghidraの機能を知ってみようという感じでした.最初の注意にもありましたが,著作権利者の許可なくリバースエンジニアリングをする行為は

等々に抵触する恐れがありますので,興味本位で他人のソフトウェアをREしてはいけません.

f:id:takuzoo3868:20190417022502p:plain
お題のcrackme01

f:id:takuzoo3868:20190417020901p:plain
ghidraを使ってCALL GetWindowTextAの該当箇所から,関数のフローを解析

IDA proには劣りますが,アセンブリデコンパイル内容をひと目で確認でき,グラフもtrue/falseがはっきり色分けされているのでわかりやすいと思いました.コードブロックの表示内容を編集できるなど,かゆいところに手が届いている印象を受けました.Ghidraを使った問題の解説については,Ninjastarsさんのブログにもある内容です.自分も大変勉強になりました.ハンズオンでは消化不足になってしまった部分について,読んで内容を吸収しつつ,crackme01~10の解説をブログに書きたいです.

www.ninjastars-net.com

www.ninjastars-net.com

Practice Cheater VS Ninjastars by 猫さん(@DoranekoSystems)

CEDEC CHALLENGE 2017の課題に単身で挑んで準優勝するなどプロの方.
twitterではお知り合いだったのですが,今回はじめてお会いすることができました.

チート行為についていくつかの具体例とその対策に関する講義形式でした.猫さんの作成した,ゆるふわ系(?)じゃんけん対戦ゲームを実例に使い解説がなされました.具体的なチート手法やツールについては,内容が内容なので記載を控えようと思います.

Memory hack

メモリに記録されているスコアを書き換え,ステータスmaxやステージ飛び,ワンパンなど古くから常套手段となっているチート行為です*1.実演では,倒せないはずの大変かわいい宇宙人をメモリ改ざんによりボコボコにしていました.

対策としては,各種パラメータの暗号化復号化を徹底すること.また,開発者以外がデバッグできないようWindowsならWindows APIのIsDebuggerPresent()やCheckRemoteDebuggerPresent()を使うこと.これについては,スマホネイティブデータにおける個人情報漏洩の危険回避から考えても大事なことだと思いました.

Datafile hack

ゲームソフトウェアをデコンパイルされ疑似ソースコードから一連の処理を把握されると,実行ファイルの書き換えられ意図しないゲーム動作を行う場合もあります.実演では,Cthulhuっぽい見た目の相手になっていました.HPデータを暗号化,更にはメモリを改ざんされた場合には検知し異常終了する様になりました.しかし,デコンパイルでゲーム処理を変更してあっさりと勝ちました...神話生物やぞ...

対策としては,とにかく耐タンパー性を上げること.具体的にはパッカーなどを利用して難読化すること.Unityならil2cppでビルドする.ファイルの改ざん検知.不要なシンボル情報を削除し,関数名などは難読化すること.

other

この他にもオンラインゲームの場合にはサーバ側との通信に仕掛けを行うPacket hack等が挙げられます.また実例では,別のアプローチからHP99999でイキっているドラゴンをボコボコにしていました.

おわりに

f:id:takuzoo3868:20190417032322j:plain

感想ですが,ちょっとセールスプッシュが強かったのと,どのセクションも駆け足でスライドが流れるように切り替わってしまったので,勉強としてインプットしたい僕としては少し残念だったかなぁと思いました.それでも,Game Securityとしてチート対策を考えたり,ghidraを使ったリバースエンジニアリングはとても楽しかったです.次回もあればまた勉強しに行きたいと思います.ではでは.

*1:完全オフラインなゲームソフトで個人の範囲で行う分には保証外の自己責任ですが,オンライン対戦などネットワークを通じて他のプレイヤーや運営とつながるゲーム形態では違法行為です.駄目です.

I love SAINO!!!

はじめに

今日,無事卒業式を迎えることが出来た.

入学当時は浪人からの解放と変にコンプレックスを抱え,ぐちゃぐちゃな精神状態だったのは今も覚えている. あれから4年間,私を迎え入れてくれた室蘭工業大学に感謝を込めて,自分の学生生活を振り返ってみる.

水元生活

いちねんめ

沢山の教養科目,のびのびとした時間割,サークル活動...何もかもが新鮮だった.生活面も特に決まった時間に起きる必要はなく,寝たい時は寝れる幸せを噛み締めていた.道内の国立大学の授業を遠隔で受講できる制度を知り,北大の光学講義をとった.レポートは毎週鬼のようにあったけれど,特にしんどいとは感じなかった.同じように,この頃の基礎科目はどれも簡単に感じていたので,試験勉強を張り切った覚えはない.毎日のようにダーツして,アニメ見て,ゲームして...それとなく学生生活を謳歌していた.

秋ごろだったか冬だったかその辺りにネパールカレーのお店SAINOが誕生した.

既に年度末はSAINO中毒になっていたらしい

夏と冬と春の長い休暇も楽しかった.北海道を6日くらいかけて一周したり,ダーツの合宿に行ったり,高校の友人へ会いに大阪・京都を巡ったり,東海道を旅行したり,色々旅をした.

f:id:takuzoo3868:20190325081223j:plain
北海道一周

にねんめ

分属があり希望通り情報系へ進んだ.就職に強いとは言うものの電磁気学が苦手だったので電電には行きたくないと思っていた.かと言って,情報に進んで将来大丈夫なのか不安は大きかった.大学の就職情報は対して当てにならないと思い,少しでも情報系の事を知ろうと6月にオープンソースカンファレンス(OSC)に参加した.OSCに参加したことが自分の進路に大きな影響を与えてくれたと思っている.いまいち知らない用語ばかりで何だこれは...と気付かされ,大学だけじゃなくて自分でもしっかりやらなきゃなと思った.故に少しずつサークル活動からは退いていった.その上でセキュリティ分野に興味が湧き,セキュリティミニキャンプや弘前で行われたCTFビギナーズ,色々な勉強会に顔を出すようになった.馬鹿にならない交通費を支援してくれたのが,道内の技術系コミュニティであるLOCALである.社会人になったら,何らかの形で恩返ししないといけない.

アルバイトは登別温泉に通っていたが,無駄な通勤時間に嫌気がさして,学内の図書館で働くようになった.このおかげで,学内の専門書を読み漁る機会も増えたと思う.Twitterで情報を集めるようになったのもこの時期だと思う.TweetDeckは優秀だ...

今はなきメニューがあったのもこの時期.皆と違い,キャリデザは絶対取るもんかと決めていたので,その時間はいつもSAINOだった.

少し予定のあいてる日は温泉に行ったり,海に行ったり休暇を楽しんだ.車さえあれば,近くに観光スポットの多い室蘭はとても恵まれた環境だと感じた.

さんねんめ

研究室配属にSecHack365やCodeBlueへの参加,enPiT.自分にとって糧となる大切な一年になった.一方で,全国を飛び回る関係で講義の欠席も増え,出席点は下がってしまった.一番ヒヤヒヤしたのが,留年しかねない必修や教職課程に必要な講義だった.何度も担当教官に詫びのメールを送り,ちょっと多めのレポートを書いていた.人前で発表の機会も増えた.異国の地で拙い英語で発表した時が一番緊張した.SecHackの事は別の記事に大体書いている.今年度は先輩も参加して優秀賞に選ばれており,後輩も頑張って参加してほしい.

takuzoo3868.hatenablog.com

takuzoo3868.hatenablog.com

takuzoo3868.hatenablog.com

夏に今の彼女と付き合い始めた.幸せな毎日だったけれど,自分の忙しさからあまり遠くへ遊びに行けず申し訳なかった.離れ離れになる今年の春からは少しでも連絡とって,休暇に遠出できたらいいなと思ってる.

年明けにはLOCALの総大会に参加して同人誌を執筆した.これは次年度の技術書典などで販売した.

よねんめ

研究室生活がスタートした.コアタイムは特に無く,自分で進捗を出していくスタイルだった.先生とテーマ決めはB3のうちにやっていたが,先行研究の教授にコンタクトしたり,机でガリガリ始めたのはちゃんと机が与えられてからだった.研究室は最高のコワーキングスペースであると感じた.自分は暗号と物理光学の融合に近い領域であり,専門分野の学び直しが一番苦労した.基礎となる大学の講義はやはり重要だった.そして見返すことのできるノートも大切だと改めて思った.6月に母校へ教育実習に赴き,7月には他大の院を受験した.

takuzoo3868.hatenablog.com

年度の後半は学会に参加したり,ICPCアジア大会に参戦したりちょこちょこ活動していた.何より研究を最優先で動いていたと思う.指導教官には最後までお世話になった.

おわりに

次に待っている研究生活は,自分の興味で選んだけれど上手くいくかどうかは不安しかない.心が折れそうな時,この4年間を思い出して奮起したい.ありがとう,室工大.

f:id:takuzoo3868:20190325153121j:plain

SAINO

私の血液であり活力の源だった.いつも笑顔で迎えてくれる店員さんと最後に別れの挨拶を出来たので悔いはない.これからも連絡をとりあう仲でいたい.

CUIでお洒落な開発環境を整えよう

はじめに

みなさん年の瀬いかがお過ごしでしょうか.こたつスウィングバイが止まらないさわだです.
工大アドベントカレンダー(一日目)の空枠を拝借して,自分も最近の事を書こうと思います.

adventar.org

今回の記事はこれです.

きっかけ

我が愛機であるThinkpad X220にArch LinuxをホストOSとしてインストールしましたが,chrome君とslack君に加え,Intellijの子供たちを使うとメモリが逼迫するのでCUIで開発環境を整えたいなと思ったのがきっかけです.

整備する

以前はneovim周辺の設定を弄って満足出来ましたが,あらゆる事をCUIで完結させたい欲が高まるにつれ,複数のターミナルを起動するのも億劫になってきました. 更にCUIでも毎日触るのが楽しくなるような見た目が欲しくなりました. となると...自分にとって必要な環境整備は以下の項目が挙げられます.

  • CUIツールの導入

  • ターミナルマルチプレクサの導入(且つ,ばぁぁーーんといい感じに起動したい)

  • 諸々の見た目の整備

では順を追って解説に入ります.

CUIツール

必要なものはすべて偉大なるArch Wikiが教えてくれました.アプリケーション一覧にあるコンソールに様々な紹介があります*1

システム監視として見た目がいい感じかつGo製のgotop,ファイルマネージャとして操作感がvimに似たranger,作業BGMを聴くためにプレイリストが管理できるmps-youtubeIRCクライアントとしてweechatを導入しました.

Archではyay -S <pkg>でこれら全てが楽にインストール出来ます.他OSもドキュメントに従えば楽にツールを導入できます.

gotop

vimに似た操作感でプロセス管理も可能です.詳しくはリポジトリのREADMEを読めばいいと思います.nodejs製のgtopはモニタリングツールとして有名ですが,CUIで使うにはちょっと軽量とは言い難い部分があります.私の環境ではgotopによりメモリ使用量はgtop 85MB から gotop 7.3MBへ節約できました.gotop -c solarizedでカラースキーム指定起動するとこんな画面です.tmux上ではminimal表示を利用しています.

f:id:takuzoo3868:20181228003551p:plain

ranger

vimに似た操作感でファイル操作ができます.設定ファイルは.config/ranger配下に作られるので,dotfilesによる設定管理も容易です.自分は見た目のカスタマイズにranger_deviconsを使っています.カラースキームはsnowを採用.

github.com

f:id:takuzoo3868:20181228004601p:plain

mps-youtube

作業用にyoutubeにあるnightcoreを聴きたいけれど,ブラウザは使いたくない自分がたどり着いたのはこのツールでした.Python製です.操作キーは上記2つのツールに比べると若干特殊なため覚える必要があります.起動はmpsyt.操作方法は以下の記事を見ればよいかと.自分はキーワード検索/hogefugaして全曲再生all,またはプレイリスト再生pl <playlist ID>を使うことが多いです.

www.blky.me

f:id:takuzoo3868:20181228010252p:plainf:id:takuzoo3868:20181228010305p:plain

weechat

IRCチャット用に軽量なweechatは様々なプラグインで拡張が出来ます.ただし,プラグインは言語依存があるので注意が必要です.入りたいIRCサーバを追加したり,バッファにチャンネルリストを表示したり,色々設定すればGUIに負けないチャットツールとして重宝できるでしょう.今は開発者コミュニティで情報収集やPKhack界隈のチャットとして使っていますが,できればtwitterやslackもweechatで表示したいと格闘中...

f:id:takuzoo3868:20181228011416p:plain

ターミナルマルチプレクサ

日本語の記事も多いtmuxを導入しました.sshを多用する場面でも何かと重宝するので以前から入っていましたが,本格的に設定したのは今回が初めてです. tmuxをいい感じに起動するためにセッションマネージャとしてtmuxpを導入します.

github.com

これは,競合であるtmuxinatorがRuby依存だったのに対して,tmuxpがpython依存だったからです(すでにpyenv+pipenvで環境構築していたため).この辺は好きなセッションマネージャを導入すればいいと思います.設定にはyamlを使います.

tmux_options: -2
session_name: work
root: ~/CTF/

windows:
- window_name: main
  layout: 86f2,151x37,0,0{84x37,0,0[84x22,0,0,2,84x14,0,23,6],66x37,85,0[66x10,85,0,3,66x26,85,11,4]}
  panes:
    - screenfetch
    - gotop -c solarized -m
    - ranger
    - mpsyt pl PLBFICLTzW2LckbdM1Ri6BBfpeSPwRYzXe
- window_name: chat
  panes:
    - weechat
- window_name: work
  focus: true
  layout: cab4,151x37,0,0{92x37,0,0,6,58x37,93,0,7}
  panes:
    - pane
    - pane

layoutの項目はtmux起動中にtmux list-windowsで調べることが出来ます.上記のような設定を書いて.tmux配下などに保存しておきtmuxp load path/to/config.yamlで起動するとウィンドウが複数立ち上がり,分割されたペイン上でそれぞれのCUIツールを走らせることが出来ます.大変便利.

諸々の見た目

Nerd Fonts

ターミナル上にもアイコン系ロゴ系フォントを導入します.理由はascii artのような見た目の楽しさと,neovimで既にairline用としてpowerline系のフォントを使っていたからです. 有名所をまとめた素晴らしいロゴフォントとしてNerd Fontsがあります.

github.com

このフォントを普段使うターミナル用フォントにfontforgeで合成し設定することで,ターミナルの表示はガラリと変化するはず.手っ取り早く導入したい方は日本語プログラミング環境用等幅フォントCica fontをおすすめします.ライセンス関係で一部表示できないため,どうしてもアイコン類を表示したい方はfontforgeを使って上書きすれば良いかと.日本語表示には影響がないので...

ターミナルでの表示確認用にシェルスクリプトを書きました.もともとNerd Fontsに同封されていた使いづらいテストコードをコマンドツールとして改良したものになります.

#!/usr/bin/env bash

# Author: takuzoo3868
# Last Modified: 07 Dec 2018.

# Check Nerd fonts drawing on terminal.
# Nerd Fonts Version: 2.0.0
# Script Version: 1.0.0

PROGNAME=$(basename $0)
VERSION="1.0.0"


usage() {
  echo "usage: ${PROGNAME} [--font_option]"
  echo ""
  echo "Check Nerd fonts v.2.0.0 drawing on terminal."
  echo ""
  echo "optional arguments:"
  echo "  -h, --help            show this help message and exit"
  echo "  --dev                 Font Devicon"
  echo "  --fa                  Font Awesome"
  echo "  --fae                 Font Awesome Extension"
  echo "  --iec                 IEC Power Symbols"
  echo "  --linux, --fontlogos  Font Linux and other open source Glyphs"
  echo "  --material, --mdi     Material Design Icon"
  echo "  --oct                 Octicons"
  echo "  --pl                  Powerline"
  echo "  --ple                 Powerline Extra"
  echo "  --pom                 Pomicon"
  echo "  --seti                Seti-UI + Custom"
  echo "  --weather             Weather Icons"
  exit 1
}

# Given an array of decimal numbers print all unicode codepoint.
function print-decimal-unicode-range() {
  local originalSequence=("$@")
  local counter=0
  # Use alternating colors to see which symbols extend out of the bounding
  # box.
  local bgColorBorder='\033[48;5;8m'
  local bgColorCode='\033[48;5;246m'
  local alternateBgColorCode='\033[48;5;240m'
  local bgColorChar='\033[48;5;66m'
  local alternateBgColorChar='\033[48;5;60m'
  local underline='\033[4m'
  local currentColorCode="${bgColorCode}"
  local currentColorChar="${bgColorChar}"
  local reset_color='\033[0m'
  local allChars=""
  local allCodes=""
  local wrapAt=5
  local topLine="${bgColorBorder}╔══════╦══════╦══════╦══════╦══════╗${reset_color}"
  local bottomLine="${bgColorBorder}╚══════╩══════╩══════╩══════╩══════╝${reset_color}"
  local line="${bgColorBorder}╠══════╬══════╬══════╬══════╬══════╣${reset_color}"
  local bar="${bgColorBorder}${reset_color}"
  local originalSequenceLength=${#originalSequence[@]}
  local leftoverSpaces=$((wrapAt - (originalSequenceLength % wrapAt)))

  # add fillers to array to maintain table:
  if [[ "$leftoverSpaces" < "$wrapAt" ]]; then
    # shellcheck disable=SC2034
    # needs rework without 'i' var?
    for i in $(seq 1 $leftoverSpaces); do
      originalSequence+=(0)
    done
  fi

  local sequenceLength=${#originalSequence[@]}

  printf "%b\\n" "$topLine"

  for decimalCode in "${originalSequence[@]}"; do
    local hexCode
    hexCode=$(printf '%x' "${decimalCode}")
    local code="${hexCode}"
    local char="\\u${hexCode}"

    # fill in placeholder cells properly formatted:
    if [ "${char}" = "\\u0" ]; then
      char=" "
      code="    "
    fi

    allCodes+="${currentColorCode} ${underline}${code}${reset_color}${currentColorCode} ${reset_color}$bar"
    allChars+="${currentColorChar}  ${char}   ${reset_color}$bar"
    counter=$((counter + 1))
    count=$(( (count + 1) % wrapAt))

    if [[ $count -eq 0 ]]; then

      if [[ "${currentColorCode}" = "${alternateBgColorCode}" ]]; then
        currentColorCode="${bgColorCode}"
        currentColorChar="${bgColorChar}"
      else
        currentColorCode="${alternateBgColorCode}"
        currentColorChar="${alternateBgColorChar}"
      fi

      printf "%b%b%b" "$bar" "$allCodes" "$reset_color"
      printf "\\n"
      printf "%b%b%b" "$bar" "$allChars" "$reset_color"
      printf "\\n"

      if [ "$counter" != "$sequenceLength" ]; then
        printf "%b\\n" "$line"
      fi

      allCodes=""
      allChars=""
    fi

  done

  printf "%b\\n" "$bottomLine"

}

function print-unicode-ranges() {
  echo ''

  local arr=($@)
  local len=$#
  local combinedRanges=()

  for ((j=0; j<len; j+=2)); do
    local start="${arr[$j]}"
    local end="${arr[(($j+1))]}"
    local startDecimal=$((16#$start))
    local endDecimal=$((16#$end))

    combinedRanges+=($(seq "${startDecimal}" "${endDecimal}"))

  done

  print-decimal-unicode-range "${combinedRanges[@]}"

}


for OPT in "$@"
do
  case $OPT in
    '-h' | '--help' )
      usage
      ;;

    '--dev' )
      echo "Nerd Fonts - Devicons"
      print-unicode-ranges e700 e7c5
      echo; echo
      ;;

    '--fa' )
      echo "Nerd Fonts - Font awesome" 
      print-unicode-ranges f000 f2e0
      echo; echo
      ;;

    '--fae' )
      echo "Nerd Fonts - Font awesome extension" 
      print-unicode-ranges e200 e2a9
      echo; echo
      ;;

    '--ice' )
      echo "Nerd Fonts - Font Power Symbols"
      print-unicode-ranges 23fb 23fe 2b58 2b58
      echo; echo
      ;;

    '--linux' | '--fontlogos')
      echo "Nerd Fonts - Font Linux"
      print-unicode-ranges f300 f31c
      echo; echo
      ;;

    '--material' | '--mdi')
      echo "Nerd Fonts - Material Design Icons"
      print-unicode-ranges f500 fd46
      echo; echo
      ;;

    '--oct' )
      echo "Nerd Fonts - Octicons"
      print-unicode-ranges 2665 2665 26A1 26A1 f400 f4a8 f67c f67c
      echo; echo
      ;;

    '--pl' )
      echo "Nerd Fonts - Powerline"
      print-unicode-ranges e0a0 e0a2 e0b0 e0b3
      echo; echo
      ;;

    '--ple' )
      echo "Nerd Fonts - Powerline Extra"
      print-unicode-ranges e0a3 e0a3 e0b4 e0c8 e0cc e0d2 e0d4 e0d4
      echo; echo
      ;;

    '--pom' )
      echo "Nerd Fonts - Pomicons"
      print-unicode-ranges e000 e00a
      echo; echo
      ;;

    '--seti' )
      echo "Nerd Fonts - Symbols original"
      print-unicode-ranges e5fa e62e
      echo; echo
      ;;

    '--weather' )
      echo "Nerd Fonts - Weather Icons"
      print-unicode-ranges e300 e3eb
      echo; echo
      ;;

    -*)
      echo "$PROGNAME: illegal option -- '$(echo $1 | sed 's/^-*//')'" 1>&2
      exit 1
      ;;

    *)
      usage
      ;;
  esac
done

Bash prompt

Nerd Fontsの導入で改善したい見た目その①はシェルのプロンプトでした.zshをお使いの方はPowerlevel9kを使えばイケイケなプロンプトになります. 自分はbashのままかfishが多いので,プロンプトをPowerlevel9kっぽく変更したいと思います.試行錯誤の末,完成したプロンプトがこちら.

f:id:takuzoo3868:20181228015533p:plain

  • お決まりのユーザー名・コンピュータ名

  • ディレクトリの種類で表示アイコンを切替

  • git管理ディレクトリ配下ではブランチ名,コミットID,各種ステータス

  • shell err code ステータス

といった表示を行っています.現時刻やipアドレスといった別ツールでも参照しやすい情報は後述するtmuxのステータスラインに表示することにしました. Nerd Fontsを中てた上でbashrcPS1を下記のように上書きすれば同様の表示となるはずです.

#!/usr/bin/env bash

if [[ $COLORTERM = gnome-* && $TERM = xterm ]] && infocmp gnome-256color >/dev/null 2>&1; then
    export TERM='gnome-256color';
elif infocmp xterm-256color >/dev/null 2>&1; then
    export TERM='xterm-256color';
fi;

if tput setaf 1 &> /dev/null; then
    tput sgr0; # reset colors
    bold=$(tput bold);
    reset=$(tput sgr0);

    black=$(tput setaf 234);
    blue=$(tput setaf 27);
    cyan=$(tput setaf 39);
    green=$(tput setaf 76);
    orange=$(tput setaf 166);
    purple=$(tput setaf 125);
    red=$(tput setaf 124);
    violet=$(tput setaf 61);
    white=$(tput setaf 15);
    yellow=$(tput setaf 154);
else
    bold='';
    reset="\e[00m";
    black="\e[1;30m";
    blue="\e[1;34m";
    cyan="\e[1;36m";
    green="\e[1;32m";
    orange="\e[1;33m";
    purple="\e[1;35m";
    red="\e[1;31m";
    violet="\e[1;35m";
    white="\e[1;37m";
    yellow="\e[1;33m";
fi;

export PROMPT_DIRTRIM=2

# icons set
ICON_HOME=""
ICON_DIR=""
ICON_ETC=""
ICON_USER=""
ICON_HOST=""

ICON_OK=""
ICON_FAIL=""
ICON_LOCK=""
ICON_NOT_FOUND=""
ICON_STOP=""

ICON_OCTOCAT=""
ICON_GIT_BITBUCKET=""
ICON_GIT_GITLAB=""

ICON_GIT_BRANCH=""
ICON_GIT_COMMIT=""
ICON_GIT_REMOTE_BRANCH=""
ICON_GIT_UNTRACKED=""
ICON_GIT_UNSTAGED=""
ICON_GIT_STAGED=""
ICON_GIT_STASH=""
ICON_GIT_INCOMING_CHANGES=""
ICON_GIT_OUTGOING_CHANGES=""
ICON_GIT_TAG=""

# git status veiw
prompt_git() {
    local s='';
    local branchName='';
    local gitHash='';

    GIT_DIR="$(git rev-parse --git-dir 2>/dev/null)"

    # Check if the current directory is in a Git repository.
    if [ $(git rev-parse --is-inside-work-tree &>/dev/null; echo "${?}") == '0' ]; then

        # check if the current directory is in .git before running git checks
        if [ "$(git rev-parse --is-inside-git-dir 2> /dev/null)" == 'false' ]; then

            # Ensure the index is up to date.
            git update-index --really-refresh -q &>/dev/null;

            # Check for uncommitted changes in the index.
            if ! $(git diff --quiet --ignore-submodules --cached); then
                s+=${ICON_GIT_STAGED};
            fi;

            # Check for unstaged changes.
            if ! $(git diff-files --quiet --ignore-submodules --); then
                s+=${ICON_GIT_UNSTAGED};
            fi;

            # Check for untracked files.
            if [ -n "$(git ls-files --others --exclude-standard)" ]; then
                s+=${ICON_GIT_UNTRACKED};
            fi;

            # Check for stashed files.
            if $(git rev-parse --verify refs/stash &>/dev/null); then
                s+=${ICON_GIT_STASH};
            fi;

        fi;

        # Get the short symbolic ref.
        # If HEAD isn’t a symbolic ref, get the short SHA for the latest commit
        # Otherwise, just give up.
        branchName=" ${ICON_GIT_BRANCH} $(git symbolic-ref --quiet --short HEAD 2> /dev/null || \
           git rev-parse --short HEAD 2> /dev/null || \
           echo '(unknown)')";

        [ -n "${s}" ] && s=" ${s}";

        # Get commit hash
        gitHash=" ${ICON_GIT_COMMIT} $(git rev-parse --short HEAD)";


        echo -e "${ICON_OCTOCAT}${1}${branchName}${gitHash}${2}${s}";
    else
        return;
    fi;
}

prompt_dir_icon(){
    case $PWD in
        $HOME)
            echo ${ICON_HOME}
            ;;
        "/etc")
            echo ${ICON_ETC}
            ;;
        *) 
            echo ${ICON_DIR}
            ;;
    esac
}

prompt_user(){
    if [[ "${USER}" == "root" ]]; then
        user_state="${orange}";
    else
        user_state="${blue}";
    fi;

    # the hostname when connected via SSH.
    if [[ "${SSH_TTY}" ]]; then
        hostStyle="${bold}${red}";
    else
        hostStyle="${yellow}";
    fi;

    echo -e "${user_state}${ICON_USER}"
}

prompt_host(){
    echo -e "${cyan}${ICON_HOST}"
}

prompt_result() {
  code=$?
  if [ ${code} == 0 ]; then
    echo -e "${ICON_OK}";
  elif [ ${code} == 126 ]; then
    echo -e "${ICON_LOCK}";            # Command invoked cannot execute
  elif [ ${code} == 127 ]; then
    echo -e "${ICON_NOT_FOUND}";       # Command not found
  elif [ ${code} == 130 ]; then
    echo -e "${ICON_STOP}";            # Script terminated by Control-C
  else
    echo -e "${ICON_FAIL} ${bold}${code}${reset}";
  fi;
}

# Set the terminal title and prompt.
PS1=" ";
PS1+="\$(prompt_user) \[${bold}\]\u ";
PS1+="\[${reset}\]";
PS1+="\$(prompt_host) \[${bold}\]\h ";
PS1+="\[${reset}\]";
PS1+="\[${green}\]\$(prompt_dir_icon) \[${bold}\]\w ";
PS1+="\[${reset}\]";
PS1+="\[${yellow}\]\$(prompt_git) ";
PS1+="\[${reset}\]";                                        
PS1+="\[${yellow}\]\$(prompt_result)";
PS1+="\[${reset}\]";                                    
PS1+="\n";
PS1+="\$ ";                            
export PS1;

Tmux status line

いい感じの見た目とすべく,昔は先人の情報をもとにtmux-powerlineを使っていました. しかし,dotfilesで管理しにくい点,及びpyenv環境ではpowerline設定がややこしい点から廃止し,ステータスラインをフルスクラッチすることにしました. 自分でも割とお気に入りの見た目を作ることが出来たと思っています.

f:id:takuzoo3868:20181228022707p:plain

メンテナンスしやすいようtmux.conftmux_local.confへ分離したり,色々やってますが長くなるので割愛します.dotfilesにあるこいつを見てくれ!独自にステータスへスクリプトを組み込むことも可能です.自分の場合は例として,local IP / global IP / 天気情報をスクリプトで追加しています.

#!/usr/bin/env bash
#
# Author: takuzoo3868
# Last Modified: 25 Nov 2018.

# Check OS
ostype() { echo $OSTYPE | tr '[A-Z]' '[a-z]'; }

export SHELL_PLATFORM='unknown'

case "$(ostype)" in
    *'linux'*  ) SHELL_PLATFORM='linux'  ;;
    *'darwin'* ) SHELL_PLATFORM='osx'        ;;
    *'bsd'*        ) SHELL_PLATFORM='bsd'        ;;
esac

shell_is_linux() { [[ $SHELL_PLATFORM == 'linux' || $SHELL_PLATFORM == 'bsd' ]]; }
shell_is_osx()   { [[ $SHELL_PLATFORM == 'osx' ]]; }
shell_is_bsd()   { [[ $SHELL_PLATFORM == 'bsd' || $SHELL_PLATFORM == 'osx' ]]; }

export -f shell_is_linux
export -f shell_is_osx
export -f shell_is_bsd

__run_lan() {
    if shell_is_bsd || shell_is_osx ; then
        all_nics=$(ifconfig 2>/dev/null | awk -F':' '/^[a-z]/ && !/^lo/ { print $1 }')
        for nic in ${all_nics[@]}; do
            ipv4s_on_nic=$(ifconfig ${nic} 2>/dev/null | awk '$1 == "inet" { print $2 }')
            for lan_ip in ${ipv4s_on_nic[@]}; do
                [[ -n "${lan_ip}" ]] && break
            done
            [[ -n "${lan_ip}" ]] && break
        done
    else
        # Get the names of all attached NICs.
        all_nics="$(ip addr show | cut -d ' ' -f2 | tr -d :)"
        all_nics=(${all_nics[@]//lo/})    # Remove lo interface.

        for nic in "${all_nics[@]}"; do
            # Parse IP address for the NIC.
            lan_ip="$(ip addr show ${nic} | grep '\<inet\>' | tr -s ' ' | cut -d ' ' -f3)"
            # Trim the CIDR suffix.
            lan_ip="${lan_ip%/*}"
            # Only display the last entry
            lan_ip="$(echo "$lan_ip" | tail -1)"

            [ -n "$lan_ip" ] && break
        done
    fi

    echo "${lan_ip-N/a}"
    return 0
}

__run_lan
#!/usr/bin/env bash
#
# Author: takuzoo3868
# Last Modified: 25 Nov 2018.

# Check OS
ostype() { echo $OSTYPE | tr '[A-Z]' '[a-z]'; }

export SHELL_PLATFORM='unknown'

case "$(ostype)" in
    *'linux'*  ) SHELL_PLATFORM='linux'  ;;
    *'darwin'* ) SHELL_PLATFORM='osx'        ;;
    *'bsd'*        ) SHELL_PLATFORM='bsd'        ;;
esac

shell_is_linux() { [[ $SHELL_PLATFORM == 'linux' || $SHELL_PLATFORM == 'bsd' ]]; }
shell_is_osx()   { [[ $SHELL_PLATFORM == 'osx' ]]; }
shell_is_bsd()   { [[ $SHELL_PLATFORM == 'bsd' || $SHELL_PLATFORM == 'osx' ]]; }

export -f shell_is_linux
export -f shell_is_osx
export -f shell_is_bsd

# Path tmp file
export DIR_TEMPORARY="/tmp/tmux-weather_${USER}"
if [ ! -d "$DIR_TEMPORARY" ]; then
    mkdir -p "$DIR_TEMPORARY"
fi

__run_wan() {
    local tmp_file="${DIR_TEMPORARY}/wan_ip.txt"
    local wan_ip

    if [ -f "$tmp_file" ]; then
        if shell_is_osx || shell_is_bsd; then
            stat >/dev/null 2>&1 && is_gnu_stat=false || is_gnu_stat=true
            if [ "$is_gnu_stat" == "true" ];then
                last_update=$(stat -c "%Y" ${tmp_file})
            else
                last_update=$(stat -f "%m" ${tmp_file})
            fi
        elif shell_is_linux || [ -z $is_gnu_stat]; then
            last_update=$(stat -c "%Y" ${tmp_file})
        fi

        time_now=$(date +%s)
        update_period=900
        up_to_date=$(echo "(${time_now}-${last_update}) < ${update_period}" | bc)

        if [ "$up_to_date" -eq 1 ]; then
            wan_ip=$(cat ${tmp_file})
        fi
    fi

    if [ -z "$wan_ip" ]; then
        wan_ip=$(curl --max-time 2 -s http://whatismyip.akamai.com/)

        if [ "$?" -eq "0" ]; then
            echo "${wan_ip}" > $tmp_file
        elif [ -f "${tmp_file}" ]; then
            wan_ip=$(cat "$tmp_file")
        fi
    fi

    if [ -n "$wan_ip" ]; then
        echo "${wan_ip}"
    fi

    return 0
}

__run_wan
#!/usr/bin/env bash
#
# Author: takuzoo3868
# Last Modified: 25 Nov 2018.
# API: http://developer.yahoo.com/weather
#
# osx need coreutils

# Check OS
ostype() { echo $OSTYPE | tr '[A-Z]' '[a-z]'; }

export SHELL_PLATFORM='unknown'

case "$(ostype)" in
  *'linux'*    ) SHELL_PLATFORM='linux'  ;;
  *'darwin'*   ) SHELL_PLATFORM='osx'        ;;
  *'bsd'*      ) SHELL_PLATFORM='bsd'        ;;
esac

shell_is_linux() { [[ $SHELL_PLATFORM == 'linux' || $SHELL_PLATFORM == 'bsd' ]]; }
shell_is_osx()   { [[ $SHELL_PLATFORM == 'osx' ]]; }
shell_is_bsd()   { [[ $SHELL_PLATFORM == 'bsd' ]]; }

export -f shell_is_linux
export -f shell_is_osx
export -f shell_is_bsd

# Path tmp file
export DIR_TEMPORARY="/tmp/tmux-weather_${USER}"
if [ ! -d "$DIR_TEMPORARY" ]; then
  mkdir -p "$DIR_TEMPORARY"
fi

# DEFAULT
WEATHER_DATA_PROVIDER_DEFAULT="yahoo"
WEATHER_UNIT_DEFAULT="c"
WEATHER_UPDATE_PERIOD_DEFAULT="600"
# input your woeid https://lab.syncer.jp/Tool/WOEID-Lookup/
WEATHER_LOCATION_DEFAULT="1118108"

export WEATHER_DATA_PROVIDER="${WEATHER_DATA_PROVIDER_DEFAULT}"
# What unit to use. Can be any of {c,f,k}.
export WEATHER_UNIT="${WEATHER_UNIT_DEFAULT}"
# How often to update the weather in seconds.
export WEATHER_UPDATE_PERIOD="${WEATHER_UPDATE_PERIOD_DEFAULT}"
# Name of GNU grep binary if in PATH, or path to it.
export WEATHER_GREP="${WEATHER_GREP_DEFAULT}"
# Your location. Find a code that works for you:
export WEATHER_LOCATION="${WEATHER_LOCATION_DEFAULT}"

# Setting grep command
if shell_is_bsd  && [ -f /user/local/bin/grep  ]; then
  WEATHER_GREP_DEFAULT="/usr/local/bin/grep"
else
  WEATHER_GREP_DEFAULT="grep"
fi

__default_settings() {
  if [ -z "$WEATHER_DATA_PROVIDER" ]; then
    export WEATHER_DATA_PROVIDER="${WEATHER_DATA_PROVIDER_DEFAULT}"
  fi
  if [ -z "$WEATHER_UNIT" ]; then
    export WEATHER_UNIT="${WEATHER_UNIT_DEFAULT}"
  fi
  if [ -z "$WEATHER_UPDATE_PERIOD" ]; then
    export WEATHER_UPDATE_PERIOD="${WEATHER_UPDATE_PERIOD_DEFAULT}"
  fi
  if [ -z "$WEATHER_GREP" ]; then
    export WEATHER_GREP="${WEATHER_GREP_DEFAULT}"
  fi
  if [ -z "$WEATHER_LOCATION" ]; then
    echo "No weather location specified.";
    exit 8
  fi
}

# Run status line in tmux
__run_weather() {
  __default_settings
  local tmp_file="${DIR_TEMPORARY}/weather_yahoo.txt"
  local weather
  case "$WEATHER_DATA_PROVIDER" in
    "yahoo") weather=$(__yahoo_weather) ;;
    *)
      echo "Unknown weather provider [${$WEATHER_DATA_PROVIDER}]";
      return 1
  esac
  if [ -n "$weather" ]; then
    echo "$weather"
  fi
}

# Get the weather from Yahoo!
__yahoo_weather() {
  degree=""
  if [ -f "$tmp_file" ]; then
    if shell_is_bsd; then
      last_update=$(stat -f "%m" ${tmp_file})
    elif shell_is_linux || shell_is_osx; then
      last_update=$(stat -c "%Y" ${tmp_file})
    fi
    time_now=$(date +%s)

    up_to_date=$(echo "(${time_now}-${last_update}) < ${WEATHER_UPDATE_PERIOD}" | bc)
    if [ "$up_to_date" -eq 1 ]; then
      __read_tmp_file
    fi
  fi

  if [ -z "$degree" ]; then
    weather_data=$(curl --max-time 4 -s "https://query.yahooapis.com/v1/public/yql?format=xml&q=SELECT%20*%20FROM%20weather.forecast%20WHERE%20u=%27${WEATHER_UNIT}%27%20AND%20woeid%20=%20%27${WEATHER_LOCATION}%27")
    if [ "$?" -eq "0" ]; then
      error=$(echo "$weather_data" | grep "problem_cause\|DOCTYPE");
      if [ -n "$error" ]; then
        echo "error"
        exit 1
      fi

      # Assume latest grep is in PATH
      gnugrep="${WEATHER_GREP}"

      # <yweather:units temperature="F" distance="mi" pressure="in" speed="mph"/>
      unit=$(echo "$weather_data" | "$gnugrep" -Zo "<yweather:units [^<>]*/>" | sed 's/.*temperature="\([^"]*\)".*/\1/')
      condition=$(echo "$weather_data" | "$gnugrep" -Zo "<yweather:condition [^<>]*/>")
      # <yweather:condition  text="Clear"  code="31"  temp="66"  date="Mon, 01 Oct 2012 8:00 pm CST" />
      degree=$(echo "$condition" | sed 's/.*temp="\([^"]*\)".*/\1/')
      condition=$(echo "$condition" | sed 's/.*text="\([^"]*\)".*/\1/')
      # Pull the times for sunrise and sunset so we know when to change the day/night indicator
      # <yweather:astronomy sunrise="6:56 am"   sunset="6:21 pm"/>
      if shell_is_bsd; then
        date_arg='-j -f "%H:%M %p "'
      else
        date_arg='-d'
      fi
      sunrise=$(date ${date_arg}"$(echo "$weather_data" | "$gnugrep" "yweather:astronomy" | sed 's/^\(.*\)sunset.*/\1/' | sed 's/^.*sunrise="\(.*m\)".*/\1/')" +%H%M)
      sunset=$(date ${date_arg}"$(echo "$weather_data" | "$gnugrep" "yweather:astronomy" | sed 's/^.*sunset="\(.*m\)".*/\1/')" +%H%M)
    elif [ -f "${tmp_file}" ]; then
      __read_tmp_file
    fi
  fi

  if [ -n "$degree" ]; then
    if [ "$WEATHER_UNIT" == "k" ]; then
      degree=$(echo "${degree} + 273.15" | bc)
    fi
    condition_symbol=$(__get_weather_image "$condition" "$sunrise" "$sunset") 
    echo "${condition_symbol} ${degree}°$(echo "$WEATHER_UNIT" | tr '[:lower:]' '[:upper:]')" | tee "${tmp_file}"
  fi
}

# Get symbol for condition. 
# Available conditions: http://developer.yahoo.com/weather/#codes
__get_weather_image() {
  local condition=$(echo "$1" | tr '[:upper:]' '[:lower:]')
  local sunrise="$2"
  local sunset="$3"
  case "$condition" in
    "tornado" | "tropical storm" | "hurricane")
      echo "" # weather_hurricane
      ;;
    "sunny" | "fair")
      time_forecast=$(date +%H%M)
      if [ "$time_forecast" -ge "$sunset" -o "$time_forecast" -le "$sunrise" ]; then
        echo ""
      else
        echo "" # mdi_weather_sunny
      fi
      ;;
    "hot")
      time_forecast=$(date +%H%M)
      if [ "$time_forecast" -ge "$sunset" -o "$time_forecast" -le "$sunrise" ]; then
        echo ""
      else
        echo "" # weather_hot
      fi
      ;;
    "rain" | "light rain" | "drizzle" | "light drizzle")
      echo ""
      ;;
    "showers" | "scattered showers")
      echo "" # weather_showers
      ;;
    "mixed rain and snow" | "mixed rain and sleet" | "freezing drizzle" | "freezing rain" | "mixed rain and hail" | "rain and snow")
      echo "" # weather_rain_mix
      ;;
    "light rain with thunder")
      echo "" # mdi_weather_lightning
      ;;
    "snow" | "mixed snow and sleet" | "snow flurries" | "light snow showers" | "blowing snow" | "sleet" | "heavy snow" | "scattered snow showers" | "snow showers" | "light snow" | "snow grains")
      echo ""
      ;;
    "hail")
      echo "" # mdi_weather_hail
      ;;
    "cloudy" | "mostly cloudy")
      echo "" # fa_cloud
      ;;
    "partly cloudy")
      echo "" # mdi_weather_partlycloudy
      ;;
    "severe thunderstorms" | "thunderstorms" | "isolated thunderstorms" | "scattered thunderstorms" | "isolated thundershowers" | "thundershowers")
      echo "" # weather_lightning
      ;;
    "dust" | "fog" | "haze" | "smoky")
      echo "" # weather_dust
      ;;
    "fog" | "foggy" | "mist")
      echo "" # weather_fog
      ;;
    "windy" | "blustery" | "breezy")
      echo "" # mdi_weather_windy
      ;;
    "clear" | "cold")
      time_forecast=$(date +%H%M)
      if [ "$time_forecast" -ge "$sunset" -o "$time_forecast" -le "$sunrise" ]; then
        echo "" # mdi_weather_night
      else
        echo ""
      fi
      ;;
    "not available")
      echo "" 
      ;;
    *)
      echo "" # unknown
      ;;
  esac
}

__read_tmp_file() {
  if [ ! -f "$tmp_file" ]; then
    return
  fi
  cat "${tmp_file}"
  exit
}

# exec
__run_weather

特に天気情報を表示するスクリプトは更新の止まっているプラグインが多かったので,書いてみて勉強にもなりました.

おわりに

今までの一連の設定を終えるとこんな感じにCUI作業環境が整います.

これらの環境で必要な設定ファイルは全て自分のdotfilesに入っていますので良かったら使ってみてください.

github.com

今後はタスク管理やメール管理もCUIで整備したいと考えています.ちなみにIRCtwitter環境やslackのTUI版であるsclackの導入も考えましたが,うまく出来なかったので知見ある方はコメントで教えていただけると助かります!それでは良きCUIライフを!

*1:筆者はこの他にMOONGIFTさんの記事をチェックしています.