くれなゐの雑記

例を上げて 自分で手を動かして学習できる入門記事を多めに書いています

椅子・机選び備忘録

この記事の読者の対象

筆者の机の上に乗っているもの

椅子

概要

椅子は多くのイラストレータプログラマが言うように、長時間の作業をサポートしたり腰を守ったりする重要な役割があります。長く座って作業をするものなので、家具屋さんで決めるときはちょっと座って決めるのではなく、一定以上の時間を座ってじっくり考えましょう。

アーロンチェア

storesystem.hermanmiller.co.jp

ハーマンミラーという会社の昔からある椅子です。歴史のある椅子ですが、実はアップデートがあってちょっとデザインが変わっていることがあります。(なので、中古品等を検討する場合は注意しましょう。)

リクライニングの硬さ変更や、アームレストの高さ変更など、高級椅子に備わっている基礎機能は大体ありますが、最近の椅子に見られる座面の移動や、ヘッドレストなどは正規品ではついてきません。

どこの部分が動くのか、どうやって動くのかは意外と難しいので、店員さんに教えてもらいましょう。ネット上ではここで確認できます。

hermanmiller-maintenance.jp

なんと言ってもやはり特徴的なのが、前傾姿勢モードでしょうか。 高い椅子を使っていても、やはり猫背気味になって背もたれに背が当たらなくては意味がありません。 そこで、猫背になっても背もたれが追従してきて、意地でも背もたれを使わさせてくるのがこの「前傾姿勢モード」です。 実際使ってみると、ゆりかごのように椅子全体が形を変え、椅子が体にひっついてくる感じが独特で個人的にハマりました。

値段は20万弱と機能や座り心地のわりに少し高い設定になっています。これはおそらく「ハーマンミラーアーロンチェア」というブランドが少し高くさせているのだと思います。

アーロンチェアについて特筆すべき項目をまとめると、

  • 前傾姿勢モードが独特
  • 座面移動がない
  • ヘッドレストが正規品では存在しない
  • 高い

といった感じです。私は今アーロンチェアを使っています。

エルゴヒューマン

www.ergohuman.jp

エルゴヒューマンには色々種類がありますが、足置きとか手元にPC置くやつとかいろんなパーツを付けることができます。 また、10万円前後と、アーロンチェアの半額強程度の金額で買うこともでき、コスパがいいです。

エルゴヒューマンの特徴は、「独立ランバーサポート」でしょうか。人間の背骨はS字になっているので、一直線の背もたれだと人間の体にフィットしません。S字の凹んでいるところにフィットさせて、負担を分散させるのが「ランバーサポート」なのですが、エルゴヒューマンのこれは独特で、独立して動きます。 なので、とても存在感があり、人によっては座ったときの「異物感」を感じるかもしれません。

このランバーサポートに体が合うかどうかがこの椅子を選択できるかどうかにかかっている印象があり、実際に座ってみて決める他ないと考えています。

その他ベーシックな機能としてはアーロンチェアと一緒です。

エルゴヒューマンについて特筆すべき項目をあげると

  • 足を置くやつやPCの台をつけられたり、アタッチメントが正規品で充実している
  • 独立ランバーサポートが独特でかなり好みを選ぶ
  • ヘッドレストもつけられる。
  • 値段が10万程度と機能の割にコスパがいい

といった感じです。ランバーサポートが私は合わなかったので、この椅子は候補にあがりませんでした。

バロンチェア

www.kagu-r.com

バロンチェアも比較的後発の椅子なので、アーロンチェアに比べて多機能です。座面移動とかもありますね。

アーロンチェアエルゴヒューマンと椅子を紹介してきましたが、バロンチェアは「オカムラ」という日本の会社が作っている椅子です。日本製の椅子なので、やはり日本人に合うのか周りの知り合いに椅子を紹介したときにバロンチェアが一番フィットすると言う人が多い気がします。会社もオカムラを使っているところが多い気がしました。

バロンチェアは「包み込まれるような」座り心地で、体を後ろに倒して、頭もヘッドレストにおいてリラックスした体制での長時間の作業に向いている椅子な気がしました。

日本人にとってバロンチェアは長所はあれど短所はない、オールラウンダーな性能をしている感じがありました。

ただし、後傾姿勢を前提とした設計をされている気がしており、前傾姿勢重視だとアーロンチェアに軍配が上がる気がしています。

値段は15万円ほどと、アーロンチェアエルゴヒューマンの中間くらいの値段をしており、こちらも無難な金額だと思います。

特筆すべき項目をあげると

  • 日本の会社が作った椅子で、日本人にフィットしやすい(?)
  • 頭まで包み込まれるような座り心地
  • 比較的新しい椅子で、多機能

といった感じです。最後の最後までアーロンチェアと悩みましたが、私は前傾姿勢で短時間で作業をすることが多いので、アーロンチェアを選びました。

机は椅子に比べてブランド云々で選ばなくて大丈夫だと思います。 ブランド観点では選ばなくていいと思いますが、いくつか観点があるので、そちらを列挙していこうと思います。

ちなみに私はこれを使っています(高い…)

rdpp-me.org

大きく分けて、以下の観点があると思っています

  • 横幅
  • 奥行き
  • 高さ・引き出し
  • 重さ
  • 天板の素材
  • 足元
  • 袖机
  • L字にするかどうか
  • 昇降機能付き

横幅

原則大きければ大きいほどいいです。冒頭で述べたものが机の上に乗っていますが、私は横幅150cmの机を使用しています。 横幅150cmあれば、モニタが2枚置けて、さらになにかを執筆するスペースを確保することができます。 かなりはかどります。

奥行き

机を買うときに気にならないがちなのですが、奥行きも重要です。 奥行きがあればキーボードを奥に寄せて紙で何かを執筆するスペースを確保することができますし、ペンタブも十分に置くことができます。

モニター+キーボード+リストレストだと意外と縦幅を消費してギリギリになるので、70cm~あればギリギリって感じですね。 私の使い方だと75~80cm以上あれば大丈夫かなという印象です。

80cm以上になると、「オフィスチェア」よりも「ダイニングテーブル」になってくるので、机の探し方には気をつけましょう。人が一人食事するスペースが奥行き40cmと言われており、80cmはそれが二人分入るスペースだからと言われています。

高さ、引き出し

自分にあった高さのものを買いましょうになるのですが、椅子との相性もあるので、家具屋さんで確かめれる場合は、椅子とセットで考えましょう。

また、椅子を机の下に収納する場合も高さの制約が付きます。 また、引き出しがある場合、アームレストがついている椅子だと絶望的になるので引き出しの有無はそこまで考えて購入しましょう。私は引き出しがついていない机を購入しました。

重さ

重い軽いは机の安定感に影響します。下にカーペットを引いているかどうかにも影響しますが、あまりに軽すぎるものには気をつけましょう。

天板の素材

IKEA等で購入する場合、安い天板がいくつかありますが、あまり安い天板はものを載せていると歪むものがあります。事前にレビューを確認して購入しましょう。

足元

足元がスッキリしているかどうかは意外と重要です。たとえば、机の足元や奥に天板に張り付く形でガードのようなものがついているものがありますが、モニターアームを取り付ける際にそれらがじゃまになってつかないケースもあります。

袖机の有無

袖机の上は基本的に使いづらく、物置になっている机をいくつか見ました。 モニターをおいてそこは基本的に使わないという意思があるのであればそれでもないないのですが、広く使いたい方には相性が相性が悪い可能性があります。

L字にするかどうか

L字はいいぞ!!!とおすすめする方も多いのですが、欠点もあります。たとえば、掃除がしにくくなったり、L字の短い辺の位置を交換できないものは引っ越ししたときの部屋のレイアウトに制限がかかる可能性があります。

広さは魅力ですが、気をつけて購入しましょう。

昇降機能付きの机

知り合いいわく、手で回すやつよりは金を払って電動式にしたほうがいいそうです。 私は机の広さを満たして、なおかつ買える金額の昇降式の机がなかったので購入を断念しました。 なにか知っている情報があれば教えてください

まとめ

以上、私的な椅子と机のレビューでした。机に関しては特にブランドに強いこだわりがなかったのですが、どこに行ってもほしい性能を満たす机がなく、結局椅子机すべて大塚家具で揃える形になってしまいました。

高級な椅子や机は多機能だったり素人にはわかりにくい、長時間使わないとわからないような特徴があったりするので、積極的に店員さんに話しかけましょう。

私が近くにいる場合は私もお手伝いします!!!

SECCON予選のCrazy Repetition of Codesのための繰り返し二乗法

はじめに

このブログは以前SECCONで解いた「Crazy Repetition of Codes」という問題の 解説記事(https://kurenaif.hatenablog.com/entry/2019/10/20/213842) の前提知識である、繰り返し二乗法というアルゴリズムを解説する記事になります。

繰り返し二乗法

繰り返し二乗法はその名の通り、二乗を繰り返して、効率的に  n 乗を求めるアルゴリズムです。

例えば、  x^ n を求めたいとします。

pow() 関数のような関数を使わずにこの値を求めたい場合は、

int x = 1;
for(int i=0;i<n;i++){
    x *= x;
}

のようなコードを書いて求めるのがお手軽です。この書き方で  n 乗を求めようとすると  \mathcal{O}(n) になりますね。 ( \Theta(n) )のほうが適切という説もありますが、このブログでは計算量をすべてビッグオー記法で説明します。)

しかし、人間が計算する場合は  x^ n を求める場合は順番にかけていくことは少ないと思います。例えば、 2^ 8 を求める場合は  1 ( 2^ 0 ) \rightarrow 2 ( 2^ 1 ) \rightarrow 4 ( 2^ 2 ) \rightarrow 16 ( 2^ 4 ) \rightarrow 256 (2^ 8)  と二乗を意識して計算することが多いのではないでしょうか。 8回計算しなければならなかった掛け算が、3,4回の計算で収まりましたね。  2^ {16} を求めたい場合は、  2^ 8 を知っているので、  256 \times 256 を計算すれば良さそうです。 これは電卓が必要になりますが、コードで実現する場合は実質電卓で計算しているようなものなので、気にしなくて良さそうですね。

では、この工夫した計算方法では何回で計算が終わるのでしょうか?乗数が倍々になっているので、  x 番目の乗数は  2^ x になっていそうです。逆に考えると、乗数  n の 計算回数が知りたい場合は log を取ればいいので、  \log(n) になりそうですね。

 x^ 4x^ 8 を求めたい場合は良かったのですが、例えば  x^ 6を求めたい場合はどのようにすればよいでしょうか?

幸いなことに、世の中には2の乗数で任意の整数を表現する方法があります!
2進数です!  6 は2進数表現で  \mathrm{0b110} です! なので、  x^ 6 = x^ 4 \times x^ 2 で求めることがきますね!

よって、大まかなアルゴリズムの流れは

  1. 乗数を2進数表現する。
  2. 1が立っているbitに対応する乗数を掛けていく

といった流れになります。

具体的なソースコードは以下のようになりますね。

int pow(int x, uint64_t n) {
	int res = 1;
	while (n > 0) {
		if (n & 1) res = res * x;
		x = x*x;
		n >>= 1;
	}
	return res;
}

これで任意の整数  n に対して、  \mathcal{O}( \log (n)) n乗を求めることができました!

大きな  n 乗を求めたい場合は多倍長整数を使うか、剰余を求めて有限体上で計算することが多いと思います。掛け算をしたあとでも mod計算は有効なので、xの10億乗を  10^ 9 + 7で割ったあまりを求めたい場合もこのやり方は有効です。

ちなみに、Pythonのpow関数では第3引数に剰余を与えることができ、計算も高速なので、気軽に整数の剰余を求めたく、Pythonを使用している場合はpow関数を使えば良いでしょう。言語や実装によっては遅い場合もあるので、自前で pow を用意したほうがいい場合もあります。

CTFにおけるCryptographyと行列

CTFにおけるCryptographyでは、アルゴリズムの内容を式に落とし込むことで考察が進むことが多々あります。

最近私が解いた問題では、SECCON Beginners CTFのPartyが直球でそういう問題でした。

kurenaif.hatenablog.com



ある(平文)ベクトル  x があり、処理  A をかけたら (暗号文)ベクトル  y になる。ということが行列演算で表現できた場合、以下のような式になります。


 y = Ax

もし、 y A が与えられ、  x がわからないという問題だった場合、  A逆行列を求めることで、  x を求めることができます。


 A^{-1} y = x

Cryptoで逆行列を求めたい場合の注意事項ですが、大体の場合は64bit整数に収まらないので、 numpy 等の機能を使用すると、bitが溢れてオーバーフローします。自前で実装するか、 Z3 などを使うと良いでしょう。
また、行列演算も普通の整数ではなく、有限体を扱う場合もあるので、問題に合わせて使うツールやアルゴリズムを選択しましょう。
高速に逆行列を求める必要がない場合は自前で実装しても良いかもしれませんね。

逆行列を用いて解く問題の例として、この問題を作ったので、よろしければ解いてみてください!

github.com

github.com


他にも、メルセンヌ・ツイスタという擬似乱数生成アルゴリズムも、624個連続した値を取得できれば次の値を予測できるということで有名ですが、これもアルゴリズムが行列で表現でき、逆行列が求められるからという說明が可能です。

行列表現と繰り返し二乗法

アルゴリズムを行列で表現できると、逆行列を使用することで平文を逆算できるといいましたが、例えばどういうものが行列表現にできるのでしょうか?

暗号とは関係ありませんが、有名所ではフィボナッチ数列があります。
フィボナッチ数列は、以下で求められる数列です。

a[i] = a[i-1] + a[i-2]
ただし、
a[0] = 0
a[1] = 1

この式は、実は行列で表現することが可能です。



\left(
\begin{array}{c}
a[i] \\
a[i-1] \\
\end{array}
\right)
=
\left(
\begin{array}{cc}
1 & 1 \\
1 & 0 \\
\end{array}
\right)

\left(
\begin{array}{c}
a[i-1] \\
a[i-2] \\
\end{array}
\right)

n番目のフィボナッチ数列の要素は、



\left(
\begin{array}{c}
a[i] \\
a[i-1] \\
\end{array}
\right)
=
\left(
\begin{array}{cc}
1 & 1 \\
1 & 0 \\
\end{array}
\right)^n

\left(
\begin{array}{c}
a[1] \\
a[0] \\
\end{array}
\right)

のようにして求められます。行列表現をすることにより、n番目のフィボナッチ数列 \mathcal{O}(\log(n)) で求められることにお気づきになられたでしょうか? 行列の掛け算を繰り返し二乗法で求められるからですね。

フィボナッチ数列の他にも、同じあみだくじを  N 個連結させた時の最終結果も繰り返し二乗法で、  \mathcal{O}(\log(N)) で求めることができますし、グラフを隣接行列形式に落とし込んで繰り返し二乗法を用いるケースもあります。競技プログラミングで使用する場合は、行列積に3乗のオーダーがかかるので、大きな行列の計算になる場合は気をつけましょう。(100個の和を求めるフィボナッチ数列みたいな問題は危なそうですね)

序盤で述べた SECCON 2019 Online CTF Crazy Repetition of Codes では、CRC32というアルゴリズム 10^ {10000} 回適用した結果を求めるという内容になっています。この回数を愚直に計算しているとコンテスト期間中に到底間に合いませんが、CRC32をうまく行列表現し、繰り返し二乗法でlogオーダーに抑えることで現実的な時間で解けます。

以前のブログで解いた手法では、CRC32を求める過程をうまく行列表現し、logで求めています。
私のアルゴリズムでは  \mathcal{O}(\log^2 N) の計算量がかかり、多倍長整数演算も行うので比較的速い言語で解いても50分ほどかかりますが、どうやら  \mathcal{O}(\log N) で抑える方法もあるらしく、かなり高速に求める手法もあるみたいです。

まとめ

このブログでは、繰り返し二乗法の說明から、行列表現、CTFにおけるCryptographyや競技プログラミングでの行列の扱い方の一例を取り上げました。問題を数式で表現すると、様々な性質が見えてくることもあり、考察が捗ることも多くあります。もし行き詰まった場合は一度数式に落とし込んで、世の中の使える定理を調べてみてはいかがでしょうか。

SECCON 2019 Online CTF Crazy Repetition of Codes write_up

SECCON 2019 Online CTF

Cryptoが3問出たので3問ときました。 特にほか2つは言うことがないので、Crazy Repetition of Codes のwriteupを書きます。

問題概要

crc32 という符号化を int("1"*10000) 回かけたので、その値を求めてね!という問題です。

import os
from Crypto.Cipher import AES

def crc32(crc, data):
  crc = 0xFFFFFFFF ^ crc
  for c in data:
    crc = crc ^ ord(c)
    for i in range(8):
      crc = (crc >> 1) ^ (0xEDB88320 * (crc & 1))
  return 0xFFFFFFFF ^ crc

key = b""

crc = 0
for i in range(int("1" * 10000)):
  crc = crc32(crc, "TSG")
assert(crc == 0xb09bc54f)
key += crc.to_bytes(4, byteorder='big')

crc = 0
for i in range(int("1" * 10000)):
  crc = crc32(crc, "is")
key += crc.to_bytes(4, byteorder='big')

crc = 0
for i in range(int("1" * 10000)):
  crc = crc32(crc, "here")
key += crc.to_bytes(4, byteorder='big')

crc = 0
for i in range(int("1" * 10000)):
  crc = crc32(crc, "at")
key += crc.to_bytes(4, byteorder='big')

crc = 0
for i in range(int("1" * 10000)):
  crc = crc32(crc, "SECCON")
key += crc.to_bytes(4, byteorder='big')

crc = 0
for i in range(int("1" * 10000)):
  crc = crc32(crc, "CTF!")
key += crc.to_bytes(4, byteorder='big')

flag = os.environ['FLAG']
assert(len(flag) == 32)

aes = AES.new(key, AES.MODE_ECB)
encoded = aes.encrypt(flag)
assert(encoded.hex() == '79833173d435b6c5d8aa08f790d6b0dc8c4ef525823d4ebdb0b4a8f2090ac81e')

ここで、crcを引き継いで使用していることがわかります。 crcwikipediaを読んで頂くと、割り算のあまりを求めてるんだなぁという気持ちになると思います。

一つの文字の場合、例えばTの場合は 0x[ord('T')]000000 = 0x54000000 ([]の部分は置き換えてねという意味で使ってます)の余りを求める問題になります。

文字列の余りはどういうことかというと、例えば、TSGの場合は 0x[ord('T')]000000[ord('S')]000000[ord('G')]000000 の余りを求める問題になります。

crc を引き継いでいるので、二回ループを回ったときは TSGTSG、3回ループを回ったときには TSGTSGTSG のように、文字列そのものの長さが長くなっていくイメージです。

さて、愚直に計算すると 10^10000 の計算をしなければならなく、1秒あたりに10^10回計算できたとしても10^9990秒かかるので無理です。

色々考えると、以下の2つの手法を思いつきました。

状態遷移をグラフとみなして閉路検出する方法

crcの状態量は高々 0x100000000 = 2^32 個しかないので、1e10000 もループを回していいたら、必ずどこかで巡回するはず。状態遷移をグラフとみなして、閉路を検出したらあとは割り算して余りの個数分回せばいいので、テーブル作成で最大 2^32、余りの個数が最大で2^32-1回で合わせて 2^33-1 回の計算で間に合うはずです。 1e10000 よりだいぶ少なくなりましたね。

適当に実装したため、メモリ使用量がえぐぐて諦めました。 冷静に考えて vector<bool> の最適化を考えればそれを使うべきでした。

kusanoさんがこの解き方です

qiita.com

繰り返し二乗法を使った方法

もし、この割り算の余りを求める処理を行列で表すことができれば繰り返し2乗法を使って、log(1e10000) = 23025 回くらいで計算を終わらせることができるはずです。残念ながら、割り算の余りの処理を行列にすることは失敗したのですが、近いアプローチには成功しました。

zlibの実装には、crc32_combine というものがあります。 これはどういう関数なのかというと、 crc1 = crc32("ABC"), crc2 = crc32("DEFG") だとすると、 crc32_combine(crc1, crc2, len("DEFG")) を渡すと、結合して crc32("ABCDEFG") を求めてくれるという関数です。

ソースコードを読み解いていくと、実は crc2 は最後の最後にしか現れなくて、 crc1 をずらす処理にほんとどの行数がかけられています。

crc1 をずらしていくというのはどういうことかというと、 crc32("ABC") から crc32("ABC\0\0\0\0") を 求めて、DEFG が入る隙間を用意してあげる処理のことです。

隙間を作ったら、crcの世界では足し算はxorで表現するので、 crc32("ABC\0\0\0\0") ^ crc32("DEFG") = crc32("ABCDEFG") が求まるということですね。

zlibの実装では、ここを行列の繰り返し二乗法を使って求めているので、O(log(文字列の長さ)) の時間がかかることが読み取れます。

