Bypassing Shopee Lite F13 Detection — Deep Dive Android Anti-Tampering dengan Frida
Demo Video: https://www.youtube.com/watch?v=pAX85cxq-qk
Target: com.shopee.lite.id (Shopee Lite Indonesia)
Tools: Frida 17.9.6 · LDPlayer 9 · Node.js · ADB
Tujuan: Riset & edukasi — memahami mekanisme anti-tampering berlapis pada aplikasi e-commerce production-grade
Latar Belakang
Shopee Lite adalah versi ringan dari aplikasi Shopee yang ditargetkan untuk perangkat low-end. Meski "lite", sistem keamanannya tidak main-main. Aplikasi ini menggunakan stack anti-tampering berlapis yang melibatkan library native, SDK pihak ketiga, dan protobuf telemetry — semua bekerja bersamaan untuk mendeteksi emulator, root, dan dynamic instrumentation tools seperti Frida.
Error yang muncul saat terdeteksi:
"Gagal Log in (F13): Kamu terdeteksi menggunakan perangkat lunak/keras yang tidak resmi. Pastikan kamu mematuhi kebijakan Shopee."
Kode F13 adalah kode internal Shopee untuk fraud detection triggered at login. Tantangannya: F13 bukan satu pengecekan tunggal — ini adalah hasil agregat dari puluhan sinyal deteksi yang dikumpulkan dari berbagai layer.
Environment Setup
Emulator : LDPlayer 9 (Android 9 / x86_64)
Frida : 17.9.6 (frida-server di /data/local/tmp/)
Script : bypass.js (Frida JS)
Tools : ADB, Node.js (untuk log filter & replay)
Penting: Tidak menggunakan proxy (Burp/mitmproxy/HTTP Toolkit). Proxy settings yang tertinggal di emulator justru menjadi sinyal deteksi tambahan yang dibaca SDK. Semua analisis dilakukan murni via Frida hook di level OkHttp dan SSL.
Arsitektur Deteksi F13 — Peta Musuh
Setelah analisis beberapa minggu, berikut layer deteksi yang ditemukan:
┌─────────────────────────────────────────────────────────┐
│ Layer 1 (Java) │ RootBeer · androidx.core.b · Build │
├─────────────────────────────────────────────────────────┤
│ Layer 2 (Java) │ CommonInfo telemetry · DeviceExt │
├─────────────────────────────────────────────────────────┤
│ Layer 3 (Java) │ SHPSSDK / TongDun (SPS token) │
├─────────────────────────────────────────────────────────┤
│ Layer 4 (Native) │ libshpssdk.so · libtoolChecker.so │
├─────────────────────────────────────────────────────────┤
│ Layer 5 (Native) │ /proc/self/maps · dl_iterate_phdr │
├─────────────────────────────────────────────────────────┤
│ Layer 6 (Native) │ uname · getauxval · cpuinfo · ARP │
├─────────────────────────────────────────────────────────┤
│ Layer 7 (WebView)│ /verify/traffic/error redirect │
└─────────────────────────────────────────────────────────┘
Layer 1: RootBeer + androidx.core.b
RootBeer Native
Library open-source yang populer untuk root detection. Shopee menggunakannya sebagai first gate.
function bypassRootBeer() {
const RootBeerNative = Java.use('com.scottyab.rootbeer.RootBeerNative');
RootBeerNative.checkForRoot.implementation = function (_paths) {
return 0;
};
}
androidx.core.b.q() — THE Root Check
Method q() di class obfuscated androidx.core.b melakukan tiga pengecekan sekaligus:
Build.TAGS.contains("test-keys") — emulator default "test-keys"
- Cek file
su di berbagai path
Runtime.exec(["which", "su"]) — eksekusi shell
CoreUtils.q.implementation = function () { return false; };
Runtime.exec.overload('[Ljava.lang.String;').implementation = function (cmdarray) {
if (cmdarray && cmdarray.length >= 2) {
const last = String(cmdarray[cmdarray.length - 1]);
if (last === 'su' || last === 'su\n') {
return this.exec(['/system/bin/true']);
}
}
return this.exec(cmdarray);
};
Challenge: q() return true bahkan setelah file hook, karena Runtime.exec(which su) di LDPlayer berhasil (su memang ada). Hook di Runtime.exec harus dipasang sebelum q() dipanggil — timing matters.
Layer 2: CommonInfo Telemetry + DeviceExt Protobuf
Shopee menggunakan protobuf untuk mengirim telemetry ke server. Field kritis: isRoot, isEmulator, deviceFingerPrint.
Karena field-nya adalah Kotlin val (immutable), kita patch via reflection sebelum serialisasi:
CommonInfo.toPBCommonInfo.implementation = function () {
const clazz = this.getClass();
const f1 = clazz.getDeclaredField('isRoot');
f1.setAccessible(true);
f1.setBoolean(this, false);
const f2 = clazz.getDeclaredField('isEmulator');
f2.setAccessible(true);
f2.setBoolean(this, false);
const fp = clazz.getDeclaredField('deviceFingerPrint');
fp.setAccessible(true);
fp.set(this, 'ed083ba0ae636b33_SM-G9750');
return this.toPBCommonInfo();
};
Layer 3: SHPSSDK + TongDun — Yang Paling Kompleks
TongDun Blackbox
SDK fraud detection asal China yang generate fingerprint terenkripsi. Bypass: set ke string kosong di level ClientIdentifier.Builder.
SHPSSDK (libshpssdk.so)
SDK buatan Shopee yang generate SPS Token — token fingerprint device terenkripsi yang dikirim bersama login request.
Key insight: Jangan return "" dari getRiskToken()! Token kosong menyebabkan server mengirim response yang lebih parah dari F13 ("Halaman Tidak Tersedia" yang block seluruh home page).
Strategi yang benar: biarkan libshpssdk.so generate token real, tapi scrub semua sinyal emulator/root/Frida via native hooks sebelum library membaca data tersebut.
SHPSSDK_cls.getRiskToken.overload('android.content.Context').implementation = function (ctx) {
const token = this.getRiskToken.call(this, ctx);
log('SPS', 'getRiskToken() len=' + (token ? token.length : 0));
return token;
};
SHPSSDK_cls2.getRiskSync.overload('android.content.Context').implementation = function (_ctx) {
return ArrayList.$new();
};
SPSDelegate.getDeviceFingerPrint.implementation = function (_ctx) {
return '';
};
Layer 4: Native Library Hooks
libtoolChecker.so
Library native khusus dengan fungsi _Z6existsPKc (C++ mangled: exists(const char*)). Cek file berbahaya ada atau tidak.
Karena di-load via dlopen() setelah startup, kita butuh dlopen watcher — hook dlopen dan pasang hook ke exists() segera setelah library masuk:
safeAttach(dlopenSym, {
onLeave: function (_ret) {
if (hooked || !this.lib || this.lib.indexOf('libtoolChecker.so') === -1) return;
const s = resolveExport('_Z6existsPKc', 'libtoolChecker.so');
if (s) {
safeAttach(s, {
onLeave: function (r) {
if (r.toInt32() !== 0) r.replace(ptr(0));
}
});
hooked = true;
}
}
});
Fake Files untuk Native Reads
echo "Processor : AArch64 Processor rev 12 (aarch64)
Hardware : Samsung Exynos 9820" > /data/local/tmp/fake_cpuinfo
echo "IP address HW type Flags HW address Mask Device
192.168.1.1 0x1 0x2 dc:a6:32:xx:xx:xx * wlan0" > /data/local/tmp/fake_arp
echo "280" > /data/local/tmp/fake_battery_cycle
Layer 5: Frida Self-Detection via /proc dan dl_iterate_phdr
/proc/self/maps
Frida meninggalkan jejak:
7f1234000000-7f1235000000 r-xp ... /memfd:frida-agent-64.so (deleted)
Hook fopen(), open(), openat(), __open_2(), dan syscall() untuk redirect ke filtered maps.
Kenapa tidak redirect ke /dev/null? File maps KOSONG adalah anomali besar — app real selalu punya 200+ entry. Kita generate filtered maps via shell subprocess:
var proc = rt.exec([
'/system/bin/sh', '-c',
"cat /proc/" + pid + "/maps | grep -v frida | grep -v gadget > /data/local/tmp/fake_maps_sps"
]);
proc.waitFor();
Shell subprocess ini membaca dari /proc/<pid>/maps secara langsung (bukan via fopen hook kita) sehingga dapat data real.
dl_iterate_phdr — Yang Paling Tricky
dl_iterate_phdr() tidak membaca /proc/self/maps — dia scan linked list internal r_debug dari dynamic linker. File redirection tidak berpengaruh!
Solusi: replace callback yang di-pass ke dl_iterate_phdr dengan wrapper yang filter entry Frida:
var _dlWrapper = new NativeCallback(function (info, sz, ud) {
var np = info.add(8).readPointer();
if (!np.isNull()) {
var nm = np.readCString();
if (nm && (nm.indexOf('frida') !== -1 || nm.indexOf('gadget') !== -1)) {
return 0;
}
}
return _dlOrigFn(info, sz, ud);
}, 'int', ['pointer', 'uint64', 'pointer']);
Interceptor.attach(dl_iter_sym, {
onEnter: function (args) {
_dlOrigFn = new NativeFunction(args[0], 'int', ['pointer', 'uint64', 'pointer']);
args[0] = _dlWrapper;
}
});
Challenge: NativeCallback harus dibuat SEKALI — dl_iterate_phdr dipanggil 100x saat startup dan per-call allocation menyebabkan V8 heap bloat yang parah.
Layer 6: Hardware & Kernel Fingerprinting
uname() — Machine Type
Emulator x86_64 return machine = "x86_64". Device ARM64 return "aarch64".
Interceptor.attach(uname_sym, {
onLeave: function (retval) {
var mach = this.utsPtr.add(65 * 4).readUtf8String();
if (mach && mach.indexOf('x86') !== -1) {
this.utsPtr.add(65 * 4).writeUtf8String('aarch64');
}
}
});
getauxval(AT_HWCAP) — CPU Capabilities
AT_HWCAP di x86_64 = 0x178bfbff. ARM64 (Exynos 9820) = 0xFFFF. SPS membaca ini untuk fingerprint arsitektur.
Trick penting: spoof hanya untuk call dari libshpssdk.so via return address check. Caller lain tidak terpengaruh.
onLeave: function (retval) {
if (this._fromSps && this._auxType === 16 ) {
retval.replace(ptr(0xFFFF));
}
}
__system_property_get — Prop Spoofing
var SPOOF_PROPS = {
'ro.hardware': 'kona',
'ro.kernel.qemu': '0',
'ro.build.tags': 'release-keys',
'ro.product.model': 'SM-G9750',
'ro.product.cpu.abi': 'arm64-v8a',
};
ASensor_getVendor — Jebakan!
Di QEMU semua sensor vendor = "Goldfish". Kita coba spoof ke "STMicroelectronics" (SM-G9750 asli) → malah dapat "Halaman Tidak Tersedia" yang lebih parah dari F13!
Kesimpulan: server cross-check sensor vendor dengan model device. SM-G9750 dengan sensor vendor aneh = inkonsistensi yang lebih mencurigakan. Tidak semua sinyal perlu di-spoof.
Layer 7: WebView Traffic Verification
Setelah login bypass F13, masih ada checkpoint via WebView. Risk score tinggi → redirect ke:
https://mall.shopee.co.id/verify/traffic/error?is_logged_in=true
Bypass: intercept loadUrl() dan relaunch app dengan FLAG_ACTIVITY_CLEAR_TASK:
WebView.loadUrl.overload('java.lang.String').implementation = function (url) {
if (url && url.indexOf('/verify/traffic/error') !== -1
&& url.indexOf('is_logged_in=true') !== -1) {
var launchIntent = pm.getLaunchIntentForPackage('com.shopee.lite.id');
launchIntent.setFlags(0x10000000 | 0x20000000);
ctx.startActivity(launchIntent);
Java.cast(ctx, Activity).finish();
return;
}
return this.loadUrl(url);
};
Build Props Spoofing
Build.FINGERPRINT.value = 'samsung/beyond1qltezxm/beyond1q:12/SP1A.210812.016/G9750ZHU7GVI5:user/release-keys';
Build.HARDWARE.value = 'kona';
Build.MODEL.value = 'SM-G9750';
Build.TAGS.value = 'release-keys';
Build.TYPE.value = 'user';
Challenges Terbesar
| # | Masalah | Root Cause | Fix |
| 1 | ClassNotFoundException untuk Shopee classes | Zygote fork sebelum DEX load | Enumerate class loaders, tunggu CommonInfo tersedia |
| 2 | F13 masih muncul setelah Java hooks | libshpssdk.so sudah scan sebelum Java hooks ready | Pasang native hooks lebih awal (outside Java.perform) |
| 3 | "Halaman Tidak Tersedia" setelah sensor spoof | Server cross-check vendor vs model device | Biarkan ASensor_getVendor logging-only |
| 4 | Empty maps file justru suspicious | App real punya 200+ entries | Generate filtered maps via shell subprocess |
| 5 | dl_iterate_phdr bypass maps redirect | Baca linker internal, bukan /proc/self/maps | Hook dan replace callback-nya |
| 6 | V8 heap bloat dari NativeCallback | Per-call allocation di hot path | Allocate wrapper sekali, reuse |
| 7 | Proxy settings jadi sinyal deteksi | HTTP Toolkit leftover settings | Delete via adb shell settings delete global global_http_proxy_* |
Hasil
✅ Login berhasil tanpa error F13
✅ Home page Shopee Lite terbuka dalam kondisi logged-in
✅ Tidak ada redirect ke /verify/traffic/error
✅ SPS token ter-generate dengan sinyal emulator/Frida terscrub
Demo: https://www.youtube.com/watch?v=pAX85cxq-qk
Kesimpulan & Lessons Learned
Defense in Depth yang sesungguhnya. Bypass satu layer tidak cukup — server mengagregasi sinyal dari semua layer. Semua harus di-address bersamaan.
Native layer lebih tricky dari Java layer. Hook Java bisa gagal karena timing class loader. Hook native harus dipasang di early startup sebelum library target melakukan pengecekan.
Spoofing tidak selalu lebih baik dari blocking. ASensor_getVendor dan beberapa sinyal lain lebih aman dibiarkan asli daripada di-spoof dengan nilai yang inkonsisten dengan model device yang diklaim.
Server-side validasi juga ada. SPS token mengandung sinyal terenkripsi yang divalidasi server. Token kosong/invalid mendapat response berbeda (lebih parah) dari F13.
Frida meninggalkan banyak jejak non-obvious: memfd di /proc/self/maps, dl_iterate_phdr, /proc/self/fd/* symlinks. Harus di-handle semuanya.
Proxy = sinyal deteksi. HTTP Toolkit/Burp proxy settings yang tertinggal di emulator terbaca oleh SDK dan menambah risk score. Selalu bersihkan setelah selesai.
Tulisan ini murni untuk keperluan riset dan edukasi. Semua pengujian dilakukan pada akun sendiri di environment terkontrol.