邪恶八进制信息安全团队技术讨论组's Archiver

金州 2006-1-11 02:46

[转载]Semi-metamorphism(Seme)理论基础的一些探讨

文章作者:Vancheer/CVC

Semi-metamorphism(Seme)理论基础的一些探讨
By Vancheer/CVC

关于Seme,很早以前(大概有一年了)就出现在我的脑海里,但因其复杂性,需要
耗费相当的精力和时间去实现,所以我一直没有实现。现在重新提出来,不仅又有
了一些新的想法,而且也让有兴趣的CVC的兄弟掌握大概的结构和原理,希望有兄
弟能实现出来 :-)。
这篇文章讨论的内容,都只是我的一些浅层次想法,很多想法可能不对或者不好,
还需要大家的讨论。但我坚信Seme的想法是非常可行的,而且应该是有效的。
以下把Semi-metamorphism简称为Seme(色魔???),把Metamorphism简称为Meta。


1,Meta初步
1.1,何为Meta
不要被那一长串英文字母吓倒,其实Meta没什么的,说白了就是把代码重新洗牌,
使之产生功能相同但指令不同的代码。原来的所谓的Polymorphism,就是把病毒代
码加密,而解密代码则是随机生成的,Meta则进了一步,把病毒代码也随机化了。


1.2,Meta基本原理
一个比较完备的Meta引擎,应该包括以下几个步骤
A,反汇编,把病毒代码反汇编成一种易于进行重新编码的格式,最重要的是判断
是否为跳转,提取源、目的操作数。
B,干掉上一代的垃圾代码。每一代的产生,里面都会有不少垃圾代码,这一步就
是清除垃圾代码。这一步是必要的,否则会导致代码长度的无限膨胀,最后会因为
代码过大而无法运行。这一步应该说是相当复杂的,也是导致Meta引擎过大的主要
原因。
C,重新生成代码。这一步,容易些的,就是把原来的指令用等价的一条或者多条
指令替代,复杂些的,还要经过一些比较智能的判断,来选择随机的寄存器,调换
指令先后顺序,等等。垃圾代码,也是在这一步加进去的。
早期的Meta病毒,没有第二部分,所以病毒体积会无限膨胀,几乎不可用,后来有
人加入了B的部分,才使得Meta略微可用,但缺陷是过分复杂,病毒体积过于庞大


1.3,做一些限制
我们没必要让Meta引擎支持所有的指令,裁槐匾С炙械难爸纺J健<虻ィ?nbsp;
单,再简单,只用最基本的指令,最基本的寻址,其它复杂的,可以由这些基本的
东西衍生。比如内存寻址,只要支持偏移,寄存器,寄存器+偏移(基地址+偏移)
就足够了,这三种的例子就是 mov edx, [402000h]/mov edx, [eax]/mov edx,  
[ebp+50h],至于要寻址mov edx, [ebp+eax*4+50h],则可以用shl eax, 2/add  
ebp, eax/mov edx[ebp+50h]替代,当然要适当保护寄存器。当然如果你愿意支持
这种复杂的,也没问题,这里说的只是一个简化的思路而已。
说到寻址,还要注意一个问题,一般PE病毒都用call $+5/pop ebp这种方式,以
ebp为基地址,这样在写Meta引擎时一定要注意寻址的正确性,还是比较麻烦的,
一种简单的方法就是不考虑重定位,直接用内存地址寻址,比如原来要用到变量X
,需要写mov edx, [ebp+X-VStart],现在只要写mov edx, [X]就行了,大大简化
了引擎的工作。这种简化是合理的,因为一般exe文件被重定位的可能性很小,所
以可以使用绝对地址。

1.4,为什么不用Meta?
主要的原因是一个真正好的Meta的实现太复杂,因而也就太大,一般一个Meta病毒
都在30K-50K之间,做为一个PE病毒太大了,而且这么大的病毒绝大多数代码都是
为Meta写的。如此庞大的Meta引擎,实在难以真正应用。

