Hatena::ブログ(Diary)

知らないけどきっとそう。 RSSフィード

 

2017-07-24 Vuls で EC2 をバルスされないために(実践編) このエントリーを含むブックマーク

これは Amazon EC2 Systems Manager Advent Calendar 2017 の -129 日目の記事です。

  • Vuls を実行する IAM ユーザ Bob が、対象 EC2 インスタンス i-1234567890abcdef0 の管理者ではない場合、アカウントの作成や SSH 公開鍵の登録や vuls-ssh-command.shインストールを依頼する必要があります
  • それでは不便なので、ユーザが Amazon EC2 Systems Manager を使用して、これらを自身でおこなえる方法を用意します
$ aws iam create-role --role-name EC2RoleSample --assume-role-policy-document '{"Version":"2012-10-17","Statement":{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}}'
$ aws iam create-instance-profile --instance-profile-name EC2RoleSample
$ aws iam add-role-to-instance-profile --instance-profile-name EC2RoleSample --role-name EC2RoleSample
$ aws ec2 associate-iam-instance-profile --instance-id i-1234567890abcdef0 --iam-instance-profile Name=EC2RoleSample
    • EC2 インスタンスにアタッチされた IAM ロールに AmazonEC2RoleforSSM ポリシーをアタッチする
$ aws iam attach-role-policy --role-name EC2RoleSample --policy-arn arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM
[ec2-user ~]$ sudo yum install amazon-ssm-agent
[ec2-user ~]$ sudo start amazon-ssm-agent
  • 前提を満たしたら、やっていきます
$ terraform apply
var.vuls_roles
  Enter a value: []

var.vuls_users
  Enter a value: ["Bob"]

...
  • 晴れて IAM ユーザ Bob は EC2 インスタンス i-1234567890abcdef0 に Vuls のためのユーザを作成できるようになりました
$ aws sts get-caller-identity --output text --query Arn
arn:aws:iam::123456789012:user/Bob
$ aws ssm send-command --document-name CreateVulsUser --instance-ids i-1234567890abcdef0 --parameters authorizedkeys="$(cat $HOME/.ssh/id_rsa.pub)"
$ ssh -t vuls@203.0.113.1 'stty cols 1000; curl --max-time 1 --retry 3 --noproxy 169.254.169.254 http://169.254.169.254/latest/meta-data/instance-id'
i-1234567890abcdef0Connection to 203.0.113.1 closed.

2017-07-04 Vuls で EC2 をバルスされないために(簡易版) このエントリーを含むブックマーク

$ sudo iptables -I OUTPUT -d 169.254.169.254 -j REJECT --reject-with icmp-admin-prohibited
  • 禁止されていることを確認
$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role_name
curl: (7) Failed to connect to 169.254.169.254 port 80: ホストへの経路がありません
  • root ユーザ (UID 0) による通信を許可するルールを追加
$ sudo iptables -I OUTPUT -d 169.254.169.254 -m owner --uid-owner 0 -j ACCEPT
$ sudo curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role_name
{
  ...
}
  • ec2-user ユーザ (UID 500) も許可します
$ sudo iptables -I OUTPUT -d 169.254.169.254 -m owner --uid-owner 500 -j ACCEPT
  • sudo なしで取れました
$ id
uid=500(ec2-user) gid=500(ec2-user) groups=500(ec2-user),10(wheel)
$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role_name
{
  ...
}
  • 最終的なルールです
$ sudo iptables -L OUTPUT
Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
ACCEPT     all  --  anywhere             instance-data.ap-northeast-1.compute.internal  owner UID match ec2-user
ACCEPT     all  --  anywhere             instance-data.ap-northeast-1.compute.internal  owner UID match root
REJECT     all  --  anywhere             instance-data.ap-northeast-1.compute.internal  reject-with icmp-admin-prohibited
[Jul  3 16:22:49] DEBUG [amazon] execResult: servername: amazon
  cmd: /usr/bin/ssh -tt -o StrictHostKeyChecking=yes -o LogLevel=quiet -o ConnectionAttempts=3 -o ConnectTimeout=10 -o ControlMaster=no -o ControlPath=none vuls-user@52.199.26.3 -p 22 -i /root/.ssh/id_rsa -o PasswordAuthentication=no stty cols 1000; curl --max-time 1 --retry 3 --noproxy 169.254.169.254 http://169.254.169.254/latest/meta-data/instance-id
  exitstatus: 0
  stdout: i-0bf0d78a268f80335
  stderr:
  err: %!s(<nil>)
[Jul  3 16:22:49]  INFO [localhost] (1/1) amazon is running on aws
[Jul  3 16:14:56] DEBUG [amazon] execResult: servername: amazon
  cmd: /usr/bin/ssh -tt -o StrictHostKeyChecking=yes -o LogLevel=quiet -o ConnectionAttempts=3 -o ConnectTimeout=10 -o ControlMaster=no -o ControlPath=none vuls-user@52.199.26.3 -p 22 -i /root/.ssh/id_rsa -o PasswordAuthentication=no stty cols 1000; curl --max-time 1 --retry 3 --noproxy 169.254.169.254 http://169.254.169.254/latest/meta-data/instance-id
  exitstatus: 7
  stdout: Warning: Transient problem: timeout Will retry in 1 seconds. 3 retries left.
curl: (7) Failed to connect to 169.254.169.254 port 80: No route to host

  stderr:
  err: %!s(<nil>)
[Jul  3 16:14:56]  INFO [localhost] (1/1) amazon is running on other

2017-06-28 Vuls で EC2 をバルスされないために このエントリーを含むブックマーク

  • 以下に Vuls の単純化した使用方法を示します
$ ssh-keygen -N '' -f $HOME/.ssh/id_rsa_vuls
$ cat $HOME/.ssh/id_rsa_vuls.pub
ssh-rsa ...
ec2-user$ sudo adduser -m vuls
ec2-user$ sudo su vuls
vuls$ mkdir -m 700 /home/vuls/.ssh
vuls$ echo 'ssh-rsa ...' > /home/vuls/.ssh/authorized_keys
  • Vuls の設定ファイルを作成
$ mkdir vuls
$ cat <<EOD > vuls/config.toml
[servers]

[servers.amazon]
host        = "203.0.113.1"
port        = "22"
user        = "vuls"
keyPath     = "/root/.ssh/id_rsa_vuls"
EOD
  • Vuls のスキャンを Docker で実行
$ docker run --rm -it -v $HOME/.ssh:/root/.ssh:ro -v $PWD:/vuls -v $PWD/vuls-log:/var/log/vuls vuls/vuls scan
$ for i in `seq 2002 $(date +"%Y")`; do docker run --rm -it -v $PWD:/vuls -v $PWD/go-cve-dictionary-log:/var/log/vuls vuls/go-cve-dictionary fetchnvd -years $i; done
  • Vuls のレポートを表示
$ docker run --rm -it -v $HOME/.ssh:/root/.ssh:ro -v $PWD:/vuls -v $PWD/vuls-log:/var/log/vuls vuls/vuls report
  • これによって、ユーザが認証情報を管理する必要がなくなるため、アプリケーションサーバとしての EC2 に、様々な権限を持つロールをアタッチするケースが考えられます
