《C++反汇编与逆向分析技术揭秘》阅读笔记——第四章 观察各种表达式的求值过程

  第四章对数学基础要求比较高,详细介绍了各种表达式的汇编形式以及编译时的代码优化方法,并总结出了套路,对阅读汇编代码中关于的计算等代码段有比较大的帮助。同时,介绍了硬件的优化方法,打开了我新世界的大门。关于本章最后的CrackMe程序,这是作者仔仔细细带我们做的一次逆向之旅,代码的逆向过程写得清楚明白,很容易读懂,但前提是要对参数和数组这两块的汇编有了解,如果基础不是很好,可以先跳过这部分,往后面看完第八章再回来看。

Debug与Release模式

  Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。
在VC++6.0中常用的优化方案有O1方案(生成文件占用空间最小,空间上),O2方案(执行效率最快,时间上),Release通常默认O2模式,Debug使用Od+ZI选项。
  关于两种模式的详细内容,可以参考这篇文章
https://blog.csdn.net/wordwarwordwar/article/details/84206111

4.1.1各种算术运算的工作形式

(1)加法运算优化
  主要是去除无用代码并将可合并代码进行归并处理,同时将常量尽可能直接计算出来或者减少变量。这部分难度不大,作者讲得很透彻,不进行赘述。
  值得一提的是,作者提到了一种避免被优化的方法,将变量的初始值0修改为命令行参数的个数argc,由于argc在编译期间无法确定,所以编译器无法在编译过程中提前计算出结果,程序中的变量在Release模式下就不会被常量替换掉。
修改前:

修改后:

(2)减法运算优化
  优化方式与加法一致。
(3)乘法运算优化
  编译器首先会尝试乘法转换成执行周期较短的加法或移位。Debug模式下,乘2的幂采用移位方式,不是2的幂直接执行乘法指令。在Release模式下,乘法优化的精髓在于移位,也即对数据进行乘2的幂,比如15=(2+1)*(4+1),则会执行如下操作
lea eax,[eax+eax
2]
lea eax,[eax+eax
4]
(4)除法运算优化
  在C语言中,两个无符号整数相除,结果依然是无符号的,两个有符号整数相除,结果是有符号的,有符号数和无符号数混除,结果是无符号的,有符号数的符号位被作为数据位对待,然后作为无符号数参与计算。
  1)debug模式
  当除数不为2的幂时,利用cdq指令扩展eax才能进行除法指令div和idiv,cdq把edx的每一位都设成eax的最高位。

  当除数为2的幂时,并不使用div或者idiv指令进行操作,移位操作用的是sar(算术右移指令,即保留操作数的符号右移,如10000000算数右移一位是11000000,这要区别于shr逻辑右移指令,它右移时要用0补足,如10000000逻辑右移一位是01000000),这时候使用cdq指令的作用并不是为了应用除法指令,而是为了满足C语言的除法向0取整的特性,由于移位运算等价于向下取整,所以需要对商为负的情况做加1调整(比如下图中加上7后再移位)。这样可以避免分支结构的产生。

  2)release模式   看过书的人都知道这部分很多,对数学功底要求较高,如果需要这样反编译,还是写出等价步骤的数学表达式然后反推吧,死记不现实,在这里我也不总结了。记住魔数c=(2^n)/o(c为大数,o为除数)

(5)取模
  对两个变量取模或对非2的幂取模,可直接用div或idiv完成,余数在dx或edx。
  对于2的k次方,余数的值只需取得被除数二进制数值中最后k位即可。这可以通过and指令来完成,如果是有符号数,符号位和后k位置1,无符号数则后k位置1,如果是正数则直接跳出,负数则进一步取绝对值。

(6)取绝对值

  当eax>=0时,edx为0,异或后eax不变,做减法也不变当eax<0时,edx为0xFFFFFFFF(相当于-1),异或后相当于eax完全取反,做减法再加1,等价于取补码。

#### 4.1.2算术结果溢出 (1)进位   无符号数超出储存范围,没有符号位,不会破坏数据,多出的一位数据被进位标志位CF保存。可检查CF位检查数据是否进位。 (2)溢出   有符号数超出储存范围,破坏了符号位。可检查溢出标志位OF。 #### 4.1.3自增自减   C语言基础内容,没什么说的。
4.2.1 关系运算和条件跳转的对应

  条件跳转大家应该都比较熟悉了,通常情况下,它们与cmp和test指令一同出现。要记住,test做的是与运算,影响标志位但但不传回结果;cmp做的是减法,影响标志位但不传回结果。
如果test的两个操作数是一样的,如test eax eax,则当eax为0时,ZF置为1,否则置为0,这种情况下对ZF的影响与cmp是一样的,在本章最后的算法逆向中作者没讲到的strcmp函数也用到了这种情况。
!在这里复习一下标志位:

