C 程序字号的修改
声明
个人可以自由转载本文,不过应保持原文的完整性,并通知我;商业转载先请和我联系。
本文没有任何明确或不明确地提示说本文完全正确,阅读和使用本文的内容是您自己的选择,本人不负任何责任。
如果您发现本文有错漏的地方,请您给我指出;如果有什么不理解的,请您给我提出。
意见、建议和提出的问题最好写在我的主页 http://llf.126.com 的留言版上。
前言
我写了两篇关于 C 程序的非资源格式的字号的文章,和伟通信,说是看不太懂,我要先说一下,对于这种非资源格式的字号的修改,其实是建立在修改程序的基础上的,而且因程序员的不同,所得到的程序设置字号的方法可能也是千变万化的,而编译器的优化,使其代码变化更多,所以并不存在一种固定的格式,不大可能在对汇编语言丝毫不懂的情况下完成修改,另外,对于 Windows 处理字体的方式和 Trw2000 的使用也必须有所了解才行。
基于以上原因,我这一次首先讲一些关于字号、API 和 Trw2000 的事,然后再讲述修改 Opera 字号的经过,希望大家在理解前面所讲内容的情况下再看修改 Opera 那一节。
不过,我这一篇文章不可能讲述太多的内容,如果对于前面的部分不是很懂,可以多看几遍,最好能看一些其它的讲述这方面内容的书籍,肯定比我这里说的清楚和全面的多,而我在这一方面不大可能提供更详细的内容了。
关于字号
在 Windows 程序设计中,坐标的单位有很多种。
一种是我们最为熟悉的像素(Pixel),像素代表的是物理的坐标,比如我们常说的 640x480、800x600 指的就是像素。
另外比较重要的就是磅(Point)了,它是一种逻辑坐标,原来是打印的单位,大约为 1/72 英寸,在 Windows 中被确定的定义为 1/72 英寸。
还有一种比较重要的是缇(Twips),VB 缺省情况下就是使用缇作为它的单位的,20 缇等于 1 磅,所以 1 缇就是 1/1440 英寸。
另外还有几种单位,代表厘米、毫米之类的逻辑单位,我在这里就不说了。
因为 Windows 其实不知道我们的显示器的大小,所以是通过我们的设置来标示英寸的大小的。好的,我们再来看一下 Windows 中关于字号的设置。
在显示属性对话框里可以设置字体的大小,不过只有两种设置,一种是小字体,另一种是大字体。其中小字体表示 96dpi ,而大字体表示 120dpi 。小字体是缺省选项,而大字体是在用户所选的屏幕分辨率太大(如 1600x1200)时,为了避免字体太小看不清楚而选择使用的。另外,用户也可以自己设置字体的分辨率,不过值就不一定是多少了。
我们常说的“宋体,9”,表示的单位其实是磅,也就是 9 磅的宋体。
我们来换算一下。在小字体的时候,分辨率是 96dpi ,也就是说一英寸能显示 96 个像素;9 磅是 1/8 英寸,所以 96/8=12 像素。也就是说,我们通常见到的字体就是这种 12x12 点阵的字体了。
另外,在大字体的时候,分辨率是 120dpi ,9 磅是 1/8 英寸,所以 120/8=15 ,就是说大字体时,显示的 9 磅字体其实是 15x15 点阵的字体。
在 VB、VC 或 Delphi 里,对于窗体设置字体后,窗体的大小会自动随用户所选择的是大字体还是小字体而自动调整窗体的大小,这一点就是因为它们使用了逻辑单位。缺省情况下,对于 VB 来说是缇,对于 VC 和 Delphi 来说是磅。
CreateFont
上次说过,如果不设置字体,将显示为“System,12”,所以只要是字体太小的程序就一定设置了字体。
Windows 中设置字体的函数有两个,一个是 CreateFont ,一个是 CreateFontIndirect 。
其中 CreateFont 的定义如下:
HFONT CreateFont(
int nHeight, // logical height of font
int nWidth, // logical average character width
int nEscapement, // angle of escapement
int nOrientation, // base-line orientation angle
int fnWeight, // font weight
DWORD fdwItalic, // italic attribute flag
DWORD fdwUnderline, // underline attribute flag
DWORD fdwStrikeOut, // strikeout attribute flag
DWORD fdwCharSet, // character set identifier
DWORD fdwOutputPrecision, // output precision
DWORD fdwClipPrecision, // clipping precision
DWORD fdwQuality, // output quality
DWORD fdwPitchAndFamily, // pitch and family
LPCTSTR lpszFace // pointer to typeface name string
);
参数的类型有三种:int、DWORD 和 LPCTSTR 。int 和 DWORD 都是四字节整数,LPCTSTR 也是四字节整数,不过它是一个指针,处理方法和前者不同。
其中,第一项 nHeight 就是我们要修改的字号了。不过这里的值其实使用的是像素,所以在 MSDN 里建议使用如下的方式设置字号:
nHeight = -MulDiv(PointSize, GetDeviceCaps(hDC, LOGPIXELSY), 72);
这就是把磅转换成像素,而且变成负数。对于为什么微软定义负数才显示正常,我不太清楚,不过没有关系,记住一定使用负数就可以了。
另外,fdwCharSet 是语系,lpszFace 是字体名,也是我们所关心的项。不过可以令字体名为空,以便 Windows 自动查找缺省字体,这时就不需要修改语系了。
CreateFontIndirect 的定义如下:
HFONT CreateFontIndirect(
CONST LOGFONT *lplf // pointer to logical font structure
);
这里传递的参数是一个指针,指向一个 LOGFONT 结构,LOGFONT 的定义如下:
typedef struct tagLOGFONT { // lf
LONG lfHeight;
LONG lfWidth;
LONG lfEscapement;
LONG lfOrientation;
LONG lfWeight;
BYTE lfItalic;
BYTE lfUnderline;
BYTE lfStrikeOut;
BYTE lfCharSet;
BYTE lfOutPrecision;
BYTE lfClipPrecision;
BYTE lfQuality;
BYTE lfPitchAndFamily;
TCHAR lfFaceName[LF_FACESIZE];
} LOGFONT;
这个结构和 VB 的字体块的结构有些相像,如果是整体读入的话,和修改内存是一样的,不过一般不会整体装入,而是一项一项的装入,这样编译的程序的字体的各项也是在一起的,不过因为是一项一项装入,那些 LONG 型的值的四个字节的顺序和实际在内存里的顺序是相反的(参见我写的《UniCode 补遗》)。不过,也有许多情况下,字体结构的各项并不是在同一个地方初始化的,这样的程序,就不会出现各项的值聚集在一起的情况了。
注意,这里的字体名 lfFaceName 的类型是 TCHAR ,说明如果程序被编译成 UniCode 格式的话,字体名将使用 UniCode 表示。另外,CreateFont 的参数字体名 lpszFace 的类型是 LPCTSTR ,注意中间的那个“T”,它说明这也是一个可以编译成 UniCode 的指针。当然,编译选项不同,函数名也会有变化,如果编译成 ASCII 的话,CreateFont 将为 CreateFontA ,如果编译成 UniCode 的话,将成为 CreateFontW 。
Windows 95/98 支持所有的 ASCII 函数和极少量的 UniCode 函数,内部使用 ASCII 模式处理;Windows NT/2000 支持所有的 ASCII 函数和所有的 UniCode 函数,内部使用 UniCode 模式处理。
(虽然和本题无关,还是要说一下,Windows 95/98 支持少量的 Win32 函数(Windows 32 位函数,可不是 Windows 3.2 函数!),而 Windows NT/2000 支持所有的 Win32 函数。这也就是为什么 Windows NT 能处理的很多任务 Windows 98 处理不了的原因 —— 比如把编辑后的资源存回可执行文件。)
API 调用约定
几乎每一种语言都有函数的概念,而作为函数就有参数,一般的说,参数的传递是通过堆栈的(堆栈是一种先入后出的结构,使用 Push 压入,使用 Pop 弹出,Push 和 Pop 必须成对使用),不过有两种不同的处理方法。
一种方法是按原顺序把参数压入堆栈,然后使用 CALL 指令呼叫函数的地址,而函数把参数使用 POP 弹出堆栈然后处理。由于堆栈的先入后出特性,所以这种方法对于调用者有利,因为被调用的函数得到的反序的参数。
另一种方法相反,是按反序把参数压入堆栈,然后使用 CALL 指令呼叫函数的地址,而函数把参数使用 POP 弹出堆栈然后处理。所以这种方法对于被调用者有利,因为被调用的函数得到的正序的参数。
C 语言和 Pascal 语言分别使用这两种方式,而 Windows 使用的调用方式和 Pascal 相同,所以以前的 C 程序编写 Windows 程序的时候需要使用关键字 PASCAL 指明使用 Pascal 调用规则;现在一般的不使用 PASCAL 关键字,而是使用 __stdcall 说明符,表明是一个标准调用。这种现在称为标准调用的就是第二种方式 —— 反序压栈。
这种反序压栈的方法确实有好处。因为压栈的行为是编译器编译好的,虽然反序麻烦,但只是在编译的时候;而弹栈的行为是动态的,使用正序不止处理上方便不少,速度也更快。
举例来说,CreateFont 的调用就像以下这个样子:
PUSH lpszFace
PUSH fdwPitchAndFamily
PUSH fdwQuality
PUSH fdwClipPrecision
PUSH fdwOutputPrecision
PUSH fdwCharSet
PUSH fdwStrikeOut
PUSH fdwUnderline
PUSH fdwItalic
PUSH fnWeight
PUSH nOrientation
PUSH nEscapement
PUSH nWidth
PUSH nHeight
CALL CreateFont
注意,这里写的是伪代码,真正的汇编代码不可能这么好读,而且可能在 PUSH 之间还有其它的代码存在,不过只要清楚的知道需要压入堆栈的参数的个数和参数的形式,我们还是可以很方便的找出每一个压入的参数是什么意思的。
接触 Trw2000
因为 Trw2000 在运行时不大可能截图,所以我做了一个作为示例的表格,如下:
寄存器及其值的显示区
内存数据显示区
反汇编的代码显示区
堆栈数据显示区
代码所在区域的显示
命令显示区
注意,这只是普通情况下 Trw2000 显示的样子,事实上用户可以切换各个区域显示的大小。
“寄存器及其值的显示区”显示寄存器及其值,其格式如“EAX=FFFFFFFF”,寄存器有很多,因为 Trw2000 会显示出所有的寄存器,我就不在这里一一列举了。
“内存数据显示区”用来显示内存数据。要显示内存数据,有四个命令:d、db、dw、dd 。其中 db 指按照字节格式显示内存数据;dw 指按照双字节整数的格式显示内存数据;dd 指按照四字节整数的格式显示内存数据;而 d 命令使用当前格式显示内存数据。这里显示的都是十六进制值。因为 Inter CPU 采取高位在后的原则,所以双字节和四字节格式的数据和单字节格式是反向排列的。(参见我写的《UniCode 补遗》)
“反汇编的代码显示区”显示反汇编的代码。这里是我们最关心的地方了,因为上一节最后示例的 CreateFont 的汇编代码就会显示在这里,而对代码的分析是我们修改字号的关键。
“堆栈数据显示区”显示堆栈数据。这里也是我们关注的焦点,如上一节所说,参数是使用堆栈的方式传递的,所以这里的数据就是要传递给函数的参数。它的格式是一行显示两个数据,第二个数据是参数,而第一个数据是第二个数据的指针。所以其实是一行显示一个数据,最后压入的数据在最上面。因为 Windows 采用正序弹栈的方式,所以在运行到 CALL 语句的时候,堆栈里的数据从上到下是第一、二、三 …… 个参数。
“代码所在区域的显示”显示代码所在区域。上两篇文章里我经常说“回到 XX 的代码区”,就是从这里知道的。比如 CreateFont 函数是 GDI 函数,如果在 CreateFont 内部的话,这里就会显示“GDI!xxxxxxxx”,其中,xxxxxxxx 表示的是当前代码的地址;如果在 Opera 的代码区的话,这里就显示“Opera!xxxxxxxx”。
“命令显示区”是输入和显示命令的地方,就没什么好说的了。
另外,要说一下 Trw2000 中我们常用的几个命令。
“F5”或“g”命令。这两种方式的效果是相同的,都是运行程序。
“F6”。此命令用来切换方向键所在的区域。缺省情况下,方向键在命令区,这时使用方向键可以控制命令区的翻页;按“F6”一次,则处于代码区,此时可以使用方向键翻动代码;再按一次“F6”,则返回命令区。
“bpx”命令。中断命令,指定在哪里中断程序的运行,可以是地址,也可以是 API 函数,以下格式都是正确的:“bpx 00441122”,“bpx CreateFontA”。(“F9”也是“bpx”指令,不过“F9”不能定义 API 函数,只能设置当前行为断点)
“F8”和“F10”。这两个命令用来跟踪程序的运行,不同之处在于,如果遇到 CALL 代码,“F8”跟踪到此呼叫的内部,而“F10”运行这个呼叫,并跟踪到呼叫返回后的第一条语句上。
“r”命令。此命令用来修改寄存器的值。比如最后一条 PUSH 语句是“PUSH EAX”,那么在此语句执行之前,修改 EAX 寄存器的值就同时也修改了要压入堆栈的值。
“e”命令。此命令用来修改内存,可以写成“e xxxxxxxx”,其中“xxxxxxxx”表示地址,但是也可以是寄存器名,比如“e esp”,因为寄存器 ESP 是堆栈指针,所以它表示修改堆栈的数据;也可以写成单独的“e”,表示修改现在正显示的内存数据。
实战 Opera
老话说“三纸无驴”,今天才发现,这也是很不容易达到的呢。 :) 不过幸好我们就要真的修改字号了。
这里使用的例子是伟乾汉化的 Opera 4.0 beta 5 。汉化之后,主窗体的按钮提示显示过小,不是我们经常见到的“宋体,9”,所以主要的焦点在于提示字体的修改。对于提示字体,如果程序调用的是 Windows 提供的提示方式,则使用缺省的提示字体字号显示,一般就是“宋体,9”了(用户可以修改提示所用的字体字号),所以 Opera 在这里肯定不是调用 Windows 的功能,而是自己实现的。
先用 eXeScope 察看 Opera.exe 的函数导入表,发现它既使用了 CreateFontA ,也使用了 CreateFontIndirectA ,这还真是可恶,就先拿 CreateFontA 开刀吧。
运行 Trw2000 ,选择 Opera.exe ,点 Load 调入,现在出现了 Trw2000 的调试窗口,键入指令“bpx CreateFontA”,令其在程序调用 CreateFontA 函数的时候中断。然后按“F5”运行程序。
提示:
如果在使用 Trw2000 调入程序之前,原程序没有运行过,则它肯定不在磁盘缓存里,而 Trw2000 自己从磁盘上调入文件的速度非常之慢。所以建议在运行 Trw2000 之前先运行一下原程序,然后关闭,之后再运行 Trw2000 调入程序,则速度要快的多。Trw2000 在这一方面是非常需要优化一下的了。
程序中断在 CreateFontA 的入口处,察看“代码所在区域的显示”,可以见到是在 GDI 模块内,这时,键入“pmodule”指令,程序会运行直到返回 Opera 的代码区,可以看到上一个调用就是 CreateFontA ,再向上查找 PUSH 语句,找到如下的代码:
偏移量 字节代码 汇编代码
015F:00441101 FF7544 push [ebp+44]
015F:00441104 50 push eax
015F:00441105 0FB6453C movzx eax, byte ptr [ebp+3C]
015F:00441109 50 push eax
015F:0044110A 0FB64538 movzx eax, byte ptr [ebp+38]
015F:0044110E 50 push eax
015F:0044110F 0FB64534 movzx eax, byte ptr [ebp+34]
015F:00441113 50 push eax
015F:00441114 0FB64530 movzx eax, byte ptr [ebp+30]
015F:00441118 50 push eax
015F:00441119 0FB6452C movzx eax, byte ptr [ebp+2C]
015F:0044111D 50 push eax
015F:0044111E 0FB64528 movzx eax, byte ptr [ebp+28]
015F:00441122 50 push eax
015F:00441123 0FB64524 movzx eax, byte ptr [ebp+24]
015F:00441127 50 push eax
015F:00441128 FF7520 push [ebp+20]
015F:0044112B FF751C push [ebp+1C]
015F:0044112E FF7518 push [ebp+18]
015F:00441131 FF7514 push [ebp+14]
015F:00441134 FF7510 push [ebp+10]
015F:00441137 FF15F8005300 Call GDI32!CreateFontA
015F:0044113D 8BF0 mov esi, eax
这一段代码和我上面做示例的代码很相似,只是其中插入了一些 movzx 指令。偏移量之后的是字节代码,在 Trw2000 中使用“Code On”指令才会显示出来,我这里为了方便也写出来了。要注意,你的机器上运行 Opera 的段偏移量可能和我这里的“015F”不同。
一般来说,现在应该按“F6”到代码区,在“015F:00441137”处的“Call GDI32!CreateFontA”上停下,按“F9”设置断点,以便下一次运行的时候可以方便的中断,不过可能是 Opera 在退出的时候有太多的工作要做,所以如果不先关掉 Trw2000 的话,Opera 退出非常缓慢,反正我是没能等到它正常退出,只好重启了。
由于以上的原因,所以记下“00441137”,以便下一次运行时设置断点。然后按“F5”运行,没有再被中断,所以在 Opera 启动的时候只调用过一次 CreateFontA 函数。
现在,关闭 Trw2000 ,然后关闭 Opera ,然后再运行 Trw2000 ,再选择 Opera.exe ,按 Load 调入,按一下“F10”,让代码处于 Opera 所在的区域,键入“bpx 00441137”,这个“00441137”就是刚才记下的那个 CreateFontA 函数所在的地址,然后按“F5”运行程序。
等 Trw2000 的调试窗体再次出现,发现代码刚好运行到“Call GDI32!CreateFontA”,正准备调用此函数,这时看一下堆栈显示区,发现最上面的数据是“FFFFFFF5”,也就是十进制的 -11 ,我们已经知道,至少需要 -12 才能正常显示,我们不妨改的多一些,所以先键入“dd esp”用四字节方式显示堆栈数据,再键入“e esp”修改堆栈,现在光标在内存数据区,键入“FFFFFFF0”,回车,见到堆栈的栈顶数据已经被改成“FFFFFFF0”了,按“F5”运行,进入 Opera 的主界面,现在把鼠标放在按钮上,可以看到按钮提示已经显示的非常大,以至于只能显示上半个汉字了。所以我们知道就是这个 CreateFontA 创建的字体用于按钮提示,其它的 CreateFontIndirectA 函数也就不用查了。(这不过是说您几位不用查了,我可是查到了所有启动时调用的十个 CreateFontIndirectA 函数,并修改了所有这十个函数的参数,才确认不是 CreateFontIndirectA 函数惹的祸,而后检查 CreateFontA 函数的。 :()
好的,现在说一下为什么不改成“FFFFFFF4”而要改成“FFFFFFF0”。因为改成“FFFFFFF4”没有效果,为什么呢?因为 Opera 在这里使用了一种非常奇怪的字体,好像叫什么“Helv”,如果在“00441104”处中断,然后使用“dd *esp”就可以看到了。这种字体显示成 -12 的时候并不是我们期望的正常的 12 像素的字体,所以是需要修改字体名的。
我懒,所以我没有去查找究竟程序在什么时候修改了“[ebp+44]”([ebp+44] 相当于 C 语言里的 *(ebp+0x44),表示指针所指处的值),而只是修改了压入堆栈的参数,把“[ebp+44]”改成了“0”,也就是一个空指针,因为没有指定字体,Windows 使用了缺省的宋体来显示。
我懒,我也没有去查找究竟程序在什么时候修改了[ebp+10],我直接把 -12 送入了堆栈,并且把这一方法用于可执行文件的修改。
所以,最后对可执行文件的修改就是把“push [ebp+44]”修改成“push BYTE 00”,把“push [ebp+10]”修改成“push BYTE -0C”。
“push [ebp+44]”的字节代码是“FF7544”,“push [ebp+10]”的字节代码是“FF7510”;而“push BYTE 00”的字节代码是“6A00”,“push BYTE -0C”的字节代码是“6AF4”。这里多出了两个字节,没有关系,汇编指令里有一条 NOP 指令,是空指令,CPU 在遇到这一条指令的时候什么也不干,正好作为填充盈余之用,它的字节代码是“90”。
所以,最后的修改是这样的:查找“FF7544500FB6453C500FB64538”,并把开头的“FF7544”修改成“6A0090”;查找“FF7510FF15F80053008BF0”,并把开头的“FF7510”修改成“6AF490”。
再战 WinRAR
作为修改这种非资源的字体字号,不使用反汇编是不大可能的,不过也不一定要使用 Trw2000 ,下面我来介绍一下使用 W32dasm 反汇编而修改字体字号的例子。
W32dasm 是一种静态反汇编工具,也就是说,它是基于对于可执行文件的分析得到的反汇编代码,原程序其实并不运行,这样,不管程序里是否有检测调试器的代码都没有关系,而且,因为原程序不需要运行,操作上也简单一些,不过相比与动态反汇编,不能知道程序运行到某一条语句时寄存器和内存的状态,算是一个缺点。不过这两种方法各有各的好处,正是相得益彰。(我的主页有 W32dasm 8.93 版的下载)
WinRAR 文件如果写了注释,则打开此文件时主窗体右侧会有注释窗体出现,其中的字体虽然也不小,但是如果是中文注释,总好像歪歪扭扭的,显得很是不舒服,所以就用它做例子。
运行 W32dasm ,出现其主窗体,选择菜单“Disassembler”->“Open File to Disassemble..”,出现文件选择框,选择“WinRAR.exe”,然后 W32dasm 会分析程序,这个过程需要一些时间,等到全部完成之后,选择菜单“Disassembler”->“Save Disassembly Text File and Create Project File”,出现保存文件对话框,我们保存文件,然后退出 W32dasm 。
这时,在 WinRAR 的目录下会有两个文件“WinRAR.prj”和“WinRAR.alf”。“WinRAR.prj”是 W32dasm 的工程文件,我们并不关心,我们关心的是反汇编的“WinRAR.alf”文件,不过这个文件真的好大,竟然有 8.22MB 。
现在,用一种打开大文件速度也很快的文本编辑器打开“WinRAR.alf”,查找“CreateFont”。第一个找到的是这样的:
Addr:000D9B00 hint(0000) Name: CreateFontA
这是函数导入表里的数据,表明 WinRAR 导入了 CreateFontA 函数。继续查找,下面一个是这样的:
* Reference To: GDI32.CreateFontA, Ord:0000h
这种使用“*”开头的句子,是 W32dasm 为了提示用户而加入的,并不是汇编代码,不过它指明下一条语句就是执行的 CreateFontA ,所以我们全面的看一下,因为 CreateFontA 有 14 个参数,所以向上查找 14 个 push 语句:
* Possible StringData Ref from Data Obj ->"MS Sans Serif"
|
:0040720D 68274E4600 push 00464E27
:00407212 6A00 push 00000000
:00407214 6A00 push 00000000
:00407216 6A00 push 00000000
:00407218 6A00 push 00000000
:0040721A 6A00 push 00000000
:0040721C 6A00 push 00000000
:0040721E 6A00 push 00000000
:00407220 6A00 push 00000000
* Possible Reference to String Resource ID=00700: "?遄"
|
:00407222 68BC020000 push 000002BC
:00407227 6A00 push 00000000
:00407229 6A00 push 00000000
:0040722B 6A00 push 00000000
* Possible Reference to String Resource ID=00244: "僢(冐??"
|
:0040722D 6AF4 push FFFFFFF4
* Reference To: GDI32.CreateFontA, Ord:0000h
|
:0040722F E84BAA0500 Call 00461C7F
这一次调用真是一板一眼,和我上面写的伪代码可以说是一摸一样。我们可以见到,第一行就是提示,说“以下这个语句使用的地址可能是‘MS Sans Serif’的指针”,这也太明显了,确实是。不过,第二个提示说“以下的这个可能是字串资源的地址”,这是不对的,因为我们知道,所谓的“push 000002BC”压入的是字符宽度,这个 2BC 就是 700 ,也就是表明是粗体。第三个提示就更不对了,我们知道,这里“push FFFFFFF4”压入的是字号,也就是表明创建 12 个像素的字体。
好的,我们修改,而且不妨改的大一些。在上面的代码中,我用绿色标出了所有的字节代码,这些值就是可执行文件中会出现的字符流,所以我们用一种十六进制编辑器打开 WinRAR.exe ,查找“6AF4E84BAA0500”,找到,并把其中的“6AF4”改成“6AD8”。运行 WinRAR ,并打开一个带注释的文件,发现注释框的字体没有变化,所以关闭 WinRAR ,继续在“WinRAR.alf”文件中查找“CreateFont”。
在“WinRAR.alf”中一共可以找到 10 个“CreateFont”,第一个是函数导入表里的,其余的都是 W32dasm 的提示了。我们每查找到一个“CreateFontA”的调用,都试一下修改它的字号,并且运行 WinRAR 测试,直到我们修改找到的第三个“CreateFontA”调用的时候,我们测试发现注释框的字体变大了,而且非常之大,好吓人呢。:) ,而第三个“CreateFontA”调用的全过程如下:
* Possible StringData Ref from Data Obj ->"Terminal"
|
:0041230D 689A594600 push 0046599A
:00412312 6A01 push 00000001
:00412314 6A00 push 00000000
:00412316 6A00 push 00000000
:00412318 6A00 push 00000000
:0041231A 68FF000000 push 000000FF
:0041231F 6A00 push 00000000
:00412321 6A00 push 00000000
:00412323 6A00 push 00000000
* Possible Reference to String Resource ID=00500: "? ?剠Xe@ 圅"
|
:00412325 68F4010000 push 000001F4
:0041232A 6A00 push 00000000
:0041232C 6A00 push 00000000
:0041232E 6A00 push 00000000
* Possible Reference to String Resource ID=00244: "僢(冐??"
|
:00412330 6AF4 push FFFFFFF4
* Reference To: GDI32.CreateFontA, Ord:0000h
|
:00412332 E848F90400 Call 00461C7F
哈!WinRAR 的调用还真是规范。好的,我们见到,在这次调用里,字号是“F4”,并没有错误,而设置的字体名是“Terminal”,好奇怪的字体,不管怎么说,先把它换掉。
查找“689A5946006A01”,并把其中的“689A594600”修改成“6800000000”或者“6A00909090”。(68 也是 Push 语句。6A 压入的是单字节整数,68 压入的是四字节整数)
然后运行,字体还是很奇怪,继续修改。紧接着的“6A01”压入的是“fdwPitchAndFamily”,不知道是什么,反正只要改成 0 ,Windows 就会用缺省值了,所以把“6A01”改成“6A00”。再次运行程序,字体正常了。
另外,我们可以知道,“push 000000FF”压入的是语系,可以修改成“push 00000086”;而“push 000001F4”压入的是字体的粗度,1F4 是 500 ,也可以修改成缺省的 400 ,就是“push 00000190”。