变速齿轮分析

各种变速齿轮基本上是通过hook一系列的时间函数进行的。
主要的hook方式有iat和inline。其中A变速器,采用的就是iat hook,其他的基本上都是使用inline hook的。
如果从驱动获取tickcount,可以部分inline hook方式的工具,而对于通过修改时钟的方式还不能。


使用Dos读取bios时间的方法
MOV       AH,0         ;GET   BIOS   TIME
INT   1AH
-----------------------
  THEN:
            CH:CL=HOUR:MINUTE
            DH:DL=SECOND:1/100SECOND
====================================
      MOV   AH,1                                     ;SET   BIOS   TIME
      CH:CL=HOUR:MINUTE
      DH:DL=SECOND:1/100SECOND
      INT   1AH

位于BIOS数据区的46CH处(双字区),可用中断1AH,功能0(即AH=0)获得,然后从返回的寄存器中获得
通过DOS调用2A也可以,或直接从CMOS中读取,先送索引给   port   70H   ,再读写用   port   71H 
     
设AH=2Ah,DOS功能调用int   21,之后CX=年(1980-2099),DH=月,DL=日,AL=星期
设AH=2Ch,DOS功能调用int   21,之后CH=时,CL=分,DH=秒,DL=1/100秒
mov   ah,2ah
int   21h

mov   ah,2ch
int   21h


masm611编译通过,看来没错
BIOS调用如下:
读系统时钟
mov     ah,0
int     1ah
CX:DX=时间计数
----------------
读CMOS系统日期
mov     ah,4
int     1ah
CH=世纪
CL=年
DH=月
DL=日
----------------
读CMOS时钟
mov     ah,2
int     1ah
CH=小时
CL=分
DH=秒




2月13日
计算机Bios时间
  RTC时钟是PC机主板上一块靠电池供电的时钟电路,它为整个计算机提供一个计时标准。由于该时钟是最原始最底层的时钟数据,因此解决了该平台的2000年问题,就彻底地解决了PC机硬件平台时钟2000年问题。遗憾的是现在许多计算机主板(包括目前流行的PII电脑,还有许多声称能解决Y2K问题的BIOS升级卡也只是简单地解决了BIOS时钟,而未对RTC时钟作任何处理,实际上也存在Y2K隐患)在设计时对该时钟生成电路均未作任何修改,主板CMOS仍沿用MC146818RTC时钟芯片,该芯片仅提供两位年份数据,故目前大部分PC机都存在RTC时钟问题(可用查虫程序进行测试),这也是PC硬件千年虫根源所在;
  BIOS时钟是指通过BIOS功能调用INT 1AH来取得的系统时钟数据,一般软件为兼容性的要求,都是通过该调用来取得时钟数据的,BIOS控制的时钟并不是真正意义上的时钟,它更应该被称为一个计数器,计数器每秒钟跳18.2下,并且每24小时清零(重置成零)一次。可以这么说,BIOS时钟里面并不存储日期信息,在PC机刚开始启动操作系统向BIOS读取日期和时间信息时,其实是通过调用BIOS里的中断功能来获取RTC芯片里的日期和时间信息。(注:有些应用程序也直接从I/O 端口地址读取RTC中的日期和时间信息);
  操作系统OS时钟是操作系统在开机时通过取得系统硬件时钟(RTC、BIOS)数据,然后往下计数而形成的,该时钟仅在开机时才有效,每次关机再重开时它都要去从硬件时钟处获得一个初始时钟值。 操作系统(包括DOS、Windows 3.1、Windows95、Windows NT等)的系统时钟一般也叫虚拟时钟,最重要的一点是,操作系统仅在刚开始启动时通过BIOS调用向RTC获取当前的日期和时间信息,此后只要系统不关闭,系统时钟便和RTC时钟完全脱离关系,系统时钟的时间信息(小时/分/秒/)主要靠BIOS里面控制的计数器来维持,它一般通过把计数器里存储的计数值除以18.2换算成秒再换算成“小时/分/秒”格式的时间信息,然后每过24小时增加一日,以此类推。

 
Linux操作系统内核的时钟中断机制

 摘要:本文主要从内核实现的角度分析了Linux 2.4.0内核的时钟中断、内核对时间的表示等。本文是为那些想要了解Linux I/O子系统的读者和Linux驱动程序开发人员而写的。
关键词:Linux、时钟、定时器

申明:这份文档是按照自由软件开放源代码的精神发布的,任何人可以免费获得、使用和重新发布,但是你没有限制别人重新发布你发布内容的权利。发布本文的目的是希望它能对读者有用,但没有任何担保,甚至没有适合特定目的的隐含的担保。更详细的情况请参阅GNU通用公共许可证(GPL),以及GNU自由文档协议(GFDL)。

你应该已经和文档一起收到一份GNU通用公共许可证(GPL)的副本。如果还没有,写信给:
The Free Software Foundation, Inc., 675 Mass Ave, Cambridge,MA02139, USA

欢迎各位指出文档中的错误与疑问。
前言
时间在一个操作系统内核中占据着重要的地位,它是驱动一个OS内核运行的“起博器”。一般说来,内核主要需要两种类型的时间:
1. 在内核运行期间持续记录当前的时间与日期,以便内核对某些对象和事件作时间标记(timestamp,也称为“时间戳”),或供用户通过时间syscall进行检索。
2. 维持一个固定周期的定时器,以提醒内核或用户一段时间已经过去了。
PC机中的时间是有三种时钟硬件提供的,而这些时钟硬件又都基于固定频率的晶体振荡器来提供时钟方波信号输入。这三种时钟硬件是:(1)实时时钟(Real Time Clock,RTC);(2)可编程间隔定时器(Programmable Interval Timer,PIT);(3)时间戳计数器(Time Stamp Counter,TSC)。

7.1 时钟硬件
7.1.1 实时时钟RTC
自从IBM PC AT起,所有的PC机就都包含了一个叫做实时时钟(RTC)的时钟芯片,以便在PC机断电后仍然能够继续保持时间。显然,RTC是通过主板上的电池来供电的,而不是通过PC机电源来供电的,因此当PC机关掉电源后,RTC仍然会继续工作。通常,CMOS RAM和RTC被集成到一块芯片上,因此RTC也称作“CMOS Timer”。最常见的RTC芯片是MC146818(Motorola)和DS12887(maxim),DS12887完全兼容于MC146818,并有一定的扩展。本节内容主要基于MC146818这一标准的RTC芯片。具体内容可以参考MC146818的Datasheet。

7.1.1.1 RTC寄存器
MC146818 RTC芯片一共有64个寄存器。它们的芯片内部地址编号为0x00~0x3F(不是I/O端口地址),这些寄存器一共可以分为三组:
(1)时钟与日历寄存器组:共有10个(0x00~0x09),表示时间、日历的具体信息。在PC机中,这些寄存器中的值都是以BCD格式来存储的(比如23dec=0x23BCD)。
(2)状态和控制寄存器组:共有4个(0x0A~0x0D),控制RTC芯片的工作方式,并表示当前的状态。
(3)CMOS配置数据:通用的CMOS RAM,它们与时间无关,因此我们不关心它。
时钟与日历寄存器组的详细解释如下:
Address Function
00 Current second for RTC
01 Alarm second
02 Current minute
03 Alarm minute
04 Current hour
05 Alarm hour
06 Current day of week(01=Sunday)
07 Current date of month
08 Current month
09 Current year(final two digits,eg:93)

状态寄存器A(地址0x0A)的格式如下:
其中:
(1)bit[7]——UIP标志(Update in Progress),为1表示RTC正在更新日历寄存器组中的值,此时日历寄存器组是不可访问的(此时访问它们将得到一个无意义的渐变值)。
(2)bit[6:4]——这三位是“除法器控制位”(divider-control bits),用来定义RTC的操作频率。各种可能的值如下:
Divider bits Time-base frequency Divider Reset Operation Mode
DV2 DV1 DV0
0 0 0 4.194304 MHZ NO YES
0 0 1 1.048576 MHZ NO YES
0 1 0 32.769 KHZ NO YES
1 1 0/1 任何 YES NO
PC机通常将Divider bits设置成“010”。
(3)bit[3:0]——速率选择位(Rate Selection bits),用于周期性或方波信号输出。
RS bits 4.194304或1.048578 MHZ 32.768 KHZ
RS3 RS2 RS1 RS0 周期性中断 方波 周期性中断 方波
0 0 0 0 None None None None
0 0 0 1 30.517μs 32.768 KHZ 3.90625ms 256 HZ
0 0 1 0 61.035μs 16.384 KHZ
0 0 1 1 122.070μs 8.192KHZ
0 1 0 0 244.141μs 4.096KHZ
0 1 0 1 488.281μs 2.048KHZ
0 1 1 0 976.562μs 1.024KHZ
0 1 1 1 1.953125ms 512HZ
1 0 0 0 3.90625ms 256HZ
1 0 0 1 7.8125ms 128HZ
1 0 1 0 15.625ms 64HZ
1 0 1 1 31.25ms 32HZ
1 1 0 0 62.5ms 16HZ
1 1 0 1 125ms 8HZ
1 1 1 0 250ms 4HZ
1 1 1 1 500ms 2HZ
PC机BIOS对其默认的设置值是“0110”。

状态寄存器B的格式如下所示:
各位的含义如下:
(1)bit[7]——SET标志。为1表示RTC的所有更新过程都将终止,用户程序随后马上对日历寄存器组中的值进行初始化设置。为0表示将允许更新过程继续。
(2)bit[6]——PIE标志,周期性中断使能标志。
(3)bit[5]——AIE标志,告警中断使能标志。
(4)bit[4]——UIE标志,更新结束中断使能标志。
(5)bit[3]——SQWE标志,方波信号使能标志。
(6)bit[2]——DM标志,用来控制日历寄存器组的数据模式,0=BCD,1=BINARY。BIOS总是将它设置为0。
(7)bit[1]——24/12标志,用来控制hour寄存器,0表示12小时制,1表示24小时制。PC机BIOS总是将它设置为1。
(8)bit[0]——DSE标志。BIOS总是将它设置为0。

状态寄存器C的格式如下:
(1)bit[7]——IRQF标志,中断请求标志,当该位为1时,说明寄存器B中断请求发生。
(2)bit[6]——PF标志,周期性中断标志,为1表示发生周期性中断请求。
(3)bit[5]——AF标志,告警中断标志,为1表示发生告警中断请求。
(4)bit[4]——UF标志,更新结束中断标志,为1表示发生更新结束中断请求。

状态寄存器D的格式如下:
(1)bit[7]——VRT标志(Valid RAM and Time),为1表示OK,为0表示RTC已经掉电。
(2)bit[6:0]——总是为0,未定义。

7.1.1.2 通过I/O端口访问RTC
在PC机中可以通过I/O端口0x70和0x71来读写RTC芯片中的寄存器。其中,端口0x70是RTC的寄存器地址索引端口,0x71是数据端口。
读RTC芯片寄存器的步骤是:
mov al, addr
out 70h, al ; Select reg_addr in RTC chip
jmp $+2 ; a slight delay to settle thing
in al, 71h ;
写RTC寄存器的步骤如下:
mov al, addr
out 70h, al ; Select reg_addr in RTC chip
jmp $+2 ; a slight delay to settle thing
mov al, value
out 71h, al

7.1.2 可编程间隔定时器PIT
每个PC机中都有一个PIT,以通过IRQ0产生周期性的时钟中断信号。当前使用最普遍的是Intel 8254 PIT芯片,它的I/O端口地址是0x40~0x43。
Intel 8254 PIT有3个计时通道,每个通道都有其不同的用途:
(1) 通道0用来负责更新系统时钟。每当一个时钟滴答过去时,它就会通过IRQ0向系统产生一次时钟中断。
(2) 通道1通常用于控制DMAC对RAM的刷新。
(3) 通道2被连接到PC机的扬声器,以产生方波信号。
每个通道都有一个向下减小的计数器,8254 PIT的输入时钟信号的频率是1193181HZ,也即一秒钟输入1193181个clock-cycle。每输入一个clock-cycle其时间通道的计数器就向下减1,一直减到0值。因此对于通道0而言,当他的计数器减到0时,PIT就向系统产生一次时钟中断,表示一个时钟滴答已经过去了。当各通道的计数器减到0时,我们就说该通道处于“Terminal count”状态。
通道计数器的最大值是10000h,所对应的时钟中断频率是1193181/(65536)=18.2HZ,也就是说,此时一秒钟之内将产生18.2次时钟中断。

7.1.2.1 PIT的I/O端口
在i386平台上,8254芯片的各寄存器的I/O端口地址如下:
Port Description
40h Channel 0 counter(read/write)
41h Channel 1 counter(read/write)
42h Channel 2 counter(read/write)
43h PIT control word(write only)
其中,由于通道0、1、2的计数器是一个16位寄存器,而相应的端口却都是8位的,因此读写通道计数器必须进行进行两次I/O端口读写操作,分别对应于计数器的高字节和低字节,至于是先读写高字节再读写低字节,还是先读写低字节再读写高字节,则由PIT的控制寄存器来决定。8254 PIT的控制寄存器的格式如下:
(1)bit[7:6]——Select Counter,选择对那个计数器进行操作。“00”表示选择Counter 0,“01”表示选择Counter 1,“10”表示选择Counter 2,“11”表示Read-Back Command(仅对于8254,对于8253无效)。
(2)bit[5:4]——Read/Write/Latch格式位。“00”表示锁存(Latch)当前计数器的值;“01”只读写计数器的高字节(MSB);“10”只读写计数器的低字节(LSB);“11”表示先读写计数器的LSB,再读写MSB。
(3)bit[3:1]——Mode bits,控制各通道的工作模式。“000”对应Mode 0;“001”对应Mode 1;“010”对应Mode 2;“011”对应Mode 3;“100”对应Mode 4;“101”对应Mode 5。
(4)bit[0]——控制计数器的存储模式。0表示以二进制格式存储,1表示计数器中的值以BCD格式存储。

7.1.2.2 PIT通道的工作模式
PIT各通道可以工作在下列6种模式下:
1. Mode 0:当通道处于“Terminal count”状态时产生中断信号。
2. Mode 1:Hardware retriggerable one-shot。
3. Mode 2:Rate Generator。这种模式典型地被用来产生实时时钟中断。此时通道的信号输出管脚OUT初始时被设置为高电平,并以此持续到计数器的值减到1。然后在接下来的这个clock-cycle期间,OUT管脚将变为低电平,直到计数器的值减到0。当计数器的值被自动地重新加载后,OUT管脚又变成高电平,然后重复上述过程。通道0通常工作在这个模式下。
4. Mode 3:方波信号发生器。
5. Mode 4:Software triggered strobe。
6. Mode 5:Hardware triggered strobe。

7.1.2.3 锁存计数器(Latch Counter)
当控制寄存器中的bit[5:4]设置成0时,将把当前通道的计数器值锁存。此时通过I/O端口可以读到一个稳定的计数器值,因为计数器表面上已经停止向下计数(PIT芯片内部并没有停止向下计数)。NOTE!一旦发出了锁存命令,就要马上读计数器的值。

7.1.3 时间戳记数器TSC
从Pentium开始,所有的Intel 80x86 CPU就都又包含一个64位的时间戳记数器(TSC)的寄存器。该寄存器实际上是一个不断增加的计数器,它在CPU的每个时钟信号到来时加1(也即每一个clock-cycle输入CPU时,该计数器的值就加1)。
汇编指令rdtsc可以用于读取TSC的值。利用CPU的TSC,操作系统通常可以得到更为精准的时间度量。假如clock-cycle的频率是400MHZ,那么TSC就将每2.5纳秒增加一次。
7.2 Linux内核对RTC的编程
MC146818 RTC芯片(或其他兼容芯片,如DS12887)可以在IRQ8上产生周期性的中断,中断的频率在2HZ~8192HZ之间。与MC146818 RTC对应的设备驱动程序实现在include/linux/rtc.h和drivers/char/rtc.c文件中,对应的设备文件是/dev/rtc(major=10,minor=135,只读字符设备)。因此用户进程可以通过对她进行编程以使得当RTC到达某个特定的时间值时激活 IRQ8线,从而将RTC当作一个闹钟来用。
而Linux内核对RTC的唯一用途就是把RTC用作“离线”或“后台”的时间与日期维护器。当Linux内核启动时,它从RTC中读取时间与日期的基准值。然后再运行期间内核就完全抛开RTC,从而以软件的形式维护系统的当前时间与日期,并在需要时将时间回写到RTC芯片中。
Linux在include/linux/mc146818rtc.h和include/asm-i386/mc146818rtc.h头文件中分别定义了mc146818 RTC芯片各寄存器的含义以及RTC芯片在i386平台上的I/O端口操作。而通用的RTC接口则声明在include/linux/rtc.h头文件中。





7.2.1 RTC芯片的I/O端口操作
Linux在include/asm-i386/mc146818rtc.h头文件中定义了RTC芯片的I/O端口操作。端口0x70被称为“RTC端口0”,端口0x71被称为“RTC端口1”,如下所示:
#ifndef RTC_PORT
#define RTC_PORT(x) (0x70 + (x))
#define RTC_ALWAYS_BCD 1 /* RTC operates in binary mode */
#endif
显然,RTC_PORT(0)就是指端口0x70,RTC_PORT(1)就是指I/O端口0x71。
端口0x70被用作RTC芯片内部寄存器的地址索引端口,而端口0x71则被用作RTC芯片内部寄存器的数据端口。再读写一个RTC寄存器之前,必须先把该寄存器在RTC芯片内部的地址索引值写到端口0x70中。根据这一点,读写一个RTC寄存器的宏定义CMOS_READ()和 CMOS_WRITE()如下:
#define CMOS_READ(addr) ({
outb_p((addr),RTC_PORT(0));
inb_p(RTC_PORT(1));
})
#define CMOS_WRITE(val, addr) ({
outb_p((addr),RTC_PORT(0));
outb_p((val),RTC_PORT(1));
})
#define RTC_IRQ 8
在上述宏定义中,参数addr是RTC寄存器在芯片内部的地址值,取值范围是0x00~0x3F,参数val是待写入寄存器的值。宏RTC_IRQ是指RTC芯片所连接的中断请求输入线号,通常是8。

7.2.2 对RTC寄存器的定义
Linux在include/linux/mc146818rtc.h这个头文件中定义了RTC各寄存器的含义。

(1)寄存器内部地址索引的定义
Linux内核仅使用RTC芯片的时间与日期寄存器组和控制寄存器组,地址为0x00~0x09之间的10个时间与日期寄存器的定义如下:
#define RTC_SECONDS 0
#define RTC_SECONDS_ALARM 1
#define RTC_MINUTES 2
#define RTC_MINUTES_ALARM 3
#define RTC_HOURS 4
#define RTC_HOURS_ALARM 5
/* RTC_*_alarm is always true if 2 MSBs are set */
# define RTC_ALARM_DONT_CARE 0xC0

#define RTC_DAY_OF_WEEK 6
#define RTC_DAY_OF_MONTH 7
#define RTC_MONTH 8
#define RTC_YEAR 9

四个控制寄存器的地址定义如下:
#define RTC_REG_A 10
#define RTC_REG_B 11
#define RTC_REG_C 12
#define RTC_REG_D 13

(2)各控制寄存器的状态位的详细定义
控制寄存器A(0x0A)主要用于选择RTC芯片的工作频率,因此也称为RTC频率选择寄存器。因此Linux用一个宏别名RTC_FREQ_SELECT来表示控制寄存器A,如下:
#define RTC_FREQ_SELECT RTC_REG_A
RTC频率寄存器中的位被分为三组:①bit[7]表示UIP标志;②bit[6:4]用于除法器的频率选择;③bit[3:0]用于速率选择。它们的定义如下:
# define RTC_UIP 0x80
# define RTC_DIV_CTL 0x70
/* Periodic intr. / Square wave rate select. 0=none, 1=32.8kHz,... 15=2Hz */
# define RTC_RATE_SELECT 0x0F
正如7.1.1.1节所介绍的那样,bit[6:4]有5中可能的取值,分别为除法器选择不同的工作频率或用于重置除法器,各种可能的取值如下定义所示:
/* divider control: refclock values 4.194 / 1.049 MHz / 32.768 kHz */
# define RTC_REF_CLCK_4MHZ 0x00
# define RTC_REF_CLCK_1MHZ 0x10
# define RTC_REF_CLCK_32KHZ 0x20
/* 2 values for divider stage reset, others for "testing purposes only" */
# define RTC_DIV_RESET1 0x60
# define RTC_DIV_RESET2 0x70

寄存器B中的各位用于使能/禁止RTC的各种特性,因此控制寄存器B(0x0B)也称为“控制寄存器”,Linux用宏别名RTC_CONTROL来表示控制寄存器B,它与其中的各标志位的定义如下所示:
#define RTC_CONTROL RTC_REG_B
# define RTC_SET 0x80 /* disable updates for clock setting */
# define RTC_PIE 0x40 /* periodic interrupt enable */
# define RTC_AIE 0x20 /* alarm interrupt enable */
# define RTC_UIE 0x10 /* update-finished interrupt enable */
# define RTC_SQWE 0x08 /* enable square-wave output */
# define RTC_DM_BINARY 0x04 /* all time/date values are BCD if clear */
# define RTC_24H 0x02 /* 24 hour mode - else hours bit 7 means pm */
# define RTC_DST_EN 0x01 /* auto switch DST - works f. USA only */

寄存器C是RTC芯片的中断请求状态寄存器,Linux用宏别名RTC_INTR_FLAGS来表示寄存器C,它与其中的各标志位的定义如下所示:
#define RTC_INTR_FLAGS RTC_REG_C
/* caution - cleared by read */
# define RTC_IRQF 0x80 /* any of the following 3 is active */
# define RTC_PF 0x40
# define RTC_AF 0x20
# define RTC_UF 0x10

寄存器D仅定义了其最高位bit[7],以表示RTC芯片是否有效。因此寄存器D也称为RTC的有效寄存器。Linux用宏别名RTC_VALID来表示寄存器D,如下:
#define RTC_VALID RTC_REG_D
# define RTC_VRT 0x80 /* valid RAM and time */

(3)二进制格式与BCD格式的相互转换
由于时间与日期寄存器中的值可能以BCD格式存储,也可能以二进制格式存储,因此需要定义二进制格式与BCD格式之间的相互转换宏,以方便编程。如下:
#ifndef BCD_TO_BIN
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)
#endif

#ifndef BIN_TO_BCD
#define BIN_TO_BCD(val) ((val)=(((val)/10)<<4) + (val)%10)
#endif

7.2.3 内核对RTC的操作
如前所述,Linux内核与RTC进行互操作的时机只有两个:(1)内核在启动时从RTC中读取启动时的时间与日期;(2)内核在需要时将时间与日期回写到RTC中。为此,Linux内核在arch/i386/kernel/time.c文件中实现了函数get_cmos_time()来进行对 RTC的第一种操作。显然,get_cmos_time()函数仅仅在内核启动时被调用一次。而对于第二种操作,Linux则同样在 arch/i386/kernel/time.c文件中实现了函数set_rtc_mmss(),以支持向RTC中回写当前时间与日期。下面我们将来分析这二个函数的实现。
在分析get_cmos_time()函数之前,我们先来看看RTC芯片对其时间与日期寄存器组的更新原理。

(1)Update In Progress
当控制寄存器B中的SET标志位为0时,MC146818芯片每秒都会在芯片内部执行一个“更新周期”(Update Cycle),其作用是增加秒寄存器的值,并检查秒寄存器是否溢出。如果溢出,则增加分钟寄存器的值,如此一致下去直到年寄存器。在“更新周期”期间,时间与日期寄存器组(0x00~0x09)是不可用的,此时如果读取它们的值将得到未定义的值,因为MC146818在整个更新周期期间会把时间与日期寄存器组从CPU总线上脱离,从而防止软件程序读到一个渐变的数据。
在MC146818的输入时钟频率(也即晶体增荡器的频率)为4.194304MHZ或1.048576MHZ的情况下,“更新周期”需要花费 248us,而对于输入时钟频率为32.768KHZ的情况,“更新周期”需要花费1984us=1.984ms。控制寄存器A中的UIP标志位用来表示 MC146818是否正处于更新周期中,当UIP从0变为1的那个时刻,就表示MC146818将在稍后马上就开更新周期。在UIP从0变到1的那个时刻与MC146818真正开始Update Cycle的那个时刻之间时有一段时间间隔的,通常是244us。也就是说,在UIP从0变到1的244us之后,时间与日期寄存器组中的值才会真正开始改变,而在这之间的244us间隔内,它们的值并不会真正改变。如下图所示:

(2)get_cmos_time()函数
该函数只被内核的初始化例程time_init()和内核的APM模块所调用。其源码如下:
/* not static: needed by APM */
unsigned long get_cmos_time(void)
{
unsigned int year, mon, day, hour, min, sec;
int i;

/* The Linux interpretation of the CMOS clock register contents:
* When the Update-In-Progress (UIP) flag goes from 1 to 0, the
* RTC registers show the second which has precisely just started.
* Let's hope other operating systems interpret the RTC the same way.
*/
/* read RTC exactly on falling edge of update flag */
for (i = 0 ; i < 1000000 ; i++) /* may take up to 1 second... */
if (CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP)
break;
for (i = 0 ; i < 1000000 ; i++) /* must try at least 2.228 ms */
if (!(CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP))
break;
do { /* Isn't this overkill ? UIP above should guarantee consistency */
sec = CMOS_READ(RTC_SECONDS);
min = CMOS_READ(RTC_MINUTES);
hour = CMOS_READ(RTC_HOURS);
day = CMOS_READ(RTC_DAY_OF_MONTH);
mon = CMOS_READ(RTC_MONTH);
year = CMOS_READ(RTC_YEAR);
} while (sec != CMOS_READ(RTC_SECONDS));
if (!(CMOS_READ(RTC_CONTROL) & RTC_DM_BINARY) || RTC_ALWAYS_BCD)
{
BCD_TO_BIN(sec);
BCD_TO_BIN(min);
BCD_TO_BIN(hour);
BCD_TO_BIN(day);
BCD_TO_BIN(mon);
BCD_TO_BIN(year);
}
if ((year += 1900) < 1970)
year += 100;
return mktime(year, mon, day, hour, min, sec);
}
对该函数的注释如下:
(1)在从RTC中读取时间时,由于RTC存在Update Cycle,因此软件发出读操作的时机是很重要的。对此,get_cmos_time()函数通过UIP标志位来解决这个问题:第一个for循环不停地读取RTC频率选择寄存器中的UIP标志位,并且只要读到UIP的值为1就马上退出这个for循环。第二个for循环同样不停地读取UIP标志位,但他只要一读到UIP的值为0就马上退出这个for循环。这两个for循环的目的就是要在软件逻辑上同步RTC的Update Cycle,显然第二个for循环最大可能需要2.228ms(TBUC+max(TUC)=244us+1984us=2.228ms)
(2)从第二个for循环退出后,RTC的Update Cycle已经结束。此时我们就已经把当前时间逻辑定准在RTC的当前一秒时间间隔内。也就是说,这是我们就可以开始从RTC寄存器中读取当前时间值。但是要注意,读操作应该保证在244us内完成(准确地说,读操作要在RTC的下一个更新周期开始之前完成,244us的限制是过分偏执的:-)。所以, get_cmos_time()函数接下来通过CMOS_READ()宏从RTC中依次读取秒、分钟、小时、日期、月份和年分。这里的do{}while (sec!=CMOS_READ(RTC_SECOND))循环就是用来确保上述6个读操作必须在下一个Update Cycle开始之前完成。
(3)接下来判定时间的数据格式,PC机中一般总是使用BCD格式的时间,因此需要通过BCD_TO_BIN()宏把BCD格式转换为二进制格式。
(4)接下来对年分进行修正,以将年份转换为“19XX”的格式,如果是1970以前的年份,则将其加上100。
(5)最后调用mktime()函数将当前时间与日期转换为相对于1970-01-01 00:00:00的秒数值,并将其作为函数返回值返回。

函数mktime()定义在include/linux/time.h头文件中,它用来根据Gauss算法将以 year/mon/day/hour/min/sec(如1980-12-31 23:59:59)格式表示的时间转换为相对于1970-01-01 00:00:00这个UNIX时间基准以来的相对秒数。其源码如下:
static inline unsigned long
mktime (unsigned int year, unsigned int mon,
unsigned int day, unsigned int hour,
unsigned int min, unsigned int sec)
{
if (0 >= (int) (mon -= 2)) { /* 1..12 -> 11,12,1..10 */
mon += 12; /* Puts Feb last since it has leap day */
year -= 1;
}

return (((
(unsigned long) (year/4 - year/100 + year/400 + 367*mon/12 + day) +
year*365 - 719499
)*24 + hour /* now have hours */
)*60 + min /* now have minutes */
)*60 + sec; /* finally seconds */
}

