栈溢出——邻接变量

一般而言,局部变量在栈中的分布是相邻的(但也可能出于编译优化等需要而出现例外)。如果这些局部变量中有数组之类的缓冲区,并且程序确实没有防护数组越界,那么越界的数组元素就有可能破坏栈中相邻变量的值,甚至破坏栈帧中所保存的EBP的值、返回地址等重要数据。

0x00 源码

又是一份密码验证功能的C程序代码:

#include <stdio.h>
#include <string.h>


#define PASSWD "qazwsxe"


int verify_password(char* password) {
	int authenticated;
	char buffer[8];
	authenticated = strcmp(password, PASSWD);
	strcpy(buffer, password);
	return authenticated;
}


void main() {
	int valid_flag;
	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;
		}
	}
}

栈溢出——邻接变量

程序的“初衷”是验证密码“qazwsxe”,如果输入错误将循环等待下次输入,只有输入正确了才能跳出循环。

程序使用TDM-GCC 9.2.0 32-bit Debug进行编译;务必避开Visual Studio系列编译的GS选项,这个选项会规避栈溢出。

0x01 分析

函数verify_password()新增了局部变量char buffer[8],注意是紧邻着变量authenticated的,这个声明位置很重要。另外,在字符串比较后有函数strcpy(buffer, password),没有发挥实现验证功能的作用,非常违和,仅仅是为了构成栈溢出漏洞。

当程序执行到调用函数verify_password()时,其栈帧状态如下所示:

char buffer[0~3](ASCII编码的输入前4位)
char buffer[4~7](ASCII编码的输入第5到8位,期望第8位为字符串截断符)
int authenticated
返回地址
上一个栈帧的EBP
形参:password
……

authenticated是int类型,在内存中占用一个DWORD,即4个字节。如果能让buffer数组越界,buffer[8]、buffer[9]、buffer[10]、buffer[11]将写入相邻变量authenticated中。

authenticated承接的是函数strcmp()的返回值,之后会返回给main函数作为密码验证成功与否的标志变量。按照strcmp()的设计,authenticated为0表示验证成功;任何非0值都表示不成功。

如果输入超过了7个字符(本来buffer第8位是用来存放字符串截断符NULL的,截断符必须占用1个字节),则越界字符的ASCII码会修改掉authenticated的值:恰好为0,则程序流程就会发生改变。

0x02 调试

先通过IDA获得函数strcpy()的VA:

栈溢出——邻接变量

使用x96dbg打开程序,在这个VA处下断点:

栈溢出——邻接变量

输入错误密码“zzzzzzz”,按照ASCII编码,有字符串的序关系“zzzzzzz”>“qazwsxe”,则strcmp()函数应当返回1,即预计authenticated会被赋值1。实际情况符合预期(断点后进行一次单步步过):

栈溢出——邻接变量

0x7A是小写字母“z”的ASCII编码,下方的0x00000001正是authenticated的值。栈帧数据分布情况一目了然。

局部变量名 内存地址 偏移3处的值 偏移2处的值 偏移1处的值 偏移0处的值
buffer[0~3] 0x0065FA94 0x7A('z') 0x7A('z') 0x7A('z') 0x7A('z')
buffer[4~7] 0x0065FA98 NULL 0x7A('z') 0x7A('z') 0x7A('z')
authenticated 0x0065FA9C 0x00 0x00 0x00 0x01

注意:实际上,变量authenticated在内存中存储为0x01000000(小端存储),由调试器自行反转显示以便阅读。所以偏移量(相对左边给出的内存地址的距离)从左至右依次为3、2、1、0。

0x03 溢出

下面尝试输入超过7个字符,看看越界数据能否写入authenticated的数据区。输入“zzzzzzzzyxw”(“w”、“x”、“y”、“z”的ASCII码依次递增1):

栈溢出——邻接变量

与预期一致,从第9个字符开始的数据被依次写入authenticated的数据区,即authenticated的值变为了0x00777879

此时的栈帧数据分布情况为:

局部变量名 内存地址 偏移3处的值 偏移2处的值 偏移1处的值 偏移0处的值
buffer[0~3] 0x0065FA94 0x7A('z') 0x7A('z') 0x7A('z') 0x7A('z')
buffer[4~7] 0x0065FA98 0x7A('z') 0x7A('z') 0x7A('z') 0x7A('z')
authenticated(被覆盖前) 0x0065FA9C 0x00 0x00 0x00 0x01
authenticated(被覆盖后) 0x0065FA9C NULL 0x77('w') 0x78('x') 0x79('y')

现已知溢出数据确实能够覆写authenticated,只需要使authenticated为0就能通过程序验证了。

输入8个字符时,第9位的截断符NULL(0x00)将会被写入内存0x0065FA9C,即下一个双字的低位字节,恰好将authenticated从0x00000001变为0x00000000

栈溢出——邻接变量

此时栈帧数据为:

局部变量名 内存地址 偏移3处的值 偏移2处的值 偏移1处的值 偏移0处的值
buffer[0~3] 0x0065FA94 0x7A('z') 0x7A('z') 0x7A('z') 0x7A('z')
buffer[4~7] 0x0065FA98 0x7A('z') 0x7A('z') 0x7A('z') 0x7A('z')
authenticated(被覆盖前) 0x0065FA9C 0x00 0x00 0x00 0x01
authenticated(被覆盖后) 0x0065FA9C 0x00 0x00 0x00 0x00(NULL)

结论就是非常简单,输入8位字符即可,让截断符去覆盖0x01

栈溢出——邻接变量

0x04 补充

并不是所有的8位字符串都可以绕过验证。

栈溢出——邻接变量

如果输入的字符串按照ASCII编码的序关系小于正确密码“qazwsxe”,如“aaaaaaa”<“qazwsxe”,strcmp()函数会返回-1,此时authenticated存储着补码形式的-1,即0xFFFFFFFF。这样NULL覆写后也只能变成0xFFFFFF00

上一篇:搭建并配置Keil嵌入式开发环境,完成一个基于STM32汇编程序的编写。


下一篇:西门子S7协议底层原理分析