Apk逆向_笔记

破解实际上是挺快乐的事情,但是在破解的路上,软件开发人(团队)总会想方设法不让你逆向。于是有了各种加固方法。现在在学习加固破解路上的一点点记录。


 1

 这里看到,360加固为了防止apktool反编译功能,添加了一个qihoo属性,这个属性apktool不认识就报错了

我们有了apktool源码,可以直接进行修复的,然后进行反编译

反编译成功了,查看他的AndroidManifest.xml文件内容:

在回编译的时候也是报错了,说找不到这个属性,为了方便这里直接把android:qihoo给干掉,因为其实他没有任何作用的,就是为了干扰反编译工作的,所以直接去掉即可。


1.正常的把apk拖进Androidkiller中分析时,报错(Exception in thread "main" brut.androlib.err.UndefinedResObject: resource spec: 0x01010490),这个问题是一些App使用或被迫使用(碰到版本大更新为了兼容新版本Android SDK不得不使用)了高版本的AndroidSDK的系统资源,而ApkTool本身的资源列表一般是比较老的,所以反编译时如果碰到相应资源找不到就会出现异常.而解决办法就是使ApkTool的资源列表更新到比较新的状态.下面说步骤.
    2.①:导出手机中的framework-res.apk(path: /system/framework)我为了使这个apk的资源比较新用Eclipse新建了Android6.0的模拟器,特意导出了这apk.
      ②将这个apk放到ApkTook目录下 执行 java -jar apktool.jar if framework-res.apk 这时资源文件就会更新到本地的1.apk中 (path: C:\Users\Administrator\apktool\framework\).熟悉ApkTool的都应该知道这个1.apk里面其实就是反编译时用到的系统资源文件.
      ③这时就可以正常反编译了,为了验证我的说法可以用aapt在 android.jar(path:$(Android_SDK)下的platforms中)查找一下资源.如:我反编译这个app时出现 0x01010490 执行如下代码: aapt.exe d resources android.jar | find "0x01010490" 时会出现如图1所示的结果.


3

同样是AndroidKiller报错(Exception in thread "main" brut.androlib.err.UndefinedResObject: resource spec: 0x7f03006e),经过上面的步骤操作后仍然是报错.解决步骤:
     ①:aapt.exe l -a 123.apk > log查看资源表如图 

的确没找到0x7f03006e 估计是伪加密,
        ②:而且经过 apktool 跟shakaapktool的测试都是同样报的找不到 0x7f03006e,由此推断这个0x7f03006e应该是写死了,所以将apk拖进UE(16进制编辑器类都行)搜索:6e00037f 至于为什么倒过来不解释,找到之后修改成已知资源即可(我修改成了 0e00037f,因为跟6e有点像 :) ,当然也可以修改成0f00037f,不知道 程序会不会报错,但是反编译会通过).祝折腾成功!
       5.补充一点现在的Apktool反编译加参数可以设置不反编译资源


4 打开系统的调试总开关 

如何在不需要反编译的情况下,添加android:debuggable属性,就可以进行调试。

这个现在已经有很多工具可以做了,先来说说具体的原理吧:

其实Android中有一些常用的配置信息都是存放在一个文件中,比如设备的系统,版本号,cpu型号等信息,而这个文件位置在:

/system/build.prop

我们查看文件的内容,可以看到很多设备的信息,而且这些ro开头的表示这些属性值是只读的,不能进行修改的。

同时Android中提供了两个命令来操作这些信息:getprop和setprop命令:

查看系统的sdk版本号

设置系统的sdk版本号为22,可是这里并没有修改成功,原因就是因为ro开头的属性是不允许后期修改的,改也是可以修改的,需要重新编译系统镜像文件boot.img,但是这里并不是本人介绍的重点了。

既然Android中的一些系统属性值存放在一个文件中的,而且这些值是只读的,当然不仅可以通过getprop命令读取,有一个api也是可以直接读取的,就是:System.getProperty("ro.build.version.sdk");其实这个方法是native层实现的,具体就不分析了。

那么这个文件是存储这些属性值的,那么是谁来进行解析加载到内存中,能够给每个app都能访问到呢?

这个工作就是init.rc进程操作的,我们应该了解了系统启动的时候第一步就是解析init.rc文件,这个文件是在系统的根目录下,这里会做很多初始化操作,这里不详细分析了,后面再分析Android中系统启动流程的时候在详细分析。这里同时会做属性文件的解析工作,所以,Android 属性系统通过系统服务提供系统配置和状态的管理。为了让运行中的所有进程共享系统运行时所需要的各种设置值,系统会开辟一个属性存储区域,并提供访问该内存区域的 API。所有进程都可以访问属性值,但是只有 init 进程可以修改属性值,其他进程若想修改属性值,需要向 init 进程发出请求,最终由 init 进程负责修改属性值。

