如何用程序判定一个PE文件是否加壳

如何用程序判定一个PE文件是否加壳

原文网址:http://bbs.pediy.com/thread-115515.htm

Lenus 2010-6-22 10:41 29100


由于工作原因,时间比较少上论坛来泡泡了。最近由于某些原因时间变得稍微宽裕了些,有空整理整理这几年的一些资料和文档。

感觉这个方向论坛上讨论得并不多,是这个方向没有什么使用价值呢,还是说太简单了大家都不愿意研究?总而言之,我个人感觉这个方向还是一个挺有趣的,所以发上来共享一下。

这篇文章涉及的东西,虽然不是很难,但可能并不适合于新手。尽管我已经尽力去解释一些东西,但由于水平有限难免会存在一些错误。还请各位大侠不吝赐教啊!

下面是doc和代码的打包下载
上传的附件:
本主题帖已收到 0 次赞赏,累计 ¥0.00
最新回复 ( 52)
3
Lenus 2010-6-22 10:42
2
前言

开始切入正题前,让我先解释一下这篇文章的结构。
       
一个很平凡的题目(命名的确是很头疼的问题,以此命名的原因是希望能在搜索引擎提高命中率:D),但却不好回答。
       
我在07年底遇到了到这样的需求:设计一个函数判定PE文件是否加壳,加了什么壳。这个需求的必要性不言自明。
       
当我拿到需求并且与需求方确认后将需求分解为以下2个层次:
1.初级需求是函数的返回值是BOOL型,返回True或者False表示是否加壳。
2.高级需求是函数的返回值必须能返回表示壳的种类,返回壳ID或者壳名称。
   
关于手动脱壳的分析员来说判断一个PE文件是否加壳方法太多,而且通常非常容易。但是如果将这些方法一一列举出来后,我发现用程序来模拟这些人工的动作有3个问题:
1.大部分方法,实现起来比较复杂。
2.有些方法误判率非常高。
3.大部分方法,判定的壳种类非常有限。

所以本文的正文部分结构上分成二个部分,分别尝试去回答上面的两个问题。第一章解决第1个小问题,其中又分成两个小节使用两种技术来分别实现;第二章解决第2个小问题。希望你能按照顺序阅读全文。
       
在开始旅行前,有一些共识需要强调一下。Pack 的英文直译过来应该是“被压缩”的意思。而中文里“壳”的意思更多的是强调“被保护”(“壳”的历史不是这篇文章的重点)这里要说明的是,尽管按照中文的习惯,我更多的使用“加壳”这个词语,但本文试图解决的问题 -- 检测一个PE文件是否“被压缩”,而不是是否“被保护”。例如:一些自解压的WinRar程序也会被判定为加壳。
3
Lenus 2010-6-22 10:43
3
第一章、你加壳了吗?

第1节、信息熵(Entropy)

In information theory, entropy is a measure of the uncertainty associated with a random variable. The term by itself in this context usually refers to the Shannon entropy, which quantifies, in the sense of an expected value, the information contained in a message, usually in units such as bits. Equivalently, the Shannon entropy is a measure of the average information content one is missing when one does not know the value of the random variable. The concept was introduced by Claude E. Shannon in his 1948 paper "A Mathematical Theory of Communication".




引用自:http://en.wikipedia.org/wiki/Information_entropy
       
一开始是否被上面这恐怖的定义吓到了?按照国内理科类教课书和论文的惯例----将公式推导和定义放在一开始,目的就是吓人。潜台词是接下来内容可信度很高很牛B。

如果你真的被吓到了,那么我的目的也就达到了。但是我的潜台词是这个理论的确很牛B,但是我们完全不需要理会它也可以。
       
“信息熵”是通信领域的一个概念。以我的理解就是在一段的数据中其所携带的信息量。应用在判定PE是否加壳时,“信息熵”可以这么理解,计算一个PE文件的信息量或者其中某段(Section)的信息量。当这个熵的值超过一定阈值时,则该PE或某段被加壳(Packed)。说得更准确(更通俗)些,就是压缩数据往往携带着更多的信息量,PE文件携带信息量得多的就意味着可能被加壳了。
       
在PEiD中就有这样的功能:



