做壳很容易,有点做病毒知识的人都可以编写出壳来。但强度就有所差别了。
什么才是凶猛的壳?
我觉的是这样的 "猛壳 = 识别 + 重定位".识别用于让壳更了解要保护的程序。重定位为了可以保证保护程序的正常运行。如果能把识别与重定位做好。那么很多新的玩意就都可以玩了。如果你能把一个壳做成一个2进制文件的2次编译器。这个编译器的输入是以编译好的程序。输出是进行重编译的程序。那么这个壳因该相当的犀利。不过事与愿违。要完美的解决以上技术问题。有相当大的难度。我们也许不能把这个程序进行这样的重编译。但是局部的代码因该还是可以的。例如"VMP"不正是这样的产物吗? 写篇文章把我最近搞的一个函数识别的小算法奉献给朋友们。
<算法流程>
0.单函数识别
从一个地址开始,在未知长度的情况下,识别出这个函数的长度。
流程算法如下:
1) 从当前位置开始进行反汇编
2) 记录所有跳转流程指令
3) 遇到ret或者retn指令后,回溯整个跳转流程链表
4) 如果找到jcc则判断是否跳转后的地址是否大于当前的指令地址
5-1) 大于,则跳转到流程,进入预分析结尾流程
5-2) 无大于的情况,此函数分析完毕,返回
预分析结尾:
这里鉴别函数帧也可能造成不稳定,有些函数在ret之后还有一些指令在这些指令之后必定有一个JMP跳转到最后一个ret或者之前利用此来进行预分析结尾
这里出现种情况:
1) 找到上述那个最后的JMP则将此视为结尾
2) 如果找到nop视为结尾(用户可以指定此选项,默认存在)
3) 如果找到int3视为结尾(用户可以指定此选项,默认存在)
4) 如果找到视为结尾(用户可以指定此选项,默认存在)
1.交叉引用
从一个函数开始,检查它的对外跳转的语句以便达到另一个函数,如此循环识别出代码段中的所有被直接引用的函数体。
识别交叉引用采用三遍分析,并在分析过程中记录每天指令,并设置是否可以HOOK(用于代码乱序)
流程算法如下:
第一遍分析
1) 依次对所有指令进行分析,设定指令是否可以交换,并进行初步HOOK分析刺探,将通过偏移的的跳转指令进行识别,并鉴别HOOK成功率,并填充这条指令对标志寄存器的影响和通用寄存器的影响
2) 通过单函数鉴别 (上述已经阐述了其算法)函数确立入口函数的长度
3) 再次遍历入口函数,通过分析入口函数的CALL指令,分析出其他函数
4) 循环,2过程直到把映射中所有的函数都鉴别出来
5) 进行链表排序(地址由低到高)
6) 将链表与映射内存由高到低依次进行对比,找出未找到的代码块,并使用,1,2方法进行鉴别将无法分析的代码识为数据
第二遍分析
1) 遍历函数链
2) 在识别过程中,鉴别出一个函数开辟的局部变量所需的空间,具体监视sub esp, XXX指令
3) 在遍历过程中如果发现有指令直接对全局变量进行引用,记录其访问地址
4) 如果遇到跳转指令,则设置它目标地址指令的跳转引用链表
第三遍分析
1) 遍历函数链
2) 找到所有动态跳转指令,并对它进行反馈分析,直到找出动态跳转寄存器值的合成结果,并进行运算
这里反馈的结束算法是:
查看当前跳转指令,是否由引用指令
如果有则遍历这个引用链
如果没有则向上分析条指令,如果没有存在可以影响寄存器的mov,lea指令,则直接视此指令为不可HOOK的指令,如果动态跳转出现使用两个寄存器做内存跳转的直接视为不可HOOK的指令。
当然这里有些步骤是可以合并的。在实际CODE中我将上面三遍合成为两遍原则。
2.识别代码段的数据与代码
在理论上代码与数据是不可分的,通过分析将代码与数据进行分离。这也是难度最大的地方。
这里给出一些代码。
很多朋友要问为什么要再次鉴别这些东西。从交叉引用就可以识别为什么还要继续在重新识别一遍
原因很简单:为了可以保证所有函数和无用区域都作为信息传递给壳。有些动态调用的从交叉引用很难分析得出。在引用链上没有。所以要采取这个方案重新把他找出来。还有那些函数与函数之间的区域,这些区域你没感觉到对于做壳来说很珍贵吗?发挥你想象力去利用这些空白区域。。。
下面算法没给出深度分析的算法。那个可以自己总结规则了。现在我的那个代码已经过W行了。太多了规则了。 规则越细,可以保证识别程度越准确。
/*
* 预读分析阶段,在分区处理是时进行,数据与代码区域
* 范围的界定,纯数据区域返回TRUE,反之为FALSE
*/
INLINE BOOL PredictBlockEnd(LPBYTE pMem, LPBYTE pCurr, DWORD dwSize, DWORD *pOutSize, PANALYZE_CONFIGURE pAnalyzeConfigure)
{
BOOL bBlock = FALSE;
DWORD dwOffset = 0;
ud_t ud_obj;
ud_init(&ud_obj);
ud_set_mode(&ud_obj, 32);
ud_set_syntax(&ud_obj, UD_SYN_INTEL);
ud_set_input_buffer(&ud_obj, pCurr, dwSize);
while (ud_disassemble(&ud_obj) != 0)
{
enum ud_mnemonic_code mnemonic = ud_obj.mnemonic;
if ((mnemonic == UD_Inop) ||
(mnemonic == UD_Iint3) ||
((mnemonic == UD_Iadd) &&
(ud_obj.inp_ctr == 2) &&
(*(WORD *)&(ud_obj.inp_sess) == 0)))
{
/*
* 到达结束条件
* 检查是否到达了用户定义代码的最小范围,如果没到直接视为数据
* 如果大于等于则进入深入鉴别
*/
if (dwOffset < pAnalyzeConfigure->bCodeMixSize)
{
bBlock = TRUE;
}
else
{
// 进入深度分析
bBlock = DeepAnalyzeBlock(pMem, pCurr, dwOffset, pAnalyzeConfigure);
}
*pOutSize = dwOffset;
return bBlock;
}/* end if */
dwOffset += ud_insn_len(&ud_obj);
}
// 这里做深度鉴别
bBlock = DeepAnalyzeBlock(pMem, pCurr, dwSize, pAnalyzeConfigure);
*pOutSize = dwOffset;
return bBlock;
}
/*
* 进行分区处理
* 以NOP,INT3,0,无效指令编码字符进行分区标志
* 遇到NOP或者INT3,0,无效指令编码则将扫描过的区域
* 作为一个区块摘出,继续查询,如果其后的数据有很长
* 一段为连续的 NOP,INT3,0字节则将此视做绝对无效区域,
* 在扫描过程中,如果遇到无效指令直接忽略当作这个区域
* 的一部分如果遇到有效指令,则进入预分析阶段,尝试向前
* 分析如果分析的指令长度小于最小代码长度则直接忽略.如果
* 大于则将此区域继续分析,往返以上流程.直到完毕
*/
INLINE PPROCEDURE FormatBlock(LPBYTE pMem, LPBYTE pStart, DWORD dwSize, PANALYZE_CONFIGURE pAnalyzeConfigure)
{
DWORD dwImageBase = GetNtHeader(pMem)->OptionalHeader.ImageBase;
PPROCEDURE pBlock, *pCurrBlockPoint = NULL;
pCurrBlockPoint = &pBlock;
DWORD dwBlockEndSignCount = 0, dwOffset = 0;
ud_t ud_obj;
ud_init(&ud_obj);
ud_set_mode(&ud_obj, 32);
ud_set_syntax(&ud_obj, UD_SYN_INTEL);
ud_set_input_buffer(&ud_obj, pStart, dwSize);
while (ud_disassemble(&ud_obj) != 0)
{
enum ud_mnemonic_code mnemonic = ud_obj.mnemonic;
if ((mnemonic == UD_Inop) ||
(mnemonic == UD_Iint3) ||
((mnemonic == UD_Iadd) &&
(ud_obj.inp_ctr == 2) &&
(*(WORD *)&(ud_obj.inp_sess) == 0)))
{
dwBlockEndSignCount += ud_insn_len(&ud_obj);
}
else
{
/*
* 遇到有效指令了
* 现在该进入预读分析阶段,这里还有一个判断
* 标志是剩余长度,如果其后的长度小于最小代码
* 长度直接将这段代码视作为数据
*/
DWORD dwRemainSize = dwSize - dwOffset;
LPBYTE pCurr = pStart + dwOffset;
if (dwRemainSize >= pAnalyzeConfigure->bCodeMixSize)
{
/*
* 进入预读分析阶段
* 在这里开始判断,如果预读处理分析的结果为单纯的数据块则
* 与先前分析的区域进行合并
* 否则单独为前面分析的块分配内存
*/
DWORD dwOutSize = 0;
if (PredictBlockEnd(pMem, pCurr, dwRemainSize, &dwOutSize, pAnalyzeConfigure) == TRUE)
{
// 进行合并
dwBlockEndSignCount += dwOutSize;
}
else
{
/*
* 判断上一组区块标记是否为0如果不为则分配空间存储它
*/
if (dwBlockEndSignCount != 0)
{
(*pCurrBlockPoint) = __new__(PROCEDURE, 1);
memset((*pCurrBlockPoint), 0, sizeof(PROCEDURE));
(*pCurrBlockPoint)->bBlock = TRUE;//代码区域
(*pCurrBlockPoint)->pFileStartAddress = pCurr - dwBlockEndSignCount;
(*pCurrBlockPoint)->dwMemoryStartAddress = dwImageBase + Raw2Rva(pMem, (DWORD)((*pCurrBlockPoint)->pFileStartAddress - pMem));
(*pCurrBlockPoint)->dwSize = dwBlockEndSignCount;
pCurrBlockPoint = &((*pCurrBlockPoint)->pNext);
dwBlockEndSignCount = 0;//清空标志记录计数
}
// 设定此段为代码区域
(*pCurrBlockPoint) = __new__(PROCEDURE, 1);
memset((*pCurrBlockPoint), 0, sizeof(PROCEDURE));
(*pCurrBlockPoint)->bBlock = FALSE;//代码区域
(*pCurrBlockPoint)->pFileStartAddress = pCurr;
(*pCurrBlockPoint)->dwMemoryStartAddress = dwImageBase + Raw2Rva(pMem, (DWORD)(pCurr - pMem));
(*pCurrBlockPoint)->dwSize = dwOutSize;
pCurrBlockPoint = &((*pCurrBlockPoint)->pNext);
}
// 重新设定反汇编指针
pCurr += dwOutSize;
dwOffset += dwOutSize;
dwRemainSize -= dwOutSize;//剩余长度
ud_set_input_buffer(&ud_obj, pCurr, dwRemainSize);
continue;
}
else
{
// 进入到这里就是进入到分析的末尾
(*pCurrBlockPoint) = __new__(PROCEDURE, 1);
memset((*pCurrBlockPoint), 0, sizeof(PROCEDURE));
(*pCurrBlockPoint)->bBlock = TRUE;//表示有可能是数据
dwBlockEndSignCount += dwRemainSize;
(*pCurrBlockPoint)->dwSize = dwBlockEndSignCount;
(*pCurrBlockPoint)->pFileStartAddress = pCurr;
(*pCurrBlockPoint)->dwMemoryStartAddress = dwImageBase + Raw2Rva(pMem, (DWORD)(pCurr - pMem));
pCurrBlockPoint = &((*pCurrBlockPoint)->pNext);
dwBlockEndSignCount = 0;//清空标志记录计数
}
}
dwOffset += ud_insn_len(&ud_obj);
}
/*
* 处理组后一个纯区域块
* 两种情况造成这种现象
* 1) 整个分析的数据都是纯数据区
* 2) 被分析的数据最后一个区域是纯数据区
*/
if (dwBlockEndSignCount != 0)
{
LPBYTE pBlockZoon = NULL;
// 第一种情况
if (dwBlockEndSignCount == dwSize)
{
pBlockZoon = pStart;
}
else
{
pBlockZoon = pStart + dwSize - dwBlockEndSignCount;
}
(*pCurrBlockPoint) = __new__(PROCEDURE, 1);
memset((*pCurrBlockPoint), 0, sizeof(PROCEDURE));
(*pCurrBlockPoint)->bBlock = TRUE;//表示有可能是数据
(*pCurrBlockPoint)->dwSize = dwBlockEndSignCount;
(*pCurrBlockPoint)->pFileStartAddress = pBlockZoon;
(*pCurrBlockPoint)->dwMemoryStartAddress = dwImageBase + Raw2Rva(pMem, (DWORD)(pBlockZoon - pMem));
pCurrBlockPoint = &((*pCurrBlockPoint)->pNext);
dwBlockEndSignCount = 0;//清空标志记录计数
}
return pBlock;
}
/*
* 进行预分析,识别出数据
* 如果分析出不是数据
*/
INLINE PPROCEDURE AnalyzeData(LPBYTE pMem, LPBYTE pCurr, DWORD dwSize, PPROCEDURE pProcedureList, PPROGRAM pParents)
{
PANALYZE_CONFIGURE pAnalyzeConfigure = &(pParents->AnalyzeConfigure);
DWORD dwImageBase = GetNtHeader(pMem)->OptionalHeader.ImageBase;
// 如果此块的长度小于用户指定长度
if (dwSize < pAnalyzeConfigure->bCodeMixSize)
{
PPROCEDURE pBlock = __new__(PROCEDURE, 1);
memset(pBlock, 0, sizeof(PROCEDURE));
pBlock->bBlock = TRUE;
pBlock->pFileStartAddress = pCurr;
pBlock->dwMemoryStartAddress = dwImageBase + Raw2Rva(pMem, (DWORD)(pCurr - pMem));
pBlock->dwSize = dwSize;
pBlock->pNext = NULL;
PPROCEDURE *pCurrMainPoint = &pProcedureList;
while (*pCurrMainPoint != NULL) pCurrMainPoint = &((*pCurrMainPoint)->pNext);
*pCurrMainPoint = pBlock;
return pProcedureList;
}
/*
* 开始对这个块进行有效的分区
* 分区完毕后对链中的代码块进行函数帧分析
* 初次分析
*/
PPROCEDURE pFormatBlockList = FormatBlock(pMem, pCurr, dwSize, pAnalyzeConfigure);
/*
* 遍历此链表进行分析
* 如果遇到代码块则进入并开始代码分析
*/
PPROCEDURE pCurrBlock = pFormatBlockList;
while (pCurrBlock != NULL)
{
if (pCurrBlock->bBlock == FALSE)//为代码块
{
// 进行分析
PPROCEDURE pProcedure = GetProcFrame(pMem, pCurrBlock->pFileStartAddress, pCurrBlock->dwSize, pParents);
/*
* 调用Procedure2Procedure过后形成的函数链中存在两种函数,一种是函数存在在已有的
* 未知区域中,一种是在做第一次扫描时分析到的函数
*/
DWORD dwCount = Procedure2Procedure(pMem, pProcedureList, pProcedure);
}
else
{
// 直接连接到主链
PPROCEDURE *pCurrMainPoint = &pProcedureList;
while (*pCurrMainPoint != NULL) pCurrMainPoint = &((*pCurrMainPoint)->pNext);
*pCurrMainPoint = __new__(PROCEDURE, 1);
memcpy(*pCurrMainPoint, pCurrBlock, sizeof(PROCEDURE));
(*pCurrMainPoint)->pNext = NULL;
}
pCurrBlock = pCurrBlock->pNext;
}
// 销毁分析链
ReleaseProcedureList(&pFormatBlockList);
return pProcedureList;
}
/*
* 找寻所有不在链上的内存区域,并归结到链上
*/
INLINE PPROCEDURE AnalyzeProcedurePass1DiscoveryUnknowZoon(LPBYTE pMem, PPROCEDURE pProcedureList, PPROGRAM pParents)
{
PPROCEDURE pCurrProcedure = pProcedureList;
LPBYTE pStart = pMem + GetEntryPointSection(pMem)->PointerToRawData, pCurr = pStart;
DWORD dwCodeSize = GetEntryPointSection(pMem)->Misc.VirtualSize;
PPROCEDURE pUnknowProcedureList = NULL;
LPBYTE pNow = NULL;
DWORD dwCurrSize = 0, dwBlockSize = 0, dwRemainSize = 0;
g_pAnalyzeDataDispatcher = MakeAnalyzeDataFromInstructionDispatcher();
_new_analyze:
while (pCurrProcedure != NULL)
{
pNow = pCurrProcedure->pFileStartAddress;
dwCurrSize = pCurrProcedure->dwSize;
if (pCurr == pNow)//如果相当则直接略过
{
pCurr += dwCurrSize;
dwRemainSize -= dwCurrSize;
}
else if (pCurr < pNow)//分析这个代码块
{
dwBlockSize = (DWORD)(pNow - pCurr);
dwRemainSize -= dwBlockSize;
goto _handler;
}
pCurrProcedure = pCurrProcedure->pNext;
}
// 判断是否到达末尾
if (dwRemainSize != 0)
{
dwBlockSize = dwRemainSize;
goto _handler;
}
// 释放派遣表
DestroyDispatcherList(&g_pAnalyzeDataDispatcher);
return pProcedureList;
// 这里为处理函数所在
_handler:
/*
* 这里的处理和前一次处理有个区别就是不知道当前这个区域是
* 数据还是代码,所有首先要做数据鉴别
*/
pUnknowProcedureList = AnalyzeData(pMem, pCurr, dwBlockSize, pProcedureList, pParents);
pProcedureList = SortProcedureList(pProcedureList);//排序
// 重新设定所有初始值,然后重新来过
pCurr = pStart;
dwRemainSize = dwCodeSize;//重新设定长度
pUnknowProcedureList = NULL;
pCurrProcedure = pProcedureList;
__PrintProcedureListCount__(pProcedureList);
goto _new_analyze;//重新进入新的一次分析
}
3.YY。。。
目前这种算法做规则很稳定。但是也很麻烦。不仅代码量大 而且要不断的更新规则。很麻烦。目前我正在通过人工神经网络去模糊识别代码和数据。已经有一个比较不错的方案。准备作为毕业的论文来搞。如果论文发表成功,我再翻译成中文奉献给论坛的朋友们。
三种流程如图:
[课程]FART 脱壳王!加量不加价!FART作者讲授!