《老码识途:从机器码到框架的系统观逆向修炼之路》- 第1章 - 总结

来源:互联网 发布:eclipse使用ubuntu字体 编辑:程序博客网 时间:2024/04/30 03:07

本章学到了什么

  1. 调试技巧:在VS中断点调试,查看反汇编代码,step into进行步进调试,运行过程中查看寄存器、内存地址、变量值变化等。
  2. 机器码构造能力:使用C/C++中的直接在C代码里写汇编语言的功能(_asm)。学会了常见的汇编指令,接触了几个带有循环、跳转的汇编语言代码。
  3. 指针机制:对C/C++中的指针机制有了更深的了解。
  4. 函数调用机制:函数调用过程中栈的变化。函数调用约定的大致了解。
  5. 数组模型
  6. 结构体模型
  7. 对齐:搞清楚了为什么对齐这么重要。
  8. switch分析:switch的汇编实现原理。
  9. 加载期重定位:二进制编译和加载过程中的重定位机制究竟是怎么回事。

学习感想

  1. “猜测 - 实证 - 构建”的学习方法。

在学习底层相关的知识时,总会遇到很多问题。那么遇到问题,一定要追根究底,不能马马虎虎就过去了。不能“大概明白了”,而是要真的搞明白。这个具体的过程就是,在学习的过程中,提出问题,猜想这个问题的答案可能是什么,根据自己的猜想去写代码或者调试求证。证实了之后,提炼出这个问题包含的知识点,再构建例子与验证它。这个完整的过程搞明白了,对于一个知识点才算是真正地理解了。

反观我自己之前的学习方法,看资料的时候,往往一带而过,缺乏追根问底的精神,所以感觉一个东西好像搞明白了,其实根本就没有。在平日学习中遇到的问题,很多时候也是上网搜一下知道怎么回事就算了,却没有自己动手做一做,下一次一遇到的时候,还是不会。

  1. 体系化的学习可遇而不可求,要学会零散式地学习。

学习底层知识时,大部分知识点都是零散的,所以我们不应该好高骛远,追求一蹴而就。在学习的过程中,我们会遇到很多实际问题,然后发现自己这也不会那也不会,感觉好像无从下手。我认为,应该就事论事,遇到问题,就去搜索对应的解决方案,发现了一个问题解决一个问题,每次解决一个小的知识点,整个知识网络就在解决这些小知识点的过程中慢慢构建起来了。打个比喻,我们的学习一开始全是漏洞,补都不补过来,但是不要失去信心,每次解决一个小问题,慢慢地零散的知识就结成网了,慢慢变得滴水不漏。

