首页
论坛
课程
招聘

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

2010-6-22 10:41 34834

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

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

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

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

下面是doc和代码的打包下载

[推荐]看雪工具下载站,全新登场!(Android、Web、漏洞分析还未更新)

上传的附件:
最新回复 (53)
Lenus 3 2010-6-22 10:42
2
0
前言

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

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

第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%左右
   
实际应用当中,针对具体的需求我还对代码和阈值做了一些微调。不过很抱歉这部分代码不能共享出来。
上传的附件:
Lenus 3 2010-6-22 10:44
4
0
第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。

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等人才比较清楚吧!),很有可能会产生较多的误判和漏判 。
上传的附件:
Lenus 3 2010-6-22 10:45
5
0
第二章、你加了什么壳
       
这章本不应该放在最后,因为我所采用的方法是绝大多数人都会想到的方法。的确,对于确定壳的种类没有比特征码更简单明了的方法了。
       
PEiD中对于壳特征开放了userdb.txt,广大分析员将自己辛勤的劳动成果共享于网络。要用代码去实现壳的分类,你需要做的就是:
1.甄选这些特征。
2.修改一个字符匹配算法(支持通配符)。

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



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

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

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

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

如果对本文有不明白的和提出不对的地方,请在下面跟帖。

btw. 2个星期后和100贴后,我基本上都不看。
littlewisp 2 2010-6-22 10:50
7
0
我要抢板凳。
海风月影 17 2010-6-22 14:37
8
0
好文章
一定要顶

收藏一下,慢慢看
Bughoho 8 2010-6-22 14:50
9
0
good 貌似在哪看过英文版的
Lenus 3 2010-6-22 15:22
10
0
有吗?能不能共享一份给我瞧瞧。
qishbi 2010-6-22 15:30
11
0
谢谢楼主提供分享
dreamzgj 2 2010-6-22 16:03
12
0
收藏。。慢慢学习
tokiii 2010-6-23 11:13
13
0
不错的文章
dongmkr 2010-6-25 09:19
14
0
强悍的理论文章
值得仔细研究研究
forgot 26 2010-6-25 14:14
15
0
感觉entropy是非常胡扯的标准,对第一个段反汇编能得到大部分有效的cfg也许稍微靠谱点。
Lenus 3 2010-6-25 14:16
16
0
非常赞成。
shlmyth 2010-6-25 14:48
17
0
学习下,好文阿
Greater 2010-6-30 11:36
18
0
好东西 我**啦
kanxue 8 2010-7-1 09:40
19
0
欢迎Lenus,有机会聚聚
kldaft 2010-7-1 10:14
20
0
Entropy = ∑Ei * Si / ∑Si (i = 1,2…n)。

看完了整篇文章,都没有搞懂这个公式中的E代表什么(本人比较笨)
edisonH 3 2010-7-3 15:00
21
0
哇,看雪终于亮了,很久没有这样的感觉了
whypro 2010-7-3 17:44
22
0
过来顶一下,文章很不错!(思路不错!)
hmilywen 2010-7-3 20:07
23
0
经典,收藏了!
nence 2010-7-4 07:46
24
0
互联网最大的好处就是知识的共享,很不错的文章
Lenus 3 2010-7-4 13:30
25
0
这个可能是我没说清楚,Ei表示的是每一段需要计算的熵!
langker 2010-7-9 18:29
26
0
感谢LZ这么好的资料
yihai逸海 2010-7-10 11:12
27
0
有空再研究研究, 看不太懂... 收藏了, 谢谢~
枫癫 2010-7-10 14:49
28
0
下收藏了再说
feilang非浪 2010-7-10 23:03
29
0
其实有一段时间有对这方面的稍微研究了一下,对于lenus所说的,加了什么壳,我的理解可能是差不多,基本的都是按照特征码来的,每一个语言开发出来的软件,都会带有自己的特征,这是肯定了,那目前有多少种语言,汇编,C,C++,Dehpi,.NET,JAVA,E语言等等,都有自己的特征码,还有已知的所有壳,PEID这类的有着很多年历史的,对于特征码的收集,肯定很多,但是对于楼主所说的神经网络,智能学习,探测类的,估计目前也许有,但是知识太深奥了。
nevsayno 2010-7-12 02:45
30
0
看不懂Entropy
wdbg 2010-7-12 09:33
31
0
这篇文章非常不错,很有耳目一新的感觉
流云似水 2010-7-12 15:06
32
0
学习了,感谢分享!
小娃崽 13 2010-7-13 09:42
33
0
LENUS的那篇ESP定律给我的印象很深,刚开始学脱壳,看的就是ESP定律
Greater 2010-7-13 10:50
34
0
不错  楼主很强大啦
关于墒的概念应该是信息论方面的知识啦
思远软件 2010-7-13 12:13
35
0
不错不错!!
Lenus 3 2010-7-13 12:37
36
0
说真的,最后一章我就是在扯淡。

