CORDEA blog

Android applications engineer

第 1 水準以外の漢字を検出する

JIS 漢字コードの第 1 水準漢字以外を検出します。
Perl が一番楽そうだったので Perl 使いました。

何がしたいか

$ echo "perl" | hoge
false
$ echo "あいうえお。" | hoge
false
$ echo "漢字 | hoge
false
$ echo "弌腕。" | hoge # 第 2 水準漢字が入っている
true

第 1 水準漢字

第 1 水準漢字は亜から腕までの範囲を指します。
ただ、亜-腕のような正規表現でチェックするだけでは不十分です。

y0m0r.hateblo.jp

この記事に詳細は書いてあり、非常に参考になりました。
ということで範囲を調べる必要があります。

コード表を参照して、Shift JIS における文字コードの範囲を調べます。
亜だったら 889F, 腕は 9872 です。

一通り調べると、

  • 889F - 88FC
  • 8940 - 97FC
  • 9840 - 9872

の範囲であることが分かります。
これを正規表現に起こすとこんな感じ

\x88[\x9F-\xFC]|[\x89-\x97][\x40-\xFC]|\x98[\x40-\x72]

第 1 水準漢字以外の漢字

第 1 水準漢字の検出はできましたが、実際に欲しいものは違います。

漢字の検出は \p{Han} で可能です。
なので、\p{Han} で取得した漢字を先程の正規表現に当てて、引っかからないものが第 1 水準漢字以外としました。

ただ、\p{Han} だと句読点なども含まれます。\p{Script=Han} にすると句読点は除くことができますが、々などは入ってきます。
なので、先程の正規表現で記号も見ることにします。コード表によると記号は

  • 8140-81AC

あたりです (実際はもうちょっとあります、サボりました)

これを付け加えて

\x81[\x40-\xAC]|\x88[\x9F-\xFC]|[\x89-\x97][\x40-\xFC]|\x98[\x40-\x72]

これでふんわり記号 + 第 1 水準漢字です。ちゃんと見るなら 1 区から 8 区までの文字コードを追加すれば良いと思います。
この正規表現で引っかからない場合は第 2 水準漢字とか、そのへんです。

あと、第 1 水準漢字のところでは Shift JIS になっている必要があるので

encode("sjis", $hans);

こういう処理が必要です。

Repository はここ

github.com

おわり。

動的解析ツール Frida を Android に使う

Frida を使ってみたメモ

Frida

frida.re

自分の Script を Inject したり、値を取得したり、色々なことができる Toolkit です。
リバースエンジニアリングとかする時に使うみたいですね。
iOS / Android にも対応しており、今回は Android の話です。
ちなみに日本語の記事もいくつかあります。

もし試す場合は自分のアプリや許可されているものを使用して試してください。
ここからの手順やコマンド、コードによって何が起きても私は責任を取りません。

入れてみる

Rooted device のほうが簡単らしいのですが、持ってないので Without root で試します。
肝心の手順はここに全部書いており、写すことはしないのでこちらを参照してください。

koz.io

簡単に手順を書くと

1. apk を device から抜く (pm path とかしてから pull する)
2. apktool を用いて apk を decode する
3. 諸々いじった後、libfrida-gadget.so を lib/ に入れる
4. apktool で apk を build
5. jarsigner で署名 / 検証
6. zipalign で最適化

という感じです。

3 での諸々いじる工程もすべて上記の記事に書いてあります。

補足
  • AndroidManifest.xmlandroid:extractNativeLibs が false になっている場合は true にしないと Install 時に失敗します。
  • 最新の Frida は動かないという Issue が上がっており、実際に私も動かなかったので frida-gadget-12.7.26-android-*.so を使用しています。
  • frida-gadget を load するタイミングは onCreate のはじめとかでいいと思います

使ってみる

試したコードとアプリはここにあります。以降はこのデモ app を使用しています。

github.com

準備

Frida の install が終わっていない場合は

$ pip install frida-tools

さて、Frida は Python で書くのですが、Inject する Sctipt は JavaScript です。
ということでまずはベースとなる Python の Script が必要になります。

こちらをベースにして、

import sys
import frida


def on_message(message, data):
    print(message)


js = """
Java.perform(function() {
});
"""

process = frida.get_usb_device().attach('Gadget')

script = process.create_script(js)
script.on('message', on_message)
script.load()

sys.stdin.read()

