Akatsuki Hackers Lab | 株式会社アカツキ(Akatsuki Inc.)

Akatsuki Hackers Labは株式会社アカツキが運営しています。

CloudFormationでAuroraのMySQL5.7インプレースアップグレードをする際のハマりポイント&対応方法

Amazon Aurora MySQL のインプレースアップグレード機能

つい先日、Amazon Aurora にて、MySQL 5.6互換のバージョン1.xから、MySQL 5.7互換のバージョン2.xへインプレースアップデートが可能になりました。

Amazon Aurora が MySQL 5.6 から 5.7 へのインプレースアップグレードをサポート

この機能を使うことで、従来のようにスナップショットからの復元やレプリケーション、インスタンスの切替といった作業をしなくても、エンドポイントなどはそのままに、簡単にMySQL5.7互換にアップグレードすることが可能になります。

早速この機能でアップグレードを試みたのですが、CloudFormationで管理されているAuroraクラスターをアップグレードする際にちょっとハマったことがあったので、その内容と対応方法をシェアします。

アップデート前のRDS用CloudFormation stack

更新前のDBクラスタのCloudFormationでの定義はこんな感じでした。(わかりやすいよう最小限の記述のみにしてあります)

DevAuroraParameterGroup というカスタムのインスタンスパラメータグループを使っています。

---
AWSTemplateFormatVersion: '2010-09-09'
Description: RDS for sandbox
Resources:
  DevAuroraParameterGroup:
    Type: AWS::RDS::DBParameterGroup
    Properties:
      Description: Development Aurora parameter group
      Family: aurora5.6
      Parameters:
        max_connections: '1024'
        wait_timeout: '300'
  DevAuroraParameterGroup57:
    Type: AWS::RDS::DBParameterGroup
    Properties:
      Description: Development Aurora parameter group for 5.7
      Family: aurora-mysql5.7
      Parameters:
        max_connections: '1024'
        wait_timeout: '300'
  DBCluster:
    Type: AWS::RDS::DBCluster
    Properties:
      DBClusterIdentifier: test-cluster
      DBSubnetGroupName:
        Fn::ImportValue: DBSubnetGroup
      Engine: aurora
      EngineVersion: 5.6.mysql_aurora.1.22.2
      MasterUsername: "{{resolve:ssm:rds.username.default:1}}"
      MasterUserPassword: "{{resolve:ssm-secure:rds.password.default:1}}"
  DBInstance:
    Type: AWS::RDS::DBInstance
    DeletionPolicy: Delete
    Properties:
      DBClusterIdentifier:
        Ref: DBCluster
      DBInstanceClass: db.t2.medium
      DBInstanceIdentifier: test-db
      Engine: aurora
      EngineVersion: 5.6.mysql_aurora.1.22.2
      DBParameterGroupName:
        Ref: DevAuroraParameterGroup
      DBSubnetGroupName:
        Fn::ImportValue: DBSubnetGroup

ハマったこと

これを、以下のように書き換えて適用することで、MySQL5.7互換バージョンへのインプレースアップグレードを試みます。

@@ -82,8 +82,8 @@
       DBClusterIdentifier: test-cluster
       DBSubnetGroupName:
         Fn::ImportValue: DBSubnetGroup
-      Engine: aurora
-      EngineVersion: 5.6.mysql_aurora.1.22.2
+      Engine: aurora-mysql
+      EngineVersion: 5.7.mysql_aurora.2.07.3
       MasterUsername: "{{resolve:ssm:rds.username.default:1}}"
       MasterUserPassword: "{{resolve:ssm-secure:rds.password.default:2}}"
   DBInstance:
@@ -94,9 +94,9 @@
         Ref: DBCluster
       DBInstanceClass: db.t2.medium
       DBInstanceIdentifier: test-db
-      Engine: aurora
-      EngineVersion: 5.6.mysql_aurora.1.22.2
+      Engine: aurora-mysql
+      EngineVersion: 5.7.mysql_aurora.2.07.3
       DBParameterGroupName:
-        Ref: DevAuroraParameterGroup
+        Ref: DevAuroraParameterGroup57
       DBSubnetGroupName:
         Fn::ImportValue: DBSubnetGroup

しかし、以下のようなエラーで失敗してしまいました。

{
  "LogicalResourceId": "DBCluster",
  "ResourceStatus": "UPDATE_FAILED",
  "ResourceStatusReason": "The current DB instance parameter
     group sandbox-rds-devauroraparametergroup-fxfe7dqcn31p
     is custom. You must explicitly specify a new DB instance
     parameter group, either default or custom, for the engine
     version upgrade. (Service: AmazonRDS; Status Code: 400; Error Code: InvalidParameterCombination; Request ID: ...)"
}

カスタムのインスタンスパラメータグループを使っているため、5.7に対応した新しい DevAuroraParameterGroup57 に切り替えるよう指定しなければなりません。 CLIであれば modify-db-cluster--db-instance-parameter-group-name オプションでこれを指定可能です。 しかし、どうやらCloudFormationではこれを渡すことができない様子です。同時に DBInstance の更新をしても、異なるリソースの変更までは拾ってはくれないようです。。