条件码:
  ①OF(Overflow Flag)溢出标志,溢出时为1,否则置0.标明一个溢出了的计算,如:结构和目标不匹配.
  ②SF(Sign Flag)符号标志,结果为负时置1,否则置0.
  ③ZF(Zero Flag)零标志,运算结果为0时置1,否则置0.
  ④CF(Carry Flag)进位标志,进位时置1,否则置0.注意:Carry标志中存放计算后最右的位.
  ⑤AF(Auxiliary carry Flag)辅助进位标志,记录运算时第3位(半个字节)产生的进位置。有进位时1,否则置0.
  ⑥PF(Parity Flag)奇偶标志.结果操作数中1的个数为偶数时置1,否则置0.

控制标志位:
  ⑦DF(Direction Flag)方向标志,在串处理指令中控制信息的方向。
  ⑧IF(Interrupt Flag)中断标志。
  ⑨TF(Trap Flag)陷井标志。

4.2.2 表达式短路

  表达式短路与后面讲到的选择分支几乎没什么区别,通过条件跳转指令实现分支或者递归,关键在于看懂跳转条件。

4.2.3条件表达式

  条件表达式构成:表达式1?表达式2:表达式3
  其执行顺序是由左向右,先判断再选择,但编译器不一定会按照这种方式进行编译。当表达式2或3有变量时,条件表达式不能被优化,会转换成分支结构。当表达式2和3都是常量时,可以优化。

  neg指令:求补   sbb指令:带借位减法:x1=x1-x2-CF
4.3 位运算

  有符号和无符号整数的左移位运算都通过shl实现(逻辑左移低位补0),有符号右移用sar(算数右移,用高位补齐),无符号右移用shr(逻辑右移高位补0)

4.4编译器使用的优化技巧

(1)编译器优化四个方向:
  1)执行速度优化(较为主要)
  2)内存储存空间优化
  3)磁盘储存空间优化
  4)编译时间优化
  编译器工作过程分为几个阶段:预处理——词法分析——语法分析——语义分析——中间代码生成——目标代码生成。优化的机会一般在中间代码生成阶段和目标代码生成阶段。

  中间代码生成阶段所做的优化不具备设备相关性,在不同硬件环境中都能用。主要方法如下:
  1.常量折叠:当计算公式中出现多个常量进行计算,且编译器可以在编译期间计算出结果时,这样源码中所有的常量计算都将被计算结果代替。
  2.常量传播:将编译期间可计算出结果的变量转换成常量,这样减少变量的使用且不影响程序运行。
  3.减少变量:若程序中某些变量的操作可由较多使用的变量简单地实现,则可用较多使用的变量代替那些变量,减少变量。
  4.公共表达式:可将多个归并为同一个表达式,减少运算量。
  5.复写传播:其实和减少变量差不多,只不过没有删去变量
  6.剪枝优化:将不可达分支剪去
  7.顺序语句代替分支:
  8.强度削弱:用加法或者移位代替乘法,用乘法或者移位代替除法。
  9.数学变换:运用数学方法减少运算次数或不必要的运算。
  10.代码外提:一般用于循环中,减少不必要的重复。

  目标代码生成阶段,是和设备有关的,与下列方案:

  1. 流水线优化
      指令的工作流程:
      a) 取指令
      b) 指令译码
      c) 按寻址方式确定操作数
      d) 取操作数
      e) 执行指令
      f) 存放计算结果
      若没有流水线优化,由于指令工作的每一步都需要时间,在进行一个步骤的时候,其他部件闲置,效率较低。在引入流水线机制之后,在第一条指令执行过程中,就可以对第二条指令进行读取和译码了,实现并行处理,提高了工作效率。
    Intel:
      长流水线设计,把每条指令划分出很多执行步骤,每个步骤工作内容相对简单。
      优点是容易设计电路,主频高。
      缺点是流水线冲洗(由于指令的相关性导致的并行错误,需要回滚,消除新指令的错误)时由于设计步骤较多,会导致发生错误后损失较大。
    AMD:
      多流水线设计,每条指令划分的工作阶段少,但流水线数量较多。
      优点是并行程度更高了,而且弥补错误更及时,错误影响较小。
      缺点是每个阶段要做的事情多,电路设计复杂,主频受到限制,处理器对流水线的管理成本增大。

  流水线机制何时出现错误:
  1)指令相关性:顺序安排的两条指令,后一条指令的执行依赖前一条指令的硬件资源。


  2)地址相关性:顺序安排的两条指令,前一条需要访问并回写到某一地址上,而后一条指令也需要访问这一地址。

