Google DroidGuard虚拟机分析

一、背景介绍

1. Google DroidGuard虚拟机的介绍

DroidGuard是由Google 开发的用于验证设备可信度的 GMS 组件,App使用它来防止在不符合安全要求的设备上运行,谷歌也使用它来防止机器人、欺诈和滥用。该模块实现了一个自定义虚拟机,该虚拟机运行由 Google 提供的专有字节码来执行设备完整性检查。广泛参与检测 Android 平台的滥用(机器人、垃圾邮件、root状态、广告欺诈等)。

2. 在GMS上的应用

以64位so的形式存在于GMS组件中,沟通Java层与so层的方法为通过native方法initNative初始化虚拟机,通过xssNative方法执行虚拟机字节码采集设备、app等信息生成protobuf数据并进行加密返回。

3. 破解方式

分析虚拟机是比较消耗时间的,所以一开始打算采用黑盒调用的方式进行绕过,但是这个虚拟机并不只是单纯的加密数据的功能,采集数据进行拼装也是虚拟机内部在做的事情,所以黑盒调用无法准确控制加密算法的输入,必须正面分析虚拟机破解加密算法。

二、虚拟机结构

1. 虚拟机如何嵌入

Java层从xssNative方法映射到so层方法sub_19BAC->sub_5B7FC->sub_1E1E4->sub_1E430->sub_1F9CC

sub_1F9CC方法控制流程图如下,是实现虚拟机的核心方法

2. 绕过so的反调试

虚拟机采用的反调试方案为接收signal信号的方式,所以绕过需要hook sigaction方法,当signum为5时,直接return 0。

3. 虚拟寄存器

3.1 虚拟寄存器的初始化

在调试过程中发现一段数据频繁参与运算,就是a1+5928这个地址指向的内存块,这个内存块是在哪里赋值的呢?在IDA搜索0x1728相关的指令,一个个看,或者根据日志里那个malloc 4112来定位这个内存块数据生成处。发现sub_59AE8方法可能性较大,断点调试一波发现就是这个方法生成的数据。sub_57928里先赋0,再调用sub_59AE8进行赋值

  

3.2 分析虚拟寄存器结构

该虚拟机一共有256个虚拟寄存器,将数据存入虚拟寄存器后就一直保持加密状态,通过指令可直接对两个加密的虚拟寄存器进行运算,避免了运算中明文数据的泄露。虚拟机函数sub_1B304,对数据进行加密存放,参数1为32字节加密信息结构,第一个8字节存放算法固定值,第二个8字节为寄存器编号同时决定算法位移值,第三个8字节存放寄存器目标内存地址,第四个8字节为JNIENV。参数2为数据类型。参数3为待加密数据。

3.3 虚拟寄存器加解密

sub_59AE8方法刚调用完毕,初始化完虚拟寄存器

sub_59AE8第二次调用在sub_19358 里,重新分配了一个内存块作为5928的值

老的5928值内存块

新的5928值内存块

到sub_19358返回时,内存块需要的数据已出现,所以赋值发生在sub_19358

继续跟到sub_1E1E4方法里

继续跟进到sub_1E430,当在地址0x1E7EC调用完sub_1F9CC方法后,需要的数据仍未出现,然后这个sub_1F9CC会多次调用,约41次调用后出现数据,有硬件断点的话,这里可直接定位。手工追踪太耗时了,进入虚拟机后只能用watch脚本检测地址数据改变了

在IDA watch脚本单步走了千条指令后,跟到了虚拟机里改变内存数据的指令

是个跳转到方法sub_1B304的指令

虚拟寄存器加解密算法位于虚拟机函数sub_1B304中,已还原成python代码

