这次来一个关于VM的混淆办法,可能只是个小trick,仅仅来自胡思乱想
也许你会觉得很奇怪,一个VM能有啥新鲜的,对,单纯来说VM保护源代码已经非常的成熟了,所以在这里只做最基本的介绍,而且这次的重点不在于VM。
VM就是Virtual Machine(虚拟机器),这里和vmware不是一样的,这里指的是自己写的引擎和opcode类型,比如Java就是基于JVM的语言,通过将代码编译成字节码,再运行在JVM上。
所以我们对于一个加密算法可以先翻译成opcode,再运行在我们自己写的引擎上,这样就大大增加逆向成本。
上图就是一个典型的VM执行引擎结构图(有点像控制流平坦化 ).....
再来个源码样式:
那么介绍完了VM,我们了解到VM保护的逆向一般都得先找到opcode,再分析引擎,再写parser,再人肉看,我们从后三个方向都容易让人恶心,这里介绍一种从Opcode这里恶心人的操作。
我们知道CTF的VM一般将opcode单独放到一个字节数组里,有一个指针慢慢的扫描过去来进行解释执行,完全是虚拟化的环境,但是有没有想过,这里就导致了opcode极其容易dump出来,要是能够让opcode和汇编指令混在一起就好了。
所以这里说的就是如何将Opcode和汇编指令放在一起,混杂着执行就很恶心了。
首先,你的静态分析会炸掉,因为混杂了不可反汇编的指令,可能会因为上下文使得这一块的代码和数据类型区分不开甚至指令分析错误,F5铁定是废了,再者,Opcode提取比较困难,你可能得手工提取那些穿插着Opcode。
其实还有一种好处,涉及到原理才能知道为什么了。
这里我们使用windows下的环境,使用的是windows 强大的调试API,具体的实现逻辑是:运行时碰到非法指令,会触发异常,通过调试器调试子进程截获异常,获取上下文进行操作
所以我们得先创造子进程,子进程被调试,本身则相当于系统处理不了的字节码的解释器(有点类似linux下基于信号的VM,大佬敏锐的见解)
STARTUPINFO si;
memset(&si,0,sizeof(si));
si.cb=sizeof(si);
PROCESS_INFORMATION pi;
HMODULE h=GetModuleHandleW(0);
CHAR Filename[260];
GetModuleFileNameA(h,(LPSTR)&Filename,260);
BOOL result=CreateProcessA(NULL,(LPSTR)&Filename,NULL,NULL,FALSE,DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS,NULL,NULL,(LPSTARTUPINFOA)&si,&pi);
这里就是创建子进程的代码,而且是以调试模式启动创建,所以会拥有许多权限。
这里实现VM最重要的API就是SetThreadContext,因为可以实现寄存器等的获取与修改,还有一个就是ReadProcessMemory和WriteProcessMemory,因为需要对内存进行读写。
最后再来个while(true)里面随时截获异常消息进行处理,处理完之后再SetThreadContext将目前执行完的寄存器的值设置。
还有一点细节,就是写个IsDebuggerPresent来判断自己是被调试的还是父进程,切勿陷入死循环,疯狂CreateProcess,那CPU直接暴毙。
void CreateSubProcess()
{
STARTUPINFO si;
memset(&si,0,sizeof(si));
si.cb=sizeof(si);
PROCESS_INFORMATION pi;
HMODULE h=GetModuleHandleW(0);
CHAR Filename[260];
GetModuleFileNameA(h,(LPSTR)&Filename,260);
BOOL result=CreateProcessA(NULL,(LPSTR)&Filename,NULL,NULL,FALSE,DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS,NULL,NULL,(LPSTARTUPINFOA)&si,&pi);
VMState vms;
if(result)
{
//ShowWindow(GetConsoleWindow(),SW_HIDE);
DEBUG_EVENT de;
while(WaitForDebugEvent(&de,INFINITE)!=0)
{
if(de.dwDebugEventCode==EXCEPTION_DEBUG_EVENT)
{
EXCEPTION_DEBUG_INFO di=de.u.Exception;
if(di.ExceptionRecord.ExceptionCode==EXCEPTION_ILLEGAL_INSTRUCTION)
{
CONTEXT context;
memset(&context,0,sizeof(context));
context.ContextFlags=CONTEXT_FULL;
GetThreadContext(pi.hThread,&context);
if(ReadRemoteByte(pi.hProcess,context.Eip)==0xC7)
{
vms.context=&context;
vms.pi=π
RunVM(vms);
SetThreadContext(pi.hThread,&context);
}
else
{
TerminateProcess(pi.hProcess,0);
exit(0);
}
}
}
if(de.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT)
exit(0);
ContinueDebugEvent(de.dwProcessId,de.dwThreadId,DBG_CONTINUE);
}
}
return;
}
所以主要代码就在这里了,然后完善下RunVM函数就可以了。如下就是完整代码,有个Opcode.h没给出不过问题不大,我们不在于写VM而在于怎么实现opcode和asm混杂
还有一个重要细节,就是每个VM的opcode之前必须要有个前缀,而且这个前缀必须不能对应任何的汇编指令,不然就不会触发非法指令的异常,最后的EIP设置也记得要跳过最开头这个字节,在代码中我选取的是0xC7
// SEHVM.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include<cstdio>
#include<cstdlib>
#include<windows.h>
#include"Opcodes.h"
using namespace std;
struct VMState
{
PCONTEXT context;
PPROCESS_INFORMATION pi;
};
BYTE ReadRemoteByte(HANDLE hProcess,int addr)
{
BYTE data;
ReadProcessMemory(hProcess,(LPVOID)addr,&data,1,NULL);
return data;
}
DWORD ReadRemoteDword(HANDLE hProcess,int addr)
{
int data;
ReadProcessMemory(hProcess,(LPVOID)addr,&data,4,NULL);
return data;
}
void WriteRemoteDword(HANDLE hProcess,int addr,DWORD data)
{
WriteProcessMemory(hProcess,(LPVOID)addr,(LPVOID)&data,4,NULL);
}
void pop(VMState vms,DWORD *val)
{
*val=ReadRemoteDword(vms.pi->hProcess,vms.context->Esp);
vms.context->Esp+=4;
}
void push(VMState vms,int val)
{
vms.context->Esp-=4;
WriteRemoteDword(vms.pi->hProcess,vms.context->Esp,val);
}
DWORD *getReg(VMState vms,BYTE x)
{
if(x==Reg1)
return &vms.context->Eax;
if(x==Reg2)
return &vms.context->Ebx;
if(x==Flag)
return &vms.context->Ecx;
if(x==LoopReg)
return &vms.context->Edx;
printf("Unknow reg id: 0x%X\n",x);
exit(0);
}
void RunVM(VMState vms)
{
DWORD *eip=&(vms.context->Eip);
BYTE op=ReadRemoteByte(vms.pi->hProcess,*eip+1);
int arg1;
BYTE arg2;
switch(op)
{
case PushImm:
arg1=ReadRemoteDword(vms.pi->hProcess,*eip+2);
push(vms,arg1);
vms.context->Eip+=6;
break;
case PopReg:
arg2=ReadRemoteByte(vms.pi->hProcess,*eip+2);
pop(vms,getReg(vms,arg2));
vms.context->Eip+=3;
break;
default:
TerminateProcess(vms.pi->hProcess,0);
exit(0);
break;
}
}
void CreateSubProcess()
{
STARTUPINFO si;
memset(&si,0,sizeof(si));
si.cb=sizeof(si);
PROCESS_INFORMATION pi;
HMODULE h=GetModuleHandleW(0);
CHAR Filename[260];
GetModuleFileNameA(h,(LPSTR)&Filename,260);
BOOL result=CreateProcessA(NULL,(LPSTR)&Filename,NULL,NULL,FALSE,DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS,NULL,NULL,(LPSTARTUPINFOA)&si,&pi);
VMState vms;
if(result)
{
//ShowWindow(GetConsoleWindow(),SW_HIDE);
DEBUG_EVENT de;
while(WaitForDebugEvent(&de,INFINITE)!=0)
{
if(de.dwDebugEventCode==EXCEPTION_DEBUG_EVENT)
{
EXCEPTION_DEBUG_INFO di=de.u.Exception;
if(di.ExceptionRecord.ExceptionCode==EXCEPTION_ILLEGAL_INSTRUCTION)
{
CONTEXT context;
memset(&context,0,sizeof(context));
context.ContextFlags=CONTEXT_FULL;
GetThreadContext(pi.hThread,&context);
if(ReadRemoteByte(pi.hProcess,context.Eip)==0xC7)
{
vms.context=&context;
vms.pi=π
RunVM(vms);
SetThreadContext(pi.hThread,&context);
}
else
{
TerminateProcess(pi.hProcess,0);
exit(0);
}
}
}
if(de.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT)
exit(0);
ContinueDebugEvent(de.dwProcessId,de.dwThreadId,DBG_CONTINUE);
}
}
return;
}
int main()
{
if(!IsDebuggerPresent())
{
CreateSubProcess();
return 0;
}
printf("I am sub process\n");
int t=0;
_asm
{
_emit 0xC7 //pushImm 0xDDCCBBAA
_emit 0x17
_emit 0xAA
_emit 0xBB
_emit 0xCC
_emit 0xDD
_emit 0xC7 //pushImm 0xEECCBBAA
_emit 0x17
_emit 0xAA
_emit 0xBB
_emit 0xCC
_emit 0xEE
pop eax
pop ebx
mov t,eax
}
printf("the value of eax: %X\n",t); //eax=0xEECCBBAA
MessageBoxA(NULL,"GOT","YES",MB_OK);
return 0;
}
这里的VM只实现了最简单的两条指令,所以等着大佬们完善,这里主要是阐述一种操作,只是个雏形罢了。
最后再说一下在反调试处的作用,由于程序分为了调试进程与非调试进程,那么当你用ida等调试器执行时则会使得程序误认为自己为子进程。
就会开始按正常操作执行代码,当执行到那个无法识别的指令时,则会触发异常,传递给ida,然而ida并不是flag父进程,不知道如何处理这些opcode
所以则会执行失败,使得做题人无法理解发生了啥,所以一定要将创建子进程和VM处理代码藏起了,才可以发挥最大效果