那么上面说到的是system/build.prop文件。里面主要是系统的配置信息,其实还有一个重要文件在根目录下面:default.prop:

这里有一个重要属性:ro.debuggable,对这里就是关系到系统中每个应用是否能够被调试的关键。其实在Android系统中一个应用能否被调试是这么判断的:

当Dalvik虚拟机从android应用框架中启动时,系统属性ro.debuggable为1,如果该值被置1,系统中所有的程序都是可以调试的。如果系统中的 ro.debuggable 为0,则会判断程序的AndroidManifest.xml中application标签中的 android:debuggable元素是否为true,如果为true则开启调试支持。

好了到这里,我们可以总结一下了:

Android系统中有一个可以调试所有设备中的应用的开关,在根目录中的default.prop文件中的ro.debuggable属性值,如果把这个值设置成1的话,那么设备中所有应用都可以被调试,即使在AndroidManifest.xml中没有android:debuggable=true,还是可以调试的。而这些系统属性的文件system/build.prop和default.prop,都是init进程来进行解析的,系统启动的时候就会去解析init.rc文件,这个文件中有配置关于系统属性的解析工作信息。然后会把这些系统属性信息解析到内存中,提供给所有app进行访问,这块信息也是内存共享的。但是这些ro开头的属性信息只能init进程进行修改。下面来分析一下修改这个属性值的三种方式:

第一种:直接修改default.prop文件中的值,然后重启设备

那么现在如果按照上面的目的:就是不需要反编译apk,添加android:debuggable属性的话,直接修改default.prop文件,把ro.debuggable属性改成1即可,但是通过上面的分析,修改完成之后肯定需要重启设备的,因为需要让init进程重新解析属性文件,把属性信息加载内存中方可起作用的。但是并没有那么顺利,在实践的过程中,修改了这个属性,结果出现的结果就是设备死机了,其实想想也是正常的,如果属性能够通过这些文件来修改的话,那就感觉系统会出现各种问题了,感觉系统是不会让修改这些文件的内容的。

第二种:改写系统文件,重新编译系统镜像文件,然后刷入到设备中

那么上面修改default.prop文件,结果导致死机,最终也是没有修改成功,我们还有什么办法呢?其实上面已经提到过一次了,就是这些属性文件其实是在系统镜像文件boot.img在系统启动的时候,释放到具体目录中的,也就是说如果我们能够直接修改boot.img中的这个属性即可,那么这个操作是可以进行的,但是困难那是不一般的顺利,至少我没成功过,修改系统文件,然后重新编译镜像文件,最后在刷到设备中。这个过程我尝试过是失败了,不过理论上是可以的。而且这种方式如果成功了,那么这个设备就是永远可以进行各种应用的调试了。

第三种:注入init进程,修改内存中的属性值

那么上面直接重新编译boot.img,然后在刷到设备中的工作是失败的,那么还有其他方法吗?肯定是有的,我们其实在上面分析了,init进程会解析这个属性文件,然后把这些属性信息解析到内存中,给所有app进行访问使用,所以在init进程的内存块中是存在这些属性值的,那么这时候就好办了,有一个技术可以做到了,就是进程注入技术,我们可以使用ptrace注入到init进程,然后修改内存中的这些属性值,只要init进程不重启的话,那么这些属性值就会起效。好了,这个方法可以尝试,但是这个方法有一个弊端,就是如果init进程挂了重启的话,那么设置就没有任何效果了,必须重新操作了,所以有效期不是很长,但是一般情况下只要保证设备不重启的话,init进程会一直存在的,而且如果发生了init进程挂掉的情况,那么设备肯定会重启的。到时候在重新操作一下即可。

好了上面分析了三种方式去设置系统中的调试属性总开关,那么最后一种方式是最靠谱的。

而且思路也很简单,但是我们不会重新去写这个代码逻辑的,因为已经有大神做了这件事,具体工具后面会给出下载地址:

这个工具用法很简单,首先把可执行文件mprop拷贝到设备中的目录下,然后运行命令:

./mprop ro.debuggable 1

这个工具可以修改内存中所有的属性值,包括机型信息。

这里修改完成之后,使用getprop命令在查看值,发现修改成功了,但是需要注意的是,我们修改的是内存的值,而不是文件中的值。所以default.prop文件中的内容是没有发生变化的。

这时候,我们可以使用Eclipse的DDMS来查看可以调试的应用列表:

当然也可以使用adb jdwp命令来查看可以调试的进程id:

但是可惜的是,发现还是没有展示设备中所有的应用,其实这里是有一个细节问题了,因为我们虽然修改了内存值,但是有一个进程我们需要重启一下,哪个进程呢?那就是adbd这个进程,这个进程是adb的守护进程,就是设备连接信息传输后台进程,所以想看到可以调试的进程信息的话,那么需要重启这个进程,这样连接信息才会更新。

