3.10 在机器级程序中将控制与数据结合起来
3.10.1 理解指针
指针以一种统一方式,对不同数据结构中的元素产生引用。
1) 每个指针都对应一个类型。(指针类型不是机器代码中的一部分;只是C语言提供的一种抽象,帮助程序员避免寻址错误。)
2) 每个指针都有一个值。这个值是某个指定类型的对象的地址。特殊的 NULL(0)
值代表该指针没有指向任何地方。
3) 指针用‘&’运算符创建。
4) 操作符是用来间接引用指针。
5) 数组与指针紧密联系。一个数组的名字可以像指针变量一样引用(但是不能修改)。如,a[3]
等价于*(a+3)
。
数组引用和指针运算都需要用对象大小对偏移量进行伸缩。
6) 将指针从一种类型强制转换成另一种类型,只改变它的类型,而不改变它的值。强制类型转换的一个效果是改变指针运算的伸缩。
例如:
char *p = 'c' ;
(int *)p + 7 的结果为 p + 28;
(int *)(p + 7) 的结果为 p + 7。
(强制类型转换的优先级高于加法)
7) 指针也可以指向函数。这提供了一个很强大的存储和像代码传递引用的功能,这些引用可以被程序的某个其他部分调用。
例如:
int fun(int x, int *p);
/** 声明指针 fp ,将它赋值为这个函数 */
int (*fp)(int, int *);
fp = fun;
/** 函数调用 */
int y = 1;
int result = fp(3, &y);
函数指针的值是该函数机器代码表示中第一条指令的地址。
3.10.2 应用:使用 GDB 调试器
先运行OBJDUMP 来获得程序的反汇编版本。如下命令行来启动 GDB:
linux> gdb prog
通常的方法是在程序感兴趣的地方附近设置断点。
命令 | 效果 |
---|---|
quit |
退出GDB |
run |
运行程序(在此给出命令行参数) |
kill |
停止程序 |
break sth |
在函数sth 入口处设置断点 |
break *0x400540 |
在地址0x400540 处设置断点 |
delete 1 |
删除断点1 |
delete |
删除所有断点 |
stepi |
执行1 条指令 |
stepi 4 |
执行4 条指令 |
nexti |
类似stepi ,但以函数调用为单位 |
continue |
继续执行 |
finish |
运行到当前函数返回 |
disas sth |
反汇编函数sth |
disas 0x400540 |
反汇编0x400540 附近的函数 |
disas 0x400540.0x40054d |
反汇编指定范围内的代码 |
print /x $rip |
以十六进制输出程序计数器(%rip) 的值 |
print $rip |
以十进制输出程序计数器(%rip) 的值 |
print /t $rip |
以二进制输出程序计数器(%rip) 的值 |
print 0x100 |
输出0x100 的十进制 |
print /x 555 |
输出555 的十六进制 |
print /x ($rsp + 8) |
以十六进制输出%rsp + 8 |
print *(long *) 0x7fff ffff e818 |
输出位于地址0x7fff ffff e818 的长整数 |
print *(long *) (%rsp + 8) |
输出位于地址%rsp + 8 的长整数 |
x/2g 0x7fffffffe818 |
检查从地址0x7fffffffe818 开始的双字(8 字节) |
x/20b sth |
检查函数sth 的前20 个字节 |
info frame |
有当前栈帧的信息 |
info registers |
所有寄存器的值 |
help |
获取有关GDB 的信息 |
3.10.3 内存越界引用和缓冲区溢出
缓冲区溢出(buffer overflow
)。在栈中分配某个字符数组来保存字符串,但是字符串的长度超出了为数组分配的空间。
示例:
echo
对应的汇编:
越界会破坏的信息:
缓冲区溢出的一个更加致命的使用就是让程序执行它本来不愿意执行的函数。这是一种最常见的通过计算机网络攻击系统安全的方法。
3.10.4 对抗缓冲区溢出攻击
1. 栈随机化
为了在系统中插入攻击代码,攻击者既要插入代码,也要插入指向这段代码的指针,这个指针也是攻击字符串的一部分。产生这个指针需要知道这个字符串放置的栈地址。
栈随机化的思想使得栈的位置在程序每次运行时都有变化。这类技术称为地址空间布局随机化(Address-Space Layout Randomization
),简称ASLR。
通常攻击者使用空操作雪橇(nop sled
),使程序”滑过“目标序列,即在实际攻击代码前插入一段很长的nop
(读作“no op”
,no operation
的缩写)指令。
示例:
个字节的nop sled
能破解的栈随机化需要枚举 次。
2. 栈破坏检测
计算机的第二道防线是能够检测到何时栈已经被破坏。
GCC提供一种栈保护者机制,来检测缓冲区越界。其思想是在栈帧中任何局部缓冲区与栈状态之间存储一个特殊的金丝雀(canary
),也称为哨兵值(guard value
),是在程序每次运行时随机产生的。
echo
函数示例:
程序第三行通过段地址%fs:40
从内存读入金丝雀的值,保存在栈中;
程序第十一行取出该值与原地址的值作比较,不相等则栈异常。
tips
:
容易越界的参数尽可能放置在栈底,以保护其他参数。
示例:
图(b
)中,参数v
比数组参数buf
更靠近栈顶,vbuf
缓冲越界不会破坏v
。
3. 限制可执行代码区域
最后一招是消除攻击者向系统插入可执行代码的能力。
随机化、栈保护和限制哪部分内存可以存储可执行代码——是用于最小化程序缓冲区溢出攻击漏洞三种最常见的机制。
3.10.5 支持变长栈帧
前面所讲的各种函数的机器级代码,都有一个共同点,即编译器能够预先确定需要为栈帧分配多少空间。
下面示例为局部存储是变长的。
%rbp
称为帧指针(frame pointer
)有时称为基址帧(base pointer
);
leave
指令将栈帧指针恢复到它之前的值(第20行)。等价于:
movq %rbp, %rsp ;Set stack pointer to begining of frame
popq %rbp ;Restore saved %rbp and set stack ptr to end of caller's frame
重点习题:
答案: