Themida 1.9.1.x CISC Processor VM简单分析

by softworm

___________________________________________________________________

 

这篇文章不是VM的完整分析,我没有看完,看的也不是Themida自己,是个用Themida保护的软件,名字就不说了。第一次看CISC指令集的VM,肯定有理解上的错误,仅供参考J

在加壳的时候,VM处理器有4个选项,我以前写过的Themida1800 Demo虚拟机分析,使用的应该是RISC-128。下面是文档中对VM Processor的描述:

 

Processor Type:

 

Mutable CISC Processor: This processor is based in CISC technology, where the size of each instruction is different. Each generated CISC processor will be totally different and unique (mutable) for each protected application to avoid a general attack over the embedded virtual machine. CISC processors run faster and with smaller size than RISC processors, though the complexity (security) level is bigger in RISC processors.  

 

Mutable CISC-2 Processor (New): This processor is based in CISC technology, where the size of each instruction is different. Each generated CISC-2 processor will be totally different and unique (mutable) for each protected application to avoid a general attack over the embedded virtual machine. CISC-2 processors have a similar design than the above CISC processor but the internal microinstructions are more complex, this requires a bigger size in the generated virtual opcodes, producing a bigger final application but with higher security level than CISC processors. Notice that if you insert many VM / CodeReplace macros, this processor can produce a bigger size in your application than RISC processors.  

 

Mutable RISC-64 Processor: This processor is based in RISC technology, where the size of each instruction is equal to 64 bits. Each generated RISC-64 processor will be totally different and unique (mutable) for each protected application to avoid a general attack over the embedded virtual machine. The RISC-64 processor is a complex processor with higher security than CISC processors but the size and execution speed are not as optimum as for CISC processors.  

 

Mutable RISC-128 Processor: This processor is based in RISC technology, where the size of each instruction is equal to 128 bits. Each generated RISC-128 processor will be totally different and unique (mutable) for each protected application to avoid a general attack over the embedded virtual machine. The RISC-128 processor offers higher complexity level than CISC and RISC-64 processors, but the execution performance is lower.  

 

从文档看,使用RISC指令集的保护更强。Processor Type可分为2,CISC/CISC-2RISC-64/RISC-128。从文档中看不出CISCCISC-2在细节上的区别,我也没有自己加壳去仔细验证,即不清楚这里用的究竟是CISC还是CISC-2。用Themida1.9.1.1分别选择4种类型加壳测试了一下,RISCCISC是可以区分的:

 

使用RISC processor,VM相关部分在动态分配的6块内存中,这也是实际使用得较多的,脱壳后需要补6个区段(如果不还原代码)。使用CISC processor,VMcontexthandler就在壳代码所在区段,脱壳后不需要补区段。VM引擎与原程序共用1个堆栈。

 

值得注意的是,我读了一遍scherzo写的<Inside Code Virtualizer>,文章截图中的入口代码与我看到的极其相似,猜测CV使用的可能就是CISC指令集的某一种。

VM入口代码___________________________________________________

原程序以1JMP开始执行VM保护代码。

进入VM,pushimm32PCODE数据的地址,同时被用作PCODE数据解码KEY

 

这里的代码与scherzo文章中截图完全相同。

ebx为进入VMpushdword,esi指向PCODE数据。

 

下面执行取指令代码,这部分代码不在handler地址表内,也是变形代码,esi下面为清理后的结果。

解码opcode,跳到对应的handler。注意进入VMpushdword,又被用作解码key。所有的handler(除了退出VM),在执行完后都会跳到这里继续取指循环。xor用的imm8(也许包括解码算法)是加壳时随机生成的。

 

RISC相似,VM保护的代码也被call及非模仿指令分成若干段。但进入VM时的代码,不象RISC指令集那样表现为连续的PUSH/JMP,1PUSH/JMP与其它的是分开的。

这是第1PUSH/JMP,上面就是这段代码的PCODE数据。向上翻:

直到PCODE数据结束,出现PUSH/JMP,这些PUSH/JMP与第1个是同属一段VM保护代码的。如果该段代码内没有call或非模仿指令,则只有第1PUSH/JMP

 

