微信插件之语音播报

微信插件之语音播报(此项目仅供逆向学习使用)

本意是为盲人群体做的一款微信语音辅助的小项目,虽然最终没有被启用,但其中涉及到的反编译思想(逆向思维)以及进程间通讯的模块还是对后续项目开发有一定裨益的。

  • 对于Android逆向项目,本项目目前使用的技术是Xposed框架,但是Xposed目前所知是需要手机Root后才可使用。

  • Xposed设计思想是借用JAVA的反射机制来实现的,Hook所需模块来进行修改,从而达到自身需求。像微信机器人、滴滴出行之类的自动抢单等的插件。

  • 在AndroidStudio的gradle中配置如图:

    image
  • 需要手机先装Xposed插件,针对不同机型,有对应模块的安装

  • 项目插件中需要在assets资源目录下创建命名为xposed_init的文件,里面声明好自身插件入口启动类,如图:


    image
  • 既然要做基于微信消息文本播报的语音插件,那么就要即时获取目标APP的即时数据内容,逆向项目最耗时最费力的模块是定位所需的代码模块加调试,因此不像正常需求开发,会有明确技术耗时与定期。定位代码并Hook无误执行一般来说是有些难定位且很耗时的。

    • 若解决微信的聊天文本信息获取的方案,目前可行有两种:
      • 1,通过目标APP的数据库用SQL语句进行数据的即时查找
      • 2,通过Hook微信在通讯时的调用消息的API(不过这块肯定是相对耗时的。所以,此文本播报项目我采用是第一种方案,毕竟时间成本太高的话,导致做出东西也相对意义上大打折扣。)
    • 对于微信的数据库解密方案,本文不做介绍。

