基础的缓冲区溢出实践通常需要确定运行状态下程序中的某些局部变量的地址,如需要确定输入缓冲区的起始地址从而获得注入缓冲区中的机器指令的起始地址等。在 Linux 环境下,可通过 gdb 对程序进行动态调试,从而获得程序运行状态下的信息( 关闭 ALSR 机制 ),基础的 gdb 操作可参见笔者的文章Linux下编辑、编译、调试命令总结——gcc和gdb描述。使用 gdb 可以方便的获取程序动态运行状态下的信息,但通过 gdb 动态调试获取的诸如缓冲区的起始地址等信息可能与程序实际运行时的信息并不相同,从而影响缓冲区溢出实践的效果。
问题描述
通过 gdb 运行的程序的内存布局较之程序单独运行时的内存布局可能略有不同.通过以下实例程序进行简单的验证。
#include<stdio.h>
int main(int argc , char *argv[] , char *envp[])
{
char buff[8];
printf("address of buff : %p
", buff );
return 0;
}
关闭 ALSR 机制后,通过 gcc -m32 -o hello -g -fno-stack-protector hello.c 编译生成可执行文件 hello。直接运行可执行程序,获得缓冲区的起始地址为 0xffffcfa8.
使用 gdb 对 hello 文件动态调试,获得缓冲区的起始地址为 0xffffcf88.可以看到两者地址之间存在32个字节的差距。
正常程序运行时,会将环境变量字符串数组和命令行参数字符串数组存放在栈顶,而程序使用的局部变量等数据则位于这些字符串数组之后。环境变量字符串数组记录了诸如当前用户名、终端类型、搜索路径等环境信息。程序直接运行时,程序进程继承的是运行其的 shell 的环境变量,而程序通过 gdb 运行时,程序进程继承的是 gdb 的环境变量,这两者存在不同,从而会造成位于栈上的局部变量的地址发生改变。用户可在 gdb 中运行 show environment 命令获得环境变量参数。
show environment //在 gdb 中获得当前程序运行的环境变量
较之程序直接运行,位于栈顶的环境变量主要有以下变化:
(1) _ 环境变量的内容发生改变,在程序直接运行时,_ 变量存放的是程序的执行路径,而通过 gdb 运行程序时,_ 变量存放的是 gdb 的执行路径。
使用 gdb 运行程序时,_ 环境变量的值为 /usr/bin/gdb.而在程序直接运行时,_ 环境变量的值应为程序的执行路径,如./hello.
(2) 通过 gdb 运行的调试程序继承了 gdb 的环境变量,其中包含新加入的环境变量 LINES 和 COLUMNS。
(3) 位于栈上的参数列表也可能不同,当用户通过 ./hello 直接在shell 中运行程序时,位于参数数组的第一项 argv[0] 内容为"./hello" ,而用户通过 gdb 运行 hello 程序时,程序的参数列表的第一项 argv[0] 的值为该程序的绝对路径"/home/yh/hello",这也会造成程序运行时局部变量地址的差异。建议终端环境下使用绝对路径运行程序,避免该差异。
功能支持
为了解决上述由于环境变量不同所产生的程序局部变量地址的差异,需要在使用 gdb 运行程序之前进行一定的操作,gdb 为用户调试提供了以下功能支持。
(1) gdb 提供命令对传递给调试进程的环境变量进行操作和修改。
show paths //显示可执行文件的搜索路径(即为PATH变量的内容)
show environment [ varname ] //显示启动调试程序时 gdb 传递给它的环境变量,不指定具体的变量名时,则会显示所有的环境变量,可使用 env 作为 environment 的简写
set environment varname [= value] //设置传递给调试程序的环境变量varname的值为value,不指定value时,值默认为NULL
unset environment varname //设置不会被传递给调试程序的环境变量
用户可以通过 unset env LINES 和 unset env COLUMNS 命令使得调试程序的栈上不会新增额外的环境变量 LINES 和 COLUMNS。
(2) Linux 平台提供了标准 wrapper 程序 env 对最终传递给程序的环境变量进行设置。env 的详细用法可通过 man env 进行查看,其简单的参数如下。
env [options] [-] [ name=value ] [command [args] ...]
//通过 command 指定由 env 启动的程序,不指定 command 时,会将当前 env 指定的环境输出,如直接运行 env ,输出的为当前的所有环境变量
options:
-i //忽略环境变量,即不传递环境变量给程序
-u varname //忽略 varname 指定的环境变量,即不将其传递给程序
- //作用与 -i 相同
(3) gdb 可通过 wrapper 函数运行调试程序。当设置好 wrapper 程序后,gdb 会以 exec wrapper hello 的 shell 命令的形式启动调试程序 Hello,wrapper程序首先运行并最终启动调试进程,之后由 gdb 对调试进程进行控制。通过 wrapper 程序,即可控制传递给调试进程的环境变量。
set exec-wrapper wrapper //设置 wrapper 程序为 wrapper
show exec-wrapper //显示当前的 wrapper 程序
unset exec-wrapper //取消对 wrapper 程序的设置
可以使用标准的 wrapper 程序 env 对传递给调试进程的环境变量进行控制。
解决方案
借助上述功能,可以通过以下方案解决 gdb 运行调试程序时局部变量地址变化的问题。
A.不传递环境变量数组给调试程序。
(1)通过 env 运行 gdb ,并将传递给 gdb 的环境变量设置为空。此时 gdb 环境下所包含的环境变量仅为其新增加的 LINES 和 COLUMNS。
env - gdb /home/yh/hello //设置传递给 gdb 的环境变量为空,注意使用待调试程序的完整路径名
此时,通过 show env 显示的 gdb 中环境变量的列表如下
(2)通过 gdb 对环境变量的设置命令 unset env ,设置不将上述两个环境变量传递给调试程序,使得最终运行调试程序时,不会有环境变量被调试程序继承。
unset env LINES unset env COLUMNS
经过以上两步,通过 gdb 运行的调试程序 hello 的局部变量的地址与通过 env - /home/yh/hello 运行的 hello 程序的地址是一致的(两者的进程栈上均不包含环境变量数组,且 argv[0] 均为绝对路径地址)。
当待调试的程序需要使用某些环境变量的值时,只需通过 env 程序指定所需的环境变量即可,而不引入不相同的环境变量如 _ ,或者直接通过 env 的 -u 选项去除引入差异的环境变量,而保留其它的环境变量。
B.基于同样的原理,可以正常启动 gdb ,但对 wrapper 程序进行设置。
gdb hello //正常启动 gdb set exec-wrapper env -u COLUMNS -u LINES -u _ //在 gdb 中设置 wrapper 程序 r //开始调试,借助设置的 wrapper 程序的功能,hello程序不会继承 gdb 中特定的引入差异的环境变量 env -u _ /home/yh/sc/hello //通过该命令直接运行程序,注意使用绝对路径
此时通过 gdb 运行程序时调试程序不会继承 gdb 引入的不同的环境变量,这样获得的程序局部变量的地址与通过 env -u _ /home/yh/hello 的方式启动的 hello 程序的局部变量地址相同。
参考资料
1.Debugging with GDB:Environment
2.Debugging with GDB: Starting
3.Buffer overflow works in gdb but not without it - Stack Overflow
4.How to predict address space layout differences between real and gdb-controlled executions? - StackExchange