概述
摇一摇”开屏广告是一种通过摇动手机触发广告跳转的广告形式,由芒果TV发明并申请了相关专利。
该功能在App中普遍存在,但由于其高灵敏度,常在非用户主动操作的情况下触发广告跳转,侵犯用户权益,
工信部发布通知,要求移动互联网应用不得利用高灵敏度“摇一摇”等方式诱导用户操作。
实现机制 “摇一摇”广告功能通常通过调用手机中的加速度、重力、陀螺仪等一种或多种传感器实现,当用户摇动手机触发相应的传感器阈值后可实现广告交互。
监管要求 根据电信终端产业协会发布的(T/TAF078.7-2022)《APP用户权益保障测评规范 第7部分:欺骗误导强迫行为》
加速度不小于15m/s²
转动角度不小于35°
操作时间不少于3秒
定位函数 实现原理分析可知,App注册传感器监听器之后,需要传递一个监听器对象,这个对象包含了 onSensorChanged 和 onAccuracyChanged 两个方法,对安装包进行反编译查找 onSensorChanged 相关代码
分析案例 京东广告 示例应用:喜马拉雅.apk
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com.jd.ad.sdk.bl.dynamicrender;import android.content.Context;import android.hardware.Sensor;import android.hardware.SensorEvent;import android.hardware.SensorEventListener;import android.hardware.SensorManager;import com.umeng.analytics.pro.ak;public abstract class ShakeListener implements SensorEventListener { public SensorManager jad_an; public int jad_bo; public ShakeListener (Context context, int i) { this .jad_bo = 19 ; try { this .jad_an = (SensorManager) context.getSystemService(ak.ac); } catch (SecurityException e2) { e2.printStackTrace(); } this .jad_bo = i; } @Override public void onAccuracyChanged (Sensor sensor, int i) { } @Override public void onSensorChanged (SensorEvent sensorEvent) { if (sensorEvent.sensor.getType() == 1 ) { float [] fArr = sensorEvent.values; if (Math.sqrt(Math.pow(fArr[2 ], 2.0d ) + Math.pow(fArr[1 ], 2.0d ) + Math.pow(fArr[0 ], 2.0d )) > this .jad_bo) { onShake(); unregister(); } } } public abstract void onShake () ; public void register () { SensorManager sensorManager = this .jad_an; if (sensorManager != null ) { sensorManager.registerListener(this , sensorManager.getDefaultSensor(1 ), 2 ); } } public void unregister () { SensorManager sensorManager = this .jad_an; if (sensorManager != null ) { sensorManager.unregisterListener(this ); } } }
在 onSensorChanged 方法中,它使用的算法
1 2 3 4 if (Math.sqrt(Math.pow(fArr[2 ], 2.0d ) + Math.pow(fArr[1 ], 2.0d ) + Math.pow(fArr[0 ], 2.0d )) > this .jad_bo) { onShake(); unregister(); }
通过计算三轴值分别平方的和,再开方,与 this.jad_bo 的值进行比较,如果大于则进入摇一摇逻辑。
在 ShakeListener 的构造函数中 this.jad_bo 的默认值是 19,通过第二个参数 i 赋值动态的阀值给 this.jad_bo
淘宝广告 示例应用:天猫.apk
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 package android.taobao.windvane.jsbridge.api;import android.content.Context;import android.hardware.Sensor;import android.hardware.SensorEvent;import android.hardware.SensorEventListener;import android.hardware.SensorManager;import android.taobao.windvane.util.TaoLog;public class ShakeListener implements SensorEventListener { private static final int SPEED_THRESHOLD = 10 ; private long mCheckFrequency; private Context mContext; private long mLastUpdateTime; private float mLastX; private float mLastY; private float mLastZ; private SensorManager mSensorManager; private OnShakeListener mShakeListener; public interface OnShakeListener { void onShake () ; } public ShakeListener (Context context, long j) { this .mContext = context; this .mCheckFrequency = j; start(); } @Override public void onAccuracyChanged (Sensor sensor, int i) { } @Override public void onSensorChanged (SensorEvent sensorEvent) { OnShakeListener onShakeListener; if (sensorEvent.sensor.getType() != 1 ) { return ; } long currentTimeMillis = System.currentTimeMillis(); if (currentTimeMillis - this .mLastUpdateTime < this .mCheckFrequency) { return ; } float [] fArr = sensorEvent.values; float f = fArr[0 ]; float f2 = fArr[1 ]; float f3 = fArr[2 ]; float f4 = f - this .mLastX; float f5 = f2 - this .mLastY; float f6 = f3 - this .mLastZ; if (Math.sqrt((f4 * f4) + (f5 * f5) + (f6 * f6)) > 10.0d && (onShakeListener = this .mShakeListener) != null && onShakeListener != null && Math.abs(this .mLastX) > 0.0f && Math.abs(this .mLastY) > 0.0f && Math.abs(this .mLastZ) > 0.0f ) { this .mShakeListener.onShake(); } this .mLastUpdateTime = currentTimeMillis; this .mLastX = f; this .mLastY = f2; this .mLastZ = f3; } public void pause () { SensorManager sensorManager = this .mSensorManager; if (sensorManager != null ) { sensorManager.unregisterListener(this ); } } public void resume () { SensorManager sensorManager = this .mSensorManager; if (sensorManager == null || sensorManager.registerListener(this , sensorManager.getDefaultSensor(1 ), 2 )) { return ; } this .mSensorManager.unregisterListener(this ); TaoLog.m263w("ShakeListener" , "start: Accelerometer not supported" ); } public void setOnShakeListener (OnShakeListener onShakeListener) { this .mShakeListener = onShakeListener; } public void start () { SensorManager sensorManager = (SensorManager) this .mContext.getSystemService("sensor" ); this .mSensorManager = sensorManager; if (sensorManager == null ) { TaoLog.m263w("ShakeListener" , "start: Sensors not supported" ); } else { if (sensorManager.registerListener(this , sensorManager.getDefaultSensor(1 ), 2 )) { return ; } this .mSensorManager.unregisterListener(this ); TaoLog.m263w("ShakeListener" , "start: Accelerometer not supported" ); } } public void stop () { SensorManager sensorManager = this .mSensorManager; if (sensorManager != null ) { sensorManager.unregisterListener(this ); this .mSensorManager = null ; } } }
在 onSensorChanged 的方法中我们可以看到
1 2 3 if (sensorEvent.sensor.getType() != 1 ) { return ; }
判断传感器类型不是加速度就终止向下执行
当加速度类型条件满足,首先做了时间间隔的判断
1 2 3 4 long currentTimeMillis = System.currentTimeMillis();if (currentTimeMillis - this .mLastUpdateTime < this .mCheckFrequency) { return ; }
并接着分别获取了三轴的值
1 2 3 4 float [] fArr = sensorEvent.values;float f = fArr[0 ];float f2 = fArr[1 ];float f3 = fArr[2 ];
计算三轴两次的变化差异
1 2 3 float f4 = f - this .mLastX;float f5 = f2 - this .mLastY;float f6 = f3 - this .mLastZ;
接着就是一个判断函数
当满足条件,执行具体的摇一摇函数 this.mShakeListener.onShake();
1 2 3 if (Math.sqrt((f4 * f4) + (f5 * f5) + (f6 * f6)) > 10.0d && (onShakeListener = this .mShakeListener) != null && onShakeListener != null && Math.abs(this .mLastX) > 0.0f &&Math.abs(this .mLastY) > 0.0f && Math.abs(this .mLastZ) > 0.0f ) { this .mShakeListener.onShake(); }
最后, 保存本次的时间戳和三轴值
1 2 3 4 this .mLastUpdateTime = currentTimeMillis;this .mLastX = f;this .mLastY = f2;this .mLastZ = f3;
分析判断条件 条件判断 1 1 Math.sqrt((f4 * f4) + (f5 * f5) + (f6 * f6))>10.0d
同样也是,这里计算三轴分别变化差异的平方和,再开方,得到sqrt 加速度的值
判断这个返回值 sqrt 是否大于 10.0d (单位:m/s²)
条件判断 2/3 1 (onShakeListener = this .mShakeListener) != null && onShakeListener != null
接下来重点看这两个函数
1 2 3 4 5 6 7 8 public ShakeListener (Context context, long j) { this .mContext = context; this .mCheckFrequency = j; start(); } public void setOnShakeListener (OnShakeListener onShakeListener) { this .mShakeListener = onShakeListener; }
ShakeListener(Context context, long j) 是 ShakeListener 类的构造函数,传递了两个参数
在 setOnShakeListener(OnShakeListener onShakeListener) 方法中,this.mShakeListener 被 onShakeListener 赋值过,如果后续 this.mShakeListener 不再被赋值,那么 this.mShakeListener = onShakeListener;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public synchronized void listeningShake (WVCallBackContext wVCallBackContext, String str) { boolean z; WVResult wVResult = new WVResult (); long j = 500 ; long j2 = 1000 ; boolean z2 = false ; if (TextUtils.isEmpty(str)) { z = false ; } else { try { str = URLDecoder.decode(str, "utf-8" ); } catch (Exception unused) { TaoLog.m254e("WVMotion" , "listeningShake: param decode error, param=" + str); z2 = true ; } try { JSONObject jSONObject = new JSONObject (str); z = jSONObject.getBoolean(Baggage.Amnet.TURN_ON); j = jSONObject.optLong("frequency" , 500L ); j2 = jSONObject.optLong("checkFrequency" , 1000L ); } catch (JSONException unused2) { TaoLog.m254e("WVMotion" , "listeningShake: param parse to JSON error, param=" + str); wVResult.setResult("HY_PARAM_ERR" ); wVCallBackContext.error(wVResult); return ; } } if (z2) { if (TaoLog.getLogStatus()) { TaoLog.m263w("WVMotion" , "listeningShake: isFail" ); } wVCallBackContext.error(wVResult); return ; } if (z) { TaoLog.m254e("WVMotion" , "listeningShake: start ..." ); ShakeListener shakeListener = this .mShakeListener; if (shakeListener != null ) { shakeListener.stop(); } ShakeListener shakeListener2 = new ShakeListener (this .mContext, j2); this .mShakeListener = shakeListener2; shakeListener2.setOnShakeListener(new MyShakeListener (wVCallBackContext, j)); wVCallBackContext.success(wVResult); } else { TaoLog.m254e("WVMotion" , "listeningShake: stop." ); Message message = new Message (); message.what = 1 ; message.obj = wVCallBackContext; Handler handler = this .handler; if (handler != null ) { handler.sendMessage(message); } } }
在以上片段中,重点看两句代码
1 2 ShakeListener shakeListener2 = new ShakeListener (this .mContext, j2); shakeListener2.setOnShakeListener(new MyShakeListener (wVCallBackContext, j));
1 2 long j = 500 ; long j2 = 1000 ;
这两个值分别默认 500、100,通过 listeningShake 方法第二个参数 str 传入的 String 转成 json 字符串判断是否存在,并动态赋值
继续看判断的条件 4、5、6
条件判断 4/5/6 1 Math.abs(this .mLastX) > 0.0f && Math.abs(this .mLastY) > 0.0f && Math.abs(this .mLastZ) > 0.0f
已知 this.mLastX / this.mLastY / this.mLastZ 分别表示三轴上一次数据 可以分析得知,条件 4、5、6 的意义
1 2 3 Math.abs(this .mLastX) > 0.0f Math.abs(this .mLastY) > 0.0f Math.abs(this .mLastZ) > 0.0f
即条件4、5、6代表三轴的数据的绝对值都要大于0
实现案例 我们按以上的实现方式,模拟实现一个类似的摇一摇跳转广告示例
ShakeListener.kt 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 package com.zcwx.sensorshakeimport android.app.Activityimport android.content.Contextimport android.hardware.Sensorimport android.hardware.SensorEventimport android.hardware.SensorEventListenerimport android.hardware.SensorManagerimport android.util.Logimport android.widget.TextViewimport java.util.Localeimport kotlin.math.acosimport kotlin.math.sqrtclass ShakeListener ( private val context: Context, private val accLog: TextView, ) : SensorEventListener { private var sensorManager: SensorManager? = null private var accSensor: Sensor? = null private var onShakeListener: OnShakeListener? = null private var lastX = 0f private var lastY = 0f private var lastZ = 0f init { start() } private fun start () { sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager if (sensorManager != null ) { accSensor = sensorManager?.getDefaultSensor(1 ) } if (accSensor != null ) { sensorManager?.registerListener(this , accSensor, SensorManager.SENSOR_DELAY_NORMAL) } } fun stop () { sensorManager?.unregisterListener(this ) } interface OnShakeListener { fun onShake () } fun setOnShakeListener (listener: OnShakeListener ?) { onShakeListener = listener } override fun onSensorChanged (event: SensorEvent ) { when (event.sensor.type) { 1 -> { val x = event.values[0 ] val y = event.values[1 ] val z = event.values[2 ] lastX = x lastY = y lastZ = z val speed = String.format (Locale.getDefault(),"%.1f" , sqrt(lastX * lastX + lastY * lastY + lastZ * lastZ),).toDouble() Log.d("TAG" , "速度:$speed " ) val str = "加速度\n" + "X:\t${event.values[0 ]} \n" + "Y:\t${event.values[1 ]} \n" + "Z:\t${event.values[2 ]} \n" + "sqrt:\t$speed \n" accLog.text = str if (speed >= SPEED_SHRES_HOLD) { Log.d("TAG" , "onSensorChanged: 触发摇一摇,跳转广告页面了..." ) Log.d("TAG" , "x:$x ,y:$y ,z:$z " ) Log.d("TAG" , "速度:$speed " ) onShakeListener!!.onShake() } } } } override fun onAccuracyChanged (sensor: Sensor , accuracy: Int ) { } companion object { private const val SPEED_SHRES_HOLD = 15 } }
MainActivity.kt 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 package com.zcwx.sensorshakeimport android.content.Intentimport android.net.Uriimport android.os.Bundleimport android.os.VibrationEffectimport android.os.Vibratorimport android.os.VibratorManagerimport android.util.Logimport android.widget.Toastimport androidx.activity.enableEdgeToEdgeimport androidx.appcompat.app.AppCompatActivityimport androidx.core.view.ViewCompatimport androidx.core.view.WindowInsetsCompatclass MainActivity : AppCompatActivity () { companion object { private const val TAG = "MainActivity" } private lateinit var shakeListener: ShakeListener override fun onCreate (savedInstanceState: Bundle ?) { super .onCreate(savedInstanceState) enableEdgeToEdge() setContentView(R.layout.activity_main) ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) insets } shakeListener = ShakeListener(this , findViewById(R.id.tvAccLog)) shakeListener.setOnShakeListener(object : ShakeListener.OnShakeListener{ override fun onShake () { Toast.makeText(this @MainActivity , "摇一摇,手机摇晃了🫨" , Toast.LENGTH_SHORT).show() onVibrator().let { openLinkInBrowser() } } }) } override fun onDestroy () { super .onDestroy() shakeListener.stop() } private fun openLinkInBrowser () { Log.d(TAG, "openLinkInBrowser: " ) val url = "https://www.apple.com/" val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) startActivity(intent) } private fun onVibrator () { Log.d(TAG, "onVibrator: " ) val vibrator:Vibrator if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.S) { val vibratorManager: VibratorManager = getSystemService(VIBRATOR_MANAGER_SERVICE) as VibratorManager vibrator= vibratorManager.defaultVibrator }else { vibrator= getSystemService(VIBRATOR_SERVICE) as Vibrator } vibrator.vibrate(VibrationEffect.createOneShot(500 , 255 )) } }
AndroidManifest.xml 1 <uses-permission android:name ="android.permission.VIBRATE" />
运行 1 2 3 4 5 6 7 8 9 10 11 12 2024-12-25 14:28:08.830 6734-6734 ShakeListener com.zcwx.sensorshake D x:0.51834464,y:-0.094570965,z:9.829395 speed:9.8 2024-12-25 14:28:09.030 6734-6734 ShakeListener com.zcwx.sensorshake D x:0.5177461,y:-0.091578215,z:9.829993 speed:9.8 2024-12-25 14:28:09.227 6734-6734 ShakeListener com.zcwx.sensorshake D x:0.5177461,y:-0.093373865,z:9.829395 speed:9.8 2024-12-25 14:28:09.424 6734-6734 ShakeListener com.zcwx.sensorshake D x:0.5177461,y:-0.093972415,z:9.831789 speed:9.8 2024-12-25 14:28:09.631 6734-6734 ShakeListener com.zcwx.sensorshake D x:19.515951,y:-0.094570965,z:9.834184 speed:21.9 2024-12-25 14:28:09.636 6734-6734 ShakeListener com.zcwx.sensorshake D onSensorChanged: 触发摇一摇,跳转广告页面了... 2024-12-25 14:28:09.636 6734-6734 ShakeListener com.zcwx.sensorshake D x:19.515951,y:-0.094570965,z:9.834184 2024-12-25 14:28:09.636 6734-6734 ShakeListener com.zcwx.sensorshake D 速度:21.9 2024-12-25 14:28:09.643 6734-6734 MainActivity com.zcwx.sensorshake D onVibrator: 2024-12-25 14:28:09.647 6734-6734 MainActivity com.zcwx.sensorshake D openLinkInBrowser: 2024-12-25 14:28:09.827 6734-6734 ShakeListener com.zcwx.sensorshake D x:0.5177461,y:-0.092176765,z:9.835381 speed:9.8 2024-12-25 14:28:10.024 6734-6734 ShakeListener com.zcwx.sensorshake D x:0.521936,y:-0.093972415,z:9.834782 speed:9.8
合规检测 检测加速度不小于15m/s² 假如,现在需要模拟手机摇一摇来测试广告的阀值,检测广告是否合规,只考虑加速度的情况下
可以通过Frida Hook的方式,模拟加速度数据
hook_sensor.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 var isHooked = false ;var SPEED_SHRES_HOLD = 15 function main ( ) { Java .perform (function ( ) { var Random = Java .use ("java.util.Random" ); var random = Random .$new() var SystemSensorManagerSensorEventQueue = Java .use ("android.hardware.SystemSensorManager$SensorEventQueue" ); SystemSensorManagerSensorEventQueue .dispatchSensorEvent .implementation = function (type, values, accuracy, timestamp ) { if (type === 11 ) { if (!isHooked) { isHooked = true ; var originalX = values[0 ]; var originalY = values[1 ]; var originalZ = values[2 ]; var originalSpeed = Math .sqrt (Math .pow (originalX, 2.0 ) + Math .pow (originalY, 2.0 ) + Math .pow (originalZ, 2.0 )); console .log ("\n原始值: [ x=" + originalX + ", y=" + originalY + ", z=" + originalZ + "] speed=" + originalSpeed + " m/s^2" ); console .log ("\n模拟摇一摇操作,设置加速度值" ); var indexToAdjust = random.nextInt (3 ); var newValues = [originalX, originalY, originalZ]; if (SPEED_SHRES_HOLD < 9.8 ) SPEED_SHRES_HOLD = 9.8 ; if (indexToAdjust == 0 ) { var xSquared = (Math .pow (SPEED_SHRES_HOLD , 2.0 )) - (Math .pow (newValues[1 ], 2.0 ) + Math .pow (newValues[2 ], 2.0 )); if (xSquared < 0 ) { console .error ("Cannot generate valid acceleration X values." ); return ; } newValues[0 ] = Math .sqrt (xSquared); } if (indexToAdjust == 1 ) { var ySquared = (Math .pow (SPEED_SHRES_HOLD , 2.0 )) - (Math .pow (newValues[0 ], 2.0 ) + Math .pow (newValues[2 ], 2.0 )); if (ySquared < 0 ) { console .error ("Cannot generate valid acceleration Y values." ); return ; } newValues[1 ] = Math .sqrt (ySquared); } if (indexToAdjust == 2 ) { var zSquared = (Math .pow (SPEED_SHRES_HOLD , 2.0 )) - (Math .pow (newValues[0 ], 2.0 ) + Math .pow (newValues[1 ], 2.0 )); if (zSquared < 0 ) { console .error ("Cannot generate valid acceleration Z values." ); return ; } newValues[2 ] = Math .sqrt (zSquared); } var newX = newValues[0 ]; var newY = newValues[1 ]; var newZ = newValues[2 ]; var newSpeed = Math .sqrt (Math .pow (newX, 2.0 ) + Math .pow (newY, 2.0 ) + Math .pow (newZ, 2.0 )); values[0 ] = newX; values[1 ] = newY; values[2 ] = newZ; console .log ("\n新的值: [ x=" + newX + ", y=" + newY + ", z=" + newZ + "] speed=" + newSpeed + " m/s^2" ); } } return this .dispatchSensorEvent .call (this , type, values, accuracy, timestamp); }; }); } setTimeout (main, 5000 );
执行hook 脚本 hook_sensor.js,输出结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 (.venv ) ~/frida-agent-example $ frida -U -f com.zcwx.sensorshake -l ./ android/hook_sensor.js ____ / _ | Frida 16.4 .8 - A world-class dynamic instrumentation toolkit | (_| | > _ | Commands : /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit/quit -> Exit . . . . . . . . More info at https : . . . . . . . . Connected to LE2110 (id=988b0e34) Spawned `com.zcwx.sensorshake` . Resuming main thread! [LE2110 ::com.zcwx .sensorshake ]-> 原始值: [ x=0.33279404044151306 , y=8.785523414611816 , z=4.349067211151123 ] speed=9.80869813732463 m/s^2 模拟摇一摇操作,设置加速度值 新的值: [ x=11.3534220710849 , y=8.785523414611816 , z=4.349067211151123 ] speed=15.000000000000002 m/s^2 [LE2110 ::com.zcwx .sensorshake ]->
静态情况下,加速度为9.8 m/s²,通过Hook的方式,模拟出加速度为15+ m/s²,从而自动触发阀值,跳转模拟的广告页面