1、源码免杀
1.1 定位产生特征的源码
定位文件特征
- 1、根据MyCCL的特征码定位工具,定位出有特征的地址
- 2、根据VS的反汇编窗口,输入有特征的地址得到特征地址与源码的关系
- 3、插入MessageBox,然后定位出特征码离哪个MessageBox最近,并在附近以更高密度安插MessageBox,最终定位出产生特征码的源码位置。
定位行为特征
- 1、启发式检测方法:特征码定位突破
- 2、虚拟机检查方法:通过控制程序仅执行部分代码,在远程线程注入前插入MessageBox,起到断点作用。
- 3、基于HOOK技术的沙盒检查方法:同2、方法。
1.2 基于源码的特征修改
- 变换编译器与编译选项
- 添加垃圾代码
- 等价语法替换原有语句
- 添加汇编花指令
2、C++壳的编写
壳的原理与蠕虫病毒感染的原理一致
2.1 壳的运行流程
- 给相关程序中添加一个足够大的区段,并将负责解密解压的代码(Stub)与相关配置文件写入这个新添加的空区段中。
- 修改宿主程序的入口点信息,使宿主程序在运行时先执行Stub部分的代码
- 修改新区段中的配置信息,使Stub部分的代码跳转到真正的宿主程序入口点。
图:加壳前与加壳后的状态
2.2 设计纯C++编写的壳
设计壳要注意以下几个重点:
- Stub程序一定要拥有重定位表,否则无法执行重定位操作。
- 修改编译选项编译出较小体积的Stub程序,并将代码段和数据段合并在一起,这样可以将Stub程序中的代码段与数据段直接复制到宿主程序中。
- 使用动态获取API地址的方法来避免直接与PE中的导入表产生纠葛。
2.3 C++壳框架
- 预处理部分:
- 1、读取目标文件并确定文件体积;
- 2、读取代码段的相关信息;
- 3、将代码段属性修改为可读、可写、可执行;
- 4、对代码段进行简单的异或加密处理;
- 植入Stub:
- 1、读取保存在资源中的Stub部分,并计算体积;
- 2、依据此体积添加区段;
- 3、将Stub复制到新区段,对Stub进行重定位等一系列处理;
- 4、修改入口点到Stub处,并将原入口点传递给Stub,留以备用
- Stub执行
- 1、读取被加密区域的起始及结束偏移;
- 2、使用异或算法解密相应区域的代码;
- 3、弹出对话框提示相关信息;
- 4、根据保存下来的OEP信息,执行原程序;
图:壳的执行流程
2.4 配置工程
第1行代码指定入口点函数指定为StubEntryPoint(),第2、3行代码将.data、.rdata这两个区段合并到.text区段中,第4行的意思是修改.text区段的属性为可读、可写、可执行。
Stub项目中的Dllmain.cpp:
#pragma comment(linker, "/entry:\"StubEntryPoint\"") // 指定程序入口函数为StubEntryPoint()
#pragma comment(linker, "/merge:.data=.text") // 将.data合并到.text
#pragma comment(linker, "/merge:.rdata=.text") // 将.rdata合并到.text
#pragma comment(linker, "/section:.text,RWE") // 将.text段的属性设置为可读、可写、可执行
2.5 编写Stub部分
- 问题1:Stub部分的正常执行需要一些必要的信息及参数,要根据Stub部分将要完成的功能,准备好运行时用到的具体数据。
// 声明一个导出的全局变量,用以保存传递给Stub部分的参数
typedef struct _GLOBAL_PARAM
{
BOOL bShowMessage; // 是否显示解密信息
DWORD dwOEP; // 程序入口点
PBYTE lpStartVA; // 起始虚拟地址(被异或加密区)
PBYTE lpEndVA; // 结束虚拟地址(被异或加密区)
}GLOBAL_PARAM,*PGLOBAL_PARAM;
extern "C" __declspec(dllexport) GLOBAL_PARAM g_stcParam;
通过以上代码,定义GLOBAL_PARAM,*PGLOBAL_PARAM的结构体类型,并用GLOBAL_PARAM类型定义一个被导出的全局变量g_stcParam。
- 问题2:Stub部分以一个DLL格式文件的形式存在,会执行很多引导代码。为了让Stub部分能健壮稳定地运行,所以要通过修改编译选项来自定义一个入口函数,这样程序在编译时就会忽略那些引导代码。
// 指定自定义入口函数StubEntryPoint()
#pragma comment(linker, "/entry:\"StubEntryPoint\"") // 指定程序入口函数为StubEntryPoint()
// 调用壳的主体部分
void start()
{
// 1. 初始化所有API
// 2. 解密宿主程序
// 3. 询问是否执行解密后的程序
// 4. 跳转到OEP
__asm jmp g_stcParam.dwOEP;
}
//定义一个裸函数,此函数将作为入口函数首先执行
void __declspec(naked) StubEntryPoint()
{
__asm sub esp,0x50; // 抬高栈顶,提高兼容性
start(); // 执行壳的主体部分
__asm add esp,0x50; // 平衡堆栈
// 主动调用ExitProcess函数退出进程可以解决一些兼容性问题
if ( g_funExitProcess )
{
g_funExitProcess(0);
}
__asm retn;
}
- 问题3:加壳的程序会丢弃掉IAT与导入表信息,因此直接调用API是不可行的,所以要求程序有一套能自动获取API函数地址的替代方案。
使用动态加载API的方法解决会碰到获取GetProcAddress()函数地址的问题,GetProcAddress()函数是从系统文件kernel32.dll中导出的,如果能找到kernel32.dll的加载基址,根据它的导出表就一定能找到GetProcAddress()函数的地址。
获取Kernel32.dll加载基址
获取Kernel32.dll加载基址的公开方法有3种:
- 1.通过特征匹配的暴力搜索
- 2.利用系统的SEH机制找到kernel32.dll并搜索出加载基址
- 3.通过线程环境块TEB的信息逐步找到Kernel32.dll的加载基址
其中代码最少的是最后一种方法。
- 1)通过段选择FS在内存中得到当前的线程环境块TEB的地址;
- 2)TEB偏移为0x30处是指进程环境块PEB的指针;
- 3)PEB偏移为0x0C处是指向PEB_LDR_DATA结构的指针;
- 4)PEB_LDR_DATA偏移为0x1C处是模拟初始化链表的头指针InInitializationOrderModuleList;
- 5)InInitializationOrderModuleList中按顺序存放着此进程初始化模块的信息,
- 在NT 5.X的内核中,第一个节点存放的是ntdll.dll的基址;第二个节点存放的是kernel32.dll的基址;
- 在NT 6.1内核中,其第二个节点存放的是KernelBase.dll的基址(KernelBase.dll中包含着kernel32.dll中绝大多数常用API的另一份实现,其中就包含GetProcAddress()函数);
图:获取Kernel32.dll基址的流程
获取kernel32.dll基址的代码:
DWORD GetKernel32Base()
{
DWORD dwKernel32Addr = 0;
__asm
{
push eax
mov eax,dword ptr fs:[0x30] // eax = PEB的地址
mov eax,[eax+0x0C] // eax = 指向PEB_LDR_DATA结构的指针
mov eax,[eax+0x1C] // eax = 模块初始化链表的头指针InInitializationOrderModuleList
mov eax,[eax] // eax = 列表中的第二个条目
mov eax,[eax+0x08] // eax = 获取到的Kernel32.dll基址(Win7下获取的是KernelBase.dll的基址)
mov dwKernel32Addr,eax
pop eax
}
return dwKernel32Addr;
}
遍历导出表
找出GetProcAddress函数地址()
DWORD GetGPAFunAddr()
{
DWORD dwAddrBase = GetKernel32Base();
// 1. 获取DOS头、NT头
PIMAGE_DOS_HEADER pDos_Header;
PIMAGE_NT_HEADERS pNt_Header;
pDos_Header = (PIMAGE_DOS_HEADER)dwAddrBase;
pNt_Header = (PIMAGE_NT_HEADERS)(dwAddrBase + pDos_Header->e_lfanew);
// 2. 获取导出表项
PIMAGE_DATA_DIRECTORY pDataDir;
PIMAGE_EXPORT_DIRECTORY pExport;
pDataDir = pNt_Header->OptionalHeader.DataDirectory+IMAGE_DIRECTORY_ENTRY_EXPORT;
pExport = (PIMAGE_EXPORT_DIRECTORY)(dwAddrBase + pDataDir->VirtualAddress);
// 3. 获取导出表详细信息
PDWORD pAddrOfFun = (PDWORD)(pExport->AddressOfFunctions + dwAddrBase);
PDWORD pAddrOfNames = (PDWORD)(pExport->AddressOfNames + dwAddrBase);
PWORD pAddrOfOrdinals = (PWORD) (pExport->AddressOfNameOrdinals + dwAddrBase);
// 4. 处理以函数名查找函数地址的请求,循环获取ENT中的函数名,并与传入值对比对,如能匹配上
// 则在EAT中以指定序号作为索引,并取出其地址值。
DWORD dwFunAddr;
for (DWORD i=0; i<pExport->NumberOfNames; i++)
{
PCHAR lpFunName = (PCHAR)(pAddrOfNames[i]+dwAddrBase);
if ( !strcmp(lpFunName, "GetProcAddress") )
{
dwFunAddr = pAddrOfFun[pAddrOfOrdinals[i]] + dwAddrBase;
break;
}
if ( i == pExport->NumberOfNames-1 )
return 0;
}
return dwFunAddr;
}
2.6 编写加壳部分
加壳程序要多做两件事,一是实现独特的参数写入方式,另一个就是需要对stub的代码段进行重定位操作。
1.设计加壳部分
建立项目时将作为加壳的部分独立成了一个DLL,为了方便调用,需要导出一个或多个函数。
#ifdef A1PACK_BASE_EXPORTS
#define A1PACK_BASE_API __declspec(dllexport)
#else
#define A1PACK_BASE_API __declspec(dllimport)
#endif
// 声明一个导出的API,共界面程序调用执行加壳操作
A1PACK_BASE_API bool A1Pack_Base(LPWSTR strPath,bool bShowMsg);
通过以上代码可知,导出了一个名为A1Pack_Base()的函数,此函数的两个参数分别为需要加壳的文件路径及加壳后的程序是否弹出提示解密成功的对话框。涉及大量的PE操作,由一个CProcessingPE()类完成。
2.读取目标文件相关信息
获取文件信息,并映射进内存中
- CreateFile:读取文件
- GetFileSize:获取文件大小
- VirtualAlloc:开辟内存空间
- ReadFile:将文件写入开辟的内存空间中
获取目标文件关键PE信息
// 关键PE信息
typedef struct _PE_INFO
{
DWORD dwOEP; // 入口点
DWORD dwImageBase; // 映像基址
PIMAGE_DATA_DIRECTORY pDataDir; // 数据目录指针
IMAGE_DATA_DIRECTORY stcExport; // 导出目录
PIMAGE_SECTION_HEADER pSectionHeader; // 区段表头部指针
}PE_INFO,*PPE_INFO;
// 获取关键PE信息的函数
BOOL CProcessingPE::GetPeInfo(LPVOID lpImageData, DWORD dwImageSize, PPE_INFO pPeInfo)
{
// 1、判断映像指针是否有效
if ( m_stcPeInfo.dwOEP )
{
CopyMemory(pPeInfo, &m_stcPeInfo, sizeof(PE_INFO));
return true;
}
else
{
if ( !lpImageData ) return false;
m_dwFileDataAddr = (DWORD)lpImageData;
m_dwFileDataSize = dwImageSize;
}
// 2. 获取基本信息
// 2.1 获取DOS头、NT头
m_pDos_Header = (PIMAGE_DOS_HEADER)lpImageData;
m_pNt_Header = (PIMAGE_NT_HEADERS)((DWORD)lpImageData+m_pDos_Header->e_lfanew);
// 2.2 获取OEP
m_stcPeInfo.dwOEP = m_pNt_Header->OptionalHeader.AddressOfEntryPoint;
// 2.3 获取映像基址
m_stcPeInfo.dwImageBase = m_pNt_Header->OptionalHeader.ImageBase;
// 2.4 获取关键数据目录表的内容
PIMAGE_DATA_DIRECTORY lpDataDir = m_pNt_Header->OptionalHeader.DataDirectory;
m_stcPeInfo.pDataDir = lpDataDir;
CopyMemory(&m_stcPeInfo.stcExport, lpDataDir+IMAGE_DIRECTORY_ENTRY_EXPORT, sizeof(IMAGE_DATA_DIRECTORY));
// 2.5 获取区段表与其他详细信息
m_stcPeInfo.pSectionHeader = IMAGE_FIRST_SECTION(m_pNt_Header);
// 3. 检查PE文件是否有效
if ( (m_pDos_Header->e_magic!=IMAGE_DOS_SIGNATURE) || (m_pNt_Header->Signature!=IMAGE_NT_SIGNATURE) )
{
// 这不是一个有效的PE文件
return false;
}
// 4. 传出处理结果
CopyMemory(pPeInfo, &m_stcPeInfo, sizeof(PE_INFO));
return true;
}
3.读取代码段信息并处理
读取代码端信息的第一步是确定哪个区段是代码段。一般可读但不可写的区段为代码段。书里的例子默认第一个区段为代码段。
// 4. 获取目标文件代码段的起始结束信息
// 读取第一个区段的相关信息,并将其加密(默认第一个区段为代码段)
PBYTE lpStart = (PBYTE)(stcPeInfo.pSectionHeader->PointerToRawData+(DWORD)lpFileImage);
PBYTE lpEnd = (PBYTE)((DWORD)lpStart+stcPeInfo.pSectionHeader->SizeOfRawData);
PBYTE lpStartVA = (PBYTE)(stcPeInfo.pSectionHeader->VirtualAddress+stcPeInfo.dwImageBase);
PBYTE lpEndVA = (PBYTE)((DWORD)lpStartVA+stcPeInfo.pSectionHeader->SizeOfRawData);
加密函数:
// 5. 对文件进行预处理(加密、给第一个区段附加上可写属性)
void Pretreatment(PBYTE lpCodeStart, PBYTE lpCodeEnd, PE_INFO stcPeInfo)
{
// 1. 加密指定区域
while ( lpCodeStart<lpCodeEnd )
{
*lpCodeStart ^= 0xA1;
*lpCodeStart += 0x88;
lpCodeStart++;
}
// 2. 给第一个区段附加上可写属性
PDWORD pChara = &(stcPeInfo.pSectionHeader->Characteristics);
*pChara = *pChara|IMAGE_SCN_MEM_WRITE;
}
以上代码除了将代码段加密外,还给代码段附加上了可写属性,因为stub在植入宿主主程序后需要对其代码段进行写操作(解密),如果代码段是不可写的,那么将导致程序崩溃。
4.根据Stub体积添加新区段
源码中使用添加区段的方式来开辟新的空间,首先将前面已经作为资源的Stub部分释放出来。
// 1. 在资源中读取文件内容
HRSRC hREC = NULL; // 资源对象
HGLOBAL hREC_Handle = NULL; // 资源句柄
DWORD dwStubSize = NULL; // 文件大小
LPVOID lpResData = NULL; // 资源数据指针
HMODULE hModule = GetModuleHandle(L"A1Pack_Base.dll");
if ( !(hREC=FindResource(hModule, MAKEINTRESOURCE(IDR_STUB1), L"STUB")) ) return false;
if ( !(hREC_Handle=LoadResource(hModule, hREC)) ) return false;
// 资源赋值
if ( !(lpResData=LockResource(hREC_Handle)) ) return false;
// 资源大小
if ( !(dwStubSize=SizeofResource(hModule, hREC)) ) return false;
添加区段函数代码
代码没有考虑到附加数据等细节
PVOID CProcessingPE::AddSection(LPCTSTR strName, DWORD dwSize, DWORD dwChara, PIMAGE_SECTION_HEADER pNewSection, PDWORD lpSize)
{
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(m_pNt_Header);
// 1. 获取基本信息
DWORD dwDosSize = m_pDos_Header->e_lfanew;
DWORD dwPeSize = sizeof(IMAGE_NT_HEADERS32);
DWORD dwStnSize = m_pNt_Header->FileHeader.NumberOfSections * sizeof(IMAGE_SECTION_HEADER);
DWORD dwHeadSize = dwDosSize+dwPeSize+dwStnSize;
// 2. 在区段表中加入新区段的信息
// 2.1 获取基本信息
CHAR szVarName[7] = {0};
DWORD dwFileAlign = m_pNt_Header->OptionalHeader.FileAlignment; // 文件粒度
DWORD dwSectAlign = m_pNt_Header->OptionalHeader.SectionAlignment; // 区段粒度
WORD dwNumOfsect = m_pNt_Header->FileHeader.NumberOfSections; // 区段数目
// 2.2 获取最后一个区段的信息
IMAGE_SECTION_HEADER stcLastSect = {0};
CopyMemory(&stcLastSect, &pSectionHeader[dwNumOfsect-1], sizeof(IMAGE_SECTION_HEADER));
// 2.3 根据区段粒度计算相应地址信息
DWORD dwVStart = 0; // 虚拟地址起始位置
DWORD dwFStart = stcLastSect.SizeOfRawData + stcLastSect.PointerToRawData; // 文件地址起始位置
if ( stcLastSect.Misc.VirtualSize%dwSectAlign )
dwVStart = (stcLastSect.Misc.VirtualSize / dwSectAlign+1) * dwSectAlign + stcLastSect.VirtualAddress;
else
dwVStart = (stcLastSect.Misc.VirtualSize / dwSectAlign ) * dwSectAlign + stcLastSect.VirtualAddress;
DWORD dwVirtualSize = 0; // 区段虚拟大小
DWORD dwSizeOfRawData = 0; // 区段文件大小
if ( dwSize%dwSectAlign)
dwVirtualSize = (dwSize / dwSectAlign+1) * dwSectAlign;
else
dwVirtualSize = (dwSize / dwSectAlign ) * dwSectAlign;
if ( dwSize%dwFileAlign )
dwSizeOfRawData = (dwSize / dwFileAlign+1) * dwFileAlign;
else
dwSizeOfRawData = (dwSize / dwFileAlign ) * dwFileAlign;
WideCharToMultiByte(CP_ACP, NULL, strName, -1, szVarName, _countof(szVarName), NULL, FALSE);
// 2.4 组装一个新的区段头
IMAGE_SECTION_HEADER stcNewSect = {0};
CopyMemory(stcNewSect.Name, szVarName, 7); // 区段名称
stcNewSect.Misc.VirtualSize = dwVirtualSize; // 虚拟大小
stcNewSect.VirtualAddress = dwVStart; // 虚拟地址
stcNewSect.SizeOfRawData = dwSizeOfRawData; // 文件大小
stcNewSect.PointerToRawData = dwFStart; // 文件地址
stcNewSect.Characteristics = dwChara; // 区段属性
// 2.5 写入指定位置
CopyMemory( (PVOID)((DWORD)m_dwFileDataAddr+dwHeadSize), &stcNewSect, sizeof(IMAGE_SECTION_HEADER) );
// 3. 修改区段数目字段NumberOfSections
m_pNt_Header->FileHeader.NumberOfSections++;
// 4. 修改PE文件的景象尺寸字段SizeOfImage
m_pNt_Header->OptionalHeader.SizeOfImage += dwVirtualSize;
// 5. 返回新区段的详细信息、大小,以及可直接访问的地址
CopyMemory(pNewSection, &stcNewSect, sizeof(IMAGE_SECTION_HEADER));
*lpSize = dwSizeOfRawData;
return (PVOID)(m_dwFileDataAddr+dwFStart);
}
5.对Stub进行重定位处理并写入配置信息
Stub程序在宿主程序上执行,那么在植入前就必须对其做重定位操作。
新加载的地址 = (新区段RVA - Stub的".Text区段RVA")+宿主程序映像基址
执行重定位操作的代码:
未考虑修复类型的问题,如果要提高兼容性,应该对3种重定位类型进行区别对待。
void CProcessingPE::FixReloc(DWORD dwLoadImageAddr)
{
// 1. 获取映像基址与代码段指针
DWORD dwImageBase;
PVOID lpCode;
dwImageBase = m_pNt_Header->OptionalHeader.ImageBase;
lpCode = (PVOID)( (DWORD)m_dwFileDataAddr + RVAToOffset(m_pNt_Header->OptionalHeader.BaseOfCode) );
// 2. 获取重定位表在内存中的地址
PIMAGE_DATA_DIRECTORY pDataDir;
PIMAGE_BASE_RELOCATION pReloc;
pDataDir = m_pNt_Header->OptionalHeader.DataDirectory+IMAGE_DIRECTORY_ENTRY_BASERELOC;
pReloc = (PIMAGE_BASE_RELOCATION)((DWORD)m_dwFileDataAddr + RVAToOffset(pDataDir->VirtualAddress));
// 3. 遍历重定位表,并对目标代码进行重定位
while ( pReloc->SizeOfBlock && pReloc->SizeOfBlock < 0x100000 )
{
// 3.1 取得重定位项TypeOffset与其数量
PWORD pTypeOffset = (PWORD)((DWORD)pReloc+sizeof(IMAGE_BASE_RELOCATION));
DWORD dwCount = (pReloc->SizeOfBlock-sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
// 3.2 循环检查重定位项
for ( DWORD i=0; i<dwCount; i++ )
{
if ( !*pTypeOffset ) continue;
// 3.2.1 获取此重定位项指向的指针
DWORD dwPointToRVA = (*pTypeOffset&0x0FFF)+pReloc->VirtualAddress;
PDWORD pPtr = (PDWORD)(RVAToOffset(dwPointToRVA)+(DWORD)m_dwFileDataAddr);
// 3.2.2 计算重定位增量值
DWORD dwIncrement = dwLoadImageAddr - dwImageBase;
// 3.2.3 修复需重定位的地址数据
*((PDWORD)pPtr) += dwIncrement;
pTypeOffset++;
}
// 3.3 指向下一个重定位块,开始另一次循环
pReloc = (PIMAGE_BASE_RELOCATION)((DWORD)pReloc + pReloc->SizeOfBlock);
}
}
写入stub配置信息
将关键的OEP信息、配置信息写入Stub的全局变量g_stcParam中,GetExpVarAddr()函数获取导出的g_stcParam地址,并向其地址中写入我们已经组织好的结构体。
获取g_stcParam地址的关键代码:
// 5. 写入配置参数
// 5.1 获取Stub的导出变量地址
PVOID lpPatam = objProcPE.GetExpVarAddr(L"g_stcParam");
// 5.2 保存配置信息到Stub中
CopyMemory(lpPatam,&stcParam,sizeof(GLOBAL_PARAM));
// 获取导出变量地址
PVOID CProcessingPE::GetExpVarAddr(LPCTSTR strVarName)
{
// 1、获取导出表地址,并将参数strVarName转为ASCII形式,方便对比查找
CHAR szVarName[MAX_PATH] = {0};
PIMAGE_EXPORT_DIRECTORY lpExport = (PIMAGE_EXPORT_DIRECTORY)(m_dwFileDataAddr + RVAToOffset(m_stcPeInfo.stcExport.VirtualAddress));
WideCharToMultiByte(CP_ACP, NULL, strVarName, -1, szVarName, _countof(szVarName), NULL, FALSE);
// 2、循环读取导出表输出项的输出函数,并依次与szVarName做比对,如果相同,则取出相对应的函数地址
for (DWORD i=0; i<lpExport->NumberOfNames; i++)
{
PDWORD pNameAddr = (PDWORD)(m_dwFileDataAddr+RVAToOffset(lpExport->AddressOfNames+i));
PCHAR strTempName = (PCHAR)(m_dwFileDataAddr + RVAToOffset(*pNameAddr));
if ( !strcmp(szVarName, strTempName) )
{
PDWORD pFunAddr = (PDWORD)(m_dwFileDataAddr+RVAToOffset(lpExport->AddressOfFunctions+i));
return (PVOID)(m_dwFileDataAddr + RVAToOffset(*pFunAddr));
}
}
return 0;
}
6.将Stub复制到区段中
讲Stub的.text段内容复制到新区段中,并将目标文件的OEP指向Stub的入口处就可以了。因为Stub的代码段被移动了位置,其RVA已经发生了变化。因此将这些因素考虑进去后。可以得出以下公式:
新OEP = (Stub的OEP - Stub代码段的RVA) + Stub所在新区段的RVA
2.7 编写界面部分
界面部分涉及到A1Pack_Base.dll的调用。这里用得很好,记录一下。
动态库调用DLL中的函数,但是增加了头文件。方便用VS调试。
#pragma once
#define A1PACK_BASE_EXPORTS
#include "../A1Pack_Base/A1Pack_Base.h"
#ifdef _DEBUG
#pragma comment(lib, "../Debug/A1Pack_Base.lib")
#else
#pragma comment(lib, "../Release/A1Pack_Base.lib")
#endif
打造免杀壳
- 导入表加密
- 代码混淆与代码乱序
- 附加驱动