漏洞利用原理(初级)

栈溢出原理与实践

    • 系统栈的工作原理
        • 内存的不同用途
        • 栈与系统栈
        • 函数调用的细节
        • 寄存器与函数栈帧
        • 函数调用约定与相关指令
    • 修改邻接变量
        • 修改邻接变量的原理
        • 突破密码验证程序
    • 修改函数返回地址
        • 返回地址与程序流程
        • 控制程序的执行流程
    • 代码植入
        • 代码植入的原理
        • 向程序中植入代码
            • 参看文献

系统栈的工作原理

内存的不同用途

进程使用的内存可以大致分为4类:

  • 代码区:存储着被装入执行的二进制机器代码,处理器会到此区域取指并执行
  • 数据区:存储全局变量等
  • 堆区:进程可以在堆区动态地请求一定大小的内存,用完之后归还堆区(动态分配和回收
  • 栈区:动态地存储函数之间的调用关系,保证被调用函数在返回时恢复到母函数中继续执行

栈与系统栈

内存的栈实际上指的是系统栈。
与栈相关的概念:

  • 入栈——PUSH
  • 出栈——POP
  • 栈顶——TOP
  • 栈底——BASE

函数调用的细节

示例代码:

#include 

int func_B(int arg_B1, int arg_B2)
{
	int var_B1, var_B2;
	var_B1=arg_B1+arg_B2;
	var_B2=arg_B1-arg_B2;
	return var_B1*var_B2;
}

int func_A(int arg_A1, int arg_A2)
{
	int var_A;
	var_A=func_B(arg_A1, arg_A2)+arg_A1;
	return var_A;
}

int main(int argc, char **argv, char **envp)
{
	int var_main;
	var_main=func_A(4,3);
	return var_main;
}
  • 不同函数的代码在内存代码区的位置是混乱无关的
  • CPU的取指轨迹
    漏洞利用原理(初级)_第1张图片
  • 函数调用过程中,系统栈中的操作
    漏洞利用原理(初级)_第2张图片

寄存器与函数栈帧

每个函数独占自己的栈帧空间,正在运行的函数的栈帧永远在栈顶。
寄存器:

  • ESP:栈顶
  • EBP:栈底
  • EIP:下一条等待执行的指令地址

函数栈帧中包含的主要信息

  • 局部变量
  • 栈帧状态值:保存当前栈帧的顶部与底部(实际只保存底部),用于本帧被弹出后恢复上一个栈帧
  • 函数返回地址:保存当前函数调用前的“断点”信息,即函数调用前的指令位置,以便返回时能继续执行后续代码

函数调用约定与相关指令

函数调用约定:描述了函数传递参数方式和栈协同工作的细节
调用方式差异
针对VC++,有三种函数调用约定(默认为_stdcall):
漏洞利用原理(初级)_第3张图片
函数调用的大致步骤:

  • 参数入栈:参数从右向左依此压入系统栈中
  • 返回地址入栈:将调用代码指令的下一条指令地址压入栈中
  • 代码区跳转:处理器从当前代码区跳转到被调用函数的入口处
  • 栈帧调整
    • 保存当前栈帧状态(EBP入栈)
    • 将当前栈帧切换到新栈帧(ESP值装入EBP)
    • 给新栈帧分配空间(把ESP减去所需空间大小,提高栈顶)
      伪代码:
push 参数3;
push 参数2;
push 参数1;

call 函数地址;

push ebp;
mov ebp, esp;
sub esp, XXX;

函数返回的大致步骤:

  • 保存返回值:一般存储在EAX中
  • 弹出当前栈帧,恢复上一个栈帧
    • 在堆栈平衡的基础上,给ESP加上栈帧大小,降低栈顶,回收当前栈帧
    • 将当前栈帧底部保存的前栈帧EBP值弹入EBP寄存器,恢复上一栈帧
    • 将函数返回地址弹给EIP寄存器
  • 跳转:跳回母函数中继续执行
    伪代码:
add esp, xxx;
pop ebp;
retn;

修改邻接变量

修改邻接变量的原理

示例代码:

#include 
#include 
#define PASSWORD "123456"

int verify_password(char *password)
{
	int authenticated;
	char buffer[8];    // add local buffto be overflowed
	authenticated=strcmp(password,PASSWORD);
	strcpy(buffer,password);  //over flowed here
	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("Congratulation! You have passed the verification!\n");
			break;
		}
	}
}

代码执行到verify_password函数时的栈帧状态:
漏洞利用原理(初级)_第4张图片
可以看到authenticated位于buffer变量下方,占4个字节,如果buffer越界,则buffer[8]、buffer[9]、buffer[10]、buffer[11]会写入其中。

