0%

C++反汇编与逆向分析 - 表达式

本章介绍了各种表达式的汇编形式以及汇编时代码优化的方法,如遇到数学模型无法理解,可跳过,后续将有结论;

运行环境:

  • 操作系统: Windows 7家庭版
  • 编译器:VC6 VS2013

Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。
在VC++6.0中常用的优化方案有O1方案(生成文件占用空间最小,空间上),O2方案(执行效率最快,时间上),Release通常默认O2模式,Debug使用Od+ZI选项。

表达式

SIG

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
md %1_objs
copy %1.lib %1_objs
pushd %1_objs
for /f %%i in ('link -lib /list %1.lib') do link -lib /extract:%%i %1.lib
for %%i in (*.obj) do pcf -g0 %%i
sigmake -n"%1.lib" *.pat %1.sig
if exist %1.exc for %%i in (%1.exc) do find /v ";" %%i > abc.exc
if exist %1.exc for %%i in (%1.exc) do > abc.exc more +2 "%%i"
copy abc.exc %1.exc
del abc.exc
sigmake -n"%1.lib" *.pat %1.sig
copy %1.sig ..\%1.sig
popd
del %1_objs /s /q
rd %1_objs

可直接提取符号,但是需要自己找到LIBC.LIB,然后将提取的SIG放入IDA的SIG文件中;

算数运算和赋值

各种算数工作形式

1)加法

加法对应的汇编指令为ADD。针对不同的操作数,转换的指令也不同,编译器会自动优化;

常见的方式为:变量+变量、变量+常量、常量+常量;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
9:        /*变量定义*/
10: int nVarOne = 0;
00401028 mov dword ptr [ebp-4],0
11: int nVarTwo = 0;
0040102F mov dword ptr [ebp-8],0
12: /*变量加常量*/
13: nVarOne = nVarOne + 1;
00401036 mov eax,dword ptr [ebp-4]
00401039 add eax,1
0040103C mov dword ptr [ebp-4],eax
14: /*常量加常量*/
15: nVarOne = 1 + 2;
0040103F mov dword ptr [ebp-4],3 ;常量加常量在编译期间直接给出结果
16: /*变量加变量*/
17: nVarOne = nVarTwo + nVarOne;
00401046 mov ecx,dword ptr [ebp-8]
00401049 add ecx,dword ptr [ebp-4]
0040104C mov dword ptr [ebp-4],ecx

当编译选项修改为Release后的汇编代码:

常量传递

将编译期间可计算出结果的变量转换为常量,这样就减少了变量的使用;

1
2
3
4
5
6
7
8
int main()
{
int nVar = 1;
printf("nVar = %d\n",nVar);
printf("nVar = %d\n",1); //常量nVar在编译期间计算出的变量,因此在所有引用nVar的地方都会被替换为常量
system("pause");
return 0;
}

2)减法

减法运算对应的汇编指令SUB,虽然计算机只会做加法,但是可以通过补码的形式将 减法转换为加法来计算;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
29:       /*变量定义*/
30: int nVarOne = argc;
0040F2A8 mov eax,dword ptr [ebp+8]
0040F2AB mov dword ptr [ebp-4],eax
31: int nVarTwo = 0;
0040F2AE mov dword ptr [ebp-8],0
32: /*获取变量nVarOne的数据,防止scanf变量被常量化*/
33: scanf("%d",&nVarTwo);
0040F2B5 lea ecx,[ebp-8]
0040F2B8 push ecx
0040F2B9 push offset string "%d" (004250d4)
0040F2BE call scanf (004114f0)
0040F2C3 add esp,8
34: //常量减变量的减法运算
35: nVarOne = nVarOne - 100;
0040F2C6 mov edx,dword ptr [ebp-4]
0040F2C9 sub edx,64h
0040F2CC mov dword ptr [ebp-4],edx
36: //加法与减法混合运算
37: nVarOne = nVarOne + 5 - nVarTwo;
0040F2CF mov eax,dword ptr [ebp-4]
0040F2D2 add eax,5
0040F2D5 sub eax,dword ptr [ebp-8]
0040F2D8 mov dword ptr [ebp-4],eax

3)乘法

乘法运算对应的汇编指令有imul(有符号)和mul(无符号)两种;由于乘法指令周期较长,在编译过程中,编译器首先会尝试将乘法转换为加法或位移等指令周期较短的指令;

