手把手教你逆向分析 Android 程序

不少人写文章,喜欢把什么行业现状啊,研究现状啊什么的写了一大通,感受好像在写毕业论文似的,我这不废话,先直接上几个图,感觉一下。php

第一张图是在把代码注入到地图里面,启动首页的时候弹出个浮窗,下载网络的图片,苍老师大家不会不认识吧?html

第二张图是微信运动步数做弊,6不6?ok,那咱们从头提及。java

1. 反编译

Android 的反编译,相信你们都应该有所了解,apktool、JEB 等工具。咱们先看一下 Apk 文件的结构吧,以下图:android

  1. META-INF:签名文件(这个是如何生成的后面会提到)git

  2. res:资源文件,里面的 xml 格式文件在编译过程当中由文本格式转化为二进制的 AXML 文件格式github

  3. AndroidManifest.xml:android 配置文件,编译过程依然被转换为 AXML 格式算法

  4. classes.dex:java 代码编译后产生的相似字节码的文件(dalvik 字节码)windows

  5. resources.arsc:具备 id 值资源的索引表(asserts 文件夹中的资源不会生成索引)服务器

  6. 其余文件:可由开发者本身添加,诸如 assets 等,或者 lib(native so 代码)等目录微信

(Android 编译打包过程分析参看:http://blog.csdn.net/luosheng...

apk的核心逻辑主要在 classes.dex 中,破解和二次打包也基本上对这个文件作手脚,因此对这个文件的保护也尤其重要。

上图为通常 Apk 的破解过程(windows 画图工具画的比较搓)

咱们首先用 apktool 工具反编译:java -jar apktool.jar d -f xxx.apk outDir(PS:outDir 不写会在当前目录输出)

反编译后的目录结构以下:

这里,res 里的 xml 和 manifset.xml 都已是解出后的 xml 了,不是 axml 格式了,res 目录里的 values 目录下的 public.xml 能够看到资源对应的 id

若是命令 java -jar apktool.jar d -f再加入 -r 表明资源文件不反编译,上图的目录中将依然有resources.arsc,xml 都是 axml 格式的,也找不到 public.xml

其实咱们主要关注的是 smali 这个目录,里面是按照 android 程序编写的时候 java 文件的目录格式生成的,可是里面的文件并非 java 格式的,而是 smali 格式的,相似 MainActivity.smali。

那么什么是 smali 文件呢?

  1. Smali 是 Android 的 Dalvik 虚拟机所使用的一种 dex 格式的中间语言

  2. 能够理解为,C 语言和汇编语言的编译与反编译,把 smali 理解为一种汇编语言

咱们能够打开一个 smali 文件看看,咱们可使用 notepad++ 打开,而后定一下 smali 语法的高亮显示

<NotepadPlus>
  <UserLang name="smali" ext="smali">
    <Settings>
      <Global caseIgnored="no" />
    </Settings>
    <KeywordLists>
      <Keywords name="Delimiters"></Keywords>
      <Keywords name="Folder+"></Keywords>
      <Keywords name="Folder-"></Keywords>
      <Keywords name="Operators">' ! " ( ) , ; : @ [ ] { }</Keywords>
      <Keywords name="Comment">0#</Keywords>
          <Keywords name="Words1">move move/from16 move/16 move-wide move-wide/from16 move-wide/16 move-object move-object/from16 move-object/16 move-result move-result-wide move-result-object move-exception return-void return return-wide return-object const/4 const/16 const const/high16 const-wide/16 const-wide/32 const-wide const-wide/high16 const-string const-string/jumbo const-class monitor-enter monitor-exit check-cast instance-of array-length new-instance new-array filled-new-array filled-new-array/range fill-array-data throw goto goto/16 goto/32 packed-switch sparse-switch cmpl-float cmpg-float cmpl-double cmpg-double cmp-long if-eq if-ne if-lt if-ge if-gt if-le if-eqz if-nez if-ltz if-gez if-gtz if-lez aget aget-wide aget-object  aget-boolean aget-byte aget-char aget-short aget-short aput aput-wide aput-object aput-boolean aput-byte aput-char aput-short iget iget-wide iget-object iget-boolean iget-char iget-short iput iput-wide iput-object iput-boolean iput-byte iput-char iput-short sget sget-wide sgetobject sget-boolean sget-byte sget-char sget-short sput sput-wide sput-object sput-boolean sput-byte sput-char sput-short invoke-virtual invoke-super invoke-direct invoke-static invoke-interface invoke-virtual/range invoke-super/range invoke-direct/range invoke-static/range invoke-interface/range neg-int not-int neg-long neg-float neg-double int-tolong int-tofloat int-to-double long-to-int long-to-float long-to-double float-to-int float-to-long double-to-double double-to-int double-to-long double-to-float int-to-byte int-to-char int-to-short add-int sub-int mul-int div-int rem-int and-int or-int xor-int shl-int shr-int ushr-int add-long sub-long mul-long div-long rem-long and-long or-long xor-long shl-long shr-long ushr-long add-float sub-float mul-float div-float rem-float add-double sub-double mul-double div-double rem-double add-int/2addr sub-int/2addr mul-int/2addr div-int/2addr rem-int/2addr and-int/2addr or-int/2addr xor-int/2addr shl-int/2addr shr-int/2addr usnhr-int/2addr add-long/2addr sub-long/2addr mul-long/2addr div-long/2addr rem-long/2addr and-long/2addr or-long/2addr xor-long/2addr shl-long/2addr shr-long/2addr ushr-long/2addr add-float/2addr sub-float/2addr mul-float/2addr div-float/2addr rem-float/2addr add-double/2addr mul-double/2addr div-double/2addr rem-double/2addr add-int/lit16 rsub-int mul-int/lit16 div-int/lit16 and-int.lit16 or-int/lit16 xor-int/lit16 and-int/lit8 mul-int/lit8 div-int/lit8</Keywords>
          <Keywords name="Words2">.method .annotation .end  .line .prologue .implements .super .class .source  .locals .parameter .field .local .restart</Keywords>
       <Keywords name="Words3">public annotation method protected static final field private synthetic local</Keywords>
      <Keywords name="Words4">Z V I F</Keywords>
    </KeywordLists>
    <Styles>
          <WordsStyle name="DEFAULT" styleID="11" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" />
              <WordsStyle name="FOLDEROPEN" styleID="12" fgColor="FF0000" bgColor="FFFFFF" fontName="" fontStyle="0" />
              <WordsStyle name="FOLDERCLOSE" styleID="13" fgColor="FF0000" bgColor="FFFFFF" fontName="" fontStyle="0" />
          <WordsStyle name="KEYWORD1" styleID="5" fgColor="FF8040" bgColor="FFFFFF" fontName="Consolas" fontStyle="1" fontSize="10" />
          <WordsStyle name="KEYWORD2" styleID="6" fgColor="91A62D" bgColor="FFFFFF" fontName="Consolas" fontStyle="2" fontSize="10" />
          <WordsStyle name="KEYWORD3" styleID="7" fgColor="004080" bgColor="FFFFFF" fontName="Consolas" fontStyle="0" fontSize="10" />
          <WordsStyle name="KEYWORD4" styleID="8" fgColor="FF0000" bgColor="FFFFFF" fontName="Consolas" fontStyle="0" fontSize="10" />
          <WordsStyle name="COMMENT" styleID="1" fgColor="FF8080" bgColor="FFFFFF" fontName="Consolas" fontStyle="2" fontSize="10" />
          <WordsStyle name="COMMENT LINE" styleID="2" fgColor="008000" bgColor="FFFFFF" fontName="Consolas" fontStyle="2" fontSize="10" />
          <WordsStyle name="NUMBER" styleID="4" fgColor="D9006C" bgColor="FFFFFF" fontName="Consolas" fontStyle="0" fontSize="10" />
          <WordsStyle name="OPERATOR" styleID="10" fgColor="008040" bgColor="FFFFFF" fontName="" fontStyle="0" />
             <WordsStyle name="DELIMINER1" styleID="14" fgColor="AF2BFF" bgColor="FFFFFF" fontName="" fontStyle="0" />
              <WordsStyle name="DELIMINER2" styleID="15" fgColor="AF2BFF" bgColor="FFFFFF" fontName="" fontStyle="0" />
              <WordsStyle name="DELIMINER3" styleID="16" fgColor="000000" bgColor="FFFFFF" fontName="" fontStyle="0" />
    </Styles>
     </UserLang>
</NotepadPlus>

能够参看 http://www.ourunix.org/post/1... 操做。

打开 MainActivity.smali 文件,头三行代码大体以下:

.class public Lcom/example/hacktest/MainActivity;
.super Landroid/app/Activity;
.source "MainActivity.java"

smali 语法这里就不介绍了,本身查资料就好 smali 文件语法参考 http://my.oschina.net/xiahuaw...

这里举个例子,咱们写个程序,一个 edittext 一个 button,当 edit 里面输入正确的文字的时候,点击 button 才会弹出正确的 toast。

咱们看一下,反编译后的关键代码

能够看到这是一个参数为 string,返回值为 boolean 名叫 check 的函数,当输入为“11”的时候才返回 true。

咱们能够把第一个红框位置的 if-eqz 改为 if-nez,这样你输入除了11的任何字符都会返回 true。或者把第二个红框位置的 0x1,改为 0x0,(0表明 true),这样这个函数无论输入什么都返回 true。

OK,这样咱们就改完了,咱们从新编译:java -jar apktool.jar b -f outDir xxx.apk
(PS:xxx.apk 能够不写,会在 outDir 里生成 dist 目录,编译好的 Apk 在这里面)

签名:能够网上下载工具 autoSign,使用方法略。

安装 Apk 后验证,经过。

可是事情并不老是如咱们所愿,有些 Apk 会作一些盗版检测机制,就是为了防止二次重打包。以手机暴风影音为例,当你按照上述步骤反编译,从新编译,签名以后,进入 APP 会出现这个页面,没法正常使用

由于你并无这个 APP 的正版签名文件(关于签名相关的东西,在后面我再仔细讲)

那么这个原理是什么呢,咱们大胆猜想一下,无非就是和上一个例子相似的 check 函数,两个值的对比,那么这个值必定是签名。

咱们先经过方法拿到正版手机暴风影音的签名 md5,而后在反编译后的代码中搜索一下这个值。

然而并无搜到,再换个思路,咱们搜索获取签名的这个函数,从而定位关键代码,获取应用签名的 java 代码相似是:

PackageInfo pi = context.getPackageManager.().getPackageInfo(packname,packageManager.GET_SIGNATURES);
对应的smali代码相似是:
Landroid/content/pm/PackageInfo;->signatures:[Landroid/content/pm/Signature

咱们再搜用这段代码搜索,在 StormUtils2.smali 里面找到了,发如今函数 getSignInfo 里面,继续跟踪到 checkPiracy 函数。

看到这个函数发现就和上例中的 check 函数相似了,改一下返回值为 true 就行了。

咱们再仔细看看这个函数,发现关键的签名 md5 值被拆开存放了,因此咱们才没有搜到,这也是防范搜索的一个举措吧(虽然我以为并没什么用)

const-string/jumbo v3, "dbbf60f096b326003"
const-string/jumbo v0, "c388a350d1578d5"

好的,修改后,咱们再从新编译、签名,验证经过。
(PS:关于签名检测的除了 java 层的,可能还有再 so 里面校验的和服务器验证的方式,在 so 里的用 IDA 打开 so 跟踪修改,服务器验证的抓包查看,再模拟发包重放攻击就行了,这里就不具体介绍了)

2. Android 的签名保护机制究竟是什么?

android 系统禁止更新安装签名不一致的 apk,若是咱们修改了 apk 又用别的签名文件签名,确定是不一致的。

咱们从签名工具 autoSign 分析,看一下 sign.bat 文件内容:

-----------------------------------
@ECHO OFF
Echo Auto-sign Created By Dave Da illest 1
Echo Update.zip is now being signed and will be renamed to update_signed.zip
 
java -jar signapk.jar testkey.x509.pem testkey.pk8 update.apk update_signed.apk
 
Echo Signing Complete
 
Pause
EXIT
-----------------------------------

看一下 java -jar signapk.jar testkey.x509.pem testkey.pk8 update.apk update_signed.apk 这行的意义:

以testkey.x509.pem 这个公钥文件和 testkey.pk8 这个私钥文件对 update.apk 进行签名,签名后保存为 update_signed.apk

咱们能够看到签名前和签名后比较,签名后的文件中多了一个文件夹“META-INF”,里面有三个文件 MANIFEST.MF 、 CERT.SF 、 CERT.RSA。

咱们经过 jd-gui 工具打开 signapk.jar,找到 main 函数,经过这个函数跟踪代码

  1. addDigestsToManifest 这个函数,遍历 apk 中全部文件,对非文件夹非签名文件的文件逐个生成 SHA1 数字签名信息,再 base64 编码。而后再写入 MANIFEST.MF 文件中,生成文件以下:

    -----------------------------------
    Manifest-Version: 1.0
    Created-By: 1.0 (Android)
     
    Name: res/drawable-xhdpi/ic_launcher.png
    SHA1-Digest: AfPh3OJoypH966MludSW6f1RHg4=
     
    Name: AndroidManifest.xml
    SHA1-Digest: NaPhUBH5WO7uGk/CfRu/SHsCvW0=
     
    Name: res/drawable-mdpi/ic_launcher.png
    SHA1-Digest: RRxOSvpmhVfCwiprVV/wZlaqQpw=
     
    Name: res/drawable-hdpi/ic_launcher.png
    SHA1-Digest: Nq8q3HeTluE5JNCBpVvNy3BXtJI=
     
    Name: res/layout/activity_main.xml
    SHA1-Digest: kxwMyILwF2K+n9ziNhcQqcCGWIU=
     
    Name: resources.arsc
    SHA1-Digest: q7Ystu6WoSWih53RGKXtE3LeTdc=
     
    Name: classes.dex
    SHA1-Digest: Ao1WOs5PXMxsWTDsjSijS2tfnHo=
     
    Name: res/drawable-xxhdpi/ic_launcher.png
    SHA1-Digest: GVIfdEOBv4gEny2T1jDhGGsZOBo=
    -----------------------------------

    SHA1 生成的摘要信息,若是你修改了某个文件,apk 安装校验时,取到的该文件的摘要与 MANIFEST.MF 中对应的摘要不一样,则安装不成功

  2. 接下来对以前生成的 manifest 使用 SHA1withRSA 算法, 用私钥签名,writeSignatureFile 这个函数,最后生成 CERT.SF 文件,以下:

    -----------------------------------
    Signature-Version: 1.0
    Created-By: 1.0 (Android)
    SHA1-Digest-Manifest: pNZ9UXN9GMqTgqAwKD6uEN6aD34=
     
    Name: res/drawable-xhdpi/ic_launcher.png
    SHA1-Digest: cIga++hy5wqjHl9IHSfbg8tqCug=
     
    Name: AndroidManifest.xml
    SHA1-Digest: oRzzLkwuvxC78suvJcAEvTqcjSA=
     
    Name: res/drawable-mdpi/ic_launcher.png
    SHA1-Digest: VY7kOF8E3rn8EUTvQC/DcBEN6kQ=
     
    Name: res/drawable-hdpi/ic_launcher.png
    SHA1-Digest: stS7pUucSY0GgAVoESyO3Y7SanU=
     
    Name: res/layout/activity_main.xml
    SHA1-Digest: Yr3img6SqiKB+1kwcg/Fga2fwcc=
     
    Name: resources.arsc
    SHA1-Digest: j1g8I4fI9dM9hAFKEtS9dHsqo5E=
     
    Name: classes.dex
    SHA1-Digest: Sci9MmGXNGnZ1d04rCrEEV7MWn4=
     
    Name: res/drawable-xxhdpi/ic_launcher.png
    SHA1-Digest: KKqaLh/DVvFp+v1KoaDw7xETvrI=
    -----------------------------------
    用私钥经过 RSA 算法对 manifest 里的摘要信息进行加密,安装的时候只能经过公钥解密,解密以后才能得到正确的摘要,再对比
  3. 最后就是如何生成 CERT.RSA,打开这个文件看到的是乱码,说明整个文件都被编码加密了,并且这个文件和公钥有关。从源码中看出他是经过 PKCS7 将整个文件加密了。

总结:

  1. 签名只是对完整性和签名发布机构的校验机制

  2. 不能阻止 apk 被修改,只是签名没法保持一致

  3. 不一样私钥对应着不一样的公钥,实质上不一样的公钥就表明了不一样的签名

3. Android 系统如何获取签名

咱们从获取下面一段代码开始分析:

packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES );
Signature[] signs = packageInfo.signatures;
md5 = getMD5Str(signs[ 0].toByteArray());
context.getPackageManager()其实拿到的是ApplicationPackageManager

看一下 context 的实现类 contextImpl 里的 getPackageManager()

@Override
public PackageManager getPackageManager() {
    if (mPackageManager != null) {
        return mPackageManager ;
    }

    IPackageManager pm = ActivityThread.getPackageManager ();
    if (pm != null) {
        // Doesn't matter if we make more than one instance.
        return (mPackageManager = new ApplicationPackageManager(this, pm));
    }

    return null ;
}

能够看到 ActivityThread 中方法 getPackageManager 获取 IPackageManager

继续看 ActivityThread 的代码:

public static IPackageManager getPackageManager() {
if (sPackageManager != null) {
    //Slog.v("PackageManager", "returning cur default = " + sPackageManager);
    return sPackageManager ;
}
IBinder b = ServiceManager.getService("package");
//Slog.v("PackageManager", "default service binder = " + b);
sPackageManager = IPackageManager.Stub.asInterface(b);
//Slog.v("PackageManager", "default service = " + sPackageManager);
return sPackageManager;
}

看源码知道是经过 ServiceManager.getService("package");获取 PackageManagerService 并获得 IBinder对象,而后经过 asInterface 函数取得接口类 IPackageManager 实例。而后做为参数构造 ApplicationPackageManager,再看 ApplicationPackageManager 这个类里的方法:

@Override
public PackageInfo getPackageInfo(String packageName, int flags)
        throws NameNotFoundException {
    try {
        PackageInfo pi = mPM.getPackageInfo(packageName, flags, mContext.getUserId());
        if (pi != null) {
            return pi;
        }
    } catch (RemoteException e) {
        throw new RuntimeException( "Package manager has died" , e);
    }

    throw new NameNotFoundException(packageName);
}

这里的mPM就是上面构造函数传进来的 IPackageManager,就能够调用 PackageManagerService 的方法了

下图是 PackagerManager 静态类结构图:

ok,看完上图的结构图,继续跟代码 ,看到这里是 mPM 继续调用的getPackageInfo(代理模式),经过进程通讯到 PackageManagerService 中执行响应操做

@Override
public PackageInfo getPackageInfo(String packageName, int flags, int userId) {
    if (!sUserManager.exists(userId)) return null;
    enforceCrossUserPermission(Binder.getCallingUid (), userId, false, "get package info");
    // reader
    synchronized (mPackages) {
        PackageParser.Package p = mPackages.get(packageName);
        if (DEBUG_PACKAGE_INFO)
            Log.v (TAG, "getPackageInfo " + packageName + ": " + p);
        if (p != null) {
            return generatePackageInfo(p, flags, userId);
        }
        if((flags & PackageManager. GET_UNINSTALLED_PACKAGES ) != 0) {
            return generatePackageInfoFromSettingsLPw(packageName, flags, userId);
        }
        }
    return null ;
}

这里 mPackages 是 hashMap,其调用put方法的时机是在 scanPackageLI 方法中,而 scanPackageLI 的调用地方是在程序安装和替换函数中,还有就是 scanDirLi 中,代码略。

scanDirLi 是用来扫描一些系统目录的的,在 PackageManagerService 的构造函数中调用的

File dataDir = Environment. getDataDirectory();
mAppDataDir = new File(dataDir, "data");
mAppInstallDir = new File(dataDir, "app");
mAppLibInstallDir = new File(dataDir, "app-lib" );
mAsecInternalPath = new File(dataDir, "app-asec" ).getPath();
mUserAppDataDir = new File(dataDir, "user");
mDrmAppPrivateInstallDir = new File(dataDir, "app-private" );

上面是扫描的位置↑↑↑↑↑↑↑

PackageManagerService 处理各类应用的安装、卸载、管理等工做,开机时由 systemServer 启动此服务。就是说以前安装过的应用或者系统应用信息都会在开机扫描过程当中存到 mPackages 这个 hashMap 中。开机后用户的安装操做也会一样存到这个 hashMap 里面。

继续看 getPackageInfo,调用的 generatePackageInfo , 里面调用的是 PackageParser 中的 generatePackageInfo,继续跟
这个函数的代码比较长,只贴出部分关键代码:

if ((flags&PackageManager. GET_SIGNATURES ) != 0) {
       int N = (p.mSignatures != null) ? p.mSignatures.length : 0;
       if (N > 0) {
            pi.signatures = new Signature[N];
            System.arraycopy (p.mSignatures, 0, pi. signatures, 0 , N);
        }
    }
    return pi;

这段代码以前主要是作一些复制的操做,就是 new 一个 PackageInfo,而后把 PackageParser.Package 对象中的一些内容复制到这个 PackageInfo 中

从这段代码能够看出来,最终获得的 signatures 信息就是中 p(PackageParser.Package )中的成员 mSignatures 中得来的。

好了,如今就是看这个PackageParser.Package是从哪来的了,经过跟踪代码,installPackageLI 和 scanPackageLI 中的 final PackageParser.Package pkg = pp.parsePackag (tmpPackageFile, null, mMetrics, parseFlags);这行代码只是生成了 pkg,并无赋值里面的 mSignatures,继续跟踪,找到函数 collectCertificatesLI,找到 pp.collectCertificates(pkg , parseFlags)

PS: 最终又进行了一系列的跟代码,找到了 JarVerifier.java 这个类的 readCertificates 这个就是用来读取.RSA 文件的,最终咱们看到的里面的代码是经过 loadCertificates 取得的 certs 赋值给了 pkg.mSignatures

至此,获取签名的全部逻辑就算是简单的过一遍了,如下是简略流程图:(发现 ppt 画图比 Windows 画图工具好用多了,哈哈)

在跟代码的过程当中也找到了签名对比的函数 compareSignatures ,有空本身看看就行了。

OK,绕了这么久咱们终于找到源头了,获取签名就是在 META-INF 中寻找,并解析。试想一下,若是咱们修改了这个函数,让他解析原来正版的 META-INF 中的 CERT.RSA 文件,这样就能够伪造为真正的签名了。

那么咱们就想到了 HOOK,(不少人都是从看雪论坛上找到的一篇文章看到的 http://bbs.pediy.com/showthre...) hook 的原来简单来讲就是,找到原函数和新函数的指针位置,而后兑换内容,将新函数替代原函数。

关于 hook 呢,网上也有不少框架可使用,好比:

  1. Cydia substrate : http://www.cydiasubstrate.com/

  2. Xposed : http://repo.xposed.info/
    网上也有不少教程能够看看

乌云(wooyun)上有一篇颇有意思的教程,就是利用 hook 进行微信运动做弊,原帖地址:http://drops.wooyun.org/tips/...

下图就是我用了上面的方法产生的效果,还差点被微信部门的人请去喝茶。

这里用的是 Xposed 框架,原理就是 hook 了手机的计步传感器的队列函数,而后把步数的返回值每步乘1000返回,前提是,你的手机硬件自己有计步传感器功能,这里微信运动里面列出了支持的手机列表:https://kf.qq.com/touch/sappf...。好像最高就是98800了,多是微信作了步数限制吧

我这里用的是小米4联通版,发现虽然是1000基数的加,可是好像隔了好久才变化,估计又是 MIUI 作了一些省电策略,传感器的采集作了对齐吧?

Xposed 框架,不少玩机爱好者,会拿它修改一些主题,字体之类的,或者系统界面,定制本身想要的系统插件等等。然而,也有缺点,须要手机root,并且这个框架,还有可能让手机变砖,还有的系统可能对这个框架支持的很差,或者不支持。XDA 论坛里面也有不少大神把 Xposed 对某些机型作了适配,大神通常都是说,若是手机变砖他们不负责,哈哈。

正式因为这些框架的诸多不便,root 等等的问题,因而就有了一些非 root 的 hook 的黑科技,好比阿里巴巴的开源框架 Dexposed(https://github.com/alibaba/de...)其也是根据 Xposed 框架修改而来的,不过看 github 上他们也很久没更新了。

也有像其余我的写的和这种比较相似的框架,这里就不介绍了。可是这类框架的缺点就是,只能在该进程下hook,不能全局 hook,即只对这个进程的应用起做用,不能对另外一个应用起做用,优势是能够 hook 自定义函数也能hook 系统函数,而且不用 root 和重启。阿里用这个框架来打在线热补丁。

那对于 APP 内部签名校验的就不用再搜相应的代码了,直接 hook 就一步到位了,android.app.ApplicationPackageManager 这个类的 getPackageInfo 这个方法直接把正确的签名返回就行了,接下来咱们就须要把 hook 的代码注入到某个 APP 里就行了。

4. 关于如何注入?

开篇的时候有个图片就是我在腾讯地图里面注入了一个苍老师的图片其实就是,本身写了个 imageloader,用来下载网络图片,再写个 activity 或者 dialog 来承载这个 imageview,而后编译,再反编译,取出相应的smali等文件,好比贴到已经反编译好的腾讯地图的里面,把开启这个苍老师图片下载的启动代码放到合适位置,最后再把腾讯地图从新打包签名,就ok 了。

hook 代码也是同理注入,验证一下,成功(我这块写的比较粗略,代码比较多,只说思路了)那么这种代码注入和 hook 相结合的方式能干什么呢,咱们也不妨搞出点事情来。一样咱们仍是进行微信运动做弊的事情,其实不少运动类的软件均可以把本身的数据同步到微信运动里,好比小米手环,乐动力,悦动圈等等。

那咱们就先拿其中一个开刀吧:

通过一系列的跟踪代码定位,最终定位到了这个类 cn.ledongli.ldl.cppwrapper.DailyStats 里的 f 方法(f 是由于代码混淆了)而后咱们注入并 hook 方法,让它返回66666,ok,咱们看到了以下效果:

而后咱们在应用里面登录微信帐号,和对接到微信运动的功能,发现很差用,是由于,微信里面作了对应用的签名校验,应用的签名已经变了。因此咱们只能破解微信了(闷声做大死),一样注入 hook 代码,让微信获取应用的签名的时候取得正确签名,关键代码:

if( packageName.equals( "cn.ledongli.ldl")){
    if ( result instanceof PackageInfo) {
        PackageInfo info = (PackageInfo) result;
        info. signatures[0] = new Signature( myHexLedongli);
        param.setResult( info);
    }
}

再把这个盗版的微信从新打包签名,从新进行应用的同步数据操做,再进微信运动看看,是否是已经66666了。至此做弊完成。

♂♂♂♂♂♂♂♂♂♂我是画风不一样的分割线♂♂♂♂♂♂♂♂♂♂♂♂

说了这么多破解的,也该聊聊防破解的了

google 最先给的就是代码混淆的方案,其实通常的混淆只是下降了代码的可读性,让你对反编译出来的函数命名等不知道什么意思,不过解读出来只是时间问题。后来还有资源混淆的,可是意义不大。

后来有了核心代码用 C 实现,写成 SO,加花指令的办法,这个办法确实会阻止一大部分人的继续破解,可是对于常常作逆向的工程师来讲也不是什么难题。

其实作这么多大多数软件的初衷就是不想软件被盗版,而后被注入乱七八糟的广告,或者被盗取信息等,后来就有了盗版检测机制。好比:JAVA 层的签名校验,NDK 层校验,分段存放签名 Hash 串,服务器校验等等,可是这些方法我都在上面说了破解方法

如今国内的通常应用市场都有对 APP 签名的检测,在你下载的时候会告诉你这个 APP 是否是盗版的,从而让用户区分出来。可是应用市场本身自己又被盗版了怎么办呢?

再后来,就有了像360加固保和腾讯的乐固等产品,so 作了加密,真正的 dex 也藏起来了,不过我的以为,就算真正的 dex 也须要变成 odex 了,root 的手机取到 odex,再转回 dex,就能拿到真正的 dex(虽然我没试过,可是我以为多是一个思路),因此这个方法就更难破解了

虽然加固产品很厉害,可是也会有他的缺陷,Android 系统不断的更新升级,也许就换了某些模式等等,好比 ART 刚出来的时候,加固保加固后的 Apk,在 ART 模式运行下就会 Crash。这些加固产品要不断的适配各类型号的手机,CPU 类型,运行模式等等,因此不少 APP 为了考虑兼容性,他们也不会轻易去加固本身的产品。

♂♂♂♂♂♂♂♂♂♂我是画风不一样的分割线♂♂♂♂♂♂♂♂♂♂♂♂

关于逆向破解 Android 应用,我以为耐心很重要吧,代码跟来跟去确实很枯燥,总结几点小技巧吧

  1. 信息反馈:经过界面的一些弹出信息,界面特色寻找突破点

  2. 特征函数:好比搜 Toast,Log,getSignature 等

  3. 代码注入:把 toast 或者 log 函数注入到程序中,跟踪位置

  4. 打印堆栈:插入 new Exception("定位").printStackTrace();

  5. 网络抓包:经过抓包获得的关键字段,在代码中定位

写在后面:

这篇文章整理了有一段时间了,以为仍是应该写出来,也不是什么高深的技术文章,就是我的总结的一点心得而已。

关于破解应用不少人可能会去破解别人的应用注入广告来获取利益,也有可能盗取别人的信息。

不过咱们做为有节操的开发工程师,应该本着瑞雪的精神看待技术,学习技术,而不是乱♂搞。可是咱们也应该知道,咱们的应用有可能会被别人怎么搞……

最后推荐一本资料书,你们能够有空看看。

相关文章
相关标签/搜索