反汇编DLL文件 HOOKAPI 作者 彬
前段时间在CSDN上有位网友问过一个问题,
反汇编DLL文件
。如何在WINDOWS下使删除文件、复制文件、变更文件名失效呢?有一个办法,就是通过拦截系统的I/O中断,可以实现此操作。但是因为网友只是问如何在WINDOWS下失效,那我们也可以试着拦截API函数。众所周知,在WINDOWS下的文件操作一般是通过调用WINAPI函数实现的,再由WINAPI函数调用系统的I/O中断实现最终的文件操作。如果我们在系统调用WINAPI函数时将其拦下,那么文件操作不就失效啦吗?
现在摆在我们面前的一个问题是,我们用何种方法来实现HOOKAPI函数,总的来分一共有两种:软模式和硬模式(呵呵,我起的名)。那什么叫软模式呢?软模式就是通过编写程序,使程序自动获得我们要截获得API函数的RVA(相对虚拟地址),然后在地址处写入我们的代码,使程序调用此API函数时先执行我们的代码,再返回执行原代码,别看我说得简单,但实际操作起来很麻烦,如程序在更改原API代码时必须进入RING0特权级等等问题都会摆在你的面前;硬模式是指利用手头上的软件直接更改相关DLL文件。由于软模式麻烦而且在2000下获得RING0特权级更麻烦,因此我们在此先讲如何利用硬模式达到我们预期的效果。
系统常用的API函数基本都存在于Gdi32.dll、Kernel32.dll和Shell32.dll三个文件中,通过文件名我们就可以知道每个DLL文件中的API函数主要用在哪方面。
这里我们要拦截的API函数是 SHFileOperation,是Shell32.dll中的函数此函数的用途删除文件、复制文件、移动文件、对文件更名。他有一个参数,是指向SHFILEOPSTRUCT结构的指针,其在ASM中的定义如下:
SHFILEOPSTRUCT STRUCT
hwnd DWORD ? ;调用者的窗口句柄
wFunc DWORD ? ;指定是何种操作。(复制、删除等)
pFrom DWORD ? ;来源目录或文件
pTo DWORD ? ;目的目录或文件
fFlags FILEOP_FLAGS ? ;操作文件的旗标
fAnyOperationsAborted DWORD ? ;是否允许使用者中断
hNameMappings DWORD ? ;不常用
lpszProgressTitle DWORD ? ;不常用
SHFILEOPSTRUCT ENDS
关于此结构的详细资料,可以通过查阅MSDN来获得。
好啦,函数名也知道啦,出自哪个文件也知道啦。下一步如何做呢?
我们只要找到此函数在Shell32.dll中的位置,在其头部插入ret 4这条语句就可以啦。为什么要插入这条语句呢?我们不要系统对删除文件、复制文件等做出任何响应,只能让此API函数一调用时就马上返回就可以啦,因为此函数有一个参数,为了使堆栈恢复原状,因此堆栈指针需要加4。
万事俱备,只欠不知道此函数在DLL文件中的地址啦。
要知道其位置,我们可以自己编写个程序,算出来,不过现在我手头上已经有一个现成的啦,为什么还要再编一个呢?????打开getModule程序,此程序可以获得某个函数在某个DLL中的RVA地址。在软件的请输入DLL名框中键入“Shell32.dll“,在请输入函数名框中键入“SHFileOperation“,按下获取,得到地址:7fcedcf1h(可能由于98版本的不同,这里的数值会不一样。注意:此软件区分大小写。)。我们要对Shell32.dll文件反汇编所用的软件是HIEW,此软件中显示的地址是指文件内部的偏移地址,因此我们还需要将RVA地址转换成文件内部偏移地址。打开第二个软件RVA Converter点击菜单“FILE“中的“LOAD“选项“,选中我们要加载的文件“Shell32.dll“,然后再下面的RVA框中键入刚才得到的RVA地址,键入完毕后,FILE框中所显示的地址:3DCF1即“SHFileOperation“函数在“Shell32.dll“文件内的偏移地址。
最关键的一步开始啦,打开HIEW软件,加载“Shell32.dll“,按下F4键,选择“DECODE“(反汇编),再按下F5键,键入刚才所得到的地址3DCF1,来到了我们要更改的函数的开头处,按下F3键进行编辑,再按下“TAB“键,可以直接键入汇编代码ret 4,之后按下F9键保存,退出程序。
重新启动机器,来到DOS下,将改好的Shell32.dll替换掉原先的文件。再重启机器进入WINDOWS,试试删除文件,怎么样?删除不了吧!
可能有人会问,我只想屏删除文件,我是不是只要按此方法拦DeleteFileA函数就可以啦?答案是不行的,开始我试验时拦的就是此函数,很怪的是如果你按下的是SHIFT+DEL进行删除文件,文件消失啦,但按一下F5刷新,文件就回来啦,达到要求,但是如果你通过回收站删除文件,那就不好用啦。因此最后我选择拦截SHFileOperation函数,如果你真得只想拦删除文件,可以通过编写程序检测SHFILEOPSTRUCT结构中的wFunc参数,来进行相应的操作达到你所想要的效果。这里有点唠叨啦。
总是让系统不删文件,我想硬盘空间会慢慢变小,呵呵,有点像病毒啦!
这种状态下我试了一个小时啦,不过没事,大家自己试试吧,出了问题可别找我啊!:)
最后我们总结一下,这种硬模式法有好处有坏处,好处是操作简单,稍懂一些汇编的人都会理解整个操作步骤的,坏处是,如果98版本不一样的话,手工改动太麻烦啦,而且由于我们只是拦下了API函数,在98内好用,但是打开98的DOS窗口仍可以进行删除操作,这一点请大家注意。这里只是教大家一种方法,具体用到哪方面,就看你自己了。
dll反汇编初步
2004-5-15 2:17:52 (文章类别:编程基础)
ozymandias(原作)
在论坛里面看到一些人讨论dll的反汇编,这几天帮一个朋友分析一个dll,此dll非常简单,现在把我的分析过
程和大家分享一下,这里没有什么特别有效的方法,靠的就是耐心和经验,反复验证,直到调用成功。
有个dll2lib的工具,不知道是我不会使用还是怎么的,反正我是没有使用成功过,所以我只能靠自己
来分析了。
首先是使用的工具,ida/win32dasm/ollydbg
win32dasm分析的速度快一些,但是智能程度不如ida,ida这个2001开发工具亚军绝对不是浪得虚名的
,它的智能程度非常高,可是识别出常用的函数,这两个都是静态反汇编的工具,必须配以动态分析的工具,
毕竟你很难一下子就分析对(至少我是这样),当然你可以使用s-ice或者trw,但是这些工具都有限制,trw不支
持2000,s-ice一旦装载只能reboot才能取消装载,还有其工作在ring0,所以你只能对者黑乎乎的屏幕,很痛
苦,这里选用的ollydbg是最新版本,支持dll的跟踪。
下面列出win32dasm反汇编的结果:
Exported fn(): GetUserNumber - Ord:0004h
:0040C1B0 55,,,,,,,,,,push ebp
:0040C1B1 8BEC,,,,,,,,,mov ebp, esp
:0040C1B3 53,,,,,,,,,,push ebx
:0040C1B4 56,,,,,,,,,,push esi
:0040C1B5 57,,,,,,,,,,push edi
:0040C1B6 8B5D08,,,,,,,,mov ebx, dword ptr [ebp+08]
:0040C1B9 33F6,,,,,,,,,xor esi, esi
:0040C1BB 8BC3,,,,,,,,,mov eax, ebx
:0040C1BD E846A5FFFF,,,,,,call 00406708
:0040C1C2 6685C0,,,,,,,,test ax, ax
:0040C1C5 7212,,,,,,,,,jb 0040C1D9
:0040C1C7 40,,,,,,,,,,inc eax
:0040C1C8 33D2,,,,,,,,,xor edx, edx
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0040C1D7(C)
|
:0040C1CA 0FB7CA,,,,,,,,movzx ecx, dx
:0040C1CD 0FB60C0B,,,,,,,movzx ecx, byte ptr [ebx+ecx]
:0040C1D1 03F1,,,,,,,,,add esi, ecx
:0040C1D3 42,,,,,,,,,,inc edx
:0040C1D4 66FFC8,,,,,,,,dec ax
:0040C1D7 75F1,,,,,,,,,jne 0040C1CA
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0040C1C5(C)
|
:0040C1D9 8B450C,,,,,,,,mov eax, dword ptr [ebp+0C]
:0040C1DC 8A4012,,,,,,,,mov al, byte ptr [eax+12]
:0040C1DF 3C61,,,,,,,,,cmp al, 61
:0040C1E1 720B,,,,,,,,,jb 0040C1EE
:0040C1E3 25FF000000,,,,,,and eax, 000000FF
:0040C1E8 6683E861,,,,,,,sub ax, 0061
:0040C1EC EB09,,,,,,,,,jmp 0040C1F7
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0040C1E1(C)
|
:0040C1EE 25FF000000,,,,,,and eax, 000000FF
:0040C1F3 6683E841,,,,,,,sub ax, 0041
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0040C1EC(U)
|
:0040C1F7 8B550C,,,,,,,,mov edx, dword ptr [ebp+0C]
:0040C1FA 8A5213,,,,,,,,mov dl, byte ptr [edx+13]
:0040C1FD 80FA61,,,,,,,,cmp dl, 61
:0040C200 720C,,,,,,,,,jb 0040C20E
:0040C202 81E2FF000000,,,,,and edx, 000000FF
:0040C208 6683EA61,,,,,,,sub dx, 0061
:0040C20C EB0A,,,,,,,,,jmp 0040C218
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0040C200(C)
|
:0040C20E 81E2FF000000,,,,,and edx, 000000FF
:0040C214 6683EA41,,,,,,,sub dx, 0041
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0040C20C(U)
|
:0040C218 0FB7C0,,,,,,,,movzx eax, ax
:0040C21B 6BF81A,,,,,,,,imul edi, eax, 0000001A
:0040C21E 0FB7C2,,,,,,,,movzx eax, dx
:0040C221 03F8,,,,,,,,,add edi, eax
:0040C223 81F74D010000,,,,,xor edi, 0000014D
:0040C229 83F701,,,,,,,,xor edi, 00000001
:0040C22C 8BC3,,,,,,,,,mov eax, ebx
:0040C22E E8D5A4FFFF,,,,,,call 00406708
:0040C233 2BF8,,,,,,,,,sub edi, eax
:0040C235 8BC6,,,,,,,,,mov eax, esi
:0040C237 B91A000000,,,,,,mov ecx, 0000001A
:0040C23C 99,,,,,,,,,,cdq
:0040C23D F7F9,,,,,,,,,ip ecx
:0040C23F 2BFA,,,,,,,,,sub edi, edx
:0040C241 8BC7,,,,,,,,,mov eax, edi
:0040C243 5F,,,,,,,,,,pop edi
:0040C244 5E,,,,,,,,,,pop esi
:0040C245 5B,,,,,,,,,,pop ebx
:0040C246 5D,,,,,,,,,,pop ebp
:0040C247 C20800,,,,,,,,ret 0008
通过ret 008我们可以知道这个函数需要两个参数,通过mov eax, esi,我们可以知道这个函数有反回
值(这是因为在高级语言里面一般通过eax设置函数返回值)具体返回什么类型现在还没办法知道,但是不管这么
多,我们假设此函数是这样的:
int GetUserNumber(int a1,int a2)
我们会在后续分析后,慢慢修正他,现在我们开始一点点分析。
:0040C1B0 55,,,,,,,,,,push ebp
:0040C1B1 8BEC,,,,,,,,,mov ebp, esp
:0040C1B3 53,,,,,,,,,,push ebx
:0040C1B4 56,,,,,,,,,,push esi
:0040C1B5 57,,,,,,,,,,push edi
这个是function prolog,建立堆栈和寄存器的保存,没什么可多说的。
:0040C1B6 8B5D08,,,,,,,,mov ebx, dword ptr [ebp+08]
:0040C1B9 33F6,,,,,,,,,xor esi, esi
:0040C1BB 8BC3,,,,,,,,,mov eax, ebx
:0040C1BD E846A5FFFF,,,,,,call 00406708
这里你需要知道函数调用过程的参数传递,可以到asm.yeah.net看看罗云彬那篇很好的关于参数传递和堆栈修
复的文章,这里简单说一下,调用函数的时候如果通过堆栈来传递参数的话,那么对于我们讨论的
函数可能是这样的:
push a2
push a1
call GetUserNumber
此时堆栈看起来是这样的
-----a2
-----a1
esp-> -----returnaddress
这时候进入函数内部push ebp后
esp+c -----a2
esp+8 -----a1
esp+4 -----returnaddress
esp-> -----ebp
mov ebp,esp
此时要寻址a1可以通过ebp+8
:0040C1B6 8B5D08,,,,,,,,mov ebx, dword ptr [ebp+08]; ebx保存第一个参数
:0040C1B9 33F6,,,,,,,,,xor esi, esi
:0040C1BB 8BC3,,,,,,,,,mov eax, ebx
:0040C1BD E846A5FFFF,,,,,,call 00406708
这个很简单esi清零,把第一个参数传递给eax,然后调用00406708
所以我们接下来就是要到00406708里面去看看
:00406708 89FA,,,,,,,,,mov edx, edi
:0040670A 89C7,,,,,,,,,mov edi, eax
:0040670C B9FFFFFFFF,,,,,,mov ecx, FFFFFFFF
:00406711 30C0,,,,,,,,,xor al, al
:00406713 F2,,,,,,,,,,repnz
:00406714 AE,,,,,,,,,,scasb
:00406715 B8FEFFFFFF,,,,,,mov eax, FFFFFFFE
:0040671A 29C8,,,,,,,,,sub eax, ecx
:0040671C 89D7,,,,,,,,,mov edi, edx
:0040671E C3,,,,,,,,,,ret
这个是很典型的求字符串长度的代码
:00406708 89FA,,,,,,,,,mov edx, edi,;保存edi
:0040670A 89C7,,,,,,,,,mov edi, eax ;eax是我们调用前的a1
:0040670C B9FFFFFFFF,,,,,,mov ecx, FFFFFFFF ecx设置成最大32数
:00406711 30C0,,,,,,,,,xor al, al ;al清零
我们知道汇编指令的字符串操作一般通过esi和edi,上面的过程在注释里给出解释
:00406713 F2,,,,,,,,,,repnz
:00406714 AE,,,,,,,,,,scasb ;判断字符串是否结束
:00406715 B8FEFFFFFF,,,,,,mov eax, FFFFFFFE
:0040671A 29C8,,,,,,,,,sub eax, ecx;eax保存了字符串长度
:0040671C 89D7,,,,,,,,,mov edi, edx;恢复edi
:0040671E C3,,,,,,,,,,ret
上面分析我们知道参数a1是一个字符串,现在把我们的函数修正如下
int GetUserNumber(char * a1,int a2)
继续我们的分析:
:0040C1C2 6685C0,,,,,,,,test ax, ax ;长度是否为零
:0040C1C5 7212,,,,,,,,,jb 0040C1D9 ;为零则跳转
:0040C1C7 40,,,,,,,,,,inc eax ;eax加一
:0040C1C8 33D2,,,,,,,,,xor edx, edx ;edx清零
我们用c语言写出等价程序如下:
int GetUserNumber(char * strUserName,test * pt)
{
esi = 0;
int len = strlen(strUserName);
if(len <= 0)
goto _SKIP_USERNAME;
_SKIP_USERNAME:
}
继续分析并且进一步修正我们的代码
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0040C1D7(C)
|
:0040C1CA 0FB7CA,,,,,,,,movzx ecx, dx,;ecx置零
:0040C1CD 0FB60C0B,,,,,,,movzx ecx, byte ptr [ebx+ecx];ebx保存第一个参数
:0040C1D1 03F1,,,,,,,,,add esi, ecx,;取出字符串的第ecx位
:0040C1D3 42,,,,,,,,,,inc edx,;edx加一
:0040C1D4 66FFC8,,,,,,,,dec ax,;ax字符串长度减一
:0040C1D7 75F1,,,,,,,,,jne 0040C1CA,;字符串未处理完则继续
上面是典型的基址加变址寻址模式,很简单,等价的c代码如下:
int GetUserNumber(char * strUserName,test * pt)
{
esi = 0;
int len = strlen(strUserName);
if(len <= 0)
goto _SKIP_USERNAME;
for(int i = 0 ; i < len ; ++i)
{
esi += strUserName[i];//计算用户名ASC和
}
_SKIP_USERNAME:
}
继续分析并且进一步修正我们的代码
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0040C1C5(C)
|
:0040C1D9 8B450C,,,,,,,,mov eax, dword ptr [ebp+0C] ;取第二个参数
:0040C1DC 8A4012,,,,,,,,mov al, byte ptr [eax+12] ;移动0X12=18个字节
:0040C1DF 3C61,,,,,,,,,cmp al, 61 ;与0X61='a'比较
:0040C1E1 720B,,,,,,,,,jb 0040C1EE ;小于则跳
:0040C1E3 25FF000000,,,,,,and eax, 000000FF,#槐A鬳ax的低8位
:0040C1E8 6683E861,,,,,,,sub ax, 0061 ;计算与'a'的差距
:0040C1EC EB09,,,,,,,,,jmp 0040C1F7 ;跳到0040C1F7
能看出在第一个参数表示的字符串长度是0的时候会直接跳到这里。
这里的问题是我们如何知道第二个参数的类型呢?
答案是没办法知道,因为就现在这些信息,我们没有办法知道第二个参数是什么类型,但是一定是一块内存区
域,因为mov al, byte ptr [eax+12]这段代码告诉我们的,现在我们来模拟出来这段区域
我们定义一个结构来模拟,事实上我们在写程序的时候,如果调用函数时候参数类型不匹配,会得到编译错误
,其实这个错误是编译器给出来的,他与我们能否成功调用函数无关,因为如果类型真的不匹配,运行时会出
错,所以调用dll的时候我们的参数类型未必一定要和实际dll原代码实现的是一模一样的,但是一定要兼容。
所以我用结构来模拟,并不会破坏我们讨论的问题的一般性。
定义如下结构:
struct test
{
char c18_unused[18];
char c19;
};
现在重新修正我们的函数
int GetUserNumber(char * strUserName,test * pt)
{
int esi;
char al;
esi = 0;
int len = strlen(strUserName);
if(len <= 0)
goto _SKIP_USERNAME;
for(int i = 0 ; i < len ; ++i)
{
esi += strUserName[i];//计算用户名ASC和
}
_SKIP_USERNAME:
al = pt->c19_IsNumber;
if(al < 'a') //判断字符大小写
goto IsLower;
}
继续分析
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0040C1E1(C)
|
:0040C1EE 25FF000000,,,,,,and eax, 000000FF ;cmp al, 61后会跳到这里,eax保留低8位
:0040C1F3 6683E841,,,,,,,sub ax, 0041#0x41='A',计算与'A'差距
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0040C1EC(U)
|
:0040C1F7 8B550C,,,,,,,,mov edx, dword ptr [ebp+0C] ;edx保存第二个参数
:0040C1FA 8A5213,,,,,,,,mov dl, byte ptr [edx+13] ;取0x13=19位
:0040C1FD 80FA61,,,,,,,,cmp dl, 61
:0040C200 720C,,,,,,,,,jb 0040C20E
:0040C202 81E2FF000000,,,,,and edx, 000000FF
:0040C208 6683EA61,,,,,,,sub dx, 0061
:0040C20C EB0A,,,,,,,,,jmp 0040C218
上面这段与上面的代码几乎一样,不做分析。值得一提的是我们需要修正我们前面定义的结构,修正后:
struct test
{
char c18_unused[18];
char c19;
char c20
};
等价c代码
int esi;
char al;
char dl;
int edx;
int edi;
int eax;
int ecx;
esi = 0;
int len = strlen(strUserName);
if(len <= 0)
goto _SKIP_USERNAME;
for(int i = 0 ; i < len ; ++i)
{
esi += strUserName[i];//计算用户名ASC和
}
_SKIP_USERNAME:
al = pt->c19_IsNumber;
if(al < 'a') //判断字符大小写
goto IsNumber1;
al -= 'a';
goto _CONTINUE1;
IsNumber1:
al -= 'A';
_CONTINUE1:
dl = pt->c20_IsNumber;
if(dl < 'a')//判断字符大小写
goto IsNumber2;
dl -= 'a';
goto _CONTINUE2;
IsNumber2:
dl -= 'A';
}
我们的目标快要到达了,最后的一段代码
* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0040C20C(U)
|
:0040C218 0FB7C0,,,,,,,,movzx eax, ax
:0040C21B 6BF81A,,,,,,,,imul edi, eax, 0000001A
:0040C21E 0FB7C2,,,,,,,,movzx eax, dx
:0040C221 03F8,,,,,,,,,add edi, eax
:0040C223 81F74D010000,,,,,xor edi, 0000014D
:0040C229 83F701,,,,,,,,xor edi, 00000001
:0040C22C 8BC3,,,,,,,,,mov eax, ebx, ;第一个参数
:0040C22E E8D5A4FFFF,,,,,,call 00406708 ;和前面分析一样求长度
:0040C233 2BF8,,,,,,,,,sub edi, eax ;减去长度值
:0040C235 8BC6,,,,,,,,,mov eax, esi ;esi是第一个参数asc码和
:0040C237 B91A000000,,,,,,mov ecx, 0000001A;ecx=0x1a=26
:0040C23C 99,,,,,,,,,,cdq,;eax扩展成edx:eax
:0040C23D F7F9,,,,,,,,,ip ecx ;除以ecx=26
:0040C23F 2BFA,,,,,,,,,sub edi, edx ;减去余数
:0040C241 8BC7,,,,,,,,,mov eax, edi ;传给eax
:0040C243 5F,,,,,,,,,,pop edi
:0040C244 5E,,,,,,,,,,pop esi
:0040C245 5B,,,,,,,,,,pop ebx
:0040C246 5D,,,,,,,,,,pop ebp
:0040C247 C20800,,,,,,,,ret 0008
这段代码也很简单,到这里我们得到近似的c源代码,这里对于cdq指令完成的功能我没有实现,但是这对问题
没有影响:
int GetUserNumber(char * strUserName,test * pt)
{
int esi;
char al;
char dl;
int edx;
int edi;
int eax;
int ecx;
esi = 0;
int len = strlen(strUserName);
if(len <= 0)
goto _SKIP_USERNAME;
for(int i = 0 ; i < len ; ++i)
{
esi += strUserName[i];//计算用户名ASC和
}
_SKIP_USERNAME:
al = pt->c19_IsNumber;
if(al < 'a') //判断字符大小写
goto IsNumber1;
al -= 'a';
goto _CONTINUE1;
IsNumber1:
al -= 'A';
_CONTINUE1:
dl = pt->c20_IsNumber;
if(dl < 'a')//判断字符大小写
goto IsNumber2;
dl -= 'a';
goto _CONTINUE2;
IsNumber2:
dl -= 'A';
_CONTINUE2:
edi = al*0x1a;//26
eax = dl;
edi += eax;
edi^=0x14d;
edi^=0x1;
edi -= len;
eax = esi;
ecx = 0x1a;//26
//edx = eax的符号位扩展
edx = eax%ecx;
eax/=ecx;
edi -= edx;
eax = edi;
return eax;
}
到这里整个分析就结束了,我要说的是这里分析的函数比较简单,不具有一般性,涉及到更复杂的函数,比如
虚拟函数、类的分析,这里我的分析没有什么技术型可言,只是就一些简单的dll函数的反汇编,给出我的一点
经验,对您有帮助我感到很荣幸,对你没帮助浪费了您的时间我感到抱歉,希望您别骂我就好.
“变速齿轮”研究手记
注意:如果你看了本文,对我们这个软件有兴趣,请到我们的主页www.vrbrothers.com下载。
注:为节省篇幅,本文对一些计算机术语直接使用而没有作详细的解释,读者若有不熟悉之处,建议参考清华大学出版社出版,周明德编著的《微型计算机系统原理及应用》一书中关于8253/8254定时器和x86保护模式的相应章节。
也许是我孤陋寡闻吧,说出来不怕您笑话,对于“变速齿轮”这样著名的软件,我一直到五天前,也就是2001年2月28号才第一次听说。我有几个同学很喜欢玩图形MUD,整天见了面就在一起切磋“泥”技。我对MUD本身并没有多大兴趣,但是那天早上偶尔听他们说某个MUD站点明文规定严禁使用“齿轮”,这才好奇地问他们什么是“齿轮”。别人告诉我,“齿轮”是一个软件,能对Windows下的游戏加速,他们在玩MUD时就依靠这个软件作弊。这不禁令我一头雾水,能让Windows游戏改变速度,太神奇了!
我一贯对技术很有兴趣,听说有这么一个神奇的软件,当然要想想它是怎么实现的。这个软件看起来并不复杂,我原以为一个早自习好好琢磨琢磨就行,可是我想了好几节课,始终不得其要领。说来也巧,我们这学期有一面必修课是Linux内核原理分析,这几天正好学到了进程调度,老师说,当一个时钟中断发生的时候,操作系统要做很多事情,比如必要时要重新调度进程从而实现抢先式多任务,还要更新系统时钟......慢着,我突发奇想,如果让时钟中断产生的更快,会发生什么事情呢?
我们已经学过“微机原理”这门课程,我知道让时钟中断产生的更快不是难事,以前我就用DOS下的汇编语言写过这样的程序,这是我们当时的作业。可是我以前的程序在Windows下虽然可以运行,但并不能对Windows系统加速,道理很显然:Windows9x是使用x86虚拟机的机制来兼容DOS程序的,我的程序只能改变虚拟机,就是那个黑窗口的时钟中断。
于是我试图把以前的DOS程序搬到32位环境中。用VC内嵌汇编做这件事再合适不过了,在一个VC程序框架中加上一个__asm,然后只管把以前的汇编程序往里贴就行。我满怀希望地运行这样一个拼凑出来的怪物,结果,出现了一个大家都很熟悉的“该程序执行了非法操作”,我的试验以失败告终。
后来冷静下来仔细想想,这次失败的原因是显然的。Windows作为一个复杂的32位操作系统,如果能让你随便对硬件进行操作,那也许运行不了几个程序就崩溃了。但是如何绕过操作系统去操作硬件呢?我首先想到了vxd,编写一个驱动程序肯定可以操作硬件,但是,很可惜,我不会设计驱动程序。于是我想到了以前看到的CIH的源码,CIH没有写vxd,却能操作硬件去烧毁BIOS,陈盈豪真是太伟大了,他的程序精巧之处我至今记忆犹新。于是我模仿他的技术,修改IDT表,创建一个中断门,然后发生中断,进入ring0,现在我可以做任何事情了,按照以前的DOS程序那样,往8253定时器里写一个控制字,再分两次写入新的时钟中断发生频率,一切顺利!(详细技术请您参考我的“兄弟变速器”源码)我看到VC编辑区的光标疯狂的闪烁;双击已经失效了,因为Windows认为我双击的时间间隔太长;Windows任务栏右方的时间飞快跳动,应该说,我已经成功了。
当时我想当然的以为“变速齿轮”的原理也是如此,可是当我从同学那里把“齿轮”拷来并研究时,发现Windows的时钟并不变快,而游戏速度照样可以加上去,也就是说,“齿轮”采用了与我的程序不同的技术,是什么技术呢?我决定继续研究。
我访问了“变速齿轮”的主页,这个主页上有一个“你问我答”的栏目,由“齿轮”的作者王荣先生进行技术支持。我试图在这里找到一些关于“齿轮”的技术细节,但是很可惜,没有找到,王荣先生只是告诉大家这个程序不能用VB编写等等根本连皮毛也不涉及的问题,好不容易见到一个外国人问能不能公布源代码,其实这也是我想问的,但是王荣先生明确表示不行,这不禁令我感到非常失望。
我也想过写信去索取原码,也许他不向外国人公布,中国人可不一定。但是咱们“臭老九”最爱一个面子,我实在拉不下脸去问。这时已经是晚上10点了,我决定祭出SoftIce,用一夜时间去研究他的程序。
当时使用的工具是SoftIce,WD32ASM和VC,手边两本参考书是《微型计算机系统原理及应用》和《Linux操作系统内核分析》(都是我们的课本,呵呵)。
起初,“变速齿轮”0.2版的一个叫hook.dll的文件很大程度上吸引了我的注意力,我怀疑他使用Windows消息钩子实现变速,消息钩子我很熟悉,但我把MSDN上面关于钩子的介绍看了好久,也没有想出它和变速有什么联系,这时偶然看了一下在王荣先生的主页上得到的“变速齿轮”0.1版,才发现老版本中并没有这个文件,也就是说,我只需要反汇编他的主程序就够了,于是,二话不说,用WD32ASM先把0.1版的“齿轮”给拆了,汇编代码5000多行,并不算多。
我是从这个程序的导入函数着手的,以前编程时用于定时的SetTimer,timeGetTime,timeSetEvent等等这里都导入了,看看它们被引用的地方,我发现这些函数都是集中出现的,而且大都以这样的形式出现:
* Reference To: WINMM.timeGetTime, Ord:0098h
:00401F3E 8B0D64424000 mov ecx, dword ptr [00404264]
:00401F44 8B11 mov edx, dword ptr [ecx]
也就是说,他并没有调用这些函数,只是取得了函数的入口地址,保存在ecx中,然后又根据这个入口地址得到了函数的前面几个字节,保存在edx中。
这让我想到了前些日子在CSDN上面和别人讨论的Hook API的原理,当时我还索取了一份Hook API的例程,如果我要Hook这里的函数timeGetTime,修改ecx中的地址或者修改edx处的头几条指令就行,用汇编语言写,与上面看到的这段代码类似。
为了测试“齿轮”是不是要Hook这里的timeGetTime,我自己编写了一个很简单的小程序,调用timeGetTime,每秒钟显示一个数字。用“齿轮”进行加速后,果然显示的速度快多了。再用SoftIce跟进这个timeGetTime函数,第一条指令变成一个跳转,这充分说明“齿轮”确实Hook了这几个API,不难猜测,他要改变函数的返回值,也就是说在timeGetTime结束时还要再跳入“齿轮”自身的代码,耐心跟下去,我发现回到timeGetTime时栈里多压了一个地址,这样,当timeGetTime用ret指令返回时,先返回“齿轮”的代码(这个思想确实很巧),返回值经过处理后,才跳回我的应用程序。至于怎么处理这个返回值就简单了,改到原先的2倍,应用程序速度也就提高了2倍。
回头再看WD32ASM反汇编的代码,我又发现在Hook API前面的不远处使用了一次SGDT指令和两次SLDT指令,这是x86保护方式的特有指令,用于获得全局描述符表,进一步得到局部描述符表,这段代码引起了我的兴趣,用SoftIce跟进去,往下走几步,一边跟一边猜,大致整理出了这样的思路:
1.创建一个内存映射,把自己的代码映射到0x80000000以上的地方,在Win9x下,这块虚存是所有进程共享的。
2.先得到局部描述符表的地址,然后利用这张表修改代码段的特权级。
3.用局部描述符表创建一个调用门,在x86的保护模式下要进入ring0必须通过门来进行,CIH是用中断门完成的,这里用调用门完成,异曲同工。
4.保存几个关键函数前六个字节,改为一条跳转指令,跳到自己已经映射到高端的代码。
5.发生函数调用时进入自己的代码,通过调用门进入ring0,恢复函数开头的几个字节,修改返回值。
这时已经是凌晨5点了,既然主要思想已经掌握,我也就没有细看这段代码,8点钟还要上课,睡觉去也。
回头想想,我认为王荣先生的代码还有几点值得推敲之处:
1.如果要Hook API,一定要改变函数的第一条指令吗?如果仅仅改变函数的入口地址,不是既容易编也容易调试吗?
2.即使要改变函数第一条指令,一定要进入ring0吗?
3.即使要进入ring0,使用中断门不是比用调用门更方便吗?
当然,按照王荣先生在他的主页上的说法,“变速齿轮”0.1版是他在三年前即1997年写的,那时Windows95刚刚出来两年,能有这样的技术已经难能可贵了,这里对王荣先生的钻研精神表示由衷的敬佩。
在我研究出“变速齿轮”的原理后三天,我以自己原先的研究结果为核心,编写出了“兄弟变速器”的最初版本,不用“变速齿轮”的技术是因为我认为我的技术更优越,何况也没有拾人牙慧之嫌了 ^_^
最后再次对王荣先生表示感谢,这样精彩的创意值得我们敬佩。
“变速齿轮”再研究
提起“变速齿轮”(以下简称“齿轮”)这个软件,大家应该都知道吧,该软件号称
是全球第一款能改变游戏速度的程序。我起初用时觉得很神奇,久而久之就不禁思考其实现原理了,但苦于个人水平有限,始终不得其解,成了长驻于脑中挥散不去的大问号。
偶然一天在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反汇编,看看它的输出函数:
?ghWnd@@3PAUHWND__@@A
?gnHotKey1@@3KA
?gnHotKey2@@3KA
?gnHotKey3@@3KA
?gnHotKey4@@3KA
?nHook@@3HA
?SetHook@@YAHPAUHWND__@@@Z
?UnHook@@YAHXZ
看函数名好象该dll只是安装钩子捕获变速热键的,与我的研究目的没太大的关系, 跳过去!
再看看变速齿轮.exe的导入函数,timeGetTim、GetTickCount等时间相关的函数都
在里面。嘿,还有CreateFileMappingA和MapViewOfFileEx,看来“齿轮”是用这两个函
数创建映射文件的。以下列出几个关键的导入函数:
Hook.?gnHotKey1@@3KA
Hook.?gnHotKey2@@3KA
Hook.?gnHotKey3@@3KA
Hook.?gnHotKey4@@3KA
Hook.?SetHook@@YAHPAUHWND__@@@Z
KERNEL32.CreateFileMappingA
KERNEL32.GetModuleFileNameA
KERNEL32.GetModuleHandleA
KERNEL32.GetTickCount
KERNEL32.MapViewOfFileEx
KERNEL32.QueryPerformanceCounte
USER32.KillTimer
USER32.SendMessageA
USER32.SetTimer
WINMM.timeGetTime
WINMM.timeSetEvent
既然“齿轮”截获了timeGetTime,那我就跟踪timeGetTime函数的执行情况,
电脑资料
《反汇编DLL文件》()。我先写了个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的首指令。到了这里,“齿轮”的初始化部分就结束了,只等着还蒙在鼓里的游戏上钩啦,哈哈!
结束代码只不过是作些恢复工作罢了,仅仅是初始化代码的逆过程,所以就不再赘述(其实是我自己懒得看了,^_^!).
至此,我对“齿轮”的加速原理已有大致的了解,深刻感受到“齿轮”代码的精巧, 所以觉得有必要将"齿轮"中所运用到的一些技巧作一个总结:
1.基址无关代码的编写
姑且以上面一句话作标题,^_^。看了“齿轮”的初始化代码,知道其映射代码的基址差不多是随机的,那么“齿轮”是怎么保证映射后的代码能正常运行的呢?如果 代码是完全顺序执行的倒没什么问题,但如果要调用自己映射代码中的子程序呢?呵呵,就只有运行时计算出子程序的入口地址并调用了,不过还是要先得到映射代码所在的地址才行。“齿轮”简单地用两条指令就得到当前正在执行的指令的地址,具体如下(地址为假设的):
0:0call 5
0:5pop esi
现在esi中的值就是5了,哈哈!
这里的call用的是近调用,整条指令为E800000000,即为调用下一条指令.所进行的操作只不过是把下一条指令的地址入栈而已.再pop将返回地址(即pop指令本身的地址)取出.
2.修改调用门,生成jmp指令,修改代码
这些都是高度依赖于CPU的操作,技巧性也很强,主要是钻了操作系统的漏洞。比如“齿轮”就是用SGDT,SLDT获得全局和局部描述符表基址来安装调用门,通过访问调用门来获取RING0权限作一些平时不为系统所允许的操作;而CIH病毒是用SIDT获得中断描述符表基址安装中断门然后出发软中断获取RING0权限的,原理都是一样的。这些在水木上讨论过很多遍,大家都很熟悉,所以也就不敢班门弄斧,写到此为止。
3.64K代码编写
由调用CreateFileMappingA函数参数可知“齿轮”只映射10000(64K)大小的区域,所以其映射在2G之上的代码和数据决不能大于64K。我想作者之所以选择64K为映射区域的大小,可能是与调用子程序或数据时容易计算地址有关。在映射代码的任意一处得到当前指令地址之后将其低16位置0即可得到映射代码的基地址,再加上子程序入口或数据的偏移即可求得其绝对地址。
我的评论:
一句话:佩服“齿轮”的作者王荣先生。
“齿轮”的代码表现他对windows运行机制的深刻理解以及深厚的汇编功底还有丰富的想象力。对我来说“齿轮”仿佛就是一件精美的艺术品,每个细处都很值得玩味一 番,所以我才在看过“齿轮”代码之后有了把我的分析过程用笔写下来的冲动。但同时 我又不得不承认“齿轮”的功能的实现是依靠其高度技巧化的代码实现的,换句话说就 是这种的方法局限性实在是太大了。不就是截获API嘛,用的着这么麻烦吗?
为了证实自己的想法,我在Codeguru上直接找了个HOOK API 的代码,该代码是通过安装WH_CBT类型全局钩子在所有 入DLL的进程中修改进程PE映像的输入节达到截获API的(这种方法在《windows核心编程》中有详细说明)。把代码稍做修改,就能工作了(在星际争霸下试过,可以改变游戏速度)。尽管只在98下试过,但我觉得肯定也能在2000下用,因为代码中只用了一两句汇编指令,而且整个程序都是在RING3下运行的,没有作出什么出轨的举动。当然这种方法也有缺点,就是对用Loadlibrary加载WINMM.dll再用GetProcAddress获取timeGetTime地址的API调用不起作用(原因在《windows核心编程》中有说明)。
我打算在将测试用程序稍稍完善后再公布源代码,届时欢迎大家下载。
我的感谢:
在我彻底弄清“齿轮”的代码之后,已经是第三天的上午了,无奈自己才疏学浅,全不像《手记》的作者只花了一个晚上就弄清楚,我可是花了一个上午、两个下午、两个晚上才结束了战斗,实在是惭愧呀。
自己之所以能自得其乐地坚持了两天多,是与寝室兄弟小强的支持分不开的。穷 困潦倒的我在这几天不知道总共抽了他多少支烟,无以为报,只有在这里说一声谢谢了!另外还要感谢sunlie非常地阅读本文,指出了原文中的错误并提出了非常宝贵的意见!
最后要说的就是个人水平有限,文中难免出现错误,欢迎大家讨论!^_^
附A:
使用工具:Softice for Windows98,W32Dasm,VisualC++ 6.0
操作系统:Window98 2nd
分析目标:变速齿轮 for 98me 版本:0.221
参考书籍或文章:
80x86汇编语言程序设计教程杨季文等编著清华大学出版社
windows剖析--初始化篇及内核篇清华大学出版社
虚拟设备驱动程序开发
intel 32位系统软件编程
80x86指令参考手册
《“变速齿轮”研究手记》
附B:
“齿轮”关键代码完全注释
一、初始化部分(从"齿轮"调用CreateFileMappingA函数开始分析)
0167:00401B0EPUSH00
0167:00401B10PUSH00010000
0167:00401B15PUSH00
0167:00401B17PUSH04
0167:00401B19PUSH00
0167:00401B1BPUSHFF
0167:00401B1DCALL[KERNEL32!CreateFileMappingA]
;调用CreateFileMappingA
;调用形式如右:CreateFileMappingA(FF,0,4,0,10000,0)
0167:00401B23MOVECX,[EBP-30]
0167:00401B26MOV[ECX+00000368],EAX
0167:00401B2CMOVDWORD PTR [EBP-14],80000000
0167:00401B33JMP00401B41
0167:00401B35MOVEDX,[EBP-14]
0167:00401B38ADDEDX,00010000
;申请基址加0x10000
0167:00401B3EMOV[EBP-14],EDX
0167:00401B41MOVEAX,[EBP-14]
0167:00401B44PUSHEAX;映射文件基址
0167:00401B45PUSH00;映射的字节数
0167:00401B47PUSH00;文件偏移低32位
0167:00401B49PUSH00;文件偏移高32位
0167:00401B4BPUSH02;访问模式
0167:00401B4DMOVECX,[EBP-30]
0167:00401B50MOVEDX,[ECX+00000368]
0167:00401B56PUSHEDX
;CreateFileMappingA返回的映射文件句柄
0167:00401B57CALL[KERNEL32!MapViewOfFileEx]
; 调用形式如右:MapViewOfFileEx(EDX,2,0,0,0,EAX)
0167:00401B5DMOVECX,[EBP-30]
;[EBP-30]为即将映射到2G之上
0167:00401B60MOV[ECX+0000036C],EAX
; 的代码的数据域的起始地址
0167:00401B66MOVEDX,[EBP-30]
0167:00401B69CMPDWORD PTR [EDX+0000036C],00
;检查MapViewOfFileEx
0167:00401B70JZ00401B74
;返回值,若为0则继续调
0167:00401B72JMP00401B76;调用MapViewOfFileEx
0167:00401B74JMP00401B35;直至成功为止
0167:00401B76MOVEAX,[EBP-30]
0167:00401B79MOVECX,[EAX+0000036C]
0167:00401B7FMOV[EBP-08],ECX
;映射文件起始地址存入[EBP-08]
0167:00401B82CALL[WINMM!timeGetTime]
0167:00401B88MOV[EBP-14],EAX
;将初次调用timeGetTime
0167:00401BA0MOVECX,[EBP-08]
;的返回值保存到[EBP-14]
0167:00401BA3MOVEDX,[EBP-14]
;以及映射文件基址+FF30处
0167:00401BA6MOV[ECX+0000FF30],EDX
...省略的代码类似的保存调用初次GetTickCount,QueryPerformanceCounter的返回值
0167:00401BEDMOVDWORD PTR [EBP-14],00000000
0167:00401BF4MOVEDX,[EBP-30]
0167:00401BF7MOVEAX,[EDX+0000036C]
0167:00401BFDMOVECX,[EBP-14]
0167:00401C00MOVBYTE PTR [ECX+EAX+0000F000],9A
;9a为远调用的指令码
0167:00401C08MOVEDX,[EBP-14]
0167:00401C0BADDEDX,01
0167:00401C0EMOV[EBP-14],EDX
0167:00401C11MOVEAX,[EBP-14]
0167:00401C14ADDEAX,04
0167:00401C17MOV[EBP-14],EAX
0167:00401C1AMOVECX,[EBP-30]
0167:00401C1DMOVEDX,[ECX+0000036C]
0167:00401C23MOVEAX,[EBP-14]
0167:00401C26MOVBYTE PTR [EAX+EDX+0000F000],14
;14为调用门描述符的索引
0167:00401C2EMOVECX,[EBP-14]
0167:00401C31ADDECX,01
0167:00401C34MOV[EBP-14],ECX
0167:00401C37MOVEDX,[EBP-30]
0167:00401C3AMOVEAX,[EDX+0000036C]
0167:00401C40MOVECX,[EBP-14]
0167:00401C43MOVBYTE PTR [ECX+EAX+0000F000],00
;CALL指令其他部分
0167:00401C4BMOVEDX,[EBP-14]
0167:00401C4EADDEDX,01
0167:00401C51MOV[EBP-14],EDX
0167:00401C54MOVEAX,[EBP-30]
0167:00401C57MOVECX,[EAX+0000036C]
0167:00401C5DMOVEDX,[EBP-14]
0167:00401C60MOVBYTE PTR [EDX+ECX+0000F000],C2
0167:00401C68MOVEAX,[EBP-14]
0167:00401C6BADDEAX,01
0167:00401C6EMOV[EBP-14],EAX
0167:00401C71MOVECX,[EBP-30]
0167:00401C74MOVEDX,[ECX+0000036C]
0167:00401C7AMOVEAX,[EBP-14]
0167:00401C7DMOVBYTE PTR [EAX+EDX+0000F000],00
0167:00401C85MOVECX,[EBP-14]
0167:00401C88ADDECX,01
0167:00401C8BMOV[EBP-14],ECX
0167:00401C8EMOVEDX,[EBP-30]
0167:00401C91MOVEAX,[EDX+0000036C]
0167:00401C97MOVECX,[EBP-14]
0167:00401C9AMOVBYTE PTR [ECX+EAX+0000F000],00
0167:00401CA2MOVEDX,[EBP-14]
;以上代码为在映射代码偏移F000处写入指令CALL 0014:0000
0167:00401CA5ADDEDX,01
;指令 A91400C20000共6个字节
0167:00401CA8MOV[EBP-14],EDX ;
0167:00401CABMOVESI,0040213B
;要复制的代码的起始地址
0167:00401CB0MOVEDI,[EBP-08]
;要复制代码的目标地址(映射区域中)
0167:00401CB3MOVECX,00402688
;402688为要复制的代码的末地址
0167:00401CB8SUBECX,ESI
0167:00401CBAREPZMOVSB;将代码全部复制到映射区域
0167:00401CBCSGDTFWORD PTR [EBP-1C];这句开始就很关键了
0167:00401CC0LEAEAX,[EBP-001C]
0167:00401CC6MOVEAX,[EAX+02];取GDT线性基址
0167:00401CC9XOREBX,EBX
0167:00401CCBSLDTBX;取LDT在GDT中的偏移
0167:00401CCEANDBX,-08
0167:00401CD2ADDEAX,EBX
0167:00401CD4MOVECX,[EAX+02]
0167:00401CD7SHLECX,08
0167:00401CDAMOVCL,[EAX+07]
0167:00401CDDRORECX,08;以上计算出LDT线性基址
0167:00401CE0MOV[EBP-0C],ECX;保存
0167:00401CE3MOVEAX,[EBP-30]
0167:00401CE6MOVECX,[EBP-0C]
0167:00401CE9MOV[EAX+00000370],ECX
0167:00401CEFMOVEDX,[EBP-30]
0167:00401CF2MOVEAX,[EDX+0000036C]
0167:00401CF8MOVECX,[EBP-0C]
0167:00401CFBMOV[EAX+0000FE00],ECX
;将LDT线性基址保存至映射代码中
0167:00401D01MOVAX,CS
;得到当前代码段描述符号
0167:00401D04ANDAX,FFF8
0167:00401D08MOV[EBP-10],AX
0167:00401D0CMOVEDX,[EBP-10]
0167:00401D0FANDEDX,0000FFFF
;EDX为代码段描述符在LDT中的偏移量
0167:00401D15MOVEAX,[EBP-30]
0167:00401D18MOVECX,[EAX+00000370] ;ECX此时为LDT线性基址
0167:00401D1EMOVEAX,[EBP-30]
0167:00401D21MOVEAX,[EAX+00000370]
;EAX此时为LDT线性基址
0167:00401D27MOVESI,[EDX+ECX]
0167:00401D2AMOV[EAX+08],ESI
0167:00401D2DMOVECX,[EDX+ECX+04]
;以上将当前代码段描述符复制到
0167:00401D31MOV[EAX+0C],ECX;LDT第1项
0167:00401D34MOVEDX,[EBP-30]
0167:00401D37MOVEAX,[EDX+00000370]
0167:00401D3DMOVCL,[EAX+0D]
0167:00401D40ANDCL,9F
0167:00401D43MOVEDX,[EBP-30]
0167:00401D46MOVEAX,[EDX+00000370]
0167:00401D4CMOV[EAX+0D],CL
;以上修改LDT第1项的DPL为0,则当由调用门转到该段代码时即获得RING0权限
0167:00401D4FMOVEAX,[EBP-0C]
0167:00401D52ADDEAX,10;获得LDT中索引为2的调用门地址
0167:00401D55MOVEBX,0040213B
0167:00401D5AMOV[EAX],EBX
0167:00401D5CMOV[EAX+04],EBX
0167:00401D5FMOVWORD PTR [EAX+02],000C
0167:00401D65MOVWORD PTR [EAX+04],EC00;调用门修改完毕
0167:00401D6BMOVECX,[EBP-08]
0167:00401D6EMOVEDX,[WINMM!timeGetTime]
0167:00401D74MOV[ECX+0000FEE0]
;EDX;保存timeGetTime入口地址
...省略部分依次保存GetTickCount,GetMessageTime,timeSetEvent,SetTimer,
timeGetSystemTime,QueryPerformanceCounter入口地址
0167:00401DD2MOVECX,[EBP-08]
0167:00401DD5MOVEAX,[WINMM!timeGetTime]
0167:00401DDAMOVEBX,[EAX]
0167:00401DDCMOV[ECX+0000FE40],EBX
0167:00401DE2MOVEBX,[EAX+04]
0167:00401DE5MOV[ECX+0000FE44],EBX
;保存timeGetTime函数前8个字节指令
...省略部分依次保存GetTickCount,GetMessageTime,timeSetEvent,timeGetSystemTime , QueryPerformanceCounter前8个字节指令
0167:00401E6DMOVBYTE PTR [ECX+0000FE90],E9
0167:00401E74MOVEAX,00402165
0167:00401E79SUBEAX,0040213B
;EAX为截获代码在映射代码中的偏移
0167:00401E7EADDEAX,ECX;计算出截获代码的线性入口地址
0167:00401E80SUBEAX,[WINMM!timeGetTime]
0167:00401E86SUBEAX,05;JMP指令总长5个字节
0167:00401E89MOV[ECX+0000FE91],EAX
;计算生成从timeGetTime跳到截获代码的JMP指令并保存
...省略部分依次计算并生成GetTickCount,GetMessageTime,timeSetEvent,timeGetSystemTime , QueryPerformanceCounter跳到截获代码的JMP指令并保存
0167:00401F58CLI;关闭中断,谨防修改代码时发生意外
0167:00401F59MOVEAX,004021F3;
0167:00401F5ESUBEAX,0040213B;计算子程序在映射代码中的偏移
0167:00401F63ADDEAX,[EBP-08];EAX=8xxx 00B8
0167:00401F66PUSHEAX;传入参数EAX为修改timeGetTime代码的
;子程序入口地址
0167:00401F67MOVEAX,[EBP-08];调用8xxx 0000
0167:00401F6ACALLEAX;返回时timeGetTime首指令被更改
...省略部分依次修改GetTickCount,GetMessageTime,timeSetEvent,
timeGetSystemTime , QueryPerformanceCounter函数的首指令
0167:00401FFSETI;设置中断,初始化代码结束
二、截获时间函数部分(以timeGetTime为例子,代码以跟踪顺序列出)
timeGetTime
JMP 832A 002A
;这是timeGetTime被修改后的首指令
0167:832A 002ACLI
;此时[esp]=40BF2C,即游戏程序中调用timeGetTime函数的下一条指令
...(6个)各寄存器分别入栈 且MOV EBP,ESP
0167:832A 0033CALL832A 0038
;将当前EIP入栈(即下一条指令的地址)
0167:832A 0038POPEDI;取出当前指令地址
XORDI, DI
MOVESI , EDI
;将64K内存首地址赋给ESI
;此时ESI=EDI=832A 0000
ADDESI , 0040 2102
SUBESI , 0040 213B ;求出映射代码首地址
PUSHESI
0167:832A 004BCALLEDI;ESI为传进的参数
;返回时已经将timeGetTime代码还原
0167:832A 004DCALL832A 0052;
0167:832A 0052POPEDI
XORDI ,DI;故技重施
CALL[EDI + 0000FEED];调用原timeGetTime函数
SUBEAX,[EDI + 0000 FF30]
;减去第一次调用timeGetTime的结果
MULDWORD PTR [EDI+0000 FE30]
;乘以用户所指定的倍数
MOVEBX ,00100000
DIVEBX
;除以常数100000
ADDEAX ,[EDI+ 0000FE20]
MOVEAX,004021F3
SUBEAX,0040213B
ADDEAX,EDI
;以上指令为修改timeGetTime函数返回值
PUSHEAX
;EAX为传进的参数
CALLEDI
;返回时又将timeGetTime首指令换成JMP
...恢复各寄存器的值,EAX中为修改后的返回值
RET ;此时[ESP]=40BF2C,执行RET将返回到游戏中去
;
0167:832A 0000CALL832A 0005
0167:832A 0005POPEDI
XORDI ,DI;老套了撒^_^
MOVESI ,[EDI+0000 FE00]
;此地址保存着LDT的线性基址
MOVEAX,[ESP+04]
MOV[ESI +10],AX
SHREAX,10
MOV[ESI+16],AX
;以上代码将LDT中索引为2的调用门描述符的偏移改为传入的参数
...
MOVEAX,0000 0F00
CALLEAX
;调用子程序修改timeGetTime代码
0167:832A 0027RET4
;弹出参数,返回
;
0167:832A F000CALL0014:00000000
RET0
;
000C:832A 0097CALL832A 009C
000C:832A 009CPOPEDI
MOVEAX,[EDI+0000 FE40]
MOVEBX,[EDI+0000 FEE0]
MOV[EBX],EAX
MOVEAX,[EDI+0000 FE44]
MOV[EBX+04],EAX
RETF
注:EDI+0000 FE40起前8个字节为原timeGetTime函数的指令
EDI+0000 FEE0保存着timeGetTime函数的入口地址
以上即恢复timeGetTime前8个字节的代码
;
000C:832A 00B8CALL832A 00BD
000C:832A 00BDPOPEDI
XORDI ,DI
...
MOVEAX,[EDI+0000 FE90]
MOVEBX,[EDI+0000 FEE0]
MOV[EBX],EAX
MOVEAX,[EDI+0000FE94]
MOV[EBX+04],EAX
RETF
注:EDI+0000 FE90 起前8个字节保存着JMP 832A 002A 指令
是由“齿轮”初始化部分代码计算出来的,以上代码将JMP 832A 002A
写入timeGetTime函数
--
--
Be...Be...BeCOMe...