2015年3月10日火曜日

IngressのGlyph Hackを自動化する ~Input Subsystem~


お蔵入り寸前だったGlyph Hackの自動化ですが、Translaterメダルやら、ハッキングボーナスやスピードボーナスの上限解放やら、なんとかモチベーションを持ち直すニュースがあったので、再チャレンジします。

ということで、今回はInput Subsystemとやらを調べてみました。

まず、Input Subsystemで利用するイベントデバイスファイルは/dev/input/にあるとのことなので確認してみます。

自分の環境では、/dev/input/配下に以下が格納されていました。

event0
event1
event2
event3
event4
mice

それぞれが何らかのデバイスに対応しているそうですが、これだけでは分からないので、BusyBoxをインストールして、hexdumpコマンドで覗いてみます。

# /system/xbin/hexdump /dev/input/event0
0000000 1d21 0003 4cbf 0004 0003 0039 69d4 0000
0000010 1d21 0003 4cbf 0004 0003 0035 023c 0000
0000020 1d21 0003 4cbf 0004 0003 0036 028a 0000
0000030 1d21 0003 4cbf 0004 0003 003a 004a 0000
0000040 1d21 0003 4cbf 0004 0003 0030 0006 0000
0000050 1d21 0003 4cbf 0004 0000 0000 0000 0000
0000060 1d21 0003 8987 0004 0003 003a 0018 0000
0000070 1d21 0003 8987 0004 0003 0030 0002 0000
0000080 1d21 0003 8987 0004 0000 0000 0000 0000
0000090 1d21 0003 c0fa 0004 0003 0039 ffff ffff
00000a0 1d21 0003 c0fa 0004 0000 0000 0000 0000

なにやら出力されてきました。
しかし、何かやろうとすると、毎回バイナリを解読させられる運命にあるようですね。

ざっと画面をタッチしたりしながら眺めていると、はじめの2byteはイベントが発生した時間のようです。

次の2byteは入力のタイプEV_ABSを表す3かと思いましたが、なんだか違いそうです。

次の2byteもx軸かy軸かを表すもの、、、じゃなさそうです。
座標は後ろから2番目の2byteのようですね。
うーん。。分からない。

ということでもうちょっと良い方法がないか調べたところありました。
これはBusyBoxのインストールも不要です。

# getevent /dev/input/event0
0003 0039 00006be2
0003 0035 0000029d
0003 0036 000002aa
0003 003a 0000004d
0003 0030 00000008
0000 0000 00000000
0003 003a 00000037
0003 0030 00000006
0000 0000 00000000
0003 0039 ffffffff
0000 0000 00000000

type、code、valueってことなんだろうけど、なんか分かりそうで分からない。

さらに色々調べたところ、geteventコマンドに素敵なオプションを発見。

# getevent -lt /dev/input/event0
[  320377.729707] EV_ABS       ABS_MT_TRACKING_ID   00009411
[  320377.729707] EV_ABS       ABS_MT_POSITION_X    00000296
[  320377.729707] EV_ABS       ABS_MT_POSITION_Y    00000348
[  320377.729707] EV_ABS       ABS_MT_PRESSURE      00000048
[  320377.729707] EV_ABS       ABS_MT_TOUCH_MAJOR   00000007
[  320377.729707] EV_SYN       SYN_REPORT           00000000
[  320377.766317] EV_ABS       ABS_MT_TRACKING_ID   ffffffff
[  320377.766317] EV_SYN       SYN_REPORT           00000000

これなら読み解こうという気になります。

では、まずは登場人物を整理します。

type
EV_ABS 3EV_SYN 0

code
SYN_REPORT 0
ABS_MT_TRACKING_ID 57
ABS_MT_POSITION_X 53
ABS_MT_POSITION_Y 54
ABS_MT_PRESSURE 58
ABS_MT_TOUCH_MAJOR 48

value
ABS_MT_TRACKING_IDのIDや、ABS_MT_POSITION_Xのx座標など。

次に、タッチイベントを発生させるための流れを整理します。

まずEV_ABSのABS_MT_TRACKING_IDで一連のイベントのIDを設定します。
Android側の動きをgeteventで見ていると、1ずつインクリメントされ連番になっているようです。
ただ、適当な値でも特に文句は言われませんので、乱数とかでも大丈夫なのかも??

次に座標を送信します。
ABS_MT_POSITION_Xと、ABS_MT_POSITION_Yでイベントが発生した座標を設定します。
続いてABS_MT_PRESSUREで圧力、ABS_MT_TOUCH_MAJORで断面積ということで、いまいち良く分かりませんが設定します。
最後に、EV_SYNのSYN_REPORTを送信します。
ここまでが座標送信のワンセットです。

座標、ABS_MT_PRESSURE、ABS_MT_TOUCH_MAJOR、SYN_REPORTのセットを連続で複数送信するとスワイプイベントになります。

イベント終了時にはABS_MT_TRACKING_IDにFFFFFFFFを設定して送信し、EV_SYNのSYN_REPORTを送信して完了です。


ではやってみましょう。

# sendevent /dev/input/event0 3 57 3800
# sendevent /dev/input/event0 3 53 1222
# sendevent /dev/input/event0 3 54 1237
# sendevent /dev/input/event0 3 58 72
# sendevent /dev/input/event0 3 48 7
# sendevent /dev/input/event0 0 0 0
# sendevent /dev/input/event0 3 57 -1
# sendevent /dev/input/event0 0 0 0

ちゃんと動きました。

さらに複数座標を送信して、動きをつけて軌跡を描けるか試しました。

成功!ちなみに描いたのは「SUCCESS」です。
なぜこんなに滑らかな曲線になるのかは謎です。


しかし、rootとってshellからなら何でも出来てしまうってのは結構恐ろしい事だなぁ、と。
Google先生が練り上げたAndroidのセキュリティの壁も、いとも簡単に超えてしまうわけですね。

まぁ当然なんですが。

とりあえずroot系アプリ入れるときは、ちょっと注意したほうがいいなと思いましたとさ。

Read More...

2015年2月16日月曜日

IngressのGlyph Hackをハックする


IngressのGlyph Hackをハック中。

フレームバッファのバイナリをBitmapに変換して必要なとこだけキャプチャするのは完了。
こうして人の脳は退化していくわけですね。

ついでに自前の検出器を作成して、それがどの図柄なのかを検出するのも完了。
画面上の表示位置が変わらないし図柄がすごく単純なので、OpenCVなんて使う必要は全くありませんでした。

そしてあとは自動で絵を描くだけだ~と思ったら、後一歩のところで壁にぶつかりました。

そもそも、画面キャプチャ、検出器作成まで出来たら、あとはイージーだと思って何も調べてなかったのですが、結構厄介な問題がありました。

よそ様のアプリを操作するためにタッチイベントを生成するには、android.permission.INJECT_EVENTSが必要なのですが、こいつがいわゆるシステム権限らしくうまく動きません。

AndroidManifest.xmlで宣言しても、SecurityExceptionが発生します。


システム権限ということであれば、/system/app配下に放り込めば良いのかと考え、以下のようにしてADB Shellから無理やり放り込んでシステムアプリ化してみました。

$ adb shell
$ su
# mount -o rw,remount /system
# cp /sdcard/AutoGlyph.apk /system/app
# chmod 644 /system/app/AutoGlyph.apk
# reboot

が、やはり動きません。


INJECT_EVENTSはprotectionLevelがsignatureで、公式の説明にはthe application that declared the permissionと同じ証明書で署名されていないとダメだとあります。
googleの署名じゃないとダメってことでしょうか?

もしかするとprotectionLevelがsignatureOrSystemであれば、システムアプリ化するだけでいけたのかもしれませんが。。

これはお蔵入りの予感。。


ただ、LMT Launcherとかは普通に動いてるわけで、何かやり方はあるんでしょう。
セキュリティ的には出来ちゃダメな気もしますが。

システムアプリを偽装できるのか、Input Subsystemあたりを利用するのか。
LMT Launcherのpackagenameがcom.android.lmtなのが気になりますが、それだけではダメな気がします。