ec2-user$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role_name
$ ssh -t -i $HOME/.ssh/id_rsa_vuls vuls@203.0.113.1 'curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role_name'
{
  "Code" : "Success",
  "LastUpdated" : "2017-06-27T09:17:33Z",
  "Type" : "AWS-HMAC",
  "AccessKeyId" : "ASIAJHNLJH7SISLPSOMA",
  "SecretAccessKey" : "...",
  "Token" : "...",
  "Expiration" : "2017-06-27T15:39:38Z"
}Connection to 203.0.113.1 closed.
    • 取得できました
  • したがって、SSH 秘密鍵 id_rsa_vuls は、ロールに応じて、高いレベルで保護される必要があります
  • しかし、その認識にギャップがあると思われ、次のように扱われる懸念があります
    • 脆弱性スキャンを自動実行させるため、秘密鍵パスフレーズを設定しない
    • 秘密鍵が置かれる、スキャン実行ホストのセキュリティレベルが低い、アクセス制御が徹底されていない
    • ロールによる権限を得られるべきではない外部のメンバー等が、秘密鍵へのアクセス権を持ち、スキャンを実行できる
  • vuls アカウントに設定することで、Vuls の実行のみが許可され、その他のコマンドは無視されます
vuls$ curl https://raw.githubusercontent.com/asannou/vuls-ssh-command/master/amazon-linux.sh > /home/vuls/.ssh/vuls-ssh-command.sh
vuls$ chmod +x /home/vuls/.ssh/vuls-ssh-command.sh
vuls$ echo 'command="/home/vuls/.ssh/vuls-ssh-command.sh" ssh-rsa ...' > /home/vuls/.ssh/authorized_keys
$ ssh -t -i $HOME/.ssh/id_rsa_vuls vuls@203.0.113.1 'curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role_name'
Connection to 203.0.113.1 closed.
  • たぶん抜け穴あり、Amazon LinuxUbuntu しか用意していませんが、よろしければお使いください

2017-06-18 Slack ストーキング このエントリーを含むブックマーク

  • message event | Slack は、いくつかのサブタイプがあり、メッセージの変化を表現しています
$ node index.js | jq --unbuffered 'select(.subtype=="message_deleted")'
  • ユーザ @bsannou がチャンネル #twitter で "Despite the constant negative press covfefe" というメッセージを削除した様子です
{
  "type": "message",
  "deleted_ts": "1497783180.894743",
  "subtype": "message_deleted",
  "hidden": true,
  "channel": {
    "id": "C5VCH7HFV",
    "name": "#twitter"
  },
  "previous_message": {
    "type": "message",
    "user": {
      "id": "U5JPRSD6U",
      "name": "@bsannou"
    },
    "text": "Despite the constant negative press covfefe",
    "ts": "1497783180.894743"
  },
  "event_ts": "1497783204.895728",
  "ts": "1497783204.895728"
}
$ node index.js | jq --unbuffered 'select(.subtype=="message_changed")'
  • 様子です
{
  "type": "message",
  "message": {
    "type": "message",
    "user": {
      "id": "U5JPRSD6U",
      "name": "@bsannou"
    },
    "text": "あなたはとてもクサレ脳ミソですね",
    "edited": {
      "user": {
        "id": "U5JPRSD6U",
        "name": "@bsannou"
      },
      "ts": "1497188145.000000"
    },
    "ts": "1497188126.496907"
  },
  "subtype": "message_changed",
  "hidden": true,
  "channel": {
    "id": "C5JT9C849",
    "name": "#vip"
  },
  "previous_message": {
    "type": "message",
    "user": {
      "id": "U5JPRSD6U",
      "name": "@bsannou"
    },
    "text": "あなたはとてもド低能ですね",
    "ts": "1497188126.496907"
  },
  "event_ts": "1497188145.497849",
  "ts": "1497188145.497849"
}
$ node index.js | jq --unbuffered 'select(.presence=="active") | .now=now'
  • 様子
{
  "type": "presence_change",
  "presence": "active",
  "user": {
    "id": "U5JPRSD6U",
    "name": "@bsannou"
  },
  "now": 1497786620.436581
}
  • 深夜に active がいるとオッとなる
$ mkfifo fifo
$ node index.js < fifo | jq --unbuffered -c 'select(.presence=="active") | {"type":"message","text":"進捗どうですか?activeだから居ますよね??","channel":{"name":.user.name}}' > fifo
$ node index.js | jq --unbuffered 'select(.type=="user_typing")'
{
  "type": "user_typing",
  "channel": {
    "id": "C5JT9C849",
    "name": "#vip"
  },
  "user": {
    "id": "U5JPRSD6U",
    "name": "@bsannou"
  }
}
  • うざみ
$ mkfifo fifo
$ node index.js < fifo | jq --unbuffered -c 'select(.type=="user_typing") | {"type":"message","text":("ちょっと待って!今 "+.user.name+" が何か言おうとしてる!"),"channel":{"name":.channel.name}}' > fifo
  • 今日書きたいことはこれくらいです

2017-05-28 Node.js Stream からいつの間にか Slack クライアントができた このエントリーを含むブックマーク

  • Mastodon は観察専門なので、使用頻度の高い Slack を題材とします
let ws;
const url = "https://slack.com/api/rtm.connect?token=xoxp-0000000000-0000000000-000000000000-0123456789abcdef0123456789abcdef";
const stream = new require("stream").PassThrough();
require("https").get(url, res => {
  res.on("data", chunk => {
    const obj = JSON.parse(chunk);
    ws = new (require("ws"))(obj.url);
    ws.on("message", data => stream.write(data + "\n"));
  });
});
stream.pipe(process.stdout);
$ node index.js
{"type":"hello"}
{"type":"reconnect_url","url":"wss://mpmulti-0a1g.slack-msgs.com/websocket/ZvwORwLc1uFhgMHJAZva-04tZg2Lp5iJEclZ8vnOCctMTnJC64LQvb7MZvgJNifnscAGXvki2UAEcJpnlwFwCY031HGGe-NizIM5cZZKfdMQcO9kbXBhb8LkUKQnhR3bx-Z5hxefvQgsVQVgZwSFO9huhgXo3cBvjCc44YTZklY="}
{"type":"presence_change","presence":"active","user":"U5HCFCV5W"}
...
  • ここに、書き込むと WebSocket にデータを送る Writable Stream のコードを追加して stdin と繋ぎます
const writable = new require("stream").Writable({
  write: (chunk, encoding, callback) => {
    ws.send(chunk.toString());
  }
});
process.stdin.pipe(writable);
  • stdin へチャンネルにメッセージを投稿するための JSON を入力します
$ node index.js
...
{"type":"message","channel":"C5JT9C849","text":"test"}
  • 直後に成功を表す JSON が送られてきたら、ブラウザ等から確認すると反映されていることがわかります
{"ok":true,"reply_to":0,"ts":"1495991511.232093","text":"test"}
  • 送られてくる JSON は channel や user の情報が "C5JT9C849" のような id となっていて扱いづらく、よって name に変換する Transform Stream を作って間にはさみます