対応方法その1

こういう時の一つの方法は、いったんCloudFormationの管理下から外してしまい、手動でアップデートしたのち、CloudFormationにインポートし直すという方法です。

  1. DBCluster および DBInstanceDeletionPolicy: Retain を指定して適用することで、CloudFormationによってこれらが削除されないようにします。

  2. DBCluster および DBInstance の定義を削除し、更新を行います。

  3. 残ったクラスターをAWSコンソールで5.7に更新します。

  4. 5.7の定義で再度 DBCluster および DBInstance の定義を追加し、「インポート」 を行います。この時、追加する各リソースに対応するインスタンスの識別子(ここでは test-clustertest-db )を指定することで、CloudFormationスタックに既存リソースを取り込むことが可能です。

  5. ドリフト検知を行い、定義と実態にズレがないことを確認します。

しかし、手動オペレーションはなるべく避けたいということで、次のような方法も考えてみました。

対応方法その2

エラーメッセージを見るに、カスタムのインスタンスパラメータグループを使わなければ良いのだろうということで、いったんデフォルトに戻し、インブレースアップグレード後に再びカスタムのパラメータを適用しなおしてみます。

まず、 DBInstance のパラメータグループをデフォルトである default.aurora5.6 にしてスタックを更新します。

@@ -96,7 +96,6 @@
       DBInstanceIdentifier: test-db
       Engine: aurora
       EngineVersion: 5.6.mysql_aurora.1.22.2
-      DBParameterGroupName:
-        Ref: DevAuroraParameterGroup
+      DBParameterGroupName: default.aurora5.6
       DBSubnetGroupName:
         Fn::ImportValue: DBSubnetGroup

次に、 インプレースアップグレードと合わせて、新しいカスタムのパラメータグループも指定して再度更新します。

@@ -82,8 +82,8 @@
       DBClusterIdentifier: test-cluster
       DBSubnetGroupName:
         Fn::ImportValue: DBSubnetGroup
-      Engine: aurora
-      EngineVersion: 5.6.mysql_aurora.1.22.2
+      Engine: aurora-mysql
+      EngineVersion: 5.7.mysql_aurora.2.07.3
       MasterUsername: "{{resolve:ssm:rds.username.default:1}}"
       MasterUserPassword: "{{resolve:ssm-secure:rds.password.default:2}}"
   DBInstance:
@@ -94,8 +94,9 @@
         Ref: DBCluster
       DBInstanceClass: db.t2.medium
       DBInstanceIdentifier: test-db
-      Engine: aurora
-      EngineVersion: 5.6.mysql_aurora.1.22.2
-      DBParameterGroupName: default.aurora5.6
+      Engine: aurora-mysql
+      EngineVersion: 5.7.mysql_aurora.2.07.3
+      DBParameterGroupName:
+        Ref: DevAuroraParameterGroup57
       DBSubnetGroupName:
         Fn::ImportValue: DBSubnetGroup

これで、無事にCloudFormationのみでインプレースアップグレードが完了しました。 またMySQL5.7への移行完了後も、DBは問題なく利用できることを確認できました。

後者の方法は途中でパラメータグループの更新が余分に入るという欠点がありますが、対象が多くて自動化したい場合などはこちらの手段の方が良いこともあるかもしれません。

もっとうまく対応できる方法が提供されるとありがたいのですけれどね。

モバイルで動くShaderでの流体表現


この記事は Akatsuki Advent Calendar 2020の25日目の記事です.
前回の記事は脆弱性診断時のAndroidのプロキシ設定を行うコマンドラインツールを作った話+その他内製ツールの紹介でした。

 

 

はじめに


新卒研修で流体を用いたスマホゲームをリリースする機会がありましたので,実装を紹介します.

具体的には,ミラーケーキ(ケーキの一種)を作成するゲーム内で,
ケーキに溶けたチョコをかけてコーティングする作業(グラサージュ)の実装です.

