Code Virtualizer逆向工程浅析

☆ 背景介绍

“Code Virtualizer”的资料不多,可能与它不如VMP被广泛使用有关,OSForensics 9用了CV。若非现实世界有实用软件用CV保护,鬼才有兴趣对之进行逆向工程。之前没有接触过CV,用TTD调试OSF时被绕得七荤八素,后来无意中确认OSF用CV保护。上网搜了些CV资料,都比较老,适用于1.3.8或更早期版本,与OSF所用CV版本差别较大。还有一点,老资料出现在32位时代,现在是64位时代。

CV将CFG扁平化,实际上没有调用栈回溯一说。CV处处是间接转移,主要是jmp寄存器这种形式,其次是将目标地址压入栈中,靠ret转移,这样一来,IDA中几乎没有显式交叉引用。敏感字符串是可以混淆存放的,这条路也断了。

本文分享一些CV逆向工程经验,基于网上能公开下到的CV 2.x。OSF所用CV是何版本,我不知道,但实测发现本文的经验大多也适用于OSF逆向工程。我只关心C语言,其他语言不在本文范畴。

☆ Code Virtualizer 2.2.2.0

1) CV SDK

公开能下到的只有CV 2.x,以此为研究对象。压缩包展开后主要关注这些文件和目录

$ tree /F /A

X:\path\CodeVirtualizer
| Code Virtualizer Help.chm // 帮助文件,组织得不好
| CVLicenseA1.dat // License
| Virtualizer.exe // 负责CV虚拟化的主程序
| Virtualizer.ini // CV虚拟化时此文件可以指定LastSectionName
|
+---custom_vms
| |
| \---public
| eagle32_black.vm // 可以看看,但不要修改,CV虚拟机配置
| shark64_black.vm
|
+---Examples
| +---C
| | +---VC // 缺省VC示例,Visual Studio 2019适当修改后可编译
|
+---Include
| +---C
| | | Readme.txt
| | | VirtualizerSDK.h // 只需要包含这一个头文件
| | | VirtualizerSDK_CustomVMs.h
| | | VirtualizerSDK_CustomVMs_VC_inline.h
| | | VirtualizerSDK_VC_inline.h
|
+---Lib
| | VirtualizerSDK32.dll
| | VirtualizerSDK64.dll // cvtest.exe运行时需要,cvtest_p.exe不需要
| |
| +---COFF
| | VirtualizerSDK32.lib
| | VirtualizerSDK64.lib // 链接时需要
|
+---scz // 自己瞎建的测试目录
| cvtest.c // 源代码
| cvtest.cv // Virtualizer.exe保存的项目文件,可以再次加载
| cvtest.exe // 原始PE
| cvtest_p.exe // 用CV虚拟机保护过的PE

2) cvtest.c

没有实际意义,只是示例,要点如下

————————————————————————–
#include "VirtualizerSDK.h"
/*
* 链接时需要,下面那对宏会转换成对该库中函数的调用,用以占坑
*/
#pragma comment( lib, "VirtualizerSDK64.lib" )
/*
* 将需要保护的代码片段置于这对宏中间
*/
VIRTUALIZER_EAGLE_BLACK_START
/*
* 被保护代码片段位于两个宏中间
*/
...
VIRTUALIZER_EAGLE_BLACK_END
————————————————————————–

“EAGLE_BLACK”是CV虚拟机的一种,看上去保护强度最高。SDK自带的vc_example用的是”TIGER_WHITE”,保护强度很低。

3) 编译

做此类测试时,对IDE无感,我用命令行编译环境

"x64 Native Tools Command Prompt for VS 2019"

cl cvtest.c /I "X:\path\CodeVirtualizer\Include\C" /Fecvtest.exe /nologo /Od /GS /guard:cf /W3 /WX /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /MD /link /LIBPATH:"X:\path\CodeVirtualizer\Lib\COFF" /RELEASE

“/Od”表示不优化,但实测发现指定后仍然有优化,无法达到汇编级所见即所得。为什么不用nasm、ml64之类的方案?懒得折腾呗。

copy /y "X:\path\CodeVirtualizer\Lib\VirtualizerSDK64.dll" "X:\path\CodeVirtualizer\scz"

复制VirtualizerSDK64.dll到cvtest.exe所在目录,可以直接执行cvtest.exe。

禁用指定PE的ASLR

editbin.exe /dynamicbase:no cvtest.exe

4) CV虚拟化

执行Virtualizer.exe

————————————————————————–
New
 Options
  Application Information
   Application
    cvtest // 任意
   Input Filename
    X:\path\CodeVirtualizer\scz\cvtest.exe
   Output Filename
    X:\path\CodeVirtualizer\scz\cvtest_p.exe
   Same as input
    清空 (缺省选中)
  Virtual Machines
   EAGLE64 (Black)
    此处不需要选,直观体现CV虚拟机保护强度
  Protection Macros
   EAGLE64 (Black)
    此处不需要选,直观体现VIRTUALIZER_EAGLE_BLACK_START宏
    可以看到保护前的汇编代码
   EAGLE64 (Black)
    有2个,表示源码中用了2次VIRTUALIZER_EAGLE_BLACK_START宏
  Extra Options
   Location of Protection Code
    Insert a new section
     可定制名字,OSForensics 9在PE中插入名为".vlizer"的section
   Virtualize String
    Ansi+Unicode Strings
   Strip Relocations (EXEs)
    选中
————————————————————————–
Save
Save as
 X:\path\CodeVirtualizer\scz\cvtest.cv
  将前面Options的内容保存到项目文件中
Open
 加载之前保存过的CV项目文件
————————————————————————–
Protect
实际进行CV虚拟化,从cvtest.exe生成cvtest_p.exe
————————————————————————–

Virtualizer.ini内容如下

————————————————————————–
[General]

LastSectionName = .scz
————————————————————————–

缺省LastSectionName可能是其他值,比如”.vlizer”。cvtest_p.exe有2个”.scz”。

同一个cvtest.exe,每次CV虚拟化生成的cvtest_p.exe都不一样。

前面简介了CV SDK的使用,最好是自己整一个cvtest.c,生成cvtest_p.exe,对后者进行逆向工程,积累经验后再去对付现实世界的例子,比如OSF。

☆ TTD

CV虚拟化本身不会增加反调试检查,调试cvtest_p.exe时不需要反”反调试”。OSF有
反调试,但OSF没有考虑到TTD技术的出现,其反调试措施没有针对TTD录制。

