程序的机器级表示--指针
理解指针
指针的关键原则:
每个指针都对应一个类型。void*表示通用指针,可以通过显式转换或者赋值操作转换成有类型指针。指针不是机器代码的一部分,只是拿来帮助程序避免寻址错误的。
每个指针都有一个值。NULL(0)代表该指针没有指向任何地方。
指针用&创建。
*用来间接引用指针,其结果是一个值,类型与指针类型一致。
数组与指针紧密联系。数组名字可以同指针一样引用。数组引用与指针操作都需要用对象大小对偏移量进行伸缩。
将指针从一种类型强制转成另一个种类型,只改变他的类型而不改变它的值。强制类型转换一个效果是改变指针运算的伸缩。强制转换的优先级高于加法。
-
指针也可以指向函数。例如:
// 函数原型 int func(int x, int *p); // 申明函数指针fp int (*fp)(int, int *); fp = func; // 用指针调用函数 int u = 1; int res = fp(u, &u); // 指针的值是该函数汇编代码的第一条指令的值 // *fp的括号是必须的,否则 int *fp(int, int *)会被解读为(int *) fp(int, int *)
利用GDB调试指针
命令 | 效果 |
---|---|
quit | 退出 |
run | 运行程序 |
kill | 停止程序 |
break func 或者b func | 在func函数处设置断点 |
break * 0x4000001 | 在0x4000001处设置断点 |
delete 1 | 删除断点编号为1的断点 |
delete | 删除所有断点 |
stepi 或者 s | 单步调试 |
stepi 4 | 执行4条指令 |
nexti 或者 n | 类型stepi,但是以函数为单位 |
continue 或者 c | 继续执行 |
finish 或者 f | 运行到当前函数返回(切换栈帧) |
disas | 反汇编 |
disas func | 反汇编函数func |
disas 0x400001 | 反汇编位于地址400001附近的函数 |
disas 0x40001,0x400005 | 反汇编指定地址范围的代码 |
print /x $rip | 以十六进制输出程序计算器rip的内容 |
print $rax | 输出寄存器rax的内容 |
print /t $rax | 以二进制输出寄存器rax的内容 |
print 0x100 | 输出0x100的十进制表示 |
print /x 555 | 输出5000的十六进制 |
print /x ($rsp + 8) | 以十六进制输出%rsp的内容加上8 |
print *(long *) 0x400001 | 输出位于地址400001的长整数 |
x/2g 0x400001 | 检查从地址400001开始的双字 |
x/20b func | 检查函数func的前20个字节 |
info frame | 有关当前栈帧的信息 |
info registers | 所有寄存器的值 |
help | 获取有关GDB的信息 |
内存越界与缓冲区溢出
局部变量与状态信息都放在栈中,如果对数组引用不做边界检查就有可能导致越界的写操作破坏了栈中的状态信息。在状态信息被破坏后,加载ret指令,就会出现严重的错误。一种常见的状态破坏称为缓冲区溢出。
缓冲区溢出即,在栈中分配字符数组来保存一个字符串,但是字符串的长度超出数组空间。
---------------------------------
调用者的栈帧
---------------------------------
返回地址 <- %rsp + 24
---------------------------------
---------------------------------
| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | <- buf=%rsp
---------------------------------
//对buf的越界操作就会导致rsp被破坏
如果ret里面的内容被破坏,程序就会跳到一个意想不到的位置。get,strcpy,sprintf等都有一个属性---不需要告诉缓冲区的大小,就会导致缓冲区溢出的漏洞。
缓冲区溢出的漏洞一个更为致命的问题就是让程序执行本来它不愿意执行的代码。给程序输入一个字符串,里面包含一些课执行代码的字节编码,称为攻击代码,还有一个指向攻击代码的指针覆盖返回地址,ret就会跳到攻击代码。
一种形式的攻击代码会使用系统调用启动一个shell程序,给攻击者提供一组操作系统函数。
一种形式的攻击代码会执行未授权任务,修复对栈的破坏,然后二次执行ret命令,(表面上)正常的返回到调用者。
对抗缓冲区的溢出攻击
预防--栈随机化
栈随机化的思想:
栈的位置在程序的每次运行时都有变化。
实现方式:
程序开始时,在栈上分配一段0~n字节之间随机大小的空间。程序不使用这些空间,他会导致每次程序运行时,栈的位置都不同。分配的n要足够大,才能获取足够多的变化,也有足够小,不至于浪费太多空间。Linux系统栈随机化又被称为地址空间布局随机化(Address-Space Layout Randomization)ASLP。每次运行程序的不同部分,包括程序代码、库代码、栈、全局变量和对数据,都会被加载到不同位置。
栈随机化不能提供完全安全的保障,总有几个傻逼会用蛮力法去破解随机化。
检测--栈破坏检测
栈检测的思想:
在栈帧中任何局部缓冲区与栈状态之间存储就一个特殊的金丝雀值(或称哨兵值),在恢复寄存器状态和函数返回之前,检查金丝雀值是否发生变化,如果是的,栈被破坏,程序终止。
最近版本的gcc会自动开启金丝雀值,通过`-fno-stack-protector`来关闭。
金丝雀值是通过段寻址的,将金丝雀值放在一段只读的段中,这样攻击者就无法修改这个值。
GCC一般只在有局部char类型缓冲区的时候才插入金丝雀值。
阻止--限制可执行代码区域
思想:
只有保存编译器产生的代码的那部分内存才需要是可执行的,其他部分可以被限制为读和写。x86曾将读和执行合并,导致栈必须即可读也可执行,最近加入一些机制可以限制页可读不可执行,但是这些机制会带来性能损耗。AMD引入NX(no-execute,不执行)位,能将栈设置位可读且不可执行,将页是否可执行交给硬件检测,避免性能损耗。