VM_Context__________________________________________________

 

00000000 VMContext       struc ; (sizeof=0x44)

00000000 ecx                dd ?

00000004 eax                dd ?

00000008 edx                dd ?

0000000C edi                 dd ?

00000010 ebx                dd ?             ; 这不是esp,而是ebx,vm不切换栈,不必保留esp

00000014 esi                 dd ?

00000018 ebp                dd ?

0000001C eflag              dd ?

00000020 JxxFlag           dd ?              ; 是否执行控制转移的标记

00000024 counter           dd ?              ; 模仿控制转移指令时使用

00000028 IndexOfEcxInCtx dd ?              ; ecxctx内的index,在模仿jcxz/jecxz时使用

0000002C DeltaOffset       dd ?

00000030 Busy              dd ?

00000034 field_34          dd ?

00000038 field_38          dd ?

0000003C RellocOffset      dd ?              ; 处理重定位数据的offset

00000040 field_40          dd ?

00000044 VMContext       ends

有几个field不清楚含义,还原代码时没有碰上,这篇文章不是完整分析J

VMConext下就是handler,所有Context成员,加上handler,1byte即可寻址,为避免混淆,opcode的编码没有从0开始,而是直接从0x11开始的。handler表内的数据已经用清理变形代码后的代码地址替换了,注释是原代码地址。

 

进入VM时保存执行环境,退出VM时恢复各寄存器。这些代码都放到VM内执行了。

 

OpcodePCODE数据____________________________________________

 

这个VM的结构与以前看过的RISC指令集VM相比,的确简单很多,绝大部分handler一目了然,清理后只有几行,不需要进行分类。所有的操作都通过stack实现,这给分析PCODE带来了一些麻烦,尤其是操作数在栈上的时候,后面再讲。

所有的Opcode只有1字节。每个handler可以有0,1,2,4字节的操作数,如果有操作数,需要对operand解码,如下面的handler解码1dword并压栈。

进入VM,有几个寄存器是有特殊含义的:

handler没有使用ebp(变形代码会PUSH/POP保护后使用)handler实际使用了3个寄存器,为避免混淆,另外命名,可以认为这3个寄存器是VM内部的寄存器。

下面以进入VM时保存context为例,其中从栈上取的数据是进入VM时的pushad/pushfd压入的。列出的PCODE解码结果,格式为:

00000   113D9802  079871D7  33  PUSH32      addr_ctx.eflag

00001   113D9804  079871DD  93  POP32       R2    ; R2<-contexteflag的地址

00002   113D9805  0798714E  1F  POP32       [R2]    ; 弹出栈上的eflag保存

00003   113D9806  07987151  33  PUSH32      addr_ctx.edi

00004   113D9808  0798715F  93  POP32       R2

00005   113D9809  079871CC  1F  POP32       [R2]   ; 弹出栈上的edi保存

00006   113D980A  079871D3  33  PUSH32      addr_ctx.esi

00007   113D980C  079871DB  93  POP32       R2

00008   113D980D  07987148  1F  POP32       [R2]   ; 弹出栈上的esi保存

00009   113D980E  07987157  33  PUSH32      addr_ctx.ebp

00010   113D9810  0798715E  93  POP32       R2

00011   113D9811  079871CD  1F  POP32       [R2]  ; 弹出栈上的ebp保存

00012   113D9812  079871D2  33  PUSH32      addr_ctx.ebx

00013   113D9814  079871DD  93  POP32       R2

00014   113D9815  0798714E  1F  POP32       [R2]    ; 弹出栈上的esp保存

00015   113D9816  07987151  AE  SetEcxIdx   00     ; 设置contextIndexOfEcxInCtx,ecx

                                                                                        ; ctx内第1dword

00016   113D9818  079871FF  33  PUSH32      addr_ctx.ebx

00017   113D981A  079871C8  93  POP32       R2

00018   113D981B  0798715B  1F  POP32       [R2]  ; 弹出栈上的ebx保存,注意与上面保存esp使

