|
注册登录后全站资源免费查看下载
您需要 登录 才可以下载或查看,没有账号?立即注册
×
转载自吾爱破/解精华帖芽衣手下的文章
零、前言1、Xposed介绍
Xposed框架(Xposed Framework)是一套开源的、在Android高权限模式下运行的框架服务,可以在不修改APK文件的情况下影响程序运行(修改系统)的框架服务,基于它可以制作出许多功能强大的模块,且在功能不冲突的情况下同时运作。
现在主流的框架有LSPosed、Xposed框架、太极等,内嵌框架有TweakMe、Lspatch等,但是我感觉最好用的就不得不提TweakMe了,好多地方都没有人出过用TweakMe破/解 软件的相关教程,实际上这个框架还是很不错的,作者确实厉害,可以安利一下。不过在安卓13上偶有过签失效的问题(第一次启动没问题,第二次启动就显示盗版了),不知道什么原因。 3、案例软件
Poweramp。之前改汇编虽然可以卡试用,但毕竟不是完整版。改汇编也能改成完整版,不过我试了一下有些小毛病,就放弃了。
我在贴吧注意到APP正常激活的时候有个提示:授权已永/久存储到设备,不再需要在线检查。于是,破/解的思路就开始浮现出来。 一、破/解目标原版有15天的使用限制,到期后无法使用。现在要用手头上的工具进行破/解,使之自动激活完整版,并且能够在不同的手机上工作。
二、准备工具1、Android Studio
2、TweakMe
3、小黄鸟
4、MT、NP等,视情况使用
5、一部root的手机
6、52.88块钱RMB
三、逆向思路首先,这软件是把授权码保存在本地的,在注册后肯定要去私有目录里面把文件给拷贝出来,我们在破/解的时候需要写个代码,在另一台设备运行的时候自动释放授权文件。
当然不会那么容易,因为唯一授权肯定包含了设备信息,要不然没法知道激活了多少次,官网也说得很清楚。 此激活操作将解锁适用于Android的音乐播放器Poweramp。它将移除试用期限限制。此激活操作仅授权用于一台设备/电子邮件组合。
提供有限的激活次数。
发布/分享您的订单ID和电子邮件以及/或通过多个不同的设备重新激活将导致许可永/久性禁用。
一经售出,概不退换。您将无法获得退款。
所以,逆向步骤如下:
1、明确激活时,向服务器发送了什么设备信息;
2、用xp模块逐个拦截,拦截后正常购买软件;
3、注册成功后前往“data/data/包名”拷贝文件备用;
4、把授权文件装入apk,编写Java文件,运行时自动释放;
5、多设备测试是否完成破/解。
四、用小黄鸟进行抓包直接安装软件,然后进入到激活界面,随便输入邮箱和订单号,看看它发送了什么信息。
从上图来看,一些机型、品牌、安卓id等都包含在里面了,看来Deviceid是由多个信息拼合而成的,并不是单一的元素。为了更加方便查阅,可以打开“/system/build.prop”查看对应的信息。
build.prop 是Android系统中一个类似于Windows系统注册表的文件,该文件内定义了系统初始(或永/久)的一些参数属性、功能的开放等。并且在 Android中虽然每一版都有自己独有的参数,但绝大部分都是通用的,且可以起到关键性作用的。
ro.build.id= #build的标识,一般在编译时产生
ro.build.version.sdk= #系统编译时,使用的SDK的版本
ro.build.version.codename= #版本编码名称
ro.build.version.release= #公布的版本,显示为手机信息的系统版本,
ro.build.date= #系统编译的时间
ro.build.type= #系统编译类型
ro.build.user= #系统用户名
ro.build.host= #系统主机名
ro.build.tags=test-keys #系统标记
ro.product.name= #机器名
ro.product.device= #设备名
ro.product.board= #主板名
ro.product.locale.language= #系统语言
ro.product.locale.region= #系统所在地区
net.bt.name= #蓝牙网络中显示的名称
ro.media.enc.jpeg.quality=100 #相机照片压缩质量,此处为100%高质量
ro.media.dec.jpeg.memcap=8000000 #相机捕捉像素,此处为800万像素
dalvik.vm.heapsize= #dalvik的虚拟内存大小
debug.sf.hw=1 #硬件GPU加速,1为开启,0为关闭
persist.adb.notify=0 #USB插入时的特别通知,1为显示,0为关闭
video.accelerate.hw=1 #视/频硬件加速,1为开启,0为关闭
debug.sf.nobootanimation=1 #不显示开机动画,1为关闭动画,0为开启动画
view.touch_slop=15 #触摸屏灵敏度,数值越大越灵敏
view.minimum_fling_velocity=25 #滑动速度
view.scroll_friction=0.008 #滑动误差
wifi.interface=eth0 #WIFI界面
wifi.supplicant_scan_interval=45 #WIFI扫描间隔时间,这里值是45秒,把这个值设置越大越省电 |
获取的这些信息基本上属于android.os.Build,它是一个获取设备一些信息的类,该类的主要信息都是通过一些static的字段获得。现在明确了软件获取了什么设备信息,等下编写xp模块hook的目标就是这些静态字段。
五、编写hook代码hook翻译过来就是钩子,也许很多小白还不知道是什么意思,但换成“拦截”应该一目了然。比如这件古玩是假的,你跟小王说是真的,这就是hook。中途拦截不该返回的数据。
根据开发文档,我们需要自己写hook代码,但是不用生成apk,框架自带编译程序,可以一键内嵌到目标软件,这样就省去了生成独立xp模块的麻烦。
正己版主的帖子也有教学,就是不知道有多少人觉得看了很多,但又觉得好像没看过一样undefined
hook设备码示范,两种写法,可根据实际情况使用。
复制代码 隐藏代码
package com.android.guobao.liao.apptweak.plugin;import android.content.ContentResolver;import com.android.guobao.liao.apptweak.xposed.IXposedHookLoadPackage;import com.android.guobao.liao.apptweak.xposed.XC_LoadPackage;import com.android.guobao.liao.apptweak.xposed.XC_MethodHook;import com.android.guobao.liao.apptweak.xposed.XposedHelpers;public class Xposed_hookid implements IXposedHookLoadPackage { protected void hook_method(String className,ClassLoader classLoader,String methodName, Object... parameterTypesAndCallback){ try { XposedHelpers.findAndHookMethod(className,classLoader,methodName,parameterTypesAndCallback); }catch (Exception ignored){ } } @Override public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpp) throws Throwable{ if ("com.maxmpz.audioplayer".equals(lpp.packageName)) { //过滤软件包名 hook_method( "android.provider.Settings$Secure", //要hook类名路径,现在是安卓id lpp.classLoader, "getString", // 要hook的方法名 ContentResolver.class, String.class, new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable{ String id = "52pojie666666666"; // 设置新的返回值 param.setResult(id); } }); XposedHelpers.setStaticObjectField(android.os.Build.class, "MODEL", "yayi"); //机型 XposedHelpers.setStaticObjectField(android.os.Build.class, "BRAND", "yayi123"); //品牌 XposedHelpers.setStaticObjectField(android.os.Build.class, "FINGERPRINT", "yayi321"); //指纹 XposedHelpers.setStaticObjectField(android.os.Build.class, "BOARD", "yayi520"); //主板 XposedHelpers.setStaticObjectField(android.os.Build.VERSION.class, "RELEASE", "12"); //安卓版本 XposedHelpers.setStaticObjectField(android.os.Build.class, "PRODUCT", "yayi23333"); //产品名称 XposedHelpers.setStaticObjectField(android.os.Build.class, "DEVICE", "yayi1111"); //开发代号 XposedHelpers.setStaticObjectField(android.os.Build.class, "ID", "yayi007"); //编译编号 XposedHelpers.setStaticObjectField(android.os.Build.class, "TAGS", "release-keys"); //release-keys XposedHelpers.setStaticObjectField(android.os.Build.class, "MANUFACTURER", "I_love_China"); //生产商 XposedHelpers.setStaticObjectField(android.os.Build.class, "SERIAL", "001100"); //序列号 XposedHelpers.setStaticObjectField(android.os.Build.class, "HARDWARE", "I_love_China"); //硬件名 XposedHelpers.setStaticObjectField(android.os.Build.VERSION.class, "CODENAME", "REL"); //系统开发代号 XposedHelpers.setStaticObjectField(android.os.Build.class, "DISPLAY", "yayi007"); //版本包 } }}
编写好后修改JavaTweak.java,激活JavaTweak_xposed(modules)插件,还有自己的xp模块。
其实还有一个比较重要的是imei,不过可能是因为隐私问题,软件并没有获取这个值。imei的方法是“getDeviceId”,属于android.telephony.TelephonyManager。
用自带的命令打包成apk后,前往官网购买激活码,保存邮箱和购买订单号,然后在已经hook的软件里面激活它。
六、提取授权文件因为文件比较多,直接把所有的文件夹给提取出来,不过一般在databases文件夹里面。
经过测试,文件databases/folders.db、folders.db-wal保存了授权验证信息,files/fsp/l文件保存了商店信息,就是购买渠道。我随机删了几个文件,发现保留folders.db、folders.db-wal即可实现注册,购买渠道的文件有没有影响不大。不过你如果是破/解共享的话建议还是把这个渠道给加上,免得日后出现反弹等问题。
七、将授权文件放入apk,并自动释放我选择的存放目录是assets,并在该目录下新建了一个“yayi”的文件夹,用于存放数据文件。 assets 目录是专门用于保存各种外部文件的。常见的有:图像、音视/频、配置文件、字体、自带数据库等。之所以说它适合用来管理这些文件,是因为应用程序在编译时不会去处理这个目录下的文件,但是却会将它们打包进 APK 中。而其它你随便创建的目录在编译时就会被直接忽略掉。同时,你可以在 assets 目录内任意创建目录层级关系,这对于有大量外部文件需要集成的应用来说,就能很方便地分类管理了。
然后写一个复制文件的代码,如:
复制代码 隐藏代码
package com.maxmpz.audioplayer;import android.content.Context;import java.io.File;import java.io.FileOutputStream;import java.io.InputStream;public class yayiUtil { public static void copy(Context context, String dataPath, String savePath){ try { String[] fileNames = context.getAssets().list(dataPath); //获取目录下的所有文件及文件夹 if (fileNames.length > 0) { //如果是文件夹 File file = new File(savePath); file.mkdirs(); //如果文件夹不存在,则递归 for (String fileName : fileNames) { copy(context, dataPath + "/" + fileName, savePath + "/" + fileName); } } else { // 如果是文件 InputStream is = context.getAssets().open(dataPath); FileOutputStream fos = new FileOutputStream(savePath); byte[] buffer = new byte[1024]; int byteCount; while ((byteCount = is.read(buffer)) != -1) fos.write(buffer, 0, byteCount); //将读取的输入流写入到输出流 fos.flush(); is.close(); fos.close(); } } catch (Exception e) { e.printStackTrace(); } }}
复制代码 隐藏代码
.class public Lcom/maxmpz/audioplayer/yayiUtil;.super Ljava/lang/Object;# direct methods.method public constructor <init>()V .registers 1 .line 8 invoke-direct {p0}, Ljava/lang/Object;-><init>()V return-void.end method.method public static copy(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)V .registers 16 const/4 v8, 0x0 .line 12 :try_start_1 invoke-virtual {p0}, Landroid/content/Context;->getAssets()Landroid/content/res/AssetManager; move-result-object v9 invoke-virtual {v9, p1}, Landroid/content/res/AssetManager;->list(Ljava/lang/String;)[Ljava/lang/String; move-result-object v5 .line 13 array-length v9, v5 if-lez v9, :cond_4d .line 14 new-instance v3, Ljava/io/File; invoke-direct {v3, p2}, Ljava/io/File;-><init>(Ljava/lang/String;)V .line 15 invoke-virtual {v3}, Ljava/io/File;->mkdirs()Z .line 16 array-length v9, v5 :goto_15 if-ge v8, v9, :cond_6f aget-object v4, v5, v8 .line 17 new-instance v10, Ljava/lang/StringBuilder; invoke-direct {v10}, Ljava/lang/StringBuilder;-><init>()V invoke-virtual {v10, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v10 const-string v11, "/" invoke-virtual {v10, v11}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v10 invoke-virtual {v10, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v10 invoke-virtual {v10}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v10 new-instance v11, Ljava/lang/StringBuilder; invoke-direct {v11}, Ljava/lang/StringBuilder;-><init>()V invoke-virtual {v11, p2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v11 const-string v12, "/" invoke-virtual {v11, v12}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v11 invoke-virtual {v11, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; move-result-object v11 invoke-virtual {v11}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; move-result-object v11 invoke-static {p0, v10, v11}, Lcom/maxmpz/audioplayer/yayiUtil;->copy(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)V .line 16 add-int/lit8 v8, v8, 0x1 goto :goto_15 .line 20 :cond_4d invoke-virtual {p0}, Landroid/content/Context;->getAssets()Landroid/content/res/AssetManager; move-result-object v8 invoke-virtual {v8, p1}, Landroid/content/res/AssetManager;->open(Ljava/lang/String;)Ljava/io/InputStream; move-result-object v7 .line 21 new-instance v6, Ljava/io/FileOutputStream; invoke-direct {v6, p2}, Ljava/io/FileOutputStream;-><init>(Ljava/lang/String;)V .line 22 const/16 v8, 0x400 new-array v0, v8, [B .line 23 const/4 v1, 0x0 .line 24 :goto_5f invoke-virtual {v7, v0}, Ljava/io/InputStream;->read([B)I move-result v1 const/4 v8, -0x1 if-eq v1, v8, :cond_70 .line 26 const/4 v8, 0x0 invoke-virtual {v6, v0, v8, v1}, Ljava/io/FileOutputStream;->write([BII)V :try_end_6a .catch Ljava/lang/Exception; {:try_start_1 .. :try_end_6a} :catch_6b goto :goto_5f .line 32 :catch_6b move-exception v2 .line 33 invoke-virtual {v2}, Ljava/lang/Exception;->printStackTrace()V .line 35 :cond_6f :goto_6f return-void .line 28 :cond_70 :try_start_70 invoke-virtual {v6}, Ljava/io/FileOutputStream;->flush()V .line 29 invoke-virtual {v7}, Ljava/io/InputStream;->close()V .line 30 invoke-virtual {v6}, Ljava/io/FileOutputStream;->close()V :try_end_79 .catch Ljava/lang/Exception; {:try_start_70 .. :try_end_79} :catch_6b goto :goto_6f.end method
再新建一个类名,用于运行后调用和删除一些残留文件。
写法仅供参考,具体需求视情况而定。
复制代码 隐藏代码
package com.maxmpz.audioplayer;import android.content.Context;import java.io.File;public class copy { private static final String a ="yayi"; private static final String b ="data/data/com.maxmpz.audioplayer"; private static final String c ="data/data/com.maxmpz.audioplayer/files/fsp"; public static void yayi(Context context){ deleteDir(c); //先把原来的文件删了 try { Thread.sleep(500); //毫秒 } catch(InterruptedException ex) { Thread.currentThread().interrupt(); } yayiUtil.copy(context,a,b); //开始复制数据 } public static void deleteDir(String fsp) { File file = new File(fsp); if (file.isFile()) { file.delete(); // 删除文件 } else { File[] files = file.listFiles(); if (files == null) { file.delete(); // 删除空文件夹 } else { for (File f : files) { deleteDir(f.getAbsolutePath()); // 迭代删除非空文件夹 } file.delete(); } } }}
复制代码 隐藏代码
.class public Lcom/maxmpz/audioplayer/copy;.super Ljava/lang/Object;# static fields.field private static final a:Ljava/lang/String; = "yayi".field private static final b:Ljava/lang/String; = "data/data/com.maxmpz.audioplayer".field private static final c:Ljava/lang/String; = "data/data/com.maxmpz.audioplayer/files/fsp"# direct methods.method public constructor <init>()V .registers 1 .line 13 invoke-direct {p0}, Ljava/lang/Object;-><init>()V return-void.end method.method public static deleteDir(Ljava/lang/String;)V .registers 7 .line 34 new-instance v1, Ljava/io/File; invoke-direct {v1, p0}, Ljava/io/File;-><init>(Ljava/lang/String;)V .line 35 invoke-virtual {v1}, Ljava/io/File;->isFile()Z move-result v3 if-eqz v3, :cond_f .line 36 invoke-virtual {v1}, Ljava/io/File;->delete()Z .line 48 :goto_e return-void .line 38 :cond_f invoke-virtual {v1}, Ljava/io/File;->listFiles()[Ljava/io/File; move-result-object v2 .line 39 if-nez v2, :cond_19 .line 40 invoke-virtual {v1}, Ljava/io/File;->delete()Z goto :goto_e .line 42 :cond_19 array-length v4, v2 const/4 v3, 0x0 :goto_1b if-ge v3, v4, :cond_29 aget-object v0, v2, v3 .line 43 invoke-virtual {v0}, Ljava/io/File;->getAbsolutePath()Ljava/lang/String; move-result-object v5 invoke-static {v5}, Lcom/maxmpz/audioplayer/copy;->deleteDir(Ljava/lang/String;)V .line 42 add-int/lit8 v3, v3, 0x1 goto :goto_1b .line 45 :cond_29 invoke-virtual {v1}, Ljava/io/File;->delete()Z goto :goto_e.end method.method public static yayi(Landroid/content/Context;)V .registers 5 .line 19 const-string v1, "data/data/com.maxmpz.audioplayer/files/fsp" invoke-static {v1}, Lcom/maxmpz/audioplayer/copy;->deleteDir(Ljava/lang/String;)V .line 21 const-wide/16 v2, 0x1f4 :try_start_7 invoke-static {v2, v3}, Ljava/lang/Thread;->sleep(J)V :try_end_a .catch Ljava/lang/InterruptedException; {:try_start_7 .. :try_end_a} :catch_12 .line 25 :goto_a const-string v1, "yayi" const-string v2, "data/data/com.maxmpz.audioplayer" invoke-static {p0, v1, v2}, Lcom/maxmpz/audioplayer/yayiUtil;->copy(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)V .line 26 return-void .line 22 :catch_12 move-exception v0 .line 23 invoke-static {}, Ljava/lang/Thread;->currentThread()Ljava/lang/Thread; move-result-object v1 invoke-virtual {v1}, Ljava/lang/Thread;->interrupt()V goto :goto_a.end method
[Asm] [color=rgb(51, 102, 153) !important]纯文本查看 [color=rgb(51, 102, 153) !important]复制代码
[backcolor=rgb(27, 36, 38) !important][color=rgb(255, 255, 255) !important][color=#ffffff !important] ?
1
| invoke-static {p0}, Lcom/maxmpz/audioplayer/copy;->yayi(Landroid/content/Context;)V
|
然后在合适的地方调用这个代码。
我选择的地方是com.maxmpz.audioplayer.dialogs.APMUnlockDialogActivity,就是上面的那个激活解锁界面,因为这里只调用一次,激活成功后这个按钮就消失了,所以是插入首选。如果选择启动界面,还需要加入判断,判断是否第一次启动或者是否复制过文件,要不然每次打开APP都会删除数据,这样就容易出现问题。
如果需要判断是否初次安装,可以利用SharedPreferences存储来完成,在之前的破/解中已经用过好多次sp了,这里就点到为止。
SharedPreferences是Android中用于实现存储方式的技术。SharedPreferences的使用非常简单,能够轻松的存放数据和读取数据。SharedPreferences只能保存简单类型的数据,例如,String、int等。一般会将复杂类型的数据转换成Base64编码,然后将转换后的数据以字符串的形式保存在XML文件中,再用SharedPreferences保存。
使用SharedPreferences保存key-value对的步骤如下:
(1)使用Activity类的getSharedPreferences方法获得SharedPreferences对象,其中存储key-value的文件的名称由getSharedPreferences方法的第一个参数指定。
(2)使用SharedPreferences接口的edit获得SharedPreferences.Editor对象。
(3)通过SharedPreferences.Editor接口的putXxx方法保存key-value对。其中Xxx表示不同的数据类型。例如:字符串类型的value需要用putString方法。
(4)通过SharedPreferences.Editor接口的commit方法保存key-value对。commit方法相当于数据库事务中的提交(commit)操作。
八、运行测试将代码打包进apk,先在另一部真机中测试是否正常运行、是否成功复制文件、是否成功自动注册。如果出现异常需要回头查找原因,属于APP的写入保护就比较头疼,是自身的代码问题还好解决。
因为插入的地方是注册界面,所以初次运行肯定是没有注册的,按照常规步骤进入注册界面,一切正常,而且私有目录已经出现了变化,说明文件已经成功删除和添加。关闭APP后再次打开,已完成注册!
九、总结个人感觉内置授权码难倒是不难,非要说难点就是愿不愿意自掏腰包了。
当然你如果说逆向算法并写死文件这种就比较逆天了……
好吧我不会。
正己发的教程贴对新手来说是不错的,但是从实操来看逆向中会有很多意想不到的情况,比如这软件如果在初次运行前就已经有文件的话会闪退,所以不能过早复制文件。从整个流程来看,会使用较多的工具进行辅/助分析,所以单单只会编写hook代码还是不足以破/解一款软件的,正己1~10新手教程会让你对逆向有个初步的认识,系统的学习后对于破/解一些简单的软件应该是没有什么阻碍。
本次实战利用了多方面的知识,方法不唯一,仅供参考。本文目的是让部分学员学习后,如何把hook代码打包到安装包里面,而不是写完就完了,怎么用都不知道。
除了文中介绍的框架外,还有Lspatch,比较成熟的一款框架(支持安卓9以上,但适配的机型会比较多),还支持模块内嵌,内嵌后可以脱离本机使用,适合黑客发布破/解包。
不知道主题怎么写,直接模仿正己的好了,发帖格式也是照搬,要是以前我懒得用MD发帖。undefined
成品在poweramp吧,想要自己去拿。(根据github反馈和吧友反馈,安卓10无法运行。安卓10的话我做成xp模块用lsp内嵌正常运行。)
|
|