春節(jié)福利-教你非Root手機實現(xiàn)微信搶紅包

前提

寫這篇的目的有兩個,一個是想告訴廣大還在堅持Android開發(fā)的小伙伴繼續(xù)加油,還有就是給自己一個今年的技術(shù)產(chǎn)出畫個句號吧。最重要的還是想把自己學到的東西開源供大家參考學習,共勉。

簡述

那我們就進入主題吧,就目前的市場上看,通過Xpose實現(xiàn)這個功能居大多數(shù),這篇文章也是基于Xpose,大家都知道,使用Xpose前提就是必須Root手機,現(xiàn)在也有一些Up主,完成了此項功能。大名鼎鼎的VirtualXpose通過內(nèi)部模擬Android原生環(huán)境實現(xiàn)加載Xpose插件,已到達Root環(huán)境hook進程,但是還是有些不穩(wěn)定的,技術(shù)難道更為復雜。還有個也是大家應該也有所了解太極Xposed,是通過二次打包Apk植入代碼完成的。但是作者并沒有公開源碼,而且很多插件用不了,必須讓作者給你發(fā)激活碼才能調(diào)試自己的插件。我呢,也是個技術(shù)迷,就想自己是否可以也做一個這樣的東西。也就有了現(xiàn)在的這篇文章。

原理

原理大概分為以下五個部分,下面不會細說具體實現(xiàn)過程,只會說核心內(nèi)容,不然這篇文章就太長了,如果想了解具體細節(jié)可以文章評論區(qū)@我就行。

既然是非Root加載Xpose框架,那么我這邊選擇的是太極的實現(xiàn)方式,而非VirtualXpose構(gòu)建虛擬Android環(huán)境,太極的方式即是二次打包Apk植入代碼完成的。那么既然是選擇二次打包完成這個功能,下面的五個部分其實就是我們這個過程的流程。

說了半天怎么還沒有說道關(guān)于微信搶紅包呢,微信搶紅包無非就是寫個Xpose插件,Hook微信內(nèi)部代碼實現(xiàn)紅包自動領(lǐng)取。我們只要讓微信加載Xpose框架,然后裝個搶紅包插件即可完成這項工作,但是如何在非Root的環(huán)境微信有這個功能呢!那就是修改微信源碼,修改內(nèi)部dex文件,讓微信啟動就加載Xpose框架,然后我們手機裝個搶紅包插件,二次打包后,微信冷啟動后就會把Xpose框架拉起來,自然而然的就會加載搶紅包的插件了。從而實現(xiàn)非Root手機微信搶紅包。

如何讓實現(xiàn)微信搶紅包

其實說到這里,可能就有點偏離了,之前我有篇文章寫道關(guān)于Hook微信朋友圈內(nèi)容的。大家有興趣可以了解下。里面就是教你如何一步步的找到源碼,插入hook點,勾住數(shù)據(jù)。微信搶紅包也是同理,只要我們找到微信領(lǐng)取紅包代碼的地方即可完成自動搶紅包。下面我也會給大家提供一份源碼,關(guān)于微信搶紅包插件的,而且最新版本的微信也是支持的,這里就不過多的敘述了。

微信逆向之朋友圈

微信搶紅包

如何加載Xpose框架

xpose 框架7.0之后作者就并沒有對項目進行支吃

既然要說加載Xpose框架,必然我們要知道他的工作原理,這個框架的牛逼之處就是可以動態(tài)劫持Android平臺。xpose框架的那么是如何做到動態(tài)劫持的呢,可過Xpose的安裝方式和腳本,可以知道,他是通過替換我們手機里面的app_process程序控制到zygote進程(從這一點就知道他必須是root手機才可以進行替換),看過Android系統(tǒng)源碼的兄弟應該知道,所有的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()顧名思義就是加載我們手機安裝的插件的。那么我們繼續(xù)進入源碼看看里面到底做了什么

/**
     * 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初始化的時候進行l(wèi)oadmodules,你會發(fā)現(xiàn)也是可以支持加載xpose插件的。如果說我們寫一個xpose插件是給我們自己App用的,那么是不是就可以實現(xiàn)熱修復功能呢。那當然是可以的。

首先我們遍歷我們手機中裝的App通過PMS拿到應用信息,找到那些是xposedmodule模塊的App,App啟動的時候加載這個apk。從而實現(xiàn)我們自己的項目加載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插件。

說到這里是不是大家已經(jīng)明白了些,只要微信在Application初始化的時候,也執(zhí)行這段代碼不就可以完成Xpose框架的加載了嗎!然后手機裝個微信搶紅包插件就可以完成我們的目的了嗎。但是如何進加載呢,那我們就需要修改微信內(nèi)部的源碼了,才能完成這一步操作。下面的環(huán)節(jié)會說道如何修改微信內(nèi)部源碼。

如何修改Dex包

既然大家通過上面的環(huán)節(jié)已經(jīng)了解了大概原理。那么接下來就是修改源碼了。大家也知道,我們下載下來的Apk是是已經(jīng)打包簽名過的。解壓出來是一堆文件,還有很多dex文件,微信源碼就是在dex文件中,我們只要修改dex文件中的源碼然后替換原有的dex,然后打包二次簽名就可以完成這個操作了。說起來容易那么如何修改dex文件源碼呢。下面聽我慢慢敘述!

Apk中植入代碼有兩種主流的方式,據(jù)我了解。

  • 通過dex2jar工程將dex轉(zhuǎn)化成java代碼,修改后,然后在通過jar2dex轉(zhuǎn)化成dex文件。
  • 將Apk反編譯成smali,修改smali文件,然后在把修改后的文件打包。

這里兩種方式都有嘗試:

dex2jar github

第一種dex2jar,我是通過xpatch作者的方式實現(xiàn)的。文章末尾會附上鏈接,這里我就簡單敘述一下。修改dex2jar源碼,操作dex文件植入代碼。可以在dex2jar文件找到這個doTranslate()方法。這里面操作了我們dex文件所有的源碼。植入過程也是在這個方法體中。至于如何編寫smali代碼可以通過Android studio下載個插件,ASM Bytecode Viewer然后自己寫一段代碼,然后轉(zhuǎn)化一下即可。

我們可以在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文件修改的,反編譯并沒有得到結(jié)果。但是功夫不負有心人,無意閱讀到一篇技術(shù)文章,才有了這個靈感,也就是接下來的第二種修改dex文件的方法

第二種也通過修改smali代碼完成代碼植入的。這個也是我在不停的尋找發(fā)現(xiàn)的方法。

smali github

首先我們可以看到這個倉庫中有這么一個項目dexlib2,也就是這個項目讓我完成了在Android設備上修改dex源碼的功能。首先帶大家看一下這個類

smali

可以發(fā)現(xiàn)這個名字很有意思dexrewrite,我在反編譯無極的代碼的時候也發(fā)現(xiàn)了這個類,但是被混淆根本無法下手。同樣可以理解為這是個修改dex類方法的實現(xiàn)體。雖然這個類不論是看起來還是聽起來都是個關(guān)鍵。但是并非我們所想。網(wǎng)上有很多關(guān)于這個的方法。使用說明。


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()));
    }

這個看起來很完美。但是我用起來并沒有卵用。接下來繼續(xù)說

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

)

這個才是關(guān)鍵,我們通過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. 在我們電腦環(huán)境的java包里面也有這個簽名工具jarsigner,網(wǎng)上有很多關(guān)于如何簽名的這里我也就不多說了。
  2. 在我們的Android設備上可以通過zip-signer的App進行簽名。

總結(jié)

至此,如果你已經(jīng)了解了以上步驟,且完成了編碼,恭喜你你完成了一個大項目。且可以和大佬媲美。

2019依舊美麗

2020展望未來

感謝大家的開源精神值得尊敬

之后會補上具體功能相關(guān)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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容