|
提起“变速齿轮”(以下简称“齿轮”)这个软件,大家应该都知道吧,该软件号称 是全球第一款能改变游戏速度的程序。我起初用时觉得很神奇,久而久之就不禁思考其实现原理了,但苦于个人水平有限,始终不得其解,成了长驻于脑中挥散不去的大问号。 偶然一天在BBS上看到了一篇名为《“变速齿轮”研究手记》(以下简称《手记》)的文章,我如获至宝,耐着性子把文章看完了,但之后还是有很多地方不解,不过还是有了比较模糊的认识:原来齿轮是通过截获游戏程序对时间相关函数的调用并修改返回结果实现的呀。 为了彻彻底底地弄清齿轮的原理,我这次打算豁出去了。考虑到《手记》的作者从是研究的“齿轮”的反汇编代码的,那我也照样从反汇编代码开始。不过自认为汇编功底不够,又从图书馆借了几本关于Windows底层机制和386汇编的书,在经过差不多两周的“修行”之后,自我感觉有点好啦,哈哈,我也有点要迫不及待地把“齿轮”大卸八块了! 在动手之前,我又把《手记》看了一遍,这次可就清楚多了:通过调用门跳到Ring0级代码段,修改各系统时间相关函数的前8个字节为jmp指令,转跳到“齿轮”映射到2G之上的代码,达到截获对各系统时间相关函数的调用的目的。但同时我的疑惑也更明确了: 1.“齿轮”怎样建立指向自己映射到2G以上内存的代码的调用门描述符的; 2.“齿轮”怎样将自己的代码映射到2G以上线性地址的; 3.映射到2G之上的代码是怎样做到在代码基址更改的情况仍能正确运行的 带着这样的疑问,我正式开始了对“齿轮”反汇编代码的分析。工具嘛,不用说当 然是Softice for Windows98、W32Dasm,OK,出发啦! 我的“齿轮”版本是0.221 for win98和winme的,内含有两个文件(变速齿轮.exe 和Hook.dll)。先看看Hook.dll里面有些什么,用W32Dasm将Hook.dll反汇编,看看它的输出函数: 看函数名好象该dll只是安装钩子捕获变速热键的,与我的研究目的没太大的关系, 跳过去! 再看看变速齿轮.exe的导入函数,timeGetTim、GetTickCount等时间相关的函数都 在里面。嘿,还有CreateFileMappingA和MapViewOfFileEx,看来“齿轮”是用这两个函 数创建映射文件的。以下列出几个关键的导入函数: KERNEL32.CreateFileMappingA KERNEL32.GetModuleFileNameA KERNEL32.GetModuleHandleA KERNEL32.GetTickCount KERNEL32.MapViewOfFileEx KERNEL32.QueryPerformanceCounte USER32.KillTimer USER32.SendMessageA USER32.SetTimer WINMM.timeGetTime WINMM.timeSetEvent 既然“齿轮”截获了timeGetTime,那我就跟踪timeGetTime函数的执行情况。 我先写了个Win32 APP (以下简称APP),当左击客户区时会调用timeGetTime并将返回的结果输出至客户区。运行这个程序,打开“齿轮”,改变当前速度。 Ctrl + D 呼出Softice,bpx timeGetTime ,退出,再左击APP客户区,Softice跳 出。哈,果然timeGetTime函数的首指令成了jmp 8xxx 002A ,好F8继续执行,进入了“ 齿轮”映射到2G线性地址之上的代码。一路F8下去,发现接着“齿轮”把timeGetTime 首指令恢复,并再次调用timeGetTime,这样就得到了timeGetTime的正确结果,保存结果。“齿轮”再把timeGetTime首指令又改为jmp 8xxx 002A 。接下来都猜得到“齿轮”要干什么了!没错,将得到的返回值修改后返回至调用timeGetTime的程序APP。 我仔细分析了一下,“齿轮”修改返回值的公式如下: 倍数*(返回值-第一次调用timeGetTime的返回值) 修改后的返回值=---------------------------------------------------+上一次修改后的返回值 100000 公式中“上次修改后的返回值”是自己猜测的未经证实,仅供参考。 代码分析已经进行一部分了,可我之前的疑问仍未解决,“齿轮”是怎么将代码映 射的?又是怎么得到修改代码的权限的? 既然“齿轮”中调用了CreateFileMappingA,我想其安装调用门,映射代码的初始 化部分应该就在调用该函数代码的附近。好,沿着这个思路,呼出Softice,在CreateF ileMappingA处设置断点,将“齿轮”关闭后再运行。Softice跳出,停在了CreateFile MappingA处,F11回到“齿轮”的代码。看到了“齿轮”调用CreateFileMappingA的形式 如下: CreateFileMappingA(FF,0,4,0,10000,0); 可见“齿轮”创建了长度为0x10000的映射文件,继续,“齿轮”接着又调用 MapViewOfFileEx,调用形式如下: MapViewOfFileEx(EDX,2,0,0,0,EAX); //EDX为CreateFileMappingA返回的映射文件句柄 //EAX为申请映射代码的基址,第一次调用时EAX为0x8000 0000 这里就是关键了,“齿轮”要将映射文件映射至基址为0x8000 0000 的内存空间中,可并不见得Windows就真的允许其映射呀?果然,“齿轮”在在调用之后判断返回值是否有效,无效则将上次申请的基址加上0x1000,再次调用MapViewOfFileEx,一直循环到成功为止,再将返回的地址保存。 接下来“齿轮”将原“齿轮”exe中的截获API的代码逐字节拷贝到映射区域去。至 此,“齿轮”已经将关键代码映射到2G以上线性地址中了。 我再F8,哈哈,和熟悉的SGDT指令打了个照面。“齿轮”保存全局描述符表线性基 址,再用SLDT指令保存局部描述符表索引,计算出LDT基址。接着呢“齿轮”在局部描述表中创建了一个特权等级为0的代码段指向需要利用Ring0特权修改代码的“齿轮”自己的代码,并把局部描述表中索引为2的调用门指向的地址改为“齿轮”映射到高于2G的代码。 然后“齿轮”依次调用各时间相关的API,保存其返回值留做计算返回时结果用。 “齿轮”又依次调用映射到高于2G的代码修改各API的首指令。到了这里,“齿轮”的初 始化部分就结束了,只等着还蒙在鼓里的游戏上钩啦,哈哈! 结束代码只不过是作些恢复工作罢了,仅仅是初始化代码的逆过程,所以就不再 赘述(其实是我自己懒得看了,^_^!).
|