一、前言
现在主流的加固平台有:梆梆加固,爱加密,360加固,腾讯加固,在之前的一篇文章中介绍了:如何脱掉“爱加密”的壳,现在这里要脱掉另外一个平台的壳:360加固,因为有了之前的脱壳经验,很多基础知识和准备工作这里就不详细介绍了,为了能够脱掉他家的壳,用一个案例来去360平台进行加固,然后进行脱壳。下面就来开始脱壳:
二、分析360加固的原理
首先拿到加固之后的apk,这里为了方便查看内部信息,先不用dex2jar+jd-gui工具进行分析了,直接使用我们之前分析了源码的一个工具:Jadx,直接查看:
其实现在的加固的常规套路都差不多,这里看到和之前分析的爱加密加固的形式几乎一样,这里的壳Application是StubApplication在attachBaseContext中做一些初始化操作,一般是将assets目录中的so文件拷贝到程序的沙盒目录下:/data/data/xxx/files/..;然后再用System.load进行加载,通过查看可以得知源程序apk已经被加密了,就是存放在这里的so中,之前的文章也是分析了,一般源程序加密之后就存放在那几个目录下,一般是:dex文件尾部,libs目录,assets目录。
下面再来看一下他的AndroidManifest.xml文件:
找到了他的入口Activity了,但是这里没有android:debuggable="true",所以程序是不能被调试的,所以我们需要添加这个属性,然后在进行回编译进行调试,这时候就需要使用到apktool工具了:
好了,这里看到,360加固为了防止apktool反编译功能,添加了一个qihoo属性,这个属性apktool不认识就报错了,但是我们之前的一篇文章已经介绍了:Apktool工具错误修复,我们有了apktool源码,可以直接进行修复的,然后进行反编译:
反编译成功了,查看他的AndroidManifest.xml文件内容:
的确,是有一个属性qihoo,这个就是Android系统在解析apk文件的时候,发现不存在的属性直接略过,但是apktool工具却不会,360加固就是利用这个漏洞来增加反编译难度的,但是我们之前的一篇文章中介绍了如何修复,这里修复很简单了。所以说只要有了apktool源码,什么都好做了。
然后我们在添加android:debuggable属性:
然后回编译:
这时候看到,在回编译的时候也是报错了,说找不到这个属性,为了方便这里直接把android:qihoo给干掉,因为其实他没有任何作用的,就是为了干扰反编译工作的,所以直接去掉即可,然后在回编译:
好了,回编译成功,然后在进行签名打包即可。这里就不在介绍了。
那么从上面我们可以看到,其实360加固为了防止反编译,就利用了Android系统本身在解析apk的时候,遇到不认识的属性直接略过,而apktool工具却不会的漏洞来给AndroidManifest.xml中添加一个混淆反编译的属性:qihoo,幸好我们有源码,可以修复这个问题,在进行反编译即可,这里也希望apktool官网能够及时修复这个漏洞。为了回编译成功,我们可以直接把这个属性删除。不然回编译也是会报错的。这个属性只是360为了混淆反编译工作,所以删除对程序逻辑没有任何影响的。
三、打开系统的调试总开关
这里就要开始介绍本文的第一个重点了:如何在不需要反编译的情况下,添加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出于被调试状态。
四、开始脱壳
讲完了上面的一个重点之后,下面我们就开始来讲解本文的另外一个重点,开始脱壳了。
第一步:开启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
五、脱壳总结
好了到这里,我们就成功了脱掉了360加固的壳了,下面来总结一下他的壳的特点和调试需要注意的点:
1、首先360加固依然是外部套一个Application壳:StubApplication,源程序加密存放在libjiagu.so,放在了assets目录下,在Application启动的时候,释放到应用的沙盒目录files下面,然后在使用System.load方法进行加载,这个和爱加密的方式是一样的
2、关于360加固的反调试,依然使用的是读取/proc/[pid]/status中的TracerPid字段值,判断是否为0,但是这里和爱加密不一样的是,在读取这个文件的时候不是用的fopen系统函数,而是mmap系统函数,所以在解决反调试的时候需要给这个函数下断点。
3、360加固底层不是采用dvmDexFileOpenPartial这个系统函数来解析dex然后加载到内存中的,而是自己实现了一个函数,所以给这个函数下断点,然后获取参数值来dump内存中的dex数据是行不通的,但是有一个思路就是不管他用哪个函数去解析dex加载到内存,最终都得使用mmap这个系统函数来操作,所以还得给这个函数下断点,所以这里在调试的时候需要时刻注意的是当断点到达了mmap函数处的时候,需要观察Stack View栈窗口中是否出现了classes.dex字样,如果出现了,说明开始解密dex文件,准备加载到内存中了,那么这时候需要观察R0寄存器的值,然后在Hex View中跳转到指定内存地址,可以观察到是否为dex内存数据
4、在观察是否为内存数据的时候,需要注意dex文件是有自己的文件格式的,那么头信息就是个根据,所以我们可以查看开头为:dex.35 这样的内容来判断此处为dex数据,因为dex头部信息中也有dex的文件大小,那么这时候就可以使用脚本dump处内存中的dex数据了。
5、在调试的过程中,会发现很多断点多次执行,特别是有一个循环,需要我们修改寄存器的值来快速结束循环,而且在关键处下断点,也是加快调试效率的。
六、技术概要
1、本文开始的时候介绍了通过注入系统init进程,修改内存中的系统属性值:ro.debuggable,让设备中所有的应用都可以被调试,这个功能将对后续逆向破解有重大意义,也会省去了反编译的工作。所以这个方式还是很具有里程碑意义的。
2、在脱爱加密的壳的时候,学习到了给fopen和fgets这两个系统函数下断点来解决反调试,在这里我们又多了一个下断点的好去处就是给mmap下断点,当发现给fopen函数下断点不好使的时候,在尝试给mmap下个断点吧。
3、在脱爱加密的壳的时候,给dvmDexFileOpenPartial函数下断点,来获取dex在内存的起始地址和大小,从而dump处内存中的dex数据,但是360加固并没有走这个函数,因为在给这个函数下断点的时候,他压根没走到,所以断定它内部使用了其他的函数去解析dex的,然后加载到内存中的,但是如果最后加载到内存中,那肯定要用到mmap函数,所以只要给mmap函数下断点即可。
七、总结
本篇文章就介绍了如何脱掉360平台加固的apk应用的壳,在结合之前的一篇脱掉爱加密家的壳的知识,看到现在在脱壳的时候其实就两点,一点是找到关键处解决反调试,一般都是fopen,fgets,mmap,open等系统函数下断点,还有一点就是如何找到内存中dex的起始地址和dex的大小,这个一般现在就是dvmDexFileOpenPartial函数下断点,还有就是给mmap函数下断点。
更多内容:点击这里
关注微信公众号,最新Android技术实时推送