之前堆栈基础(一)里面主要是window下面的堆栈,然而ctf里面大都是linux系统,因此还是很有必要分析一下Linux下的堆栈调用情况.
下面全篇都将以如下demo为例进行编译调试:
#include<stdio.h>
int sum(int i,int j){
int sum;
sum = i+j;
return sum;
}
int main(){
int i,j,k;
scanf("%d%d",&i,&j);
k = sum(i,j);
printf("%d\n",k);
}
linux下32位和64位是有区别的,我们先来看一下32位的
ubuntu下面编译32位的程序需要先安装依赖,安装命令为:
$ sudo apt-get install build-essential module-assistant
$ sudo apt-get install gcc-multilib g++-multilib
$ gcc -m32 -g -o pwn2_32 pwn2.c
gcc 添加-g参数是为了产生调试信息,方便后面gdb调试的时候可以看源码
我们来看一看objdump -d
命令反汇编出来的汇编指令.
0000056d <sum>:
56d: 55 push %ebp//保存旧栈帧
56e: 89 e5 mov %esp,%ebp//设置sum的栈帧ebp
570: 83 ec 10 sub $0x10,%esp//分配sum0x10h的堆栈大小
573: e8 86 00 00 00 call 5fe <__x86.get_pc_thunk.ax>
578: 05 88 1a 00 00 add $0x1a88,%eax
57d: 8b 55 08 mov 0x8(%ebp),%edx
580: 8b 45 0c mov 0xc(%ebp),%eax
583: 01 d0 add %edx,%eax
585: 89 45 fc mov %eax,-0x4(%ebp)
588: 8b 45 fc mov -0x4(%ebp),%eax
58b: c9 leave
58c: c3 ret
0000058d <main>:
58d: 8d 4c 24 04 lea 0x4(%esp),%ecx
591: 83 e4 f0 and $0xfffffff0,%esp
594: ff 71 fc pushl -0x4(%ecx)
597: 55 push %ebp
598: 89 e5 mov %esp,%ebp
59a: 53 push %ebx
59b: 51 push %ecx
59c: 83 ec 10 sub $0x10,%esp
59f: e8 cc fe ff ff call 470 <__x86.get_pc_thunk.bx>
5a4: 81 c3 5c 1a 00 00 add $0x1a5c,%ebx
5aa: 83 ec 04 sub $0x4,%esp
5ad: 8d 45 ec lea -0x14(%ebp),%eax
5b0: 50 push %eax
5b1: 8d 45 f0 lea -0x10(%ebp),%eax
5b4: 50 push %eax
5b5: 8d 83 90 e6 ff ff lea -0x1970(%ebx),%eax
5bb: 50 push %eax
5bc: e8 4f fe ff ff call 410 <__isoc99_scanf@plt>
5c1: 83 c4 10 add $0x10,%esp
5c4: 8b 55 ec mov -0x14(%ebp),%edx
5c7: 8b 45 f0 mov -0x10(%ebp),%eax
5ca: 83 ec 08 sub $0x8,%esp
5cd: 52 push %edx
5ce: 50 push %eax
5cf: e8 99 ff ff ff call 56d <sum>
5d4: 83 c4 10 add $0x10,%esp
5d7: 89 45 f4 mov %eax,-0xc(%ebp)
5da: 83 ec 08 sub $0x8,%esp
5dd: ff 75 f4 pushl -0xc(%ebp)
5e0: 8d 83 95 e6 ff ff lea -0x196b(%ebx),%eax
5e6: 50 push %eax
5e7: e8 04 fe ff ff call 3f0 <printf@plt>
5ec: 83 c4 10 add $0x10,%esp
5ef: b8 00 00 00 00 mov $0x0,%eax
5f4: 8d 65 f8 lea -0x8(%ebp),%esp
5f7: 59 pop %ecx
5f8: 5b pop %ebx
5f9: 5d pop %ebp
5fa: 8d 61 fc lea -0x4(%ecx),%esp
5fd: c3 ret
main函数前面有一段很有意思;
0000058d <main>:
58d: 8d 4c 24 04 lea 0x4(%esp),%ecx
591: 83 e4 f0 and $0xfffffff0,%esp
594: ff 71 fc pushl -0x4(%ecx)
粘上在http://stackoverflow.com/questions/4228261/understanding-the-purpose-of-some-assembly-statements 里面的回答:
This code makes sure that the stack is aligned to 16 bytes. After this operation esp will be less than or equal to what it was before this operation, so the stack may grow, which protects anything that might already be on the stack. This is sometimes done in main just in case the function is called with an unaligned stack, which can cause things to be really slow (16 byte is a cache line width on x86, I think, though 4 byte alignment is what is really important here). If main has a unaligned stack the rest of the program will too.
main函数调用sum函数的入栈情况:
5c4: 8b 55 ec mov -0x14(%ebp),%edx
5c7: 8b 45 f0 mov -0x10(%ebp),%eax
5ca: 83 ec 08 sub $0x8,%esp
5cd: 52 push %edx
5ce: 50 push %eax
5cf: e8 99 ff ff ff call 56d <sum>
和window下的入栈方式差不多,现实mov方式局部变量入栈,然后是push 参数入栈。
image.png我们再来分析一下sum函数,比较简单
0000056d <sum>:
56d: 55 push %ebp//保存旧栈帧
56e: 89 e5 mov %esp,%ebp//设置sum的栈帧ebp
570: 83 ec 10 sub $0x10,%esp//分配sum0x10h的堆栈大小
573: e8 86 00 00 00 call 5fe <__x86.get_pc_thunk.ax>
578: 05 88 1a 00 00 add $0x1a88,%eax
57d: 8b 55 08 mov 0x8(%ebp),%edx
580: 8b 45 0c mov 0xc(%ebp),%eax
583: 01 d0 add %edx,%eax
585: 89 45 fc mov %eax,-0x4(%ebp)
588: 8b 45 fc mov -0x4(%ebp),%eax
58b: c9 leave
58c: c3 ret
我们看看一下主要的指令
mov 0x8(%ebp),%edx
mov 0xc(%ebp),%eax
add %edx,%eax
mov %eax,-0x4(%ebp)
我们知道ebp的高八个字节是返回地址,0x8(%ebp)就代表最后入栈的参数(2),,0xc(%ebp)就是最先入栈的参数(3),就是i
然后相加后再赋值给-0x4(%ebp)
就是局部参数sum入栈的过程了。
64位elf
00000000004005d6 <_Z3sumii>:
4005d6: 55 push %rbp
4005d7: 48 89 e5 mov %rsp,%rbp
4005da: 89 7d ec mov %edi,-0x14(%rbp)
4005dd: 89 75 e8 mov %esi,-0x18(%rbp)
4005e0: 8b 55 ec mov -0x14(%rbp),%edx
4005e3: 8b 45 e8 mov -0x18(%rbp),%eax
4005e6: 01 d0 add %edx,%eax
4005e8: 89 45 fc mov %eax,-0x4(%rbp)
4005eb: 8b 45 fc mov -0x4(%rbp),%eax
4005ee: 5d pop %rbp
4005ef: c3 retq
00000000004005f0 <main>:
4005f0: 55 push %rbp
4005f1: 48 89 e5 mov %rsp,%rbp
4005f4: 48 83 ec 10 sub $0x10,%rsp
4005f8: 48 8d 55 f4 lea -0xc(%rbp),%rdx
4005fc: 48 8d 45 f8 lea -0x8(%rbp),%rax
400600: 48 89 c6 mov %rax,%rsi
400603: bf c4 06 40 00 mov $0x4006c4,%edi
400608: b8 00 00 00 00 mov $0x0,%eax
40060d: e8 ae fe ff ff callq 4004c0 <scanf@plt>
400612: 8b 55 f4 mov -0xc(%rbp),%edx
400615: 8b 45 f8 mov -0x8(%rbp),%eax
400618: 89 d6 mov %edx,%esi
40061a: 89 c7 mov %eax,%edi
40061c: e8 b5 ff ff ff callq 4005d6 <_Z3sumii>
400621: 89 45 fc mov %eax,-0x4(%rbp)
400624: 8b 45 fc mov -0x4(%rbp),%eax
400627: 89 c6 mov %eax,%esi
400629: bf c9 06 40 00 mov $0x4006c9,%edi
40062e: b8 00 00 00 00 mov $0x0,%eax
400633: e8 78 fe ff ff callq 4004b0 <printf@plt>
400638: b8 00 00 00 00 mov $0x0,%eax
40063d: c9 leaveq
40063e: c3 retq
40063f: 90 nop
64位和32位很直观的不同就是sum的函数名改变了,另外,main函数更短了,寄存器名都变成rbp,rsp了
我们简单的看一下main函数
4005f0: 55 push %rbp
4005f1: 48 89 e5 mov %rsp,%rbp
4005f4: 48 83 ec 10 sub $0x10,%rsp
4005f8: 48 8d 55 f4 lea -0xc(%rbp),%rdx
4005fc: 48 8d 45 f8 lea -0x8(%rbp),%rax
400600: 48 89 c6 mov %rax,%rsi
400603: bf c4 06 40 00 mov $0x4006c4,%edi
64位在main函数这里并没有像32位一样有栈位数检测的指令,直接就开始栈帧调整
就下来两个lea指令有点奇怪,而且还用到了两个很少见的rsi和edi,接下来我们会介绍一下这两个寄存器有什么作用
我们继续来看sum函数的调用指令
400612: 8b 55 f4 mov -0xc(%rbp),%edx //取出参数2
400615: 8b 45 f8 mov -0x8(%rbp),%eax//取出参数1
400618: 89 d6 mov %edx,%esi//右边第一个参数入栈
40061a: 89 c7 mov %eax,%edi//右边第二个参数入栈
40061c: e8 b5 ff ff ff callq 4005d6 <_Z3sumii>//调用sum函数
这里发现sum的两个参数i,j入栈的方式是采用edi,esi来入栈的,而edi,esi通过lea传地址指令大概是在栈顶的位置。
image.png我们来看一下sum函数的栈帧。
image.pnglinux 64位和window以及linux 32位的有很大差别,在调试的时候要特别注意区别。
64位中调用的sum函数并不会抬高栈顶,只在main函数中有抬高栈顶的操作,可能是为了节约堆栈空间?
64位的堆栈参考: https://zhuanlan.zhihu.com/p/27339191
网友评论