一个栈溢出样本的详细分析(一)

引言

之前在学二进制,文中给出了一个人为构造的存在栈溢出的文件,借此来分析栈溢出。

另外文章没有高深的技术,只是刚刚二进制入门的水平。

先声明,样本来自于<0day安全>一书的2.4节,文章也是通过该文学习后进行的独立分析。

(其实文章一直都有写,但是零零散散只言片语的,不成体系,不好意思发出来,想想好久没给90发文章了,借此机会写一篇分析,也便于自己梳理整个流程)

从C代码开始

源C代码如下

#include <stdio.h>
#include <windows.h>
#define PASSWORD "1234567"
int verify_password (char *password)
{
	int authenticated;
	char buffer[44];
	authenticated=strcmp(password,PASSWORD);
	strcpy(buffer,password);//over flowed here!	
	return authenticated;
}
main()
{
	int valid_flag=0;
	char password[1024];
	FILE * fp;
	LoadLibrary("user32.dll");//prepare for messagebox
	if(!(fp=fopen("password.txt","rw+")))
	{
		exit(0);
	}
	fscanf(fp,"%s",password);
	valid_flag = verify_password(password);
	if(valid_flag)
	{
		printf("incorrect password!\n");
	}
	else
	{
		printf("Congratulation! You have passed the verification!\n");
	}
	fclose(fp);
}

从上面的代码中可以看到,在主函数中程序调用了verify_password来进行密码正误的判断。

我们先来看一下verify_password函数做了哪些事。

int authenticated; 申明了一个int类型的变量,我们知道C语言中int占4个字节,那么也就是说,这里相当于程序在内存中申请了四个字节的空间。

char buffer[44]; 申明了一个char类型的数组,一个char占一个字节,这里一共申请了44个字节的空间,不过需要注意的是,由于字符串以\x00为结尾,因此可用安全空间为43个字节。

authenticated=strcmp(password,PASSWORD); 调用了strcmp函数来判定密码是否相同,至于该函数返回值,这里摘取自其他资料

int strcmp(const char *s1, const char *s2);
若参数s1 和s2 字符串相同则返回0。s1 若大于s2 则返回大于0 的值。s1 若小于s2 则返回小于0 的值

实际上也就是,s1和s2相同时返回0,s1 > s2 时返回 1,s1 < s2 时返回 -1,至于大小的比较,则是逐个按照ascii进行比较的。

那么这句代码,就会给上面为authenticated申请的空间赋值。

strcpy(buffer,password); 这里调用了strcpy,这个函数将会把password的内容复制到buffer中,这个过程实际上就是把一串内存中的内容复制到另一个内存地址中,但是代码没有进行边界检查,就会导致复制的内容超出了原来申请的内存范围,从而导致多出来的内容进入了其他未申请的内存地址中,由于被错误覆盖的内存原本是用于其他逻辑的,所以就会造成程序逻辑上的错误。本质上是申请用于缓冲的内存小于用于缓冲的内容,造成了溢出,即缓冲区溢出。

到ollydbg动态分析

函数定位

ida定位到strcpy调用位置,然后动态调试进行相关栈区的观察。

IDA打开进入main函数,打开流程图检查

跟进call,call也就是调用函数的过程。

跟进jmp地址

拿到地址00401054

细节分析

OD打开,直接在command下命令断下bp 00401054,F9运行

可以看到在call前面进行了两个push,这里就是参数入参的过程了,下面我们稍微研究一下gcc编译器是怎么处理的。

00401046  |.  83C4 08       add esp,0x8
00401049  |.  8945 FC       mov [local.1],eax
0040104C  |.  8B4D 08       mov ecx,[arg.1]
0040104F  |.  51            push ecx
00401050  |.  8D55 D0       lea edx,[local.12]
00401053  |.  52            push edx
00401054  |.  E8 47010000   call stack_ov.004011A0                   ;  strcpy

F2断在00401046处,Ctrl + F2重载一下,然后F9运行。

可以看到当前栈顶是两个数值,这个是前一个函数strcmp调用之前压入到内存的数据,参数入栈,函数调用,参数出栈是一个既定的调用函数的规矩,当函数调用完了之后就应该恢复栈原先的样子。因此下一步通过修改当前栈顶位置来恢复到原先的位置,也就是直接对esp进行操作。

F7跟进。

也就是说这里是为了上一个函数调用的堆栈平衡做的操作。

然后下一步mov [local.1],eax,大多数情况下eax会用来存放函数的运行结果,所以这里的eax保存的是上一个函数的结果,也就是00000001。而这里的[local.1]是ollydbg所创造的变量。选中这条代码可以看到OD给出的提示,包括,双击可以看到变量所对应的真实地址。

这句我们就可以知道,内存地址0012FB1C里的数据目前是CCCCCCCC,但是马上要变成00000001了,我们可以在堆栈窗口中跟随看一下,OD中的跳转到指定位置就是Ctrl+G,很多编辑器都是这个。