即便不考虑反”反调试”,对CV保护过的代码进行逆向工程时,条件允许的情况下,强
烈建议TTD录制。若对CV有过经验积累,再动用TTD,能极大地抵消CV保护。

☆ CV虚拟机框架概览

1) 从cv_entry[0]到cv_entry[4]

VIRTUALIZER_EAGLE_BLACK_START那一对宏在编译后化身为两个call

————————————————————————–
/*
* VIRTUALIZER_EAGLE_BLACK_START
*/
000000014000108F FF 15 03 10 00 00 call cs:VirtualizerSDK64_151
/*
* 被保护代码片段位于两个call中间
*/
...
/*
* VIRTUALIZER_EAGLE_BLACK_END
*/
0000000140001152 FF 15 48 0F 00 00 call cs:VirtualizerSDK64_551
————————————————————————–

这是cvtest.exe中的效果,cvtest.exe只是从C编译成PE,尚未进行CV虚拟化处理。
151、551这种数字无关紧要,要点是它们成对出现。

Virtualizer.exe靠这两个call识别出待保护代码片段,对之CV虚拟化,将11KB的
cvtest.exe膨胀成1649KB的cvtest_p.exe,这是加了多少垃圾代码?

CV虚拟化时将”call VirtualizerSDK64_151″就地转成jmp,这是cvtest_p.exe中的效

————————————————————————–
/*
* VIRTUALIZER_EAGLE_BLACK_START
*
* 为叙述方便,此处定义成cv_entry[0]
*/
000000014000108F E9 32 BB 19 00 jmp cv_entry_1_14019CBC6
/*
* 中间的字节流是啥我也不知道,反正不是原来的代码
*/
...
/*
* VIRTUALIZER_EAGLE_BLACK_END
*
* 将"call VirtualizerSDK64_551"就地转成类似nop的填充指令,模式不固定
*
* 为叙述方便,此处定义成cv_exit[0]
*/
0000000140001152 88 C9 mov cl, cl
0000000140001154 88 C9 mov cl, cl
0000000140001156 88 C9 mov cl, cl
————————————————————————–

cv_entry[0]还在.text中,但jmp的目标地址cv_entry[1]已离开.text,进入.scz。

————————————————————————–
000000014019CBC6 cv_entry_1_14019CBC6
/*
* 为叙述方便,此处定义成cv_entry[1]
*/
000000014019CBC6 9C pushfq
...
/*
* 从pushfq到jmp,无任何分支转移指令,二者就是块首、块尾
*
* 为叙述方便,此处定义成cv_entry[2]
*/
000000014019CD1C E9 18 DD FE FF jmp cv_entry_3_14018AA39
————————————————————————–

cv_entry[1]的特点是pushfq,cv_entry[1]、cv_entry[2]之间无任何分支转移指令,
二者就是块首、块尾,在IDA中用图形模式查看,非常明显,这是第二个特点。

————————————————————————–
/*
* 位于第一个".scz"中,Ctrl-S确认
*/
000000014018AA39 cv_entry_3_14018AA39
/*
* 用到自定位技巧,shellcode常用套路
*
* 为叙述方便,此处定义成cv_entry[3]
*/
000000014018AA39 E8 00 00 00 00 call $+5
...
/*
* 为叙述方便,此处定义成cv_entry[4]
*/
000000014018BE28 FF 20 jmp qword ptr [rax]
————————————————————————–

cv_entry[3]的特点是”call $+5″,一种自定位技巧,shellcode常用套路。在IDA中Alt-B搜索字节流”E8 00 00 00 00″,找出所有”call $+5″,基本上都是cv_entry[3]。

cv_entry[4]的特点是”jmp [rax]”。

即使在C代码中只使用了一对VIRTUALIZER_START/VIRTUALIZER_END,cvtest_p.exe仍有可能出现多个cv_entry[3],为什么?因为只要进入CV虚拟机一次,就会有一个cv_entry[3]等着经过,从CV虚拟机中调用外部库函数时,会临时离开CV虚拟机,执行完外部库函数,重新回到CV虚拟机。在这些进出CV虚拟机过程中,自然出现多个cv_entry[3],有些进出流程可能共用一个cv_entry[3],有些可能用自己的cv_entry[3]。

cv_entry_3_14018AA39可以p操作成函数,图形化查看时非常复杂,但把握住前述入口与出口特点,搞几次后就能轻松定位。

IDA可能缺省未将cv_entry[1]与cv_entry[3]识别成函数,我的事后复盘经验是,一定将它们p成函数,以降低静态分析难度,IDA的图形模式只能看函数。

CV虚拟机官方没有cv_entry[0]、cv_entry[4]这些概念,这是为了叙述方便自己给的定义。回顾一下流程框架

————————————————————————–
/*
* cv_entry[0]
*/
jmp cv_entry[1]
————————————————————————–
/*
* cv_entry[1]
*/
pushfq
...
/*
* cv_entry[2]
*/
jmp cv_entry[3]
————————————————————————–
/*
* cv_entry[3]
*/
call $+5
...
/*
* cv_entry[4]
*/
jmp [rax]
————————————————————————–

逻辑上cv_entry[0]在CV虚拟机外,一般在.text中,这个不绝对。之后cv_entry[1]至cv_entry[4]全部在CV虚拟机中,一般在.vlizer中。

2) 定位cv_entry[1]

已知从cv_entry[0]转向cv_entry[1],会从.text转向.scz(本例中的名字),可以用x64dbg调试,对.scz设内存访问断点,以此快速定位cv_entry[1]。这段话假设目标程序比较复杂,现在还在.text中,静态分析一时半会儿找不到cv_entry[0]。若肉眼就能发现cv_entry[0],则无需前述技巧。

定位cv_entry[1]之后,静态分析就能定位定位cv_entry[2]到cv_entry[4]。

3) 在cv_entry[4]处获取关键信息

假设调试器停在cv_entry[4]

rax=000000014018120e rbx=0000000000000360 rcx=0000000140000000
rdx=0000000000180eae rsi=5555555555555555 rdi=6666666666666666
rip=000000014018be28 rsp=000000000014fe08 rbp=00000001400ab9d6
r8=8888888888888888 r9=9999999999999999 r10=aaaaaaaaaaaaaaaa
r11=bbbbbbbbbbbbbbbb r12=cccccccccccccccc r13=dddddddddddddddd
r14=eeeeeeeeeeeeeeee r15=ffffffffffffffff
iopl=0 nv up ei pl nz na pe nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202

在cv_entry[4]处有

rsp 0x14fe08 VM_DATA
rbp 0x1400ab9d6 VM_CONTEXT
rax 0x14018120e &func_array[0x6c]
rbx 0x360 0x360/8=0x6c
rcx 0x140000000 ImageBase
rdx 0x180eae VM_CONTEXT.func_array 0x140180eae

在cv_entry[4]处可以找到VM_CONTEXT,这是CV虚拟机的核心组件之一,后面再说。

dqs @rsp-8 l 0n21
dqs 0x14fe00 l 0n21

00000000`0014fe00 00000000`0000006c
00000000`0014fe08 88888888`88888888 r8 <=rsp
00000000`0014fe10 99999999`99999999 r9
00000000`0014fe18 aaaaaaaa`aaaaaaaa r10
00000000`0014fe20 bbbbbbbb`bbbbbbbb r11
00000000`0014fe28 cccccccc`cccccccc r12
00000000`0014fe30 dddddddd`dddddddd r13
00000000`0014fe38 eeeeeeee`eeeeeeee r14
00000000`0014fe40 ffffffff`ffffffff r15
00000000`0014fe48 66666666`66666666 rdi
00000000`0014fe50 55555555`55555555 rsi
00000000`0014fe58 77777777`77777777 rbp
00000000`0014fe60 22222222`22222222 rbx
00000000`0014fe68 22222222`22222222 rbx_other
00000000`0014fe70 44444444`44444444 rdx
00000000`0014fe78 33333333`33333333 rcx
00000000`0014fe80 11111111`11111111 rax
00000000`0014fe88 00000000`00000202 efl
00000000`0014fe90 00000000`0000006c func_array_index
00000000`0014fe98 00000000`0019a914 dispatch_data 0x14019a914
00000000`0014fea0 00000000`00000001

从cv_entry[0]到cv_entry[4]真正干的大事就是将cv_entry[0]处各寄存器压栈,一堆眼花缭乱的操作都是为了掩盖这个事实。最初我还老老实实在TTD调试中一步步跟,后来意识到它的意图后,采用污点追踪的思想快速定位cv_entry[4]处栈中诸数据。

前文所用术语都是自己瞎写的,结合上下文对得上就成。

4) 定位func_array[]

func_array[]就是老资料里说的handler[],CV虚拟化将每一条位于保护区的汇编指令转换成许多个func_array[i]组合。

在cv_entry[4]处有多种办法定位func_array[],比如