このcombineを使って、

crc1 = crc32("TSG") # crc1 => "TSG"
crc2 = crc_combine(crc1, crc1, 3) #  crc2 => "TSGTSG"
crc4 = crc_combine(crc2, crc2, 6) # crc4 => "TSGTSGTSGTSG"

このように倍々に増やしていき、2進数表記で組み合わせれば求まります。たとえば 6( = 0b110) ならば、

crc6 = crc_combine(crc4, crc2, 6)

7( = 0b111) ならば、さらに1を加えて、

crc7 = crc_combine(crc6, crc1, 3)

のようにすれば求まりますね。 ダブリングでもO(log(文字の長さ))なので、全体の計算量的にはcombineがlogであることを考慮して、、log(文字の長さ)^2 で、おおよそ10^10くらいです。 ちょっと多いですがまあどうにかなるでしょう。

ちなみに実装が終わったタイミングで残り90分でした。死ぬかと思いました。(6並列で計算を回してギリギリ間に合いました。)

zlibのソースコードを参考にしつつ、K&R時代のC言語C++に直しつつ、lengthは uint64_t に入らないのでboostのBintに直しつつで、こんな感じのコードになりました。

// reference: https://github.com/madler/zlib/blob/master/crc32.c

#include <string>
#include <iostream>
#include <unordered_map>
#include <vector>
#include <boost/multiprecision/cpp_int.hpp>

using namespace std;
namespace mp = boost::multiprecision;
using Bint = mp::cpp_int;

constexpr int GF2_DIM = 32;

uint32_t gf2_matrix_times(const array<uint32_t, GF2_DIM>& mat, uint32_t vec)
{
    uint32_t sum;

    sum = 0;
    int i = 0;
    while (vec) {
        if (vec & 1)
            sum ^= mat[i];
        vec >>= 1;
        i++;
    }
    return sum;
}

void gf2_matrix_square(array<uint32_t, GF2_DIM>& square, const array<uint32_t, GF2_DIM>& mat)
{
    for (int n = 0; n < GF2_DIM; n++)
        square[n] = gf2_matrix_times(mat, mat[n]);
}

uint32_t crc32_combine(uint32_t crc1, uint32_t crc2, Bint len2){
    int n;
    unsigned long row;
    array<uint32_t, GF2_DIM> even;    /* even-power-of-two zeros operator */
    array<uint32_t, GF2_DIM> odd;   /* odd-power-of-two zeros operator */ 

    /* degenerate case (also disallow negative lengths) */
    if (len2 <= 0)
        return crc1;

    /* put operator for one zero bit in odd */
    odd[0] = 0xedb88320UL;          /* CRC-32 polynomial */
    row = 1;
    for (n = 1; n < GF2_DIM; n++) {
        odd[n] = row;
        row <<= 1;
    }

    /* put operator for two zero bits in even */
    gf2_matrix_square(even, odd);

    /* put operator for four zero bits in odd */
    gf2_matrix_square(odd, even);

    /* apply len2 zeros to crc1 (first square will put the operator for one
       zero byte, eight zero bits, in even) */
    do {
        /* apply zeros operator for this bit of len2 */
        gf2_matrix_square(even, odd);
        if (len2 & 1)
            crc1 = gf2_matrix_times(even, crc1);
        len2 >>= 1;

        /* if no more bits set, then done */
        if (len2 == 0)
            break;

        /* another iteration of the loop with odd and even swapped */
        gf2_matrix_square(odd, even);
        if (len2 & 1)
            crc1 = gf2_matrix_times(odd, crc1);
        len2 >>= 1;

        /* if no more bits set, then done */
    } while (len2 != 0);

    /* return combined crc */
    crc1 ^= crc2;
    return crc1;

}

uint32_t crc32(uint32_t crc, uint32_t data){
    crc = 0xFFFFFFFF ^ crc;
    crc = crc ^ data;
    for(unsigned int i=0;i<8;++i){
        crc = (crc >> 1) ^ (0xEDB88320 * (crc & 1));
    }
    return 0xFFFFFFFF ^ crc;
}

uint32_t crc32_str(uint32_t crc, const string& data){
    for(char c: data){
        crc = crc32(crc, c);
    }
    return crc;
}

int main(int argc,char *argv[]){
    string S = argv[1];
    uint32_t crc = crc32_str(0, S);
    Bint length = S.length();
    string num = "";
    for(int i=0;i<10000;++i) num+="1";
    Bint n(num);
    uint32_t res = 0;
    while(n > 0){
        cerr << n << endl;
        if (n & 1) {
            res = crc32_combine(res, crc, length);
        }
        crc = crc32_combine(crc, crc, length);
        length <<= 1;
        n >>= 1;
    }
    cout << res << endl;
}

50分くらい実行したら、crcが求まるので、それをもとに key を求めて、AES_ECBのdecryptをするだけです。

import os
from Crypto.Cipher import AES
from binascii import unhexlify

key = b""

crc = 2962998607
assert(crc == 0xb09bc54f)
key += crc.to_bytes(4, byteorder='big')

crc = 3836056187
key += crc.to_bytes(4, byteorder='big')

crc = 2369777541
key += crc.to_bytes(4, byteorder='big')

crc = 3007692607
key += crc.to_bytes(4, byteorder='big')

crc = 1526093488
key += crc.to_bytes(4, byteorder='big')

crc = 3679021396
key += crc.to_bytes(4, byteorder='big')

# flag = os.environ['FLAG']
# assert(len(flag) == 32)

# encoded = aes.encrypt(flag)
# assert(encoded.hex() == '79833173d435b6c5d8aa08f790d6b0dc8c4ef525823d4ebdb0b4a8f2090ac81e')
aes = AES.new(key, AES.MODE_ECB)
cipher = unhexlify(b"79833173d435b6c5d8aa08f790d6b0dc8c4ef525823d4ebdb0b4a8f2090ac81e")
print(aes.decrypt(cipher).decode())

InterKosenCTF 2019 Writeup

InterKosenCTF Writeup

interKosenCTFに参加しました! チームR19として、また僕自身がこういう大会にでるのは2回目ですね。

今回もCrypto解くマンとして参加しましたが、1日目で全完できたので、2日目はreversingとWeb問を見ていました。

チームとしては9位を取ることができました。今後は他の人の負担を減らすためにrevとWebも伸ばしたいですね。

f:id:kurenaif:20190813000154p:plain

f:id:kurenaif:20190813000617p:plain

Kurukuru Shuffle

以下のソースコードが与えられて、暗号文も与えられます。

from secret import flag
from random import randrange


def is_prime(N):
    if N % 2 == 0:
        return False
    i = 3
    while i * i < N:
        if N % i == 0:
            return False
        i += 2
    return True


L = len(flag)
assert is_prime(L)

encrypted = list(flag)
k = randrange(1, L)
while True:
    a = randrange(0, L)
    b = randrange(0, L)

    if a != b:
        break

i = k
for _ in range(L):
    s = (i + a) % L
    t = (i + b) % L
    encrypted[s], encrypted[t] = encrypted[t], encrypted[s]
    i = (i + k) % L

encrypted = "".join(encrypted)
print(encrypted)

ソースコードを読解すると、abkが実行時に定数として決まり、

for _ in range(L):
    s = (i + a) % L
    t = (i + b) % L
    encrypted[s], encrypted[t] = encrypted[t], encrypted[s]
    i = (i + k) % L

の部分でflagの文字をひたすら入れ替えています。 このflagの鍵は、 abk の3つの値なのですが、これらの値は高々 L^3 = 148877 程度なので、現実的な時間で復号可能です。 というわけで、abkの取りうる値を全探索して、以下のコードを書くと復号可能です。

flag = "1m__s4sk_s3np41m1r_836lly_cut3_34799u14}1osenCTF{5sKm"
L = len(flag)

for k in range(1, L):
    for a in range(L):
        for b in range(L):
            if a == b:
                continue
            encrypted = list(flag)
            i = k
            for _ in range(L):
                s = (i+a)%L
                t = (i+b)%L
                encrypted[s], encrypted[t] = encrypted[t], encrypted[s]
                i = (i+k)%L
            encrypted = "".join(encrypted)
            print(encrypted)

E_S_P

easyだと問題文に出ている気がしたので、2問目にときました。 特にハマることなく解けたので、運が良かったです。(1日目午前中で解けました。)

暗号生成アルゴリズムとしては、普通のRSA暗号ですね。 強いて言うなれば e が小さいのでPlainなRSAだとLow Public Exponent Attackが刺さるかもしれませんが、今回は平文が十分に長いので大丈夫です。

以下のような生成アルゴリズムが与えられます。(kurenaifがデバッグするためにいろいろいじってます)

from Crypto.Util.number import *
### from secret import flag, yukko
import re

yukko = "Yukko the ESPer: My amazing ESP can help you to get the flag! -----> "
flag = "KosenCTF{" + "0"*29 + "}"

assert re.match(r"^KosenCTF{.+}$", flag)

Nbits = 1024
p = getPrime(Nbits)
q = getPrime(Nbits)
n = p * q
e = 5
m = bytes_to_long((yukko + flag).encode())
c = pow(m, e, n)


print("N = {}".format(n))
print("e = {}".format(e))
print("c = {}".format(c))
print("m = {}".format(hex(m)))

print(yukko + "the length of the flag = {}".format(len(flag)))
print(yukko+flag)

yukko = "Yukko the ESPer: My amazing ESP can help you to get the flag! -----> "
flag = "KosenCTF{" + "\x00"*29 + "}"
print(hex(bytes_to_long((yukko+flag).encode())))
flag = "KosenCTF{" + "0"*29 + "}"
print(hex(bytes_to_long((yukko+flag).encode())))

今回は平文の前半部分がわかっているので、Coppersmith's Attackが有効です。 ももいろテクノロジー様の記事が、ソースコード付きでわかりやすいですね! しかしながら、知っていなければならない上位bitの個数が足りないため、復号ができません。 Yukko the ESPer: My amazing ESP can help you to get the flag! -----> KosenCTF{ までが既知文字列とすると、2,3byteくらい足りないので、その部分は全探索をします。

全探索を高速化する工夫点として、ASCIIの文字だけに絞って探索するようにしているため、ソースコードがやや複雑になっています。

from Crypto.Util.number import *
import os

### yukko = "Yukko the ESPer: My amazing ESP can help you to get the flag! -----> "
### flag = "KosenCTF{" + "0"*29 + "}"
### print(yukko + "the length of the flag = {}".format(len(flag)))
### print(bytes_to_long((yukko+flag).encode()))
### print(yukko+flag)

### 既知文字列を16進数表記にしたもの
base = "59756b6b6f207468652045535065723a204d7920616d617a696e67204553502063616e2068656c7020796f7520746f206765742074686520666c616721202d2d2d2d2d3e204b6f73656e4354467b"

def reverse(num):
    s = ""
    cnt = 0
    while num > 0:
        a = num % 95 + 32
        s += format(a, '02x')
        num = num // 95
        cnt += 1
    s += "0" * (60-cnt*2)
    return s, cnt

s = ""
i = 0
while True:
    s, byte = reverse(i)
    i += 1
    m = int(base + s, 16)

    N = 11854673881335985163635072085250462726008700043680492953159905880499045049107244300920837378010293967634187346804588819510452454716310449345364124188546434429828696164683059829613371961906369413632824692460386596396440796094037982036847106649198539914928384344336740248673132551761630930934635177708846275801812766262866211038764067901005598991645254669383536667044207899696798812651232711727007656913524974796752223388636251060509176811628992340395409667867485276506854748446486284884567941298744325375140225629065871881284670017042580911891049944582878712176067643299536863795670582466013430445062571854275812914317
    e = 5
    c = 4463634440284027456262787412050107955746015405738173339169842084094411947848024686618605435207920428398544523395749856128886621999609050969517923590260498735658605434612437570340238503179473934990935761387562516430309061482070214173153260521746487974982738771243619694317033056927553253615957773428298050465636465111581387005937843088303377810901324355859871291148445415087062981636966504953157489531400811741347386262410364012023870718810153108997879632008454853198551879739602978644245278315624539189505388294856981934616914835545783613517326663771942178964492093094767168721842335827464550361019195804098479315147

    beta = 1
    epsilon = beta^2/7

    nbits = N.nbits()
    kbits = (60*4 - byte*8)
    mbar = m & (2^nbits-2^kbits)
    print "upper %d bits (of %d bits) is given" % (nbits-kbits, nbits)

    PR.<x> = PolynomialRing(Zmod(N))
    f = (mbar + x)^e - c

    print hex(m)
    for x0 in f.small_roots(X=2^kbits, beta=1):
        print long_to_bytes(mbar + x0)
        exit()

flag_ticket

普通のPaddingエラーがあるCBCのブロック暗号なので、パディングオラクル攻撃を使って平文に戻したり、暗号文を改ざんすることが可能です。 Padding Oracle AttackによるCBC modeの暗号文解読と改ざん の知識がある前提の元でこの記事は執筆します。

暗号化する平文は以下のもような形式になります。 is_hit をTrueにして、サーバーに読み込ませたら勝ちです。

{"is_hit": False, "number": number}

ブロックの書き換えは、最後のブロックの変え方しか知らないので、最後のブロックにないis_hitを直接変更することは難しいです。 そこで、以下のように末尾に is_hit を付けて試しに手元のコードで読み込ませると、しっかり誤認してくれることがわかりました。

{"is_hit": False, "number": number,"is_hit":True}

文字を付け足してもブロックのサイズは変わらないようにnumberの文字数を調整して、以下のコードを使用すると、誤認させたい暗号文が生成できます。 あとはこれをcookieに流し込めば、OKです。

from Crypto.Cipher import AES
from Crypto.Util import Padding
from Crypto import Random
import json
from binascii import hexlify, unhexlify
import sys
from Crypto.Util.number import long_to_bytes, bytes_to_long
import copy
import requests

key =  b'\xa0\xda\xd8\xa3o\xd0\xed:g\x94\xd3\x8d\xe5\xb4\x13\xea'
def decoder(num):
    cipher = unhexlify(num)
    if len(cipher) < AES.block_size * 2:
        resp.text = "ERROR: cookie should be iv(16) + cipher(16*n)"
        exit(1)
    iv, cipher = cipher[: AES.block_size], cipher[AES.block_size :]
    aes = AES.new(key, AES.MODE_CBC, iv)
    data = aes.decrypt(cipher)
    print(data)
    data = Padding.unpad(data, AES.block_size)
    data = json.loads(data.decode())
    ### aes = AES.new(key, AES.MODE_CBC, iv)
    ### data = Padding.unpad(aes.decrypt(cipher), AES.block_size).decode()
    ### data = json.loads(data)
    ### resp.html = api.template("result.html", flag=flag, data=data)
    return data


def encoder(num):
    key =  b'\xa0\xda\xd8\xa3o\xd0\xed:g\x94\xd3\x8d\xe5\xb4\x13\xea'
    iv = Random.get_random_bytes(16)
    data = json.dumps({"is_hit": False, "number": num}).encode()
    data = Padding.pad(data, AES.block_size)
    iv = Random.get_random_bytes(AES.block_size)
    aes = AES.new(key, AES.MODE_CBC, iv)

    res = hexlify(iv + aes.encrypt(data)).decode()
    return res

def block2String(blocks):
    s = ""
    for block in blocks:
        for num in block:
            s += format(num, "02x")
    return s

def request(s):
    cookies = dict(result=s)
    r = requests.get('http://crypto.kosenctf.com:8000/result', cookies=cookies)
    ### print(r.text)
    if r.text == "ERROR: invalid cookie":
        assert False
    if r.text == "ERROR: unicode decode error":
        return 1
    if r.text == "ERROR: json decode error":
        return 1
    if r.text == "ERROR: padding error":
        return 0
    return 2
    ### try:
    ###     res = decoder(s)
    ###     backNum = num
    ###     return 2
    ### except TypeError as e:
    ###     return 0
    ### except UnicodeDecodeError:
    ###     f = True
    ###     return 1
    ### except json.JSONDecodeError as e:
    ###     f = True
    ###     return 1
    ### except ValueError as e:
    ###     "none"
    ###     return 0

C = []
M = []
Corg = []

### cipher = "a0dad8a36fd0ed3a6794d38de5b413ea2777446a3587a32b74432bac38014d9d070bf1451634350c6d685babf3913493c62a1fe616fec8b996d073a27cc17a692c750e22cf67b750dd5424d652aad715" 
cipher = "f8999c402717a871f501786986eebe0a6646dc3abcc2aa2336006eb9f97d23d01026308559de73d87b1d3b1fafcf7e7802484d81845249b9f943588f36f7af623f7b91083d3d97556b589adca33afdf0" ### サーバーが生成した元の暗号文
### cipher = "a0dad8a36fd0ed3a6794d38de5b413ea2777446a3587a32b74432bac38014d9d5bdbdd8f0aa33fd9c4eacdb21fe4fec884ab8f50e317a5cfd969cb1206f8682b" // 誤認させたいやつ
### m = "7b2269735f686974223a2066616c73652c20226e756d626572223a203132337d10101010101010101010101010101010" 
mdash = "7b2269735f686974223a2066616c73652c20226e756d626572223a20223132333435363738393031323334353637222c2269735f686974223a747275657d0202"### 誤認させたいやつ

while len(cipher) > 0:
    nxt, cipher = cipher[: AES.block_size*2], cipher[AES.block_size*2 :]
    block = []
    for i in range(0, len(nxt), 2):
        block.append(int(nxt[i:i+2], 16))
    C.append(copy.copy(block))
    Corg.append(copy.copy(block))

Mdash = []
Mdash.append([])
while len(mdash) > 0:
    nxt, mdash = mdash[: AES.block_size*2], mdash[AES.block_size*2 :]
    block = []
    for i in range(0, len(nxt), 2):
        block.append(int(nxt[i:i+2], 16))
    Mdash.append(copy.copy(block))

block_size = len(C)-1
ans = [[0]*16 for i in range(block_size)]
ans_block = []
for block_num in range(block_size-1,-1,-1):
    for i in range(len(C)):
        C[i] = copy.copy(Corg[i])
    print("block_num:", block_num)
    Cdash = [0]*16
    ans_block = [0]*16
    for i in range(15, -1, -1):
        print("i:" , i)
        backNum = -1
        for num in range(256):
            C[block_num][i] = num
            f = request(block2String(C[:block_num+2]))
            if f == 2:
                backNum = num

            if f == 1:
                print("found!")
                print(num)
                Cdash[i] = num
                ### ans[block_num][i] = (16-i) ^ Cdash[i] ^ Corg[block_num][i]
                ### ans_block[i] = (16-i) ^ Cdash[i] ^ Corg[block_num][i]
                ans[block_num][i] = Cdash[i] ^ (16-i) ^ Mdash[block_num+1][i]
                ans_block[i] = Cdash[i] ^ (16-i) ^ Mdash[block_num+1][i]
                print(block2String([ans_block]))
                for j in range(i,16):
                    C[block_num][j] = Cdash[j] ^ (16-j) ^ (17-i)

                print("ans:", block2String(ans))
                break

        if not f and backNum != -1:
            num = backNum
            print("found!")
            print(num)
            Cdash[i] = num
            ### ans[block_num][i] = (16-i) ^ Cdash[i] ^ Corg[block_num][i]
            ### ans_block[i] = (16-i) ^ Cdash[i] ^ Corg[block_num][i]
            ans[block_num][i] = Cdash[i] ^ (16-i) ^ Mdash[block_num+1][i]
            ans_block[i] = Cdash[i] ^ (16-i) ^ Mdash[block_num+1][i]
            print(i)
            print(block2String([ans_block]))
            for j in range(i,16):
                C[block_num][j] = Cdash[j] ^ (16-j) ^ (17-i)

            print("ans:", block2String(ans))
        Corg[block_num] = ans_block

S = block2String(ans) + block2String([C[block_size]])
print(S + block2String([C[-1]]))

pascal homomorphicity

これはRSAに見えますが、 自作暗号ですね paillier暗号というらしいですRSAのeに当たる部分が平文です。 ここで、式を見ていきましょう。

(N+1)^x mod N^2

ここの xflag にしたものが最初に与えられ、その後任意回数 x を好きに変えて暗号文を生成できます。 この時、実行を終了するまで N は共通です。

で暗号文を生成しています。 ここで、 (N+1)^x を展開して mod N2 を取ると、N2以上の項はN2で割り切れるため、

x N + 1 mod N^2

になります!つまり、 x を1変えれば、暗号文は N 変わることになります。 これで容易に N が特定できますね。 Nを特定したら、暗号文をcとして、

(c - 1)/N

でフラグを復元できます。

コード書く必要がなかったので書いてないです。

hugotto

782086ピクセルの画像が与えられ、 r,g,b のうちどれかの最終bitに1bitずつにフラグの情報が順に埋め込まれています。フラグの情報は何ビットかわかりませんが、埋め込んでいる途中に終端まで行くとまだ最初のbitを埋め込み始めます。