const url = "https://slack.com/api/rtm.start?token=xoxp-0000000000-0000000000-000000000000-0123456789abcdef0123456789abcdef";
const stream = new require("stream").PassThrough();
require("https").get(url, res => { ... });
const transform = new require("stream").Transform({
  transform: function(chunk, encoding, callback) { ... }
});
stream.pipe(transform).pipe(process.stdout);
{"type":"message","user":{"id":"U5HCFCV5W","name":"@asannou"},"text":"test","ts":"1496083043.865560","channel":{"id":"C5JT9C849","name":"#vip"}}
{"type":"message","user":{"id":"USLACKBOT","name":"@slackbot"},"text":"Hello, I’m Slackbot. I try to be helpful. (But I’m still just a bot. Sorry!) *Type something* and hit _enter_ to send your message.","ts":"1495732144.332015","channel":{"id":"D5J44CUDS","name":"@slackbot"}}
{"type":"message","user":{"id":"USLACKBOT","name":"@slackbot"},"text":"Pleasure to meet you. Let me show you a couple things about Slack.","ts":"1495732157.337778","channel":{"id":"D5J44CUDS","name":"@slackbot"}}
170525170904.332015 [@slackbot] (@slackbot) Hello, I’m Slackbot. I try to be helpful. (But I’m still just a bot. Sorry!) *Type something* and hit _enter_ to send your message.
170525170917.337778 [@slackbot] (@slackbot) Pleasure to meet you. Let me show you a couple things about Slack.
  • タイムスタンプ部分 "%y%m%d%H%M%S.%6N" が苦しいですが、生 JSON よりはマシでしょう
    • 出力のすべては JSON であるがゆえに、特定のチャンネルをフィルタリングするなど、jq の能力が発揮されます
$ node index.js | jq -c --unbuffered 'select(.channel.name != "#general")'
  • 入力についても、手打ち JSON は死ぬので jq に頼ります
$ echo "test" | jq -R -c --unbuffered '{type:"message",channel:{name:"@slackbot"},text:.}'
{"type":"message","channel":{"name":"@slackbot"},"text":"test"}
  • 入力と出力を jq で挟むことにより、ちょっとだけまともな Slack クライアントになりました
$ jq -R -c --unbuffered '{type:"message",channel:{name:"@slackbot"},text:.}' | node index.js | jq --unbuffered -r -f format.jq
help!
170528175842.970929 [@slackbot] (@asannou) help!
170528175842.970933 [@slackbot] (@slackbot) I can help by answering simple questions about how Slack works. I'm just a bot, though! If you need more help, try our <https://get.slack.help/hc/en-us/|Help Center> for loads of useful information about Slack ― it's easy to search! Or simply type */feedback* followed by your question or comment, and a human person will get back to you. :smile:
$ ./rwlap.js "jq -R -c --unbuffered '{type:\"message\",channel:{name:\"@slackbot\"},text:.}' | node index.js | jq --unbuffered -r -f format.jq"
  • Readline は、他にもタブキーによる入力補完もサポートしているため、公式クライアントのような、ユーザ名の補完なども実現可能です
$ ./rwlap.js "jq -R -c --unbuffered '{type:\"message\",channel:{name:\"@slackbot\"},text:.}' | node index.js | jq --unbuffered -r -f format.jq | ./highlight.js asannou"

170528175842.970929 [@slackbot] (@asannou) help!

  • もちろん、同様のことができるコマンドで、代替してもかまいません

f:id:asannou:20170530115133g:image

  • 個人的に Slack の機能はこれで足りているので、しばらく使ってみようと思います

2017-04-25 Mastodon のタイムラインをターミナルに流す このエントリーを含むブックマーク

  • Node.js の Stream の学習を兼ねて、これを様々なターミナルに出力できるようにする
    • 「Stream を制するものは、 Node.js を制す」らしい
$ node -e 'new (require("ws"))("ws://friends.nico/api/v1/streaming/?access_token=...&stream=public").on("message", (data, flags) => console.log(data))'
{"event":"update","payload":"{\"id\":1920985,\"created_at\":\"2017-04-24T16:10:03.248Z\",\"in_reply_to_id\":null,\"in_reply_to_account_id\":null,\"sensitive\":false,\"spoiler_text\":\"\",\"visibility\":\"public\",\"application\":{\"name\":\"Web\",\"website\":null},\"account\":{...
  • これを parse() して Stream を作成し transform して出力
const stream = new require("stream").Transform({
    objectMode: true,
    transform: function(chunk, encoding, callback) { ... }
});

new (require("ws"))("ws://friends.nico/api/v1/streaming/?access_token=...&stream=public")
    .on("message", (data, flags) => stream.write(JSON.parse(data)));

stream.pipe(process.stdout);

f:id:asannou:20170425023534g:image:w576

  • Stream は TCP Socket に pipe() でつなぐことができる
    • 標準入力の Stream を pipe() して、ポート 12345 に接続すると ping の結果が流れてくるサンプル
$ ping localhost | node -e 'process.stdin.resume();require("net").createServer(socket => process.stdin.pipe(socket)).listen(12345);'
$ telnet localhost 12345
Trying ::1...
Connected to localhost.
Escape character is '^]'.
64 bytes from 127.0.0.1: icmp_seq=16 ttl=64 time=0.065 ms
64 bytes from 127.0.0.1: icmp_seq=17 ttl=64 time=0.056 ms
64 bytes from 127.0.0.1: icmp_seq=18 ttl=64 time=0.095 ms
64 bytes from 127.0.0.1: icmp_seq=19 ttl=64 time=0.049 ms
64 bytes from 127.0.0.1: icmp_seq=20 ttl=64 time=0.054 ms
^]
telnet> Connection closed.
  • pipe() によって、何回でも Stream の分岐や合流が可能
    • クライアントが複数接続しても、それぞれに同じデータを送ることができる
    • resume() しておくことで pipe() されているものが何もない時のデータは捨てる
$ telnet tootcat.0j0.jp
$ nc tootcat.0j0.jp 7007
$ telnet tootcat.0j0.jp 7008
$ telnet tootcat.0j0.jp 7009
$ telnet tootcat.0j0.jp 7010

  • 未加工の JSON を受信できるようにした