与其说判断加什么壳方式有这样的缺点,还不如说基于特征扫描的方式都会存在误报等缺点。

虽然说建立类似神经网络等这样的系统的确能解决调大部分人工的参与,但是将这些引入到“加壳”的判定来说就是小题大作。

所以,最后一章纯属扯淡!
yzcool 2010-7-19 11:15
37
0
学习了,谢谢!!
estelle 2010-7-19 13:37
38
0
呵呵 等我有时间的时候去学习下
GuluYZ 2010-7-19 22:58
39
0
PEID的。
unsigned char data[4096] = {0x00};//测试计算

static void makeHistogram(const unsigned char *buffer, int sizeOfBuffer, int histogramArray[256])
{
int i;
memset(histogramArray, 0, sizeof(int) * 256);

for(i = 0; i < sizeOfBuffer; i++)
   histogramArray[buffer[i]]++;
}

double calc_entropy(int code[],int size)
{
      double d_key=0.0;
      double code_size=(double)size;
      if (code_size<0)
      {
        code_size+=4294967296.000000;
      }

      for (int i=0;i<256;i++)
     {
        double result=(double)code[i];
       if (result!=0)
       {
         result=(double)(int)result;
         if (result<0)
          {
           result=+4294967296.000000;
            }
        result=result / code_size;
        d_key-=result*log(result);
        }
     }
    d_key= d_key / log((double)2);
    return d_key;
}

int _tmain(int argc, _TCHAR* argv[])
{
int histogramArray[256];

double i;

makeHistogram( data, sizeof(data), histogramArray);

i=calc_entropy( histogramArray, sizeof(data));

printf("%f",i);

return 0;
}
Bughoho 8 2010-7-20 05:20
40
0
啊 我说的是看过类似的英文文章 忘了哪看的了
tjszlqq 1 2010-7-23 08:53
41
0
高人终于又现身了,理论强大啊
backervon 2010-7-30 09:39
42
0
学习中,很有帮助!
TCAV 2010-7-31 12:22
43
0
好文章!顶lenus!期待lenus再次放血
代码疯子 3 2010-8-12 13:07
44
0
虽然我是新手 也总得试着看看吧 呵呵 谢谢楼主了
lovebubble 2010-8-12 17:52
45
0
不懂,学习一下
wynney 24 2010-8-18 10:33
46
0
代码简陋了点,单靠熵的标准,误报率相当哪个的高
之前论坛上的GUnpack的误报率基本能控制在10%以内
hunain 2010-9-1 12:40
47
0
看着有点晕菜了,不过想要弄懂我想也只是时间上的问题,努力中......
hack一生 1 2010-9-8 17:30
48
0
up...看雪强人真多
鸭子 2010-9-18 22:25
49
0
好文章,特来学习,谢了
jjjackup 2010-9-20 11:56
50
0
收藏了,慢慢学习。
游客
登录 | 注册 方可回帖
返回