Android逆向之三代加壳技术分析

Android逆向破解技术交流qun:92347364

一、第一代壳:落地加载

1、原理

    a、原理很简单,就是首先将我们的dex文件或者apk文件解密,然后利用DexClassLoader加载器将其加载进内存中,然后利用反射加载待加固的apkappkication,然后运行待加固程序即可,我画了个流程图详细说明如下:

 b、上面说了大概原理,现在来说明一下具体细节,我们知道,在一个app开始运行的时候,第一个加载的类是ActivityThread,该类有个关键属性currentActivityThread,通过该属性能够获取到一系列其他关键的属性,例如mPackages,通过该属性,我们可以获取到mClassLoader属性,通过替换该属性我们可以替换系统加载器,如下所示:

接着来说怎么获取待加固apkapplication,这个通过在脱壳apkAndroidManifest.xml中使用meta-data来获取,如下所示:

  在然后就是怎么替换application,我们可以知道在android.app.LoadedApk类中有一个方法makeApplication可以生成一个application,通过该方法生成一个application,然后通过替换android.content.ContentProvider类中的mContext属性完成application的替换,如下图所示:

2、实际操作

    ps:因为第一代壳网上一大堆,所以讲得很粗略,同时这也不是本文的重点!!!

    通过上面的代码我们可以得到脱壳apk,然鹅待加固的apk放在哪里,网上大多放在脱壳dex的尾部,我又画了一张图,应该可以看图就懂了:

这个我采用通过python读取二进制然后重新计算chunksum和签名字段实现,代码入戏:

import binascii

import hashlib

import zlib


def fixCheckSum(shell):

    shell.seek(0x0c)

    data = shell.read()

    checksum =zlib.adler32(data)

    strchecksum =str(hex(checksum))

    strchecksum =strchecksum.replace('0x','')

    b =bytes(strchecksum.encode('utf-8'))

    a =bytearray(b)

    c = binascii.hexlify(binascii.unhexlify(bytes(a))[::-1])

    dataCheckSum =bytearray(c)

    shell.seek(0x08)

    shell.write(dataCheckSum)


def fixSHA1(shell):

    shell.seek(0x20)

    signBytes = shell.read()

    sha1 = hashlib.sha1()

    sha1.update(signBytes)

    sign = sha1.hexdigest()

    tmp =bytes(sign.encode('utf-8'))

    b =bytearray(tmp)

    shell.seek(0x0c)

    shell.write(b)


def fixFileSize(shell,num):

    b =bytearray()

    for i inrange(4):

        number =int(num % 256)

        b.append(number)

        num = num>>8

    shell.seek(0x20)

    shell.write(b)


def IntToHex(num):

    b =bytearray()

    for i inrange(4):

        number =int(num % 256)

        b.append(number)

        num = num >>8

    b.reverse()

    return b


def main():

    sourceApk =open('sourceApk.apk','rb+',True)

    unshell =open('unshell.dex','rb+',True)

    filename ='classes.dex'


    tmpApk =sourceApk.read()

    print('[*] 成功读取待加壳的APK文件')

    sourceArray =bytearray(tmpApk)

    tmpDex =unshell.read()

    print('[*] 成功读取脱壳DEX文件')

    unshellArray =bytearray(tmpDex)

    print('[-] 待加壳APK文件开始加密,加密类型为:未加密')


    sourceApkLen =len(sourceArray)

    unshellLen =len(unshellArray)

    print('[+] 加密后的APK大小为' + str(sourceApkLen) + 'Byte')

    totalLen =sourceApkLen + unshellLen +4


    tmpByteArray =unshellArray + sourceArray

    newdex =tmpByteArray +IntToHex(sourceApkLen)

    print('[+] 所有二进制数据合成完毕')


    shellTmp =open(filename,'wb+',True)

    shellTmp.write(newdex)

    shellTmp.close()

    print('[+] 数据写入' + filename + '完毕')


    shell =open(filename,'rb+',True)

    fixFileSize(shell,len(newdex))

    print('[+] 文件大小修改完毕')

    fixSHA1(shell)

    print('[+] 文件SHA-1签名头部修改完毕')

    fixCheckSum(shell)

    print('[+] 文件校验头头部修改完毕')

    print('[+] 待加壳APK文件sourceApk.apk加壳完毕,加壳后DEX文件' + filename + '生成完毕')

    shell.close()


if __name__ == '__main__':

    main()

    将上述apk重新签名后,安装运行,如下图所示:


3、遇到的问题

    运行时报错如下所示:


解决方案:报错显示无法实例化activity,经过检查是无法加载到正确格式的dex文件,检查你的解密代码,即使是你加密是象征型的异或了一个0xff,解密时也不能因为异或0xff值不变而不异或0xff。其次是打包成apk之前删除签名文件之后在签名!!!

二、第二代壳:不落地加载

1、原理

    大体原理和第一代壳相同,和第一代壳不同的是,第一代壳将dex文件解密出来会保存到文件中,在通过DexClassLoader加载进内存中,而不落地加载直接重写DexClassLoader使其可以直接加载字节数组,避免写入文件中。我们要做的是重写DexClassLoader,而这涉及到三个函数defineClassfindClassloadClass,在一个类被加载的时候,会先后调用这三个函数加载一个类,所以我们需要重写这三个函数,但是我们怎么在重写的过程中操控dex中的类(通过字节数组加载进来的并不能直接操控)?其实系统的DexClassLoader加载dex进入内存的也必然是通过字节加载的,而在系统so中的libdvm.so中的openDexFile可以直接加载dex文件,那么现在清楚了,我们可以通过编写so文件调用openDexFile函数加载dex字节数组,值得注意的是,openDexFile函数返回值为一个int类型的cookie,可以简单理解成一个dex文件的'身份码',通过该'身份码'即可操控这个dex文件,至于怎么调用该函数,可以通过dlopendlsym函数调用,相关代码如下所示:

2、实际操作

    a、首先编写样本,这里我写了一个类和一个方法,作用就是打印一个特征字符串,如下所示:

b、将上面的样本打包成apk后提取出dex文件然后放置到assest文件夹下(该文件夹需要自己建立)供程序调用(ps:我这里图方便未对dex文件加密然后解密,有需要的可以加上),然后脱壳apk和上面的第一代壳没什么区别,唯一不同的是就是我们使用的是我们自己重写的DexClassLoader,如下图所示:

c、运行截图如下:

3、遇到的问题

  a、报错java.lang.UnsatisfiedLinkError:

Native method not found

    解决方案:在配置文件中添加packagingOptions{

pickFirst "lib/armeabi-v7a/libtwoshell.so" pickFirst

"lib/arm64-v8a/libtwoshell.so" pickFirst

"lib/x86/libtwoshell.so" pickFirst

"lib/x86_64/libtwoshell.so" },如下所示:

 b、运行到加载dex文件中的方法时,app直接闪退

    解决方案:重写的loadClass方法有问题,不能通过直接super调用父类方法,而是应该通过反射调用defineClassNative方法,如下所示:

三、第三代壳:类指令抽取壳

1、原理

    a、什么是类指令抽取壳,从名字就能看出来,就是把dex文件中的方法指令抽空,变成nop,然后在运行时再将指令还原!!!

    b、指令抽取可以通过010修改,现在来说指令还原,其余代码和第二代基本一样,不一样的地方在加载完dex之后执行指令还原函数,指令还原现在有两种方法,第一种是通过读取maps文件获取加载的dex文件地址,然后对dex文件进行解析,找到被nop的指令处进行还原(ps:该种方法需要及其熟悉dex文件格式,不了解的可以看我之前的文章关于解析dex文件,因为我之前解析的时候用的是python,改成c要大量时间,所以我选择了第二种方法);第二种方法就是通过免root hook系统函数(最简单的就是deFindClass函数)然后进行指令还原!!!

    c、接下来就将一下怎么通过hook dexFindClass函数来进行指令还原(PS:看懂下面的内容需要理解dex文件格式)dexFindClass函数在libdvm.so库中,如下所示:

  root hook框架有点多,我选择的是android inline hook,原因很简单,很适合在so层使用,其他的经过我测试不知道为啥我写出来的没反应,该框架github地址:https://github.com/ele7enxxh/Android-Inline-Hook,用法可以参考作者github,该inline hook框架需要原函数地址、新函数地址和原始函数的二级指针,用法如下所示(怎么使用不是重点,接下来的才是重点,所以这里比较粗略)

我们要hook的是dexFindClass函数,该函数定义在DexFile.h文件中,该函数返回值为一个类结构指针,第二个参数为类名字,通过该参数我们就可以指定类进行指令还原,如下所示:


上面我们得到的classDataOff,我们可以通过该地址获取到类数据,该偏移地址指向的是一个DexClassData结构,该结构的header存储了相关类信息,该结构的directMethods指针指向的方法的结构题,如下所示:

通过directMethods指针我们可以顺着找到DexMethod结构体,通过该结构体的methodIdx调用系统函数dexGetMethodIddexStringById可以获取到方法名字,精确还原方法指令,通过该结构的codOff(这是个偏移地址)可获取方法指令,该偏移地址指向DexCode结构,该结构即存储了方法指令,利用memcpy替换即可达到指令还原的效果,如下所示:


2、实践操作

    java层基本和第二代壳一样,只是多了一个调用hook的函数,so层关键代码如下所示:(ps:不知道为啥Android inline hook稳定性很差,上一个测试app还得行,下一个就疯狂报错了,所以代码是基本完成了,但是android inline hook报错未解决,有时间我会修改)


3、遇到的问题

    报错未定义函数,如下所示:

解决方案:在CmakeLists.txt文件中将jni文件夹下面所有引用到的文件都包含进去,如下所示:

四、相关链接

    

    源码github链接:https://github.com/windy-purple/androidshell

  参考链接:

Android免Root权限通过Hook系统函数修改程序运行时内存指令逻辑

Android逆向之旅—运行时修改内存中的Dalvik指令来改变代码逻辑

Android中免root的hook框架Legend原理解析

https://github.com/asLody/legend

https://github.com/ele7enxxh/Android-Inline-Hook

Android APK加固-完善内存dex

利用动态加载技术加固APK原理解析

Android插件化框架之动态加载Activity(一)

Android APK 加固之动态加载dex(一)

Android中实现「类方法指令抽取方式」加固方案原理解析

Android中apk加固完善篇之内存加载dex方案实现原理(不落地方式加载)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351