F7一下再看

现在在这里存了strcmp的结果,下面的代码是为strcpy做准备。因为strcpy处理的是char[],在C语言中数组传入的就是指针,也就是内存地址,那么这里push的自然就是内存地址。

我们可以看到这里给出的提示是,0012FB28的内容是0012FB7C,而0012FB7C的内容才是4321432143214321...(从文件中读入的密码,无关紧要)

堆栈中跟随

可以看到在堆栈这边,0012FB28的内容是0012FB7C,但是右边给出了43214321的提示,我们进入0012FB7C

这里才是真的数据来源。

回到当前指令,0040104C |. 8B4D 08 mov ecx,[arg.1]将指向数据的内存地址赋给了ecx寄存器,然后0040104F |. 51 push ecx一句将ecx内容压栈,此时内存中就有了一个指向数据的内存地址,如图

然后下一步是

00401050  |.  8D55 D0       lea edx,[local.12]
00401053  |.  52            push edx

local.12赋给edx寄存器,和mov指令不同,local.12代表0012FAF0,如果是lea指令,则将0012FAF0赋值到edx,mov指令则将0012FAF0里的内容赋值到edx。

总之是另一个压入地址的操作。具体来说上一个压入的是被复制数据的地址,这里压入的是要被复制到的目标地址。目前目标地址为0012FAF0,我们先观察一下目前堆栈。

是传说中的烫烫烫没错了,我们还能看到下面的刚才压入的strcmp的结果00000001。在地址双击可以变成相对当前的偏移地址

我们不在意这个strcpy的执行,只要看执行完了发生了什么就好,所以F8步过。

复制完成了,然后下面原来的00000001变成了00000000,这个就是栈溢出导致的问题了,字符数组后面会带一个00,00溢出覆盖了原来的strcmp结果。

而更多的,因为下面就是EBP和返回地址,所以还可以覆盖EBP和返回地址。

EBP寄存器用于标志当前栈底,从而计算出当前栈帧范围,函数的返回相当于movjmp,把结果moveax,然后jmp到原来调用的位置,另外再增加一些用于堆栈平衡的操作。

那么控制返回地址,就可以跳转到任意地址进行继续代码的运行。我们只要在一个地方布置上shellcode,然后跳转就ok了。

为了兼容不同版本系统,应跳转到动态链接库中的地址,比如动态库中有个jmp ebp,这个指令的地址为xxxxxxx,那么填这个地址,然后想办法在相应位置布置对应shellcode。

已有shellcode如下(环境:吾爱破解专用虚拟机),可调出一个messagebox

33DB536877657374686661696C8BC453505053B85C08D577FFD0

现在就是布局了

shellcode布局(demo)

从前面我们知道,字符串被复制到这个地方

$ ==>    > CCCCCCCC  烫烫
$+4      > CCCCCCCC  烫烫
$+8      > CCCCCCCC  烫烫
$+C      > CCCCCCCC  烫烫
$+10     > CCCCCCCC  烫烫
$+14     > CCCCCCCC  烫烫
$+18     > CCCCCCCC  烫烫
$+1C     > CCCCCCCC  烫烫
$+20     > CCCCCCCC  烫烫
$+24     > CCCCCCCC  烫烫
$+28     > CCCCCCCC  烫烫
$+2C     > 00000001  ...
$+30     >/0012FF80  €.
$+34     >|00401118  @.  返回到 stack_ov.mainshort_argingAistdstalled+88 来自 stack_ov.00401005

这样换成相对地址,就很清晰,一共是(34+4)H个,也就是56个字节,刚好覆盖掉返回地址。作为demo来说,这里返回地址是写死的,并不通用,实际上在内存中有两个地方是存在来自txt的字符串的,一个是我们覆盖进来的,一个是读取进来的。覆盖进来的这个地址为0012FAF0,读取进来的,我们看一下之前压栈的操作,可以找到是0012FB7C,写死的话,这两块都可以用。
然后我们可以在可控的地方写入shellcode,无关的地方可以用90填充,也就是nop,然后把返回地址覆盖成shellcode的入口。

最后的结果大概如下

33DB536877657374686661696C8BC453505053B85C08D577FFD09090909090909090909090909090909090909090909090909090F0FA1200

有一个问题是,由于password由fscanf(fp,"%s",password);一句读取,%s 将导致在遇到空白字符时将停止读取,参考链接,而且读取的字符串以00为结尾标志,因此shellcode中不能出现09 00 20 0a 0d,除非是在结尾,就像上面这个。

产生的结果就是这样。此时运行,就可以弹一个msg了。

地址换另一个也可以。

33DB536877657374686661696C8BC453505053B85C08D577FFD090909090909090909090909090909090909090909090909090907CFB1200

不过由于没有安全退出,会导致确定后异常退出。

3 Likes

加油,二进制会越来越难,你走二进制建议先从linux用户态开始 :)

1 Like

谢谢表哥~!:laughing:

都开始二进制了……膜啊

1 Like