ターミナルをハックする方法

[フューチャー Advent Calendar 2019](https://qiita.com/advent-calendar/2019/future) 17日目の記事です。

最近は Go にはまっていますが、今回は趣向を変えてみます。前のプロジェクトでたまに使っていた、ターミナルを共有する [ttycopy](https://github.com/d-tsuji/ttycopy) というツールの紹介をしようと思います。

スーパーエンジニアがサーバ上に ssh してなんやかんやしているときに、ちょっと画面を ~~ハックしたい~~ 見たい ...! みたいなシーンがあります。あとは、サーバ上でコマンドが正しく入力されているかとかのダブルチェックとかで画面を確認するとか。物理的な距離があったりすると、モニタに画面を映すことも難しいですよね。[^1]

[^1]: もちろん、使うときは断ってから使いましょう。

画面をみせてもらうのも一つの方法ですが、ちょっとしたツールで参照できるとスマートですよね。Perl だと [ttylog](https://github.com/gitpan/ttylog) というツールがあったりします。私は ttylog にインスパイアされて、シェルで動く [ttycopy](https://github.com/d-tsuji/ttycopy) を作りました。シェルで動作するので ttycopy の実装をコピー&ペーストしてカジュアルに動かすこともできます。

### サンプル動作

以下のように動作をします。ちょっとわかりにくいですが、あるサーバに root ユーザと tsuji ユーザでログインしていて root ユーザで ttycopy を実行しています。ターミナルで途中から tsuji ユーザでログインしているターミナルが表示されていることが分かります。

![ttycopy](https://user-images.githubusercontent.com/24369487/68991645-2bc1c780-08a4-11ea-81c8-3f4af99bedb2.gif)

ttylog がどのように動作しているかは、[ターミナル画面を勝手に共有して他人の作業を覗いてみる](https://qiita.com/nashiox/items/f9b026b7433fcf984d45) などでも紹介されていますが、基本的な処理の流れは以下のとおりです。

1. tty のログインプロセスの PID を調べる
2. PID に対し strace コマンドを使ってシステムコールをのぞき見る
3. strace で出力される read システムコールの文字列を整形して表示する

ttycopy では上記の手順に沿ってシェルで実装しました。

## シェル(Bash)での実装

### 1.tty のログインプロセスの PID を調べる

シェルでの実装は以下です。

```shell
pid=`ps fauwwx | grep sshd.*${tty} | grep -v grep | sed -e 's/^[a-zA-Z0-9]\+[ \n\r\f\t]\+\([0-9]\+\).*/\1/'`
```

これは grep したときに取得できる以下のようなプロセスツリーをの文字列を正規表現を用いて PID をキャプチャしています。正規表現でスッキリかけますね。

```
[tsuji@localhost ~]$ ps fauwwx | grep sshd.
root      1531  0.0  0.1  82568  6236 ?        Ss   15:29   0:00 /usr/sbin/sshd -D
root      2830  0.0  0.2 149824  8916 ?        Ss   15:29   0:01  \_ sshd: root@pts/0
root     13315  0.0  0.2 158980 10280 ?        Ss   18:58   0:00  \_ sshd: root@notty
root     14956  0.0  0.2 154512  9352 ?        Ss   20:10   0:00  \_ sshd: tsuji [priv]
tsuji    14959  0.0  0.1 154512  4092 ?        S    20:10   0:00      \_ sshd: tsuji@pts/1
tsuji    15012  0.0  0.0 112672  2268 pts/1    S+   20:11   0:00              \_ grep --color=auto sshd.
```

### 2.PID に対し strace コマンドを使ってシステムコールをのぞき見る

シェルでの実装は以下の部分です。

```shell
strace -e read -s16384 -q -xx -p ${pid} 2>&1
```

やっていることは [strace](http://man7.org/linux/man-pages/man1/strace.1.html) を用いて、先程取得した tty に紐づくPID が発行する read システムコールを取得します。read システムコール以外にもいろいろなシステムコールが呼び出されるため、strace のオプション `-e read` で read システムコールのみ抽出しています。

read システムコールは以下で定義されているシステムコールでした。strace で取得できる結果も以下のようになっています。

```syscall
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
```

例えば参照先のターミナルで以下のようなコマンドを発行したとします。

- 取得元のターミナル

```
[tsuji@localhost gomi]$ hoge
bash: hoge: コマンドが見つかりませんでした...
[tsuji@localhost gomi]$
```

このとき `strace -p ${PID} -e read` で read システムコールを参照すると以下のように出力されます。

- strace しているターミナル

```
[root@localhost ttycopy]# strace -p 14959 -e read
strace: Process 14959 attached
read(3, "\0\0\0\20\356\202\375C&&\357q\276\210pZ)\300\26M\357T\313\303k\6p\232\351\263\32\224"..., 16384) = 36
read(11, "h", 16384)                    = 1
read(3, "\0\0\0\20\235\230\204Ud\36)\370\266\233\362\305\2219\253g\335M\23\212\374h\250i@\235/\216"..., 16384) = 36
read(11, "o", 16384)                    = 1
read(3, "\0\0\0\20\324\357\304\vn\357BbW\241m\220yS\236\362\301\337\337\237c\203\245\223\221\253;,"..., 16384) = 36
read(11, "g", 16384)                    = 1
read(3, "\0\0\0\20\344\215\235\300\226\236\0\323\376\r\217,\257\322\326w\323R\264\3}\266\7q\315\215\344\346"..., 16384) = 36
read(11, "e", 16384)                    = 1
read(3, "\0\0\0\20^``\333\263h\372Z\336\335Y2\250\203\335\221\372faj\177\260f\302Sb\35\354"..., 16384) = 36
read(11, "\r\n", 16384)                 = 2
read(11, "bash: hoge: \343\202\263\343\203\236\343\203\263\343\203\211\343\201\214\350\246\213\343\201"..., 16384) = 62
read(11, "\33]0;tsuji@localhost:~/gomi\7[tsuj"..., 16384) = 51
^Cstrace: Process 14959 detached
```

上の例だと `strace: Process 14959 attached` や `strace: Process 14959 detached` という文字列が表示されていますが、この出力を抑制するために strace のオプション `-q` を用いています。また、表示するときに扱いやすいように `-x` オプションで文字列を 16 進数に変換しています。

### 3.strace で出力される read システムコールの文字列を整形して表示する

目標は 2 の手順で取得した read システムコールの第 2 引数の内容を表示させることです。シェルでの実装は以下です。

```shell
sed -une "s/^read([0-9]\+, \"\(.*\)\".*/echo -n \$'\1'/p" | bash
```

まず前半部分 `sed -une "s/^read([0-9]\+, \"\(.*\)\".*/echo -n \$'\1'/p"` です。正規表現で read システムコールの第 2 引数をキャプチャして、それに対して `echo -n` と `$''` の文字列を付与しています。よって以下のような文字列を取得できます。

- 取得元のターミナルに出力されている文字列

```
[tsuji@localhost gomi]$ cat hoge
hoge
[tsuji@localhost gomi]$
```

- strace して加工後に出力されるターミナルの文字列

```
echo -n $'\x00\x00\x00\x10\xc6\x1e\x56\xc6\x1f\x11\x7e\x57\x11\xaf\xdb\x2a\x91\x32\x84\x8e\x6e\x5b\x12\xc1\x72\x94\x36\x17\x12\xbb\x7c\xab\x4b\xdd\x19\x33'
echo -n $'c'
echo -n $'\x00\x00\x00\x10\x5d\x68\x72\x7c\x74\xca\x3c\xd1\x57\xfc\x14\x7d\x55\x34\x66\x15\x03\xcb\x26\x7c\x17\xbc\x7f\x7a\xf5\x25\x40\xed\xa8\x21\x39\xb3'
echo -n $'a'
echo -n $'\x00\x00\x00\x10\x05\x3d\x4a\xc2\x76\x1c\xd4\x23\x2a\x17\xc6\xa1\x1c\xf2\xdb\x14\x75\x1c\x7d\xb7\x21\xfb\xfc\xcd\x2d\x5c\xef\x06\x6c\x97\x01\x28'
echo -n $'t'
echo -n $'\x00\x00\x00\x10\x66\xb0\x8c\x40\x10\xa6\xf3\x9b\x36\x75\xd5\xc1\x65\x63\x94\x4f\x77\xd9\x10\x6d\xcf\xbb\x48\xed\x8b\x43\x58\x20\x54\x08\xde\x9b'
echo -n $' '
echo -n $'\x00\x00\x00\x10\x60\x6e\xb6\x06\x43\x16\xf5\x75\x89\x90\xb6\x42\x2c\xfe\x8b\x97\xae\xad\x47\x26\xf9\x39\xfc\xd2\x84\x37\xde\x0d\xe5\x32\xbc\x80'
echo -n $'h'
echo -n $'\x00\x00\x00\x10\x00\xe4\x3d\xb7\xd9\x79\x2e\x46\x80\xd5\xa5\xc2\xa7\x9a\xc7\x0c\xe1\x58\x7b\xd5\x97\xff\x00\xab\x72\x51\xa4\xbb\xab\x7d\xd1\xaf'
echo -n $'o'
echo -n $'\x00\x00\x00\x10\xf8\x1d\xce\xe6\x7f\x1a\x43\x94\xa2\xde\x3d\x3c\xb5\xe9\xb9\x94\x39\x43\x63\xfd\xa9\x1f\x45\x83\x64\x5c\x3a\xdf\xa8\x1a\xa4\x86'
echo -n $'g'
echo -n $'\x00\x00\x00\x10\x90\x69\x42\xa9\x48\x99\x0c\x52\xe9\x49\xbd\x4e\xa5\x17\x01\xff\xac\xec\x29\x75\x2c\xc0\x2b\x7c\x07\x85\xf2\x2f\xce\x71\x8f\x46'
echo -n $'e'
echo -n $'\x00\x00\x00\x10\xd9\x91\x38\x08\x0b\x95\x78\xd4\x80\x51\xa2\xe8\xef\x20\x06\x45\xa9\x3c\xf0\xa3\x10\x7d\x06\x32\x2d\x31\x53\x57\x40\x77\x1b\x29'
echo -n $'\r\n'
echo -n $'\x68\x6f\x67\x65\x0d\x0a\x1b\x5d\x30\x3b\x74\x73\x75\x6a\x69\x40\x6c\x6f\x63\x61\x6c\x68\x6f\x73\x74\x3a\x7e\x2f\x67\x6f\x6d\x69\x07'
echo -n $'[tsuji@localhost gomi]$ '
```

`echo -n` の `-n` オプションは解釈する際に改行されないようするためです。Bash の `$''` の形式は以下の効果があります。Man page からの抜粋です。[^2]

[^2]: https://linuxjm.osdn.jp/html/GNU_bash/man1/bash.1.html

> $'string' の形式を持つ単語は特殊な扱いを受けます。 この単語は string に展開され、 それから ANSI C 標準で仕様が決められている、 バックスラッシュでエスケープされている文字に置き換えられます。 バックスラッシュエスケープシーケンスは、 (もし存在すれば) 以下のようにデコードされます:

これを用いることで、文字列が保持している意味に展開することができます。

あとは `| bash` で `echo` の文字列をシェルに解釈させることで、文字列を表示することができます。

## まとめ

strace で read システムコールの引数の文字列を取得し、リアルタイムに復元することでターミナルをカジュアルに共有することができました。他の方の作業をリアルタイムで見れるので教育的なツールではないかと思います。ターミナルをハックしてみたい方はぜひ使ってみてください。

よかったら [ttycopy](https://github.com/d-tsuji/ttycopy) に star もつけてもらえるとモチベーションが上がります:v: