寫這篇的目的有兩個,一個是想告訴廣大還在堅持Android開發的小夥伴繼續加油,還有就是給本身一個今年的技術產出畫個句號吧。最重要的仍是想把本身學到的東西開源供你們參考學習,共勉。html
那咱們就進入主題吧,就目前的市場上看,經過Xpose實現這個功能居大多數,這篇文章也是基於Xpose,你們都知道,使用Xpose前提就是必須Root手機,如今也有一些Up主,完成了此項功能。大名鼎鼎的VirtualXpose經過內部模擬Android原生環境實現加載Xpose插件,已到達Root環境hook進程,可是仍是有些不穩定的,技術難道更爲複雜。還有個也是你們應該也有所瞭解太極Xposed,是經過二次打包Apk植入代碼完成的。可是做者並無公開源碼,並且不少插件用不了,必須讓做者給你發激活碼才能調試本身的插件。我呢,也是個技術迷,就想本身是否能夠也作一個這樣的東西。也就有了如今的這篇文章。java
原理大概分爲如下五個部分,下面不會細說具體實現過程,只會說核心內容,否則這篇文章就太長了,若是想了解具體細節能夠文章評論區艾特我就行。android
既然是非Root加載Xpose框架,那麼我這邊選擇的是太極的實現方式,而非VirtualXpose構建虛擬Android環境,太極的方式便是二次打包Apk植入代碼完成的。那麼既然是選擇二次打包完成這個功能,下面的五個部分其實就是咱們這個過程的流程。git
說了半天怎麼尚未說道關於微信搶紅包呢,微信搶紅包無非就是寫個Xpose插件,Hook微信內部代碼實現紅包自動領取。咱們只要讓微信加載Xpose框架,而後裝個搶紅包插件便可完成這項工做,可是如何在非Root的環境微信有這個功能呢!那就是修改微信源碼,修改內部dex文件,讓微信啓動就加載Xpose框架,而後咱們手機裝個搶紅包插件,二次打包後,微信冷啓動後就會把Xpose框架拉起來,天然而然的就會加載搶紅包的插件了。從而實現非Root手機微信搶紅包。github
其實說到這裏,可能就有點偏離了,以前我有篇文章寫道關於Hook微信朋友圈內容的。你們有興趣能夠了解下。裏面就是教你如何一步步的找到源碼,插入hook點,勾住數據。微信搶紅包也是同理,只要咱們找到微信領取紅包代碼的地方便可完成自動搶紅包。下面我也會給你們提供一份源碼,關於微信搶紅包插件的,並且最新版本的微信也是支持的,這裏就不過多的敘述了。macos
微信逆向之朋友圈windows
微信搶紅包api
xpose 框架7.0以後做者就並無對項目進行支吃bash
既然要說加載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框架的加載了嗎!而後手機裝個微信搶紅包插件就能夠完成咱們的目的了嗎。可是如何進加載呢,那咱們就須要修改微信內部的源碼了,才能完成這一步操做。下面的環節會說道如何修改微信內部源碼。
既然你們經過上面的環節已經瞭解了大概原理。那麼接下來就是修改源碼了。你們也知道,咱們下載下來的Apk是是已經打包簽名過的。解壓出來是一堆文件,還有不少dex文件,微信源碼就是在dex文件中,咱們只要修改dex文件中的源碼而後替換原有的dex,而後打包二次簽名就能夠完成這個操做了。提及來容易那麼如何修改dex文件源碼呢。下面聽我慢慢敘述!
Apk中植入代碼有兩種主流的方式,據我瞭解。
這裏兩種方式都有嘗試:
第一種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代碼完成代碼植入的。這個也是我在不停的尋找發現的方法。
首先咱們能夠看到這個倉庫中有這麼一個項目dexlib2,也就是這個項目讓我完成了在Android設備上修改dex源碼的功能。首先帶你們看一下這個類
能夠發現這個名字頗有意思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()));
}
複製代碼
這個看起來很完美。可是我用起來並無卵用。接下來繼續說
)這個纔是關鍵,咱們經過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進行二次簽名呢。把原有解壓的文件夾,刪除META-INF文件夾中的簽名文件,進行二次壓縮。而後咱們用咱們本身的簽名文件就能夠對他進行簽名安裝。有兩種方式能夠用:
至此,若是你已經瞭解了以上步驟,且完成了編碼,恭喜你你完成了一個大項目。且能夠和大佬媲美。
2019依舊美麗
2020展望將來
感謝你們的開源精神值得尊敬、
以後會補上具體功能相關Apk
dex2jar github.com/pxb1988/dex…
smali github.com/JesusFreke/…
微信搶紅包 github.com/firesunCN/W…
Xpatch github.com/WindySha/Xp…
XposedBridge github.com/rovo89/Xpos…
SandHook(xpose兼容Android7.0-10.0) github.com/ganyao114/S…