$ nc tootcat.0j0.jp 7006 | head -c 1000
{"id":6021698,"created_at":"2017-05-06T07:18:52.994Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","application":{"name":"Web","website":null},"account":{"id":0,"username":"username","acct":"acct","display_name":"display_name","locked":false,"created_at":"2017-04-21T12:07:59.324Z","followers_count":0,"following_count":0,"statuses_count":1,"note":"note","url":"https://friends.nico/url","avatar":"avatar","avatar_static":"avatar_static","header":"header","header_static":"header_static","nico_url":"nico_url"},"media_attachments":[],"mentions":[],"tags":[],"uri":"tag:friends.nico,2017-05-06:objectId=6021698:objectType=Status","content":"content1\r\ncontent2","url":"url","reblogs_count":0,"favourites_count":0,"reblog":null}{...
$ nc tootcat.0j0.jp 7006 | docker run -i --rm -e "TZ=Asia/Tokyo" asannou/jq --unbuffered -r '.prefix = (.created_at | sub(".[0-9]*Z";"Z") | fromdate | strflocaltime("%H:%M")) | .prefix += " (" + .account.username + ") " | .prefix as $prefix | select(.uri | test("^tag:friends.nico,")) | .content | split("\r\n") | join("\r\n" + $prefix) | $prefix + .'
16:18 (username) content1
16:18 (username) content2
    • IRC
    • strflocaltime がまだ development version のみの機能だったので asannou/jq を作った
    • --unbuffered で滞りなく出力される

2017-03-06 5 月末に公開される SHAttered のコードを予想して新しい PDF を作る このエントリーを含むブックマーク

前回に引き続き https://shattered.it/ の件です。

Following Google’s vulnerability disclosure policy, we will wait 90 days before releasing code that allows anyone to create a pair of PDFs that hash to the same SHA-1 sum given two distinct images with some pre-conditions.

https://security.googleblog.com/2017/02/announcing-first-sha1-collision.html

Google 曰く「誰にでも、いくつかの前提条件を満たすふたつの異なる画像から、同じ SHA-1 ハッシュPDF のペアが作成できるコードをリリースする」とのことです。ただし、脆弱性公開ポリシーに従い、リリースは発表から 90 日後です。

現在公開されている情報から、このコードがどのようなものか予想し、実証しようと思います。

縦に長くなったので、最初に成果物へリンクしておきます。

さて、SHA-1 ハッシュが同じで違う画像が表示される GooglePDF 1, PDF 2 にはどのような技巧が用いられているでしょうか。

%PDF-1.3
%....


1 0 obj
<</Width 2 0 R/Height 3 0 R/Type 4 0 R/Subtype 5 0 R/Filter 6 0 R/ColorSpace 7 0 R/Length 8 0 R/BitsPerComponent 8>>
stream
[...]
endstream
endobj

2 0 obj
1024
endobj

3 0 obj
740
endobj

4 0 obj
/XObject
endobj

5 0 obj
/Image
endobj

6 0 obj
/DCTDecode
endobj

7 0 obj
/DeviceRGB
endobj

8 0 obj
421385
endobj

[...]
%%EOF

PDF ファイルなので、一般的な PDF の構造があります。ここに特殊な箇所は見つかりません。この構造部分は PDF 1PDF 2 で完全に一致しています。

"1 0 obj" における stream [...] endstream 間のデータが、幅 1024 高さ 740 の Image として DCTDecode されて(つまり JPEG 画像)表示されます。強いて不自然な点をあげると、"/Width 1024/Height 740" と書かず "/Width 2 0 R/Height 3 0 R" のように "2 0 obj" と "3 0 obj" への参照となっていることです。これは後述する理由により、endstream より後方にあるデータは任意に書き換えが可能なため、画像のサイズを変更できるようにする工夫と窺えます。

stream 内の JPEG データを詳しく見る前に、通常の JPEG の構造を簡単に理解しておきましょう。

JPEG は、複数のセグメントで構成されています。セグメントは、マーカーとペイロードからできていて、マーカーがセグメントがどのような種類の情報かを表します。重要なマーカーは以下のものくらいです。

  • SOI(イメージ開始)
  • SOF(フレーム開始)
  • DHT(ハフマンテーブル定義)
  • DQT(量子化テーブル定義)
  • SOS(画像データ開始)
  • EOI(イメージ終了)

例えば https://upload.wikimedia.org/wikipedia/commons/thumb/e/e8/Lichtenstein.jpg/256px-Lichtenstein.jpg の構造は下記のとおりです。

SOI  0000
APP0 0010
DQT  0043
DQT  0043
SOF0 0011
DHT  001c
DHT  0040
DHT  001b
DHT  0030
SOS  000c
SCAN 4983
EOI  0000

マーカーの後ろにある数字は、ペイロードのバイト長です。SCAN はマーカーではなく画像データです。詳細は JPEG - Wikipedia を参照してください。

それでは、件の PDF に含まれる JPEG の構造を分析していきます。

PDF 1        PDF 2

SOI  0000    SOI  0000
COM  0024    COM  0024
    V            V
COM  0173    COM  017f    <--- (A)
[   |   ]    [   |   ]    <--- (B) collision blocks
    V            |
COM  00fc        |        <--- (C)
    |            V
    |        APP0 0010    <--- (D)
    |        DQT  0043
    |        DQT  0043
    |        SOF2 0011
    |        DHT  001e
    |        DHT  001d
    V        COM  0006
COM  27f4        V
    |        SOS  000c
    |        SCAN 27a6
    |        DHT  0038
    V        COM  0006
COM  218d        V
    |        SOS  0008
    |        SCAN 210b
    |        DHT  0070
    V        COM  0006
COM  9f9a        V
    |        SOS  0008
    |        SCAN 9f1e
    |        DHT  006a
    V        COM  0006
COM  659a        V
    |        SOS  0008
    |        SCAN 6519
    |        DHT  006f
    V        COM  0006
COM  ae2e        V
    |        SOS  0008
    |        SCAN adee
    |        DHT  002e
    V        COM  0006
COM  36d2        V
    |        SOS  0008
    |        SCAN 36c2
    V        COM  0006
COM  11b5        V
    |        SOS  000c
    |        SCAN 1172
    |        DHT  002d
    V        COM  0006
COM  2c5b        V
    |        SOS  0008
    |        SCAN 2c1b
    |        DHT  002e
    V        COM  0006
COM  2dce        V
    |        SOS  0008
    |        SCAN 2d8e
    |        DHT  002e
    V        COM  0006
COM  37d2        V
    |        SOS  0008
    |        SCAN 37c0
    |        EOI  0000
    V            X
APP0 0010        X
APP1 0040        X
APP13 0038       X
SOF0 0011        X
DHT  001f        X
DHT  00b5        X
DHT  001f        X
DHT  00b5        X
DQT  0043        X
DQT  0043        X
DRI  0004        X
SOS  000c        X
SCAN 39104       X
EOI  0000        X

COM というマーカーが頻出しますが、これはコメントを表します。コメントマーカーが現れると、そのペイロードの長さ分だけ、データが無視されるため、矢印でその範囲がスキップされることを表現しています。また、EOI の後ろにあるデータも、同様に無視されるので X とします。見ての通り、PDF 1 の COM に PDF 2 のデータが、PDF 2 の COM に PDF 1 のデータが入っているという、カドゥケウスの杖のような形をしていることが判明しました。

https://commons.wikimedia.org/wiki/File:Caduceus.svg

PDF 1PDF 2 で異なる部分は (A) と (B) のみで、残りは完全に一致しています。(A) において、コメントとみなす長さを 0x0173 か 0x017f かに変えることで、続きが (C) か (D) かに分かれ、異なる画像が表示されるという原理です。

(A) でコメント長を示す数値が異なるので、当然 SHA-1 ハッシュも違う値が計算されます。ですが、その直後の SHAttered による (B) collision blocks でそれが解消され、再びハッシュが等しくなります。そして (B) 以降は、どのようなデータを追加しても、ふたつのファイルの SHA-1 ハッシュは同一のままです。"1 0 obj" の stream が、後方の obj を参照していたのは、こういった理由からでした。

結論として、本稿の目的であるコードとは、(C) 以降にあるようなデータと同様の構造を持つ JPEG を生成するコードと言えるでしょう。

ということで、そのようなコードを(Perl で)実装し、それを使って作成した PDF のペアが https://asannou.github.io/shatterized-1.pdfhttps://asannou.github.io/shatterized-2.pdf です。

無論、ハッシュが一致しています。

$ shasum shatterized-1.pdf shatterized-2.pdf
5135c8373be5ceb5763406b307cd17d179fafbe2  shatterized-1.pdf
5135c8373be5ceb5763406b307cd17d179fafbe2  shatterized-2.pdf

また、画像のみの PDF だけでなく https://asannou.github.io/d.hatena.ne.jp-asannou-20170226-1.pdfhttps://asannou.github.io/d.hatena.ne.jp-asannou-20170226-2.pdf のように、一部の画像(ブックマーク数)が改ざんされているものも作ることができます。

$ shasum d.hatena.ne.jp-asannou-20170226-1.pdf d.hatena.ne.jp-asannou-20170226-2.pdf
6a2cd0570bc6d05cd4777a17925a2095e928582d  d.hatena.ne.jp-asannou-20170226-1.pdf
6a2cd0570bc6d05cd4777a17925a2095e928582d  d.hatena.ne.jp-asannou-20170226-2.pdf

残念ながら、この手法には欠点があります。それは COM のペイロードの長さの最大が 0xffff であるため、それより長いデータが存在すると、収まりきらないことです。例として https://upload.wikimedia.org/wikipedia/commons/e/e8/Lichtenstein.jpg の構造を示します。

SOI  0000
APP0 0010
DQT  0043
DQT  0043
SOF0 0011
DHT  001c
DHT  004a
DHT  001a
DHT  0032
SOS  000c
SCAN 79009
EOI  0000

SOS マーカーの後の画像データの長さが 0x79009 もあって、コメントに収まりません。おそらく、冒頭の「いくつかの前提条件を満たすふたつの異なる画像」というのは、このことを示唆しているのではないかと推測します。

しかしまだ、画像データを分割して、段階的に表示するプログレッシブ JPEG に変換する方法が残っています。やってみましょう。

$ jpegtran -progressive Lichtenstein.jpg > Lichtenstein_p.jpg
SOI  0000
APP0 0010
DQT  0043
DQT  0043
SOF2 0011
DHT  001b
DHT  0019
SOS  000c
SCAN c486
DHT  0034
SOS  0008
SCAN f1f2
DHT  002c
SOS  0008
SCAN 1203
DHT  002d
SOS  0008
SCAN 2110
DHT  0037
SOS  0008
SCAN 7195
DHT  002c
SOS  0008
SCAN 1729d
SOS  000c
SCAN 3718
DHT  0027
SOS  0008
SCAN 358d
DHT  0029
SOS  0008
SCAN 4499
DHT  002a
SOS  0008
SCAN 2c3fb
EOI  0000

確かに 10 分割されましたが、まだ 0xffff 以上の画像データが残っています。スキャンの方法をテキストファイルで詳細に指定し、分割数を増やすことができるので、それを試します。テキストファイルの仕様は https://github.com/mozilla/mozjpeg/blob/master/wizard.txt を参照してください。

$ cat <<EOD > scans.txt
0: 0-0,   0, 0;
1: 0-0,   0, 0;
2: 0-0,   0, 0;
0: 1-1,   0, 0;
0: 2-2,   0, 0;
0: 3-3,   0, 0;
0: 4-5,   0, 0;
1: 1-63,  0, 0;
2: 1-63,  0, 0;
0: 6-7,   0, 0;
0: 8-9,   0, 0;
0: 10-12, 0, 0;
0: 13-17, 0, 0;
0: 18-63, 0, 0;
EOD
$ jpegtran -scans scans.txt Lichtenstein.jpg > Lichtenstein_pp.jpg
SOI  0000
APP0 0010
DQT  0043
DQT  0043
SOF2 0011
DHT  001c
SOS  0008
SCAN b243
DHT  001a
SOS  0008
SCAN 1ffa
DHT  0019
SOS  0008
SCAN 1b35
DHT  0021
SOS  0008
SCAN 87b7
DHT  0020
SOS  0008
SCAN 8b6f
DHT  0021
SOS  0008
SCAN 5ac4
DHT  0029
SOS  0008
SCAN cf74
DHT  0038
SOS  0008
SCAN 6474
DHT  0032
SOS  0008
SCAN 4647
DHT  0027
SOS  0008
SCAN 9426
DHT  0026
SOS  0008
SCAN 9464
DHT  002a
SOS  0008
SCAN a9a7
DHT  002e
SOS  0008
SCAN bf73
DHT  003b
SOS  0008
SCAN dcd2
EOI  0000

無事成功しました。それでも画像データが巨大な場合は、最大個数に分割しても収まらないことがあると思います。その時は、画像の品質を下げる等を検討する必要があります。

In order to prevent this attack from active use, we’ve added protections for Gmail and GSuite users that detects our PDF collision technique.

https://security.googleblog.com/2017/02/announcing-first-sha1-collision.html

最後に Gmailhttps://asannou.github.io/shatterized-1.pdfhttps://asannou.github.io/shatterized-2.pdf が検出されるかを確認しました。

f:id:asannou:20170306002735p:image:w535

手法で作られたファイルも、Gmail に添付して送ろうとするとエラーになりました。

Google がコードを公開したら、答え合わせをします。

2017-02-26 SHAttered で Git の SHA-1 ハッシュを衝突させられるか試す このエントリーを含むブックマーク

https://shattered.it/ のリリースを受けて、Git において、違うファイルをコミットしたにも関わらず、それらのコミットを参照する SHA-1 ハッシュが同じである状態を実現できるかを試しました。


同じ SHA-1 ハッシュを持つ PDF ファイルで可能か


https://shattered.it/ で公開されている、SHA-1 ハッシュが同じ PDF ファイルを、それぞれ空のレポジトリコミットします。

$ wget https://shattered.it/static/shattered-1.pdf https://shattered.it/static/shattered-2.pdf
$ diff shattered-1.pdf shattered-2.pdf
Binary files shattered-1.pdf and shattered-2.pdf differ
$ shasum shattered-1.pdf shattered-2.pdf
38762cf7f55934b34d179ae6a4c80cadccbb7f0a  shattered-1.pdf
38762cf7f55934b34d179ae6a4c80cadccbb7f0a  shattered-2.pdf
$ cp shattered-1.pdf shattered.pdf
$ git --git-dir=.git-1 --work-tree=. init
Initialized empty Git repository in /path/to/.git-1/
$ git --git-dir=.git-1 --work-tree=. add shattered.pdf
$ GIT_AUTHOR_DATE='Fri Feb 24 15:00:00 JST 2017' GIT_COMMITTER_DATE='Fri Feb 24 15:00:00 JST 2017' git --git-dir=.git-1 --work-tree=. commit -m 'test'
[master (root-commit) e95789a] test
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 shattered.pdf
$ git --git-dir=.git-1 --work-tree=. log --pretty=fuller
commit e95789af5bf00006938d8ab048ab51c9b68711a6
Author:     asannou <asannou@example.com>
AuthorDate: Fri Feb 24 15:00:00 2017 +0900
Commit:     asannou <asannou@example.com>
CommitDate: Fri Feb 24 15:00:00 2017 +0900

    test

shattered-1.pdfコミットしたときの SHA-1 ハッシュは e95789af5bf00006938d8ab048ab51c9b68711a6 です。

$ cp shattered-2.pdf shattered.pdf
$ git --git-dir=.git-2 --work-tree=. init
Initialized empty Git repository in /path/to/.git-2/
$ git --git-dir=.git-2 --work-tree=. add shattered.pdf
$ GIT_AUTHOR_DATE='Fri Feb 24 15:00:00 JST 2017' GIT_COMMITTER_DATE='Fri Feb 24 15:00:00 JST 2017' git --git-dir=.git-2 --work-tree=. commit -m 'test'
[master (root-commit) ded44e8] test
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 shattered.pdf
$ git --git-dir=.git-2 --work-tree=. log --pretty=fuller
commit ded44e864ff901c3bb6367f13ad6aeb0b6c0cfa0
Author:     asannou <asannou@example.com>
AuthorDate: Fri Feb 24 15:00:00 2017 +0900
Commit:     asannou <asannou@example.com>
CommitDate: Fri Feb 24 15:00:00 2017 +0900

    test

全く同じ日時を指定したのですが shattered-2.pdfコミットしたときは ded44e864ff901c3bb6367f13ad6aeb0b6c0cfa0 となり、違うハッシュでした。原因を知るために、コミットSHA-1 ハッシュがどのように計算されるかを調べましょう。

Gitコミットは、コミットオブジェクトというもので管理されています。コミットオブジェクトは、下記のように確認することができます。

$ git --git-dir=.git-1 --work-tree=. cat-file -p e95789af5bf00006938d8ab048ab51c9b68711a6
tree 8004c8a7b6fce1452539556bb4c4c91b92b5c2bc
author asannou <asannou@example.com> 1487916000 +0900
committer asannou <asannou@example.com> 1487916000 +0900

test

コミットSHA-1 ハッシュというのは、このコミットオブジェクトの内容の先頭に "commit <size>\0" を付加して SHA-1 ハッシュを計算したものです。

$ git --git-dir=.git-1 --work-tree=. cat-file -p e95789af5bf00006938d8ab048ab51c9b68711a6 > commit-1
$ wc -c commit-1
     163 commit-1
$ printf "commit 163\0" > commit-header-1
$ cat commit-header-1 commit-1 | shasum
e95789af5bf00006938d8ab048ab51c9b68711a6  -

実は、これをやってくれるコマンド git hash-object が既にあります。

$ git hash-object -t commit commit-1
e95789af5bf00006938d8ab048ab51c9b68711a6

ここで shattered-2.pdfコミットオブジェクトを覗くと、tree という値だけが異なっていることがわかります。

$ git --git-dir=.git-2 --work-tree=. cat-file -p ded44e864ff901c3bb6367f13ad6aeb0b6c0cfa0
tree 32a3329d74097e4a877b1180106e65d3b9f76848
author asannou <asannou@example.com> 1487916000 +0900
committer asannou <asannou@example.com> 1487916000 +0900

test

tree もツリーオブジェクトなので、内容を確認します。

$ git --git-dir=.git-1 --work-tree=. cat-file -p 8004c8a7b6fce1452539556bb4c4c91b92b5c2bc
100644 blob ba9aaa145ccd24ef760cf31c74d8f7ca1a2e47b0  shattered.pdf
$ git --git-dir=.git-2 --work-tree=. cat-file -p 32a3329d74097e4a877b1180106e65d3b9f76848
100644 blob b621eeccd5c7edac9b7dcba35a8d5afd075e24f2  shattered.pdf

ツリーオブジェクトも似たような方法で SHA-1 ハッシュが求められます。

$ printf "100644 shattered.pdf\0%s" $(echo ba9aaa145ccd24ef760cf31c74d8f7ca1a2e47b0 | xxd -r -p) | git hash-object -t tree --stdin
8004c8a7b6fce1452539556bb4c4c91b92b5c2bc
$ printf "100644 shattered.pdf\0%s" $(echo b621eeccd5c7edac9b7dcba35a8d5afd075e24f2 | xxd -r -p) | git hash-object -t tree --stdin
32a3329d74097e4a877b1180106e65d3b9f76848

つまり blob という値が一致しないため、ハッシュが異なると言えます。そして同様に blobブロブオブジェクトです。

$ git --git-dir=.git-1 --work-tree=. cat-file -p ba9aaa145ccd24ef760cf31c74d8f7ca1a2e47b0 > blob-1
$ git --git-dir=.git-2 --work-tree=. cat-file -p b621eeccd5c7edac9b7dcba35a8d5afd075e24f2 > blob-2

ブロブオブジェクトの内容は、コミットしたファイルそのものです。

$ diff blob-1 shattered-1.pdf
$ diff blob-2 shattered-2.pdf
$

ブロブオブジェクトSHA-1 ハッシュが求められます。

$ git hash-object -t blob blob-1
ba9aaa145ccd24ef760cf31c74d8f7ca1a2e47b0
$ git hash-object -t blob blob-2
b621eeccd5c7edac9b7dcba35a8d5afd075e24f2

ここで git hash-object がどのような処理をするかを思い出すと、ハッシュが異なる理由がわかります。

$ wc -c blob-1
  422435 blob-1
$ printf "blob 422435\0" > blob-header-1
$ cat blob-header-1 blob-1 | shasum
ba9aaa145ccd24ef760cf31c74d8f7ca1a2e47b0  -

要するに、ブロブオブジェクトハッシュには、コミットされたファイルそのままの SHA-1 ハッシュが使われず、ファイルに "blob <size>\0" ヘッダが付加されたものの SHA-1 ハッシュが使用されるため、それをもとに計算される、ツリーオブジェクトコミットオブジェクトハッシュも一致しないという真相でした。


f:id:asannou:20170227184909p:image:w640


後日リリースされる、同じハッシュPDF のペアを作成するコードを使えば可能か


90 日後に、ふたつの異なる画像から、同じ SHA-1 ハッシュを持つ PDF のペアを作ることができるコードがリリースされるそうです。

Following Google’s vulnerability disclosure policy, we will wait 90 days before releasing code that allows anyone to create a pair of PDFs that hash to the same SHA-1 sum given two distinct images with some pre-conditions.

https://security.googleblog.com/2017/02/announcing-first-sha1-collision.html

これを用いて、先頭が "blob <size>\0" となっていて、内容が異なり、同じハッシュのファイルを作れるでしょうか。


f:id:asannou:20170226200904p:image:w640

https://shattered.it/static/pdf_format.png


おそらく PDF に含まれる JPEG の部分のみを差し替えられるコードと思われますので prefix (pre-determined) となっている PDF Header の前に "blob <size>\0" を挿入するのは無理だろうと予想します。

このコードを予想する記事 を書きました。


prefixブロブオブジェクトにして同じ手法を用いれば可能か


https://shattered.it/static/shattered.pdf では prefixPDF にしていますが、Gitブロブオブジェクトのヘッダを prefix として、書かれている通りにすれば、同じハッシュブロブオブジェクトを作れるでしょうか。

多分可能でしょうが 6,500 年の CPU 時間と 110 年の GPU 時間が必要だそうです。

This attack required over 9,223,372,036,854,775,808 SHA1 computations. This took the equivalent processing power as 6,500 years of single-CPU computations and 110 years of single-GPU computations.

https://shattered.it/

既存のコミットと同じハッシュコミットを作ることは可能か


SHAttered は、prefix が同じで内容が異なるふたつのファイルを調整して、同じ SHA-1 ハッシュにするというアプローチなので、既にあるコミットと同じハッシュコミットを作ることには使えません。

ただし、同じハッシュを持つ、正常なコミット A と不正なコミット B を作り、まず A を信頼させてから B にすり替えるというシナリオはありえます。


コミットに GPG による署名があれば信頼してもよいか


Git には、コミットに GPG で署名する機能があるので、そのコミットオブジェクトを確認します。

$ git --git-dir=.git-3 --work-tree=. add shattered.pdf
$ git --git-dir=.git-3 --work-tree=. commit -S -m 'test'

You need a passphrase to unlock the secret key for
user: "asannou (Git signing key) <asannou@example.com>"
2048-bit RSA key, ID 05CFBEA7, created 2017-02-26

[master (root-commit) e82463b] test
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 shattered.pdf
$ git --git-dir=.git-3 --work-tree=. cat-file -p e82463b
tree 8004c8a7b6fce1452539556bb4c4c91b92b5c2bc
author asannou <asannou@example.com> 1487916000 +0900
committer asannou <asannou@example.com> 1487916000 +0900
gpgsig -----BEGIN PGP SIGNATURE-----
 Comment: GPGTools - https://gpgtools.org

 iQEcBAABCgAGBQJYsq5mAAoJEMxRxTEFz76nLr8IALmPtkI9ZgvHMtKqQOcLl51l
 YOFoMu4k2fQ65DJyJFaj/HXhcdbw21rUkf1OAsxcpewFWZV2udfJUWt3LItNKbXf
 YWM/Z074VPBIdJlme7jMfdq96Q4fJwX7Lf5ypRgzOYswIj2Yd+2viuKZjwx5yujt
 pC/H4Gc08hmOhKpVNXlmDNd6IO8McBOLAGD3NvA8xsXFlSoLquVwcaq3vWTwKpT1
 DMKG18aDYr7LRjXS3417r3zn2a2rQaZl7F6gBKy9+qH+e9gfZa/wNrzRxYxZ+lJw
 tNOA/rLflylROK+k6TtISTJXRAhIUCafbD8WMLaD9KAxxR6gKO2huGFH0yxFq9I=
 =tbnx
 -----END PGP SIGNATURE-----

test

前述までのコミットオブジェクトに gpgsig という署名が付加された形です。この署名の対象はなにかというと、コミットオブジェクトの内容のみのようです。

$ git --git-dir=.git-3 --work-tree=. cat-file -p e82463b > commit-3
$ gpg --detach-sign commit-3

You need a passphrase to unlock the secret key for
user: "asannou (Git signing key) <asannou@example.com>"
2048-bit RSA key, ID 05CFBEA7, created 2017-02-26

$ gpg --verify commit-3.sig commit-3
gpg: Signature made 日  2/26 19:33:48 2017 JST using RSA key ID 05CFBEA7
gpg: Good signature from "asannou (Git signing key) <asannou@example.com>" [ultimate]

つまり、署名によって tree, author, committer とコミットメッセージの正しさしか保証されないということです。tree には SHA-1 ハッシュしか書かれていないので、同一のハッシュを持つツリーオブジェクトがあれば、それにすり替えることが可能と考えます。


f:id:asannou:20170227191711p:image:w640


したがって、署名されていたとしても、すり替えが可能な状態にあるレポジトリのファイルを信頼するのはやめましょう。

2017-02-13 Amazon EC2 SSH 救命索 このエントリーを含むブックマーク

これは Amazon EC2 Run Command Advent Calendar 2016 の 75 日目の記事です。

皆さんは、アドベントカレンダーですか?

http://cdn-ak.f.st-hatena.com/images/fotolife/a/asannou/20170212/20170212195445.png

なんらかの理由(キー紛失、設定ミス、ユーザ不在、人類滅亡など)で Amazon EC2SSH 接続できなくなったとき インスタンス再作成 を強いられることがありますが、そうならないように Run Command - Amazon EC2 Systems Manager | AWS で保険をかけておくことができます。

Run Command は EC2 インスタンス上に SSM エージェントのインストール - Amazon EC2 Systems Manager をしておくことで、AWS CLI などでリモートからシェルコマンドの実行ができるフレンズです。

http://cdn-ak.f.st-hatena.com/images/fotolife/a/asannou/20170212/20170212195445.png

最初に、EC2 インスタンスSSM サービスと通信するための IAM ロール "EC2RoleforSSMRunShellScript" をアタッチした、インスタンスプロファイル "EC2RoleJaparipark" を Terraform で作成します。

provider "aws" {
  region = "ap-northeast-1"
}

resource "aws_iam_policy" "ec2-ssm" {
  name = "EC2RoleforSSMRunShellScript"
  path = "/"
  policy = <<EOD
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ssm:DescribeAssociation",
        "ssm:GetDeployablePatchSnapshotForInstance",
        "ssm:GetDocument",
        "ssm:GetParameters",
        "ssm:ListAssociations",
        "ssm:ListInstanceAssociations",
        "ssm:PutInventory",
        "ssm:UpdateAssociationStatus",
        "ssm:UpdateInstanceAssociationStatus",
        "ssm:UpdateInstanceInformation"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2messages:AcknowledgeMessage",
        "ec2messages:DeleteMessage",
        "ec2messages:FailMessage",
        "ec2messages:GetEndpoint",
        "ec2messages:GetMessages",
        "ec2messages:SendReply"
      ],
      "Resource": "*"
    }
  ]
}
EOD
}

resource "aws_iam_role" "ec2-ssm" {
  name = "EC2RoleJaparipark"
  path = "/"
  assume_role_policy = <<EOD
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOD
}

resource "aws_iam_role_policy_attachment" "ec2-ssm" {
  role = "${aws_iam_role.ec2-ssm.name}"
  policy_arn = "${aws_iam_policy.ec2-ssm.arn}"
}

resource "aws_iam_instance_profile" "ec2-ssm" {
  name = "${aws_iam_role.ec2-ssm.name}"
  roles = ["${aws_iam_role.ec2-ssm.name}"]
}

用意されている AmazonEC2RoleforSSM でも同じことができますが、権限が強すぎるため、必要がなさそうなものを削りました。それについては EC2’s most dangerous feature を読むと、圧倒的なわかりを得ます。

対象の EC2 インスタンス i-xxxxxxxxxxxxxxxxx に "EC2RoleJaparipark" をアタッチします。以前はインスタンス作成時にのみ可能だったのですが New! Attach an AWS IAM Role to an Existing Amazon EC2 Instance by Using the AWS CLI | AWS Security Blog にて、作成済みインスタンスでもできるようになりました。すごーい!

$ aws ec2 associate-iam-instance-profile --instance-id i-xxxxxxxxxxxxxxxxx --iam-instance-profile Name=EC2RoleJaparipark