;ctx同一个field,即丢弃了esp,因为vm

; 与原程序共用栈,不需要保存esp

00019   113D981C  07987144  33  PUSH32      addr_ctx.edx

00020   113D981E  07987175  93  POP32       R2

00021   113D981F  079871E6  1F  POP32       [R2]  ; 弹出栈上的edx保存

00022   113D9820  079871F9  33  PUSH32      addr_ctx.ecx

00023   113D9822  079871CA  93  POP32       R2

00024   113D9823  07987159  1F  POP32       [R2]  ; 弹出栈上的ecx保存

00025   113D9824  07987146  33  PUSH32      addr_ctx.eax

00026   113D9826  07987174  93  POP32       R2

00027   113D9827  079871E7  1F  POP32       [R2] ; 弹出栈上的eax保存

00028   113D9828  079871F8  11  MOV32       R2,esp

00029   113D9829  079871E9  AD  PUSH32      R2

00030   113D982A  07987144  1E  PUSH32      00000004

00031   113D982F  0798715E  66  ADD32       [esp+4],[esp](丢弃src)

00032   113D9830  07987138  26  POP32       esp  ; add esp,4 丢弃进入vmpushdword

00033   113D9831  0798711E  7D  ClearKey                 ; 解码key(ebx)0

 

大部分OPCODE根据操作数的size分为3,8/16/32位。由于操作需要通过栈实现,对于8/16位操作数,有时候需要将操作数零扩展压栈。

 

前面已经提到了Vm_Push32_Imm32,下面看看8位和16位操作。

PUSH 8位立即数。

PUSH 16位立即数。

OPCODE中复杂一点的是控制转移指令,这个handler与我在<Themida1800 Demo VM分析>中的完全相同,就不啰嗦了。不同的是Jxx是由2handler实现的:

.

2句才真正将PCODE指针置到dst

 

PCODE变形____________________________________________________

 

这个是以前没有看到过的,PCODE也是变形的。不知道下一步Themida会不会把VM保护的原程序代码也变形J

估计是这个选项造成的。

 

下面举几个例子:

明显的垃圾J

4句等于mov R2,edi。有了最后一句,4句是垃圾。

这个要复杂一些。前6行等于push ebx,push ebp,看起来似乎是sub ebx,ebp,但最后一句的dst却是R2。这种代码一般后面还跟着一个ADD/SUB/XOR… 指令,真正的操作数在前面压栈了。这几行全是垃圾。如果真正是sub ebx,ebp,最后不会是pop R2,而应该是这样的代码:

即将结果写入回目的操作数。

 

PCODE变形是递归生成的,各种变形模式可以相互嵌套,需要清理,否则难以理解。

 

由于VM通过栈来实现操作,VM又与原程序共用一个栈,有的代码看起来比较复杂,特别是操作数本身就在栈上的时候。下面举一个例子。

注意counter的值,这已经是清理过的PCODE,原始的有107行。这段代码实现的是

xchg ebx,[esp],AntiDump相关代码,原始代码就是变形代码。

 

 

全部代码等于PUSH EDX,由此可见Themida VM的性能损耗有多大J

 

Anti-Debug_________________________________________________

 

我自己原来改过的OllyDbgWinXP SP2下会被检测到,所以看了一下这部分代码,下面是修复的代码(你的OD不一定是这里过不去J)

 

      or                  eax,eax             ; IsDebuggerPresent的返回

       jnz                l_113D98C9

       cmp              dword ptr [ebp+07922CB1],00000000 ; <- 这里为1,被发现了

       jz                  l_113D9932

 

l_113D98C9:

       lea                edi,[ebp+0792501D]

       mov              eax,00000001

       jmp                      edi

      

l_113D9932:

       xor               [ebp+07920ABD],eax

       add               eax,[ebp+07921585]

       add               [ebp+0792219D],eax

       xor               eax,ebx

       ……省略

      

       ; 下面是将该dword1的代码,call分为9

      

       ; 1

 

       mov              eax,eax

       cmp              dword ptr [ebp+0792299D],0

       jnz              loc_1

 

       cmp              dword ptr [ebp+07920499],0

       jz                  loc_2

 

loc_1:

       push             eax

       push             ebx

       mov              eax,eax

       mov              eax,000004D1

       mov              [ebp+0792309D],eax

       lea                ebx,[ebp+0794FBCC]

       call                ebx

 

       ; 2

 

       pop               ebx

       pop               eax

 

loc_2:

       cmp              dword ptr [ebp+07920499],0

       jz                  loc_3 ;

 

       push             eax

       push             ebx

       mov              eax,000004D1

       mov              [ebp+0792309D],eax

       lea                ebx,[ebp+0794F8BB]

       call                ebx

 

       ; 3

 

       pop               ebx

       pop               eax

 

loc_3:

       mov              eax,eax

       cmp              dword ptr [ebp+0792340D],00000001

       jnz                loc_4 ; 跳到loc_4OK

 

       cmp              dword ptr [ebp+07920621],00000000

       jnz                loc_4

 

       cmp              dword ptr [ebp+07921F75],00000000

       jnz                loc_4

 

       mov              byte ptr [ebp+07920325],49

       push             8C1529E9

       push             dword ptr [ebp+079228D1]

       lea                eax,[ebp+07923BFA]

       call                eax        ; 调用homemade_GetProcAddress获取IsBadReadPtr

 

       ; 4

 

       mov              [ebp+07981030],eax

       mov              eax,fs:[00000030]

       mov              eax,[eax+0000000C]

 

       mov              ecx,00000010

       and               eax,FFFFF000

       add               eax,00001000          ; 从下一页开始

       jmp                      loc_5

 

loc_8:

       push             eax

       push             ecx

       push             00000004                ; 4 bytes

       push             eax                           ; _PEB_LDR_DATA下一页起始地址

       call                [ebp+07981030]     ; IsBadReadPtr

 

       ; 5

 

       mov              ebx,eax

       pop               ecx

       pop               eax

       or                  ebx,ebx

       jz                  loc_6                        ; 可读?

       jmp                      loc_7                        ; 开始检查

 

loc_6:

       add               eax,00001000        ; 到下1

       dec               ecx                           ; 16(每页的前4 bytes)

 

loc_5:

       or                  ecx,ecx

       jnz                loc_8

       jmp                      loc_4                          ; 16页均可读,则跳过下面的anti;-)

 

loc_7:

       sub               eax,00000010        ; 退到最后1个可读页(的最后16 bytes)

       push             eax

       push             00000010

       push             eax

       call                [ebp+07981030]     ; 16 bytes是否可读?

 

       ; 6

 

       mov              ebx,eax

       pop               eax

       or                  ebx,ebx

       jnz                loc_4                        ; 不能读则跳过

 

       ; 4DWORD全为FEEEFEEE则设置标记

 

       cmp              dword ptr [eax],FEEEFEEE

       jnz                loc_4

       cmp              dword ptr [eax+00000004],FEEEFEEE

       jnz                loc_4

       cmp              dword ptr [eax+00000008],FEEEFEEE

       jnz                loc_4

       cmp              dword ptr [eax+0000000C],FEEEFEEE

       jnz                loc_4

 

       mov              eax,eax

       cmp              dword ptr [ebp+0792299D],00000000

       jnz                loc_8

 

       cmp              dword ptr [ebp+07920499],00000000

       jz                  loc_9

 

loc_8:

       push             eax

       push             ebx

       mov              eax,eax

       mov              eax,000004D1

       lea                ebx,[ebp+0794F939]

       call                ebx

 

       ; 7

 

       pop               ebx

       pop               eax

       mov              eax,eax

 

loc_9:

       mov              dword ptr [ebp+07922CB1],00000001   ; debugger detected

 

 

loc_4:

       mov              eax,eax

       cmp              dword ptr [ebp+0792299D],00000000

       jnz                loc_10

 

       cmp              dword ptr [ebp+07920499],00000000

       jz                  loc_11

 