う~ん。。


Read More...

2015年2月15日日曜日

Androidアプリからスクリーンショット /system/bin/screencap


前回に続きスクリーンショットについてです。

/system/bin/screencap -p test.png

前回は上記のように-pオプションを付けて実行し、ファイルの拡張子も.pngにしていたので、pngファイルで保存されていました。

普通の用途ではそれでいいんだと思いますが、今回は訳あって1200ミリ秒ぐらいの中で全部やりたいので、時間的にギリギリです。

そこで、pngへの変換を省いて生のBitmap画像を得る方法について実験します。


/system/bin/screencapコマンドについては、pオプションやファイル名を指定せずに実行すると、いわゆるフレームバッファの生データが標準出力に出力されます。

データのはじめにはwidth、height、formatの各4byteで12byteのヘッダが付いてくるそうです。

とりあえずヘッダの中身を見てみます。

 2684682240
 655360
 16777216

うーん、まったく意味が分からない。。
実際のwidthやheightで割ってみても意味のありそうな数字になりません。

LITTLE_ENDIANで読み直してみます。

 1440
 2560
 1

これだ!

そしてその後には14,745,600byteのデータが飛んできました。

Nexus6で実験しているので、画面サイズは縦×横が2,560×1,440です。

2560 * 1440 = 3686400

14745600 / 3686400 = 4

1ピクセルあたり4byteということは、普通に考えれば32bitということでしょうか。
ただ、ネットで調べると16bitのダブルバッファリングだという説があったので、適当に16bitのヘッダを付加して表示してみます。

はい。ぜんぜんダメでした。

素直に32bitのヘッダを付加してみます。

上下が逆転しています。

Bitmapのデータはなぜか左下から右方向に向かって格納されているそうなのですが、フレームバッファには普通に並んでいるので、byte配列を逆さにしてからBitmapに食わせます。

色情報の順番も変わってしまいました。また、左右は入れ替えなくて良いようです。

行単位で逆にします。

まだちょっと色がおかしい。
どうやらRとBが逆になっているようですね。

このサンプルだと分かりにくいのでもう少しカラフルなサンプルでRとBを入れ替えると。

完成。


正体の良く分からないバイナリをいい感じに加工して読めるデータにする作業って、何か高度なパズルを解いているようで楽しいです。

まぁちゃんと調べれば正体は分かるんでしょうけど。。


Read More...

2015年2月8日日曜日

Androidアプリからスクリーンショット /system/bin/screencap -p


Androidアプリからスクリーンショットを取るには、JNIとかでフレームバッファを直接見に行くしかないのかと思っていたのですが、Android4.0から/system/bin/screencapでスクリーンショットが可能になったとのことです。

root化は必要なものの、ずいぶん楽になったようですね。

try {
    final String SAVE_DIR = "/test/";
    String fileName = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()) + ".png";
    String fullpath = Environment.getExternalStorageDirectory().getPath() + SAVE_DIR + fileName;
    Process p = Runtime.getRuntime().exec("su");
    OutputStream os = p.getOutputStream();
    os.write(("/system/bin/screencap -p " + fullpath).getBytes("ASCII"));
    os.flush();
    os.close();
    p.waitFor();
    p.destroy();
} catch (IOException e) {
    e.printStackTrace();
} catch (InterruptedException e) {
    e.printStackTrace();
}

これ終わったらちゃんとdestroy()で開放してあげないと、色々と後で死にそうですね。

Read More...

2014年11月29日土曜日

AndroidアプリのIn-app Billing version2からversion3への更新 ③テスト


In-app Billing version3の最終回です。
今回は作ったものがまともに動くかテストする方法を確認します。

テストには二通りの方法があり、ある程度の動きはサンドボックステストで確認し、最終的に実購入テストという流れかと思います。


  • サンドボックステスト
サンドボックステストのために、以下4つの予約済みアイテムIDが用意されています。

購入成功(android.test.purchased)
キャンセル、クレジットカード無効など(android.test.canceled)
払い戻し(android.test.refunded)
存在しないアイテム(android.test.item_unavailable)