> ? @rcx+@rdx
> ? @rax-qwo(@rsp+0x88)*8
Evaluate expression: 5370285742 = 00000001`40180eae

0x140180eae即func_array[]起始地址。

有个取巧的办法定位func_array[]起始地址。假设已知VM_CONTEXT在0x1400ab9d6,本例中该结构占0x174字节,但该结构大小并不固定,有可能是其他大小。在IDA中查看0x1400ab9d6处hexdump,大片的0,只有一处非零,就是VM_CONTEXT.func_array字段所在,静态查看时该值是重定位前的偏移值,加上基址才是内存地址。

IDA中看func_array[i],是重定位之前的偏移值,加上ImageBase才是函数地址。应在IDA中静态Patch,人工完成重定位,使得IDA分析出更多代码。func_array[]比较大,很可能没有以qword形式展现,一个一个手工加基址Patch不现实,写IDAPython脚本完成。

5) 确定func_array[]元素个数

没有简单办法确定func_array[]元素个数。在IDA中肉眼识别、逐步逼近当然可以,但不够放心,怕不精确。

有个辅助办法,图形化查看cv_entry[4],往低址方向找如下cmp、test指令,还是比较容易定位的。

————————————————————————–
/*
* rax=0x140180eae VM_CONTEXT.func_array+ImageBase
* rdx=0x180eae VM_CONTEXT.func_array
*/
000000014018B9D4 48 39 C2 cmp rdx, rax
000000014018B9D7 0F 84 72 03 00 00 jz loc_14018BD4F
————————————————————————–
/*
* rbx=0x97 func_array[]元素个数
*/
000000014018BABD 48 85 DB test rbx, rbx
000000014018BAC0 0F 84 7D 01 00 00 jz loc_14018BC43
————————————————————————–

找到0x14018B9D4、0x14018BABD这两个地址后,在TTD调试中对之设断点,从cv_entry[3]处正向执行,断点命中时查看寄存器,注释中写了。不一定TTD调试,普通调试就可以,但我一上来就TTD录制了,后面的分析都是在反复鞭尸,更方便。

精确知道func_array[]元素个数后,写IDAPython脚本对之批量qword化、加基址。这还不够,应该对每个func_array[i]加”repeatable FUNCTION comment”,比如这种效果

————————————————————————–
0000000140180EAE 4A BB 0A 40 01 00 00 00 dq offset sub_1400ABB4A ; func_array[0x0]
0000000140180EB6 8E BC 0A 40 01 00 00 00 dq offset sub_1400ABC8E ; func_array[0x1]
0000000140180EBE 54 BE 0A 40 01 00 00 00 dq offset sub_1400ABE54 ; func_array[0x2]
0000000140180EC6 0E C7 0A 40 01 00 00 00 dq offset sub_1400AC70E ; func_array[0x3]
————————————————————————–
00000001400ABB4A ; func_array[0x0]
00000001400ABB4A
00000001400ABB4A sub_1400ABB4A proc
00000001400ABB4A E9 3F E9 01 00 jmp sub_1400CA48E ; func_array[0x0]
00000001400ABB4A sub_1400ABB4A endp
————————————————————————–
00000001400CA48E ; func_array[0x0]
00000001400CA48E
00000001400CA48E sub_1400CA48E proc
00000001400CA48E 9C pushfq
————————————————————————–

CV虚拟机很复杂,给每个func_array[i]自动加注释,有助于聚焦。

EAGLE_BLACK虚拟机比TIGER_WHITE虚拟机复杂得多,func_array[i]只是个幌子。0x1400ABB4A处jmp到0x1400CA48E,后者也不是真正干活的handler,其实是另一个cv_entry[1],后面有另一个cv_entry[2]到cv_entry[4],最终会去找另一个func_array_2[j]。不建议初次接触CV的人一上来就逆EAGLE_BLACK虚拟机,可以拿TIGER_WHITE虚拟机练手。当然,前面我都给出提纲挈领的大框架了,再看EAGLE_BLACK虚拟机,也不是那么难。

6) 推断VM_CONTEXT结构

流程到达cv_entry[4]时,rbp指向VM_CONTEXT结构

db @rbp l 0x174

00000001`400ab9d6 01 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000001`400ab9e6 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000001`400ab9f6 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000001`400aba06 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000001`400aba16 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000001`400aba26 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000001`400aba36 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000001`400aba46 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000001`400aba56 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000001`400aba66 00 00 00 00 00 00 77 77-77 77 77 77 77 77 00 00 ......wwwwwwww..
00000001`400aba76 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000001`400aba86 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000001`400aba96 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000001`400abaa6 00 00 00 00 00 00 00 00-40 01 00 00 00 00 00 00 ........@.......
00000001`400abab6 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000001`400abac6 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000001`400abad6 00 00 00 00 00 00 00 00-00 00 14 a9 19 40 01 00 .............@..
00000001`400abae6 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000001`400abaf6 00 00 00 00 00 00 00 00-00 00 ae 0e 18 40 01 00 .............@..
00000001`400abb06 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000001`400abb16 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000001`400abb26 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000001`400abb36 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000001`400abb46 00 00 00 00 ....

本例中该结构占0x174字节,但该结构大小并不固定,主要是大片的0。流程到达cv_entry[4]时,VM_CONTEXT结构部分成员已初始化,包括

————————————————————————–
#pragma pack(1)
struct VM_CONTEXT
{
 /*
  * 进入CV虚拟机时设1,离开CV虚拟机时设0,逻辑上相当于(实际有出入)
  *
  * pusha
  * mov busy, 1
  * ...
  * mov busy, 0
  * popa
  */
  unsigned int busy; // +0x0 0x1400ab9d6
  ...
  /*
  * 保存cv_entry[0]时的rbp
  */
  unsigned long long orig_rbp; // +0x96 0x1400aba6c
  ...
  /*
  * 0x140000000
  */
  unsigned long long ImageBase; // +0xd5 0x1400abaab
  ...
  /*
  * 0x19a914+0x140000000=0x14019a914
  */
  unsigned char * dispatch_data; // +0x10a 0x1400abae0
  ...
  /*
  * 0x180eae+0x140000000=0x140180eae (func_array)
  */
  unsigned long long func_array; // +0x12a 0x1400abb00
  ...
                  // +0x174 0x1400abb4a
};
#pragma pack()
————————————————————————–

每个CV虚拟机要单独分析VM_CONTEXT结构各成员位置,总是在变,就是为了对抗逆向工程,上面只是一种示例。若非高价值目标,不建议与CV/VMP这类虚拟机搏斗,浪费生命。

可能过去VM_CONTEXT结构总是位于.vlizer起始位置,现在没这经验规律了,不能假设仍然如此,事实上OSF就不服从该规律。此外,VM_CONTEXT结构之后不能假设紧跟
func_array[],应该用VM_CONTEXT.func_array定位。流程到达cv_entry[4]时,VM_CONTEXT.func_array已是重定位后的地址。

7) 从VM_DATA复制数据到VM_CONTEXT

VM_DATA是我给压在栈上的各寄存器布局瞎起的结构名字,便于叙述,不必当真。

在cv_entry[4]处查看VM_DATA

00000000`0014fe08 88888888`88888888 r8 <=rsp
00000000`0014fe10 99999999`99999999 r9
00000000`0014fe18 aaaaaaaa`aaaaaaaa r10
00000000`0014fe20 bbbbbbbb`bbbbbbbb r11
00000000`0014fe28 cccccccc`cccccccc r12
00000000`0014fe30 dddddddd`dddddddd r13
00000000`0014fe38 eeeeeeee`eeeeeeee r14
00000000`0014fe40 ffffffff`ffffffff r15
00000000`0014fe48 66666666`66666666 rdi
00000000`0014fe50 55555555`55555555 rsi
00000000`0014fe58 77777777`77777777 rbp
00000000`0014fe60 22222222`22222222 rbx
00000000`0014fe68 22222222`22222222 rbx_other
00000000`0014fe70 44444444`44444444 rdx
00000000`0014fe78 33333333`33333333 rcx
00000000`0014fe80 11111111`11111111 rax
00000000`0014fe88 00000000`00000202 efl
00000000`0014fe90 00000000`0000006c func_array_index
00000000`0014fe98 00000000`0019a914 dispatch_data

直接对栈中各寄存器值设数据断点

ba r1 /1 @rsp "dqs 0x14fe00 l 0n21;db 0x1400ab9d6 l 0x174"

每次命中时重新设置上述数据断点,依次命中

1400167f5 pop_to_context_n_1400165FC func_array_2[0x5a]
14007d427 pop_to_context_n_14007D27C func_array_2[0x27e]
140079527 pop_to_context_n_14007940A func_array_2[0x266]
...
14001e89d pop_to_context_n_14001E7B3 func_array_2[0x8a]
1400141d7 pop_to_context_n_14001413C func_array_2[0x4f]

用这种办法可以知道VM_CONTEXT.r8的偏移,还可以找到pop_to_context_*,这种handler对应”pop [addr]”。EAGLE_BLACK有多种pop_to_context_*,TIGER_WHITE只有一种,难度相差极大。

8) 定位cv_entry[5]

从栈中弹栈到VM_CONTEXT.efl,是最后一个弹栈动作,至此所有栈中寄存器均被弹入VM_CONTEXT结构相应成员。假设流程到达cv_entry[4],不必费劲地对栈中各寄存器设数据断点,只需要对栈中的efl设数据断点即可。

ba r1 /1 @rsp+0x80 "dqs 0x14fe00 l 0n21;db 0x1400ab9d6 l 0x174"

断点命中时,查看内存中的VM_CONTEXT结构

00000001`400ab9d6 01 00 00 00 cc cc cc cc-cc cc cc cc 00 00 00 00 ................
00000001`400ab9e6 00 00 00 00 00 00 44 44-44 44 44 44 44 44 00 00 ......DDDDDDDD..
00000001`400ab9f6 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 33 ...............3
00000001`400aba06 33 33 33 33 33 33 33 ce-2d c0 54 00 00 00 00 aa 3333333.-.T.....
00000001`400aba16 aa aa aa aa aa aa aa 00-00 00 00 00 00 00 00 00 ................
00000001`400aba26 00 b1 14 5f 3f ed ff ff-ff 00 00 00 00 00 00 00 ..._?...........
00000001`400aba36 00 00 00 00 00 90 fe 14-00 00 00 00 00 00 00 00 ................
00000001`400aba46 00 00 00 00 00 ee ee ee-ee ee ee ee ee 00 00 00 ................
00000001`400aba56 00 00 00 00 00 66 66 66-66 66 66 66 66 88 88 88 .....ffffffff...
00000001`400aba66 88 88 88 88 88 24 77 77-77 77 77 77 77 77 05 50 .....$wwwwwwww.P
00000001`400aba76 42 41 e8 00 00 00 00 00-00 00 00 99 99 99 99 99 BA..............
00000001`400aba86 99 99 99 8d c5 50 05 01-00 00 00 00 00 a7 00 00 .....P..........
00000001`400aba96 00 00 00 00 00 00 70 4b-cd 87 00 00 00 00 00 00 ......pK........
00000001`400abaa6 00 00 00 be 00 00 00 00-40 01 00 00 00 00 00 00 ........@.......
00000001`400abab6 00 00 00 00 00 bb bb bb-bb bb bb bb bb 77 77 77 .............www
00000001`400abac6 77 77 77 77 77 00 00 00-00 00 00 00 00 00 00 00 wwwww...........
00000001`400abad6 00 00 d2 f2 6e ad 18 54-43 07 c4 a9 19 40 01 00 ....n..TC....@..
00000001`400abae6 00 00 55 55 55 55 55 55-55 55 86 05 aa 88 ee ff ..UUUUUUUU......
00000001`400abaf6 ff ff 00 00 00 00 00 00-00 00 ae 0e 18 40 01 00 .............@..
00000001`400abb06 00 00 00 00 00 00 00 00-00 00 00 00 02 02 00 00 ................
00000001`400abb16 00 00 00 00 00 00 00 00-00 00 00 00 11 11 11 11 ................
00000001`400abb26 11 11 11 11 ff ff ff ff-ff ff ff ff dd dd dd dd ................
00000001`400abb36 dd dd dd dd 00 00 00 00-00 00 00 00 22 22 22 22 ............""""
00000001`400abb46 22 22 22 22 """"

