首页
社区
课程
招聘
[原创]谈谈我对VMProtect代码保护”通解”的一点看法
发表于: 1天前 657

[原创]谈谈我对VMProtect代码保护”通解”的一点看法

1天前
657

偶然一阵袭来偶然论坛看到两篇关于VMProtect技术文章让我想起了多年以前我也曾花了一段时间研究过VMProtect写了不少相关代码于是翻起许久未曾改过相关代码仓库甚是感慨有了这篇文章

这篇文章不会有过于详细技术分享并非标题党文题通解就是对于VMProtect甚至一系列包括Themida在内许多代码保护方案通用解法之所以引号以为所谓通用解法只是解题思路基本相同

寻求通解那么我们需要说下这类代码保护基本思路架构这里简化

VMProtect这种代码虚拟机虚拟机Themida虚拟机工作逻辑当前线程空间储存代码执行Context上下文主要就是储存保护代码执行所需要寄存器因此VMProtectContext最终代码执行时候虚拟机代码本身数据混搭穿插中间由于VMProtect代码保护时候就要追踪空间变动因此Context分布还是随机动态调整进一步增加分析难度

VMProtect代码保护采取原始代码汇编编译转换一套定义的opcode然后每个opcode一个或者多个代码可以不同但是逻辑功能必定相同)opcode执行代码片段就是所谓的”handler”,这些handler开发时候需要针对opcode进行针对性设计所以变化演进相对很慢这就是VMProtect这类代码虚拟机比较弱点代码转换成自己的opcode序列以后形成自己一套汇编代码然后这段汇编代码还可以当成普通汇编那样处理要么继续生成另外一套opcode套娃要么堆opcode进行等价替换变形插花思路都是代码量执行效率当中平衡opcode编码完毕以后就有一套对应的opcode处理代码此时将ocpode处理代码变形插花然后联合框架代码形成程序

了解思路以后我们可以设计我们通解思路这里给出一个针对实现理解代码最终干预代码执行流程通解做到完整代码还原给出大体思路注意对于绕过调试这种代码保护无关的方法本文涉及尽管r3也有通解但是这些方法公开很快针对

首先针对于比较简单流程干预思路主要以下几个步骤

  1. 拿到代码执行记录

  2. 简化代码

  3. 染色还原context偏移

  4. 匹配Hanlder特征还原opcode代码执行记录

  5. 简化opcode

  6. opcode代码调试确定干预意图

  7. 接管代码执行进行干预

  8. 编译opcode汇编实现代码还原

下面步骤介绍

1.拿到代码执行记录

对于很多学习VMProtect来说其实这一步并不顺畅因为公开工具太少,x64dbg自带追踪太慢VMProtect这种Handler代码越来越大越多情况代码执行记录方法节省很多时间个人首推JIT转译代码虚拟机也就是自己使用方法虚拟机代码入口接管执行流程然后代码放到一个代码虚拟机执行这个虚拟机的opcode就是原始代码,hander就是转译后的代码本身寄存器存放位置实际寄存器位置以及一个空间用于存放虚拟机执行占用的1-2寄存器这种方法好处非常多实现简单不需要编写handler,不需要定义opcode)效率够高支持sm代码自修该由于是jit支持动态条件干预比如对于循环执行检测直接跳过重复代码记录

拿到代码记录以后我们就会有一份代码进入虚拟机执行完毕退出虚拟机完整代码执行记录类似

执行记录包括常用寄存器,eflags代码地址以及具体执行代码二进制数据不少情况下拿到完整代码执行记录已经能够实现软件破解效果只需要控制入口数据比如虚拟机快照功能授权授权代码执行记录通过文本对比工具一眼就能看到具体判断逻辑从而定位准确代码干预然后这个反转控制流实现破解根本无需分析一行汇编代码有了这些数据来到了第二

 

1.简化代码

拿到代码执行记录以后这个记录可能是海量的超多重复执行代码而且经过高度插花混淆变形替换代码直接分析不可能分析打死都不这样分析因此我们想办法简化代码由于代码执行记录自上而下带有寄存器因此通用简化办法这里核心优化手段就是等价替换没有什么复杂技术但是实现方法各有办法使用方法是自己写了一个轻量汇编匹配替换语言大概下面这个样子

 


这里当时代码一些用法注释大概就能明白意思

 

/*花型语法:-----------------:花型分割线,至少5个-,分割以后可以定义多个花型花型选项,包括:.mode	isReverseMode	//花型的匹配模式为倒叙或则顺序,用于过滤花型.x86			//cpu指令集类型,.x86-64			//花型作用的cpu指令集类型,如果不设置,那么支持所有.max <integer>			//花型最大匹配的指令数量.uuid	<integer>	//花型的唯一id号.off	//禁用花型,默认为启用.on		//启用花型.comment	<string>	//用于ollydbg调试时自动注释.pattern	<string>	//用于DNA时传递给分析程序使用,patternASM只作传递而已,没有任何处理.solo					//用于调试花型时使用,表示实例范围内只启用该花型//表示该花型必须包含给定的16进制数据,主要用于在大范围数据当中加速搜索,例如:.hexsig{1000,200} 55 13 ?? 1C表示先搜索55 13 ? 1C为特征的数据,然后在匹配结果的前1000字节和后200字节范围内进行花型搜索?表示1个任意字节,接着就是匹配花型:=表示表达式,可以通过简单的表达式进行简单的计算如imm1=imm2+imm1::表示指令的匹配属性,如:::{*}{+}{?}*:表示可以重复匹配该行无数次?:表示只匹配0或者1次+:表示至少匹配1次-:表示尽可能少的匹配{repeat,1,2}:表示重复次数范围,如同正则表达式规则stack:表示指令必须不影响栈平衡eflags:表示指令必须不影响eflagsuser1-user4,提供4个用户标记的flags,如果指定了{user1-4}属性,那么指令匹配必须要在testInst函数调用时给定了相应的TIF_OPT_USERFLAG1参数{mCF,mPF,mAF,mZF,mZF,mSF,mIF,mDF,mOF}:表示修改这一系列标志位{tCF,tPF,tAF,tZF,tZF,tSF,tIF,tDF,tOF}:表示测试这一系列标志位{mEflags,tEflags}:表示有Eflags修改,或测试{$1,$2}info前缀表示记录该行匹配的详细记录,这在DNA匹配时相当有用,匹配成功后,可以通过获取info的信息来分析具体的匹配情况相应的!符号,表示取反,即{!mCF}表示没有修改CF这些标志都是基于指令的,与实际执行无关,而eflags,stack,属性标记是基于指令执行结果的需要注意的是,一行指令只允许有1个::标记,即使使用了||连接,也就是{?}::mov || {+}::lea 是非法的,{+}属性并不会起作用,{?}会作用与整行寄存器有几个伪寄存器,分别为:cax,cbx,ccx,cdx,csp,cbp,csi,cdi,分别对应不同平台的eax,rax,根据patternASM初始化的位数而定指令属性:{size1}{mnem12}{strict}{!}{8}{16}{32}{64}{cs}{ds}{es}{fs}{gs}{ss}{cs}-{ss}可以强制匹配指定段的指定,替换指令则可以强制替换指令的段,如果不指定,那么替换指令的段属性将从原指令复制{0}表示按处理器指针大小匹配any表示任何指令jcc表示跳转指令,匹配distorm的FC_CND_BRANCH如any reg1,imm1{size1}{mem1}{reg1}均属于变量定义操作数属性:如push {size1}{16}reg1,imm1{size1}引用的size变量{!}not{*}所有op如果需要匹配可以使用relax属性ref和!ref还支持显示模式,如下:{ref_01234abc}	操作数需要引用rax,rbx,rcx,rdx,r10,r11,r12当中的一个{!ref_89}操作数不能引用r8,r9{ref_123_reg1_reg2}表示同时引用reg1,reg2所有ref系列都不能作用于imm,因为无意义{ref_xxx}{!ref_xxx}作用于指令的时候表示匹配或不匹配指令的隐式寄存器。这和作用于操作数时意义是不一样的。express不能应用relax,size这样的属性如call {relax}$+5可以编译成功,但是relax属性会被忽略any reg1,mem默认匹配第一个参数是reg类型,但是一旦使用了ref或!ref将导致第一个参数仍然测试非reg类型的指令,因为内存操作数仍然可能引用reg1{8}{16}{32}{64}{cover}	//表示匹配的寄存器覆盖了初始寄存器,如{stack}::{strict}any reg1 ->noppop {cover}reg1,表示此时的寄存器必须包含reg1{!cover}{belong}如果cover,只是判断依据是被包含{!belong}{relax} relax关键字表示松弛比对,目前的含义为不比较操作数的大小操作数类型op1:表示所有操作数reg1:寄存器imm1:立即数mem1:寻址如果操作数类型不跟数字,直接未reg,imm,mem那么表示只是op类型匹配。不匹配具体细节如 any {*}{!}mem表示所有没有内存寻址操作数的指令使用例子:push eax	直接匹配push reg1,凡是push 寄存器的都匹配push {!ref}esp替换花型可以使用{original}表示直接替换为原始指令,如push reg1 ->{original} && nop替换指令支持的属性:{original} {!ref}替换指令支持的操作数类型rnr1_0123456789abcdef_nr0-f后缀是寄存器选择范围,nr是表示,随机选择的寄存器还需要排除原指令使用的寄存器{rnr1}表示随机选取一个寄存器,当第一次定义的时候可以利用rnr1_0123-f定义随机选取的范围,从eax,ebx,ecx,edx,ebp,esp,esi,edi,r8-r15分别代表0-15寄存器当第二次使用时,就不需要指定了,可以直接通过rnr1引用注意替换指令当中,是用&& 连接多条指令,而非||//配合.hexsig可以加速花型搜索//例如当我们知道花型当中至少包含1个字节的具体数据时//那么大范围内存搜索就可以通过先搜索hex特征,然后//将范围缩小,这样可以呈指数型的加快搜索速度.hexsig{1000,200} 55 8B ? CCmov reg,imm32	-> push imm32 && pop reg{stack}::{mnem1}any op1,reg1	->{mnem1}any op1,imm1mnem1!=xchg|cmpxchgimm3=imm2-imm1imm4=imm3+2$n对象目前支持的属性:$n.ip表示匹配指令的ip,如果匹配集合为空,那么失败如{$1}::call $+5jmp $1.ip那么jmp $1.ip会匹配到jmp <call指令>

 

 

有了通用语义以后我们可以不断积累各种等价替换花型然后代码执行等价替换无论VMProtect经过多少混淆只要等价花型积累一定数量最终都能大大简化代码况且我们这里简化代码目的并非是要代码能够清晰可读而是只要简化能够匹配出hander特征够了这样我们知道代码对应的hander哪个至于代码如何巧妙执行我们其实并不关心比如代码简化下面情况差不多

 

 

00007FF7825429E4 JMP        R9                 

00007FF7826B37CD MOVZX      EDI, BYTE [RBP-0x1]

00007FF7826B37D2 MOV        R8D, 0x1703c7ae    

00007FF7826B37D8 LEA        RSI, [R8*4+0x652b39ae]

00007FF7826B37E0 XOR        DIL, R11B          

00007FF7826B37E3 LEA        RAX, [RSI+RSI*4-0x12666cea]

00007FF7826B37EB XCHG       RSI, RAX           

00007FF7826B37ED ADD        AX, 0x602          

00007FF7826B37F1 ROR        DIL, 0x1           

00007FF7826B37F4 DEC        DIL                

00007FF7826B37F7 ADD        RSI, 0xcbb2bb0e    

00007FF7826B37FE LEA        R8, [R8*4-0x13d9137e]

00007FF7826B3806 XOR        DIL, 0x10          

00007FF7826B380A BTS        AX, AX             

00007FF7826B380E MOV        ECX, ESI           

00007FF7826B3810 SBB        DIL, 0x90          

00007FF7826B3814 NOT        R8B                

00007FF7826B3817 BTS        R8W, 0x18          

00007FF7826B381D MOVZX      R10D, AL           

00007FF7826B3821 XOR        R11B, DIL          

00007FF7826B3824 ROR        AL, CL             

00007FF7826B3826 OR         CX, CX             

00007FF7826B3829 BSWAP      R8                 

00007FF7826B382C ADC        RDI, RSP           

00007FF7826B382F DEC        ECX                

00007FF7826B3831 DEC        SI                 

00007FF7826B3834 SHL        RCX, CL            

00007FF7826B3837 MOV        RDX, [RDI+R10-0x68]

00007FF7826B383C ROR        RSI, 0xa0          

00007FF7826B3840 BTS        SI, CX             

00007FF7826B3844 MOVSX      EDI, AX            

00007FF7826B3847 MOV        [R10+RBX-0x70], RDX

00007FF7826B384C XCHG       R10, R8            

00007FF7826B384F MOV        ESI, [R8+RBP-0x6d] 

00007FF7826B3854 XOR        ESI, R11D          

00007FF7826B3857 CQO                           

00007FF7826B3859 XADD       R10W, DI           

00007FF7826B385E JS         0x7ff78214929b     

 

上面这段经过简单少数几个花型简化当时已经满足我们要求其中我们并不关心handler如何解码偏移值解码opcode那些我们关心00007FF7826B3837 MOV        RDX, [RDI+R10-0x68] 由于VMProtect所以Context里面寄存器肯定不会储存器太远同时因为我们代码执行记录寄存器所以我们这里可以直接解码出RDI+R10-0x68最终算出便宜就是指向Context某个寄存器然后00007FF7826B3847 MOV        [R10+RBX-0x70], RDX

这里也是一样根据

rdi=000000DA82AFF090,r8=C50B364800000000,r9=00007FF7826B37CD,r10=0000000000000068,对应的寄存器那么容易标记出这里的RDI寄存器其实就是地址,R10动态解密后的偏移和-0x68计算得0实际上最终我们可以记录opcode执行记录 mov vdi,[vsp]如法炮制我们可以最终整理所有的opcode执行记录而且每个opcode执行记录我们保存对应地址寄存器然后我们还可以找到opcode地址方法也很简单我们只需要定位出opcode数据大体范围然后关注内存访问找到访问opcode数据访问指令比如00007FF7826B384F MOV        ESI, [R8+RBP-0x6d]  这里同样计算出R8+RBP-0x6d就是opcode对应地址

2.染色还原context偏移

由于代码虚拟机因此代码入口必定进行保存现场操作实际上VMProtect入口代码无论看到执行什么代码简化以后基本都是压栈操作因为保存现场一定是第一时间保存否则破坏现场那么我们可以利用这个特点使用unicorn或者bochs这些模拟执行这些入口代码寄存器染成特殊值比如

 

 

这样我们就能拿到寄存器初始布局然后后续位置追踪需要下一步还原opcode伪代码执行记录根据代码具体行为追踪这些寄存器偏移主要是VMProtect主动调整以及vsp变化引起偏移相对变化这个过程有点代码工作但是逻辑并不复杂

3.匹配Hanlder特征还原opcode代码执行记录

 

通过第二处理我们有了简化以后代码执行记录如果花型替换足够等价那么简化代码执行效果原始执行效果等价因此我们开始进行特征匹配处理这里同样使用花型匹配脚本比如

 

 

这是一个很久以前一个老版本VMProtect特征匹配部分脚本不过这种处理方式通用新版本处理流程也是一样因为前面说过VMProtect的handler是要精心设计所以不会经常变化通过上面精简代码配合hander匹配脚本匹配出hander,然后结合我们追踪寄存器偏移我们就能还原出完整的opcode执行记录形成一个类似js压缩的map信息,opcode执行记录其实上面代码执行记录没啥区别只是由于VMProtect自己opcode我们伪码表达所以分析伪码就是我们所有通解过程中比较耗时一个地方在opcode不算多而且VMProtect源代码泄露了同时VMProtect里面也是明文跨版本程序基本通用所以一次性工作

 

4.简化opcode

拿到opcode执行记录以后这里理论上我们还可以如同上面简化代码执行记录一样opcode执行记录进行花型替换简化但是实际上至少老版本VMProtect这里混淆严重只有几个逻辑指令跳转之内特殊照顾因为这里进行opcode混淆膨胀最终代码执行性能影响非常大需要平衡不可能过于复杂而且实际上这里混淆意义也不是很大对于我们分析就是多加一个花型替换机制而已由于没有实际需求所以这里并未实现具体花型替换

 

5.带opcode伪代码调试确定干预意图

这一步实际上我们就已经能够明确分析原始代码逻辑但是由于分析一个程序动态分析静态分析太多因此我们还可以进一步开发一个调试器插件然后实现类似前端开发根据map断点执行功能具体代码可以搬出我们第一步实现JIT转译代码虚拟机因为我们代码虚拟机可以控制整个流程因此我们可以监控所有代码内存访问那么我们只需要监控对opcode访问就能在opcode地址断点也能判断opcode指令开始结束长度以及执行记录寄存器里面固定常量然后就能实现单步opcode,断点等等各种调试功能这里会有不少代码之前实现一版非常的ollydbg插件但是ollydbg不支持64x64dbg插件接口鸡肋实现一个没啥UI拙劣所以这里不展示

6.带opcode伪代码调试确定干预意图

这一步实际上才是我们最终目的真正干活地方根据不同目的需要做出不同分析确定需要干扰代码执行流程地方我们可以确定出最终干扰时机特征比如哪个opcode执行地址什么寄存器或者opcode执行几次等等然后确定目标以后我们可以进入具体干预实现

 

7.接管代码执行进行干预

确定代码干预目标以后我们可以开始实现代码干预这里仍然首推使用我们之前用到JIT转译代码虚拟机根据目标我们可以虚拟机入口通过内存访问异常或者hook或者其他手段接管代码执行流程进入JIT代码虚拟机模拟执行代码但是这里执行我们可以根据效率需求关键地方编译patch代码或者patch函数调用其他地方仅仅转译性能非常高比如可以跳过函数调用监控模块内代码执行然后根据设定触发条件目标地址执行动作从而改变代码执行流程或者数据这种方式可以实现侵入patch,所以不用管VMProtect代码校验这些

8.重编译opcode到汇编(实现代码还原)

一般实现了patch我们已经达到目的基于目前软件逆向想不出需要还原VMProtect代码必要不过这里给出一个思路前面我们已经拿到了opcode执行记录那么我们可以写一个编译器将opcode重新编译成x86/arm指令从而实现逻辑等同代码还原类似llvm的ir到code或者tcg做的那样

 

好久好久文章不太会写本来打算匹配替换代码整理开源出来的一看代码好多东西一坨俗称只能作罢看看后面兴趣时候能不能AI帮忙解耦一下再说这种脚本性质汇编匹配替换很多工作还是方便比如特征匹配上面花型替换可以简化代码可以过来实现代码混淆多弄点复杂方向替换花型可以代码快速膨胀还能实现VMProtect具备代码混淆因为这个匹配替换引擎上下还可以LUA脚本




[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

最后于 1天前 被SpringB编辑 ,原因:
收藏
免费 2
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回