【Android安全-某短视频虚拟机分析和还原】此文章归类为:Android安全。
在windows流行的时候,虚拟机保护是多人不敢碰的东西现在依然也是如此,pc的性能比移动端性能要高出不少,虚拟化和变异的代码多到令人发指,因此在加密保护强度上要比移动端要强很多很多,为了移动端App更好的体验(ANR率)移动端加密强度短时间内不会达到pc上的强度,随着移动cpu性能越来越好相信加密强度会逐年加强。
早年兴趣使然分析研究过windows端VMProtect、Safengine Shielden、Themida、VProtect、Enigma Protector等等虚拟机,最近发现国内流行的短视频也有虚拟机加密同时也比较感兴趣,便开始了我的分析之旅。
分析任何虚拟机必须要扣汇编指令级细节。
安卓诞生这么多年了至今没有像windows端olldbg、x64dbg那样友好的调试器,IDA PRO虽然自带了安卓调试器总是没有相像中的稳定。lldb作为移动端iOS和android开发的御用调试器,带源码调试在开发环境中还算比较友好,而汇编级调试只能输入命令行了,这是很多用惯了gui调试器的人接受不了的,但是个人发现lldb调试稳定性出奇的好,功能上比IDA Pro的安卓调试器强大太多了。
libEncryptor.so一共包含了三套虚拟机,三套虚拟机各自独立并且代码一模一样,本文重点只分析vm2虚拟机。
虚拟机指令编解码参考借鉴了arm64的一部分规则,并实现了自己的一套规则,在后面的解码分析中会有很多和arm64解码相似的地方。
另外虚拟机并没有像VMProtect那样将一条指令分割成多条"微指令"的方式,此虚拟机没有把当前真实的上下文放到虚拟机上下文去模拟执行,而是运行了一套自己单独的上下文。
vm1
: 0xDA0vm2
: 0x2AC4vm3
: 0x444在函数中调用虚拟机时会传入一个指针数组类型参数变量,这是传入到虚拟机入口的唯一参数。
1 | void vm_entry(* int args) |
c伪代码来表示函数调用虚拟机入口
1 2 3 4 5 | void ttEncrypt( char * buff, int buff_size) { int dst_size = size +0x76; char * pDstBuff = malloc (size +0x76); vm_entry(buff, size, pDstBuff, &dst_size); } |
反汇编版本:
IDA Pro查看入口的cfg图,复杂程序看似很难其实一点都不简单,话说回来cfg看起来和ollvm的混淆平坦化非常相似,其实和ollvm混淆关系不大,只不过一部分的switch被拉平了,在了解调度逻辑后分析也不算复杂。
在代码中依然能看到两个ollvm swtich var变量,其作用没有详细分析,但整个cfg图确定与ollvm关系不是很大,猜测开启ollvm后性能会大幅下降影响app启动速度了。
在进入虚拟机运行时前,在入口需要准备虚拟机所需的内存和参数。对虚拟机内存布局情况必须了解如指掌,这样在动态和静态分析时才不会迷失方向。
SP | 类型 | 变量名 | 注释 |
---|---|---|---|
0x0 | 未使用 | ||
0x8 | char * | pSrcBuffer | |
0x10 | int | srcSize | |
0x18 | char * | pDstBuffer | |
0x20 | int * | pDstBufferSize | |
0x28 | void * | pCall_register_trampoline | |
0x30 | void * | pVMMemoryEnd | 偏移0x510 |
0x38 | vmMemoryStart | 虚拟机运行时专用内存起始位置 | |
... | |||
0x510 | vmMemoryEnd | 虚拟机运行时内存结束位置 | |
0x518 | |||
0x520 | x28 | ||
0x528 | x19 | ||
0x530 | x29 | 上一个栈桢地址 | |
0x538 | x30 |
pOpcode
:opcode指针指向虚拟机要执行的代码pArgs
:传入虚拟机的参数pReserve
:未使用,用途暂时未知pExternalFunc
:调用虚拟机外的函数列表,此列表是加密的。pVmData
:一个结构体指针,结构体存储了两指针分别是函数跳板地址另一个是虚拟机内存结束地址,{0x0: pCallRegisterTrampoline, 0x8: pVMMemoryEnd}1 | vm2_run( void * pOpcode, void * pArgs, void * pReserve, void * pExternalFunc, void * pVmData); |
vm2_run仅仅只分配了保存被调用者寄存器的堆栈内存空间,并没有分配空闲的堆栈内存,在虚拟机真实开始之前会将传递进来的5个参数即0x-x4对虚拟机中的虚拟寄存器和真实专用寄存器进行初始化。
vm_run还初始化了解码opcode的switch表,在初始化时发现一共初始化了6张switch表,当然在handler中还存在其他switch表,这么多表是如何来的?猜测编写时只有1-2张表,在编译器优化后表就被分割成多块了。
虚拟寄存器初始化
vm2_run初始化时会将传递的5个参数赋值给虚拟寄存器,其中包括PC和SP的值。
虚拟寄存器 | 参数 | 注释 |
---|---|---|
x0 | x0始终为0,XZR寄存器? | |
x4 | arg2: pArgs | |
x5 | arg3: pReserve | |
x6 | arg4: pExternalFunc | |
x7 | arg4: pVmData->pCallRegisterTrampolineFunction | |
x29(SP) | arg5: pVmData->pVmMemoryLimit - 0x150 | SP的初始值 |
x31(LR) | 初始值为0,寄存器名不确定,vm退出时保存退出代码编号 | |
PC | arg1: pOpcode |
真实专用寄存器
虚拟机运行时使用了真实虚拟器,其中包括临时寄存器和专用寄存器,临时寄存器保存多种类型的值,而专用寄存器在虚拟机从开始到退出只保存一种指定类型数据或恒定不变。
opcode位域伪代码:
w12保存了4位32字节的opcode,在opcode首次解码时,位域中的变量会放到真实寄存器。
位域伪代码示例:w12[26]
: 取第26位到放到入目标的26位w12[26->0]
: 取第26位并放入到目标的指定位 w12[27-26->1-0]
: 取27位和26位放入到目标第1位和第0位|
: 按位或
真实寄存器 | 注释 |
---|---|
x0 | temp/pcode |
x1 | temp,保存[x19-0x20]的值某种流程控制 |
x2 | temp |
x3 | temp |
x4 | 虚拟寄存x4,初始化时保存pBufferInfo(x1),虚拟机中保存跳板函数地址 |
x5 | 虚拟寄存x5,初始化时保存数值为0,虚拟机运行时跳板函数的参数指针 |
x6 | ollvm混淆switch_var,初始值:0x400000b,这个应该是llvm混淆的switch var |
w7 | ollvm混淆switch_var,初始值:0x200fff,这个应该是llvm混淆的switch var |
w8 | w12[20-16] operand1 Xt/Xm,det register index? |
w9 | w12[25-21] operand2 Xn,src register index? |
w10 | w12[15->4] | w12[14->3] | w12[13->2] | w12[12->1] | w12[31->0],5个位合并到低5位 |
w11 | w12[30->4] | w12[29->3] | w12[28->2] | w12[27-26->0-1],组成的低5位 |
w12 | 32位的opcode |
w13 | w12[31] |
w14 | w12[30] |
w15 | w12[29] |
w16 | w12[28] |
w17 | w12[27] |
w18 | w12[26] |
x19 | 虚拟机下文负偏移指针指向可使用内存最高上限的地址与vmMemoryLimit值相同,使用负偏移对上下文进行访问。 |
x20 | call_register_trampoline 跳板地址 |
x21 | Context,上下文指针 |
x22 | switch_table7 |
x23 | switch_table6 |
w24 | 默认为1 |
x25 | switch_table5 |
x26 | switch_table3 |
x27 | switch_table_main |
x28 | switch_table2 |
x29(fp) | 未使用 |
x30(lr) | switch_table_4(3A) |
虚拟机context
虚拟机中也有专用寄存器,在调用外部函数时,其中x4
虚拟机中保存外部函数地址,x5
虚拟机中保存参数指针,x25
虚拟机中调用外部函数时的跳板地址。另外虚拟寄存器和aarch64中的寄存器并不是一一对应的,在这里只是对每个虚拟寄存器启了一个相应的名字方便理解和记忆。
负偏移 | 虚拟寄存器 | 注释 |
---|---|---|
-0x150 | 虚拟机堆栈SP初始位置 | |
-0x148 | ||
-0x140 | ||
-0x138 | pc | pc指针,真实寄存器x21中的值指向此地址,handler使用 |
-0x130 | x0 | 初始化值:0,虚拟机中开始到结束始终为0,XZR寄存器? |
-0x128 | x1 | |
-0x120 | x2 | |
-0x118 | x3 | |
-0x110 | x4 | 初始化值:pBufferInfo,专用寄存器:虚拟机中调用外部函数时保存外部函数地址 |
-0x108 | x5 | 初始化值: 0,专用寄存器:虚拟机中调用外部函数时保存参数指针 |
-0x100 | x6 | 初始化值: vm2_external_func_list |
-0xF8 | x7 | 初始化值: call_register_trampoline_function |
-0xF0 | x8 | |
-0xE8 | x9 | |
-0xE0 | x10 | |
-0xD8 | x11 | |
-0xD0 | x12 | |
-0xC8 | x13 | |
-0xC0 | x14 | |
-0xB8 | x15 | |
-0xB0 | x16 | |
-0xA8 | x17 | |
-0xA0 | x18 | |
-0x98 | x19 | |
-0x90 | x20 | |
-0x88 | x21 | |
-0x80 | x22 | |
-0x78 | x23 | |
-0x70 | x24 | |
-0x68 | x25 | 虚拟机中的专用寄存器:虚拟机中调用外部函数时的跳板地址 |
-0x60 | x26 | |
-0x58 | x27 | |
-0x50 | x28 | |
-0x48 | x29(SP) | 虚拟机堆栈SP, 初始化时指向-0x150 |
-0x40 | x30 | |
-0x38 | x31(LR) | 当值为0时退出虚拟机 |
-0x30 | ||
-0x28 | ||
-0x20 | 0 | 流程控制状态标记,值范围0-3,0:正常执行状态,2和3:程序中包含分支跳转某种流程,3和1:call某个地址函数 |
-0x18 | new pc/call某个地址函数的地址/为0时退出虚拟机 | |
-0x10 | LR,在调用调用函数结束后返回虚拟机地址 | |
-0x8 | 起始pc指针,一部分分支跳转指令参考的起始基址,例如: 跳转目标地址=pc起始地址+offset |
pOpcode取出4个字节的opcode并取得低5位解码出op1。
1 2 3 4 5 6 7 8 9 | # 初始化 LDP X20, X19, [X4] ;x20:call_register_trampoline,0x19:pVmMemoryLimit SUB X21, X19, #0x138 ;x19-0x138=x21计算出pc指针在负偏移指针的内存位置 STR X0, [X21] ;保存pc到x21,此时x0和x21中的值相同并被关联。 # 取opcode LDR W12, [X0] ;取出4字节opcode AND W10, W12, #0x3F ;opcode的低5位为op1 CMP W10, #0x3F ;op1的值最大只能小于63,说明op1只有0-63一共64个 B.HI next_pc ;下一条指令 |
在opcode首次解码时会解码4个寄存器操作数,从中提取4个位域的字段,分别保存到真实专用寄存器w8、w9、w10、w11,在arm64指令集中当指令是MADD、MSUB、UMADDL、UMSUBL、SMADDL、SMADDL等会有4个寄存器操作数的情况,在这里同样也是如此。
首次解码时位域布局:w12[31,30-26,25-21,20-16,15-12,11-6,5-0]
w12[5-0:6] = op1
w12[11-6:6] = op2
w12[15-12:4]|[31:1] = Xm/Wm
w12[20-16:5] = Xt/Wt
w12[25-21:5] = Xn/Wn
w12[30-26:5] =Xa/Waw8
: Rd(Xt/Wt)目标寄存器操作数,w12[20-16->5-0],取出16-20位保存最低位,5位可以表示0-31个寄存器。w9
: Rn(Xn/Wn)(第一个源寄存器操作数,w12[25-21->5-0],取出21-25位保存最低位,5位可以表示0-31个寄存器。w10
: Rm(Xm/Wm)第二个源寄存器操作数,w12[15->4] | w12[14->3] | w12[13->2] | w12[12->1] | w12[31->0],5位可以表示0-31个寄存器。w11
: Ra(Xa/Wa)第三个源寄存器操作数,w12[30->4] | w12[29->3] | w12[28->2] | w12[27-26->1-0],5位可以表示0-31个寄存器。
寄存器操作数伪代码表示如下:操作数大小
: X为64位操作数、W为32位操作数。目标操作数
: d=destination register, t=target register。源寄存器
:第一个源寄存器为n,第二个寄存器为m,第三个寄存器为a。R
: 表示寄存器64或32位操作数,X是64位操作数、W代表32位操作数。
在高级语言中如果一个switch太多,在某些编译器编译优化后出现在多张switch子表和子表的子表的情况,对于一些相同代码的多个case会合并到一个case中再次switch分发的情况。
根据编译器的优化编译的特性,在处理switch的case为了减少查找次数找到最终的case,在代码中会经常看到大于(GT)、小于(LE)等分支跳转,看到这个不要迷惑,这是编译器优化case的结果,这样做的目的是减少查找次数使用了类似二分查找的算法,当看到B.EQ的跳转目标和B.NE的下一条指令就是匹配到了case常量了。
在首次解码后分发过程中通常还会有二次解码,除了MADD和MSUB等等指令有4个寄存器操作数,指令只需一般指令只有2-3个寄存器操作数,一条完整的arm64指令至少需要二个寄存器操作数,这里也同样如此,当指令只有二个寄存器操作数时即一个目标操作数Xt和第一个源操作数Xn,其他的位域字段就会空闲下来,例如:op2、Xm、Xa,在二次解码时这些空闲的位域字段原有的值就会覆盖被再次利用组成其他寻址方式例如:shift、extend、imm等等。
在虚拟机指令op1的值是11(0xb)时,分发处理会解码op2,解码op2时Xt、Xn、Xm等操作数位置会发生变化,原有的x10/w10第三个源操作数在op2中变成目标操作数,第一个源操作数变成x8/w8,第二个源操作数变成x9/w9,各操作数的位域解码方式不变,变的只是操作数角色。Xt
:Rd(Xt/Wt)目标寄存器操作数(x10/w10),w12[15->4] | w12[14->3] | w12[13->2] | w12[12->1] | w12[31->0],5位可以表示0-31个寄存器。Xn
: Rn(Xn/Wn)(第一个源寄存器操作数(x8/w8),w12[20-16->5-0],取出16-20位保存最低位,5位可以表示0-31个寄存器。Xm
: Rm(Xm/Wm)第二个源寄存器操作数(x9/w9),w12[25-21->5-0],取出21-25位保存最低位,5位可以表示0-31个寄存器。
从op1的取值范围为0-63一共64条指令,当op1是11时还有存在op2和op3的情况,由于虚拟机的handler太过庞大分析所有的指令太过耗时,我们目的是还原代码逻辑,只需分析执行过的handler,对于没有执行过的handler放弃分析。
在了解了op1解码方式后,通过脚本得到执行次数最多的指令并优先分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import struct pcode_start = 0xB090 pcode_size = 0x2D8 with open ( "libEncryptor.so" , "rb" ) as f: content = f.read() bytes_code = content[pcode_start : pcode_start + pcode_size] insts_sets = {} for pc in range ( 0 , pcode_size, 4 ): word = struct.unpack( "<I" , bytes_code[pc : pc + 4 ]) if len (word) > 0 : opcode = word[ 0 ] op1 = opcode & 0x3F if insts_sets.get(op1, None ) = = None : insts_sets[op1] = 1 else : insts_sets[op1] + = 1 sorted_dict = dict ( sorted (insts_sets.items(), key = lambda item: item[ 1 ], reverse = True )) for inst, count in sorted_dict.items(): print (f 'op1: {inst}, count: {count}' ) |
得到执行次数最多的指令:
1 2 3 4 5 6 7 8 9 10 | op1: 23, count: 56 op1: 11, count: 51 op1: 40, count: 38 op1: 21, count: 27 op1: 24, count: 2 op1: 12, count: 2 op1: 17, count: 2 op1: 48, count: 2 op1: 52, count: 1 op1: 7, count: 1 |
从python打印的结果来看23、11、40前面三个指令使用最为频繁,因篇幅关系只分析这三条指令,其中op1值是11的还存在第二个操作码op2,这里选择op1=11 & op2=12的指令进行分析。
: 代码开头4行代码是判断op1的范围是否20-58之间来确定case的合法性,代码中使用了首次解码的寄存器w10并被直接重新赋值使用了,首次解码时是第二个源寄存器Xm,说明此条指令可能没有第二和第三个源寄存器。
2E60-2E8C`: 首次解码的寄存器w11是第三个源寄存器,同样被重新赋值给予新的含义了,此时有效的寄存器有w12(opcode)、w8(Xt)、w9(Xn)、x21(pContext上下文指针),这段代码主要是做了三件事:.text:0000000000002E60 060 UBFX W11, W12, #6, #6 ; w12[6-11->0-5] .text:0000000000002E64 060 AND W12, W12, #0xF000 ; w12[12-15] .text:0000000000002E68 060 BFXIL W12, W18, #20, #12 ; w12[12-15] | w12[26->6] .text:0000000000002E6C 060 ORR W11, W12, W11 ; w12[12-15] | w12[26->6] | w12[6-11->0-5] .text:0000000000002E78 060 ORR W11, W11, W17,LSR#20 ; w12[27->7] .text:0000000000002E80 060 ORR W11, W11, W16,LSR#20 ; w12[28->8] .text:0000000000002E84 060 ORR W11, W11, W15,LSR#20 ; w12[29->9] .text:0000000000002E88 060 ORR W11, W11, W14,LSR#20 ; w12[30->10] .text:0000000000002E8C 060 ORR W11, W11, W13,LSR#20 ; w12[31->11]
2E70
:x21是pContext指针,w9是第一个源寄存器是一个偏移索引值,x9 = x21 + w9 * 8得到相对的偏移植。2E7C
: 从上下文中取出第一个寄存器的值,x9 = x21 + w9 * 8 + 8。.text:0000000000002E70 060 ADD X9, X21, W9,UXTW#3 .text:0000000000002E7C 060 LDR X9, [X9,#8]
2E94 060 ADD X9, X9, W11,SXTH ; w9=address,取出半字16立即数再相加
3380
: x21是pContext指针,w8是目标源寄存器是一个偏移索引值,x9 = x21 + w9 * 8得到相对的偏移植。3384
: 取出目标寄存器的值,x8 = x21 + w8 * 8 + 8。3388
: 将目标寄存器存储到偏移量地址中,[Xn + imm]=Xt。.text:0000000000003380 case_23__STR_Xt_Mem_Xn_simm .text:0000000000003380 060 ADD X8, X21, W8,UXTW#3 ; jumptable 0000000000002E98 case 23 .text:0000000000003384 060 LDR X8, [X8,#8] .text:0000000000003388 060 STR X8, [X9]
准备下一条指令: pc指针地址加4
338C 060 B next_pc
当op1值等于11时会解码第二个或第三个操作码。
3D60
: 获取上下文中的寄存器偏移3D64
:取第二次源操作数值,x9=[x11+ w9 * 8]3D68
:取第一次源操作数值,x8=[x11+ w8 * 8]3D6C
:减法运算3D70
:将结果写入到目标寄存器,[x11+ w10 * 8] = x8.text:0000000000002D50 060 UBFX W11, W12, #6, #6 ; w12[6-11->0-5] .text:0000000000002D54 060 AND W12, W12, #0xF000 ; w12[12-15] .text:0000000000002D58 060 BFXIL W12, W18, #20, #12 ; w12[12-15] | w12[26->6] .text:0000000000002D5C 060 ORR W11, W12, W11 ; w12[12-15] | w12[28->6] | w12[6-11->0-5] .text:0000000000002D68 060 ORR W11, W11, W17,LSR#20 ; w12[27->7] .text:0000000000002D70 060 ORR W11, W11, W16,LSR#20 ; w12[28->8] .text:0000000000002D74 060 ORR W11, W11, W15,LSR#20 ; w12[29->9] .text:0000000000002D78 060 ORR W11, W11, W14,LSR#20 ; w12[30->10] .text:0000000000002D7C 060 ORR W11, W11, W13,LSR#20 ;
.text:0000000000002D60 060 ADD X9, X21, W9,UXTW#3 .text:0000000000002D6C 060 LDR X9, [X9,#8] ; << Xn
.text:0000000000002D84 060 ADD X9, X9, W11,SXTH ; X9 = Xn + imm16
3318
:偏移量的内存中取值3A70
:计算目标寄存器在pContext的偏移地址,x8= x21 + w8 * 83A74
:将内存值放入目标寄存器.text:0000000000003318 060 LDR X9, [X9] .text:0000000000003A70 060 ADD X8, X21, W8,UXTW#3 .text:0000000000003A74 060 STR X9, [X8,#8]
分析完取指、解码、执行后大体得到了一个模糊的虚拟机执行框架,为了方便记忆和理解使用c伪代码来描述。
使用脚本解析opcode的op1和op2打印所有需要分析的handler。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | import struct pcode_start = 0xB090 pcode_size = 0x2D8 with open ( "libEncryptor.so" , "rb" ) as f: content = f.read() bytes_code = content[pcode_start : pcode_start + pcode_size] insts_op1_sets = {} insts_op2_sets = {} for pc in range ( 0 , pcode_size, 4 ): word = struct.unpack( "<I" , bytes_code[pc : pc + 4 ]) if len (word) > 0 : opcode = word[ 0 ] op1 = opcode & 0x3F if op1 = = 11 : op2 = (opcode >> 6 ) & 0x3F if insts_op2_sets.get(op2, None ) = = None : insts_op2_sets[op2] = 1 else : insts_op2_sets[op2] + = 1 else : if insts_op1_sets.get(op1, None ) = = None : insts_op1_sets[op1] = 1 else : insts_op1_sets[op1] + = 1 print ( "op1指令统计:" ) op1_sorted_dict = dict ( sorted (insts_op1_sets.items(), key = lambda item: item[ 1 ], reverse = True )) for inst, count in op1_sorted_dict.items(): print (f 'op1: {inst}, count: {count}' ) print ( "op2指令统计:" ) op2_sorted_dict = dict ( sorted (insts_op2_sets.items(), key = lambda item: item[ 1 ], reverse = True )) for inst, count in op2_sorted_dict.items(): print (f 'op1: 11, op2: {inst}, count: {count}' ) |
一共打印出15个handler,好在需要分析还原的指令不多:
op1指令统计: op1: 23, count: 56 op1: 40, count: 38 op1: 21, count: 27 op1: 24, count: 2 op1: 12, count: 2 op1: 17, count: 2 op1: 48, count: 2 op1: 52, count: 1 op1: 7, count: 1 op2指令统计: op1: 11, op2: 7, count: 25 op1: 11, op2: 43, count: 14 op1: 11, op2: 12, count: 8 op1: 11, op2: 25, count: 2 op1: 11, op2: 39, count: 1 op1: 11, op2: 62, count: 1
在15个handler并还原后,现在重新编写脚本decode_opcode.py
解码opcode,将打印还原的指令打印出伪汇编代码并保存文件ttencryptor.asm
和generate_aes_key_iv.asm
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | def decode(pcode_start, pcode_size, liner_disasm = False ): with open ( "libEncryptor.so" , "rb" ) as f: content = f.read() print (f "{'pc':^8} {'指令':^8} {'op1':^8} {'op2':^8}\t{'助记符':^30}" ) print (f "{'-'*8} {'-'*8} {'-'*8} {'-'*8} MOV\tx0, #0\t\t\t;x0始终为0,XZR寄存器?" ) print (f "{'-'*8} {'-'*8} {'-'*8} {'-'*8} MOV\tx4, pArgs\t\t;参数列表指针" ) print (f "{'-'*8} {'-'*8} {'-'*8} {'-'*8} MOV\tx5, #0" ) print (f "{'-'*8} {'-'*8} {'-'*8} {'-'*8} MOV\tx6, pfn_external_func_list\t\t;外部函数列表指针" ) print (f "{'-'*8} {'-'*8} {'-'*8} {'-'*8} MOV\tx7, pCallRegisterTrampolineFunction\t;保存跳转函数地址" ) print (f "{'-'*8} {'-'*8} {'-'*8} {'-'*8} MOV\tx29, pVirualStackBottom;\t\t;虚拟机堆栈栈底" ) print (f "{'-'*8} {'-'*8} {'-'*8} {'-'*8} MOV\tlr, #0\t\t\t;x31=0" ) # 已解析的指令 known_insns_op1 = {} # dcode_insns_status=1 known_insns_op2 = {} # dcode_insns_status=2 # 未解析的指令 unknown_insts_op1 = set () # dcode_insns_status=3 unknown_insts_op2 = set () # dcode_insns_status=4 bytes_code = content[pcode_start : pcode_start + pcode_size] for pc in range ( 0 , pcode_size, 4 ): word = struct.unpack( "<I" , bytes_code[pc : pc + 4 ]) asm = "" dcode_insns_status = 0 if len (word) > 0 : opcode = word[ 0 ] op1 = get_op1(opcode) Xt = get_openand1(opcode) # x8/w8 Xn = get_operand2(opcode) # x9/w9 Xm = get_operand3(opcode) # x10/w10 X4 = get_operand4(opcode) # x11/w11 match op1: case 11 : Xt = get_operand3(opcode) # x10/w10 Xn = get_openand1(opcode) # x8/w8 Xm = get_operand2(opcode) # x9/w9 op2 = get_op2(opcode) match op2: case 7 : dcode_insns_status = 2 print (f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {op2:02d}(0x{op2:02X})\tORR\t{get_regsiter_name(Xt)}, {get_regsiter_name(Xn)}, {get_regsiter_name(Xm)}" ) case 12 : dcode_insns_status = 2 print (f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {op2:02d}(0x{op2:02X})\tADD\t{get_regsiter_name(Xt)}, {get_regsiter_name(Xn)}, {get_regsiter_name(Xm)}" ) case 25 : dcode_insns_status = 2 if X4 = = 0 : print (f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {op2:02d}(0x{op2:02X})\tNOP\t\t\t\t;LSL\t{get_regsiter_name(Xt)}, {get_regsiter_name(Xn)}, #{X4}" ) else : print (f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {op2:02d}(0x{op2:02X})\tLSL)\t{get_regsiter_name(Xt)}, {get_regsiter_name(Xn)}, #{X4}" ) case 39 : dcode_insns_status = 2 print (f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {op2:02d}(0x{op2:02X})\tCMP\t{get_regsiter_name(Xm)}, {get_regsiter_name(Xn)}" ) print (f "{' '*8} {' '*8} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {op2:02d}(0x{op2:02X})\tCSET\t{get_regsiter_name(Xt)}, CC" ) case 43 : dcode_insns_status = 2 if liner_disasm: print (f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {op2:02d}(0x{op2:02X})\tBR\t{get_regsiter_name(Xm)}\t\t\t;LR={get_regsiter_name(Xt)}" ) else : asm = f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {op2:02d}(0x{op2:02X})\tBR\t{get_regsiter_name(Xm)}\t\t\t;LR={get_regsiter_name(Xt)}" set_branch_control(asm) case 62 : dcode_insns_status = 2 if liner_disasm: print (f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {op2:02d}(0x{op2:02X})\tExitVm\t0\t\t\t;{get_regsiter_name(Xm)}" ) else : asm = f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {op2:02d}(0x{op2:02X})\tExitVm\t0\t\t\t;{get_regsiter_name(Xm)}" set_branch_control(asm) case _: dcode_insns_status = 4 print (f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {op2:02d}(0x{op2:02X})\t>> op2_xxx\t{get_regsiter_name(Xt)}, {get_regsiter_name(Xn)}, {get_regsiter_name(Xm)}" ) case 7 : dcode_insns_status = 1 imm16 = get_imm16(opcode) print (f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {'-'*8}\tORR\t{get_regsiter_name(Xt)}, {get_regsiter_name(Xn)}, #{hex(imm16)}" ) case 12 : dcode_insns_status = 1 imm26 = get_imm26(opcode) offset = imm26 * 4 if liner_disasm: print (f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {'-'*8}\tB\t{hex(offset)}" ) else : asm = f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {'-'*8}\tB\t{hex(offset)}" set_branch_control(asm) case 17 : dcode_insns_status = 1 imm16 = get_imm16(opcode) print (f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {'-'*8}\tADD\t{get_regsiter_name(Xt)}, {get_regsiter_name(Xn)}, #{hex(imm16)}" ) case 21 : dcode_insns_status = 1 imm16 = get_imm16(opcode) print (f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {'-'*8}\tADD\t{get_regsiter_name(Xt)}, {get_regsiter_name(Xn)}, #{hex(imm16)}" ) case 23 : dcode_insns_status = 1 imm16 = get_imm16(opcode) print (f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {'-'*8}\tSTR\t{get_regsiter_name(Xt)}, [{get_regsiter_name(Xn)}, #{hex(imm16)}]" ) case 24 : dcode_insns_status = 1 imm16 = get_imm16(opcode) offset = pc + imm16 * 4 + 4 if liner_disasm: print (f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {'-'*8}\tB.HS\t#{hex(offset)}\t\t\t;{get_regsiter_name(Xt)}, {get_regsiter_name(Xn)}, ${hex(imm16 * 4)}" ) else : asm = f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {'-'*8}\tB.HS\t#{hex(offset)}\t\t\t;{get_regsiter_name(Xt)}, {get_regsiter_name(Xn)}, ${hex(imm16 * 4)}" set_branch_control(asm) case 40 : dcode_insns_status = 1 imm16 = get_imm16(opcode) print (f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {'-'*8}\tLDR\t{get_regsiter_name(Xt)}, [{get_regsiter_name(Xn)}, #{hex(imm16)}]" ) case 48 : dcode_insns_status = 1 imm16 = get_imm16(opcode) print (f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {'-'*8}\tSTR\t{get_regsiter_name(Xt, 32)}, [{get_regsiter_name(Xn)}, #{hex(imm16)}]" ) case 52 : dcode_insns_status = 1 imm16 = get_imm16(opcode, False ) print (f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {'-'*8}\tMOVZ\t{get_regsiter_name(Xt, 32)}, #{hex(imm16)}, LSL#16" ) print (f "{' '*8} {' '*8} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {'-'*8}\tSXTW\t{get_regsiter_name(Xt)}, {get_regsiter_name(Xt, 32)}" ) case _: dcode_insns_status = 3 print (f "{pc:08X} {opcode:08X} {opcode & 0x3F:02d}(0x{opcode & 0x3F:02X}) {'-'*8}\t>> op1_xxx\t{get_regsiter_name(Xt)}, {get_regsiter_name(Xn)}, {get_regsiter_name(Xm)}" ) if not liner_disasm: branch_pipeline_process() record_insns(dcode_insns_status, known_insns_op1, known_insns_op2, unknown_insts_op1, unknown_insts_op2, opcode) else : print ( "error" ) decode_statistics(known_insns_op1, known_insns_op2, unknown_insts_op1, unknown_insts_op2) def main(): # 解码vm2 pcode_start = 0xB090 pcode_size = 0x2D8 decode(pcode_start, pcode_size) # # 解码vm3,获取aes的key和iv pcode_start = 0xBDE0 pcode_size = 0x1C8 decode(pcode_start, pcode_size) # 解码vm1,JNI_OnLoad pcode_start = 0x85C0 pcode_size = 0xCC decode(pcode_start, pcode_size) if __name__ = = "__main__" : main() |
vm2还原的伪汇编代码,经过分析主要做了这些事件:
由于vm2会调用vm3生成aes的key和iv,因此vm3的代码也需要解析还原generate_aes_key_iv.asm:
这就是libEncryptor.so中ttEncrypt函数的加密算法了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | import secrets import hashlib from Crypto.Cipher import AES # pip install pycryptodome from Crypto.Util.Padding import pad, unpad def generate_rand_number(): return secrets.token_bytes( 32 ) def decrypt_seeds(): key1 = [ 0x52 , 0x09 , 0x6A , 0xD5 , 0x30 , 0x36 , 0xA5 , 0x38 , 0xBF , 0x40 , 0xA3 , 0x9E , 0x81 , 0xF3 , 0xD7 , 0xFB ] key1 + = [ 0x7C , 0xE3 , 0x39 , 0x82 , 0x9B , 0x2F , 0xFF , 0x87 , 0x34 , 0x8E , 0x43 , 0x44 , 0xC4 , 0xDE , 0xE9 , 0xCB ] key1 + = [ 0x54 , 0x7B , 0x94 , 0x32 , 0xA6 , 0xC2 , 0x23 , 0x3D , 0xEE , 0x4C , 0x95 , 0x0B , 0x42 , 0xFA , 0xC3 , 0x4E ] key1 + = [ 0x08 , 0x2E , 0xA1 , 0x66 , 0x28 , 0xD9 , 0x24 , 0xB2 , 0x76 , 0x5B , 0xA2 , 0x49 , 0x6D , 0x8B , 0xD1 , 0x25 ] key2 = [ 0x1F , 0xDD , 0xA8 , 0x33 , 0x88 , 0x07 , 0xC7 , 0x31 , 0xB1 , 0x12 , 0x10 , 0x59 , 0x27 , 0x80 , 0xEC , 0x5F ] key2 + = [ 0x60 , 0x51 , 0x7F , 0xA9 , 0x19 , 0xB5 , 0x4A , 0x0D , 0x2D , 0xE5 , 0x7A , 0x9F , 0x93 , 0xC9 , 0x9C , 0xEF ] key2 + = [ 0xA0 , 0xE0 , 0x3B , 0x4D , 0xAE , 0x2A , 0xF5 , 0xB0 , 0xC8 , 0xEB , 0xBB , 0x3C , 0x83 , 0x53 , 0x99 , 0x61 ] key2 + = [ 0x17 , 0x2B , 0x04 , 0x7E , 0xBA , 0x77 , 0xD6 , 0x26 , 0xE1 , 0x69 , 0x14 , 0x63 , 0x55 , 0x21 , 0x0C , 0x7D ] results = bytearray() for i in range ( len (key1)): results.append(key1[i] ^ key2[i]) return results def sha512(buff): sha512_hash = hashlib.sha512() sha512_hash.update(buff) return sha512_hash.digest() def get_aes_key_iv(rand_num): rand_num_hash = sha512(rand_num) seeds = decrypt_seeds() data = rand_num_hash + seeds key_iv_hash = sha512(data) print (f "rand_num: {rand_num.hex()}" ) print (f "rand_num_hash: {rand_num_hash.hex()}" ) print (f "seeds: {seeds.hex()}" ) print (f "rand_num_hash + seeds: {data.hex()}" ) print (f "key_iv_hash: {key_iv_hash.hex()}" ) print (f "aes key: {key_iv_hash[0:0x10].hex()}" ) print (f "aes iv: {key_iv_hash[0x10:0x20].hex()}" ) return key_iv_hash[ 0 : 0x10 ], key_iv_hash[ 0x10 : 0x20 ] def aes_128_cbc_encrypt(plaintext, key, iv): # 创建一个 AES 加密器对象 cipher = AES.new(key, AES.MODE_CBC, iv) # 添加填充并加密数据 padded_data = pad(plaintext, AES.block_size) ciphertext = cipher.encrypt(padded_data) return ciphertext def get_magic(): return b "\x74\x63\x05\x10\x00\x00" def ttEncrypt(buff): magic = get_magic() rand_number = generate_rand_number() aes_key, aes_iv = get_aes_key_iv(rand_number) buff_hash = sha512(buff) aes_plaintext = buff_hash + buff aes_ciphertext = aes_128_cbc_encrypt(aes_plaintext, aes_key, aes_iv) print (f "plaintext: {buff.hex()}" ) print (f "rand_number: {rand_number.hex()}" ) print (f "buff hash: {buff_hash.hex()}" ) print (f "aes_plaintext: {aes_plaintext.hex()}" ) print (f "ciphertext: {aes_ciphertext.hex()}" ) return magic + rand_number + aes_ciphertext def main(): buff = b 'aabbccddeeffgg' result = ttEncrypt(buff) print (f "ttEncrypt: {result.hex()}" ) def test_aes(): buff = b "\x61\x02\xbe\x54\xa6\x2a\x73\xe7\x65\xba\x38\xc9\x87\x34\x09\xbd" + \ b "\xeb\xb6\xb0\xd3\x7e\xa0\x60\x40\x3d\x0c\x26\xfe\xa5\xeb\xb6\xba" + \ b "\x5a\x0c\x7f\x36\xec\xb7\x58\xc7\x7e\x19\x37\x50\x5f\xa8\x5b\x4e" + \ b "\x77\xce\x82\x7a\x70\x09\xd2\x2b\x2f\xaf\xc4\x68\x00\xd7\xa9\xff" + \ b "\x62\x69\x61\x6e\x66\x65\x6e\x67" aes_key = b "\xe8\xaf\x6e\x91\xde\x99\x7e\xf0\xfa\xfb\xcd\xbe\x97\x73\xb2\xc5" aes_iv = b "\x03\x7e\xed\x97\x4e\x1e\xc5\x19\xdc\xc2\xb4\x35\x5b\x26\xf0\x1b" ciphertext = aes_128_cbc_encrypt(buff, aes_key, aes_iv) print (f "ciphertext: {ciphertext.hex()}" ) if __name__ = = "__main__" : main() # test_aes() |
为了验证算法是否有效,这里使用了dy模块中的ttEncrypt算和tt的设备注册代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | import secrets import uuid import time import json import hashlib import gzip import requests # pip install requests import ttEncryptorUtil # 注: 协议来自Tiktok def UUID(): return str (uuid.uuid4()) def md5(message): md5_hash = hashlib.md5() md5_hash.update(message) return md5_hash.hexdigest() def get_timestamp_in_millisecond(): return int (time.time() * 1000 ) def get_timestamp_in_second(): return int (time.time()) def generate_android_id(): return secrets.token_bytes( 8 ). hex () def http_post(url, headers, payload): response = requests.request( "POST" , url, headers = headers, data = payload) return response.text def gzip_compress(buff): return gzip.compress(buff) def get_post_data(android_id, cdid, google_aid, clientudid): openudid = android_id postDataObj = { "magic_tag" : "ss_app_log" , "header" : { "display_name" : "TikTok" , "update_version_code" : 2023205030 , "manifest_version_code" : 2023205030 , "app_version_minor" : "", "aid" : 1233 , "channel" : "googleplay" , "package" : "com.zhiliaoapp.musically" , "app_version" : "32.5.3" , "version_code" : 320503 , "sdk_version" : "3.9.17-bugfix.9" , "sdk_target_version" : 29 , "git_hash" : "3e93151" , "os" : "Android" , "os_version" : "11" , "os_api" : 30 , "device_model" : "Pixel 2" , "device_brand" : "google" , "device_manufacturer" : "Google" , "cpu_abi" : "arm64-v8a" , "release_build" : "e7cd5de_20231207" , "density_dpi" : 420 , "display_density" : "mdpi" , "resolution" : "1794x1080" , "language" : "en" , "timezone" : - 5 , "access" : "wifi" , "not_request_sender" : 1 , "rom" : "6934943" , "rom_version" : "RP1A.201005.004.A1" , "cdid" : cdid, "sig_hash" : "194326e82c84a639a52e5c023116f12a" , # md5(packageInfo.signatures[0]) "gaid_limited" : 0 , "google_aid" : google_aid, "openudid" : openudid, "clientudid" : clientudid, "tz_name" : "America\\/New_York" , "tz_offset" : - 18000 , "req_id" : UUID(), "device_platform" : "android" , "custom" : { "is_kids_mode" : 0 , "filter_warn" : 0 , "web_ua" : "Mozilla\\/5.0 (Linux; Android 11; Pixel 2 Build\\/RP1A.201005.004.A1; wv) AppleWebKit\\/537.36 (KHTML, like Gecko) Version\\/4.0 Chrome\\/116.0.0.0 Mobile Safari\\/537.36" , "user_period" : 0 , "screen_height_dp" : 683 , "user_mode" : - 1 , "apk_last_update_time" : 1702363135217 , "screen_width_dp" : 411 }, "apk_first_install_time" : 1697783355395 , "is_system_app" : 0 , "sdk_flavor" : "global" , "guest_mode" : 0 }, "_gen_time" : get_timestamp_in_millisecond() } return gzip_compress(json.dumps(postDataObj).encode(encoding = 'utf-8' )) def get_headers(md5Hash): headers = { 'log-encode-type' : 'gzip' , 'x-tt-request-tag' : 't=0;n=1' , 'sdk-version' : '2' , 'X-SS-REQ-TICKET' : f '{get_timestamp_in_millisecond()}' , 'passport-sdk-version' : '19' , 'x-tt-dm-status' : 'login=0;ct=1;rt=4' , 'x-vc-bdturing-sdk-version' : '2.3.4.i18n' , 'Content-Type' : 'application/octet-stream;tt-data=a' , 'X-SS-STUB' : md5Hash, 'Host' : 'log-va.tiktokv.com' } return headers def get_device_register_url(openudid, cdid): url = 'https://log-va.tiktokv.com/service/2/device_register/?' + \ "tt_data=a" + \ "ac=wifi" + \ "channel=googleplay" + \ "aid=1233" + \ "app_name=musical_ly" + \ "version_code=320503" + \ "version_name=32.5.3" + \ "device_platform=android" + \ "os=android" + \ "ab_version=32.5.3" + \ "ssmix=a" + \ "device_type=Pixel+2" + \ "device_brand=google" + \ "language=en" + \ "os_api=30" + \ "os_version=11" + \ f "openudid={openudid}" + \ "manifest_version_code=2023205030" + \ "resolution=1080*1794" + \ "dpi=420" + \ "update_version_code=2023205030" + \ f "_rticket={get_timestamp_in_millisecond()}" + \ "is_pad=0" + \ "current_region=TW" + \ "app_type=normal" + \ "timezone_name=America%2FNew_York" + \ "residence=TW" + \ "app_language=en" + \ "ac2=wifi5g" + \ "uoo=0" + \ "op_region=TW" + \ "timezone_offset=-18000" + \ "build_number=32.5.3" + \ "host_abi=arm64-v8a" + \ "locale=en" + \ f "ts={get_timestamp_in_second()}" + \ f "cdid={cdid}" return url def device_register(): android_id = generate_android_id() cdid = UUID() google_aid = UUID() clientudid = UUID() gzip_post_data = get_post_data(android_id, cdid, google_aid, clientudid) ttencrypt_post_data = ttEncryptorUtil.ttEncrypt(gzip_post_data) headers = get_headers(md5(ttencrypt_post_data)) url = get_device_register_url(android_id, cdid) response = http_post(url, headers, ttencrypt_post_data) print (f "设备注册结果:\n{response}" ) if __name__ = = "__main__" : device_register() |
结果:
1 2 | 设备注册结果: { "server_time" : 1719390270 , "device_id" : 7384724147647088134 , "install_id" : 7384724761467832069 , "new_user" : 1 , "device_id_str" : "7384724147647088134" , "install_id_str" : "7384724761467832069" } |
在分析完之后虚拟机保护的强度没有想像中的那么好,在分析过的虚拟化加密强度非要分10个等级的话,VMProtect为10级、Safengine Shielden强度为9级、Themida强度9级、VProtect强度7级、Enigma Protector强度3级,而它的强度等级仅为1级。
更多【Android安全-某短视频虚拟机分析和还原】相关视频教程:www.yxfzedu.com