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