2.分支优化
  为了配合流水线工作,处理器增加了一个分支目标缓冲器,在流水线工作模式下,如果遇到分支结构,就可以利用分支目标缓冲器预测并读取指令的目标地址,它可以动态记录和调整转移指令的目标地址,并对多个目标地址进行表格化管理。发生转移时,如果分支目标缓冲器中有记录,则读取记录地址,如果记录地址等于实际目标地址,则并行成功,否则流水线被冲洗。同一个分支多次预测失败则更新记录的目标地址。
  在编写程序时,把大循环放在内层可以增加分支预测的准确性,下面第一种失败10次,第二种失败10000次。

3.高速缓存优化
  计算机内存的访问效率大大低于处理器,在程序运行的过程中,被访问的数据和指令相对集中,所以处理器准备了片上高速缓存(cache)来存放经常需要访问的数据和代码。这些数据的内容和所在的虚拟地址(Virtual Address,VA)以表格方式一一对应起来,在处理器访问内存数据时,先去cache中看看这个VA有没有记录,如果有,则命中,无需访问内存单元;如果没有,则转换VA访问数据,并保存到cache中。通常,cache不仅会读取指令需要的数据,还会把这个地址附近的数据都读进来。为了节省cache的宝贵空间,VA值的低位不会被保存,即保存的数据是以2^n字节为单位的,这是由cache设计的数据组织方式确定的。
  现代操作系统的内存管理是分段加分页的管理模式,而页级管理是虚拟内存的基础,为了避免频繁访问三级页表数据,处理器准备了页表缓冲(Translation lookaside buffer, TLB )来存放长期命中的页表数据。需要访问虚拟机内存时,处理器会先去TLB查询是否命中,如果命中则直接查询TLB表中对应的物理地址。对于虚拟内存的管理,长期没有命中的分页会被交换到磁盘上,下次访问时会触发缺页中断,中断处理程序会把磁盘数据读回到RAM。
基于以上设计,cache优化有以下几点
  (1)数据对齐
Cache不保存VA的二进制低位,如果访问的地址是4的倍数,则可直接查询并提取,如果不是4的倍数,则要访问多次。因此,编译器在设置变量地址时会按照4字节边界对齐。
  (2)数据集中
将访问次数较多的数据或代码尽量安排在一起,一方面抓取命中数据时会抓取周围的其他数据;另一方面,能减少虚拟地址转换,减少缺页中断的发生次数。
  (3)减小体积
命中率高的代码段减少体积,尽量放入cache中。

4.5一次算法逆向之旅

  在这一节中,作者带大家通过IDA分析了一个加密算法,将加密的每一个部分都详细讲解了,算法逆向的过程非常好懂,但建议先学完后面的变量和数组再回来学习,不然比较难看懂。这个算法的逆向分析过程告诉了我写注释的重要性,寄存器的值不断变化,自己很容易就识别错了,需要对每一步都详细阅读并做好注释。
  逆向部分虽然不难,但这一节却有很多令我想吐槽的地方,同时也引出了我许多的疑惑。
  按照作者的最终写法把代码写出来之后会因为不能获得足够的参数而不能运行,因为这是一个加密程序,要让读者自己输入参数,也就是说,作者给出的只是加密程序,而不是破解程序,然而我还傻傻地试了半天发现运行不了,最后发现可以用cmd输入参数。最重要的是,这个加密算法用了大量位运算,这些位运算不可逆,无法推算回正确的密码,只能老老实实输入作者提供的密码,然后让程序进行密文比对丝毫没有crack的成就感。感觉这个不是CrackMe而是ReverseMe。下面是运行的三种结果(直接运行,输入正确密码,输入错误密码)

  吐完槽以后,谈谈自己的疑惑点,一开始我想不明白,数组有14个字节,但为什么最终加密的指针却是指向argv[1][11]的,后来我想明白了,这其实可以理解,因为最后是将两个字节的数据放入了强制转换为两字节的argv[1][11]中,相当于把argv[1][12]也填充了,而argv[1][13]为’\0’。
  我原本想不通为什么会是argv[1]而不是argv[0],后来上网查了一下明白了main函数三个参数的传入才明白了这其中的缘由。argv[0]是用来保存.exe文件地址的,从argv[1]开始保存输入的命令行,所以输入进去加密的那段字符就保存在了argv[1]。如果有不懂的,推荐大家去看这篇文章
https://www.cnblogs.com/sddai/p/10246775.html
  附上我自己做参数传入运行实验的结果。

首先是不输入参数。

然后是输入参数

另外,作者没有顺便把strcmp函数也分析了,但他教会了我们分析的方法,我们可以自己进行分析。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值