loc_10:

       push             eax

       push             ebx

       mov              eax,eax

       mov              eax,000004D1

       mov              [ebp+07920651],eax

       lea                ebx,[ebp+0794FC33]

       call                ebx

 

       ; 8

 

       pop               ebx

       pop               eax

 

loc_11:

       cmp              dword ptr [ebp+07920499],00000000

       jz                  loc_12

 

       push             eax

       push             ebx

       mov              eax,000004D1

       mov              [ebp+07920651],eax

       lea                eax,[ebp+0794F8D4]

       call                ebx

 

       ; 9

 

       pop               ebx

       pop               eax

 

loc_12:

       mov              eax,eax

       mov              eax,[ebp+0798102C]

       xor               eax,35F9E374

       add               eax,ebp

       jmp                      eax

这样的检测,我的OllyDbg本来应该能通过的,问题在WinXP SP自己。SP2为防止溢出攻击,PEB的地址作了随机化处理,如果用来隐藏OllyDbg的代码使用了7FFDF000的硬编码地址,就会对错误的地址进行数据清理。

 

如果清理代码是注入的,直接从fs:[30h],不会有问题。如果是在被调试进程之外用WriteProcessMemory实现的,需要用GetThreadSelectorEntry获取fs段的地址。

 

Virtual Machine Anti-Dump______________________________________

 

这里的讨论不适用于1.8.x.x。在写这篇文章的时候,Themida升级到1.9.4.0,专门提到增强了Anti-Dump,所以估计也对付不了1940了。

 

原程序中一段被VM保护的代码,开始执行时首先就是AntiDump。我看的这个程序,用于AntiDumpPCODE9500行。AntiDump原始代码就是变形代码。

 

脱壳后运行,出现内存访问异常,异常出现在VM,试图访问8D8C0的数据。下面是部分还原代码:

 

      ……

       push      dword ptr [ebp+0792090D]  ; [11372F38]= 8D8C0h

 

       push      ebp

 

       push      ebx

       mov       ebx,501B7E96

       mov       ebp,C605CE17

       add        ebp,ebx

       pop        ebx

 

       add        [esp+4],ebp

       pop        ebp

 

       mov       eax,[esp]

       add        esp,00000004

       sub        eax,16214CAD

 

       push      00001901

       mov       [esp],ecx

 

       push      esi

       mov       esi,0A09435A           ;  Magic Number为硬编码值

       sub        esi,C18ED54D          ;  487a6e0d

       mov       ecx,esi

       pop        esi

 

       push      esi

       mov       esi,ecx

       mov       edx,esi

       pop        esi

       pop        ecx

 

       cmp       [eax],edx       ; Anti-Dump

       jz        l_1142D17E      

 

这里eax8D8C0,这里的dword必须等于硬编码值487A6E0D。这是壳代码分配的内存,dump出来后自然没有。这个地址保存在壳代码区段内:

知道为什么就简单了J

 

还出现了另外一种。我看的是个DLL,却检测了宿主EXEMZ头。至于EXE文件是否存在这种AntiDump,我没有验证。

 

这段代码直接看了一下,没有还原代码了。从11374BA8获取EXE的加载地址。

检测MZ+0Chword

 

07311   1144CAA8  00000000  15  PUSH16      [R2]

07328   1144CABF  000000DC  A2  PUSH16      0600

07339   1144CACF  0000008F  4F  CMP16       [esp+2],[esp](丢弃16src,dst,eflag压栈)

07358   1144CAE9  000034E8  79  POP32       [addr_ctx.eflag]

07360   1144CAEC  00000000  3E  JZ

07361   1144CAF1  00000C3F  87  SetDst      1144CE3B  ; Anti-Dump

 

MZ+0Ch应为600h。正常的文件这里为FFFF

AntiDump我就碰到这2,不清楚还有没有别的。

 

ThemidaCISC指令集VM相比之下比较简单,实战中也出现较少,倒是可以用来学学VMJ

 

 

致谢_________________________________________________________

 

www.pediy.com

www.unpack.cn