そして EC2 インスタンス i-xxxxxxxxxxxxxxxxx に SSM エージェントをインストールしましょう。

$ sudo yum install amazon-ssm-agent
$ sudo start amazon-ssm-agent

http://cdn-ak.f.st-hatena.com/images/fotolife/a/asannou/20170212/20170212195445.png

さて、リモートからコマンドを送信するための、IAM ポリシー "SSMLuckyBeast" を作成します。AWS 管理ポリシーの AmazonSSMFullAccess なども使えますが、その場合 "EC2RoleforSSMRunShellScript" をアタッチしているすべてのインスタンス送信可能になります。

provider "aws" {
  region = "ap-northeast-1"
}

data "aws_caller_identity" "aws" {}

resource "aws_iam_policy" "ssm" {
  name = "SSMLuckyBeast"
  path = "/"
  policy = <<EOD
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "ssm:SendCommand",
      "Resource": [
        "arn:aws:ssm:ap-northeast-1::document/AWS-RunShellScript",
        "arn:aws:ec2:ap-northeast-1:${data.aws_caller_identity.aws.account_id}:instance/i-xxxxxxxxxxxxxxxxx"
      ]
    },
    {
      "Effect": "Allow",
      "Action": "ssm:ListCommandInvocations",
      "Resource": "arn:aws:ssm:ap-northeast-1:${data.aws_caller_identity.aws.account_id}:*"
    }
  ]
}
EOD
}

