[摸鱼篇]记录一次Hook差劲的英语app拦截听力音频URL并重构播放操作
41

此app疑似完全外包开发,logcat不堪入目,程序员被自己写的逻辑跳转搞破防:

3923180683aefdb3b4c8eb765cdfda51.jpgfcf0303a8965407fcd3d1a678c84a736.jpg

虽然看起来草率,实际用起来也是苦不堪言,作文输入框的长按菜单被block了,竟是要在一个自定义键盘一个个字母手敲作文。完美的防止粘贴方案其实是使用TextWatcher,起码还能获得单词补全提高效率,也不用去敲那个shit。虽然通过无障碍服务去检索屏幕上的EditText控件并使用设置文本的办法可以免注入绕过此限制(TextWatcher也能够阻止此方法),但一般没人想到,只能去敲那个{{github第10亿个仓库名}}。

在一套CET6任务里,作文写完了,接下来就是听力了。虽然高考英语考出了130+,分全扣在了作文上,但面对一套“一镜到底”的六级音频前期还是会变得很菜......于是在力所能及的范围内,去进行优化,允许读的慢一些,最好还能往回拉进度照顾一下年迈的大脑。

核心思路:劫持音频URL,ai生成并嵌入一个自定义的view去负责交互,为了提高开发效率,选择xposed模块,分发时,使用lspatch打包。

package com.sunight.bluedovehook;

import android.content.Context;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XC_MethodReplacement;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
import java.lang.reflect.Field;
import java.util.Arrays;

public class HookInit implements IXposedHookLoadPackage {
    public String globalAudioUrl;
    private android.media.MediaPlayer[] mediaPlayer = {null};
    private android.widget.Button playButton;
    private android.widget.SeekBar seekBar;
    private android.widget.TextView currentTimeView;
    private android.widget.TextView totalTimeView;
    private android.widget.TextView titleView;
    private android.os.Handler handler;
    private android.widget.Spinner speedSpinner; // 速度选择器
    private java.util.List speedOptions = Arrays.asList(0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 2.0f);
    private float currentSpeed = 1.0f; // 当前播放速度
    private int lastProgress = 0; // 保存进度
    private android.app.Activity activity;
    private android.content.Context context;

    // 格式化时间方法
    private String formatTime(int milliseconds) {
        int seconds = milliseconds / 1000;
        int minutes = seconds / 60;
        seconds = seconds % 60;
        return String.format("%02d:%02d", minutes, seconds);
    }

    private void initializeMediaPlayer(Context context) {
        try {
            mediaPlayer[0] = new android.media.MediaPlayer();
            mediaPlayer[0].setDataSource(globalAudioUrl);
            mediaPlayer[0].setAudioStreamType(android.media.AudioManager.STREAM_MUSIC);
            mediaPlayer[0].prepareAsync();

            mediaPlayer[0].setOnPreparedListener(new android.media.MediaPlayer.OnPreparedListener() {
                    @Override
                    public void onPrepared(android.media.MediaPlayer mp) {
                        // 设置播放速度
                        setPlaybackSpeed(currentSpeed);

                        // 恢复上次进度(如果有)
                        if (lastProgress > 0) {
                            mp.seekTo(lastProgress);
                        }

                        mp.start();
                        playButton.setText("暂停");
                        totalTimeView.setText(formatTime(mp.getDuration()));
                        seekBar.setMax(mp.getDuration());
                        titleView.setText("播放中...");
                    }
                });

            mediaPlayer[0].setOnCompletionListener(new android.media.MediaPlayer.OnCompletionListener() {
                    @Override
                    public void onCompletion(android.media.MediaPlayer mp) {
                        playButton.setText("播放");
                        seekBar.setProgress(0);
                        currentTimeView.setText("00:00");
                        titleView.setText("Listening Optimization");
                        lastProgress = 0; // 重置进度
                    }
                });

        } catch (Exception e) {
            android.widget.Toast.makeText(context, "播放失败: " + e.getMessage(), android.widget.Toast.LENGTH_SHORT).show();
        }
    }

    // 设置播放速度
    private void setPlaybackSpeed(float speed) {
        if (mediaPlayer[0] != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
            try {
                android.media.PlaybackParams params = new android.media.PlaybackParams();
                params.setSpeed(speed);
                mediaPlayer[0].setPlaybackParams(params);
                currentSpeed = speed;
            } catch (Exception e) {
                XposedBridge.log("设置播放速度失败: " + e.getMessage());
            }
        }
    }

    public void resetPlayer() {
        // 保存当前进度
        if (mediaPlayer[0] != null) {
            lastProgress = mediaPlayer[0].getCurrentPosition();
            if (mediaPlayer[0].isPlaying()) {
                mediaPlayer[0].stop();
            }
            mediaPlayer[0].release();
            mediaPlayer[0] = null;
        }
        lastProgress = 0;
        playButton.setText("播放");
        seekBar.setProgress(0);
        currentTimeView.setText("00:00");
        totalTimeView.setText("00:00");
        titleView.setText("Listening Optimization");

        // 如果全局URL已更新,可以在这里开始新的播放
        if (globalAudioUrl != null && !globalAudioUrl.isEmpty()) {
            initializeMediaPlayer(context);
        }
    }

    @Override
    public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable {
        if (BuildConfig.APPLICATION_ID.equals(lpparam.packageName)) {
            XposedHelpers.findAndHookMethod(
                MainActivity.class.getName(),
                lpparam.classLoader,
                "isModuleActivated",
                XC_MethodReplacement.returnConstant(true));
        }

        XposedHelpers.findAndHookMethod("android.app.Activity", lpparam.classLoader,
            "onCreate", android.os.Bundle.class, new XC_MethodHook() {
                private android.widget.PopupWindow popupWindow;
                private java.lang.Runnable updateSeekBar;
                private int[] lastX = {500};
                private int[] lastY = {1000};

                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    activity = (android.app.Activity) param.thisObject;
                    context = activity;
                    final android.view.View rootView = activity.getWindow().getDecorView();

                    // 创建主容器
                    android.widget.LinearLayout container = createPlayerContainer(context);

                    // 创建播放器UI组件
                    createPlayerComponents(context, container);

                    // 初始化PopupWindow
                    initPopupWindow(context, container);

                    // 初始化Handler和Runnable
                    initHandler();

                    // 设置拖拽功能
                    setupDragListener(container);

                    // 显示播放器
                    showPlayer(rootView);
                }

                private android.widget.LinearLayout createPlayerContainer(Context context) {
                    android.widget.LinearLayout container = new android.widget.LinearLayout(context);
                    container.setOrientation(android.widget.LinearLayout.VERTICAL);
                    container.setPadding(dpToPx(context, 16), dpToPx(context, 16), dpToPx(context, 16), dpToPx(context, 16));

                    // 设置现代背景
                    android.graphics.drawable.GradientDrawable drawable = new android.graphics.drawable.GradientDrawable();
                    drawable.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE);
                    drawable.setCornerRadius(dpToPx(context, 16));
                    drawable.setColor(0xFF2D3748); // 深蓝色背景
                    drawable.setStroke(dpToPx(context, 1), 0xFF4A5568); // 边框
                    container.setBackground(drawable);

                    return container;
                }

                private void createPlayerComponents(Context context, android.widget.LinearLayout container) {
                    // 标题栏
                    createTitleBar(context, container);

                    // 进度条区域
                    createProgressArea(context, container);

                    // 控制按钮区域
                    createControlArea(context, container);

                    // 速度控制区域
                    createSpeedControlArea(context, container);
                }

                private void createTitleBar(Context context, android.widget.LinearLayout container) {
                    // 标题容器
                    android.widget.LinearLayout titleContainer = new android.widget.LinearLayout(context);
                    titleContainer.setOrientation(android.widget.LinearLayout.HORIZONTAL);
                    titleContainer.setGravity(android.view.Gravity.CENTER_VERTICAL);

                    // 图标
                    android.widget.TextView iconView = new android.widget.TextView(context);
                    iconView.setText("😜");
                    iconView.setTextSize(18);
                    iconView.setPadding(0, 0, dpToPx(context, 8), 0);

                    // 标题
                    titleView = new android.widget.TextView(context);
                    titleView.setText("Listening Optimization");
                    titleView.setTextSize(16);
                    titleView.setTextColor(0xFFFFFFFF);
                    titleView.setTypeface(android.graphics.Typeface.DEFAULT_BOLD);

                    titleContainer.addView(iconView);
                    titleContainer.addView(titleView);
                    container.addView(titleContainer);

                    // 分隔线
                    android.view.View divider = new android.view.View(context);
                    divider.setBackgroundColor(0xFF4A5568);
                    android.widget.LinearLayout.LayoutParams dividerParams = new android.widget.LinearLayout.LayoutParams(
                        android.view.ViewGroup.LayoutParams.MATCH_PARENT, dpToPx(context, 1)
                    );
                    dividerParams.setMargins(0, dpToPx(context, 8), 0, dpToPx(context, 12));
                    divider.setLayoutParams(dividerParams);
                    container.addView(divider);
                }

                private void createProgressArea(Context context, android.widget.LinearLayout container) {
                    // 时间显示容器
                    android.widget.LinearLayout timeContainer = new android.widget.LinearLayout(context);
                    timeContainer.setOrientation(android.widget.LinearLayout.HORIZONTAL);
                    timeContainer.setLayoutParams(new android.widget.LinearLayout.LayoutParams(
                                                      android.view.ViewGroup.LayoutParams.MATCH_PARENT, 
                                                      android.view.ViewGroup.LayoutParams.WRAP_CONTENT
                                                  ));

                    // 当前时间
                    currentTimeView = new android.widget.TextView(context);
                    currentTimeView.setText("00:00");
                    currentTimeView.setTextSize(12);
                    currentTimeView.setTextColor(0xFFA0AEC0);

                    // 时间分隔符
                    android.widget.TextView timeSeparator = new android.widget.TextView(context);
                    timeSeparator.setText(" / ");
                    timeSeparator.setTextSize(12);
                    timeSeparator.setTextColor(0xFFA0AEC0);

                    // 总时间
                    totalTimeView = new android.widget.TextView(context);
                    totalTimeView.setText("00:00");
                    totalTimeView.setTextSize(12);
                    totalTimeView.setTextColor(0xFFA0AEC0);

                    timeContainer.addView(currentTimeView);
                    timeContainer.addView(timeSeparator);
                    timeContainer.addView(totalTimeView);

                    container.addView(timeContainer);

                    // 进度条
                    seekBar = new android.widget.SeekBar(context);
                    seekBar.setMax(100);
                    seekBar.setProgress(0);
                    android.widget.LinearLayout.LayoutParams seekBarParams = new android.widget.LinearLayout.LayoutParams(
                        android.view.ViewGroup.LayoutParams.MATCH_PARENT, 
                        android.view.ViewGroup.LayoutParams.WRAP_CONTENT
                    );
                    seekBarParams.setMargins(0, dpToPx(context, 4), 0, dpToPx(context, 12));
                    seekBar.setLayoutParams(seekBarParams);

                    container.addView(seekBar);
                }

                private void createControlArea(Context context, android.widget.LinearLayout container) {
                    android.widget.LinearLayout controlContainer = new android.widget.LinearLayout(context);
                    controlContainer.setOrientation(android.widget.LinearLayout.HORIZONTAL);
                    controlContainer.setGravity(android.view.Gravity.CENTER_VERTICAL);

                    // 播放按钮
                    playButton = createModernButton(context, "播放", 0xFF48BB78); // 绿色
                    android.widget.LinearLayout.LayoutParams playButtonParams = new android.widget.LinearLayout.LayoutParams(
                        0, android.view.ViewGroup.LayoutParams.WRAP_CONTENT, 1
                    );
                    playButtonParams.setMargins(0, 0, dpToPx(context, 8), 0);
                    playButton.setLayoutParams(playButtonParams);

                    // 重置按钮
                    android.widget.Button resetButton = createModernButton(context, "重置", 0xFFED8936); // 橙色
                    android.widget.LinearLayout.LayoutParams resetButtonParams = new android.widget.LinearLayout.LayoutParams(
                        0, android.view.ViewGroup.LayoutParams.WRAP_CONTENT, 1
                    );
                    resetButton.setLayoutParams(resetButtonParams);

                    controlContainer.addView(playButton);
                    controlContainer.addView(resetButton);
                    container.addView(controlContainer);

                    // 设置按钮点击事件
                    setupButtonListeners(context, resetButton);
                }

                // 新增速度控制区域
                private void createSpeedControlArea(Context context, android.widget.LinearLayout container) {
                    android.widget.LinearLayout speedContainer = new android.widget.LinearLayout(context);
                    speedContainer.setOrientation(android.widget.LinearLayout.HORIZONTAL);
                    speedContainer.setGravity(android.view.Gravity.CENTER_VERTICAL);
                    speedContainer.setPadding(0, dpToPx(context, 8), 0, 0);

                    // 速度标签
                    android.widget.TextView speedLabel = new android.widget.TextView(context);
                    speedLabel.setText("播放速度: ");
                    speedLabel.setTextSize(12);
                    speedLabel.setTextColor(0xFFA0AEC0);
                    speedLabel.setPadding(0, 0, dpToPx(context, 8), 0);

                    // 速度选择器
                    speedSpinner = new android.widget.Spinner(context);
                    android.widget.ArrayAdapter<Float> adapter = new android.widget.ArrayAdapter<Float>(
                        context, android.R.layout.simple_spinner_item, speedOptions);
                    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
                    speedSpinner.setAdapter(adapter);
                    speedSpinner.setSelection(2); // 默认1.0x

                    // 设置速度选择监听
                    speedSpinner.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
                            @Override
                            public void onItemSelected(android.widget.AdapterView<?> parent, android.view.View view, int position, long id) {
                                float selectedSpeed = speedOptions.get(position);
                                setPlaybackSpeed(selectedSpeed);
                            }

                            @Override
                            public void onNothingSelected(android.widget.AdapterView<?> parent) {}
                        });

