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」,Enter,見到堆疊的棧頂數據已經被改成「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」。

點睛工作室梁利鋒 結稿於 2000.6.16 (6.17 補充)



回教學