def rw_register(register_num, data):
    v4 = register_num
    v6_13 = data

    v10 = 0x9ab484eb8c37f9a3  # so里固定的
    v11 = 0x6b9136c76d59d9fd  # so里固定的
    v11 = (((((v11 | 1) ^ 0x3F) + (v11 & 0x3E)) & 0xFE | v11 & 1) ^ 1) & ((v11 & 0x10 & v4 | 1)  + 2 * (v11 & 0x10 ^ v4)  + (v11 & 0x10 ^ v4 ^ 0x3F))
    print(v11)
    my_v11 = v11
    if v11 >= 64:
        my_v11 = v11%64
    tmp1 = (v10 << my_v11) & 0xFFFFFFFFFFFFFFFF
    v12 = (((tmp1 | ARM64_NOT(v10 >> (64-my_v11))) + ARM64_NOT(tmp1))& 0xFFFFFFFFFFFFFFFF) - ((ARM64_NOT(ror64Bit(v10, (256-v11)))<<1) & 0xFFFFFFFFFFFFFFFF)
    rst = (v6_13 + ARM64_NOT(v12 & v6_13) - (v6_13 | ARM64_NOT(v12)) )& 0xFFFFFFFFFFFFFFFF
    return rst

算法用到的常数在so中如下

4. 内存块加密

为了避免明文数据在内存中被找到,虚拟机还对内存块进行了加密,通过hook memcpy函数即可清晰的看到数据在内存中的加密变化,下图展示了明文数据在内存中被加密的形态

三、加密算法分析

1. 算法分析过程记录

以包名字段的加密过程为例,追踪分析protobuf字段加密算法

从地址0xD678入手

向上追踪到地址0x51c98

一路f8,跟到赋值处0x51D5C

X24 X27值为0x2f

x27当前字段在protobuf二进制数据块的offset

x24当前字段的当前字节在protobuf二进制数据块的offset

X20为加密表的地址,一个加密表8字节,用完了生成下个加密表

CMP             W12, W7

W12 值为5,为当前使用的加密表,前面用过的还有0,1,2,3,4号加密表

W7 值为6,为下个字节要使用的加密表

当不相等时,进入生成下个加密表的代码块;

相等时,直接进入下个字节byte_enc环节。

6号加密表

7号加密表

加密算法最后递归只跟v212 v130 v133有关

V212地址

v130 第一轮为0

v133 0x0B45F42F3BBA5C7D 为前面字段的密文

X30每次加一,到0退出,所以加密表有0x20轮

生成加密表算法分析完,就去找初始密文,这个调试就能找到。也可以结合log猜测是密文开头的8个字节,验证发现确实是这样。

2. 算法还原

加密表生成算法

def gen_encTable(cipher, cipher_num_p2):
    v130 = 0
    const_list = gen_const_list(cipher_num_p2)
    v133 = i64_to_i32_LO(cipher)
    v134 = i64_to_i32_HI(cipher)

    for i in range(32):
        v135 = const_list[(v130 & 3)]
        v136 = v135 | v130
        v137 = v130 ^ ARM32_NOT(v135)
        v130 = i64_to_i32_LO(
            0xFFFFFFFD - i64_to_i32_LO((i64_to_i32_LO(2 * ARM32_NOT(v130)) | 0x5C856EA4) + (v130 ^ 0x2E42B752)))

        tmp1 = i64_to_i32_LO(16 * i64_to_i32_LO(v133))
        tmp2 = i64_to_i32_LO(v133) >> 5
        v134 = i64_to_i32_LO(((ARM32_NOT(2 * ARM32_NOT(v136)) + v137) ^ (
                    (tmp1 & ARM32_NOT(tmp2)) + v133 + (tmp2 & ARM32_NOT(tmp1)))) + v134)

        v138 = const_list[
            i64_to_i32_LO(((v130 >> 11) | 3) - ((v130 >> 11) & 0x1FFFFC) + ((v130 >> 11) | 0xFFFFFFFC) + 1)]

        tmp1 = i64_to_i32_LO(16 * i64_to_i32_LO(v134))
        tmp2 = i64_to_i32_LO(v134 >> 5)
        v139 = i64_to_i32_LO((0xFFFFFFFE - ((tmp1 ^ tmp2 ^ v134) + 2 * ARM32_NOT((tmp1 ^ tmp2 | v134)))) ^ (
                    2 * (v138 | v130) - (v138 ^ v130)))

        v133 = i64_to_i32_LO(i64_to_i32_LO(v139 ^ v133) + i64_to_i32_LO(2 * (v139 & v133)))
        # print("v135[{}] v136[{}] v137[{}] v130[{}] v134[{}] v138[{}] v139[{}] v133[{}] \n".format(hex(v135), hex(v136), hex(v137), hex(v130), hex(v134), hex(v138), hex(v139), hex(v133)))

    enc_table = v133 | v134 << 32
    # print(hex(enc_table))
    return enc_table
    