                    speedContainer.addView(speedLabel);
                    speedContainer.addView(speedSpinner);
                    container.addView(speedContainer);
                }

                private android.widget.Button createModernButton(Context context, String text, int color) {
                    android.widget.Button button = new android.widget.Button(context);
                    button.setText(text);
                    button.setTextColor(0xFFFFFFFF);
                    button.setAllCaps(false);
                    button.setTextSize(14);
                    button.setTypeface(android.graphics.Typeface.DEFAULT_BOLD);

                    // 设置现代背景
                    android.graphics.drawable.GradientDrawable drawable = new android.graphics.drawable.GradientDrawable();
                    drawable.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE);
                    drawable.setCornerRadius(dpToPx(context, 8));
                    drawable.setColor(color);

                    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
                        button.setElevation(dpToPx(context, 2));
                    }

                    button.setBackground(drawable);

                    // 设置点击效果
                    android.content.res.ColorStateList colorStateList = android.content.res.ColorStateList.valueOf(color);
                    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
                        button.setBackgroundTintList(colorStateList);
                    }

                    return button;
                }

                private void setupButtonListeners(final Context context, android.widget.Button resetButton) {
                    // 播放按钮点击事件
                    playButton.setOnClickListener(new android.view.View.OnClickListener() {
                            @Override
                            public void onClick(android.view.View v) {
                                handlePlayButtonClick(context);
                            }
                        });

                    // 重置按钮点击事件
                    resetButton.setOnClickListener(new android.view.View.OnClickListener() {
                            @Override
                            public void onClick(android.view.View v) {
                                resetPlayer();
                            }
                        });

                    // 进度条变化监听
                    seekBar.setOnSeekBarChangeListener(new android.widget.SeekBar.OnSeekBarChangeListener() {
                            @Override
                            public void onProgressChanged(android.widget.SeekBar seekBar, int progress, boolean fromUser) {
                                if (fromUser && mediaPlayer[0] != null) {
                                    mediaPlayer[0].seekTo(progress);
                                    currentTimeView.setText(formatTime(progress));
                                    lastProgress = progress; // 保存进度
                                }
                            }

                            @Override
                            public void onStartTrackingTouch(android.widget.SeekBar seekBar) {}

                            @Override
                            public void onStopTrackingTouch(android.widget.SeekBar seekBar) {}
                        });
                }

                private void handlePlayButtonClick(Context context) {
                    if (globalAudioUrl == null || globalAudioUrl.isEmpty()) {
                        android.widget.Toast.makeText(context, "请点击应用内音频播放按钮", android.widget.Toast.LENGTH_SHORT).show();
                        return;
                    }

                    if (mediaPlayer[0] == null) {
                        initializeMediaPlayer(context);
                    } else if (mediaPlayer[0].isPlaying()) {
                        // 暂停时保存当前进度
                        lastProgress = mediaPlayer[0].getCurrentPosition();
                        mediaPlayer[0].pause();
                        playButton.setText("播放");
                        titleView.setText("Listening Optimization");
                    } else {
                        // 继续播放时恢复进度
                        mediaPlayer[0].start();
                        playButton.setText("暂停");
                        titleView.setText("播放中...");
                    }
                }

                private void initPopupWindow(Context context, android.widget.LinearLayout container) {
                    popupWindow = new android.widget.PopupWindow(
                        container,
                        dpToPx(context, 280),
                        android.view.ViewGroup.LayoutParams.WRAP_CONTENT
                    );

                    // 关键设置:禁用所有自动关闭行为
                    popupWindow.setOutsideTouchable(false);
                    popupWindow.setFocusable(false);
                    popupWindow.setTouchable(true);
                    popupWindow.setBackgroundDrawable(null);

                    popupWindow.setAnimationStyle(0);

                    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
                        popupWindow.setElevation(dpToPx(context, 8));
                    }
                }

                private void initHandler() {
                    handler = new android.os.Handler();
                    updateSeekBar = new java.lang.Runnable() {
                        @Override
                        public void run() {
                            if (mediaPlayer[0] != null && mediaPlayer[0].isPlaying()) {
                                int currentPosition = mediaPlayer[0].getCurrentPosition();
                                seekBar.setProgress(currentPosition);
                                currentTimeView.setText(formatTime(currentPosition));
                                lastProgress = currentPosition; // 持续保存进度
                            }
                            handler.postDelayed(this, 1000);
                        }
                    };
                    handler.post(updateSeekBar);
                }

                private void setupDragListener(android.widget.LinearLayout container) {
                    android.view.View.OnTouchListener dragListener = new android.view.View.OnTouchListener() {
                        private float startRawX;
                        private float startRawY;
                        private int initialX;
                        private int initialY;

                        @Override
                        public boolean onTouch(android.view.View v, android.view.MotionEvent event) {
                            int action = event.getAction();
                            if (action == android.view.MotionEvent.ACTION_DOWN) {
                                startRawX = event.getRawX();
                                startRawY = event.getRawY();
                                initialX = lastX[0];
                                initialY = lastY[0];
                                return true;
                            } else if (action == android.view.MotionEvent.ACTION_MOVE) {
                                float deltaX = event.getRawX() - startRawX;
                                float deltaY = event.getRawY() - startRawY;
                                lastX[0] = initialX + (int) deltaX;
                                lastY[0] = initialY + (int) deltaY;
                                popupWindow.update(lastX[0], lastY[0], -1, -1);
                                return true;
                            } else if (action == android.view.MotionEvent.ACTION_UP) {
                                return true;
                            }
                            return false;
                        }
                    };

                    container.setOnTouchListener(dragListener);
                    titleView.setOnTouchListener(dragListener);
                }

                private void showPlayer(final android.view.View rootView) {
                    rootView.post(new java.lang.Runnable() {
                            @Override
                            public void run() {
                                if (popupWindow != null && !popupWindow.isShowing()) {
                                    popupWindow.showAtLocation(
                                        rootView,
                                        android.view.Gravity.TOP | android.view.Gravity.START,
                                        lastX[0],
                                        lastY[0]
                                    );
                                }
                            }
                        });
                }

                // dp转px方法
                private int dpToPx(Context context, int dp) {
                    float density = context.getResources().getDisplayMetrics().density;
                    return Math.round(dp * density);
                }
            });
		XposedHelpers.findAndHookMethod("com.lancoo.answer.widget.audioPlayView.TopicAudioView$Config", 
            lpparam.classLoader, 
            "access$700", 
            lpparam.classLoader.loadClass("com.lancoo.answer.widget.audioPlayView.TopicAudioView$Config"), 
            new XC_MethodHook() {

                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    super.beforeHookedMethod(param);

                    Object configInstance = param.args[0];
                    if (configInstance != null) {
                        XposedBridge.log("=== access$700 方法调用 ===");
                        XposedBridge.log("Config实例: " + configInstance.toString());

                        // 获取所有字段
                        Field[] fields = configInstance.getClass().getDeclaredFields();
                        XposedBridge.log("字段总数: " + fields.length);

                        // 遍历所有字段并打印String类型的值
                        for (Field field : fields) {
                            field.setAccessible(true);
                            try {
                                Object value = field.get(configInstance);
                                if (value instanceof String) {
                                    XposedBridge.log("字段: " + field.getName() + " = " + value);
                                } else if (value != null) {
                                    XposedBridge.log("字段: " + field.getName() + " = " + value + " (类型: " + value.getClass().getSimpleName() + ")");
                                }
                                if (field.getName().equals("audioUrl")) {
                                    globalAudioUrl = (String)value;
                                   
                                }
                            
                            } catch (Exception e) {
                                XposedBridge.log("无法读取字段: " + field.getName());
                            }
                        }
                    }
                }
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    super.afterHookedMethod(param);

                    XposedBridge.log("access$700 返回值: " + param.getResult());
                }
            });
        XposedHelpers.findAndHookMethod("com.lancoo.answer.widget.audioPlayView.TopicAudioView", lpparam. classLoader, "play", new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    super.beforeHookedMethod(param);
                    android.widget.Toast.makeText(context, "successfully hijacked✓", android.widget.Toast.LENGTH_SHORT).show();
                }
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    super.afterHookedMethod(param);
                    // 获取被hook的对象实例
                    Object topicAudioView = param.thisObject;
                    XposedHelpers.callMethod(topicAudioView, "pause");
                    resetPlayer();
                }
            });
	}
}

代码基本ai生成,费时间的是方案设计与逆向的过程、定位logcat,activity记录等找出要hook的参数。这么难用的APP必然是没什么混淆加密的,因此整个过程十分顺利没有压力,最后的效果也非常不错,成功将听力音频劫持到自己的播放器上。

Screenshot_2025-10-09-00-31-21-63_de76c92ef1a6a18.jpg

[摸鱼篇]记录一次Hook差劲的英语app拦截听力音频URL并重构播放操作
https://sunight.cn/archives/B6JNnD6G
作者
Sunight
发布于
更新于
许可