在上一篇文章中我说错了一件事,其实没有pc寄存器,pc全称是program counter (程序计数器),他是一个抽象的概念,在x86/x64下是ip的数值(也不是很准确,在以后的文章中我们不必纠结这些事情都管他叫做PC)
- rbp 寄存器大小
- pc 寄存器大小
在函数调用的时候会在栈中记录下函数调用后下一个指令的地址(pc),还会记录栈基址(rbp),这些数据记录在哪里?肯定就是内存里啦!我们再仔细看一眼汇编代码
//C
int testfunc(){
int a=0;
}
int testfunc2()
{
testfunc();
}
//asm
testfunc:
push rbp
mov rbp, rsp
mov DWORD PTR -4[rbp], 0
nop
pop rbp
ret
testfunc2:
push rbp
mov rbp, rsp
mov eax, 0
call testfunc
nop
pop rbp
ret
看汇编代码我们是不是看不哪里压栈了,这时候就要介绍一下call指令的执行过程
- 首先call指令会将PC入栈
- 然后会有一个jmp跳转到要执行的地址
说起来可能很抽象,下面用一张图来说明他的过程
这张图片应该就可以很清晰地描绘call指令都干了些啥了吧!
由此可见,在函数调用的过程中程序的返回地址存在栈栈中,也就是栈上的数据会影响程序的运行过程.试想如果我们修改了栈上特定位置的数值,并修改成特定的数值,那么我们就可以让程序走到意想不到的地方(栈溢出).
32位(x86情况下)
先上代码
int f=0;
int c=0;
int testfunc(){
int a=0;
f=1;
return c;
}
int testfunc2()
{
testfunc();
}
f:
.zero 4
c:
.zero 4
testfunc:
push ebp
mov ebp, esp
sub esp, 16
call __x86.get_pc_thunk.ax
add eax, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
mov DWORD PTR -4[ebp], 0
mov DWORD PTR f@GOTOFF[eax], 1
mov eax, DWORD PTR c@GOTOFF[eax]
leave
ret
testfunc2:
push ebp
mov ebp, esp
call __x86.get_pc_thunk.ax
add eax, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
call testfunc
nop
pop ebp
ret
__x86.get_pc_thunk.ax:
mov eax, DWORD PTR [esp]
ret
我们发现多了一个这个函数 __x86.get_pc_thunk.ax 这个是干什么的呢?我们来分析一下
在testfunc2中有这样一条指令
call __x86.get_pc_thunk.ax
首先call指令会将PC寄存器入栈,然后在 __x86.get_pc_thunk.ax 函数中进行了
eax, DWORD PTR [esp]
这个操作,esp是栈指针指向的是栈顶部也就是刚刚push的数据也就是PC寄存器的数值了,所以说这个函数是获取下一条指令的地址的,原因是因为x86架构下没有获取PC寄存器的指令只能用这种方式获取了.
PS:为什么要这么做
因为有一个功能叫做PIE(地址随机化)这个可以有效增加栈溢出导致REC风险
接着来看
add eax, OFFSET FLAT:_GLOBAL_OFFSET_TABLE_
这个 FLAT:GLOBAL_OFFSET_TABLE 是全局静态变量表所对应add指令的偏移量,这个是在编译时期确定的,所以这个代码的意思是找到全局静态变量表的地址并储存到eax寄存器中,为了方便我在下文分析函数调用的时候会关闭pie得到相对简单的汇编代码
int testfunc(int g){
g=8;
}
int testfunc2()
{
testfunc(2);
}
testfunc:
push ebp
mov ebp, esp
mov DWORD PTR [ebp+8], 8
nop
pop ebp
ret
testfunc2:
push ebp
mov ebp, esp
push 2
call testfunc
add esp, 4
nop
leave
ret
可以看到在x86下只有栈传参没有寄存器传参,其余的和x64下的是相同的