2,Seme原理
2.1,Seme的目标
起码我个人喜欢把复杂的事情简单化,任何人都可以把简单的事情复杂化,但不是
每个人都能够把复杂的事情变简单的*^_^*。我设计的这个Seme的目标,就是以比
较简单的算法,比较少的代码,达到几乎和Meta一样的效果,这个效果就是:AV  
Software的VM看到的是一片没有特征码的代码…………,带来好处就是:AVer的特
征码完全失效…………。

2.2,基本结构:两段式结构
Seme分为两个部分,一部分如同普通病毒代码,做病毒该做的事情,这部分简称A
。A是病毒的主体,但与传统病毒不同,这部分不是人写出来的,而是由Seme引擎
自己产生出来的,代码是完全随机的。另外一部分则是Seme引擎部分,它是Seme的
主体,也是A的生母,这部分简称B。B的代码是固定的,不会被随机化,只会被加
密。
这种结构的好处,就是我们依赖的"源"代码总是干净的,没有垃圾的,这样就可以
省去Meta步骤里的第二步也就是最麻烦最复杂的一步。

B的结构
B也分两大部分:模板和引擎。模板就是生成A的样板,引擎则是用来生成A的算法
。模板是数据,引擎是算法,可以说B是数据驱动的引擎。引擎的算法就是把模板
转化成相应的随机代码。模板的结构决定了引擎的算法,而同一套引擎可以适应于
结构相同的多种模板。这里模板的结构是关键,也是我自己还没有完全确定的。

2.2.1,Meta所需要的指令元素
在具体看模板的结构之前,我们需要了解一下要产生随机指令,需要知道哪些元素

A,指令相对地址,这是跳转(广义的跳转,包括call指令)所必须的
B,操作码,需要知道要干什么,注意,我们只考虑病毒必须的指令,不必考虑所
有指令,而且通过改写代码,可以把必须的指令集合做到最小。其实仔细想想,真
正必须的指令是很少的。
C,源操作数和目的操作数,需要知道怎么干。注意,这里只考虑最多为两个操作
数(也可能为0个或一个),对于三操作数指令(shld, imul),太特殊,而且完全
可以用别的指令替代,故不考虑。
对于没有操作数的指令(cld, ret),源操作数和目的操作数当然都为空,但对于一
个操作数的指令(push, pop),则要谨慎选择操作数的方向。原则是,如果操作数
没有被改变,则做为源操作数,如果改变了,则做为目的操作数。比如push,只是
把操作数压栈,没有改变,故操作数是源操作数,而pop则正好相反,操作数被改
变了,所以是目的操作数。
搞清操作数的方向很重要,它关系到产生的随机代码的准确性。
D,该指令的操作结果所影响的Flag是否有用。比如sub指令产生的CF,可能被下一
条指令做为跳转的依据。这一点是值得注意的,因为产生的随机代码很可能影响了
Flag。当然如果限定条件跳转必须紧跟在产生条件的指令后面,那么也就完全可以
自动判断某指令的Flag是否有用了,比如,碰到指令sub edx, 1000h/shl edx, 2
,那么可以知道那个sub产生的Flag不重要,但碰到sub edx, 1000h/jl xxxx,则
sub产生的Flag是有用的了,在产生垃圾代码时就不能影响Flag。
E,是否有前缀。对于rep这样的前缀,可以做为指令的一部分处理,但对于段前缀
(用的最多的就是FS了),则要谨慎处理。对于这种有段前缀的指令,由于非常少
,为求简单,可以不Meta,直接原样Copy。

2.2.2,模板的结构
我自己构思了两种结构,每种都有它的利和弊,现在逐一讨论之。

2.2.2.1,第一种结构:无结构模板
没有结构也是一种结构,不是吗?:-)
这种结构,其实就是把原始的病毒代码做为模板。把原始的病毒代码做为数据,根
据这些数据产生新一代的随机代码。

