Windows逆向安全(一)之基础知识(七)

汇编C语言类型转换

类型转换

类型转换的使用场景

类型转换一般为由数据宽度小的转换成数据宽度大的,不然可能会有高位数据被截断的现象,引起数据丢失

需要一个变量来存储一个数据,刚开始这个数据的数据宽度较小,后来发现存不下了,需要换一个数据宽度更大的变量来存储

类型转换相关汇编指令

MOVSX

先符号扩展,再传送

MOV AL,0FF
MOVSX CX,AL
MOV AL,80
MOVSX CX,AL

MOVZX

先零扩展,再传送

MOV AL,0FF
MOVZX CX,AL
MOV AL,80
MOVSX CX,AL

类型转换例子

#include "stdafx.h"
int main(int argc, char* argv[])
{
        unsigned char i=0xFF;
        printf("%d\n",i);
        int j=i+1;
        i=i+1;
        printf("%d\n",i);
        printf("%d\n",j);
        return 0;
}

我们来看看以上代码的运行结果:

在这里插入图片描述
在这里插入图片描述

分析结果

首先输出的是一个无符号数 i,0xFF对应的十进制为255

接着输出的是i自增1后的结果,我们发现255+1变成了0,这是因为char的数据宽度为8位,最大便是0xFF了,再加上一就超出了char的数据宽度,也就是发生了上溢。于是数据变成了0

最后输出的是类型转换后i+1的结果,正确地显示为256

汇编观察

汇编代码

大致了解了产生上述结果的原因,用汇编来更透彻地分析:

在这里插入图片描述
我们这里提取出 去除printf输出的部分,得到汇编代码如下:

8:        unsigned char i=0xFF;
0040D708   mov         byte ptr [ebp-4],0FFh
10:       int j=i+1;
0040D722   mov         ecx,dword ptr [ebp-4]
0040D725   and         ecx,0FFh
0040D72B   add         ecx,1
0040D72E   mov         dword ptr [ebp-8],ecx
11:       i=i+1;
0040D731   mov         edx,dword ptr [ebp-4]
0040D734   and         edx,0FFh
0040D73A   add         edx,1
0040D73D   mov         byte ptr [ebp-4],dl

结果有些尴尬,ԾㅂԾ, 我们发现并没有用到前面所说的movsx或movzx指令,但是先不着急,先看看这段汇编代码做了些什么

对应i赋值

很稀松平常的,char对应数据宽度为byte赋值

8:        unsigned char i=0xFF;
0040D708   mov         byte ptr [ebp-4],0FFh

对应j赋值

接下来就是j的赋值

10:       int j=i+1;
0040D722   mov         ecx,dword ptr [ebp-4]
0040D725   and         ecx,0FFh
0040D72B   add         ecx,1
0040D72E   mov         dword ptr [ebp-8],ecx

可以看到:首先是直接将前面的 i 赋值给ecx,并且赋值的长度为dword,很明显将超出char长度的内容也赋值到了ecx

mov         ecx,dword ptr [ebp-4]

然后下一句很关键

and ecx,0FFh

与操作,将之前多超出的部分和0相与,也就是将超出的部分全部清零(用零填充),相当于MOVZX指令的零填充

接下来就是加一的操作

add ecx,1

最后就是将ecx赋值给了我们的变量j

mov         dword ptr [ebp-8],ecx

对应i=i+1

和前面j的赋值似曾相识,直接将前面i赋值给edx,并且赋值的长度为dword,很明显将超出char长度的内容也赋值到了edx

mov         edx,dword ptr [ebp-4]

接着也是与操作,超出来的部分清零

and         edx,0FFh

接下来就是加一的操作

add         edx,1

最后是赋值

mov         byte ptr [ebp-4],dl

将edx的低8位赋值给i

通过前面的分析,可以发现,无论是 i 自己+1还是用数据宽度较高的 j 来接收 i +1的结果

期间都是要先取出超出 i 数据宽度的dword长度的数据,然后再使用and 0xFF,把超出的部分清零

换言之,char的计算也会先转换为int的计算,最后再转回来

还有就是汇编指令并非一成不变,不是一定要使用movsx或movzx指令,也可以通过这种取出超出长度的数据,然后再将超出的部分清零的操作来实现类movzx指令的结果

自写汇编实现功能

前面虽然我们分析了,汇编代码,但很可惜编译器并没有使用movzx指令来实现操作,本着学习巩固的精神,我们自己写汇编来实现上述的功能,以此来加深对movzx的理解

#include "stdafx.h"
unsigned char i=0xFF;
int j=0;
void _declspec (naked) func(){
        _asm{
                                //保留调用前堆栈
                push ebp
                //提升堆栈
                mov ebp,esp
                sub esp,0x40
                //保护现场
                push ebx
                push esi
                push edi
                //初始化提升的堆栈,填充缓冲区
                mov eax,0xCCCCCCCC
                mov ecx,0x10
                lea edi,dword ptr ds:[ebp-0x40]
                rep stosd
                //函数核心功能

                                //将i零扩充赋值给ecx
                                movzx ecx,i
                                //ecx自增1
                                inc ecx
                                //将ecx赋值给j
                                mov j,ecx
                                //直接让i自增1
                                inc i

                //恢复现场
                pop edi
                pop esi
                pop ebx
                //降低堆栈
                mov esp,ebp
                pop ebp                
                //返回
                ret 
        }
}

int main(int argc, char* argv[])
{
        printf("%d\n",i);
        func();
        printf("%d\n",i);
        printf("%d\n",j);
        return 0;
}

执行后的结果为:

在这里插入图片描述
和前面的执行结果一致

上面的代码看似很多,其实核心功能就只有四句:

//函数核心功能
//将i零扩充赋值给ecx
movzx ecx,i
//ecx自增1
inc ecx
//将ecx赋值给j
mov j,ecx
//直接让i自增1
inc i

我们这里使用了movzx指令,实现了相同的功能,也顺便引入了一个新的汇编语句:

inc eax 相当于 add eax,1     也就是eax=eax+1

优点是速度比add指令快,占用空间小

与之相对的便是:dec指令,自减1

关于movzx和movsx的区别

案例一

#include "stdafx.h"
void function(){
    char i=0xFF;
    int j=0;
    _asm{
        movsx eax,i
        mov j,eax
    }
    printf("%d\t%x\n",j,j);
    _asm{
        movzx eax,i
        mov j,eax
    }
    printf("%d\t%x\n",j,j);
}
int main(int argc, char* argv[])
{
    function();
    return 0;
}

运行结果

在这里插入图片描述

分析

void function(){
    char i=0xFF;
    int j=0;
    _asm{
        movsx eax,i
        mov j,eax
    }
    printf("%d\t%x\n",j,j);
    _asm{
        movzx eax,i
        mov j,eax
    }
    printf("%d\t%x\n",j,j);
}

首先声明一个char类型的变量i,默认为有符号数,然后再声明一个变量j初始化

char i=0xFF;
int j=0;

然后就是先将i带符号扩展到eax,接着再用eax赋值给j

_asm{
        movsx eax,i
        mov j,eax
    }

带符号扩展解释:

首先将 i :0xFF转化为二进制 : 1111 1111

i是一个有符号数,最高位为符号位即1

符号扩展就是将要扩展的高位全部用符号位进行填充,这里就是将eax的高24位用1填充(低8位直接为i赋值),得到的结果转换为十六进制就是ffffffff,8个f,对应输出的结果

再看下面:

_asm{
        movzx eax,i
        mov j,eax
    }

和前面类似,只不过是将movsx改为movzx

无符号扩展解释:

直接将eax的高24位用0填充(低8位直接为i赋值),得到的结果为000000ff,2个f,对应输出的结果

案例二

将上面的ff改为8b即可

void function(){
    char i=0x8b;
    int j=0;
    _asm{
        movsx eax,i
        mov j,eax
    }
    printf("%d\t%x\n",j,j);
    _asm{
        movzx eax,i
        mov j,eax
    }
    printf("%d\t%x\n",j,j);
}

运行结果

在这里插入图片描述

分析

有了前面分析的经验,直接来看f8如何有符号扩展

先将8b转化为二进制数:

8b:1000 1011

这里的符号位也就是最高位为1,于是将eax的高24位用1(符号位)填充,低8位直接为8b,得到的结果转化为十六进制:ffffff8b

与之类似,无符号扩展则是将eax的高24位用0填充,低8位依旧为8b,得到的结果转化为十六进制:0000008b

得到的结果运行结果一致

简单地来说,有符号扩展:movsx,就是将被扩展数的最高位(符号位),对应SF标志位的内容(修改SF标志位可以改变符号扩展的结果),填充到扩展的高位部分

而无符号扩展则直接用0填充扩展的高位部分

posted @ 2023-04-19 06:25  私ははいしゃ敗者です  阅读(11)  评论(0编辑  收藏  举报  来源