学习笔记

  1. 猜测 - 实证 - 构建
  2. 使用VS2008的反汇编、监视窗口、内存窗口、单步、断点、全局变量赋值的反汇编。
  3. C语言中的指针大小为4,只存放了地址,那么类型信息有什么用呢?类型信息决定了在该地址处理数据的大小,即赋值/读取时写/读多少字节。例如int *p,那么对应汇编指令会使用dword,就是4字节。
  4. 指针强制转换的影响不是发生在转换时(因为地址都是4字节),而是在转换后赋值的时候,访问内存的字节大小。要保证指针强制转换是安全的,必须保证转换后的指针指向的数据类型大小小于原数据类型大小。
  5. 对于一个补码形式的负数求其正数值,就是求反加1。
  6. x86系列CPU的call指令寻址方式为:用与call指令相关的偏移量定位到跳转的地址。
    偏移量计算:偏移量 = 跳转到的地址 - call指令后一条指令的起始地址。
  7. call指令将返回地址保存在内存中,而且ESP寄存器指向了该内存。实际上这块内存就是栈——ESP指向栈的栈顶。每次压栈,栈顶地址变小,即ESP的值变小。
    实际上call指令相当于两条指令的组合:
    push 返回地址
    jmp 函数入口地址
  8. C语言的参数传递是从右往左压栈传递参数。
  9. ESP的存在是为了指明栈顶的位置,那么EBP存在是为了什么呢?为了每一次调用函数时,能够顺利找到压到栈中的参数位置。怎么找?利用一个确定的基址加上偏移。这个基址就是EBP。为了确保EBP的正确,每个函数调用时都要有保存和恢复EBP的过程。
    push ebp

    pop ebp
    在被调用的函数里执行时,EBP用来作为基址获取参数的值。
    在函数内分配局部变量的时候,要使用栈更低地址的空间,也就是继续压栈。
    所以,在使用EBP寻址的函数中,EBP+偏移量就是参数的地址(要回溯寻找),EBP-偏移量就是局部变量的地址。
  10. ret指令将栈顶保存的地址值弹入寄存器EIP,即pop eip。
    编译器必须保证执行ret时,ESP正好指向call指令压栈保存返回地址的那段内存。
  11. 编译器习惯上使用eax作为存储返回值的寄存器,被调用方在ret前设置eax,返回后,调用方从eax获取到该值。
  12. lea eax, [ebp + 10h] 即eax = ebp + 10h
  13. 平衡栈/清栈的两种方式:①调用方清栈,call返回后,执行add esp, x指令。②被调用方清栈:执行ret x指令。前者用空间代价换取了变参功能。
  14. 调用惯例calling convention
    (1)是寄存器还是栈传递参数
    (2)栈传递时,参数是从右往左还是从左往右压栈
    (3)谁来清栈,是调用方还是被调用方
    例如,C语言的调用方式是栈传参,参数从右往左压栈,调用方清栈。
    _stdcall是微软系统调用采用的惯例,除清栈是被调用方外,其他同C语言方式。
    _fastcall是寄存器传递。
  15. 函数指针
    函数指针包括入口地址和函数原型(函数参数表、返回类型、调用惯例)两方面的信息。
    函数指针赋值的原则是:只能将与指针原型匹配的函数的入口地址赋值给它。
    大多数函数指针强制类型转换都会出错,所以不要进行函数指针的强制类型转换。
    函数指针可用于虚函数调用。
  16. 基本数据(如单字节、双字节、四字节整数)存放处的地址必须能被自己数据类型的大小整除。
    对齐的规律:首先选定一个盒子,然后依序将字段往盒子中放,当盒子放不下后,又用下一个盒子存放,直至所有字段都存放完毕。
    盒子长度 = min{max{sizeof(成员变量)}, 对齐长度}
    字段放入盒子的可放置位置如下:
    离盒子头部偏移字节数 = n * sizeof(成员变量)(n=0, 1, 2, …)
    在编程的时候,遇到结构体,要注意是否有对齐问题。
  17. switch不能处理浮点数的原因是,它会将该数映射为数组的索引。
    实际上是一种跳转地址表的方法,计算复杂度不因分支的增加而增加,在大部分情况下比if-else要快。
  18. 在CPU保护模式下,每个执行进程(程序的一个实例)都拥有自己独立的线性地址空间,这种机制叫虚存系统。用户态程序无法直接访问物理内存。
    每个进程都有自己独立的0~4GB的线性地址空间。
    编译的时候就知道全局变量地址。这是因为编译器就能确定所有全局变量相对头部的偏移量,只要程序加载到编译器希望加载的地址,则所有全局变量地址在编译器都可以计算出。
    全局变量地址 = 程序头部加载地址b + 全局变量相对程序头部的偏移量a
    程序中要存储这个希望加载的地址,称为image base(基址)。
    如果不得不加载到一个不是希望加载的地址,那么就要进行重定位(显然这种情况经常出现)。如何确保修改成功呢?只要所有变量与基址的相对位置的偏移量是确定的,那么就没问题了。这样不需要去理解指令类型。
    relocAddr = actualBase + a
    *relocAddr = *relocAddr(重定位前) + actualBase - expectedAddr
    整个程序中有好多偏移量,所以程序中有一个表来存储这些偏移量,称为重定位表。
    Windows中的真实重定位是这样的:为了节省重定位项的空间,不是用4字节表示到程序头部的偏移量,而是将需要重定位的部分划分成一个个区(section)
    总偏移 = 区起始的偏移 + 2字节表示的区中的偏移
  19. 动态链接库中的重定位
    3个重要的API:LoadLibrary,将DLL从硬盘加载到内存;GetProcAddress,接收函数名作为输入,返回该函数入口地址。FreeLibrary,当获得函数地址后,调用此API卸载已加载库。
    在Win7之前,为了达到让所有程序共享同一DLL代码的目的,系统会将所有DLL加载到同一地址,就可以共享代码段进而节省空间。但是系统DLL固定加载基址这个特性,会被Windows下的溢出攻击利用。该个性如果不存在就几乎可以消除Windows下的溢出攻击利用。Windows 7中引入了dll随机加载的选项。(其实就是ASLR)
  20. 利用RTL学习汇编:
    (1)首先用汇编实现开发环境所带的运行时库(Run Time Library, RTL)中的函数,如C语言的RTL包括strlen、strcpy等。
    (2)然后分析系统库实现的这些函数,因为它们调用频繁,所以要求有很高的效率,基本都用汇编撰写。
    (3)再做性能实验,测试自己版本与系统版本的差异,并分析修改(可利用指令级分析工具Vtune)。
    (4)最后分析不同库实现的异同和好坏,如VC、C++ Builder、Delphi、GCC。
  21. 还有一种融汇底层知识的方法:通过编写攻防软件,将操作系统、汇编、编译原理、网络等知识在极为幽微处(如字节层次)贯通起来。
0 0