美文网首页架构师安卓
春节福利-教你非Root手机实现微信抢红包

春节福利-教你非Root手机实现微信抢红包

作者: cuieney | 来源:发表于2020-01-21 15:29 被阅读0次

    前提

    写这篇的目的有两个,一个是想告诉广大还在坚持Android开发的小伙伴继续加油,还有就是给自己一个今年的技术产出画个句号吧。最重要的还是想把自己学到的东西开源供大家参考学习,共勉。

    简述

    那我们就进入主题吧,就目前的市场上看,通过Xpose实现这个功能居大多数,这篇文章也是基于Xpose,大家都知道,使用Xpose前提就是必须Root手机,现在也有一些Up主,完成了此项功能。大名鼎鼎的VirtualXpose通过内部模拟Android原生环境实现加载Xpose插件,已到达Root环境hook进程,但是还是有些不稳定的,技术难道更为复杂。还有个也是大家应该也有所了解太极Xposed,是通过二次打包Apk植入代码完成的。但是作者并没有公开源码,而且很多插件用不了,必须让作者给你发激活码才能调试自己的插件。我呢,也是个技术迷,就想自己是否可以也做一个这样的东西。也就有了现在的这篇文章。

    原理

    原理大概分为以下五个部分,下面不会细说具体实现过程,只会说核心内容,不然这篇文章就太长了,如果想了解具体细节可以文章评论区@我就行。

    既然是非Root加载Xpose框架,那么我这边选择的是太极的实现方式,而非VirtualXpose构建虚拟Android环境,太极的方式即是二次打包Apk植入代码完成的。那么既然是选择二次打包完成这个功能,下面的五个部分其实就是我们这个过程的流程。

    说了半天怎么还没有说道关于微信抢红包呢,微信抢红包无非就是写个Xpose插件,Hook微信内部代码实现红包自动领取。我们只要让微信加载Xpose框架,然后装个抢红包插件即可完成这项工作,但是如何在非Root的环境微信有这个功能呢!那就是修改微信源码,修改内部dex文件,让微信启动就加载Xpose框架,然后我们手机装个抢红包插件,二次打包后,微信冷启动后就会把Xpose框架拉起来,自然而然的就会加载抢红包的插件了。从而实现非Root手机微信抢红包。

    如何让实现微信抢红包

    其实说到这里,可能就有点偏离了,之前我有篇文章写道关于Hook微信朋友圈内容的。大家有兴趣可以了解下。里面就是教你如何一步步的找到源码,插入hook点,勾住数据。微信抢红包也是同理,只要我们找到微信领取红包代码的地方即可完成自动抢红包。下面我也会给大家提供一份源码,关于微信抢红包插件的,而且最新版本的微信也是支持的,这里就不过多的叙述了。

    微信逆向之朋友圈

    微信抢红包

    如何加载Xpose框架

    xpose 框架7.0之后作者就并没有对项目进行支吃

    既然要说加载Xpose框架,必然我们要知道他的工作原理,这个框架的牛逼之处就是可以动态劫持Android平台。xpose框架的那么是如何做到动态劫持的呢,可过Xpose的安装方式和脚本,可以知道,他是通过替换我们手机里面的app_process程序控制到zygote进程(从这一点就知道他必须是root手机才可以进行替换),看过Android系统源码的兄弟应该知道,所有的App进程都是通过zygote fork出来的。既然Xpose都控制了zygote进程,那么抓住我们App进程页不足为怪。而且当app_process进程启动的时候会加载一个jar包(XposedBridge.jar)通过观察源码可以看到main()入口,做了哪些操作。

    /**
         * Called when native methods and other things are initialized, but before preloading classes etc.
         * @hide
         */
        @SuppressWarnings("deprecation")
        protected static void main(String[] args) {
            // Initialize the Xposed framework and modules
            try {
                if (!hadInitErrors()) {
                    initXResources();
    
                    SELinuxHelper.initOnce();
                    SELinuxHelper.initForProcess(null);
    
                    runtime = getRuntime();
                    XPOSED_BRIDGE_VERSION = getXposedVersion();
    
                    if (isZygote) {
                        XposedInit.hookResources();
                        XposedInit.initForZygote();
                    }
    
                    XposedInit.loadModules();
                } else {
                    Log.e(TAG, "Not initializing Xposed because of previous errors");
                }
            } catch (Throwable t) {
                Log.e(TAG, "Errors during Xposed initialization", t);
                disableHooks = true;
            }
    
            // Call the original startup code
            if (isZygote) {
                ZygoteInit.main(args);
            } else {
                RuntimeInit.main(args);
            }
        }
    

    可以从上诉源码中看到,有一行代码XposedInit.loadModules()顾名思义就是加载我们手机安装的插件的。那么我们继续进入源码看看里面到底做了什么

    /**
         * Load a module from an APK by calling the init(String) method for all classes defined
         * in <code>assets/xposed_init</code>.
         */
        private static void loadModule(String apk, ClassLoader topClassLoader) {
            Log.i(TAG, "Loading modules from " + apk);
    
            if (!new File(apk).exists()) {
                Log.e(TAG, "  File does not exist");
                return;
            }
    
            DexFile dexFile;
            try {
                dexFile = new DexFile(apk);
            } catch (IOException e) {
                Log.e(TAG, "  Cannot load module", e);
                return;
            }
    
            if (dexFile.loadClass(INSTANT_RUN_CLASS, topClassLoader) != null) {
                Log.e(TAG, "  Cannot load module, please disable \"Instant Run\" in Android Studio.");
                closeSilently(dexFile);
                return;
            }
    
            if (dexFile.loadClass(XposedBridge.class.getName(), topClassLoader) != null) {
                Log.e(TAG, "  Cannot load module:");
                Log.e(TAG, "  The Xposed API classes are compiled into the module's APK.");
                Log.e(TAG, "  This may cause strange issues and must be fixed by the module developer.");
                Log.e(TAG, "  For details, see: http://api.xposed.info/using.html");
                closeSilently(dexFile);
                return;
            }
    
            closeSilently(dexFile);
    
            ZipFile zipFile = null;
            InputStream is;
            try {
                zipFile = new ZipFile(apk);
                ZipEntry zipEntry = zipFile.getEntry("assets/xposed_init");
                if (zipEntry == null) {
                    Log.e(TAG, "  assets/xposed_init not found in the APK");
                    closeSilently(zipFile);
                    return;
                }
                is = zipFile.getInputStream(zipEntry);
            } catch (IOException e) {
                Log.e(TAG, "  Cannot read assets/xposed_init in the APK", e);
                closeSilently(zipFile);
                return;
            }
    
            ClassLoader mcl = new PathClassLoader(apk, XposedBridge.BOOTCLASSLOADER);
            BufferedReader moduleClassesReader = new BufferedReader(new InputStreamReader(is));
            try {
                String moduleClassName;
                while ((moduleClassName = moduleClassesReader.readLine()) != null) {
                    moduleClassName = moduleClassName.trim();
                    if (moduleClassName.isEmpty() || moduleClassName.startsWith("#"))
                        continue;
    
                    try {
                        Log.i(TAG, "  Loading class " + moduleClassName);
                        Class<?> moduleClass = mcl.loadClass(moduleClassName);
    
                        if (!IXposedMod.class.isAssignableFrom(moduleClass)) {
                            Log.e(TAG, "    This class doesn't implement any sub-interface of IXposedMod, skipping it");
                            continue;
                        } else if (disableResources && IXposedHookInitPackageResources.class.isAssignableFrom(moduleClass)) {
                            Log.e(TAG, "    This class requires resource-related hooks (which are disabled), skipping it.");
                            continue;
                        }
    
                        final Object moduleInstance = moduleClass.newInstance();
                        if (XposedBridge.isZygote) {
                            if (moduleInstance instanceof IXposedHookZygoteInit) {
                                IXposedHookZygoteInit.StartupParam param = new IXposedHookZygoteInit.StartupParam();
                                param.modulePath = apk;
                                param.startsSystemServer = startsSystemServer;
                                ((IXposedHookZygoteInit) moduleInstance).initZygote(param);
                            }
    
                            if (moduleInstance instanceof IXposedHookLoadPackage)
                                XposedBridge.hookLoadPackage(new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance));
    
                            if (moduleInstance instanceof IXposedHookInitPackageResources)
                                XposedBridge.hookInitPackageResources(new IXposedHookInitPackageResources.Wrapper((IXposedHookInitPackageResources) moduleInstance));
                        } else {
                            if (moduleInstance instanceof IXposedHookCmdInit) {
                                IXposedHookCmdInit.StartupParam param = new IXposedHookCmdInit.StartupParam();
                                param.modulePath = apk;
                                param.startClassName = startClassName;
                                ((IXposedHookCmdInit) moduleInstance).initCmdApp(param);
                            }
                        }
                    } catch (Throwable t) {
                        Log.e(TAG, "    Failed to load class " + moduleClassName, t);
                    }
                }
            } catch (IOException e) {
                Log.e(TAG, "  Failed to load module from " + apk, e);
            } finally {
                closeSilently(is);
                closeSilently(zipFile);
            }
        }
    }
    

    上诉其实就是Xpose源码中加载module的源码,写过Xpose插件的小伙伴都知道,我们要把插件入口定义在项目中的assets/xposed_init中,这样Xpose框架在读取插件的时候就知道在何处了。上诉源码中也有这么一行来加载的。
    ZipEntry zipEntry = zipFile.getEntry("assets/xposed_init")
    那么我们也就按照这个操作,直接把这个源码搬下来,然后加载到我们普通的项目中,在我们App初始化的时候进行loadmodules,你会发现也是可以支持加载xpose插件的。如果说我们写一个xpose插件是给我们自己App用的,那么是不是就可以实现热修复功能呢。那当然是可以的。

    首先我们遍历我们手机中装的App通过PMS拿到应用信息,找到那些是xposedmodule模块的App,App启动的时候加载这个apk。从而实现我们自己的项目加载xpose插件

        private void loadModulsFormApk(){
            final ArrayList<String> pathList = new ArrayList<>();
            for (PackageInfo next : context.getPackageManager().getInstalledPackages(FileUtils.FileMode.MODE_IWUSR)) {
                ApplicationInfo applicationInfo2 = next.applicationInfo;
                if (applicationInfo2.enabled && applicationInfo2.metaData != null && applicationInfo2.metaData.containsKey("xposedmodule")) {
                    String str4 = next.applicationInfo.publicSourceDir;
                    String charSequence = context.getPackageManager().getApplicationLabel(next.applicationInfo).toString();
                    if (TextUtils.isEmpty(str4)) {
                        str4 = next.applicationInfo.sourceDir;
                    }
                    pathList.add(str4);
                    Log.d("XposedModuleEntry", " query installed module path -> " + str4);
                }
            }
    
        }
    

    以上代码就可以找出哪些App是xpose插件。

    说到这里是不是大家已经明白了些,只要微信在Application初始化的时候,也执行这段代码不就可以完成Xpose框架的加载了吗!然后手机装个微信抢红包插件就可以完成我们的目的了吗。但是如何进加载呢,那我们就需要修改微信内部的源码了,才能完成这一步操作。下面的环节会说道如何修改微信内部源码。

    如何修改Dex包

    既然大家通过上面的环节已经了解了大概原理。那么接下来就是修改源码了。大家也知道,我们下载下来的Apk是是已经打包签名过的。解压出来是一堆文件,还有很多dex文件,微信源码就是在dex文件中,我们只要修改dex文件中的源码然后替换原有的dex,然后打包二次签名就可以完成这个操作了。说起来容易那么如何修改dex文件源码呢。下面听我慢慢叙述!

    Apk中植入代码有两种主流的方式,据我了解。

    • 通过dex2jar工程将dex转化成java代码,修改后,然后在通过jar2dex转化成dex文件。
    • 将Apk反编译成smali,修改smali文件,然后在把修改后的文件打包。

    这里两种方式都有尝试:

    dex2jar github

    第一种dex2jar,我是通过xpatch作者的方式实现的。文章末尾会附上链接,这里我就简单叙述一下。修改dex2jar源码,操作dex文件植入代码。可以在dex2jar文件找到这个doTranslate()方法。这里面操作了我们dex文件所有的源码。植入过程也是在这个方法体中。至于如何编写smali代码可以通过Android studio下载个插件,ASM Bytecode Viewer然后自己写一段代码,然后转化一下即可。

    我们可以在doTranslate()方法中看到这个ExDex2Asm类,他对每个类进行了处理,我们在入口判断是否是我们需要的类然后进行,然后把我们需要植入的代码copy进入即可。

     private void doTranslate(final Path dist) throws IOException {
    
            DexFileNode fileNode = new DexFileNode();
           ........
    
            new ExDex2Asm(exceptionHandler) {
                public void convertCode(DexMethodNode methodNode, MethodVisitor mv) {
                    if (methodNode.method.getOwner().equals(Dex2jar.this.applicationName) && methodNode.method.getName().equals("<clinit>")) {
                        Dex2jar.this.isApplicationClassFounded = true;
                        mv.visitMethodInsn(184, XPOSED_ENTRY_CLASS_NAME, "initXpose", "()V", false);
                    }
    
                    if ((readerConfig & DexFileReader.SKIP_CODE) != 0 && methodNode.method.getName().equals("<clinit>")) {
                        // also skip clinit
                        return;
                    }
                    super.convertCode(methodNode, mv);
                }
    
                @Override
                public void addMethod(DexClassNode classNode, ClassVisitor cv) {
                    if (classNode.className.equals(Dex2jar.this.applicationName)) {
                        Dex2jar.this.isApplicationClassFounded = true;
                        boolean hasFoundClinitMethod = false;
                        if (classNode.methods != null) {
                            Iterator var4 = classNode.methods.iterator();
    
                            while(var4.hasNext()) {
                                DexMethodNode methodNode = (DexMethodNode)var4.next();
                                if (methodNode.method.getName().equals("<clinit>")) {
                                    hasFoundClinitMethod = true;
                                    break;
                                }
                            }
                        }
    
                        if (!hasFoundClinitMethod) {
                            MethodVisitor mv = cv.visitMethod(8, "<clinit>", "()V", (String)null, (String[])null);
                            mv.visitCode();
                            mv.visitMethodInsn(184, XPOSED_ENTRY_CLASS_NAME, "initXpose", "()V", false);
                            mv.visitInsn(177);
                            mv.visitMaxs(0, 0);
                            mv.visitEnd();
                        }
                    }
    
                }
    
               ...............
                @Override
                public void ir2j(IrMethod irMethod, MethodVisitor mv) {
                    new IR2JConverter(0 != (V3.OPTIMIZE_SYNCHRONIZED & v3Config)).convert(irMethod, mv);
                }
            }.convertDex(fileNode, cvf);
    
        }
        
    

    这样我们就完成了代码的植入,这是第一种方式,通过dex2jar工程完成对dex文件代码的植入。但是这个仅仅可以在macos或者windows上操作。这个jar移植到Android设备上是无法运行的,会报错。之后观察无极代码及xpatch提供的apk,看看他们怎么在Android设备上对dex文件修改的,反编译并没有得到结果。但是功夫不负有心人,无意阅读到一篇技术文章,才有了这个灵感,也就是接下来的第二种修改dex文件的方法

    第二种也通过修改smali代码完成代码植入的。这个也是我在不停的寻找发现的方法。

    smali github

    首先我们可以看到这个仓库中有这么一个项目dexlib2,也就是这个项目让我完成了在Android设备上修改dex源码的功能。首先带大家看一下这个类

    smali

    可以发现这个名字很有意思dexrewrite,我在反编译无极的代码的时候也发现了这个类,但是被混淆根本无法下手。同样可以理解为这是个修改dex类方法的实现体。虽然这个类不论是看起来还是听起来都是个关键。但是并非我们所想。网上有很多关于这个的方法。使用说明。

    
    public void modifyDexFile(String filePath){
            DexRewriter dexRewriter = new DexRewriter(new RewriterModule(){
                @Nonnull
                @Override
                public Rewriter<Method> getMethodRewriter(@Nonnull Rewriters rewriters) {
                    return new MethodRewriter(rewriters){
                        @Nonnull
                        @Override
                        public Method rewrite(@Nonnull Method value) {
                            ......添加植入操作......
                            return super.rewrite(value);
                        }
                    };
                }
    
                @Nonnull
                @Override
                public Rewriter<ClassDef> getClassDefRewriter(@Nonnull Rewriters rewriters) {
                    return new ClassDefRewriter(rewriters){
                        @Nonnull
                        @Override
                        public ClassDef rewrite(@Nonnull ClassDef classDef) {
                            ......添加植入操作......
                            return super.rewrite(classDef);
                        }
                    };
                }
            });
            dexRewriter.rewriteDexFile(DexFileFactory.loadDexFile(filePath,Opcodes.getDefault()));
        }
    
    

    这个看起来很完美。但是我用起来并没有卵用。接下来继续说

    屏幕快照 2020-01-21 下午2.50.41.png

    )

    这个才是关键,我们通过DexBackedDexFile加载dex 文件然后,获取他里面的ClassDef集合,然后我们再wrapper一个ClassDef的子类,通过编写smali代码植入到子类中,添加method 或者添加具体代码都是可以的,替换ClassDef,把这个wrapper的类覆盖原有的类。之后把dclassdef文件流重新读入dexfile 达到dex 文件的修改。

    public static void main(String[] args) {
            DexRewriter dexRewriter = new DexRewriter(new RewriterModule() {
                @Nonnull
                @Override
                public Rewriter<Field> getFieldRewriter(@Nonnull Rewriters rewriters) {
                    System.out.println(rewriters);
                    return new FieldRewriter(rewriters) {
                        @Nonnull
                        @Override
                        public Field rewrite(@Nonnull Field field) {
                            System.out.println(field.getName());
                            return super.rewrite(field);
                        }
                    };
                }
    
                @Nonnull
                @Override
                public Rewriter<Method> getMethodRewriter(@Nonnull Rewriters rewriters) {
                    return new MethodRewriter(rewriters) {
                        @Nonnull
                        @Override
                        public Method rewrite(@Nonnull Method value) {
                            System.out.println(value.getName());
                            if (value.getName().equals("onCreate")) {
                                System.out.println("onCreate");
                                return value;
                            }
                            return value;
                        }
                    };
                }
            });
    
    
            try {
                DexBackedDexFile rewrittenDexFile = DexFileFactory.loadDexFile(new File("/Users/cuieney/Downloads/classes.dex"), Opcodes.getDefault());
                dexRewriter.rewriteDexFile(rewrittenDexFile);
    
                DexPool dexPool = new DexPool(rewrittenDexFile.getOpcodes());
                Set<? extends DexBackedClassDef> classes = rewrittenDexFile.getClasses();
                for (ClassDef classDef : classes) {
                    if (classDef.getSuperclass().equals("Landroid/app/Application;")) {
                        System.out.println(classDef.getType());
                        for (Method method : classDef.getVirtualMethods()) {
                            System.out.println("---------virtual method----------");
                            System.out.println(method.getName());
                            System.out.println(method.getParameters());
    
                            if (method.getName().equals("onCreate")) {
                                for (Instruction instruction : method.getImplementation().getInstructions()) {
                                    System.out.println(instruction);
                                }
    
    
                                System.out.println("初始化代码onCreate");
                                ClassDefWrapper classDefWrapper;
                                classDefWrapper = new ClassDefWrapper(classDef);
    
                                Method onCreateMethodInjected = buildOnCreateMethod( method);
                                classDefWrapper.replaceVirtualMethod(onCreateMethodInjected);
                                classDef = classDefWrapper;
                            }
                            System.out.println("---------virtual method end----------");
                        }
    
    
                        for (Method directMethod : classDef.getDirectMethods()) {
                            System.out.println("---------Direct method----------");
                            System.out.println(directMethod.getName());
                            System.out.println(directMethod.getParameters());
                            if (directMethod.getName().equals("<clinit>")) {
                                System.out.println("初始化代码<clinit>");
                                ClassDefWrapper classDefWrapper;
                                classDefWrapper = new ClassDefWrapper(classDef);
    
                                Method onCreateMethodInjected = buildOnCreateMethod( directMethod);
                                classDefWrapper.replaceDirectMethod(onCreateMethodInjected);
                                classDef = classDefWrapper;
                            }
    
                            System.out.println("---------Direct method end----------");
                        }
    
    
                    }
                    dexPool.internClass(classDef);
                }
                String dexfilepath = "/Users/cuieney/Downloads/";
                String outStream = "/Users/cuieney/Downloads/test.dex";
    
                FileOutputStream outputStream = new FileOutputStream(outStream);
                File tempFile = new File(dexfilepath, "targeet.dex");
                dexPool.writeTo(new FileDataStore(tempFile));
                // 再从文件里读取出来
                FileInputStream fileInputStream = new FileInputStream(tempFile);
                byte[] fileData = new byte[512 * 1024];
                int readSize;
                while ((readSize = fileInputStream.read(fileData)) > 0) {
                    outputStream.write(fileData, 0, readSize);
                }
                fileInputStream.close();
                // 删除临时文件
                tempFile.delete();
    
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    

    如果想在电脑上操作修改App dex file 可以通过第一种方式修改,如果想在设备上则可以通过第二种方式,但也不排除可能有第三种方式甚至第四种。以上的描述即可完成dex file的修改。

    如何二次签名Apk

    万事俱备只欠东风,那么如何给我们的这个Apk进行二次签名呢。把原有解压的文件夹,删除META-INF文件夹中的签名文件,进行二次压缩。然后我们用我们自己的签名文件就可以对他进行签名安装。有两种方式可以用:

    1. 在我们电脑环境的java包里面也有这个签名工具jarsigner,网上有很多关于如何签名的这里我也就不多说了。
    2. 在我们的Android设备上可以通过zip-signer的App进行签名。

    总结

    至此,如果你已经了解了以上步骤,且完成了编码,恭喜你你完成了一个大项目。且可以和大佬媲美。

    2019依旧美丽

    2020展望未来

    感谢大家的开源精神值得尊敬

    之后会补上具体功能相关Apk

    参考项目

    dex2jar https://github.com/pxb1988/dex2jar

    smali https://github.com/JesusFreke/smali

    微信抢红包 https://github.com/firesunCN/WechatEnhancement

    Xpatch https://github.com/WindySha/Xpatch

    XposedBridge https://github.com/rovo89/XposedBridge

    太极 https://github.com/taichi-framework/TaiChi

    SandHook(xpose兼容Android7.0-10.0) https://github.com/ganyao114/SandHook

    相关文章

      网友评论

        本文标题:春节福利-教你非Root手机实现微信抢红包

        本文链接:https://www.haomeiwen.com/subject/eijezctx.html