编写变形的shellcode[原理篇]


X'Con04据说轰轰烈烈的结束了,马上安焦就毫无保留地把所有的演讲用幻灯都放了出来。作为菜鸟,我也好事地下载了一份来看,首先看到的是Plan9 大虾的《高级shellcode设计技巧》。这篇文章似乎总结了一下当前Shellcode的各种技巧和设计思路,比较有意思的是当中提到的 ADMMutate,虽然在2001年就有见过这个工具,但是似乎一直都在*nix下而且这么多年来没见过升级。闲下来没有事情的时候,我就想做上这么一 个东西来打发时间,面向的平台是Win32——至少有一点要与ADMMutate不一样吧?
切入正题之前先科普并八卦一下。摆弄shellcode似乎是蛮好玩的事情,Phrack从老早老早就几乎每一期都有相关的文章,从全字母数字的,到 390的,然后到符合Unicode或UTF8的shellcode都有。这些文章大多都给出的是一些路子,甚至是一些可用的指令集而已,像我们这种菜鸟 看了不免一头雾水。不过深入一点就可以发现,需要解决的问题就是如何利用有限的指令,来“修补”出所有可能的指令来——正如这个系列前面所说的,关键的是 解码的部分,因为我们可以想出各种各样的算法来对起作用的shellcode来编码,但是解码的工作却会有各种各样的限制(特殊字符啊之类的),这些文章 也就是解决了解码部分的问题。从这个意义上来说,这些技术不单单可以应用在shellcode上面,它很可能最早来自病毒的技术,而且可以同样的适用于对 文件进行加密等应用上。
ADMMutate是将shellcode变形的工具。变形的概念似乎最早出自于病毒方面(具体何时我没法考证,因为估计变形病毒出现的时候我还在流着鼻 涕摆弄Apple呢),大概的意思就是说生成出来的shellcode总是有不同的形式。这看起来似乎有点不可思议,但是确实是可行的,举个简单的例子来 说,比如我们要问一个在北京的四川人昨天晚上干什么去了,北京人可以说“你丫挺的昨晚上那晃悠去了?”,四川人可以说“龟儿子昨晚黑抓子去老?”,都是可 以让对方了解的——这就是同样意思的不同表达方式;或者北京人可以说“昨晚你丫挺的上那晃悠去了?”,四川人说“昨晚黑龟儿子抓子去老?”,对方也可以理 解——这是在前面的基础上,调换了一下表达的顺序,这种调换不影响意思的表达;再或者北京人可以说“你丫挺的昨晚好事不做上那晃悠去了?”,四川人说“昨 晚黑你个龟儿子的不落教抓子去老哦?”,添加了很多没有用的东西,这样还是可以清晰地表达。这里说的,对于同样的意思,不同方言的表达,不同语句顺序的表 达,插入无关紧要的字句,就是变形的基本方式。换个角度,我们可以认为是表达的意思不变的情况下,对其中的因素进行变换次序或者插入无关因素,就是一种变 形。
那,我们就一个一个的来看基本的变形方法。先是基本的概念,然后才是实战嘛。

“你丫挺的” VS “龟儿子” —— 选择不同的寄存器

同样的意思,可以有多种不同的表达方式,最简单的一种就是用不同的方言来替换。同样的,对于计算机能够理解的语言,要实现同样方式的不同表达,最简单的就是选用不同的寄存器。
早期的计算机上,寄存器的使用似乎不像现在这样随便,这从寄存器的名字就可以看出来,eax是累加(accumulate)寄存器,ecx是计数 (counter)寄存器,不过后来这种限制就不多了,对内存的操作几乎所有的寄存器都可以。在这样的情况下,几乎所有的寄存器都可以被我们选用来做为操 作所需的那个寄存器,简单的举个例子,我们要通过寄存器放一个数入栈,可以:

mov eax, xxxxxxxx
push eax

也可以:

mov ecx, xxxxxxxx
push ecx

两个是等价的。
那么,就出来可能的第一个问题,这样子变换最终生成的机器码长度是否是一样的呢?为了编写变形shellcode的方便,我们当然希望简单的对寄存器的选 择变换,最后生成的机器码长度一样,因为这样我们比较好定位,然后通过直接对机器码的变换来实现每一次不同的变形。事实上很多指令长度是固定的,比如 push一个寄存器,不管你push的是什么(正常的寄存器,FS之类的不算),都是一个字节——甚至,我们可以一个小公式来算出来:

pushcode = '"x50' + register;

其中,每一个寄存器的对应值是:

EAX = 0, ECX = 1, EDX = 2, EBX = 3, ESP = 4, EBP = 5, ESI = 6, EDI = 7

直接带入register就可以算出来。即倘若是eax,那么对应该是'"x50' + 0,push eax的指令就是0x50。
这样的公式有什么用呢?如果我们写了一段汇编的代码,里面某些指令全部用的eax操作的,而且这些指令的位置我们清楚地知道,我们就能通过这样一类公式来 直接计算,同样的操作替换成其他寄存器(比如ecx)的时候相对应的机器码应该是什么,然后用新计算出来的替换掉老的,这不就是一个变形了么?
这一步可以完全由计算机来做,而不用人手工替换,需要的代价是我们写出一个类似于汇编编译器的部分,这是可以实现的。上面的公式也并不是拼凑出来的,而是 汇编在编译的时候遵从的一些约定,限于篇幅,这一类的公式就不一一给出了,如果有兴趣的话,可以查看汇编手册,自己稍微花点时间来列举一下。
感性化一点的解释可以是这样,我们用四川话、北京(普通)话和英语来表示不同的寄存器操作,假设有一个完成某项任务的指令表示为:

小样 tonight 在哪儿 eat 白食? [普通话 + 英语]

然后按照公式(如果有的话,嘿嘿)计算出普通话对应的四川话和英语对应的普通话,然后替换掉,就成了:

龟儿 今晚 到哪点 吃 趴活?

替换后还是一个意思,都是问对方晚上那里蹭饭去了。但是,没有两个字重复是不是,这就是替换寄存器的变形。
上面说的最后替换出来的长度都一样,如果变换寄存器后指令长度不一样(比如xor xxx, xxx),那就很麻烦,我们留在后面的“插入无关语句”部分去说。
还有需要提醒的是,并不是随便什么寄存器都可以拿来随便用,如果你有loop一类的指令,那就要避免改变ecx的值,如果在栈上有操作,就不要把寄存器替 换成esp或者ebp。类似需要注意到的地方还有很多,在你写的时候,注意一下就可以,不是什么很重要的问题,所以就不详细的写了。

“昨晚你丫” VS “你丫昨晚” ——调整块的顺序

我们说话的时候,如果前后没有什么很大的联系的话,语序的调换也不会影响表达。像两个人见面的时候要问“在哪里发财啊?老婆孩子好啊?火车转弯灯要不 要?”,这三句话前后没有必然的逻辑关系,所以按句子进行整体的调换也没有什么问题,先问对方要不要火车转弯等也行。
同样的,如果shellcode中也像这样有着明显的功能模块,调换其前后顺序不会产生影响,这是最大粒度上的通过调换顺序的变形。
粒度更小的,可以是调换若干单一指令之间的顺序,当然前提条件是不能改变表达的意思,例如有一句话是“一边打电话一边打架”,换成“一边打架一边打电话”也无妨,前提条件就是互相不影响,结果不会给别人造成困扰就行。
还有一种中间粒度的调换,这个例子不好举,只好用比较形式化的方式来描述一下:比如有两个操作序列A和B,分别完成两个功能且互相不干扰(包括寄存器), A的序列是A1/A2/A3/A4……,B的序列是B1/B2/B3/B4……,然后我们像洗牌一样把这两个序列洗一下,得出来的结果还是没有问题的,像 序列A1/A2/B1/A3/B2/B3/B4/A4或者是序列A1/B1/B2/B3/B4/A2/A3/A4都没有关系,只要保证抽取出来的所有关于 A的序列和所有关于B的序列都正常即可。
一个不太恰当的例子是用两种语言来说两件事情,普通话对应上面的A,英语对应上面的B。普通话是“火车的 转弯灯 要不要?”,英语是“I want home”,那么这样说是可以的:

火车的 I 转弯灯 要不要 want home

这样也可以:

I want 火车的 转弯灯 home 要不要