最後に、適当な IAM ユーザに "SSMLuckyBeast" をアタッチして準備は終わりです。

$ aws iam attach-user-policy --user-name kaban --policy-arn arn:aws:iam::$(aws sts get-caller-identity --output text --query Account):policy/SSMLuckyBeast

http://cdn-ak.f.st-hatena.com/images/fotolife/a/asannou/20170212/20170212195445.png

それでは、やっていきます

$ aws ssm send-command --output text --document-name AWS-RunShellScript --instance-ids i-xxxxxxxxxxxxxxxxx --parameters commands="uname -a"
COMMAND	8020f507-8adc-4536-80af-d18dd2dafa5f		0	AWS-RunShellScript	0	1486890312.52	50	0			1486886712.52	Pending	Pending	1
INSTANCEIDS	i-xxxxxxxxxxxxxxxxx
NOTIFICATIONCONFIG
COMMANDS	uname -a

command-id で結果

$ aws ssm list-command-invocations --output text --details --command-id 8020f507-8adc-4536-80af-d18dd2dafa5f
COMMANDINVOCATIONS	8020f507-8adc-4536-80af-d18dd2dafa5f		AWS-RunShellScript	i-xxxxxxxxxxxxxxxxx		1486886712.64			Success	Success
COMMANDPLUGINS	aws:runShellScript	Linux ip-172-31-28-103 4.4.30-32.54.amzn1.x86_64 #1 SMP Thu Nov 10 15:52:05 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
			ap-northeast-1	0	1486886713.22	1486886713.22			Success	Success