思路与实现

  • 首先在Xposed项目初始化时,实时检测微信进程,从而Hook住微信并在其运行时做对应的Hook处理。

    • CallingTheDog为本插件的入口类,用来初始化检测微信的主进程,以及微信的APP主UI(LauncherUI)的启动监听、数据库Cursor游标对象的获取。
    •   public class CallingTheDog implements IXposedHookLoadPackage {
      
         //Specify the currently required version.
         public static String currentVersion = WE_CHAT_FLAG.VERSION_6_5_4;
         private WeChatLauncherUI weChatLauncherUI;
         private WeChatDBHelper weChatDBHelper;
      
         @Override
        public void handleLoadPackage(XC_LoadPackage.LoadPackageParam LPParam) throws Throwable {
      
         if (APP_PACKAGE_NAME.WE_CHAT.equals(LPParam.packageName)) {
             if (weChatLauncherUI == null) {
                  LoggerUtils.xd("We Chat init.");
                  weChatLauncherUI = new WeChatLauncherUI(LPParam);
             }
      
             toHookWeChatAttach(LPParam);
         }
      }
      
         private void toHookWeChatAttach(final XC_LoadPackage.LoadPackageParam lpParam) {
         findAndHookMethod(Application.class, "attach", Context.class,
                    new XC_MethodHook() {
                       @Override
                      protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                         super.afterHookedMethod(param);
                         if (weChatDBHelper == null) {
                             LoggerUtils.xd("WeChatDBHelper init.");
                             weChatDBHelper = new WeChatDBHelper();
                             weChatDBHelper.init(lpParam);
                          }
                     }
                  });
             }
         }
      
  • 微信的Cursor数据库游标对象获取代码如下

    •   public class WeChatDBHelper {
      
         public static Method method = null;
         public static Object receiver = null;
      
         /**
         * 微信Cursor读写初始化。
         */
         public void init(final XC_LoadPackage.LoadPackageParam    loadPackageParam){
             String targetSqlClass = "";
            if(WE_CHAT_FLAG.VERSION_6_5_4.equals(CallingTheDog.currentVersion)){
                targetSqlClass = "com.tencent.mm.bg.g";
         }else if(WE_CHAT_FLAG.VERSION_7_0_4.equals(CallingTheDog.currentVersion)){
             targetSqlClass = "com.tencent.mm.bb.g";
          }
          String targetSqlMethod = "rawQuery";
         findAndHookMethod(targetSqlClass, loadPackageParam.classLoader, targetSqlMethod,
             String.class, String[].class, new XC_MethodHook() {
                   @Override
                   protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                        super.beforeHookedMethod(param);
                        //hook数据库连接对象,用于发起数据主动查询
                       if(method == null){
                          method = (Method) param.method;
                          receiver = param.thisObject;
                     }
                }
      
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    super.afterHookedMethod(param);
                    //监听所有的消息查询语句
                    Cursor result = (Cursor) param.getResult();
                    String sqlStr = String.valueOf(param.args[0]);
                    if (result != null && result.getCount()>0 && sqlStr.startsWith("select * from message")) {
                        MsgListeners.listenerPath(result, loadPackageParam);
                    }
                }
            });
        }
        }
      
  • 借用游标对象获取文本信息的监听管理类,根据监听数据库插入的新消息数据来进行即时播报处理

    • public class MsgListeners {
      
        private static Socket mClientSocket;
        private static PrintWriter mClientPrintWriter;
      
        public static void listenerPath(Cursor cursor, XC_LoadPackage.LoadPackageParam loadPackageParam) {
        //主动发送的status=2,接收的为3
          int status = WeChatMessage.getStatus(cursor);
          WeChatMessage.Type msgType = WeChatMessage.getType(cursor);
      
          //建立Client端
          if (mClientPrintWriter == null) {
                new Thread() {
                   @Override
                   public void run() {
                    connectTCPServer();
                }
            }.start();
         } else {
          LoggerUtils.xd("mClientPrintWriter is " + mClientPrintWriter);
        }
         switch (msgType) {
          case TEXT_MESSAGE:
             List<TextMessage> textMessages = WeChatMessage.getTextMessage(cursor);
      
                //初始化监测音量键提供者。
              VolumeProvider volumeProvider = new VolumeProvider(loadPackageParam);
      
                for (TextMessage textMessage : textMessages) {
                 if (textMessage == null) {
                    continue;
                }
                if (textMessage.getCreateTime() > WeChatMessage.lastSend) {
                    WeChatMessage.lastSend = textMessage.getCreateTime();
                    //处理转发的消息
                }
      
                SystemClock.sleep(1000);
                String textContent = QueryWeChatDB.getNickname(textMessage.getFromUser())
                        + " 来消息:  " + textMessage.getContent();
                LoggerUtils.xd("GenialSir Msg textContent " + textContent);
      
                if (!TextUtils.isEmpty(textContent) && mClientPrintWriter != null) {
                    mClientPrintWriter.println(textContent);
                    LoggerUtils.xd("GenialSir mClientPrintWriter textContent " + textContent);
                } else {
                    LoggerUtils.xd("mClientPrintWriter is null.. ");
                    new Thread() {
                        @Override
                        public void run() {
                            connectTCPServer();
                        }
                    }.start();
                }
            }
            break;
        case VIDEO_MESSAGE:
            break;
        case IMG_MESSAGE:
            break;
        default:
            break;
        }
        }
      }
      
  • 再进行语音播报模块, ** 注意,如果是Android系统>=21(5.0),则直接使用原生API的TextToSpeech即可实现语音播报,若Android系统<21(5.0),则TextToSpeech不支持中文。 **

    • 如果需要支持中文,那首先可以想到使用三方语音API,如讯飞、百度语音等都可以实现,我在初次使用过程中遇到一些意料之外的问题:
      • 语音API的初始化问题,APP的key签名注册问题。显然,这块直接使用微信的Context注册是有问题的。
      • 如果不依赖微信的Context,可以使用自身插件的Context进行一个三方语音注册,我的 TextVoiceHelper 使用的是讯飞语音。但在使用自身插件的Context时,后面又遇到因进程间通讯而导致语音无法播报的问题。
      • 在使用ALDL过程中,直接支持的数据类型如下:(本项目采用的是Socket)
        • 基本数据类型(int、long、char、boolean、double等);
        • String和CharSequence;
        • List:只支持ArrayList,里面每个元素都必须被AIDL支持;
        • Map:只支持HashMap,里面每个元素都必须被AIDL支持,包括key和value;
        • Parcelable:所有实现了Parceable接口的对象;
        • AIDL:所有的AIDL接口本身也可以在AIDL文件中使用(AIDL接口中只支持方法,不支持声明静态常量,这一点区别于传统接口)。
    • 若采用将插件的Context传递给微信进程中来进行初始化讯飞语音API的操作,这个思路整体感觉很矛盾且不清晰,并且Context在多进程情况下也是问题很多。
      • 那么就不用在跨进程传输Context上下功夫,而是直接将聊天数据跨进程传输,从而借助Socket来进行跨进程传输通信,只需在TextVoiceHelper进程内注册的讯飞直接播报即可。也避免了使用微信Context初始化自己注册讯飞的尴尬与TextVoiceHelper在注册讯飞后,获取微信聊天数据遇到进程通讯的问题。
      • 按照上述思路,将微信文本数据监听这块视为Socket的发送端,自身插件注册服务视为Socket接受端,那么整体语音播报处理流程是不是更清晰简洁了呢?
  • 通过将微信与插件分为客户发送端与服务接受端,得到如下的Socket客户端代码:

    •   private static void connectTCPServer() {
      
         Socket socket = null;
         while (socket == null) {
            try {
              socket = new Socket("localhost", 8688);
              mClientSocket = socket;
              mClientPrintWriter = new PrintWriter(new BufferedWriter(
                     new OutputStreamWriter(socket.getOutputStream())), true);
      
             LoggerUtils.xd("genial sir connect tcp server success.");
          } catch (IOException e) {
              e.printStackTrace();
              SystemClock.sleep(1000);
              LoggerUtils.xd("genial sir connect tcp server failed, retry...");
         }
         }
      
        try {
         //接受服务器端的消息
         BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
         while (true) {
             LoggerUtils.xd("genial sir receive br : " + br);
             String msg = br.readLine();
             LoggerUtils.xd("genial sir receive : " + msg);
             if (msg.contains("Close Socket service")) {
                    break;
                }
         }
         LoggerUtils.xd("genial sir quit...");
         CloseUtils.closeQuietly(mClientPrintWriter);
         CloseUtils.closeQuietly(br);
         socket.close();
      } catch (IOException e) {
         e.printStackTrace();
         }
         }
      
  • Socket服务端初始化与实现:

    •   private void initSocket() {
        //启动Socket服务类
        Intent serviceIntent = new Intent(MainActivity.this, VoiceSocketManager.class);
        startService(serviceIntent);
        }
      
    •   public class VoiceSocketManager extends Service {
      
        private TTSUtils ttsUtils;
        private boolean mIsServiceDestroyed = false;
      
        @Override
        public void onCreate() {
            super.onCreate();
            new Thread(new TcpServer()).start();
        }
      
        @Nullable
        @Override
        public IBinder onBind(Intent intent) {
            return null;
        }
      
      
        private class TcpServer implements Runnable {
      
            @Override
            public void run() {
                ServerSocket serverSocket;
                try {
                    //监听本地8688端口
                 serverSocket = new ServerSocket(8688);
                } catch (IOException e) {
                    LoggerUtils.d("establish tcp server failed, port:8688");
                    e.printStackTrace();
                    return;
                }
                while (!mIsServiceDestroyed) {
                    try {
                        //接受客户端请求
                        final Socket client = serverSocket.accept();
                        new Thread() {
                            @Override
                            public void run() {
                                try {
                                    responseClient(client);
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            }
                        }.start();
                    } catch (Exception e) {
                        LoggerUtils.d("error " + e.toString());
                        e.printStackTrace();
                    }
                }
            }
         }
      
        private void responseClient(Socket client) throws IOException {
            Context applicationContext =         getApplication().getApplicationContext();
            initXF(applicationContext);
            //用于接受客户端消息
            BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
            //用于向客户端发送消息
            PrintWriter printWriter = new PrintWriter(new BufferedWriter(
                    new OutputStreamWriter(client.getOutputStream())), true);
      
      
            String clientContent;
            while (true) {
                clientContent = in.readLine();
                LoggerUtils.d("responseClient msg from client: " + clientContent);
                if ("Close socket".equals(clientContent)) {
                    //客户端断开链接
                    if (ttsUtils != null) {
                        ttsUtils.speak("我是Voice Socket Manager, 客户端请求断开链接,撒哟啦啦");
            }
                    LoggerUtils.d("客户端断开链接.");
                    break;
                }
                if (ttsUtils != null) {
                    ttsUtils.speak(clientContent);
                }else {
                    LoggerUtils.e("ttsUtils is null.");
                }
            }
            LoggerUtils.d("client quit.");
            //关闭流
            CloseUtils.closeQuietly(printWriter);
            CloseUtils.closeQuietly(in);
            client.close();
        }
      
        private void initXF(Context context) {
            SpeechUtility.createUtility(context, "appid=5d07631c");
            Setting.setShowLog(true);
            ttsUtils = TTSUtils.getInstance(context);
            ttsUtils.init();
        }
      
        @Override
        public void onDestroy() {
            mIsServiceDestroyed = true;
            super.onDestroy();
        }
        }
      
  • 项目TextVoiceHelperGithub地址

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345