突破密码验证程序

需要相应的环境进行编译,可下载书籍提供的编译好的EXE文件。
使用OD加载程序,输入“qqqqqqq”,运行到strcpy:
漏洞利用原理(初级)_第5张图片
漏洞利用原理(初级)_第6张图片
我们尝试输入"qqqqqqqrst":
漏洞利用原理(初级)_第7张图片
我们尝试输入“qqqqqqqq”:
漏洞利用原理(初级)_第8张图片
漏洞利用原理(初级)_第9张图片

需要注意:strcmp函数第一个字符串大于第二个返回1,小于返回-1,即0xFFFFFFFF,溢出后为0xFFFFFF00,无法成功。

修改函数返回地址

返回地址与程序流程

改写邻接变量对代码环境的要求相对苛刻,一般是瞄准栈帧最下方的EBP和函数返回地址等栈帧状态值。

再来看下上一个程序的相关信息:
漏洞利用原理(初级)_第10张图片
如果buffer[8]的输入还要长就会逐渐覆盖前栈帧EBP和返回地址。以输入“·4321432143214321432”为例:
漏洞利用原理(初级)_第11张图片
我们可以发现溢出成功。

控制程序的执行流程

由于键盘输入的ASCII码有限(0x11、0x12等无法输入),对源代码进行一定的修改,让程序从文件中读取字符串。

#include 
#include 
#include 
#define PASSWORD "123456"

int verify_password(char *password)
{
	int authenticated;
	char buffer[8];    // add local buffto be overflowed
	authenticated=strcmp(password,PASSWORD);
	strcpy(buffer,password);  //over flowed here
	return authenticated;
}

void main()
{
	int valid_flag=0;
	char password[1024];
	FILE * fp;
	if(!(fp=fopen("password.txt","rw+")))
	{
		exit(0);
	}
	fscanf(fp,"%s",password);    //从文件读取密码
	valid_flag=verify_password(password);
	if (valid_flag)
	{
		printf("incorrect password\n\n");
	}
	else
	{
		printf("Congratulation! You have passed the verification!\n");
	}
	fclose(fp);
}

我们的目的是尝试在输入错误密码的情况下修改返回地址,直接跳转到密码正确的地方继续执行:
漏洞利用原理(初级)_第12张图片
构建password.txt的文件:

  • 先写入5个“4321
  • 使用16进制将最后的4321修改为0x00401122
    漏洞利用原理(初级)_第13张图片

运行后:
漏洞利用原理(初级)_第14张图片
由于栈内EBP也被覆盖为无效值,使得程序在退出时堆栈无法平衡,导致崩溃。

代码植入

代码植入的原理

我们在buffer里包含自己要执行的代码,也可以通过修改返回地址跳转执行。
漏洞利用原理(初级)_第15张图片

向程序中植入代码

先对前面的代码进行修改:

#include 
#include 
#include 
#include     //为顺利调用LoadLibrary函数装载user32.dll
#define PASSWORD "123456"

int verify_password(char *password)
{
	int authenticated;
	char buffer[44];    // 修改了空间大小为填入代码提供条件
	authenticated=strcmp(password,PASSWORD);
	strcpy(buffer,password);  //over flowed here
	return authenticated;
}

void main()
{
	int valid_flag=0;
	char password[1024];
	FILE * fp;
	LoadLibrary("user32.dll");   //prepare for message
	if(!(fp=fopen("password.txt","rw+")))
	{
		exit(0);
	}
	fscanf(fp,"%s",password);    //从文件读取密码
	valid_flag=verify_password(password);
	if (valid_flag)
	{
		printf("incorrect password\n\n");
	}
	else
	{
		printf("Congratulation! You have passed the verification!\n");
	}
	fclose(fp);
}

目标是:调用Windows的API函数MessageBoxA,并显示“failwest”字样。

创建password.txt文件获取相关信息,在文件中写入11组“4321”,可以得到以下信息:
漏洞利用原理(初级)_第16张图片
汇编语言调用MessageboxA的步骤:

  • 装载动态链接库user32.dll
  • 在汇编语言中调用这个函数需要获得这个函数的入口地址
    • 工具:Dependency Walker
    • 入口地址为:0x77D40000+0x000404EA=0x77D804EA
  • 在调用前向栈中从右向左的顺序压入MessageboxA的四个参数
    漏洞利用原理(初级)_第17张图片

创建password.txt文件:
将上面的代码写入文件,53-56字节写入buffer起始地址0x0019FAB0,其余用0x90(nop指令)填充。

参看文献

《0day安全:软件漏洞分析技术》

你可能感兴趣的:(逆向,0day安全:软件漏洞分析技术)