こんにちは、セキュリティエンジニアの小竹 泰一(aka tkmru)です。 アカツキでは、Webアプリケーション、ゲームアプリに対する脆弱性診断や社内ネットワークに対するペネトレーションテスト、ツール開発/検証などを担当しています。
メモリ改ざんによるチートとは
UI上に表示されている値を端末のメモリ上から検索し、見つけた値を改ざんすることでチートを行うことができる場合があります。 これはゲームのチート方法の中で最も簡単な方法で、脆弱性診断の際にも実際にメモリ上のデータを改ざんをすることでチートできるかどうか確認しています。 対策としては、XOR等を使ってメモリ上ではエンコードされた状態で値を保持し、UI上に表示されている値を検索されても見つからないようにする方法があります。
作ったツール
apk-medit
という脆弱性診断のためのAndroidアプリ向けメモリ改ざんツールを作成しました。
CUIで動作し、非ルート化端末でも動作するのが特徴です。また、Goで実装しているため、Android NDKに依存していません。
使い方
非ルート化端末でメモリを読むためには、run-as
コマンドを使い、実行しているアプリの権限でシェルを動作させる必要があります。
run-as
コマンドはdebuggableなアプリにしか使えないため、対象のアプリがdebuggableでなかった場合には、
AndroidManifest.xml
を開きapplication nodeに対して、以下のattributeを追加する必要があります。
android:debuggable="true"
対象のアプリの準備ができたら、adbコマンドで/data/local/tmp/
以下に実行バイナリをpushしてください。
$ adb push medit /data/local/tmp/medit medit: 1 file pushed. 29.0 MB/s (3135769 bytes in 0.103s)
run-as
コマンドの引数にターゲットのアプリのパッケージ名を指定し実行したあとは、自動的にディレクトリが変更されます。
apk-meditの実行バイナリを/data/local/tmp/
からコピーして、実行してください。
$ adb shell $ pm list packages # パッケージ名を確認 $ run-as <target-package-name> $ cp /data/local/tmp/medit ./medit $ ./medit
apk-medit
を実行すると、インタラクティブなプロンプトが立ちがります。
プロンプト上に実装されたfindコマンドやpatchコマンドを用いて以下のようにメモリを改ざんすることができます。
フィルタ機能
小さく、キリがいい値はメモリ上にいくつもあり、1回の検索で目的のアドレスを見つけることは困難です。
そこで前回の検索で見つけたアドレスの中から、指定した値に変化しているアドレスを見つけるfilter
コマンドを実装しました。
> find 100 Search UTF-8 String... Target Value: 100([49 48 48]) Found: 712! ------------------------ Search Word... Target Value: 100([100 0]) Found: 6605! > filter 94 Check previous results of searching UTF-8 string... Target Value: 94([57 52]) Found: 0!!! ------------------------ Check previous results of searching word... Target Value: 94([94 0]) Found: 1!!! Address: 0xe7021f70 > patch 5 Successfully patched!
filter
コマンドを繰り返し使って見つかるアドレスを絞っていくことで、目的の値に到達することができます。
実装
ここでは、Goを使って開発することの利点は何なのか、 どのようにしてメモリ上のデータを読み込んでいるのかについて解説します。
開発にGoを使うメリット
今回の開発にあたり、Goを使って感じたメリットは以下の4つです。
- ARM向けにバイナリを用意するのが簡単
- システムコールの呼び出しが簡単
- 大きいバイト列から目的のバイト列を高速に検索するのが簡単
- GitHub ActionsとGoReleaserのおかげでバイナリを配布するのが簡単
ARM環境向けにLinuxバイナリを用意するのが簡単
Goコンパイラはクロスコンパイルをサポートしており、環境変数GOARCH
、GOARM
にアーキテクチャ名とバージョンを、
環境変数GOOS
にOS名を指定するだけでARM環境で動くLinuxバイナリを作成できます。
$ GOOS=linux GOARCH=arm64 GOARM=7 go build -o medit
システムコールの呼び出しが簡単
Golangにはシステムコールをいいかんじにラップしてくれているunixパッケージがあり、 システムコールを簡単に呼び出せます。 ptraceシステムコールを用いてプロセスへのアタッチするコードをC言語の場合と見比べてみましょう。
C言語でptraceシステムコールを用いたプロセスへアタッチは、<sys/ptrace.h>
をインクルードすることで以下のコードでできます。
ptrace()の第3引数はcaddr_t addr
, 第4引数はint data
となっており、それぞれに読み出しや書き込みの対象とするメモリのアドレス、
書き込むデータを指定できるのですが、第1引数にPTRACE_ATTACH
を設定したときは第3引数、第4引数がともに無視されます。
そのため、プロセスへアタッチする際にはそれぞれにNULLを指定する必要があります。
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
Goではunixパッケージを使うと、以下のようなコードでptraceシステムコールを用いてプロセスへのアタッチができます。 Goでは不要な引数を指定しなくていいように、利便性を考えてシステムコールをラップしており、アタッチに使う関数の引数はひとつだけです。
sys.PtraceAttach(pid)
このようにGoではC言語を使うより簡単にシステムコールを扱うことができます。 Goにおけるシステムコールの扱いについては、少し古いsyscallパッケージを使ったものになりますが以下の記事が詳しいです。
apk-medit
では動きが激しいアプリの動作を止めるために、ptraceシステムコールによるアタッチをattachコマンドを使ってできるようにしていますが、
メモリの読み込み、メモリへの書き込みにはptraceシステムコールを使っていません。
どのようにして、メモリ上のデータを検索しているのかは後ほど、解説します。
大きいバイト列から目的のバイト列を高速に検索するのが簡単
Rabin-Karp string search algorithmという高速に文字列を検索するアルゴリズムが bytes.Index()の内部で使われています。 これによって、複雑なアルゴリズムを自前で実装することなく、bytes.Index()を使うだけで高速にメモリ上のデータを検索することができました。
GitHub ActionsとGoReleaserのおかげでバイナリを配布するのが簡単
これはGoの仕様によるものではなく、Goを使った開発を便利にしてくれるツールのおかげですが、
GoReleaserというGithub Releasesにバイナリを登録してくれるツールを、
GitHub Actions上で動かすことで、リリースバイナリを簡単に配布することができました。
以下のようなymlファイルを.github/workflows/
以下に配置するだけで、GitHubにtag付きのコミットがアップロードされた際に、
GitHub Actions上でビルドが走り、GoReleaserがGithub Releasesにバイナリを登録してくれます。
name: release on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v1 with: fetch-depth: 1 - name: Setup Go uses: actions/setup-go@v1 with: go-version: 1.14 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v1 with: version: latest args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
どうやってメモリを読み込んでいるか
Linux系OSにおいて/proc/
以下には、プロセスの情報にアクセスするための疑似ファイルが置かれています。
/proc/$pid/maps
、/proc/$pid/mem
を用いて、特定のプロセスのメモリマッピングを取得し、メモリを読み出す事が可能です。
/proc/$pid/maps
には以下のようなフォーマットでメモリマッピング情報が載っています。
ここには、pidで指定したプロセスがメモリのどの部分に書き込み権限、読み込み権限を持っているのかという情報が記載されています。
sargo:/data/data/jp.aktsk.tap1000000 $ cat /proc/11283/maps 12c00000-12d40000 rw-p 00000000 00:05 23292 /dev/ashmem/dalvik-main space (region space) (deleted) 12d40000-133c0000 ---p 00140000 00:05 23292 /dev/ashmem/dalvik-main space (region space) (deleted) 133c0000-13700000 ---p 007c0000 00:05 23292 /dev/ashmem/dalvik-main space (region space) (deleted) 13700000-13780000 rw-p 00b00000 00:05 23292 /dev/ashmem/dalvik-main space (region space) (deleted) 13780000-14140000 ---p 00b80000 00:05 23292 /dev/ashmem/dalvik-main space (region space) (deleted) 14140000-2ac00000 rw-p 01540000 00:05 23292 /dev/ashmem/dalvik-main space (region space) (deleted) 6f181000-6f3a6000 rw-p 00000000 fd:01 221 /data/dalvik-cache/arm/system@framework@boot.art 6f3a6000-6f3bc000 r--p 00225000 fd:01 221 /data/dalvik-cache/arm/system@framework@boot.art 6f3bc000-6f4b3000 rw-p 00000000 fd:01 229 /data/dalvik-cache/arm/system@framework@boot-core-libart.art 6f4b3000-6f4c5000 r--p 000f7000 fd:01 229 /data/dalvik-cache/arm/system@framework@boot-core-libart.art 6f4c5000-6f4f6000 rw-p 00000000 fd:01 232 /data/dalvik-cache/arm/system@framework@boot-conscrypt.art 6f4f6000-6f4f9000 r--p 00031000 fd:01 232 /data/dalvik-cache/arm/system@framework@boot-conscrypt.art 6f4f9000-6f526000 rw-p 00000000 fd:01 235 /data/dalvik-cache/arm/system@framework@boot-okhttp.art 6f526000-6f529000 r--p 0002d000 fd:01 235 /data/dalvik-cache/arm/system@framework@boot-okhttp.art 6f529000-6f57f000 rw-p 00000000 fd:01 240 /data/dalvik-cache/arm/system@framework@boot-bouncycastle.art ...
/proc/$pid/mem
を使って、pidで指定したプロセスが持つメモリを読むことができます。
よくある誤解として、ptraceシステムコールを使ってターゲットのプロセスにアタッチ(PTRACE_ATTACH)し、
プロセスを止めないとメモリを読めないというのがありますが、
最近のLinuxカーネルではアタッチしなくとも、システムコールのopen()、read()、lseek() を使ってメモリを読み出すことが可能です。
apk-meditでは、open()、read()、lseek()を直接は使っていませんが、
内部でそれぞれのシステムコールを使用しているio.NewSectionReader
、File.WriteAt
を使って、読み込み、書き込みを行っています。
メモリマッピング情報から読み込み、書き込みが共にできる部分がどこなのかを得て、/proc/$pid/mem
を使ってメモリを読み出し、ターゲットの値を検索しています。
おわりに
apk-medit
が脆弱性診断に従事するエンジニアに広く使われるよう育てていきたいと思っています。
ぜひ、気づいたことがあればフィードバックしてもらえるとうれしいです。みなさまからのIssueやプルリクエストをお待ちしております。