有利之处:最大的利益就是这种模板的体积很小,一个普通的PE病毒的主体代码一
般就在4K左右,那么这种模板的大小也就4K左右。
不利之处:由于这种模板根本没有保存额外的信息,所以仍然要做反汇编的工作,
代码复杂。而且如果要随机选择寄存器,就要分析指令间的寄存器关系,这个算法
还是很复杂的,另外如果要调换指令顺序,还要分析指令的逻辑关系,还是复杂。
其实可以用无效指令插入一些额外信息,但这样就像脚本模板了。
总体说来,这种结构就是以复杂的算法换取空间上的利益,我个人不是很偏向这种
结构,不过使用无效指令来获取额外信息倒是个不错的主意,但好像只有0d6h和
0f1h开头的才是无效指令,贸然使用,万一以后它们成有效指令了,就比较危险了
。但我们需要的指令集是很小的,所以可以用永远用不到的指令做为无效指令,比
如浮点指令。

2.2.2.2,第二种结构:脚本模板
大家可以看到,说白了我们的Seme引擎其实就是一端输入一段代码,另一段输出另
外一段随机化的代码,那么为何输入的代码一定要局限于机器指令呢?其实放开思
路,我们可以把引擎当成一个小汇编器甚至编译器,只要输入的代码是它认识的,
它就能输出代码,如果你愿意,可以让引擎支持C语言:-)。
脚本模板,顾名思义,就是以"脚本"来写病毒原始代码,当然这种脚本绝不是像
VBS,JS那样适合人类阅读和书写的,但这种脚本可以给Seme引擎以更多的信息,
可以让引擎工作的更容易更好,产生的代码更有效。
脚本模板,我想到的,也可以分为两种,指令模板和功能模板。
指令模板是非常低级的,就是一些指令和操作数的集合,当然也可以带有额外的信
息。这种模板,几乎等同于汇编指令,但与第一种无结构模板相比,好处就是可以
省去反汇编的部分,而且随机选择寄存器和调换指令位置变得更容易。而且脚本模
板可以支持不认识的指令,只要告诉它指令的长度,就可以原样Copy一下,大不了
不变形了。
功能模板,目前我还没有太多考虑。顾名思义,这种模板包含的不是具体的指令,
而是一些要完成的功能了。比如模板的一项说"开始SEH",那么引擎就会生成SEH的
随机代码。这种模板,已经和真正的脚本非常相似了,而且可以从功能而不是指令
级别上进行Meta了。功能模板可以完全不关心指令的产生,着重考虑要完成什么功
能,产生指令的事情,全都交给引擎去做了。要用这种模板,功能的抽取是很重要
的,小的抽取可以是进行一些加减计算,大的抽取可能只是告诉引擎"我要搜索
API地址","我要搜索磁盘"等等,功能越大,引擎越不好实现,而且功能也不能太
大,否则失去意义了。

有利之处:引擎的编写相对比较容易,而且可以高度随机。
不利之处:最大的也几乎是唯一一个不利之处就是这种模板太大,即使按比机器指
令膨胀3倍(其实不止三倍)计算,一个4K的病毒的模板也要12K。所以一定要精心
构造模板的结构,尽量压缩信息。其实我发现最紧凑的结构,还是机器指令的结构
∶-)。我正在考虑用机器指令和额外数据混合的方法来做模板,感觉挺好,但还没
深想,主要是怎么用随机寄存器的问题。

3,Seme实现初步
下面简要讨论一下各种模板的结构和引擎的算法,希望对于真正的实现有所帮助。


3.1,无结构模板的结构和引擎算法
模板的结构,其实就是汇编指令,但只做为数据存放,永远不会执行到它们
Template:
mov edx, 12345678h ;病毒代码的开始。举例而已。
call xxx
......
TemplateEnd:

引擎算法,需要多遍扫描:
Pass1,反汇编Template,把每条指令存到一个结构数组里,结构的大致情况为
TempStruc struc
RawIP dd ? ;原始指令的地址,用来对跳转指令重定位用
Opcode db ? ;操作码,比如可以定义I_MOV为1,
Src dd ? ;源操作数,如果是跳转指令,则为跳转的偏移
Dst dd ? ;目的操作数
Mod db ? ;指令的寻址模式,操作数大小(8 bit or 32 bit),等等
TempStruc ends

Pass2:预处理跳转指令,把原来的地址偏移转换成以模板结构为单位的偏移,这
样方便重定位。比如,原来的指令为
addrA: jnz addrB
addrB: mov edx, 1234h
假设addrB处的指令经过Pass1存在第38个结构内,那么addrA处指令的结构里跳转
的目标就为38。
由于存在向后跳转的缘故,所以此扫描无法和Pass1合并。