Debug版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
48:       /*变量定义*/
49: int nVarOne = argc;
00401178 mov eax,dword ptr [ebp+8]
0040117B mov dword ptr [ebp-4],eax
50: int nVarTwo = argc;
0040117E mov ecx,dword ptr [ebp+8]
00401181 mov dword ptr [ebp-8],ecx
51:
52: printf("nVarOne * 15 = %d\n",nVarOne * 15);
00401184 mov edx,dword ptr [ebp-4]
00401187 imul edx,edx,0Fh //使用有符号乘法,imul结果存入edx中
0040118A push edx
0040118B push offset string "nVarOne * 15 = %d\n" (0042709c)
00401190 call printf (00401350)
00401195 add esp,8
53:
54:
55:
56: printf("nVarOne * 16 = %d\n",nVarOne * 16);
00401198 mov eax,dword ptr [ebp-4]
0040119B shl eax,4 //左移操作符代替乘法运算
0040119E push eax
0040119F push offset string "nVarOne * 16 = %d\n" (00427084)
004011A4 call printf (00401350)
004011A9 add esp,8
57:
58:
59: printf("2 * 2 = %d\n",2 * 2);
004011AC push 4 //常量做运算直接在编译时得到结果
004011AE push offset string "2 * 2 = %d\n" (00427074)
004011B3 call printf (00401350)
004011B8 add esp,8
60:
61:
62: printf("nVarOne * 4 + 5 = %d\n",nVarOne * 4 + 5);
004011BB mov ecx,dword ptr [ebp-4]
004011BE lea edx,[ecx*4+5] //使用lea指令完成组合运算
004011C5 push edx
004011C6 push offset string "nVarOne * 4 + 5 = %d\n" (00427058)
004011CB call printf (00401350)
004011D0 add esp,8
63:
64: printf("nVarOne * nVarTwo = %d\n",nVarOne * nVarTwo);
004011D3 mov eax,dword ptr [ebp-4]
004011D6 imul eax,dword ptr [ebp-8] //直接使用有符号乘法
004011DA push eax
004011DB push offset string "nVarOne * nVarTwo = %d\n" (0042703c)
004011E0 call printf (00401350)
004011E5 add esp,8

Release版本:

4)除法

  • 除法算数约定
    • 除法运算对应的汇编指令一般为div和idiv两种。除法指令周期较长,效率也很低,所以编译器会在编译期间用其他指令代替除法指令;
    • 向下取整
      • 根据整数的取值范围,如3.5向下取整即为3,-3.5向下取整则为-4
    • 向上取整
      • 取往正无穷大的最接近X的整数值
    • 向零取整
      • 取得往0方向最接近x的整数值

符号是由除数决定

