Linux平台的x86栈学习
时间:2011-02-25 来源:老钟古
Linux平台的x86栈学习
一、栈的基本概念和操作
本文主要讲的是在系统底层中的栈的概念。要想透彻的理解什么是栈,理解栈的两个操作是很重要的,PUSH和POP。
指令“pushl %eax”在C语言中就像下面的表示:
esp = esp – (sizeof(int)); memory[esp] = eax;
在汇编语言中就像:
subl $4, %esp movl %eax, (%esp)
而指令“popl %eax”在C语言中就像下面的表示:
eax = memory[esp]; esp = esp + (sizeof(int))
在汇编语言中就像:
movl (%esp), %eax addl $4, %esp
二、例子演示
栈是一种数据结构,栈是以后进先出(LIFO)的原则进行工作的。LIFO意味着最后一个放进栈的元素是第一个可以从栈中取走的元素(在这里可以想象一下取走叠放好的盘子)。尽管你不能够移除在栈中除了最后放进去的那个元素之外(即栈顶元素),但你仍有权限去访问在栈中的任何元素。访问元素并不是将元素从栈中删除,所以应该要明白这一点。在计算机系统中,栈的作用是非常大的。系统中的栈是从内存的高地址开始并且向下增长(这里表示栈的大小)。因此,假如栈顶地址从0xbfffffff开始,我们需要增加四个字节的内容到栈的话,就应该进行0xbfffffff - 4的运算,即得到0xbffffffb。在下面进行的十六进制计算我们都使用gdb来操作(前提是你要知道C语言中的printf和格式化字符串输出)。
studyrush@studyrush-desktop:~$ gdb
GNU gdb 6.8-debian
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-linux-gnu".
(gdb) printf "0x%x\n", 0xbfffffff - 4
0xbffffffb
(gdb) quit
上面的计算可以用十六进制的计算器很快可以算出,但我们仍是在gdb中来进行运算,下面我们用一个具体的汇编语言编写的程序来演示栈的操作。
studyrush@studyrush-desktop:~/Exploit$ vi stack.s
.section .text
.globl _start
_start:
nop
pushl $100
pushl $200
pushl $300
popl %eax
popl %eax
popl %eax
movl $1, %eax
movl $0, %ebx
int $0x80
studyrush@studyrush-desktop:~/Exploit$ as -g stack.s -o stack.o
studyrush@studyrush-desktop:~/Exploit$ ld stack.o -o stack
在上面的汇编语言程序中,我们将三个整型数据(每个数据4字节)放进栈中,然后又将数据从栈中弹出放进寄存器eax中,当然我们也可以将弹出的数据放进其它的通用寄存器中。接下来是exit系统调用(将1放进寄存器eax中,1是exit的系统调用号,将0放进寄存器ebx中,这是exit的返回值)。
从上面的程序中我们期望看见什么呢?假设栈是从某些地址开始,这里用X代替。第一次将数据压入栈中,栈的地址将会X—4(在这里应该记住栈是向着低内存增长的),再次压入数据栈的地址为X—8,第三次压入数据栈的地址为X-12。弹出数据操作则会使得栈的地址增加,三次弹出之后则栈的地址为原来的X。
studyrush@studyrush-desktop:~/Exploit$ gdb stack
GNU gdb 6.8-debian
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-linux-gnu"...
为了更好的演示栈的表示,我们在标号_start+1的地址处设置断点,来观察程序的运行细节。
(gdb) break *_start+1
Breakpoint 1 at 0x8048055: file stack.s, line 5.
列出我们的汇编程序的源代码
(gdb) list _start
1 .section .text
2 .globl _start
3 _start:
4 nop
5 pushl $100
6 pushl $200
7 pushl $300
8 popl %eax
9 popl %eax
10 popl %eax
(gdb)
11 movl $1, %eax
12 movl $0, %ebx
13 int $0x80
然后我们对程序进行运行。
(gdb) run
Starting program: /home/studyrush/Exploit/stack
Breakpoint 1, _start () at stack.s:5
5 pushl $100
Current language: auto; currently asm
运行程序之后,程序会在指令pushl $100中停下来,这也就是我们设置好的断点地方,现在让我们来看看esp当前的值是多少,esp的值指示出当前我们在栈中的什么地方,esp也可以叫做扩展栈顶指针(extended stack pointer)。
(gdb) x $esp
0xbfd0eab0: 0x00000001
从上面的结果我们可以看到esp指向的地址为0xbfd0eab0和存储的值为0x00000001,这个值与现在进行的操作并没有太大的关系。所有的elf二进制文件栈的地址都是从0xbfffffff开始的,这是个虚拟地址。这里根Windows下的exe是从0x00410000处开始执行有点相像。
(gdb) printf "0x%x\n", 0xbfd0eab0 - 4
0xbfd0eaac
我们将数据压入栈中之后,栈顶指针esp将会指向地址0xbfd0eaac,执行指令“pushl $100 ”。
单步运行程序
(gdb) step
_start () at stack.s:6
6 pushl $200
让我们来检查一下esp当前所指向的内容
(gdb) x $esp
0xbfd0eaac: 0x00000064
从上面的结果我们可以看出esp的确是指向地址0xbfd0eaac,但0x00000064又是什么呢?让我们把它打印成十进制的结果来看看。
(gdb) printf "%d\n", 0x00000064
100
100就是我们压入栈中的数据,在系统中是用16进制数来表示而不是十进制数。(6*16 + 4 = 100)
当我们进行再次压入数据到栈中时,esp将会指向哪里呢? esp当前的地址是0xbfd0eaac再次减去4。
(gdb) printf "0x%x\n", 0xbfd0eaac - 4
0xbfd0eaa8
再次单步运行程序
(gdb) step
_start () at stack.s:7
7 pushl $300
检查esp指向的地址
(gdb) x $esp
0xbfd0eaa8: 0x000000c8
这里跟上面的一样,0x000000c8是十六进制,在十进制数表示为200。
(gdb) printf "%d\n", 0x000000c8
200
我们最后压入数据到栈中将会在地址0xbfd0eaa8 – 4处,让我们来看看是否确实在那个地址。
(gdb) printf "0x%x\n", 0xbfd0eaa8 - 4
0xbfd0eaa4
下一个地址将会是0xbfd0eaa4
(gdb) step
_start () at stack.s:8
8 popl %eax
(gdb) x $esp
0xbfd0eaa4: 0x0000012c
上面的内容我们已经能够明白其中的含义了。
(gdb) printf "%d\n", 0x0000012c
300
向栈中压入数据的操作我们已经完成了(即pushl操作),现在继续来进行popl操作。popl操作将数据从栈中弹出并放入到一个寄存器中。
(gdb) x $esp
0xbfd0eaa4: 0x0000012c
当前栈顶指针esp指向的地址为0xbfd0eaa4,当第一次popl操作将会使得esp指向0xbfd0eaa4 + 4的地址处,因为此时我们是把数据从栈中弹出来。
(gdb) printf "0x%x\n", 0xbfd0eaa4 + 4
0xbfd0eaa8
第一次popl将会使得esp指向地址0xbfd0eaa8,让我们来看看。
(gdb) step
_start () at stack.s:9
9 popl %eax
(gdb) x $esp
0xbfd0eaa8: 0x000000c8
从上面的结果来看,我们计算出来的地址是正确的,popl操作已经将地址0xbfd0eaa8的内容放入到寄存器eax中,让我们来检查一下寄存器eax的内容
(gdb) print $eax
$1 = 300
整数300正是我们最后压入栈中的数据,下一次popl又会使得栈的地址增加4。
(gdb) printf "0x%x\n", 0xbfae91c8 + 4
0xbfd0eaac
(gdb) step
_start () at stack.s:10
10 popl %eax
(gdb) x $esp
0xbfd0eaac: 0x00000064
再来看看现在寄存器eax的内容。
(gdb) print $eax
$2 = 200
地址再增加4。
(gdb) printf "0x%x\n", 0xbfd0eaac + 4
0xbfd0eab0
(gdb) step
_start () at stack.s:11
11 movl $1, %eax
(gdb) x $esp
0xbfd0eab0: 0x00000001
所有的数据都从栈中弹出来了,我们此时也注意到esp指向地址0xbfd0eab0,这个地址就是我们栈地址开始。
(gdb) print $eax
$1 = 100
(gdb) step
_start () at stack.s:12
12 movl $0, %ebx
(gdb) step
_start () at stack.s:13
13 int $0x80
(gdb) step
Program exited normally.
(gdb) quit
在程序开始运行时,来看看栈顶地址。如下图所示:
地址值(Address Value)
--------------
0xbfd0eab0: | 0x00000001 | <--- esp points here
--------------
0xbfd0eaac: | 0x00000000 |
--------------
0xbfd0eaa8: | 0x00000000 |
--------------
0xbfd0eaa4: | 0x00000000 |
--------------
执行指令push l $100后, esp地址将会减去4,此时esp将会指向地址减4的地方。
地址值(Address Value)
--------------
0xbfd0eab0: | 0x00000001 |
--------------
0xbfd0eaac: | 0x00000064 | <--- esp points here now
--------------
0xbfd0eaa8: | 0x00000000 |
--------------
0xbfd0eaa4: | 0x00000000 |
--------------
执行指令push l $200后, esp地址将会减去4,此时esp将会指向地址减4的地方。
地址值(Address Value)
--------------
0xbfd0eab0: | 0x00000001 |
--------------
0xbfd0eaac: | 0x00000064 |
--------------
0xbfd0eaa8: | 0x000000c8 | <--- esp points here now
--------------
0xbfd0eaa4: | 0x00000000 |
--------------
执行指令push l $300后, esp地址将会减去4,此时esp将会指向地址减4的地方。
地址值(Address Value)
--------------
0xbfd0eab0: | 0x00000001 |
--------------
0xbfd0eaac: | 0x00000064 |
--------------
0xbfd0eaa8: | 0x000000c8 |
--------------
0xbfd0eaa4: | 0x0000012c | <--- esp points here now
--------------
push操作已经完成,接下来进行pop操作
指令popl %eax,把数据从栈中弹出来放进寄存器eax中,这样也使得esp的地址增加4。
地址值(Address Value)
--------------
0xbfd0eab0: | 0x00000001 |
--------------
0xbfae91cc: | 0x00000064 |
--------------
0xbfae91c8: | 0x000000c8 | <--- esp points here now
-------------- eax is 0x0000012c (300)
0xbfae91c4: | 0x0000012c |
--------------
和上面的操作后的结果相同。
地址值(Address Value)
--------------
0xbfd0eab0: | 0x00000001 |
--------------
0xbfae91cc: | 0x00000064 | <--- esp points here now
-------------- eax is 0x000000c8 (200)
0xbfae91c8: | 0x000000c8 |
--------------
0xbfae91c4: | 0x0000012c |
--------------
这里也是
地址值(Address Value)
--------------
0xbfd0eab0: | 0x00000001 | <--- esp points here now
-------------- eax is 0x00000064 (100)
0xbfae91cc: | 0x00000064 |
--------------
0xbfae91c8: | 0x000000c8 |
--------------
0xbfae91c4: | 0x0000012c |
--------------
所有pop操作完成之后就会进行exit系统调用。跟着这篇教程进行操作你就会明白栈操作的原理。
三、学习总结
通过学习这篇文章,自己对栈有了更深刻的理解,也对GDB的应用有了进一步的,对自己以后学习更深的知识打好基础。其实英语好点的去看参考资料中的更好,作者已经把文章写得非常地详细,不怕看不懂。
四、参考资料
http://www.exploit-db.com/papers/13223/