(実際のアプリ: iOS, Android

実際の動画


実際のゲームプレイ動画がこちらになります.この部分の実装方法を以下で説明していきます.

f:id:AxI:20201223125005g:plain
ちなみに実際のミラーケーキはこんな感じです.

実装

概要:ケーキの柄が変わってるだけ

モバイルで三次元流体表現を行うことはスペック上難しいので,二次元で液体挙動の近似計算を行い,影で立体感をつけることで上記の挙動を実現しています.
具体的には,ケーキオブジェクトのテクスチャをリアルタイムで変化させることで,ケーキの表面に液体が流れているかのように見せています.

f:id:AxI:20201224123839p:plain
ぱっと見,ケーキになにか物体が覆いかぶさっていくように見えますが,実はケーキの柄が変わっているだけです.

実装は主に3つのパートに分かれています.以下の処理はUnity×Shaderで実装されています.
1.液体の広がりを表現
2.液体の流れを表現
3.液体の立体感を表現

液体の広がり


液体の広がりでは,ミラーケーキでのグラサージュの特徴である,注いだ点からじわーっと広がる挙動を表現しています.

具体的には二次元上で液量を定義し,”ぼかし”(平滑化)を行うことで液体が多いところから少ないところへ広がる動きを計算しています.毎フレームShader上で周囲のピクセルの液量の平均をそのピクセルの次の値とすることで,下図のように注いだ点から周囲に広がり,付近の液量と影響しあう挙動となります.

f:id:AxI:20201223133432p:plain

上図において,液量2以上など,閾値以上の部分のみ液体が存在すると定義し,液体が存在するところは別のテクスチャを参照するとすることで下図のようになります.

f:id:AxI:20201223133450p:plain

液体の流れ


液体の流れでは,液体が流れていく方向へ色も移動する処理を行います.

先ほどの液体の広がりで用いた図を見ると,液体範囲の縁は閾値に近くなることがわかります.よって,閾値に近い部分に黄色をつけるとするならば,黄色は常に外側,つまり液体が流れていく方向へ移動することになります.
このように液量が似ているピクセルは似た色を参照するという仕組みにすることで,流れていく方向に色が移動しているような挙動になります.

f:id:AxI:20201223133505p:plain
具体的には,液体範囲が液体テクスチャを参照する際,テクスチャの参照座標を液量分ずらすことで,任意のテクスチャにおいて液量に伴い変動するマーブル模様が生成され.流体っぽい挙動を実現しています.(時間が経つと戻っていく など,実際の流体挙動とは大きく異なる部分があります)

f:id:AxI:20201223133516p:plain

液体の立体感


液体の立体感は,液体の広がりで計算された,液体が存在する範囲の周囲に影をつけることで表現されています.
具体的には,テクスチャ上で光の方向を定義し,液体が存在しない かつ 光の逆向きに隣接するピクセルに液体が存在するピクセルを黒くします.

f:id:AxI:20201223133532p:plain

f:id:AxI:20201223133542p:plain

影をつけることで一気に立体感が出たと思います.影と反対側にハイライトをつけることでより立体感,艶感がでます.
上記の三つの要素を合体させることでモバイルで動く流体表現を実装しました.

おわりに


実際のアプリリンクです.楽しんでいただけたら幸いです.

Mirror cakes - Apps on Google Play

Mirror cakes on the App Store

 

限定的な状況下の実装ですがみなさまの発想の助けになれば幸いです.
来年も,おもしろいがたくさん生まれますように,メリークリスマス.

 

【LT会】Akatsuki Geek Live 2020-2021 開催レポート!Vol.1

こんにちは!アカツキ採用担当のこさだです。

12/14(月)に、本年度1回目となる「Akatsuki Geek Live2020-2021 Vol.1」を開催しました!この状況下ですので、初の完全オンライン実施となりましたが、50名以上の方にご参加をいただき、とても良い場になりました!今回は、そのイベント当日の様子をご紹介したいと思います!

 

「Akatsuki Geek Live」とは?

エンジニアを志す学生さんと、アカツキエンジニアが登壇するLT会です。フリーテーマで7分間のLTを実施、その後懇親会を実施し、交流の場を設けています。楽しみながらアカツキのことを知れる場、そして学生さん同士の横のつながりも作れる場として実施しております。

 

▼イベント概要はこちらから

aktsk.connpass.com

 

 #当日の様子

今回は完全オンラインでの開催となりました。昨年まではアカツキオフィスに実際にお越しいただいての開催だったため、少し不安もありましたが、当日は50名を超える方にご参加いただきました!

はじめに採用担当のまえだからスタートし、アカツキのエンジニア3名、来春入社予定の内定者2名、一般学生1名の計7名が登壇。
登壇者から資料掲載の承諾をいただきましたので、下記にスライドをご紹介します!

 

▼@まえださん(アカツキ/採用担当)

なるほどよくわかる!アカツキ理解のヒント 

トップバッターはエンジニアの採用を担当しているまえださんから。
アカツキを知るにはどうしたらいいか?や学生のみなさんがなかなか聞きづらいポイントを解説しました!

 【登壇資料】

 

▼@になさん(アカツキ/17年度新卒入社)

Unity の CI 環境をGithub Actions で構築した話

エンジニアトップバッターはになさん。
なぜやるか?「やってみたかったから」というのが正直でいいですよね!
問題意識をもったこと、やりたいと思う方法をやってみる。「ワクワクして働く」ということを体現している一つの例かもしれませんね。

【登壇資料】

 

▼@Bapliscaさん(一般学生)

「ボイストラベラー」サークルで新感覚ボイスゲームを作った話
学祭で制作した音声認識でUFOを操るゲームについて発表していただきました。
「あ〜〜〜⤴︎⤴︎あ〜⤵︎⤵︎ ・・・ビーム!!!!」とデモも披露していただき、笑いが巻き起こり、チャットも盛り上がる時間でした(個人的にも画面越しにめちゃくちゃ笑いました)
アセットを使わずに制作したとのことで、様々な工夫が素晴らしかったです。
「子供が操作しやすいゲームを作りたい」という目的が達成されていて、とても素晴らしいと思いました!遊んでみたい!(888888)

【登壇資料】

 

▼@なかじさん(学生/21年度新卒内定)

DOTweenはいいぞ

来年、アカツキへの入社を決めてくれている21卒内定者のなかじさんです。
ゲームを開発されている学生の皆さんから「DOTween使ってみたい!」という声がチャットから溢れてきていました!
【登壇資料】

 

▼@いたみんさん(アカツキ/19年度新卒入社)

研修でゲームをリリースした話

入社後の研修で作ったゲームの話でした。毎年少しずつ研修内容は変わるのですが、入社直後の研修で作ったゲームをすぐリリース!というのはなかなか実施されている企業は多くないのではないでしょうか。作ってみて初めてわかる難しさ、「感覚的に楽しくできる」にたどり着くまでは色々な苦労があるんですよね。

【登壇資料】

 

▼@すぎやんさん(学生/21年度新卒内定)

競プロと業プロの関係性について

なかじさんと同じく、内定者のすぎやんさん。
就職活動で感じた違和感と、何から初めていいかわからないという思いから、「競プロ」を始めてよかったぞ!という話をしてくれました。
面接での自己アピールの伝え方を変えてみることや、自分でもサービスを作ってみることで自分にも変化があったとのこと。視点を変えて考えてみるというのは本当に大事!

【登壇資料】

 

▼@ちょうさん(アカツキ/19年度新卒入社)

RailsのN+1あるある

アカツキで実際の業務で使っているRuby on Railsdでの例をもとに、わかりやすく解説してくれました!当日司会をしていた島村と同じチームということもあり、先輩後輩による掛け合いも面白かったですねw

【登壇資料】

 

 

#LTを経て懇親会!
オンラインでの開催でしたが、チャットで盛り上げてくださる学生の皆さんのおかげもあり、発表者も楽しくLT登壇を終えられました。その空気感のままオンライン懇親会へ。
なんだかまるでラジオの公開収録をみているような雰囲気でしたね!皆さんからチャットで投稿される質問に答えていきながら、アカツキの業務やコロナ禍リモートでの業務について、登壇者へ向けての質問など様々な話題が時間いっぱいまで活発に飛び交っていました。アカツキの雰囲気も伝わる有意義な時間になったのではないでしょうか?
オフィスで直接みなさんにお会いできなかったのは残念でしたが、ご来社いただける状況が戻ってきた時には、ぜひ遊びに来てくださいね^^
 

改めて、ご参加いただいた皆さま本当にありがとうございました!この時間ががみなさんのエンジニアとしての気付きや新たな1歩、また良き出会いの場になっていれば嬉しいです!

 

最後にオフショットを★(広〜い空間で、こんな風にお届けしておりました)

f:id:megumikosada:20201221093503j:plain

次回は2月頃に開催できたらいいな、と思っています!ぜひまた会いましょう〜!

メリークリスマス!!

脆弱性診断時のAndroidのプロキシ設定を行うコマンドラインツールを作った話+その他内製ツールの紹介

こんにちは、セキュリティエンジニアの小竹 泰一(aka tkmru)です。 アカツキでは、Webアプリケーション、ゲームアプリに対する脆弱性診断や社内ネットワークに対するペネトレーションテスト、ツール開発/検証などを担当しています。

今日は、先日作成したAndroidのプロキシ設定をコマンドラインから行うツールの紹介と、その他にもある脆弱性診断に使える内製ツールの紹介をします。 この記事は、Akatsuki Advent Calendar 2020の24日目の記事です。

脆弱性診断に使うプロキシツールとは

脆弱性を発見する方法のひとつに、対象となるWebアプリ、スマホアプリの通信内容をプロキシツール上で確認し、リクエストやレスポンスを編集するという方法があります。 脆弱性診断時に用いるプロキシツールは、Burp SuiteやPacketProxyなどが知られています。

f:id:TAKEmaru:20201217044534p:plain
中間者攻撃を行う様子

スマホアプリの通信をプロキシツールへと向ける方法

スマホアプリの通信をプロキシツール上で確認するには、端末のネットワーク設定を変更しプロキシツールへと通信を曲げる必要があります。 ここではよく用いられる2つの方法を紹介します。

プロキシのIPアドレス、ポートを指定

プロキシツールが動作しているPCのIPアドレス、ポートを端末のネットワーク設定画面で指定することでプロキシツールへと通信を向けることができます。 これは最も簡単な方法です。

Androidでは、Wi-Fiの詳細設定から、プロキシのIPアドレス、ポートを指定可能です。iOSを使用している場合も設定する項目は同じです。

Android上でプロキシを設定する画面

DNSサーバーを追加し、レコードを偽装する

PacketProxyには自由にDNSレコードを変更できるDNSサーバーが組み込まれています。 このDNSサーバー上でアプリの通信先のドメインを指定し、プロキシツールを経由して通信するようにDNSレコードを偽装することで、通信内容を取得できます。 この方法は、指定したドメインとの通信のみを取得できる点や、SSL Pinningが施されていても通信先のドメインを確認することができる点で便利です。

Androidでは、プロキシのIPアドレスを指定する場合と同じくWi-Fiの詳細設定画面から、DNSの設定を変更可能です。 ここで指定するDNSサーバーのIPアドレスはPacketProxyが動作しているPCのアドレスです。 Androidの場合は、次の画像のように端末のIPアドレス、ゲートウェイのIPアドレス、ネットワークプレフィックス長も指定する必要があります。 iOSの場合はDNSのアドレスを差し替えるだけでOKです。

Android上でDNSサーバーを設定する画面

コマンドラインからプロキシの設定を行えるツールを作成した

プロキシの設定をGUIを操作して行うのは少し手間がかかります。 特にAndroidでDNSサーバーを指定するには、DHCPをオフにして端末のIPアドレス等も指定しないといけないのでめんどくさいです。 そのため、コマンドラインからプロキシの設定を簡単に行えるよう、aprox を作成しました。

github.com

インストール方法

aproxはPythonで作成されているため、pipでインストール可能です。

$ pip install git+ssh://git@github.com/aktsk/aprox.git

使い方

サブコマンドを指定することで、機能を呼び出せます。

プロキシを指定する

proxyコマンドでプロキシが待ち受けているIPアドレス、ポートを設定できます。

$ aprox proxy 192.168.100.10:8080
Local proxy has been set up

DNSサーバーを指定する(root化端末限定)

dnsコマンドでDNSサーバーのIPアドレスを指定できます。 この機能はroot権限が必要なndcコマンドを使っているため、root化端末をPCにつないでいる場合しか使えません。

$ aprox dns 192.168.100.10
200 0 Resolver command succeeded

設定をクリアする

clearコマンドでプロキシ、DNSサーバの設定をクリアできます。

$ aprox clear
Local proxy is not configured...
Cleared local DNS settings!!

開発時に知ったデバッグテクニック

ndcコマンドに関してはあまりドキュメントがなく、扱いに苦戦しました。そのため、DNSのリクエストが本当に端末から出ているのか確かめたくなりました。 ちなみにaproxの内部では次のようにndcコマンドを使って、DNSサーバーを追加しています。

$ su
# dumpsys netd # 現在の使用しているネットワークIDを確認
# ndc resolver setnetdns <ネットワークID> '' <IP アドレス> 

DNSのログは次のように確認できました。 iptablesコマンドを使い、ログの先頭にDNS_QUERIESという文字列を追加し、dmesgコマンドでログを確認しています。

3|sargo:/ $ su
sargo:/ # iptables -I OUTPUT -m udp -p udp --dport 53 -j LOG --log-prefix 'DNS_QUERIES '
sargo:/ # dmesg -w | grep 'DNS_QUERIES'
[54672.362220] DNS_QUERIES IN= OUT=wlan0 SRC=192.168.100.20 DST=192.168.100.15 LEN=59 TOS=0x00 PREC=0x00 TTL=64 ID=40850 DF PROTO=UDP SPT=41015 DPT=53 LEN=39 MARK=0xf0068 
[54672.525921] DNS_QUERIES IN= OUT=wlan0 SRC=192.168.100.20 DST=106.187.2.33 LEN=59 TOS=0x00 PREC=0x00 TTL=64 ID=50180 DF PROTO=UDP SPT=55042 DPT=53 LEN=39 MARK=0xc0068 
[54672.526507] DNS_QUERIES IN= OUT=wlan0 SRC=192.168.100.20 DST=106.187.2.33 LEN=59 TOS=0x00 PREC=0x00 TTL=64 ID=50181 DF PROTO=UDP SPT=26049 DPT=53 LEN=39 MARK=0xc0068 
[54672.589872] DNS_QUERIES IN= OUT=wlan0 SRC=192.168.100.20 DST=192.168.100.15 LEN=59 TOS=0x00 PREC=0x00 TTL=64 ID=40865 DF PROTO=UDP SPT=42598 DPT=53 LEN=39 MARK=0xf0068 
[54672.847426] DNS_QUERIES IN= OUT=wlan0 SRC=192.168.100.20 DST=106.187.2.33 LEN=67 TOS=0x00 PREC=0x00 TTL=64 ID=50221 DF PROTO=UDP SPT=33378 DPT=53 LEN=47 MARK=0xc0068 
[54672.848117] DNS_QUERIES IN= OUT=wlan0 SRC=192.168.100.20 DST=106.187.2.33 LEN=67 TOS=0x00 PREC=0x00 TTL=64 ID=50222 DF PROTO=UDP SPT=31970 DPT=53 LEN=47 MARK=0xc0068 
[54672.849920] DNS_QUERIES IN= OUT=wlan0 SRC=192.168.100.20 DST=106.187.2.33 LEN=63 TOS=0x00 PREC=0x00 TTL=64 ID=50223 DF PROTO=UDP SPT=50412 DPT=53 LEN=43 MARK=0xc0068

また、このようにログを確認することで、アプリから出るDNSのリクエストの宛先は指定したものに変更されているが、ブラウザから出るDNSのリクエストの宛先はデフォルトのままであることが分かりました。 アプリの脆弱性診断では現状のままで困らないので、このまま公開しています。

他の内製診断ツール

アカツキでは、他にも脆弱性診断時に使えるツールを作成し、OSSとして公開しています。

apk-medit

apk-meditは、root権限を必要としないメモリ改ざんツールです。 メモリ改ざんはスマホゲームの脆弱性診断時に見る項目のひとつです。 このツールに関しては、Black Hat USA Arsenalや、CODE BLUE Blueboxなどで発表しました。

github.com

以前にも社のブログに紹介記事を書いたので、興味があればこちらも読んで見てください!

hackerslab.aktsk.jp

apkutil

apkutilは、APKファイルをデコードする際にandroidmanifest.xmlをパースし様々な情報を表示したり、脆弱性診断に必要なパッチを自動で当てたりしてくれるツールです。

github.com

次のようにコマンドを実行することで、debuggable属性をtrueにしたり、networkSecurityConfigの設定をしたりしつつ、再度ビルドしてくれます。

$ apkutil all sample.apk
Decoding APK by Apktool...
I: Using Apktool 2.4.1 on sample.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
...

Potentially Sensitive Files:
sample/README.md
sample/hoge.sh

Checking AndroidManifest.xml...
Permission:
android.permission.INTERNET

Debuggable:
False

AllowBackup:
False

Custom schemas:
None

Set debuggable attribute to true in AndroidManifest!

Set networkSecurityConfig attribute to true in AndroidManifest!

Building APK by Apktool...
I: Using Apktool 2.4.1
I: Checking whether sources has changed...
...

Signing APK by apksigner...
Signed

Output: sample.patched.apk

上で挙げたapk-meditはdebuggable属性がtrueになっているアプリを前提に作られているので、apk-meditを使う際にはとても便利です。

ipautil

ipautilは、IPAファイルをデコードする際にInfo.plistをパースし様々な情報を表示したり、センシティブなファイルがないか確認したりしてくれるツールです。

github.com

次のようにコマンドを実行することで、IPAファイルをデコードできます。

$ ipautil decode sample.ipa
Decoding IPA...
Archive: sample.ipa
   creating: Payload/
   creating: Payload/demo-client iOS develop.app/
   creating: Payload/demo-client iOS develop.app/_CodeSignature/
  inflating: Payload/demo-client iOS develop.app/_CodeSignature/CodeResources  
  inflating: Payload/demo-client iOS develop.app/demo-client iOS develop  
...
  inflating: Payload/demo-client iOS develop.app/Info.plist  

CFBundleName:
demo-client iOS develop

CFBundleDisplayName:
demo-dev

Checking AppTransportSecurity...
True

NSExceptionDomains:
facebook.com

Custom schemas (CFBundleURLSchemes):
None

Potentially Sensitive Files:
./Payload/demo-client iOS develop.app/README.md
./Payload/demo-client iOS develop.app/hoge.sh

Output: ./Payload

また、IPAファイルはパッチ当てた後、リザインしないとインストールできませんが、リザインは結構手間です。 ~/ipautil.jsonに証明書の情報を書いておくことで、コマンド1発でリザインする機能もあります。

$ ipautil sign Payload/
Signing IPA by codesign...
Payload/demo-client iOS develop.app/Frameworks/Hoge.framework: replacing existing signature
Payload/demo-client iOS develop.app/Frameworks/Fuga.framework: replacing existing signature
Payload/demo-client iOS develop.app: replacing existing signature
Signed

NWPentestUtils

NWPentestUtilsは、内部ネットワークでのペネトレーションテスト業務で使用しているスクリプト集です。

github.com

DBサーバーを指定したネットワークレンジから見つけるfind-db.shやブルートフォース攻撃を行うbruteforce-postgres.shbruteforce-mysql.shbruteforce-redis.shあたりは特にお気に入りです。

$ ./find-db.sh target-ip.txt 
Target: 192.168.100.0/24
172.16.0.0/12
Now Launching: nmap -p3306 -v -oX results/20201221/192.168.100.0_24_mysql_20201221_140057.xml 192.168.100.0/24
Starting Nmap 7.91 ( https://nmap.org ) at 2020-12-21 14:00 JST
Initiating Ping Scan at 14:00
Scanning 512 hosts [2 ports/host]
...
Read data files from: /usr/local/bin/../share/nmap
Nmap done: 512 IP addresses (9 hosts up) scanned in 34.01 seconds
Now Launching: nmap -p5432 -v -oX results/20201221/192.168.100.0_24_postgre_20201221_140131.xml 192.168.100.0/24
Starting Nmap 7.91 ( https://nmap.org ) at 2020-12-21 14:01 JST
Initiating Ping Scan at 14:01
Scanning 512 hosts [2 ports/host]
Completed Ping Scan at 14:01, 14.59s elapsed (512 total hosts)
Initiating Parallel DNS resolution of 9 hosts. at 14:01
Completed Parallel DNS resolution of 9 hosts. at 14:01, 11.03s elapsed
...

DBサーバーにログインできた場合、RCEにまで持ち込めるケースがあります。

hackerslab.aktsk.jp

また、Nmapが出力したXMLファイルをスプレッドシートにコピーしやすいようCSV形式に変換するnmap-xml2csv.rbは地味に便利です。

$ ruby nmap-xml2csv.rb ./results/20201204/192.168.1.0_24_20201204_072122.xml
[+] parse these xml files:
./results/20201204/192.168.1.0_24_20201204_072122.xml
-----------------------------------------------------
192.168.1.1 53(domain), 80(http)
192.168.1.17    22(ssh)
-----------------------------------------------------
[+] Output: portscan-result.csv

おわりに

aproxという最近作成したツールの紹介と、今までに作成したツールの紹介をしました。 アカツキのセキュリティチームでは脆弱性診断をただ単に行うだけではなく、効率よく行うためのツールの作成も積極的に行い、OSSとして公開することで業界に貢献していく姿勢をとっています。

一緒に脆弱性診断やツール開発を行いたい方は、ぜひ採用ページから応募してください。お待ちしています。

hrmos.co

Ruby on Rails 6.1 の水平シャーディング対応 & Octopusからの移行事例

この記事は Akatsuki Advent Calendar 2020 の20日目の記事です。

はじめに

ゲームサーバでは大量のユーザーデータなどを取り扱うため、データベースの負荷分散のために水平シャーディング(水平分割)が行われることがあります。 アカツキでも、これまで Ruby on Rails や Elixir 等でゲームサーバを開発する中で、それぞれの方法で水平分割を行ってきています。

さて、先日リリースされた Ruby on Rails 6.1 では、待望の水平シャーディング機能が標準でサポートされました。

早速使っていきたいところですが、これまで別の方法で水平シャーディングを実現していたアプリケーションを移行するにあたってはいくつか課題があるため、 それをどう解決するかの一例をご紹介したいと思います。

また、その解決の一環で利用した Ruby の BasicObject クラスを用いた Proxy パターンについても説明します。

Ruby on Rails 6.1 の水平シャーディング機能

Ruby on Rails 6.1 の水平シャーディング機能を利用するには以下のようにします。

まず、 database.yml に複数のDB接続を定義します。 *1

development:
  user01:
    <<: *default
    database: user01
  user02:
    <<: *default
    database: user02
  user01_readonly:
    <<: *default
    host: user01-ro....
    database: user01
    replica: true
  ...

レプリカへの接続には replica: true という設定をすることで、マイグレーションや db:create などDB操作の対象外となります。

次に、shard・role (シャード名とwrite/read等の役割)とDB接続のマッピングを、 ApplicationRecord 等モデルの親となるクラスにて connects_to メソッドで設定します。

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  connects_to shards: {
    user01: { writing: :user01, reading: :user01_readonly },
    user02: { writing: :user02, reading: :user02_readonly },
    ...
  }
end

class User < ApplicationRecord
end

あとは、レコードを取得するときに、 ActiveRecord::Base.connected_to メソッドを使って shard, role を指定します。

ActiveRecord::Base.connected_to(role: :reading, shard: :user01) do
  User.find(user_id)
end

これによって、指定したDB接続に対してクエリが発行されるようになります。

取得したレコードを書き換えて save するときなど、クエリが発行される際には必ずこのブロック内で行う必要があります。そうしないと別のシャードに対してクエリが発行されてしまいかねません。

ActiveRecord::Base.connected_to(role: :writing, shard: :user01) do
  user.save!
end

※ なお上記はあくまでメソッドの使用例であり、トランザクションを全く考慮していません。 多くの場合、API単位などで一貫して connected_to ブロック内で処理することを想定しているものと思われます。

Octopusからの移行

さて、あるアプリケーションでは、これまで Octopus という外部gemを使って水平シャーディングを実現していました。 Octopusでは、上記の User の例だと以下のようにアプリケーションを記述します。 *2

user = User.using(:user01).first
...
user.save!

Octopus では各レコードがどのシャードに属すかを記憶しており、 save! などの各種メソッドを呼び出した時には、自動的にそのシャードに対してクエリを発行してくれるようになっています。

リレーションも同様で、例えば user が has_many リレーション items を持っているとき、単に user.items と書けば自動的に user の属するシャードから items の検索を行ってくれます。

user.items.where.not(expired: true)... といったようにリレーションに対して where 等をメソッドチェインでつなげた場合も、最終的にクエリが発行されるところまでシャード指定が伝わります。 これをシャードトラッキングと呼んでいます。

これにより、間違ったシャードにクエリを発行してしまう心配をしなくてすみますし、特に異なるシャードから取得した複数のレコードをまとめて処理するような時には記述が簡単になるという利点があります。

しかし、 Octopus は Rails 6.1 に対応しておらず、シャードの記述や管理の方法も異なるために、このままでは Rails 6.1 に移行するためにはアプリケーションを大幅に書き直す必要が生じてしまいます。

そこで、Octopus ライクなシャードの指定方法はそのままで、水平シャーディングのDB接続ハンドリングには Rails 6.1 のネイティヴ機能を使用する Octoball という gem を開発しました。

github.com

これを使えば、アプリケーションをほとんど書き換えることなく Rails 6.1 に移行することが可能となっています。

ただし、DB接続の管理は Rails 6.1 に任せるため、接続の定義は Octopus 独自の設定ファイルから上記の database.yml に移す必要があります。 using() の引数に指定するのは connected_to に渡す shard に相当するシンボルになります。

Proxy パターンによるシャードトラッキングの実現方法

Octoball でシャードトラッキングを実現するにあたっては、いわゆる Proxy パターンを利用しています。 *3

user.items.where ... といった記述をした場合、各メソッドは ActiveRecord::Relation の様々な子クラス等を返しており、最終的にこの結果のレコード取得が必要になった段階でクエリが構築・発行されます。

各レコードや ActiveRecord::Relation は多様なメソッドを提供しているので、これらに逐一シャードトラッキング機能を入れていくのは現実的ではありません。

そこで、最初にこれらをラップする Octoball::RelationProxy というクラスのインスタンスを生成して返すようにします。

これに対して何かメソッドが呼ばれたときには、必要に応じてシャードを ActiveRecord::Base.connected_to で切り替えてから、ラップしているオブジェクトのメソッドを呼び出します。 そして、返り値が ActiveRecord::Relation などシャードトラッキングの対象であれば、再びそれをラップして呼び出し元に返します。

これが各メソッドチェインごとに繰り返されることで、最終的にクエリが発行されるところまでシャード指定が伝搬されてきいます。

Ruby には、まさにこうした Proxy パターンを実現するために、 BasicObject というクラスが用意されています。以下ではこれについて少し紹介します。

BasicObject による Proxy パターンの実装

BasicObject クラスは、通常の Object の親である Kernel のさらに親クラスにあたり、ほとんど何もメソッドを持っていないクラスです。 従って、これに対して何かしらのメソッドを呼び出そうとすると、基本的には method_missing が呼ばれることになります。 Proxy パターンでは、 method_missing で何かしらの処理をしてからラップしているオブジェクトのメソッドを呼び出すことで、そのオブジェクトの機能を拡張します。

例えば、ナイーブな Octoball::RelationProxy の実装だと、以下のように method_missing で元のオブジェクトのメソッド呼び出し前にシャードを切り替える動作を入れます:

class Octoball::RelationProxy < BasicObject
  def initialize(rel, shard)
    @rel = rel  # ラップしているオブジェクト
    @current_shard = shard
  end

  def respond_to?(method, include_all = false)
    @rel.respond_to?(method, include_all)
  end

  def method_missing(method, *args, &block)
    ret = nil

    # シャードを切り替えてから @rel に対してメソッドを呼び出す
    ::ActiveRecord::Base.connected_to(
        role: ::Octoball.current_role, shard: @current_shard) do
      ret = @rel.public_send(method, *args, &block)

      # connected_to のブロックから ActiveRecord::Relation を
      # 返すと自動的にその場でクエリを発行してしまうので、
      # nilを返してこれを抑制する
      nil  
    end

    # retがラップする必要のあるオブジェクトでなければそのまま返却
    return ret unless ret.is_a?(::ActiveRecord::Relation) || ret.is_a?...

    # retをRelationProxyでラップして返却
    ::Octoball::RelationProxy.new(ret, @current_shard)
  end

  # Ruby3でのキーワード引数デリゲーション対応
  ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
end

BasicObject を使わない場合、例えば ActiveRecord のコード中で

if x.is_a?(ActiveRecord::Relation)
  ...

などとクラスの判別をしている箇所に、 ActiveRecord::Relation をラップしている RelationProxy が渡った場合、 RelationProxy#is_a? が呼ばれることになり、ラップしたことで結果が変わってしまいます。 一方、 BasicObject なら is_a? メソッドすら持っていませんので、 method_missing 経由で @rel.is_a? に転送され、ラップされたオブジェクトのクラスに基づいた動作になってくれるというわけです。

ちなみに最後の、ruby2_keywords の件は、Ruby2.7以前でもRuby3以降でも **kwargs をつけずにキーワード引数のデリゲーションを可能にするために付加してあります。

なお、実際の Octoball では、メソッドに渡されたブロック内でのデフォルトシャードの振る舞いを Octopus に合わせたりするためにもっと複雑なことになっています。 また、 method_missing を毎回呼ぶのは遅そうなので、最初の呼び出し時に同名の Proxy メソッドを動的に定義してしまうという方法でいくらか高速化を狙っていたりもします。

まとめ

本記事では、 Ruby on Rails 6.1 で対応した水平シャーディング機能と、Octopus を使ったアプリケーションをそこに乗せるために開発した Octoball gem について紹介しました。 また、Octoball でシャードトラッキングを実現するのに用いた Proxy パターンと、その実装のための BasicObject についても説明しました。

Octoball 自体は割とニッチな gem だと思いますが、Proxy パターンなどのテクニックが何かの参考になれば幸いです。

*1:複数DB接続の定義自体はrails6から可能になっていた機能です。

*2: この using は ActiveRecord::Base に追加されるメソッドで、 Ruby の Refinement で使われる Module#using とは関係ありません。

*3: 元になった Octopus においても Proxy パターンが用いられています。