ActiveResourceを使ったRailsアプリをRedisで高速化した
ActiveResource とは
ActiveResourceはRESTful APIをマッピングしActiveRecord のモデルとして利用可能にするgemで、これを使うとActiveRecordでDB操作を行うのと同じようにRESTful APIを利用できます。
ボトルネックになることもある
ActiveResourceを頻繁に使うとAPIに過剰にアクセスしているのと同じこととなり、ボトルネックとなりえます。 以下は今回、チューニングの対象となったアプリのNginxのアクセスログをkataribeでパースしたものです。
$ cat /var/log/nginx/access.log | kataribe ... TOP 12 Slow Requests 1 14.933 GET / HTTP/1.1 2 14.898 GET / HTTP/1.1 3 14.722 GET / HTTP/1.1 4 14.692 GET / HTTP/1.1 ...
rootへのGETリクエストが14秒台後半で非常に遅いことがわかります。これはActiveResourceを使って取ってきたデータを一覧するページで、ActiveResourceがボトルネックとなっていると推測できました。
Redisでキャッシュする
オンメモリ KVSのDBであるRedisにキャッシュすることで高速化を図ります。Redisはメモリ上でデータを管理するため、Read/Writeともに高速でキャッシュとして適しています。ActiveResourceを使ってデータを引っ張ってくるところをキャッシュしてみました。
RedisとRailsアプリケーションの接続にはmperham/connection_pool: Generic connection pooling for Rubyを使いました。コネクションプールというのはDBのコネクションをあらかじめ一定数確立しておいて使いまわす手法で、これを使うとDBへの接続に必要となるオーバーヘッドをカットできWeb/DBの双方の負荷を下げることができます。また、Web/DB間の接続を使いまわすことで同時接続数を節約します。MongoDB gem、ActiveRecord gemは自前でコネクションプールを持っていますが、Redis gemは持っていないので、このgemを使って接続することでコネクションプールを使えるようにしました。以下がRedisへの接続のために追加したファイルです。
config/initializers/redis.rb
# frozen_string_literal: true # Load the redis.yml configuration file redis_config = YAML.load_file(Rails.root + 'config/redis.yml')[Rails.env] Redis.current = ConnectionPool.new(size: 10, timeout: 5) do Redis.new host: redis_config['host'], port: redis_config['port'] end
config/redis.yml
default: &default host: localhost port: 6379 development: <<: *default test: <<: *default production: <<: *default
Redisへのアクセス
Redisへのアクセスはcontroller内で以下のように行えますが、ここで注意しないといけないのは、keyがハッシュであるオブジェクトをJSONに変換しRedisにいれると、Redisから取り出しJSONにした際にkeyがstringになるところです。with_indifferent_access
を使うなどして対処しましょう。
Redis.current.with do |redis| test_user = { :name => "tkmru", :email => "tkmru@hoge"} redis.set('test_user', test_user.to_json) test_user = JSON.parse(redis.get('test_user')) # {"name"=>"tkmru", "email"=>"tkmru@hoge"} p test_user[:name] # nil p test_user['name'] # tkmru test_user = test_user.with_indifferent_access p test_user[:name] # tkmru p test_user['name'] # tkmru end
結果
$ cat /var/log/nginx/access.log | kataribe ... TOP 10 Slow Requests 1 3.566 GET /hoge/231 HTTP/1.1 2 1.866 GET / HTTP/1.1 3 1.482 GET /hoge/240 HTTP/1.1 4 1.479 GET /hoge/226 HTTP/1.1 5 1.439 GET / HTTP/1.1 6 1.415 GET /hoge/238 HTTP/1.1 7 1.410 GET / HTTP/1.1 8 1.293 GET / HTTP/1.1 9 1.119 GET /hoge/243 HTTP/1.1 ...
14秒台後半だったrootへのGETリクエストが1.4秒ちょっとで終わりました。 /hoge 以下もキャッシュすればもっと早くできそう。
おわりに
今回は原因が自明であったため、kataribeによるNginxのアクセスログのプロファイリングしか行いませんでしたが、pt-query-digestによるMySQLのスロークエリの解析や、stackprof、rblineprofによるRubyのコードのプロファイリングを行うとより詳細にボトルネックを見つけることが可能です。 また、Redisでキャッシュすることによる高速化は、ActiveResourceを使ったアプリケーションに限定されるテクニックではなく、様々なアプリケーションで使うことができます。やっていきましょう。
コマンドラインオプションをflagでパースしたとき-hを指定するとexit status2と出てしまう
どういうこと
golangではコマンドラインオプションをflagパッケージを使ってパースすることができる。
しかし、オプションに-h
、--help
を指定すると、以下のようにexit status2
と出てしまう。
なぜこんな仕様なのか...
$ go run flagSample.go -h Usage: -n int number to use (default 1234) exit status 2
これを出ないようにしたい。
対応策
flag.Usage
を使ってUsageを表示するときに動かす関数を指定してあげ、その関数内で os.exit(0)
を呼ぶと-h
、--help
を指定したときの statusコードは0となり、exit status2
は出なくなる。以下にコード例を示す。
コード
これは、-n
で指定した数字を表示するだけのコマンドラインツールのコードである。flag.Usage
で指定したUsage()
内でos.exit(0)
を呼んでいるので、exit status2
は出ない。
package main import ( "flag" "fmt" "os" ) var number int func usage() { fmt.Println("Usage:") flag.PrintDefaults() os.Exit(0) } func init() { const defaultNumber = 1234 flag.IntVar(&number, "n", defaultNumber, "number to use") flag.Usage = func() { usage() } } func main() { flag.Usage = func() { usage() } flag.Parse() fmt.Println("flag test") fmt.Printf("Number: %d\n", number) }
実行結果
$ go run flagSample.go -n 6666 flag test Number: 6666 $ go run flagSample.go -h Usage: -n int number to use (default 1234)
複数人のSSHの鍵をGitHubに登録している鍵を使ってシュッと鯖にいれる
各ユーザーがGitHubに登録している公開鍵は公開されていて誰でも見れるのでこれを使う。 大体の人はGitHubに鍵を登録しているだろうし、これを使えばシュッと鯖にSSHの公開鍵を設定することができて便利。 以下のようにcurlコマンド一発で複数人のSSHの鍵を登録することができる。
$ curl https://github.com/{user_id,user_id2,user_id3}.keys >> ~/.ssh/authorized_keys #user_id間にスペースいれると動かないので注意
ISUCONのときにも役立った。ISUCON、あと3000点ちょい....というところで予選敗退したので来年リベンジするぞ!!
Mitamaeでdotfilesの管理をやるようにした
Mitamaeでdotfilesの管理をするようにした。
Mitamaeとは
プロビジョニングツールのitameのmruby実装である。mruby実装にすることで何がうれしいかというとシングルバイナリとして動作するので、Ruby や gem に依存しなくなるというのがある。Mitamaeのバイナリがあれば動くので、curlで引っ張ってきてシュッと使うことができる。ちなみに読みは「見たまえ」ではなく「えむいたまえ」らしい。
これを使うと環境構築のための環境構築の手間を最小限にできる。 dotfilesのデプロイには今までシェルスクリプトを使っていたが、mitamaeを使うほうが便利であった。
構成
ぼくのdotfilesはtkmru/dotfilesで、k0kubun/dotfilesとakito19/dotfiles をかなり参考にしている。
$ tree -aL 2 . ├── .git │ ... ├── .gitignore ├── README.md ├── bin │ └── setup_mitamae.sh ├── config │ ├── .bash_aliases │ ├── .bash_profile │ ├── .bashrc │ ├── .gdbinit │ ├── .gitconfig │ ├── .profile │ ├── .tmux.conf │ └── .vimrc ├── cookbooks │ ├── gdb │ ├── git │ ├── symboliclinks │ ├── tmux │ └── vim ├── deploy.sh ├── init.sh ├── lib │ └── bootstrap.rb └── roles ├── darwin └── ubuntu
最初に実行するのはinit.shで、Mitamaeをcurlでダウンロードする。OSがmacだった場合、Xcode Command Line Toolsのインストールやhomebrewのインストールもここで行う。そして最後に、ホームディレクトリからdotfilesへのシンボリックリンクを張るdeploy.shを実行している。deploy.shはMitamaeを用いてlib/bootstrap.rbを実行し、roles.rbからcookbook以下のレシピを実行することでシンボリックリンクを張っている。 また、OSごとに使うdotfilesが違う(ubuntuの.profileとか)ので、deploy.sh内でunameを使ってOSを判定し、roles以下で異なる処理をさせている。
参考資料
GSoC完走できた〜💪
GSoC完走できた〜!!metasploit-framework に3ヶ月間、コントリビュートしていました。
linuxのstager周りを触るということでアセンブリ書いてお金💰もらってたけれど、そうそう業務でアセンブリ書かないだろうし、これが人生で最初で最後になる気がする。Googleからお金もらうのもこれ最後か〜と思うとエモくなってきた。
参加前にwebで参加記みたいなの読んでた時は、進捗報告ミーティングがしょっちゅうあるみたいなことを見ていたのでビビっていたけど、Metasploitでは適宜困ったら相談というかんじで平和に進行していった。試験期間はあまりコミットできないって言ったら了承してもらえたりもした。採用されるOSSによるだろうけど、ビビらずに応募するとよさそう。
期間中のおもしろエピソードとしては、チャットツールが最初はIRCだったけど、「ログ取るのにサーバ必要だしやだ〜」って言ったらTwitterのDMになって、それからGoogle Hangoutになって、最終的にslackになったこととか、rapid7から日本語できる人が出てきたというのがある。
書きかけのshellcodeが2つあるのでこれは後々プルリク送りたい〜。OSSに名前を残せたとかより、チャットなら英語でコミュニケーション問題なくとれることが分かったのが一番の収穫な気がする。とは顔本で書いたものの著名なOSSに名前が入るとアガる。
これからも継続してコミットしていけるといいですね。
HackIT CTF 2017 rev200 writeup
問題
Description: You haxor, come on you little sciddie… debug me, eh? You fucking little lamer… You fuckin’ come on, come debug me! I’ll get your ass, you jerk! Oh, you IDA monkey! Fuck all you and your tools! Come on, you scum haxor, you try to reverse me? Come on, you asshole!!
$ file rev200.efi rev200.efi: PE32+ executable (DLL) (EFI application) x86-64 (stripped to external PDB), for MS Windows
解く
シンボル付きのバイナリなので読みやすい。efi_main()を見ると、InitializeLib()でライブラリの初期化とInput()で入力の受け付けを行っているのが分かる。Inputの直後でalgo()をcallしていてあやしい。
algo()を見るとCorrect、Wrongを出力していて、ここで入力値のチェックをしていると分かる。algo()をhopperでデコンパイルすると以下のようになった。
int algo(int arg0) { rsp = rsp - 0x180; rbp = rsp + 0x80; arg_32 = arg0; for (arg_-1 = 0x0; arg_-1 <= 0x13; arg_-1 = arg_-1 + 0x1) { *(int32_t *)(rbp + sign_extend_32(arg_-1) * 0x4 + 0x90) = *(int8_t *)(arg_32 + sign_extend_64(arg_-1)) & 0xff & 0xff; } for (arg_-1 = 0x0; arg_-1 <= 0x13; arg_-1 = arg_-1 + 0x1) { // check input *(int32_t *)(rbp + sign_extend_32(arg_-1) * 0x4 + 0x90) = (((*(int32_t *)(rbp + sign_extend_32(arg_-1) * 0x4 + 0x90) ^ 0xc) + 0x6 ^ 0xd) + 0x7 ^ 0xe) + 0x8; *(int32_t *)(rbp + sign_extend_32(arg_-1) * 0x4 + 0x40) = (((*(int32_t *)(rbp + sign_extend_32(arg_-1) * 0x4 + 0x40) ^ 0xf) + 0x9 ^ 0x10) + 0xa ^ 0x11) + 0xb; } for (arg_28 = 0x0; arg_28 <= 0x13; arg_28 = arg_28 + 0x1) { *(int32_t *)(rbp + sign_extend_32(arg_28) * 0x4 + 0xffffffffffffffa0) = *(int32_t *)(rbp + sign_extend_32(arg_28) * 0x4 + 0x90); } for (arg_29 = 0x14; arg_29 <= 0x27; arg_29 = arg_29 + 0x1) { *(int32_t *)(rbp + sign_extend_64(arg_29 + 0xffffffffffffffec) * 0x4 + 0x40) = *(int8_t *)(arg_32 + sign_extend_64(arg_29)) & 0xff & 0xff; } for (arg_-1 = 0x14; arg_-1 <= 0x27; arg_-1 = arg_-1 + 0x1) { *(int32_t *)(rbp + sign_extend_32(arg_-1) * 0x4 + 0xffffffffffffffa0) = *(int32_t *)(rbp + sign_extend_32(arg_-1 - 0x14) * 0x4 + 0x40); } if (memcmp(&var_-96, correct, 0xa0) == 0x0) { rax = Print(u"\nCorrect\n", correct, 0xa0, r9); } else { rax = Print(u"\nWrong\n", correct, 0xa0, r9); } return rax; }
入力値をxorしたあと、memcmpでcorrectと比較している。 総当りで解くsolverを書いた。
# coding: UTF-8 correct = [104, 60, 121, 113, 99, 124, 129, 146, 146, 101, 101, 147, 146, 73, 121, 146, 56, 108, 60, 111, 123, 135, 88, 85, 137, 90, 89, 126, 126, 107, 135, 108, 87, 108, 107, 88, 89, 90, 90, 111]; flag = "" for i in range(20): for j in range(256): if ((((((j ^ 0xc) + 6) ^ 0xD) + 7) ^ 0xe) + 8) == correct[i]: flag += chr(j) for i in range(20): for j in range(256): if (((((j ^ 0xf) + 9) ^ 0x10) + 10) ^ 0x11) + 11 == correct[i+20]: flag += chr(j) print(flag)
z3pyを使っても解くことができる
# coding: UTF-8 from z3 import * FLAG_LENGTH = 40 correct = [104, 60, 121, 113, 99, 124, 129, 146, 146, 101, 101, 147, 146, 73, 121, 146, 56, 108, 60, 111, 123, 135, 88, 85, 137, 90, 89, 126, 126, 107, 135, 108, 87, 108, 107, 88, 89, 90, 90, 111]; s = Solver() text = [] for i in range(FLAG_LENGTH): text.append(BitVec(i, 8)) s.add(And(text[i] >= 0x20, text[i] < 0x7f)) # in printable ascii for i in range(20): s.add(((((((text[i] ^ 0xC) + 6) ^ 0xD) + 7) ^ 0xE) + 8) == correct[i]) for i in range(20, 40): s.add((((((text[i] ^ 0xF) + 9) ^ 0x10) + 10) ^ 0x11) + 11 == correct[i]) if s.check() == sat: m = s.model() flag = '' for i in range(FLAG_LENGTH): flag += chr(int(str(m[text[i]]))) print(flag)
$ python solver.py h4ck1t{ff77af3cf8d4e1e67c4300aeb5ba6344}
HackIT CTF 2017 rev150 writeup
問題
Description: Looks like this packer can not unpack what has been packed :( There are 2 mistakes in unpacking procedure. It leads to the error. Try to fix unpacker and figure out what is inside.
packerとpackedという2つの64bitのELFが与えられる。
解く
packedを動かすと0x6b5489でSegmentation faultで落ちる。
$ ulimit -c unlimited $ strace ./packed execve("./packed", ["./packed"], [/* 74 vars */]) = 0 --- SIGSEGV {si_signo=SIGSEGV, si_code=SI_KERNEL, si_addr=0} --- +++ killed by SIGSEGV (core dumped) +++ Segmentation fault (コアダンプ) $ gdb ./packed core Reading symbols from ./packed...(no debugging symbols found)...done. [New LWP 10428] Core was generated by `./packed'. Program terminated with signal SIGSEGV, Segmentation fault. #0 0x00000000006b5489 in ?? () gdb-peda$
落ちた0x6b5489周辺を見てみると、0x6b548fでraxがindexのカウンタとして使われているのに0x6b5489ではrdxをindexにしていておかしいような気がしてくる。 0x6b548fでraxに+2しているのもあやしい。
0x6b548f、0x6b5489を以下のようにhopperで変更する。
00000000006b5489 xor byte [rax], dl ; CODE XREF=_6b5468+46 00000000006b548b ror rdx, 0x8 00000000006b548f add rax, 0x1 00000000006b5493 cmp rax, rcx
実行するとまたも Segmentation faultで落ちる。
$ ulimit -c unlimited $ strace ./modify_packed execve("./modify_packed", ["./modify_packed"], [/* 74 vars */]) = 0 uname({sys="Linux", node="ubuntu", ...}) = 0 brk(0) = 0x25ec000 brk(0x25ed1c0) = 0x25ed1c0 arch_prctl(ARCH_SET_FS, 0x25ec880) = 0 readlink("/proc/self/exe", "/mnt/hgfs/ctf2/hackit/rev150/Bro"..., 4096) = 55 brk(0x260e1c0) = 0x260e1c0 brk(0x260f000) = 0x260f000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 9), ...}) = 0 write(1, "The hardest part is overcome.\n", 30The hardest part is overcome. ) = 30 --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0} --- +++ killed by SIGSEGV (core dumped) +++ Segmentation fault (コアダンプ) $ gdb ./modify_packed core Reading symbols from ./modify_packed...(no debugging symbols found)...done. [New LWP 10774] Core was generated by `./modify_packed'. Program terminated with signal SIGSEGV, Segmentation fault. #0 0x000000000042194a in ?? () gdb-peda$ bt #0 0x000000000042194a in ?? () #1 0x0000000000400bde in ?? () #2 0x0000000000400e63 in ?? () #3 0x00000000004010ee in () #4 0x00000000004009ba in ()
The hardest part is overcomeと出力された。やったぜ。 バックトレース結果にある0x400bdbを見ていく。
gdb-peda$ x/19i 0x400bdb =>0x400bdb: idiv edi 0x400bdd: inc DWORD PTR [rbp+0x481375c0] 0x400be3: mov eax,DWORD PTR [rbp-0x10] 0x400be6: add rax,0x10 0x400bea: mov rax,QWORD PTR [rax] 0x400bed: mov rdi,rax 0x400bf0: call 0x400aae <"> 0x400bf5: mov rax,QWORD PTR [rbp-0x10] 0x400bf9: add rax,0x8 0x400bfd: mov rax,QWORD PTR [rax] 0x400c00: lea rsi,[rip+0x88fa9] # 0x489bb0 0x400c07: mov rdi,rax 0x400c0a: call 0x400370 0x400c0f: test eax,eax 0x400c11: jne 0x400c18 0x400c13: call 0x400b39 0x400c18: mov eax,0x0 0x400c1d: leave 0x400c1e: ret
0x400c00でrsiに0x489bb0のstringを入れている。
gdb-peda$ x/s 0x489bb0 0x489bb0: "decr"
直後でcallされている0x400b39があやしいので見る。
gdb-peda$ x/33i 0x400b39 0x400b39: push rbp 0x400b3a: mov rbp,rsp 0x400b3d: sub rsp,0x10 0x400b41: lea rdi,[rip+0x2b1558] # 0x6b20a0 0x400b48: call 0x417bb0 0x400b4d: mov DWORD PTR [rbp-0x8],eax 0x400b50: mov eax,DWORD PTR [rbp-0x8] 0x400b53: mov DWORD PTR [rbp-0x4],eax 0x400b56: cmp DWORD PTR [rbp-0x4],0x0 0x400b5a: js 0x400ba6 0x400b5c: mov eax,DWORD PTR [rbp-0x4] 0x400b5f: movsxd rdx,eax 0x400b62: lea rax,[rip+0x2b1537] # 0x6b20a0 0x400b69: movzx ecx,BYTE PTR [rdx+rax*1] 0x400b6d: mov eax,DWORD PTR [rbp-0x8] 0x400b70: sub eax,0x1 0x400b73: sub eax,DWORD PTR [rbp-0x4] 0x400b76: movsxd rdx,eax 0x400b79: lea rax,[rip+0x2b1520] # 0x6b20a0 0x400b80: movzx eax,BYTE PTR [rdx+rax*1] 0x400b84: xor eax,ecx 0x400b86: xor eax,0xffffff80 # 実質0x80 0x400b89: mov BYTE PTR [rbp-0x9],al 0x400b8c: mov eax,DWORD PTR [rbp-0x4] 0x400b8f: movsxd rdx,eax 0x400b92: lea rax,[rip+0x2b1507] # 0x6b20a0 0x400b99: movzx ecx,BYTE PTR [rbp-0x9] 0x400b9d: mov BYTE PTR [rdx+rax*1],cl 0x400ba0: sub DWORD PTR [rbp-0x4],0x1 0x400ba4: jmp 0x400b56 0x400ba6: nop 0x400ba7: leave 0x400ba8: ret
0x400b62、0x400b79、 0x400b92でraxに代入されているアドレス 0x6b20a0 はフラグの一部を含んでいた。この関数内でxorすることでフラグを生成しているようである。
gdb-peda$ x/s 0x6b20a0 0x6b20a0: "\225\300Ӆ\302\307ˉ\260\201\302ޚ\255\223\223\263\204\227\300\264\214گ_no_se1ler_sn0rom{t1kc4h"
フラグ生成処理を書き起こす。
# coding: UTF-8 flag_parts = bytearray(open("packed","rb").read()) offset = 0xb20a0 first_flag = "" latter_flag = str(flag_parts[offset+0x30/2: offset + 0x30]) for i in range(0x30/2): first_flag += chr(flag_parts[offset+i] ^flag_parts[offset+0x2f-i] ^ 0x80) print((first_flag + latter_flag)[::-1])
フラグが出た
$ python solver.py h4ck1t{mor0ns_rel1es_on_p4ck3r5_vari0rs_d03sn0t}
最初のバイナリを修正するのがちょっとエスパーなかんじがする。意図的に動作しないバイナリが与えられる問題は珍しい。