こんな感じです。今回は Gadget を使っているので attach('Gadget') となります。
アプリが待ち受けている状態等で、

$ python example.py

を実行すると何も起きないはずです。
エラーが出る場合はおそらくどこか間違えています。

Detector の返す値を変更する

Repository を見てもらうと分かりますが jp.cordea.fridademo.Detector という Class があり、Detector#detect が false を返しています。
Detector#detect が true だと Button を click した際に toast が表示されます。ということで toast が表示されるようにします。

var detector = Java.use('jp.cordea.fridademo.Detector');
detector.detect.overload().implementation = function() {
    return true;
}

Button を click すると toast が表示されるはずです。

fab の click を上書きする

続いて fab の click を上書きします。まず fab を取得する必要があります。

var fabId = activity.findViewById(0x7f080069);
var fab = Java.cast(
    fabId.$handle,
    Java.use('com.google.android.material.floatingactionbutton.FloatingActionButton')
);

findViewById で fab を取得し、FloatingActionButton に cast しています。
なお、この ID はデモ app の場合は smali/jp/cordea/fridademo/R\$id.smali を検索することで取得できます。

Listener も必要です。

var listener = Java.use('android.view.View$OnClickListener');

そして Listener をセットします。

fab.setOnClickListener(Java.registerClass({
    name: 'jp.cordea.fridademo.OnClickListener',
    implements: [listener],
    methods: {
        onClick: function(v) {
        }
    }
}).$new());
TextView の count を上書きする

さて、先程 fab の click を上書きしたので今まで click 毎に +1 されていた TextView の count が動かなくなりました。
これを *2 するようにしてみましょう。

var textViewId = activity.findViewById(0x7f0800de);
var textView = Java.cast(
    textViewId.$handle,
    Java.use('android.widget.TextView')
);

そしてさきほどの onClick の中に *2 する実装を入れます

var count = 1;
fab.setOnClickListener(Java.registerClass({
    name: 'jp.cordea.fridademo.OnClickListener',
    implements: [listener],
    methods: {
        onClick: function(v) {
            count *= 2;
        }
    }
}).$new());

そして上書きしますが、java.lang.String としてセットする必要があります。
なので、以下のようにセットします。

var count = 1;
fab.setOnClickListener(Java.registerClass({
    name: 'jp.cordea.fridademo.OnClickListener',
    implements: [listener],
    methods: {
        onClick: function(v) {
            count *= 2;
            var string = Java.use('java.lang.String');
            textView.setText(string.$new(count.toString()));
        }
    }
}).$new());

これで fab を click すると +1 ではなく *2 で値が増えていく様子が見られると思います。
詳しくは Repository の example.py を見てください。

感想

使うためにはそれなりに知識が必要そうです。
Android の解析をするなら Android の知識、Frida の知識、Smali も軽く読める必要があります。
今回は難読化してないですが、難読化されている場合はそのあたりの知識も必要かもしれないですね。根気があればなんとかなりそうな気もしますが...

あと、ネイティブアプリエンジニアの観点から、これらにどう対処するかという話に興味がある場合
anti-frida や anti-reverse engineering で検索すると色々でてきますので見ると良いと思います。
中でも良かったものを 2 つ置いておきます

Podcast を Google Podcasts に登録する

outer-heaven.fm を先日 Google Podcasts に登録したので、その時のメモです。

podcasts.google.com

play.google.com

公式に手順がまとめられており、これを少し噛み砕いた内容です。

developers.google.com

1. Podcast の Feed URL を取得する

まずは Podcast の Feed URL が必要です。outer-heaven.fm では Hugo の Zen theme を使っており、こちらは Podcast に対応しているので RSS Feed を生成してくれます。

https://outerheavenproject.github.io/podcast/index.xml

2. Feed URL が Google に登録されるのを待つ

Google に登録されていない状態で Google Podcasts への登録を行おうとしても以下のようなエラーが表示されます。

f:id:CORDEA:20191231095432p:plain

outer-heaven.fm では URL を Search Console に登録し、
Feed URL をサイトマップに登録しておいて URL Inspection Tool で登録状況を確認していました。

これが正しいのかは分かりません...

3. Direct Link を生成する

Podcast Publisher Tools にアクセスし、フォームに Feed URL を入力して GENERATE を押します。

search.google.com

生成された URL を SNS にシェアしたりしましょう!
これで Google Podcasts app on Android の検索にも引っかかるようになります。