まずは文字列の長さを求めます。文字列は KosenCTF{ から始まることは既知なので、最初のある程度のbit数は既知として扱うことができます。 イメージとしては

KosenCTF{?????????????????KosenCTF{?????????????????KosenCTF{?????????????????KosenCTF{?????????????????KosenCTF{?????????????????

のように繰り返しになっているはずなので、このKとKの間を引き算してやると文字列の長さが求まるはずです。 よって、すべてのbitを二重ループ的に操作して、この KosenCTF{ になりうる開始位置を調べます。

開始位置の調べ方としては、0であるはずなのに、 rgbの最終bitが rgb = (1,1,1) であったり、 1であるはずなのに rgb = (0,0,0) であったりするケースを弾いていって、残ったものがそうなります。

誤認識の可能性があるので、最も多いものをフラグ長として採用しました。 以下のようになったのでフラグのbitの長さは544ですね。

{544: 1272, 104: 1, 13: 2, 427: 1, 150: 1, 21: 2, 88: 6, 285: 1, 456: 5, 130: 1, 14: 1, 3: 3, 397: 2, 520: 4, 24: 9, 160: 5, 384: 3, 72: 13, 472: 12, 336: 2, 208: 3, 300: 2, 244: 1, 224: 5, 320: 4, 106: 1, 418: 3, 20: 1, 152: 3, 392: 4, 56: 3, 16: 4, 8: 8, 216: 5, 328: 4, 539: 1, 5: 5, 144: 9, 11: 4, 203: 1, 44: 1, 100: 4, 400: 5, 245: 2, 299: 2, 312: 3, 232: 3, 376: 4, 168: 4, 280: 2, 264: 3, 93: 1, 451: 1, 32: 1, 512: 1, 352: 8, 192: 10, 176: 2, 368: 1, 126: 2, 528: 1, 464: 2, 349: 1, 179: 3, 463: 1, 81: 1, 146: 1, 230: 2, 36: 2, 278: 1, 222: 1, 322: 1, 61: 1, 475: 1, 240: 2, 304: 2, 128: 3, 416: 2, 218: 2, 326: 2, 444: 2, 436: 1, 108: 2, 283: 2, 261: 2, 344: 2, 200: 1, 288: 6, 256: 7, 129: 1, 415: 1, 69: 1, 459: 1, 541: 1, 296: 7, 248: 7, 504: 3, 40: 5, 272: 2, 365: 2, 238: 1, 306: 1, 432: 3, 112: 4, 123: 1, 421: 1, 408: 2, 325: 1, 170: 1, 374: 1, 38: 1, 506: 1, 424: 1, 120: 2, 35: 1, 509: 1, 162: 1, 382: 1, 373: 1, 171: 1, 136: 2, 460: 1, 84: 1, 508: 1, 96: 1, 448: 1, 533: 1, 82: 1, 462: 1, 396: 2, 148: 2, 184: 2, 360: 2, 48: 1, 536: 1, 403: 1, 141: 1, 488: 1, 12: 2, 260: 1, 76: 2, 468: 2, 363: 1, 186: 1, 358: 1, 426: 1, 118: 1, 9: 1, 2: 2, 4: 3, 491: 1, 198: 1, 346: 1, 147: 1, 59: 1, 279: 1, 265: 1, 80: 1, 431: 1, 1: 1, 429: 1, 115: 1, 386: 1, 134: 1, 47: 1, 281: 1, 402: 1, 142: 1, 132: 1, 412: 1, 28: 1, 99: 1, 263: 1, 182: 1}

フラグの長さがわかったので、その分の配列を用意して、rgbの最終bitが (0,0,0) になっているものか (1,1,1) になっているもの見つけて、その値を代入していけばフラグを復元できます。

from PIL import Image
from datetime import datetime
import tarfile
import sys
from Crypto.Util.number import long_to_bytes

import random

random.seed(int(datetime.now().timestamp()))


img = Image.open("./steg_emiru.png")
new_img = Image.new("RGB", img.size)

w, h = img.size

### 既知文字列のbit配列
prefix = []
for c in "KosenCTF{":
    for i in range(8):
        prefix.append((ord(c) >> i) & 1)

print(prefix)

### 配列データへと変換
rgb = []
for x in range(w):
    for y in range(h):
        r, g, b = img.getpixel((x, y))
        flag = [False]*2
        r = (r & 0x01)
        g = (g & 0x01)
        b = (b & 0x01)
        ### 0,1のどちらが立っているか
        flag[r] = True
        flag[g] = True
        flag[b] = True
        rgb.append(flag)

### ピクセル数
print(len(rgb))

### 既知文字列が現れる可能性がある位置
startList = []
for start in range(len(rgb)-len(prefix)):
    f = True
    for i in range(len(prefix)):
        if not rgb[start+i][prefix[i]]:
            f = False
            break
    if f:
        startList.append(start)

### 文字列の開始位置と思われる位置を引き算して、文字列の長さを集計する。
cnt = {} ### {開始位置の差分: 出現回数}
for i in range(len(startList)-1):
    diff = startList[i+1] - startList[i]
    cnt[diff] = cnt.setdefault(diff, 0) + 1
print(cnt)

### 最も多い長さがフラグの長さと仮定する
ma = [-1, -1]
for k, v in cnt.items():
    if ma[1] < v:
        ma = [k, v]

n = ma[0] ### 文字列長さ

### (0,0,0) か (1,1,1) を見つけて、確定したものから代入していく。
res = [-1]*n
for i in range(len(rgb)):
    ### どっちか確定したbitはもうそれで確定
    if rgb[i][0] == True and rgb[i][1] == False:
        res[i%n] = 0
    if rgb[i][0] == False and rgb[i][1] == True:
        res[i%n] = 1

print(res)

### 文字列化
ans = ""
for start in range(0, len(res), 8):
    c = 0
    for i in range(8):
        c += (res[start+i] << i)
    ans += chr(c)
print(ans)

Temple of Time

pcap ファイルが与えられるので、wiresharkでみるとすごい量のリクエストが見えます。 wiresharkで見るときは、httpリクエストで絞るといいことが多いので、とりあえず絞って、ついてにcsv出力をしておきます。

"No.","Time","Source","Destination","Protocol","Length","Info"
"12","5.629457982","150.95.139.51","192.168.1.2","HTTP","92","POST /login.php HTTP/1.1  (application/x-www-form-urlencoded)"
"14","5.631738584","192.168.1.2","150.95.139.51","HTTP","540","HTTP/1.1 302 Found  (text/html)"
"22","5.650384252","150.95.139.51","192.168.1.2","HTTP","428","GET /index.php?portal=%27OR%28SELECT%28IF%28ORD%28SUBSTR%28%28SELECT+password+FROM+Users+WHERE+username%3D%27admin%27%29%2C1%2C1%29%29%3D48%2CSLEEP%281%29%2C%27%27%29%29%29%23 HTTP/1.1 "
"24","5.652948109","192.168.1.2","150.95.139.51","HTTP","683","HTTP/1.1 200 OK  (text/html)"

どうやらSQLっぽいものがURLにひっついてますね。 デコードして内容を整理してみましょう

OR(
    SELECT(
        IF(
            ORD(
                SUBSTR(
                    (SELECT+password+FROM+Users+WHERE+username='admin'),
                    x,  ### note: 実際にはxではなく整数
                    1
                )
            )=y, ### note: 実際にはyではなく整数
            SLEEP(1),
            ''
        )
    )
)#

どうやら、パスワードの x 文字目が y であれば SLEEP(1) するみたいです。 なので、リクエストが帰ってくるのに時間がかかっていたら、パスワードのx文字目はyであることがわかりますね。

先程書き出したwiresharkcsvから、適当にしきい値を決めて次のリクエストが帰ってくるまでの時間を取り、それが一定以上であれば絞り込むスクリプトを書きました。

import csv
import urllib.parse

f = open("./packet", "r")

reader = csv.reader(f)

next(reader) ### ['No.', 'Time', 'Source', 'Destination', 'Protocol', 'Length', 'Info']
contents = []
for row in reader:
    time_start = float(row[1])
    content = urllib.parse.unquote(row[-1])
    ### print(time_start, content)
    if "SELECT" in content:
        row = next(reader)
        time_end = float(row[1])
        diff = time_end - time_start
        if diff > 0.01: ### しきい値
            contents.append(content)

chars = []
for content in contents:
   chars.append((int(content.split(',')[1]), int(content.split('=')[3].split(',')[0])))

ma = 0
for char in chars:
    ma = max(char[0], ma)

res = [ord('?')]*ma
for char in chars:
    res[char[0]-1] = char[1]

print("".join(map(chr, res)))

passcode

うちのrev担当からwindowsで辛いという声を頂き、やることがなかったので参戦。

.NET系はdnSpyを使うと便利なので、使う。 Correct あたりで探索して、

f:id:kurenaif:20190813000957p:plain

        private void shuffle()
        {
            int num = 0;
            foreach (int num2 in this.state)
            {
                num = num * 10 + num2;
            }
            Random random = new Random(num);
            for (int i = 0; i < 9; i++)
            {
                int index = random.Next(9);
                int value = this.vars[i];
                this.vars[i] = this.vars[index];
                this.vars[index] = value;
            }
        }

        // Token: 0x06000004 RID: 4 RVA: 0x000021CC File Offset: 0x000003CC
        private void push(int index)
        {
            this.indices.Add(index);
            this.state.Add(this.vars[index]);
            this.shuffle();
            if (Enumerable.SequenceEqual<int>(this.state, this.correct_state))
            {
                string text = "";
                for (int i = 0; i < this.indices.Count / 3; i++)
                {
                    text += ((char)(this.indices[i * 3] * 64 + this.indices[i * 3 + 1] * 8 + this.indices[i * 3 + 2])).ToString();
                }
                MessageBox.Show(text, "Correct!");
            }
        }

前回の入力をシード値に、ボタンを押されたときの数字をシャッフルしているらしい。 次に押す数字が、correct_state のものであればよい。 ボタンをポチポチ押す必要もなく、以下のソースコードで再現する。

using System;
using System.Collections.Generic;
using System.Linq;
<200b>
namespace ConsoleApp3
{
    internal class Program
    {
        private List<int> correct_state;
        private List<int> state;
        private List<int> vars;
        private List<int> indices;
<200b>
        private void shuffle()
        {
            int num = 0;
            foreach (int num2 in this.state)
            {
                num = num * 10 + num2;
            }
            Random random = new Random(num);
            for (int i = 0; i < 9; i++)
            {
                int index = random.Next(9);
                int value = this.vars[i];
                this.vars[i] = this.vars[index];
                this.vars[index] = value;
            }
        }
<200b>
        public void f()
        {
            this.vars = new List<int>
            {
                1,
                2,
                3,
                4,
                5,
                6,
                7,
                8,
                9
            };
            this.correct_state = Enumerable.ToList<int>(Enumerable.Select<char, int>("231947329526721682516992571486892842339532472728294975864291475665969671246186815549145112147349184871155162521147273481838", (char c) => (int)(c - '0')));
            this.indices = new List<int>();
            this.state = new List<int>();
<200b>
            for (int cnt = 0; cnt < this.correct_state.Count(); cnt++)
            {
                //次押すやつ
                int target = this.correct_state[cnt];
                // index: ボタン番号
                for (int index = 0; index < this.vars.Count(); index++)
                {
                    // ボタンを押す
                    if (this.vars[index] == target)
                    {
                        this.indices.Add(index);
                        this.state.Add(this.vars[index]);
                    }
                }
                this.shuffle();
                if (Enumerable.SequenceEqual<int>(this.state, this.correct_state))
                {
                    string text = "";
                    for (int i = 0; i < this.indices.Count / 3; i++)
                    {
                        text += ((char)(this.indices[i * 3] * 64 + this.indices[i * 3 + 1] * 8 + this.indices[i * 3 + 2])).ToString();
                    }
                    Console.WriteLine(text);
                }
            }
        }
<200b>
        private static void Main(string[] args)
        {
            Program p = new Program();
            p.f();
        }
    }
}

Beginners CTF 2019 writeup

はじめてCTFにチームとして参加しました!!!!!

R19 というチームで参加してました! kurenaifと申します

常設じゃないCTFはやるのは初めてです!

知り合いにpwnをひたすら布教されていたので、CTFはpwnだと思っていたのですが、実はCryptoもあり、それが面白そうだったのでチームメイトに俺はCryptoをやるぞーーーー!!!と言ってCryptoだけやりました。

pwnもやってみたのですが、すべてを忘却していたため頑張って思い出そうと思います。

[Crypto] [warmup] So Tired

so_tired.tar.gz が渡されました中身は encrypted.txt だったのですがこれがとても長い… なんか最後の方に == とかついてますし、 base64 encode らしさがありますね というわけで

$ cat encrypted.txt | tr -d '\n' | base64 -d  > src

out を見てみると、よくわからないバイナリですね… よくわからないバイナリはとりあえず file を見てみましょう

$ file src
out: zlib compressed data

どうやらzlibだそうです!

#!/usr/bin/python3
# a.py
import zlib

f = open("src", "rb")
data = f.read()
f.close()

dc = zlib.decompress(data)
f = open("out", "wb")
f.write(dc)
f.close()

outを見てみると…

$ cat encrypted.txt | wc -c
373728
$ file out 
out: ASCII text, with very long lines, with no line terminators
$ cat out | wc -c
370052

あっ…(察し)

また base64 してみると、どうやらこれも zlib みたいです。 base64zlib をひたすらぐるぐるさせたのが、この暗号みたいですね。

それではさっきのPythonと組み合わせて

# a.sh
while true
do
cp out out.bak
cat out | tr -d "\n" | base64 -d > src
python3 a.py
wc -c out
done

これでエラーが出るまで待ち続けましょう!

base64: invalid input
Traceback (most recent call last):
  File "a.py", line 8, in <module>
    dc = zlib.decompress(data)
zlib.error: Error -3 while decompressing data: incorrect header check
37 out

エラーが出ました! いい文字数ですね!

$ cat out
ctf4b{very_l0ng_l0ng_BASE64_3nc0ding}

[Crypto] Party

どうやら、3組のpair、すなわち合計6つの数字が与えられるみたいですね。

  • coeffx
  • 出力された数字を y
  • partyr

とおくと、以下の式が成り立っていることがわかります!



\begin{bmatrix}
r_0^0 & r_0^1 & r_0^2 \\\ 
r_1^0 & r_1^1 & r_1^2 \\\
r_2^0 & r_2^1 & r_2^2
\end{bmatrix}

\begin{bmatrix}
x_0 \\\ 
x_1 \\\
x_2
\end{bmatrix}

=

\begin{bmatrix}
y_0 \\\ 
y_1 \\\
y_2
\end{bmatrix}

 x_0 がフラグですね

行列の形がわかれば、あとは行列を斜めにするやつをやるだけです! 少数型に落とすと情報が欠けそうで怖かったので、有理数クラスを使って解いてます。

#!/usr/bin/python3

import numpy as np
from fractions import Fraction
from Crypto.Util.number import long_to_bytes

def f(x, coeff):
    y = 0
    for i in range(len(coeff)):
        y += coeff[i] * pow(x, i)
    return y

def solve(mat):
    for row in range(len(mat)):
        tar = mat[row][row]
        for col in range(len(mat[row])):
            mat[row][col] /= tar
        for r in range(len(mat)):
            if r == row:
                continue
            boost = mat[r][row]
            for c in range(len(mat[r])):
                mat[r][c] -= mat[row][c] * boost
    return mat

N = 512
M = 3

r = [0]*3
y = [0]*3

file = open("src", "r")

for i in range(3):
    r[i] = Fraction(int(file.readline()))
    y[i] = Fraction(int(file.readline()))

mat = [
    [Fraction(1), Fraction(r[0]), Fraction(r[0]*r[0]), y[0]],
    [Fraction(1), Fraction(r[1]), Fraction(r[1]*r[1]), y[1]],
    [Fraction(1), Fraction(r[2]), Fraction(r[2]*r[2]), y[2]],
]

print(solve(mat))
for i in range(len(mat)):
    for j in range(len(mat[i])):
        print(mat[i][j], end=" ")
    print()

print(long_to_bytes(mat[0][-1]))

しっかり単位行列になってフラグが取れました!

1 0 0 175721217420600153444809007773872697631803507409137493048703574941320093728 
0 1 0 6759741750199108721817212574266152064959437506612887142001761070682826541920627672362291016337903640265385249474489124882116454124173716091800442011015857 
0 0 1 8559415203809303629563171044315478022492879973152936590413420646926860552595649298493153041683835412421908115002277197166850496088216040975415228249635834 
b'ctf4b{just_d0ing_sh4mir}  

Go RSA

RSA暗号d と encryptされたデータは教えてくれますが、 N をなくしてしまったみたいです!!!!!

Nないのにお前どうやって暗号化してんだよ!!!!

この問題、2つ気づくことに気づけばなんと瞬殺できます

  • まず一つ、数字が入力できること
  • そして、その数字に負の数が入ること

ガチャガチャ遊んでいたら、負の数を入ることを発見してしまいました。 -1 が通るならもう自明です。

RSA暗号の暗号化は

c = m^e MOD N

復号は

m = c^d MOD N

ですね。 このNがわからないので、復号できないという問題でした。

この問題では、フラグの c の他に、3回まで好きな整数の m を入力することができます。

では、 m-1 を入れたら…?

通常eは65537とか3とかです。

m = -1^e MOD N = -1 MOD N = N-1

なんと N-1 をもらえました!!!

これを +1 して、複合してやると…

from Crypto.Util.number import long_to_bytes

a = 19721739460005715839687258097463606567568088122562143766673559079928993888011218840527723736945976855276429750686840511245308087039361625859575588315010586932109088534073184785632230817871748507755371081564038161176335857405579592850018586825018841366905735384823298027525055483718836334184117597308146143832746609071410404320299844124897769731428479677374581456236061208983710496710928583658704287830103346312036579012291640838414960806920681837417786550306083430969817954317601886422446799325599439885265181833346633337600066770273573882073976043621513973625591205643086917193039382944499499255990087899534083558712
n = a + 1
d = 16012534574459681485963634139862001145411989213568735706066294645804685807634846967717788543366836663962557081706478331993745344148366136283532609515693857534590486442567835507091820407552582660881197653556068092746906439239029795613811114691641020613919176733171071476305533169723408153840900277253517317842880767356950185288540335520596457402867899206001172130409041175548610181861286325063434009922901699522599455879079648553315178156923597492552797606051253955520510125897947319770306996525090494984301910656673155680183032096435051027220919313430919758310229110059410508088941767032227875386851849410203872128257

x = 4166307417617957284869321340663074830305356736973969287118610781379422763896670624160917668898929485302592913693740635487274812853279249498082813735026859474333821827825022846601676094308501172617202223248824318344700000867606631910692133738577886686239665573753583206491647139406448572619516253202158832537788022928614879944248391229292623896316715442295616471480180228286163207028450461678895309475635821698950149318482145068775702644341882119883921129616029519739738868428874523531712307489141815448685352233906955519708655061860955347613402457489376739490167812575480528575373033977959010646084855165180428882395
print(long_to_bytes(pow(x,d,n)))

いえい!

b'ctf4b{f1nd_7he_p4ramet3rs}'

これ、あってんのか?と思ってましたがどうやら想定解放で、周りを眺めていたら少数派っぽいです…

別解(天才解法)

qiita.com

zeosutt.hatenablog.com

[crypto] bit_flip

元の数字があり、 nc でつなぐたびに、下1/4くらいのランダムな1bitを変えて、暗号化した数字を返してくれます。 難しいです。 これはRSAなのですが、

Nec はわかるのですが、dを教えてくれません。ただの公開鍵の情報です

もしこれで復号できたら、RSA暗号を解読したことになります。まともに解いてられません。

考察1 e が小さい

eが3なので、ワンチャン 3乗した結果が N より小さければ 3乗根を求めるだけで解けます。

... 無理でした

考察2 1bitしか変わっていない

もとの数字を x として、i bit目が変わったとすると


(x + 2^i)^3 \mathrm{or} (x - 2^i)^3

が大量に渡されるので、なんとかして連立方程式を解けないかと考えました

...無理でした

実はできた

qiita.com

考察3 Flanklin-Reiter Related Message Attack

RSAの暗号の脆弱性を調べていると見つけました。

2つのメッセージ m1m2 の差がわかれば解けるという問題です。

1024/4 = 256 bitのうち、1bit変えただけなので、差のパターン数は 256^2 そんなに多くありません。

差は、 xor をとっているので

abs((1 << i) - (1 << j)) or (1 << i) + (1 << j)

の2択です。

2つの差分がわかれば、あとはその全てに対して複合するだけです。

ももいろテクノロジー様のソースコードを使わせていただいて、

inaz2.hatenablog.com

以下のようになりました

# coppersmiths_short_pad_attack.sage

# reference: http://inaz2.hatenablog.com/entry/2016/01/20/022936
import sys


# reference: http://inaz2.hatenablog.com/entry/2016/01/20/022936
def short_pad_attack(c1, c2, e, n):
    PRxy.<x,y> = PolynomialRing(Zmod(n))
    PRx.<xn> = PolynomialRing(Zmod(n))
    PRZZ.<xz,yz> = PolynomialRing(Zmod(n))

    g1 = x^e - c1
    g2 = (x+y)^e - c2

    q1 = g1.change_ring(PRZZ)
    q2 = g2.change_ring(PRZZ)

    h = q2.resultant(q1)
    h = h.univariate_polynomial()
    h = h.change_ring(PRx).subs(y=xn)
    h = h.monic()

    kbits = n.nbits()//(18)
    print(h.small_roots(X=2^kbits, beta=0.5))
    diff = h.small_roots(X=2^kbits, beta=0.5)[0]  # find root < 2^kbits with factor >= n^0.5

    return diff

# reference: http://inaz2.hatenablog.com/entry/2016/01/20/022936
def related_message_attack(c1, c2, diff, e, n):
    PRx.<x> = PolynomialRing(Zmod(n))
    g1 = x^e - c1
    g2 = (x+diff)^e - c2

    def gcd(g1, g2):
        while g2:
            g1, g2 = g2, g1 % g2
        return g1.monic()

    return -gcd(g1, g2)[0]

if __name__ == '__main__':
    n = 82212154608576254900096226483113810717974464677637469172151624370076874445177909757467220517368961706061745548693538272183076941444005809369433342423449908965735182462388415108238954782902658438063972198394192220357503336925109727386083951661191494159560430569334665763264352163167121773914831172831824145331
    e = 3

    c1 = 38448272237964375371726759592758385386616013779162822338653392077256549091123869239648616008586564655973797454298230191967696840378043670012083704181791314856097307077627229165947387478341901251979390480932220983592258282304304001581658323909165412718287474168394422138841873902714319309538983680281287491186
    c2 = 51531223430957857733934880383408638185102108212202467189651292360878470909281785640325226719442938173566544600753270928156891865771525435019551126712738055654692625329783768014054928002128679874456939510971058344318886221948942145053253549048822643888005377157270281027834500602254102431820137131985798724504

    for bit1 in range(1024//4):
        for bit2 in range(bit1):
            sys.stderr.write(str(bit1) + "," + str(bit2) + "\n")
            a = (1<<bit1)
            b = (1<<bit2)
            for diff in [(a+b), (a-b), -(b-a)]:
                # print bit1, bit2
                # print "difference of two messages is %d" % diff

                m1 = int(related_message_attack(c1, c2, diff, e, n))
                m2 = m1 + diff

                pos = []
                i = 0
                while i < 1025: 
                    bm1 = ((m1 >> i) & 1)
                    bm2 = ((m2 >> i) & 1)
                    if bm1 != bm2:
                        pos.append(i)
                    i += 1
                
                if len(pos) != 2:
                    continue

                print(m1)
                print(m1 ^^ (1 << pos[0]))
                print(m1 ^^ (1 << pos[1]))
                print(m1 ^^ (1 << pos[1]) ^^ (1 << pos[0]))

あとは出てきた数字をすべてbyte文字列に戻すと…?

見つかりました!

f:id:kurenaif:20190528005152p:plain

[misc] Dump

休憩時間にときました。 いっぱいパケットがありますが、 wireshark の機能を使えば実はhtmlだけ取り出せます!

`File>Export Objects>HTTP...>Packet 3193`

hexdumpの8進数なので、 あとはこれをdecodeするだけですね

import struct

file = open("src", "r")

lines = file.readlines()

nums = []
for line in lines:
    nums += list(map(lambda x: int(x, 8), line.split()))

with open("out", "wb") as fout:
    for x in nums:
        print(x)
        fout.write(struct.pack("B", x))

[misc] Sliding puzzle

幅優先探索で出来るくらいの計算量ですが、ちょっと書くのが辛かったので先人のコードをお借りしました。

host = '133.242.50.201'
port = 24912
bufsize = 4096

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))

while True:
    a = sock.recv(bufsize).decode()
    print(a)
    rows = a.split('\n')[1:4]

    rows = list(map(lambda x:x.split('|')[1:-1], rows))
    for i in range(len(rows)):
        for j in range(len(rows[i])):
            rows[i][j] = int(rows[i][j])
    ans = checkio(rows)
    res = ",".join(ans)
    sock.send(res.encode())

所感

Crypto たのしい!!!!!!! pwnwebreversingは完全に人に任せていたので、反省です。 今度反省会をするので、そのときにコツを教えてもらいます!

新卒就活体験記

会社名に関しては隠したり隠さなかったりしているが今回は念の為隠す方針にする。 もし興味がある人は直接聞いてほしい。

全体

基本的には逆求人(アカリク、Gスタイラス)を通じて会社の人事さんとつながってそこから選考~という感じ。 特に下調べはせずに、人事さんやあったエンジニアさんの話、会社の雰囲気を中心に就活を始めた。 検討した会社は逆求人等で会った会社は多分20社くらいで検討した会社は7社くらい。 自分のキャパやスケジュール調整能力的に7社が限界だったけど本当はもっといろんな会社を見たかった B3くらから動けばよかったとちょっと後悔。 2月から7月間で長い間研究と並行で進めた。 基本的にいっぱい受けて受かった企業から選ぶつもりだった。

全体の面接対策等

逆求人ベースなので、逆求人で説明するために資料を用意してそれをベースに話した。 情報系以外の人でもわかるような話と、短時間では情報系の人にしか伝えられないような深い内容の話の2通りがあり、前者を優先すると後者があまり満足せず、後者を優先すると前者が満足しない。 自分は両方を満たすことが出なかったので、技術的な話を優先して話した。

自己アピールのトピックとしては、

  • 中学生からプログラミングをしており、当時は簡単なものしか作れなかったが一応ロボカップジュニアで全国大会に行ったこと
  • 高専時代で作ったゲーム用のライブラリの話
  • 高専時代の卒論(ロボットの歩行アルゴリズム)の話
  • 高専時代は一応複数人での開発を経験しているよという話
  • 大学で成績が良かったよいう話
  • 大学では競技プログラミングちょっとやってて一応青コーダーだよという話
  • 競技プログラミングだと何故かペアプロが得意だよという話
  • 大学院の研究室ではサーバー管理等やっているよという話
  • 大学院の研究では先生のコードがバグってるので大量に直したよという話
  • イラストレーターとも交流があって50人規模の企画を動かしてたよという話

があり、これをさっと流しながら面接官のリアクション等を見ながら雑談をした。 大体「いろいろやってるね」というリアクションが帰ってくる。

面接中にも情報を色々仕入れた。 20社以上に今会社の困っていることを聞くと、結構似たようなことに困っていることに気づいた。 それに対する解決法等も色々で面白かった。

競技プログラミングを長くやっているので、競技プログラミング中心で話をした。

それぞれの会社

A社とかC社とかD社とかG社とかF社とか書いてるけど実際に受けた企業の頭文字とは関係ないよ

A社

プログラミングテストがあったので、気軽に受けてみたら通ってしまった。 面接に関して良くない噂が流れていたが、ゲーム会社だけあってやはりゲーム(ライブラリ自作)の話がずいぶん盛り上がった。

B社

最終面接で落ちた。 最終面接までは楽しく技術面接等をしてきたが、最後の面接で僕のできること/やりたいことと会社でやることが微妙にマッチしていないことに気づく。 社員がとにかく忙しそうだった。 忙しいと思って働きたくないので、ちょっとそれもミスマッチだった。 英語が喋れなさすぎた。

C社

逆求人の人におすすめされたので、受けてみた。 会社の作っている製品や、やっている内容にとても興味があったが、業績がちょっと不安だったのと知り合いからの評判がイマイチだったのでお断りしてしまった。

D社

遊びにおいでよ!と言われて遊びに行ったら面接だった。 準備してなくてコミュ症が発動してしまい1次で落ちてしまった。

E社

ここに受かればここに行っていた。みんなも知ってるあの会社。 面接で前泊をさせてくれたが、現地に行ったら3万の激ヤバホテルでびっくりした。 めったに落ちないと言われる最終選考で落ちてしまい、正直受かったと思っていたのでかなり大きなショックを受けた。 ちょっと英語が喋れた。

F社

ここに受かればここに行っていた第2社。 E社の選考待ちで精神不安定状態になり、簡単なはずの面接で力が出ずに落ちた。 英語が喋れなかった。 メンタルを鍛えたい。

G社

インターンに行った会社。 受けてないけど考えてはいたので一応。 低レイヤ系はめちゃめちゃ消耗するので、体が耐えきれないと思って後回ししていて、そのまま終わってしまった…。 ごめんなさい…

H社

最終的にここにした。 自分と似たスキルを持つ人が多く感じており、楽しく働いていそうだった。

おわりに

Atcoderをやっていたおかげで、某社を除いた会社でコーディングテストに困ることはなかった。 ただ、青コーダーなので、自分より上のレートの人は世の中にいっぱいいる。 「うちの会社にはもっと上のレートの人もいますけど」みたいな返しをしてくる会社も当然いる。 トップクラスならいいけど、中の上くらいであれば他にも武器は持っておいたほうがいい。

「〜できますか?」という曖昧な質問がよくあるが、この質問に対する答えは相対的なもので決まる。 入門書を読んで、簡単なものを作った程度の人でもやっていない人に比べればそれはできることになる。 入門書レベルから趣味で普段から使っているレベル、業務レベル、研究レベル等色々なレベルがあるが、何がどれくらいできて何ができないのか 相手の企業の事業内容やリアクションを見ながら武器をたくさん見せたほうが良いと感じた。 入門書レベルでも、自力で興味を持って勉強をすることはかなり大きな価値があると思う。

大学の成績は一社で提出させられたけど、それ以外はまったく役に立っている感じはなかった。 趣味の開発もいいけど大学で真面目に勉強した分評価される世の中になってほしい。

合同誌主催を振り返って 〜花札合同運営の裏方〜

記事のモチベーション

おそらくこのタイトルでこのブログに到達する人ははじめましてだと思います。

合同誌を主催したり、競技プログラミングの問題を解いているkurenaif(f.くれなゐ)と申します。 この記事では二度合同誌を主催し、この機会を通じて様々な反省があったので自身の今後の様々な活動や、また他の合同誌主催をしたいと思っている方の参考に少しでもなればと思い、筆を取りました。

普段はこんなブログを書いています。もし興味があれば覗いてみてください。 かなり難しい問題を解説していると思います。

kurenaif.hatenablog.com

自己紹介・主催した合同誌の概要

自分が主催した合同誌は 東方花札風イラスト合同 〜幻想二十四の花かるた〜東方花札合同 百華蒐集 です。

前者の合同は両面ポストカードサイズ12枚のもので、各月に対して両面2人ずつ、その月のテーマの札をデザインしていただく合同で、後者の合同は名刺サイズ48枚で花札の各札に対して1枚ずつ割り当てて札をデザインしていただくといった合同になっています。

実はここだけの話なのですが、花札合同第1段の企画をした次点で第2段の予定は少し立てていました。(一定以上部数売れたり、満足する完成度のものができれば第2段をするつもりでした)

他の一般的な合同誌と違い、以下の点で大きく異なっていると思います。

  • 一人でも人数の過不足が合ってはならない。
  • 本ではないので、パッケージング等は自力で行わなければならない。
  • 本ではないので、編集は意外と楽
  • 全員同じテーマでイラストを書くわけではない

総じて、合同主催の負担としては編集は楽ですがその他でそれ相応の負担を被ることになります。 なので、私の主催した合同ではそのあたりの工夫が必要になってきますね。

合同誌主催として、最低限気をつけなければならないこと

これに関しては既存の記事が非常に優秀なので、私から言うことは特にありません。 特に、以下の記事が参考になりました。 自分から強調して言いたいことは

  • 合同参加者の工数もかなり多く取りますし、お金に関してはよく相談しましょう。
  • 人の募集から頒布まで、予算や期間についてきっちり目処を立ててから募集をかけよう。

の二点ですね。

www.clipstudio.net

blog.kasei-san.com

合同誌主催として次点で気をつけなければならないこと・心がけ

最低限のことを気をつけた上で、次点で気をつけなければならない点を紹介します。 必ずしも守らなくていいとは思いますが、気をつけるとスムーズにことが進むと思います。

文章は「短く・端的に」を 意識しすぎてはいけない

文章はよく「短く・端的に」とよく言うと思います。 間違ってはいませんし、重要なことだと思います。 特にtwitterのDMのようなインターフェイスでは特に短く書こうと努力すると思います。 しかしながら、私のように普段文章を書かない人は文章の推敲の際に短く書こうとしすぎて、必要な情報を削りすぎてしまう場合があります。 二度合同誌をした経験から、私のような素人が無理に短く書くよりも、長くても良いのでわかりやすいと思う文章のほうがミスが少なく、また質問も少なかったです。

「わかりやすさ」と「短さ」というのはトレードオフの関係にあると思います。悩んだ場合は「わかりやすさ」を優先しましょう。 大丈夫。 合同誌参加者は長くてもちゃんと読んでくれます。

25人規模を超える場合twitterのDMを過信しない

多くの参加者はtwitterのDMでの連絡を取りたい方が多いと思います。 自分も過去そう思い、二度の合同でそのようにしました。 しかしながら、大人数の合同では

twitterのDMはAPIがキツすぎる問題」

が発生します。「API」とはtwitterの機能を使う窓口みたいなものなのですが、あることをすれば窓口がスパム扱いするのです。 例えば、同じメッセージを何度も送るとスパム扱いだったり、一気にDMを送るとスパム扱いだったり… 体感25人あたりから""""キツい""""です…

大人数の場合はメールでの連絡を推奨しましょう

文章は5%前後くらいの確率で誤解されてると思え

この5%はあくまで僕の文章力です。人によって違うと思います。 このブログみたいな文章力だと5%くらいの人が誤読するってことですね。 合同誌を主催すると、自分がどれだけ物事を正確に伝える力があるのか定量的に測れてとてもいいです。 かなり貴重な経験ですね。

誤読・誤解・勘違いをしたまま進行することを防ぐために工夫をするのも合同誌主催の仕事です。 最低限、わかっていてほしいことは参加者自身の手で二重確認させるように設計しましょう。

余裕があればメッセージを共有する前に参加者に確認してもらうといいですね。

参加者自身のチェック: 募集のチェックの例

以下は募集の際に設けたチェックボックスです。 要項にも書いているのですが、募集の際に最も重要な「金銭面・誤解されやすい点・締め切り」をこのような形で二重チェックさせています。

f:id:kurenaif:20190102214319p:plain

参加者自身のチェック: 配置のチェックの例

配置ミスはこの合同で最も気をつけなければならない点です。 スプレッドシートを用いて参加者全員の配置をお互い確認できるようにし、 また確認した配置を自らDMで復唱していただくことで、さらなるミスを防止しています。

そのようなDMが来ない場合はリプライとかで促します。 ここは妥協すると本当にめんどくさくなるので最大限注意しました。

あとがきと原稿の提出は同時にお願いしよう

タイトル通りです。 「あとがきは後でもいい」スタイルは絶対忘れます。 これは先人の知恵でもあり僕もやったら忘れました。 ごめんなさい。

提出要項ウェブページを作ろう

最終的にウェブサーバーを借りて合同誌の宣伝をするのですから、早い間にウェブサーバーを借りて参加者のためのページを作っちゃいましょう。 以下が自分の作ったページの例ですね。

ここを見ると合同の提出に関するすべてが細かな注意点を含めて書いてあります。 質問等もここにまとめておき、主催や参加者の質問をする手間を減らします。

大きな手間ではありません。 メールやDMなどで内容を書くよりも、以下のメリットがあります。

  • twitterが凍結等されてもウェブページさえあれば原稿の提出方法の確認ができる
  • 太字や色を使うことができるのでDMよりも見やすい
  • 画像等を使った丁寧な説明ができる
  • リンクを貼ることができる(twitterDMではかなり制限されます)
  • テンプレートをサーバー上に置くことでいつでもダウンロードすることができる
  • twitterのDMと違い、重要性の低い内容を送信するリスクが少ない(メールやDMではどうでもいい情報のせいで重要な情報が流れてしまいます)

あとは「内容を更新したのでウェブページ見てね」とDMで告知したら終わりです。 参加者的にも主催者的にもいいですね。

f:id:kurenaif:20190102215425p:plain

f:id:kurenaif:20190102220757p:plain

メッセージの書き方

基本的に論文書くときと同じ心得だと思っています。誰しもが読んで誤解のない文章を書くべきです。 以下の本がおすすめです。あまり内容は数学的ではないので、是非読んでみてください。

メッセージのスタイル

メッセージはtwitterDM、pixivメッセ、メールで以下のスタイルで書きました。

(伝えたい内容の概要)
締め切りまであとN日になりました。hogehogeについて、以下2点確認をよろしくお願いします。

(伝えたい内容の箇条書き)
1. XXXXについて
2. YYYYについて

1. XXXXについて
(XXXXについての詳細)
(やってほしい理由)

2. YYYYについて
(YYYYについての詳細)
(やってほしい理由)

以上2点、よろしくお願いします。

僕の場合文章が長くなりがちなので、構造的に内容をわけられるよう考慮した結果がこうなりました。 このスタイルの書き方の利点としては、以下の4点があります。

  1. 詳細や理由を添えることで、誤読や勘違いを減らす
  2. 内容が箇条書きで書かれており、メッセージの概要がひと目でわかる。
  3. 番号をつけることで、内容の参照が可能になる
  4. 伝えたい内容の個数がわかるので、理解した内容の確認が容易になる
1. 詳細や理由を添えることで、誤読や勘違いを減らす

書いてあるとおり、作業内容を書くだけではなく、や「なぜそれが必要なのか」を付け加えます。

ただただ「端的に短く」やってほしいことを書いてもらってもいいのですが、「なぜそうしなければならないか」という「冗長性」を付け加えることで、誤読のリスクを減らす役割があります。

これがあるのとないのでは体感で相当な誤読が減りました。

2. 内容が箇条書きで書かれており、メッセージの概要がひと目でわかる。

1.で詳細や理由を付け加えたことにより、文章が長くなってしまいました。この冗長性は必要なもので削ってはいけないものです。 でも、長い文章はやはり短く書かれたものよりも読みにくいです。

なので、内容を端的に短く書いた目次のようなものを追加しましょう。それが

1. XXXXについて
2. YYYYについて

この両方を書き、番号でお互いにリンクさせることで短い箇条書きの文章と長い文章の利点の両立を図っています。

3. 番号をつけることで、内容の参照が可能になる

あえて難しい言葉で書いてみました。 2.で目次を作りましたが、例えば「3番がわかりにくいなぁ」と思ったら「3番の詳細(今読んでいるこれ)」を読めばよいのです。

これが3番の伝えたい内容です。過去の文章で

1.で詳細や理由を付け加えたことにより

であったり

2.で目次を作りましたが

みたいに番号で他の内容を参照することができます。これが「・」の箇条書きではなく番号の箇条書きを使う理由の一つです。

4. 伝えたい内容の個数がわかるので、理解した内容の確認が容易になる

文章を読み終わった後、最後に自分の理解した内容を再確認します。 そのときに数がわかると便利です。

ここまでの内容を読んだあなたは最後に

1. 詳細や理由を添えることで、誤読や勘違いを減らす
2. 内容が箇条書きで書かれており、メッセージの概要がひと目でわかる。
3. 番号をつけることで、内容の参照が可能になる
4. 伝えたい内容の個数がわかるので、理解した内容の確認が容易になる

の4点が理解できたことを確認するのではないでしょうか? 3点しか理解してなかったらそれは理解が不足しているということです。

最後に

以上N点、よろしくお願いします。

のように書き加えることで、何点相手に理解してほしいかを強調する文章を書いています。

まとめ

これがこのスタイルの文章の書き方の利点です。 大学のサークルや合同誌等の連絡で色々試行錯誤した結果これが一番誤解が少なかったっぽかったので私はこのスタイルを採用しています。

おわりに

1回目の反省点を活かし2回目では様々な工夫をしてかなりの誤読や誤解を減らせたと思います。 この記事ではその中でも特に効果が合ったと思う部分を紹介させていただいています。

今回の合同では誤読しても問題ない部分では少しだけトラブルが起き、問題がある部分は全くトラブル等は起きなくスムーズに進行できたと自負しています。

しかし、まだまだトラブルが全く起きなかったわけではないので軽微なミスも0にするようマネジメントを模索していきたいと思います。 こういう知見を集めるために合同誌主催が集まるオフ会みたいなの企画してみたいですね。