ARM逆向-表达式求值

Posted 嘻嘻兮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ARM逆向-表达式求值相关的知识,希望对你有一定的参考价值。

表达式求值也就是把各类加减乘除的运算都看一遍,这里主要记录的是Release版中的一些优化,各种优化手段我感觉有x86的逆向基础还是会比较好理解的,因为很多都很类似。

首先看一下加减的运算,这里主要可能会出现常量折叠和常量传播等优化手段

int main(int argc,char* argv[])

    int n1 = argc;
    int n2 = 0;
    scanf("%d", &n2);
    n1 = n1 - 100;
    n1 = n1 + 5 - n2 ;
    printf("n1 = %d \\r\\n", n1);
    return 0;
汇编代码如下(armeabi-v7a)
.text:0000060C                 MOV             R4, R0  ;n1 = argc
.text:00000610                 MOV             R1, SP  ;scanf的参数二
.text:0000061A                 MOVS            R0, #0
.text:0000061C                 STR             R0, [SP]  ;n2=0 初始化
.text:0000061E                 LDR             R0, =(unk_768 - 0x624)
.text:00000620                 ADD             R0, PC  ; unk_768 ; format
.text:00000622                 BLX             scanf
.text:00000626                 LDR             R0, [SP]  ;r0 = n2
.text:00000628                 SUBS            R0, R4, R0 ;r0 = argc - n2
.text:0000062A                 SUB.W           R1, R0, #0x5F //r1 = r0 - 0x5F = argc - n2 - 95
.text:0000062E                 LDR             R0, =(aN1D - 0x634)
.text:00000630                 ADD             R0, PC  ; "n1 = %d \\r\\n"
.text:00000632                 BLX             printf

通过上面可以看到,对于最后的n1的结果是可以优化的

n1 = n1 - 100
n1 = n1 + 5 - n2 
   = n1 - 100 + 5 - n2
   = n1 - 95 - n2
   = n1 - n2 - 95

而n1的值就是argc,所以上面的计算结果就使用argc减去n2,最后再减去95获取结果。

下面再来看乘法

int main(int argc,char* argv[])

    int n1 = argc;
    int n2 = 0;
    scanf("%d", &n2);

    printf("n1 * 15 = %d\\n", n1 * 15);       //变量乘常量 ( 常量值为非 2 的幂 )
    printf("n1 * 16 = %d\\n", n1 * 16);       //变量乘常量 ( 常量值为 2 的幂 )
    printf("2 * 2 = %d\\n", 2 * 2);           //两常量相乘
    printf("n2 * 4 + 5 = %d\\n", n2 * 4 + 5); //混合运算
    printf("n1 * n2 = %d\\n", n1 * n2);       //两变量相乘
    return 0;

汇编代码如下(armeabi-v7a)

.text:00000620                 LDR             R0, =(printf_ptr - 0x62A)
.text:00000622                 RSB.W           R1, R4, R4,LSL#4   //r1 = r4 << 4 - r4 = r4 * 16 - r4 = r4 * 15
.text:00000626                 ADD             R0, PC  ; printf_ptr
.text:00000628                 LDR             R6, [R0] ; printf
.text:0000062A                 LDR             R0, =(aN115D - 0x630)
.text:0000062C                 ADD             R0, PC  ; "n1 * 15 = %d\\n"
.text:0000062E                 BLX             R6      ; printf
.text:00000630                 LDR             R0, =(aN116D - 0x638)
.text:00000632                 LSLS            R1, R4, #4  //左移4位,相当于直接乘以16
.text:00000634                 ADD             R0, PC  ; "n1 * 16 = %d\\n"
.text:00000636                 BLX             R6      ; printf
.text:00000638                 LDR             R0, =(a22D - 0x640)
.text:0000063A                 MOVS            R1, #4  //这里常量折叠直接得出结果4
.text:0000063C                 ADD             R0, PC  ; "2 * 2 = %d\\n"
.text:0000063E                 BLX             R6      ; printf
.text:00000640                 LDR             R1, [SP]  ;获取n2
.text:00000642                 MOVS            R0, #5
.text:00000644                 ADD.W           R1, R0, R1,LSL#2  //r1 = 5 + n2 << 2 = 5 + n2*4
.text:00000648                 LDR             R0, =(aN245D - 0x64E)
.text:0000064A                 ADD             R0, PC  ; "n2 * 4 + 5 = %d\\n"
.text:0000064C                 BLX             R6      ; printf
.text:0000064E                 LDR             R0, [SP]
.text:00000650                 MUL.W           R1, R0, R4  //n1*n2
.text:00000654                 LDR             R0, =(aN1N2D - 0x65A)
.text:00000656                 ADD             R0, PC  ; "n1 * n2 = %d\\n"
.text:00000658                 BLX             R6      ; printf

