做壳很容易,有点做病毒知识的人都可以编写出壳来。但强度就有所差别了。
什么才是凶猛的壳?
我觉的是这样的 "猛壳 = 识别 + 重定位".识别用于让壳更了解要保护的程序。重定位为了可以保证保护程序的正常运行。如果能把识别与重定位做好。那么很多新的玩意就都可以玩了。如果你能把一个壳做成一个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;//重新进入新的一次分析 }
目前这种算法做规则很稳定。但是也很麻烦。不仅代码量大 而且要不断的更新规则。很麻烦。目前我正在通过人工神经网络去模糊识别代码和数据。已经有一个比较不错的方案。准备作为毕业的论文来搞。如果论文发表成功,我再翻译成中文奉献给论坛的朋友们。
三种流程如图: