学习笔记Ⅰ系列是对《0day安全:软件漏洞分析技术》的学习记录和总结,希望可以由此入门PWN方向。
0x00 基础知识
0x01 内存
进程使用的内存都可以按照功能大致分成以下 4 个部分:
- 代码区
- 数据区
- 堆区
- 栈区
具体来说:
- PE 文件代码段中包含的二进制级别的机器代码会被装入内存的代码区(.text),处理器将到内存的这个区域一条一条地取出指令和操作数,并送入算术逻辑单元进行运算;
- 如果代码中请求开辟动态内存,则会在内存的堆区分配一块大小合适的区域返回给代码区的代码使用;
- 当 函数调用发生时,函数的调用关系等信息会动态地保存在内存的栈区,以供处理器在执行完被调用函数的代码时,返回母函数。这个协作过程如下图所示。
0x02 栈
一种数据结构,先进后出的数据表
栈的常见操作:
- 压栈(PUSH)
- 弹栈(POP)
标识栈的属性:
- 栈顶(TOP)
- 栈底(BASE)
0x03 系统栈
同一文件不同函数的代码在内存代码区中的分布可能相邻,也可能相离甚远,可能先后有序,也可能无序;但它们都在同一个 PE 文件的代码所映射的一个“节”里。我们可以简单地把它们在内存代码区中的分布位置理解成是散乱无关的。
函数调用的代码示例:
int func_B(int arg_B1, int arg_B2) { int var_B1, var_B2; var_B1=arg_B1+arg_B2; var_B2=arg_B1-arg_B2; return var_B1*var_B2; } int func_A(int arg_A1, int arg_A2) { int var_A; var_A = func_B(arg_A1,arg_A2) + arg_A1 ; return var_A; } int main(int argc, char **argv, char **envp) { int var_main; var_main=func_A(4,3); return var_main; }
上述代码流程:
- 执行main函数时调用fun_A;
- 执行fun_A时调用fun_B;
- fun_B执行完后回到fun_A;
- 继续执行完fun_A后回到main。
在上述函数调转中系统栈的变化如下:
- 在 main 函数调用 func_A 的时候,首先在自己的栈帧中压入函数返回地址,然后为 func_A 创建新栈帧并压入系统栈;
- 在 func_A 调用 func_B 的时候,同样先在自己的栈帧中压入函数返回地址,然后为 func_B 创建新栈帧并压入系统栈;
- 在 func_B 返回时,func_B 的栈帧被弹出系统栈,func_A 栈帧中的返回地址被“露” 在栈顶,此时处理器按照这个返回地址重新跳到 func_A 代码区中执行;
- 在 func_A 返回时,func_A的栈帧被弹出系统栈,main 函数栈帧中的返回地址被“露” 在栈顶,此时处理器按照这个返回地址跳到 main 函数代码区中执行。
0x04 寄存器
- ESP(Extended Stack Pointer),指针指向栈顶
- EBP(Extended Base Pointer),指针指向栈底
- EIP(Extended Instruction Pointer),指针指向下一条等待执行的指令地址
0x05 函数调用
函数调用需要用到的指令序列:
- push arg3
- push arg2
- push arg1
- call 保存retn(当前代码区的下一条指令),同时跳转到所调函数的入口处
- push ebp
- mov ebp, esp
- sub esp, xxx
函数返回时的相关指令:
- addesp, xxx; 降低栈顶,回收当前的栈帧;
- pop ebp; 将上一个栈帧底部位置恢复到 ebp;
- retn; 这条指令有两个功能:a)弹出当前栈顶元素,即弹出栈帧中的返回地址。至此,栈帧恢复工作完成。b)让处理器跳转到弹出的返回地址,恢复调用前的代码区。