这里主要说的就是第一个表达式,使用了一个反向减法,也就是结果等于操作数三减去操作数二的值。

下面就是除法运算了,对于除数是变量的情况,这里就直接调用除法函数了

.text:0000063A                 LDR             R1, [SP]
.text:0000063C                 MOV             R0, R4
.text:0000063E                 BL              __divsi3  ;除法函数 r0/r1

所以下面主要就探讨一下除数为常量的情况,这里一共有九种情况,首先这九种情况是分被除数是有无符号的,无符号的情况三种,有符号的情况六种。对于除法优化而言,下面就直接给出一些公式了,也就是说这个公式是对应着汇编的表现形式,至于需要深入了解原因的可以参考x86的逆向部分,对于除法有细讲了两篇,其优化原理是一致的。

先来看无符号情况下的三种

int main(int argc,char* argv[])

    printf("argc / 16 = %u", (unsigned int)argc / 16);  //变量除以常量,常量为无符号2的幂
    printf("argc / 3 = %u", (unsigned int)argc / 3);  //变量除以常量,常量为无符号非2的幂(上)
    printf("argc / 7 = %u", (unsigned int)argc / 7);  //变量除以常量,常量为无符号非2的幂(下)
    return 0;

首先在我armeabi-v7a这个版本中,只有除以2的幂时才出现了优化,也许除以非2的幂出现了Magic Number就不太划算了直接调用了上面的除法函数,所以对于后面两种就使用arm64-v8a来观察优化

首先来看常量为无符号2的幂

.text:00000600                 MOV             R4, R0
.text:00000602                 LSRS            R1, R0, #4  //无符号右移n位(2^n)
.text:00000604                 LDR             R0, =(printf_ptr - 0x60A)
.text:00000606                 ADD             R0, PC  ; printf_ptr
.text:00000608                 LDR             R5, [R0] ; printf
.text:0000060A                 LDR             R0, =(aArgc16U - 0x610)
.text:0000060C                 ADD             R0, PC  ; "argc / 16 = %u"
.text:0000060E                 BLX             R5      ; printf

这一种情况比较简单,公式如下,直接右移n位即可

x >> n

再来看常量为无符号非2的幂,这里是上

.text:00000000000006C8                 MOV             W8, #0xAAAB
.text:00000000000006CC                 MOVK            W8, #0xAAAA,LSL#16  //这里和上面拼接位 0xAAAAAAAB
.text:00000000000006D0                 UMADDL          X8, W19, W8, XZR  //w19就是argc
.text:00000000000006D4                 ADRP            X0, #aArgc3U@PAGE ; "argc / 3 = %u"
.text:00000000000006D8                 LSR             X1, X8, #33  //最后结果右移33位
.text:00000000000006DC                 ADD             X0, X0, #aArgc3U@PAGEOFF ; "argc / 3 = %u"
.text:00000000000006E0                 BL              .printf

这一种情况是最基础的情况,也就是将除法运算转为乘法运算,公式如下:(原因可参考这

x * M >> 32 >> n

这里的x表示被除数,也就是argc,M表示Magic Number,也就上面的0xAAAAAAAB,最后的右移n位需要根据具体情况判断

那么这里的话很明显,因为最后右移了33位,所以这里的n值为1。还原公式如下

2^(32+n) / M

对于上例的还原
2 ^ 33 / 0xAAAAAAAB = 2.9999999996507540345598531381041  ->  3

注意,这里的还原说的是如何还原出常量值,因为常量值确定,被除数在代码中是有体现的,这样子就能还原出除法了。还有这里最终的结果都需要向上取整,而且一般结果小数点都是.9999...,如果不是这个结果,那么需要注意哦,是不是有可能求错了。

再来看上面的那种情况二,这种情况是Magic Number会溢出的情况,而Magic Number一旦溢出则这个乘法可能就是一个大数乘法了,所以需要再次优化

.text:00000000000006E4                 MOV             W8, #0x4925
.text:00000000000006E8                 MOVK            W8, #0x2492,LSL#16  //0x24924925
.text:00000000000006EC                 UMADDL          X8, W19, W8, XZR  //argc * M
.text:00000000000006F0                 LSR             X8, X8, #32     //x8 = (argc * M) >> 32
.text:00000000000006F4                 SUB             W9, W19, W8  //使用argc再去减去上面值
.text:00000000000006F8                 ADD             W8, W8, W9,LSR#1 //w8 + w9 >> 1
.text:00000000000006FC                 ADRP            X0, #aArgc7U@PAGE ; "argc / 7 = %u"
.text:0000000000000700                 LSR             W1, W8, #2  //最终结果右移2位
.text:0000000000000704                 ADD             X0, X0, #aArgc7U@PAGEOFF ; "argc / 7 = %u"
.text:0000000000000708                 BL              .printf

可以看出来,这个公式就复杂很多了,公式如下

((x - (x * M) >> 32) >> n1) + ((x * M) >> 32) >> n2

注意上面公式中的加减运算的优先级是高于移位运算的,别看花了,还有个比较好记的口诀就是乘减移加移。对于上面的例子而言,n1的值就是1了,而n2的值就是2了

还原公式如下,因为上面有说了M值有进位的情况,所以这里的还原需要将M值进一(编译器只会控制进一,也就是加上2^32)

2^(32 + n1 + n2) / (2^32 + M)

对于上例的还原
  2 ^ (32 + 1 + 2) / (2^32 + 0x24924925)
= 2^35 / 0x124924925
= 6.999999999388819560461955299793
-> 7

 

OK,下面再来看一下有符号的情况了,这里共有六种

int main(int argc,char* argv[])

    printf("argc / 8 = %d", argc / 8);  //变量除以常量,常量为 2 的幂
    printf("argc / 9 = %d", argc / 9);  //变量除以常量,常量为非2的幂上
    printf("argc / 7 = %d", argc / 7);  //变量除以常量,常量为非2的幂下
    printf("argc / -8 = %d", argc / -8);  //变量除以常量,常量为负2的幂
    printf("argc / -5 = %d", argc / -5);  //变量除以常量,常量为负非2的幂上
    printf("argc / -7 = %d", argc / -7);  //变量除以常量,常量为负非2的幂下
    return 0;

先来看第一种,常量为 2 的幂,对于有符号数而言,他是需要考虑正负数的,其中正数其实就是上面无符号的情况,而对于负数的情况就是需要做一些调整,先来看公式,再来看对于的汇编代码

    x >= 0  
        x >> n
    x < 0  
        (x + 2^n-1) >> n  //调整值需要加上2^n-1

汇编代码如下

.text:00000604                 MOV             R4, R0
.text:00000606                 ASRS            R0, R0, #31 //有符号右移32位 0 || -1
.text:00000608                 ADD.W           R6, R4, R0,LSR#29 //argc + r0 >> 29 (无分支调整 +0 || +7)
.text:0000060C                 LDR             R0, =(printf_ptr - 0x612)
.text:0000060E                 ADD             R0, PC  ; printf_ptr
.text:00000610                 ASRS            R1, R6, #3 //最后有符号右移3位
.text:00000612                 LDR             R5, [R0] ; printf
.text:00000614                 LDR             R0, =(aArgc8D - 0x61A)
.text:00000616                 ADD             R0, PC  ; "argc / 8 = %d"
.text:00000618                 BLX             R5      ; printf

对于上面的代码,开始做了一个无分支的优化,首先如果是负数,那么这里需要加上2^n-1(也就是7) ,所以如果是负数,那么开始R0 = 0xFFFFFFFF,然后R0又无符号右移29位(高位填0),此时这个值就是7了。

因为这里相对于无符号而言,就是多了一个负数的调整,所以也没什么公式可言,根据最后右移多少位来计算常量值即可(2^n)

再来看常量为非2的幂上的情况,这里又需要看arm64-v8a了,v7a中调用除法函数

这种情况相对于无符号而言也是一样的,对于负数需要做一个调整(最终的结果+1),公式如下

    x >= 0 
        x * M >> 32 >> n 
    x < 0 
        (x * M >> 32 >> n) + 1

汇编代码如下

.text:00000000000006D8                 MOV             W8, #0x8E39
.text:00000000000006DC                 MOVK            W8, #0x38E3,LSL#16  //0x38E38E39
.text:00000000000006E0                 SMADDL          X8, W19, W8, XZR  //argc * M
.text:00000000000006E4                 LSR             X9, X8, #32  //右移32位
.text:00000000000006E8                 LSR             X8, X8, #63  //这里是无分支,如果结果为正,那么x8=0,否则x8=1
.text:00000000000006EC                 ADRP            X0, #aArgc9D@PAGE ; "argc / 9 = %d"
.text:00000000000006F0                 ADD             W1, W8, W9,ASR#1  //最终右移后再加上调整值
.text:00000000000006F4                 ADD             X0, X0, #aArgc9D@PAGEOFF ; "argc / 9 = %d"
.text:00000000000006F8                 BL              .printf

上面的汇编代码就是前面公式的具体体现,里面也用到了一个无分支,右移63位,如果原本值是负的,那么结果会为1。

对于还原的公式,和前面是一样的,因为这里还是只多了一个负数的情况判断而言,还原公式如下

2^(32+n) / M

对于上例的还原
  2^(32+1) / 0x38E38E39 
= 2^33 / 0x38E38E39 
= 8.9999999989522621036795594143123
-> 9

再来看常量为非2的幂下的情况

对于这种情况,其实是因为出现了Magic Number为负的情况,但是由于Magic Number本身的定义就是一个无符号数值,那么也就是说我们原本需要乘以一个正数(大于0x7FFFFFFF),此时变为了乘以一个负数,因为这样子其值肯定变小了,所以需要调整肯定是加上一个值,公式如下

    x >= 0 
        (x * M >> 32) + x >> n 
    x < 0  
        ((x * M >> 32) + x >> n) + 1

对于的汇编代码如下

.text:00000000000006FC                 MOV             W8, #0x2493
.text:0000000000000700                 MOVK            W8, #0x9249,LSL#16  //0x92492493 负数值
.text:0000000000000704                 SMADDL          X8, W19, W8, XZR //先乘以M
.text:0000000000000708                 LSR             X8, X8, #32
.text:000000000000070C                 ADD             W8, W8, W19 //这里就是调整的值 +x
.text:0000000000000710                 ASR             W9, W8, #2  //调整后再右移2位
.text:0000000000000714                 ADRP            X0, #aArgc7D@PAGE ; "argc / 7 = %d"
.text:0000000000000718                 ADD             W1, W9, W8,LSR#31  //W8 >> 31这里是用于判断是不是负数,负数需要最终结果+1
.text:000000000000071C                 ADD             X0, X0, #aArgc7D@PAGEOFF ; "argc / 7 = %d"
.text:0000000000000720                 BL              .printf

所以这种情况其实本质是解决的是乘以无符号的数如何转化为有符号的数,其本质原理不变,所以其还原的公式也没有变化

2^(32+n) / M

对于上例的还原
  2^(32+2) / 0x92492493
= 2^34 / 0x92492493
= 6.9999999979627318686215638099758
-> 7

注意对于公式中的Magic Number值,我们需要还是需要将其看成一个无符号的数,而不是一个负数。

好了,下面可以看负数的三种情况了,先看2的幂的情况,对于这种情况,其实也就是最终结果求个负即可

a/-b = -(a/b)
x / -2^n = -(x / 2^n)

所以对于负数的这三种情况,其实本质都是最终结果求个负即可,汇编代码如下

.text:00000604                 MOV             R4, R0
.text:00000606                 ASRS            R0, R0, #31
.text:00000608                 ADD.W           R6, R4, R0,LSR#29 //这里就是无分支调整 +0 || +7
.text:0000063A                 MOVS            R0, #0
.text:0000063C                 SUB.W           R1, R0, R6,ASR#3 //0 - r6>>3
.text:00000640                 LDR             R0, =(aArgc8D_0 - 0x646)
.text:00000642                 ADD             R0, PC  ; "argc / -8 = %d"
.text:00000644                 BLX             R5      ; printf

可以发现最终使用0去减结果,也就是相当于最后取了一个负数,这里的还原也就简单了,直接看右移多少位,如果最后有取反的操作,那么说明此时的常量值是一个负数。

再来看常量为负非2的幂上的情况,这里也就是最终结果求个负,因为在非2的幂中涉及到了Magic Number(常量值),所以可以直接将这个求反操作放在常量值中即可,公式如下

    x >= 0 
        x * -M >> 32 >> n 
    x < 0  
        (x * -M >> 32 >> n) + 1

此时的M值就是一个已经求反过后的值。汇编代码如下

.text:0000000000000734                 MOV             W8, #0x99999999
.text:0000000000000738                 SMADDL          X8, W19, W8, XZR
.text:000000000000073C                 LSR             X9, X8, #32  //右移32位
.text:0000000000000740                 LSR             X8, X8, #63  //无分支求 0 || 1,最终结果的调整
.text:0000000000000744                 ADRP            X0, #aArgc5D@PAGE ; "argc / -5 = %d"
.text:0000000000000748                 ADD             W1, W8, W9,ASR#1  //右移一位后加上调整值
.text:000000000000074C                 ADD             X0, X0, #aArgc5D@PAGEOFF ; "argc / -5 = %d"
.text:0000000000000750                 BL              .printf

可以发现,这里的代码和上面的非2的幂情况是一摸一样的,不过注意看,此时的M值是一个负数,因为取反了(原先是正数)

还原的公式如下,因为这里的M是一个取反值,所以在还原中,我们还是需要使用原本的值来计算(取反前的值,也就是2^32 -  M)

2^(32+n) / (2^32-M)

对于上例的还原
 2^(32+1) / (0 - 0x99999999)
=2^33 / 0x66666667
=4.9999999982537701732058415050132
-> -5

好了,下面就是最后一种情况,对于上面正数的情况中,会出现一种Magic Number为负数的情况,那么此时如果在对其取反,那么这个M值就是一个正数了(取反后M值是负数为正常情况,就是需要乘以一个负数),此时相乘肯定结果会变大,所以此时需要调整减去一个值。公式如下

x >= 0   
    (x * -M >> 32) - x >> n
x <  0    
    ((x * -M >> 32) - x >> n) + 1

对应的汇编代码如下

.text:0000000000000754                 MOV             W8, #0xDB6D
.text:0000000000000758                 MOVK            W8, #0x6DB6,LSL#16  //0x6DB6DB6D
.text:000000000000075C                 SMADDL          X8, W19, W8, XZR
.text:0000000000000760                 LSR             X8, X8, #32
.text:0000000000000764                 SUB             W8, W8, W19  //这里的调整就是需要减去argc
.text:0000000000000768                 ASR             W9, W8, #2  //再右移2位
.text:000000000000076C                 ADRP            X0, #aArgc7D_0@PAGE ; "argc / -7 = %d"
.text:0000000000000770                 ADD             W1, W9, W8,LSR#31 //最后需要加上一个调整值
.text:0000000000000774                 ADD             X0, X0, #aArgc7D_0@PAGEOFF ; "argc / -7 = %d"

可以发现,上面的Magic Number的确是一个正数值,对于还原公式而言,和上面是一样的,因为本质,没有变化

2^(32+n) / (2^32-M)

对于上例的还原
 2^(32+2) / (2^32 - 0x6DB6DB6D)
=2^34 / 0x92492493
=6.9999999979627318686215638099758
-> -7

 

除法整理完就看下面的取模运算,对于取模运算而言,首先其结果的符号位是跟着被除数走的,所以就没有正负数的情况考虑了,主要就以下两种

int main(int argc,char* argv[])

    printf("argc % 8 = %d", argc % 8);  //变量模常量,常量为2的幂
    printf("argc % 9 = %d", argc % 9);  //变量模常量,常量为非2的幂
    return 0;

先来看第一种,常量为2的幂的情况,公式如下

    x > 0  
        x - (x & ~(2^n-1))
	x < 0  
        x - (x + (2^n-1) & ~(2^n-1))

这个公式的本质其实就是取低几位,如模8,那么就是取低三位即可,一般情况下我们可以直接and 7就是结果了(正数)

假设x的二进制值为YYYYYYYYYXXX  

x & ~7
YYYYYYYYYXXX
111111111000
-------------
YYYYYYYYY000

x - (x & ~7)
YYYYYYYYYXXX
YYYYYYYYY000
-------------
000000000XXX

因为在x86的逆向中没有遇到过这种公式,所以这里就稍微详细的记录一下,本质上就是求最后的低n位,当然上面只是举一个简单例子(Y的位数略有不足)。汇编代码如下

.text:0000060C                 MOV             R4, R0
.text:0000060E                 ASRS            R0, R0, #31  // 无分支 -1 || 0
.text:00000610                 ADD.W           R0, R4, R0,LSR#29  // if argc > 0 r0 = argc + 0 
                                                                  //else r0 = argc + 7
.text:00000614                 BIC.W           R0, R0, #7  //下面开始取低n位,先 &~7
.text:00000618                 SUBS            R1, R4, R0  //然后减去即可得到结果
.text:0000061A                 LDR             R0, =(aArgc8D - 0x620)
.text:0000061C                 ADD             R0, PC  ; "argc % 8 = %d"
.text:0000061E                 BLX             printf

首先开始先做了一个无分支的优化,因为对于负数需要先加上一个2^n-1,毕竟符号需要跟着被除数走,那么这里加一个2^n-1,最终减去肯定是一个负数值。

对于BIC是位清零指令,也就是最后的n位清零,相当于 and ~n的操作,后面的两行代码就是求最后的n位了。

下面再来看非2的幂情况,公式如下,这个公式还是很好理解的,不理解可以参考取模运算

x - (x / n * n)

那么可以看到,上面的公式出现了除法,所以此时就和上面的除法优化又有关系了,汇编代码如下

.text:00000000000006D8                 MOV             W8, #0x8E39
.text:00000000000006DC                 MOVK            W8, #0x38E3,LSL#16  //0x8E3938E3
.text:00000000000006E0                 SMADDL          X8, W19, W8, XZR
.text:00000000000006E4                 LSR             X9, X8, #0x3F
.text:00000000000006E8                 ASR             X8, X8, #0x21
.text:00000000000006EC                 ADD             W8, W8, W9   //上面都是除法优化,此时w8就是商
.text:00000000000006F0                 ADD             W8, W8, W8,LSL#3  //w8 = w8 + w8*8 = w8*9  也就是*n
.text:00000000000006F4                 ADRP            X0, #aArgc9D@PAGE ; "argc % 9 = %d"
.text:00000000000006F8                 SUB             W1, W19, W8  //最后argc减去w8获取余数
.text:00000000000006FC                 ADD             X0, X0, #aArgc9D@PAGEOFF ; "argc % 9 = %d"
.text:0000000000000700                 BL              .printf

最后简单记录下三目运算表达式而言,因为ARM汇编中提供了条件执行,那么此时很容易使用汇编来表达

printf("%d\\n", argc == 5 ? 5 : 6);

汇编代码如下

.text:000005F8                 MOV             R1, R0  //r1 = argc,也就是等于5
.text:000005FA                 CMP             R0, #5
.text:000005FC                 IT NE  //这里表示下一条指令NE条件执行
.text:000005FE                 MOVNE           R1, #6  //不相等则执行 - r1=6
.text:00000600                 LDR             R0, =(unk_7E7 - 0x606)
.text:00000602                 ADD             R0, PC  ; unk_7E7 ; format
.text:00000604                 BLX             printf

 

以上是关于ARM逆向-表达式求值的主要内容,如果未能解决你的问题,请参考以下文章

逆波兰表达式求值

逆波兰表达式求值

逆波兰表达式求值

LeetCode:逆波兰表达式求值150

150. 逆波兰表达式求值

150. 逆波兰表达式求值