好友
阅读权限 20
听众
最后登录 1970-1-1
本帖最后由 longs75 于 2021-10-29 09:51 编辑
上次发了个贴:《32位、64位汇编语言混合编程终极大法》
之后内心隐隐不安:刚刚学到一点儿知识,居然口出狂言声称“终极大法”,万一被高手过来打脸怎么办?毕竟自己也是一瓶不满半瓶晃荡的水平。
幸好,高手没来,大概是嗤之以鼻,不屑一顾,也可能忙着泡妞没时间给我上课,希望是后一种情况,不至于让我灰头土脸……总之,不能再说大话了。
言归正传,高手不来,我自己打脸。
然而我这个帖子重点并不是在32位、64位模式之间进行切换,毕竟我已经实现了,虽然算不上“终极”,但也相当好用。重点要探讨由此引出的对64位模式下operation size的认识。
以下代码用X96dbg(x64dbg+x32dbg)进行调试。
上个帖子实现32位、64位模式切换,实际上是使用了JMP远跳转指令。但是在转移指令中,具备远跳转的不只是 JMP,还有CALL/RET指令组,下面以RET为例进行探讨。
RET指令从堆栈中取出一个值给RIP(EIP),实现程序流程跳转,RET指令同样有NEAR、FAR,功能与JMP基本相同,但RET能自动平衡堆栈指针,要说终极,当然是RET。
下面用RET指令改写32位、64位模式之间进行切换的代码:
1、由32位模式进入64位模式:
00401000 | 6A 33 | push 0x33 | 1、把立即数00000033入栈,准备切换64位模式
00401002 | E8 02000000 | call 401009 | 2、把下一条指令的地址入栈,并跳到ret far执行远返回
00401007 | EB 01 | jmp 40100A | 4、远返回到这里啦!这条指令已经在64位模式下!现在跳出这个圈子
00401009 | CB | ret far | 3、远返回到: 0x33:00401007,进入64位模式!
0040100A | 90 | nop | 5、好了,跳到这里,开始自己的64位编程吧
上面这个代码段用ret far 指令实现了由32位模式进入64位模式,而且堆栈是平衡的,不用额外调整,比jmp far舒服多了,就是进入时的流程有点儿怪怪的,这是因为利用了call指令获取当前EIP,避免绝对寻址,如果写成下面这样:
00401040 | 6A 33 | push 0x33 |
00401042 | 68 48104000 | push 401048 | 绝对地址入栈?代码写成这样太失败了
00401047 | CB | ret far |
00401048 | 90 | nop |
虽然也能进入64位模式,但是68 48104000 | push 401048已经把绝对地址嵌入了代码,换个地址就不能用,相当于一次性使用,玩汇编的人一般都接受不了这种情况。
好了,到目前为止,利用ret far指令由32位模式进入64位模式很顺利。下面要实现由64位模式进入32位模式。
64位模式下,所有操作应当都是64位的吧?(是这样吗?打个问号吧)
查Intel 指令手册,ret指令的操作真是太复杂了,总共有10页!!大多数都是出错信息,鬼才看得懂,我是没耐心、也没能力完整的看,只能找重点。
……
ELSE (* OperandSize = 64 *) 如果操作数尺寸=64;
RIP := Pop(); 弹出RIP;
CS := Pop(); (* 64-bit pop; high-order 48 bits discarded; seg. descriptor loaded *) 弹出CS(弹出64位,高48位丢弃)
……
指令手册上说,如果操作数是64位,则先弹出RIP(64位),再弹出CS(弹出64位,高48位丢弃,CS=低16位)
好象明白了,64位操作系统不就是64位操作数嘛(是这样吗?再打个问号吧),仿照上面进入64位模式的代码,写一下退出64位模式的代码:
000000013FAF1030 | 6A 23 | push 0x23 | 1、64位立即数0000000000000023入栈
000000013FAF1032 | E8 02000000 | call 13FAF1039 | 2、把下一条指令的地址(64位)入栈,并跳到ret far执行远返回
000000013FAF1037 | EB 01 | jmp 13FAF103A | 真的能返回到这里吗?别做梦了!
000000013FAF1039 | CB | ret far | 3、希望能远返回到: 0x23:000000013FAF1037,进入32位模式!——结果坐飞机了,弹出异常
000000013FAF103A | 90 | nop | 这是一段失败的代码,跑飞了
问题出在哪里?上面打问号的地方是重点!!!
再查Intel指令手册,从OperandSize(操作数尺寸)入手,找出问题所在。手册版本是:Order Number: 325462-075US June 2021
手册第2部分第2章有这样一节:
2.2.1.7 Default 64-Bit Operand Size 默认的64位操作数尺寸
In 64-bit mode, two groups of instructions have a default operand size of 64 bits (do not need a REX prefix for this
operand size). These are: 在64位模式中,两组指令的操作数尺寸默认是64位(不需要用REX前缀表明是64位操作数尺寸),它们是:
. Near branches. 近转移
. All instructions, except far branches, that implicitly reference the RSP. 所有隐含涉及RSP的指令(远转移除外)。
根据手册描述,ret far 远返回指令,默认的Operand Size不是64位!!!
再查Intel指令手册第2部分第4章第4.3节关于ret指令的描述中,有这么一段:
In 64-bit mode, the default operation size of this instruction is the stack-address size, i.e. 64 bits. This applies to
near returns, not far returns; the default operation size of far returns is 32 bits.
翻译一下:在64位模式中,这条指令默认的操作数尺寸是堆栈的尺寸,即:64位,这适用于近返回,不是远返回;远返回默认的操作数尺寸是32位!
这下全明白了!!
在64位模式中,近转移(jmp\call\ret 的near方式)默认操作数尺寸是64位,远转移(jmp\call\ret 的far方式)默认操作数尺寸是32位!
再看上面那段有问题的代码,PUSH了两个64位数,然后用默认的ret far远返回,而ret far 默认的是32位的操作数,CS寄存器都乱套了,所以直接跑飞。
重写退出64位的代码:
2、由64位模式进入32位模式:
000000013FAF1040 | E8 00000000 | call 13FAF1045 | 把下一条指令的地址(64位)入栈,并跳到下一条指令继续执行
000000013FAF1045 | 830424 0E | add dword ptr ss:[rsp],E | [rsp]中是这条指令的地址,dword ptr[rsp]是rip的低32位(即eip),计算一下出口地址
000000013FAF1049 | 66:C74424 04 2300 | mov word ptr ss:[rsp+4],23 | 高32位是00000023,给CS,用来返回32位模式
000000013FAF1050 | CB | ret far | 64位模式下的远返回,默认操作数尺寸是32位!所以上面两个操作数都是32位的
000000013FAF1051 | 90 | nop | 这就是正确的32位模式返回地址,这里已经是32位模式了
=========我是分割线,问题解决了,但思考还在继续===========
前面提到64位模式下的REX前缀,它是一个字节,指令手册上对它有详细描述,这个字节的高4位(即第4—7位)固定是0100,即16进制数字“4”,第3位表示operation size, 1:64位,0:其它 。第0位、第2位与寄存器和寻址方式有关
再进一步总结一下:REX前缀用于64位模式,一个主要功能是指示operation size。它是一个字节,范围是40—4F:40—47表示指令是32位operation size,48—4F表示指令是64位operation size。看看下面这些例子:
000000013FAF105B | 48:8B03 | mov rax,qword ptr ds:[rbx] | REX=48,64位operation size
000000013FAF105E | 4D:89D0 | mov r8,r10 | REX=4D,64位operation size
000000013FAF1061 | 44:89D8 | mov eax,r11d | REX=44,32位operation size
000000013FAF1064 | 41:8936 | mov dword ptr ds:[r14],esi | REX=41,32位operation size
上面提到过,64位模式中,只有两种情况下默认的operation size是64位,一种情况是转移指令,一种情况是与堆栈有关的指令。换句话说,其它指令默认的operation size都是32位,再看下面的例子:
000000013FAF1070 | 8B03 | mov eax,dword ptr ds:[rbx] |
000000013FAF1072 | 8932 | mov dword ptr ds:[rdx],esi |
000000013FAF1074 | 89C8 | mov eax,ecx |
可以看到,前两个指令虽然用到了64位寄存器,但执行的是32位操作,指令operation size都是32位,因此不需要REX前缀。当然,加了REX前缀也无所谓,比如:
000000013FAF101C | 40:89D8 | mov eax,ebx |
000000013FAF101F | 89D8 | mov eax,ebx |
铺垫了这么多内容,再次回到32位、64位模式切换话题,现在的问题是:ret far默认的operation size是32位,给指令加个REX前缀,强制operation size为64位,可以吗?
首先告诉大家,不要翻手册了,手册上没有明确答案,手册给了我们原理,弄明白了原理,下面就是自由发挥的空间:
在ret far 指令前加个REX前缀(48—4F都可以), 强制operation size为64位,重写刚才那个错误的由64位模式进入32位模式的代码:
000000013FAF1030 | 6A 23 | push 23 | 1、64位立即数0000000000000023入栈
000000013FAF1032 | E8 02000000 | call empty64.13FAF1039 | 2、把下一条指令的地址(64位)入栈,并跳到ret far执行远返回
000000013FAF1037 | EB 02 | jmp empty64.13FAF103B | 4、成功了!指令返回到这里,进入32位模式,现在跳出圈吧
000000013FAF1039 | 4B:CB | ret far | 3、希望能远返回到: 0x23:000000013FAF1037,进入32位模式!
000000013FAF103B | 90 | nop | 5、好了,跳到这里,继续自己的32位编程
需要注意的是,在调试软件X96dbg(x64dbg+x32dbg)中,没办法输入 4B:CB 对应的汇编语句,X96dbg(x64dbg+x32dbg)使用了两个独立的汇编模块:XEDParse和asmjit都不支持带REX前缀的 ret far,我在github上找的Keystone甚至连远转移指令都不支持,
所以只能以编辑内存数据的方式,手工填入4B CB。
===========这就是底线,另外再补充两句===========
虽然是以32位、64位模式切换为例,但重点探讨了64位模式下的operation size问题,理解了operation size的规则,以及两种特殊情况运用,对64位编程就会有更深刻的理解。上面只是以ret指令为例进行了详细探讨,jmp、call指令与ret也是同样问题,这里就不再详细讲了。
一点儿学习心得与大家共享,有不对的地方,欢迎批评指正。
免费评分
查看全部评分
发帖前要善用【论坛搜索 】 功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。