(该图片引用自小喂的帖子http://bbs.pediy.com/showthread.php?t=39443)

计算一个节或者一个PE文件的熵,可以从论坛中小喂的帖子下载到源代码。

小喂的代码的核心算法就是一个函数(一句话):
     

   
对于判定是否加壳的Entropy阈值定义如下:
加壳(Packed):                                       Entropy > 6.75
可能加壳(Maybe Packed):           6.75 > Entropy > 6.5
无壳(No Packed):                        6.5  > Entropy



ps.关于这个定义的来源我一直没弄明白。这是order-0 entropy的定义如此,还是经验所得?
   
到这里基本的理论已经介绍完毕了,这章结束了吗?还没有!

当你将这套代码做测试的时候,你会发现它的误判和漏判率非常的高。当我发现这个情况的时候,我尝试使用PEiD来对比,结果让我大吃一惊。PEiD计算的Entropy和用小喂代码计算的Entropy竟然完全不一样!(貌似计算一个Section的Entropy是一样的)而且PEiD判定的命中率非常之高。
       
我非常想知道PEiD是如何做到的,于是通过google我来到PEiD的官方论坛。在论坛中有一个帖子引起了我的注意。



http://www.peid.info/forum/viewtopic.php?t=42
       
巫术(voodoo)?!啊哈,别开玩笑了。对于客户端程序来说可没有秘密可言!:D
       
既然人家作者不愿意说,那我只好踏上PEiD的逆向分析之路。

很抱歉的是由于逆向是在07年时完成的。现在已经找不到自己当时逆向的一些注释或者资料。所以这个部分就略过吧 -- 我想你也不是很感兴趣。

下面进入结论阶段,让我们看看PEiD都做了哪些工作。

1.重新组织需要计算的数据
  i.以下数据不列入计算熵的范围:导出表数据、导入表数据、资源数据、重定向数据。
  ii.  尾部全0的数据不列入计算熵的范围。
  iii. PE头不列入计算熵的范围。

2.分别计算每一部分数据的熵E和该部分数据大小S。

3.以下列公式得到整个PE文件的熵 Entropy = ∑Ei * Si / ∑Si (i = 1,2…n)。
   
经过上面的条件限制和方法,我计算出来的Entropy与PEiD的就基本上一样了。尝试使用100个加壳的样本和100个没有加壳的样本进行测试得到下面的数据:
       
1.当定义Entropy > 6.5 为 Packed时,误报率是20%左右;遗漏样本在10%以下
2.当定义Entropy > 6.75为 Packed时,误报率是10%左右;遗漏样本在15%左右
   
实际应用当中,针对具体的需求我还对代码和阈值做了一些微调。不过很抱歉这部分代码不能共享出来。
上传的附件:
3
Lenus 2010-6-22 10:44
4
第2节、节表的秘密

在小喂的帖子中提到了猜测Process Explorer对文件的判定也使用的是Entropy的方法,其实不然。这一章就是要揭开Process Explorer对文件加壳判定的秘密。
       
先来看看Process Explorer在对加壳与否界面上的展示。



Process Explorer默认用紫色高亮显示被加壳的镜像,从图中我们知道Totalcmd被判定是Packed Images。

Sysinternal自从"弃明投暗"纳入微软帐下后:D,源代码就不再公开。无奈之下还是只能选择逆向Process Explorer(版本号 v11.12)这条路。其实逆向完Process Explorer后,我发现远没有想象中的复杂。下面将一些逆向的过程展示出来。

1.判定的函数这里开始,CreateFile打开要判定的文件。



2.校验一下PE文件头,然后判定是win32还是win64



3.下面进入了核心算法,一共有10条规则进行判定镜像是否加壳



4.主循环结束,通过一个标志位决定了是否上色:)



我原来打算总结一下这10条规则,但是这次我决定偷懒了。还是让代码说明一切吧!
在下载这个文档的同时,你应该能下载我的测试代码(procexp_judge_pack.cpp),代码中没有实现对win64的PE文件的判定,又偷懒了:D。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
BOOL _IsPack_32(PBYTE pBuf, UINT uSize)
{
     BOOL bRet = FALSE;
 
     PIMAGE_DOS_HEADER      pDosHdr  = NULL;
     PIMAGE_NT_HEADERS      pNTHdr   = NULL;
     PIMAGE_FILE_HEADER     pFileHdr = NULL;
     PIMAGE_SECTION_HEADER  pSecHdr  = NULL;
     UINT                   i        = 0;
     
     pDosHdr  = (PIMAGE_DOS_HEADER)pBuf;
     pNTHdr   = (PIMAGE_NT_HEADERS)((PBYTE)pDosHdr + pDosHdr->e_lfanew);
     pFileHdr = &pNTHdr->FileHeader;
     pSecHdr  = (PIMAGE_SECTION_HEADER)((PBYTE)pNTHdr + sizeof(IMAGE_NT_HEADERS32));
 
     for (i = 0; i < pFileHdr->NumberOfSections; i++)
     {
         if (IsBadReadPtr(pSecHdr, sizeof(IMAGE_SECTION_HEADER)))
             break ;
 
         // Rule1
         if (pSecHdr[i].Characteristics & IMAGE_SCN_CNT_CODE && 0x1000 < pSecHdr[i].Misc.VirtualSize && pSecHdr[i].SizeOfRawData <= pSecHdr[i].Misc.VirtualSize - 0x1000)
             goto Exit1;
 
         // Rule2
         if (!memcmp(pSecHdr[i].Name, szSecNameText, sizeof(szSecNameText)) && 0x1000 < pSecHdr[i].Misc.VirtualSize && pSecHdr[i].SizeOfRawData < pSecHdr[i].Misc.VirtualSize - 0x1000)
             goto Exit1;
 
         // Rule3
         if (0 == *pSecHdr[i].Name && pSecHdr[i].SizeOfRawData < pSecHdr[i].Misc.VirtualSize)
             goto Exit1;
 
         // Rule4
         if (0 != pSecHdr[i].SizeOfRawData)
             continue ;
 
         // Rule5
         if (0x1000 > pSecHdr[i].Misc.VirtualSize)
             continue ;
 
         // Rule6
         if (!memcmp(pSecHdr[i].Name, szSecNameTextBSS, sizeof(szSecNameTextBSS)))
             continue ;
 
         // Rule7
         if (!memcmp(pSecHdr[i].Name, szSecNameTLS, sizeof(szSecNameTLS)))
             continue ;
         
         // Rule8
         if (!memcmp(pSecHdr[i].Name, szSecNameBSS, sizeof(szSecNameBSS)))
             continue ;
 
         // Rule9
         if (!memcmp(pSecHdr[i].Name, szSecNameData, sizeof(szSecNameData)))
             continue ;
 
         // Rule10
         if (pSecHdr[i].Characteristics & (IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_CNT_UNINITIALIZED_DATA))
             goto Exit1;
     }
     goto Exit0;
Exit1:
     bRet = TRUE;
Exit0:
     return bRet;
}

   
由于我没有对此方法做样本的误报和漏报测试,只是简单的验证了一下代码的正确性,所以我无法肯定的指出哪种方法(Entropy vs Section)更优秀。
       
个人认为,Entropy的方式有强大的理论背景支持(尽管我也不太懂那是为什么),可能更准确些;而Section的方式貌似没有很强的理论背景支持(谁知道这些规则有什么道理,也许这只有微软和Matt Pietrek等人才比较清楚吧!),很有可能会产生较多的误判和漏判 。
上传的附件:
3
Lenus 2010-6-22 10:45
5
第二章、你加了什么壳
       
这章本不应该放在最后,因为我所采用的方法是绝大多数人都会想到的方法。的确,对于确定壳的种类没有比特征码更简单明了的方法了。
       
PEiD中对于壳特征开放了userdb.txt,广大分析员将自己辛勤的劳动成果共享于网络。要用代码去实现壳的分类,你需要做的就是:
1.甄选这些特征。
2.修改一个字符匹配算法(支持通配符)。

ps.值得一提的是PEiD自己还维护了一个内部特征,我们不得而知:(



       
这就够了吗?
       
用人工维护特征这种方法,向来就具备以下几个弱点:
1.滞后性
  特征只能针对已知的壳,那些最新的壳,你需要时间去发现和收集

2.局限性
  你不可能提取出所有壳的特征,判定程序只能无奈的返回Unknown

3.误判
  人永远无法保证100%的正确

我曾经试图想解决三个问题,可是目前还没有更好的解决方案。即便如此让机器代理人来提取特征仍然是有可能的。设计一个类似神经网络的系统,让壳样本不断的去训练你的特征。当所有的流程无需人工参与的时候,虽然滞后性无法避免但可以缩短时间;虽然局限性无法根除,但可以降低比率;误判无法保证100%,那就保证99.999999%。
       
好吧,我承认。这一章我有点扯...你们还是无视吧!
上传的附件:
  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值