重启这个进程很简单:直接使用stop;start命令即可

其实这是两个命令,用分号隔开,首先是干掉进程,然后在重启。

运行完命令之后,再去看DDMS窗口信息:

这时候所有的应用进程都是可以调试的了,这时候我们在使用dumpsys package命令查看一个应用的包信息:

这里可以看到,这个应用的flags标志中并没有debuggable属性值,但是这个应用是可以调试的。所以看到ro.debuggable这个是总开关,只要他为1,开启的话,即使没有android:debuggable也是可以的了。

好了到这里,我们来总结一下:

1、我们的目的是怎么在不需要反编译apk包,添加android:debuggable属性,就可以进行apk的调试?

2、我们通过分析系统属性文件和系统启动流程以及解析系统属性文件的流程,知道了设备中关于调试有一个总开关属性值:ro.debuggable,默认是0,不开启的。那么这时候我们就可以猜想有这几种方式可以去修改。

3、分析了三种方式去修改这个属性值:

第一种方式:直接修改default.prop文件中的这个字段值,但是可惜的是修改失败,在修改的过程中出现死机,重启设备之后,属性值还是0。

第二种方式:修改系统源码的编译脚本,直接修改属性值,然后重新编译镜像文件boot.img,然后刷入到设备中,但是在实践的过程中并没有成功,所以放弃了,而且这种方式有一个好处就是一旦修改了,只要不在重新刷系统,那么这个字段将永远有效。

第三种方式:注入到init进程,修改内存中的这些系统属性值,这种方式实现是最简单的,但是有一个问题,就是一旦设备重启,init进程重新解析default.prop文件的话,那么ro.debuggable值将又重新被清空,需要再次注入修改。

4、最后采用了第三种方式,不过网上已经有人写了这样的工具,用法也很简单:./mprop ro.debuggable 1;但是修改完成之后,一定要记得重新启动adbd进程,这样才能够获取到可以调试应用信息。

5、使用工具修改完成之后,在Eclipse中的DDMS窗口发现,设备中的所有应用都处于可以调试状态了。也就是说我们的操作成功了。

那么上面的这个过程成功之后的意义还是很大的:标志着我们以后如果是单纯的想让一个apk能够被调试,去反编译在添加属性值的话,其实这种方式很高效的。可以让任意一个apk出于被调试状态。


5 IDA动调so,dump出连续的dex

讲完了上面的一个重点之后,下面我们就开始来讲解本文的另外一个重点,开始脱壳了。

第一步:开启android_server

第二步:端口转发

第三步:启动应用

adb shell am start -D -n com.CMapp/com.e4a.runtime.android.mainActivity

第四步:开启IDA,附加进程

第五步:设置Debugger Option选项

第六步:运行jdb调试等待

jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=10265

注意:这里需要注意了,因为我们改了系统的ro.debuggable属性,设备中所有的应用都处于可调式状态,基本端口8700已经被占用了,那么这时候需要使用被调试程序的独有端口了,可以在DDMS窗口进行查看。

第七步:关键函数下断点

首先找到mmap函数的内存地址,这里可以直接使用G键,通过函数名来跳转:

注意:这里和之前的脱爱加密的壳方法可能不一样了,还记得之前脱爱加密的壳的时候,给fopen和fgets函数下断点,因为如果有反调试的话,肯定是读取/proc/pid/status文件中的TracerPid字段值的,然后修改TracerPid值为0即可,但是这个方法对360加固的不好使了,因为360加固的反调试是通过mmap函数来读取/proc/pid/status,所以这里需要给mmap函数下断点了,而且后面还会看到给dvmDexFileOpenPartial这个函数下断点也不好使了,原因是360加固自己在底层实现了解析dex的函数来替代了这个dvmDexFileOpenPartial函数。但是不管是他自己实现dex解析加载,最终都是需要把dex文件加载到内存中,还是得用mmap函数来进行操作。所以在脱360加固的壳的时候mmap函数是重点。

好了给mmap函数下了断点,下面就F9运行程序吧:

进入到了mmap的断点处,这里因为mmap函数代码比较长,为了节省时间,我们可以在mmap函数的结束处下一个断点,然后直接F9运行到函数的结尾处,因为系统中有很多个so需要加载到内存中,所以mmap函数会执行多次,但是其实我们最关心的是加载我们自己的so文件,即libjiagu.so文件,因为这个才是我们的native层代码,所以等出现如下界面:

这时候,说明这个so文件被加载到内存中了,也就是程序的native层代码开始执行了,注意不能在F9了,而是使用F8单步调试:

