写这篇的目的有两个,一个是想告诉广大还在坚持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…