汇编Ring 3下实现 HOOK API
【文章標(biāo)題】匯編ring3下實(shí)現(xiàn)HOOK?API
【文章作者】nohacks(非安全,hacker0058)
【作者主頁】hacker0058.ys168.com
【文章出處】看雪論壇(bbs.pediy.com)
==================[?匯編ring3下實(shí)現(xiàn)HOOK?API?]=====================
?????????????????????????????????????????????????Author:?nohacks
?????????????????????????????????????????????????Emil:?kker.cn@163.com
?????????????????????????????????????????????????Version:?1.1
?????????????????????????????????????????????????Date:?7.18.2006
?????????????????????????????????????????????????
=====[?1.?內(nèi)容?]=============================================
1.?內(nèi)容
2.?介紹
??2.1?什么叫Hook?API?
??2.2?API?Hook的應(yīng)用介紹
??2.3?API?Hook的原則
3.?掛鉤方法
??3.1?改寫IAT導(dǎo)入表法
??3.2?改寫內(nèi)存地址JMP法
4.?匯編實(shí)現(xiàn)
??4.1.?代碼?
??4.2.?分析
5.?結(jié)束語
=====[?2.?介紹?]================================================
???這篇文章是有關(guān)在OS?Windows下掛鉤API函數(shù)的方法。所有例子都在基于NT技術(shù)的Windows版本NT4.0
及以上有效(Windows?NT?4.0,?Windows?2000,?Windows?XP)。可能在其它Windows系統(tǒng)也會(huì)有效。
???你應(yīng)該比較熟悉Windows下的進(jìn)程、匯編器、和一些API函數(shù),才能明白這篇文章里的內(nèi)容。
=====[2.1?什么叫Hook?API?]=================================
???
???所謂Hook就是鉤子的意思,而API是指Windows開放給程序員的編程接口,使得在用戶級(jí)別下可
以對(duì)操作系統(tǒng)進(jìn)行控制,也就是一般的應(yīng)用程序都需要調(diào)用API來完成某些功能,Hook?API的意思
就是在這些應(yīng)用程序調(diào)用真正的系統(tǒng)API前可以先被截獲,從而進(jìn)行一些處理再調(diào)用真正的API來完
成功能。
?====[2.2?API?Hook的應(yīng)用介紹]=================================
????
???API?Hook技術(shù)應(yīng)用廣泛,常用于屏幕取詞,網(wǎng)絡(luò)防火墻,病毒木馬,加殼軟件,串口紅外通訊,游戲外
掛,internet通信等領(lǐng)域API?HOOK的中文意思就是鉤住API,對(duì)API進(jìn)行預(yù)處理,先執(zhí)行我們的函數(shù),例
如我們用API?Hook技術(shù)掛接ExitWindowsEx?API函數(shù),使關(guān)機(jī)失效,掛接ZwOpenProcess函數(shù)(如:老王的
EncryptPE),隱藏進(jìn)程等等......
====[2.3?API?Hook的原則]=====================================
???
???HOOK?API有一個(gè)原則,這個(gè)原則就是:被HOOK的API的原有功能不能受到任何影響。就象醫(yī)生救人,
如果把病人身體里的病毒殺死了,病人也死了,那么這個(gè)“救人”就沒有任何意義了。如果你HOOK?API
之后,你的目的達(dá)到了,但API的原有功能失效了,這樣不是HOOK,而是REPLACE,操作系統(tǒng)的正常功能
就會(huì)受到影響,甚至?xí)罎ⅰ?br />
====[?3.?掛鉤方法?]==============================================
總的來說,常用的掛鉤API方法有以下兩種:
3.1?改寫IAT導(dǎo)入表法
???修改可執(zhí)行文件的IAT表(即輸入表)因?yàn)樵谠摫碇杏涗浟怂姓{(diào)用API的函數(shù)地址,則只需將這些
地址改為自己函數(shù)的地址即可,但是這樣有一個(gè)局限,因?yàn)橛械某绦驎?huì)加殼,這樣會(huì)隱藏真實(shí)的IAT表
,從而使該方法失效。
3.2?改寫內(nèi)存地址JMP法
????直接跳轉(zhuǎn),改變API函數(shù)的入口或出口的幾個(gè)字節(jié),使程序跳轉(zhuǎn)到自己的函數(shù),該方法不受程序加殼
的限制。這種技術(shù),說起來也不復(fù)雜,就是改變程序流程的技術(shù)。在CPU的指令里,有幾條指令可以改變
程序的流程:JMP,CALL,INT,RET,RETF,IRET等指令。理論上只要改變API入口和出口的任何機(jī)器碼
,都可以HOOK,下面我就說說常用的改寫API入口點(diǎn)的方法:
????
????因?yàn)楣ぷ髟赗ing3模式下,我們不能直接修改物理內(nèi)存,只能一個(gè)一個(gè)打開修改,但具體的方法又分成
好幾種,我給大家介紹幾種操作思路:
??<1>首先改寫API首字節(jié),要實(shí)現(xiàn)原API的功能需要調(diào)用API時(shí)先還原被修改的字節(jié),然后再調(diào)用原API,調(diào)
用完后再改回來,這樣實(shí)現(xiàn)有點(diǎn)麻煩,但最簡單,從理論上說有漏HOOK的可能,因?yàn)槲覀兿冗€原了API,如果
在這之前程序調(diào)用了API,就有可能逃過HOOK的可能!
??(2)把被覆蓋的匯編代碼保存起來,在替代函數(shù)里模擬被被覆蓋的功能,然后調(diào)用原函數(shù)(原地址+被覆
蓋長度).但這樣會(huì)產(chǎn)生一個(gè)問題,不同的匯編指令長度是不一樣的(比如說我們寫入的JMP指令占用5個(gè)字
節(jié),而我們寫入的這5個(gè)字節(jié)占用的位置不一定正好是一個(gè)或多個(gè)完整的指令,有可能需要保存7個(gè)字節(jié),
才不能打亂程序原有的功能,需要編寫一個(gè)龐大的判斷體系來判斷指令長度,網(wǎng)上已經(jīng)有這樣的匯編程序
(Z0MBiE寫的LDE32),非常的復(fù)雜!
??(3)把被HOOK的函數(shù)備份一下,調(diào)用時(shí)在替代函數(shù)里調(diào)用備份函數(shù).為了避免麻煩,可以直接備份整個(gè)
DLL缺點(diǎn)就是太犧牲內(nèi)存,一般不推薦使用這種方法!
?
=====[?4.?匯編實(shí)現(xiàn)?]==============================================
本文就是建立在第2種方法之上的!本著先易后難的原則,今天我們先來說說它的第1種操作思路.
??
??我們拿API函數(shù)ExitWindowsEx來說明,下面是我在OD里攔下的ExitWindowsEx原入口部分
?????77D59E2D????????????$??8BFF??????????mov?edi,edi??
?????77D59E2F????????????.??55????????????push?ebp
?????77D59E30????????????.??8BEC??????????mov?ebp,esp
?????77D59E32????????????.??83EC?18???????sub?esp,18
??????......
??如果我們把ExitWindowsEx的入口點(diǎn)改為下面的,會(huì)出現(xiàn)什么情況?
????77D59E2D???????????????B8?00400000???mov?eax,4000
????77D59E32???????????????FFE0??????????jmp?eax
????......
??我們可想而知,程序執(zhí)行到77D59E32處就會(huì)改變流程跳到00400000的地方
??如果我們的00400000處是這樣的子程:
=======================
MyAPI?proc??bs:DWORD??,dwReserved:DWORD??;和ExitWindowsEx一樣帶2個(gè)參數(shù)?????????????????
;做你想做的事
......
;這里放API入口點(diǎn)改回原機(jī)器碼的代碼
;如果你是備份的整個(gè)DLL,就直接調(diào)用備份API,不用改來改去了,不會(huì)有漏勾API的可能!
invoke?ExitWindowsEx,bs,dwReserved?
??????????????????????????
;這里放HOOK?API的代碼
??
.endif
mov?eax,TRUE
ret
=======================
???這里的MyAPI是和ExitWindowsEx參數(shù)一樣的的子程,因?yàn)槌绦蚴窃贏PI的入口部分跳轉(zhuǎn)的,根據(jù)
stdcall約定(參數(shù)數(shù)據(jù)從右向左依次壓棧,恢復(fù)堆棧的工作交由被調(diào)用者),此時(shí)堆棧還沒有恢復(fù),我們
在子程里取出的參數(shù)數(shù)據(jù)依然有效,我們可以在這里執(zhí)行自己的代碼,你可以決定是否繼續(xù)按原參數(shù)或改
變參數(shù)后再調(diào)用原API,也可以什么都不做,當(dāng)然在調(diào)用之前,我們要先還原我們修改過的API(可以事先用
API函數(shù)ReadProcessMemory讀出原API的前幾個(gè)字節(jié)備份之),調(diào)用完后再改回來繼續(xù)HOOK?API,不過這種
方法有漏API的可能(原因前面已經(jīng)說了),你如果覺得這個(gè)方法不妥,因?yàn)橐话阆到y(tǒng)DLL都不大,你可以備
份整個(gè)DLL.
下面我就列出ring3下HOOK?API的幾個(gè)步驟:
1.得到要掛勾API的入口點(diǎn)
2.修改API的入口點(diǎn)所在頁的頁面保護(hù)為可讀寫模式
3.用ReadProcessMemory讀出API的入口點(diǎn)開始的幾字節(jié)備份
4.用WriteProcessMemory修改API的入口點(diǎn)象這樣的形式:
??
??mov?eax,4000
??
??jmp?eax
?其中的4000要用和原API參數(shù)一樣的子程序地址代替
??在這個(gè)子程序里我們決定用什么參數(shù)再調(diào)用原API,不過調(diào)用之前要用備份的前8字節(jié)改回來
調(diào)用之后在掛勾,如此反復(fù).
=====[?4.1.?代碼?]==============================================
??前面所講的是本進(jìn)程掛勾,我們要掛勾所有進(jìn)程,可以用全局勾子,需要單獨(dú)的一個(gè)DLL,我們可
以在DLL的DLL_PROCESS_ATTACH事件里來HOOK?API
=================================hookdll.dll==========================
.486?
.model?flat,stdcall???;參數(shù)的傳遞約定是stdcall(從右到左,恢復(fù)堆棧的工作交由被調(diào)用者)
option?casemap:none?
include?\masm32\include\windows.inc?
include?\masm32\include\kernel32.inc?
includelib?\masm32\lib\kernel32.lib?
include?\masm32\include\user32.inc?
includelib?\masm32\lib\user32.lib?
HOOKAPI?struct?
a??byte???
PMyapi?DWORD?????
d?BYTE????
e?BYTE??
HOOKAPI?ends
;子程序聲明
WriteApi?proto?:DWORD?,:DWORD,:DWORD,:DWORD
MyAPI?proto??:DWORD??,:DWORD
GetApi?proto??:DWORD,:DWORD
;已初始化數(shù)據(jù)
.data?
hInstance?dd?0
WProcess?dd?0
hacker?HOOKAPI?<>?
CommandLine?LPSTR???
Papi1?DWORD???
Myapi1?DWORD??
ApiBak1?db?10?dup(?)?
DllName1??db?"user32.dll",0??????
ApiName1??db?"ExitWindowsEx",0?
mdb?db?"下面的程序想關(guān)閉計(jì)算機(jī),要保持阻止嗎?",0
;未初始化數(shù)據(jù)
.data??
hHook?dd???
hWnd?dd???
;程序代碼段
.code?
DllEntry?proc?hInst:HINSTANCE,?reason:DWORD,?reserved1:DWORD?
???
??
?.if?reason==DLL_PROCESS_ATTACH?????;當(dāng)DLL加載時(shí)產(chǎn)生此事件
????????push?hInst?
????????pop?hInstance?
invoke?GetCommandLine???
mov?CommandLine,eax?????????????????????????????????????????;取程序命令行
;初始化
mov?hacker.a,0B8h?????;mov?eax,
;mov?hacker.d?PMyapi??;0x000000
mov?hacker.d,0FFh?????;jmp?
mov?hacker.e,?0E0h????;eax
?
invoke???GetCurrentProcess???????????????????????????????????;取進(jìn)程偽句柄
?mov?WProcess?,eax
????
invoke?GetApi,addr?DllName1,addr?ApiName1????????????????????;取API地址
??
?mov?Papi1,eax???????????????????????????????????????????????;保存API地址
invoke?ReadProcessMemory,WProcess,Papi1,addr?ApiBak1,8,NULL??;備份原API的前8字節(jié)
?mov?hacker.PMyapi,offset?MyAPI???;0x0000,這里設(shè)置替代API的函數(shù)地址
invoke?WriteApi,WProcess,Papi1,?addr?hacker?,size?HOOKAPI????;HOOK?API
.endif?
.if??reason==DLL_PROCESS_DETACH?
invoke?WriteApi,WProcess,Papi1,?addr?ApiBak1?,8???????????????;還原API
.endif?
?mov??eax,TRUE?
????ret?
DllEntry?Endp?
GetMsgProc?proc?nCode:DWORD,wParam:DWORD,lParam:DWORD?
????invoke?CallNextHookEx,hHook,nCode,wParam,lParam?
?????mov?eax,TRUE
?????
??????ret?
GetMsgProc?endp?
InstallHook?proc
???
????invoke?SetWindowsHookEx,WH_GETMESSAGE,addr?GetMsgProc,hInstance,NULL?
????mov?hHook,eax?
????ret?
InstallHook?endp?
UninstallHook?proc?
????invoke?UnhookWindowsHookEx,hHook?
???invoke?WriteApi,WProcess,Papi1,?addr?ApiBak1?,8
??ret?
UninstallHook?endp?
GetApi?proc?DllNameAddress:DWORD,ApiNameAddress:DWORD
invoke??GetModuleHandle,DllNameAddress?????;取DLL模塊句柄
???
??.if?eax==NULL
??
??invoke?LoadLibrary?,DllNameAddress????;加載DLL
??
???.endif
??
?invoke?GetProcAddress,eax,ApiNameAddress??;取API地址
???
mov?eax,eax
??
ret
GetApi?endp
;============================下面是核心部分=========================
WriteApi?proc?Process:DWORD?,Papi:DWORD,Ptype:DWORD,Psize:DWORD
LOCAL?mbi:MEMORY_BASIC_INFORMATION
LOCAL?msize:DWORD
;返回頁面虛擬信息
invoke?VirtualQueryEx,Process,?Papi,addr?mbi,SIZEOF?MEMORY_BASIC_INFORMATION
;修改為可讀寫模式
invoke?VirtualProtectEx,Process,?mbi.BaseAddress,8h,PAGE_EXECUTE_READWRITE,addr?
mbi.Protect
;開始寫內(nèi)存
invoke??WriteProcessMemory,Process,?Papi,?Ptype,Psize?,NULL
PUSH?eax
;改回只讀模式
invoke?VirtualProtectEx,Process,mbi.BaseAddress,8h,PAGE_EXECUTE_READ,addr?mbi.Protect
pop?eax
ret
WriteApi?endp
;替代的API,參數(shù)要和原來一樣
MyAPI?proc??bs:DWORD??,dwReserved:DWORD??????????????????????
invoke?MessageBox,?NULL,??CommandLine,?addr?mdb,?MB_YESNO??????;彈出信息框選擇是否阻止
.if?eax==7???????????????????????????????????????????????????;如果選擇否
?invoke?WriteApi,WProcess,Papi1,?addr?ApiBak1?,8??????????????;先還原API
?
?invoke?ExitWindowsEx,bs,dwReserved???????????????????????????;再調(diào)用API
?
?invoke?WriteApi,WProcess,Papi1,?addr?hacker?,sizeof?HOOKAPI??;調(diào)用完后再改回來
??
.endif
mov?eax,TRUE?
ret
MyAPI?endp
End?DllEntry
===============================hookdll.def=============================
LIBRARY?hookdll
EXPORTS?InstallHook
EXPORTS?UninstallHook
=====[?4.2.?分析?]==============================================
HOOKAPI?struct?
a??byte???
PMyapi?DWORD?????
d?BYTE????
e?BYTE??
HOOKAPI?ends
???為了便于理解和使用,我定義了一個(gè)結(jié)構(gòu):這個(gè)結(jié)構(gòu)有4個(gè)成員,第一個(gè)成員a,是個(gè)字節(jié)型,我用來放
0B8h(mov?eax),PMyapi一個(gè)整數(shù)型,用來放我們的替代API函數(shù)的地址(0X000),第3個(gè)和第4個(gè)成員我分別
用來放JMP和EAX(jmp?eax)那么連起來就是?mov,0X0000?;?jmp?eax??
?.if?reason==DLL_PROCESS_ATTACH?????
????????push?hInst?
????????pop?hInstance?
invoke?GetCommandLine???
mov?CommandLine,eax?????????????????????????????????????????
;初始化
mov?hacker.a,0B8h?????;mov?eax,
;mov?hacker.d?PMyapi??;0x0000
mov?hacker.d,0FFh?????;jmp?
mov?hacker.e,?0E0h????;eax
invoke???GetCurrentProcess???????????????????????????????????
?mov?WProcess?,eax
??當(dāng)DLL加載時(shí),我們先保存模塊句柄,讀取程序命令行,然后初始化HOOKAPI結(jié)構(gòu),寫入我們要寫到內(nèi)存的
指令(PMyapi以后寫入)并調(diào)用GetCurrentProcess取出進(jìn)程偽句柄方便以后寫內(nèi)存.
invoke?GetApi,addr?DllName1,addr?ApiName1????????????????????
??
?mov?Papi1,eax???????????????????????????????????????????????
invoke?ReadProcessMemory,WProcess,Papi1,addr?ApiBak1,8,NULL??
?
mov?hacker.PMyapi,offset?MyAPI???;0x0000???
invoke?WriteApi,WProcess,Papi1,?addr?hacker?,size?HOOKAPI????;HOOK?API
??接下來用子程GetApi取出要掛勾API的入口點(diǎn),并用ReadProcessMemory讀出入口點(diǎn)8字節(jié)備份之,寫入
PMyapi調(diào)用子程WriteApi改寫API的入口點(diǎn),這個(gè)子程我不準(zhǔn)備詳細(xì)說了,它非常的簡單,無非就是幾個(gè)
API的調(diào)用.它的核心就是通過WriteProcessMemory改寫內(nèi)存.
.if??reason==DLL_PROCESS_DETACH?
invoke?WriteApi,WProcess,Papi1,?addr?ApiBak1?,8???????????????
.endif?
?mov??eax,TRUE?
????ret?
???如果這個(gè)DLL被卸載了,那么那個(gè)在DLL里的替代函數(shù)(MyAPI)將是無效的,如果這個(gè)時(shí)候程序再調(diào)用這
個(gè)API,將出現(xiàn)非法操作,因此在DLL卸載前,我們必須還原API.
???總結(jié)一下,現(xiàn)在只要程序加載這個(gè)DLL,這個(gè)程序的ExitWindowsEx就會(huì)被我們勾住,接下來要怎樣才能
讓所有的程序都加載這個(gè)DLL呢?這就需要安裝全局勾子:
??InstallHook?proc
???
??????invoke?SetWindowsHookEx,WH_GETMESSAGE,addr?GetMsgProc,hInstance,NULL?
????
??????invoke?WriteApi,WProcess,Papi1,?addr?hacker?,sizeof?HOOKAPI
??????mov?hHook,eax?
?????ret?
??InstallHook?endp?
???通過SetWindowsHookEx安裝勾子,最后一個(gè)參數(shù)可以決定該鉤子是局部的還是系統(tǒng)范圍的。如果該值
為NULL,那么該鉤子將被解釋成系統(tǒng)范圍內(nèi)的,那它就可以監(jiān)控所有的進(jìn)程及它們的線程。
如果該函數(shù)調(diào)用成功的話,將在eax中返回鉤子的句柄,否則返回NULL。我們必須保存該句柄,因?yàn)楹?br />
面我們還要它來卸載鉤子,可以看出,我們創(chuàng)建的Hook類型是WH_CALLWNDPROC類型,該類型的Hook在進(jìn)程
與系統(tǒng)一通信時(shí)就會(huì)被加載到進(jìn)程空間,從而調(diào)用dll的初始化函數(shù)完成真正的Hook,值得一提的是:因
為要調(diào)用SetWindowsHookEx來安裝鉤子,我們GUI程序的這個(gè)DLL不會(huì)被
UnhookWidowHookEx卸載,也就只有一次DLL_PROCESS_ATTACH事件,因此這里再要
HOOK?API一次!
我們回頭來看看鉤子回調(diào)函數(shù):
??GetMsgProc?proc?nCode:DWORD,wParam:DWORD,lParam:DWORD?
??????invoke?CallNextHookEx,hHook,nCode,wParam,lParam?
???????mov?eax,TRUE
?????
???????ret?
??GetMsgProc?endp?
???可以看到這里只是調(diào)用CallNextHookEx將消息交給Hook鏈中下一個(gè)環(huán)節(jié)處理,因?yàn)檫@里API函數(shù)
SetWindowsHookEx的唯一作用就是讓進(jìn)程加載我們的dll。
??UninstallHook?proc?
?????invoke?UnhookWindowsHookEx,hHook?
?????invoke?WriteApi,WProcess,Papi1,?addr?ApiBak1?,8
???ret?
??UninstallHook?endp?
要卸載一個(gè)鉤子時(shí)調(diào)用UnhookWidowHookEx函數(shù),該函數(shù)僅有一個(gè)參數(shù),就是欲卸載的鉤子的句柄。鉤
子卸載后我們也要還原我們GUI程序的API.
??LIBRARY?hookdll
??EXPORTS?InstallHook
??EXPORTS?UninstallHook
???我們公開DLL里的InstallHook和UninstallHook函數(shù),方便程序調(diào)用,這樣我們只要在另外的程序中調(diào)
用InstallHook便可安裝全局勾子,勾住所有程序中的API:ExitWindowsEx,執(zhí)行我們自定的子程!
如果不需要了,可以調(diào)用UninstallHook卸載全局勾子.
???請(qǐng)注意:對(duì)于遠(yuǎn)程鉤子,鉤子函數(shù)必須放到DLL中,它們將從DLL中映射到其它的進(jìn)程空間中去。當(dāng)
WINDOWS映射DLL到其它的進(jìn)程空間中去時(shí),不會(huì)把數(shù)據(jù)段也進(jìn)行映射。簡言之,所有的進(jìn)程僅共享DLL
的代碼,至于數(shù)據(jù)段,每一個(gè)進(jìn)程都將有其單獨(dú)的拷貝。這是一個(gè)很容易被忽視的問題。您可能想當(dāng)然
的以為,在DLL中保存的值可以在所有映射該DLL的進(jìn)程之間共享。在通常情況下,由于每一個(gè)映射該
DLL的進(jìn)程都有自己的數(shù)據(jù)段,所以在大多數(shù)的情況下您的程序運(yùn)行得都不錯(cuò)。但是鉤子函數(shù)卻不是如
此。對(duì)于鉤子函數(shù)來說,要求DLL的數(shù)據(jù)段對(duì)所有的進(jìn)程也必須相同。這樣您就必須把數(shù)據(jù)段設(shè)成共享
的:
?一般來說,?目標(biāo)文件有三個(gè)段,?分別是?text/data/bss?段.
.text?段放置代碼,?是只讀且可運(yùn)行段?
.data?段放置靜態(tài)數(shù)據(jù),?這些數(shù)據(jù)會(huì)被放置入?exe?文件.?這個(gè)段是可讀寫,?但是不能運(yùn)行的.?
.bss?段放置動(dòng)態(tài)數(shù)據(jù),?這些數(shù)據(jù)不被放入?exe?文件,?在exe文件被加載入內(nèi)存后才分配的空間.
你可以通過在鏈接開關(guān)中指定段的屬性來實(shí)現(xiàn):
/SECTION:name,[E][R][W][S][D][K][L][P][X]
其中S表示共享,已初期化的段名是.data,未初始化的段名是.bss。假如您想要寫一個(gè)包含鉤子函數(shù)的
DLL,而且想使它的未初始化的數(shù)據(jù)段在所有進(jìn)程間共享,您必須這么做:
?
link?/section:.bss[S]??/DLL??/SUBSYSTEM:WINDOWS?..........
否則,您的全局勾子將不能正常工作!
=====[?5.?結(jié)束語?]================================================
????我歡迎任何人提出更多的這里沒有提到的掛鉤方法,我肯定那會(huì)有很多。同樣歡迎補(bǔ)充我介紹得不
是很詳細(xì)的方法。也可以把我懶得寫的其它方法完成,把源代碼發(fā)給我。這篇文檔的目的是演示掛鉤技
術(shù)的細(xì)節(jié),我希望我做到了。
????
============================[?End?]========================
轉(zhuǎn)載于:https://www.cnblogs.com/milantgh/p/3873191.html
總結(jié)
以上是生活随笔為你收集整理的汇编Ring 3下实现 HOOK API的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [Javascript] Avoid C
- 下一篇: 十六、Struts2文件上传与下载