SMC 代码自修改技术浅析

0x00 概述

  在计算机科学中,自修改代码是在执行时改变自己指令的代码,通常是减少指令路径长度并提高性能,或者简单地减少重复类似的代码,从而简化维护。自我修改是“标志设置”和条件程序分支方法的替代方法,主要用于减少条件需要测试的次数。该术语通常仅适用于自我修改是有意的代码,而不是在代码由于诸如缓冲区溢出之类的错误而自动修改的情况下。(via wiki.)

  在SMC的历史上,此技术早期被用于减少有限内存的使用,而后用于优化状态依赖型循环、隐藏80s基于磁盘的程序复制保护指令等。其中,隐藏代码以防止逆向工程是众多用法之一,也是这篇博客的主题。

0x01 Reverse中的smc

之前看过一篇博客(-用C/C++实现SMC动态代码加密技术:http://blog.csdn.net/orbit/article/details/1497457),文中对3种对象使用smc来实现加密解密:

  1. 代码段(section 或称节)
  2. 函数体
  3. 代码片段

概括以上,使用smc的充分条件:能找到用户想要加解密的代码的位置、大小并允许操作。

  • 代码段的位置最容易寻找,pe分析就能知道节的起始地址及其大小,
    但容易暴露加密位置。
  • 函数体的文件偏移通过打印此函数的起始地址再计算相对于段首的偏移量而得到,而函数体大小文中使用的方法是将两个相邻的函数体相减,得到一个相对大小。
    缺点为不是很稳定,因为编译器可能进行其他修改,不过使用反汇编会较为轻松点。
  • 代码片段的位置,”目前最广泛采用的方法是使用对某个特征代码序列的查找来定位开始位置”。
    嗯,很复杂。

得到了地址与大小之后,准备工作已经完成,接下来就是调用被smc后的函数基本流程:

1
DecryptSMCFunc(...)--> SMCedFunc(...)--> EncryptSMCFunc(...)

需要先将被smc的对象解密,再调用解密后的函数,最后加密还原。
DecryptSMCFunc(…)和EncryptSMCFunc(…)至少需要两个参数: VirtualAddr && Size。

最后,还需要另外的程序来进行support,此程序的目的是将本地文件SMC,否则主程序解密无任何意义。为此在辅助程序中需要文件偏移和大小作为参数,调用EncryptSMCFunc2(…)。

0x02 Get Addr and Size

01. 代码段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
bool GetSectionPointer(void *pModuleBase,const char *lpszSection,void** ppPos,LPDWORD lpSize)
{
IMAGE_DOS_HEADER *pDosHead;
IMAGE_FILE_HEADER *pPEHead;
IMAGE_SECTION_HEADER *pSection;
*ppPos = NULL;
*lpSize = 0;
if(::IsBadReadPtr(pModuleBase,sizeof(IMAGE_DOS_HEADER)) || ::IsBadReadPtr(lpszSection,8))
return false;
if(strlen(lpszSection) >= 16)
return false;
char szSecName[16];
memset(szSecName,0,16);
strncpy(szSecName,lpszSection,IMAGE_SIZEOF_SHORT_NAME);
unsigned char *pszModuleBase = (unsigned char *)pModuleBase;
pDosHead = (IMAGE_DOS_HEADER *)pszModuleBase;
//跳过DOS头不和DOS stub代码,定位到PE标志位置
DWORD Signature = *(DWORD *)(pszModuleBase + pDosHead->e_lfanew);
if(Signature != IMAGE_NT_SIGNATURE) //"PE\0\0"
return false;
//定位到PE header
pPEHead = (IMAGE_FILE_HEADER *)(pszModuleBase + pDosHead->e_lfanew + sizeof(DWORD));
int nSizeofOptionHeader;
if(pPEHead->SizeOfOptionalHeader == 0)
nSizeofOptionHeader = sizeof(IMAGE_OPTIONAL_HEADER);
else
nSizeofOptionHeader = pPEHead->SizeOfOptionalHeader;
bool bFind = false;
//跳过PE header和Option Header,定位到Section表位置
pSection = (IMAGE_SECTION_HEADER *)((unsigned char *)pPEHead + sizeof(IMAGE_FILE_HEADER) + nSizeofOptionHeader);
for(int i = 0; i < pPEHead->NumberOfSections; i++)
{
if(!strncmp(szSecName, (const char*)pSection[i].Name,IMAGE_SIZEOF_SHORT_NAME)) //比较段名称
{
*ppPos = (void *)(pszModuleBase + pSection[i].VirtualAddress);//计算实际虚地址
*lpSize = pSection[i].SizeOfRawData;//整段大小
bFind = true;
break;
}
}
return bFind;
}

02. 函数体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int VAtoFileOffset(void *pModuleBase,void *pVA)
{
IMAGE_DOS_HEADER *pDosHead;
IMAGE_FILE_HEADER *pPEHead;
IMAGE_SECTION_HEADER *pSection;
if(::IsBadReadPtr(pModuleBase,sizeof(IMAGE_DOS_HEADER)) || ::IsBadReadPtr(pVA,4))
return -1;
unsigned char *pszModuleBase = (unsigned char *)pModuleBase;
pDosHead = (IMAGE_DOS_HEADER *)pszModuleBase;
//跳过DOS头不和DOS stub代码,定位到PE标志位置
DWORD Signature = *(DWORD *)(pszModuleBase + pDosHead->e_lfanew);
if(Signature != IMAGE_NT_SIGNATURE) //"PE\0\0"
return -1;
unsigned char *pszVA = (unsigned char *)pVA;
int nFileOffset = -1;
//定位到PE header
pPEHead = (IMAGE_FILE_HEADER *)(pszModuleBase + pDosHead->e_lfanew + sizeof(DWORD));
int nSizeofOptionHeader;
if(pPEHead->SizeOfOptionalHeader == 0)
nSizeofOptionHeader = sizeof(IMAGE_OPTIONAL_HEADER);
else
nSizeofOptionHeader = pPEHead->SizeOfOptionalHeader;
//跳过PE header和Option Header,定位到Section表位置
pSection = (IMAGE_SECTION_HEADER *)((unsigned char *)pPEHead + sizeof(IMAGE_FILE_HEADER) + nSizeofOptionHeader);
for(int i = 0; i < pPEHead->NumberOfSections; i++)
{
if(!strncmp(".text", (const char*)pSection[i].Name,5)) //比较段名称
{
//代码文件偏移量 = 代码内存虚拟地址 - (代码段内存虚拟地址 - 代码段的文件偏移)
nFileOffset = pszVA - (pszModuleBase + pSection[i].VirtualAddress - pSection[i].PointerToRawData);
break;
}
}
return nFileOffset;
}
1
2
int nFileOffset = VAtoFileOffset((void *)::GetModuleHandle(NULL),(void *)CalcRegCode);
int nFuncSize = ((char *)CalcRegCodeEnd - (char *)CalcRegCode);

计算nFuncSize可以读到return的opcode:0xC3,但这样做也可能会出差错,因为ret后也许增加了其他指令。
而缺点依然保留,nFileOffset和nFuncSize需要”隐蔽”打印出来。
其实可以将主程序编译链接后进行动态调试,这样就可以做到完全隐藏。

03. 代码片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int FindCodeTag(void *pStartAddr, unsigned long *pTagLoc, unsigned long lTagValue, int nSerachLength)
{
int nPos = -1;
int i = 0;
unsigned char *pAddr = (unsigned char *)pStartAddr;
while(i < nSerachLength)
{
if((*pAddr == 0xC7) && (*(pAddr + 1) == 0x05))//查找mov指令
{
unsigned long *Loc = (unsigned long *)((unsigned char*)pAddr + 2);
if(*Loc == (unsigned long)pTagLoc)//此处的数据*Loc就是全局静态变量的地址
{
unsigned long *Val = (unsigned long *)((unsigned char*)pAddr + 6);
if(*Val == lTagValue)//此处的数据*Val就是常数lTagValue值
{
nPos = i;
break;//find begin tag
}
}
}
pAddr++;
i++;
}
return nPos;
}
1
2
3
4
5
6
7
8
int nStartPos = FindCodeTag((void *)CalcRegCode,&slStartSignVar,0x5A5A5A5A,1000);
nStartPos += nMovCodeLength;
int nEndPos = FindCodeTag((void *)CalcRegCode,&slEndSignVar,0x61616161,1000);
int nSize = nEndPos - nStartPos;
char *pCodeStart = (char *)CalcRegCode;
pCodeStart += nStartPos;
int nFileOffset = VAtoFileOffset((void *)::GetModuleHandle(NULL),(void *)pCodeStart);

0x03 SupportFunc

1
2
3
4
5
6
7
8
file.seekg(lfileOffset,ios_base::beg);
ios::pos_type mark = file.tellp();
file.read(pBuf,nBlockLen);
EncryptBlock(pBuf,nBlockLen,0x5A);
file.seekp(mark);
file.write(pBuf,nBlockLen);

核心部分的实现