|
当一款网游产品规模越做越大,服务器数量越来越多时,全服停机一次的成本会变成很高。一款几十万人在线的游戏全服停机维护维护一次至少一至两小时(已经算是比较快的了),一般情况下至少会减少当天约五分之一的收入。
正常的版本更新或是例行维护是不可避免的需要停机的,这个似乎没有什么比较好的解决方案。我们需要解决的是降低因为服务器程序软件的BUG导致的停机维护。
在很多情况下,临时修复一个程序的BUG并不需要改动太多代码,甚至只要对判断条件稍作一些修改,或只是把某个函数屏蔽掉就能够解决问题。请注意这里指的是“临时”的解决方案。我们并不排除某个BUG要完美的解决需要动到很多底层机制,需要涉及修改大量代码,但基本上“临时”解决方案只需要修改很少的代码就能达到可以接受的效果。这个时候就可以考虑使用内存补丁技术。
内存补丁技术主要是通过动态修改服务器的可执行代码而达到临时解决问题的效果。除去编写内存补丁有一定的难度外,建立这样的一套机制成本很低,基本不会给服务器带来额外的负面影响,是否启用完全可以取决于当时的实际状况。
对于建立这样一套机制,需要注意的一些问题:
1。被修改的代码可能正在被执行
虽然这种机率很低,但还是需要考虑一下。为了简单,内存补丁机制应该提供在服务器内部实现,在每次逻辑轮循完成后检测是否存在内存补丁,如果存在则给进程内存打上补丁。这样基本能够避免逻辑代码上的改动同时正在被执行。框架底层的代码是否使用可以稍微赌一下自己的人品,觉得自己人品不算很差的话其实也可以用,基本上问题不会太大。
2。提供的内存补丁接口可能被误操作
要求提供原有地址的代码进行检测,完全相同才进行操作,再对代码提供一个特殊算法生成的校验码进行检测,可以完全避免运维人员失误操作调用或者是错误的版本调用。
3。修改后的代码有所添加,修改容纳不了新添加的代码
正常情况下不应该添加太多代码。同时在程序中事先定义好无用的废弃函数,专们用于新添加的代码存放。放不下时直接call到废弃函数的地址。注意:一定需要使用预先定义好的废弃函数地址存放而不能简单定义一个数据块存放补丁代码。否则会被DEP强制关闭程序。虽然关闭程序的DEP保护也可以达到效果,但不建议这么做。安全性倒是其次(拿不到完整的服务器程序分析,是不可能写出有危害的缓冲区利用代码的),主要是一旦溢出后数据段能够运行,可能会导致程序崩溃的地址变得很怪异,不利于事后分析问题。
4。修改后利用的变量有所添加
同上,预先定义好一块数据块。
5。代码段的内存修改权限
使用VirtualProtect设置代码页的读写属性
6。VC编译器不要设置随机基址(/DYNAMICBASE:NO 在链接器->高级中设置)
随机基址本意就是为了让黑客的shellcode编写更为复杂,但我们不是黑客攻击,所以不需要打开这个选项给自己找麻烦。
下面的提供的代码(部分):
CMemFix::CMemFix()
{
//避免此函数被优化掉不生成,调用一下。
if (EmptyFunc() != 0)
{
memset(m_DataSeg, 0xCC, sizeof(m_DataSeg));
}
}
CMemFix::~CMemFix()
{
}
bool CMemFix::FixMem(void* pProcAddr, const char* pOldCode, const char* pNewCode, size_t nCodeLen)
{
DWORD dwOldProtece = 0;
if (::VirtualProtect(pProcAddr, nCodeLen, PAGE_EXECUTE_WRITECOPY, &dwOldProtece) == FALSE)
{
return false;
}
try
{
if (memcmp(pProcAddr, pOldCode, nCodeLen) != 0)
{
::VirtualProtect(pProcAddr, nCodeLen, dwOldProtece, &dwOldProtece);
return false;
}
memcpy(pProcAddr, pNewCode, nCodeLen);
}
catch (...)
{
try
{
memcpy(pProcAddr, pOldCode, nCodeLen);
}
catch (...)
{
}
::VirtualProtect(pProcAddr, nCodeLen, dwOldProtece, &dwOldProtece);
return false;
}
::VirtualProtect(pProcAddr, nCodeLen, dwOldProtece, &dwOldProtece);
return true;
}
//填充废弃代码(x64下无法直接嵌入汇编指令,使用正常代码替代)
int CMemFix::EmptyFunc()
{
#define ADDONE nTmp=0;nTmp=0;nTmp=0;nTmp=0;nTmp=0;nTmp=0;nTmp=0;nTmp=0;
#define LOOPADD ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE; ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE; ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE; ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE; ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;ADDONE;
volatile int nTmp = 0;
LOOPADD;
LOOPADD;
LOOPADD;
LOOPADD;
LOOPADD;
LOOPADD;
LOOPADD;
LOOPADD;
LOOPADD;
LOOPADD;
LOOPADD;
LOOPADD;
LOOPADD;
LOOPADD;
LOOPADD;
LOOPADD;
return nTmp;
}
x64下的shellcode编写可能有些困难,因为相关的资料文档比较少,也没有像OllyDbg这么直观的双击相应的汇编指令即可进行修改的软件,不过可以使用Windbg的命令在调试过程中动态修改汇编指令,只是用起来不如OD那么舒服了。如果只用VC写shellcode也可以,把相应的代码地址拖进内存窗口可以直接修改内存opcode,在VC的汇编窗口下会立即显示为修改过后的汇编指令,不过这样需要涉及到opcode的相关知识,建议去intel网站上下载最新的《Intel® 64 and IA-32 Architectures Software Developer’s Manual》,看一看第二章Instruction Format中关于opcode和指令前缀的知识。大概了解下能当手册查即可。
最后:内存补丁只是一种手段,它不能完全的解决因为程序BUG而需要停机的问题,就像反外挂一样没有任何一种技术可以完全的解决。但是它在某些特定的情况下是可以有效的解决一些问题。还有往常的一些不是很紧要但确实会带来不便,还上升不到需要停机的问题也可以用它来解决。至于什么时候使用它就看使用者各自的情况了。
附上一个小小的修改示例:这个程序中未判断除数为0的情况,将导致输入除数为0时程序会崩溃。这个时候我们使用内存补丁将它修复,当除数为0时,商为0。将内存补丁的文字复制下来在控制台的输入中粘贴进去回车后即可看到补丁是否已经成功打上。如果不想打补丁则直接输入回车或随便乱输一气使补丁打失败即可。
示例使用VC2008编译,未安装VC2008则需要安装VC2008 SP1的C RUNTIME:
内存补丁示例
VC2008 SP1 C RUNTIME(大小4M) |
|