def gen_const_list(cipher_num_p2):
    const_list = []
    # cipher_num_p2 = 0x185c8200a2d37281
    const_list.append(cipher_num_p2 & 0xffffffff)
    const_list.append(((cipher_num_p2 & 0xffff) << 16) | (cipher_num_p2 >> 48))
    const_list.append(cipher_num_p2 >> 32)
    const_list.append((cipher_num_p2 & 0xffffffff0000) >> 16)
    return const_list

 主体加密算法

def enc_proto_bin(bstring, cipher_bstring, cipher_num_p2):
    cur_round = -1
    encTable = 0
    for i in range(len(bstring)):
        if i // 8 > cur_round:
            encTable = gen_encTable(get_cipher_from_bstring(cipher_bstring), cipher_num_p2)
            cur_round += 1
        cur_byte = byte_enc(bstring[i], get_byte_by_num(encTable, i % 8))
        cipher_bstring += struct.pack('<B', cur_byte)
    return cipher_bstring

def order_proto(rst_list, p_message):
    rst_list.append(p_message.SerializeToString())
    p_message.Clear()

def byte_enc(a, b):
    # return (a - b + 2*(b & ~a) ) & 0xff
    return (a ^ b) & 0xff

四、pcbc文件与种子密钥

在分析protobuf字段加密算法的过程中,发现一些重要的初始数据来源不明,分别是初始密文cipher_bstring、初始常量cipher_num_p2及长段数据partial_pcbc。我将这三个数据组成一个list称为种子密钥。

1. 种子密钥分析过程

通过hook打印日志,从地址0xCf5c开始分析种子密钥的来源,->0xD310->0xD964->0xDd44->0x298b4

298FC处BL sub_1C6A8后第一次出现数据

29950代码块处循环生成需要的值

LSR 将x8右移8位

BFI 将x8的0位开始共7位,插入到x11的0x39位处

      X8   0x72CB91F365621959

      X11 0x0072CB91F3656219

      BFI             X11, X8, #0x39, #7

      X11 0xB272CB91F3656219

      Ps:0x59<<1 == 0xB2

EON 将操作数取反后进行逻辑异或

      X8  0x72CB91F365621959    取反  0x8d346e0c9a9de6a6

      X9  0x000000000000000E    取反  0xFFFFFFFFFFFFFFF1

      EON             W9, W8, W9

      得到x9 0x000000009A9DE6A8

      结果0x72cb91f365621957

分析sub_1C6A8方法

V27每次取前16个字节之一

从地址B82,取值0x75,依次取后续值

这里的值是一个长度22266的长数据段的尾部。

长数据块是啥,来自哪里? 

来自Java层initNative函数传入的jbyte数组

Constructor(Context.class, String.class, byte[].class, Object.class, Bundle.class, Integer.TYPE);

DroidGuard(Context arg4, String arg5, byte[] arg6, Object arg7, Bundle arg8, int arg9)

来自/data/user/0/com.google.ads.rewardedvideoexample/app_pccache/5/43DD0D45399166CCF9057785EDF137EC7719BB95/pcbc  这个文件

原始文件,位于gms

通过测试得到以下结论:

  • 同一手机pcam.jar 和 pcbc都是一样的

  • 不同手机pcam.jar可以一样

  • 同一手机重装GMS,pcam.jar一样,pcbc不一样,种子密钥不一样

将当前的pcbc文件替换成前面的pcbc,结果跟之前一样

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值