2008年9月3日星期三

inline hook中替换前5字节的通用方式

inline hook指替换目标api的指令序列改变执行流程使之运行我们hook程序的一种方法。一般都是替换目标api的入口为一个跳转指令跳到我们的代码执行,我的代码使用的也是这种方法,替换入口指令的最大好处是可以在原始api运行前和运行后都有机会修改参数和返回值。如果替换其他位置的指令则只能在原始api运行前或者后获得一次截获机会, 并且hook函数不得不使用汇编来写(至少需要用嵌入汇编写函数的入口和出口,为了防止c/c++编译器生成的代码破坏堆栈)。 这段代码的最大特点是支持多重hook。

我用了一个结构保存hook的信息,为了能够用于恢复和查找, 具体代码如下:

#include "X86IL.h " // 这个头文件是用于计算x86指令长度的,可以从http://www.defendion.com/EliCZ/export.htm下载

// 代码用到的在ntddk中的类型定义
#define void *PVOID
#define unsigned long ULONG
#define unsigned char UCHAR
#define UCHAR *PUCHAR

// 跳转指令长度
#define JMP_INS_SIZE 5

typedef struct HookInfo
{
HookInfo* next; // 指向下一个hook, 这样所有的hook可以构成一个链表便于查找
PVOID newEntry; // 我们hook代码的入口
PVOID replaceAddress; // 被替换的原始api入口地址
ULONG callingCount; // 使用计数,当前有多少正在使用这个api者,
// 供unhook时使用,如果该值不为0 则不能unhook(尤其unhook内核api时)
UCHAR entryCodes[24]; // 新api入口,由程序生成, 长度定位24是为了对齐考虑,实际只使用了18字节
// 它的具体内容是下面这段汇编代码:
// INC callingCount
// CALL newEntry
// DEC callingCount
// RET
UCHAR oldEntry[32]; // “原始”api入口地址, 用于在newEntry中调用原始api,
// 其内容是保存的原始api入口处指令+一个跳转指令跳回原始api继续执行
} HOOKINFO, *PHOOKINFO;

// 这个函数的作用是从source复制至少size字节的完整指令到dest,返回已复制的字节数, 0表示失败

UCHAR MoveInstructions(PVOID source, PVOID dest, UCHAR size)
{
PUCHAR s = (PUCHAR)source, d = (PUCHAR)dest;
int cnt = 0;
int l, pl, ml, il;
while( cnt < (int)size )
{
l = X86IL(0, s, d, &pl, &ml, &il); // 计算指令长度,具体参数意义参看x86il.h
switch(d[pl]) // 指令字节
{
// 如果是短条件跳转
case 0x70:
case 0x71:
case 0x72:
case 0x73:
case 0x74:
case 0x75:
case 0x76:
case 0x77:
case 0x78:
case 0x79:
case 0x7a:
case 0x7b:
case 0x7c:
case 0x7d:
case 0x7e:
case 0x7f:
return 0; // 返回失败
// 如果是loopXXX 和 jcxz 指令
case 0xe0:
case 0xe1:
case 0xe2:
case 0xe3:
return 0; // 返回失败
// 如果是call和jmp指令
case 0xe8:
case 0xe9:
{
if( il < 4 ) // 如果操作数小于32位则失败
return 0;
int* pi = (int*)&d[pl+1];
*pi += (int)s - (int)d; // 修改跳转到的相对地址
}
break;
// 短 jmp 指令
case 0xeb:
return 0; // 失败
case 0x0f:
{
if( d[pl+1] > = 0x80 && d[pl+1] <= 0x8f ) // 远距离条件跳转, 指令 0F80 ~ 0F8F
{
if( il < 4 ) // 操作数小于32位
return 0; // 失败
int* pi = (int*)&d[pl+2];
*pi += (int)s - (int)d; // 修改要跳转到的相对地址

}
}
break;
// 如果是ret, retn, retf,iret指令
case 0xc2:
case 0xc3:
case 0xca:
case 0xcb:
case 0xcf:
if( cnt + l < (int)size ) // 如果不够容纳替换字节
return 0; // 失败
break;
case 0x9a:
case 0xea:
case 0xff:
// 使用寄存器和绝对地址的jmp和call指令比较讨厌,不过如果不是故意编码造成的,一般都可以被安全替换
}
d += l; // 成功, 继续计算下一条指令
s += l;
cnt += l;
}
return (UCHAR)cnt;
}


// oldEntry是要替换的原始api入口,newEntry是我们的程序,
// 成功的话返回可以在newEntry中调用的原始api入口,并且填写info内容。不成功的话返回NULL

PVOID InlineHookApi(PHOOKINFO info, PVOID oldEntry, PVOID newEntry)
{
UCHAR n = MoveInstructions( oldEntry, info-> oldEntry, JMP_INS_SIZE ); // 复制并保存要替换的内容
if(!n ) return NULL; // 失败

// 生成跳回原始api的跳转指令
info-> oldEntry[n] = 0xe9;
*((int*)&info-> oldEntry[n+1]) = (int)oldEntry - (int)info-> oldEntry - JMP_INS_SIZE;

// 生成新api入口
PUCHAR dest = info-> entryCodes;

// 生成汇编语句: INC info-> callingCount
*dest++ = 0x0f;
*dest++ = 0x05;
*((int*)dest) = (int)&info-> callingCount;
dest += sizeof(int);

// 生成汇编指令 call info-> newEntry
*dest = 0xe8;
*((int*)&dest[1]) = (int)newEntry - (int)dest - JMP_INS_SIZE;
dest += JMP_INS_SIZE;

// 生成汇编指令 DEC info-> callingCount
*dest++ = 0x0f;
*dest++ = 0x0d;
*((int*)dest) = (int)&info-> callingCount;
dest += sizeof(int);

// 生成汇编指令 RET
*dest = 0xc3;

// 替换原始api入口为 JMP info-> entryCodes
dest = (PUCHAR)oldEntry;

*dest ++ = 0xe9;
*((int*)dest) = (int)info-> oldEntry - (int)oldEntry - JMP_INS_SIZE;

// 填写剩余的info内容

info-> callingCount = 0;
info-> replaceAddress = oldEntry;
info-> newEntry = newEntry;

return info-> oldEntry;
}

代码还有很多可以完善的地方,比如如何支持短条件跳转指令, 比如如何判断后续代码中是否有引用被替换地址内容的语句以及如何避免等等。

如果大家觉得不错,下一篇我会给一个使用上述代码的完整的内核级api hook框架,包括如何解决多CPU环境下hook api时的共享问题。

没有评论: