Skip to content

标题: Code Virtualizer逆向工程浅析

创建: 2022-06-30 16:56 更新: 2022-07-08 10:58 链接: https://scz.617.cn/misc/202206301656.txt https://bbs.pediy.com/thread-273533.htm


目录:

☆ 背景介绍
☆ Code Virtualizer 2.2.2.0
    1) CV SDK
    2) cvtest.c
    3) 编译
    4) CV虚拟化
☆ TTD
☆ CV虚拟机框架概览
    1) 从cv_entry[0]到cv_entry[4]
    2) 定位cv_entry[1]
    3) 在cv_entry[4]处获取关键信息
    4) 定位func_array[]
    5) 确定func_array[]元素个数
    6) 推断VM_CONTEXT结构
    7) 从VM_DATA复制数据到VM_CONTEXT
    8) 定位cv_entry[5]
☆ CV虚拟机逆向工程经验
    1) VM_CONTEXT.dispatch_data
    2) 跟踪func_array[i]
    3) VM_CONTEXT.efl
    4) VM_CONTEXT.rsp
    5) deref操作
    6) 库函数调用
    7) 分析func_array[i]
        7.1) 复杂func_array[i]的简单示例
    8) 静态定位func_array[i]出口
        8.1) 全连接图的非递归广度优先遍历
    9) 寻找流程分叉点
   10) 反向执行寻找pushfq
   11) cv_entry[3]的函数化
☆ 后记

☆ 背景介绍

"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

000000000014fe00 000000000000006c 000000000014fe08 8888888888888888 r8 <=rsp 000000000014fe10 9999999999999999 r9 000000000014fe18 aaaaaaaaaaaaaaaa r10 000000000014fe20 bbbbbbbbbbbbbbbb r11 000000000014fe28 cccccccccccccccc r12 000000000014fe30 dddddddddddddddd r13 000000000014fe38 eeeeeeeeeeeeeeee r14 000000000014fe40 ffffffffffffffff r15 000000000014fe48 6666666666666666 rdi 000000000014fe50 5555555555555555 rsi 000000000014fe58 7777777777777777 rbp 000000000014fe60 2222222222222222 rbx 000000000014fe68 2222222222222222 rbx_other 000000000014fe70 4444444444444444 rdx 000000000014fe78 3333333333333333 rcx 000000000014fe80 1111111111111111 rax 000000000014fe88 0000000000000202 efl 000000000014fe90 000000000000006c func_array_index 000000000014fe98 000000000019a914 dispatch_data 0x14019a914 000000000014fea0 0000000000000001

从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

00000001400ab9d6 01 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000001400ab9e6 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000001400ab9f6 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000001400aba06 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000001400aba16 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000001400aba26 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000001400aba36 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000001400aba46 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000001400aba56 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000001400aba66 00 00 00 00 00 00 77 77-77 77 77 77 77 77 00 00 ......wwwwwwww.. 00000001400aba76 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000001400aba86 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000001400aba96 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000001400abaa6 00 00 00 00 00 00 00 00-40 01 00 00 00 00 00 00 ........@....... 00000001400abab6 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000001400abac6 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000001400abad6 00 00 00 00 00 00 00 00-00 00 14 a9 19 40 01 00 .............@.. 00000001400abae6 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000001400abaf6 00 00 00 00 00 00 00 00-00 00 ae 0e 18 40 01 00 .............@.. 00000001400abb06 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000001400abb16 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000001400abb26 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000001400abb36 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000001400abb46 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

000000000014fe08 8888888888888888 r8 <=rsp 000000000014fe10 9999999999999999 r9 000000000014fe18 aaaaaaaaaaaaaaaa r10 000000000014fe20 bbbbbbbbbbbbbbbb r11 000000000014fe28 cccccccccccccccc r12 000000000014fe30 dddddddddddddddd r13 000000000014fe38 eeeeeeeeeeeeeeee r14 000000000014fe40 ffffffffffffffff r15 000000000014fe48 6666666666666666 rdi 000000000014fe50 5555555555555555 rsi 000000000014fe58 7777777777777777 rbp 000000000014fe60 2222222222222222 rbx 000000000014fe68 2222222222222222 rbx_other 000000000014fe70 4444444444444444 rdx 000000000014fe78 3333333333333333 rcx 000000000014fe80 1111111111111111 rax 000000000014fe88 0000000000000202 efl 000000000014fe90 000000000000006c func_array_index 000000000014fe98 000000000019a914 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结构

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

从func_array[i]转移到func_array[j],受dispatch_data[k]影响。

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 l 0n21;db l 0x174

最好是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操作。

8.1) 全连接图的非递归广度优先遍历

这是成熟算法,转一个

  1. 创建一个空队列Q来存储尚未打印的顶点
  2. 创建一个空列表L来存储访问过的顶点
  3. 将起始顶点插入Q和L
  4. 如果Q为空,则转至9,否则转至5
  5. 从Q中取出一个顶点v
  6. 输出顶点v
  7. 将所有不在L中的v的邻居插入Q和L
  8. 转到4
  9. 停止

假设用Python实现上述算法,不需要queue模块,Q与L都用内置的list即可。

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=;$$< 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可以 定制修改以满足其他需求,这是一种非凡的、普适的调试技巧。

若非试图分享CV逆向工程经验,我不会进行二次总结、三次总结,也不会对总结过的 经验复审,从而不会意识到自己用了个笨办法,可能相当长时间里陷入笨办法的思维 定势。这正是分享、交流的意义,是反复总结的意义,是文档化的意义。

半个月前bluerust因故跟我感慨,年岁大了,文档化太重要。当时我就斥责他,你看, 我在你们旁边耳提面命了多年,你们就是不好好听、不好好实践,仗着自己智商高、 记忆力强肆无忌惮,老了后悔了吧。

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逆向工程实战,可快速入门。