F8单步运行到这里的时候,遇到一个问题,就是F8了很多次,始终在这个地方执行,后来分析了arm指令之后,发现原来这里是一个循环,初始值是0,存储在R11中,然后逐步加1,和R3中存储的阈值作比较,通过查看寄存器的值,发现R3寄存器中是A7,所以这里得去修改寄存器R11的值了,不然我们得单步A7次,这里直接把R11值修改为A6:

修改寄存器也是很容易的,直接右击寄存器:

点击Modify value:

点击OK,之后再来看看R11的寄存器的值:

修改成功了,这时候在单步F8,两次之后就执行完了循环了,从这里也可以看到,这个地方也算是为了防止被调试,加大调试成本的一种方式。继续往下走:

到这里,执行完BL之后就退出调试界面了,尝试多次都一样,所以猜想反调试肯定在这里,可以F7跟进去看看:

到BLX这里,每次之前完也是退出调试界面,所以这里还得F7单步进入看看:

这里看到了一行重要的arm指令:CMP比较指令,而且是和0比较,很可能这里就是比较TracerPid的值是否为0,如果不为0就退出,可以查看R0寄存器的内容:

然后在查看被调试进程的TracerPid的值:

果然R0存储的是TracerPid的值,为了验证正确性,这里继续:

果然,运行到了自杀的地方,一直单步运行:

退出程序了。

那么上面就知道了反调试的地方,就好办了,直接修改寄存器R0的值为0即可:

然后继续单步F8运行,后面还有一个CMP和0进行比较的地方,我们一样进行置零操作,再次单步F8,当运行到此处的时候:

看到memcpy函数的时候,这时候可以直接运行F9,又会执行到mmap那里,然后依次F9,还是运行到了上面的那个循环,这样依次类推,在这个过程中我运行了7次循环,改了R0值改了9次,所以这个地方会执行多次是正常的,但是这里在我多次调试之后总了一个好的方法,就是看到多次执行的路线都差不多:

mmap函数=》循环=》(MOV R0,R8)BL=》(MOV LR,R4)BLX=》CMP R0,#0=》mmap....

这个过程中,其实为了简便我们可以

1》在mmap函数的开始处,结束处下一个断点,这两个断点是为了后面加载内存的dex文件做准备

2》在循环处下一个断点,这个断点是为了修改循环值,节省时间

3》在BL处下个断点,是为了进入BLX

4》在BLX处下个断点,是为了进入比较TracerPid处

5》在CMP下断点,是为了修改TracerPid的值

同时在这个过程中,需要使用F9,直接跳转到下一个断点,高效,只有在到达了CMP处的时候,要用F8单步调试,而且这个地方一定要小心,不能按错了,不然又得从头再来,我吃了很多次亏,也重来了很多次。只要当看到了memcpy函数的时候,再次F9到下一个断点处。更需要注意的是:每次到达mmap断点处的时候,一定要看当前栈信息的视图窗口,看看是否出现了classes.dex的字样,因为最终都是使用mmap来把解密之后的dex加载到内存中的,所以这里一定要注意,是本次调试的核心。

当然这个只是个人的调试思路,每个人都有自己的思路,只要能成功都可以。

就这样来回搞了几次之后,终于看到了曙光:

当再次来到了mmap函数处的时候,终于看到了classes.dex字样了,说明这里开始解密dex然后进行加载到内存了,这时候不能在F9跳转了,而是F8单步运行,然后查看R0寄存器的值:

每次都是执行完__mmap2这个函数之后,R0就有值了,每次看到R0中有值的时候,可以到Hex View窗口中使用G键开始地址跳转,查看是否为dex内容:

如果发现不是,就还是单步F8,知道mmap函数结束,然后再次F9,到达mmap函数开始处,时刻看紧Hex View,栈窗口,R0寄存器这三个地方的值:

在多次尝试之后,终于成功了,这里看到了熟悉的dex文件的头信息,关于dex文件的头部信息可以看这篇文章:Dex文件格式解析

所以这里在头部信息的第33个字节然后连续4个字节就是dex的长度了,那么现在有了dex在内存中的其实位置,长度大小,下面就可以使用Shirt+F2打开脚本执行窗口,dump出内存中的dex数据:

static main(void)
{
  auto fp, begin, end, dexbyte;
  fp = fopen("E:\\dump.dex", "wb");
  begin = 0x755A9000;
  //偏移0x20处,取4字节为dex文件大小
  end = 0x755A9000 + 0x0004BC38;  
  for ( dexbyte = begin; dexbyte < end; dexbyte ++ )
      fputc(Byte(dexbyte), fp);
}

保存到E:\dump.dex,然后在使用Jadx工具进行查看:

这里可以查看到源码了,而且类名,方法名,变量名都是用中文来命名的,感觉好不习惯,但是Java中是支持这么干的,因为Java采用的是Unicode编码的。

案例下载:http://download.csdn.net/detail/jiangwei0910410003/9561416

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值