(3)set_rtc_mmss()函数
该函数用来更新RTC中的时间,它仅有一个参数nowtime,是以秒数表示的当前时间,其源码如下:
static int set_rtc_mmss(unsigned long nowtime)
{
int retval = 0;
int real_seconds, real_minutes, cmos_minutes;
unsigned char save_control, save_freq_select;

/* gets recalled with irq locally disabled */
spin_lock(&rtc_lock);
save_control = CMOS_READ(RTC_CONTROL); /* tell the clock it's being set */
CMOS_WRITE((save_control|RTC_SET), RTC_CONTROL);

save_freq_select = CMOS_READ(RTC_FREQ_SELECT); /* stop and reset prescaler */
CMOS_WRITE((save_freq_select|RTC_DIV_RESET2), RTC_FREQ_SELECT);

cmos_minutes = CMOS_READ(RTC_MINUTES);
if (!(save_control & RTC_DM_BINARY) || RTC_ALWAYS_BCD)
BCD_TO_BIN(cmos_minutes);

/*
* since we're only adjusting minutes and seconds,
* don't interfere with hour overflow. This avoids
* messing with unknown time zones but requires your
* RTC not to be off by more than 15 minutes
*/
real_seconds = nowtime % 60;
real_minutes = nowtime / 60;
if (((abs(real_minutes - cmos_minutes) + 15)/30) & 1)
real_minutes += 30; /* correct for half hour time zone */
real_minutes %= 60;

if (abs(real_minutes - cmos_minutes) < 30) {
if (!(save_control & RTC_DM_BINARY) || RTC_ALWAYS_BCD) {
BIN_TO_BCD(real_seconds);
BIN_TO_BCD(real_minutes);
}
CMOS_WRITE(real_seconds,RTC_SECONDS);
CMOS_WRITE(real_minutes,RTC_MINUTES);
} else {
printk(KERN_WARNING
"set_rtc_mmss: can't update from %d to %d/n",
cmos_minutes, real_minutes);
retval = -1;
}

/* The following flags have to be released exactly in this order,
* otherwise the DS12887 (popular MC146818A clone with integrated
* battery and quartz) will not reset the oscillator and will not
* update precisely 500 ms later. You won't find this mentioned in
* the Dallas Semiconductor data sheets, but who believes data
* sheets anyway ... -- Markus Kuhn
*/
CMOS_WRITE(save_control, RTC_CONTROL);
CMOS_WRITE(save_freq_select, RTC_FREQ_SELECT);
spin_unlock(&rtc_lock);

return retval;
}
对该函数的注释如下:
(1)首先对自旋锁rtc_lock进行加锁。定义在arch/i386/kernel/time.c文件中的全局自旋锁rtc_lock用来串行化所有CPU对RTC的操作。
(2)接下来,在RTC控制寄存器中设置SET标志位,以便通知RTC软件程序随后马上将要更新它的时间与日期。为此先把RTC_CONTROL 寄存器的当前值读到变量save_control中,然后再把值(save_control | RTC_SET)回写到寄存器RTC_CONTROL中。
(3)然后,通过RTC_FREQ_SELECT寄存器中bit[6:4]重启RTC芯片内部的除法器。为此,类似地先把 RTC_FREQ_SELECT寄存器的当前值读到变量save_freq_select中,然后再把值(save_freq_select | RTC_DIV_RESET2)回写到RTC_FREQ_SELECT寄存器中。
(4)接着将RTC_MINUTES寄存器的当前值读到变量cmos_minutes中,并根据需要将它从BCD格式转化为二进制格式。
(5)从nowtime参数中得到当前时间的秒数和分钟数。分别保存到real_seconds和real_minutes变量。注意,这里对于半小时区的情况要修正分钟数real_minutes的值。
(6)然后,在real_minutes与RTC_MINUTES寄存器的原值cmos_minutes二者相差不超过30分钟的情况下,将 real_seconds和real_minutes所表示的时间值写到RTC的秒寄存器和分钟寄存器中。当然,在回写之前要记得把二进制转换为BCD格式。
(7)最后,恢复RTC_CONTROL寄存器和RTC_FREQ_SELECT寄存器原来的值。这二者的先后次序是:先恢复RTC_CONTROL寄存器,再恢复RTC_FREQ_SELECT寄存器。然后在解除自旋锁rtc_lock后就可以返回了。

最后,需要说明的一点是,set_rtc_mmss()函数尽可能在靠近一秒时间间隔的中间位置(也即500ms处)左右被调用。此外, Linux内核对每一次成功的更新RTC时间都留下时间轨迹,它用一个系统全局变量last_rtc_update来表示内核最近一次成功地对RTC进行更新的时间(单位是秒数)。该变量定义在arch/i386/kernel/time.c文件中:
/* last time the cmos clock got updated */
static long last_rtc_update;
每一次成功地调用set_rtc_mmss()函数后,内核都会马上将last_rtc_update更新为当前时间(具体请见7.4.3节)



 

kernel32

GetTickCount
QueryPerformanceCount
GetLocalTime
GetSystemTime
Sleep


User32

GetMessageTime
SetTimer


Winmm

timeGetTimer
timeSetEvent


变速齿轮工作原理解密

[ 转自铁血社区 http://bbs.tiexue.net/ ]



绝大部分游戏里面即时都会用到使用的定时器,变速齿轮之所欲可以给游戏加速,就是改变了定时器的性质。

有两种办法可以改变系统定时器。

1)API代码注入,通过注入自己的代码,使得API跳转到自己的代码处运行




计时API函数有GETTICKCOUNT和TIMEGETTIME,它是windows系统函数,如果可以找到他们的未知,修改成对自己有利的形式,就达到了加速的目的。




下面以GETTICKCOUNT为例进行分析:原本的GETTICKCOUNT汇编:

kernel32!gettickcount mov gs,[bffcaea18]