NOTIFICATIONCONFIG

がおー

$ aws ssm send-command --output text --query "Command.CommandId" --document-name AWS-RunShellScript --instance-ids i-xxxxxxxxxxxxxxxxx --parameters commands="echo $(cat ~/.ssh/id_rsa.pub) >> /home/ec2-user/.ssh/authorized_keys"
7b410797-1d73-4cc9-a29c-e845e6f0621a
$ aws ssm list-command-invocations --output text --details --command-id 7b410797-1d73-4cc9-a29c-e845e6f0621a
COMMANDINVOCATIONS	7b410797-1d73-4cc9-a29c-e845e6f0621a		AWS-RunShellScript	i-xxxxxxxxxxxxxxxxx		1486888202.22			Success	Success
COMMANDPLUGINS	aws:runShellScript				ap-northeast-1	0	1486888202.66	1486888202.66			Success	Success
NOTIFICATIONCONFIG

よかったですね

$ ssh ec2-user@203.0.113.1
Last login: Thu Feb  9 19:08:31 2017 from 198.51.100.1

       __|  __|_  )
       _|  (     /   Amazon Linux AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-ami/2016.09-release-notes/
No packages needed for security; 3 packages available
Run "sudo yum update" to apply all updates.

http://cdn-ak.f.st-hatena.com/images/fotolife/a/asannou/20170212/20170212195445.png

Run Command をバックドアっぽく使いましたが、玄関にしてもよさがあります。そうすると、権限を IAM で管理でき AWS CloudTrail (AWS API の呼び出し記録とログファイル送信) | AWS でコマンド送信が記録されるため、監査にも便利です。

Run Command を対話的にする GitHub - koshigoe/aws-ssm-console があり asannou/aws-ssm-console - Docker Hub したので、こうなります。

$ docker run -it --rm -v ~/.aws:/root/.aws asannou/aws-ssm-console --instance-ids i-xxxxxxxxxxxxxxxxx
>> uname -a
Running uname -a
[i-xxxxxxxxxxxxxxxxx]    Success: uname -a
	Linux ip-192-168-1-5 4.4.41-36.55.amzn1.x86_64 #1 SMP Wed Jan 18 01:03:26 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

たっのしー!

http://cdn-ak.f.st-hatena.com/images/fotolife/a/asannou/20170212/20170212195445.png

なお、お分かりですが ssm:ListCommandInvocations の Resource が適切ではないので、ことごとく実行結果が見放題です。カスタムロールおよびポリシー使用したアクセス設定 - Amazon EC2 Systems Manager にあるのもガバガバだし、そうか、アマゾンは、、

2016-10-10 I am 最小権限でアクセスキーを発行したいマン このエントリーを含むブックマーク

ここまでは、一般的なウェブサービスのユーザ発行フローとほぼ同じ。

利用者が AWSリソースにアクセスするためには、アクセスキーを取得する必要がある。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iam:*LoginProfile",
        "iam:*AccessKey*",
        "iam:*SSHPublicKey*"
      ],
      "Resource": "arn:aws:iam::account-id-without-hyphens:user/${aws:username}"
    },
    {
      "Effect": "Allow",
      "Action": [
        "iam:ListAccount*",
        "iam:GetAccountSummary",
        "iam:GetAccountPasswordPolicy",
        "iam:ListUsers"
      ],
      "Resource": "*"
    }
  ]
}
      • ユーザの一覧画面を経由するマネジメントコンソールの設計上、本来は不要な権限を許可しなければならない問題がある(Statement の 2 要素目)

最小権限の原則 - Wikipedia に従って、下記のポリシーのみでアクセスキーを発行する方法を考える。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iam:*AccessKey*"
      ],
      "Resource": "arn:aws:iam::account-id-without-hyphens:user/${aws:username}"
    }
  ]
}
  1. FirefoxChromehttps://console.aws.amazon.com/iam/home にアクセス
  2. アドレスバーに "j" を入力
  3. 続けて下記のブックマークレットを入力して、アクセスキーが書かれたファイルをダウンロード
avascript:$.ajax({url:'/iam/service/proxy/CreateAccessKey',type:'POST',contentType:'application/json',data:JSON.stringify({userName:UserInfo.name})}).done(function(data){a=document.createElement('a');document.body.appendChild(a);a.download=UserInfo.name+'.accesskey.txt';a.target='_blank';a.href=window.URL.createObjectURL(new Blob([data],{type:'text/plain'}));a.click()}).fail(function(data){alert(JSON.stringify(data))})

2016 年も終盤というのに、未だにブックマークレットなんて書いている(あとはてな記法)。javascript スキームが対策されていたり、Microsoft Edge では全く使えなかったりして時の流れを感じた。