定制自己的shellcode
F.Zh这个名字被女友无情枪毙,以后这个系列就换成沙布拉尼尔为大家介绍了。
定制自己的shellcode无非是两个原因:exploit的特殊要求,或者是逃脱各种杀毒软件或者IDS什么的。上次说了一个把shellcode先 拆分然后再组合,其结果是会加长shellcode的字节一倍左右,这次再来说说其他的方法,难度可能稍微大一些,但是相应的效果会更好。
在这里依然作一个约定,首先,shellcode是在原来的基础上修改,也就是说,我们只考虑编码的方式,而具体的shellcode编写方法,外面其实 已经有介绍了;其次,shellcode一般分为前面的decode部分和后面编码过的真正起作用的部分,没有特殊的说明,文中出现的shellcode 都指后面一部分。
依然以一个具体的要求来作为引子,我们一步一步地去构造。假设我们需要的shellcode长度不能超过500字符(这个其实已经很宽松了),而且只能作 为非大写字母的文件名,也就是意味着我们不希望有小写字母、点号、星号、问号还有左右斜杠等出现。总的来说,这还是算一个比较苛刻的条件,如果没有长度的 限制,上面一篇文章已经能够很好的解决了,这里对长度的要求有点死,我们来试试看其他的路子。
倘若将起作用的shellcode看成一个没有规律的字符串,首要的一个工作是用一个较为简单的算法(之所以说要较为简单的算法,是为了解码部分的编写工 作量不至于太大),把这个乱七八糟的字串编码,以避开那些禁用的字符。从可操作性上而言,用异或的方法无疑是最快最好的,因为异或操作映射后的结果比较发 散,相比单独的与或操作可以得到的结果更多,更重要的是这是一个可逆的运算,便于我们还原。我记得上次说过如果shellcode依次与多个字符异或,那 么出现特殊字符的几率将会大大的减小,对于这样的说法,我们做理论分析如下。
下面我们来看这种字符串等价划分方法。
用图表示一下这个等价划分方法是这样的,其中SSSS表示原来的shellcode,abcd表示标识。
SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS...
----------------------------------------------------
abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcda...
如果是一个模四的等价划分,即为所有原来字符第0、4、8……个字符重新排列成一个字符串(也就是所有标识为a的S),而第1、5、9……个字符重新排列 成另一个字符串(标识为b的S),以此类推。这样划分出来的若干个字符串,相对来说是独立的,可以单独的进行运算而互相不影响。
需要说明的是,一般的编码方法只是考虑不要出现结束符""x00"就可以,如果要使得编码后不出现上面这么多的字符,只和一个简单字符做异或运算能成功的 几率还是比较小的。考虑一个足够长而且字符分布均匀的字串,进行等价划分后得到的字串和原来应该还是有着相似的性质,即同样分布均匀而且异或任何一个字符 后的结果还是几乎覆盖了全部字符(不用证明了吧……),但事实上shellcode的长度有限而且内容相对固定,所以采用等价划分的方法后取出的每个字符 串,在与某个字符异或后的结果在全部ASCII字符中的分布情况肯定相对集中而且稀疏(有很多不会出现),一旦所有禁用字符都没有出现,这个划分就应该是 成功的。而且显而易见的,等价子串的长度越小,这种划分成功的几率越大。
关于这个结论,还有一个更加直观理解方法。我们始终假设字符串的分布均匀,所有的ASCII码总共是255个(抛开不能用的0x00),长度为300的字 符串经过异或以后分布肯定还是比较均匀的,但是一个只有8个字符的字串异或后就很有可能最多只有8个不同字符,不出现禁用字符的可能性相当大。
上面所有的讨论都是理论上的,对于出现的几率,分析的时候可以说“很大”或者“很小”,然而具体的设计中只有成功或者失败,即使很小的概率也不能保证成 功,现实世界一个特点就是小概率事件往往还是会发生(就象我被三楼扔下来的篮球砸到过一样),不做做看是不晓得的。
划分等价类的时候,要决定划分的依据是模几,这个数字虽然说是越小越好,但是还是要实际的去选择,先写个程序来选择一下,当然,选择的标准是,划分后每一个子串都可以与某个字符异或而不产生禁用的字符。
1 for(int wokao=0; wokao<para; wokao++)
2 {
3 printf("%d :",wokao);
4 int ko=(sizeof(sc)-1)/para;
5 for(int i=0; i<ko; i++)
6 p[i] = sc[i*para+wokao];
7 for(i=1; i<255; i++)
8 {
9 for(int j=0; j<ko; j++)
10 if(inbadchar(p[j]^i))
11 goto l;
12 playboy = i;
13 if(!inbadchar(playboy))
14 {
15 printf("0x%0.2x,", playboy);
16 temps[wokao] = playboy;
17 }
18 exit(0);
19 l:;
20 }
21 printf(""n");
22 }
这个微小的片断可以描述我们选择的依据。参数para作为我们划分等价子串的标准,在写出具体的判定程序时候,最好能是一个动态确定的数。ko是每一个子 串的长度,在内层的第一个for循环中,ko作为循环的长度,依次把原来shellcode中的数据取到临时一个串p[]里面去,每一次大的循环取一条子 串,当外层循环做完的时候,一个划分也完毕了。
从第7行开始,是一个划分是否成功的判断。前面说过,成功的必要条件是这个子串能够与某一个字符异或而不出现特殊字符,9行到11行就是做这个工作的。 12行记录下这个字符即playboy,如果整个子串与playboy异或而不产生特殊字符,那么这一个子串是完美的,如果所有的子串都完美,那么这个划 分就是完美的。
细心的你也许发现了一个问题,为什么还要判断playboy是否是禁用字符呢?其实这是为了以后的考虑,因为编码后的shellcode满足要求只是我们 完全成功的一个必要条件,另外一个必要条件是解码部分也不能出现禁用的字符。一般来说,playboy这些字符在解码部分会原封不动的出现(XOR指令编 译成机器码后),我们固然可以用寄存器加上运算的方法来避免,但是这样的工作量太大,已经远远的超出了我们的想象。
在多次修改para的值以后,你会发现最好的数字是13。其实很多情况下我们只要知道这个数字就可以了,上面很多分析只是提供思路上的一个说明,告诉人家 这个数字怎么得到而已。我们可以想象,para这个数字很小的话,便于解码部分的书写,如果太大的话,shellcode编码后的数据产生禁用字符的可能 性减小了,但是解码部分又太难书写,折衷下来,还是7或者13比较好。当然,这是经验性质的,遇到特殊情况,你还是要自己算。在计算的时候,有些有趣的结 论可以在这里说一下,如果你发现para为N的划分可以满足要求的话,那么N的倍数的划分肯定是可以的,而反之则不然,所以最好从小往大去试那个 para。
那么,我们以13作为等价划分的标准做一个例子,其他划分标准原理是一样的。
以13为划分标准的话,首先,我们的shellcode被我们分成了13个子串。按照下图所表示,相同子母标识的在同一个子串中,相应的与不同的字符异或。
SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS...
---------------------------------------------------------
abcdefghijklmabcdefghijklmabcdefghijklmabcdefghijklmab...
可以看到,动态的把原始的shellcode编码以后,进行解码的时候是一个长度为13的循环解码过程。上面那段小程序把para改为13就是完整的动态 编码,最后在temps这个字符串中,记录的结果是这13个满足条件的异或字符,用直观的表来表示的话,我们假设原始的shellcode是ssss,编 码后的shellcode是tttt,而满足条件的异或字符是a~m,下面就是编码的过程:
ssssssssssssssssssssssssssssssssssssssssssssssssssssss...
XOR) abcdefghijklmabcdefghijklmabcdefghijklmabcdefghijklmab...
--------------------------------------------------------------
tttttttttttttttttttttttttttttttttttttttttttttttttttttt...
异或的可逆性决定了其逆向解码的过程是:
tttttttttttttttttttttttttttttttttttttttttttttttttttttt...
XOR) abcdefghijklmabcdefghijklmabcdefghijklmabcdefghijklmab...
--------------------------------------------------------------
ssssssssssssssssssssssssssssssssssssssssssssssssssssss...
由于固定了等价划分的标准是13,上面的程序可以帮助我们生成满足条件的异或字符(表示为a~m的)。剩下的工作是来看解码的部分如何手工的编写。
我们的个人计算机通常字长是32位的,也就是说是4个字节长,如果是要依次与13个字符循环进行异或运算,除了比较通用的嵌套循环的方法外,我们可以单用 一层循环(这样比较好控制),即把这13个字符拆成三个四字节长的整数和一个单字节的字符,每循环一次就用这些东西和编码后的shellcode异或运算 一次。
根据《定制自己的shellcode(一)》中所给的解码部分的模版,很容易写出解码部分如下:
_asm
{
jmp short getaddr
dstart:
pop ebx
xor ecx, ecx
mov cx, 0x112
xor esi, esi
decode:
xor DWORD ptr [ebx+esi], 0xD4FDFBE7
add esi, 4
xor DWORD ptr [ebx+esi], 0xFBE7FCF4
add esi, 4
xor DWORD ptr [ebx+esi], 0x61FA72F9
add esi, 4
xor [ebx+esi], 0xF4
add esi, 1
nop
loop decode
jmp short scstart
getaddr:
call dstart
scstart:
nop
}
和以往一样,用一个jmp和一个call来获得编码后的shellcode开始位置。cx是控制循环次数的,按道理最少应该是shellcode的长度除 以13,这里为了方便,就固定下来一个数字。esi用来控制偏移量,也就是控制解码到了shellcode的第多少个字符。后面从decode:开始的段 就是上面说的那种方法,依次与三个四字节长的整数和一个单字节的字符运算解码。值得注意的是,这里的操作数不是固定的,按照我们前面所述,这一串的操作数 都是动态生成的,在编写程序的时候是要动态的填入,所以这里先是随便写了一些数字,只要确定一下位置就是。
编译后可以收集一下,这一段代码的机器码就是:
""xeb"x33"x5b"x33"xc9"x66"xb9"x12"x01"x33"xf6"x81"x34"x33"xe7"xfb"xfd"xd4"x83"xc6"x04"x81"x34"x33"xf4"xfc"xe7"xfb"x83"xc6"x04"x81"x34"x33"xf9"x72"xfa"x61"x83"xc6"x04"x80"x34"x33"xf4"x83"xc6"x01"x90"xe2"xd8"xeb"x05"xe8"xc8"xff"xff"xff";
四个需要动态确定的常量在这一段代码中的偏移分别是14,24,34和44。
写到这里发句牢骚,decode部分无疑是要精心构造的,但是这偏偏是一个非常费体力的活动,没有什么技巧,就是道理简单运算复杂。我在这里只是单纯给出 这一段,因为方法前面已经反复说过,而且麻烦的是具体做起来是不可能手把手的去教。老吴子叫我给录像,我的个歪歪(请用扬州普通话读),我这么笨的菜鸟调 一个shellcode超过了8小时,且不说录像后的文件多大,录下来了是否有人看都是一个大大的问题——反正如果是我的话,有时间还不如去看三场电影!
扯远了。言归正传,编码和解码的架子都已经搭好,剩下的就是组装,前面有了怎样计算那些个异或字符的代码片断,收集起来写到解码部分,解码部分就已经完整,为了方便调用起见,这个我写成了一个函数。
void MakeDecode(int i, int j, int k, unsigned char l)
{
*(int*)(de+14) = i;
*(int*)(de+24) = j;
*(int*)(de+34) = k;
*(de+44) = l;
}
三个变量i,j,k是头十二个字符,l是最后一个字符,这个函数可以完成解码部分的填充工作。前面说到,计算出来的变量在temps[]里面,拿出来调用这个函数即可。
shellcode部分也要预先与temps[]这个串异或运算以逃脱禁用的字符。这部分工作也是依据temps[]来进行的,其实和解码部分的工作一模 一样(说白了,就是因为相同的异或做两遍就还原了)。代码稍微长了点,好在算法比较容易,可以参见光盘里面的代码。另外,这一段其实可以在计算temps []的过程中就做掉,不过为了便于理解,我将他们分开了。
说到这里这个算法差不多就已经算是完了,几个重点的问题已解决,只要把解码部分和编码后的shellcode部分连接在一起就成功的完成了所有的任务。完 整的可用代码我会在光盘里面一并给出,按照main中的用法,就可以直接构造变态要求的expoit。对于禁用字符的定义,在源代码的badchar中给 出了一些,你也可以在一定程度上进行修改,但是不要太过分了,太多字符不能出现的话,这个构造shellcode的方法是会失败的。
在上一篇文章的最后我提到过这种方法,里面说的是倾向于标准为四或者是八的划分,想来是臆断了,实际的情况是这个数字通常在七到十七之间。剩下没有解释的 代码都很简单,大家可以直接拿来用而不用去管。其实这两篇文章都是关于shellcode的一些想法和简单的算法,我也只是有点心得就写了出来,让高手见 笑了。另外,如果你有什么更好的想法,也希望你能够写出来与我们共享。