[ 转自铁血社区 http://bbs.tiexue.net/ ]



mov eax,gs:[00000000]




sub edx,edx




mov gs,dx




ret




变速齿轮修改后的GETTICKCOUNT汇编:




kernel32!gettickcount

[ 转自铁血社区 http://bbs.tiexue.net/ ]



这里是关键-->jmp 840500d9(840500d9并不是绝对的)




add [eax],al




add [ecx+00000000],ah




sub edx,edx




mov gs,dx




ret

[ 转自铁血社区 http://bbs.tiexue.net/ ]



可以看出变速齿轮修改了gettickcount的代码,当游戏和程序使用gettickcount时就会自动跳转到840500d9处执行。




再看?40500d9处的代码汇编:




840500d9:CLI




push ebp




mov ebp,esp




push ebx

[ 转自铁血社区 http://bbs.tiexue.net/ ]



push ecx




push edx




push esi




push edi




call 840500e7




840500e7:pop edi

[ 转自铁血社区 http://bbs.tiexue.net/ ]



xor di,di




mov esi,edi




add esi,00402051




sub esi,00401f0b




push esi




call edi

[ 转自铁血社区 http://bbs.tiexue.net/ ]



call 84050101




84050101:pop edi




xor di,di




call [edi+0000fef0]




call 84050110




84050110:sub eax,[edi+0000ff30]

[ 转自铁血社区 http://bbs.tiexue.net/ ]



mul dword,ptr[edi+0000ff30]




mov ebx,00100000




div ebx




add eax,[edi+0000fe20]




push eax




mov eax,00402072

[ 转自铁血社区 http://bbs.tiexue.net/ ]



sub eax,00401f08




add eax,edi




push eax




call edi




pop eax




pop edi

[ 转自铁血社区 http://bbs.tiexue.net/ ]



pop esi




pop edx




pop ecx




pop ebx




pop ebp




sil

[ 转自铁血社区 http://bbs.tiexue.net/ ]



ret




以上正是变速齿轮变速的核心所在。(GETTICKCOUNT返回的是EAX的值你可以对EAX进行跟踪)




  下面说一下变速齿轮挂接API的方法:首先变速齿轮在MMF区(WIN9X/ME)申请一块内存,把上面的代码从程序中移到该内存。使用修改描述符的方法从应用程序级跳到核心级,修改GETTICKCOUNT开头的代码使之指向申请的内存的首地址实现挂接




这是普遍采用的一种方式。




2)还有一种方式。从驱动级修改。

在win95 /98时代,我们是可以直接操作IO端口的,win NT以后,包括2000,XP系统,当我们试图操作IO端口,改变硬件设置的时候,系统会拒绝或者引起冲突。事实上在386以后的CPU上,用户程序都是运行在保护模式下,用户对IO端口对设备的访问都要经过操作系统的管理和监督。用户程序是无法直接对设备操作的。

[ 转自铁血社区 http://bbs.tiexue.net/ ]
那么在window下,要访问设备怎么办?windows提供了2中办法:

第一,通过修改 EFLAGS 寄存器中的特权级别(IOPL)实现

第二,任务状态段(TSS)中的IO许可映射BIt位




Windows NT下,只使用了2种等级的特权级,0特权级和3特权级。用户程序运行在3特权级,设备驱动和系统内核运行在0特权级。只有系统内核程序和设备驱动才被允许直接操作IO端口。所有的用户程序在访问IO端口前,都需要得到设备驱动的准入,低信任级别的程序是无法访问的。




IO bit映射就是为了使得那些低信任级别的程序能够访问设备IO而设置的。




在NT系统下,直接操作IO端口就有两种方式,一是,编写设备驱动,运行在0特权级直接操作IO设备端口。另外一种可行的办法就是修改IO bit映射。

[ 转自铁血社区 http://bbs.tiexue.net/ ]
我们建议采用第一种办法,通过工作在0特权级的驱动访问IO端口。




这里有写好的设备驱动,PortTalk.sys,并提供了对外的接口,下面是用c语言写的,通过这个驱动对IO访问的一个例子




#include

#include

#include




void __cdecl main(void)

[ 转自铁血社区 http://bbs.tiexue.net/ ]
{

unsigned char value;

printf("IoExample for PortTalk V2.0/nCopyright 2001 Craig Peacock//n");

OpenPortTalk();

outportb(0x378, 0xFF);

value = inportb(0x378);

[ 转自铁血社区 http://bbs.tiexue.net/ ]
printf("Value returned = 0x%02X /n",value);

outp(0x378, 0xAA);

value = inp(0x378);

printf("Value returned = 0x%02X /n",value);

ClosePortTalk();

}

[ 转自铁血社区 http://bbs.tiexue.net/ ]






这里我根据查到的信息和自己的理解,用VC6编写的一个sample,在2000系统下测试通过的。




porttalk22.zip 是原版的,C语言写的




IOCtrl.rar 是改造后的,VC6编写的MFC版本,可以读写指定IO端口




当然你在运行前先解压缩porttalk22 ,运行里面的porttalk.reg,和AllowIO进行注册。




里面有详细的源码。 porttalk22 (66.41 KB) porttalk22 (66.41 KB)





“变速齿轮”再研究
      作者        BBBKOM 
      关键词     变速齿轮  调用门  RING0

    提起“变速齿轮”(以下简称“齿轮”)这个软件,大家应该都知道吧,该软件号称
是全球第一款能改变游戏速度的程序。我起初用时觉得很神奇,久而久之就不禁思考其实现原理了,但苦于个人水平有限,始终不得其解,成了长驻于脑中挥散不去的大问号。
   偶然一天在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函数的执行情况。
     我先写了个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:0   call 5
              0:5   pop 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:00401B0E  PUSH      00
                  0167:00401B10  PUSH      00010000
                  0167:00401B15  PUSH      00
                  0167:00401B17  PUSH      04
                  0167:00401B19  PUSH      00
                  0167:00401B1B  PUSH      FF
                  0167:00401B1D  CALL      [KERNEL32!CreateFileMappingA]
   ;调用CreateFileMappingA
   ; 调用形式如右:CreateFileMappingA(FF,0,4,0,10000,0)
                  0167:00401B23  MOV       ECX,[EBP-30]
                  0167:00401B26  MOV       [ECX+00000368],EAX
                  0167:00401B2C  MOV       DWORD PTR [EBP-14],80000000
                  0167:00401B33  JMP       00401B41
                  0167:00401B35  MOV       EDX,[EBP-14]
                  0167:00401B38  ADD       EDX,00010000
  ;申请基址加0x10000
                  0167:00401B3E  MOV       [EBP-14],EDX
                  0167:00401B41  MOV       EAX,[EBP-14]
                  0167:00401B44  PUSH      EAX      ;映射文件基址
                  0167:00401B45  PUSH      00       ;映射的字节数
                  0167:00401B47  PUSH      00       ;文件偏移低32位
                  0167:00401B49  PUSH      00       ;文件偏移高32位
                  0167:00401B4B  PUSH      02       ;访问模式
                  0167:00401B4D  MOV       ECX,[EBP-30]
                  0167:00401B50  MOV       EDX,[ECX+00000368]
                  0167:00401B56  PUSH      EDX
  ;CreateFileMappingA返回的映射文件句柄
                  0167:00401B57  CALL      [KERNEL32!MapViewOfFileEx]
  ; 调用形式如右:MapViewOfFileEx(EDX,2,0,0,0,EAX)
                  0167:00401B5D  MOV       ECX,[EBP-30]
  ;[EBP-30]为即将映射到2G之上
                  0167:00401B60  MOV       [ECX+0000036C],EAX
  ; 的代码的数据域的起始地址
                  0167:00401B66  MOV       EDX,[EBP-30]
                  0167:00401B69  CMP       DWORD PTR [EDX+0000036C],00
  ;检查MapViewOfFileEx
                  0167:00401B70  JZ          00401B74
                ;返回值,若为0则继续调
                  0167:00401B72  JMP       00401B76   ;调用MapViewOfFileEx
                  0167:00401B74  JMP       00401B35   ;直至成功为止
                  0167:00401B76  MOV       EAX,[EBP-30]
                  0167:00401B79  MOV       ECX,[EAX+0000036C]
                  0167:00401B7F  MOV       [EBP-08],ECX
  ;映射文件起始地址存入[EBP-08]
                  0167:00401B82  CALL      [WINMM!timeGetTime]
                  0167:00401B88  MOV       [EBP-14],EAX
  ;将初次调用timeGetTime
                 0167:00401BA0  MOV       ECX,[EBP-08]
  ;的返回值保存到[EBP-14]
                 0167:00401BA3  MOV       EDX,[EBP-14]
  ;以及映射文件基址+FF30处
                 0167:00401BA6  MOV       [ECX+0000FF30],EDX
 ...省略的代码类似的保存调用初次GetTickCount,QueryPerformanceCounter的返回值
 
                 0167:00401BED  MOV       DWORD PTR [EBP-14],00000000
                 0167:00401BF4  MOV       EDX,[EBP-30]
                 0167:00401BF7  MOV       EAX,[EDX+0000036C]
                 0167:00401BFD  MOV       ECX,[EBP-14]
                 0167:00401C00  MOV       BYTE PTR [ECX+EAX+0000F000],9A
  ;9a为远调用的指令码
                 0167:00401C08  MOV       EDX,[EBP-14]
                 0167:00401C0B  ADD       EDX,01
                 0167:00401C0E  MOV       [EBP-14],EDX
                 0167:00401C11  MOV       EAX,[EBP-14]
                 0167:00401C14  ADD       EAX,04
                 0167:00401C17  MOV       [EBP-14],EAX
                 0167:00401C1A  MOV       ECX,[EBP-30]
                 0167:00401C1D  MOV       EDX,[ECX+0000036C]
                 0167:00401C23  MOV       EAX,[EBP-14]
                 0167:00401C26  MOV       BYTE PTR [EAX+EDX+0000F000],14
  ;14为调用门描述符的索引
                 0167:00401C2E  MOV       ECX,[EBP-14]
                 0167:00401C31  ADD       ECX,01
                 0167:00401C34  MOV       [EBP-14],ECX
                 0167:00401C37  MOV       EDX,[EBP-30]
                 0167:00401C3A  MOV       EAX,[EDX+0000036C]
                 0167:00401C40  MOV       ECX,[EBP-14]
                 0167:00401C43  MOV       BYTE PTR [ECX+EAX+0000F000],00
  ;CALL指令其他部分
                 0167:00401C4B  MOV       EDX,[EBP-14]
                 0167:00401C4E  ADD       EDX,01
                 0167:00401C51  MOV       [EBP-14],EDX
                 0167:00401C54  MOV       EAX,[EBP-30]
                 0167:00401C57  MOV       ECX,[EAX+0000036C]
                 0167:00401C5D  MOV       EDX,[EBP-14]
                 0167:00401C60  MOV       BYTE PTR [EDX+ECX+0000F000],C2
                 0167:00401C68  MOV       EAX,[EBP-14]
                 0167:00401C6B  ADD       EAX,01
                 0167:00401C6E  MOV       [EBP-14],EAX
                 0167:00401C71  MOV       ECX,[EBP-30]
                 0167:00401C74  MOV       EDX,[ECX+0000036C]
                 0167:00401C7A  MOV       EAX,[EBP-14]
                 0167:00401C7D  MOV       BYTE PTR [EAX+EDX+0000F000],00
                 0167:00401C85  MOV       ECX,[EBP-14]
                 0167:00401C88  ADD       ECX,01
                 0167:00401C8B  MOV       [EBP-14],ECX
                 0167:00401C8E  MOV       EDX,[EBP-30]
                 0167:00401C91  MOV       EAX,[EDX+0000036C]
                 0167:00401C97  MOV       ECX,[EBP-14]
                 0167:00401C9A  MOV       BYTE PTR [ECX+EAX+0000F000],00
                 0167:00401CA2  MOV       EDX,[EBP-14]
  ;以上代码为在映射代码偏移F000处写入指令CALL 0014:0000
                 0167:00401CA5  ADD       EDX,01
  ;指令 A91400C20000共6个字节
                 0167:00401CA8  MOV       [EBP-14],EDX ;
                 0167:00401CAB  MOV       ESI,0040213B
  ;要复制的代码的起始地址
                 0167:00401CB0  MOV       EDI,[EBP-08]
  ;要复制代码的目标地址(映射区域中)
                 0167:00401CB3  MOV       ECX,00402688
  ;402688为要复制的代码的末地址
                 0167:00401CB8  SUB       ECX,ESI
                 0167:00401CBA  REPZ MOVSB  ;将代码全部复制到映射区域
                 0167:00401CBC  SGDT      FWORD PTR [EBP-1C]  ;这句开始就很关键了
                 0167:00401CC0  LEA       EAX,[EBP-001C]
                 0167:00401CC6  MOV       EAX,[EAX+02]        ;取GDT线性基址
                 0167:00401CC9  XOR       EBX,EBX
                 0167:00401CCB  SLDT      BX                  ;取LDT在GDT中的偏移
                 0167:00401CCE  AND       BX,-08
                 0167:00401CD2  ADD       EAX,EBX
                 0167:00401CD4  MOV       ECX,[EAX+02]
                 0167:00401CD7  SHL       ECX,08
                 0167:00401CDA  MOV       CL,[EAX+07]
                 0167:00401CDD  ROR       ECX,08             ;以上计算出LDT线性基址
                 0167:00401CE0  MOV       [EBP-0C],ECX       ;保存
                 0167:00401CE3  MOV       EAX,[EBP-30]
                 0167:00401CE6  MOV       ECX,[EBP-0C]
                 0167:00401CE9  MOV       [EAX+00000370],ECX
                 0167:00401CEF  MOV       EDX,[EBP-30]
                 0167:00401CF2  MOV       EAX,[EDX+0000036C]
                 0167:00401CF8  MOV       ECX,[EBP-0C]
                 0167:00401CFB  MOV       [EAX+0000FE00],ECX
   ;将LDT线性基址保存至映射代码中
                 0167:00401D01  MOV       AX,CS
   ;得到当前代码段描述符号
                 0167:00401D04  AND       AX,FFF8
                 0167:00401D08  MOV       [EBP-10],AX
                 0167:00401D0C  MOV       EDX,[EBP-10]
                 0167:00401D0F  AND       EDX,0000FFFF
  ;EDX为代码段描述符在LDT中的偏移量
                 0167:00401D15  MOV       EAX,[EBP-30]
                 0167:00401D18  MOV    ECX,[EAX+00000370] ;ECX此时为LDT线性基址                     0167:00401D1E  MOV       EAX,[EBP-30]
                 0167:00401D21  MOV     EAX,[EAX+00000370]

;EAX此时为LDT线性基址          

                 0167:00401D27  MOV       ESI,[EDX+ECX]
                 0167:00401D2A  MOV       [EAX+08],ESI
                 0167:00401D2D  MOV       ECX,[EDX+ECX+04]
  ;以上将当前代码段描述符复制到
                 0167:00401D31  MOV       [EAX+0C],ECX    ;LDT第1项
                 0167:00401D34  MOV       EDX,[EBP-30]
                 0167:00401D37  MOV       EAX,[EDX+00000370]
                 0167:00401D3D  MOV       CL,[EAX+0D]
                 0167:00401D40  AND       CL,9F
                 0167:00401D43  MOV       EDX,[EBP-30]
                 0167:00401D46  MOV       EAX,[EDX+00000370]
                 0167:00401D4C  MOV       [EAX+0D],CL
  ;以上修改LDT第1项的DPL为0,则当由调用门转到该段代码时即获得RING0权限
                 0167:00401D4F  MOV       EAX,[EBP-0C]
                 0167:00401D52  ADD       EAX,10       ;获得LDT中索引为2的调用门地址
                 0167:00401D55  MOV       EBX,0040213B
                 0167:00401D5A  MOV       [EAX],EBX
                 0167:00401D5C  MOV       [EAX+04],EBX
                 0167:00401D5F  MOV       WORD PTR [EAX+02],000C
                 0167:00401D65  MOV       WORD PTR [EAX+04],EC00  ;调用门修改完毕
                 0167:00401D6B  MOV       ECX,[EBP-08]
                 0167:00401D6E  MOV       EDX,[WINMM!timeGetTime]
                 0167:00401D74  MOV       [ECX+0000FEE0]

;EDX;保存timeGetTime入口地址
      ...省略部分依次保存GetTickCount,GetMessageTime,timeSetEvent,SetTimer,
            timeGetSystemTime,QueryPerformanceCounter入口地址
                 0167:00401DD2  MOV       ECX,[EBP-08]
                 0167:00401DD5  MOV       EAX,[WINMM!timeGetTime]
                 0167:00401DDA  MOV       EBX,[EAX]
                 0167:00401DDC  MOV       [ECX+0000FE40],EBX
                 0167:00401DE2  MOV       EBX,[EAX+04]
                 0167:00401DE5  MOV       [ECX+0000FE44],EBX
                                   ;保存timeGetTime函数前8个字节指令
          ...省略部分依次保存GetTickCount,GetMessageTime,timeSetEvent,
            timeGetSystemTime , QueryPerformanceCounter前8个字节指令
                 0167:00401E6D  MOV       BYTE PTR [ECX+0000FE90],E9
                 0167:00401E74  MOV       EAX,00402165
                 0167:00401E79  SUB       EAX,0040213B
            ;EAX为截获代码在映射代码中的偏移
                 0167:00401E7E  ADD       EAX,ECX    ;计算出截获代码的线性入口地址
                 0167:00401E80  SUB       EAX,[WINMM!timeGetTime]
                 0167:00401E86  SUB       EAX,05     ;JMP指令总长5个字节
                 0167:00401E89  MOV       [ECX+0000FE91],EAX
            ;计算生成从timeGetTime跳到截获代码的JMP指令并保存
 
       ...省略部分依次计算并生成GetTickCount,GetMessageTime,timeSetEvent,
        timeGetSystemTime , QueryPerformanceCounter跳到截获代码的JMP指令
        并保存
 
                 0167:00401F58  CLI    ;关闭中断,谨防修改代码时发生意外
                 0167:00401F59  MOV       EAX,004021F3         ;
                 0167:00401F5E  SUB       EAX,0040213B;计算子程序在映射代码中的偏移
                 0167:00401F63  ADD       EAX,[EBP-08]          ;EAX=8xxx 00B8
                 0167:00401F66  PUSH      EAX    ;传入参数EAX为修改timeGetTime代码的
                                                           ;子程序入口地址
                 0167:00401F67  MOV       EAX,[EBP-08]          ;调用8xxx 0000
                 0167:00401F6A  CALL      EAX       ;返回时timeGetTime首指令被更改
 
          ...省略部分依次修改GetTickCount,GetMessageTime,timeSetEvent,
            timeGetSystemTime , QueryPerformanceCounter函数的首指令
 
                 0167:00401FF   SETI      ;设置中断,初始化代码结束
           二、截获时间函数部分(以timeGetTime为例子,代码以跟踪顺序列出)
           timeGetTime
                        JMP 832A 002A
          ;这是timeGetTime被修改后的首指令
                 0167:832A 002A         CLI
          ;此时[esp]=40BF2C,即游戏程序中调用timeGetTime函数的下一条指令
          ...(6个)各寄存器分别入栈 且MOV EBP,ESP
                 0167:832A 0033         CALL   832A 0038
          ;将当前EIP入栈(即下一条指令的地址)
                 0167:832A 0038         POP    EDI       ;取出当前指令地址
                                                 XOR    DI   , DI
                                                 MOV   ESI , EDI
         ;将64K内存首地址赋给ESI
         ;此时ESI=EDI=832A 0000
                                                 ADD    ESI , 0040 2102
                                                 SUB    ESI , 0040 213B ;求出映射代码首地址
                                                 PUSH  ESI
                 0167:832A 004B        CALL  EDI        ;ESI为传进的参数
                                           ;返回时已经将timeGetTime代码还原
                 0167:832A 004D       CALL  832A 0052    ;
                 0167:832A 0052        POP   EDI
                                                XOR   DI ,DI        ;故技重施
                                               CALL  [EDI + 0000FEED];调用原timeGetTime函数
                                               SUB   EAX,[EDI + 0000 FF30]
        ;减去第一次调用timeGetTime的结果
                                              MUL    DWORD PTR [EDI+0000 FE30]
        ;乘以用户所指定的倍数
                                              MOV    EBX ,00100000
                                              DIV    EBX
        ;除以常数100000
                                             ADD    EAX ,[EDI+ 0000FE20]
                                            MOV   EAX,004021F3
                                            SUB    EAX,0040213B
                                            ADD    EAX,EDI
        ;以上指令为修改timeGetTime函数返回值
                                            PUSH  EAX
        ;EAX为传进的参数
                                            CALL   EDI
        ;返回时又将timeGetTime首指令换成JMP
        ...恢复各寄存器的值,EAX中为修改后的返回值
                                            RET ;此时[ESP]=40BF2C,执行RET将返回到游戏中去
        ;
                 0167:832A 0000           CALL 832A 0005
                 0167:832A 0005           POP  EDI
                                                   XOR  DI ,DI            ;老套了撒^_^
                                                   MOV ESI ,[EDI+0000 FE00]
        ;此地址保存着LDT的线性基址
                                                    MOV EAX,[ESP+04]
                                                    MOV [ESI +10],AX
                                                    SHR  EAX,10
                                                    MOV [ESI+16],AX
        ;以上代码将LDT中索引为2的调用门描述符的偏移改为传入的参数
         ...
                                                    MOV EAX,0000 0F00
                                                    CALL EAX
        ;调用子程序修改timeGetTime代码
         0167:832A 0027           RET 4
        ;弹出参数,返回
        ;
                 0167:832A F000           CALL 0014:00000000
                                                    RET 0
        ;
                 000C:832A 0097           CALL 832A 009C
                 000C:832A 009C           POP EDI
                                                    MOV EAX,[EDI+0000 FE40]
                                                    MOV EBX,[EDI+0000 FEE0]
                                                    MOV [EBX],EAX
                                                    MOV EAX,[EDI+0000 FE44]
                                                    MOV [EBX+04],EAX
                                                    RETF
        注:EDI+0000 FE40起前8个字节为原timeGetTime函数的指令
            EDI+0000 FEE0保存着timeGetTime函数的入口地址
            以上即恢复timeGetTime前8个字节的代码
        ;
                 000C:832A 00B8         CALL 832A 00BD
                 000C:832A 00BD         POP EDI
                                                  XOR DI ,DI
         ...
                                                  MOV EAX,[EDI+0000 FE90]
                                                  MOV EBX,[EDI+0000 FEE0]
                                                  MOV [EBX],EAX
                                                  MOV EAX,[EDI+0000FE94]
                                                  MOV [EBX+04],EAX
                                                  RETF

        注:EDI+0000 FE90 起前8个字节保存着JMP 832A 002A 指令
            是由“齿轮”初始化部分代码计算出来的,以上代码将JMP 832A 002A
            写入timeGetTime函数
--




我看“变速齿轮”的工作原理 

  最近,我在“大众软件”上看到一则关于软件“变速齿轮”的报道。我很少上网,所以对这方面了解比较少,不知道它在网上已经流行好几个月了。当时的感觉就是太惊奇了,很佩服王荣先生是怎么做到如此神奇的事,尤其是他如何保证各种游戏的兼容,他如何知道不同游戏对时间的处理。我立刻上网DOWN了个0.22b版,在试用的过程中,我发现“变速齿轮”不但可以加速游戏,实际上,它可以加速任何windows程序(从某种程度上),我逐渐认识到它的工作原理,不一定对,仅是猜测而已。先声明一点,我是使用UNIX的,并不是很了解windows编程,所以只能给出概念上大体的认识,而无法说出具体的实现办法。

  首先,先看看计算机是如何有时间概念的。在主板上有一个时钟晶振,依靠电池供电,本质上就是一块电子表。计算机软件中所有的时间概念,归根结底都来自着个“硬件时间”,换句话说,如果这个时间不对,则任何运行在该主板上的程序对时间的处理都不可能正确。(包扩各种操作系统)

  那么,操作系统是如何知道这个时间的呢?这是因为这个时钟每过一定时间都会产生一个硬件中断(INT)操作系统可以截取这个中断并做相应的处理,从而获得时间的概念。好象是20ms产生一次中断,一秒钟50次。具体的中断号我忘了,就称为 INT A 吧。对Dos而言,它在启动时就准备好了对INT A的中断处理程序(Dos核心的一部分),当我们使用DOs的时候,在提示符状态下,即使不做任何操作,Dos内部在一秒钟时间内,也会接受50次INT A,执行50次中断处理程序。只是这一过程在幕后完成,我们无法感受到。Dos的中断处理程序所做的,就是让Dos能够了解当前的时间。(如保留当前日期,时间在内部变量中等操作)。但很重要的一点,在INT A中断处理程序的末尾,又调用INT B。
INT B是Dos为用户保留的软中断,在缺省情况下,Dos的INT B中断处理程序立即返回,不做任何事。而
用户可以编写自己的INT B中断处理程序定时处理自己的操作,然后把它替换Dos原来的空INT B中断程序。比如我有一个程序需要在12:00运行,一种方法是写如下代码:
for(;;){
if(时间是12:00) break;
sleep(5); /*休息5秒钟,这句在Dos中没有,意为让程序不做任何事,只是等待一段时间*/
}
{具体的处理程序}

  然后在提示符状态下运行。因为Dos没有多用户的概念,在程序12:00退出以前,无法在使用这台机器。另一种方法是,把要运行的程序写成TSR(长驻内存程序),运行后执行代码长驻在内存中,程序本身返回提示符,供用户使用。那么该执行码如何保证在12:00被执行呢,就要靠INT B,在长驻该程序时,也要编写新的INT B中断处理程序,内容大概是:
{屏蔽INT B中断} /*这一步是因为Dos的大部分中断是不可重入的,
即在中断还未处理完时,再次*/
/*被中断,这一般会让Dos死掉*/
{执行旧的INT B中断处理程序} /*这一步的目的是防止自己的TSR影响其他TSR程序*/
if(时间是12:00){
把处理转向TSR程序的入口;
} else {
{恢复被屏蔽的中断}
退出;
}
综合上面讲到的,Dos下的时间处理大概是:

机器时间------->INT A(操作系统用)-------->INT B(用户用)-------->TSR程序

当然,由于Dos对运行级别几乎没有控制,用户也可以绕过INT A,INT B直接访问硬件。这时我们即使改变操作系统的时间,用户程序也能得到正确的时间。
在windows中,情况也很类似。但程序不会直接访问硬件,而是通过叫VxD的虚拟设备驱动程序来工作。由VxD来和硬件打交道,而应用程序只和VxD交流信息。对时钟中断的处理也是一样,windows有专门的时钟虚拟设备驱动来捕获来自晶振的硬件中断,并为windows提供时间和定时的功能。这与Dos下的INT A功能基本是一致的,但更强大,功能更广。
我们在来看应用程序需要怎样的时间处理机制。一个典型的游戏,如射击游戏,如果没有时间控制,敌人的飞机如果要连开10枪,程序应该是:
for(i=0;i<10;i++){
开枪;
}
但是有一个问题,机器的速度可能太快,以至于只需要0.1秒十枪就完成了,另外在不同的机器上,这段程会有不同的运行时间。为了解决这个问题,我们改进这段程序:
for(i=0;i<10;i++){
开枪;
sleep(1); /*休息1秒,不做任何事*/
}
这样我们至少保证一秒只开一枪。但还有一个问题,就是可能屏幕上有多个敌人,不可能在一个敌人开10枪的过程中其他敌人不动(而且自己也不动)。所以我认为,一般的游戏程序都是用定时器来实现主要功能的。即先为每一种动作编写相应的处理程序,如开枪,移动等,在为每个对象申请一个定时器,一旦定时器的时间到,就激活该对象相应动作的程序代码。如屏幕上的十个敌人对应十个定时器,定时器互不干涉,哪个时间到转向哪个处理程序,控制他是否该移动或射击。至于定时器的创建,由应用程序向系统申请。

|--->用户定时器1------->用户程序1
|--->用户定时器2------->用户程序2
机器时间------>windows的时钟VxD---|--->用户定时器3------->用户程序3
|--->系统接口------->系统应用

  这种机制可以保证星际争霸中的小狗在P100上和在PIII上奔跑的速度一样快,前提是这两台机器的硬件时间是一样准的,但是,如果有一台机器的时钟快或慢了,那问题就有变化了。(注意,这里的快或慢,并不是指两台机器的时间不一样,而是指在相同的现实时间下,他们产生硬件中断的次数不一样)

  那么,说了这么多,我认为“变速齿轮”的工作原理,就是修改用户申请的windows定时器中的等待时间。我不太了解windows编程,不好说这种修改是如何实现的。他没有修改VxD因为windows系统的时间并没有因为启动“变速齿轮”而跑快或跑慢,某些应用,如双击桌面图标时的间隔时间上的设定(即两次击鼠标的间隔时间多长以内才算是“双击”)也没有变。通俗的描述是:在启动“变速齿轮”后,当应用程序(特别是游戏)向windows申请定时器时,“变速齿轮”修改了申请的等待时间参数,因次改变了程序正常的定时,才使程序有了不正长的
速度(这个结论只是猜的)。只所以这样猜,是因为对已经启动的程序,他并不能改变速度,而只能先启动“变速齿轮”,再运行程序。另外,某些系统接口,也是无法修改的。

  所以,一旦启动“变速齿轮”,所有应用程序(申请了定时器,并要依靠定时器来做一些操作的程序)都会受到他的影响。在Word中,等待输入的光标会因为调成了加速而更快的闪动,各种提示信息的出现时间会变快(或变慢)很多,最夸张的是,当把速度调成最慢时,在同一位置,间隔十秒钟击一次鼠标会被系统认为是双击(发生在应用程序内,而不是桌面上)

  我很佩服王荣先生的想象力和编程能力。“变速齿轮”的出现,证明在虚拟的世界里:没有做不到的,只有想不到的。
 




“变速齿轮”研究手记,变速齿轮,逆向工程技术2009-12-10 23:56注:为节省篇幅,本文对一些计算机术语直接使用而没有作详细的解释,读者若有不熟悉之处,建议参考清华大学出版社出版,周明德编著的《微型计算机系统原理及应用》一书中关于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函数的执行情况。

我先写了个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:0 call 5

0:5 pop 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:00401b0e push 00

0167:00401b10 push 00010000

0167:00401b15 push 00

0167:00401b17 push 04

0167:00401b19 push 00

0167:00401b1b push ff

0167:00401b1d call [kernel32!createfilemappinga]

;调用createfilemappinga

; 调用形式如右:createfilemappinga(ff,0,4,0,10000,0)

0167:00401b23 mov ecx,[ebp-30]

0167:00401b26 mov [ecx 00000368],eax

0167:00401b2c mov dword ptr [ebp-14],80000000

0167:00401b33 jmp 00401b41

0167:00401b35 mov edx,[ebp-14]

0167:00401b38 add edx,00010000

;申请基址加0x10000

0167:00401b3e mov [ebp-14],edx

0167:00401b41 mov eax,[ebp-14]

0167:00401b44 push eax ;映射文件基址

0167:00401b45 push 00 ;映射的字节数

0167:00401b47 push 00 ;文件偏移低32位

0167:00401b49 push 00 ;文件偏移高32位

0167:00401b4b push 02 ;访问模式

0167:00401b4d mov ecx,[ebp-30]

0167:00401b50 mov edx,[ecx 00000368]

0167:00401b56 push edx

;createfilemappinga返回的映射文件句柄

0167:00401b57 call [kernel32!mapviewoffileex]

; 调用形式如右:mapviewoffileex(edx,2,0,0,0,eax)

0167:00401b5d mov ecx,[ebp-30]

;[ebp-30]为即将映射到2g之上

0167:00401b60 mov [ecx 0000036c],eax

; 的代码的数据域的起始地址

0167:00401b66 mov edx,[ebp-30]

0167:00401b69 cmp dword ptr [edx 0000036c],00

;检查mapviewoffileex

0167:00401b70 jz 00401b74

;返回值,若为0则继续调

0167:00401b72 jmp 00401b76 ;调用mapviewoffileex

0167:00401b74 jmp 00401b35 ;直至成功为止

0167:00401b76 mov eax,[ebp-30]

0167:00401b79 mov ecx,[eax 0000036c]

0167:00401b7f mov [ebp-08],ecx

;映射文件起始地址存入[ebp-08]

0167:00401b82 call [winmm!timegettime]

0167:00401b88 mov [ebp-14],eax

;将初次调用timegettime

0167:00401ba0 mov ecx,[ebp-08]

;的返回值保存到[ebp-14]

0167:00401ba3 mov edx,[ebp-14]

;以及映射文件基址 ff30处

0167:00401ba6 mov [ecx 0000ff30],edx

...省略的代码类似的保存调用初次gettickcount,queryperformancecounter的返回值



0167:00401bed mov dword ptr [ebp-14],00000000

0167:00401bf4 mov edx,[ebp-30]

0167:00401bf7 mov eax,[edx 0000036c]

0167:00401bfd mov ecx,[ebp-14]

0167:00401c00 mov byte ptr [ecx eax 0000f000],9a

;9a为远调用的指令码

0167:00401c08 mov edx,[ebp-14]

0167:00401c0b add edx,01

0167:00401c0e mov [ebp-14],edx

0167:00401c11 mov eax,[ebp-14]

0167:00401c14 add eax,04

0167:00401c17 mov [ebp-14],eax

0167:00401c1a mov ecx,[ebp-30]

0167:00401c1d mov edx,[ecx 0000036c]

0167:00401c23 mov eax,[ebp-14]

0167:00401c26 mov byte ptr [eax edx 0000f000],14

;14为调用门描述符的索引

0167:00401c2e mov ecx,[ebp-14]

0167:00401c31 add ecx,01

0167:00401c34 mov [ebp-14],ecx

0167:00401c37 mov edx,[ebp-30]

0167:00401c3a mov eax,[edx 0000036c]

0167:00401c40 mov ecx,[ebp-14]

0167:00401c43 mov byte ptr [ecx eax 0000f000],00

;call指令其他部分

0167:00401c4b mov edx,[ebp-14]

0167:00401c4e add edx,01

0167:00401c51 mov [ebp-14],edx

0167:00401c54 mov eax,[ebp-30]

0167:00401c57 mov ecx,[eax 0000036c]

0167:00401c5d mov edx,[ebp-14]

0167:00401c60 mov byte ptr [edx ecx 0000f000],c2

0167:00401c68 mov eax,[ebp-14]

0167:00401c6b add eax,01

0167:00401c6e mov [ebp-14],eax

0167:00401c71 mov ecx,[ebp-30]

0167:00401c74 mov edx,[ecx 0000036c]

0167:00401c7a mov eax,[ebp-14]

0167:00401c7d mov byte ptr [eax edx 0000f000],00

0167:00401c85 mov ecx,[ebp-14]

0167:00401c88 add ecx,01

0167:00401c8b mov [ebp-14],ecx

0167:00401c8e mov edx,[ebp-30]

0167:00401c91 mov eax,[edx 0000036c]

0167:00401c97 mov ecx,[ebp-14]

0167:00401c9a mov byte ptr [ecx eax 0000f000],00

0167:00401ca2 mov edx,[ebp-14]

;以上代码为在映射代码偏移f000处写入指令call 0014:0000

0167:00401ca5 add edx,01

;指令 a91400c20000共6个字节

0167:00401ca8 mov [ebp-14],edx ;

0167:00401cab mov esi,0040213b

;要复制的代码的起始地址

0167:00401cb0 mov edi,[ebp-08]

;要复制代码的目标地址(映射区域中)

0167:00401cb3 mov ecx,00402688

;402688为要复制的代码的末地址

0167:00401cb8 sub ecx,esi

0167:00401cba repz movsb ;将代码全部复制到映射区域

0167:00401cbc sgdt fword ptr [ebp-1c] ;这句开始就很关键了

0167:00401cc0 lea eax,[ebp-001c]

0167:00401cc6 mov eax,[eax 02] ;取gdt线性基址

0167:00401cc9 xor ebx,ebx

0167:00401ccb sldt bx ;取ldt在gdt中的偏移

0167:00401cce and bx,-08

0167:00401cd2 add eax,ebx

0167:00401cd4 mov ecx,[eax 02]

0167:00401cd7 shl ecx,08

0167:00401cda mov cl,[eax 07]

0167:00401cdd ror ecx,08 ;以上计算出ldt线性基址

0167:00401ce0 mov [ebp-0c],ecx ;保存

0167:00401ce3 mov eax,[ebp-30]

0167:00401ce6 mov ecx,[ebp-0c]

0167:00401ce9 mov [eax 00000370],ecx

0167:00401cef mov edx,[ebp-30]

0167:00401cf2 mov eax,[edx 0000036c]

0167:00401cf8 mov ecx,[ebp-0c]

0167:00401cfb mov [eax 0000fe00],ecx

;将ldt线性基址保存至映射代码中

0167:00401d01 mov ax,cs

;得到当前代码段描述符号

0167:00401d04 and ax,fff8

0167:00401d08 mov [ebp-10],ax

0167:00401d0c mov edx,[ebp-10]

0167:00401d0f and edx,0000ffff

;edx为代码段描述符在ldt中的偏移量

0167:00401d15 mov eax,[ebp-30]

0167:00401d18 mov ecx,[eax 00000370] ;ecx此时为ldt线性基址 0167:00401d1e mov eax,[ebp-30]

0167:00401d21 mov eax,[eax 00000370] ;eax此时为ldt线性基址


0167:00401d27 mov esi,[edx ecx]

0167:00401d2a mov [eax 08],esi

0167:00401d2d mov ecx,[edx ecx 04]

;以上将当前代码段描述符复制到

0167:00401d31 mov [eax 0c],ecx ;ldt第1项

0167:00401d34 mov edx,[ebp-30]

0167:00401d37 mov eax,[edx 00000370]

0167:00401d3d mov cl,[eax 0d]

0167:00401d40 and cl,9f

0167:00401d43 mov edx,[ebp-30]

0167:00401d46 mov eax,[edx 00000370]

0167:00401d4c mov [eax 0d],cl

;以上修改ldt第1项的dpl为0,则当由调用门转到该段代码时即获得ring0权限

0167:00401d4f mov eax,[ebp-0c]

0167:00401d52 add eax,10 ;获得ldt中索引为2的调用门地址

0167:00401d55 mov ebx,0040213b

0167:00401d5a mov [eax],ebx

0167:00401d5c mov [eax 04],ebx

0167:00401d5f mov word ptr [eax 02],000c

0167:00401d65 mov word ptr [eax 04],ec00 ;调用门修改完毕

0167:00401d6b mov ecx,[ebp-08]

0167:00401d6e mov edx,[winmm!timegettime]

0167:00401d74 mov [ecx 0000fee0]


;edx;保存timegettime入口地址

...省略部分依次保存gettickcount,getmessagetime,timesetevent,settimer,

timegetsystemtime,queryperformancecounter入口地址

0167:00401dd2 mov ecx,[ebp-08]

0167:00401dd5 mov eax,[winmm!timegettime]

0167:00401dda mov ebx,[eax]

0167:00401ddc mov [ecx 0000fe40],ebx

0167:00401de2 mov ebx,[eax 04]

0167:00401de5 mov [ecx 0000fe44],ebx

;保存timegettime函数前8个字节指令

...省略部分依次保存gettickcount,getmessagetime,timesetevent,

timegetsystemtime , queryperformancecounter前8个字节指令

0167:00401e6d mov byte ptr [ecx 0000fe90],e9

0167:00401e74 mov eax,00402165

0167:00401e79 sub eax,0040213b

;eax为截获代码在映射代码中的偏移

0167:00401e7e add eax,ecx ;计算出截获代码的线性入口地址

0167:00401e80 sub eax,[winmm!timegettime]

0167:00401e86 sub eax,05 ;jmp指令总长5个字节

0167:00401e89 mov [ecx 0000fe91],eax

;计算生成从timegettime跳到截获代码的jmp指令并保存



...省略部分依次计算并生成gettickcount,getmessagetime,timesetevent,

timegetsystemtime , queryperformancecounter跳到截获代码的jmp指令

并保存



0167:00401f58 cli ;关闭中断,谨防修改代码时发生意外

0167:00401f59 mov eax,004021f3 ;

0167:00401f5e sub eax,0040213b;计算子程序在映射代码中的偏移

0167:00401f63 add eax,[ebp-08] ;eax=8xxx 00b8

0167:00401f66 push eax ;传入参数eax为修改timegettime代码的

;子程序入口地址

0167:00401f67 mov eax,[ebp-08] ;调用8xxx 0000

0167:00401f6a call eax ;返回时timegettime首指令被更改



...省略部分依次修改gettickcount,getmessagetime,timesetevent,

timegetsystemtime , queryperformancecounter函数的首指令



0167:00401ff seti ;设置中断,初始化代码结束

二、截获时间函数部分(以timegettime为例子,代码以跟踪顺序列出)

timegettime

jmp 832a 002a

;这是timegettime被修改后的首指令

0167:832a 002a cli

;此时[esp]=40bf2c,即游戏程序中调用timegettime函数的下一条指令

...(6个)各寄存器分别入栈 且mov ebp,esp

0167:832a 0033 call 832a 0038

;将当前eip入栈(即下一条指令的地址)

0167:832a 0038 pop edi ;取出当前指令地址

xor di , di

mov esi , edi

;将64k内存首地址赋给esi

;此时esi=edi=832a 0000

add esi , 0040 2102

sub esi , 0040 213b ;求出映射代码首地址

push esi

0167:832a 004b call edi ;esi为传进的参数

;返回时已经将timegettime代码还原

0167:832a 004d call 832a 0052 ;

0167:832a 0052 pop edi

xor di ,di ;故技重施

call [edi 0000feed];调用原timegettime函数

sub eax,[edi 0000 ff30]

;减去第一次调用timegettime的结果

mul dword ptr [edi 0000 fe30]

;乘以用户所指定的倍数

mov ebx ,00100000

div ebx

;除以常数100000

add eax ,[edi 0000fe20]

mov eax,004021f3

sub eax,0040213b

add eax,edi

;以上指令为修改timegettime函数返回值

push eax

;eax为传进的参数

call edi

;返回时又将timegettime首指令换成jmp

...恢复各寄存器的值,eax中为修改后的返回值

ret ;此时[esp]=40bf2c,执行ret将返回到游戏中去

;

0167:832a 0000 call 832a 0005

&a, mp;n, bsp; 0167:832a 0005 pop edi

xor di ,di ;老套了撒^_^

mov esi ,[edi 0000 fe00]

;此地址保存着ldt的线性基址

mov eax,[esp 04]

mov [esi 10],ax

shr eax,10

mov [esi 16],ax

;以上代码将ldt中索引为2的调用门描述符的偏移改为传入的参数

...

mov eax,0000 0f00

call eax

;调用子程序修改timegettime代码

0167:832a 0027 ret 4

;弹出参数,返回

;

0167:832a f000 call 0014:00000000

ret 0

;

000c:832a 0097 call 832a 009c

000c:832a 009c pop edi

mov eax,[edi 0000 fe40]

mov ebx,[edi 0000 fee0]

mov [ebx],eax

mov eax,[edi 0000 fe44]

mov [ebx 04],eax

retf

注:edi 0000 fe40起前8个字节为原timegettime函数的指令

edi 0000 fee0保存着timegettime函数的入口地址

以上即恢复timegettime前8个字节的代码

;

000c:832a 00b8 call 832a 00bd

000c:832a 00bd pop edi

xor di ,di

...

mov eax,[edi 0000 fe90]

mov ebx,[edi 0000 fee0]

mov [ebx],eax

mov eax,[edi 0000fe94]

mov [ebx 04],eax

retf


注:edi 0000 fe90 起前8个字节保存着jmp 832a 002a 指令

是由“齿轮”初始化部分代码计算出来的,以上代码将jmp 832a 002a

写入timegettime函数

 




  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值