JetBrains MPS をはじめよう

この記事では、JetBrains が開発した言語ワークベンチである JetBrains MPS の使い方を紹介します。

Kyash Advent Calendar 2019 13日目の記事です。

JetBrains MPS について

JetBrains MPS (Meta Programming System) は公式にも書かれている通り、DSL を構築できるツールです。
Open Source であり、Apache-2.0 でライセンスされています。

www.jetbrains.com

日本語の紹介記事としては InfoQ が良さそうです。

www.infoq.com

今は Java code の生成に優れており、実際私も Java 以外で試したことがありませんが、C なども生成できるようです。

使い方

ここからは実際に MPS を使って手を動かしながら使い方を確認していこうと思います。
全部 Blog にまとめようかと思ったのですが、とっても長くなりそうだったので Codelab にしました。
所要時間は約 1 時間です。

mps-codelab.netlify.com

MPS に関する日本語の記事をあまり見かけたことがないので比較はできませんが、それなりに詳しく書いたつもりです。
長いですが気が向いたらやってみてください。
変えたほうがよいところを見つけたら、左下の Report a mistake から Issue を立てていただけると嬉しいです。Twitter とかでも大丈夫です。

ハマったときに色々調べつつ時間をかけて自己解決することが多いので、MPS User が 1 人でも増えて知見が集まるといいなぁと思っています。

claat で生成した codelab を netlify で CD する

先日私達が公開した Android Dagger codelab は netlify を使用して公開しており、今回はその方法を紹介します。

github.com

claat は google が公開している codelabs を生成するための command line tool です。

github.com

行っていることは非常に簡単で、deploy script を netlify で走らせているだけです。
はじめに検討したのは github.io で、実際にはじめの方は github.io で公開されていましたが、
その時は GitHub Actions も有効化されておらず、自動化を実現できなかったため netlify に移行しました。

dagger-codelabs には scripts 配下にいくつかの shell script が配置されています。

dagger-codelabs/scripts at master · outerheavenproject/dagger-codelabs · GitHub

その中でも deploy.sh を netlify の build command としており、これで deploy しています。
それぞれの shell script がやっていることを簡単に説明します。

deploy.sh

dagger-codelabs/deploy.sh at master · outerheavenproject/dagger-codelabs · GitHub

download_claat, codelab_gen を叩いているだけです。

download_claat.sh

dagger-codelabs/download_claat.sh at master · outerheavenproject/dagger-codelabs · GitHub

readonly url=$(curl https://api.github.com/repos/googlecodelabs/tools/releases/latest | \
    jq -r '.assets[] | select(.name == "claat-linux-amd64") | .url')
curl -LJ -o /tmp/claat -H 'Accept: application/octet-stream' $url
chmod u+x /tmp/claat

claat command を使用できるよう、googlecodelabs/tools から download しています。
まず latest release を見て、返ってくる json から download 用の url を取得しています。
実際に json を見てみると分かりやすいでしょう。jq は新規に install などしなくても使用できます。
次に claat command を download します。このあたりに書いてあります。

あとは実行権限を付与します。

codelab_gen.sh

dagger-codelabs/codelab_gen.sh at master · outerheavenproject/dagger-codelabs · GitHub

readonly base_dir="/tmp/dagger_codelabs"
readonly file="$base_dir/$(date +%s).md"

mkdir $base_dir

cp md/chapters/*.png "$base_dir/."

cat md/INDEX.md > $file
cat md/chapters/*.md >> $file
/tmp/claat export -o docs/ $file

rm -r $base_dir

ここでは章毎に分割された markdown をくっつけています。これは章によって担当が決まっているため、少なくとも章単位で別々の人が編集できる必要があったからです。
ついでに png もあるので、参照可能な位置に copy したあと、各章を /tmp/ にある deploy 用の md に追記していきます。
最後に claat command で markdown から html に変換します。

欠点として、最後に改行を入れないとうまく出力できないという問題があります (cat で結合しているだけなため)。
今回はメンバー間の認識を合わせるのも簡単だったため、このあたりの雑さを許容していましたが、
もっと広く Contributor が集まるような場合にはもう少しちゃんと作ったほうがいいだろうと考えています。

もっと簡単に実現できる方法もあると思います。何かあればぜひコメント、Issue、PR 等ください。

【Rust】imageproc で任意の位置に text を描画する際の注意点

imageproc に限らず、rusttype で font の layout を使用する際には概ね同じ問題に当たりそうなんですが、
そこまで調べてないので imageproc に限った話として紹介します。

今回はテキストの描画位置をちょうど真ん中にすることを考えてみます。
なお、ここで紹介している描画部分のコードは GitHub にあります。

github.com


まず、imageproc の draw_text_mut で任意の文字を描画してみます。

draw_text_mut(&mut image, Rgba([0u8, 0u8, 0u8, 255u8]), 0, 0, scale, &font, "rustpythonclojureC#");


f:id:CORDEA:20190616145632p:plain

中央にする場合、テキストの正確な width と height が必要になります。
これは font.layout で取得できるので、取得して各文字の min.x, min.y, max.x, max.y をもとに周りを囲ってみます。

f:id:CORDEA:20190616151012p:plain

見れば分かる通り、例えば max.x - min.x で文字ごとの width は取れるものの、
文字間の padding (kerning 処理?) は考慮されないため、ここを考慮して width を計算する必要があります。
また、height に関しては一文字目の max.y - min.y だけを見た場合、'r' の高さしか考慮されないため、
min.y の一番小さい文字 ('h' など) - max.y の一番大きい文字 ('p' など) を取得して高さを計算する必要があります。
高さに関してはどこを baseline と置くかで計算方法も変わってくるでしょう。

このあたりが 1 つ目の注意したほうが良さそうな点です。

以上を踏まえて、文字列全体の width と height を計算し、周りを囲ってみます。

f:id:CORDEA:20190616152046p:plain

これで正確な width, height が取得できました。
ただ、ここから中央揃えにするために (image.width / 2) - (width / 2) などとして x, y を計算して描画すると微妙にずれることに気がつくはずです。
というかそもそも画像でも margin が入っていますよね...

これが 2 つめの注意点で、描画する際に x, y に margin が入るため、描画する際にここを考慮する必要があります。

実際にこの margin がどの程度かですが
x に関しては一番はじめの文字の min.x を見れば良いです。
y に関しては、min.y が一番小さい文字の min.y を見れば良さそうです (ex. 'h')。

以上を踏まえて、x, y を margin 分左と上にずらすことで、ちょうど真ん中に描画することが出来ます。

f:id:CORDEA:20190616152910p:plain

【RoN BHS】set_object_health の挙動

Rise of Nations という Microsoft から発売された名作 RTS ゲームがあります。

このゲームには Big Huge Script という、ゲーム内でシナリオやルールを作るための言語があり、
今回はこの Script で使える関数の中から、 set_object_health という関数の挙動について説明します。

set_object_health とは

ゲーム内の Script Editor では次のように表示されます。

f:id:CORDEA:20190518122404j:plain

指定した ID の object の HP を 0-100 % の範囲で指定することができます。

tl;dr

  • 3 人 1 unit の場合、0-68% までは HP - math.floor(HP / 3)

挙動

この set_object_health は歩兵などの 3 人 1 unit の場合に不思議な挙動をします。
なお、ここでは Slingers を使用して確認しています。

どういうことかというと、単純に % 指定してそのとおりに動くとは限らないということです。

まず、ターゲットの HP を 0 に設定する場合を考えてみます。

set_object_health(who, id, 0);

これを騎兵に適用すると正常に行われます。
これは想定通りの挙動と言えます。

では、歩兵に適用してみます。

f:id:CORDEA:20190518122433j:plain

これは 3 人で 1 unit という扱いであるものの set_object_health は 1 人に対して適用される場合がある、ということの気がしています。
様々な数値で試してみると判るのですが、0-68% までは HP は 57 です。必ずしも指定する値と HP は一致しません。
なぜ 57 なのかは、1 人減っていることから

85 - (85 / 3) = 57

と考えられます。(より正確には 85.0 - math.floor(85.0 / 3.0))

さて、先程 1 人に対して適用される場合がありそうと書きましたが、では 80% など 69% 以上を設定する場合を見てみます。

set_object_health(who, id, 80);

この結果は以下のようになります。

すなわち、

85 * 0.8 = 68

で、1 人ではなく、1 unit に対して適用されていそうなことがわかります。
以上のように、歩兵などの unit に適用する場合は指定 % に対して HP が比例しないため、注意が必要です。

確認に使用した Script は以下にあります。

RoN_rules/Example at master · CORDEA/RoN_rules · GitHub