Debug版本的除法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
.text:00401010 _main_0         proc near               ; CODE XREF: _main↑j
.text:00401010
.text:00401010 var_48 = byte ptr -48h
.text:00401010 var_8 = dword ptr -8
.text:00401010 var_4 = dword ptr -4
.text:00401010 arg_0 = dword ptr 8
.text:00401010
.text:00401010 push ebp
.text:00401011 mov ebp, esp
.text:00401013 sub esp, 48h
.text:00401016 push ebx
.text:00401017 push esi
.text:00401018 push edi
.text:00401019 lea edi, [ebp+var_48]
.text:0040101C mov ecx, 12h
.text:00401021 mov eax, 0CCCCCCCCh
.text:00401026 rep stosd
.text:00401028 mov eax, [ebp+arg_0]
.text:0040102B mov [ebp+var_4], eax
.text:0040102E mov ecx, [ebp+arg_0]
.text:00401031 mov [ebp+var_8], ecx
.text:00401034 mov eax, [ebp+var_4]
.text:00401037 cdq
.text:00401038 idiv [ebp+var_8] ; 两个变量做除法,可以直接使用除法指令idiv
.text:0040103B push eax
.text:0040103C push offset aNvaroneNvartwo ; "nVarOne / nVarTwo = %d\n"
.text:00401041 call _printf
.text:00401046 add esp, 8
.text:00401049 mov eax, [ebp+var_4]
.text:0040104C cdq
.text:0040104D sub eax, edx ; 自身减去扩展的高位
.text:0040104F sar eax, 1 ; 右移指令,2的1次幂
.text:00401051 push eax
.text:00401052 push offset aNvarone2D ; "nVarOne / 2 = %d\n"
.text:00401057 call _printf
.text:0040105C add esp, 8
.text:0040105F mov eax, [ebp+var_8]
.text:00401062 cdq
.text:00401063 mov ecx, 7
.text:00401068 idiv ecx ; 无优化,直接除以常量7
.text:0040106A push eax
.text:0040106B push offset aNvartwo7D ; "nVarTwo / 7 = %d\n"
.text:00401070 call _printf
.text:00401075 add esp, 8
.text:00401078 mov eax, [ebp+var_8]
.text:0040107B cdq
.text:0040107C mov ecx, 7
.text:00401081 idiv ecx ; 无优化,直接使用常量7
.text:00401083 push edx
.text:00401084 push offset aNvarone7D ; "nVarOne % 7 = %d\n"
.text:00401089 call _printf
.text:0040108E add esp, 8
.text:00401091 mov eax, [ebp+var_4]
.text:00401094 cdq
.text:00401095 and edx, 7 ; 扩展EAX高位到EDX,EAX中为负数,则EDX为0xFFFFFFFF
.text:00401098 add eax, edx ; 使用EAX加EDX,若EAX为负数则加7
.text:0040109A sar eax, 3 ; 右移2的3次幂
.text:0040109D push eax
.text:0040109E push offset aNvarone8D ; "nVarOne / 8 = %d\n"
.text:004010A3 call _printf
.text:004010A8 add esp, 8
.text:004010AB xor eax, eax
.text:004010AD pop edi
.text:004010AE pop esi
.text:004010AF pop ebx
.text:004010B0 add esp, 48h
.text:004010B3 cmp ebp, esp
.text:004010B5 call __chkesp
.text:004010BA mov esp, ebp
.text:004010BC pop ebp
.text:004010BD retn
.text:004010BD _main_0 endp
.text:004010BD

Release版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
.text:00401000 _main           proc near               ; CODE XREF: start+AF↓p
.text:00401000
.text:00401000 argc = dword ptr 4
.text:00401000 argv = dword ptr 8
.text:00401000 envp = dword ptr 0Ch
.text:00401000
.text:00401000 push esi
.text:00401001 mov esi, [esp+4+argc]
.text:00401005 mov eax, esi
.text:00401007 cdq
.text:00401008 idiv esi
.text:0040100A push eax
.text:0040100B push offset aNvaroneNvartwo ; "nVarOne / nVarTwo = %d\n"
.text:00401010 call sub_401080
.text:00401015 mov eax, esi
.text:00401017 cdq
.text:00401018 sub eax, edx
.text:0040101A sar eax, 1
.text:0040101C push eax
.text:0040101D push offset aNvarone2D ; "nVarOne / 2 = %d\n"
.text:00401022 call sub_401080
.text:00401027 mov eax, 92492493h
.text:0040102C imul esi
.text:0040102E add edx, esi
.text:00401030 sar edx, 2 ;右移两位
.text:00401033 mov eax, edx
.text:00401035 shr eax, 1Fh ;右移31次
.text:00401038 add edx, eax
.text:0040103A push edx
.text:0040103B push offset aNvartwo7D ; "nVarTwo / 7 = %d\n"
.text:00401040 call sub_401080
.text:00401045 mov eax, esi
.text:00401047 mov ecx, 7
.text:0040104C cdq
.text:0040104D idiv ecx
.text:0040104F push edx
.text:00401050 push offset aNvarone7D ; "nVarOne % 7 = %d\n"
.text:00401055 call sub_401080
.text:0040105A mov eax, esi
.text:0040105C cdq
.text:0040105D and edx, 7
.text:00401060 add eax, edx
.text:00401062 sar eax, 3
.text:00401065 push eax
.text:00401066 push offset aNvarone8D ; "nVarOne / 8 = %d\n"
.text:0040106B call sub_401080
.text:00401070 add esp, 28h
.text:00401073 xor eax, eax
.text:00401075 pop esi
.text:00401076 retn
.text:00401076 _main endp
.text:00401060 add eax, edx
.text:00401062 sar eax, 3
.text:00401065 push eax
.text:00401066 push offset aNvarone8D ; "nVarOne / 8 = %d\n"
.text:0040106B call sub_401080
.text:00401070 add esp, 28h
.text:00401073 xor eax, eax
.text:00401075 pop esi
.text:00401076 retn
.text:00401076 _main endp
.text:00401076

