之前在学二进制,文中给出了一个人为构造的存在栈溢出的文件,借此来分析栈溢出。
另外文章没有高深的技术,只是刚刚二进制入门的水平。
先声明,样本来自于<0day安全>一书的2.4节,文章也是通过该文学习后进行的独立分析。
(其实文章一直都有写,但是零零散散只言片语的,不成体系,不好意思发出来,想想好久没给90发文章了,借此机会写一篇分析,也便于自己梳理整个流程)
源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
中,这个过程实际上就是把一串内存中的内容复制到另一个内存地址中,但是代码没有进行边界检查,就会导致复制的内容超出了原来申请的内存范围,从而导致多出来的内容进入了其他未申请的内存地址中,由于被错误覆盖的内存原本是用于其他逻辑的,所以就会造成程序逻辑上的错误。本质上是申请用于缓冲的内存小于用于缓冲的内容,造成了溢出,即缓冲区溢出。
函数定位
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寄存器用于标志当前栈底,从而计算出当前栈帧范围,函数的返回相当于mov
和jmp
,把结果mov
到eax
,然后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
不过由于没有安全退出,会导致确定后异常退出。