因 为他们分别表达了不同的意思,而且互相之间不干扰理解。但是如果相互之间有干扰的话,这种方法就不在适用,比如“火车的 转弯灯 要不要”和“你 装不装 宽带网”,打乱顺序组合一下就有可能是“火车的 你 装不装 转弯灯 宽带网 要不要”,似乎成了问别人装不装转弯灯,意思完全变了。
总之,调换块的顺序就是在不影响表达的情况下调换可能的顺序。通常上面三种粒度上的调换是综合使用的,第一个最简单,实现其来也最方便,最后一个比较麻烦,而且适用的范围似乎不是很广。

“你” vs “你!@#$#!@#$”——插入无关的字句

插入无关的字句作为变形是最容易理解的,这种方法的基本出发点就是只要不是喧宾夺主的废话,基本上都可以被省略掉。从这个意义上来说,我写的这篇文章也在 大量的进行变形,尤其以插入哈哈、嘻嘻之类没有意义的助词或者是拟声词为最,可是在你读起来,最多嘴角向上一翘,并没有影响你了解文章的内容,这就是成功 的变形。
插入的字句一般被称之为nop-like,从字面上理解就是插入一些类似于NOP的指令。这种指令包括很多,最常见的就是0x90即NOP,这个指令还有 另外一个解释(也许是原本的解释),那就是xchg eax, eax——这就是一个纯粹的没用的指令,效果类似于文章中的“呵呵”。同样的,xchg exx, exx基本上都是,不过除了交换两个eax,交换其他两个相同的寄存器最后生成出来指令长度都是两个字节,所以大家不爱用。
除了这种纯粹没用的指令以外,还可以选择一些对偶指令,让其执行后没有任何影响。比如说,inc edx + dec edx一类的,开始对edx进行增一操作,然后马上又进行edx的减一操作,最后对任何东西几乎都没有影响(对标志位还是可能有影响),这有点类似于一个 人说话,开始说了一句“我天下无敌了”然后接上一句“那是不可能的”,别人听了以后觉得没有什么问题,你的正常表述几乎不受任何影响。
还有一类的无关语句是在其他寄存器上的操作。比如我整个要变形的部分只用到了eax和ecx,那么中间无论什么地方插入对ebx的操作,都没有问题。这个 有点像说话的时候,你一直用普通话来表达你所要说的,这时候突然你走火说了两句英文,好像也没有多大的关系,至少别人理解你要表达的东西上没有受到影响。
插入无关的字句理论上最简单,但在实践过程中可能存在一个巨大的问题,那就是一些相对跳转或者是相对调用的指令会受到影响。举个最简单的例子而言:

a: xor dword ptr [eax], ebx
loop a

a: xor dword ptr [eax], ebx
nop
loop a

两个完全一样功能的代码,后面一个是前面一个的变形。但是单单的插入一个nop到前面一个去肯定会出错,因为前面一个和后面一个loop a两个编译出来指令码相差1,这是不得不考虑的!前面说所的,替换寄存器后指令长度是否一样也是一个重要问题,就指的这些方面——我们不得不精心的去调整 我们需要变形的部分,而这种调整最主要的应该集中到相对跳转类指令上去,避免它们出问题。

其他,相同语义的不同表达

写完一本书,可以叫杀青,也可以直接说写完了准备付梓,这是同一种语义的不同表达方式。一般意义上而言,所有绑定cmd的shellcode都可以看作是 同样语义(目的)的不同实现,当然可以算作一种变形。只不过这种变形似乎代价稍微大了一点,而且不容易自动化生成。
小范围而言,这种相同语义的不同表达是可以的,前面说的替换寄存器就是简单的实现。另外的,把jmp替换成jno和jo或者是诸如此类的东西,也是一个不 错的变形方案,这种变形可能需要一个很大的库来支持,而且相同语义的选择需要人为的预先定义。一旦有了一个庞大的库来支持,剩下的事情就是选择其中的某些 内容来进行组合,工作量就小多了,而且效果是很好的。因为实现起来比较困难,所以这个使用不是太普遍,简单提一下就好。

基础性的东西介绍完毕以后,下面就可以开始实战操练了。

posted on 2007-08-24 16:54  dhb133  阅读(1062)  评论(0编辑  收藏  举报

导航