x64汇编和逆向0x01

Reverse

x64汇编寄存器写和跳转

前置知识

x64 汇编和 x32 汇编在大体上是大差不差的,但是有些细节点有所不同,这里的不同点主要是指硬编码的不同,硬编码就是比汇编语言更靠近 cpu 的东西,可以说是很接近 cpu 可以读懂的01比特流了。

image-20220628145214427

x64汇编所对应的通用寄存器和虚拟地址空间如图所示

image-20220628145625281

x64汇编所对应的寄存器大小为64位,这64位是可以拆出一个32位寄存器来使用的,而32位寄存器可以拆出一个16位寄存器来使用,16位寄存器可以拆成一个高8位和一个低8位来使用,如上图所示:rax、eax、ax、al 这些都是可以出现在 x64 汇编中的。

在 x64 中,用户可以使用的内存空间是最上面那一段,从 00000000’00000000 到 00007ffff’ffffffff,地址虽然有64位,但是有效的长度只有48位,对于应用程序来说地址的高4位永远是0,如果是内核程序高4位则永远都是f。

增加的寄存器的位数用符号扩展的方式将用户区和内核区隔离开来。

x64寄存器的写操作

32位写操作高32位清零

只要对32位寄存器进行写操作,相应的64位寄存器的高32位清零,其中写操作也包含了运算结果自动的储存操作。

对 eax 进行 mov 操作前后的状态,rax 的高8位从非0变成了0

image-20220628202951730

image-20220628203415857

对 eax 进行运算操作后,前32位也会被清零

image-20220628210303496

image-20220628210330254

但是如果仅仅对 ax 或者 al 进行操作并不会对高位产生影响

image-20220628204151383

image-20220628204314178

对于这一点我们所要意识到的东西是:对于一些指令,尽管操作的对象不一样,但是实现的效果可能是一模一样的,因此在对程序编译操作的时候尽管你写的是64位,但是编译器依旧使用32位

image-20220628211151277

上面的两个语句进行的操作都是对 rax 进行置0,但是他们的硬编码是不一样的

立即数优先使用32位扩展

当我们想使用立即数对寄存器进行赋值时,如果所使用的立即数长度没有超过32位时会优先使用。

我对这条指令进行修改,所要修改的汇编指令是 mov rax,0x01

image-20220628205538371

由于优先扩展,这条指令会被自动变为 mov eax,0x01

image-20220628205809581

以下是立即数的使用的 3 个语句,可以体会这之间的差别

image-20220628205954423

下面就是立即数使用32位优先扩展的程序例子,所要返回的是64位的数字,由于32位立即数的指令比较短,所以编译器优先使用32位。

image-20220629094857187

image-20220629094942398

虽然在效果上面没有什么差别,但是由于硬编码的不同会导致内存地址存储和所想的有偏差

image-20220629100259129

上面的两个汇编指令是一模一样的,但是它们的硬编码不同,这个不同就是我们所要注意的,尤其是当我们在对内存地址上面做文章的时候。

上述总结: 在32位编译器可用的情况下,编译器一般都会优先使用32位寄存器来代替使用,因为32位寄存器编译后的长度相对较短,对于 CPU 的运算更友好些。

x64中的 jmp 操作

其实这个和32位和16位的jmp指令比较类似,它们也都是优先偏移地址寻址,应该只是偏移地址的大小不太相同(还未求证),在 x64 汇编中跳转范围在4个字节 -2GB~2GB 之间使用的都是偏移地址跳转。

什么是偏移地址

所谓的偏移地址就是:将某一地址作为起始地址,目的地址到这个起始地址的偏差。

举个简单的例子:

image-20220629105231199

如上图所示,很明确使用的是偏移地址跳转,在硬编码 EB 07 中我们是找不到目的地址这个数值的,而 07 的来源就是起始地址和目的地址的差值,起始地址是该指令下一条指令的地址。

偏移地址跳转

下图中绿色为偏移地址复制,红色为绝对地址赋值,这里可以好好体会一下

image-20220629111324575

因为偏移地址跳转有大小的限制,也就是正负 2GB 的限制,因此我们在使用偏移地址跳转的时候需要注意地址的有效值

EB 和 E9 跳转

image-20220629120145210

都是偏移地址大小为 4 的跳转

  • EB 的起始地址是当前指令下一条指令的地址,EB是1字节跳
  • E9 的起始地址是当前指令的地址,E9是4字节跳,跳转范围是正负2GB,在使用 E9 的时候一定要注意跳转地址的范围

E9跳转在 hook 上面使用较多。

FF25 跳转

该跳转的意思是跳转到所指向的目的地址的内容里的地址。非常绕,也就是说偏移地址所指向的目的地址并不是所要跳转的目的地址,偏移目的地址里面的内容才是。

举个例子就很明了了

image-20220629132354933

虽然这个跳转目的地址很显然是会报错的,但是没关系,懂意思就好

FF25在导入导出表上面使用较多,在跳转地址范围超出正负 2GB 的时候也可以使用该指令,只需要在偏移目的地址的8个字节内写入想要跳转的目的地址即可。

bug

在 ida 里面,这里有个小 bug,下图的汇编和上面偏移地址跳转示例相匹配

image-20220629113750207

由于是相对偏移地址寻址,而起始地址在 cs 段,因此在 ida 中对于偏移地址寻址它的默认段是 cs 段,但其实这个是不对的

image-20220629114114413

因为段地址的不同会导致硬编码的前置数字的不同,cs 段的前缀比 ds 段多了个 2E

虽然段地址有标注错误,但是对于这个错误实际上是没有什么影响的,因为在 x64汇编中已经取消了段地址保护这个概念。这里不细说,在之后的内核中会提到。

上述总结: 内存优先使用相对偏移地址寻址,直接寻址较少

常用的 hook 跳转及返回方式

跳转

最好使用寄存器进行跳转操作,这样比较安全

1
2
3
4
5
6
mov rax, 目的地址
push rax
ret

mov rax, 目的地址
jmp rax

上面的两种方式都可以,而且它们的硬编码所占的字节数大小都是一样的。

返回

1
2
3
4
sub rsp,8
mov dword ptr ss:[rsp],返回地址低4字节
mov dword ptr ss:[rsp+4],返回地址高4字节
ret

下面的这个方式使用的硬编码多占的字节数更小一点

1
2
3
push 返回地址低4字节
mov dword ptr ss:[rsp+4],返回地址高4字节
ret

为什么不连着用两个 push 是因为,虽然 push 的数字没有占满8个字节,但是在 x64 汇编中 push 命令就会给 rsp 的数值增加8个字节的大小,这样的或就会使我们的返回地址被4个字节的0隔成了两段。

小结

关注寄存器进行写操作后的高位是否受到影响。

对于跳转指令,除了关注跳转的效果以及汇编效果之外,还需要关注跳转指令的字节码长度,有时虽然汇编指令一模一样的但是在字节码长度上有区别。

编译器总是偏向于字节码较短的编译方式,短指令优先,编译结果有时会和自己所写的有偏差。

本文作者:GhDemi

本文链接: https://ghdemi.github.io/2022/07/07/x64%E6%B1%87%E7%BC%96%E5%92%8C%E9%80%86%E5%90%910x01/

文章默认使用 CC BY-NC-SA 4.0 协议进行许可,使用时请注意遵守协议。