一、背景介绍
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,结果跟之前一样