【原創】Winamp標題欄中文亂碼原因分析及修正

看雪資料發表於2004-12-12

Winamp標題欄中文亂碼原因分析及修正

前言:
實在是忍受不了MediaPlayer對記憶體龐大的佔用率和我的小記憶體之間的矛盾,同時又找不到foobat的WMA外掛(有知道的兄弟,請給個下載地址,多謝!),於是裝上了Winamp 2.81 簡體中文經典懷念版,就是水手漢化的那個(夠豪華)。開啟一首抒情的歌曲,安撫一下我受傷的心靈――如果連續幾天倒黴,先是筆記本電池掛掉、然後是系統癱瘓、接下來是硬碟壞掉(全面癱瘓阿,可憐的國產筆記本,幸好是全球聯保的,可是又有什麼用呢?剛過了質保期,就全部壞掉,真是牛x),你就會明白什麼叫“受傷”――好久沒有用過Winamp了,記得上次用還是在N年前(懷念我的大奔133和那個激情的時代),於是隨手點選,熟悉一下功能。漢化過的版本就是好,畢竟中國人還是看中文最舒服――慢著,莫非這麼古老的BUG還沒有被修正麼?就是那個滾動標題欄顯示中文時亂碼的BUG,一個陳舊的毛病。看著下面時而清晰時而混亂的標題,總覺得不舒服,算了,修復一下這個陳年舊瘡吧。

分析物件:
Nullsoft Winamp 2.81 簡體中文經典懷念版
水手漢化 豪華版

故障分析:
從顯示的亂碼時常清晰時常混亂就能很明白的想出來,亂碼的原因就是可謂“經典”的“半個漢字”,大部分不支援中文的軟體都是因為這個。在Ascll碼中,每個英文用一個byte來記錄,而在GB碼中,每一個漢字用一個word來記錄。沒有經過改造的程式,在處理英文漢字混排的字串時,如果刪除一個漢字的話,每次只能刪除這個漢字GB碼的低8位,這時,亂碼就產生了。只要我們能正確的在英文漢字混排的字串分辨出漢字和英語,然後針對處理,問題就解決了。

程式碼分析:
要操刀修改,首先就要找到修改的位置。600多k的Winamp也不算小,蠻力去找自然是大海撈針。且先對其工作流程作一分析。要實現滾動標題欄其實很簡單,取得要顯示的字串,把它的長度按需要處理一下(不同的時間起始位置不同),然後用SetWindowTextA顯示出來即可。也就是說,Winamp也有可能是用上面的方法來實現的。
經過檢測,Winamp沒有加殼(其實這樣的軟體也沒有必要加殼:))。開啟W32Dasm載入Winamp,選擇“查詢”-〉“查詢文字”,輸入SetWindowTextA進行查詢。在經歷的N多個“查詢下一個”後,在0x42EE60停下了,這是一段可疑的程式碼(由此可見,正確的分析是很重要的,尤其在沒有任何線索的時候。當然,運氣同樣很重要:))。向上找,找到這一段子程式的起始位置,Copy下來分析:


* Referenced by a CALL at Address:
|:0041D2A1   
|
:0042ED10 55                      push ebp
:0042ED11 8BEC                    mov ebpesp
:0042ED13 B800100000              mov eax, 00001000
:0042ED18 E8D3DF0000              call 0043CCF0
:0042ED1D A000874400              mov albyte ptr [00448700]
:0042ED22 56                      push esi
:0042ED23 57                      push edi
:0042ED24 888500F0FFFF            mov byte ptr [ebp+FFFFF000], al
:0042ED2A B9FF030000              mov ecx, 000003FF
:0042ED2F 33C0                    xor eaxeax
:0042ED31 8DBD01F0FFFF            lea edidword ptr [ebp+FFFFF001]
:0042ED37 BEC0384500              mov esi, 004538C0                      ;在這個地方放的是完整的標題
:0042ED3C F3                      repz                                   ;清空緩衝區
:0042ED3D AB                      stosd
:0042ED3E 66AB                    stosw
:0042ED40 56                      push esi
:0042ED41 AA                      stosb

* Reference To: MSVCRT.strlen, Ord:02BEh
                                  |
:0042ED42 E8F7DF0000              Call 0043CD3E                           ;算一下標題有多少個字
:0042ED47 59                      pop ecx
:0042ED48 8B0D48124500            mov ecxdword ptr [00451248]           ;這個就是迴圈用的計數器地址
:0042ED4E 3BC8                    cmp ecxeax                            ;到頭了,就重新再來吧。走馬燈效果的實現。
:0042ED50 7D57                    jge 0042EDA9
:0042ED52 8D81C0384500            lea eaxdword ptr [ecx+004538C0]
:0042ED58 50                      push eax
:0042ED59 8D8500F0FFFF            lea eaxdword ptr [ebp+FFFFF000]
:0042ED5F 50                      push eax

* Reference To: MSVCRT.strcpy, Ord:02BAh
                                  |
:0042ED60 E87DDF0000              Call 0043CCE2                          ;按照計數器指定的偏移,把要顯示的字串移到緩衝區
:0042ED65 8D8500F0FFFF            lea eaxdword ptr [ebp+FFFFF000]

* Possible StringData Ref from Data Obj ->" *** "                        ;熟悉的東西:)
                                  |
:0042ED6B 686C644400              push 0044646C
:0042ED70 50                      push eax

* Reference To: MSVCRT.strcat, Ord:02B6h
                                  |
:0042ED71 E872DF0000              Call 0043CCE8
:0042ED76 83C410                  add esp, 00000010
:0042ED79 8D8500F0FFFF            lea eaxdword ptr [ebp+FFFFF000]
:0042ED7F FF3548124500            push dword ptr [00451248]
:0042ED85 56                      push esi
:0042ED86 50                      push eax

* Reference To: MSVCRT.strlen, Ord:02BEh
                                  |
:0042ED87 E8B2DF0000              Call 0043CD3E
:0042ED8C 59                      pop ecx
:0042ED8D 8D840500F0FFFF          lea eaxdword ptr [ebp+eax-00001000]  ;繼續塞字元
:0042ED94 50                      push eax

* Reference To: MSVCRT.strncpy, Ord:02C1h
                                  |
:0042ED95 FF1558E24300            Call dword ptr [0043E258]
:0042ED9B 83C40C                  add esp, 0000000C
:0042ED9E FF0548124500            inc dword ptr [00451248]                ;重點到了!每次不判斷當前字元的型別,就簡單的把計數器加一,“半個漢字”自然就產生了
:0042EDA4 E984000000              jmp 0042EE2D

* Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:0042ED50(C)                                                             ;這個子程式是用作字元全部顯示完成後重新開始的,沒有什麼問題。
|
:0042EDA9 56                      push esi

* Reference To: MSVCRT.strlen, Ord:02BEh
                                  |
:0042EDAA E88FDF0000              Call 0043CD3E
:0042EDAF 8B0D48124500            mov ecxdword ptr [00451248]
:0042EDB5 8D896C644400            lea ecxdword ptr [ecx+0044646C]
:0042EDBB 2BC8                    sub ecxeax
:0042EDBD 8D8500F0FFFF            lea eaxdword ptr [ebp+FFFFF000]
:0042EDC3 51                      push ecx
:0042EDC4 50                      push eax

* Reference To: MSVCRT.strcpy, Ord:02BAh
                                  |
:0042EDC5 E818DF0000              Call 0043CCE2
:0042EDCA 8D8500F0FFFF            lea eaxdword ptr [ebp+FFFFF000]
:0042EDD0 56                      push esi
:0042EDD1 50                      push eax

* Reference To: MSVCRT.strcat, Ord:02B6h
                                  |
:0042EDD2 E811DF0000              Call 0043CCE8
:0042EDD7 56                      push esi

* Reference To: MSVCRT.strlen, Ord:02BEh
                                  |
:0042EDD8 E861DF0000              Call 0043CD3E
:0042EDDD 83C418                  add esp, 00000018
:0042EDE0 6A03                    push 00000003
:0042EDE2 59                      pop ecx
:0042EDE3 2B0D48124500            sub ecxdword ptr [00451248]
:0042EDE9 03C1                    add eaxecx
:0042EDEB 50                      push eax
:0042EDEC 8D8500F0FFFF            lea eaxdword ptr [ebp+FFFFF000]

* Possible StringData Ref from Data Obj ->" *** "
                                  |
:0042EDF2 686C644400              push 0044646C
:0042EDF7 50                      push eax

* Reference To: MSVCRT.strlen, Ord:02BEh
                                  |
:0042EDF8 E841DF0000              Call 0043CD3E
:0042EDFD 59                      pop ecx
:0042EDFE 8D840500F0FFFF          lea eaxdword ptr [ebp+eax-00001000]
:0042EE05 50                      push eax

* Reference To: MSVCRT.strncpy, Ord:02C1h
                                  |
:0042EE06 FF1558E24300            Call dword ptr [0043E258]
:0042EE0C FF0548124500            inc dword ptr [00451248]                    ;個人感覺沒有必要處理這個地方。
:0042EE12 56                      push esi

* Reference To: MSVCRT.strlen, Ord:02BEh
                                  |
:0042EE13 E826DF0000              Call 0043CD3E
:0042EE18 83C003                  add eax, 00000003
:0042EE1B 83C410                  add esp, 00000010
:0042EE1E 390548124500            cmp dword ptr [00451248], eax
:0042EE24 7C07                    jl 0042EE2D
:0042EE26 83254812450000          and dword ptr [00451248], 00000000

* Referenced by a (U)nconditional or (C)onditional Jump at Addresses:
|:0042EDA4(U), :0042EE24(C)
|

* Reference To: USER32.GetWindowLongA, Ord:0156h
                                  |
:0042EE2D 8B35B8E34300            mov esidword ptr [0043E3B8]
:0042EE33 6AF0                    push FFFFFFF0
:0042EE35 FF3520334500            push dword ptr [00453320]
:0042EE3B FFD6                    call esi

* Reference To: USER32.SetWindowLongA, Ord:0258h
                                  |
:0042EE3D 8B3D50E34300            mov edidword ptr [0043E350]
:0042EE43 25FFFF3FFF              and eax, FF3FFFFF
:0042EE48 50                      push eax
:0042EE49 6AF0                    push FFFFFFF0
:0042EE4B FF3520334500            push dword ptr [00453320]
:0042EE51 FFD7                    call edi
:0042EE53 8D8500F0FFFF            lea eaxdword ptr [ebp+FFFFF000]
:0042EE59 50                      push eax
:0042EE5A FF3520334500            push dword ptr [00453320]

* Reference To: USER32.SetWindowTextA, Ord:025Eh                                 ;嘿嘿,終於顯示出來了;)
                                  |
:0042EE60 FF15E0E34300            Call dword ptr [0043E3E0]
:0042EE66 6AF0                    push FFFFFFF0
:0042EE68 FF3520334500            push dword ptr [00453320]
:0042EE6E FFD6                    call esi
:0042EE70 0D0000C000              or eax, 00C00000
:0042EE75 50                      push eax
:0042EE76 6AF0                    push FFFFFFF0
:0042EE78 FF3520334500            push dword ptr [00453320]
:0042EE7E FFD7                    call edi
:0042EE80 5F                      pop edi
:0042EE81 5E                      pop esi
:0042EE82 C9                      leave
:0042EE83 C3                      ret

經過分析,發現從0x0042ED10到0x0042EE60的這段程式碼就是實現滾動標題效果的核心程式碼。其中0x004538C0放置的是完整的字串,0x00451248放置的是顯示位置計數器值,0x006DA9EC放置的是將要顯示出來的字串。由於0x0042ED9E處沒有對當前字元進行型別分析,簡單的移動1次計數器,導致了漢字顯示亂碼。為了證實剛才的分析,開啟TRW載入Winamp,下斷點BP 0042ED10,把0x0042ED9E處的程式碼都改為nop。然後繼續執行,發現標題欄已經不再繼續滾動。證明上面的分析正確。

修改程式碼:
經過上面分析,只要正確的判斷當前字元的型別並做出相應的處理即可。問題是怎麼區分英文和漢字。記得可以顯示的英文Ascll碼到0x80為止。查了一下資料,GB code的內碼的兩個位元組都是從A0H - FEH之間的。這樣的話,程式碼基本上就可以寫出來了:

mov ecx,dword ptr [00451248]
lea eax,dword ptr [ecx+004538C0]
cmp byte ptr [eax],A0
jb 1
inc dword ptr [00451248]
1:
inc dword ptr [00451248]  
retn

由於個人比較懶的原因,這裡對漢字的判斷作了簡化處理,只要小於A0的字元都認為是英文,反之則是中文。一般情況下,這樣都是可以正常現實的,當然BIG5碼除外。關於BIG5碼的判斷標準如下:“BIG code 的內碼的第一個位元組是80H - FFH,第二個位元組是00H - FFH”有興趣的朋友可以自行修改。
下面的問題就是找一塊能夠放下程式碼的空間。感覺Winamp是用VC++編寫的,Language2000證實了這一點。由於程式碼作了最佳化處理,所以基本上沒有什麼縫隙,所以只有從.text節末端尋找可以利用的空間。根據VirtualSize、SizeOfRawData和PointerToRawData計算出在檔案0x0003C8E0偏移(RVA:0x0043D4E0)後即為空閒空間。我們就把程式碼放在這個地方。opcode如下:

8B0D481245008D81C03845008038A07206FF0548124500FF0548124500C3

然後修改0x004538C0(檔案偏移0x0002E19E)處的程式碼:

call 0043D4E0
nop                   ;佔位

opcode如下:E83DE7000090

再做些收尾工作,修改.text的實際尺寸。這樣就基本上已經完工了。
下面再測試一下,開啟Winamp,隨著悠揚的樂曲傳出,我們發現,滾動標題欄終於可以正確顯示中文了:)

後記:
希望上文能把我要說的表達出來。如果有描述不清晰的地方或有錯誤,請與我聯絡:coffin13@183.ha.cn。
匆忙之作,加上我水平又菜,不免錯誤百出,望高手予以指正。在此先謝過了。





monkeycz
2004年12月12日凌晨

相關文章