Pass3:生成随机代码,现在还不是产生真正目标代码的时候,要把产生的代码存
到另外一个结构数组里,该结构的大致结构为
OpcodeStruc struc
RawIndex dw ? ;原指令在TempStruc数组里的序号,如果是垃圾指令,则该序号为
-1
OpLen db ? ;新指令的长度
Opcode db 128 dup(?) ;新指令
OpcodeStruc ends
注意,每个OpcodeStruc储存的不是单独的一条机器指令,而是要么是一些垃圾代
码,要么是对应原来一条指令功能的多条指令。由于可能调换指令顺序,所以
RawIndex不一定是渐增排序的。

Pass4:根据目标代码的地址,做最后的重定位,重新装配跳转指令,然后把代码
Copy到目标地址。
至此,Seme引擎完成了它的使命。

3.2,脚本模板的结构和引擎算法
模板的结构,其实就是上面提到的TempStruc,但为了考虑空间大小,该结构一定
要进行尽量的简化和压缩,可以考虑变长结构。

引擎算法,和无结构模板的算法很像,只是省略了反汇编和预处理跳转指令(这些
已经包含在模板里了),但如果是用变长结构来存储模板,那么应该把模板转成
TempStruc那样的定长结构数组。

下面讨论一些脚本模板中的细节问题,为求方便,假定模板结构就为TempStruc及
其扩充。
对于脚本模板,调换指令顺序是很容易的,可以在模板中加入顺序信息。
TempStruc struc
...... ;同无结构模板
GroupIndex db ?
TempStruc ends
其中的GroupIndex,记录的就是可以调换的同一组中的分组顺序,如果该指令不可
以调换顺序,则GroupIndex为0。比如有两条指令可以调换 mov edx, 38h/push  
ecx,那么这两条指令的GroupIndex为9(假设已经有8个组可以调换了),如果下
一条指令不能和它的前后指令调换,则GroupIndex为0。这个GroupIndex,也可以
用别的方式定义,只要能区分可调换指令的分组就行了。

寄存器的使用。为了方便随机选取寄存器,我们在脚本模板中可以用符号代替真实
的寄存器,下面是以汇编指令形式写的模板,
@use c1
mov c0, 38h
push c2
add r1, c1
@clear c1
其中cn(n=0,1,2)是三个通用寄存器中的任意一个(eax, ecx, edx,没有ebx!),
而rn(n=0,1,2,3)表示四个略微特殊的寄存器(ebx, esi, edi, ebp),当然了,上
面的例子太简单,还应该考虑8位和16位的寄存器。
寄存器经过如此符号化后,Seme引擎就可以任意选择随机寄存器了。当一个寄存器
不再被需要时,应该通知引擎可以把它回收了,这就是@clear的功能。引擎一看到
@clear,就可以让相应的寄存器参与下一轮的随机选取了,@use则表示现在要开始
使用c1了。

3.3,对模板和引擎的加密和解密
可以看到,虽然病毒代码是随机产生的,但模板和引擎是固定的,所以一定要对这
两部分进行加密。加密可以由引擎自己进行,然后生成一个解密头,由A部分调用
。这个原理很简单,但一定要考虑解密时机的问题。如果病毒一执行就进行解密,
那么Seme就成了普通的Poly,很容易被AV Software的VM干掉。鉴于此,一定要延
后解密。延后到什么时候?最起码应该在病毒代码执行1000条指令以后,这时很可
能没有被跟踪了,然后休息,Sleep一下,不睡够时间绝不醒,或者随便搜索一些
文件,或者干一些其它费时间的事情,总之,一定不要在病毒执行的前5秒钟进行
解密。

本文写的匆忙,也不够准确,目的只是要扔一块砖头,捡回一大块玉 :-)。我自己
估计难以有时间和精力来实现一个Seme,希望各位CVC兄弟能够集思广益,写出一
个够Cool的Seme出来。

页: [1]
© 1999-2008 EvilOctal Security Team