由于我采用了污点追踪的思想,肉眼就能识别各寄存器在VM_CONTEXT结构中的偏移,据此可进一步完善VM_CONTEXT结构定义。

cv_entry[5]是个虚概念,只是为了叙述方便。流程到达cv_entry[5]时,VM_CONTEXT中各寄存器已填写完毕。若在TTD调试中,记下断点命中时所在position值,方便回滚。

cv_entry[5]位于func_array_2[j]中,j不固定。func_array_2[j]没有显著特征,无法通过静态分析定位cv_entry[5],只能动态调试定位,这与cv_entry[4]不同。

cv_entry[5]之后的流程才真正对应”被保护代码片段”,之前的流程都是CV虚拟机初始化。若不知道这点,一上来就楞调试,早早陷入CV虚拟机的圈套,很容易失焦。

cv_entry[5]之后也不见得马上对应”被保护代码片段”,某些func_array_2[i]实际对应nop操作,看上去又很复杂,nop操作想插多少有多少,想插哪里插哪里。分析CV虚拟机时,还得动其他脑子。

☆ CV虚拟机逆向工程经验

为叙述方便,本节不区分func_array[i]、func_array_2[j]等,概念上它们地位相当。

1) VM_CONTEXT.dispatch_data

VM_CONTEXT.dispatch_data是个指针,指向IDA中静态可见的数据区域。每个func_array[i]都会从VM_CONTEXT中取dispatch_data指针,再从dispatch_data[]取数据。

dispatch_data[]是一段字节流,没有固定的结构,没有固定的大小。使用它时,从哪个位置取几个字节上来,完全由当前用它的func_array[i]决定,几乎每个func_array[i]使用dispatch_data[]的方式都不一样,这是对抗逆向工程的手段之一。

以mov操作为例,可能wo(dispatch_data+5)是一个16位偏移,加上VM_CONTEXT基址后定位到VM_CONTEXT.rax成员;可能dwo(dispatch_data+0x13)是虚拟化之前的mov指令中的立即数。理论上,找到合适的dispatch_data[i]可以暴破CV虚拟化过的代码。

每个func_array[i]用完当前dispatch_data[]后,会更新VM_CONTEXT.dispatch_data,确切地说,是递增,使之对应func_array[i+1]。

2) 跟踪func_array[i]

前面讲过定位func_array[]起始地址,现在想知道依次执行了哪些func_array[i]。

已知在各个func_array[i]之间转移时,VM_CONTEXT.dispatch_data会递增,对之设数据断点,即可跟踪func_array[i]。前述数据断点命中时,有些CV虚拟机可能位于func_array[i]的最后一条指令,一般是相对转移指令,这是理想情况。OSF所用CV虚拟机更变态,更新VM_CONTEXT.dispatch_data的代码在func_array[i]中部,而不是尾部。

3) VM_CONTEXT.efl

虚拟化前add/sub/xor/cmp/test等指令在虚拟化后都有各自对应的func_array[i]。简单的CV虚拟机可能add指令对应唯一的func_array[i],早期CV可能就这样,现在不是了,多条add指令可能对应不同的func_array[i],防止在逆向工程中一次标定多次使用。好不容易标定某func_array[i]对应add操作,结果下一个add操作不过这个func_array[i],抓狂。

前述这些指令有个共同点,实际执行时会修改efl。CV虚拟化后,它们对应的func_array[i]会修改VM_CONTEXT.efl,可能是这样的片段

————————————————————————–
/*
* r13d=op1
* edi=op2
*/
000000014007DEAD 41 85 FD test r13d, edi
000000014007DEB0 9C pushfq
...
000000014007DF4B 5B pop rbx
...
/*
* rbx=efl
* r15=VM_CONTEXT.efl
*/
000000014007E003 49 89 1F mov [r15], rbx
————————————————————————–

对VM_CONTEXT.efl设数据断点,能加快func_array[i]的标定。

上面是理想情况。EAGLE_BLACK虚拟机比较变态,test指令修改了efl,但当前func_array[i]不会更新VM_CONTEXT.efl,它将efl存到tmp中;然后其他func_array[i]不断搬运tmp,push/pop/mov操作对应的func_array[i]挨个来,无效搬运,很久之后才将源自tmp的数据搬运进VM_CONTEXT.efl。我碰上过test操作与最终更新VM_CONTEXT.efl的操作相差619个func_array[i],中间的全是垃圾操作,目的是让你搞不清发生了什么。OSF所用CV虚拟机更新VM_CONTEXT.efl时没这么变态,但有其他变态之处。

4) VM_CONTEXT.rsp

push/pop操作对应的func_array[i]可能同步更新VM_CONTEXT.rsp,对之设数据断点,能加快标定。

5) deref操作

虚拟化前的指针操作被虚拟化成某种func_array[i],且不唯一。

对于汇编指令”mov rcx,[r15]”,逻辑上相当于”rcx=*(r15)”,这就是一种deref操作,对应某种func_array[i]。

对于”mov edx,[rsp+0x48]”,这种会拆分成”tmp=rsp+0x48″、”edx=*(tmp)”,至少对应两个不同的func_array[i]。

6) 库函数调用

部分情况通过ret进行库函数调用,并不都是。无论如何,从CV虚拟机内部调用外部库函数,都涉及离开、重入CV虚拟机,该过程必然更新VM_CONTEXT.busy字段。

流程到达cv_entry[4]时,rbp指向VM_CONTEXT结构,”db @rbp l 0x174″,有个明显的位置是1,那儿就是VM_CONTEXT.busy字段。

IDA中图形化查看cv_entry[3],第2个block中有一段将busy置1,可在TTD调试中辅助确认。

————————————————————————–
000000014018B314 31 C0 xor eax, eax
/*
* rbp=VM_CONTEXT
* rbx=0 偏移
* ecx=1
*
* 访问VM_CONTEXT.busy
*/
000000014018B316 F0 0F B1 0C 2B lock cmpxchg [rbx+rbp], ecx
000000014018B31B 0F 84 07 00 00 00 jz loc_14018B328
————————————————————————–

定位VM_CONTEXT.busy后,对之设数据断点,找到离开CV虚拟机的func_array[i],再具体分析。

动态调试CV虚拟机时,无法通过库函数入口处的调用栈回溯寻找主调位置,因为不是call过来的,要么ret过来,要么jmp过来。若是ret过来的,在库函数入口处qwo(@rsp-8)应该等于rip,据此识别此类情况。若是jmp过来的,运气好的话,r查看寄存器,应该有某个其他寄存器等于rip。若是”jmp [rax]”这种,除非一个个检查,很难一眼识别出来。即使识别出怎么过来的,也无助于寻找主调位置。若是TTD调试,直接断在库函数入口,然后”t-“就找到主调位置,对付CV,必须上TTD。

假设CV虚拟机通过ret调用外部库函数,在ret指令处,qwo(@rsp)即库函数地址,一般qwo(@rsp+8)是库函数重入CV虚拟机的点,这取决于库函数怎么维持栈平衡并返回。可提前在”重入CV虚拟机的点”设断,很可能是另一个cv_entry[0]或cv_entry[1]。

7) 分析func_array[i]

IDA中将func_array[i]整成函数,图形化查看,快速确定函数入口、出口。

写IDAPython脚本批量处理OSF的func_array[i]时,碰上很多p操作失败的情形,一般是64位操作数前缀所致,比如

————————————————————————–
/*
* 失败情形
*/
000000014C46CABF 49 db 49h
000000014C46CAC0 81 CE 04 00 00 00 or esi, 4
000000014C46CAC6 49 C7 C2 00 04 00 00 mov r10, 400h
————————————————————————–
/*
* 成功情形
*/
000000014C46CABF 49 81 CE 04 00 00 00 or r14, 4
000000014C46CAC6 49 C7 C2 00 04 00 00 mov r10, 400h
————————————————————————–

在IDA中先p一下,会提示

000000014C46CABF: The function has undefined instruction/data at the specified address.

跳至0x14C46CABF,选中其后指令一起先u后c,再回到函数入口p即可。

TTD调试,在函数入口、出口分别执行如下命令,并用BC对比结果

r;dqs <stack> l 0n21;db <VM_CONTEXT> l 0x174

<stack>最好是VM_DATA附近的值。用BC对比,快速找出发生变化的数据,比如push/pop会导致rsp变化,push会导致栈中数据变化,func_array[i]可能同步修改VM_CONTEXT.rsp,某个pop可能更新VM_CONTEXT.rcx,等等。基于变化的数据,很可能直接猜中func_array[i]大概在干什么。TTD调试,在func_array[i]出口处对发生变化的数据设数据断点,反向执行,找出其变化的逻辑。

基本上每个func_array[i]都含有干扰静态分析的代码,比如这种

————————————————————————–
mov rcx, [rax]
xor rcx, 0x13141314
sub rcx, 0x51201314
mov [rsi], rcx // 保存中间值
...
mov rdx, [rdi] // 取出中间值,rdi等于之前的rsi
add rdx, 0x51201314
xor rdx, 0x13141314
————————————————————————–

一堆垃圾代码互相夹杂着,实际做的是”mov rdx,[rax]”。这是个最简情形,OSF中func_array[i]更复杂,xor/add/sub的op2不再是常量,而是[reg],reg的值也是各种混淆、反混淆得来。不管怎么复杂,混淆与反混淆总是对称出现,用TTD调试相对更容易发现规律。

EAGLE_BLACK虚拟机的func_array[i]功能很单一,OSF所用CV虚拟机在这方面非常变态,会将imul/add/sub/mov/deref等操作组合到某个func_array[i]中,无法对单个func_array[i]标定唯一功能,这是对抗逆向工程的手段之一。

7.1) 复杂func_array[i]的简单示例

本来我尽量避免在本文中展现大段汇编代码,但有时为了产生感性认识,不上例子就差点意思。下面是OSF中某个复杂func_array[i],是个几合一功能的,其中一个功能是add操作,具体到本例,是”add rcx,rax”。如此复杂的逻辑,整下来就干了这么一件简单的事,妥妥地对抗逆向工程。

