敏捷开发,非常强调效率。如果只顾着效率,代码质量必须很低,未来的维护成本必定大增,会得不偿失。
那么敏捷开发中,CodeReview和单元测试是保证代码质量的重要手段。
1. 问题
如下代码,通过宏开关来控制桩函数,虽然功能上能够达到打桩的效果,但是对待测代码增加了大量的宏,影响代码的可读性。
int Fun1()
{
return 12;
}
int Fun()
{
#ifdef STUB_ENABLED
return 2 + Fun1_Stub();
#else
return 2 + Fun1();
#endif
}
2. 方法
面对上述问题,有没有尽量不修改源文件的方法来达到打桩替换函数的效果呢?下面介绍两种效果不错的,用于Windows平台上C/C++打桩的方法。
2.1. 宏
利用宏的不定参加上##连接符,可以完成打桩的效果。主要原理是调用函数和定义函数参数不同来达到宏展开结果的不同。
#define A1(...) FUN1(A1, __VA_ARGS__)(__VA_ARGS__)
#define ARG_int __Dummy,A1,
#define FUN1(arg1, ...) FUN2(arg1, arg1##Stub, __VA_ARGS__)
#define FUN2(arg1, arg2, arg3,...) FUN3(arg2, ARG_##arg3)
#define FUN3(arg1, ...) FUN4(FUN5(__VA_ARGS__, arg1))
#define FUN4(...) __VA_ARGS__ // 此句很关键,此句会让上面的(__VA_ARGS__, arg1)先展开成普通字符串再展开FUN5
#define FUN5(x, y,...) y
int A1Stub(int a)
{
return 11;
}
int A1(int a)
{
return 20;
}
void TestMacro()
{
A1(4);
}
可以通过编译器的预编译命令来查看宏展开的情况。
VS:cl -E xxx.cpp
g++: g++ -E xxx.cpp
2.2. 注入跳转代码
- 待测文件
#include <stdio.h>
#include "Fun.h"
int AFunc(void)
{
return 55 + BFunc();
}
int BFunc(void)
{
return 200;
}
- 测试代码
#define FLATJMPCODE_LENGTH 5 //x86 平坦内存模式下,绝对跳转指令长度
#define FLATJMPCMD_LENGTH 1 //机械码0xe9长度
#define FLATJMPCMD 0xe9 //相应汇编的jmp指令
// 记录被打桩函数的内容。以便恢复
BYTE g_apiBackup[FLATJMPCODE_LENGTH+FLATJMPCMD_LENGTH];
BOOL SetStub(LPVOID pFun, LPVOID hookFun)
{
DWORD oldProtect =0;
DWORD TempProtectVar = 0;
char newCode[6] = {0}; //用于读取函数原有内存信息
HANDLE hProgress = GetCurrentProcess(); //获取进程伪句柄
int SIZE = FLATJMPCODE_LENGTH + FLATJMPCMD_LENGTH; //须要改动的内存大小
if (!VirtualProtect(pFun, SIZE, PAGE_EXECUTE_READWRITE, &oldProtect)) //改动内存为可读写
{
return FALSE;
}
if (!ReadProcessMemory(hProgress, pFun, newCode, SIZE, NULL)) //读取内存
{
return FALSE;
}
memcpy((void*)g_apiBackup, (const void*)newCode, sizeof(g_apiBackup)); //保存被打桩函数信息
*(BYTE*)pFun = FLATJMPCMD;
*(DWORD*)((BYTE*)pFun + FLATJMPCMD_LENGTH) = (DWORD)hookFun - (DWORD)pFun - FLATJMPCODE_LENGTH; //桩函数注入
VirtualProtect(pFun, SIZE, oldProtect, &TempProtectVar); //恢复保护属性
return TRUE;
}
BOOL ClearStub(LPVOID pFun)
{
BOOL IsSuccess = FALSE;
DWORD TempProtectVar; //暂时保护属性变量
MEMORY_BASIC_INFORMATION MemInfo; //内存分页属性信息
VirtualQuery(pFun,&MemInfo,sizeof(MEMORY_BASIC_INFORMATION));
if(VirtualProtect(MemInfo.BaseAddress,MemInfo.RegionSize, PAGE_EXECUTE_READWRITE,&MemInfo.Protect)) //改动页面为可写
{
memcpy((void*)pFun, (const void*)g_apiBackup, sizeof(g_apiBackup)); //恢复代码段
VirtualProtect(MemInfo.BaseAddress,MemInfo.RegionSize, MemInfo.Protect,&TempProtectVar); //改回原属性
IsSuccess = TRUE;
}
return IsSuccess;
}
int BFuncStub(void)
{
return 300;
}
int main(int argc, char* argv[])
{
printf("The value:%d\r\n", AFunc());
SetStub(BFunc, BFuncStub);
printf("The value:%d\r\n", AFunc());
ClearStub(BFunc);
printf("The value:%d\r\n", AFunc());
return 0;
}
- 关键点
0021100F E9 0C 04 00 00 jmp BFunc (211420h)
E9是跳转jmp指令0C0400是BFunc的相对地址。
那么如果想跳转到BFuncStub, 就需要将跳转地址之后的相对地址换成BFuncStub的即可。如:
(DWORD)((BYTE*)pFun + FLATJMPCMD_LENGTH) = (DWORD)hookFun - (DWORD)pFun - FLATJMPCODE_LENGTH; //桩函数注入
因为整个跳转指令占5个字节,所以需要减去FLATJMPCODE_LENGTH。
-
修改跳转地址之前:
-
修改跳转地址内存之后
-