5)取模

无符号数模2的幂

还原定式:and reg,2^n - 1

1
2
3
4
5
高级代码:
printf("%d\n", argc % 8)

汇编代码:
and reg 7

无符号数模非2的幂

VC++6.0没有优化,VS2013开始release版本做了优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
r:余数
a:被除数
q:商
b:除数
r = a - qb

汇编指令:

;计算商部分
mov eax, m
mul esi
mov ecx, esi
sub ecx, edx
shr ecx, n1
add ecx, edx
shr ecx, 2

;商乘除数部分
lea eax, [ecx * 8] //argc / 7 = argc * 8 - argc
sub eax, ecx

;被除数 - 商乘除数得到余数
sub esi, eax

有符号模非2的幂

和无符号模非2的幂定式一样,区别在于计算商部分改为有符号的除法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
汇编指令:

;计算商部分
mov eax, m
imul esi
add edx, esi
sar edx, n
mov ecx, edx
shr ecx, 1Fh
add ecx, edx

;商乘除数部分
lea eax, [ecx * 8]
sub eax, ecx

;被除数 - 商乘除数得到余数
sub esi, eax

有符号模2的幂

  • 先取被除数的符号为和低3位(and), 判断最高位是否为1(正数)
  • 如果是正数,走正常流程,
  • 如果是负数,or修正数值(除最高位和低3位以外填符号位)
    • 加减是对0值的特殊处理
1
2
3
4
5
6
7
mov ebp, esp
mov eax, [ebp + arg_0]
and eax, 80000007h
jns offset
dec eax
or eax, 0fffffff8h
dinc eax

有符号模-2的幂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
数学模型:
此处A为变量,C为常量
当:C < 0时
有:A % C
可以:|A| % |C| = R
所以:当A < 0, 取 -R

定式
mov eax, argc
cdq
xor eax, edx
sub eax, edx ;等于把eax取绝对值
and argc-1 ;等于把eax取模
xor eax, edx ;等于对eax取反
sub eax, edx ;等于eax+1

解决方法:
and 之后的参数取反+1

abs(eax)

;如果eax是正数,edx = 0
;如果eax是负数,edx = 1
cdq

;eax为正数,eax xor 0 = eax
;eax为负数,eax xor ffffffff = not eax
xor eax, edx

;eax为正数,eax - 0 = eax
;eax为正数,eax - ffffffff = eax + 1
sub eax, edx

结论:
如果eax为正数,eax的值不变
如果eax为负数,eax的值取反+1

A xor 0 = A
A xor 1 = not A

如果模的除数为变量,即没有优化空间,指令为div

总结

加减乘看指令

  • 变量乘法使用imul,多操作数
  • 优化乘法使用lea和shl组合

除法

  • 除数为常量的除法必优化
  • 除数为变量的除法,idiv和div,可以据此推断出有无符号

除数为2的幂

无符号除以2的幂

使用 shr 指令

有符号除以2的幂
正除数
1
2
3
4
cdq
and edx, 2^n - 1
add eax, edx
sar eax, n
负除数
1
2
3
4
5
cdq 
and edx, 2^n - 1
add eax, edx
sar eax, n
neg eax

除数为非2的幂

原型:

1
2
设MagicNumber = 2^n / C
A / C = A * M >> n
无符号除以非2的幂
M无进位
  • 还原除数:C = 2^n / M
1
2
3
4
mov reg, A
mov eax, M
mul reg
shr edx, n
M有进位
  • 还原除数:C = 2^(n + 1) / (2^32 + M)
1
2
3
4
5
6
7
mov reg, A
mov eax, M
mul reg
sub reg, edx
shr edx, 1
add reg, edx
shr edx, n
有符号除以非2的幂
M为正数
  • 除数为正数,还原除数 C = 2^n / M
1
2
3
4
5
6
mov eax, M
imul reg/men ;被除数
sar edx, n
mov reg, edx
shr reg, 31
add edx, reg
  • 除数为负数还原除数 |C| = 2^n / neg(M)