————————————————————————–
/*
* func_array[0xd7]
*
* add操作
* op1源自dispatch_data[0x13]
* op2源自dispatch_data[5]
*
* 同时支持8/16/32/64位操作,add结果混淆后再保存,会更新VM_CONTEXT.efl
*/
000000014BD10C20 add_mov_deref_n_14BD10C20
...
/*
* 略去求op1、op2所在偏移量的复杂运算
*/
...
/*
* 求出op1在VM_CONTEXT中的偏移,即VM_CONTEXT.rcx的偏移,源自dispatch_data[0x13]
*/
000000014BD11326 49 81 F4 E0 2D 75 32 xor r12, 32752DE0h
...
/*
* 从VM_CONTEXT.rcx取op1原始值,源自dispatch_data[0x13]
*/
000000014BD11D3F 4D 8B 20 mov r12, [r8]
/*
* 混淆op1,源自dispatch_data[0x13]
*/
000000014BD11D42 49 81 F4 E0 2D 75 32 xor r12, 32752DE0h
...
/*
* 继续混淆op1,得到中间值,源自dispatch_data[0x13]
*/
000000014BD12127 4C 2B 26 sub r12, [rsi]
...
/*
* 保存op1中间值,源自dispatch_data[0x13]
*/
000000014BD12FEE 4C 89 26 mov [rsi], r12
...
000000014BD17353 4D 8B 34 24 mov r14, [r12]
...
000000014BD142F7 4D 03 34 24 add r14, [r12]
...
/*
* 求出op2在VM_CONTEXT中的偏移,即VM_CONTEXT.rax的偏移,源自dispatch_data[5]
*/
000000014BD14F8F 4D 33 34 24 xor r14, [r12]
...
/*
* 从VM_CONTEXT.rax取op2原始值,源自dispatch_data[5]
*/
000000014BD10EE4 4D 8B 36 mov r14, [r14]
...
/*
* 混淆op2,源自dispatch_data[5]
*/
000000014BD10EF1 4D 33 34 24 xor r14, [r12]
...
/*
* 继续混淆op2,得到中间值,源自dispatch_data[5]
*/
000000014BD114B9 4D 2B 34 24 sub r14, [r12]
...
/*
* 保存op2中间值,源自dispatch_data[5]
*/
000000014BD114C7 4D 89 34 24 mov [r12], r14
...
/*
* 取op1中间值,源自dispatch_data[0x13]
*/
000000014BD1483E 49 8B 5D 00 mov rbx, [r13+0]
...
/*
* 反混淆op1,源自dispatch_data[0x13]
*/
000000014BD155B2 49 03 5D 00 add rbx, [r13+0]
...
/*
* 继续反混淆op1,得到原始值,源自dispatch_data[0x13]
*/
000000014BD124D2 48 81 F3 E0 2D 75 32 xor rbx, 32752DE0h
...
/*
* 取op2中间值,源自dispatch_data[5]
*/
000000014BD14960 4D 8B 5D 00 mov r11, [r13+0]
...
/*
* 反混淆op2,源自dispatch_data[5]
*/
000000014BD11077 4D 03 5D 00 add r11, [r13+0]
...
/*
* 继续反混淆op2,得到原始值,源自dispatch_data[5]
*/
000000014BD190B7 4D 33 5D 00 xor r11, [r13+0]
...
/*
* 真实add操作所在
*
* rbx=0 op1源自dispatch_data[0x13]
* r11=5 op2源自dispatch_data[5]
*
* 0+5=5 add结果原始值
*/
000000014BD13AE4 4C 01 DB add rbx, r11
/*
* 将add操作产生的efl压栈
*
* rsp=0xbf2b20
* efl=0x206
*/
000000014BD13AE7 9C pushfq
...
/*
* rbx=5 add结果原始值
*
* 5-0x32752de0=0xffffffffcd8ad225 add结果混淆值
*/
000000014BD19061 48 81 EB E0 2D 75 32 sub rbx, 32752DE0h
...
/*
* 保存add结果混淆值
*/
000000014BD16C02 49 89 5D 00 mov [r13+0], rbx
...
/*
* 从栈中弹出efl
*
* rsp=0xbf2b18
* qwo(rsp)=0x206
*/
000000014BD193B9 41 59 pop r9
...
/*
* r15=0x14c3e7ce9 dispatch_data[0x17]
* wo(r15)=0x111 VM_CONTEXT.efl字段的偏移
*/
000000014BD18E76 66 45 8B 27 mov r12w, [r15]
000000014BD18E7A E9 0F F9 FF FF jmp loc_14BD1878E
000000014BD1878E 49 01 EC add r12, rbp
/*
* 更新VM_CONTEXT.efl
*
* r9=0x206
* r12=0x14bbf9a67
*/
000000014BD18791 4D 89 0C 24 mov [r12], r9
...
000000014BD13DCD 41 FF E4 jmp r12
————————————————————————–

为了突出要点,上述代码已做了极大精简,看上去仍很复杂。我是按执行顺序从上到下展示汇编指令,若细看,会发现指令地址并非单向递增的。若非借助TTD调试,很难分析透。

分析并标定func_array[i]功能是CV逆向工程中最繁琐的部分,相当枯燥。借助TTD调试,只要耗下去,肯定能分析清楚,就是性价比太低。

OSF有个func_array_2[0x653],这么多handler,一个个分析过去,会死人的。

即使成功标定了所有func_array[i]的所有功能,意义也很有限。几合一的功能函数,断在入口时几乎无法确定后续走哪个功能流程,不是简单的switch逻辑。

8) 静态定位func_array[i]出口

某些CV虚拟机的func_array[i]相对简单,IDA缺省能识别函数出口。OSF所用CV对单个func_array[i]做了大量block切分操作,就是两三条指令一个block,然后jmp到下一个block,各block之间非物理连续,一会儿前一会儿后的。这种在IDA中用图形模式看还可以,但图形模式只能看函数,若代码片段不属于函数,就得设法p出函数来,还得确保p出来的函数确实包含完整的代码。block切分使得IDA识别函数时包含完整代码的能力下降,可以”Append function tail”人工添加block到指定函数中。

OSF中func_array[i]大多极其复杂,IDA缺省无法将之p成函数,很容易找到函数入口,但肉眼极难找到函数出口。可以写IDAPython脚本从函数入口开始进行类似”全连接图非递归广度优先遍历”的操作,寻找间接跳转或ret指令,以此定位func_array[i]出口。可用同样的技术从函数入口开始自动c操作,直至出口,再自动p,因为OSF中大量代码未被IDA识别,缺省以数据形式展现。

有2个出口的func_array[i]一般对应jxx操作。

9) 寻找流程分叉点

已知流程会过[addr_a,addr_b]区间,我想在此区间单步执行每一条指令,每次单步后想执行一些命令,比如检查相关寄存器值并据此做出不同动作。

该需求与ta/pa命令无关,这两个命令无法在每次单步后执行指定命令。也与wt命令无关。

编辑tcmd.txt如下

————————————————————————–
.if(@rip==@$t1){}.else{r $t0,rip;r $t0=@$t0+1;t "$$< tcmd.txt"}
————————————————————————–

在addr_a处执行如下命令

t “r $t0=0;r $t1=<addr_b>;$$< tcmd.txt”

效果是,从addr_a单步执行至addr_b,每次都执行”r $t0,rip;r $t0=@$t0+1″,输出中会看到一堆”$t0=… rip=…”。

这只是示例,根据原始需求调整tcmd.txt的内容,比如当rax等于特征值时停止单步执行,修改每次单步时所执行的命令,等等。这是土法”Run Trace”功能。

配合.logopen/.logclose,对指定范围内被执行的指令进行定制化记录,当流程因in不同而不同时,对两次log进行BC比较,快速找出分叉点。根据[addr_a,addr_b]的具体情况,将t换成p,避免失焦。

我用类似技术快速定位了jnz操作对应的func_array[i]的分叉点。当时看到的代码片段是这样的

————————————————————————–
/*
* esi=0x202 源自VM_CONTEXT.efl
*
* 0x40是ZF
*/
000000014006BEE5 81 E6 40 00 00 00 and esi, 40h
000000014006BEEB 0F 85 31 00 00 00 jnz loc_14006BF22
————————————————————————–

参看

https://en.wikipedia.org/wiki/FLAGS_register

10) 反向执行寻找pushfq

调试OSF对试用期过期天数反向溯源时,有次通过数据断点反向找到0x14BCAF348处的代码。在IDA中图形化查看其所在func_array[i],TTD调试对比过入口、出口处相关数据区,注意到VM_CONTEXT.efl有变,合理猜测该函数可能提供add或sub操作。但该函数相当复杂,IDA中静态查看,很难找到原始的add或sub操作。

基于已积累的经验,add或sub操作之后必有pushfq,从0x14BCAF348处反向执行,找到pushfq,其低址方向的指令就会揭示究竟是add还是sub,或是其他什么操作。

————————————————————————–
/*
* func_array[0x45]
*
* sub操作,会更新VM_CONTEXT.efl(0x14bbf9a67)
*/
000000014BCA98C7 imul_add_sub_mov_n_14BCA98C7
...
/*
* r8d=0x278d00 (30天对应的秒数)
* r13d=0x3d862 (已过去的秒数)
*
* 0x278d00-0x3d862=0x23b49e (以秒为单位的过期时间)
*/
000000014BCB0222 45 29 E8 sub r8d, r13d
000000014BCB0225 E9 AA EC FF FF jmp loc_14BCAEED4
000000014BCAEED4 9C pushfq
...
/*
* r13d=0x23b49e (以秒为单位的过期时间)
* r12=0x14bbf99bd VM_CONTEXT.rcx
*
* t- "r $t0=0;$$< tcmd_1.txt"
*
* 用上述命令反向定位pushfq指令,更快的办法是
*
* ba w1 /1 @rsp;g-
*/
000000014BCAF348 45 89 2C 24 mov [r12], r13d
————————————————————————–

撰写本文时,意识到反向寻找pushfq最简办法是,TTD调试,在0x14BCAF348处执行

ba w1 /1 @rsp;g-

当时不知哪里想岔了,用一个笨办法。编辑tcmd_1.txt如下

————————————————————————–
.if(by(@rip)==0x9c){}.else{r $t0,rip;r $t0=@$t0+1;t- "$$< tcmd_1.txt"}
————————————————————————–

在0x14BCAF348处执行如下命令

t- “r $t0=0;$$< tcmd_1.txt”

笨办法也能成功反向定位pushfq。单就前例而言,不推荐笨办法,但tcmd_1.txt可以定制修改以满足其他需求,这是一种非凡的、普适的调试技巧。

11) cv_entry[3]的函数化

CV虚拟化过的代码会多次离开、重入CV虚拟机,并非只有一组cv_entry[0]到cv_entry[5]。有些重入CV虚拟机的流程可能有各自的cv_entry[1],但共用一个cv_entry[3],OSF就出现了这种情况。此时IDA缺省无法将cv_entry[3]函数化,手工p会失败,其主要原因是多个cv_entry[1]已经函数化,IDA将它们共用的cv_entry[3]纳入它们的函数范畴。

cv_entry[1]是否函数化的重要性远比不上将cv_entry[3]函数化,宁可牺牲前者,也得成全后者,有很多重要调试点位于cv_entry[3]与cv_entry[4]之间。

可以写IDAPython脚本,实现OSFDeleteFunc(),遍历指定地址的反向交叉引用,对所有反向交叉引用点所在函数进行删除函数的操作。有的被共用的cv_entry[3],其反向交叉引用有几十个,一个个手工删除函数不现实。删干净后再在cv_entry[3]处p,一般都会成功。待cv_entry[3]函数化成功之后,再将其反向交叉引用点所在代码片段重新函数化,这个随意,无所谓。

————————————————————————–
def OSFDeleteFunc ( ea, delete=False ) :
 for x in idautils.XrefsTo( ea, 0 ) :
  if ida_xref.fl_JN == x.type :
   func = ida_funcs.get_func( x.frm )
   if func is not None :
     print( hex( func.start_ea ) )
     if delete :
      ida_funcs.del_func( func.start_ea )
————————————————————————–

☆ 后记

分析CV虚拟化时容易像无头苍蝇一样乱转,较好的方式是,给自己定几个比较明确的目标,比如

a) 调用外部库函数时如何组织、传递形参
b) 调用外部库函数时字符串形参是否涉及反混淆
c) 调用外部库函数时如何离开、重入CV虚拟机
d) 寻找test/cmp这类操作对应的func_array[i]
e) 寻找jxx指令对应的func_array[i]

带着这些具体问题去分析CV虚拟化,搞清楚后在暴破场景能派上用场。CV虚拟机虽然每次都变形,但总有一些套路是不变的。

本文给出的各种技巧与思路是普适的、提纲挈领的,刻意避免陷入只见树木不见森林的境地。我是按照给从未接触过CV逆向工程的新手进行可操作式科普来撰写本文的,有意上手者,对照本文进行一次CV逆向工程实战,可快速入门。

版权声明

本站“技术博客”所有内容的版权持有者为绿盟科技集团股份有限公司(“绿盟科技”)。作为分享技术资讯的平台,绿盟科技期待与广大用户互动交流,并欢迎在标明出处(绿盟科技-技术博客)及网址的情形下,全文转发。
上述情形之外的任何使用形式,均需提前向绿盟科技(010-68438880-5462)申请版权授权。如擅自使用,绿盟科技保留追责权利。同时,如因擅自使用博客内容引发法律纠纷,由使用者自行承担全部法律责任,与绿盟科技无关。

Spread the word. Share this post!

Meet The Author

C/ASM程序员