android.test.canceledやandroid.test.refundedの場合は、RESPONSE_CODEにBILLING_RESPONSE_RESULT_USER_CANCELEDあたりが返ってくるのかと思ったのですが、BILLING_RESPONSE_RESULT_OKが返ってきます。

ただし、INAPP_PURCHASE_DATAやINAPP_DATA_SIGNATUREはnullです。

android.test.item_unavailableの場合は、onActivityResultに渡されてくるresultCodeでRESULT_CANCELEDが返ってきます。

こっちのActivityのRESULT_CANCELEDは0で、RESULT_OKが-1です。In-app Billingで返ってくるレスポンスコードはBILLING_RESPONSE_RESULT_OKが0なのでややこしいです。


ちなみにandroid.test.purchasedで一度購入成功すると、二度と購入出来なくなるのですが、「設定→アプリ→Google Play→データを削除」でデータを削除すると、再度購入出来るようになります。

  • 実購入テスト
サンドボックステストに飽きたら実購入テストに入ります。
実購入テストでは、Google Playと実際に通信して購入処理をテストします。


アルファ版配布用に、google グループで適当にグループを作成し、テスト用のgmailアドレスを招待しておきます。

テスト購入で課金が発生したりしないように、Developer Consoleにログインし、テストアカウントを設定します。
「設定→アカウントの詳細→ライセンス テスト→テスト用のアクセス権がある Gmail アカウント」にテスト用のgmailアドレスを入力します。

テストしたいapkをリリース用のKeyStoreで署名して、アルファ版テストにアップロードします。

「テスターのリストを管理→アプリのアルファ テスト版を提供するユーザー」という画面で先ほど作成したグループを登録すると、オプトインのためのアドレスが表示されます。

テスターが「テスターになる」をタップすると設定は完了で、あとは実際にGoogle Playから実機にインストールして試すだけです。

ただ、アルファ版が有効になるまで結構時間がかかる(半日とか1日とか)ようなので、作ってはアップロード作ってはアップロードという試し方は出来なそうな雰囲気です。

とは言え、やはり課金関連で失敗するとひどいことになるので、十分なテストの上で公開したいところです。

時間を置くと、実際にGoogle Playからダウンロード出来るようになります。

ダウンロードしたアルファ版のアプリで購入ボタンを押すと、しっかりとテスト注文である旨が表示されます。
(アプリ名やカード番号は消しました)

アルファテストで購入完了すると、Googleウォレット販売者コンソールでは以下のように「追加済み」と表示されます。

ということで、苦手意識のあったIn-app Billingですが、version3になって本当にすっきり分かりやすくなりました。

食わず嫌いだった人はこれを機に、是非再チャレンジしてみると良いと思います。


Read More...

AndroidアプリのIn-app Billing version2からversion3への更新 ②レスポンスを受け取る


前回はIn-app Billing version3のリクエスト送信方法について触りましたので、今回はそのレスポンス処理方法についてです。

  • レスポンスを受け取る
startIntentSenderForResultのレスポンスはActivityのonActivityResultで受け取ります。

startIntentSenderForResultの第2引数で渡したリクエストコードとの一致を確認した上で、resultCodeとresponseCodeが問題ないか確認し、レスポンスの中身を取り出します。

purchaseDataはJSON形式になっていて、その中のproductIdを確認すると、どの商品が購入されたかが分かります。

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_CODE) {
        int responseCode = data.getIntExtra("RESPONSE_CODE", -1);
        if (resultCode == RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
            String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
            if(purchaseData != null) {
                String productId = "";
                try {
                    JSONObject jsonObj = new JSONObject(purchaseData);
                    productId = jsonObj.optString("productId");
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                //必要な処理
            }
        }
    }
}

ちなみに、getIntExtraのdefaultValueを0にしているサンプルを良く見かけるのですが、0だとBILLING_RESPONSE_RESULT_OKになってしまって微妙な気がしたので-1にしてみました。
なんか理由があるのかもしれませんが。

  • 署名の確認
