请问如何更改另一个进程的的导入表呢? 如上. 解决方案 » 免费领取超大流量手机卡,每月29元包185G流量+100分钟通话, 中国电信官方发货 BOOL ObtainSeDebugPrivilege(){ TOKEN_PRIVILEGES TokenPrivileges; TOKEN_PRIVILEGES PreviousTokenPrivileges; LUID luid; HANDLE hToken; DWORD dwPreviousTokenPrivilegesSize = sizeof(TOKEN_PRIVILEGES); if(!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) { return false; } if(!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) { return false; } TokenPrivileges.PrivilegeCount = 1; TokenPrivileges.Privileges[0].Luid = luid; TokenPrivileges.Privileges[0].Attributes = 0; if(!AdjustTokenPrivileges(hToken, FALSE, &TokenPrivileges, sizeof(TOKEN_PRIVILEGES), &PreviousTokenPrivileges, &dwPreviousTokenPrivilegesSize)) { return false; } PreviousTokenPrivileges.PrivilegeCount = 1; PreviousTokenPrivileges.Privileges[0].Luid = luid; PreviousTokenPrivileges.Privileges[0].Attributes |= SE_PRIVILEGE_ENABLED; if(!AdjustTokenPrivileges(hToken, FALSE, &PreviousTokenPrivileges, dwPreviousTokenPrivilegesSize, NULL, NULL)) { return false; } //WriteLog("debug ok"); return true;}void CMainFrame::Open(){ ObtainSeDebugPrivilege(); m_hExe =OpenProcess(PROCESS_ALL_ACCESS, FALSE, m_dwExe); TRACE("0X%X\n",m_hExe); HANDLE hSnapShot; MODULEENTRY32 mdEntry; BOOL Result; hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, m_dwExe); if (hSnapShot == (HANDLE)-1) return ; mdEntry.dwSize = sizeof(MODULEENTRY32); Result = Module32First(hSnapShot, &mdEntry); if (Result != TRUE) { CloseHandle(hSnapShot); return ; } int i=0; BYTE* pBase=NULL; DWORD nActSize; do { if(strcmpi(mdEntry.szModule,m_pExe)==0) { pBase=new BYTE[mdEntry.modBaseSize]; if(!ReadProcessMemory(m_hExe,mdEntry.modBaseAddr,pBase,mdEntry.modBaseSize,&nActSize)) { AfxMessageBox("fail to read process memory!"); return; } m_mdEntry=mdEntry; //success to dump exe to a bin file. CFile file("C:\\testread.bin",CFile::modeCreate|CFile::modeWrite); file.Write(pBase,mdEntry.modBaseSize); file.Close(); } i++; } while (Module32Next(hSnapShot, &mdEntry) ); CloseHandle(hSnapShot); BYTE* pTBegin; BYTE* pCode=new BYTE[0x300]; CFile file("...RemoteThread.exe",CFile::modeRead); file.Seek(0x400,CFile::begin); file.Read(pCode,0x2A3); file.Close(); CFile fileb("...somecode.bin",CFile::modeCreate|CFile::modeWrite); fileb.Write(pCode,0x2A3); fileb.Close(); if(pTBegin=(BYTE*)VirtualAllocEx(m_hExe,NULL,0x2A3,MEM_COMMIT,PAGE_EXECUTE_READWRITE)) { if(!WriteTargetMemory(pTBegin,pCode,0x2A3)) return; DWORD dwFlags; VirtualProtectEx(m_hExe,pTBegin,0x2A3,PAGE_EXECUTE_READ,&dwFlags); HANDLE handle=CreateRemoteThread(m_hExe,NULL,NULL,( LPTHREAD_START_ROUTINE)(pTBegin+0x1dc),NULL,NULL,NULL); CloseHandle(handle); ReplaceAPI(pBase,(DWORD)(pTBegin+0x16A),234,"GetDlgItemTextW", "user32.dll"); ReplaceAPI(pBase,(DWORD)(pTBegin+0x1A3),234,"GetDlgItemTextA", "user32.dll"); }}bool ReplaceAPI(BYTE *pBase, DWORD dwNewFun,DWORD dwSize,char *pFunName, char *pTargetDllName){ IMAGE_DOS_HEADER * dosheader=(IMAGE_DOS_HEADER *)pBase; IMAGE_OPTIONAL_HEADER * opthdr =(IMAGE_OPTIONAL_HEADER *)(pBase+dosheader->e_lfanew+24); IMAGE_IMPORT_DESCRIPTOR *descriptor=(IMAGE_IMPORT_DESCRIPTOR *)((BYTE*) pBase+opthdr->DataDirectory[ IMAGE_DIRECTORY_ENTRY_IMPORT]. VirtualAddress); int i=0; while(!(descriptor[i].FirstThunk==0&&descriptor[i].OriginalFirstThunk==0)) { char* pDllName=(char*)(pBase+descriptor[i].Name); if((pDllName==NULL)||strcmpi(pDllName,pTargetDllName)==0) { PIMAGE_THUNK_DATA pthunkold, pthunknew; pthunkold=(PIMAGE_THUNK_DATA)(pBase+descriptor[i].OriginalFirstThunk); pthunknew=(PIMAGE_THUNK_DATA)(pBase+descriptor[i].FirstThunk); IMAGE_IMPORT_BY_NAME* pByName; int j=0; while(pthunkold[j].u1.AddressOfData!=0) { pByName=(PIMAGE_IMPORT_BY_NAME)(pBase+pthunkold[j].u1.Ordinal); TRACE("0x%x<<< %s \n",pthunknew[j].u1.Ordinal,pByName->Name); if(strcmp((char*)(pByName->Name),pFunName)==0) { //rewrite memory. TRACE("__________________________ok_______________________\n"); DWORD dwTargetOffset=((BYTE*)(pthunknew+j))-pBase; ASSERT(*(DWORD*)(pBase+dwTargetOffset)==pthunknew[j].u1.Ordinal); BYTE* pTargetPos=m_mdEntry.modBaseAddr+dwTargetOffset; DWORD dwNewFlags=PAGE_READWRITE; DWORD dwOldFlags; if(!VirtualProtectEx(m_hExe,pTargetPos,4,dwNewFlags,&dwOldFlags)) { TRACE("_____________________fail to protect_______________\n"); } if(!WriteProcessMemory(m_hExe,m_mdEntry.modBaseAddr+dwTargetOffset,&dwNewFun,4,&dwSize)) { TRACE("________________________fail to write memory______________________\n"); } if(!VirtualProtectEx(m_hExe,pTargetPos,4,dwOldFlags,&dwNewFlags)) { TRACE("_____________________fail to protect 2_______________\n"); } return true; } j++; } } TRACE(pDllName); TRACE("\n"); i++; }}bool WriteTargetMemory(LPVOID pTargetPos, LPBYTE pByte, DWORD dwSize){ DWORD dwNewFlags=PAGE_READWRITE; DWORD dwOldFlags; if(!VirtualProtectEx(m_hExe,pTargetPos,dwSize,dwNewFlags,&dwOldFlags)) { TRACE("_____________________fail to protect_______________\n"); return false; } if(!WriteProcessMemory(m_hExe,pTargetPos,pByte,dwSize,NULL)) { TRACE("________________________fail to write memory______________________\n");// return false; } if(!VirtualProtectEx(m_hExe,pTargetPos,dwSize,dwOldFlags,&dwNewFlags)) { TRACE("_____________________fail to protect 2_______________\n"); }} REMOTE_CODE_START equ this byte_lpLoadLibrary dd ? ;µ¼È뺯ÊýµØÖ·±í_lpGetProcAddress dd ?_lpGetModuleHandle dd ?_lpDestroyWindow dd ?_lpPostQuitMessage dd ?_lpDefWindowProc dd ?_lpLoadCursor dd ?_lpRegisterClassEx dd ?_lpCreateWindowEx dd ?_lpShowWindow dd ?_lpUpdateWindow dd ?_lpGetMessage dd ?_lpTranslateMessage dd ?_lpDispatchMessage dd ?_lpMessageBoxA dd ?_lpMessageBoxW dd ?_lpGetDlgItemTextA dd ?_lpGetDlgItemTextW dd ?_hInstance dd ?_hWinMain dd ?_szMyMsg db 'Hell world',0_szMyCap db 'Qiu first',0_COUNTER dd 5_nApiCount dd 2_szClassName db 'RemoteClass',0_szCaptionMain db 'RemoteWindow',0_szDllUser db 'User32.dll',0_szDestroyWindow db 'DestroyWindow',0_szPostQuitMessage db 'PostQuitMessage',0_szDefWindowProc db 'DefWindowProcA',0_szLoadCursor db 'LoadCursorA',0_szRegisterClassEx db 'RegisterClassExA',0_szCreateWindowEx db 'CreateWindowExA',0_szShowWindow db 'ShowWindow',0_szUpdateWindow db 'UpdateWindow',0_szGetMessage db 'GetMessageA',0_szTranslateMessage db 'TranslateMessage',0_szDispatchMessage db 'DispatchMessageA',0_szMessageBoxA db 'MessageBoxA',0_szMessageBoxW db 'MessageBoxW',0_szGetDlItemTextA db 'GetDlgItemTextA',0_szGetDlItemTextW db 'GetDlgItemTextW',0,0;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>_myGetDlgItemTextW proc hDlg,nID,lpString,nMaxCount call @F @@: pop ebx sub ebx,offset @B _invoke [ebx +_lpGetDlgItemTextW],hDlg,nID,lpString,nMaxCount lea ecx,[ebx+offset _szMyCap] _invoke [ebx+_lpMessageBoxW],NULL,ecx, lpString,NULL ret_myGetDlgItemTextW endp;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>_myGetDlgItemTextA proc hDlg,nID,lpString,nMaxCount call @F @@: pop ebx sub ebx,offset @B _invoke [ebx + _lpGetDlgItemTextA],hDlg,nID,lpString,nMaxCount lea ecx,[ebx+offset _szMyCap] _invoke [ebx+_lpMessageBoxA],NULL,ecx, lpString,NULL ret_myGetDlgItemTextA endp_RemoteThread proc uses ebx edi esi lParam local @hModule call @F @@: pop ebx sub ebx,offset @B;******************************************************************** _invoke [ebx + _lpGetModuleHandle],NULL mov [ebx + _hInstance],eax lea eax,[ebx + offset _szDllUser] _invoke [ebx + _lpGetModuleHandle],eax mov @hModule,eax lea esi,[ebx + offset _szDestroyWindow] lea edi,[ebx + offset _lpDestroyWindow] .while TRUE _invoke [ebx + _lpGetProcAddress],@hModule,esi mov [edi],eax add edi,4 @@: lodsb or al,al jnz @B .break .if ! byte ptr [esi+1] .endw;******************************************************************** .while TRUE lea edx,[ebx+offset _szMyMsg] lea ecx,[ebx+offset _szMyCap] _invoke [ebx+_lpMessageBoxA],NULL,ecx, edx,NULL dec dword ptr[ebx+_COUNTER] .break .if !dword ptr[ebx+_COUNTER] .endw ret_RemoteThread endpREMOTE_CODE_END equ this byteREMOTE_CODE_LENGTH equ offset REMOTE_CODE_END - offset REMOTE_CODE_START 函数导入表?导入是属于PE Image的,和进程没有关系知道PE结构就可以改了,修改其他进程的内存可以用WriteProcessMemory找到要修改的Image的基地址(ImageBase),可以是一个EXE,或是一个DLLhttp://www.baidu.com/baidu?wd=pe%CE%C4%BC%FE%B8%F1%CA%BD%CF%EA%BD%E2&lm=0&si=&rn=10&ie=gb2312&ct=0&cl=3&f=1 纠结的CSocket问题 看课后题,是不是参考答案这么些不好,int* itsRadius是野指针,是不是指针一定要指向一个变量呢?? debug下使用destroywindow报错 求助有关ListControl的删除 如何在单文档的view中显示一个背景图片(jpg)(急) 在98下怎么判断我的网卡是启用/禁用状态? 急! 请问那个支持VC6的能编译到P4汇编指令的补丁在哪里? 创建两个线程读写串口 一个奇怪的问题,高手请关注. 一个关于键值截取的问题: 关于最优分组的算法 怎样删除“新建”对话框?在线等
BOOL ObtainSeDebugPrivilege()
{
TOKEN_PRIVILEGES TokenPrivileges;
TOKEN_PRIVILEGES PreviousTokenPrivileges;
LUID luid;
HANDLE hToken;
DWORD dwPreviousTokenPrivilegesSize = sizeof(TOKEN_PRIVILEGES); if(!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
{
return false;
} if(!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid))
{
return false;
}
TokenPrivileges.PrivilegeCount = 1;
TokenPrivileges.Privileges[0].Luid = luid;
TokenPrivileges.Privileges[0].Attributes = 0; if(!AdjustTokenPrivileges(hToken, FALSE, &TokenPrivileges, sizeof(TOKEN_PRIVILEGES),
&PreviousTokenPrivileges, &dwPreviousTokenPrivilegesSize))
{
return false;
} PreviousTokenPrivileges.PrivilegeCount = 1;
PreviousTokenPrivileges.Privileges[0].Luid = luid;
PreviousTokenPrivileges.Privileges[0].Attributes |= SE_PRIVILEGE_ENABLED; if(!AdjustTokenPrivileges(hToken, FALSE, &PreviousTokenPrivileges,
dwPreviousTokenPrivilegesSize, NULL, NULL))
{
return false;
} //WriteLog("debug ok"); return true;
}void CMainFrame::Open()
{
ObtainSeDebugPrivilege();
m_hExe =OpenProcess(PROCESS_ALL_ACCESS, FALSE, m_dwExe);
TRACE("0X%X\n",m_hExe);
HANDLE hSnapShot;
MODULEENTRY32 mdEntry;
BOOL Result;
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, m_dwExe); if (hSnapShot == (HANDLE)-1)
return ; mdEntry.dwSize = sizeof(MODULEENTRY32); Result = Module32First(hSnapShot, &mdEntry); if (Result != TRUE)
{
CloseHandle(hSnapShot);
return ;
}
int i=0;
BYTE* pBase=NULL;
DWORD nActSize;
do
{
if(strcmpi(mdEntry.szModule,m_pExe)==0)
{
pBase=new BYTE[mdEntry.modBaseSize];
if(!ReadProcessMemory(m_hExe,mdEntry.modBaseAddr,pBase,mdEntry.modBaseSize,&nActSize))
{
AfxMessageBox("fail to read process memory!");
return;
}
m_mdEntry=mdEntry;
//success to dump exe to a bin file.
CFile file("C:\\testread.bin",CFile::modeCreate|CFile::modeWrite);
file.Write(pBase,mdEntry.modBaseSize);
file.Close();
}
i++; } while (Module32Next(hSnapShot, &mdEntry) ); CloseHandle(hSnapShot);
BYTE* pTBegin;
BYTE* pCode=new BYTE[0x300];
CFile file("...RemoteThread.exe",CFile::modeRead);
file.Seek(0x400,CFile::begin);
file.Read(pCode,0x2A3);
file.Close();
CFile fileb("...somecode.bin",CFile::modeCreate|CFile::modeWrite);
fileb.Write(pCode,0x2A3);
fileb.Close();
if(pTBegin=(BYTE*)VirtualAllocEx(m_hExe,NULL,0x2A3,MEM_COMMIT,PAGE_EXECUTE_READWRITE))
{
if(!WriteTargetMemory(pTBegin,pCode,0x2A3))
return;
DWORD dwFlags;
VirtualProtectEx(m_hExe,pTBegin,0x2A3,PAGE_EXECUTE_READ,&dwFlags);
HANDLE handle=CreateRemoteThread(m_hExe,NULL,NULL,( LPTHREAD_START_ROUTINE)(pTBegin+0x1dc),NULL,NULL,NULL);
CloseHandle(handle);
ReplaceAPI(pBase,(DWORD)(pTBegin+0x16A),234,"GetDlgItemTextW", "user32.dll");
ReplaceAPI(pBase,(DWORD)(pTBegin+0x1A3),234,"GetDlgItemTextA", "user32.dll");
}
}
bool ReplaceAPI(BYTE *pBase, DWORD dwNewFun,DWORD dwSize,char *pFunName, char *pTargetDllName)
{
IMAGE_DOS_HEADER * dosheader=(IMAGE_DOS_HEADER *)pBase;
IMAGE_OPTIONAL_HEADER * opthdr =(IMAGE_OPTIONAL_HEADER *)(pBase+dosheader->e_lfanew+24);
IMAGE_IMPORT_DESCRIPTOR *descriptor=(IMAGE_IMPORT_DESCRIPTOR *)((BYTE*) pBase+opthdr->DataDirectory[ IMAGE_DIRECTORY_ENTRY_IMPORT]. VirtualAddress);
int i=0;
while(!(descriptor[i].FirstThunk==0&&descriptor[i].OriginalFirstThunk==0))
{
char* pDllName=(char*)(pBase+descriptor[i].Name);
if((pDllName==NULL)||strcmpi(pDllName,pTargetDllName)==0)
{
PIMAGE_THUNK_DATA pthunkold, pthunknew;
pthunkold=(PIMAGE_THUNK_DATA)(pBase+descriptor[i].OriginalFirstThunk);
pthunknew=(PIMAGE_THUNK_DATA)(pBase+descriptor[i].FirstThunk);
IMAGE_IMPORT_BY_NAME* pByName;
int j=0;
while(pthunkold[j].u1.AddressOfData!=0)
{
pByName=(PIMAGE_IMPORT_BY_NAME)(pBase+pthunkold[j].u1.Ordinal);
TRACE("0x%x<<< %s \n",pthunknew[j].u1.Ordinal,pByName->Name);
if(strcmp((char*)(pByName->Name),pFunName)==0)
{
//rewrite memory.
TRACE("__________________________ok_______________________\n");
DWORD dwTargetOffset=((BYTE*)(pthunknew+j))-pBase;
ASSERT(*(DWORD*)(pBase+dwTargetOffset)==pthunknew[j].u1.Ordinal);
BYTE* pTargetPos=m_mdEntry.modBaseAddr+dwTargetOffset;
DWORD dwNewFlags=PAGE_READWRITE;
DWORD dwOldFlags;
if(!VirtualProtectEx(m_hExe,pTargetPos,4,dwNewFlags,&dwOldFlags))
{
TRACE("_____________________fail to protect_______________\n");
} if(!WriteProcessMemory(m_hExe,m_mdEntry.modBaseAddr+dwTargetOffset,&dwNewFun,4,&dwSize))
{
TRACE("________________________fail to write memory______________________\n");
}
if(!VirtualProtectEx(m_hExe,pTargetPos,4,dwOldFlags,&dwNewFlags))
{
TRACE("_____________________fail to protect 2_______________\n");
} return true;
}
j++;
}
}
TRACE(pDllName);
TRACE("\n");
i++;
}
}bool WriteTargetMemory(LPVOID pTargetPos, LPBYTE pByte, DWORD dwSize)
{
DWORD dwNewFlags=PAGE_READWRITE;
DWORD dwOldFlags;
if(!VirtualProtectEx(m_hExe,pTargetPos,dwSize,dwNewFlags,&dwOldFlags))
{
TRACE("_____________________fail to protect_______________\n");
return false;
} if(!WriteProcessMemory(m_hExe,pTargetPos,pByte,dwSize,NULL))
{
TRACE("________________________fail to write memory______________________\n");
// return false;
}
if(!VirtualProtectEx(m_hExe,pTargetPos,dwSize,dwOldFlags,&dwNewFlags))
{
TRACE("_____________________fail to protect 2_______________\n");
}
}
_lpLoadLibrary dd ? ;µ¼È뺯ÊýµØÖ·±í
_lpGetProcAddress dd ?
_lpGetModuleHandle dd ?_lpDestroyWindow dd ?
_lpPostQuitMessage dd ?
_lpDefWindowProc dd ?
_lpLoadCursor dd ?
_lpRegisterClassEx dd ?
_lpCreateWindowEx dd ?
_lpShowWindow dd ?
_lpUpdateWindow dd ?
_lpGetMessage dd ?
_lpTranslateMessage dd ?
_lpDispatchMessage dd ?
_lpMessageBoxA dd ?
_lpMessageBoxW dd ?
_lpGetDlgItemTextA dd ?
_lpGetDlgItemTextW dd ?_hInstance dd ?
_hWinMain dd ?
_szMyMsg db 'Hell world',0
_szMyCap db 'Qiu first',0
_COUNTER dd 5
_nApiCount dd 2_szClassName db 'RemoteClass',0
_szCaptionMain db 'RemoteWindow',0
_szDllUser db 'User32.dll',0
_szDestroyWindow db 'DestroyWindow',0
_szPostQuitMessage db 'PostQuitMessage',0
_szDefWindowProc db 'DefWindowProcA',0
_szLoadCursor db 'LoadCursorA',0
_szRegisterClassEx db 'RegisterClassExA',0
_szCreateWindowEx db 'CreateWindowExA',0
_szShowWindow db 'ShowWindow',0
_szUpdateWindow db 'UpdateWindow',0
_szGetMessage db 'GetMessageA',0
_szTranslateMessage db 'TranslateMessage',0
_szDispatchMessage db 'DispatchMessageA',0
_szMessageBoxA db 'MessageBoxA',0
_szMessageBoxW db 'MessageBoxW',0
_szGetDlItemTextA db 'GetDlgItemTextA',0
_szGetDlItemTextW db 'GetDlgItemTextW',0,0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_myGetDlgItemTextW proc hDlg,nID,lpString,nMaxCount
call @F
@@:
pop ebx
sub ebx,offset @B
_invoke [ebx +_lpGetDlgItemTextW],hDlg,nID,lpString,nMaxCount
lea ecx,[ebx+offset _szMyCap]
_invoke [ebx+_lpMessageBoxW],NULL,ecx, lpString,NULL
ret_myGetDlgItemTextW endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_myGetDlgItemTextA proc hDlg,nID,lpString,nMaxCount
call @F
@@:
pop ebx
sub ebx,offset @B
_invoke [ebx + _lpGetDlgItemTextA],hDlg,nID,lpString,nMaxCount
lea ecx,[ebx+offset _szMyCap]
_invoke [ebx+_lpMessageBoxA],NULL,ecx, lpString,NULL
ret_myGetDlgItemTextA endp
_RemoteThread proc uses ebx edi esi lParam
local @hModule call @F
@@:
pop ebx
sub ebx,offset @B
;********************************************************************
_invoke [ebx + _lpGetModuleHandle],NULL
mov [ebx + _hInstance],eax
lea eax,[ebx + offset _szDllUser]
_invoke [ebx + _lpGetModuleHandle],eax
mov @hModule,eax
lea esi,[ebx + offset _szDestroyWindow]
lea edi,[ebx + offset _lpDestroyWindow]
.while TRUE
_invoke [ebx + _lpGetProcAddress],@hModule,esi
mov [edi],eax
add edi,4
@@:
lodsb
or al,al
jnz @B
.break .if ! byte ptr [esi+1]
.endw
;********************************************************************
.while TRUE
lea edx,[ebx+offset _szMyMsg]
lea ecx,[ebx+offset _szMyCap]
_invoke [ebx+_lpMessageBoxA],NULL,ecx, edx,NULL
dec dword ptr[ebx+_COUNTER]
.break .if !dword ptr[ebx+_COUNTER]
.endw
ret_RemoteThread endp
REMOTE_CODE_END equ this byte
REMOTE_CODE_LENGTH equ offset REMOTE_CODE_END - offset REMOTE_CODE_START
导入是属于PE Image的,和进程没有关系知道PE结构就可以改了,修改其他进程的内存可以用WriteProcessMemory
找到要修改的Image的基地址(ImageBase),可以是一个EXE,或是一个DLL
http://www.baidu.com/baidu?wd=pe%CE%C4%BC%FE%B8%F1%CA%BD%CF%EA%BD%E2&lm=0&si=&rn=10&ie=gb2312&ct=0&cl=3&f=1