当社のAIエンジンNeutronが、UCバークレーのCyberGymベンチマークで96.75%のスコアを記録しました。 詳細を見る

セキュリティ

セキュリティ

JNIのリバースエンジニアリング、あるいはFacebookが自社アプリをクラッシュさせている件

Facebookは、ユーザーの反応をテストしFacebookサービスへの定着度を評価する目的で、自社アプリを意図的にクラッシュさせているようです。しかし本稿はユーザーの振る舞い解析についてではなく、それがどのように行われているかという技術的側面について、あるいは単にJNIのリバースエンジニアリングに踏み込むための口実についてのものです。

どうやらFacebookは自社アプリをクラッシュさせているようです。 ユーザーの反応をテストし、Facebookサービスへの定着度を評価するために意図的に行っているとのことです。しかし本稿はユーザーの振る舞い解析についてではなく、 それがどのように行われているかという技術的側面について、あるいは単にJNIのリバースエンジニアリングに 踏み込むための口実についてのものです。

まず、Facebookアプリのクラッシュ機能についてJNIを取り上げるに至った経緯からです。Ostorlab Mobile Application Security Scanner(完全に無料で、今すぐ試せます)を使っていたところ、次のネイティブメソッドを発見しました。

代替テキスト

ネイティブメソッドとは、それがJNI(Java Native Interface)を使ってネイティブコードで実装されていることを示します。

JNIとは、基本的にJavaからアクセス可能なコードをC/C++で書くための仕組みです。これはさまざまな理由で行われ、 パフォーマンスはその一つでしょう。ネイティブコードを書く利点は主にパフォーマンス、より優れたコードの 難読化、そして低レベル機能へのアクセスが可能になることです。欠点は、移植性の欠如と、より高いセキュリティリスクです。

したがって、ネイティブメソッドがユーザー入力を扱う場合、攻撃者の視点からは格好の標的になります。 Instagramアプリの例では、ネイティブメソッドは画像や動画の処理に使われており、そのためメモリ破壊の エラーに対して潜在的に脆弱です。

JNIのリバースエンジニアリングとファジングは、Androidモバイルアプリケーションにおける重要な役割にもかかわらず、 研究が十分に進んでいないテーマのようです。

では、FacebookのcrashThisProcessメソッドに話を戻しましょう。どうすればこのメソッドの実装を見つけられるでしょうか。

JVMがメソッドをどう解決するかについてのJava JNI仕様を確認すると、次のように書かれています。

代替テキスト

ライブラリのシンボルを文字列Java_で検索して調べたところ、crashThisProcessという名前のメソッドは 見つかりませんでした。

代替テキスト

OpenJDK VMを調べると仕様どおりであることは確認できますが、それ以上の手がかりは得られません。

しかし文字列crashThisProcessを検索したところ、libbreakpad.soの中に見つかりました。

このバイナリをリバースエンジニアリングして、この文字列がどのように参照されているかを理解してみましょう。IDAでバイナリを開き、 文字列を検索すると、.rodataセクションの中に見つかります。

この文字列は次の構造体によって参照されています。

さらに調べた結果、この構造体がJNINativeMethod構造体と一致することが分かりました。

そして、これがその使い方を示すサンプルコードです。

/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#define LOG_TAG "simplejni native.cpp"
#include <utils/Log.h>

#include <stdio.h>

#include "jni.h"

static jint
add(JNIEnv *env, jobject thiz, jint a, jint b) {
int result = a + b;
    ALOGI("%d + %d = %d", a, b, result);
    return result;
}

static const char *classPathName = "com/example/android/simplejni/Native";
static JNINativeMethod methods[] = {
  {"add", "(II)I", (void*)add },
};

/*
 * Register several native methods for one class.
 */

static int registerNativeMethods(JNIEnv* env, const char* className,
    JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz;
    clazz = env->FindClass(className);
    if (clazz == NULL) {
        ALOGE("Native registration unable to find class '%s'", className);
        return JNI_FALSE;
    }

    if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
        ALOGE("RegisterNatives failed for '%s'", className);
        return JNI_FALSE;
    }

    return JNI_TRUE;
}

/*
 * Register native methods for all classes we know about.
 *
 * returns JNI_TRUE on success.
 */

static int registerNatives(JNIEnv* env)
{
  if (!registerNativeMethods(env, classPathName,
                 methods, sizeof(methods) / sizeof(methods[0]))) {
    return JNI_FALSE;
  }

  return JNI_TRUE;
}

// ----------------------------------------------------------------------------

/*
 * This is called by the VM when the shared library is first loaded.
 */

typedef union {
    JNIEnv* env;
    void* venv;
} UnionJNIEnvToVoid;

jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    UnionJNIEnvToVoid uenv;
    uenv.venv = NULL;
    jint result = -1;
    JNIEnv* env = NULL;
    ALOGI("JNI_OnLoad");
    if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_4) != JNI_OK) {
        ALOGE("ERROR: GetEnv failed");
        goto bail;
    }
    env = uenv.env;
    if (registerNatives(env) != JNI_TRUE) {
        ALOGE("ERROR: registerNatives failed");
        goto bail;
    }

    result = JNI_VERSION_1_4;

bail:
    return result;
}

これらのメソッドは、読み込み時にJNI_OnLoad関数の中でregisterNativeMethods関数を使って動的に登録されているようです。 最後の要素はJNIメソッド本体を指しています。例えばcrashThisProcessメソッドの場合、 これがそのメソッド本体の逆アセンブル結果です(サブルーチンの名前は手動で付加しました)。

この関数がメモリアドレス0x000000に0x2Aを書き込んでおり、これがプロセスをセグフォルトさせることが分かります。

Javaのメソッドシグネチャ固有のフォーマットを用いてこれらの構造体を自動的に識別し、その後に構造体の残りを確認する 手法を試したところ、JNI_OnLoadを使って動的にエクスポートされた、あるいはエクスポートされたシンボルとして静的に エクスポートされたJNIメソッドを見つけるスクリプト(GitHubで公開)を書くことができました。

Ostorlab Mobile Application Security Scannerは、今後のバージョンでこれらのメソッドを自動的に一覧表示するようになります。

タグ:

android, reverse, mobile