├── .gitignore ├── .idea ├── compiler.xml ├── encodings.xml ├── jarRepositories.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── ORGIN_README.md ├── README.md ├── Wechat8Spellbook ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── maven.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ ├── kotlin │ │ └── com │ │ │ └── gh0u1l5 │ │ │ └── wechatmagician │ │ │ └── spellbook │ │ │ └── MirrorUnitTest.kt │ └── resources │ │ └── apks │ │ ├── rename.sh │ │ └── simplify.sh │ ├── main │ ├── AndroidManifest.xml │ └── kotlin │ │ └── com │ │ └── gh0u1l5 │ │ └── wechatmagician │ │ └── spellbook │ │ ├── Alias.kt │ │ ├── C.kt │ │ ├── SpellBook.kt │ │ ├── WechatGlobal.kt │ │ ├── WechatStatus.kt │ │ ├── base │ │ ├── Classes.kt │ │ ├── EventCenter.kt │ │ ├── Hooker.kt │ │ ├── HookerProvider.kt │ │ ├── Operation.kt │ │ ├── Version.kt │ │ └── WaitChannel.kt │ │ ├── hookers │ │ ├── Activities.kt │ │ ├── Adapters.kt │ │ ├── Database.kt │ │ ├── FileSystem.kt │ │ ├── ListViewHider.kt │ │ ├── MenuAppender.kt │ │ ├── Notifications.kt │ │ ├── SearchBar.kt │ │ ├── Storage.kt │ │ ├── UriRouter.kt │ │ ├── XLog.kt │ │ └── XmlParser.kt │ │ ├── interfaces │ │ ├── IActivityHook.kt │ │ ├── IAdapterHook.kt │ │ ├── IDatabaseHook.kt │ │ ├── IFileSystemHook.kt │ │ ├── IImageStorageHook.kt │ │ ├── IMessageStorageHook.kt │ │ ├── INotificationHook.kt │ │ ├── IPopupMenuHook.kt │ │ ├── ISearchBarConsole.kt │ │ ├── IUriRouterHook.kt │ │ ├── IXLogHook.kt │ │ └── IXmlParserHook.kt │ │ ├── mirror │ │ ├── MirrorClasses.kt │ │ ├── MirrorFields.kt │ │ ├── MirrorMethods.kt │ │ ├── android │ │ │ └── support │ │ │ │ └── v4 │ │ │ │ └── app │ │ │ │ └── Classes.kt │ │ └── com │ │ │ └── tencent │ │ │ ├── mars │ │ │ └── xlog │ │ │ │ ├── Classes.kt │ │ │ │ ├── Constants.kt │ │ │ │ └── Methods.kt │ │ │ ├── mm │ │ │ ├── Classes.kt │ │ │ ├── Fields.kt │ │ │ ├── Methods.kt │ │ │ ├── booter │ │ │ │ └── notification │ │ │ │ │ ├── Classes.kt │ │ │ │ │ └── queue │ │ │ │ │ ├── Classes.kt │ │ │ │ │ └── Methods.kt │ │ │ ├── modelsfs │ │ │ │ ├── Classes.kt │ │ │ │ └── Methods.kt │ │ │ ├── plugin │ │ │ │ ├── base │ │ │ │ │ └── stub │ │ │ │ │ │ ├── Classes.kt │ │ │ │ │ │ └── Methods.kt │ │ │ │ ├── gallery │ │ │ │ │ └── ui │ │ │ │ │ │ └── Classes.kt │ │ │ │ ├── remittance │ │ │ │ │ └── ui │ │ │ │ │ │ └── Classes.kt │ │ │ │ ├── sns │ │ │ │ │ └── ui │ │ │ │ │ │ ├── Classes.kt │ │ │ │ │ │ └── Fields.kt │ │ │ │ └── webwx │ │ │ │ │ └── ui │ │ │ │ │ └── Classes.kt │ │ │ ├── sdk │ │ │ │ └── platformtools │ │ │ │ │ ├── Classes.kt │ │ │ │ │ └── Methods.kt │ │ │ ├── storage │ │ │ │ ├── Classes.kt │ │ │ │ └── Methods.kt │ │ │ └── ui │ │ │ │ ├── Classes.kt │ │ │ │ ├── Methods.kt │ │ │ │ ├── base │ │ │ │ └── Classes.kt │ │ │ │ ├── chatting │ │ │ │ └── Classes.kt │ │ │ │ ├── contact │ │ │ │ └── Classes.kt │ │ │ │ ├── conversation │ │ │ │ └── Classes.kt │ │ │ │ ├── tools │ │ │ │ └── Classes.kt │ │ │ │ └── transmit │ │ │ │ ├── Classes.kt │ │ │ │ └── Methods.kt │ │ │ └── wcdb │ │ │ ├── Classes.kt │ │ │ ├── Package.kt │ │ │ ├── database │ │ │ └── Classes.kt │ │ │ └── support │ │ │ └── Classes.kt │ │ ├── parser │ │ ├── ApkFile.kt │ │ ├── ClassTrie.kt │ │ ├── DexHeader.kt │ │ └── DexParser.kt │ │ └── util │ │ ├── BasicUtil.kt │ │ ├── FileUtil.kt │ │ ├── MirrorUtil.kt │ │ ├── ParallelUtil.kt │ │ ├── ReflectionUtil.kt │ │ └── XposedUtil.kt │ └── test │ └── kotlin │ └── com │ └── gh0u1l5 │ └── wechatmagician │ └── spellbook │ ├── base │ └── VersionUnitTest.kt │ └── mirror │ └── ReflectionUnitTest.kt ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── src │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── huruwo │ │ │ └── app_xposed │ │ │ └── ExampleInstrumentedTest.java │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── assets │ │ │ └── xposed_init │ │ ├── java │ │ │ └── com │ │ │ │ └── huruwo │ │ │ │ └── hposed │ │ │ │ ├── HookLoader.java │ │ │ │ ├── MainHookLoader.java │ │ │ │ ├── action │ │ │ │ ├── Alert.java │ │ │ │ └── Message.java │ │ │ │ ├── net │ │ │ │ ├── ApiClient.java │ │ │ │ ├── AppApiService.java │ │ │ │ ├── AppDataRepository.java │ │ │ │ ├── ScalarRequestBodyConverter.java │ │ │ │ ├── ScalarResponseBodyConverters.java │ │ │ │ ├── ScalarsConverterFactory.java │ │ │ │ └── SubscriberManager.java │ │ │ │ ├── ui │ │ │ │ └── MainActivity.java │ │ │ │ └── utils │ │ │ │ ├── Constants.java │ │ │ │ ├── GsonUtils.java │ │ │ │ ├── LogXUtils.java │ │ │ │ └── XSharePrefeUtil.java │ │ └── res │ │ │ ├── layout │ │ │ └── activity_main.xml │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ └── test │ │ └── java │ │ └── com │ │ └── huruwo │ │ └── app_xposed │ │ └── ExampleUnitTest.java └── xx.jks ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── release └── output.json ├── settings.gradle └── xxx.jks /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### Android template 3 | # Built application files 4 | *.apk 5 | *.ap_ 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | 28 | # Log Files 29 | *.log 30 | 31 | # Android Studio Navigation editor temp files 32 | .navigation/ 33 | 34 | # Android Studio captures folder 35 | captures/ 36 | 37 | # IntelliJ 38 | *.iml 39 | .idea/workspace.xml 40 | .idea/tasks.xml 41 | .idea/gradle.xml 42 | .idea/assetWizardSettings.xml 43 | .idea/dictionaries 44 | .idea/libraries 45 | .idea/caches 46 | 47 | # Keystore files 48 | # Uncomment the following line if you do not want to check your keystore files in. 49 | #*.jks 50 | 51 | # External native build folder generated in Android Studio 2.2 and later 52 | .externalNativeBuild 53 | 54 | # Google Services (e.g. APIs or Firebase) 55 | google-services.json 56 | 57 | # Freeline 58 | freeline.py 59 | freeline/ 60 | freeline_project_description.json 61 | 62 | # fastlane 63 | fastlane/report.xml 64 | fastlane/Preview.html 65 | fastlane/screenshots 66 | fastlane/test_output 67 | fastlane/readme.md 68 | 69 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ORGIN_README.md: -------------------------------------------------------------------------------- 1 | # Wechat Spellbook 2 | 3 | [![Build Status](https://travis-ci.org/Gh0u1L5/WechatSpellbook.svg?branch=master)](https://travis-ci.org/Gh0u1L5/WechatSpellbook) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.gh0u1l5/wechat-spellbook/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.gh0u1l5/wechat-spellbook) 4 | 5 | --- 6 | 7 | ### 2018.11.24 状态更新 8 | 9 | 最近半年实在是太忙了,一直没有更新项目。 10 | 11 | 最近先是花了一天多时间,解决了一下拖了很久的性能问题。通过重写APK Parser、启用新的数据结构和并行计算模块,把性能提升了10倍不止,详情可以参见这两个commit:[5bf7804](../../commit/5bf7804664606dd6280d5a0dc6e33f3a9ffbb5a6) & [388e25f](../../commit/388e25f904e73633f6639ae1b9e1aa1d959607cb) 12 | 13 | 然后,就是把项目中的注释几乎全盘改成了中文,方便以后的交流。剩下还没有改完的部分诚求热心人帮一下忙,翻译起来实在是太累了。 14 | 15 | 接下来会找机会把目前提交的issue看一下,把6.6.x及以上的微信版本都适配一下,不过这可能要等到12月底了吧。 16 | 17 | --- 18 | 19 | Wechat Spellbook 是一个使用Kotlin编写的开源微信插件框架,底层需要 [Xposed](https://forum.xda-developers.com/xposed) 或 [VirtualXposed](https://github.com/android-hacker/VirtualXposed) 等Hooking框架的支持,而顶层可以轻松对接Java、Kotlin、Scala等JVM系语言。让程序员能够在几分钟内编写出简单的微信插件,随意揉捏微信的内部逻辑。 20 | 21 | 另外,在编写项目文档的过程中,也会找机会向大家分享一些逆向微信的经验和适配不同操作系统踩到的坑,也欢迎大家把自己的经验分享上来自由讨论。 22 | 23 | ## 便利特色 24 | 25 | * __精心设计各项机制,合理运用多线程和惰性求值等技巧,用不到的功能永远不会影响你的性能。__ 26 | * 使用一套API __自动分析__ 微信内部结构特征,__避免手工适配__ 每个微信版本不同的类名、方法名。 27 | - 每次微信更新的时候,都会使用写好的单元测试自动验证是否有特征失效。 28 | - 精心设计的框架保证了开发者可以轻松拓展添加自己需要的特征。 29 | * 框架内部设计了 _EventCenter_ 和 _HookerProvider_ 两类不同的事件处理方式。 30 | - _EventCenter_ 让开发者直接使用设计好的事件消息来截获微信数据,保证 __便利性__ 。 31 | - _HookerProvider_ 允许熟悉Xposed的开发者调用Xposed接口进行自由发挥,保证 __自由度__ 。 32 | * 正确使用 _EventCenter_ 方案,有助于回避Xposed的一些小问题,如 33 | - 函数调用被前一个劫持者打断导致的插件相互冲突。 34 | - Xposed自Android 7.0后偶发的,由于多线程导致ART崩溃的问题。 35 | 36 | ## 衍生项目 37 | * [WechatMagician](https://github.com/Gh0u1L5/WechatMagician) 38 | * [WechatBotXposed](https://github.com/Blankeer/WechatBotXposed) 39 | 40 | ## 开发文档 41 | * [简介](https://github.com/Gh0u1L5/WechatSpellbook/wiki/Home) 42 | * [快速上手](https://github.com/Gh0u1L5/WechatSpellbook/wiki/快速上手) 43 | * 开发教程 44 | - [事件机制](https://github.com/Gh0u1L5/WechatSpellbook/wiki/事件机制) 45 | - 反射集合 46 | - 混淆与自动适配 47 | * 逆向技巧 48 | - 常见逆向工具 49 | - 调试输出 50 | - 堆栈跟踪 51 | - Support库 52 | - 免重启调试插件 53 | 54 | 55 | ## 关于VirtualXposed 56 | 57 | 目前对于VirtualXposed的支持还算不上完善,因为VirtualXposed的环境和原生Xposed的环境实在是差了太多,我和weishu折腾了很久才算是在部分设备上解决了黑屏卡死的问题。现在我们俩发布代码的日子里,一个拜三清一个拜关公,希望能够帮Bug们早日往生。 58 | 59 | ## 写给开发者 60 | 61 | 在出于兴趣接触微信逆向的短短一年里,我接触到了形形色色的开发者、投资者、支持者。被微信本身复杂成熟的架构深深吸引的同时,也惊异于微信衍生出来的灰色产业之庞大、第三方微信竞争之激烈。 62 | 有不少朋友劝我闭源、商业化,而且拿出了很多细致的想法,我很感激他们的帮助,但是最后还是决定在开源的方向上再次迈出了一大步,原因有二。 63 | 64 | 其一,我不想停在这里。商业化必然牵扯大量的时间精力和权益纠葛。我不想在自己正处在上升期的时候,就草率地把大量的时间精力都花在一个刚满20岁的时候意外做出来的小成就上,然后吃上十几年的老本。而且这个项目,归根结底是一个寄生在微信上的项目。这种格局,跟我每天在实习岗位上接触到的、跟大学里的同学谈论到的,都根本不在一个层面上。我虽然知道自己的才华有限,但是只要有机会的话,我还是想看看更高处的风景。 65 | 66 | 其二,我太过理想主义。中国互联网在很早的时候就已经是一个相当商业化的世界了。我并不反对商业化,也不觉得商业化有任何的道德问题。人,总归是要吃饭的。像fkzhang这样能靠自己的才华吃饭,既不偷也不抢,这有什么错呢?但是我心里总归是感觉到失落的,因为我最早爱上的,是一个自由的互联网,是一个为每个有才华有求知欲的年轻人准备的儿童乐园。只要思想用0和1表达出来,就再也没有什么规则能够阻挡他们,他们就是儿童乐园之王。我希望当我国的年轻一代对计算机产生好奇时,也能像西方国家的孩子们一样,能够轻松地、无语言障碍地,接触到大量好玩有趣的个人开源项目,进而爱上这个0与1的世界。如果有年轻的初中、高中的学弟学妹,能够用我的项目把微信像橡皮泥一样随意揉捏,像当年的我一样深深地享受到这个世界的乐趣,那么对我实在是一种莫大的鼓励与快乐。 67 | 68 | 当然,对于诸位想要基于我的项目做商业项目的开发者,也请尽管拿去。我不喜欢道德绑架别人,也不认为这有什么绑架的必要。但是如果你在闲暇时间能够贡献十几行代码、修复些你发现的Bug,那就已经十分感激。 69 | 70 | ## 打赏二维码 71 | 72 | 应 [Issue #5](https://github.com/Gh0u1L5/WechatSpellbook/issues/5) 的请求,贴上打赏二维码,让我们回归平和的技术讨论。 73 | 74 | 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 新增基于WeChat8Xposed的微信机器人示例 3 | 4 | 提供基本的功能: 5 | 6 | 1.微信群消息自动接收 7 | 8 | 2.微信群消息自动回复 9 | 10 | 3.关键词触发以及后台管理 11 | 12 | 13 | ------------------------- 14 | 15 | # 项目说明 16 | 17 | 本项目拷贝自 [https://github.com/Gh0u1L5/WechatSpellbook](https://github.com/Gh0u1L5/WechatSpellbook) 18 | 19 | 个人尝试做新版微信的适配计划 20 | 21 | 因为原来的项目因为已经停更了很久 在新版的微信上会有些问题 22 | 本人针对新版的微信 尤其是8版本 做了全新的适配 使其在新版的微信上也可以使用 23 | 24 | 25 | # 基于Wechat8Spellbook的聊天机器人项目 26 | 27 | [WeChatSimpleBot](https://github.com/HuRuWo/WeChatSimpleBot) 28 | 29 | - 目前代码还没有上传 因为包含了私人信息,需要处理 30 | 31 | ## 为了方便大家理解 本人做了一些相关的笔记 如何依赖 Wechat8Spellbook 做插件开发 32 | 33 | [1.WeChat8Xposed通用hook框架适配新版微信-单元测试适配](https://www.huruwo.top/wechat8xposed%e9%80%9a%e7%94%a8hook%e6%a1%86%e6%9e%b6%e9%80%82%e9%85%8d%e6%96%b0%e7%89%88%e5%be%ae%e4%bf%a1-%e5%8d%95%e5%85%83%e6%b5%8b%e8%af%95%e9%80%82%e9%85%8d%e6%96%b0%e5%be%ae%e4%bf%a1/) 34 | 35 | [2.WeChat8Xposed通用hook框架适配新版微信-修复NotificationManagerCompat](https://www.huruwo.top/wechat8xposed%e9%80%9a%e7%94%a8hook%e6%a1%86%e6%9e%b6%e9%80%82%e9%85%8d%e6%96%b0%e7%89%88%e5%be%ae%e4%bf%a1-%e4%bf%ae%e5%a4%8dnotificationmanagercompat%e9%80%82%e9%85%8d%e5%bc%82%e5%b8%b8/) 36 | 37 | 38 | ## 更新说明 39 | 40 | |更新时间| 更新内容|更新说明| 41 | |----|----| ----| 42 | | 更新计划 | 更加详细的开发文档编写 || 43 | |更新计划|com.tencent.mm modelsfs 异常修复|| 44 | | 2021-8-9 | com.tencent.mm notification异常修复 |notification和消息通知相关的兼容类| 45 | |2021-6-23 | 验证修复wcdb包下的类是否路径异常做修复调整|| 46 | |2021-6-20 | 修正部分类的特征值 包括有 NotificationManagerCompat 等类的视频 即v4兼容包切换到 androidx 兼容包|| 47 | |2021-6-19 |测试代码测试最新版微信的适配程度|| 48 | |2021-6-18 |项目结构调整|| 49 | 50 | 51 | ## 使用说明 52 | 53 | Wechat Spellbook 是一个使用Kotlin编写的开源微信插件框架 54 | 55 | 详情可以原来项目的README说明[ORGIN_README.md](ORGIN_README.md) 56 | 57 | ## 其他说明 58 | 59 | 个人认为这套框架非常有意思,但是作者停更了非常可惜。 60 | 两个有意思的地方在于: 61 | 62 | 1.提供一套合理的框架给第三方开发者 简化开发步骤 63 | 64 | 2.通过apk的dex分析做出一套忽略版本的差异的 并且可以添加新的特征规则 65 | 66 | ## 鸣谢 67 | 68 | [Gh0u1L5](https://github.com/Gh0u1L5) 69 | 提供的项目 70 | [https://github.com/Gh0u1L5/WechatSpellbook](https://github.com/Gh0u1L5/WechatSpellbook) 支持 71 | 72 | -------------------------------------------------------------------------------- /Wechat8Spellbook/.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # Intellij 36 | *.iml 37 | .idea/workspace.xml 38 | .idea/tasks.xml 39 | .idea/gradle.xml 40 | .idea/dictionaries 41 | .idea/libraries 42 | 43 | # Keystore files 44 | *.jks 45 | 46 | # External native build folder generated in Android Studio 2.2 and later 47 | .externalNativeBuild 48 | 49 | # Google Services (e.g. APIs or Firebase) 50 | google-services.json 51 | 52 | # Freeline 53 | freeline.py 54 | freeline/ 55 | freeline_project_description.json 56 | -------------------------------------------------------------------------------- /Wechat8Spellbook/.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | jdk: oraclejdk8 3 | 4 | env: 5 | global: 6 | - GRADLE_VERSION=4.10 7 | - BUILD_API=android-28 8 | - BUILD_TOOLS=build-tools-28.0.3 9 | 10 | android: 11 | components: 12 | - tools 13 | - platform-tools 14 | # The SDK version used to compile your project 15 | - $BUILD_API 16 | # The BuildTools version used by your project 17 | - $BUILD_TOOLS 18 | 19 | before_cache: 20 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 21 | - rm -rf $HOME/.gradle/caches/*/plugin-resolution/ 22 | 23 | cache: 24 | directories: 25 | - $HOME/.gradle/caches/ 26 | - $HOME/.gradle/wrapper/ 27 | - $HOME/.android/build-cache 28 | 29 | before_install: 30 | # Setup the correct gradle version. 31 | - wget http://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip 32 | - unzip gradle-${GRADLE_VERSION}-bin.zip 33 | - export GRADLE_HOME=$PWD/gradle-${GRADLE_VERSION} 34 | - export PATH=$GRADLE_HOME/bin:$PATH 35 | 36 | script: 37 | - gradle build check 38 | -------------------------------------------------------------------------------- /Wechat8Spellbook/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Emerson Lin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Wechat8Spellbook/README.md: -------------------------------------------------------------------------------- 1 | # Wechat Spellbook 2 | 3 | [![Build Status](https://travis-ci.org/Gh0u1L5/WechatSpellbook.svg?branch=master)](https://travis-ci.org/Gh0u1L5/WechatSpellbook) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.gh0u1l5/wechat-spellbook/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.gh0u1l5/wechat-spellbook) 4 | 5 | --- 6 | 7 | ### 2018.11.24 状态更新 8 | 9 | 最近半年实在是太忙了,一直没有更新项目。 10 | 11 | 最近先是花了一天多时间,解决了一下拖了很久的性能问题。通过重写APK Parser、启用新的数据结构和并行计算模块,把性能提升了10倍不止,详情可以参见这两个commit:[5bf7804](../../commit/5bf7804664606dd6280d5a0dc6e33f3a9ffbb5a6) & [388e25f](../../commit/388e25f904e73633f6639ae1b9e1aa1d959607cb) 12 | 13 | 然后,就是把项目中的注释几乎全盘改成了中文,方便以后的交流。剩下还没有改完的部分诚求热心人帮一下忙,翻译起来实在是太累了。 14 | 15 | 接下来会找机会把目前提交的issue看一下,把6.6.x及以上的微信版本都适配一下,不过这可能要等到12月底了吧。 16 | 17 | --- 18 | 19 | Wechat Spellbook 是一个使用Kotlin编写的开源微信插件框架,底层需要 [Xposed](https://forum.xda-developers.com/xposed) 或 [VirtualXposed](https://github.com/android-hacker/VirtualXposed) 等Hooking框架的支持,而顶层可以轻松对接Java、Kotlin、Scala等JVM系语言。让程序员能够在几分钟内编写出简单的微信插件,随意揉捏微信的内部逻辑。 20 | 21 | 另外,在编写项目文档的过程中,也会找机会向大家分享一些逆向微信的经验和适配不同操作系统踩到的坑,也欢迎大家把自己的经验分享上来自由讨论。 22 | 23 | ## 便利特色 24 | 25 | * __精心设计各项机制,合理运用多线程和惰性求值等技巧,用不到的功能永远不会影响你的性能。__ 26 | * 使用一套API __自动分析__ 微信内部结构特征,__避免手工适配__ 每个微信版本不同的类名、方法名。 27 | - 每次微信更新的时候,都会使用写好的单元测试自动验证是否有特征失效。 28 | - 精心设计的框架保证了开发者可以轻松拓展添加自己需要的特征。 29 | * 框架内部设计了 _EventCenter_ 和 _HookerProvider_ 两类不同的事件处理方式。 30 | - _EventCenter_ 让开发者直接使用设计好的事件消息来截获微信数据,保证 __便利性__ 。 31 | - _HookerProvider_ 允许熟悉Xposed的开发者调用Xposed接口进行自由发挥,保证 __自由度__ 。 32 | * 正确使用 _EventCenter_ 方案,有助于回避Xposed的一些小问题,如 33 | - 函数调用被前一个劫持者打断导致的插件相互冲突。 34 | - Xposed自Android 7.0后偶发的,由于多线程导致ART崩溃的问题。 35 | 36 | ## 衍生项目 37 | * [WechatMagician](https://github.com/Gh0u1L5/WechatMagician) 38 | * [WechatBotXposed](https://github.com/Blankeer/WechatBotXposed) 39 | 40 | ## 开发文档 41 | * [简介](https://github.com/Gh0u1L5/WechatSpellbook/wiki/Home) 42 | * [快速上手](https://github.com/Gh0u1L5/WechatSpellbook/wiki/快速上手) 43 | * 开发教程 44 | - [事件机制](https://github.com/Gh0u1L5/WechatSpellbook/wiki/事件机制) 45 | - 反射集合 46 | - 混淆与自动适配 47 | * 逆向技巧 48 | - 常见逆向工具 49 | - 调试输出 50 | - 堆栈跟踪 51 | - Support库 52 | - 免重启调试插件 53 | 54 | 55 | ## 关于VirtualXposed 56 | 57 | 目前对于VirtualXposed的支持还算不上完善,因为VirtualXposed的环境和原生Xposed的环境实在是差了太多,我和weishu折腾了很久才算是在部分设备上解决了黑屏卡死的问题。现在我们俩发布代码的日子里,一个拜三清一个拜关公,希望能够帮Bug们早日往生。 58 | 59 | ## 写给开发者 60 | 61 | 在出于兴趣接触微信逆向的短短一年里,我接触到了形形色色的开发者、投资者、支持者。被微信本身复杂成熟的架构深深吸引的同时,也惊异于微信衍生出来的灰色产业之庞大、第三方微信竞争之激烈。 62 | 有不少朋友劝我闭源、商业化,而且拿出了很多细致的想法,我很感激他们的帮助,但是最后还是决定在开源的方向上再次迈出了一大步,原因有二。 63 | 64 | 其一,我不想停在这里。商业化必然牵扯大量的时间精力和权益纠葛。我不想在自己正处在上升期的时候,就草率地把大量的时间精力都花在一个刚满20岁的时候意外做出来的小成就上,然后吃上十几年的老本。而且这个项目,归根结底是一个寄生在微信上的项目。这种格局,跟我每天在实习岗位上接触到的、跟大学里的同学谈论到的,都根本不在一个层面上。我虽然知道自己的才华有限,但是只要有机会的话,我还是想看看更高处的风景。 65 | 66 | 其二,我太过理想主义。中国互联网在很早的时候就已经是一个相当商业化的世界了。我并不反对商业化,也不觉得商业化有任何的道德问题。人,总归是要吃饭的。像fkzhang这样能靠自己的才华吃饭,既不偷也不抢,这有什么错呢?但是我心里总归是感觉到失落的,因为我最早爱上的,是一个自由的互联网,是一个为每个有才华有求知欲的年轻人准备的儿童乐园。只要思想用0和1表达出来,就再也没有什么规则能够阻挡他们,他们就是儿童乐园之王。我希望当我国的年轻一代对计算机产生好奇时,也能像西方国家的孩子们一样,能够轻松地、无语言障碍地,接触到大量好玩有趣的个人开源项目,进而爱上这个0与1的世界。如果有年轻的初中、高中的学弟学妹,能够用我的项目把微信像橡皮泥一样随意揉捏,像当年的我一样深深地享受到这个世界的乐趣,那么对我实在是一种莫大的鼓励与快乐。 67 | 68 | 当然,对于诸位想要基于我的项目做商业项目的开发者,也请尽管拿去。我不喜欢道德绑架别人,也不认为这有什么绑架的必要。但是如果你在闲暇时间能够贡献十几行代码、修复些你发现的Bug,那就已经十分感激。 69 | 70 | ## 打赏二维码 71 | 72 | 应 [Issue #5](https://github.com/Gh0u1L5/WechatSpellbook/issues/5) 的请求,贴上打赏二维码,让我们回归平和的技术讨论。 73 | 74 | 75 | -------------------------------------------------------------------------------- /Wechat8Spellbook/build.gradle: -------------------------------------------------------------------------------- 1 | // 重复添加同样的依赖,以保证Spellbook能够独立编译 2 | buildscript { 3 | ext { 4 | dokka_version = '0.9.17' 5 | kotlin_version = '1.3.10' 6 | } 7 | repositories { 8 | jcenter() 9 | google() 10 | } 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:3.2.1' 13 | classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:$dokka_version" 14 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 15 | } 16 | } 17 | 18 | apply plugin: 'com.android.library' 19 | apply plugin: 'kotlin-android' 20 | apply plugin: 'org.jetbrains.dokka-android' 21 | 22 | android { 23 | compileSdkVersion 28 24 | defaultConfig { 25 | minSdkVersion 19 26 | targetSdkVersion 28 27 | versionCode 1 28 | versionName "1.0" 29 | 30 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 31 | } 32 | buildTypes { 33 | release { 34 | postprocessing { 35 | removeUnusedCode false 36 | removeUnusedResources false 37 | obfuscate false 38 | optimizeCode false 39 | proguardFile 'proguard-rules.pro' 40 | } 41 | } 42 | } 43 | lintOptions { 44 | abortOnError false 45 | } 46 | sourceSets { 47 | main.java.srcDirs += 'src/main/kotlin' 48 | test.java.srcDirs += 'src/test/kotlin' 49 | androidTest.java.srcDirs += "src/androidTest/kotlin" 50 | } 51 | android.libraryVariants.all { variant -> 52 | variant.javaCompiler.dependsOn(generateMirrorClassesList) 53 | variant.javaCompiler.dependsOn(generateMirrorMethodsList) 54 | variant.javaCompiler.dependsOn(generateMirrorFieldsList) 55 | } 56 | } 57 | 58 | repositories { 59 | jcenter() 60 | google() 61 | } 62 | 63 | dependencies { 64 | testImplementation 'junit:junit:4.12' 65 | androidTestImplementation 'androidx.test:runner:1.1.0' 66 | androidTestImplementation 'androidx.test.ext:junit:1.0.0' 67 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' 68 | 69 | //noinspection GradleDependency 70 | compileOnly 'de.robv.android.xposed:api:53' 71 | compileOnly 'de.robv.android.xposed:api:53:sources' 72 | api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 73 | } 74 | 75 | task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaAndroidTask) { 76 | outputFormat = 'javadoc' 77 | outputDirectory = "$buildDir/dokkaJavadoc" 78 | externalDocumentationLink { 79 | url = new URL("https://kotlinlang.org/api/latest/jvm/stdlib/") 80 | } 81 | externalDocumentationLink { 82 | url = new URL("https://developer.android.com/reference/") 83 | } 84 | externalDocumentationLink { 85 | url = new URL("http://api.xposed.info/reference/") 86 | } 87 | } 88 | 89 | def generateMirrorList(String type, String filename) { 90 | def SRC_DIR = "src/main/kotlin/" 91 | def MIRROR_DIR = "src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror" 92 | 93 | def prefixLength = file(SRC_DIR).absolutePath.length() + 1 94 | def classes = fileTree(MIRROR_DIR).filter { file -> 95 | file.name == "${type}.kt" 96 | }.collect { file -> 97 | def relativePath = file.absolutePath.drop(prefixLength) 98 | relativePath.replaceAll('[\\\\/]', '.').substring(0, relativePath.length() - 3) 99 | }.sort() 100 | file("$MIRROR_DIR/${filename}.kt").text = """package com.gh0u1l5.wechatmagician.spellbook.mirror 101 | 102 | /** 103 | * Dynamically generated by build.gradle 104 | */ 105 | val $filename = listOf ( 106 | ${classes.join(",\n ")} 107 | ) 108 | """ 109 | } 110 | 111 | task generateMirrorClassesList() { 112 | generateMirrorList("Classes", "MirrorClasses") 113 | } 114 | 115 | task generateMirrorMethodsList() { 116 | generateMirrorList("Methods", "MirrorMethods") 117 | } 118 | 119 | task generateMirrorFieldsList() { 120 | generateMirrorList("Fields", "MirrorFields") 121 | } 122 | 123 | apply from: 'maven.gradle' -------------------------------------------------------------------------------- /Wechat8Spellbook/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.daemon=true 2 | 3 | POM_NAME=WechatSpellbook 4 | POM_ARTIFACT_ID=wechat-spellbook 5 | POM_PACKAGING=aar 6 | 7 | VERSION_NAME=0.0.6 8 | VERSION_CODE=6 9 | GROUP=com.github.gh0u1l5 10 | 11 | POM_DESCRIPTION=Wechat Spellbook is a powerful and scalable framework, which allows everyone to develop a Wechat Xposed module in minutes. 12 | POM_URL=https://github.com/Gh0u1L5/WechatSpellbook 13 | POM_SCM_URL=https://github.com/Gh0u1L5/WechatSpellbook 14 | POM_SCM_CONNECTION=scm:git@github.com:Gh0u1L5/WechatSpellbook.git 15 | POM_SCM_DEV_CONNECTION=scm:git@github.com:Gh0u1L5/WechatSpellbook.git 16 | POM_LICENCE_NAME=GNU General Public License v3.0 17 | POM_LICENCE_URL=https://www.gnu.org/licenses/gpl-3.0.en.html 18 | POM_LICENCE_DIST=repo 19 | POM_DEVELOPER_ID=Gh0u1L5 20 | POM_DEVELOPER_NAME=Emerson Lin 21 | -------------------------------------------------------------------------------- /Wechat8Spellbook/maven.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven' 2 | apply plugin: 'signing' 3 | 4 | def isReleaseBuild() { 5 | return !VERSION_NAME.contains("SNAPSHOT") 6 | } 7 | 8 | def getReleaseRepositoryUrl() { 9 | return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL 10 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 11 | } 12 | 13 | def getSnapshotRepositoryUrl() { 14 | return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL 15 | : "https://oss.sonatype.org/content/repositories/snapshots/" 16 | } 17 | 18 | def getRepositoryUsername() { 19 | return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : "" 20 | } 21 | 22 | def getRepositoryPassword() { 23 | return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : "" 24 | } 25 | 26 | afterEvaluate { project -> 27 | uploadArchives { 28 | repositories { 29 | mavenDeployer { 30 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 31 | 32 | pom.groupId = GROUP 33 | pom.artifactId = POM_ARTIFACT_ID 34 | pom.version = VERSION_NAME 35 | 36 | repository(url: getReleaseRepositoryUrl()) { 37 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 38 | } 39 | snapshotRepository(url: getSnapshotRepositoryUrl()) { 40 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) 41 | } 42 | 43 | pom.project { 44 | name POM_NAME 45 | packaging POM_PACKAGING 46 | description POM_DESCRIPTION 47 | url POM_URL 48 | 49 | scm { 50 | url POM_SCM_URL 51 | connection POM_SCM_CONNECTION 52 | developerConnection POM_SCM_DEV_CONNECTION 53 | } 54 | 55 | licenses { 56 | license { 57 | name POM_LICENCE_NAME 58 | url POM_LICENCE_URL 59 | distribution POM_LICENCE_DIST 60 | } 61 | } 62 | 63 | developers { 64 | developer { 65 | id POM_DEVELOPER_ID 66 | name POM_DEVELOPER_NAME 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | signing { 75 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } 76 | sign configurations.archives 77 | } 78 | 79 | task javadocJar(type: Jar, dependsOn: dokkaJavadoc) { 80 | classifier = 'javadoc' 81 | from "$buildDir/dokkaJavadoc" 82 | } 83 | 84 | task sourcesJar(type: Jar) { 85 | classifier = 'sources' 86 | from fileTree(dir: 'src/main/kotlin') 87 | } 88 | 89 | artifacts { 90 | archives sourcesJar 91 | archives javadocJar 92 | } 93 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /Wechat8Spellbook/src/androidTest/kotlin/com/gh0u1l5/wechatmagician/spellbook/MirrorUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import androidx.test.platform.app.InstrumentationRegistry 7 | import com.gh0u1l5.wechatmagician.spellbook.base.Version 8 | import com.gh0u1l5.wechatmagician.spellbook.mirror.MirrorClasses 9 | import com.gh0u1l5.wechatmagician.spellbook.mirror.MirrorFields 10 | import com.gh0u1l5.wechatmagician.spellbook.mirror.MirrorMethods 11 | import com.gh0u1l5.wechatmagician.spellbook.parser.ApkFile 12 | import com.gh0u1l5.wechatmagician.spellbook.util.FileUtil 13 | import com.gh0u1l5.wechatmagician.spellbook.util.MirrorUtil 14 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil 15 | import dalvik.system.PathClassLoader 16 | import org.junit.Assert.* 17 | import org.junit.Before 18 | import org.junit.Test 19 | import org.junit.runner.RunWith 20 | import java.io.File 21 | import java.lang.ClassLoader.getSystemClassLoader 22 | import kotlin.system.measureTimeMillis 23 | 24 | /** 25 | * 自动化的微信版本适配测试 26 | */ 27 | @ExperimentalUnsignedTypes 28 | @RunWith(AndroidJUnit4::class) 29 | class MirrorUnitTest { 30 | companion object { 31 | private const val DOMESTIC_DIR = "apks/domestic" 32 | private const val PLAY_STORE_DIR = "apks/play-store" 33 | } 34 | 35 | private var context: Context? = null 36 | 37 | @Before 38 | fun initialize() { 39 | context = InstrumentationRegistry.getInstrumentation().targetContext 40 | } 41 | 42 | private fun verifyPackage(apkPath: String) { 43 | // 解析 APK 版本 44 | val regex = Regex("wechat-v(.*)\\.apk") 45 | val match = regex.find(apkPath) ?: throw Exception("Unexpected path format") 46 | val version = match.groupValues[1] 47 | 48 | // 将 APK 文件保存至 Cache 目录 49 | val cacheDir = context!!.cacheDir 50 | val apkFile = File(cacheDir, apkPath) 51 | try { 52 | javaClass.classLoader!!.getResourceAsStream(apkPath).use { 53 | FileUtil.writeInputStreamToDisk(apkFile.absolutePath, it) 54 | } 55 | } catch (t: Throwable) { 56 | Log.w("MirrorUnitTest", t) 57 | return // ignore if the apk isn't accessible 58 | } 59 | 60 | // 确保 APK 文件存在 并开始自动化适配测试 61 | assertTrue(apkFile.exists()) 62 | ApkFile(apkFile).use { 63 | // 测试 APK Parser 的解析速度 64 | val timeParseDex = measureTimeMillis { it.classTypes } 65 | Log.d("MirrorUnitTest", "Benchmark: Parsing APK takes $timeParseDex ms.") 66 | 67 | // 初始化 WechatGlobal 68 | WechatGlobal.wxUnitTestMode = true 69 | WechatGlobal.wxVersion = Version(version) 70 | WechatGlobal.wxPackageName = "com.tencent.mm" 71 | WechatGlobal.wxLoader = PathClassLoader(apkFile.absolutePath, getSystemClassLoader()) 72 | WechatGlobal.wxClasses = it.classTypes 73 | 74 | // 清理上次测试留下的缓存 75 | val objects = MirrorClasses + MirrorMethods + MirrorFields 76 | ReflectionUtil.clearClassCache() 77 | ReflectionUtil.clearMethodCache() 78 | objects.forEach { instance -> 79 | MirrorUtil.clearUnitTestLazyFields(instance) 80 | } 81 | 82 | // 进行适配测试并生成结果 83 | var result: List>? = null 84 | val timeSearch = measureTimeMillis { 85 | result = MirrorUtil.generateReportWithForceEval(objects) 86 | } 87 | Log.d("MirrorUnitTest", "Benchmark: Searching over classes takes $timeSearch ms.") 88 | result?.forEach { entry -> 89 | Log.d("MirrorUnitTest", "Verified: ${entry.first} -> ${entry.second}") 90 | } 91 | } 92 | 93 | apkFile.delete() 94 | } 95 | 96 | @Test 97 | fun verifyDomesticPackage8_0_6() { 98 | verifyPackage("$DOMESTIC_DIR/wechat-v8.0.6.apk") 99 | } 100 | 101 | // @Test fun verifyDomesticPackage6_6_0() { 102 | // verifyPackage("$DOMESTIC_DIR/wechat-v6.6.0.apk") 103 | // } 104 | // 105 | // @Test fun verifyDomesticPackage6_6_1() { 106 | // verifyPackage("$DOMESTIC_DIR/wechat-v6.6.1.apk") 107 | // } 108 | // 109 | // @Test fun verifyDomesticPackage6_6_2() { 110 | // verifyPackage("$DOMESTIC_DIR/wechat-v6.6.2.apk") 111 | // } 112 | // 113 | // @Test fun verifyDomesticPackage6_6_3() { 114 | // verifyPackage("$DOMESTIC_DIR/wechat-v6.6.3.apk") 115 | // } 116 | // 117 | // @Test fun verifyDomesticPackage6_6_5() { 118 | // verifyPackage("$DOMESTIC_DIR/wechat-v6.6.5.apk") 119 | // } 120 | // 121 | // @Test fun verifyDomesticPackage6_6_6() { 122 | // verifyPackage("$DOMESTIC_DIR/wechat-v6.6.6.apk") 123 | // } 124 | // 125 | // @Test fun verifyDomesticPackage6_6_7() { 126 | // verifyPackage("$DOMESTIC_DIR/wechat-v6.6.7.apk") 127 | // } 128 | // 129 | // @Test fun verifyDomesticPackage6_7_2() { 130 | // verifyPackage("$DOMESTIC_DIR/wechat-v6.7.2.apk") 131 | // } 132 | // 133 | // @Test fun verifyDomesticPackage6_7_3() { 134 | // verifyPackage("$DOMESTIC_DIR/wechat-v6.7.3.apk") 135 | // } 136 | // 137 | // @Test fun verifyPlayStorePackage6_6_1() { 138 | // verifyPackage("$PLAY_STORE_DIR/wechat-v6.6.1.apk") 139 | // } 140 | // 141 | // @Test fun verifyPlayStorePackage6_6_2() { 142 | // verifyPackage("$PLAY_STORE_DIR/wechat-v6.6.2.apk") 143 | // } 144 | // 145 | // @Test fun verifyPlayStorePackage6_6_6() { 146 | // verifyPackage("$PLAY_STORE_DIR/wechat-v6.6.6.apk") 147 | // } 148 | // 149 | // @Test fun verifyPlayStorePackage6_6_7() { 150 | // verifyPackage("$PLAY_STORE_DIR/wechat-v6.6.7.apk") 151 | // } 152 | // 153 | // @Test fun verifyPlayStorePackage6_7_3() { 154 | // verifyPackage("$PLAY_STORE_DIR/wechat-v6.7.3.apk") 155 | // } 156 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/androidTest/resources/apks/rename.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | re='(.*?)/com\.tencent\.mm_(.*?)_.*?\.apk' 3 | for file in */*.apk; do 4 | if [[ $file =~ $re ]]; then 5 | mv "$file" "${BASH_REMATCH[1]}/wechat-v${BASH_REMATCH[2]}.apk" 6 | fi 7 | done 8 | -------------------------------------------------------------------------------- /Wechat8Spellbook/src/androidTest/resources/apks/simplify.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | workdir=`pwd` 4 | re='(.*?)/(.*?)\.apk' 5 | 6 | for file in */*.apk; do 7 | if [[ $file =~ $re ]]; then 8 | basename="${BASH_REMATCH[2]}" 9 | tempdir="${BASH_REMATCH[1]}/${BASH_REMATCH[2]}" 10 | 11 | unzip "$file" -d "$tempdir" 12 | cd "$tempdir" 13 | rm -rf META-INF assets lib r resources.arsc 14 | zip -9 "${basename}.apk" * && mv "${basename}.apk" ../ 15 | cd "$workdir" 16 | rm -rf "$tempdir" 17 | fi 18 | done 19 | -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/Alias.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook 2 | 3 | typealias Predicate = (Any?) -> Boolean -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/C.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | package com.gh0u1l5.wechatmagician.spellbook 4 | 5 | /** 6 | * C.(class name) 是 (class name)::class.java 的缩写形式 7 | * 8 | * 没有什么特别的意义, 仅仅是为了让 Kotlin 代码看起来比较优雅(事逼) 9 | */ 10 | object C { 11 | val Boolean = Boolean::class.java 12 | val File = java.io.File::class.java 13 | val FileInputStream = java.io.FileInputStream::class.java 14 | val FileOutputStream = java.io.FileOutputStream::class.java 15 | val Int = Int::class.java 16 | val Iterator = java.util.Iterator::class.java 17 | val Long = Long::class.java 18 | val Map = Map::class.java 19 | val Object = Object::class.java 20 | val String = String::class.java 21 | val Throwable = Throwable::class.java 22 | 23 | val Activity = android.app.Activity::class.java 24 | val AdapterView = android.widget.AdapterView::class.java 25 | val AdapterView_OnItemClickListener = android.widget.AdapterView.OnItemClickListener::class.java 26 | val AttributeSet = android.util.AttributeSet::class.java 27 | val BaseAdapter = android.widget.BaseAdapter::class.java 28 | val Bundle = android.os.Bundle::class.java 29 | val Button = android.widget.Button::class.java 30 | val Configuration = android.content.res.Configuration::class.java 31 | val ContentValues = android.content.ContentValues::class.java 32 | val Context = android.content.Context::class.java 33 | val ContextMenu = android.view.ContextMenu::class.java 34 | val ContextMenuInfo = android.view.ContextMenu.ContextMenuInfo::class.java 35 | val HeaderViewListAdapter = android.widget.HeaderViewListAdapter::class.java 36 | val Intent = android.content.Intent::class.java 37 | val KeyEvent = android.view.KeyEvent::class.java 38 | val ListAdapter = android.widget.ListAdapter::class.java 39 | val ListView = android.widget.ListView::class.java 40 | val Menu = android.view.Menu::class.java 41 | val Message = android.os.Message::class.java 42 | val MotionEvent = android.view.MotionEvent::class.java 43 | val Notification = android.app.Notification::class.java 44 | val NotificationManager = android.app.NotificationManager::class.java 45 | val View = android.view.View::class.java 46 | val ViewGroup = android.view.ViewGroup::class.java 47 | 48 | val ByteArray = ByteArray::class.java 49 | val IntArray = IntArray::class.java 50 | val ObjectArray = Array::class.java 51 | val StringArray = Array::class.java 52 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/SpellBook.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook 2 | 3 | import android.content.Context 4 | import com.gh0u1l5.wechatmagician.spellbook.base.EventCenter 5 | import com.gh0u1l5.wechatmagician.spellbook.base.HookerProvider 6 | import com.gh0u1l5.wechatmagician.spellbook.base.Version 7 | import com.gh0u1l5.wechatmagician.spellbook.hookers.* 8 | import com.gh0u1l5.wechatmagician.spellbook.util.ParallelUtil.parallelForEach 9 | import com.gh0u1l5.wechatmagician.spellbook.util.XposedUtil 10 | import de.robv.android.xposed.XposedBridge.log 11 | import de.robv.android.xposed.XposedHelpers.* 12 | import de.robv.android.xposed.callbacks.XC_LoadPackage 13 | import de.robv.android.xposed.IXposedHookLoadPackage 14 | import java.io.File 15 | 16 | /** 17 | * Wechat Magician SpellBook的核心引擎部分 18 | * 19 | * Refer: https://github.com/Gh0u1L5/WechatSpellbook/wiki 20 | */ 21 | object SpellBook { 22 | /** 23 | * 目前支持的 [EventCenter] 列表 24 | * 25 | * Refer: https://github.com/Gh0u1L5/WechatSpellbook/wiki/事件机制 26 | */ 27 | private val centers: List = listOf( 28 | Activities, 29 | Adapters, 30 | Database, 31 | FileSystem, 32 | MenuAppender, 33 | Notifications, 34 | SearchBar, 35 | Storage, 36 | UriRouter, 37 | XLog, 38 | XmlParser 39 | ) 40 | 41 | /** 42 | * 判断当前进程是否为微信的重要进程, 目前会被判定为重要进程的只有主进程和 :tools 进程 43 | * 44 | * @param lpparam 通过重载 [IXposedHookLoadPackage.handleLoadPackage] 方法拿到的 45 | * [XC_LoadPackage.LoadPackageParam] 对象 46 | */ 47 | fun isImportantWechatProcess(lpparam: XC_LoadPackage.LoadPackageParam): Boolean { 48 | // 检查进程名 49 | val processName = lpparam.processName 50 | when { 51 | !processName.contains(':') -> { 52 | // 找到主进程 继续 53 | } 54 | processName.endsWith(":tools") -> { 55 | // 找到 :tools 进程 继续 56 | } 57 | else -> return false 58 | } 59 | // 检查微信依赖的JNI库是否存在, 以此判断当前应用是不是微信 60 | val features = listOf ( 61 | "libwechatcommon.so", 62 | "libwechatmm.so", 63 | "libwechatnetwork.so", 64 | "libwechatsight.so", 65 | "libwechatxlog.so" 66 | ) 67 | return try { 68 | val libraryDir = File(lpparam.appInfo.nativeLibraryDir) 69 | features.filter { filename -> 70 | File(libraryDir, filename).exists() 71 | }.size >= 3 72 | } catch (t: Throwable) { false } 73 | } 74 | 75 | /** 76 | * 利用 Reflection 获取当前的系统 Context 77 | */ 78 | fun getSystemContext(): Context { 79 | val activityThreadClass = findClass("android.app.ActivityThread", null) 80 | val activityThread = callStaticMethod(activityThreadClass, "currentActivityThread") 81 | val context = callMethod(activityThread, "getSystemContext") as Context? 82 | return context ?: throw Error("Failed to get system context.") 83 | } 84 | 85 | /** 86 | * 获取指定应用的 APK 路径 87 | */ 88 | fun getApplicationApkPath(packageName: String): String { 89 | val pm = getSystemContext().packageManager 90 | val apkPath = pm.getApplicationInfo(packageName, 0)?.publicSourceDir 91 | return apkPath ?: throw Error("Failed to get the APK path of $packageName") 92 | } 93 | 94 | /** 95 | * 获取指定应用的版本号 96 | */ 97 | fun getApplicationVersion(packageName: String): Version { 98 | val pm = getSystemContext().packageManager 99 | val versionName = pm.getPackageInfo(packageName, 0)?.versionName 100 | return Version(versionName 101 | ?: throw Error("Failed to get the version of $packageName")) 102 | } 103 | 104 | /** 105 | * 启动 SpellBook 框架, 注册相关插件 106 | * 107 | * @param lpparam 通过重载 [IXposedHookLoadPackage.handleLoadPackage] 方法拿到的 108 | * [XC_LoadPackage.LoadPackageParam] 对象 109 | * @param plugins 由开发者编写的 SpellBook 插件, 这些插件应当实现 [HookerProvider.provideStaticHookers] 110 | * 方法, 或 interfaces 包中提供的标准接口 111 | * 112 | * Refer: https://github.com/Gh0u1L5/WechatSpellbook/wiki/事件机制 113 | */ 114 | fun startup(lpparam: XC_LoadPackage.LoadPackageParam, plugins: List?) { 115 | log("Wechat SpellBook: ${plugins?.size ?: 0} plugins.") 116 | WechatGlobal.init(lpparam) 117 | registerPlugins(plugins) 118 | registerHookers(plugins) 119 | } 120 | 121 | /** 122 | * 检查插件是否实现了标准化的接口, 并将它们注册到对应的 [EventCenter] 中 123 | */ 124 | private fun registerPlugins(plugins: List?) { 125 | val observers = plugins?.filter { it !is HookerProvider } ?: listOf() 126 | centers.parallelForEach { center -> 127 | center.interfaces.forEach { `interface` -> 128 | observers.forEach { plugin -> 129 | val assignable = `interface`.isAssignableFrom(plugin::class.java) 130 | if (assignable) { 131 | center.register(`interface`, plugin) 132 | } 133 | } 134 | } 135 | } 136 | } 137 | 138 | /** 139 | * 检查插件中是否存在自定义的事件, 将它们直接注册到 Xposed 框架上 140 | */ 141 | private fun registerHookers(plugins: List?) { 142 | val providers = plugins?.filter { it is HookerProvider } ?: listOf() 143 | (providers + listOf(ListViewHider, MenuAppender)).parallelForEach { provider -> 144 | (provider as HookerProvider).provideStaticHookers()?.forEach { hooker -> 145 | if (!hooker.hasHooked) { 146 | XposedUtil.postHooker(hooker) 147 | } 148 | } 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/WechatGlobal.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook 2 | 3 | import android.widget.Adapter 4 | import android.widget.BaseAdapter 5 | import com.gh0u1l5.wechatmagician.spellbook.SpellBook.getApplicationVersion 6 | import com.gh0u1l5.wechatmagician.spellbook.base.Version 7 | import com.gh0u1l5.wechatmagician.spellbook.base.WaitChannel 8 | import com.gh0u1l5.wechatmagician.spellbook.parser.ApkFile 9 | import com.gh0u1l5.wechatmagician.spellbook.parser.ClassTrie 10 | import com.gh0u1l5.wechatmagician.spellbook.util.BasicUtil.tryAsynchronously 11 | import de.robv.android.xposed.IXposedHookLoadPackage 12 | import de.robv.android.xposed.callbacks.XC_LoadPackage 13 | import java.lang.ref.WeakReference 14 | 15 | /** 16 | * 用于记录所有关于 Wechat 的关键全局变量 17 | */ 18 | object WechatGlobal { 19 | 20 | /** 21 | * 若初始化操作耗费2秒以上, 视作初始化失败, 直接让微信开始正常运行 22 | */ 23 | @Suppress("MemberVisibilityCanBePrivate") 24 | const val INIT_TIMEOUT = 2000L // ms 25 | 26 | /** 27 | * 用于防止其他线程在初始化完成之前访问 WechatGlobal的变量 28 | */ 29 | private val initChannel = WaitChannel() 30 | 31 | /** 32 | * 微信版本 33 | * 34 | * 如果初始化还未完成的话, 访问该对象的线程会自动阻塞 [INIT_TIMEOUT] ms 35 | */ 36 | @Volatile var wxVersion: Version? = null 37 | get() { 38 | if (!wxUnitTestMode) { 39 | initChannel.wait(INIT_TIMEOUT) 40 | initChannel.done() 41 | } 42 | return field 43 | } 44 | 45 | /** 46 | * 微信包名(用于处理多开的情况) 47 | * 48 | * 如果初始化还未完成的话, 访问该对象的线程会自动阻塞 [INIT_TIMEOUT] ms 49 | */ 50 | @Volatile var wxPackageName: String = "" 51 | get() { 52 | if (!wxUnitTestMode) { 53 | initChannel.wait(INIT_TIMEOUT) 54 | initChannel.done() 55 | } 56 | return field 57 | } 58 | 59 | /** 60 | * 微信 APK 所使用的 ClassLoader, 用于加载 Class 对象 61 | * 62 | * 如果初始化还未完成的话, 访问该对象的线程会自动阻塞 [INIT_TIMEOUT] ms 63 | */ 64 | @Volatile var wxLoader: ClassLoader? = null 65 | get() { 66 | if (!wxUnitTestMode) { 67 | initChannel.wait(INIT_TIMEOUT) 68 | initChannel.done() 69 | } 70 | return field 71 | } 72 | 73 | /** 74 | * 微信 APK 所包含的全部类名, 依据 package 结构组织在一起, 用于动态适配不同的微信版本 75 | * 76 | * 如果初始化还未完成的话, 访问该对象的线程会自动阻塞 [INIT_TIMEOUT] ms 77 | */ 78 | @Volatile var wxClasses: ClassTrie? = null 79 | get() { 80 | if (!wxUnitTestMode) { 81 | initChannel.wait(INIT_TIMEOUT) 82 | initChannel.done() 83 | } 84 | return field 85 | } 86 | 87 | /** 88 | * 单元测试模式的开关, 只应该在单元测试中打开 89 | */ 90 | @Volatile var wxUnitTestMode: Boolean = false 91 | 92 | // 缓存一些重要的微信全局对象 93 | @Volatile var AddressAdapterObject: WeakReference = WeakReference(null) 94 | @Volatile var ConversationAdapterObject: WeakReference = WeakReference(null) 95 | @Volatile var SnsUserUIAdapterObject: WeakReference = WeakReference(null) 96 | @Volatile var MsgStorageObject: Any? = null 97 | @Volatile var ImgStorageObject: Any? = null 98 | @Volatile var MainDatabaseObject: Any? = null 99 | @Volatile var SnsDatabaseObject: Any? = null 100 | 101 | /** 102 | * 创建一个惰性求值对象, 只有被用到的时候才会自动求值 103 | * 104 | * 当单元测试模式开启的时候, 会使用不同的 Lazy Implementation 辅助测试 105 | * 106 | * @param name 对象名称, 打印错误日志的时候会用到 107 | * @param initializer 用来求值的回调函数 108 | */ 109 | inline fun wxLazy(name: String, crossinline initializer: () -> T?): Lazy { 110 | return if (wxUnitTestMode) { 111 | UnitTestLazyImpl { 112 | initializer() ?: throw Error("Failed to evaluate $name") 113 | } 114 | } else { 115 | lazy(LazyThreadSafetyMode.PUBLICATION) { 116 | when (null) { 117 | wxVersion -> throw Error("Invalid wxVersion") 118 | wxPackageName -> throw Error("Invalid wxPackageName") 119 | wxLoader -> throw Error("Invalid wxLoader") 120 | wxClasses -> throw Error("Invalid wxClasses") 121 | } 122 | initializer() ?: throw Error("Failed to evaluate $name") 123 | } 124 | } 125 | } 126 | 127 | /** 128 | * 用来帮助单元测试的一个 Lazy Implementation, 允许开发者多次初始化一个惰性求值对象 129 | */ 130 | class UnitTestLazyImpl(private val initializer: () -> T): Lazy, java.io.Serializable { 131 | @Volatile private var lazyValue: Lazy = lazy(initializer) 132 | 133 | fun refresh() { 134 | lazyValue = lazy(initializer) 135 | } 136 | 137 | override val value: T 138 | get() = lazyValue.value 139 | 140 | override fun toString(): String = lazyValue.toString() 141 | 142 | override fun isInitialized(): Boolean = lazyValue.isInitialized() 143 | } 144 | 145 | /** 146 | * 初始化当前的 [WechatGlobal] 147 | * 148 | * @param lpparam 通过重载 [IXposedHookLoadPackage.handleLoadPackage] 方法拿到的 149 | * [XC_LoadPackage.LoadPackageParam] 对象 150 | */ 151 | @JvmStatic fun init(lpparam: XC_LoadPackage.LoadPackageParam) { 152 | tryAsynchronously { 153 | if (initChannel.isDone()) { 154 | return@tryAsynchronously 155 | } 156 | 157 | try { 158 | wxVersion = getApplicationVersion(lpparam.packageName) 159 | wxPackageName = lpparam.packageName 160 | wxLoader = lpparam.classLoader 161 | 162 | ApkFile(lpparam.appInfo.sourceDir).use { 163 | wxClasses = it.classTypes 164 | } 165 | } finally { 166 | initChannel.done() 167 | } 168 | } 169 | } 170 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/WechatStatus.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.base.EventCenter 4 | 5 | /** 6 | * 用来记录各个 [EventCenter] 运行状态的单例对象 7 | */ 8 | object WechatStatus { 9 | 10 | /** 11 | * 目前支持的所有功能的标识 12 | */ 13 | enum class StatusFlag { 14 | STATUS_FLAG_ACTIVITIES, 15 | STATUS_FLAG_ADAPTERS, 16 | STATUS_FLAG_BASE_ADAPTER, 17 | STATUS_FLAG_COMMAND, 18 | STATUS_FLAG_CONTACT_POPUP, 19 | STATUS_FLAG_CONVERSATION_POPUP, 20 | STATUS_FLAG_DATABASE, 21 | STATUS_FLAG_FILESYSTEM, 22 | STATUS_FLAG_IMG_STORAGE, 23 | STATUS_FLAG_MSG_STORAGE, 24 | STATUS_FLAG_NOTIFICATIONS, 25 | STATUS_FLAG_RESOURCES, 26 | STATUS_FLAG_URI_ROUTER, 27 | STATUS_FLAG_XML_PARSER 28 | } 29 | 30 | /** 31 | * 用于记录所有成功启动的功能 32 | */ 33 | private var valid: IntArray = intArrayOf() 34 | 35 | /** 36 | * 报告当前活跃的功能 37 | */ 38 | @Synchronized fun report(): IntArray = valid 39 | 40 | /** 41 | * 记录某功能启动完成 42 | */ 43 | @Synchronized fun toggle(flag: StatusFlag) { valid += flag.ordinal } 44 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/base/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.base 2 | 3 | import android.util.Log 4 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findFieldIfExists 5 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findFieldsWithType 6 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findMethodExactIfExists 7 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findMethodsByExactParameters 8 | 9 | /** 10 | * 一组 Class 对象的集合, 可以通过调用不同的 filter 函数筛选得到想要的结果 11 | */ 12 | class Classes(private val classes: List>) { 13 | /** 14 | * @suppress 15 | */ 16 | private companion object { 17 | private const val TAG = "Reflection" 18 | } 19 | 20 | fun filterBySuper(superClass: Class<*>?): Classes { 21 | return Classes(classes.filter { it.superclass == superClass }.also { 22 | if (it.isEmpty()) { 23 | Log.w(TAG, "filterBySuper found nothing, super class = ${superClass?.simpleName}") 24 | } 25 | }) 26 | } 27 | 28 | fun filterByEnclosingClass(enclosingClass: Class<*>?): Classes { 29 | return Classes(classes.filter { it.enclosingClass == enclosingClass }.also { 30 | if (it.isEmpty()) { 31 | Log.w(TAG, "filterByEnclosingClass found nothing, enclosing class = ${enclosingClass?.simpleName} ") 32 | } 33 | }) 34 | } 35 | 36 | fun filterByMethod(returnType: Class<*>?, methodName: String, vararg parameterTypes: Class<*>): Classes { 37 | return Classes(classes.filter { clazz -> 38 | val method = findMethodExactIfExists(clazz, methodName, *parameterTypes) 39 | method != null && method.returnType == returnType ?: method.returnType 40 | }.also { 41 | if (it.isEmpty()) { 42 | Log.w(TAG, "filterByMethod found nothing, returnType = ${returnType?.simpleName}, methodName = $methodName, parameterTypes = ${parameterTypes.joinToString("|") { it.simpleName }}") 43 | } 44 | }) 45 | } 46 | 47 | fun filterByMethod(returnType: Class<*>?, vararg parameterTypes: Class<*>): Classes { 48 | return Classes(classes.filter { clazz -> 49 | findMethodsByExactParameters(clazz, returnType, *parameterTypes).isNotEmpty() 50 | }.also { 51 | if (it.isEmpty()) { 52 | Log.w(TAG, "filterByMethod found nothing, returnType = ${returnType?.simpleName}, parameterTypes = ${parameterTypes.joinToString("|") { it.simpleName }}") 53 | } 54 | }) 55 | } 56 | 57 | fun filterByField(fieldName: String, fieldType: String): Classes { 58 | return Classes(classes.filter { clazz -> 59 | val field = findFieldIfExists(clazz, fieldName) 60 | field != null && field.type.canonicalName == fieldType 61 | }.also { 62 | if (it.isEmpty()) { 63 | Log.w(TAG, "filterByField found nothing, fieldName = $fieldName, fieldType = $fieldType") 64 | } 65 | }) 66 | } 67 | 68 | fun filterByField(fieldType: String): Classes { 69 | return Classes(classes.filter { clazz -> 70 | findFieldsWithType(clazz, fieldType).isNotEmpty() 71 | }.also { 72 | if (it.isEmpty()) { 73 | Log.w(TAG, "filterByField found nothing, fieldType = $fieldType") 74 | } 75 | }) 76 | } 77 | 78 | fun firstOrNull(): Class<*>? { 79 | if (classes.size > 1) { 80 | val names = classes.map { it.canonicalName } 81 | Log.w("Xposed", "found a signature that matches more than one class: $names") 82 | } 83 | return classes.firstOrNull() 84 | } 85 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/base/EventCenter.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.base 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.util.BasicUtil.tryVerbosely 4 | import com.gh0u1l5.wechatmagician.spellbook.util.ParallelUtil.parallelForEach 5 | import com.gh0u1l5.wechatmagician.spellbook.util.XposedUtil 6 | import de.robv.android.xposed.XC_MethodHook 7 | import java.util.concurrent.ConcurrentHashMap 8 | 9 | /** 10 | * SpellBook 框架的事件中心, 用于提供标准化的事件通知 11 | */ 12 | abstract class EventCenter: HookerProvider { 13 | 14 | /** 15 | * 事件中心所支持的接口列表, 任何想要注册到该中心的插件必须实现其中起码一个接口 16 | */ 17 | abstract val interfaces: List> 18 | 19 | /** 20 | * 不同事件所对应的观察者列表 21 | */ 22 | private val observers: MutableMap> = ConcurrentHashMap() 23 | 24 | /** 25 | * 判断指定插件对象是否关注了某个事件 26 | * 27 | * "关注" 的判断标准是这个对象有没有直接实现一个和事件同名的方法, 从基类继承的方法不算在内 28 | */ 29 | private fun Any.hasEvent(event: String) = 30 | this::class.java.declaredMethods.any { it.name == event } 31 | 32 | /** 33 | * 向事件中心注册一个观察者 34 | * 35 | * @param event 观察者关注的事件 36 | * @param observer 观察者本人 37 | */ 38 | private fun register(event: String, observer: Any) { 39 | if (observer.hasEvent(event)) { 40 | val hooker = provideEventHooker(event) 41 | if (hooker != null && !hooker.hasHooked) { 42 | XposedUtil.postHooker(hooker) 43 | } 44 | val existing = observers[event] ?: emptySet() 45 | observers[event] = existing + observer 46 | } 47 | } 48 | 49 | /** 50 | * 向事件中心注册一个插件 51 | * 52 | * @param inface 该插件所实现的接口, 必须为 [interfaces] 中存在的接口 53 | * @param plugin 插件对象 54 | */ 55 | fun register(inface: Class<*>, plugin: Any) { 56 | inface.methods.forEach { method -> 57 | register(method.name, plugin) 58 | } 59 | } 60 | 61 | /** 62 | * 找到关注某个事件的所有观察者, 若不存在则返回 null 63 | */ 64 | fun findObservers(event: String): Set? = observers[event] 65 | 66 | /** 67 | * 通知所有正在观察某个事件的观察者 68 | * 69 | * @param event 具体发生的事件 70 | * @param action 对观察者进行通知的回调函数 71 | */ 72 | inline fun notify(event: String, action: (Any) -> Unit) { 73 | findObservers(event)?.forEach { 74 | tryVerbosely { action(it) } 75 | } 76 | } 77 | 78 | /** 79 | * 通知所有正在观察某个事件的观察者(并行计算版本) 80 | * 81 | * @param event 具体发生的事件 82 | * @param action 对观察者进行通知的回调函数 83 | */ 84 | inline fun notifyParallel(event: String, crossinline action: (Any) -> Unit) { 85 | findObservers(event)?.parallelForEach { observer -> 86 | tryVerbosely { action(observer) } 87 | } 88 | } 89 | 90 | /** 91 | * 通知所有正在观察某个事件的观察者, 并收集它们的反馈 92 | * 93 | * @param event 具体发生的事件 94 | * @param action 对观察者进行通知的回调函数 95 | */ 96 | inline fun notifyForResults(event: String, action: (Any) -> T?): List { 97 | return findObservers(event)?.mapNotNull { 98 | tryVerbosely { action(it) } 99 | } ?: emptyList() 100 | } 101 | 102 | /** 103 | * 通知所有正在观察某个事件的观察者, 并收集它们的反馈, 以确认是否需要拦截该事件 104 | * 105 | * 如果有任何一个观察者返回了 true, 我们就认定当前事件是一个需要被拦截的事件. 例如当微信写文件的时候, 某个观察者 106 | * 检查过文件路径后返回了 true, 那么框架就会拦截这次写文件操作, 向微信返回一个默认值 107 | * 108 | * @param event 具体发生的事件 109 | * @param param 拦截函数调用后得到的 [XC_MethodHook.MethodHookParam] 对象 110 | * @param default 跳过函数调用之后, 仍然需要向 caller 提供一个返回值 111 | * @param action 对观察者进行通知的回调函数 112 | */ 113 | inline fun notifyForBypassFlags(event: String, param: XC_MethodHook.MethodHookParam, default: Any? = null, action: (Any) -> Boolean) { 114 | val shouldBypass = notifyForResults(event, action).any() 115 | if (shouldBypass) { 116 | param.result = default 117 | } 118 | } 119 | 120 | /** 121 | * 通知所有正在观察某个事件的观察者, 并收集它们的反馈, 以确认该对这次事件采取什么操作 122 | * 123 | * 在获取了观察者建议的操作之后, 我们会对这些操作的优先级进行排序, 从优先级最高的操作中选择一个予以采纳 124 | * 125 | * @param event 具体发生的事件 126 | * @param param 拦截函数调用后得到的 [XC_MethodHook.MethodHookParam] 对象 127 | * @param action 对观察者进行通知的回调函数 128 | */ 129 | inline fun notifyForOperations(event: String, param: XC_MethodHook.MethodHookParam, action: (Any) -> Operation<*>) { 130 | val operations = notifyForResults(event, action) 131 | val result = operations.filter { it.returnEarly }.maxBy { it.priority } 132 | if (result != null) { 133 | if (result.value != null) { 134 | param.result = result.value 135 | } 136 | if (result.error != null) { 137 | param.throwable = result.error 138 | } 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/base/Hooker.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.base 2 | 3 | /** 4 | * 将一次 Hook 操作封装成对象, 防止对同样的函数反复下钩, 造成难以调查的BUG 5 | * 6 | * 这个类是线程安全的, 多个线程同时调用只会有一个线程成功下钩 7 | * 8 | * @property hooker 实际向 Xposed 框架注册钩子的回调函数 9 | * @constructor 将一次 Hook 操作封装成一个 Hooker 对象 10 | */ 11 | class Hooker(private val hooker: () -> Unit) { 12 | /** 13 | * 用来防止重复 Hook 的标记 14 | */ 15 | var hasHooked = false 16 | private set 17 | 18 | /** 19 | * 尝试执行一次 Hook 操作, 如果已经钩过了就不再重复 20 | */ 21 | @Synchronized fun hook() { 22 | if (!hasHooked) { 23 | hooker() 24 | hasHooked = true 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/base/HookerProvider.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.base 2 | 3 | /** 4 | * 一个 HookerProvider 可以向 SpellBook 框架提供自定义的钩子, 框架将根据具体的情况将这些钩子注册到 Xposed 框架里 5 | */ 6 | interface HookerProvider { 7 | /** 8 | * 返回一组静态钩子, 即传统的 Xposed 钩子 9 | * 10 | * 不论发生啥事, 这些钩子都会被注册到 Xposed 里 11 | */ 12 | fun provideStaticHookers(): List? = null 13 | 14 | /** 15 | * 返回一个针对具体事件进行监听的钩子 16 | * 17 | * 只有在某个插件要求监听某个事件的情况下, 对应的钩子才会被注册, 在目前的版本中, 只有 [EventCenter] 才需要实 18 | * 现该方法 19 | * 20 | * WARN: 对于同一个事件, 请返回相同的 Hooker 对象, 这样可以有效避免重复的 hook 行为, 根除潜在的 Bug 21 | */ 22 | fun provideEventHooker(event: String): Hooker? = null 23 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/base/Operation.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.base 2 | 3 | /** 4 | * 当插件监听到某个事件发生, 并拦截到相应的函数调用的时候, 插件可能会需要对拦截住的函数进行某些操作, 这个操作需要被封 5 | * 装成一个 [Operation] 对象传递给 SpellBook 框架 6 | */ 7 | class Operation( 8 | val value: T? = null, 9 | val error: Throwable? = null, 10 | val priority: Int = 0, 11 | val returnEarly: Boolean = false 12 | ) { 13 | companion object { 14 | /** 15 | * 创建一个空操作, 表明自己什么也不做 16 | */ 17 | @JvmStatic fun nop(priority: Int = 0): Operation { 18 | return Operation(priority = priority) 19 | } 20 | 21 | /** 22 | * 创建一个打断操作, 跳过原函数的执行, 直接抛出一个异常 23 | * 24 | * @param error 要抛出的异常 25 | * @param priority 操作的优先级, 当多个插件同时做出操作的时候, 框架将选取优先级较高的结果, 优先级相同的 26 | * 情况下随机选择一个操作 27 | */ 28 | @JvmStatic fun interruption(error: Throwable, priority: Int = 0): Operation { 29 | return Operation(error = error, priority = priority, returnEarly = true) 30 | } 31 | 32 | /** 33 | * 创建一个替换操作, 跳过原函数的执行, 直接返回一个结果 34 | * 35 | * @param value 要返回的结果 36 | * @param priority 操作的优先级, 当多个插件同时做出操作的时候, 框架将选取优先级较高的结果, 优先级相同的 37 | * 情况下随机选择一个操作 38 | */ 39 | @JvmStatic fun replacement(value: T, priority: Int = 0): Operation { 40 | return Operation(value = value, priority = priority, returnEarly = true) 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/base/Version.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.base 2 | 3 | /** 4 | * 用于比较 Android 版本字符串的类 5 | */ 6 | class Version(private val versionName: String) { 7 | 8 | private val version: List = 9 | versionName.split('.').mapNotNull(String::toIntOrNull) 10 | 11 | override fun toString() = versionName 12 | 13 | override fun hashCode(): Int = version.hashCode() 14 | 15 | override fun equals(other: Any?): Boolean = when (other) { 16 | null -> false 17 | !is Version -> false 18 | else -> this.version == other.version 19 | } 20 | 21 | operator fun compareTo(other: Version): Int { 22 | var result = 0 23 | when { 24 | this.version.size > other.version.size -> result = 1 25 | this.version.size < other.version.size -> result = -1 26 | } 27 | 28 | var index = 0 29 | while (index < this.version.size && index < other.version.size) { 30 | when { 31 | this.version[index] > other.version[index] -> return 1 32 | this.version[index] < other.version[index] -> return -1 33 | } 34 | index++ 35 | } 36 | 37 | return result 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/base/WaitChannel.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.base 2 | 3 | /** 4 | * 用 Java 实现的一个安全的 Wait Channel, 用来让若干线程安全地阻塞到事件结束 5 | */ 6 | class WaitChannel { 7 | @Volatile private var done = false 8 | private val channel = java.lang.Object() 9 | 10 | private val current: Long 11 | get() = System.currentTimeMillis() 12 | 13 | fun wait(timeout: Long = 0L): Boolean { 14 | if (done) return false 15 | 16 | val start = current 17 | synchronized(channel) { 18 | // 处理可能的 spurious wakeup 19 | while (!done && start + timeout > current) { 20 | channel.wait(start + timeout - current) 21 | } 22 | return true 23 | } 24 | } 25 | 26 | fun done() { 27 | if (done) return 28 | 29 | synchronized(channel) { 30 | done = true 31 | channel.notifyAll() 32 | } 33 | } 34 | 35 | fun isDone() = done 36 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/hookers/Activities.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.hookers 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import android.view.Menu 6 | import com.gh0u1l5.wechatmagician.spellbook.C 7 | import com.gh0u1l5.wechatmagician.spellbook.base.EventCenter 8 | import com.gh0u1l5.wechatmagician.spellbook.base.Hooker 9 | import com.gh0u1l5.wechatmagician.spellbook.interfaces.IActivityHook 10 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.Classes.MMActivity 11 | import de.robv.android.xposed.XC_MethodHook 12 | import de.robv.android.xposed.XposedHelpers.findAndHookMethod 13 | 14 | object Activities : EventCenter() { 15 | 16 | override val interfaces: List> 17 | get() = listOf(IActivityHook::class.java) 18 | 19 | override fun provideEventHooker(event: String) = when (event) { 20 | "onMMActivityOptionsMenuCreated" -> onCreateOptionsMenuHooker 21 | "onActivityCreating" -> onCreateHooker 22 | "onActivityStarting" -> onStartHooker 23 | "onActivityResuming" -> onResumeHooker 24 | else -> throw IllegalArgumentException("Unknown event: $event") 25 | } 26 | 27 | private val onCreateOptionsMenuHooker = Hooker { 28 | findAndHookMethod(MMActivity, "onCreateOptionsMenu", C.Menu, object : XC_MethodHook() { 29 | override fun afterHookedMethod(param: MethodHookParam) { 30 | val activity = param.thisObject as? Activity ?: return 31 | val menu = param.args[0] as? Menu ?: return 32 | notify("onMMActivityOptionsMenuCreated") { plugin -> 33 | (plugin as IActivityHook).onMMActivityOptionsMenuCreated(activity, menu) 34 | } 35 | } 36 | }) 37 | } 38 | 39 | private val onCreateHooker = Hooker { 40 | findAndHookMethod(C.Activity, "onCreate", C.Bundle, object : XC_MethodHook() { 41 | override fun beforeHookedMethod(param: MethodHookParam) { 42 | val activity = param.thisObject as? Activity ?: return 43 | val savedInstanceState = param.args[0] as Bundle? 44 | notify("onActivityCreating") { plugin -> 45 | (plugin as IActivityHook).onActivityCreating(activity, savedInstanceState) 46 | } 47 | } 48 | }) 49 | } 50 | 51 | private val onStartHooker = Hooker { 52 | findAndHookMethod(C.Activity, "onStart", object : XC_MethodHook() { 53 | override fun beforeHookedMethod(param: MethodHookParam) { 54 | val activity = param.thisObject as? Activity ?: return 55 | notify("onActivityStarting") { plugin -> 56 | (plugin as IActivityHook).onActivityStarting(activity) 57 | } 58 | } 59 | }) 60 | } 61 | 62 | private val onResumeHooker = Hooker { 63 | findAndHookMethod(C.Activity, "onResume", object : XC_MethodHook() { 64 | override fun beforeHookedMethod(param: MethodHookParam) { 65 | val activity = param.thisObject as? Activity ?: return 66 | notify("onActivityResuming") { plugin -> 67 | (plugin as IActivityHook).onActivityResuming(activity) 68 | } 69 | } 70 | }) 71 | } 72 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/hookers/Adapters.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.hookers 2 | 3 | import android.view.View 4 | import android.view.ViewGroup 5 | import android.widget.BaseAdapter 6 | import com.gh0u1l5.wechatmagician.spellbook.C 7 | import com.gh0u1l5.wechatmagician.spellbook.base.EventCenter 8 | import com.gh0u1l5.wechatmagician.spellbook.base.Hooker 9 | import com.gh0u1l5.wechatmagician.spellbook.interfaces.IAdapterHook 10 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.contact.Classes.AddressAdapter 11 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.conversation.Classes.ConversationWithCacheAdapter 12 | import de.robv.android.xposed.XC_MethodHook 13 | import de.robv.android.xposed.XposedBridge.hookAllConstructors 14 | import de.robv.android.xposed.XposedBridge.log 15 | import de.robv.android.xposed.XposedHelpers.findAndHookMethod 16 | 17 | object Adapters : EventCenter() { 18 | 19 | override val interfaces: List> 20 | get() = listOf(IAdapterHook::class.java) 21 | 22 | override fun provideEventHooker(event: String) = when (event) { 23 | "onAddressAdapterCreated" -> 24 | onAddressAdapterCreateHooker 25 | "onConversationAdapterCreated" -> 26 | onConversationWithCacheAdapterCreateHooker 27 | "onHeaderViewListAdapterGettingView", "onHeaderViewListAdapterGotView" -> 28 | onHeaderViewListAdapterGetViewHooker 29 | else -> 30 | throw IllegalArgumentException("Unknown event: $event") 31 | } 32 | 33 | private val onAddressAdapterCreateHooker = Hooker { 34 | hookAllConstructors(AddressAdapter, object : XC_MethodHook() { 35 | override fun afterHookedMethod(param: MethodHookParam) { 36 | val adapter = param.thisObject as? BaseAdapter 37 | if (adapter == null) { 38 | log("Expect address adapter to be BaseAdapter, get ${param.thisObject::class.java}") 39 | return 40 | } 41 | notify("onAddressAdapterCreated") { plugin -> 42 | (plugin as IAdapterHook).onAddressAdapterCreated(adapter) 43 | } 44 | } 45 | }) 46 | } 47 | 48 | private val onConversationWithCacheAdapterCreateHooker = Hooker { 49 | hookAllConstructors(ConversationWithCacheAdapter, object : XC_MethodHook() { 50 | override fun afterHookedMethod(param: MethodHookParam) { 51 | val adapter = param.thisObject as? BaseAdapter 52 | if (adapter == null) { 53 | log("Expect conversation adapter to be BaseAdapter, get ${param.thisObject::class.java}") 54 | return 55 | } 56 | notify("onConversationAdapterCreated") { plugin -> 57 | (plugin as IAdapterHook).onConversationAdapterCreated(adapter) 58 | } 59 | } 60 | }) 61 | } 62 | 63 | private val onHeaderViewListAdapterGetViewHooker = Hooker { 64 | findAndHookMethod( 65 | C.HeaderViewListAdapter, "getView", 66 | C.Int, C.View, C.ViewGroup, object : XC_MethodHook() { 67 | override fun beforeHookedMethod(param: MethodHookParam) { 68 | val adapter = param.thisObject 69 | val position = param.args[0] as Int 70 | val convertView = param.args[1] as View? 71 | val parent = param.args[2] as ViewGroup 72 | notifyForOperations("onHeaderViewListAdapterGettingView", param) { plugin -> 73 | (plugin as IAdapterHook).onHeaderViewListAdapterGettingView(adapter, position, convertView, parent) 74 | } 75 | } 76 | override fun afterHookedMethod(param: MethodHookParam) { 77 | val adapter = param.thisObject 78 | val position = param.args[0] as Int 79 | val convertView = param.args[1] as View? 80 | val parent = param.args[2] as ViewGroup 81 | val result = param.result as View? 82 | notifyForOperations("onHeaderViewListAdapterGotView", param) { plugin -> 83 | (plugin as IAdapterHook).onHeaderViewListAdapterGotView(adapter, position, convertView, parent, result) 84 | } 85 | } 86 | }) 87 | } 88 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/hookers/FileSystem.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.hookers 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.C 4 | import com.gh0u1l5.wechatmagician.spellbook.base.EventCenter 5 | import com.gh0u1l5.wechatmagician.spellbook.base.Hooker 6 | import com.gh0u1l5.wechatmagician.spellbook.interfaces.IFileSystemHook 7 | import de.robv.android.xposed.XC_MethodHook 8 | import de.robv.android.xposed.XposedHelpers.findAndHookConstructor 9 | import de.robv.android.xposed.XposedHelpers.findAndHookMethod 10 | import java.io.File 11 | 12 | object FileSystem : EventCenter() { 13 | 14 | override val interfaces: List> 15 | get() = listOf(IFileSystemHook::class.java) 16 | 17 | override fun provideEventHooker(event: String): Hooker? { 18 | return when (event) { 19 | "onFileDeleting", "onFileDeleted" -> onDeleteHooker 20 | "onFileReading" -> onReadHooker 21 | "onFileWriting" -> onWriteHooker 22 | else -> throw IllegalArgumentException("Unknown event: $event") 23 | } 24 | } 25 | 26 | private val onDeleteHooker = Hooker { 27 | findAndHookMethod(C.File, "delete", object : XC_MethodHook() { 28 | override fun beforeHookedMethod(param: MethodHookParam) { 29 | val file = param.thisObject as File 30 | notifyForOperations("onFileDeleting", param) { plugin -> 31 | (plugin as IFileSystemHook).onFileDeleting(file) 32 | } 33 | } 34 | override fun afterHookedMethod(param: MethodHookParam) { 35 | val file = param.thisObject as File 36 | val result = param.result as Boolean 37 | notifyForOperations("onFileDeleted", param) { plugin -> 38 | (plugin as IFileSystemHook).onFileDeleted(file, result) 39 | } 40 | } 41 | }) 42 | } 43 | 44 | private val onReadHooker = Hooker { 45 | findAndHookConstructor(C.FileInputStream, C.File, object : XC_MethodHook() { 46 | override fun beforeHookedMethod(param: MethodHookParam) { 47 | val file = param.args[0] as File 48 | notify("onFileReading") { plugin -> 49 | (plugin as IFileSystemHook).onFileReading(file) 50 | } 51 | } 52 | }) 53 | } 54 | 55 | private val onWriteHooker = Hooker { 56 | findAndHookConstructor(C.FileOutputStream, C.File, C.Boolean, object : XC_MethodHook() { 57 | override fun beforeHookedMethod(param: MethodHookParam) { 58 | val file = param.args[0] as File 59 | val append = param.args[1] as Boolean 60 | notify("onFileWriting") { plugin -> 61 | (plugin as IFileSystemHook).onFileWriting(file, append) 62 | } 63 | } 64 | }) 65 | } 66 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/hookers/Notifications.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.hookers 2 | 3 | import android.os.Message 4 | import com.gh0u1l5.wechatmagician.spellbook.C 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatStatus 6 | import com.gh0u1l5.wechatmagician.spellbook.base.EventCenter 7 | import com.gh0u1l5.wechatmagician.spellbook.base.Hooker 8 | import com.gh0u1l5.wechatmagician.spellbook.interfaces.INotificationHook 9 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.booter.notification.Classes.MMNotification_MessageHandler 10 | import de.robv.android.xposed.XC_MethodHook 11 | import de.robv.android.xposed.XposedHelpers.findAndHookMethod 12 | 13 | object Notifications : EventCenter() { 14 | 15 | override val interfaces: List> 16 | get() = listOf(INotificationHook::class.java) 17 | 18 | override fun provideEventHooker(event: String): Hooker? { 19 | return when (event) { 20 | "onMessageHandling", "onMessageHandled" -> MessageHandlerHooker 21 | else -> throw IllegalArgumentException("Unknown event: $event") 22 | } 23 | } 24 | 25 | private val MessageHandlerHooker = Hooker { 26 | findAndHookMethod(MMNotification_MessageHandler, "handleMessage", C.Message, object : XC_MethodHook() { 27 | override fun beforeHookedMethod(param: MethodHookParam) { 28 | val raw = param.args[0] as? Message ?: return 29 | val talker = raw.data.getString("notification.show.talker") ?: return 30 | val content = raw.data.getString("notification.show.message.content") ?: return 31 | val type = raw.data.getInt("notification.show.message.type") 32 | val tipsFlag = raw.data.getInt("notification.show.tipsflag") 33 | notifyForBypassFlags("onMessageHandling", param) { plugin -> 34 | (plugin as INotificationHook).onMessageHandling( 35 | INotificationHook.Message(talker, content, type, tipsFlag)) 36 | } 37 | } 38 | override fun afterHookedMethod(param: MethodHookParam) { 39 | val raw = param.args[0] as? Message ?: return 40 | val talker = raw.data.getString("notification.show.talker") ?: return 41 | val content = raw.data.getString("notification.show.message.content") ?: return 42 | val type = raw.data.getInt("notification.show.message.type") 43 | val tipsFlag = raw.data.getInt("notification.show.tipsflag") 44 | notify("onMessageHandled") { plugin -> 45 | (plugin as INotificationHook).onMessageHandled( 46 | INotificationHook.Message(talker, content, type, tipsFlag)) 47 | } 48 | } 49 | }) 50 | 51 | WechatStatus.toggle(WechatStatus.StatusFlag.STATUS_FLAG_NOTIFICATIONS) 52 | } 53 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/hookers/SearchBar.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.hookers 2 | 3 | import android.content.Context.INPUT_METHOD_SERVICE 4 | import android.text.Editable 5 | import android.text.TextWatcher 6 | import android.view.inputmethod.InputMethodManager 7 | import android.widget.EditText 8 | import com.gh0u1l5.wechatmagician.spellbook.WechatStatus 9 | import com.gh0u1l5.wechatmagician.spellbook.base.EventCenter 10 | import com.gh0u1l5.wechatmagician.spellbook.base.Hooker 11 | import com.gh0u1l5.wechatmagician.spellbook.interfaces.ISearchBarConsole 12 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.tools.Classes.ActionBarEditText 13 | import de.robv.android.xposed.XC_MethodHook 14 | import de.robv.android.xposed.XposedBridge.hookAllConstructors 15 | 16 | object SearchBar : EventCenter() { 17 | 18 | override val interfaces: List> 19 | get() = listOf(ISearchBarConsole::class.java) 20 | 21 | override fun provideEventHooker(event: String): Hooker? { 22 | return when (event) { 23 | "onHandleCommand" -> SearchBarHooker 24 | else -> throw IllegalArgumentException("Unknown event: $event") 25 | } 26 | } 27 | 28 | private val SearchBarHooker = Hooker { 29 | hookAllConstructors(ActionBarEditText, object : XC_MethodHook() { 30 | override fun afterHookedMethod(param: MethodHookParam) { 31 | val search = param.thisObject as EditText 32 | search.addTextChangedListener(object : TextWatcher { 33 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit 34 | 35 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit 36 | 37 | override fun afterTextChanged(editable: Editable?) { 38 | val context = search.context 39 | var command = editable.toString() 40 | if (command.startsWith("#") && command.endsWith("#")) { 41 | command = command.drop(1).dropLast(1) 42 | notifyParallel("onHandleCommand") { plugin -> 43 | val consumed = (plugin as ISearchBarConsole).onHandleCommand(context, command) 44 | if (consumed) { 45 | // Hide Input Method 46 | val imm = search.context.getSystemService(INPUT_METHOD_SERVICE) 47 | (imm as InputMethodManager).hideSoftInputFromWindow(search.windowToken, 0) 48 | } 49 | } 50 | } 51 | } 52 | }) 53 | } 54 | }) 55 | 56 | WechatStatus.toggle(WechatStatus.StatusFlag.STATUS_FLAG_COMMAND) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/hookers/Storage.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.hookers 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.base.EventCenter 4 | import com.gh0u1l5.wechatmagician.spellbook.base.Hooker 5 | import com.gh0u1l5.wechatmagician.spellbook.interfaces.IImageStorageHook 6 | import com.gh0u1l5.wechatmagician.spellbook.interfaces.IMessageStorageHook 7 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.Classes.ImgInfoStorage 8 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.Methods.ImgInfoStorage_load 9 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.storage.Classes.MsgInfoStorage 10 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.storage.Methods.MsgInfoStorage_insert 11 | import de.robv.android.xposed.XC_MethodHook 12 | import de.robv.android.xposed.XposedBridge.hookAllConstructors 13 | import de.robv.android.xposed.XposedBridge.hookMethod 14 | import de.robv.android.xposed.XposedHelpers.getLongField 15 | 16 | object Storage : EventCenter() { 17 | 18 | override val interfaces: List> 19 | get() = listOf(IMessageStorageHook::class.java, IImageStorageHook::class.java) 20 | 21 | override fun provideEventHooker(event: String): Hooker? { 22 | return when (event) { 23 | "onMessageStorageCreated" -> onMessageStorageCreateHooker 24 | "onMessageStorageInserting", "onMessageStorageInserted" -> onMessageStorageInsertHooker 25 | "onImageStorageCreated" -> onImageStorageCreateHooker 26 | "onImageStorageLoading", "onImageStorageLoaded" -> onImageStorageLoadHooker 27 | else -> throw IllegalArgumentException("Unknown event: $event") 28 | } 29 | } 30 | 31 | private val onMessageStorageCreateHooker = Hooker { 32 | hookAllConstructors(MsgInfoStorage, object : XC_MethodHook() { 33 | override fun afterHookedMethod(param: MethodHookParam) { 34 | notify("onMessageStorageCreated") { plugin -> 35 | (plugin as IMessageStorageHook).onMessageStorageCreated(param.thisObject) 36 | } 37 | } 38 | }) 39 | } 40 | 41 | private val onMessageStorageInsertHooker = Hooker { 42 | hookMethod(MsgInfoStorage_insert, object : XC_MethodHook() { 43 | override fun beforeHookedMethod(param: MethodHookParam) { 44 | val msgObject = param.args[0] 45 | val msgId = getLongField(msgObject, "field_msgId") 46 | notifyForBypassFlags("onMessageStorageInserting", param) { plugin -> 47 | (plugin as IMessageStorageHook).onMessageStorageInserting(msgId, msgObject) 48 | } 49 | } 50 | override fun afterHookedMethod(param: MethodHookParam) { 51 | val msgObject = param.args[0] 52 | val msgId = getLongField(msgObject, "field_msgId") 53 | notify("onMessageStorageInserted") { plugin -> 54 | (plugin as IMessageStorageHook).onMessageStorageInserted(msgId, msgObject) 55 | } 56 | } 57 | }) 58 | } 59 | 60 | private val onImageStorageCreateHooker = Hooker { 61 | hookAllConstructors(ImgInfoStorage, object : XC_MethodHook() { 62 | override fun afterHookedMethod(param: MethodHookParam) { 63 | notify("onImageStorageCreated") { plugin -> 64 | (plugin as IImageStorageHook).onImageStorageCreated(param.thisObject) 65 | } 66 | } 67 | }) 68 | } 69 | 70 | private val onImageStorageLoadHooker = Hooker { 71 | hookMethod(ImgInfoStorage_load, object : XC_MethodHook() { 72 | override fun beforeHookedMethod(param: MethodHookParam) { 73 | val imageId = param.args[0] as String? 74 | val prefix = param.args[1] as String? 75 | val suffix = param.args[2] as String? 76 | notifyForBypassFlags("onImageStorageLoading", param) { plugin -> 77 | (plugin as IImageStorageHook).onImageStorageLoading(imageId, prefix, suffix) 78 | } 79 | } 80 | override fun afterHookedMethod(param: MethodHookParam) { 81 | val imageId = param.args[0] as String? 82 | val prefix = param.args[1] as String? 83 | val suffix = param.args[2] as String? 84 | notify("onImageStorageLoaded") { plugin -> 85 | (plugin as IImageStorageHook).onImageStorageLoaded(imageId, prefix, suffix) 86 | } 87 | } 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/hookers/UriRouter.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.hookers 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatStatus 6 | import com.gh0u1l5.wechatmagician.spellbook.base.EventCenter 7 | import com.gh0u1l5.wechatmagician.spellbook.base.Hooker 8 | import com.gh0u1l5.wechatmagician.spellbook.interfaces.IUriRouterHook 9 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.plugin.base.stub.Methods.WXCustomScheme_entry 10 | import de.robv.android.xposed.XC_MethodHook 11 | import de.robv.android.xposed.XposedBridge.hookMethod 12 | 13 | object UriRouter : EventCenter() { 14 | 15 | override val interfaces: List> 16 | get() = listOf(IUriRouterHook::class.java) 17 | 18 | override fun provideEventHooker(event: String): Hooker? { 19 | return when (event) { 20 | "onReceiveUri" -> UriRouterHooker 21 | else -> throw IllegalArgumentException("Unknown event: $event") 22 | } 23 | } 24 | 25 | private val UriRouterHooker = Hooker { 26 | hookMethod(WXCustomScheme_entry, object : XC_MethodHook() { 27 | override fun beforeHookedMethod(param: MethodHookParam) { 28 | val intent = param.args[0] as Intent? 29 | val uri = intent?.data ?: return 30 | val activity = param.thisObject as Activity 31 | if (uri.host == "magician") { 32 | notifyParallel("onReceiveUri") { plugin -> 33 | (plugin as IUriRouterHook).onReceiveUri(activity, uri) 34 | } 35 | param.result = false 36 | } 37 | } 38 | }) 39 | 40 | WechatStatus.toggle(WechatStatus.StatusFlag.STATUS_FLAG_URI_ROUTER) 41 | } 42 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/hookers/XLog.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.hookers 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.base.EventCenter 4 | import com.gh0u1l5.wechatmagician.spellbook.base.Hooker 5 | import com.gh0u1l5.wechatmagician.spellbook.interfaces.IXLogHook 6 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mars.xlog.Constants.toHumanReadableLevel 7 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mars.xlog.Methods.Xlog_logWrite2 8 | import de.robv.android.xposed.XC_MethodHook 9 | import de.robv.android.xposed.XposedBridge.hookMethod 10 | 11 | object XLog : EventCenter() { 12 | override val interfaces: List> 13 | get() = listOf(IXLogHook::class.java) 14 | 15 | override fun provideEventHooker(event: String) = when (event) { 16 | "onXLogWrite" -> onXLogWriteHooker 17 | else -> throw IllegalArgumentException("Unknown event: $event") 18 | } 19 | 20 | private val onXLogWriteHooker = Hooker { 21 | hookMethod(Xlog_logWrite2, object : XC_MethodHook() { 22 | override fun afterHookedMethod(param: MethodHookParam) { 23 | val level = toHumanReadableLevel(param.args[0] as Int) 24 | val tag = param.args[1] as String? ?: "" 25 | val msg = param.args[8] as String? ?: "" 26 | notifyParallel("onXLogWrite", { plugin -> 27 | (plugin as IXLogHook).onXLogWrite(level, tag, msg) 28 | }) 29 | } 30 | }) 31 | } 32 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/hookers/XmlParser.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.hookers 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.WechatStatus 4 | import com.gh0u1l5.wechatmagician.spellbook.base.EventCenter 5 | import com.gh0u1l5.wechatmagician.spellbook.base.Hooker 6 | import com.gh0u1l5.wechatmagician.spellbook.interfaces.IXmlParserHook 7 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.sdk.platformtools.Methods.XmlParser_parse 8 | import de.robv.android.xposed.XC_MethodHook 9 | import de.robv.android.xposed.XposedBridge.hookMethod 10 | 11 | object XmlParser : EventCenter() { 12 | 13 | override val interfaces: List> 14 | get() = listOf(IXmlParserHook::class.java) 15 | 16 | override fun provideEventHooker(event: String): Hooker? { 17 | return when (event) { 18 | "onXmlParsing", "onXmlParsed" -> onXmlParseHooker 19 | else -> throw IllegalArgumentException("Unknown event: $event") 20 | } 21 | } 22 | 23 | private val onXmlParseHooker = Hooker { 24 | hookMethod(XmlParser_parse, object : XC_MethodHook() { 25 | override fun beforeHookedMethod(param: MethodHookParam) { 26 | val xml = param.args[0] as String 27 | val root = param.args[1] as String 28 | notifyForOperations("onXmlParsing", param) { plugin -> 29 | (plugin as IXmlParserHook).onXmlParsing(xml, root) 30 | } 31 | } 32 | @Suppress("UNCHECKED_CAST") 33 | override fun afterHookedMethod(param: MethodHookParam) { 34 | val xml = param.args[0] as String 35 | val root = param.args[1] as String 36 | val result = param.result as MutableMap? ?: return 37 | notify("onXmlParsed") { plugin -> 38 | (plugin as IXmlParserHook).onXmlParsed(xml, root, result) 39 | } 40 | } 41 | }) 42 | 43 | WechatStatus.toggle(WechatStatus.StatusFlag.STATUS_FLAG_XML_PARSER) 44 | } 45 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/interfaces/IActivityHook.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.interfaces 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import android.view.Menu 6 | 7 | interface IActivityHook { 8 | 9 | /** 10 | * Called when a Wechat MMActivity has created a options menu. 11 | * 12 | * @param activity the activity shown in foreground. 13 | * @param menu the options menu just created by the activity. 14 | */ 15 | fun onMMActivityOptionsMenuCreated(activity: Activity, menu: Menu) { } 16 | 17 | /** 18 | * Called when an Activity is going to invoke [Activity.onCreate] method. 19 | * 20 | * @param activity the activity object that is creating. 21 | * @param savedInstanceState the saved instance state for restoring the state. 22 | */ 23 | fun onActivityCreating(activity: Activity, savedInstanceState: Bundle?) { } 24 | 25 | /** 26 | * Called when an activity is going to invoke [Activity.onStart] method. 27 | * 28 | * @param activity the activity object that is starting. 29 | */ 30 | fun onActivityStarting(activity: Activity) { } 31 | 32 | /** 33 | * Called when an activity is going to invoke [Activity.onResume] method. 34 | * 35 | * @param activity the activity object that is resuming. 36 | */ 37 | fun onActivityResuming(activity: Activity) { } 38 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/interfaces/IAdapterHook.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.interfaces 2 | 3 | import android.view.View 4 | import android.view.ViewGroup 5 | import android.widget.Adapter 6 | import android.widget.BaseAdapter 7 | import com.gh0u1l5.wechatmagician.spellbook.base.Operation 8 | import com.gh0u1l5.wechatmagician.spellbook.base.Operation.Companion.nop 9 | 10 | interface IAdapterHook { 11 | 12 | /** 13 | * Called when a Wechat AddressAdapter has been created. This adapter will be used in the 14 | * ListView for all the contacts (which is shown in the second tab of Wechat). 15 | * 16 | * @param adapter the created AddressAdapter object. 17 | */ 18 | fun onAddressAdapterCreated(adapter: BaseAdapter) { } 19 | 20 | /** 21 | * Called when a Wechat ConversationAdapter has been created. This adapter will be used in the 22 | * ListView for all the conversations (which is shown in the first tab of Wechat). 23 | * 24 | * @param adapter the created ConversationAdapter object. 25 | */ 26 | fun onConversationAdapterCreated(adapter: BaseAdapter) { } 27 | 28 | /** 29 | * Called when an HeaderViewListAdapter object is going to invoke [Adapter.getView] method. 30 | * 31 | * @param adapter the HeaderViewListAdapter object calling the getView(). 32 | * @param position the position of the item whose view we want. 33 | * @param convertView the old view to reuse, if possible. 34 | * @param parent the parent that this view will eventually be attached to. 35 | * @return to bypass the original method, return a View object wrapped by [Operation.replacement] 36 | * or a throwable wrapped by [Operation.interruption], otherwise return [Operation.nop]. 37 | */ 38 | fun onHeaderViewListAdapterGettingView(adapter: Any, position: Int, convertView: View?, parent: ViewGroup): Operation = nop() 39 | 40 | /** 41 | * Called when an HeaderViewListAdapter object has returned from [Adapter.getView] method. 42 | * 43 | * @param adapter the HeaderViewListAdapter object calling the getView(). 44 | * @param position the position of the item whose view we want. 45 | * @param convertView the old view to reuse, if possible. 46 | * @param parent the parent that this view will eventually be attached to. 47 | * @param result the view that is returned by the original getView() function. 48 | * @return to replace the original result, return a View object wrapped by [Operation.replacement], 49 | * otherwise return [Operation.nop]. 50 | */ 51 | fun onHeaderViewListAdapterGotView(adapter: Any, position: Int, convertView: View?, parent: ViewGroup, result: View?): Operation = nop() 52 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/interfaces/IFileSystemHook.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.interfaces 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.base.Operation 4 | import com.gh0u1l5.wechatmagician.spellbook.base.Operation.Companion.nop 5 | import java.io.File 6 | import java.io.FileInputStream 7 | import java.io.FileOutputStream 8 | 9 | interface IFileSystemHook { 10 | 11 | /** 12 | * Called when a File object is going to invoke [File.delete] 13 | * 14 | * @param file the [File] object calling [File.delete] 15 | * @return to bypass the original method, return a Boolean wrapped by [Operation.replacement], 16 | * otherwise return [Operation.nop]. 17 | */ 18 | fun onFileDeleting(file: File): Operation = nop() 19 | 20 | /** 21 | * Called when a File object has returned from [File.delete] 22 | * 23 | * @param file the [File] object calling [File.delete] 24 | * @param result the result returned by [File.delete] 25 | * @return to replace the original result, return a Boolean wrapped by [Operation.replacement], 26 | * otherwise return [Operation.nop]. 27 | */ 28 | fun onFileDeleted(file: File, result: Boolean): Operation = nop() 29 | 30 | /** 31 | * Called when a [FileInputStream] object is being created. 32 | * 33 | * @param file the [File] object the stream reading from. 34 | */ 35 | fun onFileReading(file: File) { } 36 | 37 | /** 38 | * Called when a [FileOutputStream] object is being created. 39 | * 40 | * @param file the [File] object the stream writing to 41 | * @param append if `true`, then bytes will be written to the end rather than the beginning 42 | */ 43 | fun onFileWriting(file: File, append: Boolean) { } 44 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/interfaces/IImageStorageHook.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.interfaces 2 | 3 | interface IImageStorageHook { 4 | 5 | /** 6 | * Called when an ImgInfoStorage object has been created. 7 | * 8 | * @param storage the ImgInfoStorage object created by a constructor. 9 | */ 10 | fun onImageStorageCreated(storage: Any) { } 11 | 12 | /** 13 | * Called when an ImgInfoStorage is loading a new image information. 14 | * 15 | * @param imageId the internal ID of the image. 16 | * @param prefix 17 | * @param suffix 18 | * @return to bypass the original method, return `true`, otherwise return `false`. 19 | */ 20 | fun onImageStorageLoading(imageId: String?, prefix: String?, suffix: String?) = false 21 | 22 | /** 23 | * Called when an ImgInfoStorage has loaded a new image information. 24 | * 25 | * @param imageId the internal ID of the image. 26 | * @param prefix 27 | * @param suffix 28 | */ 29 | fun onImageStorageLoaded(imageId: String?, prefix: String?, suffix: String?) { } 30 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/interfaces/IMessageStorageHook.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.interfaces 2 | 3 | interface IMessageStorageHook { 4 | 5 | /** 6 | * Called when an MsgInfoStorage object has been created. 7 | * 8 | * @param storage the MsgInfoStorage object created by a constructor. 9 | */ 10 | fun onMessageStorageCreated(storage: Any) { } 11 | 12 | /** 13 | * Called when an MsgInfoStorage is inserting a new message. 14 | * 15 | * @param msgId the internal ID of the message. 16 | * @param msgObject the message object stored into storage. 17 | * @return to bypass the original method, return `true`, otherwise return `false`. 18 | */ 19 | fun onMessageStorageInserting(msgId: Long, msgObject: Any) = false 20 | 21 | /** 22 | * Called when an MsgInfoStorage has inserted a new message. 23 | * 24 | * @param msgId the internal ID of the message. 25 | * @param msgObject the message object stored into storage. 26 | */ 27 | fun onMessageStorageInserted(msgId: Long, msgObject: Any) { } 28 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/interfaces/INotificationHook.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.interfaces 2 | 3 | interface INotificationHook { 4 | 5 | /** 6 | * The bean class that describes a notification message. 7 | */ 8 | data class Message( 9 | val talker: String, 10 | val content: String, 11 | val type: Int, 12 | val tipsFlag: Int 13 | ) 14 | 15 | /** 16 | * Called when the global message handler is going to handle a [Message]. 17 | * 18 | * @param message the message received by the handler. 19 | * @return to prevent showing the message, return `true`, otherwise return `false` 20 | */ 21 | fun onMessageHandling(message: Message) = false 22 | 23 | /** 24 | * Called when the global message handler has handled a [Message]. 25 | * 26 | * @param message the message received by the handler. 27 | */ 28 | fun onMessageHandled(message: Message) { } 29 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/interfaces/IPopupMenuHook.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.interfaces 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.hookers.MenuAppender 4 | 5 | interface IPopupMenuHook { 6 | 7 | /** 8 | * Called when the popup menu for a contact is going to be created. 9 | * 10 | * @param username the username of the contact that has been long clicking. 11 | * @return to add your own menu items, return a list of [MenuAppender.PopupMenuItem], otherwise 12 | * return null. 13 | */ 14 | fun onPopupMenuForContactsCreating(username: String): List? = null 15 | 16 | /** 17 | * Called when the popup menu for a conversation is going to be created. 18 | * 19 | * @param username the username of the conversation that has been long clicking. 20 | * @return to add your own menu items, return a list of [MenuAppender.PopupMenuItem], otherwise 21 | * return null. 22 | */ 23 | fun onPopupMenuForConversationsCreating(username: String): List? = null 24 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/interfaces/ISearchBarConsole.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.interfaces 2 | 3 | import android.content.Context 4 | 5 | interface ISearchBarConsole { 6 | /** 7 | * Called when the user entered a command in the search bar. 8 | * 9 | * @param context a [Context] that can be used for later operations. 10 | * @param command the commend entered by the user. 11 | * @return true if the command should be consumed, otherwise return false. 12 | */ 13 | fun onHandleCommand(context: Context, command: String) = false 14 | } 15 | -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/interfaces/IUriRouterHook.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.interfaces 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.net.Uri 6 | 7 | interface IUriRouterHook { 8 | /** 9 | * Called when the URI router get a new request 10 | * 11 | * @param activity an activity that can be used as a [Context]. 12 | * @param uri the uri sent from the other applications. It should start with "weixin://magician/". 13 | */ 14 | fun onReceiveUri(activity: Activity, uri: Uri) { } 15 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/interfaces/IXLogHook.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.interfaces 2 | 3 | interface IXLogHook { 4 | /** 5 | * Called when Tencent XLog has written some log information. 6 | * 7 | * @param level a human readable log level. 8 | * @param tag a human readable log tag. 9 | * @param msg the message printed to the output. 10 | */ 11 | fun onXLogWrite(level: String, tag: String, msg: String) { } 12 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/interfaces/IXmlParserHook.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.interfaces 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.base.Operation 4 | import com.gh0u1l5.wechatmagician.spellbook.base.Operation.Companion.nop 5 | 6 | interface IXmlParserHook { 7 | 8 | /** 9 | * Called when the XML parser is going to parse a XML string. 10 | * 11 | * @param xml the XML string given by the caller. 12 | * @param root the tag name of the section the caller want to parse. 13 | * @return to bypass the original method, return a MutableMap object wrapped by 14 | * [Operation.replacement], or a throwable wrapped by [Operation.interruption], otherwise return 15 | * [Operation.nop]. 16 | */ 17 | fun onXmlParsing(xml: String, root: String): Operation?> = nop() 18 | 19 | /** 20 | * Called when the XML parser has parsed a XML string. 21 | * 22 | * @param xml the XML string given by the caller. 23 | * @param root the tag name of the section the caller want to parse. 24 | * @param result the map generated from the XML string. 25 | */ 26 | fun onXmlParsed(xml: String, root: String, result: MutableMap) { } 27 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/MirrorClasses.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror 2 | 3 | /** 4 | * Dynamically generated by build.gradle 5 | */ 6 | val MirrorClasses = listOf ( 7 | com.gh0u1l5.wechatmagician.spellbook.mirror.android.support.v4.app.Classes, 8 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mars.xlog.Classes, 9 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.Classes, 10 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.booter.notification.Classes, 11 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.booter.notification.queue.Classes, 12 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.modelsfs.Classes, 13 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.plugin.base.stub.Classes, 14 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.plugin.gallery.ui.Classes, 15 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.plugin.remittance.ui.Classes, 16 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.plugin.sns.ui.Classes, 17 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.plugin.webwx.ui.Classes, 18 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.sdk.platformtools.Classes, 19 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.storage.Classes, 20 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.Classes, 21 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.base.Classes, 22 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.chatting.Classes, 23 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.contact.Classes, 24 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.conversation.Classes, 25 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.tools.Classes, 26 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.transmit.Classes, 27 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.wcdb.Classes, 28 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.wcdb.database.Classes, 29 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.wcdb.support.Classes 30 | ) 31 | -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/MirrorFields.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror 2 | 3 | /** 4 | * Dynamically generated by build.gradle 5 | */ 6 | val MirrorFields = listOf ( 7 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.Fields, 8 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.plugin.sns.ui.Fields 9 | ) 10 | -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/MirrorMethods.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror 2 | 3 | /** 4 | * Dynamically generated by build.gradle 5 | */ 6 | val MirrorMethods = listOf ( 7 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mars.xlog.Methods, 8 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.Methods, 9 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.booter.notification.queue.Methods, 10 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.modelsfs.Methods, 11 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.plugin.base.stub.Methods, 12 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.sdk.platformtools.Methods, 13 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.storage.Methods, 14 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.Methods, 15 | com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.transmit.Methods 16 | ) 17 | -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/android/support/v4/app/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.android.support.v4.app 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxClasses 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 6 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassesFromPackage 7 | 8 | object Classes { 9 | val NotificationManagerCompat: Class<*> by wxLazy("NotificationManagerCompat") { 10 | // v4切换成 androidx 兼容包 11 | findClassesFromPackage(wxLoader!!, wxClasses!!, "androidx.core.app") 12 | .filterByField("android.app.NotificationManager") 13 | .firstOrNull() 14 | } 15 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mars/xlog/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mars.xlog 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 5 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassIfExists 6 | 7 | /** 8 | * 路径正确 9 | */ 10 | object Classes { 11 | val Xlog: Class<*> by wxLazy("Xlog") { 12 | findClassIfExists("com.tencent.mars.xlog.Xlog", wxLoader!!) 13 | } 14 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mars/xlog/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mars.xlog 2 | 3 | /** 4 | * 常量正确 5 | */ 6 | object Constants { 7 | const val Xlog_AppenderModeAsync = 0 8 | const val Xlog_AppenderModeSync = 1 9 | 10 | const val Xlog_LEVEL_ALL = 0 11 | const val Xlog_LEVEL_VERBOSE = 0 12 | const val Xlog_LEVEL_DEBUG = 1 13 | const val Xlog_LEVEL_INFO = 2 14 | const val Xlog_LEVEL_WARNING = 3 15 | const val Xlog_LEVEL_ERROR = 4 16 | const val Xlog_LEVEL_FATAL = 5 17 | const val Xlog_LEVEL_NONE = 6 18 | 19 | fun toHumanReadableLevel(level: Int): String = when (level) { 20 | Xlog_LEVEL_VERBOSE -> "VERBOSE" 21 | Xlog_LEVEL_DEBUG -> "DEBUG" 22 | Xlog_LEVEL_INFO -> "INFO" 23 | Xlog_LEVEL_WARNING -> "WARNING" 24 | Xlog_LEVEL_ERROR -> "ERROR" 25 | Xlog_LEVEL_FATAL -> "FATAL" 26 | Xlog_LEVEL_NONE -> "NONE" 27 | else -> "UNKNOWN($level)" 28 | } 29 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mars/xlog/Methods.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mars.xlog 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 4 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mars.xlog.Classes.Xlog 5 | import java.lang.reflect.Method 6 | 7 | /** 8 | * 方法正确 9 | */ 10 | object Methods { 11 | val Xlog_logWrite: Method by wxLazy("Xlog_logWrite") { 12 | Xlog.declaredMethods.find { it.name == "logWrite" } 13 | } 14 | 15 | val Xlog_logWrite2: Method by wxLazy("Xlog_logWrite2") { 16 | Xlog.declaredMethods.find { it.name == "logWrite2" } 17 | } 18 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.C 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxClasses 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 6 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 7 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxPackageName 8 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.sdk.platformtools.Classes.LruCache 9 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassesFromPackage 10 | 11 | object Classes { 12 | val ImgInfoStorage: Class<*> by wxLazy("ImgInfoStorage") { 13 | findClassesFromPackage(wxLoader!!, wxClasses!!, wxPackageName, 1) 14 | .filterByMethod(C.String, C.String, C.String, C.String, C.Boolean) 15 | .firstOrNull() 16 | } 17 | 18 | val LruCacheWithListener: Class<*> by wxLazy("LruCacheWithListener") { 19 | findClassesFromPackage(wxLoader!!, wxClasses!!, wxPackageName, 1) 20 | .filterBySuper(LruCache) 21 | .firstOrNull() 22 | } 23 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/Fields.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 4 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.Classes.ImgInfoStorage 5 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.Classes.LruCacheWithListener 6 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findFieldsWithGenericType 7 | import java.lang.reflect.Field 8 | 9 | object Fields { 10 | val ImgInfoStorage_mBitmapCache: Field by wxLazy("ImgInfoStorage_mBitmapCache") { 11 | findFieldsWithGenericType( 12 | ImgInfoStorage, "${LruCacheWithListener.canonicalName}") 13 | .firstOrNull()?.apply { isAccessible = true } 14 | } 15 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/Methods.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.C 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 5 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.Classes.ImgInfoStorage 6 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.Classes.LruCacheWithListener 7 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findMethodsByExactParameters 8 | import java.lang.reflect.Method 9 | 10 | object Methods { 11 | val ImgInfoStorage_load: Method by wxLazy("ImgInfoStorage_load") { 12 | findMethodsByExactParameters(ImgInfoStorage, C.String, C.String, C.String, C.String, C.Boolean) 13 | .firstOrNull()?.apply { isAccessible = true } 14 | } 15 | 16 | val LruCacheWithListener_put: Method by wxLazy("LruCacheWithListener_put") { 17 | findMethodsByExactParameters(LruCacheWithListener, null, C.Object, C.Object) 18 | .firstOrNull()?.apply { isAccessible = true } 19 | } 20 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/booter/notification/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.booter.notification 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.C 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxClasses 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 6 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 7 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxPackageName 8 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassIfExists 9 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassesFromPackage 10 | 11 | object Classes { 12 | 13 | val MMNotification: Class<*> by wxLazy("MMNotification") { 14 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.booter.notification") 15 | .filterByField("talker") 16 | .firstOrNull() 17 | } 18 | 19 | val MMNotification_MessageHandler: Class<*> by wxLazy("MMNotification_MessageHandler") { 20 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.booter.notification") 21 | .filterByMethod(null, "handleMessage", C.Message) 22 | .firstOrNull() 23 | } 24 | 25 | val NotificationItem: Class<*> by wxLazy("NotificationItem") { 26 | findClassIfExists("$wxPackageName.booter.notification.NotificationItem", wxLoader!!) 27 | } 28 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/booter/notification/queue/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.booter.notification.queue 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.C 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxClasses 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 6 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 7 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxPackageName 8 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.booter.notification.Classes.NotificationItem 9 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassesFromPackage 10 | 11 | object Classes { 12 | val NotificationAppMsgQueue: Class<*> by wxLazy("NotificationAppMsgQueue") { 13 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.booter.notification.queue") 14 | .filterByMethod(null, NotificationItem) 15 | .filterByMethod(C.Iterator, "iterator") 16 | .firstOrNull() 17 | } 18 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/booter/notification/queue/Methods.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.booter.notification.queue 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.C 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 5 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.booter.notification.Classes.NotificationItem 6 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.booter.notification.queue.Classes.NotificationAppMsgQueue 7 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findMethodsByExactParameters 8 | import java.lang.reflect.Method 9 | 10 | object Methods { 11 | val NotificationAppMsgQueue_add: Method by wxLazy("NotificationAppMsgQueue_add") { 12 | findMethodsByExactParameters(NotificationAppMsgQueue, null, NotificationItem) 13 | .firstOrNull()?.apply { isAccessible = true } 14 | } 15 | val NotificationAppMsgQueue_remove: Method by wxLazy("NotificationAppMsgQueue_remove") { 16 | findMethodsByExactParameters(NotificationAppMsgQueue, C.Boolean, C.String) 17 | .firstOrNull()?.apply { isAccessible = true } 18 | } 19 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/modelsfs/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.modelsfs 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.C 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxClasses 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 6 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 7 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxPackageName 8 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassesFromPackage 9 | 10 | object Classes { 11 | val EncEngine: Class<*> by wxLazy("EncEngine") { 12 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.modelsfs") 13 | .filterByMethod(null, "seek", C.Long) 14 | .filterByMethod(null, "free") 15 | .firstOrNull() 16 | } 17 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/modelsfs/Methods.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.modelsfs 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.C 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 5 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.modelsfs.Classes.EncEngine 6 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findMethodsByExactParameters 7 | import java.lang.reflect.Method 8 | 9 | object Methods { 10 | val EncEngine_transFor: Method by wxLazy("EncEngine_transFor") { 11 | findMethodsByExactParameters(EncEngine, C.Int, C.ByteArray, C.Int).firstOrNull() 12 | } 13 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/plugin/base/stub/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.plugin.base.stub 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxPackageName 6 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassIfExists 7 | 8 | object Classes { 9 | val WXCustomScheme: Class<*> by wxLazy("WXCustomScheme") { 10 | findClassIfExists("$wxPackageName.plugin.base.stub.WXCustomSchemeEntryActivity", wxLoader!!) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/plugin/base/stub/Methods.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.plugin.base.stub 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.C 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 5 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.plugin.base.stub.Classes.WXCustomScheme 6 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findMethodsByExactParameters 7 | import java.lang.reflect.Method 8 | 9 | object Methods { 10 | val WXCustomScheme_entry: Method by wxLazy("WXCustomScheme_entry") { 11 | findMethodsByExactParameters(WXCustomScheme, C.Boolean, C.Intent).firstOrNull() 12 | } 13 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/plugin/gallery/ui/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.plugin.gallery.ui 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxPackageName 6 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassIfExists 7 | 8 | object Classes { 9 | /** 10 | * 当用户需要从手机相册中选择照片时启动的Activity 11 | */ 12 | val AlbumPreviewUI: Class<*> by wxLazy("AlbumPreviewUI") { 13 | findClassIfExists("$wxPackageName.plugin.gallery.ui.AlbumPreviewUI", wxLoader!!) 14 | } 15 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/plugin/remittance/ui/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.plugin.remittance.ui 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxPackageName 6 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassIfExists 7 | 8 | object Classes { 9 | val RemittanceAdapter: Class<*> by wxLazy("RemittanceAdapter") { 10 | findClassIfExists("$wxPackageName.plugin.remittance.ui.RemittanceAdapterUI", wxLoader!!) 11 | } 12 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/plugin/sns/ui/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.plugin.sns.ui 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxClasses 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 6 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxPackageName 7 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassIfExists 8 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassesFromPackage 9 | 10 | object Classes { 11 | val SnsActivity: Class<*> by wxLazy("SnsActivity") { 12 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.plugin.sns.ui") 13 | .filterByField("$wxPackageName.ui.base.MMPullDownView") 14 | .firstOrNull() 15 | } 16 | 17 | val SnsTimeLineUI: Class<*> by wxLazy("SnsTimeLineUI") { 18 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.plugin.sns.ui") 19 | .filterByField("android.support.v7.app.ActionBar") 20 | .firstOrNull() 21 | } 22 | 23 | val SnsUploadUI: Class<*> by wxLazy("SnsUploadUI") { 24 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.plugin.sns.ui") 25 | .filterByField("$wxPackageName.plugin.sns.ui.LocationWidget") 26 | .filterByField("$wxPackageName.plugin.sns.ui.SnsUploadSayFooter") 27 | .firstOrNull() 28 | } 29 | 30 | val SnsUserUI: Class<*> by wxLazy("SnsUserUI") { 31 | findClassIfExists("$wxPackageName.plugin.sns.ui.SnsUserUI", wxLoader!!) 32 | } 33 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/plugin/sns/ui/Fields.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.plugin.sns.ui 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxPackageName 5 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.plugin.sns.ui.Classes.SnsUploadUI 6 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findFieldsWithType 7 | import java.lang.reflect.Field 8 | 9 | object Fields { 10 | val SnsUploadUI_mSnsEditText: Field by wxLazy("SnsUploadUI_mSnsEditText") { 11 | findFieldsWithType(SnsUploadUI, "$wxPackageName.plugin.sns.ui.SnsEditText") 12 | .firstOrNull()?.apply { isAccessible = true } 13 | } 14 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/plugin/webwx/ui/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.plugin.webwx.ui 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxPackageName 6 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassIfExists 7 | 8 | object Classes { 9 | val ExtDeviceWXLoginUI: Class<*> by wxLazy("ExtDeviceWXLoginUI") { 10 | findClassIfExists("$wxPackageName.plugin.webwx.ui.ExtDeviceWXLoginUI", wxLoader!!) 11 | } 12 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/sdk/platformtools/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.sdk.platformtools 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.C 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxClasses 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 6 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 7 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxPackageName 8 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassesFromPackage 9 | 10 | object Classes { 11 | val Logcat: Class<*> by wxLazy("Logcat") { 12 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.sdk.platformtools") 13 | .filterByEnclosingClass(null) 14 | .filterByMethod(C.Int, "getLogLevel") 15 | .firstOrNull() 16 | } 17 | 18 | val LruCache: Class<*> by wxLazy("LruCache") { 19 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.sdk.platformtools") 20 | .filterByMethod(null, "trimToSize", C.Int) 21 | .firstOrNull() 22 | } 23 | 24 | val XmlParser: Class<*> by wxLazy("XmlParser") { 25 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.sdk.platformtools") 26 | .filterByMethod(C.Map, C.String, C.String) 27 | .firstOrNull() 28 | } 29 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/sdk/platformtools/Methods.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.sdk.platformtools 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.C 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 5 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.sdk.platformtools.Classes.XmlParser 6 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findMethodsByExactParameters 7 | import java.lang.reflect.Method 8 | 9 | object Methods { 10 | val XmlParser_parse: Method by wxLazy("XmlParser_parse") { 11 | findMethodsByExactParameters(XmlParser, C.Map, C.String, C.String).firstOrNull() 12 | } 13 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/storage/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.storage 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.C 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxClasses 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 6 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 7 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxPackageName 8 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassesFromPackage 9 | 10 | object Classes { 11 | val MsgInfo: Class<*> by wxLazy("MsgInfo") { 12 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.storage") 13 | .filterByMethod(C.Boolean, "isSystem") 14 | .firstOrNull() 15 | } 16 | 17 | val MsgInfoStorage: Class<*> by wxLazy("MsgInfoStorage") { 18 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.storage") 19 | .filterByMethod(C.Long, MsgInfo, C.Boolean) 20 | .firstOrNull() 21 | } 22 | 23 | val ContactInfo: Class<*> by wxLazy("ContactInfo") { 24 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.storage") 25 | .filterByMethod(C.String, "getCityCode") 26 | .filterByMethod(C.String, "getCountryCode") 27 | .firstOrNull() 28 | } 29 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/storage/Methods.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.storage 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.C 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 5 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.storage.Classes.MsgInfo 6 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.storage.Classes.MsgInfoStorage 7 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findMethodsByExactParameters 8 | import java.lang.reflect.Method 9 | 10 | object Methods { 11 | val MsgInfoStorage_insert: Method by wxLazy("MsgInfoStorage_insert") { 12 | findMethodsByExactParameters(MsgInfoStorage, C.Long, MsgInfo, C.Boolean).firstOrNull() 13 | } 14 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/ui/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui 2 | 3 | import android.widget.BaseAdapter 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxClasses 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 6 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 7 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxPackageName 8 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassIfExists 9 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassesFromPackage 10 | 11 | object Classes { 12 | val LauncherUI: Class<*> by wxLazy("LauncherUI") { 13 | findClassIfExists("$wxPackageName.ui.LauncherUI", wxLoader!!) 14 | } 15 | 16 | val MMActivity: Class<*> by wxLazy("MMActivity") { 17 | findClassIfExists("$wxPackageName.ui.MMActivity", wxLoader!!) 18 | } 19 | 20 | val MMFragmentActivity: Class<*> by wxLazy("MMFragmentActivity") { 21 | findClassIfExists("$wxPackageName.ui.MMFragmentActivity", wxLoader!!) 22 | } 23 | 24 | val MMBaseAdapter: Class<*> by wxLazy("MMBaseAdapter") { 25 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.ui") 26 | .filterBySuper(BaseAdapter::class.java) 27 | .filterByField("TAG", "java.lang.String") 28 | .firstOrNull() 29 | } 30 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/ui/Methods.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.C 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 5 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.Classes.MMBaseAdapter 6 | 7 | object Methods { 8 | val MMBaseAdapter_getItemInternal: String by wxLazy("MMBaseAdapter_getItemInternal") { 9 | MMBaseAdapter.declaredMethods.filter { 10 | it.parameterTypes.size == 1 && it.parameterTypes[0] == C.Int 11 | }.firstOrNull { 12 | it.name != "getItem" && it.name != "getItemId" 13 | }?.name 14 | } 15 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/ui/base/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.base 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxPackageName 6 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassIfExists 7 | 8 | object Classes { 9 | val MMListPopupWindow: Class<*> by wxLazy("MMListPopupWindow") { 10 | findClassIfExists("$wxPackageName.ui.base.MMListPopupWindow", wxLoader!!) 11 | } 12 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/ui/chatting/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.chatting 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.C 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxClasses 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 6 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 7 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxPackageName 8 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.Classes.MMFragmentActivity 9 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassesFromPackage 10 | 11 | object Classes { 12 | val ChattingUI: Class<*> by wxLazy("ChattingUI") { 13 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.ui.chatting") 14 | .filterBySuper(MMFragmentActivity) 15 | .filterByMethod(null, "onRequestPermissionsResult", C.Int, C.StringArray, C.IntArray) 16 | .firstOrNull() 17 | } 18 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/ui/contact/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.contact 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.C 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxClasses 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 6 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 7 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxPackageName 8 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassIfExists 9 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassesFromPackage 10 | 11 | object Classes { 12 | val AddressUI: Class<*> by wxLazy("AddressUI") { 13 | findClassIfExists("$wxPackageName.ui.contact.AddressUI\$a", wxLoader!!) 14 | } 15 | 16 | val AddressAdapter: Class<*> by wxLazy("AddressAdapter") { 17 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.ui.contact") 18 | .filterByMethod(null, "pause") 19 | .firstOrNull() 20 | } 21 | 22 | val ContactLongClickListener: Class<*> by wxLazy("ContactLongClickListener") { 23 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.ui.contact") 24 | .filterByEnclosingClass(AddressUI) 25 | .filterByMethod(C.Boolean, "onItemLongClick", C.AdapterView, C.View, C.Int, C.Long) 26 | .firstOrNull() 27 | } 28 | 29 | val SelectContactUI: Class<*> by wxLazy("SelectContactUI") { 30 | findClassIfExists("$wxPackageName.ui.contact.SelectContactUI", wxLoader!!) 31 | } 32 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/ui/conversation/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.conversation 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.C 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxClasses 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 6 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 7 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxPackageName 8 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassesFromPackage 9 | 10 | object Classes { 11 | val ConversationWithCacheAdapter: Class<*> by wxLazy("ConversationWithCacheAdapter") { 12 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.ui.conversation") 13 | .filterByMethod(null, "clearCache") 14 | .firstOrNull() 15 | } 16 | 17 | val ConversationCreateContextMenuListener: Class<*> by wxLazy("ConversationCreateContextMenuListener") { 18 | ConversationLongClickListener 19 | } 20 | 21 | val ConversationLongClickListener: Class<*> by wxLazy("ConversationLongClickListener") { 22 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.ui.conversation") 23 | .filterByMethod(null, "onCreateContextMenu", C.ContextMenu, C.View, C.ContextMenuInfo) 24 | .filterByMethod(C.Boolean, "onItemLongClick", C.AdapterView, C.View, C.Int, C.Long) 25 | .firstOrNull() 26 | } 27 | 28 | val MainUI: Class<*> by wxLazy("MainUI") { 29 | findClassesFromPackage(wxLoader!!, wxClasses!!, "$wxPackageName.ui.conversation") 30 | .filterByMethod(C.Int, "getLayoutId") 31 | .filterByMethod(null, "onConfigurationChanged", C.Configuration) 32 | .firstOrNull() 33 | } 34 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/ui/tools/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.tools 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxPackageName 6 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassIfExists 7 | 8 | object Classes { 9 | val ActionBarEditText: Class<*> by wxLazy("ActionBarEditText") { 10 | findClassIfExists("$wxPackageName.ui.tools.ActionBarSearchView\$ActionBarEditText", wxLoader!!) 11 | } 12 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/ui/transmit/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.transmit 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 5 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxPackageName 6 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassIfExists 7 | 8 | object Classes { 9 | val SelectConversationUI: Class<*> by wxLazy("SelectConversationUI") { 10 | findClassIfExists("$wxPackageName.ui.transmit.SelectConversationUI", wxLoader!!) 11 | } 12 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/mm/ui/transmit/Methods.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.transmit 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.C 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 5 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.mm.ui.transmit.Classes.SelectConversationUI 6 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findMethodsByExactParameters 7 | import java.lang.reflect.Method 8 | 9 | object Methods { 10 | val SelectConversationUI_checkLimit: Method by wxLazy("SelectConversationUI_checkLimit") { 11 | findMethodsByExactParameters(SelectConversationUI, C.Boolean, C.Boolean).firstOrNull() 12 | } 13 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/wcdb/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.wcdb 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 5 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.wcdb.Package.WECHAT_PACKAGE_SQLITE 6 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassIfExists 7 | 8 | /** 9 | * 路径类名正确 10 | */ 11 | object Classes { 12 | val SQLiteErrorHandler: Class<*> by wxLazy("SQLiteErrorHandler") { 13 | findClassIfExists("$WECHAT_PACKAGE_SQLITE.DatabaseErrorHandler", wxLoader!!) 14 | } 15 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/wcdb/Package.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.wcdb 2 | 3 | /** 4 | * 路径正确 5 | */ 6 | object Package { 7 | const val WECHAT_PACKAGE_SQLITE = "com.tencent.wcdb" 8 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/wcdb/database/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.wcdb.database 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 5 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.wcdb.Package.WECHAT_PACKAGE_SQLITE 6 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassIfExists 7 | /** 8 | * 路径类名正确 9 | */ 10 | object Classes { 11 | val SQLiteDatabase: Class<*> by wxLazy("SQLiteDatabase") { 12 | findClassIfExists("$WECHAT_PACKAGE_SQLITE.database.SQLiteDatabase", wxLoader!!) 13 | } 14 | val SQLiteCursorFactory: Class<*> by wxLazy("SQLiteCursorFactory") { 15 | findClassIfExists("$WECHAT_PACKAGE_SQLITE.database.SQLiteDatabase\$CursorFactory", wxLoader!!) 16 | } 17 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/com/tencent/wcdb/support/Classes.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.wcdb.support 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLazy 4 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal.wxLoader 5 | import com.gh0u1l5.wechatmagician.spellbook.mirror.com.tencent.wcdb.Package.WECHAT_PACKAGE_SQLITE 6 | import com.gh0u1l5.wechatmagician.spellbook.util.ReflectionUtil.findClassIfExists 7 | /** 8 | * 路径类名正确 9 | */ 10 | object Classes { 11 | val SQLiteCancellationSignal: Class<*> by wxLazy("SQLiteCancellationSignal") { 12 | findClassIfExists("$WECHAT_PACKAGE_SQLITE.support.CancellationSignal", wxLoader!!) 13 | } 14 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/parser/ApkFile.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.parser 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.util.ParallelUtil.parallelForEach 4 | 5 | import java.io.Closeable 6 | import java.io.File 7 | import java.nio.ByteBuffer 8 | import java.util.zip.ZipEntry 9 | import java.util.zip.ZipFile 10 | 11 | /** 12 | * 封装对 APK 文件的解析操作 13 | * 14 | * 参考了 dongliu 的 apk-parser 项目 15 | * 16 | * Refer: https://github.com/hsiafan/apk-parser 17 | */ 18 | @ExperimentalUnsignedTypes 19 | class ApkFile(apkFile: File) : Closeable { 20 | /** 21 | * @suppress 22 | */ 23 | private companion object { 24 | private const val DEX_FILE = "classes.dex" 25 | private const val DEX_ADDITIONAL = "classes%d.dex" 26 | } 27 | 28 | constructor(path: String) : this(File(path)) 29 | 30 | private val zipFile: ZipFile = ZipFile(apkFile) 31 | 32 | private fun readEntry(entry: ZipEntry): ByteArray = 33 | zipFile.getInputStream(entry).use { it.readBytes() } 34 | 35 | override fun close() = 36 | zipFile.close() 37 | 38 | private fun getDexFilePath(idx: Int) = 39 | if (idx == 1) DEX_FILE else String.format(DEX_ADDITIONAL, idx) 40 | 41 | private fun isDexFileExist(idx: Int): Boolean { 42 | val path = getDexFilePath(idx) 43 | return zipFile.getEntry(path) != null 44 | } 45 | 46 | private fun readDexFile(idx: Int): ByteArray { 47 | val path = getDexFilePath(idx) 48 | return readEntry(zipFile.getEntry(path)) 49 | } 50 | 51 | val classTypes: ClassTrie by lazy { 52 | var end = 2 53 | while (isDexFileExist(end)) end++ 54 | 55 | val ret = ClassTrie() 56 | (1 until end).parallelForEach { idx -> 57 | val data = readDexFile(idx) 58 | val buffer = ByteBuffer.wrap(data) 59 | val parser = DexParser(buffer) 60 | ret += parser.parseClassTypes() 61 | } 62 | return@lazy ret.apply { mutable = false } 63 | } 64 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/parser/ClassTrie.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.parser 2 | 3 | import java.util.concurrent.ConcurrentHashMap 4 | import kotlin.collections.ArrayList 5 | 6 | /** 7 | * 用来储存一个 APK 的 package 结构 8 | * 9 | * 出于性能考虑, 这个类不支持读线程和写线程同时操作, 但支持同类型的线程同时操作 10 | */ 11 | class ClassTrie { 12 | /** 13 | * @suppress 14 | */ 15 | private companion object { 16 | /** 17 | * 用来将 JVM 格式的类型标识符转换为类名 18 | * 19 | * Example: String 的类型标识符为 "Ljava/lang/String;" 20 | * Refer: https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3 21 | */ 22 | private fun convertJVMTypeToClassName(type: String) = 23 | type.substring(1, type.length - 1).replace('/', '.') 24 | } 25 | 26 | /** 27 | * 读写开关, 用于增强线程间的安全性 28 | * 29 | * 只有开关设为 true 的时候, 写操作才会被执行 30 | * 只有开关设为 false 的时候, 读操作才会返回有效数据 31 | */ 32 | @Volatile var mutable = true 33 | 34 | /** 35 | * package 结构的根结点 36 | */ 37 | private val root: TrieNode = TrieNode() 38 | 39 | /** 40 | * 插入一个单独的 JVM 格式的类型标识符 41 | */ 42 | operator fun plusAssign(type: String) { 43 | if (mutable) { 44 | root.add(convertJVMTypeToClassName(type)) 45 | } 46 | } 47 | 48 | /** 49 | * 插入一组 JVM 格式的类型标识符 50 | */ 51 | operator fun plusAssign(types: Array) { 52 | types.forEach { this += it } 53 | } 54 | 55 | /** 56 | * 查找指定包里指定深度的所有类 57 | * 58 | * 出于性能方面的考虑, 只有深度相等的类才会被返回, 比如搜索深度为0的时候, 就只返回这个包自己拥有的类, 不包括它 59 | * 里面其他包拥有的类. 60 | */ 61 | fun search(packageName: String, depth: Int): List { 62 | if (mutable) return emptyList() 63 | return root.search(packageName, depth) 64 | } 65 | 66 | /** 67 | * 私有的节点结构 68 | */ 69 | private class TrieNode { 70 | val classes: MutableList = ArrayList(50) 71 | 72 | val children: MutableMap = ConcurrentHashMap() 73 | 74 | fun add(className: String) { 75 | add(className, 0) 76 | } 77 | 78 | private fun add(className: String, pos: Int) { 79 | val delimiterAt = className.indexOf('.', pos) 80 | if (delimiterAt == -1) { 81 | synchronized(this) { 82 | classes.add(className) 83 | } 84 | return 85 | } 86 | val pkg = className.substring(pos, delimiterAt) 87 | if (pkg !in children) { 88 | children[pkg] = TrieNode() 89 | } 90 | children[pkg]!!.add(className, delimiterAt + 1) 91 | } 92 | 93 | fun get(depth: Int = 0): List { 94 | if (depth == 0) return classes 95 | return children.flatMap { it.value.get(depth - 1) } 96 | } 97 | 98 | fun search(packageName: String, depth: Int): List { 99 | return search(packageName, depth, 0) 100 | } 101 | 102 | private fun search(packageName: String, depth: Int, pos: Int): List { 103 | val delimiterAt = packageName.indexOf('.', pos) 104 | if (delimiterAt == -1) { 105 | val pkg = packageName.substring(pos) 106 | return children[pkg]?.get(depth) ?: emptyList() 107 | } 108 | val pkg = packageName.substring(pos, delimiterAt) 109 | val next = children[pkg] ?: return emptyList() 110 | return next.search(packageName, depth, delimiterAt + 1) 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/parser/DexHeader.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.parser 2 | 3 | /** 4 | * Dex 格式的文件头 5 | * 6 | * Refer: https://source.android.com/devices/tech/dalvik/dex-format 7 | */ 8 | @ExperimentalUnsignedTypes 9 | class DexHeader { 10 | var version: Int = 0 11 | 12 | var checksum: UInt = 0u 13 | 14 | var signature: ByteArray = ByteArray(kSHA1DigestLen) 15 | 16 | var fileSize: UInt = 0u 17 | 18 | var headerSize: UInt = 0u 19 | 20 | var endianTag: UInt = 0u 21 | 22 | var linkSize: UInt = 0u 23 | 24 | var linkOff: UInt = 0u 25 | 26 | var mapOff: UInt = 0u 27 | 28 | var stringIdsSize: Int = 0 29 | 30 | var stringIdsOff: UInt = 0u 31 | 32 | var typeIdsSize: Int = 0 33 | 34 | var typeIdsOff: UInt = 0u 35 | 36 | var protoIdsSize: Int = 0 37 | 38 | var protoIdsOff: UInt = 0u 39 | 40 | var fieldIdsSize: Int = 0 41 | 42 | var fieldIdsOff: UInt = 0u 43 | 44 | var methodIdsSize: Int = 0 45 | 46 | var methodIdsOff: UInt = 0u 47 | 48 | var classDefsSize: Int = 0 49 | 50 | var classDefsOff: UInt = 0u 51 | 52 | var dataSize: Int = 0 53 | 54 | var dataOff: UInt = 0u 55 | 56 | /** 57 | * @suppress 58 | */ 59 | companion object { 60 | const val kSHA1DigestLen = 20 61 | const val kSHA1DigestOutputLen = kSHA1DigestLen * 2 + 1 62 | } 63 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/util/BasicUtil.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.util 2 | 3 | import android.util.Log 4 | import kotlin.concurrent.thread 5 | 6 | /** 7 | * 封装了一批很便利的常用操作 8 | */ 9 | object BasicUtil { 10 | /** 11 | * 执行回调函数, 无视它抛出的任何异常 12 | */ 13 | @JvmStatic inline fun trySilently(func: () -> T?): T? { 14 | return try { func() } catch (t: Throwable) { null } 15 | } 16 | 17 | /** 18 | * 执行回调函数, 将它抛出的异常记录到 Xposed 的日志里 19 | */ 20 | @JvmStatic inline fun tryVerbosely(func: () -> T?): T? { 21 | return try { func() } catch (t: Throwable) { 22 | Log.e("Xposed", Log.getStackTraceString(t)); null 23 | } 24 | } 25 | 26 | /** 27 | * 异步执行回调函数, 将它抛出的记录到 Xposed 的日志里 28 | * 29 | * WARN: 别忘了任何 UI 操作都必须使用 runOnUiThread 30 | */ 31 | @JvmStatic inline fun tryAsynchronously(crossinline func: () -> Unit): Thread { 32 | return thread(start = true) { func() }.apply { 33 | setUncaughtExceptionHandler { _, t -> 34 | Log.e("Xposed", Log.getStackTraceString(t)) 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/util/FileUtil.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.util 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.graphics.Bitmap 6 | import android.net.Uri 7 | import android.os.SystemClock.elapsedRealtime 8 | import java.io.* 9 | import java.lang.System.currentTimeMillis 10 | import java.text.SimpleDateFormat 11 | import java.util.* 12 | 13 | /** 14 | * 封装了一批关于磁盘 I/O 的方法 15 | */ 16 | object FileUtil { 17 | /** 18 | * 将一段数据写入磁盘指定位置 19 | * 20 | * 若文件及其所在目录不存在的话, 会尝试建立该文件和目录 21 | */ 22 | @JvmStatic fun writeBytesToDisk(path: String, content: ByteArray) { 23 | val file = File(path).also { it.parentFile.mkdirs() } 24 | val fout = FileOutputStream(file) 25 | BufferedOutputStream(fout).use { it.write(content) } 26 | } 27 | 28 | /** 29 | * 从磁盘上读取一个文件的全部数据 30 | */ 31 | @JvmStatic fun readBytesFromDisk(path: String): ByteArray { 32 | val fin = FileInputStream(path) 33 | return BufferedInputStream(fin).use { it.readBytes() } 34 | } 35 | 36 | /** 37 | * 将一个 [Serializable] 对象写入磁盘指定位置 38 | */ 39 | @JvmStatic fun writeObjectToDisk(path: String, obj: Serializable) { 40 | val out = ByteArrayOutputStream() 41 | ObjectOutputStream(out).use { 42 | it.writeObject(obj) 43 | } 44 | writeBytesToDisk(path, out.toByteArray()) 45 | } 46 | 47 | /** 48 | * 从磁盘上读取一个 [Serializable] 对象 49 | */ 50 | @JvmStatic fun readObjectFromDisk(path: String): Any? { 51 | val bytes = readBytesFromDisk(path) 52 | val ins = ByteArrayInputStream(bytes) 53 | return ObjectInputStream(ins).use { 54 | it.readObject() 55 | } 56 | } 57 | 58 | /** 59 | * 将一个 [InputStream] 的内容写入磁盘指定位置 60 | * 61 | * 该函数会同步进行读写, 比较节约内存, 在内存空间不足的设备上非常有帮助 62 | * 63 | * @param path 数据保存路径 64 | * @param ins 提供数据的 [InputStream] 对象 65 | * @param bufferSize 缓冲区大小, 默认值为8192, 设置更大的值可以换来线性的性能提升 66 | */ 67 | @JvmStatic fun writeInputStreamToDisk(path: String, ins: InputStream, bufferSize: Int = 8192) { 68 | val file = File(path) 69 | file.parentFile.mkdirs() 70 | val fout = FileOutputStream(file) 71 | BufferedOutputStream(fout).use { 72 | val buffer = ByteArray(bufferSize) 73 | var length = ins.read(buffer) 74 | while (length != -1) { 75 | it.write(buffer, 0, length) 76 | length = ins.read(buffer) 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * 将一张 [Bitmap] 写入磁盘指定位置 83 | */ 84 | @JvmStatic fun writeBitmapToDisk(path: String, bitmap: Bitmap) { 85 | val out = ByteArrayOutputStream() 86 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, out) 87 | writeBytesToDisk(path, out.toByteArray()) 88 | } 89 | 90 | /** 91 | * 如果指定文件开机后还没被修改过, 那么执行一次写操作 92 | * 93 | * @param writeCallback 实际进行写操作的回调函数 94 | */ 95 | @JvmStatic inline fun writeOnce(path: String, writeCallback: (String) -> Unit) { 96 | val file = File(path) 97 | if (!file.exists()) { 98 | writeCallback(path) 99 | return 100 | } 101 | val bootAt = currentTimeMillis() - elapsedRealtime() 102 | val modifiedAt = file.lastModified() 103 | if (modifiedAt < bootAt) { 104 | writeCallback(path) 105 | } 106 | } 107 | 108 | /** 109 | * 基于当前时间创建一个时间戳 110 | */ 111 | @JvmStatic fun createTimeTag(): String { 112 | val formatter = SimpleDateFormat("yyyy-MM-dd-HHmmss", Locale.getDefault()) 113 | return formatter.format(Calendar.getInstance().time) 114 | } 115 | 116 | /** 117 | * 广播告知所有应用: 磁盘上添加了新的图片 118 | */ 119 | @JvmStatic fun notifyNewMediaFile(path: String, context: Context?) { 120 | val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) 121 | context?.sendBroadcast(intent.apply { 122 | data = Uri.fromFile(File(path)) 123 | }) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/util/MirrorUtil.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.util 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal 4 | 5 | /** 6 | * 封装了一批用于检查“自动适配表达式”的函数 7 | */ 8 | object MirrorUtil { 9 | /** 10 | * 返回一个 Object 所声明的所有成员变量(不含基类成员) 11 | */ 12 | @JvmStatic fun collectFields(instance: Any): List> { 13 | return instance::class.java.declaredFields.filter { field -> 14 | field.name != "INSTANCE" && field.name != "\$\$delegatedProperties" 15 | }.map { field -> 16 | field.isAccessible = true 17 | val key = field.name.removeSuffix("\$delegate") 18 | val value = field.get(instance) 19 | key to value 20 | } 21 | } 22 | 23 | /** 24 | * 生成一份适配报告, 记录每个自动适配表达式最终指向了微信中的什么位置 25 | */ 26 | @JvmStatic fun generateReport(instances: List): List> { 27 | return instances.map { instance -> 28 | collectFields(instance).map { 29 | "${instance::class.java.canonicalName}.${it.first}" to it.second.toString() 30 | } 31 | }.flatten().sortedBy { it.first } 32 | } 33 | 34 | /** 35 | * 将一个用于单元测试的惰性求值对象还原到未求值的状态 36 | * 37 | * WARN: 仅供单元测试使用 38 | */ 39 | @JvmStatic fun clearUnitTestLazyFields(instance: Any) { 40 | instance::class.java.declaredFields.forEach { field -> 41 | if (Lazy::class.java.isAssignableFrom(field.type)) { 42 | field.isAccessible = true 43 | val lazyObject = field.get(instance) 44 | if (lazyObject is WechatGlobal.UnitTestLazyImpl<*>) { 45 | lazyObject.refresh() 46 | } 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * 生成一份适配报告, 记录每个自动适配表达式最终指向了微信中的什么位置 53 | * 54 | * 如果某个自动适配表达式还没有进行求值的话, 该函数会强制进行一次求值 55 | * 56 | * WARN: 仅供单元测试使用 57 | */ 58 | @JvmStatic fun generateReportWithForceEval(instances: List): List> { 59 | return instances.map { instance -> 60 | collectFields(instance).map { 61 | val value = it.second 62 | if (value is Lazy<*>) { 63 | if (!value.isInitialized()) { 64 | value.value 65 | } 66 | } 67 | "${instance::class.java.canonicalName}.${it.first}" to it.second.toString() 68 | } 69 | }.flatten() // 为了 Benchmark 的准确性, 不对结果进行排序 70 | } 71 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/util/ParallelUtil.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.util 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.util.BasicUtil.tryVerbosely 4 | import java.util.concurrent.ExecutorService 5 | import java.util.concurrent.Executors 6 | import java.util.concurrent.TimeUnit 7 | import kotlin.concurrent.thread 8 | 9 | /** 10 | * 封装了一批用于并行计算的函数 11 | */ 12 | object ParallelUtil { 13 | /** 14 | * 当前设备可用 CPU 数量 15 | */ 16 | val processors: Int = Runtime.getRuntime().availableProcessors() 17 | 18 | /** 19 | * 创建一个线程池, 其线程总数固定不变 20 | * 21 | * @param nThread 该线程池的线程总数, 默认值为当前设备的 CPU 总数 22 | */ 23 | @JvmStatic fun createThreadPool(nThread: Int = processors): ExecutorService = 24 | Executors.newFixedThreadPool(nThread) 25 | 26 | /** 27 | * 进行一次并行计算的 Map 操作 28 | */ 29 | @JvmStatic inline fun List.parallelMap(crossinline transform: (T) -> R): List { 30 | val sectionSize = size / processors 31 | 32 | val main = List(processors) { mutableListOf() } 33 | (0 until processors).map { section -> 34 | thread(start = true) { 35 | for (offset in 0 until sectionSize) { 36 | val idx = section * sectionSize + offset 37 | main[section].add(transform(this[idx])) 38 | } 39 | } 40 | }.forEach { it.join() } 41 | 42 | val rest = (0 until size % processors).map { offset -> 43 | val idx = processors * sectionSize + offset 44 | transform(this[idx]) 45 | } 46 | 47 | return main.flatten() + rest 48 | } 49 | 50 | /** 51 | * 进行一次并行计算的 ForEach 操作 52 | */ 53 | @JvmStatic inline fun Iterable.parallelForEach(crossinline action: (T) -> Unit) { 54 | val pool = createThreadPool() 55 | val iterator = iterator() 56 | while (iterator.hasNext()) { 57 | val item = iterator.next() 58 | pool.execute { 59 | tryVerbosely { action(item) } // 避免进程崩溃 60 | } 61 | } 62 | pool.shutdown() 63 | pool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS) 64 | } 65 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/main/kotlin/com/gh0u1l5/wechatmagician/spellbook/util/XposedUtil.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.util 2 | 3 | import android.os.Build 4 | import android.os.Handler 5 | import android.os.HandlerThread 6 | import com.gh0u1l5.wechatmagician.spellbook.base.Hooker 7 | import com.gh0u1l5.wechatmagician.spellbook.util.BasicUtil.trySilently 8 | import com.gh0u1l5.wechatmagician.spellbook.util.BasicUtil.tryVerbosely 9 | 10 | /** 11 | * 封装了一批用于和 Xposed 框架通信的方法 12 | */ 13 | object XposedUtil { 14 | /** 15 | * 用于处理 Hook 任务的线程池 16 | */ 17 | private val workerPool = ParallelUtil.createThreadPool() 18 | 19 | /** 20 | * 用于分发 Hook 任务的管理线程 21 | */ 22 | private val managerThread = HandlerThread("HookHandler").apply { start() } 23 | 24 | /** 25 | * 用于分发 Hook 任务的 [Handler] 26 | */ 27 | private val managerHandler: Handler = Handler(managerThread.looper) 28 | 29 | /** 30 | * 依据当前系统的版本选择合适的 Hook 策略 31 | * 32 | * @param hook 用于向 Xposed 框架注册事件的回调函数 33 | */ 34 | @JvmStatic private inline fun tryHook(crossinline hook: () -> Unit) { 35 | when { 36 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> { 37 | /** 38 | * WARN: 对于 Android 7.x 及以上, Xposed 多线程 HOOK 会导致崩溃. 39 | */ 40 | tryVerbosely(hook) 41 | } 42 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> { 43 | workerPool.execute { tryVerbosely(hook) } 44 | } 45 | else -> { 46 | /** 47 | * WARN: 对于 Android 4.x 及以下, MultiDex 的支持还不完善, 日志中会出现大量误报. 48 | */ 49 | workerPool.execute { trySilently(hook) } 50 | } 51 | } 52 | } 53 | 54 | /** 55 | * 将 [Hooker] 对象发送给管理线程, 等待进一步的处理 56 | */ 57 | @JvmStatic fun postHooker(hooker: Hooker) { 58 | managerHandler.post { 59 | tryHook { hooker.hook() } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/test/kotlin/com/gh0u1l5/wechatmagician/spellbook/base/VersionUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.base 2 | 3 | import org.junit.Test as Test 4 | import org.junit.Assert.* 5 | 6 | class VersionUnitTest { 7 | @Test fun testVersionCompare() { 8 | assertTrue(Version("1") < Version("2")) 9 | assertTrue(Version("1") == Version("1")) 10 | assertTrue(Version("1.2") < Version("1.2.1")) 11 | assertTrue(Version("1.12") > Version("1.1")) 12 | 13 | assertTrue(Version("6.5.8") > Version("6.5.4")) 14 | assertTrue(Version("6.5.8") >= Version("6.5.4")) 15 | assertTrue(Version("6.5.4") >= Version("6.5.4")) 16 | assertTrue(Version("6.5.4") == Version("6.5.4")) 17 | } 18 | } -------------------------------------------------------------------------------- /Wechat8Spellbook/src/test/kotlin/com/gh0u1l5/wechatmagician/spellbook/mirror/ReflectionUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.gh0u1l5.wechatmagician.spellbook.mirror 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.WechatGlobal 4 | import com.gh0u1l5.wechatmagician.spellbook.util.MirrorUtil.clearUnitTestLazyFields 5 | import com.gh0u1l5.wechatmagician.spellbook.util.MirrorUtil.collectFields 6 | import com.gh0u1l5.wechatmagician.spellbook.util.MirrorUtil.generateReport 7 | import com.gh0u1l5.wechatmagician.spellbook.util.MirrorUtil.generateReportWithForceEval 8 | import org.junit.Assert.assertEquals 9 | import org.junit.Test 10 | 11 | class ReflectionUnitTest { 12 | companion object { 13 | const val TestObject1Name = 14 | "com.gh0u1l5.wechatmagician.spellbook.mirror.ReflectionUnitTest.TestObject1" 15 | const val TestObject2Name = 16 | "com.gh0u1l5.wechatmagician.spellbook.mirror.ReflectionUnitTest.TestObject2" 17 | } 18 | 19 | object TestObject1 { 20 | val var1 = 1 21 | val var2 = 2 22 | val var3 = 3 23 | } 24 | 25 | object TestObject2 { 26 | val var1 by lazy { 1 } 27 | val var2 by lazy { 2 } 28 | } 29 | 30 | object TestObject3 { 31 | val var1 by WechatGlobal.UnitTestLazyImpl{ 1 } 32 | val var2 by WechatGlobal.UnitTestLazyImpl{ 2 } 33 | } 34 | 35 | @Test fun testCollectFields() { 36 | val fields = collectFields(TestObject1) 37 | assertEquals(3, fields.size) 38 | fields.forEach { 39 | when (it.first) { 40 | "var1" -> assertEquals(1, it.second) 41 | "var2" -> assertEquals(2, it.second) 42 | "var3" -> assertEquals(3, it.second) 43 | else -> throw Exception("Unknown key: ${it.first}") 44 | } 45 | } 46 | } 47 | 48 | @Test fun testGenerateReport() { 49 | val report1 = generateReport(listOf(TestObject1, TestObject2)) 50 | assertEquals(5, report1.size) 51 | report1.forEach { 52 | when (it.first) { 53 | "$TestObject1Name.var1" -> assertEquals("1", it.second) 54 | "$TestObject1Name.var2" -> assertEquals("2", it.second) 55 | "$TestObject1Name.var3" -> assertEquals("3", it.second) 56 | "$TestObject2Name.var1" -> assertEquals(lazy { 1 }.toString(), it.second) 57 | "$TestObject2Name.var2" -> assertEquals(lazy { 2 }.toString(), it.second) 58 | else -> throw Exception("Unknown key: ${it.first}") 59 | } 60 | } 61 | 62 | val report2 = generateReportWithForceEval(listOf(TestObject1, TestObject2)) 63 | assertEquals(5, report2.size) 64 | report2.forEach { 65 | when (it.first) { 66 | "$TestObject1Name.var1" -> assertEquals("1", it.second) 67 | "$TestObject1Name.var2" -> assertEquals("2", it.second) 68 | "$TestObject1Name.var3" -> assertEquals("3", it.second) 69 | "$TestObject2Name.var1" -> assertEquals("1", it.second) 70 | "$TestObject2Name.var2" -> assertEquals("2", it.second) 71 | else -> throw Exception("Unknown key: ${it.first}") 72 | } 73 | } 74 | } 75 | 76 | @Test fun testClearUnitTestLazyFields() { 77 | assertEquals(1, TestObject3.var1) 78 | assertEquals(2, TestObject3.var2) 79 | 80 | clearUnitTestLazyFields(TestObject3) 81 | 82 | val fields = collectFields(TestObject3) 83 | assertEquals(2, fields.size) 84 | fields.forEach { 85 | when (it.first) { 86 | "var1" -> assertEquals(lazy { 1 }.toString(), it.second.toString()) 87 | "var2" -> assertEquals(lazy { 2 }.toString(), it.second.toString()) 88 | else -> throw Exception("Unknown key: ${it.first}") 89 | } 90 | } 91 | 92 | assertEquals(1, TestObject3.var1) 93 | assertEquals(2, TestObject3.var2) 94 | } 95 | } -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | buildToolsVersion '28.0.3' 6 | 7 | 8 | defaultConfig { 9 | applicationId "com.huruwo.hposed" 10 | minSdkVersion 21 11 | targetSdkVersion 28 12 | versionCode 10 13 | versionName "12.7" 14 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 15 | } 16 | 17 | packagingOptions { 18 | exclude 'META-INF/*' 19 | } 20 | 21 | 22 | buildTypes { 23 | // release { 24 | // minifyEnabled false 25 | // proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 26 | // } 27 | } 28 | compileOptions { 29 | sourceCompatibility JavaVersion.VERSION_1_8 30 | targetCompatibility JavaVersion.VERSION_1_8 31 | } 32 | 33 | android.applicationVariants.all { variant -> 34 | variant.outputs.all { 35 | outputFileName = "KT-V${variant.versionName}.apk" 36 | } 37 | } 38 | 39 | } 40 | 41 | dependencies { 42 | implementation fileTree(include: ['*.jar'], dir: 'libs') 43 | implementation 'com.android.support:appcompat-v7:28.0.0' 44 | implementation 'com.android.support.constraint:constraint-layout:1.0.2' 45 | implementation project(path: ':Wechat8Spellbook') 46 | testImplementation 'junit:junit:4.12' 47 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 48 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 49 | compileOnly 'de.robv.android.xposed:api:82' 50 | implementation 'com.blankj:utilcode:1.20.0' 51 | implementation 'com.google.code.gson:gson:2.8.2' 52 | implementation 'com.squareup.okhttp3:okhttp:3.10.0' 53 | implementation group: 'com.squareup.retrofit2', name: 'retrofit', version: '2.5.0' 54 | api 'com.github.tbruyelle:rxpermissions:0.10.2' 55 | api 'io.reactivex.rxjava2:rxandroid:2.0.1' 56 | api 'io.reactivex.rxjava2:rxjava:2.1.3' 57 | api 'com.squareup.retrofit2:converter-gson:2.3.0' 58 | api 'com.squareup.retrofit2:adapter-rxjava2:2.3.0' 59 | api 'com.squareup.okhttp3:logging-interceptor:3.12.0' 60 | implementation('com.yanzhenjie:permission:2.0.0-rc12') { 61 | exclude group: 'com.android.support' 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/huruwo/app_xposed/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.huruwo.app_xposed; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.huruwo.app_xposed.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 28 | 29 | 30 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/assets/xposed_init: -------------------------------------------------------------------------------- 1 | com.huruwo.hposed.HookLoader 2 | -------------------------------------------------------------------------------- /app/src/main/java/com/huruwo/hposed/HookLoader.java: -------------------------------------------------------------------------------- 1 | package com.huruwo.hposed; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.content.pm.ApplicationInfo; 6 | import android.content.pm.PackageManager; 7 | 8 | import com.blankj.utilcode.util.AppUtils; 9 | import com.huruwo.hposed.utils.LogXUtils; 10 | 11 | import java.io.File; 12 | 13 | import dalvik.system.PathClassLoader; 14 | import de.robv.android.xposed.IXposedHookLoadPackage; 15 | import de.robv.android.xposed.IXposedHookZygoteInit; 16 | import de.robv.android.xposed.XC_MethodHook; 17 | import de.robv.android.xposed.XposedHelpers; 18 | import de.robv.android.xposed.callbacks.XC_LoadPackage; 19 | 20 | import static com.huruwo.hposed.MainHookLoader.MAIN_APP; 21 | 22 | /** 23 | * @author DX 24 | * 这种方案建议只在开发调试的时候使用,因为这将损耗一些性能(需要额外加载apk文件),调试没问题后,直接修改xposed_init文件为正确的类即可 25 | * 可以实现免重启,由于存在缓存,需要杀死宿主程序以后才能生效 26 | * Created by DX on 2017/10/4. 27 | * Modified by chengxuncc on 2019/4/16. 28 | */ 29 | 30 | public class HookLoader implements IXposedHookLoadPackage, IXposedHookZygoteInit { 31 | //按照实际使用情况修改下面几项的值 32 | /** 33 | * 当前Xposed模块的包名,方便寻找apk文件 34 | */ 35 | private final static String modulePackageName = MAIN_APP; 36 | 37 | /** 38 | * 实际hook逻辑处理类 39 | */ 40 | private final String handleHookClass = MainHookLoader.class.getName(); 41 | /** 42 | * 实际hook逻辑处理类的入口方法 43 | */ 44 | private final String handleHookMethod = "handleLoadPackage"; 45 | 46 | private final String initMethod = "initZygote"; 47 | 48 | private StartupParam startupparam; 49 | 50 | /** 51 | * 重定向handleLoadPackage函数前会执行initZygote 52 | * 53 | * @param loadPackageParam 54 | * @throws Throwable 55 | */ 56 | @Override 57 | public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { 58 | 59 | // 排除系统应用 60 | if (loadPackageParam.appInfo == null || 61 | (loadPackageParam.appInfo.flags & (ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) == 1) { 62 | return; 63 | } 64 | //将loadPackageParam的classloader替换为宿主程序Application的classloader,解决宿主程序存在多个.dex文件时,有时候ClassNotFound的问题 65 | XposedHelpers.findAndHookMethod(Application.class, "attach", Context.class, new XC_MethodHook() { 66 | @Override 67 | protected void afterHookedMethod(MethodHookParam param) throws Throwable { 68 | Context context = (Context) param.args[0]; 69 | loadPackageParam.classLoader = context.getClassLoader(); 70 | Class cls = getApkClass(context, modulePackageName, handleHookClass); 71 | Object instance = cls.newInstance(); 72 | try { 73 | cls.getDeclaredMethod(initMethod, startupparam.getClass()).invoke(instance, startupparam); 74 | }catch (NoSuchMethodException e){ 75 | // 找不到initZygote方法 76 | } 77 | try { 78 | cls.getDeclaredMethod(handleHookMethod, loadPackageParam.getClass()).invoke(instance, loadPackageParam); 79 | }catch (NoSuchMethodException e){ 80 | // 找不到initZygote方法 81 | } 82 | } 83 | }); 84 | } 85 | 86 | /** 87 | * 实现initZygote,保存启动参数。 88 | * 89 | * @param startupParam 90 | */ 91 | @Override 92 | public void initZygote(StartupParam startupParam) { 93 | this.startupparam = startupParam; 94 | } 95 | 96 | private Class getApkClass(Context context, String modulePackageName, String handleHookClass) throws Throwable { 97 | File apkFile = findApkFile(context, modulePackageName); 98 | if (apkFile == null) { 99 | throw new RuntimeException("寻找模块apk失败"); 100 | } 101 | //加载指定的hook逻辑处理类,并调用它的handleHook方法 102 | PathClassLoader pathClassLoader = new PathClassLoader(apkFile.getAbsolutePath(), ClassLoader.getSystemClassLoader()); 103 | Class cls = Class.forName(handleHookClass, true, pathClassLoader); 104 | return cls; 105 | } 106 | 107 | /** 108 | * 根据包名构建目标Context,并调用getPackageCodePath()来定位apk 109 | * 110 | * @param context context参数 111 | * @param modulePackageName 当前模块包名 112 | * @return apk file 113 | */ 114 | private File findApkFile(Context context, String modulePackageName) { 115 | if (context == null) { 116 | return null; 117 | } 118 | try { 119 | Context moudleContext = context.createPackageContext(modulePackageName, Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY); 120 | String apkPath = moudleContext.getPackageCodePath(); 121 | return new File(apkPath); 122 | } catch (PackageManager.NameNotFoundException e) { 123 | e.printStackTrace(); 124 | } 125 | return null; 126 | } 127 | } -------------------------------------------------------------------------------- /app/src/main/java/com/huruwo/hposed/MainHookLoader.java: -------------------------------------------------------------------------------- 1 | package com.huruwo.hposed; 2 | 3 | import com.gh0u1l5.wechatmagician.spellbook.SpellBook; 4 | import com.huruwo.hposed.action.Alert; 5 | import com.huruwo.hposed.action.Message; 6 | import com.huruwo.hposed.utils.LogXUtils; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | import de.robv.android.xposed.IXposedHookLoadPackage; 12 | import de.robv.android.xposed.callbacks.XC_LoadPackage; 13 | 14 | 15 | public class MainHookLoader implements IXposedHookLoadPackage { 16 | 17 | public static String MAIN_APP = "com.huruwo.hposed"; 18 | 19 | @Override 20 | public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable { 21 | LogXUtils.e(loadPackageParam.packageName); 22 | if(SpellBook.INSTANCE.isImportantWechatProcess(loadPackageParam)){ 23 | LogXUtils.e("hook wechat" +loadPackageParam.packageName); 24 | List list = new ArrayList<>(); 25 | list.add(new Alert()); 26 | list.add(new Message()); 27 | SpellBook.INSTANCE.startup(loadPackageParam,list); 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/huruwo/hposed/action/Alert.java: -------------------------------------------------------------------------------- 1 | package com.huruwo.hposed.action; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.view.Menu; 6 | import android.widget.Toast; 7 | 8 | import com.gh0u1l5.wechatmagician.spellbook.interfaces.IActivityHook; 9 | 10 | import org.jetbrains.annotations.NotNull; 11 | import org.jetbrains.annotations.Nullable; 12 | 13 | /** 14 | * 测试ActivityHook 15 | */ 16 | 17 | public class Alert implements IActivityHook { 18 | @Override 19 | public void onActivityCreating(@NotNull Activity activity, @Nullable Bundle bundle) { 20 | 21 | } 22 | 23 | @Override 24 | public void onActivityResuming(@NotNull Activity activity) { 25 | 26 | } 27 | 28 | @Override 29 | public void onActivityStarting(@NotNull Activity activity) { 30 | Toast.makeText(activity, "Hello Wechat2021!", Toast.LENGTH_LONG).show(); 31 | } 32 | 33 | @Override 34 | public void onMMActivityOptionsMenuCreated(@NotNull Activity activity, @NotNull Menu menu) { 35 | 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/huruwo/hposed/action/Message.java: -------------------------------------------------------------------------------- 1 | package com.huruwo.hposed.action; 2 | 3 | import android.content.ContentValues; 4 | 5 | import com.gh0u1l5.wechatmagician.spellbook.base.Operation; 6 | import com.gh0u1l5.wechatmagician.spellbook.interfaces.IDatabaseHook; 7 | import com.huruwo.hposed.utils.LogXUtils; 8 | 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | 12 | /** 13 | * 测试数据库读写 包括消息的读写 14 | */ 15 | public class Message implements IDatabaseHook { 16 | @NotNull 17 | @Override 18 | public Operation onDatabaseInserted(@NotNull Object thisObject, @NotNull String table, @Nullable String nullColumnHack, @Nullable ContentValues initialValues, int conflictAlgorithm, @Nullable Long result) { 19 | if (table == "message") { 20 | LogXUtils.e("New Message:"+initialValues); 21 | } 22 | return null; 23 | } 24 | 25 | @NotNull 26 | @Override 27 | public Operation onDatabaseOpening(@NotNull String path, @Nullable Object factory, int flags, @Nullable Object errorHandler) { 28 | return null; 29 | } 30 | 31 | @NotNull 32 | @Override 33 | public Operation onDatabaseOpened(@NotNull String path, @Nullable Object factory, int flags, @Nullable Object errorHandler, @Nullable Object result) { 34 | return null; 35 | } 36 | 37 | @NotNull 38 | @Override 39 | public Operation onDatabaseQuerying(@NotNull Object thisObject, @Nullable Object factory, @NotNull String sql, @Nullable String[] selectionArgs, @Nullable String editTable, @Nullable Object cancellationSignal) { 40 | return null; 41 | } 42 | 43 | @NotNull 44 | @Override 45 | public Operation onDatabaseQueried(@NotNull Object thisObject, @Nullable Object factory, @NotNull String sql, @Nullable String[] selectionArgs, @Nullable String editTable, @Nullable Object cancellationSignal, @Nullable Object result) { 46 | return null; 47 | } 48 | 49 | @NotNull 50 | @Override 51 | public Operation onDatabaseInserting(@NotNull Object thisObject, @NotNull String table, @Nullable String nullColumnHack, @Nullable ContentValues initialValues, int conflictAlgorithm) { 52 | return null; 53 | } 54 | 55 | @NotNull 56 | @Override 57 | public Operation onDatabaseUpdating(@NotNull Object thisObject, @NotNull String table, @NotNull ContentValues values, @Nullable String whereClause, @Nullable String[] whereArgs, int conflictAlgorithm) { 58 | return null; 59 | } 60 | 61 | @NotNull 62 | @Override 63 | public Operation onDatabaseUpdated(@NotNull Object thisObject, @NotNull String table, @NotNull ContentValues values, @Nullable String whereClause, @Nullable String[] whereArgs, int conflictAlgorithm, int result) { 64 | return null; 65 | } 66 | 67 | @NotNull 68 | @Override 69 | public Operation onDatabaseDeleting(@NotNull Object thisObject, @NotNull String table, @Nullable String whereClause, @Nullable String[] whereArgs) { 70 | return null; 71 | } 72 | 73 | @NotNull 74 | @Override 75 | public Operation onDatabaseDeleted(@NotNull Object thisObject, @NotNull String table, @Nullable String whereClause, @Nullable String[] whereArgs, int result) { 76 | return null; 77 | } 78 | 79 | @Override 80 | public boolean onDatabaseExecuting(@NotNull Object thisObject, @NotNull String sql, @Nullable Object[] bindArgs, @Nullable Object cancellationSignal) { 81 | return false; 82 | } 83 | 84 | @Override 85 | public void onDatabaseExecuted(@NotNull Object thisObject, @NotNull String sql, @Nullable Object[] bindArgs, @Nullable Object cancellationSignal) { 86 | 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/com/huruwo/hposed/net/ApiClient.java: -------------------------------------------------------------------------------- 1 | package com.huruwo.hposed.net; 2 | 3 | 4 | 5 | import com.huruwo.hposed.BuildConfig; 6 | 7 | import java.net.Proxy; 8 | import java.util.ArrayList; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | import okhttp3.Cookie; 14 | import okhttp3.CookieJar; 15 | import okhttp3.HttpUrl; 16 | import okhttp3.OkHttpClient; 17 | import okhttp3.logging.HttpLoggingInterceptor; 18 | import retrofit2.Retrofit; 19 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; 20 | 21 | /** 22 | * Created by dxx on 2017/11/8. 23 | */ 24 | 25 | public class ApiClient { 26 | 27 | public static String getApiHost() { 28 | return "http://127.0.0.1:80"; 29 | } 30 | 31 | public static final HashMap> cookieStore = new HashMap<>(); 32 | 33 | /** 34 | * 获取Service 35 | * 配置缓存 36 | * 37 | * @param clazz 38 | * @param 39 | * @return 40 | */ 41 | public static T create(Class clazz) { 42 | Retrofit retrofit = getRetrofitInstance(); 43 | return retrofit.create(clazz); 44 | } 45 | 46 | /** 47 | * 单例retrofit 48 | */ 49 | private static Retrofit retrofitInstance; 50 | 51 | private static Retrofit getRetrofitInstance() { 52 | if (retrofitInstance == null) { 53 | synchronized (ApiClient.class) { 54 | if (retrofitInstance == null) { 55 | retrofitInstance = new Retrofit.Builder() 56 | .baseUrl(getApiHost() ) 57 | .addConverterFactory(ScalarsConverterFactory.create()) //String 58 | //.addConverterFactory(GsonConverterFactory.create()) 59 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 60 | .client(getOkHttpClientInstance()) 61 | .build(); 62 | } 63 | } 64 | } 65 | return retrofitInstance; 66 | } 67 | 68 | /** 69 | * 单例OkHttpClient 70 | */ 71 | private static OkHttpClient okHttpClientInstance; 72 | 73 | public static OkHttpClient getOkHttpClientInstance() { 74 | 75 | 76 | if (okHttpClientInstance == null) { 77 | synchronized (ApiClient.class) { 78 | if (okHttpClientInstance == null) { 79 | OkHttpClient.Builder builder = new OkHttpClient().newBuilder(); 80 | 81 | builder.proxy(Proxy.NO_PROXY).retryOnConnectionFailure(true)//默认重试一次,若需要重试N次,则要实现拦截器 82 | .connectTimeout(10, TimeUnit.SECONDS) 83 | .readTimeout(20, TimeUnit.SECONDS) 84 | .writeTimeout(20, TimeUnit.SECONDS) 85 | .cookieJar(new CookieJar() { 86 | @Override 87 | public void saveFromResponse(HttpUrl httpUrl, List list) { 88 | cookieStore.put(httpUrl.host(), list); 89 | } 90 | 91 | @Override 92 | public List loadForRequest(HttpUrl httpUrl) { 93 | List cookies = cookieStore.get(httpUrl.host()); 94 | return cookies != null ? cookies : new ArrayList(); 95 | } 96 | }); 97 | //builder.addInterceptor(new TokenInterceptor()); 98 | 99 | 100 | //两个忽略证书操作 101 | //builder.sslSocketFactory(SSLSocketClient.getSSLSocketFactory()); 102 | //builder.hostnameVerifier(SSLSocketClient.getHostnameVerifier()); 103 | 104 | //-------------------------------缓存相关------------------------------------------ 105 | // if (cacheConfig != null) { 106 | // if(cacheConfig.cacheType==CacheConfig.PRE_CACHE_AFTER_NETWORK) { 107 | // builder.addInterceptor(new CacheInterceptor(cacheConfig.maxAge)).addNetworkInterceptor(new CacheNetworkInterceptor()).cache(cache); 108 | // } 109 | // } 110 | 111 | if (BuildConfig.DEBUG) { 112 | HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(); 113 | httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); 114 | builder.addInterceptor(httpLoggingInterceptor); 115 | } 116 | okHttpClientInstance = builder.build(); 117 | } 118 | } 119 | } 120 | return okHttpClientInstance; 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /app/src/main/java/com/huruwo/hposed/net/AppApiService.java: -------------------------------------------------------------------------------- 1 | package com.huruwo.hposed.net; 2 | 3 | 4 | import java.util.Map; 5 | 6 | import io.reactivex.Observable; 7 | import okhttp3.ResponseBody; 8 | import retrofit2.Call; 9 | import retrofit2.http.Body; 10 | import retrofit2.http.FieldMap; 11 | import retrofit2.http.FormUrlEncoded; 12 | import retrofit2.http.GET; 13 | import retrofit2.http.HeaderMap; 14 | import retrofit2.http.Headers; 15 | import retrofit2.http.POST; 16 | import retrofit2.http.QueryMap; 17 | import retrofit2.http.Url; 18 | 19 | /** 20 | * Created by lw on 2018/1/23. 21 | */ 22 | 23 | public interface AppApiService { 24 | 25 | @GET() 26 | Observable rx_get( 27 | @Url String url, @HeaderMap Map headers, 28 | @QueryMap Map maps); 29 | 30 | @FormUrlEncoded 31 | @POST() 32 | Observable rx_post( 33 | @Url String url, 34 | @HeaderMap Map headers, 35 | @FieldMap Map maps); 36 | 37 | @Headers({"Content-Type: application/json", "Accept: application/json"}) 38 | @POST() 39 | Call post(@Url String url, @Body String data); 40 | 41 | @Headers({"Content-Type: application/json", "Accept: application/json"}) 42 | @GET() 43 | Call get(@Url String url); 44 | 45 | @GET() 46 | Call getWithHeader(@Url String url, @HeaderMap Map headers); 47 | 48 | @POST() 49 | Call postWithHeader(@Url String url, @Body String data,@HeaderMap Map headers); 50 | 51 | @GET 52 | Call getCall(@Url String url); 53 | 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/huruwo/hposed/net/AppDataRepository.java: -------------------------------------------------------------------------------- 1 | package com.huruwo.hposed.net; 2 | 3 | 4 | import com.blankj.utilcode.util.ToastUtils; 5 | 6 | import java.io.IOException; 7 | import java.util.Map; 8 | 9 | import io.reactivex.Observable; 10 | import io.reactivex.Observer; 11 | import retrofit2.Response; 12 | 13 | 14 | /** 15 | * Created by dxx on 2017/11/8. 16 | */ 17 | 18 | public class AppDataRepository { 19 | 20 | 21 | AppApiService service; 22 | 23 | public AppDataRepository() { 24 | service = ApiClient.create(AppApiService.class); 25 | } 26 | 27 | public static Response getData(String url) { 28 | try { 29 | return ApiClient.create(AppApiService.class).get(url).execute(); 30 | } catch (IOException e) { 31 | ToastUtils.showLong("访问错误:"+e.getMessage()); 32 | return null; 33 | } 34 | } 35 | 36 | public static Response getWithHeader(String url,Map headMap){ 37 | try { 38 | return ApiClient.create(AppApiService.class).getWithHeader(url,headMap).execute(); 39 | } catch (IOException e) { 40 | ToastUtils.showLong("访问错误:"+e.getMessage()); 41 | return null; 42 | } 43 | } 44 | 45 | public static Observable getRxData(String url, Map headers, Map maps, Observer s) { 46 | SubscriberManager subscriberManager = new SubscriberManager(); 47 | Observable o = ApiClient.create(AppApiService.class).rx_get(url, headers, maps); 48 | subscriberManager.toSubscribe(o, s); 49 | return o; 50 | } 51 | 52 | 53 | public static Observable postRxData(String url, Map headers, Map maps, Observer s) { 54 | SubscriberManager subscriberManager = new SubscriberManager(); 55 | Observable o = ApiClient.create(AppApiService.class).rx_post(url, headers, maps); 56 | subscriberManager.toSubscribe(o, s); 57 | return o; 58 | } 59 | 60 | public static Response postBodyData(String url, String json) { 61 | try { 62 | return ApiClient.create(AppApiService.class).post(url, json).execute(); 63 | } catch (IOException e) { 64 | ToastUtils.showLong("访问错误:"+e.getMessage()); 65 | return null; 66 | } 67 | } 68 | 69 | public static Response postWithHeader(String url,String json,Map headMap){ 70 | try { 71 | return ApiClient.create(AppApiService.class).postWithHeader(url,json,headMap).execute(); 72 | } catch (IOException e) { 73 | ToastUtils.showLong("访问错误:"+e.getMessage()); 74 | return null; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/huruwo/hposed/net/ScalarRequestBodyConverter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.huruwo.hposed.net; 17 | 18 | import java.io.IOException; 19 | 20 | import okhttp3.MediaType; 21 | import okhttp3.RequestBody; 22 | import retrofit2.Converter; 23 | 24 | final class ScalarRequestBodyConverter implements Converter { 25 | static final ScalarRequestBodyConverter INSTANCE = new ScalarRequestBodyConverter<>(); 26 | private static final MediaType MEDIA_TYPE = MediaType.parse("text/plain; charset=UTF-8"); 27 | 28 | private ScalarRequestBodyConverter() { 29 | } 30 | 31 | @Override public RequestBody convert(T value) throws IOException { 32 | return RequestBody.create(MEDIA_TYPE, String.valueOf(value)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/huruwo/hposed/net/ScalarResponseBodyConverters.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Square, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.huruwo.hposed.net; 17 | 18 | import java.io.IOException; 19 | 20 | import okhttp3.ResponseBody; 21 | import retrofit2.Converter; 22 | 23 | final class ScalarResponseBodyConverters { 24 | private ScalarResponseBodyConverters() { 25 | } 26 | 27 | static final class StringResponseBodyConverter implements Converter { 28 | static final StringResponseBodyConverter INSTANCE = new StringResponseBodyConverter(); 29 | 30 | @Override 31 | public String convert(ResponseBody value) throws IOException { 32 | return value.string(); 33 | } 34 | } 35 | 36 | static final class BooleanResponseBodyConverter implements Converter { 37 | static final BooleanResponseBodyConverter INSTANCE = new BooleanResponseBodyConverter(); 38 | 39 | @Override 40 | public Boolean convert(ResponseBody value) throws IOException { 41 | return Boolean.valueOf(value.string()); 42 | } 43 | } 44 | 45 | static final class ByteResponseBodyConverter implements Converter { 46 | static final ByteResponseBodyConverter INSTANCE = new ByteResponseBodyConverter(); 47 | 48 | @Override 49 | public Byte convert(ResponseBody value) throws IOException { 50 | return Byte.valueOf(value.string()); 51 | } 52 | } 53 | 54 | static final class CharacterResponseBodyConverter implements Converter { 55 | static final CharacterResponseBodyConverter INSTANCE = new CharacterResponseBodyConverter(); 56 | 57 | @Override 58 | public Character convert(ResponseBody value) throws IOException { 59 | String body = value.string(); 60 | if (body.length() != 1) { 61 | throw new IOException( 62 | "Expected body of length 1 for Character conversion but was " + body.length()); 63 | } 64 | return body.charAt(0); 65 | } 66 | } 67 | 68 | static final class DoubleResponseBodyConverter implements Converter { 69 | static final DoubleResponseBodyConverter INSTANCE = new DoubleResponseBodyConverter(); 70 | 71 | @Override 72 | public Double convert(ResponseBody value) throws IOException { 73 | return Double.valueOf(value.string()); 74 | } 75 | } 76 | 77 | static final class FloatResponseBodyConverter implements Converter { 78 | static final FloatResponseBodyConverter INSTANCE = new FloatResponseBodyConverter(); 79 | 80 | @Override 81 | public Float convert(ResponseBody value) throws IOException { 82 | return Float.valueOf(value.string()); 83 | } 84 | } 85 | 86 | static final class IntegerResponseBodyConverter implements Converter { 87 | static final IntegerResponseBodyConverter INSTANCE = new IntegerResponseBodyConverter(); 88 | 89 | @Override 90 | public Integer convert(ResponseBody value) throws IOException { 91 | return Integer.valueOf(value.string()); 92 | } 93 | } 94 | 95 | static final class LongResponseBodyConverter implements Converter { 96 | static final LongResponseBodyConverter INSTANCE = new LongResponseBodyConverter(); 97 | 98 | @Override 99 | public Long convert(ResponseBody value) throws IOException { 100 | return Long.valueOf(value.string()); 101 | } 102 | } 103 | 104 | static final class ShortResponseBodyConverter implements Converter { 105 | static final ShortResponseBodyConverter INSTANCE = new ShortResponseBodyConverter(); 106 | 107 | @Override 108 | public Short convert(ResponseBody value) throws IOException { 109 | return Short.valueOf(value.string()); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/java/com/huruwo/hposed/net/ScalarsConverterFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Square, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.huruwo.hposed.net; 17 | 18 | import java.lang.annotation.Annotation; 19 | import java.lang.reflect.Type; 20 | 21 | import okhttp3.RequestBody; 22 | import okhttp3.ResponseBody; 23 | import retrofit2.Converter; 24 | import retrofit2.Retrofit; 25 | 26 | 27 | public final class ScalarsConverterFactory extends Converter.Factory { 28 | public static ScalarsConverterFactory create() { 29 | return new ScalarsConverterFactory(); 30 | } 31 | 32 | private ScalarsConverterFactory() { 33 | } 34 | 35 | @Override public Converter requestBodyConverter(Type type, 36 | Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) { 37 | if (type == String.class 38 | || type == boolean.class 39 | || type == Boolean.class 40 | || type == byte.class 41 | || type == Byte.class 42 | || type == char.class 43 | || type == Character.class 44 | || type == double.class 45 | || type == Double.class 46 | || type == float.class 47 | || type == Float.class 48 | || type == int.class 49 | || type == Integer.class 50 | || type == long.class 51 | || type == Long.class 52 | || type == short.class 53 | || type == Short.class) { 54 | return ScalarRequestBodyConverter.INSTANCE; 55 | } 56 | return null; 57 | } 58 | 59 | @Override 60 | public Converter responseBodyConverter(Type type, Annotation[] annotations, 61 | Retrofit retrofit) { 62 | if (type == String.class) { 63 | return ScalarResponseBodyConverters.StringResponseBodyConverter.INSTANCE; 64 | } 65 | if (type == Boolean.class || type == boolean.class) { 66 | return ScalarResponseBodyConverters.BooleanResponseBodyConverter.INSTANCE; 67 | } 68 | if (type == Byte.class || type == byte.class) { 69 | return ScalarResponseBodyConverters.ByteResponseBodyConverter.INSTANCE; 70 | } 71 | if (type == Character.class || type == char.class) { 72 | return ScalarResponseBodyConverters.CharacterResponseBodyConverter.INSTANCE; 73 | } 74 | if (type == Double.class || type == double.class) { 75 | return ScalarResponseBodyConverters.DoubleResponseBodyConverter.INSTANCE; 76 | } 77 | if (type == Float.class || type == float.class) { 78 | return ScalarResponseBodyConverters.FloatResponseBodyConverter.INSTANCE; 79 | } 80 | if (type == Integer.class || type == int.class) { 81 | return ScalarResponseBodyConverters.IntegerResponseBodyConverter.INSTANCE; 82 | } 83 | if (type == Long.class || type == long.class) { 84 | return ScalarResponseBodyConverters.LongResponseBodyConverter.INSTANCE; 85 | } 86 | if (type == Short.class || type == short.class) { 87 | return ScalarResponseBodyConverters.ShortResponseBodyConverter.INSTANCE; 88 | } 89 | return null; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/com/huruwo/hposed/net/SubscriberManager.java: -------------------------------------------------------------------------------- 1 | package com.huruwo.hposed.net; 2 | 3 | 4 | import io.reactivex.Observable; 5 | import io.reactivex.Observer; 6 | import io.reactivex.android.schedulers.AndroidSchedulers; 7 | import io.reactivex.annotations.NonNull; 8 | import io.reactivex.disposables.Disposable; 9 | import io.reactivex.functions.Consumer; 10 | import io.reactivex.schedulers.Schedulers; 11 | 12 | public class SubscriberManager { 13 | 14 | 15 | public void toSubscribe(Observable o, final Observer s) { 16 | o.subscribeOn(Schedulers.io()) //被观察者产生事件的线程 17 | .unsubscribeOn(Schedulers.io()) //反注册的线程 18 | .observeOn(AndroidSchedulers.mainThread()) //事件消费的线程 19 | .doOnSubscribe(new Consumer() { 20 | @Override 21 | public void accept(@NonNull Disposable disposable) throws Exception { 22 | 23 | } 24 | }).subscribe(s); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/huruwo/hposed/ui/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.huruwo.hposed.ui; 2 | 3 | import android.Manifest; 4 | import android.os.Bundle; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.widget.Button; 7 | 8 | import com.huruwo.hposed.R; 9 | import com.tbruyelle.rxpermissions2.RxPermissions; 10 | 11 | 12 | public class MainActivity extends AppCompatActivity { 13 | 14 | final RxPermissions rxPermissions = new RxPermissions(this); 15 | private Button button; 16 | 17 | 18 | @Override 19 | public void finish() { 20 | super.finish(); 21 | } 22 | 23 | @Override 24 | protected void onResume() { 25 | super.onResume(); 26 | } 27 | 28 | @Override 29 | protected void onStart() { 30 | super.onStart(); 31 | } 32 | 33 | @Override 34 | protected void onCreate(Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | setContentView(R.layout.activity_main); 37 | rxPermissions 38 | .request(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE) 39 | .subscribe(granted -> { 40 | if (granted) { // Always true pre-M 41 | } else { 42 | // Oups permission denied 43 | } 44 | }); 45 | 46 | } 47 | 48 | @Override 49 | protected void onDestroy() { 50 | super.onDestroy(); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/huruwo/hposed/utils/Constants.java: -------------------------------------------------------------------------------- 1 | package com.huruwo.hposed.utils; 2 | 3 | import android.os.Environment; 4 | 5 | 6 | 7 | public class Constants { 8 | 9 | public static String rootPath = Environment.getExternalStorageDirectory().getAbsolutePath(); 10 | public static String INTERCEPTORPATH = String.format("%s/hook.dex", rootPath); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/huruwo/hposed/utils/GsonUtils.java: -------------------------------------------------------------------------------- 1 | package com.huruwo.hposed.utils; 2 | 3 | 4 | import com.google.gson.Gson; 5 | import com.google.gson.reflect.TypeToken; 6 | 7 | import java.lang.reflect.Type; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | 12 | public class GsonUtils { 13 | 14 | private static Gson gson = new Gson(); 15 | 16 | public static String toJson(Object o) { 17 | return gson.toJson(o); 18 | } 19 | 20 | public static T fromJson(String json, Type type) { 21 | return gson.fromJson(json, type); 22 | } 23 | 24 | public static List listFromJson(String json) { 25 | 26 | List lists = gson.fromJson(json, new TypeToken>() { 27 | }.getType()); 28 | 29 | return lists; 30 | 31 | } 32 | 33 | public static Map mapFromJson(String json) { 34 | 35 | Map map = gson.fromJson(json, new TypeToken>() { 36 | }.getType()); 37 | return map; 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/huruwo/hposed/utils/LogXUtils.java: -------------------------------------------------------------------------------- 1 | package com.huruwo.hposed.utils; 2 | 3 | import android.util.Log; 4 | 5 | import com.blankj.utilcode.util.StringUtils; 6 | import com.blankj.utilcode.util.ToastUtils; 7 | 8 | 9 | 10 | public class LogXUtils { 11 | 12 | public static String TAG = "LogXUtils"; 13 | public static String DerviceId = ""; 14 | 15 | //v d i w e a 16 | 17 | public static void appPause(long millis) { 18 | if (millis > 0) { 19 | try { 20 | Thread.sleep(millis); 21 | } catch (InterruptedException e) { 22 | e.printStackTrace(); 23 | } 24 | } 25 | } 26 | 27 | public static void init(String tag, String derviceId) { 28 | TAG = tag; 29 | DerviceId = derviceId; 30 | } 31 | 32 | public static void v(String log) { 33 | if (!StringUtils.isEmpty(log)) { 34 | Log.v(TAG, log); 35 | } 36 | } 37 | 38 | public static void d(String log) { 39 | if (!StringUtils.isEmpty(log)) { 40 | Log.d(TAG, log); 41 | } 42 | } 43 | 44 | public static void i(String log) { 45 | 46 | if (!StringUtils.isEmpty(log)) { 47 | Log.i(TAG, log); 48 | } 49 | 50 | } 51 | 52 | public static void w(String log) { 53 | if (!StringUtils.isEmpty(log)) { 54 | Log.w(TAG, log); 55 | } 56 | } 57 | 58 | public static void e(String log) { 59 | if (!StringUtils.isEmpty(log)) { 60 | Log.e(TAG, log); 61 | } 62 | } 63 | 64 | 65 | public static void e(String log, boolean toast) { 66 | if (!StringUtils.isEmpty(log)) { 67 | LogXUtils.e(log); 68 | if (toast) { 69 | ToastUtils.showLong(log); 70 | } 71 | } 72 | } 73 | 74 | public static void e(String log, boolean toast,boolean save) { 75 | if (!StringUtils.isEmpty(log)) { 76 | LogXUtils.e(log); 77 | if (toast) { 78 | ToastUtils.showLong(log); 79 | } 80 | if(save){ 81 | 82 | } 83 | } 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/huruwo/hposed/utils/XSharePrefeUtil.java: -------------------------------------------------------------------------------- 1 | package com.huruwo.hposed.utils; 2 | 3 | import android.app.AndroidAppHelper; 4 | import android.content.SharedPreferences; 5 | import android.preference.PreferenceManager; 6 | 7 | import com.blankj.utilcode.util.DeviceUtils; 8 | import com.blankj.utilcode.util.TimeUtils; 9 | 10 | import java.text.SimpleDateFormat; 11 | 12 | 13 | /** 14 | * @ClassName: XSharePrefeUtil 15 | * * 16 | * @Description: 文件类型 17 | * @Author: huruwo 18 | * @Date: 2019/4/16 23:39 19 | */ 20 | public class XSharePrefeUtil { 21 | 22 | 23 | public static String getDeviceId() { 24 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(AndroidAppHelper.currentApplication().getApplicationContext()); 25 | return prefs.getString("DeviceId", DeviceUtils.getAndroidID()); 26 | } 27 | 28 | public static void setDeviceId(String deviceId) { 29 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(AndroidAppHelper.currentApplication().getApplicationContext()); 30 | prefs.edit().putString("DeviceId", deviceId).apply(); 31 | } 32 | 33 | public static String getDataHost() { 34 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(AndroidAppHelper.currentApplication().getApplicationContext()); 35 | return prefs.getString("DataHost", "82.156.27.58"); 36 | } 37 | 38 | public static void setDataHost(String host) { 39 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(AndroidAppHelper.currentApplication().getApplicationContext()); 40 | prefs.edit().putString("DataHost", host).apply(); 41 | } 42 | 43 | } 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 有名侠客 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/huruwo/app_xposed/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.huruwo.app_xposed; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /app/xx.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crewcutbro/WeChat8Xposed-1/d0b0dbeff799c93e4f5121bbabbb04817e9eb143/app/xx.jks -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | //ext.kotlin_version = '1.3.0' 5 | repositories { 6 | google() 7 | jcenter() 8 | maven { url "https://dl.google.com/dl/android/maven2/"} 9 | maven { url 'https://jitpack.io' } 10 | } 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:3.4.1' 13 | //classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | 15 | // NOTE: Do not place your application dependencies here; they belong 16 | // in the individual module build.gradle files 17 | } 18 | } 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | jcenter() 24 | maven { url "https://dl.google.com/dl/android/maven2/"} 25 | maven { url 'https://jitpack.io' } 26 | } 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | 15 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crewcutbro/WeChat8Xposed-1/d0b0dbeff799c93e4f5121bbabbb04817e9eb143/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Jun 27 15:47:13 CST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /release/output.json: -------------------------------------------------------------------------------- 1 | [{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":10,"versionName":"12.7","enabled":true,"outputFile":"KT-V12.7.apk","fullName":"release","baseName":"release"},"path":"KT-V12.7.apk","properties":{}}] -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':Wechat8Spellbook' 2 | include ':app' 3 | -------------------------------------------------------------------------------- /xxx.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Crewcutbro/WeChat8Xposed-1/d0b0dbeff799c93e4f5121bbabbb04817e9eb143/xxx.jks --------------------------------------------------------------------------------