主要是自己看加密与解密4的时候vm部分的笔记
VM的存在,主要是将可执行代码转换为字节码指令系统的代码,达到原有的指令不会被容易的逆向以及篡改,流程图:
VStartVM部分初始化虚拟机,VMDispatcher调度Handler,每个Handler就是CPU支持的一条指令,ByteCode是CPU执行的二进制代码
VM的启动框架和调用约定,每种代码执行模式都需要有启动框架和调用约定,在C语言中,在进入main函数之前,会有一些C库添加的启动函数,它们负责对栈区,变量进行初始化,在main函数执行完毕之后再收回控制权,这就叫做启动框架,而C语言 CALL,StdCall等约定就是调用约定,它们规定了传参方式和堆栈平衡方式,同样,对与VM虚拟机,我们也需要有一种启动框架和调用约定,来保证虚拟指令的正确执行以及虚拟和现实代码之间的切换
我们的VStartVM将环境压入栈后生成一个VMDispatcher标签,当我们的Handler执行完在跳回这里,形成循环
VStartVM将所有的寄存器压入堆栈,esi指向字节码的起始地址,ebp指向真实的栈,edi指向VMContext,esp的值进行了运算就是我们所说的VM使用的栈地址,VM的环境结构和栈都放在了当前栈的200h处,如果接近自己存放的数据,我们就直接读取字节码,读取一个字节,在JUMP表中寻找对应的Handler跳转过去执行
push opcode地址
jmp VStartVM
VStartVM:
push eax
push ebx
push ecx
push edx
push esi
push edi
push ebp
pushfd
mov esi,[esp+0x20] ;字节码开始位置
mov ebp,esp ;真实栈
sub esp,0x200
mov edi,esp ;VMContext
sub esp,0x40 ;这时的esp就是VM用的堆栈了
vBegin:
...
jmp VMDispatcher
VMDispatcher:
movzx eax,byte ptr [esi] ;获得bytecode
lea esi,[esi+1]
jmp dword ptr [eax*4+JUMPADDR] ;跳到handler执行处
堆栈低地址...
... --------
... |
... 0x40字节的空间,VM使用的堆栈空间(不一定非要0x40大小)
... |
... -------- edi指向这里(VMcontext)
... |
... 0x200字节的空间,用于存放VMcontext
... |
... --------
flags 保存的标志寄存器 ebp指向这里
ebp --------
edi |
esi |
edx 保存的寄存器
ecx |
ebx |
eax --------
伪代码开始地址(虚拟机参数) esi指向这里(伪代码地址)
...
堆栈高地址...
VMcontext:虚拟环境结构,存放了一些需要用的值
struct VMContext{
DWORD v_eax;
DWORD v_ebx;
DWORD v_ecx;
DWORD v_edx;
DWORD v_esi;
DWORD v_edi;
DWORD v_ebp;
DWORD v_efl;
}
VStartVM将所有的寄存器都压入了栈,栈达到平衡后,才开始执行真正的代码
Handle vBegin代码:
vBegin:
mov eax,dword ptr [ebp]
mov [edi+0x1c],eax ;v_efl
add ebp,4
mov eax,dword ptr [ebp]
mov [edi+0x18],eax ;v_ebp
add ebp,4
mov eax,dword ptr [ebp]
mov [edi+0x14],eax ;v_edi
add ebp,4
mov eax,dword ptr [ebp]
mov [edi+0x10],eax ;v_esi
add ebp,4
mov eax,dword ptr [ebp]
mov [edi+0xC],eax ;v_edx
add ebp,4
mov eax,dword ptr [ebp]
mov [edi+0x8],eax ;v_ecx
add ebp,4
mov eax,dword ptr [ebp]
mov [edi+0x4],eax ;v_ebx
add ebp,4
mov eax,dword ptr [ebp]
mov [edi],eax ;v_eax
add ebp,4
add ebp,4 ;push字节码地址
jmp VMDispatcher
为了避免改写VMcontext的内容,结构覆盖,设计了Handler vCheckESP:
vCheckESP:
lea eax,dword ptr [edi+0x100]
cmp eax,ebp ;比较了ebp和中间位置的地址
jl VMDispatcher ;小于就跳转
mov edx,edi
mov ecx,esp
sub ecx,edx
push esi
mov esi,esp
sub esp,0x60
mov edi,esp
push edi
sub esp,0x40
cld ;递增的方式
rep movsb ;复制
pop edi
pop esi
jmp VMDispatcher
涉及到栈的Handler执行后跳转到vCheckESP,判断esp是否接近VMContext,接近就提升
handler分两大类:
- 辅助handler,指一些更重要的、更基本的指令,如堆栈指令
- 普通handler,指用来执行普通的x86指令的指令,如运算指令
辅助handler除了VBegin这些维护虚拟机不会导致崩溃的handler之外,就是专门用来处理堆栈的handler了
vPushReg32:
mov eax, dword ptr [esi] ;从字节码中得到VMContext中的寄存器偏移
add esi, 4
mov eax, dword ptr [edi+eax] ;得到寄存器的值
push eax ;压入寄存器
jmp VMDispatcher
vPushImm32:
mov eax, dword ptr [esi]
add esi, 4
push eax
jmp VMDispatcher
vPushMem32:
mov eax, 0
mov ecx, 0
mov eax. dword ptr [esp] ;第一个寄存器偏移
test eax, eax
cmovge edx, dword ptr [edi+eax] ;如果不是负数则赋值
mov eax, dword ptr [esp+4] ;第二个寄存器偏移
test eax, eax
cmovge ecx, dword ptr [edi+eax] ;如果不是负数则赋值
imul ecx, dword ptr [esp+8] ;第二个寄存器的乘积
add ecx, dword ptr [esp+0x0C] ;第三个为内存地址常量
add edx, ecx
add esp, 0x10 ;释放参数
push edx ;插入参数
jmp VMDispatcher
vPopReg32:
mov eax, dword ptr [esi] ;得到reg偏移
add esi, 4
pop dword ptr [edi+eax] ;弹回寄存器
jmp VMDispatcher
vFree:
add esp, 4
jmp VMDispatcher
用个图来举个例子:正常的x86指令的Handler
指令是由我们的普通Handler处理,源操作数,目的操作数,寄存器都是由辅助Handler处理,这么设计的好处就是,假设我们的add指令有add reg,imm add reg,reg add reg,mem ... 那么我们如果有辅助handler和普通handler就不需要每一个模式都写一个handler,我们先把操作数都交给辅助handler处理,那么执行普通handler的时候,操作数已经变成了立即数存放了,直接用立即数就可以了,将辅助指令和普通指令配合起来来一起完成x86指令到伪指令的转换
实现一个vadd:
vadd:
mov eax, [esp+4] ;取源操作数
mov ebx, [esp] ;取目的操作数
add ebx, eax
add esp, 8 ;平衡堆栈
push ebx ;压入堆栈
指令:
add esi, eax
转换代码:
vPushReg32 eax_index ;eax在VMContext下的偏移
vPushReg32 esi_index
vadd
vPopReg32 esi_index
--------------------------------------------------------------------
vPushReg32:
mov eax, dword ptr [esi] ;从字节码中得到VMContext中的寄存器偏移
add esi, 4
mov eax, dword ptr [edi+eax] ;得到寄存器的值
push eax ;压入寄存器
jmp VMDispatcher
---------------------------------------------------------------------
vPopReg32:
mov eax, dword ptr [esi] ;得到reg偏移
add esi, 4
pop dword ptr [edi+eax] ;弹回寄存器
jmp VMDispatcher
指令:
add esi,1234
转换代码:
vPushImm32 1234
vPushReg32 esi_index
vadd
vPopReg32 esi_index
---------------------------------------------------------------------
vPushImm32:
mov eax, dword ptr [esi]
add esi, 4
push eax
jmp VMDispatcher
---------------------------------------------------------------------
vPushReg32:
mov eax, dword ptr [esi] ;从字节码中得到VMContext中的寄存器偏移
add esi, 4
mov eax, dword ptr [edi+eax] ;得到寄存器的值
push eax ;压入寄存器
jmp VMDispatcher
---------------------------------------------------------------------
vPopReg32:
mov eax, dword ptr [esi] ;得到reg偏移
add esi, 4
pop dword ptr [edi+eax] ;弹回寄存器
jmp VMDispatcher
---------------------------------------------------------------------
vadd:
mov eax, [esp+4] ;取源操作数
mov ebx, [esp] ;取目的操作数
add ebx, eax
add esp, 8 ;平衡堆栈
push ebx ;压入堆栈
指令: 原操作数是一个内存数,内存数真实结构[ imm + reg * scale + reg2 ]
add esi,dword ptr [401000]
转换代码 :
vPushImm32 401000
vPushImm32 1 ;scale
vPushImm32 -1 ;reg2_index
vPushImm32 -1 ;reg_index
vPushMem32 ;压入内存地址
vPushReg32 esi_index
vadd
vPopReg32 esi_index
---------------------------------------------------------------------
vPushImm32:
mov eax, dword ptr [esi]
add esi, 4
push eax
jmp VMDispatcher
---------------------------------------------------------------------
vPushMem32:
mov eax, 0
mov ecx, 0
mov eax, dword ptr [esp] ;第一个寄存器偏移
test eax, eax
cmovge edx, dword ptr [edi+eax] ;如果不是负数则赋值
mov eax, dword ptr [esp+4] ;第二个寄存器偏移
test eax, eax
cmovge ecx, dword ptr [edi+eax] ;如果不是负数则赋值
imul ecx, dword ptr [esp+8] ;第二个寄存器的乘积
add ecx, dword ptr [esp+0x0C] ;第三个为内存地址常量
add edx, ecx
add esp, 0x10 ;释放参数
push edx ;插入参数
jmp VMDispatcher
---------------------------------------------------------------------
vadd:
mov eax, [esp+4] ;取源操作数
mov ebx, [esp] ;取目的操作数
add ebx, eax
add esp, 8 ;平衡堆栈
push ebx ;压入堆栈
---------------------------------------------------------------------
vPopReg32:
mov eax, dword ptr [esi] ;得到reg偏移
add esi, 4
pop dword ptr [edi+eax] ;弹回寄存器
jmp VMDispatcher
无论什么形式,都可以使用vadd来执行,只是使用了不用的栈Handler
标志的问题一般来说,有的指令是设置标志位,有的指令是判断标志位,所以,应该在相关Handler执行之前保存标志位,在相关Handler执行后恢复标志位,比如stc命令是让标志位置1
VStc:
push [edi+0x1C] ;EFL
popfd ;返回到EFL
stc ;改变
pushfd ;EFL到堆栈
pop [edi+0x1C] ;返回给EFL
jmp VMDispatcher
esi指针相当于我们真实CPU中的eip寄存器,可以通过改写esi寄存器的值来改变流程
jmp:
vJmp:
mov esi, dword ptr [esp] ;[esp]指向要跳转到的地址
add esp, 4
jmp VMDispatcher
jcc条件转移指令和comvcc条件传输指令高度匹配
条件跳转指令 | 条件传输指令 |
---|---|
jne //不等于则跳转 | cmovne |
ja //无符号大于则跳转 | comva |
jae //无符号大于等于则跳转 | cmovae |
jb //无符号小于则跳转 | cmovb |
jbe //无符号小于等于则跳转 | cmovbe |
je //等于则跳转 | cmove |
jg //有符号大于则跳转 | cmovg |
所有的跳转都有条件传输指令
vJne:
cmovne esi, [esp]
add esp, 4
jmp VMDispatcher
vJa:
cmova esi, [esp]
add esp, 4
jmp VMDispatcher
vJae:
cmovae esi, [esp]
add esp, 4
jmp VMDispatcher
vJb:
cmovb esi, [esp]
add esp, 4
jmp VMDispatcher
vJbe:
cmovbe esi, [esp]
add esp, 4
jmp VMDispatcher
je:
cmove esi, [esp]
add esp, 4
jmp VMDispatcher
jg:
cmovg esi, [esp]
add esp, 4
jmp VMDispatcher
jecxz: //JECXZ(ECX 为 0 则跳转)
mov ecx,[edi+8]
test ecx,ecx
cmovz esi,eax //JZ ;为 0 则跳转
add esp,4
jmp VMDispatcher
如果按照上面的做法来进行模拟跳转比较简单,所以可以在模拟转移指令时候判断标示位
如果知道了标志位所在的位置,就可以模拟条件跳转
vJAE:
push [edi+0x1C]
pop eax
and eax,1
cmove esi,[esp]
add esp,4
jmp VMDispatcher
也可以调用指令:
vPush jumptoaddr ;跳转的地址
vJae
这个指令首先得到标识位,然后1和eax = EFL 进行and,comve用于判断ZF标志是否为0,如果是0就改变esi指向,JAE指令只判断我们的CF位
vJBE:
push [edi+0x1C]
pop eax
and eax,0x41 ;1001Bh
cmp eax,0x41 ;如果小于等于则转移(CF=1 || ZF=1)
cmove esi,[esp]
add esp,4
jmp VMDispatcher
其他同理
首先,虚拟机设计为只在一个堆栈层次上运行
mov eax, 1234
push 1234
call anotherfunc
theNext:
add esp, 4
其中第1、2、4条指令都是在当前堆栈层次上执行的,而call anotherfunc是调用子函数,会将控制权移交给另外的代码,这些代码是不受虚拟机控制的,所以碰到call指令,必须退出虚拟机,让子函数在真实CPU中执行完毕后再交回给虚拟机执行下一条指令
push theNext
jmp anotherfunc
如果想在推出虚拟机后让anotherfunc这个函数返回后再次拿回控制权,可以更改返回地址,来达到继续接管代码的操作,在一个地址上写上这样的代码:
theNextVM:
push theNextByteCode
jmp VStartVM
这是一个重新进入虚拟机的代码,theNextByteCode代表了theNext之后的代码字节码。只需将theNext的地址改为theNextVM的地址,即可完美地模拟call指令了。当虚拟机外部的代码执行完毕后ret回来的时候就会执行theNextVM的代码,从而使虚拟机继续接管控制权
vcall:
push all vreg ;所有虚拟寄存器
pop all reg ;弹出到真实寄存器中
push 返回地址
push 要调用的函数的地址
retn
retn在虚拟机里当作一个退出函数,retn一种是不带参数的,一种带参数
retn
retn 4
第一种得到esp存放的返回地址,释放返回地址的栈并跳转到返回地址,第二种释放返回地址的栈时再释放操作数空间
vRetn:
xor eax,eax
mov ax,word ptr [esi] ;retn的操作数是WORD的,所以值为0xFFFFh
add esi,2
mov ebx,dword ptr [ebp] ;得到要返回的地址
add ebp,4 ;释放空间
add ebp,eax ;如果有操作数,释放
push ebx ;压入返回地址
push ebp ;压入栈指针
push [edi+0x1c]
push [edi+0x18]
push [edi+0x14]
push [edi+0x10]
push [edi+0x0c]
push [edi+0x08]
push [edi+0x04]
push [edi]
pop eax
pop ebx
pop ecx
pop edx
pop esi
pop edi
pop ebp
popfd
pop esp ;将栈指针还原到esp中,VM_Context自动销毁
retn
不能识别的指令为不可模拟指令,我们只能vcall退出虚拟机,执行这个指令
VC 7编译器生成的栈帧布局,Scopetable是一个记录(record)的数组,每个record描述了一个_try块,以及块之间的关系
sturt _SCOPETABLE_ENTRY
{
DWORD EnclosingLevel;
void* FilterFunc;
void* HandlerFunc;
}
Stack frame: 栈帧, 被某个函数使用的一段堆栈段. 通常包含函数参数, 返回地址, 保存的寄存器现场, 局部变量和其他一些特定于这个函数的数据. 在x86(和大多数的其他架构)上, 调用者与被调用者的栈帧是相邻的.
Frame pointer: 栈帧指针, 一个指向栈帧里固定地址的寄存器或其他变量. 通常栈帧里的所有数据都是用它来作相对寻址. 在x86上它通常是ebp而且通常是指向返回地址的下面.
Object: 对象, 一个(C++)类的实例.
Unwindable Object: 可展开对象, 一个被指定为auto存储级别(auto storage-class)的局部对象, 它被分配在栈里, 而且在离开作用范围之后被销毁.
Stack Unwinding: 栈展开, 自动销毁上面那些对象, 在因异常使程序流离开它们的作用范围时发生
在C或C++程序里可以使用两种异常:
SEH(Structured Exception Handling) 异常, 结构化异常处理异常. 就是平常说的Win32或系统异常
C++异常(有时称为"EH"). 在SEH之上实现. C++异常允许抛出和捕获任意类型
Whidbey(MSVC 2005)编译器在SEH帧里添加了一些缓冲区溢出的保护措施:
struct _EH4_SCOPETABLE
{
DWORD GSCookieOffset;
DWORD GSCookieXOROffset;
DWORD EHCookieOffset;
DWORD EHCookieXOROffset;
_EH4_SCOPETABLE_RECORD ScopeRecord[1];
};
struct _EH4_SCOPETABLE_RECORD
{
DWORD EnclosingLevel;
long (*FilterFunc)();
union
{
void (*HandlerAddress)();
void (*FinallyFunc)();
};
};