• 1079阅读
  • 1回复

x64 windows下的inline hook [复制链接]

上一主题 下一主题
离线天道酬勤
 

只看楼主 倒序阅读 使用道具 楼主  发表于: 2016-05-20
以前做过ia32的inline hook,现在自然扩展em64t


x64中,虚拟地址变成64位,但大部分指令中的地址和立即数还是32位,执行时符号位扩展至64位
因此不能简单的放一个带偏移的near jump在函数开头,因为源地址和目标地址之间的差距很可能超过2G,这是32位有符号整数能表示的范围.
为了能够任意地跳转,可以选择两种方法
1:mov GPR64,targetaddr
jmp GPR64
2:push targetaddrlow
mov [rsp-4],targetaddrhigh
ret
第一种的指令较短,占12字节,但会修改某个通用寄存器的值,第二种只依赖栈,占14字节
由调用者保存的GPR有rax, rcx, rdx, r8-r11,第一种方法可以选择这些,我使用的是第二种方法


在目标例程开头安置转移指令后,执行目标函数时即会转移到指定的例程,当需要调用原来的例程时首先要执行被覆盖的指令,此时又有两种途径
1:用原来的指令覆盖目标例程开头,然后调用该例程
2:在另外的位置执行被覆盖的指令,然后转移到被覆盖的指令之后


显然第一种方法只能用于不会重入的例程,否则很容易出问题。加锁是不能解决问题的,因为被hook的例程可能本来允许多线程同时执行,加锁就变更了这个特性。


因此选用第二个方法。为了备份完整的指令,需要知道每个指令的长度。
在32位中,许多可hotpatch的函数开头是
mov edi,edi
push ebp
mov ebp,esp
这样的prolog,占5字节,如果安置0xe9的jmp,正好5字节,于是可以直接备份前5字节,执行完后转移到该函数的地址加5即可。但64位中安置的指令是12或14直接,函数的prolog不再能够提供完整的12或14直接指令。于是要计算每个指令的长度。
初始的想法是利用单步中断,但会受到分支指令以及指令的副作用的干扰,因此不可行。
解决方法是自己分析指令的结构,指令的编码可见intel提供的参考手册。由于编程分析每个指令的编码很麻烦,于是我从指令手册复制了一张2进制编码表(需要少量的修正),然后把内存中的数转成字符串和这个表匹配,从而可以知道指令的长度。大致的步骤是:
从给定地址的开头按字节判断是否是传统的指令前缀,有段覆盖、操作数尺寸覆盖、地址尺寸覆盖、lock、rep,直到出现其他值
然后判断下一个字节是否是REX前缀,REX前缀的W位,传统的指令前缀和操作码中的w、s位共同影响立即数的尺寸
与编码表进行比对


现在可以备份完整的指令了,但是备份的指令中可能有相对寻址。
如果相对寻址的目标地址在备份的指令当中,则不需修正,否则需要做出修正。
x64中相对寻址的指令有带直接的(偏移在指令中)short jmp,near jmp,near call,short jcc,near jcc,jcxz和REX与ModR\M指定的rip相对寻址。如果指令的displacement是4字节且备份的指令处和指令引用地址的距离小于2G,这样可以直接修正指令中的displacement(位移),新位移=原位移-原指令地址+新指令地址。如果displacement是1字节,这样的有short jmp,short jcc,和jcxz,则需要变换成near jmp或near jcc或near jmp和jcxz的组合,这样会导致指令长度的改变,这样会影响到后续指令的位置,需要予以考虑。x64中没有2字节偏移的分支指令。


如果原指令引用地址与备份的指令的地址相差超过2G,则需要进行更大的变化。
对于direct short jmp和direct near jmp,可以变换成
push targetaddrlow
mov [rsp-4],targetaddrhigh
ret
对于条件分支指令,可以变换成上述指令与条件分支指令的组合
对于direct near call指令,可以变换成
jmp rip+15
nop
nop
...
label a:(要保证a的地址是8的倍数,因为x64可以产生未对齐异常)
target address
...
nop
call [rip-b+a]
label b:
对于内存操作数的rip相对寻址,我目前没有找到比较好的修正方法
本来想变换成
mov rax,[moffset]
原指令操作数改为rax
mov [moffset],rax(如果指令会写内存)
x64中只有操作码为A0,A1,A2,A3的mov指令能够接受8字节的段偏移,另一个操作数是累加寄存器
但是intel提供的表中不易判断指令是否会写内存,因此不易判断是否应该加mov [moffset],rax
除此外,这样做会修改寄存器的值,可以用push备份和pop还原,但不适用于间接分支指令.


我目前使用的方法是寻找一块合适地址的内存,然后在这上面放置备份的指令。大致过程如下:
计算出原指令中rip相对寻址所的目标地址的最小值LowRef和最大值HighRef
计算LowBound=HighRef-2G,HighBound=LowRef+2G
则区间[LowBound,HighBound]中的指令到原指令所有rip相对寻址的操作数的距离小于2G
如果LowBound>=HighBound则无法继续
用NtQueryVirtualMemory寻找一块被[LowBound,HighBound]包含的空闲区域,然后用NtAllocateVirtualMemory在其中分配一块内存,交给RtlCreateHeap创建一个堆,并记录下堆的起始地址和堆的最大长度
然后即可用RtlAllocateHeap在堆上分配内存,安置备份的指令并进行修正


今后再需备份指令时,先从已创建的堆寻找合适的,没有时再创建新的堆


这个仅作为学习资料,在正式场合请使用Detours(x64要钱)或免费的N-CodeHook
离线寻幽老鬼

只看该作者 沙发  发表于: 2016-05-20
   ,虽然我看不懂,但是我还是要赞一个
快速回复
限100 字节
如果您提交过一次失败了,可以用”恢复数据”来恢复帖子内容
 
上一个 下一个