テストアカウントであればサンドボックステストでも署名が返ってくるはずですが、うまくいかなかったので実購入テストで試しました。

登場人物はonActivityResultに渡されてきたIntentに入っている、INAPP_PURCHASE_DATAとINAPP_DATA_SIGNATUREと、公開鍵であるBASE64_ENCODED_PUBLIC_KEYです。

BASE64_ENCODED_PUBLIC_KEYは、Developers Consoleで確認出来る公開鍵です。

INAPP_PURCHASE_DATAはレスポンスのjson文字列で、INAPP_DATA_SIGNATUREはそれを公開鍵で署名したものです。

署名検証にはjava.security.Signatureを利用し、流れとしてはSignatureをgetInstanceメソッドで生成し、initVerifyメソッドで公開鍵で初期化。updateメソッドでレスポンスのjson文字列をバイト配列に変換したものを渡しverifyメソッドで検証します。

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_CODE) {
        int responseCode = data.getIntExtra("RESPONSE_CODE", -1);
        if (resultCode == RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
            String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
            String signatureData = data.getStringExtra("INAPP_DATA_SIGNATURE");
            if (purchaseData != null && signatureData != null) {
                if(verify(generatePublicKey(BASE64_ENCODED_PUBLIC_KEY),purchaseData, signatureData)) {
                    //署名OK
                }
            }
        }
    }
}

private final String SIGNATURE_ALGORITHM = "SHA1withRSA";
private final String KEY_FACTORY_ALGORITHM = "RSA";

public boolean verify(PublicKey publicKey, String signedData, String signature) {
    Signature sig;
    try {
        sig = Signature.getInstance(SIGNATURE_ALGORITHM);
        sig.initVerify(publicKey);
        sig.update(signedData.getBytes());
        if (!sig.verify(Base64.decode(signature, Base64.DEFAULT))) {
            return false;
        }
        return true;
    } catch (InvalidKeyException e) {
    } catch (NoSuchAlgorithmException e) {
    } catch (SignatureException e) {
    }
    return false;
}

public PublicKey generatePublicKey(String encodedPublicKey) {
    PublicKey pKey = null;
    try {
        byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
        pKey = keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
    } catch (NoSuchAlgorithmException e) {
    } catch (InvalidKeySpecException e) {
    }
    return pKey;
}

署名のところはもう少し色々と考えなくてはいけないこともありますが、とりあえずは検証出来ました。
次回はここまで作成したもののテスト方法についてです。

Read More...

AndroidアプリのIn-app Billing version2からversion3への更新 ①リクエストを送る



GoogleがIn-app Billing Version 2 APIを殺すそうなので、もう長らく更新していなかったAndroidアプリを更新する羽目になっています。

昔version2の頃にアプリ内課金を実装したときは、正直意味が分からないけどサンプル通りに書いたら動いたというレベルだったので、結構苦手意識があります。

ただ、すごくシンプルになったという話も聞くので、これを機にまとめてみようと思います。


  • AIDLファイルの追加
サンプルアプリTrivialDriveに含まれている、Android Definititon Language(AIDL)ファイルを利用し、インターフェースファイルIIAppBillingService.javaを作ります。

Android SDK ManagerでGoogle Play Billing Libraryをインストールします 。


<android-sdk>/extras/google/play_billing/samples/TrivialDrive/からサンプルプロジェクトをインポートします。

TrivialDrivaサンプルの/srcディレクトリからIInAppBillingService.aidlファイルをコピーし、自分のアプリの/srcディレクトリへ貼り付けます。

/genディレクトリ内にIInAppBillingService.javaが自動的に作成されます。

  • AndroidManifestの更新
これはversion2と変わっていないようです。

AndroidManifest.xmlに以下のパーミッションを追加します。

<uses-permission android:name="com.android.vending.BILLING" />

  • ServiceConnectionの作成とIInAppBillingServiceへのバインド
接続時と切断時のコールバックメソッドを持つServiceConnectionを実装し、bindServiceメソッドでバインドします。

ServiceConnectionを実装します。

private IInAppBillingService mIInAppBillingService;
private ServiceConnection mServiceConnection= new ServiceConnection() {
    @Override
    public void onServiceDisconnected(ComponentName name) {
        mIInAppBillingService = null;
    }

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mIInAppBillingService = IInAppBillingService.Stub.asInterface(service);
    }
};

IInAppBillingServiceを使用できるようにするため、ActivityのonCreateメソッドでbindServiceメソッドを呼びます。
Intentの引数は、クラス名のIInAppBillingServiceではなく、InAppBillingServiceなのが紛らわしいですね。

Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
bindService(serviceIntent, mServiceConnection, Context.BIND_AUTO_CREATE);

終了処理として、Activityが終了時にonDestroyメソッドでunbindServiceメソッドでアンバインドします。

if (mServiceConnection != null) {
    unbindService(mServiceConnection);
}


なお、このあたりで大きくハマったのですが、bindServiceを呼んでからServiceConnectionのonServiceConnectedが呼ばれるまでにはタイムラグがあり、直後にIInAppBillingServiceをいじくろうとするとぬるぽで死にます。

bindService自体はtrueで返ってくるのに、IInAppBillingServiceはnullのまま。

時間を計ってみたところ、50~150ミリ秒後ぐらいでまちまちでしたが、何にしろタイムラグがあります。

直後にスレッド止めたりして待ち合わせをしても、ほぼ同時に返ってくるものの結局ぬるぽ。

まぁ何のためにonServiceConnectedメソッドを実装する必要があるのか考えれば、分かりそうなものでしたが。。

ということで、わざわざコールバックメソッドを実装させられている訳なので、onServiceConnectedからトリガーしてあげればぬるぽにはならない訳です。

  • サポート状況の確認
version3のAPIをサポートしているか確認します。

int response = mIInAppBillingService.isBillingSupported(3, getPackageName(), "inapp");
if(response == BILLING_RESPONSE_RESULT_OK) {
    //OKの場合の処理
}

  • 購入状況の確認
getPurchasesメソッドを呼ぶと、購入済みアイテムの情報を取得出来ます。

RESPONSE_CODEや、INAPP_PURCHASE_ITEM_LISTなど、いくつかの情報を含むBundleが返されるので、必要に応じて取り出し利用します。

Bundle bundle = mIInAppBillingService.getPurchases(3, getPackageName(), "inapp", null);
if(bundle.getInt("RESPONSE_CODE") == BILLING_RESPONSE_RESULT_OK) {
    ArrayList<String> purchases = bundle.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
    for (int i = 0, n = purchases.size(); i < n; i++) {
        //purchases.get(i)の中身によって処理を変える。
    }
}

  • 購入処理
getBuyIntentメソッドを呼んで、返されたBundleからBUY_INTENTキーのPendingIntentを取得します。
取得したPendingIntenでstartIntentSenderForResultメソッドを呼び出します。

getBuyIntentメソッドの第4引数はDeveloper Payloadというやつで、空文字でもnullでもレスポンス時には空文字が返ってきます。
本来は任意の文字列を入れて、海賊版対策に使います。

startIntentSenderForResultメソッドの第2引数はコールバックを受けたときにリクエストを識別するための任意のint値です。

Bundle buyIntentBundle = mIInAppBillingService.getBuyIntent(3,
        getPackageName(), itemId, "inapp", "");
if (buyIntentBundle.getInt("RESPONSE_CODE") == BILLING_RESPONSE_RESULT_OK) {
    PendingIntent pendingIntent = buyIntentBundle
        .getParcelable("BUY_INTENT");
    startIntentSenderForResult(pendingIntent.getIntentSender(),
        REQUEST_CODE, new Intent(), Integer.valueOf(0),
        Integer.valueOf(0), Integer.valueOf(0));
}

なお、サポート状況の確認から購入処理まで、IInAppBillingServiceのメソッドを呼ぶ際には、RemoteExceptionが発生するので、適時catchします。

あとはActivityのonActivityResultへレスポンスが来るので、適時処理するだけです。


次回はstartIntentSenderForResultのレスポンス部分、onActivityResultでの処理についてです。


Read More...