前言:总结的都是基于我在计算机系统学习中的相对个人重难点。
栈帧
栈帧的内部模拟图片(来源于百度图片):
图上所述结构一步一步来看:
- 返回地址以上部分的参数是存储在调用函数p中的栈帧中的变量值。而这个值是为下一步被调函数Q传参做准备的。需要注意的是,存在该部分的条件是因为寄存器中最多传递六个整型(即整型和指针参数)。而当传入的个数大于6时,超过六的部分参数,需要直接存储在栈帧中,并且是倒着顺序存入栈帧中(由栈顶到栈底顺序)。
补充栈上的局部存储:
1:寄存器不够存放所有的本地数据;
2:对某一个局部变量使用地址运算符“&”,必须为它产生一个地址;
3:某些局部变量数数组或者结构时。
引用《深入了解计算机系统》书上的两个例子:
第一个:
在long caller函数调用被调函数swap之前,开辟空间,将arg1和arg2先放入栈中,并且rsp存着&arg1,rsp+8存着&arg2,然后再将返回地址放入栈中,进行调用函数swap_add。
第二个
发现传入的参数的个数是8个,并且存在几个参数传入的地址,那么我们首先选出地址引用的&x1,&x2,&x3,&x4和第七个x4与第八个参数&x4.那么如上图根据“对某一个局部变量使用地址运算符“&”,必须为它产生一个地址”原则,为x1,x2,x3,x4按顺序产生地址,然后第七个和第八个参数就按照倒序的顺序存入即可x4存在rsp,&x4存在rsp+8,满足前面说的原则。而剩下的六个参数当然是存入寄存器中即可。
- 返回地址是由调用函数时存入的,如:
.
.
.
400560 e8 e3 ff ff ff callq 400548 < first >
400568 48 89 c2 mov...
在执行callq的时候,将返回地址存入栈中,并且跳转到被调用函数的第一条指令处。在执行被调函数过程中,遇到ret之后,就会弹出栈顶的指令,而此时就是地址0x400568
,然后跳转到这个地址继续执行。
- 被保存的寄存器,一般是%rbx,%rbp,%r12~%r15,这些输入被调用这个保存寄存器,就是需要被调函数执行保存这个几个寄存器的值的指令而调用者保存寄存器输入调用函数的范围,一样,除了以上说的寄存器和%rsp之外,被调用函数可以随意修改寄存器的值。
函数调用
0000000000400540 <multstore>:
•
•
400544: callq 400550 <mult2>
400549: mov %rax,(%rbx)
•
•
0000000000400550 <mult2>:
400550: mov %rdi,%rax
•
•
400557: retq
如上指令代码:在执行到callq之前,此时rip已经指向400544
地址处,然后执行下一步时,紧接在callq之后的地址400549
存入栈中,并且rip进入callq的调用函数中,直到rip遍历完被调函数,然后此时执行被调函数,rsp执行被调函数中,rip指向返回地址。而递归函数如上类似。
示例:
long add5(long b0, long b1, long b2, long b3, long b4) {
return b0+b1+b2+b3+b4;
}
long add10(long a0, long a1, long a2, long a3, long a4, long a5,
long a6, long a7, long a8, long a9) {
return add5(a0, a1, a2, a3, a4)+
add5(a5, a6, a7, a8, a9);
}
在第一次执行add5是,需要保存rbx,rbp,r9,因为他只使用了前五个参数,而第六个参数没使用,需要用r9寄存器存值并且需要被保护,防止可能被改变。而rbx与rbp属于被调用者保护寄存器,后面剩下4个参数,保存rbx,rbp个人觉得会是因为这两个寄存器是比较常使用的,也是防止内容被改变。
调用者保存
long call_incr() {
long v1 = 15213;
long v2 = incr(&v1, 3000);
return v1+v2;
}
/*汇编代码*/
call_incr:
subq $16, %rsp
movq $15213, 8(%rsp)
movl $3000, %esi
leaq 8(%rsp), %rdi
call incr
addq 8(%rsp), %rax
addq $16, %rsp
ret
step one:保存返回地址,将返回地址保存在栈中
step two:
subq $16, %rsp
movq $15213, 8(%rsp)
step three:
movl $3000, %esi
leaq 8(%rsp), %rdi
step four:
call incr
addq 8(%rsp), %rax
step five:
addq $16, %rsp
ret
被调用者保存寄存器
long call_incr2(long x) {
long v1 = 15213;
long v2 = incr(&v1, 3000);
return x+v2;
}
/*汇编代码:*/
call_incr2:
pushq %rbx
subq $16, %rsp
movq %rdi, %rbx
movq $15213, 8(%rsp)
movl $3000, %esi
leaq 8(%rsp), %rdi
call incr
addq %rbx, %rax
addq $16, %rsp
popq %rbx
ret
step one:保存返回地址,将返回地址保存在栈中(此处的空间不需要认为开辟,系统自动开辟空间,所以rsp也不需要认为修改,增加使用栈的大小)
step two:保存x的值在%rbx寄存器中,防止调用过程中被修改。
pushq %rbx
step three:开辟空间存变量值,因为此处参数是v1的地址,因此在栈中存值。
subq $16, %rsp
step four:存入v1的值。
movq %rdi, %rbx
movq $15213, 8(%rsp)
step five:调用完incr之后
movl $3000, %esi
leaq 8(%rsp), %rdi
call incr
addq %rbx, %rax
step six:恢复v1的值
addq $16, %rsp
popq %rbx
step seven:返回调用函数,结束调用
ret
ROP技术
以下内容部分参考csdn博主:海枫:使用ROP攻击技术
概念: 就是面向返回语句的编程方法,它借用libc代码段里面的多个retq前的一段指令拼凑成一段有效的逻辑,从而达到攻击的目标。
retq指令: 因为retq指令返到哪里执行,由栈上的内容决定,而这是攻击者很容易控制的地址
参数控制: 利用retq执行前的pop rax指令,将栈上的内容弹到指令的寄存器上,来达到预期目的。而一段retq指令未必能完全到想攻击目标的前提条件,那可在栈上控制retq指令跳到另一段retq指令表,如果它还达不到目标,再跳到另一段retq,直到攻击目标实现。
原理: 攻击目标为实现system(“echo success”) 这个函数调用。ROP可以借用libc的指令实现任何逻辑功能。
攻击实现:
调用system的参数为”echo success”字符串的地址,而字符串是栈注入的内容,那它的地址应该是rsp + offset。而函数调用时,第一个参数是放到rdi寄存里面。 所以需要从libc里面,在retq或者call *reg指令前找到rdi = rsp + offset逻辑等价的指令序顺,发现有如下的两条指令:
<setval>:
4004d9: c7 07 d4 48 89 c7 movl $0xc78948d4,(%rdi)
4004df: c3
48 89 c7:Encodes movq %rax, %rdi//该指令获得的地址Gadget address = 0x4004d9 + 0x3 = 0x4004dc
c3: rdi <- rax//地址:0x4004df
具体实现见计算机系统实验操作
来源:CSDN
作者:德林恩宝
链接:https://blog.csdn.net/qq_44116998/article/details/103321710