实验准备
- Unbuntu 16.04 32位
实验一
“example.c”代码:
将“example.c”文件复制至“Home”目录下。
“Ctrl”+“Alt”+“T”调出命令行。
在命令行中输入代码:
gcc -o example example.c
将“example.c”编译为可执行文件“example”:
运行可执行文件“example”:
./example
通过对比“buf”输出可得知,此时“buf02”发生了缓冲区溢出。
实验二
“mem_distribute.c”代码:
将“mem_distribute.c”文件复制至“Home”目录下。
“Ctrl”+“Alt”+“T”调出命令行。
在命令行中输入代码:
gcc -o men mem_distribute.c
将“mem_distribute.c”编译为可执行文件“mem”:
运行可执行文件“mem”:
./mem
由此可见:
-
可执行代码fun1, fun2, main存放在内存的低地址,且按照源代码中的顺序从低地址到高地址排列(先定义的函数的代码存放在内存的低地址)。
-
全局变量(x, y, z)也存放内存的低地址,位于可执行代码之上(起始地址高于可执行代码的地址)。
-
初始化的全局变量存放在较低的地址,而未初始化的全局变量位于较高的地址。
-
局部变量位于内存高地址区(0xbfff efxx),字符串变量放在高地址,其它变量从低地址到高地址依次(先定义的放在低地址)存放。
-
函数的入口参数的地址(> 0xbfff efxx)更高,位于函数的局部变量更高的地址之上。
-
main函数从环境中获得参数,因此,环境变量位于最高的地址。
-
可以推断出,栈底(最高地址)位于0xc000 0000,环境变量和局部变量位于进程的栈区。
-
函数的返回地址也位于进程的栈区。
实验三:IA32构架缓冲区溢出
“buffer_overflow.c”代码:
将“buffer_overflow.c”文件复制至“Home”目录下。
“Ctrl”+“Alt”+“T”调出命令行。
在命令行中输入代码:
gcc -fno-stack-protector -o buf buffer_overflow.c
将“buffer_overflow.c”编译为可执行文件“buf”:
运行可执行文件“buf”:
./buf
发生段错误。
使用“gdb”命令调试文件:
gdb buf
r
发生段错误。
使用“gdb”命令调试文件,找出错误原因
disas main
disas foo
在函数foo的入口、对strcpy的调用、出口及其他需要重点分析的位置设置断点
b*(foo+0)
b*(foo+18)
b*(foo+28)
display/i $eip
运行程序并在断点处观察寄存器的值
r
x/x $esp
函数入口处的堆栈指针esp指向的栈(地址为0xbffff0ac)保存了函数foo()返回到调用函数(main)的地址(0x080484ee),即“函数的返回地址”.
查看main的汇编代码核实结论
在地址为0x080484ee指令的前一条指令为call 0x804840b ,而地址0x804840b为函数foo()的第一条指令的地址,因此,函数入口处的堆栈保存的是被调用函数的返回地址。也可以用下面的gdb命令证实这一点:
x/2i 0x080484ee-5
记录堆栈指针esp的值,在此以A标记:A=$esp
c
查看执行汇编代码strcpy@plt之前堆栈的内容
由于C语言默认将参数逆序推入堆栈,因此,C函数strcpy(des, src)的src(全局变量Lbuffer的地址)先进栈(高地址),des(foo()中buff的首地址)后进栈(低地址)。
C函数strcpy(des, src) 的参数
x/x $esp
x/x 0x0804a040
Lbuffer 的地址0x804a040 保存在地址为0xbffff0ac的栈中,buff的首地址0xbfffef90保存在地址为0xbfffef80的栈中。
令B = buff的首地址,则buff的首地址与返回地址所在栈的离=A-B=0xbffff0ac - 0xbfffef90=0x1c=28。
因此,如果Lbuffer的内容超过28字节,则将发生缓冲区溢出并返回地址被改写。
Lbuffer的长度为32字节,其中最后的4个字节为“ABCD”,因此,执行strcpy之后,返回地址由原来的0x0804840b变为”ABCD”(0x44434241),即返回地址被改写。
继续执行到下一个断点:
c
即将执行的指令为ret。
执行ret时把堆栈的内容(4个字节)弹出到指令寄存器eip,esp的值增加4,然后跳转到eip所保存的地址去继续执行:ret指令让eip等于esp指向的内容,并且 esp等于esp+4.
x/x $esp
x/s $esp
执行ret之前的堆栈的内容为”ABCD”,即0x44434241。可以推断执行ret后将跳到地址0x44434241去执行。
si
x/s $esp
x/s $eip
程序指针eip的值为0x44434241,是不可访问的地址,因此发生段错误。
eip=0x44434241,正好是”ABCD”倒过来,这是由于IA32默认字节序为little_endian(低字节存放在低地址)。
通过修改Lbuffer的内容(将ABCD改成期望的地址),就可以设置需要的返回地址,从而可以将eip变为可以控制的地址,也就是说可以控制程序的执行流程。
调试重点
在函数的3个关键之处设置断点:
- 第一条汇编语句:在此记下函数的返回地址(A=$esp本身的值) (会动态变化)
- 调用strcpy对应的汇编语句:记下smallbuf的起始地址=$esp指向的内存的值=B (会动态变化),与A相减可以得到产生缓冲区溢出所需的字节数(偏移offset)= A-B
- ret语句:查看esp指向的内容,确定被修改后的返回地址。
实验四
homework
./homework
发生段错误。
反汇编main函数:
disas main
设置断点“1”、“1 2”:
set args 1
r
set args 1 2
r
得出“foo”、“foo01”产生溢出,“foo02”无溢出。
foo
反汇编“foo”:
disas foo
在函数foo的入口、对strcpy的调用、出口及其他需要重点分析的位置设置断点:
b*(foo+0)
b*(foo+24)
b*(foo+34)
display/i $eip
运行程序并在断点处观察寄存器的值:
r
x/x $esp
函数入口处的堆栈指针esp指向的栈(地址为0xbffff08c)保存了函数foo()返回到调用函数(main)的地址(0x0804859a),即“函数的返回地址”。
c
此时进入第二个断点<foo+24>:
x/x $esp
查看缓存器内的值:
x/x 0x0804a040
p(0xbffff08c-0xbfffefa2)
x/x 0x0804a040+234
Lbuffer 的地址0x804a040 保存在地址为0xbfffef94的栈中,
buff的首地址0xbfffefa2保存在地址为0xbfffef90的栈中。
Buff的首地址与返回地址所在栈的距离为234。
继续执行到下一个断点:
c
即将执行的指令为ret。
执行ret时把堆栈的内容(4个字节)弹出到指令寄存器eip,esp的值增加4,然后跳转到eip所保存的地址去继续执行:ret指令让eip等于esp指向的内容,并且 esp等于esp+4。
x/x $esp
x/s $esp
执行ret之前的堆栈的内容为”ABCD……”,即0x44434241。可以推断执行ret后将跳到地址0x44434241去执行。
si
x/s $esp
x/s $eip
程序指针eip的值为0x44434241,是不可访问的地址,因此发生段错误。
eip=0x44434241,正好是”ABCD……”倒过来,这是由于IA32默认字节序为little_endian(低字节存放在低地址)。
通过修改Lbuffer的内容(将ABCD改成期望的地址),就可以设置需要的返回地址,从而可以将eip变为可以控制的地址,也就是说可以控制程序的执行流程。
foo01
设置断点“1”:
set args 1
r
反汇编“foo01”:
disas foo01
在函数foo01的入口、对strcpy的调用、出口及其他需要重点分析的位置设置断点:
b*(foo01+0)
b*(foo01+77)
b*(foo01+87)
display/i $eip
运行程序并在断点处观察寄存器的值:
r
x/x $esp
函数入口处的堆栈指针esp指向的栈(地址为0xbffff08c)保存了函数foo01()返回到调用函数(main)的地址(0x080485a1),即“函数的返回地址”。
c
此时进入第二个断点<foo01+24>:
x/x $esp
查看缓存器内的值:
x/x 0xbffff04f
p(0xbffff08c-0xbffff070)
x/x 0xbffff04f+28
Lbuffer 的地址0xbffff04f保存在地址为0xbffff034的栈中,
buff的首地址0xbffff070保存在地址为0xbffff030的栈中。
Buff的首地址与返回地址所在栈的距离为28。
继续执行到下一个断点:
c
即将执行的指令为ret。
执行ret时把堆栈的内容(4个字节)弹出到指令寄存器eip,esp的值增加4,然后跳转到eip所保存的地址去继续执行:ret指令让eip等于esp指向的内容,并且 esp等于esp+4。
x/x $esp
x/s $esp
执行ret之前的堆栈的内容为”ABCD”,即0x44434241。可以推断执行ret后将跳到地址0x44434241去执行。
si
x/s $esp
x/s $eip
程序指针eip的值为0x44434241,是不可访问的地址,因此发生段错误。
eip=0x44434241,正好是”ABCD……”倒过来,这是由于IA32默认字节序为little_endian(低字节存放在低地址)。
通过修改Lbuffer的内容(将ABCD……改成期望的地址),就可以设置需要的返回地址,从而可以将eip变为可以控制的地址,也就是说可以控制程序的执行流程。
foo02
设置断点“1 2”:
set args 1 2
r
run时显示正常退出,foo02无溢出。
反汇编“foo02”:
disas foo02
在函数foo02的入口、对strcpy的调用、出口及其他需要重点分析的位置设置断点:
b*(foo02+0)
b*(foo02+77)
b*(foo02+87)
display/i $eip
运行程序并在断点处观察寄存器的值:
r
x/x $esp
函数入口处的堆栈指针esp指向的栈(地址为0xbffff08c)保存了函数foo()返回到调用函数(main)的地址(0x0804859a),即“函数的返回地址”。
c
此时进入第二个断点<foo02+24>:
x/x $esp
查看缓存器内的值:
x/x 0xbffff05f
p(0xbffff08c-0xbffff04f)
x/x 0xbffff05f+61
Lbuffer 的地址0xbffff05f保存在地址为0xbffff034的栈中,
buff的首地址0xbffff04f保存在地址为0xbffff030的栈中。
Buff的首地址与返回地址所在栈的距离为61。
继续执行到下一个断点:
c
即将执行的指令为ret。
执行ret时把堆栈的内容(4个字节)弹出到指令寄存器eip,esp的值增加4,然后跳转到eip所保存的地址去继续执行:ret指令让eip等于esp指向的内容,并且 esp等于esp+4。
x/x $esp
x/s $esp
执行ret之前的堆栈的内容为”\250\205\004\b\003”,即0x040485a8。可以推断执行ret后将跳到地址0x040485a8去执行。
si
x/s $esp
x/s $eip
程序指针eip的值为0x040485a8,是可访问的地址,正常执行,未发生段错误。
eip=0x040485a8,正好是”\250\205\004\b\003”倒过来,这是由于IA32默认字节序为little_endian(低字节存放在低地址)。