1
2
3
4
5
6
7
mov eax, M
imul reg/men ;被除数
sub edx, reg/men
sar edx, n
mov reg, edx
shr reg, 31
add edx, reg
M为负数
  • 除数为负还原除数 |C| = 2^n / neg(M)
  • 计算器计算~M时先Dword计算16进制取反+1,得到的16进制值转Qword转十进制
1
2
3
4
5
6
mov eax, M
imul reg/men ;被除数
sar edx, n
mov reg, edx
shr reg, 31
add edx, reg
  • 除数为正数还原除数 C = 2^n / M
1
2
3
4
5
6
7
mov eax, M
imul reg/mem ;被除数
add edx, reg/men
sar edx, n
mov reg, edx
shr reg, 31
add edx, reg

取模

  • 无优化取模看div/idiv,其后使用edx,可以据此推断出有无符号

无符号取模

除数为2的幂
1
and reg, 2^n - 1
除数为非2的幂

r:余数 a:被除数 q:商 b:除数

  • 低版本编译器无优化
  • 高版本使用 r = a - qb 的数学原型求得余数

有符号取模

除数为2的幂
除数为正数
1
2
3
4
5
and reg, 80000xxxh
jns xx
dec reg
and reg, fffffyyyh
inc reg
  • xx:看xxx二进制有多少个连续的1,或者看yyy有多少个连续的0,其个数就是2^n中的指数n
除数为负数
  • 高版本同正数
  • 低版本
1
2
3
4
5
6
cdq
xor eax, edx
sub eax, edx
and eax, 2^n - 1
xor eax, edx
sub eax, edx
除数为非2的幂
  • 低版本无优化,根据除法指令还原
  • 高版本使用 r = a - qb 的数学模型求得余数

条件表达式

  • 表达式1 ? 表达式2 : 表达式3
  • 当表达式2或表达式3不为常量,无优化,直接编译为分支结构,酌情还原
  • 当表达式1为等值比较,表达式2和3为常量时,优化原型为:
1
2
3
eax = 0 ? 0 0 : -1
neg eax
sbb eax, eax
  • 当表达式1扩展为非0等值比较,表达式2和表达式3扩展为其他任意常量时:
1
2
3
4
5
6
7
A == B ? C : 0
mov reg, A
sub reg, B
neg reg
sbb reg, reg
and reg, D - C
add reg, C
  • 当表达式1扩展为区间比较,表达式2和3扩展为其他任意常量时:
1
A ? C : D
  • 低版本使用setxx 系列指令,dec以后构造出一个0或者-1的值,然后思路同上例
  • 高版本使用cmovxx 系列指令,不存在位运算技巧,直接分析cmovxx 条件还原即可

三目运算

无优化版

产生分支和if else产生的汇编代码相同
如果返回的都是变量则无优化

1
2
无优化版高级代码:
argc?100 argc*3:argc*-3

恒等优化版

取消分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
比较还原为 == 0

例:
高级代码:
eax == 100 ? 0 : -1;

汇编代码:
sub eax, 100
neg eax
sbb eax, eax


高级代码:
eax == 0 ? 0 : -1;

汇编代码:
neg eax
sbb eax, eax

neg reg ; reg = 0 - reg

sbb r1, r2
r1 = r1 - r2 - CF

立即数优化版
例:

高级代码:
eax == 100 ?39 : 93;

汇编代码:
sub eax, 100
neg eax
sbb eax, eax ;if eax == 100, eax = 0, else eax = ffffffffh
and eax, 93 - 39 ;接上面, eax = 0, else eax = 93-39
add eax, 39 ;接上面, eax = 39, else eax = 39 + 93 - 39 = 93

非恒等式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
使用set命令做比较
setg / setng

例:
高级代码:
eax > 100 ? 39 : 93;

汇编代码:
xor ecx, ecx
cmp eax, 100

方式1:
setng cl ;if eax > 100, cl = 0, else ecx = 0
dec ecx ;...接上面, ecx = ffffffffh, else ecx = 0
and ecx, 39 - 93
add ecx, 93

方式2:
setg cl
dec ecx
and ecx, 93 - 39
add ecx, 39


高级编译器新增指令:
cmovxx

解决方法: